Replace existing Robolectric test task with functioning one.

This CL does the following:
- Creates a dir for multivalentTests
- Creates symlinks for the dir to keep Android Studio happy
- Moves many files to the multivalentTests dir
- Adjusts gradle and soong build files to use the new dir as part of
their source sets.

Test: ./gradlew :NexusLauncher:testGoogleWithQuickstepDebugUnitTest
Test: atest Launcher3RoboTests
Fix: 316553886
Bug: 316553889
Flag: NA
Change-Id: Iae28fd0c0191b3ecf9bd2950800875950cca2622
This commit is contained in:
Uwais Ashraf
2023-12-27 11:44:16 +00:00
parent 82bf25b017
commit 77b97c0729
149 changed files with 62 additions and 54 deletions
@@ -1,162 +0,0 @@
/*
* 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;
import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
import android.graphics.Point;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.stream.Collectors;
public class CellLayoutTestCaseReader {
public abstract static class TestSection {
State state;
public TestSection(State state) {
this.state = state;
}
public State getState() {
return state;
}
public void setState(State state) {
this.state = state;
}
}
public static class Comment extends TestSection {
public Comment() {
super(State.COMMENT);
}
}
public static class Arguments extends TestSection {
String[] arguments;
public Arguments(String[] arguments) {
super(State.ARGUMENTS);
this.arguments = arguments;
}
}
public static class Board extends TestSection {
public Point gridSize;
public String board;
public Board(Point gridSize, String board) {
super(State.BOARD);
this.gridSize = gridSize;
this.board = board;
}
}
public enum State {
START,
ARGUMENTS,
BOARD,
END,
COMMENT
}
String mTest;
protected CellLayoutTestCaseReader(String test) {
mTest = test;
}
public static CellLayoutTestCaseReader readFromFile(String fileName) throws IOException {
String fileStr = new BufferedReader(new InputStreamReader(
getInstrumentation().getContext().getAssets().open(fileName))
).lines().collect(Collectors.joining("\n"));
return new CellLayoutTestCaseReader(fileStr);
}
private State getStateFromLine(String line) {
String typeWithColons = line.trim().split(" ")[0].trim();
String type = typeWithColons.substring(0, typeWithColons.length() - 1);
try {
return Enum.valueOf(State.class, type.toUpperCase());
} catch (Exception e) {
throw new RuntimeException(
"The given tag " + typeWithColons + " doesn't match with the existing tags");
}
}
private String removeTag(String line) {
return line.split(":")[1];
}
private TestSection parseNextLine(Iterator<String> it) {
String line = it.next();
if (line.trim().charAt(0) == '#') {
return new Comment();
}
State state = getStateFromLine(line);
line = removeTag(line);
switch (state) {
case ARGUMENTS:
return new Arguments(parseArgumentsLine(line));
case BOARD:
Point grid = parseGridSize(line);
return new Board(grid, parseBoard(it, grid.y));
default:
return new Comment();
}
}
public List<TestSection> parse() {
List<TestSection> sections = new ArrayList<>();
String[] lines = mTest.split("\n");
Iterator<String> it = Arrays.stream(lines).iterator();
while (it.hasNext()) {
TestSection section = parseNextLine(it);
if (section.state == State.COMMENT) {
continue;
}
sections.add(section);
}
return sections;
}
private String parseBoard(Iterator<String> it, int rows) {
StringBuilder board = new StringBuilder();
for (int j = 0; j < rows; j++) {
board.append(it.next() + "\n");
}
return board.toString();
}
private String[] parseArgumentsLine(String line) {
return Arrays.stream(line.trim().split(" ")).map(String::trim).toArray(String[]::new);
}
private Point parseGridSize(String line) {
String[] values = line.toLowerCase(Locale.ROOT).split("x");
int x = Integer.parseInt(values[0].trim());
int y = Integer.parseInt(values[1].trim());
return new Point(x, y);
}
}
@@ -1,79 +0,0 @@
/*
* 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;
import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_DESKTOP;
import android.view.View;
import com.android.launcher3.CellLayout;
import com.android.launcher3.Launcher;
import com.android.launcher3.celllayout.board.CellLayoutBoard;
import com.android.launcher3.views.DoubleShadowBubbleTextView;
import java.util.ArrayList;
import java.util.List;
public class CellLayoutTestUtils {
public static ArrayList<CellLayoutBoard> workspaceToBoards(Launcher launcher) {
ArrayList<CellLayoutBoard> boards = new ArrayList<>();
for (CellLayout cellLayout : launcher.getWorkspace().mWorkspaceScreens) {
int count = cellLayout.getShortcutsAndWidgets().getChildCount();
for (int i = 0; i < count; i++) {
View callView = cellLayout.getShortcutsAndWidgets().getChildAt(i);
CellLayoutLayoutParams params =
(CellLayoutLayoutParams) callView.getLayoutParams();
CellPosMapper.CellPos pos = launcher.getCellPosMapper().mapPresenterToModel(
params.getCellX(), params.getCellY(),
launcher.getWorkspace().getIdForScreen(cellLayout), CONTAINER_DESKTOP);
int screenId = pos.screenId;
for (int j = boards.size(); j <= screenId; j++) {
boards.add(new CellLayoutBoard(cellLayout.getCountX(), cellLayout.getCountY()));
}
CellLayoutBoard board = boards.get(screenId);
// is icon
if (callView instanceof DoubleShadowBubbleTextView) {
board.addIcon(pos.cellX, pos.cellY);
} else {
// is widget
board.addWidget(pos.cellX, pos.cellY, params.cellHSpan,
params.cellVSpan);
}
}
}
return boards;
}
public static CellLayoutBoard viewsToBoard(List<View> views, int width, int height) {
CellLayoutBoard board = new CellLayoutBoard(width, height);
for (View callView : views) {
CellLayoutLayoutParams params = (CellLayoutLayoutParams) callView.getLayoutParams();
// is icon
if (callView instanceof DoubleShadowBubbleTextView) {
board.addIcon(params.getCellX(), params.getCellY());
} else {
// is widget
board.addWidget(params.getCellX(), params.getCellY(), params.cellHSpan,
params.cellVSpan);
}
}
return board;
}
}
@@ -1,120 +0,0 @@
/*
* 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;
import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
import static com.android.launcher3.LauncherSettings.Favorites.TABLE_NAME;
import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
import static com.android.launcher3.util.TestUtil.runOnExecutorSync;
import android.content.Context;
import androidx.test.uiautomator.UiDevice;
import com.android.launcher3.LauncherAppState;
import com.android.launcher3.LauncherModel;
import com.android.launcher3.LauncherSettings;
import com.android.launcher3.model.ModelDbController;
import com.android.launcher3.model.data.FolderInfo;
import com.android.launcher3.model.data.ItemInfo;
import com.android.launcher3.provider.LauncherDbUtils.SQLiteTransaction;
import com.android.launcher3.tapl.LauncherInstrumentation;
import com.android.launcher3.util.ContentWriter;
import com.android.launcher3.util.ModelTestExtensions;
import java.util.ArrayList;
import java.util.function.Supplier;
public class FavoriteItemsTransaction {
private ArrayList<Supplier<ItemInfo>> mItemsToSubmit;
private Context mContext;
public FavoriteItemsTransaction(Context context) {
mItemsToSubmit = new ArrayList<>();
mContext = context;
}
public FavoriteItemsTransaction addItem(Supplier<ItemInfo> itemInfo) {
this.mItemsToSubmit.add(itemInfo);
return this;
}
/**
* Commits all the ItemInfo into the database of Favorites
**/
public void commit() {
LauncherModel model = LauncherAppState.getInstance(mContext).getModel();
// Load the model once so that there is no pending migration:
ModelTestExtensions.INSTANCE.loadModelSync(model);
runOnExecutorSync(MODEL_EXECUTOR, () -> {
ModelDbController controller = model.getModelDbController();
// Migrate any previous data so that the DB state is correct
controller.tryMigrateDB(null /* restoreEventLogger */);
// Create DB again to load fresh data
controller.createEmptyDB();
controller.clearEmptyDbFlag();
// Add new data
try (SQLiteTransaction transaction = controller.newTransaction()) {
int count = mItemsToSubmit.size();
ArrayList<ItemInfo> containerItems = new ArrayList<>();
for (int i = 0; i < count; i++) {
ContentWriter writer = new ContentWriter(mContext);
ItemInfo item = mItemsToSubmit.get(i).get();
if (item.itemType == LauncherSettings.Favorites.ITEM_TYPE_FOLDER) {
FolderInfo folderInfo = (FolderInfo) item;
for (ItemInfo itemInfo : folderInfo.contents) {
itemInfo.container = i;
containerItems.add(itemInfo);
}
}
item.onAddToDatabase(writer);
writer.put(LauncherSettings.Favorites._ID, i);
controller.insert(TABLE_NAME, writer.getValues(mContext));
}
for (int i = 0; i < containerItems.size(); i++) {
ContentWriter writer = new ContentWriter(mContext);
ItemInfo item = containerItems.get(i);
item.onAddToDatabase(writer);
writer.put(LauncherSettings.Favorites._ID, count + i);
controller.insert(TABLE_NAME, writer.getValues(mContext));
}
transaction.commit();
}
});
// Reload model
runOnExecutorSync(MAIN_EXECUTOR, model::forceReload);
ModelTestExtensions.INSTANCE.loadModelSync(model);
}
/**
* Commits the transaction and waits for home load
*/
public void commitAndLoadHome(LauncherInstrumentation inst) {
commit();
// Launch the home activity
UiDevice.getInstance(getInstrumentation()).pressHome();
inst.waitForLauncherInitialized();
}
}
@@ -1,153 +0,0 @@
/*
* 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;
import com.android.launcher3.celllayout.board.CellLayoutBoard;
import java.util.Iterator;
/**
* Represents a test case for {@code ReorderAlgorithmUnitTest}. The test cases are generated from
* text, an example of a test is the following:
*
* board: 10x8
* aaaaaaaaai
* bbbbbcciii
* ---------f
* ---------f
* ---------f
* ---------i
* iiddddddii
* iieeiiiiii
* arguments: 2 5 7 1 3 1 widget valid
* board: 10x8
* bbbbbbbbbi
* eeeeecciii
* ---------a
* ---------a
* ---------a
* --zzzzzzzi
* iiddddddii
* iiffiiiiii
*
*
* This represents a Workspace boards and a dragged widget that wants to be dropped on the
* workspace. The endBoard represents the result from such drag
* The first board is the startBoard, the arguments are as follow: cellX, cellY, widget spanX,
* widget spanY, minimum spanX, minimum spanX, type of object being drag (icon, widget, folder ),
* if the resulting board is a valid solution or not reorder was found.
*
* For more information on how to read the board please go to the text file
* reorder_algorithm_test_cases
*/
public class ReorderAlgorithmUnitTestCase {
CellLayoutBoard startBoard;
int x, y, spanX, spanY, minSpanX, minSpanY;
String type;
boolean isValidSolution;
CellLayoutBoard endBoard;
public static ReorderAlgorithmUnitTestCase readNextCase(
Iterator<CellLayoutTestCaseReader.TestSection> sections) {
ReorderAlgorithmUnitTestCase testCase = new ReorderAlgorithmUnitTestCase();
CellLayoutTestCaseReader.Board startBoard =
(CellLayoutTestCaseReader.Board) sections.next();
testCase.startBoard = CellLayoutBoard.boardFromString(startBoard.board);
CellLayoutTestCaseReader.Arguments arguments =
(CellLayoutTestCaseReader.Arguments) sections.next();
testCase.x = Integer.parseInt(arguments.arguments[0]);
testCase.y = Integer.parseInt(arguments.arguments[1]);
testCase.spanX = Integer.parseInt(arguments.arguments[2]);
testCase.spanY = Integer.parseInt(arguments.arguments[3]);
testCase.minSpanX = Integer.parseInt(arguments.arguments[4]);
testCase.minSpanY = Integer.parseInt(arguments.arguments[5]);
testCase.type = arguments.arguments[6];
testCase.isValidSolution = arguments.arguments[7].compareTo("valid") == 0;
CellLayoutTestCaseReader.Board endBoard = (CellLayoutTestCaseReader.Board) sections.next();
testCase.endBoard = CellLayoutBoard.boardFromString(endBoard.board);
return testCase;
}
public CellLayoutBoard getStartBoard() {
return startBoard;
}
public int getX() {
return x;
}
public void setX(int x) {
this.x = x;
}
public int getY() {
return y;
}
public void setY(int y) {
this.y = y;
}
public int getSpanX() {
return spanX;
}
public void setSpanX(int spanX) {
this.spanX = spanX;
}
public int getSpanY() {
return spanY;
}
public void setSpanY(int spanY) {
this.spanY = spanY;
}
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
public boolean isValidSolution() {
return isValidSolution;
}
public void setValidSolution(boolean validSolution) {
isValidSolution = validSolution;
}
public CellLayoutBoard getEndBoard() {
return endBoard;
}
public void setEndBoard(CellLayoutBoard endBoard) {
this.endBoard = endBoard;
}
@Override
public String toString() {
String valid = isValidSolution ? "valid" : "invalid";
return startBoard + "arguments: " + x + " " + y + " " + spanX + " " + spanY + " " + minSpanX
+ " " + minSpanY + " " + type + " " + valid + "\n" + endBoard;
}
}
@@ -1,47 +0,0 @@
/*
* 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;
import android.graphics.Point;
import com.android.launcher3.celllayout.board.CellLayoutBoard;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public class ReorderTestCase {
public List<CellLayoutBoard> mStart;
public Point moveMainTo;
public List<List<CellLayoutBoard>> mEnd;
public ReorderTestCase(List<CellLayoutBoard> start, Point moveMainTo,
List<CellLayoutBoard>... end) {
mStart = start;
this.moveMainTo = moveMainTo;
mEnd = Arrays.asList(end);
}
public ReorderTestCase(String start, Point moveMainTo, String ... end) {
mStart = CellLayoutBoard.boardListFromString(start);
this.moveMainTo = moveMainTo;
mEnd = Arrays
.asList(end)
.stream()
.map(CellLayoutBoard::boardListFromString)
.collect(Collectors.toList());
}
}
@@ -1,415 +0,0 @@
/*
* 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;
}
}
@@ -1,32 +0,0 @@
/*
* 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
}
@@ -1,37 +0,0 @@
/*
* 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;
}
}
@@ -1,45 +0,0 @@
/*
* 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;
}
}
@@ -1,101 +0,0 @@
/*
* 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
}
}
}
@@ -1,43 +0,0 @@
/*
* 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)
}
}
@@ -1,200 +0,0 @@
/*
* 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;
}
}
@@ -1,59 +0,0 @@
/*
* 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();
}
}
@@ -1,22 +0,0 @@
/*
* 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))
}
@@ -1,59 +0,0 @@
/*
* 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
}
}
@@ -1,36 +0,0 @@
/*
* 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
)
}
}
@@ -1,135 +0,0 @@
/*
* 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.model
import android.content.ComponentName
import android.content.Context
import android.graphics.Rect
import com.android.launcher3.InvariantDeviceProfile
import com.android.launcher3.LauncherAppState
import com.android.launcher3.model.data.AppInfo
import com.android.launcher3.model.data.WorkspaceItemInfo
import com.android.launcher3.util.GridOccupancy
import com.android.launcher3.util.IntArray
import com.android.launcher3.util.IntSparseArrayMap
import com.android.launcher3.util.LauncherLayoutBuilder
import com.android.launcher3.util.LauncherModelHelper
import com.android.launcher3.util.LauncherModelHelper.TEST_ACTIVITY
import com.android.launcher3.util.LauncherModelHelper.TEST_PACKAGE
import java.util.UUID
/** Base class for workspace related tests. */
abstract class AbstractWorkspaceModelTest {
companion object {
val emptyScreenSpaces = listOf(Rect(0, 0, 5, 5))
val fullScreenSpaces = emptyList<Rect>()
val nonEmptyScreenSpaces = listOf(Rect(1, 2, 3, 4))
}
protected lateinit var mLayoutBuilder: LauncherLayoutBuilder
protected lateinit var mTargetContext: Context
protected lateinit var mIdp: InvariantDeviceProfile
protected lateinit var mAppState: LauncherAppState
protected lateinit var mModelHelper: LauncherModelHelper
protected lateinit var mExistingScreens: IntArray
protected lateinit var mNewScreens: IntArray
protected lateinit var mScreenOccupancy: IntSparseArrayMap<GridOccupancy>
open fun setup() {
mLayoutBuilder = LauncherLayoutBuilder()
mModelHelper = LauncherModelHelper()
mTargetContext = mModelHelper.sandboxContext
mIdp = InvariantDeviceProfile.INSTANCE[mTargetContext]
mIdp.numRows = 5
mIdp.numColumns = mIdp.numRows
mAppState = LauncherAppState.getInstance(mTargetContext)
mExistingScreens = IntArray()
mScreenOccupancy = IntSparseArrayMap()
mNewScreens = IntArray()
}
open fun tearDown() {
mModelHelper.destroy()
}
/** Sets up workspaces with the given screen IDs with some items and a 2x2 space. */
fun setupWorkspaces(screenIdsWithItems: List<Int>) {
screenIdsWithItems.forEach { screenId -> setupWorkspace(screenId, nonEmptyScreenSpaces) }
mModelHelper.setupDefaultLayoutProvider(mLayoutBuilder)
mIdp.numRows = 5
mIdp.numColumns = mIdp.numRows
mModelHelper.loadModelSync()
}
/**
* Sets up the given workspaces with the given spaces, and fills the remaining space with items.
*/
fun setupWorkspacesWithSpaces(
screen0: List<Rect>? = null,
screen1: List<Rect>? = null,
screen2: List<Rect>? = null,
screen3: List<Rect>? = null,
) {
listOf(screen0, screen1, screen2, screen3).let(this::setupWithSpaces)
mModelHelper.setupDefaultLayoutProvider(mLayoutBuilder)
mIdp.numRows = 5
mIdp.numColumns = mIdp.numRows
mModelHelper.loadModelSync()
}
private fun setupWithSpaces(workspaceSpaces: List<List<Rect>?>) {
workspaceSpaces.forEachIndexed { screenId, spaces ->
if (spaces != null) {
setupWorkspace(screenId, spaces)
}
}
}
private fun setupWorkspace(screenId: Int, spaces: List<Rect>) {
val occupancy = GridOccupancy(mIdp.numColumns, mIdp.numRows)
occupancy.markCells(0, 0, mIdp.numColumns, mIdp.numRows, true)
spaces.forEach { spaceRect -> occupancy.markCells(spaceRect, false) }
mExistingScreens.add(screenId)
mScreenOccupancy.append(screenId, occupancy)
for (x in 0 until mIdp.numColumns) {
for (y in 0 until mIdp.numRows) {
if (occupancy.cells[x][y]) {
mLayoutBuilder.atWorkspace(x, y, screenId).putApp(TEST_PACKAGE, TEST_ACTIVITY)
}
}
}
}
fun getExistingItem() =
WorkspaceItemInfo().apply {
intent = AppInfo.makeLaunchIntent(ComponentName(TEST_PACKAGE, TEST_ACTIVITY))
}
fun getNewItem(): WorkspaceItemInfo {
val itemPackage = UUID.randomUUID().toString()
return WorkspaceItemInfo().apply {
intent = AppInfo.makeLaunchIntent(ComponentName(itemPackage, itemPackage))
}
}
}
data class NewItemSpace(val screenId: Int, val cellX: Int, val cellY: Int) {
fun toIntArray() = intArrayOf(screenId, cellX, cellY)
companion object {
fun fromIntArray(array: kotlin.IntArray) = NewItemSpace(array[0], array[1], array[2])
}
}
@@ -1,60 +0,0 @@
package com.android.launcher3.model
import android.content.Context
import android.database.Cursor
import android.database.sqlite.SQLiteDatabase
import androidx.test.platform.app.InstrumentationRegistry
import java.io.BufferedReader
import java.io.InputStreamReader
private val All_COLUMNS =
arrayOf(
"_id",
"title",
"intent",
"container",
"screen",
"cellX",
"cellY",
"spanX",
"spanY",
"itemType",
"appWidgetId",
"icon",
"appWidgetProvider",
"modified",
"restored",
"profileId",
"rank",
"options",
"appWidgetSource"
)
class FactitiousDbController(context: Context, insertFile: String) : ModelDbController(context) {
val inMemoryDb: SQLiteDatabase by lazy {
SQLiteDatabase.createInMemory(SQLiteDatabase.OpenParams.Builder().build()).also { db ->
BufferedReader(
InputStreamReader(
InstrumentationRegistry.getInstrumentation().context.assets.open(insertFile)
)
)
.lines()
.forEach { sqlStatement -> db.execSQL(sqlStatement) }
}
}
override fun query(
table: String,
projection: Array<out String>?,
selection: String?,
selectionArgs: Array<out String>?,
sortOrder: String?
): Cursor {
return inMemoryDb.query(table, All_COLUMNS, selection, selectionArgs, null, null, sortOrder)
}
override fun loadDefaultFavoritesIfNecessary() {
// No-Op
}
}
@@ -1,25 +0,0 @@
/*
* Copyright (C) 2013 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.testcomponent;
import android.appwidget.AppWidgetProvider;
/**
* A simple app widget showing a primary, secondary and neutral color.
*/
public class AppWidgetDynamicColors extends AppWidgetProvider {
}
@@ -1,23 +0,0 @@
/*
* Copyright (C) 2018 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.testcomponent;
import android.appwidget.AppWidgetProvider;
/**
* A simple app widget without any configuration screen and is hidden in picker.
*/
public class AppWidgetHidden extends AppWidgetProvider { }
@@ -1,26 +0,0 @@
/*
* Copyright (C) 2017 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.testcomponent;
import android.appwidget.AppWidgetProvider;
/**
* A simple app widget without any configuration screen.
*/
public class AppWidgetNoConfig extends AppWidgetProvider {
}
@@ -1,23 +0,0 @@
/*
* Copyright (C) 2017 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.testcomponent;
/**
* A simple app widget with configuration sceen.
*/
public class AppWidgetWithConfig extends AppWidgetNoConfig {
}
@@ -1,41 +0,0 @@
/*
* 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.testcomponent;
import android.app.PendingIntent;
import android.appwidget.AppWidgetManager;
import android.content.Context;
import android.content.Intent;
import android.widget.RemoteViews;
/**
* A simple app widget with shows a dialog on clicking.
*/
public class AppWidgetWithDialog extends AppWidgetNoConfig {
@Override
public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
int layoutId = context.getResources().getIdentifier(
"test_layout_appwidget_blue", "layout", context.getPackageName());
RemoteViews views = new RemoteViews(context.getPackageName(), layoutId);
PendingIntent pi = PendingIntent.getActivity(context, 0,
new Intent(context, DialogTestActivity.class), PendingIntent.FLAG_IMMUTABLE);
views.setOnClickPendingIntent(android.R.id.content, pi);
AppWidgetManager.getInstance(context).updateAppWidget(appWidgetIds, views);
}
}
@@ -1,147 +0,0 @@
/*
* Copyright (C) 2017 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.testcomponent;
import android.app.Activity;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.graphics.Color;
import android.os.Bundle;
import android.util.TypedValue;
import android.view.View;
import android.view.WindowInsets;
import android.widget.Button;
import android.widget.EditText;
import android.widget.LinearLayout;
import android.widget.LinearLayout.LayoutParams;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
/**
* Base activity with utility methods to help automate testing.
*/
public class BaseTestingActivity extends Activity implements View.OnClickListener {
public static final String SUFFIX_COMMAND = "-command";
public static final String EXTRA_METHOD = "method";
public static final String EXTRA_PARAM = "param_";
private static final int MARGIN_DP = 20;
private final String mAction = this.getClass().getName();
private LinearLayout mView;
private int mMargin;
private final BroadcastReceiver mCommandReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
handleCommand(intent);
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mMargin = Math.round(TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP, MARGIN_DP, getResources().getDisplayMetrics()));
mView = new LinearLayout(this);
mView.setPadding(mMargin, mMargin, mMargin, mMargin);
mView.setOrientation(LinearLayout.VERTICAL);
mView.setBackgroundColor(Color.BLUE);
setContentView(mView);
registerReceiver(
mCommandReceiver,
new IntentFilter(mAction + SUFFIX_COMMAND),
RECEIVER_EXPORTED);
}
protected void addButton(String title, String method) {
Button button = new Button(this);
button.setText(title);
button.setTag(method);
button.setOnClickListener(this);
LayoutParams lp = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
lp.bottomMargin = mMargin;
mView.addView(button, lp);
}
protected void addEditor(String initText, String hint, boolean requestIme) {
EditText editText = new EditText(this);
editText.setHint(hint);
editText.setText(initText);
LayoutParams lp = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
lp.bottomMargin = mMargin;
mView.addView(editText, lp);
if (requestIme) {
editText.requestFocus();
mView.getWindowInsetsController().show(WindowInsets.Type.ime());
}
}
@Override
protected void onResume() {
super.onResume();
sendBroadcast(new Intent(mAction).putExtra(Intent.EXTRA_INTENT, getIntent()));
}
@Override
protected void onDestroy() {
unregisterReceiver(mCommandReceiver);
super.onDestroy();
}
@Override
public void onClick(View view) {
handleCommand(new Intent().putExtra(EXTRA_METHOD, (String) view.getTag()));
}
private void handleCommand(Intent cmd) {
String methodName = cmd.getStringExtra(EXTRA_METHOD);
try {
Method method = null;
for (Method m : this.getClass().getDeclaredMethods()) {
if (methodName.equals(m.getName()) &&
!Modifier.isStatic(m.getModifiers()) &&
Modifier.isPublic(m.getModifiers())) {
method = m;
break;
}
}
Object[] args = new Object[method.getParameterTypes().length];
Bundle extras = cmd.getExtras();
for (int i = 0; i < args.length; i++) {
args[i] = extras.get(EXTRA_PARAM + i);
}
method.invoke(this, args);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
public static Intent getCommandIntent(Class<?> clazz, String method) {
return new Intent(clazz.getName() + SUFFIX_COMMAND)
.putExtra(EXTRA_METHOD, method);
}
}
@@ -1,66 +0,0 @@
/*
* Copyright (C) 2019 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.testcomponent;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ShortcutInfo;
import android.content.pm.ShortcutManager;
import android.graphics.drawable.Icon;
import android.os.Bundle;
import com.android.launcher3.R;
import java.util.UUID;
/**
* A custom shortcut is a 1x1 widget that launches a specific intent when user tap on it.
* Custom shortcuts are replaced by deep shortcuts after api 25.
*/
public class CustomShortcutConfigActivity extends BaseTestingActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Intent launchIntent = new Intent(this, BaseTestingActivity.class)
.setAction("com.android.launcher3.intent.action.test_shortcut");
Intent shortcutIntent = createShortcutResultIntent(
this, UUID.randomUUID().toString(), "Shortcut",
R.drawable.ic_widget, launchIntent);
setResult(RESULT_OK, shortcutIntent);
finish();
}
private static Intent createShortcutResultIntent(
Context context, String uniqueId, String name, int iconId, Intent launchIntent) {
ShortcutInfo shortcutInfo =
createShortcutInfo(context, uniqueId, name, iconId, launchIntent);
ShortcutManager sm = context.getSystemService(ShortcutManager.class);
return sm.createShortcutResultIntent(shortcutInfo);
}
private static ShortcutInfo createShortcutInfo(
Context context, String uniqueId, String name, int iconId, Intent launchIntent) {
return new ShortcutInfo.Builder(context, uniqueId)
.setShortLabel(name)
.setLongLabel(name)
.setIcon(Icon.createWithResource(context, iconId))
.setIntent(launchIntent)
.build();
}
}
@@ -1,23 +0,0 @@
/*
* 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.testcomponent;
/**
* Extension of BaseTestingActivity with a Dialog theme
*/
public class DialogTestActivity extends BaseTestingActivity {}
@@ -1,27 +0,0 @@
/*
* 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.testcomponent;
import android.os.Bundle;
public class ImeTestActivity extends OtherBaseTestingActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Requests to focus an editor and show IME for test.
addEditor("Focused editor for test", "Focused editor for test", true);
}
}
@@ -1,95 +0,0 @@
/*
* Copyright (C) 2019 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.testcomponent;
import android.content.Intent;
import android.os.IBinder;
import android.widget.RemoteViews;
import android.widget.RemoteViewsService;
public class ListViewService extends RemoteViewsService {
public static IBinder sBinderForTest;
@Override
public RemoteViewsFactory onGetViewFactory(Intent intent) {
return new SimpleViewsFactory();
}
@Override
public IBinder onBind(Intent intent) {
return sBinderForTest != null ? sBinderForTest : super.onBind(intent);
}
public static class SimpleViewsFactory implements RemoteViewsFactory {
public int viewCount = 0;
@Override
public void onCreate() { }
@Override
public void onDataSetChanged() { }
@Override
public void onDestroy() { }
@Override
public int getCount() {
return viewCount;
}
@Override
public RemoteViews getViewAt(int i) {
RemoteViews views = new RemoteViews("android", android.R.layout.simple_list_item_1);
views.setTextViewText(android.R.id.text1, getLabel(i));
return views;
}
public String getLabel(int i) {
return "Item " + i;
}
@Override
public RemoteViews getLoadingView() {
return null;
}
@Override
public int getViewTypeCount() {
return 1;
}
@Override
public long getItemId(int i) {
return i;
}
@Override
public boolean hasStableIds() {
return false;
}
public IBinder toBinder() {
return new RemoteViewsService() {
@Override
public RemoteViewsFactory onGetViewFactory(Intent intent) {
return SimpleViewsFactory.this;
}
}.onBind(new Intent("stub_intent"));
}
}
}
@@ -1,22 +0,0 @@
/*
* Copyright (C) 2018 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.testcomponent;
/**
* Extension of BaseTestingActivity to help test many activities open at once.
*/
public class OtherBaseTestingActivity extends BaseTestingActivity {}
@@ -1,105 +0,0 @@
/*
* Copyright (C) 2017 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.testcomponent;
import android.annotation.TargetApi;
import android.app.PendingIntent;
import android.appwidget.AppWidgetManager;
import android.content.ComponentName;
import android.content.IntentSender;
import android.content.pm.ShortcutInfo;
import android.content.pm.ShortcutManager;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.drawable.Icon;
import android.os.Bundle;
import android.widget.RemoteViews;
/**
* Sample activity to request pinning an item.
*/
@TargetApi(26)
public class RequestPinItemActivity extends BaseTestingActivity {
private PendingIntent mCallback = null;
private String mShortcutId = "test-id";
private int mRemoteViewColor = Color.TRANSPARENT;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
addButton("Pin Shortcut", "pinShortcut");
addButton("Pin Widget without config ", "pinWidgetNoConfig");
addButton("Pin Widget with config", "pinWidgetWithConfig");
}
public void setCallback(PendingIntent callback) {
mCallback = callback;
}
public void setRemoteViewColor(int color) {
mRemoteViewColor = color;
}
public void setShortcutId(String id) {
mShortcutId = id;
}
public void pinShortcut() {
ShortcutManager sm = getSystemService(ShortcutManager.class);
// Generate icon
int r = sm.getIconMaxWidth() / 2;
Bitmap icon = Bitmap.createBitmap(r * 2, r * 2, Bitmap.Config.ARGB_8888);
Paint p = new Paint();
p.setColor(Color.RED);
new Canvas(icon).drawCircle(r, r, r, p);
ShortcutInfo info = new ShortcutInfo.Builder(this, mShortcutId)
.setIntent(getPackageManager().getLaunchIntentForPackage(getPackageName()))
.setIcon(Icon.createWithBitmap(icon))
.setShortLabel("Test shortcut")
.build();
IntentSender callback = mCallback == null ? null : mCallback.getIntentSender();
sm.requestPinShortcut(info, callback);
}
public void pinWidgetNoConfig() {
requestWidget(new ComponentName(this, AppWidgetNoConfig.class));
}
public void pinWidgetWithConfig() {
requestWidget(new ComponentName(this, AppWidgetWithConfig.class));
}
private void requestWidget(ComponentName cn) {
Bundle extras = null;
if (mRemoteViewColor != Color.TRANSPARENT) {
int layoutId = getResources().getIdentifier(
"test_layout_appwidget_view", "layout", getPackageName());
RemoteViews views = new RemoteViews(getPackageName(), layoutId);
views.setInt(android.R.id.icon, "setBackgroundColor", mRemoteViewColor);
extras = new Bundle();
extras.putParcelable(AppWidgetManager.EXTRA_APPWIDGET_PREVIEW, views);
}
AppWidgetManager.getInstance(this).requestPinAppWidget(cn, extras, mCallback);
}
}
@@ -1,131 +0,0 @@
/*
* Copyright (C) 2019 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.testcomponent;
import static android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_DISABLED;
import static android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_ENABLED;
import static android.content.pm.PackageManager.DONT_KILL_APP;
import static android.os.ParcelFileDescriptor.MODE_READ_WRITE;
import static com.android.launcher3.testcomponent.TestCommandReceiver.DISABLE_TEST_LAUNCHER;
import static com.android.launcher3.testcomponent.TestCommandReceiver.ENABLE_TEST_LAUNCHER;
import static com.android.launcher3.testcomponent.TestCommandReceiver.EXTRA_VALUE;
import static com.android.launcher3.testcomponent.TestCommandReceiver.GET_SYSTEM_HEALTH_MESSAGE;
import static com.android.launcher3.testcomponent.TestCommandReceiver.KILL_PROCESS;
import static com.android.launcher3.testcomponent.TestCommandReceiver.SET_LIST_VIEW_SERVICE_BINDER;
import android.app.Activity;
import android.app.ActivityManager;
import android.content.ComponentName;
import android.content.ContentProvider;
import android.content.ContentValues;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.os.ParcelFileDescriptor;
import android.util.Base64;
import com.android.launcher3.tapl.TestHelpers;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
public class TestCommandProvider extends ContentProvider {
@Override
public boolean onCreate() {
return true;
}
@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
throw new UnsupportedOperationException("unimplemented mock method");
}
@Override
public String getType(Uri uri) {
throw new UnsupportedOperationException("unimplemented mock method");
}
@Override
public Uri insert(Uri uri, ContentValues values) {
throw new UnsupportedOperationException("unimplemented mock method");
}
@Override
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
String sortOrder) {
throw new UnsupportedOperationException("unimplemented mock method");
}
@Override
public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
throw new UnsupportedOperationException("unimplemented mock method");
}
@Override
public Bundle call(String method, String arg, Bundle extras) {
switch (method) {
case ENABLE_TEST_LAUNCHER: {
getContext().getPackageManager().setComponentEnabledSetting(
new ComponentName(getContext(), TestLauncherActivity.class),
COMPONENT_ENABLED_STATE_ENABLED, DONT_KILL_APP);
return null;
}
case DISABLE_TEST_LAUNCHER: {
getContext().getPackageManager().setComponentEnabledSetting(
new ComponentName(getContext(), TestLauncherActivity.class),
COMPONENT_ENABLED_STATE_DISABLED, DONT_KILL_APP);
return null;
}
case KILL_PROCESS: {
((ActivityManager) getContext().getSystemService(Activity.ACTIVITY_SERVICE))
.killBackgroundProcesses(arg);
return null;
}
case GET_SYSTEM_HEALTH_MESSAGE: {
final Bundle response = new Bundle();
response.putString("result",
TestHelpers.getSystemHealthMessage(getContext(), Long.parseLong(arg)));
return response;
}
case SET_LIST_VIEW_SERVICE_BINDER: {
ListViewService.sBinderForTest = extras.getBinder(EXTRA_VALUE);
return null;
}
}
return super.call(method, arg, extras);
}
@Override
public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {
String path = Base64.encodeToString(uri.getPath().getBytes(),
Base64.NO_CLOSE | Base64.NO_PADDING | Base64.NO_WRAP);
File file = new File(getContext().getCacheDir(), path);
if (!file.exists()) {
// Create an empty file so that we can pass its descriptor
try {
file.createNewFile();
} catch (IOException e) {
}
}
return ParcelFileDescriptor.open(file, MODE_READ_WRITE);
}
}
@@ -1,50 +0,0 @@
/*
* Copyright (C) 2018 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.testcomponent;
import android.app.Instrumentation;
import android.net.Uri;
import android.os.Bundle;
import androidx.test.InstrumentationRegistry;
/**
* Content provider to receive commands from tests
*/
public class TestCommandReceiver {
public static final String ENABLE_TEST_LAUNCHER = "enable-test-launcher";
public static final String DISABLE_TEST_LAUNCHER = "disable-test-launcher";
public static final String KILL_PROCESS = "kill-process";
public static final String GET_SYSTEM_HEALTH_MESSAGE = "get-system-health-message";
public static final String SET_LIST_VIEW_SERVICE_BINDER = "set-list-view-service-binder";
public static final String EXTRA_VALUE = "value";
public static Bundle callCommand(String command) {
return callCommand(command, null);
}
public static Bundle callCommand(String command, String arg) {
return callCommand(command, arg, null);
}
public static Bundle callCommand(String command, String arg, Bundle extras) {
Instrumentation inst = InstrumentationRegistry.getInstrumentation();
Uri uri = Uri.parse("content://" + inst.getContext().getPackageName() + ".commands");
return inst.getTargetContext().getContentResolver().call(uri, command, arg, extras);
}
}
@@ -1,34 +0,0 @@
/*
* Copyright (C) 2018 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.testcomponent;
import static android.content.Intent.ACTION_MAIN;
import static android.content.Intent.CATEGORY_LAUNCHER;
import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
import static android.content.Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED;
import android.app.LauncherActivity;
import android.content.Intent;
public class TestLauncherActivity extends LauncherActivity {
@Override
protected Intent getTargetIntent() {
return new Intent(ACTION_MAIN, null)
.addCategory(CATEGORY_LAUNCHER)
.addFlags(FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_RESET_TASK_IF_NEEDED);
}
}
@@ -1,275 +0,0 @@
/*
* Copyright (C) 2017 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.testcomponent;
import android.graphics.Point;
import android.util.Pair;
import android.view.InputDevice;
import android.view.MotionEvent;
import android.view.MotionEvent.PointerCoords;
import android.view.MotionEvent.PointerProperties;
import java.util.HashMap;
import java.util.Map;
/**
* Utility class to generate MotionEvent event sequences for testing touch gesture detectors.
*/
public class TouchEventGenerator {
/**
* Amount of time between two generated events.
*/
private static final long TIME_INCREMENT_MS = 20L;
/**
* Id of the fake device generating the events.
*/
private static final int DEVICE_ID = 2104;
/**
* The fingers currently present on the emulated touch screen.
*/
private Map<Integer, Point> mFingers;
/**
* Initial event time for the current sequence.
*/
private long mInitialTime;
/**
* Time of the last generated event.
*/
private long mLastEventTime;
/**
* Time of the next event.
*/
private long mTime;
/**
* Receives the generated events.
*/
public interface Listener {
/**
* Called when an event was generated.
*/
void onTouchEvent(MotionEvent event);
}
private final Listener mListener;
public TouchEventGenerator(Listener listener) {
mListener = listener;
mFingers = new HashMap<Integer, Point>();
}
/**
* Adds a finger on the touchscreen.
*/
public TouchEventGenerator put(int id, int x, int y, long ms) {
checkFingerExistence(id, false);
boolean isInitialDown = mFingers.isEmpty();
mFingers.put(id, new Point(x, y));
int action;
if (isInitialDown) {
action = MotionEvent.ACTION_DOWN;
} else {
action = MotionEvent.ACTION_POINTER_DOWN;
// Set the id of the changed pointer.
action |= id << MotionEvent.ACTION_POINTER_INDEX_SHIFT;
}
generateEvent(action, ms);
return this;
}
/**
* Adds a finger on the touchscreen after advancing default time interval.
*/
public TouchEventGenerator put(int id, int x, int y) {
return put(id, x, y, TIME_INCREMENT_MS);
}
/**
* Adjusts the position of a finger for an upcoming move event.
*
* @see #move(long ms)
*/
public TouchEventGenerator position(int id, int x, int y) {
checkFingerExistence(id, true);
mFingers.get(id).set(x, y);
return this;
}
/**
* Commits the finger position changes of {@link #position(int, int, int)} by generating a move
* event.
*
* @see #position(int, int, int)
*/
public TouchEventGenerator move(long ms) {
generateEvent(MotionEvent.ACTION_MOVE, ms);
return this;
}
/**
* Commits the finger position changes of {@link #position(int, int, int)} by generating a move
* event after advancing the default time interval.
*
* @see #position(int, int, int)
*/
public TouchEventGenerator move() {
return move(TIME_INCREMENT_MS);
}
/**
* Moves a single finger on the touchscreen.
*/
public TouchEventGenerator move(int id, int x, int y, long ms) {
return position(id, x, y).move(ms);
}
/**
* Moves a single finger on the touchscreen after advancing default time interval.
*/
public TouchEventGenerator move(int id, int x, int y) {
return move(id, x, y, TIME_INCREMENT_MS);
}
/**
* Removes an existing finger from the touchscreen.
*/
public TouchEventGenerator lift(int id, long ms) {
checkFingerExistence(id, true);
boolean isFinalUp = mFingers.size() == 1;
int action;
if (isFinalUp) {
action = MotionEvent.ACTION_UP;
} else {
action = MotionEvent.ACTION_POINTER_UP;
// Set the id of the changed pointer.
action |= id << MotionEvent.ACTION_POINTER_INDEX_SHIFT;
}
generateEvent(action, ms);
mFingers.remove(id);
return this;
}
/**
* Removes a finger from the touchscreen.
*/
public TouchEventGenerator lift(int id, int x, int y, long ms) {
checkFingerExistence(id, true);
mFingers.get(id).set(x, y);
return lift(id, ms);
}
/**
* Removes an existing finger from the touchscreen after advancing default time interval.
*/
public TouchEventGenerator lift(int id) {
return lift(id, TIME_INCREMENT_MS);
}
/**
* Cancels an ongoing sequence.
*/
public TouchEventGenerator cancel(long ms) {
generateEvent(MotionEvent.ACTION_CANCEL, ms);
mFingers.clear();
return this;
}
/**
* Cancels an ongoing sequence.
*/
public TouchEventGenerator cancel() {
return cancel(TIME_INCREMENT_MS);
}
private void checkFingerExistence(int id, boolean shouldExist) {
if (shouldExist != mFingers.containsKey(id)) {
throw new IllegalArgumentException(
shouldExist ? "Finger does not exist" : "Finger already exists");
}
}
private void generateEvent(int action, long ms) {
mTime = mLastEventTime + ms;
Pair<PointerProperties[], PointerCoords[]> state = getFingerState();
MotionEvent event = MotionEvent.obtain(
mInitialTime,
mTime,
action,
state.first.length,
state.first,
state.second,
0 /* metaState */,
0 /* buttonState */,
1.0f /* xPrecision */,
1.0f /* yPrecision */,
DEVICE_ID,
0 /* edgeFlags */,
InputDevice.SOURCE_TOUCHSCREEN,
0 /* flags */);
mListener.onTouchEvent(event);
if (action == MotionEvent.ACTION_UP) {
resetTime();
}
event.recycle();
mLastEventTime = mTime;
}
/**
* Returns the description of the fingers' state expected by MotionEvent.
*/
private Pair<PointerProperties[], PointerCoords[]> getFingerState() {
int nFingers = mFingers.size();
PointerProperties[] properties = new PointerProperties[nFingers];
PointerCoords[] coordinates = new PointerCoords[nFingers];
int index = 0;
for (Map.Entry<Integer, Point> entry : mFingers.entrySet()) {
int id = entry.getKey();
Point location = entry.getValue();
PointerProperties property = new PointerProperties();
property.id = id;
property.toolType = MotionEvent.TOOL_TYPE_FINGER;
properties[index] = property;
PointerCoords coordinate = new PointerCoords();
coordinate.x = location.x;
coordinate.y = location.y;
coordinate.pressure = 1.0f;
coordinates[index] = coordinate;
index++;
}
return new Pair<MotionEvent.PointerProperties[], MotionEvent.PointerCoords[]>(
properties, coordinates);
}
/**
* Resets the time references for a new sequence.
*/
private void resetTime() {
mInitialTime = 0L;
mLastEventTime = -1L;
mTime = 0L;
}
}
@@ -1,44 +0,0 @@
/*
* Copyright (C) 2017 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.testcomponent;
import android.os.Bundle;
/**
* Simple activity for widget configuration
*/
public class WidgetConfigActivity extends BaseTestingActivity {
public static final String SUFFIX_FINISH = "-finish";
public static final String EXTRA_CODE = "code";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
addButton("Cancel", "clickCancel");
addButton("OK", "clickOK");
}
public void clickCancel() {
setResult(RESULT_CANCELED);
finish();
}
public void clickOK() {
setResult(RESULT_OK);
finish();
}
}
@@ -1,755 +0,0 @@
/*
* Copyright (C) 2017 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.ui;
import static android.platform.test.flag.junit.SetFlagsRule.DefaultInitValueType.DEVICE_DEFAULT;
import static androidx.test.InstrumentationRegistry.getInstrumentation;
import static com.android.launcher3.testing.shared.TestProtocol.ICON_MISSING;
import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.ActivityInfo;
import android.content.pm.LauncherActivityInfo;
import android.content.pm.LauncherApps;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.graphics.Point;
import android.os.Debug;
import android.os.Process;
import android.os.RemoteException;
import android.os.UserHandle;
import android.os.UserManager;
import android.platform.test.flag.junit.SetFlagsRule;
import android.system.OsConstants;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.test.InstrumentationRegistry;
import androidx.test.uiautomator.By;
import androidx.test.uiautomator.BySelector;
import androidx.test.uiautomator.UiDevice;
import androidx.test.uiautomator.Until;
import com.android.launcher3.Launcher;
import com.android.launcher3.LauncherState;
import com.android.launcher3.Utilities;
import com.android.launcher3.statemanager.StateManager;
import com.android.launcher3.tapl.HomeAllApps;
import com.android.launcher3.tapl.HomeAppIcon;
import com.android.launcher3.tapl.LauncherInstrumentation;
import com.android.launcher3.tapl.LauncherInstrumentation.ContainerType;
import com.android.launcher3.tapl.TestHelpers;
import com.android.launcher3.testcomponent.TestCommandReceiver;
import com.android.launcher3.testing.shared.TestProtocol;
import com.android.launcher3.util.LooperExecutor;
import com.android.launcher3.util.SimpleBroadcastReceiver;
import com.android.launcher3.util.TestUtil;
import com.android.launcher3.util.Wait;
import com.android.launcher3.util.rule.FailureWatcher;
import com.android.launcher3.util.rule.SamplerRule;
import com.android.launcher3.util.rule.ScreenRecordRule;
import com.android.launcher3.util.rule.ShellCommandRule;
import com.android.launcher3.util.rule.TestIsolationRule;
import com.android.launcher3.util.rule.TestStabilityRule;
import com.android.launcher3.util.rule.ViewCaptureRule;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Rule;
import org.junit.rules.RuleChain;
import org.junit.rules.TestRule;
import java.io.IOException;
import java.util.concurrent.Callable;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;
/**
* Base class for all instrumentation tests providing various utility methods.
*/
public abstract class AbstractLauncherUiTest {
public static final long DEFAULT_ACTIVITY_TIMEOUT = TimeUnit.SECONDS.toMillis(10);
public static final long DEFAULT_BROADCAST_TIMEOUT_SECS = 5;
public static final long DEFAULT_UI_TIMEOUT = TestUtil.DEFAULT_UI_TIMEOUT;
private static final String TAG = "AbstractLauncherUiTest";
private static boolean sDumpWasGenerated = false;
private static boolean sActivityLeakReported = false;
private static boolean sSeenKeyguard = false;
private static boolean sFirstTimeWaitingForWizard = true;
private static final String SYSTEMUI_PACKAGE = "com.android.systemui";
protected LooperExecutor mMainThreadExecutor = MAIN_EXECUTOR;
protected final UiDevice mDevice = getUiDevice();
protected final LauncherInstrumentation mLauncher = createLauncherInstrumentation();
@NonNull
public static LauncherInstrumentation createLauncherInstrumentation() {
waitForSetupWizardDismissal(); // precondition for creating LauncherInstrumentation
return new LauncherInstrumentation(true);
}
protected Context mTargetContext;
protected String mTargetPackage;
private int mLauncherPid;
/** Detects activity leaks and throws an exception if a leak is found. */
public static void checkDetectedLeaks(LauncherInstrumentation launcher) {
checkDetectedLeaks(launcher, false);
}
/** Detects activity leaks and throws an exception if a leak is found. */
public static void checkDetectedLeaks(LauncherInstrumentation launcher,
boolean requireOneActiveActivityUnused) {
if (TestStabilityRule.isPresubmit()) return; // b/313501215
final boolean requireOneActiveActivity =
false; // workaround for leaks when there is an unexpected Recents activity
if (sActivityLeakReported) return;
// Check whether activity leak detector has found leaked activities.
Wait.atMost(() -> getActivityLeakErrorMessage(launcher, requireOneActiveActivity),
() -> {
launcher.forceGc();
return MAIN_EXECUTOR.submit(
() -> launcher.noLeakedActivities(requireOneActiveActivity)).get();
}, DEFAULT_UI_TIMEOUT, launcher);
}
public static String getAppPackageName() {
return getInstrumentation().getContext().getPackageName();
}
private static String getActivityLeakErrorMessage(LauncherInstrumentation launcher,
boolean requireOneActiveActivity) {
sActivityLeakReported = true;
return "Activity leak detector has found leaked activities, requirining 1 activity: "
+ requireOneActiveActivity + "; "
+ dumpHprofData(launcher, false, requireOneActiveActivity) + ".";
}
private static String dumpHprofData(LauncherInstrumentation launcher, boolean intentionalLeak,
boolean requireOneActiveActivity) {
if (intentionalLeak) return "intentional leak; not generating dump";
String result;
if (sDumpWasGenerated) {
result = "dump has already been generated by another test";
} else {
try {
final String fileName =
getInstrumentation().getTargetContext().getFilesDir().getPath()
+ "/ActivityLeakHeapDump.hprof";
if (TestHelpers.isInLauncherProcess()) {
Debug.dumpHprofData(fileName);
} else {
final UiDevice device = getUiDevice();
device.executeShellCommand(
"am dumpheap " + device.getLauncherPackageName() + " " + fileName);
}
Log.d(TAG, "Saved leak dump, the leak is still present: "
+ !launcher.noLeakedActivities(requireOneActiveActivity));
sDumpWasGenerated = true;
result = "saved memory dump as an artifact";
} catch (Throwable e) {
Log.e(TAG, "dumpHprofData failed", e);
result = "failed to save memory dump";
}
}
return result + ". Full list of activities: " + launcher.getRootedActivitiesList();
}
protected AbstractLauncherUiTest() {
mLauncher.enableCheckEventsForSuccessfulGestures();
try {
mDevice.setOrientationNatural();
} catch (RemoteException e) {
throw new RuntimeException(e);
}
if (TestHelpers.isInLauncherProcess()) {
Utilities.enableRunningInTestHarnessForTests();
mLauncher.setSystemHealthSupplier(startTime -> TestCommandReceiver.callCommand(
TestCommandReceiver.GET_SYSTEM_HEALTH_MESSAGE, startTime.toString())
.getString("result"));
mLauncher.setOnSettledStateAction(
containerType -> executeOnLauncher(
launcher ->
checkLauncherIntegrity(launcher, containerType)));
}
mLauncher.enableDebugTracing();
// Avoid double-reporting of Launcher crashes.
mLauncher.setOnLauncherCrashed(() -> mLauncherPid = 0);
}
@Rule
public ShellCommandRule mDisableHeadsUpNotification =
ShellCommandRule.disableHeadsUpNotification();
@Rule
public ScreenRecordRule mScreenRecordRule = new ScreenRecordRule();
@Rule
public SetFlagsRule mSetFlagsRule = new SetFlagsRule(DEVICE_DEFAULT);
public static void initialize(AbstractLauncherUiTest test) throws Exception {
initialize(test, false);
}
public static void initialize(
AbstractLauncherUiTest test, boolean clearWorkspace) throws Exception {
test.reinitializeLauncherData(clearWorkspace);
test.mDevice.pressHome();
test.waitForLauncherCondition("Launcher didn't start", launcher -> launcher != null);
test.waitForState("Launcher internal state didn't switch to Home",
() -> LauncherState.NORMAL);
test.waitForResumed("Launcher internal state is still Background");
// Check that we switched to home.
test.mLauncher.getWorkspace();
AbstractLauncherUiTest.checkDetectedLeaks(test.mLauncher, true);
}
protected void clearPackageData(String pkg) throws IOException, InterruptedException {
final CountDownLatch count = new CountDownLatch(2);
final SimpleBroadcastReceiver broadcastReceiver =
new SimpleBroadcastReceiver(i -> count.countDown());
broadcastReceiver.registerPkgActions(mTargetContext, pkg,
Intent.ACTION_PACKAGE_RESTARTED, Intent.ACTION_PACKAGE_DATA_CLEARED);
mDevice.executeShellCommand("pm clear " + pkg);
assertTrue(pkg + " didn't restart", count.await(10, TimeUnit.SECONDS));
mTargetContext.unregisterReceiver(broadcastReceiver);
}
protected TestRule getRulesInsideActivityMonitor() {
final ViewCaptureRule viewCaptureRule = new ViewCaptureRule(
Launcher.ACTIVITY_TRACKER::getCreatedActivity);
final RuleChain inner = RuleChain
.outerRule(new PortraitLandscapeRunner(this))
.around(new FailureWatcher(mLauncher, viewCaptureRule::getViewCaptureData))
// .around(viewCaptureRule) // b/315482167
.around(new TestIsolationRule(mLauncher, true));
return TestHelpers.isInLauncherProcess()
? RuleChain.outerRule(ShellCommandRule.setDefaultLauncher()).around(inner)
: inner;
}
@Rule
public TestRule mOrderSensitiveRules = RuleChain
.outerRule(new SamplerRule())
.around(new TestStabilityRule())
.around(getRulesInsideActivityMonitor());
public UiDevice getDevice() {
return mDevice;
}
@Before
public void setUp() throws Exception {
mLauncher.onTestStart();
final String launcherPackageName = mDevice.getLauncherPackageName();
try {
final Context context = InstrumentationRegistry.getContext();
final PackageManager pm = context.getPackageManager();
final PackageInfo launcherPackage = pm.getPackageInfo(launcherPackageName, 0);
if (!launcherPackage.versionName.equals("BuildFromAndroidStudio")) {
Assert.assertEquals("Launcher version doesn't match tests version",
pm.getPackageInfo(context.getPackageName(), 0).getLongVersionCode(),
launcherPackage.getLongVersionCode());
}
} catch (PackageManager.NameNotFoundException e) {
throw new RuntimeException(e);
}
mLauncherPid = 0;
mTargetContext = InstrumentationRegistry.getTargetContext();
mTargetPackage = mTargetContext.getPackageName();
mLauncherPid = mLauncher.getPid();
UserManager userManager = mTargetContext.getSystemService(UserManager.class);
if (userManager != null) {
for (UserHandle userHandle : userManager.getUserProfiles()) {
if (!userHandle.isSystem()) {
mDevice.executeShellCommand("pm remove-user " + userHandle.getIdentifier());
}
}
}
onTestStart();
}
/** Method that should be called when a test starts. */
public static void onTestStart() {
waitForSetupWizardDismissal();
if (TestStabilityRule.isPresubmit()) {
aggressivelyUnlockSysUi();
} else {
verifyKeyguardInvisible();
}
}
private static boolean hasSystemUiObject(String resId) {
return getUiDevice().hasObject(
By.res(SYSTEMUI_PACKAGE, resId));
}
@NonNull
private static UiDevice getUiDevice() {
return UiDevice.getInstance(getInstrumentation());
}
private static void aggressivelyUnlockSysUi() {
final UiDevice device = getUiDevice();
for (int i = 0; i < 10 && hasSystemUiObject("keyguard_status_view"); ++i) {
Log.d(TAG, "Before attempting to unlock the phone");
try {
device.executeShellCommand("input keyevent 82");
} catch (IOException e) {
throw new RuntimeException(e);
}
device.waitForIdle();
}
Assert.assertTrue("Keyguard still visible",
TestHelpers.wait(
Until.gone(By.res(SYSTEMUI_PACKAGE, "keyguard_status_view")), 60000));
Log.d(TAG, "Keyguard is not visible");
}
/** Waits for setup wizard to go away. */
private static void waitForSetupWizardDismissal() {
if (!TestStabilityRule.isPresubmit()) return;
if (sFirstTimeWaitingForWizard) {
try {
getUiDevice().executeShellCommand(
"am force-stop com.google.android.setupwizard");
} catch (IOException e) {
throw new RuntimeException(e);
}
}
final boolean wizardDismissed = TestHelpers.wait(
Until.gone(By.pkg("com.google.android.setupwizard").depth(0)),
sFirstTimeWaitingForWizard ? 120000 : 0);
sFirstTimeWaitingForWizard = false;
Assert.assertTrue("Setup wizard is still visible", wizardDismissed);
}
private static void verifyKeyguardInvisible() {
final boolean keyguardAlreadyVisible = sSeenKeyguard;
sSeenKeyguard = sSeenKeyguard
|| !TestHelpers.wait(
Until.gone(By.res(SYSTEMUI_PACKAGE, "keyguard_status_view")), 60000);
Assert.assertFalse(
"Keyguard is visible, which is likely caused by a crash in SysUI, seeing keyguard"
+ " for the first time = "
+ !keyguardAlreadyVisible,
sSeenKeyguard);
}
@After
public void verifyLauncherState() {
try {
// Limits UI tests affecting tests running after them.
mLauncher.waitForLauncherInitialized();
if (mLauncherPid != 0) {
assertEquals("Launcher crashed, pid mismatch:",
mLauncherPid, mLauncher.getPid().intValue());
}
} finally {
mLauncher.onTestFinish();
}
}
protected void reinitializeLauncherData() {
reinitializeLauncherData(false);
}
protected void reinitializeLauncherData(boolean clearWorkspace) {
if (clearWorkspace) {
mLauncher.clearLauncherData();
} else {
mLauncher.reinitializeLauncherData();
}
mLauncher.waitForLauncherInitialized();
}
/**
* Runs the callback on the UI thread and returns the result.
*/
protected <T> T getOnUiThread(final Callable<T> callback) {
try {
return mMainThreadExecutor.submit(callback).get(DEFAULT_UI_TIMEOUT,
TimeUnit.MILLISECONDS);
} catch (TimeoutException e) {
Log.e(TAG, "Timeout in getOnUiThread, sending SIGABRT", e);
Process.sendSignal(Process.myPid(), OsConstants.SIGABRT);
throw new RuntimeException(e);
} catch (Throwable e) {
throw new RuntimeException(e);
}
}
protected <T> T getFromLauncher(Function<Launcher, T> f) {
if (!TestHelpers.isInLauncherProcess()) return null;
return getOnUiThread(() -> f.apply(Launcher.ACTIVITY_TRACKER.getCreatedActivity()));
}
protected void executeOnLauncher(Consumer<Launcher> f) {
getFromLauncher(launcher -> {
f.accept(launcher);
return null;
});
}
// Execute an action on Launcher, but forgive it when launcher is null.
// Launcher can be null if teardown is happening after a failed setup step where launcher
// activity failed to be created.
protected void executeOnLauncherInTearDown(Consumer<Launcher> f) {
executeOnLauncher(launcher -> {
if (launcher != null) f.accept(launcher);
});
}
// Cannot be used in TaplTests between a Tapl call injecting a gesture and a tapl call
// expecting the results of that gesture because the wait can hide flakeness.
protected void waitForState(String message, Supplier<LauncherState> state) {
waitForLauncherCondition(message,
launcher -> launcher.getStateManager().getCurrentStableState() == state.get());
}
// Cannot be used in TaplTests between a Tapl call injecting a gesture and a tapl call
// expecting the results of that gesture because the wait can hide flakeness.
protected void waitForStateTransitionToEnd(String message, Supplier<LauncherState> state) {
waitForLauncherCondition(message,
launcher -> launcher.getStateManager().isInStableState(state.get())
&& !launcher.getStateManager().isInTransition());
}
protected void waitForResumed(String message) {
waitForLauncherCondition(message, launcher -> launcher.hasBeenResumed());
}
// Cannot be used in TaplTests after injecting any gesture using Tapl because this can hide
// flakiness.
protected void waitForLauncherCondition(String
message, Function<Launcher, Boolean> condition) {
waitForLauncherCondition(message, condition, DEFAULT_ACTIVITY_TIMEOUT);
}
// Cannot be used in TaplTests after injecting any gesture using Tapl because this can hide
// flakiness.
protected <T> T getOnceNotNull(String message, Function<Launcher, T> f) {
return getOnceNotNull(message, f, DEFAULT_ACTIVITY_TIMEOUT);
}
// Cannot be used in TaplTests after injecting any gesture using Tapl because this can hide
// flakiness.
protected void waitForLauncherCondition(
String message, Function<Launcher, Boolean> condition, long timeout) {
verifyKeyguardInvisible();
if (!TestHelpers.isInLauncherProcess()) return;
Wait.atMost(message, () -> getFromLauncher(condition), timeout, mLauncher);
}
// Cannot be used in TaplTests after injecting any gesture using Tapl because this can hide
// flakiness.
protected <T> T getOnceNotNull(String message, Function<Launcher, T> f, long timeout) {
if (!TestHelpers.isInLauncherProcess()) return null;
final Object[] output = new Object[1];
Wait.atMost(message, () -> {
final Object fromLauncher = getFromLauncher(f);
output[0] = fromLauncher;
return fromLauncher != null;
}, timeout, mLauncher);
return (T) output[0];
}
// Cannot be used in TaplTests after injecting any gesture using Tapl because this can hide
// flakiness.
protected void waitForLauncherCondition(
String message,
Runnable testThreadAction, Function<Launcher, Boolean> condition,
long timeout) {
if (!TestHelpers.isInLauncherProcess()) return;
Wait.atMost(message, () -> {
testThreadAction.run();
return getFromLauncher(condition);
}, timeout, mLauncher);
}
protected LauncherActivityInfo getSettingsApp() {
return mTargetContext.getSystemService(LauncherApps.class)
.getActivityList("com.android.settings", Process.myUserHandle()).get(0);
}
/**
* Broadcast receiver which blocks until the result is received.
*/
public class BlockingBroadcastReceiver extends BroadcastReceiver {
private final CountDownLatch latch = new CountDownLatch(1);
private Intent mIntent;
public BlockingBroadcastReceiver(String action) {
mTargetContext.registerReceiver(this, new IntentFilter(action),
Context.RECEIVER_EXPORTED/*UNAUDITED*/);
}
@Override
public void onReceive(Context context, Intent intent) {
mIntent = intent;
latch.countDown();
}
public Intent blockingGetIntent() throws InterruptedException {
latch.await(DEFAULT_BROADCAST_TIMEOUT_SECS, TimeUnit.SECONDS);
mTargetContext.unregisterReceiver(this);
return mIntent;
}
public Intent blockingGetExtraIntent() throws InterruptedException {
Intent intent = blockingGetIntent();
return intent == null ? null : (Intent) intent.getParcelableExtra(
Intent.EXTRA_INTENT);
}
}
public static void startAppFast(String packageName) {
startIntent(
getInstrumentation().getContext().getPackageManager().getLaunchIntentForPackage(
packageName),
By.pkg(packageName).depth(0),
true /* newTask */);
}
public static void startTestActivity(int activityNumber) {
final String packageName = getAppPackageName();
final Intent intent = getInstrumentation().getContext().getPackageManager().
getLaunchIntentForPackage(packageName);
intent.setComponent(new ComponentName(packageName,
"com.android.launcher3.tests.Activity" + activityNumber));
startIntent(intent, By.pkg(packageName).text("TestActivity" + activityNumber),
false /* newTask */);
}
public static void startImeTestActivity() {
final String packageName = getAppPackageName();
final Intent intent = getInstrumentation().getContext().getPackageManager().
getLaunchIntentForPackage(packageName);
intent.setComponent(new ComponentName(packageName,
"com.android.launcher3.testcomponent.ImeTestActivity"));
startIntent(intent, By.pkg(packageName).text("ImeTestActivity"),
false /* newTask */);
}
private static void startIntent(Intent intent, BySelector selector, boolean newTask) {
intent.addCategory(Intent.CATEGORY_LAUNCHER);
if (newTask) {
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
} else {
intent.addFlags(
Intent.FLAG_ACTIVITY_MULTIPLE_TASK | Intent.FLAG_ACTIVITY_NEW_DOCUMENT);
}
getInstrumentation().getTargetContext().startActivity(intent);
assertTrue("App didn't start: " + selector,
TestHelpers.wait(Until.hasObject(selector), DEFAULT_UI_TIMEOUT));
// Wait for the Launcher to stop.
final LauncherInstrumentation launcherInstrumentation = new LauncherInstrumentation();
Wait.atMost("Launcher activity didn't stop",
() -> !launcherInstrumentation.isLauncherActivityStarted(),
DEFAULT_ACTIVITY_TIMEOUT, launcherInstrumentation);
}
public static ActivityInfo resolveSystemAppInfo(String category) {
return getInstrumentation().getContext().getPackageManager().resolveActivity(
new Intent(Intent.ACTION_MAIN).addCategory(category),
PackageManager.MATCH_SYSTEM_ONLY).
activityInfo;
}
public static String resolveSystemApp(String category) {
return resolveSystemAppInfo(category).packageName;
}
protected void closeLauncherActivity() {
// Destroy Launcher activity.
executeOnLauncher(launcher -> {
if (launcher != null) {
onLauncherActivityClose(launcher);
launcher.finish();
}
});
waitForLauncherCondition(
"Launcher still active", launcher -> launcher == null, DEFAULT_UI_TIMEOUT);
}
protected boolean isInLaunchedApp(Launcher launcher) {
return launcher == null || !launcher.hasBeenResumed();
}
protected boolean isInState(Supplier<LauncherState> state) {
if (!TestHelpers.isInLauncherProcess()) return true;
return getFromLauncher(
launcher -> launcher.getStateManager().getState() == state.get());
}
protected int getAllAppsScroll(Launcher launcher) {
return launcher.getAppsView().getActiveRecyclerView().computeVerticalScrollOffset();
}
private void checkLauncherIntegrity(
Launcher launcher, ContainerType expectedContainerType) {
if (launcher != null) {
final StateManager<LauncherState> stateManager = launcher.getStateManager();
final LauncherState stableState = stateManager.getCurrentStableState();
assertTrue("Stable state != state: " + stableState.getClass().getSimpleName() + ", "
+ stateManager.getState().getClass().getSimpleName(),
stableState == stateManager.getState());
final boolean isResumed = launcher.hasBeenResumed();
final boolean isStarted = launcher.isStarted();
checkLauncherState(launcher, expectedContainerType, isResumed, isStarted);
final int ordinal = stableState.ordinal;
switch (expectedContainerType) {
case WORKSPACE:
case WIDGETS: {
assertTrue(
"Launcher is not resumed in state: " + expectedContainerType,
isResumed);
assertTrue(TestProtocol.stateOrdinalToString(ordinal),
ordinal == TestProtocol.NORMAL_STATE_ORDINAL);
break;
}
case HOME_ALL_APPS: {
assertTrue(
"Launcher is not resumed in state: " + expectedContainerType,
isResumed);
assertTrue(TestProtocol.stateOrdinalToString(ordinal),
ordinal == TestProtocol.ALL_APPS_STATE_ORDINAL);
break;
}
case OVERVIEW: {
verifyOverviewState(launcher, expectedContainerType, isStarted, isResumed,
ordinal, TestProtocol.OVERVIEW_STATE_ORDINAL);
break;
}
case SPLIT_SCREEN_SELECT: {
verifyOverviewState(launcher, expectedContainerType, isStarted, isResumed,
ordinal, TestProtocol.OVERVIEW_SPLIT_SELECT_ORDINAL);
break;
}
case TASKBAR_ALL_APPS:
case LAUNCHED_APP: {
assertTrue("Launcher is resumed in state: " + expectedContainerType,
!isResumed);
assertTrue(TestProtocol.stateOrdinalToString(ordinal),
ordinal == TestProtocol.NORMAL_STATE_ORDINAL);
break;
}
default:
throw new IllegalArgumentException(
"Illegal container: " + expectedContainerType);
}
} else {
assertTrue(
"Container type is not LAUNCHED_APP, TASKBAR_ALL_APPS "
+ "or FALLBACK_OVERVIEW: " + expectedContainerType,
expectedContainerType == ContainerType.LAUNCHED_APP
|| expectedContainerType == ContainerType.TASKBAR_ALL_APPS
|| expectedContainerType == ContainerType.FALLBACK_OVERVIEW);
}
}
protected void checkLauncherState(Launcher launcher, ContainerType expectedContainerType,
boolean isResumed, boolean isStarted) {
assertTrue("hasBeenResumed() != isStarted(), hasBeenResumed(): " + isResumed,
isResumed == isStarted);
assertTrue("hasBeenResumed() != isUserActive(), hasBeenResumed(): " + isResumed,
isResumed == launcher.isUserActive());
}
protected void checkLauncherStateInOverview(Launcher launcher,
ContainerType expectedContainerType, boolean isStarted, boolean isResumed) {
assertTrue("Launcher is not resumed in state: " + expectedContainerType,
isResumed);
}
protected void onLauncherActivityClose(Launcher launcher) {
}
protected HomeAppIcon createShortcutInCenterIfNotExist(String name) {
Point dimension = mLauncher.getWorkspace().getIconGridDimensions();
return createShortcutIfNotExist(name, dimension.x / 2, dimension.y / 2);
}
protected HomeAppIcon createShortcutIfNotExist(String name, Point cellPosition) {
return createShortcutIfNotExist(name, cellPosition.x, cellPosition.y);
}
protected HomeAppIcon createShortcutIfNotExist(String name, int cellX, int cellY) {
HomeAppIcon homeAppIcon = mLauncher.getWorkspace().tryGetWorkspaceAppIcon(name);
Log.d(ICON_MISSING, "homeAppIcon: " + homeAppIcon + " name: " + name +
" cell: " + cellX + ", " + cellY);
if (homeAppIcon == null) {
HomeAllApps allApps = mLauncher.getWorkspace().switchToAllApps();
allApps.freeze();
try {
allApps.getAppIcon(name).dragToWorkspace(cellX, cellY);
} finally {
allApps.unfreeze();
}
homeAppIcon = mLauncher.getWorkspace().getWorkspaceAppIcon(name);
}
return homeAppIcon;
}
private void verifyOverviewState(Launcher launcher, ContainerType expectedContainerType,
boolean isStarted, boolean isResumed, int ordinal, int expectedOrdinal) {
checkLauncherStateInOverview(launcher, expectedContainerType, isStarted, isResumed);
assertEquals(TestProtocol.stateOrdinalToString(ordinal), ordinal, expectedOrdinal);
}
}
@@ -1,89 +0,0 @@
package com.android.launcher3.ui;
import android.util.Log;
import android.view.Surface;
import com.android.launcher3.tapl.TestHelpers;
import com.android.launcher3.util.rule.TestStabilityRule;
import org.junit.rules.TestRule;
import org.junit.runner.Description;
import org.junit.runners.model.Statement;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
public class PortraitLandscapeRunner implements TestRule {
private static final String TAG = "PortraitLandscapeRunner";
private AbstractLauncherUiTest mTest;
// Annotation for tests that need to be run in portrait and landscape modes.
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface PortraitLandscape {
}
public PortraitLandscapeRunner(AbstractLauncherUiTest test) {
mTest = test;
}
@Override
public Statement apply(Statement base, Description description) {
if (!TestHelpers.isInLauncherProcess()
|| description.getAnnotation(PortraitLandscape.class) == null
// If running in presubmit, don't run in both orientations.
// It's important to keep presubmits fast even if we will occasionally miss
// regressions in presubmit.
|| TestStabilityRule.isPresubmit()) {
return base;
}
return new Statement() {
@Override
public void evaluate() throws Throwable {
try {
mTest.mDevice.pressHome();
mTest.waitForLauncherCondition("Launcher activity wasn't created",
launcher -> launcher != null);
mTest.executeOnLauncher(launcher ->
launcher.getRotationHelper().forceAllowRotationForTesting(
true));
evaluateInPortrait();
evaluateInLandscape();
} catch (Throwable e) {
Log.e(TAG, "Error", e);
throw e;
} finally {
mTest.mDevice.setOrientationNatural();
mTest.executeOnLauncher(launcher ->
{
if (launcher != null) {
launcher.getRotationHelper().forceAllowRotationForTesting(false);
}
});
mTest.mLauncher.setExpectedRotation(Surface.ROTATION_0);
}
}
private void evaluateInPortrait() throws Throwable {
mTest.mDevice.setOrientationNatural();
mTest.mLauncher.setExpectedRotation(Surface.ROTATION_0);
AbstractLauncherUiTest.checkDetectedLeaks(mTest.mLauncher, true);
base.evaluate();
mTest.getDevice().pressHome();
}
private void evaluateInLandscape() throws Throwable {
mTest.mDevice.setOrientationLeft();
mTest.mLauncher.setExpectedRotation(Surface.ROTATION_90);
AbstractLauncherUiTest.checkDetectedLeaks(mTest.mLauncher, true);
base.evaluate();
mTest.getDevice().pressHome();
}
};
}
}
@@ -1,66 +0,0 @@
/*
* Copyright (C) 2017 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.ui;
import static android.os.Process.myUserHandle;
import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
import static com.android.launcher3.util.TestUtil.getOnUiThread;
import android.app.Instrumentation;
import android.content.ComponentName;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import com.android.launcher3.testcomponent.AppWidgetNoConfig;
import com.android.launcher3.testcomponent.AppWidgetWithConfig;
import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
import com.android.launcher3.widget.WidgetManagerHelper;
import java.util.function.Function;
public class TestViewHelpers {
private static final String TAG = "TestViewHelpers";
/**
* Finds a widget provider which can fit on the home screen.
*
* @param hasConfigureScreen if true, a provider with a config screen is returned.
*/
public static LauncherAppWidgetProviderInfo findWidgetProvider(boolean hasConfigureScreen) {
LauncherAppWidgetProviderInfo info = getOnUiThread(() -> {
Instrumentation i = getInstrumentation();
ComponentName cn = new ComponentName(i.getContext(),
hasConfigureScreen ? AppWidgetWithConfig.class : AppWidgetNoConfig.class);
Log.d(TAG, "findWidgetProvider componentName=" + cn.flattenToString());
return new WidgetManagerHelper(i.getTargetContext()).findProvider(cn, myUserHandle());
});
if (info == null) {
throw new IllegalArgumentException("No valid widget provider");
}
return info;
}
public static View findChildView(ViewGroup parent, Function<View, Boolean> condition) {
for (int i = 0; i < parent.getChildCount(); ++i) {
final View child = parent.getChildAt(i);
if (condition.apply(child)) return child;
}
return null;
}
}
@@ -1,73 +0,0 @@
/*
* Copyright (C) 2021 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.util;
import android.content.Context;
import android.content.ContextWrapper;
import android.view.ContextThemeWrapper;
import com.android.launcher3.DeviceProfile;
import com.android.launcher3.DeviceProfile.OnDeviceProfileChangeListener;
import com.android.launcher3.InvariantDeviceProfile;
import com.android.launcher3.views.ActivityContext;
import com.android.launcher3.views.BaseDragLayer;
import java.util.ArrayList;
import java.util.List;
/**
* {@link ContextWrapper} with internal Launcher interface for testing
*/
public class ActivityContextWrapper extends ContextThemeWrapper implements ActivityContext {
private final List<OnDeviceProfileChangeListener> mDpChangeListeners = new ArrayList<>();
private final DeviceProfile mProfile;
private final MyDragLayer mMyDragLayer;
public ActivityContextWrapper(Context base) {
super(base, android.R.style.Theme_DeviceDefault);
mProfile = InvariantDeviceProfile.INSTANCE.get(base).getDeviceProfile(base).copy(base);
mMyDragLayer = new MyDragLayer(this);
}
@Override
public BaseDragLayer getDragLayer() {
return mMyDragLayer;
}
@Override
public List<OnDeviceProfileChangeListener> getOnDeviceProfileChangeListeners() {
return mDpChangeListeners;
}
@Override
public DeviceProfile getDeviceProfile() {
return mProfile;
}
private static class MyDragLayer extends BaseDragLayer<ActivityContextWrapper> {
MyDragLayer(Context context) {
super(context, null, 1);
}
@Override
public void recreateControllers() {
mControllers = new TouchController[0];
}
}
}
@@ -1,137 +0,0 @@
/*
* 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.util
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.launcher3.util.rule.TestStabilityRule
import java.util.concurrent.ExecutorService
import java.util.concurrent.locks.ReentrantLock
import junit.framework.Assert.assertEquals
import junit.framework.Assert.assertFalse
import junit.framework.Assert.assertTrue
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
/** Unit test for [ExecutorRunnable] */
@SmallTest
@RunWith(AndroidJUnit4::class)
class ExecutorRunnableTest {
private lateinit var underTest: ExecutorRunnable<Int>
private val lock = ReentrantLock()
private var result: Int = -1
private var isTaskExecuted = false
private var isCallbackExecuted = false
@get:Rule(order = 0) val testStabilityRule = TestStabilityRule()
@Before
fun setup() {
reset()
submitJob()
}
private fun submitJob() {
underTest =
ExecutorRunnable.createAndExecute(
Executors.UI_HELPER_EXECUTOR,
{
isTaskExecuted = true
1
},
Executors.VIEW_PREINFLATION_EXECUTOR,
{
isCallbackExecuted = true
result = it + 1
}
)
}
@Test
fun run_and_complete() {
awaitAllExecutorCompleted()
assertTrue("task should be executed", isTaskExecuted)
assertTrue("callback should be executed", isCallbackExecuted)
assertEquals(2, result)
}
@Test
fun run_and_cancel_cancelTaskAndCallback() {
awaitAllExecutorCompleted()
reset()
lock.lock()
Executors.UI_HELPER_EXECUTOR.submit { lock.lock() }
submitJob()
underTest.cancel(false)
lock.unlock() // unblock task on UI_HELPER_EXECUTOR
awaitAllExecutorCompleted()
assertFalse("task should not be executed.", isTaskExecuted)
assertFalse("callback should not be executed.", isCallbackExecuted)
assertEquals(0, result)
}
@Test
fun run_and_cancel_cancelCallback() {
awaitAllExecutorCompleted()
reset()
lock.lock()
Executors.VIEW_PREINFLATION_EXECUTOR.submit { lock.lock() }
submitJob()
awaitExecutorCompleted(Executors.UI_HELPER_EXECUTOR)
assertTrue("task should be executed.", isTaskExecuted)
underTest.cancel(false)
lock.unlock() // unblock callback on VIEW_PREINFLATION_EXECUTOR
awaitExecutorCompleted(Executors.VIEW_PREINFLATION_EXECUTOR)
assertFalse("callback should not be executed.", isCallbackExecuted)
assertEquals(0, result)
}
@Test
fun run_and_cancelAfterCompletion_executeAll() {
awaitAllExecutorCompleted()
underTest.cancel(false)
assertTrue("task should be executed", isTaskExecuted)
assertTrue("callback should be executed", isCallbackExecuted)
assertEquals(2, result)
}
private fun awaitExecutorCompleted(executor: ExecutorService) {
executor.submit<Any> { null }.get()
}
private fun awaitAllExecutorCompleted() {
awaitExecutorCompleted(Executors.UI_HELPER_EXECUTOR)
awaitExecutorCompleted(Executors.VIEW_PREINFLATION_EXECUTOR)
}
private fun reset() {
result = 0
isTaskExecuted = false
isCallbackExecuted = false
}
}
@@ -1,70 +0,0 @@
package com.android.launcher3.util;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
import org.junit.Test;
import org.junit.runner.RunWith;
/**
* Unit tests for {@link GridOccupancy}
*/
@SmallTest
@RunWith(AndroidJUnit4.class)
public class GridOccupancyTest {
@Test
public void testFindVacantCell() {
GridOccupancy grid = initGrid(4,
1, 1, 1, 0, 0,
0, 0, 1, 1, 0,
0, 0, 0, 0, 0,
1, 1, 0, 0, 0
);
int[] vacant = new int[2];
assertTrue(grid.findVacantCell(vacant, 2, 2));
assertEquals(vacant[0], 0);
assertEquals(vacant[1], 1);
assertTrue(grid.findVacantCell(vacant, 3, 2));
assertEquals(vacant[0], 2);
assertEquals(vacant[1], 2);
assertFalse(grid.findVacantCell(vacant, 3, 3));
}
@Test
public void testIsRegionVacant() {
GridOccupancy grid = initGrid(4,
1, 1, 1, 0, 0,
0, 0, 1, 1, 0,
0, 0, 0, 0, 0,
1, 1, 0, 0, 0
);
assertTrue(grid.isRegionVacant(4, 0, 1, 4));
assertTrue(grid.isRegionVacant(0, 1, 2, 2));
assertTrue(grid.isRegionVacant(2, 2, 3, 2));
assertFalse(grid.isRegionVacant(3, 0, 2, 4));
assertFalse(grid.isRegionVacant(0, 0, 2, 1));
}
private GridOccupancy initGrid(int rows, int... cells) {
int cols = cells.length / rows;
int i = 0;
GridOccupancy grid = new GridOccupancy(cols, rows);
for (int y = 0; y < rows; y++) {
for (int x = 0; x < cols; x++) {
grid.cells[x][y] = cells[i] != 0;
i++;
}
}
return grid;
}
}
@@ -1,67 +0,0 @@
/*
* 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.util
import android.content.Context
import android.content.res.Configuration
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.google.common.truth.Truth.assertThat
import org.junit.Assert.*
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
@SmallTest
@RunWith(AndroidJUnit4::class)
class IconSizeStepsTest {
private var context: Context? = null
private val runningContext: Context = ApplicationProvider.getApplicationContext()
private lateinit var iconSizeSteps: IconSizeSteps
@Before
fun setup() {
// 160dp makes 1px = 1dp
val config =
Configuration(runningContext.resources.configuration).apply { this.densityDpi = 160 }
context = runningContext.createConfigurationContext(config)
iconSizeSteps = IconSizeSteps(context!!.resources)
}
@Test
fun minimumIconSize() {
assertThat(iconSizeSteps.minimumIconSize()).isEqualTo(52)
}
@Test
fun getNextLowerIconSize() {
assertThat(iconSizeSteps.getNextLowerIconSize(66)).isEqualTo(63)
// Assert that never goes below minimum
assertThat(iconSizeSteps.getNextLowerIconSize(52)).isEqualTo(52)
assertThat(iconSizeSteps.getNextLowerIconSize(30)).isEqualTo(52)
}
@Test
fun getIconSmallerThan() {
assertThat(iconSizeSteps.getIconSmallerThan(60)).isEqualTo(59)
// Assert that never goes below minimum
assertThat(iconSizeSteps.getIconSmallerThan(52)).isEqualTo(52)
assertThat(iconSizeSteps.getIconSmallerThan(30)).isEqualTo(52)
}
}
@@ -1,41 +0,0 @@
/*
* Copyright (C) 2019 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.util;
import static com.google.common.truth.Truth.assertThat;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
import org.junit.Test;
import org.junit.runner.RunWith;
/**
* Unit tests for {@link IntArray}
*/
@SmallTest
@RunWith(AndroidJUnit4.class)
public class IntArrayTest {
@Test
public void concatAndParseString() {
int[] array = new int[] {0, 2, 3, 9};
String concat = IntArray.wrap(array).toConcatString();
int[] parsed = IntArray.fromConcatString(concat).toArray();
assertThat(array).isEqualTo(parsed);
}
}
@@ -1,81 +0,0 @@
/*
* Copyright (C) 2018 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.util;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
import org.junit.Test;
import org.junit.runner.RunWith;
/**
* Unit tests for {@link IntSet}
*/
@SmallTest
@RunWith(AndroidJUnit4.class)
public class IntSetTest {
@Test
public void shouldBeEmptyInitially() {
IntSet set = new IntSet();
assertThat(set.size()).isEqualTo(0);
}
@Test
public void oneElementSet() {
IntSet set = new IntSet();
set.add(2);
assertThat(set.size()).isEqualTo(1);
assertTrue(set.contains(2));
assertFalse(set.contains(1));
}
@Test
public void twoElementSet() {
IntSet set = new IntSet();
set.add(2);
set.add(1);
assertThat(set.size()).isEqualTo(2);
assertTrue(set.contains(2));
assertTrue(set.contains(1));
}
@Test
public void threeElementSet() {
IntSet set = new IntSet();
set.add(2);
set.add(1);
set.add(10);
assertThat(set.size()).isEqualTo(3);
assertEquals("1, 2, 10", set.mArray.toConcatString());
}
@Test
public void duplicateEntries() {
IntSet set = new IntSet();
set.add(2);
set.add(2);
assertEquals(1, set.size());
}
}
@@ -1,199 +0,0 @@
/**
* Copyright (C) 2019 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.util;
import android.text.TextUtils;
import android.util.Pair;
import android.util.Xml;
import org.xmlpull.v1.XmlSerializer;
import java.io.IOException;
import java.io.StringWriter;
import java.io.Writer;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
/**
* Helper class to build xml for Launcher Layout
*/
public class LauncherLayoutBuilder {
// Object Tags
private static final String TAG_WORKSPACE = "workspace";
private static final String TAG_AUTO_INSTALL = "autoinstall";
private static final String TAG_FOLDER = "folder";
private static final String TAG_APPWIDGET = "appwidget";
private static final String TAG_SHORTCUT = "shortcut";
private static final String TAG_EXTRA = "extra";
private static final String ATTR_CONTAINER = "container";
private static final String ATTR_RANK = "rank";
private static final String ATTR_PACKAGE_NAME = "packageName";
private static final String ATTR_CLASS_NAME = "className";
private static final String ATTR_TITLE = "title";
private static final String ATTR_TITLE_TEXT = "titleText";
private static final String ATTR_SCREEN = "screen";
private static final String ATTR_SHORTCUT_ID = "shortcutId";
// x and y can be specified as negative integers, in which case -1 represents the
// last row / column, -2 represents the second last, and so on.
private static final String ATTR_X = "x";
private static final String ATTR_Y = "y";
private static final String ATTR_SPAN_X = "spanX";
private static final String ATTR_SPAN_Y = "spanY";
private static final String ATTR_CHILDREN = "children";
// Style attrs -- "Extra"
private static final String ATTR_KEY = "key";
private static final String ATTR_VALUE = "value";
private static final String CONTAINER_DESKTOP = "desktop";
private static final String CONTAINER_HOTSEAT = "hotseat";
private final ArrayList<Pair<String, HashMap<String, Object>>> mNodes = new ArrayList<>();
public Location atHotseat(int rank) {
Location l = new Location();
l.items.put(ATTR_CONTAINER, CONTAINER_HOTSEAT);
l.items.put(ATTR_RANK, Integer.toString(rank));
return l;
}
public Location atWorkspace(int x, int y, int screen) {
Location l = new Location();
l.items.put(ATTR_CONTAINER, CONTAINER_DESKTOP);
l.items.put(ATTR_X, Integer.toString(x));
l.items.put(ATTR_Y, Integer.toString(y));
l.items.put(ATTR_SCREEN, Integer.toString(screen));
return l;
}
public String build() throws IOException {
StringWriter writer = new StringWriter();
build(writer);
return writer.toString();
}
public void build(Writer writer) throws IOException {
XmlSerializer serializer = Xml.newSerializer();
serializer.setOutput(writer);
serializer.startDocument("UTF-8", true);
serializer.startTag(null, TAG_WORKSPACE);
writeNodes(serializer, mNodes);
serializer.endTag(null, TAG_WORKSPACE);
serializer.endDocument();
serializer.flush();
}
private static void writeNodes(XmlSerializer serializer,
ArrayList<Pair<String, HashMap<String, Object>>> nodes) throws IOException {
for (Pair<String, HashMap<String, Object>> node : nodes) {
ArrayList<Pair<String, HashMap<String, Object>>> children = null;
serializer.startTag(null, node.first);
for (Map.Entry<String, Object> attr : node.second.entrySet()) {
if (ATTR_CHILDREN.equals(attr.getKey())) {
children = (ArrayList<Pair<String, HashMap<String, Object>>>) attr.getValue();
} else {
serializer.attribute(null, attr.getKey(), (String) attr.getValue());
}
}
if (children != null) {
writeNodes(serializer, children);
}
serializer.endTag(null, node.first);
}
}
public class Location {
final HashMap<String, Object> items = new HashMap<>();
public LauncherLayoutBuilder putApp(String packageName, String className) {
items.put(ATTR_PACKAGE_NAME, packageName);
items.put(ATTR_CLASS_NAME, TextUtils.isEmpty(className) ? packageName : className);
mNodes.add(Pair.create(TAG_AUTO_INSTALL, items));
return LauncherLayoutBuilder.this;
}
public LauncherLayoutBuilder putShortcut(String packageName, String shortcutId) {
items.put(ATTR_PACKAGE_NAME, packageName);
items.put(ATTR_SHORTCUT_ID, shortcutId);
mNodes.add(Pair.create(TAG_SHORTCUT, items));
return LauncherLayoutBuilder.this;
}
public LauncherLayoutBuilder putWidget(String packageName, String className,
int spanX, int spanY) {
items.put(ATTR_PACKAGE_NAME, packageName);
items.put(ATTR_CLASS_NAME, className);
items.put(ATTR_SPAN_X, Integer.toString(spanX));
items.put(ATTR_SPAN_Y, Integer.toString(spanY));
mNodes.add(Pair.create(TAG_APPWIDGET, items));
return LauncherLayoutBuilder.this;
}
public FolderBuilder putFolder(int titleResId) {
items.put(ATTR_TITLE, Integer.toString(titleResId));
return putFolder();
}
public FolderBuilder putFolder(String title) {
items.put(ATTR_TITLE_TEXT, title);
return putFolder();
}
private FolderBuilder putFolder() {
FolderBuilder folderBuilder = new FolderBuilder();
items.put(ATTR_CHILDREN, folderBuilder.mChildren);
mNodes.add(Pair.create(TAG_FOLDER, items));
return folderBuilder;
}
}
public class FolderBuilder {
final ArrayList<Pair<String, HashMap<String, Object>>> mChildren = new ArrayList<>();
public FolderBuilder addApp(String packageName, String className) {
HashMap<String, Object> items = new HashMap<>();
items.put(ATTR_PACKAGE_NAME, packageName);
items.put(ATTR_CLASS_NAME, TextUtils.isEmpty(className) ? packageName : className);
mChildren.add(Pair.create(TAG_AUTO_INSTALL, items));
return this;
}
public FolderBuilder addShortcut(String packageName, String shortcutId) {
HashMap<String, Object> items = new HashMap<>();
items.put(ATTR_PACKAGE_NAME, packageName);
items.put(ATTR_SHORTCUT_ID, shortcutId);
mChildren.add(Pair.create(TAG_SHORTCUT, items));
return this;
}
public LauncherLayoutBuilder build() {
return LauncherLayoutBuilder.this;
}
}
}
@@ -1,339 +0,0 @@
/*
* Copyright (C) 2019 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.util;
import static android.content.pm.PackageInstaller.SessionParams.MODE_FULL_INSTALL;
import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
import static com.android.launcher3.util.TestUtil.runOnExecutorSync;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.spy;
import android.content.ContentProvider;
import android.content.ContentResolver;
import android.content.pm.PackageInstaller;
import android.content.pm.PackageInstaller.SessionParams;
import android.content.pm.PackageManager;
import android.content.pm.ProviderInfo;
import android.graphics.Bitmap;
import android.graphics.Bitmap.Config;
import android.graphics.Color;
import android.net.Uri;
import android.os.ParcelFileDescriptor;
import android.os.ParcelFileDescriptor.AutoCloseOutputStream;
import android.provider.Settings;
import android.test.mock.MockContentResolver;
import android.util.ArrayMap;
import androidx.annotation.NonNull;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.uiautomator.UiDevice;
import com.android.launcher3.InvariantDeviceProfile;
import com.android.launcher3.LauncherAppState;
import com.android.launcher3.LauncherModel;
import com.android.launcher3.LauncherModel.ModelUpdateTask;
import com.android.launcher3.LauncherPrefs;
import com.android.launcher3.model.AllAppsList;
import com.android.launcher3.model.BgDataModel;
import com.android.launcher3.model.BgDataModel.Callbacks;
import com.android.launcher3.model.ItemInstallQueue;
import com.android.launcher3.pm.InstallSessionHelper;
import com.android.launcher3.pm.UserCache;
import com.android.launcher3.testing.TestInformationProvider;
import com.android.launcher3.uioverrides.plugins.PluginManagerWrapper;
import com.android.launcher3.util.MainThreadInitializedObject.SandboxContext;
import com.android.launcher3.util.window.WindowManagerProxy;
import com.android.launcher3.widget.custom.CustomWidgetManager;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.util.UUID;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor;
/**
* Utility class to help manage Launcher Model and related objects for test.
*/
public class LauncherModelHelper {
public static final String TEST_PACKAGE = getInstrumentation().getContext().getPackageName();
public static final String TEST_ACTIVITY = "com.android.launcher3.tests.Activity2";
public static final String TEST_ACTIVITY2 = "com.android.launcher3.tests.Activity3";
public static final String TEST_ACTIVITY3 = "com.android.launcher3.tests.Activity4";
public static final String TEST_ACTIVITY4 = "com.android.launcher3.tests.Activity5";
public static final String TEST_ACTIVITY5 = "com.android.launcher3.tests.Activity6";
public static final String TEST_ACTIVITY6 = "com.android.launcher3.tests.Activity7";
public static final String TEST_ACTIVITY7 = "com.android.launcher3.tests.Activity8";
public static final String TEST_ACTIVITY8 = "com.android.launcher3.tests.Activity9";
public static final String TEST_ACTIVITY9 = "com.android.launcher3.tests.Activity10";
public static final String TEST_ACTIVITY10 = "com.android.launcher3.tests.Activity11";
public static final String TEST_ACTIVITY11 = "com.android.launcher3.tests.Activity12";
public static final String TEST_ACTIVITY12 = "com.android.launcher3.tests.Activity13";
public static final String TEST_ACTIVITY13 = "com.android.launcher3.tests.Activity14";
public static final String TEST_ACTIVITY14 = "com.android.launcher3.tests.Activity15";
// Authority for providing a test default-workspace-layout data.
private static final String TEST_PROVIDER_AUTHORITY =
LauncherModelHelper.class.getName().toLowerCase();
private static final int DEFAULT_BITMAP_SIZE = 10;
private static final int DEFAULT_GRID_SIZE = 4;
public final SandboxModelContext sandboxContext;
private final RunnableList mDestroyTask = new RunnableList();
private BgDataModel mDataModel;
public LauncherModelHelper() {
sandboxContext = new SandboxModelContext();
}
public void setupProvider(String authority, ContentProvider provider) {
sandboxContext.setupProvider(authority, provider);
}
public LauncherModel getModel() {
return LauncherAppState.getInstance(sandboxContext).getModel();
}
public synchronized BgDataModel getBgDataModel() {
if (mDataModel == null) {
getModel().enqueueModelUpdateTask(new ModelUpdateTask() {
@Override
public void init(@NonNull LauncherAppState app, @NonNull LauncherModel model,
@NonNull BgDataModel dataModel, @NonNull AllAppsList allAppsList,
@NonNull Executor uiExecutor) {
mDataModel = dataModel;
}
@Override
public void run() { }
});
}
return mDataModel;
}
/**
* Creates a installer session for the provided package.
*/
public int createInstallerSession(String pkg) throws IOException {
SessionParams sp = new SessionParams(MODE_FULL_INSTALL);
sp.setAppPackageName(pkg);
Bitmap icon = Bitmap.createBitmap(100, 100, Config.ARGB_8888);
icon.eraseColor(Color.RED);
sp.setAppIcon(icon);
sp.setAppLabel(pkg);
PackageInstaller pi = sandboxContext.getPackageManager().getPackageInstaller();
int sessionId = pi.createSession(sp);
mDestroyTask.add(() -> pi.abandonSession(sessionId));
return sessionId;
}
public void destroy() {
// When destroying the context, make sure that the model thread is blocked, so that no
// new jobs get posted while we are cleaning up
CountDownLatch l1 = new CountDownLatch(1);
CountDownLatch l2 = new CountDownLatch(1);
MODEL_EXECUTOR.execute(() -> {
l1.countDown();
waitOrThrow(l2);
});
waitOrThrow(l1);
sandboxContext.onDestroy();
l2.countDown();
mDestroyTask.executeAllAndDestroy();
}
private void waitOrThrow(CountDownLatch latch) {
try {
latch.await();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
/**
* Sets up a mock provider to load the provided layout by default, next time the layout loads
*/
public LauncherModelHelper setupDefaultLayoutProvider(LauncherLayoutBuilder builder)
throws Exception {
InvariantDeviceProfile idp = InvariantDeviceProfile.INSTANCE.get(sandboxContext);
idp.numRows = idp.numColumns = idp.numDatabaseHotseatIcons = DEFAULT_GRID_SIZE;
idp.iconBitmapSize = DEFAULT_BITMAP_SIZE;
UiDevice.getInstance(getInstrumentation()).executeShellCommand(
"settings put secure launcher3.layout.provider " + TEST_PROVIDER_AUTHORITY);
ContentProvider cp = new TestInformationProvider() {
@Override
public ParcelFileDescriptor openFile(Uri uri, String mode)
throws FileNotFoundException {
try {
ParcelFileDescriptor[] pipe = ParcelFileDescriptor.createPipe();
AutoCloseOutputStream outputStream = new AutoCloseOutputStream(pipe[1]);
ByteArrayOutputStream bos = new ByteArrayOutputStream();
builder.build(new OutputStreamWriter(bos));
outputStream.write(bos.toByteArray());
outputStream.flush();
outputStream.close();
return pipe[0];
} catch (Exception e) {
throw new FileNotFoundException(e.getMessage());
}
}
};
setupProvider(TEST_PROVIDER_AUTHORITY, cp);
mDestroyTask.add(() -> runOnExecutorSync(MODEL_EXECUTOR, () ->
UiDevice.getInstance(getInstrumentation()).executeShellCommand(
"settings delete secure launcher3.layout.provider")));
return this;
}
/**
* Loads the model in memory synchronously
*/
public void loadModelSync() throws ExecutionException, InterruptedException {
Callbacks mockCb = new Callbacks() { };
MAIN_EXECUTOR.submit(() -> getModel().addCallbacksAndLoad(mockCb)).get();
Executors.MODEL_EXECUTOR.submit(() -> { }).get();
MAIN_EXECUTOR.submit(() -> { }).get();
MAIN_EXECUTOR.submit(() -> getModel().removeCallbacks(mockCb)).get();
}
public static class SandboxModelContext extends SandboxContext {
private final MockContentResolver mMockResolver = new MockContentResolver();
private final ArrayMap<String, Object> mSpiedServices = new ArrayMap<>();
private final PackageManager mPm;
private final File mDbDir;
public SandboxModelContext() {
super(ApplicationProvider.getApplicationContext(),
UserCache.INSTANCE, InstallSessionHelper.INSTANCE, LauncherPrefs.INSTANCE,
LauncherAppState.INSTANCE, InvariantDeviceProfile.INSTANCE,
DisplayController.INSTANCE, CustomWidgetManager.INSTANCE,
SettingsCache.INSTANCE, PluginManagerWrapper.INSTANCE,
LockedUserState.INSTANCE, WallpaperColorHints.INSTANCE,
ItemInstallQueue.INSTANCE, WindowManagerProxy.INSTANCE);
// System settings cache content provider. Ensure that they are statically initialized
Settings.Secure.getString(
ApplicationProvider.getApplicationContext().getContentResolver(), "test");
Settings.System.getString(
ApplicationProvider.getApplicationContext().getContentResolver(), "test");
Settings.Global.getString(
ApplicationProvider.getApplicationContext().getContentResolver(), "test");
mPm = spy(getBaseContext().getPackageManager());
mDbDir = new File(getCacheDir(), UUID.randomUUID().toString());
}
@Override
protected <T> T createObject(MainThreadInitializedObject<T> object) {
if (object == LauncherAppState.INSTANCE) {
return (T) new LauncherAppState(this, null /* iconCacheFileName */);
}
return super.createObject(object);
}
public SandboxModelContext allow(MainThreadInitializedObject object) {
mAllowedObjects.add(object);
return this;
}
@Override
public File getDatabasePath(String name) {
if (!mDbDir.exists()) {
mDbDir.mkdirs();
}
return new File(mDbDir, name);
}
@Override
public ContentResolver getContentResolver() {
return mMockResolver;
}
@Override
public void onDestroy() {
if (deleteContents(mDbDir)) {
mDbDir.delete();
}
super.onDestroy();
}
@Override
public PackageManager getPackageManager() {
return mPm;
}
@Override
public Object getSystemService(String name) {
Object service = mSpiedServices.get(name);
return service != null ? service : super.getSystemService(name);
}
public <T> T spyService(Class<T> tClass) {
String name = getSystemServiceName(tClass);
Object service = mSpiedServices.get(name);
if (service != null) {
return (T) service;
}
T result = spy(getSystemService(tClass));
mSpiedServices.put(name, result);
return result;
}
public void setupProvider(String authority, ContentProvider provider) {
ProviderInfo providerInfo = new ProviderInfo();
providerInfo.authority = authority;
providerInfo.applicationInfo = getApplicationInfo();
provider.attachInfo(this, providerInfo);
mMockResolver.addProvider(providerInfo.authority, provider);
doReturn(providerInfo).when(mPm).resolveContentProvider(eq(authority), anyInt());
}
private static boolean deleteContents(File dir) {
File[] files = dir.listFiles();
boolean success = true;
if (files != null) {
for (File file : files) {
if (file.isDirectory()) {
success &= deleteContents(file);
}
if (!file.delete()) {
success = false;
}
}
}
return success;
}
}
}
@@ -1,101 +0,0 @@
package com.android.launcher3.util
import android.content.ContentValues
import com.android.launcher3.LauncherModel
import com.android.launcher3.LauncherSettings.Favorites
import com.android.launcher3.LauncherSettings.Favorites.APPWIDGET_ID
import com.android.launcher3.LauncherSettings.Favorites.APPWIDGET_PROVIDER
import com.android.launcher3.LauncherSettings.Favorites.APPWIDGET_SOURCE
import com.android.launcher3.LauncherSettings.Favorites.CELLX
import com.android.launcher3.LauncherSettings.Favorites.CELLY
import com.android.launcher3.LauncherSettings.Favorites.CONTAINER
import com.android.launcher3.LauncherSettings.Favorites.CONTAINER_DESKTOP
import com.android.launcher3.LauncherSettings.Favorites.INTENT
import com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE
import com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_APPLICATION
import com.android.launcher3.LauncherSettings.Favorites.PROFILE_ID
import com.android.launcher3.LauncherSettings.Favorites.RESTORED
import com.android.launcher3.LauncherSettings.Favorites.SCREEN
import com.android.launcher3.LauncherSettings.Favorites.SPANX
import com.android.launcher3.LauncherSettings.Favorites.SPANY
import com.android.launcher3.LauncherSettings.Favorites.TITLE
import com.android.launcher3.LauncherSettings.Favorites._ID
import com.android.launcher3.model.BgDataModel
import com.android.launcher3.model.ModelDbController
object ModelTestExtensions {
/** Clears and reloads Launcher db to cleanup the workspace */
fun LauncherModel.clearModelDb() {
// Load the model once so that there is no pending migration:
loadModelSync()
TestUtil.runOnExecutorSync(Executors.MODEL_EXECUTOR) {
modelDbController.run {
tryMigrateDB(null /* restoreEventLogger */)
createEmptyDB()
clearEmptyDbFlag()
}
}
// Reload model
TestUtil.runOnExecutorSync(Executors.MAIN_EXECUTOR) { forceReload() }
loadModelSync()
}
/** Loads the model in memory synchronously */
fun LauncherModel.loadModelSync() {
val mockCb: BgDataModel.Callbacks = object : BgDataModel.Callbacks {}
TestUtil.runOnExecutorSync(Executors.MAIN_EXECUTOR) { addCallbacksAndLoad(mockCb) }
TestUtil.runOnExecutorSync(Executors.MODEL_EXECUTOR) {}
TestUtil.runOnExecutorSync(Executors.MAIN_EXECUTOR) {}
TestUtil.runOnExecutorSync(Executors.MAIN_EXECUTOR) { removeCallbacks(mockCb) }
}
/** Adds and commits a new item to Launcher.db */
fun LauncherModel.addItem(
title: String = "LauncherTestApp",
intent: String =
"#Intent;action=android.intent.action.MAIN;category=android.intent.category.LAUNCHER;component=com.google.android.apps.nexuslauncher.tests/com.android.launcher3.testcomponent.BaseTestingActivity;launchFlags=0x10200000;end",
type: Int = ITEM_TYPE_APPLICATION,
restoreFlags: Int = 0,
screen: Int = 0,
container: Int = CONTAINER_DESKTOP,
x: Int,
y: Int,
spanX: Int = 1,
spanY: Int = 1,
id: Int = 0,
profileId: Int = 0,
tableName: String = Favorites.TABLE_NAME,
appWidgetId: Int = -1,
appWidgetSource: Int = -1,
appWidgetProvider: String? = null
) {
loadModelSync()
TestUtil.runOnExecutorSync(Executors.MODEL_EXECUTOR) {
val controller: ModelDbController = modelDbController
controller.tryMigrateDB(null /* restoreEventLogger */)
modelDbController.newTransaction().use { transaction ->
val values =
ContentValues().apply {
values[_ID] = id
values[TITLE] = title
values[PROFILE_ID] = profileId
values[CONTAINER] = container
values[SCREEN] = screen
values[CELLX] = x
values[CELLY] = y
values[SPANX] = spanX
values[SPANY] = spanY
values[ITEM_TYPE] = type
values[RESTORED] = restoreFlags
values[INTENT] = intent
values[APPWIDGET_ID] = appWidgetId
values[APPWIDGET_SOURCE] = appWidgetSource
values[APPWIDGET_PROVIDER] = appWidgetProvider
}
// Migrate any previous data so that the DB state is correct
controller.insert(tableName, values)
transaction.commit()
}
}
}
}
@@ -1,72 +0,0 @@
/*
* 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.util
import android.util.FloatProperty
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.google.common.truth.Truth.assertThat
import org.junit.Test
import org.junit.runner.RunWith
/** Unit tests for [MultiPropertyFactory] */
@SmallTest
@RunWith(AndroidJUnit4::class)
class MultiPropertyFactoryTest {
private val received = mutableListOf<Float>()
private val receiveProperty: FloatProperty<Any> =
object : FloatProperty<Any>("receive") {
override fun setValue(obj: Any?, value: Float) {
received.add(value)
}
override fun get(o: Any): Float {
return 0f
}
}
private val factory =
MultiPropertyFactory(null, receiveProperty, 3) { x: Float, y: Float -> x + y }
private val p1 = factory.get(0)
private val p2 = factory.get(1)
private val p3 = factory.get(2)
@Test
fun set_sameIndexes_allApplied() {
val v1 = 50f
val v2 = 100f
p1.value = v1
p1.value = v1
p1.value = v2
assertThat(received).containsExactly(v1, v1, v2)
}
@Test
fun set_differentIndexes_aggregationApplied() {
val v1 = 50f
val v2 = 100f
val v3 = 150f
p1.value = v1
p2.value = v2
p3.value = v3
assertThat(received).containsExactly(v1, v1 + v2, v1 + v2 + v3)
}
}
@@ -1,109 +0,0 @@
/*
* 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.util
import android.view.View
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.google.common.truth.Truth.assertThat
import org.junit.Test
import org.junit.runner.RunWith
/** Unit tests for [MultiScalePropertyFactory] */
@SmallTest
@RunWith(AndroidJUnit4::class)
class MultiScalePropertyTest {
private val received = mutableListOf<Float>()
private val factory =
object : MultiScalePropertyFactory<View?>("Test") {
override fun apply(obj: View?, value: Float) {
received.add(value)
}
}
private val p1 = factory.get(1)
private val p2 = factory.get(2)
private val p3 = factory.get(3)
@Test
fun set_multipleSame_bothAppliedd() {
p1.set(null, 0.5f)
p1.set(null, 0.5f)
assertThat(received).containsExactly(0.5f, 0.5f)
}
@Test
fun set_differentIndexes_oneValuesNotCounted() {
val v1 = 0.5f
val v2 = 1.0f
p1.set(null, v1)
p2.set(null, v2)
assertThat(received).containsExactly(v1, v1)
}
@Test
fun set_onlyOneSetToOne_oneApplied() {
p1.set(null, 1.0f)
assertThat(received).containsExactly(1.0f)
}
@Test
fun set_onlyOneLessThanOne_applied() {
p1.set(null, 0.5f)
assertThat(received).containsExactly(0.5f)
}
@Test
fun set_differentIndexes_boundToMin() {
val v1 = 0.5f
val v2 = 0.6f
p1.set(null, v1)
p2.set(null, v2)
assertThat(received).containsExactly(v1, v1)
}
@Test
fun set_allHigherThanOne_boundToMax() {
val v1 = 3.0f
val v2 = 2.0f
val v3 = 1.0f
p1.set(null, v1)
p2.set(null, v2)
p3.set(null, v3)
assertThat(received).containsExactly(v1, v1, v1)
}
@Test
fun set_differentIndexes_firstModified_aggregationApplied() {
val v1 = 0.5f
val v2 = 0.6f
val v3 = 4f
p1.set(null, v1)
p2.set(null, v2)
p3.set(null, v3)
assertThat(received).containsExactly(v1, v1, v1 * v2 * v3)
}
}
@@ -1,162 +0,0 @@
/*
* Copyright (C) 2021 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.util;
import static com.android.launcher3.widget.WidgetSections.NO_CATEGORY;
import static com.google.common.truth.Truth.assertThat;
import android.os.UserHandle;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
import com.android.launcher3.model.data.PackageItemInfo;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.junit.runner.RunWith;
@SmallTest
@RunWith(AndroidJUnit4.class)
public final class PackageUserKeyTest {
@Rule
public ExpectedException exception = ExpectedException.none();
private static final String TEST_PACKAGE = "com.android.test.package";
private static final int CONVERSATIONS = 0;
private static final int WEATHER = 1;
@Test
public void fromPackageItemInfo_shouldCreateExpectedObject() {
PackageUserKey packageUserKey = PackageUserKey.fromPackageItemInfo(
new PackageItemInfo(TEST_PACKAGE, UserHandle.CURRENT));
assertThat(packageUserKey.mPackageName).isEqualTo(TEST_PACKAGE);
assertThat(packageUserKey.mWidgetCategory).isEqualTo(NO_CATEGORY);
assertThat(packageUserKey.mUser).isEqualTo(UserHandle.CURRENT);
}
@Test
public void constructor_packageNameAndUserHandle_shouldCreateExpectedObject() {
PackageUserKey packageUserKey = new PackageUserKey(TEST_PACKAGE, UserHandle.CURRENT);
assertThat(packageUserKey.mPackageName).isEqualTo(TEST_PACKAGE);
assertThat(packageUserKey.mWidgetCategory).isEqualTo(NO_CATEGORY);
assertThat(packageUserKey.mUser).isEqualTo(UserHandle.CURRENT);
}
@Test
public void constructor_widgetCategoryAndUserHandle_shouldCreateExpectedObject() {
PackageUserKey packageUserKey = new PackageUserKey(CONVERSATIONS, UserHandle.CURRENT);
assertThat(packageUserKey.mPackageName).isEqualTo("");
assertThat(packageUserKey.mWidgetCategory).isEqualTo(CONVERSATIONS);
assertThat(packageUserKey.mUser).isEqualTo(UserHandle.CURRENT);
}
@Test
public void equals_sameObject_shouldReturnTrue() {
PackageUserKey packageUserKey = new PackageUserKey(TEST_PACKAGE, UserHandle.CURRENT);
PackageUserKey otherPackageUserKey = packageUserKey;
assertThat(packageUserKey).isEqualTo(otherPackageUserKey);
}
@Test
public void equals_differentObjectSameContent_shouldReturnTrue() {
PackageUserKey packageUserKey = new PackageUserKey(TEST_PACKAGE, UserHandle.CURRENT);
PackageUserKey otherPackageUserKey = new PackageUserKey(TEST_PACKAGE, UserHandle.CURRENT);
assertThat(packageUserKey).isEqualTo(otherPackageUserKey);
}
@Test
public void equals_compareAgainstNull_shouldReturnFalse() {
PackageUserKey packageUserKey = new PackageUserKey(TEST_PACKAGE, UserHandle.CURRENT);
assertThat(packageUserKey).isNotEqualTo(null);
}
@Test
public void equals_differentPackage_shouldReturnFalse() {
PackageUserKey packageUserKey = new PackageUserKey(TEST_PACKAGE, UserHandle.CURRENT);
PackageUserKey otherPackageUserKey = new PackageUserKey(TEST_PACKAGE + "1",
UserHandle.CURRENT);
assertThat(packageUserKey).isNotEqualTo(otherPackageUserKey);
}
@Test
public void equals_differentCategory_shouldReturnFalse() {
PackageUserKey packageUserKey = new PackageUserKey(WEATHER, UserHandle.CURRENT);
PackageUserKey otherPackageUserKey = new PackageUserKey(CONVERSATIONS, UserHandle.CURRENT);
assertThat(packageUserKey).isNotEqualTo(otherPackageUserKey);
}
@Test
public void equals_differentUser_shouldReturnFalse() {
PackageUserKey packageUserKey = new PackageUserKey(TEST_PACKAGE, UserHandle.of(1));
PackageUserKey otherPackageUserKey = new PackageUserKey(TEST_PACKAGE, UserHandle.of(2));
assertThat(packageUserKey).isNotEqualTo(otherPackageUserKey);
}
@Test
public void hashCode_sameObject_shouldBeTheSame() {
PackageUserKey packageUserKey = new PackageUserKey(WEATHER, UserHandle.CURRENT);
PackageUserKey otherPackageUserKey = packageUserKey;
assertThat(packageUserKey.hashCode()).isEqualTo(otherPackageUserKey.hashCode());
}
@Test
public void hashCode_differentObjectSameContent_shouldBeTheSame() {
PackageUserKey packageUserKey = new PackageUserKey(TEST_PACKAGE, UserHandle.CURRENT);
PackageUserKey otherPackageUserKey = new PackageUserKey(TEST_PACKAGE, UserHandle.CURRENT);
assertThat(packageUserKey.hashCode()).isEqualTo(otherPackageUserKey.hashCode());
}
@Test
public void hashCode_differentPackage_shouldBeDifferent() {
PackageUserKey packageUserKey = new PackageUserKey(TEST_PACKAGE, UserHandle.CURRENT);
PackageUserKey otherPackageUserKey = new PackageUserKey(TEST_PACKAGE + "1",
UserHandle.CURRENT);
assertThat(packageUserKey.hashCode()).isNotEqualTo(otherPackageUserKey.hashCode());
}
@Test
public void hashCode_differentCategory_shouldBeDifferent() {
PackageUserKey packageUserKey = new PackageUserKey(WEATHER, UserHandle.CURRENT);
PackageUserKey otherPackageUserKey = new PackageUserKey(CONVERSATIONS, UserHandle.CURRENT);
assertThat(packageUserKey.hashCode()).isNotEqualTo(otherPackageUserKey.hashCode());
}
@Test
public void hashCode_differentUser_shouldBeDifferent() {
PackageUserKey packageUserKey = new PackageUserKey(TEST_PACKAGE, UserHandle.of(1));
PackageUserKey otherPackageUserKey = new PackageUserKey(TEST_PACKAGE, UserHandle.of(2));
assertThat(packageUserKey.hashCode()).isNotEqualTo(otherPackageUserKey.hashCode());
}
}
@@ -1,58 +0,0 @@
/*
* Copyright (C) 2021 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.util;
import java.lang.reflect.Field;
public class ReflectionHelpers {
/**
* Reflectively get the value of a field.
*
* @param object Target object.
* @param fieldName The field name.
* @param <R> The return type.
* @return Value of the field on the object.
*/
public static <R> R getField(Object object, String fieldName) {
try {
Field field = object.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
return (R) field.get(object);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
/**
* Reflectively set the value of a field.
*
* @param object Target object.
* @param fieldName The field name.
* @param fieldNewValue New value.
*/
public static void setField(Object object, String fieldName, Object fieldNewValue) {
try {
Field field = object.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(object, fieldNewValue);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
@@ -1,29 +0,0 @@
/*
* 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.util;
public class TestConstants {
public static class AppNames {
public static final String TEST_APP_NAME = "LauncherTestApp";
public static final String DUMMY_APP_NAME = "Aardwolf";
public static final String MAPS_APP_NAME = "Maps";
public static final String STORE_APP_NAME = "Play Store";
public static final String GMAIL_APP_NAME = "Gmail";
public static final String CHROME_APP_NAME = "Chrome";
public static final String MESSAGES_APP_NAME = "Messages";
}
}
@@ -1,323 +0,0 @@
/*
* Copyright (C) 2018 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.util;
import static android.util.Base64.NO_PADDING;
import static android.util.Base64.NO_WRAP;
import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
import static com.android.launcher3.LauncherSettings.Settings.LAYOUT_DIGEST_KEY;
import static com.android.launcher3.LauncherSettings.Settings.LAYOUT_DIGEST_LABEL;
import static com.android.launcher3.LauncherSettings.Settings.LAYOUT_DIGEST_TAG;
import static org.junit.Assert.assertTrue;
import android.app.Instrumentation;
import android.app.blob.BlobHandle;
import android.app.blob.BlobStoreManager;
import android.content.Context;
import android.content.pm.LauncherApps;
import android.content.res.Resources;
import android.graphics.Point;
import android.os.AsyncTask;
import android.os.Handler;
import android.os.Looper;
import android.os.ParcelFileDescriptor.AutoCloseOutputStream;
import android.os.Process;
import android.os.UserHandle;
import android.provider.Settings;
import android.system.OsConstants;
import android.util.Base64;
import android.util.Log;
import androidx.test.uiautomator.UiDevice;
import com.android.launcher3.config.FeatureFlags;
import com.android.launcher3.config.FeatureFlags.BooleanFlag;
import com.android.launcher3.config.FeatureFlags.IntFlag;
import com.android.launcher3.tapl.LauncherInstrumentation;
import com.android.launcher3.tapl.Workspace;
import com.android.launcher3.util.rule.TestStabilityRule;
import org.junit.Assert;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.security.MessageDigest;
import java.util.concurrent.Callable;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.FutureTask;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.function.Predicate;
import java.util.function.ToIntFunction;
public class TestUtil {
private static final String TAG = "TestUtil";
public static final String DUMMY_PACKAGE = "com.example.android.aardwolf";
public static final String DUMMY_CLASS_NAME = "com.example.android.aardwolf.Activity1";
public static final long DEFAULT_UI_TIMEOUT = 10000;
public static void installDummyApp() throws IOException {
final int defaultUserId = getMainUserId();
installDummyAppForUser(defaultUserId);
}
public static void installDummyAppForUser(int userId) throws IOException {
Instrumentation instrumentation = getInstrumentation();
// Copy apk from resources to a local file and install from there.
final Resources resources = instrumentation.getContext().getResources();
final InputStream in = resources.openRawResource(
resources.getIdentifier("aardwolf_dummy_app",
"raw", instrumentation.getContext().getPackageName()));
final String apkFilename = instrumentation.getTargetContext()
.getFilesDir().getPath() + "/dummy_app.apk";
try (PackageInstallCheck pic = new PackageInstallCheck()) {
final FileOutputStream out = new FileOutputStream(apkFilename);
byte[] buff = new byte[1024];
int read;
while ((read = in.read(buff)) > 0) {
out.write(buff, 0, read);
}
in.close();
out.close();
final String result = UiDevice.getInstance(instrumentation)
.executeShellCommand(String.format("pm install -i %s --user ",
instrumentation.getContext().getPackageName())
+ userId + " " + apkFilename);
Assert.assertTrue(
"Failed to install wellbeing test apk; make sure the device is rooted",
"Success".equals(result.replaceAll("\\s+", "")));
pic.mAddWait.await();
} catch (InterruptedException e) {
throw new IOException(e);
}
}
/**
* Returns the main user ID. NOTE: For headless system it is NOT 0. Returns 0 by default, if
* there is no main user.
*
* @return a main user ID
*/
public static int getMainUserId() throws IOException {
Instrumentation instrumentation = getInstrumentation();
final String result = UiDevice.getInstance(instrumentation)
.executeShellCommand("cmd user get-main-user");
try {
return Integer.parseInt(result.trim());
} catch (NumberFormatException e) {
return 0;
}
}
/**
* @return Grid coordinates from the center and corners of the Workspace. Those are not pixels.
* See {@link Workspace#getIconGridDimensions()}
*/
public static Point[] getCornersAndCenterPositions(LauncherInstrumentation launcher) {
final Point dimensions = launcher.getWorkspace().getIconGridDimensions();
if (TestStabilityRule.isPresubmit()) {
// Return only center in presubmit to fit under the presubmit SLO.
return new Point[]{
new Point(dimensions.x / 2, dimensions.y / 2)
};
} else {
return new Point[]{
new Point(0, 1),
new Point(0, dimensions.y - 2),
new Point(dimensions.x - 1, 1),
new Point(dimensions.x - 1, dimensions.y - 2),
new Point(dimensions.x / 2, dimensions.y / 2)
};
}
}
/**
* Utility class to override a boolean flag during test. Note that the returned SafeCloseable
* must be closed to restore the original state
*/
public static SafeCloseable overrideFlag(BooleanFlag flag, boolean value) {
Predicate<BooleanFlag> originalProxy = FeatureFlags.sBooleanReader;
Predicate<BooleanFlag> testProxy = f -> f == flag ? value : originalProxy.test(f);
FeatureFlags.sBooleanReader = testProxy;
return () -> {
if (FeatureFlags.sBooleanReader == testProxy) {
FeatureFlags.sBooleanReader = originalProxy;
}
};
}
/**
* Utility class to override a int flag during test. Note that the returned SafeCloseable
* must be closed to restore the original state
*/
public static SafeCloseable overrideFlag(IntFlag flag, int value) {
ToIntFunction<IntFlag> originalProxy = FeatureFlags.sIntReader;
ToIntFunction<IntFlag> testProxy = f -> f == flag ? value : originalProxy.applyAsInt(f);
FeatureFlags.sIntReader = testProxy;
return () -> {
if (FeatureFlags.sIntReader == testProxy) {
FeatureFlags.sIntReader = originalProxy;
}
};
}
public static void uninstallDummyApp() throws IOException {
UiDevice.getInstance(getInstrumentation()).executeShellCommand(
"pm uninstall " + DUMMY_PACKAGE);
}
/**
* Sets the default layout for Launcher and returns an object which can be used to clear
* the data
*/
public static AutoCloseable setLauncherDefaultLayout(
Context context, LauncherLayoutBuilder layoutBuilder) throws Exception {
byte[] data = layoutBuilder.build().getBytes();
byte[] digest = MessageDigest.getInstance("SHA-256").digest(data);
BlobHandle handle = BlobHandle.createWithSha256(
digest, LAYOUT_DIGEST_LABEL, 0, LAYOUT_DIGEST_TAG);
BlobStoreManager blobManager = context.getSystemService(BlobStoreManager.class);
final long sessionId = blobManager.createSession(handle);
CountDownLatch wait = new CountDownLatch(1);
try (BlobStoreManager.Session session = blobManager.openSession(sessionId)) {
try (OutputStream out = new AutoCloseOutputStream(session.openWrite(0, -1))) {
out.write(data);
}
session.allowPublicAccess();
session.commit(AsyncTask.THREAD_POOL_EXECUTOR, i -> wait.countDown());
}
String key = Base64.encodeToString(digest, NO_WRAP | NO_PADDING);
Settings.Secure.putString(context.getContentResolver(), LAYOUT_DIGEST_KEY, key);
wait.await();
return () ->
Settings.Secure.putString(context.getContentResolver(), LAYOUT_DIGEST_KEY, null);
}
/**
* Utility method to run a task synchronously which converts any exceptions to RuntimeException
*/
public static void runOnExecutorSync(ExecutorService executor, UncheckedRunnable task) {
try {
executor.submit(() -> {
try {
task.run();
} catch (Exception e) {
throw new RuntimeException(e);
}
}).get();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
/**
* Runs the callback on the UI thread and returns the result.
*/
public static <T> T getOnUiThread(final Callable<T> callback) {
try {
FutureTask<T> task = new FutureTask<>(callback);
if (Looper.myLooper() == Looper.getMainLooper()) {
task.run();
} else {
new Handler(Looper.getMainLooper()).post(task);
}
return task.get(DEFAULT_UI_TIMEOUT, TimeUnit.MILLISECONDS);
} catch (TimeoutException e) {
Log.e(TAG, "Timeout in getOnUiThread, sending SIGABRT", e);
Process.sendSignal(Process.myPid(), OsConstants.SIGABRT);
throw new RuntimeException(e);
} catch (Throwable e) {
throw new RuntimeException(e);
}
}
// Please don't add negative test cases for methods that fail only after a long wait.
public static void expectFail(String message, Runnable action) {
boolean failed = false;
try {
action.run();
} catch (AssertionError e) {
failed = true;
}
assertTrue(message, failed);
}
/** Interface to indicate a runnable which can throw any exception. */
public interface UncheckedRunnable {
/** Method to run the task */
void run() throws Exception;
}
private static class PackageInstallCheck extends LauncherApps.Callback
implements AutoCloseable {
final CountDownLatch mAddWait = new CountDownLatch(1);
final LauncherApps mLauncherApps;
PackageInstallCheck() {
mLauncherApps = getInstrumentation().getTargetContext()
.getSystemService(LauncherApps.class);
mLauncherApps.registerCallback(this, new Handler(Looper.getMainLooper()));
}
private void verifyPackage(String packageName) {
if (DUMMY_PACKAGE.equals(packageName)) {
mAddWait.countDown();
}
}
@Override
public void onPackageAdded(String packageName, UserHandle user) {
verifyPackage(packageName);
}
@Override
public void onPackageChanged(String packageName, UserHandle user) {
verifyPackage(packageName);
}
@Override
public void onPackageRemoved(String packageName, UserHandle user) { }
@Override
public void onPackagesAvailable(String[] packageNames, UserHandle user, boolean replacing) {
for (String packageName : packageNames) {
verifyPackage(packageName);
}
}
@Override
public void onPackagesUnavailable(String[] packageNames, UserHandle user,
boolean replacing) { }
@Override
public void close() {
mLauncherApps.unregisterCallback(this);
}
}
}
@@ -1,67 +0,0 @@
/*
* 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.util
import android.view.InputDevice
import android.view.MotionEvent
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.google.common.truth.Truth.assertThat
import org.junit.Test
import org.junit.runner.RunWith
/** Unit tests for [TouchUtil] */
@SmallTest
@RunWith(AndroidJUnit4::class)
class TouchUtilTest {
@Test
fun isMouseRightClickDownOrMove_onMouseRightButton_returnsTrue() {
val ev = MotionEvent.obtain(200, 300, MotionEvent.ACTION_MOVE, 1.0f, 0.0f, 0)
ev.source = InputDevice.SOURCE_MOUSE
ev.buttonState = MotionEvent.BUTTON_SECONDARY
assertThat(TouchUtil.isMouseRightClickDownOrMove(ev)).isTrue()
}
@Test
fun isMouseRightClickDownOrMove_onMouseLeftButton_returnsFalse() {
val ev = MotionEvent.obtain(200, 300, MotionEvent.ACTION_MOVE, 1.0f, 0.0f, 0)
ev.source = InputDevice.SOURCE_MOUSE
ev.buttonState = MotionEvent.BUTTON_PRIMARY
assertThat(TouchUtil.isMouseRightClickDownOrMove(ev)).isFalse()
}
@Test
fun isMouseRightClickDownOrMove_onMouseTertiaryButton_returnsFalse() {
val ev = MotionEvent.obtain(200, 300, MotionEvent.ACTION_MOVE, 1.0f, 0.0f, 0)
ev.source = InputDevice.SOURCE_MOUSE
ev.buttonState = MotionEvent.BUTTON_TERTIARY
assertThat(TouchUtil.isMouseRightClickDownOrMove(ev)).isFalse()
}
@Test
fun isMouseRightClickDownOrMove_onDpadRightButton_returnsFalse() {
val ev = MotionEvent.obtain(200, 300, MotionEvent.ACTION_MOVE, 1.0f, 0.0f, 0)
ev.source = InputDevice.SOURCE_DPAD
ev.buttonState = MotionEvent.BUTTON_SECONDARY
assertThat(TouchUtil.isMouseRightClickDownOrMove(ev)).isFalse()
}
}
@@ -1,66 +0,0 @@
package com.android.launcher3.util;
import android.os.SystemClock;
import android.util.Log;
import com.android.launcher3.tapl.LauncherInstrumentation;
import org.junit.Assert;
import java.util.function.Supplier;
/**
* A utility class for waiting for a condition to be true.
*/
public class Wait {
private static final long DEFAULT_SLEEP_MS = 200;
public static void atMost(String message, Condition condition, long timeout,
LauncherInstrumentation launcher) {
atMost(() -> message, condition, timeout, DEFAULT_SLEEP_MS, launcher);
}
public static void atMost(Supplier<String> message, Condition condition, long timeout,
LauncherInstrumentation launcher) {
atMost(message, condition, timeout, DEFAULT_SLEEP_MS, launcher);
}
public static void atMost(Supplier<String> message, Condition condition, long timeout,
long sleepMillis,
LauncherInstrumentation launcher) {
final long startTime = SystemClock.uptimeMillis();
long endTime = startTime + timeout;
Log.d("Wait", "atMost: " + startTime + " - " + endTime);
while (SystemClock.uptimeMillis() < endTime) {
try {
if (condition.isTrue()) {
return;
}
} catch (Throwable t) {
throw new RuntimeException(t);
}
SystemClock.sleep(sleepMillis);
}
// Check once more before returning false.
try {
if (condition.isTrue()) {
return;
}
} catch (Throwable t) {
throw new RuntimeException(t);
}
Log.d("Wait", "atMost: timed out: " + SystemClock.uptimeMillis());
launcher.checkForAnomaly(false, false);
Assert.fail(message.get());
}
/**
* Interface representing a generic condition
*/
public interface Condition {
boolean isTrue() throws Throwable;
}
}
@@ -1,97 +0,0 @@
/*
* Copyright (C) 2020 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.util;
import static androidx.test.core.app.ApplicationProvider.getApplicationContext;
import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
import android.appwidget.AppWidgetManager;
import android.appwidget.AppWidgetProviderInfo;
import android.content.ComponentName;
import android.content.Context;
import android.os.Bundle;
import android.os.Process;
import com.android.launcher3.LauncherSettings;
import com.android.launcher3.model.data.LauncherAppWidgetInfo;
import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
import com.android.launcher3.widget.LauncherWidgetHolder;
import com.android.launcher3.widget.PendingAddWidgetInfo;
import com.android.launcher3.widget.WidgetManagerHelper;
/**
* Common method for widget binding
*/
public class WidgetUtils {
/**
* Creates a LauncherAppWidgetInfo corresponding to {@param info}
*
* @param bindWidget if true the info is bound and a valid widgetId is assigned to
* the LauncherAppWidgetInfo
*/
public static LauncherAppWidgetInfo createWidgetInfo(
LauncherAppWidgetProviderInfo info, Context targetContext, boolean bindWidget) {
LauncherAppWidgetInfo item = new LauncherAppWidgetInfo(
LauncherAppWidgetInfo.NO_ID, info.provider);
item.spanX = info.minSpanX;
item.spanY = info.minSpanY;
item.minSpanX = info.minSpanX;
item.minSpanY = info.minSpanY;
item.user = info.getProfile();
item.cellX = 0;
item.cellY = 1;
item.container = LauncherSettings.Favorites.CONTAINER_DESKTOP;
if (bindWidget) {
PendingAddWidgetInfo pendingInfo =
new PendingAddWidgetInfo(
info, LauncherSettings.Favorites.CONTAINER_WIDGETS_TRAY);
pendingInfo.spanX = item.spanX;
pendingInfo.spanY = item.spanY;
pendingInfo.minSpanX = item.minSpanX;
pendingInfo.minSpanY = item.minSpanY;
Bundle options = pendingInfo.getDefaultSizeOptions(targetContext);
LauncherWidgetHolder holder = LauncherWidgetHolder.newInstance(targetContext);
try {
int widgetId = holder.allocateAppWidgetId();
if (!new WidgetManagerHelper(targetContext)
.bindAppWidgetIdIfAllowed(widgetId, info, options)) {
holder.deleteAppWidgetId(widgetId);
throw new IllegalArgumentException("Unable to bind widget id");
}
item.appWidgetId = widgetId;
} finally {
// Necessary to destroy the holder to free up possible activity context
holder.destroy();
}
}
return item;
}
/**
* Creates a {@link AppWidgetProviderInfo} for the provided component name
*/
public static AppWidgetProviderInfo createAppWidgetProviderInfo(ComponentName cn) {
AppWidgetProviderInfo info = AppWidgetManager.getInstance(getApplicationContext())
.getInstalledProvidersForPackage(
getInstrumentation().getContext().getPackageName(), Process.myUserHandle())
.get(0);
info.provider = cn;
return info;
}
}
@@ -1,157 +0,0 @@
package com.android.launcher3.util.rule;
import static androidx.test.InstrumentationRegistry.getInstrumentation;
import android.os.FileUtils;
import android.os.ParcelFileDescriptor.AutoCloseInputStream;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.test.uiautomator.UiDevice;
import com.android.app.viewcapture.data.ExportedData;
import com.android.launcher3.tapl.LauncherInstrumentation;
import com.android.launcher3.ui.AbstractLauncherUiTest;
import org.junit.rules.TestWatcher;
import org.junit.runner.Description;
import org.junit.runners.model.Statement;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.function.Supplier;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
public class FailureWatcher extends TestWatcher {
private static final String TAG = "FailureWatcher";
private static boolean sSavedBugreport = false;
private final LauncherInstrumentation mLauncher;
@NonNull
private final Supplier<ExportedData> mViewCaptureDataSupplier;
public FailureWatcher(LauncherInstrumentation launcher,
@NonNull Supplier<ExportedData> viewCaptureDataSupplier) {
mLauncher = launcher;
mViewCaptureDataSupplier = viewCaptureDataSupplier;
}
@Override
protected void succeeded(Description description) {
super.succeeded(description);
AbstractLauncherUiTest.checkDetectedLeaks(mLauncher);
}
@Override
public Statement apply(Statement base, Description description) {
return new Statement() {
@Override
public void evaluate() throws Throwable {
try {
FailureWatcher.super.apply(base, description).evaluate();
} finally {
// Detect touch events coming from physical screen.
if (mLauncher.hadNontestEvents()) {
throw new AssertionError(
"Launcher received events not sent by the test. This may mean "
+ "that the touch screen of the lab device has sent false"
+ " events. See the logcat for "
+ "TaplEvents|LauncherEvents|TaplTarget tag and look for "
+ "events with deviceId != -1");
}
}
}
};
}
@Override
protected void failed(Throwable e, Description description) {
onError(mLauncher, description, e, mViewCaptureDataSupplier);
}
static File diagFile(Description description, String prefix, String ext) {
return new File(getInstrumentation().getTargetContext().getFilesDir(),
prefix + "-" + description.getTestClass().getSimpleName() + "."
+ description.getMethodName() + "." + ext);
}
public static void onError(LauncherInstrumentation launcher, Description description,
Throwable e) {
onError(launcher, description, e, null);
}
private static void onError(LauncherInstrumentation launcher, Description description,
Throwable e, @Nullable Supplier<ExportedData> viewCaptureDataSupplier) {
final File sceenshot = diagFile(description, "TestScreenshot", "png");
final File hierarchy = diagFile(description, "Hierarchy", "zip");
// Dump window hierarchy
try (ZipOutputStream out = new ZipOutputStream(new FileOutputStream(hierarchy))) {
out.putNextEntry(new ZipEntry("bugreport.txt"));
dumpStringCommand("dumpsys window windows", out);
dumpStringCommand("dumpsys package", out);
dumpStringCommand("dumpsys activity service TouchInteractionService", out);
out.closeEntry();
out.putNextEntry(new ZipEntry("visible_windows.zip"));
dumpCommand("cmd window dump-visible-window-views", out);
out.closeEntry();
if (viewCaptureDataSupplier != null) {
out.putNextEntry(new ZipEntry("FS/data/misc/wmtrace/failed_test.vc"));
final ExportedData exportedData = viewCaptureDataSupplier.get();
if (exportedData != null) exportedData.writeTo(out);
out.closeEntry();
}
} catch (Exception ignored) {
}
Log.e(TAG, "Failed test " + description.getMethodName()
+ ",\nscreenshot will be saved to " + sceenshot
+ ",\nUI dump at: " + hierarchy
+ " (use go/web-hv to open the dump file)", e);
final UiDevice device = launcher.getDevice();
device.takeScreenshot(sceenshot);
// Dump accessibility hierarchy
try {
device.dumpWindowHierarchy(diagFile(description, "AccessibilityHierarchy", "uix"));
} catch (IOException ex) {
Log.e(TAG, "Failed to save accessibility hierarchy", ex);
}
dumpCommand("logcat -d -s TestRunner", diagFile(description, "FilteredLogcat", "txt"));
// Dump bugreport
if (!sSavedBugreport) {
dumpCommand("bugreportz -s", diagFile(description, "Bugreport", "zip"));
// Not saving bugreport for each failure for time and space economy.
sSavedBugreport = true;
}
}
private static void dumpStringCommand(String cmd, OutputStream out) throws IOException {
out.write(("\n\n" + cmd + "\n").getBytes());
dumpCommand(cmd, out);
}
private static void dumpCommand(String cmd, File out) {
try (BufferedOutputStream buffered = new BufferedOutputStream(
new FileOutputStream(out))) {
dumpCommand(cmd, buffered);
} catch (IOException ex) {
}
}
private static void dumpCommand(String cmd, OutputStream out) throws IOException {
try (AutoCloseInputStream in = new AutoCloseInputStream(getInstrumentation()
.getUiAutomation().executeShellCommand(cmd))) {
FileUtils.copy(in, out);
}
}
}
@@ -1,129 +0,0 @@
/*
* 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.util.rule;
import android.os.SystemClock;
import androidx.test.InstrumentationRegistry;
import org.junit.rules.TestRule;
import org.junit.runner.Description;
import org.junit.runners.model.Statement;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.text.SimpleDateFormat;
import java.util.Date;
/**
* A rule that generates a file that helps diagnosing cases when the test process was terminated
* because the test execution took too long, and tests that ran for too long even without being
* terminated. If the process was terminated or the test was long, the test leaves an artifact with
* stack traces of all threads, every SAMPLE_INTERVAL_MS. This will help understanding where we
* stuck.
*/
public class SamplerRule implements TestRule {
private static final int TOO_LONG_TEST_MS = 180000;
private static final int SAMPLE_INTERVAL_MS = 3000;
public static Thread startThread(Description description) {
Thread thread =
new Thread() {
@Override
public void run() {
// Write all-threads stack stace every SAMPLE_INTERVAL_MS while the test
// is running.
// After the test finishes, delete that file. If the test process is
// terminated due to timeout, the trace file won't be deleted.
final File file = getFile();
final long startTime = SystemClock.elapsedRealtime();
try (OutputStreamWriter outputStreamWriter =
new OutputStreamWriter(
new BufferedOutputStream(
new FileOutputStream(file)))) {
writeSamples(outputStreamWriter);
} catch (IOException | InterruptedException e) {
// Simply suppressing the exceptions, nothing to do here.
} finally {
// If the process is not killed, then there was no test timeout, and
// we are not interested in the trace file, unless the test ran too
// long.
if (SystemClock.elapsedRealtime() - startTime < TOO_LONG_TEST_MS) {
file.delete();
}
}
}
private File getFile() {
final String strDate = new SimpleDateFormat("HH:mm:ss").format(new Date());
final String descStr = description.getTestClass().getSimpleName() + "."
+ description.getMethodName();
return artifactFile(
"ThreadStackSamples-" + strDate + "-" + descStr + ".txt");
}
private void writeSamples(OutputStreamWriter writer)
throws IOException, InterruptedException {
int count = 0;
while (true) {
writer.write(
"#"
+ (count++)
+ " =============================================\r\n");
for (StackTraceElement[] stack : getAllStackTraces().values()) {
writer.write("---------------------\r\n");
for (StackTraceElement frame : stack) {
writer.write(frame.toString() + "\r\n");
}
}
writer.flush();
sleep(SAMPLE_INTERVAL_MS);
}
}
};
thread.start();
return thread;
}
@Override
public Statement apply(Statement base, Description description) {
return new Statement() {
@Override
public void evaluate() throws Throwable {
final Thread traceThread = startThread(description);
try {
base.evaluate();
} finally {
traceThread.interrupt();
traceThread.join();
}
}
};
}
private static File artifactFile(String fileName) {
return new File(
InstrumentationRegistry.getInstrumentation().getTargetContext().getFilesDir(),
fileName);
}
}
@@ -1,88 +0,0 @@
/*
* Copyright (C) 2021 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.util.rule;
import static androidx.test.InstrumentationRegistry.getInstrumentation;
import android.app.Instrumentation;
import android.app.UiAutomation;
import android.os.ParcelFileDescriptor;
import android.util.Log;
import androidx.test.uiautomator.UiDevice;
import org.junit.rules.TestRule;
import org.junit.runner.Description;
import org.junit.runners.model.Statement;
import java.io.File;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Rule which captures a screen record for a test.
* After adding this rule to the test class, apply the annotation @ScreenRecord to individual tests
*/
public class ScreenRecordRule implements TestRule {
private static final String TAG = "ScreenRecordRule";
@Override
public Statement apply(Statement base, Description description) {
if (description.getAnnotation(ScreenRecord.class) == null) {
return base;
}
return new Statement() {
@Override
public void evaluate() throws Throwable {
Instrumentation inst = getInstrumentation();
UiAutomation automation = inst.getUiAutomation();
UiDevice device = UiDevice.getInstance(inst);
File outputFile = new File(inst.getTargetContext().getFilesDir(),
"screenrecord-" + description.getMethodName() + ".mp4");
device.executeShellCommand("killall screenrecord");
ParcelFileDescriptor output =
automation.executeShellCommand("screenrecord " + outputFile);
String screenRecordPid = device.executeShellCommand("pidof screenrecord");
boolean success = false;
try {
base.evaluate();
success = true;
} finally {
device.executeShellCommand("kill -INT " + screenRecordPid);
Log.e(TAG, "Screenrecord captured at: " + outputFile);
output.close();
if (success) {
automation.executeShellCommand("rm " + outputFile);
}
}
}
};
}
/**
* Interface to indicate that the test should capture screenrecord
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ScreenRecord {
}
}
@@ -1,23 +0,0 @@
/*
* Copyright (C) 2008 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.util.rule
import android.platform.test.flag.junit.SetFlagsRule
fun SetFlagsRule.setFlags(enabled: Boolean, vararg flagName: String) {
if (enabled) enableFlags(*flagName) else disableFlags(*flagName)
}
@@ -1,125 +0,0 @@
/*
* Copyright (C) 2017 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.util.rule;
import static androidx.test.InstrumentationRegistry.getInstrumentation;
import static com.android.launcher3.tapl.TestHelpers.getLauncherInMyProcess;
import android.content.ComponentName;
import android.content.pm.ActivityInfo;
import androidx.annotation.Nullable;
import androidx.test.InstrumentationRegistry;
import androidx.test.uiautomator.UiDevice;
import com.android.systemui.shared.system.PackageManagerWrapper;
import org.junit.Assert;
import org.junit.rules.TestRule;
import org.junit.runner.Description;
import org.junit.runners.model.Statement;
import java.util.ArrayList;
/**
* Test rule which executes a shell command at the start of the test.
*/
public class ShellCommandRule implements TestRule {
private final String mCmd;
private final String mRevertCommand;
private final boolean mCheckSuccess;
private final Runnable mAdditionalChecks;
public ShellCommandRule(String cmd, @Nullable String revertCommand, boolean checkSuccess,
Runnable additionalChecks) {
mCmd = cmd;
mRevertCommand = revertCommand;
mCheckSuccess = checkSuccess;
mAdditionalChecks = additionalChecks;
}
public ShellCommandRule(String cmd, @Nullable String revertCommand) {
this(cmd, revertCommand, false, null);
}
@Override
public Statement apply(Statement base, Description description) {
return new Statement() {
@Override
public void evaluate() throws Throwable {
final String result =
UiDevice.getInstance(getInstrumentation()).executeShellCommand(mCmd);
if (mCheckSuccess) {
Assert.assertTrue(
"Failed command: " + mCmd + ", result: " + result,
"Success".equals(result.replaceAll("\\s", "")));
}
if (mAdditionalChecks != null) mAdditionalChecks.run();
try {
base.evaluate();
} finally {
if (mRevertCommand != null) {
final String revertResult = UiDevice.getInstance(
getInstrumentation()).executeShellCommand(
mRevertCommand);
if (mCheckSuccess) {
Assert.assertTrue(
"Failed command: " + mRevertCommand
+ ", result: " + revertResult,
"Success".equals(result.replaceAll("\\s", "")));
}
}
}
}
};
}
/**
* Grants the launcher permission to bind widgets.
*/
public static ShellCommandRule grantWidgetBind() {
return new ShellCommandRule("appwidget grantbind --package "
+ InstrumentationRegistry.getTargetContext().getPackageName(), null);
}
/**
* Sets the target launcher as default launcher.
*/
public static ShellCommandRule setDefaultLauncher() {
final ActivityInfo launcher = getLauncherInMyProcess();
return new ShellCommandRule(getLauncherCommand(launcher), null, true, () ->
Assert.assertEquals("Setting default launcher failed",
new ComponentName(launcher.packageName, launcher.name)
.flattenToString(),
PackageManagerWrapper.getInstance().getHomeActivities(new ArrayList<>())
.flattenToString()));
}
public static String getLauncherCommand(ActivityInfo launcher) {
return "cmd package set-home-activity " +
new ComponentName(launcher.packageName, launcher.name).flattenToString();
}
/**
* Disables heads up notification for the duration of the test
*/
public static ShellCommandRule disableHeadsUpNotification() {
return new ShellCommandRule("settings put global heads_up_notifications_enabled 0",
"settings put global heads_up_notifications_enabled 1");
}
}
@@ -1,55 +0,0 @@
/*
* 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.util.rule;
import androidx.annotation.NonNull;
import androidx.test.InstrumentationRegistry;
import androidx.test.uiautomator.UiDevice;
import com.android.launcher3.tapl.LauncherInstrumentation;
import com.android.launcher3.ui.AbstractLauncherUiTest;
import org.junit.rules.TestRule;
import org.junit.runner.Description;
import org.junit.runners.model.Statement;
/**
* Isolates tests from some of the state created by the previous test.
*/
public class TestIsolationRule implements TestRule {
private final LauncherInstrumentation mLauncher;
private final boolean mRequireOneActiveActivity;
public TestIsolationRule(LauncherInstrumentation launcher, boolean requireOneActiveActivity) {
mLauncher = launcher;
mRequireOneActiveActivity = requireOneActiveActivity;
}
@NonNull
@Override
public Statement apply(@NonNull Statement base, @NonNull Description description) {
return new Statement() {
@Override
public void evaluate() throws Throwable {
base.evaluate();
// Make sure that Launcher workspace looks correct.
UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()).pressHome();
AbstractLauncherUiTest.checkDetectedLeaks(mLauncher, mRequireOneActiveActivity);
}
};
}
}
@@ -1,158 +0,0 @@
/*
* Copyright (C) 2019 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.util.rule;
import static androidx.test.InstrumentationRegistry.getInstrumentation;
import static org.junit.Assume.assumeTrue;
import android.content.pm.PackageManager;
import android.os.Build;
import android.util.Log;
import androidx.test.InstrumentationRegistry;
import androidx.test.uiautomator.UiDevice;
import org.junit.rules.TestRule;
import org.junit.runner.Description;
import org.junit.runners.model.Statement;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class TestStabilityRule implements TestRule {
private static final String TAG = "TestStabilityRule";
private static final Pattern LAUNCHER_BUILD =
Pattern.compile("^("
+ "(?<local>(BuildFromAndroidStudio|"
+ "([0-9]+|[A-Z])-eng\\.[a-z]+\\.[0-9]+\\.[0-9]+))|"
+ "(?<platform>([A-Z][a-z]*[0-9]*|[0-9]+)*)"
+ ")$");
private static final Pattern PLATFORM_BUILD =
Pattern.compile("^("
+ "(?<commandLine>eng\\.[a-z]+\\.[0-9]+\\.[0-9]+)|"
+ "(?<presubmit>P[0-9]+)|"
+ "(?<postsubmit>[0-9]+)"
+ ")$");
public static final int LOCAL = 0x1;
public static final int PLATFORM_PRESUBMIT = 0x8;
public static final int PLATFORM_POSTSUBMIT = 0x10;
private static int sRunFlavor;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Stability {
int flavors();
}
@Override
public Statement apply(Statement base, Description description) {
final Stability stability = description.getAnnotation(Stability.class);
if (stability != null) {
return new Statement() {
@Override
public void evaluate() throws Throwable {
assumeTrue("Ignoring the test due to @Stability annotation",
(stability.flavors() & getRunFlavor()) != 0);
base.evaluate();
}
};
} else {
return base;
}
}
public static int getRunFlavor() {
if (sRunFlavor != 0) return sRunFlavor;
if (isRobolectricTest()) return PLATFORM_POSTSUBMIT;
final String flavorOverride = InstrumentationRegistry.getArguments().getString("flavor");
if (flavorOverride != null) {
Log.d(TAG, "Flavor override: " + flavorOverride);
try {
return (int) TestStabilityRule.class.getField(flavorOverride).get(null);
} catch (NoSuchFieldException e) {
throw new AssertionError("Unrecognized run flavor override: " + flavorOverride);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
final String launcherVersion;
try {
final String launcherPackageName = UiDevice.getInstance(getInstrumentation())
.getLauncherPackageName();
Log.d(TAG, "Launcher package: " + launcherPackageName);
launcherVersion = getInstrumentation().
getContext().
getPackageManager().
getPackageInfo(launcherPackageName, 0)
.versionName;
} catch (PackageManager.NameNotFoundException e) {
throw new RuntimeException(e);
}
final String platformVersion = Build.VERSION.INCREMENTAL;
Log.d(TAG, "Launcher: " + launcherVersion + ", platform: " + platformVersion);
final Matcher launcherBuildMatcher = LAUNCHER_BUILD.matcher(launcherVersion);
if (!launcherBuildMatcher.find()) {
throw new AssertionError("Launcher build match not found");
}
final Matcher platformBuildMatcher = PLATFORM_BUILD.matcher(platformVersion);
if (!platformBuildMatcher.find()) {
throw new AssertionError("Platform build match not found");
}
if (launcherBuildMatcher.group("local") != null && (
platformBuildMatcher.group("commandLine") != null ||
platformBuildMatcher.group("postsubmit") != null)) {
Log.d(TAG, "LOCAL RUN");
sRunFlavor = LOCAL;
} else if (launcherBuildMatcher.group("platform") != null
&& platformBuildMatcher.group("presubmit") != null) {
Log.d(TAG, "PLATFORM PRESUBMIT");
sRunFlavor = PLATFORM_PRESUBMIT;
} else if (launcherBuildMatcher.group("platform") != null
&& (platformBuildMatcher.group("postsubmit") != null
|| platformBuildMatcher.group("commandLine") != null)) {
Log.d(TAG, "PLATFORM POSTSUBMIT");
sRunFlavor = PLATFORM_POSTSUBMIT;
} else {
throw new AssertionError("Unrecognized run flavor");
}
return sRunFlavor;
}
public static boolean isPresubmit() {
return getRunFlavor() == PLATFORM_PRESUBMIT;
}
public static boolean isRobolectricTest() {
return Build.FINGERPRINT.contains("robolectric");
}
}
@@ -1,168 +0,0 @@
/*
* 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.util.rule
import android.app.Activity
import android.app.Application
import android.media.permission.SafeCloseable
import android.os.Bundle
import androidx.test.core.app.ApplicationProvider
import androidx.test.platform.app.InstrumentationRegistry
import com.android.app.viewcapture.SimpleViewCapture
import com.android.app.viewcapture.ViewCapture.MAIN_EXECUTOR
import com.android.app.viewcapture.data.ExportedData
import com.android.launcher3.tapl.TestHelpers
import com.android.launcher3.util.ActivityLifecycleCallbacksAdapter
import com.android.launcher3.util.rule.TestStabilityRule.PLATFORM_POSTSUBMIT
import com.android.launcher3.util.viewcapture_analysis.ViewCaptureAnalyzer
import java.io.BufferedOutputStream
import java.io.FileOutputStream
import java.io.IOException
import java.io.OutputStreamWriter
import java.util.function.Supplier
import org.junit.Assert.assertTrue
import org.junit.Assert.fail
import org.junit.rules.TestRule
import org.junit.runner.Description
import org.junit.runners.model.Statement
/**
* This JUnit TestRule registers a listener for activity lifecycle events to attach a ViewCapture
* instance that other test rules use to dump the timelapse hierarchy upon an error during a test.
*
* This rule will not work in OOP tests that don't have access to the activity under test.
*/
class ViewCaptureRule(var alreadyOpenActivitySupplier: Supplier<Activity?>) : TestRule {
private val viewCapture = SimpleViewCapture("test-view-capture")
var viewCaptureData: ExportedData? = null
private set
override fun apply(base: Statement, description: Description): Statement {
// Skip view capture collection in Launcher3 tests to avoid hidden API check exception.
if (
"com.android.launcher3.tests" ==
InstrumentationRegistry.getInstrumentation().context.packageName
)
return base
return object : Statement() {
override fun evaluate() {
viewCaptureData = null
val windowListenerCloseables = mutableListOf<SafeCloseable>()
startCapturingExistingActivity(windowListenerCloseables)
val lifecycleCallbacks =
object : ActivityLifecycleCallbacksAdapter {
override fun onActivityCreated(activity: Activity, bundle: Bundle?) {
startCapture(windowListenerCloseables, activity)
}
override fun onActivityDestroyed(activity: Activity) {
viewCapture.stopCapture(activity.window.decorView)
}
}
val application = ApplicationProvider.getApplicationContext<Application>()
application.registerActivityLifecycleCallbacks(lifecycleCallbacks)
try {
base.evaluate()
} finally {
application.unregisterActivityLifecycleCallbacks(lifecycleCallbacks)
viewCaptureData =
viewCapture.getExportedData(ApplicationProvider.getApplicationContext())
// Clean up ViewCapture references here rather than in onActivityDestroyed so
// test code can access view hierarchy capture. onActivityDestroyed would delete
// view capture data before FailureWatcher could output it as a test artifact.
// This is on the main thread to avoid a race condition where the onDrawListener
// is removed while onDraw is running, resulting in an IllegalStateException.
MAIN_EXECUTOR.execute { windowListenerCloseables.onEach(SafeCloseable::close) }
}
analyzeViewCapture(description)
}
private fun startCapturingExistingActivity(
windowListenerCloseables: MutableCollection<SafeCloseable>
) {
val alreadyOpenActivity = alreadyOpenActivitySupplier.get()
if (alreadyOpenActivity != null) {
startCapture(windowListenerCloseables, alreadyOpenActivity)
}
}
private fun startCapture(
windowListenerCloseables: MutableCollection<SafeCloseable>,
activity: Activity
) {
windowListenerCloseables.add(
viewCapture.startCapture(
activity.window.decorView,
"${description.testClass?.simpleName}.${description.methodName}"
)
)
}
}
}
private fun analyzeViewCapture(description: Description) {
// OOP tests don't produce ViewCapture data
if (!TestHelpers.isInLauncherProcess()) return
// Due to flakiness of ViewCapture verifier, don't run the check in presubmit
if (TestStabilityRule.getRunFlavor() != PLATFORM_POSTSUBMIT) return
var frameCount = 0
for (i in 0 until viewCaptureData!!.windowDataCount) {
frameCount += viewCaptureData!!.getWindowData(i).frameDataCount
}
val mayProduceNoFrames = description.getAnnotation(MayProduceNoFrames::class.java) != null
assertTrue("Empty ViewCapture data", mayProduceNoFrames || frameCount > 0)
val anomalies: Map<String, String> = ViewCaptureAnalyzer.getAnomalies(viewCaptureData)
if (!anomalies.isEmpty()) {
val diagFile = FailureWatcher.diagFile(description, "ViewAnomalies", "txt")
try {
OutputStreamWriter(BufferedOutputStream(FileOutputStream(diagFile))).use { writer ->
writer.write("View animation anomalies detected.\r\n")
writer.write(
"To suppress an anomaly for a view, add its full path to the PATHS_TO_IGNORE list in the corresponding AnomalyDetector.\r\n"
)
writer.write("List of views with animation anomalies:\r\n")
for ((viewPath, message) in anomalies) {
writer.write("View: $viewPath\r\n $message\r\n")
}
}
} catch (ex: IOException) {
throw RuntimeException(ex)
}
val (viewPath, message) = anomalies.entries.first()
fail(
"${anomalies.size} view(s) had animation anomalies during the test, including view: $viewPath: $message\r\nSee ${diagFile.name} for details."
)
}
}
@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.FUNCTION)
annotation class MayProduceNoFrames
}
@@ -1,184 +0,0 @@
/*
* 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.util.viewcapture_analysis;
import com.android.launcher3.util.viewcapture_analysis.ViewCaptureAnalyzer.AnalysisNode;
import java.util.List;
/**
* Anomaly detector that triggers an error when alpha of a view changes too rapidly.
* Invisible views are treated as if they had zero alpha.
*/
final class AlphaJumpDetector extends AnomalyDetector {
// Commonly used parts of the paths to ignore.
private static final String CONTENT = "DecorView|LinearLayout|FrameLayout:id/content|";
private static final String DRAG_LAYER =
CONTENT + "LauncherRootView:id/launcher|DragLayer:id/drag_layer|";
private static final String RECENTS_DRAG_LAYER =
CONTENT + "LauncherRootView:id/launcher|RecentsDragLayer:id/drag_layer|";
private static final IgnoreNode IGNORED_NODES_ROOT = buildIgnoreNodesTree(List.of(
CONTENT
+ "SimpleDragLayer:id/add_item_drag_layer|AddItemWidgetsBottomSheet:id"
+ "/add_item_bottom_sheet|LinearLayout:id/add_item_bottom_sheet_content"
+ "|ScrollView:id/widget_preview_scroll_view|WidgetCell:id/widget_cell"
+ "|WidgetCellPreview:id/widget_preview_container|ImageView:id/widget_badge",
CONTENT
+ "SimpleDragLayer:id/add_item_drag_layer|AddItemWidgetsBottomSheet:id"
+ "/add_item_bottom_sheet|LinearLayout:id/add_item_bottom_sheet_content"
+ "|ScrollView:id/widget_preview_scroll_view|WidgetCell:id/widget_cell"
+ "|WidgetCellPreview:id/widget_preview_container|WidgetCell$1|FrameLayout"
+ "|ImageView:id/icon",
CONTENT + "SimpleDragLayer:id/add_item_drag_layer|View",
DRAG_LAYER
+ "AppWidgetResizeFrame|FrameLayout|ImageButton:id/widget_reconfigure_button",
DRAG_LAYER
+ "AppWidgetResizeFrame|FrameLayout|ImageView:id/widget_resize_bottom_handle",
DRAG_LAYER + "AppWidgetResizeFrame|FrameLayout|ImageView:id/widget_resize_frame",
DRAG_LAYER + "AppWidgetResizeFrame|FrameLayout|ImageView:id/widget_resize_left_handle",
DRAG_LAYER + "AppWidgetResizeFrame|FrameLayout|ImageView:id/widget_resize_right_handle",
DRAG_LAYER + "AppWidgetResizeFrame|FrameLayout|ImageView:id/widget_resize_top_handle",
DRAG_LAYER + "FloatingTaskView|FloatingTaskThumbnailView:id/thumbnail",
DRAG_LAYER + "FloatingTaskView|SplitPlaceholderView:id/split_placeholder",
DRAG_LAYER + "Folder|FolderPagedView:id/folder_content",
DRAG_LAYER + "LauncherAllAppsContainerView:id/apps_view",
DRAG_LAYER + "LauncherDragView",
DRAG_LAYER + "LauncherRecentsView:id/overview_panel",
DRAG_LAYER
+ "NexusOverviewActionsView:id/overview_actions_view|FrameLayout:id"
+ "/select_mode_buttons|ImageButton:id/close",
DRAG_LAYER
+ "PopupContainerWithArrow:id/popup_container|LinearLayout:id"
+ "/deep_shortcuts_container|DeepShortcutView:id/deep_shortcut_material"
+ "|DeepShortcutTextView:id/bubble_text",
DRAG_LAYER
+ "PopupContainerWithArrow:id/popup_container|LinearLayout:id"
+ "/deep_shortcuts_container|DeepShortcutView:id/deep_shortcut_material|View"
+ ":id/icon",
DRAG_LAYER
+ "PopupContainerWithArrow:id/popup_container|LinearLayout:id"
+ "/system_shortcuts_container|DeepShortcutView:id/system_shortcut"
+ "|BubbleTextView:id/bubble_text",
DRAG_LAYER
+ "PopupContainerWithArrow:id/popup_container|LinearLayout:id"
+ "/system_shortcuts_container|DeepShortcutView:id/system_shortcut|View:id"
+ "/icon",
DRAG_LAYER
+ "PopupContainerWithArrow:id/popup_container|LinearLayout:id"
+ "/system_shortcuts_container|ImageView",
DRAG_LAYER
+ "PopupContainerWithArrow:id/popup_container|LinearLayout:id"
+ "/widget_shortcut_container|DeepShortcutView:id/system_shortcut"
+ "|BubbleTextView:id/bubble_text",
DRAG_LAYER
+ "PopupContainerWithArrow:id/popup_container|LinearLayout:id"
+ "/widget_shortcut_container|DeepShortcutView:id/system_shortcut|View:id/icon",
DRAG_LAYER + "SearchContainerView:id/apps_view",
DRAG_LAYER + "Snackbar|TextView:id/action",
DRAG_LAYER + "Snackbar|TextView:id/label",
DRAG_LAYER + "SplitInstructionsView|AppCompatTextView:id/split_instructions_text",
DRAG_LAYER + "TaskMenuView|LinearLayout:id/menu_option_layout",
DRAG_LAYER + "TaskMenuViewWithArrow|LinearLayout:id/menu_option_layout",
DRAG_LAYER + "TaskMenuView|TextView:id/task_name",
DRAG_LAYER + "View",
DRAG_LAYER + "WidgetsFullSheet|SpringRelativeLayout:id/container",
DRAG_LAYER + "WidgetsTwoPaneSheet|SpringRelativeLayout:id/container",
CONTENT + "LauncherRootView:id/launcher|FloatingIconView",
RECENTS_DRAG_LAYER + "ArrowTipView",
DRAG_LAYER + "ArrowTipView",
DRAG_LAYER + "FallbackRecentsView:id/overview_panel",
RECENTS_DRAG_LAYER + "FallbackRecentsView:id/overview_panel",
DRAG_LAYER
+ "NexusOverviewActionsView:id/overview_actions_view"
+ "|LinearLayout:id/action_buttons",
RECENTS_DRAG_LAYER
+ "NexusOverviewActionsView:id/overview_actions_view"
+ "|LinearLayout:id/action_buttons",
DRAG_LAYER + "IconView",
DRAG_LAYER
+ "OptionsPopupView:id/popup_container|DeepShortcutView:id/system_shortcut"
+ "|BubbleTextView:id/bubble_text"
));
// Minimal increase or decrease of view's alpha between frames that triggers the error.
private static final float ALPHA_JUMP_THRESHOLD = 1f;
// Per-AnalysisNode data that's specific to this detector.
private static class NodeData {
public boolean ignoreAlphaJumps;
// If ignoreNode is null, then this AnalysisNode node will be ignored if its parent is
// ignored.
// Otherwise, this AnalysisNode will be ignored if ignoreNode is a leaf i.e. has no
// children.
public IgnoreNode ignoreNode;
}
private NodeData getNodeData(AnalysisNode info) {
return (NodeData) info.detectorsData[detectorOrdinal];
}
@Override
void initializeNode(AnalysisNode info) {
final NodeData nodeData = new NodeData();
info.detectorsData[detectorOrdinal] = nodeData;
// If the parent view ignores alpha jumps, its descendants will too.
final boolean parentIgnoresAlphaJumps = info.parent != null && getNodeData(
info.parent).ignoreAlphaJumps;
if (parentIgnoresAlphaJumps) {
nodeData.ignoreAlphaJumps = true;
return;
}
// Parent view doesn't ignore alpha jumps.
// Initialize this AnalysisNode's ignore-node with the corresponding child of the
// ignore-node of the parent, if present.
final IgnoreNode parentIgnoreNode = info.parent != null
? getNodeData(info.parent).ignoreNode
: IGNORED_NODES_ROOT;
nodeData.ignoreNode = parentIgnoreNode != null
? parentIgnoreNode.children.get(info.nodeIdentity) : null;
// AnalysisNode will be ignored if the corresponding ignore-node is a leaf.
nodeData.ignoreAlphaJumps =
nodeData.ignoreNode != null && nodeData.ignoreNode.children.isEmpty();
}
@Override
String detectAnomalies(AnalysisNode oldInfo, AnalysisNode newInfo, int frameN, long timestamp,
int windowSizePx) {
// If the view was previously seen, proceed with analysis only if it was present in the
// view hierarchy in the previous frame.
if (oldInfo != null && oldInfo.frameN != frameN) return null;
final AnalysisNode latestInfo = newInfo != null ? newInfo : oldInfo;
final NodeData nodeData = getNodeData(latestInfo);
if (nodeData.ignoreAlphaJumps) return null;
final float oldAlpha = oldInfo != null ? oldInfo.alpha : 0;
final float newAlpha = newInfo != null ? newInfo.alpha : 0;
final float alphaDeltaAbs = Math.abs(newAlpha - oldAlpha);
if (alphaDeltaAbs >= ALPHA_JUMP_THRESHOLD) {
nodeData.ignoreAlphaJumps = true; // No need to report alpha jump in children.
return String.format(
"Alpha jump detected: alpha change: %s (%s -> %s), threshold: %s",
alphaDeltaAbs, oldAlpha, newAlpha, ALPHA_JUMP_THRESHOLD);
}
return null;
}
}
@@ -1,85 +0,0 @@
/*
* 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.util.viewcapture_analysis;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.util.HashMap;
import java.util.Map;
/**
* Detector of one kind of anomaly.
*/
abstract class AnomalyDetector {
// Index of this detector in ViewCaptureAnalyzer.ANOMALY_DETECTORS
public int detectorOrdinal;
/**
* Element of the tree of ignored nodes.
* If the "children" map is empty, then this node should be ignored, i.e. the analysis shouldn't
* run for it.
* I.e. ignored nodes correspond to the leaves in the ignored nodes tree.
*/
protected static class IgnoreNode {
// Map from child node identities to ignore-nodes for these children.
public final Map<String, IgnoreNode> children = new HashMap<>();
}
// Converts the list of full paths of nodes to ignore to a more efficient tree of ignore-nodes.
protected static IgnoreNode buildIgnoreNodesTree(Iterable<String> pathsToIgnore) {
final IgnoreNode root = new IgnoreNode();
for (String pathToIgnore : pathsToIgnore) {
// Scan the diag path of an ignored node and add its elements into the tree.
IgnoreNode currentIgnoreNode = root;
for (String part : pathToIgnore.split("\\|")) {
// Ensure that the child of the node is added to the tree.
IgnoreNode child = currentIgnoreNode.children.get(part);
if (child == null) {
currentIgnoreNode.children.put(part, child = new IgnoreNode());
}
currentIgnoreNode = child;
}
}
return root;
}
/**
* Initializes fields of the node that are specific to the anomaly detected by this
* detector.
*/
abstract void initializeNode(@NonNull ViewCaptureAnalyzer.AnalysisNode info);
/**
* Detects anomalies by looking at the last occurrence of a view, and the current one.
* null value means that the view. 'oldInfo' and 'newInfo' cannot be both null.
* If an anomaly is detected, an exception will be thrown.
*
* @param oldInfo the view, as seen in the last frame that contained it in the view
* hierarchy before 'currentFrame'. 'null' means that the view is first seen
* in the 'currentFrame'.
* @param newInfo the view in the view hierarchy of the 'currentFrame'. 'null' means that
* the view is not present in the 'currentFrame', but was present in the
* previous frame.
* @param frameN number of the current frame.
* @param windowSizePx maximum of the window width and height, in pixels.
* @return Anomaly diagnostic message if an anomaly has been detected; null otherwise.
*/
abstract String detectAnomalies(
@Nullable ViewCaptureAnalyzer.AnalysisNode oldInfo,
@Nullable ViewCaptureAnalyzer.AnalysisNode newInfo, int frameN,
long frameTimeNs, int windowSizePx);
}
@@ -1,180 +0,0 @@
/*
* 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.util.viewcapture_analysis;
import static org.junit.Assert.assertTrue;
import com.android.launcher3.util.viewcapture_analysis.ViewCaptureAnalyzer.AnalysisNode;
import java.util.List;
/**
* Anomaly detector that triggers an error when a view flashes, i.e. appears or disappears for a too
* short period of time.
*/
final class FlashDetector extends AnomalyDetector {
// Maximum time period of a view visibility or invisibility that is recognized as a flash.
private static final int FLASH_DURATION_MS = 300;
// Commonly used parts of the paths to ignore.
private static final String CONTENT = "DecorView|LinearLayout|FrameLayout:id/content|";
private static final String DRAG_LAYER =
CONTENT + "LauncherRootView:id/launcher|DragLayer:id/drag_layer|";
private static final String RECENTS_DRAG_LAYER =
CONTENT + "LauncherRootView:id/launcher|RecentsDragLayer:id/drag_layer|";
private static final IgnoreNode IGNORED_NODES_ROOT = buildIgnoreNodesTree(List.of(
CONTENT + "LauncherRootView:id/launcher|FloatingIconView",
DRAG_LAYER + "LauncherRecentsView:id/overview_panel|TaskView",
DRAG_LAYER + "LauncherRecentsView:id/overview_panel|ClearAllButton:id/clear_all",
DRAG_LAYER
+ "LauncherAllAppsContainerView:id/apps_view|AllAppsRecyclerView:id"
+ "/apps_list_view|BubbleTextView:id/icon",
CONTENT
+ "SimpleDragLayer:id/add_item_drag_layer|AddItemWidgetsBottomSheet:id"
+ "/add_item_bottom_sheet|LinearLayout:id/add_item_bottom_sheet_content"
+ "|ScrollView:id/widget_preview_scroll_view|WidgetCell:id/widget_cell"
+ "|WidgetCellPreview:id/widget_preview_container|WidgetImageView:id"
+ "/widget_preview",
CONTENT
+ "SimpleDragLayer:id/add_item_drag_layer|AddItemWidgetsBottomSheet:id"
+ "/add_item_bottom_sheet|LinearLayout:id/add_item_bottom_sheet_content"
+ "|ScrollView:id/widget_preview_scroll_view|WidgetCell:id/widget_cell"
+ "|WidgetCellPreview:id/widget_preview_container|ImageView:id/widget_badge",
RECENTS_DRAG_LAYER + "FallbackRecentsView:id/overview_panel|TaskView",
RECENTS_DRAG_LAYER
+ "FallbackRecentsView:id/overview_panel|ClearAllButton:id/clear_all",
DRAG_LAYER + "SearchContainerView:id/apps_view",
DRAG_LAYER + "LauncherDragView",
DRAG_LAYER + "FloatingTaskView|FloatingTaskThumbnailView:id/thumbnail",
DRAG_LAYER
+ "WidgetsFullSheet|SpringRelativeLayout:id/container|WidgetsRecyclerView:id"
+ "/primary_widgets_list_view|WidgetsListHeader:id/widgets_list_header",
DRAG_LAYER
+ "WidgetsTwoPaneSheet|SpringRelativeLayout:id/container|LinearLayout:id"
+ "/linear_layout_container|FrameLayout:id/recycler_view_container"
+ "|FrameLayout:id/widgets_two_pane_sheet_recyclerview|WidgetsRecyclerView:id"
+ "/primary_widgets_list_view|WidgetsListHeader:id/widgets_list_header",
DRAG_LAYER + "NexusOverviewActionsView:id/overview_actions_view"
));
// Per-AnalysisNode data that's specific to this detector.
private static class NodeData {
public boolean ignoreFlashes;
// If ignoreNode is null, then this AnalysisNode node will be ignored if its parent is
// ignored.
// Otherwise, this AnalysisNode will be ignored if ignoreNode is a leaf i.e. has no
// children.
public IgnoreNode ignoreNode;
}
private NodeData getNodeData(AnalysisNode info) {
return (NodeData) info.detectorsData[detectorOrdinal];
}
@Override
void initializeNode(AnalysisNode info) {
final NodeData nodeData = new NodeData();
info.detectorsData[detectorOrdinal] = nodeData;
// If the parent view ignores flashes, its descendants will too.
final boolean parentIgnoresFlashes = info.parent != null && getNodeData(
info.parent).ignoreFlashes;
if (parentIgnoresFlashes) {
nodeData.ignoreFlashes = true;
return;
}
// Parent view doesn't ignore flashes.
// Initialize this AnalysisNode's ignore-node with the corresponding child of the
// ignore-node of the parent, if present.
final IgnoreNode parentIgnoreNode = info.parent != null
? getNodeData(info.parent).ignoreNode
: IGNORED_NODES_ROOT;
nodeData.ignoreNode = parentIgnoreNode != null
? parentIgnoreNode.children.get(info.nodeIdentity) : null;
// AnalysisNode will be ignored if the corresponding ignore-node is a leaf.
nodeData.ignoreFlashes =
nodeData.ignoreNode != null && nodeData.ignoreNode.children.isEmpty();
}
@Override
String detectAnomalies(AnalysisNode oldInfo, AnalysisNode newInfo, int frameN,
long frameTimeNs, int windowSizePx) {
// Should we check when a view was visible for a short period, then its alpha became 0?
// Then 'lastVisible' time should be the last one still visible?
// Check only transitions of alpha between 0 and 1?
// If this is the first time ever when we see the view, there have been no flashes yet.
if (oldInfo == null) return null;
// A flash requires a view to go from the full visibility to no-visibility and then back,
// or vice versa.
// If the last time the view was seen before the current frame, it didn't have full
// visibility; no flash can possibly be detected at the current frame.
if (oldInfo.alpha < 1) return null;
final AnalysisNode latestInfo = newInfo != null ? newInfo : oldInfo;
final NodeData nodeData = getNodeData(latestInfo);
if (nodeData.ignoreFlashes) return null;
// Once the view becomes invisible, see for how long it was visible prior to that. If it
// was visible only for a short interval of time, it's a flash.
if (
// View is invisible in the current frame
newInfo == null
// When the view became visible last time, it was a transition from
// no-visibility to full visibility.
&& oldInfo.timeBecameVisibleNs != -1) {
final long wasVisibleTimeMs = (frameTimeNs - oldInfo.timeBecameVisibleNs) / 1000000;
if (wasVisibleTimeMs <= FLASH_DURATION_MS) {
nodeData.ignoreFlashes = true; // No need to report flashes in children.
return
String.format(
"View was visible for a too short period of time %dms, which is a"
+ " flash",
wasVisibleTimeMs
);
}
}
// Once a view becomes visible, see for how long it was invisible prior to that. If it
// was invisible only for a short interval of time, it's a flash.
if (
// The view is fully visible now
newInfo != null && newInfo.alpha >= 1
// The view wasn't visible in the previous frame
&& frameN != oldInfo.frameN + 1) {
// We can assert the below condition because at this point, we know that
// oldInfo.alpha >= 1, i.e. it disappeared abruptly.
assertTrue("oldInfo.timeBecameInvisibleNs must not be -1",
oldInfo.timeBecameInvisibleNs != -1);
final long wasInvisibleTimeMs = (frameTimeNs - oldInfo.timeBecameInvisibleNs) / 1000000;
if (wasInvisibleTimeMs <= FLASH_DURATION_MS) {
nodeData.ignoreFlashes = true; // No need to report flashes in children.
return
String.format(
"View was invisible for a too short period of time %dms, which "
+ "is a flash",
wasInvisibleTimeMs);
}
}
return null;
}
}
@@ -1,126 +0,0 @@
/*
* 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.util.viewcapture_analysis;
import com.android.launcher3.util.viewcapture_analysis.ViewCaptureAnalyzer.AnalysisNode;
import java.util.List;
/**
* Anomaly detector that triggers an error when a view position jumps.
*/
final class PositionJumpDetector extends AnomalyDetector {
// Maximum allowed jump in "milliwindows", i.e. a 1/1000's of the maximum of the window
// dimensions.
private static final float JUMP_MIW = 250;
private static final String[] BORDER_NAMES = {"left", "top", "right", "bottom"};
// Commonly used parts of the paths to ignore.
private static final String CONTENT = "DecorView|LinearLayout|FrameLayout:id/content|";
private static final String DRAG_LAYER =
CONTENT + "LauncherRootView:id/launcher|DragLayer:id/drag_layer|";
private static final String RECENTS_DRAG_LAYER =
CONTENT + "LauncherRootView:id/launcher|RecentsDragLayer:id/drag_layer|";
private static final IgnoreNode IGNORED_NODES_ROOT = buildIgnoreNodesTree(List.of(
DRAG_LAYER + "SearchContainerView:id/apps_view",
DRAG_LAYER + "AppWidgetResizeFrame",
DRAG_LAYER + "LauncherAllAppsContainerView:id/apps_view",
CONTENT
+ "SimpleDragLayer:id/add_item_drag_layer|AddItemWidgetsBottomSheet:id"
+ "/add_item_bottom_sheet|LinearLayout:id/add_item_bottom_sheet_content",
DRAG_LAYER + "WidgetsTwoPaneSheet|SpringRelativeLayout:id/container",
DRAG_LAYER + "WidgetsFullSheet|SpringRelativeLayout:id/container",
DRAG_LAYER + "LauncherDragView",
RECENTS_DRAG_LAYER + "FallbackRecentsView:id/overview_panel",
CONTENT + "LauncherRootView:id/launcher|FloatingIconView",
DRAG_LAYER + "FloatingTaskView",
DRAG_LAYER + "LauncherRecentsView:id/overview_panel"
));
// Per-AnalysisNode data that's specific to this detector.
private static class NodeData {
public boolean ignoreJumps;
// If ignoreNode is null, then this AnalysisNode node will be ignored if its parent is
// ignored.
// Otherwise, this AnalysisNode will be ignored if ignoreNode is a leaf i.e. has no
// children.
public IgnoreNode ignoreNode;
}
private NodeData getNodeData(AnalysisNode info) {
return (NodeData) info.detectorsData[detectorOrdinal];
}
@Override
void initializeNode(AnalysisNode info) {
final NodeData nodeData = new NodeData();
info.detectorsData[detectorOrdinal] = nodeData;
// If the parent view ignores jumps, its descendants will too.
final boolean parentIgnoresJumps = info.parent != null && getNodeData(
info.parent).ignoreJumps;
if (parentIgnoresJumps) {
nodeData.ignoreJumps = true;
return;
}
// Parent view doesn't ignore jumps.
// Initialize this AnalysisNode's ignore-node with the corresponding child of the
// ignore-node of the parent, if present.
final IgnoreNode parentIgnoreNode = info.parent != null
? getNodeData(info.parent).ignoreNode
: IGNORED_NODES_ROOT;
nodeData.ignoreNode = parentIgnoreNode != null
? parentIgnoreNode.children.get(info.nodeIdentity) : null;
// AnalysisNode will be ignored if the corresponding ignore-node is a leaf.
nodeData.ignoreJumps =
nodeData.ignoreNode != null && nodeData.ignoreNode.children.isEmpty();
}
@Override
String detectAnomalies(AnalysisNode oldInfo, AnalysisNode newInfo, int frameN,
long frameTimeNs, int windowSizePx) {
// If the view is not present in the current frame, there can't be a jump detected in the
// current frame.
if (newInfo == null) return null;
// We only detect position jumps if the view was visible in the previous frame.
if (oldInfo == null || frameN != oldInfo.frameN + 1) return null;
final NodeData newNodeData = getNodeData(newInfo);
if (newNodeData.ignoreJumps) return null;
final float[] positionDiffs = {
newInfo.left - oldInfo.left,
newInfo.top - oldInfo.top,
newInfo.right - oldInfo.right,
newInfo.bottom - oldInfo.bottom
};
for (int i = 0; i < 4; ++i) {
final float positionDiffAbs = Math.abs(positionDiffs[i]);
if (positionDiffAbs * 1000 > JUMP_MIW * windowSizePx) {
newNodeData.ignoreJumps = true;
return String.format("Position jump: %s jumped by %s",
BORDER_NAMES[i], positionDiffAbs);
}
}
return null;
}
}
@@ -1,315 +0,0 @@
/*
* 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.util.viewcapture_analysis;
import static android.view.View.VISIBLE;
import com.android.app.viewcapture.data.ExportedData;
import com.android.app.viewcapture.data.FrameData;
import com.android.app.viewcapture.data.ViewNode;
import com.android.app.viewcapture.data.WindowData;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
/**
* Utility that analyzes ViewCapture data and finds anomalies such as views appearing or
* disappearing without alpha-fading.
*/
public class ViewCaptureAnalyzer {
private static final String SCRIM_VIEW_CLASS = "com.android.launcher3.views.ScrimView";
// All detectors. They will be invoked in the order listed here.
private static final AnomalyDetector[] ANOMALY_DETECTORS = {
// new AlphaJumpDetector(), // b/309014345
// new FlashDetector(), // b/309014345
new PositionJumpDetector()
};
static {
for (int i = 0; i < ANOMALY_DETECTORS.length; ++i) ANOMALY_DETECTORS[i].detectorOrdinal = i;
}
// A view from view capture data converted to a form that's convenient for detecting anomalies.
static class AnalysisNode {
public String className;
public String resourceId;
public AnalysisNode parent;
// Window coordinates of the view.
public float left;
public float top;
public float right;
public float bottom;
// Visible scale and alpha, build recursively from the ancestor list.
public float scaleX;
public float scaleY;
public float alpha; // Always > 0
public int frameN;
// Timestamp of the frame when this view became abruptly visible, i.e. its alpha became 1
// the next frame after it was 0 or the view wasn't visible.
// If the view is currently invisible or the last appearance wasn't abrupt, the value is -1.
public long timeBecameVisibleNs;
// Timestamp of the frame when this view became abruptly invisible last time, i.e. its
// alpha became 0, or view disappeared, after being 1 in the previous frame.
// If the view is currently visible or the last disappearance wasn't abrupt, the value is
// -1.
public long timeBecameInvisibleNs;
public ViewNode viewCaptureNode;
// Class name + resource id
public String nodeIdentity;
// Collection of detector-specific data for this node.
public final Object[] detectorsData = new Object[ANOMALY_DETECTORS.length];
@Override
public String toString() {
return String.format("view window coordinates: (%s, %s, %s, %s)",
left, top, right, bottom);
}
}
/**
* Scans a view capture record and searches for view animation anomalies. Can find anomalies for
* multiple views.
* Returns a map from the view path to the anomaly message for the view. Non-empty map means
* that anomalies were detected.
*/
public static Map<String, String> getAnomalies(ExportedData viewCaptureData) {
final Map<String, String> anomalies = new HashMap<>();
final int scrimClassIndex = viewCaptureData.getClassnameList().indexOf(SCRIM_VIEW_CLASS);
final int windowDataCount = viewCaptureData.getWindowDataCount();
for (int i = 0; i < windowDataCount; ++i) {
analyzeWindowData(
viewCaptureData, viewCaptureData.getWindowData(i), scrimClassIndex, anomalies);
}
return anomalies;
}
private static void analyzeWindowData(ExportedData viewCaptureData, WindowData windowData,
int scrimClassIndex, Map<String, String> anomalies) {
// View hash code => Last seen node with this hash code.
// The view is added when we analyze the first frame where it's visible.
// After that, it gets updated for every frame where it's visible.
// As we go though frames, if a view becomes invisible, it stays in the map.
final Map<Integer, AnalysisNode> lastSeenNodes = new HashMap<>();
int windowWidthPx = -1;
int windowHeightPx = -1;
for (int frameN = 0; frameN < windowData.getFrameDataCount(); ++frameN) {
final FrameData frame = windowData.getFrameData(frameN);
final ViewNode rootNode = frame.getNode();
// If the rotation or window size has changed, reset the analyzer state.
final boolean isFirstFrame = windowWidthPx != rootNode.getWidth()
|| windowHeightPx != rootNode.getHeight();
if (isFirstFrame) {
windowWidthPx = rootNode.getWidth();
windowHeightPx = rootNode.getHeight();
lastSeenNodes.clear();
}
final int windowSizePx = Math.max(rootNode.getWidth(), rootNode.getHeight());
analyzeFrame(frameN, isFirstFrame, frame, viewCaptureData, lastSeenNodes,
scrimClassIndex, anomalies, windowSizePx);
}
}
private static void analyzeFrame(int frameN, boolean isFirstFrame, FrameData frame,
ExportedData viewCaptureData,
Map<Integer, AnalysisNode> lastSeenNodes, int scrimClassIndex,
Map<String, String> anomalies, int windowSizePx) {
// Analyze the node tree starting from the root.
long frameTimeNs = frame.getTimestamp();
analyzeView(
frameTimeNs,
frame.getNode(),
/* parent = */ null,
frameN,
isFirstFrame,
/* leftShift = */ 0,
/* topShift = */ 0,
viewCaptureData,
lastSeenNodes,
scrimClassIndex,
anomalies,
windowSizePx);
// Analyze transitions when a view visible in the previous frame became invisible in the
// current one.
for (AnalysisNode info : lastSeenNodes.values()) {
if (info.frameN == frameN - 1) {
if (!info.viewCaptureNode.getWillNotDraw()) {
Arrays.stream(ANOMALY_DETECTORS).forEach(
detector ->
detectAnomaly(
detector,
frameN,
/* oldInfo = */ info,
/* newInfo = */ null,
anomalies,
frameTimeNs,
windowSizePx)
);
}
info.timeBecameInvisibleNs = info.alpha == 1 ? frameTimeNs : -1;
info.timeBecameVisibleNs = -1;
}
}
}
private static void analyzeView(long frameTimeNs, ViewNode viewCaptureNode, AnalysisNode parent,
int frameN,
boolean isFirstFrame, float leftShift, float topShift, ExportedData viewCaptureData,
Map<Integer, AnalysisNode> lastSeenNodes, int scrimClassIndex,
Map<String, String> anomalies, int windowSizePx) {
// Skip analysis of invisible views
final float parentAlpha = parent != null ? parent.alpha : 1;
final float alpha = getVisibleAlpha(viewCaptureNode, parentAlpha);
if (alpha <= 0.0) return;
// Calculate analysis node parameters
final int hashcode = viewCaptureNode.getHashcode();
final int classIndex = viewCaptureNode.getClassnameIndex();
final float parentScaleX = parent != null ? parent.scaleX : 1;
final float parentScaleY = parent != null ? parent.scaleY : 1;
final float scaleX = parentScaleX * viewCaptureNode.getScaleX();
final float scaleY = parentScaleY * viewCaptureNode.getScaleY();
final float left = leftShift
+ (viewCaptureNode.getLeft() + viewCaptureNode.getTranslationX()) * parentScaleX
+ viewCaptureNode.getWidth() * (parentScaleX - scaleX) / 2;
final float top = topShift
+ (viewCaptureNode.getTop() + viewCaptureNode.getTranslationY()) * parentScaleY
+ viewCaptureNode.getHeight() * (parentScaleY - scaleY) / 2;
final float width = viewCaptureNode.getWidth() * scaleX;
final float height = viewCaptureNode.getHeight() * scaleY;
// Initialize new analysis node
final AnalysisNode newAnalysisNode = new AnalysisNode();
newAnalysisNode.className = viewCaptureData.getClassname(classIndex);
newAnalysisNode.resourceId = viewCaptureNode.getId();
newAnalysisNode.nodeIdentity =
getNodeIdentity(newAnalysisNode.className, newAnalysisNode.resourceId);
newAnalysisNode.parent = parent;
newAnalysisNode.left = left;
newAnalysisNode.top = top;
newAnalysisNode.right = left + width;
newAnalysisNode.bottom = top + height;
newAnalysisNode.scaleX = scaleX;
newAnalysisNode.scaleY = scaleY;
newAnalysisNode.alpha = alpha;
newAnalysisNode.frameN = frameN;
newAnalysisNode.timeBecameInvisibleNs = -1;
newAnalysisNode.viewCaptureNode = viewCaptureNode;
Arrays.stream(ANOMALY_DETECTORS).forEach(
detector -> detector.initializeNode(newAnalysisNode));
final AnalysisNode oldAnalysisNode = lastSeenNodes.get(hashcode); // may be null
if (oldAnalysisNode != null && oldAnalysisNode.frameN + 1 == frameN) {
// If this view was present in the previous frame, keep the time when it became visible.
newAnalysisNode.timeBecameVisibleNs = oldAnalysisNode.timeBecameVisibleNs;
} else {
// If the view is becoming visible after being invisible, initialize the time when it
// became visible with a new value.
// If the view became visible abruptly, i.e. alpha jumped from 0 to 1 between the
// previous and the current frames, then initialize with the time of the current
// frame. Otherwise, use -1.
newAnalysisNode.timeBecameVisibleNs = newAnalysisNode.alpha >= 1 ? frameTimeNs : -1;
}
// Detect anomalies for the view.
if (!isFirstFrame && !viewCaptureNode.getWillNotDraw()) {
Arrays.stream(ANOMALY_DETECTORS).forEach(
detector ->
detectAnomaly(detector, frameN, oldAnalysisNode, newAnalysisNode,
anomalies, frameTimeNs, windowSizePx)
);
}
lastSeenNodes.put(hashcode, newAnalysisNode);
// Enumerate children starting from the topmost one. Stop at ScrimView, if present.
final float leftShiftForChildren = left - viewCaptureNode.getScrollX();
final float topShiftForChildren = top - viewCaptureNode.getScrollY();
for (int i = viewCaptureNode.getChildrenCount() - 1; i >= 0; --i) {
final ViewNode child = viewCaptureNode.getChildren(i);
// Don't analyze anything under scrim view because we don't know whether it's
// transparent.
if (child.getClassnameIndex() == scrimClassIndex) break;
analyzeView(frameTimeNs, child, newAnalysisNode, frameN, isFirstFrame,
leftShiftForChildren,
topShiftForChildren,
viewCaptureData, lastSeenNodes, scrimClassIndex, anomalies, windowSizePx);
}
}
private static void detectAnomaly(AnomalyDetector detector, int frameN,
AnalysisNode oldAnalysisNode, AnalysisNode newAnalysisNode,
Map<String, String> anomalies, long frameTimeNs, int windowSizePx) {
final String maybeAnomaly =
detector.detectAnomalies(oldAnalysisNode, newAnalysisNode, frameN, frameTimeNs,
windowSizePx);
if (maybeAnomaly != null) {
AnalysisNode latestInfo = newAnalysisNode != null ? newAnalysisNode : oldAnalysisNode;
final String viewDiagPath = diagPathFromRoot(latestInfo);
if (!anomalies.containsKey(viewDiagPath)) {
anomalies.put(viewDiagPath, String.format("%s, %s", maybeAnomaly, latestInfo));
}
}
}
private static float getVisibleAlpha(ViewNode node, float parenVisibleAlpha) {
return node.getVisibility() == VISIBLE
? parenVisibleAlpha * Math.max(0, Math.min(node.getAlpha(), 1))
: 0f;
}
private static String classNameToSimpleName(String className) {
return className.substring(className.lastIndexOf(".") + 1);
}
private static String diagPathFromRoot(AnalysisNode analysisNode) {
final StringBuilder path = new StringBuilder(analysisNode.nodeIdentity);
for (AnalysisNode ancestor = analysisNode.parent;
ancestor != null;
ancestor = ancestor.parent) {
path.insert(0, ancestor.nodeIdentity + "|");
}
return path.toString();
}
private static String getNodeIdentity(String className, String resourceId) {
final StringBuilder sb = new StringBuilder();
sb.append(classNameToSimpleName(className));
if (!"NO_ID".equals(resourceId)) sb.append(":" + resourceId);
return sb.toString();
}
}