• 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.BroadcastReceiver;
22 import android.content.ComponentName;
23 import android.content.Context;
24 import android.content.Intent;
25 import android.content.IntentFilter;
26 import android.content.pm.ApplicationInfo;
27 import android.content.pm.PackageManager;
28 import android.content.pm.PackageManager.NameNotFoundException;
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.SystemProperties;
35 import android.os.UserHandle;
36 import android.text.TextUtils;
37 import android.util.ArrayMap;
38 import android.util.ArraySet;
39 import android.util.Log;
40 import android.util.Log.TerribleFailure;
41 import android.util.Log.TerribleFailureHandler;
42 import android.widget.Toast;
43 
44 import com.android.internal.annotations.VisibleForTesting;
45 import com.android.internal.messages.nano.SystemMessageProto.SystemMessage;
46 import com.android.systemui.Dependency;
47 import com.android.systemui.plugins.PluginInstanceManager.PluginContextWrapper;
48 import com.android.systemui.plugins.PluginInstanceManager.PluginInfo;
49 import com.android.systemui.plugins.annotations.ProvidesInterface;
50 
51 import dalvik.system.PathClassLoader;
52 
53 import java.io.FileDescriptor;
54 import java.io.PrintWriter;
55 import java.lang.Thread.UncaughtExceptionHandler;
56 import java.util.Map;
57 
58 /**
59  * @see Plugin
60  */
61 public class PluginManagerImpl extends BroadcastReceiver implements PluginManager {
62 
63     static final String DISABLE_PLUGIN = "com.android.systemui.action.DISABLE_PLUGIN";
64 
65     private static PluginManager sInstance;
66 
67     private final ArrayMap<PluginListener<?>, PluginInstanceManager> mPluginMap
68             = new ArrayMap<>();
69     private final Map<String, ClassLoader> mClassLoaders = new ArrayMap<>();
70     private final ArraySet<String> mOneShotPackages = new ArraySet<>();
71     private final Context mContext;
72     private final PluginInstanceManagerFactory mFactory;
73     private final boolean isDebuggable;
74     private final PluginPrefs mPluginPrefs;
75     private ClassLoaderFilter mParentClassLoader;
76     private boolean mListening;
77     private boolean mHasOneShot;
78     private Looper mLooper;
79     private boolean mWtfsSet;
80 
PluginManagerImpl(Context context)81     public PluginManagerImpl(Context context) {
82         this(context, new PluginInstanceManagerFactory(),
83                 Build.IS_DEBUGGABLE, Thread.getUncaughtExceptionPreHandler());
84     }
85 
86     @VisibleForTesting
PluginManagerImpl(Context context, PluginInstanceManagerFactory factory, boolean debuggable, UncaughtExceptionHandler defaultHandler)87     PluginManagerImpl(Context context, PluginInstanceManagerFactory factory, boolean debuggable,
88             UncaughtExceptionHandler defaultHandler) {
89         mContext = context;
90         mFactory = factory;
91         mLooper = Dependency.get(Dependency.BG_LOOPER);
92         isDebuggable = debuggable;
93         mPluginPrefs = new PluginPrefs(mContext);
94 
95         PluginExceptionHandler uncaughtExceptionHandler = new PluginExceptionHandler(
96                 defaultHandler);
97         Thread.setUncaughtExceptionPreHandler(uncaughtExceptionHandler);
98         if (isDebuggable) {
99             new Handler(mLooper).post(() -> {
100                 // Plugin dependencies that don't have another good home can go here, but
101                 // dependencies that have better places to init can happen elsewhere.
102                 Dependency.get(PluginDependencyProvider.class)
103                         .allowPluginDependency(ActivityStarter.class);
104             });
105         }
106     }
107 
getOneShotPlugin(Class<T> cls)108     public <T extends Plugin> T getOneShotPlugin(Class<T> cls) {
109         ProvidesInterface info = cls.getDeclaredAnnotation(ProvidesInterface.class);
110         if (info == null) {
111             throw new RuntimeException(cls + " doesn't provide an interface");
112         }
113         if (TextUtils.isEmpty(info.action())) {
114             throw new RuntimeException(cls + " doesn't provide an action");
115         }
116         return getOneShotPlugin(info.action(), cls);
117     }
118 
getOneShotPlugin(String action, Class<?> cls)119     public <T extends Plugin> T getOneShotPlugin(String action, Class<?> cls) {
120         if (!isDebuggable) {
121             // Never ever ever allow these on production builds, they are only for prototyping.
122             return null;
123         }
124         if (Looper.myLooper() != Looper.getMainLooper()) {
125             throw new RuntimeException("Must be called from UI thread");
126         }
127         PluginInstanceManager<T> p = mFactory.createPluginInstanceManager(mContext, action, null,
128                 false, mLooper, cls, this);
129         mPluginPrefs.addAction(action);
130         PluginInfo<T> info = p.getPlugin();
131         if (info != null) {
132             mOneShotPackages.add(info.mPackage);
133             mHasOneShot = true;
134             startListening();
135             return info.mPlugin;
136         }
137         return null;
138     }
139 
addPluginListener(PluginListener<T> listener, Class<?> cls)140     public <T extends Plugin> void addPluginListener(PluginListener<T> listener, Class<?> cls) {
141         addPluginListener(listener, cls, false);
142     }
143 
addPluginListener(PluginListener<T> listener, Class<?> cls, boolean allowMultiple)144     public <T extends Plugin> void addPluginListener(PluginListener<T> listener, Class<?> cls,
145             boolean allowMultiple) {
146         addPluginListener(PluginManager.getAction(cls), listener, cls, allowMultiple);
147     }
148 
addPluginListener(String action, PluginListener<T> listener, Class<?> cls)149     public <T extends Plugin> void addPluginListener(String action, PluginListener<T> listener,
150             Class<?> cls) {
151         addPluginListener(action, listener, cls, false);
152     }
153 
addPluginListener(String action, PluginListener<T> listener, Class cls, boolean allowMultiple)154     public <T extends Plugin> void addPluginListener(String action, PluginListener<T> listener,
155             Class cls, boolean allowMultiple) {
156         if (!isDebuggable) {
157             // Never ever ever allow these on production builds, they are only for prototyping.
158             return;
159         }
160         mPluginPrefs.addAction(action);
161         PluginInstanceManager p = mFactory.createPluginInstanceManager(mContext, action, listener,
162                 allowMultiple, mLooper, cls, this);
163         p.loadAll();
164         mPluginMap.put(listener, p);
165         startListening();
166     }
167 
removePluginListener(PluginListener<?> listener)168     public void removePluginListener(PluginListener<?> listener) {
169         if (!isDebuggable) {
170             // Never ever ever allow these on production builds, they are only for prototyping.
171             return;
172         }
173         if (!mPluginMap.containsKey(listener)) return;
174         mPluginMap.remove(listener).destroy();
175         if (mPluginMap.size() == 0) {
176             stopListening();
177         }
178     }
179 
startListening()180     private void startListening() {
181         if (mListening) return;
182         mListening = true;
183         IntentFilter filter = new IntentFilter(Intent.ACTION_PACKAGE_ADDED);
184         filter.addAction(Intent.ACTION_PACKAGE_CHANGED);
185         filter.addAction(Intent.ACTION_PACKAGE_REMOVED);
186         filter.addAction(PLUGIN_CHANGED);
187         filter.addAction(DISABLE_PLUGIN);
188         filter.addDataScheme("package");
189         mContext.registerReceiver(this, filter);
190         filter = new IntentFilter(Intent.ACTION_USER_UNLOCKED);
191         mContext.registerReceiver(this, filter);
192     }
193 
stopListening()194     private void stopListening() {
195         // Never stop listening if a one-shot is present.
196         if (!mListening || mHasOneShot) return;
197         mListening = false;
198         mContext.unregisterReceiver(this);
199     }
200 
201     @Override
onReceive(Context context, Intent intent)202     public void onReceive(Context context, Intent intent) {
203         if (Intent.ACTION_USER_UNLOCKED.equals(intent.getAction())) {
204             for (PluginInstanceManager manager : mPluginMap.values()) {
205                 manager.loadAll();
206             }
207         } else if (DISABLE_PLUGIN.equals(intent.getAction())) {
208             Uri uri = intent.getData();
209             ComponentName component = ComponentName.unflattenFromString(
210                     uri.toString().substring(10));
211             mContext.getPackageManager().setComponentEnabledSetting(component,
212                     PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
213                     PackageManager.DONT_KILL_APP);
214             mContext.getSystemService(NotificationManager.class).cancel(component.getClassName(),
215                     SystemMessage.NOTE_PLUGIN);
216         } else {
217             Uri data = intent.getData();
218             String pkg = data.getEncodedSchemeSpecificPart();
219             if (mOneShotPackages.contains(pkg)) {
220                 int icon = mContext.getResources().getIdentifier("tuner", "drawable",
221                         mContext.getPackageName());
222                 int color = Resources.getSystem().getIdentifier(
223                         "system_notification_accent_color", "color", "android");
224                 String label = pkg;
225                 try {
226                     PackageManager pm = mContext.getPackageManager();
227                     label = pm.getApplicationInfo(pkg, 0).loadLabel(pm).toString();
228                 } catch (NameNotFoundException e) {
229                 }
230                 // Localization not required as this will never ever appear in a user build.
231                 final Notification.Builder nb =
232                         new Notification.Builder(mContext, NOTIFICATION_CHANNEL_ID)
233                                 .setSmallIcon(icon)
234                                 .setWhen(0)
235                                 .setShowWhen(false)
236                                 .setPriority(Notification.PRIORITY_MAX)
237                                 .setVisibility(Notification.VISIBILITY_PUBLIC)
238                                 .setColor(mContext.getColor(color))
239                                 .setContentTitle("Plugin \"" + label + "\" has updated")
240                                 .setContentText("Restart SysUI for changes to take effect.");
241                 Intent i = new Intent("com.android.systemui.action.RESTART").setData(
242                             Uri.parse("package://" + pkg));
243                 PendingIntent pi = PendingIntent.getBroadcast(mContext, 0, i, 0);
244                 nb.addAction(new Action.Builder(null, "Restart SysUI", pi).build());
245                 mContext.getSystemService(NotificationManager.class).notifyAsUser(pkg,
246                         SystemMessage.NOTE_PLUGIN, nb.build(), UserHandle.ALL);
247             }
248             if (clearClassLoader(pkg)) {
249                 Toast.makeText(mContext, "Reloading " + pkg, Toast.LENGTH_LONG).show();
250             }
251             if (!Intent.ACTION_PACKAGE_REMOVED.equals(intent.getAction())) {
252                 for (PluginInstanceManager manager : mPluginMap.values()) {
253                     manager.onPackageChange(pkg);
254                 }
255             } else {
256                 for (PluginInstanceManager manager : mPluginMap.values()) {
257                     manager.onPackageRemoved(pkg);
258                 }
259             }
260         }
261     }
262 
getClassLoader(String sourceDir, String pkg)263     public ClassLoader getClassLoader(String sourceDir, String pkg) {
264         if (mClassLoaders.containsKey(pkg)) {
265             return mClassLoaders.get(pkg);
266         }
267         ClassLoader classLoader = new PathClassLoader(sourceDir, getParentClassLoader());
268         mClassLoaders.put(pkg, classLoader);
269         return classLoader;
270     }
271 
clearClassLoader(String pkg)272     private boolean clearClassLoader(String pkg) {
273         return mClassLoaders.remove(pkg) != null;
274     }
275 
getParentClassLoader()276     ClassLoader getParentClassLoader() {
277         if (mParentClassLoader == null) {
278             // Lazily load this so it doesn't have any effect on devices without plugins.
279             mParentClassLoader = new ClassLoaderFilter(getClass().getClassLoader(),
280                     "com.android.systemui.plugin");
281         }
282         return mParentClassLoader;
283     }
284 
getContext(ApplicationInfo info, String pkg)285     public Context getContext(ApplicationInfo info, String pkg) throws NameNotFoundException {
286         ClassLoader classLoader = getClassLoader(info.sourceDir, pkg);
287         return new PluginContextWrapper(mContext.createApplicationContext(info, 0), classLoader);
288     }
289 
dependsOn(Plugin p, Class<T> cls)290     public <T> boolean dependsOn(Plugin p, Class<T> cls) {
291         for (int i = 0; i < mPluginMap.size(); i++) {
292             if (mPluginMap.valueAt(i).dependsOn(p, cls)) {
293                 return true;
294             }
295         }
296         return false;
297     }
298 
handleWtfs()299     public void handleWtfs() {
300         if (!mWtfsSet) {
301             mWtfsSet = true;
302             Log.setWtfHandler((tag, what, system) -> {
303                 throw new CrashWhilePluginActiveException(what);
304             });
305         }
306     }
307 
dump(FileDescriptor fd, PrintWriter pw, String[] args)308     public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
309         pw.println(String.format("  plugin map (%d):", mPluginMap.size()));
310         for (PluginListener listener: mPluginMap.keySet()) {
311             pw.println(String.format("    %s -> %s",
312                     listener, mPluginMap.get(listener)));
313         }
314     }
315 
316     @VisibleForTesting
317     public static class PluginInstanceManagerFactory {
createPluginInstanceManager(Context context, String action, PluginListener<T> listener, boolean allowMultiple, Looper looper, Class<?> cls, PluginManagerImpl manager)318         public <T extends Plugin> PluginInstanceManager createPluginInstanceManager(Context context,
319                 String action, PluginListener<T> listener, boolean allowMultiple, Looper looper,
320                 Class<?> cls, PluginManagerImpl manager) {
321             return new PluginInstanceManager(context, action, listener, allowMultiple, looper,
322                     new VersionInfo().addClass(cls), manager);
323         }
324     }
325 
326     // This allows plugins to include any libraries or copied code they want by only including
327     // classes from the plugin library.
328     private static class ClassLoaderFilter extends ClassLoader {
329         private final String mPackage;
330         private final ClassLoader mBase;
331 
ClassLoaderFilter(ClassLoader base, String pkg)332         public ClassLoaderFilter(ClassLoader base, String pkg) {
333             super(ClassLoader.getSystemClassLoader());
334             mBase = base;
335             mPackage = pkg;
336         }
337 
338         @Override
loadClass(String name, boolean resolve)339         protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
340             if (!name.startsWith(mPackage)) super.loadClass(name, resolve);
341             return mBase.loadClass(name);
342         }
343     }
344 
345     private class PluginExceptionHandler implements UncaughtExceptionHandler {
346         private final UncaughtExceptionHandler mHandler;
347 
PluginExceptionHandler(UncaughtExceptionHandler handler)348         private PluginExceptionHandler(UncaughtExceptionHandler handler) {
349             mHandler = handler;
350         }
351 
352         @Override
uncaughtException(Thread thread, Throwable throwable)353         public void uncaughtException(Thread thread, Throwable throwable) {
354             if (SystemProperties.getBoolean("plugin.debugging", false)) {
355                 mHandler.uncaughtException(thread, throwable);
356                 return;
357             }
358             // Search for and disable plugins that may have been involved in this crash.
359             boolean disabledAny = checkStack(throwable);
360             if (!disabledAny) {
361                 // We couldn't find any plugins involved in this crash, just to be safe
362                 // disable all the plugins, so we can be sure that SysUI is running as
363                 // best as possible.
364                 for (PluginInstanceManager manager : mPluginMap.values()) {
365                     disabledAny |= manager.disableAll();
366                 }
367             }
368             if (disabledAny) {
369                 throwable = new CrashWhilePluginActiveException(throwable);
370             }
371 
372             // Run the normal exception handler so we can crash and cleanup our state.
373             mHandler.uncaughtException(thread, throwable);
374         }
375 
checkStack(Throwable throwable)376         private boolean checkStack(Throwable throwable) {
377             if (throwable == null) return false;
378             boolean disabledAny = false;
379             for (StackTraceElement element : throwable.getStackTrace()) {
380                 for (PluginInstanceManager manager : mPluginMap.values()) {
381                     disabledAny |= manager.checkAndDisable(element.getClassName());
382                 }
383             }
384             return disabledAny | checkStack(throwable.getCause());
385         }
386     }
387 
388     private class CrashWhilePluginActiveException extends RuntimeException {
CrashWhilePluginActiveException(Throwable throwable)389         public CrashWhilePluginActiveException(Throwable throwable) {
390             super(throwable);
391         }
392     }
393 }
394