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