• 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 android.annotation.TargetApi;
20 import android.content.Context;
21 import android.content.res.TypedArray;
22 import android.content.res.XmlResourceParser;
23 import android.graphics.Point;
24 import android.util.DisplayMetrics;
25 import android.util.Xml;
26 import android.view.Display;
27 import android.view.WindowManager;
28 
29 import com.android.launcher3.config.FeatureFlags;
30 import com.android.launcher3.config.ProviderConfig;
31 import com.android.launcher3.util.Thunk;
32 
33 import org.xmlpull.v1.XmlPullParser;
34 import org.xmlpull.v1.XmlPullParserException;
35 
36 import java.io.IOException;
37 import java.util.ArrayList;
38 import java.util.Collections;
39 import java.util.Comparator;
40 
41 public class InvariantDeviceProfile {
42 
43     // This is a static that we use for the default icon size on a 4/5-inch phone
44     private static float DEFAULT_ICON_SIZE_DP = 60;
45 
46     private static final float ICON_SIZE_DEFINED_IN_APP_DP = 48;
47 
48     // Constants that affects the interpolation curve between statically defined device profile
49     // buckets.
50     private static float KNEARESTNEIGHBOR = 3;
51     private static float WEIGHT_POWER = 5;
52 
53     // used to offset float not being able to express extremely small weights in extreme cases.
54     private static float WEIGHT_EFFICIENT = 100000f;
55 
56     // Profile-defining invariant properties
57     String name;
58     float minWidthDps;
59     float minHeightDps;
60 
61     /**
62      * Number of icons per row and column in the workspace.
63      */
64     public int numRows;
65     public int numColumns;
66 
67     /**
68      * The minimum number of predicted apps in all apps.
69      */
70     @Deprecated
71     int minAllAppsPredictionColumns;
72 
73     /**
74      * Number of icons per row and column in the folder.
75      */
76     public int numFolderRows;
77     public int numFolderColumns;
78     public float iconSize;
79     public int iconBitmapSize;
80     public int fillResIconDpi;
81     public float iconTextSize;
82 
83     /**
84      * Number of icons inside the hotseat area.
85      */
86     public int numHotseatIcons;
87     float hotseatIconSize;
88     int defaultLayoutId;
89 
90     DeviceProfile landscapeProfile;
91     DeviceProfile portraitProfile;
92 
93     public Point defaultWallpaperSize;
94 
InvariantDeviceProfile()95     public InvariantDeviceProfile() {
96     }
97 
InvariantDeviceProfile(InvariantDeviceProfile p)98     public InvariantDeviceProfile(InvariantDeviceProfile p) {
99         this(p.name, p.minWidthDps, p.minHeightDps, p.numRows, p.numColumns,
100                 p.numFolderRows, p.numFolderColumns, p.minAllAppsPredictionColumns,
101                 p.iconSize, p.iconTextSize, p.numHotseatIcons, p.hotseatIconSize,
102                 p.defaultLayoutId);
103     }
104 
InvariantDeviceProfile(String n, float w, float h, int r, int c, int fr, int fc, int maapc, float is, float its, int hs, float his, int dlId)105     InvariantDeviceProfile(String n, float w, float h, int r, int c, int fr, int fc, int maapc,
106             float is, float its, int hs, float his, int dlId) {
107         name = n;
108         minWidthDps = w;
109         minHeightDps = h;
110         numRows = r;
111         numColumns = c;
112         numFolderRows = fr;
113         numFolderColumns = fc;
114         minAllAppsPredictionColumns = maapc;
115         iconSize = is;
116         iconTextSize = its;
117         numHotseatIcons = hs;
118         hotseatIconSize = his;
119         defaultLayoutId = dlId;
120     }
121 
122     @TargetApi(23)
InvariantDeviceProfile(Context context)123     InvariantDeviceProfile(Context context) {
124         WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
125         Display display = wm.getDefaultDisplay();
126         DisplayMetrics dm = new DisplayMetrics();
127         display.getMetrics(dm);
128 
129         Point smallestSize = new Point();
130         Point largestSize = new Point();
131         display.getCurrentSizeRange(smallestSize, largestSize);
132 
133         // This guarantees that width < height
134         minWidthDps = Utilities.dpiFromPx(Math.min(smallestSize.x, smallestSize.y), dm);
135         minHeightDps = Utilities.dpiFromPx(Math.min(largestSize.x, largestSize.y), dm);
136 
137         ArrayList<InvariantDeviceProfile> closestProfiles = findClosestDeviceProfiles(
138                 minWidthDps, minHeightDps, getPredefinedDeviceProfiles(context));
139         InvariantDeviceProfile interpolatedDeviceProfileOut =
140                 invDistWeightedInterpolate(minWidthDps,  minHeightDps, closestProfiles);
141 
142         InvariantDeviceProfile closestProfile = closestProfiles.get(0);
143         numRows = closestProfile.numRows;
144         numColumns = closestProfile.numColumns;
145         numHotseatIcons = closestProfile.numHotseatIcons;
146         defaultLayoutId = closestProfile.defaultLayoutId;
147         numFolderRows = closestProfile.numFolderRows;
148         numFolderColumns = closestProfile.numFolderColumns;
149         minAllAppsPredictionColumns = closestProfile.minAllAppsPredictionColumns;
150 
151         iconSize = interpolatedDeviceProfileOut.iconSize;
152         iconBitmapSize = Utilities.pxFromDp(iconSize, dm);
153         iconTextSize = interpolatedDeviceProfileOut.iconTextSize;
154         hotseatIconSize = interpolatedDeviceProfileOut.hotseatIconSize;
155         fillResIconDpi = getLauncherIconDensity(iconBitmapSize);
156 
157         // If the partner customization apk contains any grid overrides, apply them
158         // Supported overrides: numRows, numColumns, iconSize
159         applyPartnerDeviceProfileOverrides(context, dm);
160 
161         Point realSize = new Point();
162         display.getRealSize(realSize);
163         // The real size never changes. smallSide and largeSide will remain the
164         // same in any orientation.
165         int smallSide = Math.min(realSize.x, realSize.y);
166         int largeSide = Math.max(realSize.x, realSize.y);
167 
168         landscapeProfile = new DeviceProfile(context, this, smallestSize, largestSize,
169                 largeSide, smallSide, true /* isLandscape */);
170         portraitProfile = new DeviceProfile(context, this, smallestSize, largestSize,
171                 smallSide, largeSide, false /* isLandscape */);
172 
173         // We need to ensure that there is enough extra space in the wallpaper
174         // for the intended parallax effects
175         if (context.getResources().getConfiguration().smallestScreenWidthDp >= 720) {
176             defaultWallpaperSize = new Point(
177                     (int) (largeSide * wallpaperTravelToScreenWidthRatio(largeSide, smallSide)),
178                     largeSide);
179         } else {
180             defaultWallpaperSize = new Point(Math.max(smallSide * 2, largeSide), largeSide);
181         }
182     }
183 
getPredefinedDeviceProfiles(Context context)184     ArrayList<InvariantDeviceProfile> getPredefinedDeviceProfiles(Context context) {
185         ArrayList<InvariantDeviceProfile> profiles = new ArrayList<>();
186         try (XmlResourceParser parser = context.getResources().getXml(R.xml.device_profiles)) {
187             final int depth = parser.getDepth();
188             int type;
189 
190             while (((type = parser.next()) != XmlPullParser.END_TAG ||
191                     parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) {
192                 if ((type == XmlPullParser.START_TAG) && "profile".equals(parser.getName())) {
193                     TypedArray a = context.obtainStyledAttributes(
194                             Xml.asAttributeSet(parser), R.styleable.InvariantDeviceProfile);
195                     int numRows = a.getInt(R.styleable.InvariantDeviceProfile_numRows, 0);
196                     int numColumns = a.getInt(R.styleable.InvariantDeviceProfile_numColumns, 0);
197                     float iconSize = a.getFloat(R.styleable.InvariantDeviceProfile_iconSize, 0);
198                     profiles.add(new InvariantDeviceProfile(
199                             a.getString(R.styleable.InvariantDeviceProfile_name),
200                             a.getFloat(R.styleable.InvariantDeviceProfile_minWidthDps, 0),
201                             a.getFloat(R.styleable.InvariantDeviceProfile_minHeightDps, 0),
202                             numRows,
203                             numColumns,
204                             a.getInt(R.styleable.InvariantDeviceProfile_numFolderRows, numRows),
205                             a.getInt(R.styleable.InvariantDeviceProfile_numFolderColumns, numColumns),
206                             a.getInt(R.styleable.InvariantDeviceProfile_minAllAppsPredictionColumns, numColumns),
207                             iconSize,
208                             a.getFloat(R.styleable.InvariantDeviceProfile_iconTextSize, 0),
209                             a.getInt(R.styleable.InvariantDeviceProfile_numHotseatIcons, numColumns),
210                             a.getFloat(R.styleable.InvariantDeviceProfile_hotseatIconSize, iconSize),
211                             a.getResourceId(R.styleable.InvariantDeviceProfile_defaultLayoutId, 0)));
212                     a.recycle();
213                 }
214             }
215         } catch (IOException|XmlPullParserException e) {
216             throw new RuntimeException(e);
217         }
218         return profiles;
219     }
220 
getLauncherIconDensity(int requiredSize)221     private int getLauncherIconDensity(int requiredSize) {
222         // Densities typically defined by an app.
223         int[] densityBuckets = new int[] {
224                 DisplayMetrics.DENSITY_LOW,
225                 DisplayMetrics.DENSITY_MEDIUM,
226                 DisplayMetrics.DENSITY_TV,
227                 DisplayMetrics.DENSITY_HIGH,
228                 DisplayMetrics.DENSITY_XHIGH,
229                 DisplayMetrics.DENSITY_XXHIGH,
230                 DisplayMetrics.DENSITY_XXXHIGH
231         };
232 
233         int density = DisplayMetrics.DENSITY_XXXHIGH;
234         for (int i = densityBuckets.length - 1; i >= 0; i--) {
235             float expectedSize = ICON_SIZE_DEFINED_IN_APP_DP * densityBuckets[i]
236                     / DisplayMetrics.DENSITY_DEFAULT;
237             if (expectedSize >= requiredSize) {
238                 density = densityBuckets[i];
239             }
240         }
241 
242         return density;
243     }
244 
245     /**
246      * Apply any Partner customization grid overrides.
247      *
248      * Currently we support: all apps row / column count.
249      */
applyPartnerDeviceProfileOverrides(Context context, DisplayMetrics dm)250     private void applyPartnerDeviceProfileOverrides(Context context, DisplayMetrics dm) {
251         Partner p = Partner.get(context.getPackageManager());
252         if (p != null) {
253             p.applyInvariantDeviceProfileOverrides(this, dm);
254         }
255     }
256 
dist(float x0, float y0, float x1, float y1)257     @Thunk float dist(float x0, float y0, float x1, float y1) {
258         return (float) Math.hypot(x1 - x0, y1 - y0);
259     }
260 
261     /**
262      * Returns the closest device profiles ordered by closeness to the specified width and height
263      */
264     // Package private visibility for testing.
findClosestDeviceProfiles( final float width, final float height, ArrayList<InvariantDeviceProfile> points)265     ArrayList<InvariantDeviceProfile> findClosestDeviceProfiles(
266             final float width, final float height, ArrayList<InvariantDeviceProfile> points) {
267 
268         // Sort the profiles by their closeness to the dimensions
269         ArrayList<InvariantDeviceProfile> pointsByNearness = points;
270         Collections.sort(pointsByNearness, new Comparator<InvariantDeviceProfile>() {
271             public int compare(InvariantDeviceProfile a, InvariantDeviceProfile b) {
272                 return Float.compare(dist(width, height, a.minWidthDps, a.minHeightDps),
273                         dist(width, height, b.minWidthDps, b.minHeightDps));
274             }
275         });
276 
277         return pointsByNearness;
278     }
279 
280     // Package private visibility for testing.
invDistWeightedInterpolate(float width, float height, ArrayList<InvariantDeviceProfile> points)281     InvariantDeviceProfile invDistWeightedInterpolate(float width, float height,
282                 ArrayList<InvariantDeviceProfile> points) {
283         float weights = 0;
284 
285         InvariantDeviceProfile p = points.get(0);
286         if (dist(width, height, p.minWidthDps, p.minHeightDps) == 0) {
287             return p;
288         }
289 
290         InvariantDeviceProfile out = new InvariantDeviceProfile();
291         for (int i = 0; i < points.size() && i < KNEARESTNEIGHBOR; ++i) {
292             p = new InvariantDeviceProfile(points.get(i));
293             float w = weight(width, height, p.minWidthDps, p.minHeightDps, WEIGHT_POWER);
294             weights += w;
295             out.add(p.multiply(w));
296         }
297         return out.multiply(1.0f/weights);
298     }
299 
add(InvariantDeviceProfile p)300     private void add(InvariantDeviceProfile p) {
301         iconSize += p.iconSize;
302         iconTextSize += p.iconTextSize;
303         hotseatIconSize += p.hotseatIconSize;
304     }
305 
multiply(float w)306     private InvariantDeviceProfile multiply(float w) {
307         iconSize *= w;
308         iconTextSize *= w;
309         hotseatIconSize *= w;
310         return this;
311     }
312 
getAllAppsButtonRank()313     public int getAllAppsButtonRank() {
314         if (ProviderConfig.IS_DOGFOOD_BUILD && FeatureFlags.NO_ALL_APPS_ICON) {
315             throw new IllegalAccessError("Accessing all apps rank when all-apps is disabled");
316         }
317         return numHotseatIcons / 2;
318     }
319 
isAllAppsButtonRank(int rank)320     public boolean isAllAppsButtonRank(int rank) {
321         return rank == getAllAppsButtonRank();
322     }
323 
weight(float x0, float y0, float x1, float y1, float pow)324     private float weight(float x0, float y0, float x1, float y1, float pow) {
325         float d = dist(x0, y0, x1, y1);
326         if (Float.compare(d, 0f) == 0) {
327             return Float.POSITIVE_INFINITY;
328         }
329         return (float) (WEIGHT_EFFICIENT / Math.pow(d, pow));
330     }
331 
332     /**
333      * As a ratio of screen height, the total distance we want the parallax effect to span
334      * horizontally
335      */
wallpaperTravelToScreenWidthRatio(int width, int height)336     private static float wallpaperTravelToScreenWidthRatio(int width, int height) {
337         float aspectRatio = width / (float) height;
338 
339         // At an aspect ratio of 16/10, the wallpaper parallax effect should span 1.5 * screen width
340         // At an aspect ratio of 10/16, the wallpaper parallax effect should span 1.2 * screen width
341         // We will use these two data points to extrapolate how much the wallpaper parallax effect
342         // to span (ie travel) at any aspect ratio:
343 
344         final float ASPECT_RATIO_LANDSCAPE = 16/10f;
345         final float ASPECT_RATIO_PORTRAIT = 10/16f;
346         final float WALLPAPER_WIDTH_TO_SCREEN_RATIO_LANDSCAPE = 1.5f;
347         final float WALLPAPER_WIDTH_TO_SCREEN_RATIO_PORTRAIT = 1.2f;
348 
349         // To find out the desired width at different aspect ratios, we use the following two
350         // formulas, where the coefficient on x is the aspect ratio (width/height):
351         //   (16/10)x + y = 1.5
352         //   (10/16)x + y = 1.2
353         // We solve for x and y and end up with a final formula:
354         final float x =
355                 (WALLPAPER_WIDTH_TO_SCREEN_RATIO_LANDSCAPE - WALLPAPER_WIDTH_TO_SCREEN_RATIO_PORTRAIT) /
356                         (ASPECT_RATIO_LANDSCAPE - ASPECT_RATIO_PORTRAIT);
357         final float y = WALLPAPER_WIDTH_TO_SCREEN_RATIO_PORTRAIT - x * ASPECT_RATIO_PORTRAIT;
358         return x * aspectRatio + y;
359     }
360 
361 }