| /* |
| * Copyright (C) 2017 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); you may not |
| * use this file except in compliance with the License. You may obtain a copy of |
| * the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
| * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
| * License for the specific language governing permissions and limitations under |
| * the License. |
| */ |
| package com.android.launcher3.ui; |
| |
| import static androidx.test.InstrumentationRegistry.getInstrumentation; |
| |
| import static com.android.launcher3.ui.TaplTestsLauncher3.getAppPackageName; |
| import static com.android.launcher3.util.Executors.MAIN_EXECUTOR; |
| |
| import static org.junit.Assert.assertEquals; |
| import static org.junit.Assert.assertTrue; |
| |
| import android.content.BroadcastReceiver; |
| import android.content.ComponentName; |
| import android.content.Context; |
| import android.content.Intent; |
| import android.content.IntentFilter; |
| import android.content.pm.ActivityInfo; |
| import android.content.pm.LauncherActivityInfo; |
| import android.content.pm.LauncherApps; |
| import android.content.pm.PackageInfo; |
| import android.content.pm.PackageManager; |
| import android.os.Debug; |
| import android.os.Process; |
| import android.os.RemoteException; |
| import android.os.StrictMode; |
| import android.os.UserHandle; |
| import android.os.UserManager; |
| import android.util.Log; |
| |
| import androidx.test.InstrumentationRegistry; |
| import androidx.test.uiautomator.By; |
| import androidx.test.uiautomator.BySelector; |
| import androidx.test.uiautomator.UiDevice; |
| import androidx.test.uiautomator.Until; |
| |
| import com.android.launcher3.Launcher; |
| import com.android.launcher3.LauncherAppState; |
| import com.android.launcher3.LauncherSettings; |
| import com.android.launcher3.LauncherState; |
| import com.android.launcher3.Utilities; |
| import com.android.launcher3.model.data.ItemInfo; |
| import com.android.launcher3.statemanager.StateManager; |
| import com.android.launcher3.tapl.LauncherInstrumentation; |
| import com.android.launcher3.tapl.LauncherInstrumentation.ContainerType; |
| import com.android.launcher3.tapl.TestHelpers; |
| import com.android.launcher3.testcomponent.TestCommandReceiver; |
| import com.android.launcher3.testing.TestProtocol; |
| import com.android.launcher3.util.LooperExecutor; |
| import com.android.launcher3.util.PackageManagerHelper; |
| import com.android.launcher3.util.Wait; |
| import com.android.launcher3.util.WidgetUtils; |
| import com.android.launcher3.util.rule.FailureWatcher; |
| import com.android.launcher3.util.rule.LauncherActivityRule; |
| import com.android.launcher3.util.rule.ScreenRecordRule; |
| import com.android.launcher3.util.rule.ShellCommandRule; |
| import com.android.launcher3.util.rule.TestStabilityRule; |
| |
| import org.junit.After; |
| import org.junit.Assert; |
| import org.junit.Before; |
| import org.junit.Rule; |
| import org.junit.rules.RuleChain; |
| import org.junit.rules.TestRule; |
| |
| import java.io.IOException; |
| import java.lang.annotation.ElementType; |
| import java.lang.annotation.Retention; |
| import java.lang.annotation.RetentionPolicy; |
| import java.lang.annotation.Target; |
| import java.util.concurrent.Callable; |
| import java.util.concurrent.CountDownLatch; |
| import java.util.concurrent.TimeUnit; |
| import java.util.function.Consumer; |
| import java.util.function.Function; |
| import java.util.function.Supplier; |
| |
| /** |
| * Base class for all instrumentation tests providing various utility methods. |
| */ |
| public abstract class AbstractLauncherUiTest { |
| |
| public static final long DEFAULT_ACTIVITY_TIMEOUT = TimeUnit.SECONDS.toMillis(10); |
| public static final long DEFAULT_BROADCAST_TIMEOUT_SECS = 5; |
| |
| public static final long DEFAULT_UI_TIMEOUT = 10000; |
| private static final String TAG = "AbstractLauncherUiTest"; |
| |
| private static String sStrictmodeDetectedActivityLeak; |
| private static boolean sDumpWasGenerated = false; |
| private static boolean sActivityLeakReported; |
| private static final String SYSTEMUI_PACKAGE = "com.android.systemui"; |
| protected static final ActivityLeakTracker ACTIVITY_LEAK_TRACKER = new ActivityLeakTracker(); |
| |
| protected LooperExecutor mMainThreadExecutor = MAIN_EXECUTOR; |
| protected final UiDevice mDevice = UiDevice.getInstance(getInstrumentation()); |
| protected final LauncherInstrumentation mLauncher = new LauncherInstrumentation(); |
| protected Context mTargetContext; |
| protected String mTargetPackage; |
| private int mLauncherPid; |
| |
| static { |
| if (TestHelpers.isInLauncherProcess()) { |
| StrictMode.VmPolicy.Builder builder = |
| new StrictMode.VmPolicy.Builder() |
| .penaltyLog() |
| .penaltyListener(Runnable::run, violation -> { |
| if (sStrictmodeDetectedActivityLeak == null) { |
| sStrictmodeDetectedActivityLeak = violation.toString() + ", " |
| + dumpHprofData() + "."; |
| } |
| }); |
| StrictMode.setVmPolicy(builder.build()); |
| } |
| } |
| |
| public static void checkDetectedLeaks(LauncherInstrumentation launcher) { |
| if (sActivityLeakReported) return; |
| |
| if (sStrictmodeDetectedActivityLeak != null) { |
| // Report from the test thread strictmode violations detected in the main thread. |
| sActivityLeakReported = true; |
| Assert.fail(sStrictmodeDetectedActivityLeak); |
| } |
| |
| // Check whether activity leak detector has found leaked activities. |
| Wait.atMost(AbstractLauncherUiTest::getActivityLeakErrorMessage, |
| () -> { |
| launcher.forceGc(); |
| return MAIN_EXECUTOR.submit( |
| () -> ACTIVITY_LEAK_TRACKER.noLeakedActivities()).get(); |
| }, DEFAULT_UI_TIMEOUT, launcher); |
| } |
| |
| private static String getActivityLeakErrorMessage() { |
| sActivityLeakReported = true; |
| return "Activity leak detector has found leaked activities, " + dumpHprofData() + "."; |
| } |
| |
| public static String dumpHprofData() { |
| String result; |
| if (sDumpWasGenerated) { |
| Log.d("b/195319692", "dump has already been generated by another test", |
| new Exception()); |
| result = "dump has already been generated by another test"; |
| } else { |
| try { |
| final String fileName = |
| getInstrumentation().getTargetContext().getFilesDir().getPath() |
| + "/ActivityLeakHeapDump.hprof"; |
| if (TestHelpers.isInLauncherProcess()) { |
| Debug.dumpHprofData(fileName); |
| } else { |
| final UiDevice device = UiDevice.getInstance(getInstrumentation()); |
| device.executeShellCommand( |
| "am dumpheap " + device.getLauncherPackageName() + " " + fileName); |
| } |
| sDumpWasGenerated = true; |
| Log.d("b/195319692", "sDumpWasGenerated := true", new Exception()); |
| result = "memory dump filename: " + fileName; |
| } catch (Throwable e) { |
| Log.e(TAG, "dumpHprofData failed", e); |
| result = "failed to save memory dump"; |
| } |
| } |
| return result |
| + ". Full list of activities: " + ACTIVITY_LEAK_TRACKER.getActivitiesList(); |
| } |
| |
| protected AbstractLauncherUiTest() { |
| mLauncher.enableCheckEventsForSuccessfulGestures(); |
| try { |
| mDevice.setOrientationNatural(); |
| } catch (RemoteException e) { |
| throw new RuntimeException(e); |
| } |
| if (TestHelpers.isInLauncherProcess()) { |
| Utilities.enableRunningInTestHarnessForTests(); |
| mLauncher.setSystemHealthSupplier(startTime -> TestCommandReceiver.callCommand( |
| TestCommandReceiver.GET_SYSTEM_HEALTH_MESSAGE, startTime.toString()). |
| getString("result")); |
| mLauncher.setOnSettledStateAction( |
| containerType -> executeOnLauncher( |
| launcher -> |
| checkLauncherIntegrity(launcher, containerType))); |
| } |
| mLauncher.enableDebugTracing(); |
| // Avoid double-reporting of Launcher crashes. |
| mLauncher.setOnLauncherCrashed(() -> mLauncherPid = 0); |
| } |
| |
| protected final LauncherActivityRule mActivityMonitor = new LauncherActivityRule(); |
| |
| @Rule |
| public ShellCommandRule mDisableHeadsUpNotification = |
| ShellCommandRule.disableHeadsUpNotification(); |
| |
| @Rule |
| public ScreenRecordRule mScreenRecordRule = new ScreenRecordRule(); |
| |
| protected void clearPackageData(String pkg) throws IOException, InterruptedException { |
| final CountDownLatch count = new CountDownLatch(2); |
| final BroadcastReceiver broadcastReceiver = new BroadcastReceiver() { |
| @Override |
| public void onReceive(Context context, Intent intent) { |
| count.countDown(); |
| } |
| }; |
| mTargetContext.registerReceiver(broadcastReceiver, |
| PackageManagerHelper.getPackageFilter(pkg, |
| Intent.ACTION_PACKAGE_RESTARTED, Intent.ACTION_PACKAGE_DATA_CLEARED)); |
| |
| mDevice.executeShellCommand("pm clear " + pkg); |
| assertTrue(pkg + " didn't restart", count.await(10, TimeUnit.SECONDS)); |
| mTargetContext.unregisterReceiver(broadcastReceiver); |
| } |
| |
| // Annotation for tests that need to be run in portrait and landscape modes. |
| @Retention(RetentionPolicy.RUNTIME) |
| @Target(ElementType.METHOD) |
| protected @interface PortraitLandscape { |
| } |
| |
| protected TestRule getRulesInsideActivityMonitor() { |
| final RuleChain inner = RuleChain.outerRule(new PortraitLandscapeRunner(this)) |
| .around(new FailureWatcher(mDevice, mLauncher)); |
| |
| return TestHelpers.isInLauncherProcess() |
| ? RuleChain.outerRule(ShellCommandRule.setDefaultLauncher()) |
| .around(inner) : |
| inner; |
| } |
| |
| @Rule |
| public TestRule mOrderSensitiveRules = RuleChain |
| .outerRule(new TestStabilityRule()) |
| .around(mActivityMonitor) |
| .around(getRulesInsideActivityMonitor()); |
| |
| public UiDevice getDevice() { |
| return mDevice; |
| } |
| |
| @Before |
| public void setUp() throws Exception { |
| mLauncher.onTestStart(); |
| Assert.assertTrue("Keyguard is visible, which is likely caused by a crash in SysUI", |
| TestHelpers.wait( |
| Until.gone(By.res(SYSTEMUI_PACKAGE, "keyguard_status_view")), 60000)); |
| |
| final String launcherPackageName = mDevice.getLauncherPackageName(); |
| try { |
| final Context context = InstrumentationRegistry.getContext(); |
| final PackageManager pm = context.getPackageManager(); |
| final PackageInfo launcherPackage = pm.getPackageInfo(launcherPackageName, 0); |
| |
| if (!launcherPackage.versionName.equals("BuildFromAndroidStudio")) { |
| Assert.assertEquals("Launcher version doesn't match tests version", |
| pm.getPackageInfo(context.getPackageName(), 0).getLongVersionCode(), |
| launcherPackage.getLongVersionCode()); |
| } |
| } catch (PackageManager.NameNotFoundException e) { |
| throw new RuntimeException(e); |
| } |
| |
| mLauncherPid = 0; |
| |
| mTargetContext = InstrumentationRegistry.getTargetContext(); |
| mTargetPackage = mTargetContext.getPackageName(); |
| mLauncherPid = mLauncher.getPid(); |
| |
| UserManager userManager = mTargetContext.getSystemService(UserManager.class); |
| if (userManager != null) { |
| for (UserHandle userHandle : userManager.getUserProfiles()) { |
| if (!userHandle.isSystem()) { |
| Log.d(TestProtocol.WORK_PROFILE_REMOVED, |
| "removing user " + userHandle.getIdentifier()); |
| mDevice.executeShellCommand("pm remove-user " + userHandle.getIdentifier()); |
| } |
| } |
| } |
| } |
| |
| @After |
| public void verifyLauncherState() { |
| try { |
| // Limits UI tests affecting tests running after them. |
| mLauncher.waitForLauncherInitialized(); |
| if (mLauncherPid != 0) { |
| assertEquals("Launcher crashed, pid mismatch:", |
| mLauncherPid, mLauncher.getPid().intValue()); |
| } |
| } finally { |
| mLauncher.onTestFinish(); |
| } |
| } |
| |
| protected void clearLauncherData() { |
| mLauncher.clearLauncherData(); |
| mLauncher.waitForLauncherInitialized(); |
| } |
| |
| /** |
| * Removes all icons from homescreen and hotseat. |
| */ |
| public void clearHomescreen() throws Throwable { |
| LauncherSettings.Settings.call(mTargetContext.getContentResolver(), |
| LauncherSettings.Settings.METHOD_CREATE_EMPTY_DB); |
| LauncherSettings.Settings.call(mTargetContext.getContentResolver(), |
| LauncherSettings.Settings.METHOD_CLEAR_EMPTY_DB_FLAG); |
| resetLoaderState(); |
| } |
| |
| protected void resetLoaderState() { |
| try { |
| mMainThreadExecutor.execute( |
| () -> LauncherAppState.getInstance( |
| mTargetContext).getModel().forceReload()); |
| } catch (Throwable t) { |
| throw new IllegalArgumentException(t); |
| } |
| mLauncher.waitForLauncherInitialized(); |
| } |
| |
| /** |
| * Adds {@param item} on the homescreen on the 0th screen |
| */ |
| protected void addItemToScreen(ItemInfo item) { |
| WidgetUtils.addItemToScreen(item, mTargetContext); |
| resetLoaderState(); |
| |
| // Launch the home activity |
| mDevice.pressHome(); |
| mLauncher.waitForLauncherInitialized(); |
| } |
| |
| /** |
| * Runs the callback on the UI thread and returns the result. |
| */ |
| protected <T> T getOnUiThread(final Callable<T> callback) { |
| try { |
| return mMainThreadExecutor.submit(callback).get(); |
| } catch (Throwable e) { |
| throw new RuntimeException(e); |
| } |
| } |
| |
| protected <T> T getFromLauncher(Function<Launcher, T> f) { |
| if (!TestHelpers.isInLauncherProcess()) return null; |
| return getOnUiThread(() -> f.apply(mActivityMonitor.getActivity())); |
| } |
| |
| protected void executeOnLauncher(Consumer<Launcher> f) { |
| getFromLauncher(launcher -> { |
| f.accept(launcher); |
| return null; |
| }); |
| } |
| |
| // Cannot be used in TaplTests between a Tapl call injecting a gesture and a tapl call |
| // expecting |
| // the results of that gesture because the wait can hide flakeness. |
| protected void waitForState(String message, Supplier<LauncherState> state) { |
| waitForLauncherCondition(message, |
| launcher -> launcher.getStateManager().getCurrentStableState() == state.get()); |
| } |
| |
| protected void waitForResumed(String message) { |
| waitForLauncherCondition(message, launcher -> launcher.hasBeenResumed()); |
| } |
| |
| // Cannot be used in TaplTests after injecting any gesture using Tapl because this can hide |
| // flakiness. |
| protected void waitForLauncherCondition(String |
| message, Function<Launcher, Boolean> condition) { |
| waitForLauncherCondition(message, condition, DEFAULT_ACTIVITY_TIMEOUT); |
| } |
| |
| // Cannot be used in TaplTests after injecting any gesture using Tapl because this can hide |
| // flakiness. |
| protected <T> T getOnceNotNull(String message, Function<Launcher, T> f) { |
| return getOnceNotNull(message, f, DEFAULT_ACTIVITY_TIMEOUT); |
| } |
| |
| // Cannot be used in TaplTests after injecting any gesture using Tapl because this can hide |
| // flakiness. |
| protected void waitForLauncherCondition( |
| String message, Function<Launcher, Boolean> condition, long timeout) { |
| if (!TestHelpers.isInLauncherProcess()) return; |
| Wait.atMost(message, () -> getFromLauncher(condition), timeout, mLauncher); |
| } |
| |
| // Cannot be used in TaplTests after injecting any gesture using Tapl because this can hide |
| // flakiness. |
| protected <T> T getOnceNotNull(String message, Function<Launcher, T> f, long timeout) { |
| if (!TestHelpers.isInLauncherProcess()) return null; |
| |
| final Object[] output = new Object[1]; |
| Wait.atMost(message, () -> { |
| final Object fromLauncher = getFromLauncher(f); |
| output[0] = fromLauncher; |
| return fromLauncher != null; |
| }, timeout, mLauncher); |
| return (T) output[0]; |
| } |
| |
| // Cannot be used in TaplTests after injecting any gesture using Tapl because this can hide |
| // flakiness. |
| protected void waitForLauncherCondition( |
| String message, |
| Runnable testThreadAction, Function<Launcher, Boolean> condition, |
| long timeout) { |
| if (!TestHelpers.isInLauncherProcess()) return; |
| Wait.atMost(message, () -> { |
| testThreadAction.run(); |
| return getFromLauncher(condition); |
| }, timeout, mLauncher); |
| } |
| |
| protected LauncherActivityInfo getSettingsApp() { |
| return mTargetContext.getSystemService(LauncherApps.class) |
| .getActivityList("com.android.settings", Process.myUserHandle()).get(0); |
| } |
| |
| /** |
| * Broadcast receiver which blocks until the result is received. |
| */ |
| public class BlockingBroadcastReceiver extends BroadcastReceiver { |
| |
| private final CountDownLatch latch = new CountDownLatch(1); |
| private Intent mIntent; |
| |
| public BlockingBroadcastReceiver(String action) { |
| mTargetContext.registerReceiver(this, new IntentFilter(action)); |
| } |
| |
| @Override |
| public void onReceive(Context context, Intent intent) { |
| mIntent = intent; |
| latch.countDown(); |
| } |
| |
| public Intent blockingGetIntent() throws InterruptedException { |
| latch.await(DEFAULT_BROADCAST_TIMEOUT_SECS, TimeUnit.SECONDS); |
| mTargetContext.unregisterReceiver(this); |
| return mIntent; |
| } |
| |
| public Intent blockingGetExtraIntent() throws InterruptedException { |
| Intent intent = blockingGetIntent(); |
| return intent == null ? null : (Intent) intent.getParcelableExtra( |
| Intent.EXTRA_INTENT); |
| } |
| } |
| |
| public static void startAppFast(String packageName) { |
| startIntent( |
| getInstrumentation().getContext().getPackageManager().getLaunchIntentForPackage( |
| packageName), |
| By.pkg(packageName).depth(0), |
| true /* newTask */); |
| } |
| |
| public static void startTestActivity(int activityNumber) { |
| final String packageName = getAppPackageName(); |
| final Intent intent = getInstrumentation().getContext().getPackageManager(). |
| getLaunchIntentForPackage(packageName); |
| intent.setComponent(new ComponentName(packageName, |
| "com.android.launcher3.tests.Activity" + activityNumber)); |
| startIntent(intent, By.pkg(packageName).text("TestActivity" + activityNumber), |
| false /* newTask */); |
| } |
| |
| private static void startIntent(Intent intent, BySelector selector, boolean newTask) { |
| intent.addCategory(Intent.CATEGORY_LAUNCHER); |
| if (newTask) { |
| intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); |
| } else { |
| intent.addFlags( |
| Intent.FLAG_ACTIVITY_MULTIPLE_TASK | Intent.FLAG_ACTIVITY_NEW_DOCUMENT); |
| } |
| getInstrumentation().getTargetContext().startActivity(intent); |
| assertTrue("App didn't start: " + selector, |
| TestHelpers.wait(Until.hasObject(selector), DEFAULT_UI_TIMEOUT)); |
| } |
| |
| public static ActivityInfo resolveSystemAppInfo(String category) { |
| return getInstrumentation().getContext().getPackageManager().resolveActivity( |
| new Intent(Intent.ACTION_MAIN).addCategory(category), |
| PackageManager.MATCH_SYSTEM_ONLY). |
| activityInfo; |
| } |
| |
| |
| public static String resolveSystemApp(String category) { |
| return resolveSystemAppInfo(category).packageName; |
| } |
| |
| protected void closeLauncherActivity() { |
| // Destroy Launcher activity. |
| executeOnLauncher(launcher -> { |
| if (launcher != null) { |
| onLauncherActivityClose(launcher); |
| launcher.finish(); |
| } |
| }); |
| waitForLauncherCondition( |
| "Launcher still active", launcher -> launcher == null, DEFAULT_UI_TIMEOUT); |
| } |
| |
| protected boolean isInBackground(Launcher launcher) { |
| return launcher == null || !launcher.hasBeenResumed(); |
| } |
| |
| protected boolean isInState(Supplier<LauncherState> state) { |
| if (!TestHelpers.isInLauncherProcess()) return true; |
| return getFromLauncher( |
| launcher -> launcher.getStateManager().getState() == state.get()); |
| } |
| |
| protected int getAllAppsScroll(Launcher launcher) { |
| return launcher.getAppsView().getActiveRecyclerView().getCurrentScrollY(); |
| } |
| |
| private void checkLauncherIntegrity( |
| Launcher launcher, ContainerType expectedContainerType) { |
| if (launcher != null) { |
| final StateManager<LauncherState> stateManager = launcher.getStateManager(); |
| final LauncherState stableState = stateManager.getCurrentStableState(); |
| |
| assertTrue("Stable state != state: " + stableState.getClass().getSimpleName() + ", " |
| + stateManager.getState().getClass().getSimpleName(), |
| stableState == stateManager.getState()); |
| |
| final boolean isResumed = launcher.hasBeenResumed(); |
| final boolean isStarted = launcher.isStarted(); |
| checkLauncherState(launcher, expectedContainerType, isResumed, isStarted); |
| |
| final int ordinal = stableState.ordinal; |
| |
| switch (expectedContainerType) { |
| case WORKSPACE: |
| case WIDGETS: { |
| assertTrue( |
| "Launcher is not resumed in state: " + expectedContainerType, |
| isResumed); |
| assertTrue(TestProtocol.stateOrdinalToString(ordinal), |
| ordinal == TestProtocol.NORMAL_STATE_ORDINAL); |
| break; |
| } |
| case ALL_APPS: { |
| assertTrue( |
| "Launcher is not resumed in state: " + expectedContainerType, |
| isResumed); |
| assertTrue(TestProtocol.stateOrdinalToString(ordinal), |
| ordinal == TestProtocol.ALL_APPS_STATE_ORDINAL); |
| break; |
| } |
| case OVERVIEW: { |
| checkLauncherStateInOverview(launcher, expectedContainerType, isStarted, |
| isResumed); |
| assertTrue(TestProtocol.stateOrdinalToString(ordinal), |
| ordinal == TestProtocol.OVERVIEW_STATE_ORDINAL); |
| break; |
| } |
| case BACKGROUND: { |
| assertTrue("Launcher is resumed in state: " + expectedContainerType, |
| !isResumed); |
| assertTrue(TestProtocol.stateOrdinalToString(ordinal), |
| ordinal == TestProtocol.NORMAL_STATE_ORDINAL); |
| break; |
| } |
| default: |
| throw new IllegalArgumentException( |
| "Illegal container: " + expectedContainerType); |
| } |
| } else { |
| assertTrue( |
| "Container type is not BACKGROUND or FALLBACK_OVERVIEW: " |
| + expectedContainerType, |
| expectedContainerType == ContainerType.BACKGROUND || |
| expectedContainerType == ContainerType.FALLBACK_OVERVIEW); |
| } |
| } |
| |
| protected void checkLauncherState(Launcher launcher, ContainerType expectedContainerType, |
| boolean isResumed, boolean isStarted) { |
| assertTrue("hasBeenResumed() != isStarted(), hasBeenResumed(): " + isResumed, |
| isResumed == isStarted); |
| assertTrue("hasBeenResumed() != isUserActive(), hasBeenResumed(): " + isResumed, |
| isResumed == launcher.isUserActive()); |
| } |
| |
| protected void checkLauncherStateInOverview(Launcher launcher, |
| ContainerType expectedContainerType, boolean isStarted, boolean isResumed) { |
| assertTrue("Launcher is not resumed in state: " + expectedContainerType, |
| isResumed); |
| } |
| |
| protected void onLauncherActivityClose(Launcher launcher) { |
| } |
| } |