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