New system for versioning sysui plugins

Use annotations to handle the multi-dimensionalness of interface
versions, but still maintain compile time inclusion of current
versions.

Test: runtest systemui
Change-Id: I0789a72112cf6630a6406f76020071c8a6d9e24c
This commit is contained in:
Jason Monk
2017-02-08 20:45:10 -08:00
parent efdb428959
commit 5bec68fbea
32 changed files with 563 additions and 110 deletions

View File

@@ -24,7 +24,9 @@ import android.view.ViewGroup;
import android.view.ViewTreeObserver.InternalInsetsInfo;
import android.view.ViewTreeObserver.OnComputeInternalInsetsListener;
import com.android.systemui.plugins.OverlayPlugin;
import com.android.systemui.plugins.annotations.Requires;
@Requires(target = OverlayPlugin.class, version = OverlayPlugin.VERSION)
public class SampleOverlayPlugin implements OverlayPlugin {
private static final String TAG = "SampleOverlayPlugin";
private Context mPluginContext;
@@ -35,12 +37,6 @@ public class SampleOverlayPlugin implements OverlayPlugin {
private boolean mCollapseDesired;
private float mStatusBarHeight;
@Override
public int getVersion() {
Log.d(TAG, "getVersion " + VERSION);
return VERSION;
}
@Override
public void onCreate(Context sysuiContext, Context pluginContext) {
Log.d(TAG, "onCreate");

View File

@@ -14,6 +14,8 @@
package com.android.systemui.plugins;
import com.android.systemui.plugins.annotations.ProvidesInterface;
import android.content.Intent;
import android.graphics.drawable.Drawable;
@@ -21,6 +23,7 @@ import android.graphics.drawable.Drawable;
* An Intent Button represents a triggerable element in SysUI that consists of an
* Icon and an intent to trigger when it is activated (clicked, swiped, etc.).
*/
@ProvidesInterface(version = IntentButtonProvider.VERSION)
public interface IntentButtonProvider extends Plugin {
public static final int VERSION = 1;

View File

@@ -13,12 +13,15 @@
*/
package com.android.systemui.plugins;
import com.android.systemui.plugins.annotations.ProvidesInterface;
import android.view.View;
@ProvidesInterface(action = OverlayPlugin.ACTION, version = OverlayPlugin.VERSION)
public interface OverlayPlugin extends Plugin {
String ACTION = "com.android.systemui.action.PLUGIN_OVERLAY";
int VERSION = 1;
int VERSION = 2;
void setup(View statusBar, View navBar);

View File

@@ -13,6 +13,8 @@
*/
package com.android.systemui.plugins;
import com.android.systemui.plugins.annotations.Requires;
import android.content.Context;
/**
@@ -111,18 +113,13 @@ import android.content.Context;
public interface Plugin {
/**
* Should be implemented as the following directly referencing the version constant
* from the plugin interface being implemented, this will allow recompiles to automatically
* pick up the current version.
* <pre class="prettyprint">
* {@literal
* public int getVersion() {
* return VERSION;
* }
* }
* @return
* @deprecated
* @see Requires
*/
int getVersion();
default int getVersion() {
// Default of -1 indicates the plugin supports the new Requires model.
return -1;
}
default void onCreate(Context sysuiContext, Context pluginContext) {
}

View File

@@ -0,0 +1,27 @@
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
* except in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the
* License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/
package com.android.systemui.plugins.annotations;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
/**
* Used for repeated @DependsOn internally, not for plugin
* use.
*/
@Retention(RetentionPolicy.RUNTIME)
public @interface Dependencies {
DependsOn[] value();
}

View File

@@ -0,0 +1,32 @@
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
* except in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the
* License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/
package com.android.systemui.plugins.annotations;
import java.lang.annotation.Repeatable;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
/**
* Used to indicate that an interface in the plugin library needs another
* interface to function properly. When this is added, it will be enforced
* that all plugins that @Requires the annotated interface also @Requires
* the specified class as well.
*/
@Retention(RetentionPolicy.RUNTIME)
@Repeatable(value = Dependencies.class)
public @interface DependsOn {
Class<?> target();
}

View File

@@ -0,0 +1,30 @@
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
* except in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the
* License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/
package com.android.systemui.plugins.annotations;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
/**
* Should be added to all interfaces in plugin lib to specify their
* current version and optionally their action to implement the plugin.
*/
@Retention(RetentionPolicy.RUNTIME)
public @interface ProvidesInterface {
int version();
String action() default "";
}

View File

@@ -0,0 +1,27 @@
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
* except in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the
* License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/
package com.android.systemui.plugins.annotations;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
/**
* Used for repeated @Requires internally, not for plugin
* use.
*/
@Retention(RetentionPolicy.RUNTIME)
public @interface Requirements {
Requires[] value();
}

View File

@@ -0,0 +1,33 @@
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
* except in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the
* License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/
package com.android.systemui.plugins.annotations;
import java.lang.annotation.Repeatable;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
/**
* Used to annotate which interfaces a given plugin depends on.
*
* At minimum all plugins should have at least one @Requires annotation
* for the plugin interface that they are implementing. They will also
* need an @Requires for each class that the plugin interface @DependsOn.
*/
@Retention(RetentionPolicy.RUNTIME)
@Repeatable(value = Requirements.class)
public @interface Requires {
Class<?> target();
int version();
}

View File

@@ -20,10 +20,12 @@ import android.app.PendingIntent;
import android.content.Context;
import com.android.systemui.plugins.Plugin;
import com.android.systemui.plugins.annotations.ProvidesInterface;
/**
* Provides a {@link DozeUi}.
*/
@ProvidesInterface(action = DozeProvider.ACTION, version = DozeProvider.VERSION)
public interface DozeProvider extends Plugin {
String ACTION = "com.android.systemui.action.PLUGIN_DOZE";

View File

@@ -14,29 +14,32 @@
package com.android.systemui.plugins.qs;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.PendingIntent;
import com.android.systemui.plugins.FragmentBase;
import com.android.systemui.plugins.annotations.DependsOn;
import com.android.systemui.plugins.annotations.ProvidesInterface;
import com.android.systemui.plugins.qs.QS.Callback;
import com.android.systemui.plugins.qs.QS.DetailAdapter;
import com.android.systemui.plugins.qs.QS.HeightListener;
import android.content.Context;
import android.content.Intent;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import android.widget.RelativeLayout;
import com.android.systemui.plugins.FragmentBase;
/**
* Fragment that contains QS in the notification shade. Most of the interface is for
* handling the expand/collapsing of the view interaction.
*/
@ProvidesInterface(action = QS.ACTION, version = QS.VERSION)
@DependsOn(target = HeightListener.class)
@DependsOn(target = Callback.class)
@DependsOn(target = DetailAdapter.class)
public interface QS extends FragmentBase {
public static final String ACTION = "com.android.systemui.action.PLUGIN_QS";
// This should be incremented any time this class or ActivityStarter or BaseStatusBarHeader
// change in incompatible ways.
public static final int VERSION = 5;
String TAG = "QS";
@@ -64,17 +67,23 @@ public interface QS extends FragmentBase {
public abstract void setContainer(ViewGroup container);
@ProvidesInterface(version = HeightListener.VERSION)
public interface HeightListener {
public static final int VERSION = 1;
void onQsHeightChanged();
}
@ProvidesInterface(version = Callback.VERSION)
public interface Callback {
public static final int VERSION = 1;
void onShowingDetail(DetailAdapter detail, int x, int y);
void onToggleStateChanged(boolean state);
void onScanStateChanged(boolean state);
}
@ProvidesInterface(version = DetailAdapter.VERSION)
public interface DetailAdapter {
public static final int VERSION = 1;
CharSequence getTitle();
Boolean getToggleState();
default boolean getToggleEnabled() {
@@ -92,7 +101,9 @@ public interface QS extends FragmentBase {
default boolean hasHeader() { return true; }
}
@ProvidesInterface(version = BaseStatusBarHeader.VERSION)
public abstract static class BaseStatusBarHeader extends RelativeLayout {
public static final int VERSION = 1;
public BaseStatusBarHeader(Context context, AttributeSet attrs) {
super(context, attrs);

View File

@@ -10,7 +10,10 @@ import android.view.View;
import java.util.ArrayList;
import com.android.systemui.plugins.Plugin;
import com.android.systemui.plugins.annotations.ProvidesInterface;
@ProvidesInterface(action = NotificationMenuRowProvider.ACTION,
version = NotificationMenuRowProvider.VERSION)
public interface NotificationMenuRowProvider extends Plugin {
public static final String ACTION = "com.android.systemui.action.PLUGIN_NOTIFICATION_MENU_ROW";

View File

@@ -14,14 +14,15 @@
package com.android.systemui.plugins.statusbar.phone;
import android.annotation.DrawableRes;
import android.annotation.Nullable;
import android.graphics.drawable.Drawable;
import android.view.View;
import android.view.ViewGroup;
import com.android.systemui.plugins.Plugin;
import com.android.systemui.plugins.annotations.ProvidesInterface;
@ProvidesInterface(action = NavBarButtonProvider.ACTION, version = NavBarButtonProvider.VERSION)
public interface NavBarButtonProvider extends Plugin {
public static final String ACTION = "com.android.systemui.action.PLUGIN_NAV_BUTTON";

View File

@@ -17,7 +17,9 @@ package com.android.systemui.plugins.statusbar.phone;
import android.view.MotionEvent;
import com.android.systemui.plugins.Plugin;
import com.android.systemui.plugins.annotations.ProvidesInterface;
@ProvidesInterface(action = NavGesture.ACTION, version = NavBarButtonProvider.VERSION)
public interface NavGesture extends Plugin {
public static final String ACTION = "com.android.systemui.action.PLUGIN_NAV_GESTURE";

View File

@@ -53,8 +53,7 @@ public class PluginInflateContainer extends AutoReinflateContainer
private static final String TAG = "PluginInflateContainer";
private String mAction;
private int mVersion;
private Class<?> mClass;
private View mPluginView;
public PluginInflateContainer(Context context, @Nullable AttributeSet attrs) {
@@ -62,28 +61,25 @@ public class PluginInflateContainer extends AutoReinflateContainer
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.PluginInflateContainer);
String viewType = a.getString(R.styleable.PluginInflateContainer_viewType);
try {
Class c = Class.forName(viewType);
mAction = (String) c.getDeclaredField("ACTION").get(null);
mVersion = (int) c.getDeclaredField("VERSION").get(null);
mClass = Class.forName(viewType);
} catch (Exception e) {
Log.d(TAG, "Problem getting class info " + viewType, e);
mAction = null;
mVersion = 0;
mClass = null;
}
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
if (mAction != null) {
Dependency.get(PluginManager.class).addPluginListener(mAction, this, mVersion);
if (mClass != null) {
Dependency.get(PluginManager.class).addPluginListener(this, mClass);
}
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
if (mAction != null) {
if (mClass != null) {
Dependency.get(PluginManager.class).removePluginListener(this);
}
}

View File

@@ -36,6 +36,7 @@ import com.android.systemui.keyguard.KeyguardViewMediator;
import com.android.systemui.media.RingtonePlayer;
import com.android.systemui.pip.PipUI;
import com.android.systemui.plugins.OverlayPlugin;
import com.android.systemui.plugins.Plugin;
import com.android.systemui.plugins.PluginListener;
import com.android.systemui.plugins.PluginManager;
import com.android.systemui.power.PowerUI;
@@ -205,7 +206,7 @@ public class SystemUIApplication extends Application implements SysUiServiceProv
mServices[i].onBootCompleted();
}
}
Dependency.get(PluginManager.class).addPluginListener(OverlayPlugin.ACTION,
Dependency.get(PluginManager.class).addPluginListener(
new PluginListener<OverlayPlugin>() {
private ArraySet<OverlayPlugin> mOverlays;
@@ -234,7 +235,7 @@ public class SystemUIApplication extends Application implements SysUiServiceProv
Dependency.get(StatusBarWindowManager.class).setForcePluginOpen(
mOverlays.size() != 0);
}
}, OverlayPlugin.VERSION, true /* Allow multiple plugins */);
}, OverlayPlugin.class, true /* Allow multiple plugins */);
mServicesStarted = true;
}

View File

@@ -49,7 +49,7 @@ public class DozeService extends DreamService implements DozeMachine.Service {
}
DozeProvider provider = Dependency.get(PluginManager.class)
.getOneShotPlugin(DozeProvider.ACTION, DozeProvider.VERSION);
.getOneShotPlugin(DozeProvider.class);
mDozeMachine = new DozeFactory(provider).assembleMachine(this);
}

View File

@@ -44,8 +44,9 @@ public class PluginFragmentListener implements PluginListener<Plugin> {
mDefaultClass = defaultFragment;
}
public void startListening(String action, int version) {
mPluginManager.addPluginListener(action, this, version, false /* Only allow one */);
public void startListening() {
mPluginManager.addPluginListener(this, mExpectedInterface,
false /* Only allow one */);
}
public void stopListening() {

View File

@@ -18,12 +18,10 @@ import android.app.Notification;
import android.app.Notification.Action;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.ContextWrapper;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
@@ -40,6 +38,7 @@ import android.view.LayoutInflater;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.messages.nano.SystemMessageProto.SystemMessage;
import com.android.systemui.plugins.VersionInfo.InvalidVersionException;
import java.util.ArrayList;
import java.util.List;
@@ -55,7 +54,7 @@ public class PluginInstanceManager<T extends Plugin> {
private final PluginListener<T> mListener;
private final String mAction;
private final boolean mAllowMultiple;
private final int mVersion;
private final VersionInfo mVersion;
@VisibleForTesting
final MainHandler mMainHandler;
@@ -66,14 +65,14 @@ public class PluginInstanceManager<T extends Plugin> {
private final PluginManager mManager;
PluginInstanceManager(Context context, String action, PluginListener<T> listener,
boolean allowMultiple, Looper looper, int version, PluginManager manager) {
boolean allowMultiple, Looper looper, VersionInfo version, PluginManager manager) {
this(context, context.getPackageManager(), action, listener, allowMultiple, looper, version,
manager, Build.IS_DEBUGGABLE);
}
@VisibleForTesting
PluginInstanceManager(Context context, PackageManager pm, String action,
PluginListener<T> listener, boolean allowMultiple, Looper looper, int version,
PluginListener<T> listener, boolean allowMultiple, Looper looper, VersionInfo version,
PluginManager manager, boolean debuggable) {
mMainHandler = new MainHandler(Looper.getMainLooper());
mPluginHandler = new PluginHandler(looper);
@@ -301,8 +300,14 @@ public class PluginInstanceManager<T extends Plugin> {
Context pluginContext = new PluginContextWrapper(
mContext.createApplicationContext(info, 0), classLoader);
Class<?> pluginClass = Class.forName(cls, true, classLoader);
// TODO: Only create the plugin before version check if we need it for
// legacy version check.
T plugin = (T) pluginClass.newInstance();
if (plugin.getVersion() != mVersion) {
try {
checkVersion(pluginClass, plugin, mVersion);
if (DEBUG) Log.d(TAG, "createPlugin");
return new PluginInfo(pkg, cls, plugin, pluginContext);
} catch (InvalidVersionException e) {
final int icon = mContext.getResources().getIdentifier("tuner", "drawable",
mContext.getPackageName());
final int color = Resources.getSystem().getIdentifier(
@@ -318,20 +323,18 @@ public class PluginInstanceManager<T extends Plugin> {
String label = cls;
try {
label = mPm.getServiceInfo(component, 0).loadLabel(mPm).toString();
} catch (NameNotFoundException e) {
} catch (NameNotFoundException e2) {
}
if (plugin.getVersion() < mVersion) {
if (!e.isTooNew()) {
// Localization not required as this will never ever appear in a user build.
nb.setContentTitle("Plugin \"" + label + "\" is too old")
.setContentText("Contact plugin developer to get an updated"
+ " version.\nPlugin version: " + plugin.getVersion()
+ "\nSystem version: " + mVersion);
+ " version.\n" + e.getMessage());
} else {
// Localization not required as this will never ever appear in a user build.
nb.setContentTitle("Plugin \"" + label + "\" is too new")
.setContentText("Check to see if an OTA is available.\n"
+ "Plugin version: " + plugin.getVersion()
+ "\nSystem version: " + mVersion);
+ e.getMessage());
}
Intent i = new Intent(PluginManager.DISABLE_PLUGIN).setData(
Uri.parse("package://" + component.flattenToString()));
@@ -345,13 +348,24 @@ public class PluginInstanceManager<T extends Plugin> {
+ ", expected " + mVersion);
return null;
}
if (DEBUG) Log.d(TAG, "createPlugin");
return new PluginInfo(pkg, cls, plugin, pluginContext);
} catch (Exception e) {
Log.w(TAG, "Couldn't load plugin: " + pkg, e);
return null;
}
}
private void checkVersion(Class<?> pluginClass, T plugin, VersionInfo version)
throws InvalidVersionException {
VersionInfo pv = new VersionInfo().addClass(pluginClass);
if (pv.hasVersionInfo()) {
version.checkVersion(pv);
} else {
int fallbackVersion = plugin.getVersion();
if (fallbackVersion != version.getDefaultVersion()) {
throw new InvalidVersionException("Invalid legacy version", false);
}
}
}
}
public static class PluginContextWrapper extends ContextWrapper {

View File

@@ -33,6 +33,7 @@ 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;
@@ -40,6 +41,7 @@ import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.messages.nano.SystemMessageProto.SystemMessage;
import com.android.systemui.plugins.PluginInstanceManager.PluginContextWrapper;
import com.android.systemui.plugins.PluginInstanceManager.PluginInfo;
import com.android.systemui.plugins.annotations.ProvidesInterface;
import dalvik.system.PathClassLoader;
@@ -93,7 +95,18 @@ public class PluginManager extends BroadcastReceiver {
Thread.setDefaultUncaughtExceptionHandler(uncaughtExceptionHandler);
}
public <T extends Plugin> T getOneShotPlugin(String action, int version) {
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;
@@ -102,7 +115,7 @@ public class PluginManager extends BroadcastReceiver {
throw new RuntimeException("Must be called from UI thread");
}
PluginInstanceManager<T> p = mFactory.createPluginInstanceManager(mContext, action, null,
false, mBackgroundThread.getLooper(), version, this);
false, mBackgroundThread.getLooper(), cls, this);
mPluginPrefs.addAction(action);
PluginInfo<T> info = p.getPlugin();
if (info != null) {
@@ -114,20 +127,36 @@ public class PluginManager extends BroadcastReceiver {
return null;
}
public <T extends Plugin> void addPluginListener(String action, PluginListener<T> listener,
int version) {
addPluginListener(action, listener, version, false);
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) {
ProvidesInterface info = cls.getDeclaredAnnotation(ProvidesInterface.class);
if (info == null) {
throw new RuntimeException(cls + " doesn't provide an interface");
}
if (TextUtils.isEmpty(info.action())) {
throw new RuntimeException(cls + " doesn't provide an action");
}
addPluginListener(info.action(), listener, cls, allowMultiple);
}
public <T extends Plugin> void addPluginListener(String action, PluginListener<T> listener,
int version, boolean allowMultiple) {
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(), version, this);
allowMultiple, mBackgroundThread.getLooper(), cls, this);
p.loadAll();
mPluginMap.put(listener, p);
startListening();
@@ -282,9 +311,9 @@ public class PluginManager extends BroadcastReceiver {
public static class PluginInstanceManagerFactory {
public <T extends Plugin> PluginInstanceManager createPluginInstanceManager(Context context,
String action, PluginListener<T> listener, boolean allowMultiple, Looper looper,
int version, PluginManager manager) {
Class<?> cls, PluginManager manager) {
return new PluginInstanceManager(context, action, listener, allowMultiple, looper,
version, manager);
new VersionInfo().addClass(cls), manager);
}
}

View File

@@ -0,0 +1,134 @@
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
* except in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the
* License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/
package com.android.systemui.plugins;
import com.android.systemui.plugins.annotations.Dependencies;
import com.android.systemui.plugins.annotations.DependsOn;
import com.android.systemui.plugins.annotations.ProvidesInterface;
import com.android.systemui.plugins.annotations.Requirements;
import com.android.systemui.plugins.annotations.Requires;
import android.util.ArrayMap;
public class VersionInfo {
private final ArrayMap<Class<?>, Version> mVersions = new ArrayMap<>();
private Class<?> mDefault;
public boolean hasVersionInfo() {
return !mVersions.isEmpty();
}
public int getDefaultVersion() {
return mVersions.get(mDefault).mVersion;
}
public VersionInfo addClass(Class<?> cls) {
if (mDefault == null) {
// The legacy default version is from the first class we add.
mDefault = cls;
}
addClass(cls, false);
return this;
}
private void addClass(Class<?> cls, boolean required) {
ProvidesInterface provider = cls.getDeclaredAnnotation(ProvidesInterface.class);
if (provider != null) {
mVersions.put(cls, new Version(provider.version(), true));
}
Requires requires = cls.getDeclaredAnnotation(Requires.class);
if (requires != null) {
mVersions.put(requires.target(), new Version(requires.version(), required));
}
Requirements requirements = cls.getDeclaredAnnotation(Requirements.class);
if (requirements != null) {
for (Requires r : requirements.value()) {
mVersions.put(r.target(), new Version(r.version(), required));
}
}
DependsOn depends = cls.getDeclaredAnnotation(DependsOn.class);
if (depends != null) {
addClass(depends.target(), true);
}
Dependencies dependencies = cls.getDeclaredAnnotation(Dependencies.class);
if (dependencies != null) {
for (DependsOn d : dependencies.value()) {
addClass(d.target(), true);
}
}
}
public void checkVersion(VersionInfo plugin) throws InvalidVersionException {
ArrayMap<Class<?>, Version> versions = new ArrayMap<>(mVersions);
plugin.mVersions.forEach((aClass, version) -> {
Version v = versions.remove(aClass);
if (v == null) {
v = createVersion(aClass);
}
if (v == null) {
throw new InvalidVersionException(aClass.getSimpleName()
+ " does not provide an interface", false);
}
if (v.mVersion != version.mVersion) {
throw new InvalidVersionException(aClass, v.mVersion < version.mVersion, v.mVersion,
version.mVersion);
}
});
versions.forEach((aClass, version) -> {
if (version.mRequired) {
throw new InvalidVersionException("Missing required dependency "
+ aClass.getSimpleName(), false);
}
});
}
private Version createVersion(Class<?> cls) {
ProvidesInterface provider = cls.getDeclaredAnnotation(ProvidesInterface.class);
if (provider != null) {
return new Version(provider.version(), false);
}
return null;
}
public static class InvalidVersionException extends RuntimeException {
private final boolean mTooNew;
public InvalidVersionException(String str, boolean tooNew) {
super(str);
mTooNew = tooNew;
}
public InvalidVersionException(Class<?> cls, boolean tooNew, int expected, int actual) {
super(cls.getSimpleName() + " expected version " + expected + " but had " + actual);
mTooNew = tooNew;
}
public boolean isTooNew() {
return mTooNew;
}
}
private static class Version {
private final int mVersion;
private final boolean mRequired;
public Version(int version, boolean required) {
mVersion = version;
mRequired = required;
}
}
}

View File

@@ -86,6 +86,10 @@ public class QSPanel extends LinearLayout implements Tunable, Callback {
setOrientation(VERTICAL);
mBrightnessView = LayoutInflater.from(context).inflate(
R.layout.quick_settings_brightness_dialog, this, false);
addView(mBrightnessView);
setupTileLayout();
mFooter = new QSFooter(this, context);
@@ -100,10 +104,6 @@ public class QSPanel extends LinearLayout implements Tunable, Callback {
updateResources();
mBrightnessView = LayoutInflater.from(context).inflate(
R.layout.quick_settings_brightness_dialog, this, false);
addView(mBrightnessView);
mBrightnessController = new BrightnessController(getContext(),
(ImageView) findViewById(R.id.brightness_icon),
(ToggleSliderView) findViewById(R.id.brightness_slider));

View File

@@ -90,8 +90,7 @@ public class NotificationMenuRow extends FrameLayout
protected void onAttachedToWindow() {
super.onAttachedToWindow();
Dependency.get(PluginManager.class).addPluginListener(
NotificationMenuRowProvider.ACTION, this,
NotificationMenuRowProvider.VERSION, false /* Allow multiple */);
this, NotificationMenuRowProvider.class, false /* Allow multiple */);
}
@Override

View File

@@ -261,9 +261,9 @@ public class KeyguardBottomAreaView extends FrameLayout implements View.OnClickL
super.onAttachedToWindow();
mAccessibilityController.addStateChangedCallback(this);
Dependency.get(PluginManager.class).addPluginListener(RIGHT_BUTTON_PLUGIN,
mRightListener, IntentButtonProvider.VERSION, false /* Only allow one */);
mRightListener, IntentButtonProvider.class, false /* Only allow one */);
Dependency.get(PluginManager.class).addPluginListener(LEFT_BUTTON_PLUGIN,
mLeftListener, IntentButtonProvider.VERSION, false /* Only allow one */);
mLeftListener, IntentButtonProvider.class, false /* Only allow one */);
Dependency.get(TunerService.class).addTunable(this, LockscreenFragment.LOCKSCREEN_LEFT_BUTTON,
LockscreenFragment.LOCKSCREEN_RIGHT_BUTTON);
}

View File

@@ -138,8 +138,8 @@ public class NavigationBarInflaterView extends FrameLayout
super.onAttachedToWindow();
Dependency.get(TunerService.class).addTunable(this, NAV_BAR_VIEWS, NAV_BAR_LEFT,
NAV_BAR_RIGHT);
Dependency.get(PluginManager.class).addPluginListener(NavBarButtonProvider.ACTION, this,
NavBarButtonProvider.VERSION, true /* Allow multiple */);
Dependency.get(PluginManager.class).addPluginListener(this,
NavBarButtonProvider.class, true /* Allow multiple */);
}
@Override

View File

@@ -783,8 +783,8 @@ public class NavigationBarView extends FrameLayout implements PluginListener<Nav
protected void onAttachedToWindow() {
super.onAttachedToWindow();
onPluginDisconnected(null); // Create default gesture helper
Dependency.get(PluginManager.class).addPluginListener(NavGesture.ACTION, this,
NavGesture.VERSION, false /* Only one */);
Dependency.get(PluginManager.class).addPluginListener(this,
NavGesture.class, false /* Only one */);
}
@Override

View File

@@ -1128,7 +1128,7 @@ public class StatusBar extends SystemUI implements DemoMode,
.replace(R.id.qs_frame, new QSFragment(), QS.TAG)
.commit();
new PluginFragmentListener(container, QS.TAG, QSFragment.class, QS.class)
.startListening(QS.ACTION, QS.VERSION);
.startListening();
final QSTileHost qsh = SystemUIFactory.getInstance().createQSTileHost(mContext, this,
mIconController);
mBrightnessMirrorController = new BrightnessMirrorController(mStatusBarWindow);

View File

@@ -17,14 +17,27 @@ 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;
import static org.mockito.Matchers.anyInt;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
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.VersionInfo.InvalidVersionException;
import com.android.systemui.plugins.annotations.Requires;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mockito;
import android.app.Activity;
import android.app.NotificationManager;
import android.content.BroadcastReceiver;
@@ -35,7 +48,6 @@ import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.pm.ResolveInfo;
import android.content.pm.ServiceInfo;
import android.os.HandlerThread;
@@ -44,17 +56,6 @@ 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;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mockito;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
@@ -72,6 +73,7 @@ public class PluginInstanceManagerTest extends SysuiTestCase {
private PluginListener mMockListener;
private PluginInstanceManager mPluginInstanceManager;
private PluginManager mMockManager;
private VersionInfo mMockVersionInfo;
@Before
public void setup() throws Exception {
@@ -83,8 +85,10 @@ public class PluginInstanceManagerTest extends SysuiTestCase {
mMockManager = mock(PluginManager.class);
when(mMockManager.getClassLoader(any(), any()))
.thenReturn(getClass().getClassLoader());
mMockVersionInfo = mock(VersionInfo.class);
mPluginInstanceManager = new PluginInstanceManager(mContextWrapper, mMockPm, "myAction",
mMockListener, true, mHandlerThread.getLooper(), 1, mMockManager, true);
mMockListener, true, mHandlerThread.getLooper(), mMockVersionInfo,
mMockManager, true);
sMockPlugin = mock(Plugin.class);
when(sMockPlugin.getVersion()).thenReturn(1);
}
@@ -145,7 +149,7 @@ public class PluginInstanceManagerTest extends SysuiTestCase {
NotificationManager nm = mock(NotificationManager.class);
mContext.addMockSystemService(Context.NOTIFICATION_SERVICE, nm);
setupFakePmQuery();
when(sMockPlugin.getVersion()).thenReturn(2);
doThrow(new InvalidVersionException("", false)).when(mMockVersionInfo).checkVersion(any());
mPluginInstanceManager.loadAll();
@@ -181,7 +185,8 @@ public class PluginInstanceManagerTest extends SysuiTestCase {
public void testNonDebuggable() throws Exception {
// Create a version that thinks the build is not debuggable.
mPluginInstanceManager = new PluginInstanceManager(mContextWrapper, mMockPm, "myAction",
mMockListener, true, mHandlerThread.getLooper(), 1, mMockManager, false);
mMockListener, true, mHandlerThread.getLooper(), mMockVersionInfo,
mMockManager, false);
setupFakePmQuery();
mPluginInstanceManager.loadAll();
@@ -270,6 +275,9 @@ public class PluginInstanceManagerTest extends SysuiTestCase {
}
}
// This target class doesn't matter, it just needs to have a Requires to hit the flow where
// the mock version info is called.
@Requires(target = PluginManagerTest.class, version = 1)
public static class TestPlugin implements Plugin {
@Override
public int getVersion() {

View File

@@ -32,6 +32,7 @@ import android.test.suitebuilder.annotation.SmallTest;
import com.android.internal.messages.nano.SystemMessageProto.SystemMessage;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.plugins.annotations.ProvidesInterface;
import com.android.systemui.plugins.PluginInstanceManager.PluginInfo;
import com.android.systemui.plugins.PluginManager.PluginInstanceManagerFactory;
@@ -63,7 +64,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.any()))
Mockito.anyBoolean(), Mockito.any(), Mockito.any(), Mockito.any()))
.thenReturn(mMockPluginInstance);
mPluginManager = new PluginManager(getContext(), mMockFactory, true, mMockExceptionHandler);
resetExceptionHandler();
@@ -76,20 +77,20 @@ public class PluginManagerTest extends SysuiTestCase {
Plugin mockPlugin = mock(Plugin.class);
when(mMockPluginInstance.getPlugin()).thenReturn(new PluginInfo(null, null, mockPlugin,
null));
Plugin result = mPluginManager.getOneShotPlugin("myAction", 1);
Plugin result = mPluginManager.getOneShotPlugin("myAction", TestPlugin.class);
assertTrue(result == mockPlugin);
}
@Test
public void testAddListener() {
mPluginManager.addPluginListener("myAction", mMockListener, 1);
mPluginManager.addPluginListener("myAction", mMockListener, TestPlugin.class);
verify(mMockPluginInstance).loadAll();
}
@Test
public void testRemoveListener() {
mPluginManager.addPluginListener("myAction", mMockListener, 1);
mPluginManager.addPluginListener("myAction", mMockListener, TestPlugin.class);
mPluginManager.removePluginListener(mMockListener);
verify(mMockPluginInstance).destroy();
@@ -101,16 +102,16 @@ public class PluginManagerTest extends SysuiTestCase {
mMockExceptionHandler);
resetExceptionHandler();
mPluginManager.addPluginListener("myAction", mMockListener, 1);
mPluginManager.addPluginListener("myAction", mMockListener, TestPlugin.class);
verify(mMockPluginInstance, Mockito.never()).loadAll();
assertNull(mPluginManager.getOneShotPlugin("myPlugin", 1));
assertNull(mPluginManager.getOneShotPlugin("myPlugin", TestPlugin.class));
verify(mMockPluginInstance, Mockito.never()).getPlugin();
}
@Test
public void testExceptionHandler_foundPlugin() {
mPluginManager.addPluginListener("myAction", mMockListener, 1);
mPluginManager.addPluginListener("myAction", mMockListener, TestPlugin.class);
when(mMockPluginInstance.checkAndDisable(Mockito.any())).thenReturn(true);
mPluginExceptionHandler.uncaughtException(Thread.currentThread(), new Throwable());
@@ -125,7 +126,7 @@ public class PluginManagerTest extends SysuiTestCase {
@Test
public void testExceptionHandler_noFoundPlugin() {
mPluginManager.addPluginListener("myAction", mMockListener, 1);
mPluginManager.addPluginListener("myAction", mMockListener, TestPlugin.class);
when(mMockPluginInstance.checkAndDisable(Mockito.any())).thenReturn(false);
mPluginExceptionHandler.uncaughtException(Thread.currentThread(), new Throwable());
@@ -161,4 +162,10 @@ public class PluginManagerTest extends SysuiTestCase {
// Set back the real exception handler so the test can crash if it wants to.
Thread.setDefaultUncaughtExceptionHandler(mRealExceptionHandler);
}
@ProvidesInterface(action = TestPlugin.ACTION, version = TestPlugin.VERSION)
public static interface TestPlugin extends Plugin {
public static final String ACTION = "testAction";
public static final int VERSION = 1;
}
}

View File

@@ -0,0 +1,103 @@
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
* except in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the
* License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/
package com.android.systemui.plugins;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.plugins.VersionInfo.InvalidVersionException;
import com.android.systemui.plugins.annotations.Requires;
import com.android.systemui.plugins.qs.QS;
import com.android.systemui.plugins.qs.QS.Callback;
import com.android.systemui.plugins.qs.QS.DetailAdapter;
import com.android.systemui.plugins.qs.QS.HeightListener;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
public class VersionInfoTest extends SysuiTestCase {
@Rule
public ExpectedException mThrown = ExpectedException.none();
@Test
public void testHasInfo() {
VersionInfo info = new VersionInfo();
info.addClass(VersionInfoTest.class); // Has no annotations.
assertFalse(info.hasVersionInfo());
info.addClass(OverlayPlugin.class);
assertTrue(info.hasVersionInfo());
}
@Test
public void testSingleProvides() {
VersionInfo overlay = new VersionInfo().addClass(OverlayPlugin.class);
VersionInfo impl = new VersionInfo().addClass(OverlayImpl.class);
overlay.checkVersion(impl);
}
@Test
public void testIncorrectVersion() {
VersionInfo overlay = new VersionInfo().addClass(OverlayPlugin.class);
VersionInfo impl = new VersionInfo().addClass(OverlayImplIncorrectVersion.class);
mThrown.expect(InvalidVersionException.class);
overlay.checkVersion(impl);
}
@Test
public void testMissingRequired() {
VersionInfo overlay = new VersionInfo().addClass(OverlayPlugin.class);
VersionInfo impl = new VersionInfo();
mThrown.expect(InvalidVersionException.class);
overlay.checkVersion(impl);
}
@Test
public void testMissingDependencies() {
VersionInfo overlay = new VersionInfo().addClass(QS.class);
VersionInfo impl = new VersionInfo().addClass(QSImplNoDeps.class);
mThrown.expect(InvalidVersionException.class);
overlay.checkVersion(impl);
}
@Test
public void testHasDependencies() {
VersionInfo overlay = new VersionInfo().addClass(QS.class);
VersionInfo impl = new VersionInfo().addClass(QSImpl.class);
overlay.checkVersion(impl);
}
@Requires(target = OverlayPlugin.class, version = OverlayPlugin.VERSION)
public static class OverlayImpl {
}
@Requires(target = OverlayPlugin.class, version = 0)
public static class OverlayImplIncorrectVersion {
}
@Requires(target = QS.class, version = QS.VERSION)
public static class QSImplNoDeps {
}
@Requires(target = QS.class, version = QS.VERSION)
@Requires(target = Callback.class, version = Callback.VERSION)
@Requires(target = HeightListener.class, version = HeightListener.VERSION)
@Requires(target = DetailAdapter.class, version = DetailAdapter.VERSION)
public static class QSImpl {
}
}

View File

@@ -31,13 +31,7 @@ public class FakePluginManager extends PluginManager {
@Override
public <T extends Plugin> void addPluginListener(String action, PluginListener<T> listener,
int version) {
mLeakChecker.addCallback(listener);
}
@Override
public <T extends Plugin> void addPluginListener(String action, PluginListener<T> listener,
int version, boolean allowMultiple) {
Class cls, boolean allowMultiple) {
mLeakChecker.addCallback(listener);
}
@@ -47,7 +41,7 @@ public class FakePluginManager extends PluginManager {
}
@Override
public <T extends Plugin> T getOneShotPlugin(String action, int version) {
public <T extends Plugin> T getOneShotPlugin(String action, Class<?> cls) {
return null;
}
}