diff --git a/flutter_local_notifications/README.md b/flutter_local_notifications/README.md index b3eca84eb..72ba60979 100644 --- a/flutter_local_notifications/README.md +++ b/flutter_local_notifications/README.md @@ -413,6 +413,75 @@ When specifying the large icon bitmap or big picture bitmap (associated with the ⚠️ For Android 8.0+, sounds and vibrations are associated with notification channels and can only be configured when they are first created. Showing/scheduling a notification will create a channel with the specified id if it doesn't exist already. If another notification specifies the same channel id but tries to specify another sound or vibration pattern then nothing occurs. + +### Bind ForegroundService to your FlutterActivity + +In your activity (e.g., `MainActivity.kt`), set up a broadcast receiver and a `ServiceConnection` to manage binding and unbinding. This is not required to use a `ForegroundService` but it will decrease the likelyhood of your activity being [killed or frozen by the OS while the activity is in the background](https://source.android.com/docs/core/perf/cached-apps-freezer#handling-custom-features): + +```kotlin +class MainActivity: FlutterActivity() { + private var isServiceBound = false + private val serviceStartedAction = "com.dexterous.flutterlocalnotifications.FOREGROUND_SERVICE_STARTED" + private val serviceStoppedAction = "com.dexterous.flutterlocalnotifications.FOREGROUND_SERVICE_STOPPED" + + private val serviceConnection = object : ServiceConnection { + override fun onServiceConnected(name: ComponentName?, binder: IBinder?) { + isServiceBound = true + Log.d("MainActivity", "Service bound") + } + override fun onServiceDisconnected(name: ComponentName?) { + isServiceBound = false + Log.d("MainActivity", "Service disconnected") + } + } + + private val serviceBroadcastReceiver = object : android.content.BroadcastReceiver() { + override fun onReceive(context: Context?, intent: Intent?) { + Log.d("MainActivity", "Received broadcast: ${intent?.action}") + when (intent?.action) { + serviceStartedAction -> { + if (!isServiceBound) { + val bindIntent = Intent(context, ForegroundService::class.java) + bindService(bindIntent, serviceConnection, Context.BIND_AUTO_CREATE) + } + } + serviceStoppedAction -> { + if (isServiceBound) { + unbindService(serviceConnection) + isServiceBound = false + Log.d("MainActivity", "Service unbound from broadcast") + } + } + } + } + } + + @SuppressLint("UnspecifiedRegisterReceiverFlag") + override fun onCreate(savedInstanceState: android.os.Bundle?) { + super.onCreate(savedInstanceState) + val filter = android.content.IntentFilter().apply { + addAction(serviceStartedAction) + addAction(serviceStoppedAction) + } + if (android.os.Build.VERSION.SDK_INT >= 33) { + registerReceiver(serviceBroadcastReceiver, filter, Context.RECEIVER_NOT_EXPORTED) + } else { + registerReceiver(serviceBroadcastReceiver, filter) + } + } + + override fun onDestroy() { + super.onDestroy() + unregisterReceiver(serviceBroadcastReceiver) + if (isServiceBound) { + unbindService(serviceConnection) + isServiceBound = false + } + } + // ...existing code... +} +``` + ### Full-screen intent notifications If your application needs the ability to schedule full-screen intent notifications, add the following attributes to the activity you're opening. For a Flutter application, there is typically only one activity extends from `FlutterActivity`. These attributes ensure the screen turns on and shows when the device is locked. diff --git a/flutter_local_notifications/android/src/main/java/com/dexterous/flutterlocalnotifications/FlutterLocalNotificationsPlugin.java b/flutter_local_notifications/android/src/main/java/com/dexterous/flutterlocalnotifications/FlutterLocalNotificationsPlugin.java index 10520339a..5ef6b6a15 100644 --- a/flutter_local_notifications/android/src/main/java/com/dexterous/flutterlocalnotifications/FlutterLocalNotificationsPlugin.java +++ b/flutter_local_notifications/android/src/main/java/com/dexterous/flutterlocalnotifications/FlutterLocalNotificationsPlugin.java @@ -2306,6 +2306,8 @@ private void startForegroundService(MethodCall call, Result result) { Intent intent = new Intent(applicationContext, ForegroundService.class); intent.putExtra(ForegroundServiceStartParameter.EXTRA, parameter); ContextCompat.startForegroundService(applicationContext, intent); + applicationContext.sendBroadcast(new Intent("com.dexterous.flutterlocalnotifications.FOREGROUND_SERVICE_STARTED") + .setPackage(applicationContext.getPackageName())); result.success(null); } else { result.error( @@ -2326,6 +2328,7 @@ private void startForegroundService(MethodCall call, Result result) { private void stopForegroundService(Result result) { applicationContext.stopService(new Intent(applicationContext, ForegroundService.class)); + applicationContext.sendBroadcast(new Intent("com.dexterous.flutterlocalnotifications.FOREGROUND_SERVICE_STOPPED").setPackage(applicationContext.getPackageName())); result.success(null); } diff --git a/flutter_local_notifications/android/src/main/java/com/dexterous/flutterlocalnotifications/ForegroundService.java b/flutter_local_notifications/android/src/main/java/com/dexterous/flutterlocalnotifications/ForegroundService.java index ce27856c2..9d4e84edd 100644 --- a/flutter_local_notifications/android/src/main/java/com/dexterous/flutterlocalnotifications/ForegroundService.java +++ b/flutter_local_notifications/android/src/main/java/com/dexterous/flutterlocalnotifications/ForegroundService.java @@ -8,7 +8,16 @@ import java.util.ArrayList; +import android.os.Binder; + public class ForegroundService extends Service { + private final IBinder binder = new LocalBinder(); + + public class LocalBinder extends Binder { + ForegroundService getService() { + return ForegroundService.this; + } + } @Override @SuppressWarnings("deprecation") @@ -36,6 +45,7 @@ public int onStartCommand(Intent intent, int flags, int startId) { } else { startForeground(parameter.notificationData.id, notification); } + return parameter.startMode; } @@ -49,6 +59,6 @@ private static int orCombineFlags(ArrayList flags) { @Override public IBinder onBind(Intent intent) { - return null; + return binder; } } diff --git a/flutter_local_notifications/example/android/app/src/main/kotlin/com/dexterous/flutter_local_notifications_example/MainActivity.kt b/flutter_local_notifications/example/android/app/src/main/kotlin/com/dexterous/flutter_local_notifications_example/MainActivity.kt index e36ed7c92..2e9dde6c8 100644 --- a/flutter_local_notifications/example/android/app/src/main/kotlin/com/dexterous/flutter_local_notifications_example/MainActivity.kt +++ b/flutter_local_notifications/example/android/app/src/main/kotlin/com/dexterous/flutter_local_notifications_example/MainActivity.kt @@ -1,15 +1,57 @@ package com.dexterous.flutter_local_notifications_example +import android.annotation.SuppressLint import android.content.ContentResolver import android.content.Context +import android.content.Intent +import android.content.ServiceConnection +import android.content.ComponentName +import android.os.IBinder import android.media.RingtoneManager +import android.util.Log +import com.dexterous.flutterlocalnotifications.ForegroundService import io.flutter.embedding.android.FlutterActivity import io.flutter.embedding.engine.FlutterEngine import io.flutter.plugin.common.MethodChannel -import java.util.* class MainActivity: FlutterActivity() { + private var isServiceBound = false + private val serviceStartedAction = "com.dexterous.flutterlocalnotifications.FOREGROUND_SERVICE_STARTED" + private val serviceStoppedAction = "com.dexterous.flutterlocalnotifications.FOREGROUND_SERVICE_STOPPED" + + private val serviceConnection = object : ServiceConnection { + override fun onServiceConnected(name: ComponentName?, binder: IBinder?) { + isServiceBound = true + Log.d("MainActivity", "Service bound") + } + override fun onServiceDisconnected(name: ComponentName?) { + isServiceBound = false + Log.d("MainActivity", "Service disconnected") + } + } + + private val serviceBroadcastReceiver = object : android.content.BroadcastReceiver() { + override fun onReceive(context: Context?, intent: Intent?) { + Log.d("MainActivity", "Received broadcast: ${intent?.action}") + when (intent?.action) { + serviceStartedAction -> { + if (!isServiceBound) { + val bindIntent = Intent(context, ForegroundService::class.java) + bindService(bindIntent, serviceConnection, Context.BIND_AUTO_CREATE) + } + } + serviceStoppedAction -> { + if (isServiceBound) { + unbindService(serviceConnection) + isServiceBound = false + Log.d("MainActivity", "Service unbound from broadcast") + } + } + } + } + } + override fun configureFlutterEngine(flutterEngine: FlutterEngine) { super.configureFlutterEngine(flutterEngine) MethodChannel(flutterEngine.dartExecutor.binaryMessenger, "dexterx.dev/flutter_local_notifications_example").setMethodCallHandler { call, result -> @@ -23,6 +65,29 @@ class MainActivity: FlutterActivity() { } } + @SuppressLint("UnspecifiedRegisterReceiverFlag") + override fun onCreate(savedInstanceState: android.os.Bundle?) { + super.onCreate(savedInstanceState) + val filter = android.content.IntentFilter().apply { + addAction(serviceStartedAction) + addAction(serviceStoppedAction) + } + if (android.os.Build.VERSION.SDK_INT >= 33) { + registerReceiver(serviceBroadcastReceiver, filter, Context.RECEIVER_NOT_EXPORTED) + } else { + registerReceiver(serviceBroadcastReceiver, filter) + } + } + + override fun onDestroy() { + super.onDestroy() + unregisterReceiver(serviceBroadcastReceiver) + if (isServiceBound) { + unbindService(serviceConnection) + isServiceBound = false + } + } + private fun resourceToUriString(context: Context, resId: Int): String? { return (ContentResolver.SCHEME_ANDROID_RESOURCE + "://" + context.resources.getResourcePackageName(resId)