Merge "Add more tests to NotificationMsgDelegate" into rvc-dev
diff --git a/src/com/android/car/companiondevicesupport/feature/notificationmsg/NotificationMsgDelegate.java b/src/com/android/car/companiondevicesupport/feature/notificationmsg/NotificationMsgDelegate.java
index e2c3ab7..9c00b93 100644
--- a/src/com/android/car/companiondevicesupport/feature/notificationmsg/NotificationMsgDelegate.java
+++ b/src/com/android/car/companiondevicesupport/feature/notificationmsg/NotificationMsgDelegate.java
@@ -59,12 +59,12 @@
     private static final String TAG = "NotificationMsgDelegate";
 
     /** Key for the Reply string in a {@link MapEntry}. **/
-    private static final String REPLY_KEY = "REPLY";
+    protected static final String REPLY_KEY = "REPLY";
     /**
      * Value for {@link ClearAppDataRequest#getMessagingAppPackageName()}, representing
      * when all messaging applications' data should be removed.
      */
-    private static final String REMOVE_ALL_APP_DATA = "ALL";
+    protected static final String REMOVE_ALL_APP_DATA = "ALL";
 
     private static final AudioAttributes AUDIO_ATTRIBUTES = new AudioAttributes.Builder()
             .setUsage(AudioAttributes.USAGE_NOTIFICATION)
@@ -83,7 +83,7 @@
     protected final Map<SenderKey, Bitmap> mOneOnOneConversationAvatarMap = new HashMap<>();
 
     /** Tracks whether a projection application is active in the foreground. **/
-    private final ProjectionStateListener mProjectionStateListener;
+    private ProjectionStateListener mProjectionStateListener;
 
     public NotificationMsgDelegate(Context context) {
         super(context, /* useLetterTile */ false);
@@ -279,9 +279,9 @@
             mAppNameToChannel.put(appDisplayName,
                     new NotificationChannelWrapper(appDisplayName));
         }
-        boolean isProjectionActive = mProjectionStateListener.isProjectionInActiveForeground(
-                        mConnectedDeviceBluetoothAddress);
-        return mAppNameToChannel.get(appDisplayName).getChannelId(isProjectionActive);
+        return mAppNameToChannel.get(appDisplayName).getChannelId(
+                mProjectionStateListener.isProjectionInActiveForeground(
+                        mConnectedDeviceBluetoothAddress));
     }
 
     private void createNewMessage(String deviceAddress, MessagingStyleMessage messagingStyleMessage,
@@ -367,4 +367,9 @@
     void setNotificationManager(NotificationManager manager) {
         mNotificationManager = manager;
     }
+
+    @VisibleForTesting
+    void setProjectionStateListener(ProjectionStateListener listener) {
+        mProjectionStateListener = listener;
+    }
 }
diff --git a/tests/unit/src/com/android/car/companiondevicesupport/feature/notificationmsg/NotificationMsgDelegateTest.java b/tests/unit/src/com/android/car/companiondevicesupport/feature/notificationmsg/NotificationMsgDelegateTest.java
index 6adbf61..e41df2c 100644
--- a/tests/unit/src/com/android/car/companiondevicesupport/feature/notificationmsg/NotificationMsgDelegateTest.java
+++ b/tests/unit/src/com/android/car/companiondevicesupport/feature/notificationmsg/NotificationMsgDelegateTest.java
@@ -17,21 +17,33 @@
 package com.android.car.companiondevicesupport.feature.notificationmsg;
 
 
+import static android.app.NotificationManager.IMPORTANCE_HIGH;
+import static android.app.NotificationManager.IMPORTANCE_LOW;
+
 import static androidx.core.app.NotificationCompat.CATEGORY_MESSAGE;
 
+import static com.android.car.messenger.NotificationMsgProto.NotificationMsg.Action.ActionName.DISMISS;
+import static com.android.car.messenger.NotificationMsgProto.NotificationMsg.Action.ActionName.MARK_AS_READ;
+import static com.android.car.messenger.NotificationMsgProto.NotificationMsg.Action.ActionName.REPLY;
+
 import static com.google.common.truth.Truth.assertThat;
 
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.atLeastOnce;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyZeroInteractions;
 import static org.mockito.Mockito.when;
 
 import android.app.Notification;
+import android.app.NotificationChannel;
 import android.app.NotificationManager;
 import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
 import android.graphics.drawable.Icon;
 
 import androidx.core.app.NotificationCompat;
@@ -39,21 +51,35 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 
 import com.android.car.companiondevicesupport.api.external.CompanionDevice;
+import com.android.car.messenger.NotificationMsgProto.NotificationMsg;
+import com.android.car.messenger.NotificationMsgProto.NotificationMsg.Action;
+import com.android.car.messenger.NotificationMsgProto.NotificationMsg.AvatarIconSync;
+import com.android.car.messenger.NotificationMsgProto.NotificationMsg.CarToPhoneMessage;
+import com.android.car.messenger.NotificationMsgProto.NotificationMsg.ClearAppDataRequest;
 import com.android.car.messenger.NotificationMsgProto.NotificationMsg.ConversationNotification;
 import com.android.car.messenger.NotificationMsgProto.NotificationMsg.MessagingStyle;
 import com.android.car.messenger.NotificationMsgProto.NotificationMsg.MessagingStyleMessage;
 import com.android.car.messenger.NotificationMsgProto.NotificationMsg.Person;
+import com.android.car.messenger.NotificationMsgProto.NotificationMsg.PhoneMetadata;
 import com.android.car.messenger.NotificationMsgProto.NotificationMsg.PhoneToCarMessage;
+import com.android.car.messenger.common.ConversationKey;
+import com.android.car.messenger.common.ProjectionStateListener;
+import com.android.car.messenger.common.SenderKey;
+import com.android.car.protobuf.ByteString;
 
+import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
+import java.io.ByteArrayOutputStream;
 import java.util.ArrayList;
 import java.util.List;
+import java.util.UUID;
 
 @RunWith(AndroidJUnit4.class)
 public class NotificationMsgDelegateTest {
@@ -62,15 +88,23 @@
 
     private static final String COMPANION_DEVICE_ID = "sampleId";
     private static final String COMPANION_DEVICE_NAME = "sampleName";
+    private static final String DEVICE_ADDRESS = UUID.randomUUID().toString();
+    private static final String BT_DEVICE_ADDRESS = UUID.randomUUID().toString();
 
     private static final String MESSAGING_APP_NAME = "Messaging App";
     private static final String MESSAGING_PACKAGE_NAME = "com.android.messaging.app";
     private static final String CONVERSATION_TITLE = "Conversation";
     private static final String USER_DISPLAY_NAME = "User";
     private static final String SENDER_1 = "Sender";
+    private static final String MESSAGE_TEXT_1 = "Message 1";
+    private static final String MESSAGE_TEXT_2 = "Message 2";
+
+    /** ConversationKey for {@link NotificationMsgDelegateTest#VALID_CONVERSATION_MSG}. **/
+    private static final ConversationKey CONVERSATION_KEY_1
+            = new ConversationKey(COMPANION_DEVICE_ID, NOTIFICATION_KEY_1);
 
     private static final MessagingStyleMessage MESSAGE_2 = MessagingStyleMessage.newBuilder()
-            .setTextMessage("Message 2")
+            .setTextMessage(MESSAGE_TEXT_2)
             .setSender(Person.newBuilder()
                     .setName(SENDER_1))
             .setTimestamp((long) 1577909718950f)
@@ -81,7 +115,7 @@
             .setUserDisplayName(USER_DISPLAY_NAME)
             .setIsGroupConvo(false)
             .addMessagingStyleMsg(MessagingStyleMessage.newBuilder()
-                    .setTextMessage("Message 1")
+                    .setTextMessage(MESSAGE_TEXT_1)
                     .setSender(Person.newBuilder()
                             .setName(SENDER_1))
                     .setTimestamp((long) 1577909718050f)
@@ -101,14 +135,20 @@
             .setConversation(VALID_CONVERSATION)
             .build();
 
+    private Bitmap mIconBitmap;
+    private byte[] mIconByteArray;
+
     @Mock
     CompanionDevice mCompanionDevice;
     @Mock
     NotificationManager mMockNotificationManager;
+    @Mock
+    ProjectionStateListener mMockProjectionStateListener;
 
-    ArgumentCaptor<Notification> mNotificationCaptor =
-            ArgumentCaptor.forClass(Notification.class);
-    ArgumentCaptor<Integer> mNotificationIdCaptor = ArgumentCaptor.forClass(Integer.class);
+    @Captor
+    ArgumentCaptor<Notification> mNotificationCaptor;
+    @Captor
+    ArgumentCaptor<Integer> mNotificationIdCaptor;
 
     Context mContext = ApplicationProvider.getApplicationContext();
     NotificationMsgDelegate mNotificationMsgDelegate;
@@ -122,6 +162,20 @@
 
         mNotificationMsgDelegate = new NotificationMsgDelegate(mContext);
         mNotificationMsgDelegate.setNotificationManager(mMockNotificationManager);
+        mNotificationMsgDelegate.setProjectionStateListener(mMockProjectionStateListener);
+
+        mIconBitmap = Bitmap.createBitmap(200, 200, Bitmap.Config.ARGB_8888);
+
+        ByteArrayOutputStream stream = new ByteArrayOutputStream();
+        mIconBitmap.compress(Bitmap.CompressFormat.PNG, 100, stream);
+        mIconByteArray = stream.toByteArray();
+        stream.reset();
+
+    }
+
+    @After
+    public void tearDown() {
+        mIconBitmap.recycle();
     }
 
     @Test
@@ -174,11 +228,8 @@
         int messageCount = VALID_CONVERSATION_MSG.getConversation().getMessagingStyle()
                 .getMessagingStyleMsgCount();
 
-        PhoneToCarMessage updateConvo = PhoneToCarMessage.newBuilder()
-                .setNotificationKey(NOTIFICATION_KEY_1)
-                .setMessage(MESSAGE_2)
-                .build();
-        mNotificationMsgDelegate.onMessageReceived(mCompanionDevice, updateConvo);
+        // Post a new message in this conversation.
+        updateConversationWithMessage2();
 
         // Verify same notification id is posted twice.
         verify(mMockNotificationManager, times(2)).notify(eq(notificationId),
@@ -223,11 +274,7 @@
     @Test
     public void messageForUnknownConversationShouldDoNothing() {
         // A message for an unknown conversation should be dropped.
-        PhoneToCarMessage updateConvo = PhoneToCarMessage.newBuilder()
-                .setNotificationKey(NOTIFICATION_KEY_1)
-                .setMessage(MESSAGE_2)
-                .build();
-        mNotificationMsgDelegate.onMessageReceived(mCompanionDevice, updateConvo);
+        updateConversationWithMessage2();
 
         verify(mMockNotificationManager, never()).notify(anyInt(), any(Notification.class));
     }
@@ -252,6 +299,309 @@
         verify(mMockNotificationManager).notify(anyInt(), any(Notification.class));
     }
 
+    @Test
+    public void invalidAvatarIconSyncShouldDoNothing() {
+        mNotificationMsgDelegate.onMessageReceived(mCompanionDevice, VALID_CONVERSATION_MSG);
+
+        // Create AvatarIconSync message without required field (Icon), ensure it's treated as an
+        // invalid message.
+        PhoneToCarMessage invalidMessage = PhoneToCarMessage.newBuilder()
+                .setNotificationKey(NOTIFICATION_KEY_1)
+                .setAvatarIconSync(AvatarIconSync.newBuilder()
+                        .setPerson(Person.newBuilder()
+                                .setName(SENDER_1))
+                        .build())
+                .build();
+        mNotificationMsgDelegate.onMessageReceived(mCompanionDevice, invalidMessage);
+        assertThat(mNotificationMsgDelegate.mOneOnOneConversationAvatarMap.isEmpty()).isTrue();
+    }
+
+    @Test
+    public void avatarIconSyncForGroupConversationShouldDoNothing() {
+        // We only sync avatars for 1-1 conversations.
+        sendGroupConversationMessage();
+
+        sendValidAvatarIconSyncMessage();
+
+        assertThat(mNotificationMsgDelegate.mOneOnOneConversationAvatarMap.isEmpty()).isTrue();
+    }
+
+    @Test
+    public void avatarIconSyncForUnknownConversationShouldDoNothing() {
+        // Drop avatar if it's for a conversation that is unknown.
+        sendValidAvatarIconSyncMessage();
+
+        assertThat(mNotificationMsgDelegate.mOneOnOneConversationAvatarMap.isEmpty()).isTrue();
+    }
+
+    @Test
+    public void avatarIconSyncSetsAvatarInNotification() {
+        // Check that a conversation that didn't have an avatar, but gets this message posts
+        // a notification with the avatar.
+        mNotificationMsgDelegate.onMessageReceived(mCompanionDevice, VALID_CONVERSATION_MSG);
+        verify(mMockNotificationManager).notify(anyInt(), mNotificationCaptor.capture());
+        Icon notSetIcon = mNotificationCaptor.getValue().getLargeIcon();
+
+        sendValidAvatarIconSyncMessage();
+
+        // Post an update so we update the notification and can see the new icon.
+        updateConversationWithMessage2();
+
+        verify(mMockNotificationManager, times(2)).notify(anyInt(), mNotificationCaptor.capture());
+        Icon newIcon = mNotificationCaptor.getValue().getLargeIcon();
+        assertThat(newIcon).isNotEqualTo(notSetIcon);
+    }
+
+
+    @Test
+    public void avatarIconSyncStoresBitmapCorrectly() {
+        // Post a conversation notification first, so we don't drop the avatar message.
+        mNotificationMsgDelegate.onMessageReceived(mCompanionDevice, VALID_CONVERSATION_MSG);
+
+        sendValidAvatarIconSyncMessage();
+
+        AvatarIconSync iconSync = createValidAvatarIconSync();
+        SenderKey senderKey = SenderKey.createSenderKey(CONVERSATION_KEY_1, iconSync.getPerson());
+        byte[] iconArray = iconSync.getPerson().getAvatar().toByteArray();
+        Bitmap bitmap = BitmapFactory.decodeByteArray(iconArray, /* offset= */ 0, iconArray.length);
+
+        assertThat(mNotificationMsgDelegate.mOneOnOneConversationAvatarMap).hasSize(1);
+        Bitmap actualBitmap = mNotificationMsgDelegate.mOneOnOneConversationAvatarMap.get(
+                senderKey);
+        assertThat(actualBitmap).isNotNull();
+        assertThat(actualBitmap.sameAs(bitmap)).isTrue();
+    }
+
+    @Test
+    public void phoneMetadataUsedToCheckProjectionStatus_projectionActive() {
+        // Assert projectionListener gets called with phone metadata address.
+        when(mMockProjectionStateListener.isProjectionInActiveForeground(
+                BT_DEVICE_ADDRESS)).thenReturn(true);
+        sendValidPhoneMetadataMessage();
+
+        // Send a new conversation to trigger Projection State check.
+        mNotificationMsgDelegate.onMessageReceived(mCompanionDevice, VALID_CONVERSATION_MSG);
+
+        verify(mMockProjectionStateListener).isProjectionInActiveForeground(BT_DEVICE_ADDRESS);
+        verify(mMockNotificationManager).notify(anyInt(), mNotificationCaptor.capture());
+        checkChannelImportanceLevel(
+                mNotificationCaptor.getValue().getChannelId(), /* isLowImportance= */ true);
+    }
+
+    @Test
+    public void phoneMetadataUsedCorrectlyToCheckProjectionStatus_projectionInactive() {
+        // Assert projectionListener gets called with phone metadata address.
+        when(mMockProjectionStateListener.isProjectionInActiveForeground(
+                BT_DEVICE_ADDRESS)).thenReturn(false);
+        sendValidPhoneMetadataMessage();
+
+        // Send a new conversation to trigger Projection State check.
+        mNotificationMsgDelegate.onMessageReceived(mCompanionDevice, VALID_CONVERSATION_MSG);
+
+        verify(mMockProjectionStateListener).isProjectionInActiveForeground(BT_DEVICE_ADDRESS);
+        verify(mMockNotificationManager).notify(anyInt(), mNotificationCaptor.capture());
+        checkChannelImportanceLevel(
+                mNotificationCaptor.getValue().getChannelId(), /* isLowImportance= */ false);
+    }
+
+    @Test
+    public void delegateChecksProjectionStatus_projectionActive() {
+        // Assert projectionListener gets called with phone metadata address.
+        when(mMockProjectionStateListener.isProjectionInActiveForeground(null)).thenReturn(true);
+
+        // Send a new conversation to trigger Projection State check.
+        mNotificationMsgDelegate.onMessageReceived(mCompanionDevice, VALID_CONVERSATION_MSG);
+
+        verify(mMockProjectionStateListener).isProjectionInActiveForeground(null);
+        verify(mMockNotificationManager).notify(anyInt(), mNotificationCaptor.capture());
+        checkChannelImportanceLevel(
+                mNotificationCaptor.getValue().getChannelId(), /* isLowImportance= */ true);
+    }
+
+    @Test
+    public void delegateChecksProjectionStatus_projectionInactive() {
+        // Assert projectionListener gets called with phone metadata address.
+        when(mMockProjectionStateListener.isProjectionInActiveForeground(null)).thenReturn(false);
+
+        // Send a new conversation to trigger Projection State check.
+        mNotificationMsgDelegate.onMessageReceived(mCompanionDevice, VALID_CONVERSATION_MSG);
+
+        verify(mMockProjectionStateListener).isProjectionInActiveForeground(null);
+        verify(mMockNotificationManager).notify(anyInt(), mNotificationCaptor.capture());
+        checkChannelImportanceLevel(
+                mNotificationCaptor.getValue().getChannelId(), /* isLowImportance= */ false);
+    }
+
+    @Test
+    public void clearAllAppDataShouldClearInternalDataAndNotifications() {
+        mNotificationMsgDelegate.onMessageReceived(mCompanionDevice, VALID_CONVERSATION_MSG);
+        verify(mMockNotificationManager).notify(mNotificationIdCaptor.capture(),
+                any(Notification.class));
+        int notificationId = mNotificationIdCaptor.getValue();
+
+        sendClearAppDataRequest(NotificationMsgDelegate.REMOVE_ALL_APP_DATA);
+
+        verify(mMockNotificationManager).cancel(eq(notificationId));
+    }
+
+    @Test
+    public void clearSpecificAppDataShouldDoNothing() {
+        mNotificationMsgDelegate.onMessageReceived(mCompanionDevice, VALID_CONVERSATION_MSG);
+        verify(mMockNotificationManager).notify(anyInt(), any(Notification.class));
+
+        sendClearAppDataRequest(MESSAGING_PACKAGE_NAME);
+
+        verify(mMockNotificationManager, never()).cancel(anyInt());
+    }
+
+    @Test
+    public void conversationsFromSameApplicationPostedOnSameChannel() {
+        mNotificationMsgDelegate.onMessageReceived(mCompanionDevice, VALID_CONVERSATION_MSG);
+
+        verify(mMockNotificationManager).notify(anyInt(), mNotificationCaptor.capture());
+        String firstChannelId = mNotificationCaptor.getValue().getChannelId();
+
+        mNotificationMsgDelegate.onMessageReceived(mCompanionDevice,
+                VALID_CONVERSATION_MSG.toBuilder()
+                        .setNotificationKey(NOTIFICATION_KEY_2)
+                        .setConversation(VALID_CONVERSATION.toBuilder()
+                                .setMessagingStyle(
+                                        VALID_STYLE.toBuilder().addMessagingStyleMsg(MESSAGE_2))
+                                .build())
+                        .build());
+        verify(mMockNotificationManager, times(2)).notify(anyInt(), mNotificationCaptor.capture());
+
+        assertThat(mNotificationCaptor.getValue().getChannelId()).isEqualTo(firstChannelId);
+    }
+
+    @Test
+    public void messageDataNotSetShouldDoNothing() {
+        // For a PhoneToCarMessage w/ no MessageData
+        mNotificationMsgDelegate.onMessageReceived(mCompanionDevice, PhoneToCarMessage.newBuilder()
+                .setNotificationKey(NOTIFICATION_KEY_1)
+                .build());
+
+        verifyZeroInteractions(mMockNotificationManager);
+    }
+
+    @Test
+    public void dismissShouldCreateCarToPhoneMessage() {
+        mNotificationMsgDelegate.onMessageReceived(mCompanionDevice, VALID_CONVERSATION_MSG);
+
+        CarToPhoneMessage dismissMessage = mNotificationMsgDelegate.dismiss(CONVERSATION_KEY_1);
+
+        verifyCarToPhoneActionMessage(dismissMessage, NOTIFICATION_KEY_1, DISMISS);
+    }
+
+    @Test
+    public void dismissShouldDismissNotification() {
+        mNotificationMsgDelegate.onMessageReceived(mCompanionDevice, VALID_CONVERSATION_MSG);
+        verify(mMockNotificationManager).notify(mNotificationIdCaptor.capture(),
+                any(Notification.class));
+        int notificationId = mNotificationIdCaptor.getValue();
+
+        mNotificationMsgDelegate.dismiss(CONVERSATION_KEY_1);
+
+        verify(mMockNotificationManager).cancel(eq(notificationId));
+    }
+
+    @Test
+    public void markAsReadShouldCreateCarToPhoneMessage() {
+        // Mark message as read, verify message sent to phone.
+        mNotificationMsgDelegate.onMessageReceived(mCompanionDevice, VALID_CONVERSATION_MSG);
+
+        CarToPhoneMessage markAsRead = mNotificationMsgDelegate.markAsRead(CONVERSATION_KEY_1);
+
+        verifyCarToPhoneActionMessage(markAsRead, NOTIFICATION_KEY_1, MARK_AS_READ);
+    }
+
+    @Test
+    public void markAsReadShouldExcludeMessageFromNotification() {
+        // Mark message as read, verify when new message comes in, read
+        // messages are not in notification.
+        mNotificationMsgDelegate.onMessageReceived(mCompanionDevice, VALID_CONVERSATION_MSG);
+        verify(mMockNotificationManager).notify(anyInt(), any(Notification.class));
+
+        mNotificationMsgDelegate.markAsRead(CONVERSATION_KEY_1);
+        // Post an update to this conversation to ensure the now read message is not in
+        // notification.
+        updateConversationWithMessage2();
+        verify(mMockNotificationManager, times(2)).notify(anyInt(),
+                mNotificationCaptor.capture());
+
+        // Verify the notification contains only the latest message.
+        NotificationCompat.MessagingStyle messagingStyle =
+                NotificationCompat.MessagingStyle.extractMessagingStyleFromNotification(
+                        mNotificationCaptor.getValue());
+        assertThat(messagingStyle.getMessages().size()).isEqualTo(1);
+        verifyMessage(MESSAGE_2, messagingStyle.getMessages().get(0));
+
+    }
+
+    @Test
+    public void replyShouldCreateCarToPhoneMessage() {
+        mNotificationMsgDelegate.onMessageReceived(mCompanionDevice, VALID_CONVERSATION_MSG);
+
+        CarToPhoneMessage reply = mNotificationMsgDelegate.reply(CONVERSATION_KEY_1,
+                MESSAGE_TEXT_2);
+        Action replyAction = reply.getActionRequest();
+        NotificationMsg.MapEntry replyEntry = replyAction.getMapEntry(0);
+
+        verifyCarToPhoneActionMessage(reply, NOTIFICATION_KEY_1, REPLY);
+        assertThat(replyAction.getMapEntryCount()).isEqualTo(1);
+        assertThat(replyEntry.getKey()).isEqualTo(NotificationMsgDelegate.REPLY_KEY);
+        assertThat(replyEntry.getValue()).isEqualTo(MESSAGE_TEXT_2);
+    }
+
+    @Test
+    public void onDestroyShouldClearInternalDataAndNotifications() {
+        mNotificationMsgDelegate.onMessageReceived(mCompanionDevice, VALID_CONVERSATION_MSG);
+        verify(mMockNotificationManager).notify(mNotificationIdCaptor.capture(),
+                any(Notification.class));
+        int notificationId = mNotificationIdCaptor.getValue();
+        sendValidAvatarIconSyncMessage();
+
+        mNotificationMsgDelegate.onDestroy();
+
+        assertThat(mNotificationMsgDelegate.mOneOnOneConversationAvatarMap.isEmpty()).isTrue();
+        verify(mMockNotificationManager).cancel(eq(notificationId));
+    }
+
+    @Test
+    public void deviceDisconnectedShouldClearDeviceNotificationsAndMetadata() {
+        // Test that after a device disconnects, all the avatars, notifications for the device
+        // is removed.
+        mNotificationMsgDelegate.onMessageReceived(mCompanionDevice, VALID_CONVERSATION_MSG);
+        verify(mMockNotificationManager).notify(mNotificationIdCaptor.capture(),
+                any(Notification.class));
+        int notificationId = mNotificationIdCaptor.getValue();
+        sendValidAvatarIconSyncMessage();
+
+        mNotificationMsgDelegate.onDeviceDisconnected(COMPANION_DEVICE_ID);
+
+        assertThat(mNotificationMsgDelegate.mOneOnOneConversationAvatarMap.isEmpty()).isTrue();
+        verify(mMockNotificationManager).cancel(eq(notificationId));
+    }
+
+    @Test
+    public void deviceDisconnectedShouldResetProjectionDeviceAddress() {
+        // Test that after a device disconnects, then reconnects, the projection device address
+        // is reset.
+        when(mMockProjectionStateListener.isProjectionInActiveForeground(
+                BT_DEVICE_ADDRESS)).thenReturn(true);
+        sendValidPhoneMetadataMessage();
+
+        mNotificationMsgDelegate.onDeviceDisconnected(COMPANION_DEVICE_ID);
+
+        // Now post a new notification for this device and ensure it is not posted silently.
+        mNotificationMsgDelegate.onMessageReceived(mCompanionDevice, VALID_CONVERSATION_MSG);
+
+        verify(mMockProjectionStateListener).isProjectionInActiveForeground(null);
+        verify(mMockNotificationManager).notify(anyInt(), mNotificationCaptor.capture());
+        checkChannelImportanceLevel(
+                mNotificationCaptor.getValue().getChannelId(), /* isLowImportance= */ false);
+    }
+
     private void verifyNotification(ConversationNotification expected, Notification notification) {
         verifyConversationLevelMetadata(expected, notification);
         verifyMessagingStyle(expected.getMessagingStyle(), notification);
@@ -320,6 +670,31 @@
         }
     }
 
+    private void verifyCarToPhoneActionMessage(CarToPhoneMessage message, String notificationKey,
+            Action.ActionName actionName) {
+        assertThat(message.getNotificationKey()).isEqualTo(notificationKey);
+        assertThat(message.getActionRequest()).isNotNull();
+        assertThat(message.getActionRequest().getNotificationKey()).isEqualTo(notificationKey);
+        assertThat(message.getActionRequest().getActionName()).isEqualTo(actionName);
+    }
+
+    private void checkChannelImportanceLevel(String channelId, boolean isLowImportance) {
+        ArgumentCaptor<NotificationChannel> channelCaptor = ArgumentCaptor.forClass(
+                NotificationChannel.class);
+        verify(mMockNotificationManager, atLeastOnce()).createNotificationChannel(
+                channelCaptor.capture());
+
+        int desiredImportance = isLowImportance ? IMPORTANCE_LOW : IMPORTANCE_HIGH;
+        List<String> desiredImportanceChannelIds = new ArrayList<>();
+        // Each messaging app has 2 channels, one high and one low importance.
+        for (NotificationChannel notificationChannel : channelCaptor.getAllValues()) {
+            if (notificationChannel.getImportance() == desiredImportance) {
+                desiredImportanceChannelIds.add(notificationChannel.getId());
+            }
+        }
+        assertThat(desiredImportanceChannelIds.contains(channelId)).isTrue();
+    }
+
     private NotificationCompat.MessagingStyle getMessagingStyle(Notification notification) {
         return NotificationCompat.MessagingStyle.extractMessagingStyleFromNotification(
                 notification);
@@ -346,4 +721,65 @@
                 .setMessagingStyle(
                         VALID_STYLE.toBuilder().addMessagingStyleMsg(MESSAGE_2)).build();
     }
+
+    private AvatarIconSync createValidAvatarIconSync() {
+        return AvatarIconSync.newBuilder()
+                .setMessagingAppPackageName(MESSAGING_PACKAGE_NAME)
+                .setMessagingAppDisplayName(MESSAGING_APP_NAME)
+                .setPerson(Person.newBuilder()
+                        .setName(SENDER_1)
+                        .setAvatar(ByteString.copyFrom(mIconByteArray))
+                        .build())
+                .build();
+    }
+
+    /**
+     * Small helper method that updates {@link NotificationMsgDelegateTest#VALID_CONVERSATION} with
+     * a new message.
+     */
+    private void updateConversationWithMessage2() {
+        PhoneToCarMessage updateConvo = PhoneToCarMessage.newBuilder()
+                .setNotificationKey(NOTIFICATION_KEY_1)
+                .setMessage(MESSAGE_2)
+                .build();
+        mNotificationMsgDelegate.onMessageReceived(mCompanionDevice, updateConvo);
+    }
+
+    private void sendValidAvatarIconSyncMessage() {
+        PhoneToCarMessage validMessage = PhoneToCarMessage.newBuilder()
+                .setNotificationKey(NOTIFICATION_KEY_1)
+                .setAvatarIconSync(createValidAvatarIconSync())
+                .build();
+        mNotificationMsgDelegate.onMessageReceived(mCompanionDevice, validMessage);
+    }
+
+    private void sendValidPhoneMetadataMessage() {
+        PhoneToCarMessage metadataMessage = PhoneToCarMessage.newBuilder()
+                .setPhoneMetadata(PhoneMetadata.newBuilder()
+                        .setBluetoothDeviceAddress(BT_DEVICE_ADDRESS)
+                        .build())
+                .build();
+        mNotificationMsgDelegate.onMessageReceived(mCompanionDevice, metadataMessage);
+    }
+
+    private void sendGroupConversationMessage() {
+        mNotificationMsgDelegate.onMessageReceived(mCompanionDevice,
+                VALID_CONVERSATION_MSG.toBuilder()
+                        .setConversation(VALID_CONVERSATION.toBuilder()
+                                .setMessagingStyle(VALID_STYLE.toBuilder()
+                                        .setIsGroupConvo(true)
+                                        .build())
+                                .build())
+                        .build());
+    }
+
+
+    private void sendClearAppDataRequest(String messagingAppPackageName) {
+        mNotificationMsgDelegate.onMessageReceived(mCompanionDevice,
+                PhoneToCarMessage.newBuilder()
+                        .setClearAppDataRequest(ClearAppDataRequest.newBuilder()
+                                .setMessagingAppPackageName(messagingAppPackageName)
+                                .build())
+                        .build());
+    }
 }