• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2021 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.systemui.shared.plugins;
18 
19 import android.app.LoadedApk;
20 import android.content.ComponentName;
21 import android.content.Context;
22 import android.content.pm.ApplicationInfo;
23 import android.content.pm.PackageManager;
24 import android.content.pm.PackageManager.NameNotFoundException;
25 import android.text.TextUtils;
26 import android.util.Log;
27 
28 import androidx.annotation.Nullable;
29 
30 import com.android.internal.annotations.VisibleForTesting;
31 import com.android.systemui.plugins.Plugin;
32 import com.android.systemui.plugins.PluginFragment;
33 import com.android.systemui.plugins.PluginLifecycleManager;
34 import com.android.systemui.plugins.PluginListener;
35 
36 import dalvik.system.PathClassLoader;
37 
38 import java.io.File;
39 import java.util.ArrayList;
40 import java.util.List;
41 import java.util.function.BiConsumer;
42 import java.util.function.Supplier;
43 
44 /**
45  * Contains a single instantiation of a Plugin.
46  *
47  * This class and its related Factory are in charge of actually instantiating a plugin and
48  * managing any state related to it.
49  *
50  * @param <T> The type of plugin that this contains.
51  */
52 public class PluginInstance<T extends Plugin> implements PluginLifecycleManager {
53     private static final String TAG = "PluginInstance";
54 
55     private final Context mAppContext;
56     private final PluginListener<T> mListener;
57     private final ComponentName mComponentName;
58     private final PluginFactory<T> mPluginFactory;
59     private final String mTag;
60 
61     private BiConsumer<String, String> mLogConsumer = null;
62     private Context mPluginContext;
63     private T mPlugin;
64 
65     /** */
PluginInstance( Context appContext, PluginListener<T> listener, ComponentName componentName, PluginFactory<T> pluginFactory, @Nullable T plugin)66     public PluginInstance(
67             Context appContext,
68             PluginListener<T> listener,
69             ComponentName componentName,
70             PluginFactory<T> pluginFactory,
71             @Nullable T plugin) {
72         mAppContext = appContext;
73         mListener = listener;
74         mComponentName = componentName;
75         mPluginFactory = pluginFactory;
76         mPlugin = plugin;
77         mTag = TAG + "[" + mComponentName.getShortClassName() + "]"
78                 + '@' + Integer.toHexString(hashCode());
79 
80         if (mPlugin != null) {
81             mPluginContext = mPluginFactory.createPluginContext();
82         }
83     }
84 
85     @Override
toString()86     public String toString() {
87         return mTag;
88     }
89 
setLogFunc(BiConsumer logConsumer)90     public void setLogFunc(BiConsumer logConsumer) {
91         mLogConsumer = logConsumer;
92     }
93 
log(String message)94     private void log(String message) {
95         if (mLogConsumer != null) {
96             mLogConsumer.accept(mTag, message);
97         }
98     }
99 
100     /** Alerts listener and plugin that the plugin has been created. */
onCreate()101     public synchronized void onCreate() {
102         boolean loadPlugin = mListener.onPluginAttached(this);
103         if (!loadPlugin) {
104             if (mPlugin != null) {
105                 log("onCreate: auto-unload");
106                 unloadPlugin();
107             }
108             return;
109         }
110 
111         if (mPlugin == null) {
112             log("onCreate auto-load");
113             loadPlugin();
114             return;
115         }
116 
117         log("onCreate: load callbacks");
118         mPluginFactory.checkVersion(mPlugin);
119         if (!(mPlugin instanceof PluginFragment)) {
120             // Only call onCreate for plugins that aren't fragments, as fragments
121             // will get the onCreate as part of the fragment lifecycle.
122             mPlugin.onCreate(mAppContext, mPluginContext);
123         }
124         mListener.onPluginLoaded(mPlugin, mPluginContext, this);
125     }
126 
127     /** Alerts listener and plugin that the plugin is being shutdown. */
onDestroy()128     public synchronized void onDestroy() {
129         log("onDestroy");
130         unloadPlugin();
131         mListener.onPluginDetached(this);
132     }
133 
134     /** Returns the current plugin instance (if it is loaded). */
135     @Nullable
getPlugin()136     public T getPlugin() {
137         return mPlugin;
138     }
139 
140     /**
141      * Loads and creates the plugin if it does not exist.
142      */
loadPlugin()143     public synchronized void loadPlugin() {
144         if (mPlugin != null) {
145             log("Load request when already loaded");
146             return;
147         }
148 
149         // Both of these calls take about 1 - 1.5 seconds in test runs
150         mPlugin = mPluginFactory.createPlugin();
151         mPluginContext = mPluginFactory.createPluginContext();
152         if (mPlugin == null || mPluginContext == null) {
153             Log.e(mTag, "Requested load, but failed");
154             return;
155         }
156 
157         log("Loaded plugin; running callbacks");
158         mPluginFactory.checkVersion(mPlugin);
159         if (!(mPlugin instanceof PluginFragment)) {
160             // Only call onCreate for plugins that aren't fragments, as fragments
161             // will get the onCreate as part of the fragment lifecycle.
162             mPlugin.onCreate(mAppContext, mPluginContext);
163         }
164         mListener.onPluginLoaded(mPlugin, mPluginContext, this);
165     }
166 
167     /**
168      * Unloads and destroys the current plugin instance if it exists.
169      *
170      * This will free the associated memory if there are not other references.
171      */
unloadPlugin()172     public synchronized void unloadPlugin() {
173         if (mPlugin == null) {
174             log("Unload request when already unloaded");
175             return;
176         }
177 
178         log("Unloading plugin, running callbacks");
179         mListener.onPluginUnloaded(mPlugin, this);
180         if (!(mPlugin instanceof PluginFragment)) {
181             // Only call onDestroy for plugins that aren't fragments, as fragments
182             // will get the onDestroy as part of the fragment lifecycle.
183             mPlugin.onDestroy();
184         }
185         mPlugin = null;
186         mPluginContext = null;
187     }
188 
189     /**
190      * Returns if the contained plugin matches the passed in class name.
191      *
192      * It does this by string comparison of the class names.
193      **/
containsPluginClass(Class pluginClass)194     public boolean containsPluginClass(Class pluginClass) {
195         return mComponentName.getClassName().equals(pluginClass.getName());
196     }
197 
getComponentName()198     public ComponentName getComponentName() {
199         return mComponentName;
200     }
201 
getPackage()202     public String getPackage() {
203         return mComponentName.getPackageName();
204     }
205 
getVersionInfo()206     public VersionInfo getVersionInfo() {
207         return mPluginFactory.checkVersion(mPlugin);
208     }
209 
210     @VisibleForTesting
getPluginContext()211     Context getPluginContext() {
212         return mPluginContext;
213     }
214 
215     /** Used to create new {@link PluginInstance}s. */
216     public static class Factory {
217         private final ClassLoader mBaseClassLoader;
218         private final InstanceFactory<?> mInstanceFactory;
219         private final VersionChecker mVersionChecker;
220         private final boolean mIsDebug;
221         private final List<String> mPrivilegedPlugins;
222 
223         /** Factory used to construct {@link PluginInstance}s. */
Factory(ClassLoader classLoader, InstanceFactory<?> instanceFactory, VersionChecker versionChecker, List<String> privilegedPlugins, boolean isDebug)224         public Factory(ClassLoader classLoader, InstanceFactory<?> instanceFactory,
225                 VersionChecker versionChecker,
226                 List<String> privilegedPlugins,
227                 boolean isDebug) {
228             mPrivilegedPlugins = privilegedPlugins;
229             mBaseClassLoader = classLoader;
230             mInstanceFactory = instanceFactory;
231             mVersionChecker = versionChecker;
232             mIsDebug = isDebug;
233         }
234 
235         /** Construct a new PluginInstance. */
create( Context context, ApplicationInfo appInfo, ComponentName componentName, Class<T> pluginClass, PluginListener<T> listener)236         public <T extends Plugin> PluginInstance<T> create(
237                 Context context,
238                 ApplicationInfo appInfo,
239                 ComponentName componentName,
240                 Class<T> pluginClass,
241                 PluginListener<T> listener)
242                 throws PackageManager.NameNotFoundException, ClassNotFoundException,
243                 InstantiationException, IllegalAccessException {
244 
245             PluginFactory<T> pluginFactory = new PluginFactory<T>(
246                     context, mInstanceFactory, appInfo, componentName, mVersionChecker, pluginClass,
247                     () -> getClassLoader(appInfo, mBaseClassLoader));
248             return new PluginInstance<T>(
249                     context, listener, componentName, pluginFactory, null);
250         }
251 
isPluginPackagePrivileged(String packageName)252         private boolean isPluginPackagePrivileged(String packageName) {
253             for (String componentNameOrPackage : mPrivilegedPlugins) {
254                 ComponentName componentName = ComponentName.unflattenFromString(
255                         componentNameOrPackage);
256                 if (componentName != null) {
257                     if (componentName.getPackageName().equals(packageName)) {
258                         return true;
259                     }
260                 } else if (componentNameOrPackage.equals(packageName)) {
261                     return true;
262                 }
263             }
264             return false;
265         }
266 
getParentClassLoader(ClassLoader baseClassLoader)267         private ClassLoader getParentClassLoader(ClassLoader baseClassLoader) {
268             return new PluginManagerImpl.ClassLoaderFilter(
269                     baseClassLoader,
270                     "androidx.constraintlayout.widget",
271                     "com.android.systemui.common",
272                     "com.android.systemui.log",
273                     "com.android.systemui.plugin");
274         }
275 
276         /** Returns class loader specific for the given plugin. */
getClassLoader(ApplicationInfo appInfo, ClassLoader baseClassLoader)277         private ClassLoader getClassLoader(ApplicationInfo appInfo,
278                 ClassLoader baseClassLoader) {
279             if (!mIsDebug && !isPluginPackagePrivileged(appInfo.packageName)) {
280                 Log.w(TAG, "Cannot get class loader for non-privileged plugin. Src:"
281                         + appInfo.sourceDir + ", pkg: " + appInfo.packageName);
282                 return null;
283             }
284 
285             List<String> zipPaths = new ArrayList<>();
286             List<String> libPaths = new ArrayList<>();
287             LoadedApk.makePaths(null, true, appInfo, zipPaths, libPaths);
288             ClassLoader classLoader = new PathClassLoader(
289                     TextUtils.join(File.pathSeparator, zipPaths),
290                     TextUtils.join(File.pathSeparator, libPaths),
291                     getParentClassLoader(baseClassLoader));
292             return classLoader;
293         }
294     }
295 
296     /** Class that compares a plugin class against an implementation for version matching. */
297     public interface VersionChecker {
298         /** Compares two plugin classes. */
checkVersion( Class<T> instanceClass, Class<T> pluginClass, Plugin plugin)299         <T extends Plugin> VersionInfo checkVersion(
300                 Class<T> instanceClass, Class<T> pluginClass, Plugin plugin);
301     }
302 
303     /** Class that compares a plugin class against an implementation for version matching. */
304     public static class VersionCheckerImpl implements VersionChecker {
305         @Override
306         /** Compares two plugin classes. */
checkVersion( Class<T> instanceClass, Class<T> pluginClass, Plugin plugin)307         public <T extends Plugin> VersionInfo checkVersion(
308                 Class<T> instanceClass, Class<T> pluginClass, Plugin plugin) {
309             VersionInfo pluginVersion = new VersionInfo().addClass(pluginClass);
310             VersionInfo instanceVersion = new VersionInfo().addClass(instanceClass);
311             if (instanceVersion.hasVersionInfo()) {
312                 pluginVersion.checkVersion(instanceVersion);
313             } else if (plugin != null) {
314                 int fallbackVersion = plugin.getVersion();
315                 if (fallbackVersion != pluginVersion.getDefaultVersion()) {
316                     throw new VersionInfo.InvalidVersionException("Invalid legacy version", false);
317                 }
318                 return null;
319             }
320             return instanceVersion;
321         }
322     }
323 
324     /**
325      *  Simple class to create a new instance. Useful for testing.
326      *
327      * @param <T> The type of plugin this create.
328      **/
329     public static class InstanceFactory<T extends Plugin> {
create(Class cls)330         T create(Class cls) throws IllegalAccessException, InstantiationException {
331             return (T) cls.newInstance();
332         }
333     }
334 
335     /**
336      * Instanced wrapper of InstanceFactory
337      *
338      * @param <T> is the type of the plugin object to be built
339      **/
340     public static class PluginFactory<T extends Plugin> {
341         private final Context mContext;
342         private final InstanceFactory<?> mInstanceFactory;
343         private final ApplicationInfo mAppInfo;
344         private final ComponentName mComponentName;
345         private final VersionChecker mVersionChecker;
346         private final Class<T> mPluginClass;
347         private final Supplier<ClassLoader> mClassLoaderFactory;
348 
PluginFactory( Context context, InstanceFactory<?> instanceFactory, ApplicationInfo appInfo, ComponentName componentName, VersionChecker versionChecker, Class<T> pluginClass, Supplier<ClassLoader> classLoaderFactory)349         public PluginFactory(
350                 Context context,
351                 InstanceFactory<?> instanceFactory,
352                 ApplicationInfo appInfo,
353                 ComponentName componentName,
354                 VersionChecker versionChecker,
355                 Class<T> pluginClass,
356                 Supplier<ClassLoader> classLoaderFactory) {
357             mContext = context;
358             mInstanceFactory = instanceFactory;
359             mAppInfo = appInfo;
360             mComponentName = componentName;
361             mVersionChecker = versionChecker;
362             mPluginClass = pluginClass;
363             mClassLoaderFactory = classLoaderFactory;
364         }
365 
366         /** Creates the related plugin object from the factory */
createPlugin()367         public T createPlugin() {
368             try {
369                 ClassLoader loader = mClassLoaderFactory.get();
370                 Class<T> instanceClass = (Class<T>) Class.forName(
371                         mComponentName.getClassName(), true, loader);
372                 T result = (T) mInstanceFactory.create(instanceClass);
373                 Log.v(TAG, "Created plugin: " + result);
374                 return result;
375             } catch (ClassNotFoundException ex) {
376                 Log.e(TAG, "Failed to load plugin", ex);
377             } catch (IllegalAccessException ex) {
378                 Log.e(TAG, "Failed to load plugin", ex);
379             } catch (InstantiationException ex) {
380                 Log.e(TAG, "Failed to load plugin", ex);
381             }
382             return null;
383         }
384 
385         /** Creates a context wrapper for the plugin */
createPluginContext()386         public Context createPluginContext() {
387             try {
388                 ClassLoader loader = mClassLoaderFactory.get();
389                 return new PluginActionManager.PluginContextWrapper(
390                     mContext.createApplicationContext(mAppInfo, 0), loader);
391             } catch (NameNotFoundException ex) {
392                 Log.e(TAG, "Failed to create plugin context", ex);
393             }
394             return null;
395         }
396 
397         /** Check Version and create VersionInfo for instance */
checkVersion(T instance)398         public VersionInfo checkVersion(T instance) {
399             if (instance == null) {
400                 instance = createPlugin();
401             }
402             return mVersionChecker.checkVersion(
403                     (Class<T>) instance.getClass(), mPluginClass, instance);
404         }
405     }
406 }
407