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