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.ComponentName;
     22 import android.content.Context;
     23 import android.content.ContextWrapper;
     24 import android.content.Intent;
     25 import android.content.pm.ApplicationInfo;
     26 import android.content.pm.PackageManager;
     27 import android.content.pm.PackageManager.NameNotFoundException;
     28 import android.content.pm.ResolveInfo;
     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.Message;
     35 import android.os.UserHandle;
     36 import android.util.Log;
     37 import android.view.LayoutInflater;
     38 
     39 import com.android.internal.annotations.VisibleForTesting;
     40 import com.android.internal.messages.nano.SystemMessageProto.SystemMessage;
     41 import com.android.systemui.plugins.VersionInfo.InvalidVersionException;
     42 
     43 import java.util.ArrayList;
     44 import java.util.List;
     45 
     46 public class PluginInstanceManager<T extends Plugin> {
     47 
     48     private static final boolean DEBUG = false;
     49 
     50     private static final String TAG = "PluginInstanceManager";
     51     public static final String PLUGIN_PERMISSION = "com.android.systemui.permission.PLUGIN";
     52 
     53     private final Context mContext;
     54     private final PluginListener<T> mListener;
     55     private final String mAction;
     56     private final boolean mAllowMultiple;
     57     private final VersionInfo mVersion;
     58 
     59     @VisibleForTesting
     60     final MainHandler mMainHandler;
     61     @VisibleForTesting
     62     final PluginHandler mPluginHandler;
     63     private final boolean isDebuggable;
     64     private final PackageManager mPm;
     65     private final PluginManagerImpl mManager;
     66 
     67     PluginInstanceManager(Context context, String action, PluginListener<T> listener,
     68             boolean allowMultiple, Looper looper, VersionInfo version, PluginManagerImpl manager) {
     69         this(context, context.getPackageManager(), action, listener, allowMultiple, looper, version,
     70                 manager, Build.IS_DEBUGGABLE);
     71     }
     72 
     73     @VisibleForTesting
     74     PluginInstanceManager(Context context, PackageManager pm, String action,
     75             PluginListener<T> listener, boolean allowMultiple, Looper looper, VersionInfo version,
     76             PluginManagerImpl manager, boolean debuggable) {
     77         mMainHandler = new MainHandler(Looper.getMainLooper());
     78         mPluginHandler = new PluginHandler(looper);
     79         mManager = manager;
     80         mContext = context;
     81         mPm = pm;
     82         mAction = action;
     83         mListener = listener;
     84         mAllowMultiple = allowMultiple;
     85         mVersion = version;
     86         isDebuggable = debuggable;
     87     }
     88 
     89     public PluginInfo<T> getPlugin() {
     90         if (Looper.myLooper() != Looper.getMainLooper()) {
     91             throw new RuntimeException("Must be called from UI thread");
     92         }
     93         mPluginHandler.handleQueryPlugins(null /* All packages */);
     94         if (mPluginHandler.mPlugins.size() > 0) {
     95             mMainHandler.removeMessages(MainHandler.PLUGIN_CONNECTED);
     96             PluginInfo<T> info = mPluginHandler.mPlugins.get(0);
     97             PluginPrefs.setHasPlugins(mContext);
     98             info.mPlugin.onCreate(mContext, info.mPluginContext);
     99             return info;
    100         }
    101         return null;
    102     }
    103 
    104     public void loadAll() {
    105         if (DEBUG) Log.d(TAG, "startListening");
    106         mPluginHandler.sendEmptyMessage(PluginHandler.QUERY_ALL);
    107     }
    108 
    109     public void destroy() {
    110         if (DEBUG) Log.d(TAG, "stopListening");
    111         ArrayList<PluginInfo> plugins = new ArrayList<>(mPluginHandler.mPlugins);
    112         for (PluginInfo plugin : plugins) {
    113             mMainHandler.obtainMessage(MainHandler.PLUGIN_DISCONNECTED,
    114                     plugin.mPlugin).sendToTarget();
    115         }
    116     }
    117 
    118     public void onPackageRemoved(String pkg) {
    119         mPluginHandler.obtainMessage(PluginHandler.REMOVE_PKG, pkg).sendToTarget();
    120     }
    121 
    122     public void onPackageChange(String pkg) {
    123         mPluginHandler.obtainMessage(PluginHandler.REMOVE_PKG, pkg).sendToTarget();
    124         mPluginHandler.obtainMessage(PluginHandler.QUERY_PKG, pkg).sendToTarget();
    125     }
    126 
    127     public boolean checkAndDisable(String className) {
    128         boolean disableAny = false;
    129         ArrayList<PluginInfo> plugins = new ArrayList<>(mPluginHandler.mPlugins);
    130         for (PluginInfo info : plugins) {
    131             if (className.startsWith(info.mPackage)) {
    132                 disable(info);
    133                 disableAny = true;
    134             }
    135         }
    136         return disableAny;
    137     }
    138 
    139     public boolean disableAll() {
    140         ArrayList<PluginInfo> plugins = new ArrayList<>(mPluginHandler.mPlugins);
    141         for (int i = 0; i < plugins.size(); i++) {
    142             disable(plugins.get(i));
    143         }
    144         return plugins.size() != 0;
    145     }
    146 
    147     private void disable(PluginInfo info) {
    148         // Live by the sword, die by the sword.
    149         // Misbehaving plugins get disabled and won't come back until uninstall/reinstall.
    150 
    151         // If a plugin is detected in the stack of a crash then this will be called for that
    152         // plugin, if the plugin causing a crash cannot be identified, they are all disabled
    153         // assuming one of them must be bad.
    154         Log.w(TAG, "Disabling plugin " + info.mPackage + "/" + info.mClass);
    155         mPm.setComponentEnabledSetting(
    156                 new ComponentName(info.mPackage, info.mClass),
    157                 PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
    158                 PackageManager.DONT_KILL_APP);
    159     }
    160 
    161     public <T> boolean dependsOn(Plugin p, Class<T> cls) {
    162         ArrayList<PluginInfo> plugins = new ArrayList<>(mPluginHandler.mPlugins);
    163         for (PluginInfo info : plugins) {
    164             if (info.mPlugin.getClass().getName().equals(p.getClass().getName())) {
    165                 return info.mVersion != null && info.mVersion.hasClass(cls);
    166             }
    167         }
    168         return false;
    169     }
    170 
    171     private class MainHandler extends Handler {
    172         private static final int PLUGIN_CONNECTED = 1;
    173         private static final int PLUGIN_DISCONNECTED = 2;
    174 
    175         public MainHandler(Looper looper) {
    176             super(looper);
    177         }
    178 
    179         @Override
    180         public void handleMessage(Message msg) {
    181             switch (msg.what) {
    182                 case PLUGIN_CONNECTED:
    183                     if (DEBUG) Log.d(TAG, "onPluginConnected");
    184                     PluginPrefs.setHasPlugins(mContext);
    185                     PluginInfo<T> info = (PluginInfo<T>) msg.obj;
    186                     mManager.handleWtfs();
    187                     if (!(msg.obj instanceof PluginFragment)) {
    188                         // Only call onDestroy for plugins that aren't fragments, as fragments
    189                         // will get the onCreate as part of the fragment lifecycle.
    190                         info.mPlugin.onCreate(mContext, info.mPluginContext);
    191                     }
    192                     mListener.onPluginConnected(info.mPlugin, info.mPluginContext);
    193                     break;
    194                 case PLUGIN_DISCONNECTED:
    195                     if (DEBUG) Log.d(TAG, "onPluginDisconnected");
    196                     mListener.onPluginDisconnected((T) msg.obj);
    197                     if (!(msg.obj instanceof PluginFragment)) {
    198                         // Only call onDestroy for plugins that aren't fragments, as fragments
    199                         // will get the onDestroy as part of the fragment lifecycle.
    200                         ((T) msg.obj).onDestroy();
    201                     }
    202                     break;
    203                 default:
    204                     super.handleMessage(msg);
    205                     break;
    206             }
    207         }
    208     }
    209 
    210     private class PluginHandler extends Handler {
    211         private static final int QUERY_ALL = 1;
    212         private static final int QUERY_PKG = 2;
    213         private static final int REMOVE_PKG = 3;
    214 
    215         private final ArrayList<PluginInfo<T>> mPlugins = new ArrayList<>();
    216 
    217         public PluginHandler(Looper looper) {
    218             super(looper);
    219         }
    220 
    221         @Override
    222         public void handleMessage(Message msg) {
    223             switch (msg.what) {
    224                 case QUERY_ALL:
    225                     if (DEBUG) Log.d(TAG, "queryAll " + mAction);
    226                     for (int i = mPlugins.size() - 1; i >= 0; i--) {
    227                         PluginInfo<T> plugin = mPlugins.get(i);
    228                         mListener.onPluginDisconnected(plugin.mPlugin);
    229                         if (!(plugin.mPlugin instanceof PluginFragment)) {
    230                             // Only call onDestroy for plugins that aren't fragments, as fragments
    231                             // will get the onDestroy as part of the fragment lifecycle.
    232                             plugin.mPlugin.onDestroy();
    233                         }
    234                     }
    235                     mPlugins.clear();
    236                     handleQueryPlugins(null);
    237                     break;
    238                 case REMOVE_PKG:
    239                     String pkg = (String) msg.obj;
    240                     for (int i = mPlugins.size() - 1; i >= 0; i--) {
    241                         final PluginInfo<T> plugin = mPlugins.get(i);
    242                         if (plugin.mPackage.equals(pkg)) {
    243                             mMainHandler.obtainMessage(MainHandler.PLUGIN_DISCONNECTED,
    244                                     plugin.mPlugin).sendToTarget();
    245                             mPlugins.remove(i);
    246                         }
    247                     }
    248                     break;
    249                 case QUERY_PKG:
    250                     String p = (String) msg.obj;
    251                     if (DEBUG) Log.d(TAG, "queryPkg " + p + " " + mAction);
    252                     if (mAllowMultiple || (mPlugins.size() == 0)) {
    253                         handleQueryPlugins(p);
    254                     } else {
    255                         if (DEBUG) Log.d(TAG, "Too many of " + mAction);
    256                     }
    257                     break;
    258                 default:
    259                     super.handleMessage(msg);
    260             }
    261         }
    262 
    263         private void handleQueryPlugins(String pkgName) {
    264             // This isn't actually a service and shouldn't ever be started, but is
    265             // a convenient PM based way to manage our plugins.
    266             Intent intent = new Intent(mAction);
    267             if (pkgName != null) {
    268                 intent.setPackage(pkgName);
    269             }
    270             List<ResolveInfo> result =
    271                     mPm.queryIntentServices(intent, 0);
    272             if (DEBUG) Log.d(TAG, "Found " + result.size() + " plugins");
    273             if (result.size() > 1 && !mAllowMultiple) {
    274                 // TODO: Show warning.
    275                 Log.w(TAG, "Multiple plugins found for " + mAction);
    276                 return;
    277             }
    278             for (ResolveInfo info : result) {
    279                 ComponentName name = new ComponentName(info.serviceInfo.packageName,
    280                         info.serviceInfo.name);
    281                 PluginInfo<T> t = handleLoadPlugin(name);
    282                 if (t == null) continue;
    283                 mMainHandler.obtainMessage(mMainHandler.PLUGIN_CONNECTED, t).sendToTarget();
    284                 mPlugins.add(t);
    285             }
    286         }
    287 
    288         protected PluginInfo<T> handleLoadPlugin(ComponentName component) {
    289             // This was already checked, but do it again here to make extra extra sure, we don't
    290             // use these on production builds.
    291             if (!isDebuggable) {
    292                 // Never ever ever allow these on production builds, they are only for prototyping.
    293                 Log.d(TAG, "Somehow hit second debuggable check");
    294                 return null;
    295             }
    296             String pkg = component.getPackageName();
    297             String cls = component.getClassName();
    298             try {
    299                 ApplicationInfo info = mPm.getApplicationInfo(pkg, 0);
    300                 // TODO: This probably isn't needed given that we don't have IGNORE_SECURITY on
    301                 if (mPm.checkPermission(PLUGIN_PERMISSION, pkg)
    302                         != PackageManager.PERMISSION_GRANTED) {
    303                     Log.d(TAG, "Plugin doesn't have permission: " + pkg);
    304                     return null;
    305                 }
    306                 // Create our own ClassLoader so we can use our own code as the parent.
    307                 ClassLoader classLoader = mManager.getClassLoader(info.sourceDir, info.packageName);
    308                 Context pluginContext = new PluginContextWrapper(
    309                         mContext.createApplicationContext(info, 0), classLoader);
    310                 Class<?> pluginClass = Class.forName(cls, true, classLoader);
    311                 // TODO: Only create the plugin before version check if we need it for
    312                 // legacy version check.
    313                 T plugin = (T) pluginClass.newInstance();
    314                 try {
    315                     VersionInfo version = checkVersion(pluginClass, plugin, mVersion);
    316                     if (DEBUG) Log.d(TAG, "createPlugin");
    317                     return new PluginInfo(pkg, cls, plugin, pluginContext, version);
    318                 } catch (InvalidVersionException e) {
    319                     final int icon = mContext.getResources().getIdentifier("tuner", "drawable",
    320                             mContext.getPackageName());
    321                     final int color = Resources.getSystem().getIdentifier(
    322                             "system_notification_accent_color", "color", "android");
    323                     final Notification.Builder nb = new Notification.Builder(mContext,
    324                             PluginManager.NOTIFICATION_CHANNEL_ID)
    325                                     .setStyle(new Notification.BigTextStyle())
    326                                     .setSmallIcon(icon)
    327                                     .setWhen(0)
    328                                     .setShowWhen(false)
    329                                     .setVisibility(Notification.VISIBILITY_PUBLIC)
    330                                     .setColor(mContext.getColor(color));
    331                     String label = cls;
    332                     try {
    333                         label = mPm.getServiceInfo(component, 0).loadLabel(mPm).toString();
    334                     } catch (NameNotFoundException e2) {
    335                     }
    336                     if (!e.isTooNew()) {
    337                         // Localization not required as this will never ever appear in a user build.
    338                         nb.setContentTitle("Plugin \"" + label + "\" is too old")
    339                                 .setContentText("Contact plugin developer to get an updated"
    340                                         + " version.\n" + e.getMessage());
    341                     } else {
    342                         // Localization not required as this will never ever appear in a user build.
    343                         nb.setContentTitle("Plugin \"" + label + "\" is too new")
    344                                 .setContentText("Check to see if an OTA is available.\n"
    345                                         + e.getMessage());
    346                     }
    347                     Intent i = new Intent(PluginManagerImpl.DISABLE_PLUGIN).setData(
    348                             Uri.parse("package://" + component.flattenToString()));
    349                     PendingIntent pi = PendingIntent.getBroadcast(mContext, 0, i, 0);
    350                     nb.addAction(new Action.Builder(null, "Disable plugin", pi).build());
    351                     mContext.getSystemService(NotificationManager.class)
    352                             .notifyAsUser(cls, SystemMessage.NOTE_PLUGIN, nb.build(),
    353                                     UserHandle.ALL);
    354                     // TODO: Warn user.
    355                     Log.w(TAG, "Plugin has invalid interface version " + plugin.getVersion()
    356                             + ", expected " + mVersion);
    357                     return null;
    358                 }
    359             } catch (Throwable e) {
    360                 Log.w(TAG, "Couldn't load plugin: " + pkg, e);
    361                 return null;
    362             }
    363         }
    364 
    365         private VersionInfo checkVersion(Class<?> pluginClass, T plugin, VersionInfo version)
    366                 throws InvalidVersionException {
    367             VersionInfo pv = new VersionInfo().addClass(pluginClass);
    368             if (pv.hasVersionInfo()) {
    369                 version.checkVersion(pv);
    370             } else {
    371                 int fallbackVersion = plugin.getVersion();
    372                 if (fallbackVersion != version.getDefaultVersion()) {
    373                     throw new InvalidVersionException("Invalid legacy version", false);
    374                 }
    375                 return null;
    376             }
    377             return pv;
    378         }
    379     }
    380 
    381     public static class PluginContextWrapper extends ContextWrapper {
    382         private final ClassLoader mClassLoader;
    383         private LayoutInflater mInflater;
    384 
    385         public PluginContextWrapper(Context base, ClassLoader classLoader) {
    386             super(base);
    387             mClassLoader = classLoader;
    388         }
    389 
    390         @Override
    391         public ClassLoader getClassLoader() {
    392             return mClassLoader;
    393         }
    394 
    395         @Override
    396         public Object getSystemService(String name) {
    397             if (LAYOUT_INFLATER_SERVICE.equals(name)) {
    398                 if (mInflater == null) {
    399                     mInflater = LayoutInflater.from(getBaseContext()).cloneInContext(this);
    400                 }
    401                 return mInflater;
    402             }
    403             return getBaseContext().getSystemService(name);
    404         }
    405     }
    406 
    407     static class PluginInfo<T> {
    408         private final Context mPluginContext;
    409         private final VersionInfo mVersion;
    410         private String mClass;
    411         T mPlugin;
    412         String mPackage;
    413 
    414         public PluginInfo(String pkg, String cls, T plugin, Context pluginContext,
    415                 VersionInfo info) {
    416             mPlugin = plugin;
    417             mClass = cls;
    418             mPackage = pkg;
    419             mPluginContext = pluginContext;
    420             mVersion = info;
    421         }
    422     }
    423 }
    424