• 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     @Override
toString()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 
MainHandler(Looper looper)181         public MainHandler(Looper looper) {
182             super(looper);
183         }
184 
185         @Override
handleMessage(Message msg)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 
PluginHandler(Looper looper)223         public PluginHandler(Looper looper) {
224             super(looper);
225         }
226 
227         @Override
handleMessage(Message msg)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 
handleQueryPlugins(String pkgName)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 
handleLoadPlugin(ComponentName component)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 
checkVersion(Class<?> pluginClass, T plugin, VersionInfo version)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 
PluginContextWrapper(Context base, ClassLoader classLoader)391         public PluginContextWrapper(Context base, ClassLoader classLoader) {
392             super(base);
393             mClassLoader = classLoader;
394         }
395 
396         @Override
getClassLoader()397         public ClassLoader getClassLoader() {
398             return mClassLoader;
399         }
400 
401         @Override
getSystemService(String name)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 
PluginInfo(String pkg, String cls, T plugin, Context pluginContext, VersionInfo info)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