QS: Add long-press to customize prototype - part 1

Start adding prototype to long-press to go to customization view
for QS.  Currently it allows re-arranging and resetting.  Later
it will have more.

Change-Id: Ib2ba0f93ac2f4cced4f146d39771a8a17ac05bc2
diff --git a/packages/SystemUI/res/layout/horizontal_divider.xml b/packages/SystemUI/res/layout/horizontal_divider.xml
new file mode 100644
index 0000000..a060f08
--- /dev/null
+++ b/packages/SystemUI/res/layout/horizontal_divider.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright (C) 2015 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.
+-->
+<View
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="1dp"
+    android:layout_marginTop="10dp"
+    android:layout_marginBottom="10dp"
+    android:layout_marginStart="40dp"
+    android:layout_marginEnd="40dp"
+    android:background="#4dffffff" />
diff --git a/packages/SystemUI/res/layout/qs_customize_layout.xml b/packages/SystemUI/res/layout/qs_customize_layout.xml
new file mode 100644
index 0000000..91cf894
--- /dev/null
+++ b/packages/SystemUI/res/layout/qs_customize_layout.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright (C) 2015 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.
+-->
+<com.android.systemui.qs.customize.NonPagedTileLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/tiles_container"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:orientation="vertical">
+
+    <com.android.systemui.qs.QuickTileLayout
+        android:id="@+id/quick_tile_layout"
+        android:layout_width="match_parent"
+        android:layout_height="@dimen/qs_quick_actions_height"
+        android:orientation="horizontal"
+        android:paddingStart="@dimen/qs_quick_actions_padding"
+        android:paddingEnd="@dimen/qs_quick_actions_padding" />
+
+    <view
+        class="com.android.systemui.qs.PagedTileLayout$TilePage"
+        android:id="@+id/tile_page"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content" />
+
+</com.android.systemui.qs.customize.NonPagedTileLayout>
+
diff --git a/packages/SystemUI/res/layout/qs_customize_panel.xml b/packages/SystemUI/res/layout/qs_customize_panel.xml
new file mode 100644
index 0000000..6beac31
--- /dev/null
+++ b/packages/SystemUI/res/layout/qs_customize_panel.xml
@@ -0,0 +1,47 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright (C) 2015 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.
+-->
+<com.android.systemui.qs.customize.QSCustomizer
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:orientation="vertical"
+    android:background="?android:attr/windowBackground">
+
+    <Toolbar
+        android:id="@*android:id/action_bar"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:navigationContentDescription="@*android:string/action_bar_up_description"
+        android:background="?android:attr/colorPrimary"
+        style="?android:attr/toolbarStyle" />
+
+    <com.android.systemui.tuner.AutoScrollView
+        android:layout_width="match_parent"
+        android:layout_height="fill_parent"
+        android:paddingTop="8dp"
+        android:paddingBottom="8dp"
+        android:elevation="2dp">
+
+        <com.android.systemui.qs.customize.CustomQSPanel
+            android:id="@+id/quick_settings_panel"
+            android:background="#0000"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content" />
+
+    </com.android.systemui.tuner.AutoScrollView>
+
+</com.android.systemui.qs.customize.QSCustomizer>
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index b732e99..da7eadc 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -1156,4 +1156,7 @@
          settings are -->
     <string name="experimental">Experimental</string>
 
+    <string name="save" translatable="false">Save</string>
+    <string name="qs_customize" translatable="false">Allow long-press customize in Quick Settings</string>
+
 </resources>
diff --git a/packages/SystemUI/res/xml/tuner_prefs.xml b/packages/SystemUI/res/xml/tuner_prefs.xml
index 5980108..07e7688d 100644
--- a/packages/SystemUI/res/xml/tuner_prefs.xml
+++ b/packages/SystemUI/res/xml/tuner_prefs.xml
@@ -37,6 +37,10 @@
                 android:key="qs_paged_panel"
                 android:title="@string/qs_paging" />
 
+            <com.android.systemui.tuner.TunerSwitch
+                android:key="qs_allow_customize"
+                android:title="@string/qs_customize" />
+
         </PreferenceCategory>
 
     </PreferenceScreen>
diff --git a/packages/SystemUI/src/com/android/systemui/qs/PagedTileLayout.java b/packages/SystemUI/src/com/android/systemui/qs/PagedTileLayout.java
index ece7022..c612600 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/PagedTileLayout.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/PagedTileLayout.java
@@ -75,8 +75,8 @@
     @Override
     public void setTileVisibility(TileRecord tile, int visibility) {
         tile.tileView.setVisibility(visibility);
-        // TODO: Do something smarter here.
-        distributeTiles();
+//        // TODO: Do something smarter here.
+//        distributeTiles();
     }
 
     @Override
@@ -183,13 +183,17 @@
             mAllowDual = false;
         }
 
+        public void setMaxRows(int maxRows) {
+            mMaxRows = maxRows;
+        }
+
         private void clear() {
             if (DEBUG) Log.d(TAG, "Clearing page");
             removeAllViews();
             mRecords.clear();
         }
 
-        private boolean isFull() {
+        public boolean isFull() {
             return mRecords.size() >= mColumns * mMaxRows;
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java b/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java
index 683af97..880349e 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java
@@ -39,6 +39,7 @@
 import com.android.systemui.FontSizeUtils;
 import com.android.systemui.R;
 import com.android.systemui.qs.QSTile.DetailAdapter;
+import com.android.systemui.qs.customize.QSCustomizer;
 import com.android.systemui.settings.BrightnessController;
 import com.android.systemui.settings.ToggleSlider;
 import com.android.systemui.statusbar.phone.QSTileHost;
@@ -54,8 +55,9 @@
 
     public static final String QS_SHOW_BRIGHTNESS = "qs_show_brightness";
     public static final String QS_PAGED_PANEL = "qs_paged_panel";
+    public static final String QS_ALLOW_CUSTOMIZE = "qs_allow_customize";
 
-    private final Context mContext;
+    protected final Context mContext;
     protected final ArrayList<TileRecord> mRecords = new ArrayList<TileRecord>();
     private final View mDetail;
     private final ViewGroup mDetailContent;
@@ -79,8 +81,10 @@
     private QSFooter mFooter;
     private boolean mGridContentVisible = true;
 
-    private LinearLayout mQsContainer;
-    private QSTileLayout mTileLayout;
+    protected LinearLayout mQsContainer;
+    protected QSTileLayout mTileLayout;
+
+    private QSCustomizer mCustomizePanel;
 
     public QSPanel(Context context) {
         this(context, null);
@@ -131,7 +135,8 @@
     @Override
     protected void onAttachedToWindow() {
         super.onAttachedToWindow();
-        TunerService.get(mContext).addTunable(this, QS_SHOW_BRIGHTNESS, QS_PAGED_PANEL);
+        TunerService.get(mContext).addTunable(this,
+                QS_SHOW_BRIGHTNESS, QS_PAGED_PANEL, QS_ALLOW_CUSTOMIZE);
     }
 
     @Override
@@ -160,6 +165,17 @@
             for (int i = 0; i < mRecords.size(); i++) {
                 mTileLayout.addTile(mRecords.get(i));
             }
+        } else if (QS_ALLOW_CUSTOMIZE.equals(key)) {
+            if (newValue != null && Integer.parseInt(newValue) != 0) {
+                mCustomizePanel = (QSCustomizer) LayoutInflater.from(mContext)
+                        .inflate(R.layout.qs_customize_panel, null);
+                mCustomizePanel.setHost(mHost);
+            } else {
+                if (mCustomizePanel != null && mCustomizePanel.isCustomizing()) {
+                    mCustomizePanel.hide();
+                }
+                mCustomizePanel = null;
+            }
         }
     }
 
@@ -224,6 +240,12 @@
         mFooter.onConfigurationChanged();
     }
 
+    public void onCollapse() {
+        if (mCustomizePanel != null && mCustomizePanel.isCustomizing()) {
+            mCustomizePanel.hide();
+        }
+    }
+
     public void setExpanded(boolean expanded) {
         if (mExpanded == expanded) return;
         mExpanded = expanded;
@@ -307,7 +329,7 @@
         r.tileView.onStateChanged(state);
     }
 
-    private void addTile(final QSTile<?> tile) {
+    protected void addTile(final QSTile<?> tile) {
         final TileRecord r = new TileRecord();
         r.tile = tile;
         r.tileView = tile.createTileView(mContext);
@@ -358,7 +380,13 @@
         final View.OnLongClickListener longClick = new View.OnLongClickListener() {
             @Override
             public boolean onLongClick(View v) {
-                r.tile.longClick();
+                if (mCustomizePanel != null) {
+                    if (!mCustomizePanel.isCustomizing()) {
+                        mCustomizePanel.show();
+                    }
+                } else {
+                    r.tile.longClick();
+                }
                 return true;
             }
         };
@@ -374,10 +402,16 @@
     }
 
     public boolean isShowingDetail() {
-        return mDetailRecord != null;
+        return mDetailRecord != null
+                || (mCustomizePanel != null && mCustomizePanel.isCustomizing());
     }
 
     public void closeDetail() {
+        if (mCustomizePanel != null && mCustomizePanel.isCustomizing()) {
+            // Treat this as a detail panel for now, to make things easy.
+            mCustomizePanel.hide();
+            return;
+        }
         showDetail(false, mDetailRecord);
     }
 
@@ -527,7 +561,7 @@
         int y;
     }
 
-    protected static final class TileRecord extends Record {
+    public static final class TileRecord extends Record {
         public QSTile<?> tile;
         public QSTileView tileView;
         public int row;
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSTile.java b/packages/SystemUI/src/com/android/systemui/qs/QSTile.java
index 9b3372c3..3b3593b 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSTile.java
@@ -65,6 +65,8 @@
     private TState mTmpState = newTileState();
     private boolean mAnnounceNextStateChange;
 
+    private String mTileSpec;
+
     abstract protected TState newTileState();
     abstract protected void handleClick();
     abstract protected void handleUpdateState(TState state, Object arg);
@@ -83,7 +85,15 @@
         mContext = host.getContext();
         mHandler = new H(host.getLooper());
     }
-    
+
+    public String getTileSpec() {
+        return mTileSpec;
+    }
+
+    public void setTileSpec(String tileSpec) {
+        mTileSpec = tileSpec;
+    }
+
     public int getTileType() {
         return QSTileView.QS_TYPE_NORMAL;
     }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/TileLayout.java b/packages/SystemUI/src/com/android/systemui/qs/TileLayout.java
index e0c39c5..8bd05fa 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/TileLayout.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/TileLayout.java
@@ -54,6 +54,11 @@
         removeView(tile.tileView);
     }
 
+    public void removeAllViews() {
+        mRecords.clear();
+        super.removeAllViews();
+    }
+
     @Override
     public void setTileVisibility(TileRecord tile, int visibility) {
         tile.tileView.setVisibility(visibility);
diff --git a/packages/SystemUI/src/com/android/systemui/qs/customize/CustomQSPanel.java b/packages/SystemUI/src/com/android/systemui/qs/customize/CustomQSPanel.java
new file mode 100644
index 0000000..fe75220
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/customize/CustomQSPanel.java
@@ -0,0 +1,113 @@
+/*
+ * Copyright (C) 2015 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.customize;
+
+import android.content.ClipData;
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.View.OnTouchListener;
+
+import com.android.systemui.R;
+import com.android.systemui.qs.QSPanel;
+import com.android.systemui.qs.QSTile;
+import com.android.systemui.qs.QSTileView;
+import com.android.systemui.statusbar.phone.QSTileHost;
+
+/**
+ * A version of QSPanel that allows tiles to be dragged around rather than
+ * clicked on.  Dragging is started here, receiving is handled in the NonPagedTileLayout,
+ * and the saving/ordering is handled by the CustomQSTileHost.
+ */
+public class CustomQSPanel extends QSPanel implements OnTouchListener {
+
+    private CustomQSTileHost mCustomHost;
+    private ClipData mCurrentClip;
+    private View mCurrentView;
+
+    public CustomQSPanel(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        mTileLayout = (QSTileLayout) LayoutInflater.from(mContext)
+                .inflate(R.layout.qs_customize_layout, mQsContainer, false);
+        mQsContainer.addView((View) mTileLayout, 1 /* Between brightness and footer */);
+        ((NonPagedTileLayout) mTileLayout).setCustomQsPanel(this);
+    }
+
+    @Override
+    public void setHost(QSTileHost host) {
+        super.setHost(host);
+        mCustomHost = (CustomQSTileHost) host;
+    }
+
+    @Override
+    public void onTuningChanged(String key, String newValue) {
+        if (key.equals(QS_SHOW_BRIGHTNESS)) {
+            // No Brightness for you.
+            super.onTuningChanged(key, "0");
+        }
+    }
+
+    @Override
+    protected void addTile(QSTile<?> tile) {
+        super.addTile(tile);
+        if (tile.getTileType() != QSTileView.QS_TYPE_QUICK) {
+            TileRecord record = mRecords.get(mRecords.size() - 1);
+            if (record.tileView.getTag() == record.tile) {
+                return;
+            }
+            record.tileView.setTag(record.tile);
+            record.tileView.setVisibility(View.VISIBLE);
+            record.tileView.init(null, null, null);
+            record.tileView.setOnTouchListener(this);
+            if (mCurrentClip != null
+                    && mCurrentClip.getItemAt(0).getText().toString().equals(tile.getTileSpec())) {
+                record.tileView.setAlpha(.3f);
+                mCurrentView = record.tileView;
+            }
+        }
+    }
+
+    public void tileSelected(View v) {
+        String sourceSpec = mCurrentClip.getItemAt(0).getText().toString();
+        String destSpec = ((QSTile<?>) v.getTag()).getTileSpec();
+        if (!sourceSpec.equals(destSpec)) {
+            mCustomHost.moveTo(sourceSpec, destSpec);
+        }
+    }
+
+    @Override
+    public boolean onTouch(View v, MotionEvent event) {
+        switch (event.getAction()) {
+            case MotionEvent.ACTION_DOWN:
+                String tileSpec = (String) ((QSTile<?>) v.getTag()).getTileSpec();
+                mCurrentView = v;
+                mCurrentClip = ClipData.newPlainText(tileSpec, tileSpec);
+                View.DragShadowBuilder shadow = new View.DragShadowBuilder(v);
+                ((View) getParent().getParent()).startDrag(mCurrentClip, shadow, null, 0);
+                v.setAlpha(.3f);
+                return true;
+        }
+        return false;
+    }
+
+    public void onDragEnded() {
+        mCurrentView.setAlpha(1f);
+        mCurrentView = null;
+        mCurrentClip = null;
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/customize/CustomQSTileHost.java b/packages/SystemUI/src/com/android/systemui/qs/customize/CustomQSTileHost.java
new file mode 100644
index 0000000..0951129
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/customize/CustomQSTileHost.java
@@ -0,0 +1,167 @@
+/*
+ * Copyright (C) 2015 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.customize;
+
+import android.app.ActivityManager;
+import android.content.Context;
+import android.provider.Settings.Secure;
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.android.internal.logging.MetricsLogger;
+import com.android.systemui.qs.QSTile;
+import com.android.systemui.statusbar.phone.QSTileHost;
+import com.android.systemui.statusbar.policy.SecurityController;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * @see CustomQSPanel
+ */
+public class CustomQSTileHost extends QSTileHost {
+
+    private static final String TAG = "CustomHost";
+    private List<String> mTiles;
+    private List<String> mSavedTiles;
+
+    public CustomQSTileHost(Context context, QSTileHost host) {
+        super(context, null, host.getBluetoothController(), host.getLocationController(),
+                host.getRotationLockController(), host.getNetworkController(),
+                host.getZenModeController(), host.getHotspotController(), host.getCastController(),
+                host.getFlashlightController(), host.getUserSwitcherController(),
+                host.getKeyguardMonitor(), new BlankSecurityController());
+    }
+
+    @Override
+    protected QSTile<?> createTile(String tileSpec) {
+        QSTile<?> tile = super.createTile(tileSpec);
+        tile.setTileSpec(tileSpec);
+        return tile;
+    }
+
+    @Override
+    public void onTuningChanged(String key, String newValue) {
+        // No Tunings For You.
+        if (TILES_SETTING.equals(key)) {
+            mSavedTiles = super.loadTileSpecs(newValue);
+        }
+    }
+
+    public void setSavedTiles() {
+        setTiles(mSavedTiles);
+    }
+
+    public void saveCurrentTiles() {
+        Secure.putStringForUser(getContext().getContentResolver(), TILES_SETTING,
+                TextUtils.join(",", mTiles), ActivityManager.getCurrentUser());
+    }
+
+    public void moveTo(String from, String to) {
+        int fromIndex = mTiles.indexOf(from);
+        if (fromIndex < 0) {
+            Log.e(TAG, "Unknown from tile " + from);
+            return;
+        }
+        int index = mTiles.indexOf(to);
+        if (index < 0) {
+            Log.e(TAG, "Unknown to tile " + to);
+            return;
+        }
+        mTiles.remove(fromIndex);
+        mTiles.add(index, from);
+        super.onTuningChanged(TILES_SETTING, null);
+    }
+
+    public void setTiles(List<String> tiles) {
+        mTiles = new ArrayList<>(tiles);
+        super.onTuningChanged(TILES_SETTING, null);
+    }
+
+    @Override
+    protected List<String> loadTileSpecs(String tileList) {
+        return mTiles;
+    }
+
+    public void replace(String oldTile, String newTile) {
+        if (oldTile.equals(newTile)) {
+            return;
+        }
+        MetricsLogger.action(getContext(), MetricsLogger.TUNER_QS_REORDER, oldTile + ","
+                + newTile);
+        List<String> order = new ArrayList<>(mTileSpecs);
+        int index = order.indexOf(oldTile);
+        if (index < 0) {
+            Log.e(TAG, "Can't find " + oldTile);
+            return;
+        }
+        order.remove(newTile);
+        order.add(index, newTile);
+        setTiles(order);
+    }
+
+    /**
+     * Blank so that the customizing QS view doesn't show any security messages in the footer.
+     */
+    private static class BlankSecurityController implements SecurityController {
+        @Override
+        public boolean hasDeviceOwner() {
+            return false;
+        }
+
+        @Override
+        public boolean hasProfileOwner() {
+            return false;
+        }
+
+        @Override
+        public String getDeviceOwnerName() {
+            return null;
+        }
+
+        @Override
+        public String getProfileOwnerName() {
+            return null;
+        }
+
+        @Override
+        public boolean isVpnEnabled() {
+            return false;
+        }
+
+        @Override
+        public String getPrimaryVpnName() {
+            return null;
+        }
+
+        @Override
+        public String getProfileVpnName() {
+            return null;
+        }
+
+        @Override
+        public void onUserSwitched(int newUserId) {
+        }
+
+        @Override
+        public void addCallback(SecurityControllerCallback callback) {
+        }
+
+        @Override
+        public void removeCallback(SecurityControllerCallback callback) {
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/customize/NonPagedTileLayout.java b/packages/SystemUI/src/com/android/systemui/qs/customize/NonPagedTileLayout.java
new file mode 100644
index 0000000..47c9384
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/customize/NonPagedTileLayout.java
@@ -0,0 +1,167 @@
+/*
+ * Copyright (C) 2015 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.customize;
+
+import android.content.Context;
+import android.graphics.Rect;
+import android.util.AttributeSet;
+import android.view.DragEvent;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.LinearLayout;
+
+import com.android.systemui.R;
+import com.android.systemui.qs.PagedTileLayout;
+import com.android.systemui.qs.PagedTileLayout.TilePage;
+import com.android.systemui.qs.QSPanel.QSTileLayout;
+import com.android.systemui.qs.QSPanel.TileRecord;
+import com.android.systemui.qs.QSTileView;
+import com.android.systemui.qs.QuickTileLayout;
+
+import java.util.ArrayList;
+
+/**
+ * Similar to PagedTileLayout, except that instead of pages it lays them out
+ * vertically and expects to be inside a ScrollView.
+ * @see CustomQSPanel
+ */
+public class NonPagedTileLayout extends LinearLayout implements QSTileLayout {
+
+    private QuickTileLayout mQuickTiles;
+    private final ArrayList<TilePage> mPages = new ArrayList<>();
+    private final ArrayList<TileRecord> mTiles = new ArrayList<TileRecord>();
+    private CustomQSPanel mPanel;
+    private final Rect mHitRect = new Rect();
+
+    public NonPagedTileLayout(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    @Override
+    protected void onFinishInflate() {
+        super.onFinishInflate();
+        mQuickTiles = (QuickTileLayout) findViewById(R.id.quick_tile_layout);
+        TilePage page = (PagedTileLayout.TilePage) findViewById(R.id.tile_page);
+        page.setMaxRows(3 /* First page only gets 3 */);
+        mPages.add(page);
+    }
+
+    public void setCustomQsPanel(CustomQSPanel qsPanel) {
+        mPanel = qsPanel;
+    }
+
+    private void clear() {
+        mQuickTiles.removeAllViews();
+        for (int i = 0; i < mPages.size(); i++) {
+            mPages.get(i).removeAllViews();
+        }
+    }
+
+    @Override
+    public void addTile(TileRecord tile) {
+        mTiles.add(tile);
+        distributeTiles();
+    }
+
+    @Override
+    public void removeTile(TileRecord tile) {
+        if (mTiles.remove(tile)) {
+            distributeTiles();
+        }
+    }
+
+    private void distributeTiles() {
+        mQuickTiles.removeAllViews();
+        final int NP = mPages.size();
+        for (int i = 0; i < NP; i++) {
+            mPages.get(i).removeAllViews();
+        }
+        int index = 0;
+        final int NT = mTiles.size();
+        for (int i = 0; i < NT; i++) {
+            TileRecord tile = mTiles.get(i);
+            if (tile.tile.getTileType() == QSTileView.QS_TYPE_QUICK) {
+                tile.tileView.setType(QSTileView.QS_TYPE_QUICK);
+                mQuickTiles.addView(tile.tileView);
+                continue;
+            }
+            mPages.get(index).addTile(tile);
+            if (mPages.get(index).isFull()) {
+                if (++index == mPages.size()) {
+                    LayoutInflater inflater = LayoutInflater.from(mContext);
+                    inflater.inflate(R.layout.horizontal_divider, this);
+                    mPages.add((TilePage) inflater.inflate(R.layout.qs_paged_page, this, false));
+                    addView(mPages.get(mPages.size() - 1));
+                }
+            }
+        }
+    }
+
+    @Override
+    public void setTileVisibility(TileRecord tile, int visibility) {
+        // All tiles visible here, so that they can be re-arranged.
+        tile.tileView.setVisibility(View.VISIBLE);
+    }
+
+    @Override
+    public int getOffsetTop(TileRecord tile) {
+        // TODO: Fix this.
+        return getTop();
+    }
+
+    @Override
+    public void updateResources() {
+    }
+
+    @Override
+    public boolean onDragEvent(DragEvent event) {
+        switch (event.getAction()) {
+            case DragEvent.ACTION_DRAG_LOCATION:
+                float x = event.getX();
+                float y = event.getY();
+                if (contains(mQuickTiles, x, y)) {
+                    // TODO: Reset to pre-drag state.
+                } else {
+                    final int NP = mPages.size();
+                    for (int i = 0; i < NP; i++) {
+                        TilePage page = mPages.get(i);
+                        if (contains(page, x, y)) {
+                            x -= page.getLeft();
+                            y -= page.getTop();
+                            final int NC = page.getChildCount();
+                            for (int j = 0; j < NC; j++) {
+                                View child = page.getChildAt(j);
+                                if (contains(child, x, y)) {
+                                    mPanel.tileSelected(child);
+                                }
+                            }
+                            break;
+                        }
+                    }
+                }
+                break;
+            case DragEvent.ACTION_DRAG_ENDED:
+                mPanel.onDragEnded();
+                break;
+        }
+        return true;
+    }
+
+    private boolean contains(View v, float x, float y) {
+        v.getHitRect(mHitRect);
+        return mHitRect.contains((int) x, (int) y);
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/customize/QSCustomizer.java b/packages/SystemUI/src/com/android/systemui/qs/customize/QSCustomizer.java
new file mode 100644
index 0000000..7724a87
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/customize/QSCustomizer.java
@@ -0,0 +1,142 @@
+/*
+ * Copyright (C) 2015 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.customize;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.util.TypedValue;
+import android.view.ContextThemeWrapper;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.widget.LinearLayout;
+import android.widget.Toolbar;
+import android.widget.Toolbar.OnMenuItemClickListener;
+
+import com.android.systemui.R;
+import com.android.systemui.SystemUIApplication;
+import com.android.systemui.qs.QSTile.Host.Callback;
+import com.android.systemui.statusbar.phone.PhoneStatusBar;
+import com.android.systemui.statusbar.phone.QSTileHost;
+import com.android.systemui.tuner.QSPagingSwitch;
+
+import java.util.ArrayList;
+
+/**
+ * Allows full-screen customization of QS, through show() and hide().
+ *
+ * This adds itself to the status bar window, so it can appear on top of quick settings and
+ * *someday* do fancy animations to get into/out of it.
+ */
+public class QSCustomizer extends LinearLayout implements OnMenuItemClickListener, Callback {
+
+    private static final int MENU_SAVE = Menu.FIRST;
+    private static final int MENU_RESET = Menu.FIRST + 1;
+
+    private PhoneStatusBar mPhoneStatusBar;
+
+    private Toolbar mToolbar;
+    private CustomQSPanel mQsPanel;
+
+    private boolean isShown;
+    private CustomQSTileHost mHost;
+
+    public QSCustomizer(Context context, AttributeSet attrs) {
+        super(new ContextThemeWrapper(context, android.R.style.Theme_Material), attrs);
+        mPhoneStatusBar = ((SystemUIApplication) mContext.getApplicationContext())
+                .getComponent(PhoneStatusBar.class);
+    }
+
+    public void setHost(QSTileHost host) {
+        mHost = new CustomQSTileHost(mContext, host);
+        mHost.setCallback(this);
+        mQsPanel.setTiles(mHost.getTiles());
+        mQsPanel.setHost(mHost);
+        mHost.setSavedTiles();
+    }
+
+    @Override
+    protected void onFinishInflate() {
+        super.onFinishInflate();
+        mToolbar = (Toolbar) findViewById(com.android.internal.R.id.action_bar);
+        TypedValue value = new TypedValue();
+        mContext.getTheme().resolveAttribute(android.R.attr.homeAsUpIndicator, value, true);
+        mToolbar.setNavigationIcon(
+                getResources().getDrawable(value.resourceId, mContext.getTheme()));
+        mToolbar.setNavigationOnClickListener(new OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                // TODO: Is this all we want...?
+                hide();
+            }
+        });
+        mToolbar.setOnMenuItemClickListener(this);
+        mToolbar.getMenu().add(Menu.NONE, MENU_SAVE, 0, mContext.getString(R.string.save))
+                .setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
+        mToolbar.getMenu().add(Menu.NONE, MENU_RESET, 0,
+                mContext.getString(com.android.internal.R.string.reset));
+
+        mQsPanel = (CustomQSPanel) findViewById(R.id.quick_settings_panel);
+    }
+
+    public void show() {
+        isShown = true;
+        mHost.setSavedTiles();
+        // TODO: Fancy shmancy reveal.
+        mPhoneStatusBar.getStatusBarWindow().addView(this);
+    }
+
+    public void hide() {
+        isShown = false;
+        // TODO: Similarly awesome or better hide.
+        mPhoneStatusBar.getStatusBarWindow().removeView(this);
+    }
+
+    public boolean isCustomizing() {
+        return isShown;
+    }
+
+    private void reset() {
+        ArrayList<String> tiles = new ArrayList<>();
+        for (String tile : QSPagingSwitch.QS_PAGE_TILES.split(",")) {
+            tiles.add(tile);
+        }
+        mHost.setTiles(tiles);
+    }
+
+    private void save() {
+        mHost.saveCurrentTiles();
+        hide();
+    }
+
+    @Override
+    public boolean onMenuItemClick(MenuItem item) {
+        switch (item.getItemId()) {
+            case MENU_SAVE:
+                save();
+                break;
+            case MENU_RESET:
+                reset();
+                break;
+        }
+        return true;
+    }
+
+    @Override
+    public void onTilesChanged() {
+        mQsPanel.setTiles(mHost.getTiles());
+    }
+}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/tuner/QSPagingSwitch.java b/packages/SystemUI/src/com/android/systemui/tuner/QSPagingSwitch.java
index 343a231..4387b33 100644
--- a/packages/SystemUI/src/com/android/systemui/tuner/QSPagingSwitch.java
+++ b/packages/SystemUI/src/com/android/systemui/tuner/QSPagingSwitch.java
@@ -8,7 +8,7 @@
 
 public class QSPagingSwitch extends TunerSwitch {
 
-    private static final String QS_PAGE_TILES =
+    public static final String QS_PAGE_TILES =
             "dwifi,dbt,inversion,dnd,cell,airplane,rotation,flashlight,location,"
              + "hotspot,qwifi,qbt,qrotation,qflashlight,qairplane,cast";