• 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.NORMAL_STATE_ORDINAL;
26 
27 import android.app.ActivityManager;
28 import android.app.Instrumentation;
29 import android.app.UiAutomation;
30 import android.content.ComponentName;
31 import android.content.ContentProviderClient;
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.Insets;
38 import android.graphics.Point;
39 import android.graphics.Rect;
40 import android.net.Uri;
41 import android.os.Bundle;
42 import android.os.DeadObjectException;
43 import android.os.Parcelable;
44 import android.os.RemoteException;
45 import android.os.SystemClock;
46 import android.text.TextUtils;
47 import android.util.Log;
48 import android.view.InputDevice;
49 import android.view.MotionEvent;
50 import android.view.Surface;
51 import android.view.ViewConfiguration;
52 import android.view.WindowManager;
53 import android.view.accessibility.AccessibilityEvent;
54 
55 import androidx.annotation.NonNull;
56 import androidx.annotation.Nullable;
57 import androidx.test.InstrumentationRegistry;
58 import androidx.test.uiautomator.By;
59 import androidx.test.uiautomator.BySelector;
60 import androidx.test.uiautomator.Configurator;
61 import androidx.test.uiautomator.Direction;
62 import androidx.test.uiautomator.StaleObjectException;
63 import androidx.test.uiautomator.UiDevice;
64 import androidx.test.uiautomator.UiObject2;
65 import androidx.test.uiautomator.Until;
66 
67 import com.android.launcher3.ResourceUtils;
68 import com.android.launcher3.testing.TestProtocol;
69 import com.android.systemui.shared.system.ContextUtils;
70 import com.android.systemui.shared.system.QuickStepContract;
71 
72 import org.junit.Assert;
73 
74 import java.io.ByteArrayOutputStream;
75 import java.io.IOException;
76 import java.lang.ref.WeakReference;
77 import java.util.ArrayList;
78 import java.util.Collection;
79 import java.util.Collections;
80 import java.util.Deque;
81 import java.util.LinkedList;
82 import java.util.List;
83 import java.util.concurrent.TimeoutException;
84 import java.util.function.Consumer;
85 import java.util.function.Function;
86 import java.util.function.Supplier;
87 import java.util.regex.Pattern;
88 import java.util.stream.Collectors;
89 
90 /**
91  * The main tapl object. The only object that can be explicitly constructed by the using code. It
92  * produces all other objects.
93  */
94 public final class LauncherInstrumentation {
95 
96     private static final String TAG = "Tapl";
97     private static final int ZERO_BUTTON_STEPS_FROM_BACKGROUND_TO_HOME = 20;
98     private static final int GESTURE_STEP_MS = 16;
99 
100     private static final Pattern EVENT_TOUCH_DOWN = getTouchEventPattern("ACTION_DOWN");
101     private static final Pattern EVENT_TOUCH_UP = getTouchEventPattern("ACTION_UP");
102     private static final Pattern EVENT_TOUCH_CANCEL = getTouchEventPattern("ACTION_CANCEL");
103     private static final Pattern EVENT_PILFER_POINTERS = Pattern.compile("pilferPointers");
104     static final Pattern EVENT_START = Pattern.compile("start:");
105 
106     static final Pattern EVENT_TOUCH_DOWN_TIS = getTouchEventPatternTIS("ACTION_DOWN");
107     static final Pattern EVENT_TOUCH_UP_TIS = getTouchEventPatternTIS("ACTION_UP");
108     private final String mLauncherPackage;
109     private Boolean mIsLauncher3;
110     private long mTestStartTime = -1;
111 
112     // Types for launcher containers that the user is interacting with. "Background" is a
113     // pseudo-container corresponding to inactive launcher covered by another app.
114     public enum ContainerType {
115         WORKSPACE, ALL_APPS, OVERVIEW, WIDGETS, BACKGROUND, FALLBACK_OVERVIEW
116     }
117 
118     public enum NavigationModel {ZERO_BUTTON, TWO_BUTTON, THREE_BUTTON}
119 
120     // Where the gesture happens: outside of Launcher, inside or from inside to outside and
121     // whether the gesture recognition triggers pilfer.
122     public enum GestureScope {
123         OUTSIDE_WITHOUT_PILFER, OUTSIDE_WITH_PILFER, INSIDE, INSIDE_TO_OUTSIDE
124     }
125 
126     ;
127 
128     // Base class for launcher containers.
129     static abstract class VisibleContainer {
130         protected final LauncherInstrumentation mLauncher;
131 
VisibleContainer(LauncherInstrumentation launcher)132         protected VisibleContainer(LauncherInstrumentation launcher) {
133             mLauncher = launcher;
134             launcher.setActiveContainer(this);
135         }
136 
getContainerType()137         protected abstract ContainerType getContainerType();
138 
139         /**
140          * Asserts that the launcher is in the mode matching 'this' object.
141          *
142          * @return UI object for the container.
143          */
verifyActiveContainer()144         final UiObject2 verifyActiveContainer() {
145             mLauncher.assertTrue("Attempt to use a stale container",
146                     this == sActiveContainer.get());
147             return mLauncher.verifyContainerType(getContainerType());
148         }
149     }
150 
151     public interface Closable extends AutoCloseable {
close()152         void close();
153     }
154 
155     private static final String WORKSPACE_RES_ID = "workspace";
156     private static final String APPS_RES_ID = "apps_view";
157     private static final String OVERVIEW_RES_ID = "overview_panel";
158     private static final String WIDGETS_RES_ID = "primary_widgets_list_view";
159     private static final String CONTEXT_MENU_RES_ID = "popup_container";
160     public static final int WAIT_TIME_MS = 60000;
161     private static final String SYSTEMUI_PACKAGE = "com.android.systemui";
162     private static final String ANDROID_PACKAGE = "android";
163 
164     private static WeakReference<VisibleContainer> sActiveContainer = new WeakReference<>(null);
165 
166     private final UiDevice mDevice;
167     private final Instrumentation mInstrumentation;
168     private int mExpectedRotation = Surface.ROTATION_0;
169     private final Uri mTestProviderUri;
170     private final Deque<String> mDiagnosticContext = new LinkedList<>();
171     private Function<Long, String> mSystemHealthSupplier;
172 
173     private Consumer<ContainerType> mOnSettledStateAction;
174 
175     private LogEventChecker mEventChecker;
176 
177     private boolean mCheckEventsForSuccessfulGestures = false;
178     private Runnable mOnLauncherCrashed;
179 
getTouchEventPattern(String prefix, String action)180     private static Pattern getTouchEventPattern(String prefix, String action) {
181         // The pattern includes checks that we don't get a multi-touch events or other surprises.
182         return Pattern.compile(
183                 prefix + ": MotionEvent.*?action=" + action + ".*?id\\[0\\]=0"
184                         + ".*?toolType\\[0\\]=TOOL_TYPE_FINGER.*?buttonState=0.*?pointerCount=1");
185     }
186 
getTouchEventPattern(String action)187     private static Pattern getTouchEventPattern(String action) {
188         return getTouchEventPattern("Touch event", action);
189     }
190 
getTouchEventPatternTIS(String action)191     private static Pattern getTouchEventPatternTIS(String action) {
192         return getTouchEventPattern("TouchInteractionService.onInputEvent", action);
193     }
194 
195     /**
196      * Constructs the root of TAPL hierarchy. You get all other objects from it.
197      */
LauncherInstrumentation()198     public LauncherInstrumentation() {
199         this(InstrumentationRegistry.getInstrumentation());
200     }
201 
202     /**
203      * Constructs the root of TAPL hierarchy. You get all other objects from it.
204      * Deprecated: use the constructor without parameters instead.
205      */
206     @Deprecated
LauncherInstrumentation(Instrumentation instrumentation)207     public LauncherInstrumentation(Instrumentation instrumentation) {
208         mInstrumentation = instrumentation;
209         mDevice = UiDevice.getInstance(instrumentation);
210 
211         // Launcher should run in test harness so that custom accessibility protocol between
212         // Launcher and TAPL is enabled. In-process tests enable this protocol with a direct call
213         // into Launcher.
214         assertTrue("Device must run in a test harness",
215                 TestHelpers.isInLauncherProcess() || ActivityManager.isRunningInTestHarness());
216 
217         final String testPackage = getContext().getPackageName();
218         final String targetPackage = mInstrumentation.getTargetContext().getPackageName();
219 
220         // Launcher package. As during inproc tests the tested launcher may not be selected as the
221         // current launcher, choosing target package for inproc. For out-of-proc, use the installed
222         // launcher package.
223         mLauncherPackage = testPackage.equals(targetPackage)
224                 ? getLauncherPackageName()
225                 : targetPackage;
226 
227         String testProviderAuthority = mLauncherPackage + ".TestInfo";
228         mTestProviderUri = new Uri.Builder()
229                 .scheme(ContentResolver.SCHEME_CONTENT)
230                 .authority(testProviderAuthority)
231                 .build();
232 
233         mInstrumentation.getUiAutomation().grantRuntimePermission(
234                 testPackage, "android.permission.WRITE_SECURE_SETTINGS");
235 
236         PackageManager pm = getContext().getPackageManager();
237         ProviderInfo pi = pm.resolveContentProvider(
238                 testProviderAuthority, MATCH_ALL | MATCH_DISABLED_COMPONENTS);
239         assertNotNull("Cannot find content provider for " + testProviderAuthority, pi);
240         ComponentName cn = new ComponentName(pi.packageName, pi.name);
241 
242         if (pm.getComponentEnabledSetting(cn) != COMPONENT_ENABLED_STATE_ENABLED) {
243             if (TestHelpers.isInLauncherProcess()) {
244                 pm.setComponentEnabledSetting(cn, COMPONENT_ENABLED_STATE_ENABLED, DONT_KILL_APP);
245             } else {
246                 try {
247                     final int userId = ContextUtils.getUserId(getContext());
248                     mDevice.executeShellCommand(
249                             "pm enable --user " + userId + " " + cn.flattenToString());
250                 } catch (IOException e) {
251                     fail(e.toString());
252                 }
253             }
254         }
255     }
256 
enableCheckEventsForSuccessfulGestures()257     public void enableCheckEventsForSuccessfulGestures() {
258         mCheckEventsForSuccessfulGestures = true;
259     }
260 
setOnLauncherCrashed(Runnable onLauncherCrashed)261     public void setOnLauncherCrashed(Runnable onLauncherCrashed) {
262         mOnLauncherCrashed = onLauncherCrashed;
263     }
264 
getContext()265     Context getContext() {
266         return mInstrumentation.getContext();
267     }
268 
getTestInfo(String request)269     Bundle getTestInfo(String request) {
270         try (ContentProviderClient client = getContext().getContentResolver()
271                 .acquireContentProviderClient(mTestProviderUri)) {
272             return client.call(request, null, null);
273         } catch (DeadObjectException e) {
274             fail("Launcher crashed");
275             return null;
276         } catch (RemoteException e) {
277             throw new RuntimeException(e);
278         }
279     }
280 
getTargetInsets()281     Insets getTargetInsets() {
282         return getTestInfo(TestProtocol.REQUEST_WINDOW_INSETS)
283                 .getParcelable(TestProtocol.TEST_INFO_RESPONSE_FIELD);
284     }
285 
setActiveContainer(VisibleContainer container)286     void setActiveContainer(VisibleContainer container) {
287         sActiveContainer = new WeakReference<>(container);
288     }
289 
getNavigationModel()290     public NavigationModel getNavigationModel() {
291         final Context baseContext = mInstrumentation.getTargetContext();
292         try {
293             // Workaround, use constructed context because both the instrumentation context and the
294             // app context are not constructed with resources that take overlays into account
295             final Context ctx = baseContext.createPackageContext(getLauncherPackageName(), 0);
296             for (int i = 0; i < 100; ++i) {
297                 final int currentInteractionMode = getCurrentInteractionMode(ctx);
298                 final NavigationModel model = getNavigationModel(currentInteractionMode);
299                 log("Interaction mode = " + currentInteractionMode + " (" + model + ")");
300                 if (model != null) return model;
301                 Thread.sleep(100);
302             }
303             fail("Can't detect navigation mode");
304         } catch (Exception e) {
305             fail(e.toString());
306         }
307         return NavigationModel.THREE_BUTTON;
308     }
309 
getNavigationModel(int currentInteractionMode)310     public static NavigationModel getNavigationModel(int currentInteractionMode) {
311         if (QuickStepContract.isGesturalMode(currentInteractionMode)) {
312             return NavigationModel.ZERO_BUTTON;
313         } else if (QuickStepContract.isSwipeUpMode(currentInteractionMode)) {
314             return NavigationModel.TWO_BUTTON;
315         } else if (QuickStepContract.isLegacyMode(currentInteractionMode)) {
316             return NavigationModel.THREE_BUTTON;
317         }
318         return null;
319     }
320 
log(String message)321     static void log(String message) {
322         Log.d(TAG, message);
323     }
324 
addContextLayer(String piece)325     Closable addContextLayer(String piece) {
326         mDiagnosticContext.addLast(piece);
327         log("Entering context: " + piece);
328         return () -> {
329             log("Leaving context: " + piece);
330             mDiagnosticContext.removeLast();
331         };
332     }
333 
dumpViewHierarchy()334     public void dumpViewHierarchy() {
335         final ByteArrayOutputStream stream = new ByteArrayOutputStream();
336         try {
337             mDevice.dumpWindowHierarchy(stream);
338             stream.flush();
339             stream.close();
340             for (String line : stream.toString().split("\\r?\\n")) {
341                 Log.e(TAG, line.trim());
342             }
343         } catch (IOException e) {
344             Log.e(TAG, "error dumping XML to logcat", e);
345         }
346     }
347 
getSystemAnomalyMessage()348     private String getSystemAnomalyMessage() {
349         try {
350             {
351                 final StringBuilder sb = new StringBuilder();
352 
353                 UiObject2 object = mDevice.findObject(By.res("android", "alertTitle"));
354                 if (object != null) {
355                     sb.append("TITLE: ").append(object.getText());
356                 }
357 
358                 object = mDevice.findObject(By.res("android", "message"));
359                 if (object != null) {
360                     sb.append(" PACKAGE: ").append(object.getApplicationPackage())
361                             .append(" MESSAGE: ").append(object.getText());
362                 }
363 
364                 if (sb.length() != 0) {
365                     return "System alert popup is visible: " + sb;
366                 }
367             }
368 
369             if (hasSystemUiObject("keyguard_status_view")) return "Phone is locked";
370 
371             if (!mDevice.wait(Until.hasObject(getAnyObjectSelector()), WAIT_TIME_MS)) {
372                 return "Screen is empty";
373             }
374 
375             final String navigationModeError = getNavigationModeMismatchError(true);
376             if (navigationModeError != null) return navigationModeError;
377         } catch (Throwable e) {
378             Log.w(TAG, "getSystemAnomalyMessage failed", e);
379         }
380 
381         return null;
382     }
383 
checkForAnomaly()384     public void checkForAnomaly() {
385         final String systemAnomalyMessage = getSystemAnomalyMessage();
386         if (systemAnomalyMessage != null) {
387             Assert.fail(formatSystemHealthMessage(formatErrorWithEvents(
388                     "http://go/tapl : Tests are broken by a non-Launcher system error: "
389                             + systemAnomalyMessage, false)));
390         }
391     }
392 
getVisiblePackages()393     private String getVisiblePackages() {
394         return mDevice.findObjects(getAnyObjectSelector())
395                 .stream()
396                 .map(LauncherInstrumentation::getApplicationPackageSafe)
397                 .distinct()
398                 .filter(pkg -> pkg != null && !"com.android.systemui".equals(pkg))
399                 .collect(Collectors.joining(", "));
400     }
401 
getApplicationPackageSafe(UiObject2 object)402     private static String getApplicationPackageSafe(UiObject2 object) {
403         try {
404             return object.getApplicationPackage();
405         } catch (StaleObjectException e) {
406             // We are looking at all object in the system; external ones can suddenly go away.
407             return null;
408         }
409     }
410 
getVisibleStateMessage()411     private String getVisibleStateMessage() {
412         if (hasLauncherObject(CONTEXT_MENU_RES_ID)) return "Context Menu";
413         if (hasLauncherObject(WIDGETS_RES_ID)) return "Widgets";
414         if (hasLauncherObject(OVERVIEW_RES_ID)) return "Overview";
415         if (hasLauncherObject(WORKSPACE_RES_ID)) return "Workspace";
416         if (hasLauncherObject(APPS_RES_ID)) return "AllApps";
417         return "Background (" + getVisiblePackages() + ")";
418     }
419 
setSystemHealthSupplier(Function<Long, String> supplier)420     public void setSystemHealthSupplier(Function<Long, String> supplier) {
421         this.mSystemHealthSupplier = supplier;
422     }
423 
setOnSettledStateAction(Consumer<ContainerType> onSettledStateAction)424     public void setOnSettledStateAction(Consumer<ContainerType> onSettledStateAction) {
425         mOnSettledStateAction = onSettledStateAction;
426     }
427 
onTestStart()428     public void onTestStart() {
429         mTestStartTime = System.currentTimeMillis();
430     }
431 
onTestFinish()432     public void onTestFinish() {
433         mTestStartTime = -1;
434     }
435 
formatSystemHealthMessage(String message)436     private String formatSystemHealthMessage(String message) {
437         final String testPackage = getContext().getPackageName();
438 
439         mInstrumentation.getUiAutomation().grantRuntimePermission(
440                 testPackage, "android.permission.READ_LOGS");
441         mInstrumentation.getUiAutomation().grantRuntimePermission(
442                 testPackage, "android.permission.PACKAGE_USAGE_STATS");
443 
444         if (mTestStartTime > 0) {
445             final String systemHealth = mSystemHealthSupplier != null
446                     ? mSystemHealthSupplier.apply(mTestStartTime)
447                     : TestHelpers.getSystemHealthMessage(getContext(), mTestStartTime);
448 
449             if (systemHealth != null) {
450                 return message
451                         + ";\nPerhaps linked to system health problems:\n<<<<<<<<<<<<<<<<<<\n"
452                         + systemHealth + "\n>>>>>>>>>>>>>>>>>>";
453             }
454         }
455         return message;
456     }
457 
formatErrorWithEvents(String message, boolean checkEvents)458     private String formatErrorWithEvents(String message, boolean checkEvents) {
459         if (mEventChecker != null) {
460             final LogEventChecker eventChecker = mEventChecker;
461             mEventChecker = null;
462             if (checkEvents) {
463                 final String eventMismatch = eventChecker.verify(0, false);
464                 if (eventMismatch != null) {
465                     message = message + ";\n" + eventMismatch;
466                 }
467             } else {
468                 eventChecker.finishNoWait();
469             }
470         }
471 
472         dumpDiagnostics(message);
473 
474         log("Hierarchy dump for: " + message);
475         dumpViewHierarchy();
476 
477         return message;
478     }
479 
dumpDiagnostics(String message)480     private void dumpDiagnostics(String message) {
481         log("Diagnostics for failure: " + message);
482         log("Input:");
483         logShellCommand("dumpsys input");
484         log("TIS:");
485         logShellCommand("dumpsys activity service TouchInteractionService");
486     }
487 
logShellCommand(String command)488     private void logShellCommand(String command) {
489         try {
490             for (String line : mDevice.executeShellCommand(command).split("\\n")) {
491                 SystemClock.sleep(10);
492                 log(line);
493             }
494         } catch (IOException e) {
495             log("Failed to execute " + command);
496         }
497     }
498 
fail(String message)499     void fail(String message) {
500         checkForAnomaly();
501         Assert.fail(formatSystemHealthMessage(formatErrorWithEvents(
502                 "http://go/tapl test failure:\nContext: " + getContextDescription()
503                         + " - visible state is " + getVisibleStateMessage()
504                         + ";\nDetails: " + message, true)));
505     }
506 
getContextDescription()507     private String getContextDescription() {
508         return mDiagnosticContext.isEmpty()
509                 ? "(no context)" : String.join(", ", mDiagnosticContext);
510     }
511 
assertTrue(String message, boolean condition)512     void assertTrue(String message, boolean condition) {
513         if (!condition) {
514             fail(message);
515         }
516     }
517 
assertNotNull(String message, Object object)518     void assertNotNull(String message, Object object) {
519         assertTrue(message, object != null);
520     }
521 
failEquals(String message, Object actual)522     private void failEquals(String message, Object actual) {
523         fail(message + ". " + "Actual: " + actual);
524     }
525 
assertEquals(String message, int expected, int actual)526     private void assertEquals(String message, int expected, int actual) {
527         if (expected != actual) {
528             fail(message + " expected: " + expected + " but was: " + actual);
529         }
530     }
531 
assertEquals(String message, String expected, String actual)532     void assertEquals(String message, String expected, String actual) {
533         if (!TextUtils.equals(expected, actual)) {
534             fail(message + " expected: '" + expected + "' but was: '" + actual + "'");
535         }
536     }
537 
assertEquals(String message, long expected, long actual)538     void assertEquals(String message, long expected, long actual) {
539         if (expected != actual) {
540             fail(message + " expected: " + expected + " but was: " + actual);
541         }
542     }
543 
assertNotEquals(String message, int unexpected, int actual)544     void assertNotEquals(String message, int unexpected, int actual) {
545         if (unexpected == actual) {
546             failEquals(message, actual);
547         }
548     }
549 
setExpectedRotation(int expectedRotation)550     public void setExpectedRotation(int expectedRotation) {
551         mExpectedRotation = expectedRotation;
552     }
553 
getNavigationModeMismatchError(boolean waitForCorrectState)554     public String getNavigationModeMismatchError(boolean waitForCorrectState) {
555         final int waitTime = waitForCorrectState ? WAIT_TIME_MS : 0;
556         final NavigationModel navigationModel = getNavigationModel();
557 
558         if (navigationModel == NavigationModel.THREE_BUTTON) {
559             if (!mDevice.wait(Until.hasObject(By.res(SYSTEMUI_PACKAGE, "recent_apps")), waitTime)) {
560                 return "Recents button not present in 3-button mode";
561             }
562         } else {
563             if (!mDevice.wait(Until.gone(By.res(SYSTEMUI_PACKAGE, "recent_apps")), waitTime)) {
564                 return "Recents button is present in non-3-button mode";
565             }
566         }
567 
568         if (navigationModel == NavigationModel.ZERO_BUTTON) {
569             if (!mDevice.wait(Until.gone(By.res(SYSTEMUI_PACKAGE, "home")), waitTime)) {
570                 return "Home button is present in gestural mode";
571             }
572         } else {
573             if (!mDevice.wait(Until.hasObject(By.res(SYSTEMUI_PACKAGE, "home")), waitTime)) {
574                 return "Home button not present in non-gestural mode";
575             }
576         }
577         return null;
578     }
579 
verifyContainerType(ContainerType containerType)580     private UiObject2 verifyContainerType(ContainerType containerType) {
581         waitForLauncherInitialized();
582 
583         assertEquals("Unexpected display rotation",
584                 mExpectedRotation, mDevice.getDisplayRotation());
585 
586         final String error = getNavigationModeMismatchError(true);
587         assertTrue(error, error == null);
588 
589         log("verifyContainerType: " + containerType);
590 
591         final UiObject2 container = verifyVisibleObjects(containerType);
592 
593         if (mOnSettledStateAction != null) mOnSettledStateAction.accept(containerType);
594 
595         return container;
596     }
597 
verifyVisibleObjects(ContainerType containerType)598     private UiObject2 verifyVisibleObjects(ContainerType containerType) {
599         try (Closable c = addContextLayer(
600                 "but the current state is not " + containerType.name())) {
601             switch (containerType) {
602                 case WORKSPACE: {
603                     waitUntilLauncherObjectGone(APPS_RES_ID);
604                     waitUntilLauncherObjectGone(OVERVIEW_RES_ID);
605                     waitUntilLauncherObjectGone(WIDGETS_RES_ID);
606                     return waitForLauncherObject(WORKSPACE_RES_ID);
607                 }
608                 case WIDGETS: {
609                     waitUntilLauncherObjectGone(WORKSPACE_RES_ID);
610                     waitUntilLauncherObjectGone(APPS_RES_ID);
611                     waitUntilLauncherObjectGone(OVERVIEW_RES_ID);
612                     return waitForLauncherObject(WIDGETS_RES_ID);
613                 }
614                 case ALL_APPS: {
615                     waitUntilLauncherObjectGone(WORKSPACE_RES_ID);
616                     waitUntilLauncherObjectGone(OVERVIEW_RES_ID);
617                     waitUntilLauncherObjectGone(WIDGETS_RES_ID);
618                     return waitForLauncherObject(APPS_RES_ID);
619                 }
620                 case OVERVIEW: {
621                     waitUntilLauncherObjectGone(APPS_RES_ID);
622                     waitUntilLauncherObjectGone(WORKSPACE_RES_ID);
623                     waitUntilLauncherObjectGone(WIDGETS_RES_ID);
624 
625                     return waitForLauncherObject(OVERVIEW_RES_ID);
626                 }
627                 case FALLBACK_OVERVIEW: {
628                     return waitForFallbackLauncherObject(OVERVIEW_RES_ID);
629                 }
630                 case BACKGROUND: {
631                     waitUntilLauncherObjectGone(WORKSPACE_RES_ID);
632                     waitUntilLauncherObjectGone(APPS_RES_ID);
633                     waitUntilLauncherObjectGone(OVERVIEW_RES_ID);
634                     waitUntilLauncherObjectGone(WIDGETS_RES_ID);
635                     return null;
636                 }
637                 default:
638                     fail("Invalid state: " + containerType);
639                     return null;
640             }
641         }
642     }
643 
waitForLauncherInitialized()644     public void waitForLauncherInitialized() {
645         for (int i = 0; i < 100; ++i) {
646             if (getTestInfo(
647                     TestProtocol.REQUEST_IS_LAUNCHER_INITIALIZED).
648                     getBoolean(TestProtocol.TEST_INFO_RESPONSE_FIELD)) {
649                 return;
650             }
651             SystemClock.sleep(100);
652         }
653         checkForAnomaly();
654         fail("Launcher didn't initialize");
655     }
656 
executeAndWaitForLauncherEvent(Runnable command, UiAutomation.AccessibilityEventFilter eventFilter, Supplier<String> message, String actionName)657     Parcelable executeAndWaitForLauncherEvent(Runnable command,
658             UiAutomation.AccessibilityEventFilter eventFilter, Supplier<String> message,
659             String actionName) {
660         return executeAndWaitForEvent(
661                 command,
662                 e -> mLauncherPackage.equals(e.getPackageName()) && eventFilter.accept(e),
663                 message, actionName);
664     }
665 
executeAndWaitForEvent(Runnable command, UiAutomation.AccessibilityEventFilter eventFilter, Supplier<String> message, String actionName)666     Parcelable executeAndWaitForEvent(Runnable command,
667             UiAutomation.AccessibilityEventFilter eventFilter, Supplier<String> message,
668             String actionName) {
669         try (LauncherInstrumentation.Closable c = addContextLayer(actionName)) {
670             try {
671                 final AccessibilityEvent event =
672                         mInstrumentation.getUiAutomation().executeAndWaitForEvent(
673                                 command, eventFilter, WAIT_TIME_MS);
674                 assertNotNull("executeAndWaitForEvent returned null (this can't happen)", event);
675                 final Parcelable parcelableData = event.getParcelableData();
676                 event.recycle();
677                 return parcelableData;
678             } catch (TimeoutException e) {
679                 fail(message.get());
680                 return null;
681             }
682         }
683     }
684 
685     /**
686      * Presses nav bar home button.
687      *
688      * @return the Workspace object.
689      */
pressHome()690     public Workspace pressHome() {
691         try (LauncherInstrumentation.Closable e = eventsCheck()) {
692             waitForLauncherInitialized();
693             // Click home, then wait for any accessibility event, then wait until accessibility
694             // events stop.
695             // We need waiting for any accessibility event generated after pressing Home because
696             // otherwise waitForIdle may return immediately in case when there was a big enough
697             // pause in accessibility events prior to pressing Home.
698             final String action;
699             final boolean launcherWasVisible = isLauncherVisible();
700             if (getNavigationModel() == NavigationModel.ZERO_BUTTON) {
701                 checkForAnomaly();
702 
703                 final Point displaySize = getRealDisplaySize();
704 
705                 if (hasLauncherObject(CONTEXT_MENU_RES_ID)) {
706                     linearGesture(
707                             displaySize.x / 2, displaySize.y - 1,
708                             displaySize.x / 2, 0,
709                             ZERO_BUTTON_STEPS_FROM_BACKGROUND_TO_HOME,
710                             false, GestureScope.INSIDE_TO_OUTSIDE);
711                     try (LauncherInstrumentation.Closable c = addContextLayer(
712                             "Swiped up from context menu to home")) {
713                         waitUntilLauncherObjectGone(CONTEXT_MENU_RES_ID);
714                         // Swiping up can temporarily bring Nexus Launcher if the current
715                         // Launcher is a Launcher3 one. Wait for the current launcher to reappear.
716                         SystemClock.sleep(5000); // b/187080582
717                         waitForLauncherObject(getAnyObjectSelector());
718                     }
719                 }
720                 if (hasLauncherObject(WORKSPACE_RES_ID)) {
721                     log(action = "already at home");
722                 } else {
723                     log("Hierarchy before swiping up to home:");
724                     dumpViewHierarchy();
725                     action = "swiping up to home";
726 
727                     swipeToState(
728                             displaySize.x / 2, displaySize.y - 1,
729                             displaySize.x / 2, 0,
730                             ZERO_BUTTON_STEPS_FROM_BACKGROUND_TO_HOME, NORMAL_STATE_ORDINAL,
731                             launcherWasVisible
732                                     ? GestureScope.INSIDE_TO_OUTSIDE
733                                     : GestureScope.OUTSIDE_WITH_PILFER);
734                 }
735             } else {
736                 log("Hierarchy before clicking home:");
737                 dumpViewHierarchy();
738                 action = "clicking home button";
739                 if (!isLauncher3() && getNavigationModel() == NavigationModel.TWO_BUTTON) {
740                     expectEvent(TestProtocol.SEQUENCE_TIS, EVENT_TOUCH_DOWN_TIS);
741                     expectEvent(TestProtocol.SEQUENCE_TIS, EVENT_TOUCH_UP_TIS);
742                 }
743 
744                 runToState(
745                         waitForSystemUiObject("home")::click,
746                         NORMAL_STATE_ORDINAL,
747                         !hasLauncherObject(WORKSPACE_RES_ID)
748                                 && (hasLauncherObject(APPS_RES_ID)
749                                 || hasLauncherObject(OVERVIEW_RES_ID)),
750                         action);
751             }
752             try (LauncherInstrumentation.Closable c = addContextLayer(
753                     "performed action to switch to Home - " + action)) {
754                 return getWorkspace();
755             }
756         }
757     }
758 
getAnyObjectSelector()759     private static BySelector getAnyObjectSelector() {
760         return By.textStartsWith("");
761     }
762 
isLauncherVisible()763     boolean isLauncherVisible() {
764         mDevice.waitForIdle();
765         return hasLauncherObject(getAnyObjectSelector());
766     }
767 
768     /**
769      * Gets the Workspace object if the current state is "active home", i.e. workspace. Fails if the
770      * launcher is not in that state.
771      *
772      * @return Workspace object.
773      */
774     @NonNull
getWorkspace()775     public Workspace getWorkspace() {
776         try (LauncherInstrumentation.Closable c = addContextLayer("want to get workspace object")) {
777             return new Workspace(this);
778         }
779     }
780 
781     /**
782      * Gets the Workspace object if the current state is "background home", i.e. some other app is
783      * active. Fails if the launcher is not in that state.
784      *
785      * @return Background object.
786      */
787     @NonNull
getBackground()788     public Background getBackground() {
789         return new Background(this);
790     }
791 
792     /**
793      * Gets the Widgets object if the current state is showing all widgets. Fails if the launcher is
794      * not in that state.
795      *
796      * @return Widgets object.
797      */
798     @NonNull
getAllWidgets()799     public Widgets getAllWidgets() {
800         try (LauncherInstrumentation.Closable c = addContextLayer("want to get widgets")) {
801             return new Widgets(this);
802         }
803     }
804 
805     @NonNull
getAddToHomeScreenPrompt()806     public AddToHomeScreenPrompt getAddToHomeScreenPrompt() {
807         try (LauncherInstrumentation.Closable c = addContextLayer("want to get widget cell")) {
808             return new AddToHomeScreenPrompt(this);
809         }
810     }
811 
812     /**
813      * Gets the Overview object if the current state is showing the overview panel. Fails if the
814      * launcher is not in that state.
815      *
816      * @return Overview object.
817      */
818     @NonNull
getOverview()819     public Overview getOverview() {
820         try (LauncherInstrumentation.Closable c = addContextLayer("want to get overview")) {
821             return new Overview(this);
822         }
823     }
824 
825     /**
826      * Gets the All Apps object if the current state is showing the all apps panel opened by swiping
827      * from workspace. Fails if the launcher is not in that state. Please don't call this method if
828      * App Apps was opened by swiping up from Overview, as it won't fail and will return an
829      * incorrect object.
830      *
831      * @return All Aps object.
832      */
833     @NonNull
getAllApps()834     public AllApps getAllApps() {
835         try (LauncherInstrumentation.Closable c = addContextLayer("want to get all apps object")) {
836             return new AllApps(this);
837         }
838     }
839 
840     /**
841      * Gets the All Apps object if the current state is showing the all apps panel opened by swiping
842      * from overview. Fails if the launcher is not in that state. Please don't call this method if
843      * App Apps was opened by swiping up from home, as it won't fail and will return an
844      * incorrect object.
845      *
846      * @return All Aps object.
847      */
848     @NonNull
getAllAppsFromOverview()849     public AllAppsFromOverview getAllAppsFromOverview() {
850         try (LauncherInstrumentation.Closable c = addContextLayer("want to get all apps object")) {
851             return new AllAppsFromOverview(this);
852         }
853     }
854 
855     /**
856      * Gets the Options Popup Menu object if the current state is showing the popup menu. Fails if
857      * the launcher is not in that state.
858      *
859      * @return Options Popup Menu object.
860      */
861     @NonNull
getOptionsPopupMenu()862     public OptionsPopupMenu getOptionsPopupMenu() {
863         try (LauncherInstrumentation.Closable c = addContextLayer(
864                 "want to get context menu object")) {
865             return new OptionsPopupMenu(this);
866         }
867     }
868 
waitUntilLauncherObjectGone(String resId)869     void waitUntilLauncherObjectGone(String resId) {
870         waitUntilGoneBySelector(getLauncherObjectSelector(resId));
871     }
872 
waitUntilLauncherObjectGone(BySelector selector)873     void waitUntilLauncherObjectGone(BySelector selector) {
874         waitUntilGoneBySelector(makeLauncherSelector(selector));
875     }
876 
waitUntilGoneBySelector(BySelector launcherSelector)877     private void waitUntilGoneBySelector(BySelector launcherSelector) {
878         assertTrue("Unexpected launcher object visible: " + launcherSelector,
879                 mDevice.wait(Until.gone(launcherSelector),
880                         WAIT_TIME_MS));
881     }
882 
hasSystemUiObject(String resId)883     private boolean hasSystemUiObject(String resId) {
884         return mDevice.hasObject(By.res(SYSTEMUI_PACKAGE, resId));
885     }
886 
887     @NonNull
waitForSystemUiObject(String resId)888     UiObject2 waitForSystemUiObject(String resId) {
889         final UiObject2 object = mDevice.wait(
890                 Until.findObject(By.res(SYSTEMUI_PACKAGE, resId)), WAIT_TIME_MS);
891         assertNotNull("Can't find a systemui object with id: " + resId, object);
892         return object;
893     }
894 
895     @Nullable
findObjectInContainer(UiObject2 container, BySelector selector)896     UiObject2 findObjectInContainer(UiObject2 container, BySelector selector) {
897         try {
898             return container.findObject(selector);
899         } catch (StaleObjectException e) {
900             fail("The container disappeared from screen");
901             return null;
902         }
903     }
904 
905     @NonNull
getObjectsInContainer(UiObject2 container, String resName)906     List<UiObject2> getObjectsInContainer(UiObject2 container, String resName) {
907         try {
908             return container.findObjects(getLauncherObjectSelector(resName));
909         } catch (StaleObjectException e) {
910             fail("The container disappeared from screen");
911             return null;
912         }
913     }
914 
915     @NonNull
waitForObjectInContainer(UiObject2 container, String resName)916     UiObject2 waitForObjectInContainer(UiObject2 container, String resName) {
917         try {
918             final UiObject2 object = container.wait(
919                     Until.findObject(getLauncherObjectSelector(resName)),
920                     WAIT_TIME_MS);
921             assertNotNull("Can't find a view in Launcher, id: " + resName + " in container: "
922                     + container.getResourceName(), object);
923             return object;
924         } catch (StaleObjectException e) {
925             fail("The container disappeared from screen");
926             return null;
927         }
928     }
929 
waitForObjectEnabled(UiObject2 object, String waitReason)930     void waitForObjectEnabled(UiObject2 object, String waitReason) {
931         try {
932             assertTrue("Timed out waiting for object to be enabled for " + waitReason + " "
933                     + object.getResourceName(),
934                     object.wait(Until.enabled(true), WAIT_TIME_MS));
935         } catch (StaleObjectException e) {
936             fail("The object disappeared from screen");
937         }
938     }
939 
940     @NonNull
waitForObjectInContainer(UiObject2 container, BySelector selector)941     UiObject2 waitForObjectInContainer(UiObject2 container, BySelector selector) {
942         try {
943             final UiObject2 object = container.wait(
944                     Until.findObject(selector),
945                     WAIT_TIME_MS);
946             assertNotNull("Can't find a view in Launcher, id: " + selector + " in container: "
947                     + container.getResourceName(), object);
948             return object;
949         } catch (StaleObjectException e) {
950             fail("The container disappeared from screen");
951             return null;
952         }
953     }
954 
hasLauncherObject(String resId)955     private boolean hasLauncherObject(String resId) {
956         return mDevice.hasObject(getLauncherObjectSelector(resId));
957     }
958 
hasLauncherObject(BySelector selector)959     boolean hasLauncherObject(BySelector selector) {
960         return mDevice.hasObject(makeLauncherSelector(selector));
961     }
962 
makeLauncherSelector(BySelector selector)963     private BySelector makeLauncherSelector(BySelector selector) {
964         return By.copy(selector).pkg(getLauncherPackageName());
965     }
966 
967     @NonNull
waitForLauncherObject(String resName)968     UiObject2 waitForLauncherObject(String resName) {
969         return waitForObjectBySelector(getLauncherObjectSelector(resName));
970     }
971 
972     @NonNull
waitForLauncherObject(BySelector selector)973     UiObject2 waitForLauncherObject(BySelector selector) {
974         return waitForObjectBySelector(makeLauncherSelector(selector));
975     }
976 
977     @NonNull
tryWaitForLauncherObject(BySelector selector, long timeout)978     UiObject2 tryWaitForLauncherObject(BySelector selector, long timeout) {
979         return tryWaitForObjectBySelector(makeLauncherSelector(selector), timeout);
980     }
981 
982     @NonNull
waitForFallbackLauncherObject(String resName)983     UiObject2 waitForFallbackLauncherObject(String resName) {
984         return waitForObjectBySelector(getOverviewObjectSelector(resName));
985     }
986 
987     @NonNull
waitForAndroidObject(String resId)988     UiObject2 waitForAndroidObject(String resId) {
989         final UiObject2 object = TestHelpers.wait(
990                 Until.findObject(By.res(ANDROID_PACKAGE, resId)), WAIT_TIME_MS);
991         assertNotNull("Can't find a android object with id: " + resId, object);
992         return object;
993     }
994 
waitForObjectBySelector(BySelector selector)995     private UiObject2 waitForObjectBySelector(BySelector selector) {
996         final UiObject2 object = mDevice.wait(Until.findObject(selector), WAIT_TIME_MS);
997         assertNotNull("Can't find a view in Launcher, selector: " + selector, object);
998         return object;
999     }
1000 
tryWaitForObjectBySelector(BySelector selector, long timeout)1001     private UiObject2 tryWaitForObjectBySelector(BySelector selector, long timeout) {
1002         return mDevice.wait(Until.findObject(selector), timeout);
1003     }
1004 
getLauncherObjectSelector(String resName)1005     BySelector getLauncherObjectSelector(String resName) {
1006         return By.res(getLauncherPackageName(), resName);
1007     }
1008 
getOverviewObjectSelector(String resName)1009     BySelector getOverviewObjectSelector(String resName) {
1010         return By.res(getOverviewPackageName(), resName);
1011     }
1012 
getLauncherPackageName()1013     String getLauncherPackageName() {
1014         return mDevice.getLauncherPackageName();
1015     }
1016 
isFallbackOverview()1017     boolean isFallbackOverview() {
1018         return !getOverviewPackageName().equals(getLauncherPackageName());
1019     }
1020 
1021     @NonNull
getDevice()1022     public UiDevice getDevice() {
1023         return mDevice;
1024     }
1025 
eventListToString(List<Integer> actualEvents)1026     private static String eventListToString(List<Integer> actualEvents) {
1027         if (actualEvents.isEmpty()) return "no events";
1028 
1029         return "["
1030                 + actualEvents.stream()
1031                 .map(state -> TestProtocol.stateOrdinalToString(state))
1032                 .collect(Collectors.joining(", "))
1033                 + "]";
1034     }
1035 
runToState(Runnable command, int expectedState, boolean requireEvent, String actionName)1036     void runToState(Runnable command, int expectedState, boolean requireEvent, String actionName) {
1037         if (requireEvent) {
1038             runToState(command, expectedState, actionName);
1039         } else {
1040             command.run();
1041         }
1042     }
1043 
runToState(Runnable command, int expectedState, String actionName)1044     void runToState(Runnable command, int expectedState, String actionName) {
1045         final List<Integer> actualEvents = new ArrayList<>();
1046         executeAndWaitForLauncherEvent(
1047                 command,
1048                 event -> isSwitchToStateEvent(event, expectedState, actualEvents),
1049                 () -> "Failed to receive an event for the state change: expected ["
1050                         + TestProtocol.stateOrdinalToString(expectedState)
1051                         + "], actual: " + eventListToString(actualEvents),
1052                         actionName);
1053     }
1054 
isSwitchToStateEvent( AccessibilityEvent event, int expectedState, List<Integer> actualEvents)1055     private boolean isSwitchToStateEvent(
1056             AccessibilityEvent event, int expectedState, List<Integer> actualEvents) {
1057         if (!TestProtocol.SWITCHED_TO_STATE_MESSAGE.equals(event.getClassName())) return false;
1058 
1059         final Bundle parcel = (Bundle) event.getParcelableData();
1060         final int actualState = parcel.getInt(TestProtocol.STATE_FIELD);
1061         actualEvents.add(actualState);
1062         return actualState == expectedState;
1063     }
1064 
swipeToState(int startX, int startY, int endX, int endY, int steps, int expectedState, GestureScope gestureScope)1065     void swipeToState(int startX, int startY, int endX, int endY, int steps, int expectedState,
1066             GestureScope gestureScope) {
1067         runToState(
1068                 () -> linearGesture(startX, startY, endX, endY, steps, false, gestureScope),
1069                 expectedState,
1070                 "swiping");
1071     }
1072 
getBottomGestureSize()1073     private int getBottomGestureSize() {
1074         return ResourceUtils.getNavbarSize(
1075                 ResourceUtils.NAVBAR_BOTTOM_GESTURE_SIZE, getResources()) + 1;
1076     }
1077 
getBottomGestureMarginInContainer(UiObject2 container)1078     int getBottomGestureMarginInContainer(UiObject2 container) {
1079         final int bottomGestureStartOnScreen = getBottomGestureStartOnScreen();
1080         return getVisibleBounds(container).bottom - bottomGestureStartOnScreen;
1081     }
1082 
getBottomGestureStartOnScreen()1083     int getBottomGestureStartOnScreen() {
1084         return getRealDisplaySize().y - getBottomGestureSize();
1085     }
1086 
clickLauncherObject(UiObject2 object)1087     void clickLauncherObject(UiObject2 object) {
1088         waitForObjectEnabled(object, "clickLauncherObject");
1089         expectEvent(TestProtocol.SEQUENCE_MAIN, LauncherInstrumentation.EVENT_TOUCH_DOWN);
1090         expectEvent(TestProtocol.SEQUENCE_MAIN, LauncherInstrumentation.EVENT_TOUCH_UP);
1091         if (!isLauncher3() && getNavigationModel() != NavigationModel.THREE_BUTTON) {
1092             expectEvent(TestProtocol.SEQUENCE_TIS, LauncherInstrumentation.EVENT_TOUCH_DOWN_TIS);
1093             expectEvent(TestProtocol.SEQUENCE_TIS, LauncherInstrumentation.EVENT_TOUCH_UP_TIS);
1094         }
1095         object.click();
1096     }
1097 
scrollToLastVisibleRow( UiObject2 container, Collection<UiObject2> items, int topPaddingInContainer)1098     void scrollToLastVisibleRow(
1099             UiObject2 container,
1100             Collection<UiObject2> items,
1101             int topPaddingInContainer) {
1102         final UiObject2 lowestItem = Collections.max(items, (i1, i2) ->
1103                 Integer.compare(getVisibleBounds(i1).top, getVisibleBounds(i2).top));
1104 
1105         final int itemRowCurrentTopOnScreen = getVisibleBounds(lowestItem).top;
1106         final Rect containerRect = getVisibleBounds(container);
1107         final int itemRowNewTopOnScreen = containerRect.top + topPaddingInContainer;
1108         final int distance = itemRowCurrentTopOnScreen - itemRowNewTopOnScreen + getTouchSlop();
1109 
1110         scrollDownByDistance(container, distance);
1111     }
1112 
scrollDownByDistance(UiObject2 container, int distance)1113     void scrollDownByDistance(UiObject2 container, int distance) {
1114         final Rect containerRect = getVisibleBounds(container);
1115         final int bottomGestureMarginInContainer = getBottomGestureMarginInContainer(container);
1116         scroll(
1117                 container,
1118                 Direction.DOWN,
1119                 new Rect(
1120                         0,
1121                         containerRect.height() - distance - bottomGestureMarginInContainer,
1122                         0,
1123                         bottomGestureMarginInContainer),
1124                 10,
1125                 true);
1126     }
1127 
scroll( UiObject2 container, Direction direction, Rect margins, int steps, boolean slowDown)1128     void scroll(
1129             UiObject2 container, Direction direction, Rect margins, int steps, boolean slowDown) {
1130         final Rect rect = getVisibleBounds(container);
1131         if (margins != null) {
1132             rect.left += margins.left;
1133             rect.top += margins.top;
1134             rect.right -= margins.right;
1135             rect.bottom -= margins.bottom;
1136         }
1137 
1138         final int startX;
1139         final int startY;
1140         final int endX;
1141         final int endY;
1142 
1143         switch (direction) {
1144             case UP: {
1145                 startX = endX = rect.centerX();
1146                 startY = rect.top;
1147                 endY = rect.bottom - 1;
1148             }
1149             break;
1150             case DOWN: {
1151                 startX = endX = rect.centerX();
1152                 startY = rect.bottom - 1;
1153                 endY = rect.top;
1154             }
1155             break;
1156             case LEFT: {
1157                 startY = endY = rect.centerY();
1158                 startX = rect.left;
1159                 endX = rect.right - 1;
1160             }
1161             break;
1162             case RIGHT: {
1163                 startY = endY = rect.centerY();
1164                 startX = rect.right - 1;
1165                 endX = rect.left;
1166             }
1167             break;
1168             default:
1169                 fail("Unsupported direction");
1170                 return;
1171         }
1172 
1173         executeAndWaitForLauncherEvent(
1174                 () -> linearGesture(
1175                         startX, startY, endX, endY, steps, slowDown, GestureScope.INSIDE),
1176                 event -> TestProtocol.SCROLL_FINISHED_MESSAGE.equals(event.getClassName()),
1177                 () -> "Didn't receive a scroll end message: " + startX + ", " + startY
1178                         + ", " + endX + ", " + endY,
1179                         "scrolling");
1180     }
1181 
1182     // Inject a swipe gesture. Inject exactly 'steps' motion points, incrementing event time by a
1183     // fixed interval each time.
linearGesture(int startX, int startY, int endX, int endY, int steps, boolean slowDown, GestureScope gestureScope)1184     public void linearGesture(int startX, int startY, int endX, int endY, int steps,
1185             boolean slowDown,
1186             GestureScope gestureScope) {
1187         log("linearGesture: " + startX + ", " + startY + " -> " + endX + ", " + endY);
1188         final long downTime = SystemClock.uptimeMillis();
1189         final Point start = new Point(startX, startY);
1190         final Point end = new Point(endX, endY);
1191         sendPointer(downTime, downTime, MotionEvent.ACTION_DOWN, start, gestureScope);
1192         final long endTime = movePointer(start, end, steps, downTime, slowDown, gestureScope);
1193         sendPointer(downTime, endTime, MotionEvent.ACTION_UP, end, gestureScope);
1194     }
1195 
movePointer(Point start, Point end, int steps, long downTime, boolean slowDown, GestureScope gestureScope)1196     long movePointer(Point start, Point end, int steps, long downTime, boolean slowDown,
1197             GestureScope gestureScope) {
1198         long endTime = movePointer(
1199                 downTime, downTime, steps * GESTURE_STEP_MS, start, end, gestureScope);
1200         if (slowDown) {
1201             endTime = movePointer(downTime, endTime + GESTURE_STEP_MS, 5 * GESTURE_STEP_MS, end,
1202                     end, gestureScope);
1203         }
1204         return endTime;
1205     }
1206 
waitForIdle()1207     void waitForIdle() {
1208         mDevice.waitForIdle();
1209     }
1210 
getTouchSlop()1211     int getTouchSlop() {
1212         return ViewConfiguration.get(getContext()).getScaledTouchSlop();
1213     }
1214 
getResources()1215     public Resources getResources() {
1216         return getContext().getResources();
1217     }
1218 
getMotionEvent(long downTime, long eventTime, int action, float x, float y)1219     private static MotionEvent getMotionEvent(long downTime, long eventTime, int action,
1220             float x, float y) {
1221         MotionEvent.PointerProperties properties = new MotionEvent.PointerProperties();
1222         properties.id = 0;
1223         properties.toolType = Configurator.getInstance().getToolType();
1224 
1225         MotionEvent.PointerCoords coords = new MotionEvent.PointerCoords();
1226         coords.pressure = 1;
1227         coords.size = 1;
1228         coords.x = x;
1229         coords.y = y;
1230 
1231         return MotionEvent.obtain(downTime, eventTime, action, 1,
1232                 new MotionEvent.PointerProperties[]{properties},
1233                 new MotionEvent.PointerCoords[]{coords},
1234                 0, 0, 1.0f, 1.0f, 0, 0, InputDevice.SOURCE_TOUCHSCREEN, 0);
1235     }
1236 
sendPointer(long downTime, long currentTime, int action, Point point, GestureScope gestureScope)1237     public void sendPointer(long downTime, long currentTime, int action, Point point,
1238             GestureScope gestureScope) {
1239         final boolean notLauncher3 = !isLauncher3();
1240         switch (action) {
1241             case MotionEvent.ACTION_DOWN:
1242                 if (gestureScope != GestureScope.OUTSIDE_WITH_PILFER
1243                         && gestureScope != GestureScope.OUTSIDE_WITHOUT_PILFER) {
1244                     expectEvent(TestProtocol.SEQUENCE_MAIN, EVENT_TOUCH_DOWN);
1245                 }
1246                 if (notLauncher3 && getNavigationModel() != NavigationModel.THREE_BUTTON) {
1247                     expectEvent(TestProtocol.SEQUENCE_TIS, EVENT_TOUCH_DOWN_TIS);
1248                 }
1249                 break;
1250             case MotionEvent.ACTION_UP:
1251                 if (notLauncher3 && gestureScope != GestureScope.INSIDE
1252                         && (gestureScope == GestureScope.OUTSIDE_WITH_PILFER
1253                         || gestureScope == GestureScope.INSIDE_TO_OUTSIDE)) {
1254                     expectEvent(TestProtocol.SEQUENCE_PILFER, EVENT_PILFER_POINTERS);
1255                 }
1256                 if (gestureScope != GestureScope.OUTSIDE_WITH_PILFER
1257                         && gestureScope != GestureScope.OUTSIDE_WITHOUT_PILFER) {
1258                     expectEvent(TestProtocol.SEQUENCE_MAIN,
1259                             gestureScope == GestureScope.INSIDE
1260                                     || gestureScope == GestureScope.OUTSIDE_WITHOUT_PILFER
1261                                     ? EVENT_TOUCH_UP : EVENT_TOUCH_CANCEL);
1262                 }
1263                 if (notLauncher3 && getNavigationModel() != NavigationModel.THREE_BUTTON) {
1264                     expectEvent(TestProtocol.SEQUENCE_TIS, EVENT_TOUCH_UP_TIS);
1265                 }
1266                 break;
1267         }
1268 
1269         final MotionEvent event = getMotionEvent(downTime, currentTime, action, point.x, point.y);
1270         // b/190748682
1271         switch (action) {
1272             case MotionEvent.ACTION_DOWN:
1273             case MotionEvent.ACTION_UP:
1274                 log("b/190748682: injecting " + event);
1275                 break;
1276         }
1277         assertTrue("injectInputEvent failed",
1278                 mInstrumentation.getUiAutomation().injectInputEvent(event, true, false));
1279         event.recycle();
1280     }
1281 
movePointer(long downTime, long startTime, long duration, Point from, Point to, GestureScope gestureScope)1282     public long movePointer(long downTime, long startTime, long duration, Point from, Point to,
1283             GestureScope gestureScope) {
1284         log("movePointer: " + from + " to " + to);
1285         final Point point = new Point();
1286         long steps = duration / GESTURE_STEP_MS;
1287         long currentTime = startTime;
1288         for (long i = 0; i < steps; ++i) {
1289             sleep(GESTURE_STEP_MS);
1290 
1291             currentTime += GESTURE_STEP_MS;
1292             final float progress = (currentTime - startTime) / (float) duration;
1293 
1294             point.x = from.x + (int) (progress * (to.x - from.x));
1295             point.y = from.y + (int) (progress * (to.y - from.y));
1296 
1297             sendPointer(downTime, currentTime, MotionEvent.ACTION_MOVE, point, gestureScope);
1298         }
1299         return currentTime;
1300     }
1301 
getCurrentInteractionMode(Context context)1302     public static int getCurrentInteractionMode(Context context) {
1303         return getSystemIntegerRes(context, "config_navBarInteractionMode");
1304     }
1305 
1306     @NonNull
clickAndGet( @onNull final UiObject2 target, @NonNull String resName, Pattern longClickEvent)1307     UiObject2 clickAndGet(
1308             @NonNull final UiObject2 target, @NonNull String resName, Pattern longClickEvent) {
1309         final Point targetCenter = target.getVisibleCenter();
1310         final long downTime = SystemClock.uptimeMillis();
1311         sendPointer(downTime, downTime, MotionEvent.ACTION_DOWN, targetCenter, GestureScope.INSIDE);
1312         expectEvent(TestProtocol.SEQUENCE_MAIN, longClickEvent);
1313         final UiObject2 result = waitForLauncherObject(resName);
1314         sendPointer(downTime, SystemClock.uptimeMillis(), MotionEvent.ACTION_UP, targetCenter,
1315                 GestureScope.INSIDE);
1316         return result;
1317     }
1318 
getSystemIntegerRes(Context context, String resName)1319     private static int getSystemIntegerRes(Context context, String resName) {
1320         Resources res = context.getResources();
1321         int resId = res.getIdentifier(resName, "integer", "android");
1322 
1323         if (resId != 0) {
1324             return res.getInteger(resId);
1325         } else {
1326             Log.e(TAG, "Failed to get system resource ID. Incompatible framework version?");
1327             return -1;
1328         }
1329     }
1330 
getSystemDimensionResId(Context context, String resName)1331     private static int getSystemDimensionResId(Context context, String resName) {
1332         Resources res = context.getResources();
1333         int resId = res.getIdentifier(resName, "dimen", "android");
1334 
1335         if (resId != 0) {
1336             return resId;
1337         } else {
1338             Log.e(TAG, "Failed to get system resource ID. Incompatible framework version?");
1339             return -1;
1340         }
1341     }
1342 
sleep(int duration)1343     static void sleep(int duration) {
1344         SystemClock.sleep(duration);
1345     }
1346 
getEdgeSensitivityWidth()1347     int getEdgeSensitivityWidth() {
1348         try {
1349             final Context context = mInstrumentation.getTargetContext().createPackageContext(
1350                     getLauncherPackageName(), 0);
1351             return context.getResources().getDimensionPixelSize(
1352                     getSystemDimensionResId(context, "config_backGestureInset")) + 1;
1353         } catch (PackageManager.NameNotFoundException e) {
1354             fail("Can't get edge sensitivity: " + e);
1355             return 0;
1356         }
1357     }
1358 
getRealDisplaySize()1359     Point getRealDisplaySize() {
1360         final Point size = new Point();
1361         getContext().getSystemService(WindowManager.class).getDefaultDisplay().getRealSize(size);
1362         return size;
1363     }
1364 
enableDebugTracing()1365     public void enableDebugTracing() {
1366         getTestInfo(TestProtocol.REQUEST_ENABLE_DEBUG_TRACING);
1367     }
1368 
overviewShareEnabled()1369     boolean overviewShareEnabled() {
1370         return getTestInfo(TestProtocol.REQUEST_OVERVIEW_SHARE_ENABLED).getBoolean(
1371                 TestProtocol.TEST_INFO_RESPONSE_FIELD);
1372     }
1373 
overviewContentPushEnabled()1374     boolean overviewContentPushEnabled() {
1375         return getTestInfo(TestProtocol.REQUEST_OVERVIEW_CONTENT_PUSH_ENABLED).getBoolean(
1376                 TestProtocol.TEST_INFO_RESPONSE_FIELD);
1377     }
1378 
disableSensorRotation()1379     private void disableSensorRotation() {
1380         getTestInfo(TestProtocol.REQUEST_MOCK_SENSOR_ROTATION);
1381     }
1382 
disableDebugTracing()1383     public void disableDebugTracing() {
1384         getTestInfo(TestProtocol.REQUEST_DISABLE_DEBUG_TRACING);
1385     }
1386 
forceGc()1387     public void forceGc() {
1388         // GC the system & sysui first before gc'ing launcher
1389         logShellCommand("cmd statusbar run-gc");
1390         getTestInfo(TestProtocol.REQUEST_FORCE_GC);
1391     }
1392 
getPid()1393     public Integer getPid() {
1394         final Bundle testInfo = getTestInfo(TestProtocol.REQUEST_PID);
1395         return testInfo != null ? testInfo.getInt(TestProtocol.TEST_INFO_RESPONSE_FIELD) : null;
1396     }
1397 
produceViewLeak()1398     public void produceViewLeak() {
1399         getTestInfo(TestProtocol.REQUEST_VIEW_LEAK);
1400     }
1401 
getRecentTasks()1402     public ArrayList<ComponentName> getRecentTasks() {
1403         ArrayList<ComponentName> tasks = new ArrayList<>();
1404         ArrayList<String> components = getTestInfo(TestProtocol.REQUEST_RECENT_TASKS_LIST)
1405                 .getStringArrayList(TestProtocol.TEST_INFO_RESPONSE_FIELD);
1406         for (String s : components) {
1407             tasks.add(ComponentName.unflattenFromString(s));
1408         }
1409         return tasks;
1410     }
1411 
clearLauncherData()1412     public void clearLauncherData() {
1413         getTestInfo(TestProtocol.REQUEST_CLEAR_DATA);
1414     }
1415 
eventsCheck()1416     public Closable eventsCheck() {
1417         Assert.assertTrue("Nested event checking", mEventChecker == null);
1418         disableSensorRotation();
1419         final Integer initialPid = getPid();
1420         final LogEventChecker eventChecker = new LogEventChecker(this);
1421         if (eventChecker.start()) mEventChecker = eventChecker;
1422 
1423         return () -> {
1424             if (initialPid != null && initialPid.intValue() != getPid()) {
1425                 if (mOnLauncherCrashed != null) mOnLauncherCrashed.run();
1426                 checkForAnomaly();
1427                 Assert.fail(
1428                         formatSystemHealthMessage(
1429                                 formatErrorWithEvents("Launcher crashed", false)));
1430             }
1431 
1432             if (mEventChecker != null) {
1433                 mEventChecker = null;
1434                 if (mCheckEventsForSuccessfulGestures) {
1435                     final String message = eventChecker.verify(WAIT_TIME_MS, true);
1436                     if (message != null) {
1437                         dumpDiagnostics(message);
1438                         checkForAnomaly();
1439                         Assert.fail(formatSystemHealthMessage(
1440                                 "http://go/tapl : successful gesture produced " + message));
1441                     }
1442                 } else {
1443                     eventChecker.finishNoWait();
1444                 }
1445             }
1446         };
1447     }
1448 
1449     boolean isLauncher3() {
1450         if (mIsLauncher3 == null) {
1451             mIsLauncher3 = "com.android.launcher3".equals(getLauncherPackageName());
1452         }
1453         return mIsLauncher3;
1454     }
1455 
1456     void expectEvent(String sequence, Pattern expected) {
1457         if (mEventChecker != null) {
1458             mEventChecker.expectPattern(sequence, expected);
1459         } else {
1460             Log.d(TAG, "Expecting: " + sequence + " / " + expected);
1461         }
1462     }
1463 
1464     Rect getVisibleBounds(UiObject2 object) {
1465         try {
1466             return object.getVisibleBounds();
1467         } catch (StaleObjectException e) {
1468             fail("Object " + object + " disappeared from screen");
1469             return null;
1470         } catch (Throwable t) {
1471             fail(t.toString());
1472             return null;
1473         }
1474     }
1475 
1476     float getWindowCornerRadius() {
1477         final Resources resources = getResources();
1478         if (!supportsRoundedCornersOnWindows(resources)) {
1479             return 0f;
1480         }
1481 
1482         // Radius that should be used in case top or bottom aren't defined.
1483         float defaultRadius = ResourceUtils.getDimenByName("rounded_corner_radius", resources, 0);
1484 
1485         float topRadius = ResourceUtils.getDimenByName("rounded_corner_radius_top", resources, 0);
1486         if (topRadius == 0f) {
1487             topRadius = defaultRadius;
1488         }
1489         float bottomRadius = ResourceUtils.getDimenByName(
1490                 "rounded_corner_radius_bottom", resources, 0);
1491         if (bottomRadius == 0f) {
1492             bottomRadius = defaultRadius;
1493         }
1494 
1495         // Always use the smallest radius to make sure the rounded corners will
1496         // completely cover the display.
1497         return Math.min(topRadius, bottomRadius);
1498     }
1499 
1500     private static boolean supportsRoundedCornersOnWindows(Resources resources) {
1501         return ResourceUtils.getBoolByName(
1502                 "config_supportsRoundedCornersOnWindows", resources, false);
1503     }
1504 }