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