Home | History | Annotate | Download | only in plugins
      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