OSDN Git Service

Add one-shot plugin support
authorJason Monk <jmonk@google.com>
Wed, 4 Jan 2017 20:13:11 +0000 (15:13 -0500)
committerJason Monk <jmonk@google.com>
Wed, 11 Jan 2017 16:29:17 +0000 (11:29 -0500)
Should have happened a while ago.

Test: runtest systemui
Change-Id: I0da4deb5c297e8030213810815a408364ec97e14

packages/SystemUI/AndroidManifest.xml
packages/SystemUI/plugin/src/com/android/systemui/plugins/PluginInstanceManager.java
packages/SystemUI/plugin/src/com/android/systemui/plugins/PluginManager.java
packages/SystemUI/src/com/android/systemui/SystemUIApplication.java
packages/SystemUI/src/com/android/systemui/SysuiRestartReceiver.java [new file with mode: 0644]
packages/SystemUI/src/com/android/systemui/doze/DozeFactory.java
packages/SystemUI/src/com/android/systemui/doze/DozeService.java
packages/SystemUI/tests/src/com/android/systemui/plugins/PluginInstanceManagerTest.java
packages/SystemUI/tests/src/com/android/systemui/plugins/PluginManagerTest.java

index 5574753..19c34ae 100644 (file)
             </intent-filter>
         </receiver>
 
+        <receiver android:name=".SysuiRestartReceiver"
+            android:exported="false">
+            <intent-filter>
+                <action android:name="com.android.systemui.action.RESTART" />
+
+                <data android:scheme="package" />
+            </intent-filter>
+        </receiver>
+
         <service android:name=".ImageWallpaper"
                 android:permission="android.permission.BIND_WALLPAPER"
                 android:exported="true" />
index 388c71d..7b8eae2 100644 (file)
@@ -87,6 +87,21 @@ public class PluginInstanceManager<T extends Plugin> {
         isDebuggable = debuggable;
     }
 
+    public PluginInfo<T> 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<T> 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);
@@ -366,11 +381,11 @@ public class PluginInstanceManager<T extends Plugin> {
         }
     }
 
-    private static class PluginInfo<T> {
+    static class PluginInfo<T> {
         private final Context mPluginContext;
-        private T mPlugin;
         private String mClass;
-        private String mPackage;
+        T mPlugin;
+        String mPackage;
 
         public PluginInfo(String pkg, String cls, T plugin, Context pluginContext) {
             mPlugin = plugin;
index 6096eaf..4714547 100644 (file)
 
 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;
@@ -23,16 +26,20 @@ 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;
 
@@ -54,11 +61,14 @@ public class PluginManager extends BroadcastReceiver {
     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;
 
     private PluginManager(Context context) {
         this(context, new PluginInstanceManagerFactory(),
@@ -80,6 +90,27 @@ public class PluginManager extends BroadcastReceiver {
         Thread.setDefaultUncaughtExceptionHandler(uncaughtExceptionHandler);
     }
 
+    public <T extends Plugin> 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<T> p = mFactory.createPluginInstanceManager(mContext, action, null,
+                false, mBackgroundThread.getLooper(), version, 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(String action, PluginListener<T> listener,
             int version) {
         addPluginListener(action, listener, version, false);
@@ -96,9 +127,7 @@ public class PluginManager extends BroadcastReceiver {
                 allowMultiple, mBackgroundThread.getLooper(), version, this);
         p.loadAll();
         mPluginMap.put(listener, p);
-        if (mPluginMap.size() == 1) {
-            startListening();
-        }
+        startListening();
     }
 
     public void removePluginListener(PluginListener<?> listener) {
@@ -108,12 +137,12 @@ public class PluginManager extends BroadcastReceiver {
         }
         if (!mPluginMap.containsKey(listener)) return;
         mPluginMap.remove(listener).destroy();
-        if (mPluginMap.size() == 0) {
-            stopListening();
-        }
+        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);
@@ -126,6 +155,9 @@ public class PluginManager extends BroadcastReceiver {
     }
 
     private void stopListening() {
+        // Never stop listening if a one-shot is present.
+        if (!mListening || mHasOneShot) return;
+        mListening = false;
         mContext.unregisterReceiver(this);
     }
 
@@ -147,6 +179,34 @@ public class PluginManager extends BroadcastReceiver {
         } 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)
+                        .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()) {
index d109ae1..1988023 100644 (file)
@@ -80,7 +80,6 @@ public class SystemUIApplication extends Application {
             ShortcutKeyDispatcher.class,
             VendorServices.class,
             LatencyTester.class,
-            DozeFactory.Initializer.class,
     };
 
     /**
diff --git a/packages/SystemUI/src/com/android/systemui/SysuiRestartReceiver.java b/packages/SystemUI/src/com/android/systemui/SysuiRestartReceiver.java
new file mode 100644 (file)
index 0000000..cdeef2f
--- /dev/null
@@ -0,0 +1,37 @@
+/*
+ * 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;
+
+import android.app.NotificationManager;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Process;
+
+import com.android.internal.messages.nano.SystemMessageProto.SystemMessage;
+
+public class SysuiRestartReceiver extends BroadcastReceiver {
+
+    public static String ACTION = "com.android.systemui.action.RESTART";
+
+    @Override
+    public void onReceive(Context context, Intent intent) {
+        if (ACTION.equals(intent.getAction())) {
+            String pkg = intent.getData().toString().substring(10);
+            NotificationManager.from(context).cancel(pkg, SystemMessage.NOTE_PLUGIN);
+            Process.killProcess(Process.myPid());
+        }
+    }
+}
index 5b10756..f0deaf0 100644 (file)
@@ -34,31 +34,10 @@ import com.android.systemui.statusbar.phone.DozeParameters;
 
 public class DozeFactory {
 
-    private static DozeFactory sInstance;
-
-    private DozeProvider mDozePlugin;
-
-    /** Returns the singleton instance. */
-    public static DozeFactory getInstance(Context context) {
-        if (sInstance == null) {
-            sInstance = new DozeFactory();
-            PluginManager.getInstance(context).addPluginListener(DozeProvider.ACTION,
-                    new PluginListener<DozeProvider>() {
-                        @Override
-                        public void onPluginConnected(DozeProvider plugin) {
-                            sInstance.mDozePlugin = plugin;
-                        }
-
-                        @Override
-                        public void onPluginDisconnected(DozeProvider plugin) {
-                            if (sInstance.mDozePlugin == plugin) {
-                                sInstance.mDozePlugin = null;
-                            }
-                        }
-                    },
-                    DozeProvider.VERSION, false /* Only one */);
-        }
-        return sInstance;
+    private final DozeProvider mDozePlugin;
+
+    public DozeFactory(DozeProvider plugin) {
+        mDozePlugin = plugin;
     }
 
     /** Creates a DozeMachine with its parts for {@code dozeService}. */
@@ -202,13 +181,4 @@ public class DozeFactory {
             return mInner.wrap(runnable);
         }
     }
-
-    /** Hack: We need to initialize the plugin listener before doze actually starts.
-     * This will be unnecessary once we have proper one-shot support */
-    public static class Initializer extends SystemUI {
-        @Override
-        public void start() {
-            getInstance(mContext);
-        }
-    }
 }
index 78b96b3..52f1fb4 100644 (file)
@@ -19,6 +19,10 @@ package com.android.systemui.doze;
 import android.service.dreams.DreamService;
 import android.util.Log;
 
+import com.android.systemui.plugins.Plugin;
+import com.android.systemui.plugins.PluginManager;
+import com.android.systemui.plugins.doze.DozeProvider;
+
 import java.io.FileDescriptor;
 import java.io.PrintWriter;
 
@@ -38,7 +42,9 @@ public class DozeService extends DreamService implements DozeMachine.Service {
 
         setWindowless(true);
 
-        mDozeMachine = DozeFactory.getInstance(getApplication()).assembleMachine(this);
+        DozeProvider provider = PluginManager.getInstance(this)
+                .getOneShotPlugin(DozeProvider.ACTION, DozeProvider.VERSION);
+        mDozeMachine = new DozeFactory(provider).assembleMachine(this);
     }
 
     @Override
index 01fd97c..d529ee1 100644 (file)
@@ -15,6 +15,7 @@
 package com.android.systemui.plugins;
 
 import static junit.framework.Assert.assertFalse;
+import static junit.framework.Assert.assertNotNull;
 import static junit.framework.Assert.assertTrue;
 
 import static org.mockito.Matchers.any;
@@ -39,11 +40,13 @@ import android.content.pm.ResolveInfo;
 import android.content.pm.ServiceInfo;
 import android.os.HandlerThread;
 import android.os.UserHandle;
+import android.support.test.annotation.UiThreadTest;
 import android.support.test.runner.AndroidJUnit4;
 import android.test.suitebuilder.annotation.SmallTest;
 
 import com.android.internal.messages.nano.SystemMessageProto.SystemMessage;
 import com.android.systemui.SysuiTestCase;
+import com.android.systemui.plugins.PluginInstanceManager.PluginInfo;
 
 import org.junit.After;
 import org.junit.Before;
@@ -92,6 +95,15 @@ public class PluginInstanceManagerTest extends SysuiTestCase {
         sMockPlugin = null;
     }
 
+    @UiThreadTest
+    @Test
+    public void testGetPlugin() throws Exception {
+        setupFakePmQuery();
+        PluginInfo p = mPluginInstanceManager.getPlugin();
+        assertNotNull(p.mPlugin);
+        verify(sMockPlugin).onCreate(any(), any());
+    }
+
     @Test
     public void testNoPlugins() throws Exception {
         when(mMockPm.queryIntentServices(any(), anyInt())).thenReturn(
index d5ada67..a58407b 100644 (file)
@@ -13,6 +13,8 @@
  */
 package com.android.systemui.plugins;
 
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
 import static org.mockito.Matchers.eq;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.verify;
@@ -24,11 +26,13 @@ import android.content.Context;
 import android.content.Intent;
 import android.content.pm.PackageManager;
 import android.net.Uri;
+import android.support.test.annotation.UiThreadTest;
 import android.support.test.runner.AndroidJUnit4;
 import android.test.suitebuilder.annotation.SmallTest;
 
 import com.android.internal.messages.nano.SystemMessageProto.SystemMessage;
 import com.android.systemui.SysuiTestCase;
+import com.android.systemui.plugins.PluginInstanceManager.PluginInfo;
 import com.android.systemui.plugins.PluginManager.PluginInstanceManagerFactory;
 
 import org.junit.Before;
@@ -66,6 +70,16 @@ public class PluginManagerTest extends SysuiTestCase {
         mMockListener = mock(PluginListener.class);
     }
 
+    @UiThreadTest
+    @Test
+    public void testOneShot() {
+        Plugin mockPlugin = mock(Plugin.class);
+        when(mMockPluginInstance.getPlugin()).thenReturn(new PluginInfo(null, null, mockPlugin,
+                null));
+        Plugin result = mPluginManager.getOneShotPlugin("myAction", 1);
+        assertTrue(result == mockPlugin);
+    }
+
     @Test
     public void testAddListener() {
         mPluginManager.addPluginListener("myAction", mMockListener, 1);
@@ -86,9 +100,12 @@ public class PluginManagerTest extends SysuiTestCase {
         mPluginManager = new PluginManager(getContext(), mMockFactory, false,
                 mMockExceptionHandler);
         resetExceptionHandler();
-        mPluginManager.addPluginListener("myAction", mMockListener, 1);
 
+        mPluginManager.addPluginListener("myAction", mMockListener, 1);
         verify(mMockPluginInstance, Mockito.never()).loadAll();
+
+        assertNull(mPluginManager.getOneShotPlugin("myPlugin", 1));
+        verify(mMockPluginInstance, Mockito.never()).getPlugin();
     }
 
     @Test