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