1 /* 2 * Copyright (C) 2018 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file 5 * except in compliance with the License. You may obtain a copy of the License at 6 * 7 * http://www.apache.org/licenses/LICENSE-2.0 8 * 9 * Unless required by applicable law or agreed to in writing, software distributed under the 10 * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 11 * KIND, either express or implied. See the License for the specific language governing 12 * permissions and limitations under the License. 13 */ 14 15 package com.android.systemui.statusbar.policy; 16 17 import static android.view.ContentInfo.SOURCE_CLIPBOARD; 18 19 import static com.android.systemui.statusbar.notification.stack.StackStateAnimator.ANIMATION_DURATION_STANDARD; 20 21 import static com.google.common.truth.Truth.assertThat; 22 23 import static junit.framework.Assert.assertEquals; 24 import static junit.framework.Assert.assertFalse; 25 import static junit.framework.Assert.assertNotNull; 26 import static junit.framework.Assert.assertTrue; 27 28 import static org.mockito.ArgumentMatchers.any; 29 import static org.mockito.ArgumentMatchers.anyInt; 30 import static org.mockito.ArgumentMatchers.eq; 31 import static org.mockito.Mockito.doReturn; 32 import static org.mockito.Mockito.mock; 33 import static org.mockito.Mockito.spy; 34 import static org.mockito.Mockito.verify; 35 import static org.mockito.Mockito.when; 36 37 import android.app.ActivityManager; 38 import android.app.PendingIntent; 39 import android.app.RemoteInput; 40 import android.content.ClipData; 41 import android.content.ClipDescription; 42 import android.content.Context; 43 import android.content.Intent; 44 import android.content.IntentFilter; 45 import android.content.pm.ShortcutManager; 46 import android.net.Uri; 47 import android.os.Handler; 48 import android.os.Process; 49 import android.os.UserHandle; 50 import android.testing.AndroidTestingRunner; 51 import android.testing.TestableLooper; 52 import android.view.ContentInfo; 53 import android.view.View; 54 import android.view.ViewRootImpl; 55 import android.view.inputmethod.EditorInfo; 56 import android.view.inputmethod.InputConnection; 57 import android.widget.EditText; 58 import android.widget.FrameLayout; 59 import android.widget.ImageButton; 60 import android.window.OnBackInvokedCallback; 61 import android.window.OnBackInvokedDispatcher; 62 import android.window.WindowOnBackInvokedDispatcher; 63 64 import androidx.annotation.NonNull; 65 import androidx.core.animation.AnimatorTestRule2; 66 import androidx.test.filters.SmallTest; 67 68 import com.android.internal.logging.UiEventLogger; 69 import com.android.internal.logging.testing.UiEventLoggerFake; 70 import com.android.systemui.Dependency; 71 import com.android.systemui.R; 72 import com.android.systemui.SysuiTestCase; 73 import com.android.systemui.flags.FakeFeatureFlags; 74 import com.android.systemui.flags.Flags; 75 import com.android.systemui.statusbar.NotificationRemoteInputManager; 76 import com.android.systemui.statusbar.RemoteInputController; 77 import com.android.systemui.statusbar.notification.collection.NotificationEntry; 78 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; 79 import com.android.systemui.statusbar.notification.row.NotificationTestHelper; 80 import com.android.systemui.statusbar.notification.stack.StackStateAnimator; 81 import com.android.systemui.statusbar.phone.LightBarController; 82 83 import org.junit.After; 84 import org.junit.Before; 85 import org.junit.ClassRule; 86 import org.junit.Test; 87 import org.junit.runner.RunWith; 88 import org.mockito.ArgumentCaptor; 89 import org.mockito.Mock; 90 import org.mockito.MockitoAnnotations; 91 92 @RunWith(AndroidTestingRunner.class) 93 @TestableLooper.RunWithLooper 94 @SmallTest 95 public class RemoteInputViewTest extends SysuiTestCase { 96 97 private static final String TEST_RESULT_KEY = "test_result_key"; 98 private static final String TEST_REPLY = "hello"; 99 private static final String TEST_ACTION = "com.android.REMOTE_INPUT_VIEW_ACTION"; 100 101 private static final String DUMMY_MESSAGE_APP_PKG = 102 "com.android.sysuitest.dummynotificationsender"; 103 private static final int DUMMY_MESSAGE_APP_ID = Process.LAST_APPLICATION_UID - 1; 104 105 @Mock private RemoteInputController mController; 106 @Mock private ShortcutManager mShortcutManager; 107 @Mock private RemoteInputQuickSettingsDisabler mRemoteInputQuickSettingsDisabler; 108 @Mock private LightBarController mLightBarController; 109 private BlockingQueueIntentReceiver mReceiver; 110 private final UiEventLoggerFake mUiEventLoggerFake = new UiEventLoggerFake(); 111 112 @ClassRule 113 public static AnimatorTestRule2 mAnimatorTestRule = new AnimatorTestRule2(); 114 115 @Before setUp()116 public void setUp() throws Exception { 117 allowTestableLooperAsMainThread(); 118 MockitoAnnotations.initMocks(this); 119 120 mDependency.injectTestDependency(RemoteInputQuickSettingsDisabler.class, 121 mRemoteInputQuickSettingsDisabler); 122 mDependency.injectTestDependency(LightBarController.class, 123 mLightBarController); 124 mDependency.injectTestDependency(UiEventLogger.class, mUiEventLoggerFake); 125 mDependency.injectMockDependency(NotificationRemoteInputManager.class); 126 127 mReceiver = new BlockingQueueIntentReceiver(); 128 mContext.registerReceiver(mReceiver, new IntentFilter(TEST_ACTION), null, 129 Handler.createAsync(Dependency.get(Dependency.BG_LOOPER)), 130 Context.RECEIVER_EXPORTED_UNAUDITED); 131 132 // Avoid SecurityException RemoteInputView#sendRemoteInput(). 133 mContext.addMockSystemService(ShortcutManager.class, mShortcutManager); 134 } 135 136 @After tearDown()137 public void tearDown() { 138 mContext.unregisterReceiver(mReceiver); 139 } 140 setTestPendingIntent(RemoteInputViewController controller)141 private void setTestPendingIntent(RemoteInputViewController controller) { 142 PendingIntent pendingIntent = PendingIntent.getBroadcast(mContext, 0, 143 new Intent(TEST_ACTION), PendingIntent.FLAG_MUTABLE); 144 RemoteInput input = new RemoteInput.Builder(TEST_RESULT_KEY).build(); 145 RemoteInput[] inputs = {input}; 146 147 controller.setPendingIntent(pendingIntent); 148 controller.setRemoteInput(input); 149 controller.setRemoteInputs(inputs); 150 } 151 152 @Test testSendRemoteInput_intentContainsResultsAndSource()153 public void testSendRemoteInput_intentContainsResultsAndSource() throws Exception { 154 NotificationTestHelper helper = new NotificationTestHelper( 155 mContext, 156 mDependency, 157 TestableLooper.get(this)); 158 ExpandableNotificationRow row = helper.createRow(); 159 RemoteInputView view = RemoteInputView.inflate(mContext, null, row.getEntry(), mController); 160 RemoteInputViewController controller = bindController(view, row.getEntry()); 161 162 setTestPendingIntent(controller); 163 164 view.focus(); 165 166 EditText editText = view.findViewById(R.id.remote_input_text); 167 editText.setText(TEST_REPLY); 168 ImageButton sendButton = view.findViewById(R.id.remote_input_send); 169 sendButton.performClick(); 170 171 Intent resultIntent = mReceiver.waitForIntent(); 172 assertNotNull(resultIntent); 173 assertEquals(TEST_REPLY, 174 RemoteInput.getResultsFromIntent(resultIntent).get(TEST_RESULT_KEY)); 175 assertEquals(RemoteInput.SOURCE_FREE_FORM_INPUT, 176 RemoteInput.getResultsSource(resultIntent)); 177 } 178 getTargetInputMethodUser(UserHandle fromUser, UserHandle toUser)179 private UserHandle getTargetInputMethodUser(UserHandle fromUser, UserHandle toUser) 180 throws Exception { 181 /** 182 * RemoteInputView, Icon, and Bubble have the situation need to handle the other user. 183 * SystemUI cross multiple user but this test(com.android.systemui.tests) doesn't cross 184 * multiple user. It needs some of mocking multiple user environment to ensure the 185 * createContextAsUser without throwing IllegalStateException. 186 */ 187 Context contextSpy = spy(mContext); 188 doReturn(contextSpy).when(contextSpy).createContextAsUser(any(), anyInt()); 189 doReturn(toUser.getIdentifier()).when(contextSpy).getUserId(); 190 191 NotificationTestHelper helper = new NotificationTestHelper( 192 contextSpy, 193 mDependency, 194 TestableLooper.get(this)); 195 ExpandableNotificationRow row = helper.createRow( 196 DUMMY_MESSAGE_APP_PKG, 197 UserHandle.getUid(fromUser.getIdentifier(), DUMMY_MESSAGE_APP_ID), 198 toUser); 199 RemoteInputView view = RemoteInputView.inflate(mContext, null, row.getEntry(), mController); 200 RemoteInputViewController controller = bindController(view, row.getEntry()); 201 EditText editText = view.findViewById(R.id.remote_input_text); 202 203 setTestPendingIntent(controller); 204 assertThat(editText.isEnabled()).isFalse(); 205 view.onVisibilityAggregated(true); 206 assertThat(editText.isEnabled()).isTrue(); 207 208 view.focus(); 209 210 EditorInfo editorInfo = new EditorInfo(); 211 editorInfo.packageName = DUMMY_MESSAGE_APP_PKG; 212 editorInfo.fieldId = editText.getId(); 213 InputConnection ic = editText.onCreateInputConnection(editorInfo); 214 assertNotNull(ic); 215 return editorInfo.targetInputMethodUser; 216 } 217 218 @Test testEditorInfoTargetInputMethodUserForCallingUser()219 public void testEditorInfoTargetInputMethodUserForCallingUser() throws Exception { 220 UserHandle callingUser = Process.myUserHandle(); 221 assertEquals(callingUser, getTargetInputMethodUser(callingUser, callingUser)); 222 } 223 224 @Test testEditorInfoTargetInputMethodUserForDifferentUser()225 public void testEditorInfoTargetInputMethodUserForDifferentUser() throws Exception { 226 UserHandle differentUser = UserHandle.of(UserHandle.getCallingUserId() + 1); 227 assertEquals(differentUser, getTargetInputMethodUser(differentUser, differentUser)); 228 } 229 230 @Test testEditorInfoTargetInputMethodUserForAllUser()231 public void testEditorInfoTargetInputMethodUserForAllUser() throws Exception { 232 // For the special pseudo user UserHandle.ALL, EditorInfo#targetInputMethodUser must be 233 // resolved as the current user. 234 UserHandle callingUser = Process.myUserHandle(); 235 assertEquals(UserHandle.of(ActivityManager.getCurrentUser()), 236 getTargetInputMethodUser(callingUser, UserHandle.ALL)); 237 } 238 239 @Test testNoCrashWithoutVisibilityListener()240 public void testNoCrashWithoutVisibilityListener() throws Exception { 241 NotificationTestHelper helper = new NotificationTestHelper( 242 mContext, 243 mDependency, 244 TestableLooper.get(this)); 245 ExpandableNotificationRow row = helper.createRow(); 246 RemoteInputView view = RemoteInputView.inflate(mContext, null, row.getEntry(), mController); 247 248 view.addOnVisibilityChangedListener(null); 249 view.setVisibility(View.INVISIBLE); 250 view.setVisibility(View.VISIBLE); 251 } 252 253 @Test testPredictiveBack_registerAndUnregister()254 public void testPredictiveBack_registerAndUnregister() throws Exception { 255 NotificationTestHelper helper = new NotificationTestHelper( 256 mContext, 257 mDependency, 258 TestableLooper.get(this)); 259 ExpandableNotificationRow row = helper.createRow(); 260 RemoteInputView view = RemoteInputView.inflate(mContext, null, row.getEntry(), mController); 261 262 ViewRootImpl viewRoot = mock(ViewRootImpl.class); 263 WindowOnBackInvokedDispatcher backInvokedDispatcher = mock( 264 WindowOnBackInvokedDispatcher.class); 265 ArgumentCaptor<OnBackInvokedCallback> onBackInvokedCallbackCaptor = ArgumentCaptor.forClass( 266 OnBackInvokedCallback.class); 267 when(viewRoot.getOnBackInvokedDispatcher()).thenReturn(backInvokedDispatcher); 268 view.setViewRootImpl(viewRoot); 269 270 /* verify that predictive back callback registered when RemoteInputView becomes visible */ 271 view.onVisibilityAggregated(true); 272 verify(backInvokedDispatcher).registerOnBackInvokedCallback( 273 eq(OnBackInvokedDispatcher.PRIORITY_OVERLAY), 274 onBackInvokedCallbackCaptor.capture()); 275 276 /* verify that same callback unregistered when RemoteInputView becomes invisible */ 277 view.onVisibilityAggregated(false); 278 verify(backInvokedDispatcher).unregisterOnBackInvokedCallback( 279 eq(onBackInvokedCallbackCaptor.getValue())); 280 } 281 282 @Test testUiPredictiveBack_openAndDispatchCallback()283 public void testUiPredictiveBack_openAndDispatchCallback() throws Exception { 284 NotificationTestHelper helper = new NotificationTestHelper( 285 mContext, 286 mDependency, 287 TestableLooper.get(this)); 288 ExpandableNotificationRow row = helper.createRow(); 289 RemoteInputView view = RemoteInputView.inflate(mContext, null, row.getEntry(), mController); 290 ViewRootImpl viewRoot = mock(ViewRootImpl.class); 291 WindowOnBackInvokedDispatcher backInvokedDispatcher = mock( 292 WindowOnBackInvokedDispatcher.class); 293 ArgumentCaptor<OnBackInvokedCallback> onBackInvokedCallbackCaptor = ArgumentCaptor.forClass( 294 OnBackInvokedCallback.class); 295 when(viewRoot.getOnBackInvokedDispatcher()).thenReturn(backInvokedDispatcher); 296 view.setViewRootImpl(viewRoot); 297 view.onVisibilityAggregated(true); 298 view.setEditTextReferenceToSelf(); 299 300 /* capture the callback during registration */ 301 verify(backInvokedDispatcher).registerOnBackInvokedCallback( 302 eq(OnBackInvokedDispatcher.PRIORITY_OVERLAY), 303 onBackInvokedCallbackCaptor.capture()); 304 305 view.focus(); 306 307 /* invoke the captured callback */ 308 onBackInvokedCallbackCaptor.getValue().onBackInvoked(); 309 310 /* wait for RemoteInputView disappear animation to finish */ 311 mAnimatorTestRule.advanceTimeBy(StackStateAnimator.ANIMATION_DURATION_STANDARD); 312 313 /* verify that the RemoteInputView goes away */ 314 assertEquals(view.getVisibility(), View.GONE); 315 } 316 317 @Test testUiEventLogging_openAndSend()318 public void testUiEventLogging_openAndSend() throws Exception { 319 NotificationTestHelper helper = new NotificationTestHelper( 320 mContext, 321 mDependency, 322 TestableLooper.get(this)); 323 ExpandableNotificationRow row = helper.createRow(); 324 RemoteInputView view = RemoteInputView.inflate(mContext, null, row.getEntry(), mController); 325 RemoteInputViewController controller = bindController(view, row.getEntry()); 326 327 setTestPendingIntent(controller); 328 329 // Open view, send a reply 330 view.focus(); 331 EditText editText = view.findViewById(R.id.remote_input_text); 332 editText.setText(TEST_REPLY); 333 ImageButton sendButton = view.findViewById(R.id.remote_input_send); 334 sendButton.performClick(); 335 336 mReceiver.waitForIntent(); 337 338 assertEquals(2, mUiEventLoggerFake.numLogs()); 339 assertEquals( 340 RemoteInputView.NotificationRemoteInputEvent.NOTIFICATION_REMOTE_INPUT_OPEN.getId(), 341 mUiEventLoggerFake.eventId(0)); 342 assertEquals( 343 RemoteInputView.NotificationRemoteInputEvent.NOTIFICATION_REMOTE_INPUT_SEND.getId(), 344 mUiEventLoggerFake.eventId(1)); 345 } 346 347 @Test testUiEventLogging_openAndAttach()348 public void testUiEventLogging_openAndAttach() throws Exception { 349 NotificationTestHelper helper = new NotificationTestHelper( 350 mContext, 351 mDependency, 352 TestableLooper.get(this)); 353 ExpandableNotificationRow row = helper.createRow(); 354 RemoteInputView view = RemoteInputView.inflate(mContext, null, row.getEntry(), mController); 355 RemoteInputViewController controller = bindController(view, row.getEntry()); 356 357 setTestPendingIntent(controller); 358 359 // Open view, attach an image 360 view.focus(); 361 EditText editText = view.findViewById(R.id.remote_input_text); 362 editText.setText(TEST_REPLY); 363 ClipDescription description = new ClipDescription("", new String[] {"image/png"}); 364 // We need to use an (arbitrary) real resource here so that an actual image gets attached 365 ClipData clip = new ClipData(description, new ClipData.Item( 366 Uri.parse("android.resource://android/" + android.R.drawable.btn_default))); 367 ContentInfo payload = 368 new ContentInfo.Builder(clip, SOURCE_CLIPBOARD).build(); 369 view.setAttachment(payload); 370 mReceiver.waitForIntent(); 371 372 assertEquals(2, mUiEventLoggerFake.numLogs()); 373 assertEquals( 374 RemoteInputView.NotificationRemoteInputEvent.NOTIFICATION_REMOTE_INPUT_OPEN.getId(), 375 mUiEventLoggerFake.eventId(0)); 376 assertEquals( 377 RemoteInputView.NotificationRemoteInputEvent 378 .NOTIFICATION_REMOTE_INPUT_ATTACH_IMAGE.getId(), 379 mUiEventLoggerFake.eventId(1)); 380 } 381 382 @Test testFocusAnimation()383 public void testFocusAnimation() throws Exception { 384 NotificationTestHelper helper = new NotificationTestHelper( 385 mContext, 386 mDependency, 387 TestableLooper.get(this)); 388 ExpandableNotificationRow row = helper.createRow(); 389 RemoteInputView view = RemoteInputView.inflate(mContext, null, row.getEntry(), mController); 390 bindController(view, row.getEntry()); 391 view.setVisibility(View.GONE); 392 393 View fadeOutView = new View(mContext); 394 fadeOutView.setId(com.android.internal.R.id.actions_container_layout); 395 396 FrameLayout parent = new FrameLayout(mContext); 397 parent.addView(view); 398 parent.addView(fadeOutView); 399 400 // Start focus animation 401 view.focusAnimated(); 402 assertTrue(view.isAnimatingAppearance()); 403 404 // fast forward to 1 ms before end of animation and verify fadeOutView has alpha set to 0f 405 mAnimatorTestRule.advanceTimeBy(ANIMATION_DURATION_STANDARD - 1); 406 assertEquals(0f, fadeOutView.getAlpha()); 407 408 // fast forward to end of animation 409 mAnimatorTestRule.advanceTimeBy(1); 410 411 // assert that fadeOutView's alpha is reset to 1f after the animation (hidden behind 412 // RemoteInputView) 413 assertEquals(1f, fadeOutView.getAlpha()); 414 assertFalse(view.isAnimatingAppearance()); 415 assertEquals(View.VISIBLE, view.getVisibility()); 416 assertEquals(1f, view.getAlpha()); 417 } 418 419 @Test testDefocusAnimation()420 public void testDefocusAnimation() throws Exception { 421 NotificationTestHelper helper = new NotificationTestHelper( 422 mContext, 423 mDependency, 424 TestableLooper.get(this)); 425 ExpandableNotificationRow row = helper.createRow(); 426 RemoteInputView view = RemoteInputView.inflate(mContext, null, row.getEntry(), mController); 427 bindController(view, row.getEntry()); 428 429 View fadeInView = new View(mContext); 430 fadeInView.setId(com.android.internal.R.id.actions_container_layout); 431 432 FrameLayout parent = new FrameLayout(mContext); 433 parent.addView(view); 434 parent.addView(fadeInView); 435 436 // Start defocus animation 437 view.onDefocus(true /* animate */, false /* logClose */, null /* doAfterDefocus */); 438 assertEquals(View.VISIBLE, view.getVisibility()); 439 assertEquals(0f, fadeInView.getAlpha()); 440 441 // fast forward to end of animation 442 mAnimatorTestRule.advanceTimeBy(ANIMATION_DURATION_STANDARD); 443 444 // assert that RemoteInputView is no longer visible 445 assertEquals(View.GONE, view.getVisibility()); 446 assertEquals(1f, fadeInView.getAlpha()); 447 } 448 449 // NOTE: because we're refactoring the RemoteInputView and moving logic into the 450 // RemoteInputViewController, it's easiest to just test the system of the two classes together. 451 @NonNull bindController( RemoteInputView view, NotificationEntry entry)452 private RemoteInputViewController bindController( 453 RemoteInputView view, 454 NotificationEntry entry) { 455 FakeFeatureFlags fakeFeatureFlags = new FakeFeatureFlags(); 456 fakeFeatureFlags.set(Flags.NOTIFICATION_INLINE_REPLY_ANIMATION, true); 457 RemoteInputViewControllerImpl viewController = new RemoteInputViewControllerImpl( 458 view, 459 entry, 460 mRemoteInputQuickSettingsDisabler, 461 mController, 462 mShortcutManager, 463 mUiEventLoggerFake, 464 fakeFeatureFlags 465 ); 466 viewController.bind(); 467 return viewController; 468 } 469 } 470