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