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     @Override
    172     public String toString() {
    173         return String.format("%s@%s (action=%s)",
    174                 getClass().getSimpleName(), hashCode(), mAction);
    175     }
    176 
    177     private class MainHandler extends Handler {
    178         private static final int PLUGIN_CONNECTED = 1;
    179         private static final int PLUGIN_DISCONNECTED = 2;
    180 
    181         public MainHandler(Looper looper) {
    182             super(looper);
    183         }
    184 
    185         @Override
    186         public void handleMessage(Message msg) {
    187             switch (msg.what) {
    188                 case PLUGIN_CONNECTED:
    189                     if (DEBUG) Log.d(TAG, "onPluginConnected");
    190                     PluginPrefs.setHasPlugins(mContext);
    191                     PluginInfo<T> info = (PluginInfo<T>) msg.obj;
    192                     mManager.handleWtfs();
    193                     if (!(msg.obj instanceof PluginFragment)) {
    194                         // Only call onDestroy for plugins that aren't fragments, as fragments
    195                         // will get the onCreate as part of the fragment lifecycle.
    196                         info.mPlugin.onCreate(mContext, info.mPluginContext);
    197                     }
    198                     mListener.onPluginConnected(info.mPlugin, info.mPluginContext);
    199                     break;
    200                 case PLUGIN_DISCONNECTED:
    201                     if (DEBUG) Log.d(TAG, "onPluginDisconnected");
    202                     mListener.onPluginDisconnected((T) msg.obj);
    203                     if (!(msg.obj instanceof PluginFragment)) {
    204                         // Only call onDestroy for plugins that aren't fragments, as fragments
    205                         // will get the onDestroy as part of the fragment lifecycle.
    206                         ((T) msg.obj).onDestroy();
    207                     }
    208                     break;
    209                 default:
    210                     super.handleMessage(msg);
    211                     break;
    212             }
    213         }
    214     }
    215 
    216     private class PluginHandler extends Handler {
    217         private static final int QUERY_ALL = 1;
    218         private static final int QUERY_PKG = 2;
    219         private static final int REMOVE_PKG = 3;
    220 
    221         private final ArrayList<PluginInfo<T>> mPlugins = new ArrayList<>();
    222 
    223         public PluginHandler(Looper looper) {
    224             super(looper);
    225         }
    226 
    227         @Override
    228         public void handleMessage(Message msg) {
    229             switch (msg.what) {
    230                 case QUERY_ALL:
    231                     if (DEBUG) Log.d(TAG, "queryAll " + mAction);
    232                     for (int i = mPlugins.size() - 1; i >= 0; i--) {
    233                         PluginInfo<T> plugin = mPlugins.get(i);
    234                         mListener.onPluginDisconnected(plugin.mPlugin);
    235                         if (!(plugin.mPlugin instanceof PluginFragment)) {
    236                             // Only call onDestroy for plugins that aren't fragments, as fragments
    237                             // will get the onDestroy as part of the fragment lifecycle.
    238                             plugin.mPlugin.onDestroy();
    239                         }
    240                     }
    241                     mPlugins.clear();
    242                     handleQueryPlugins(null);
    243                     break;
    244                 case REMOVE_PKG:
    245                     String pkg = (String) msg.obj;
    246                     for (int i = mPlugins.size() - 1; i >= 0; i--) {
    247                         final PluginInfo<T> plugin = mPlugins.get(i);
    248                         if (plugin.mPackage.equals(pkg)) {
    249                             mMainHandler.obtainMessage(MainHandler.PLUGIN_DISCONNECTED,
    250                                     plugin.mPlugin).sendToTarget();
    251                             mPlugins.remove(i);
    252                         }
    253                     }
    254                     break;
    255                 case QUERY_PKG:
    256                     String p = (String) msg.obj;
    257                     if (DEBUG) Log.d(TAG, "queryPkg " + p + " " + mAction);
    258                     if (mAllowMultiple || (mPlugins.size() == 0)) {
    259                         handleQueryPlugins(p);
    260                     } else {
    261                         if (DEBUG) Log.d(TAG, "Too many of " + mAction);
    262                     }
    263                     break;
    264                 default:
    265                     super.handleMessage(msg);
    266             }
    267         }
    268 
    269         private void handleQueryPlugins(String pkgName) {
    270             // This isn't actually a service and shouldn't ever be started, but is
    271             // a convenient PM based way to manage our plugins.
    272             Intent intent = new Intent(mAction);
    273             if (pkgName != null) {
    274                 intent.setPackage(pkgName);
    275             }
    276             List<ResolveInfo> result =
    277                     mPm.queryIntentServices(intent, 0);
    278             if (DEBUG) Log.d(TAG, "Found " + result.size() + " plugins");
    279             if (result.size() > 1 && !mAllowMultiple) {
    280                 // TODO: Show warning.
    281                 Log.w(TAG, "Multiple plugins found for " + mAction);
    282                 return;
    283             }
    284             for (ResolveInfo info : result) {
    285                 ComponentName name = new ComponentName(info.serviceInfo.packageName,
    286                         info.serviceInfo.name);
    287                 PluginInfo<T> t = handleLoadPlugin(name);
    288                 if (t == null) continue;
    289                 mMainHandler.obtainMessage(mMainHandler.PLUGIN_CONNECTED, t).sendToTarget();
    290                 mPlugins.add(t);
    291             }
    292         }
    293 
    294         protected PluginInfo<T> handleLoadPlugin(ComponentName component) {
    295             // This was already checked, but do it again here to make extra extra sure, we don't
    296             // use these on production builds.
    297             if (!isDebuggable) {
    298                 // Never ever ever allow these on production builds, they are only for prototyping.
    299                 Log.d(TAG, "Somehow hit second debuggable check");
    300                 return null;
    301             }
    302             String pkg = component.getPackageName();
    303             String cls = component.getClassName();
    304             try {
    305                 ApplicationInfo info = mPm.getApplicationInfo(pkg, 0);
    306                 // TODO: This probably isn't needed given that we don't have IGNORE_SECURITY on
    307                 if (mPm.checkPermission(PLUGIN_PERMISSION, pkg)
    308                         != PackageManager.PERMISSION_GRANTED) {
    309                     Log.d(TAG, "Plugin doesn't have permission: " + pkg);
    310                     return null;
    311                 }
    312                 // Create our own ClassLoader so we can use our own code as the parent.
    313                 ClassLoader classLoader = mManager.getClassLoader(info.sourceDir, info.packageName);
    314                 Context pluginContext = new PluginContextWrapper(
    315                         mContext.createApplicationContext(info, 0), classLoader);
    316                 Class<?> pluginClass = Class.forName(cls, true, classLoader);
    317                 // TODO: Only create the plugin before version check if we need it for
    318                 // legacy version check.
    319                 T plugin = (T) pluginClass.newInstance();
    320                 try {
    321                     VersionInfo version = checkVersion(pluginClass, plugin, mVersion);
    322                     if (DEBUG) Log.d(TAG, "createPlugin");
    323                     return new PluginInfo(pkg, cls, plugin, pluginContext, version);
    324                 } catch (InvalidVersionException e) {
    325                     final int icon = mContext.getResources().getIdentifier("tuner", "drawable",
    326                             mContext.getPackageName());
    327                     final int color = Resources.getSystem().getIdentifier(
    328                             "system_notification_accent_color", "color", "android");
    329                     final Notification.Builder nb = new Notification.Builder(mContext,
    330                             PluginManager.NOTIFICATION_CHANNEL_ID)
    331                                     .setStyle(new Notification.BigTextStyle())
    332                                     .setSmallIcon(icon)
    333                                     .setWhen(0)
    334                                     .setShowWhen(false)
    335                                     .setVisibility(Notification.VISIBILITY_PUBLIC)
    336                                     .setColor(mContext.getColor(color));
    337                     String label = cls;
    338                     try {
    339                         label = mPm.getServiceInfo(component, 0).loadLabel(mPm).toString();
    340                     } catch (NameNotFoundException e2) {
    341                     }
    342                     if (!e.isTooNew()) {
    343                         // Localization not required as this will never ever appear in a user build.
    344                         nb.setContentTitle("Plugin \"" + label + "\" is too old")
    345                                 .setContentText("Contact plugin developer to get an updated"
    346                                         + " version.\n" + e.getMessage());
    347                     } else {
    348                         // Localization not required as this will never ever appear in a user build.
    349                         nb.setContentTitle("Plugin \"" + label + "\" is too new")
    350                                 .setContentText("Check to see if an OTA is available.\n"
    351                                         + e.getMessage());
    352                     }
    353                     Intent i = new Intent(PluginManagerImpl.DISABLE_PLUGIN).setData(
    354                             Uri.parse("package://" + component.flattenToString()));
    355                     PendingIntent pi = PendingIntent.getBroadcast(mContext, 0, i, 0);
    356                     nb.addAction(new Action.Builder(null, "Disable plugin", pi).build());
    357                     mContext.getSystemService(NotificationManager.class)
    358                             .notifyAsUser(cls, SystemMessage.NOTE_PLUGIN, nb.build(),
    359                                     UserHandle.ALL);
    360                     // TODO: Warn user.
    361                     Log.w(TAG, "Plugin has invalid interface version " + plugin.getVersion()
    362                             + ", expected " + mVersion);
    363                     return null;
    364                 }
    365             } catch (Throwable e) {
    366                 Log.w(TAG, "Couldn't load plugin: " + pkg, e);
    367                 return null;
    368             }
    369         }
    370 
    371         private VersionInfo checkVersion(Class<?> pluginClass, T plugin, VersionInfo version)
    372                 throws InvalidVersionException {
    373             VersionInfo pv = new VersionInfo().addClass(pluginClass);
    374             if (pv.hasVersionInfo()) {
    375                 version.checkVersion(pv);
    376             } else {
    377                 int fallbackVersion = plugin.getVersion();
    378                 if (fallbackVersion != version.getDefaultVersion()) {
    379                     throw new InvalidVersionException("Invalid legacy version", false);
    380                 }
    381                 return null;
    382             }
    383             return pv;
    384         }
    385     }
    386 
    387     public static class PluginContextWrapper extends ContextWrapper {
    388         private final ClassLoader mClassLoader;
    389         private LayoutInflater mInflater;
    390 
    391         public PluginContextWrapper(Context base, ClassLoader classLoader) {
    392             super(base);
    393             mClassLoader = classLoader;
    394         }
    395 
    396         @Override
    397         public ClassLoader getClassLoader() {
    398             return mClassLoader;
    399         }
    400 
    401         @Override
    402         public Object getSystemService(String name) {
    403             if (LAYOUT_INFLATER_SERVICE.equals(name)) {
    404                 if (mInflater == null) {
    405                     mInflater = LayoutInflater.from(getBaseContext()).cloneInContext(this);
    406                 }
    407                 return mInflater;
    408             }
    409             return getBaseContext().getSystemService(name);
    410         }
    411     }
    412 
    413     static class PluginInfo<T> {
    414         private final Context mPluginContext;
    415         private final VersionInfo mVersion;
    416         private String mClass;
    417         T mPlugin;
    418         String mPackage;
    419 
    420         public PluginInfo(String pkg, String cls, T plugin, Context pluginContext,
    421                 VersionInfo info) {
    422             mPlugin = plugin;
    423             mClass = cls;
    424             mPackage = pkg;
    425             mPluginContext = pluginContext;
    426             mVersion = info;
    427         }
    428     }
    429 }
    430