Move out test utilities to a Testables library

Test: runtest --path frameworks/base/tests/testablets/tests
        && runtest systemui

Change-Id: Ideef4aef5f26136b1741c556b9be5884f38842a0
diff --git a/packages/SystemUI/src/com/android/systemui/Dependency.java b/packages/SystemUI/src/com/android/systemui/Dependency.java
index bb7e19d..4dfaf45 100644
--- a/packages/SystemUI/src/com/android/systemui/Dependency.java
+++ b/packages/SystemUI/src/com/android/systemui/Dependency.java
@@ -30,6 +30,7 @@
 import com.android.systemui.plugins.ActivityStarter;
 import com.android.systemui.plugins.PluginDependencyProvider;
 import com.android.systemui.plugins.PluginManager;
+import com.android.systemui.plugins.PluginManagerImpl;
 import com.android.systemui.statusbar.phone.ConfigurationControllerImpl;
 import com.android.systemui.statusbar.phone.DarkIconDispatcherImpl;
 import com.android.systemui.statusbar.phone.ManagedProfileController;
@@ -73,6 +74,7 @@
 import com.android.systemui.statusbar.policy.ZenModeController;
 import com.android.systemui.statusbar.policy.ZenModeControllerImpl;
 import com.android.systemui.tuner.TunerService;
+import com.android.systemui.tuner.TunerServiceImpl;
 import com.android.systemui.util.leak.GarbageMonitor;
 import com.android.systemui.util.leak.LeakDetector;
 import com.android.systemui.util.leak.LeakReporter;
@@ -200,7 +202,7 @@
                 new DeviceProvisionedControllerImpl(mContext));
 
         mProviders.put(PluginManager.class, () ->
-                new PluginManager(mContext));
+                new PluginManagerImpl(mContext));
 
         mProviders.put(AssistManager.class, () ->
                 new AssistManager(getDependency(DeviceProvisionedController.class), mContext));
@@ -223,7 +225,7 @@
                 getDependency(LeakReporter.class)));
 
         mProviders.put(TunerService.class, () ->
-                new TunerService(mContext));
+                new TunerServiceImpl(mContext));
 
         mProviders.put(StatusBarWindowManager.class, () ->
                 new StatusBarWindowManager(mContext));
diff --git a/packages/SystemUI/src/com/android/systemui/plugins/PluginInstanceManager.java b/packages/SystemUI/src/com/android/systemui/plugins/PluginInstanceManager.java
index 79f78c9..07ac52d 100644
--- a/packages/SystemUI/src/com/android/systemui/plugins/PluginInstanceManager.java
+++ b/packages/SystemUI/src/com/android/systemui/plugins/PluginInstanceManager.java
@@ -62,10 +62,10 @@
     final PluginHandler mPluginHandler;
     private final boolean isDebuggable;
     private final PackageManager mPm;
-    private final PluginManager mManager;
+    private final PluginManagerImpl mManager;
 
     PluginInstanceManager(Context context, String action, PluginListener<T> listener,
-            boolean allowMultiple, Looper looper, VersionInfo version, PluginManager manager) {
+            boolean allowMultiple, Looper looper, VersionInfo version, PluginManagerImpl manager) {
         this(context, context.getPackageManager(), action, listener, allowMultiple, looper, version,
                 manager, Build.IS_DEBUGGABLE);
     }
@@ -73,7 +73,7 @@
     @VisibleForTesting
     PluginInstanceManager(Context context, PackageManager pm, String action,
             PluginListener<T> listener, boolean allowMultiple, Looper looper, VersionInfo version,
-            PluginManager manager, boolean debuggable) {
+            PluginManagerImpl manager, boolean debuggable) {
         mMainHandler = new MainHandler(Looper.getMainLooper());
         mPluginHandler = new PluginHandler(looper);
         mManager = manager;
@@ -346,7 +346,7 @@
                                 .setContentText("Check to see if an OTA is available.\n"
                                         + e.getMessage());
                     }
-                    Intent i = new Intent(PluginManager.DISABLE_PLUGIN).setData(
+                    Intent i = new Intent(PluginManagerImpl.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());
diff --git a/packages/SystemUI/src/com/android/systemui/plugins/PluginManager.java b/packages/SystemUI/src/com/android/systemui/plugins/PluginManager.java
index 9ad862d..298eaf1 100644
--- a/packages/SystemUI/src/com/android/systemui/plugins/PluginManager.java
+++ b/packages/SystemUI/src/com/android/systemui/plugins/PluginManager.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2016 The Android Open Source Project
+ * 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
@@ -14,276 +14,33 @@
 
 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.Handler;
-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.Dependency;
-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;
+public interface PluginManager {
 
-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";
+    String PLUGIN_CHANGED = "com.android.systemui.action.PLUGIN_CHANGED";
 
     // must be one of the channels created in NotificationChannels.java
-    static final String NOTIFICATION_CHANNEL_ID = "ALR";
+    String NOTIFICATION_CHANNEL_ID = "ALR";
 
-    private static PluginManager sInstance;
+    <T extends Plugin> T getOneShotPlugin(Class<T> cls);
+    <T extends Plugin> T getOneShotPlugin(String action, Class<?> cls);
 
-    private final HandlerThread mBackgroundThread;
-    private final ArrayMap<PluginListener<?>, PluginInstanceManager> mPluginMap
-            = new ArrayMap<>();
-    private final Map<String, ClassLoader> mClassLoaders = new ArrayMap<>();
-    private final ArraySet<String> 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;
+    <T extends Plugin> void addPluginListener(PluginListener<T> listener, Class<?> cls);
+    <T extends Plugin> void addPluginListener(PluginListener<T> listener, Class<?> cls,
+            boolean allowMultiple);
+    <T extends Plugin> void addPluginListener(String action, PluginListener<T> listener,
+            Class<?> cls);
+    <T extends Plugin> void addPluginListener(String action, PluginListener<T> listener,
+            Class cls, boolean allowMultiple);
 
-    public PluginManager(Context context) {
-        this(context, new PluginInstanceManagerFactory(),
-                Build.IS_DEBUGGABLE, Thread.getDefaultUncaughtExceptionHandler());
-    }
+    void removePluginListener(PluginListener<?> listener);
 
-    @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);
+    <T> boolean dependsOn(Plugin p, Class<T> cls);
 
-        PluginExceptionHandler uncaughtExceptionHandler = new PluginExceptionHandler(
-                defaultHandler);
-        Thread.setDefaultUncaughtExceptionHandler(uncaughtExceptionHandler);
-        if (isDebuggable) {
-            new Handler(mBackgroundThread.getLooper()).post(() -> {
-                // Plugin dependencies that don't have another good home can go here, but
-                // dependencies that have better places to init can happen elsewhere.
-                Dependency.get(PluginDependencyProvider.class)
-                        .allowPluginDependency(ActivityStarter.class);
-            });
-        }
-    }
-
-    public <T extends Plugin> T getOneShotPlugin(Class<T> 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 extends Plugin> 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<T> p = mFactory.createPluginInstanceManager(mContext, action, null,
-                false, mBackgroundThread.getLooper(), cls, this);
-        mPluginPrefs.addAction(action);
-        PluginInfo<T> info = p.getPlugin();
-        if (info != null) {
-            mOneShotPackages.add(info.mPackage);
-            mHasOneShot = true;
-            startListening();
-            return info.mPlugin;
-        }
-        return null;
-    }
-
-    public <T extends Plugin> void addPluginListener(PluginListener<T> listener, Class<?> cls) {
-        addPluginListener(listener, cls, false);
-    }
-
-    public <T extends Plugin> void addPluginListener(PluginListener<T> listener, Class<?> cls,
-            boolean allowMultiple) {
-        addPluginListener(getAction(cls), listener, cls, allowMultiple);
-    }
-
-    public <T extends Plugin> void addPluginListener(String action, PluginListener<T> listener,
-            Class<?> cls) {
-        addPluginListener(action, listener, cls, false);
-    }
-
-    public <T extends Plugin> void addPluginListener(String action, PluginListener<T> 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 getContext(ApplicationInfo info, String pkg) throws NameNotFoundException {
-        ClassLoader classLoader = getClassLoader(info.sourceDir, pkg);
-        return new PluginContextWrapper(mContext.createApplicationContext(info, 0), classLoader);
-    }
-
-    public static <P> String getAction(Class<P> cls) {
+    static <P> String getAction(Class<P> cls) {
         ProvidesInterface info = cls.getDeclaredAnnotation(ProvidesInterface.class);
         if (info == null) {
             throw new RuntimeException(cls + " doesn't provide an interface");
@@ -293,82 +50,4 @@
         }
         return info.action();
     }
-
-    public <T> boolean dependsOn(Plugin p, Class<T> cls) {
-        for (int i = 0; i < mPluginMap.size(); i++) {
-            if (mPluginMap.valueAt(i).dependsOn(p, cls)) {
-                return true;
-            }
-        }
-        return false;
-    }
-
-    @VisibleForTesting
-    public static class PluginInstanceManagerFactory {
-        public <T extends Plugin> PluginInstanceManager createPluginInstanceManager(Context context,
-                String action, PluginListener<T> 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/PluginManagerImpl.java b/packages/SystemUI/src/com/android/systemui/plugins/PluginManagerImpl.java
new file mode 100644
index 0000000..1fb6c87
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/plugins/PluginManagerImpl.java
@@ -0,0 +1,358 @@
+/*
+ * 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.Handler;
+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.Dependency;
+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 PluginManagerImpl extends BroadcastReceiver implements PluginManager {
+
+    static final String DISABLE_PLUGIN = "com.android.systemui.action.DISABLE_PLUGIN";
+
+    private static PluginManager sInstance;
+
+    private final HandlerThread mBackgroundThread;
+    private final ArrayMap<PluginListener<?>, PluginInstanceManager> mPluginMap
+            = new ArrayMap<>();
+    private final Map<String, ClassLoader> mClassLoaders = new ArrayMap<>();
+    private final ArraySet<String> 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 PluginManagerImpl(Context context) {
+        this(context, new PluginInstanceManagerFactory(),
+                Build.IS_DEBUGGABLE, Thread.getDefaultUncaughtExceptionHandler());
+    }
+
+    @VisibleForTesting
+    PluginManagerImpl(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);
+        if (isDebuggable) {
+            new Handler(mBackgroundThread.getLooper()).post(() -> {
+                // Plugin dependencies that don't have another good home can go here, but
+                // dependencies that have better places to init can happen elsewhere.
+                Dependency.get(PluginDependencyProvider.class)
+                        .allowPluginDependency(ActivityStarter.class);
+            });
+        }
+    }
+
+    public <T extends Plugin> T getOneShotPlugin(Class<T> 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 extends Plugin> 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<T> p = mFactory.createPluginInstanceManager(mContext, action, null,
+                false, mBackgroundThread.getLooper(), cls, this);
+        mPluginPrefs.addAction(action);
+        PluginInfo<T> info = p.getPlugin();
+        if (info != null) {
+            mOneShotPackages.add(info.mPackage);
+            mHasOneShot = true;
+            startListening();
+            return info.mPlugin;
+        }
+        return null;
+    }
+
+    public <T extends Plugin> void addPluginListener(PluginListener<T> listener, Class<?> cls) {
+        addPluginListener(listener, cls, false);
+    }
+
+    public <T extends Plugin> void addPluginListener(PluginListener<T> listener, Class<?> cls,
+            boolean allowMultiple) {
+        addPluginListener(PluginManager.getAction(cls), listener, cls, allowMultiple);
+    }
+
+    public <T extends Plugin> void addPluginListener(String action, PluginListener<T> listener,
+            Class<?> cls) {
+        addPluginListener(action, listener, cls, false);
+    }
+
+    public <T extends Plugin> void addPluginListener(String action, PluginListener<T> 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 getContext(ApplicationInfo info, String pkg) throws NameNotFoundException {
+        ClassLoader classLoader = getClassLoader(info.sourceDir, pkg);
+        return new PluginContextWrapper(mContext.createApplicationContext(info, 0), classLoader);
+    }
+
+    public <T> boolean dependsOn(Plugin p, Class<T> cls) {
+        for (int i = 0; i < mPluginMap.size(); i++) {
+            if (mPluginMap.valueAt(i).dependsOn(p, cls)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    @VisibleForTesting
+    public static class PluginInstanceManagerFactory {
+        public <T extends Plugin> PluginInstanceManager createPluginInstanceManager(Context context,
+                String action, PluginListener<T> listener, boolean allowMultiple, Looper looper,
+                Class<?> cls, PluginManagerImpl 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/tuner/TunerService.java b/packages/SystemUI/src/com/android/systemui/tuner/TunerService.java
index 7c4f2ee..369ce69 100644
--- a/packages/SystemUI/src/com/android/systemui/tuner/TunerService.java
+++ b/packages/SystemUI/src/com/android/systemui/tuner/TunerService.java
@@ -1,226 +1,94 @@
 /*
- * Copyright (C) 2015 The Android Open Source Project
+ * 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
+ * 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
+ * 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.tuner;
 
 import android.app.ActivityManager;
 import android.content.BroadcastReceiver;
 import android.content.ComponentName;
-import android.content.ContentResolver;
 import android.content.Context;
 import android.content.DialogInterface;
 import android.content.DialogInterface.OnClickListener;
 import android.content.Intent;
 import android.content.pm.PackageManager;
 import android.content.pm.PackageManager.NameNotFoundException;
-import android.content.pm.UserInfo;
-import android.database.ContentObserver;
-import android.net.Uri;
-import android.os.Handler;
-import android.os.Looper;
 import android.os.UserHandle;
-import android.os.UserManager;
 import android.provider.Settings;
-import android.provider.Settings.Secure;
-import android.text.TextUtils;
-import android.util.ArrayMap;
-import android.util.ArraySet;
 
 import static android.provider.Settings.System.SHOW_BATTERY_PERCENT;
 import com.android.systemui.DemoMode;
 import com.android.systemui.Dependency;
 import com.android.systemui.R;
-import com.android.systemui.SysUiServiceProvider;
-import com.android.systemui.SystemUI;
-import com.android.systemui.SystemUIApplication;
-import com.android.systemui.settings.CurrentUserTracker;
-import com.android.systemui.statusbar.phone.StatusBarIconController;
 import com.android.systemui.statusbar.phone.SystemUIDialog;
-import com.android.systemui.util.leak.LeakDetector;
 
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.Set;
-
-
-public class TunerService {
+public abstract class TunerService {
 
     public static final String ACTION_CLEAR = "com.android.systemui.action.CLEAR_TUNER";
 
-    private static final String TUNER_VERSION = "sysui_tuner_version";
+    public abstract void clearAll();
+    public abstract void destroy();
 
-    private static final int CURRENT_TUNER_VERSION = 1;
+    public abstract String getValue(String setting);
+    public abstract int getValue(String setting, int def);
+    public abstract String getValue(String setting, String def);
 
-    private final Observer mObserver = new Observer();
-    // Map of Uris we listen on to their settings keys.
-    private final ArrayMap<Uri, String> mListeningUris = new ArrayMap<>();
-    // Map of settings keys to the listener.
-    private final HashMap<String, Set<Tunable>> mTunableLookup = new HashMap<>();
-    // Set of all tunables, used for leak detection.
-    private final HashSet<Tunable> mTunables = LeakDetector.ENABLED ? new HashSet<>() : null;
-    private final Context mContext;
+    public abstract void setValue(String setting, String value);
+    public abstract void setValue(String setting, int value);
 
-    private ContentResolver mContentResolver;
-    private int mCurrentUser;
-    private CurrentUserTracker mUserTracker;
+    public abstract void addTunable(Tunable tunable, String... keys);
+    public abstract void removeTunable(Tunable tunable);
 
-    public TunerService(Context context) {
-        mContext = context;
-        mContentResolver = mContext.getContentResolver();
+    public interface Tunable {
+        void onTuningChanged(String key, String newValue);
+    }
 
-        for (UserInfo user : UserManager.get(mContext).getUsers()) {
-            mCurrentUser = user.getUserHandle().getIdentifier();
-            if (getValue(TUNER_VERSION, 0) != CURRENT_TUNER_VERSION) {
-                upgradeTuner(getValue(TUNER_VERSION, 0), CURRENT_TUNER_VERSION);
+    private static Context userContext(Context context) {
+        try {
+            return context.createPackageContextAsUser(context.getPackageName(), 0,
+                    new UserHandle(ActivityManager.getCurrentUser()));
+        } catch (NameNotFoundException e) {
+            return context;
+        }
+    }
+
+    public static final void setTunerEnabled(Context context, boolean enabled) {
+        userContext(context).getPackageManager().setComponentEnabledSetting(
+                new ComponentName(context, TunerActivity.class),
+                enabled ? PackageManager.COMPONENT_ENABLED_STATE_ENABLED
+                        : PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
+                PackageManager.DONT_KILL_APP);
+
+        userContext(context).getPackageManager().setComponentEnabledSetting(
+                new ComponentName(context, TunerActivity.ACTIVITY_ALIAS_NAME),
+                enabled ? PackageManager.COMPONENT_ENABLED_STATE_ENABLED
+                        : PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
+                PackageManager.DONT_KILL_APP);
+    }
+
+    public static final boolean isTunerEnabled(Context context) {
+        return userContext(context).getPackageManager().getComponentEnabledSetting(
+                new ComponentName(context, TunerActivity.class))
+                == PackageManager.COMPONENT_ENABLED_STATE_ENABLED;
+    }
+
+    public static class ClearReceiver extends BroadcastReceiver {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            if (ACTION_CLEAR.equals(intent.getAction())) {
+                Dependency.get(TunerService.class).clearAll();
             }
         }
-
-        mCurrentUser = ActivityManager.getCurrentUser();
-        mUserTracker = new CurrentUserTracker(mContext) {
-            @Override
-            public void onUserSwitched(int newUserId) {
-                mCurrentUser = newUserId;
-                reloadAll();
-                reregisterAll();
-            }
-        };
-        mUserTracker.startTracking();
-    }
-
-    public void destroy() {
-        mUserTracker.stopTracking();
-    }
-
-    private void upgradeTuner(int oldVersion, int newVersion) {
-        if (oldVersion < 1) {
-            String blacklistStr = getValue(StatusBarIconController.ICON_BLACKLIST);
-            if (blacklistStr != null) {
-                ArraySet<String> iconBlacklist =
-                        StatusBarIconController.getIconBlacklist(blacklistStr);
-
-                iconBlacklist.add("rotate");
-                iconBlacklist.add("headset");
-
-                Settings.Secure.putStringForUser(mContentResolver,
-                        StatusBarIconController.ICON_BLACKLIST,
-                        TextUtils.join(",", iconBlacklist), mCurrentUser);
-            }
-        }
-        setValue(TUNER_VERSION, newVersion);
-    }
-
-    public String getValue(String setting) {
-        return Settings.Secure.getStringForUser(mContentResolver, setting, mCurrentUser);
-    }
-
-    public void setValue(String setting, String value) {
-         Settings.Secure.putStringForUser(mContentResolver, setting, value, mCurrentUser);
-    }
-
-    public int getValue(String setting, int def) {
-        return Settings.Secure.getIntForUser(mContentResolver, setting, def, mCurrentUser);
-    }
-
-    public String getValue(String setting, String def) {
-        String ret = Secure.getStringForUser(mContentResolver, setting, mCurrentUser);
-        if (ret == null) return def;
-        return ret;
-    }
-
-    public void setValue(String setting, int value) {
-         Settings.Secure.putIntForUser(mContentResolver, setting, value, mCurrentUser);
-    }
-
-    public void addTunable(Tunable tunable, String... keys) {
-        for (String key : keys) {
-            addTunable(tunable, key);
-        }
-    }
-
-    private void addTunable(Tunable tunable, String key) {
-        if (!mTunableLookup.containsKey(key)) {
-            mTunableLookup.put(key, new ArraySet<Tunable>());
-        }
-        mTunableLookup.get(key).add(tunable);
-        if (LeakDetector.ENABLED) {
-            mTunables.add(tunable);
-            Dependency.get(LeakDetector.class).trackCollection(mTunables, "TunerService.mTunables");
-        }
-        Uri uri = Settings.Secure.getUriFor(key);
-        if (!mListeningUris.containsKey(uri)) {
-            mListeningUris.put(uri, key);
-            mContentResolver.registerContentObserver(uri, false, mObserver, mCurrentUser);
-        }
-        // Send the first state.
-        String value = Settings.Secure.getStringForUser(mContentResolver, key, mCurrentUser);
-        tunable.onTuningChanged(key, value);
-    }
-
-    public void removeTunable(Tunable tunable) {
-        for (Set<Tunable> list : mTunableLookup.values()) {
-            list.remove(tunable);
-        }
-        if (LeakDetector.ENABLED) {
-            mTunables.remove(tunable);
-        }
-    }
-
-    protected void reregisterAll() {
-        if (mListeningUris.size() == 0) {
-            return;
-        }
-        mContentResolver.unregisterContentObserver(mObserver);
-        for (Uri uri : mListeningUris.keySet()) {
-            mContentResolver.registerContentObserver(uri, false, mObserver, mCurrentUser);
-        }
-    }
-
-    public void reloadSetting(Uri uri) {
-        String key = mListeningUris.get(uri);
-        Set<Tunable> tunables = mTunableLookup.get(key);
-        if (tunables == null) {
-            return;
-        }
-        String value = Settings.Secure.getStringForUser(mContentResolver, key, mCurrentUser);
-        for (Tunable tunable : tunables) {
-            tunable.onTuningChanged(key, value);
-        }
-    }
-
-    private void reloadAll() {
-        for (String key : mTunableLookup.keySet()) {
-            String value = Settings.Secure.getStringForUser(mContentResolver, key,
-                    mCurrentUser);
-            for (Tunable tunable : mTunableLookup.get(key)) {
-                tunable.onTuningChanged(key, value);
-            }
-        }
-    }
-
-    public void clearAll() {
-        // A couple special cases.
-        Settings.Global.putString(mContentResolver, DemoMode.DEMO_MODE_ALLOWED, null);
-        Settings.System.putString(mContentResolver,
-                SHOW_BATTERY_PERCENT, null);
-        Intent intent = new Intent(DemoMode.ACTION_DEMO);
-        intent.putExtra(DemoMode.EXTRA_COMMAND, DemoMode.COMMAND_EXIT);
-        mContext.sendBroadcast(intent);
-
-        for (String key : mTunableLookup.keySet()) {
-            Settings.Secure.putString(mContentResolver, key, null);
-        }
     }
 
     public static final void showResetRequest(final Context context, final Runnable onDisabled) {
@@ -247,59 +115,4 @@
         });
         dialog.show();
     }
-
-    public static final void setTunerEnabled(Context context, boolean enabled) {
-        userContext(context).getPackageManager().setComponentEnabledSetting(
-                new ComponentName(context, TunerActivity.class),
-                enabled ? PackageManager.COMPONENT_ENABLED_STATE_ENABLED
-                        : PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
-                        PackageManager.DONT_KILL_APP);
-
-        userContext(context).getPackageManager().setComponentEnabledSetting(
-                new ComponentName(context, TunerActivity.ACTIVITY_ALIAS_NAME),
-                enabled ? PackageManager.COMPONENT_ENABLED_STATE_ENABLED
-                        : PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
-                PackageManager.DONT_KILL_APP);
-    }
-
-    public static final boolean isTunerEnabled(Context context) {
-        return userContext(context).getPackageManager().getComponentEnabledSetting(
-                new ComponentName(context, TunerActivity.class))
-                == PackageManager.COMPONENT_ENABLED_STATE_ENABLED;
-    }
-
-    private static Context userContext(Context context) {
-        try {
-            return context.createPackageContextAsUser(context.getPackageName(), 0,
-                    new UserHandle(ActivityManager.getCurrentUser()));
-        } catch (NameNotFoundException e) {
-            return context;
-        }
-    }
-
-    private class Observer extends ContentObserver {
-        public Observer() {
-            super(new Handler(Looper.getMainLooper()));
-        }
-
-        @Override
-        public void onChange(boolean selfChange, Uri uri, int userId) {
-            if (userId == ActivityManager.getCurrentUser()) {
-                reloadSetting(uri);
-            }
-        }
-    }
-
-    public interface Tunable {
-        void onTuningChanged(String key, String newValue);
-    }
-
-    public static class ClearReceiver extends BroadcastReceiver {
-        @Override
-        public void onReceive(Context context, Intent intent) {
-            if (ACTION_CLEAR.equals(intent.getAction())) {
-                Dependency.get(TunerService.class).clearAll();
-            }
-        }
-    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/tuner/TunerServiceImpl.java b/packages/SystemUI/src/com/android/systemui/tuner/TunerServiceImpl.java
new file mode 100644
index 0000000..8e584bc
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/tuner/TunerServiceImpl.java
@@ -0,0 +1,242 @@
+/*
+ * Copyright (C) 2015 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.tuner;
+
+import android.app.ActivityManager;
+import android.content.BroadcastReceiver;
+import android.content.ComponentName;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnClickListener;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.pm.UserInfo;
+import android.database.ContentObserver;
+import android.net.Uri;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.provider.Settings;
+import android.provider.Settings.Secure;
+import android.text.TextUtils;
+import android.util.ArrayMap;
+import android.util.ArraySet;
+
+import com.android.systemui.DemoMode;
+import com.android.systemui.Dependency;
+import com.android.systemui.R;
+import com.android.systemui.SysUiServiceProvider;
+import com.android.systemui.SystemUI;
+import com.android.systemui.SystemUIApplication;
+import com.android.systemui.settings.CurrentUserTracker;
+import com.android.systemui.statusbar.phone.StatusBarIconController;
+import com.android.systemui.statusbar.phone.SystemUIDialog;
+import com.android.systemui.util.leak.LeakDetector;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Set;
+
+
+public class TunerServiceImpl extends TunerService {
+
+    private static final String TUNER_VERSION = "sysui_tuner_version";
+
+    private static final int CURRENT_TUNER_VERSION = 1;
+
+    private final Observer mObserver = new Observer();
+    // Map of Uris we listen on to their settings keys.
+    private final ArrayMap<Uri, String> mListeningUris = new ArrayMap<>();
+    // Map of settings keys to the listener.
+    private final HashMap<String, Set<Tunable>> mTunableLookup = new HashMap<>();
+    // Set of all tunables, used for leak detection.
+    private final HashSet<Tunable> mTunables = LeakDetector.ENABLED ? new HashSet<>() : null;
+    private final Context mContext;
+
+    private ContentResolver mContentResolver;
+    private int mCurrentUser;
+    private CurrentUserTracker mUserTracker;
+
+    public TunerServiceImpl(Context context) {
+        mContext = context;
+        mContentResolver = mContext.getContentResolver();
+
+        for (UserInfo user : UserManager.get(mContext).getUsers()) {
+            mCurrentUser = user.getUserHandle().getIdentifier();
+            if (getValue(TUNER_VERSION, 0) != CURRENT_TUNER_VERSION) {
+                upgradeTuner(getValue(TUNER_VERSION, 0), CURRENT_TUNER_VERSION);
+            }
+        }
+
+        mCurrentUser = ActivityManager.getCurrentUser();
+        mUserTracker = new CurrentUserTracker(mContext) {
+            @Override
+            public void onUserSwitched(int newUserId) {
+                mCurrentUser = newUserId;
+                reloadAll();
+                reregisterAll();
+            }
+        };
+        mUserTracker.startTracking();
+    }
+
+    @Override
+    public void destroy() {
+        mUserTracker.stopTracking();
+    }
+
+    private void upgradeTuner(int oldVersion, int newVersion) {
+        if (oldVersion < 1) {
+            String blacklistStr = getValue(StatusBarIconController.ICON_BLACKLIST);
+            if (blacklistStr != null) {
+                ArraySet<String> iconBlacklist =
+                        StatusBarIconController.getIconBlacklist(blacklistStr);
+
+                iconBlacklist.add("rotate");
+                iconBlacklist.add("headset");
+
+                Settings.Secure.putStringForUser(mContentResolver,
+                        StatusBarIconController.ICON_BLACKLIST,
+                        TextUtils.join(",", iconBlacklist), mCurrentUser);
+            }
+        }
+        setValue(TUNER_VERSION, newVersion);
+    }
+
+    @Override
+    public String getValue(String setting) {
+        return Settings.Secure.getStringForUser(mContentResolver, setting, mCurrentUser);
+    }
+
+    @Override
+    public void setValue(String setting, String value) {
+         Settings.Secure.putStringForUser(mContentResolver, setting, value, mCurrentUser);
+    }
+
+    @Override
+    public int getValue(String setting, int def) {
+        return Settings.Secure.getIntForUser(mContentResolver, setting, def, mCurrentUser);
+    }
+
+    @Override
+    public String getValue(String setting, String def) {
+        String ret = Secure.getStringForUser(mContentResolver, setting, mCurrentUser);
+        if (ret == null) return def;
+        return ret;
+    }
+
+    @Override
+    public void setValue(String setting, int value) {
+         Settings.Secure.putIntForUser(mContentResolver, setting, value, mCurrentUser);
+    }
+
+    @Override
+    public void addTunable(Tunable tunable, String... keys) {
+        for (String key : keys) {
+            addTunable(tunable, key);
+        }
+    }
+
+    private void addTunable(Tunable tunable, String key) {
+        if (!mTunableLookup.containsKey(key)) {
+            mTunableLookup.put(key, new ArraySet<Tunable>());
+        }
+        mTunableLookup.get(key).add(tunable);
+        if (LeakDetector.ENABLED) {
+            mTunables.add(tunable);
+            Dependency.get(LeakDetector.class).trackCollection(mTunables, "TunerService.mTunables");
+        }
+        Uri uri = Settings.Secure.getUriFor(key);
+        if (!mListeningUris.containsKey(uri)) {
+            mListeningUris.put(uri, key);
+            mContentResolver.registerContentObserver(uri, false, mObserver, mCurrentUser);
+        }
+        // Send the first state.
+        String value = Settings.Secure.getStringForUser(mContentResolver, key, mCurrentUser);
+        tunable.onTuningChanged(key, value);
+    }
+
+    @Override
+    public void removeTunable(Tunable tunable) {
+        for (Set<Tunable> list : mTunableLookup.values()) {
+            list.remove(tunable);
+        }
+        if (LeakDetector.ENABLED) {
+            mTunables.remove(tunable);
+        }
+    }
+
+    protected void reregisterAll() {
+        if (mListeningUris.size() == 0) {
+            return;
+        }
+        mContentResolver.unregisterContentObserver(mObserver);
+        for (Uri uri : mListeningUris.keySet()) {
+            mContentResolver.registerContentObserver(uri, false, mObserver, mCurrentUser);
+        }
+    }
+
+    private void reloadSetting(Uri uri) {
+        String key = mListeningUris.get(uri);
+        Set<Tunable> tunables = mTunableLookup.get(key);
+        if (tunables == null) {
+            return;
+        }
+        String value = Settings.Secure.getStringForUser(mContentResolver, key, mCurrentUser);
+        for (Tunable tunable : tunables) {
+            tunable.onTuningChanged(key, value);
+        }
+    }
+
+    private void reloadAll() {
+        for (String key : mTunableLookup.keySet()) {
+            String value = Settings.Secure.getStringForUser(mContentResolver, key,
+                    mCurrentUser);
+            for (Tunable tunable : mTunableLookup.get(key)) {
+                tunable.onTuningChanged(key, value);
+            }
+        }
+    }
+
+    @Override
+    public void clearAll() {
+        // A couple special cases.
+        Settings.Global.putString(mContentResolver, DemoMode.DEMO_MODE_ALLOWED, null);
+        Intent intent = new Intent(DemoMode.ACTION_DEMO);
+        intent.putExtra(DemoMode.EXTRA_COMMAND, DemoMode.COMMAND_EXIT);
+        mContext.sendBroadcast(intent);
+
+        for (String key : mTunableLookup.keySet()) {
+            Settings.Secure.putString(mContentResolver, key, null);
+        }
+    }
+
+    private class Observer extends ContentObserver {
+        public Observer() {
+            super(new Handler(Looper.getMainLooper()));
+        }
+
+        @Override
+        public void onChange(boolean selfChange, Uri uri, int userId) {
+            if (userId == ActivityManager.getCurrentUser()) {
+                reloadSetting(uri);
+            }
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/util/LayoutInflaterBuilder.java b/packages/SystemUI/src/com/android/systemui/util/LayoutInflaterBuilder.java
deleted file mode 100644
index 5cfe677..0000000
--- a/packages/SystemUI/src/com/android/systemui/util/LayoutInflaterBuilder.java
+++ /dev/null
@@ -1,174 +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.util;
-
-import android.annotation.NonNull;
-import android.content.Context;
-import android.util.ArrayMap;
-import android.util.ArraySet;
-import android.util.AttributeSet;
-import android.util.Log;
-import android.view.LayoutInflater;
-import android.view.View;
-import java.util.Map;
-import java.util.Set;
-
-/**
- * Builder class to create a {@link LayoutInflater} with various properties.
- *
- * Call any desired configuration methods on the Builder and then use
- * {@link Builder#build} to create the LayoutInflater. This is an alternative to directly using
- * {@link LayoutInflater#setFilter} and {@link LayoutInflater#setFactory}.
- * @hide for use by framework
- */
-public class LayoutInflaterBuilder {
-    private static final String TAG = "LayoutInflaterBuilder";
-
-    private Context mFromContext;
-    private Context mTargetContext;
-    private Map<String, String> mReplaceMap;
-    private Set<Class> mDisallowedClasses;
-    private LayoutInflater mBuiltInflater;
-
-    /**
-     * Creates a new Builder which will construct a LayoutInflater.
-     *
-     * @param fromContext This context's LayoutInflater will be cloned by the Builder using
-     * {@link LayoutInflater#cloneInContext}. By default, the new LayoutInflater will point at
-     * this same Context.
-     */
-    public LayoutInflaterBuilder(@NonNull Context fromContext) {
-        mFromContext = fromContext;
-        mTargetContext = fromContext;
-        mReplaceMap = null;
-        mDisallowedClasses = null;
-        mBuiltInflater = null;
-    }
-
-    /**
-     * Instructs the Builder to point the LayoutInflater at a different Context.
-     *
-     * @param targetContext Context to be provided to
-     * {@link LayoutInflater#cloneInContext(Context)}.
-     * @return Builder object post-modification.
-     */
-    public LayoutInflaterBuilder target(@NonNull Context targetContext) {
-        assertIfAlreadyBuilt();
-        mTargetContext = targetContext;
-        return this;
-    }
-
-    /**
-     * Instructs the Builder to configure the LayoutInflater such that all instances
-     * of one {@link View} will be replaced with instances of another during inflation.
-     *
-     * @param from Instances of this class will be replaced during inflation.
-     * @param to Instances of this class will be inflated as replacements.
-     * @return Builder object post-modification.
-     */
-    public LayoutInflaterBuilder replace(@NonNull Class from, @NonNull Class to) {
-        return replace(from.getName(), to);
-    }
-
-    /**
-     * Instructs the Builder to configure the LayoutInflater such that all instances
-     * of one {@link View} will be replaced with instances of another during inflation.
-     *
-     * @param tag Instances of this tag will be replaced during inflation.
-     * @param to Instances of this class will be inflated as replacements.
-     * @return Builder object post-modification.
-     */
-    public LayoutInflaterBuilder replace(@NonNull String tag, @NonNull Class to) {
-        assertIfAlreadyBuilt();
-        if (mReplaceMap == null) {
-            mReplaceMap = new ArrayMap<String, String>();
-        }
-        mReplaceMap.put(tag, to.getName());
-        return this;
-    }
-
-    /**
-     * Instructs the Builder to configure the LayoutInflater such that any attempt to inflate
-     * a {@link View} of a given type will throw a {@link InflateException}.
-     *
-     * @param disallowedClass The Class type that will be disallowed.
-     * @return Builder object post-modification.
-     */
-    public LayoutInflaterBuilder disallow(@NonNull Class disallowedClass) {
-        assertIfAlreadyBuilt();
-        if (mDisallowedClasses == null) {
-            mDisallowedClasses = new ArraySet<Class>();
-        }
-        mDisallowedClasses.add(disallowedClass);
-        return this;
-    }
-
-    /**
-     * Builds and returns the LayoutInflater.  Afterwards, this Builder can no longer can be
-     * used, all future calls on the Builder will throw {@link AssertionError}.
-     */
-    public LayoutInflater build() {
-        assertIfAlreadyBuilt();
-        mBuiltInflater =
-                LayoutInflater.from(mFromContext).cloneInContext(mTargetContext);
-        setFactoryIfNeeded(mBuiltInflater);
-        setFilterIfNeeded(mBuiltInflater);
-        return mBuiltInflater;
-    }
-
-    private void assertIfAlreadyBuilt() {
-        if (mBuiltInflater != null) {
-            throw new AssertionError("Cannot use this Builder after build() has been called.");
-        }
-    }
-
-    private void setFactoryIfNeeded(LayoutInflater inflater) {
-        if (mReplaceMap == null) {
-            return;
-        }
-        inflater.setFactory(
-                new LayoutInflater.Factory() {
-                    @Override
-                    public View onCreateView(String name, Context context, AttributeSet attrs) {
-                        String replacingClassName = mReplaceMap.get(name);
-                        if (replacingClassName != null) {
-                            try {
-                                return inflater.createView(replacingClassName, null, attrs);
-                            } catch (ClassNotFoundException e) {
-                                Log.e(TAG, "Could not replace " + name
-                                        + " with " + replacingClassName
-                                        + ", Exception: ", e);
-                            }
-                        }
-                        return null;
-                    }
-                });
-    }
-
-    private void setFilterIfNeeded(LayoutInflater inflater) {
-        if (mDisallowedClasses == null) {
-            return;
-        }
-        inflater.setFilter(
-                new LayoutInflater.Filter() {
-                    @Override
-                    public boolean onLoadClass(Class clazz) {
-                        return !mDisallowedClasses.contains(clazz);
-                    }
-                });
-    }
-}