From d96c64bc6e6c6b7579308f9af971dd687efd9123 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20A=C3=9Fmann?= Date: Sun, 14 Nov 2021 23:13:27 +0100 Subject: [PATCH] Add WifiLock ability (#47) * Add WifiLock ability and improve IsolateHolderService * Change example to specifically enable the WiFi lock --- CHANGELOG.md | 4 + README.md | 14 ++- .../FlutterBackgroundPlugin.kt | 57 ++++++----- .../IsolateHolderService.kt | 95 ++++++++++++------- example/lib/home_page.dart | 1 + example/pubspec.lock | 10 +- lib/src/android_config.dart | 15 ++- lib/src/flutter_background.dart | 1 + pubspec.lock | 8 +- pubspec.yaml | 2 +- 10 files changed, 132 insertions(+), 75 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fb6f3d9..22cd687 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 1.1.0 + +* Add capability to enable Android Wifi Lock in the initialize function. + ## 1.0.2+2 * Fix crash when targeting Android S+ due to a missing immutable flag for pending intents diff --git a/README.md b/README.md index 92f3004..449ed75 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ PRs for iOS are very welcome, although I am not sure if a similiar effect can be ## Getting started -To use this plugin, add `flutter_background` as a [dependency in your `pubspec.yaml` file](https://flutter.dev/docs/development/packages-and-plugins/using-packages). +To use this plugin, add `flutter_background` as a [dependency in your `pubspec.yaml` file](https://pub.dev/packages/flutter_background/install). ### Android @@ -62,9 +62,15 @@ This ensures all permissions are granted and requests them if necessary. It also foreground notification. The configuration above results in the foreground notification shown below when running `FlutterBackground.enableBackgroundExecution()`. -![The foreground notification created by the code above.](./images/notification.png "Foreground notification created by the code above.") +![The foreground notification created by the code above.](./images/notification.png "The foreground notification created by the code above.") + +The arguments are: +- `notificationTitle`: The title used for the foreground service notification. +- `notificationText`: The body used for the foreground service notification. +- `notificationImportance`: The importance of the foreground service notification. +- `notificationIcon`: The icon used for the foreground service notification shown in the top left corner. This must be a drawable Android Resource (see [here](https://developer.android.com/reference/android/app/Notification.Builder#setSmallIcon(int,%20int)) for more). E. g. if the icon with name "background_icon" is in the "drawable" resource folder, it should be of value `AndroidResource(name: 'background_icon', defType: 'drawable'). +- `enableWifiLock`: Indicates whether or not a WifiLock is acquired when background execution is started. This allows the application to keep the Wi-Fi radio awake, even when the user has not used the device in a while (e.g. for background network communications). -The notification icon is for the small icon displayed in the top left of a notification and must be a drawable Android Resource (see [here](https://developer.android.com/reference/android/app/Notification.Builder#setSmallIcon(int,%20int)) for more). In this example, `background_icon` is a drawable resource in the `drawable` folders (see the example app). For more information check out the [Android documentation for creating notification icons](https://developer.android.com/studio/write/image-asset-studio#create-notification) for more information how to create and store an icon. @@ -100,7 +106,7 @@ you can stop the background execution of the app. You must call `FlutterBackgrou To check whether background execution is currently enabled, use ```dart -bool enabled = FlutterBackground.isBackgroundExecutionEnabled +bool enabled = FlutterBackground.isBackgroundExecutionEnabled; ``` ## Example diff --git a/android/src/main/kotlin/de/julianassmann/flutter_background/FlutterBackgroundPlugin.kt b/android/src/main/kotlin/de/julianassmann/flutter_background/FlutterBackgroundPlugin.kt index 5ca05d2..1923007 100644 --- a/android/src/main/kotlin/de/julianassmann/flutter_background/FlutterBackgroundPlugin.kt +++ b/android/src/main/kotlin/de/julianassmann/flutter_background/FlutterBackgroundPlugin.kt @@ -1,11 +1,9 @@ package de.julianassmann.flutter_background -import android.annotation.TargetApi import android.app.Activity import android.content.Context import android.content.Intent -import android.os.Build -import androidx.annotation.NonNull; +import androidx.annotation.NonNull import androidx.core.app.NotificationCompat import io.flutter.embedding.engine.plugins.FlutterPlugin @@ -32,44 +30,52 @@ class FlutterBackgroundPlugin: FlutterPlugin, MethodCallHandler, ActivityAware { channel.setMethodCallHandler(FlutterBackgroundPlugin()) } - @JvmStatic - var notificationTitle: String? = "flutter_background foreground service" @JvmStatic val NOTIFICATION_TITLE_KEY = "android.notificationTitle" @JvmStatic - var notificationText: String? = "Keeps the flutter app running in the background" + val NOTIFICATION_ICON_NAME_KEY = "android.notificationIconName" @JvmStatic - val NOTIFICATION_TEXT_KEY = "android.notificationText" + val NOTIFICATION_ICON_DEF_TYPE_KEY = "android.notificationIconDefType" @JvmStatic - var notificationImportance: Int? = NotificationCompat.PRIORITY_DEFAULT + val NOTIFICATION_TEXT_KEY = "android.notificationText" @JvmStatic val NOTIFICATION_IMPORTANCE_KEY = "android.notificationImportance" @JvmStatic - var notificationIconName: String? = "ic_launcher" + val ENABLE_WIFI_LOCK_KEY = "android.enableWifiLock" + + @JvmStatic + var notificationTitle: String = "flutter_background foreground service" @JvmStatic - val NOTIFICATION_ICON_NAME_KEY = "android.notificationIconName" + var notificationText: String = "Keeps the flutter app running in the background" @JvmStatic - var notificationIconDefType: String? = "mipmap" + var notificationImportance: Int = NotificationCompat.PRIORITY_DEFAULT @JvmStatic - val NOTIFICATION_ICON_DEF_TYPE_KEY = "android.notificationIconDefType" + var notificationIconName: String = "ic_launcher" + @JvmStatic + var notificationIconDefType: String = "mipmap" + @JvmStatic + var enableWifiLock: Boolean = true + fun loadNotificationConfiguration(context: Context?) { - var sharedPref = context?.getSharedPreferences(context?.packageName + "_preferences", Context.MODE_PRIVATE) - notificationTitle = sharedPref?.getString(NOTIFICATION_TITLE_KEY, notificationTitle) - notificationText = sharedPref?.getString(NOTIFICATION_TEXT_KEY, notificationText) - notificationImportance = sharedPref?.getInt(NOTIFICATION_IMPORTANCE_KEY, notificationImportance!!) - notificationIconName = sharedPref?.getString(NOTIFICATION_ICON_NAME_KEY, notificationIconName) - notificationIconDefType = sharedPref?.getString(NOTIFICATION_ICON_DEF_TYPE_KEY, notificationIconDefType) + val sharedPref = context?.getSharedPreferences(context.packageName + "_preferences", Context.MODE_PRIVATE) + notificationTitle = sharedPref?.getString(NOTIFICATION_TITLE_KEY, notificationTitle) ?: notificationTitle + notificationText = sharedPref?.getString(NOTIFICATION_TEXT_KEY, notificationText) ?: notificationText + notificationImportance = sharedPref?.getInt(NOTIFICATION_IMPORTANCE_KEY, notificationImportance) ?: notificationImportance + notificationIconName = sharedPref?.getString(NOTIFICATION_ICON_NAME_KEY, notificationIconName) ?: notificationIconName + notificationIconDefType = sharedPref?.getString(NOTIFICATION_ICON_DEF_TYPE_KEY, notificationIconDefType) ?: notificationIconDefType + enableWifiLock = sharedPref?.getBoolean(ENABLE_WIFI_LOCK_KEY, false) ?: false } fun saveNotificationConfiguration(context: Context?) { - var sharedPref = context?.getSharedPreferences(context?.packageName + "_preferences", Context.MODE_PRIVATE) + val sharedPref = context?.getSharedPreferences(context.packageName + "_preferences", Context.MODE_PRIVATE) with (sharedPref?.edit()) { this?.putString(NOTIFICATION_TITLE_KEY, notificationTitle) this?.putString(NOTIFICATION_TEXT_KEY, notificationText) - this?.putInt(NOTIFICATION_IMPORTANCE_KEY, notificationImportance!!) + this?.putInt(NOTIFICATION_IMPORTANCE_KEY, notificationImportance) this?.putString(NOTIFICATION_ICON_NAME_KEY, notificationIconName) this?.putString(NOTIFICATION_ICON_DEF_TYPE_KEY, notificationIconDefType) + this?.putBoolean(ENABLE_WIFI_LOCK_KEY, enableWifiLock) this?.apply() } } @@ -77,9 +83,9 @@ class FlutterBackgroundPlugin: FlutterPlugin, MethodCallHandler, ActivityAware { private fun isValidResource(context: Context, name: String, defType: String, result: Result, errorCode: String): Boolean { - val resourceId = context.getResources().getIdentifier(name, defType, context.getPackageName()) + val resourceId = context.resources.getIdentifier(name, defType, context.packageName) if (resourceId == 0) { - result.error("ResourceError", "The resource $defType/$name could not be found. Please make sure it has been added as a resource to your Android head project.", null) + result.error("ResourceError", "The resource $defType/$name could not be found. Please make sure it has been added as a resource to your Android head project.", errorCode) return false } return true @@ -95,7 +101,7 @@ class FlutterBackgroundPlugin: FlutterPlugin, MethodCallHandler, ActivityAware { result.success("Android ${android.os.Build.VERSION.RELEASE}") } "hasPermissions" -> { - var hasPermissions = permissionHandler!!.isIgnoringBatteryOptimizations() + val hasPermissions = permissionHandler!!.isIgnoringBatteryOptimizations() && permissionHandler!!.isWakeLockPermissionGranted() result.success(hasPermissions) } @@ -105,13 +111,15 @@ class FlutterBackgroundPlugin: FlutterPlugin, MethodCallHandler, ActivityAware { val importance = call.argument(NOTIFICATION_IMPORTANCE_KEY) val iconName = call.argument(NOTIFICATION_ICON_NAME_KEY) val iconDefType = call.argument(NOTIFICATION_ICON_DEF_TYPE_KEY) + val wifiLock = call.argument(ENABLE_WIFI_LOCK_KEY) // Set static values so the IsolateHolderService can use them later on to configure the notification notificationImportance = importance ?: notificationImportance notificationTitle = title ?: notificationTitle - notificationText = text ?: text + notificationText = text ?: notificationText notificationIconName = iconName ?: notificationIconName notificationIconDefType = iconDefType ?: notificationIconDefType + enableWifiLock = wifiLock ?: enableWifiLock saveNotificationConfiguration(context) @@ -144,6 +152,7 @@ class FlutterBackgroundPlugin: FlutterPlugin, MethodCallHandler, ActivityAware { result.error("PermissionError", "The battery optimizations are not turned off.", "") } else { val intent = Intent(context, IsolateHolderService::class.java) + intent.action = IsolateHolderService.ACTION_START if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) { context!!.startForegroundService(intent) } else { diff --git a/android/src/main/kotlin/de/julianassmann/flutter_background/IsolateHolderService.kt b/android/src/main/kotlin/de/julianassmann/flutter_background/IsolateHolderService.kt index f45fa10..3f90384 100644 --- a/android/src/main/kotlin/de/julianassmann/flutter_background/IsolateHolderService.kt +++ b/android/src/main/kotlin/de/julianassmann/flutter_background/IsolateHolderService.kt @@ -7,6 +7,7 @@ import android.app.PendingIntent import android.app.Service import android.content.Context import android.content.Intent +import android.net.wifi.WifiManager import android.os.Build import android.os.IBinder import android.os.PowerManager @@ -21,25 +22,59 @@ class IsolateHolderService : Service() { @JvmStatic val WAKELOCK_TAG = "FlutterBackgroundPlugin:Wakelock" @JvmStatic + val WIFILOCK_TAG = "FlutterBackgroundPlugin:WifiLock" + @JvmStatic val CHANNEL_ID = "flutter_background" @JvmStatic private val TAG = "IsolateHolderService" - @JvmStatic - val EXTRA_NOTIFICATION_IMPORTANCE = "de.julianassmann.flutter_background:Importance" - @JvmStatic - val EXTRA_NOTIFICATION_TITLE = "de.julianassmann.flutter_background:Title" - @JvmStatic - val EXTRA_NOTIFICATION_TEXT = "de.julianassmann.flutter_background:Text" } + private var wakeLock: PowerManager.WakeLock? = null + private var wifiLock: WifiManager.WifiLock? = null + override fun onBind(intent: Intent) : IBinder? { return null } - @SuppressLint("WakelockTimeout") override fun onCreate() { FlutterBackgroundPlugin.loadNotificationConfiguration(applicationContext) + } + + override fun onDestroy() { + cleanupService() + super.onDestroy() + } + + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int) : Int { + if (intent?.action == ACTION_SHUTDOWN) { + cleanupService() + stopSelf() + } else if (intent?.action == ACTION_START) { + startService() + } + return START_STICKY + } + + private fun cleanupService() { + wakeLock?.apply { + if (isHeld) { + release() + } + } + + if (FlutterBackgroundPlugin.enableWifiLock) { + wifiLock?.apply { + if (isHeld) { + release() + } + } + } + + stopForeground(true) + } + @SuppressLint("WakelockTimeout") + private fun startService() { val pm = applicationContext.packageManager val notificationIntent = pm.getLaunchIntentForPackage(applicationContext.packageName) @@ -55,54 +90,46 @@ class IsolateHolderService : Service() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { val channel = NotificationChannel( - CHANNEL_ID, - FlutterBackgroundPlugin.notificationTitle, - FlutterBackgroundPlugin.notificationImportance ?: NotificationCompat.PRIORITY_DEFAULT).apply { + CHANNEL_ID, + FlutterBackgroundPlugin.notificationTitle, + FlutterBackgroundPlugin.notificationImportance).apply { description = FlutterBackgroundPlugin.notificationText } // Register the channel with the system val notificationManager: NotificationManager = - getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager notificationManager.createNotificationChannel(channel) } val imageId = resources.getIdentifier(FlutterBackgroundPlugin.notificationIconName, FlutterBackgroundPlugin.notificationIconDefType, packageName) val notification = NotificationCompat.Builder(this, CHANNEL_ID) - .setContentTitle(FlutterBackgroundPlugin.notificationTitle) - .setContentText(FlutterBackgroundPlugin.notificationText) - .setSmallIcon(imageId) - .setContentIntent(pendingIntent) - .setPriority(FlutterBackgroundPlugin.notificationImportance ?: NotificationCompat.PRIORITY_DEFAULT) - .build() + .setContentTitle(FlutterBackgroundPlugin.notificationTitle) + .setContentText(FlutterBackgroundPlugin.notificationText) + .setSmallIcon(imageId) + .setContentIntent(pendingIntent) + .setPriority(FlutterBackgroundPlugin.notificationImportance) + .build() (getSystemService(Context.POWER_SERVICE) as PowerManager).run { - newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, WAKELOCK_TAG).apply { + wakeLock = newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, WAKELOCK_TAG).apply { setReferenceCounted(false) acquire() } } - startForeground(1, notification) - super.onCreate() - } - - override fun onStartCommand(intent: Intent?, flags: Int, startId: Int) : Int { - if (intent?.action == ACTION_SHUTDOWN) { - (getSystemService(Context.POWER_SERVICE) as PowerManager).run { - newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, WAKELOCK_TAG).apply { - if (isHeld) { - release() - } - } + (applicationContext.getSystemService(Context.WIFI_SERVICE) as WifiManager).run { + wifiLock = createWifiLock(WifiManager.WIFI_MODE_FULL, WIFILOCK_TAG).apply { + setReferenceCounted(false) + acquire() } - stopForeground(true) - stopSelf() } - return START_STICKY - } + + startForeground(1, notification) + } override fun onTaskRemoved(rootIntent: Intent) { super.onTaskRemoved(rootIntent) + cleanupService() stopSelf() } } diff --git a/example/lib/home_page.dart b/example/lib/home_page.dart index 4d9cda0..e78585a 100644 --- a/example/lib/home_page.dart +++ b/example/lib/home_page.dart @@ -179,6 +179,7 @@ class _HomePageState extends State { 'Background notification for keeping the example app running in the background', notificationIcon: AndroidResource(name: 'background_icon'), notificationImportance: AndroidNotificationImportance.Default, + enableWifiLock: true, ); var hasPermissions = await FlutterBackground.hasPermissions; diff --git a/example/pubspec.lock b/example/pubspec.lock index 6a0cbb2..baee101 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -7,7 +7,7 @@ packages: name: async url: "https://pub.dartlang.org" source: hosted - version: "2.6.1" + version: "2.8.1" bloc: dependency: transitive description: @@ -42,7 +42,7 @@ packages: name: charcode url: "https://pub.dartlang.org" source: hosted - version: "1.2.0" + version: "1.3.1" clock: dependency: transitive description: @@ -82,7 +82,7 @@ packages: path: ".." relative: true source: path - version: "1.0.2+1" + version: "1.1.0" flutter_bloc: dependency: "direct main" description: @@ -122,7 +122,7 @@ packages: name: meta url: "https://pub.dartlang.org" source: hosted - version: "1.3.0" + version: "1.7.0" nested: dependency: transitive description: @@ -204,7 +204,7 @@ packages: name: test_api url: "https://pub.dartlang.org" source: hosted - version: "0.3.0" + version: "0.4.2" timezone: dependency: transitive description: diff --git a/lib/src/android_config.dart b/lib/src/android_config.dart index c734db9..de11f06 100644 --- a/lib/src/android_config.dart +++ b/lib/src/android_config.dart @@ -37,6 +37,12 @@ class FlutterBackgroundAndroidConfig { /// The resource name of the icon to be used for the foreground notification. final AndroidResource notificationIcon; + /// When enabled, a WifiLock is acquired when background execution is started. + /// This allows the application to keep the Wi-Fi radio awake, even when the + /// user has not used the device in a while (e.g. for background network + /// communications). + final bool enableWifiLock; + /// Creates an Android specific configuration for the [FlutterBackground] plugin. /// /// [notificationTitle] is the title used for the foreground service notification. @@ -44,12 +50,15 @@ class FlutterBackgroundAndroidConfig { /// [notificationImportance] is the importance of the foreground service notification. /// [notificationIcon] must be a drawable resource. /// E. g. if the icon with name "background_icon" is in the "drawable" resource folder, - /// [notificationIcon] should be of value `AndroidResource(name: 'background_icon', defType: 'drawable'). - /// It must be greater than [AndroidNotificationImportance.Min]. + /// it should be of value `AndroidResource(name: 'background_icon', defType: 'drawable'). + /// [enableWifiLock] indicates wether or not a WifiLock is acquired, when the + /// background execution is started. This allows the application to keep the + /// Wi-Fi radio awake, even when the user has not used the device in a while. const FlutterBackgroundAndroidConfig( {this.notificationTitle = 'Notification title', this.notificationText = 'Notification text', this.notificationImportance = AndroidNotificationImportance.Default, this.notificationIcon = - const AndroidResource(name: 'ic_launcher', defType: 'mipmap')}); + const AndroidResource(name: 'ic_launcher', defType: 'mipmap'), + this.enableWifiLock = true}); } diff --git a/lib/src/flutter_background.dart b/lib/src/flutter_background.dart index 4765ef3..3dbc487 100644 --- a/lib/src/flutter_background.dart +++ b/lib/src/flutter_background.dart @@ -26,6 +26,7 @@ class FlutterBackground { 'android.notificationIconName': androidConfig.notificationIcon.name, 'android.notificationIconDefType': androidConfig.notificationIcon.defType, + 'android.enableWifiLock': androidConfig.enableWifiLock, }) == true; return _isInitialized; diff --git a/pubspec.lock b/pubspec.lock index 4ed2c66..4935c14 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -7,7 +7,7 @@ packages: name: async url: "https://pub.dartlang.org" source: hosted - version: "2.6.1" + version: "2.8.1" boolean_selector: dependency: transitive description: @@ -28,7 +28,7 @@ packages: name: charcode url: "https://pub.dartlang.org" source: hosted - version: "1.2.0" + version: "1.3.1" clock: dependency: transitive description: @@ -73,7 +73,7 @@ packages: name: meta url: "https://pub.dartlang.org" source: hosted - version: "1.3.0" + version: "1.7.0" path: dependency: transitive description: @@ -127,7 +127,7 @@ packages: name: test_api url: "https://pub.dartlang.org" source: hosted - version: "0.3.0" + version: "0.4.2" typed_data: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 303a336..0d00520 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: flutter_background description: A plugin to keep flutter apps running in the background by using foreground service, wake lock and disabling battery optimizations -version: 1.0.2+2 +version: 1.1.0 repository: https://github.com/JulianAssmann/flutter_background homepage: https://julianassmann.de/