[go: nahoru, domu]

Adds CallStyle to NotificationCompat

Adds CallStyle to NotificationCompat. Adds support for use of
Notification.CallStyle in API version 31 and higher, and creates an
unstyled notification that mimics the look of CallStyle in versions
lower than 31. Also adds testing to cover these cases.

Test: Added unit test coverage for all new class uses
Bug: 199294989
Change-Id: Id9a5321ab9172a004215c85aa2e6d7165a01e074
Relnote: Adds Callstyle to NotificationCompat
diff --git a/core/core/api/current.txt b/core/core/api/current.txt
index 4a48718..c27adf82 100644
--- a/core/core/api/current.txt
+++ b/core/core/api/current.txt
@@ -306,9 +306,15 @@
     field public static final int DEFAULT_LIGHTS = 4; // 0x4
     field public static final int DEFAULT_SOUND = 1; // 0x1
     field public static final int DEFAULT_VIBRATE = 2; // 0x2
+    field public static final String EXTRA_ANSWER_COLOR = "android.answerColor";
+    field public static final String EXTRA_ANSWER_INTENT = "android.answerIntent";
     field public static final String EXTRA_AUDIO_CONTENTS_URI = "android.audioContents";
     field public static final String EXTRA_BACKGROUND_IMAGE_URI = "android.backgroundImageUri";
     field public static final String EXTRA_BIG_TEXT = "android.bigText";
+    field public static final String EXTRA_CALL_IS_VIDEO = "android.callIsVideo";
+    field public static final String EXTRA_CALL_PERSON = "android.callPerson";
+    field public static final String EXTRA_CALL_PERSON_COMPAT = "android.callPersonCompat";
+    field public static final String EXTRA_CALL_TYPE = "android.callType";
     field public static final String EXTRA_CHANNEL_GROUP_ID = "android.intent.extra.CHANNEL_GROUP_ID";
     field public static final String EXTRA_CHANNEL_ID = "android.intent.extra.CHANNEL_ID";
     field public static final String EXTRA_CHRONOMETER_COUNT_DOWN = "android.chronometerCountDown";
@@ -316,6 +322,9 @@
     field public static final String EXTRA_COMPACT_ACTIONS = "android.compactActions";
     field public static final String EXTRA_COMPAT_TEMPLATE = "androidx.core.app.extra.COMPAT_TEMPLATE";
     field public static final String EXTRA_CONVERSATION_TITLE = "android.conversationTitle";
+    field public static final String EXTRA_DECLINE_COLOR = "android.declineColor";
+    field public static final String EXTRA_DECLINE_INTENT = "android.declineIntent";
+    field public static final String EXTRA_HANG_UP_INTENT = "android.hangUpIntent";
     field public static final String EXTRA_HIDDEN_CONVERSATION_TITLE = "android.hiddenConversationTitle";
     field public static final String EXTRA_HISTORIC_MESSAGES = "android.messages.historic";
     field public static final String EXTRA_INFO_TEXT = "android.infoText";
@@ -348,6 +357,9 @@
     field public static final String EXTRA_TEXT_LINES = "android.textLines";
     field public static final String EXTRA_TITLE = "android.title";
     field public static final String EXTRA_TITLE_BIG = "android.title.big";
+    field public static final String EXTRA_VERIFICATION_ICON = "android.verificationIcon";
+    field public static final String EXTRA_VERIFICATION_ICON_COMPAT = "android.verificationIconCompat";
+    field public static final String EXTRA_VERIFICATION_TEXT = "android.verificationText";
     field public static final int FLAG_AUTO_CANCEL = 16; // 0x10
     field public static final int FLAG_BUBBLE = 4096; // 0x1000
     field public static final int FLAG_FOREGROUND_SERVICE = 64; // 0x40
@@ -404,7 +416,7 @@
     field public static final int SEMANTIC_ACTION_THUMBS_DOWN = 9; // 0x9
     field public static final int SEMANTIC_ACTION_THUMBS_UP = 8; // 0x8
     field public static final int SEMANTIC_ACTION_UNMUTE = 7; // 0x7
-    field public android.app.PendingIntent! actionIntent;
+    field public android.app.PendingIntent? actionIntent;
     field @Deprecated public int icon;
     field public CharSequence! title;
   }
@@ -580,6 +592,24 @@
     field @Deprecated public java.util.ArrayList<java.lang.String!>! mPeople;
   }
 
+  public static class NotificationCompat.CallStyle extends androidx.core.app.NotificationCompat.Style {
+    ctor public NotificationCompat.CallStyle();
+    ctor public NotificationCompat.CallStyle(androidx.core.app.NotificationCompat.Builder?);
+    method public static androidx.core.app.NotificationCompat.CallStyle forIncomingCall(androidx.core.app.Person, android.app.PendingIntent, android.app.PendingIntent);
+    method public static androidx.core.app.NotificationCompat.CallStyle forOngoingCall(androidx.core.app.Person, android.app.PendingIntent);
+    method public static androidx.core.app.NotificationCompat.CallStyle forScreeningCall(androidx.core.app.Person, android.app.PendingIntent, android.app.PendingIntent);
+    method public androidx.core.app.NotificationCompat.CallStyle setAnswerButtonColorHint(@ColorInt int);
+    method public androidx.core.app.NotificationCompat.CallStyle setDeclineButtonColorHint(@ColorInt int);
+    method public androidx.core.app.NotificationCompat.CallStyle setIsVideo(boolean);
+    method @RequiresApi(23) public androidx.core.app.NotificationCompat.CallStyle setVerificationIcon(android.graphics.drawable.Icon?);
+    method public androidx.core.app.NotificationCompat.CallStyle setVerificationIcon(android.graphics.Bitmap?);
+    method public androidx.core.app.NotificationCompat.CallStyle setVerificationText(CharSequence?);
+    field public static final int CALL_TYPE_INCOMING = 1; // 0x1
+    field public static final int CALL_TYPE_ONGOING = 2; // 0x2
+    field public static final int CALL_TYPE_SCREENING = 3; // 0x3
+    field public static final int CALL_TYPE_UNKNOWN = 0; // 0x0
+  }
+
   public static final class NotificationCompat.CarExtender implements androidx.core.app.NotificationCompat.Extender {
     ctor public NotificationCompat.CarExtender();
     ctor public NotificationCompat.CarExtender(android.app.Notification);
diff --git a/core/core/api/public_plus_experimental_current.txt b/core/core/api/public_plus_experimental_current.txt
index be192dd..4b5b186 100644
--- a/core/core/api/public_plus_experimental_current.txt
+++ b/core/core/api/public_plus_experimental_current.txt
@@ -306,9 +306,15 @@
     field public static final int DEFAULT_LIGHTS = 4; // 0x4
     field public static final int DEFAULT_SOUND = 1; // 0x1
     field public static final int DEFAULT_VIBRATE = 2; // 0x2
+    field public static final String EXTRA_ANSWER_COLOR = "android.answerColor";
+    field public static final String EXTRA_ANSWER_INTENT = "android.answerIntent";
     field public static final String EXTRA_AUDIO_CONTENTS_URI = "android.audioContents";
     field public static final String EXTRA_BACKGROUND_IMAGE_URI = "android.backgroundImageUri";
     field public static final String EXTRA_BIG_TEXT = "android.bigText";
+    field public static final String EXTRA_CALL_IS_VIDEO = "android.callIsVideo";
+    field public static final String EXTRA_CALL_PERSON = "android.callPerson";
+    field public static final String EXTRA_CALL_PERSON_COMPAT = "android.callPersonCompat";
+    field public static final String EXTRA_CALL_TYPE = "android.callType";
     field public static final String EXTRA_CHANNEL_GROUP_ID = "android.intent.extra.CHANNEL_GROUP_ID";
     field public static final String EXTRA_CHANNEL_ID = "android.intent.extra.CHANNEL_ID";
     field public static final String EXTRA_CHRONOMETER_COUNT_DOWN = "android.chronometerCountDown";
@@ -316,6 +322,9 @@
     field public static final String EXTRA_COMPACT_ACTIONS = "android.compactActions";
     field public static final String EXTRA_COMPAT_TEMPLATE = "androidx.core.app.extra.COMPAT_TEMPLATE";
     field public static final String EXTRA_CONVERSATION_TITLE = "android.conversationTitle";
+    field public static final String EXTRA_DECLINE_COLOR = "android.declineColor";
+    field public static final String EXTRA_DECLINE_INTENT = "android.declineIntent";
+    field public static final String EXTRA_HANG_UP_INTENT = "android.hangUpIntent";
     field public static final String EXTRA_HIDDEN_CONVERSATION_TITLE = "android.hiddenConversationTitle";
     field public static final String EXTRA_HISTORIC_MESSAGES = "android.messages.historic";
     field public static final String EXTRA_INFO_TEXT = "android.infoText";
@@ -348,6 +357,9 @@
     field public static final String EXTRA_TEXT_LINES = "android.textLines";
     field public static final String EXTRA_TITLE = "android.title";
     field public static final String EXTRA_TITLE_BIG = "android.title.big";
+    field public static final String EXTRA_VERIFICATION_ICON = "android.verificationIcon";
+    field public static final String EXTRA_VERIFICATION_ICON_COMPAT = "android.verificationIconCompat";
+    field public static final String EXTRA_VERIFICATION_TEXT = "android.verificationText";
     field public static final int FLAG_AUTO_CANCEL = 16; // 0x10
     field public static final int FLAG_BUBBLE = 4096; // 0x1000
     field public static final int FLAG_FOREGROUND_SERVICE = 64; // 0x40
@@ -404,7 +416,7 @@
     field public static final int SEMANTIC_ACTION_THUMBS_DOWN = 9; // 0x9
     field public static final int SEMANTIC_ACTION_THUMBS_UP = 8; // 0x8
     field public static final int SEMANTIC_ACTION_UNMUTE = 7; // 0x7
-    field public android.app.PendingIntent! actionIntent;
+    field public android.app.PendingIntent? actionIntent;
     field @Deprecated public int icon;
     field public CharSequence! title;
   }
@@ -580,6 +592,24 @@
     field @Deprecated public java.util.ArrayList<java.lang.String!>! mPeople;
   }
 
+  public static class NotificationCompat.CallStyle extends androidx.core.app.NotificationCompat.Style {
+    ctor public NotificationCompat.CallStyle();
+    ctor public NotificationCompat.CallStyle(androidx.core.app.NotificationCompat.Builder?);
+    method public static androidx.core.app.NotificationCompat.CallStyle forIncomingCall(androidx.core.app.Person, android.app.PendingIntent, android.app.PendingIntent);
+    method public static androidx.core.app.NotificationCompat.CallStyle forOngoingCall(androidx.core.app.Person, android.app.PendingIntent);
+    method public static androidx.core.app.NotificationCompat.CallStyle forScreeningCall(androidx.core.app.Person, android.app.PendingIntent, android.app.PendingIntent);
+    method public androidx.core.app.NotificationCompat.CallStyle setAnswerButtonColorHint(@ColorInt int);
+    method public androidx.core.app.NotificationCompat.CallStyle setDeclineButtonColorHint(@ColorInt int);
+    method public androidx.core.app.NotificationCompat.CallStyle setIsVideo(boolean);
+    method @RequiresApi(23) public androidx.core.app.NotificationCompat.CallStyle setVerificationIcon(android.graphics.drawable.Icon?);
+    method public androidx.core.app.NotificationCompat.CallStyle setVerificationIcon(android.graphics.Bitmap?);
+    method public androidx.core.app.NotificationCompat.CallStyle setVerificationText(CharSequence?);
+    field public static final int CALL_TYPE_INCOMING = 1; // 0x1
+    field public static final int CALL_TYPE_ONGOING = 2; // 0x2
+    field public static final int CALL_TYPE_SCREENING = 3; // 0x3
+    field public static final int CALL_TYPE_UNKNOWN = 0; // 0x0
+  }
+
   public static final class NotificationCompat.CarExtender implements androidx.core.app.NotificationCompat.Extender {
     ctor public NotificationCompat.CarExtender();
     ctor public NotificationCompat.CarExtender(android.app.Notification);
diff --git a/core/core/api/restricted_current.txt b/core/core/api/restricted_current.txt
index 1ec3d1d..10ec3bf 100644
--- a/core/core/api/restricted_current.txt
+++ b/core/core/api/restricted_current.txt
@@ -352,9 +352,15 @@
     field public static final int DEFAULT_LIGHTS = 4; // 0x4
     field public static final int DEFAULT_SOUND = 1; // 0x1
     field public static final int DEFAULT_VIBRATE = 2; // 0x2
+    field public static final String EXTRA_ANSWER_COLOR = "android.answerColor";
+    field public static final String EXTRA_ANSWER_INTENT = "android.answerIntent";
     field public static final String EXTRA_AUDIO_CONTENTS_URI = "android.audioContents";
     field public static final String EXTRA_BACKGROUND_IMAGE_URI = "android.backgroundImageUri";
     field public static final String EXTRA_BIG_TEXT = "android.bigText";
+    field public static final String EXTRA_CALL_IS_VIDEO = "android.callIsVideo";
+    field public static final String EXTRA_CALL_PERSON = "android.callPerson";
+    field public static final String EXTRA_CALL_PERSON_COMPAT = "android.callPersonCompat";
+    field public static final String EXTRA_CALL_TYPE = "android.callType";
     field public static final String EXTRA_CHANNEL_GROUP_ID = "android.intent.extra.CHANNEL_GROUP_ID";
     field public static final String EXTRA_CHANNEL_ID = "android.intent.extra.CHANNEL_ID";
     field public static final String EXTRA_CHRONOMETER_COUNT_DOWN = "android.chronometerCountDown";
@@ -362,6 +368,9 @@
     field public static final String EXTRA_COMPACT_ACTIONS = "android.compactActions";
     field public static final String EXTRA_COMPAT_TEMPLATE = "androidx.core.app.extra.COMPAT_TEMPLATE";
     field public static final String EXTRA_CONVERSATION_TITLE = "android.conversationTitle";
+    field public static final String EXTRA_DECLINE_COLOR = "android.declineColor";
+    field public static final String EXTRA_DECLINE_INTENT = "android.declineIntent";
+    field public static final String EXTRA_HANG_UP_INTENT = "android.hangUpIntent";
     field public static final String EXTRA_HIDDEN_CONVERSATION_TITLE = "android.hiddenConversationTitle";
     field public static final String EXTRA_HISTORIC_MESSAGES = "android.messages.historic";
     field public static final String EXTRA_INFO_TEXT = "android.infoText";
@@ -394,6 +403,9 @@
     field public static final String EXTRA_TEXT_LINES = "android.textLines";
     field public static final String EXTRA_TITLE = "android.title";
     field public static final String EXTRA_TITLE_BIG = "android.title.big";
+    field public static final String EXTRA_VERIFICATION_ICON = "android.verificationIcon";
+    field public static final String EXTRA_VERIFICATION_ICON_COMPAT = "android.verificationIconCompat";
+    field public static final String EXTRA_VERIFICATION_TEXT = "android.verificationText";
     field public static final int FLAG_AUTO_CANCEL = 16; // 0x10
     field public static final int FLAG_BUBBLE = 4096; // 0x1000
     field public static final int FLAG_FOREGROUND_SERVICE = 64; // 0x40
@@ -413,6 +425,7 @@
     field public static final int GROUP_ALERT_SUMMARY = 1; // 0x1
     field public static final String GROUP_KEY_SILENT = "silent";
     field public static final String INTENT_CATEGORY_NOTIFICATION_PREFERENCES = "android.intent.category.NOTIFICATION_PREFERENCES";
+    field @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static final int MAX_ACTION_BUTTONS = 3; // 0x3
     field public static final int PRIORITY_DEFAULT = 0; // 0x0
     field public static final int PRIORITY_HIGH = 1; // 0x1
     field public static final int PRIORITY_LOW = -1; // 0xffffffff
@@ -450,7 +463,7 @@
     field public static final int SEMANTIC_ACTION_THUMBS_DOWN = 9; // 0x9
     field public static final int SEMANTIC_ACTION_THUMBS_UP = 8; // 0x8
     field public static final int SEMANTIC_ACTION_UNMUTE = 7; // 0x7
-    field public android.app.PendingIntent! actionIntent;
+    field public android.app.PendingIntent? actionIntent;
     field @Deprecated public int icon;
     field public CharSequence! title;
   }
@@ -643,6 +656,28 @@
     field @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public java.util.ArrayList<androidx.core.app.Person!> mPersonList;
   }
 
+  public static class NotificationCompat.CallStyle extends androidx.core.app.NotificationCompat.Style {
+    ctor public NotificationCompat.CallStyle();
+    ctor public NotificationCompat.CallStyle(androidx.core.app.NotificationCompat.Builder?);
+    method public static androidx.core.app.NotificationCompat.CallStyle forIncomingCall(androidx.core.app.Person, android.app.PendingIntent, android.app.PendingIntent);
+    method public static androidx.core.app.NotificationCompat.CallStyle forOngoingCall(androidx.core.app.Person, android.app.PendingIntent);
+    method public static androidx.core.app.NotificationCompat.CallStyle forScreeningCall(androidx.core.app.Person, android.app.PendingIntent, android.app.PendingIntent);
+    method @RequiresApi(20) @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public java.util.ArrayList<androidx.core.app.NotificationCompat.Action!> getActionsListWithSystemActions();
+    method public androidx.core.app.NotificationCompat.CallStyle setAnswerButtonColorHint(@ColorInt int);
+    method public androidx.core.app.NotificationCompat.CallStyle setDeclineButtonColorHint(@ColorInt int);
+    method public androidx.core.app.NotificationCompat.CallStyle setIsVideo(boolean);
+    method @RequiresApi(23) public androidx.core.app.NotificationCompat.CallStyle setVerificationIcon(android.graphics.drawable.Icon?);
+    method public androidx.core.app.NotificationCompat.CallStyle setVerificationIcon(android.graphics.Bitmap?);
+    method public androidx.core.app.NotificationCompat.CallStyle setVerificationText(CharSequence?);
+    field public static final int CALL_TYPE_INCOMING = 1; // 0x1
+    field public static final int CALL_TYPE_ONGOING = 2; // 0x2
+    field public static final int CALL_TYPE_SCREENING = 3; // 0x3
+    field public static final int CALL_TYPE_UNKNOWN = 0; // 0x0
+  }
+
+  @IntDef({androidx.core.app.NotificationCompat.CallStyle.CALL_TYPE_UNKNOWN, androidx.core.app.NotificationCompat.CallStyle.CALL_TYPE_INCOMING, androidx.core.app.NotificationCompat.CallStyle.CALL_TYPE_ONGOING, androidx.core.app.NotificationCompat.CallStyle.CALL_TYPE_SCREENING}) @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) public static @interface NotificationCompat.CallStyle.CallType {
+  }
+
   public static final class NotificationCompat.CarExtender implements androidx.core.app.NotificationCompat.Extender {
     ctor public NotificationCompat.CarExtender();
     ctor public NotificationCompat.CarExtender(android.app.Notification);
diff --git a/core/core/src/androidTest/java/androidx/core/app/NotificationCompatTest.java b/core/core/src/androidTest/java/androidx/core/app/NotificationCompatTest.java
index 041e5ab..1b47be8 100644
--- a/core/core/src/androidTest/java/androidx/core/app/NotificationCompatTest.java
+++ b/core/core/src/androidTest/java/androidx/core/app/NotificationCompatTest.java
@@ -55,12 +55,15 @@
 import android.os.Bundle;
 import android.os.Parcelable;
 import android.support.v4.BaseInstrumentationTestCase;
+import android.text.SpannableStringBuilder;
+import android.text.style.ForegroundColorSpan;
 import android.widget.RemoteViews;
 
 import androidx.collection.ArraySet;
 import androidx.core.R;
 import androidx.core.app.NotificationCompat.MessagingStyle.Message;
 import androidx.core.app.NotificationCompat.Style;
+import androidx.core.content.ContextCompat;
 import androidx.core.content.LocusIdCompat;
 import androidx.core.content.pm.ShortcutInfoCompat;
 import androidx.core.graphics.drawable.IconCompat;
@@ -260,6 +263,18 @@
     }
 
     @Test
+    public void testGetActions() {
+        NotificationCompat.Builder builder = new NotificationCompat.Builder(mContext);
+        builder.addAction(0, "testAction", null);
+        builder.addAction(0, "testAction2", null);
+
+        List<NotificationCompat.Action> actions = builder.mActions;
+
+        assertEquals(2, actions.size());
+        assertEquals("testAction2", actions.get(1).getTitle());
+    }
+
+    @Test
     public void testInvisibleActions() {
         NotificationCompat.Builder builder = new NotificationCompat.Builder(mContext);
         Notification nWith = builder.addInvisibleAction(0, "testAction", null)
@@ -2042,6 +2057,859 @@
     }
 
     @Test
+    public void testCallStyle_className() {
+        NotificationCompat.CallStyle callStyle = new NotificationCompat.CallStyle();
+        assertEquals("androidx.core.app.NotificationCompat$CallStyle", callStyle.getClassName());
+    }
+
+    @SdkSuppress(minSdkVersion = 31)
+    @Test
+    public void testCallStyle_callStyleIncomingSetsIntents() {
+        PendingIntent answerIntent = createIntent("answer");
+        PendingIntent declineIntent = createIntent("decline");
+        Person person = new Person.Builder().setName("test name").build();
+        NotificationCompat.CallStyle callStyle = NotificationCompat.CallStyle.forIncomingCall(
+                person, declineIntent, answerIntent);
+
+        Notification notification = new NotificationCompat.Builder(mContext, "test id")
+                .setSmallIcon(1)
+                .setContentTitle("test title")
+                .setStyle(callStyle)
+                .build();
+
+        // Test extras values. This is equivalent to creating a new NotificationCompat.Builder,
+        // and checking the values in it (because those are created via restoreFromCompatExtras).
+        Bundle extras = notification.extras;
+        assertEquals(NotificationCompat.CallStyle.CALL_TYPE_INCOMING,
+                extras.getInt(NotificationCompat.EXTRA_CALL_TYPE));
+        assertEquals("test name", ((android.app.Person) extras.getParcelable(
+                NotificationCompat.EXTRA_CALL_PERSON)).getName());
+
+        assertTrue(extras.containsKey(NotificationCompat.EXTRA_ANSWER_INTENT));
+        assertEquals(answerIntent,
+                (PendingIntent) extras.getParcelable(NotificationCompat.EXTRA_ANSWER_INTENT));
+        assertTrue(extras.containsKey(NotificationCompat.EXTRA_DECLINE_INTENT));
+        assertEquals(declineIntent,
+                ((PendingIntent) extras.getParcelable(NotificationCompat.EXTRA_DECLINE_INTENT)));
+
+        // Create a new NotificationCompat Builder object based on the notification.
+        // This allows us to inspect various fields, including actions.
+        NotificationCompat.Builder builder = new NotificationCompat.Builder(mContext, notification);
+        assertEquals(2, builder.mActions.size());
+        assertEquals(mContext.getString(R.string.call_notification_decline_action),
+                builder.mActions.get(0).getTitle().toString());
+        assertEquals(mContext.getString(R.string.call_notification_answer_action),
+                builder.mActions.get(1).getTitle().toString());
+    }
+
+    @SdkSuppress(minSdkVersion = 31)
+    @Test
+    public void testCallStyle_callStyleOngoingSetsIntents() {
+        PendingIntent hangupIntent = createIntent("hangup");
+        Person person = new Person.Builder().setName("test name").build();
+        NotificationCompat.CallStyle callStyle = NotificationCompat.CallStyle.forOngoingCall(
+                person, hangupIntent);
+
+        Notification notification = new NotificationCompat.Builder(mContext, "test id")
+                .setSmallIcon(1)
+                .setContentTitle("test title")
+                .setStyle(callStyle)
+                .build();
+
+        // Test extras values. This is equivalent to creating a new NotificationCompat.Builder,
+        // and checking the values in it (because those are created via restoreFromCompatExtras).
+        Bundle extras = notification.extras;
+        assertEquals(NotificationCompat.CallStyle.CALL_TYPE_ONGOING,
+                extras.getInt(NotificationCompat.EXTRA_CALL_TYPE));
+        assertEquals("test name", ((android.app.Person) extras.getParcelable(
+                NotificationCompat.EXTRA_CALL_PERSON)).getName());
+
+        assertTrue(extras.containsKey(NotificationCompat.EXTRA_HANG_UP_INTENT));
+        assertEquals(hangupIntent,
+                ((PendingIntent) extras.getParcelable(NotificationCompat.EXTRA_HANG_UP_INTENT)));
+
+        // Create a new NotificationCompat Builder object based on the notification.
+        // This allows us to inspect various fields, including actions.
+        NotificationCompat.Builder builder = new NotificationCompat.Builder(mContext, notification);
+        assertEquals(1, builder.mActions.size());
+        assertEquals(mContext.getString(R.string.call_notification_hang_up_action),
+                builder.mActions.get(0).getTitle().toString());
+    }
+
+    @SdkSuppress(minSdkVersion = 31)
+    @Test
+    public void testCallStyle_callStyleScreeningSetsIntents() {
+        PendingIntent answerIntent = createIntent("answer");
+        PendingIntent hangupIntent = createIntent("hangup");
+        Person person = new Person.Builder().setName("test name").build();
+        NotificationCompat.CallStyle callStyle = NotificationCompat.CallStyle.forScreeningCall(
+                person, hangupIntent, answerIntent);
+
+        Notification notification = new NotificationCompat.Builder(mContext, "test id")
+                .setSmallIcon(1)
+                .setContentTitle("test title")
+                .setStyle(callStyle)
+                .build();
+
+        // Test extras values. This is equivalent to creating a new NotificationCompat.Builder,
+        // and checking the values in it (because those are created via restoreFromCompatExtras).
+        Bundle extras = notification.extras;
+        assertEquals(NotificationCompat.CallStyle.CALL_TYPE_SCREENING,
+                extras.getInt(NotificationCompat.EXTRA_CALL_TYPE));
+        assertEquals("test name", ((android.app.Person) extras.getParcelable(
+                NotificationCompat.EXTRA_CALL_PERSON)).getName());
+
+        assertTrue(extras.containsKey(NotificationCompat.EXTRA_ANSWER_INTENT));
+        assertEquals(answerIntent,
+                ((PendingIntent) extras.getParcelable(NotificationCompat.EXTRA_ANSWER_INTENT)));
+        assertTrue(extras.containsKey(NotificationCompat.EXTRA_HANG_UP_INTENT));
+        assertEquals(hangupIntent,
+                ((PendingIntent) extras.getParcelable(NotificationCompat.EXTRA_HANG_UP_INTENT)));
+
+        // Create a new NotificationCompat Builder object based on the notification.
+        // This allows us to inspect various fields, including actions.
+        NotificationCompat.Builder builder = new NotificationCompat.Builder(mContext, notification);
+        assertEquals(2, builder.mActions.size());
+        assertEquals(mContext.getString(R.string.call_notification_hang_up_action),
+                builder.mActions.get(0).getTitle().toString());
+        assertEquals(mContext.getString(R.string.call_notification_answer_action),
+                builder.mActions.get(1).getTitle().toString());
+    }
+
+    @SdkSuppress(minSdkVersion = 31)
+    @Test
+    public void testCallStyle_callStyleSetAdditionalFields() {
+        PendingIntent answerIntent = createIntent("answer");
+        PendingIntent declineIntent = createIntent("decline");
+        Person person = new Person.Builder().setName("test name").build();
+        NotificationCompat.CallStyle callStyle = NotificationCompat.CallStyle.forIncomingCall(
+                person, declineIntent, answerIntent);
+
+        IconCompat icon = IconCompat.createWithResource(mContext, R.drawable.ic_call_decline);
+        assertNotNull(icon);
+
+        callStyle.setAnswerButtonColorHint(Color.BLUE);
+        callStyle.setDeclineButtonColorHint(Color.MAGENTA);
+        callStyle.setVerificationText("Verified");
+        callStyle.setVerificationIcon(icon.toIcon(mContext));
+        callStyle.setIsVideo(true);
+
+        Notification notification = new NotificationCompat.Builder(mContext, "test id")
+                .setSmallIcon(1)
+                .setContentTitle("test title")
+                .setStyle(callStyle)
+                .build();
+
+        // Test extras values. This is equivalent to creating a new NotificationCompat.Builder,
+        // and checking the values in it (because those are created via restoreFromCompatExtras).
+        Bundle extras = notification.extras;
+
+        assertEquals(Color.BLUE, extras.getInt(NotificationCompat.EXTRA_ANSWER_COLOR));
+        assertEquals(Color.MAGENTA, extras.getInt(NotificationCompat.EXTRA_DECLINE_COLOR));
+        assertEquals("Verified",
+                extras.getCharSequence(NotificationCompat.EXTRA_VERIFICATION_TEXT));
+        assertTrue(extras.containsKey(NotificationCompat.EXTRA_VERIFICATION_ICON));
+        assertEquals(((Icon) extras.getParcelable(
+                        NotificationCompat.EXTRA_VERIFICATION_ICON)).getResId(),
+                icon.toIcon(mContext).getResId());
+        assertTrue(extras.getBoolean(NotificationCompat.EXTRA_CALL_IS_VIDEO));
+    }
+
+    @SdkSuppress(minSdkVersion = 31)
+    @Test
+    public void testCallStyle_callStyleNullAdditionalFields() {
+        PendingIntent answerIntent = createIntent("answer");
+        PendingIntent declineIntent = createIntent("decline");
+        Person person = new Person.Builder().setName("test name").build();
+        NotificationCompat.CallStyle callStyle = NotificationCompat.CallStyle.forIncomingCall(
+                person, declineIntent, answerIntent);
+
+        callStyle.setVerificationText(null);
+        callStyle.setVerificationIcon((Icon) null);
+        callStyle.setIsVideo(false);
+
+        Notification notification = new NotificationCompat.Builder(mContext, "test id")
+                .setSmallIcon(1)
+                .setContentTitle("test title")
+                .setStyle(callStyle)
+                .build();
+
+        // Test extras values. This is equivalent to creating a new NotificationCompat.Builder,
+        // and checking the values in it (because those are created via restoreFromCompatExtras).
+        Bundle extras = notification.extras;
+
+        assertEquals(0, extras.getInt(NotificationCompat.EXTRA_ANSWER_COLOR));
+        assertEquals(0, extras.getInt(NotificationCompat.EXTRA_DECLINE_COLOR));
+        assertNull(extras.getCharSequence(NotificationCompat.EXTRA_VERIFICATION_TEXT));
+        assertNull(extras.getParcelable(NotificationCompat.EXTRA_VERIFICATION_ICON));
+        assertFalse(extras.getBoolean(NotificationCompat.EXTRA_CALL_IS_VIDEO));
+    }
+
+    @SdkSuppress(minSdkVersion = 20)
+    @Test
+    public void testCallStyle_getActionsListWithSystemAndContextualActionsForIncoming() {
+        PendingIntent positiveIntent = createIntent("answerIntent");
+        PendingIntent negativeIntent = createIntent("declineIntent");
+        Person person = new Person.Builder().setName("test name").build();
+        NotificationCompat.CallStyle callStyle = NotificationCompat.CallStyle.forIncomingCall(
+                person, negativeIntent, positiveIntent);
+
+        // Create three "system" actions. The second action is marked as Contextual.
+        NotificationCompat.Action fooAction = new NotificationCompat.Action(0, "foo",
+                createIntent("foo"));
+        NotificationCompat.Action barAction = new NotificationCompat.Action(0, "bar",
+                createIntent("bar"), null, null, null, false, 0, false, /*isContextual=*/true,
+                false);
+        NotificationCompat.Action bazAction = new NotificationCompat.Action(0, "baz",
+                createIntent("baz"));
+
+        // The style must be attached to a builder for the context to be initialized.
+        NotificationCompat.Builder builder = new NotificationCompat.Builder(mContext, "test id")
+                .setSmallIcon(1)
+                .setContentTitle("test title")
+                .addAction(fooAction)
+                .addAction(barAction)
+                .addAction(bazAction)
+                .setStyle(callStyle);
+
+        ArrayList<NotificationCompat.Action> resultActions =
+                callStyle.getActionsListWithSystemActions();
+        // Note that contextual actions do not count toward the MAX_ACTION_BUTTON limit.
+        // Thus the resulting number of actions can be more than MAX_ACTION_BUTTON.
+        assertEquals(4, resultActions.size());
+
+        // The negative action always gets placed first.
+        assertEquals(mContext.getString(R.string.call_notification_decline_action),
+                resultActions.get(0).getTitle().toString());
+        assertEquals(negativeIntent, resultActions.get(0).getActionIntent());
+        resultActions.get(0).getIconCompat();
+
+        assertEquals("foo", resultActions.get(1).getTitle().toString());
+
+        assertEquals(mContext.getString(R.string.call_notification_answer_action),
+                resultActions.get(2).getTitle().toString());
+        assertEquals(positiveIntent, resultActions.get(2).getActionIntent());
+
+        // The "bar" action is Contextual, so it is always included, beyond the limit of 3.
+        assertEquals("bar", resultActions.get(3).getTitle().toString());
+    }
+
+    @SdkSuppress(minSdkVersion = 20)
+    @Test
+    public void testCallStyle_getActionsListWithSystemAndContextualActionsForOngoing() {
+        PendingIntent negativeIntent = createIntent("hang up");
+        Person person = new Person.Builder().setName("test name").build();
+        NotificationCompat.CallStyle callStyle = NotificationCompat.CallStyle.forOngoingCall(
+                person, negativeIntent);
+
+        // Create three "system" actions. The second action is marked as Contextual.
+        NotificationCompat.Action fooAction = new NotificationCompat.Action(0, "foo",
+                createIntent("foo"));
+        NotificationCompat.Action barAction = new NotificationCompat.Action(0, "bar",
+                createIntent("bar"), null, null, null, false, 0, false, /*isContextual=*/true,
+                false);
+        NotificationCompat.Action bazAction = new NotificationCompat.Action(0, "baz",
+                createIntent("baz"));
+        NotificationCompat.Action bbqAction = new NotificationCompat.Action(0, "bbq",
+                createIntent("bbq"));
+
+        // The style must be attached to a builder for the context to be initialized.
+        NotificationCompat.Builder builder = new NotificationCompat.Builder(mContext, "test id")
+                .setSmallIcon(1)
+                .setContentTitle("test title")
+                .addAction(fooAction)
+                .addAction(barAction)
+                .addAction(bazAction)
+                .addAction(bbqAction)
+                .setStyle(callStyle);
+
+        ArrayList<NotificationCompat.Action> resultActions =
+                callStyle.getActionsListWithSystemActions();
+        assertEquals(3, resultActions.size());
+
+        // The negative Intent is always placed first.
+        assertEquals(mContext.getString(R.string.call_notification_hang_up_action),
+                resultActions.get(0).getTitle().toString());
+        assertEquals(negativeIntent, resultActions.get(0).getActionIntent());
+        assertEquals("foo", resultActions.get(1).getTitle().toString());
+        assertEquals("bar", resultActions.get(2).getTitle().toString());
+    }
+
+    @SdkSuppress(minSdkVersion = 20)
+    @Test
+    public void testCallStyle_getActionsListWithSystemAndContextualActionsForScreening() {
+        PendingIntent positiveIntent = createIntent("answer");
+        PendingIntent negativeIntent = createIntent("hangup");
+        Person person = new Person.Builder().setName("test name").build();
+        NotificationCompat.CallStyle callStyle = NotificationCompat.CallStyle.forScreeningCall(
+                person, negativeIntent, positiveIntent);
+
+        // Create three "system" actions. The second action is marked as Contextual.
+        NotificationCompat.Action fooAction = new NotificationCompat.Action(0, "foo",
+                createIntent("foo"));
+        NotificationCompat.Action barAction = new NotificationCompat.Action(0, "bar",
+                createIntent("bar"), null, null, null, false, 0, false, /*isContextual=*/true,
+                false);
+        NotificationCompat.Action bazAction = new NotificationCompat.Action(0, "baz",
+                createIntent("baz"));
+
+        // The style must be attached to a builder for the context to be initialized.
+        NotificationCompat.Builder builder = new NotificationCompat.Builder(mContext, "test id")
+                .setSmallIcon(1)
+                .setContentTitle("test title")
+                .addAction(fooAction)
+                .addAction(barAction)
+                .addAction(bazAction)
+                .setStyle(callStyle);
+
+        ArrayList<NotificationCompat.Action> resultActions =
+                callStyle.getActionsListWithSystemActions();
+        assertEquals(4, resultActions.size());
+
+        // The negative intent is always placed first.
+        assertEquals(mContext.getString(R.string.call_notification_hang_up_action),
+                resultActions.get(0).getTitle().toString());
+        assertEquals(negativeIntent, resultActions.get(0).getActionIntent());
+
+        assertEquals("foo", resultActions.get(1).getTitle().toString());
+
+        assertEquals(mContext.getString(R.string.call_notification_answer_action),
+                resultActions.get(2).getTitle().toString());
+        assertEquals(positiveIntent, resultActions.get(2).getActionIntent());
+
+        assertEquals("bar", resultActions.get(3).getTitle().toString());
+    }
+
+    @SdkSuppress(minSdkVersion = 20)
+    @Test
+    public void testCallStyle_getActionsListForIncomingVideo() {
+        PendingIntent positiveIntent = createIntent("answerIntent");
+        PendingIntent negativeIntent = createIntent("declineIntent");
+        Person person = new Person.Builder().setName("test name").build();
+        NotificationCompat.CallStyle callStyle = NotificationCompat.CallStyle.forIncomingCall(
+                person, negativeIntent, positiveIntent).setIsVideo(true);
+
+        // The style must be attached to a builder for the context to be initialized.
+        NotificationCompat.Builder builder = new NotificationCompat.Builder(mContext, "test id")
+                .setSmallIcon(1)
+                .setContentTitle("test title")
+                .setStyle(callStyle);
+
+        ArrayList<NotificationCompat.Action> resultActions =
+                callStyle.getActionsListWithSystemActions();
+        assertEquals(2, resultActions.size());
+
+        assertEquals(mContext.getString(R.string.call_notification_decline_action),
+                resultActions.get(0).getTitle().toString());
+        assertEquals(negativeIntent, resultActions.get(0).getActionIntent());
+        resultActions.get(0).getIconCompat();
+
+        assertEquals(mContext.getString(R.string.call_notification_answer_video_action),
+                resultActions.get(1).getTitle().toString());
+        assertEquals(positiveIntent, resultActions.get(1).getActionIntent());
+        assertEquals(R.drawable.ic_call_answer_video,
+                resultActions.get(1).getIconCompat().getResId());
+    }
+
+    @SdkSuppress(minSdkVersion = 19, maxSdkVersion = 30)
+    @Test
+    public void testCallStyle_callStyleLegacyNotificationExtraText() {
+        PendingIntent answerIntent = createIntent("answer");
+        PendingIntent declineIntent = createIntent("decline");
+        Person callerPerson = new Person.Builder()
+                .setName("test name")
+                .build();
+        NotificationCompat.CallStyle callStyle = NotificationCompat.CallStyle.forIncomingCall(
+                callerPerson, declineIntent, answerIntent);
+
+        Bundle inputBundle = new Bundle();
+        inputBundle.putCharSequence(NotificationCompat.EXTRA_TEXT, "Supplemental Text");
+
+        Notification notification = new NotificationCompat.Builder(mContext, "test id")
+                .setSmallIcon(1)
+                .setContentTitle("test title")
+                .setStyle(callStyle)
+                .setExtras(inputBundle)
+                .build();
+
+        // Test extras values. This is equivalent to creating a new NotificationCompat.Builder,
+        // and checking the values in it (because those are created via restoreFromCompatExtras).
+        Bundle extras = notification.extras;
+
+        // Checks that the notification text is set to the EXTRA_TEXT value. 11 >=
+        assertEquals("Supplemental Text",
+                extras.getCharSequence(NotificationCompat.EXTRA_TEXT));
+    }
+
+    @SdkSuppress(minSdkVersion = 19, maxSdkVersion = 30)
+    @Test
+    public void testCallStyle_callStyleLegacyNotificationVerificationInfo() {
+        PendingIntent answerIntent = createIntent("answer");
+        PendingIntent declineIntent = createIntent("decline");
+        Person callerPerson = new Person.Builder().setName("test name").build();
+        NotificationCompat.CallStyle callStyle = NotificationCompat.CallStyle.forIncomingCall(
+                callerPerson, declineIntent, answerIntent);
+
+        // Use a bitmap to test that setVerificationIcon can accept a bitmap.
+        Bitmap bitmap = BitmapFactory.decodeResource(mContext.getResources(),
+                R.drawable.notification_bg_low_pressed);
+        callStyle.setVerificationText("Verified");
+        callStyle.setVerificationIcon(bitmap);
+
+        Bundle inputBundle = new Bundle();
+        NotificationCompat.Builder originalBuilder =
+                new NotificationCompat.Builder(mContext, "test id")
+                        .setSmallIcon(1)
+                        .setContentTitle("test title")
+                        .setExtras(inputBundle)
+                        .setStyle(callStyle);
+
+        Notification notification = originalBuilder.build();
+        // Test extras values. This is equivalent to creating a new NotificationCompat.Builder,
+        // and checking the values in it (because those are created via restoreFromCompatExtras).
+        // Extras are only available in API 19 and above, hence the minSdkVersion of this test.
+        Bundle extras = notification.extras;
+
+        assertEquals("Verified",
+                extras.getCharSequence(NotificationCompat.EXTRA_VERIFICATION_TEXT));
+        // Unfortunately there is no easy way to directly compare the bitmaps of two icons,
+        // so we settle for checking that it's the right size via the toString method.
+        // Icon is only available in API level 23 and above.
+        if (Build.VERSION.SDK_INT >= 23) {
+            assertTrue(extras.containsKey(NotificationCompat.EXTRA_VERIFICATION_ICON));
+            assertEquals(((Icon) extras.getParcelable(
+                            NotificationCompat.EXTRA_VERIFICATION_ICON)).toString(),
+                    IconCompat.createWithBitmap(bitmap).toIcon(mContext).toString());
+        } else {
+            // In older versions, to avoid losing the verification icon, the icon is bundled.
+            assertTrue(extras.containsKey(NotificationCompat.EXTRA_VERIFICATION_ICON_COMPAT));
+            IconCompat bundleIcon =
+                    IconCompat.createFromBundle(extras.getParcelable(
+                            NotificationCompat.EXTRA_VERIFICATION_ICON_COMPAT));
+            assertEquals(bundleIcon.toString(),
+                    IconCompat.createWithBitmap(bitmap).toString());
+        }
+    }
+
+    @SdkSuppress(minSdkVersion = 19, maxSdkVersion = 30)
+    @Test
+    public void testCallStyle_callStyleLegacyNotificationPersonInfo() {
+        PendingIntent answerIntent = createIntent("answer");
+        PendingIntent declineIntent = createIntent("decline");
+        Person callerPerson = new Person.Builder().setName("test name").build();
+        NotificationCompat.CallStyle callStyle = NotificationCompat.CallStyle.forIncomingCall(
+                callerPerson, declineIntent, answerIntent);
+
+        NotificationCompat.Builder originalBuilder =
+                new NotificationCompat.Builder(mContext, "test id")
+                        .setSmallIcon(1)
+                        .setContentTitle("test title")
+                        .setStyle(callStyle);
+
+        Notification notification = originalBuilder.build();
+        // Test extras values. This is equivalent to creating a new NotificationCompat.Builder,
+        // and checking the values in it (because those are created via restoreFromCompatExtras).
+        // Extras are only available in API 19 and above, hence the minSdkVersion of this test.
+        Bundle extras = notification.extras;
+
+        if (Build.VERSION.SDK_INT >= 28) {
+            assertEquals(callerPerson.getName(),
+                    Person.fromAndroidPerson((android.app.Person) extras.getParcelable(
+                            NotificationCompat.EXTRA_CALL_PERSON)).getName());
+        } else {
+            assertEquals(callerPerson.getName(),
+                    Person.fromBundle(extras.getBundle(
+                            NotificationCompat.EXTRA_CALL_PERSON_COMPAT)).getName());
+        }
+    }
+
+    @SdkSuppress(minSdkVersion = 16, maxSdkVersion = 30)
+    @Test
+    public void testCallStyle_callStyleLegacyNotificationIncoming() {
+        // Create a placeholder icon for use with the person.
+        IconCompat personIcon = IconCompat.createWithResource(mContext,
+                R.drawable.ic_call_answer_video_low);
+
+        PendingIntent answerIntent = createIntent("answer");
+        PendingIntent declineIntent = createIntent("decline");
+        Person callerPerson = new Person.Builder()
+                .setName("test name")
+                .setIcon(personIcon)
+                .setUri("personUri")
+                .build();
+        NotificationCompat.CallStyle callStyle = NotificationCompat.CallStyle.forIncomingCall(
+                callerPerson, declineIntent, answerIntent);
+
+        callStyle.setAnswerButtonColorHint(Color.BLUE);
+        callStyle.setDeclineButtonColorHint(Color.MAGENTA);
+        callStyle.setIsVideo(false);
+
+        NotificationCompat.Builder originalBuilder =
+                new NotificationCompat.Builder(mContext, "test id")
+                .setSmallIcon(1)
+                .setContentTitle("test title")
+                .setStyle(callStyle);
+
+        Notification notification = originalBuilder.build();
+
+        // Test extras values. This is equivalent to creating a new NotificationCompat.Builder,
+        // and checking the values in it (because those are created via restoreFromCompatExtras).
+        // Extras and NotificationCompatBuilder only available on API 19 and greater.
+        if (Build.VERSION.SDK_INT >= 19) {
+            Bundle extras = notification.extras;
+
+            // Checks that the notification title is set to the caller name. 11 >=
+            assertEquals("test name", extras.getCharSequence(NotificationCompat.EXTRA_TITLE));
+            // Checks that the notification text is set to the default text (since EXTRA_TEXT isn't
+            // set).
+            assertEquals(
+                    mContext.getResources().getString(R.string.call_notification_incoming_text),
+                    extras.getCharSequence(NotificationCompat.EXTRA_TEXT));
+
+            // Create a new NotificationCompat Builder object based on the notification.
+            // This allows us to inspect various fields, including actions.
+            NotificationCompat.Builder builder = new NotificationCompat.Builder(mContext,
+                    notification);
+            // For versions above 11, the "Person" name from the style is applied to the title.
+            assertEquals("test name", builder.mContentTitle);
+        }
+
+        if (Build.VERSION.SDK_INT >= 20) {
+            assertNotNull(notification.actions);
+            assertEquals(2, notification.actions.length);
+
+            // Check that the decline action has the suggested color and title.
+            assertEquals(mContext.getString(R.string.call_notification_decline_action),
+                    notification.actions[0].title.toString());
+            assertEquals(Color.MAGENTA,
+                    ((SpannableStringBuilder) notification.actions[0].title).getSpans(0,
+                            notification.actions[0].title.length(),
+                            ForegroundColorSpan.class)[0].getForegroundColor());
+
+            // Check that the answer action has the suggested color and title.
+            assertEquals(mContext.getString(R.string.call_notification_answer_action),
+                    notification.actions[1].title.toString());
+            assertEquals(Color.BLUE,
+                    ((SpannableStringBuilder) notification.actions[1].title).getSpans(0,
+                            notification.actions[1].title.length(),
+                            ForegroundColorSpan.class)[0].getForegroundColor());
+        }
+
+        // For versions above 23, the Person icon is added as the notification's large Icon.
+        // Icons were unavailable before API 23.
+        if (Build.VERSION.SDK_INT >= 23) {
+            assertEquals(personIcon.toString(), notification.getLargeIcon().toString());
+        }
+    }
+
+    @SdkSuppress(minSdkVersion = 16, maxSdkVersion = 30)
+    @Test
+    public void testCallStyle_callStyleLegacyNotificationIncomingVideo() {
+        PendingIntent answerIntent = createIntent("answer");
+        PendingIntent declineIntent = createIntent("decline");
+        Person callerPerson = new Person.Builder().setName("test name").build();
+        NotificationCompat.CallStyle callStyle = NotificationCompat.CallStyle.forIncomingCall(
+                callerPerson, declineIntent, answerIntent);
+
+        callStyle.setAnswerButtonColorHint(Color.BLUE);
+        callStyle.setDeclineButtonColorHint(Color.MAGENTA);
+        callStyle.setIsVideo(true);
+
+        NotificationCompat.Builder originalBuilder =
+                new NotificationCompat.Builder(mContext, "test id")
+                        .setSmallIcon(1)
+                        .setContentTitle("test title")
+                        .setStyle(callStyle);
+        Notification notification = originalBuilder.build();
+
+        // Checks in this section check values in the Notification's extras, which were unavailable
+        // prior to API 19.
+        if (Build.VERSION.SDK_INT >= 19) {
+            NotificationCompat.Builder builder = new NotificationCompat.Builder(mContext,
+                    notification);
+
+            Bundle extras = builder.getExtras();
+            assertTrue(extras.getBoolean(NotificationCompat.EXTRA_CALL_IS_VIDEO));
+        }
+
+        // Actions were introduced in API 20.
+        if (Build.VERSION.SDK_INT >= 20) {
+            assertNotNull(notification.actions);
+            assertEquals(2, notification.actions.length);
+
+            // Check that the answer action has the suggested color and title.
+            assertEquals(mContext.getString(R.string.call_notification_answer_video_action),
+                    notification.actions[1].title.toString());
+            assertEquals(Color.BLUE,
+                    ((SpannableStringBuilder) notification.actions[1].title).getSpans(0,
+                            notification.actions[1].title.length(),
+                            ForegroundColorSpan.class)[0].getForegroundColor());
+        }
+    }
+
+    @SdkSuppress(minSdkVersion = 16, maxSdkVersion = 30)
+    @Test
+    public void testCallStyle_callStyleLegacyNotificationOngoing() {
+        // Create a placeholder icon for use with the person.
+        IconCompat personIcon = IconCompat.createWithResource(mContext,
+                R.drawable.ic_call_answer_video_low);
+
+        PendingIntent hangupIntent = createIntent("hangup");
+        Person callerPerson = new Person.Builder()
+                .setName("test name")
+                .setIcon(personIcon)
+                .setUri("personUri")
+                .build();
+        NotificationCompat.CallStyle callStyle = NotificationCompat.CallStyle.forOngoingCall(
+                callerPerson, hangupIntent);
+
+        callStyle.setAnswerButtonColorHint(Color.BLUE);
+        callStyle.setDeclineButtonColorHint(Color.MAGENTA);
+
+        NotificationCompat.Builder originalBuilder =
+                new NotificationCompat.Builder(mContext, "test id")
+                        .setSmallIcon(1)
+                        .setContentTitle("test title")
+                        .setStyle(callStyle);
+        Notification notification = originalBuilder.build();
+
+        // Checks in this section check values in the Notification's extras, which were unavailable
+        // prior to API 19.
+        if (Build.VERSION.SDK_INT >= 19) {
+            // Test extras values. This is equivalent to creating a new NotificationCompat.Builder,
+            // and checking the values in it (as those are created via restoreFromCompatExtras).
+            Bundle extras = notification.extras;
+
+            // Checks that the notification title is set to the caller name. 11 >=
+            assertEquals("test name", extras.getCharSequence(NotificationCompat.EXTRA_TITLE));
+
+            // Checks that the notification text is set to the default text (since EXTRA_TEXT isn't
+            // set). 11 >=
+            assertEquals(mContext.getResources().getString(R.string.call_notification_ongoing_text),
+                    extras.getCharSequence(NotificationCompat.EXTRA_TEXT));
+
+            // Create a new NotificationCompat Builder object based on the notification.
+            // This allows us to inspect various fields, including actions.
+            NotificationCompat.Builder builder = new NotificationCompat.Builder(mContext,
+                    notification);
+
+            // For versions above 11, the "Person" name from the style is applied to the title.
+            assertEquals("test name", builder.mContentTitle);
+        }
+
+        // Actions were introduced in API 20.
+        if (Build.VERSION.SDK_INT >= 20) {
+            assertNotNull(notification.actions);
+            assertEquals(1, notification.actions.length);
+
+            // Check that the hangup action has the suggested color and title.
+            assertEquals(mContext.getString(R.string.call_notification_hang_up_action),
+                    notification.actions[0].title.toString());
+            assertEquals(Color.MAGENTA,
+                    ((SpannableStringBuilder) notification.actions[0].title).getSpans(0,
+                            notification.actions[0].title.length(),
+                            ForegroundColorSpan.class)[0].getForegroundColor());
+        }
+
+        // For versions above 23, the Person icon is added as the notification's large Icon.
+        if (Build.VERSION.SDK_INT >= 23) {
+            assertEquals(personIcon.toString(), notification.getLargeIcon().toString());
+        }
+    }
+
+    @SdkSuppress(minSdkVersion = 16, maxSdkVersion = 30)
+    @Test
+    public void testCallStyle_callStyleLegacyNotificationScreening() {
+        // Create a placeholder icon for use with the person.
+        IconCompat personIcon = IconCompat.createWithResource(mContext,
+                R.drawable.ic_call_answer_video_low);
+
+        PendingIntent answerIntent = createIntent("answer");
+        PendingIntent hangupIntent = createIntent("hangup");
+        Person callerPerson = new Person.Builder()
+                .setName("test name")
+                .setIcon(personIcon)
+                .setUri("personUri")
+                .build();
+        NotificationCompat.CallStyle callStyle = NotificationCompat.CallStyle.forScreeningCall(
+                callerPerson, hangupIntent, answerIntent);
+
+        NotificationCompat.Builder originalBuilder =
+                new NotificationCompat.Builder(mContext, "test id")
+                        .setSmallIcon(1)
+                        .setContentTitle("test title")
+                        .setStyle(callStyle);
+        Notification notification = originalBuilder.build();
+
+        // Checks in this section check values in the Notification's extras, which were unavailable
+        // prior to API 19.
+        if (Build.VERSION.SDK_INT >= 19) {
+            // Test extras values. This is equivalent to creating a new NotificationCompat.Builder,
+            // and checking the values in it (as those are created via restoreFromCompatExtras).
+            Bundle extras = notification.extras;
+
+            // Checks that the notification title is set to the caller name. 11 >=
+            assertEquals("test name", extras.getCharSequence(NotificationCompat.EXTRA_TITLE));
+
+            // Checks that the notification text is set to the default text (since EXTRA_TEXT isn't
+            // set). 11 >=
+            assertEquals(
+                    mContext.getResources().getString(R.string.call_notification_screening_text),
+                    extras.getCharSequence(NotificationCompat.EXTRA_TEXT));
+
+            // Create a new NotificationCompat Builder object based on the notification.
+            // This allows us to inspect various fields, including actions.
+            NotificationCompat.Builder builder = new NotificationCompat.Builder(mContext,
+                    notification);
+
+            // For versions above 11, the "Person" name from the style is applied to the title.
+            assertEquals("test name", builder.mContentTitle);
+        }
+
+        // Actions were introduced in API 20.
+        if (Build.VERSION.SDK_INT >= 20) {
+            assertNotNull(notification.actions);
+            assertEquals(2, notification.actions.length);
+
+            // Check that the decline action has the suggested color and title.
+            assertEquals(mContext.getString(R.string.call_notification_hang_up_action),
+                    notification.actions[0].title.toString());
+            assertEquals(ContextCompat.getColor(mContext, R.color.call_notification_decline_color),
+                    ((SpannableStringBuilder) notification.actions[0].title).getSpans(0,
+                            notification.actions[0].title.length(),
+                            ForegroundColorSpan.class)[0].getForegroundColor());
+
+            // Check that the answer action has the suggested color and title.
+            assertEquals(mContext.getString(R.string.call_notification_answer_action),
+                    notification.actions[1].title.toString());
+            assertEquals(ContextCompat.getColor(mContext, R.color.call_notification_answer_color),
+                    ((SpannableStringBuilder) notification.actions[1].title).getSpans(0,
+                            notification.actions[1].title.length(),
+                            ForegroundColorSpan.class)[0].getForegroundColor());
+        }
+
+        // For versions above 23, the Person icon is added as the notification's large Icon.
+        if (Build.VERSION.SDK_INT >= 23) {
+            assertEquals(personIcon.toString(), notification.getLargeIcon().toString());
+        }
+    }
+
+    @SdkSuppress(minSdkVersion = 21, maxSdkVersion = 27)
+    @Test
+    public void testCallStyle_callStyleLegacyNotificationImportantPeopleUri() {
+        PendingIntent answerIntent = createIntent("answer");
+        PendingIntent declineIntent = createIntent("decline");
+        Person callerPerson = new Person.Builder()
+                .setName("test name")
+                .setUri("personUri")
+                .build();
+        NotificationCompat.CallStyle callStyle = NotificationCompat.CallStyle.forIncomingCall(
+                callerPerson, declineIntent, answerIntent);
+
+        Notification notification = new NotificationCompat.Builder(mContext, "test id")
+                .setSmallIcon(1)
+                .setContentTitle("test title")
+                .setStyle(callStyle)
+                .build();
+
+        // Create a new NotificationCompat Builder object based on the notification.
+        // This allows us to inspect various fields, including actions.
+        NotificationCompat.Builder builder = new NotificationCompat.Builder(mContext, notification);
+
+        // For API versions 21 through 28, we check the URI is in the mPeople set of string URIs.
+        assertTrue(builder.mPeople.contains(callerPerson.getUri()));
+    }
+
+    @SdkSuppress(minSdkVersion = 28, maxSdkVersion = 30)
+    @Test
+    public void testCallStyle_callStyleLegacyNotificationImportantPeople() {
+        // Create a placeholder icon for use with the person.
+        IconCompat personIcon = IconCompat.createWithResource(mContext,
+                R.drawable.ic_call_answer_video);
+
+        PendingIntent answerIntent = createIntent("answer");
+        PendingIntent declineIntent = createIntent("decline");
+        Person callerPerson = new Person.Builder()
+                .setName("test name")
+                .setIcon(personIcon)
+                .setUri("personUri")
+                .build();
+        NotificationCompat.CallStyle callStyle = NotificationCompat.CallStyle.forIncomingCall(
+                callerPerson, declineIntent, answerIntent);
+
+        Notification notification = new NotificationCompat.Builder(mContext, "test id")
+                .setSmallIcon(1)
+                .setContentTitle("test title")
+                .setStyle(callStyle)
+                .build();
+
+        // Create a new NotificationCompat Builder object based on the notification.
+        // This allows us to inspect various fields, including actions.
+        NotificationCompat.Builder builder = new NotificationCompat.Builder(mContext, notification);
+
+        // For API versions after 28, the person object is added as relevant to the notification.
+        boolean foundPerson = false;
+        for (Person person : builder.mPersonList) {
+            foundPerson |= person.getUri() == callerPerson.getUri();
+        }
+        assertTrue(foundPerson);
+    }
+
+    @SdkSuppress(minSdkVersion = 21, maxSdkVersion = 30)
+    @Test
+    public void testCallStyle_callStyleLegacySetsCategory() {
+        PendingIntent answerIntent = createIntent("answer");
+        PendingIntent declineIntent = createIntent("decline");
+        Person callerPerson = new Person.Builder().setName("test name").build();
+        NotificationCompat.CallStyle callStyle = NotificationCompat.CallStyle.forIncomingCall(
+                callerPerson, declineIntent, answerIntent);
+
+        Notification notification = new NotificationCompat.Builder(mContext, "test id")
+                .setSmallIcon(1)
+                .setContentTitle("test title")
+                .setStyle(callStyle)
+                .build();
+
+        assertEquals(NotificationCompat.CATEGORY_CALL, notification.category);
+    }
+
+    @SdkSuppress(minSdkVersion = 31)
+    @Test
+    public void testBuilderFromNotification_fromCallStyleCompat() {
+        // Create the NotificationCompat CallStyle for an Incoming Call.
+        PendingIntent answerIntent = createIntent("answer");
+        PendingIntent declineIntent = createIntent("decline");
+        NotificationCompat.CallStyle callStyle = NotificationCompat.CallStyle.forIncomingCall(
+                new Person.Builder().setName(
+                        "test name").build(), answerIntent, declineIntent);
+
+        Bundle testBundle = new Bundle();
+        testBundle.putString("testExtraKey", "testExtraValue");
+
+        Notification expectedNotification = new NotificationCompat.Builder(mContext, "test id")
+                .setContentTitle("test name")
+                .setContentText("contentText")
+                .setContentInfo("contentInfo")
+                .setSubText("subText")
+                .setNumber(1)
+                .setProgress(10, 1, false)
+                .setSettingsText("settingsText")
+                .setLocalOnly(true)
+                .setAutoCancel(true)
+                .setStyle(callStyle)
+                .addExtras(testBundle)
+                .build();
+        Notification recoveredNotification = new NotificationCompat.Builder(mContext,
+                expectedNotification).build();
+        assertNotificationEquals(expectedNotification, recoveredNotification);
+    }
+
+    private PendingIntent createIntent(String actionName) {
+        return PendingIntent.getActivity(mContext, 0, new Intent(actionName),
+                PendingIntent.FLAG_IMMUTABLE);
+    }
+
+    @Test
     public void action_builder_hasDefault() {
         NotificationCompat.Action action =
                 newActionBuilder().build();
diff --git a/core/core/src/main/java/androidx/core/app/NotificationCompat.java b/core/core/src/main/java/androidx/core/app/NotificationCompat.java
index dde3da7..1dcb0cd 100644
--- a/core/core/src/main/java/androidx/core/app/NotificationCompat.java
+++ b/core/core/src/main/java/androidx/core/app/NotificationCompat.java
@@ -20,12 +20,12 @@
 import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX;
 
 import static java.lang.annotation.RetentionPolicy.SOURCE;
+import static java.util.Objects.requireNonNull;
 
 import android.annotation.SuppressLint;
 import android.app.Activity;
 import android.app.Notification;
 import android.app.PendingIntent;
-import android.app.RemoteInput.Builder;
 import android.content.Context;
 import android.content.LocusId;
 import android.content.res.ColorStateList;
@@ -47,7 +47,9 @@
 import android.text.SpannableStringBuilder;
 import android.text.Spanned;
 import android.text.TextUtils;
+import android.text.style.ForegroundColorSpan;
 import android.text.style.TextAppearanceSpan;
+import android.util.Log;
 import android.util.SparseArray;
 import android.util.TypedValue;
 import android.view.Gravity;
@@ -64,6 +66,7 @@
 import androidx.annotation.RequiresApi;
 import androidx.annotation.RestrictTo;
 import androidx.core.R;
+import androidx.core.content.ContextCompat;
 import androidx.core.content.LocusIdCompat;
 import androidx.core.content.pm.ShortcutInfoCompat;
 import androidx.core.graphics.drawable.IconCompat;
@@ -82,6 +85,7 @@
  * Helper for accessing features in {@link android.app.Notification}.
  */
 public class NotificationCompat {
+    private static final String TAG = "NotifCompat";
 
     /**
      * An activity that provides a user interface for adjusting notification preferences for its
@@ -446,7 +450,7 @@
     public static final String EXTRA_PICTURE_ICON = "android.pictureIcon";
 
     /**
-     * {@link #extras} key: this is a content description of the big picture supplied from
+     * {@link #getExtras extras} key: this is a content description of the big picture supplied from
      * {@link BigPictureStyle#bigPicture(Bitmap)}, supplied to
      * {@link BigPictureStyle#setContentDescription(CharSequence)}.
      */
@@ -576,6 +580,99 @@
     public static final String EXTRA_IS_GROUP_CONVERSATION = "android.isGroupConversation";
 
     /**
+     * {@link #getExtras extras} key: the type of call represented by the
+     * {@link android.app.Notification.CallStyle} notification. This extra is an int.
+     */
+    @SuppressLint("ActionValue")  // Field & value copied from android.app.Notification
+    public static final String EXTRA_CALL_TYPE = "android.callType";
+
+    /**
+     * {@link #getExtras extras} key: whether the  {@link android.app.Notification.CallStyle} notification
+     * is for a call that will activate video when answered. This extra is a boolean.
+     */
+    @SuppressLint("ActionValue")  // Field & value copied from android.app.Notification
+    public static final String EXTRA_CALL_IS_VIDEO = "android.callIsVideo";
+
+    /**
+     * {@link #getExtras extras} key: the person to be displayed as calling for the
+     * {@link android.app.Notification.CallStyle} notification. This extra is a {@link Person}.
+     */
+    @SuppressLint("ActionValue")  // Field & value copied from android.app.Notification
+    public static final String EXTRA_CALL_PERSON = "android.callPerson";
+
+    /**
+     * {@link #getExtras extras} key: the person to be displayed as calling for the
+     * {@link android.app.Notification.CallStyle} notification, for Android versions before the
+     * {@link Person} class was introduced. This extra is a {@link Bundle} representing a
+     * {@link Person}.
+     */
+    @SuppressLint("ActionValue")
+    public static final String EXTRA_CALL_PERSON_COMPAT = "android.callPersonCompat";
+
+    /**
+     * {@link #getExtras extras} key: the icon to be displayed as a verification status of the
+     * caller on a {@link android.app.Notification.CallStyle} notification. This extra is an
+     * {@link Icon}.
+     */
+    @SuppressLint("ActionValue")  // Field & value copied from android.app.Notification
+    public static final String EXTRA_VERIFICATION_ICON = "android.verificationIcon";
+
+    /**
+     * {@link #getExtras extras} key: the icon to be displayed as a verification status of the
+     * caller on a {@link android.app.Notification.CallStyle} notification, for Android versions
+     * before the {@link Icon} class was introduced. This extra is an {@link Bundle} representing an
+     * {@link Icon}.
+     */
+    @SuppressLint("ActionValue")
+    public static final String EXTRA_VERIFICATION_ICON_COMPAT = "android.verificationIconCompat";
+
+    /**
+     * {@link #getExtras extras} key: the text to be displayed as a verification status of the
+     * caller on a {@link android.app.Notification.CallStyle} notification. This extra is a
+     * {@link CharSequence}.
+     */
+    @SuppressLint("ActionValue")  // Field & value copied from android.app.Notification
+    public static final String EXTRA_VERIFICATION_TEXT = "android.verificationText";
+
+    /**
+     * {@link #getExtras extras} key: the intent to be sent when the users answers a
+     * {@link android.app.Notification.CallStyle} notification. This extra is a
+     * {@link PendingIntent}.
+     */
+    @SuppressLint("ActionValue")  // Field & value copied from android.app.Notification
+    public static final String EXTRA_ANSWER_INTENT = "android.answerIntent";
+
+    /**
+     * {@link #getExtras extras} key: the intent to be sent when the users declines a
+     * {@link android.app.Notification.CallStyle} notification. This extra is a
+     * {@link PendingIntent}.
+     */
+    @SuppressLint("ActionValue")  // Field & value copied from android.app.Notification
+    public static final String EXTRA_DECLINE_INTENT = "android.declineIntent";
+
+    /**
+     * {@link #getExtras extras} key: the intent to be sent when the users hangs up a
+     * {@link android.app.Notification.CallStyle} notification. This extra is a
+     * {@link PendingIntent}.
+     */
+    @SuppressLint("ActionValue")  // Field & value copied from android.app.Notification
+    public static final String EXTRA_HANG_UP_INTENT = "android.hangUpIntent";
+
+    /**
+     * {@link #getExtras extras} key: the color used as a hint for the Answer action button of a
+     * {@link android.app.Notification.CallStyle} notification. This extra is a {@link ColorInt}.
+     */
+    @SuppressLint("ActionValue")  // Field & value copied from android.app.Notification
+    public static final String EXTRA_ANSWER_COLOR = "android.answerColor";
+
+    /**
+     * {@link #getExtras extras} key: the color used as a hint for the Decline or Hang Up action button of a
+     * {@link android.app.Notification.CallStyle} notification. This extra is a {@link ColorInt}.
+     */
+    @SuppressLint("ActionValue")  // Field & value copied from android.app.Notification
+    public static final String EXTRA_DECLINE_COLOR = "android.declineColor";
+
+    /**
      * Key for compat's {@link MessagingStyle#getConversationTitle()}. This allows backwards support
      * for conversation titles as SDK < P uses the title to denote group status. This hidden title
      * doesn't appear in the notification shade.
@@ -619,6 +716,14 @@
     @ColorInt
     public static final int COLOR_DEFAULT = Color.TRANSPARENT;
 
+    /**
+     * Maximum number of (generic) action buttons in a notification (contextual action buttons are
+     * handled separately).
+     * @hide
+     */
+    @RestrictTo(LIBRARY_GROUP_PREFIX)
+    public static final int MAX_ACTION_BUTTONS = 3;
+
     /** @hide */
     @RestrictTo(LIBRARY_GROUP_PREFIX)
     @IntDef({AudioManager.STREAM_VOICE_CALL, AudioManager.STREAM_SYSTEM, AudioManager.STREAM_RING,
@@ -979,7 +1084,7 @@
         /**
          * Creates a NotificationCompat.Builder which can be used to build a notification that is
          * equivalent to the given one, such that updates can be made to an existing notification
-         * with the NotitifactionCompat.Builder API.
+         * with the NotificationCompat.Builder API.
          */
         @RequiresApi(19)
         @SuppressWarnings("deprecation")
@@ -2814,6 +2919,8 @@
                         return new DecoratedCustomViewStyle();
                     case MessagingStyle.TEMPLATE_CLASS_NAME:
                         return new MessagingStyle();
+                    case CallStyle.TEMPLATE_CLASS_NAME:
+                        return new CallStyle();
                 }
             }
             return null;
@@ -2840,6 +2947,8 @@
                 return new BigTextStyle();
             } else if (extras.containsKey(EXTRA_TEXT_LINES)) {
                 return new InboxStyle();
+            } else if (extras.containsKey(EXTRA_CALL_TYPE)) {
+                return new CallStyle();
             }
             // If individual extras do not help identify the style, use the framework style name.
             return constructCompatStyleByPlatformName(extras.getString(EXTRA_TEMPLATE));
@@ -4573,6 +4682,844 @@
     }
 
     /**
+     * Helper class for generating large-format notifications that include a caller and required
+     * actions, and indicate an incoming call.
+     * <br>
+     * If the platform does not provide large-format notifications, this method has no effect. The
+     * user will always see the normal notification view.
+     * <br>
+     * This class is a "rebuilder": It attaches to a Builder object and modifies its behavior,
+     * like so:
+     * <pre class="prettyprint">
+     * Notification notification = new NotificationCompat.Builder(mContext)
+     *     .setSmallIcon(R.drawable.new_post)
+     *     .setStyle(
+     *             new Notification.CallStyle.forIncomingCall(caller, declineIntent, answerIntent))
+     *     .build();
+     * </pre>
+     * <br>
+     * Note that for CallStyle Notifications on API versions before 31 to be ranked as they are
+     * in API versions 31+, they must be associated with a foreground service. Additionally,
+     * CallStyle Notifications on API versions before 31 can achieve the similar ranking by marking
+     * the Notification as colorized, using {@link Builder#setColorized(boolean)}.
+     */
+    public static class CallStyle extends Style {
+
+        private static final String TEMPLATE_CLASS_NAME =
+                "androidx.core.app.NotificationCompat$CallStyle";
+
+        /**
+         * @hide
+         */
+        @RestrictTo(LIBRARY_GROUP_PREFIX)
+        @Retention(RetentionPolicy.SOURCE)
+        @IntDef({
+                CALL_TYPE_UNKNOWN,
+                CALL_TYPE_INCOMING,
+                CALL_TYPE_ONGOING,
+                CALL_TYPE_SCREENING
+        })
+        public @interface CallType {
+        }
+
+        ;
+
+        // TODO: Replace these with the real CALL_TYPE constants, once they are available.
+        /**
+         * Unknown call type.
+         *
+         * See {@link #EXTRA_CALL_TYPE}.
+         */
+        public static final int CALL_TYPE_UNKNOWN = 0;
+
+        /**
+         * Call type for incoming calls.
+         *
+         * See {@link #EXTRA_CALL_TYPE}.
+         */
+        public static final int CALL_TYPE_INCOMING = 1;
+        /**
+         * Call type for ongoing calls.
+         *
+         * See {@link #EXTRA_CALL_TYPE}.
+         */
+        public static final int CALL_TYPE_ONGOING = 2;
+        /**
+         * Call type for calls that are being screened.
+         *
+         * See {@link #EXTRA_CALL_TYPE}.
+         */
+        public static final int CALL_TYPE_SCREENING = 3;
+
+        /**
+         * This is a key used privately on the action.extras to give spacing priority
+         * to the required call actions
+         */
+        private static final String KEY_ACTION_PRIORITY = "key_action_priority";
+
+        private int mCallType;
+        private Person mPerson;
+        private PendingIntent mAnswerIntent;
+        private PendingIntent mDeclineIntent;
+        private PendingIntent mHangUpIntent;
+        private boolean mIsVideo;
+        private Integer mAnswerButtonColor;
+        private Integer mDeclineButtonColor;
+        private IconCompat mVerificationIcon;
+        private CharSequence mVerificationText;
+
+        public CallStyle() {
+        }
+
+        public CallStyle(@Nullable Builder builder) {
+            setBuilder(builder);
+        }
+
+        /**
+         * Create a CallStyle for an incoming call.
+         * This notification will have a decline and an answer action, will allow a single
+         * custom {@link Builder#addAction(Action) action}, and will have a default
+         * {@link Builder#setContentText(CharSequence) content text} for an incoming call.
+         *
+         * @param person        The person displayed as the caller.
+         *                      The person also needs to have a non-empty name associated with it.
+         * @param declineIntent The intent to be sent when the user taps the decline action
+         * @param answerIntent  The intent to be sent when the user taps the answer action
+         */
+        @NonNull
+        public static CallStyle forIncomingCall(@NonNull Person person,
+                @NonNull PendingIntent declineIntent, @NonNull PendingIntent answerIntent) {
+            return new CallStyle(CALL_TYPE_INCOMING, person,
+                    null /* hangUpIntent */,
+                    requireNonNull(declineIntent, "declineIntent is required"),
+                    requireNonNull(answerIntent, "answerIntent is required")
+            );
+        }
+
+        /**
+         * Create a CallStyle for an ongoing call.
+         * This notification will have a hang up action, will allow up to two
+         * custom {@link Builder#addAction(Action) actions}, and will have a default
+         * {@link Builder#setContentText(CharSequence) content text} for an ongoing call.
+         *
+         * @param person       The person displayed as being on the other end of the call.
+         *                     The person also needs to have a non-empty name associated with it.
+         * @param hangUpIntent The intent to be sent when the user taps the hang up action
+         */
+        @NonNull
+        public static CallStyle forOngoingCall(@NonNull Person person,
+                @NonNull PendingIntent hangUpIntent) {
+            return new CallStyle(CALL_TYPE_ONGOING, person,
+                    requireNonNull(hangUpIntent, "hangUpIntent is required"),
+                    null /* declineIntent */,
+                    null /* answerIntent */
+            );
+        }
+
+        /**
+         * Create a CallStyle for a call that is being screened.
+         * This notification will have a hang up and an answer action, will allow a single
+         * custom {@link Builder#addAction(Action) action}, and will have a default
+         * {@link Builder#setContentText(CharSequence) content text} for a call that is being
+         * screened.
+         *
+         * @param person       The person displayed as the caller.
+         *                     The person also needs to have a non-empty name associated with it.
+         * @param hangUpIntent The intent to be sent when the user taps the hang up action
+         * @param answerIntent The intent to be sent when the user taps the answer action
+         */
+        @NonNull
+        public static CallStyle forScreeningCall(@NonNull Person person,
+                @NonNull PendingIntent hangUpIntent, @NonNull PendingIntent answerIntent) {
+            return new CallStyle(CALL_TYPE_SCREENING, person,
+                    requireNonNull(hangUpIntent, "hangUpIntent is required"),
+                    null /* declineIntent */,
+                    requireNonNull(answerIntent, "answerIntent is required")
+            );
+        }
+
+        /**
+         * @param callType      The type of the call (for example, CALL_TYPE_INCOMING).
+         * @param person        The person displayed for the incoming call.
+         *                      The user also needs to have a non-empty name associated with it.
+         * @param hangUpIntent  The intent to be sent when the user taps the hang up action
+         * @param declineIntent The intent to be sent when the user taps the decline action
+         * @param answerIntent  The intent to be sent when the user taps the answer action
+         */
+        private CallStyle(@CallType int callType, @NonNull Person person,
+                @Nullable PendingIntent hangUpIntent, @Nullable PendingIntent declineIntent,
+                @Nullable PendingIntent answerIntent) {
+            if (person == null || TextUtils.isEmpty(person.getName())) {
+                throw new IllegalArgumentException("person must have a non-empty a name");
+            }
+            mCallType = callType;
+            mPerson = person;
+            mAnswerIntent = answerIntent;
+            mDeclineIntent = declineIntent;
+            mHangUpIntent = hangUpIntent;
+        }
+
+        /**
+         * Sets whether the call is a video call, which may affect the icons or text used on the
+         * required action buttons.
+         */
+        @NonNull
+        public CallStyle setIsVideo(boolean isVideo) {
+            mIsVideo = isVideo;
+            return this;
+        }
+
+        /**
+         * Optional icon to be displayed with {@link #setVerificationText(CharSequence) text}
+         * as a verification status of the caller.
+         */
+        @RequiresApi(23)
+        @NonNull
+        public CallStyle setVerificationIcon(@Nullable Icon verificationIcon) {
+            mVerificationIcon = verificationIcon == null ? null :
+                    IconCompat.createFromIcon(verificationIcon);
+            return this;
+        }
+
+        /**
+         * Optional icon to be displayed with {@link #setVerificationText(CharSequence) text}
+         * as a verification status of the caller.
+         */
+        @NonNull
+        public CallStyle setVerificationIcon(@Nullable Bitmap verificationIcon) {
+            mVerificationIcon = IconCompat.createWithBitmap(verificationIcon);
+            return this;
+        }
+
+        /**
+         * Optional text to be displayed with an {@link #setVerificationIcon(Icon) icon}
+         * as a verification status of the caller.
+         */
+        @NonNull
+        public CallStyle setVerificationText(@Nullable CharSequence verificationText) {
+            mVerificationText = verificationText;
+            return this;
+        }
+
+        /**
+         * Optional color to be used as a hint for the Answer action button's color.
+         * The system may change this color to ensure sufficient contrast with the background.
+         * The system may choose to disregard this hint if the notification is not colorized.
+         */
+        @NonNull
+        public CallStyle setAnswerButtonColorHint(@ColorInt int color) {
+            mAnswerButtonColor = color;
+            return this;
+        }
+
+        /**
+         * Optional color to be used as a hint for the Decline or Hang Up action button's color.
+         * The system may change this color to ensure sufficient contrast with the background.
+         * The system may choose to disregard this hint if the notification is not colorized.
+         */
+        @NonNull
+        public CallStyle setDeclineButtonColorHint(@ColorInt int color) {
+            mDeclineButtonColor = color;
+            return this;
+        }
+
+        /**
+         * @hide
+         */
+        @RestrictTo(LIBRARY_GROUP_PREFIX)
+        @Override
+        public boolean displayCustomViewInline() {
+            // This is a lie; True is returned to make sure that the custom view is not used
+            // instead of the template, but it will not actually be included.
+            return true;
+        }
+
+
+        /**
+         * @hide
+         */
+        @RestrictTo(LIBRARY_GROUP_PREFIX)
+        @Override
+        protected void restoreFromCompatExtras(@NonNull Bundle extras) {
+            super.restoreFromCompatExtras(extras);
+
+            mCallType = extras.getInt(EXTRA_CALL_TYPE);
+            mIsVideo = extras.getBoolean(EXTRA_CALL_IS_VIDEO);
+            if (Build.VERSION.SDK_INT >= 28
+                    && extras.containsKey(EXTRA_CALL_PERSON)) {
+                mPerson = Person.fromAndroidPerson(
+                        (android.app.Person)
+                        extras.getParcelable(EXTRA_CALL_PERSON));
+            } else if (extras.containsKey(EXTRA_CALL_PERSON_COMPAT)) {
+                mPerson = Person.fromBundle(extras.getBundle(EXTRA_CALL_PERSON_COMPAT));
+            }
+            if (Build.VERSION.SDK_INT >= 23 && extras.containsKey(EXTRA_VERIFICATION_ICON)) {
+                mVerificationIcon = IconCompat.createFromIcon((Icon) extras.getParcelable(
+                        EXTRA_VERIFICATION_ICON));
+            } else if (extras.containsKey(EXTRA_VERIFICATION_ICON_COMPAT)) {
+                mVerificationIcon =
+                        IconCompat.createFromBundle(
+                                extras.getBundle(EXTRA_VERIFICATION_ICON_COMPAT));
+            }
+            mVerificationText = extras.getCharSequence(EXTRA_VERIFICATION_TEXT);
+            mAnswerIntent = (PendingIntent) extras.getParcelable(EXTRA_ANSWER_INTENT);
+            mDeclineIntent = (PendingIntent) extras.getParcelable(EXTRA_DECLINE_INTENT);
+            mHangUpIntent = (PendingIntent) extras.getParcelable(EXTRA_HANG_UP_INTENT);
+            mAnswerButtonColor = extras.containsKey(EXTRA_ANSWER_COLOR)
+                    ? extras.getInt(EXTRA_ANSWER_COLOR) : null;
+            mDeclineButtonColor = extras.containsKey(EXTRA_DECLINE_COLOR)
+                    ? extras.getInt(EXTRA_DECLINE_COLOR) : null;
+        }
+
+        /**
+         * @hide
+         */
+        @RestrictTo(LIBRARY_GROUP_PREFIX)
+        @Override
+        public void addCompatExtras(@NonNull Bundle extras) {
+            super.addCompatExtras(extras);
+            // Reminder: this method only needs to add fields which are not added by the platform
+            // builder (and only needs to work at all for API 19+).
+
+            extras.putInt(EXTRA_CALL_TYPE, mCallType);
+            extras.putBoolean(EXTRA_CALL_IS_VIDEO, mIsVideo);
+            if (mPerson != null) {
+                if (Build.VERSION.SDK_INT >= 28) {
+                    extras.putParcelable(EXTRA_CALL_PERSON, mPerson.toAndroidPerson());
+                } else {
+                    extras.putParcelable(EXTRA_CALL_PERSON_COMPAT, mPerson.toBundle());
+                }
+            }
+            if (mVerificationIcon != null) {
+                if (Build.VERSION.SDK_INT >= 23) {
+                    extras.putParcelable(EXTRA_VERIFICATION_ICON, mVerificationIcon.toIcon(
+                            mBuilder.mContext));
+                } else {
+                    extras.putParcelable(EXTRA_VERIFICATION_ICON_COMPAT,
+                            mVerificationIcon.toBundle());
+                }
+            }
+            extras.putCharSequence(EXTRA_VERIFICATION_TEXT, mVerificationText);
+            extras.putParcelable(EXTRA_ANSWER_INTENT, mAnswerIntent);
+            extras.putParcelable(EXTRA_DECLINE_INTENT, mDeclineIntent);
+            extras.putParcelable(EXTRA_HANG_UP_INTENT, mHangUpIntent);
+            if (mAnswerButtonColor != null) {
+                extras.putInt(EXTRA_ANSWER_COLOR, mAnswerButtonColor);
+            }
+            if (mDeclineButtonColor != null) {
+                extras.putInt(EXTRA_DECLINE_COLOR, mDeclineButtonColor);
+            }
+        }
+
+        /**
+         * @hide
+         */
+        @RestrictTo(LIBRARY_GROUP_PREFIX)
+        @Override
+        @NonNull
+        protected String getClassName() {
+            return TEMPLATE_CLASS_NAME;
+        }
+
+        /**
+         * @hide
+         */
+        @RestrictTo(LIBRARY_GROUP_PREFIX)
+        @Override
+        public void apply(NotificationBuilderWithBuilderAccessor builderAccessor) {
+            if (Build.VERSION.SDK_INT >= 31) {
+                Notification.CallStyle style = null;
+                switch (mCallType) {
+                    case CALL_TYPE_INCOMING:
+                        style = Api31Impl.forIncomingCall(mPerson.toAndroidPerson(),
+                                mDeclineIntent, mAnswerIntent);
+                        break;
+                    case CALL_TYPE_ONGOING:
+                        style = Api31Impl.forOngoingCall(mPerson.toAndroidPerson(),
+                                mHangUpIntent);
+                        break;
+                    case CALL_TYPE_SCREENING:
+                        style = Api31Impl.forScreeningCall(mPerson.toAndroidPerson(),
+                                mHangUpIntent, mAnswerIntent);
+                        break;
+                    default:
+                        if (Log.isLoggable(TAG, Log.DEBUG)) {
+                            Log.d(TAG,
+                                    "Unrecognized call type in CallStyle: " + String.valueOf(
+                                            mCallType));
+                        }
+                }
+                if (style != null) {
+                    // Before applying the style, we clear the actions.
+                    Api24Impl.setActions(builderAccessor.getBuilder());
+
+                    Api16Impl.setBuilder(style, builderAccessor.getBuilder());
+                    if (mAnswerButtonColor != null) {
+                        Api31Impl.setAnswerButtonColorHint(style, mAnswerButtonColor);
+                    }
+                    if (mDeclineButtonColor != null) {
+                        Api31Impl.setDeclineButtonColorHint(style, mDeclineButtonColor);
+                    }
+                    Api31Impl.setVerificationText(style, mVerificationText);
+                    if (mVerificationIcon != null) {
+                        Api31Impl.setVerificationIcon(style,
+                                mVerificationIcon.toIcon(mBuilder.mContext));
+                    }
+                    Api31Impl.setIsVideo(style, mIsVideo);
+                }
+            } else {
+                // For versions before CallStyle existed, we fallback to an unstyled
+                // notification, and modify the builder directly to set the relevant fields.
+                // Fields not settable in the builder are added separately as part of the
+                // RemoteView.
+                Notification.Builder builder = builderAccessor.getBuilder();
+
+                // Sets the notification title to the caller name.
+                CharSequence title = mPerson != null ? mPerson.getName() : null;
+                builder.setContentTitle(title);
+
+                // Sets the text of the notification either to EXTRA_TEXT, or (if not set),
+                // uses the default call notification text.
+                CharSequence text =
+                        mBuilder.mExtras != null && mBuilder.mExtras.containsKey(EXTRA_TEXT)
+                                ? mBuilder.mExtras.getCharSequence(EXTRA_TEXT) : null;
+                if (text == null) {
+                    text = getDefaultText();
+                }
+                builder.setContentText(text);
+
+                // Adds person information to the notification.
+                if (mPerson != null) {
+                    // Adds the caller icon, if available.
+                    if (Build.VERSION.SDK_INT >= 23 && mPerson.getIcon() != null) {
+                        Api23Impl.setLargeIcon(builder,
+                                mPerson.getIcon().toIcon(mBuilder.mContext));
+                    }
+
+                    // Adds the caller person as being relevant to this notification.
+                    if (Build.VERSION.SDK_INT >= 28) {
+                        Api28Impl.addPerson(builder, mPerson.toAndroidPerson());
+                    } else if (Build.VERSION.SDK_INT >= 21) {
+                        Api21Impl.addPerson(builder, mPerson.getUri());
+                    }
+                }
+
+                // Adds actions to the notification.
+                if (Build.VERSION.SDK_INT >= 20) {
+                    // Retrieves call style actions, including contextual and system actions.
+                    List<Action> actionsList = getActionsListWithSystemActions();
+                    // Clear any existing actions.
+                    if (Build.VERSION.SDK_INT >= 24) {
+                        Api24Impl.setActions(builder);
+                    }
+                    // Adds the actions to the builder in the proper order.
+                    for (Action action : actionsList) {
+                        Api20Impl.addAction(builder, getActionFromActionCompat(action));
+                    }
+                }
+
+                // Sets the category of the notification to CATEGORY_CALL; if the notification
+                // has this set and is also from the default phone app, it will be ranked in the
+                // shade similarly to how CallStyle notifications are ranked in API 31+.
+                if (Build.VERSION.SDK_INT >= 21) {
+                    Api21Impl.setCategory(builder, NotificationCompat.CATEGORY_CALL);
+                }
+            }
+        }
+
+        /**
+         * Provides the default text for a CallStyle notification. Corresponds to Notification
+         * .CallStyle
+         */
+        @Nullable
+        private String getDefaultText() {
+            switch (mCallType) {
+                case CALL_TYPE_INCOMING:
+                    return mBuilder.mContext.getResources().getString(
+                            R.string.call_notification_incoming_text);
+                case CALL_TYPE_ONGOING:
+                    return mBuilder.mContext.getResources().getString(
+                            R.string.call_notification_ongoing_text);
+                case CALL_TYPE_SCREENING:
+                    return mBuilder.mContext.getResources().getString(
+                            R.string.call_notification_screening_text);
+            }
+            return null;
+        }
+
+        @NonNull
+        @RequiresApi(20)
+        private Action makeNegativeAction() {
+            int icon = R.drawable.ic_call_decline_low;
+            if (Build.VERSION.SDK_INT >= 21) {
+                icon = R.drawable.ic_call_decline;
+            }
+            if (mDeclineIntent == null) {
+                return makeAction(icon, R.string.call_notification_hang_up_action,
+                        mDeclineButtonColor,
+                        R.color.call_notification_decline_color,
+                        mHangUpIntent);
+            } else {
+                return makeAction(icon, R.string.call_notification_decline_action,
+                        mDeclineButtonColor,
+                        R.color.call_notification_decline_color,
+                        mDeclineIntent);
+            }
+        }
+
+        @Nullable
+        @RequiresApi(20)
+        private Action makeAnswerAction() {
+            int videoIcon = R.drawable.ic_call_answer_video_low;
+            int icon = R.drawable.ic_call_answer_low;
+            if (Build.VERSION.SDK_INT >= 21) {
+                videoIcon = R.drawable.ic_call_answer_video;
+                icon = R.drawable.ic_call_answer;
+            }
+
+            return mAnswerIntent == null ? null : makeAction(
+                    mIsVideo ? videoIcon : icon,
+                    mIsVideo ? R.string.call_notification_answer_video_action
+                            : R.string.call_notification_answer_action,
+                    mAnswerButtonColor, R.color.call_notification_answer_color,
+                    mAnswerIntent);
+        }
+
+        @NonNull
+        @RequiresApi(20)
+        private Action makeAction(int icon, int title, Integer colorInt, int defaultColorRes,
+                PendingIntent intent) {
+            if (colorInt == null) {
+                colorInt = ContextCompat.getColor(mBuilder.mContext, defaultColorRes);
+            }
+            SpannableStringBuilder stringBuilder = new SpannableStringBuilder();
+            stringBuilder.append(mBuilder.mContext.getResources().getString(title));
+            stringBuilder.setSpan(new ForegroundColorSpan(colorInt), 0, stringBuilder.length(),
+                    SpannableStringBuilder.SPAN_INCLUSIVE_INCLUSIVE);
+
+            Action action = new Action.Builder(
+                    IconCompat.createWithResource(mBuilder.mContext, icon), stringBuilder,
+                    intent).build();
+            action.getExtras().putBoolean(KEY_ACTION_PRIORITY, true);
+            return action;
+        }
+
+        private boolean isActionAddedByCallStyle(Action action) {
+            // This is an internal extra added by the style to these actions. If an app were to add
+            // this extra to the action themselves, the action would be dropped.  :shrug:
+            return action != null && action.getExtras().getBoolean(KEY_ACTION_PRIORITY);
+        }
+
+        /**
+         * Gets the actions list for the call with the answer/decline/hangUp actions inserted in
+         * the correct place.  This returns the correct result even if the system actions have
+         * already been added, and even if more actions were added since then.
+         *
+         * @hide
+         */
+        @RestrictTo(LIBRARY_GROUP_PREFIX)
+        @NonNull
+        @RequiresApi(20)
+        public ArrayList<Action> getActionsListWithSystemActions() {
+            // Define the system actions we expect to see.
+            final Action firstAction = makeNegativeAction();
+            final Action lastAction = makeAnswerAction();
+
+            // Start creating the result list.
+            int nonContextualActionSlotsRemaining = MAX_ACTION_BUTTONS;
+            ArrayList<Action> resultActions = new ArrayList<>(MAX_ACTION_BUTTONS);
+            if (nonContextualActionSlotsRemaining > 0) {
+                resultActions.add(firstAction);
+                --nonContextualActionSlotsRemaining;
+            }
+
+            // Copy actions into the new list, correcting system actions.
+            List<Action> existingActions = mBuilder.mActions;
+            if (existingActions != null) {
+                for (Action action : existingActions) {
+                    if (action.isContextual()) {
+                        // Always include all contextual actions
+                        resultActions.add(action);
+                    } else if (isActionAddedByCallStyle(action)) {
+                        // Drops any old versions of system actions.
+                    } else {
+                        // Copies non-contextual actions; decrement the remaining action slots.
+                        // Only do this if there are at least two slots left; the lastAction
+                        // needs a reserved space.
+                        if (nonContextualActionSlotsRemaining > 1) {
+                            resultActions.add(action);
+                            --nonContextualActionSlotsRemaining;
+                        }
+                    }
+                    // If there's exactly one action slot left, fill it with the lastAction.
+                    if (lastAction != null && nonContextualActionSlotsRemaining == 1) {
+                        resultActions.add(lastAction);
+                        --nonContextualActionSlotsRemaining;
+                    }
+                }
+            }
+            // If there are any action slots left, the lastAction still needs to be added.
+            if (lastAction != null && nonContextualActionSlotsRemaining >= 1) {
+                resultActions.add(lastAction);
+            }
+            return resultActions;
+        }
+
+        @RequiresApi(20)
+        private static Notification.Action getActionFromActionCompat(Action actionCompat) {
+            Notification.Action.Builder actionBuilder;
+            if (Build.VERSION.SDK_INT >= 23) {
+                IconCompat iconCompat = actionCompat.getIconCompat();
+                actionBuilder = Api23Impl.createActionBuilder(
+                        iconCompat == null ? null : iconCompat.toIcon(), actionCompat.getTitle(),
+                        actionCompat.getActionIntent());
+            } else {
+                IconCompat icon = actionCompat.getIconCompat();
+                int iconResId = 0;
+                if (icon != null && icon.getType() == IconCompat.TYPE_RESOURCE) {
+                    iconResId = icon.getResId();
+                }
+                actionBuilder =
+                        Api20Impl.createActionBuilder(iconResId, actionCompat.getTitle(),
+                        actionCompat.getActionIntent());
+            }
+            Bundle actionExtras;
+            if (actionCompat.getExtras() != null) {
+                actionExtras = new Bundle(actionCompat.getExtras());
+            } else {
+                actionExtras = new Bundle();
+            }
+            actionExtras.putBoolean(NotificationCompatJellybean.EXTRA_ALLOW_GENERATED_REPLIES,
+                    actionCompat.getAllowGeneratedReplies());
+            if (Build.VERSION.SDK_INT >= 24) {
+                Api24Impl.setAllowGeneratedReplies(actionBuilder,
+                        actionCompat.getAllowGeneratedReplies());
+            }
+            if (Build.VERSION.SDK_INT >= 31) {
+                Api31Impl.setAuthenticationRequired(actionBuilder,
+                        actionCompat.isAuthenticationRequired());
+            }
+            Api20Impl.addExtras(actionBuilder, actionExtras);
+            RemoteInput[] remoteInputCompats = actionCompat.getRemoteInputs();
+            if (remoteInputCompats != null) {
+                android.app.RemoteInput[] remoteInputs = RemoteInput.fromCompat(remoteInputCompats);
+                for (android.app.RemoteInput remoteInput : remoteInputs) {
+                    Api20Impl.addRemoteInput(actionBuilder, remoteInput);
+                }
+            }
+            return Api20Impl.build(actionBuilder);
+        }
+
+        /**
+         * A class for wrapping calls to {@link Notification.CallStyle} methods which
+         * were added in API 16; these calls must be wrapped to avoid performance issues.
+         * See the UnsafeNewApiCall lint rule for more details.
+         */
+        @RequiresApi(16)
+        static class Api16Impl {
+            private Api16Impl() { }
+
+            @DoNotInline
+            static void setBuilder(Notification.CallStyle style, Notification.Builder builder) {
+                style.setBuilder(builder);
+            }
+        }
+
+        /**
+         * A class for wrapping calls to {@link Notification.CallStyle} methods which
+         * were added in API 20; these calls must be wrapped to avoid performance issues.
+         * See the UnsafeNewApiCall lint rule for more details.
+         */
+        @RequiresApi(20)
+        static class Api20Impl {
+            private Api20Impl() {
+            }
+
+            @DoNotInline
+            static Notification.Builder addAction(Notification.Builder builder,
+                    Notification.Action action) {
+                return builder.addAction(action);
+            }
+
+            @DoNotInline
+            static Notification.Action build(Notification.Action.Builder builder) {
+                return builder.build();
+            }
+
+            @DoNotInline
+            static Notification.Action.Builder createActionBuilder(int icon,
+                    CharSequence title,
+                    android.app.PendingIntent intent) {
+                return new Notification.Action.Builder(icon, title, intent);
+
+            }
+
+            @DoNotInline
+            static Notification.Action.Builder addExtras(Notification.Action.Builder builder,
+                    android.os.Bundle extras) {
+                return builder.addExtras(extras);
+            }
+
+            @DoNotInline
+            static Notification.Action.Builder addRemoteInput(Notification.Action.Builder builder,
+                    android.app.RemoteInput remoteInput) {
+                return builder.addRemoteInput(remoteInput);
+            }
+        }
+
+        /**
+         * A class for wrapping calls to {@link Notification.CallStyle} methods which
+         * were added in API 21; these calls must be wrapped to avoid performance issues.
+         * See the UnsafeNewApiCall lint rule for more details.
+         */
+        @RequiresApi(21)
+        static class Api21Impl {
+            private Api21Impl() {
+            }
+
+            @DoNotInline
+            static Notification.Builder addPerson(Notification.Builder builder, String uri) {
+                return builder.addPerson(uri);
+            }
+
+            @DoNotInline
+            static Notification.Builder setCategory(Notification.Builder builder, String category) {
+                return builder.setCategory(category);
+            }
+        }
+
+        /**
+         * A class for wrapping calls to {@link Notification.CallStyle} methods which
+         * were added in API 23; these calls must be wrapped to avoid performance issues.
+         * See the UnsafeNewApiCall lint rule for more details.
+         */
+        @RequiresApi(23)
+        static class Api23Impl {
+            private Api23Impl() {
+            }
+
+            @DoNotInline
+            static void setLargeIcon(Notification.Builder builder,
+                    Icon icon) {
+                builder.setLargeIcon(icon);
+            }
+
+            @DoNotInline
+            static Notification.Action.Builder createActionBuilder(
+                    android.graphics.drawable.Icon icon,
+                    CharSequence title,
+                    android.app.PendingIntent intent) {
+                return new Notification.Action.Builder(icon, title, intent);
+            }
+        }
+
+        /**
+         * A class for wrapping calls to {@link Notification.CallStyle} methods which
+         * were added in API 24; these calls must be wrapped to avoid performance issues.
+         * See the UnsafeNewApiCall lint rule for more details.
+         */
+        @RequiresApi(24)
+        static class Api24Impl {
+            private Api24Impl() {
+            }
+
+            @DoNotInline
+            static Notification.Builder setActions(Notification.Builder builder,
+                    Notification.Action... actions) {
+                return builder.setActions(actions);
+            }
+
+            @DoNotInline
+            static Notification.Action.Builder setAllowGeneratedReplies(
+                    Notification.Action.Builder builder, boolean allowGeneratedReplies) {
+                return builder.setAllowGeneratedReplies(allowGeneratedReplies);
+            }
+        }
+
+        /**
+         * A class for wrapping calls to {@link Notification.CallStyle} methods which
+         * were added in API 28; these calls must be wrapped to avoid performance issues.
+         * See the UnsafeNewApiCall lint rule for more details.
+         */
+        @RequiresApi(28)
+        static class Api28Impl {
+            private Api28Impl() {
+            }
+
+            @DoNotInline
+            static Notification.Builder addPerson(Notification.Builder builder,
+                    android.app.Person person) {
+                return builder.addPerson(person);
+            }
+        }
+
+        /**
+         * A class for wrapping calls to {@link Notification.CallStyle} methods which
+         * were added in API 31; these calls must be wrapped to avoid performance issues.
+         * See the UnsafeNewApiCall lint rule for more details.
+         */
+        @RequiresApi(31)
+        static class Api31Impl {
+            private Api31Impl() {
+            }
+
+            @DoNotInline
+            static Notification.CallStyle forIncomingCall(@NonNull android.app.Person person,
+                    @NonNull PendingIntent declineIntent, @NonNull PendingIntent answerIntent) {
+                return Notification.CallStyle.forIncomingCall(person, declineIntent, answerIntent);
+            }
+
+            @DoNotInline
+            static Notification.CallStyle forOngoingCall(@NonNull android.app.Person person,
+                    @NonNull PendingIntent hangUpIntent) {
+                return Notification.CallStyle.forOngoingCall(person, hangUpIntent);
+            }
+
+            @DoNotInline
+            static Notification.CallStyle forScreeningCall(@NonNull android.app.Person person,
+                    @NonNull PendingIntent hangUpIntent, @NonNull PendingIntent answerIntent) {
+                return Notification.CallStyle.forScreeningCall(person, hangUpIntent, answerIntent);
+            }
+
+            @DoNotInline
+            static Notification.CallStyle setIsVideo(Notification.CallStyle callStyle,
+                    boolean isVideo) {
+                return callStyle.setIsVideo(isVideo);
+            }
+
+            @DoNotInline
+            static Notification.CallStyle setVerificationIcon(Notification.CallStyle callStyle,
+                    @Nullable Icon verificationIcon) {
+                return callStyle.setVerificationIcon(verificationIcon);
+            }
+
+            @DoNotInline
+            static Notification.CallStyle setVerificationText(Notification.CallStyle callStyle,
+                    @Nullable CharSequence verificationText) {
+                return callStyle.setVerificationText(verificationText);
+            }
+
+            @DoNotInline
+            static Notification.CallStyle setAnswerButtonColorHint(Notification.CallStyle callStyle,
+                    @ColorInt int color) {
+                return callStyle.setAnswerButtonColorHint(color);
+            }
+
+            @DoNotInline
+            static Notification.CallStyle setDeclineButtonColorHint(
+                    Notification.CallStyle callStyle, @ColorInt int color) {
+                return callStyle.setDeclineButtonColorHint(color);
+            }
+
+            @DoNotInline
+            static Notification.Action.Builder setAuthenticationRequired(
+                    Notification.Action.Builder actionBuilder, boolean authenticationRequired) {
+                return actionBuilder.setAuthenticationRequired(authenticationRequired);
+            }
+        }
+    }
+
+    /**
      * Helper class for generating large-format notifications that include a list of (up to 5) strings.
      *
      * <br>
@@ -4900,8 +5847,7 @@
             IconCompat icon = action.getIconCompat();
             if (icon != null) {
                 button.setImageViewBitmap(R.id.action_image,
-                        createColoredBitmap(icon, mBuilder.mContext.getResources()
-                                .getColor(R.color.notification_action_color_filter)));
+                        createColoredBitmap(icon, R.color.notification_action_color_filter));
             }
             button.setTextViewText(R.id.action_text, action.title);
             if (!tombstone) {
@@ -5086,7 +6032,7 @@
          * Intent to send when the user invokes this action. May be null, in which case the action
          * may be rendered in a disabled presentation.
          */
-        public PendingIntent actionIntent;
+        public @Nullable PendingIntent actionIntent;
 
         private boolean mAuthenticationRequired;
 
@@ -5310,7 +6256,7 @@
             public Builder(@Nullable IconCompat icon, @Nullable CharSequence title,
                     @Nullable PendingIntent intent) {
                 this(icon, title, intent, new Bundle(), null, true, SEMANTIC_ACTION_NONE, true,
-                        false /* isContextual */, false /* authRequiored */);
+                        false /* isContextual */, false /* authRequired */);
             }
 
             /**
diff --git a/core/core/src/main/res/drawable/ic_call_answer.xml b/core/core/src/main/res/drawable/ic_call_answer.xml
new file mode 100644
index 0000000..da8a42f
--- /dev/null
+++ b/core/core/src/main/res/drawable/ic_call_answer.xml
@@ -0,0 +1,36 @@
+<!--
+     Copyright (C) 2022 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.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    tools:targetApi="21"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="20"
+    android:viewportHeight="20"
+    android:tint="?android:attr/colorControlNormal"
+    android:autoMirrored="true">
+    <path
+        android:fillColor="#FF000000"
+        android:pathData="M6.0168,3.3333L6.5751,5.575L4.6668,7.4916C3.8751,5.7166 3.6001,4.1916
+            3.4834,3.3333H6.0168ZM14.4251,13.375L16.6668,13.9416V16.5166C15.8084,16.4 14.2668,16.125
+            12.4834,15.325L14.4251,13.375ZM6.3418,1.6666H2.5668C2.0918,1.6666 1.7001,2.0666
+            1.7334,2.5416C2.4834,12.875 11.7668,18.275 17.5168,18.275C17.9668,18.275 18.3334,17.9
+            18.3334,17.4416V13.6166C18.3334,13.0416 17.9418,12.5416
+            17.3834,12.4083L14.5918,11.7083C14.2251,11.6166 13.7584,11.6833
+            13.4084,12.0333L10.9251,14.5166C8.6751,13.1833 6.7918,11.3
+            5.4668,9.0416L7.9168,6.5916C8.2251,6.2833 8.3501,5.8333
+            8.2418,5.4083L7.5584,2.6166C7.4168,2.0583 6.9168,1.6666 6.3418,1.6666Z"/>
+</vector>
diff --git a/core/core/src/main/res/drawable/ic_call_answer_low.xml b/core/core/src/main/res/drawable/ic_call_answer_low.xml
new file mode 100644
index 0000000..89e62a5
--- /dev/null
+++ b/core/core/src/main/res/drawable/ic_call_answer_low.xml
@@ -0,0 +1,33 @@
+<!--
+     Copyright (C) 2022 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.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="20"
+    android:viewportHeight="20"
+    android:autoMirrored="true">
+    <path
+        android:fillColor="#FF000000"
+        android:pathData="M6.0168,3.3333L6.5751,5.575L4.6668,7.4916C3.8751,5.7166 3.6001,4.1916
+            3.4834,3.3333H6.0168ZM14.4251,13.375L16.6668,13.9416V16.5166C15.8084,16.4 14.2668,16.125
+            12.4834,15.325L14.4251,13.375ZM6.3418,1.6666H2.5668C2.0918,1.6666 1.7001,2.0666
+            1.7334,2.5416C2.4834,12.875 11.7668,18.275 17.5168,18.275C17.9668,18.275 18.3334,17.9
+            18.3334,17.4416V13.6166C18.3334,13.0416 17.9418,12.5416
+            17.3834,12.4083L14.5918,11.7083C14.2251,11.6166 13.7584,11.6833
+            13.4084,12.0333L10.9251,14.5166C8.6751,13.1833 6.7918,11.3
+            5.4668,9.0416L7.9168,6.5916C8.2251,6.2833 8.3501,5.8333
+            8.2418,5.4083L7.5584,2.6166C7.4168,2.0583 6.9168,1.6666 6.3418,1.6666Z"/>
+</vector>
diff --git a/core/core/src/main/res/drawable/ic_call_answer_video.xml b/core/core/src/main/res/drawable/ic_call_answer_video.xml
new file mode 100644
index 0000000..243ca3c
--- /dev/null
+++ b/core/core/src/main/res/drawable/ic_call_answer_video.xml
@@ -0,0 +1,29 @@
+<!--
+     Copyright (C) 2022 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.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    tools:targetApi="21"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="20"
+    android:viewportHeight="20"
+    android:tint="?android:attr/colorControlNormal"
+    android:autoMirrored="true">
+    <path
+        android:fillColor="#FF000000"
+        android:pathData="M15,8v8H5V8h10m1,-2H4c-0.55,0 -1,0.45 -1,1v10c0,0.55 0.45,1 1,1h12c0.55,0
+            1,-0.45 1,-1v-3.5l4,4v-11l-4,4V7c0,-0.55 -0.45,-1 -1,-1z"/>
+</vector>
diff --git a/core/core/src/main/res/drawable/ic_call_answer_video_low.xml b/core/core/src/main/res/drawable/ic_call_answer_video_low.xml
new file mode 100644
index 0000000..24b6942
--- /dev/null
+++ b/core/core/src/main/res/drawable/ic_call_answer_video_low.xml
@@ -0,0 +1,26 @@
+<!--
+     Copyright (C) 2022 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.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="20"
+    android:viewportHeight="20"
+    android:autoMirrored="true">
+    <path
+        android:fillColor="#FF000000"
+        android:pathData="M15,8v8H5V8h10m1,-2H4c-0.55,0 -1,0.45 -1,1v10c0,0.55 0.45,1 1,1h12c0.55,0
+            1,-0.45 1,-1v-3.5l4,4v-11l-4,4V7c0,-0.55 -0.45,-1 -1,-1z"/>
+</vector>
diff --git a/core/core/src/main/res/drawable/ic_call_decline.xml b/core/core/src/main/res/drawable/ic_call_decline.xml
new file mode 100644
index 0000000..be9593c
--- /dev/null
+++ b/core/core/src/main/res/drawable/ic_call_decline.xml
@@ -0,0 +1,38 @@
+<!--
+     Copyright (C) 2022 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.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    tools:targetApi="21"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="20"
+    android:viewportHeight="20"
+    android:tint="?android:attr/colorControlNormal"
+    android:autoMirrored="true">
+    <path
+        android:fillColor="#FF000000"
+        android:pathData="M5.2834,9.3083V10.7833L4.1084,11.4916L3.1334,10.5166C3.8084,10.0416
+            4.5251,9.6333 5.2834,9.3083ZM14.75,9.325C15.4917,9.65 16.2084,10.05
+            16.875,10.525L15.925,11.475L14.75,10.7666V9.325ZM9.975,6.6666C6.8584,6.6666 3.725,7.7333
+            1.1751,9.9416C0.8084,10.2583 0.9667,10.7166 1.1501,10.9L3.2917,13.0416C3.4917,13.2333
+            3.7417,13.3333 4.0001,13.3333C4.175,13.3333 4.35,13.2833
+            4.5084,13.1916L6.4667,12.0166C6.725,11.8583 6.95,11.5583 6.95,11.1666V8.3833C7.95,8.125
+            8.975,8 10.0084,8C11.0417,8 12.075,8.1333 13.0834,8.3916V11.1416C13.0834,11.4916
+            13.2667,11.8166 13.5667,11.9916L15.525,13.1666C15.6834,13.2583 15.8584,13.3083
+            16.0334,13.3083C16.2917,13.3083 16.5417,13.2083
+            16.7334,13.0166L18.85,10.9C19.1167,10.6333 19.1167,10.1916 18.825,9.9416C16.3334,7.7833
+            13.1667,6.6666 9.975,6.6666Z"/>
+</vector>
diff --git a/core/core/src/main/res/drawable/ic_call_decline_low.xml b/core/core/src/main/res/drawable/ic_call_decline_low.xml
new file mode 100644
index 0000000..990af5f
--- /dev/null
+++ b/core/core/src/main/res/drawable/ic_call_decline_low.xml
@@ -0,0 +1,35 @@
+<!--
+     Copyright (C) 2022 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.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="20"
+    android:viewportHeight="20"
+    android:autoMirrored="true">
+    <path
+        android:fillColor="#FF000000"
+        android:pathData="M5.2834,9.3083V10.7833L4.1084,11.4916L3.1334,10.5166C3.8084,10.0416
+            4.5251,9.6333 5.2834,9.3083ZM14.75,9.325C15.4917,9.65 16.2084,10.05
+            16.875,10.525L15.925,11.475L14.75,10.7666V9.325ZM9.975,6.6666C6.8584,6.6666 3.725,7.7333
+            1.1751,9.9416C0.8084,10.2583 0.9667,10.7166 1.1501,10.9L3.2917,13.0416C3.4917,13.2333
+            3.7417,13.3333 4.0001,13.3333C4.175,13.3333 4.35,13.2833
+            4.5084,13.1916L6.4667,12.0166C6.725,11.8583 6.95,11.5583 6.95,11.1666V8.3833C7.95,8.125
+            8.975,8 10.0084,8C11.0417,8 12.075,8.1333 13.0834,8.3916V11.1416C13.0834,11.4916
+            13.2667,11.8166 13.5667,11.9916L15.525,13.1666C15.6834,13.2583 15.8584,13.3083
+            16.0334,13.3083C16.2917,13.3083 16.5417,13.2083
+            16.7334,13.0166L18.85,10.9C19.1167,10.6333 19.1167,10.1916 18.825,9.9416C16.3334,7.7833
+            13.1667,6.6666 9.975,6.6666Z"/>
+</vector>
diff --git a/core/core/src/main/res/values/colors.xml b/core/core/src/main/res/values/colors.xml
index a3eccb7..cad8425 100644
--- a/core/core/src/main/res/values/colors.xml
+++ b/core/core/src/main/res/values/colors.xml
@@ -19,4 +19,9 @@
     <drawable name="notification_template_icon_low_bg">#0cffffff</drawable>
     <color name="notification_action_color_filter">#ffffffff</color>
     <color name="notification_icon_bg_color">#ff9e9e9e</color>
+
+    <!-- The color of the Decline and Hang Up actions on a CallStyle notification -->
+    <color name="call_notification_decline_color">#d93025</color>
+    <!-- The color of the Answer action on a CallStyle notification -->
+    <color name="call_notification_answer_color">#1d873b</color>
 </resources>
diff --git a/core/core/src/main/res/values/strings.xml b/core/core/src/main/res/values/strings.xml
index ceeb183..fb0cfa8 100644
--- a/core/core/src/main/res/values/strings.xml
+++ b/core/core/src/main/res/values/strings.xml
@@ -22,4 +22,18 @@
          for most appropriate textual indicator of "more than X".
          [CHAR LIMIT=4] -->
     <string name="status_bar_notification_info_overflow">999+</string>
+    <!-- Action text to be displayed for the "answer" action of an incoming call [CHAR LIMIT=13] -->
+    <string name="call_notification_answer_action">Answer</string>
+    <!-- Action text to be displayed for the "answer" action of an incoming VIDEO call [CHAR LIMIT=13] -->
+    <string name="call_notification_answer_video_action">Video</string>
+    <!-- Action text to be displayed for the "decline" action of an incoming call [CHAR LIMIT=13] -->
+    <string name="call_notification_decline_action">Decline</string>
+    <!-- Action text to be displayed for the "hang up" action of an ongoing call [CHAR LIMIT=13] -->
+    <string name="call_notification_hang_up_action">Hang Up</string>
+    <!-- Default notification text to be displayed in incoming call notifications [CHAR LIMIT=40] -->
+    <string name="call_notification_incoming_text">Incoming call</string>
+    <!-- Default notification text to be displayed in ongoing call notifications [CHAR LIMIT=40] -->
+    <string name="call_notification_ongoing_text">Ongoing call</string>
+    <!-- Default notification text to be displayed in screening call notifications [CHAR LIMIT=40] -->
+    <string name="call_notification_screening_text">Screening an incoming call</string>
 </resources>