/* * Copyright (C) 2022 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.wm.shell.sysui; import static android.content.pm.ActivityInfo.CONFIG_ASSETS_PATHS; import static android.content.pm.ActivityInfo.CONFIG_FONT_SCALE; import static android.content.pm.ActivityInfo.CONFIG_LAYOUT_DIRECTION; import static android.content.pm.ActivityInfo.CONFIG_LOCALE; import static android.content.pm.ActivityInfo.CONFIG_SMALLEST_SCREEN_SIZE; import static android.content.pm.ActivityInfo.CONFIG_UI_MODE; import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_INIT; import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_SYSUI_EVENTS; import android.content.Context; import android.content.pm.ActivityInfo; import android.content.pm.UserInfo; import android.content.res.Configuration; import android.os.Bundle; import android.util.ArrayMap; import androidx.annotation.NonNull; import androidx.annotation.VisibleForTesting; import com.android.internal.protolog.common.ProtoLog; import com.android.wm.shell.common.ExternalInterfaceBinder; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.annotations.ExternalThread; import java.io.PrintWriter; import java.util.List; import java.util.concurrent.CopyOnWriteArrayList; import java.util.function.Supplier; /** * Handles event callbacks from SysUI that can be used within the Shell. */ public class ShellController { private static final String TAG = ShellController.class.getSimpleName(); private final ShellInit mShellInit; private final ShellCommandHandler mShellCommandHandler; private final ShellExecutor mMainExecutor; private final ShellInterfaceImpl mImpl = new ShellInterfaceImpl(); private final CopyOnWriteArrayList mConfigChangeListeners = new CopyOnWriteArrayList<>(); private final CopyOnWriteArrayList mKeyguardChangeListeners = new CopyOnWriteArrayList<>(); private final CopyOnWriteArrayList mUserChangeListeners = new CopyOnWriteArrayList<>(); private ArrayMap> mExternalInterfaceSuppliers = new ArrayMap<>(); // References to the existing interfaces, to be invalidated when they are recreated private ArrayMap mExternalInterfaces = new ArrayMap<>(); private Configuration mLastConfiguration; public ShellController(ShellInit shellInit, ShellCommandHandler shellCommandHandler, ShellExecutor mainExecutor) { mShellInit = shellInit; mShellCommandHandler = shellCommandHandler; mMainExecutor = mainExecutor; shellInit.addInitCallback(this::onInit, this); } private void onInit() { mShellCommandHandler.addDumpCallback(this::dump, this); } /** * Returns the external interface to this controller. */ public ShellInterface asShell() { return mImpl; } /** * Adds a new configuration listener. The configuration change callbacks are not made in any * particular order. */ public void addConfigurationChangeListener(ConfigurationChangeListener listener) { mConfigChangeListeners.remove(listener); mConfigChangeListeners.add(listener); } /** * Removes an existing configuration listener. */ public void removeConfigurationChangeListener(ConfigurationChangeListener listener) { mConfigChangeListeners.remove(listener); } /** * Adds a new Keyguard listener. The Keyguard change callbacks are not made in any * particular order. */ public void addKeyguardChangeListener(KeyguardChangeListener listener) { mKeyguardChangeListeners.remove(listener); mKeyguardChangeListeners.add(listener); } /** * Removes an existing Keyguard listener. */ public void removeKeyguardChangeListener(KeyguardChangeListener listener) { mKeyguardChangeListeners.remove(listener); } /** * Adds a new user-change listener. The user change callbacks are not made in any * particular order. */ public void addUserChangeListener(UserChangeListener listener) { mUserChangeListeners.remove(listener); mUserChangeListeners.add(listener); } /** * Removes an existing user-change listener. */ public void removeUserChangeListener(UserChangeListener listener) { mUserChangeListeners.remove(listener); } /** * Adds an interface that can be called from a remote process. This method takes a supplier * because each binder reference is valid for a single process, and in multi-user mode, SysUI * will request new binder instances for each instance of Launcher that it provides binders * to. * * @param extra the key for the interface, {@see ShellSharedConstants} * @param binderSupplier the supplier of the binder to pass to the external process * @param callerInstance the instance of the caller, purely for logging */ public void addExternalInterface(String extra, Supplier binderSupplier, Object callerInstance) { ProtoLog.v(WM_SHELL_INIT, "Adding external interface from %s with key %s", callerInstance.getClass().getSimpleName(), extra); if (mExternalInterfaceSuppliers.containsKey(extra)) { throw new IllegalArgumentException("Supplier with same key already exists: " + extra); } mExternalInterfaceSuppliers.put(extra, binderSupplier); } /** * Updates the given bundle with the set of external interfaces, invalidating the old set of * binders. */ @VisibleForTesting public void createExternalInterfaces(Bundle output) { // Invalidate the old binders for (int i = 0; i < mExternalInterfaces.size(); i++) { mExternalInterfaces.valueAt(i).invalidate(); } mExternalInterfaces.clear(); // Create new binders for each key for (int i = 0; i < mExternalInterfaceSuppliers.size(); i++) { final String key = mExternalInterfaceSuppliers.keyAt(i); final ExternalInterfaceBinder b = mExternalInterfaceSuppliers.valueAt(i).get(); mExternalInterfaces.put(key, b); output.putBinder(key, b.asBinder()); } } @VisibleForTesting void onConfigurationChanged(Configuration newConfig) { // The initial config is send on startup and doesn't trigger listener callbacks if (mLastConfiguration == null) { mLastConfiguration = new Configuration(newConfig); ProtoLog.v(WM_SHELL_SYSUI_EVENTS, "Initial Configuration: %s", newConfig); return; } final int diff = newConfig.diff(mLastConfiguration); ProtoLog.v(WM_SHELL_SYSUI_EVENTS, "New configuration change: %s", newConfig); ProtoLog.v(WM_SHELL_SYSUI_EVENTS, "\tchanges=%s", Configuration.configurationDiffToString(diff)); final boolean densityFontScaleChanged = (diff & CONFIG_FONT_SCALE) != 0 || (diff & ActivityInfo.CONFIG_DENSITY) != 0; final boolean smallestScreenWidthChanged = (diff & CONFIG_SMALLEST_SCREEN_SIZE) != 0; final boolean themeChanged = (diff & CONFIG_ASSETS_PATHS) != 0 || (diff & CONFIG_UI_MODE) != 0; final boolean localOrLayoutDirectionChanged = (diff & CONFIG_LOCALE) != 0 || (diff & CONFIG_LAYOUT_DIRECTION) != 0; // Update the last configuration and call listeners mLastConfiguration.updateFrom(newConfig); for (ConfigurationChangeListener listener : mConfigChangeListeners) { listener.onConfigurationChanged(newConfig); if (densityFontScaleChanged) { listener.onDensityOrFontScaleChanged(); } if (smallestScreenWidthChanged) { listener.onSmallestScreenWidthChanged(); } if (themeChanged) { listener.onThemeChanged(); } if (localOrLayoutDirectionChanged) { listener.onLocaleOrLayoutDirectionChanged(); } } } @VisibleForTesting void onKeyguardVisibilityChanged(boolean visible, boolean occluded, boolean animatingDismiss) { ProtoLog.v(WM_SHELL_SYSUI_EVENTS, "Keyguard visibility changed: visible=%b " + "occluded=%b animatingDismiss=%b", visible, occluded, animatingDismiss); for (KeyguardChangeListener listener : mKeyguardChangeListeners) { listener.onKeyguardVisibilityChanged(visible, occluded, animatingDismiss); } } @VisibleForTesting void onKeyguardDismissAnimationFinished() { ProtoLog.v(WM_SHELL_SYSUI_EVENTS, "Keyguard dismiss animation finished"); for (KeyguardChangeListener listener : mKeyguardChangeListeners) { listener.onKeyguardDismissAnimationFinished(); } } @VisibleForTesting void onUserChanged(int newUserId, @NonNull Context userContext) { ProtoLog.v(WM_SHELL_SYSUI_EVENTS, "User changed: id=%d", newUserId); for (UserChangeListener listener : mUserChangeListeners) { listener.onUserChanged(newUserId, userContext); } } @VisibleForTesting void onUserProfilesChanged(@NonNull List profiles) { ProtoLog.v(WM_SHELL_SYSUI_EVENTS, "User profiles changed"); for (UserChangeListener listener : mUserChangeListeners) { listener.onUserProfilesChanged(profiles); } } public void dump(@NonNull PrintWriter pw, String prefix) { final String innerPrefix = prefix + " "; pw.println(prefix + TAG); pw.println(innerPrefix + "mConfigChangeListeners=" + mConfigChangeListeners.size()); pw.println(innerPrefix + "mLastConfiguration=" + mLastConfiguration); pw.println(innerPrefix + "mKeyguardChangeListeners=" + mKeyguardChangeListeners.size()); pw.println(innerPrefix + "mUserChangeListeners=" + mUserChangeListeners.size()); if (!mExternalInterfaces.isEmpty()) { pw.println(innerPrefix + "mExternalInterfaces={"); for (String key : mExternalInterfaces.keySet()) { pw.println(innerPrefix + "\t" + key + ": " + mExternalInterfaces.get(key)); } pw.println(innerPrefix + "}"); } } /** * The interface for calls from outside the Shell, within the host process. */ @ExternalThread private class ShellInterfaceImpl implements ShellInterface { @Override public void onInit() { try { mMainExecutor.executeBlocking(() -> mShellInit.init()); } catch (InterruptedException e) { throw new RuntimeException("Failed to initialize the Shell in 2s", e); } } @Override public void onConfigurationChanged(Configuration newConfiguration) { mMainExecutor.execute(() -> ShellController.this.onConfigurationChanged(newConfiguration)); } @Override public void onKeyguardVisibilityChanged(boolean visible, boolean occluded, boolean animatingDismiss) { mMainExecutor.execute(() -> ShellController.this.onKeyguardVisibilityChanged(visible, occluded, animatingDismiss)); } @Override public void onKeyguardDismissAnimationFinished() { mMainExecutor.execute(() -> ShellController.this.onKeyguardDismissAnimationFinished()); } @Override public void onUserChanged(int newUserId, @NonNull Context userContext) { mMainExecutor.execute(() -> ShellController.this.onUserChanged(newUserId, userContext)); } @Override public void onUserProfilesChanged(@NonNull List profiles) { mMainExecutor.execute(() -> ShellController.this.onUserProfilesChanged(profiles)); } @Override public boolean handleCommand(String[] args, PrintWriter pw) { try { boolean[] result = new boolean[1]; mMainExecutor.executeBlocking(() -> { result[0] = mShellCommandHandler.handleCommand(args, pw); }); return result[0]; } catch (InterruptedException e) { throw new RuntimeException("Failed to handle Shell command in 2s", e); } } @Override public void createExternalInterfaces(Bundle bundle) { try { mMainExecutor.executeBlocking(() -> { ShellController.this.createExternalInterfaces(bundle); }); } catch (InterruptedException e) { throw new RuntimeException("Failed to get Shell command in 2s", e); } } @Override public void dump(PrintWriter pw) { try { mMainExecutor.executeBlocking(() -> mShellCommandHandler.dump(pw)); } catch (InterruptedException e) { throw new RuntimeException("Failed to dump the Shell in 2s", e); } } } }