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