1 /* 2 * Copyright (C) 2015 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.settingslib.display; 18 19 import android.content.Context; 20 import android.content.res.Resources; 21 import android.hardware.display.DisplayManager; 22 import android.os.AsyncTask; 23 import android.os.RemoteException; 24 import android.os.UserHandle; 25 import android.util.DisplayMetrics; 26 import android.util.Log; 27 import android.util.MathUtils; 28 import android.view.Display; 29 import android.view.DisplayInfo; 30 import android.view.IWindowManager; 31 import android.view.WindowManagerGlobal; 32 import android.window.ConfigurationChangeSetting; 33 34 import androidx.annotation.NonNull; 35 import androidx.annotation.Nullable; 36 37 import com.android.settingslib.R; 38 39 import java.util.ArrayList; 40 import java.util.Arrays; 41 import java.util.List; 42 import java.util.function.Predicate; 43 44 /** 45 * Utility methods for working with display density. 46 */ 47 public class DisplayDensityUtils { 48 private static final String LOG_TAG = "DisplayDensityUtils"; 49 50 /** Summary used for "default" scale. */ 51 public static final int SUMMARY_DEFAULT = R.string.screen_zoom_summary_default; 52 53 /** Summary used for "custom" scale. */ 54 private static final int SUMMARY_CUSTOM = R.string.screen_zoom_summary_custom; 55 56 /** 57 * Summaries for scales smaller than "default" in order of smallest to 58 * largest. 59 */ 60 private static final int[] SUMMARIES_SMALLER = new int[]{ 61 R.string.screen_zoom_summary_small 62 }; 63 64 /** 65 * Summaries for scales larger than "default" in order of smallest to 66 * largest. 67 */ 68 private static final int[] SUMMARIES_LARGER = new int[]{ 69 R.string.screen_zoom_summary_large, 70 R.string.screen_zoom_summary_very_large, 71 R.string.screen_zoom_summary_extremely_large, 72 }; 73 74 /** 75 * Minimum allowed screen dimension, corresponds to resource qualifiers 76 * "small" or "sw320dp". This value must be at least the minimum screen 77 * size required by the CDD so that we meet developer expectations. 78 */ 79 private static final int MIN_DIMENSION_DP = 320; 80 81 private static final Predicate<DisplayInfo> INTERNAL_ONLY = 82 (info) -> info.type == Display.TYPE_INTERNAL; 83 84 private final Predicate<DisplayInfo> mPredicate; 85 86 private final DisplayManager mDisplayManager; 87 88 /** 89 * The text description of the density values. 90 */ 91 @Nullable 92 private final String[] mEntries; 93 94 /** 95 * The density values. 96 */ 97 @Nullable 98 private final int[] mValues; 99 100 /** 101 * The density values before rounding to an integer. 102 */ 103 @Nullable 104 private final float[] mFloatValues; 105 106 private final int mDefaultDensity; 107 private final int mCurrentIndex; 108 DisplayDensityUtils(@onNull Context context)109 public DisplayDensityUtils(@NonNull Context context) { 110 this(context, INTERNAL_ONLY); 111 } 112 113 /** 114 * Creates an instance that stores the density values for the smallest display that satisfies 115 * the predicate. It is enough to store the values for one display because the same density 116 * should be set to all the displays that satisfy the predicate. 117 * 118 * @param context The context 119 * @param predicate Determines what displays the density should be set for. The default display 120 * must satisfy this predicate. 121 */ DisplayDensityUtils(@onNull Context context, @NonNull Predicate<DisplayInfo> predicate)122 public DisplayDensityUtils(@NonNull Context context, 123 @NonNull Predicate<DisplayInfo> predicate) { 124 mPredicate = predicate; 125 mDisplayManager = context.getSystemService(DisplayManager.class); 126 127 Display defaultDisplay = mDisplayManager.getDisplay(Display.DEFAULT_DISPLAY); 128 DisplayInfo defaultDisplayInfo = new DisplayInfo(); 129 if (!defaultDisplay.getDisplayInfo(defaultDisplayInfo)) { 130 Log.w(LOG_TAG, "Cannot fetch display info for the default display"); 131 mEntries = null; 132 mValues = null; 133 mFloatValues = null; 134 mDefaultDensity = 0; 135 mCurrentIndex = -1; 136 return; 137 } 138 139 int idOfSmallestDisplay = Display.INVALID_DISPLAY; 140 int minDimensionPx = Integer.MAX_VALUE; 141 DisplayInfo smallestDisplayInfo = null; 142 for (Display display : mDisplayManager.getDisplays( 143 DisplayManager.DISPLAY_CATEGORY_ALL_INCLUDING_DISABLED)) { 144 DisplayInfo info = new DisplayInfo(); 145 if (!display.getDisplayInfo(info)) { 146 Log.w(LOG_TAG, "Cannot fetch display info for display " + display.getDisplayId()); 147 continue; 148 } 149 if (!mPredicate.test(info)) { 150 continue; 151 } 152 int minDimension = Math.min(info.logicalWidth, info.logicalHeight); 153 if (minDimension < minDimensionPx) { 154 minDimensionPx = minDimension; 155 idOfSmallestDisplay = display.getDisplayId(); 156 smallestDisplayInfo = info; 157 } 158 } 159 160 if (smallestDisplayInfo == null) { 161 Log.w(LOG_TAG, "No display satisfies the predicate"); 162 mEntries = null; 163 mValues = null; 164 mFloatValues = null; 165 mDefaultDensity = 0; 166 mCurrentIndex = -1; 167 return; 168 } 169 170 final int defaultDensity = 171 DisplayDensityUtils.getDefaultDensityForDisplay(idOfSmallestDisplay); 172 if (defaultDensity <= 0) { 173 Log.w(LOG_TAG, "Cannot fetch default density for display " + idOfSmallestDisplay); 174 mEntries = null; 175 mValues = null; 176 mFloatValues = null; 177 mDefaultDensity = 0; 178 mCurrentIndex = -1; 179 return; 180 } 181 182 final Resources res = context.getResources(); 183 184 int currentDensity; 185 if (mPredicate.test(defaultDisplayInfo)) { 186 currentDensity = defaultDisplayInfo.logicalDensityDpi; 187 } else { 188 currentDensity = smallestDisplayInfo.logicalDensityDpi; 189 } 190 int currentDensityIndex = -1; 191 192 // Compute number of "larger" and "smaller" scales for this display. 193 final int maxDensity = 194 DisplayMetrics.DENSITY_MEDIUM * minDimensionPx / MIN_DIMENSION_DP; 195 final float maxScaleDimen = context.getResources().getFraction( 196 R.fraction.display_density_max_scale, 1, 1); 197 final float maxScale = Math.min(maxScaleDimen, maxDensity / (float) defaultDensity); 198 final float minScale = context.getResources().getFraction( 199 R.fraction.display_density_min_scale, 1, 1); 200 final float minScaleInterval = context.getResources().getFraction( 201 R.fraction.display_density_min_scale_interval, 1, 1); 202 final int numLarger = (int) MathUtils.constrain((maxScale - 1) / minScaleInterval, 203 0, SUMMARIES_LARGER.length); 204 final int numSmaller = (int) MathUtils.constrain((1 - minScale) / minScaleInterval, 205 0, SUMMARIES_SMALLER.length); 206 207 String[] entries = new String[1 + numSmaller + numLarger]; 208 int[] values = new int[entries.length]; 209 float[] valuesFloat = new float[entries.length]; 210 int curIndex = 0; 211 212 if (numSmaller > 0) { 213 final float interval = (1 - minScale) / numSmaller; 214 for (int i = numSmaller - 1; i >= 0; i--) { 215 // Save the float density value before rounding to be used to set the density ratio 216 // of overridden density to default density in WM. 217 final float densityFloat = defaultDensity * (1 - (i + 1) * interval); 218 // Round down to a multiple of 2 by truncating the low bit. 219 // LINT.IfChange 220 final int density = ((int) densityFloat) & ~1; 221 // LINT.ThenChange(/services/core/java/com/android/server/wm/DisplayContent.java:getBaseDensityFromRatio) 222 if (currentDensity == density) { 223 currentDensityIndex = curIndex; 224 } 225 values[curIndex] = density; 226 valuesFloat[curIndex] = densityFloat; 227 entries[curIndex] = res.getString(SUMMARIES_SMALLER[i]); 228 curIndex++; 229 } 230 } 231 232 if (currentDensity == defaultDensity) { 233 currentDensityIndex = curIndex; 234 } 235 values[curIndex] = defaultDensity; 236 valuesFloat[curIndex] = (float) defaultDensity; 237 entries[curIndex] = res.getString(SUMMARY_DEFAULT); 238 curIndex++; 239 240 if (numLarger > 0) { 241 final float interval = (maxScale - 1) / numLarger; 242 for (int i = 0; i < numLarger; i++) { 243 // Save the float density value before rounding to be used to set the density ratio 244 // of overridden density to default density in WM. 245 final float densityFloat = defaultDensity * (1 + (i + 1) * interval); 246 // Round down to a multiple of 2 by truncating the low bit. 247 // LINT.IfChange 248 final int density = ((int) densityFloat) & ~1; 249 // LINT.ThenChange(/services/core/java/com/android/server/wm/DisplayContent.java:getBaseDensityFromRatio) 250 if (currentDensity == density) { 251 currentDensityIndex = curIndex; 252 } 253 values[curIndex] = density; 254 valuesFloat[curIndex] = densityFloat; 255 entries[curIndex] = res.getString(SUMMARIES_LARGER[i]); 256 curIndex++; 257 } 258 } 259 260 final int displayIndex; 261 if (currentDensityIndex >= 0) { 262 displayIndex = currentDensityIndex; 263 } else { 264 // We don't understand the current density. Must have been set by 265 // someone else. Make room for another entry... 266 int newLength = values.length + 1; 267 values = Arrays.copyOf(values, newLength); 268 values[curIndex] = currentDensity; 269 270 valuesFloat = Arrays.copyOf(valuesFloat, newLength); 271 valuesFloat[curIndex] = (float) currentDensity; 272 273 entries = Arrays.copyOf(entries, newLength); 274 entries[curIndex] = res.getString(SUMMARY_CUSTOM, currentDensity); 275 276 displayIndex = curIndex; 277 } 278 279 mDefaultDensity = defaultDensity; 280 mCurrentIndex = displayIndex; 281 mEntries = entries; 282 mValues = values; 283 mFloatValues = valuesFloat; 284 } 285 286 @Nullable getEntries()287 public String[] getEntries() { 288 return mEntries; 289 } 290 291 @Nullable getValues()292 public int[] getValues() { 293 return mValues; 294 } 295 getCurrentIndex()296 public int getCurrentIndex() { 297 return mCurrentIndex; 298 } 299 getDefaultDensity()300 public int getDefaultDensity() { 301 return mDefaultDensity; 302 } 303 304 /** 305 * Returns the default density for the specified display. 306 * 307 * @param displayId the identifier of the display 308 * @return the default density of the specified display, or {@code -1} if the display does not 309 * exist or the density could not be obtained 310 */ getDefaultDensityForDisplay(int displayId)311 private static int getDefaultDensityForDisplay(int displayId) { 312 try { 313 final IWindowManager wm = WindowManagerGlobal.getWindowManagerService(); 314 return wm.getInitialDisplayDensity(displayId); 315 } catch (RemoteException exc) { 316 return -1; 317 } 318 } 319 320 /** 321 * Asynchronously applies display density changes to the displays that satisfy the predicate. 322 * <p> 323 * The change will be applied to the user specified by the value of 324 * {@link UserHandle#myUserId()} at the time the method is called. 325 */ clearForcedDisplayDensity()326 public void clearForcedDisplayDensity() { 327 final int userId = UserHandle.myUserId(); 328 AsyncTask.execute(() -> { 329 try { 330 for (Display display : mDisplayManager.getDisplays( 331 DisplayManager.DISPLAY_CATEGORY_ALL_INCLUDING_DISABLED)) { 332 int displayId = display.getDisplayId(); 333 DisplayInfo info = new DisplayInfo(); 334 if (!display.getDisplayInfo(info)) { 335 Log.w(LOG_TAG, "Unable to clear forced display density setting " 336 + "for display " + displayId); 337 continue; 338 } 339 if (!mPredicate.test(info)) { 340 continue; 341 } 342 343 final IWindowManager wm = WindowManagerGlobal.getWindowManagerService(); 344 wm.clearForcedDisplayDensityForUser(displayId, userId); 345 } 346 } catch (RemoteException exc) { 347 Log.w(LOG_TAG, "Unable to clear forced display density setting"); 348 } 349 }); 350 } 351 352 /** 353 * Asynchronously applies display density changes to the displays that satisfy the predicate. 354 * <p> 355 * The change will be applied to the user specified by the value of 356 * {@link UserHandle#myUserId()} at the time the method is called. 357 * 358 * @param index The index of the density value 359 */ setForcedDisplayDensity(final int index)360 public void setForcedDisplayDensity(final int index) { 361 final int userId = UserHandle.myUserId(); 362 AsyncTask.execute(() -> { 363 try { 364 for (Display display : mDisplayManager.getDisplays( 365 DisplayManager.DISPLAY_CATEGORY_ALL_INCLUDING_DISABLED)) { 366 int displayId = display.getDisplayId(); 367 DisplayInfo info = new DisplayInfo(); 368 if (!display.getDisplayInfo(info)) { 369 Log.w(LOG_TAG, "Unable to save forced display density setting " 370 + "for display " + displayId); 371 continue; 372 } 373 if (!mPredicate.test(info)) { 374 continue; 375 } 376 377 final IWindowManager wm = WindowManagerGlobal.getWindowManagerService(); 378 // Only set the ratio for external displays as Settings uses 379 // ScreenResolutionFragment to handle density update for internal display. 380 if (info.type == Display.TYPE_EXTERNAL) { 381 wm.setForcedDisplayDensityRatio(displayId, 382 mFloatValues[index] / mDefaultDensity, userId); 383 } else { 384 wm.setForcedDisplayDensityForUser(displayId, mValues[index], userId); 385 } 386 } 387 } catch (RemoteException exc) { 388 Log.w(LOG_TAG, "Unable to save forced display density setting"); 389 } 390 }); 391 } 392 393 /** 394 * Returns a list of {@link ConfigurationChangeSetting} object representing the forced display 395 * density settings for the displays that satisfy the predicate. 396 * 397 * @param index the index of the density value in the available density values array. 398 * @return a list of {@link ConfigurationChangeSetting} objects. 399 * @see IWindowManager#setConfigurationChangeSettingsForUser 400 */ 401 @NonNull getForcedDisplayDensitySetting(final int index)402 public List<ConfigurationChangeSetting> getForcedDisplayDensitySetting(final int index) { 403 final ArrayList<ConfigurationChangeSetting> settings = new ArrayList<>(); 404 for (final Display display : mDisplayManager.getDisplays( 405 DisplayManager.DISPLAY_CATEGORY_ALL_INCLUDING_DISABLED)) { 406 final int displayId = display.getDisplayId(); 407 final DisplayInfo info = new DisplayInfo(); 408 if (!display.getDisplayInfo(info)) { 409 Log.w(LOG_TAG, "Unable to get display info for display " + displayId); 410 continue; 411 } 412 if (!mPredicate.test(info)) { 413 continue; 414 } 415 settings.add(new ConfigurationChangeSetting.DensitySetting(displayId, mValues[index])); 416 } 417 return settings; 418 } 419 } 420