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 will 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, which can 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 NavigationAPI 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. To prevent
incompatibility issues, 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. In v2.0 of the Navigation
SDK, a checked exception is added to the API for this purpose.
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’s 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 want complete control of the shared persistent notification’s content, then
the new 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’ main purposes
is that it provides an easy means to call updateNotification()
without needing
to access the ForegroundServiceManager
. This helper method can be handy if you
use an instance of the notification provider to receive new notification
messages, in which case 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". This call can also be used to set the resume intent that will fire 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 via
initForegroundServiceManagerProvider()
. 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 yet 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 "";
}
}
}
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 yet provide a simple means for retrieving turn-by-turn information that a notification provider might insert into the notification.