blob: 3f944cb6d62876e5186221653014b1e2b15e961e [file] [log] [blame]
/*
* 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<ConfigurationChangeListener> mConfigChangeListeners =
new CopyOnWriteArrayList<>();
private final CopyOnWriteArrayList<KeyguardChangeListener> mKeyguardChangeListeners =
new CopyOnWriteArrayList<>();
private final CopyOnWriteArrayList<UserChangeListener> mUserChangeListeners =
new CopyOnWriteArrayList<>();
private ArrayMap<String, Supplier<ExternalInterfaceBinder>> mExternalInterfaceSuppliers =
new ArrayMap<>();
// References to the existing interfaces, to be invalidated when they are recreated
private ArrayMap<String, ExternalInterfaceBinder> 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<ExternalInterfaceBinder> 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<UserInfo> 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<UserInfo> 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);
}
}
}
}