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 86 public InvariantDeviceProfile() { 87 } 88 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 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) 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 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 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 */ 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 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. 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. 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 282 private void add(InvariantDeviceProfile p) { 283 iconSize += p.iconSize; 284 iconTextSize += p.iconTextSize; 285 hotseatIconSize += p.hotseatIconSize; 286 } 287 288 private InvariantDeviceProfile multiply(float w) { 289 iconSize *= w; 290 iconTextSize *= w; 291 hotseatIconSize *= w; 292 return this; 293 } 294 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 }