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.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 
     79     public PluginManagerImpl(Context context) {
     80         this(context, new PluginInstanceManagerFactory(),
     81                 Build.IS_DEBUGGABLE, Thread.getUncaughtExceptionPreHandler());
     82     }
     83 
     84     @VisibleForTesting
     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 
    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 
    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 
    138     public <T extends Plugin> void addPluginListener(PluginListener<T> listener, Class<?> cls) {
    139         addPluginListener(listener, cls, false);
    140     }
    141 
    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 
    147     public <T extends Plugin> void addPluginListener(String action, PluginListener<T> listener,
    148             Class<?> cls) {
    149         addPluginListener(action, listener, cls, false);
    150     }
    151 
    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 
    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 
    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 
    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
    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 
    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 
    270     private boolean clearClassLoader(String pkg) {
    271         return mClassLoaders.remove(pkg) != null;
    272     }
    273 
    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 
    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 
    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 
    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 {
    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 
    322         public ClassLoaderFilter(ClassLoader base, String pkg) {
    323             super(ClassLoader.getSystemClassLoader());
    324             mBase = base;
    325             mPackage = pkg;
    326         }
    327 
    328         @Override
    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 
    338         private PluginExceptionHandler(UncaughtExceptionHandler handler) {
    339             mHandler = handler;
    340         }
    341 
    342         @Override
    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 
    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 {
    379         public CrashWhilePluginActiveException(Throwable throwable) {
    380             super(throwable);
    381         }
    382     }
    383 }
    384