Mark Renouf | 6b2331c | 2019-03-21 13:40:08 -0400 | [diff] [blame] | 1 | /* |
| 2 | * Copyright (C) 2019 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.systemui.statusbar.phone; |
| 18 | |
Mady Mellor | c2ff011 | 2019-03-28 14:18:06 -0700 | [diff] [blame] | 19 | import static android.service.notification.NotificationListenerService.REASON_CLICK; |
| 20 | |
Mark Renouf | 6b2331c | 2019-03-21 13:40:08 -0400 | [diff] [blame] | 21 | import static org.mockito.AdditionalAnswers.answerVoid; |
| 22 | import static org.mockito.ArgumentMatchers.any; |
| 23 | import static org.mockito.ArgumentMatchers.anyBoolean; |
| 24 | import static org.mockito.ArgumentMatchers.anyInt; |
| 25 | import static org.mockito.ArgumentMatchers.eq; |
| 26 | import static org.mockito.Mockito.atLeastOnce; |
| 27 | import static org.mockito.Mockito.doAnswer; |
| 28 | import static org.mockito.Mockito.mock; |
Mark Renouf | fec45da | 2019-03-13 13:24:27 -0400 | [diff] [blame] | 29 | import static org.mockito.Mockito.never; |
Mark Renouf | 6b2331c | 2019-03-21 13:40:08 -0400 | [diff] [blame] | 30 | import static org.mockito.Mockito.verify; |
Mark Renouf | fec45da | 2019-03-13 13:24:27 -0400 | [diff] [blame] | 31 | import static org.mockito.Mockito.verifyNoMoreInteractions; |
| 32 | import static org.mockito.Mockito.verifyZeroInteractions; |
Mark Renouf | 6b2331c | 2019-03-21 13:40:08 -0400 | [diff] [blame] | 33 | import static org.mockito.Mockito.when; |
| 34 | |
| 35 | import android.app.KeyguardManager; |
| 36 | import android.app.Notification; |
| 37 | import android.app.PendingIntent; |
| 38 | import android.content.Context; |
| 39 | import android.content.Intent; |
| 40 | import android.os.Handler; |
| 41 | import android.os.RemoteException; |
| 42 | import android.os.UserHandle; |
| 43 | import android.service.dreams.IDreamManager; |
| 44 | import android.service.notification.StatusBarNotification; |
| 45 | import android.testing.AndroidTestingRunner; |
| 46 | import android.testing.TestableLooper; |
| 47 | |
| 48 | import androidx.test.filters.SmallTest; |
| 49 | |
| 50 | import com.android.internal.logging.MetricsLogger; |
| 51 | import com.android.internal.statusbar.IStatusBarService; |
| 52 | import com.android.internal.statusbar.NotificationVisibility; |
| 53 | import com.android.internal.widget.LockPatternUtils; |
| 54 | import com.android.systemui.ActivityIntentHelper; |
| 55 | import com.android.systemui.SysuiTestCase; |
| 56 | import com.android.systemui.assist.AssistManager; |
Mark Renouf | fec45da | 2019-03-13 13:24:27 -0400 | [diff] [blame] | 57 | import com.android.systemui.bubbles.BubbleController; |
Mark Renouf | 6b2331c | 2019-03-21 13:40:08 -0400 | [diff] [blame] | 58 | import com.android.systemui.plugins.ActivityStarter; |
| 59 | import com.android.systemui.plugins.statusbar.StatusBarStateController; |
| 60 | import com.android.systemui.statusbar.CommandQueue; |
| 61 | import com.android.systemui.statusbar.NotificationLockscreenUserManager; |
| 62 | import com.android.systemui.statusbar.NotificationPresenter; |
| 63 | import com.android.systemui.statusbar.NotificationRemoteInputManager; |
| 64 | import com.android.systemui.statusbar.NotificationTestHelper; |
| 65 | import com.android.systemui.statusbar.RemoteInputController; |
| 66 | import com.android.systemui.statusbar.StatusBarState; |
| 67 | import com.android.systemui.statusbar.notification.ActivityLaunchAnimator; |
| 68 | import com.android.systemui.statusbar.notification.NotificationActivityStarter; |
| 69 | import com.android.systemui.statusbar.notification.NotificationEntryManager; |
| 70 | import com.android.systemui.statusbar.notification.NotificationInterruptionStateProvider; |
| 71 | import com.android.systemui.statusbar.notification.collection.NotificationData; |
| 72 | import com.android.systemui.statusbar.notification.collection.NotificationEntry; |
| 73 | import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; |
| 74 | import com.android.systemui.statusbar.policy.KeyguardMonitor; |
| 75 | |
| 76 | import org.junit.Before; |
| 77 | import org.junit.Test; |
| 78 | import org.junit.runner.RunWith; |
| 79 | import org.mockito.Mock; |
| 80 | import org.mockito.MockitoAnnotations; |
| 81 | import org.mockito.stubbing.Answer; |
| 82 | |
| 83 | import java.util.ArrayList; |
| 84 | |
| 85 | @SmallTest |
| 86 | @RunWith(AndroidTestingRunner.class) |
| 87 | @TestableLooper.RunWithLooper(setAsMainLooper = true) |
| 88 | public class StatusBarNotificationActivityStarterTest extends SysuiTestCase { |
| 89 | |
| 90 | @Mock |
| 91 | private AssistManager mAssistManager; |
| 92 | @Mock |
| 93 | private NotificationEntryManager mEntryManager; |
| 94 | @Mock |
| 95 | private ActivityStarter mActivityStarter; |
| 96 | @Mock |
| 97 | private IStatusBarService mStatusBarService; |
| 98 | @Mock |
| 99 | private StatusBarStateController mStatusBarStateController; |
| 100 | @Mock |
| 101 | private NotificationRemoteInputManager mRemoteInputManager; |
| 102 | @Mock |
| 103 | private RemoteInputController mRemoteInputController; |
| 104 | @Mock |
| 105 | private ShadeController mShadeController; |
| 106 | @Mock |
| 107 | private KeyguardMonitor mKeyguardMonitor; |
| 108 | @Mock |
| 109 | private Handler mHandler; |
Mark Renouf | fec45da | 2019-03-13 13:24:27 -0400 | [diff] [blame] | 110 | @Mock |
| 111 | private BubbleController mBubbleController; |
Mark Renouf | 6b2331c | 2019-03-21 13:40:08 -0400 | [diff] [blame] | 112 | |
| 113 | @Mock |
| 114 | private ActivityIntentHelper mActivityIntentHelper; |
| 115 | @Mock |
| 116 | private PendingIntent mContentIntent; |
| 117 | @Mock |
Mark Renouf | fec45da | 2019-03-13 13:24:27 -0400 | [diff] [blame] | 118 | private Intent mContentIntentInner; |
| 119 | @Mock |
Mark Renouf | 6b2331c | 2019-03-21 13:40:08 -0400 | [diff] [blame] | 120 | private NotificationData mNotificationData; |
Mark Renouf | 6b2331c | 2019-03-21 13:40:08 -0400 | [diff] [blame] | 121 | |
| 122 | private NotificationActivityStarter mNotificationActivityStarter; |
| 123 | |
| 124 | private NotificationTestHelper mNotificationTestHelper; |
Mark Renouf | fec45da | 2019-03-13 13:24:27 -0400 | [diff] [blame] | 125 | private ExpandableNotificationRow mNotificationRow; |
| 126 | private ExpandableNotificationRow mBubbleNotificationRow; |
Mark Renouf | 6b2331c | 2019-03-21 13:40:08 -0400 | [diff] [blame] | 127 | |
| 128 | private final Answer<Void> mCallOnDismiss = answerVoid( |
| 129 | (ActivityStarter.OnDismissAction dismissAction, Runnable cancel, |
| 130 | Boolean afterKeyguardGone) -> dismissAction.onDismiss()); |
| 131 | private ArrayList<NotificationEntry> mActiveNotifications; |
| 132 | |
| 133 | @Before |
| 134 | public void setUp() throws Exception { |
| 135 | MockitoAnnotations.initMocks(this); |
| 136 | when(mRemoteInputManager.getController()).thenReturn(mRemoteInputController); |
| 137 | when(mEntryManager.getNotificationData()).thenReturn(mNotificationData); |
| 138 | |
Mark Renouf | fec45da | 2019-03-13 13:24:27 -0400 | [diff] [blame] | 139 | when(mContentIntent.isActivity()).thenReturn(true); |
| 140 | when(mContentIntent.getCreatorUserHandle()).thenReturn(UserHandle.of(1)); |
| 141 | when(mContentIntent.getIntent()).thenReturn(mContentIntentInner); |
Mark Renouf | 6b2331c | 2019-03-21 13:40:08 -0400 | [diff] [blame] | 142 | |
| 143 | mNotificationTestHelper = new NotificationTestHelper(mContext); |
Mark Renouf | fec45da | 2019-03-13 13:24:27 -0400 | [diff] [blame] | 144 | |
| 145 | // Create standard notification with contentIntent |
Mark Renouf | 6b2331c | 2019-03-21 13:40:08 -0400 | [diff] [blame] | 146 | mNotificationRow = mNotificationTestHelper.createRow(); |
Mark Renouf | fec45da | 2019-03-13 13:24:27 -0400 | [diff] [blame] | 147 | StatusBarNotification sbn = mNotificationRow.getStatusBarNotification(); |
| 148 | sbn.getNotification().contentIntent = mContentIntent; |
| 149 | sbn.getNotification().flags |= Notification.FLAG_AUTO_CANCEL; |
| 150 | |
| 151 | // Create bubble notification row with contentIntent |
| 152 | mBubbleNotificationRow = mNotificationTestHelper.createBubble(); |
| 153 | StatusBarNotification bubbleSbn = mBubbleNotificationRow.getStatusBarNotification(); |
| 154 | bubbleSbn.getNotification().contentIntent = mContentIntent; |
| 155 | bubbleSbn.getNotification().flags |= Notification.FLAG_AUTO_CANCEL; |
| 156 | // Do what BubbleController's NotificationEntryListener#onPendingEntryAdded does: |
Mark Renouf | fec45da | 2019-03-13 13:24:27 -0400 | [diff] [blame] | 157 | mBubbleNotificationRow.getEntry().setShowInShadeWhenBubble(true); |
| 158 | |
| 159 | mActiveNotifications = new ArrayList<>(); |
| 160 | mActiveNotifications.add(mNotificationRow.getEntry()); |
| 161 | mActiveNotifications.add(mBubbleNotificationRow.getEntry()); |
| 162 | when(mNotificationData.getActiveNotifications()).thenReturn(mActiveNotifications); |
| 163 | when(mStatusBarStateController.getState()).thenReturn(StatusBarState.SHADE); |
Mark Renouf | 6b2331c | 2019-03-21 13:40:08 -0400 | [diff] [blame] | 164 | |
| 165 | mNotificationActivityStarter = new StatusBarNotificationActivityStarter(getContext(), |
| 166 | mock(CommandQueue.class), mAssistManager, mock(NotificationPanelView.class), |
| 167 | mock(NotificationPresenter.class), mEntryManager, mock(HeadsUpManagerPhone.class), |
| 168 | mActivityStarter, mock(ActivityLaunchAnimator.class), mStatusBarService, |
| 169 | mock(StatusBarStateController.class), mock(KeyguardManager.class), |
| 170 | mock(IDreamManager.class), mRemoteInputManager, |
| 171 | mock(StatusBarRemoteInputCallback.class), mock(NotificationGroupManager.class), |
| 172 | mock(NotificationLockscreenUserManager.class), mShadeController, mKeyguardMonitor, |
| 173 | mock(NotificationInterruptionStateProvider.class), mock(MetricsLogger.class), |
Mark Renouf | fec45da | 2019-03-13 13:24:27 -0400 | [diff] [blame] | 174 | mock(LockPatternUtils.class), mHandler, mHandler, mActivityIntentHelper, |
| 175 | mBubbleController); |
Mark Renouf | 6b2331c | 2019-03-21 13:40:08 -0400 | [diff] [blame] | 176 | |
| 177 | // set up dismissKeyguardThenExecute to synchronously invoke the OnDismissAction arg |
| 178 | doAnswer(mCallOnDismiss).when(mActivityStarter).dismissKeyguardThenExecute( |
| 179 | any(ActivityStarter.OnDismissAction.class), any(), anyBoolean()); |
| 180 | |
| 181 | // set up addAfterKeyguardGoneRunnable to synchronously invoke the Runnable arg |
| 182 | doAnswer(answerVoid(Runnable::run)) |
| 183 | .when(mShadeController).addAfterKeyguardGoneRunnable(any(Runnable.class)); |
| 184 | |
| 185 | // set up addPostCollapseAction to synchronously invoke the Runnable arg |
| 186 | doAnswer(answerVoid(Runnable::run)) |
| 187 | .when(mShadeController).addPostCollapseAction(any(Runnable.class)); |
| 188 | |
| 189 | // set up Handler to synchronously invoke the Runnable arg |
| 190 | doAnswer(answerVoid(Runnable::run)) |
| 191 | .when(mHandler).post(any(Runnable.class)); |
Mark Renouf | fec45da | 2019-03-13 13:24:27 -0400 | [diff] [blame] | 192 | |
| 193 | doAnswer(answerVoid(Runnable::run)) |
| 194 | .when(mHandler).postAtFrontOfQueue(any(Runnable.class)); |
Mark Renouf | 6b2331c | 2019-03-21 13:40:08 -0400 | [diff] [blame] | 195 | } |
| 196 | |
| 197 | @Test |
Mark Renouf | fec45da | 2019-03-13 13:24:27 -0400 | [diff] [blame] | 198 | public void testOnNotificationClicked_keyGuardShowing() |
Mark Renouf | 6b2331c | 2019-03-21 13:40:08 -0400 | [diff] [blame] | 199 | throws PendingIntent.CanceledException, RemoteException { |
| 200 | // Given |
Mark Renouf | fec45da | 2019-03-13 13:24:27 -0400 | [diff] [blame] | 201 | StatusBarNotification sbn = mNotificationRow.getStatusBarNotification(); |
| 202 | sbn.getNotification().contentIntent = mContentIntent; |
| 203 | sbn.getNotification().flags |= Notification.FLAG_AUTO_CANCEL; |
| 204 | |
Mark Renouf | 6b2331c | 2019-03-21 13:40:08 -0400 | [diff] [blame] | 205 | when(mKeyguardMonitor.isShowing()).thenReturn(true); |
| 206 | when(mShadeController.isOccluded()).thenReturn(true); |
Mark Renouf | 6b2331c | 2019-03-21 13:40:08 -0400 | [diff] [blame] | 207 | |
| 208 | // When |
Mark Renouf | fec45da | 2019-03-13 13:24:27 -0400 | [diff] [blame] | 209 | mNotificationActivityStarter.onNotificationClicked(sbn, mNotificationRow); |
Mark Renouf | 6b2331c | 2019-03-21 13:40:08 -0400 | [diff] [blame] | 210 | |
| 211 | // Then |
Mark Renouf | 6b2331c | 2019-03-21 13:40:08 -0400 | [diff] [blame] | 212 | verify(mShadeController, atLeastOnce()).collapsePanel(); |
| 213 | |
| 214 | verify(mContentIntent).sendAndReturnResult( |
| 215 | any(Context.class), |
| 216 | anyInt() /* code */, |
| 217 | any() /* fillInIntent */, |
| 218 | any() /* PendingIntent.OnFinished */, |
| 219 | any() /* Handler */, |
| 220 | any() /* requiredPermission */, |
| 221 | any() /* Bundle options */); |
| 222 | |
| 223 | verify(mAssistManager).hideAssist(); |
| 224 | |
| 225 | verify(mStatusBarService).onNotificationClick( |
Mark Renouf | fec45da | 2019-03-13 13:24:27 -0400 | [diff] [blame] | 226 | eq(sbn.getKey()), any(NotificationVisibility.class)); |
Mark Renouf | 6b2331c | 2019-03-21 13:40:08 -0400 | [diff] [blame] | 227 | |
| 228 | // Notification is removed due to FLAG_AUTO_CANCEL |
Mady Mellor | c2ff011 | 2019-03-28 14:18:06 -0700 | [diff] [blame] | 229 | verify(mEntryManager).performRemoveNotification(eq(sbn), eq(REASON_CLICK)); |
Mark Renouf | fec45da | 2019-03-13 13:24:27 -0400 | [diff] [blame] | 230 | } |
| 231 | |
| 232 | @Test |
| 233 | public void testOnNotificationClicked_bubble_noContentIntent_noKeyGuard() |
| 234 | throws RemoteException { |
| 235 | StatusBarNotification sbn = mBubbleNotificationRow.getStatusBarNotification(); |
| 236 | |
| 237 | // Given |
| 238 | sbn.getNotification().contentIntent = null; |
| 239 | |
| 240 | // When |
| 241 | mNotificationActivityStarter.onNotificationClicked(sbn, mBubbleNotificationRow); |
| 242 | |
| 243 | // Then |
| 244 | verify(mBubbleController).expandStackAndSelectBubble(eq(sbn.getKey())); |
| 245 | |
| 246 | // This is called regardless, and simply short circuits when there is nothing to do. |
| 247 | verify(mShadeController, atLeastOnce()).collapsePanel(); |
| 248 | |
| 249 | verify(mAssistManager).hideAssist(); |
| 250 | |
| 251 | verify(mStatusBarService).onNotificationClick( |
| 252 | eq(sbn.getKey()), any(NotificationVisibility.class)); |
| 253 | |
| 254 | // The content intent should NOT be sent on click. |
| 255 | verifyZeroInteractions(mContentIntent); |
| 256 | |
| 257 | // Notification should not be cancelled. |
Mady Mellor | c2ff011 | 2019-03-28 14:18:06 -0700 | [diff] [blame] | 258 | verify(mEntryManager, never()).performRemoveNotification(eq(sbn), anyInt()); |
Mark Renouf | fec45da | 2019-03-13 13:24:27 -0400 | [diff] [blame] | 259 | } |
| 260 | |
| 261 | @Test |
| 262 | public void testOnNotificationClicked_bubble_noContentIntent_keyGuardShowing() |
| 263 | throws RemoteException { |
| 264 | StatusBarNotification sbn = mBubbleNotificationRow.getStatusBarNotification(); |
| 265 | |
| 266 | // Given |
| 267 | sbn.getNotification().contentIntent = null; |
| 268 | when(mKeyguardMonitor.isShowing()).thenReturn(true); |
| 269 | when(mShadeController.isOccluded()).thenReturn(true); |
| 270 | |
| 271 | // When |
| 272 | mNotificationActivityStarter.onNotificationClicked(sbn, mBubbleNotificationRow); |
| 273 | |
| 274 | // Then |
| 275 | verify(mBubbleController).expandStackAndSelectBubble(eq(sbn.getKey())); |
| 276 | |
| 277 | verify(mShadeController, atLeastOnce()).collapsePanel(); |
| 278 | |
| 279 | verify(mAssistManager).hideAssist(); |
| 280 | |
| 281 | verify(mStatusBarService).onNotificationClick( |
| 282 | eq(sbn.getKey()), any(NotificationVisibility.class)); |
| 283 | |
| 284 | // The content intent should NOT be sent on click. |
| 285 | verifyZeroInteractions(mContentIntent); |
| 286 | |
| 287 | // Notification should not be cancelled. |
Mady Mellor | c2ff011 | 2019-03-28 14:18:06 -0700 | [diff] [blame] | 288 | verify(mEntryManager, never()).performRemoveNotification(eq(sbn), anyInt()); |
Mark Renouf | fec45da | 2019-03-13 13:24:27 -0400 | [diff] [blame] | 289 | } |
| 290 | |
| 291 | @Test |
| 292 | public void testOnNotificationClicked_bubble_withContentIntent_keyGuardShowing() |
| 293 | throws RemoteException { |
| 294 | StatusBarNotification sbn = mBubbleNotificationRow.getStatusBarNotification(); |
| 295 | |
| 296 | // Given |
| 297 | sbn.getNotification().contentIntent = mContentIntent; |
| 298 | when(mKeyguardMonitor.isShowing()).thenReturn(true); |
| 299 | when(mShadeController.isOccluded()).thenReturn(true); |
| 300 | |
| 301 | // When |
| 302 | mNotificationActivityStarter.onNotificationClicked(sbn, mBubbleNotificationRow); |
| 303 | |
| 304 | // Then |
| 305 | verify(mBubbleController).expandStackAndSelectBubble(eq(sbn.getKey())); |
| 306 | |
| 307 | verify(mShadeController, atLeastOnce()).collapsePanel(); |
| 308 | |
| 309 | verify(mAssistManager).hideAssist(); |
| 310 | |
| 311 | verify(mStatusBarService).onNotificationClick( |
| 312 | eq(sbn.getKey()), any(NotificationVisibility.class)); |
| 313 | |
| 314 | // The content intent should NOT be sent on click. |
| 315 | verify(mContentIntent).getIntent(); |
| 316 | verify(mContentIntent).isActivity(); |
| 317 | verifyNoMoreInteractions(mContentIntent); |
| 318 | |
| 319 | // Notification should not be cancelled. |
Mady Mellor | c2ff011 | 2019-03-28 14:18:06 -0700 | [diff] [blame] | 320 | verify(mEntryManager, never()).performRemoveNotification(eq(sbn), anyInt()); |
Mark Renouf | 6b2331c | 2019-03-21 13:40:08 -0400 | [diff] [blame] | 321 | } |
| 322 | } |