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