Skip to content
Open
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
69 changes: 69 additions & 0 deletions flutter_local_notifications/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The table of contents is missing an update that allows for jumping to this section


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):
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this start off by saying it's optional and only applicable for apps using foreground services? Thinking behind this is some readers may not pick this up or know enough about Android development to realise


```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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let me know what you think but I believe it's best to do the following

  • add additional folders that so that com.dexterous.flutterlocalnotifications.intent.action namespace exists. This is to follow convention of having the strings behind a namespace where last segment has action like other actions e.g. android.intent.action.<...> and htc.com.intent.action.<...>
  • create a class within the action (i.e. com.dexterous.flutterlocalnotifications.intent.action namespace) that holds public static strings for com.dexterous.flutterlocalnotifications.action.FOREGROUND_SERVICE_STARTED and com.dexterous.flutterlocalnotifications.action.FOREGROUND_SERVICE_STOPPED. This to allow developers an easier way to reference the strings through the plugin. The best name I can come up with for the class is FlutterLocalNotificationsPluginIntent. It'd be similar to how developers can reference Intent.ACTION_VIEW_LOCUS . In this case, they can reference FlutterLocalNotificationsPluginIntent.FOREGROUND_SERVICE_STARTED and FlutterLocalNotificationsPluginIntent.FOREGROUND_SERVICE_STOPPED

.setPackage(applicationContext.getPackageName()));
result.success(null);
} else {
result.error(
Expand All @@ -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);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -36,6 +45,7 @@ public int onStartCommand(Intent intent, int flags, int startId) {
} else {
startForeground(parameter.notificationData.id, notification);
}

return parameter.startMode;
}

Expand All @@ -49,6 +59,6 @@ private static int orCombineFlags(ArrayList<Integer> flags) {

@Override
public IBinder onBind(Intent intent) {
return null;
return binder;
}
}
Original file line number Diff line number Diff line change
@@ -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 ->
Expand All @@ -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)
Expand Down