summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/com/android/car/companiondevicesupport/activity/AssociationActivity.java16
-rw-r--r--src/com/android/car/companiondevicesupport/feature/notificationmsg/NotificationMsgDelegate.java29
-rw-r--r--src/com/android/car/companiondevicesupport/feature/trust/ui/TrustedDeviceActivity.java4
-rw-r--r--tests/unit/src/com/android/car/companiondevicesupport/feature/notificationmsg/NotificationMsgDelegateTest.java349
-rw-r--r--tests/unit/src/com/android/car/companiondevicesupport/feature/notificationmsg/NotificationMsgFeatureTest.java159
-rw-r--r--tests/unit/src/com/android/car/companiondevicesupport/feature/notificationmsg/OWNERS3
6 files changed, 541 insertions, 19 deletions
diff --git a/src/com/android/car/companiondevicesupport/activity/AssociationActivity.java b/src/com/android/car/companiondevicesupport/activity/AssociationActivity.java
index f4fb9bd..aae0a9c 100644
--- a/src/com/android/car/companiondevicesupport/activity/AssociationActivity.java
+++ b/src/com/android/car/companiondevicesupport/activity/AssociationActivity.java
@@ -77,7 +77,7 @@ public class AssociationActivity extends FragmentActivity {
if (saveInstanceState != null) {
resumePreviousState();
}
- mToolbar.showProgressBar();
+ mToolbar.getProgressBar().setVisible(true);
}
@Override
@@ -85,7 +85,7 @@ public class AssociationActivity extends FragmentActivity {
super.onBackPressed();
mModel.stopAssociation();
dismissConfirmButtons();
- mToolbar.hideProgressBar();
+ mToolbar.getProgressBar().setVisible(false);
}
private void observeViewModel() {
@@ -170,26 +170,26 @@ public class AssociationActivity extends FragmentActivity {
private void showTurnOnBluetoothFragment() {
TurnOnBluetoothFragment fragment = new TurnOnBluetoothFragment();
- mToolbar.showProgressBar();
+ mToolbar.getProgressBar().setVisible(true);
launchFragment(fragment, TURN_ON_BLUETOOTH_FRAGMENT_TAG);
}
private void showAddAssociatedDeviceFragment(String deviceName) {
AddAssociatedDeviceFragment fragment = AddAssociatedDeviceFragment.newInstance(deviceName);
launchFragment(fragment, ADD_DEVICE_FRAGMENT_TAG);
- mToolbar.showProgressBar();
+ mToolbar.getProgressBar().setVisible(true);
}
private void showConfirmPairingCodeFragment(String pairingCode) {
ConfirmPairingCodeFragment fragment = ConfirmPairingCodeFragment.newInstance(pairingCode);
launchFragment(fragment, PAIRING_CODE_FRAGMENT_TAG);
showConfirmButtons();
- mToolbar.hideProgressBar();
+ mToolbar.getProgressBar().setVisible(false);
}
private void showAssociationErrorFragment() {
dismissConfirmButtons();
- mToolbar.showProgressBar();
+ mToolbar.getProgressBar().setVisible(true);
AssociationErrorFragment fragment = new AssociationErrorFragment();
launchFragment(fragment, ASSOCIATION_ERROR_FRAGMENT_TAG);
}
@@ -197,7 +197,7 @@ public class AssociationActivity extends FragmentActivity {
private void showAssociatedDeviceDetailFragment() {
AssociatedDeviceDetailFragment fragment = new AssociatedDeviceDetailFragment();
launchFragment(fragment, DEVICE_DETAIL_FRAGMENT_TAG);
- mToolbar.hideProgressBar();
+ mToolbar.getProgressBar().setVisible(false);
showTurnOnBluetoothDialog();
}
@@ -257,7 +257,7 @@ public class AssociationActivity extends FragmentActivity {
private void retryAssociation() {
dismissConfirmButtons();
- mToolbar.showProgressBar();
+ mToolbar.getProgressBar().setVisible(true);
Fragment fragment = getSupportFragmentManager()
.findFragmentByTag(PAIRING_CODE_FRAGMENT_TAG);
if (fragment != null) {
diff --git a/src/com/android/car/companiondevicesupport/feature/notificationmsg/NotificationMsgDelegate.java b/src/com/android/car/companiondevicesupport/feature/notificationmsg/NotificationMsgDelegate.java
index 1956a10..e2c3ab7 100644
--- a/src/com/android/car/companiondevicesupport/feature/notificationmsg/NotificationMsgDelegate.java
+++ b/src/com/android/car/companiondevicesupport/feature/notificationmsg/NotificationMsgDelegate.java
@@ -45,6 +45,7 @@ import com.android.car.messenger.common.Message;
import com.android.car.messenger.common.ProjectionStateListener;
import com.android.car.messenger.common.SenderKey;
import com.android.car.messenger.common.Utils;
+import com.android.internal.annotations.VisibleForTesting;
import java.util.HashMap;
import java.util.List;
@@ -82,7 +83,7 @@ public class NotificationMsgDelegate extends BaseNotificationDelegate {
protected final Map<SenderKey, Bitmap> mOneOnOneConversationAvatarMap = new HashMap<>();
/** Tracks whether a projection application is active in the foreground. **/
- private ProjectionStateListener mProjectionStateListener;
+ private final ProjectionStateListener mProjectionStateListener;
public NotificationMsgDelegate(Context context) {
super(context, /* useLetterTile */ false);
@@ -187,19 +188,23 @@ public class NotificationMsgDelegate extends BaseNotificationDelegate {
ConversationNotification notification, String notificationKey) {
String deviceAddress = device.getDeviceId();
ConversationKey convoKey = new ConversationKey(deviceAddress, notificationKey);
- if (mNotificationInfos.containsKey(convoKey)) {
- logw(TAG, "Conversation already exists! " + notificationKey);
- }
if (!Utils.isValidConversationNotification(notification, /* isShallowCheck= */ false)) {
logd(TAG, "Failed to initialize new Conversation, object missing required fields");
return;
}
- ConversationNotificationInfo convoInfo = ConversationNotificationInfo.
- createConversationNotificationInfo(device.getDeviceName(), device.getDeviceId(),
- notification, notificationKey);
- mNotificationInfos.put(convoKey, convoInfo);
+ ConversationNotificationInfo convoInfo;
+ if (mNotificationInfos.containsKey(convoKey)) {
+ logw(TAG, "Conversation already exists! " + notificationKey);
+ convoInfo = mNotificationInfos.get(convoKey);
+ } else {
+ convoInfo = ConversationNotificationInfo.
+ createConversationNotificationInfo(device.getDeviceName(), device.getDeviceId(),
+ notification, notificationKey);
+ mNotificationInfos.put(convoKey, convoInfo);
+ }
+
String appDisplayName = convoInfo.getAppDisplayName();
@@ -242,7 +247,8 @@ public class NotificationMsgDelegate extends BaseNotificationDelegate {
if (!notificationInfo.isGroupConvo()) {
return mOneOnOneConversationAvatarMap.get(
SenderKey.createSenderKey(convoKey, message.getSender()));
- } else if (message.getSender().getAvatar() != null) {
+ } else if (message.getSender().getAvatar() != null
+ || !message.getSender().getAvatar().isEmpty()) {
byte[] iconArray = message.getSender().getAvatar().toByteArray();
return BitmapFactory.decodeByteArray(iconArray, 0, iconArray.length);
}
@@ -356,4 +362,9 @@ public class NotificationMsgDelegate extends BaseNotificationDelegate {
return ++NEXT_NOTIFICATION_CHANNEL_ID;
}
}
+
+ @VisibleForTesting
+ void setNotificationManager(NotificationManager manager) {
+ mNotificationManager = manager;
+ }
}
diff --git a/src/com/android/car/companiondevicesupport/feature/trust/ui/TrustedDeviceActivity.java b/src/com/android/car/companiondevicesupport/feature/trust/ui/TrustedDeviceActivity.java
index 844af1c..e53e821 100644
--- a/src/com/android/car/companiondevicesupport/feature/trust/ui/TrustedDeviceActivity.java
+++ b/src/com/android/car/companiondevicesupport/feature/trust/ui/TrustedDeviceActivity.java
@@ -119,7 +119,7 @@ public class TrustedDeviceActivity extends FragmentActivity {
mToolbar = requireToolbar(this);
mToolbar.setState(SUBPAGE);
mToolbar.setTitle(R.string.trusted_device_feature_title);
- mToolbar.showProgressBar();
+ mToolbar.getProgressBar().setVisible(true);
mIsScreenLockNewlyCreated.set(false);
mIsStartedForEnrollment.set(false);
mHasPendingCredential.set(false);
@@ -378,7 +378,7 @@ public class TrustedDeviceActivity extends FragmentActivity {
}
private void showTrustedDeviceDetailFragment(AssociatedDevice device) {
- mToolbar.hideProgressBar();
+ mToolbar.getProgressBar().setVisible(false);
TrustedDeviceDetailFragment fragment = TrustedDeviceDetailFragment.newInstance(device);
getSupportFragmentManager().beginTransaction()
.replace(R.id.fragment_container, fragment, DEVICE_DETAIL_FRAGMENT_TAG)
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
new file mode 100644
index 0000000..6adbf61
--- /dev/null
+++ b/tests/unit/src/com/android/car/companiondevicesupport/feature/notificationmsg/NotificationMsgDelegateTest.java
@@ -0,0 +1,349 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.companiondevicesupport.feature.notificationmsg;
+
+
+import static androidx.core.app.NotificationCompat.CATEGORY_MESSAGE;
+
+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.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.content.Context;
+import android.graphics.drawable.Icon;
+
+import androidx.core.app.NotificationCompat;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import com.android.car.companiondevicesupport.api.external.CompanionDevice;
+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.PhoneToCarMessage;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@RunWith(AndroidJUnit4.class)
+public class NotificationMsgDelegateTest {
+ private static final String NOTIFICATION_KEY_1 = "notification_key_1";
+ private static final String NOTIFICATION_KEY_2 = "notification_key_2";
+
+ private static final String COMPANION_DEVICE_ID = "sampleId";
+ private static final String COMPANION_DEVICE_NAME = "sampleName";
+
+ 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 MessagingStyleMessage MESSAGE_2 = MessagingStyleMessage.newBuilder()
+ .setTextMessage("Message 2")
+ .setSender(Person.newBuilder()
+ .setName(SENDER_1))
+ .setTimestamp((long) 1577909718950f)
+ .build();
+
+ private static final MessagingStyle VALID_STYLE = MessagingStyle.newBuilder()
+ .setConvoTitle(CONVERSATION_TITLE)
+ .setUserDisplayName(USER_DISPLAY_NAME)
+ .setIsGroupConvo(false)
+ .addMessagingStyleMsg(MessagingStyleMessage.newBuilder()
+ .setTextMessage("Message 1")
+ .setSender(Person.newBuilder()
+ .setName(SENDER_1))
+ .setTimestamp((long) 1577909718050f)
+ .build())
+ .build();
+
+ private static final ConversationNotification VALID_CONVERSATION =
+ ConversationNotification.newBuilder()
+ .setMessagingAppDisplayName(MESSAGING_APP_NAME)
+ .setMessagingAppPackageName(MESSAGING_PACKAGE_NAME)
+ .setTimeMs((long) 1577909716000f)
+ .setMessagingStyle(VALID_STYLE)
+ .build();
+
+ private static final PhoneToCarMessage VALID_CONVERSATION_MSG = PhoneToCarMessage.newBuilder()
+ .setNotificationKey(NOTIFICATION_KEY_1)
+ .setConversation(VALID_CONVERSATION)
+ .build();
+
+ @Mock
+ CompanionDevice mCompanionDevice;
+ @Mock
+ NotificationManager mMockNotificationManager;
+
+ ArgumentCaptor<Notification> mNotificationCaptor =
+ ArgumentCaptor.forClass(Notification.class);
+ ArgumentCaptor<Integer> mNotificationIdCaptor = ArgumentCaptor.forClass(Integer.class);
+
+ Context mContext = ApplicationProvider.getApplicationContext();
+ NotificationMsgDelegate mNotificationMsgDelegate;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+
+ when(mCompanionDevice.getDeviceId()).thenReturn(COMPANION_DEVICE_ID);
+ when(mCompanionDevice.getDeviceName()).thenReturn(COMPANION_DEVICE_NAME);
+
+ mNotificationMsgDelegate = new NotificationMsgDelegate(mContext);
+ mNotificationMsgDelegate.setNotificationManager(mMockNotificationManager);
+ }
+
+ @Test
+ public void newConversationShouldPostNewNotification() {
+ // Test that a new conversation notification is posted with the correct fields.
+ mNotificationMsgDelegate.onMessageReceived(mCompanionDevice, VALID_CONVERSATION_MSG);
+
+ verify(mMockNotificationManager).notify(anyInt(), mNotificationCaptor.capture());
+
+ Notification postedNotification = mNotificationCaptor.getValue();
+ verifyNotification(VALID_CONVERSATION, postedNotification);
+ }
+
+ @Test
+ public void multipleNewConversationShouldPostMultipleNewNotifications() {
+ mNotificationMsgDelegate.onMessageReceived(mCompanionDevice, VALID_CONVERSATION_MSG);
+
+ verify(mMockNotificationManager).notify(mNotificationIdCaptor.capture(),
+ any(Notification.class));
+ int firstNotificationId = mNotificationIdCaptor.getValue();
+
+ mNotificationMsgDelegate.onMessageReceived(mCompanionDevice,
+ createSecondConversation());
+ verify(mMockNotificationManager, times(2)).notify(mNotificationIdCaptor.capture(),
+ any(Notification.class));
+
+ // Verify the notification id is different than the first.
+ assertThat((long) mNotificationIdCaptor.getValue()).isNotEqualTo(firstNotificationId);
+ }
+
+ @Test
+ public void invalidConversationShouldDoNothing() {
+ // Test that a conversation without all the required fields is dropped.
+ PhoneToCarMessage newConvo = PhoneToCarMessage.newBuilder()
+ .setNotificationKey(NOTIFICATION_KEY_1)
+ .setConversation(VALID_CONVERSATION.toBuilder().clearMessagingStyle())
+ .build();
+ mNotificationMsgDelegate.onMessageReceived(mCompanionDevice, newConvo);
+
+ verify(mMockNotificationManager, never()).notify(anyInt(), any(Notification.class));
+ }
+
+ @Test
+ public void newMessageShouldUpdateConversationNotification() {
+ // Check whether a new message updates the notification of the conversation it belongs to.
+ mNotificationMsgDelegate.onMessageReceived(mCompanionDevice, VALID_CONVERSATION_MSG);
+ verify(mMockNotificationManager).notify(mNotificationIdCaptor.capture(),
+ any(Notification.class));
+ int notificationId = mNotificationIdCaptor.getValue();
+ int messageCount = VALID_CONVERSATION_MSG.getConversation().getMessagingStyle()
+ .getMessagingStyleMsgCount();
+
+ PhoneToCarMessage updateConvo = PhoneToCarMessage.newBuilder()
+ .setNotificationKey(NOTIFICATION_KEY_1)
+ .setMessage(MESSAGE_2)
+ .build();
+ mNotificationMsgDelegate.onMessageReceived(mCompanionDevice, updateConvo);
+
+ // Verify same notification id is posted twice.
+ verify(mMockNotificationManager, times(2)).notify(eq(notificationId),
+ mNotificationCaptor.capture());
+
+ // Verify the notification contains one more message.
+ NotificationCompat.MessagingStyle messagingStyle = getMessagingStyle(
+ mNotificationCaptor.getValue());
+ assertThat(messagingStyle.getMessages().size()).isEqualTo(messageCount + 1);
+
+ // Verify notification's latest message matches the new message.
+ verifyMessage(MESSAGE_2, messagingStyle.getMessages().get(messageCount));
+ }
+
+ @Test
+ public void existingConversationShouldUpdateNotification() {
+ // Test that a conversation that already exists, but gets a new conversation message
+ // is updated with the new conversation metadata.
+ mNotificationMsgDelegate.onMessageReceived(mCompanionDevice, VALID_CONVERSATION_MSG);
+ verify(mMockNotificationManager).notify(mNotificationIdCaptor.capture(),
+ any(Notification.class));
+ int notificationId = mNotificationIdCaptor.getValue();
+
+ ConversationNotification updatedConversation = addSecondMessageToConversation().toBuilder()
+ .setMessagingAppDisplayName("New Messaging App")
+ .build();
+ mNotificationMsgDelegate.onMessageReceived(mCompanionDevice, PhoneToCarMessage.newBuilder()
+ .setNotificationKey(NOTIFICATION_KEY_1)
+ .setConversation(updatedConversation)
+ .build());
+
+ verify(mMockNotificationManager, times(2)).notify(eq(notificationId),
+ mNotificationCaptor.capture());
+ Notification postedNotification = mNotificationCaptor.getValue();
+
+ // Verify Conversation level metadata does NOT change
+ verifyConversationLevelMetadata(VALID_CONVERSATION, postedNotification);
+ // Verify the MessagingStyle metadata does update with the new message.
+ verifyMessagingStyle(updatedConversation.getMessagingStyle(), postedNotification);
+ }
+
+ @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);
+
+ verify(mMockNotificationManager, never()).notify(anyInt(), any(Notification.class));
+ }
+
+ @Test
+ public void invalidMessageShouldDoNothing() {
+ // Message without all the required fields is dropped.
+ mNotificationMsgDelegate.onMessageReceived(mCompanionDevice, VALID_CONVERSATION_MSG);
+
+ // Create a MessagingStyleMessage without a required field (Sender information).
+ MessagingStyleMessage invalidMsgStyleMessage = MessagingStyleMessage.newBuilder()
+ .setTextMessage("Message 2")
+ .setTimestamp((long) 1577909718950f)
+ .build();
+ PhoneToCarMessage invalidMessage = PhoneToCarMessage.newBuilder()
+ .setNotificationKey(NOTIFICATION_KEY_1)
+ .setMessage(invalidMsgStyleMessage)
+ .build();
+ mNotificationMsgDelegate.onMessageReceived(mCompanionDevice, invalidMessage);
+
+ // Verify only one notification is posted, and never updated.
+ verify(mMockNotificationManager).notify(anyInt(), any(Notification.class));
+ }
+
+ private void verifyNotification(ConversationNotification expected, Notification notification) {
+ verifyConversationLevelMetadata(expected, notification);
+ verifyMessagingStyle(expected.getMessagingStyle(), notification);
+ }
+
+ /**
+ * Verifies the conversation level metadata and other aspects of a notification that do not
+ * change when a new message is added to it (such as the actions, intents).
+ */
+ private void verifyConversationLevelMetadata(ConversationNotification expected,
+ Notification notification) {
+ assertThat(notification.category).isEqualTo(CATEGORY_MESSAGE);
+
+ assertThat(notification.getSmallIcon()).isNotNull();
+ if (!expected.getAppIcon().isEmpty()) {
+ byte[] iconBytes = expected.getAppIcon().toByteArray();
+ Icon appIcon = Icon.createWithData(iconBytes, 0, iconBytes.length);
+ assertThat(notification.getSmallIcon()).isEqualTo(appIcon);
+ }
+
+ assertThat(notification.deleteIntent).isNotNull();
+
+ if (expected.getMessagingAppPackageName() != null) {
+ CharSequence appName = notification.extras.getCharSequence(
+ Notification.EXTRA_SUBSTITUTE_APP_NAME);
+ assertThat(appName).isEqualTo(expected.getMessagingAppDisplayName());
+ }
+
+ assertThat(notification.actions.length).isEqualTo(2);
+ for (NotificationCompat.Action action : getAllActions(notification)) {
+ if (action.getSemanticAction() == NotificationCompat.Action.SEMANTIC_ACTION_REPLY) {
+ assertThat(action.getRemoteInputs().length).isEqualTo(1);
+ }
+ assertThat(action.getShowsUserInterface()).isFalse();
+ }
+ }
+
+ private void verifyMessagingStyle(MessagingStyle expected, Notification notification) {
+ final NotificationCompat.MessagingStyle messagingStyle = getMessagingStyle(notification);
+
+ assertThat(messagingStyle.getUser().getName()).isEqualTo(expected.getUserDisplayName());
+ assertThat(messagingStyle.isGroupConversation()).isEqualTo(expected.getIsGroupConvo());
+ assertThat(messagingStyle.getMessages().size()).isEqualTo(expected.getMessagingStyleMsgCount());
+
+ for (int i = 0; i < expected.getMessagingStyleMsgCount(); i++) {
+ MessagingStyleMessage expectedMsg = expected.getMessagingStyleMsg(i);
+ NotificationCompat.MessagingStyle.Message actualMsg = messagingStyle.getMessages().get(
+ i);
+ verifyMessage(expectedMsg, actualMsg);
+
+ }
+ }
+
+ private void verifyMessage(MessagingStyleMessage expectedMsg,
+ NotificationCompat.MessagingStyle.Message actualMsg) {
+ assertThat(actualMsg.getTimestamp()).isEqualTo(expectedMsg.getTimestamp());
+ assertThat(actualMsg.getText()).isEqualTo(expectedMsg.getTextMessage());
+
+ Person expectedSender = expectedMsg.getSender();
+ androidx.core.app.Person actualSender = actualMsg.getPerson();
+ assertThat(actualSender.getName()).isEqualTo(expectedSender.getName());
+ if (!expectedSender.getAvatar().isEmpty()) {
+ assertThat(actualSender.getIcon()).isNotNull();
+ } else {
+ assertThat(actualSender.getIcon()).isNull();
+ }
+ }
+
+ private NotificationCompat.MessagingStyle getMessagingStyle(Notification notification) {
+ return NotificationCompat.MessagingStyle.extractMessagingStyleFromNotification(
+ notification);
+ }
+
+ private List<NotificationCompat.Action> getAllActions(Notification notification) {
+ List<NotificationCompat.Action> actions = new ArrayList<>();
+ actions.addAll(NotificationCompat.getInvisibleActions(notification));
+ for (int i = 0; i < NotificationCompat.getActionCount(notification); i++) {
+ actions.add(NotificationCompat.getAction(notification, i));
+ }
+ return actions;
+ }
+
+ private PhoneToCarMessage createSecondConversation() {
+ return VALID_CONVERSATION_MSG.toBuilder()
+ .setNotificationKey(NOTIFICATION_KEY_2)
+ .setConversation(addSecondMessageToConversation())
+ .build();
+ }
+
+ private ConversationNotification addSecondMessageToConversation() {
+ return VALID_CONVERSATION.toBuilder()
+ .setMessagingStyle(
+ VALID_STYLE.toBuilder().addMessagingStyleMsg(MESSAGE_2)).build();
+ }
+}
diff --git a/tests/unit/src/com/android/car/companiondevicesupport/feature/notificationmsg/NotificationMsgFeatureTest.java b/tests/unit/src/com/android/car/companiondevicesupport/feature/notificationmsg/NotificationMsgFeatureTest.java
new file mode 100644
index 0000000..e195610
--- /dev/null
+++ b/tests/unit/src/com/android/car/companiondevicesupport/feature/notificationmsg/NotificationMsgFeatureTest.java
@@ -0,0 +1,159 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.companiondevicesupport.feature.notificationmsg;
+
+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.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.content.ServiceConnection;
+import android.os.IBinder;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import com.android.car.companiondevicesupport.R;
+import com.android.car.companiondevicesupport.api.external.CompanionDevice;
+import com.android.car.messenger.NotificationMsgProto.NotificationMsg;
+import com.android.car.messenger.common.CompositeKey;
+import com.android.car.messenger.common.ConversationKey;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.UUID;
+import java.util.function.Predicate;
+
+@RunWith(AndroidJUnit4.class)
+public class NotificationMsgFeatureTest {
+
+ private static final String DEVICE_ID = UUID.randomUUID().toString();
+ private static final NotificationMsg.PhoneToCarMessage PHONE_TO_CAR_MESSAGE =
+ NotificationMsg.PhoneToCarMessage.newBuilder()
+ .build();
+
+ @Mock
+ private Context mContext;
+ @Mock
+ private IBinder mIBinder;
+ @Mock
+ private NotificationMsgDelegate mNotificationMsgDelegate;
+ @Mock
+ private CompanionDevice mCompanionDevice;
+
+
+ private NotificationMsgFeature mNotificationMsgFeature;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+
+ when(mContext.getString(eq(R.string.notification_msg_feature_id))).thenReturn(
+ UUID.randomUUID().toString());
+ when(mContext.bindServiceAsUser(any(), any(), anyInt(), any())).thenReturn(true);
+ when(mCompanionDevice.getDeviceId()).thenReturn(DEVICE_ID);
+
+ mNotificationMsgFeature = new NotificationMsgFeature(mContext, mNotificationMsgDelegate);
+ }
+
+ @Test
+ public void startShouldClearInternalMemory() {
+ mNotificationMsgFeature.start();
+
+ ArgumentCaptor<Predicate<CompositeKey>> predicateArgumentCaptor = ArgumentCaptor.forClass(
+ Predicate.class);
+ verify(mNotificationMsgDelegate).cleanupMessagesAndNotifications(
+ predicateArgumentCaptor.capture());
+
+ // There's no way to test if two predicates have the same logic, so test the logic itself.
+ // The expected predicate should return true for any CompositeKey. Since CompositeKey is
+ // an abstract class, test with a class that extends it.
+ ConversationKey device_key = new ConversationKey(DEVICE_ID, "subKey");
+ ConversationKey device_key_1 = new ConversationKey("NOT_DEVICE_ID", "subKey");
+ assertThat(predicateArgumentCaptor.getValue().test(device_key)).isTrue();
+ assertThat(predicateArgumentCaptor.getValue().test(device_key_1)).isTrue();
+ assertThat(predicateArgumentCaptor.getValue().test(null)).isTrue();
+ }
+
+ @Test
+ public void stopShouldDestroyDelegate() {
+ mNotificationMsgFeature.start();
+
+ ArgumentCaptor<ServiceConnection> serviceConnectionCaptor =
+ ArgumentCaptor.forClass(ServiceConnection.class);
+ verify(mContext).bindServiceAsUser(any(), serviceConnectionCaptor.capture(), anyInt(),
+ any());
+ serviceConnectionCaptor.getValue().onServiceConnected(null, mIBinder);
+ mNotificationMsgFeature.stop();
+ verify(mNotificationMsgDelegate).onDestroy();
+ }
+
+ @Test
+ public void onMessageReceivedShouldPassMessageToDelegate() {
+ startWithSecureDevice();
+
+ mNotificationMsgFeature.onMessageReceived(mCompanionDevice,
+ PHONE_TO_CAR_MESSAGE.toByteArray());
+ verify(mNotificationMsgDelegate).onMessageReceived(mCompanionDevice, PHONE_TO_CAR_MESSAGE);
+ }
+
+ @Test
+ public void onMessageReceivedShouldCheckDeviceConnection() {
+ when(mCompanionDevice.hasSecureChannel()).thenReturn(false);
+ when(mCompanionDevice.isActiveUser()).thenReturn(true);
+ mNotificationMsgFeature.start();
+
+ mNotificationMsgFeature.onMessageReceived(mCompanionDevice,
+ PHONE_TO_CAR_MESSAGE.toByteArray());
+ verify(mNotificationMsgDelegate, never()).onMessageReceived(mCompanionDevice,
+ PHONE_TO_CAR_MESSAGE);
+ }
+
+ @Test
+ public void unknownDeviceDisconnectedShouldDoNothing() {
+ when(mCompanionDevice.hasSecureChannel()).thenReturn(true);
+ when(mCompanionDevice.isActiveUser()).thenReturn(true);
+ mNotificationMsgFeature.start();
+
+ mNotificationMsgFeature.onDeviceDisconnected(mCompanionDevice);
+ verify(mNotificationMsgDelegate, never()).onDeviceDisconnected(DEVICE_ID);
+ }
+
+ @Test
+ public void secureDeviceDisconnectedShouldAlertDelegate() {
+ startWithSecureDevice();
+
+ mNotificationMsgFeature.onDeviceDisconnected(mCompanionDevice);
+ verify(mNotificationMsgDelegate).onDeviceDisconnected(DEVICE_ID);
+ }
+
+ private void startWithSecureDevice() {
+ when(mCompanionDevice.hasSecureChannel()).thenReturn(true);
+ when(mCompanionDevice.isActiveUser()).thenReturn(true);
+ mNotificationMsgFeature.start();
+ mNotificationMsgFeature.onSecureChannelEstablished(mCompanionDevice);
+ }
+}
diff --git a/tests/unit/src/com/android/car/companiondevicesupport/feature/notificationmsg/OWNERS b/tests/unit/src/com/android/car/companiondevicesupport/feature/notificationmsg/OWNERS
new file mode 100644
index 0000000..0c113c4
--- /dev/null
+++ b/tests/unit/src/com/android/car/companiondevicesupport/feature/notificationmsg/OWNERS
@@ -0,0 +1,3 @@
+# People who can approve changes for submission.
+igorr@google.com
+jiayuzhou@google.com