Implement Settings UI for Screen zoom preference

Also cleans up unnecessary 0s and moves tint to the proper place in
relevant vector drawable XML files.

Preview layout is still WIP pending UX input, but I wanted to get this
CL in place so that we can test/demo the feature ASAP.

Bug: 22450672
Change-Id: Ic3f1500006c763df99fba2d1d16782b89d6fcae1
diff --git a/src/com/android/settings/DisplayDensityPreference.java b/src/com/android/settings/DisplayDensityPreference.java
deleted file mode 100644
index 85f2fb2..0000000
--- a/src/com/android/settings/DisplayDensityPreference.java
+++ /dev/null
@@ -1,247 +0,0 @@
-/*
- * 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.settings;
-
-import android.content.Context;
-import android.content.res.Resources;
-import android.os.AsyncTask;
-import android.os.RemoteException;
-import android.support.annotation.ArrayRes;
-import android.support.v7.preference.ListPreference;
-import android.util.AttributeSet;
-import android.util.DisplayMetrics;
-import android.util.Log;
-import android.util.MathUtils;
-import android.view.Display;
-import android.view.IWindowManager;
-import android.view.WindowManagerGlobal;
-
-import com.android.settings.R;
-
-import java.util.Arrays;
-
-/**
- * Preference for changing the density of the display on which the preference
- * is visible.
- */
-public class DisplayDensityPreference extends ListPreference {
-    private static final String LOG_TAG = "DisplayDensityPreference";
-
-    /** Minimum increment between density scales. */
-    private static final float MIN_SCALE_INTERVAL = 0.09f;
-
-    /** Minimum density scale. This is available on all devices. */
-    private static final float MIN_SCALE = 0.85f;
-
-    /** Maximum density scale. The actual scale used depends on the device. */
-    private static final float MAX_SCALE = 1.50f;
-
-    /** Sentinel value for "normal" scaling (effectively disabled). */
-    private static final int DENSITY_VALUE_NORMAL = -1;
-
-    /** Summary used for "normal" scale. */
-    private static final int DENSITY_SUMMARY_NORMAL = R.string.force_density_summary_normal;
-
-    /**
-     * Summaries for scales smaller than "normal" in order of smallest to
-     * largest.
-     */
-    private static final int[] SMALLER_SUMMARIES = new int[] {
-            R.string.force_density_summary_small
-    };
-
-    /**
-     * Summaries for scales larger than "normal" in order of smallest to
-     * largest.
-     */
-    private static final int[] LARGER_SUMMARIES = new int[] {
-            R.string.force_density_summary_large,
-            R.string.force_density_summary_very_large,
-            R.string.force_density_summary_extremely_large,
-    };
-
-    /**
-     * Minimum allowed screen dimension, corresponds to resource qualifiers
-     * "small" or "sw320dp". This value must be at least the minimum screen
-     * size required by the CDD so that we meet developer expectations.
-     */
-    private static final int MIN_DIMENSION_DP = 320;
-
-    /** The ID of the display affected by this preference. */
-    private int mDisplayId = Display.DEFAULT_DISPLAY;
-
-    public DisplayDensityPreference(Context context, AttributeSet attrs) {
-        super(context, attrs);
-
-        if (!prepareList()) {
-            setEnabled(false);
-        }
-    }
-
-    private boolean prepareList() {
-        final int initialDensity = getInitialDisplayDensity(mDisplayId);
-        if (initialDensity <= 0) {
-            return false;
-        }
-
-        final Resources res = getContext().getResources();
-        final DisplayMetrics metrics = res.getDisplayMetrics();
-        final int currentDensity = metrics.densityDpi;
-        int currentDensityIndex = -1;
-
-        // Compute number of "larger" and "smaller" scales for this display.
-        final int minDimensionPx = Math.min(metrics.widthPixels, metrics.heightPixels);
-        final int maxDensity = DisplayMetrics.DENSITY_MEDIUM * minDimensionPx / MIN_DIMENSION_DP;
-        final float maxScale = Math.min(MAX_SCALE, maxDensity / (float) initialDensity);
-        final float minScale = MIN_SCALE;
-        final int numLarger = (int) MathUtils.constrain((maxScale - 1) / MIN_SCALE_INTERVAL,
-                0, LARGER_SUMMARIES.length);
-        final int numSmaller = (int) MathUtils.constrain((1 - minScale) / MIN_SCALE_INTERVAL,
-                0, SMALLER_SUMMARIES.length);
-
-        CharSequence[] values = new CharSequence[1 + numSmaller + numLarger];
-        CharSequence[] entries = new CharSequence[values.length];
-        int curIndex = 0;
-
-        if (numSmaller > 0) {
-            final float interval = (1 - minScale) / numSmaller;
-            for (int i = numSmaller - 1; i >= 0; i--) {
-                final int density = (int) (initialDensity * (1 - (i + 1) * interval));
-                if (currentDensity == density) {
-                    currentDensityIndex = curIndex;
-                }
-                values[curIndex] = Integer.toString(density);
-                entries[curIndex] = res.getText(SMALLER_SUMMARIES[i]);
-                curIndex++;
-            }
-        }
-
-        if (currentDensity == initialDensity) {
-            currentDensityIndex = curIndex;
-        }
-        values[curIndex] = Integer.toString(DENSITY_VALUE_NORMAL);
-        entries[curIndex] = res.getText(DENSITY_SUMMARY_NORMAL);
-        curIndex++;
-
-        if (numLarger > 0) {
-            final float interval = (maxScale - 1) / numLarger;
-            for (int i = 0; i < numLarger; i++) {
-                final int density = (int) (initialDensity * (1 + (i + 1) * interval));
-                if (currentDensity == density) {
-                    currentDensityIndex = curIndex;
-                }
-                values[curIndex] = Integer.toString(density);
-                entries[curIndex] = res.getText(LARGER_SUMMARIES[i]);
-                curIndex++;
-            }
-        }
-
-        final int displayIndex;
-        if (currentDensityIndex >= 0) {
-            displayIndex = currentDensityIndex;
-        } else {
-            // We don't understand the current density. Must have been set by
-            // someone else. Make room for another entry...
-            values = Arrays.copyOf(values, values.length + 1);
-            values[curIndex] = res.getString(R.string.force_density_summary_custom, currentDensity);
-
-            entries = Arrays.copyOf(entries, values.length + 1);
-            entries[curIndex] = Integer.toString(currentDensity);
-
-            displayIndex = curIndex;
-        }
-
-        super.setEntryValues(values);
-        super.setEntries(entries);
-
-        setValueIndex(displayIndex);
-
-        return true;
-    }
-
-    @Override
-    public boolean callChangeListener(Object newValue) {
-        final boolean allowed = super.callChangeListener(newValue);
-        if (allowed) {
-            final int density = Integer.parseInt((String) newValue);
-            setForcedDisplayDensity(mDisplayId, density);
-        }
-
-        return allowed;
-    }
-
-    @Override
-    public void setEntries(CharSequence[] entries) {
-        throw new UnsupportedOperationException();
-    }
-
-    @Override
-    public void setEntries(@ArrayRes int entriesResId) {
-        throw new UnsupportedOperationException();
-    }
-
-    @Override
-    public void setEntryValues(CharSequence[] entryValues) {
-        throw new UnsupportedOperationException();
-    }
-
-    @Override
-    public void setEntryValues(@ArrayRes int entryValuesResId) {
-        throw new UnsupportedOperationException();
-    }
-
-    /**
-     * Returns the initial density for the specified display.
-     *
-     * @param displayId the identifier of the display
-     * @return the initial density of the specified display, or {@code -1} if
-     *         the display does not exist or the density could not be obtained
-     */
-    private static int getInitialDisplayDensity(int displayId) {
-        try {
-            IWindowManager wm = WindowManagerGlobal.getWindowManagerService();
-            return wm.getInitialDisplayDensity(displayId);
-        } catch (RemoteException exc) {
-            return -1;
-        }
-    }
-
-    /**
-     * Asynchronously applies display density changes to the specified display.
-     *
-     * @param displayId the identifier of the display to modify
-     * @param density the density to force for the specified display, or <= 0
-     *                to clear any previously forced density
-     */
-    private static void setForcedDisplayDensity(final int displayId, final int density) {
-        AsyncTask.execute(new Runnable() {
-            @Override
-            public void run() {
-                try {
-                    IWindowManager wm = WindowManagerGlobal.getWindowManagerService();
-                    if (density <= 0) {
-                        wm.clearForcedDisplayDensity(displayId);
-                    } else {
-                        wm.setForcedDisplayDensity(displayId, density);
-                    }
-                } catch (RemoteException exc) {
-                    Log.w(LOG_TAG, "Unable to save forced display density setting");
-                }
-            }
-        });
-    }
-}
diff --git a/src/com/android/settings/DisplaySettings.java b/src/com/android/settings/DisplaySettings.java
index 0d52aa7..e9ab1f7 100644
--- a/src/com/android/settings/DisplaySettings.java
+++ b/src/com/android/settings/DisplaySettings.java
@@ -76,7 +76,6 @@
     private static final String KEY_CAMERA_GESTURE = "camera_gesture";
     private static final String KEY_CAMERA_DOUBLE_TAP_POWER_GESTURE
             = "camera_double_tap_power_gesture";
-    private static final String KEY_DISPLAY_DENSITY = "display_density";
 
     private DropDownPreference mFontSizePref;
 
@@ -214,9 +213,6 @@
             mNightModePreference.setValue(String.valueOf(currentNightMode));
             mNightModePreference.setOnPreferenceChangeListener(this);
         }
-
-        final Preference displayDensity = findPreference(KEY_DISPLAY_DENSITY);
-        displayDensity.setOnPreferenceChangeListener(this);
     }
 
     private static boolean allowAllRotations(Context context) {
diff --git a/src/com/android/settings/InstrumentedFragment.java b/src/com/android/settings/InstrumentedFragment.java
index 44fd110..cc0014d 100644
--- a/src/com/android/settings/InstrumentedFragment.java
+++ b/src/com/android/settings/InstrumentedFragment.java
@@ -32,6 +32,7 @@
     public static final int SOUND = UNDECLARED + 2;
     public static final int CONFIGURE_NOTIFICATION = UNDECLARED + 3;
     public static final int CONFIGURE_WIFI = UNDECLARED + 4;
+    public static final int DISPLAY_SCREEN_ZOOM = UNDECLARED + 5;
 
     /**
      * Declare the view of this category.
diff --git a/src/com/android/settings/display/DisplayDensityUtils.java b/src/com/android/settings/display/DisplayDensityUtils.java
new file mode 100644
index 0000000..cef5418
--- /dev/null
+++ b/src/com/android/settings/display/DisplayDensityUtils.java
@@ -0,0 +1,240 @@
+/*
+ * 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.settings.display;
+
+import com.android.settings.R;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.os.AsyncTask;
+import android.os.RemoteException;
+import android.util.DisplayMetrics;
+import android.util.Log;
+import android.util.MathUtils;
+import android.view.Display;
+import android.view.IWindowManager;
+import android.view.WindowManagerGlobal;
+
+import java.util.Arrays;
+
+/**
+ * Utility methods for working with display density.
+ */
+class DisplayDensityUtils {
+    private static final String LOG_TAG = "DisplayDensityUtils";
+
+    /** Minimum increment between density scales. */
+    private static final float MIN_SCALE_INTERVAL = 0.09f;
+
+    /** Minimum density scale. This is available on all devices. */
+    private static final float MIN_SCALE = 0.85f;
+
+    /** Maximum density scale. The actual scale used depends on the device. */
+    private static final float MAX_SCALE = 1.50f;
+
+    /** Summary used for "normal" scale. */
+    private static final int SUMMARY_NORMAL = R.string.screen_zoom_summary_normal;
+
+    /** Summary used for "custom" scale. */
+    private static final int SUMMARY_CUSTOM = R.string.screen_zoom_summary_custom;
+
+    /**
+     * Summaries for scales smaller than "normal" in order of smallest to
+     * largest.
+     */
+    private static final int[] SUMMARIES_SMALLER = new int[] {
+            R.string.screen_zoom_summary_small
+    };
+
+    /**
+     * Summaries for scales larger than "normal" in order of smallest to
+     * largest.
+     */
+    private static final int[] SUMMARIES_LARGER = new int[] {
+            R.string.screen_zoom_summary_large,
+            R.string.screen_zoom_summary_very_large,
+            R.string.screen_zoom_summary_extremely_large,
+    };
+
+    /**
+     * Minimum allowed screen dimension, corresponds to resource qualifiers
+     * "small" or "sw320dp". This value must be at least the minimum screen
+     * size required by the CDD so that we meet developer expectations.
+     */
+    private static final int MIN_DIMENSION_DP = 320;
+
+    private final String[] mEntries;
+    private final int[] mValues;
+
+    private final int mNormalDensity;
+    private final int mCurrentIndex;
+
+    public DisplayDensityUtils(Context context) {
+        final int normalDensity = DisplayDensityUtils.getNormalDisplayDensity(
+                Display.DEFAULT_DISPLAY);
+        if (normalDensity <= 0) {
+            mEntries = null;
+            mValues = null;
+            mNormalDensity = 0;
+            mCurrentIndex = -1;
+            return;
+        }
+
+        final Resources res = context.getResources();
+        final DisplayMetrics metrics = res.getDisplayMetrics();
+        final int currentDensity = metrics.densityDpi;
+        int currentDensityIndex = -1;
+
+        // Compute number of "larger" and "smaller" scales for this display.
+        final int minDimensionPx = Math.min(metrics.widthPixels, metrics.heightPixels);
+        final int maxDensity = DisplayMetrics.DENSITY_MEDIUM * minDimensionPx / MIN_DIMENSION_DP;
+        final float maxScale = Math.min(MAX_SCALE, maxDensity / (float) normalDensity);
+        final float minScale = MIN_SCALE;
+        final int numLarger = (int) MathUtils.constrain((maxScale - 1) / MIN_SCALE_INTERVAL,
+                0, SUMMARIES_LARGER.length);
+        final int numSmaller = (int) MathUtils.constrain((1 - minScale) / MIN_SCALE_INTERVAL,
+                0, SUMMARIES_SMALLER.length);
+
+        String[] entries = new String[1 + numSmaller + numLarger];
+        int[] values = new int[entries.length];
+        int curIndex = 0;
+
+        if (numSmaller > 0) {
+            final float interval = (1 - minScale) / numSmaller;
+            for (int i = numSmaller - 1; i >= 0; i--) {
+                final int density = (int) (normalDensity * (1 - (i + 1) * interval));
+                if (currentDensity == density) {
+                    currentDensityIndex = curIndex;
+                }
+                entries[curIndex] = res.getString(SUMMARIES_SMALLER[i]);
+                values[curIndex] = density;
+                curIndex++;
+            }
+        }
+
+        if (currentDensity == normalDensity) {
+            currentDensityIndex = curIndex;
+        }
+        values[curIndex] = normalDensity;
+        entries[curIndex] = res.getString(SUMMARY_NORMAL);
+        curIndex++;
+
+        if (numLarger > 0) {
+            final float interval = (maxScale - 1) / numLarger;
+            for (int i = 0; i < numLarger; i++) {
+                final int density = (int) (normalDensity * (1 + (i + 1) * interval));
+                if (currentDensity == density) {
+                    currentDensityIndex = curIndex;
+                }
+                values[curIndex] = density;
+                entries[curIndex] = res.getString(SUMMARIES_LARGER[i]);
+                curIndex++;
+            }
+        }
+
+        final int displayIndex;
+        if (currentDensityIndex >= 0) {
+            displayIndex = currentDensityIndex;
+        } else {
+            // We don't understand the current density. Must have been set by
+            // someone else. Make room for another entry...
+            values = Arrays.copyOf(values, values.length + 1);
+            values[curIndex] = currentDensity;
+
+            entries = Arrays.copyOf(entries, values.length + 1);
+            entries[curIndex] = res.getString(SUMMARY_CUSTOM, currentDensity);
+
+            displayIndex = curIndex;
+        }
+
+        mNormalDensity = normalDensity;
+        mCurrentIndex = displayIndex;
+        mEntries = entries;
+        mValues = values;
+    }
+
+    public String[] getEntries() {
+        return mEntries;
+    }
+
+    public int[] getValues() {
+        return mValues;
+    }
+
+    public int getCurrentIndex() {
+        return mCurrentIndex;
+    }
+
+    public int getNormalDensity() {
+        return mNormalDensity;
+    }
+
+    /**
+     * Returns the normal (default) density for the specified display.
+     *
+     * @param displayId the identifier of the display
+     * @return the normal density of the specified display, or {@code -1} if
+     *         the display does not exist or the density could not be obtained
+     */
+    private static int getNormalDisplayDensity(int displayId) {
+       try {
+           final IWindowManager wm = WindowManagerGlobal.getWindowManagerService();
+           return wm.getInitialDisplayDensity(displayId);
+       } catch (RemoteException exc) {
+           return -1;
+       }
+    }
+
+    /**
+     * Asynchronously applies display density changes to the specified display.
+     *
+     * @param displayId the identifier of the display to modify
+     */
+    public static void clearForcedDisplayDensity(final int displayId) {
+        AsyncTask.execute(new Runnable() {
+            @Override
+            public void run() {
+                try {
+                    final IWindowManager wm = WindowManagerGlobal.getWindowManagerService();
+                    wm.clearForcedDisplayDensity(displayId);
+                } catch (RemoteException exc) {
+                    Log.w(LOG_TAG, "Unable to clear forced display density setting");
+                }
+            }
+        });
+    }
+
+    /**
+     * Asynchronously applies display density changes to the specified display.
+     *
+     * @param displayId the identifier of the display to modify
+     * @param density the density to force for the specified display
+     */
+    public static void setForcedDisplayDensity(final int displayId, final int density) {
+        AsyncTask.execute(new Runnable() {
+            @Override
+            public void run() {
+                try {
+                    final IWindowManager wm = WindowManagerGlobal.getWindowManagerService();
+                    wm.setForcedDisplayDensity(displayId, density);
+                } catch (RemoteException exc) {
+                    Log.w(LOG_TAG, "Unable to save forced display density setting");
+                }
+            }
+        });
+    }
+}
diff --git a/src/com/android/settings/display/ScreenZoomPreference.java b/src/com/android/settings/display/ScreenZoomPreference.java
new file mode 100644
index 0000000..78cc49c
--- /dev/null
+++ b/src/com/android/settings/display/ScreenZoomPreference.java
@@ -0,0 +1,53 @@
+/*
+ * 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.settings.display;
+
+import android.content.Context;
+import android.content.Intent;
+import android.support.v4.content.res.TypedArrayUtils;
+import android.support.v7.preference.PreferenceGroup;
+import android.util.AttributeSet;
+
+/**
+ * Preference for changing the density of the display on which the preference
+ * is visible.
+ */
+public class ScreenZoomPreference extends PreferenceGroup {
+    public ScreenZoomPreference(Context context, AttributeSet attrs) {
+        super(context, attrs, TypedArrayUtils.getAttr(context,
+                android.support.v7.preference.R.attr.preferenceScreenStyle,
+                android.R.attr.preferenceScreenStyle));
+
+        setFragment("com.android.settings.display.ScreenZoomSettings");
+
+        final DisplayDensityUtils density = new DisplayDensityUtils(context);
+        final int defaultIndex = density.getCurrentIndex();
+        if (defaultIndex < 0) {
+            setVisible(false);
+            setEnabled(false);
+        } else {
+            final String[] entries = density.getEntries();
+            final int currentIndex = density.getCurrentIndex();
+            setSummary(entries[currentIndex]);
+        }
+    }
+
+    @Override
+    protected boolean isOnSameScreenAsChildren() {
+        return false;
+    }
+}
diff --git a/src/com/android/settings/display/ScreenZoomSettings.java b/src/com/android/settings/display/ScreenZoomSettings.java
new file mode 100644
index 0000000..1ddcaab
--- /dev/null
+++ b/src/com/android/settings/display/ScreenZoomSettings.java
@@ -0,0 +1,272 @@
+/*
+ * 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.settings.display;
+
+import com.android.settings.InstrumentedFragment;
+import com.android.settings.R;
+import com.android.settings.SettingsPreferenceFragment;
+import com.android.settings.search.BaseSearchIndexProvider;
+import com.android.settings.search.Indexable;
+import com.android.settings.search.SearchIndexableRaw;
+
+import android.annotation.Nullable;
+import android.content.Context;
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.os.Bundle;
+import android.view.Display;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.view.animation.AccelerateInterpolator;
+import android.view.animation.DecelerateInterpolator;
+import android.view.animation.Interpolator;
+import android.widget.FrameLayout;
+import android.widget.SeekBar;
+import android.widget.SeekBar.OnSeekBarChangeListener;
+import android.widget.TextView;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Preference fragment used to control screen zoom.
+ */
+public class ScreenZoomSettings extends SettingsPreferenceFragment implements Indexable {
+    /** Duration to use when cross-fading between previews. */
+    private static final long CROSS_FADE_DURATION_MS = 400;
+
+    /** Interpolator to use when cross-fading between previews. */
+    private static final Interpolator FADE_IN_INTERPOLATOR = new DecelerateInterpolator();
+
+    /** Interpolator to use when cross-fading between previews. */
+    private static final Interpolator FADE_OUT_INTERPOLATOR = new AccelerateInterpolator();
+
+    private ViewGroup mPreviewFrame;
+    private TextView mLabel;
+    private View mLarger;
+    private View mSmaller;
+
+    private String[] mEntries;
+    private int[] mValues;
+    private int mNormalDensity;
+    private int mInitialIndex;
+
+    private int mCurrentIndex;
+
+    @Override
+    public void onCreate(@Nullable Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        final DisplayDensityUtils density = new DisplayDensityUtils(getContext());
+
+        final int initialIndex = density.getCurrentIndex();
+        if (initialIndex < 0) {
+            // Failed to obtain normal density, which means we failed to
+            // connect to the window manager service. Just use the current
+            // density and don't let the user change anything.
+            final int densityDpi = getResources().getDisplayMetrics().densityDpi;
+            mValues = new int[] { densityDpi };
+            mEntries = new String[] { getString(R.string.screen_zoom_summary_normal) };
+            mInitialIndex = 0;
+            mNormalDensity = densityDpi;
+        } else {
+            mValues = density.getValues();
+            mEntries = density.getEntries();
+            mInitialIndex = initialIndex;
+            mNormalDensity = density.getNormalDensity();
+        }
+    }
+
+    @Override
+    public View onCreateView(LayoutInflater inflater, ViewGroup container,
+            Bundle savedInstanceState) {
+        final View root = super.onCreateView(inflater, container, savedInstanceState);
+        final ViewGroup list_container = (ViewGroup) root.findViewById(android.R.id.list_container);
+        list_container.removeAllViews();
+
+        final View content = inflater.inflate(R.layout.screen_zoom_activity, list_container, false);
+        list_container.addView(content);
+
+        mLabel = (TextView) content.findViewById(R.id.current_density);
+
+        // The maximum SeekBar value always needs to be non-zero. If there's
+        // only one available zoom level, we'll handle this by disabling the
+        // seek bar.
+        final int max = Math.max(1, mValues.length - 1);
+
+        final SeekBar seekBar = (SeekBar) content.findViewById(R.id.seek_bar);
+        seekBar.setMax(max);
+        seekBar.setProgress(mInitialIndex);
+        seekBar.setOnSeekBarChangeListener(new OnSeekBarChangeListener() {
+            @Override
+            public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
+                setPreviewLayer(progress, true);
+            }
+
+            @Override
+            public void onStartTrackingTouch(SeekBar seekBar) {}
+
+            @Override
+            public void onStopTrackingTouch(SeekBar seekBar) {}
+        });
+
+        mSmaller = content.findViewById(R.id.smaller);
+        mSmaller.setOnClickListener(new OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                final int progress = seekBar.getProgress();
+                if (progress > 0) {
+                    seekBar.setProgress(progress - 1, true);
+                }
+            }
+        });
+
+        mLarger = content.findViewById(R.id.larger);
+        mLarger.setOnClickListener(new OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                final int progress = seekBar.getProgress();
+                if (progress < seekBar.getMax()) {
+                    seekBar.setProgress(progress + 1, true);
+                }
+            }
+        });
+
+        if (mValues.length == 1) {
+            // The larger and smaller buttons will be disabled when we call
+            // setPreviewLayer() later in this method.
+            seekBar.setEnabled(false);
+        }
+
+        mPreviewFrame = (FrameLayout) content.findViewById(R.id.preview_frame);
+
+        // Populate the sample layouts.
+        final Context context = getContext();
+        final Configuration origConfig = context.getResources().getConfiguration();
+        for (int mValue : mValues) {
+            final Configuration config = new Configuration(origConfig);
+            config.densityDpi = mValue;
+
+            // Create a new configuration for the specified density. It won't
+            // have any theme set, so manually apply the current theme.
+            final Context configContext = context.createConfigurationContext(config);
+            configContext.setTheme(context.getThemeResId());
+
+            final LayoutInflater configInflater = LayoutInflater.from(configContext);
+            final View sampleView = configInflater.inflate(
+                    R.layout.screen_zoom_preview, mPreviewFrame, false);
+            sampleView.setAlpha(0);
+            sampleView.setVisibility(View.INVISIBLE);
+
+            mPreviewFrame.addView(sampleView);
+        }
+
+        setPreviewLayer(mInitialIndex, false);
+
+        return root;
+    }
+
+    @Override
+    public void onDetach() {
+        super.onDetach();
+
+        // This will adjust the density SLIGHTLY after the activity has
+        // finished, which could be considered a feature or a bug...
+        commit();
+    }
+
+    private void setPreviewLayer(int index, boolean animate) {
+        mLabel.setText(mEntries[index]);
+
+        if (mCurrentIndex >= 0) {
+            final View lastLayer = mPreviewFrame.getChildAt(mCurrentIndex);
+            if (animate) {
+                lastLayer.animate()
+                        .alpha(0)
+                        .setInterpolator(FADE_OUT_INTERPOLATOR)
+                        .setDuration(CROSS_FADE_DURATION_MS)
+                        .withEndAction(new Runnable() {
+                            @Override
+                            public void run() {
+                                lastLayer.setVisibility(View.INVISIBLE);
+                            }
+                        });
+            } else {
+                lastLayer.setAlpha(0);
+                lastLayer.setVisibility(View.INVISIBLE);
+            }
+        }
+
+        final View nextLayer = mPreviewFrame.getChildAt(index);
+        if (animate) {
+            nextLayer.animate()
+                    .alpha(1)
+                    .setInterpolator(FADE_IN_INTERPOLATOR)
+                    .setDuration(CROSS_FADE_DURATION_MS)
+                    .withStartAction(new Runnable() {
+                @Override
+                public void run() {
+                    nextLayer.setVisibility(View.VISIBLE);
+                }
+            });
+        } else {
+            nextLayer.setVisibility(View.VISIBLE);
+            nextLayer.setAlpha(1);
+        }
+
+        mSmaller.setEnabled(index > 0);
+        mLarger.setEnabled(index < mEntries.length - 1);
+
+        mCurrentIndex = index;
+    }
+
+    /**
+     * Persists the selected density and sends a configuration change.
+     */
+    private void commit() {
+        final int densityDpi = mValues[mCurrentIndex];
+        if (densityDpi == mNormalDensity) {
+            DisplayDensityUtils.clearForcedDisplayDensity(Display.DEFAULT_DISPLAY);
+        } else {
+            DisplayDensityUtils.setForcedDisplayDensity(Display.DEFAULT_DISPLAY, densityDpi);
+        }
+    }
+
+    @Override
+    protected int getMetricsCategory() {
+        return InstrumentedFragment.DISPLAY_SCREEN_ZOOM;
+    }
+
+    /** Index provider used to expose this fragment in search. */
+    public static final SearchIndexProvider SEARCH_INDEX_DATA_PROVIDER =
+            new BaseSearchIndexProvider() {
+                @Override
+                public List<SearchIndexableRaw> getRawDataToIndex(Context context, boolean enabled) {
+                    final Resources res = context.getResources();
+                    final SearchIndexableRaw data = new SearchIndexableRaw(context);
+                    data.title = res.getString(R.string.screen_zoom_title);
+                    data.screenTitle = res.getString(R.string.screen_zoom_title);
+                    data.keywords = res.getString(R.string.screen_zoom_keywords);
+
+                    final List<SearchIndexableRaw> result = new ArrayList<>(1);
+                    result.add(data);
+                    return result;
+                }
+            };
+}
diff --git a/src/com/android/settings/display/TouchBlockingFrameLayout.java b/src/com/android/settings/display/TouchBlockingFrameLayout.java
new file mode 100644
index 0000000..3f5483d
--- /dev/null
+++ b/src/com/android/settings/display/TouchBlockingFrameLayout.java
@@ -0,0 +1,42 @@
+/*
+ * 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.settings.display;
+
+import android.annotation.Nullable;
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.widget.FrameLayout;
+
+/**
+ * Extension of FrameLayout that consumes all touch events.
+ */
+public class TouchBlockingFrameLayout extends FrameLayout {
+    public TouchBlockingFrameLayout(Context context, @Nullable AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    @Override
+    public boolean onInterceptTouchEvent(MotionEvent ev) {
+        return true;
+    }
+
+    @Override
+    public boolean onTouchEvent(MotionEvent event) {
+        return true;
+    }
+}
diff --git a/src/com/android/settings/search/Ranking.java b/src/com/android/settings/search/Ranking.java
index 4ac4fb8..3d5ff35 100644
--- a/src/com/android/settings/search/Ranking.java
+++ b/src/com/android/settings/search/Ranking.java
@@ -36,6 +36,7 @@
 import com.android.settings.applications.ManageDefaultApps;
 import com.android.settings.bluetooth.BluetoothSettings;
 import com.android.settings.deviceinfo.StorageSettings;
+import com.android.settings.display.ScreenZoomSettings;
 import com.android.settings.fuelgauge.BatterySaverSettings;
 import com.android.settings.fuelgauge.PowerUsageSummary;
 import com.android.settings.inputmethod.InputMethodAndLanguageSettings;
@@ -122,6 +123,7 @@
 
         // Display
         sRankMap.put(DisplaySettings.class.getName(), RANK_DISPLAY);
+        sRankMap.put(ScreenZoomSettings.class.getName(), RANK_WIFI);
 
         // Wallpapers
         sRankMap.put(WallpaperTypeSettings.class.getName(), RANK_WALLPAPER);
diff --git a/src/com/android/settings/search/SearchIndexableResources.java b/src/com/android/settings/search/SearchIndexableResources.java
index 6d6ca08..f15ff63 100644
--- a/src/com/android/settings/search/SearchIndexableResources.java
+++ b/src/com/android/settings/search/SearchIndexableResources.java
@@ -38,6 +38,7 @@
 import com.android.settings.applications.ManageDefaultApps;
 import com.android.settings.bluetooth.BluetoothSettings;
 import com.android.settings.deviceinfo.StorageSettings;
+import com.android.settings.display.ScreenZoomSettings;
 import com.android.settings.fuelgauge.BatterySaverSettings;
 import com.android.settings.fuelgauge.PowerUsageSummary;
 import com.android.settings.inputmethod.InputMethodAndLanguageSettings;
@@ -131,6 +132,13 @@
                         HomeSettings.class.getName(),
                         R.drawable.ic_settings_home));
 
+        sResMap.put(ScreenZoomSettings.class.getName(),
+                new SearchIndexableResource(
+                        Ranking.getRankForClassName(ScreenZoomSettings.class.getName()),
+                        NO_DATA_RES_ID,
+                        ScreenZoomSettings.class.getName(),
+                        R.drawable.ic_settings_display));
+
         sResMap.put(DisplaySettings.class.getName(),
                 new SearchIndexableResource(
                         Ranking.getRankForClassName(DisplaySettings.class.getName()),