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