From f4e019aaad6f9da3165e8f35966299c43d5cfebf Mon Sep 17 00:00:00 2001 From: Jason Monk Date: Thu, 3 Nov 2016 09:07:20 -0400 Subject: [PATCH] Plugin fragment support Allows fragments to be easily switched over to plugins and a provides a convenient base class for plugins to use that makes sure the layout inflater and context point at the plugin's and not sysui's. Bug: 32609190 Test: runtest systemui Change-Id: I6503947e980f66ddcd826f6ca9a92b591ce0eb1e --- .../android/systemui/plugins/PluginFragment.java | 77 +++++++++++++++++++ .../systemui/plugins/PluginInstanceManager.java | 12 ++- .../android/systemui/plugins/PluginManager.java | 36 ++++++++- .../systemui/fragments/FragmentHostManager.java | 3 +- .../systemui/fragments/PluginFragmentListener.java | 86 ++++++++++++++++++++++ .../statusbar/phone/NotificationPanelView.java | 3 +- .../systemui/statusbar/phone/PhoneStatusBar.java | 10 +-- 7 files changed, 215 insertions(+), 12 deletions(-) create mode 100644 packages/SystemUI/plugin/src/com/android/systemui/plugins/PluginFragment.java create mode 100644 packages/SystemUI/src/com/android/systemui/fragments/PluginFragmentListener.java diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/PluginFragment.java b/packages/SystemUI/plugin/src/com/android/systemui/plugins/PluginFragment.java new file mode 100644 index 000000000000..a9d1fa94cf10 --- /dev/null +++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/PluginFragment.java @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.android.systemui.plugins; + +import android.annotation.Nullable; +import android.app.Fragment; +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager.NameNotFoundException; +import android.os.Bundle; +import android.view.LayoutInflater; + +public abstract class PluginFragment extends Fragment implements Plugin { + + private static final String KEY_PLUGIN_PACKAGE = "plugin_package_name"; + private Context mPluginContext; + + @Override + public void onCreate(Context sysuiContext, Context pluginContext) { + mPluginContext = pluginContext; + } + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + if (savedInstanceState != null) { + Context sysuiContext = getContext(); + Context pluginContext = recreatePluginContext(sysuiContext, savedInstanceState); + onCreate(sysuiContext, pluginContext); + } + if (mPluginContext == null) { + throw new RuntimeException("PluginFragments must call super.onCreate(" + + "Context sysuiContext, Context pluginContext)"); + } + } + + @Override + public void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + outState.putString(KEY_PLUGIN_PACKAGE, getContext().getPackageName()); + } + + private Context recreatePluginContext(Context sysuiContext, Bundle savedInstanceState) { + final String pkg = savedInstanceState.getString(KEY_PLUGIN_PACKAGE); + try { + ApplicationInfo appInfo = sysuiContext.getPackageManager().getApplicationInfo(pkg, 0); + return PluginManager.getInstance(sysuiContext).getContext(appInfo, pkg); + } catch (NameNotFoundException e) { + throw new RuntimeException("Plugin with invalid package? " + pkg, e); + } + } + + @Override + public LayoutInflater getLayoutInflater(Bundle savedInstanceState) { + return super.getLayoutInflater(savedInstanceState).cloneInContext(mPluginContext); + } + + /** + * Should only be called after {@link Plugin#onCreate(Context, Context)}. + */ + @Override + public Context getContext() { + return mPluginContext != null ? mPluginContext : super.getContext(); + } +} diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/PluginInstanceManager.java b/packages/SystemUI/plugin/src/com/android/systemui/plugins/PluginInstanceManager.java index c64b18826f5e..62d3ce43474f 100644 --- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/PluginInstanceManager.java +++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/PluginInstanceManager.java @@ -158,7 +158,11 @@ public class PluginInstanceManager { case PLUGIN_DISCONNECTED: if (DEBUG) Log.d(TAG, "onPluginDisconnected"); mListener.onPluginDisconnected((T) msg.obj); - ((T) msg.obj).onDestroy(); + if (!(msg.obj instanceof PluginFragment)) { + // Only call onDestroy for plugins that aren't fragments, as fragments + // will get the onDestroy as part of the fragment lifecycle. + ((T) msg.obj).onDestroy(); + } break; default: super.handleMessage(msg); @@ -186,7 +190,11 @@ public class PluginInstanceManager { for (int i = mPlugins.size() - 1; i >= 0; i--) { PluginInfo plugin = mPlugins.get(i); mListener.onPluginDisconnected(plugin.mPlugin); - plugin.mPlugin.onDestroy(); + if (!(plugin.mPlugin instanceof PluginFragment)) { + // Only call onDestroy for plugins that aren't fragments, as fragments + // will get the onDestroy as part of the fragment lifecycle. + plugin.mPlugin.onDestroy(); + } } mPlugins.clear(); handleQueryPlugins(null); diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/PluginManager.java b/packages/SystemUI/plugin/src/com/android/systemui/plugins/PluginManager.java index 85f2e2ad5e9a..60cf3122966a 100644 --- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/PluginManager.java +++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/PluginManager.java @@ -18,6 +18,8 @@ import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager.NameNotFoundException; import android.net.Uri; import android.os.Build; import android.os.HandlerThread; @@ -26,6 +28,7 @@ import android.os.SystemProperties; import android.util.ArrayMap; import com.android.internal.annotations.VisibleForTesting; +import com.android.systemui.plugins.PluginInstanceManager.PluginContextWrapper; import dalvik.system.PathClassLoader; @@ -163,6 +166,16 @@ public class PluginManager extends BroadcastReceiver { return mParentClassLoader; } + public Context getAllPluginContext(Context context) { + return new PluginContextWrapper(context, + new AllPluginClassLoader(context.getClassLoader())); + } + + public Context getContext(ApplicationInfo info, String pkg) throws NameNotFoundException { + ClassLoader classLoader = getClassLoader(info.sourceDir, pkg); + return new PluginContextWrapper(mContext.createApplicationContext(info, 0), classLoader); + } + public static PluginManager getInstance(Context context) { if (sInstance == null) { sInstance = new PluginManager(context.getApplicationContext()); @@ -170,6 +183,28 @@ public class PluginManager extends BroadcastReceiver { return sInstance; } + private class AllPluginClassLoader extends ClassLoader { + public AllPluginClassLoader(ClassLoader classLoader) { + super(classLoader); + } + + @Override + public Class loadClass(String s) throws ClassNotFoundException { + try { + return super.loadClass(s); + } catch (ClassNotFoundException e) { + for (ClassLoader classLoader : mClassLoaders.values()) { + try { + return classLoader.loadClass(s); + } catch (ClassNotFoundException e1) { + // Will re-throw e if all fail. + } + } + throw e; + } + } + } + @VisibleForTesting public static class PluginInstanceManagerFactory { public PluginInstanceManager createPluginInstanceManager(Context context, @@ -180,7 +215,6 @@ public class PluginManager extends BroadcastReceiver { } } - // 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 { diff --git a/packages/SystemUI/src/com/android/systemui/fragments/FragmentHostManager.java b/packages/SystemUI/src/com/android/systemui/fragments/FragmentHostManager.java index 634e5974a39e..5f27b74a8351 100644 --- a/packages/SystemUI/src/com/android/systemui/fragments/FragmentHostManager.java +++ b/packages/SystemUI/src/com/android/systemui/fragments/FragmentHostManager.java @@ -32,6 +32,7 @@ import android.view.View; import com.android.settingslib.applications.InterestingConfigChanges; import com.android.systemui.SystemUIApplication; +import com.android.systemui.plugins.PluginManager; import java.io.FileDescriptor; import java.io.PrintWriter; @@ -51,7 +52,7 @@ public class FragmentHostManager { private FragmentLifecycleCallbacks mLifecycleCallbacks; FragmentHostManager(Context context, FragmentService manager, View rootView) { - mContext = context; + mContext = PluginManager.getInstance(context).getAllPluginContext(context); mManager = manager; mRootView = rootView; mConfigChanges.applyNewConfig(context.getResources()); diff --git a/packages/SystemUI/src/com/android/systemui/fragments/PluginFragmentListener.java b/packages/SystemUI/src/com/android/systemui/fragments/PluginFragmentListener.java new file mode 100644 index 000000000000..e107fcd5ebae --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/fragments/PluginFragmentListener.java @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.android.systemui.fragments; + +import android.app.Fragment; +import android.util.Log; +import android.view.View; + +import com.android.systemui.plugins.FragmentBase; +import com.android.systemui.plugins.Plugin; +import com.android.systemui.plugins.PluginListener; +import com.android.systemui.plugins.PluginManager; + +public class PluginFragmentListener implements PluginListener { + + private static final String TAG = "PluginFragmentListener"; + + private final FragmentHostManager mFragmentHostManager; + private final PluginManager mPluginManager; + private final Class mDefaultClass; + private final int mId; + private final String mTag; + private final Class mExpectedInterface; + + public PluginFragmentListener(View view, String tag, int id, + Class defaultFragment, + Class expectedInterface) { + mFragmentHostManager = FragmentHostManager.get(view); + mPluginManager = PluginManager.getInstance(view.getContext()); + mExpectedInterface = expectedInterface; + mTag = tag; + mDefaultClass = defaultFragment; + mId = id; + } + + public void startListening(String action, int version) { + try { + setFragment(mDefaultClass.newInstance()); + } catch (InstantiationException | IllegalAccessException e) { + Log.e(TAG, "Couldn't instantiate " + mDefaultClass.getName(), e); + } + mPluginManager.addPluginListener(action, this, version, false /* Only allow one */); + } + + public void stopListening() { + mPluginManager.removePluginListener(this); + } + + private void setFragment(Fragment fragment) { + mFragmentHostManager.getFragmentManager().beginTransaction() + .replace(mId, fragment, mTag) + .commit(); + } + + @Override + public void onPluginConnected(Plugin plugin) { + try { + mExpectedInterface.cast(plugin); + setFragment((Fragment) plugin); + } catch (ClassCastException e) { + Log.e(TAG, plugin.getClass().getName() + " must be a Fragment and implement " + + mExpectedInterface.getName(), e); + } + } + + @Override + public void onPluginDisconnected(Plugin plugin) { + try { + setFragment(mDefaultClass.newInstance()); + } catch (InstantiationException | IllegalAccessException e) { + Log.e(TAG, "Couldn't instantiate " + mDefaultClass.getName(), e); + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelView.java index 749b34e90746..69d76e56594e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelView.java @@ -1621,6 +1621,7 @@ public class NotificationPanelView extends PanelView implements } // Since there are QS tiles in the header now, we need to make sure we start listening // immediately so they can be up to date. + if (mQs == null) return; mQs.setHeaderListening(true); } @@ -1749,7 +1750,7 @@ public class NotificationPanelView extends PanelView implements } public void onQsHeightChanged() { - mQsMaxExpansionHeight = mQs.getDesiredHeight(); + mQsMaxExpansionHeight = mQs != null ? mQs.getDesiredHeight() : 0; if (mQsExpanded && mQsFullyExpanded) { mQsExpansionHeight = mQsMaxExpansionHeight; requestScrollerTopPaddingUpdate(false /* animate */); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBar.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBar.java index 197fe24feab4..471cb3224482 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBar.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBar.java @@ -90,7 +90,6 @@ import android.os.UserHandle; import android.os.UserManager; import android.os.Vibrator; import android.provider.Settings; -import android.service.notification.NotificationListenerService; import android.service.notification.NotificationListenerService.RankingMap; import android.service.notification.StatusBarNotification; import android.telecom.TelecomManager; @@ -126,8 +125,6 @@ import com.android.keyguard.KeyguardStatusView; import com.android.keyguard.KeyguardUpdateMonitor; import com.android.keyguard.KeyguardUpdateMonitorCallback; import com.android.keyguard.ViewMediatorCallback; -import com.android.systemui.AutoReinflateContainer; -import com.android.systemui.AutoReinflateContainer.InflateListener; import com.android.systemui.BatteryMeterView; import com.android.systemui.DemoMode; import com.android.systemui.EventLogConstants; @@ -142,11 +139,11 @@ import com.android.systemui.classifier.FalsingManager; import com.android.systemui.doze.DozeHost; import com.android.systemui.doze.DozeLog; import com.android.systemui.fragments.FragmentHostManager; +import com.android.systemui.fragments.PluginFragmentListener; import com.android.systemui.keyguard.KeyguardViewMediator; import com.android.systemui.plugins.qs.QS; import com.android.systemui.plugins.qs.QS.ActivityStarter; import com.android.systemui.plugins.qs.QS.BaseStatusBarHeader; -import com.android.systemui.qs.QSContainerImpl; import com.android.systemui.qs.QSFragment; import com.android.systemui.qs.QSPanel; import com.android.systemui.recents.ScreenPinningRequest; @@ -928,9 +925,8 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, View container = mStatusBarWindow.findViewById(R.id.qs_frame); if (container != null) { FragmentHostManager fragmentHostManager = FragmentHostManager.get(container); - fragmentHostManager.getFragmentManager().beginTransaction() - .replace(R.id.qs_frame, new QSFragment(), QS.TAG) - .commit(); + new PluginFragmentListener(container, QS.TAG, R.id.qs_frame, QSFragment.class, QS.class) + .startListening(QS.ACTION, QS.VERSION); final QSTileHost qsh = SystemUIFactory.getInstance().createQSTileHost(mContext, this, mBluetoothController, mLocationController, mRotationLockController, mNetworkController, mZenModeController, mHotspotController, -- 2.11.0