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.support.annotation.VisibleForTesting; 26 import android.util.DisplayMetrics; 27 import android.util.Xml; 28 import android.view.Display; 29 import android.view.WindowManager; 30 31 import com.android.launcher3.config.FeatureFlags; 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 * Number of icons per row and column in the folder. 70 */ 71 public int numFolderRows; 72 public int numFolderColumns; 73 public float iconSize; 74 public float landscapeIconSize; 75 public int iconBitmapSize; 76 public int fillResIconDpi; 77 public float iconTextSize; 78 79 /** 80 * Number of icons inside the hotseat area. 81 */ 82 public int numHotseatIcons; 83 84 int defaultLayoutId; 85 int demoModeLayoutId; 86 87 public DeviceProfile landscapeProfile; 88 public DeviceProfile portraitProfile; 89 90 public Point defaultWallpaperSize; 91 92 @VisibleForTesting InvariantDeviceProfile()93 public InvariantDeviceProfile() { 94 } 95 InvariantDeviceProfile(InvariantDeviceProfile p)96 private InvariantDeviceProfile(InvariantDeviceProfile p) { 97 this(p.name, p.minWidthDps, p.minHeightDps, p.numRows, p.numColumns, 98 p.numFolderRows, p.numFolderColumns, 99 p.iconSize, p.landscapeIconSize, p.iconTextSize, p.numHotseatIcons, 100 p.defaultLayoutId, p.demoModeLayoutId); 101 } 102 InvariantDeviceProfile(String n, float w, float h, int r, int c, int fr, int fc, float is, float lis, float its, int hs, int dlId, int dmlId)103 private InvariantDeviceProfile(String n, float w, float h, int r, int c, int fr, int fc, 104 float is, float lis, float its, int hs, int dlId, int dmlId) { 105 name = n; 106 minWidthDps = w; 107 minHeightDps = h; 108 numRows = r; 109 numColumns = c; 110 numFolderRows = fr; 111 numFolderColumns = fc; 112 iconSize = is; 113 landscapeIconSize = lis; 114 iconTextSize = its; 115 numHotseatIcons = hs; 116 defaultLayoutId = dlId; 117 demoModeLayoutId = dmlId; 118 } 119 120 @TargetApi(23) InvariantDeviceProfile(Context context)121 public InvariantDeviceProfile(Context context) { 122 WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); 123 Display display = wm.getDefaultDisplay(); 124 DisplayMetrics dm = new DisplayMetrics(); 125 display.getMetrics(dm); 126 127 Point smallestSize = new Point(); 128 Point largestSize = new Point(); 129 display.getCurrentSizeRange(smallestSize, largestSize); 130 131 // This guarantees that width < height 132 minWidthDps = Utilities.dpiFromPx(Math.min(smallestSize.x, smallestSize.y), dm); 133 minHeightDps = Utilities.dpiFromPx(Math.min(largestSize.x, largestSize.y), dm); 134 135 ArrayList<InvariantDeviceProfile> closestProfiles = findClosestDeviceProfiles( 136 minWidthDps, minHeightDps, getPredefinedDeviceProfiles(context)); 137 InvariantDeviceProfile interpolatedDeviceProfileOut = 138 invDistWeightedInterpolate(minWidthDps, minHeightDps, closestProfiles); 139 140 InvariantDeviceProfile closestProfile = closestProfiles.get(0); 141 numRows = closestProfile.numRows; 142 numColumns = closestProfile.numColumns; 143 numHotseatIcons = closestProfile.numHotseatIcons; 144 defaultLayoutId = closestProfile.defaultLayoutId; 145 demoModeLayoutId = closestProfile.demoModeLayoutId; 146 numFolderRows = closestProfile.numFolderRows; 147 numFolderColumns = closestProfile.numFolderColumns; 148 149 iconSize = interpolatedDeviceProfileOut.iconSize; 150 landscapeIconSize = interpolatedDeviceProfileOut.landscapeIconSize; 151 iconBitmapSize = Utilities.pxFromDp(iconSize, dm); 152 iconTextSize = interpolatedDeviceProfileOut.iconTextSize; 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 */, false /* isMultiWindowMode */); 168 portraitProfile = new DeviceProfile(context, this, smallestSize, largestSize, 169 smallSide, largeSide, false /* isLandscape */, false /* isMultiWindowMode */); 170 171 // We need to ensure that there is enough extra space in the wallpaper 172 // for the intended parallax effects 173 if (context.getResources().getConfiguration().smallestScreenWidthDp >= 720) { 174 defaultWallpaperSize = new Point( 175 (int) (largeSide * wallpaperTravelToScreenWidthRatio(largeSide, smallSide)), 176 largeSide); 177 } else { 178 defaultWallpaperSize = new Point(Math.max(smallSide * 2, largeSide), largeSide); 179 } 180 } 181 getPredefinedDeviceProfiles(Context context)182 ArrayList<InvariantDeviceProfile> getPredefinedDeviceProfiles(Context context) { 183 ArrayList<InvariantDeviceProfile> profiles = new ArrayList<>(); 184 try (XmlResourceParser parser = context.getResources().getXml(R.xml.device_profiles)) { 185 final int depth = parser.getDepth(); 186 int type; 187 188 while (((type = parser.next()) != XmlPullParser.END_TAG || 189 parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) { 190 if ((type == XmlPullParser.START_TAG) && "profile".equals(parser.getName())) { 191 TypedArray a = context.obtainStyledAttributes( 192 Xml.asAttributeSet(parser), R.styleable.InvariantDeviceProfile); 193 int numRows = a.getInt(R.styleable.InvariantDeviceProfile_numRows, 0); 194 int numColumns = a.getInt(R.styleable.InvariantDeviceProfile_numColumns, 0); 195 float iconSize = a.getFloat(R.styleable.InvariantDeviceProfile_iconSize, 0); 196 profiles.add(new InvariantDeviceProfile( 197 a.getString(R.styleable.InvariantDeviceProfile_name), 198 a.getFloat(R.styleable.InvariantDeviceProfile_minWidthDps, 0), 199 a.getFloat(R.styleable.InvariantDeviceProfile_minHeightDps, 0), 200 numRows, 201 numColumns, 202 a.getInt(R.styleable.InvariantDeviceProfile_numFolderRows, numRows), 203 a.getInt(R.styleable.InvariantDeviceProfile_numFolderColumns, numColumns), 204 iconSize, 205 a.getFloat(R.styleable.InvariantDeviceProfile_landscapeIconSize, iconSize), 206 a.getFloat(R.styleable.InvariantDeviceProfile_iconTextSize, 0), 207 a.getInt(R.styleable.InvariantDeviceProfile_numHotseatIcons, numColumns), 208 a.getResourceId(R.styleable.InvariantDeviceProfile_defaultLayoutId, 0), 209 a.getResourceId(R.styleable.InvariantDeviceProfile_demoModeLayoutId, 0))); 210 a.recycle(); 211 } 212 } 213 } catch (IOException|XmlPullParserException e) { 214 throw new RuntimeException(e); 215 } 216 return profiles; 217 } 218 getLauncherIconDensity(int requiredSize)219 private int getLauncherIconDensity(int requiredSize) { 220 // Densities typically defined by an app. 221 int[] densityBuckets = new int[] { 222 DisplayMetrics.DENSITY_LOW, 223 DisplayMetrics.DENSITY_MEDIUM, 224 DisplayMetrics.DENSITY_TV, 225 DisplayMetrics.DENSITY_HIGH, 226 DisplayMetrics.DENSITY_XHIGH, 227 DisplayMetrics.DENSITY_XXHIGH, 228 DisplayMetrics.DENSITY_XXXHIGH 229 }; 230 231 int density = DisplayMetrics.DENSITY_XXXHIGH; 232 for (int i = densityBuckets.length - 1; i >= 0; i--) { 233 float expectedSize = ICON_SIZE_DEFINED_IN_APP_DP * densityBuckets[i] 234 / DisplayMetrics.DENSITY_DEFAULT; 235 if (expectedSize >= requiredSize) { 236 density = densityBuckets[i]; 237 } 238 } 239 240 return density; 241 } 242 243 /** 244 * Apply any Partner customization grid overrides. 245 * 246 * Currently we support: all apps row / column count. 247 */ applyPartnerDeviceProfileOverrides(Context context, DisplayMetrics dm)248 private void applyPartnerDeviceProfileOverrides(Context context, DisplayMetrics dm) { 249 Partner p = Partner.get(context.getPackageManager()); 250 if (p != null) { 251 p.applyInvariantDeviceProfileOverrides(this, dm); 252 } 253 } 254 dist(float x0, float y0, float x1, float y1)255 @Thunk float dist(float x0, float y0, float x1, float y1) { 256 return (float) Math.hypot(x1 - x0, y1 - y0); 257 } 258 259 /** 260 * Returns the closest device profiles ordered by closeness to the specified width and height 261 */ 262 // Package private visibility for testing. findClosestDeviceProfiles( final float width, final float height, ArrayList<InvariantDeviceProfile> points)263 ArrayList<InvariantDeviceProfile> findClosestDeviceProfiles( 264 final float width, final float height, ArrayList<InvariantDeviceProfile> points) { 265 266 // Sort the profiles by their closeness to the dimensions 267 ArrayList<InvariantDeviceProfile> pointsByNearness = points; 268 Collections.sort(pointsByNearness, new Comparator<InvariantDeviceProfile>() { 269 public int compare(InvariantDeviceProfile a, InvariantDeviceProfile b) { 270 return Float.compare(dist(width, height, a.minWidthDps, a.minHeightDps), 271 dist(width, height, b.minWidthDps, b.minHeightDps)); 272 } 273 }); 274 275 return pointsByNearness; 276 } 277 278 // Package private visibility for testing. invDistWeightedInterpolate(float width, float height, ArrayList<InvariantDeviceProfile> points)279 InvariantDeviceProfile invDistWeightedInterpolate(float width, float height, 280 ArrayList<InvariantDeviceProfile> points) { 281 float weights = 0; 282 283 InvariantDeviceProfile p = points.get(0); 284 if (dist(width, height, p.minWidthDps, p.minHeightDps) == 0) { 285 return p; 286 } 287 288 InvariantDeviceProfile out = new InvariantDeviceProfile(); 289 for (int i = 0; i < points.size() && i < KNEARESTNEIGHBOR; ++i) { 290 p = new InvariantDeviceProfile(points.get(i)); 291 float w = weight(width, height, p.minWidthDps, p.minHeightDps, WEIGHT_POWER); 292 weights += w; 293 out.add(p.multiply(w)); 294 } 295 return out.multiply(1.0f/weights); 296 } 297 add(InvariantDeviceProfile p)298 private void add(InvariantDeviceProfile p) { 299 iconSize += p.iconSize; 300 landscapeIconSize += p.landscapeIconSize; 301 iconTextSize += p.iconTextSize; 302 } 303 multiply(float w)304 private InvariantDeviceProfile multiply(float w) { 305 iconSize *= w; 306 landscapeIconSize *= w; 307 iconTextSize *= w; 308 return this; 309 } 310 getAllAppsButtonRank()311 public int getAllAppsButtonRank() { 312 if (FeatureFlags.IS_DOGFOOD_BUILD && FeatureFlags.NO_ALL_APPS_ICON) { 313 throw new IllegalAccessError("Accessing all apps rank when all-apps is disabled"); 314 } 315 return numHotseatIcons / 2; 316 } 317 isAllAppsButtonRank(int rank)318 public boolean isAllAppsButtonRank(int rank) { 319 return rank == getAllAppsButtonRank(); 320 } 321 getDeviceProfile(Context context)322 public DeviceProfile getDeviceProfile(Context context) { 323 return context.getResources().getConfiguration().orientation 324 == Configuration.ORIENTATION_LANDSCAPE ? landscapeProfile : portraitProfile; 325 } 326 weight(float x0, float y0, float x1, float y1, float pow)327 private float weight(float x0, float y0, float x1, float y1, float pow) { 328 float d = dist(x0, y0, x1, y1); 329 if (Float.compare(d, 0f) == 0) { 330 return Float.POSITIVE_INFINITY; 331 } 332 return (float) (WEIGHT_EFFICIENT / Math.pow(d, pow)); 333 } 334 335 /** 336 * As a ratio of screen height, the total distance we want the parallax effect to span 337 * horizontally 338 */ wallpaperTravelToScreenWidthRatio(int width, int height)339 private static float wallpaperTravelToScreenWidthRatio(int width, int height) { 340 float aspectRatio = width / (float) height; 341 342 // At an aspect ratio of 16/10, the wallpaper parallax effect should span 1.5 * screen width 343 // At an aspect ratio of 10/16, the wallpaper parallax effect should span 1.2 * screen width 344 // We will use these two data points to extrapolate how much the wallpaper parallax effect 345 // to span (ie travel) at any aspect ratio: 346 347 final float ASPECT_RATIO_LANDSCAPE = 16/10f; 348 final float ASPECT_RATIO_PORTRAIT = 10/16f; 349 final float WALLPAPER_WIDTH_TO_SCREEN_RATIO_LANDSCAPE = 1.5f; 350 final float WALLPAPER_WIDTH_TO_SCREEN_RATIO_PORTRAIT = 1.2f; 351 352 // To find out the desired width at different aspect ratios, we use the following two 353 // formulas, where the coefficient on x is the aspect ratio (width/height): 354 // (16/10)x + y = 1.5 355 // (10/16)x + y = 1.2 356 // We solve for x and y and end up with a final formula: 357 final float x = 358 (WALLPAPER_WIDTH_TO_SCREEN_RATIO_LANDSCAPE - WALLPAPER_WIDTH_TO_SCREEN_RATIO_PORTRAIT) / 359 (ASPECT_RATIO_LANDSCAPE - ASPECT_RATIO_PORTRAIT); 360 final float y = WALLPAPER_WIDTH_TO_SCREEN_RATIO_PORTRAIT - x * ASPECT_RATIO_PORTRAIT; 361 return x * aspectRatio + y; 362 } 363 364 }