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 }