• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
PluginInstanceManager(Context context, String action, PluginListener<T> listener, boolean allowMultiple, Looper looper, VersionInfo version, PluginManagerImpl manager)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
PluginInstanceManager(Context context, PackageManager pm, String action, PluginListener<T> listener, boolean allowMultiple, Looper looper, VersionInfo version, PluginManagerImpl manager, boolean debuggable)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 
getPlugin()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 
loadAll()104     public void loadAll() {
105         if (DEBUG) Log.d(TAG, "startListening");
106         mPluginHandler.sendEmptyMessage(PluginHandler.QUERY_ALL);
107     }
108 
destroy()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 
onPackageRemoved(String pkg)118     public void onPackageRemoved(String pkg) {
119         mPluginHandler.obtainMessage(PluginHandler.REMOVE_PKG, pkg).sendToTarget();
120     }
121 
onPackageChange(String pkg)122     public void onPackageChange(String pkg) {
123         mPluginHandler.obtainMessage(PluginHandler.REMOVE_PKG, pkg).sendToTarget();
124         mPluginHandler.obtainMessage(PluginHandler.QUERY_PKG, pkg).sendToTarget();
125     }
126 
checkAndDisable(String className)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 
disableAll()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 
disable(PluginInfo info)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 
dependsOn(Plugin p, Class<T> cls)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 
MainHandler(Looper looper)175         public MainHandler(Looper looper) {
176             super(looper);
177         }
178 
179         @Override
handleMessage(Message msg)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 
PluginHandler(Looper looper)217         public PluginHandler(Looper looper) {
218             super(looper);
219         }
220 
221         @Override
handleMessage(Message msg)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 
handleQueryPlugins(String pkgName)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 
handleLoadPlugin(ComponentName component)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 
checkVersion(Class<?> pluginClass, T plugin, VersionInfo version)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 
PluginContextWrapper(Context base, ClassLoader classLoader)385         public PluginContextWrapper(Context base, ClassLoader classLoader) {
386             super(base);
387             mClassLoader = classLoader;
388         }
389 
390         @Override
getClassLoader()391         public ClassLoader getClassLoader() {
392             return mClassLoader;
393         }
394 
395         @Override
getSystemService(String name)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 
PluginInfo(String pkg, String cls, T plugin, Context pluginContext, VersionInfo info)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