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