mirror of
https://github.com/LawnchairLauncher/lawnchair.git
synced 2026-02-27 23:36:47 +00:00
433 lines
17 KiB
Java
433 lines
17 KiB
Java
/*
|
|
* Copyright (C) 2016 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.shared.plugins;
|
|
|
|
import android.app.Notification;
|
|
import android.app.Notification.Action;
|
|
import android.app.NotificationManager;
|
|
import android.app.PendingIntent;
|
|
import android.content.ComponentName;
|
|
import android.content.Context;
|
|
import android.content.ContextWrapper;
|
|
import android.content.Intent;
|
|
import android.content.pm.ApplicationInfo;
|
|
import android.content.pm.PackageManager;
|
|
import android.content.pm.PackageManager.NameNotFoundException;
|
|
import android.content.pm.ResolveInfo;
|
|
import android.content.res.Resources;
|
|
import android.net.Uri;
|
|
import android.util.ArraySet;
|
|
import android.util.Log;
|
|
import android.view.LayoutInflater;
|
|
|
|
import com.android.internal.annotations.VisibleForTesting;
|
|
import com.android.internal.messages.nano.SystemMessageProto.SystemMessage;
|
|
import com.android.systemui.plugins.Plugin;
|
|
import com.android.systemui.plugins.PluginListener;
|
|
import com.android.systemui.shared.plugins.VersionInfo.InvalidVersionException;
|
|
|
|
import java.util.ArrayList;
|
|
import java.util.List;
|
|
import java.util.concurrent.Executor;
|
|
|
|
/**
|
|
* Coordinates all the available plugins for a given action.
|
|
*
|
|
* The available plugins are queried from the {@link PackageManager} via an an {@link Intent}
|
|
* action.
|
|
*
|
|
* @param <T> The type of plugin that this contains.
|
|
*/
|
|
public class PluginActionManager<T extends Plugin> {
|
|
|
|
private static final boolean DEBUG = false;
|
|
|
|
private static final String TAG = "PluginInstanceManager";
|
|
public static final String PLUGIN_PERMISSION = "com.android.systemui.permission.PLUGIN";
|
|
|
|
private final Context mContext;
|
|
private final PluginListener<T> mListener;
|
|
private final String mAction;
|
|
private final boolean mAllowMultiple;
|
|
private final NotificationManager mNotificationManager;
|
|
private final PluginEnabler mPluginEnabler;
|
|
private final PluginInstance.Factory mPluginInstanceFactory;
|
|
private final ArraySet<String> mPrivilegedPlugins = new ArraySet<>();
|
|
|
|
@VisibleForTesting
|
|
private final ArrayList<PluginInstance<T>> mPluginInstances = new ArrayList<>();
|
|
private final boolean mIsDebuggable;
|
|
private final PackageManager mPm;
|
|
private final Class<T> mPluginClass;
|
|
private final Executor mMainExecutor;
|
|
private final Executor mBgExecutor;
|
|
|
|
private PluginActionManager(
|
|
Context context,
|
|
PackageManager pm,
|
|
String action,
|
|
PluginListener<T> listener,
|
|
Class<T> pluginClass,
|
|
boolean allowMultiple,
|
|
Executor mainExecutor,
|
|
Executor bgExecutor,
|
|
boolean debuggable,
|
|
NotificationManager notificationManager,
|
|
PluginEnabler pluginEnabler,
|
|
List<String> privilegedPlugins,
|
|
PluginInstance.Factory pluginInstanceFactory) {
|
|
mPluginClass = pluginClass;
|
|
mMainExecutor = mainExecutor;
|
|
mBgExecutor = bgExecutor;
|
|
mContext = context;
|
|
mPm = pm;
|
|
mAction = action;
|
|
mListener = listener;
|
|
mAllowMultiple = allowMultiple;
|
|
mNotificationManager = notificationManager;
|
|
mPluginEnabler = pluginEnabler;
|
|
mPluginInstanceFactory = pluginInstanceFactory;
|
|
mPrivilegedPlugins.addAll(privilegedPlugins);
|
|
mIsDebuggable = debuggable;
|
|
}
|
|
|
|
/** Load all plugins matching this instance's action. */
|
|
public void loadAll() {
|
|
if (DEBUG) Log.d(TAG, "startListening");
|
|
mBgExecutor.execute(this::queryAll);
|
|
}
|
|
|
|
/** Unload all plugins managed by this instance. */
|
|
public void destroy() {
|
|
if (DEBUG) Log.d(TAG, "stopListening");
|
|
ArrayList<PluginInstance<T>> plugins = new ArrayList<>(mPluginInstances);
|
|
for (PluginInstance<T> plugInstance : plugins) {
|
|
mMainExecutor.execute(() -> onPluginDisconnected(plugInstance));
|
|
}
|
|
}
|
|
|
|
/** Unload all matching plugins managed by this instance. */
|
|
public void onPackageRemoved(String pkg) {
|
|
mBgExecutor.execute(() -> removePkg(pkg));
|
|
}
|
|
|
|
/** Unload and then reload all matching plugins managed by this instance. */
|
|
public void reloadPackage(String pkg) {
|
|
mBgExecutor.execute(() -> {
|
|
removePkg(pkg);
|
|
queryPkg(pkg);
|
|
});
|
|
}
|
|
|
|
/** Disable a specific plugin managed by this instance. */
|
|
public boolean checkAndDisable(String className) {
|
|
boolean disableAny = false;
|
|
ArrayList<PluginInstance<T>> plugins = new ArrayList<>(mPluginInstances);
|
|
for (PluginInstance<T> info : plugins) {
|
|
if (className.startsWith(info.getPackage())) {
|
|
disableAny |= disable(info, PluginEnabler.DISABLED_FROM_EXPLICIT_CRASH);
|
|
}
|
|
}
|
|
return disableAny;
|
|
}
|
|
|
|
/** Disable all plugins managed by this instance. */
|
|
public boolean disableAll() {
|
|
ArrayList<PluginInstance<T>> plugins = new ArrayList<>(mPluginInstances);
|
|
boolean disabledAny = false;
|
|
for (int i = 0; i < plugins.size(); i++) {
|
|
disabledAny |= disable(plugins.get(i), PluginEnabler.DISABLED_FROM_SYSTEM_CRASH);
|
|
}
|
|
return disabledAny;
|
|
}
|
|
|
|
boolean isPluginPrivileged(ComponentName pluginName) {
|
|
for (String componentNameOrPackage : mPrivilegedPlugins) {
|
|
ComponentName componentName = ComponentName.unflattenFromString(componentNameOrPackage);
|
|
if (componentName == null) {
|
|
if (componentNameOrPackage.equals(pluginName.getPackageName())) {
|
|
return true;
|
|
}
|
|
} else {
|
|
if (componentName.equals(pluginName)) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
private boolean disable(
|
|
PluginInstance<T> pluginInstance, @PluginEnabler.DisableReason int reason) {
|
|
// Live by the sword, die by the sword.
|
|
// Misbehaving plugins get disabled and won't come back until uninstall/reinstall.
|
|
|
|
ComponentName pluginComponent = pluginInstance.getComponentName();
|
|
// If a plugin is detected in the stack of a crash then this will be called for that
|
|
// plugin, if the plugin causing a crash cannot be identified, they are all disabled
|
|
// assuming one of them must be bad.
|
|
if (isPluginPrivileged(pluginComponent)) {
|
|
// Don't disable privileged plugins as they are a part of the OS.
|
|
return false;
|
|
}
|
|
Log.w(TAG, "Disabling plugin " + pluginComponent.flattenToShortString());
|
|
mPluginEnabler.setDisabled(pluginComponent, reason);
|
|
|
|
return true;
|
|
}
|
|
|
|
<C> boolean dependsOn(Plugin p, Class<C> cls) {
|
|
ArrayList<PluginInstance<T>> instances = new ArrayList<>(mPluginInstances);
|
|
for (PluginInstance<T> instance : instances) {
|
|
if (instance.containsPluginClass(p.getClass())) {
|
|
return instance.getVersionInfo() != null && instance.getVersionInfo().hasClass(cls);
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
@Override
|
|
public String toString() {
|
|
return String.format("%s@%s (action=%s)",
|
|
getClass().getSimpleName(), hashCode(), mAction);
|
|
}
|
|
|
|
private void onPluginConnected(PluginInstance<T> pluginInstance) {
|
|
if (DEBUG) Log.d(TAG, "onPluginConnected");
|
|
PluginPrefs.setHasPlugins(mContext);
|
|
pluginInstance.onCreate(mContext, mListener);
|
|
}
|
|
|
|
private void onPluginDisconnected(PluginInstance<T> pluginInstance) {
|
|
if (DEBUG) Log.d(TAG, "onPluginDisconnected");
|
|
pluginInstance.onDestroy(mListener);
|
|
}
|
|
|
|
private void queryAll() {
|
|
if (DEBUG) Log.d(TAG, "queryAll " + mAction);
|
|
for (int i = mPluginInstances.size() - 1; i >= 0; i--) {
|
|
PluginInstance<T> pluginInstance = mPluginInstances.get(i);
|
|
mMainExecutor.execute(() -> onPluginDisconnected(pluginInstance));
|
|
}
|
|
mPluginInstances.clear();
|
|
handleQueryPlugins(null);
|
|
}
|
|
|
|
private void removePkg(String pkg) {
|
|
for (int i = mPluginInstances.size() - 1; i >= 0; i--) {
|
|
final PluginInstance<T> pluginInstance = mPluginInstances.get(i);
|
|
if (pluginInstance.getPackage().equals(pkg)) {
|
|
mMainExecutor.execute(() -> onPluginDisconnected(pluginInstance));
|
|
mPluginInstances.remove(i);
|
|
}
|
|
}
|
|
}
|
|
|
|
private void queryPkg(String pkg) {
|
|
if (DEBUG) Log.d(TAG, "queryPkg " + pkg + " " + mAction);
|
|
if (mAllowMultiple || (mPluginInstances.size() == 0)) {
|
|
handleQueryPlugins(pkg);
|
|
} else {
|
|
if (DEBUG) Log.d(TAG, "Too many of " + mAction);
|
|
}
|
|
}
|
|
|
|
private void handleQueryPlugins(String pkgName) {
|
|
// This isn't actually a service and shouldn't ever be started, but is
|
|
// a convenient PM based way to manage our plugins.
|
|
Intent intent = new Intent(mAction);
|
|
if (pkgName != null) {
|
|
intent.setPackage(pkgName);
|
|
}
|
|
List<ResolveInfo> result = mPm.queryIntentServices(intent, 0);
|
|
if (DEBUG) Log.d(TAG, "Found " + result.size() + " plugins");
|
|
if (result.size() > 1 && !mAllowMultiple) {
|
|
// TODO: Show warning.
|
|
Log.w(TAG, "Multiple plugins found for " + mAction);
|
|
if (DEBUG) {
|
|
for (ResolveInfo info : result) {
|
|
ComponentName name = new ComponentName(info.serviceInfo.packageName,
|
|
info.serviceInfo.name);
|
|
Log.w(TAG, " " + name);
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
for (ResolveInfo info : result) {
|
|
ComponentName name = new ComponentName(info.serviceInfo.packageName,
|
|
info.serviceInfo.name);
|
|
PluginInstance<T> pluginInstance = loadPluginComponent(name);
|
|
if (pluginInstance != null) {
|
|
// add plugin before sending PLUGIN_CONNECTED message
|
|
mPluginInstances.add(pluginInstance);
|
|
mMainExecutor.execute(() -> onPluginConnected(pluginInstance));
|
|
}
|
|
}
|
|
}
|
|
|
|
private PluginInstance<T> loadPluginComponent(ComponentName component) {
|
|
// This was already checked, but do it again here to make extra extra sure, we don't
|
|
// use these on production builds.
|
|
if (!mIsDebuggable && !isPluginPrivileged(component)) {
|
|
// Never ever ever allow these on production builds, they are only for prototyping.
|
|
Log.w(TAG, "Plugin cannot be loaded on production build: " + component);
|
|
return null;
|
|
}
|
|
if (!mPluginEnabler.isEnabled(component)) {
|
|
if (DEBUG) {
|
|
Log.d(TAG, "Plugin is not enabled, aborting load: " + component);
|
|
}
|
|
return null;
|
|
}
|
|
String packageName = component.getPackageName();
|
|
try {
|
|
// TODO: This probably isn't needed given that we don't have IGNORE_SECURITY on
|
|
if (mPm.checkPermission(PLUGIN_PERMISSION, packageName)
|
|
!= PackageManager.PERMISSION_GRANTED) {
|
|
Log.d(TAG, "Plugin doesn't have permission: " + packageName);
|
|
return null;
|
|
}
|
|
|
|
ApplicationInfo appInfo = mPm.getApplicationInfo(packageName, 0);
|
|
// TODO: Only create the plugin before version check if we need it for
|
|
// legacy version check.
|
|
if (DEBUG) {
|
|
Log.d(TAG, "createPlugin");
|
|
}
|
|
try {
|
|
return mPluginInstanceFactory.create(
|
|
mContext, appInfo, component,
|
|
mPluginClass);
|
|
} catch (InvalidVersionException e) {
|
|
reportInvalidVersion(component, component.getClassName(), e);
|
|
}
|
|
} catch (Throwable e) {
|
|
Log.w(TAG, "Couldn't load plugin: " + packageName, e);
|
|
return null;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private void reportInvalidVersion(
|
|
ComponentName component, String className, InvalidVersionException e) {
|
|
final int icon = Resources.getSystem().getIdentifier(
|
|
"stat_sys_warning", "drawable", "android");
|
|
final int color = Resources.getSystem().getIdentifier(
|
|
"system_notification_accent_color", "color", "android");
|
|
final Notification.Builder nb = new Notification.Builder(mContext,
|
|
PluginManager.NOTIFICATION_CHANNEL_ID)
|
|
.setStyle(new Notification.BigTextStyle())
|
|
.setSmallIcon(icon)
|
|
.setWhen(0)
|
|
.setShowWhen(false)
|
|
.setVisibility(Notification.VISIBILITY_PUBLIC)
|
|
.setColor(mContext.getColor(color));
|
|
String label = className;
|
|
try {
|
|
label = mPm.getServiceInfo(component, 0).loadLabel(mPm).toString();
|
|
} catch (NameNotFoundException e2) {
|
|
// no-op
|
|
}
|
|
if (!e.isTooNew()) {
|
|
// Localization not required as this will never ever appear in a user build.
|
|
nb.setContentTitle("Plugin \"" + label + "\" is too old")
|
|
.setContentText("Contact plugin developer to get an updated"
|
|
+ " version.\n" + e.getMessage());
|
|
} else {
|
|
// Localization not required as this will never ever appear in a user build.
|
|
nb.setContentTitle("Plugin \"" + label + "\" is too new")
|
|
.setContentText("Check to see if an OTA is available.\n"
|
|
+ e.getMessage());
|
|
}
|
|
Intent i = new Intent(PluginManagerImpl.DISABLE_PLUGIN).setData(
|
|
Uri.parse("package://" + component.flattenToString()));
|
|
PendingIntent pi = PendingIntent.getBroadcast(mContext, 0, i,
|
|
PendingIntent.FLAG_IMMUTABLE);
|
|
nb.addAction(new Action.Builder(null, "Disable plugin", pi).build());
|
|
mNotificationManager.notify(SystemMessage.NOTE_PLUGIN, nb.build());
|
|
// TODO: Warn user.
|
|
Log.w(TAG, "Plugin has invalid interface version " + e.getActualVersion()
|
|
+ ", expected " + e.getExpectedVersion());
|
|
}
|
|
|
|
/**
|
|
* Construct a {@link PluginActionManager}
|
|
*/
|
|
public static class Factory {
|
|
private final Context mContext;
|
|
private final PackageManager mPackageManager;
|
|
private final Executor mMainExecutor;
|
|
private final Executor mBgExecutor;
|
|
private final NotificationManager mNotificationManager;
|
|
private final PluginEnabler mPluginEnabler;
|
|
private final List<String> mPrivilegedPlugins;
|
|
private final PluginInstance.Factory mPluginInstanceFactory;
|
|
|
|
public Factory(Context context, PackageManager packageManager,
|
|
Executor mainExecutor, Executor bgExecutor,
|
|
NotificationManager notificationManager, PluginEnabler pluginEnabler,
|
|
List<String> privilegedPlugins, PluginInstance.Factory pluginInstanceFactory) {
|
|
mContext = context;
|
|
mPackageManager = packageManager;
|
|
mMainExecutor = mainExecutor;
|
|
mBgExecutor = bgExecutor;
|
|
mNotificationManager = notificationManager;
|
|
mPluginEnabler = pluginEnabler;
|
|
mPrivilegedPlugins = privilegedPlugins;
|
|
mPluginInstanceFactory = pluginInstanceFactory;
|
|
}
|
|
|
|
<T extends Plugin> PluginActionManager<T> create(
|
|
String action, PluginListener<T> listener, Class<T> pluginClass,
|
|
boolean allowMultiple, boolean debuggable) {
|
|
return new PluginActionManager<>(mContext, mPackageManager, action, listener,
|
|
pluginClass, allowMultiple, mMainExecutor, mBgExecutor,
|
|
debuggable, mNotificationManager, mPluginEnabler,
|
|
mPrivilegedPlugins, mPluginInstanceFactory);
|
|
}
|
|
}
|
|
|
|
/** */
|
|
public static class PluginContextWrapper extends ContextWrapper {
|
|
private final ClassLoader mClassLoader;
|
|
private LayoutInflater mInflater;
|
|
|
|
public PluginContextWrapper(Context base, ClassLoader classLoader) {
|
|
super(base);
|
|
mClassLoader = classLoader;
|
|
}
|
|
|
|
@Override
|
|
public ClassLoader getClassLoader() {
|
|
return mClassLoader;
|
|
}
|
|
|
|
@Override
|
|
public Object getSystemService(String name) {
|
|
if (LAYOUT_INFLATER_SERVICE.equals(name)) {
|
|
if (mInflater == null) {
|
|
mInflater = LayoutInflater.from(getBaseContext()).cloneInContext(this);
|
|
}
|
|
return mInflater;
|
|
}
|
|
return getBaseContext().getSystemService(name);
|
|
}
|
|
}
|
|
|
|
}
|