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