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/qs/QSPanel.java b/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java
index 3679b4c..cbf6e29 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java
@@ -37,6 +37,7 @@
 import com.android.systemui.statusbar.phone.QSTileHost;
 
 import java.util.ArrayList;
+import java.util.Collection;
 
 /** View that represents the quick settings tile panel. **/
 public class QSPanel extends ViewGroup {
@@ -186,12 +187,25 @@
         v.setVisibility(visible ? VISIBLE : GONE);
     }
 
-    public void addTile(final QSTile<?> tile) {
+    public void setTiles(Collection<QSTile<?>> tiles) {
+        for (TileRecord record : mRecords) {
+            removeView(record.tileView);
+        }
+        mRecords.clear();
+        for (QSTile<?> tile : tiles) {
+            addTile(tile);
+        }
+        if (isShowingDetail()) {
+            mDetail.bringToFront();
+        }
+    }
+
+    private void addTile(final QSTile<?> tile) {
         final TileRecord r = new TileRecord();
         r.tile = tile;
         r.tileView = tile.createTileView(mContext);
         r.tileView.setVisibility(View.GONE);
-        r.tile.setCallback(new QSTile.Callback() {
+        final QSTile.Callback callback = new QSTile.Callback() {
             @Override
             public void onStateChanged(QSTile.State state) {
                 setTileVisibility(r.tileView, state.visible);
@@ -213,7 +227,8 @@
                     fireScanStateChanged(state);
                 }
             }
-        });
+        };
+        r.tile.setCallback(callback);
         final View.OnClickListener click = new View.OnClickListener() {
             @Override
             public void onClick(View v) {
@@ -227,6 +242,8 @@
             }
         };
         r.tileView.init(click, clickSecondary);
+        r.tile.setListening(mListening);
+        callback.onStateChanged(r.tile.getState());
         r.tile.refreshState();
         mRecords.add(r);
 
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSTile.java b/packages/SystemUI/src/com/android/systemui/qs/QSTile.java
index 93766af..6975541 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSTile.java
@@ -37,9 +37,8 @@
 import com.android.systemui.statusbar.policy.RotationLockController;
 import com.android.systemui.statusbar.policy.HotspotController;
 import com.android.systemui.statusbar.policy.ZenModeController;
-import com.android.systemui.volume.VolumeComponent;
 
-import java.util.List;
+import java.util.Collection;
 import java.util.Objects;
 
 /**
@@ -134,6 +133,14 @@
         mHandler.obtainMessage(H.SCAN_STATE_CHANGED, state ? 1 : 0, 0).sendToTarget();
     }
 
+    public void destroy() {
+        mHandler.sendEmptyMessage(H.DESTROY);
+    }
+
+    public TState getState() {
+        return mState;
+    }
+
     // call only on tile worker looper
 
     private void handleSetCallback(Callback callback) {
@@ -181,6 +188,11 @@
         handleRefreshState(null);
     }
 
+    protected void handleDestroy() {
+        setListening(false);
+        mCallback = null;
+    }
+
     protected final class H extends Handler {
         private static final int SET_CALLBACK = 1;
         private static final int CLICK = 2;
@@ -190,6 +202,7 @@
         private static final int USER_SWITCH = 6;
         private static final int TOGGLE_STATE_CHANGED = 7;
         private static final int SCAN_STATE_CHANGED = 8;
+        private static final int DESTROY = 9;
 
         private H(Looper looper) {
             super(looper);
@@ -223,6 +236,11 @@
                 } else if (msg.what == SCAN_STATE_CHANGED) {
                     name = "handleScanStateChanged";
                     handleScanStateChanged(msg.arg1 != 0);
+                } else if (msg.what == DESTROY) {
+                    name = "handleDestroy";
+                    handleDestroy();
+                } else {
+                    throw new IllegalArgumentException("Unknown msg: " + msg.what);
                 }
             } catch (Throwable t) {
                 final String error = "Error in " + name;
@@ -245,7 +263,8 @@
         void collapsePanels();
         Looper getLooper();
         Context getContext();
-        List<QSTile<?>> getTiles();
+        Collection<QSTile<?>> getTiles();
+        void setCallback(Callback callback);
         BluetoothController getBluetoothController();
         LocationController getLocationController();
         RotationLockController getRotationLockController();
@@ -255,6 +274,10 @@
         CastController getCastController();
         FlashlightController getFlashlightController();
         KeyguardMonitor getKeyguardMonitor();
+
+        public interface Callback {
+            void onTilesChanged();
+        }
     }
 
     public static class State {
diff --git a/packages/SystemUI/src/com/android/systemui/qs/UsageTracker.java b/packages/SystemUI/src/com/android/systemui/qs/UsageTracker.java
index e72d3a9..ad79aba 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/UsageTracker.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/UsageTracker.java
@@ -23,8 +23,9 @@
 import android.content.SharedPreferences;
 
 import com.android.systemui.R;
+import com.android.systemui.statusbar.policy.Listenable;
 
-public class UsageTracker {
+public class UsageTracker implements Listenable {
     private static final long MILLIS_PER_DAY = 1000 * 60 * 60 * 24;
 
     private final Context mContext;
@@ -32,7 +33,7 @@
     private final String mPrefKey;
     private final String mResetAction;
 
-    private BroadcastReceiver mReceiver;
+    private boolean mRegistered;
 
     public UsageTracker(Context context, Class<?> tile) {
         mContext = context;
@@ -42,17 +43,14 @@
         mResetAction = "com.android.systemui.qs." + tile.getSimpleName() + ".usage_reset";
     }
 
-    public void listenForReset() {
-        if (mReceiver != null) {
-            mReceiver = new BroadcastReceiver() {
-                @Override
-                public void onReceive(Context context, Intent intent) {
-                    if (mResetAction.equals(intent.getAction())) {
-                        reset();
-                    }
-                }
-            };
-            mContext.registerReceiver(mReceiver, new IntentFilter(mResetAction));
+    @Override
+    public void setListening(boolean listen) {
+        if (listen && !mRegistered) {
+             mContext.registerReceiver(mReceiver, new IntentFilter(mResetAction));
+             mRegistered = true;
+        } else if (!listen && mRegistered) {
+            mContext.unregisterReceiver(mReceiver);
+            mRegistered = false;
         }
     }
 
@@ -72,4 +70,13 @@
     private SharedPreferences getSharedPrefs() {
         return mContext.getSharedPreferences(mContext.getPackageName(), 0);
     }
+
+    private BroadcastReceiver mReceiver = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            if (mResetAction.equals(intent.getAction())) {
+                reset();
+            }
+        }
+    };
 }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/AirplaneModeTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/AirplaneModeTile.java
index 191bac9..5d1fa80 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/AirplaneModeTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/AirplaneModeTile.java
@@ -31,6 +31,8 @@
 public class AirplaneModeTile extends QSTile<QSTile.BooleanState> {
     private final GlobalSetting mSetting;
 
+    private boolean mListening;
+
     public AirplaneModeTile(Host host) {
         super(host);
 
@@ -79,6 +81,8 @@
     }
 
     public void setListening(boolean listening) {
+        if (mListening == listening) return;
+        mListening = listening;
         if (listening) {
             final IntentFilter filter = new IntentFilter();
             filter.addAction(Intent.ACTION_AIRPLANE_MODE_CHANGED);
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/ColorInversionTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/ColorInversionTile.java
index 9c88466..21254d4 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/ColorInversionTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/ColorInversionTile.java
@@ -41,7 +41,13 @@
             }
         };
         mUsageTracker = new UsageTracker(host.getContext(), ColorInversionTile.class);
-        mUsageTracker.listenForReset();
+        mUsageTracker.setListening(true);
+    }
+
+    @Override
+    protected void handleDestroy() {
+        super.handleDestroy();
+        mUsageTracker.setListening(false);
     }
 
     @Override
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/FlashlightTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/FlashlightTile.java
index f4ddd84..3ddf5e3 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/FlashlightTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/FlashlightTile.java
@@ -16,18 +16,13 @@
 
 package com.android.systemui.qs.tiles;
 
+import android.app.ActivityManager;
+import android.os.SystemClock;
+
 import com.android.systemui.R;
 import com.android.systemui.qs.QSTile;
-import com.android.systemui.qs.SecureSetting;
 import com.android.systemui.statusbar.policy.FlashlightController;
 
-import android.app.ActivityManager;
-import android.os.Handler;
-import android.os.Looper;
-import android.os.SystemClock;
-import android.provider.Settings.Secure;
-import android.util.Log;
-
 /** Quick settings tile: Control flashlight **/
 public class FlashlightTile extends QSTile<QSTile.BooleanState> implements
         FlashlightController.FlashlightListener {
@@ -46,6 +41,12 @@
     }
 
     @Override
+    protected void handleDestroy() {
+        super.handleDestroy();
+        mFlashlightController.removeListener(this);
+    }
+
+    @Override
     protected BooleanState newTileState() {
         return new BooleanState();
     }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/HotspotTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/HotspotTile.java
index ff26b54..96333a3 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/HotspotTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/HotspotTile.java
@@ -35,7 +35,13 @@
         super(host);
         mController = host.getHotspotController();
         mUsageTracker = new UsageTracker(host.getContext(), HotspotTile.class);
-        mUsageTracker.listenForReset();
+        mUsageTracker.setListening(true);
+    }
+
+    @Override
+    protected void handleDestroy() {
+        super.handleDestroy();
+        mUsageTracker.setListening(false);
     }
 
     @Override
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/IntentTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/IntentTile.java
new file mode 100644
index 0000000..58587e6
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/IntentTile.java
@@ -0,0 +1,139 @@
+/*
+ * Copyright (C) 2014 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.qs.tiles;
+
+import android.app.PendingIntent;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.os.UserHandle;
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.android.systemui.qs.QSTile;
+
+public class IntentTile extends QSTile<QSTile.State> {
+    public static final String PREFIX = "intent(";
+
+    private PendingIntent mOnClick;
+    private String mOnClickUri;
+    private int mCurrentUserId;
+
+    private IntentTile(Host host, String action) {
+        super(host);
+        mContext.registerReceiver(mReceiver, new IntentFilter(action));
+    }
+
+    @Override
+    protected void handleDestroy() {
+        super.handleDestroy();
+        mContext.unregisterReceiver(mReceiver);
+    }
+
+    public static QSTile<?> create(Host host, String spec) {
+        if (spec == null || !spec.startsWith(PREFIX) || !spec.endsWith(")")) {
+            throw new IllegalArgumentException("Bad intent tile spec: " + spec);
+        }
+        final String action = spec.substring(PREFIX.length(), spec.length() - 1);
+        if (action.isEmpty()) {
+            throw new IllegalArgumentException("Empty intent tile spec action");
+        }
+        return new IntentTile(host, action);
+    }
+
+    @Override
+    public void setListening(boolean listening) {
+    }
+
+    @Override
+    protected State newTileState() {
+        return new State();
+    }
+
+    @Override
+    protected void handleUserSwitch(int newUserId) {
+        super.handleUserSwitch(newUserId);
+        mCurrentUserId = newUserId;
+    }
+
+    @Override
+    protected void handleClick() {
+        try {
+            if (mOnClick != null) {
+                mOnClick.send();
+            } else if (mOnClickUri != null) {
+                final Intent intent = Intent.parseUri(mOnClickUri, Intent.URI_INTENT_SCHEME);
+                mContext.sendBroadcastAsUser(intent, new UserHandle(mCurrentUserId));
+            }
+        } catch (Throwable t) {
+            Log.w(TAG, "Error sending click intent", t);
+        }
+    }
+
+    @Override
+    protected void handleUpdateState(State state, Object arg) {
+        if (!(arg instanceof Intent)) return;
+        final Intent intent = (Intent) arg;
+        state.visible = intent.getBooleanExtra("visible", true);
+        state.contentDescription = intent.getStringExtra("contentDescription");
+        state.label = intent.getStringExtra("label");
+        state.iconId = 0;
+        state.icon = null;
+        final byte[] iconBitmap = intent.getByteArrayExtra("iconBitmap");
+        if (iconBitmap != null) {
+            try {
+                final Bitmap b = BitmapFactory.decodeByteArray(iconBitmap, 0, iconBitmap.length);
+                state.icon = new BitmapDrawable(mContext.getResources(), b);
+            } catch (Throwable t) {
+                Log.w(TAG, "Error loading icon bitmap, length " + iconBitmap.length, t);
+            }
+        } else {
+            final int iconId = intent.getIntExtra("iconId", 0);
+            if (iconId != 0) {
+                final String iconPackage = intent.getStringExtra("iconPackage");
+                if (!TextUtils.isEmpty(iconPackage)) {
+                    state.icon = getPackageDrawable(iconPackage, iconId);
+                } else {
+                    state.iconId = iconId;
+                }
+            }
+        }
+        mOnClick = intent.getParcelableExtra("onClick");
+        mOnClickUri = intent.getStringExtra("onClickUri");
+    }
+
+    private Drawable getPackageDrawable(String pkg, int id) {
+        try {
+            return mContext.createPackageContext(pkg, 0).getDrawable(id);
+        } catch (Throwable t) {
+            Log.w(TAG, "Error loading package drawable pkg=" + pkg + " id=" + id, t);
+            return null;
+        }
+    }
+
+    private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            refreshState(intent);
+        }
+    };
+}
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();
+        }
+    }
 }