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