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