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