• 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 import static android.view.MotionEvent.AXIS_GESTURE_SWIPE_FINGER_COUNT;
24 
25 import static com.android.launcher3.tapl.Folder.FOLDER_CONTENT_RES_ID;
26 import static com.android.launcher3.tapl.TestHelpers.getOverviewPackageName;
27 import static com.android.launcher3.testing.shared.TestProtocol.NORMAL_STATE_ORDINAL;
28 
29 import android.app.ActivityManager;
30 import android.app.Instrumentation;
31 import android.app.UiAutomation;
32 import android.content.ComponentName;
33 import android.content.ContentProviderClient;
34 import android.content.ContentResolver;
35 import android.content.Context;
36 import android.content.pm.PackageManager;
37 import android.content.pm.ProviderInfo;
38 import android.content.res.Resources;
39 import android.graphics.Insets;
40 import android.graphics.Point;
41 import android.graphics.Rect;
42 import android.net.Uri;
43 import android.os.Bundle;
44 import android.os.DeadObjectException;
45 import android.os.Parcelable;
46 import android.os.RemoteException;
47 import android.os.SystemClock;
48 import android.text.TextUtils;
49 import android.util.Log;
50 import android.view.InputDevice;
51 import android.view.MotionEvent;
52 import android.view.ViewConfiguration;
53 import android.view.WindowManager;
54 import android.view.accessibility.AccessibilityEvent;
55 
56 import androidx.annotation.NonNull;
57 import androidx.annotation.Nullable;
58 import androidx.test.InstrumentationRegistry;
59 import androidx.test.uiautomator.By;
60 import androidx.test.uiautomator.BySelector;
61 import androidx.test.uiautomator.Configurator;
62 import androidx.test.uiautomator.Direction;
63 import androidx.test.uiautomator.StaleObjectException;
64 import androidx.test.uiautomator.UiDevice;
65 import androidx.test.uiautomator.UiObject2;
66 import androidx.test.uiautomator.Until;
67 
68 import com.android.launcher3.testing.shared.ResourceUtils;
69 import com.android.launcher3.testing.shared.TestInformationRequest;
70 import com.android.launcher3.testing.shared.TestProtocol;
71 import com.android.systemui.shared.system.QuickStepContract;
72 
73 import org.junit.Assert;
74 
75 import java.io.ByteArrayOutputStream;
76 import java.io.IOException;
77 import java.lang.ref.WeakReference;
78 import java.util.ArrayList;
79 import java.util.Arrays;
80 import java.util.Deque;
81 import java.util.LinkedList;
82 import java.util.List;
83 import java.util.Optional;
84 import java.util.concurrent.TimeoutException;
85 import java.util.function.BooleanSupplier;
86 import java.util.function.Consumer;
87 import java.util.function.Function;
88 import java.util.function.Supplier;
89 import java.util.regex.Pattern;
90 import java.util.stream.Collectors;
91 
92 /**
93  * The main tapl object. The only object that can be explicitly constructed by the using code. It
94  * produces all other objects.
95  */
96 public final class LauncherInstrumentation {
97 
98     private static final String TAG = "Tapl";
99     private static final int ZERO_BUTTON_STEPS_FROM_BACKGROUND_TO_HOME = 15;
100     private static final int GESTURE_STEP_MS = 16;
101 
102     static final Pattern EVENT_PILFER_POINTERS = Pattern.compile("pilferPointers");
103     static final Pattern EVENT_START = Pattern.compile("start:");
104 
105     private static final Pattern EVENT_KEY_BACK_DOWN =
106             getKeyEventPattern("ACTION_DOWN", "KEYCODE_BACK");
107     private static final Pattern EVENT_KEY_BACK_UP =
108             getKeyEventPattern("ACTION_UP", "KEYCODE_BACK");
109     private static final Pattern EVENT_ON_BACK_INVOKED = Pattern.compile("onBackInvoked");
110 
111     private final String mLauncherPackage;
112     private Boolean mIsLauncher3;
113     private long mTestStartTime = -1;
114 
115     // Types for launcher containers that the user is interacting with. "Background" is a
116     // pseudo-container corresponding to inactive launcher covered by another app.
117     public enum ContainerType {
118         WORKSPACE, HOME_ALL_APPS, OVERVIEW, SPLIT_SCREEN_SELECT, WIDGETS, FALLBACK_OVERVIEW,
119         LAUNCHED_APP, TASKBAR_ALL_APPS
120     }
121 
122     public enum NavigationModel {ZERO_BUTTON, THREE_BUTTON}
123 
124     // Defines whether the gesture recognition triggers pilfer.
125     public enum GestureScope {
126         DONT_EXPECT_PILFER,
127         EXPECT_PILFER,
128     }
129 
130     public enum TrackpadGestureType {
131         NONE,
132         TWO_FINGER,
133         THREE_FINGER,
134         FOUR_FINGER
135     }
136 
137     // Base class for launcher containers.
138     abstract static class VisibleContainer {
139         protected final LauncherInstrumentation mLauncher;
140 
VisibleContainer(LauncherInstrumentation launcher)141         protected VisibleContainer(LauncherInstrumentation launcher) {
142             mLauncher = launcher;
143             launcher.setActiveContainer(this);
144         }
145 
getContainerType()146         protected abstract ContainerType getContainerType();
147 
148         /**
149          * Asserts that the launcher is in the mode matching 'this' object.
150          *
151          * @return UI object for the container.
152          */
verifyActiveContainer()153         final UiObject2 verifyActiveContainer() {
154             mLauncher.assertTrue("Attempt to use a stale container",
155                     this == sActiveContainer.get());
156             return mLauncher.verifyContainerType(getContainerType());
157         }
158     }
159 
160     public interface Closable extends AutoCloseable {
close()161         void close();
162     }
163 
164     static final String WORKSPACE_RES_ID = "workspace";
165     private static final String APPS_RES_ID = "apps_view";
166     private static final String OVERVIEW_RES_ID = "overview_panel";
167     private static final String WIDGETS_RES_ID = "primary_widgets_list_view";
168     private static final String CONTEXT_MENU_RES_ID = "popup_container";
169     private static final String OPEN_FOLDER_RES_ID = "folder_content";
170     static final String TASKBAR_RES_ID = "taskbar_view";
171     private static final String SPLIT_PLACEHOLDER_RES_ID = "split_placeholder";
172     public static final int WAIT_TIME_MS = 30000;
173     static final long DEFAULT_POLL_INTERVAL = 1000;
174     private static final String SYSTEMUI_PACKAGE = "com.android.systemui";
175     private static final String ANDROID_PACKAGE = "android";
176 
177     private static WeakReference<VisibleContainer> sActiveContainer = new WeakReference<>(null);
178 
179     private final UiDevice mDevice;
180     private final Instrumentation mInstrumentation;
181     private Integer mExpectedRotation = null;
182     private boolean mExpectedRotationCheckEnabled = true;
183     private final Uri mTestProviderUri;
184     private final Deque<String> mDiagnosticContext = new LinkedList<>();
185     private Function<Long, String> mSystemHealthSupplier;
186 
187     private boolean mIgnoreTaskbarVisibility = false;
188 
189     private Consumer<ContainerType> mOnSettledStateAction;
190 
191     private LogEventChecker mEventChecker;
192 
193     private boolean mCheckEventsForSuccessfulGestures = false;
194     private Runnable mOnLauncherCrashed;
195 
196     private TrackpadGestureType mTrackpadGestureType = TrackpadGestureType.NONE;
197     private int mPointerCount = 0;
198 
getKeyEventPattern(String action, String keyCode)199     private static Pattern getKeyEventPattern(String action, String keyCode) {
200         return Pattern.compile("Key event: KeyEvent.*action=" + action + ".*keyCode=" + keyCode);
201     }
202 
203     /**
204      * Constructs the root of TAPL hierarchy. You get all other objects from it.
205      */
LauncherInstrumentation()206     public LauncherInstrumentation() {
207         this(InstrumentationRegistry.getInstrumentation());
208     }
209 
210     /**
211      * Constructs the root of TAPL hierarchy. You get all other objects from it.
212      * Deprecated: use the constructor without parameters instead.
213      */
214     @Deprecated
LauncherInstrumentation(Instrumentation instrumentation)215     public LauncherInstrumentation(Instrumentation instrumentation) {
216         mInstrumentation = instrumentation;
217         mDevice = UiDevice.getInstance(instrumentation);
218 
219         // Launcher should run in test harness so that custom accessibility protocol between
220         // Launcher and TAPL is enabled. In-process tests enable this protocol with a direct call
221         // into Launcher.
222         assertTrue("Device must run in a test harness. "
223                         + "Run `adb shell setprop ro.test_harness 1` to enable it.",
224                 TestHelpers.isInLauncherProcess() || ActivityManager.isRunningInTestHarness());
225 
226         final String testPackage = getContext().getPackageName();
227         final String targetPackage = mInstrumentation.getTargetContext().getPackageName();
228 
229         // Launcher package. As during inproc tests the tested launcher may not be selected as the
230         // current launcher, choosing target package for inproc. For out-of-proc, use the installed
231         // launcher package.
232         mLauncherPackage = testPackage.equals(targetPackage) || isGradleInstrumentation()
233                 ? getLauncherPackageName()
234                 : targetPackage;
235 
236         String testProviderAuthority = mLauncherPackage + ".TestInfo";
237         mTestProviderUri = new Uri.Builder()
238                 .scheme(ContentResolver.SCHEME_CONTENT)
239                 .authority(testProviderAuthority)
240                 .build();
241 
242         mInstrumentation.getUiAutomation().grantRuntimePermission(
243                 testPackage, "android.permission.WRITE_SECURE_SETTINGS");
244 
245         PackageManager pm = getContext().getPackageManager();
246         ProviderInfo pi = pm.resolveContentProvider(
247                 testProviderAuthority, MATCH_ALL | MATCH_DISABLED_COMPONENTS);
248         assertNotNull("Cannot find content provider for " + testProviderAuthority, pi);
249         ComponentName cn = new ComponentName(pi.packageName, pi.name);
250 
251         if (pm.getComponentEnabledSetting(cn) != COMPONENT_ENABLED_STATE_ENABLED) {
252             if (TestHelpers.isInLauncherProcess()) {
253                 pm.setComponentEnabledSetting(cn, COMPONENT_ENABLED_STATE_ENABLED, DONT_KILL_APP);
254                 // b/195031154
255                 SystemClock.sleep(5000);
256             } else {
257                 try {
258                     final int userId = getContext().getUserId();
259                     final String launcherPidCommand = "pidof " + pi.packageName;
260                     final String initialPid = mDevice.executeShellCommand(launcherPidCommand)
261                             .replaceAll("\\s", "");
262                     mDevice.executeShellCommand(
263                             "pm enable --user " + userId + " " + cn.flattenToString());
264                     // Wait for Launcher restart after enabling test provider.
265                     for (int i = 0; i < 100; ++i) {
266                         final String currentPid = mDevice.executeShellCommand(launcherPidCommand)
267                                 .replaceAll("\\s", "");
268                         if (!currentPid.isEmpty() && !currentPid.equals(initialPid)) break;
269                         if (i == 99) fail("Launcher didn't restart after enabling test provider");
270                         SystemClock.sleep(100);
271                     }
272                 } catch (IOException e) {
273                     fail(e.toString());
274                 }
275             }
276         }
277     }
278 
279     /**
280      * Gradle only supports out of process instrumentation. The test package is automatically
281      * generated by appending `.test` to the target package.
282      */
isGradleInstrumentation()283     private boolean isGradleInstrumentation() {
284         final String testPackage = getContext().getPackageName();
285         final String targetPackage = mInstrumentation.getTargetContext().getPackageName();
286         final String testSuffix = ".test";
287 
288         return testPackage.endsWith(testSuffix) && testPackage.length() > testSuffix.length()
289                 && testPackage.substring(0, testPackage.length() - testSuffix.length())
290                 .equals(targetPackage);
291     }
292 
enableCheckEventsForSuccessfulGestures()293     public void enableCheckEventsForSuccessfulGestures() {
294         mCheckEventsForSuccessfulGestures = true;
295     }
296 
setOnLauncherCrashed(Runnable onLauncherCrashed)297     public void setOnLauncherCrashed(Runnable onLauncherCrashed) {
298         mOnLauncherCrashed = onLauncherCrashed;
299     }
300 
getContext()301     Context getContext() {
302         return mInstrumentation.getContext();
303     }
304 
getTestInfo(String request)305     Bundle getTestInfo(String request) {
306         return getTestInfo(request, /*arg=*/ null);
307     }
308 
getTestInfo(String request, String arg)309     Bundle getTestInfo(String request, String arg) {
310         return getTestInfo(request, arg, null);
311     }
312 
getTestInfo(String request, String arg, Bundle extra)313     Bundle getTestInfo(String request, String arg, Bundle extra) {
314         try (ContentProviderClient client = getContext().getContentResolver()
315                 .acquireContentProviderClient(mTestProviderUri)) {
316             return client.call(request, arg, extra);
317         } catch (DeadObjectException e) {
318             fail("Launcher crashed");
319             return null;
320         } catch (RemoteException e) {
321             throw new RuntimeException(e);
322         }
323     }
324 
getTestInfo(TestInformationRequest request)325     Bundle getTestInfo(TestInformationRequest request) {
326         Bundle extra = new Bundle();
327         extra.putParcelable(TestProtocol.TEST_INFO_REQUEST_FIELD, request);
328         return getTestInfo(request.getRequestName(), null, extra);
329     }
330 
getTargetInsets()331     Insets getTargetInsets() {
332         return getTestInfo(TestProtocol.REQUEST_TARGET_INSETS)
333                 .getParcelable(TestProtocol.TEST_INFO_RESPONSE_FIELD);
334     }
335 
getWindowInsets()336     Insets getWindowInsets() {
337         return getTestInfo(TestProtocol.REQUEST_WINDOW_INSETS)
338                 .getParcelable(TestProtocol.TEST_INFO_RESPONSE_FIELD);
339     }
340 
isTablet()341     public boolean isTablet() {
342         return getTestInfo(TestProtocol.REQUEST_IS_TABLET)
343                 .getBoolean(TestProtocol.TEST_INFO_RESPONSE_FIELD);
344     }
345 
isTwoPanels()346     public boolean isTwoPanels() {
347         return getTestInfo(TestProtocol.REQUEST_IS_TWO_PANELS)
348                 .getBoolean(TestProtocol.TEST_INFO_RESPONSE_FIELD);
349     }
350 
getFocusedTaskHeightForTablet()351     int getFocusedTaskHeightForTablet() {
352         return getTestInfo(TestProtocol.REQUEST_GET_FOCUSED_TASK_HEIGHT_FOR_TABLET).getInt(
353                 TestProtocol.TEST_INFO_RESPONSE_FIELD);
354     }
355 
getGridTaskRectForTablet()356     Rect getGridTaskRectForTablet() {
357         return ((Rect) getTestInfo(TestProtocol.REQUEST_GET_GRID_TASK_SIZE_RECT_FOR_TABLET)
358                 .getParcelable(TestProtocol.TEST_INFO_RESPONSE_FIELD));
359     }
360 
getOverviewPageSpacing()361     int getOverviewPageSpacing() {
362         return getTestInfo(TestProtocol.REQUEST_GET_OVERVIEW_PAGE_SPACING)
363                 .getInt(TestProtocol.TEST_INFO_RESPONSE_FIELD);
364     }
365 
getExactScreenCenterX()366     float getExactScreenCenterX() {
367         return getRealDisplaySize().x / 2f;
368     }
369 
setEnableRotation(boolean on)370     public void setEnableRotation(boolean on) {
371         getTestInfo(TestProtocol.REQUEST_ENABLE_ROTATION, Boolean.toString(on));
372     }
373 
setEnableSuggestion(boolean enableSuggestion)374     public void setEnableSuggestion(boolean enableSuggestion) {
375         getTestInfo(TestProtocol.REQUEST_ENABLE_SUGGESTION, Boolean.toString(enableSuggestion));
376     }
377 
hadNontestEvents()378     public boolean hadNontestEvents() {
379         return getTestInfo(TestProtocol.REQUEST_GET_HAD_NONTEST_EVENTS)
380                 .getBoolean(TestProtocol.TEST_INFO_RESPONSE_FIELD);
381     }
382 
setActiveContainer(VisibleContainer container)383     void setActiveContainer(VisibleContainer container) {
384         sActiveContainer = new WeakReference<>(container);
385     }
386 
387     /**
388      * Sets the accesibility interactive timeout to be effectively indefinite (UI using this
389      * accesibility timeout will not automatically dismiss if true).
390      */
setIndefiniteAccessibilityInteractiveUiTimeout(boolean indefiniteTimeout)391     void setIndefiniteAccessibilityInteractiveUiTimeout(boolean indefiniteTimeout) {
392         final String cmd = indefiniteTimeout
393                 ? "settings put secure accessibility_interactive_ui_timeout_ms 10000"
394                 : "settings delete secure accessibility_interactive_ui_timeout_ms";
395         logShellCommand(cmd);
396     }
397 
getNavigationModel()398     public NavigationModel getNavigationModel() {
399         final Context baseContext = mInstrumentation.getTargetContext();
400         try {
401             // Workaround, use constructed context because both the instrumentation context and the
402             // app context are not constructed with resources that take overlays into account
403             final Context ctx = baseContext.createPackageContext(getLauncherPackageName(), 0);
404             for (int i = 0; i < 100; ++i) {
405                 final int currentInteractionMode = getCurrentInteractionMode(ctx);
406                 final NavigationModel model = getNavigationModel(currentInteractionMode);
407                 log("Interaction mode = " + currentInteractionMode + " (" + model + ")");
408                 if (model != null) return model;
409                 Thread.sleep(100);
410             }
411             fail("Can't detect navigation mode");
412         } catch (Exception e) {
413             fail(e.toString());
414         }
415         return NavigationModel.THREE_BUTTON;
416     }
417 
getNavigationModel(int currentInteractionMode)418     public static NavigationModel getNavigationModel(int currentInteractionMode) {
419         if (QuickStepContract.isGesturalMode(currentInteractionMode)) {
420             return NavigationModel.ZERO_BUTTON;
421         } else if (QuickStepContract.isLegacyMode(currentInteractionMode)) {
422             return NavigationModel.THREE_BUTTON;
423         }
424         return null;
425     }
426 
log(String message)427     static void log(String message) {
428         Log.d(TAG, message);
429     }
430 
addContextLayer(String piece)431     Closable addContextLayer(String piece) {
432         mDiagnosticContext.addLast(piece);
433         log("Entering context: " + piece);
434         return () -> {
435             log("Leaving context: " + piece);
436             mDiagnosticContext.removeLast();
437         };
438     }
439 
dumpViewHierarchy()440     public void dumpViewHierarchy() {
441         final ByteArrayOutputStream stream = new ByteArrayOutputStream();
442         try {
443             mDevice.dumpWindowHierarchy(stream);
444             stream.flush();
445             stream.close();
446             for (String line : stream.toString().split("\\r?\\n")) {
447                 Log.e(TAG, line.trim());
448             }
449         } catch (IOException e) {
450             Log.e(TAG, "error dumping XML to logcat", e);
451         }
452     }
453 
getSystemAnomalyMessage( boolean ignoreNavmodeChangeStates, boolean ignoreOnlySystemUiViews)454     public String getSystemAnomalyMessage(
455             boolean ignoreNavmodeChangeStates, boolean ignoreOnlySystemUiViews) {
456         try {
457             {
458                 final StringBuilder sb = new StringBuilder();
459 
460                 UiObject2 object =
461                         mDevice.findObject(By.res("android", "alertTitle").pkg("android"));
462                 if (object != null) {
463                     sb.append("TITLE: ").append(object.getText());
464                 }
465 
466                 object = mDevice.findObject(By.res("android", "message").pkg("android"));
467                 if (object != null) {
468                     sb.append(" PACKAGE: ").append(object.getApplicationPackage())
469                             .append(" MESSAGE: ").append(object.getText());
470                 }
471 
472                 if (sb.length() != 0) {
473                     return "System alert popup is visible: " + sb;
474                 }
475             }
476 
477             if (hasSystemUiObject("keyguard_status_view")) return "Phone is locked";
478 
479             if (!ignoreOnlySystemUiViews) {
480                 final String visibleApps = mDevice.findObjects(getAnyObjectSelector())
481                         .stream()
482                         .map(LauncherInstrumentation::getApplicationPackageSafe)
483                         .distinct()
484                         .filter(pkg -> pkg != null)
485                         .collect(Collectors.joining(","));
486                 if (SYSTEMUI_PACKAGE.equals(visibleApps)) return "Only System UI views are visible";
487             }
488             if (!ignoreNavmodeChangeStates) {
489                 if (!mDevice.wait(Until.hasObject(getAnyObjectSelector()), WAIT_TIME_MS)) {
490                     return "Screen is empty";
491                 }
492             }
493 
494             final String navigationModeError = getNavigationModeMismatchError(true);
495             if (navigationModeError != null) return navigationModeError;
496         } catch (Throwable e) {
497             Log.w(TAG, "getSystemAnomalyMessage failed", e);
498         }
499 
500         return null;
501     }
502 
checkForAnomaly()503     private void checkForAnomaly() {
504         checkForAnomaly(false, false);
505     }
506 
checkForAnomaly( boolean ignoreNavmodeChangeStates, boolean ignoreOnlySystemUiViews)507     public void checkForAnomaly(
508             boolean ignoreNavmodeChangeStates, boolean ignoreOnlySystemUiViews) {
509         final String systemAnomalyMessage =
510                 getSystemAnomalyMessage(ignoreNavmodeChangeStates, ignoreOnlySystemUiViews);
511         if (systemAnomalyMessage != null) {
512             Assert.fail(formatSystemHealthMessage(formatErrorWithEvents(
513                     "http://go/tapl : Tests are broken by a non-Launcher system error: "
514                             + systemAnomalyMessage, false)));
515         }
516     }
517 
getVisiblePackages()518     private String getVisiblePackages() {
519         final String apps = mDevice.findObjects(getAnyObjectSelector())
520                 .stream()
521                 .map(LauncherInstrumentation::getApplicationPackageSafe)
522                 .distinct()
523                 .filter(pkg -> pkg != null && !SYSTEMUI_PACKAGE.equals(pkg))
524                 .collect(Collectors.joining(", "));
525         return !apps.isEmpty()
526                 ? "active app: " + apps
527                 : "the test doesn't see views from any app, including Launcher";
528     }
529 
getApplicationPackageSafe(UiObject2 object)530     private static String getApplicationPackageSafe(UiObject2 object) {
531         try {
532             return object.getApplicationPackage();
533         } catch (StaleObjectException e) {
534             // We are looking at all object in the system; external ones can suddenly go away.
535             return null;
536         }
537     }
538 
getVisibleStateMessage()539     private String getVisibleStateMessage() {
540         if (hasLauncherObject(CONTEXT_MENU_RES_ID)) return "Context Menu";
541         if (hasLauncherObject(OPEN_FOLDER_RES_ID)) return "Open Folder";
542         if (hasLauncherObject(WIDGETS_RES_ID)) return "Widgets";
543         if (hasSystemLauncherObject(OVERVIEW_RES_ID)) return "Overview";
544         if (hasLauncherObject(WORKSPACE_RES_ID)) return "Workspace";
545         if (hasLauncherObject(APPS_RES_ID)) return "AllApps";
546         return "LaunchedApp (" + getVisiblePackages() + ")";
547     }
548 
setSystemHealthSupplier(Function<Long, String> supplier)549     public void setSystemHealthSupplier(Function<Long, String> supplier) {
550         this.mSystemHealthSupplier = supplier;
551     }
552 
setOnSettledStateAction(Consumer<ContainerType> onSettledStateAction)553     public void setOnSettledStateAction(Consumer<ContainerType> onSettledStateAction) {
554         mOnSettledStateAction = onSettledStateAction;
555     }
556 
onTestStart()557     public void onTestStart() {
558         mTestStartTime = System.currentTimeMillis();
559     }
560 
onTestFinish()561     public void onTestFinish() {
562         mTestStartTime = -1;
563     }
564 
formatSystemHealthMessage(String message)565     private String formatSystemHealthMessage(String message) {
566         final String testPackage = getContext().getPackageName();
567 
568         mInstrumentation.getUiAutomation().grantRuntimePermission(
569                 testPackage, "android.permission.READ_LOGS");
570         mInstrumentation.getUiAutomation().grantRuntimePermission(
571                 testPackage, "android.permission.PACKAGE_USAGE_STATS");
572 
573         if (mTestStartTime > 0) {
574             final String systemHealth = mSystemHealthSupplier != null
575                     ? mSystemHealthSupplier.apply(mTestStartTime)
576                     : TestHelpers.getSystemHealthMessage(getContext(), mTestStartTime);
577 
578             if (systemHealth != null) {
579                 message += ";\nPerhaps linked to system health problems:\n<<<<<<<<<<<<<<<<<<\n"
580                         + systemHealth + "\n>>>>>>>>>>>>>>>>>>";
581             }
582         }
583         Log.d(TAG, "About to throw the error: " + message, new Exception());
584         return message;
585     }
586 
formatErrorWithEvents(String message, boolean checkEvents)587     private String formatErrorWithEvents(String message, boolean checkEvents) {
588         if (mEventChecker != null) {
589             final LogEventChecker eventChecker = mEventChecker;
590             mEventChecker = null;
591             if (checkEvents) {
592                 final String eventMismatch = eventChecker.verify(0, false);
593                 if (eventMismatch != null) {
594                     message = message + ";\n" + eventMismatch;
595                 }
596             } else {
597                 eventChecker.finishNoWait();
598             }
599         }
600 
601         dumpDiagnostics(message);
602 
603         log("Hierarchy dump for: " + message);
604         dumpViewHierarchy();
605 
606         return message;
607     }
608 
dumpDiagnostics(String message)609     private void dumpDiagnostics(String message) {
610         log("Diagnostics for failure: " + message);
611         log("Input:");
612         logShellCommand("dumpsys input");
613         log("TIS:");
614         logShellCommand("dumpsys activity service TouchInteractionService");
615     }
616 
logShellCommand(String command)617     private void logShellCommand(String command) {
618         try {
619             for (String line : mDevice.executeShellCommand(command).split("\\n")) {
620                 SystemClock.sleep(10);
621                 log(line);
622             }
623         } catch (IOException e) {
624             log("Failed to execute " + command);
625         }
626     }
627 
fail(String message)628     void fail(String message) {
629         checkForAnomaly();
630         Assert.fail(formatSystemHealthMessage(formatErrorWithEvents(
631                 "http://go/tapl test failure: " + message + ";\nContext: " + getContextDescription()
632                         + "; now visible state is " + getVisibleStateMessage(), true)));
633     }
634 
getContextDescription()635     private String getContextDescription() {
636         return mDiagnosticContext.isEmpty()
637                 ? "(no context)" : String.join(", ", mDiagnosticContext);
638     }
639 
assertTrue(String message, boolean condition)640     void assertTrue(String message, boolean condition) {
641         if (!condition) {
642             fail(message);
643         }
644     }
645 
assertNotNull(String message, Object object)646     void assertNotNull(String message, Object object) {
647         assertTrue(message, object != null);
648     }
649 
failEquals(String message, Object actual)650     private void failEquals(String message, Object actual) {
651         fail(message + ". " + "Actual: " + actual);
652     }
653 
assertEquals(String message, int expected, int actual)654     private void assertEquals(String message, int expected, int actual) {
655         if (expected != actual) {
656             fail(message + " expected: " + expected + " but was: " + actual);
657         }
658     }
659 
assertEquals(String message, String expected, String actual)660     void assertEquals(String message, String expected, String actual) {
661         if (!TextUtils.equals(expected, actual)) {
662             fail(message + " expected: '" + expected + "' but was: '" + actual + "'");
663         }
664     }
665 
assertEquals(String message, long expected, long actual)666     void assertEquals(String message, long expected, long actual) {
667         if (expected != actual) {
668             fail(message + " expected: " + expected + " but was: " + actual);
669         }
670     }
671 
assertNotEquals(String message, int unexpected, int actual)672     void assertNotEquals(String message, int unexpected, int actual) {
673         if (unexpected == actual) {
674             failEquals(message, actual);
675         }
676     }
677 
678     /**
679      * Whether to ignore verifying the task bar visibility during instrumenting.
680      *
681      * @param ignoreTaskbarVisibility {@code true} will ignore the instrumentation implicitly
682      *                                verifying the task bar visibility with
683      *                                {@link VisibleContainer#verifyActiveContainer}.
684      *                                {@code false} otherwise.
685      */
setIgnoreTaskbarVisibility(boolean ignoreTaskbarVisibility)686     public void setIgnoreTaskbarVisibility(boolean ignoreTaskbarVisibility) {
687         mIgnoreTaskbarVisibility = ignoreTaskbarVisibility;
688     }
689 
690     /**
691      * Set the trackpad gesture type of the interaction.
692      * @param trackpadGestureType whether it's not from trackpad, two-finger, three-finger, or
693      *                            four-finger gesture.
694      */
setTrackpadGestureType(TrackpadGestureType trackpadGestureType)695     public void setTrackpadGestureType(TrackpadGestureType trackpadGestureType) {
696         mTrackpadGestureType = trackpadGestureType;
697     }
698 
getTrackpadGestureType()699     TrackpadGestureType getTrackpadGestureType() {
700         return mTrackpadGestureType;
701     }
702 
703     /**
704      * Sets expected rotation.
705      * TAPL periodically checks that Launcher didn't suddenly change the rotation to unexpected one.
706      * Null parameter disables checks. The initial state is "no checks".
707      */
setExpectedRotation(Integer expectedRotation)708     public void setExpectedRotation(Integer expectedRotation) {
709         mExpectedRotation = expectedRotation;
710     }
711 
setExpectedRotationCheckEnabled(boolean expectedRotationCheckEnabled)712     public void setExpectedRotationCheckEnabled(boolean expectedRotationCheckEnabled) {
713         mExpectedRotationCheckEnabled = expectedRotationCheckEnabled;
714     }
715 
getNavigationModeMismatchError(boolean waitForCorrectState)716     public String getNavigationModeMismatchError(boolean waitForCorrectState) {
717         final int waitTime = waitForCorrectState ? WAIT_TIME_MS : 0;
718         final NavigationModel navigationModel = getNavigationModel();
719         String resPackage = getNavigationButtonResPackage();
720         if (navigationModel == NavigationModel.THREE_BUTTON) {
721             if (!mDevice.wait(Until.hasObject(By.res(resPackage, "recent_apps")), waitTime)) {
722                 return "Recents button not present in 3-button mode";
723             }
724         } else {
725             if (!mDevice.wait(Until.gone(By.res(resPackage, "recent_apps")), waitTime)) {
726                 return "Recents button is present in non-3-button mode";
727             }
728         }
729 
730         if (navigationModel == NavigationModel.ZERO_BUTTON) {
731             if (!mDevice.wait(Until.gone(By.res(resPackage, "home")), waitTime)) {
732                 return "Home button is present in gestural mode";
733             }
734         } else {
735             if (!mDevice.wait(Until.hasObject(By.res(resPackage, "home")), waitTime)) {
736                 return "Home button not present in non-gestural mode";
737             }
738         }
739         return null;
740     }
741 
getNavigationButtonResPackage()742     private String getNavigationButtonResPackage() {
743         return isTablet() ? getLauncherPackageName() : SYSTEMUI_PACKAGE;
744     }
745 
verifyContainerType(ContainerType containerType)746     private UiObject2 verifyContainerType(ContainerType containerType) {
747         waitForLauncherInitialized();
748 
749         if (mExpectedRotationCheckEnabled && mExpectedRotation != null) {
750             assertEquals("Unexpected display rotation",
751                     mExpectedRotation, mDevice.getDisplayRotation());
752         }
753 
754         final String error = getNavigationModeMismatchError(true);
755         assertTrue(error, error == null);
756 
757         log("verifyContainerType: " + containerType);
758 
759         final UiObject2 container = verifyVisibleObjects(containerType);
760 
761         if (mOnSettledStateAction != null) mOnSettledStateAction.accept(containerType);
762 
763         return container;
764     }
765 
verifyVisibleObjects(ContainerType containerType)766     private UiObject2 verifyVisibleObjects(ContainerType containerType) {
767         try (Closable c = addContextLayer(
768                 "but the current state is not " + containerType.name())) {
769             switch (containerType) {
770                 case WORKSPACE: {
771                     waitUntilLauncherObjectGone(APPS_RES_ID);
772                     waitUntilLauncherObjectGone(WIDGETS_RES_ID);
773                     waitUntilSystemLauncherObjectGone(OVERVIEW_RES_ID);
774                     waitUntilSystemLauncherObjectGone(SPLIT_PLACEHOLDER_RES_ID);
775 
776                     if (is3PLauncher() && isTablet()) {
777                         waitForSystemLauncherObject(TASKBAR_RES_ID);
778                     } else {
779                         waitUntilSystemLauncherObjectGone(TASKBAR_RES_ID);
780                     }
781 
782                     return waitForLauncherObject(WORKSPACE_RES_ID);
783                 }
784                 case WIDGETS: {
785                     waitUntilLauncherObjectGone(WORKSPACE_RES_ID);
786                     waitUntilLauncherObjectGone(APPS_RES_ID);
787                     waitUntilSystemLauncherObjectGone(OVERVIEW_RES_ID);
788                     waitUntilSystemLauncherObjectGone(SPLIT_PLACEHOLDER_RES_ID);
789 
790                     if (is3PLauncher() && isTablet()) {
791                         waitForSystemLauncherObject(TASKBAR_RES_ID);
792                     } else {
793                         waitUntilSystemLauncherObjectGone(TASKBAR_RES_ID);
794                     }
795 
796                     return waitForLauncherObject(WIDGETS_RES_ID);
797                 }
798                 case TASKBAR_ALL_APPS: {
799                     waitUntilLauncherObjectGone(WORKSPACE_RES_ID);
800                     waitUntilLauncherObjectGone(WIDGETS_RES_ID);
801                     waitUntilSystemLauncherObjectGone(OVERVIEW_RES_ID);
802                     waitUntilSystemLauncherObjectGone(TASKBAR_RES_ID);
803                     waitUntilSystemLauncherObjectGone(SPLIT_PLACEHOLDER_RES_ID);
804 
805                     return waitForLauncherObject(APPS_RES_ID);
806                 }
807                 case HOME_ALL_APPS: {
808                     waitUntilLauncherObjectGone(WORKSPACE_RES_ID);
809                     waitUntilLauncherObjectGone(WIDGETS_RES_ID);
810                     waitUntilSystemLauncherObjectGone(OVERVIEW_RES_ID);
811                     waitUntilSystemLauncherObjectGone(SPLIT_PLACEHOLDER_RES_ID);
812 
813                     if (is3PLauncher() && isTablet()) {
814                         waitForSystemLauncherObject(TASKBAR_RES_ID);
815                     } else {
816                         waitUntilSystemLauncherObjectGone(TASKBAR_RES_ID);
817                     }
818 
819                     return waitForLauncherObject(APPS_RES_ID);
820                 }
821                 case OVERVIEW:
822                 case FALLBACK_OVERVIEW: {
823                     waitUntilLauncherObjectGone(APPS_RES_ID);
824                     waitUntilLauncherObjectGone(WORKSPACE_RES_ID);
825                     waitUntilLauncherObjectGone(WIDGETS_RES_ID);
826                     if (isTablet()) {
827                         waitForSystemLauncherObject(TASKBAR_RES_ID);
828                     } else {
829                         waitUntilSystemLauncherObjectGone(TASKBAR_RES_ID);
830                     }
831                     waitUntilSystemLauncherObjectGone(SPLIT_PLACEHOLDER_RES_ID);
832 
833                     return waitForSystemLauncherObject(OVERVIEW_RES_ID);
834                 }
835                 case SPLIT_SCREEN_SELECT: {
836                     waitUntilLauncherObjectGone(APPS_RES_ID);
837                     waitUntilLauncherObjectGone(WORKSPACE_RES_ID);
838                     waitUntilLauncherObjectGone(WIDGETS_RES_ID);
839                     if (isTablet()) {
840                         waitForSystemLauncherObject(TASKBAR_RES_ID);
841                     } else {
842                         waitUntilSystemLauncherObjectGone(TASKBAR_RES_ID);
843                     }
844 
845                     waitForSystemLauncherObject(SPLIT_PLACEHOLDER_RES_ID);
846                     return waitForSystemLauncherObject(OVERVIEW_RES_ID);
847                 }
848                 case LAUNCHED_APP: {
849                     waitUntilLauncherObjectGone(WORKSPACE_RES_ID);
850                     waitUntilLauncherObjectGone(APPS_RES_ID);
851                     waitUntilLauncherObjectGone(WIDGETS_RES_ID);
852                     waitUntilSystemLauncherObjectGone(OVERVIEW_RES_ID);
853                     waitUntilSystemLauncherObjectGone(SPLIT_PLACEHOLDER_RES_ID);
854 
855                     if (mIgnoreTaskbarVisibility) {
856                         return null;
857                     }
858 
859                     if (isTablet()) {
860                         waitForSystemLauncherObject(TASKBAR_RES_ID);
861                     } else {
862                         waitUntilSystemLauncherObjectGone(TASKBAR_RES_ID);
863                     }
864                     return null;
865                 }
866                 default:
867                     fail("Invalid state: " + containerType);
868                     return null;
869             }
870         }
871     }
872 
waitForModelQueueCleared()873     public void waitForModelQueueCleared() {
874         getTestInfo(TestProtocol.REQUEST_MODEL_QUEUE_CLEARED);
875     }
876 
waitForLauncherInitialized()877     public void waitForLauncherInitialized() {
878         for (int i = 0; i < 100; ++i) {
879             if (getTestInfo(
880                     TestProtocol.REQUEST_IS_LAUNCHER_INITIALIZED).
881                     getBoolean(TestProtocol.TEST_INFO_RESPONSE_FIELD)) {
882                 return;
883             }
884             SystemClock.sleep(100);
885         }
886         checkForAnomaly();
887         fail("Launcher didn't initialize");
888     }
889 
executeAndWaitForLauncherEvent(Runnable command, UiAutomation.AccessibilityEventFilter eventFilter, Supplier<String> message, String actionName)890     Parcelable executeAndWaitForLauncherEvent(Runnable command,
891             UiAutomation.AccessibilityEventFilter eventFilter, Supplier<String> message,
892             String actionName) {
893         return executeAndWaitForEvent(
894                 command,
895                 e -> mLauncherPackage.equals(e.getPackageName()) && eventFilter.accept(e),
896                 message, actionName);
897     }
898 
executeAndWaitForEvent(Runnable command, UiAutomation.AccessibilityEventFilter eventFilter, Supplier<String> message, String actionName)899     Parcelable executeAndWaitForEvent(Runnable command,
900             UiAutomation.AccessibilityEventFilter eventFilter, Supplier<String> message,
901             String actionName) {
902         try (LauncherInstrumentation.Closable c = addContextLayer(actionName)) {
903             try {
904                 final AccessibilityEvent event =
905                         mInstrumentation.getUiAutomation().executeAndWaitForEvent(
906                                 command, eventFilter, WAIT_TIME_MS);
907                 assertNotNull("executeAndWaitForEvent returned null (this can't happen)", event);
908                 final Parcelable parcelableData = event.getParcelableData();
909                 event.recycle();
910                 return parcelableData;
911             } catch (TimeoutException e) {
912                 fail(message.get());
913                 return null;
914             }
915         }
916     }
917 
918     /**
919      * Get the resource ID of visible floating view.
920      */
getFloatingResId()921     private Optional<String> getFloatingResId() {
922         if (hasLauncherObject(CONTEXT_MENU_RES_ID)) {
923             return Optional.of(CONTEXT_MENU_RES_ID);
924         }
925         if (hasLauncherObject(FOLDER_CONTENT_RES_ID)) {
926             return Optional.of(FOLDER_CONTENT_RES_ID);
927         }
928         return Optional.empty();
929     }
930 
931     /**
932      * Using swiping up gesture to dismiss closable floating views, such as Menu or Folder Content.
933      */
swipeUpToCloseFloatingView(boolean gestureStartFromLauncher)934     private void swipeUpToCloseFloatingView(boolean gestureStartFromLauncher) {
935         final Point displaySize = getRealDisplaySize();
936 
937         final Optional<String> floatingRes = getFloatingResId();
938 
939         if (!floatingRes.isPresent()) {
940             return;
941         }
942 
943         GestureScope gestureScope = gestureStartFromLauncher
944                 // Without the navigation bar layer, the gesture scope on tablets remains inside the
945                 // launcher process.
946                 ? (isTablet() ? GestureScope.DONT_EXPECT_PILFER : GestureScope.EXPECT_PILFER)
947                 : GestureScope.EXPECT_PILFER;
948         linearGesture(
949                 displaySize.x / 2, displaySize.y - 1,
950                 displaySize.x / 2, 0,
951                 ZERO_BUTTON_STEPS_FROM_BACKGROUND_TO_HOME,
952                 false, gestureScope);
953 
954         try (LauncherInstrumentation.Closable c1 = addContextLayer(
955                 String.format("Swiped up from floating view %s to home", floatingRes.get()))) {
956             waitUntilLauncherObjectGone(floatingRes.get());
957             waitForLauncherObject(getAnyObjectSelector());
958         }
959     }
960 
961     /**
962      * @return the Workspace object.
963      * @deprecated use goHome().
964      * Presses nav bar home button.
965      */
966     @Deprecated
pressHome()967     public Workspace pressHome() {
968         return goHome();
969     }
970 
971     /**
972      * Goes to home by swiping up in zero-button mode or pressing Home button.
973      * Calling it after another TAPL call is safe because all TAPL methods wait for the animations
974      * to finish.
975      * When calling it after a non-TAPL method, make sure that all animations have already
976      * completed, otherwise it may detect the current state (for example "Application" or "Home")
977      * incorrectly.
978      * The method expects either app or Launcher to be active when it's called. Other states, such
979      * as visible notification shade are not supported.
980      *
981      * @return the Workspace object.
982      */
goHome()983     public Workspace goHome() {
984         try (LauncherInstrumentation.Closable e = eventsCheck();
985              LauncherInstrumentation.Closable c = addContextLayer("want to switch to home")) {
986             waitForLauncherInitialized();
987             // Click home, then wait for any accessibility event, then wait until accessibility
988             // events stop.
989             // We need waiting for any accessibility event generated after pressing Home because
990             // otherwise waitForIdle may return immediately in case when there was a big enough
991             // pause in accessibility events prior to pressing Home.
992             boolean isThreeFingerTrackpadGesture =
993                     mTrackpadGestureType == TrackpadGestureType.THREE_FINGER;
994             final String action;
995             if (getNavigationModel() == NavigationModel.ZERO_BUTTON
996                     || isThreeFingerTrackpadGesture) {
997                 checkForAnomaly(false, true);
998 
999                 final Point displaySize = getRealDisplaySize();
1000 
1001                 boolean gestureStartFromLauncher =
1002                         isTablet() ? !isLauncher3() : isLauncherVisible();
1003 
1004                 // CLose floating views before going back to home.
1005                 swipeUpToCloseFloatingView(gestureStartFromLauncher);
1006 
1007                 if (hasLauncherObject(WORKSPACE_RES_ID)) {
1008                     log(action = "already at home");
1009                 } else {
1010                     action = "swiping up to home";
1011 
1012                     int startY = isThreeFingerTrackpadGesture ? displaySize.y * 3 / 4
1013                             : displaySize.y - 1;
1014                     int endY = isThreeFingerTrackpadGesture ? displaySize.y / 4 : displaySize.y / 2;
1015                     swipeToState(
1016                             displaySize.x / 2, startY,
1017                             displaySize.x / 2, endY,
1018                             ZERO_BUTTON_STEPS_FROM_BACKGROUND_TO_HOME, NORMAL_STATE_ORDINAL,
1019                             GestureScope.EXPECT_PILFER);
1020                 }
1021             } else {
1022                 log("Hierarchy before clicking home:");
1023                 dumpViewHierarchy();
1024                 action = "clicking home button";
1025 
1026                 runToState(
1027                         waitForNavigationUiObject("home")::click,
1028                         NORMAL_STATE_ORDINAL,
1029                         !hasLauncherObject(WORKSPACE_RES_ID)
1030                                 && (hasLauncherObject(APPS_RES_ID)
1031                                 || hasSystemLauncherObject(OVERVIEW_RES_ID)),
1032                         action);
1033             }
1034             try (LauncherInstrumentation.Closable c1 = addContextLayer(
1035                     "performed action to switch to Home - " + action)) {
1036                 return getWorkspace();
1037             }
1038         }
1039     }
1040 
1041     /**
1042      * Press navbar back button or swipe back if in gesture navigation mode.
1043      */
pressBack()1044     public void pressBack() {
1045         try (Closable e = eventsCheck(); Closable c = addContextLayer("want to press back")) {
1046             waitForLauncherInitialized();
1047             final boolean launcherVisible =
1048                     isTablet() ? isLauncherContainerVisible() : isLauncherVisible();
1049             boolean isThreeFingerTrackpadGesture =
1050                     mTrackpadGestureType == TrackpadGestureType.THREE_FINGER;
1051             if (getNavigationModel() == NavigationModel.ZERO_BUTTON
1052                     || isThreeFingerTrackpadGesture) {
1053                 final Point displaySize = getRealDisplaySize();
1054                 // TODO(b/225505986): change startY and endY back to displaySize.y / 2 once the
1055                 //  issue is solved.
1056                 int startX = isThreeFingerTrackpadGesture ? displaySize.x / 4 : 0;
1057                 int endX = isThreeFingerTrackpadGesture ? displaySize.x * 3 / 4 : displaySize.x / 2;
1058                 linearGesture(startX, displaySize.y / 4, endX, displaySize.y / 4,
1059                         10, false, GestureScope.DONT_EXPECT_PILFER);
1060             } else {
1061                 waitForNavigationUiObject("back").click();
1062             }
1063             if (launcherVisible) {
1064                 if (getContext().getApplicationInfo().isOnBackInvokedCallbackEnabled()) {
1065                     expectEvent(TestProtocol.SEQUENCE_MAIN, EVENT_ON_BACK_INVOKED);
1066                 } else {
1067                     expectEvent(TestProtocol.SEQUENCE_MAIN, EVENT_KEY_BACK_DOWN);
1068                     expectEvent(TestProtocol.SEQUENCE_MAIN, EVENT_KEY_BACK_UP);
1069                 }
1070             }
1071         }
1072     }
1073 
getAnyObjectSelector()1074     private static BySelector getAnyObjectSelector() {
1075         return By.textStartsWith("");
1076     }
1077 
isLauncherVisible()1078     boolean isLauncherVisible() {
1079         mDevice.waitForIdle();
1080         return hasLauncherObject(getAnyObjectSelector());
1081     }
1082 
isLauncherContainerVisible()1083     boolean isLauncherContainerVisible() {
1084         final String[] containerResources = {WORKSPACE_RES_ID, OVERVIEW_RES_ID, APPS_RES_ID};
1085         return Arrays.stream(containerResources).anyMatch(
1086                 r -> r.equals(OVERVIEW_RES_ID) ? hasSystemLauncherObject(r) : hasLauncherObject(r));
1087     }
1088 
1089     /**
1090      * Gets the Workspace object if the current state is "active home", i.e. workspace. Fails if the
1091      * launcher is not in that state.
1092      *
1093      * @return Workspace object.
1094      */
1095     @NonNull
getWorkspace()1096     public Workspace getWorkspace() {
1097         try (LauncherInstrumentation.Closable c = addContextLayer("want to get workspace object")) {
1098             return new Workspace(this);
1099         }
1100     }
1101 
1102     /**
1103      * Gets the LaunchedApp object if another app is active. Fails if the launcher is not in that
1104      * state.
1105      *
1106      * @return LaunchedApp object.
1107      */
1108     @NonNull
getLaunchedAppState()1109     public LaunchedAppState getLaunchedAppState() {
1110         return new LaunchedAppState(this);
1111     }
1112 
1113     /**
1114      * Gets the Widgets object if the current state is showing all widgets. Fails if the launcher is
1115      * not in that state.
1116      *
1117      * @return Widgets object.
1118      */
1119     @NonNull
getAllWidgets()1120     public Widgets getAllWidgets() {
1121         try (LauncherInstrumentation.Closable c = addContextLayer("want to get widgets")) {
1122             return new Widgets(this);
1123         }
1124     }
1125 
1126     @NonNull
getAddToHomeScreenPrompt()1127     public AddToHomeScreenPrompt getAddToHomeScreenPrompt() {
1128         try (LauncherInstrumentation.Closable c = addContextLayer("want to get widget cell")) {
1129             return new AddToHomeScreenPrompt(this);
1130         }
1131     }
1132 
1133     /**
1134      * Gets the Overview object if the current state is showing the overview panel. Fails if the
1135      * launcher is not in that state.
1136      *
1137      * @return Overview object.
1138      */
1139     @NonNull
getOverview()1140     public Overview getOverview() {
1141         try (LauncherInstrumentation.Closable c = addContextLayer("want to get overview")) {
1142             return new Overview(this);
1143         }
1144     }
1145 
1146     /**
1147      * Gets the homescreen All Apps object if the current state is showing the all apps panel opened
1148      * by swiping from workspace. Fails if the launcher is not in that state. Please don't call this
1149      * method if App Apps was opened by swiping up from Overview, as it won't fail and will return
1150      * an incorrect object.
1151      *
1152      * @return Home All Apps object.
1153      */
1154     @NonNull
getAllApps()1155     public HomeAllApps getAllApps() {
1156         try (LauncherInstrumentation.Closable c = addContextLayer("want to get all apps object")) {
1157             return new HomeAllApps(this);
1158         }
1159     }
1160 
waitUntilLauncherObjectGone(String resId)1161     void waitUntilLauncherObjectGone(String resId) {
1162         waitUntilGoneBySelector(getLauncherObjectSelector(resId));
1163     }
1164 
waitUntilOverviewObjectGone(String resId)1165     void waitUntilOverviewObjectGone(String resId) {
1166         waitUntilGoneBySelector(getOverviewObjectSelector(resId));
1167     }
1168 
waitUntilSystemLauncherObjectGone(String resId)1169     void waitUntilSystemLauncherObjectGone(String resId) {
1170         if (is3PLauncher()) {
1171             waitUntilOverviewObjectGone(resId);
1172         } else {
1173             waitUntilLauncherObjectGone(resId);
1174         }
1175     }
1176 
waitUntilLauncherObjectGone(BySelector selector)1177     void waitUntilLauncherObjectGone(BySelector selector) {
1178         waitUntilGoneBySelector(makeLauncherSelector(selector));
1179     }
1180 
waitUntilGoneBySelector(BySelector launcherSelector)1181     private void waitUntilGoneBySelector(BySelector launcherSelector) {
1182         assertTrue("Unexpected launcher object visible: " + launcherSelector,
1183                 mDevice.wait(Until.gone(launcherSelector),
1184                         WAIT_TIME_MS));
1185     }
1186 
hasSystemUiObject(String resId)1187     private boolean hasSystemUiObject(String resId) {
1188         return mDevice.hasObject(By.res(SYSTEMUI_PACKAGE, resId));
1189     }
1190 
1191     @NonNull
waitForSystemUiObject(String resId)1192     UiObject2 waitForSystemUiObject(String resId) {
1193         final UiObject2 object = mDevice.wait(
1194                 Until.findObject(By.res(SYSTEMUI_PACKAGE, resId)), WAIT_TIME_MS);
1195         assertNotNull("Can't find a systemui object with id: " + resId, object);
1196         return object;
1197     }
1198 
1199     @NonNull
waitForSystemUiObject(BySelector selector)1200     UiObject2 waitForSystemUiObject(BySelector selector) {
1201         final UiObject2 object = TestHelpers.wait(
1202                 Until.findObject(selector), WAIT_TIME_MS);
1203         assertNotNull("Can't find a systemui object with selector: " + selector, object);
1204         return object;
1205     }
1206 
1207     @NonNull
waitForNavigationUiObject(String resId)1208     UiObject2 waitForNavigationUiObject(String resId) {
1209         String resPackage = getNavigationButtonResPackage();
1210         final UiObject2 object = mDevice.wait(
1211                 Until.findObject(By.res(resPackage, resId)), WAIT_TIME_MS);
1212         assertNotNull("Can't find a navigation UI object with id: " + resId, object);
1213         return object;
1214     }
1215 
1216     @Nullable
findObjectInContainer(UiObject2 container, String resName)1217     UiObject2 findObjectInContainer(UiObject2 container, String resName) {
1218         try {
1219             return container.findObject(getLauncherObjectSelector(resName));
1220         } catch (StaleObjectException e) {
1221             fail("The container disappeared from screen");
1222             return null;
1223         }
1224     }
1225 
1226     @Nullable
findObjectInContainer(UiObject2 container, BySelector selector)1227     UiObject2 findObjectInContainer(UiObject2 container, BySelector selector) {
1228         try {
1229             return container.findObject(selector);
1230         } catch (StaleObjectException e) {
1231             fail("The container disappeared from screen");
1232             return null;
1233         }
1234     }
1235 
1236     @NonNull
getObjectsInContainer(UiObject2 container, String resName)1237     List<UiObject2> getObjectsInContainer(UiObject2 container, String resName) {
1238         try {
1239             return container.findObjects(getLauncherObjectSelector(resName));
1240         } catch (StaleObjectException e) {
1241             fail("The container disappeared from screen");
1242             return null;
1243         }
1244     }
1245 
1246     @NonNull
waitForObjectInContainer(UiObject2 container, String resName)1247     UiObject2 waitForObjectInContainer(UiObject2 container, String resName) {
1248         try {
1249             final UiObject2 object = container.wait(
1250                     Until.findObject(getLauncherObjectSelector(resName)),
1251                     WAIT_TIME_MS);
1252             assertNotNull("Can't find a view in Launcher, id: " + resName + " in container: "
1253                     + container.getResourceName(), object);
1254             return object;
1255         } catch (StaleObjectException e) {
1256             fail("The container disappeared from screen");
1257             return null;
1258         }
1259     }
1260 
waitForObjectEnabled(UiObject2 object, String waitReason)1261     void waitForObjectEnabled(UiObject2 object, String waitReason) {
1262         try {
1263             assertTrue("Timed out waiting for object to be enabled for " + waitReason + " "
1264                             + object.getResourceName(),
1265                     object.wait(Until.enabled(true), WAIT_TIME_MS));
1266         } catch (StaleObjectException e) {
1267             fail("The object disappeared from screen");
1268         }
1269     }
1270 
1271     @NonNull
waitForObjectInContainer(UiObject2 container, BySelector selector)1272     UiObject2 waitForObjectInContainer(UiObject2 container, BySelector selector) {
1273         return waitForObjectsInContainer(container, selector).get(0);
1274     }
1275 
1276     @NonNull
waitForObjectsInContainer( UiObject2 container, BySelector selector)1277     List<UiObject2> waitForObjectsInContainer(
1278             UiObject2 container, BySelector selector) {
1279         try {
1280             final List<UiObject2> objects = container.wait(
1281                     Until.findObjects(selector),
1282                     WAIT_TIME_MS);
1283             assertNotNull("Can't find views in Launcher, id: " + selector + " in container: "
1284                     + container.getResourceName(), objects);
1285             assertTrue("Can't find views in Launcher, id: " + selector + " in container: "
1286                     + container.getResourceName(), objects.size() > 0);
1287             return objects;
1288         } catch (StaleObjectException e) {
1289             fail("The container disappeared from screen");
1290             return null;
1291         }
1292     }
1293 
getChildren(UiObject2 container)1294     List<UiObject2> getChildren(UiObject2 container) {
1295         try {
1296             return container.getChildren();
1297         } catch (StaleObjectException e) {
1298             fail("The container disappeared from screen");
1299             return null;
1300         }
1301     }
1302 
hasLauncherObject(String resId)1303     private boolean hasLauncherObject(String resId) {
1304         return mDevice.hasObject(getLauncherObjectSelector(resId));
1305     }
1306 
hasSystemLauncherObject(String resId)1307     private boolean hasSystemLauncherObject(String resId) {
1308         return mDevice.hasObject(is3PLauncher() ? getOverviewObjectSelector(resId)
1309                 : getLauncherObjectSelector(resId));
1310     }
1311 
hasLauncherObject(BySelector selector)1312     boolean hasLauncherObject(BySelector selector) {
1313         return mDevice.hasObject(makeLauncherSelector(selector));
1314     }
1315 
makeLauncherSelector(BySelector selector)1316     private BySelector makeLauncherSelector(BySelector selector) {
1317         return By.copy(selector).pkg(getLauncherPackageName());
1318     }
1319 
1320     @NonNull
waitForOverviewObject(String resName)1321     UiObject2 waitForOverviewObject(String resName) {
1322         return waitForObjectBySelector(getOverviewObjectSelector(resName));
1323     }
1324 
1325     @NonNull
waitForLauncherObject(String resName)1326     UiObject2 waitForLauncherObject(String resName) {
1327         return waitForObjectBySelector(getLauncherObjectSelector(resName));
1328     }
1329 
1330     @NonNull
waitForSystemLauncherObject(String resName)1331     UiObject2 waitForSystemLauncherObject(String resName) {
1332         return is3PLauncher() ? waitForOverviewObject(resName)
1333                 : waitForLauncherObject(resName);
1334     }
1335 
1336     @NonNull
waitForLauncherObject(BySelector selector)1337     UiObject2 waitForLauncherObject(BySelector selector) {
1338         return waitForObjectBySelector(makeLauncherSelector(selector));
1339     }
1340 
1341     @NonNull
tryWaitForLauncherObject(BySelector selector, long timeout)1342     UiObject2 tryWaitForLauncherObject(BySelector selector, long timeout) {
1343         return tryWaitForObjectBySelector(makeLauncherSelector(selector), timeout);
1344     }
1345 
1346     @NonNull
waitForAndroidObject(String resId)1347     UiObject2 waitForAndroidObject(String resId) {
1348         final UiObject2 object = TestHelpers.wait(
1349                 Until.findObject(By.res(ANDROID_PACKAGE, resId)), WAIT_TIME_MS);
1350         assertNotNull("Can't find a android object with id: " + resId, object);
1351         return object;
1352     }
1353 
1354     @NonNull
waitForObjectsBySelector(BySelector selector)1355     List<UiObject2> waitForObjectsBySelector(BySelector selector) {
1356         final List<UiObject2> objects = mDevice.wait(Until.findObjects(selector), WAIT_TIME_MS);
1357         assertNotNull("Can't find any view in Launcher, selector: " + selector, objects);
1358         return objects;
1359     }
1360 
waitForObjectBySelector(BySelector selector)1361     private UiObject2 waitForObjectBySelector(BySelector selector) {
1362         final UiObject2 object = mDevice.wait(Until.findObject(selector), WAIT_TIME_MS);
1363         assertNotNull("Can't find a view in Launcher, selector: " + selector, object);
1364         return object;
1365     }
1366 
tryWaitForObjectBySelector(BySelector selector, long timeout)1367     private UiObject2 tryWaitForObjectBySelector(BySelector selector, long timeout) {
1368         return mDevice.wait(Until.findObject(selector), timeout);
1369     }
1370 
getLauncherObjectSelector(String resName)1371     BySelector getLauncherObjectSelector(String resName) {
1372         return By.res(getLauncherPackageName(), resName);
1373     }
1374 
getOverviewObjectSelector(String resName)1375     BySelector getOverviewObjectSelector(String resName) {
1376         return By.res(getOverviewPackageName(), resName);
1377     }
1378 
getLauncherPackageName()1379     String getLauncherPackageName() {
1380         return mDevice.getLauncherPackageName();
1381     }
1382 
is3PLauncher()1383     boolean is3PLauncher() {
1384         return !getOverviewPackageName().equals(getLauncherPackageName());
1385     }
1386 
1387     @NonNull
getDevice()1388     public UiDevice getDevice() {
1389         return mDevice;
1390     }
1391 
eventListToString(List<Integer> actualEvents)1392     private static String eventListToString(List<Integer> actualEvents) {
1393         if (actualEvents.isEmpty()) return "no events";
1394 
1395         return "["
1396                 + actualEvents.stream()
1397                 .map(state -> TestProtocol.stateOrdinalToString(state))
1398                 .collect(Collectors.joining(", "))
1399                 + "]";
1400     }
1401 
runToState(Runnable command, int expectedState, boolean requireEvent, String actionName)1402     void runToState(Runnable command, int expectedState, boolean requireEvent, String actionName) {
1403         if (requireEvent) {
1404             runToState(command, expectedState, actionName);
1405         } else {
1406             command.run();
1407         }
1408     }
1409 
runToState(Runnable command, int expectedState, String actionName)1410     void runToState(Runnable command, int expectedState, String actionName) {
1411         final List<Integer> actualEvents = new ArrayList<>();
1412         executeAndWaitForLauncherEvent(
1413                 command,
1414                 event -> isSwitchToStateEvent(event, expectedState, actualEvents),
1415                 () -> "Failed to receive an event for the state change: expected ["
1416                         + TestProtocol.stateOrdinalToString(expectedState)
1417                         + "], actual: " + eventListToString(actualEvents),
1418                 actionName);
1419     }
1420 
isSwitchToStateEvent( AccessibilityEvent event, int expectedState, List<Integer> actualEvents)1421     private boolean isSwitchToStateEvent(
1422             AccessibilityEvent event, int expectedState, List<Integer> actualEvents) {
1423         if (!TestProtocol.SWITCHED_TO_STATE_MESSAGE.equals(event.getClassName())) return false;
1424 
1425         final Bundle parcel = (Bundle) event.getParcelableData();
1426         final int actualState = parcel.getInt(TestProtocol.STATE_FIELD);
1427         actualEvents.add(actualState);
1428         return actualState == expectedState;
1429     }
1430 
swipeToState(int startX, int startY, int endX, int endY, int steps, int expectedState, GestureScope gestureScope)1431     void swipeToState(int startX, int startY, int endX, int endY, int steps, int expectedState,
1432             GestureScope gestureScope) {
1433         runToState(
1434                 () -> linearGesture(startX, startY, endX, endY, steps, false, gestureScope),
1435                 expectedState,
1436                 "swiping");
1437     }
1438 
getBottomGestureSize()1439     int getBottomGestureSize() {
1440         return Math.max(getWindowInsets().bottom, ResourceUtils.getNavbarSize(
1441                 ResourceUtils.NAVBAR_BOTTOM_GESTURE_SIZE, getResources())) + 1;
1442     }
1443 
getBottomGestureMarginInContainer(UiObject2 container)1444     int getBottomGestureMarginInContainer(UiObject2 container) {
1445         final int bottomGestureStartOnScreen = getBottomGestureStartOnScreen();
1446         return getVisibleBounds(container).bottom - bottomGestureStartOnScreen;
1447     }
1448 
getRightGestureMarginInContainer(UiObject2 container)1449     int getRightGestureMarginInContainer(UiObject2 container) {
1450         final int rightGestureStartOnScreen = getRightGestureStartOnScreen();
1451         return getVisibleBounds(container).right - rightGestureStartOnScreen;
1452     }
1453 
getBottomGestureStartOnScreen()1454     int getBottomGestureStartOnScreen() {
1455         return getRealDisplaySize().y - getBottomGestureSize();
1456     }
1457 
getRightGestureStartOnScreen()1458     int getRightGestureStartOnScreen() {
1459         return getRealDisplaySize().x - getWindowInsets().right - 1;
1460     }
1461 
1462     /**
1463      * Click on the ui object right away without waiting for animation.
1464      *
1465      * [UiObject2.click] would wait for all animations finished before clicking. Not waiting for
1466      * animations because in some scenarios there is a playing animations when the click is
1467      * attempted.
1468      */
clickObject(UiObject2 uiObject)1469     void clickObject(UiObject2 uiObject) {
1470         final long clickTime = SystemClock.uptimeMillis();
1471         final Point center = uiObject.getVisibleCenter();
1472         sendPointer(clickTime, clickTime, MotionEvent.ACTION_DOWN, center,
1473                 GestureScope.DONT_EXPECT_PILFER);
1474         sendPointer(clickTime, clickTime, MotionEvent.ACTION_UP, center,
1475                 GestureScope.DONT_EXPECT_PILFER);
1476     }
1477 
clickLauncherObject(UiObject2 object)1478     void clickLauncherObject(UiObject2 object) {
1479         clickObject(object);
1480     }
1481 
scrollToLastVisibleRow( UiObject2 container, Rect bottomVisibleIconBounds, int topPaddingInContainer, int appsListBottomPadding)1482     void scrollToLastVisibleRow(
1483             UiObject2 container, Rect bottomVisibleIconBounds, int topPaddingInContainer,
1484             int appsListBottomPadding) {
1485         final int itemRowCurrentTopOnScreen = bottomVisibleIconBounds.top;
1486         final Rect containerRect = getVisibleBounds(container);
1487         final int itemRowNewTopOnScreen = containerRect.top + topPaddingInContainer;
1488         final int distance = itemRowCurrentTopOnScreen - itemRowNewTopOnScreen + getTouchSlop();
1489 
1490         scrollDownByDistance(container, distance, appsListBottomPadding);
1491     }
1492 
scrollDownByDistance(UiObject2 container, int distance)1493     void scrollDownByDistance(UiObject2 container, int distance) {
1494         scrollDownByDistance(container, distance, 0);
1495     }
1496 
scrollDownByDistance(UiObject2 container, int distance, int bottomPadding)1497     void scrollDownByDistance(UiObject2 container, int distance, int bottomPadding) {
1498         final Rect containerRect = getVisibleBounds(container);
1499         final int bottomGestureMarginInContainer = getBottomGestureMarginInContainer(container);
1500         scroll(
1501                 container,
1502                 Direction.DOWN,
1503                 new Rect(
1504                         0,
1505                         containerRect.height() - distance - bottomGestureMarginInContainer,
1506                         0,
1507                         bottomGestureMarginInContainer + bottomPadding),
1508                 /* steps= */ 10,
1509                 /* slowDown= */ true);
1510     }
1511 
scrollLeftByDistance(UiObject2 container, int distance)1512     void scrollLeftByDistance(UiObject2 container, int distance) {
1513         final Rect containerRect = getVisibleBounds(container);
1514         final int rightGestureMarginInContainer = getRightGestureMarginInContainer(container);
1515         final int leftGestureMargin = getTargetInsets().left + getEdgeSensitivityWidth();
1516         scroll(
1517                 container,
1518                 Direction.LEFT,
1519                 new Rect(leftGestureMargin, 0,
1520                         containerRect.width() - distance - rightGestureMarginInContainer, 0),
1521                 10,
1522                 true);
1523     }
1524 
scroll( UiObject2 container, Direction direction, Rect margins, int steps, boolean slowDown)1525     void scroll(
1526             UiObject2 container, Direction direction, Rect margins, int steps, boolean slowDown) {
1527         final Rect rect = getVisibleBounds(container);
1528         if (margins != null) {
1529             rect.left += margins.left;
1530             rect.top += margins.top;
1531             rect.right -= margins.right;
1532             rect.bottom -= margins.bottom;
1533         }
1534 
1535         final int startX;
1536         final int startY;
1537         final int endX;
1538         final int endY;
1539 
1540         switch (direction) {
1541             case UP: {
1542                 startX = endX = rect.centerX();
1543                 startY = rect.top;
1544                 endY = rect.bottom - 1;
1545             }
1546             break;
1547             case DOWN: {
1548                 startX = endX = rect.centerX();
1549                 startY = rect.bottom - 1;
1550                 endY = rect.top;
1551             }
1552             break;
1553             case LEFT: {
1554                 startY = endY = rect.centerY();
1555                 startX = rect.left;
1556                 endX = rect.right - 1;
1557             }
1558             break;
1559             case RIGHT: {
1560                 startY = endY = rect.centerY();
1561                 startX = rect.right - 1;
1562                 endX = rect.left;
1563             }
1564             break;
1565             default:
1566                 fail("Unsupported direction");
1567                 return;
1568         }
1569 
1570         executeAndWaitForLauncherEvent(
1571                 () -> linearGesture(
1572                         startX, startY, endX, endY, steps, slowDown,
1573                         GestureScope.DONT_EXPECT_PILFER),
1574                 event -> TestProtocol.SCROLL_FINISHED_MESSAGE.equals(event.getClassName()),
1575                 () -> "Didn't receive a scroll end message: " + startX + ", " + startY
1576                         + ", " + endX + ", " + endY,
1577                 "scrolling");
1578     }
1579 
1580     // Inject a swipe gesture. Inject exactly 'steps' motion points, incrementing event time by a
1581     // fixed interval each time.
linearGesture(int startX, int startY, int endX, int endY, int steps, boolean slowDown, GestureScope gestureScope)1582     public void linearGesture(int startX, int startY, int endX, int endY, int steps,
1583             boolean slowDown, GestureScope gestureScope) {
1584         log("linearGesture: " + startX + ", " + startY + " -> " + endX + ", " + endY);
1585         final long downTime = SystemClock.uptimeMillis();
1586         final Point start = new Point(startX, startY);
1587         final Point end = new Point(endX, endY);
1588         sendPointer(downTime, downTime, MotionEvent.ACTION_DOWN, start, gestureScope);
1589         if (mTrackpadGestureType != TrackpadGestureType.NONE) {
1590             sendPointer(downTime, downTime, getPointerAction(MotionEvent.ACTION_POINTER_DOWN, 1),
1591                     start, gestureScope);
1592             if (mTrackpadGestureType == TrackpadGestureType.THREE_FINGER
1593                     || mTrackpadGestureType == TrackpadGestureType.FOUR_FINGER) {
1594                 sendPointer(downTime, downTime,
1595                         getPointerAction(MotionEvent.ACTION_POINTER_DOWN, 2),
1596                         start, gestureScope);
1597                 if (mTrackpadGestureType == TrackpadGestureType.FOUR_FINGER) {
1598                     sendPointer(downTime, downTime,
1599                             getPointerAction(MotionEvent.ACTION_POINTER_DOWN, 3),
1600                             start, gestureScope);
1601                 }
1602             }
1603         }
1604         final long endTime = movePointer(
1605                 start, end, steps, false, downTime, downTime, slowDown, gestureScope);
1606         if (mTrackpadGestureType != TrackpadGestureType.NONE) {
1607             for (int i = mPointerCount; i >= 2; i--) {
1608                 sendPointer(downTime, downTime,
1609                         getPointerAction(MotionEvent.ACTION_POINTER_UP, i - 1),
1610                         start, gestureScope);
1611             }
1612         }
1613         sendPointer(downTime, endTime, MotionEvent.ACTION_UP, end, gestureScope);
1614     }
1615 
getPointerAction(int action, int index)1616     private static int getPointerAction(int action, int index) {
1617         return action + (index << MotionEvent.ACTION_POINTER_INDEX_SHIFT);
1618     }
1619 
movePointer(Point start, Point end, int steps, boolean isDecelerating, long downTime, long startTime, boolean slowDown, GestureScope gestureScope)1620     long movePointer(Point start, Point end, int steps, boolean isDecelerating, long downTime,
1621             long startTime, boolean slowDown, GestureScope gestureScope) {
1622         long endTime = movePointer(downTime, startTime, steps * GESTURE_STEP_MS,
1623                 isDecelerating, start, end, gestureScope);
1624         if (slowDown) {
1625             endTime = movePointer(downTime, endTime + GESTURE_STEP_MS, 5 * GESTURE_STEP_MS, end,
1626                     end, gestureScope);
1627         }
1628         return endTime;
1629     }
1630 
waitForIdle()1631     void waitForIdle() {
1632         mDevice.waitForIdle();
1633     }
1634 
getTouchSlop()1635     int getTouchSlop() {
1636         return ViewConfiguration.get(getContext()).getScaledTouchSlop();
1637     }
1638 
getResources()1639     public Resources getResources() {
1640         return getContext().getResources();
1641     }
1642 
getTrackpadMotionEvent(long downTime, long eventTime, int action, float x, float y, int pointerCount, TrackpadGestureType gestureType)1643     private static MotionEvent getTrackpadMotionEvent(long downTime, long eventTime,
1644             int action, float x, float y, int pointerCount, TrackpadGestureType gestureType) {
1645         MotionEvent.PointerProperties[] pointerProperties =
1646                 new MotionEvent.PointerProperties[pointerCount];
1647         MotionEvent.PointerCoords[] pointerCoords = new MotionEvent.PointerCoords[pointerCount];
1648         boolean isMultiFingerGesture = gestureType != TrackpadGestureType.TWO_FINGER;
1649         for (int i = 0; i < pointerCount; i++) {
1650             pointerProperties[i] = getPointerProperties(i);
1651             pointerCoords[i] = getPointerCoords(x, y);
1652             if (isMultiFingerGesture) {
1653                 pointerCoords[i].setAxisValue(AXIS_GESTURE_SWIPE_FINGER_COUNT,
1654                         gestureType == TrackpadGestureType.THREE_FINGER ? 3 : 4);
1655             }
1656         }
1657         return MotionEvent.obtain(downTime, eventTime, action, pointerCount, pointerProperties,
1658                 pointerCoords, 0, 0, 1.0f, 1.0f, 0, 0,
1659                 InputDevice.SOURCE_MOUSE | InputDevice.SOURCE_CLASS_POINTER, 0, 0,
1660                 isMultiFingerGesture ? MotionEvent.CLASSIFICATION_MULTI_FINGER_SWIPE
1661                         : MotionEvent.CLASSIFICATION_TWO_FINGER_SWIPE);
1662     }
1663 
getMotionEvent(long downTime, long eventTime, int action, float x, float y, int source)1664     private static MotionEvent getMotionEvent(long downTime, long eventTime, int action,
1665             float x, float y, int source) {
1666         return MotionEvent.obtain(downTime, eventTime, action, 1,
1667                 new MotionEvent.PointerProperties[]{getPointerProperties(0)},
1668                 new MotionEvent.PointerCoords[]{getPointerCoords(x, y)},
1669                 0, 0, 1.0f, 1.0f, 0, 0, source, 0);
1670     }
1671 
getPointerProperties(int pointerId)1672     private static MotionEvent.PointerProperties getPointerProperties(int pointerId) {
1673         MotionEvent.PointerProperties properties = new MotionEvent.PointerProperties();
1674         properties.id = pointerId;
1675         properties.toolType = Configurator.getInstance().getToolType();
1676         return properties;
1677     }
1678 
getPointerCoords(float x, float y)1679     private static MotionEvent.PointerCoords getPointerCoords(float x, float y) {
1680         MotionEvent.PointerCoords coords = new MotionEvent.PointerCoords();
1681         coords.pressure = 1;
1682         coords.size = 1;
1683         coords.x = x;
1684         coords.y = y;
1685         return coords;
1686     }
1687 
hasTIS()1688     private boolean hasTIS() {
1689         return getTestInfo(TestProtocol.REQUEST_HAS_TIS).getBoolean(
1690                 TestProtocol.TEST_INFO_RESPONSE_FIELD);
1691     }
1692 
isGridOnlyOverviewEnabled()1693     boolean isGridOnlyOverviewEnabled() {
1694         return getTestInfo(TestProtocol.REQUEST_FLAG_ENABLE_GRID_ONLY_OVERVIEW).getBoolean(
1695                 TestProtocol.TEST_INFO_RESPONSE_FIELD);
1696     }
1697 
sendPointer(long downTime, long currentTime, int action, Point point, GestureScope gestureScope)1698     public void sendPointer(long downTime, long currentTime, int action, Point point,
1699             GestureScope gestureScope) {
1700         sendPointer(downTime, currentTime, action, point, gestureScope,
1701                 InputDevice.SOURCE_TOUCHSCREEN);
1702     }
1703 
sendPointer(long downTime, long currentTime, int action, Point point, GestureScope gestureScope, int source)1704     public void sendPointer(long downTime, long currentTime, int action, Point point,
1705             GestureScope gestureScope, int source) {
1706         final boolean hasTIS = hasTIS();
1707         int pointerCount = mPointerCount;
1708 
1709         boolean isTrackpadGesture = mTrackpadGestureType != TrackpadGestureType.NONE;
1710         switch (action & MotionEvent.ACTION_MASK) {
1711             case MotionEvent.ACTION_DOWN:
1712                 if (isTrackpadGesture) {
1713                     mPointerCount = 1;
1714                     pointerCount = mPointerCount;
1715                 }
1716                 break;
1717             case MotionEvent.ACTION_UP:
1718                 if (hasTIS && gestureScope == GestureScope.EXPECT_PILFER) {
1719                     expectEvent(TestProtocol.SEQUENCE_PILFER, EVENT_PILFER_POINTERS);
1720                 }
1721                 break;
1722             case MotionEvent.ACTION_POINTER_DOWN:
1723                 mPointerCount++;
1724                 pointerCount = mPointerCount;
1725                 break;
1726             case MotionEvent.ACTION_POINTER_UP:
1727                 // When the gesture is handled outside, it's cancelled within launcher.
1728                 mPointerCount--;
1729                 break;
1730         }
1731 
1732         final MotionEvent event = isTrackpadGesture
1733                 ? getTrackpadMotionEvent(
1734                         downTime, currentTime, action, point.x, point.y, pointerCount,
1735                         mTrackpadGestureType)
1736                 : getMotionEvent(downTime, currentTime, action, point.x, point.y, source);
1737         if (action == MotionEvent.ACTION_BUTTON_PRESS
1738                 || action == MotionEvent.ACTION_BUTTON_RELEASE) {
1739             event.setActionButton(MotionEvent.BUTTON_PRIMARY);
1740         }
1741         assertTrue("injectInputEvent failed",
1742                 mInstrumentation.getUiAutomation().injectInputEvent(event, true, false));
1743         event.recycle();
1744     }
1745 
movePointer(long downTime, long startTime, long duration, Point from, Point to, GestureScope gestureScope)1746     public long movePointer(long downTime, long startTime, long duration, Point from, Point to,
1747             GestureScope gestureScope) {
1748         return movePointer(downTime, startTime, duration, false, from, to, gestureScope);
1749     }
1750 
movePointer(long downTime, long startTime, long duration, boolean isDecelerating, Point from, Point to, GestureScope gestureScope)1751     public long movePointer(long downTime, long startTime, long duration, boolean isDecelerating,
1752             Point from, Point to, GestureScope gestureScope) {
1753         log("movePointer: " + from + " to " + to);
1754         final Point point = new Point();
1755         long steps = duration / GESTURE_STEP_MS;
1756 
1757         long currentTime = startTime;
1758 
1759         if (isDecelerating) {
1760             // formula: V = V0 - D*T, assuming V = 0 when T = duration
1761 
1762             // vx0: initial speed at the x-dimension, set as twice the avg speed
1763             // dx: the constant deceleration at the x-dimension
1764             double vx0 = 2.0 * (to.x - from.x) / duration;
1765             double dx = vx0 / duration;
1766             // vy0: initial speed at the y-dimension, set as twice the avg speed
1767             // dy: the constant deceleration at the y-dimension
1768             double vy0 = 2.0 * (to.y - from.y) / duration;
1769             double dy = vy0 / duration;
1770 
1771             for (long i = 0; i < steps; ++i) {
1772                 sleep(GESTURE_STEP_MS);
1773                 currentTime += GESTURE_STEP_MS;
1774 
1775                 // formula: P = P0 + V0*T - (D*T^2/2)
1776                 final double t = (i + 1) * GESTURE_STEP_MS;
1777                 point.x = from.x + (int) (vx0 * t - 0.5 * dx * t * t);
1778                 point.y = from.y + (int) (vy0 * t - 0.5 * dy * t * t);
1779 
1780                 sendPointer(downTime, currentTime, MotionEvent.ACTION_MOVE, point, gestureScope);
1781             }
1782         } else {
1783             for (long i = 0; i < steps; ++i) {
1784                 sleep(GESTURE_STEP_MS);
1785                 currentTime += GESTURE_STEP_MS;
1786 
1787                 final float progress = (currentTime - startTime) / (float) duration;
1788                 point.x = from.x + (int) (progress * (to.x - from.x));
1789                 point.y = from.y + (int) (progress * (to.y - from.y));
1790 
1791                 sendPointer(downTime, currentTime, MotionEvent.ACTION_MOVE, point, gestureScope);
1792 
1793             }
1794         }
1795 
1796         return currentTime;
1797     }
1798 
getCurrentInteractionMode(Context context)1799     public static int getCurrentInteractionMode(Context context) {
1800         return getSystemIntegerRes(context, "config_navBarInteractionMode");
1801     }
1802 
1803     @NonNull
clickAndGet( @onNull final UiObject2 target, @NonNull String resName, Pattern longClickEvent)1804     UiObject2 clickAndGet(
1805             @NonNull final UiObject2 target, @NonNull String resName, Pattern longClickEvent) {
1806         final Point targetCenter = target.getVisibleCenter();
1807         final long downTime = SystemClock.uptimeMillis();
1808         sendPointer(downTime, downTime, MotionEvent.ACTION_DOWN, targetCenter,
1809                 GestureScope.DONT_EXPECT_PILFER);
1810         expectEvent(TestProtocol.SEQUENCE_MAIN, longClickEvent);
1811         final UiObject2 result = waitForLauncherObject(resName);
1812         sendPointer(downTime, SystemClock.uptimeMillis(), MotionEvent.ACTION_UP, targetCenter,
1813                 GestureScope.DONT_EXPECT_PILFER);
1814         return result;
1815     }
1816 
getSystemIntegerRes(Context context, String resName)1817     private static int getSystemIntegerRes(Context context, String resName) {
1818         Resources res = context.getResources();
1819         int resId = res.getIdentifier(resName, "integer", "android");
1820 
1821         if (resId != 0) {
1822             return res.getInteger(resId);
1823         } else {
1824             Log.e(TAG, "Failed to get system resource ID. Incompatible framework version?");
1825             return -1;
1826         }
1827     }
1828 
getSystemDimensionResId(Context context, String resName)1829     private static int getSystemDimensionResId(Context context, String resName) {
1830         Resources res = context.getResources();
1831         int resId = res.getIdentifier(resName, "dimen", "android");
1832 
1833         if (resId != 0) {
1834             return resId;
1835         } else {
1836             Log.e(TAG, "Failed to get system resource ID. Incompatible framework version?");
1837             return -1;
1838         }
1839     }
1840 
sleep(int duration)1841     static void sleep(int duration) {
1842         SystemClock.sleep(duration);
1843     }
1844 
getEdgeSensitivityWidth()1845     int getEdgeSensitivityWidth() {
1846         try {
1847             final Context context = mInstrumentation.getTargetContext().createPackageContext(
1848                     getLauncherPackageName(), 0);
1849             return context.getResources().getDimensionPixelSize(
1850                     getSystemDimensionResId(context, "config_backGestureInset")) + 1;
1851         } catch (PackageManager.NameNotFoundException e) {
1852             fail("Can't get edge sensitivity: " + e);
1853             return 0;
1854         }
1855     }
1856 
getRealDisplaySize()1857     Point getRealDisplaySize() {
1858         final Rect displayBounds = getContext().getSystemService(WindowManager.class)
1859                 .getMaximumWindowMetrics()
1860                 .getBounds();
1861         return new Point(displayBounds.width(), displayBounds.height());
1862     }
1863 
enableDebugTracing()1864     public void enableDebugTracing() {
1865         getTestInfo(TestProtocol.REQUEST_ENABLE_DEBUG_TRACING);
1866     }
1867 
disableSensorRotation()1868     private void disableSensorRotation() {
1869         getTestInfo(TestProtocol.REQUEST_MOCK_SENSOR_ROTATION);
1870     }
1871 
disableDebugTracing()1872     public void disableDebugTracing() {
1873         getTestInfo(TestProtocol.REQUEST_DISABLE_DEBUG_TRACING);
1874     }
1875 
forceGc()1876     public void forceGc() {
1877         // GC the system & sysui first before gc'ing launcher
1878         logShellCommand("cmd statusbar run-gc");
1879         getTestInfo(TestProtocol.REQUEST_FORCE_GC);
1880     }
1881 
getPid()1882     public Integer getPid() {
1883         final Bundle testInfo = getTestInfo(TestProtocol.REQUEST_PID);
1884         return testInfo != null ? testInfo.getInt(TestProtocol.TEST_INFO_RESPONSE_FIELD) : null;
1885     }
1886 
getRecentTasks()1887     public ArrayList<ComponentName> getRecentTasks() {
1888         ArrayList<ComponentName> tasks = new ArrayList<>();
1889         ArrayList<String> components = getTestInfo(TestProtocol.REQUEST_RECENT_TASKS_LIST)
1890                 .getStringArrayList(TestProtocol.TEST_INFO_RESPONSE_FIELD);
1891         for (String s : components) {
1892             tasks.add(ComponentName.unflattenFromString(s));
1893         }
1894         return tasks;
1895     }
1896 
1897     /** Reinitializes the workspace to its default layout. */
reinitializeLauncherData()1898     public void reinitializeLauncherData() {
1899         getTestInfo(TestProtocol.REQUEST_REINITIALIZE_DATA);
1900     }
1901 
1902     /** Clears the workspace, leaving it empty. */
clearLauncherData()1903     public void clearLauncherData() {
1904         getTestInfo(TestProtocol.REQUEST_CLEAR_DATA);
1905     }
1906 
1907     /** Shows the taskbar if it is hidden, otherwise does nothing. */
showTaskbarIfHidden()1908     public void showTaskbarIfHidden() {
1909         getTestInfo(TestProtocol.REQUEST_UNSTASH_TASKBAR_IF_STASHED);
1910     }
1911 
1912     /** Blocks the taskbar from automatically stashing based on time. */
enableBlockTimeout(boolean enable)1913     public void enableBlockTimeout(boolean enable) {
1914         getTestInfo(enable
1915                 ? TestProtocol.REQUEST_ENABLE_BLOCK_TIMEOUT
1916                 : TestProtocol.REQUEST_DISABLE_BLOCK_TIMEOUT);
1917     }
1918 
1919     /** Enables transient taskbar for testing purposes only. */
enableTransientTaskbar(boolean enable)1920     public void enableTransientTaskbar(boolean enable) {
1921         getTestInfo(enable
1922                 ? TestProtocol.REQUEST_ENABLE_TRANSIENT_TASKBAR
1923                 : TestProtocol.REQUEST_DISABLE_TRANSIENT_TASKBAR);
1924     }
1925 
1926     /**
1927      * Recreates the taskbar (outside of tests this is done for certain configuration changes).
1928      * The expected behavior is that the taskbar retains its current state after being recreated.
1929      * For example, if taskbar is currently stashed, it should still be stashed after recreating.
1930      */
recreateTaskbar()1931     public void recreateTaskbar() {
1932         getTestInfo(TestProtocol.REQUEST_RECREATE_TASKBAR);
1933     }
1934 
getHotseatIconNames()1935     public List<String> getHotseatIconNames() {
1936         return getTestInfo(TestProtocol.REQUEST_HOTSEAT_ICON_NAMES)
1937                 .getStringArrayList(TestProtocol.TEST_INFO_RESPONSE_FIELD);
1938     }
1939 
getActivities()1940     private String[] getActivities() {
1941         return getTestInfo(TestProtocol.REQUEST_GET_ACTIVITIES)
1942                 .getStringArray(TestProtocol.TEST_INFO_RESPONSE_FIELD);
1943     }
1944 
getRootedActivitiesList()1945     public String getRootedActivitiesList() {
1946         return String.join(", ", getActivities());
1947     }
1948 
noLeakedActivities()1949     public boolean noLeakedActivities() {
1950         final String[] activities = getActivities();
1951         for (String activity : activities) {
1952             if (activity.contains("(destroyed)")) {
1953                 return false;
1954             }
1955         }
1956         return activities.length <= 2;
1957     }
1958 
getActivitiesCreated()1959     public int getActivitiesCreated() {
1960         return getTestInfo(TestProtocol.REQUEST_GET_ACTIVITIES_CREATED_COUNT)
1961                 .getInt(TestProtocol.TEST_INFO_RESPONSE_FIELD);
1962     }
1963 
eventsCheck()1964     public Closable eventsCheck() {
1965         Assert.assertTrue("Nested event checking", mEventChecker == null);
1966         disableSensorRotation();
1967         final Integer initialPid = getPid();
1968         final LogEventChecker eventChecker = new LogEventChecker(this);
1969         if (eventChecker.start()) mEventChecker = eventChecker;
1970 
1971         return () -> {
1972             if (initialPid != null && initialPid.intValue() != getPid()) {
1973                 if (mOnLauncherCrashed != null) mOnLauncherCrashed.run();
1974                 checkForAnomaly();
1975                 Assert.fail(
1976                         formatSystemHealthMessage(
1977                                 formatErrorWithEvents("Launcher crashed", false)));
1978             }
1979 
1980             if (mEventChecker != null) {
1981                 mEventChecker = null;
1982                 if (mCheckEventsForSuccessfulGestures) {
1983                     final String message = eventChecker.verify(WAIT_TIME_MS, true);
1984                     if (message != null) {
1985                         dumpDiagnostics(message);
1986                         checkForAnomaly();
1987                         Assert.fail(formatSystemHealthMessage(
1988                                 "http://go/tapl : successful gesture produced " + message));
1989                     }
1990                 } else {
1991                     eventChecker.finishNoWait();
1992                 }
1993             }
1994         };
1995     }
1996 
1997     boolean isLauncher3() {
1998         if (mIsLauncher3 == null) {
1999             mIsLauncher3 = "com.android.launcher3".equals(getLauncherPackageName());
2000         }
2001         return mIsLauncher3;
2002     }
2003 
2004     void expectEvent(String sequence, Pattern expected) {
2005         if (mEventChecker != null) {
2006             mEventChecker.expectPattern(sequence, expected);
2007         } else {
2008             Log.d(TAG, "Expecting: " + sequence + " / " + expected);
2009         }
2010     }
2011 
2012     Rect getVisibleBounds(UiObject2 object) {
2013         try {
2014             return object.getVisibleBounds();
2015         } catch (StaleObjectException e) {
2016             fail("Object disappeared from screen");
2017             return null;
2018         } catch (Throwable t) {
2019             fail(t.toString());
2020             return null;
2021         }
2022     }
2023 
2024     float getWindowCornerRadius() {
2025         // TODO(b/197326121): Check if the touch is overlapping with the corners by offsetting
2026         final float tmpBuffer = 100f;
2027         final Resources resources = getResources();
2028         if (!supportsRoundedCornersOnWindows(resources)) {
2029             Log.d(TAG, "No rounded corners");
2030             return tmpBuffer;
2031         }
2032 
2033         // Radius that should be used in case top or bottom aren't defined.
2034         float defaultRadius = ResourceUtils.getDimenByName("rounded_corner_radius", resources, 0);
2035 
2036         float topRadius = ResourceUtils.getDimenByName("rounded_corner_radius_top", resources, 0);
2037         if (topRadius == 0f) {
2038             topRadius = defaultRadius;
2039         }
2040         float bottomRadius = ResourceUtils.getDimenByName(
2041                 "rounded_corner_radius_bottom", resources, 0);
2042         if (bottomRadius == 0f) {
2043             bottomRadius = defaultRadius;
2044         }
2045 
2046         // Always use the smallest radius to make sure the rounded corners will
2047         // completely cover the display.
2048         Log.d(TAG, "Rounded corners top: " + topRadius + " bottom: " + bottomRadius);
2049         return Math.max(topRadius, bottomRadius) + tmpBuffer;
2050     }
2051 
2052     private static boolean supportsRoundedCornersOnWindows(Resources resources) {
2053         return ResourceUtils.getBoolByName(
2054                 "config_supportsRoundedCornersOnWindows", resources, false);
2055     }
2056 
2057     /**
2058      * Taps outside container to dismiss, centered vertically and halfway to the edge of the screen.
2059      *
2060      * @param container container to be dismissed
2061      * @param tapRight  tap on the right of the container if true, or left otherwise
2062      */
2063     void touchOutsideContainer(UiObject2 container, boolean tapRight) {
2064         touchOutsideContainer(container, tapRight, true);
2065     }
2066 
2067     /**
2068      * Taps outside the container, to the right or left, and centered vertically.
2069      *
2070      * @param tapRight      if true touches to the right of the container, otherwise touches on left
2071      * @param halfwayToEdge if true touches halfway to the screen edge, if false touches 1 px from
2072      *                      container
2073      */
2074     void touchOutsideContainer(UiObject2 container, boolean tapRight, boolean halfwayToEdge) {
2075         try (LauncherInstrumentation.Closable c = addContextLayer(
2076                 "want to tap outside container on the " + (tapRight ? "right" : "left"))) {
2077             Rect containerBounds = getVisibleBounds(container);
2078 
2079             int x;
2080             if (halfwayToEdge) {
2081                 x = tapRight
2082                         ? (containerBounds.right + getRealDisplaySize().x) / 2
2083                         : containerBounds.left / 2;
2084             } else {
2085                 x = tapRight
2086                         ? containerBounds.right + 1
2087                         : containerBounds.left - 1;
2088             }
2089             int y = containerBounds.top + containerBounds.height() / 2;
2090 
2091             final long downTime = SystemClock.uptimeMillis();
2092             final Point tapTarget = new Point(x, y);
2093             sendPointer(downTime, downTime, MotionEvent.ACTION_DOWN, tapTarget,
2094                     LauncherInstrumentation.GestureScope.DONT_EXPECT_PILFER);
2095             sendPointer(downTime, downTime, MotionEvent.ACTION_UP, tapTarget,
2096                     LauncherInstrumentation.GestureScope.DONT_EXPECT_PILFER);
2097         }
2098     }
2099 
2100     /**
2101      * Waits until a particular condition is true. Based on WaitMixin.
2102      */
2103     boolean waitAndGet(BooleanSupplier condition, long timeout, long interval) {
2104         long startTime = SystemClock.uptimeMillis();
2105 
2106         boolean result = condition.getAsBoolean();
2107         for (long elapsedTime = 0; !result; elapsedTime = SystemClock.uptimeMillis() - startTime) {
2108             if (elapsedTime >= timeout) {
2109                 break;
2110             }
2111             SystemClock.sleep(interval);
2112             result = condition.getAsBoolean();
2113         }
2114         return result;
2115     }
2116 }
2117