Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions workmanager_android/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,5 +27,24 @@ dependencies:

For detailed setup instructions, usage examples, and API documentation, please refer to the main [`workmanager`][workmanager] package documentation.

## Foreground service permissions

Expedited work runs as a `shortService` foreground service, so the plugin
always declares `FOREGROUND_SERVICE` and `FOREGROUND_SERVICE_SHORT_SERVICE`.

`FOREGROUND_SERVICE_DATA_SYNC` (the `dataSync` foreground service type, for
long-running workers) is **opt-in**: it is a Play Console "special type" that
requires a declaration form and a demonstration video even when unused, so it
is only added to your merged manifest when you enable it in your app's
`gradle.properties` (or pass it on the command line):

```properties
workmanager.enableDataSyncForegroundService=true
```

Only set this if your app actually runs long-running workers with the
`dataSync` foreground service type — see the `ForegroundServiceConfig` API
in the main package. See [issue #725](https://github.com/fluttercommunity/flutter_workmanager/issues/725).

[workmanager]: https://pub.dartlang.org/packages/workmanager
[federated_plugin_docs]: https://flutter.dev/go/federated-plugins
14 changes: 14 additions & 0 deletions workmanager_android/android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,20 @@ android {
sourceSets {
main.java.srcDirs += 'src/main/kotlin'
test.java.srcDirs += 'src/test/kotlin'
// FOREGROUND_SERVICE_DATA_SYNC is a Play Console "special type" that
// requires a declaration + demonstration video even when unused, so it
// is opt-in (see AndroidManifest.xml). Apps that run long-running
// workers with the `dataSync` foreground service type must set
// `workmanager.enableDataSyncForegroundService=true` in their
// gradle.properties (or via -P). See #725.
def wmDataSyncEnabled =
project.findProperty('workmanager.enableDataSyncForegroundService')
?.toString() == 'true'
if (wmDataSyncEnabled) {
// Swap in the opt-in manifest (adds the DATA_SYNC permission and
// the `dataSync` service type). See #725.
main.manifest.srcFile 'src/main/AndroidManifest.dataSync.xml'
}
}
defaultConfig {
compileSdk 35
Expand Down
24 changes: 24 additions & 0 deletions workmanager_android/android/src/main/AndroidManifest.dataSync.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
<!-- Foreground service support for long-running workers. -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<!-- Android 14+ requires a type-specific permission for each FGS type. -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SHORT_SERVICE"/>

<!--
Opt-in variant of AndroidManifest.xml, swapped in when the app sets
`workmanager.enableDataSyncForegroundService=true` (see build.gradle).
Keep this file in sync with AndroidManifest.xml — the only differences
are the FOREGROUND_SERVICE_DATA_SYNC permission and the `dataSync`
service type. See #725.
-->
<application>
<service
android:name="androidx.work.impl.foreground.SystemForegroundService"
android:foregroundServiceType="dataSync|shortService"
tools:node="merge"
tools:targetApi="34"/>
</application>
</manifest>
27 changes: 17 additions & 10 deletions workmanager_android/android/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,28 @@
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
<!-- Foreground service support for long-running workers. -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<!-- Android 14+ requires a type-specific permission for each FGS type. -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SHORT_SERVICE"/>

<!--
Android 14+ (target SDK 34+) requires the foreground service type to be
declared in the manifest for the service that is started. WorkManager
runs long-running workers in androidx.work.impl.foreground.SystemForegroundService,
which declares no type of its own, so the plugin merges the supported
types (dataSync, shortService) onto it.
Android 14+ (target SDK 34+) requires a type-specific permission for
each FGS type the app starts.

FOREGROUND_SERVICE_SHORT_SERVICE is always declared — expedited work
runs as a shortService FGS (see #715). It is not a Play Console
"special type" and needs no declaration/video.

FOREGROUND_SERVICE_DATA_SYNC is a Play Console special type that
requires a declaration form and a demonstration video even when the
app never starts a data-sync foreground service. It is therefore
NOT declared here: apps that run long-running workers with the
`dataSync` foreground service type must set
`workmanager.enableDataSyncForegroundService=true` in gradle.properties
(or via -P), which swaps in the opt-in manifest
(src/main/AndroidManifest.dataSync.xml). See #725.
-->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SHORT_SERVICE"/>
<application>
<service
android:name="androidx.work.impl.foreground.SystemForegroundService"
android:foregroundServiceType="dataSync|shortService"
android:foregroundServiceType="shortService"
tools:node="merge"
tools:targetApi="34"/>
</application>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package dev.fluttercommunity.workmanager
import android.app.NotificationChannel
import android.app.NotificationManager
import android.content.Context
import android.content.pm.PackageManager
import android.content.pm.ServiceInfo
import android.os.Build
import androidx.core.app.NotificationCompat
Expand Down Expand Up @@ -46,7 +47,7 @@ fun createForegroundInfo(
ForegroundInfo(
config.notificationId?.toInt() ?: DEFAULT_NOTIFICATION_ID,
notification,
resolveForegroundServiceType(config),
resolveForegroundServiceType(context, config),
)
} else {
ForegroundInfo(config.notificationId?.toInt() ?: DEFAULT_NOTIFICATION_ID, notification)
Expand Down Expand Up @@ -75,12 +76,49 @@ private fun createNotificationChannel(
.createNotificationChannel(channel)
}

private fun resolveForegroundServiceType(config: ForegroundServiceConfig): Int =
/**
* Fails loudly when a foreground-service feature is used without the manifest
* declaration it needs (see issue #725). Normal permissions are granted at
* install when declared, so a missing declaration surfaces as DENIED here.
*/
internal fun requireForegroundServicePermission(
context: Context,
permission: String,
featureDescription: String,
fixHint: String,
) {
val granted =
context.packageManager.checkPermission(permission, context.packageName) ==
PackageManager.PERMISSION_GRANTED
if (!granted) {
throw IllegalStateException(
"workmanager_android: $featureDescription requires the '$permission' permission " +
"in the merged manifest, but it is missing. $fixHint",
)
}
}

private fun resolveForegroundServiceType(
context: Context,
config: ForegroundServiceConfig,
): Int =
when (config.foregroundServiceType) {
ForegroundServiceType.SHORT_SERVICE ->
ServiceInfo.FOREGROUND_SERVICE_TYPE_SHORT_SERVICE
else ->
else -> {
// dataSync is opt-in (see README, issue #725): the manifest only
// declares FOREGROUND_SERVICE_DATA_SYNC when the app sets
// workmanager.enableDataSyncForegroundService=true. Fail loudly
// here instead of a cryptic SecurityException on Android 14+.
requireForegroundServicePermission(
context,
android.Manifest.permission.FOREGROUND_SERVICE_DATA_SYNC,
"foregroundServiceType=dataSync",
"Add 'workmanager.enableDataSyncForegroundService=true' to your " +
"gradle.properties (see the workmanager_android README, issue #725).",
)
ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,21 @@ class WorkManagerWrapper(

fun enqueueOneOffTask(request: dev.fluttercommunity.workmanager.pigeon.OneOffTaskRequest) {
try {
if (request.expedited == true) {
// Expedited work runs as a shortService FGS on Android 14+.
// The plugin declares the permission by default; if the app
// stripped it from the merged manifest, fail loudly now.
requireForegroundServicePermission(
context,
// No Manifest.permission constant exists for this one in
// the SDK stubs; the manifest uses the literal string.
"android.permission.FOREGROUND_SERVICE_SHORT_SERVICE",
"expedited work (setExpedited)",
"FOREGROUND_SERVICE_SHORT_SERVICE is declared by the plugin by default; " +
"keep it if you use expedited work (see the workmanager_android " +
"README, issue #725).",
)
}
val oneOffTaskRequest = createOneOffWorkRequest(request)
workManager.enqueueUniqueWork(
request.uniqueName,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package dev.fluttercommunity.workmanager

import android.content.pm.ServiceInfo
import dev.fluttercommunity.workmanager.pigeon.ForegroundServiceConfig
import dev.fluttercommunity.workmanager.pigeon.ForegroundServiceType
import org.junit.Assert.assertEquals
import org.junit.Assert.assertThrows
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.RuntimeEnvironment
import org.robolectric.annotation.Config

@RunWith(RobolectricTestRunner::class)
@Config(sdk = [34])
class ForegroundServicePermissionTest {
@Test
fun `dataSync foreground service throws when the permission is not declared`() {
// The default plugin manifest does not declare
// FOREGROUND_SERVICE_DATA_SYNC (it is opt-in, see issue #725), so
// checkPermission reports DENIED and the guard fails loudly.
val config =
ForegroundServiceConfig(
notificationTitle = "Syncing",
notificationText = "Uploading 42 files",
foregroundServiceType = ForegroundServiceType.DATA_SYNC,
)

assertThrows(IllegalStateException::class.java) {
createForegroundInfo(RuntimeEnvironment.getApplication(), config)
}
}

@Test
fun `shortService foreground service builds without the dataSync permission`() {
// FOREGROUND_SERVICE_SHORT_SERVICE is always declared by the plugin,
// so the shortService path must not require the opt-in permission.
val config =
ForegroundServiceConfig(
notificationTitle = "Task",
notificationText = "Working",
foregroundServiceType = ForegroundServiceType.SHORT_SERVICE,
)

val info = createForegroundInfo(RuntimeEnvironment.getApplication(), config)
assertEquals(
ServiceInfo.FOREGROUND_SERVICE_TYPE_SHORT_SERVICE.toLong(),
info.foregroundServiceType.toLong(),
)
}
}
Loading