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