[go: nahoru, domu]

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);
+        }
+    }
+}