/* * 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.systemui.recents; import static android.app.Flags.keyguardPrivateNotifications; import static android.content.Intent.ACTION_PACKAGE_ADDED; import static android.content.Intent.EXTRA_CHANGED_COMPONENT_NAME_LIST; import static android.content.pm.PackageManager.MATCH_SYSTEM_ONLY; import static android.view.MotionEvent.ACTION_CANCEL; import static android.view.MotionEvent.ACTION_DOWN; import static android.view.MotionEvent.ACTION_UP; import static android.view.WindowManagerPolicyConstants.NAV_BAR_MODE_3BUTTON; import static android.window.BackEvent.EDGE_NONE; import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_AWAKE; import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_BOUNCER_SHOWING; import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_COMMUNAL_HUB_SHOWING; import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_DEVICE_DOZING; import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_DEVICE_DREAMING; import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_FREEFORM_ACTIVE_IN_DESKTOP_MODE; import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_STATUS_BAR_KEYGUARD_GOING_AWAY; import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING; import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING_OCCLUDED; import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_VOICE_INTERACTION_WINDOW_SHOWING; import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_WAKEFULNESS_TRANSITION; import static com.android.systemui.shared.system.QuickStepContract.addInterface; import static com.android.window.flags.Flags.predictiveBackSwipeEdgeNoneApi; import static com.android.window.flags.Flags.predictiveBackThreeButtonNav; import android.annotation.FloatRange; import android.annotation.Nullable; import android.app.ActivityTaskManager; import android.content.BroadcastReceiver; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.ServiceConnection; import android.content.pm.ResolveInfo; import android.graphics.Region; import android.hardware.input.InputManager; import android.hardware.input.InputManagerGlobal; import android.os.Binder; import android.os.Bundle; import android.os.Handler; import android.os.IBinder; import android.os.IRemoteCallback; import android.os.Looper; import android.os.PatternMatcher; import android.os.RemoteException; import android.os.SystemClock; import android.os.UserHandle; import android.os.UserManager; import android.util.Log; import android.view.Display; import android.view.InputDevice; import android.view.KeyCharacterMap; import android.view.KeyEvent; import android.view.MotionEvent; import android.view.Surface; import android.view.accessibility.AccessibilityManager; import android.view.inputmethod.Flags; import android.view.inputmethod.InputMethodManager; import androidx.annotation.NonNull; import com.android.app.displaylib.PerDisplayRepository; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.app.AssistUtils; import com.android.internal.app.IVoiceInteractionSessionListener; import com.android.internal.logging.UiEventLogger; import com.android.internal.util.ScreenshotHelper; import com.android.internal.util.ScreenshotRequest; import com.android.systemui.Dumpable; import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.contextualeducation.GestureType; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.display.data.repository.DisplayRepository; import com.android.systemui.dump.DumpManager; import com.android.systemui.keyguard.KeyguardUnlockAnimationController; import com.android.systemui.keyguard.KeyguardWmStateRefactor; import com.android.systemui.keyguard.WakefulnessLifecycle; import com.android.systemui.keyguard.ui.view.InWindowLauncherUnlockAnimationManager; import com.android.systemui.model.SysUiState; import com.android.systemui.model.SysUiState.SysUiStateCallback; import com.android.systemui.navigationbar.NavigationBarController; import com.android.systemui.navigationbar.NavigationModeController; import com.android.systemui.navigationbar.views.NavigationBar; import com.android.systemui.navigationbar.views.NavigationBarView; import com.android.systemui.navigationbar.views.buttons.KeyButtonView; import com.android.systemui.process.ProcessWrapper; import com.android.systemui.recents.LauncherProxyService.LauncherProxyListener; import com.android.systemui.scene.domain.interactor.SceneInteractor; import com.android.systemui.scene.shared.flag.SceneContainerFlag; import com.android.systemui.settings.DisplayTracker; import com.android.systemui.settings.UserTracker; import com.android.systemui.shade.ShadeViewController; import com.android.systemui.shade.domain.interactor.ShadeInteractor; import com.android.systemui.shade.shared.flag.ShadeWindowGoesAround; import com.android.systemui.shared.recents.ILauncherProxy; import com.android.systemui.shared.recents.ISystemUiProxy; import com.android.systemui.shared.system.QuickStepContract; import com.android.systemui.shared.system.QuickStepContract.SystemUiStateFlags; import com.android.systemui.shared.system.smartspace.ISysuiUnlockAnimationController; import com.android.systemui.statusbar.CommandQueue; import com.android.systemui.statusbar.NotificationShadeWindowController; import com.android.systemui.statusbar.phone.StatusBarWindowCallback; import com.android.systemui.statusbar.policy.CallbackController; import com.android.systemui.unfold.progress.UnfoldTransitionProgressForwarder; import com.android.wm.shell.back.BackAnimation; import com.android.wm.shell.shared.desktopmode.DesktopModeStatus; import com.android.wm.shell.sysui.ShellInterface; import java.io.PrintWriter; import java.util.ArrayList; import java.util.List; import java.util.Objects; import java.util.Optional; import java.util.concurrent.Executor; import java.util.function.Supplier; import javax.inject.Inject; import javax.inject.Provider; import dagger.Lazy; /** * Class to send information from SysUI to Launcher with a binder. */ @SysUISingleton public class LauncherProxyService implements CallbackController, NavigationModeController.ModeChangedListener, Dumpable { @VisibleForTesting static final String ACTION_QUICKSTEP = "android.intent.action.QUICKSTEP_SERVICE"; public static final String TAG_OPS = "LauncherProxyService"; private static final long BACKOFF_MILLIS = 1000; private static final long DEFERRED_CALLBACK_MILLIS = 5000; // Max backoff caps at 5 mins private static final long MAX_BACKOFF_MILLIS = 10 * 60 * 1000; private final Context mContext; private final Executor mMainExecutor; private final ShellInterface mShellInterface; private final Lazy mShadeViewControllerLazy; private final PerDisplayRepository mPerDisplaySysUiStateRepository; private final DisplayRepository mDisplayRepository; private SysUiState mDefaultDisplaySysUIState; private final Handler mHandler; private final Lazy mNavBarControllerLazy; private final ScreenPinningRequest mScreenPinningRequest; private final NotificationShadeWindowController mStatusBarWinController; private final Provider mSceneInteractor; private final Provider mShadeInteractor; private final Runnable mConnectionRunnable = () -> internalConnectToCurrentUser("runnable: startConnectionToCurrentUser"); private final ComponentName mRecentsComponentName; private final List mConnectionCallbacks = new ArrayList<>(); private final Intent mQuickStepIntent; private final ScreenshotHelper mScreenshotHelper; private final CommandQueue mCommandQueue; private final UserTracker mUserTracker; private final ISysuiUnlockAnimationController mSysuiUnlockAnimationController; private final Optional mUnfoldTransitionProgressForwarder; private final UiEventLogger mUiEventLogger; private final DisplayTracker mDisplayTracker; private Region mActiveNavBarRegion; private final BroadcastDispatcher mBroadcastDispatcher; private final BackAnimation mBackAnimation; private ILauncherProxy mLauncherProxy; private int mConnectionBackoffAttempts; private boolean mBound; private boolean mIsEnabled; // This is set to false when the launcher service is requested to be bound until it is notified // that the previous service has been cleaned up in ILauncherProxy#onUnbind(). It is also set to // true after a 1000ms timeout by mDeferredBindAfterTimedOutCleanup. private boolean mIsPrevServiceCleanedUp = true; private boolean mIsSystemOrVisibleBgUser; private int mCurrentBoundedUserId = -1; private boolean mInputFocusTransferStarted; private float mInputFocusTransferStartY; private long mInputFocusTransferStartMillis; private int mNavBarMode = NAV_BAR_MODE_3BUTTON; @VisibleForTesting public ISystemUiProxy mSysUiProxy = new ISystemUiProxy.Stub() { @Override public void startScreenPinning(int taskId) { verifyCallerAndClearCallingIdentityPostMain("startScreenPinning", () -> mScreenPinningRequest.showPrompt(taskId, false /* allowCancel */)); } @Override public void stopScreenPinning() { verifyCallerAndClearCallingIdentityPostMain("stopScreenPinning", () -> { try { ActivityTaskManager.getService().stopSystemLockTaskMode(); } catch (RemoteException e) { Log.e(TAG_OPS, "Failed to stop screen pinning"); } }); } // TODO: change the method signature to use (boolean inputFocusTransferStarted) @Override public void onStatusBarTouchEvent(MotionEvent event) { verifyCallerAndClearCallingIdentity("onStatusBarTouchEvent", () -> { if (SceneContainerFlag.isEnabled()) { //TODO(b/329863123) implement latency tracking for shade scene Log.i(TAG_OPS, "Scene container enabled. Latency tracking not started."); } else if (event.getActionMasked() == ACTION_DOWN) { mShadeViewControllerLazy.get().startExpandLatencyTracking(); } mHandler.post(() -> { int action = event.getActionMasked(); if (action == ACTION_DOWN) { mInputFocusTransferStarted = true; mInputFocusTransferStartY = event.getY(); mInputFocusTransferStartMillis = event.getEventTime(); // If scene framework is enabled, set the scene container window to // visible and let the touch "slip" into that window. if (SceneContainerFlag.isEnabled()) { mSceneInteractor.get().onRemoteUserInputStarted("launcher swipe"); } else { mShadeViewControllerLazy.get().startInputFocusTransfer(); } } if (action == ACTION_UP || action == ACTION_CANCEL) { mInputFocusTransferStarted = false; if (!SceneContainerFlag.isEnabled()) { float velocity = (event.getY() - mInputFocusTransferStartY) / (event.getEventTime() - mInputFocusTransferStartMillis); if (action == ACTION_CANCEL) { mShadeViewControllerLazy.get().cancelInputFocusTransfer(); } else { mShadeViewControllerLazy.get().finishInputFocusTransfer(velocity); } } else if (action == ACTION_UP) { // Gesture was too short to be picked up by scene container touch // handling; programmatically start the transition to the shade. mShadeInteractor.get() .expandNotificationsShade("short launcher swipe", null); } } event.recycle(); }); }); } @Override public void onStatusBarTrackpadEvent(MotionEvent event) { verifyCallerAndClearCallingIdentityPostMain("onStatusBarTrackpadEvent", () -> { if (SceneContainerFlag.isEnabled()) { int action = event.getActionMasked(); if (action == ACTION_DOWN) { mSceneInteractor.get().onRemoteUserInputStarted( "trackpad swipe"); } else if (action == ACTION_UP) { mShadeInteractor.get() .expandNotificationsShade("short trackpad swipe", null); } mStatusBarWinController.getWindowRootView().dispatchTouchEvent(event); } else { mShadeViewControllerLazy.get().handleExternalTouch(event); } }); } @Override public void animateNavBarLongPress(boolean isTouchDown, boolean shrink, long durationMs) { verifyCallerAndClearCallingIdentityPostMain("animateNavBarLongPress", () -> notifyAnimateNavBarLongPress(isTouchDown, shrink, durationMs)); } @Override public void setOverrideHomeButtonLongPress(long duration, float slopMultiplier, boolean haptic) { verifyCallerAndClearCallingIdentityPostMain("setOverrideHomeButtonLongPress", () -> notifySetOverrideHomeButtonLongPress(duration, slopMultiplier, haptic)); } @Override public void onBackEvent(@Nullable KeyEvent keyEvent) throws RemoteException { if (predictiveBackThreeButtonNav() && predictiveBackSwipeEdgeNoneApi() && mBackAnimation != null && keyEvent != null) { mBackAnimation.setTriggerBack(!keyEvent.isCanceled()); mBackAnimation.onBackMotion(/* touchX */ 0, /* touchY */ 0, keyEvent.getAction(), EDGE_NONE); } else { verifyCallerAndClearCallingIdentityPostMain("onBackPressed", () -> { sendEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_BACK); sendEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_BACK); }); } } @Override public void onImeSwitcherPressed() { // TODO(b/204901476) We're intentionally using the default display for now since // Launcher/Taskbar isn't display aware. if (Flags.imeSwitcherRevamp()) { mContext.getSystemService(InputMethodManager.class) .onImeSwitchButtonClickFromSystem(mDisplayTracker.getDefaultDisplayId()); } else { mContext.getSystemService(InputMethodManager.class) .showInputMethodPickerFromSystem(true /* showAuxiliarySubtypes */, mDisplayTracker.getDefaultDisplayId()); } mUiEventLogger.log(KeyButtonView.NavBarButtonEvent.NAVBAR_IME_SWITCHER_BUTTON_TAP); } @Override public void onImeSwitcherLongPress() { if (!Flags.imeSwitcherRevamp()) { return; } // TODO(b/204901476) We're intentionally using the default display for now since // Launcher/Taskbar isn't display aware. mContext.getSystemService(InputMethodManager.class) .showInputMethodPickerFromSystem(true /* showAuxiliarySubtypes */, mDisplayTracker.getDefaultDisplayId()); mUiEventLogger.log( KeyButtonView.NavBarButtonEvent.NAVBAR_IME_SWITCHER_BUTTON_LONGPRESS); } @Override public void updateContextualEduStats(boolean isTrackpadGesture, String gestureType) { verifyCallerAndClearCallingIdentityPostMain("updateContextualEduStats", () -> mHandler.post(() -> LauncherProxyService.this.updateContextualEduStats( isTrackpadGesture, GestureType.valueOf(gestureType)))); } @Override public void setHomeRotationEnabled(boolean enabled) { verifyCallerAndClearCallingIdentityPostMain("setHomeRotationEnabled", () -> mHandler.post(() -> notifyHomeRotationEnabled(enabled))); } @Override public void notifyTaskbarStatus(boolean visible, boolean stashed) { verifyCallerAndClearCallingIdentityPostMain("notifyTaskbarStatus", () -> onTaskbarStatusUpdated(visible, stashed)); } @Override public void notifyTaskbarAutohideSuspend(boolean suspend) { verifyCallerAndClearCallingIdentityPostMain("notifyTaskbarAutohideSuspend", () -> onTaskbarAutohideSuspend(suspend)); } private boolean sendEvent(int action, int code) { long when = SystemClock.uptimeMillis(); final KeyEvent ev = new KeyEvent(when, when, action, code, 0 /* repeat */, 0 /* metaState */, KeyCharacterMap.VIRTUAL_KEYBOARD, 0 /* scancode */, KeyEvent.FLAG_FROM_SYSTEM | KeyEvent.FLAG_VIRTUAL_HARD_KEY, InputDevice.SOURCE_KEYBOARD); ev.setDisplayId(mContext.getDisplay().getDisplayId()); return InputManagerGlobal.getInstance() .injectInputEvent(ev, InputManager.INJECT_INPUT_EVENT_MODE_ASYNC); } @Override public void onOverviewShown(boolean fromHome) { verifyCallerAndClearCallingIdentityPostMain("onOverviewShown", () -> { for (int i = mConnectionCallbacks.size() - 1; i >= 0; --i) { mConnectionCallbacks.get(i).onOverviewShown(fromHome); } }); } @Override public void onAssistantProgress(@FloatRange(from = 0.0, to = 1.0) float progress) { verifyCallerAndClearCallingIdentityPostMain("onAssistantProgress", () -> notifyAssistantProgress(progress)); } @Override public void onAssistantGestureCompletion(float velocity) { verifyCallerAndClearCallingIdentityPostMain("onAssistantGestureCompletion", () -> notifyAssistantGestureCompletion(velocity)); } @Override public void startAssistant(Bundle bundle) { verifyCallerAndClearCallingIdentityPostMain("startAssistant", () -> notifyStartAssistant(bundle)); } @Override public void setAssistantOverridesRequested(int[] invocationTypes) { verifyCallerAndClearCallingIdentityPostMain("setAssistantOverridesRequested", () -> notifyAssistantOverrideRequested(invocationTypes)); } @Override public void notifyAccessibilityButtonClicked(int displayId) { verifyCallerAndClearCallingIdentity("notifyAccessibilityButtonClicked", () -> AccessibilityManager.getInstance(mContext).notifyAccessibilityButtonClicked( displayId)); } @Override public void notifyAccessibilityButtonLongClicked() { verifyCallerAndClearCallingIdentity("notifyAccessibilityButtonLongClicked", () -> AccessibilityManager.getInstance(mContext) .notifyAccessibilityButtonLongClicked( mDisplayTracker.getDefaultDisplayId())); } @Override public void notifyPrioritizedRotation(@Surface.Rotation int rotation) { verifyCallerAndClearCallingIdentityPostMain("notifyPrioritizedRotation", () -> notifyPrioritizedRotationInternal(rotation)); } @Override public void takeScreenshot(ScreenshotRequest request) { mScreenshotHelper.takeScreenshot(request, mHandler, null); } @Override public void expandNotificationPanel() { verifyCallerAndClearCallingIdentityPostMain("expandNotificationPanel", () -> mCommandQueue.handleSystemKey(new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_SYSTEM_NAVIGATION_DOWN))); } @Override public void toggleNotificationPanel() { verifyCallerAndClearCallingIdentityPostMain("toggleNotificationPanel", () -> mCommandQueue.toggleNotificationsPanel()); } @Override public void toggleQuickSettingsPanel() { verifyCallerAndClearCallingIdentityPostMain("toggleQuickSettingsPanel", () -> mCommandQueue.toggleQuickSettingsPanel()); } private boolean verifyCaller(String reason) { final int callerId = Binder.getCallingUserHandle().getIdentifier(); if (callerId != mCurrentBoundedUserId) { Log.w(TAG_OPS, "Launcher called sysui with invalid user: " + callerId + ", reason: " + reason); return false; } return true; } private T verifyCallerAndClearCallingIdentity(String reason, Supplier supplier) { if (!verifyCaller(reason)) { return null; } final long token = Binder.clearCallingIdentity(); try { return supplier.get(); } finally { Binder.restoreCallingIdentity(token); } } private void verifyCallerAndClearCallingIdentity(String reason, Runnable runnable) { verifyCallerAndClearCallingIdentity(reason, () -> { runnable.run(); return null; }); } private void verifyCallerAndClearCallingIdentityPostMain(String reason, Runnable runnable) { verifyCallerAndClearCallingIdentity(reason, () -> mHandler.post(runnable)); } }; private final Runnable mDeferredConnectionCallback = () -> { Log.w(TAG_OPS, "Binder supposed established connection but actual connection to service " + "timed out, trying again"); retryConnectionWithBackoff(); }; private final Runnable mDeferredBindAfterTimedOutCleanup = () -> { Log.w(TAG_OPS, "Timed out waiting for previous service to clean up, binding to new one"); mIsPrevServiceCleanedUp = true; maybeBindService(); }; private final BroadcastReceiver mUserEventReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { if (Objects.equals(intent.getAction(), Intent.ACTION_USER_UNLOCKED)) { if (keyguardPrivateNotifications()) { // Start the launcher connection to the launcher service // Connect if user hasn't connected yet if (getProxy() == null) { startConnectionToCurrentUser(); } } } } }; private final BroadcastReceiver mLauncherStateChangedReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { // If adding, bind immediately if (Objects.equals(intent.getAction(), ACTION_PACKAGE_ADDED)) { updateEnabledAndBinding(); return; } // ACTION_PACKAGE_CHANGED String[] compsList = intent.getStringArrayExtra(EXTRA_CHANGED_COMPONENT_NAME_LIST); if (compsList == null) { return; } // Only rebind for TouchInteractionService component from launcher ResolveInfo ri = context.getPackageManager() .resolveService(new Intent(ACTION_QUICKSTEP), 0); if (ri == null) { return; } String interestingComponent = ri.serviceInfo.name; for (String component : compsList) { if (interestingComponent.equals(component)) { Log.i(TAG_OPS, "Rebinding for component [" + component + "] change"); updateEnabledAndBinding(); return; } } } }; private final ServiceConnection mLauncherServiceConnection = new ServiceConnection() { @Override public void onServiceConnected(ComponentName name, IBinder service) { Log.d(TAG_OPS, "Launcher proxy service connected"); mConnectionBackoffAttempts = 0; mHandler.removeCallbacks(mDeferredConnectionCallback); try { service.linkToDeath(mLauncherServiceDeathRcpt, 0); } catch (RemoteException e) { // Failed to link to death (process may have died between binding and connecting), // just unbind the service for now and retry again Log.e(TAG_OPS, "Lost connection to launcher service", e); disconnectFromLauncherService("Lost connection to launcher service"); retryConnectionWithBackoff(); return; } mCurrentBoundedUserId = mUserTracker.getUserId(); mLauncherProxy = ILauncherProxy.Stub.asInterface(service); Bundle params = new Bundle(); addInterface(mSysUiProxy, params); addInterface(mSysuiUnlockAnimationController, params); addInterface(mUnfoldTransitionProgressForwarder.orElse(null), params); // Add all the interfaces exposed by the shell mShellInterface.createExternalInterfaces(params); try { Log.d(TAG_OPS, "LauncherProxyService connected, initializing launcher proxy"); mLauncherProxy.onInitialize(params); } catch (RemoteException e) { mCurrentBoundedUserId = -1; Log.e(TAG_OPS, "Failed to call onInitialize()", e); } dispatchNavButtonBounds(); // Force-update the systemui state flags updateSystemUiStateFlags(); if (ShadeWindowGoesAround.isEnabled()) { notifySysUiStateFlagsForAllDisplays(); } else { notifySystemUiStateFlags(mDefaultDisplaySysUIState.getFlags(), Display.DEFAULT_DISPLAY); } notifyConnectionChanged(); } @Override public void onNullBinding(ComponentName name) { Log.w(TAG_OPS, "Null binding of '" + name + "', try reconnecting"); mCurrentBoundedUserId = -1; retryConnectionWithBackoff(); } @Override public void onBindingDied(ComponentName name) { Log.w(TAG_OPS, "Binding died of '" + name + "', try reconnecting"); mCurrentBoundedUserId = -1; retryConnectionWithBackoff(); } @Override public void onServiceDisconnected(ComponentName name) { Log.w(TAG_OPS, "Service disconnected"); // Do nothing mCurrentBoundedUserId = -1; } }; /** Propagates the flags for all displays to be notified to Launcher. */ @VisibleForTesting public void notifySysUiStateFlagsForAllDisplays() { var displays = mDisplayRepository.getDisplayIds().getValue(); for (int displayId : displays) { var state = mPerDisplaySysUiStateRepository.get(displayId); if (state != null) { notifySystemUiStateFlags(state.getFlags(), displayId); } } } private final StatusBarWindowCallback mStatusBarWindowCallback = this::onStatusBarStateChanged; // This is the death handler for the binder from the launcher service private final IBinder.DeathRecipient mLauncherServiceDeathRcpt = this::cleanupAfterDeath; private final IVoiceInteractionSessionListener mVoiceInteractionSessionListener = new IVoiceInteractionSessionListener.Stub() { @Override public void onVoiceSessionShown() { // Do nothing } @Override public void onVoiceSessionHidden() { // Do nothing } @Override public void onVoiceSessionWindowVisibilityChanged(boolean visible) { mContext.getMainExecutor().execute(() -> LauncherProxyService.this.onVoiceSessionWindowVisibilityChanged(visible)); } @Override public void onSetUiHints(Bundle hints) { // Do nothing } }; private final UserTracker.Callback mUserChangedCallback = new UserTracker.Callback() { @Override public void onUserChanged(int newUser, @NonNull Context userContext) { mConnectionBackoffAttempts = 0; internalConnectToCurrentUser("User changed"); } }; private final SysUiStateCallback mSysUiStateCallback = new SysUiStateCallback() { @Override public void onSystemUiStateChanged(long sysUiFlags, int displayId) { notifySystemUiStateFlags(sysUiFlags, displayId); } }; @SuppressWarnings("OptionalUsedAsFieldOrParameterType") @Inject public LauncherProxyService(Context context, @Main Executor mainExecutor, CommandQueue commandQueue, ShellInterface shellInterface, Lazy navBarControllerLazy, Lazy shadeViewControllerLazy, ScreenPinningRequest screenPinningRequest, NavigationModeController navModeController, NotificationShadeWindowController statusBarWinController, PerDisplayRepository perDisplaySysUiStateRepository, Provider sceneInteractor, Provider shadeInteractor, UserTracker userTracker, UserManager userManager, WakefulnessLifecycle wakefulnessLifecycle, UiEventLogger uiEventLogger, DisplayTracker displayTracker, KeyguardUnlockAnimationController sysuiUnlockAnimationController, InWindowLauncherUnlockAnimationManager inWindowLauncherUnlockAnimationManager, AssistUtils assistUtils, DumpManager dumpManager, Optional unfoldTransitionProgressForwarder, BroadcastDispatcher broadcastDispatcher, Optional backAnimation, ProcessWrapper processWrapper, DisplayRepository displayRepository ) { // b/241601880: This component should only be running for primary users or // secondaryUsers when visibleBackgroundUsers are supported. boolean isSystemUser = processWrapper.isSystemUser(); boolean isVisibleBackgroundUser = userManager.isVisibleBackgroundUsersSupported() && !userManager.isUserForeground(); if (!isSystemUser && isVisibleBackgroundUser) { Log.d(TAG_OPS, "Initialization for visibleBackgroundUser"); } mIsSystemOrVisibleBgUser = isSystemUser || isVisibleBackgroundUser; if (!mIsSystemOrVisibleBgUser) { Log.wtf(TAG_OPS, "Unexpected initialization for non-system foreground user", new Throwable()); } mContext = context; mMainExecutor = mainExecutor; mShellInterface = shellInterface; mShadeViewControllerLazy = shadeViewControllerLazy; mHandler = new Handler(); mNavBarControllerLazy = navBarControllerLazy; mScreenPinningRequest = screenPinningRequest; mStatusBarWinController = statusBarWinController; mSceneInteractor = sceneInteractor; mShadeInteractor = shadeInteractor; mUserTracker = userTracker; mConnectionBackoffAttempts = 0; mRecentsComponentName = ComponentName.unflattenFromString(context.getString( com.android.internal.R.string.config_recentsComponentName)); mQuickStepIntent = new Intent(ACTION_QUICKSTEP) .setPackage(mRecentsComponentName.getPackageName()); mPerDisplaySysUiStateRepository = perDisplaySysUiStateRepository; mDisplayRepository = displayRepository; mDefaultDisplaySysUIState = perDisplaySysUiStateRepository.get(Display.DEFAULT_DISPLAY); mDefaultDisplaySysUIState.addCallback(mSysUiStateCallback); mUiEventLogger = uiEventLogger; mDisplayTracker = displayTracker; mUnfoldTransitionProgressForwarder = unfoldTransitionProgressForwarder; mBroadcastDispatcher = broadcastDispatcher; mBackAnimation = backAnimation.orElse(null); if (!KeyguardWmStateRefactor.isEnabled()) { mSysuiUnlockAnimationController = sysuiUnlockAnimationController; } else { mSysuiUnlockAnimationController = inWindowLauncherUnlockAnimationManager; } dumpManager.registerDumpable(getClass().getSimpleName(), this); // Listen for nav bar mode changes mNavBarMode = navModeController.addListener(this); // Listen for launcher package changes IntentFilter filter = new IntentFilter(Intent.ACTION_PACKAGE_ADDED); filter.addDataScheme("package"); filter.addDataSchemeSpecificPart(mRecentsComponentName.getPackageName(), PatternMatcher.PATTERN_LITERAL); filter.addAction(Intent.ACTION_PACKAGE_CHANGED); mContext.registerReceiver(mLauncherStateChangedReceiver, filter); if (keyguardPrivateNotifications()) { mBroadcastDispatcher.registerReceiver(mUserEventReceiver, new IntentFilter(Intent.ACTION_USER_UNLOCKED), null /* executor */, UserHandle.ALL); } // Listen for status bar state changes statusBarWinController.registerCallback(mStatusBarWindowCallback); mScreenshotHelper = new ScreenshotHelper(context); commandQueue.addCallback(new CommandQueue.Callbacks() { // Listen for tracing state changes @Override public void onTracingStateChanged(boolean enabled) { // TODO(b/286509643) Cleanup callers of this; Unused downstream } @Override public void moveFocusedTaskToStageSplit(int displayId, boolean leftOrTop) { if (mLauncherProxy != null) { try { if (DesktopModeStatus.canEnterDesktopMode(mContext) && (mDefaultDisplaySysUIState.getFlags() & SYSUI_STATE_FREEFORM_ACTIVE_IN_DESKTOP_MODE) != 0) { return; } mLauncherProxy.enterStageSplitFromRunningApp(leftOrTop); } catch (RemoteException e) { Log.w(TAG_OPS, "Unable to enter stage split from the current running app"); } } } }); mCommandQueue = commandQueue; // Listen for user setup mUserTracker.addCallback(mUserChangedCallback, mMainExecutor); wakefulnessLifecycle.addObserver(mWakefulnessLifecycleObserver); // Connect to the service updateEnabledAndBinding(); // Listen for assistant changes assistUtils.registerVoiceInteractionSessionListener(mVoiceInteractionSessionListener); } public void onVoiceSessionWindowVisibilityChanged(boolean visible) { mDefaultDisplaySysUIState.setFlag(SYSUI_STATE_VOICE_INTERACTION_WINDOW_SHOWING, visible) .commitUpdate(mContext.getDisplayId()); } private void updateEnabledAndBinding() { updateEnabledState(); startConnectionToCurrentUser(); } private void updateSysUIStateForNavbars() { if (ShadeWindowGoesAround.isEnabled()) { var displays = mDisplayRepository.getDisplayIds().getValue(); for (int displayId : displays) { updateSysUIStateForNavbarWithDisplayId(displayId); } } else { updateSysUIStateForNavbarWithDisplayId(Display.DEFAULT_DISPLAY); } } private void updateSysUIStateForNavbarWithDisplayId(int displayId) { final NavigationBar navBarFragment = mNavBarControllerLazy.get().getNavigationBar(displayId); final NavigationBarView navBarView = mNavBarControllerLazy.get().getNavigationBarView(displayId); if (SysUiState.DEBUG) { Log.d(TAG_OPS, "Updating sysui state flags: navBarFragment=" + navBarFragment + " navBarView=" + navBarView + " shadeViewController=" + mShadeViewControllerLazy.get()); } final SysUiState displaySysuiState = mPerDisplaySysUiStateRepository.get(displayId); if (displaySysuiState == null) return; if (navBarFragment != null) { navBarFragment.updateSystemUiStateFlags(); } if (navBarView != null) { navBarView.updateDisabledSystemUiStateFlags(displaySysuiState); } } /** Force updates SystemUI state flags prior to sending them to Launcher. */ public void updateSystemUiStateFlags() { updateSysUIStateForNavbars(); mShadeViewControllerLazy.get().updateSystemUiStateFlags(); if (mStatusBarWinController != null) { mStatusBarWinController.notifyStateChangedCallbacks(); } } private void notifySystemUiStateFlags(@SystemUiStateFlags long flags, int displayId) { if (SysUiState.DEBUG) { Log.d(TAG_OPS, "Notifying sysui state change to launcher service: proxy=" + mLauncherProxy + " display=" + displayId + " flags=" + QuickStepContract.getSystemUiStateString(flags) + " displayId=" + displayId); } try { if (mLauncherProxy != null) { mLauncherProxy.onSystemUiStateChanged(flags, displayId); } } catch (RemoteException e) { Log.e(TAG_OPS, "Failed to notify sysui state change", e); } } private void onStatusBarStateChanged(boolean keyguardShowing, boolean keyguardOccluded, boolean keyguardGoingAway, boolean bouncerShowing, boolean isDozing, boolean panelExpanded, boolean isDreaming, boolean communalShowing) { mDefaultDisplaySysUIState.setFlag(SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING, keyguardShowing && !keyguardOccluded) .setFlag(SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING_OCCLUDED, keyguardShowing && keyguardOccluded) .setFlag(SYSUI_STATE_STATUS_BAR_KEYGUARD_GOING_AWAY, keyguardGoingAway) .setFlag(SYSUI_STATE_BOUNCER_SHOWING, bouncerShowing) .setFlag(SYSUI_STATE_DEVICE_DOZING, isDozing) .setFlag(SYSUI_STATE_DEVICE_DREAMING, isDreaming) .setFlag(SYSUI_STATE_COMMUNAL_HUB_SHOWING, communalShowing) .commitUpdate(mContext.getDisplayId()); } /** * Sets the navbar region which can receive touch inputs */ public void onActiveNavBarRegionChanges(Region activeRegion) { mActiveNavBarRegion = activeRegion; dispatchNavButtonBounds(); } private void dispatchNavButtonBounds() { if (mLauncherProxy != null && mActiveNavBarRegion != null) { try { mLauncherProxy.onActiveNavBarRegionChanges(mActiveNavBarRegion); } catch (RemoteException e) { Log.e(TAG_OPS, "Failed to call onActiveNavBarRegionChanges()", e); } } } public void cleanupAfterDeath() { if (mInputFocusTransferStarted) { mHandler.post(() -> { mInputFocusTransferStarted = false; mShadeViewControllerLazy.get().cancelInputFocusTransfer(); }); } mIsPrevServiceCleanedUp = true; startConnectionToCurrentUser(); } public void startConnectionToCurrentUser() { Log.v(TAG_OPS, "startConnectionToCurrentUser: connection is restarted"); if (mHandler.getLooper() != Looper.myLooper()) { mHandler.post(mConnectionRunnable); } else { internalConnectToCurrentUser("startConnectionToCurrentUser"); } } private void internalConnectToCurrentUser(String reason) { if (!mIsSystemOrVisibleBgUser) { // This should not happen, but if any per-user SysUI component has a dependency on OPS, // then this could get triggered Log.w(TAG_OPS, "Skipping connection to launcher service due to non-system foreground user " + "caller"); return; } disconnectFromLauncherService(reason); // If user has not setup yet or already connected, do not try to connect if (!isEnabled()) { Log.v(TAG_OPS, "Cannot attempt connection, is enabled " + isEnabled()); return; } mHandler.removeCallbacks(mConnectionRunnable); maybeBindService(); } private void maybeBindService() { if (!mIsPrevServiceCleanedUp) { Log.w(TAG_OPS, "Skipping connection to TouchInteractionService until previous" + " instance is cleaned up."); if (!mHandler.hasCallbacks(mDeferredConnectionCallback)) { mHandler.postDelayed(mDeferredBindAfterTimedOutCleanup, BACKOFF_MILLIS); } return; } // Avoid creating TouchInteractionService because the System user in HSUM mode does not // interact with UI elements UserHandle currentUser = UserHandle.of(mUserTracker.getUserId()); if (UserManager.isHeadlessSystemUserMode() && currentUser.isSystem()) { Log.w(TAG_OPS, "Skipping connection to TouchInteractionService for the System user in HSUM " + "mode."); return; } try { mBound = mContext.bindServiceAsUser(mQuickStepIntent, mLauncherServiceConnection, Context.BIND_AUTO_CREATE | Context.BIND_FOREGROUND_SERVICE_WHILE_AWAKE, currentUser); } catch (SecurityException e) { Log.e(TAG_OPS, "Unable to bind because of security error", e); } if (mBound) { mIsPrevServiceCleanedUp = false; // Ensure that connection has been established even if it thinks it is bound mHandler.postDelayed(mDeferredConnectionCallback, DEFERRED_CALLBACK_MILLIS); } else { // Retry after exponential backoff timeout retryConnectionWithBackoff(); } } private void retryConnectionWithBackoff() { if (mHandler.hasCallbacks(mConnectionRunnable)) { return; } final long timeoutMs = (long) Math.min( Math.scalb(BACKOFF_MILLIS, mConnectionBackoffAttempts), MAX_BACKOFF_MILLIS); mHandler.postDelayed(mConnectionRunnable, timeoutMs); mConnectionBackoffAttempts++; Log.w(TAG_OPS, "Failed to connect on attempt " + mConnectionBackoffAttempts + " will try again in " + timeoutMs + "ms"); } @Override public void addCallback(@NonNull LauncherProxyListener listener) { if (!mConnectionCallbacks.contains(listener)) { mConnectionCallbacks.add(listener); } listener.onConnectionChanged(mLauncherProxy != null); } @Override public void removeCallback(@NonNull LauncherProxyListener listener) { mConnectionCallbacks.remove(listener); } public boolean shouldShowSwipeUpUI() { return isEnabled() && !QuickStepContract.isLegacyMode(mNavBarMode); } public boolean isEnabled() { return mIsEnabled; } public ILauncherProxy getProxy() { return mLauncherProxy; } private void disconnectFromLauncherService(String disconnectReason) { Log.d(TAG_OPS, "disconnectFromLauncherService bound?: " + mBound + " currentProxy: " + mLauncherProxy + " disconnectReason: " + disconnectReason, new Throwable()); if (mBound) { // Always unbind the service (ie. if called through onNullBinding or onBindingDied) mContext.unbindService(mLauncherServiceConnection); mBound = false; if (mLauncherProxy != null) { try { mLauncherProxy.onUnbind(new IRemoteCallback.Stub() { @Override public void sendResult(Bundle data) throws RemoteException { // Received Launcher reply, try to bind anew. mIsPrevServiceCleanedUp = true; if (mHandler.hasCallbacks(mDeferredBindAfterTimedOutCleanup)) { mHandler.removeCallbacks(mDeferredBindAfterTimedOutCleanup); maybeBindService(); } } }); } catch (RemoteException e) { Log.w(TAG_OPS, "disconnectFromLauncherService failed to notify Launcher"); mIsPrevServiceCleanedUp = true; } } } if (mLauncherProxy != null) { mLauncherProxy.asBinder().unlinkToDeath(mLauncherServiceDeathRcpt, 0); mLauncherProxy = null; notifyConnectionChanged(); } } /** * Updates contextual education stats when a gesture is triggered * @param isTrackpadGesture indicates if the gesture is triggered by trackpad * @param gestureType type of gesture triggered */ public void updateContextualEduStats(boolean isTrackpadGesture, GestureType gestureType) { for (int i = mConnectionCallbacks.size() - 1; i >= 0; --i) { mConnectionCallbacks.get(i).updateContextualEduStats(isTrackpadGesture, gestureType); } } private void notifyHomeRotationEnabled(boolean enabled) { for (int i = mConnectionCallbacks.size() - 1; i >= 0; --i) { mConnectionCallbacks.get(i).onHomeRotationEnabled(enabled); } } private void onTaskbarStatusUpdated(boolean visible, boolean stashed) { for (int i = mConnectionCallbacks.size() - 1; i >= 0; --i) { mConnectionCallbacks.get(i).onTaskbarStatusUpdated(visible, stashed); } } private void onTaskbarAutohideSuspend(boolean suspend) { for (int i = mConnectionCallbacks.size() - 1; i >= 0; --i) { mConnectionCallbacks.get(i).onTaskbarAutohideSuspend(suspend); } } private void notifyConnectionChanged() { for (int i = mConnectionCallbacks.size() - 1; i >= 0; --i) { mConnectionCallbacks.get(i).onConnectionChanged(mLauncherProxy != null); } } private void notifyPrioritizedRotationInternal(@Surface.Rotation int rotation) { for (int i = mConnectionCallbacks.size() - 1; i >= 0; --i) { mConnectionCallbacks.get(i).onPrioritizedRotation(rotation); } } private void notifyAssistantProgress(@FloatRange(from = 0.0, to = 1.0) float progress) { for (int i = mConnectionCallbacks.size() - 1; i >= 0; --i) { mConnectionCallbacks.get(i).onAssistantProgress(progress); } } private void notifyAssistantGestureCompletion(float velocity) { for (int i = mConnectionCallbacks.size() - 1; i >= 0; --i) { mConnectionCallbacks.get(i).onAssistantGestureCompletion(velocity); } } private void notifyStartAssistant(Bundle bundle) { for (int i = mConnectionCallbacks.size() - 1; i >= 0; --i) { mConnectionCallbacks.get(i).startAssistant(bundle); } } private void notifyAssistantOverrideRequested(int[] invocationTypes) { for (int i = mConnectionCallbacks.size() - 1; i >= 0; --i) { mConnectionCallbacks.get(i).setAssistantOverridesRequested(invocationTypes); } } private void notifyAnimateNavBarLongPress(boolean isTouchDown, boolean shrink, long durationMs) { for (int i = mConnectionCallbacks.size() - 1; i >= 0; --i) { mConnectionCallbacks.get(i).animateNavBarLongPress(isTouchDown, shrink, durationMs); } } private void notifySetOverrideHomeButtonLongPress(long duration, float slopMultiplier, boolean haptic) { for (int i = mConnectionCallbacks.size() - 1; i >= 0; --i) { mConnectionCallbacks.get(i) .setOverrideHomeButtonLongPress(duration, slopMultiplier, haptic); } } public void notifyAssistantVisibilityChanged(float visibility) { try { if (mLauncherProxy != null) { mLauncherProxy.onAssistantVisibilityChanged(visibility); } else { Log.e(TAG_OPS, "Failed to get launcher proxy for assistant visibility."); } } catch (RemoteException e) { Log.e(TAG_OPS, "Failed to call notifyAssistantVisibilityChanged()", e); } } private final WakefulnessLifecycle.Observer mWakefulnessLifecycleObserver = new WakefulnessLifecycle.Observer() { @Override public void onStartedWakingUp() { mDefaultDisplaySysUIState .setFlag(SYSUI_STATE_AWAKE, true) .setFlag(SYSUI_STATE_WAKEFULNESS_TRANSITION, true) .commitUpdate(mContext.getDisplayId()); } @Override public void onFinishedWakingUp() { mDefaultDisplaySysUIState .setFlag(SYSUI_STATE_AWAKE, true) .setFlag(SYSUI_STATE_WAKEFULNESS_TRANSITION, false) .commitUpdate(mContext.getDisplayId()); } @Override public void onStartedGoingToSleep() { mDefaultDisplaySysUIState .setFlag(SYSUI_STATE_AWAKE, false) .setFlag(SYSUI_STATE_WAKEFULNESS_TRANSITION, true) .commitUpdate(mContext.getDisplayId()); } @Override public void onFinishedGoingToSleep() { mDefaultDisplaySysUIState .setFlag(SYSUI_STATE_AWAKE, false) .setFlag(SYSUI_STATE_WAKEFULNESS_TRANSITION, false) .commitUpdate(mContext.getDisplayId()); } }; void notifyToggleRecentApps() { for (int i = mConnectionCallbacks.size() - 1; i >= 0; --i) { mConnectionCallbacks.get(i).onToggleRecentApps(); } } public void disable(int displayId, int state1, int state2, boolean animate) { try { if (mLauncherProxy != null) { mLauncherProxy.disable(displayId, state1, state2, animate); } else { Log.e(TAG_OPS, "Failed to get launcher proxy for disable flags."); } } catch (RemoteException e) { Log.e(TAG_OPS, "Failed to call disable()", e); } } public void onRotationProposal(int rotation, boolean isValid) { try { if (mLauncherProxy != null) { mLauncherProxy.onRotationProposal(rotation, isValid); } else { Log.e(TAG_OPS, "Failed to get launcher proxy for proposing rotation."); } } catch (RemoteException e) { Log.e(TAG_OPS, "Failed to call onRotationProposal()", e); } } public void onSystemBarAttributesChanged(int displayId, int behavior) { try { if (mLauncherProxy != null) { mLauncherProxy.onSystemBarAttributesChanged(displayId, behavior); } else { Log.e(TAG_OPS, "Failed to get launcher proxy for system bar attr change."); } } catch (RemoteException e) { Log.e(TAG_OPS, "Failed to call onSystemBarAttributesChanged()", e); } } public void onNavButtonsDarkIntensityChanged(float darkIntensity) { try { if (mLauncherProxy != null) { mLauncherProxy.onNavButtonsDarkIntensityChanged(darkIntensity); } else { Log.e(TAG_OPS, "Failed to get launcher proxy to update nav buttons dark intensity"); } } catch (RemoteException e) { Log.e(TAG_OPS, "Failed to call onNavButtonsDarkIntensityChanged()", e); } } public void onNavigationBarLumaSamplingEnabled(int displayId, boolean enable) { try { if (mLauncherProxy != null) { mLauncherProxy.onNavigationBarLumaSamplingEnabled(displayId, enable); } else { Log.e(TAG_OPS, "Failed to get launcher proxy to enable/disable nav bar luma" + "sampling"); } } catch (RemoteException e) { Log.e(TAG_OPS, "Failed to call onNavigationBarLumaSamplingEnabled()", e); } } private void updateEnabledState() { final int currentUser = mUserTracker.getUserId(); mIsEnabled = mContext.getPackageManager().resolveServiceAsUser(mQuickStepIntent, MATCH_SYSTEM_ONLY, currentUser) != null; } @Override public void onNavigationModeChanged(int mode) { mNavBarMode = mode; } @Override public void dump(PrintWriter pw, String[] args) { pw.println(TAG_OPS + " state:"); pw.print(" isConnected="); pw.println(mLauncherProxy != null); pw.print(" mIsEnabled="); pw.println(isEnabled()); pw.print(" mRecentsComponentName="); pw.println(mRecentsComponentName); pw.print(" mQuickStepIntent="); pw.println(mQuickStepIntent); pw.print(" mBound="); pw.println(mBound); pw.print(" mCurrentBoundedUserId="); pw.println(mCurrentBoundedUserId); pw.print(" mConnectionBackoffAttempts="); pw.println(mConnectionBackoffAttempts); pw.print(" mInputFocusTransferStarted="); pw.println(mInputFocusTransferStarted); pw.print(" mInputFocusTransferStartY="); pw.println(mInputFocusTransferStartY); pw.print(" mInputFocusTransferStartMillis="); pw.println(mInputFocusTransferStartMillis); pw.print(" mActiveNavBarRegion="); pw.println(mActiveNavBarRegion); pw.print(" mNavBarMode="); pw.println(mNavBarMode); pw.print(" mIsPrevServiceCleanedUp="); pw.println(mIsPrevServiceCleanedUp); mDefaultDisplaySysUIState.dump(pw, args); } public interface LauncherProxyListener { default void onConnectionChanged(boolean isConnected) {} default void onPrioritizedRotation(@Surface.Rotation int rotation) {} default void onOverviewShown(boolean fromHome) {} /** Notify the recents app (overview) is started by 3-button navigation. */ default void onToggleRecentApps() {} default void onHomeRotationEnabled(boolean enabled) {} default void onTaskbarStatusUpdated(boolean visible, boolean stashed) {} default void onTaskbarAutohideSuspend(boolean suspend) {} default void onAssistantProgress(@FloatRange(from = 0.0, to = 1.0) float progress) {} default void onAssistantGestureCompletion(float velocity) {} default void startAssistant(Bundle bundle) {} default void setAssistantOverridesRequested(int[] invocationTypes) {} default void animateNavBarLongPress(boolean isTouchDown, boolean shrink, long durationMs) {} /** Set override of home button long press duration, touch slop multiplier, and haptic. */ default void setOverrideHomeButtonLongPress( long override, float slopMultiplier, boolean haptic) {} /** Updates contextual education stats when target gesture type is triggered. */ default void updateContextualEduStats( boolean isTrackpadGesture, GestureType gestureType) {} } /** * Shuts down this service at the end of a testcase. *

* The in-production service is never shuts down, and it was not designed with testing in mind. * This unregisters the mechanisms by which the service will be revived after a testcase. *

* NOTE: This is a stop-gap introduced when first added some tests to this class. It should * probably be replaced by proper lifecycle management on this class. */ @VisibleForTesting() void shutdownForTest() { mContext.unregisterReceiver(mLauncherStateChangedReceiver); mIsEnabled = false; mHandler.removeCallbacks(mConnectionRunnable); disconnectFromLauncherService("Shutdown for test"); } }