1 /*
2  * Copyright (C) 2016 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.systemui.shared.plugins;
18 
19 import android.app.Notification;
20 import android.app.Notification.Action;
21 import android.app.NotificationManager;
22 import android.app.PendingIntent;
23 import android.content.ComponentName;
24 import android.content.Context;
25 import android.content.ContextWrapper;
26 import android.content.Intent;
27 import android.content.pm.ApplicationInfo;
28 import android.content.pm.PackageManager;
29 import android.content.pm.PackageManager.NameNotFoundException;
30 import android.content.pm.ResolveInfo;
31 import android.content.res.Resources;
32 import android.net.Uri;
33 import android.util.ArraySet;
34 import android.util.Log;
35 import android.view.LayoutInflater;
36 
37 import com.android.internal.annotations.VisibleForTesting;
38 import com.android.internal.messages.nano.SystemMessageProto.SystemMessage;
39 import com.android.systemui.plugins.Plugin;
40 import com.android.systemui.plugins.PluginListener;
41 import com.android.systemui.plugins.PluginManager;
42 import com.android.systemui.shared.plugins.VersionInfo.InvalidVersionException;
43 
44 import java.util.ArrayList;
45 import java.util.List;
46 import java.util.concurrent.Executor;
47 
48 /**
49  * Coordinates all the available plugins for a given action.
50  *
51  * The available plugins are queried from the {@link PackageManager} via an an {@link Intent}
52  * action.
53  *
54  * @param <T> The type of plugin that this contains.
55  */
56 public class PluginActionManager<T extends Plugin> {
57 
58     private static final boolean DEBUG = false;
59 
60     private static final String TAG = "PluginActionManager";
61     public static final String PLUGIN_PERMISSION = "com.android.systemui.permission.PLUGIN";
62 
63     private final Context mContext;
64     private final PluginListener<T> mListener;
65     private final String mAction;
66     private final boolean mAllowMultiple;
67     private final NotificationManager mNotificationManager;
68     private final PluginEnabler mPluginEnabler;
69     private final PluginInstance.Factory mPluginInstanceFactory;
70     private final ArraySet<String> mPrivilegedPlugins = new ArraySet<>();
71 
72     @VisibleForTesting
73     private final ArrayList<PluginInstance<T>> mPluginInstances = new ArrayList<>();
74     private final boolean mIsDebuggable;
75     private final PackageManager mPm;
76     private final Class<T> mPluginClass;
77     private final Executor mMainExecutor;
78     private final Executor mBgExecutor;
79 
PluginActionManager( Context context, PackageManager pm, String action, PluginListener<T> listener, Class<T> pluginClass, boolean allowMultiple, Executor mainExecutor, Executor bgExecutor, boolean debuggable, NotificationManager notificationManager, PluginEnabler pluginEnabler, List<String> privilegedPlugins, PluginInstance.Factory pluginInstanceFactory)80     private PluginActionManager(
81             Context context,
82             PackageManager pm,
83             String action,
84             PluginListener<T> listener,
85             Class<T> pluginClass,
86             boolean allowMultiple,
87             Executor mainExecutor,
88             Executor bgExecutor,
89             boolean debuggable,
90             NotificationManager notificationManager,
91             PluginEnabler pluginEnabler,
92             List<String> privilegedPlugins,
93             PluginInstance.Factory pluginInstanceFactory) {
94         mPluginClass = pluginClass;
95         mMainExecutor = mainExecutor;
96         mBgExecutor = bgExecutor;
97         mContext = context;
98         mPm = pm;
99         mAction = action;
100         mListener = listener;
101         mAllowMultiple = allowMultiple;
102         mNotificationManager = notificationManager;
103         mPluginEnabler = pluginEnabler;
104         mPluginInstanceFactory = pluginInstanceFactory;
105         mPrivilegedPlugins.addAll(privilegedPlugins);
106         mIsDebuggable = debuggable;
107     }
108 
109     /** Load all plugins matching this instance's action. */
loadAll()110     public void loadAll() {
111         if (DEBUG) Log.d(TAG, "startListening");
112         mBgExecutor.execute(() -> queryAll());
113     }
114 
115     /** Unload all plugins managed by this instance. */
destroy()116     public void destroy() {
117         if (DEBUG) Log.d(TAG, "stopListening");
118         ArrayList<PluginInstance<T>> plugins = new ArrayList<>(mPluginInstances);
119         for (PluginInstance<T> plugInstance : plugins) {
120             mMainExecutor.execute(() -> onPluginDisconnected(plugInstance));
121         }
122     }
123 
124     /** Unload all matching plugins managed by this instance. */
onPackageRemoved(String pkg)125     public void onPackageRemoved(String pkg) {
126         mBgExecutor.execute(() -> removePkg(pkg));
127     }
128 
129     /** Unload and then reload all matching plugins managed by this instance. */
reloadPackage(String pkg)130     public void reloadPackage(String pkg) {
131         mBgExecutor.execute(() -> {
132             removePkg(pkg);
133             queryPkg(pkg);
134         });
135     }
136 
137     /** Disable a specific plugin managed by this instance. */
checkAndDisable(String className)138     public boolean checkAndDisable(String className) {
139         boolean disableAny = false;
140         ArrayList<PluginInstance<T>> plugins = new ArrayList<>(mPluginInstances);
141         for (PluginInstance<T> info : plugins) {
142             if (className.startsWith(info.getPackage())) {
143                 disableAny |= disable(info, PluginEnabler.DISABLED_FROM_EXPLICIT_CRASH);
144             }
145         }
146         return disableAny;
147     }
148 
149     /** Disable all plugins managed by this instance. */
disableAll()150     public boolean disableAll() {
151         ArrayList<PluginInstance<T>> plugins = new ArrayList<>(mPluginInstances);
152         boolean disabledAny = false;
153         for (int i = 0; i < plugins.size(); i++) {
154             disabledAny |= disable(plugins.get(i), PluginEnabler.DISABLED_FROM_SYSTEM_CRASH);
155         }
156         return disabledAny;
157     }
158 
isPluginPrivileged(ComponentName pluginName)159     boolean isPluginPrivileged(ComponentName pluginName) {
160         for (String componentNameOrPackage : mPrivilegedPlugins) {
161             ComponentName componentName = ComponentName.unflattenFromString(componentNameOrPackage);
162             if (componentName == null) {
163                 if (componentNameOrPackage.equals(pluginName.getPackageName())) {
164                     return true;
165                 }
166             } else {
167                 if (componentName.equals(pluginName)) {
168                     return true;
169                 }
170             }
171         }
172         return false;
173     }
174 
disable( PluginInstance<T> pluginInstance, @PluginEnabler.DisableReason int reason)175     private boolean disable(
176             PluginInstance<T> pluginInstance, @PluginEnabler.DisableReason int reason) {
177         // Live by the sword, die by the sword.
178         // Misbehaving plugins get disabled and won't come back until uninstall/reinstall.
179 
180         ComponentName pluginComponent = pluginInstance.getComponentName();
181         // If a plugin is detected in the stack of a crash then this will be called for that
182         // plugin, if the plugin causing a crash cannot be identified, they are all disabled
183         // assuming one of them must be bad.
184         if (isPluginPrivileged(pluginComponent)) {
185             // Don't disable privileged plugins as they are a part of the OS.
186             return false;
187         }
188         Log.w(TAG, "Disabling plugin " + pluginComponent.flattenToShortString());
189         mPluginEnabler.setDisabled(pluginComponent, reason);
190 
191         return true;
192     }
193 
dependsOn(Plugin p, Class<C> cls)194     <C> boolean dependsOn(Plugin p, Class<C> cls) {
195         ArrayList<PluginInstance<T>> instances = new ArrayList<>(mPluginInstances);
196         for (PluginInstance<T> instance : instances) {
197             if (instance.containsPluginClass(p.getClass())) {
198                 return instance.getVersionInfo() != null && instance.getVersionInfo().hasClass(cls);
199             }
200         }
201         return false;
202     }
203 
204     @Override
toString()205     public String toString() {
206         return String.format("%s@%s (action=%s)",
207                 getClass().getSimpleName(), hashCode(), mAction);
208     }
209 
onPluginConnected(PluginInstance<T> pluginInstance)210     private void onPluginConnected(PluginInstance<T> pluginInstance) {
211         if (DEBUG) Log.d(TAG, "onPluginConnected");
212         PluginPrefs.setHasPlugins(mContext);
213         pluginInstance.onCreate();
214     }
215 
onPluginDisconnected(PluginInstance<T> pluginInstance)216     private void onPluginDisconnected(PluginInstance<T> pluginInstance) {
217         if (DEBUG) Log.d(TAG, "onPluginDisconnected");
218         pluginInstance.onDestroy();
219     }
220 
queryAll()221     private void queryAll() {
222         if (DEBUG) Log.d(TAG, "queryAll " + mAction);
223         for (int i = mPluginInstances.size() - 1; i >= 0; i--) {
224             PluginInstance<T> pluginInstance = mPluginInstances.get(i);
225             mMainExecutor.execute(() -> onPluginDisconnected(pluginInstance));
226         }
227         mPluginInstances.clear();
228         handleQueryPlugins(null);
229     }
230 
removePkg(String pkg)231     private void removePkg(String pkg) {
232         for (int i = mPluginInstances.size() - 1; i >= 0; i--) {
233             final PluginInstance<T> pluginInstance = mPluginInstances.get(i);
234             if (pluginInstance.getPackage().equals(pkg)) {
235                 mMainExecutor.execute(() -> onPluginDisconnected(pluginInstance));
236                 mPluginInstances.remove(i);
237             }
238         }
239     }
240 
queryPkg(String pkg)241     private void queryPkg(String pkg) {
242         if (DEBUG) Log.d(TAG, "queryPkg " + pkg + " " + mAction);
243         if (mAllowMultiple || (mPluginInstances.size() == 0)) {
244             handleQueryPlugins(pkg);
245         } else {
246             if (DEBUG) Log.d(TAG, "Too many of " + mAction);
247         }
248     }
249 
handleQueryPlugins(String pkgName)250     private void handleQueryPlugins(String pkgName) {
251         // This isn't actually a service and shouldn't ever be started, but is
252         // a convenient PM based way to manage our plugins.
253         Intent intent = new Intent(mAction);
254         if (pkgName != null) {
255             intent.setPackage(pkgName);
256         }
257         List<ResolveInfo> result = mPm.queryIntentServices(intent, 0);
258         if (DEBUG) {
259             Log.d(TAG, "Found " + result.size() + " plugins");
260             for (ResolveInfo info : result) {
261                 ComponentName name = new ComponentName(info.serviceInfo.packageName,
262                         info.serviceInfo.name);
263                 Log.d(TAG, "  " + name);
264             }
265         }
266 
267         if (result.size() > 1 && !mAllowMultiple) {
268             // TODO: Show warning.
269             Log.w(TAG, "Multiple plugins found for " + mAction);
270             return;
271         }
272         for (ResolveInfo info : result) {
273             ComponentName name = new ComponentName(info.serviceInfo.packageName,
274                     info.serviceInfo.name);
275             PluginInstance<T> pluginInstance = loadPluginComponent(name);
276             if (pluginInstance != null) {
277                 // add plugin before sending PLUGIN_CONNECTED message
278                 mPluginInstances.add(pluginInstance);
279                 mMainExecutor.execute(() -> onPluginConnected(pluginInstance));
280             }
281         }
282     }
283 
loadPluginComponent(ComponentName component)284     private PluginInstance<T> loadPluginComponent(ComponentName component) {
285         // This was already checked, but do it again here to make extra extra sure, we don't
286         // use these on production builds.
287         if (!mIsDebuggable && !isPluginPrivileged(component)) {
288             // Never ever ever allow these on production builds, they are only for prototyping.
289             Log.w(TAG, "Plugin cannot be loaded on production build: " + component);
290             return null;
291         }
292         if (!mPluginEnabler.isEnabled(component)) {
293             if (DEBUG) {
294                 Log.d(TAG, "Plugin is not enabled, aborting load: " + component);
295             }
296             return null;
297         }
298         String packageName = component.getPackageName();
299         try {
300             // TODO: This probably isn't needed given that we don't have IGNORE_SECURITY on
301             if (mPm.checkPermission(PLUGIN_PERMISSION, packageName)
302                     != PackageManager.PERMISSION_GRANTED) {
303                 Log.d(TAG, "Plugin doesn't have permission: " + packageName);
304                 return null;
305             }
306 
307             ApplicationInfo appInfo = mPm.getApplicationInfo(packageName, 0);
308             // TODO: Only create the plugin before version check if we need it for
309             // legacy version check.
310             if (DEBUG) {
311                 Log.d(TAG, "createPlugin: " + component);
312             }
313             try {
314                 return mPluginInstanceFactory.create(
315                         mContext, appInfo, component,
316                         mPluginClass, mListener);
317             } catch (InvalidVersionException e) {
318                 reportInvalidVersion(component, component.getClassName(), e);
319             }
320         } catch (Throwable e) {
321             Log.w(TAG, "Couldn't load plugin: " + component, e);
322             return null;
323         }
324 
325         return null;
326     }
327 
reportInvalidVersion( ComponentName component, String className, InvalidVersionException e)328     private void reportInvalidVersion(
329             ComponentName component, String className, InvalidVersionException e) {
330         final int icon = Resources.getSystem().getIdentifier(
331                 "stat_sys_warning", "drawable", "android");
332         final int color = Resources.getSystem().getIdentifier(
333                 "system_notification_accent_color", "color", "android");
334         final Notification.Builder nb = new Notification.Builder(mContext,
335                 PluginManager.NOTIFICATION_CHANNEL_ID)
336                 .setStyle(new Notification.BigTextStyle())
337                 .setSmallIcon(icon)
338                 .setWhen(0)
339                 .setShowWhen(false)
340                 .setVisibility(Notification.VISIBILITY_PUBLIC)
341                 .setColor(mContext.getColor(color));
342         String label = className;
343         try {
344             label = mPm.getServiceInfo(component, 0).loadLabel(mPm).toString();
345         } catch (NameNotFoundException e2) {
346             // no-op
347         }
348         if (!e.isTooNew()) {
349             // Localization not required as this will never ever appear in a user build.
350             nb.setContentTitle("Plugin \"" + label + "\" is too old")
351                     .setContentText("Contact plugin developer to get an updated"
352                             + " version.\n" + e.getMessage());
353         } else {
354             // Localization not required as this will never ever appear in a user build.
355             nb.setContentTitle("Plugin \"" + label + "\" is too new")
356                     .setContentText("Check to see if an OTA is available.\n"
357                             + e.getMessage());
358         }
359         Intent i = new Intent(PluginManagerImpl.DISABLE_PLUGIN).setData(
360                 Uri.parse("package://" + component.flattenToString()));
361         PendingIntent pi = PendingIntent.getBroadcast(mContext, 0, i,
362                 PendingIntent.FLAG_IMMUTABLE);
363         nb.addAction(new Action.Builder(null, "Disable plugin", pi).build());
364         mNotificationManager.notify(SystemMessage.NOTE_PLUGIN, nb.build());
365         // TODO: Warn user.
366         Log.w(TAG, "Error loading plugin; " + e.getMessage());
367     }
368 
369     /**
370      * Construct a {@link PluginActionManager}
371      */
372     public static class Factory {
373         private final Context mContext;
374         private final PackageManager mPackageManager;
375         private final Executor mMainExecutor;
376         private final Executor mBgExecutor;
377         private final NotificationManager mNotificationManager;
378         private final PluginEnabler mPluginEnabler;
379         private final List<String> mPrivilegedPlugins;
380         private final PluginInstance.Factory mPluginInstanceFactory;
381 
Factory(Context context, PackageManager packageManager, Executor mainExecutor, Executor bgExecutor, NotificationManager notificationManager, PluginEnabler pluginEnabler, List<String> privilegedPlugins, PluginInstance.Factory pluginInstanceFactory)382         public Factory(Context context, PackageManager packageManager,
383                 Executor mainExecutor, Executor bgExecutor,
384                 NotificationManager notificationManager, PluginEnabler pluginEnabler,
385                 List<String> privilegedPlugins, PluginInstance.Factory pluginInstanceFactory) {
386             mContext = context;
387             mPackageManager = packageManager;
388             mMainExecutor = mainExecutor;
389             mBgExecutor = bgExecutor;
390             mNotificationManager = notificationManager;
391             mPluginEnabler = pluginEnabler;
392             mPrivilegedPlugins = privilegedPlugins;
393             mPluginInstanceFactory = pluginInstanceFactory;
394         }
395 
create( String action, PluginListener<T> listener, Class<T> pluginClass, boolean allowMultiple, boolean debuggable)396         <T extends Plugin> PluginActionManager<T> create(
397                 String action, PluginListener<T> listener, Class<T> pluginClass,
398                 boolean allowMultiple, boolean debuggable) {
399             return new PluginActionManager<>(mContext, mPackageManager, action, listener,
400                     pluginClass, allowMultiple, mMainExecutor, mBgExecutor,
401                     debuggable, mNotificationManager, mPluginEnabler,
402                     mPrivilegedPlugins, mPluginInstanceFactory);
403         }
404     }
405 
406     /** */
407     public static class PluginContextWrapper extends ContextWrapper {
408         private final ClassLoader mClassLoader;
409         private LayoutInflater mInflater;
410 
PluginContextWrapper(Context base, ClassLoader classLoader)411         public PluginContextWrapper(Context base, ClassLoader classLoader) {
412             super(base);
413             mClassLoader = classLoader;
414         }
415 
416         @Override
getClassLoader()417         public ClassLoader getClassLoader() {
418             return mClassLoader;
419         }
420 
421         @Override
getSystemService(String name)422         public Object getSystemService(String name) {
423             if (LAYOUT_INFLATER_SERVICE.equals(name)) {
424                 if (mInflater == null) {
425                     mInflater = LayoutInflater.from(getBaseContext()).cloneInContext(this);
426                 }
427                 return mInflater;
428             }
429             return getBaseContext().getSystemService(name);
430         }
431     }
432 
433 }
434