1 /* 2 * Copyright (C) 2024 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.ui; 18 19 import static android.os.Process.myUserHandle; 20 import static android.platform.test.flag.junit.SetFlagsRule.DefaultInitValueType.DEVICE_DEFAULT; 21 22 import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation; 23 24 import static com.android.launcher3.testing.shared.TestProtocol.ICON_MISSING; 25 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR; 26 27 import static org.junit.Assert.assertEquals; 28 import static org.junit.Assert.assertTrue; 29 30 import android.app.ActivityManager; 31 import android.content.ComponentName; 32 import android.content.Context; 33 import android.content.Intent; 34 import android.content.pm.ActivityInfo; 35 import android.content.pm.PackageInfo; 36 import android.content.pm.PackageManager; 37 import android.graphics.Point; 38 import android.os.Debug; 39 import android.os.RemoteException; 40 import android.os.UserHandle; 41 import android.os.UserManager; 42 import android.platform.test.flag.junit.SetFlagsRule; 43 import android.platform.test.rule.LimitDevicesRule; 44 import android.util.Log; 45 46 import androidx.annotation.NonNull; 47 import androidx.test.InstrumentationRegistry; 48 import androidx.test.uiautomator.By; 49 import androidx.test.uiautomator.BySelector; 50 import androidx.test.uiautomator.UiDevice; 51 import androidx.test.uiautomator.Until; 52 53 import com.android.launcher3.celllayout.FavoriteItemsTransaction; 54 import com.android.launcher3.tapl.HomeAllApps; 55 import com.android.launcher3.tapl.HomeAppIcon; 56 import com.android.launcher3.tapl.LauncherInstrumentation; 57 import com.android.launcher3.tapl.TestHelpers; 58 import com.android.launcher3.util.TestUtil; 59 import com.android.launcher3.util.Wait; 60 import com.android.launcher3.util.rule.ExtendedLongPressTimeoutRule; 61 import com.android.launcher3.util.rule.FailureWatcher; 62 import com.android.launcher3.util.rule.SamplerRule; 63 import com.android.launcher3.util.rule.ScreenRecordRule; 64 import com.android.launcher3.util.rule.ShellCommandRule; 65 import com.android.launcher3.util.rule.TestIsolationRule; 66 import com.android.launcher3.util.rule.TestStabilityRule; 67 68 import org.junit.After; 69 import org.junit.Assert; 70 import org.junit.Before; 71 import org.junit.Rule; 72 import org.junit.rules.RuleChain; 73 import org.junit.rules.TestRule; 74 75 import java.io.IOException; 76 import java.util.concurrent.TimeUnit; 77 78 /** 79 * Base class for all TAPL tests in Launcher providing various utility methods. 80 */ 81 public abstract class BaseLauncherTaplTest { 82 83 public static final long DEFAULT_ACTIVITY_TIMEOUT = TimeUnit.SECONDS.toMillis(10); 84 public static final long DEFAULT_BROADCAST_TIMEOUT_SECS = 10; 85 86 public static final long DEFAULT_UI_TIMEOUT = TestUtil.DEFAULT_UI_TIMEOUT; 87 private static final String TAG = "BaseLauncherTaplTest"; 88 89 private static final long BYTES_PER_MEGABYTE = 1 << 20; 90 91 private static boolean sDumpWasGenerated = false; 92 private static boolean sActivityLeakReported = false; 93 private static boolean sSeenKeyguard = false; 94 private static boolean sFirstTimeWaitingForWizard = true; 95 96 private static final String SYSTEMUI_PACKAGE = "com.android.systemui"; 97 98 protected final UiDevice mDevice = getUiDevice(); 99 protected final LauncherInstrumentation mLauncher = createLauncherInstrumentation(); 100 101 @NonNull createLauncherInstrumentation()102 public static LauncherInstrumentation createLauncherInstrumentation() { 103 waitForSetupWizardDismissal(); // precondition for creating LauncherInstrumentation 104 return new LauncherInstrumentation(true); 105 } 106 107 protected Context mTargetContext; 108 protected String mTargetPackage; 109 private int mLauncherPid; 110 111 private final ActivityManager.MemoryInfo mMemoryInfo = new ActivityManager.MemoryInfo(); 112 private final ActivityManager mActivityManager; 113 private long mMemoryBefore; 114 115 /** Detects activity leaks and throws an exception if a leak is found. */ checkDetectedLeaks(LauncherInstrumentation launcher)116 public static void checkDetectedLeaks(LauncherInstrumentation launcher) { 117 checkDetectedLeaks(launcher, false); 118 } 119 120 /** Detects activity leaks and throws an exception if a leak is found. */ checkDetectedLeaks(LauncherInstrumentation launcher, boolean requireOneActiveActivityUnused)121 public static void checkDetectedLeaks(LauncherInstrumentation launcher, 122 boolean requireOneActiveActivityUnused) { 123 if (TestStabilityRule.isPresubmit()) return; // b/313501215 124 125 final boolean requireOneActiveActivity = 126 false; // workaround for leaks when there is an unexpected Recents activity 127 128 if (sActivityLeakReported) return; 129 130 // Check whether activity leak detector has found leaked activities. 131 Wait.atMost(() -> getActivityLeakErrorMessage(launcher, requireOneActiveActivity), 132 () -> { 133 launcher.forceGc(); 134 return MAIN_EXECUTOR.submit( 135 () -> launcher.noLeakedActivities(requireOneActiveActivity)).get(); 136 }, launcher, DEFAULT_UI_TIMEOUT); 137 } 138 getAppPackageName()139 public static String getAppPackageName() { 140 return getInstrumentation().getContext().getPackageName(); 141 } 142 getActivityLeakErrorMessage(LauncherInstrumentation launcher, boolean requireOneActiveActivity)143 private static String getActivityLeakErrorMessage(LauncherInstrumentation launcher, 144 boolean requireOneActiveActivity) { 145 sActivityLeakReported = true; 146 return "Activity leak detector has found leaked activities, requirining 1 activity: " 147 + requireOneActiveActivity + "; " 148 + dumpHprofData(launcher, false, requireOneActiveActivity) + "."; 149 } 150 dumpHprofData(LauncherInstrumentation launcher, boolean intentionalLeak, boolean requireOneActiveActivity)151 private static String dumpHprofData(LauncherInstrumentation launcher, boolean intentionalLeak, 152 boolean requireOneActiveActivity) { 153 if (intentionalLeak) return "intentional leak; not generating dump"; 154 155 String result; 156 if (sDumpWasGenerated) { 157 result = "dump has already been generated by another test"; 158 } else { 159 try { 160 final String fileName = 161 getInstrumentation().getTargetContext().getFilesDir().getPath() 162 + "/ActivityLeakHeapDump.hprof"; 163 if (TestHelpers.isInLauncherProcess()) { 164 Debug.dumpHprofData(fileName); 165 } else { 166 final UiDevice device = getUiDevice(); 167 device.executeShellCommand( 168 "am dumpheap " + device.getLauncherPackageName() + " " + fileName); 169 } 170 Log.d(TAG, "Saved leak dump, the leak is still present: " 171 + !launcher.noLeakedActivities(requireOneActiveActivity)); 172 sDumpWasGenerated = true; 173 result = "saved memory dump as an artifact"; 174 } catch (Throwable e) { 175 Log.e(TAG, "dumpHprofData failed", e); 176 result = "failed to save memory dump"; 177 } 178 } 179 return result + ". Full list of activities: " + launcher.getRootedActivitiesList(); 180 } 181 BaseLauncherTaplTest()182 protected BaseLauncherTaplTest() { 183 mActivityManager = InstrumentationRegistry.getContext() 184 .getSystemService(ActivityManager.class); 185 mLauncher.enableCheckEventsForSuccessfulGestures(); 186 mLauncher.setAnomalyChecker(BaseLauncherTaplTest::verifyKeyguardInvisible); 187 try { 188 mDevice.setOrientationNatural(); 189 } catch (RemoteException e) { 190 throw new RuntimeException(e); 191 } 192 mLauncher.enableDebugTracing(); 193 // Avoid double-reporting of Launcher crashes. 194 mLauncher.setOnLauncherCrashed(() -> mLauncherPid = 0); 195 } 196 197 @Rule 198 public ShellCommandRule mDisableHeadsUpNotification = 199 ShellCommandRule.disableHeadsUpNotification(); 200 201 @Rule 202 public ScreenRecordRule mScreenRecordRule = new ScreenRecordRule(); 203 204 @Rule 205 public SetFlagsRule mSetFlagsRule = new SetFlagsRule(DEVICE_DEFAULT); 206 207 @Rule 208 public ExtendedLongPressTimeoutRule mLongPressTimeoutRule = new ExtendedLongPressTimeoutRule(); 209 210 @Rule 211 public LimitDevicesRule mlimitDevicesRule = new LimitDevicesRule(); 212 performInitialization()213 protected void performInitialization() { 214 reinitializeLauncherData(); 215 mDevice.pressHome(); 216 // Check that we switched to home. 217 mLauncher.getWorkspace(); 218 checkDetectedLeaks(mLauncher, true); 219 } 220 clearPackageData(String pkg)221 protected void clearPackageData(String pkg) throws IOException, InterruptedException { 222 assertTrue("pm clear command failed", 223 mDevice.executeShellCommand( 224 String.format("pm clear --user %d %s", myUserHandle().getIdentifier(), pkg)) 225 .contains("Success")); 226 assertTrue("pm wait-for-handler command failed", 227 mDevice.executeShellCommand("pm wait-for-handler") 228 .contains("Success")); 229 } 230 getRulesInsideActivityMonitor()231 protected TestRule getRulesInsideActivityMonitor() { 232 final RuleChain inner = RuleChain 233 .outerRule(new FailureWatcher(mLauncher, null)) 234 .around(new TestIsolationRule(mLauncher, true)); 235 return TestHelpers.isInLauncherProcess() 236 ? RuleChain.outerRule(ShellCommandRule.setDefaultLauncher()).around(inner) 237 : inner; 238 } 239 240 @Rule 241 public TestRule mOrderSensitiveRules = RuleChain 242 .outerRule(new SamplerRule()) 243 .around(new TestStabilityRule()) 244 .around(getRulesInsideActivityMonitor()); 245 getDevice()246 public UiDevice getDevice() { 247 return mDevice; 248 } 249 250 @Before setUp()251 public void setUp() throws Exception { 252 mLauncher.onTestStart(); 253 254 final String launcherPackageName = mDevice.getLauncherPackageName(); 255 try { 256 final Context context = InstrumentationRegistry.getContext(); 257 final PackageManager pm = context.getPackageManager(); 258 final PackageInfo launcherPackage = pm.getPackageInfo(launcherPackageName, 0); 259 260 if (!launcherPackage.versionName.equals("BuildFromAndroidStudio")) { 261 Assert.assertEquals("Launcher version doesn't match tests version", 262 pm.getPackageInfo(context.getPackageName(), 0).getLongVersionCode(), 263 launcherPackage.getLongVersionCode()); 264 } 265 } catch (PackageManager.NameNotFoundException e) { 266 throw new RuntimeException(e); 267 } 268 269 mLauncherPid = 0; 270 271 mTargetContext = InstrumentationRegistry.getTargetContext(); 272 mTargetPackage = mTargetContext.getPackageName(); 273 mLauncherPid = mLauncher.getPid(); 274 275 UserManager userManager = mTargetContext.getSystemService(UserManager.class); 276 if (userManager != null) { 277 for (UserHandle userHandle : userManager.getUserProfiles()) { 278 if (!userHandle.isSystem()) { 279 mDevice.executeShellCommand( 280 "pm remove-user --wait " + userHandle.getIdentifier()); 281 } 282 } 283 } 284 285 onTestStart(); 286 performInitialization(); 287 } 288 getAvailableMemory()289 private long getAvailableMemory() { 290 mActivityManager.getMemoryInfo(mMemoryInfo); 291 292 return Math.divideExact(mMemoryInfo.availMem, BYTES_PER_MEGABYTE); 293 } 294 295 @Before saveMemoryBefore()296 public void saveMemoryBefore() { 297 mMemoryBefore = getAvailableMemory(); 298 } 299 300 @After logMemoryAfter()301 public void logMemoryAfter() { 302 long memoryAfter = getAvailableMemory(); 303 304 Log.d(TAG, "Available memory: before=" + mMemoryBefore 305 + "MB, after=" + memoryAfter 306 + "MB, delta=" + (memoryAfter - mMemoryBefore) + "MB"); 307 } 308 309 /** Method that should be called when a test starts. */ onTestStart()310 public static void onTestStart() { 311 waitForSetupWizardDismissal(); 312 313 if (TestStabilityRule.isPresubmit()) { 314 aggressivelyUnlockSysUi(); 315 } else { 316 verifyKeyguardInvisible(); 317 } 318 } 319 hasSystemUiObject(String resId)320 private static boolean hasSystemUiObject(String resId) { 321 return getUiDevice().hasObject( 322 By.res(SYSTEMUI_PACKAGE, resId)); 323 } 324 325 @NonNull getUiDevice()326 private static UiDevice getUiDevice() { 327 return UiDevice.getInstance(getInstrumentation()); 328 } 329 aggressivelyUnlockSysUi()330 private static void aggressivelyUnlockSysUi() { 331 final UiDevice device = getUiDevice(); 332 for (int i = 0; i < 10 && hasSystemUiObject("keyguard_status_view"); ++i) { 333 Log.d(TAG, "Before attempting to unlock the phone"); 334 try { 335 device.executeShellCommand("input keyevent 82"); 336 } catch (IOException e) { 337 throw new RuntimeException(e); 338 } 339 device.waitForIdle(); 340 } 341 Assert.assertTrue("Keyguard still visible", 342 TestHelpers.wait( 343 Until.gone(By.res(SYSTEMUI_PACKAGE, "keyguard_status_view")), 60000)); 344 Log.d(TAG, "Keyguard is not visible"); 345 } 346 347 /** Waits for setup wizard to go away. */ waitForSetupWizardDismissal()348 private static void waitForSetupWizardDismissal() { 349 if (sFirstTimeWaitingForWizard) { 350 try { 351 getUiDevice().executeShellCommand( 352 "am force-stop com.google.android.setupwizard"); 353 } catch (IOException e) { 354 throw new RuntimeException(e); 355 } 356 } 357 358 final boolean wizardDismissed = TestHelpers.wait( 359 Until.gone(By.pkg("com.google.android.setupwizard").depth(0)), 360 sFirstTimeWaitingForWizard ? 120000 : 0); 361 sFirstTimeWaitingForWizard = false; 362 Assert.assertTrue("Setup wizard is still visible", wizardDismissed); 363 } 364 365 /** Asserts that keyguard is not visible */ verifyKeyguardInvisible()366 public static void verifyKeyguardInvisible() { 367 final boolean keyguardAlreadyVisible = sSeenKeyguard; 368 369 sSeenKeyguard = sSeenKeyguard 370 || !TestHelpers.wait( 371 Until.gone(By.res(SYSTEMUI_PACKAGE, "keyguard_status_view")), 60000); 372 373 Assert.assertFalse( 374 "Keyguard is visible, which is likely caused by a crash in SysUI, seeing keyguard" 375 + " for the first time = " 376 + !keyguardAlreadyVisible, 377 sSeenKeyguard); 378 } 379 380 @After resetFreezeRecentTaskList()381 public void resetFreezeRecentTaskList() { 382 try { 383 mDevice.executeShellCommand("wm reset-freeze-recent-tasks"); 384 } catch (IOException e) { 385 Log.e(TAG, "Failed to reset fozen recent tasks list", e); 386 } 387 } 388 389 @After verifyLauncherState()390 public void verifyLauncherState() { 391 try { 392 // Limits UI tests affecting tests running after them. 393 mDevice.pressHome(); 394 mLauncher.waitForLauncherInitialized(); 395 if (mLauncherPid != 0) { 396 assertEquals("Launcher crashed, pid mismatch:", 397 mLauncherPid, mLauncher.getPid().intValue()); 398 } 399 } finally { 400 mLauncher.onTestFinish(); 401 } 402 } 403 reinitializeLauncherData()404 protected void reinitializeLauncherData() { 405 reinitializeLauncherData(false); 406 } 407 reinitializeLauncherData(boolean clearWorkspace)408 protected void reinitializeLauncherData(boolean clearWorkspace) { 409 if (clearWorkspace) { 410 mLauncher.clearLauncherData(); 411 } else { 412 mLauncher.reinitializeLauncherData(); 413 } 414 mLauncher.waitForLauncherInitialized(); 415 } 416 startAppFast(String packageName)417 public static void startAppFast(String packageName) { 418 startIntent( 419 getInstrumentation().getContext().getPackageManager().getLaunchIntentForPackage( 420 packageName), 421 By.pkg(packageName).depth(0), 422 true /* newTask */); 423 } 424 startTestActivity(String activityName, String activityLabel)425 public static void startTestActivity(String activityName, String activityLabel) { 426 final String packageName = getAppPackageName(); 427 final Intent intent = getInstrumentation().getContext().getPackageManager() 428 .getLaunchIntentForPackage(packageName); 429 intent.setComponent(new ComponentName(packageName, 430 "com.android.launcher3.tests." + activityName)); 431 startIntent(intent, By.pkg(packageName).text(activityLabel), 432 false /* newTask */); 433 } 434 startTestActivity(int activityNumber)435 public static void startTestActivity(int activityNumber) { 436 startTestActivity("Activity" + activityNumber, "TestActivity" + activityNumber); 437 } 438 startImeTestActivity()439 public static void startImeTestActivity() { 440 final String packageName = getAppPackageName(); 441 final Intent intent = getInstrumentation().getContext().getPackageManager() 442 .getLaunchIntentForPackage(packageName); 443 intent.setComponent(new ComponentName(packageName, 444 "com.android.launcher3.testcomponent.ImeTestActivity")); 445 startIntent(intent, By.pkg(packageName).text("ImeTestActivity"), 446 false /* newTask */); 447 } 448 449 /** Starts ExcludeFromRecentsTestActivity, which has excludeFromRecents="true". */ startExcludeFromRecentsTestActivity()450 public static void startExcludeFromRecentsTestActivity() { 451 final String packageName = getAppPackageName(); 452 final Intent intent = getInstrumentation().getContext().getPackageManager() 453 .getLaunchIntentForPackage(packageName); 454 intent.setComponent(new ComponentName(packageName, 455 "com.android.launcher3.testcomponent.ExcludeFromRecentsTestActivity")); 456 startIntent(intent, By.pkg(packageName).text("ExcludeFromRecentsTestActivity"), 457 false /* newTask */); 458 } 459 startIntent(Intent intent, BySelector selector, boolean newTask)460 private static void startIntent(Intent intent, BySelector selector, boolean newTask) { 461 intent.addCategory(Intent.CATEGORY_LAUNCHER); 462 if (newTask) { 463 intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); 464 } else { 465 intent.addFlags( 466 Intent.FLAG_ACTIVITY_MULTIPLE_TASK | Intent.FLAG_ACTIVITY_NEW_DOCUMENT); 467 } 468 getInstrumentation().getTargetContext().startActivity(intent); 469 assertTrue("App didn't start: " + selector, 470 TestHelpers.wait(Until.hasObject(selector), DEFAULT_UI_TIMEOUT)); 471 472 // Wait for the Launcher to stop. 473 final LauncherInstrumentation launcherInstrumentation = new LauncherInstrumentation(); 474 Wait.atMost("Launcher activity didn't stop", 475 () -> !launcherInstrumentation.isLauncherActivityStarted(), 476 launcherInstrumentation, DEFAULT_ACTIVITY_TIMEOUT); 477 } 478 resolveSystemAppInfo(String category)479 public static ActivityInfo resolveSystemAppInfo(String category) { 480 return getInstrumentation().getContext().getPackageManager().resolveActivity( 481 new Intent(Intent.ACTION_MAIN).addCategory(category), 482 PackageManager.MATCH_SYSTEM_ONLY) 483 .activityInfo; 484 } 485 486 resolveSystemApp(String category)487 public static String resolveSystemApp(String category) { 488 return resolveSystemAppInfo(category).packageName; 489 } 490 createShortcutInCenterIfNotExist(String name)491 protected HomeAppIcon createShortcutInCenterIfNotExist(String name) { 492 Point dimension = mLauncher.getWorkspace().getIconGridDimensions(); 493 return createShortcutIfNotExist(name, dimension.x / 2, dimension.y / 2); 494 } 495 createShortcutIfNotExist(String name, Point cellPosition)496 protected HomeAppIcon createShortcutIfNotExist(String name, Point cellPosition) { 497 return createShortcutIfNotExist(name, cellPosition.x, cellPosition.y); 498 } 499 createShortcutIfNotExist(String name, int cellX, int cellY)500 protected HomeAppIcon createShortcutIfNotExist(String name, int cellX, int cellY) { 501 HomeAppIcon homeAppIcon = mLauncher.getWorkspace().tryGetWorkspaceAppIcon(name); 502 Log.d(ICON_MISSING, "homeAppIcon: " + homeAppIcon + " name: " + name 503 + " cell: " + cellX + ", " + cellY); 504 if (homeAppIcon == null) { 505 HomeAllApps allApps = mLauncher.getWorkspace().switchToAllApps(); 506 allApps.freeze(); 507 try { 508 allApps.getAppIcon(name).dragToWorkspace(cellX, cellY); 509 } finally { 510 allApps.unfreeze(); 511 } 512 homeAppIcon = mLauncher.getWorkspace().getWorkspaceAppIcon(name); 513 } 514 return homeAppIcon; 515 } 516 commitTransactionAndLoadHome(FavoriteItemsTransaction transaction)517 protected void commitTransactionAndLoadHome(FavoriteItemsTransaction transaction) { 518 transaction.commit(); 519 520 // Launch the home activity 521 UiDevice.getInstance(getInstrumentation()).pressHome(); 522 mLauncher.waitForLauncherInitialized(); 523 } 524 525 /** Clears all recent tasks */ clearAllRecentTasks()526 protected void clearAllRecentTasks() { 527 if (!mLauncher.getRecentTasks().isEmpty()) { 528 mLauncher.goHome().switchToOverview().dismissAllTasks(); 529 } 530 } 531 } 532