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
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
## 1.3.1

* Fix battery optimization callback not being invoked on Android
* Update example app to use Gradle 8.7, AGP 8.6.0, and Kotlin 2.1.0

## 1.3.0+1

* Update README to not include unecessary `<uses-permission>` in example `AndroidManifest.xml`, as these are already defined in the plugins `AndroidManifest.xml`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.MethodChannel.MethodCallHandler
import io.flutter.plugin.common.MethodChannel.Result
import io.flutter.plugin.common.PluginRegistry

class FlutterBackgroundPlugin: FlutterPlugin, MethodCallHandler, ActivityAware {
private var methodChannel : MethodChannel? = null
Expand Down Expand Up @@ -201,10 +200,7 @@ class FlutterBackgroundPlugin: FlutterPlugin, MethodCallHandler, ActivityAware {
}

override fun onAttachedToActivity(binding: ActivityPluginBinding) {
startListeningToActivity(
binding.activity,
binding::addActivityResultListener,
binding::addRequestPermissionsResultListener)
startListeningToActivity(binding.activity)
}

override fun onDetachedFromActivityForConfigChanges() {
Expand All @@ -226,19 +222,13 @@ class FlutterBackgroundPlugin: FlutterPlugin, MethodCallHandler, ActivityAware {
context = null
}

private fun startListeningToActivity(
activity: Activity,
addActivityResultListener: ((PluginRegistry.ActivityResultListener) -> Unit),
addRequestPermissionResultListener: ((PluginRegistry.RequestPermissionsResultListener) -> Unit)
) {
private fun startListeningToActivity(activity: Activity) {
this.activity = activity
permissionHandler = PermissionHandler(
activity.applicationContext,
addActivityResultListener,
addRequestPermissionResultListener)
permissionHandler = PermissionHandler(activity.applicationContext)
}

private fun stopListeningToActivity() {
permissionHandler?.cleanup()
this.activity = null
permissionHandler = null
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,30 +2,35 @@ package de.julianassmann.flutter_background

import android.Manifest
import android.app.Activity
import android.app.Application
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.os.PowerManager
import android.provider.Settings
import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.PluginRegistry

class PermissionHandler(private val context: Context,
private val addActivityResultListener: ((PluginRegistry.ActivityResultListener) -> Unit),
private val addRequestPermissionsResultListener: ((PluginRegistry.RequestPermissionsResultListener) -> Unit)) {
class PermissionHandler(private val context: Context) {
companion object {
const val PERMISSION_CODE_IGNORE_BATTERY_OPTIMIZATIONS = 5672353
private const val TAG = "PermissionHandler"
private const val TIMEOUT_MS = 60000L
}

fun isWakeLockPermissionGranted(): Boolean
{
private var lifecycleCallback: Application.ActivityLifecycleCallbacks? = null
private var pendingBatteryOptimizationResult: MethodChannel.Result? = null
private var isWaitingForBatteryOptimization = false

fun isWakeLockPermissionGranted(): Boolean {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
context.checkSelfPermission(Manifest.permission.WAKE_LOCK) == PackageManager.PERMISSION_GRANTED
} else {
true
};
}
}

fun isIgnoringBatteryOptimizations(): Boolean {
Expand All @@ -38,54 +43,109 @@ class PermissionHandler(private val context: Context,
}
}

fun requestBatteryOptimizationsOff(
result: MethodChannel.Result,
activity: Activity) {
fun requestBatteryOptimizationsOff(result: MethodChannel.Result, activity: Activity) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
// Before Android M the battery optimization doesn't exist -> Always "ignoring"
result.success(true)
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
val powerManager = (context.getSystemService(Context.POWER_SERVICE) as PowerManager)
when {
powerManager.isIgnoringBatteryOptimizations(context.packageName) -> {
result.success(true)
return
}

val powerManager = context.getSystemService(Context.POWER_SERVICE) as PowerManager

when {
powerManager.isIgnoringBatteryOptimizations(context.packageName) -> {
result.success(true)
}
context.checkSelfPermission(Manifest.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS) == PackageManager.PERMISSION_DENIED -> {
result.error(
"flutter_background.PermissionHandler",
"The app does not have the REQUEST_IGNORE_BATTERY_OPTIMIZATIONS permission required to ask the user for whitelisting.See the documentation on how to setup this plugin properly.",
null
)
}
else -> {
setupLifecycleDetection(activity, result)

val intent = Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS).apply {
data = Uri.parse("package:${context.packageName}")
}

try {
activity.startActivity(intent)
} catch (e: Exception) {
cleanupLifecycleDetection()
result.error("BatteryOptimizationError", "Unable to request battery optimization permission", e.message)
}
context.checkSelfPermission(Manifest.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS) == PackageManager.PERMISSION_DENIED -> {
result.error(
"flutter_background.PermissionHandler",
"The app does not have the REQUEST_IGNORE_BATTERY_OPTIMIZATIONS permission required to ask the user for whitelisting. See the documentation on how to setup this plugin properly.",
null)
}
}
}

private fun setupLifecycleDetection(activity: Activity, result: MethodChannel.Result) {
cleanupLifecycleDetection()

pendingBatteryOptimizationResult = result
isWaitingForBatteryOptimization = true

lifecycleCallback = object : Application.ActivityLifecycleCallbacks {
private var wasPaused = false
private val timeoutHandler = Handler(Looper.getMainLooper())
private val timeoutRunnable = Runnable { handleBatteryOptimizationReturn() }

override fun onActivityResumed(activity: Activity) {
if (wasPaused && isWaitingForBatteryOptimization) {
timeoutHandler.postDelayed({ handleBatteryOptimizationReturn() }, 500)
}
else -> {
addActivityResultListener(PermissionActivityResultListener(result::success, result::error))
val intent = Intent()
intent.action = Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS
intent.data = Uri.parse("package:${context.packageName}")
activity.startActivityForResult(intent, PERMISSION_CODE_IGNORE_BATTERY_OPTIMIZATIONS)
}

override fun onActivityPaused(activity: Activity) {
if (isWaitingForBatteryOptimization) {
wasPaused = true
timeoutHandler.postDelayed(timeoutRunnable, TIMEOUT_MS)
}
}

override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {}
override fun onActivityStarted(activity: Activity) {}
override fun onActivityStopped(activity: Activity) {}
override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {}
override fun onActivityDestroyed(activity: Activity) {}
}

activity.application.registerActivityLifecycleCallbacks(lifecycleCallback)
}
}

class PermissionActivityResultListener(
private val onSuccess: (Any?) -> Unit,
private val onError: (String, String?, Any?) -> Unit) : PluginRegistry.ActivityResultListener {
private fun cleanupLifecycleDetection() {
lifecycleCallback?.let { callback ->
val app = context.applicationContext as Application
app.unregisterActivityLifecycleCallbacks(callback)
}
lifecycleCallback = null
pendingBatteryOptimizationResult = null
isWaitingForBatteryOptimization = false
}

private fun handleBatteryOptimizationReturn() {
if (!isWaitingForBatteryOptimization || pendingBatteryOptimizationResult == null) {
return
}

private var alreadyCalled: Boolean = false;
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?): Boolean {
try {
if (alreadyCalled || requestCode != PermissionHandler.PERMISSION_CODE_IGNORE_BATTERY_OPTIMIZATIONS) {
return false
val isIgnoringBatteryOptimizations = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
val powerManager = context.getSystemService(Context.POWER_SERVICE) as PowerManager
powerManager.isIgnoringBatteryOptimizations(context.packageName)
} else {
true
}

alreadyCalled = true

onSuccess(resultCode == Activity.RESULT_OK)
} catch (ex: Exception) {
onError("flutter_background.PermissionHandler", "Error while waiting for user to disable battery optimizations", ex.localizedMessage)
pendingBatteryOptimizationResult?.success(isIgnoringBatteryOptimizations)
} catch (e: Exception) {
pendingBatteryOptimizationResult?.error("BatteryOptimizationError", "Error checking permission status", e.message)
} finally {
cleanupLifecycleDetection()
}
}

return true
fun cleanup() {
cleanupLifecycleDetection()
}
}
2 changes: 1 addition & 1 deletion example/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ You need to have the following tools installed:
To start the server, go into the `server` folder and run

```bash
dart server.dart
dart server/server.dart
```

To start the app, go into the `app` folder and run
Expand Down
4 changes: 3 additions & 1 deletion example/android/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,12 @@ if (flutterVersionName == null) {
}

android {
namespace "de.julianassmann.flutter_background_example"
compileSdkVersion flutter.compileSdkVersion
ndkVersion flutter.ndkVersion

compileOptions {
coreLibraryDesugaringEnabled true
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
Expand Down Expand Up @@ -60,5 +62,5 @@ flutter {

// Version 10+ of flutter_local_notifications relies on desguaring to support scheduled notifications with backwards compatibility on older versions of Android.
dependencies {
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.2.2'
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.1.4'
}
2 changes: 1 addition & 1 deletion example/android/gradle/wrapper/gradle-wrapper.properties
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-all.zip
4 changes: 2 additions & 2 deletions example/android/settings.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ pluginManagement {

plugins {
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
id "com.android.application" version "7.3.1" apply false
id "org.jetbrains.kotlin.android" version "1.7.10" apply false
id "com.android.application" version "8.6.0" apply false
id "org.jetbrains.kotlin.android" version "2.1.0" apply false
}

include ":app"
Loading