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.text.TextUtils;
25 import android.util.ArrayMap;
26 import android.util.Log;
27 
28 import com.android.internal.annotations.VisibleForTesting;
29 import com.android.systemui.plugins.Plugin;
30 import com.android.systemui.plugins.PluginFragment;
31 import com.android.systemui.plugins.PluginListener;
32 
33 import dalvik.system.PathClassLoader;
34 
35 import java.io.File;
36 import java.util.ArrayList;
37 import java.util.List;
38 import java.util.Map;
39 
40 /**
41  * Contains a single instantiation of a Plugin.
42  *
43  * This class and its related Factory are in charge of actually instantiating a plugin and
44  * managing any state related to it.
45  *
46  * @param <T> The type of plugin that this contains.
47  */
48 public class PluginInstance<T extends Plugin> {
49     private static final String TAG = "PluginInstance";
50     private static final Map<String, ClassLoader> sClassLoaders = new ArrayMap<>();
51 
52     private final Context mPluginContext;
53     private final VersionInfo mVersionInfo;
54     private final ComponentName mComponentName;
55     private final T mPlugin;
56 
57     /** */
PluginInstance(ComponentName componentName, T plugin, Context pluginContext, VersionInfo versionInfo)58     public PluginInstance(ComponentName componentName, T plugin, Context pluginContext,
59             VersionInfo versionInfo) {
60         mComponentName = componentName;
61         mPlugin = plugin;
62         mPluginContext = pluginContext;
63         mVersionInfo = versionInfo;
64     }
65 
66     /** Alerts listener and plugin that the plugin has been created. */
onCreate(Context appContext, PluginListener<T> listener)67     public void onCreate(Context appContext, PluginListener<T> listener) {
68         if (!(mPlugin instanceof PluginFragment)) {
69             // Only call onCreate for plugins that aren't fragments, as fragments
70             // will get the onCreate as part of the fragment lifecycle.
71             mPlugin.onCreate(appContext, mPluginContext);
72         }
73         listener.onPluginConnected(mPlugin, mPluginContext);
74     }
75 
76     /** Alerts listener and plugin that the plugin is being shutdown. */
onDestroy(PluginListener<T> listener)77     public void onDestroy(PluginListener<T> listener) {
78         listener.onPluginDisconnected(mPlugin);
79         if (!(mPlugin instanceof PluginFragment)) {
80             // Only call onDestroy for plugins that aren't fragments, as fragments
81             // will get the onDestroy as part of the fragment lifecycle.
82             mPlugin.onDestroy();
83         }
84     }
85 
86     /**
87      * Returns if the contained plugin matches the passed in class name.
88      *
89      * It does this by string comparison of the class names.
90      **/
containsPluginClass(Class pluginClass)91     public boolean containsPluginClass(Class pluginClass) {
92         return mPlugin.getClass().getName().equals(pluginClass.getName());
93     }
94 
getComponentName()95     public ComponentName getComponentName() {
96         return mComponentName;
97     }
98 
getPackage()99     public String getPackage() {
100         return mComponentName.getPackageName();
101     }
102 
getVersionInfo()103     public VersionInfo getVersionInfo() {
104         return mVersionInfo;
105     }
106 
107     @VisibleForTesting
getPluginContext()108     Context getPluginContext() {
109         return mPluginContext;
110     }
111 
112     /** Used to create new {@link PluginInstance}s. */
113     public static class Factory {
114         private final ClassLoader mBaseClassLoader;
115         private final InstanceFactory<?> mInstanceFactory;
116         private final VersionChecker mVersionChecker;
117         private final boolean mIsDebug;
118         private final List<String> mPrivilegedPlugins;
119 
120         /** Factory used to construct {@link PluginInstance}s. */
Factory(ClassLoader classLoader, InstanceFactory<?> instanceFactory, VersionChecker versionChecker, List<String> privilegedPlugins, boolean isDebug)121         public Factory(ClassLoader classLoader, InstanceFactory<?> instanceFactory,
122                 VersionChecker versionChecker,
123                 List<String> privilegedPlugins,
124                 boolean isDebug) {
125             mPrivilegedPlugins = privilegedPlugins;
126             mBaseClassLoader = classLoader;
127             mInstanceFactory = instanceFactory;
128             mVersionChecker = versionChecker;
129             mIsDebug = isDebug;
130         }
131 
132         /** Construct a new PluginInstance. */
create( Context context, ApplicationInfo appInfo, ComponentName componentName, Class<T> pluginClass)133         public <T extends Plugin> PluginInstance<T> create(
134                 Context context,
135                 ApplicationInfo appInfo,
136                 ComponentName componentName,
137                 Class<T> pluginClass)
138                 throws PackageManager.NameNotFoundException, ClassNotFoundException,
139                 InstantiationException, IllegalAccessException {
140 
141             ClassLoader classLoader = getClassLoader(appInfo, mBaseClassLoader);
142             Context pluginContext = new PluginActionManager.PluginContextWrapper(
143                     context.createApplicationContext(appInfo, 0), classLoader);
144             Class<T> instanceClass = (Class<T>) Class.forName(
145                     componentName.getClassName(), true, classLoader);
146             // TODO: Only create the plugin before version check if we need it for
147             // legacy version check.
148             T instance = (T) mInstanceFactory.create(instanceClass);
149             VersionInfo version = mVersionChecker.checkVersion(
150                     instanceClass, pluginClass, instance);
151             return new PluginInstance<T>(componentName, instance, pluginContext, version);
152         }
153 
isPluginPackagePrivileged(String packageName)154         private boolean isPluginPackagePrivileged(String packageName) {
155             for (String componentNameOrPackage : mPrivilegedPlugins) {
156                 ComponentName componentName = ComponentName.unflattenFromString(
157                         componentNameOrPackage);
158                 if (componentName != null) {
159                     if (componentName.getPackageName().equals(packageName)) {
160                         return true;
161                     }
162                 } else if (componentNameOrPackage.equals(packageName)) {
163                     return true;
164                 }
165             }
166             return false;
167         }
168 
getParentClassLoader(ClassLoader baseClassLoader)169         private ClassLoader getParentClassLoader(ClassLoader baseClassLoader) {
170             return new PluginManagerImpl.ClassLoaderFilter(
171                     baseClassLoader, "com.android.systemui.plugin");
172         }
173 
174         /** Returns class loader specific for the given plugin. */
getClassLoader(ApplicationInfo appInfo, ClassLoader baseClassLoader)175         private ClassLoader getClassLoader(ApplicationInfo appInfo,
176                 ClassLoader baseClassLoader) {
177             if (!mIsDebug && !isPluginPackagePrivileged(appInfo.packageName)) {
178                 Log.w(TAG, "Cannot get class loader for non-privileged plugin. Src:"
179                         + appInfo.sourceDir + ", pkg: " + appInfo.packageName);
180                 return null;
181             }
182             if (sClassLoaders.containsKey(appInfo.packageName)) {
183                 return sClassLoaders.get(appInfo.packageName);
184             }
185 
186             List<String> zipPaths = new ArrayList<>();
187             List<String> libPaths = new ArrayList<>();
188             LoadedApk.makePaths(null, true, appInfo, zipPaths, libPaths);
189             ClassLoader classLoader = new PathClassLoader(
190                     TextUtils.join(File.pathSeparator, zipPaths),
191                     TextUtils.join(File.pathSeparator, libPaths),
192                     getParentClassLoader(baseClassLoader));
193             sClassLoaders.put(appInfo.packageName, classLoader);
194             return classLoader;
195         }
196     }
197 
198     /** Class that compares a plugin class against an implementation for version matching. */
199     public static class VersionChecker {
200         /** Compares two plugin classes. */
checkVersion( Class<T> instanceClass, Class<T> pluginClass, Plugin plugin)201         public <T extends Plugin> VersionInfo checkVersion(
202                 Class<T> instanceClass, Class<T> pluginClass, Plugin plugin) {
203             VersionInfo pluginVersion = new VersionInfo().addClass(pluginClass);
204             VersionInfo instanceVersion = new VersionInfo().addClass(instanceClass);
205             if (instanceVersion.hasVersionInfo()) {
206                 pluginVersion.checkVersion(instanceVersion);
207             } else {
208                 int fallbackVersion = plugin.getVersion();
209                 if (fallbackVersion != pluginVersion.getDefaultVersion()) {
210                     throw new VersionInfo.InvalidVersionException("Invalid legacy version", false);
211                 }
212                 return null;
213             }
214             return instanceVersion;
215         }
216     }
217 
218     /**
219      *  Simple class to create a new instance. Useful for testing.
220      *
221      * @param <T> The type of plugin this create.
222      **/
223     public static class InstanceFactory<T extends Plugin> {
create(Class cls)224         T create(Class cls) throws IllegalAccessException, InstantiationException {
225             return (T) cls.newInstance();
226         }
227     }
228 }
229