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 }