1 /*
2  * Copyright (C) 2018 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 com.android.launcher3.tapl;
18 
19 import static android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_ENABLED;
20 import static android.content.pm.PackageManager.DONT_KILL_APP;
21 import static android.content.pm.PackageManager.MATCH_ALL;
22 import static android.content.pm.PackageManager.MATCH_DISABLED_COMPONENTS;
23 
24 import static com.android.launcher3.tapl.TestHelpers.getOverviewPackageName;
25 import static com.android.launcher3.testing.TestProtocol.BACKGROUND_APP_STATE_ORDINAL;
26 import static com.android.launcher3.testing.TestProtocol.NORMAL_STATE_ORDINAL;
27 
28 import android.app.ActivityManager;
29 import android.app.Instrumentation;
30 import android.app.UiAutomation;
31 import android.content.ComponentName;
32 import android.content.ContentResolver;
33 import android.content.Context;
34 import android.content.pm.PackageManager;
35 import android.content.pm.ProviderInfo;
36 import android.content.res.Resources;
37 import android.graphics.Point;
38 import android.graphics.Rect;
39 import android.net.Uri;
40 import android.os.Bundle;
41 import android.os.Parcelable;
42 import android.os.SystemClock;
43 import android.text.TextUtils;
44 import android.util.Log;
45 import android.view.InputDevice;
46 import android.view.MotionEvent;
47 import android.view.Surface;
48 import android.view.ViewConfiguration;
49 import android.view.WindowManager;
50 import android.view.accessibility.AccessibilityEvent;
51 
52 import androidx.annotation.NonNull;
53 import androidx.annotation.Nullable;
54 import androidx.test.InstrumentationRegistry;
55 import androidx.test.uiautomator.By;
56 import androidx.test.uiautomator.BySelector;
57 import androidx.test.uiautomator.Configurator;
58 import androidx.test.uiautomator.Direction;
59 import androidx.test.uiautomator.UiDevice;
60 import androidx.test.uiautomator.UiObject2;
61 import androidx.test.uiautomator.Until;
62 
63 import com.android.launcher3.ResourceUtils;
64 import com.android.launcher3.testing.TestProtocol;
65 import com.android.systemui.shared.system.QuickStepContract;
66 
67 import org.junit.Assert;
68 
69 import java.io.ByteArrayOutputStream;
70 import java.io.IOException;
71 import java.lang.ref.WeakReference;
72 import java.util.Collection;
73 import java.util.Collections;
74 import java.util.Deque;
75 import java.util.LinkedList;
76 import java.util.List;
77 import java.util.concurrent.TimeoutException;
78 import java.util.function.Consumer;
79 import java.util.function.Function;
80 
81 /**
82  * The main tapl object. The only object that can be explicitly constructed by the using code. It
83  * produces all other objects.
84  */
85 public final class LauncherInstrumentation {
86 
87     private static final String TAG = "Tapl";
88     private static final int ZERO_BUTTON_STEPS_FROM_BACKGROUND_TO_HOME = 20;
89     private static final int GESTURE_STEP_MS = 16;
90     private static long START_TIME = System.currentTimeMillis();
91 
92     // Types for launcher containers that the user is interacting with. "Background" is a
93     // pseudo-container corresponding to inactive launcher covered by another app.
94     public enum ContainerType {
95         WORKSPACE, ALL_APPS, OVERVIEW, WIDGETS, BACKGROUND, FALLBACK_OVERVIEW
96     }
97 
98     public enum NavigationModel {ZERO_BUTTON, TWO_BUTTON, THREE_BUTTON}
99 
100     // Base class for launcher containers.
101     static abstract class VisibleContainer {
102         protected final LauncherInstrumentation mLauncher;
103 
VisibleContainer(LauncherInstrumentation launcher)104         protected VisibleContainer(LauncherInstrumentation launcher) {
105             mLauncher = launcher;
106             launcher.setActiveContainer(this);
107         }
108 
getContainerType()109         protected abstract ContainerType getContainerType();
110 
111         /**
112          * Asserts that the launcher is in the mode matching 'this' object.
113          *
114          * @return UI object for the container.
115          */
verifyActiveContainer()116         final UiObject2 verifyActiveContainer() {
117             mLauncher.assertTrue("Attempt to use a stale container",
118                     this == sActiveContainer.get());
119             return mLauncher.verifyContainerType(getContainerType());
120         }
121     }
122 
123     interface Closable extends AutoCloseable {
close()124         void close();
125     }
126 
127     private static final String WORKSPACE_RES_ID = "workspace";
128     private static final String APPS_RES_ID = "apps_view";
129     private static final String OVERVIEW_RES_ID = "overview_panel";
130     private static final String WIDGETS_RES_ID = "widgets_list_view";
131     public static final int WAIT_TIME_MS = 10000;
132     private static final String SYSTEMUI_PACKAGE = "com.android.systemui";
133 
134     private static WeakReference<VisibleContainer> sActiveContainer = new WeakReference<>(null);
135 
136     private final UiDevice mDevice;
137     private final Instrumentation mInstrumentation;
138     private int mExpectedRotation = Surface.ROTATION_0;
139     private final Uri mTestProviderUri;
140     private final Deque<String> mDiagnosticContext = new LinkedList<>();
141     private Function<Long, String> mSystemHealthSupplier;
142 
143     private Consumer<ContainerType> mOnSettledStateAction;
144 
145     /**
146      * Constructs the root of TAPL hierarchy. You get all other objects from it.
147      */
LauncherInstrumentation()148     public LauncherInstrumentation() {
149         this(InstrumentationRegistry.getInstrumentation());
150     }
151 
152     /**
153      * Constructs the root of TAPL hierarchy. You get all other objects from it.
154      * Deprecated: use the constructor without parameters instead.
155      */
156     @Deprecated
LauncherInstrumentation(Instrumentation instrumentation)157     public LauncherInstrumentation(Instrumentation instrumentation) {
158         mInstrumentation = instrumentation;
159         mDevice = UiDevice.getInstance(instrumentation);
160 
161         // Launcher should run in test harness so that custom accessibility protocol between
162         // Launcher and TAPL is enabled. In-process tests enable this protocol with a direct call
163         // into Launcher.
164         assertTrue("Device must run in a test harness",
165                 TestHelpers.isInLauncherProcess() || ActivityManager.isRunningInTestHarness());
166 
167         final String testPackage = getContext().getPackageName();
168         final String targetPackage = mInstrumentation.getTargetContext().getPackageName();
169 
170         // Launcher package. As during inproc tests the tested launcher may not be selected as the
171         // current launcher, choosing target package for inproc. For out-of-proc, use the installed
172         // launcher package.
173         final String authorityPackage = testPackage.equals(targetPackage) ?
174                 getLauncherPackageName() :
175                 targetPackage;
176 
177         String testProviderAuthority = authorityPackage + ".TestInfo";
178         mTestProviderUri = new Uri.Builder()
179                 .scheme(ContentResolver.SCHEME_CONTENT)
180                 .authority(testProviderAuthority)
181                 .build();
182 
183         try {
184             mDevice.executeShellCommand("pm grant " + testPackage +
185                     " android.permission.WRITE_SECURE_SETTINGS");
186         } catch (IOException e) {
187             fail(e.toString());
188         }
189 
190 
191         PackageManager pm = getContext().getPackageManager();
192         ProviderInfo pi = pm.resolveContentProvider(
193                 testProviderAuthority, MATCH_ALL | MATCH_DISABLED_COMPONENTS);
194         assertNotNull("Cannot find content provider for " + testProviderAuthority, pi);
195         ComponentName cn = new ComponentName(pi.packageName, pi.name);
196 
197         if (pm.getComponentEnabledSetting(cn) != COMPONENT_ENABLED_STATE_ENABLED) {
198             if (TestHelpers.isInLauncherProcess()) {
199                 getContext().getPackageManager().setComponentEnabledSetting(
200                         cn, COMPONENT_ENABLED_STATE_ENABLED, DONT_KILL_APP);
201             } else {
202                 try {
203                     mDevice.executeShellCommand("pm enable " + cn.flattenToString());
204                 } catch (IOException e) {
205                     fail(e.toString());
206                 }
207             }
208         }
209     }
210 
getContext()211     Context getContext() {
212         return mInstrumentation.getContext();
213     }
214 
getTestInfo(String request)215     Bundle getTestInfo(String request) {
216         return getContext().getContentResolver().call(mTestProviderUri, request, null, null);
217     }
218 
setActiveContainer(VisibleContainer container)219     void setActiveContainer(VisibleContainer container) {
220         sActiveContainer = new WeakReference<>(container);
221     }
222 
getNavigationModel()223     public NavigationModel getNavigationModel() {
224         final Context baseContext = mInstrumentation.getTargetContext();
225         try {
226             // Workaround, use constructed context because both the instrumentation context and the
227             // app context are not constructed with resources that take overlays into account
228             final Context ctx = baseContext.createPackageContext(getLauncherPackageName(), 0);
229             for (int i = 0; i < 100; ++i) {
230                 final int currentInteractionMode = getCurrentInteractionMode(ctx);
231                 final NavigationModel model = getNavigationModel(currentInteractionMode);
232                 log("Interaction mode = " + currentInteractionMode + " (" + model + ")");
233                 if (model != null) return model;
234                 Thread.sleep(100);
235             }
236             fail("Can't detect navigation mode");
237         } catch (Exception e) {
238             fail(e.toString());
239         }
240         return NavigationModel.THREE_BUTTON;
241     }
242 
getNavigationModel(int currentInteractionMode)243     public static NavigationModel getNavigationModel(int currentInteractionMode) {
244         if (QuickStepContract.isGesturalMode(currentInteractionMode)) {
245             return NavigationModel.ZERO_BUTTON;
246         } else if (QuickStepContract.isSwipeUpMode(currentInteractionMode)) {
247             return NavigationModel.TWO_BUTTON;
248         } else if (QuickStepContract.isLegacyMode(currentInteractionMode)) {
249             return NavigationModel.THREE_BUTTON;
250         }
251         return null;
252     }
253 
log(String message)254     static void log(String message) {
255         Log.d(TAG, message);
256     }
257 
addContextLayer(String piece)258     Closable addContextLayer(String piece) {
259         mDiagnosticContext.addLast(piece);
260         log("Added context: " + getContextDescription());
261         return () -> {
262             log("Removing context: " + getContextDescription());
263             mDiagnosticContext.removeLast();
264         };
265     }
266 
dumpViewHierarchy()267     private void dumpViewHierarchy() {
268         final ByteArrayOutputStream stream = new ByteArrayOutputStream();
269         try {
270             mDevice.dumpWindowHierarchy(stream);
271             stream.flush();
272             stream.close();
273             for (String line : stream.toString().split("\\r?\\n")) {
274                 Log.e(TAG, line.trim());
275             }
276         } catch (IOException e) {
277             Log.e(TAG, "error dumping XML to logcat", e);
278         }
279     }
280 
getAnomalyMessage()281     private String getAnomalyMessage() {
282         UiObject2 object = mDevice.findObject(By.res("android", "alertTitle"));
283         if (object != null) {
284             return "System alert popup is visible: " + object.getText();
285         }
286 
287         object = mDevice.findObject(By.res("android", "message"));
288         if (object != null) {
289             return "Message popup by " + object.getApplicationPackage() + " is visible: "
290                     + object.getText();
291         }
292 
293         if (hasSystemUiObject("keyguard_status_view")) return "Phone is locked";
294 
295         if (!mDevice.hasObject(By.textStartsWith(""))) return "Screen is empty";
296 
297         return null;
298     }
299 
getVisibleStateMessage()300     private String getVisibleStateMessage() {
301         if (hasLauncherObject(WIDGETS_RES_ID)) return "Widgets";
302         if (hasLauncherObject(OVERVIEW_RES_ID)) return "Overview";
303         if (hasLauncherObject(WORKSPACE_RES_ID)) return "Workspace";
304         if (hasLauncherObject(APPS_RES_ID)) return "AllApps";
305         return "Background";
306     }
307 
setSystemHealthSupplier(Function<Long, String> supplier)308     public void setSystemHealthSupplier(Function<Long, String> supplier) {
309         this.mSystemHealthSupplier = supplier;
310     }
311 
setOnSettledStateAction(Consumer<ContainerType> onSettledStateAction)312     public void setOnSettledStateAction(Consumer<ContainerType> onSettledStateAction) {
313         mOnSettledStateAction = onSettledStateAction;
314     }
315 
getSystemHealthMessage()316     private String getSystemHealthMessage() {
317         final String testPackage = getContext().getPackageName();
318         try {
319             mDevice.executeShellCommand("pm grant " + testPackage +
320                     " android.permission.READ_LOGS");
321             mDevice.executeShellCommand("pm grant " + testPackage +
322                     " android.permission.PACKAGE_USAGE_STATS");
323         } catch (IOException e) {
324             e.printStackTrace();
325         }
326 
327         return mSystemHealthSupplier != null
328                 ? mSystemHealthSupplier.apply(START_TIME)
329                 : TestHelpers.getSystemHealthMessage(getContext(), START_TIME);
330     }
331 
fail(String message)332     private void fail(String message) {
333         message = "http://go/tapl : " + getContextDescription() + message;
334 
335         final String anomaly = getAnomalyMessage();
336         if (anomaly != null) {
337             message = anomaly + ", which causes:\n" + message;
338         } else {
339             message = message + " (visible state: " + getVisibleStateMessage() + ")";
340         }
341 
342         final String systemHealth = getSystemHealthMessage();
343         if (systemHealth != null) {
344             message = message
345                     + ", which might be a consequence of system health "
346                     + "problems:\n<<<<<<<<<<<<<<<<<<\n"
347                     + systemHealth + "\n>>>>>>>>>>>>>>>>>>";
348         }
349 
350         log("Hierarchy dump for: " + message);
351         dumpViewHierarchy();
352 
353         Assert.fail(message);
354     }
355 
getContextDescription()356     private String getContextDescription() {
357         return mDiagnosticContext.isEmpty() ? "" : String.join(", ", mDiagnosticContext) + "; ";
358     }
359 
assertTrue(String message, boolean condition)360     void assertTrue(String message, boolean condition) {
361         if (!condition) {
362             fail(message);
363         }
364     }
365 
assertNotNull(String message, Object object)366     void assertNotNull(String message, Object object) {
367         assertTrue(message, object != null);
368     }
369 
failEquals(String message, Object actual)370     private void failEquals(String message, Object actual) {
371         fail(message + ". " + "Actual: " + actual);
372     }
373 
assertEquals(String message, int expected, int actual)374     private void assertEquals(String message, int expected, int actual) {
375         if (expected != actual) {
376             fail(message + " expected: " + expected + " but was: " + actual);
377         }
378     }
379 
assertEquals(String message, String expected, String actual)380     void assertEquals(String message, String expected, String actual) {
381         if (!TextUtils.equals(expected, actual)) {
382             fail(message + " expected: '" + expected + "' but was: '" + actual + "'");
383         }
384     }
385 
assertEquals(String message, long expected, long actual)386     void assertEquals(String message, long expected, long actual) {
387         if (expected != actual) {
388             fail(message + " expected: " + expected + " but was: " + actual);
389         }
390     }
391 
assertNotEquals(String message, int unexpected, int actual)392     void assertNotEquals(String message, int unexpected, int actual) {
393         if (unexpected == actual) {
394             failEquals(message, actual);
395         }
396     }
397 
setExpectedRotation(int expectedRotation)398     public void setExpectedRotation(int expectedRotation) {
399         mExpectedRotation = expectedRotation;
400     }
401 
getNavigationModeMismatchError()402     public String getNavigationModeMismatchError() {
403         final NavigationModel navigationModel = getNavigationModel();
404         final boolean hasRecentsButton = hasSystemUiObject("recent_apps");
405         final boolean hasHomeButton = hasSystemUiObject("home");
406         if ((navigationModel == NavigationModel.THREE_BUTTON) != hasRecentsButton) {
407             return "Presence of recents button doesn't match the interaction mode, mode="
408                     + navigationModel.name() + ", hasRecents=" + hasRecentsButton;
409         }
410         if ((navigationModel != NavigationModel.ZERO_BUTTON) != hasHomeButton) {
411             return "Presence of home button doesn't match the interaction mode, mode="
412                     + navigationModel.name() + ", hasHome=" + hasHomeButton;
413         }
414         return null;
415     }
416 
verifyContainerType(ContainerType containerType)417     private UiObject2 verifyContainerType(ContainerType containerType) {
418         waitForLauncherInitialized();
419 
420         assertEquals("Unexpected display rotation",
421                 mExpectedRotation, mDevice.getDisplayRotation());
422 
423         // b/136278866
424         for (int i = 0; i != 100; ++i) {
425             if (getNavigationModeMismatchError() == null) break;
426             try {
427                 Thread.sleep(100);
428             } catch (InterruptedException e) {
429                 e.printStackTrace();
430             }
431         }
432 
433         final String error = getNavigationModeMismatchError();
434         assertTrue(error, error == null);
435         log("verifyContainerType: " + containerType);
436 
437         final UiObject2 container = verifyVisibleObjects(containerType);
438 
439         if (mOnSettledStateAction != null) mOnSettledStateAction.accept(containerType);
440 
441         return container;
442     }
443 
verifyVisibleObjects(ContainerType containerType)444     private UiObject2 verifyVisibleObjects(ContainerType containerType) {
445         try (Closable c = addContextLayer(
446                 "but the current state is not " + containerType.name())) {
447             switch (containerType) {
448                 case WORKSPACE: {
449                     if (mDevice.isNaturalOrientation()) {
450                         waitForLauncherObject(APPS_RES_ID);
451                     } else {
452                         waitUntilGone(APPS_RES_ID);
453                     }
454                     waitUntilGone(OVERVIEW_RES_ID);
455                     waitUntilGone(WIDGETS_RES_ID);
456                     return waitForLauncherObject(WORKSPACE_RES_ID);
457                 }
458                 case WIDGETS: {
459                     waitUntilGone(WORKSPACE_RES_ID);
460                     waitUntilGone(APPS_RES_ID);
461                     waitUntilGone(OVERVIEW_RES_ID);
462                     return waitForLauncherObject(WIDGETS_RES_ID);
463                 }
464                 case ALL_APPS: {
465                     waitUntilGone(WORKSPACE_RES_ID);
466                     waitUntilGone(OVERVIEW_RES_ID);
467                     waitUntilGone(WIDGETS_RES_ID);
468                     return waitForLauncherObject(APPS_RES_ID);
469                 }
470                 case OVERVIEW: {
471                     if (mDevice.isNaturalOrientation()) {
472                         waitForLauncherObject(APPS_RES_ID);
473                     } else {
474                         waitUntilGone(APPS_RES_ID);
475                     }
476                     waitUntilGone(WORKSPACE_RES_ID);
477                     waitUntilGone(WIDGETS_RES_ID);
478 
479                     return waitForLauncherObject(OVERVIEW_RES_ID);
480                 }
481                 case FALLBACK_OVERVIEW: {
482                     return waitForFallbackLauncherObject(OVERVIEW_RES_ID);
483                 }
484                 case BACKGROUND: {
485                     waitUntilGone(WORKSPACE_RES_ID);
486                     waitUntilGone(APPS_RES_ID);
487                     waitUntilGone(OVERVIEW_RES_ID);
488                     waitUntilGone(WIDGETS_RES_ID);
489                     return null;
490                 }
491                 default:
492                     fail("Invalid state: " + containerType);
493                     return null;
494             }
495         }
496     }
497 
waitForLauncherInitialized()498     private void waitForLauncherInitialized() {
499         for (int i = 0; i < 100; ++i) {
500             if (getTestInfo(
501                     TestProtocol.REQUEST_IS_LAUNCHER_INITIALIZED).
502                     getBoolean(TestProtocol.TEST_INFO_RESPONSE_FIELD)) {
503                 return;
504             }
505             SystemClock.sleep(100);
506         }
507         fail("Launcher didn't initialize");
508     }
509 
executeAndWaitForEvent(Runnable command, UiAutomation.AccessibilityEventFilter eventFilter, String message)510     Parcelable executeAndWaitForEvent(Runnable command,
511             UiAutomation.AccessibilityEventFilter eventFilter, String message) {
512         try {
513             final AccessibilityEvent event =
514                     mInstrumentation.getUiAutomation().executeAndWaitForEvent(
515                             command, eventFilter, WAIT_TIME_MS);
516             assertNotNull("executeAndWaitForEvent returned null (this can't happen)", event);
517             return event.getParcelableData();
518         } catch (TimeoutException e) {
519             fail(message);
520             return null;
521         }
522     }
523 
getAnswerFromLauncher(UiObject2 view, String requestTag)524     Bundle getAnswerFromLauncher(UiObject2 view, String requestTag) {
525         // Send a fake set-text request to Launcher to initiate a response with requested data.
526         final String responseTag = requestTag + TestProtocol.RESPONSE_MESSAGE_POSTFIX;
527         return (Bundle) executeAndWaitForEvent(
528                 () -> view.setText(requestTag),
529                 event -> responseTag.equals(event.getClassName()),
530                 "Launcher didn't respond to request: " + requestTag);
531     }
532 
533     /**
534      * Presses nav bar home button.
535      *
536      * @return the Workspace object.
537      */
pressHome()538     public Workspace pressHome() {
539         // Click home, then wait for any accessibility event, then wait until accessibility events
540         // stop.
541         // We need waiting for any accessibility event generated after pressing Home because
542         // otherwise waitForIdle may return immediately in case when there was a big enough pause in
543         // accessibility events prior to pressing Home.
544         final String action;
545         if (getNavigationModel() == NavigationModel.ZERO_BUTTON) {
546             final String anomaly = getAnomalyMessage();
547             if (anomaly != null) fail("Can't swipe up to Home: " + anomaly);
548 
549             final Point displaySize = getRealDisplaySize();
550 
551             if (hasLauncherObject("deep_shortcuts_container")) {
552                 linearGesture(
553                         displaySize.x / 2, displaySize.y - 1,
554                         displaySize.x / 2, 0,
555                         ZERO_BUTTON_STEPS_FROM_BACKGROUND_TO_HOME);
556                 try (LauncherInstrumentation.Closable c = addContextLayer(
557                         "Swiped up from context menu to home")) {
558                     waitUntilGone("deep_shortcuts_container");
559                 }
560             }
561             if (hasLauncherObject(WORKSPACE_RES_ID)) {
562                 log(action = "already at home");
563             } else {
564                 log("Hierarchy before swiping up to home");
565                 dumpViewHierarchy();
566                 log(action = "swiping up to home from " + getVisibleStateMessage());
567                 final int finalState = mDevice.hasObject(By.pkg(getLauncherPackageName()))
568                         ? NORMAL_STATE_ORDINAL : BACKGROUND_APP_STATE_ORDINAL;
569 
570                 swipeToState(
571                         displaySize.x / 2, displaySize.y - 1,
572                         displaySize.x / 2, 0,
573                         ZERO_BUTTON_STEPS_FROM_BACKGROUND_TO_HOME, finalState);
574             }
575         } else {
576             log(action = "clicking home button");
577             executeAndWaitForEvent(
578                     () -> {
579                         log("LauncherInstrumentation.pressHome before clicking");
580                         waitForSystemUiObject("home").click();
581                     },
582                     event -> true,
583                     "Pressing Home didn't produce any events");
584             mDevice.waitForIdle();
585         }
586         try (LauncherInstrumentation.Closable c = addContextLayer(
587                 "performed action to switch to Home - " + action)) {
588             return getWorkspace();
589         }
590     }
591 
592     /**
593      * Gets the Workspace object if the current state is "active home", i.e. workspace. Fails if the
594      * launcher is not in that state.
595      *
596      * @return Workspace object.
597      */
598     @NonNull
getWorkspace()599     public Workspace getWorkspace() {
600         try (LauncherInstrumentation.Closable c = addContextLayer("want to get workspace object")) {
601             return new Workspace(this);
602         }
603     }
604 
605     /**
606      * Gets the Workspace object if the current state is "background home", i.e. some other app is
607      * active. Fails if the launcher is not in that state.
608      *
609      * @return Background object.
610      */
611     @NonNull
getBackground()612     public Background getBackground() {
613         return new Background(this);
614     }
615 
616     /**
617      * Gets the Widgets object if the current state is showing all widgets. Fails if the launcher is
618      * not in that state.
619      *
620      * @return Widgets object.
621      */
622     @NonNull
getAllWidgets()623     public Widgets getAllWidgets() {
624         try (LauncherInstrumentation.Closable c = addContextLayer("want to get widgets")) {
625             return new Widgets(this);
626         }
627     }
628 
629     @NonNull
getAddToHomeScreenPrompt()630     public AddToHomeScreenPrompt getAddToHomeScreenPrompt() {
631         try (LauncherInstrumentation.Closable c = addContextLayer("want to get widget cell")) {
632             return new AddToHomeScreenPrompt(this);
633         }
634     }
635 
636     /**
637      * Gets the Overview object if the current state is showing the overview panel. Fails if the
638      * launcher is not in that state.
639      *
640      * @return Overview object.
641      */
642     @NonNull
getOverview()643     public Overview getOverview() {
644         try (LauncherInstrumentation.Closable c = addContextLayer("want to get overview")) {
645             return new Overview(this);
646         }
647     }
648 
649     /**
650      * Gets the All Apps object if the current state is showing the all apps panel opened by swiping
651      * from workspace. Fails if the launcher is not in that state. Please don't call this method if
652      * App Apps was opened by swiping up from Overview, as it won't fail and will return an
653      * incorrect object.
654      *
655      * @return All Aps object.
656      */
657     @NonNull
getAllApps()658     public AllApps getAllApps() {
659         try (LauncherInstrumentation.Closable c = addContextLayer("want to get all apps object")) {
660             return new AllApps(this);
661         }
662     }
663 
664     /**
665      * Gets the All Apps object if the current state is showing the all apps panel opened by swiping
666      * from overview. Fails if the launcher is not in that state. Please don't call this method if
667      * App Apps was opened by swiping up from home, as it won't fail and will return an
668      * incorrect object.
669      *
670      * @return All Aps object.
671      */
672     @NonNull
getAllAppsFromOverview()673     public AllAppsFromOverview getAllAppsFromOverview() {
674         try (LauncherInstrumentation.Closable c = addContextLayer("want to get all apps object")) {
675             return new AllAppsFromOverview(this);
676         }
677     }
678 
waitUntilGone(String resId)679     void waitUntilGone(String resId) {
680         assertTrue("Unexpected launcher object visible: " + resId,
681                 mDevice.wait(Until.gone(getLauncherObjectSelector(resId)),
682                         WAIT_TIME_MS));
683     }
684 
hasSystemUiObject(String resId)685     private boolean hasSystemUiObject(String resId) {
686         return mDevice.hasObject(By.res(SYSTEMUI_PACKAGE, resId));
687     }
688 
689     @NonNull
waitForSystemUiObject(String resId)690     UiObject2 waitForSystemUiObject(String resId) {
691         final UiObject2 object = mDevice.wait(
692                 Until.findObject(By.res(SYSTEMUI_PACKAGE, resId)), WAIT_TIME_MS);
693         assertNotNull("Can't find a systemui object with id: " + resId, object);
694         return object;
695     }
696 
697     @NonNull
getObjectsInContainer(UiObject2 container, String resName)698     List<UiObject2> getObjectsInContainer(UiObject2 container, String resName) {
699         return container.findObjects(getLauncherObjectSelector(resName));
700     }
701 
702     @NonNull
waitForObjectInContainer(UiObject2 container, String resName)703     UiObject2 waitForObjectInContainer(UiObject2 container, String resName) {
704         final UiObject2 object = container.wait(
705                 Until.findObject(getLauncherObjectSelector(resName)),
706                 WAIT_TIME_MS);
707         assertNotNull("Can't find a launcher object id: " + resName + " in container: " +
708                 container.getResourceName(), object);
709         return object;
710     }
711 
712     @NonNull
waitForObjectInContainer(UiObject2 container, BySelector selector)713     UiObject2 waitForObjectInContainer(UiObject2 container, BySelector selector) {
714         final UiObject2 object = container.wait(
715                 Until.findObject(selector),
716                 WAIT_TIME_MS);
717         assertNotNull("Can't find a launcher object id: " + selector + " in container: " +
718                 container.getResourceName(), object);
719         return object;
720     }
721 
722     @Nullable
hasLauncherObject(String resId)723     private boolean hasLauncherObject(String resId) {
724         return mDevice.hasObject(getLauncherObjectSelector(resId));
725     }
726 
727     @NonNull
waitForLauncherObject(String resName)728     UiObject2 waitForLauncherObject(String resName) {
729         return waitForObjectBySelector(getLauncherObjectSelector(resName));
730     }
731 
732     @NonNull
waitForLauncherObject(BySelector selector)733     UiObject2 waitForLauncherObject(BySelector selector) {
734         return waitForObjectBySelector(By.copy(selector).pkg(getLauncherPackageName()));
735     }
736 
737     @NonNull
tryWaitForLauncherObject(BySelector selector, long timeout)738     UiObject2 tryWaitForLauncherObject(BySelector selector, long timeout) {
739         return tryWaitForObjectBySelector(By.copy(selector).pkg(getLauncherPackageName()), timeout);
740     }
741 
742     @NonNull
waitForFallbackLauncherObject(String resName)743     UiObject2 waitForFallbackLauncherObject(String resName) {
744         return waitForObjectBySelector(getFallbackLauncherObjectSelector(resName));
745     }
746 
waitForObjectBySelector(BySelector selector)747     private UiObject2 waitForObjectBySelector(BySelector selector) {
748         final UiObject2 object = mDevice.wait(Until.findObject(selector), WAIT_TIME_MS);
749         assertNotNull("Can't find a launcher object; selector: " + selector, object);
750         return object;
751     }
752 
tryWaitForObjectBySelector(BySelector selector, long timeout)753     private UiObject2 tryWaitForObjectBySelector(BySelector selector, long timeout) {
754         return mDevice.wait(Until.findObject(selector), timeout);
755     }
756 
getLauncherObjectSelector(String resName)757     BySelector getLauncherObjectSelector(String resName) {
758         return By.res(getLauncherPackageName(), resName);
759     }
760 
getFallbackLauncherObjectSelector(String resName)761     BySelector getFallbackLauncherObjectSelector(String resName) {
762         return By.res(getOverviewPackageName(), resName);
763     }
764 
getLauncherPackageName()765     String getLauncherPackageName() {
766         return mDevice.getLauncherPackageName();
767     }
768 
isFallbackOverview()769     boolean isFallbackOverview() {
770         return !getOverviewPackageName().equals(getLauncherPackageName());
771     }
772 
773     @NonNull
getDevice()774     public UiDevice getDevice() {
775         return mDevice;
776     }
777 
swipeToState(int startX, int startY, int endX, int endY, int steps, int expectedState)778     void swipeToState(int startX, int startY, int endX, int endY, int steps, int expectedState) {
779         final Bundle parcel = (Bundle) executeAndWaitForEvent(
780                 () -> linearGesture(startX, startY, endX, endY, steps),
781                 event -> TestProtocol.SWITCHED_TO_STATE_MESSAGE.equals(event.getClassName()),
782                 "Swipe failed to receive an event for the swipe end");
783         assertEquals("Swipe switched launcher to a wrong state;",
784                 TestProtocol.stateOrdinalToString(expectedState),
785                 TestProtocol.stateOrdinalToString(parcel.getInt(TestProtocol.STATE_FIELD)));
786     }
787 
getBottomGestureSize()788     int getBottomGestureSize() {
789         return ResourceUtils.getNavbarSize(
790                 ResourceUtils.NAVBAR_BOTTOM_GESTURE_SIZE, getResources()) + 1;
791     }
792 
getBottomGestureMargin(UiObject2 container)793     int getBottomGestureMargin(UiObject2 container) {
794         return container.getVisibleBounds().bottom - getRealDisplaySize().y +
795                 getBottomGestureSize();
796     }
797 
scrollToLastVisibleRow(UiObject2 container, Collection<UiObject2> items, int topPadding)798     void scrollToLastVisibleRow(UiObject2 container, Collection<UiObject2> items, int topPadding) {
799         final UiObject2 lowestItem = Collections.max(items, (i1, i2) ->
800                 Integer.compare(i1.getVisibleBounds().top, i2.getVisibleBounds().top));
801 
802         final int gestureStart = lowestItem.getVisibleBounds().top + getTouchSlop();
803         final int distance = gestureStart - container.getVisibleBounds().top - topPadding;
804         final int bottomMargin = container.getVisibleBounds().height() - distance;
805 
806         scroll(
807                 container,
808                 Direction.DOWN,
809                 new Rect(
810                         0,
811                         0,
812                         0,
813                         Math.max(bottomMargin, getBottomGestureMargin(container))),
814                 150);
815     }
816 
scroll(UiObject2 container, Direction direction, Rect margins, int steps)817     void scroll(UiObject2 container, Direction direction, Rect margins, int steps) {
818         final Rect rect = container.getVisibleBounds();
819         if (margins != null) {
820             rect.left += margins.left;
821             rect.top += margins.top;
822             rect.right -= margins.right;
823             rect.bottom -= margins.bottom;
824         }
825 
826         final int startX;
827         final int startY;
828         final int endX;
829         final int endY;
830 
831         switch (direction) {
832             case UP: {
833                 startX = endX = rect.centerX();
834                 final int vertCenter = rect.centerY();
835                 final float halfGestureHeight = rect.height() / 2.0f;
836                 startY = (int) (vertCenter - halfGestureHeight) + 1;
837                 endY = (int) (vertCenter + halfGestureHeight);
838             }
839             break;
840             case DOWN: {
841                 startX = endX = rect.centerX();
842                 final int vertCenter = rect.centerY();
843                 final float halfGestureHeight = rect.height() / 2.0f;
844                 startY = (int) (vertCenter + halfGestureHeight) - 1;
845                 endY = (int) (vertCenter - halfGestureHeight);
846             }
847             break;
848             case LEFT: {
849                 startY = endY = rect.centerY();
850                 final int horizCenter = rect.centerX();
851                 final float halfGestureWidth = rect.width() / 2.0f;
852                 startX = (int) (horizCenter - halfGestureWidth) + 1;
853                 endX = (int) (horizCenter + halfGestureWidth);
854             }
855             break;
856             case RIGHT: {
857                 startY = endY = rect.centerY();
858                 final int horizCenter = rect.centerX();
859                 final float halfGestureWidth = rect.width() / 2.0f;
860                 startX = (int) (horizCenter + halfGestureWidth) - 1;
861                 endX = (int) (horizCenter - halfGestureWidth);
862             }
863             break;
864             default:
865                 fail("Unsupported direction");
866                 return;
867         }
868 
869         executeAndWaitForEvent(
870                 () -> linearGesture(startX, startY, endX, endY, steps),
871                 event -> TestProtocol.SCROLL_FINISHED_MESSAGE.equals(event.getClassName()),
872                 "Didn't receive a scroll end message: " + startX + ", " + startY
873                         + ", " + endX + ", " + endY);
874     }
875 
876     // Inject a swipe gesture. Inject exactly 'steps' motion points, incrementing event time by a
877     // fixed interval each time.
linearGesture(int startX, int startY, int endX, int endY, int steps)878     void linearGesture(int startX, int startY, int endX, int endY, int steps) {
879         log("linearGesture: " + startX + ", " + startY + " -> " + endX + ", " + endY);
880         final long downTime = SystemClock.uptimeMillis();
881         final Point start = new Point(startX, startY);
882         final Point end = new Point(endX, endY);
883         sendPointer(downTime, downTime, MotionEvent.ACTION_DOWN, start);
884         final long endTime = movePointer(downTime, downTime, steps * GESTURE_STEP_MS, start, end);
885         sendPointer(downTime, endTime, MotionEvent.ACTION_UP, end);
886     }
887 
waitForIdle()888     void waitForIdle() {
889         mDevice.waitForIdle();
890     }
891 
getTouchSlop()892     int getTouchSlop() {
893         return ViewConfiguration.get(getContext()).getScaledTouchSlop();
894     }
895 
getResources()896     public Resources getResources() {
897         return getContext().getResources();
898     }
899 
getMotionEvent(long downTime, long eventTime, int action, float x, float y)900     private static MotionEvent getMotionEvent(long downTime, long eventTime, int action,
901             float x, float y) {
902         MotionEvent.PointerProperties properties = new MotionEvent.PointerProperties();
903         properties.id = 0;
904         properties.toolType = Configurator.getInstance().getToolType();
905 
906         MotionEvent.PointerCoords coords = new MotionEvent.PointerCoords();
907         coords.pressure = 1;
908         coords.size = 1;
909         coords.x = x;
910         coords.y = y;
911 
912         return MotionEvent.obtain(downTime, eventTime, action, 1,
913                 new MotionEvent.PointerProperties[]{properties},
914                 new MotionEvent.PointerCoords[]{coords},
915                 0, 0, 1.0f, 1.0f, 0, 0, InputDevice.SOURCE_TOUCHSCREEN, 0);
916     }
917 
sendPointer(long downTime, long currentTime, int action, Point point)918     void sendPointer(long downTime, long currentTime, int action, Point point) {
919         final MotionEvent event = getMotionEvent(downTime, currentTime, action, point.x, point.y);
920         mInstrumentation.getUiAutomation().injectInputEvent(event, true);
921         event.recycle();
922     }
923 
movePointer(long downTime, long startTime, long duration, Point from, Point to)924     long movePointer(long downTime, long startTime, long duration, Point from, Point to) {
925         log("movePointer: " + from + " to " + to);
926         final Point point = new Point();
927         long steps = duration / GESTURE_STEP_MS;
928         long currentTime = startTime;
929         for (long i = 0; i < steps; ++i) {
930             sleep(GESTURE_STEP_MS);
931 
932             currentTime += GESTURE_STEP_MS;
933             final float progress = (currentTime - startTime) / (float) duration;
934 
935             point.x = from.x + (int) (progress * (to.x - from.x));
936             point.y = from.y + (int) (progress * (to.y - from.y));
937 
938             sendPointer(downTime, currentTime, MotionEvent.ACTION_MOVE, point);
939         }
940         return currentTime;
941     }
942 
getCurrentInteractionMode(Context context)943     public static int getCurrentInteractionMode(Context context) {
944         return getSystemIntegerRes(context, "config_navBarInteractionMode");
945     }
946 
getSystemIntegerRes(Context context, String resName)947     private static int getSystemIntegerRes(Context context, String resName) {
948         Resources res = context.getResources();
949         int resId = res.getIdentifier(resName, "integer", "android");
950 
951         if (resId != 0) {
952             return res.getInteger(resId);
953         } else {
954             Log.e(TAG, "Failed to get system resource ID. Incompatible framework version?");
955             return -1;
956         }
957     }
958 
getSystemDimensionResId(Context context, String resName)959     private static int getSystemDimensionResId(Context context, String resName) {
960         Resources res = context.getResources();
961         int resId = res.getIdentifier(resName, "dimen", "android");
962 
963         if (resId != 0) {
964             return resId;
965         } else {
966             Log.e(TAG, "Failed to get system resource ID. Incompatible framework version?");
967             return -1;
968         }
969     }
970 
sleep(int duration)971     static void sleep(int duration) {
972         SystemClock.sleep(duration);
973     }
974 
getEdgeSensitivityWidth()975     int getEdgeSensitivityWidth() {
976         try {
977             final Context context = mInstrumentation.getTargetContext().createPackageContext(
978                     getLauncherPackageName(), 0);
979             return context.getResources().getDimensionPixelSize(
980                     getSystemDimensionResId(context, "config_backGestureInset")) + 1;
981         } catch (PackageManager.NameNotFoundException e) {
982             fail("Can't get edge sensitivity: " + e);
983             return 0;
984         }
985     }
986 
getRealDisplaySize()987     Point getRealDisplaySize() {
988         final Point size = new Point();
989         getContext().getSystemService(WindowManager.class).getDefaultDisplay().getRealSize(size);
990         return size;
991     }
992 
enableDebugTracing()993     public void enableDebugTracing() {
994         getTestInfo(TestProtocol.REQUEST_ENABLE_DEBUG_TRACING);
995     }
996 
disableDebugTracing()997     public void disableDebugTracing() {
998         getTestInfo(TestProtocol.REQUEST_DISABLE_DEBUG_TRACING);
999     }
1000 
getTotalPssKb()1001     public int getTotalPssKb() {
1002         return getTestInfo(TestProtocol.REQUEST_TOTAL_PSS_KB).
1003                 getInt(TestProtocol.TEST_INFO_RESPONSE_FIELD);
1004     }
1005 
produceJavaLeak()1006     public void produceJavaLeak() {
1007         getTestInfo(TestProtocol.REQUEST_JAVA_LEAK);
1008     }
1009 
produceNativeLeak()1010     public void produceNativeLeak() {
1011         getTestInfo(TestProtocol.REQUEST_NATIVE_LEAK);
1012     }
1013 }