diff --git a/res/values/strings.xml b/res/values/strings.xml index 2dcbe566f89..aef3f782a9d 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -6978,4 +6978,22 @@ Use sRGB + + + Display scale + + display density zoom scale scaling + + Small + + Normal + + Large + + Larger + + Largest + + Custom (%d) diff --git a/res/xml/display_settings.xml b/res/xml/display_settings.xml index ad23bc249bf..60730a9397d 100644 --- a/res/xml/display_settings.xml +++ b/res/xml/display_settings.xml @@ -99,6 +99,13 @@ android:entries="@array/entries_font_size" android:entryValues="@array/entryvalues_font_size" /> + + diff --git a/src/com/android/settings/DisplayDensityPreference.java b/src/com/android/settings/DisplayDensityPreference.java new file mode 100644 index 00000000000..85f2fb25d33 --- /dev/null +++ b/src/com/android/settings/DisplayDensityPreference.java @@ -0,0 +1,247 @@ +/* + * 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 8e323227c10..e26200a8263 100644 --- a/src/com/android/settings/DisplaySettings.java +++ b/src/com/android/settings/DisplaySettings.java @@ -76,6 +76,7 @@ public class DisplaySettings extends SettingsPreferenceFragment implements 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; @@ -213,6 +214,9 @@ public class DisplaySettings extends SettingsPreferenceFragment implements mNightModePreference.setValue(String.valueOf(currentNightMode)); mNightModePreference.setOnPreferenceChangeListener(this); } + + final Preference displayDensity = findPreference(KEY_DISPLAY_DENSITY); + displayDensity.setOnPreferenceChangeListener(this); } private static boolean allowAllRotations(Context context) {