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