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