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 junit.framework.Assert.assertEquals;
18 import static junit.framework.Assert.assertNotNull;
19 
20 import static org.mockito.ArgumentMatchers.any;
21 import static org.mockito.ArgumentMatchers.anyInt;
22 import static org.mockito.Mockito.doReturn;
23 import static org.mockito.Mockito.spy;
24 
25 import android.app.ActivityManager;
26 import android.app.PendingIntent;
27 import android.app.RemoteInput;
28 import android.content.Context;
29 import android.content.Intent;
30 import android.content.IntentFilter;
31 import android.content.pm.ShortcutManager;
32 import android.os.Handler;
33 import android.os.Process;
34 import android.os.UserHandle;
35 import android.testing.AndroidTestingRunner;
36 import android.testing.TestableLooper;
37 import android.view.View;
38 import android.view.inputmethod.EditorInfo;
39 import android.view.inputmethod.InputConnection;
40 import android.widget.EditText;
41 import android.widget.ImageButton;
42 
43 import androidx.test.filters.SmallTest;
44 
45 import com.android.internal.logging.UiEventLogger;
46 import com.android.internal.logging.testing.UiEventLoggerFake;
47 import com.android.systemui.Dependency;
48 import com.android.systemui.R;
49 import com.android.systemui.SysuiTestCase;
50 import com.android.systemui.statusbar.NotificationRemoteInputManager;
51 import com.android.systemui.statusbar.RemoteInputController;
52 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
53 import com.android.systemui.statusbar.notification.row.NotificationTestHelper;
54 import com.android.systemui.statusbar.phone.LightBarController;
55 
56 import org.junit.After;
57 import org.junit.Before;
58 import org.junit.Test;
59 import org.junit.runner.RunWith;
60 import org.mockito.Mock;
61 import org.mockito.MockitoAnnotations;
62 
63 @RunWith(AndroidTestingRunner.class)
64 @TestableLooper.RunWithLooper
65 @SmallTest
66 public class RemoteInputViewTest extends SysuiTestCase {
67 
68     private static final String TEST_RESULT_KEY = "test_result_key";
69     private static final String TEST_REPLY = "hello";
70     private static final String TEST_ACTION = "com.android.REMOTE_INPUT_VIEW_ACTION";
71 
72     private static final String DUMMY_MESSAGE_APP_PKG =
73             "com.android.sysuitest.dummynotificationsender";
74     private static final int DUMMY_MESSAGE_APP_ID = Process.LAST_APPLICATION_UID - 1;
75 
76     @Mock private RemoteInputController mController;
77     @Mock private ShortcutManager mShortcutManager;
78     @Mock private RemoteInputQuickSettingsDisabler mRemoteInputQuickSettingsDisabler;
79     @Mock private LightBarController mLightBarController;
80     private BlockingQueueIntentReceiver mReceiver;
81     private final UiEventLoggerFake mUiEventLoggerFake = new UiEventLoggerFake();
82     private RemoteInputView mView;
83 
84     @Before
setUp()85     public void setUp() throws Exception {
86         allowTestableLooperAsMainThread();
87         MockitoAnnotations.initMocks(this);
88 
89         mDependency.injectTestDependency(RemoteInputQuickSettingsDisabler.class,
90                 mRemoteInputQuickSettingsDisabler);
91         mDependency.injectTestDependency(LightBarController.class,
92                 mLightBarController);
93         mDependency.injectTestDependency(UiEventLogger.class, mUiEventLoggerFake);
94         mDependency.injectMockDependency(NotificationRemoteInputManager.class);
95 
96         mReceiver = new BlockingQueueIntentReceiver();
97         mContext.registerReceiver(mReceiver, new IntentFilter(TEST_ACTION), null,
98                 Handler.createAsync(Dependency.get(Dependency.BG_LOOPER)));
99 
100         // Avoid SecurityException RemoteInputView#sendRemoteInput().
101         mContext.addMockSystemService(ShortcutManager.class, mShortcutManager);
102     }
103 
104     @After
tearDown()105     public void tearDown() {
106         mContext.unregisterReceiver(mReceiver);
107     }
108 
setTestPendingIntent(RemoteInputView view)109     private void setTestPendingIntent(RemoteInputView view) {
110         PendingIntent pendingIntent = PendingIntent.getBroadcast(mContext, 0,
111                 new Intent(TEST_ACTION), PendingIntent.FLAG_MUTABLE);
112         RemoteInput input = new RemoteInput.Builder(TEST_RESULT_KEY).build();
113 
114         view.setPendingIntent(pendingIntent);
115         view.setRemoteInput(new RemoteInput[]{input}, input, null /* editedSuggestionInfo */);
116     }
117 
118     @Test
testSendRemoteInput_intentContainsResultsAndSource()119     public void testSendRemoteInput_intentContainsResultsAndSource() throws Exception {
120         NotificationTestHelper helper = new NotificationTestHelper(
121                 mContext,
122                 mDependency,
123                 TestableLooper.get(this));
124         ExpandableNotificationRow row = helper.createRow();
125         RemoteInputView view = RemoteInputView.inflate(mContext, null, row.getEntry(), mController);
126 
127         setTestPendingIntent(view);
128 
129         view.focus();
130 
131         EditText editText = view.findViewById(R.id.remote_input_text);
132         editText.setText(TEST_REPLY);
133         ImageButton sendButton = view.findViewById(R.id.remote_input_send);
134         sendButton.performClick();
135 
136         Intent resultIntent = mReceiver.waitForIntent();
137         assertEquals(TEST_REPLY,
138                 RemoteInput.getResultsFromIntent(resultIntent).get(TEST_RESULT_KEY));
139         assertEquals(RemoteInput.SOURCE_FREE_FORM_INPUT,
140                 RemoteInput.getResultsSource(resultIntent));
141     }
142 
getTargetInputMethodUser(UserHandle fromUser, UserHandle toUser)143     private UserHandle getTargetInputMethodUser(UserHandle fromUser, UserHandle toUser)
144             throws Exception {
145         /**
146          * RemoteInputView, Icon, and Bubble have the situation need to handle the other user.
147          * SystemUI cross multiple user but this test(com.android.systemui.tests) doesn't cross
148          * multiple user. It needs some of mocking multiple user environment to ensure the
149          * createContextAsUser without throwing IllegalStateException.
150          */
151         Context contextSpy = spy(mContext);
152         doReturn(contextSpy).when(contextSpy).createContextAsUser(any(), anyInt());
153         doReturn(toUser.getIdentifier()).when(contextSpy).getUserId();
154 
155         NotificationTestHelper helper = new NotificationTestHelper(
156                 contextSpy,
157                 mDependency,
158                 TestableLooper.get(this));
159         ExpandableNotificationRow row = helper.createRow(
160                 DUMMY_MESSAGE_APP_PKG,
161                 UserHandle.getUid(fromUser.getIdentifier(), DUMMY_MESSAGE_APP_ID),
162                 toUser);
163         RemoteInputView view = RemoteInputView.inflate(mContext, null, row.getEntry(), mController);
164 
165         setTestPendingIntent(view);
166 
167         view.focus();
168 
169         EditText editText = view.findViewById(R.id.remote_input_text);
170         EditorInfo editorInfo = new EditorInfo();
171         editorInfo.packageName = DUMMY_MESSAGE_APP_PKG;
172         editorInfo.fieldId = editText.getId();
173         InputConnection ic = editText.onCreateInputConnection(editorInfo);
174         assertNotNull(ic);
175         return editorInfo.targetInputMethodUser;
176     }
177 
178     @Test
testEditorInfoTargetInputMethodUserForCallingUser()179     public void testEditorInfoTargetInputMethodUserForCallingUser() throws Exception {
180         UserHandle callingUser = Process.myUserHandle();
181         assertEquals(callingUser, getTargetInputMethodUser(callingUser, callingUser));
182     }
183 
184     @Test
testEditorInfoTargetInputMethodUserForDifferentUser()185     public void testEditorInfoTargetInputMethodUserForDifferentUser() throws Exception {
186         UserHandle differentUser = UserHandle.of(UserHandle.getCallingUserId() + 1);
187         assertEquals(differentUser, getTargetInputMethodUser(differentUser, differentUser));
188     }
189 
190     @Test
testEditorInfoTargetInputMethodUserForAllUser()191     public void testEditorInfoTargetInputMethodUserForAllUser() throws Exception {
192         // For the special pseudo user UserHandle.ALL, EditorInfo#targetInputMethodUser must be
193         // resolved as the current user.
194         UserHandle callingUser = Process.myUserHandle();
195         assertEquals(UserHandle.of(ActivityManager.getCurrentUser()),
196                 getTargetInputMethodUser(callingUser, UserHandle.ALL));
197     }
198 
199     @Test
testNoCrashWithoutVisibilityListener()200     public void testNoCrashWithoutVisibilityListener() throws Exception {
201         NotificationTestHelper helper = new NotificationTestHelper(
202                 mContext,
203                 mDependency,
204                 TestableLooper.get(this));
205         ExpandableNotificationRow row = helper.createRow();
206         RemoteInputView view = RemoteInputView.inflate(mContext, null, row.getEntry(), mController);
207 
208         view.addOnVisibilityChangedListener(null);
209         view.setVisibility(View.INVISIBLE);
210         view.setVisibility(View.VISIBLE);
211     }
212 
213     @Test
testUiEventLogging_openAndSend()214     public void testUiEventLogging_openAndSend() throws Exception {
215         NotificationTestHelper helper = new NotificationTestHelper(
216                 mContext,
217                 mDependency,
218                 TestableLooper.get(this));
219         ExpandableNotificationRow row = helper.createRow();
220         RemoteInputView view = RemoteInputView.inflate(mContext, null, row.getEntry(), mController);
221 
222         setTestPendingIntent(view);
223 
224         // Open view, send a reply
225         view.focus();
226         EditText editText = view.findViewById(R.id.remote_input_text);
227         editText.setText(TEST_REPLY);
228         ImageButton sendButton = view.findViewById(R.id.remote_input_send);
229         sendButton.performClick();
230 
231         mReceiver.waitForIntent();
232 
233         assertEquals(2, mUiEventLoggerFake.numLogs());
234         assertEquals(
235                 RemoteInputView.NotificationRemoteInputEvent.NOTIFICATION_REMOTE_INPUT_OPEN.getId(),
236                 mUiEventLoggerFake.eventId(0));
237         assertEquals(
238                 RemoteInputView.NotificationRemoteInputEvent.NOTIFICATION_REMOTE_INPUT_SEND.getId(),
239                 mUiEventLoggerFake.eventId(1));
240     }
241 }
242