• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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