1 /* 2 * Copyright (C) 2021 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.config.FeatureFlags.ENABLE_TWO_PANEL_HOME; 21 import static com.android.launcher3.util.DisplayController.CHANGE_DENSITY; 22 import static com.android.launcher3.util.DisplayController.CHANGE_SUPPORTED_BOUNDS; 23 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR; 24 25 import android.annotation.TargetApi; 26 import android.appwidget.AppWidgetHostView; 27 import android.content.ComponentName; 28 import android.content.Context; 29 import android.content.res.Configuration; 30 import android.content.res.Resources; 31 import android.content.res.TypedArray; 32 import android.content.res.XmlResourceParser; 33 import android.graphics.Point; 34 import android.graphics.PointF; 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.IntDef; 46 import androidx.annotation.Nullable; 47 import androidx.annotation.VisibleForTesting; 48 49 import com.android.launcher3.model.DeviceGridState; 50 import com.android.launcher3.provider.RestoreDbTask; 51 import com.android.launcher3.util.DisplayController; 52 import com.android.launcher3.util.DisplayController.Info; 53 import com.android.launcher3.util.IntArray; 54 import com.android.launcher3.util.MainThreadInitializedObject; 55 import com.android.launcher3.util.Themes; 56 import com.android.launcher3.util.WindowBounds; 57 58 import org.xmlpull.v1.XmlPullParser; 59 import org.xmlpull.v1.XmlPullParserException; 60 61 import java.io.IOException; 62 import java.lang.annotation.Retention; 63 import java.lang.annotation.RetentionPolicy; 64 import java.util.ArrayList; 65 import java.util.Arrays; 66 import java.util.Collections; 67 import java.util.List; 68 69 public class InvariantDeviceProfile { 70 71 public static final String TAG = "IDP"; 72 // We do not need any synchronization for this variable as its only written on UI thread. 73 public static final MainThreadInitializedObject<InvariantDeviceProfile> INSTANCE = 74 new MainThreadInitializedObject<>(InvariantDeviceProfile::new); 75 76 @Retention(RetentionPolicy.SOURCE) 77 @IntDef({TYPE_PHONE, TYPE_MULTI_DISPLAY, TYPE_TABLET}) 78 public @interface DeviceType{} 79 public static final int TYPE_PHONE = 0; 80 public static final int TYPE_MULTI_DISPLAY = 1; 81 public static final int TYPE_TABLET = 2; 82 83 private static final String KEY_IDP_GRID_NAME = "idp_grid_name"; 84 85 private static final float ICON_SIZE_DEFINED_IN_APP_DP = 48; 86 87 // Constants that affects the interpolation curve between statically defined device profile 88 // buckets. 89 private static final float KNEARESTNEIGHBOR = 3; 90 private static final float WEIGHT_POWER = 5; 91 92 // used to offset float not being able to express extremely small weights in extreme cases. 93 private static final float WEIGHT_EFFICIENT = 100000f; 94 95 // Used for arrays to specify different sizes (e.g. border spaces, width/height) in different 96 // constraints 97 static final int COUNT_SIZES = 5; 98 static final int INDEX_DEFAULT = 0; 99 static final int INDEX_LANDSCAPE = 1; 100 static final int INDEX_TWO_PANEL_PORTRAIT = 2; 101 static final int INDEX_TWO_PANEL_LANDSCAPE = 3; 102 static final int INDEX_ALL_APPS = 4; 103 104 /** 105 * Number of icons per row and column in the workspace. 106 */ 107 public int numRows; 108 public int numColumns; 109 110 /** 111 * Number of icons per row and column in the folder. 112 */ 113 public int numFolderRows; 114 public int numFolderColumns; 115 public float[] iconSize; 116 public float[] iconTextSize; 117 public int iconBitmapSize; 118 public int fillResIconDpi; 119 public @DeviceType int deviceType; 120 121 public PointF[] minCellSize; 122 123 public PointF[] borderSpaces; 124 public float folderBorderSpace; 125 126 public float[] horizontalMargin; 127 128 private SparseArray<TypedValue> mExtraAttrs; 129 130 /** 131 * Number of icons inside the hotseat area. 132 */ 133 protected int numShownHotseatIcons; 134 135 /** 136 * Number of icons inside the hotseat area that is stored in the database. This is greater than 137 * or equal to numnShownHotseatIcons, allowing for a seamless transition between two hotseat 138 * sizes that share the same DB. 139 */ 140 public int numDatabaseHotseatIcons; 141 142 /** 143 * Number of columns in the all apps list. 144 */ 145 public int numAllAppsColumns; 146 public int numDatabaseAllAppsColumns; 147 148 /** 149 * Do not query directly. see {@link DeviceProfile#isScalableGrid}. 150 */ 151 protected boolean isScalable; 152 public int devicePaddingId; 153 154 public String dbFile; 155 public int defaultLayoutId; 156 int demoModeLayoutId; 157 158 /** 159 * An immutable list of supported profiles. 160 */ 161 public List<DeviceProfile> supportedProfiles = Collections.EMPTY_LIST; 162 163 @Nullable 164 public DevicePaddings devicePaddings; 165 166 public Point defaultWallpaperSize; 167 public Rect defaultWidgetPadding; 168 169 private final ArrayList<OnIDPChangeListener> mChangeListeners = new ArrayList<>(); 170 171 @VisibleForTesting InvariantDeviceProfile()172 public InvariantDeviceProfile() { 173 } 174 175 @TargetApi(23) InvariantDeviceProfile(Context context)176 private InvariantDeviceProfile(Context context) { 177 String gridName = getCurrentGridName(context); 178 String newGridName = initGrid(context, gridName); 179 if (!newGridName.equals(gridName)) { 180 Utilities.getPrefs(context).edit().putString(KEY_IDP_GRID_NAME, newGridName).apply(); 181 } 182 new DeviceGridState(this).writeToPrefs(context); 183 184 DisplayController.INSTANCE.get(context).setPriorityListener( 185 (displayContext, info, flags) -> { 186 if ((flags & (CHANGE_DENSITY | CHANGE_SUPPORTED_BOUNDS)) != 0) { 187 onConfigChanged(displayContext); 188 } 189 }); 190 } 191 192 /** 193 * This constructor should NOT have any monitors by design. 194 */ InvariantDeviceProfile(Context context, String gridName)195 public InvariantDeviceProfile(Context context, String gridName) { 196 String newName = initGrid(context, gridName); 197 if (newName == null || !newName.equals(gridName)) { 198 throw new IllegalArgumentException("Unknown grid name"); 199 } 200 } 201 202 /** 203 * This constructor should NOT have any monitors by design. 204 */ InvariantDeviceProfile(Context context, Display display)205 public InvariantDeviceProfile(Context context, Display display) { 206 // Ensure that the main device profile is initialized 207 INSTANCE.get(context); 208 String gridName = getCurrentGridName(context); 209 210 // Get the display info based on default display and interpolate it to existing display 211 Info defaultInfo = DisplayController.INSTANCE.get(context).getInfo(); 212 @DeviceType int defaultDeviceType = getDeviceType(defaultInfo); 213 DisplayOption defaultDisplayOption = invDistWeightedInterpolate( 214 defaultInfo, 215 getPredefinedDeviceProfiles(context, gridName, defaultDeviceType, 216 /*allowDisabledGrid=*/false), 217 defaultDeviceType); 218 219 Info myInfo = new Info(context, display); 220 @DeviceType int deviceType = getDeviceType(myInfo); 221 DisplayOption myDisplayOption = invDistWeightedInterpolate( 222 myInfo, 223 getPredefinedDeviceProfiles(context, gridName, deviceType, 224 /*allowDisabledGrid=*/false), 225 deviceType); 226 227 DisplayOption result = new DisplayOption(defaultDisplayOption.grid) 228 .add(myDisplayOption); 229 result.iconSizes[INDEX_DEFAULT] = 230 defaultDisplayOption.iconSizes[INDEX_DEFAULT]; 231 for (int i = 1; i < COUNT_SIZES; i++) { 232 result.iconSizes[i] = Math.min( 233 defaultDisplayOption.iconSizes[i], myDisplayOption.iconSizes[i]); 234 } 235 236 System.arraycopy(defaultDisplayOption.minCellSize, 0, result.minCellSize, 0, 237 COUNT_SIZES); 238 System.arraycopy(defaultDisplayOption.borderSpaces, 0, result.borderSpaces, 0, 239 COUNT_SIZES); 240 241 initGrid(context, myInfo, result, deviceType); 242 } 243 244 /** 245 * Reinitialize the current grid after a restore, where some grids might now be disabled. 246 */ reinitializeAfterRestore(Context context)247 public void reinitializeAfterRestore(Context context) { 248 String currentDbFile = dbFile; 249 String gridName = getCurrentGridName(context); 250 String newGridName = initGrid(context, gridName); 251 if (!newGridName.equals(gridName)) { 252 Log.d(TAG, "Restored grid is disabled : " + gridName 253 + ", migrating to: " + newGridName 254 + ", removing all other grid db files"); 255 for (String gridDbFile : LauncherFiles.GRID_DB_FILES) { 256 if (gridDbFile.equals(currentDbFile)) { 257 continue; 258 } 259 if (context.getDatabasePath(gridDbFile).delete()) { 260 Log.d(TAG, "Removed old grid db file: " + gridDbFile); 261 } 262 } 263 setCurrentGrid(context, gridName); 264 } 265 } 266 getDeviceType(Info displayInfo)267 private static @DeviceType int getDeviceType(Info displayInfo) { 268 // Each screen has two profiles (portrait/landscape), so devices with four or more 269 // supported profiles implies two or more internal displays. 270 if (displayInfo.supportedBounds.size() >= 4 && ENABLE_TWO_PANEL_HOME.get()) { 271 return TYPE_MULTI_DISPLAY; 272 } else if (displayInfo.supportedBounds.stream().allMatch(displayInfo::isTablet)) { 273 return TYPE_TABLET; 274 } else { 275 return TYPE_PHONE; 276 } 277 } 278 getCurrentGridName(Context context)279 public static String getCurrentGridName(Context context) { 280 return Utilities.isGridOptionsEnabled(context) 281 ? Utilities.getPrefs(context).getString(KEY_IDP_GRID_NAME, null) : null; 282 } 283 initGrid(Context context, String gridName)284 private String initGrid(Context context, String gridName) { 285 Info displayInfo = DisplayController.INSTANCE.get(context).getInfo(); 286 @DeviceType int deviceType = getDeviceType(displayInfo); 287 288 ArrayList<DisplayOption> allOptions = 289 getPredefinedDeviceProfiles(context, gridName, deviceType, 290 RestoreDbTask.isPending(context)); 291 DisplayOption displayOption = 292 invDistWeightedInterpolate(displayInfo, allOptions, deviceType); 293 initGrid(context, displayInfo, displayOption, deviceType); 294 return displayOption.grid.name; 295 } 296 initGrid(Context context, Info displayInfo, DisplayOption displayOption, @DeviceType int deviceType)297 private void initGrid(Context context, Info displayInfo, DisplayOption displayOption, 298 @DeviceType int deviceType) { 299 DisplayMetrics metrics = context.getResources().getDisplayMetrics(); 300 GridOption closestProfile = displayOption.grid; 301 numRows = closestProfile.numRows; 302 numColumns = closestProfile.numColumns; 303 dbFile = closestProfile.dbFile; 304 defaultLayoutId = closestProfile.defaultLayoutId; 305 demoModeLayoutId = closestProfile.demoModeLayoutId; 306 numFolderRows = closestProfile.numFolderRows; 307 numFolderColumns = closestProfile.numFolderColumns; 308 isScalable = closestProfile.isScalable; 309 devicePaddingId = closestProfile.devicePaddingId; 310 this.deviceType = deviceType; 311 312 mExtraAttrs = closestProfile.extraAttrs; 313 314 iconSize = displayOption.iconSizes; 315 float maxIconSize = iconSize[0]; 316 for (int i = 1; i < iconSize.length; i++) { 317 maxIconSize = Math.max(maxIconSize, iconSize[i]); 318 } 319 iconBitmapSize = ResourceUtils.pxFromDp(maxIconSize, metrics); 320 fillResIconDpi = getLauncherIconDensity(iconBitmapSize); 321 322 iconTextSize = displayOption.textSizes; 323 324 minCellSize = displayOption.minCellSize; 325 326 borderSpaces = displayOption.borderSpaces; 327 folderBorderSpace = displayOption.folderBorderSpace; 328 329 horizontalMargin = displayOption.horizontalMargin; 330 331 numShownHotseatIcons = closestProfile.numHotseatIcons; 332 numDatabaseHotseatIcons = deviceType == TYPE_MULTI_DISPLAY 333 ? closestProfile.numDatabaseHotseatIcons : closestProfile.numHotseatIcons; 334 335 numAllAppsColumns = closestProfile.numAllAppsColumns; 336 numDatabaseAllAppsColumns = deviceType == TYPE_MULTI_DISPLAY 337 ? closestProfile.numDatabaseAllAppsColumns : closestProfile.numAllAppsColumns; 338 339 if (!Utilities.isGridOptionsEnabled(context)) { 340 iconSize[INDEX_ALL_APPS] = iconSize[INDEX_DEFAULT]; 341 iconTextSize[INDEX_ALL_APPS] = iconTextSize[INDEX_DEFAULT]; 342 } 343 344 if (devicePaddingId != 0) { 345 devicePaddings = new DevicePaddings(context, devicePaddingId); 346 } 347 348 // If the partner customization apk contains any grid overrides, apply them 349 // Supported overrides: numRows, numColumns, iconSize 350 applyPartnerDeviceProfileOverrides(context, metrics); 351 352 final List<DeviceProfile> localSupportedProfiles = new ArrayList<>(); 353 defaultWallpaperSize = new Point(displayInfo.currentSize); 354 for (WindowBounds bounds : displayInfo.supportedBounds) { 355 localSupportedProfiles.add(new DeviceProfile.Builder(context, this, displayInfo) 356 .setUseTwoPanels(deviceType == TYPE_MULTI_DISPLAY) 357 .setWindowBounds(bounds).build()); 358 359 // Wallpaper size should be the maximum of the all possible sizes Launcher expects 360 int displayWidth = bounds.bounds.width(); 361 int displayHeight = bounds.bounds.height(); 362 defaultWallpaperSize.y = Math.max(defaultWallpaperSize.y, displayHeight); 363 364 // We need to ensure that there is enough extra space in the wallpaper 365 // for the intended parallax effects 366 float parallaxFactor = 367 dpiFromPx(Math.min(displayWidth, displayHeight), displayInfo.densityDpi) < 720 368 ? 2 369 : wallpaperTravelToScreenWidthRatio(displayWidth, displayHeight); 370 defaultWallpaperSize.x = 371 Math.max(defaultWallpaperSize.x, Math.round(parallaxFactor * displayWidth)); 372 } 373 supportedProfiles = Collections.unmodifiableList(localSupportedProfiles); 374 375 ComponentName cn = new ComponentName(context.getPackageName(), getClass().getName()); 376 defaultWidgetPadding = AppWidgetHostView.getDefaultPaddingForWidget(context, cn, null); 377 } 378 379 public void addOnChangeListener(OnIDPChangeListener listener) { 380 mChangeListeners.add(listener); 381 } 382 383 public void removeOnChangeListener(OnIDPChangeListener listener) { 384 mChangeListeners.remove(listener); 385 } 386 387 388 public void setCurrentGrid(Context context, String gridName) { 389 Context appContext = context.getApplicationContext(); 390 Utilities.getPrefs(appContext).edit().putString(KEY_IDP_GRID_NAME, gridName).apply(); 391 MAIN_EXECUTOR.execute(() -> onConfigChanged(appContext)); 392 } 393 toModelState()394 private Object[] toModelState() { 395 return new Object[]{ 396 numColumns, numRows, numDatabaseHotseatIcons, iconBitmapSize, fillResIconDpi, 397 numDatabaseAllAppsColumns, dbFile}; 398 } 399 onConfigChanged(Context context)400 private void onConfigChanged(Context context) { 401 Object[] oldState = toModelState(); 402 403 // Re-init grid 404 String gridName = getCurrentGridName(context); 405 initGrid(context, gridName); 406 407 boolean modelPropsChanged = !Arrays.equals(oldState, toModelState()); 408 for (OnIDPChangeListener listener : mChangeListeners) { 409 listener.onIdpChanged(modelPropsChanged); 410 } 411 } 412 getPredefinedDeviceProfiles(Context context, String gridName, @DeviceType int deviceType, boolean allowDisabledGrid)413 private static ArrayList<DisplayOption> getPredefinedDeviceProfiles(Context context, 414 String gridName, @DeviceType int deviceType, boolean allowDisabledGrid) { 415 ArrayList<DisplayOption> profiles = new ArrayList<>(); 416 417 try (XmlResourceParser parser = context.getResources().getXml(R.xml.device_profiles)) { 418 final int depth = parser.getDepth(); 419 int type; 420 while (((type = parser.next()) != XmlPullParser.END_TAG || 421 parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) { 422 if ((type == XmlPullParser.START_TAG) 423 && GridOption.TAG_NAME.equals(parser.getName())) { 424 425 GridOption gridOption = new GridOption(context, Xml.asAttributeSet(parser), 426 deviceType); 427 if (gridOption.isEnabled || allowDisabledGrid) { 428 final int displayDepth = parser.getDepth(); 429 while (((type = parser.next()) != XmlPullParser.END_TAG 430 || parser.getDepth() > displayDepth) 431 && type != XmlPullParser.END_DOCUMENT) { 432 if ((type == XmlPullParser.START_TAG) && "display-option".equals( 433 parser.getName())) { 434 profiles.add(new DisplayOption(gridOption, context, 435 Xml.asAttributeSet(parser))); 436 } 437 } 438 } 439 } 440 } 441 } catch (IOException | XmlPullParserException e) { 442 throw new RuntimeException(e); 443 } 444 445 ArrayList<DisplayOption> filteredProfiles = new ArrayList<>(); 446 if (!TextUtils.isEmpty(gridName)) { 447 for (DisplayOption option : profiles) { 448 if (gridName.equals(option.grid.name) 449 && (option.grid.isEnabled || allowDisabledGrid)) { 450 filteredProfiles.add(option); 451 } 452 } 453 } 454 if (filteredProfiles.isEmpty()) { 455 // No grid found, use the default options 456 for (DisplayOption option : profiles) { 457 if (option.canBeDefault) { 458 filteredProfiles.add(option); 459 } 460 } 461 } 462 if (filteredProfiles.isEmpty()) { 463 throw new RuntimeException("No display option with canBeDefault=true"); 464 } 465 return filteredProfiles; 466 } 467 468 /** 469 * @return all the grid options that can be shown on the device 470 */ parseAllGridOptions(Context context)471 public List<GridOption> parseAllGridOptions(Context context) { 472 List<GridOption> result = new ArrayList<>(); 473 474 try (XmlResourceParser parser = context.getResources().getXml(R.xml.device_profiles)) { 475 final int depth = parser.getDepth(); 476 int type; 477 while (((type = parser.next()) != XmlPullParser.END_TAG 478 || parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) { 479 if ((type == XmlPullParser.START_TAG) 480 && GridOption.TAG_NAME.equals(parser.getName())) { 481 GridOption option = 482 new GridOption(context, Xml.asAttributeSet(parser), deviceType); 483 if (option.isEnabled) { 484 result.add(option); 485 } 486 } 487 } 488 } catch (IOException | XmlPullParserException e) { 489 Log.e(TAG, "Error parsing device profile", e); 490 return Collections.emptyList(); 491 } 492 return result; 493 } 494 getLauncherIconDensity(int requiredSize)495 private int getLauncherIconDensity(int requiredSize) { 496 // Densities typically defined by an app. 497 int[] densityBuckets = new int[]{ 498 DisplayMetrics.DENSITY_LOW, 499 DisplayMetrics.DENSITY_MEDIUM, 500 DisplayMetrics.DENSITY_TV, 501 DisplayMetrics.DENSITY_HIGH, 502 DisplayMetrics.DENSITY_XHIGH, 503 DisplayMetrics.DENSITY_XXHIGH, 504 DisplayMetrics.DENSITY_XXXHIGH 505 }; 506 507 int density = DisplayMetrics.DENSITY_XXXHIGH; 508 for (int i = densityBuckets.length - 1; i >= 0; i--) { 509 float expectedSize = ICON_SIZE_DEFINED_IN_APP_DP * densityBuckets[i] 510 / DisplayMetrics.DENSITY_DEFAULT; 511 if (expectedSize >= requiredSize) { 512 density = densityBuckets[i]; 513 } 514 } 515 516 return density; 517 } 518 519 /** 520 * Apply any Partner customization grid overrides. 521 * 522 * Currently we support: all apps row / column count. 523 */ applyPartnerDeviceProfileOverrides(Context context, DisplayMetrics dm)524 private void applyPartnerDeviceProfileOverrides(Context context, DisplayMetrics dm) { 525 Partner p = Partner.get(context.getPackageManager()); 526 if (p != null) { 527 p.applyInvariantDeviceProfileOverrides(this, dm); 528 } 529 } 530 dist(float x0, float y0, float x1, float y1)531 private static float dist(float x0, float y0, float x1, float y1) { 532 return (float) Math.hypot(x1 - x0, y1 - y0); 533 } 534 invDistWeightedInterpolate( Info displayInfo, ArrayList<DisplayOption> points, @DeviceType int deviceType)535 private static DisplayOption invDistWeightedInterpolate( 536 Info displayInfo, ArrayList<DisplayOption> points, @DeviceType int deviceType) { 537 int minWidthPx = Integer.MAX_VALUE; 538 int minHeightPx = Integer.MAX_VALUE; 539 for (WindowBounds bounds : displayInfo.supportedBounds) { 540 boolean isTablet = displayInfo.isTablet(bounds); 541 if (isTablet && deviceType == TYPE_MULTI_DISPLAY) { 542 // For split displays, take half width per page 543 minWidthPx = Math.min(minWidthPx, bounds.availableSize.x / 2); 544 minHeightPx = Math.min(minHeightPx, bounds.availableSize.y); 545 546 } else if (!isTablet && bounds.isLandscape()) { 547 // We will use transposed layout in this case 548 minWidthPx = Math.min(minWidthPx, bounds.availableSize.y); 549 minHeightPx = Math.min(minHeightPx, bounds.availableSize.x); 550 } else { 551 minWidthPx = Math.min(minWidthPx, bounds.availableSize.x); 552 minHeightPx = Math.min(minHeightPx, bounds.availableSize.y); 553 } 554 } 555 556 float width = dpiFromPx(minWidthPx, displayInfo.densityDpi); 557 float height = dpiFromPx(minHeightPx, displayInfo.densityDpi); 558 559 // Sort the profiles based on the closeness to the device size 560 Collections.sort(points, (a, b) -> 561 Float.compare(dist(width, height, a.minWidthDps, a.minHeightDps), 562 dist(width, height, b.minWidthDps, b.minHeightDps))); 563 564 DisplayOption closestPoint = points.get(0); 565 GridOption closestOption = closestPoint.grid; 566 float weights = 0; 567 568 if (dist(width, height, closestPoint.minWidthDps, closestPoint.minHeightDps) == 0) { 569 return closestPoint; 570 } 571 572 DisplayOption out = new DisplayOption(closestOption); 573 for (int i = 0; i < points.size() && i < KNEARESTNEIGHBOR; ++i) { 574 DisplayOption p = points.get(i); 575 float w = weight(width, height, p.minWidthDps, p.minHeightDps, WEIGHT_POWER); 576 weights += w; 577 out.add(new DisplayOption().add(p).multiply(w)); 578 } 579 out.multiply(1.0f / weights); 580 581 // Since the bitmaps are persisted, ensure that all bitmap sizes are not larger than 582 // predefined size to avoid cache invalidation 583 for (int i = INDEX_DEFAULT; i < COUNT_SIZES; i++) { 584 out.iconSizes[i] = Math.min(out.iconSizes[i], closestPoint.iconSizes[i]); 585 } 586 587 return out; 588 } 589 getDeviceProfile(Context context)590 public DeviceProfile getDeviceProfile(Context context) { 591 Resources res = context.getResources(); 592 Configuration config = context.getResources().getConfiguration(); 593 594 float screenWidth = config.screenWidthDp * res.getDisplayMetrics().density; 595 float screenHeight = config.screenHeightDp * res.getDisplayMetrics().density; 596 return getBestMatch(screenWidth, screenHeight); 597 } 598 getBestMatch(float screenWidth, float screenHeight)599 public DeviceProfile getBestMatch(float screenWidth, float screenHeight) { 600 DeviceProfile bestMatch = supportedProfiles.get(0); 601 float minDiff = Float.MAX_VALUE; 602 603 for (DeviceProfile profile : supportedProfiles) { 604 float diff = Math.abs(profile.widthPx - screenWidth) 605 + Math.abs(profile.heightPx - screenHeight); 606 if (diff < minDiff) { 607 minDiff = diff; 608 bestMatch = profile; 609 } 610 } 611 return bestMatch; 612 } 613 weight(float x0, float y0, float x1, float y1, float pow)614 private static float weight(float x0, float y0, float x1, float y1, float pow) { 615 float d = dist(x0, y0, x1, y1); 616 if (Float.compare(d, 0f) == 0) { 617 return Float.POSITIVE_INFINITY; 618 } 619 return (float) (WEIGHT_EFFICIENT / Math.pow(d, pow)); 620 } 621 622 /** 623 * As a ratio of screen height, the total distance we want the parallax effect to span 624 * horizontally 625 */ wallpaperTravelToScreenWidthRatio(int width, int height)626 private static float wallpaperTravelToScreenWidthRatio(int width, int height) { 627 float aspectRatio = width / (float) height; 628 629 // At an aspect ratio of 16/10, the wallpaper parallax effect should span 1.5 * screen width 630 // At an aspect ratio of 10/16, the wallpaper parallax effect should span 1.2 * screen width 631 // We will use these two data points to extrapolate how much the wallpaper parallax effect 632 // to span (ie travel) at any aspect ratio: 633 634 final float ASPECT_RATIO_LANDSCAPE = 16 / 10f; 635 final float ASPECT_RATIO_PORTRAIT = 10 / 16f; 636 final float WALLPAPER_WIDTH_TO_SCREEN_RATIO_LANDSCAPE = 1.5f; 637 final float WALLPAPER_WIDTH_TO_SCREEN_RATIO_PORTRAIT = 1.2f; 638 639 // To find out the desired width at different aspect ratios, we use the following two 640 // formulas, where the coefficient on x is the aspect ratio (width/height): 641 // (16/10)x + y = 1.5 642 // (10/16)x + y = 1.2 643 // We solve for x and y and end up with a final formula: 644 final float x = 645 (WALLPAPER_WIDTH_TO_SCREEN_RATIO_LANDSCAPE 646 - WALLPAPER_WIDTH_TO_SCREEN_RATIO_PORTRAIT) / 647 (ASPECT_RATIO_LANDSCAPE - ASPECT_RATIO_PORTRAIT); 648 final float y = WALLPAPER_WIDTH_TO_SCREEN_RATIO_PORTRAIT - x * ASPECT_RATIO_PORTRAIT; 649 return x * aspectRatio + y; 650 } 651 652 public interface OnIDPChangeListener { 653 654 /** 655 * Called when the device provide changes 656 */ 657 void onIdpChanged(boolean modelPropertiesChanged); 658 } 659 660 661 public static final class GridOption { 662 663 public static final String TAG_NAME = "grid-option"; 664 665 private static final int DEVICE_CATEGORY_PHONE = 1 << 0; 666 private static final int DEVICE_CATEGORY_TABLET = 1 << 1; 667 private static final int DEVICE_CATEGORY_MULTI_DISPLAY = 1 << 2; 668 private static final int DEVICE_CATEGORY_ALL = 669 DEVICE_CATEGORY_PHONE | DEVICE_CATEGORY_TABLET | DEVICE_CATEGORY_MULTI_DISPLAY; 670 671 public final String name; 672 public final int numRows; 673 public final int numColumns; 674 public final boolean isEnabled; 675 676 private final int numFolderRows; 677 private final int numFolderColumns; 678 679 private final int numAllAppsColumns; 680 private final int numDatabaseAllAppsColumns; 681 private final int numHotseatIcons; 682 private final int numDatabaseHotseatIcons; 683 684 private final String dbFile; 685 686 private final int defaultLayoutId; 687 private final int demoModeLayoutId; 688 689 private final boolean isScalable; 690 private final int devicePaddingId; 691 692 private final SparseArray<TypedValue> extraAttrs; 693 GridOption(Context context, AttributeSet attrs, @DeviceType int deviceType)694 public GridOption(Context context, AttributeSet attrs, @DeviceType int deviceType) { 695 TypedArray a = context.obtainStyledAttributes( 696 attrs, R.styleable.GridDisplayOption); 697 name = a.getString(R.styleable.GridDisplayOption_name); 698 numRows = a.getInt(R.styleable.GridDisplayOption_numRows, 0); 699 numColumns = a.getInt(R.styleable.GridDisplayOption_numColumns, 0); 700 701 dbFile = a.getString(R.styleable.GridDisplayOption_dbFile); 702 defaultLayoutId = a.getResourceId(deviceType == TYPE_MULTI_DISPLAY && a.hasValue( 703 R.styleable.GridDisplayOption_defaultSplitDisplayLayoutId) 704 ? R.styleable.GridDisplayOption_defaultSplitDisplayLayoutId 705 : R.styleable.GridDisplayOption_defaultLayoutId, 0); 706 demoModeLayoutId = a.getResourceId( 707 R.styleable.GridDisplayOption_demoModeLayoutId, defaultLayoutId); 708 709 numAllAppsColumns = a.getInt( 710 R.styleable.GridDisplayOption_numAllAppsColumns, numColumns); 711 numDatabaseAllAppsColumns = a.getInt( 712 R.styleable.GridDisplayOption_numExtendedAllAppsColumns, 2 * numAllAppsColumns); 713 714 numHotseatIcons = a.getInt( 715 R.styleable.GridDisplayOption_numHotseatIcons, numColumns); 716 numDatabaseHotseatIcons = a.getInt( 717 R.styleable.GridDisplayOption_numExtendedHotseatIcons, 2 * numHotseatIcons); 718 719 numFolderRows = a.getInt( 720 R.styleable.GridDisplayOption_numFolderRows, numRows); 721 numFolderColumns = a.getInt( 722 R.styleable.GridDisplayOption_numFolderColumns, numColumns); 723 724 isScalable = a.getBoolean( 725 R.styleable.GridDisplayOption_isScalable, false); 726 devicePaddingId = a.getResourceId( 727 R.styleable.GridDisplayOption_devicePaddingId, 0); 728 729 int deviceCategory = a.getInt(R.styleable.GridDisplayOption_deviceCategory, 730 DEVICE_CATEGORY_ALL); 731 isEnabled = (deviceType == TYPE_PHONE 732 && ((deviceCategory & DEVICE_CATEGORY_PHONE) == DEVICE_CATEGORY_PHONE)) 733 || (deviceType == TYPE_TABLET 734 && ((deviceCategory & DEVICE_CATEGORY_TABLET) == DEVICE_CATEGORY_TABLET)) 735 || (deviceType == TYPE_MULTI_DISPLAY 736 && ((deviceCategory & DEVICE_CATEGORY_MULTI_DISPLAY) 737 == DEVICE_CATEGORY_MULTI_DISPLAY)); 738 739 a.recycle(); 740 extraAttrs = Themes.createValueMap(context, attrs, 741 IntArray.wrap(R.styleable.GridDisplayOption)); 742 } 743 } 744 745 @VisibleForTesting 746 static final class DisplayOption { 747 748 public final GridOption grid; 749 750 private final float minWidthDps; 751 private final float minHeightDps; 752 private final boolean canBeDefault; 753 754 private final PointF[] minCellSize = new PointF[COUNT_SIZES]; 755 756 private float folderBorderSpace; 757 private final PointF[] borderSpaces = new PointF[COUNT_SIZES]; 758 private final float[] horizontalMargin = new float[COUNT_SIZES]; 759 760 private final float[] iconSizes = new float[COUNT_SIZES]; 761 private final float[] textSizes = new float[COUNT_SIZES]; 762 DisplayOption(GridOption grid, Context context, AttributeSet attrs)763 DisplayOption(GridOption grid, Context context, AttributeSet attrs) { 764 this.grid = grid; 765 766 TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ProfileDisplayOption); 767 768 minWidthDps = a.getFloat(R.styleable.ProfileDisplayOption_minWidthDps, 0); 769 minHeightDps = a.getFloat(R.styleable.ProfileDisplayOption_minHeightDps, 0); 770 771 canBeDefault = a.getBoolean(R.styleable.ProfileDisplayOption_canBeDefault, false); 772 773 float x; 774 float y; 775 776 x = a.getFloat(R.styleable.ProfileDisplayOption_minCellWidthDps, 0); 777 y = a.getFloat(R.styleable.ProfileDisplayOption_minCellHeightDps, 0); 778 minCellSize[INDEX_DEFAULT] = new PointF(x, y); 779 minCellSize[INDEX_LANDSCAPE] = new PointF(x, y); 780 minCellSize[INDEX_ALL_APPS] = new PointF(x, y); 781 782 x = a.getFloat(R.styleable.ProfileDisplayOption_twoPanelPortraitMinCellWidthDps, 783 minCellSize[INDEX_DEFAULT].x); 784 y = a.getFloat(R.styleable.ProfileDisplayOption_twoPanelPortraitMinCellHeightDps, 785 minCellSize[INDEX_DEFAULT].y); 786 minCellSize[INDEX_TWO_PANEL_PORTRAIT] = new PointF(x, y); 787 788 x = a.getFloat(R.styleable.ProfileDisplayOption_twoPanelLandscapeMinCellWidthDps, 789 minCellSize[INDEX_DEFAULT].x); 790 y = a.getFloat(R.styleable.ProfileDisplayOption_twoPanelLandscapeMinCellHeightDps, 791 minCellSize[INDEX_DEFAULT].y); 792 minCellSize[INDEX_TWO_PANEL_LANDSCAPE] = new PointF(x, y); 793 794 float borderSpace = a.getFloat(R.styleable.ProfileDisplayOption_borderSpaceDps, 0); 795 float twoPanelPortraitBorderSpaceDps = a.getFloat( 796 R.styleable.ProfileDisplayOption_twoPanelPortraitBorderSpaceDps, borderSpace); 797 float twoPanelLandscapeBorderSpaceDps = a.getFloat( 798 R.styleable.ProfileDisplayOption_twoPanelLandscapeBorderSpaceDps, borderSpace); 799 800 x = a.getFloat(R.styleable.ProfileDisplayOption_borderSpaceHorizontalDps, borderSpace); 801 y = a.getFloat(R.styleable.ProfileDisplayOption_borderSpaceVerticalDps, borderSpace); 802 borderSpaces[INDEX_DEFAULT] = new PointF(x, y); 803 borderSpaces[INDEX_LANDSCAPE] = new PointF(x, y); 804 805 x = a.getFloat( 806 R.styleable.ProfileDisplayOption_twoPanelPortraitBorderSpaceHorizontalDps, 807 twoPanelPortraitBorderSpaceDps); 808 y = a.getFloat( 809 R.styleable.ProfileDisplayOption_twoPanelPortraitBorderSpaceVerticalDps, 810 twoPanelPortraitBorderSpaceDps); 811 borderSpaces[INDEX_TWO_PANEL_PORTRAIT] = new PointF(x, y); 812 813 x = a.getFloat( 814 R.styleable.ProfileDisplayOption_twoPanelLandscapeBorderSpaceHorizontalDps, 815 twoPanelLandscapeBorderSpaceDps); 816 y = a.getFloat( 817 R.styleable.ProfileDisplayOption_twoPanelLandscapeBorderSpaceVerticalDps, 818 twoPanelLandscapeBorderSpaceDps); 819 borderSpaces[INDEX_TWO_PANEL_LANDSCAPE] = new PointF(x, y); 820 821 x = y = a.getFloat(R.styleable.ProfileDisplayOption_allAppsCellSpacingDps, 822 borderSpace); 823 borderSpaces[INDEX_ALL_APPS] = new PointF(x, y); 824 folderBorderSpace = borderSpace; 825 826 iconSizes[INDEX_DEFAULT] = 827 a.getFloat(R.styleable.ProfileDisplayOption_iconImageSize, 0); 828 iconSizes[INDEX_LANDSCAPE] = 829 a.getFloat(R.styleable.ProfileDisplayOption_landscapeIconSize, 830 iconSizes[INDEX_DEFAULT]); 831 iconSizes[INDEX_ALL_APPS] = 832 a.getFloat(R.styleable.ProfileDisplayOption_allAppsIconSize, 833 iconSizes[INDEX_DEFAULT]); 834 iconSizes[INDEX_TWO_PANEL_PORTRAIT] = 835 a.getFloat(R.styleable.ProfileDisplayOption_twoPanelPortraitIconSize, 836 iconSizes[INDEX_DEFAULT]); 837 iconSizes[INDEX_TWO_PANEL_LANDSCAPE] = 838 a.getFloat(R.styleable.ProfileDisplayOption_twoPanelLandscapeIconSize, 839 iconSizes[INDEX_DEFAULT]); 840 841 textSizes[INDEX_DEFAULT] = 842 a.getFloat(R.styleable.ProfileDisplayOption_iconTextSize, 0); 843 textSizes[INDEX_LANDSCAPE] = 844 a.getFloat(R.styleable.ProfileDisplayOption_landscapeIconTextSize, 845 textSizes[INDEX_DEFAULT]); 846 textSizes[INDEX_ALL_APPS] = 847 a.getFloat(R.styleable.ProfileDisplayOption_allAppsIconTextSize, 848 textSizes[INDEX_DEFAULT]); 849 textSizes[INDEX_TWO_PANEL_PORTRAIT] = 850 a.getFloat(R.styleable.ProfileDisplayOption_twoPanelPortraitIconTextSize, 851 textSizes[INDEX_DEFAULT]); 852 textSizes[INDEX_TWO_PANEL_LANDSCAPE] = 853 a.getFloat(R.styleable.ProfileDisplayOption_twoPanelLandscapeIconTextSize, 854 textSizes[INDEX_DEFAULT]); 855 856 horizontalMargin[INDEX_DEFAULT] = a.getFloat( 857 R.styleable.ProfileDisplayOption_horizontalMargin, 0); 858 horizontalMargin[INDEX_LANDSCAPE] = horizontalMargin[INDEX_DEFAULT]; 859 horizontalMargin[INDEX_ALL_APPS] = horizontalMargin[INDEX_DEFAULT]; 860 horizontalMargin[INDEX_TWO_PANEL_LANDSCAPE] = a.getFloat( 861 R.styleable.ProfileDisplayOption_twoPanelLandscapeHorizontalMargin, 862 horizontalMargin[INDEX_DEFAULT]); 863 horizontalMargin[INDEX_TWO_PANEL_PORTRAIT] = a.getFloat( 864 R.styleable.ProfileDisplayOption_twoPanelPortraitHorizontalMargin, 865 horizontalMargin[INDEX_DEFAULT]); 866 867 a.recycle(); 868 } 869 DisplayOption()870 DisplayOption() { 871 this(null); 872 } 873 DisplayOption(GridOption grid)874 DisplayOption(GridOption grid) { 875 this.grid = grid; 876 minWidthDps = 0; 877 minHeightDps = 0; 878 canBeDefault = false; 879 for (int i = 0; i < COUNT_SIZES; i++) { 880 iconSizes[i] = 0; 881 textSizes[i] = 0; 882 borderSpaces[i] = new PointF(); 883 minCellSize[i] = new PointF(); 884 } 885 } 886 multiply(float w)887 private DisplayOption multiply(float w) { 888 for (int i = 0; i < COUNT_SIZES; i++) { 889 iconSizes[i] *= w; 890 textSizes[i] *= w; 891 borderSpaces[i].x *= w; 892 borderSpaces[i].y *= w; 893 minCellSize[i].x *= w; 894 minCellSize[i].y *= w; 895 horizontalMargin[i] *= w; 896 } 897 898 folderBorderSpace *= w; 899 900 return this; 901 } 902 add(DisplayOption p)903 private DisplayOption add(DisplayOption p) { 904 for (int i = 0; i < COUNT_SIZES; i++) { 905 iconSizes[i] += p.iconSizes[i]; 906 textSizes[i] += p.textSizes[i]; 907 borderSpaces[i].x += p.borderSpaces[i].x; 908 borderSpaces[i].y += p.borderSpaces[i].y; 909 minCellSize[i].x += p.minCellSize[i].x; 910 minCellSize[i].y += p.minCellSize[i].y; 911 horizontalMargin[i] += p.horizontalMargin[i]; 912 } 913 914 folderBorderSpace += p.folderBorderSpace; 915 916 return this; 917 } 918 } 919 } 920