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.shared.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.ComponentName; 22 import android.content.Context; 23 import android.content.ContextWrapper; 24 import android.content.Intent; 25 import android.content.pm.ApplicationInfo; 26 import android.content.pm.PackageManager; 27 import android.content.pm.PackageManager.NameNotFoundException; 28 import android.content.pm.ResolveInfo; 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.Message; 35 import android.os.UserHandle; 36 import android.util.ArraySet; 37 import android.util.Log; 38 import android.view.LayoutInflater; 39 40 import com.android.internal.annotations.VisibleForTesting; 41 import com.android.internal.messages.nano.SystemMessageProto.SystemMessage; 42 import com.android.systemui.plugins.Plugin; 43 import com.android.systemui.plugins.PluginFragment; 44 import com.android.systemui.plugins.PluginListener; 45 import com.android.systemui.shared.plugins.VersionInfo.InvalidVersionException; 46 47 import java.util.ArrayList; 48 import java.util.Arrays; 49 import java.util.List; 50 51 public class PluginInstanceManager<T extends Plugin> { 52 53 private static final boolean DEBUG = false; 54 55 private static final String TAG = "PluginInstanceManager"; 56 public static final String PLUGIN_PERMISSION = "com.android.systemui.permission.PLUGIN"; 57 58 private final Context mContext; 59 private final PluginListener<T> mListener; 60 private final String mAction; 61 private final boolean mAllowMultiple; 62 private final VersionInfo mVersion; 63 64 @VisibleForTesting 65 final MainHandler mMainHandler; 66 @VisibleForTesting 67 final PluginHandler mPluginHandler; 68 private final boolean isDebuggable; 69 private final PackageManager mPm; 70 private final PluginManagerImpl mManager; 71 private final ArraySet<String> mWhitelistedPlugins = new ArraySet<>(); 72 PluginInstanceManager(Context context, String action, PluginListener<T> listener, boolean allowMultiple, Looper looper, VersionInfo version, PluginManagerImpl manager)73 PluginInstanceManager(Context context, String action, PluginListener<T> listener, 74 boolean allowMultiple, Looper looper, VersionInfo version, PluginManagerImpl manager) { 75 this(context, context.getPackageManager(), action, listener, allowMultiple, looper, version, 76 manager, Build.IS_DEBUGGABLE, manager.getWhitelistedPlugins()); 77 } 78 79 @VisibleForTesting PluginInstanceManager(Context context, PackageManager pm, String action, PluginListener<T> listener, boolean allowMultiple, Looper looper, VersionInfo version, PluginManagerImpl manager, boolean debuggable, String[] pluginWhitelist)80 PluginInstanceManager(Context context, PackageManager pm, String action, 81 PluginListener<T> listener, boolean allowMultiple, Looper looper, VersionInfo version, 82 PluginManagerImpl manager, boolean debuggable, String[] pluginWhitelist) { 83 mMainHandler = new MainHandler(Looper.getMainLooper()); 84 mPluginHandler = new PluginHandler(looper); 85 mManager = manager; 86 mContext = context; 87 mPm = pm; 88 mAction = action; 89 mListener = listener; 90 mAllowMultiple = allowMultiple; 91 mVersion = version; 92 mWhitelistedPlugins.addAll(Arrays.asList(pluginWhitelist)); 93 isDebuggable = debuggable; 94 } 95 getPlugin()96 public PluginInfo<T> getPlugin() { 97 if (Looper.myLooper() != Looper.getMainLooper()) { 98 throw new RuntimeException("Must be called from UI thread"); 99 } 100 mPluginHandler.handleQueryPlugins(null /* All packages */); 101 if (mPluginHandler.mPlugins.size() > 0) { 102 mMainHandler.removeMessages(MainHandler.PLUGIN_CONNECTED); 103 PluginInfo<T> info = mPluginHandler.mPlugins.get(0); 104 PluginPrefs.setHasPlugins(mContext); 105 info.mPlugin.onCreate(mContext, info.mPluginContext); 106 return info; 107 } 108 return null; 109 } 110 loadAll()111 public void loadAll() { 112 if (DEBUG) Log.d(TAG, "startListening"); 113 mPluginHandler.sendEmptyMessage(PluginHandler.QUERY_ALL); 114 } 115 destroy()116 public void destroy() { 117 if (DEBUG) Log.d(TAG, "stopListening"); 118 ArrayList<PluginInfo> plugins = new ArrayList<PluginInfo>(mPluginHandler.mPlugins); 119 for (PluginInfo plugin : plugins) { 120 mMainHandler.obtainMessage(MainHandler.PLUGIN_DISCONNECTED, 121 plugin.mPlugin).sendToTarget(); 122 } 123 } 124 onPackageRemoved(String pkg)125 public void onPackageRemoved(String pkg) { 126 mPluginHandler.obtainMessage(PluginHandler.REMOVE_PKG, pkg).sendToTarget(); 127 } 128 onPackageChange(String pkg)129 public void onPackageChange(String pkg) { 130 mPluginHandler.obtainMessage(PluginHandler.REMOVE_PKG, pkg).sendToTarget(); 131 mPluginHandler.obtainMessage(PluginHandler.QUERY_PKG, pkg).sendToTarget(); 132 } 133 checkAndDisable(String className)134 public boolean checkAndDisable(String className) { 135 boolean disableAny = false; 136 ArrayList<PluginInfo> plugins = new ArrayList<PluginInfo>(mPluginHandler.mPlugins); 137 for (PluginInfo info : plugins) { 138 if (className.startsWith(info.mPackage)) { 139 disable(info, PluginEnabler.DISABLED_FROM_EXPLICIT_CRASH); 140 disableAny = true; 141 } 142 } 143 return disableAny; 144 } 145 disableAll()146 public boolean disableAll() { 147 ArrayList<PluginInfo> plugins = new ArrayList<PluginInfo>(mPluginHandler.mPlugins); 148 for (int i = 0; i < plugins.size(); i++) { 149 disable(plugins.get(i), PluginEnabler.DISABLED_FROM_SYSTEM_CRASH); 150 } 151 return plugins.size() != 0; 152 } 153 isPluginWhitelisted(ComponentName pluginName)154 private boolean isPluginWhitelisted(ComponentName pluginName) { 155 for (String componentNameOrPackage : mWhitelistedPlugins) { 156 ComponentName componentName = ComponentName.unflattenFromString(componentNameOrPackage); 157 if (componentName == null) { 158 if (componentNameOrPackage.equals(pluginName.getPackageName())) { 159 return true; 160 } 161 } else { 162 if (componentName.equals(pluginName)) { 163 return true; 164 } 165 } 166 } 167 return false; 168 } 169 disable(PluginInfo info, @PluginEnabler.DisableReason int reason)170 private void disable(PluginInfo info, @PluginEnabler.DisableReason int reason) { 171 // Live by the sword, die by the sword. 172 // Misbehaving plugins get disabled and won't come back until uninstall/reinstall. 173 174 ComponentName pluginComponent = new ComponentName(info.mPackage, info.mClass); 175 // If a plugin is detected in the stack of a crash then this will be called for that 176 // plugin, if the plugin causing a crash cannot be identified, they are all disabled 177 // assuming one of them must be bad. 178 if (isPluginWhitelisted(pluginComponent)) { 179 // Don't disable whitelisted plugins as they are a part of the OS. 180 return; 181 } 182 Log.w(TAG, "Disabling plugin " + pluginComponent.flattenToShortString()); 183 mManager.getPluginEnabler().setDisabled(pluginComponent, reason); 184 } 185 dependsOn(Plugin p, Class<T> cls)186 public <T> boolean dependsOn(Plugin p, Class<T> cls) { 187 ArrayList<PluginInfo> plugins = new ArrayList<PluginInfo>(mPluginHandler.mPlugins); 188 for (PluginInfo info : plugins) { 189 if (info.mPlugin.getClass().getName().equals(p.getClass().getName())) { 190 return info.mVersion != null && info.mVersion.hasClass(cls); 191 } 192 } 193 return false; 194 } 195 196 @Override toString()197 public String toString() { 198 return String.format("%s@%s (action=%s)", 199 getClass().getSimpleName(), hashCode(), mAction); 200 } 201 202 private class MainHandler extends Handler { 203 private static final int PLUGIN_CONNECTED = 1; 204 private static final int PLUGIN_DISCONNECTED = 2; 205 MainHandler(Looper looper)206 public MainHandler(Looper looper) { 207 super(looper); 208 } 209 210 @Override handleMessage(Message msg)211 public void handleMessage(Message msg) { 212 switch (msg.what) { 213 case PLUGIN_CONNECTED: 214 if (DEBUG) Log.d(TAG, "onPluginConnected"); 215 PluginPrefs.setHasPlugins(mContext); 216 PluginInfo<T> info = (PluginInfo<T>) msg.obj; 217 mManager.handleWtfs(); 218 if (!(msg.obj instanceof PluginFragment)) { 219 // Only call onDestroy for plugins that aren't fragments, as fragments 220 // will get the onCreate as part of the fragment lifecycle. 221 info.mPlugin.onCreate(mContext, info.mPluginContext); 222 } 223 mListener.onPluginConnected(info.mPlugin, info.mPluginContext); 224 break; 225 case PLUGIN_DISCONNECTED: 226 if (DEBUG) Log.d(TAG, "onPluginDisconnected"); 227 mListener.onPluginDisconnected((T) msg.obj); 228 if (!(msg.obj instanceof PluginFragment)) { 229 // Only call onDestroy for plugins that aren't fragments, as fragments 230 // will get the onDestroy as part of the fragment lifecycle. 231 ((T) msg.obj).onDestroy(); 232 } 233 break; 234 default: 235 super.handleMessage(msg); 236 break; 237 } 238 } 239 } 240 241 private class PluginHandler extends Handler { 242 private static final int QUERY_ALL = 1; 243 private static final int QUERY_PKG = 2; 244 private static final int REMOVE_PKG = 3; 245 246 private final ArrayList<PluginInfo<T>> mPlugins = new ArrayList<>(); 247 PluginHandler(Looper looper)248 public PluginHandler(Looper looper) { 249 super(looper); 250 } 251 252 @Override handleMessage(Message msg)253 public void handleMessage(Message msg) { 254 switch (msg.what) { 255 case QUERY_ALL: 256 if (DEBUG) Log.d(TAG, "queryAll " + mAction); 257 for (int i = mPlugins.size() - 1; i >= 0; i--) { 258 PluginInfo<T> plugin = mPlugins.get(i); 259 mListener.onPluginDisconnected(plugin.mPlugin); 260 if (!(plugin.mPlugin instanceof PluginFragment)) { 261 // Only call onDestroy for plugins that aren't fragments, as fragments 262 // will get the onDestroy as part of the fragment lifecycle. 263 plugin.mPlugin.onDestroy(); 264 } 265 } 266 mPlugins.clear(); 267 handleQueryPlugins(null); 268 break; 269 case REMOVE_PKG: 270 String pkg = (String) msg.obj; 271 for (int i = mPlugins.size() - 1; i >= 0; i--) { 272 final PluginInfo<T> plugin = mPlugins.get(i); 273 if (plugin.mPackage.equals(pkg)) { 274 mMainHandler.obtainMessage(MainHandler.PLUGIN_DISCONNECTED, 275 plugin.mPlugin).sendToTarget(); 276 mPlugins.remove(i); 277 } 278 } 279 break; 280 case QUERY_PKG: 281 String p = (String) msg.obj; 282 if (DEBUG) Log.d(TAG, "queryPkg " + p + " " + mAction); 283 if (mAllowMultiple || (mPlugins.size() == 0)) { 284 handleQueryPlugins(p); 285 } else { 286 if (DEBUG) Log.d(TAG, "Too many of " + mAction); 287 } 288 break; 289 default: 290 super.handleMessage(msg); 291 } 292 } 293 handleQueryPlugins(String pkgName)294 private void handleQueryPlugins(String pkgName) { 295 // This isn't actually a service and shouldn't ever be started, but is 296 // a convenient PM based way to manage our plugins. 297 Intent intent = new Intent(mAction); 298 if (pkgName != null) { 299 intent.setPackage(pkgName); 300 } 301 List<ResolveInfo> result = mPm.queryIntentServices(intent, 0); 302 if (DEBUG) Log.d(TAG, "Found " + result.size() + " plugins"); 303 if (result.size() > 1 && !mAllowMultiple) { 304 // TODO: Show warning. 305 Log.w(TAG, "Multiple plugins found for " + mAction); 306 if (DEBUG) { 307 for (ResolveInfo info : result) { 308 ComponentName name = new ComponentName(info.serviceInfo.packageName, 309 info.serviceInfo.name); 310 Log.w(TAG, " " + name); 311 } 312 } 313 return; 314 } 315 for (ResolveInfo info : result) { 316 ComponentName name = new ComponentName(info.serviceInfo.packageName, 317 info.serviceInfo.name); 318 PluginInfo<T> t = handleLoadPlugin(name); 319 if (t == null) continue; 320 321 // add plugin before sending PLUGIN_CONNECTED message 322 mPlugins.add(t); 323 mMainHandler.obtainMessage(mMainHandler.PLUGIN_CONNECTED, t).sendToTarget(); 324 } 325 } 326 handleLoadPlugin(ComponentName component)327 protected PluginInfo<T> handleLoadPlugin(ComponentName component) { 328 // This was already checked, but do it again here to make extra extra sure, we don't 329 // use these on production builds. 330 if (!isDebuggable && !isPluginWhitelisted(component)) { 331 // Never ever ever allow these on production builds, they are only for prototyping. 332 Log.w(TAG, "Plugin cannot be loaded on production build: " + component); 333 return null; 334 } 335 if (!mManager.getPluginEnabler().isEnabled(component)) { 336 if (DEBUG) Log.d(TAG, "Plugin is not enabled, aborting load: " + component); 337 return null; 338 } 339 String pkg = component.getPackageName(); 340 String cls = component.getClassName(); 341 try { 342 ApplicationInfo info = mPm.getApplicationInfo(pkg, 0); 343 // TODO: This probably isn't needed given that we don't have IGNORE_SECURITY on 344 if (mPm.checkPermission(PLUGIN_PERMISSION, pkg) 345 != PackageManager.PERMISSION_GRANTED) { 346 Log.d(TAG, "Plugin doesn't have permission: " + pkg); 347 return null; 348 } 349 // Create our own ClassLoader so we can use our own code as the parent. 350 ClassLoader classLoader = mManager.getClassLoader(info); 351 Context pluginContext = new PluginContextWrapper( 352 mContext.createApplicationContext(info, 0), classLoader); 353 Class<?> pluginClass = Class.forName(cls, true, classLoader); 354 // TODO: Only create the plugin before version check if we need it for 355 // legacy version check. 356 T plugin = (T) pluginClass.newInstance(); 357 try { 358 VersionInfo version = checkVersion(pluginClass, plugin, mVersion); 359 if (DEBUG) Log.d(TAG, "createPlugin"); 360 return new PluginInfo(pkg, cls, plugin, pluginContext, version); 361 } catch (InvalidVersionException e) { 362 final int icon = mContext.getResources().getIdentifier("tuner", "drawable", 363 mContext.getPackageName()); 364 final int color = Resources.getSystem().getIdentifier( 365 "system_notification_accent_color", "color", "android"); 366 final Notification.Builder nb = new Notification.Builder(mContext, 367 PluginManager.NOTIFICATION_CHANNEL_ID) 368 .setStyle(new Notification.BigTextStyle()) 369 .setSmallIcon(icon) 370 .setWhen(0) 371 .setShowWhen(false) 372 .setVisibility(Notification.VISIBILITY_PUBLIC) 373 .setColor(mContext.getColor(color)); 374 String label = cls; 375 try { 376 label = mPm.getServiceInfo(component, 0).loadLabel(mPm).toString(); 377 } catch (NameNotFoundException e2) { 378 } 379 if (!e.isTooNew()) { 380 // Localization not required as this will never ever appear in a user build. 381 nb.setContentTitle("Plugin \"" + label + "\" is too old") 382 .setContentText("Contact plugin developer to get an updated" 383 + " version.\n" + e.getMessage()); 384 } else { 385 // Localization not required as this will never ever appear in a user build. 386 nb.setContentTitle("Plugin \"" + label + "\" is too new") 387 .setContentText("Check to see if an OTA is available.\n" 388 + e.getMessage()); 389 } 390 Intent i = new Intent(PluginManagerImpl.DISABLE_PLUGIN).setData( 391 Uri.parse("package://" + component.flattenToString())); 392 PendingIntent pi = PendingIntent.getBroadcast(mContext, 0, i, 0); 393 nb.addAction(new Action.Builder(null, "Disable plugin", pi).build()); 394 mContext.getSystemService(NotificationManager.class) 395 .notifyAsUser(cls, SystemMessage.NOTE_PLUGIN, nb.build(), 396 UserHandle.ALL); 397 // TODO: Warn user. 398 Log.w(TAG, "Plugin has invalid interface version " + plugin.getVersion() 399 + ", expected " + mVersion); 400 return null; 401 } 402 } catch (Throwable e) { 403 Log.w(TAG, "Couldn't load plugin: " + pkg, e); 404 return null; 405 } 406 } 407 checkVersion(Class<?> pluginClass, T plugin, VersionInfo version)408 private VersionInfo checkVersion(Class<?> pluginClass, T plugin, VersionInfo version) 409 throws InvalidVersionException { 410 VersionInfo pv = new VersionInfo().addClass(pluginClass); 411 if (pv.hasVersionInfo()) { 412 version.checkVersion(pv); 413 } else { 414 int fallbackVersion = plugin.getVersion(); 415 if (fallbackVersion != version.getDefaultVersion()) { 416 throw new InvalidVersionException("Invalid legacy version", false); 417 } 418 return null; 419 } 420 return pv; 421 } 422 } 423 424 public static class PluginContextWrapper extends ContextWrapper { 425 private final ClassLoader mClassLoader; 426 private LayoutInflater mInflater; 427 PluginContextWrapper(Context base, ClassLoader classLoader)428 public PluginContextWrapper(Context base, ClassLoader classLoader) { 429 super(base); 430 mClassLoader = classLoader; 431 } 432 433 @Override getClassLoader()434 public ClassLoader getClassLoader() { 435 return mClassLoader; 436 } 437 438 @Override getSystemService(String name)439 public Object getSystemService(String name) { 440 if (LAYOUT_INFLATER_SERVICE.equals(name)) { 441 if (mInflater == null) { 442 mInflater = LayoutInflater.from(getBaseContext()).cloneInContext(this); 443 } 444 return mInflater; 445 } 446 return getBaseContext().getSystemService(name); 447 } 448 } 449 450 static class PluginInfo<T> { 451 private final Context mPluginContext; 452 private final VersionInfo mVersion; 453 private String mClass; 454 T mPlugin; 455 String mPackage; 456 PluginInfo(String pkg, String cls, T plugin, Context pluginContext, VersionInfo info)457 public PluginInfo(String pkg, String cls, T plugin, Context pluginContext, 458 VersionInfo info) { 459 mPlugin = plugin; 460 mClass = cls; 461 mPackage = pkg; 462 mPluginContext = pluginContext; 463 mVersion = info; 464 } 465 } 466 } 467