From 5bec68fbea1a02d7debc2eeeaf01b9478de216e5 Mon Sep 17 00:00:00 2001 From: Jason Monk Date: Wed, 8 Feb 2017 20:45:10 -0800 Subject: New system for versioning sysui plugins Use annotations to handle the multi-dimensionalness of interface versions, but still maintain compile time inclusion of current versions. Test: runtest systemui Change-Id: I0789a72112cf6630a6406f76020071c8a6d9e24c --- .../testoverlayplugin/SampleOverlayPlugin.java | 8 +- .../systemui/plugins/IntentButtonProvider.java | 3 + .../android/systemui/plugins/OverlayPlugin.java | 5 +- .../src/com/android/systemui/plugins/Plugin.java | 19 +- .../systemui/plugins/PluginInstanceManager.java | 396 -------------------- .../android/systemui/plugins/PluginManager.java | 349 ------------------ .../com/android/systemui/plugins/PluginPrefs.java | 61 --- .../systemui/plugins/annotations/Dependencies.java | 27 ++ .../systemui/plugins/annotations/DependsOn.java | 32 ++ .../plugins/annotations/ProvidesInterface.java | 30 ++ .../systemui/plugins/annotations/Requirements.java | 27 ++ .../systemui/plugins/annotations/Requires.java | 33 ++ .../systemui/plugins/doze/DozeProvider.java | 2 + .../src/com/android/systemui/plugins/qs/QS.java | 27 +- .../statusbar/NotificationMenuRowProvider.java | 3 + .../statusbar/phone/NavBarButtonProvider.java | 3 +- .../plugins/statusbar/phone/NavGesture.java | 2 + .../android/systemui/PluginInflateContainer.java | 16 +- .../com/android/systemui/SystemUIApplication.java | 5 +- .../src/com/android/systemui/doze/DozeService.java | 2 +- .../systemui/fragments/PluginFragmentListener.java | 5 +- .../systemui/plugins/PluginInstanceManager.java | 410 +++++++++++++++++++++ .../android/systemui/plugins/PluginManager.java | 378 +++++++++++++++++++ .../com/android/systemui/plugins/PluginPrefs.java | 61 +++ .../com/android/systemui/plugins/VersionInfo.java | 134 +++++++ .../src/com/android/systemui/qs/QSPanel.java | 8 +- .../systemui/statusbar/NotificationMenuRow.java | 3 +- .../statusbar/phone/KeyguardBottomAreaView.java | 4 +- .../statusbar/phone/NavigationBarInflaterView.java | 4 +- .../statusbar/phone/NavigationBarView.java | 4 +- .../systemui/statusbar/phone/StatusBar.java | 2 +- .../plugins/PluginInstanceManagerTest.java | 40 +- .../systemui/plugins/PluginManagerTest.java | 23 +- .../android/systemui/plugins/VersionInfoTest.java | 103 ++++++ .../systemui/utils/leaks/FakePluginManager.java | 10 +- 35 files changed, 1346 insertions(+), 893 deletions(-) delete mode 100644 packages/SystemUI/plugin/src/com/android/systemui/plugins/PluginInstanceManager.java delete mode 100644 packages/SystemUI/plugin/src/com/android/systemui/plugins/PluginManager.java delete mode 100644 packages/SystemUI/plugin/src/com/android/systemui/plugins/PluginPrefs.java create mode 100644 packages/SystemUI/plugin/src/com/android/systemui/plugins/annotations/Dependencies.java create mode 100644 packages/SystemUI/plugin/src/com/android/systemui/plugins/annotations/DependsOn.java create mode 100644 packages/SystemUI/plugin/src/com/android/systemui/plugins/annotations/ProvidesInterface.java create mode 100644 packages/SystemUI/plugin/src/com/android/systemui/plugins/annotations/Requirements.java create mode 100644 packages/SystemUI/plugin/src/com/android/systemui/plugins/annotations/Requires.java create mode 100644 packages/SystemUI/src/com/android/systemui/plugins/PluginInstanceManager.java create mode 100644 packages/SystemUI/src/com/android/systemui/plugins/PluginManager.java create mode 100644 packages/SystemUI/src/com/android/systemui/plugins/PluginPrefs.java create mode 100644 packages/SystemUI/src/com/android/systemui/plugins/VersionInfo.java create mode 100644 packages/SystemUI/tests/src/com/android/systemui/plugins/VersionInfoTest.java diff --git a/packages/SystemUI/plugin/ExamplePlugin/src/com/android/systemui/plugin/testoverlayplugin/SampleOverlayPlugin.java b/packages/SystemUI/plugin/ExamplePlugin/src/com/android/systemui/plugin/testoverlayplugin/SampleOverlayPlugin.java index 13fc76c5da49..79a0c3599078 100644 --- a/packages/SystemUI/plugin/ExamplePlugin/src/com/android/systemui/plugin/testoverlayplugin/SampleOverlayPlugin.java +++ b/packages/SystemUI/plugin/ExamplePlugin/src/com/android/systemui/plugin/testoverlayplugin/SampleOverlayPlugin.java @@ -24,7 +24,9 @@ import android.view.ViewGroup; import android.view.ViewTreeObserver.InternalInsetsInfo; import android.view.ViewTreeObserver.OnComputeInternalInsetsListener; import com.android.systemui.plugins.OverlayPlugin; +import com.android.systemui.plugins.annotations.Requires; +@Requires(target = OverlayPlugin.class, version = OverlayPlugin.VERSION) public class SampleOverlayPlugin implements OverlayPlugin { private static final String TAG = "SampleOverlayPlugin"; private Context mPluginContext; @@ -35,12 +37,6 @@ public class SampleOverlayPlugin implements OverlayPlugin { private boolean mCollapseDesired; private float mStatusBarHeight; - @Override - public int getVersion() { - Log.d(TAG, "getVersion " + VERSION); - return VERSION; - } - @Override public void onCreate(Context sysuiContext, Context pluginContext) { Log.d(TAG, "onCreate"); diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/IntentButtonProvider.java b/packages/SystemUI/plugin/src/com/android/systemui/plugins/IntentButtonProvider.java index 1b8efa79ad73..7742425a9b63 100644 --- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/IntentButtonProvider.java +++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/IntentButtonProvider.java @@ -14,6 +14,8 @@ package com.android.systemui.plugins; +import com.android.systemui.plugins.annotations.ProvidesInterface; + import android.content.Intent; import android.graphics.drawable.Drawable; @@ -21,6 +23,7 @@ import android.graphics.drawable.Drawable; * An Intent Button represents a triggerable element in SysUI that consists of an * Icon and an intent to trigger when it is activated (clicked, swiped, etc.). */ +@ProvidesInterface(version = IntentButtonProvider.VERSION) public interface IntentButtonProvider extends Plugin { public static final int VERSION = 1; diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/OverlayPlugin.java b/packages/SystemUI/plugin/src/com/android/systemui/plugins/OverlayPlugin.java index f5074f75b7a4..61aa60bb9675 100644 --- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/OverlayPlugin.java +++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/OverlayPlugin.java @@ -13,12 +13,15 @@ */ package com.android.systemui.plugins; +import com.android.systemui.plugins.annotations.ProvidesInterface; + import android.view.View; +@ProvidesInterface(action = OverlayPlugin.ACTION, version = OverlayPlugin.VERSION) public interface OverlayPlugin extends Plugin { String ACTION = "com.android.systemui.action.PLUGIN_OVERLAY"; - int VERSION = 1; + int VERSION = 2; void setup(View statusBar, View navBar); diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/Plugin.java b/packages/SystemUI/plugin/src/com/android/systemui/plugins/Plugin.java index e75ecb7a127b..bb93367c3791 100644 --- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/Plugin.java +++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/Plugin.java @@ -13,6 +13,8 @@ */ package com.android.systemui.plugins; +import com.android.systemui.plugins.annotations.Requires; + import android.content.Context; /** @@ -111,18 +113,13 @@ import android.content.Context; public interface Plugin { /** - * Should be implemented as the following directly referencing the version constant - * from the plugin interface being implemented, this will allow recompiles to automatically - * pick up the current version. - *
-     * {@literal
-     * public int getVersion() {
-     *     return VERSION;
-     * }
-     * }
-     * @return
+     * @deprecated
+     * @see Requires
      */
-    int getVersion();
+    default int getVersion() {
+        // Default of -1 indicates the plugin supports the new Requires model.
+        return -1;
+    }
 
     default void onCreate(Context sysuiContext, Context pluginContext) {
     }
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/PluginInstanceManager.java b/packages/SystemUI/plugin/src/com/android/systemui/plugins/PluginInstanceManager.java
deleted file mode 100644
index dd1614b50ca6..000000000000
--- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/PluginInstanceManager.java
+++ /dev/null
@@ -1,396 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
- * except in compliance with the License. You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software distributed under the
- * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the specific language governing
- * permissions and limitations under the License.
- */
-
-package com.android.systemui.plugins;
-
-import android.app.Notification;
-import android.app.Notification.Action;
-import android.app.NotificationManager;
-import android.app.PendingIntent;
-import android.content.BroadcastReceiver;
-import android.content.ComponentName;
-import android.content.Context;
-import android.content.ContextWrapper;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.content.pm.ApplicationInfo;
-import android.content.pm.PackageManager;
-import android.content.pm.PackageManager.NameNotFoundException;
-import android.content.pm.ResolveInfo;
-import android.content.res.Resources;
-import android.net.Uri;
-import android.os.Build;
-import android.os.Handler;
-import android.os.Looper;
-import android.os.Message;
-import android.os.UserHandle;
-import android.util.Log;
-import android.view.LayoutInflater;
-
-import com.android.internal.annotations.VisibleForTesting;
-import com.android.internal.messages.nano.SystemMessageProto.SystemMessage;
-
-import java.util.ArrayList;
-import java.util.List;
-
-public class PluginInstanceManager {
-
-    private static final boolean DEBUG = false;
-
-    private static final String TAG = "PluginInstanceManager";
-    private static final String PLUGIN_PERMISSION = "com.android.systemui.permission.PLUGIN";
-
-    private final Context mContext;
-    private final PluginListener mListener;
-    private final String mAction;
-    private final boolean mAllowMultiple;
-    private final int mVersion;
-
-    @VisibleForTesting
-    final MainHandler mMainHandler;
-    @VisibleForTesting
-    final PluginHandler mPluginHandler;
-    private final boolean isDebuggable;
-    private final PackageManager mPm;
-    private final PluginManager mManager;
-
-    PluginInstanceManager(Context context, String action, PluginListener listener,
-            boolean allowMultiple, Looper looper, int version, PluginManager manager) {
-        this(context, context.getPackageManager(), action, listener, allowMultiple, looper, version,
-                manager, Build.IS_DEBUGGABLE);
-    }
-
-    @VisibleForTesting
-    PluginInstanceManager(Context context, PackageManager pm, String action,
-            PluginListener listener, boolean allowMultiple, Looper looper, int version,
-            PluginManager manager, boolean debuggable) {
-        mMainHandler = new MainHandler(Looper.getMainLooper());
-        mPluginHandler = new PluginHandler(looper);
-        mManager = manager;
-        mContext = context;
-        mPm = pm;
-        mAction = action;
-        mListener = listener;
-        mAllowMultiple = allowMultiple;
-        mVersion = version;
-        isDebuggable = debuggable;
-    }
-
-    public PluginInfo getPlugin() {
-        if (Looper.myLooper() != Looper.getMainLooper()) {
-            throw new RuntimeException("Must be called from UI thread");
-        }
-        mPluginHandler.handleQueryPlugins(null /* All packages */);
-        if (mPluginHandler.mPlugins.size() > 0) {
-            mMainHandler.removeMessages(MainHandler.PLUGIN_CONNECTED);
-            PluginInfo info = mPluginHandler.mPlugins.get(0);
-            PluginPrefs.setHasPlugins(mContext);
-            info.mPlugin.onCreate(mContext, info.mPluginContext);
-            return info;
-        }
-        return null;
-    }
-
-    public void loadAll() {
-        if (DEBUG) Log.d(TAG, "startListening");
-        mPluginHandler.sendEmptyMessage(PluginHandler.QUERY_ALL);
-    }
-
-    public void destroy() {
-        if (DEBUG) Log.d(TAG, "stopListening");
-        ArrayList plugins = new ArrayList<>(mPluginHandler.mPlugins);
-        for (PluginInfo plugin : plugins) {
-            mMainHandler.obtainMessage(MainHandler.PLUGIN_DISCONNECTED,
-                    plugin.mPlugin).sendToTarget();
-        }
-    }
-
-    public void onPackageRemoved(String pkg) {
-        mPluginHandler.obtainMessage(PluginHandler.REMOVE_PKG, pkg).sendToTarget();
-    }
-
-    public void onPackageChange(String pkg) {
-        mPluginHandler.obtainMessage(PluginHandler.REMOVE_PKG, pkg).sendToTarget();
-        mPluginHandler.obtainMessage(PluginHandler.QUERY_PKG, pkg).sendToTarget();
-    }
-
-    public boolean checkAndDisable(String className) {
-        boolean disableAny = false;
-        ArrayList plugins = new ArrayList<>(mPluginHandler.mPlugins);
-        for (PluginInfo info : plugins) {
-            if (className.startsWith(info.mPackage)) {
-                disable(info);
-                disableAny = true;
-            }
-        }
-        return disableAny;
-    }
-
-    public void disableAll() {
-        ArrayList plugins = new ArrayList<>(mPluginHandler.mPlugins);
-        for (int i = 0; i < plugins.size(); i++) {
-            disable(plugins.get(i));
-        }
-    }
-
-    private void disable(PluginInfo info) {
-        // Live by the sword, die by the sword.
-        // Misbehaving plugins get disabled and won't come back until uninstall/reinstall.
-
-        // If a plugin is detected in the stack of a crash then this will be called for that
-        // plugin, if the plugin causing a crash cannot be identified, they are all disabled
-        // assuming one of them must be bad.
-        Log.w(TAG, "Disabling plugin " + info.mPackage + "/" + info.mClass);
-        mPm.setComponentEnabledSetting(
-                new ComponentName(info.mPackage, info.mClass),
-                PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
-                PackageManager.DONT_KILL_APP);
-        final String pkg = info.mPackage;
-        final Intent intent = new Intent(PluginManager.PLUGIN_CHANGED,
-                pkg != null ? Uri.fromParts("package", pkg, null) : null);
-        mContext.sendBroadcast(intent);
-    }
-
-    private class MainHandler extends Handler {
-        private static final int PLUGIN_CONNECTED = 1;
-        private static final int PLUGIN_DISCONNECTED = 2;
-
-        public MainHandler(Looper looper) {
-            super(looper);
-        }
-
-        @Override
-        public void handleMessage(Message msg) {
-            switch (msg.what) {
-                case PLUGIN_CONNECTED:
-                    if (DEBUG) Log.d(TAG, "onPluginConnected");
-                    PluginPrefs.setHasPlugins(mContext);
-                    PluginInfo info = (PluginInfo) msg.obj;
-                    if (!(msg.obj instanceof PluginFragment)) {
-                        // Only call onDestroy for plugins that aren't fragments, as fragments
-                        // will get the onCreate as part of the fragment lifecycle.
-                        info.mPlugin.onCreate(mContext, info.mPluginContext);
-                    }
-                    mListener.onPluginConnected(info.mPlugin, info.mPluginContext);
-                    break;
-                case PLUGIN_DISCONNECTED:
-                    if (DEBUG) Log.d(TAG, "onPluginDisconnected");
-                    mListener.onPluginDisconnected((T) msg.obj);
-                    if (!(msg.obj instanceof PluginFragment)) {
-                        // Only call onDestroy for plugins that aren't fragments, as fragments
-                        // will get the onDestroy as part of the fragment lifecycle.
-                        ((T) msg.obj).onDestroy();
-                    }
-                    break;
-                default:
-                    super.handleMessage(msg);
-                    break;
-            }
-        }
-    }
-
-    private class PluginHandler extends Handler {
-        private static final int QUERY_ALL = 1;
-        private static final int QUERY_PKG = 2;
-        private static final int REMOVE_PKG = 3;
-
-        private final ArrayList> mPlugins = new ArrayList<>();
-
-        public PluginHandler(Looper looper) {
-            super(looper);
-        }
-
-        @Override
-        public void handleMessage(Message msg) {
-            switch (msg.what) {
-                case QUERY_ALL:
-                    if (DEBUG) Log.d(TAG, "queryAll " + mAction);
-                    for (int i = mPlugins.size() - 1; i >= 0; i--) {
-                        PluginInfo plugin = mPlugins.get(i);
-                        mListener.onPluginDisconnected(plugin.mPlugin);
-                        if (!(plugin.mPlugin instanceof PluginFragment)) {
-                            // Only call onDestroy for plugins that aren't fragments, as fragments
-                            // will get the onDestroy as part of the fragment lifecycle.
-                            plugin.mPlugin.onDestroy();
-                        }
-                    }
-                    mPlugins.clear();
-                    handleQueryPlugins(null);
-                    break;
-                case REMOVE_PKG:
-                    String pkg = (String) msg.obj;
-                    for (int i = mPlugins.size() - 1; i >= 0; i--) {
-                        final PluginInfo plugin = mPlugins.get(i);
-                        if (plugin.mPackage.equals(pkg)) {
-                            mMainHandler.obtainMessage(MainHandler.PLUGIN_DISCONNECTED,
-                                    plugin.mPlugin).sendToTarget();
-                            mPlugins.remove(i);
-                        }
-                    }
-                    break;
-                case QUERY_PKG:
-                    String p = (String) msg.obj;
-                    if (DEBUG) Log.d(TAG, "queryPkg " + p + " " + mAction);
-                    if (mAllowMultiple || (mPlugins.size() == 0)) {
-                        handleQueryPlugins(p);
-                    } else {
-                        if (DEBUG) Log.d(TAG, "Too many of " + mAction);
-                    }
-                    break;
-                default:
-                    super.handleMessage(msg);
-            }
-        }
-
-        private void handleQueryPlugins(String pkgName) {
-            // This isn't actually a service and shouldn't ever be started, but is
-            // a convenient PM based way to manage our plugins.
-            Intent intent = new Intent(mAction);
-            if (pkgName != null) {
-                intent.setPackage(pkgName);
-            }
-            List result =
-                    mPm.queryIntentServices(intent, 0);
-            if (DEBUG) Log.d(TAG, "Found " + result.size() + " plugins");
-            if (result.size() > 1 && !mAllowMultiple) {
-                // TODO: Show warning.
-                Log.w(TAG, "Multiple plugins found for " + mAction);
-                return;
-            }
-            for (ResolveInfo info : result) {
-                ComponentName name = new ComponentName(info.serviceInfo.packageName,
-                        info.serviceInfo.name);
-                PluginInfo t = handleLoadPlugin(name);
-                if (t == null) continue;
-                mMainHandler.obtainMessage(mMainHandler.PLUGIN_CONNECTED, t).sendToTarget();
-                mPlugins.add(t);
-            }
-        }
-
-        protected PluginInfo handleLoadPlugin(ComponentName component) {
-            // This was already checked, but do it again here to make extra extra sure, we don't
-            // use these on production builds.
-            if (!isDebuggable) {
-                // Never ever ever allow these on production builds, they are only for prototyping.
-                Log.d(TAG, "Somehow hit second debuggable check");
-                return null;
-            }
-            String pkg = component.getPackageName();
-            String cls = component.getClassName();
-            try {
-                ApplicationInfo info = mPm.getApplicationInfo(pkg, 0);
-                // TODO: This probably isn't needed given that we don't have IGNORE_SECURITY on
-                if (mPm.checkPermission(PLUGIN_PERMISSION, pkg)
-                        != PackageManager.PERMISSION_GRANTED) {
-                    Log.d(TAG, "Plugin doesn't have permission: " + pkg);
-                    return null;
-                }
-                // Create our own ClassLoader so we can use our own code as the parent.
-                ClassLoader classLoader = mManager.getClassLoader(info.sourceDir, info.packageName);
-                Context pluginContext = new PluginContextWrapper(
-                        mContext.createApplicationContext(info, 0), classLoader);
-                Class pluginClass = Class.forName(cls, true, classLoader);
-                T plugin = (T) pluginClass.newInstance();
-                if (plugin.getVersion() != mVersion) {
-                    final int icon = mContext.getResources().getIdentifier("tuner", "drawable",
-                            mContext.getPackageName());
-                    final int color = Resources.getSystem().getIdentifier(
-                            "system_notification_accent_color", "color", "android");
-                    final Notification.Builder nb = new Notification.Builder(mContext,
-                            PluginManager.NOTIFICATION_CHANNEL_ID)
-                                    .setStyle(new Notification.BigTextStyle())
-                                    .setSmallIcon(icon)
-                                    .setWhen(0)
-                                    .setShowWhen(false)
-                                    .setVisibility(Notification.VISIBILITY_PUBLIC)
-                                    .setColor(mContext.getColor(color));
-                    String label = cls;
-                    try {
-                        label = mPm.getServiceInfo(component, 0).loadLabel(mPm).toString();
-                    } catch (NameNotFoundException e) {
-                    }
-                    if (plugin.getVersion() < mVersion) {
-                        // Localization not required as this will never ever appear in a user build.
-                        nb.setContentTitle("Plugin \"" + label + "\" is too old")
-                                .setContentText("Contact plugin developer to get an updated"
-                                        + " version.\nPlugin version: " + plugin.getVersion()
-                                        + "\nSystem version: " + mVersion);
-                    } else {
-                        // Localization not required as this will never ever appear in a user build.
-                        nb.setContentTitle("Plugin \"" + label + "\" is too new")
-                                .setContentText("Check to see if an OTA is available.\n"
-                                        + "Plugin version: " + plugin.getVersion()
-                                        + "\nSystem version: " + mVersion);
-                    }
-                    Intent i = new Intent(PluginManager.DISABLE_PLUGIN).setData(
-                            Uri.parse("package://" + component.flattenToString()));
-                    PendingIntent pi = PendingIntent.getBroadcast(mContext, 0, i, 0);
-                    nb.addAction(new Action.Builder(null, "Disable plugin", pi).build());
-                    mContext.getSystemService(NotificationManager.class)
-                            .notifyAsUser(cls, SystemMessage.NOTE_PLUGIN, nb.build(),
-                                    UserHandle.ALL);
-                    // TODO: Warn user.
-                    Log.w(TAG, "Plugin has invalid interface version " + plugin.getVersion()
-                            + ", expected " + mVersion);
-                    return null;
-                }
-                if (DEBUG) Log.d(TAG, "createPlugin");
-                return new PluginInfo(pkg, cls, plugin, pluginContext);
-            } catch (Exception e) {
-                Log.w(TAG, "Couldn't load plugin: " + pkg, e);
-                return null;
-            }
-        }
-    }
-
-    public static class PluginContextWrapper extends ContextWrapper {
-        private final ClassLoader mClassLoader;
-        private LayoutInflater mInflater;
-
-        public PluginContextWrapper(Context base, ClassLoader classLoader) {
-            super(base);
-            mClassLoader = classLoader;
-        }
-
-        @Override
-        public ClassLoader getClassLoader() {
-            return mClassLoader;
-        }
-
-        @Override
-        public Object getSystemService(String name) {
-            if (LAYOUT_INFLATER_SERVICE.equals(name)) {
-                if (mInflater == null) {
-                    mInflater = LayoutInflater.from(getBaseContext()).cloneInContext(this);
-                }
-                return mInflater;
-            }
-            return getBaseContext().getSystemService(name);
-        }
-    }
-
-    static class PluginInfo {
-        private final Context mPluginContext;
-        private String mClass;
-        T mPlugin;
-        String mPackage;
-
-        public PluginInfo(String pkg, String cls, T plugin, Context pluginContext) {
-            mPlugin = plugin;
-            mClass = cls;
-            mPackage = pkg;
-            mPluginContext = pluginContext;
-        }
-    }
-}
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/PluginManager.java b/packages/SystemUI/plugin/src/com/android/systemui/plugins/PluginManager.java
deleted file mode 100644
index cef485e673ad..000000000000
--- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/PluginManager.java
+++ /dev/null
@@ -1,349 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
- * except in compliance with the License. You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software distributed under the
- * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the specific language governing
- * permissions and limitations under the License.
- */
-
-package com.android.systemui.plugins;
-
-import android.app.Notification;
-import android.app.Notification.Action;
-import android.app.NotificationManager;
-import android.app.PendingIntent;
-import android.content.BroadcastReceiver;
-import android.content.ComponentName;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.content.pm.ApplicationInfo;
-import android.content.pm.PackageManager;
-import android.content.pm.PackageManager.NameNotFoundException;
-import android.content.res.Resources;
-import android.net.Uri;
-import android.os.Build;
-import android.os.HandlerThread;
-import android.os.Looper;
-import android.os.SystemProperties;
-import android.os.UserHandle;
-import android.util.ArrayMap;
-import android.util.ArraySet;
-
-import com.android.internal.annotations.VisibleForTesting;
-import com.android.internal.messages.nano.SystemMessageProto.SystemMessage;
-import com.android.systemui.plugins.PluginInstanceManager.PluginContextWrapper;
-import com.android.systemui.plugins.PluginInstanceManager.PluginInfo;
-
-import dalvik.system.PathClassLoader;
-
-import java.lang.Thread.UncaughtExceptionHandler;
-import java.util.Map;
-
-/**
- * @see Plugin
- */
-public class PluginManager extends BroadcastReceiver {
-
-    public static final String PLUGIN_CHANGED = "com.android.systemui.action.PLUGIN_CHANGED";
-
-    static final String DISABLE_PLUGIN = "com.android.systemui.action.DISABLE_PLUGIN";
-
-    // must be one of the channels created in NotificationChannels.java
-    static final String NOTIFICATION_CHANNEL_ID = "ALR";
-
-    private static PluginManager sInstance;
-
-    private final HandlerThread mBackgroundThread;
-    private final ArrayMap, PluginInstanceManager> mPluginMap
-            = new ArrayMap<>();
-    private final Map mClassLoaders = new ArrayMap<>();
-    private final ArraySet mOneShotPackages = new ArraySet<>();
-    private final Context mContext;
-    private final PluginInstanceManagerFactory mFactory;
-    private final boolean isDebuggable;
-    private final PluginPrefs mPluginPrefs;
-    private ClassLoaderFilter mParentClassLoader;
-    private boolean mListening;
-    private boolean mHasOneShot;
-
-    public PluginManager(Context context) {
-        this(context, new PluginInstanceManagerFactory(),
-                Build.IS_DEBUGGABLE, Thread.getDefaultUncaughtExceptionHandler());
-    }
-
-    @VisibleForTesting
-    PluginManager(Context context, PluginInstanceManagerFactory factory, boolean debuggable,
-            UncaughtExceptionHandler defaultHandler) {
-        mContext = context;
-        mFactory = factory;
-        mBackgroundThread = new HandlerThread("Plugins");
-        mBackgroundThread.start();
-        isDebuggable = debuggable;
-        mPluginPrefs = new PluginPrefs(mContext);
-
-        PluginExceptionHandler uncaughtExceptionHandler = new PluginExceptionHandler(
-                defaultHandler);
-        Thread.setDefaultUncaughtExceptionHandler(uncaughtExceptionHandler);
-    }
-
-    public  T getOneShotPlugin(String action, int version) {
-        if (!isDebuggable) {
-            // Never ever ever allow these on production builds, they are only for prototyping.
-            return null;
-        }
-        if (Looper.myLooper() != Looper.getMainLooper()) {
-            throw new RuntimeException("Must be called from UI thread");
-        }
-        PluginInstanceManager p = mFactory.createPluginInstanceManager(mContext, action, null,
-                false, mBackgroundThread.getLooper(), version, this);
-        mPluginPrefs.addAction(action);
-        PluginInfo info = p.getPlugin();
-        if (info != null) {
-            mOneShotPackages.add(info.mPackage);
-            mHasOneShot = true;
-            startListening();
-            return info.mPlugin;
-        }
-        return null;
-    }
-
-    public  void addPluginListener(String action, PluginListener listener,
-            int version) {
-        addPluginListener(action, listener, version, false);
-    }
-
-    public  void addPluginListener(String action, PluginListener listener,
-            int version, boolean allowMultiple) {
-        if (!isDebuggable) {
-            // Never ever ever allow these on production builds, they are only for prototyping.
-            return;
-        }
-        mPluginPrefs.addAction(action);
-        PluginInstanceManager p = mFactory.createPluginInstanceManager(mContext, action, listener,
-                allowMultiple, mBackgroundThread.getLooper(), version, this);
-        p.loadAll();
-        mPluginMap.put(listener, p);
-        startListening();
-    }
-
-    public void removePluginListener(PluginListener listener) {
-        if (!isDebuggable) {
-            // Never ever ever allow these on production builds, they are only for prototyping.
-            return;
-        }
-        if (!mPluginMap.containsKey(listener)) return;
-        mPluginMap.remove(listener).destroy();
-        stopListening();
-    }
-
-    private void startListening() {
-        if (mListening) return;
-        mListening = true;
-        IntentFilter filter = new IntentFilter(Intent.ACTION_PACKAGE_ADDED);
-        filter.addAction(Intent.ACTION_PACKAGE_CHANGED);
-        filter.addAction(Intent.ACTION_PACKAGE_REMOVED);
-        filter.addAction(PLUGIN_CHANGED);
-        filter.addAction(DISABLE_PLUGIN);
-        filter.addDataScheme("package");
-        mContext.registerReceiver(this, filter);
-        filter = new IntentFilter(Intent.ACTION_USER_UNLOCKED);
-        mContext.registerReceiver(this, filter);
-    }
-
-    private void stopListening() {
-        // Never stop listening if a one-shot is present.
-        if (!mListening || mHasOneShot) return;
-        mListening = false;
-        mContext.unregisterReceiver(this);
-    }
-
-    @Override
-    public void onReceive(Context context, Intent intent) {
-        if (Intent.ACTION_USER_UNLOCKED.equals(intent.getAction())) {
-            for (PluginInstanceManager manager : mPluginMap.values()) {
-                manager.loadAll();
-            }
-        } else if (DISABLE_PLUGIN.equals(intent.getAction())) {
-            Uri uri = intent.getData();
-            ComponentName component = ComponentName.unflattenFromString(
-                    uri.toString().substring(10));
-            mContext.getPackageManager().setComponentEnabledSetting(component,
-                    PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
-                    PackageManager.DONT_KILL_APP);
-            mContext.getSystemService(NotificationManager.class).cancel(component.getClassName(),
-                    SystemMessage.NOTE_PLUGIN);
-        } else {
-            Uri data = intent.getData();
-            String pkg = data.getEncodedSchemeSpecificPart();
-            if (mOneShotPackages.contains(pkg)) {
-                int icon = mContext.getResources().getIdentifier("tuner", "drawable",
-                        mContext.getPackageName());
-                int color = Resources.getSystem().getIdentifier(
-                        "system_notification_accent_color", "color", "android");
-                String label = pkg;
-                try {
-                    PackageManager pm = mContext.getPackageManager();
-                    label = pm.getApplicationInfo(pkg, 0).loadLabel(pm).toString();
-                } catch (NameNotFoundException e) {
-                }
-                // Localization not required as this will never ever appear in a user build.
-                final Notification.Builder nb =
-                        new Notification.Builder(mContext, NOTIFICATION_CHANNEL_ID)
-                                .setSmallIcon(icon)
-                                .setWhen(0)
-                                .setShowWhen(false)
-                                .setPriority(Notification.PRIORITY_MAX)
-                                .setVisibility(Notification.VISIBILITY_PUBLIC)
-                                .setColor(mContext.getColor(color))
-                                .setContentTitle("Plugin \"" + label + "\" has updated")
-                                .setContentText("Restart SysUI for changes to take effect.");
-                Intent i = new Intent("com.android.systemui.action.RESTART").setData(
-                            Uri.parse("package://" + pkg));
-                PendingIntent pi = PendingIntent.getBroadcast(mContext, 0, i, 0);
-                nb.addAction(new Action.Builder(null, "Restart SysUI", pi).build());
-                mContext.getSystemService(NotificationManager.class).notifyAsUser(pkg,
-                        SystemMessage.NOTE_PLUGIN, nb.build(), UserHandle.ALL);
-            }
-            clearClassLoader(pkg);
-            if (!Intent.ACTION_PACKAGE_REMOVED.equals(intent.getAction())) {
-                for (PluginInstanceManager manager : mPluginMap.values()) {
-                    manager.onPackageChange(pkg);
-                }
-            } else {
-                for (PluginInstanceManager manager : mPluginMap.values()) {
-                    manager.onPackageRemoved(pkg);
-                }
-            }
-        }
-    }
-
-    public ClassLoader getClassLoader(String sourceDir, String pkg) {
-        if (mClassLoaders.containsKey(pkg)) {
-            return mClassLoaders.get(pkg);
-        }
-        ClassLoader classLoader = new PathClassLoader(sourceDir, getParentClassLoader());
-        mClassLoaders.put(pkg, classLoader);
-        return classLoader;
-    }
-
-    private void clearClassLoader(String pkg) {
-        mClassLoaders.remove(pkg);
-    }
-
-    ClassLoader getParentClassLoader() {
-        if (mParentClassLoader == null) {
-            // Lazily load this so it doesn't have any effect on devices without plugins.
-            mParentClassLoader = new ClassLoaderFilter(getClass().getClassLoader(),
-                    "com.android.systemui.plugin");
-        }
-        return mParentClassLoader;
-    }
-
-    public Context getAllPluginContext(Context context) {
-        return new PluginContextWrapper(context,
-                new AllPluginClassLoader(context.getClassLoader()));
-    }
-
-    public Context getContext(ApplicationInfo info, String pkg) throws NameNotFoundException {
-        ClassLoader classLoader = getClassLoader(info.sourceDir, pkg);
-        return new PluginContextWrapper(mContext.createApplicationContext(info, 0), classLoader);
-    }
-
-    private class AllPluginClassLoader extends ClassLoader {
-        public AllPluginClassLoader(ClassLoader classLoader) {
-            super(classLoader);
-        }
-
-        @Override
-        public Class loadClass(String s) throws ClassNotFoundException {
-            try {
-                return super.loadClass(s);
-            } catch (ClassNotFoundException e) {
-                for (ClassLoader classLoader : mClassLoaders.values()) {
-                    try {
-                        return classLoader.loadClass(s);
-                    } catch (ClassNotFoundException e1) {
-                        // Will re-throw e if all fail.
-                    }
-                }
-                throw e;
-            }
-        }
-    }
-
-    @VisibleForTesting
-    public static class PluginInstanceManagerFactory {
-        public  PluginInstanceManager createPluginInstanceManager(Context context,
-                String action, PluginListener listener, boolean allowMultiple, Looper looper,
-                int version, PluginManager manager) {
-            return new PluginInstanceManager(context, action, listener, allowMultiple, looper,
-                    version, manager);
-        }
-    }
-
-    // This allows plugins to include any libraries or copied code they want by only including
-    // classes from the plugin library.
-    private static class ClassLoaderFilter extends ClassLoader {
-        private final String mPackage;
-        private final ClassLoader mBase;
-
-        public ClassLoaderFilter(ClassLoader base, String pkg) {
-            super(ClassLoader.getSystemClassLoader());
-            mBase = base;
-            mPackage = pkg;
-        }
-
-        @Override
-        protected Class loadClass(String name, boolean resolve) throws ClassNotFoundException {
-            if (!name.startsWith(mPackage)) super.loadClass(name, resolve);
-            return mBase.loadClass(name);
-        }
-    }
-
-    private class PluginExceptionHandler implements UncaughtExceptionHandler {
-        private final UncaughtExceptionHandler mHandler;
-
-        private PluginExceptionHandler(UncaughtExceptionHandler handler) {
-            mHandler = handler;
-        }
-
-        @Override
-        public void uncaughtException(Thread thread, Throwable throwable) {
-            if (SystemProperties.getBoolean("plugin.debugging", false)) {
-                mHandler.uncaughtException(thread, throwable);
-                return;
-            }
-            // Search for and disable plugins that may have been involved in this crash.
-            boolean disabledAny = checkStack(throwable);
-            if (!disabledAny) {
-                // We couldn't find any plugins involved in this crash, just to be safe
-                // disable all the plugins, so we can be sure that SysUI is running as
-                // best as possible.
-                for (PluginInstanceManager manager : mPluginMap.values()) {
-                    manager.disableAll();
-                }
-            }
-
-            // Run the normal exception handler so we can crash and cleanup our state.
-            mHandler.uncaughtException(thread, throwable);
-        }
-
-        private boolean checkStack(Throwable throwable) {
-            if (throwable == null) return false;
-            boolean disabledAny = false;
-            for (StackTraceElement element : throwable.getStackTrace()) {
-                for (PluginInstanceManager manager : mPluginMap.values()) {
-                    disabledAny |= manager.checkAndDisable(element.getClassName());
-                }
-            }
-            return disabledAny | checkStack(throwable.getCause());
-        }
-    }
-}
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/PluginPrefs.java b/packages/SystemUI/plugin/src/com/android/systemui/plugins/PluginPrefs.java
deleted file mode 100644
index 3671b3c1689f..000000000000
--- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/PluginPrefs.java
+++ /dev/null
@@ -1,61 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
- * except in compliance with the License. You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software distributed under the
- * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the specific language governing
- * permissions and limitations under the License.
- */
-
-package com.android.systemui.plugins;
-
-import android.content.Context;
-import android.content.SharedPreferences;
-import android.util.ArraySet;
-
-import java.util.Set;
-
-/**
- * Storage for all plugin actions in SharedPreferences.
- *
- * This allows the list of actions that the Tuner needs to search for to be generated
- * instead of hard coded.
- */
-public class PluginPrefs {
-
-    private static final String PREFS = "plugin_prefs";
-
-    private static final String PLUGIN_ACTIONS = "actions";
-    private static final String HAS_PLUGINS = "plugins";
-
-    private final Set mPluginActions;
-    private final SharedPreferences mSharedPrefs;
-
-    public PluginPrefs(Context context) {
-        mSharedPrefs = context.getSharedPreferences(PREFS, 0);
-        mPluginActions = new ArraySet<>(mSharedPrefs.getStringSet(PLUGIN_ACTIONS, null));
-    }
-
-    public Set getPluginList() {
-        return mPluginActions;
-    }
-
-    public synchronized void addAction(String action) {
-        if (mPluginActions.add(action)){
-            mSharedPrefs.edit().putStringSet(PLUGIN_ACTIONS, mPluginActions).commit();
-        }
-    }
-
-    public static boolean hasPlugins(Context context) {
-        return context.getSharedPreferences(PREFS, 0).getBoolean(HAS_PLUGINS, false);
-    }
-
-    public static void setHasPlugins(Context context) {
-        context.getSharedPreferences(PREFS, 0).edit().putBoolean(HAS_PLUGINS, true).commit();
-    }
-}
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/annotations/Dependencies.java b/packages/SystemUI/plugin/src/com/android/systemui/plugins/annotations/Dependencies.java
new file mode 100644
index 000000000000..dbbf047519bb
--- /dev/null
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/annotations/Dependencies.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
+ * except in compliance with the License. You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the
+ * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+package com.android.systemui.plugins.annotations;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Used for repeated @DependsOn internally, not for plugin
+ * use.
+ */
+@Retention(RetentionPolicy.RUNTIME)
+public @interface Dependencies {
+    DependsOn[] value();
+}
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/annotations/DependsOn.java b/packages/SystemUI/plugin/src/com/android/systemui/plugins/annotations/DependsOn.java
new file mode 100644
index 000000000000..b81d67306307
--- /dev/null
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/annotations/DependsOn.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
+ * except in compliance with the License. You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the
+ * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+package com.android.systemui.plugins.annotations;
+
+import java.lang.annotation.Repeatable;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Used to indicate that an interface in the plugin library needs another
+ * interface to function properly. When this is added, it will be enforced
+ * that all plugins that @Requires the annotated interface also @Requires
+ * the specified class as well.
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Repeatable(value = Dependencies.class)
+public @interface DependsOn {
+    Class target();
+
+}
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/annotations/ProvidesInterface.java b/packages/SystemUI/plugin/src/com/android/systemui/plugins/annotations/ProvidesInterface.java
new file mode 100644
index 000000000000..d0e14b8657ff
--- /dev/null
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/annotations/ProvidesInterface.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
+ * except in compliance with the License. You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the
+ * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+package com.android.systemui.plugins.annotations;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Should be added to all interfaces in plugin lib to specify their
+ * current version and optionally their action to implement the plugin.
+ */
+@Retention(RetentionPolicy.RUNTIME)
+public @interface ProvidesInterface {
+    int version();
+
+    String action() default "";
+
+}
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/annotations/Requirements.java b/packages/SystemUI/plugin/src/com/android/systemui/plugins/annotations/Requirements.java
new file mode 100644
index 000000000000..9cfa279b9c19
--- /dev/null
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/annotations/Requirements.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
+ * except in compliance with the License. You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the
+ * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+package com.android.systemui.plugins.annotations;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Used for repeated @Requires internally, not for plugin
+ * use.
+ */
+@Retention(RetentionPolicy.RUNTIME)
+public @interface Requirements {
+    Requires[] value();
+}
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/annotations/Requires.java b/packages/SystemUI/plugin/src/com/android/systemui/plugins/annotations/Requires.java
new file mode 100644
index 000000000000..e1b1303b8cb5
--- /dev/null
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/annotations/Requires.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
+ * except in compliance with the License. You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the
+ * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+package com.android.systemui.plugins.annotations;
+
+import java.lang.annotation.Repeatable;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Used to annotate which interfaces a given plugin depends on.
+ *
+ * At minimum all plugins should have at least one @Requires annotation
+ * for the plugin interface that they are implementing. They will also
+ * need an @Requires for each class that the plugin interface @DependsOn.
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Repeatable(value = Requirements.class)
+public @interface Requires {
+    Class target();
+    int version();
+}
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/doze/DozeProvider.java b/packages/SystemUI/plugin/src/com/android/systemui/plugins/doze/DozeProvider.java
index 688df466d244..068848120103 100644
--- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/doze/DozeProvider.java
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/doze/DozeProvider.java
@@ -20,10 +20,12 @@ import android.app.PendingIntent;
 import android.content.Context;
 
 import com.android.systemui.plugins.Plugin;
+import com.android.systemui.plugins.annotations.ProvidesInterface;
 
 /**
  * Provides a {@link DozeUi}.
  */
+@ProvidesInterface(action = DozeProvider.ACTION, version = DozeProvider.VERSION)
 public interface DozeProvider extends Plugin {
 
     String ACTION = "com.android.systemui.action.PLUGIN_DOZE";
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/QS.java b/packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/QS.java
index e21a282581a4..b7467eba1b9c 100644
--- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/QS.java
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/QS.java
@@ -14,29 +14,32 @@
 
 package com.android.systemui.plugins.qs;
 
-import android.annotation.NonNull;
-import android.annotation.Nullable;
-import android.app.PendingIntent;
+import com.android.systemui.plugins.FragmentBase;
+import com.android.systemui.plugins.annotations.DependsOn;
+import com.android.systemui.plugins.annotations.ProvidesInterface;
+import com.android.systemui.plugins.qs.QS.Callback;
+import com.android.systemui.plugins.qs.QS.DetailAdapter;
+import com.android.systemui.plugins.qs.QS.HeightListener;
+
 import android.content.Context;
 import android.content.Intent;
 import android.util.AttributeSet;
 import android.view.View;
 import android.view.ViewGroup;
-import android.widget.FrameLayout;
 import android.widget.RelativeLayout;
 
-import com.android.systemui.plugins.FragmentBase;
-
 /**
  * Fragment that contains QS in the notification shade.  Most of the interface is for
  * handling the expand/collapsing of the view interaction.
  */
+@ProvidesInterface(action = QS.ACTION, version = QS.VERSION)
+@DependsOn(target = HeightListener.class)
+@DependsOn(target = Callback.class)
+@DependsOn(target = DetailAdapter.class)
 public interface QS extends FragmentBase {
 
     public static final String ACTION = "com.android.systemui.action.PLUGIN_QS";
 
-    // This should be incremented any time this class or ActivityStarter or BaseStatusBarHeader
-    // change in incompatible ways.
     public static final int VERSION = 5;
 
     String TAG = "QS";
@@ -64,17 +67,23 @@ public interface QS extends FragmentBase {
 
     public abstract void setContainer(ViewGroup container);
 
+    @ProvidesInterface(version = HeightListener.VERSION)
     public interface HeightListener {
+        public static final int VERSION = 1;
         void onQsHeightChanged();
     }
 
+    @ProvidesInterface(version = Callback.VERSION)
     public interface Callback {
+        public static final int VERSION = 1;
         void onShowingDetail(DetailAdapter detail, int x, int y);
         void onToggleStateChanged(boolean state);
         void onScanStateChanged(boolean state);
     }
 
+    @ProvidesInterface(version = DetailAdapter.VERSION)
     public interface DetailAdapter {
+        public static final int VERSION = 1;
         CharSequence getTitle();
         Boolean getToggleState();
         default boolean getToggleEnabled() {
@@ -92,7 +101,9 @@ public interface QS extends FragmentBase {
         default boolean hasHeader() { return true; }
     }
 
+    @ProvidesInterface(version = BaseStatusBarHeader.VERSION)
     public abstract static class BaseStatusBarHeader extends RelativeLayout {
+        public static final int VERSION = 1;
 
         public BaseStatusBarHeader(Context context, AttributeSet attrs) {
             super(context, attrs);
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/statusbar/NotificationMenuRowProvider.java b/packages/SystemUI/plugin/src/com/android/systemui/plugins/statusbar/NotificationMenuRowProvider.java
index 93ba39cbba9d..570c5c3be295 100644
--- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/statusbar/NotificationMenuRowProvider.java
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/statusbar/NotificationMenuRowProvider.java
@@ -10,7 +10,10 @@ import android.view.View;
 import java.util.ArrayList;
 
 import com.android.systemui.plugins.Plugin;
+import com.android.systemui.plugins.annotations.ProvidesInterface;
 
+@ProvidesInterface(action = NotificationMenuRowProvider.ACTION,
+        version = NotificationMenuRowProvider.VERSION)
 public interface NotificationMenuRowProvider extends Plugin {
 
     public static final String ACTION = "com.android.systemui.action.PLUGIN_NOTIFICATION_MENU_ROW";
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/statusbar/phone/NavBarButtonProvider.java b/packages/SystemUI/plugin/src/com/android/systemui/plugins/statusbar/phone/NavBarButtonProvider.java
index d54e33fc4dff..5243228121c6 100644
--- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/statusbar/phone/NavBarButtonProvider.java
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/statusbar/phone/NavBarButtonProvider.java
@@ -14,14 +14,15 @@
 
 package com.android.systemui.plugins.statusbar.phone;
 
-import android.annotation.DrawableRes;
 import android.annotation.Nullable;
 import android.graphics.drawable.Drawable;
 import android.view.View;
 import android.view.ViewGroup;
 
 import com.android.systemui.plugins.Plugin;
+import com.android.systemui.plugins.annotations.ProvidesInterface;
 
+@ProvidesInterface(action = NavBarButtonProvider.ACTION, version = NavBarButtonProvider.VERSION)
 public interface NavBarButtonProvider extends Plugin {
 
     public static final String ACTION = "com.android.systemui.action.PLUGIN_NAV_BUTTON";
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/statusbar/phone/NavGesture.java b/packages/SystemUI/plugin/src/com/android/systemui/plugins/statusbar/phone/NavGesture.java
index 918d6e96784d..ddee89ed74ec 100644
--- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/statusbar/phone/NavGesture.java
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/statusbar/phone/NavGesture.java
@@ -17,7 +17,9 @@ package com.android.systemui.plugins.statusbar.phone;
 import android.view.MotionEvent;
 
 import com.android.systemui.plugins.Plugin;
+import com.android.systemui.plugins.annotations.ProvidesInterface;
 
+@ProvidesInterface(action = NavGesture.ACTION, version = NavBarButtonProvider.VERSION)
 public interface NavGesture extends Plugin {
 
     public static final String ACTION = "com.android.systemui.action.PLUGIN_NAV_GESTURE";
diff --git a/packages/SystemUI/src/com/android/systemui/PluginInflateContainer.java b/packages/SystemUI/src/com/android/systemui/PluginInflateContainer.java
index 9cc66138cfb4..ddd48330e679 100644
--- a/packages/SystemUI/src/com/android/systemui/PluginInflateContainer.java
+++ b/packages/SystemUI/src/com/android/systemui/PluginInflateContainer.java
@@ -53,8 +53,7 @@ public class PluginInflateContainer extends AutoReinflateContainer
 
     private static final String TAG = "PluginInflateContainer";
 
-    private String mAction;
-    private int mVersion;
+    private Class mClass;
     private View mPluginView;
 
     public PluginInflateContainer(Context context, @Nullable AttributeSet attrs) {
@@ -62,28 +61,25 @@ public class PluginInflateContainer extends AutoReinflateContainer
         TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.PluginInflateContainer);
         String viewType = a.getString(R.styleable.PluginInflateContainer_viewType);
         try {
-            Class c = Class.forName(viewType);
-            mAction = (String) c.getDeclaredField("ACTION").get(null);
-            mVersion = (int) c.getDeclaredField("VERSION").get(null);
+            mClass = Class.forName(viewType);
         } catch (Exception e) {
             Log.d(TAG, "Problem getting class info " + viewType, e);
-            mAction = null;
-            mVersion = 0;
+            mClass = null;
         }
     }
 
     @Override
     protected void onAttachedToWindow() {
         super.onAttachedToWindow();
-        if (mAction != null) {
-            Dependency.get(PluginManager.class).addPluginListener(mAction, this, mVersion);
+        if (mClass != null) {
+            Dependency.get(PluginManager.class).addPluginListener(this, mClass);
         }
     }
 
     @Override
     protected void onDetachedFromWindow() {
         super.onDetachedFromWindow();
-        if (mAction != null) {
+        if (mClass != null) {
             Dependency.get(PluginManager.class).removePluginListener(this);
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java b/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java
index e5bda7edfc58..1b1e7be0a70d 100644
--- a/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java
+++ b/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java
@@ -36,6 +36,7 @@ import com.android.systemui.keyguard.KeyguardViewMediator;
 import com.android.systemui.media.RingtonePlayer;
 import com.android.systemui.pip.PipUI;
 import com.android.systemui.plugins.OverlayPlugin;
+import com.android.systemui.plugins.Plugin;
 import com.android.systemui.plugins.PluginListener;
 import com.android.systemui.plugins.PluginManager;
 import com.android.systemui.power.PowerUI;
@@ -205,7 +206,7 @@ public class SystemUIApplication extends Application implements SysUiServiceProv
                 mServices[i].onBootCompleted();
             }
         }
-        Dependency.get(PluginManager.class).addPluginListener(OverlayPlugin.ACTION,
+        Dependency.get(PluginManager.class).addPluginListener(
                 new PluginListener() {
                     private ArraySet mOverlays;
 
@@ -234,7 +235,7 @@ public class SystemUIApplication extends Application implements SysUiServiceProv
                         Dependency.get(StatusBarWindowManager.class).setForcePluginOpen(
                                 mOverlays.size() != 0);
                     }
-                }, OverlayPlugin.VERSION, true /* Allow multiple plugins */);
+                }, OverlayPlugin.class, true /* Allow multiple plugins */);
 
         mServicesStarted = true;
     }
diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeService.java b/packages/SystemUI/src/com/android/systemui/doze/DozeService.java
index 94dc9a344a34..6186df14ef3f 100644
--- a/packages/SystemUI/src/com/android/systemui/doze/DozeService.java
+++ b/packages/SystemUI/src/com/android/systemui/doze/DozeService.java
@@ -49,7 +49,7 @@ public class DozeService extends DreamService implements DozeMachine.Service {
         }
 
         DozeProvider provider = Dependency.get(PluginManager.class)
-                .getOneShotPlugin(DozeProvider.ACTION, DozeProvider.VERSION);
+                .getOneShotPlugin(DozeProvider.class);
         mDozeMachine = new DozeFactory(provider).assembleMachine(this);
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/fragments/PluginFragmentListener.java b/packages/SystemUI/src/com/android/systemui/fragments/PluginFragmentListener.java
index 1eaca6ffd9db..03bb73da3902 100644
--- a/packages/SystemUI/src/com/android/systemui/fragments/PluginFragmentListener.java
+++ b/packages/SystemUI/src/com/android/systemui/fragments/PluginFragmentListener.java
@@ -44,8 +44,9 @@ public class PluginFragmentListener implements PluginListener {
         mDefaultClass = defaultFragment;
     }
 
-    public void startListening(String action, int version) {
-        mPluginManager.addPluginListener(action, this, version, false /* Only allow one */);
+    public void startListening() {
+        mPluginManager.addPluginListener(this, mExpectedInterface,
+                false /* Only allow one */);
     }
 
     public void stopListening() {
diff --git a/packages/SystemUI/src/com/android/systemui/plugins/PluginInstanceManager.java b/packages/SystemUI/src/com/android/systemui/plugins/PluginInstanceManager.java
new file mode 100644
index 000000000000..e895fa226cf3
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/plugins/PluginInstanceManager.java
@@ -0,0 +1,410 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
+ * except in compliance with the License. You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the
+ * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+package com.android.systemui.plugins;
+
+import android.app.Notification;
+import android.app.Notification.Action;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.ContextWrapper;
+import android.content.Intent;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.pm.ResolveInfo;
+import android.content.res.Resources;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.os.UserHandle;
+import android.util.Log;
+import android.view.LayoutInflater;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.messages.nano.SystemMessageProto.SystemMessage;
+import com.android.systemui.plugins.VersionInfo.InvalidVersionException;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class PluginInstanceManager {
+
+    private static final boolean DEBUG = false;
+
+    private static final String TAG = "PluginInstanceManager";
+    private static final String PLUGIN_PERMISSION = "com.android.systemui.permission.PLUGIN";
+
+    private final Context mContext;
+    private final PluginListener mListener;
+    private final String mAction;
+    private final boolean mAllowMultiple;
+    private final VersionInfo mVersion;
+
+    @VisibleForTesting
+    final MainHandler mMainHandler;
+    @VisibleForTesting
+    final PluginHandler mPluginHandler;
+    private final boolean isDebuggable;
+    private final PackageManager mPm;
+    private final PluginManager mManager;
+
+    PluginInstanceManager(Context context, String action, PluginListener listener,
+            boolean allowMultiple, Looper looper, VersionInfo version, PluginManager manager) {
+        this(context, context.getPackageManager(), action, listener, allowMultiple, looper, version,
+                manager, Build.IS_DEBUGGABLE);
+    }
+
+    @VisibleForTesting
+    PluginInstanceManager(Context context, PackageManager pm, String action,
+            PluginListener listener, boolean allowMultiple, Looper looper, VersionInfo version,
+            PluginManager manager, boolean debuggable) {
+        mMainHandler = new MainHandler(Looper.getMainLooper());
+        mPluginHandler = new PluginHandler(looper);
+        mManager = manager;
+        mContext = context;
+        mPm = pm;
+        mAction = action;
+        mListener = listener;
+        mAllowMultiple = allowMultiple;
+        mVersion = version;
+        isDebuggable = debuggable;
+    }
+
+    public PluginInfo getPlugin() {
+        if (Looper.myLooper() != Looper.getMainLooper()) {
+            throw new RuntimeException("Must be called from UI thread");
+        }
+        mPluginHandler.handleQueryPlugins(null /* All packages */);
+        if (mPluginHandler.mPlugins.size() > 0) {
+            mMainHandler.removeMessages(MainHandler.PLUGIN_CONNECTED);
+            PluginInfo info = mPluginHandler.mPlugins.get(0);
+            PluginPrefs.setHasPlugins(mContext);
+            info.mPlugin.onCreate(mContext, info.mPluginContext);
+            return info;
+        }
+        return null;
+    }
+
+    public void loadAll() {
+        if (DEBUG) Log.d(TAG, "startListening");
+        mPluginHandler.sendEmptyMessage(PluginHandler.QUERY_ALL);
+    }
+
+    public void destroy() {
+        if (DEBUG) Log.d(TAG, "stopListening");
+        ArrayList plugins = new ArrayList<>(mPluginHandler.mPlugins);
+        for (PluginInfo plugin : plugins) {
+            mMainHandler.obtainMessage(MainHandler.PLUGIN_DISCONNECTED,
+                    plugin.mPlugin).sendToTarget();
+        }
+    }
+
+    public void onPackageRemoved(String pkg) {
+        mPluginHandler.obtainMessage(PluginHandler.REMOVE_PKG, pkg).sendToTarget();
+    }
+
+    public void onPackageChange(String pkg) {
+        mPluginHandler.obtainMessage(PluginHandler.REMOVE_PKG, pkg).sendToTarget();
+        mPluginHandler.obtainMessage(PluginHandler.QUERY_PKG, pkg).sendToTarget();
+    }
+
+    public boolean checkAndDisable(String className) {
+        boolean disableAny = false;
+        ArrayList plugins = new ArrayList<>(mPluginHandler.mPlugins);
+        for (PluginInfo info : plugins) {
+            if (className.startsWith(info.mPackage)) {
+                disable(info);
+                disableAny = true;
+            }
+        }
+        return disableAny;
+    }
+
+    public void disableAll() {
+        ArrayList plugins = new ArrayList<>(mPluginHandler.mPlugins);
+        for (int i = 0; i < plugins.size(); i++) {
+            disable(plugins.get(i));
+        }
+    }
+
+    private void disable(PluginInfo info) {
+        // Live by the sword, die by the sword.
+        // Misbehaving plugins get disabled and won't come back until uninstall/reinstall.
+
+        // If a plugin is detected in the stack of a crash then this will be called for that
+        // plugin, if the plugin causing a crash cannot be identified, they are all disabled
+        // assuming one of them must be bad.
+        Log.w(TAG, "Disabling plugin " + info.mPackage + "/" + info.mClass);
+        mPm.setComponentEnabledSetting(
+                new ComponentName(info.mPackage, info.mClass),
+                PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
+                PackageManager.DONT_KILL_APP);
+        final String pkg = info.mPackage;
+        final Intent intent = new Intent(PluginManager.PLUGIN_CHANGED,
+                pkg != null ? Uri.fromParts("package", pkg, null) : null);
+        mContext.sendBroadcast(intent);
+    }
+
+    private class MainHandler extends Handler {
+        private static final int PLUGIN_CONNECTED = 1;
+        private static final int PLUGIN_DISCONNECTED = 2;
+
+        public MainHandler(Looper looper) {
+            super(looper);
+        }
+
+        @Override
+        public void handleMessage(Message msg) {
+            switch (msg.what) {
+                case PLUGIN_CONNECTED:
+                    if (DEBUG) Log.d(TAG, "onPluginConnected");
+                    PluginPrefs.setHasPlugins(mContext);
+                    PluginInfo info = (PluginInfo) msg.obj;
+                    if (!(msg.obj instanceof PluginFragment)) {
+                        // Only call onDestroy for plugins that aren't fragments, as fragments
+                        // will get the onCreate as part of the fragment lifecycle.
+                        info.mPlugin.onCreate(mContext, info.mPluginContext);
+                    }
+                    mListener.onPluginConnected(info.mPlugin, info.mPluginContext);
+                    break;
+                case PLUGIN_DISCONNECTED:
+                    if (DEBUG) Log.d(TAG, "onPluginDisconnected");
+                    mListener.onPluginDisconnected((T) msg.obj);
+                    if (!(msg.obj instanceof PluginFragment)) {
+                        // Only call onDestroy for plugins that aren't fragments, as fragments
+                        // will get the onDestroy as part of the fragment lifecycle.
+                        ((T) msg.obj).onDestroy();
+                    }
+                    break;
+                default:
+                    super.handleMessage(msg);
+                    break;
+            }
+        }
+    }
+
+    private class PluginHandler extends Handler {
+        private static final int QUERY_ALL = 1;
+        private static final int QUERY_PKG = 2;
+        private static final int REMOVE_PKG = 3;
+
+        private final ArrayList> mPlugins = new ArrayList<>();
+
+        public PluginHandler(Looper looper) {
+            super(looper);
+        }
+
+        @Override
+        public void handleMessage(Message msg) {
+            switch (msg.what) {
+                case QUERY_ALL:
+                    if (DEBUG) Log.d(TAG, "queryAll " + mAction);
+                    for (int i = mPlugins.size() - 1; i >= 0; i--) {
+                        PluginInfo plugin = mPlugins.get(i);
+                        mListener.onPluginDisconnected(plugin.mPlugin);
+                        if (!(plugin.mPlugin instanceof PluginFragment)) {
+                            // Only call onDestroy for plugins that aren't fragments, as fragments
+                            // will get the onDestroy as part of the fragment lifecycle.
+                            plugin.mPlugin.onDestroy();
+                        }
+                    }
+                    mPlugins.clear();
+                    handleQueryPlugins(null);
+                    break;
+                case REMOVE_PKG:
+                    String pkg = (String) msg.obj;
+                    for (int i = mPlugins.size() - 1; i >= 0; i--) {
+                        final PluginInfo plugin = mPlugins.get(i);
+                        if (plugin.mPackage.equals(pkg)) {
+                            mMainHandler.obtainMessage(MainHandler.PLUGIN_DISCONNECTED,
+                                    plugin.mPlugin).sendToTarget();
+                            mPlugins.remove(i);
+                        }
+                    }
+                    break;
+                case QUERY_PKG:
+                    String p = (String) msg.obj;
+                    if (DEBUG) Log.d(TAG, "queryPkg " + p + " " + mAction);
+                    if (mAllowMultiple || (mPlugins.size() == 0)) {
+                        handleQueryPlugins(p);
+                    } else {
+                        if (DEBUG) Log.d(TAG, "Too many of " + mAction);
+                    }
+                    break;
+                default:
+                    super.handleMessage(msg);
+            }
+        }
+
+        private void handleQueryPlugins(String pkgName) {
+            // This isn't actually a service and shouldn't ever be started, but is
+            // a convenient PM based way to manage our plugins.
+            Intent intent = new Intent(mAction);
+            if (pkgName != null) {
+                intent.setPackage(pkgName);
+            }
+            List result =
+                    mPm.queryIntentServices(intent, 0);
+            if (DEBUG) Log.d(TAG, "Found " + result.size() + " plugins");
+            if (result.size() > 1 && !mAllowMultiple) {
+                // TODO: Show warning.
+                Log.w(TAG, "Multiple plugins found for " + mAction);
+                return;
+            }
+            for (ResolveInfo info : result) {
+                ComponentName name = new ComponentName(info.serviceInfo.packageName,
+                        info.serviceInfo.name);
+                PluginInfo t = handleLoadPlugin(name);
+                if (t == null) continue;
+                mMainHandler.obtainMessage(mMainHandler.PLUGIN_CONNECTED, t).sendToTarget();
+                mPlugins.add(t);
+            }
+        }
+
+        protected PluginInfo handleLoadPlugin(ComponentName component) {
+            // This was already checked, but do it again here to make extra extra sure, we don't
+            // use these on production builds.
+            if (!isDebuggable) {
+                // Never ever ever allow these on production builds, they are only for prototyping.
+                Log.d(TAG, "Somehow hit second debuggable check");
+                return null;
+            }
+            String pkg = component.getPackageName();
+            String cls = component.getClassName();
+            try {
+                ApplicationInfo info = mPm.getApplicationInfo(pkg, 0);
+                // TODO: This probably isn't needed given that we don't have IGNORE_SECURITY on
+                if (mPm.checkPermission(PLUGIN_PERMISSION, pkg)
+                        != PackageManager.PERMISSION_GRANTED) {
+                    Log.d(TAG, "Plugin doesn't have permission: " + pkg);
+                    return null;
+                }
+                // Create our own ClassLoader so we can use our own code as the parent.
+                ClassLoader classLoader = mManager.getClassLoader(info.sourceDir, info.packageName);
+                Context pluginContext = new PluginContextWrapper(
+                        mContext.createApplicationContext(info, 0), classLoader);
+                Class pluginClass = Class.forName(cls, true, classLoader);
+                // TODO: Only create the plugin before version check if we need it for
+                // legacy version check.
+                T plugin = (T) pluginClass.newInstance();
+                try {
+                    checkVersion(pluginClass, plugin, mVersion);
+                    if (DEBUG) Log.d(TAG, "createPlugin");
+                    return new PluginInfo(pkg, cls, plugin, pluginContext);
+                } catch (InvalidVersionException e) {
+                    final int icon = mContext.getResources().getIdentifier("tuner", "drawable",
+                            mContext.getPackageName());
+                    final int color = Resources.getSystem().getIdentifier(
+                            "system_notification_accent_color", "color", "android");
+                    final Notification.Builder nb = new Notification.Builder(mContext,
+                            PluginManager.NOTIFICATION_CHANNEL_ID)
+                                    .setStyle(new Notification.BigTextStyle())
+                                    .setSmallIcon(icon)
+                                    .setWhen(0)
+                                    .setShowWhen(false)
+                                    .setVisibility(Notification.VISIBILITY_PUBLIC)
+                                    .setColor(mContext.getColor(color));
+                    String label = cls;
+                    try {
+                        label = mPm.getServiceInfo(component, 0).loadLabel(mPm).toString();
+                    } catch (NameNotFoundException e2) {
+                    }
+                    if (!e.isTooNew()) {
+                        // Localization not required as this will never ever appear in a user build.
+                        nb.setContentTitle("Plugin \"" + label + "\" is too old")
+                                .setContentText("Contact plugin developer to get an updated"
+                                        + " version.\n" + e.getMessage());
+                    } else {
+                        // Localization not required as this will never ever appear in a user build.
+                        nb.setContentTitle("Plugin \"" + label + "\" is too new")
+                                .setContentText("Check to see if an OTA is available.\n"
+                                        + e.getMessage());
+                    }
+                    Intent i = new Intent(PluginManager.DISABLE_PLUGIN).setData(
+                            Uri.parse("package://" + component.flattenToString()));
+                    PendingIntent pi = PendingIntent.getBroadcast(mContext, 0, i, 0);
+                    nb.addAction(new Action.Builder(null, "Disable plugin", pi).build());
+                    mContext.getSystemService(NotificationManager.class)
+                            .notifyAsUser(cls, SystemMessage.NOTE_PLUGIN, nb.build(),
+                                    UserHandle.ALL);
+                    // TODO: Warn user.
+                    Log.w(TAG, "Plugin has invalid interface version " + plugin.getVersion()
+                            + ", expected " + mVersion);
+                    return null;
+                }
+            } catch (Exception e) {
+                Log.w(TAG, "Couldn't load plugin: " + pkg, e);
+                return null;
+            }
+        }
+
+        private void checkVersion(Class pluginClass, T plugin, VersionInfo version)
+                throws InvalidVersionException {
+            VersionInfo pv = new VersionInfo().addClass(pluginClass);
+            if (pv.hasVersionInfo()) {
+                version.checkVersion(pv);
+            } else {
+                int fallbackVersion = plugin.getVersion();
+                if (fallbackVersion != version.getDefaultVersion()) {
+                    throw new InvalidVersionException("Invalid legacy version", false);
+                }
+            }
+        }
+    }
+
+    public static class PluginContextWrapper extends ContextWrapper {
+        private final ClassLoader mClassLoader;
+        private LayoutInflater mInflater;
+
+        public PluginContextWrapper(Context base, ClassLoader classLoader) {
+            super(base);
+            mClassLoader = classLoader;
+        }
+
+        @Override
+        public ClassLoader getClassLoader() {
+            return mClassLoader;
+        }
+
+        @Override
+        public Object getSystemService(String name) {
+            if (LAYOUT_INFLATER_SERVICE.equals(name)) {
+                if (mInflater == null) {
+                    mInflater = LayoutInflater.from(getBaseContext()).cloneInContext(this);
+                }
+                return mInflater;
+            }
+            return getBaseContext().getSystemService(name);
+        }
+    }
+
+    static class PluginInfo {
+        private final Context mPluginContext;
+        private String mClass;
+        T mPlugin;
+        String mPackage;
+
+        public PluginInfo(String pkg, String cls, T plugin, Context pluginContext) {
+            mPlugin = plugin;
+            mClass = cls;
+            mPackage = pkg;
+            mPluginContext = pluginContext;
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/plugins/PluginManager.java b/packages/SystemUI/src/com/android/systemui/plugins/PluginManager.java
new file mode 100644
index 000000000000..8b4bd7bc9f7b
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/plugins/PluginManager.java
@@ -0,0 +1,378 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
+ * except in compliance with the License. You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the
+ * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+package com.android.systemui.plugins;
+
+import android.app.Notification;
+import android.app.Notification.Action;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.content.BroadcastReceiver;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.res.Resources;
+import android.net.Uri;
+import android.os.Build;
+import android.os.HandlerThread;
+import android.os.Looper;
+import android.os.SystemProperties;
+import android.os.UserHandle;
+import android.text.TextUtils;
+import android.util.ArrayMap;
+import android.util.ArraySet;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.messages.nano.SystemMessageProto.SystemMessage;
+import com.android.systemui.plugins.PluginInstanceManager.PluginContextWrapper;
+import com.android.systemui.plugins.PluginInstanceManager.PluginInfo;
+import com.android.systemui.plugins.annotations.ProvidesInterface;
+
+import dalvik.system.PathClassLoader;
+
+import java.lang.Thread.UncaughtExceptionHandler;
+import java.util.Map;
+
+/**
+ * @see Plugin
+ */
+public class PluginManager extends BroadcastReceiver {
+
+    public static final String PLUGIN_CHANGED = "com.android.systemui.action.PLUGIN_CHANGED";
+
+    static final String DISABLE_PLUGIN = "com.android.systemui.action.DISABLE_PLUGIN";
+
+    // must be one of the channels created in NotificationChannels.java
+    static final String NOTIFICATION_CHANNEL_ID = "ALR";
+
+    private static PluginManager sInstance;
+
+    private final HandlerThread mBackgroundThread;
+    private final ArrayMap, PluginInstanceManager> mPluginMap
+            = new ArrayMap<>();
+    private final Map mClassLoaders = new ArrayMap<>();
+    private final ArraySet mOneShotPackages = new ArraySet<>();
+    private final Context mContext;
+    private final PluginInstanceManagerFactory mFactory;
+    private final boolean isDebuggable;
+    private final PluginPrefs mPluginPrefs;
+    private ClassLoaderFilter mParentClassLoader;
+    private boolean mListening;
+    private boolean mHasOneShot;
+
+    public PluginManager(Context context) {
+        this(context, new PluginInstanceManagerFactory(),
+                Build.IS_DEBUGGABLE, Thread.getDefaultUncaughtExceptionHandler());
+    }
+
+    @VisibleForTesting
+    PluginManager(Context context, PluginInstanceManagerFactory factory, boolean debuggable,
+            UncaughtExceptionHandler defaultHandler) {
+        mContext = context;
+        mFactory = factory;
+        mBackgroundThread = new HandlerThread("Plugins");
+        mBackgroundThread.start();
+        isDebuggable = debuggable;
+        mPluginPrefs = new PluginPrefs(mContext);
+
+        PluginExceptionHandler uncaughtExceptionHandler = new PluginExceptionHandler(
+                defaultHandler);
+        Thread.setDefaultUncaughtExceptionHandler(uncaughtExceptionHandler);
+    }
+
+    public  T getOneShotPlugin(Class cls) {
+        ProvidesInterface info = cls.getDeclaredAnnotation(ProvidesInterface.class);
+        if (info == null) {
+            throw new RuntimeException(cls + " doesn't provide an interface");
+        }
+        if (TextUtils.isEmpty(info.action())) {
+            throw new RuntimeException(cls + " doesn't provide an action");
+        }
+        return getOneShotPlugin(info.action(), cls);
+    }
+
+    public  T getOneShotPlugin(String action, Class cls) {
+        if (!isDebuggable) {
+            // Never ever ever allow these on production builds, they are only for prototyping.
+            return null;
+        }
+        if (Looper.myLooper() != Looper.getMainLooper()) {
+            throw new RuntimeException("Must be called from UI thread");
+        }
+        PluginInstanceManager p = mFactory.createPluginInstanceManager(mContext, action, null,
+                false, mBackgroundThread.getLooper(), cls, this);
+        mPluginPrefs.addAction(action);
+        PluginInfo info = p.getPlugin();
+        if (info != null) {
+            mOneShotPackages.add(info.mPackage);
+            mHasOneShot = true;
+            startListening();
+            return info.mPlugin;
+        }
+        return null;
+    }
+
+    public  void addPluginListener(PluginListener listener, Class cls) {
+        addPluginListener(listener, cls, false);
+    }
+
+    public  void addPluginListener(PluginListener listener, Class cls,
+            boolean allowMultiple) {
+        ProvidesInterface info = cls.getDeclaredAnnotation(ProvidesInterface.class);
+        if (info == null) {
+            throw new RuntimeException(cls + " doesn't provide an interface");
+        }
+        if (TextUtils.isEmpty(info.action())) {
+            throw new RuntimeException(cls + " doesn't provide an action");
+        }
+        addPluginListener(info.action(), listener, cls, allowMultiple);
+    }
+
+    public  void addPluginListener(String action, PluginListener listener,
+            Class cls) {
+        addPluginListener(action, listener, cls, false);
+    }
+
+    public  void addPluginListener(String action, PluginListener listener,
+            Class cls, boolean allowMultiple) {
+        if (!isDebuggable) {
+            // Never ever ever allow these on production builds, they are only for prototyping.
+            return;
+        }
+        mPluginPrefs.addAction(action);
+        PluginInstanceManager p = mFactory.createPluginInstanceManager(mContext, action, listener,
+                allowMultiple, mBackgroundThread.getLooper(), cls, this);
+        p.loadAll();
+        mPluginMap.put(listener, p);
+        startListening();
+    }
+
+    public void removePluginListener(PluginListener listener) {
+        if (!isDebuggable) {
+            // Never ever ever allow these on production builds, they are only for prototyping.
+            return;
+        }
+        if (!mPluginMap.containsKey(listener)) return;
+        mPluginMap.remove(listener).destroy();
+        stopListening();
+    }
+
+    private void startListening() {
+        if (mListening) return;
+        mListening = true;
+        IntentFilter filter = new IntentFilter(Intent.ACTION_PACKAGE_ADDED);
+        filter.addAction(Intent.ACTION_PACKAGE_CHANGED);
+        filter.addAction(Intent.ACTION_PACKAGE_REMOVED);
+        filter.addAction(PLUGIN_CHANGED);
+        filter.addAction(DISABLE_PLUGIN);
+        filter.addDataScheme("package");
+        mContext.registerReceiver(this, filter);
+        filter = new IntentFilter(Intent.ACTION_USER_UNLOCKED);
+        mContext.registerReceiver(this, filter);
+    }
+
+    private void stopListening() {
+        // Never stop listening if a one-shot is present.
+        if (!mListening || mHasOneShot) return;
+        mListening = false;
+        mContext.unregisterReceiver(this);
+    }
+
+    @Override
+    public void onReceive(Context context, Intent intent) {
+        if (Intent.ACTION_USER_UNLOCKED.equals(intent.getAction())) {
+            for (PluginInstanceManager manager : mPluginMap.values()) {
+                manager.loadAll();
+            }
+        } else if (DISABLE_PLUGIN.equals(intent.getAction())) {
+            Uri uri = intent.getData();
+            ComponentName component = ComponentName.unflattenFromString(
+                    uri.toString().substring(10));
+            mContext.getPackageManager().setComponentEnabledSetting(component,
+                    PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
+                    PackageManager.DONT_KILL_APP);
+            mContext.getSystemService(NotificationManager.class).cancel(component.getClassName(),
+                    SystemMessage.NOTE_PLUGIN);
+        } else {
+            Uri data = intent.getData();
+            String pkg = data.getEncodedSchemeSpecificPart();
+            if (mOneShotPackages.contains(pkg)) {
+                int icon = mContext.getResources().getIdentifier("tuner", "drawable",
+                        mContext.getPackageName());
+                int color = Resources.getSystem().getIdentifier(
+                        "system_notification_accent_color", "color", "android");
+                String label = pkg;
+                try {
+                    PackageManager pm = mContext.getPackageManager();
+                    label = pm.getApplicationInfo(pkg, 0).loadLabel(pm).toString();
+                } catch (NameNotFoundException e) {
+                }
+                // Localization not required as this will never ever appear in a user build.
+                final Notification.Builder nb =
+                        new Notification.Builder(mContext, NOTIFICATION_CHANNEL_ID)
+                                .setSmallIcon(icon)
+                                .setWhen(0)
+                                .setShowWhen(false)
+                                .setPriority(Notification.PRIORITY_MAX)
+                                .setVisibility(Notification.VISIBILITY_PUBLIC)
+                                .setColor(mContext.getColor(color))
+                                .setContentTitle("Plugin \"" + label + "\" has updated")
+                                .setContentText("Restart SysUI for changes to take effect.");
+                Intent i = new Intent("com.android.systemui.action.RESTART").setData(
+                            Uri.parse("package://" + pkg));
+                PendingIntent pi = PendingIntent.getBroadcast(mContext, 0, i, 0);
+                nb.addAction(new Action.Builder(null, "Restart SysUI", pi).build());
+                mContext.getSystemService(NotificationManager.class).notifyAsUser(pkg,
+                        SystemMessage.NOTE_PLUGIN, nb.build(), UserHandle.ALL);
+            }
+            clearClassLoader(pkg);
+            if (!Intent.ACTION_PACKAGE_REMOVED.equals(intent.getAction())) {
+                for (PluginInstanceManager manager : mPluginMap.values()) {
+                    manager.onPackageChange(pkg);
+                }
+            } else {
+                for (PluginInstanceManager manager : mPluginMap.values()) {
+                    manager.onPackageRemoved(pkg);
+                }
+            }
+        }
+    }
+
+    public ClassLoader getClassLoader(String sourceDir, String pkg) {
+        if (mClassLoaders.containsKey(pkg)) {
+            return mClassLoaders.get(pkg);
+        }
+        ClassLoader classLoader = new PathClassLoader(sourceDir, getParentClassLoader());
+        mClassLoaders.put(pkg, classLoader);
+        return classLoader;
+    }
+
+    private void clearClassLoader(String pkg) {
+        mClassLoaders.remove(pkg);
+    }
+
+    ClassLoader getParentClassLoader() {
+        if (mParentClassLoader == null) {
+            // Lazily load this so it doesn't have any effect on devices without plugins.
+            mParentClassLoader = new ClassLoaderFilter(getClass().getClassLoader(),
+                    "com.android.systemui.plugin");
+        }
+        return mParentClassLoader;
+    }
+
+    public Context getAllPluginContext(Context context) {
+        return new PluginContextWrapper(context,
+                new AllPluginClassLoader(context.getClassLoader()));
+    }
+
+    public Context getContext(ApplicationInfo info, String pkg) throws NameNotFoundException {
+        ClassLoader classLoader = getClassLoader(info.sourceDir, pkg);
+        return new PluginContextWrapper(mContext.createApplicationContext(info, 0), classLoader);
+    }
+
+    private class AllPluginClassLoader extends ClassLoader {
+        public AllPluginClassLoader(ClassLoader classLoader) {
+            super(classLoader);
+        }
+
+        @Override
+        public Class loadClass(String s) throws ClassNotFoundException {
+            try {
+                return super.loadClass(s);
+            } catch (ClassNotFoundException e) {
+                for (ClassLoader classLoader : mClassLoaders.values()) {
+                    try {
+                        return classLoader.loadClass(s);
+                    } catch (ClassNotFoundException e1) {
+                        // Will re-throw e if all fail.
+                    }
+                }
+                throw e;
+            }
+        }
+    }
+
+    @VisibleForTesting
+    public static class PluginInstanceManagerFactory {
+        public  PluginInstanceManager createPluginInstanceManager(Context context,
+                String action, PluginListener listener, boolean allowMultiple, Looper looper,
+                Class cls, PluginManager manager) {
+            return new PluginInstanceManager(context, action, listener, allowMultiple, looper,
+                    new VersionInfo().addClass(cls), manager);
+        }
+    }
+
+    // This allows plugins to include any libraries or copied code they want by only including
+    // classes from the plugin library.
+    private static class ClassLoaderFilter extends ClassLoader {
+        private final String mPackage;
+        private final ClassLoader mBase;
+
+        public ClassLoaderFilter(ClassLoader base, String pkg) {
+            super(ClassLoader.getSystemClassLoader());
+            mBase = base;
+            mPackage = pkg;
+        }
+
+        @Override
+        protected Class loadClass(String name, boolean resolve) throws ClassNotFoundException {
+            if (!name.startsWith(mPackage)) super.loadClass(name, resolve);
+            return mBase.loadClass(name);
+        }
+    }
+
+    private class PluginExceptionHandler implements UncaughtExceptionHandler {
+        private final UncaughtExceptionHandler mHandler;
+
+        private PluginExceptionHandler(UncaughtExceptionHandler handler) {
+            mHandler = handler;
+        }
+
+        @Override
+        public void uncaughtException(Thread thread, Throwable throwable) {
+            if (SystemProperties.getBoolean("plugin.debugging", false)) {
+                mHandler.uncaughtException(thread, throwable);
+                return;
+            }
+            // Search for and disable plugins that may have been involved in this crash.
+            boolean disabledAny = checkStack(throwable);
+            if (!disabledAny) {
+                // We couldn't find any plugins involved in this crash, just to be safe
+                // disable all the plugins, so we can be sure that SysUI is running as
+                // best as possible.
+                for (PluginInstanceManager manager : mPluginMap.values()) {
+                    manager.disableAll();
+                }
+            }
+
+            // Run the normal exception handler so we can crash and cleanup our state.
+            mHandler.uncaughtException(thread, throwable);
+        }
+
+        private boolean checkStack(Throwable throwable) {
+            if (throwable == null) return false;
+            boolean disabledAny = false;
+            for (StackTraceElement element : throwable.getStackTrace()) {
+                for (PluginInstanceManager manager : mPluginMap.values()) {
+                    disabledAny |= manager.checkAndDisable(element.getClassName());
+                }
+            }
+            return disabledAny | checkStack(throwable.getCause());
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/plugins/PluginPrefs.java b/packages/SystemUI/src/com/android/systemui/plugins/PluginPrefs.java
new file mode 100644
index 000000000000..3671b3c1689f
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/plugins/PluginPrefs.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
+ * except in compliance with the License. You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the
+ * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+package com.android.systemui.plugins;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.util.ArraySet;
+
+import java.util.Set;
+
+/**
+ * Storage for all plugin actions in SharedPreferences.
+ *
+ * This allows the list of actions that the Tuner needs to search for to be generated
+ * instead of hard coded.
+ */
+public class PluginPrefs {
+
+    private static final String PREFS = "plugin_prefs";
+
+    private static final String PLUGIN_ACTIONS = "actions";
+    private static final String HAS_PLUGINS = "plugins";
+
+    private final Set mPluginActions;
+    private final SharedPreferences mSharedPrefs;
+
+    public PluginPrefs(Context context) {
+        mSharedPrefs = context.getSharedPreferences(PREFS, 0);
+        mPluginActions = new ArraySet<>(mSharedPrefs.getStringSet(PLUGIN_ACTIONS, null));
+    }
+
+    public Set getPluginList() {
+        return mPluginActions;
+    }
+
+    public synchronized void addAction(String action) {
+        if (mPluginActions.add(action)){
+            mSharedPrefs.edit().putStringSet(PLUGIN_ACTIONS, mPluginActions).commit();
+        }
+    }
+
+    public static boolean hasPlugins(Context context) {
+        return context.getSharedPreferences(PREFS, 0).getBoolean(HAS_PLUGINS, false);
+    }
+
+    public static void setHasPlugins(Context context) {
+        context.getSharedPreferences(PREFS, 0).edit().putBoolean(HAS_PLUGINS, true).commit();
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/plugins/VersionInfo.java b/packages/SystemUI/src/com/android/systemui/plugins/VersionInfo.java
new file mode 100644
index 000000000000..84f7761a8043
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/plugins/VersionInfo.java
@@ -0,0 +1,134 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
+ * except in compliance with the License. You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the
+ * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+package com.android.systemui.plugins;
+
+import com.android.systemui.plugins.annotations.Dependencies;
+import com.android.systemui.plugins.annotations.DependsOn;
+import com.android.systemui.plugins.annotations.ProvidesInterface;
+import com.android.systemui.plugins.annotations.Requirements;
+import com.android.systemui.plugins.annotations.Requires;
+
+import android.util.ArrayMap;
+
+public class VersionInfo {
+
+    private final ArrayMap, Version> mVersions = new ArrayMap<>();
+    private Class mDefault;
+
+    public boolean hasVersionInfo() {
+        return !mVersions.isEmpty();
+    }
+
+    public int getDefaultVersion() {
+        return mVersions.get(mDefault).mVersion;
+    }
+
+    public VersionInfo addClass(Class cls) {
+        if (mDefault == null) {
+            // The legacy default version is from the first class we add.
+            mDefault = cls;
+        }
+        addClass(cls, false);
+        return this;
+    }
+
+    private void addClass(Class cls, boolean required) {
+        ProvidesInterface provider = cls.getDeclaredAnnotation(ProvidesInterface.class);
+        if (provider != null) {
+            mVersions.put(cls, new Version(provider.version(), true));
+        }
+        Requires requires = cls.getDeclaredAnnotation(Requires.class);
+        if (requires != null) {
+            mVersions.put(requires.target(), new Version(requires.version(), required));
+        }
+        Requirements requirements = cls.getDeclaredAnnotation(Requirements.class);
+        if (requirements != null) {
+            for (Requires r : requirements.value()) {
+                mVersions.put(r.target(), new Version(r.version(), required));
+            }
+        }
+        DependsOn depends = cls.getDeclaredAnnotation(DependsOn.class);
+        if (depends != null) {
+            addClass(depends.target(), true);
+        }
+        Dependencies dependencies = cls.getDeclaredAnnotation(Dependencies.class);
+        if (dependencies != null) {
+            for (DependsOn d : dependencies.value()) {
+                addClass(d.target(), true);
+            }
+        }
+    }
+
+    public void checkVersion(VersionInfo plugin) throws InvalidVersionException {
+        ArrayMap, Version> versions = new ArrayMap<>(mVersions);
+        plugin.mVersions.forEach((aClass, version) -> {
+            Version v = versions.remove(aClass);
+            if (v == null) {
+                v = createVersion(aClass);
+            }
+            if (v == null) {
+                throw new InvalidVersionException(aClass.getSimpleName()
+                        + " does not provide an interface", false);
+            }
+            if (v.mVersion != version.mVersion) {
+                throw new InvalidVersionException(aClass, v.mVersion < version.mVersion, v.mVersion,
+                        version.mVersion);
+            }
+        });
+        versions.forEach((aClass, version) -> {
+            if (version.mRequired) {
+                throw new InvalidVersionException("Missing required dependency "
+                        + aClass.getSimpleName(), false);
+            }
+        });
+    }
+
+    private Version createVersion(Class cls) {
+        ProvidesInterface provider = cls.getDeclaredAnnotation(ProvidesInterface.class);
+        if (provider != null) {
+            return new Version(provider.version(), false);
+        }
+        return null;
+    }
+
+    public static class InvalidVersionException extends RuntimeException {
+        private final boolean mTooNew;
+
+        public InvalidVersionException(String str, boolean tooNew) {
+            super(str);
+            mTooNew = tooNew;
+        }
+
+        public InvalidVersionException(Class cls, boolean tooNew, int expected, int actual) {
+            super(cls.getSimpleName() + " expected version " + expected + " but had " + actual);
+            mTooNew = tooNew;
+        }
+
+        public boolean isTooNew() {
+            return mTooNew;
+        }
+    }
+
+    private static class Version {
+
+        private final int mVersion;
+        private final boolean mRequired;
+
+        public Version(int version, boolean required) {
+            mVersion = version;
+            mRequired = required;
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java b/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java
index 504678c7f1c5..1569b0ca0249 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java
@@ -86,6 +86,10 @@ public class QSPanel extends LinearLayout implements Tunable, Callback {
 
         setOrientation(VERTICAL);
 
+        mBrightnessView = LayoutInflater.from(context).inflate(
+                R.layout.quick_settings_brightness_dialog, this, false);
+        addView(mBrightnessView);
+
         setupTileLayout();
 
         mFooter = new QSFooter(this, context);
@@ -100,10 +104,6 @@ public class QSPanel extends LinearLayout implements Tunable, Callback {
 
         updateResources();
 
-        mBrightnessView = LayoutInflater.from(context).inflate(
-                R.layout.quick_settings_brightness_dialog, this, false);
-        addView(mBrightnessView);
-
         mBrightnessController = new BrightnessController(getContext(),
                 (ImageView) findViewById(R.id.brightness_icon),
                 (ToggleSliderView) findViewById(R.id.brightness_slider));
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMenuRow.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMenuRow.java
index 355022f9794c..534a71936b5a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMenuRow.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMenuRow.java
@@ -90,8 +90,7 @@ public class NotificationMenuRow extends FrameLayout
     protected void onAttachedToWindow() {
         super.onAttachedToWindow();
         Dependency.get(PluginManager.class).addPluginListener(
-                NotificationMenuRowProvider.ACTION, this,
-                NotificationMenuRowProvider.VERSION, false /* Allow multiple */);
+                this, NotificationMenuRowProvider.class, false /* Allow multiple */);
     }
 
     @Override
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaView.java
index 14f991997e35..b42c498ef05d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaView.java
@@ -261,9 +261,9 @@ public class KeyguardBottomAreaView extends FrameLayout implements View.OnClickL
         super.onAttachedToWindow();
         mAccessibilityController.addStateChangedCallback(this);
         Dependency.get(PluginManager.class).addPluginListener(RIGHT_BUTTON_PLUGIN,
-                mRightListener, IntentButtonProvider.VERSION, false /* Only allow one */);
+                mRightListener, IntentButtonProvider.class, false /* Only allow one */);
         Dependency.get(PluginManager.class).addPluginListener(LEFT_BUTTON_PLUGIN,
-                mLeftListener, IntentButtonProvider.VERSION, false /* Only allow one */);
+                mLeftListener, IntentButtonProvider.class, false /* Only allow one */);
         Dependency.get(TunerService.class).addTunable(this, LockscreenFragment.LOCKSCREEN_LEFT_BUTTON,
                 LockscreenFragment.LOCKSCREEN_RIGHT_BUTTON);
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarInflaterView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarInflaterView.java
index 5fb99dabfeab..720ca1499bf1 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarInflaterView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarInflaterView.java
@@ -138,8 +138,8 @@ public class NavigationBarInflaterView extends FrameLayout
         super.onAttachedToWindow();
         Dependency.get(TunerService.class).addTunable(this, NAV_BAR_VIEWS, NAV_BAR_LEFT,
                 NAV_BAR_RIGHT);
-        Dependency.get(PluginManager.class).addPluginListener(NavBarButtonProvider.ACTION, this,
-                NavBarButtonProvider.VERSION, true /* Allow multiple */);
+        Dependency.get(PluginManager.class).addPluginListener(this,
+                NavBarButtonProvider.class, true /* Allow multiple */);
     }
 
     @Override
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarView.java
index 5d13289ef9cc..ad875f13d27e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarView.java
@@ -783,8 +783,8 @@ public class NavigationBarView extends FrameLayout implements PluginListener