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