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