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