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