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.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 
     97     public InvariantDeviceProfile() {
     98     }
     99 
    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 
    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)
    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 
    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 
    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      */
    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 
    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.
    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.
    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 
    306     private void add(InvariantDeviceProfile p) {
    307         iconSize += p.iconSize;
    308         iconTextSize += p.iconTextSize;
    309         hotseatIconSize += p.hotseatIconSize;
    310     }
    311 
    312     private InvariantDeviceProfile multiply(float w) {
    313         iconSize *= w;
    314         iconTextSize *= w;
    315         hotseatIconSize *= w;
    316         return this;
    317     }
    318 
    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 
    326     public boolean isAllAppsButtonRank(int rank) {
    327         return rank == getAllAppsButtonRank();
    328     }
    329 
    330     public DeviceProfile getDeviceProfile(Context context) {
    331         return context.getResources().getConfiguration().orientation
    332                 == Configuration.ORIENTATION_LANDSCAPE ? landscapeProfile : portraitProfile;
    333     }
    334 
    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      */
    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 }