1 /*
2  * Copyright (C) 2022 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package android.view.inputmethod.cts;
18 
19 import static android.view.WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE;
20 import static android.view.WindowManager.LayoutParams.SOFT_INPUT_STATE_UNCHANGED;
21 import static android.view.inputmethod.cts.util.InputMethodVisibilityVerifier.expectImeInvisible;
22 import static android.view.inputmethod.cts.util.InputMethodVisibilityVerifier.expectImeVisible;
23 
24 import static com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity;
25 
26 import static com.google.common.truth.Truth.assertThat;
27 import static com.google.common.truth.Truth.assertWithMessage;
28 
29 import static org.junit.Assert.fail;
30 
31 import android.app.Activity;
32 import android.app.Instrumentation;
33 import android.platform.test.annotations.AppModeSdkSandbox;
34 import android.platform.test.annotations.RequiresFlagsDisabled;
35 import android.platform.test.flag.junit.CheckFlagsRule;
36 import android.platform.test.flag.junit.DeviceFlagsValueProvider;
37 import android.support.test.uiautomator.UiDevice;
38 import android.view.MotionEvent;
39 import android.view.WindowInsets;
40 import android.view.WindowInsetsController.OnControllableInsetsChangedListener;
41 import android.view.WindowManager;
42 import android.view.inputmethod.Flags;
43 import android.view.inputmethod.InputMethodManager;
44 import android.view.inputmethod.cts.util.EndToEndImeTestBase;
45 import android.view.inputmethod.cts.util.MetricsRecorder;
46 import android.view.inputmethod.cts.util.TestActivity;
47 import android.view.inputmethod.cts.util.TestUtils;
48 import android.view.inputmethod.nano.ImeProtoEnums;
49 import android.widget.EditText;
50 import android.widget.LinearLayout;
51 import android.widget.TextView;
52 
53 import androidx.annotation.NonNull;
54 import androidx.test.ext.junit.runners.AndroidJUnit4;
55 import androidx.test.platform.app.InstrumentationRegistry;
56 
57 import com.android.compatibility.common.util.PollingCheck;
58 import com.android.cts.mockime.ImeSettings;
59 import com.android.cts.mockime.MockImeSession;
60 import com.android.os.nano.AtomsProto;
61 
62 import org.junit.After;
63 import org.junit.Before;
64 import org.junit.Rule;
65 import org.junit.Test;
66 import org.junit.runner.RunWith;
67 
68 import java.util.List;
69 import java.util.concurrent.CountDownLatch;
70 import java.util.concurrent.TimeUnit;
71 
72 /**
73  * Test suite to ensure IME stats get tracked and logged correctly.
74  */
75 @RunWith(AndroidJUnit4.class)
76 @AppModeSdkSandbox(reason = "Allow test in the SDK sandbox (does not prevent other modes).")
77 public class InputMethodStatsTest extends EndToEndImeTestBase {
78 
79     private static final String TAG = "InputMethodStatsTest";
80 
81     @Rule
82     public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
83 
84     private static final int EDIT_TEXT_ID = 1;
85     private static final int TEXT_VIEW_ID = 2;
86 
87     private static final long TIMEOUT = TimeUnit.SECONDS.toMillis(5);
88 
89     /** Time to wait for statsd to setup. */
90     private static final long WAIT_TIME_LONG = 1000;
91 
92     /** Time to wait for atoms to be reported. */
93     private static final long WAIT_TIME_SHORT = 500;
94 
95     private Instrumentation mInstrumentation;
96 
97     /** The test app package name from which atoms will be logged. */
98     private String mPkgName;
99 
100     @Before
setUp()101     public void setUp() throws Exception {
102         mInstrumentation = InstrumentationRegistry.getInstrumentation();
103         mPkgName = mInstrumentation.getContext().getPackageName();
104 
105         // Finish tracking any pending IME visibility requests from previous tests to avoid issues.
106         final var imm = mInstrumentation.getContext().getSystemService(InputMethodManager.class);
107         runWithShellPermissionIdentity(() -> {
108             imm.finishTrackingPendingImeVisibilityRequests();
109         });
110 
111         MetricsRecorder.removeConfig();
112         MetricsRecorder.clearReports();
113         // TODO(b/330143218): Add a proper fence for statsd
114         Thread.sleep(WAIT_TIME_LONG);
115     }
116 
117     @After
tearDown()118     public void tearDown() throws Exception {
119         MetricsRecorder.removeConfig();
120         MetricsRecorder.clearReports();
121 
122         mInstrumentation = null;
123         mPkgName = "";
124     }
125 
126     /**
127      * Creates and launches a test activity.
128      *
129      * @param mode the {@link WindowManager.LayoutParams#softInputMode softInputMode} for the
130      *             activity.
131      *
132      * @return the created activity.
133      */
createTestActivity(final int mode)134     private TestActivity createTestActivity(final int mode) {
135         return TestActivity.startSync(activity -> createLayout(mode, activity));
136     }
137 
138     /**
139      * Creates a linear layout with one EditText.
140      *
141      * @param mode     the {@link WindowManager.LayoutParams#softInputMode softInputMode} for the
142      *                 activity.
143      * @param activity the activity to create the layout for.
144      *
145      * @return the created layout.
146      */
createLayout(final int mode, final Activity activity)147     private LinearLayout createLayout(final int mode, final Activity activity) {
148         final var layout = new LinearLayout(activity);
149         layout.setOrientation(LinearLayout.VERTICAL);
150 
151         final var editText = new EditText(activity);
152         editText.setId(EDIT_TEXT_ID);
153         editText.setText("Editable");
154 
155         final var textView = new TextView(activity);
156         textView.setId(TEXT_VIEW_ID);
157         textView.setText("Not Editable");
158 
159         layout.addView(editText);
160         layout.addView(textView);
161         editText.requestFocus();
162         activity.getWindow().setSoftInputMode(mode);
163         return layout;
164     }
165 
166     /**
167      * Waits for the given inset type to be controllable on the given activity's
168      * {@link android.view.WindowInsetsController}.
169      *
170      * @param type     the inset type waiting to be controllable.
171      * @param activity the activity whose Window Insets Controller to wait on.
172      *
173      * @implNote This is used to avoid the case where
174      * {@link android.view.InsetsController#show(int)}
175      * is called before IME insets control is available, starting a more complex flow which is
176      * currently harder to track with the {@link com.android.server.inputmethod.ImeTrackerService}
177      * system.
178      *
179      * TODO(b/263069667): Remove this method when the ImeInsetsSourceConsumer show flow is fixed.
180      */
awaitControl(final int type, final Activity activity)181     private void awaitControl(final int type, final Activity activity) {
182         final var latch = new CountDownLatch(1);
183         final OnControllableInsetsChangedListener listener = (controller, typeMask) -> {
184             if ((typeMask & type) != 0) {
185                 latch.countDown();
186             }
187         };
188         TestUtils.runOnMainSync(() -> activity.getWindow()
189                 .getDecorView()
190                 .getWindowInsetsController()
191                 .addOnControllableInsetsChangedListener(listener));
192 
193         try {
194             if (!latch.await(TIMEOUT, TimeUnit.SECONDS)) {
195                 fail("IME insets controls not available");
196             }
197         } catch (InterruptedException e) {
198             fail("Waiting for IMe insets controls to be available failed");
199         } finally {
200             TestUtils.runOnMainSync(() -> activity.getWindow()
201                     .getDecorView()
202                     .getWindowInsetsController()
203                     .removeOnControllableInsetsChangedListener(listener));
204         }
205     }
206 
207     /**
208      * Test the logging for an IME show request from the client.
209      */
210     @Test
211     @RequiresFlagsDisabled(Flags.FLAG_REFACTOR_INSETS_CONTROLLER)
testClientShowImeRequestFinished()212     public void testClientShowImeRequestFinished() throws Throwable {
213         verifyLogging(true /* show */,
214                 List.of(ImeProtoEnums.ORIGIN_CLIENT, ImeProtoEnums.ORIGIN_CLIENT_SHOW_SOFT_INPUT),
215                 false /* fromUser */, (imeSession, activity) -> {
216                     awaitControl(WindowInsets.Type.ime(), activity);
217                     expectImeInvisible(TIMEOUT);
218 
219                     TestUtils.runOnMainSync(() -> activity.getWindow()
220                             .getDecorView()
221                             .getWindowInsetsController()
222                             .show(WindowInsets.Type.ime()));
223 
224                     expectImeVisible(TIMEOUT);
225                 });
226     }
227 
228     /**
229      * Test the logging for an IME hide request from the client.
230      */
231     @Test
232     @RequiresFlagsDisabled(Flags.FLAG_REFACTOR_INSETS_CONTROLLER)
testClientHideImeRequestFinished()233     public void testClientHideImeRequestFinished() throws Exception {
234         verifyLogging(false /* show */,
235                 List.of(ImeProtoEnums.ORIGIN_CLIENT, ImeProtoEnums.ORIGIN_CLIENT_HIDE_SOFT_INPUT),
236                 false /* fromUser */, (imeSession, activity) -> {
237                     TestUtils.runOnMainSync(() -> activity.getWindow()
238                             .getDecorView()
239                             .getWindowInsetsController()
240                             .hide(WindowInsets.Type.ime()));
241 
242                     expectImeInvisible(TIMEOUT);
243                 });
244     }
245 
246     /**
247      * Test the logging for an IME show request from the server.
248      */
249     @Test
250     @RequiresFlagsDisabled(Flags.FLAG_REFACTOR_INSETS_CONTROLLER)
testServerShowImeRequestFinished()251     public void testServerShowImeRequestFinished() throws Exception {
252         verifyLogging(true /* show */,
253                 List.of(ImeProtoEnums.ORIGIN_SERVER, ImeProtoEnums.ORIGIN_SERVER_START_INPUT),
254                 false /* fromUser */, (imeSession, activity) -> {
255                     createTestActivity(SOFT_INPUT_STATE_ALWAYS_VISIBLE);
256 
257                     expectImeVisible(TIMEOUT);
258                 });
259     }
260 
261     /**
262      * Test the logging for an IME hide request from the server.
263      */
264     @Test
265     @RequiresFlagsDisabled(Flags.FLAG_REFACTOR_INSETS_CONTROLLER)
testServerHideImeRequestFinished()266     public void testServerHideImeRequestFinished() throws Exception {
267         verifyLogging(false /* show */,
268                 List.of(ImeProtoEnums.ORIGIN_SERVER, ImeProtoEnums.ORIGIN_SERVER_HIDE_INPUT),
269                 false /* fromUser */, (imeSession, activity) -> {
270                     imeSession.hideSoftInputFromServerForTest();
271 
272                     expectImeInvisible(TIMEOUT);
273                 });
274     }
275 
276     /**
277      * Test the logging for an IME show request from the IME.
278      */
279     @Test
280     @RequiresFlagsDisabled(Flags.FLAG_REFACTOR_INSETS_CONTROLLER)
testImeShowImeRequestFinished()281     public void testImeShowImeRequestFinished() throws Exception {
282         // In the past, the origin of this request was considered in the server.
283         verifyLogging(true /* show */,
284                 List.of(ImeProtoEnums.ORIGIN_IME, ImeProtoEnums.ORIGIN_SERVER_START_INPUT),
285                 false /* fromUser */, (imeSession, activity) -> {
286                     imeSession.callRequestShowSelf(0 /* flags */);
287 
288                     expectImeVisible(TIMEOUT);
289                 });
290 
291     }
292 
293     /**
294      * Test the logging for an IME hide request from the IME.
295      */
296     @Test
297     @RequiresFlagsDisabled(Flags.FLAG_REFACTOR_INSETS_CONTROLLER)
testImeHideImeRequestFinished()298     public void testImeHideImeRequestFinished() throws Exception {
299         verifyLogging(false /* show */,
300                 List.of(ImeProtoEnums.ORIGIN_IME, ImeProtoEnums.ORIGIN_SERVER_HIDE_INPUT),
301                 false /* fromUser */, (imeSession, activity) -> {
302                     imeSession.callRequestHideSelf(0 /* flags */);
303 
304                     expectImeInvisible(TIMEOUT);
305                 });
306     }
307 
308     /**
309      * Test the logging for an IME show request from a user interaction using InputMethodManager.
310      */
311     @Test
312     @RequiresFlagsDisabled(Flags.FLAG_REFACTOR_INSETS_CONTROLLER)
testFromUser_withImm_showImeRequestFinished()313     public void testFromUser_withImm_showImeRequestFinished() throws Exception {
314         verifyLogging(true /* show */,
315                 List.of(ImeProtoEnums.ORIGIN_CLIENT, ImeProtoEnums.ORIGIN_CLIENT_SHOW_SOFT_INPUT),
316                 true /* fromUser */, (imeSession, activity) -> {
317                     final EditText editText = activity.requireViewById(EDIT_TEXT_ID);
318                     editText.setShowSoftInputOnFocus(false);
319                     // onClickListener is run later, so ViewRootImpl#isHandlingPointeEvent will
320                     // be false. onTouchListener runs immediately, so the value will be true.
321                     editText.setOnTouchListener((v, ev) -> {
322                         // Three motion events are sent, only react to one of them.
323                         if (ev.getAction() != MotionEvent.ACTION_DOWN) {
324                             return false;
325                         }
326                         editText.getContext().getSystemService(InputMethodManager.class)
327                                 .showSoftInput(editText, 0 /* flags */);
328                         return true;
329                     });
330                     mCtsTouchUtils.emulateTapOnViewCenter(mInstrumentation, null, editText);
331 
332                     expectImeVisible(TIMEOUT);
333                 });
334     }
335 
336     /**
337      * Test the logging for an IME hide request from a user interaction using InputMethodManager.
338      */
339     @Test
340     @RequiresFlagsDisabled(Flags.FLAG_REFACTOR_INSETS_CONTROLLER)
testFromUser_withImm_hideImeRequestFinished()341     public void testFromUser_withImm_hideImeRequestFinished() throws Exception {
342         verifyLogging(false /* show */,
343                 List.of(ImeProtoEnums.ORIGIN_CLIENT, ImeProtoEnums.ORIGIN_CLIENT_HIDE_SOFT_INPUT),
344                 true /* formUser */, (imeSession, activity) -> {
345                     final TextView textView = activity.requireViewById(TEXT_VIEW_ID);
346                     // onClickListener is run later, so ViewRootImpl#isHandlingPointeEvent will
347                     // be false. onTouchListener runs immediately, so the value will be true.
348                     textView.setOnTouchListener((v, ev) -> {
349                         // Three motion events are sent, only react to one of them.
350                         if (ev.getActionMasked() != MotionEvent.ACTION_DOWN) {
351                             return false;
352                         }
353                         textView.getContext().getSystemService(InputMethodManager.class)
354                                 .hideSoftInputFromWindow(textView.getWindowToken(), 0 /* flags */);
355                         return true;
356                     });
357                     mCtsTouchUtils.emulateTapOnViewCenter(mInstrumentation, null, textView);
358 
359                     expectImeInvisible(TIMEOUT);
360                 });
361     }
362 
363     /**
364      * Test the logging for an IME show request from a user interaction using
365      * WindowInsetsController.
366      */
367     @Test
368     @RequiresFlagsDisabled(Flags.FLAG_REFACTOR_INSETS_CONTROLLER)
testFromUser_withWic_showImeRequestFinished()369     public void testFromUser_withWic_showImeRequestFinished() throws Exception {
370         verifyLogging(true /* show */,
371                 List.of(ImeProtoEnums.ORIGIN_CLIENT, ImeProtoEnums.ORIGIN_CLIENT_SHOW_SOFT_INPUT),
372                 true /* fromUser */, (imeSession, activity) -> {
373                     final EditText editText = activity.requireViewById(EDIT_TEXT_ID);
374                     editText.setShowSoftInputOnFocus(false);
375                     // onClickListener is run later, so ViewRootImpl#isHandlingPointeEvent will
376                     // be false. onTouchListener runs immediately, so the value will be true.
377                     editText.setOnTouchListener((v, ev) -> {
378                         // Three motion events are sent, only react to one of them.
379                         if (ev.getActionMasked() != MotionEvent.ACTION_DOWN) {
380                             return false;
381                         }
382                         activity.getWindow().getInsetsController().show(WindowInsets.Type.ime());
383                         return true;
384                     });
385                     mCtsTouchUtils.emulateTapOnViewCenter(mInstrumentation, null, editText);
386 
387                     expectImeVisible(TIMEOUT);
388                 });
389     }
390 
391     /**
392      * Test the logging for an IME hide request from a user interaction using
393      * WindowInsetsController.
394      */
395     @Test
396     @RequiresFlagsDisabled(Flags.FLAG_REFACTOR_INSETS_CONTROLLER)
testFromUser_withWic_hideImeRequestFinished()397     public void testFromUser_withWic_hideImeRequestFinished() throws Exception {
398         verifyLogging(false /* show */,
399                 List.of(ImeProtoEnums.ORIGIN_CLIENT, ImeProtoEnums.ORIGIN_CLIENT_HIDE_SOFT_INPUT),
400                 true /* fromUser */, (imeSession, activity) -> {
401                     final TextView textView = activity.requireViewById(TEXT_VIEW_ID);
402                     // onClickListener is run later, so ViewRootImpl#isHandlingPointeEvent will
403                     // be false. onTouchListener runs immediately, so the value will be true.
404                     textView.setOnTouchListener((v, ev) -> {
405                         // Three motion events are sent, only react to one of them.
406                         if (ev.getActionMasked() != MotionEvent.ACTION_DOWN) {
407                             return false;
408                         }
409                         activity.getWindow().getInsetsController().hide(WindowInsets.Type.ime());
410                         return true;
411                     });
412                     mCtsTouchUtils.emulateTapOnViewCenter(mInstrumentation, null, textView);
413 
414                     expectImeInvisible(TIMEOUT);
415                 });
416     }
417 
418     /**
419      * Test the logging for an IME hide request from a user interaction using back button press.
420      */
421     @Test
422     @RequiresFlagsDisabled(Flags.FLAG_REFACTOR_INSETS_CONTROLLER)
testFromUser_withBackPress_hideImeRequestFinished()423     public void testFromUser_withBackPress_hideImeRequestFinished() throws Exception {
424         verifyLogging(false /* show */,
425                 List.of(ImeProtoEnums.ORIGIN_IME, ImeProtoEnums.ORIGIN_SERVER_HIDE_INPUT),
426                 true /* fromUser */, (imeSession, activity) -> {
427                     UiDevice.getInstance(mInstrumentation)
428                             .pressBack();
429 
430                     expectImeInvisible(TIMEOUT);
431                 });
432     }
433 
434     /**
435      * Verifies the logged atom events for the given test runnable and expected values.
436      *
437      * @param show           whether this is testing a show request (starts with IME hidden),
438      *                       or hide request (starts with IME shown).
439      * @param origins        the expected IME request origins. This is a list of possible origins,
440      *                       to also allow previously deprecated ones.
441      * @param fromUser       whether this request is expected to be created from user interaction.
442      * @param runnable       the runnable with the test code to execute.
443      */
verifyLogging(boolean show, @NonNull List<Integer> origins, boolean fromUser, @NonNull TestRunnable runnable)444     private void verifyLogging(boolean show, @NonNull List<Integer> origins, boolean fromUser,
445             @NonNull TestRunnable runnable) throws Exception {
446         // Create mockImeSession to decouple from real IMEs,
447         // and enable calling expectImeVisible and expectImeInvisible.
448         try (var imeSession = MockImeSession.create(
449                 mInstrumentation.getContext(),
450                 mInstrumentation.getUiAutomation(),
451                 new ImeSettings.Builder())) {
452             // Wait for any outstanding IME requests to finish, to not interfere with test.
453             PollingCheck.waitFor(() -> !imeSession.hasPendingImeVisibilityRequests(),
454                     "Test Setup Failed: There should be no pending IME requests present when the "
455                             + "test starts.");
456 
457             final TestActivity activity;
458             if (show) {
459                 // Use STATE_UNCHANGED to not trigger any other IME requests.
460                 activity = createTestActivity(SOFT_INPUT_STATE_UNCHANGED);
461                 expectImeInvisible(TIMEOUT);
462             } else {
463                 // If running a hide test, start with the IME showing already.
464                 activity = createTestActivity(SOFT_INPUT_STATE_ALWAYS_VISIBLE);
465                 expectImeVisible(TIMEOUT);
466                 // Wait for any outstanding IME requests to finish, to capture all atoms.
467                 PollingCheck.waitFor(() -> !imeSession.hasPendingImeVisibilityRequests(),
468                         "Test Error: Pending IME requests took too long, likely timing out.");
469             }
470 
471             // Wait for any atoms from activity start to be sent.
472             // TODO(b/330143218): Add a proper fence for statsd
473             Thread.sleep(WAIT_TIME_SHORT);
474 
475             // Expect atoms pushed from either the IME process, or from the test app process.
476             MetricsRecorder.uploadConfigForPushedAtomWithUid(
477                     new String[]{mPkgName, imeSession.getMockImePackageName()},
478                     AtomsProto.Atom.IME_REQUEST_FINISHED_FIELD_NUMBER,
479                     false /* useUidAttributionChain */);
480 
481             // Run the given test.
482             runnable.run(imeSession, activity);
483 
484             // Wait for any outstanding IME requests to finish, to capture all atoms.
485             PollingCheck.waitFor(() -> !imeSession.hasPendingImeVisibilityRequests(),
486                     "Test Error: Pending IME requests took too long, likely timing out.");
487 
488             // Wait for any atoms from the test runnable to be sent.
489             // TODO(b/330143218): Add a proper fence for statsd
490             Thread.sleep(WAIT_TIME_SHORT);
491 
492             // Must have at least one atom received.
493             final var data = MetricsRecorder.getEventMetricDataList();
494             assertWithMessage("Number of atoms logged")
495                     .that(data.size())
496                     .isAtLeast(1);
497 
498             // Check received atom data.
499             try {
500                 int successfulAtoms = 0;
501                 for (int i = 0; i < data.size(); i++) {
502                     final var atom = data.get(i).atom;
503                     assertThat(atom).isNotNull();
504 
505                     final var imeRequestFinished = atom.getImeRequestFinished();
506                     assertThat(imeRequestFinished).isNotNull();
507 
508                     // Skip cancelled requests.
509                     if (imeRequestFinished.status == ImeProtoEnums.STATUS_CANCEL) continue;
510 
511                     successfulAtoms++;
512 
513                     assertWithMessage("Ime Request type")
514                             .that(imeRequestFinished.type)
515                             .isEqualTo(show ? ImeProtoEnums.TYPE_SHOW : ImeProtoEnums.TYPE_HIDE);
516                     assertWithMessage("Ime Request status")
517                             .that(imeRequestFinished.status)
518                             .isEqualTo(ImeProtoEnums.STATUS_SUCCESS);
519                     assertWithMessage("Ime Request origin")
520                             .that(imeRequestFinished.origin)
521                             .isIn(origins);
522                     if (fromUser) {
523                         // Assert only when fromUser was expected to be true.
524                         assertWithMessage("Ime Request fromUser")
525                                 .that(imeRequestFinished.fromUser)
526                                 .isEqualTo(true);
527                     }
528                 }
529 
530                 // Must have at least one successful request received.
531                 assertWithMessage("Number of successful atoms logged")
532                         .that(successfulAtoms)
533                         .isAtLeast(1);
534             } catch (AssertionError e) {
535                 throw new AssertionError(e.getMessage() + "\natoms data:\n" + data, e);
536             }
537         }
538     }
539 
540     /** Interface for the test code to be ran. */
541     private interface TestRunnable {
542 
543         /**
544          * Execute the given test code given the ime session and activity.
545          *
546          * @param imeSession the initialized mock ime session.
547          * @param activity   the initialized test activity.
548          */
run(@onNull MockImeSession imeSession, @NonNull TestActivity activity)549         void run(@NonNull MockImeSession imeSession, @NonNull TestActivity activity);
550     }
551 }
552