1 /*
2  * Copyright (C) 2020 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.car.companiondevicesupport.feature.notificationmsg;
18 
19 
20 import static android.app.NotificationManager.IMPORTANCE_HIGH;
21 import static android.app.NotificationManager.IMPORTANCE_LOW;
22 
23 import static androidx.core.app.NotificationCompat.CATEGORY_MESSAGE;
24 
25 import static com.android.car.messenger.NotificationMsgProto.NotificationMsg.Action.ActionName.DISMISS;
26 import static com.android.car.messenger.NotificationMsgProto.NotificationMsg.Action.ActionName.MARK_AS_READ;
27 import static com.android.car.messenger.NotificationMsgProto.NotificationMsg.Action.ActionName.REPLY;
28 
29 import static com.google.common.truth.Truth.assertThat;
30 
31 import static org.mockito.ArgumentMatchers.any;
32 import static org.mockito.ArgumentMatchers.anyInt;
33 import static org.mockito.ArgumentMatchers.eq;
34 import static org.mockito.Mockito.atLeastOnce;
35 import static org.mockito.Mockito.never;
36 import static org.mockito.Mockito.times;
37 import static org.mockito.Mockito.verify;
38 import static org.mockito.Mockito.verifyZeroInteractions;
39 import static org.mockito.Mockito.when;
40 
41 import android.app.Notification;
42 import android.app.NotificationChannel;
43 import android.app.NotificationManager;
44 import android.content.Context;
45 import android.graphics.Bitmap;
46 import android.graphics.BitmapFactory;
47 import android.graphics.drawable.Icon;
48 
49 import androidx.core.app.NotificationCompat;
50 import androidx.test.core.app.ApplicationProvider;
51 import androidx.test.ext.junit.runners.AndroidJUnit4;
52 
53 import com.android.car.companiondevicesupport.api.external.CompanionDevice;
54 import com.android.car.messenger.NotificationMsgProto.NotificationMsg;
55 import com.android.car.messenger.NotificationMsgProto.NotificationMsg.Action;
56 import com.android.car.messenger.NotificationMsgProto.NotificationMsg.AvatarIconSync;
57 import com.android.car.messenger.NotificationMsgProto.NotificationMsg.CarToPhoneMessage;
58 import com.android.car.messenger.NotificationMsgProto.NotificationMsg.ClearAppDataRequest;
59 import com.android.car.messenger.NotificationMsgProto.NotificationMsg.ConversationNotification;
60 import com.android.car.messenger.NotificationMsgProto.NotificationMsg.MessagingStyle;
61 import com.android.car.messenger.NotificationMsgProto.NotificationMsg.MessagingStyleMessage;
62 import com.android.car.messenger.NotificationMsgProto.NotificationMsg.Person;
63 import com.android.car.messenger.NotificationMsgProto.NotificationMsg.PhoneMetadata;
64 import com.android.car.messenger.NotificationMsgProto.NotificationMsg.PhoneToCarMessage;
65 import com.android.car.messenger.common.ConversationKey;
66 import com.android.car.messenger.common.ProjectionStateListener;
67 import com.android.car.messenger.common.SenderKey;
68 import com.android.car.protobuf.ByteString;
69 
70 import org.junit.After;
71 import org.junit.Before;
72 import org.junit.Test;
73 import org.junit.runner.RunWith;
74 import org.mockito.ArgumentCaptor;
75 import org.mockito.Captor;
76 import org.mockito.Mock;
77 import org.mockito.MockitoAnnotations;
78 
79 import java.io.ByteArrayOutputStream;
80 import java.util.ArrayList;
81 import java.util.List;
82 import java.util.UUID;
83 
84 @RunWith(AndroidJUnit4.class)
85 public class NotificationMsgDelegateTest {
86     private static final String NOTIFICATION_KEY_1 = "notification_key_1";
87     private static final String NOTIFICATION_KEY_2 = "notification_key_2";
88 
89     private static final String COMPANION_DEVICE_ID = "sampleId";
90     private static final String COMPANION_DEVICE_NAME = "sampleName";
91     private static final String DEVICE_ADDRESS = UUID.randomUUID().toString();
92     private static final String BT_DEVICE_ADDRESS = UUID.randomUUID().toString();
93 
94     private static final String MESSAGING_APP_NAME = "Messaging App";
95     private static final String MESSAGING_PACKAGE_NAME = "com.android.messaging.app";
96     private static final String CONVERSATION_TITLE = "Conversation";
97     private static final String USER_DISPLAY_NAME = "User";
98     private static final String SENDER_1 = "Sender";
99     private static final String MESSAGE_TEXT_1 = "Message 1";
100     private static final String MESSAGE_TEXT_2 = "Message 2";
101 
102     /** ConversationKey for {@link NotificationMsgDelegateTest#VALID_CONVERSATION_MSG}. **/
103     private static final ConversationKey CONVERSATION_KEY_1
104             = new ConversationKey(COMPANION_DEVICE_ID, NOTIFICATION_KEY_1);
105 
106     private static final MessagingStyleMessage MESSAGE_2 = MessagingStyleMessage.newBuilder()
107             .setTextMessage(MESSAGE_TEXT_2)
108             .setSender(Person.newBuilder()
109                     .setName(SENDER_1))
110             .setTimestamp((long) 1577909718950f)
111             .build();
112 
113     private static final MessagingStyle VALID_STYLE = MessagingStyle.newBuilder()
114             .setConvoTitle(CONVERSATION_TITLE)
115             .setUserDisplayName(USER_DISPLAY_NAME)
116             .setIsGroupConvo(false)
117             .addMessagingStyleMsg(MessagingStyleMessage.newBuilder()
118                     .setTextMessage(MESSAGE_TEXT_1)
119                     .setSender(Person.newBuilder()
120                             .setName(SENDER_1))
121                     .setTimestamp((long) 1577909718050f)
122                     .build())
123             .build();
124 
125     private static final ConversationNotification VALID_CONVERSATION =
126             ConversationNotification.newBuilder()
127                     .setMessagingAppDisplayName(MESSAGING_APP_NAME)
128                     .setMessagingAppPackageName(MESSAGING_PACKAGE_NAME)
129                     .setTimeMs((long) 1577909716000f)
130                     .setMessagingStyle(VALID_STYLE)
131                     .build();
132 
133     private static final PhoneToCarMessage VALID_CONVERSATION_MSG = PhoneToCarMessage.newBuilder()
134             .setNotificationKey(NOTIFICATION_KEY_1)
135             .setConversation(VALID_CONVERSATION)
136             .build();
137 
138     private Bitmap mIconBitmap;
139     private byte[] mIconByteArray;
140 
141     @Mock
142     CompanionDevice mCompanionDevice;
143     @Mock
144     NotificationManager mMockNotificationManager;
145     @Mock
146     ProjectionStateListener mMockProjectionStateListener;
147 
148     @Captor
149     ArgumentCaptor<Notification> mNotificationCaptor;
150     @Captor
151     ArgumentCaptor<Integer> mNotificationIdCaptor;
152 
153     Context mContext = ApplicationProvider.getApplicationContext();
154     NotificationMsgDelegate mNotificationMsgDelegate;
155 
156     @Before
setUp()157     public void setUp() {
158         MockitoAnnotations.initMocks(this);
159 
160         when(mCompanionDevice.getDeviceId()).thenReturn(COMPANION_DEVICE_ID);
161         when(mCompanionDevice.getDeviceName()).thenReturn(COMPANION_DEVICE_NAME);
162 
163         mNotificationMsgDelegate = new NotificationMsgDelegate(mContext);
164         mNotificationMsgDelegate.setNotificationManager(mMockNotificationManager);
165         mNotificationMsgDelegate.setProjectionStateListener(mMockProjectionStateListener);
166 
167         mIconBitmap = Bitmap.createBitmap(200, 200, Bitmap.Config.ARGB_8888);
168 
169         ByteArrayOutputStream stream = new ByteArrayOutputStream();
170         mIconBitmap.compress(Bitmap.CompressFormat.PNG, 100, stream);
171         mIconByteArray = stream.toByteArray();
172         stream.reset();
173 
174     }
175 
176     @After
tearDown()177     public void tearDown() {
178         mIconBitmap.recycle();
179     }
180 
181     @Test
newConversationShouldPostNewNotification()182     public void newConversationShouldPostNewNotification() {
183         // Test that a new conversation notification is posted with the correct fields.
184         mNotificationMsgDelegate.onMessageReceived(mCompanionDevice, VALID_CONVERSATION_MSG);
185 
186         verify(mMockNotificationManager).notify(anyInt(), mNotificationCaptor.capture());
187 
188         Notification postedNotification = mNotificationCaptor.getValue();
189         verifyNotification(VALID_CONVERSATION, postedNotification);
190     }
191 
192     @Test
multipleNewConversationShouldPostMultipleNewNotifications()193     public void multipleNewConversationShouldPostMultipleNewNotifications() {
194         mNotificationMsgDelegate.onMessageReceived(mCompanionDevice, VALID_CONVERSATION_MSG);
195 
196         verify(mMockNotificationManager).notify(mNotificationIdCaptor.capture(),
197                 any(Notification.class));
198         int firstNotificationId = mNotificationIdCaptor.getValue();
199 
200         mNotificationMsgDelegate.onMessageReceived(mCompanionDevice,
201                 createSecondConversation());
202         verify(mMockNotificationManager, times(2)).notify(mNotificationIdCaptor.capture(),
203                 any(Notification.class));
204 
205         // Verify the notification id is different than the first.
206         assertThat((long) mNotificationIdCaptor.getValue()).isNotEqualTo(firstNotificationId);
207     }
208 
209     @Test
invalidConversationShouldDoNothing()210     public void invalidConversationShouldDoNothing() {
211         // Test that a conversation without all the required fields is dropped.
212         PhoneToCarMessage newConvo = PhoneToCarMessage.newBuilder()
213                 .setNotificationKey(NOTIFICATION_KEY_1)
214                 .setConversation(VALID_CONVERSATION.toBuilder().clearMessagingStyle())
215                 .build();
216         mNotificationMsgDelegate.onMessageReceived(mCompanionDevice, newConvo);
217 
218         verify(mMockNotificationManager, never()).notify(anyInt(), any(Notification.class));
219     }
220 
221     @Test
newMessageShouldUpdateConversationNotification()222     public void newMessageShouldUpdateConversationNotification() {
223         // Check whether a new message updates the notification of the conversation it belongs to.
224         mNotificationMsgDelegate.onMessageReceived(mCompanionDevice, VALID_CONVERSATION_MSG);
225         verify(mMockNotificationManager).notify(mNotificationIdCaptor.capture(),
226                 any(Notification.class));
227         int notificationId = mNotificationIdCaptor.getValue();
228         int messageCount = VALID_CONVERSATION_MSG.getConversation().getMessagingStyle()
229                 .getMessagingStyleMsgCount();
230 
231         // Post a new message in this conversation.
232         updateConversationWithMessage2();
233 
234         // Verify same notification id is posted twice.
235         verify(mMockNotificationManager, times(2)).notify(eq(notificationId),
236                 mNotificationCaptor.capture());
237 
238         // Verify the notification contains one more message.
239         NotificationCompat.MessagingStyle messagingStyle = getMessagingStyle(
240                 mNotificationCaptor.getValue());
241         assertThat(messagingStyle.getMessages().size()).isEqualTo(messageCount + 1);
242 
243         // Verify notification's latest message matches the new message.
244         verifyMessage(MESSAGE_2, messagingStyle.getMessages().get(messageCount));
245     }
246 
247     @Test
existingConversationShouldUpdateNotification()248     public void existingConversationShouldUpdateNotification() {
249         // Test that a conversation that already exists, but gets a new conversation message
250         // is updated with the new conversation metadata.
251         mNotificationMsgDelegate.onMessageReceived(mCompanionDevice, VALID_CONVERSATION_MSG);
252         verify(mMockNotificationManager).notify(mNotificationIdCaptor.capture(),
253                 any(Notification.class));
254         int notificationId = mNotificationIdCaptor.getValue();
255 
256         ConversationNotification updatedConversation = addSecondMessageToConversation().toBuilder()
257                 .setMessagingAppDisplayName("New Messaging App")
258                 .build();
259         mNotificationMsgDelegate.onMessageReceived(mCompanionDevice, PhoneToCarMessage.newBuilder()
260                 .setNotificationKey(NOTIFICATION_KEY_1)
261                 .setConversation(updatedConversation)
262                 .build());
263 
264         verify(mMockNotificationManager, times(2)).notify(eq(notificationId),
265                 mNotificationCaptor.capture());
266         Notification postedNotification = mNotificationCaptor.getValue();
267 
268         // Verify Conversation level metadata does NOT change
269         verifyConversationLevelMetadata(VALID_CONVERSATION, postedNotification);
270         // Verify the MessagingStyle metadata does update with the new message.
271         verifyMessagingStyle(updatedConversation.getMessagingStyle(), postedNotification);
272     }
273 
274     @Test
messageForUnknownConversationShouldDoNothing()275     public void messageForUnknownConversationShouldDoNothing() {
276         // A message for an unknown conversation should be dropped.
277         updateConversationWithMessage2();
278 
279         verify(mMockNotificationManager, never()).notify(anyInt(), any(Notification.class));
280     }
281 
282     @Test
invalidMessageShouldDoNothing()283     public void invalidMessageShouldDoNothing() {
284         // Message without all the required fields is dropped.
285         mNotificationMsgDelegate.onMessageReceived(mCompanionDevice, VALID_CONVERSATION_MSG);
286 
287         // Create a MessagingStyleMessage without a required field (Sender information).
288         MessagingStyleMessage invalidMsgStyleMessage = MessagingStyleMessage.newBuilder()
289                 .setTextMessage("Message 2")
290                 .setTimestamp((long) 1577909718950f)
291                 .build();
292         PhoneToCarMessage invalidMessage = PhoneToCarMessage.newBuilder()
293                 .setNotificationKey(NOTIFICATION_KEY_1)
294                 .setMessage(invalidMsgStyleMessage)
295                 .build();
296         mNotificationMsgDelegate.onMessageReceived(mCompanionDevice, invalidMessage);
297 
298         // Verify only one notification is posted, and never updated.
299         verify(mMockNotificationManager).notify(anyInt(), any(Notification.class));
300     }
301 
302     @Test
invalidAvatarIconSyncShouldDoNothing()303     public void invalidAvatarIconSyncShouldDoNothing() {
304         mNotificationMsgDelegate.onMessageReceived(mCompanionDevice, VALID_CONVERSATION_MSG);
305 
306         // Create AvatarIconSync message without required field (Icon), ensure it's treated as an
307         // invalid message.
308         PhoneToCarMessage invalidMessage = PhoneToCarMessage.newBuilder()
309                 .setNotificationKey(NOTIFICATION_KEY_1)
310                 .setAvatarIconSync(AvatarIconSync.newBuilder()
311                         .setPerson(Person.newBuilder()
312                                 .setName(SENDER_1))
313                         .build())
314                 .build();
315         mNotificationMsgDelegate.onMessageReceived(mCompanionDevice, invalidMessage);
316         assertThat(mNotificationMsgDelegate.mOneOnOneConversationAvatarMap.isEmpty()).isTrue();
317     }
318 
319     @Test
avatarIconSyncForGroupConversationShouldDoNothing()320     public void avatarIconSyncForGroupConversationShouldDoNothing() {
321         // We only sync avatars for 1-1 conversations.
322         sendGroupConversationMessage();
323 
324         sendValidAvatarIconSyncMessage();
325 
326         assertThat(mNotificationMsgDelegate.mOneOnOneConversationAvatarMap.isEmpty()).isTrue();
327     }
328 
329     @Test
avatarIconSyncForUnknownConversationShouldDoNothing()330     public void avatarIconSyncForUnknownConversationShouldDoNothing() {
331         // Drop avatar if it's for a conversation that is unknown.
332         sendValidAvatarIconSyncMessage();
333 
334         assertThat(mNotificationMsgDelegate.mOneOnOneConversationAvatarMap.isEmpty()).isTrue();
335     }
336 
337     @Test
avatarIconSyncSetsAvatarInNotification()338     public void avatarIconSyncSetsAvatarInNotification() {
339         // Check that a conversation that didn't have an avatar, but gets this message posts
340         // a notification with the avatar.
341         mNotificationMsgDelegate.onMessageReceived(mCompanionDevice, VALID_CONVERSATION_MSG);
342         verify(mMockNotificationManager).notify(anyInt(), mNotificationCaptor.capture());
343         Icon notSetIcon = mNotificationCaptor.getValue().getLargeIcon();
344 
345         sendValidAvatarIconSyncMessage();
346 
347         // Post an update so we update the notification and can see the new icon.
348         updateConversationWithMessage2();
349 
350         verify(mMockNotificationManager, times(2)).notify(anyInt(), mNotificationCaptor.capture());
351         Icon newIcon = mNotificationCaptor.getValue().getLargeIcon();
352         assertThat(newIcon).isNotEqualTo(notSetIcon);
353     }
354 
355 
356     @Test
avatarIconSyncStoresBitmapCorrectly()357     public void avatarIconSyncStoresBitmapCorrectly() {
358         // Post a conversation notification first, so we don't drop the avatar message.
359         mNotificationMsgDelegate.onMessageReceived(mCompanionDevice, VALID_CONVERSATION_MSG);
360 
361         sendValidAvatarIconSyncMessage();
362 
363         AvatarIconSync iconSync = createValidAvatarIconSync();
364         SenderKey senderKey = SenderKey.createSenderKey(CONVERSATION_KEY_1, iconSync.getPerson());
365         byte[] iconArray = iconSync.getPerson().getAvatar().toByteArray();
366         Bitmap bitmap = BitmapFactory.decodeByteArray(iconArray, /* offset= */ 0, iconArray.length);
367 
368         assertThat(mNotificationMsgDelegate.mOneOnOneConversationAvatarMap).hasSize(1);
369         Bitmap actualBitmap = mNotificationMsgDelegate.mOneOnOneConversationAvatarMap.get(
370                 senderKey);
371         assertThat(actualBitmap).isNotNull();
372         assertThat(actualBitmap.sameAs(bitmap)).isTrue();
373     }
374 
375     @Test
phoneMetadataUsedToCheckProjectionStatus_projectionActive()376     public void phoneMetadataUsedToCheckProjectionStatus_projectionActive() {
377         // Assert projectionListener gets called with phone metadata address.
378         when(mMockProjectionStateListener.isProjectionInActiveForeground(
379                 BT_DEVICE_ADDRESS)).thenReturn(true);
380         sendValidPhoneMetadataMessage();
381 
382         // Send a new conversation to trigger Projection State check.
383         mNotificationMsgDelegate.onMessageReceived(mCompanionDevice, VALID_CONVERSATION_MSG);
384 
385         verify(mMockProjectionStateListener).isProjectionInActiveForeground(BT_DEVICE_ADDRESS);
386         verify(mMockNotificationManager).notify(anyInt(), mNotificationCaptor.capture());
387         checkChannelImportanceLevel(
388                 mNotificationCaptor.getValue().getChannelId(), /* isLowImportance= */ true);
389     }
390 
391     @Test
phoneMetadataUsedCorrectlyToCheckProjectionStatus_projectionInactive()392     public void phoneMetadataUsedCorrectlyToCheckProjectionStatus_projectionInactive() {
393         // Assert projectionListener gets called with phone metadata address.
394         when(mMockProjectionStateListener.isProjectionInActiveForeground(
395                 BT_DEVICE_ADDRESS)).thenReturn(false);
396         sendValidPhoneMetadataMessage();
397 
398         // Send a new conversation to trigger Projection State check.
399         mNotificationMsgDelegate.onMessageReceived(mCompanionDevice, VALID_CONVERSATION_MSG);
400 
401         verify(mMockProjectionStateListener).isProjectionInActiveForeground(BT_DEVICE_ADDRESS);
402         verify(mMockNotificationManager).notify(anyInt(), mNotificationCaptor.capture());
403         checkChannelImportanceLevel(
404                 mNotificationCaptor.getValue().getChannelId(), /* isLowImportance= */ false);
405     }
406 
407     @Test
delegateChecksProjectionStatus_projectionActive()408     public void delegateChecksProjectionStatus_projectionActive() {
409         // Assert projectionListener gets called with phone metadata address.
410         when(mMockProjectionStateListener.isProjectionInActiveForeground(null)).thenReturn(true);
411 
412         // Send a new conversation to trigger Projection State check.
413         mNotificationMsgDelegate.onMessageReceived(mCompanionDevice, VALID_CONVERSATION_MSG);
414 
415         verify(mMockProjectionStateListener).isProjectionInActiveForeground(null);
416         verify(mMockNotificationManager).notify(anyInt(), mNotificationCaptor.capture());
417         checkChannelImportanceLevel(
418                 mNotificationCaptor.getValue().getChannelId(), /* isLowImportance= */ true);
419     }
420 
421     @Test
delegateChecksProjectionStatus_projectionInactive()422     public void delegateChecksProjectionStatus_projectionInactive() {
423         // Assert projectionListener gets called with phone metadata address.
424         when(mMockProjectionStateListener.isProjectionInActiveForeground(null)).thenReturn(false);
425 
426         // Send a new conversation to trigger Projection State check.
427         mNotificationMsgDelegate.onMessageReceived(mCompanionDevice, VALID_CONVERSATION_MSG);
428 
429         verify(mMockProjectionStateListener).isProjectionInActiveForeground(null);
430         verify(mMockNotificationManager).notify(anyInt(), mNotificationCaptor.capture());
431         checkChannelImportanceLevel(
432                 mNotificationCaptor.getValue().getChannelId(), /* isLowImportance= */ false);
433     }
434 
435     @Test
clearAllAppDataShouldClearInternalDataAndNotifications()436     public void clearAllAppDataShouldClearInternalDataAndNotifications() {
437         mNotificationMsgDelegate.onMessageReceived(mCompanionDevice, VALID_CONVERSATION_MSG);
438         verify(mMockNotificationManager).notify(mNotificationIdCaptor.capture(),
439                 any(Notification.class));
440         int notificationId = mNotificationIdCaptor.getValue();
441 
442         sendClearAppDataRequest(NotificationMsgDelegate.REMOVE_ALL_APP_DATA);
443 
444         verify(mMockNotificationManager).cancel(eq(notificationId));
445     }
446 
447     @Test
clearSpecificAppDataShouldDoNothing()448     public void clearSpecificAppDataShouldDoNothing() {
449         mNotificationMsgDelegate.onMessageReceived(mCompanionDevice, VALID_CONVERSATION_MSG);
450         verify(mMockNotificationManager).notify(anyInt(), any(Notification.class));
451 
452         sendClearAppDataRequest(MESSAGING_PACKAGE_NAME);
453 
454         verify(mMockNotificationManager, never()).cancel(anyInt());
455     }
456 
457     @Test
conversationsFromSameApplicationPostedOnSameChannel()458     public void conversationsFromSameApplicationPostedOnSameChannel() {
459         mNotificationMsgDelegate.onMessageReceived(mCompanionDevice, VALID_CONVERSATION_MSG);
460 
461         verify(mMockNotificationManager).notify(anyInt(), mNotificationCaptor.capture());
462         String firstChannelId = mNotificationCaptor.getValue().getChannelId();
463 
464         mNotificationMsgDelegate.onMessageReceived(mCompanionDevice,
465                 VALID_CONVERSATION_MSG.toBuilder()
466                         .setNotificationKey(NOTIFICATION_KEY_2)
467                         .setConversation(VALID_CONVERSATION.toBuilder()
468                                 .setMessagingStyle(
469                                         VALID_STYLE.toBuilder().addMessagingStyleMsg(MESSAGE_2))
470                                 .build())
471                         .build());
472         verify(mMockNotificationManager, times(2)).notify(anyInt(), mNotificationCaptor.capture());
473 
474         assertThat(mNotificationCaptor.getValue().getChannelId()).isEqualTo(firstChannelId);
475     }
476 
477     @Test
messageDataNotSetShouldDoNothing()478     public void messageDataNotSetShouldDoNothing() {
479         // For a PhoneToCarMessage w/ no MessageData
480         mNotificationMsgDelegate.onMessageReceived(mCompanionDevice, PhoneToCarMessage.newBuilder()
481                 .setNotificationKey(NOTIFICATION_KEY_1)
482                 .build());
483 
484         verifyZeroInteractions(mMockNotificationManager);
485     }
486 
487     @Test
dismissShouldCreateCarToPhoneMessage()488     public void dismissShouldCreateCarToPhoneMessage() {
489         mNotificationMsgDelegate.onMessageReceived(mCompanionDevice, VALID_CONVERSATION_MSG);
490 
491         CarToPhoneMessage dismissMessage = mNotificationMsgDelegate.dismiss(CONVERSATION_KEY_1);
492 
493         verifyCarToPhoneActionMessage(dismissMessage, NOTIFICATION_KEY_1, DISMISS);
494     }
495 
496     @Test
dismissShouldDismissNotification()497     public void dismissShouldDismissNotification() {
498         mNotificationMsgDelegate.onMessageReceived(mCompanionDevice, VALID_CONVERSATION_MSG);
499         verify(mMockNotificationManager).notify(mNotificationIdCaptor.capture(),
500                 any(Notification.class));
501         int notificationId = mNotificationIdCaptor.getValue();
502 
503         mNotificationMsgDelegate.dismiss(CONVERSATION_KEY_1);
504 
505         verify(mMockNotificationManager).cancel(eq(notificationId));
506     }
507 
508     @Test
markAsReadShouldCreateCarToPhoneMessage()509     public void markAsReadShouldCreateCarToPhoneMessage() {
510         // Mark message as read, verify message sent to phone.
511         mNotificationMsgDelegate.onMessageReceived(mCompanionDevice, VALID_CONVERSATION_MSG);
512 
513         CarToPhoneMessage markAsRead = mNotificationMsgDelegate.markAsRead(CONVERSATION_KEY_1);
514 
515         verifyCarToPhoneActionMessage(markAsRead, NOTIFICATION_KEY_1, MARK_AS_READ);
516     }
517 
518     @Test
markAsReadShouldExcludeMessageFromNotification()519     public void markAsReadShouldExcludeMessageFromNotification() {
520         // Mark message as read, verify when new message comes in, read
521         // messages are not in notification.
522         mNotificationMsgDelegate.onMessageReceived(mCompanionDevice, VALID_CONVERSATION_MSG);
523         verify(mMockNotificationManager).notify(anyInt(), any(Notification.class));
524 
525         mNotificationMsgDelegate.markAsRead(CONVERSATION_KEY_1);
526         // Post an update to this conversation to ensure the now read message is not in
527         // notification.
528         updateConversationWithMessage2();
529         verify(mMockNotificationManager, times(2)).notify(anyInt(),
530                 mNotificationCaptor.capture());
531 
532         // Verify the notification contains only the latest message.
533         NotificationCompat.MessagingStyle messagingStyle =
534                 NotificationCompat.MessagingStyle.extractMessagingStyleFromNotification(
535                         mNotificationCaptor.getValue());
536         assertThat(messagingStyle.getMessages().size()).isEqualTo(1);
537         verifyMessage(MESSAGE_2, messagingStyle.getMessages().get(0));
538 
539     }
540 
541     @Test
replyShouldCreateCarToPhoneMessage()542     public void replyShouldCreateCarToPhoneMessage() {
543         mNotificationMsgDelegate.onMessageReceived(mCompanionDevice, VALID_CONVERSATION_MSG);
544 
545         CarToPhoneMessage reply = mNotificationMsgDelegate.reply(CONVERSATION_KEY_1,
546                 MESSAGE_TEXT_2);
547         Action replyAction = reply.getActionRequest();
548         NotificationMsg.MapEntry replyEntry = replyAction.getMapEntry(0);
549 
550         verifyCarToPhoneActionMessage(reply, NOTIFICATION_KEY_1, REPLY);
551         assertThat(replyAction.getMapEntryCount()).isEqualTo(1);
552         assertThat(replyEntry.getKey()).isEqualTo(NotificationMsgDelegate.REPLY_KEY);
553         assertThat(replyEntry.getValue()).isEqualTo(MESSAGE_TEXT_2);
554     }
555 
556     @Test
onDestroyShouldClearInternalDataAndNotifications()557     public void onDestroyShouldClearInternalDataAndNotifications() {
558         mNotificationMsgDelegate.onMessageReceived(mCompanionDevice, VALID_CONVERSATION_MSG);
559         verify(mMockNotificationManager).notify(mNotificationIdCaptor.capture(),
560                 any(Notification.class));
561         int notificationId = mNotificationIdCaptor.getValue();
562         sendValidAvatarIconSyncMessage();
563 
564         mNotificationMsgDelegate.onDestroy();
565 
566         assertThat(mNotificationMsgDelegate.mOneOnOneConversationAvatarMap.isEmpty()).isTrue();
567         verify(mMockNotificationManager).cancel(eq(notificationId));
568     }
569 
570     @Test
deviceDisconnectedShouldClearDeviceNotificationsAndMetadata()571     public void deviceDisconnectedShouldClearDeviceNotificationsAndMetadata() {
572         // Test that after a device disconnects, all the avatars, notifications for the device
573         // is removed.
574         mNotificationMsgDelegate.onMessageReceived(mCompanionDevice, VALID_CONVERSATION_MSG);
575         verify(mMockNotificationManager).notify(mNotificationIdCaptor.capture(),
576                 any(Notification.class));
577         int notificationId = mNotificationIdCaptor.getValue();
578         sendValidAvatarIconSyncMessage();
579 
580         mNotificationMsgDelegate.onDeviceDisconnected(COMPANION_DEVICE_ID);
581 
582         assertThat(mNotificationMsgDelegate.mOneOnOneConversationAvatarMap.isEmpty()).isTrue();
583         verify(mMockNotificationManager).cancel(eq(notificationId));
584     }
585 
586     @Test
deviceDisconnectedShouldResetProjectionDeviceAddress()587     public void deviceDisconnectedShouldResetProjectionDeviceAddress() {
588         // Test that after a device disconnects, then reconnects, the projection device address
589         // is reset.
590         when(mMockProjectionStateListener.isProjectionInActiveForeground(
591                 BT_DEVICE_ADDRESS)).thenReturn(true);
592         sendValidPhoneMetadataMessage();
593 
594         mNotificationMsgDelegate.onDeviceDisconnected(COMPANION_DEVICE_ID);
595 
596         // Now post a new notification for this device and ensure it is not posted silently.
597         mNotificationMsgDelegate.onMessageReceived(mCompanionDevice, VALID_CONVERSATION_MSG);
598 
599         verify(mMockProjectionStateListener).isProjectionInActiveForeground(null);
600         verify(mMockNotificationManager).notify(anyInt(), mNotificationCaptor.capture());
601         checkChannelImportanceLevel(
602                 mNotificationCaptor.getValue().getChannelId(), /* isLowImportance= */ false);
603     }
604 
verifyNotification(ConversationNotification expected, Notification notification)605     private void verifyNotification(ConversationNotification expected, Notification notification) {
606         verifyConversationLevelMetadata(expected, notification);
607         verifyMessagingStyle(expected.getMessagingStyle(), notification);
608     }
609 
610     /**
611      * Verifies the conversation level metadata and other aspects of a notification that do not
612      * change when a new message is added to it (such as the actions, intents).
613      */
verifyConversationLevelMetadata(ConversationNotification expected, Notification notification)614     private void verifyConversationLevelMetadata(ConversationNotification expected,
615             Notification notification) {
616         assertThat(notification.category).isEqualTo(CATEGORY_MESSAGE);
617 
618         assertThat(notification.getSmallIcon()).isNotNull();
619         if (!expected.getAppIcon().isEmpty()) {
620             byte[] iconBytes = expected.getAppIcon().toByteArray();
621             Icon appIcon = Icon.createWithData(iconBytes, 0, iconBytes.length);
622             assertThat(notification.getSmallIcon()).isEqualTo(appIcon);
623         }
624 
625         assertThat(notification.deleteIntent).isNotNull();
626 
627         if (expected.getMessagingAppPackageName() != null) {
628             CharSequence appName = notification.extras.getCharSequence(
629                     Notification.EXTRA_SUBSTITUTE_APP_NAME);
630             assertThat(appName).isEqualTo(expected.getMessagingAppDisplayName());
631         }
632 
633         assertThat(notification.actions.length).isEqualTo(2);
634         for (NotificationCompat.Action action : getAllActions(notification)) {
635             if (action.getSemanticAction() == NotificationCompat.Action.SEMANTIC_ACTION_REPLY) {
636                 assertThat(action.getRemoteInputs().length).isEqualTo(1);
637             }
638             assertThat(action.getShowsUserInterface()).isFalse();
639         }
640     }
641 
verifyMessagingStyle(MessagingStyle expected, Notification notification)642     private void verifyMessagingStyle(MessagingStyle expected, Notification notification) {
643         final NotificationCompat.MessagingStyle messagingStyle = getMessagingStyle(notification);
644 
645         assertThat(messagingStyle.getUser().getName()).isEqualTo(expected.getUserDisplayName());
646         assertThat(messagingStyle.isGroupConversation()).isEqualTo(expected.getIsGroupConvo());
647         assertThat(messagingStyle.getMessages().size()).isEqualTo(expected.getMessagingStyleMsgCount());
648 
649         for (int i = 0; i < expected.getMessagingStyleMsgCount(); i++) {
650             MessagingStyleMessage expectedMsg = expected.getMessagingStyleMsg(i);
651             NotificationCompat.MessagingStyle.Message actualMsg = messagingStyle.getMessages().get(
652                     i);
653             verifyMessage(expectedMsg, actualMsg);
654 
655         }
656     }
657 
verifyMessage(MessagingStyleMessage expectedMsg, NotificationCompat.MessagingStyle.Message actualMsg)658     private void verifyMessage(MessagingStyleMessage expectedMsg,
659             NotificationCompat.MessagingStyle.Message actualMsg) {
660         assertThat(actualMsg.getTimestamp()).isEqualTo(expectedMsg.getTimestamp());
661         assertThat(actualMsg.getText()).isEqualTo(expectedMsg.getTextMessage());
662 
663         Person expectedSender = expectedMsg.getSender();
664         androidx.core.app.Person actualSender = actualMsg.getPerson();
665         assertThat(actualSender.getName()).isEqualTo(expectedSender.getName());
666         if (!expectedSender.getAvatar().isEmpty()) {
667             assertThat(actualSender.getIcon()).isNotNull();
668         } else {
669             assertThat(actualSender.getIcon()).isNull();
670         }
671     }
672 
verifyCarToPhoneActionMessage(CarToPhoneMessage message, String notificationKey, Action.ActionName actionName)673     private void verifyCarToPhoneActionMessage(CarToPhoneMessage message, String notificationKey,
674             Action.ActionName actionName) {
675         assertThat(message.getNotificationKey()).isEqualTo(notificationKey);
676         assertThat(message.getActionRequest()).isNotNull();
677         assertThat(message.getActionRequest().getNotificationKey()).isEqualTo(notificationKey);
678         assertThat(message.getActionRequest().getActionName()).isEqualTo(actionName);
679     }
680 
checkChannelImportanceLevel(String channelId, boolean isLowImportance)681     private void checkChannelImportanceLevel(String channelId, boolean isLowImportance) {
682         ArgumentCaptor<NotificationChannel> channelCaptor = ArgumentCaptor.forClass(
683                 NotificationChannel.class);
684         verify(mMockNotificationManager, atLeastOnce()).createNotificationChannel(
685                 channelCaptor.capture());
686 
687         int desiredImportance = isLowImportance ? IMPORTANCE_LOW : IMPORTANCE_HIGH;
688         List<String> desiredImportanceChannelIds = new ArrayList<>();
689         // Each messaging app has 2 channels, one high and one low importance.
690         for (NotificationChannel notificationChannel : channelCaptor.getAllValues()) {
691             if (notificationChannel.getImportance() == desiredImportance) {
692                 desiredImportanceChannelIds.add(notificationChannel.getId());
693             }
694         }
695         assertThat(desiredImportanceChannelIds.contains(channelId)).isTrue();
696     }
697 
getMessagingStyle(Notification notification)698     private NotificationCompat.MessagingStyle getMessagingStyle(Notification notification) {
699         return NotificationCompat.MessagingStyle.extractMessagingStyleFromNotification(
700                 notification);
701     }
702 
getAllActions(Notification notification)703     private List<NotificationCompat.Action> getAllActions(Notification notification) {
704         List<NotificationCompat.Action> actions = new ArrayList<>();
705         actions.addAll(NotificationCompat.getInvisibleActions(notification));
706         for (int i = 0; i < NotificationCompat.getActionCount(notification); i++) {
707             actions.add(NotificationCompat.getAction(notification, i));
708         }
709         return actions;
710     }
711 
createSecondConversation()712     private PhoneToCarMessage createSecondConversation() {
713         return VALID_CONVERSATION_MSG.toBuilder()
714                 .setNotificationKey(NOTIFICATION_KEY_2)
715                 .setConversation(addSecondMessageToConversation())
716                 .build();
717     }
718 
addSecondMessageToConversation()719     private ConversationNotification addSecondMessageToConversation() {
720         return VALID_CONVERSATION.toBuilder()
721                 .setMessagingStyle(
722                         VALID_STYLE.toBuilder().addMessagingStyleMsg(MESSAGE_2)).build();
723     }
724 
createValidAvatarIconSync()725     private AvatarIconSync createValidAvatarIconSync() {
726         return AvatarIconSync.newBuilder()
727                 .setMessagingAppPackageName(MESSAGING_PACKAGE_NAME)
728                 .setMessagingAppDisplayName(MESSAGING_APP_NAME)
729                 .setPerson(Person.newBuilder()
730                         .setName(SENDER_1)
731                         .setAvatar(ByteString.copyFrom(mIconByteArray))
732                         .build())
733                 .build();
734     }
735 
736     /**
737      * Small helper method that updates {@link NotificationMsgDelegateTest#VALID_CONVERSATION} with
738      * a new message.
739      */
updateConversationWithMessage2()740     private void updateConversationWithMessage2() {
741         PhoneToCarMessage updateConvo = PhoneToCarMessage.newBuilder()
742                 .setNotificationKey(NOTIFICATION_KEY_1)
743                 .setMessage(MESSAGE_2)
744                 .build();
745         mNotificationMsgDelegate.onMessageReceived(mCompanionDevice, updateConvo);
746     }
747 
sendValidAvatarIconSyncMessage()748     private void sendValidAvatarIconSyncMessage() {
749         PhoneToCarMessage validMessage = PhoneToCarMessage.newBuilder()
750                 .setNotificationKey(NOTIFICATION_KEY_1)
751                 .setAvatarIconSync(createValidAvatarIconSync())
752                 .build();
753         mNotificationMsgDelegate.onMessageReceived(mCompanionDevice, validMessage);
754     }
755 
sendValidPhoneMetadataMessage()756     private void sendValidPhoneMetadataMessage() {
757         PhoneToCarMessage metadataMessage = PhoneToCarMessage.newBuilder()
758                 .setPhoneMetadata(PhoneMetadata.newBuilder()
759                         .setBluetoothDeviceAddress(BT_DEVICE_ADDRESS)
760                         .build())
761                 .build();
762         mNotificationMsgDelegate.onMessageReceived(mCompanionDevice, metadataMessage);
763     }
764 
sendGroupConversationMessage()765     private void sendGroupConversationMessage() {
766         mNotificationMsgDelegate.onMessageReceived(mCompanionDevice,
767                 VALID_CONVERSATION_MSG.toBuilder()
768                         .setConversation(VALID_CONVERSATION.toBuilder()
769                                 .setMessagingStyle(VALID_STYLE.toBuilder()
770                                         .setIsGroupConvo(true)
771                                         .build())
772                                 .build())
773                         .build());
774     }
775 
776 
sendClearAppDataRequest(String messagingAppPackageName)777     private void sendClearAppDataRequest(String messagingAppPackageName) {
778         mNotificationMsgDelegate.onMessageReceived(mCompanionDevice,
779                 PhoneToCarMessage.newBuilder()
780                         .setClearAppDataRequest(ClearAppDataRequest.newBuilder()
781                                 .setMessagingAppPackageName(messagingAppPackageName)
782                                 .build())
783                         .build());
784     }
785 }
786