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

Commit 35c3c7f5 authored by Hyunyoung Song's avatar Hyunyoung Song
Browse files

Start adding unit tests for the invariant device profile / Refactor

- removed redundant code to sort the device profiles
- removed DeviceProfileQuery class
- Added a helper method inside the test to easily generate
interpolation graph looks like:
https://docs.google.com/a/google.com/spreadsheets/d/1a1fdemrOqIDixiql77h0anWzUD3GlYfGsbP2FfIhyPM/edit?usp=sharing

Change-Id: Ia4c54a8d59a049c418c08d1b766f07ac6e1d0944
parent d0ad0711
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.
}