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

Commit ecc3306f authored by Dave Mankoff's avatar Dave Mankoff
Browse files

2/N Rename PluginInstanceManager to PluginActionManager.

Introduce PluginInstance.

PluginManager is in charge of coordinating the whole plugin system.

PluginActionManager is in charge of querying for the PackageManager
for plugins matching a specific action.

PluginActionManager creates PluginInstances for each package it finds,
and the PluginInstance is in charge of loading in the actual plugin
into the system.

This is another step down the path of being able to manually load in
plugins into the code before the PluginManager is ready.

Bug: 194781951
Test: atest SystemUITests && manual
Change-Id: Id525f27e362c29625168b56dc55f2899cd3f8810
parent c08fc30d
Loading
Loading
Loading
Loading
+438 −0
Original line number Diff line number Diff line
/*
 * 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
 * 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.shared.plugins;

import android.app.LoadedApk;
import android.app.Notification;
import android.app.Notification.Action;
import android.app.NotificationManager;
@@ -29,8 +30,6 @@ import android.content.pm.PackageManager.NameNotFoundException;
import android.content.pm.ResolveInfo;
import android.content.res.Resources;
import android.net.Uri;
import android.text.TextUtils;
import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.Log;
import android.view.LayoutInflater;
@@ -38,19 +37,22 @@ import android.view.LayoutInflater;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.messages.nano.SystemMessageProto.SystemMessage;
import com.android.systemui.plugins.Plugin;
import com.android.systemui.plugins.PluginFragment;
import com.android.systemui.plugins.PluginListener;
import com.android.systemui.shared.plugins.VersionInfo.InvalidVersionException;

import dalvik.system.PathClassLoader;

import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Executor;

public class PluginInstanceManager<T extends Plugin> {
/**
 * Coordinates all the available plugins for a given action.
 *
 * The available plugins are queried from the {@link PackageManager} via an an {@link Intent}
 * action.
 *
 * @param <T> The type of plugin that this contains.
 */
public class PluginActionManager<T extends Plugin> {

    private static final boolean DEBUG = false;

@@ -61,29 +63,36 @@ public class PluginInstanceManager<T extends Plugin> {
    private final PluginListener<T> mListener;
    private final String mAction;
    private final boolean mAllowMultiple;
    private final VersionInfo mVersion;
    private final NotificationManager mNotificationManager;
    private final PluginEnabler mPluginEnabler;
    private final InstanceFactory<T> mInstanceFactory;
    private final PluginInstance.Factory mPluginInstanceFactory;
    private final ArraySet<String> mPrivilegedPlugins = new ArraySet<>();
    private final Map<String, ClassLoader> mClassLoaders = new ArrayMap<>();

    @VisibleForTesting
    private final ArrayList<PluginInfo<T>> mPlugins = new ArrayList<>();
    private final ArrayList<PluginInstance<T>> mPluginInstances = new ArrayList<>();
    private final boolean mIsDebuggable;
    private final PackageManager mPm;
    private final Class<T> mPluginClass;
    private final PluginInitializer mInitializer;
    private final Executor mMainExecutor;
    private final Executor mBgExecutor;

    private PluginManagerImpl.ClassLoaderFilter mParentClassLoader;

    private PluginInstanceManager(Context context, PackageManager pm, String action,
            PluginListener<T> listener, boolean allowMultiple, Executor mainExecutor,
            Executor bgExecutor, VersionInfo version, boolean debuggable,
            PluginInitializer initializer, NotificationManager notificationManager,
            PluginEnabler pluginEnabler, List<String> privilegedPlugins,
            InstanceFactory<T> instanceFactory) {
    private PluginActionManager(
            Context context,
            PackageManager pm,
            String action,
            PluginListener<T> listener,
            Class<T> pluginClass,
            boolean allowMultiple,
            Executor mainExecutor,
            Executor bgExecutor,
            boolean debuggable,
            PluginInitializer initializer,
            NotificationManager notificationManager,
            PluginEnabler pluginEnabler,
            List<String> privilegedPlugins,
            PluginInstance.Factory pluginInstanceFactory) {
        mPluginClass = pluginClass;
        mInitializer = initializer;
        mMainExecutor = mainExecutor;
        mBgExecutor = bgExecutor;
@@ -92,49 +101,56 @@ public class PluginInstanceManager<T extends Plugin> {
        mAction = action;
        mListener = listener;
        mAllowMultiple = allowMultiple;
        mVersion = version;
        mNotificationManager = notificationManager;
        mPluginEnabler = pluginEnabler;
        mInstanceFactory = instanceFactory;
        mPluginInstanceFactory = pluginInstanceFactory;
        mPrivilegedPlugins.addAll(privilegedPlugins);
        mIsDebuggable = debuggable;
    }

    /** Load all plugins matching this instance's action. */
    public void loadAll() {
        if (DEBUG) Log.d(TAG, "startListening");
        mBgExecutor.execute(this::queryAll);
    }

    /** Unload all plugins managed by this instance. */
    public void destroy() {
        if (DEBUG) Log.d(TAG, "stopListening");
        ArrayList<PluginInfo<T>> plugins = new ArrayList<>(mPlugins);
        for (PluginInfo<T> pluginInfo : plugins) {
            mMainExecutor.execute(() -> onPluginDisconnected(pluginInfo.mPlugin));
        ArrayList<PluginInstance<T>> plugins = new ArrayList<>(mPluginInstances);
        for (PluginInstance<T> plugInstance : plugins) {
            mMainExecutor.execute(() -> onPluginDisconnected(plugInstance));
        }
    }

    /** Unload all matching plugins managed by this instance. */
    public void onPackageRemoved(String pkg) {
        mBgExecutor.execute(() -> removePkg(pkg));
    }

    public void onPackageChange(String pkg) {
        mBgExecutor.execute(() -> removePkg(pkg));
        mBgExecutor.execute(() -> queryPkg(pkg));
    /** Unload and then reload all matching plugins managed by this instance. */
    public void reloadPackage(String pkg) {
        mBgExecutor.execute(() -> {
            removePkg(pkg);
            queryPkg(pkg);
        });
    }

    /** Disable a specific plugin managed by this instance. */
    public boolean checkAndDisable(String className) {
        boolean disableAny = false;
        ArrayList<PluginInfo<T>> plugins = new ArrayList<>(mPlugins);
        for (PluginInfo<T> info : plugins) {
            if (className.startsWith(info.mPackage)) {
        ArrayList<PluginInstance<T>> plugins = new ArrayList<>(mPluginInstances);
        for (PluginInstance<T> info : plugins) {
            if (className.startsWith(info.getPackage())) {
                disableAny |= disable(info, PluginEnabler.DISABLED_FROM_EXPLICIT_CRASH);
            }
        }
        return disableAny;
    }

    /** Disable all plugins managed by this instance. */
    public boolean disableAll() {
        ArrayList<PluginInfo<T>> plugins = new ArrayList<>(mPlugins);
        ArrayList<PluginInstance<T>> plugins = new ArrayList<>(mPluginInstances);
        boolean disabledAny = false;
        for (int i = 0; i < plugins.size(); i++) {
            disabledAny |= disable(plugins.get(i), PluginEnabler.DISABLED_FROM_SYSTEM_CRASH);
@@ -142,21 +158,7 @@ public class PluginInstanceManager<T extends Plugin> {
        return disabledAny;
    }

    private boolean isPluginPackagePrivileged(String packageName) {
        for (String componentNameOrPackage : mPrivilegedPlugins) {
            ComponentName componentName = ComponentName.unflattenFromString(componentNameOrPackage);
            if (componentName != null) {
                if (componentName.getPackageName().equals(packageName)) {
                    return true;
                }
            } else if (componentNameOrPackage.equals(packageName)) {
                return true;
            }
        }
        return false;
    }

    private boolean isPluginPrivileged(ComponentName pluginName) {
    boolean isPluginPrivileged(ComponentName pluginName) {
        for (String componentNameOrPackage : mPrivilegedPlugins) {
            ComponentName componentName = ComponentName.unflattenFromString(componentNameOrPackage);
            if (componentName == null) {
@@ -172,16 +174,17 @@ public class PluginInstanceManager<T extends Plugin> {
        return false;
    }

    private boolean disable(PluginInfo<T> info, @PluginEnabler.DisableReason int reason) {
    private boolean disable(
            PluginInstance<T> pluginInstance, @PluginEnabler.DisableReason int reason) {
        // Live by the sword, die by the sword.
        // Misbehaving plugins get disabled and won't come back until uninstall/reinstall.

        ComponentName pluginComponent = new ComponentName(info.mPackage, info.mClass);
        ComponentName pluginComponent = pluginInstance.getComponentName();
        // If a plugin is detected in the stack of a crash then this will be called for that
        // plugin, if the plugin causing a crash cannot be identified, they are all disabled
        // assuming one of them must be bad.
        if (isPluginPrivileged(pluginComponent)) {
            // Don't disable whitelisted plugins as they are a part of the OS.
            // Don't disable privileged plugins as they are a part of the OS.
            return false;
        }
        Log.w(TAG, "Disabling plugin " + pluginComponent.flattenToShortString());
@@ -191,10 +194,10 @@ public class PluginInstanceManager<T extends Plugin> {
    }

    <C> boolean dependsOn(Plugin p, Class<C> cls) {
        ArrayList<PluginInfo<T>> plugins = new ArrayList<>(mPlugins);
        for (PluginInfo<T> info : plugins) {
            if (info.mPlugin.getClass().getName().equals(p.getClass().getName())) {
                return info.mVersion != null && info.mVersion.hasClass(cls);
        ArrayList<PluginInstance<T>> instances = new ArrayList<>(mPluginInstances);
        for (PluginInstance<T> instance : instances) {
            if (instance.containsPluginClass(p.getClass())) {
                return instance.getVersionInfo() != null && instance.getVersionInfo().hasClass(cls);
            }
        }
        return false;
@@ -206,51 +209,41 @@ public class PluginInstanceManager<T extends Plugin> {
                getClass().getSimpleName(), hashCode(), mAction);
    }

    private void onPluginConnected(PluginInfo<T> pluginInfo) {
    private void onPluginConnected(PluginInstance<T> pluginInstance) {
        if (DEBUG) Log.d(TAG, "onPluginConnected");
        PluginPrefs.setHasPlugins(mContext);
        mInitializer.handleWtfs();
        if (!(pluginInfo.mPlugin instanceof PluginFragment)) {
            // Only call onCreate for plugins that aren't fragments, as fragments
            // will get the onCreate as part of the fragment lifecycle.
            pluginInfo.mPlugin.onCreate(mContext, pluginInfo.mPluginContext);
        }
        mListener.onPluginConnected(pluginInfo.mPlugin, pluginInfo.mPluginContext);
        pluginInstance.onCreate(mContext, mListener);
    }

    private void onPluginDisconnected(T plugin) {
    private void onPluginDisconnected(PluginInstance<T> pluginInstance) {
        if (DEBUG) Log.d(TAG, "onPluginDisconnected");
        mListener.onPluginDisconnected(plugin);
        if (!(plugin instanceof PluginFragment)) {
            // Only call onDestroy for plugins that aren't fragments, as fragments
            // will get the onDestroy as part of the fragment lifecycle.
            plugin.onDestroy();
        }
        pluginInstance.onDestroy(mListener);
    }

    private void queryAll() {
        if (DEBUG) Log.d(TAG, "queryAll " + mAction);
        for (int i = mPlugins.size() - 1; i >= 0; i--) {
            PluginInfo<T> pluginInfo = mPlugins.get(i);
            mMainExecutor.execute(() -> onPluginDisconnected(pluginInfo.mPlugin));
        for (int i = mPluginInstances.size() - 1; i >= 0; i--) {
            PluginInstance<T> pluginInstance = mPluginInstances.get(i);
            mMainExecutor.execute(() -> onPluginDisconnected(pluginInstance));
        }
        mPlugins.clear();
        mPluginInstances.clear();
        handleQueryPlugins(null);
    }

    private void removePkg(String pkg) {
        for (int i = mPlugins.size() - 1; i >= 0; i--) {
            final PluginInfo<T> pluginInfo = mPlugins.get(i);
            if (pluginInfo.mPackage.equals(pkg)) {
                mMainExecutor.execute(() -> onPluginDisconnected(pluginInfo.mPlugin));
                mPlugins.remove(i);
        for (int i = mPluginInstances.size() - 1; i >= 0; i--) {
            final PluginInstance<T> pluginInstance = mPluginInstances.get(i);
            if (pluginInstance.getPackage().equals(pkg)) {
                mMainExecutor.execute(() -> onPluginDisconnected(pluginInstance));
                mPluginInstances.remove(i);
            }
        }
    }

    private void queryPkg(String pkg) {
        if (DEBUG) Log.d(TAG, "queryPkg " + pkg + " " + mAction);
        if (mAllowMultiple || (mPlugins.size() == 0)) {
        if (mAllowMultiple || (mPluginInstances.size() == 0)) {
            handleQueryPlugins(pkg);
        } else {
            if (DEBUG) Log.d(TAG, "Too many of " + mAction);
@@ -281,16 +274,16 @@ public class PluginInstanceManager<T extends Plugin> {
        for (ResolveInfo info : result) {
            ComponentName name = new ComponentName(info.serviceInfo.packageName,
                    info.serviceInfo.name);
            PluginInfo<T> pluginInfo = handleLoadPlugin(name);
            if (pluginInfo == null) continue;

            PluginInstance<T> pluginInstance = loadPluginComponent(name);
            if (pluginInstance != null) {
                // add plugin before sending PLUGIN_CONNECTED message
            mPlugins.add(pluginInfo);
            mMainExecutor.execute(() -> onPluginConnected(pluginInfo));
                mPluginInstances.add(pluginInstance);
                mMainExecutor.execute(() -> onPluginConnected(pluginInstance));
            }
        }
    }

    protected PluginInfo<T> handleLoadPlugin(ComponentName component) {
    private PluginInstance<T> loadPluginComponent(ComponentName component) {
        // This was already checked, but do it again here to make extra extra sure, we don't
        // use these on production builds.
        if (!mIsDebuggable && !isPluginPrivileged(component)) {
@@ -299,32 +292,43 @@ public class PluginInstanceManager<T extends Plugin> {
            return null;
        }
        if (!mPluginEnabler.isEnabled(component)) {
            if (DEBUG) Log.d(TAG, "Plugin is not enabled, aborting load: " + component);
            if (DEBUG) {
                Log.d(TAG, "Plugin is not enabled, aborting load: " + component);
            }
            return null;
        }
        String pkg = component.getPackageName();
        String cls = component.getClassName();
        String packageName = component.getPackageName();
        try {
            ApplicationInfo info = mPm.getApplicationInfo(pkg, 0);
            // TODO: This probably isn't needed given that we don't have IGNORE_SECURITY on
            if (mPm.checkPermission(PLUGIN_PERMISSION, pkg)
            if (mPm.checkPermission(PLUGIN_PERMISSION, packageName)
                    != PackageManager.PERMISSION_GRANTED) {
                Log.d(TAG, "Plugin doesn't have permission: " + pkg);
                Log.d(TAG, "Plugin doesn't have permission: " + packageName);
                return null;
            }
            // Create our own ClassLoader so we can use our own code as the parent.
            ClassLoader classLoader = getClassLoader(info);
            Context pluginContext = new PluginContextWrapper(
                    mContext.createApplicationContext(info, 0), classLoader);
            Class<?> pluginClass = Class.forName(cls, true, classLoader);

            ApplicationInfo appInfo = mPm.getApplicationInfo(packageName, 0);
            // TODO: Only create the plugin before version check if we need it for
            // legacy version check.
            T plugin = mInstanceFactory.create(pluginClass);
            if (DEBUG) {
                Log.d(TAG, "createPlugin");
            }
            try {
                VersionInfo version = checkVersion(pluginClass, plugin, mVersion);
                if (DEBUG) Log.d(TAG, "createPlugin");
                return new PluginInfo<>(pkg, cls, plugin, pluginContext, version);
                return mPluginInstanceFactory.create(
                        mContext, appInfo, component,
                        mPluginClass);
            } catch (InvalidVersionException e) {
                reportInvalidVersion(component, component.getClassName(), e);
            }
        } catch (Throwable e) {
            Log.w(TAG, "Couldn't load plugin: " + packageName, e);
            return null;
        }

        return null;
    }

    private void reportInvalidVersion(
            ComponentName component, String className, InvalidVersionException e) {
        final int icon = Resources.getSystem().getIdentifier(
                "stat_sys_warning", "drawable", "android");
        final int color = Resources.getSystem().getIdentifier(
@@ -337,10 +341,11 @@ public class PluginInstanceManager<T extends Plugin> {
                .setShowWhen(false)
                .setVisibility(Notification.VISIBILITY_PUBLIC)
                .setColor(mContext.getColor(color));
                String label = cls;
        String label = className;
        try {
            label = mPm.getServiceInfo(component, 0).loadLabel(mPm).toString();
        } catch (NameNotFoundException e2) {
            // no-op
        }
        if (!e.isTooNew()) {
            // Localization not required as this will never ever appear in a user build.
@@ -360,64 +365,12 @@ public class PluginInstanceManager<T extends Plugin> {
        nb.addAction(new Action.Builder(null, "Disable plugin", pi).build());
        mNotificationManager.notify(SystemMessage.NOTE_PLUGIN, nb.build());
        // TODO: Warn user.
                Log.w(TAG, "Plugin has invalid interface version " + plugin.getVersion()
                        + ", expected " + mVersion);
                return null;
            }
        } catch (Throwable e) {
            Log.w(TAG, "Couldn't load plugin: " + pkg, e);
            return null;
        }
    }

    private VersionInfo checkVersion(Class<?> pluginClass, T plugin, VersionInfo version)
            throws InvalidVersionException {
        VersionInfo pv = new VersionInfo().addClass(pluginClass);
        if (pv.hasVersionInfo()) {
            version.checkVersion(pv);
        } else {
            int fallbackVersion = plugin.getVersion();
            if (fallbackVersion != version.getDefaultVersion()) {
                throw new InvalidVersionException("Invalid legacy version", false);
            }
            return null;
        }
        return pv;
    }

    /** Returns class loader specific for the given plugin. */
    public ClassLoader getClassLoader(ApplicationInfo appInfo) {
        if (!mIsDebuggable && !isPluginPackagePrivileged(appInfo.packageName)) {
            Log.w(TAG, "Cannot get class loader for non-privileged plugin. Src:"
                    + appInfo.sourceDir + ", pkg: " + appInfo.packageName);
            return null;
        }
        if (mClassLoaders.containsKey(appInfo.packageName)) {
            return mClassLoaders.get(appInfo.packageName);
        }

        List<String> zipPaths = new ArrayList<>();
        List<String> libPaths = new ArrayList<>();
        LoadedApk.makePaths(null, true, appInfo, zipPaths, libPaths);
        ClassLoader classLoader = new PathClassLoader(
                TextUtils.join(File.pathSeparator, zipPaths),
                TextUtils.join(File.pathSeparator, libPaths),
                getParentClassLoader());
        mClassLoaders.put(appInfo.packageName, classLoader);
        return classLoader;
    }

    private ClassLoader getParentClassLoader() {
        if (mParentClassLoader == null) {
            // Lazily load this so it doesn't have any effect on devices without plugins.
            mParentClassLoader = new PluginManagerImpl.ClassLoaderFilter(
                    getClass().getClassLoader(), "com.android.systemui.plugin");
        }
        return mParentClassLoader;
        Log.w(TAG, "Plugin has invalid interface version " + e.getActualVersion()
                + ", expected " + e.getExpectedVersion());
    }

    /**
     * Construct a {@link PluginInstanceManager}
     * Construct a {@link PluginActionManager}
     */
    public static class Factory {
        private final Context mContext;
@@ -428,12 +381,12 @@ public class PluginInstanceManager<T extends Plugin> {
        private final NotificationManager mNotificationManager;
        private final PluginEnabler mPluginEnabler;
        private final List<String> mPrivilegedPlugins;
        private InstanceFactory<?> mInstanceFactory;
        private final PluginInstance.Factory mPluginInstanceFactory;

        public Factory(Context context, PackageManager packageManager,
                Executor mainExecutor, Executor bgExecutor, PluginInitializer initializer,
                NotificationManager notificationManager, PluginEnabler pluginEnabler,
                List<String> privilegedPlugins) {
                List<String> privilegedPlugins, PluginInstance.Factory pluginInstanceFactory) {
            mContext = context;
            mPackageManager = packageManager;
            mMainExecutor = mainExecutor;
@@ -442,26 +395,20 @@ public class PluginInstanceManager<T extends Plugin> {
            mNotificationManager = notificationManager;
            mPluginEnabler = pluginEnabler;
            mPrivilegedPlugins = privilegedPlugins;

            mInstanceFactory = new InstanceFactory<>();
        }

        @VisibleForTesting
        <T extends Plugin> Factory setInstanceFactory(InstanceFactory<T> instanceFactory) {
            mInstanceFactory = instanceFactory;
            return this;
            mPluginInstanceFactory = pluginInstanceFactory;
        }

        <T extends Plugin> PluginInstanceManager<T> create(
                String action, PluginListener<T> listener, boolean allowMultiple,
                VersionInfo version, boolean debuggable) {
            return new PluginInstanceManager<T>(mContext, mPackageManager, action, listener,
                    allowMultiple, mMainExecutor, mBgExecutor, version, debuggable,
                    mInitializer, mNotificationManager, mPluginEnabler,
                    mPrivilegedPlugins, (InstanceFactory<T>) mInstanceFactory);
        <T extends Plugin> PluginActionManager<T> create(
                String action, PluginListener<T> listener, Class<T> pluginClass,
                boolean allowMultiple, boolean debuggable) {
            return new PluginActionManager<>(mContext, mPackageManager, action, listener,
                    pluginClass, allowMultiple, mMainExecutor, mBgExecutor,
                    debuggable, mInitializer, mNotificationManager, mPluginEnabler,
                    mPrivilegedPlugins, mPluginInstanceFactory);
        }
    }

    /** */
    public static class PluginContextWrapper extends ContextWrapper {
        private final ClassLoader mClassLoader;
        private LayoutInflater mInflater;
@@ -488,26 +435,4 @@ public class PluginInstanceManager<T extends Plugin> {
        }
    }

    static class PluginInfo<T extends Plugin> {
        private final Context mPluginContext;
        private final VersionInfo mVersion;
        private final String mClass;
        T mPlugin;
        String mPackage;

        public PluginInfo(String pkg, String cls, T plugin, Context pluginContext,
                VersionInfo info) {
            mPlugin = plugin;
            mClass = cls;
            mPackage = pkg;
            mPluginContext = pluginContext;
            mVersion = info;
        }
    }

    static class InstanceFactory<T extends Plugin> {
        T create(Class cls) throws IllegalAccessException, InstantiationException {
            return (T) cls.newInstance();
        }
    }
}
+1 −1
Original line number Diff line number Diff line
@@ -28,7 +28,7 @@ public interface PluginInitializer {


    /**
     * Called from {@link PluginInstanceManager}.
     * Called from {@link PluginActionManager}.
     */
    void handleWtfs();
}
+228 −0

File added.

Preview size limit exceeded, changes collapsed.

+6 −4
Original line number Diff line number Diff line
@@ -30,13 +30,15 @@ public interface PluginManager {
    /** Returns plugins that don't get disabled when an exceptoin occurs. */
    String[] getPrivilegedPlugins();

    <T extends Plugin> void addPluginListener(PluginListener<T> listener, Class<?> cls);
    <T extends Plugin> void addPluginListener(PluginListener<T> listener, Class<?> cls,
    /** */
    <T extends Plugin> void addPluginListener(PluginListener<T> listener, Class<T> cls);
    /** */
    <T extends Plugin> void addPluginListener(PluginListener<T> listener, Class<T> cls,
            boolean allowMultiple);
    <T extends Plugin> void addPluginListener(String action, PluginListener<T> listener,
            Class<?> cls);
            Class<T> cls);
    <T extends Plugin> void addPluginListener(String action, PluginListener<T> listener,
            Class<?> cls, boolean allowMultiple);
            Class<T> cls, boolean allowMultiple);

    void removePluginListener(PluginListener<?> listener);

+22 −18

File changed.

Preview size limit exceeded, changes collapsed.

Loading