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:
@@ -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();
|
||||
}
|
||||
}
|
||||
-22
@@ -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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user