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