Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit dfa1c6c8 authored by TreeHugger Robot's avatar TreeHugger Robot Committed by Android (Google) Code Review
Browse files

Merge "SysUI Plugin ClassLoader work"

parents f9b5c2fa e96fa00e
Loading
Loading
Loading
Loading
+16 −41
Original line number Diff line number Diff line
@@ -14,12 +14,10 @@

package com.android.systemui.plugins;

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.ResolveInfo;
@@ -33,12 +31,10 @@ import android.view.LayoutInflater;

import com.android.internal.annotations.VisibleForTesting;

import dalvik.system.PathClassLoader;

import java.util.ArrayList;
import java.util.List;

public class PluginInstanceManager<T extends Plugin> extends BroadcastReceiver {
public class PluginInstanceManager<T extends Plugin> {

    private static final boolean DEBUG = false;

@@ -57,20 +53,21 @@ public class PluginInstanceManager<T extends Plugin> extends BroadcastReceiver {
    final PluginHandler mPluginHandler;
    private final boolean isDebuggable;
    private final PackageManager mPm;
    private final ClassLoaderFactory mClassLoaderFactory;
    private final PluginManager mManager;

    PluginInstanceManager(Context context, String action, PluginListener<T> listener,
            boolean allowMultiple, Looper looper, int version) {
            boolean allowMultiple, Looper looper, int version, PluginManager manager) {
        this(context, context.getPackageManager(), action, listener, allowMultiple, looper, version,
                Build.IS_DEBUGGABLE, new ClassLoaderFactory());
                manager, Build.IS_DEBUGGABLE);
    }

    @VisibleForTesting
    PluginInstanceManager(Context context, PackageManager pm, String action,
            PluginListener<T> listener, boolean allowMultiple, Looper looper, int version,
            boolean debuggable, ClassLoaderFactory classLoaderFactory) {
            PluginManager manager, boolean debuggable) {
        mMainHandler = new MainHandler(Looper.getMainLooper());
        mPluginHandler = new PluginHandler(looper);
        mManager = manager;
        mContext = context;
        mPm = pm;
        mAction = action;
@@ -78,44 +75,29 @@ public class PluginInstanceManager<T extends Plugin> extends BroadcastReceiver {
        mAllowMultiple = allowMultiple;
        mVersion = version;
        isDebuggable = debuggable;
        mClassLoaderFactory = classLoaderFactory;
    }

    public void startListening() {
    public void loadAll() {
        if (DEBUG) Log.d(TAG, "startListening");
        mPluginHandler.sendEmptyMessage(PluginHandler.QUERY_ALL);
        IntentFilter filter = new IntentFilter(Intent.ACTION_PACKAGE_ADDED);
        filter.addAction(Intent.ACTION_PACKAGE_CHANGED);
        filter.addAction(Intent.ACTION_PACKAGE_REMOVED);
        filter.addDataScheme("package");
        mContext.registerReceiver(this, filter);
        filter = new IntentFilter(Intent.ACTION_USER_UNLOCKED);
        mContext.registerReceiver(this, filter);
    }

    public void stopListening() {
    public void destroy() {
        if (DEBUG) Log.d(TAG, "stopListening");
        ArrayList<PluginInfo> plugins = new ArrayList<>(mPluginHandler.mPlugins);
        for (PluginInfo plugin : plugins) {
            mMainHandler.obtainMessage(MainHandler.PLUGIN_DISCONNECTED,
                    plugin.mPlugin).sendToTarget();
        }
        mContext.unregisterReceiver(this);
    }

    @Override
    public void onReceive(Context context, Intent intent) {
        if (DEBUG) Log.d(TAG, "onReceive " + intent);
        if (Intent.ACTION_USER_UNLOCKED.equals(intent.getAction())) {
            mPluginHandler.sendEmptyMessage(PluginHandler.QUERY_ALL);
        } else {
            Uri data = intent.getData();
            String pkgName = data.getEncodedSchemeSpecificPart();
            mPluginHandler.obtainMessage(PluginHandler.REMOVE_PKG, pkgName).sendToTarget();
            if (!Intent.ACTION_PACKAGE_REMOVED.equals(intent.getAction())) {
                mPluginHandler.obtainMessage(PluginHandler.QUERY_PKG, pkgName).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) {
@@ -179,12 +161,6 @@ public class PluginInstanceManager<T extends Plugin> extends BroadcastReceiver {
        }
    }

    static class ClassLoaderFactory {
        public ClassLoader createClassLoader(String path, ClassLoader base) {
            return new PathClassLoader(path, base);
        }
    }

    private class PluginHandler extends Handler {
        private static final int QUERY_ALL = 1;
        private static final int QUERY_PKG = 2;
@@ -279,8 +255,7 @@ public class PluginInstanceManager<T extends Plugin> extends BroadcastReceiver {
                    return null;
                }
                // Create our own ClassLoader so we can use our own code as the parent.
                ClassLoader classLoader = mClassLoaderFactory.createClassLoader(info.sourceDir,
                        getClass().getClassLoader());
                ClassLoader classLoader = mManager.getClassLoader(info.sourceDir, info.packageName);
                Context pluginContext = new PluginContextWrapper(
                        mContext.createApplicationContext(info, 0), classLoader);
                Class<?> pluginClass = Class.forName(cls, true, classLoader);
+101 −8
Original line number Diff line number Diff line
@@ -14,7 +14,11 @@

package com.android.systemui.plugins;

import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.net.Uri;
import android.os.Build;
import android.os.HandlerThread;
import android.os.Looper;
@@ -22,26 +26,31 @@ import android.util.ArrayMap;

import com.android.internal.annotations.VisibleForTesting;

import dalvik.system.PathClassLoader;

import java.lang.Thread.UncaughtExceptionHandler;
import java.util.Map;

/**
 * @see Plugin
 */
public class PluginManager {
public class PluginManager extends BroadcastReceiver {

    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 Context mContext;
    private final PluginInstanceManagerFactory mFactory;
    private final boolean isDebuggable;
    private final PluginPrefs mPluginPrefs;
    private ClassLoaderFilter mParentClassLoader;

    private PluginManager(Context context) {
        this(context, new PluginInstanceManagerFactory(), Build.IS_DEBUGGABLE,
                Thread.getDefaultUncaughtExceptionHandler());
        this(context, new PluginInstanceManagerFactory(),
                Build.IS_DEBUGGABLE, Thread.getDefaultUncaughtExceptionHandler());
    }

    @VisibleForTesting
@@ -72,9 +81,12 @@ public class PluginManager {
        }
        mPluginPrefs.addAction(action);
        PluginInstanceManager p = mFactory.createPluginInstanceManager(mContext, action, listener,
                allowMultiple, mBackgroundThread.getLooper(), version);
        p.startListening();
                allowMultiple, mBackgroundThread.getLooper(), version, this);
        p.loadAll();
        mPluginMap.put(listener, p);
        if (mPluginMap.size() == 1) {
            startListening();
        }
    }

    public void removePluginListener(PluginListener<?> listener) {
@@ -83,7 +95,68 @@ public class PluginManager {
            return;
        }
        if (!mPluginMap.containsKey(listener)) return;
        mPluginMap.remove(listener).stopListening();
        mPluginMap.remove(listener).destroy();
        if (mPluginMap.size() == 0) {
            stopListening();
        }
    }

    private void startListening() {
        IntentFilter filter = new IntentFilter(Intent.ACTION_PACKAGE_ADDED);
        filter.addAction(Intent.ACTION_PACKAGE_CHANGED);
        filter.addAction(Intent.ACTION_PACKAGE_REMOVED);
        filter.addDataScheme("package");
        mContext.registerReceiver(this, filter);
        filter = new IntentFilter(Intent.ACTION_USER_UNLOCKED);
        mContext.registerReceiver(this, filter);
    }

    private void stopListening() {
        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 {
            Uri data = intent.getData();
            String pkg = data.getEncodedSchemeSpecificPart();
            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 static PluginManager getInstance(Context context) {
@@ -97,9 +170,29 @@ public class PluginManager {
    public static class PluginInstanceManagerFactory {
        public <T extends Plugin> PluginInstanceManager createPluginInstanceManager(Context context,
                String action, PluginListener<T> listener, boolean allowMultiple, Looper looper,
                int version) {
                int version, PluginManager manager) {
            return new PluginInstanceManager(context, action, listener, allowMultiple, looper,
                    version);
                    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);
        }
    }

+12 −22
Original line number Diff line number Diff line
@@ -33,13 +33,11 @@ import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.pm.ResolveInfo;
import android.content.pm.ServiceInfo;
import android.net.Uri;
import android.os.HandlerThread;
import android.support.test.runner.AndroidJUnit4;
import android.test.suitebuilder.annotation.SmallTest;

import com.android.systemui.SysuiTestCase;
import com.android.systemui.plugins.PluginInstanceManager.ClassLoaderFactory;

import org.junit.After;
import org.junit.Before;
@@ -64,6 +62,7 @@ public class PluginInstanceManagerTest extends SysuiTestCase {
    private PackageManager mMockPm;
    private PluginListener mMockListener;
    private PluginInstanceManager mPluginInstanceManager;
    private PluginManager mMockManager;

    @Before
    public void setup() throws Exception {
@@ -72,9 +71,11 @@ public class PluginInstanceManagerTest extends SysuiTestCase {
        mContextWrapper = new MyContextWrapper(getContext());
        mMockPm = mock(PackageManager.class);
        mMockListener = mock(PluginListener.class);
        mMockManager = mock(PluginManager.class);
        when(mMockManager.getClassLoader(Mockito.any(), Mockito.any()))
                .thenReturn(getClass().getClassLoader());
        mPluginInstanceManager = new PluginInstanceManager(mContextWrapper, mMockPm, "myAction",
                mMockListener, true, mHandlerThread.getLooper(), 1, true,
                new TestClassLoaderFactory());
                mMockListener, true, mHandlerThread.getLooper(), 1, mMockManager, true);
        sMockPlugin = mock(Plugin.class);
        when(sMockPlugin.getVersion()).thenReturn(1);
    }
@@ -89,7 +90,7 @@ public class PluginInstanceManagerTest extends SysuiTestCase {
    public void testNoPlugins() {
        when(mMockPm.queryIntentServices(Mockito.any(), Mockito.anyInt())).thenReturn(
                Collections.emptyList());
        mPluginInstanceManager.startListening();
        mPluginInstanceManager.loadAll();

        waitForIdleSync(mPluginInstanceManager.mPluginHandler);
        waitForIdleSync(mPluginInstanceManager.mMainHandler);
@@ -112,7 +113,7 @@ public class PluginInstanceManagerTest extends SysuiTestCase {
    public void testPluginDestroy() {
        createPlugin(); // Get into valid created state.

        mPluginInstanceManager.stopListening();
        mPluginInstanceManager.destroy();

        waitForIdleSync(mPluginInstanceManager.mPluginHandler);
        waitForIdleSync(mPluginInstanceManager.mMainHandler);
@@ -127,7 +128,7 @@ public class PluginInstanceManagerTest extends SysuiTestCase {
        setupFakePmQuery();
        when(sMockPlugin.getVersion()).thenReturn(2);

        mPluginInstanceManager.startListening();
        mPluginInstanceManager.loadAll();

        waitForIdleSync(mPluginInstanceManager.mPluginHandler);
        waitForIdleSync(mPluginInstanceManager.mMainHandler);
@@ -141,10 +142,7 @@ public class PluginInstanceManagerTest extends SysuiTestCase {
    public void testReloadOnChange() {
        createPlugin(); // Get into valid created state.

        // Send a package changed broadcast.
        Intent i = new Intent(Intent.ACTION_PACKAGE_CHANGED,
                Uri.fromParts("package", "com.android.systemui", null));
        mPluginInstanceManager.onReceive(mContextWrapper, i);
        mPluginInstanceManager.onPackageChange("com.android.systemui");

        waitForIdleSync(mPluginInstanceManager.mPluginHandler);
        waitForIdleSync(mPluginInstanceManager.mMainHandler);
@@ -164,11 +162,10 @@ public class PluginInstanceManagerTest extends SysuiTestCase {
    public void testNonDebuggable() {
        // Create a version that thinks the build is not debuggable.
        mPluginInstanceManager = new PluginInstanceManager(mContextWrapper, mMockPm, "myAction",
                mMockListener, true, mHandlerThread.getLooper(), 1, false,
                new TestClassLoaderFactory());
                mMockListener, true, mHandlerThread.getLooper(), 1, mMockManager, false);
        setupFakePmQuery();

        mPluginInstanceManager.startListening();
        mPluginInstanceManager.loadAll();

        waitForIdleSync(mPluginInstanceManager.mPluginHandler);
        waitForIdleSync(mPluginInstanceManager.mMainHandler);;
@@ -236,19 +233,12 @@ public class PluginInstanceManagerTest extends SysuiTestCase {
    private void createPlugin() {
        setupFakePmQuery();

        mPluginInstanceManager.startListening();
        mPluginInstanceManager.loadAll();

        waitForIdleSync(mPluginInstanceManager.mPluginHandler);
        waitForIdleSync(mPluginInstanceManager.mMainHandler);
    }

    private static class TestClassLoaderFactory extends ClassLoaderFactory {
        @Override
        public ClassLoader createClassLoader(String path, ClassLoader base) {
            return base;
        }
    }

    // Real context with no registering/unregistering of receivers.
    private static class MyContextWrapper extends ContextWrapper {
        public MyContextWrapper(Context base) {
+4 −4
Original line number Diff line number Diff line
@@ -51,7 +51,7 @@ public class PluginManagerTest extends SysuiTestCase {
        mMockFactory = mock(PluginInstanceManagerFactory.class);
        mMockPluginInstance = mock(PluginInstanceManager.class);
        when(mMockFactory.createPluginInstanceManager(Mockito.any(), Mockito.any(), Mockito.any(),
                Mockito.anyBoolean(), Mockito.any(), Mockito.anyInt()))
                Mockito.anyBoolean(), Mockito.any(), Mockito.anyInt(), Mockito.any()))
                .thenReturn(mMockPluginInstance);
        mPluginManager = new PluginManager(getContext(), mMockFactory, true, mMockExceptionHandler);
        resetExceptionHandler();
@@ -62,7 +62,7 @@ public class PluginManagerTest extends SysuiTestCase {
    public void testAddListener() {
        mPluginManager.addPluginListener("myAction", mMockListener, 1);

        verify(mMockPluginInstance).startListening();
        verify(mMockPluginInstance).loadAll();
    }

    @Test
@@ -70,7 +70,7 @@ public class PluginManagerTest extends SysuiTestCase {
        mPluginManager.addPluginListener("myAction", mMockListener, 1);

        mPluginManager.removePluginListener(mMockListener);
        verify(mMockPluginInstance).stopListening();
        verify(mMockPluginInstance).destroy();
    }

    @Test
@@ -80,7 +80,7 @@ public class PluginManagerTest extends SysuiTestCase {
        resetExceptionHandler();
        mPluginManager.addPluginListener("myAction", mMockListener, 1);

        verify(mMockPluginInstance, Mockito.never()).startListening();
        verify(mMockPluginInstance, Mockito.never()).loadAll();
    }

    @Test