Beginning with Android API level 26, persistent notifications are required for foreground services. This requirement is meant to prevent you from hiding services that might put excessive demands on system resources, including the battery in particular. This requirement creates a potential problem: If an app with multiple foreground services doesn't carefully manage the notification so that it is shared across all services, then there can be multiple persistent undismissable notifications, leading to unwelcomed clutter in the active list of notifications.
This problem becomes more challenging when you use SDKs such as the Navigation
SDK, that run foreground services independent of the app that have their
own independent persistent notifications, making them difficult to consolidate.
To address these issues, the Navigation SDK v1.11 introduced a simple API to
help manage persistent notifications across the app, including within the SDK.
Components
The foreground service manager provides a wrapper around the Android foreground service class and the persistent notification class. This wrapper's main function is to enforce reuse of the Notification ID so that the notification is shared across all foreground services using the manager.
The Navigation SDK contains static methods for initializing and getting the
ForegroundServiceManager
singleton. This singleton can only be initialized
once in the lifetime of the Navigation SDK. Consequently, if you use one of the
initialization calls (initForegroundServiceManagerMessageAndIntent()
or
initForegroundServiceManagerProvider()
), then you should surround
it with a try-catch block in case that path is reentered. The Navigation SDK
throws a runtime exception if you call either method more than once unless you
first clear all references to the ForegroundServiceManager
and call
clearForegroundServiceManager()
before each subsequent call.
The four parameters of initForegroundServiceManagerMessageAndIntent()
are
application
, notificationId
, defaultMessage
, and resumeIntent
. If the
final three parameters are null, then the notification is the standard
Navigation SDK notification. It's still possible to hide other foreground
services in the app behind this notification. The notificationId
parameter
specifies the notification ID that should be used for the notification. If it is
null, then an arbitrary value is used. You can set it explicitly to work around
conflicts with other notifications, such as those from another SDK. The
defaultMessage
is a string that is displayed when the system is not
navigating. The resumeIntent
is an intent that is fired when the notification
is clicked on. If the resumeIntent
is null, then clicks on the notification
are ignored.
The three parameters of initForegroundServiceManagerProvider()
are
application
, notificationId
, and notificationProvider
. If the final
two parameters are null, then the notification is the standard Navigation SDK
notification. The notificationId
parameter specifies the notification ID that
should be used for the notification. If it is null, then an arbitrary value is
used. You can set it explicitly to work around conflicts with other
notifications, such as those from another SDK. If the notificationProvider
is
set, then the provider is always responsible for
generating the notification to be rendered.
The Navigation SDK getForegroundServiceManager()
method returns the
foreground service manager singleton. If you haven't generated one yet, then
it's the equivalent of calling initForegroundServiceManagerMessageAndIntent()
with null parameters for the notificationId
, defaultMessage
, and
resumeIntent
.
The ForegroundServiceManager
has three simple methods. The first two are for
moving a service into and out of the foreground, and are typically called from
within the service that has been created. Using these methods ensures that the
services are associated with the shared persistent notification. The final
method, updateNotification()
, flags the manager that the notification has
changed, and should be re-rendered.
If you need complete control of the shared persistent notification, then
the API provides a NotificationContentProvider
interface for defining a
notification provider, which contains a single method for getting a notification
with the current content. It also provides a base class, which you can
optionally use to help define the provider. One of the base class's main
purposes is that it provides a way to call updateNotification()
without the
need to access the ForegroundServiceManager
. If you use an instance of the
notification provider to receive new notification messages, you can call this
internal method directly to render the message in the notification.
Usage scenarios
This section details the usage scenarios for using shared persistent notifications.
- Hide persistent notifications of other app foreground services
- The easiest scenario is to preserve current behavior, and only use the
persistent notification for rendering Navigation SDK information. Other services
can hide behind this notification by using the foreground service manager
startForeground()
andstopForeground()
methods. - Hide persistent notifications of other app foreground services, but set default text shown when not navigating
- The second easiest scenario is to preserve current behavior, and only use
the persistent notification for rendering Navigation SDK information, except
when the system is not navigating. When the system is not navigating, the
string provided to
initForegroundServiceManagerMessageAndIntent()
is displayed rather than the default Navigation SDK string that mentions "Google Maps". You can also use this call to set the resume intent that fires when the notification is clicked. - Take full control of the rendering of the persistent notification
- The final scenario requires defining and creating a notification provider
and passing it to the
ForegroundServiceManager
usinginitForegroundServiceManagerProvider()
. This option gives you full control of what is rendered in the notification, but it also disconnects the Navigation SDK notification information from the notification, thereby removing the helpful turn-by-turn prompts shown in the notification. Google doesn't provide a simple means for retrieving this information and inserting it into the notification.
Example notification provider
The following code example demonstrates how to create and return notifications using a simple notification content provider.
public class NotificationContentProviderImpl
extends NotificationContentProviderBase
implements NotificationContentProvider {
private String channelId;
private Context context;
private String message;
/** Constructor */
public NotificationContentProviderImpl(Application application) {
super(application);
message = "-- uninitialized --";
channelId = null;
this.context = application;
}
/**
* Sets message to display in the notification. Calls updateNotification
* to display the message immediately.
*
* @param msg The message to display in the notification.
*/
public void setMessage(String msg) {
message = msg;
updateNotification();
}
/**
* Returns the notification as it should be rendered.
*/
@Override
public Notification getNotification() {
Notification notification;
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
Spanned styledText = Html.fromHtml(message, FROM_HTML_MODE_LEGACY);
String channelId = getChannelId(context);
notification =
new Notification.Builder(context, channelId)
.setContentTitle("Notifications Demo")
.setStyle(new Notification.BigTextStyle()
.bigText(styledText))
.setSmallIcon(R.drawable.ic_navigation_white_24dp)
.setTicker("ticker text")
.build();
} else {
notification = new Notification.Builder(context)
.setContentTitle("Notification Demo")
.setContentText("testing non-O text")
.build();
}
return notification;
}
// Helper to set up a channel ID.
private String getChannelId(Context context) {
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
if (channelId == null) {
NotificationManager notificationManager =
(NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
NotificationChannel channel = new NotificationChannel(
"default", "navigation", NotificationManager.IMPORTANCE_DEFAULT);
channel.setDescription("For navigation persistent notification.");
notificationManager.createNotificationChannel(channel);
channelId = channel.getId();
}
return channelId;
} else {
return "";
}
}
}
After you've created NotificationContentProviderImpl
, you connect the
Navigation SDK to it using the following code:
ForegroundServiceManager f = NavigationApi.getForegroundServiceManager(getApplication());
mNotification = new NotificationContentProviderImpl(getApplication());
NavigationApi.clearForegroundServiceManager();
NavigationApi.initForegroundServiceManagerProvider(getApplication(), null, mNotification);
Caveats and future plans
- Be sure to call
initForegroundServiceManagerMessageAndIntent()
orinitForegroundServiceManagerProvider()
early so that the expected usage scenario is well-defined. You must call this method before you create a new Navigator. - Be sure to catch exceptions from calls to
initForegroundServiceManagerMessageAndIntent()
orinitForegroundServiceManagerProvider()
in case the code pathway is entered more than once. In the Navigation SDK v2.0, calling this method multiple times throws a checked exception rather than a runtime exception. - Google still might have work to do to get consistent styling over the lifetime of the notification that matches the header styling.
- When you define a notification provider, you can control heads-up behavior with the priority.
- Google does not provide a simple means for retrieving turn-by-turn information that a notification provider might insert into the notification.