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 97 public InvariantDeviceProfile() { 98 } 99 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 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) 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 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 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 */ 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 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. 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. 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 306 private void add(InvariantDeviceProfile p) { 307 iconSize += p.iconSize; 308 iconTextSize += p.iconTextSize; 309 hotseatIconSize += p.hotseatIconSize; 310 } 311 312 private InvariantDeviceProfile multiply(float w) { 313 iconSize *= w; 314 iconTextSize *= w; 315 hotseatIconSize *= w; 316 return this; 317 } 318 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 326 public boolean isAllAppsButtonRank(int rank) { 327 return rank == getAllAppsButtonRank(); 328 } 329 330 public DeviceProfile getDeviceProfile(Context context) { 331 return context.getResources().getConfiguration().orientation 332 == Configuration.ORIENTATION_LANDSCAPE ? landscapeProfile : portraitProfile; 333 } 334 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 */ 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 }