Moving ReorderWidgetsTest to an integration test instead of E2E
Flag: TEST_ONLY
Test: IntegrationReorderWidgetsTest
Bug: 324261526
Fix: 343761044
Change-Id: I9bfe8b587e1024260ddeeb48157c3d204f2d58f2
diff --git a/src/com/android/launcher3/AppWidgetResizeFrame.java b/src/com/android/launcher3/AppWidgetResizeFrame.java
index b51e850..ef56246 100644
--- a/src/com/android/launcher3/AppWidgetResizeFrame.java
+++ b/src/com/android/launcher3/AppWidgetResizeFrame.java
@@ -37,6 +37,8 @@
import com.android.launcher3.celllayout.CellLayoutLayoutParams;
import com.android.launcher3.celllayout.CellPosMapper.CellPos;
import com.android.launcher3.config.FeatureFlags;
+import com.android.launcher3.debug.TestEvent;
+import com.android.launcher3.debug.TestEventEmitter;
import com.android.launcher3.dragndrop.DragLayer;
import com.android.launcher3.keyboard.ViewGroupFocusHelper;
import com.android.launcher3.logging.InstanceId;
@@ -221,6 +223,9 @@
dl.addView(frame);
frame.mIsOpen = true;
frame.post(() -> frame.snapToWidget(false));
+ TestEventEmitter.INSTANCE.get(widget.getContext()).sendEvent(
+ TestEvent.RESIZE_FRAME_SHOWING
+ );
}
private void setCornerRadiusFromWidget() {
diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java
index d905801..6a2f536 100644
--- a/src/com/android/launcher3/Launcher.java
+++ b/src/com/android/launcher3/Launcher.java
@@ -166,7 +166,6 @@
import androidx.core.os.BuildCompat;
import androidx.window.embedding.RuleController;
-import com.android.launcher3.DeviceProfile;
import com.android.launcher3.DropTarget.DragObject;
import com.android.launcher3.accessibility.LauncherAccessibilityDelegate;
import com.android.launcher3.allapps.ActivityAllAppsContainerView;
@@ -181,6 +180,8 @@
import com.android.launcher3.celllayout.CellPosMapper.TwoPanelCellPosMapper;
import com.android.launcher3.compat.AccessibilityManagerCompat;
import com.android.launcher3.config.FeatureFlags;
+import com.android.launcher3.debug.TestEvent;
+import com.android.launcher3.debug.TestEventEmitter;
import com.android.launcher3.dot.DotInfo;
import com.android.launcher3.dragndrop.DragController;
import com.android.launcher3.dragndrop.DragLayer;
@@ -595,6 +596,7 @@
RuleController.getInstance(this).setRules(
RuleController.parseRules(this, R.xml.split_configuration));
}
+ TestEventEmitter.INSTANCE.get(this).sendEvent(TestEvent.LAUNCHER_ON_CREATE);
}
protected ModelCallbacks createModelCallbacks() {
diff --git a/src/com/android/launcher3/ModelCallbacks.kt b/src/com/android/launcher3/ModelCallbacks.kt
index 13062b6..83c34ce 100644
--- a/src/com/android/launcher3/ModelCallbacks.kt
+++ b/src/com/android/launcher3/ModelCallbacks.kt
@@ -11,6 +11,8 @@
import com.android.launcher3.WorkspaceLayoutManager.FIRST_SCREEN_ID
import com.android.launcher3.allapps.AllAppsStore
import com.android.launcher3.config.FeatureFlags
+import com.android.launcher3.debug.TestEvent
+import com.android.launcher3.debug.TestEventEmitter
import com.android.launcher3.model.BgDataModel
import com.android.launcher3.model.StringCache
import com.android.launcher3.model.data.AppInfo
@@ -156,6 +158,7 @@
/*pause=*/ false,
deviceProfile.isTwoPanels
)
+ TestEventEmitter.INSTANCE.get(launcher).sendEvent(TestEvent.WORKSPACE_FINISH_LOADING)
}
/**
diff --git a/src/com/android/launcher3/Workspace.java b/src/com/android/launcher3/Workspace.java
index e601a3e..2995e8a 100644
--- a/src/com/android/launcher3/Workspace.java
+++ b/src/com/android/launcher3/Workspace.java
@@ -80,6 +80,8 @@
import com.android.launcher3.celllayout.CellPosMapper;
import com.android.launcher3.celllayout.CellPosMapper.CellPos;
import com.android.launcher3.config.FeatureFlags;
+import com.android.launcher3.debug.TestEvent;
+import com.android.launcher3.debug.TestEventEmitter;
import com.android.launcher3.dot.FolderDotInfo;
import com.android.launcher3.dragndrop.DragController;
import com.android.launcher3.dragndrop.DragLayer;
@@ -314,7 +316,6 @@
*/
public Workspace(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
-
mLauncher = Launcher.getLauncher(context);
mStateTransitionAnimation = new WorkspaceStateTransitionAnimation(mLauncher, this);
mWallpaperManager = WallpaperManager.getInstance(context);
@@ -2218,6 +2219,7 @@
if (d.stateAnnouncer != null && !droppedOnOriginalCell) {
d.stateAnnouncer.completeAction(R.string.item_moved);
}
+ TestEventEmitter.INSTANCE.get(getContext()).sendEvent(TestEvent.WORKSPACE_ON_DROP);
}
@Nullable
diff --git a/src/com/android/launcher3/debug/TestEventsEmitterProduction.kt b/src/com/android/launcher3/debug/TestEventsEmitterProduction.kt
new file mode 100644
index 0000000..650df5a
--- /dev/null
+++ b/src/com/android/launcher3/debug/TestEventsEmitterProduction.kt
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2024 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.debug
+
+import android.content.Context
+import com.android.launcher3.util.MainThreadInitializedObject
+import com.android.launcher3.util.SafeCloseable
+
+/** Events fired by the launcher. */
+enum class TestEvent(val event: String) {
+ LAUNCHER_ON_CREATE("LAUNCHER_ON_CREATE"),
+ WORKSPACE_ON_DROP("WORKSPACE_ON_DROP"),
+ RESIZE_FRAME_SHOWING("RESIZE_FRAME_SHOWING"),
+ WORKSPACE_FINISH_LOADING("WORKSPACE_FINISH_LOADING"),
+}
+
+/** Interface to create TestEventEmitters. */
+interface TestEventEmitter : SafeCloseable {
+
+ companion object {
+ @JvmField
+ val INSTANCE =
+ MainThreadInitializedObject<TestEventEmitter> { _: Context? ->
+ TestEventsEmitterProduction()
+ }
+ }
+
+ fun sendEvent(event: TestEvent)
+}
+
+/**
+ * TestEventsEmitterProduction shouldn't do anything since it runs on the launcher code and not on
+ * tests. This is just a placeholder and test should override this class.
+ */
+class TestEventsEmitterProduction : TestEventEmitter {
+
+ override fun close() {}
+
+ override fun sendEvent(event: TestEvent) {}
+}
diff --git a/src/com/android/launcher3/dragndrop/DragController.java b/src/com/android/launcher3/dragndrop/DragController.java
index bc5a164..c50c008 100644
--- a/src/com/android/launcher3/dragndrop/DragController.java
+++ b/src/com/android/launcher3/dragndrop/DragController.java
@@ -27,6 +27,7 @@
import android.view.View;
import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
import com.android.app.animation.Interpolators;
import com.android.launcher3.DragSource;
@@ -69,8 +70,9 @@
*/
protected DragDriver mDragDriver = null;
+ @VisibleForTesting
/** Options controlling the drag behavior. */
- protected DragOptions mOptions;
+ public DragOptions mOptions;
/** Coordinate for motion down event */
protected final Point mMotionDown = new Point();
@@ -79,7 +81,8 @@
protected final Point mTmpPoint = new Point();
- protected DropTarget.DragObject mDragObject;
+ @VisibleForTesting
+ public DropTarget.DragObject mDragObject;
/** Who can receive drop events */
private final ArrayList<DropTarget> mDropTargets = new ArrayList<>();
diff --git a/tests/assets/ReorderWidgets/full_reorder_case b/tests/assets/ReorderWidgets/full_reorder_case
index 850e4fd..2890b79 100644
--- a/tests/assets/ReorderWidgets/full_reorder_case
+++ b/tests/assets/ReorderWidgets/full_reorder_case
@@ -17,12 +17,12 @@
# Test 4x4
board: 4x4
xxxx
-bbmm
+bbaa
iimm
-iiaa
+iimm
arguments: 0 2
board: 4x4
xxxx
-bbii
+bbaa
mmii
-mmaa
\ No newline at end of file
+mmii
\ No newline at end of file
diff --git a/tests/assets/ReorderWidgets/push_reorder_case b/tests/assets/ReorderWidgets/push_reorder_case
index 8e845a2..1eacfae 100644
--- a/tests/assets/ReorderWidgets/push_reorder_case
+++ b/tests/assets/ReorderWidgets/push_reorder_case
@@ -17,28 +17,28 @@
#Test 5x5
board: 5x5
xxxxx
-bbbm-
+bbb--
--ccc
--ddd
------
-arguments: 2 1
+----m
+arguments: 2 2
board: 5x5
xxxxx
---m--
bbb--
+--m--
--ccc
--ddd
#6x5 Test
board: 6x5
xxxxxx
-bbbbm-
+bbbb--
--aaa-
--ddd-
-------
-arguments: 2 1
+-----m
+arguments: 2 2
board: 6x5
xxxxxx
---m---
bbbb--
+--m---
--aaa-
--ddd-
\ No newline at end of file
diff --git a/tests/assets/ReorderWidgets/simple_reorder_case b/tests/assets/ReorderWidgets/simple_reorder_case
index 2c50ce4..991ccb5 100644
--- a/tests/assets/ReorderWidgets/simple_reorder_case
+++ b/tests/assets/ReorderWidgets/simple_reorder_case
@@ -21,7 +21,7 @@
--mm-
-----
-----
-arguments: 0 4
+arguments: 0 3
board: 5x5
xxxxx
-----
diff --git a/tests/multivalentTests/src/com/android/launcher3/celllayout/CellLayoutTestCaseReader.java b/tests/multivalentTests/src/com/android/launcher3/celllayout/CellLayoutTestCaseReader.java
index 419cb3d..f1403e5 100644
--- a/tests/multivalentTests/src/com/android/launcher3/celllayout/CellLayoutTestCaseReader.java
+++ b/tests/multivalentTests/src/com/android/launcher3/celllayout/CellLayoutTestCaseReader.java
@@ -54,7 +54,7 @@
}
public static class Arguments extends TestSection {
- String[] arguments;
+ public String[] arguments;
public Arguments(String[] arguments) {
super(State.ARGUMENTS);
diff --git a/tests/src/com/android/launcher3/celllayout/TaplReorderWidgetsTest.java b/tests/src/com/android/launcher3/celllayout/TaplReorderWidgetsTest.java
deleted file mode 100644
index 28a1325..0000000
--- a/tests/src/com/android/launcher3/celllayout/TaplReorderWidgetsTest.java
+++ /dev/null
@@ -1,312 +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 android.platform.uiautomator_helpers.DeviceHelpers.getContext;
-
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertTrue;
-
-import android.content.ContentResolver;
-import android.content.ContentValues;
-import android.graphics.Point;
-import android.net.Uri;
-import android.util.Log;
-
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SmallTest;
-
-import com.android.launcher3.InvariantDeviceProfile;
-import com.android.launcher3.Launcher;
-import com.android.launcher3.LauncherAppState;
-import com.android.launcher3.MultipageCellLayout;
-import com.android.launcher3.celllayout.board.CellLayoutBoard;
-import com.android.launcher3.celllayout.board.TestWorkspaceBuilder;
-import com.android.launcher3.celllayout.board.WidgetRect;
-import com.android.launcher3.tapl.Widget;
-import com.android.launcher3.tapl.WidgetResizeFrame;
-import com.android.launcher3.ui.AbstractLauncherUiTest;
-import com.android.launcher3.util.ModelTestExtensions;
-import com.android.launcher3.util.rule.ShellCommandRule;
-
-import org.junit.After;
-import org.junit.Assert;
-import org.junit.Assume;
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.Iterator;
-import java.util.List;
-import java.util.Map;
-
-@SmallTest
-@RunWith(AndroidJUnit4.class)
-public class TaplReorderWidgetsTest extends AbstractLauncherUiTest<Launcher> {
-
- @Rule
- public ShellCommandRule mGrantWidgetRule = ShellCommandRule.grantWidgetBind();
-
- private static final String TAG = TaplReorderWidgetsTest.class.getSimpleName();
-
- private static final List<String> FOLDABLE_GRIDS = List.of("normal", "practical", "reasonable");
-
- TestWorkspaceBuilder mWorkspaceBuilder;
-
- @Before
- public void setup() throws Throwable {
- mWorkspaceBuilder = new TestWorkspaceBuilder(mTargetContext);
- super.setUp();
- }
-
- @After
- public void tearDown() {
- ModelTestExtensions.INSTANCE.clearModelDb(
- LauncherAppState.getInstance(getContext()).getModel()
- );
- }
-
- /**
- * Validate if the given board represent the current CellLayout
- **/
- private boolean validateBoard(List<CellLayoutBoard> testBoards) {
- ArrayList<CellLayoutBoard> workspaceBoards = workspaceToBoards();
- if (workspaceBoards.size() < testBoards.size()) {
- return false;
- }
- for (int i = 0; i < testBoards.size(); i++) {
- if (testBoards.get(i).compareTo(workspaceBoards.get(i)) != 0) {
- return false;
- }
- }
- return true;
- }
-
- private FavoriteItemsTransaction buildWorkspaceFromBoards(List<CellLayoutBoard> boards,
- FavoriteItemsTransaction transaction) {
- for (int i = 0; i < boards.size(); i++) {
- CellLayoutBoard board = boards.get(i);
- mWorkspaceBuilder.buildFromBoard(board, transaction, i);
- }
- return transaction;
- }
-
- private void printCurrentWorkspace() {
- InvariantDeviceProfile idp = InvariantDeviceProfile.INSTANCE.get(mTargetContext);
- ArrayList<CellLayoutBoard> boards = workspaceToBoards();
- for (int i = 0; i < boards.size(); i++) {
- Log.d(TAG, "Screen number " + i);
- Log.d(TAG, ".\n" + boards.get(i).toString(idp.numColumns, idp.numRows));
- }
- }
-
- private ArrayList<CellLayoutBoard> workspaceToBoards() {
- return getFromLauncher(CellLayoutTestUtils::workspaceToBoards);
- }
-
- private WidgetRect getWidgetClosestTo(Point point) {
- ArrayList<CellLayoutBoard> workspaceBoards = workspaceToBoards();
- int maxDistance = 9999;
- WidgetRect bestRect = null;
- for (int i = 0; i < workspaceBoards.get(0).getWidgets().size(); i++) {
- WidgetRect widget = workspaceBoards.get(0).getWidgets().get(i);
- if (widget.getCellX() == 0 && widget.getCellY() == 0) {
- continue;
- }
- int distance = Math.abs(point.x - widget.getCellX())
- + Math.abs(point.y - widget.getCellY());
- if (distance == 0) {
- break;
- }
- if (distance < maxDistance) {
- maxDistance = distance;
- bestRect = widget;
- }
- }
- return bestRect;
- }
-
- /**
- * This function might be odd, its function is to select a widget and leave it in its place.
- * The idea is to make the test broader and also test after a widgets resized because the
- * underlying code does different things in that case
- */
- private void triggerWidgetResize(ReorderTestCase testCase) {
- WidgetRect widgetRect = getWidgetClosestTo(testCase.moveMainTo);
- if (widgetRect == null) {
- // Some test doesn't have a widget in the final position, in those cases we will ignore
- // them
- return;
- }
- Widget widget = mLauncher.getWorkspace().getWidgetAtCell(widgetRect.getCellX(),
- widgetRect.getCellY());
- WidgetResizeFrame resizeFrame = widget.dragWidgetToWorkspace(widgetRect.getCellX(),
- widgetRect.getCellY(), widgetRect.getSpanX(), widgetRect.getSpanY());
- resizeFrame.dismiss();
- }
-
- private void runTestCase(ReorderTestCase testCase) {
- WidgetRect mainWidgetCellPos = CellLayoutBoard.getMainFromList(
- testCase.mStart);
-
- FavoriteItemsTransaction transaction =
- new FavoriteItemsTransaction(mTargetContext);
- transaction = buildWorkspaceFromBoards(testCase.mStart, transaction);
- transaction.commit();
- mLauncher.waitForLauncherInitialized();
- // resetLoaderState triggers the launcher to start loading the workspace which allows
- // waitForLauncherCondition to wait for that condition, otherwise the condition would
- // always be true and it wouldn't wait for the changes to be applied.
- waitForLauncherCondition("Workspace didn't finish loading", l -> !l.isWorkspaceLoading());
-
- triggerWidgetResize(testCase);
-
- Widget widget = mLauncher.getWorkspace().getWidgetAtCell(mainWidgetCellPos.getCellX(),
- mainWidgetCellPos.getCellY());
- assertNotNull(widget);
- WidgetResizeFrame resizeFrame = widget.dragWidgetToWorkspace(testCase.moveMainTo.x,
- testCase.moveMainTo.y, mainWidgetCellPos.getSpanX(), mainWidgetCellPos.getSpanY());
- resizeFrame.dismiss();
-
- boolean isValid = false;
- for (List<CellLayoutBoard> boards : testCase.mEnd) {
- isValid |= validateBoard(boards);
- if (isValid) break;
- }
- printCurrentWorkspace();
- assertTrue("Non of the valid boards match with the current state", isValid);
- }
-
- /**
- * Run only the test define for the current grid size if such test exist
- *
- * @param testCaseMap map containing all the tests per grid size (Point)
- */
- private boolean runTestCaseMap(Map<Point, ReorderTestCase> testCaseMap, String testName) {
- Point iconGridDimensions = mLauncher.getWorkspace().getIconGridDimensions();
- Log.d(TAG, "Running test " + testName + " for grid " + iconGridDimensions);
- if (!testCaseMap.containsKey(iconGridDimensions)) {
- Log.d(TAG, "The test " + testName + " doesn't support " + iconGridDimensions
- + " grid layout");
- return false;
- }
- runTestCase(testCaseMap.get(iconGridDimensions));
-
- return true;
- }
-
- private void runTestCaseMapForAllGrids(Map<Point, ReorderTestCase> testCaseMap,
- String testName) {
- boolean runAtLeastOnce = false;
- for (String grid : FOLDABLE_GRIDS) {
- applyGridOption(grid);
- mLauncher.waitForLauncherInitialized();
- runAtLeastOnce |= runTestCaseMap(testCaseMap, testName);
- }
- Assume.assumeTrue("None of the grids are supported", runAtLeastOnce);
- }
-
- private void applyGridOption(Object argValue) {
- String testProviderAuthority = mTargetContext.getPackageName() + ".grid_control";
- Uri gridUri = new Uri.Builder()
- .scheme(ContentResolver.SCHEME_CONTENT)
- .authority(testProviderAuthority)
- .appendPath("default_grid")
- .build();
- ContentValues values = new ContentValues();
- values.putObject("name", argValue);
- Assert.assertEquals(1,
- mTargetContext.getContentResolver().update(gridUri, values, null, null));
- }
-
- @Test
- public void simpleReorder() throws Exception {
- runTestCaseMap(getTestMap("ReorderWidgets/simple_reorder_case"),
- "push_reorder_case");
- }
-
- @Test
- public void pushTest() throws Exception {
- runTestCaseMap(getTestMap("ReorderWidgets/push_reorder_case"),
- "push_reorder_case");
- }
-
- @Test
- public void fullReorder() throws Exception {
- runTestCaseMap(getTestMap("ReorderWidgets/full_reorder_case"),
- "full_reorder_case");
- }
-
- @Test
- public void moveOutReorder() throws Exception {
- runTestCaseMap(getTestMap("ReorderWidgets/move_out_reorder_case"),
- "move_out_reorder_case");
- }
-
- @Test
- public void multipleCellLayoutsSimpleReorder() throws Exception {
- Assume.assumeTrue("Test doesn't support foldables", getFromLauncher(
- l -> l.getWorkspace().getScreenWithId(0) instanceof MultipageCellLayout));
- runTestCaseMapForAllGrids(getTestMap("ReorderWidgets/multiple_cell_layouts_simple_reorder"),
- "multiple_cell_layouts_simple_reorder");
- }
-
- @Test
- public void multipleCellLayoutsNoSpaceReorder() throws Exception {
- Assume.assumeTrue("Test doesn't support foldables", getFromLauncher(
- l -> l.getWorkspace().getScreenWithId(0) instanceof MultipageCellLayout));
- runTestCaseMapForAllGrids(
- getTestMap("ReorderWidgets/multiple_cell_layouts_no_space_reorder"),
- "multiple_cell_layouts_no_space_reorder");
- }
-
- @Test
- public void multipleCellLayoutsReorderToOtherSide() throws Exception {
- Assume.assumeTrue("Test doesn't support foldables", getFromLauncher(
- l -> l.getWorkspace().getScreenWithId(0) instanceof MultipageCellLayout));
- runTestCaseMapForAllGrids(
- getTestMap("ReorderWidgets/multiple_cell_layouts_reorder_other_side"),
- "multiple_cell_layouts_reorder_other_side");
- }
-
- private void addTestCase(Iterator<CellLayoutTestCaseReader.TestSection> sections,
- Map<Point, ReorderTestCase> testCaseMap) {
- CellLayoutTestCaseReader.Board startBoard =
- ((CellLayoutTestCaseReader.Board) sections.next());
- CellLayoutTestCaseReader.Arguments point =
- ((CellLayoutTestCaseReader.Arguments) sections.next());
- CellLayoutTestCaseReader.Board endBoard =
- ((CellLayoutTestCaseReader.Board) sections.next());
- Point moveTo = new Point(Integer.parseInt(point.arguments[0]),
- Integer.parseInt(point.arguments[1]));
- testCaseMap.put(endBoard.gridSize,
- new ReorderTestCase(startBoard.board, moveTo, endBoard.board));
- }
-
- private Map<Point, ReorderTestCase> getTestMap(String testPath) throws IOException {
- Map<Point, ReorderTestCase> testCaseMap = new HashMap<>();
- Iterator<CellLayoutTestCaseReader.TestSection> iterableSection =
- CellLayoutTestCaseReader.readFromFile(testPath).parse().iterator();
- while (iterableSection.hasNext()) {
- addTestCase(iterableSection, testCaseMap);
- }
- return testCaseMap;
- }
-}
diff --git a/tests/src/com/android/launcher3/celllayout/integrationtest/TestUtils.kt b/tests/src/com/android/launcher3/celllayout/integrationtest/TestUtils.kt
new file mode 100644
index 0000000..4cecb5a
--- /dev/null
+++ b/tests/src/com/android/launcher3/celllayout/integrationtest/TestUtils.kt
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2024 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.integrationtest
+
+import android.graphics.Point
+import android.graphics.Rect
+import android.view.View
+import android.view.ViewGroup
+import com.android.launcher3.CellLayout
+import com.android.launcher3.Workspace
+import com.android.launcher3.util.CellAndSpan
+import com.android.launcher3.widget.LauncherAppWidgetHostView
+
+object TestUtils {
+ fun <T> searchChildren(viewGroup: ViewGroup, type: Class<T>): T? where T : View {
+ for (i in 0..<viewGroup.childCount) {
+ val child = viewGroup.getChildAt(i)
+ if (type.isInstance(child)) {
+ return type.cast(child)
+ }
+ if (child is ViewGroup) {
+ val result = searchChildren(child, type)
+ if (result != null) {
+ return result
+ }
+ }
+ }
+ return null
+ }
+
+ fun getWidgetAtCell(
+ workspace: Workspace<*>,
+ cellX: Int,
+ cellY: Int
+ ): LauncherAppWidgetHostView {
+ val view =
+ (workspace.getPageAt(workspace.currentPage) as CellLayout).getChildAt(cellX, cellY)
+ assert(view != null) { "There is no view at $cellX , $cellY" }
+ assert(view is LauncherAppWidgetHostView) { "The view at $cellX , $cellY is not a widget" }
+ return view as LauncherAppWidgetHostView
+ }
+
+ fun getCellTopLeftRelativeToCellLayout(
+ workspace: Workspace<*>,
+ cellAndSpan: CellAndSpan
+ ): Point {
+ val target = Rect()
+ val cellLayout = workspace.getPageAt(workspace.currentPage) as CellLayout
+ cellLayout.cellToRect(
+ cellAndSpan.cellX,
+ cellAndSpan.cellY,
+ cellAndSpan.spanX,
+ cellAndSpan.spanY,
+ target
+ )
+ return Point(target.left, target.top)
+ }
+}
diff --git a/tests/src/com/android/launcher3/celllayout/integrationtest/events/EventsRule.kt b/tests/src/com/android/launcher3/celllayout/integrationtest/events/EventsRule.kt
new file mode 100644
index 0000000..fb61ced
--- /dev/null
+++ b/tests/src/com/android/launcher3/celllayout/integrationtest/events/EventsRule.kt
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2024 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.integrationtest.events
+
+import android.content.Context
+import com.android.launcher3.debug.TestEvent
+import com.android.launcher3.debug.TestEventEmitter
+import org.junit.rules.TestRule
+import org.junit.runner.Description
+import org.junit.runners.model.Statement
+
+/**
+ * Rule to create EventWaiters to wait for events that happens on the Launcher. For reference look
+ * at [TestEvent] for existing events.
+ *
+ * Waiting for event should be used to prevent race conditions, it provides a more precise way of
+ * waiting for events compared to [AbstractLauncherUiTest#waitForLauncherCondition].
+ *
+ * This class overrides the [TestEventEmitter] with [TestEventsEmitterImplementation] and makes sure
+ * to return the [TestEventEmitter] to the previous value when finished.
+ */
+class EventsRule(val context: Context) : TestRule {
+
+ private var prevEventEmitter: TestEventEmitter? = null
+
+ private val eventEmitter = TestEventsEmitterImplementation()
+
+ override fun apply(base: Statement, description: Description?): Statement {
+ return object : Statement() {
+ override fun evaluate() {
+ beforeTest()
+ base.evaluate()
+ afterTest()
+ }
+ }
+ }
+
+ fun createEventWaiter(expectedEvent: TestEvent): EventWaiter {
+ return eventEmitter.createEventWaiter(expectedEvent)
+ }
+
+ private fun beforeTest() {
+ prevEventEmitter = TestEventEmitter.INSTANCE.get(context)
+ TestEventEmitter.INSTANCE.initializeForTesting(eventEmitter)
+ }
+
+ private fun afterTest() {
+ TestEventEmitter.INSTANCE.initializeForTesting(prevEventEmitter)
+ }
+}
diff --git a/tests/src/com/android/launcher3/celllayout/integrationtest/events/TestEventsEmitterImplementation.kt b/tests/src/com/android/launcher3/celllayout/integrationtest/events/TestEventsEmitterImplementation.kt
new file mode 100644
index 0000000..365ad4b
--- /dev/null
+++ b/tests/src/com/android/launcher3/celllayout/integrationtest/events/TestEventsEmitterImplementation.kt
@@ -0,0 +1,89 @@
+/*
+ * Copyright (C) 2024 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.integrationtest.events
+
+import android.util.Log
+import com.android.launcher3.debug.TestEvent
+import com.android.launcher3.debug.TestEventEmitter
+import java.util.concurrent.TimeUnit
+import kotlinx.coroutines.CompletableDeferred
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.withTimeoutOrNull
+
+enum class EventStatus() {
+ SUCCESS,
+ FAILURE,
+ TIMEOUT,
+}
+
+class EventWaiter(val eventToWait: TestEvent) {
+ private val deferrable = CompletableDeferred<EventStatus>()
+
+ companion object {
+ private const val TAG = "EventWaiter"
+ }
+
+ fun waitForSignal(timeout: Long = TimeUnit.SECONDS.toMillis(10)) = runBlocking {
+ var status = withTimeoutOrNull(timeout) { deferrable.await() }
+ if (status == null) {
+ status = EventStatus.TIMEOUT
+ }
+ if (status != EventStatus.SUCCESS) {
+ throw Exception("Failure waiting for event $eventToWait, failure = $status")
+ }
+ }
+
+ fun terminate() {
+ deferrable.complete(EventStatus.SUCCESS)
+ }
+}
+
+class TestEventsEmitterImplementation() : TestEventEmitter {
+ companion object {
+ private const val TAG = "TestEvents"
+ }
+
+ private val expectedEvents: ArrayDeque<EventWaiter> = ArrayDeque()
+
+ fun createEventWaiter(expectedEvent: TestEvent): EventWaiter {
+ val eventWaiter = EventWaiter(expectedEvent)
+ expectedEvents.add(eventWaiter)
+ return eventWaiter
+ }
+
+ private fun clearQueue() {
+ expectedEvents.clear()
+ }
+
+ override fun sendEvent(event: TestEvent) {
+ Log.d(TAG, "Signal received $event")
+ Log.d(TAG, "Total expected events ${expectedEvents.size}")
+ if (expectedEvents.isEmpty()) return
+ val eventWaiter = expectedEvents.last()
+ if (eventWaiter.eventToWait == event) {
+ Log.d(TAG, "Removing $event")
+ expectedEvents.removeLast()
+ eventWaiter.terminate()
+ } else {
+ Log.d(TAG, "Not matching $event")
+ }
+ }
+
+ override fun close() {
+ clearQueue()
+ }
+}