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