Home | History | Annotate | Download | only in launcher3
      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
     93     public InvariantDeviceProfile() {
     94     }
     95 
     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 
    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)
    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 
    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 
    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      */
    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 
    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.
    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.
    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 
    298     private void add(InvariantDeviceProfile p) {
    299         iconSize += p.iconSize;
    300         landscapeIconSize += p.landscapeIconSize;
    301         iconTextSize += p.iconTextSize;
    302     }
    303 
    304     private InvariantDeviceProfile multiply(float w) {
    305         iconSize *= w;
    306         landscapeIconSize *= w;
    307         iconTextSize *= w;
    308         return this;
    309     }
    310 
    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 
    318     public boolean isAllAppsButtonRank(int rank) {
    319         return rank == getAllAppsButtonRank();
    320     }
    321 
    322     public DeviceProfile getDeviceProfile(Context context) {
    323         return context.getResources().getConfiguration().orientation
    324                 == Configuration.ORIENTATION_LANDSCAPE ? landscapeProfile : portraitProfile;
    325     }
    326 
    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      */
    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 }