/*
* Copyright 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.car.app.notification;
import static java.util.Objects.requireNonNull;
import android.app.Notification;
import android.app.Notification.Action;
import android.app.PendingIntent;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.os.Bundle;
import android.util.Log;
import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.car.app.model.CarColor;
import androidx.car.app.serialization.Bundler;
import androidx.car.app.serialization.BundlerException;
import androidx.car.app.utils.CollectionUtils;
import androidx.core.app.NotificationCompat;
import androidx.core.app.NotificationManagerCompat;
import java.util.ArrayList;
import java.util.List;
/**
* Helper class to add car app extensions to notifications.
*
* <p>By default, notifications in a car screen have the properties provided by
* {@link NotificationCompat.Builder}. This helper class provides methods to
* override those properties for the car screen. However, notifications only show up in the car
* screen if it is extended with {@link CarAppExtender}, even if the extender does not override any
* properties. To create a notification with car extensions:
*
* <ol>
* <li>Create a {@link NotificationCompat.Builder}, setting any desired properties.
*
* <li>Create a {@link CarAppExtender.Builder}.
*
* <li>Set car-specific properties using the {@code set} methods of {@link
* CarAppExtender.Builder}.
*
* <li>Create a {@link CarAppExtender} by calling {@link Builder#build()}.
*
* <li>Call {@link NotificationCompat.Builder#extend} to apply the extensions to a notification.
*
* <li>Post the notification to the notification system with the {@code
* CarNotificationManager.notify(...)} methods. Do not use the {@code
* NotificationManager.notify(...)}, nor the NotificationManagerCompat.notify(...)} methods.
* </ol>
*
* <pre class="prettyprint">
* Notification notification = new NotificationCompat.Builder(context)
* ...
* .extend(new CarAppExtender.Builder()
* .set*(...)
* .build())
* .build();
* </pre>
*
* <p>Car extensions can be accessed on an existing notification by using the {@code
* CarAppExtender(Notification)} constructor, and then using the {@code get} methods to access
* values.
*
* <p>The car screen UI is affected by the notification channel importance (Android O and above) or
* notification priority (below Android O) in the following ways:
*
* <ul>
* <li>A heads-up-notification (HUN) will show if the importance is set to
* {@link NotificationManagerCompat#IMPORTANCE_HIGH}, or the priority is set
* to {@link NotificationCompat#PRIORITY_HIGH} or above.
*
* <li>The notification center icon, which opens a screen with all posted notifications when
* tapped, will show a badge for a new notification if the importance is set to
* {@link NotificationManagerCompat#IMPORTANCE_DEFAULT} or above, or the
* priority is set to {@link NotificationCompat#PRIORITY_DEFAULT} or above.
* <li>The notification entry will show in the notification center for all priority levels.
* </ul>
*
* Calling {@link Builder#setImportance(int)} will override the importance for the notification in
* the car screen.
*
* <p>Calling {@code NotificationCompat.Builder#setOnlyAlertOnce(true)} will alert a high-priority
* notification only once in the HUN. Updating the same notification will not trigger another HUN
* event.
*
* <h4>Navigation</h4>
*
* <p>For a navigation app's turn-by-turn (TBT) notifications, which update the same notification
* frequently with navigation information, the notification UI has a slightly different behavior.
* The app can post a TBT notification by calling {@code
* NotificationCompat.Builder#setOngoing(true)} and {@code
* NotificationCompat.Builder#setCategory(NotificationCompat.CATEGORY_NAVIGATION)}.
* <p>TBT notifications behave the same as regular notifications with the following
* exceptions:
*
* <ul>
* <li>The notification will not be displayed if the navigation app is not the currently active
* navigation app, or if the app is already displaying routing information in the navigation
* template.
*
* <li>The heads-up-notification (HUN) can be customized with a background color through
* {@link Builder#setColor}.
*
* <li>The notification will not be displayed in the notification center.
* </ul>
*
* <p>In addition to that, the information in the navigation notification will be displayed in the
* rail widget at the bottom of the screen when the app is in the background.
*
* <p>Note that frequent HUNs distract the driver. The recommended practice is to update the TBT
* notification regularly on distance changes, which updates the rail widget, but call {@code
* NotificationCompat.Builder#setOnlyAlertOnce(true)} unless there is a significant navigation turn
* event.
*/
public final class CarAppExtender implements NotificationCompat.Extender {
private static final String TAG = "CarAppExtender";
private static final String EXTRA_CAR_EXTENDER = "androidx.car.app.EXTENSIONS";
private static final String EXTRA_CONTENT_TITLE = "content_title";
private static final String EXTRA_CONTENT_TEXT = "content_text";
private static final String EXTRA_SMALL_RES_ID = "small_res_id";
private static final String EXTRA_LARGE_BITMAP = "large_bitmap";
private static final String EXTRA_CONTENT_INTENT = "content_intent";
private static final String EXTRA_DELETE_INTENT = "delete_intent";
private static final String EXTRA_ACTIONS = "actions";
private static final String EXTRA_IMPORTANCE = "importance";
private static final String EXTRA_COLOR = "color";
private static final String EXTRA_CHANNEL_ID = "channel_id";
@Nullable
private CharSequence mContentTitle;
@Nullable
private CharSequence mContentText;
private int mSmallIconResId;
@Nullable
private Bitmap mLargeIconBitmap;
@Nullable
private PendingIntent mContentIntent;
@Nullable
private PendingIntent mDeleteIntent;
@Nullable
private ArrayList<Action> mActions;
private int mImportance;
@Nullable
private CarColor mColor;
@Nullable
private String mChannelId;
/**
* Creates a {@link CarAppExtender} from the {@link CarAppExtender} of an existing notification.
*/
@SuppressWarnings("deprecation")
public CarAppExtender(@NonNull Notification notification) {
Bundle extras = NotificationCompat.getExtras(notification);
if (extras == null) {
return;
}
Bundle carBundle = extras.getBundle(EXTRA_CAR_EXTENDER);
if (carBundle == null) {
return;
}
mContentTitle = carBundle.getCharSequence(EXTRA_CONTENT_TITLE);
mContentText = carBundle.getCharSequence(EXTRA_CONTENT_TEXT);
mSmallIconResId = carBundle.getInt(EXTRA_SMALL_RES_ID);
mLargeIconBitmap = carBundle.getParcelable(EXTRA_LARGE_BITMAP);
mContentIntent = carBundle.getParcelable(EXTRA_CONTENT_INTENT);
mDeleteIntent = carBundle.getParcelable(EXTRA_DELETE_INTENT);
ArrayList<Action> actions = carBundle.getParcelableArrayList(EXTRA_ACTIONS);
mActions = actions == null ? new ArrayList<>() : actions;
mImportance =
carBundle.getInt(EXTRA_IMPORTANCE,
NotificationManagerCompat.IMPORTANCE_UNSPECIFIED);
Bundle colorBundle = carBundle.getBundle(EXTRA_COLOR);
if (colorBundle != null) {
try {
mColor = (CarColor) Bundler.fromBundle(colorBundle);
} catch (BundlerException e) {
Log.e(TAG, "Failed to deserialize the notification color", e);
}
}
mChannelId = carBundle.getString(EXTRA_CHANNEL_ID);
}
CarAppExtender(Builder builder) {
mContentTitle = builder.mContentTitle;
mContentText = builder.mContentText;
mSmallIconResId = builder.mSmallIconResId;
mLargeIconBitmap = builder.mLargeIconBitmap;
mContentIntent = builder.mContentIntent;
mDeleteIntent = builder.mDeleteIntent;
mActions = builder.mActions;
mImportance = builder.mImportance;
mColor = builder.mColor;
mChannelId = builder.mChannelId;
}
/**
* Applies car extensions to a notification that is being built.
*
* <p>This is typically called by
* {@link NotificationCompat.Builder#extend(NotificationCompat.Extender)}.
*
* @throws NullPointerException if {@code builder} is {@code null}
*/
@NonNull
@Override
public NotificationCompat.Builder extend(@NonNull NotificationCompat.Builder builder) {
requireNonNull(builder);
Bundle carExtensions = new Bundle();
if (mContentTitle != null) {
carExtensions.putCharSequence(EXTRA_CONTENT_TITLE, mContentTitle);
}
if (mContentText != null) {
carExtensions.putCharSequence(EXTRA_CONTENT_TEXT, mContentText);
}
if (mSmallIconResId != Resources.ID_NULL) {
carExtensions.putInt(EXTRA_SMALL_RES_ID, mSmallIconResId);
}
if (mLargeIconBitmap != null) {
carExtensions.putParcelable(EXTRA_LARGE_BITMAP, mLargeIconBitmap);
}
if (mContentIntent != null) {
carExtensions.putParcelable(EXTRA_CONTENT_INTENT, mContentIntent);
}
if (mDeleteIntent != null) {
carExtensions.putParcelable(EXTRA_DELETE_INTENT, mDeleteIntent);
}
if (mActions != null && !mActions.isEmpty()) {
carExtensions.putParcelableArrayList(EXTRA_ACTIONS, mActions);
}
carExtensions.putInt(EXTRA_IMPORTANCE, mImportance);
if (mColor != null) {
try {
Bundle bundle = Bundler.toBundle(mColor);
carExtensions.putBundle(EXTRA_COLOR, bundle);
} catch (BundlerException e) {
Log.e(TAG, "Failed to serialize the notification color", e);
}
}
if (mChannelId != null) {
carExtensions.putString(EXTRA_CHANNEL_ID, mChannelId);
}
builder.getExtras().putBundle(EXTRA_CAR_EXTENDER, carExtensions);
return builder;
}
/**
* Returns whether the given notification was extended with {@link CarAppExtender}.
*
* @throws NullPointerException if {@code notification} is {@code null}
*/
public static boolean isExtended(@NonNull Notification notification) {
Bundle extras = NotificationCompat.getExtras(requireNonNull(notification));
if (extras == null) {
return false;
}
return extras.getBundle(EXTRA_CAR_EXTENDER) != null;
}
/**
* Returns the content title for the notification or {@code null} if not set.
*
* @see Builder#setContentTitle
*/
@Nullable
public CharSequence getContentTitle() {
return mContentTitle;
}
/**
* Returns the content text of the notification or {@code null} if not set.
*
* @see Builder#setContentText
*/
@Nullable
public CharSequence getContentText() {
return mContentText;
}
/**
* Returns the resource ID of the small icon drawable to use.
*
* @see Builder#setSmallIcon(int)
*/
@DrawableRes
public int getSmallIcon() {
return mSmallIconResId;
}
/**
* Returns the large icon bitmap to display in the notification or {@code null} if not set.
*
* @see Builder#setLargeIcon(Bitmap)
*/
@Nullable
public Bitmap getLargeIcon() {
return mLargeIconBitmap;
}
/**
* Returns the {@link PendingIntent} to send when the notification is clicked in the car or
* {@code null} if not set.
*
* @see Builder#setContentIntent(PendingIntent)
*/
@Nullable
public PendingIntent getContentIntent() {
return mContentIntent;
}
/**
* Returns the {@link PendingIntent} to send when the notification is cleared by the user or
* {@code null} if not set.
*
* @see Builder#setDeleteIntent(PendingIntent)
*/
@Nullable
public PendingIntent getDeleteIntent() {
return mDeleteIntent;
}
/**
* Returns the list of {@link Action} present on this car notification.
*
* @see Builder#addAction(int, CharSequence, PendingIntent)
*/
@NonNull
public List<Action> getActions() {
return CollectionUtils.emptyIfNull(mActions);
}
/**
* Returns the importance of the notification in the car screen.
*
* @see Builder#setImportance(int)
*/
public int getImportance() {
return mImportance;
}
/**
* Returns the background color of the notification or {@code null} if a default color is to
* be used.
*
* @see Builder#setColor(CarColor)
*/
@Nullable
public CarColor getColor() {
return mColor;
}
/**
* Returns the channel id of the notification channel to use in the car.
*
* @see Builder#setChannelId(String)
*/
@Nullable
public String getChannelId() {
return mChannelId;
}
/** A builder of {@link CarAppExtender}. */
public static final class Builder {
@Nullable
CharSequence mContentTitle;
@Nullable
CharSequence mContentText;
int mSmallIconResId;
@Nullable
Bitmap mLargeIconBitmap;
@Nullable
PendingIntent mContentIntent;
@Nullable
PendingIntent mDeleteIntent;
final ArrayList<Action> mActions = new ArrayList<>();
int mImportance = NotificationManagerCompat.IMPORTANCE_UNSPECIFIED;
@Nullable
CarColor mColor;
@Nullable
String mChannelId;
/**
* Sets the title of the notification in the car screen.
*
* <p>This will be the most prominently displayed text in the car notification.
*
* <p>This method is equivalent to
* {@link NotificationCompat.Builder#setContentTitle(CharSequence)} for the car
* screen.
*
* <p>Spans are not supported in the input string and will be ignored.
*
* @throws NullPointerException if {@code contentTitle} is {@code null}
*/
@NonNull
public Builder setContentTitle(@NonNull CharSequence contentTitle) {
mContentTitle = requireNonNull(contentTitle);
return this;
}
/**
* Sets the content text of the notification in the car screen.
*
* <p>This method is equivalent to
* {@link NotificationCompat.Builder#setContentText(CharSequence)} for the car screen.
*
* <p>Spans are not supported in the input string and will be ignored.
*
* @param contentText override for the notification's content text. If set to an empty
* string, it will be treated as if there is no context text
* @throws NullPointerException if {@code contentText} is {@code null}
*/
@NonNull
public Builder setContentText(@NonNull CharSequence contentText) {
mContentText = requireNonNull(contentText);
return this;
}
/**
* Sets the small icon of the notification in the car screen.
*
* <p>This is used as the primary icon to represent the notification.
*
* <p>This method is equivalent to {@link NotificationCompat.Builder#setSmallIcon(int)} for
* the car screen.
*/
@NonNull
public Builder setSmallIcon(int iconResId) {
mSmallIconResId = iconResId;
return this;
}
/**
* Sets the large icon of the notification in the car screen.
*
* <p>This is used as the secondary icon to represent the notification in the notification
* center.
*
* <p>This method is equivalent to {@link NotificationCompat.Builder#setLargeIcon(Bitmap)}
* for the car screen.
*
* <p>The large icon will be shown in the notification badge. If the large icon is not
* set in the {@link CarAppExtender} or the notification, the small icon will show instead.
*
* @throws NullPointerException if {@code bitmap} is {@code null}
*/
@NonNull
public Builder setLargeIcon(@NonNull Bitmap bitmap) {
mLargeIconBitmap = requireNonNull(bitmap);
return this;
}
/**
* Supplies a {@link PendingIntent} to send when the notification is clicked in the car.
*
* <p>If not set, the notification's content intent will be used.
*
* <p>In the case of navigation notifications in the rail widget, this intent will be
* sent when the user taps on the rail widget.
*
* <p>This method is equivalent to
* {@link NotificationCompat.Builder#setContentIntent(PendingIntent)} for the car screen.
*
* @param contentIntent override for the notification's content intent.
* @throws NullPointerException if {@code contentIntent} is {@code null}
*/
@NonNull
public Builder setContentIntent(@NonNull PendingIntent contentIntent) {
mContentIntent = requireNonNull(contentIntent);
return this;
}
/**
* Supplies a {@link PendingIntent} to send when the user clears the notification by either
* using the "clear all" functionality in the notification center, or tapping the individual
* "close" buttons on the notification in the car screen.
*
* <p>If not set, the notification's content intent will be used.
*
* <p>This method is equivalent to
* {@link NotificationCompat.Builder#setDeleteIntent(PendingIntent)} for the car screen.
*
* @param deleteIntent override for the notification's delete intent
* @throws NullPointerException if {@code deleteIntent} is {@code null}
*/
@NonNull
public Builder setDeleteIntent(@NonNull PendingIntent deleteIntent) {
mDeleteIntent = requireNonNull(deleteIntent);
return this;
}
/**
* Adds an action to this notification.
*
* <p>Actions are typically displayed by the system as a button adjacent to the notification
* content.
*
* <p>A notification may offer up to 2 actions. The system may not display some actions
* in the compact notification UI (e.g. heads-up-notifications).
*
* <p>If one or more action is added with this method, any action added by
* {@link NotificationCompat.Builder#addAction(int, CharSequence, PendingIntent)} will be
* ignored.
*
* <p>This method is equivalent to
* {@link NotificationCompat.Builder#addAction(int, CharSequence, PendingIntent)} for the
* car screen.
*
* @param icon resource ID of a drawable that represents the action. In order to
* display the actions properly, a valid resource id for the icon must be
* provided
* @param title text describing the action
* @param intent {@link PendingIntent} to send when the action is invoked. In the case of
* navigation notifications in the rail widget, this intent will be sent
* when the user taps on the action icon in the rail
* widget
* @throws NullPointerException if {@code title} or {@code intent} are {@code null}
*/
@SuppressWarnings("deprecation")
@NonNull
public Builder addAction(
@DrawableRes int icon, @NonNull CharSequence title, @NonNull PendingIntent intent) {
mActions.add(new Action(icon, requireNonNull(title), requireNonNull(intent)));
return this;
}
/**
* For Android Auto only, sets the importance of the notification in the car screen.
*
* <p>The default value is {@link NotificationManagerCompat#IMPORTANCE_UNSPECIFIED},
* and will not be used to override.
*
* <p>The importance is used to determine whether the notification will show as a HUN on
* the car screen. See the class description for more details.
*
* <p>See {@link NotificationManagerCompat} for all supported importance values.
*
* @see #setChannelId(String)
*/
@NonNull
public Builder setImportance(int importance) {
mImportance = importance;
return this;
}
/**
* Sets the background color of the notification in the car screen.
*
* <p>This method is equivalent to {@link NotificationCompat.Builder#setColor(int)} for
* the car screen.
*
* <p>This color is only used for navigation notifications. See the "Navigation" section
* of {@link CarAppExtender} for more details.
*
* @throws NullPointerException if {@code color} is {@code null}
*/
@NonNull
public Builder setColor(@NonNull CarColor color) {
mColor = requireNonNull(color);
return this;
}
/**
* For Android Automotive OS only, sets the channel id of the notification channel to be
* used in the car.
*
* <p>This is used in the case where your notification is to have a different importance
* in the car then it does on the phone.
*
* <p>It is used for the same purposes you'd use {@link #setImportance(int)} for
* Auto.
*
* @see #setImportance(int)
*/
@NonNull
public Builder setChannelId(@NonNull String channelId) {
mChannelId = channelId;
return this;
}
/**
* Constructs the {@link CarAppExtender} defined by this builder.
*/
@NonNull
public CarAppExtender build() {
return new CarAppExtender(this);
}
/** Creates an empty {@link Builder} instance. */
public Builder() {
}
}
}