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