diff --git a/src/com/android/launcher3/celllayout/MulticellReorderAlgorithm.java b/src/com/android/launcher3/celllayout/MulticellReorderAlgorithm.java index b7d8093055..7deb65315a 100644 --- a/src/com/android/launcher3/celllayout/MulticellReorderAlgorithm.java +++ b/src/com/android/launcher3/celllayout/MulticellReorderAlgorithm.java @@ -38,8 +38,8 @@ public class MulticellReorderAlgorithm extends ReorderAlgorithm { mSeam = new View(cellLayout.getContext()); } - private ItemConfiguration removeSeamFromSolution( - ItemConfiguration solution) { + public ItemConfiguration removeSeamFromSolution(ItemConfiguration solution) { + solution.map.remove(mSeam); solution.map.forEach((view, cell) -> cell.cellX = cell.cellX > mCellLayout.getCountX() / 2 ? cell.cellX - 1 : cell.cellX); solution.cellX = @@ -48,9 +48,8 @@ public class MulticellReorderAlgorithm extends ReorderAlgorithm { } @Override - public ItemConfiguration closestEmptySpaceReorder(int pixelX, int pixelY, - int minSpanX, int minSpanY, - int spanX, int spanY) { + public ItemConfiguration closestEmptySpaceReorder(int pixelX, int pixelY, int minSpanX, + int minSpanY, int spanX, int spanY) { return removeSeamFromSolution(simulateSeam( () -> super.closestEmptySpaceReorder(pixelX, pixelY, minSpanX, minSpanY, spanX, spanY))); diff --git a/src/com/android/launcher3/celllayout/ReorderAlgorithm.java b/src/com/android/launcher3/celllayout/ReorderAlgorithm.java index 05bd13da81..0f6464b100 100644 --- a/src/com/android/launcher3/celllayout/ReorderAlgorithm.java +++ b/src/com/android/launcher3/celllayout/ReorderAlgorithm.java @@ -62,6 +62,14 @@ public class ReorderAlgorithm { public ItemConfiguration findReorderSolution(int pixelX, int pixelY, int minSpanX, int minSpanY, int spanX, int spanY, int[] direction, View dragView, boolean decX, ItemConfiguration solution) { + return findReorderSolutionRecursive(pixelX, pixelY, minSpanX, minSpanY, spanX, spanY, + direction, dragView, decX, solution); + } + + + private ItemConfiguration findReorderSolutionRecursive(int pixelX, int pixelY, + int minSpanX, int minSpanY, int spanX, int spanY, int[] direction, View dragView, + boolean decX, ItemConfiguration solution) { // Copy the current state into the solution. This solution will be manipulated as necessary. mCellLayout.copyCurrentStateToSolution(solution, false); // Copy the current occupied array into the temporary occupied array. This array will be @@ -83,11 +91,11 @@ public class ReorderAlgorithm { // We try shrinking the widget down to size in an alternating pattern, shrink 1 in // x, then 1 in y etc. if (spanX > minSpanX && (minSpanY == spanY || decX)) { - return findReorderSolution(pixelX, pixelY, minSpanX, minSpanY, spanX - 1, spanY, - direction, dragView, false, solution); + return findReorderSolutionRecursive(pixelX, pixelY, minSpanX, minSpanY, spanX - 1, + spanY, direction, dragView, false, solution); } else if (spanY > minSpanY) { - return findReorderSolution(pixelX, pixelY, minSpanX, minSpanY, spanX, spanY - 1, - direction, dragView, true, solution); + return findReorderSolutionRecursive(pixelX, pixelY, minSpanX, minSpanY, spanX, + spanY - 1, direction, dragView, true, solution); } solution.isSolution = false; } else { @@ -193,8 +201,7 @@ public class ReorderAlgorithm { mCellLayout.getDirectionVectorForDrop(pixelX, pixelY, spanX, spanY, dragView, mCellLayout.mDirectionVector); - ItemConfiguration dropInPlaceSolution = dropInPlaceSolution(pixelX, pixelY, - spanX, spanY, + ItemConfiguration dropInPlaceSolution = dropInPlaceSolution(pixelX, pixelY, spanX, spanY, dragView); // Find a solution involving pushing / displacing any items in the way @@ -203,8 +210,8 @@ public class ReorderAlgorithm { new ItemConfiguration()); // We attempt the approach which doesn't shuffle views at all - ItemConfiguration closestSpaceSolution = closestEmptySpaceReorder( - pixelX, pixelY, minSpanX, minSpanY, spanX, spanY); + ItemConfiguration closestSpaceSolution = closestEmptySpaceReorder(pixelX, pixelY, minSpanX, + minSpanY, spanX, spanY); // If the reorder solution requires resizing (shrinking) the item being dropped, we instead // favor a solution in which the item is not resized, but diff --git a/tests/src/com/android/launcher3/celllayout/CellLayoutTestUtils.java b/tests/src/com/android/launcher3/celllayout/CellLayoutTestUtils.java index 1fe02b2058..d8ae74bf3c 100644 --- a/tests/src/com/android/launcher3/celllayout/CellLayoutTestUtils.java +++ b/tests/src/com/android/launcher3/celllayout/CellLayoutTestUtils.java @@ -61,9 +61,7 @@ public class CellLayoutTestUtils { } public static CellLayoutBoard viewsToBoard(List views, int width, int height) { - CellLayoutBoard board = new CellLayoutBoard(); - board.mWidth = width; - board.mHeight = height; + CellLayoutBoard board = new CellLayoutBoard(width, height); for (View callView : views) { CellLayoutLayoutParams params = (CellLayoutLayoutParams) callView.getLayoutParams(); diff --git a/tests/src/com/android/launcher3/celllayout/ReorderAlgorithmUnitTest.java b/tests/src/com/android/launcher3/celllayout/ReorderAlgorithmUnitTest.java index 88f54a2b43..bd52bda6b4 100644 --- a/tests/src/com/android/launcher3/celllayout/ReorderAlgorithmUnitTest.java +++ b/tests/src/com/android/launcher3/celllayout/ReorderAlgorithmUnitTest.java @@ -22,7 +22,7 @@ import static org.junit.Assert.assertTrue; import android.content.Context; import android.graphics.Point; -import android.graphics.Rect; +import android.util.Log; import android.view.View; import androidx.test.ext.junit.runners.AndroidJUnit4; @@ -31,9 +31,13 @@ import androidx.test.filters.SmallTest; import com.android.launcher3.CellLayout; import com.android.launcher3.DeviceProfile; import com.android.launcher3.InvariantDeviceProfile; +import com.android.launcher3.MultipageCellLayout; import com.android.launcher3.celllayout.board.CellLayoutBoard; import com.android.launcher3.celllayout.board.IconPoint; +import com.android.launcher3.celllayout.board.PermutedBoardComparator; import com.android.launcher3.celllayout.board.WidgetRect; +import com.android.launcher3.celllayout.testgenerator.RandomBoardGenerator; +import com.android.launcher3.celllayout.testgenerator.RandomMultiBoardGenerator; import com.android.launcher3.util.ActivityContextWrapper; import com.android.launcher3.views.DoubleShadowBubbleTextView; @@ -53,10 +57,25 @@ import java.util.Random; @SmallTest @RunWith(AndroidJUnit4.class) public class ReorderAlgorithmUnitTest { + + private static final String TAG = "ReorderAlgorithmUnitTest"; + private static final char MAIN_WIDGET_TYPE = 'z'; + + // There is nothing special about this numbers, the random seed is just to be able to reproduce + // the test cases and the height and width is a random number similar to what users expect on + // their devices + private static final int SEED = 897; + private static final int MAX_BOARD_SIZE = 13; + + private static final int TOTAL_OF_CASES_GENERATED = 300; private Context mApplicationContext; private int mPrevNumColumns, mPrevNumRows; + /** + * This test reads existing test cases and makes sure the CellLayout produces the same + * output for each of them for a given input. + */ @Test public void testAllCases() throws IOException { List testCases = getTestCases( @@ -65,7 +84,7 @@ public class ReorderAlgorithmUnitTest { List failingCases = new ArrayList<>(); for (int i = 0; i < testCases.size(); i++) { try { - evaluateTestCase(testCases.get(i)); + evaluateTestCase(testCases.get(i), false); } catch (AssertionError e) { e.printStackTrace(); failingCases.add(i); @@ -75,6 +94,47 @@ public class ReorderAlgorithmUnitTest { failingCases.size()); } + /** + * This test generates random CellLayout configurations and then try to reorder it and makes + * sure the result is a valid board meaning it didn't remove any widget or icon. + */ + @Test + public void generateValidTests() { + Random generator = new Random(SEED); + mApplicationContext = new ActivityContextWrapper(getApplicationContext()); + for (int i = 0; i < TOTAL_OF_CASES_GENERATED; i++) { + // Using a new seed so that we can replicate the same test cases. + int seed = generator.nextInt(); + Log.d(TAG, "Seed = " + seed); + ReorderAlgorithmUnitTestCase testCase = generateRandomTestCase( + new RandomBoardGenerator(new Random(seed)) + ); + Log.d(TAG, "testCase = " + testCase); + assertTrue("invalid case " + i, + validateIntegrity(testCase.startBoard, testCase.endBoard, testCase)); + } + } + + /** + * Same as above but testing the Multipage CellLayout. + */ + @Test + public void generateValidTests_Multi() { + Random generator = new Random(SEED); + mApplicationContext = new ActivityContextWrapper(getApplicationContext()); + for (int i = 0; i < TOTAL_OF_CASES_GENERATED; i++) { + // Using a new seed so that we can replicate the same test cases. + int seed = generator.nextInt(); + Log.d(TAG, "Seed = " + seed); + ReorderAlgorithmUnitTestCase testCase = generateRandomTestCase( + new RandomMultiBoardGenerator(new Random(seed)) + ); + Log.d(TAG, "testCase = " + testCase); + assertTrue("invalid case " + i, + validateIntegrity(testCase.startBoard, testCase.endBoard, testCase)); + } + } + private void addViewInCellLayout(CellLayout cellLayout, int cellX, int cellY, int spanX, int spanY, boolean isWidget) { View cell = isWidget ? new View(mApplicationContext) : new DoubleShadowBubbleTextView( @@ -84,15 +144,16 @@ public class ReorderAlgorithmUnitTest { (CellLayoutLayoutParams) cell.getLayoutParams(), true); } - public CellLayout createCellLayout(int width, int height) { + public CellLayout createCellLayout(int width, int height, boolean isMulti) { Context c = mApplicationContext; DeviceProfile dp = InvariantDeviceProfile.INSTANCE.get(c).getDeviceProfile(c).copy(c); // modify the device profile. - dp.inv.numColumns = width; + dp.inv.numColumns = isMulti ? width / 2 : width; dp.inv.numRows = height; dp.cellLayoutBorderSpacePx = new Point(0, 0); - CellLayout cl = new CellLayout(getWrappedContext(c, dp)); + CellLayout cl = isMulti ? new MultipageCellLayout(getWrappedContext(c, dp)) + : new CellLayout(getWrappedContext(c, dp)); // I put a very large number for width and height so that all the items can fit, it doesn't // need to be exact, just bigger than the sum of cell border cl.measure(View.MeasureSpec.makeMeasureSpec(10000, View.MeasureSpec.EXACTLY), @@ -109,8 +170,8 @@ public class ReorderAlgorithmUnitTest { } public ItemConfiguration solve(CellLayoutBoard board, int x, int y, int spanX, - int spanY, int minSpanX, int minSpanY) { - CellLayout cl = createCellLayout(board.getWidth(), board.getHeight()); + int spanY, int minSpanX, int minSpanY, boolean isMulti) { + CellLayout cl = createCellLayout(board.getWidth(), board.getHeight(), isMulti); // The views have to be sorted or the result can vary board.getIcons() @@ -118,11 +179,15 @@ public class ReorderAlgorithmUnitTest { .map(IconPoint::getCoord) .sorted(Comparator.comparing(p -> ((Point) p).x).thenComparing(p -> ((Point) p).y)) .forEach(p -> addViewInCellLayout(cl, p.x, p.y, 1, 1, false)); - board.getWidgets().stream() - .sorted(Comparator.comparing(WidgetRect::getCellX) - .thenComparing(WidgetRect::getCellY)) - .forEach(widget -> addViewInCellLayout(cl, widget.getCellX(), widget.getCellY(), - widget.getSpanX(), widget.getSpanY(), true)); + board.getWidgets() + .stream() + .sorted(Comparator + .comparing(WidgetRect::getCellX) + .thenComparing(WidgetRect::getCellY) + ).forEach( + widget -> addViewInCellLayout(cl, widget.getCellX(), widget.getCellY(), + widget.getSpanX(), widget.getSpanY(), true) + ); int[] testCaseXYinPixels = new int[2]; cl.regionToCenterPoint(x, y, spanX, spanY, testCaseXYinPixels); @@ -133,6 +198,15 @@ public class ReorderAlgorithmUnitTest { solution = new ItemConfiguration(); solution.isSolution = false; } + if (!solution.isSolution) { + cl.copyCurrentStateToSolution(solution, false); + if (cl instanceof MultipageCellLayout) { + solution = + ((MultipageCellLayout) cl).createReorderAlgorithm().removeSeamFromSolution( + solution); + } + solution.isSolution = false; + } return solution; } @@ -143,17 +217,18 @@ public class ReorderAlgorithmUnitTest { new CellLayoutLayoutParams(val.cellX, val.cellY, val.spanX, val.spanY))); CellLayoutBoard board = CellLayoutTestUtils.viewsToBoard( new ArrayList<>(solution.map.keySet()), width, height); - board.addWidget(solution.cellX, solution.cellY, solution.spanX, solution.spanY, - 'z'); + if (solution.isSolution) { + board.addWidget(solution.cellX, solution.cellY, solution.spanX, solution.spanY, + MAIN_WIDGET_TYPE); + } return board; } - public void evaluateTestCase(ReorderAlgorithmUnitTestCase testCase) { - ItemConfiguration solution = solve(testCase.startBoard, testCase.x, - testCase.y, testCase.spanX, testCase.spanY, testCase.minSpanX, - testCase.minSpanY); - assertEquals("should be a valid solution", solution.isSolution, - testCase.isValidSolution); + public void evaluateTestCase(ReorderAlgorithmUnitTestCase testCase, boolean isMultiCellLayout) { + ItemConfiguration solution = solve(testCase.startBoard, testCase.x, testCase.y, + testCase.spanX, testCase.spanY, testCase.minSpanX, testCase.minSpanY, + isMultiCellLayout); + assertEquals("should be a valid solution", solution.isSolution, testCase.isValidSolution); if (testCase.isValidSolution) { CellLayoutBoard finishBoard = boardFromSolution(solution, testCase.startBoard.getWidth(), testCase.startBoard.getHeight()); @@ -178,36 +253,35 @@ public class ReorderAlgorithmUnitTest { dp.inv.numRows = mPrevNumRows; } - @SuppressWarnings("UnusedMethod") - /** - * Utility function used to generate all the test cases - */ - private ReorderAlgorithmUnitTestCase generateRandomTestCase() { + private ReorderAlgorithmUnitTestCase generateRandomTestCase( + RandomBoardGenerator boardGenerator) { ReorderAlgorithmUnitTestCase testCase = new ReorderAlgorithmUnitTestCase(); - int width = getRandom(3, 8); - int height = getRandom(3, 8); + boolean isMultiCellLayout = boardGenerator instanceof RandomMultiBoardGenerator; - int targetWidth = getRandom(1, width - 2); - int targetHeight = getRandom(1, height - 2); + int width = isMultiCellLayout + ? boardGenerator.getRandom(3, MAX_BOARD_SIZE / 2) * 2 + : boardGenerator.getRandom(3, MAX_BOARD_SIZE); + int height = boardGenerator.getRandom(3, MAX_BOARD_SIZE); - int minTargetWidth = getRandom(1, targetWidth); - int minTargetHeight = getRandom(1, targetHeight); + int targetWidth = boardGenerator.getRandom(1, width - 2); + int targetHeight = boardGenerator.getRandom(1, height - 2); - int x = getRandom(0, width - targetWidth); - int y = getRandom(0, height - targetHeight); + int minTargetWidth = boardGenerator.getRandom(1, targetWidth); + int minTargetHeight = boardGenerator.getRandom(1, targetHeight); - CellLayoutBoard board = generateBoard(new CellLayoutBoard(width, height), - new Rect(0, 0, width, height), targetWidth * targetHeight); + int x = boardGenerator.getRandom(0, width - targetWidth); + int y = boardGenerator.getRandom(0, height - targetHeight); + + CellLayoutBoard board = boardGenerator.generateBoard(width, height, + targetWidth * targetHeight); ItemConfiguration solution = solve(board, x, y, targetWidth, targetHeight, - minTargetWidth, minTargetHeight); + minTargetWidth, minTargetHeight, isMultiCellLayout); - CellLayoutBoard finishBoard = solution.isSolution ? boardFromSolution(solution, - board.getWidth(), board.getHeight()) : new CellLayoutBoard(board.getWidth(), + CellLayoutBoard finishBoard = boardFromSolution(solution, board.getWidth(), board.getHeight()); - testCase.startBoard = board; testCase.endBoard = finishBoard; testCase.isValidSolution = solution.isSolution; @@ -222,39 +296,24 @@ public class ReorderAlgorithmUnitTest { return testCase; } - private int getRandom(int start, int end) { - int random = end == 0 ? 0 : new Random().nextInt(end); - return start + random; - } - - private CellLayoutBoard generateBoard(CellLayoutBoard board, Rect area, - int emptySpaces) { - if (area.height() * area.width() <= 0) return board; - - int width = getRandom(1, area.width() - 1); - int height = getRandom(1, area.height() - 1); - - int x = area.left + getRandom(0, area.width() - width); - int y = area.top + getRandom(0, area.height() - height); - - if (emptySpaces > 0) { - emptySpaces -= width * height; - } else if (width * height > 1) { - board.addWidget(x, y, width, height); - } else { - board.addIcon(x, y); + /** + * Makes sure the final solution has valid integrity meaning that the number and sizes of + * widgets is the expect and there are no missing widgets. + */ + public boolean validateIntegrity(CellLayoutBoard startBoard, CellLayoutBoard finishBoard, + ReorderAlgorithmUnitTestCase testCase) { + if (!testCase.isValidSolution) { + // if we couldn't place the widget then the solution should be identical to the board + return startBoard.compareTo(finishBoard) == 0; } - - generateBoard(board, - new Rect(area.left, area.top, area.right, y), emptySpaces); - generateBoard(board, - new Rect(area.left, y, x, area.bottom), emptySpaces); - generateBoard(board, - new Rect(x, y + height, area.right, area.bottom), emptySpaces); - generateBoard(board, - new Rect(x + width, y, area.right, y + height), emptySpaces); - - return board; + WidgetRect addedWidget = finishBoard.getWidgetOfType(MAIN_WIDGET_TYPE); + finishBoard.removeItem(MAIN_WIDGET_TYPE); + Comparator comparator = new PermutedBoardComparator(); + if (comparator.compare(startBoard, finishBoard) != 0) { + return false; + } + return addedWidget.getSpanX() >= testCase.minSpanX + && addedWidget.getSpanY() >= testCase.minSpanY; } private static List getTestCases(String testPath) diff --git a/tests/src/com/android/launcher3/celllayout/board/CellLayoutBoard.java b/tests/src/com/android/launcher3/celllayout/board/CellLayoutBoard.java index e90e14540e..dbbdcf519a 100644 --- a/tests/src/com/android/launcher3/celllayout/board/CellLayoutBoard.java +++ b/tests/src/com/android/launcher3/celllayout/board/CellLayoutBoard.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023 The Android Open Source Project + * Copyright (C) 2022 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. @@ -18,8 +18,11 @@ package com.android.launcher3.celllayout.board; import android.graphics.Point; import android.graphics.Rect; +import androidx.annotation.NonNull; + import java.util.ArrayDeque; import java.util.ArrayList; +import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; import java.util.List; @@ -31,73 +34,13 @@ import java.util.stream.Collectors; public class CellLayoutBoard implements Comparable { - private boolean intersects(Rect r1, Rect r2) { - // If one rectangle is on left side of other - if (r1.left > r2.right || r2.left > r1.right) { - return false; - } - - // If one rectangle is above other - if (r1.bottom > r2.top || r2.bottom > r1.top) { - return false; - } - - return true; - } - - private boolean overlapsWithIgnored(Set ignoredRectangles, Rect rect) { - for (Rect ignoredRect : ignoredRectangles) { - // Using the built in intersects doesn't work because it doesn't account for area 0 - if (intersects(ignoredRect, rect)) { - return true; - } - } - return false; - } + public static final Comparator COMPARATOR = new IdenticalBoardComparator(); @Override - public int compareTo(CellLayoutBoard cellLayoutBoard) { - // to be equal they need to have the same number of widgets and the same dimensions - // their order can be different - Set widgetsSet = new HashSet<>(); - Set ignoredRectangles = new HashSet<>(); - for (WidgetRect rect : mWidgetsRects) { - if (rect.shouldIgnore()) { - ignoredRectangles.add(rect.mBounds); - } else { - widgetsSet.add(rect.mBounds); - } - } - for (WidgetRect rect : cellLayoutBoard.mWidgetsRects) { - // ignore rectangles overlapping with the area marked by x - if (overlapsWithIgnored(ignoredRectangles, rect.mBounds)) { - continue; - } - if (!widgetsSet.contains(rect.mBounds)) { - return -1; - } - widgetsSet.remove(rect.mBounds); - } - if (!widgetsSet.isEmpty()) { - return 1; - } - - // to be equal they need to have the same number of icons their order can be different - Set iconsSet = new HashSet<>(); - mIconPoints.forEach(icon -> iconsSet.add(icon.getCoord())); - for (IconPoint icon : cellLayoutBoard.mIconPoints) { - if (!iconsSet.contains(icon.getCoord())) { - return -1; - } - iconsSet.remove(icon.getCoord()); - } - if (!iconsSet.isEmpty()) { - return 1; - } - return 0; + public int compareTo(@NonNull CellLayoutBoard cellLayoutBoard) { + return COMPARATOR.compare(this, cellLayoutBoard); } - private HashSet mUsedWidgetTypes = new HashSet<>(); static final int INFINITE = 99999; @@ -112,7 +55,7 @@ public class CellLayoutBoard implements Comparable { WidgetRect mMain = null; - public int mWidth, mHeight; + int mWidth, mHeight; public CellLayoutBoard() { for (int x = 0; x < mWidget.length; x++) { @@ -123,7 +66,7 @@ public class CellLayoutBoard implements Comparable { } public CellLayoutBoard(int width, int height) { - mWidget = new char[width][height]; + mWidget = new char[width + 1][height + 1]; this.mWidth = width; this.mHeight = height; for (int x = 0; x < mWidget.length; x++) { @@ -139,6 +82,15 @@ public class CellLayoutBoard implements Comparable { return isXInRect && isYInRect; } + public WidgetRect getWidgetAt(Point p) { + return getWidgetAt(p.x, p.y); + } + + public WidgetRect getWidgetOfType(char type) { + return mWidgetsRects.stream() + .filter(widgetRect -> widgetRect.mType == type).findFirst().orElse(null); + } + public WidgetRect getWidgetAt(int x, int y) { return mWidgetsRects.stream() .filter(widgetRect -> pointInsideRect(x, y, widgetRect)).findFirst().orElse(null); @@ -165,8 +117,8 @@ public class CellLayoutBoard implements Comparable { } private void removeWidgetFromBoard(WidgetRect widget) { - for (int xi = widget.mBounds.left; xi < widget.mBounds.right; xi++) { - for (int yi = widget.mBounds.top; yi < widget.mBounds.bottom; yi++) { + for (int xi = widget.mBounds.left; xi <= widget.mBounds.right; xi++) { + for (int yi = widget.mBounds.bottom; yi <= widget.mBounds.top; yi++) { mWidget[xi][yi] = '-'; } } @@ -207,7 +159,7 @@ public class CellLayoutBoard implements Comparable { private void removeOverlappingItems(Point p) { // Remove overlapping widgets and remove them from the board mWidgetsRects = mWidgetsRects.stream().filter(widget -> { - if (widget.mBounds.contains(p.x, p.y)) { + if (IdenticalBoardComparator.Companion.touchesPoint(widget.mBounds, p)) { removeWidgetFromBoard(widget); return false; } @@ -237,8 +189,9 @@ public class CellLayoutBoard implements Comparable { } private char getNextWidgetType() { - for (char type = 'a'; type <= 'z'; type++) { - if (type == 'i') continue; + for (char type = 'a'; type < 'z'; type++) { + if (type == CellType.ICON) continue; + if (type == CellType.IGNORE) continue; if (mUsedWidgetTypes.contains(type)) continue; mUsedWidgetTypes.add(type); return type; @@ -258,6 +211,17 @@ public class CellLayoutBoard implements Comparable { } } + public void removeItem(char type) { + mWidgetsRects.stream() + .filter(widgetRect -> widgetRect.mType == type) + .forEach(widgetRect -> removeOverlappingItems( + new Point(widgetRect.getCellX(), widgetRect.getCellY()))); + } + + public void removeItem(Point p) { + removeOverlappingItems(p); + } + public void addWidget(int x, int y, int spanX, int spanY) { addWidget(x, y, spanX, spanY, getNextWidgetType()); } @@ -407,8 +371,8 @@ public class CellLayoutBoard implements Comparable { s.append("\n"); maxX = Math.min(maxX, mWidget.length); maxY = Math.min(maxY, mWidget[0].length); - for (int y = 0; y < maxY; y++) { - for (int x = 0; x < maxX; x++) { + for (int y = 0; y <= maxY; y++) { + for (int x = 0; x <= maxX; x++) { s.append(mWidget[x][y]); } s.append('\n'); diff --git a/tests/src/com/android/launcher3/celllayout/board/IdenticalBoardComparator.kt b/tests/src/com/android/launcher3/celllayout/board/IdenticalBoardComparator.kt new file mode 100644 index 0000000000..a4a420cf59 --- /dev/null +++ b/tests/src/com/android/launcher3/celllayout/board/IdenticalBoardComparator.kt @@ -0,0 +1,101 @@ +/* + * Copyright (C) 2023 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.celllayout.board + +import android.graphics.Point +import android.graphics.Rect + +/** + * Compares two [CellLayoutBoard] and returns 0 if they are identical, meaning they have the same + * widget and icons in the same place, they can be different letters tough. + */ +class IdenticalBoardComparator : Comparator { + + /** Converts a list of WidgetRect into a map of the count of different widget.bounds */ + private fun widgetsToBoundsMap(widgets: List) = + widgets.groupingBy { it.mBounds }.eachCount() + + /** Converts a list of IconPoint into a map of the count of different icon.coord */ + private fun iconsToPosCountMap(widgets: List) = + widgets.groupingBy { it.getCoord() }.eachCount() + + override fun compare( + cellLayoutBoard: CellLayoutBoard, + otherCellLayoutBoard: CellLayoutBoard + ): Int { + // to be equal they need to have the same number of widgets and the same dimensions + // their order can be different + val widgetsMap: Map = + widgetsToBoundsMap(cellLayoutBoard.widgets.filter { !it.shouldIgnore() }) + val ignoredRectangles: Map = + widgetsToBoundsMap(cellLayoutBoard.widgets.filter { it.shouldIgnore() }) + + val otherWidgetMap: Map = + widgetsToBoundsMap( + otherCellLayoutBoard.widgets + .filter { !it.shouldIgnore() } + .filter { !overlapsWithIgnored(ignoredRectangles, it.mBounds) } + ) + + if (widgetsMap != otherWidgetMap) { + return -1 + } + + // to be equal they need to have the same number of icons their order can be different + return if ( + iconsToPosCountMap(cellLayoutBoard.icons) == + iconsToPosCountMap(otherCellLayoutBoard.icons) + ) { + 0 + } else { + 1 + } + } + + private fun overlapsWithIgnored(ignoredRectangles: Map, rect: Rect): Boolean { + for (ignoredRect in ignoredRectangles.keys) { + // Using the built in intersects doesn't work because it doesn't account for area 0 + if (touches(ignoredRect, rect)) { + return true + } + } + return false + } + + companion object { + /** + * Similar function to {@link Rect#intersects} but this one returns true if the rectangles + * are intersecting or touching whereas {@link Rect#intersects} doesn't return true when + * they are touching. + */ + fun touches(r1: Rect, r2: Rect): Boolean { + // If one rectangle is on left side of other + return if (r1.left > r2.right || r2.left > r1.right) { + false + } else r1.bottom <= r2.top && r2.bottom <= r1.top + + // If one rectangle is above other + } + + /** + * Similar function to {@link Rect#contains} but this one returns true if {link @Point} is + * intersecting or touching the {@link Rect}. Similar to {@link touches}. + */ + fun touchesPoint(r1: Rect, p: Point): Boolean { + return r1.left <= p.x && p.x <= r1.right && r1.bottom <= p.y && p.y <= r1.top + } + } +} diff --git a/tests/src/com/android/launcher3/celllayout/board/PermutedBoardComparator.kt b/tests/src/com/android/launcher3/celllayout/board/PermutedBoardComparator.kt new file mode 100644 index 0000000000..c3d13a5701 --- /dev/null +++ b/tests/src/com/android/launcher3/celllayout/board/PermutedBoardComparator.kt @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2023 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.celllayout.board + +import android.graphics.Point + +/** + * Compares two [CellLayoutBoard] and returns 0 if they contain the same widgets and icons even if + * they are in different positions i.e. in a different permutation. + */ +class PermutedBoardComparator : Comparator { + + /** + * The key for the set is the span since the widgets could change location but shouldn't change + * size + */ + private fun boardToSpanCountMap(widgets: List) = + widgets.groupingBy { Point(it.spanX, it.spanY) }.eachCount() + override fun compare( + cellLayoutBoard: CellLayoutBoard, + otherCellLayoutBoard: CellLayoutBoard + ): Int { + return if ( + boardToSpanCountMap(cellLayoutBoard.widgets) != + boardToSpanCountMap(otherCellLayoutBoard.widgets) + ) { + 1 + } else cellLayoutBoard.icons.size.compareTo(otherCellLayoutBoard.icons.size) + } +} diff --git a/tests/src/com/android/launcher3/celllayout/testgenerator/DeterministicRandomGenerator.kt b/tests/src/com/android/launcher3/celllayout/testgenerator/DeterministicRandomGenerator.kt new file mode 100644 index 0000000000..e5829730c0 --- /dev/null +++ b/tests/src/com/android/launcher3/celllayout/testgenerator/DeterministicRandomGenerator.kt @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2023 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.celllayout.testgenerator + +import java.util.Random + +abstract class DeterministicRandomGenerator(private val generator: Random) { + fun getRandom(start: Int, end: Int): Int = start + (if (end == 0) 0 else generator.nextInt(end)) +} diff --git a/tests/src/com/android/launcher3/celllayout/testgenerator/RandomBoardGenerator.kt b/tests/src/com/android/launcher3/celllayout/testgenerator/RandomBoardGenerator.kt new file mode 100644 index 0000000000..770024fb5d --- /dev/null +++ b/tests/src/com/android/launcher3/celllayout/testgenerator/RandomBoardGenerator.kt @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2023 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.celllayout.testgenerator + +import android.graphics.Rect +import com.android.launcher3.celllayout.board.CellLayoutBoard +import java.util.Random + +/** Generates a random CellLayoutBoard. */ +open class RandomBoardGenerator(generator: Random) : DeterministicRandomGenerator(generator) { + /** + * @param remainingEmptySpaces the maximum number of spaces we will fill with icons and widgets + * meaning that if the number is 100 we will try to fill the board with at most 100 spaces + * usually less than 100. + * @return a randomly generated board filled with icons and widgets. + */ + open fun generateBoard(width: Int, height: Int, remainingEmptySpaces: Int): CellLayoutBoard? { + val cellLayoutBoard = CellLayoutBoard(width, height) + return fillBoard(cellLayoutBoard, Rect(0, 0, width, height), remainingEmptySpaces) + } + + protected fun fillBoard( + board: CellLayoutBoard, + area: Rect, + remainingEmptySpacesArg: Int + ): CellLayoutBoard { + var remainingEmptySpaces = remainingEmptySpacesArg + if (area.height() * area.width() <= 0) return board + val width = getRandom(1, area.width() - 1) + val height = getRandom(1, area.height() - 1) + val x = area.left + getRandom(0, area.width() - width) + val y = area.top + getRandom(0, area.height() - height) + if (remainingEmptySpaces > 0) { + remainingEmptySpaces -= width * height + } else if (board.widgets.size <= 22 && width * height > 1) { + board.addWidget(x, y, width, height) + } else { + board.addIcon(x, y) + } + fillBoard(board, Rect(area.left, area.top, area.right, y), remainingEmptySpaces) + fillBoard(board, Rect(area.left, y, x, area.bottom), remainingEmptySpaces) + fillBoard(board, Rect(x, y + height, area.right, area.bottom), remainingEmptySpaces) + fillBoard(board, Rect(x + width, y, area.right, y + height), remainingEmptySpaces) + return board + } +} diff --git a/tests/src/com/android/launcher3/celllayout/testgenerator/RandomMultiBoardGenerator.kt b/tests/src/com/android/launcher3/celllayout/testgenerator/RandomMultiBoardGenerator.kt new file mode 100644 index 0000000000..da4de7ddf2 --- /dev/null +++ b/tests/src/com/android/launcher3/celllayout/testgenerator/RandomMultiBoardGenerator.kt @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2023 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.celllayout.testgenerator + +import android.graphics.Rect +import com.android.launcher3.celllayout.board.CellLayoutBoard +import java.util.Random + +class RandomMultiBoardGenerator(generator: Random) : RandomBoardGenerator(generator) { + override fun generateBoard( + width: Int, + height: Int, + remainingEmptySpaces: Int + ): CellLayoutBoard { + val cellLayoutBoard = CellLayoutBoard(width, height) + fillBoard(cellLayoutBoard, Rect(0, 0, width / 2, height), remainingEmptySpaces / 2) + return fillBoard( + cellLayoutBoard, + Rect(width / 2, 0, width, height), + remainingEmptySpaces / 2 + ) + } +}