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