QS: Fix some QS layout issues.

- Make the tile list configurable for testing.
- Support an external tile backed by a sticky broadcast intent.
- Ensure tiles clean up properly when no longer needed.

Bug:16818269
Bug:16822505
Change-Id: Ie24f878aae0d19c7f1feca4c519d10667023bef3
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 74ae4a4..a07bc5c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelView.java
@@ -197,6 +197,20 @@
         mAfforanceHelper = new KeyguardAffordanceHelper(this, getContext());
         mSecureCameraLaunchManager =
                 new SecureCameraLaunchManager(getContext(), mKeyguardBottomArea);
+
+        // recompute internal state when qspanel height changes
+        mQsContainer.addOnLayoutChangeListener(new OnLayoutChangeListener() {
+            @Override
+            public void onLayoutChange(View v, int left, int top, int right,
+                    int bottom, int oldLeft, int oldTop, int oldRight,
+                    int oldBottom) {
+                final int height = bottom - top;
+                final int oldHeight = oldBottom - oldTop;
+                if (height != oldHeight) {
+                    onScrollChanged();
+                }
+            }
+        });
     }
 
     @Override
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 5eb45df..a2f8931 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBar.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBar.java
@@ -818,10 +818,14 @@
                     mUserSwitcherController, mKeyguardMonitor,
                     mSecurityController);
             mQSPanel.setHost(qsh);
-            for (QSTile<?> tile : qsh.getTiles()) {
-                mQSPanel.addTile(tile);
-            }
+            mQSPanel.setTiles(qsh.getTiles());
             mHeader.setQSPanel(mQSPanel);
+            qsh.setCallback(new QSTileHost.Callback() {
+                @Override
+                public void onTilesChanged() {
+                    mQSPanel.setTiles(qsh.getTiles());
+                }
+            });
         }
 
         mBackdrop = (FrameLayout) mStatusBarWindow.findViewById(R.id.backdrop);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/QSTileHost.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/QSTileHost.java
index 8f25fb97..729d459 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/QSTileHost.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/QSTileHost.java
@@ -18,9 +18,16 @@
 
 import android.content.Context;
 import android.content.Intent;
+import android.content.res.Resources;
+import android.database.ContentObserver;
+import android.net.Uri;
+import android.os.Handler;
 import android.os.HandlerThread;
 import android.os.Looper;
+import android.provider.Settings.Secure;
+import android.util.Log;
 
+import com.android.systemui.R;
 import com.android.systemui.qs.QSTile;
 import com.android.systemui.qs.tiles.AirplaneModeTile;
 import com.android.systemui.qs.tiles.BluetoothTile;
@@ -29,6 +36,7 @@
 import com.android.systemui.qs.tiles.ColorInversionTile;
 import com.android.systemui.qs.tiles.FlashlightTile;
 import com.android.systemui.qs.tiles.HotspotTile;
+import com.android.systemui.qs.tiles.IntentTile;
 import com.android.systemui.qs.tiles.LocationTile;
 import com.android.systemui.qs.tiles.RotationLockTile;
 import com.android.systemui.qs.tiles.WifiTile;
@@ -46,13 +54,23 @@
 import com.android.systemui.statusbar.policy.ZenModeController;
 
 import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.LinkedHashMap;
 import java.util.List;
+import java.util.Map;
 
 /** Platform implementation of the quick settings tile host **/
 public class QSTileHost implements QSTile.Host {
+    private static final String TAG = "QSTileHost";
+    private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
+
+    private static final String TILES_SETTING = "sysui_qs_tiles";
 
     private final Context mContext;
     private final PhoneStatusBar mStatusBar;
+    private final LinkedHashMap<String, QSTile<?>> mTiles = new LinkedHashMap<>();
+    private final Observer mObserver = new Observer();
     private final BluetoothController mBluetooth;
     private final LocationController mLocation;
     private final RotationLockController mRotation;
@@ -62,12 +80,13 @@
     private final CastController mCast;
     private final Looper mLooper;
     private final CurrentUserTracker mUserTracker;
-    private final ArrayList<QSTile<?>> mTiles = new ArrayList<QSTile<?>>();
     private final FlashlightController mFlashlight;
     private final UserSwitcherController mUserSwitcherController;
     private final KeyguardMonitor mKeyguard;
     private final SecurityController mSecurity;
 
+    private Callback mCallback;
+
     public QSTileHost(Context context, PhoneStatusBar statusBar,
             BluetoothController bluetooth, LocationController location,
             RotationLockController rotation, NetworkController network,
@@ -93,31 +112,30 @@
         ht.start();
         mLooper = ht.getLooper();
 
-        mTiles.add(new WifiTile(this));
-        mTiles.add(new BluetoothTile(this));
-        mTiles.add(new ColorInversionTile(this));
-        mTiles.add(new CellularTile(this));
-        mTiles.add(new AirplaneModeTile(this));
-        mTiles.add(new RotationLockTile(this));
-        mTiles.add(new FlashlightTile(this));
-        mTiles.add(new LocationTile(this));
-        mTiles.add(new CastTile(this));
-        mTiles.add(new HotspotTile(this));
-
         mUserTracker = new CurrentUserTracker(mContext) {
             @Override
             public void onUserSwitched(int newUserId) {
-                for (QSTile<?> tile : mTiles) {
+                recreateTiles();
+                for (QSTile<?> tile : mTiles.values()) {
                     tile.userSwitch(newUserId);
                 }
+                mObserver.register();
             }
         };
+        recreateTiles();
+
         mUserTracker.startTracking();
+        mObserver.register();
     }
 
     @Override
-    public List<QSTile<?>> getTiles() {
-        return mTiles;
+    public void setCallback(Callback callback) {
+        mCallback = callback;
+    }
+
+    @Override
+    public Collection<QSTile<?>> getTiles() {
+        return mTiles.values();
     }
 
     @Override
@@ -197,4 +215,99 @@
     public SecurityController getSecurityController() {
         return mSecurity;
     }
+
+    private void recreateTiles() {
+        if (DEBUG) Log.d(TAG, "Recreating tiles");
+        final List<String> tileSpecs = loadTileSpecs();
+        for (Map.Entry<String, QSTile<?>> tile : mTiles.entrySet()) {
+            if (!tileSpecs.contains(tile.getKey())) {
+                if (DEBUG) Log.d(TAG, "Destroying tile: " + tile.getKey());
+                tile.getValue().destroy();
+            }
+        }
+        final LinkedHashMap<String, QSTile<?>> newTiles = new LinkedHashMap<>();
+        for (String tileSpec : tileSpecs) {
+            if (mTiles.containsKey(tileSpec)) {
+                newTiles.put(tileSpec, mTiles.get(tileSpec));
+            } else {
+                if (DEBUG) Log.d(TAG, "Creating tile: " + tileSpec);
+                try {
+                    newTiles.put(tileSpec, createTile(tileSpec));
+                } catch (Throwable t) {
+                    Log.w(TAG, "Error creating tile for spec: " + tileSpec, t);
+                }
+            }
+        }
+        if (mTiles.equals(newTiles)) return;
+        mTiles.clear();
+        mTiles.putAll(newTiles);
+        if (mCallback != null) {
+            mCallback.onTilesChanged();
+        }
+    }
+
+    private QSTile<?> createTile(String tileSpec) {
+        if (tileSpec.equals("wifi")) return new WifiTile(this);
+        else if (tileSpec.equals("bt")) return new BluetoothTile(this);
+        else if (tileSpec.equals("inversion")) return new ColorInversionTile(this);
+        else if (tileSpec.equals("cell")) return new CellularTile(this);
+        else if (tileSpec.equals("airplane")) return new AirplaneModeTile(this);
+        else if (tileSpec.equals("rotation")) return new RotationLockTile(this);
+        else if (tileSpec.equals("flashlight")) return new FlashlightTile(this);
+        else if (tileSpec.equals("location")) return new LocationTile(this);
+        else if (tileSpec.equals("cast")) return new CastTile(this);
+        else if (tileSpec.equals("hotspot")) return new HotspotTile(this);
+        else if (tileSpec.startsWith(IntentTile.PREFIX)) return IntentTile.create(this,tileSpec);
+        else throw new IllegalArgumentException("Bad tile spec: " + tileSpec);
+    }
+
+    private List<String> loadTileSpecs() {
+        final Resources res = mContext.getResources();
+        final String defaultTileList = res.getString(R.string.quick_settings_tiles_default);
+        String tileList = Secure.getStringForUser(mContext.getContentResolver(), TILES_SETTING,
+                mUserTracker.getCurrentUserId());
+        if (tileList == null) {
+            tileList = res.getString(R.string.quick_settings_tiles);
+            if (DEBUG) Log.d(TAG, "Loaded tile specs from config: " + tileList);
+        } else {
+            if (DEBUG) Log.d(TAG, "Loaded tile specs from setting: " + tileList);
+        }
+        final ArrayList<String> tiles = new ArrayList<String>();
+        boolean addedDefault = false;
+        for (String tile : tileList.split(",")) {
+            tile = tile.trim();
+            if (tile.isEmpty()) continue;
+            if (tile.equals("default")) {
+                if (!addedDefault) {
+                    tiles.addAll(Arrays.asList(defaultTileList.split(",")));
+                    addedDefault = true;
+                }
+            } else {
+                tiles.add(tile);
+            }
+        }
+        return tiles;
+    }
+
+    private class Observer extends ContentObserver {
+        private boolean mRegistered;
+
+        public Observer() {
+            super(new Handler(Looper.getMainLooper()));
+        }
+
+        public void register() {
+            if (mRegistered) {
+                mContext.getContentResolver().unregisterContentObserver(this);
+            }
+            mContext.getContentResolver().registerContentObserver(Secure.getUriFor(TILES_SETTING),
+                    false, this, mUserTracker.getCurrentUserId());
+            mRegistered = true;
+        }
+
+        @Override
+        public void onChange(boolean selfChange, Uri uri) {
+            recreateTiles();
+        }
+    }
 }