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