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