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 InvariantDeviceProfile()97 public InvariantDeviceProfile() { 98 } 99 InvariantDeviceProfile(InvariantDeviceProfile p)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 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)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) InvariantDeviceProfile(Context context)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 getPredefinedDeviceProfiles(Context context)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 getLauncherIconDensity(int requiredSize)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 */ applyPartnerDeviceProfileOverrides(Context context, DisplayMetrics dm)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 dist(float x0, float y0, float x1, float y1)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. findClosestDeviceProfiles( final float width, final float height, ArrayList<InvariantDeviceProfile> points)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. invDistWeightedInterpolate(float width, float height, ArrayList<InvariantDeviceProfile> points)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 add(InvariantDeviceProfile p)306 private void add(InvariantDeviceProfile p) { 307 iconSize += p.iconSize; 308 iconTextSize += p.iconTextSize; 309 hotseatIconSize += p.hotseatIconSize; 310 } 311 multiply(float w)312 private InvariantDeviceProfile multiply(float w) { 313 iconSize *= w; 314 iconTextSize *= w; 315 hotseatIconSize *= w; 316 return this; 317 } 318 getAllAppsButtonRank()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 isAllAppsButtonRank(int rank)326 public boolean isAllAppsButtonRank(int rank) { 327 return rank == getAllAppsButtonRank(); 328 } 329 getDeviceProfile(Context context)330 public DeviceProfile getDeviceProfile(Context context) { 331 return context.getResources().getConfiguration().orientation 332 == Configuration.ORIENTATION_LANDSCAPE ? landscapeProfile : portraitProfile; 333 } 334 weight(float x0, float y0, float x1, float y1, float pow)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 */ wallpaperTravelToScreenWidthRatio(int width, int height)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 }