Add CarAppExtender to car app library
Relnote: N/A
Bug: 170982012
Test: ./gradlew car:app:app:connectedAndroidTest
Change-Id: Ie393b505e8d6895337282e2a4be8568cfdf8fc03
diff --git a/car/app/app/api/current.txt b/car/app/app/api/current.txt
index bfa3df9..a9eecc8 100644
--- a/car/app/app/api/current.txt
+++ b/car/app/app/api/current.txt
@@ -1032,6 +1032,40 @@
}
+package androidx.car.app.notification {
+
+ public class CarAppExtender implements androidx.core.app.NotificationCompat.Extender {
+ ctor public CarAppExtender(android.app.Notification);
+ method public static androidx.car.app.notification.CarAppExtender.Builder builder();
+ method public androidx.core.app.NotificationCompat.Builder extend(androidx.core.app.NotificationCompat.Builder);
+ method public java.util.List<android.app.Notification.Action!> getActions();
+ method public android.app.PendingIntent? getContentIntent();
+ method public CharSequence? getContentText();
+ method public CharSequence? getContentTitle();
+ method public android.app.PendingIntent? getDeleteIntent();
+ method public int getImportance();
+ method public android.graphics.Bitmap? getLargeIconBitmap();
+ method public int getSmallIconResId();
+ method public boolean isExtended();
+ method public static boolean isExtended(android.app.Notification);
+ }
+
+ public static final class CarAppExtender.Builder {
+ ctor public CarAppExtender.Builder();
+ method public androidx.car.app.notification.CarAppExtender.Builder addAction(@DrawableRes int, CharSequence, android.app.PendingIntent);
+ method public androidx.car.app.notification.CarAppExtender build();
+ method public androidx.car.app.notification.CarAppExtender.Builder clearActions();
+ method public androidx.car.app.notification.CarAppExtender.Builder setContentIntent(android.app.PendingIntent?);
+ method public androidx.car.app.notification.CarAppExtender.Builder setContentText(CharSequence?);
+ method public androidx.car.app.notification.CarAppExtender.Builder setContentTitle(CharSequence?);
+ method public androidx.car.app.notification.CarAppExtender.Builder setDeleteIntent(android.app.PendingIntent?);
+ method public androidx.car.app.notification.CarAppExtender.Builder setImportance(int);
+ method public androidx.car.app.notification.CarAppExtender.Builder setLargeIcon(android.graphics.Bitmap?);
+ method public androidx.car.app.notification.CarAppExtender.Builder setSmallIcon(int);
+ }
+
+}
+
package androidx.car.app.serialization {
public final class Bundleable implements android.os.Parcelable {
diff --git a/car/app/app/api/public_plus_experimental_current.txt b/car/app/app/api/public_plus_experimental_current.txt
index bfa3df9..a9eecc8 100644
--- a/car/app/app/api/public_plus_experimental_current.txt
+++ b/car/app/app/api/public_plus_experimental_current.txt
@@ -1032,6 +1032,40 @@
}
+package androidx.car.app.notification {
+
+ public class CarAppExtender implements androidx.core.app.NotificationCompat.Extender {
+ ctor public CarAppExtender(android.app.Notification);
+ method public static androidx.car.app.notification.CarAppExtender.Builder builder();
+ method public androidx.core.app.NotificationCompat.Builder extend(androidx.core.app.NotificationCompat.Builder);
+ method public java.util.List<android.app.Notification.Action!> getActions();
+ method public android.app.PendingIntent? getContentIntent();
+ method public CharSequence? getContentText();
+ method public CharSequence? getContentTitle();
+ method public android.app.PendingIntent? getDeleteIntent();
+ method public int getImportance();
+ method public android.graphics.Bitmap? getLargeIconBitmap();
+ method public int getSmallIconResId();
+ method public boolean isExtended();
+ method public static boolean isExtended(android.app.Notification);
+ }
+
+ public static final class CarAppExtender.Builder {
+ ctor public CarAppExtender.Builder();
+ method public androidx.car.app.notification.CarAppExtender.Builder addAction(@DrawableRes int, CharSequence, android.app.PendingIntent);
+ method public androidx.car.app.notification.CarAppExtender build();
+ method public androidx.car.app.notification.CarAppExtender.Builder clearActions();
+ method public androidx.car.app.notification.CarAppExtender.Builder setContentIntent(android.app.PendingIntent?);
+ method public androidx.car.app.notification.CarAppExtender.Builder setContentText(CharSequence?);
+ method public androidx.car.app.notification.CarAppExtender.Builder setContentTitle(CharSequence?);
+ method public androidx.car.app.notification.CarAppExtender.Builder setDeleteIntent(android.app.PendingIntent?);
+ method public androidx.car.app.notification.CarAppExtender.Builder setImportance(int);
+ method public androidx.car.app.notification.CarAppExtender.Builder setLargeIcon(android.graphics.Bitmap?);
+ method public androidx.car.app.notification.CarAppExtender.Builder setSmallIcon(int);
+ }
+
+}
+
package androidx.car.app.serialization {
public final class Bundleable implements android.os.Parcelable {
diff --git a/car/app/app/api/restricted_current.txt b/car/app/app/api/restricted_current.txt
index bfa3df9..a9eecc8 100644
--- a/car/app/app/api/restricted_current.txt
+++ b/car/app/app/api/restricted_current.txt
@@ -1032,6 +1032,40 @@
}
+package androidx.car.app.notification {
+
+ public class CarAppExtender implements androidx.core.app.NotificationCompat.Extender {
+ ctor public CarAppExtender(android.app.Notification);
+ method public static androidx.car.app.notification.CarAppExtender.Builder builder();
+ method public androidx.core.app.NotificationCompat.Builder extend(androidx.core.app.NotificationCompat.Builder);
+ method public java.util.List<android.app.Notification.Action!> getActions();
+ method public android.app.PendingIntent? getContentIntent();
+ method public CharSequence? getContentText();
+ method public CharSequence? getContentTitle();
+ method public android.app.PendingIntent? getDeleteIntent();
+ method public int getImportance();
+ method public android.graphics.Bitmap? getLargeIconBitmap();
+ method public int getSmallIconResId();
+ method public boolean isExtended();
+ method public static boolean isExtended(android.app.Notification);
+ }
+
+ public static final class CarAppExtender.Builder {
+ ctor public CarAppExtender.Builder();
+ method public androidx.car.app.notification.CarAppExtender.Builder addAction(@DrawableRes int, CharSequence, android.app.PendingIntent);
+ method public androidx.car.app.notification.CarAppExtender build();
+ method public androidx.car.app.notification.CarAppExtender.Builder clearActions();
+ method public androidx.car.app.notification.CarAppExtender.Builder setContentIntent(android.app.PendingIntent?);
+ method public androidx.car.app.notification.CarAppExtender.Builder setContentText(CharSequence?);
+ method public androidx.car.app.notification.CarAppExtender.Builder setContentTitle(CharSequence?);
+ method public androidx.car.app.notification.CarAppExtender.Builder setDeleteIntent(android.app.PendingIntent?);
+ method public androidx.car.app.notification.CarAppExtender.Builder setImportance(int);
+ method public androidx.car.app.notification.CarAppExtender.Builder setLargeIcon(android.graphics.Bitmap?);
+ method public androidx.car.app.notification.CarAppExtender.Builder setSmallIcon(int);
+ }
+
+}
+
package androidx.car.app.serialization {
public final class Bundleable implements android.os.Parcelable {
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/notification/CarAppExtenderTest.java b/car/app/app/src/androidTest/java/androidx/car/app/notification/CarAppExtenderTest.java
new file mode 100644
index 0000000..72ef309
--- /dev/null
+++ b/car/app/app/src/androidTest/java/androidx/car/app/notification/CarAppExtenderTest.java
@@ -0,0 +1,247 @@
+/*
+ * 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 com.google.common.truth.Truth.assertThat;
+
+import android.app.Notification.Action;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.os.Bundle;
+
+import androidx.annotation.NonNull;
+import androidx.car.app.test.R;
+import androidx.core.app.NotificationCompat;
+import androidx.core.app.NotificationManagerCompat;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.List;
+
+/** Tests for {@link CarAppExtender}. */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public final class CarAppExtenderTest {
+ private static final String NOTIFICATION_CHANNEL_ID = "test carextender channel id";
+ private static final String INTENT_PRIMARY_ACTION =
+ "androidx.car.app.INTENT_PRIMARY_ACTION";
+ private static final String INTENT_SECONDARY_ACTION =
+ "androidx.car.app.INTENT_SECONDARY_ACTION";
+
+ private final Context mContext = ApplicationProvider.getApplicationContext();
+
+ @Test
+ public void carAppExtender_checkDefaultValues() {
+ NotificationCompat.Builder builder =
+ new NotificationCompat.Builder(mContext, NOTIFICATION_CHANNEL_ID)
+ .extend(
+ // Simulate sending a notification that has the same bundle key
+ // but no value is set.
+ new NotificationCompat.Extender() {
+ @NonNull
+ @Override
+ public NotificationCompat.Builder extend(
+ @NonNull NotificationCompat.Builder builder) {
+ Bundle carExtensions = new Bundle();
+
+ builder.getExtras().putBundle("android.car.EXTENSIONS",
+ carExtensions);
+ return builder;
+ }
+ });
+
+ CarAppExtender carAppExtender = new CarAppExtender(builder.build());
+ assertThat(carAppExtender.isExtended()).isFalse();
+ assertThat(carAppExtender.getContentTitle()).isNull();
+ assertThat(carAppExtender.getContentText()).isNull();
+ assertThat(carAppExtender.getSmallIconResId()).isEqualTo(0);
+ assertThat(carAppExtender.getLargeIconBitmap()).isNull();
+ assertThat(carAppExtender.getContentIntent()).isNull();
+ assertThat(carAppExtender.getDeleteIntent()).isNull();
+ assertThat(carAppExtender.getActions()).isEmpty();
+ assertThat(carAppExtender.getImportance())
+ .isEqualTo(NotificationManagerCompat.IMPORTANCE_UNSPECIFIED);
+ }
+
+ @Test
+ public void notification_extended() {
+ NotificationCompat.Builder builder =
+ new NotificationCompat.Builder(mContext, NOTIFICATION_CHANNEL_ID)
+ .extend(CarAppExtender.builder().build());
+
+ assertThat(CarAppExtender.isExtended(builder.build())).isTrue();
+ }
+
+ @Test
+ public void notification_notExtended() {
+ NotificationCompat.Builder builder =
+ new NotificationCompat.Builder(mContext, NOTIFICATION_CHANNEL_ID);
+
+ assertThat(CarAppExtender.isExtended(builder.build())).isFalse();
+ }
+
+ @Test
+ public void notification_extended_setTitle() {
+ CharSequence title = "TestTitle";
+ NotificationCompat.Builder builder =
+ new NotificationCompat.Builder(mContext, NOTIFICATION_CHANNEL_ID)
+ .extend(CarAppExtender.builder().setContentTitle(title).build());
+
+ assertThat(
+ title.toString().contentEquals(
+ new CarAppExtender(builder.build()).getContentTitle()))
+ .isTrue();
+ }
+
+ @Test
+ public void notification_extended_setText() {
+ CharSequence text = "TestText";
+ NotificationCompat.Builder builder =
+ new NotificationCompat.Builder(mContext, NOTIFICATION_CHANNEL_ID)
+ .extend(CarAppExtender.builder().setContentText(text).build());
+
+ assertThat(
+ text.toString().contentEquals(new CarAppExtender(builder.build()).getContentText()))
+ .isTrue();
+ }
+
+ @Test
+ public void notification_extended_setSmallIcon() {
+ int resId = R.drawable.ic_test_1;
+ NotificationCompat.Builder builder =
+ new NotificationCompat.Builder(mContext, NOTIFICATION_CHANNEL_ID)
+ .extend(CarAppExtender.builder().setSmallIcon(resId).build());
+
+ assertThat(new CarAppExtender(builder.build()).getSmallIconResId()).isEqualTo(resId);
+ }
+
+ @Test
+ public void notification_extended_setLargeIcon() {
+ Bitmap bitmap = BitmapFactory.decodeResource(mContext.getResources(), R.drawable.ic_test_2);
+ NotificationCompat.Builder builder =
+ new NotificationCompat.Builder(mContext, NOTIFICATION_CHANNEL_ID)
+ .extend(CarAppExtender.builder().setLargeIcon(bitmap).build());
+
+ assertThat(new CarAppExtender(builder.build()).getLargeIconBitmap()).isEqualTo(bitmap);
+ }
+
+ @Test
+ public void notification_extended_setContentIntent() {
+ Intent intent = new Intent(INTENT_PRIMARY_ACTION);
+ PendingIntent contentIntent = PendingIntent.getBroadcast(mContext, 0, intent, 0);
+ NotificationCompat.Builder builder =
+ new NotificationCompat.Builder(mContext, NOTIFICATION_CHANNEL_ID)
+ .extend(CarAppExtender.builder().setContentIntent(contentIntent).build());
+
+ assertThat(new CarAppExtender(builder.build()).getContentIntent()).isEqualTo(contentIntent);
+ }
+
+ @Test
+ public void notification_extended_setDeleteIntent() {
+ Intent intent = new Intent(INTENT_PRIMARY_ACTION);
+ PendingIntent deleteIntent = PendingIntent.getBroadcast(mContext, 0, intent, 0);
+ NotificationCompat.Builder builder =
+ new NotificationCompat.Builder(mContext, NOTIFICATION_CHANNEL_ID)
+ .extend(CarAppExtender.builder().setDeleteIntent(deleteIntent).build());
+
+ assertThat(new CarAppExtender(builder.build()).getDeleteIntent()).isEqualTo(deleteIntent);
+ }
+
+ @Test
+ public void notification_extended_noActions() {
+ NotificationCompat.Builder builder =
+ new NotificationCompat.Builder(mContext, NOTIFICATION_CHANNEL_ID)
+ .extend(CarAppExtender.builder().build());
+
+ assertThat(new CarAppExtender(builder.build()).getActions()).isEmpty();
+ }
+
+ @Test
+ public void notification_extended_addActions() {
+ int icon1 = R.drawable.ic_test_1;
+ CharSequence title1 = "FirstAction";
+ Intent intent1 = new Intent(INTENT_PRIMARY_ACTION);
+ PendingIntent actionIntent1 = PendingIntent.getBroadcast(mContext, 0, intent1, 0);
+
+ int icon2 = R.drawable.ic_test_2;
+ CharSequence title2 = "SecondAction";
+ Intent intent2 = new Intent(INTENT_SECONDARY_ACTION);
+ PendingIntent actionIntent2 = PendingIntent.getBroadcast(mContext, 0, intent2, 0);
+
+ NotificationCompat.Builder builder =
+ new NotificationCompat.Builder(mContext, NOTIFICATION_CHANNEL_ID)
+ .extend(
+ CarAppExtender.builder()
+ .addAction(icon1, title1, actionIntent1)
+ .addAction(icon2, title2, actionIntent2)
+ .build());
+
+ List<Action> actions = new CarAppExtender(builder.build()).getActions();
+ assertThat(actions).hasSize(2);
+ assertThat(actions.get(0).getIcon().getResId()).isEqualTo(icon1);
+ assertThat(title1.toString().contentEquals(actions.get(0).title)).isTrue();
+ assertThat(actions.get(0).actionIntent).isEqualTo(actionIntent1);
+ assertThat(actions.get(1).getIcon().getResId()).isEqualTo(icon2);
+ assertThat(title2.toString().contentEquals(actions.get(1).title)).isTrue();
+ assertThat(actions.get(1).actionIntent).isEqualTo(actionIntent2);
+ }
+
+ @Test
+ public void notification_extended_clearActions() {
+ int icon1 = R.drawable.ic_test_1;
+ CharSequence title1 = "FirstAction";
+ Intent intent1 = new Intent(INTENT_PRIMARY_ACTION);
+ PendingIntent actionIntent1 = PendingIntent.getBroadcast(mContext, 0, intent1, 0);
+
+ int icon2 = R.drawable.ic_test_2;
+ CharSequence title2 = "SecondAction";
+ Intent intent2 = new Intent(INTENT_SECONDARY_ACTION);
+ PendingIntent actionIntent2 = PendingIntent.getBroadcast(mContext, 0, intent2, 0);
+
+ NotificationCompat.Builder builder =
+ new NotificationCompat.Builder(mContext, NOTIFICATION_CHANNEL_ID)
+ .extend(
+ CarAppExtender.builder()
+ .addAction(icon1, title1, actionIntent1)
+ .addAction(icon2, title2, actionIntent2)
+ .clearActions()
+ .build());
+
+ List<Action> actions = new CarAppExtender(builder.build()).getActions();
+ assertThat(actions).isEmpty();
+ }
+
+ @Test
+ public void notification_extended_setImportance() {
+ NotificationCompat.Builder builder =
+ new NotificationCompat.Builder(mContext, NOTIFICATION_CHANNEL_ID)
+ .extend(
+ CarAppExtender.builder()
+ .setImportance(NotificationManagerCompat.IMPORTANCE_HIGH)
+ .build());
+
+ assertThat(new CarAppExtender(builder.build()).getImportance())
+ .isEqualTo(NotificationManagerCompat.IMPORTANCE_HIGH);
+ }
+}
diff --git a/car/app/app/src/main/java/androidx/car/app/notification/CarAppExtender.java b/car/app/app/src/main/java/androidx/car/app/notification/CarAppExtender.java
new file mode 100644
index 0000000..05fc5a1
--- /dev/null
+++ b/car/app/app/src/main/java/androidx/car/app/notification/CarAppExtender.java
@@ -0,0 +1,529 @@
+/*
+ * 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 androidx.annotation.DrawableRes;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+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} with {@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
+ * NotificationManagerCompat.notify(...)} methods and not the {@code
+ * NotificationManager.notify(...)} methods.
+ * </ol>
+ *
+ * <pre class="prettyprint">
+ * Notification notification = new NotificationCompat.Builder(context)
+ * ...
+ * .extend(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)}. The car screen
+ * UI is affected in the following ways:
+ *
+ * <ul>
+ * <li>The same heads-up-notification (HUN) behavior as regular notifications.
+ * <li>A rail widget at the bottom of the screen will show when the navigation app is in the
+ * background.
+ * </ul>
+ *
+ * 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 class CarAppExtender implements NotificationCompat.Extender {
+ private static final String EXTRA_CAR_EXTENDER = "android.car.EXTENSIONS";
+ private static final String EXTRA_IS_EXTENDED = "android.car.app.EXTENDED";
+ 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 boolean mIsExtended;
+ @Nullable
+ private CharSequence mContentTitle;
+ @Nullable
+ private CharSequence mContentText;
+ private int mSmallIconResId;
+ @Nullable
+ private Bitmap mLargeIconBitmap;
+ @Nullable
+ private PendingIntent mContentIntent;
+ @Nullable
+ private PendingIntent mDeleteIntent;
+ private ArrayList<Action> mActions;
+ private int mImportance;
+
+ /** Creates a {@link CarAppExtender.Builder}. */
+ @NonNull
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ /**
+ * Creates a {@link CarAppExtender} from the {@link CarAppExtender} of an existing notification.
+ */
+ 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;
+ }
+
+ mIsExtended = carBundle.getBoolean(EXTRA_IS_EXTENDED);
+ 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);
+ this.mActions = actions == null ? new ArrayList<>() : actions;
+ mImportance =
+ carBundle.getInt(EXTRA_IMPORTANCE,
+ NotificationManagerCompat.IMPORTANCE_UNSPECIFIED);
+ }
+
+ private CarAppExtender(Builder builder) {
+ this.mContentTitle = builder.mContentTitle;
+ this.mContentText = builder.mContentText;
+ this.mSmallIconResId = builder.mSmallIconResId;
+ this.mLargeIconBitmap = builder.mLargeIconBitmap;
+ this.mContentIntent = builder.mContentIntent;
+ this.mDeleteIntent = builder.mDeleteIntent;
+ this.mActions = builder.mActions;
+ this.mImportance = builder.mImportance;
+ }
+
+ /**
+ * Applies car extensions to a notification that is being built. 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();
+
+ carExtensions.putBoolean(EXTRA_IS_EXTENDED, true);
+
+ 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.isEmpty()) {
+ carExtensions.putParcelableArrayList(EXTRA_ACTIONS, mActions);
+ }
+
+ carExtensions.putInt(EXTRA_IMPORTANCE, mImportance);
+
+ builder.getExtras().putBundle(EXTRA_CAR_EXTENDER, carExtensions);
+ return builder;
+ }
+
+ /**
+ * Returns {@code true} if the notification was extended with {@link CarAppExtender}, {@code
+ * false} otherwise.
+ */
+ public boolean isExtended() {
+ return mIsExtended;
+ }
+
+ /**
+ * Returns {@code true} if the notification was extended with {@link CarAppExtender}, {@code
+ * false} otherwise.
+ *
+ * @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;
+ }
+
+ extras = extras.getBundle(EXTRA_CAR_EXTENDER);
+ return extras != null && extras.getBoolean(EXTRA_IS_EXTENDED);
+ }
+
+ /**
+ * Gets the content title for the notification.
+ *
+ * @see Builder#setContentTitle
+ */
+ @Nullable
+ public CharSequence getContentTitle() {
+ return mContentTitle;
+ }
+
+ /**
+ * Returns the content text of the notification.
+ *
+ * @see Builder#setContentText
+ */
+ @Nullable
+ public CharSequence getContentText() {
+ return mContentText;
+ }
+
+ /**
+ * Gets the resource ID of the small icon drawable to use.
+ *
+ * @see Builder#setSmallIcon(int)
+ */
+ public int getSmallIconResId() {
+ return mSmallIconResId;
+ }
+
+ /**
+ * Gets the large icon bitmap to display in the notification.
+ *
+ * @see Builder#setLargeIcon(Bitmap)
+ */
+ @Nullable
+ public Bitmap getLargeIconBitmap() {
+ return mLargeIconBitmap;
+ }
+
+ /**
+ * Gets the {@link PendingIntent} to send when the notification is clicked in the car, or {@code
+ * null} if one is not set.
+ *
+ * @see Builder#setContentIntent(PendingIntent)
+ */
+ @Nullable
+ public PendingIntent getContentIntent() {
+ return mContentIntent;
+ }
+
+ /**
+ * Gets the {@link PendingIntent} to send when the notification is cleared by the user, or
+ * {@code null} if one is not set.
+ *
+ * @see Builder#setDeleteIntent(PendingIntent)
+ */
+ @Nullable
+ public PendingIntent getDeleteIntent() {
+ return mDeleteIntent;
+ }
+
+ /**
+ * Gets the list of {@link Action} present on this car notification.
+ *
+ * @see Builder#addAction(int, CharSequence, PendingIntent)
+ */
+ @NonNull
+ public List<Action> getActions() {
+ return mActions;
+ }
+
+ /**
+ * Gets the importance of the notification in the car screen.
+ *
+ * @see Builder#setImportance(int)
+ */
+ public int getImportance() {
+ return mImportance;
+ }
+
+ /** A builder of {@link CarAppExtender}. */
+ public static final class Builder {
+ @Nullable
+ private CharSequence mContentTitle;
+ @Nullable
+ private CharSequence mContentText;
+ private int mSmallIconResId;
+ @Nullable
+ private Bitmap mLargeIconBitmap;
+ @Nullable
+ private PendingIntent mContentIntent;
+ @Nullable
+ private PendingIntent mDeleteIntent;
+ private final ArrayList<Action> mActions = new ArrayList<>();
+ private int mImportance = NotificationManagerCompat.IMPORTANCE_UNSPECIFIED;
+
+ /**
+ * Sets the title of the notification in the car screen, or {@code null} to not override the
+ * notification title.
+ *
+ * <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.
+ */
+ @NonNull
+ public Builder setContentTitle(@Nullable CharSequence contentTitle) {
+ this.mContentTitle = contentTitle;
+ return this;
+ }
+
+ /**
+ * Sets the content text of the notification in the car screen, or {@code null} to not
+ * override the content text.
+ *
+ * <p>This method is equivalent to
+ * {@link NotificationCompat.Builder#setContentText(CharSequence)} for the car screen.
+ *
+ * @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(@Nullable CharSequence contentText) {
+ this.mContentText = contentText;
+ return this;
+ }
+
+ /**
+ * Sets the small icon of the notification in the car screen, or
+ * {@link Resources#ID_NULL} to not override the notification small icon.
+ *
+ * <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.
+ */
+ // TODO(rampara): Revisit small icon getter API
+ @SuppressWarnings("MissingGetterMatchingBuilder")
+ @NonNull
+ public Builder setSmallIcon(int iconResId) {
+ this.mSmallIconResId = iconResId;
+ return this;
+ }
+
+ /**
+ * Sets the large icon of the notification in the car screen, or {@code null} to not
+ * override the large icon of the notification.
+ *
+ * <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.
+ */
+ // TODO(rampara): Revisit small icon getter API
+ @SuppressWarnings("MissingGetterMatchingBuilder")
+ @NonNull
+ public Builder setLargeIcon(@Nullable Bitmap bitmap) {
+ this.mLargeIconBitmap = bitmap;
+ return this;
+ }
+
+ /**
+ * Supplies a {@link PendingIntent} to send when the notification is clicked in the car.
+ *
+ * <p>If set to {@code null}, 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.
+ */
+ @NonNull
+ public Builder setContentIntent(@Nullable PendingIntent contentIntent) {
+ this.mContentIntent = 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 set to {@code null}, 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.
+ */
+ @NonNull
+ public Builder setDeleteIntent(@Nullable PendingIntent deleteIntent) {
+ this.mDeleteIntent = 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} is {@code null}.
+ * @throws NullPointerException if {@code intent} is {@code null}.
+ */
+ // TODO(rampara): Update to remove use of deprecated Action API
+ @SuppressWarnings("deprecation")
+ @NonNull
+ public Builder addAction(
+ @DrawableRes int icon, @NonNull CharSequence title, @NonNull PendingIntent intent) {
+ this.mActions.add(new Action(icon, requireNonNull(title), requireNonNull(intent)));
+ return this;
+ }
+
+ /** Clears any actions that may have been added with {@link #addAction} up to this point. */
+ @NonNull
+ public Builder clearActions() {
+ this.mActions.clear();
+ return this;
+ }
+
+ /**
+ * Sets the importance of the notification in the car screen.
+ *
+ * <p>The default value is {@link NotificationManagerCompat#IMPORTANCE_UNSPECIFIED}.
+ *
+ * <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.
+ */
+ @NonNull
+ public Builder setImportance(int importance) {
+ this.mImportance = importance;
+ return this;
+ }
+
+ /**
+ * Constructs the {@link CarAppExtender} defined by this builder.
+ */
+ @NonNull
+ public CarAppExtender build() {
+ return new CarAppExtender(this);
+ }
+ }
+}