1 /* 2 * Copyright (C) 2016 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file 5 * except in compliance with the License. You may obtain a copy of the License at 6 * 7 * http://www.apache.org/licenses/LICENSE-2.0 8 * 9 * Unless required by applicable law or agreed to in writing, software distributed under the 10 * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 11 * KIND, either express or implied. See the License for the specific language governing 12 * permissions and limitations under the License. 13 */ 14 15 package com.android.systemui.plugins; 16 17 import android.app.Notification; 18 import android.app.Notification.Action; 19 import android.app.NotificationManager; 20 import android.app.PendingIntent; 21 import android.content.BroadcastReceiver; 22 import android.content.ComponentName; 23 import android.content.Context; 24 import android.content.Intent; 25 import android.content.IntentFilter; 26 import android.content.pm.ApplicationInfo; 27 import android.content.pm.PackageManager; 28 import android.content.pm.PackageManager.NameNotFoundException; 29 import android.content.res.Resources; 30 import android.net.Uri; 31 import android.os.Build; 32 import android.os.Handler; 33 import android.os.Looper; 34 import android.os.SystemProperties; 35 import android.os.UserHandle; 36 import android.text.TextUtils; 37 import android.util.ArrayMap; 38 import android.util.ArraySet; 39 import android.util.Log; 40 import android.util.Log.TerribleFailure; 41 import android.util.Log.TerribleFailureHandler; 42 import android.widget.Toast; 43 44 import com.android.internal.annotations.VisibleForTesting; 45 import com.android.internal.messages.nano.SystemMessageProto.SystemMessage; 46 import com.android.systemui.Dependency; 47 import com.android.systemui.plugins.PluginInstanceManager.PluginContextWrapper; 48 import com.android.systemui.plugins.PluginInstanceManager.PluginInfo; 49 import com.android.systemui.plugins.annotations.ProvidesInterface; 50 51 import dalvik.system.PathClassLoader; 52 53 import java.lang.Thread.UncaughtExceptionHandler; 54 import java.util.Map; 55 56 /** 57 * @see Plugin 58 */ 59 public class PluginManagerImpl extends BroadcastReceiver implements PluginManager { 60 61 static final String DISABLE_PLUGIN = "com.android.systemui.action.DISABLE_PLUGIN"; 62 63 private static PluginManager sInstance; 64 65 private final ArrayMap<PluginListener<?>, PluginInstanceManager> mPluginMap 66 = new ArrayMap<>(); 67 private final Map<String, ClassLoader> mClassLoaders = new ArrayMap<>(); 68 private final ArraySet<String> mOneShotPackages = new ArraySet<>(); 69 private final Context mContext; 70 private final PluginInstanceManagerFactory mFactory; 71 private final boolean isDebuggable; 72 private final PluginPrefs mPluginPrefs; 73 private ClassLoaderFilter mParentClassLoader; 74 private boolean mListening; 75 private boolean mHasOneShot; 76 private Looper mLooper; 77 private boolean mWtfsSet; 78 PluginManagerImpl(Context context)79 public PluginManagerImpl(Context context) { 80 this(context, new PluginInstanceManagerFactory(), 81 Build.IS_DEBUGGABLE, Thread.getUncaughtExceptionPreHandler()); 82 } 83 84 @VisibleForTesting PluginManagerImpl(Context context, PluginInstanceManagerFactory factory, boolean debuggable, UncaughtExceptionHandler defaultHandler)85 PluginManagerImpl(Context context, PluginInstanceManagerFactory factory, boolean debuggable, 86 UncaughtExceptionHandler defaultHandler) { 87 mContext = context; 88 mFactory = factory; 89 mLooper = Dependency.get(Dependency.BG_LOOPER); 90 isDebuggable = debuggable; 91 mPluginPrefs = new PluginPrefs(mContext); 92 93 PluginExceptionHandler uncaughtExceptionHandler = new PluginExceptionHandler( 94 defaultHandler); 95 Thread.setUncaughtExceptionPreHandler(uncaughtExceptionHandler); 96 if (isDebuggable) { 97 new Handler(mLooper).post(() -> { 98 // Plugin dependencies that don't have another good home can go here, but 99 // dependencies that have better places to init can happen elsewhere. 100 Dependency.get(PluginDependencyProvider.class) 101 .allowPluginDependency(ActivityStarter.class); 102 }); 103 } 104 } 105 getOneShotPlugin(Class<T> cls)106 public <T extends Plugin> T getOneShotPlugin(Class<T> cls) { 107 ProvidesInterface info = cls.getDeclaredAnnotation(ProvidesInterface.class); 108 if (info == null) { 109 throw new RuntimeException(cls + " doesn't provide an interface"); 110 } 111 if (TextUtils.isEmpty(info.action())) { 112 throw new RuntimeException(cls + " doesn't provide an action"); 113 } 114 return getOneShotPlugin(info.action(), cls); 115 } 116 getOneShotPlugin(String action, Class<?> cls)117 public <T extends Plugin> T getOneShotPlugin(String action, Class<?> cls) { 118 if (!isDebuggable) { 119 // Never ever ever allow these on production builds, they are only for prototyping. 120 return null; 121 } 122 if (Looper.myLooper() != Looper.getMainLooper()) { 123 throw new RuntimeException("Must be called from UI thread"); 124 } 125 PluginInstanceManager<T> p = mFactory.createPluginInstanceManager(mContext, action, null, 126 false, mLooper, cls, this); 127 mPluginPrefs.addAction(action); 128 PluginInfo<T> info = p.getPlugin(); 129 if (info != null) { 130 mOneShotPackages.add(info.mPackage); 131 mHasOneShot = true; 132 startListening(); 133 return info.mPlugin; 134 } 135 return null; 136 } 137 addPluginListener(PluginListener<T> listener, Class<?> cls)138 public <T extends Plugin> void addPluginListener(PluginListener<T> listener, Class<?> cls) { 139 addPluginListener(listener, cls, false); 140 } 141 addPluginListener(PluginListener<T> listener, Class<?> cls, boolean allowMultiple)142 public <T extends Plugin> void addPluginListener(PluginListener<T> listener, Class<?> cls, 143 boolean allowMultiple) { 144 addPluginListener(PluginManager.getAction(cls), listener, cls, allowMultiple); 145 } 146 addPluginListener(String action, PluginListener<T> listener, Class<?> cls)147 public <T extends Plugin> void addPluginListener(String action, PluginListener<T> listener, 148 Class<?> cls) { 149 addPluginListener(action, listener, cls, false); 150 } 151 addPluginListener(String action, PluginListener<T> listener, Class cls, boolean allowMultiple)152 public <T extends Plugin> void addPluginListener(String action, PluginListener<T> listener, 153 Class cls, boolean allowMultiple) { 154 if (!isDebuggable) { 155 // Never ever ever allow these on production builds, they are only for prototyping. 156 return; 157 } 158 mPluginPrefs.addAction(action); 159 PluginInstanceManager p = mFactory.createPluginInstanceManager(mContext, action, listener, 160 allowMultiple, mLooper, cls, this); 161 p.loadAll(); 162 mPluginMap.put(listener, p); 163 startListening(); 164 } 165 removePluginListener(PluginListener<?> listener)166 public void removePluginListener(PluginListener<?> listener) { 167 if (!isDebuggable) { 168 // Never ever ever allow these on production builds, they are only for prototyping. 169 return; 170 } 171 if (!mPluginMap.containsKey(listener)) return; 172 mPluginMap.remove(listener).destroy(); 173 if (mPluginMap.size() == 0) { 174 stopListening(); 175 } 176 } 177 startListening()178 private void startListening() { 179 if (mListening) return; 180 mListening = true; 181 IntentFilter filter = new IntentFilter(Intent.ACTION_PACKAGE_ADDED); 182 filter.addAction(Intent.ACTION_PACKAGE_CHANGED); 183 filter.addAction(Intent.ACTION_PACKAGE_REMOVED); 184 filter.addAction(PLUGIN_CHANGED); 185 filter.addAction(DISABLE_PLUGIN); 186 filter.addDataScheme("package"); 187 mContext.registerReceiver(this, filter); 188 filter = new IntentFilter(Intent.ACTION_USER_UNLOCKED); 189 mContext.registerReceiver(this, filter); 190 } 191 stopListening()192 private void stopListening() { 193 // Never stop listening if a one-shot is present. 194 if (!mListening || mHasOneShot) return; 195 mListening = false; 196 mContext.unregisterReceiver(this); 197 } 198 199 @Override onReceive(Context context, Intent intent)200 public void onReceive(Context context, Intent intent) { 201 if (Intent.ACTION_USER_UNLOCKED.equals(intent.getAction())) { 202 for (PluginInstanceManager manager : mPluginMap.values()) { 203 manager.loadAll(); 204 } 205 } else if (DISABLE_PLUGIN.equals(intent.getAction())) { 206 Uri uri = intent.getData(); 207 ComponentName component = ComponentName.unflattenFromString( 208 uri.toString().substring(10)); 209 mContext.getPackageManager().setComponentEnabledSetting(component, 210 PackageManager.COMPONENT_ENABLED_STATE_DISABLED, 211 PackageManager.DONT_KILL_APP); 212 mContext.getSystemService(NotificationManager.class).cancel(component.getClassName(), 213 SystemMessage.NOTE_PLUGIN); 214 } else { 215 Uri data = intent.getData(); 216 String pkg = data.getEncodedSchemeSpecificPart(); 217 if (mOneShotPackages.contains(pkg)) { 218 int icon = mContext.getResources().getIdentifier("tuner", "drawable", 219 mContext.getPackageName()); 220 int color = Resources.getSystem().getIdentifier( 221 "system_notification_accent_color", "color", "android"); 222 String label = pkg; 223 try { 224 PackageManager pm = mContext.getPackageManager(); 225 label = pm.getApplicationInfo(pkg, 0).loadLabel(pm).toString(); 226 } catch (NameNotFoundException e) { 227 } 228 // Localization not required as this will never ever appear in a user build. 229 final Notification.Builder nb = 230 new Notification.Builder(mContext, NOTIFICATION_CHANNEL_ID) 231 .setSmallIcon(icon) 232 .setWhen(0) 233 .setShowWhen(false) 234 .setPriority(Notification.PRIORITY_MAX) 235 .setVisibility(Notification.VISIBILITY_PUBLIC) 236 .setColor(mContext.getColor(color)) 237 .setContentTitle("Plugin \"" + label + "\" has updated") 238 .setContentText("Restart SysUI for changes to take effect."); 239 Intent i = new Intent("com.android.systemui.action.RESTART").setData( 240 Uri.parse("package://" + pkg)); 241 PendingIntent pi = PendingIntent.getBroadcast(mContext, 0, i, 0); 242 nb.addAction(new Action.Builder(null, "Restart SysUI", pi).build()); 243 mContext.getSystemService(NotificationManager.class).notifyAsUser(pkg, 244 SystemMessage.NOTE_PLUGIN, nb.build(), UserHandle.ALL); 245 } 246 if (clearClassLoader(pkg)) { 247 Toast.makeText(mContext, "Reloading " + pkg, Toast.LENGTH_LONG).show(); 248 } 249 if (!Intent.ACTION_PACKAGE_REMOVED.equals(intent.getAction())) { 250 for (PluginInstanceManager manager : mPluginMap.values()) { 251 manager.onPackageChange(pkg); 252 } 253 } else { 254 for (PluginInstanceManager manager : mPluginMap.values()) { 255 manager.onPackageRemoved(pkg); 256 } 257 } 258 } 259 } 260 getClassLoader(String sourceDir, String pkg)261 public ClassLoader getClassLoader(String sourceDir, String pkg) { 262 if (mClassLoaders.containsKey(pkg)) { 263 return mClassLoaders.get(pkg); 264 } 265 ClassLoader classLoader = new PathClassLoader(sourceDir, getParentClassLoader()); 266 mClassLoaders.put(pkg, classLoader); 267 return classLoader; 268 } 269 clearClassLoader(String pkg)270 private boolean clearClassLoader(String pkg) { 271 return mClassLoaders.remove(pkg) != null; 272 } 273 getParentClassLoader()274 ClassLoader getParentClassLoader() { 275 if (mParentClassLoader == null) { 276 // Lazily load this so it doesn't have any effect on devices without plugins. 277 mParentClassLoader = new ClassLoaderFilter(getClass().getClassLoader(), 278 "com.android.systemui.plugin"); 279 } 280 return mParentClassLoader; 281 } 282 getContext(ApplicationInfo info, String pkg)283 public Context getContext(ApplicationInfo info, String pkg) throws NameNotFoundException { 284 ClassLoader classLoader = getClassLoader(info.sourceDir, pkg); 285 return new PluginContextWrapper(mContext.createApplicationContext(info, 0), classLoader); 286 } 287 dependsOn(Plugin p, Class<T> cls)288 public <T> boolean dependsOn(Plugin p, Class<T> cls) { 289 for (int i = 0; i < mPluginMap.size(); i++) { 290 if (mPluginMap.valueAt(i).dependsOn(p, cls)) { 291 return true; 292 } 293 } 294 return false; 295 } 296 handleWtfs()297 public void handleWtfs() { 298 if (!mWtfsSet) { 299 mWtfsSet = true; 300 Log.setWtfHandler((tag, what, system) -> { 301 throw new CrashWhilePluginActiveException(what); 302 }); 303 } 304 } 305 306 @VisibleForTesting 307 public static class PluginInstanceManagerFactory { createPluginInstanceManager(Context context, String action, PluginListener<T> listener, boolean allowMultiple, Looper looper, Class<?> cls, PluginManagerImpl manager)308 public <T extends Plugin> PluginInstanceManager createPluginInstanceManager(Context context, 309 String action, PluginListener<T> listener, boolean allowMultiple, Looper looper, 310 Class<?> cls, PluginManagerImpl manager) { 311 return new PluginInstanceManager(context, action, listener, allowMultiple, looper, 312 new VersionInfo().addClass(cls), manager); 313 } 314 } 315 316 // This allows plugins to include any libraries or copied code they want by only including 317 // classes from the plugin library. 318 private static class ClassLoaderFilter extends ClassLoader { 319 private final String mPackage; 320 private final ClassLoader mBase; 321 ClassLoaderFilter(ClassLoader base, String pkg)322 public ClassLoaderFilter(ClassLoader base, String pkg) { 323 super(ClassLoader.getSystemClassLoader()); 324 mBase = base; 325 mPackage = pkg; 326 } 327 328 @Override loadClass(String name, boolean resolve)329 protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { 330 if (!name.startsWith(mPackage)) super.loadClass(name, resolve); 331 return mBase.loadClass(name); 332 } 333 } 334 335 private class PluginExceptionHandler implements UncaughtExceptionHandler { 336 private final UncaughtExceptionHandler mHandler; 337 PluginExceptionHandler(UncaughtExceptionHandler handler)338 private PluginExceptionHandler(UncaughtExceptionHandler handler) { 339 mHandler = handler; 340 } 341 342 @Override uncaughtException(Thread thread, Throwable throwable)343 public void uncaughtException(Thread thread, Throwable throwable) { 344 if (SystemProperties.getBoolean("plugin.debugging", false)) { 345 mHandler.uncaughtException(thread, throwable); 346 return; 347 } 348 // Search for and disable plugins that may have been involved in this crash. 349 boolean disabledAny = checkStack(throwable); 350 if (!disabledAny) { 351 // We couldn't find any plugins involved in this crash, just to be safe 352 // disable all the plugins, so we can be sure that SysUI is running as 353 // best as possible. 354 for (PluginInstanceManager manager : mPluginMap.values()) { 355 disabledAny |= manager.disableAll(); 356 } 357 } 358 if (disabledAny) { 359 throwable = new CrashWhilePluginActiveException(throwable); 360 } 361 362 // Run the normal exception handler so we can crash and cleanup our state. 363 mHandler.uncaughtException(thread, throwable); 364 } 365 checkStack(Throwable throwable)366 private boolean checkStack(Throwable throwable) { 367 if (throwable == null) return false; 368 boolean disabledAny = false; 369 for (StackTraceElement element : throwable.getStackTrace()) { 370 for (PluginInstanceManager manager : mPluginMap.values()) { 371 disabledAny |= manager.checkAndDisable(element.getClassName()); 372 } 373 } 374 return disabledAny | checkStack(throwable.getCause()); 375 } 376 } 377 378 private class CrashWhilePluginActiveException extends RuntimeException { CrashWhilePluginActiveException(Throwable throwable)379 public CrashWhilePluginActiveException(Throwable throwable) { 380 super(throwable); 381 } 382 } 383 } 384