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.graphics.Point;
22 import android.util.DisplayMetrics;
23 import android.view.Display;
24 import android.view.WindowManager;
25 
26 import com.android.launcher3.util.Thunk;
27 
28 import java.util.ArrayList;
29 import java.util.Collections;
30 import java.util.Comparator;
31 
32 public class InvariantDeviceProfile {
33 
34     // This is a static that we use for the default icon size on a 4/5-inch phone
35     private static float DEFAULT_ICON_SIZE_DP = 60;
36 
37     private static final float ICON_SIZE_DEFINED_IN_APP_DP = 48;
38 
39     // Constants that affects the interpolation curve between statically defined device profile
40     // buckets.
41     private static float KNEARESTNEIGHBOR = 3;
42     private static float WEIGHT_POWER = 5;
43 
44     // used to offset float not being able to express extremely small weights in extreme cases.
45     private static float WEIGHT_EFFICIENT = 100000f;
46 
47     // Profile-defining invariant properties
48     String name;
49     float minWidthDps;
50     float minHeightDps;
51 
52     /**
53      * Number of icons per row and column in the workspace.
54      */
55     public int numRows;
56     public int numColumns;
57 
58     /**
59      * The minimum number of predicted apps in all apps.
60      */
61     int minAllAppsPredictionColumns;
62 
63     /**
64      * Number of icons per row and column in the folder.
65      */
66     public int numFolderRows;
67     public int numFolderColumns;
68     public float iconSize;
69     public int iconBitmapSize;
70     public int fillResIconDpi;
71     public float iconTextSize;
72 
73     /**
74      * Number of icons inside the hotseat area.
75      */
76     public int numHotseatIcons;
77     float hotseatIconSize;
78     int defaultLayoutId;
79 
80     // Derived invariant properties
81     public int hotseatAllAppsRank;
82 
83     DeviceProfile landscapeProfile;
84     DeviceProfile portraitProfile;
85 
InvariantDeviceProfile()86     public InvariantDeviceProfile() {
87     }
88 
InvariantDeviceProfile(InvariantDeviceProfile p)89     public InvariantDeviceProfile(InvariantDeviceProfile p) {
90         this(p.name, p.minWidthDps, p.minHeightDps, p.numRows, p.numColumns,
91                 p.numFolderRows, p.numFolderColumns, p.minAllAppsPredictionColumns,
92                 p.iconSize, p.iconTextSize, p.numHotseatIcons, p.hotseatIconSize,
93                 p.defaultLayoutId);
94     }
95 
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)96     InvariantDeviceProfile(String n, float w, float h, int r, int c, int fr, int fc, int maapc,
97             float is, float its, int hs, float his, int dlId) {
98         // Ensure that we have an odd number of hotseat items (since we need to place all apps)
99         if (hs % 2 == 0) {
100             throw new RuntimeException("All Device Profiles must have an odd number of hotseat spaces");
101         }
102 
103         name = n;
104         minWidthDps = w;
105         minHeightDps = h;
106         numRows = r;
107         numColumns = c;
108         numFolderRows = fr;
109         numFolderColumns = fc;
110         minAllAppsPredictionColumns = maapc;
111         iconSize = is;
112         iconTextSize = its;
113         numHotseatIcons = hs;
114         hotseatIconSize = his;
115         defaultLayoutId = dlId;
116     }
117 
118     @TargetApi(23)
InvariantDeviceProfile(Context context)119     InvariantDeviceProfile(Context context) {
120         WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
121         Display display = wm.getDefaultDisplay();
122         DisplayMetrics dm = new DisplayMetrics();
123         display.getMetrics(dm);
124 
125         Point smallestSize = new Point();
126         Point largestSize = new Point();
127         display.getCurrentSizeRange(smallestSize, largestSize);
128 
129         // This guarantees that width < height
130         minWidthDps = Utilities.dpiFromPx(Math.min(smallestSize.x, smallestSize.y), dm);
131         minHeightDps = Utilities.dpiFromPx(Math.min(largestSize.x, largestSize.y), dm);
132 
133         ArrayList<InvariantDeviceProfile> closestProfiles =
134                 findClosestDeviceProfiles(minWidthDps, minHeightDps, getPredefinedDeviceProfiles());
135         InvariantDeviceProfile interpolatedDeviceProfileOut =
136                 invDistWeightedInterpolate(minWidthDps,  minHeightDps, closestProfiles);
137 
138         InvariantDeviceProfile closestProfile = closestProfiles.get(0);
139         numRows = closestProfile.numRows;
140         numColumns = closestProfile.numColumns;
141         numHotseatIcons = closestProfile.numHotseatIcons;
142         hotseatAllAppsRank = (int) (numHotseatIcons / 2);
143         defaultLayoutId = closestProfile.defaultLayoutId;
144         numFolderRows = closestProfile.numFolderRows;
145         numFolderColumns = closestProfile.numFolderColumns;
146         minAllAppsPredictionColumns = closestProfile.minAllAppsPredictionColumns;
147 
148         iconSize = interpolatedDeviceProfileOut.iconSize;
149         iconBitmapSize = Utilities.pxFromDp(iconSize, dm);
150         iconTextSize = interpolatedDeviceProfileOut.iconTextSize;
151         hotseatIconSize = interpolatedDeviceProfileOut.hotseatIconSize;
152         fillResIconDpi = getLauncherIconDensity(iconBitmapSize);
153 
154         // If the partner customization apk contains any grid overrides, apply them
155         // Supported overrides: numRows, numColumns, iconSize
156         applyPartnerDeviceProfileOverrides(context, dm);
157 
158         Point realSize = new Point();
159         display.getRealSize(realSize);
160         // The real size never changes. smallSide and largeSide will remain the
161         // same in any orientation.
162         int smallSide = Math.min(realSize.x, realSize.y);
163         int largeSide = Math.max(realSize.x, realSize.y);
164 
165         landscapeProfile = new DeviceProfile(context, this, smallestSize, largestSize,
166                 largeSide, smallSide, true /* isLandscape */);
167         portraitProfile = new DeviceProfile(context, this, smallestSize, largestSize,
168                 smallSide, largeSide, false /* isLandscape */);
169     }
170 
getPredefinedDeviceProfiles()171     ArrayList<InvariantDeviceProfile> getPredefinedDeviceProfiles() {
172         ArrayList<InvariantDeviceProfile> predefinedDeviceProfiles = new ArrayList<>();
173         // width, height, #rows, #columns, #folder rows, #folder columns,
174         // iconSize, iconTextSize, #hotseat, #hotseatIconSize, defaultLayoutId.
175         predefinedDeviceProfiles.add(new InvariantDeviceProfile("Super Short Stubby",
176                 255, 300,     2, 3, 2, 3, 3, 48, 13, 3, 48, R.xml.default_workspace_3x3));
177         predefinedDeviceProfiles.add(new InvariantDeviceProfile("Shorter Stubby",
178                 255, 400,     3, 3, 3, 3, 3, 48, 13, 3, 48, R.xml.default_workspace_3x3));
179         predefinedDeviceProfiles.add(new InvariantDeviceProfile("Short Stubby",
180                 275, 420,     3, 4, 3, 4, 4, 48, 13, 5, 48, R.xml.default_workspace_4x4));
181         predefinedDeviceProfiles.add(new InvariantDeviceProfile("Stubby",
182                 255, 450,     3, 4, 3, 4, 4, 48, 13, 5, 48, R.xml.default_workspace_4x4));
183         predefinedDeviceProfiles.add(new InvariantDeviceProfile("Nexus S",
184                 296, 491.33f, 4, 4, 4, 4, 4, 48, 13, 5, 48, R.xml.default_workspace_4x4));
185         predefinedDeviceProfiles.add(new InvariantDeviceProfile("Nexus 4",
186                 359, 567,     4, 4, 4, 4, 4, DEFAULT_ICON_SIZE_DP, 13, 5, 56, R.xml.default_workspace_4x4));
187         predefinedDeviceProfiles.add(new InvariantDeviceProfile("Nexus 5",
188                 335, 567,     4, 4, 4, 4, 4, DEFAULT_ICON_SIZE_DP, 13, 5, 56, R.xml.default_workspace_4x4));
189         predefinedDeviceProfiles.add(new InvariantDeviceProfile("Large Phone",
190                 406, 694,     5, 5, 4, 4, 4, 64, 14.4f,  5, 56, R.xml.default_workspace_5x5));
191         // The tablet profile is odd in that the landscape orientation
192         // also includes the nav bar on the side
193         predefinedDeviceProfiles.add(new InvariantDeviceProfile("Nexus 7",
194                 575, 904,     5, 6, 4, 5, 4, 72, 14.4f,  7, 60, R.xml.default_workspace_5x6));
195         // Larger tablet profiles always have system bars on the top & bottom
196         predefinedDeviceProfiles.add(new InvariantDeviceProfile("Nexus 10",
197                 727, 1207,    5, 6, 4, 5, 4, 76, 14.4f,  7, 76, R.xml.default_workspace_5x6));
198         predefinedDeviceProfiles.add(new InvariantDeviceProfile("20-inch Tablet",
199                 1527, 2527,   7, 7, 6, 6, 4, 100, 20,  7, 72, R.xml.default_workspace_5x6));
200         return predefinedDeviceProfiles;
201     }
202 
getLauncherIconDensity(int requiredSize)203     private int getLauncherIconDensity(int requiredSize) {
204         // Densities typically defined by an app.
205         int[] densityBuckets = new int[] {
206                 DisplayMetrics.DENSITY_LOW,
207                 DisplayMetrics.DENSITY_MEDIUM,
208                 DisplayMetrics.DENSITY_TV,
209                 DisplayMetrics.DENSITY_HIGH,
210                 DisplayMetrics.DENSITY_XHIGH,
211                 DisplayMetrics.DENSITY_XXHIGH,
212                 DisplayMetrics.DENSITY_XXXHIGH
213         };
214 
215         int density = DisplayMetrics.DENSITY_XXXHIGH;
216         for (int i = densityBuckets.length - 1; i >= 0; i--) {
217             float expectedSize = ICON_SIZE_DEFINED_IN_APP_DP * densityBuckets[i]
218                     / DisplayMetrics.DENSITY_DEFAULT;
219             if (expectedSize >= requiredSize) {
220                 density = densityBuckets[i];
221             }
222         }
223 
224         return density;
225     }
226 
227     /**
228      * Apply any Partner customization grid overrides.
229      *
230      * Currently we support: all apps row / column count.
231      */
applyPartnerDeviceProfileOverrides(Context context, DisplayMetrics dm)232     private void applyPartnerDeviceProfileOverrides(Context context, DisplayMetrics dm) {
233         Partner p = Partner.get(context.getPackageManager());
234         if (p != null) {
235             p.applyInvariantDeviceProfileOverrides(this, dm);
236         }
237     }
238 
dist(float x0, float y0, float x1, float y1)239     @Thunk float dist(float x0, float y0, float x1, float y1) {
240         return (float) Math.hypot(x1 - x0, y1 - y0);
241     }
242 
243     /**
244      * Returns the closest device profiles ordered by closeness to the specified width and height
245      */
246     // Package private visibility for testing.
findClosestDeviceProfiles( final float width, final float height, ArrayList<InvariantDeviceProfile> points)247     ArrayList<InvariantDeviceProfile> findClosestDeviceProfiles(
248             final float width, final float height, ArrayList<InvariantDeviceProfile> points) {
249 
250         // Sort the profiles by their closeness to the dimensions
251         ArrayList<InvariantDeviceProfile> pointsByNearness = points;
252         Collections.sort(pointsByNearness, new Comparator<InvariantDeviceProfile>() {
253             public int compare(InvariantDeviceProfile a, InvariantDeviceProfile b) {
254                 return Float.compare(dist(width, height, a.minWidthDps, a.minHeightDps),
255                         dist(width, height, b.minWidthDps, b.minHeightDps));
256             }
257         });
258 
259         return pointsByNearness;
260     }
261 
262     // Package private visibility for testing.
invDistWeightedInterpolate(float width, float height, ArrayList<InvariantDeviceProfile> points)263     InvariantDeviceProfile invDistWeightedInterpolate(float width, float height,
264                 ArrayList<InvariantDeviceProfile> points) {
265         float weights = 0;
266 
267         InvariantDeviceProfile p = points.get(0);
268         if (dist(width, height, p.minWidthDps, p.minHeightDps) == 0) {
269             return p;
270         }
271 
272         InvariantDeviceProfile out = new InvariantDeviceProfile();
273         for (int i = 0; i < points.size() && i < KNEARESTNEIGHBOR; ++i) {
274             p = new InvariantDeviceProfile(points.get(i));
275             float w = weight(width, height, p.minWidthDps, p.minHeightDps, WEIGHT_POWER);
276             weights += w;
277             out.add(p.multiply(w));
278         }
279         return out.multiply(1.0f/weights);
280     }
281 
add(InvariantDeviceProfile p)282     private void add(InvariantDeviceProfile p) {
283         iconSize += p.iconSize;
284         iconTextSize += p.iconTextSize;
285         hotseatIconSize += p.hotseatIconSize;
286     }
287 
multiply(float w)288     private InvariantDeviceProfile multiply(float w) {
289         iconSize *= w;
290         iconTextSize *= w;
291         hotseatIconSize *= w;
292         return this;
293     }
294 
weight(float x0, float y0, float x1, float y1, float pow)295     private float weight(float x0, float y0, float x1, float y1, float pow) {
296         float d = dist(x0, y0, x1, y1);
297         if (Float.compare(d, 0f) == 0) {
298             return Float.POSITIVE_INFINITY;
299         }
300         return (float) (WEIGHT_EFFICIENT / Math.pow(d, pow));
301     }
302 }