Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 8522c80b authored by Hyunyoung Song's avatar Hyunyoung Song Committed by Android (Google) Code Review
Browse files

Merge "Start adding unit tests for the invariant device profile / Refactor"...

Merge "Start adding unit tests for the invariant device profile / Refactor" into ub-launcher3-burnaby
parents 80aa3ec4 35c3c7f5
Loading
Loading
Loading
Loading
+119 −129
Original line number Diff line number Diff line
@@ -19,12 +19,13 @@ package com.android.launcher3;
import android.annotation.TargetApi;
import android.content.Context;
import android.graphics.Point;
import android.graphics.PointF;
import android.os.Build;
import android.util.DisplayMetrics;
import android.view.Display;
import android.view.WindowManager;

import com.android.launcher3.util.Thunk;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
@@ -34,61 +35,36 @@ public class InvariantDeviceProfile {
    // This is a static that we use for the default icon size on a 4/5-inch phone
    private static float DEFAULT_ICON_SIZE_DP = 60;

    private static final ArrayList<InvariantDeviceProfile> sDeviceProfiles = new ArrayList<>();
    static {
        sDeviceProfiles.add(new InvariantDeviceProfile("Super Short Stubby",
                255, 300,  2, 3, 2, 3, 48, 13, 3, 48, R.xml.default_workspace_4x4));
        sDeviceProfiles.add(new InvariantDeviceProfile("Shorter Stubby",
                255, 400,  3, 3, 3, 3, 48, 13, 3, 48, R.xml.default_workspace_4x4));
        sDeviceProfiles.add(new InvariantDeviceProfile("Short Stubby",
                275, 420,  3, 4, 3, 4, 48, 13, 5, 48, R.xml.default_workspace_4x4));
        sDeviceProfiles.add(new InvariantDeviceProfile("Stubby",
                255, 450,  3, 4, 3, 4, 48, 13, 5, 48, R.xml.default_workspace_4x4));
        sDeviceProfiles.add(new InvariantDeviceProfile("Nexus S",
                296, 491.33f,  4, 4, 4, 4, 48, 13, 5, 48, R.xml.default_workspace_4x4));
        sDeviceProfiles.add(new InvariantDeviceProfile("Nexus 4",
                335, 567,  4, 4, 4, 4, DEFAULT_ICON_SIZE_DP, 13, 5, 56, R.xml.default_workspace_4x4));
        sDeviceProfiles.add(new InvariantDeviceProfile("Nexus 5",
                359, 567,  4, 4, 4, 4, DEFAULT_ICON_SIZE_DP, 13, 5, 56, R.xml.default_workspace_4x4));
        sDeviceProfiles.add(new InvariantDeviceProfile("Large Phone",
                406, 694,  5, 5, 4, 4,  64, 14.4f,  5, 56, R.xml.default_workspace_5x5));
        // The tablet profile is odd in that the landscape orientation
        // also includes the nav bar on the side
        sDeviceProfiles.add(new InvariantDeviceProfile("Nexus 7",
                575, 904,  5, 6, 4, 5, 72, 14.4f,  7, 60, R.xml.default_workspace_5x6));
        // Larger tablet profiles always have system bars on the top & bottom
        sDeviceProfiles.add(new InvariantDeviceProfile("Nexus 10",
                727, 1207,  5, 6, 4, 5, 76, 14.4f,  7, 64, R.xml.default_workspace_5x6));
        sDeviceProfiles.add(new InvariantDeviceProfile("20-inch Tablet",
                1527, 2527,  7, 7, 6, 6, 100, 20,  7, 72, R.xml.default_workspace_4x4));
    }
    // Constants that affects the interpolation curve between statically defined device profile
    // buckets.
    private static float KNEARESTNEIGHBOR = 3;
    private static float WEIGHT_POWER = 5;

    private class DeviceProfileQuery {
        InvariantDeviceProfile profile;
        float widthDps;
        float heightDps;
        float value;
        PointF dimens;

        DeviceProfileQuery(InvariantDeviceProfile p, float v) {
            widthDps = p.minWidthDps;
            heightDps = p.minHeightDps;
            value = v;
            dimens = new PointF(widthDps, heightDps);
            profile = p;
        }
    }
    // used to offset float not being able to express extremely small weights in extreme cases.
    private static float WEIGHT_EFFICIENT = 100000f;

    // Profile-defining invariant properties
    String name;
    float minWidthDps;
    float minHeightDps;

    /**
     * Number of icons per row and column in the workspace.
     */
    public int numRows;
    public int numColumns;

    /**
     * Number of icons per row and column in the folder.
     */
    public int numFolderRows;
    public int numFolderColumns;
    float iconSize;
    float iconTextSize;

    /**
     * Number of icons inside the hotseat area.
     */
    float numHotseatIcons;
    float hotseatIconSize;
    int defaultLayoutId;
@@ -102,6 +78,12 @@ public class InvariantDeviceProfile {
    InvariantDeviceProfile() {
    }

    public InvariantDeviceProfile(InvariantDeviceProfile p) {
        this(p.name, p.minWidthDps, p.minHeightDps, p.numRows, p.numColumns,
                p.numFolderRows, p.numFolderColumns, p.iconSize, p.iconTextSize, p.numHotseatIcons,
                p.hotseatIconSize, p.defaultLayoutId);
    }

    InvariantDeviceProfile(String n, float w, float h, int r, int c, int fr, int fc,
            float is, float its, float hs, float his, int dlId) {
        // Ensure that we have an odd number of hotseat items (since we need to place all apps)
@@ -134,21 +116,16 @@ public class InvariantDeviceProfile {
        Point largestSize = new Point();
        display.getCurrentSizeRange(smallestSize, largestSize);

        // This guarantees that width < height
        minWidthDps = Utilities.dpiFromPx(Math.min(smallestSize.x, smallestSize.y), dm);
        minHeightDps = Utilities.dpiFromPx(Math.min(largestSize.x, largestSize.y), dm);

        ArrayList<DeviceProfileQuery> points =
                new ArrayList<DeviceProfileQuery>();
        ArrayList<InvariantDeviceProfile> closestProfiles =
                findClosestDeviceProfiles(minWidthDps, minHeightDps, getPredefinedDeviceProfiles());
        InvariantDeviceProfile interpolatedDeviceProfileOut =
                invDistWeightedInterpolate(minWidthDps,  minHeightDps, closestProfiles);

        // Find the closes profile given the width/height
        for (InvariantDeviceProfile p : sDeviceProfiles) {
            points.add(new DeviceProfileQuery(p, 0f));
        }

        InvariantDeviceProfile closestProfile =
                findClosestDeviceProfile(minWidthDps, minHeightDps, points);

        // The following properties are inherited directly from the nearest archetypal profile
        InvariantDeviceProfile closestProfile = closestProfiles.get(0);
        numRows = closestProfile.numRows;
        numColumns = closestProfile.numColumns;
        numHotseatIcons = closestProfile.numHotseatIcons;
@@ -157,24 +134,9 @@ public class InvariantDeviceProfile {
        numFolderRows = closestProfile.numFolderRows;
        numFolderColumns = closestProfile.numFolderColumns;


        // The following properties are interpolated based on proximity to nearby archetypal
        // profiles
        points.clear();
        for (InvariantDeviceProfile p : sDeviceProfiles) {
            points.add(new DeviceProfileQuery(p, p.iconSize));
        }
        iconSize = invDistWeightedInterpolate(minWidthDps, minHeightDps, points);
        points.clear();
        for (InvariantDeviceProfile p : sDeviceProfiles) {
            points.add(new DeviceProfileQuery(p, p.iconTextSize));
        }
        iconTextSize = invDistWeightedInterpolate(minWidthDps, minHeightDps, points);
        points.clear();
        for (InvariantDeviceProfile p : sDeviceProfiles) {
            points.add(new DeviceProfileQuery(p, p.hotseatIconSize));
        }
        hotseatIconSize = invDistWeightedInterpolate(minWidthDps, minHeightDps, points);
        iconSize = interpolatedDeviceProfileOut.iconSize;
        iconTextSize = interpolatedDeviceProfileOut.iconTextSize;
        hotseatIconSize = interpolatedDeviceProfileOut.hotseatIconSize;

        // If the partner customization apk contains any grid overrides, apply them
        // Supported overrides: numRows, numColumns, iconSize
@@ -182,7 +144,7 @@ public class InvariantDeviceProfile {

        Point realSize = new Point();
        display.getRealSize(realSize);
        // The real size never changes. smallSide and largeSize will remain the
        // The real size never changes. smallSide and largeSide will remain the
        // same in any orientation.
        int smallSide = Math.min(realSize.x, realSize.y);
        int largeSide = Math.max(realSize.x, realSize.y);
@@ -193,84 +155,112 @@ public class InvariantDeviceProfile {
                smallSide, largeSide, false /* isLandscape */);
    }

    ArrayList<InvariantDeviceProfile> getPredefinedDeviceProfiles() {
        ArrayList<InvariantDeviceProfile> predefinedDeviceProfiles = new ArrayList<>();
        // width, height, #rows, #columns, #folder rows, #folder columns,
        // iconSize, iconTextSize, #hotseat, #hotseatIconSize, defaultLayoutId.
        predefinedDeviceProfiles.add(new InvariantDeviceProfile("Super Short Stubby",
                255, 300,     2, 3, 2, 3, 48, 13, 3, 48, R.xml.default_workspace_4x4));
        predefinedDeviceProfiles.add(new InvariantDeviceProfile("Shorter Stubby",
                255, 400,     3, 3, 3, 3, 48, 13, 3, 48, R.xml.default_workspace_4x4));
        predefinedDeviceProfiles.add(new InvariantDeviceProfile("Short Stubby",
                275, 420,     3, 4, 3, 4, 48, 13, 5, 48, R.xml.default_workspace_4x4));
        predefinedDeviceProfiles.add(new InvariantDeviceProfile("Stubby",
                255, 450,     3, 4, 3, 4, 48, 13, 5, 48, R.xml.default_workspace_4x4));
        predefinedDeviceProfiles.add(new InvariantDeviceProfile("Nexus S",
                296, 491.33f, 4, 4, 4, 4, 48, 13, 5, 48, R.xml.default_workspace_4x4));
        predefinedDeviceProfiles.add(new InvariantDeviceProfile("Nexus 4",
                335, 567,     4, 4, 4, 4, DEFAULT_ICON_SIZE_DP, 13, 5, 56, R.xml.default_workspace_4x4));
        predefinedDeviceProfiles.add(new InvariantDeviceProfile("Nexus 5",
                359, 567,     4, 4, 4, 4, DEFAULT_ICON_SIZE_DP, 13, 5, 56, R.xml.default_workspace_4x4));
        predefinedDeviceProfiles.add(new InvariantDeviceProfile("Large Phone",
                406, 694,     5, 5, 4, 4,  64, 14.4f,  5, 56, R.xml.default_workspace_5x5));
        // The tablet profile is odd in that the landscape orientation
        // also includes the nav bar on the side
        predefinedDeviceProfiles.add(new InvariantDeviceProfile("Nexus 7",
                575, 904,     5, 6, 4, 5, 72, 14.4f,  7, 60, R.xml.default_workspace_5x6));
        // Larger tablet profiles always have system bars on the top & bottom
        predefinedDeviceProfiles.add(new InvariantDeviceProfile("Nexus 10",
                727, 1207,    5, 6, 4, 5, 76, 14.4f,  7, 64, R.xml.default_workspace_5x6));
        predefinedDeviceProfiles.add(new InvariantDeviceProfile("20-inch Tablet",
                1527, 2527,   7, 7, 6, 6, 100, 20,  7, 72, R.xml.default_workspace_4x4));
        return predefinedDeviceProfiles;
    }


    /**
     * Apply any Partner customization grid overrides.
     *
     * Currently we support: all apps row / column count.
     */
    private void applyPartnerDeviceProfileOverrides(Context ctx, DisplayMetrics dm) {
        Partner p = Partner.get(ctx.getPackageManager());
    private void applyPartnerDeviceProfileOverrides(Context context, DisplayMetrics dm) {
        Partner p = Partner.get(context.getPackageManager());
        if (p != null) {
            p.applyInvariantDeviceProfileOverrides(this, dm);
        }
    }

    @Thunk float dist(PointF p0, PointF p1) {
        return (float) Math.sqrt((p1.x - p0.x)*(p1.x-p0.x) +
                (p1.y-p0.y)*(p1.y-p0.y));
    }

    private float weight(PointF a, PointF b,
                        float pow) {
        float d = dist(a, b);
        if (d == 0f) {
            return Float.POSITIVE_INFINITY;
        }
        return (float) (1f / Math.pow(d, pow));
    }

    /** Returns the closest device profile given the width and height and a list of profiles */
    private InvariantDeviceProfile findClosestDeviceProfile(float width, float height,
                                                   ArrayList<DeviceProfileQuery> points) {
        return findClosestDeviceProfiles(width, height, points).get(0).profile;
    @Thunk float dist(float x0, float y0, float x1, float y1) {
        return (float) Math.hypot(x1 - x0, y1 - y0);
    }

    /** Returns the closest device profiles ordered by closeness to the specified width and height */
    private ArrayList<DeviceProfileQuery> findClosestDeviceProfiles(float width, float height,
                                                   ArrayList<DeviceProfileQuery> points) {
        final PointF xy = new PointF(width, height);
    /**
     * Returns the closest device profiles ordered by closeness to the specified width and height
     */
    // Package private visibility for testing.
    ArrayList<InvariantDeviceProfile> findClosestDeviceProfiles(
            final float width, final float height, ArrayList<InvariantDeviceProfile> points) {

        // Sort the profiles by their closeness to the dimensions
        ArrayList<DeviceProfileQuery> pointsByNearness = points;
        Collections.sort(pointsByNearness, new Comparator<DeviceProfileQuery>() {
            public int compare(DeviceProfileQuery a, DeviceProfileQuery b) {
                return (int) (dist(xy, a.dimens) - dist(xy, b.dimens));
        ArrayList<InvariantDeviceProfile> pointsByNearness = points;
        Collections.sort(pointsByNearness, new Comparator<InvariantDeviceProfile>() {
            public int compare(InvariantDeviceProfile a, InvariantDeviceProfile b) {
                return (int) (dist(width, height, a.minWidthDps, a.minHeightDps)
                        - dist(width, height, b.minWidthDps, b.minHeightDps));
            }
        });

        return pointsByNearness;
    }

    private float invDistWeightedInterpolate(float width, float height,
                ArrayList<DeviceProfileQuery> points) {
        float sum = 0;
    // Package private visibility for testing.
    InvariantDeviceProfile invDistWeightedInterpolate(float width, float height,
                ArrayList<InvariantDeviceProfile> points) {
        float weights = 0;
        float pow = 5;
        float kNearestNeighbors = 3;
        final PointF xy = new PointF(width, height);

        ArrayList<DeviceProfileQuery> pointsByNearness = findClosestDeviceProfiles(width, height,
                points);

        for (int i = 0; i < pointsByNearness.size(); ++i) {
            DeviceProfileQuery p = pointsByNearness.get(i);
            if (i < kNearestNeighbors) {
                float w = weight(xy, p.dimens, pow);
                if (w == Float.POSITIVE_INFINITY) {
                    return p.value;

        InvariantDeviceProfile p = points.get(0);
        if (dist(width, height, p.minWidthDps, p.minHeightDps) == 0) {
            return p;
        }

        InvariantDeviceProfile out = new InvariantDeviceProfile();
        for (int i = 0; i < points.size() && i < KNEARESTNEIGHBOR; ++i) {
            p = new InvariantDeviceProfile(points.get(i));
            float w = weight(width, height, p.minWidthDps, p.minHeightDps, WEIGHT_POWER);
            weights += w;
            out.add(p.multiply(w));
        }
        return out.multiply(1.0f/weights);
    }

        for (int i = 0; i < pointsByNearness.size(); ++i) {
            DeviceProfileQuery p = pointsByNearness.get(i);
            if (i < kNearestNeighbors) {
                float w = weight(xy, p.dimens, pow);
                sum += w * p.value / weights;
    private void add(InvariantDeviceProfile p) {
        iconSize += p.iconSize;
        iconTextSize += p.iconTextSize;
        hotseatIconSize += p.hotseatIconSize;
    }

    private InvariantDeviceProfile multiply(float w) {
        iconSize *= w;
        iconTextSize *= w;
        hotseatIconSize *= w;
        return this;
    }

        return sum;
    private float weight(float x0, float y0, float x1, float y1, float pow) {
        float d = dist(x0, y0, x1, y1);
        if (Float.compare(d, 0f) == 0) {
            return Float.POSITIVE_INFINITY;
        }
        return (float) (WEIGHT_EFFICIENT / Math.pow(d, pow));
    }
}
 No newline at end of file
+123 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2015 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */
package com.android.launcher3;

import android.graphics.PointF;
import android.test.AndroidTestCase;
import android.test.suitebuilder.annotation.SmallTest;
import android.util.Log;

import com.android.launcher3.DeviceProfile;
import com.android.launcher3.InvariantDeviceProfile;
import com.android.launcher3.util.FocusLogic;

import java.util.ArrayList;

/**
 * Tests the {@link DeviceProfile} and {@link InvariantDeviceProfile}.
 */
@SmallTest
public class InvariantDeviceProfileTest extends AndroidTestCase {

    private static final String TAG = "DeviceProfileTest";
    private static final boolean DEBUG = false;

    private InvariantDeviceProfile mInvariantProfile;
    private ArrayList<InvariantDeviceProfile> mPredefinedDeviceProfiles;

    @Override
    protected void setUp() throws Exception {
        super.setUp();
        mInvariantProfile = new InvariantDeviceProfile(getContext());
        mPredefinedDeviceProfiles = mInvariantProfile.getPredefinedDeviceProfiles();
    }

    @Override
    protected void tearDown() throws Exception {
        // Nothing to tear down as this class only tests static methods.
    }

    public void testFindClosestDeviceProfile2() {
        for (InvariantDeviceProfile idf: mPredefinedDeviceProfiles) {
            ArrayList<InvariantDeviceProfile> closestProfiles =
                    mInvariantProfile.findClosestDeviceProfiles(
                            idf.minWidthDps, idf.minHeightDps, mPredefinedDeviceProfiles);
            assertTrue(closestProfiles.get(0).equals(idf));
        }
    }

    /**
     * Used to print out how the invDistWeightedInterpolate works between device profiles to
     * tweak the two constants that control how the interpolation curve is shaped.
     */
    public void testInvInterpolation() {

        InvariantDeviceProfile p1 = mPredefinedDeviceProfiles.get(7); // e.g., Large Phone
        InvariantDeviceProfile p2 = mPredefinedDeviceProfiles.get(8); // e.g., Nexus 7

        ArrayList<PointF> pts = createInterpolatedPoints(
                new PointF(p1.minWidthDps, p1.minHeightDps),
                new PointF(p2.minWidthDps, p2.minHeightDps),
                20f);

        for (int i = 0; i < pts.size(); i++) {
            ArrayList<InvariantDeviceProfile> closestProfiles =
                    mInvariantProfile.findClosestDeviceProfiles(
                            pts.get(i).x, pts.get(i).y, mPredefinedDeviceProfiles);
            InvariantDeviceProfile result =
                    mInvariantProfile.invDistWeightedInterpolate(
                            pts.get(i).x, pts.get(i).y, closestProfiles);
            if (DEBUG) {
                Log.d(TAG, String.format("width x height = (%f, %f)] iconSize = %f",
                        pts.get(i).x, pts.get(i).y, result.iconSize));
            }
        }
    }

    private ArrayList<PointF> createInterpolatedPoints(PointF a, PointF b, float numPts) {
        ArrayList<PointF> result = new ArrayList<PointF>();
        result.add(a);
        for (float i = 1; i < numPts; i = i + 1.0f) {
            result.add(new PointF((b.x * i +  a.x * (numPts - i)) / numPts,
                    (b.y * i + a.y * (numPts - i)) / numPts));
        }
        result.add(b);
        return result;
    }

    /**
     * Ensures that system calls (e.g., WindowManager, DisplayMetrics) that require contexts are
     * properly working to generate minimum width and height of the display.
     */
    public void test_hammerhead() {
        if (!android.os.Build.DEVICE.equals("hammerhead")) {
            return;
        }
        assertEquals(mInvariantProfile.numRows, 4);
        assertEquals(mInvariantProfile.numColumns, 4);
        assertEquals((int) mInvariantProfile.numHotseatIcons, 5);

        DeviceProfile landscapeProfile = mInvariantProfile.landscapeProfile;
        DeviceProfile portraitProfile = mInvariantProfile.portraitProfile;

        assertEquals(portraitProfile.allAppsNumCols, 3);
        assertEquals(landscapeProfile.allAppsNumCols, 5); // not used
    }

    // Add more tests for other devices, however, running them once on a single device is enough
    // for verifying that for a platform version, the WindowManager and DisplayMetrics is
    // working as intended.
}