| /* |
| * Copyright (C) 2017 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.systemui.statusbar; |
| |
| import static junit.framework.Assert.assertNotNull; |
| import static junit.framework.Assert.assertNull; |
| import static junit.framework.Assert.assertTrue; |
| |
| import static org.junit.Assert.assertEquals; |
| import static org.mockito.ArgumentMatchers.any; |
| import static org.mockito.ArgumentMatchers.anyInt; |
| import static org.mockito.ArgumentMatchers.anyString; |
| import static org.mockito.ArgumentMatchers.eq; |
| import static org.mockito.Mockito.doAnswer; |
| import static org.mockito.Mockito.mock; |
| 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.ActivityManager; |
| import android.app.AppOpsManager; |
| import android.app.Notification; |
| import android.app.NotificationManager; |
| import android.content.Context; |
| import android.os.Handler; |
| import android.os.Looper; |
| import android.os.UserHandle; |
| import android.service.notification.NotificationListenerService; |
| import android.service.notification.StatusBarNotification; |
| import android.testing.AndroidTestingRunner; |
| import android.testing.TestableLooper; |
| import android.util.ArraySet; |
| import android.widget.FrameLayout; |
| |
| import androidx.test.filters.SmallTest; |
| |
| import com.android.internal.logging.MetricsLogger; |
| import com.android.internal.statusbar.IStatusBarService; |
| import com.android.systemui.ForegroundServiceController; |
| import com.android.systemui.R; |
| import com.android.systemui.SysuiTestCase; |
| import com.android.systemui.statusbar.notification.VisualStabilityManager; |
| import com.android.systemui.statusbar.phone.NotificationGroupManager; |
| import com.android.systemui.statusbar.policy.DeviceProvisionedController; |
| import com.android.systemui.statusbar.policy.HeadsUpManager; |
| |
| import junit.framework.Assert; |
| |
| 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.concurrent.CountDownLatch; |
| import java.util.concurrent.TimeUnit; |
| |
| @SmallTest |
| @RunWith(AndroidTestingRunner.class) |
| @TestableLooper.RunWithLooper |
| public class NotificationEntryManagerTest extends SysuiTestCase { |
| private static final String TEST_PACKAGE_NAME = "test"; |
| private static final int TEST_UID = 0; |
| |
| @Mock private NotificationPresenter mPresenter; |
| @Mock private ExpandableNotificationRow mRow; |
| @Mock private NotificationListContainer mListContainer; |
| @Mock private NotificationEntryManager.Callback mCallback; |
| @Mock private HeadsUpManager mHeadsUpManager; |
| @Mock private NotificationListenerService.RankingMap mRankingMap; |
| @Mock private RemoteInputController mRemoteInputController; |
| @Mock private IStatusBarService mBarService; |
| |
| // Dependency mocks: |
| @Mock private ForegroundServiceController mForegroundServiceController; |
| @Mock private NotificationLockscreenUserManager mLockscreenUserManager; |
| @Mock private NotificationGroupManager mGroupManager; |
| @Mock private NotificationGutsManager mGutsManager; |
| @Mock private NotificationRemoteInputManager mRemoteInputManager; |
| @Mock private NotificationMediaManager mMediaManager; |
| @Mock private NotificationListener mNotificationListener; |
| @Mock private DeviceProvisionedController mDeviceProvisionedController; |
| @Mock private VisualStabilityManager mVisualStabilityManager; |
| @Mock private MetricsLogger mMetricsLogger; |
| @Mock private SmartReplyController mSmartReplyController; |
| |
| private NotificationData.Entry mEntry; |
| private StatusBarNotification mSbn; |
| private TestableNotificationEntryManager mEntryManager; |
| private CountDownLatch mCountDownLatch; |
| |
| private class TestableNotificationEntryManager extends NotificationEntryManager { |
| private final CountDownLatch mCountDownLatch; |
| |
| public TestableNotificationEntryManager(Context context, IStatusBarService barService) { |
| super(context); |
| mBarService = barService; |
| mCountDownLatch = new CountDownLatch(1); |
| mUseHeadsUp = true; |
| } |
| |
| @Override |
| public void onAsyncInflationFinished(NotificationData.Entry entry) { |
| super.onAsyncInflationFinished(entry); |
| |
| mCountDownLatch.countDown(); |
| } |
| |
| public CountDownLatch getCountDownLatch() { |
| return mCountDownLatch; |
| } |
| } |
| |
| private void setUserSentiment(String key, int sentiment) { |
| doAnswer(invocationOnMock -> { |
| NotificationListenerService.Ranking ranking = (NotificationListenerService.Ranking) |
| invocationOnMock.getArguments()[1]; |
| ranking.populate( |
| key, |
| 0, |
| false, |
| 0, |
| 0, |
| NotificationManager.IMPORTANCE_DEFAULT, |
| null, null, |
| null, null, null, true, sentiment, false); |
| return true; |
| }).when(mRankingMap).getRanking(eq(key), any(NotificationListenerService.Ranking.class)); |
| } |
| |
| @Before |
| public void setUp() { |
| MockitoAnnotations.initMocks(this); |
| mDependency.injectTestDependency(ForegroundServiceController.class, |
| mForegroundServiceController); |
| mDependency.injectTestDependency(NotificationLockscreenUserManager.class, |
| mLockscreenUserManager); |
| mDependency.injectTestDependency(NotificationGroupManager.class, mGroupManager); |
| mDependency.injectTestDependency(NotificationGutsManager.class, mGutsManager); |
| mDependency.injectTestDependency(NotificationRemoteInputManager.class, mRemoteInputManager); |
| mDependency.injectTestDependency(NotificationMediaManager.class, mMediaManager); |
| mDependency.injectTestDependency(NotificationListener.class, mNotificationListener); |
| mDependency.injectTestDependency(DeviceProvisionedController.class, |
| mDeviceProvisionedController); |
| mDependency.injectTestDependency(VisualStabilityManager.class, mVisualStabilityManager); |
| mDependency.injectTestDependency(MetricsLogger.class, mMetricsLogger); |
| mDependency.injectTestDependency(SmartReplyController.class, mSmartReplyController); |
| |
| mCountDownLatch = new CountDownLatch(1); |
| |
| when(mPresenter.getHandler()).thenReturn(Handler.createAsync(Looper.myLooper())); |
| when(mPresenter.getNotificationLockscreenUserManager()).thenReturn(mLockscreenUserManager); |
| when(mPresenter.getGroupManager()).thenReturn(mGroupManager); |
| when(mRemoteInputManager.getController()).thenReturn(mRemoteInputController); |
| when(mListContainer.getViewParentForNotification(any())).thenReturn( |
| new FrameLayout(mContext)); |
| |
| Notification.Builder n = new Notification.Builder(mContext, "") |
| .setSmallIcon(R.drawable.ic_person) |
| .setContentTitle("Title") |
| .setContentText("Text"); |
| mSbn = new StatusBarNotification(TEST_PACKAGE_NAME, TEST_PACKAGE_NAME, 0, null, TEST_UID, |
| 0, n.build(), new UserHandle(ActivityManager.getCurrentUser()), null, 0); |
| mEntry = new NotificationData.Entry(mSbn); |
| mEntry.expandedIcon = mock(StatusBarIconView.class); |
| |
| mEntryManager = new TestableNotificationEntryManager(mContext, mBarService); |
| mEntryManager.setUpWithPresenter(mPresenter, mListContainer, mCallback, mHeadsUpManager); |
| |
| setUserSentiment(mEntry.key, NotificationListenerService.Ranking.USER_SENTIMENT_NEUTRAL); |
| } |
| |
| @Test |
| public void testAddNotification() throws Exception { |
| com.android.systemui.util.Assert.isNotMainThread(); |
| TestableLooper.get(this).processAllMessages(); |
| |
| doAnswer(invocation -> { |
| mCountDownLatch.countDown(); |
| return null; |
| }).when(mCallback).onBindRow(any(), any(), any(), any()); |
| |
| // Post on main thread, otherwise we will be stuck waiting here for the inflation finished |
| // callback forever, since it won't execute until the tests ends. |
| mEntryManager.addNotification(mSbn, mRankingMap); |
| TestableLooper.get(this).processMessages(1); |
| assertTrue(mCountDownLatch.await(10, TimeUnit.SECONDS)); |
| assertTrue(mEntryManager.getCountDownLatch().await(10, TimeUnit.SECONDS)); |
| |
| // Check that no inflation error occurred. |
| verify(mBarService, never()).onNotificationError(any(), any(), anyInt(), anyInt(), anyInt(), |
| any(), anyInt()); |
| verify(mForegroundServiceController).addNotification(eq(mSbn), anyInt()); |
| |
| // Row inflation: |
| ArgumentCaptor<NotificationData.Entry> entryCaptor = ArgumentCaptor.forClass( |
| NotificationData.Entry.class); |
| verify(mCallback).onBindRow(entryCaptor.capture(), any(), eq(mSbn), any()); |
| NotificationData.Entry entry = entryCaptor.getValue(); |
| verify(mRemoteInputManager).bindRow(entry.row); |
| |
| // Row content inflation: |
| verify(mCallback).onNotificationAdded(entry); |
| verify(mPresenter).updateNotificationViews(); |
| |
| assertEquals(mEntryManager.getNotificationData().get(mSbn.getKey()), entry); |
| assertNotNull(entry.row); |
| assertEquals(mEntry.userSentiment, |
| NotificationListenerService.Ranking.USER_SENTIMENT_NEUTRAL); |
| } |
| |
| @Test |
| public void testUpdateNotification() throws Exception { |
| com.android.systemui.util.Assert.isNotMainThread(); |
| TestableLooper.get(this).processAllMessages(); |
| |
| mEntryManager.getNotificationData().add(mEntry); |
| |
| setUserSentiment(mEntry.key, NotificationListenerService.Ranking.USER_SENTIMENT_NEGATIVE); |
| |
| mEntryManager.updateNotification(mSbn, mRankingMap); |
| TestableLooper.get(this).processMessages(1); |
| // Wait for content update. |
| assertTrue(mEntryManager.getCountDownLatch().await(10, TimeUnit.SECONDS)); |
| |
| verify(mBarService, never()).onNotificationError(any(), any(), anyInt(), anyInt(), anyInt(), |
| any(), anyInt()); |
| |
| verify(mRemoteInputManager).onUpdateNotification(mEntry); |
| verify(mPresenter).updateNotificationViews(); |
| verify(mForegroundServiceController).updateNotification(eq(mSbn), anyInt()); |
| verify(mCallback).onNotificationUpdated(mSbn); |
| assertNotNull(mEntry.row); |
| assertEquals(mEntry.userSentiment, |
| NotificationListenerService.Ranking.USER_SENTIMENT_NEGATIVE); |
| } |
| |
| @Test |
| public void testRemoveNotification() throws Exception { |
| com.android.systemui.util.Assert.isNotMainThread(); |
| |
| mEntry.row = mRow; |
| mEntryManager.getNotificationData().add(mEntry); |
| |
| mEntryManager.removeNotification(mSbn.getKey(), mRankingMap); |
| |
| verify(mBarService, never()).onNotificationError(any(), any(), anyInt(), anyInt(), anyInt(), |
| any(), anyInt()); |
| |
| verify(mMediaManager).onNotificationRemoved(mSbn.getKey()); |
| verify(mRemoteInputManager).onRemoveNotification(mEntry); |
| verify(mSmartReplyController).stopSending(mEntry); |
| verify(mForegroundServiceController).removeNotification(mSbn); |
| verify(mListContainer).cleanUpViewState(mRow); |
| verify(mPresenter).updateNotificationViews(); |
| verify(mCallback).onNotificationRemoved(mSbn.getKey(), mSbn); |
| verify(mRow).setRemoved(); |
| |
| assertNull(mEntryManager.getNotificationData().get(mSbn.getKey())); |
| } |
| |
| @Test |
| public void testRemoveNotification_blockedBySendingSmartReply() throws Exception { |
| com.android.systemui.util.Assert.isNotMainThread(); |
| |
| mEntry.row = mRow; |
| mEntryManager.getNotificationData().add(mEntry); |
| when(mSmartReplyController.isSendingSmartReply(mEntry.key)).thenReturn(true); |
| |
| mEntryManager.removeNotification(mSbn.getKey(), mRankingMap); |
| |
| assertNotNull(mEntryManager.getNotificationData().get(mSbn.getKey())); |
| assertTrue(mEntryManager.isNotificationKeptForRemoteInput(mEntry.key)); |
| } |
| |
| @Test |
| public void testUpdateAppOps_foregroundNoti() { |
| com.android.systemui.util.Assert.isNotMainThread(); |
| |
| when(mForegroundServiceController.getStandardLayoutKey(anyInt(), anyString())) |
| .thenReturn(mEntry.key); |
| mEntry.row = mRow; |
| mEntryManager.getNotificationData().add(mEntry); |
| |
| mEntryManager.updateNotificationsForAppOp( |
| AppOpsManager.OP_CAMERA, mEntry.notification.getUid(), |
| mEntry.notification.getPackageName(), true); |
| |
| verify(mPresenter, times(1)).updateNotificationViews(); |
| assertTrue(mEntryManager.getNotificationData().get(mEntry.key).mActiveAppOps.contains( |
| AppOpsManager.OP_CAMERA)); |
| } |
| |
| @Test |
| public void testUpdateAppOps_otherNoti() { |
| com.android.systemui.util.Assert.isNotMainThread(); |
| |
| when(mForegroundServiceController.getStandardLayoutKey(anyInt(), anyString())) |
| .thenReturn(null); |
| mEntryManager.updateNotificationsForAppOp(AppOpsManager.OP_CAMERA, 1000, "pkg", true); |
| |
| verify(mPresenter, never()).updateNotificationViews(); |
| } |
| |
| @Test |
| public void testAddNotificationExistingAppOps() { |
| mEntry.row = mRow; |
| mEntryManager.getNotificationData().add(mEntry); |
| ArraySet<Integer> expected = new ArraySet<>(); |
| expected.add(3); |
| expected.add(235); |
| expected.add(1); |
| |
| when(mForegroundServiceController.getAppOps(mEntry.notification.getUserId(), |
| mEntry.notification.getPackageName())).thenReturn(expected); |
| when(mForegroundServiceController.getStandardLayoutKey( |
| mEntry.notification.getUserId(), |
| mEntry.notification.getPackageName())).thenReturn(mEntry.key); |
| |
| mEntryManager.tagForeground(mEntry.notification); |
| |
| Assert.assertEquals(expected.size(), mEntry.mActiveAppOps.size()); |
| for (int op : expected) { |
| assertTrue("Entry missing op " + op, mEntry.mActiveAppOps.contains(op)); |
| } |
| } |
| |
| @Test |
| public void testAdd_noExistingAppOps() { |
| mEntry.row = mRow; |
| mEntryManager.getNotificationData().add(mEntry); |
| when(mForegroundServiceController.getStandardLayoutKey( |
| mEntry.notification.getUserId(), |
| mEntry.notification.getPackageName())).thenReturn(mEntry.key); |
| when(mForegroundServiceController.getAppOps(mEntry.notification.getUserId(), |
| mEntry.notification.getPackageName())).thenReturn(null); |
| |
| mEntryManager.tagForeground(mEntry.notification); |
| Assert.assertEquals(0, mEntry.mActiveAppOps.size()); |
| } |
| |
| @Test |
| public void testAdd_existingAppOpsNotForegroundNoti() { |
| mEntry.row = mRow; |
| mEntryManager.getNotificationData().add(mEntry); |
| ArraySet<Integer> ops = new ArraySet<>(); |
| ops.add(3); |
| ops.add(235); |
| ops.add(1); |
| when(mForegroundServiceController.getAppOps(mEntry.notification.getUserId(), |
| mEntry.notification.getPackageName())).thenReturn(ops); |
| when(mForegroundServiceController.getStandardLayoutKey( |
| mEntry.notification.getUserId(), |
| mEntry.notification.getPackageName())).thenReturn("something else"); |
| |
| mEntryManager.tagForeground(mEntry.notification); |
| Assert.assertEquals(0, mEntry.mActiveAppOps.size()); |
| } |
| |
| @Test |
| public void testRebuildWithRemoteInput_noExistingInputNoSpinner() { |
| StatusBarNotification newSbn = |
| mEntryManager.rebuildNotificationWithRemoteInput(mEntry, "A Reply", false); |
| CharSequence[] messages = newSbn.getNotification().extras |
| .getCharSequenceArray(Notification.EXTRA_REMOTE_INPUT_HISTORY); |
| Assert.assertEquals(1, messages.length); |
| Assert.assertEquals("A Reply", messages[0]); |
| Assert.assertFalse(newSbn.getNotification().extras |
| .getBoolean(Notification.EXTRA_SHOW_REMOTE_INPUT_SPINNER, false)); |
| Assert.assertTrue(newSbn.getNotification().extras |
| .getBoolean(Notification.EXTRA_HIDE_SMART_REPLIES, false)); |
| } |
| |
| @Test |
| public void testRebuildWithRemoteInput_noExistingInputWithSpinner() { |
| StatusBarNotification newSbn = |
| mEntryManager.rebuildNotificationWithRemoteInput(mEntry, "A Reply", true); |
| CharSequence[] messages = newSbn.getNotification().extras |
| .getCharSequenceArray(Notification.EXTRA_REMOTE_INPUT_HISTORY); |
| Assert.assertEquals(1, messages.length); |
| Assert.assertEquals("A Reply", messages[0]); |
| Assert.assertTrue(newSbn.getNotification().extras |
| .getBoolean(Notification.EXTRA_SHOW_REMOTE_INPUT_SPINNER, false)); |
| Assert.assertTrue(newSbn.getNotification().extras |
| .getBoolean(Notification.EXTRA_HIDE_SMART_REPLIES, false)); |
| } |
| |
| @Test |
| public void testRebuildWithRemoteInput_withExistingInput() { |
| // Setup a notification entry with 1 remote input. |
| StatusBarNotification newSbn = |
| mEntryManager.rebuildNotificationWithRemoteInput(mEntry, "A Reply", false); |
| NotificationData.Entry entry = new NotificationData.Entry(newSbn); |
| |
| // Try rebuilding to add another reply. |
| newSbn = mEntryManager.rebuildNotificationWithRemoteInput(entry, "Reply 2", true); |
| CharSequence[] messages = newSbn.getNotification().extras |
| .getCharSequenceArray(Notification.EXTRA_REMOTE_INPUT_HISTORY); |
| Assert.assertEquals(2, messages.length); |
| Assert.assertEquals("Reply 2", messages[0]); |
| Assert.assertEquals("A Reply", messages[1]); |
| } |
| |
| @Test |
| public void testRebuildNotificationForCanceledSmartReplies() { |
| // Try rebuilding to remove spinner and hide buttons. |
| StatusBarNotification newSbn = |
| mEntryManager.rebuildNotificationForCanceledSmartReplies(mEntry); |
| Assert.assertFalse(newSbn.getNotification().extras |
| .getBoolean(Notification.EXTRA_SHOW_REMOTE_INPUT_SPINNER, false)); |
| Assert.assertTrue(newSbn.getNotification().extras |
| .getBoolean(Notification.EXTRA_HIDE_SMART_REPLIES, false)); |
| } |
| } |