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
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 0000000..a9d1fa9
--- /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 c64b188..62d3ce4 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 @@
                 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 @@
                     for (int i = mPlugins.size() - 1; i >= 0; i--) {
                         PluginInfo<T> 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 85f2e2a..60cf312 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.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.util.ArrayMap;
 
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.systemui.plugins.PluginInstanceManager.PluginContextWrapper;
 
 import dalvik.system.PathClassLoader;
 
@@ -163,6 +166,16 @@
         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 @@
         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 <T extends Plugin> PluginInstanceManager createPluginInstanceManager(Context context,
@@ -180,7 +215,6 @@
         }
     }
 
-
     // 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 634e597..5f27b74 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 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 @@
     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 0000000..e107fcd
--- /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<Plugin> {
+
+    private static final String TAG = "PluginFragmentListener";
+
+    private final FragmentHostManager mFragmentHostManager;
+    private final PluginManager mPluginManager;
+    private final Class<? extends Fragment> mDefaultClass;
+    private final int mId;
+    private final String mTag;
+    private final Class<? extends FragmentBase> mExpectedInterface;
+
+    public PluginFragmentListener(View view, String tag, int id,
+            Class<? extends Fragment> defaultFragment,
+            Class<? extends FragmentBase> 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 749b34e..69d76e5 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 @@
         }
         // 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 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 197fe24..471cb32 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.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.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.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 @@
         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,