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.launcher3; 18 19 import static com.android.launcher3.Utilities.dpiFromPx; 20 import static com.android.launcher3.Utilities.getPointString; 21 import static com.android.launcher3.config.FeatureFlags.ENABLE_TWO_PANEL_HOME; 22 import static com.android.launcher3.util.DisplayController.CHANGE_DENSITY; 23 import static com.android.launcher3.util.DisplayController.CHANGE_SUPPORTED_BOUNDS; 24 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR; 25 26 import android.annotation.TargetApi; 27 import android.appwidget.AppWidgetHostView; 28 import android.content.ComponentName; 29 import android.content.Context; 30 import android.content.res.Configuration; 31 import android.content.res.Resources; 32 import android.content.res.TypedArray; 33 import android.content.res.XmlResourceParser; 34 import android.graphics.Point; 35 import android.graphics.Rect; 36 import android.text.TextUtils; 37 import android.util.AttributeSet; 38 import android.util.DisplayMetrics; 39 import android.util.Log; 40 import android.util.SparseArray; 41 import android.util.TypedValue; 42 import android.util.Xml; 43 import android.view.Display; 44 45 import androidx.annotation.Nullable; 46 import androidx.annotation.VisibleForTesting; 47 48 import com.android.launcher3.testing.TestProtocol; 49 import com.android.launcher3.util.DisplayController; 50 import com.android.launcher3.util.DisplayController.Info; 51 import com.android.launcher3.util.IntArray; 52 import com.android.launcher3.util.MainThreadInitializedObject; 53 import com.android.launcher3.util.Themes; 54 import com.android.launcher3.util.WindowBounds; 55 56 import org.xmlpull.v1.XmlPullParser; 57 import org.xmlpull.v1.XmlPullParserException; 58 59 import java.io.IOException; 60 import java.util.ArrayList; 61 import java.util.Collections; 62 import java.util.List; 63 64 public class InvariantDeviceProfile { 65 66 public static final String TAG = "IDP"; 67 // We do not need any synchronization for this variable as its only written on UI thread. 68 public static final MainThreadInitializedObject<InvariantDeviceProfile> INSTANCE = 69 new MainThreadInitializedObject<>(InvariantDeviceProfile::new); 70 71 public static final String KEY_MIGRATION_SRC_WORKSPACE_SIZE = "migration_src_workspace_size"; 72 public static final String KEY_MIGRATION_SRC_HOTSEAT_COUNT = "migration_src_hotseat_count"; 73 74 private static final int DEFAULT_TRUE = -1; 75 private static final int DEFAULT_SPLIT_DISPLAY = 2; 76 77 private static final String KEY_IDP_GRID_NAME = "idp_grid_name"; 78 79 private static final float ICON_SIZE_DEFINED_IN_APP_DP = 48; 80 81 // Constants that affects the interpolation curve between statically defined device profile 82 // buckets. 83 private static final float KNEARESTNEIGHBOR = 3; 84 private static final float WEIGHT_POWER = 5; 85 86 // used to offset float not being able to express extremely small weights in extreme cases. 87 private static final float WEIGHT_EFFICIENT = 100000f; 88 89 /** 90 * Number of icons per row and column in the workspace. 91 */ 92 public int numRows; 93 public int numColumns; 94 95 /** 96 * Number of icons per row and column in the folder. 97 */ 98 public int numFolderRows; 99 public int numFolderColumns; 100 public float iconSize; 101 public float landscapeIconSize; 102 public float landscapeIconTextSize; 103 public int iconBitmapSize; 104 public int fillResIconDpi; 105 public float iconTextSize; 106 public float allAppsIconSize; 107 public float allAppsIconTextSize; 108 109 public float minCellHeight; 110 public float minCellWidth; 111 public float borderSpacing; 112 113 private SparseArray<TypedValue> mExtraAttrs; 114 115 /** 116 * Number of icons inside the hotseat area. 117 */ 118 protected int numShownHotseatIcons; 119 120 /** 121 * Number of icons inside the hotseat area that is stored in the database. This is greater than 122 * or equal to numnShownHotseatIcons, allowing for a seamless transition between two hotseat 123 * sizes that share the same DB. 124 */ 125 public int numDatabaseHotseatIcons; 126 127 /** 128 * Number of columns in the all apps list. 129 */ 130 public int numAllAppsColumns; 131 public int numDatabaseAllAppsColumns; 132 133 /** 134 * Do not query directly. see {@link DeviceProfile#isScalableGrid}. 135 */ 136 protected boolean isScalable; 137 public int devicePaddingId; 138 139 public String dbFile; 140 public int defaultLayoutId; 141 int demoModeLayoutId; 142 143 /** 144 * An immutable list of supported profiles. 145 */ 146 public List<DeviceProfile> supportedProfiles = Collections.EMPTY_LIST; 147 148 @Nullable public DevicePaddings devicePaddings; 149 150 public Point defaultWallpaperSize; 151 public Rect defaultWidgetPadding; 152 153 private final ArrayList<OnIDPChangeListener> mChangeListeners = new ArrayList<>(); 154 155 @VisibleForTesting InvariantDeviceProfile()156 public InvariantDeviceProfile() {} 157 InvariantDeviceProfile(InvariantDeviceProfile p)158 private InvariantDeviceProfile(InvariantDeviceProfile p) { 159 numRows = p.numRows; 160 numColumns = p.numColumns; 161 numFolderRows = p.numFolderRows; 162 numFolderColumns = p.numFolderColumns; 163 iconSize = p.iconSize; 164 landscapeIconSize = p.landscapeIconSize; 165 iconBitmapSize = p.iconBitmapSize; 166 iconTextSize = p.iconTextSize; 167 landscapeIconTextSize = p.landscapeIconTextSize; 168 numShownHotseatIcons = p.numShownHotseatIcons; 169 numDatabaseHotseatIcons = p.numDatabaseHotseatIcons; 170 numAllAppsColumns = p.numAllAppsColumns; 171 numDatabaseAllAppsColumns = p.numDatabaseAllAppsColumns; 172 isScalable = p.isScalable; 173 devicePaddingId = p.devicePaddingId; 174 minCellHeight = p.minCellHeight; 175 minCellWidth = p.minCellWidth; 176 borderSpacing = p.borderSpacing; 177 dbFile = p.dbFile; 178 allAppsIconSize = p.allAppsIconSize; 179 allAppsIconTextSize = p.allAppsIconTextSize; 180 defaultLayoutId = p.defaultLayoutId; 181 demoModeLayoutId = p.demoModeLayoutId; 182 mExtraAttrs = p.mExtraAttrs; 183 devicePaddings = p.devicePaddings; 184 } 185 186 @TargetApi(23) InvariantDeviceProfile(Context context)187 private InvariantDeviceProfile(Context context) { 188 String gridName = getCurrentGridName(context); 189 String newGridName = initGrid(context, gridName); 190 if (!newGridName.equals(gridName)) { 191 Utilities.getPrefs(context).edit().putString(KEY_IDP_GRID_NAME, newGridName).apply(); 192 } 193 Utilities.getPrefs(context).edit() 194 .putInt(KEY_MIGRATION_SRC_HOTSEAT_COUNT, numDatabaseHotseatIcons) 195 .putString(KEY_MIGRATION_SRC_WORKSPACE_SIZE, getPointString(numColumns, numRows)) 196 .apply(); 197 198 DisplayController.INSTANCE.get(context).addChangeListener( 199 (displayContext, info, flags) -> { 200 if ((flags & (CHANGE_DENSITY | CHANGE_SUPPORTED_BOUNDS)) != 0) { 201 onConfigChanged(displayContext); 202 } 203 }); 204 } 205 206 /** 207 * This constructor should NOT have any monitors by design. 208 */ InvariantDeviceProfile(Context context, String gridName)209 public InvariantDeviceProfile(Context context, String gridName) { 210 String newName = initGrid(context, gridName); 211 if (newName == null || !newName.equals(gridName)) { 212 throw new IllegalArgumentException("Unknown grid name"); 213 } 214 } 215 216 /** 217 * This constructor should NOT have any monitors by design. 218 */ InvariantDeviceProfile(Context context, Display display)219 public InvariantDeviceProfile(Context context, Display display) { 220 // Ensure that the main device profile is initialized 221 INSTANCE.get(context); 222 String gridName = getCurrentGridName(context); 223 224 // Get the display info based on default display and interpolate it to existing display 225 DisplayOption defaultDisplayOption = invDistWeightedInterpolate( 226 DisplayController.INSTANCE.get(context).getInfo(), 227 getPredefinedDeviceProfiles(context, gridName, false), false); 228 229 Info myInfo = new Info(context, display); 230 DisplayOption myDisplayOption = invDistWeightedInterpolate( 231 myInfo, getPredefinedDeviceProfiles(context, gridName, false), false); 232 233 DisplayOption result = new DisplayOption(defaultDisplayOption.grid) 234 .add(myDisplayOption); 235 result.iconSize = defaultDisplayOption.iconSize; 236 result.landscapeIconSize = defaultDisplayOption.landscapeIconSize; 237 if (defaultDisplayOption.allAppsIconSize < myDisplayOption.allAppsIconSize) { 238 result.allAppsIconSize = defaultDisplayOption.allAppsIconSize; 239 } else { 240 result.allAppsIconSize = myDisplayOption.allAppsIconSize; 241 } 242 result.minCellHeight = defaultDisplayOption.minCellHeight; 243 result.minCellWidth = defaultDisplayOption.minCellWidth; 244 result.borderSpacing = defaultDisplayOption.borderSpacing; 245 246 initGrid(context, myInfo, result, false); 247 } 248 getCurrentGridName(Context context)249 public static String getCurrentGridName(Context context) { 250 return Utilities.isGridOptionsEnabled(context) 251 ? Utilities.getPrefs(context).getString(KEY_IDP_GRID_NAME, null) : null; 252 } 253 initGrid(Context context, String gridName)254 private String initGrid(Context context, String gridName) { 255 Info displayInfo = DisplayController.INSTANCE.get(context).getInfo(); 256 // Determine if we have split display 257 258 boolean isTablet = false, isPhone = false; 259 for (WindowBounds bounds : displayInfo.supportedBounds) { 260 if (displayInfo.isTablet(bounds)) { 261 isTablet = true; 262 } else { 263 isPhone = true; 264 } 265 } 266 boolean isSplitDisplay = isPhone && isTablet && ENABLE_TWO_PANEL_HOME.get(); 267 268 ArrayList<DisplayOption> allOptions = 269 getPredefinedDeviceProfiles(context, gridName, isSplitDisplay); 270 DisplayOption displayOption = 271 invDistWeightedInterpolate(displayInfo, allOptions, isSplitDisplay); 272 initGrid(context, displayInfo, displayOption, isSplitDisplay); 273 return displayOption.grid.name; 274 } 275 initGrid( Context context, Info displayInfo, DisplayOption displayOption, boolean isSplitDisplay)276 private void initGrid( 277 Context context, Info displayInfo, DisplayOption displayOption, 278 boolean isSplitDisplay) { 279 DisplayMetrics metrics = context.getResources().getDisplayMetrics(); 280 GridOption closestProfile = displayOption.grid; 281 numRows = closestProfile.numRows; 282 numColumns = closestProfile.numColumns; 283 dbFile = closestProfile.dbFile; 284 defaultLayoutId = closestProfile.defaultLayoutId; 285 demoModeLayoutId = closestProfile.demoModeLayoutId; 286 numFolderRows = closestProfile.numFolderRows; 287 numFolderColumns = closestProfile.numFolderColumns; 288 isScalable = closestProfile.isScalable; 289 devicePaddingId = closestProfile.devicePaddingId; 290 291 mExtraAttrs = closestProfile.extraAttrs; 292 293 iconSize = displayOption.iconSize; 294 landscapeIconSize = displayOption.landscapeIconSize; 295 iconBitmapSize = ResourceUtils.pxFromDp(iconSize, metrics); 296 iconTextSize = displayOption.iconTextSize; 297 landscapeIconTextSize = displayOption.landscapeIconTextSize; 298 fillResIconDpi = getLauncherIconDensity(iconBitmapSize); 299 300 minCellHeight = displayOption.minCellHeight; 301 minCellWidth = displayOption.minCellWidth; 302 borderSpacing = displayOption.borderSpacing; 303 304 numShownHotseatIcons = closestProfile.numHotseatIcons; 305 numDatabaseHotseatIcons = isSplitDisplay 306 ? closestProfile.numDatabaseHotseatIcons : closestProfile.numHotseatIcons; 307 308 numAllAppsColumns = closestProfile.numAllAppsColumns; 309 numDatabaseAllAppsColumns = isSplitDisplay 310 ? closestProfile.numDatabaseAllAppsColumns : closestProfile.numAllAppsColumns; 311 312 if (Utilities.isGridOptionsEnabled(context)) { 313 allAppsIconSize = displayOption.allAppsIconSize; 314 allAppsIconTextSize = displayOption.allAppsIconTextSize; 315 } else { 316 allAppsIconSize = iconSize; 317 allAppsIconTextSize = iconTextSize; 318 } 319 320 if (devicePaddingId != 0) { 321 devicePaddings = new DevicePaddings(context, devicePaddingId); 322 } 323 324 // If the partner customization apk contains any grid overrides, apply them 325 // Supported overrides: numRows, numColumns, iconSize 326 applyPartnerDeviceProfileOverrides(context, metrics); 327 328 final List<DeviceProfile> localSupportedProfiles = new ArrayList<>(); 329 defaultWallpaperSize = new Point(displayInfo.currentSize); 330 for (WindowBounds bounds : displayInfo.supportedBounds) { 331 localSupportedProfiles.add(new DeviceProfile.Builder(context, this, displayInfo) 332 .setUseTwoPanels(isSplitDisplay) 333 .setWindowBounds(bounds).build()); 334 335 // Wallpaper size should be the maximum of the all possible sizes Launcher expects 336 int displayWidth = bounds.bounds.width(); 337 int displayHeight = bounds.bounds.height(); 338 defaultWallpaperSize.y = Math.max(defaultWallpaperSize.y, displayHeight); 339 340 // We need to ensure that there is enough extra space in the wallpaper 341 // for the intended parallax effects 342 float parallaxFactor = 343 dpiFromPx(Math.min(displayWidth, displayHeight), displayInfo.densityDpi) < 720 344 ? 2 345 : wallpaperTravelToScreenWidthRatio(displayWidth, displayHeight); 346 defaultWallpaperSize.x = 347 Math.max(defaultWallpaperSize.x, Math.round(parallaxFactor * displayWidth)); 348 } 349 supportedProfiles = Collections.unmodifiableList(localSupportedProfiles); 350 351 ComponentName cn = new ComponentName(context.getPackageName(), getClass().getName()); 352 defaultWidgetPadding = AppWidgetHostView.getDefaultPaddingForWidget(context, cn, null); 353 } 354 355 @Nullable 356 public TypedValue getAttrValue(int attr) { 357 return mExtraAttrs == null ? null : mExtraAttrs.get(attr); 358 } 359 360 public void addOnChangeListener(OnIDPChangeListener listener) { 361 mChangeListeners.add(listener); 362 } 363 364 public void removeOnChangeListener(OnIDPChangeListener listener) { 365 mChangeListeners.remove(listener); 366 } 367 368 369 public void setCurrentGrid(Context context, String gridName) { 370 Context appContext = context.getApplicationContext(); 371 Utilities.getPrefs(appContext).edit().putString(KEY_IDP_GRID_NAME, gridName).apply(); 372 MAIN_EXECUTOR.execute(() -> onConfigChanged(appContext)); 373 } 374 onConfigChanged(Context context)375 private void onConfigChanged(Context context) { 376 // Re-init grid 377 String gridName = getCurrentGridName(context); 378 initGrid(context, gridName); 379 380 for (OnIDPChangeListener listener : mChangeListeners) { 381 listener.onIdpChanged(this); 382 } 383 } 384 getPredefinedDeviceProfiles( Context context, String gridName, boolean isSplitDisplay)385 private static ArrayList<DisplayOption> getPredefinedDeviceProfiles( 386 Context context, String gridName, boolean isSplitDisplay) { 387 ArrayList<DisplayOption> profiles = new ArrayList<>(); 388 try (XmlResourceParser parser = context.getResources().getXml(R.xml.device_profiles)) { 389 final int depth = parser.getDepth(); 390 int type; 391 while (((type = parser.next()) != XmlPullParser.END_TAG || 392 parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) { 393 if ((type == XmlPullParser.START_TAG) 394 && GridOption.TAG_NAME.equals(parser.getName())) { 395 396 GridOption gridOption = new GridOption(context, Xml.asAttributeSet(parser)); 397 final int displayDepth = parser.getDepth(); 398 while (((type = parser.next()) != XmlPullParser.END_TAG || 399 parser.getDepth() > displayDepth) 400 && type != XmlPullParser.END_DOCUMENT) { 401 if ((type == XmlPullParser.START_TAG) && "display-option".equals( 402 parser.getName())) { 403 profiles.add(new DisplayOption(gridOption, context, 404 Xml.asAttributeSet(parser), 405 isSplitDisplay ? DEFAULT_SPLIT_DISPLAY : DEFAULT_TRUE)); 406 } 407 } 408 } 409 } 410 } catch (IOException|XmlPullParserException e) { 411 throw new RuntimeException(e); 412 } 413 414 ArrayList<DisplayOption> filteredProfiles = new ArrayList<>(); 415 if (!TextUtils.isEmpty(gridName)) { 416 for (DisplayOption option : profiles) { 417 if (gridName.equals(option.grid.name)) { 418 filteredProfiles.add(option); 419 } 420 } 421 } 422 if (filteredProfiles.isEmpty()) { 423 // No grid found, use the default options 424 for (DisplayOption option : profiles) { 425 if (option.canBeDefault) { 426 filteredProfiles.add(option); 427 } 428 } 429 } 430 if (filteredProfiles.isEmpty()) { 431 throw new RuntimeException("No display option with canBeDefault=true"); 432 } 433 return filteredProfiles; 434 } 435 getLauncherIconDensity(int requiredSize)436 private int getLauncherIconDensity(int requiredSize) { 437 // Densities typically defined by an app. 438 int[] densityBuckets = new int[] { 439 DisplayMetrics.DENSITY_LOW, 440 DisplayMetrics.DENSITY_MEDIUM, 441 DisplayMetrics.DENSITY_TV, 442 DisplayMetrics.DENSITY_HIGH, 443 DisplayMetrics.DENSITY_XHIGH, 444 DisplayMetrics.DENSITY_XXHIGH, 445 DisplayMetrics.DENSITY_XXXHIGH 446 }; 447 448 int density = DisplayMetrics.DENSITY_XXXHIGH; 449 for (int i = densityBuckets.length - 1; i >= 0; i--) { 450 float expectedSize = ICON_SIZE_DEFINED_IN_APP_DP * densityBuckets[i] 451 / DisplayMetrics.DENSITY_DEFAULT; 452 if (expectedSize >= requiredSize) { 453 density = densityBuckets[i]; 454 } 455 } 456 457 return density; 458 } 459 460 /** 461 * Apply any Partner customization grid overrides. 462 * 463 * Currently we support: all apps row / column count. 464 */ applyPartnerDeviceProfileOverrides(Context context, DisplayMetrics dm)465 private void applyPartnerDeviceProfileOverrides(Context context, DisplayMetrics dm) { 466 Partner p = Partner.get(context.getPackageManager()); 467 if (p != null) { 468 p.applyInvariantDeviceProfileOverrides(this, dm); 469 } 470 } 471 dist(float x0, float y0, float x1, float y1)472 private static float dist(float x0, float y0, float x1, float y1) { 473 return (float) Math.hypot(x1 - x0, y1 - y0); 474 } 475 invDistWeightedInterpolate( Info displayInfo, ArrayList<DisplayOption> points, boolean isSplitDisplay)476 private static DisplayOption invDistWeightedInterpolate( 477 Info displayInfo, ArrayList<DisplayOption> points, boolean isSplitDisplay) { 478 int minWidthPx = Integer.MAX_VALUE; 479 int minHeightPx = Integer.MAX_VALUE; 480 for (WindowBounds bounds : displayInfo.supportedBounds) { 481 boolean isTablet = displayInfo.isTablet(bounds); 482 if (isTablet && isSplitDisplay) { 483 // For split displays, take half width per page 484 minWidthPx = Math.min(minWidthPx, bounds.availableSize.x / 2); 485 minHeightPx = Math.min(minHeightPx, bounds.availableSize.y); 486 487 } else if (!isTablet && bounds.isLandscape()) { 488 // We will use transposed layout in this case 489 minWidthPx = Math.min(minWidthPx, bounds.availableSize.y); 490 minHeightPx = Math.min(minHeightPx, bounds.availableSize.x); 491 } else { 492 minWidthPx = Math.min(minWidthPx, bounds.availableSize.x); 493 minHeightPx = Math.min(minHeightPx, bounds.availableSize.y); 494 } 495 } 496 497 float width = dpiFromPx(minWidthPx, displayInfo.densityDpi); 498 float height = dpiFromPx(minHeightPx, displayInfo.densityDpi); 499 500 // Sort the profiles based on the closeness to the device size 501 Collections.sort(points, (a, b) -> 502 Float.compare(dist(width, height, a.minWidthDps, a.minHeightDps), 503 dist(width, height, b.minWidthDps, b.minHeightDps))); 504 505 GridOption closestOption = points.get(0).grid; 506 float weights = 0; 507 508 DisplayOption p = points.get(0); 509 if (dist(width, height, p.minWidthDps, p.minHeightDps) == 0) { 510 return p; 511 } 512 513 DisplayOption out = new DisplayOption(closestOption); 514 for (int i = 0; i < points.size() && i < KNEARESTNEIGHBOR; ++i) { 515 p = points.get(i); 516 float w = weight(width, height, p.minWidthDps, p.minHeightDps, WEIGHT_POWER); 517 weights += w; 518 out.add(new DisplayOption().add(p).multiply(w)); 519 } 520 return out.multiply(1.0f / weights); 521 } 522 getDeviceProfile(Context context)523 public DeviceProfile getDeviceProfile(Context context) { 524 Resources res = context.getResources(); 525 Configuration config = context.getResources().getConfiguration(); 526 527 float availableWidth = config.screenWidthDp * res.getDisplayMetrics().density; 528 float availableHeight = config.screenHeightDp * res.getDisplayMetrics().density; 529 530 DeviceProfile bestMatch = supportedProfiles.get(0); 531 float minDiff = Float.MAX_VALUE; 532 533 for (DeviceProfile profile : supportedProfiles) { 534 float diff = Math.abs(profile.availableWidthPx - availableWidth) 535 + Math.abs(profile.availableHeightPx - availableHeight); 536 if (diff < minDiff) { 537 minDiff = diff; 538 bestMatch = profile; 539 } 540 } 541 return bestMatch; 542 } 543 weight(float x0, float y0, float x1, float y1, float pow)544 private static float weight(float x0, float y0, float x1, float y1, float pow) { 545 float d = dist(x0, y0, x1, y1); 546 if (Float.compare(d, 0f) == 0) { 547 return Float.POSITIVE_INFINITY; 548 } 549 return (float) (WEIGHT_EFFICIENT / Math.pow(d, pow)); 550 } 551 552 /** 553 * As a ratio of screen height, the total distance we want the parallax effect to span 554 * horizontally 555 */ wallpaperTravelToScreenWidthRatio(int width, int height)556 private static float wallpaperTravelToScreenWidthRatio(int width, int height) { 557 float aspectRatio = width / (float) height; 558 559 // At an aspect ratio of 16/10, the wallpaper parallax effect should span 1.5 * screen width 560 // At an aspect ratio of 10/16, the wallpaper parallax effect should span 1.2 * screen width 561 // We will use these two data points to extrapolate how much the wallpaper parallax effect 562 // to span (ie travel) at any aspect ratio: 563 564 final float ASPECT_RATIO_LANDSCAPE = 16/10f; 565 final float ASPECT_RATIO_PORTRAIT = 10/16f; 566 final float WALLPAPER_WIDTH_TO_SCREEN_RATIO_LANDSCAPE = 1.5f; 567 final float WALLPAPER_WIDTH_TO_SCREEN_RATIO_PORTRAIT = 1.2f; 568 569 // To find out the desired width at different aspect ratios, we use the following two 570 // formulas, where the coefficient on x is the aspect ratio (width/height): 571 // (16/10)x + y = 1.5 572 // (10/16)x + y = 1.2 573 // We solve for x and y and end up with a final formula: 574 final float x = 575 (WALLPAPER_WIDTH_TO_SCREEN_RATIO_LANDSCAPE - WALLPAPER_WIDTH_TO_SCREEN_RATIO_PORTRAIT) / 576 (ASPECT_RATIO_LANDSCAPE - ASPECT_RATIO_PORTRAIT); 577 final float y = WALLPAPER_WIDTH_TO_SCREEN_RATIO_PORTRAIT - x * ASPECT_RATIO_PORTRAIT; 578 return x * aspectRatio + y; 579 } 580 581 public interface OnIDPChangeListener { 582 583 /** 584 * Called when the device provide changes 585 */ 586 void onIdpChanged(InvariantDeviceProfile profile); 587 } 588 589 590 public static final class GridOption { 591 592 public static final String TAG_NAME = "grid-option"; 593 594 public final String name; 595 public final int numRows; 596 public final int numColumns; 597 598 private final int numFolderRows; 599 private final int numFolderColumns; 600 601 private final int numAllAppsColumns; 602 private final int numDatabaseAllAppsColumns; 603 private final int numHotseatIcons; 604 private final int numDatabaseHotseatIcons; 605 606 private final String dbFile; 607 608 private final int defaultLayoutId; 609 private final int demoModeLayoutId; 610 611 private final boolean isScalable; 612 private final int devicePaddingId; 613 614 private final SparseArray<TypedValue> extraAttrs; 615 GridOption(Context context, AttributeSet attrs)616 public GridOption(Context context, AttributeSet attrs) { 617 TypedArray a = context.obtainStyledAttributes( 618 attrs, R.styleable.GridDisplayOption); 619 name = a.getString(R.styleable.GridDisplayOption_name); 620 numRows = a.getInt(R.styleable.GridDisplayOption_numRows, 0); 621 numColumns = a.getInt(R.styleable.GridDisplayOption_numColumns, 0); 622 623 dbFile = a.getString(R.styleable.GridDisplayOption_dbFile); 624 defaultLayoutId = a.getResourceId( 625 R.styleable.GridDisplayOption_defaultLayoutId, 0); 626 demoModeLayoutId = a.getResourceId( 627 R.styleable.GridDisplayOption_demoModeLayoutId, defaultLayoutId); 628 629 numAllAppsColumns = a.getInt( 630 R.styleable.GridDisplayOption_numAllAppsColumns, numColumns); 631 numDatabaseAllAppsColumns = a.getInt( 632 R.styleable.GridDisplayOption_numExtendedAllAppsColumns, 2 * numAllAppsColumns); 633 634 numHotseatIcons = a.getInt( 635 R.styleable.GridDisplayOption_numHotseatIcons, numColumns); 636 numDatabaseHotseatIcons = a.getInt( 637 R.styleable.GridDisplayOption_numExtendedHotseatIcons, 2 * numHotseatIcons); 638 639 numFolderRows = a.getInt( 640 R.styleable.GridDisplayOption_numFolderRows, numRows); 641 numFolderColumns = a.getInt( 642 R.styleable.GridDisplayOption_numFolderColumns, numColumns); 643 644 isScalable = a.getBoolean( 645 R.styleable.GridDisplayOption_isScalable, false); 646 devicePaddingId = a.getResourceId( 647 R.styleable.GridDisplayOption_devicePaddingId, 0); 648 649 a.recycle(); 650 extraAttrs = Themes.createValueMap(context, attrs, 651 IntArray.wrap(R.styleable.GridDisplayOption)); 652 } 653 } 654 655 @VisibleForTesting 656 static final class DisplayOption { 657 658 public final GridOption grid; 659 660 private final float minWidthDps; 661 private final float minHeightDps; 662 private final boolean canBeDefault; 663 664 private float minCellHeight; 665 private float minCellWidth; 666 private float borderSpacing; 667 668 private float iconSize; 669 private float iconTextSize; 670 private float landscapeIconSize; 671 private float landscapeIconTextSize; 672 private float allAppsIconSize; 673 private float allAppsIconTextSize; 674 DisplayOption(GridOption grid, Context context, AttributeSet attrs, int defaultFlagValue)675 DisplayOption(GridOption grid, Context context, AttributeSet attrs, int defaultFlagValue) { 676 this.grid = grid; 677 678 TypedArray a = context.obtainStyledAttributes( 679 attrs, R.styleable.ProfileDisplayOption); 680 681 minWidthDps = a.getFloat(R.styleable.ProfileDisplayOption_minWidthDps, 0); 682 minHeightDps = a.getFloat(R.styleable.ProfileDisplayOption_minHeightDps, 0); 683 684 canBeDefault = a.getInt(R.styleable.ProfileDisplayOption_canBeDefault, 0) 685 == defaultFlagValue; 686 687 minCellHeight = a.getFloat(R.styleable.ProfileDisplayOption_minCellHeightDps, 0); 688 minCellWidth = a.getFloat(R.styleable.ProfileDisplayOption_minCellWidthDps, 0); 689 borderSpacing = a.getFloat(R.styleable.ProfileDisplayOption_borderSpacingDps, 0); 690 691 iconSize = a.getFloat(R.styleable.ProfileDisplayOption_iconImageSize, 0); 692 landscapeIconSize = a.getFloat(R.styleable.ProfileDisplayOption_landscapeIconSize, 693 iconSize); 694 iconTextSize = a.getFloat(R.styleable.ProfileDisplayOption_iconTextSize, 0); 695 landscapeIconTextSize = a.getFloat( 696 R.styleable.ProfileDisplayOption_landscapeIconTextSize, iconTextSize); 697 698 allAppsIconSize = a.getFloat(R.styleable.ProfileDisplayOption_allAppsIconSize, 699 iconSize); 700 allAppsIconTextSize = a.getFloat(R.styleable.ProfileDisplayOption_allAppsIconTextSize, 701 iconTextSize); 702 a.recycle(); 703 } 704 DisplayOption()705 DisplayOption() { 706 this(null); 707 } 708 DisplayOption(GridOption grid)709 DisplayOption(GridOption grid) { 710 this.grid = grid; 711 minWidthDps = 0; 712 minHeightDps = 0; 713 canBeDefault = false; 714 minCellHeight = 0; 715 minCellWidth = 0; 716 borderSpacing = 0; 717 } 718 multiply(float w)719 private DisplayOption multiply(float w) { 720 iconSize *= w; 721 landscapeIconSize *= w; 722 allAppsIconSize *= w; 723 iconTextSize *= w; 724 landscapeIconTextSize *= w; 725 allAppsIconTextSize *= w; 726 minCellHeight *= w; 727 minCellWidth *= w; 728 borderSpacing *= w; 729 return this; 730 } 731 add(DisplayOption p)732 private DisplayOption add(DisplayOption p) { 733 iconSize += p.iconSize; 734 landscapeIconSize += p.landscapeIconSize; 735 allAppsIconSize += p.allAppsIconSize; 736 iconTextSize += p.iconTextSize; 737 landscapeIconTextSize += p.landscapeIconTextSize; 738 allAppsIconTextSize += p.allAppsIconTextSize; 739 minCellHeight += p.minCellHeight; 740 minCellWidth += p.minCellWidth; 741 borderSpacing += p.borderSpacing; 742 return this; 743 } 744 } 745 } 746