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