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