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