summaryrefslogtreecommitdiff
path: root/tests/src/com/android/launcher3/celllayout/board
diff options
context:
space:
mode:
Diffstat (limited to 'tests/src/com/android/launcher3/celllayout/board')
-rw-r--r--tests/src/com/android/launcher3/celllayout/board/CellLayoutBoard.java415
-rw-r--r--tests/src/com/android/launcher3/celllayout/board/CellType.java32
-rw-r--r--tests/src/com/android/launcher3/celllayout/board/FolderPoint.java37
-rw-r--r--tests/src/com/android/launcher3/celllayout/board/IconPoint.java45
-rw-r--r--tests/src/com/android/launcher3/celllayout/board/IdenticalBoardComparator.kt101
-rw-r--r--tests/src/com/android/launcher3/celllayout/board/PermutedBoardComparator.kt43
-rw-r--r--tests/src/com/android/launcher3/celllayout/board/TestWorkspaceBuilder.java200
-rw-r--r--tests/src/com/android/launcher3/celllayout/board/WidgetRect.java59
8 files changed, 932 insertions, 0 deletions
diff --git a/tests/src/com/android/launcher3/celllayout/board/CellLayoutBoard.java b/tests/src/com/android/launcher3/celllayout/board/CellLayoutBoard.java
new file mode 100644
index 0000000000..dbbdcf519a
--- /dev/null
+++ b/tests/src/com/android/launcher3/celllayout/board/CellLayoutBoard.java
@@ -0,0 +1,415 @@
+/*
+ * 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.
+ * 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;
+
+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;
+import java.util.Map;
+import java.util.Queue;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+
+public class CellLayoutBoard implements Comparable<CellLayoutBoard> {
+
+ public static final Comparator<CellLayoutBoard> COMPARATOR = new IdenticalBoardComparator();
+
+ @Override
+ public int compareTo(@NonNull CellLayoutBoard cellLayoutBoard) {
+ return COMPARATOR.compare(this, cellLayoutBoard);
+ }
+
+ private HashSet<Character> mUsedWidgetTypes = new HashSet<>();
+
+ static final int INFINITE = 99999;
+
+ char[][] mWidget = new char[30][30];
+
+ List<WidgetRect> mWidgetsRects = new ArrayList<>();
+ Map<Character, WidgetRect> mWidgetsMap = new HashMap<>();
+
+ List<IconPoint> mIconPoints = new ArrayList<>();
+ List<FolderPoint> mFolderPoints = new ArrayList<>();
+
+ WidgetRect mMain = null;
+
+ int mWidth, mHeight;
+
+ public CellLayoutBoard() {
+ for (int x = 0; x < mWidget.length; x++) {
+ for (int y = 0; y < mWidget[0].length; y++) {
+ mWidget[x][y] = CellType.EMPTY;
+ }
+ }
+ }
+
+ public CellLayoutBoard(int width, int height) {
+ mWidget = new char[width + 1][height + 1];
+ this.mWidth = width;
+ this.mHeight = height;
+ for (int x = 0; x < mWidget.length; x++) {
+ for (int y = 0; y < mWidget[0].length; y++) {
+ mWidget[x][y] = CellType.EMPTY;
+ }
+ }
+ }
+
+ public boolean pointInsideRect(int x, int y, WidgetRect rect) {
+ Boolean isXInRect = x >= rect.getCellX() && x < rect.getCellX() + rect.getSpanX();
+ Boolean isYInRect = y >= rect.getCellY() && y < rect.getCellY() + rect.getSpanY();
+ 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);
+ }
+
+ public List<WidgetRect> getWidgets() {
+ return mWidgetsRects;
+ }
+
+ public List<IconPoint> getIcons() {
+ return mIconPoints;
+ }
+
+ public List<FolderPoint> getFolders() {
+ return mFolderPoints;
+ }
+
+ public WidgetRect getMain() {
+ return mMain;
+ }
+
+ public WidgetRect getWidgetRect(char c) {
+ return mWidgetsMap.get(c);
+ }
+
+ private void removeWidgetFromBoard(WidgetRect widget) {
+ 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] = '-';
+ }
+ }
+ }
+
+ private void removeOverlappingItems(Rect rect) {
+ // Remove overlapping widgets and remove them from the board
+ mWidgetsRects = mWidgetsRects.stream().filter(widget -> {
+ if (rect.intersect(widget.mBounds)) {
+ removeWidgetFromBoard(widget);
+ return false;
+ }
+ return true;
+ }).collect(Collectors.toList());
+ // Remove overlapping icons and remove them from the board
+ mIconPoints = mIconPoints.stream().filter(iconPoint -> {
+ int x = iconPoint.coord.x;
+ int y = iconPoint.coord.y;
+ if (rect.contains(x, y)) {
+ mWidget[x][y] = '-';
+ return false;
+ }
+ return true;
+ }).collect(Collectors.toList());
+
+ // Remove overlapping folders and remove them from the board
+ mFolderPoints = mFolderPoints.stream().filter(folderPoint -> {
+ int x = folderPoint.coord.x;
+ int y = folderPoint.coord.y;
+ if (rect.contains(x, y)) {
+ mWidget[x][y] = '-';
+ return false;
+ }
+ return true;
+ }).collect(Collectors.toList());
+ }
+
+ private void removeOverlappingItems(Point p) {
+ // Remove overlapping widgets and remove them from the board
+ mWidgetsRects = mWidgetsRects.stream().filter(widget -> {
+ if (IdenticalBoardComparator.Companion.touchesPoint(widget.mBounds, p)) {
+ removeWidgetFromBoard(widget);
+ return false;
+ }
+ return true;
+ }).collect(Collectors.toList());
+ // Remove overlapping icons and remove them from the board
+ mIconPoints = mIconPoints.stream().filter(iconPoint -> {
+ int x = iconPoint.coord.x;
+ int y = iconPoint.coord.y;
+ if (p.x == x && p.y == y) {
+ mWidget[x][y] = '-';
+ return false;
+ }
+ return true;
+ }).collect(Collectors.toList());
+
+ // Remove overlapping folders and remove them from the board
+ mFolderPoints = mFolderPoints.stream().filter(folderPoint -> {
+ int x = folderPoint.coord.x;
+ int y = folderPoint.coord.y;
+ if (p.x == x && p.y == y) {
+ mWidget[x][y] = '-';
+ return false;
+ }
+ return true;
+ }).collect(Collectors.toList());
+ }
+
+ private char getNextWidgetType() {
+ 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;
+ }
+ return 'z';
+ }
+
+ public void addWidget(int x, int y, int spanX, int spanY, char type) {
+ Rect rect = new Rect(x, y + spanY - 1, x + spanX - 1, y);
+ removeOverlappingItems(rect);
+ WidgetRect widgetRect = new WidgetRect(type, rect);
+ mWidgetsRects.add(widgetRect);
+ for (int xi = rect.left; xi < rect.right + 1; xi++) {
+ for (int yi = rect.bottom; yi < rect.top + 1; yi++) {
+ mWidget[xi][yi] = type;
+ }
+ }
+ }
+
+ 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());
+ }
+
+ public void addIcon(int x, int y) {
+ Point iconCoord = new Point(x, y);
+ removeOverlappingItems(iconCoord);
+ mIconPoints.add(new IconPoint(iconCoord, CellType.ICON));
+ mWidget[x][y] = 'i';
+ }
+
+ public static WidgetRect getWidgetRect(int x, int y, Set<Point> used, char[][] board) {
+ char type = board[x][y];
+ Queue<Point> search = new ArrayDeque<Point>();
+ Point current = new Point(x, y);
+ search.add(current);
+ used.add(current);
+ List<Point> neighbors = new ArrayList<>(List.of(
+ new Point(-1, 0),
+ new Point(0, -1),
+ new Point(1, 0),
+ new Point(0, 1))
+ );
+ Rect widgetRect = new Rect(INFINITE, -INFINITE, -INFINITE, INFINITE);
+ while (!search.isEmpty()) {
+ current = search.poll();
+ widgetRect.top = Math.max(widgetRect.top, current.y);
+ widgetRect.right = Math.max(widgetRect.right, current.x);
+ widgetRect.bottom = Math.min(widgetRect.bottom, current.y);
+ widgetRect.left = Math.min(widgetRect.left, current.x);
+ for (Point p : neighbors) {
+ Point next = new Point(current.x + p.x, current.y + p.y);
+ if (next.x < 0 || next.x >= board.length) continue;
+ if (next.y < 0 || next.y >= board[0].length) continue;
+ if (board[next.x][next.y] == type && !used.contains(next)) {
+ used.add(next);
+ search.add(next);
+ }
+ }
+ }
+ return new WidgetRect(type, widgetRect);
+ }
+
+ public static boolean isFolder(char type) {
+ return type >= 'A' && type <= 'Z';
+ }
+
+ public static boolean isWidget(char type) {
+ return type != CellType.ICON && type != CellType.EMPTY && (type >= 'a' && type <= 'z');
+ }
+
+ public static boolean isIcon(char type) {
+ return type == CellType.ICON;
+ }
+
+ private static List<WidgetRect> getRects(char[][] board) {
+ Set<Point> used = new HashSet<>();
+ List<WidgetRect> widgetsRects = new ArrayList<>();
+ for (int x = 0; x < board.length; x++) {
+ for (int y = 0; y < board[0].length; y++) {
+ if (!used.contains(new Point(x, y)) && isWidget(board[x][y])) {
+ widgetsRects.add(getWidgetRect(x, y, used, board));
+ }
+ }
+ }
+ return widgetsRects;
+ }
+
+ private static List<IconPoint> getIconPoints(char[][] board) {
+ List<IconPoint> iconPoints = new ArrayList<>();
+ for (int x = 0; x < board.length; x++) {
+ for (int y = 0; y < board[0].length; y++) {
+ if (isIcon(board[x][y])) {
+ iconPoints.add(new IconPoint(new Point(x, y), board[x][y]));
+ }
+ }
+ }
+ return iconPoints;
+ }
+
+ private static List<FolderPoint> getFolderPoints(char[][] board) {
+ List<FolderPoint> folderPoints = new ArrayList<>();
+ for (int x = 0; x < board.length; x++) {
+ for (int y = 0; y < board[0].length; y++) {
+ if (isFolder(board[x][y])) {
+ folderPoints.add(new FolderPoint(new Point(x, y), board[x][y]));
+ }
+ }
+ }
+ return folderPoints;
+ }
+
+ public static WidgetRect getMainFromList(List<CellLayoutBoard> boards) {
+ for (CellLayoutBoard board : boards) {
+ WidgetRect main = board.getMain();
+ if (main != null) {
+ return main;
+ }
+ }
+ return null;
+ }
+
+ public static WidgetRect getWidgetIn(List<CellLayoutBoard> boards, int x, int y) {
+ for (CellLayoutBoard board : boards) {
+ WidgetRect main = board.getWidgetAt(x, y);
+ if (main != null) {
+ return main;
+ }
+ x -= board.mWidth;
+ }
+ return null;
+ }
+
+ public static CellLayoutBoard boardFromString(String boardStr) {
+ String[] lines = boardStr.split("\n");
+ CellLayoutBoard board = new CellLayoutBoard();
+
+ for (int y = 0; y < lines.length; y++) {
+ String line = lines[y];
+ for (int x = 0; x < line.length(); x++) {
+ char c = line.charAt(x);
+ if (c != CellType.EMPTY) {
+ board.mWidget[x][y] = line.charAt(x);
+ }
+ }
+ }
+ board.mHeight = lines.length;
+ board.mWidth = lines[0].length();
+ board.mWidgetsRects = getRects(board.mWidget);
+ board.mWidgetsRects.forEach(widgetRect -> {
+ if (widgetRect.mType == CellType.MAIN_WIDGET) {
+ board.mMain = widgetRect;
+ }
+ board.mWidgetsMap.put(widgetRect.mType, widgetRect);
+ });
+ board.mIconPoints = getIconPoints(board.mWidget);
+ board.mFolderPoints = getFolderPoints(board.mWidget);
+ return board;
+ }
+
+ public String toString(int maxX, int maxY) {
+ StringBuilder s = new StringBuilder();
+ s.append("board: ");
+ s.append(maxX);
+ s.append("x");
+ s.append(maxY);
+ 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++) {
+ s.append(mWidget[x][y]);
+ }
+ s.append('\n');
+ }
+ return s.toString();
+ }
+
+ @Override
+ public String toString() {
+ return toString(mWidth, mHeight);
+ }
+
+ public static List<CellLayoutBoard> boardListFromString(String boardsStr) {
+ String[] lines = boardsStr.split("\n");
+ ArrayList<String> individualBoards = new ArrayList<>();
+ ArrayList<CellLayoutBoard> boards = new ArrayList<>();
+ for (String line : lines) {
+ String[] boardSegment = line.split("\\|");
+ for (int i = 0; i < boardSegment.length; i++) {
+ if (i >= individualBoards.size()) {
+ individualBoards.add(boardSegment[i]);
+ } else {
+ individualBoards.set(i, individualBoards.get(i) + "\n" + boardSegment[i]);
+ }
+ }
+ }
+ for (String board : individualBoards) {
+ boards.add(CellLayoutBoard.boardFromString(board));
+ }
+ return boards;
+ }
+
+ public int getWidth() {
+ return mWidth;
+ }
+
+ public int getHeight() {
+ return mHeight;
+ }
+}
diff --git a/tests/src/com/android/launcher3/celllayout/board/CellType.java b/tests/src/com/android/launcher3/celllayout/board/CellType.java
new file mode 100644
index 0000000000..49c146b32a
--- /dev/null
+++ b/tests/src/com/android/launcher3/celllayout/board/CellType.java
@@ -0,0 +1,32 @@
+/*
+ * 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;
+
+public class CellType {
+ // The cells marked by this will be filled by 1x1 widgets and will be ignored when
+ // validating
+ public static final char IGNORE = 'x';
+ // The cells marked by this will be filled by app icons
+ public static final char ICON = 'i';
+ // The cells marked by FOLDER will be filled by folders with 27 app icons inside
+ public static final char FOLDER = 'Z';
+ // Empty space
+ public static final char EMPTY = '-';
+ // Widget that will be saved as "main widget" for easier retrieval
+ public static final char MAIN_WIDGET = 'm';
+ // Everything else will be consider a widget
+}
diff --git a/tests/src/com/android/launcher3/celllayout/board/FolderPoint.java b/tests/src/com/android/launcher3/celllayout/board/FolderPoint.java
new file mode 100644
index 0000000000..39ba434dc0
--- /dev/null
+++ b/tests/src/com/android/launcher3/celllayout/board/FolderPoint.java
@@ -0,0 +1,37 @@
+/*
+ * 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;
+
+public class FolderPoint {
+ public Point coord;
+ public char mType;
+
+ public FolderPoint(Point coord, char type) {
+ this.coord = coord;
+ mType = type;
+ }
+
+ /**
+ * [A-Z]: Represents a folder and number of icons in the folder is represented by
+ * the order of letter in the alphabet, A=2, B=3, C=4 ... etc.
+ */
+ public int getNumberIconsInside() {
+ return (mType - 'A') + 2;
+ }
+}
diff --git a/tests/src/com/android/launcher3/celllayout/board/IconPoint.java b/tests/src/com/android/launcher3/celllayout/board/IconPoint.java
new file mode 100644
index 0000000000..d3d297003d
--- /dev/null
+++ b/tests/src/com/android/launcher3/celllayout/board/IconPoint.java
@@ -0,0 +1,45 @@
+/*
+ * 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;
+
+public class IconPoint {
+ public Point coord;
+ public char mType;
+
+ public IconPoint(Point coord, char type) {
+ this.coord = coord;
+ mType = type;
+ }
+
+ public char getType() {
+ return mType;
+ }
+
+ public void setType(char type) {
+ mType = type;
+ }
+
+ public Point getCoord() {
+ return coord;
+ }
+
+ public void setCoord(Point coord) {
+ this.coord = coord;
+ }
+}
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<CellLayoutBoard> {
+
+ /** Converts a list of WidgetRect into a map of the count of different widget.bounds */
+ private fun widgetsToBoundsMap(widgets: List<WidgetRect>) =
+ 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<IconPoint>) =
+ 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<Rect, Int> =
+ widgetsToBoundsMap(cellLayoutBoard.widgets.filter { !it.shouldIgnore() })
+ val ignoredRectangles: Map<Rect, Int> =
+ widgetsToBoundsMap(cellLayoutBoard.widgets.filter { it.shouldIgnore() })
+
+ val otherWidgetMap: Map<Rect, Int> =
+ 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, Int>, 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<CellLayoutBoard> {
+
+ /**
+ * The key for the set is the span since the widgets could change location but shouldn't change
+ * size
+ */
+ private fun boardToSpanCountMap(widgets: List<WidgetRect>) =
+ 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/board/TestWorkspaceBuilder.java b/tests/src/com/android/launcher3/celllayout/board/TestWorkspaceBuilder.java
new file mode 100644
index 0000000000..06a7db2ae0
--- /dev/null
+++ b/tests/src/com/android/launcher3/celllayout/board/TestWorkspaceBuilder.java
@@ -0,0 +1,200 @@
+/*
+ * 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 static androidx.test.core.app.ApplicationProvider.getApplicationContext;
+import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
+
+import static com.android.launcher3.ui.TestViewHelpers.findWidgetProvider;
+import static com.android.launcher3.util.WidgetUtils.createWidgetInfo;
+
+import android.content.ComponentName;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.graphics.Rect;
+import android.os.Process;
+import android.os.UserHandle;
+import android.util.Log;
+
+import com.android.launcher3.InvariantDeviceProfile;
+import com.android.launcher3.LauncherSettings;
+import com.android.launcher3.celllayout.FavoriteItemsTransaction;
+import com.android.launcher3.celllayout.board.CellLayoutBoard;
+import com.android.launcher3.celllayout.board.CellType;
+import com.android.launcher3.celllayout.board.FolderPoint;
+import com.android.launcher3.celllayout.board.IconPoint;
+import com.android.launcher3.celllayout.board.WidgetRect;
+import com.android.launcher3.model.data.AppInfo;
+import com.android.launcher3.model.data.FolderInfo;
+import com.android.launcher3.model.data.ItemInfo;
+import com.android.launcher3.model.data.LauncherAppWidgetInfo;
+import com.android.launcher3.model.data.WorkspaceItemInfo;
+import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
+
+import java.util.function.Supplier;
+import java.util.stream.IntStream;
+
+public class TestWorkspaceBuilder {
+
+ private static final String TAG = "CellLayoutBoardBuilder";
+ private static final String TEST_ACTIVITY_PACKAGE_PREFIX = "com.android.launcher3.tests.";
+ private ComponentName mAppComponentName = new ComponentName(
+ "com.google.android.calculator", "com.android.calculator2.Calculator");
+ private UserHandle mMyUser;
+
+ private Context mContext;
+ private ContentResolver mResolver;
+
+ public TestWorkspaceBuilder(Context context) {
+ mMyUser = Process.myUserHandle();
+ mContext = context;
+ mResolver = mContext.getContentResolver();
+ }
+
+ /**
+ * Fills the given rect in WidgetRect with 1x1 widgets. This is useful to equalize cases.
+ */
+ private FavoriteItemsTransaction fillWithWidgets(WidgetRect widgetRect,
+ FavoriteItemsTransaction transaction, int screenId) {
+ int initX = widgetRect.getCellX();
+ int initY = widgetRect.getCellY();
+ for (int x = initX; x < initX + widgetRect.getSpanX(); x++) {
+ for (int y = initY; y < initY + widgetRect.getSpanY(); y++) {
+ try {
+ // this widgets are filling, we don't care if we can't place them
+ transaction.addItem(createWidgetInCell(
+ new WidgetRect(CellType.IGNORE,
+ new Rect(x, y, x, y)), screenId));
+ } catch (Exception e) {
+ Log.d(TAG, "Unable to place filling widget at " + x + "," + y);
+ }
+ }
+ }
+ return transaction;
+ }
+
+ private AppInfo getApp() {
+ return new AppInfo(mAppComponentName, "test icon", mMyUser,
+ AppInfo.makeLaunchIntent(mAppComponentName));
+ }
+
+ /**
+ * Helper to set the app to use for the test workspace,
+ * using activity-alias from AndroidManifest-common.
+ * @param testAppName the android:name field of the test app activity-alias to use
+ */
+ public void setTestAppActivityAlias(String testAppName) {
+ this.mAppComponentName = new ComponentName(
+ getInstrumentation().getContext().getPackageName(),
+ TEST_ACTIVITY_PACKAGE_PREFIX + testAppName
+ );
+ }
+
+ private void addCorrespondingWidgetRect(WidgetRect widgetRect,
+ FavoriteItemsTransaction transaction, int screenId) {
+ if (widgetRect.mType == 'x') {
+ fillWithWidgets(widgetRect, transaction, screenId);
+ } else {
+ transaction.addItem(createWidgetInCell(widgetRect, screenId));
+ }
+ }
+
+ /**
+ * Builds the given board into the transaction
+ */
+ public FavoriteItemsTransaction buildFromBoard(CellLayoutBoard board,
+ FavoriteItemsTransaction transaction, final int screenId) {
+ board.getWidgets().forEach(
+ (widgetRect) -> addCorrespondingWidgetRect(widgetRect, transaction, screenId));
+ board.getIcons().forEach((iconPoint) ->
+ transaction.addItem(() -> createIconInCell(iconPoint, screenId))
+ );
+ board.getFolders().forEach((folderPoint) ->
+ transaction.addItem(() -> createFolderInCell(folderPoint, screenId))
+ );
+ return transaction;
+ }
+
+ /**
+ * Fills the hotseat row with apps instead of suggestions, for this to work the workspace should
+ * be clean otherwise this doesn't overrides the existing icons.
+ */
+ public FavoriteItemsTransaction fillHotseatIcons(FavoriteItemsTransaction transaction) {
+ IntStream.range(0, InvariantDeviceProfile.INSTANCE.get(mContext).numDatabaseHotseatIcons)
+ .forEach(i -> transaction.addItem(() -> getHotseatValues(i)));
+ return transaction;
+ }
+
+ private Supplier<ItemInfo> createWidgetInCell(
+ WidgetRect widgetRect, int screenId) {
+ // Create the widget lazily since the appWidgetId can get lost during setup
+ return () -> {
+ LauncherAppWidgetProviderInfo info = findWidgetProvider(false);
+ LauncherAppWidgetInfo item = createWidgetInfo(info, getApplicationContext(), true);
+ item.cellX = widgetRect.getCellX();
+ item.cellY = widgetRect.getCellY();
+ item.spanX = widgetRect.getSpanX();
+ item.spanY = widgetRect.getSpanY();
+ item.screenId = screenId;
+ return item;
+ };
+ }
+
+ public FolderInfo createFolderInCell(FolderPoint folderPoint, int screenId) {
+ FolderInfo folderInfo = new FolderInfo();
+ folderInfo.screenId = screenId;
+ folderInfo.container = LauncherSettings.Favorites.CONTAINER_DESKTOP;
+ folderInfo.cellX = folderPoint.coord.x;
+ folderInfo.cellY = folderPoint.coord.y;
+ folderInfo.minSpanY = folderInfo.minSpanX = folderInfo.spanX = folderInfo.spanY = 1;
+ folderInfo.setOption(FolderInfo.FLAG_MULTI_PAGE_ANIMATION, true, null);
+
+ for (int i = 0; i < folderPoint.getNumberIconsInside(); i++) {
+ folderInfo.add(getDefaultWorkspaceItem(screenId), false);
+ }
+
+ return folderInfo;
+ }
+
+ private WorkspaceItemInfo getDefaultWorkspaceItem(int screenId) {
+ WorkspaceItemInfo item = new WorkspaceItemInfo(getApp());
+ item.screenId = screenId;
+ item.minSpanY = item.minSpanX = item.spanX = item.spanY = 1;
+ item.container = LauncherSettings.Favorites.CONTAINER_DESKTOP;
+ return item;
+ }
+
+ private ItemInfo createIconInCell(IconPoint iconPoint, int screenId) {
+ WorkspaceItemInfo item = new WorkspaceItemInfo(getApp());
+ item.screenId = screenId;
+ item.cellX = iconPoint.getCoord().x;
+ item.cellY = iconPoint.getCoord().y;
+ item.minSpanY = item.minSpanX = item.spanX = item.spanY = 1;
+ item.container = LauncherSettings.Favorites.CONTAINER_DESKTOP;
+ return item;
+ }
+
+ private ItemInfo getHotseatValues(int x) {
+ WorkspaceItemInfo item = new WorkspaceItemInfo(getApp());
+ item.cellX = x;
+ item.cellY = 0;
+ item.minSpanY = item.minSpanX = item.spanX = item.spanY = 1;
+ item.rank = x;
+ item.screenId = x;
+ item.container = LauncherSettings.Favorites.CONTAINER_HOTSEAT;
+ return item;
+ }
+}
diff --git a/tests/src/com/android/launcher3/celllayout/board/WidgetRect.java b/tests/src/com/android/launcher3/celllayout/board/WidgetRect.java
new file mode 100644
index 0000000000..c90ce8504f
--- /dev/null
+++ b/tests/src/com/android/launcher3/celllayout/board/WidgetRect.java
@@ -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.board;
+
+import android.graphics.Rect;
+
+public class WidgetRect {
+ public char mType;
+ public Rect mBounds;
+
+ public WidgetRect(char type, Rect bounds) {
+ this.mType = type;
+ this.mBounds = bounds;
+ }
+
+ public int getSpanX() {
+ return mBounds.right - mBounds.left + 1;
+ }
+
+ public int getSpanY() {
+ return mBounds.top - mBounds.bottom + 1;
+ }
+
+ public int getCellX() {
+ return mBounds.left;
+ }
+
+ public int getCellY() {
+ return mBounds.bottom;
+ }
+
+ boolean shouldIgnore() {
+ return this.mType == CellType.IGNORE;
+ }
+
+ boolean contains(int x, int y) {
+ return mBounds.contains(x, y);
+ }
+
+ @Override
+ public String toString() {
+ return "WidgetRect type = " + mType + " x = " + getCellX() + " | y " + getCellY()
+ + " xs = " + getSpanX() + " ys = " + getSpanY();
+ }
+}