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.util.Thunk; 32 33 import org.xmlpull.v1.XmlPullParser; 34 import org.xmlpull.v1.XmlPullParserException; 35 36 import java.io.IOException; 37 import java.util.ArrayList; 38 import java.util.Collections; 39 import java.util.Comparator; 40 41 public class InvariantDeviceProfile { 42 43 // This is a static that we use for the default icon size on a 4/5-inch phone 44 private static float DEFAULT_ICON_SIZE_DP = 60; 45 46 private static final float ICON_SIZE_DEFINED_IN_APP_DP = 48; 47 48 // Constants that affects the interpolation curve between statically defined device profile 49 // buckets. 50 private static float KNEARESTNEIGHBOR = 3; 51 private static float WEIGHT_POWER = 5; 52 53 // used to offset float not being able to express extremely small weights in extreme cases. 54 private static float WEIGHT_EFFICIENT = 100000f; 55 56 // Profile-defining invariant properties 57 String name; 58 float minWidthDps; 59 float minHeightDps; 60 61 /** 62 * Number of icons per row and column in the workspace. 63 */ 64 public int numRows; 65 public int numColumns; 66 67 /** 68 * The minimum number of predicted apps in all apps. 69 */ 70 @Deprecated 71 int minAllAppsPredictionColumns; 72 73 /** 74 * Number of icons per row and column in the folder. 75 */ 76 public int numFolderRows; 77 public int numFolderColumns; 78 public float iconSize; 79 public float landscapeIconSize; 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 89 int defaultLayoutId; 90 int demoModeLayoutId; 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.landscapeIconSize, p.iconTextSize, p.numHotseatIcons, 104 p.defaultLayoutId, p.demoModeLayoutId); 105 } 106 InvariantDeviceProfile(String n, float w, float h, int r, int c, int fr, int fc, int maapc, float is, float lis, float its, int hs, int dlId, int dmlId)107 InvariantDeviceProfile(String n, float w, float h, int r, int c, int fr, int fc, int maapc, 108 float is, float lis, float its, int hs, int dlId, int dmlId) { 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 landscapeIconSize = lis; 119 iconTextSize = its; 120 numHotseatIcons = hs; 121 defaultLayoutId = dlId; 122 demoModeLayoutId = dmlId; 123 } 124 125 @TargetApi(23) InvariantDeviceProfile(Context context)126 InvariantDeviceProfile(Context context) { 127 WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); 128 Display display = wm.getDefaultDisplay(); 129 DisplayMetrics dm = new DisplayMetrics(); 130 display.getMetrics(dm); 131 132 Point smallestSize = new Point(); 133 Point largestSize = new Point(); 134 display.getCurrentSizeRange(smallestSize, largestSize); 135 136 // This guarantees that width < height 137 minWidthDps = Utilities.dpiFromPx(Math.min(smallestSize.x, smallestSize.y), dm); 138 minHeightDps = Utilities.dpiFromPx(Math.min(largestSize.x, largestSize.y), dm); 139 140 ArrayList<InvariantDeviceProfile> closestProfiles = findClosestDeviceProfiles( 141 minWidthDps, minHeightDps, getPredefinedDeviceProfiles(context)); 142 InvariantDeviceProfile interpolatedDeviceProfileOut = 143 invDistWeightedInterpolate(minWidthDps, minHeightDps, closestProfiles); 144 145 InvariantDeviceProfile closestProfile = closestProfiles.get(0); 146 numRows = closestProfile.numRows; 147 numColumns = closestProfile.numColumns; 148 numHotseatIcons = closestProfile.numHotseatIcons; 149 defaultLayoutId = closestProfile.defaultLayoutId; 150 demoModeLayoutId = closestProfile.demoModeLayoutId; 151 numFolderRows = closestProfile.numFolderRows; 152 numFolderColumns = closestProfile.numFolderColumns; 153 minAllAppsPredictionColumns = closestProfile.minAllAppsPredictionColumns; 154 155 iconSize = interpolatedDeviceProfileOut.iconSize; 156 landscapeIconSize = interpolatedDeviceProfileOut.landscapeIconSize; 157 iconBitmapSize = Utilities.pxFromDp(iconSize, dm); 158 iconTextSize = interpolatedDeviceProfileOut.iconTextSize; 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 Point realSize = new Point(); 166 display.getRealSize(realSize); 167 // The real size never changes. smallSide and largeSide will remain the 168 // same in any orientation. 169 int smallSide = Math.min(realSize.x, realSize.y); 170 int largeSide = Math.max(realSize.x, realSize.y); 171 172 landscapeProfile = new DeviceProfile(context, this, smallestSize, largestSize, 173 largeSide, smallSide, true /* isLandscape */); 174 portraitProfile = new DeviceProfile(context, this, smallestSize, largestSize, 175 smallSide, largeSide, false /* isLandscape */); 176 177 // We need to ensure that there is enough extra space in the wallpaper 178 // for the intended parallax effects 179 if (context.getResources().getConfiguration().smallestScreenWidthDp >= 720) { 180 defaultWallpaperSize = new Point( 181 (int) (largeSide * wallpaperTravelToScreenWidthRatio(largeSide, smallSide)), 182 largeSide); 183 } else { 184 defaultWallpaperSize = new Point(Math.max(smallSide * 2, largeSide), largeSide); 185 } 186 } 187 getPredefinedDeviceProfiles(Context context)188 ArrayList<InvariantDeviceProfile> getPredefinedDeviceProfiles(Context context) { 189 ArrayList<InvariantDeviceProfile> profiles = new ArrayList<>(); 190 try (XmlResourceParser parser = context.getResources().getXml(R.xml.device_profiles)) { 191 final int depth = parser.getDepth(); 192 int type; 193 194 while (((type = parser.next()) != XmlPullParser.END_TAG || 195 parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) { 196 if ((type == XmlPullParser.START_TAG) && "profile".equals(parser.getName())) { 197 TypedArray a = context.obtainStyledAttributes( 198 Xml.asAttributeSet(parser), R.styleable.InvariantDeviceProfile); 199 int numRows = a.getInt(R.styleable.InvariantDeviceProfile_numRows, 0); 200 int numColumns = a.getInt(R.styleable.InvariantDeviceProfile_numColumns, 0); 201 float iconSize = a.getFloat(R.styleable.InvariantDeviceProfile_iconSize, 0); 202 profiles.add(new InvariantDeviceProfile( 203 a.getString(R.styleable.InvariantDeviceProfile_name), 204 a.getFloat(R.styleable.InvariantDeviceProfile_minWidthDps, 0), 205 a.getFloat(R.styleable.InvariantDeviceProfile_minHeightDps, 0), 206 numRows, 207 numColumns, 208 a.getInt(R.styleable.InvariantDeviceProfile_numFolderRows, numRows), 209 a.getInt(R.styleable.InvariantDeviceProfile_numFolderColumns, numColumns), 210 a.getInt(R.styleable.InvariantDeviceProfile_minAllAppsPredictionColumns, numColumns), 211 iconSize, 212 a.getFloat(R.styleable.InvariantDeviceProfile_landscapeIconSize, iconSize), 213 a.getFloat(R.styleable.InvariantDeviceProfile_iconTextSize, 0), 214 a.getInt(R.styleable.InvariantDeviceProfile_numHotseatIcons, numColumns), 215 a.getResourceId(R.styleable.InvariantDeviceProfile_defaultLayoutId, 0), 216 a.getResourceId(R.styleable.InvariantDeviceProfile_demoModeLayoutId, 0))); 217 a.recycle(); 218 } 219 } 220 } catch (IOException|XmlPullParserException e) { 221 throw new RuntimeException(e); 222 } 223 return profiles; 224 } 225 getLauncherIconDensity(int requiredSize)226 private int getLauncherIconDensity(int requiredSize) { 227 // Densities typically defined by an app. 228 int[] densityBuckets = new int[] { 229 DisplayMetrics.DENSITY_LOW, 230 DisplayMetrics.DENSITY_MEDIUM, 231 DisplayMetrics.DENSITY_TV, 232 DisplayMetrics.DENSITY_HIGH, 233 DisplayMetrics.DENSITY_XHIGH, 234 DisplayMetrics.DENSITY_XXHIGH, 235 DisplayMetrics.DENSITY_XXXHIGH 236 }; 237 238 int density = DisplayMetrics.DENSITY_XXXHIGH; 239 for (int i = densityBuckets.length - 1; i >= 0; i--) { 240 float expectedSize = ICON_SIZE_DEFINED_IN_APP_DP * densityBuckets[i] 241 / DisplayMetrics.DENSITY_DEFAULT; 242 if (expectedSize >= requiredSize) { 243 density = densityBuckets[i]; 244 } 245 } 246 247 return density; 248 } 249 250 /** 251 * Apply any Partner customization grid overrides. 252 * 253 * Currently we support: all apps row / column count. 254 */ applyPartnerDeviceProfileOverrides(Context context, DisplayMetrics dm)255 private void applyPartnerDeviceProfileOverrides(Context context, DisplayMetrics dm) { 256 Partner p = Partner.get(context.getPackageManager()); 257 if (p != null) { 258 p.applyInvariantDeviceProfileOverrides(this, dm); 259 } 260 } 261 dist(float x0, float y0, float x1, float y1)262 @Thunk float dist(float x0, float y0, float x1, float y1) { 263 return (float) Math.hypot(x1 - x0, y1 - y0); 264 } 265 266 /** 267 * Returns the closest device profiles ordered by closeness to the specified width and height 268 */ 269 // Package private visibility for testing. findClosestDeviceProfiles( final float width, final float height, ArrayList<InvariantDeviceProfile> points)270 ArrayList<InvariantDeviceProfile> findClosestDeviceProfiles( 271 final float width, final float height, ArrayList<InvariantDeviceProfile> points) { 272 273 // Sort the profiles by their closeness to the dimensions 274 ArrayList<InvariantDeviceProfile> pointsByNearness = points; 275 Collections.sort(pointsByNearness, new Comparator<InvariantDeviceProfile>() { 276 public int compare(InvariantDeviceProfile a, InvariantDeviceProfile b) { 277 return Float.compare(dist(width, height, a.minWidthDps, a.minHeightDps), 278 dist(width, height, b.minWidthDps, b.minHeightDps)); 279 } 280 }); 281 282 return pointsByNearness; 283 } 284 285 // Package private visibility for testing. invDistWeightedInterpolate(float width, float height, ArrayList<InvariantDeviceProfile> points)286 InvariantDeviceProfile invDistWeightedInterpolate(float width, float height, 287 ArrayList<InvariantDeviceProfile> points) { 288 float weights = 0; 289 290 InvariantDeviceProfile p = points.get(0); 291 if (dist(width, height, p.minWidthDps, p.minHeightDps) == 0) { 292 return p; 293 } 294 295 InvariantDeviceProfile out = new InvariantDeviceProfile(); 296 for (int i = 0; i < points.size() && i < KNEARESTNEIGHBOR; ++i) { 297 p = new InvariantDeviceProfile(points.get(i)); 298 float w = weight(width, height, p.minWidthDps, p.minHeightDps, WEIGHT_POWER); 299 weights += w; 300 out.add(p.multiply(w)); 301 } 302 return out.multiply(1.0f/weights); 303 } 304 add(InvariantDeviceProfile p)305 private void add(InvariantDeviceProfile p) { 306 iconSize += p.iconSize; 307 landscapeIconSize += p.landscapeIconSize; 308 iconTextSize += p.iconTextSize; 309 } 310 multiply(float w)311 private InvariantDeviceProfile multiply(float w) { 312 iconSize *= w; 313 landscapeIconSize *= w; 314 iconTextSize *= w; 315 return this; 316 } 317 getAllAppsButtonRank()318 public int getAllAppsButtonRank() { 319 if (FeatureFlags.IS_DOGFOOD_BUILD && FeatureFlags.NO_ALL_APPS_ICON) { 320 throw new IllegalAccessError("Accessing all apps rank when all-apps is disabled"); 321 } 322 return numHotseatIcons / 2; 323 } 324 isAllAppsButtonRank(int rank)325 public boolean isAllAppsButtonRank(int rank) { 326 return rank == getAllAppsButtonRank(); 327 } 328 getDeviceProfile(Context context)329 public DeviceProfile getDeviceProfile(Context context) { 330 return context.getResources().getConfiguration().orientation 331 == Configuration.ORIENTATION_LANDSCAPE ? landscapeProfile : portraitProfile; 332 } 333 weight(float x0, float y0, float x1, float y1, float pow)334 private float weight(float x0, float y0, float x1, float y1, float pow) { 335 float d = dist(x0, y0, x1, y1); 336 if (Float.compare(d, 0f) == 0) { 337 return Float.POSITIVE_INFINITY; 338 } 339 return (float) (WEIGHT_EFFICIENT / Math.pow(d, pow)); 340 } 341 342 /** 343 * As a ratio of screen height, the total distance we want the parallax effect to span 344 * horizontally 345 */ wallpaperTravelToScreenWidthRatio(int width, int height)346 private static float wallpaperTravelToScreenWidthRatio(int width, int height) { 347 float aspectRatio = width / (float) height; 348 349 // At an aspect ratio of 16/10, the wallpaper parallax effect should span 1.5 * screen width 350 // At an aspect ratio of 10/16, the wallpaper parallax effect should span 1.2 * screen width 351 // We will use these two data points to extrapolate how much the wallpaper parallax effect 352 // to span (ie travel) at any aspect ratio: 353 354 final float ASPECT_RATIO_LANDSCAPE = 16/10f; 355 final float ASPECT_RATIO_PORTRAIT = 10/16f; 356 final float WALLPAPER_WIDTH_TO_SCREEN_RATIO_LANDSCAPE = 1.5f; 357 final float WALLPAPER_WIDTH_TO_SCREEN_RATIO_PORTRAIT = 1.2f; 358 359 // To find out the desired width at different aspect ratios, we use the following two 360 // formulas, where the coefficient on x is the aspect ratio (width/height): 361 // (16/10)x + y = 1.5 362 // (10/16)x + y = 1.2 363 // We solve for x and y and end up with a final formula: 364 final float x = 365 (WALLPAPER_WIDTH_TO_SCREEN_RATIO_LANDSCAPE - WALLPAPER_WIDTH_TO_SCREEN_RATIO_PORTRAIT) / 366 (ASPECT_RATIO_LANDSCAPE - ASPECT_RATIO_PORTRAIT); 367 final float y = WALLPAPER_WIDTH_TO_SCREEN_RATIO_PORTRAIT - x * ASPECT_RATIO_PORTRAIT; 368 return x * aspectRatio + y; 369 } 370 371 }