• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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