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