blob: 9a9a2426507f8c77bc670afda0ae0d602a47f21d [file] [log] [blame]
/*
* Copyright (C) 2021 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.LoadedApk;
import android.content.ComponentName;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.text.TextUtils;
import android.util.Log;
import androidx.annotation.Nullable;
import com.android.internal.annotations.VisibleForTesting;
import com.android.systemui.plugins.Plugin;
import com.android.systemui.plugins.PluginFragment;
import com.android.systemui.plugins.PluginLifecycleManager;
import com.android.systemui.plugins.PluginListener;
import dalvik.system.PathClassLoader;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Supplier;
/**
* Contains a single instantiation of a Plugin.
*
* This class and its related Factory are in charge of actually instantiating a plugin and
* managing any state related to it.
*
* @param <T> The type of plugin that this contains.
*/
public class PluginInstance<T extends Plugin> implements PluginLifecycleManager {
private static final String TAG = "PluginInstance";
private final Context mAppContext;
private final PluginListener<T> mListener;
private final ComponentName mComponentName;
private final PluginFactory<T> mPluginFactory;
private Context mPluginContext;
private T mPlugin;
/** */
public PluginInstance(
Context appContext,
PluginListener<T> listener,
ComponentName componentName,
PluginFactory<T> pluginFactory,
@Nullable T plugin) {
mAppContext = appContext;
mListener = listener;
mComponentName = componentName;
mPluginFactory = pluginFactory;
mPlugin = plugin;
if (mPlugin != null) {
mPluginContext = mPluginFactory.createPluginContext();
}
}
/** Alerts listener and plugin that the plugin has been created. */
public void onCreate() {
mListener.onPluginAttached(this);
if (mPlugin == null) {
loadPlugin();
} else {
if (!(mPlugin instanceof PluginFragment)) {
// Only call onCreate for plugins that aren't fragments, as fragments
// will get the onCreate as part of the fragment lifecycle.
mPlugin.onCreate(mAppContext, mPluginContext);
}
mListener.onPluginLoaded(mPlugin, mPluginContext, this);
}
}
/** Alerts listener and plugin that the plugin is being shutdown. */
public void onDestroy() {
unloadPlugin();
mListener.onPluginDetached(this);
}
/** Returns the current plugin instance (if it is loaded). */
@Nullable
public T getPlugin() {
return mPlugin;
}
/**
* Loads and creates the plugin if it does not exist.
*/
public void loadPlugin() {
if (mPlugin != null) {
return;
}
mPlugin = mPluginFactory.createPlugin();
mPluginContext = mPluginFactory.createPluginContext();
if (mPlugin == null || mPluginContext == null) {
return;
}
if (!(mPlugin instanceof PluginFragment)) {
// Only call onCreate for plugins that aren't fragments, as fragments
// will get the onCreate as part of the fragment lifecycle.
mPlugin.onCreate(mAppContext, mPluginContext);
}
mListener.onPluginLoaded(mPlugin, mPluginContext, this);
}
/**
* Unloads and destroys the current plugin instance if it exists.
*
* This will free the associated memory if there are not other references.
*/
public void unloadPlugin() {
if (mPlugin == null) {
return;
}
mListener.onPluginUnloaded(mPlugin, this);
if (!(mPlugin instanceof PluginFragment)) {
// Only call onDestroy for plugins that aren't fragments, as fragments
// will get the onDestroy as part of the fragment lifecycle.
mPlugin.onDestroy();
}
mPlugin = null;
mPluginContext = null;
}
/**
* Returns if the contained plugin matches the passed in class name.
*
* It does this by string comparison of the class names.
**/
public boolean containsPluginClass(Class pluginClass) {
return mComponentName.getClassName().equals(pluginClass.getName());
}
public ComponentName getComponentName() {
return mComponentName;
}
public String getPackage() {
return mComponentName.getPackageName();
}
public VersionInfo getVersionInfo() {
return mPluginFactory.checkVersion(mPlugin);
}
@VisibleForTesting
Context getPluginContext() {
return mPluginContext;
}
/** Used to create new {@link PluginInstance}s. */
public static class Factory {
private final ClassLoader mBaseClassLoader;
private final InstanceFactory<?> mInstanceFactory;
private final VersionChecker mVersionChecker;
private final boolean mIsDebug;
private final List<String> mPrivilegedPlugins;
/** Factory used to construct {@link PluginInstance}s. */
public Factory(ClassLoader classLoader, InstanceFactory<?> instanceFactory,
VersionChecker versionChecker,
List<String> privilegedPlugins,
boolean isDebug) {
mPrivilegedPlugins = privilegedPlugins;
mBaseClassLoader = classLoader;
mInstanceFactory = instanceFactory;
mVersionChecker = versionChecker;
mIsDebug = isDebug;
}
/** Construct a new PluginInstance. */
public <T extends Plugin> PluginInstance<T> create(
Context context,
ApplicationInfo appInfo,
ComponentName componentName,
Class<T> pluginClass,
PluginListener<T> listener)
throws PackageManager.NameNotFoundException, ClassNotFoundException,
InstantiationException, IllegalAccessException {
PluginFactory<T> pluginFactory = new PluginFactory<T>(
context, mInstanceFactory, appInfo, componentName, mVersionChecker, pluginClass,
() -> getClassLoader(appInfo, mBaseClassLoader));
// TODO: Only create the plugin before version check if we need it for
// legacy version check.
T instance = pluginFactory.createPlugin();
pluginFactory.checkVersion(instance);
return new PluginInstance<T>(
context, listener, componentName, pluginFactory, instance);
}
private boolean isPluginPackagePrivileged(String packageName) {
for (String componentNameOrPackage : mPrivilegedPlugins) {
ComponentName componentName = ComponentName.unflattenFromString(
componentNameOrPackage);
if (componentName != null) {
if (componentName.getPackageName().equals(packageName)) {
return true;
}
} else if (componentNameOrPackage.equals(packageName)) {
return true;
}
}
return false;
}
private ClassLoader getParentClassLoader(ClassLoader baseClassLoader) {
return new PluginManagerImpl.ClassLoaderFilter(
baseClassLoader,
"com.android.systemui.common",
"com.android.systemui.log",
"com.android.systemui.plugin");
}
/** Returns class loader specific for the given plugin. */
private ClassLoader getClassLoader(ApplicationInfo appInfo,
ClassLoader baseClassLoader) {
if (!mIsDebug && !isPluginPackagePrivileged(appInfo.packageName)) {
Log.w(TAG, "Cannot get class loader for non-privileged plugin. Src:"
+ appInfo.sourceDir + ", pkg: " + appInfo.packageName);
return null;
}
List<String> zipPaths = new ArrayList<>();
List<String> libPaths = new ArrayList<>();
LoadedApk.makePaths(null, true, appInfo, zipPaths, libPaths);
ClassLoader classLoader = new PathClassLoader(
TextUtils.join(File.pathSeparator, zipPaths),
TextUtils.join(File.pathSeparator, libPaths),
getParentClassLoader(baseClassLoader));
return classLoader;
}
}
/** Class that compares a plugin class against an implementation for version matching. */
public interface VersionChecker {
/** Compares two plugin classes. */
<T extends Plugin> VersionInfo checkVersion(
Class<T> instanceClass, Class<T> pluginClass, Plugin plugin);
}
/** Class that compares a plugin class against an implementation for version matching. */
public static class VersionCheckerImpl implements VersionChecker {
@Override
/** Compares two plugin classes. */
public <T extends Plugin> VersionInfo checkVersion(
Class<T> instanceClass, Class<T> pluginClass, Plugin plugin) {
VersionInfo pluginVersion = new VersionInfo().addClass(pluginClass);
VersionInfo instanceVersion = new VersionInfo().addClass(instanceClass);
if (instanceVersion.hasVersionInfo()) {
pluginVersion.checkVersion(instanceVersion);
} else if (plugin != null) {
int fallbackVersion = plugin.getVersion();
if (fallbackVersion != pluginVersion.getDefaultVersion()) {
throw new VersionInfo.InvalidVersionException("Invalid legacy version", false);
}
return null;
}
return instanceVersion;
}
}
/**
* Simple class to create a new instance. Useful for testing.
*
* @param <T> The type of plugin this create.
**/
public static class InstanceFactory<T extends Plugin> {
T create(Class cls) throws IllegalAccessException, InstantiationException {
return (T) cls.newInstance();
}
}
/**
* Instanced wrapper of InstanceFactory
*
* @param <T> is the type of the plugin object to be built
**/
public static class PluginFactory<T extends Plugin> {
private final Context mContext;
private final InstanceFactory<?> mInstanceFactory;
private final ApplicationInfo mAppInfo;
private final ComponentName mComponentName;
private final VersionChecker mVersionChecker;
private final Class<T> mPluginClass;
private final Supplier<ClassLoader> mClassLoaderFactory;
public PluginFactory(
Context context,
InstanceFactory<?> instanceFactory,
ApplicationInfo appInfo,
ComponentName componentName,
VersionChecker versionChecker,
Class<T> pluginClass,
Supplier<ClassLoader> classLoaderFactory) {
mContext = context;
mInstanceFactory = instanceFactory;
mAppInfo = appInfo;
mComponentName = componentName;
mVersionChecker = versionChecker;
mPluginClass = pluginClass;
mClassLoaderFactory = classLoaderFactory;
}
/** Creates the related plugin object from the factory */
public T createPlugin() {
try {
ClassLoader loader = mClassLoaderFactory.get();
Class<T> instanceClass = (Class<T>) Class.forName(
mComponentName.getClassName(), true, loader);
return (T) mInstanceFactory.create(instanceClass);
} catch (ClassNotFoundException ex) {
Log.e(TAG, "Failed to load plugin", ex);
} catch (IllegalAccessException ex) {
Log.e(TAG, "Failed to load plugin", ex);
} catch (InstantiationException ex) {
Log.e(TAG, "Failed to load plugin", ex);
}
return null;
}
/** Creates a context wrapper for the plugin */
public Context createPluginContext() {
try {
ClassLoader loader = mClassLoaderFactory.get();
return new PluginActionManager.PluginContextWrapper(
mContext.createApplicationContext(mAppInfo, 0), loader);
} catch (NameNotFoundException ex) {
Log.e(TAG, "Failed to create plugin context", ex);
}
return null;
}
/** Check Version and create VersionInfo for instance */
public VersionInfo checkVersion(T instance) {
if (instance == null) {
instance = createPlugin();
}
return mVersionChecker.checkVersion(
(Class<T>) instance.getClass(), mPluginClass, instance);
}
}
}