From a3c17fc075ede3d83fd70902c1b25073a8de8e40 Mon Sep 17 00:00:00 2001 From: Riddle Hsu Date: Thu, 1 Aug 2024 16:36:39 +0800 Subject: [PATCH 1/4] Use rotation hint to update layout The info from DisplayController is the rotation of display. While the rotation hint is from the current configuration of the activity. Because system supports to display activities with individual rotation at the same time, it is more accurate to use the hint for layout. Because there are 4 DeviceProfile instances for 4 rotations. The instance will be used for corresponding rotation. So the seascape attributes is a final state which no longer needs to be updated. Bug: 356164050 Flag: EXEMPT bugfix Test: Enable auto rotation. Enable home rotation. Launch a fixed portrait activity. Rotate device to 270 degree (seascape). Swipe navigation to return to home. Home should use 270 layout directly instead of 90 and then change to 270 after animation is finished. Change-Id: Iad0f6ada0ec1cb7f0b5281b508d58276332076fb --- .../android/quickstep/AbsSwipeUpHandler.java | 1 - .../interaction/TutorialController.java | 1 - .../launcher3/BaseDraggingActivity.java | 4 ---- src/com/android/launcher3/DeviceProfile.java | 23 +------------------ 4 files changed, 1 insertion(+), 28 deletions(-) diff --git a/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java b/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java index 5a03ae650b..343f88ab2d 100644 --- a/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java +++ b/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java @@ -989,7 +989,6 @@ public abstract class AbsSwipeUpHandler Date: Thu, 1 Aug 2024 16:40:46 -0700 Subject: [PATCH 2/4] Convert testing classest to Kotlin Bug: 324261526 Test: ImageTest Test: FolderTest Test: IntegrationReorderWidgets Flag: TEST_ONLY Change-Id: Ieb4e451cc44656fc98d87163203eaae379ec0512 --- .../celllayout/HotseatReorderUnitTest.kt | 6 +- .../celllayout/board/BoardClasses.kt | 63 ++++++ .../celllayout/board/CellLayoutBoard.java | 32 +-- .../launcher3/celllayout/board/CellType.java | 32 --- .../celllayout/board/FolderPoint.java | 37 ---- .../launcher3/celllayout/board/IconPoint.java | 45 ---- .../board/IdenticalBoardComparator.kt | 6 +- .../board/TestWorkspaceBuilder.java | 192 ------------------ .../celllayout/board/TestWorkspaceBuilder.kt | 191 +++++++++++++++++ .../celllayout/board/WidgetRect.java | 59 ------ 10 files changed, 275 insertions(+), 388 deletions(-) create mode 100644 tests/multivalentTests/src/com/android/launcher3/celllayout/board/BoardClasses.kt delete mode 100644 tests/multivalentTests/src/com/android/launcher3/celllayout/board/CellType.java delete mode 100644 tests/multivalentTests/src/com/android/launcher3/celllayout/board/FolderPoint.java delete mode 100644 tests/multivalentTests/src/com/android/launcher3/celllayout/board/IconPoint.java delete mode 100644 tests/multivalentTests/src/com/android/launcher3/celllayout/board/TestWorkspaceBuilder.java create mode 100644 tests/multivalentTests/src/com/android/launcher3/celllayout/board/TestWorkspaceBuilder.kt delete mode 100644 tests/multivalentTests/src/com/android/launcher3/celllayout/board/WidgetRect.java diff --git a/tests/multivalentTests/src/com/android/launcher3/celllayout/HotseatReorderUnitTest.kt b/tests/multivalentTests/src/com/android/launcher3/celllayout/HotseatReorderUnitTest.kt index c32461ea98..a3c7f4fb49 100644 --- a/tests/multivalentTests/src/com/android/launcher3/celllayout/HotseatReorderUnitTest.kt +++ b/tests/multivalentTests/src/com/android/launcher3/celllayout/HotseatReorderUnitTest.kt @@ -104,7 +104,7 @@ class HotseatReorderUnitTest { val cl = cellLayoutBuilder.createCellLayout(board.width, board.height, false) // The views have to be sorted or the result can vary board.icons - .map(IconPoint::getCoord) + .map(IconPoint::coord) .sortedWith( Comparator.comparing { p: Any -> (p as Point).x } .thenComparing { p: Any -> (p as Point).y } @@ -120,9 +120,7 @@ class HotseatReorderUnitTest { ) } board.widgets - .sortedWith( - Comparator.comparing(WidgetRect::getCellX).thenComparing(WidgetRect::getCellY) - ) + .sortedWith(Comparator.comparing(WidgetRect::cellX).thenComparing(WidgetRect::cellY)) .forEach { widget -> addViewInCellLayout( cl, diff --git a/tests/multivalentTests/src/com/android/launcher3/celllayout/board/BoardClasses.kt b/tests/multivalentTests/src/com/android/launcher3/celllayout/board/BoardClasses.kt new file mode 100644 index 0000000000..3cbfc5a252 --- /dev/null +++ b/tests/multivalentTests/src/com/android/launcher3/celllayout/board/BoardClasses.kt @@ -0,0 +1,63 @@ +/* + * 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 + +/** Represents a widget in a CellLayoutBoard */ +data class WidgetRect( + val type: Char, + val bounds: Rect, +) { + val spanX: Int = bounds.right - bounds.left + 1 + val spanY: Int = bounds.top - bounds.bottom + 1 + val cellY: Int = bounds.bottom + val cellX: Int = bounds.left + + fun shouldIgnore() = type == CellType.IGNORE + + fun contains(x: Int, y: Int) = bounds.contains(x, y) +} + +/** + * [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. + */ +data class FolderPoint(val coord: Point, val type: Char) { + val numberIconsInside: Int = type.code - 'A'.code + 2 +} + +/** Represents an icon in a CellLayoutBoard */ +data class IconPoint(val coord: Point, val type: Char = CellType.ICON) + +object CellType { + // The cells marked by this will be filled by 1x1 widgets and will be ignored when + // validating + const val IGNORE = 'x' + + // The cells marked by this will be filled by app icons + const val ICON = 'i' + + // The cells marked by FOLDER will be filled by folders with 27 app icons inside + const val FOLDER = 'Z' + + // Empty space + const val EMPTY = '-' + + // Widget that will be saved as "main widget" for easier retrieval + const val MAIN_WIDGET = 'm' // Everything else will be consider a widget +} diff --git a/tests/multivalentTests/src/com/android/launcher3/celllayout/board/CellLayoutBoard.java b/tests/multivalentTests/src/com/android/launcher3/celllayout/board/CellLayoutBoard.java index e5ad888a8c..04bfee97be 100644 --- a/tests/multivalentTests/src/com/android/launcher3/celllayout/board/CellLayoutBoard.java +++ b/tests/multivalentTests/src/com/android/launcher3/celllayout/board/CellLayoutBoard.java @@ -88,7 +88,7 @@ public class CellLayoutBoard implements Comparable { public WidgetRect getWidgetOfType(char type) { return mWidgetsRects.stream() - .filter(widgetRect -> widgetRect.mType == type).findFirst().orElse(null); + .filter(widgetRect -> widgetRect.getType() == type).findFirst().orElse(null); } public WidgetRect getWidgetAt(int x, int y) { @@ -117,8 +117,8 @@ public class CellLayoutBoard implements Comparable { } private void removeWidgetFromBoard(WidgetRect widget) { - for (int xi = widget.mBounds.left; xi <= widget.mBounds.right; xi++) { - for (int yi = widget.mBounds.bottom; yi <= widget.mBounds.top; yi++) { + for (int xi = widget.getBounds().left; xi <= widget.getBounds().right; xi++) { + for (int yi = widget.getBounds().bottom; yi <= widget.getBounds().top; yi++) { mWidget[xi][yi] = '-'; } } @@ -127,7 +127,7 @@ public class CellLayoutBoard implements Comparable { private void removeOverlappingItems(Rect rect) { // Remove overlapping widgets and remove them from the board mWidgetsRects = mWidgetsRects.stream().filter(widget -> { - if (rect.intersect(widget.mBounds)) { + if (rect.intersect(widget.getBounds())) { removeWidgetFromBoard(widget); return false; } @@ -135,8 +135,8 @@ public class CellLayoutBoard implements Comparable { }).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; + int x = iconPoint.getCoord().x; + int y = iconPoint.getCoord().y; if (rect.contains(x, y)) { mWidget[x][y] = '-'; return false; @@ -146,8 +146,8 @@ public class CellLayoutBoard implements Comparable { // Remove overlapping folders and remove them from the board mFolderPoints = mFolderPoints.stream().filter(folderPoint -> { - int x = folderPoint.coord.x; - int y = folderPoint.coord.y; + int x = folderPoint.getCoord().x; + int y = folderPoint.getCoord().y; if (rect.contains(x, y)) { mWidget[x][y] = '-'; return false; @@ -159,7 +159,7 @@ public class CellLayoutBoard implements Comparable { private void removeOverlappingItems(Point p) { // Remove overlapping widgets and remove them from the board mWidgetsRects = mWidgetsRects.stream().filter(widget -> { - if (IdenticalBoardComparator.Companion.touchesPoint(widget.mBounds, p)) { + if (IdenticalBoardComparator.Companion.touchesPoint(widget.getBounds(), p)) { removeWidgetFromBoard(widget); return false; } @@ -167,8 +167,8 @@ public class CellLayoutBoard implements Comparable { }).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; + int x = iconPoint.getCoord().x; + int y = iconPoint.getCoord().y; if (p.x == x && p.y == y) { mWidget[x][y] = '-'; return false; @@ -178,8 +178,8 @@ public class CellLayoutBoard implements Comparable { // Remove overlapping folders and remove them from the board mFolderPoints = mFolderPoints.stream().filter(folderPoint -> { - int x = folderPoint.coord.x; - int y = folderPoint.coord.y; + int x = folderPoint.getCoord().x; + int y = folderPoint.getCoord().y; if (p.x == x && p.y == y) { mWidget[x][y] = '-'; return false; @@ -226,7 +226,7 @@ public class CellLayoutBoard implements Comparable { public void removeItem(char type) { mWidgetsRects.stream() - .filter(widgetRect -> widgetRect.mType == type) + .filter(widgetRect -> widgetRect.getType() == type) .forEach(widgetRect -> removeOverlappingItems( new Point(widgetRect.getCellX(), widgetRect.getCellY()))); } @@ -365,10 +365,10 @@ public class CellLayoutBoard implements Comparable { board.mWidth = lines[0].length(); board.mWidgetsRects = getRects(board.mWidget); board.mWidgetsRects.forEach(widgetRect -> { - if (widgetRect.mType == CellType.MAIN_WIDGET) { + if (widgetRect.getType() == CellType.MAIN_WIDGET) { board.mMain = widgetRect; } - board.mWidgetsMap.put(widgetRect.mType, widgetRect); + board.mWidgetsMap.put(widgetRect.getType(), widgetRect); }); board.mIconPoints = getIconPoints(board.mWidget); board.mFolderPoints = getFolderPoints(board.mWidget); diff --git a/tests/multivalentTests/src/com/android/launcher3/celllayout/board/CellType.java b/tests/multivalentTests/src/com/android/launcher3/celllayout/board/CellType.java deleted file mode 100644 index 49c146b32a..0000000000 --- a/tests/multivalentTests/src/com/android/launcher3/celllayout/board/CellType.java +++ /dev/null @@ -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 -} diff --git a/tests/multivalentTests/src/com/android/launcher3/celllayout/board/FolderPoint.java b/tests/multivalentTests/src/com/android/launcher3/celllayout/board/FolderPoint.java deleted file mode 100644 index 39ba434dc0..0000000000 --- a/tests/multivalentTests/src/com/android/launcher3/celllayout/board/FolderPoint.java +++ /dev/null @@ -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; - } -} diff --git a/tests/multivalentTests/src/com/android/launcher3/celllayout/board/IconPoint.java b/tests/multivalentTests/src/com/android/launcher3/celllayout/board/IconPoint.java deleted file mode 100644 index d3d297003d..0000000000 --- a/tests/multivalentTests/src/com/android/launcher3/celllayout/board/IconPoint.java +++ /dev/null @@ -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; - } -} diff --git a/tests/multivalentTests/src/com/android/launcher3/celllayout/board/IdenticalBoardComparator.kt b/tests/multivalentTests/src/com/android/launcher3/celllayout/board/IdenticalBoardComparator.kt index a4a420cf59..aacd940460 100644 --- a/tests/multivalentTests/src/com/android/launcher3/celllayout/board/IdenticalBoardComparator.kt +++ b/tests/multivalentTests/src/com/android/launcher3/celllayout/board/IdenticalBoardComparator.kt @@ -26,11 +26,11 @@ class IdenticalBoardComparator : Comparator { /** Converts a list of WidgetRect into a map of the count of different widget.bounds */ private fun widgetsToBoundsMap(widgets: List) = - widgets.groupingBy { it.mBounds }.eachCount() + widgets.groupingBy { it.bounds }.eachCount() /** Converts a list of IconPoint into a map of the count of different icon.coord */ private fun iconsToPosCountMap(widgets: List) = - widgets.groupingBy { it.getCoord() }.eachCount() + widgets.groupingBy { it.coord }.eachCount() override fun compare( cellLayoutBoard: CellLayoutBoard, @@ -47,7 +47,7 @@ class IdenticalBoardComparator : Comparator { widgetsToBoundsMap( otherCellLayoutBoard.widgets .filter { !it.shouldIgnore() } - .filter { !overlapsWithIgnored(ignoredRectangles, it.mBounds) } + .filter { !overlapsWithIgnored(ignoredRectangles, it.bounds) } ) if (widgetsMap != otherWidgetMap) { diff --git a/tests/multivalentTests/src/com/android/launcher3/celllayout/board/TestWorkspaceBuilder.java b/tests/multivalentTests/src/com/android/launcher3/celllayout/board/TestWorkspaceBuilder.java deleted file mode 100644 index 8a427dd81b..0000000000 --- a/tests/multivalentTests/src/com/android/launcher3/celllayout/board/TestWorkspaceBuilder.java +++ /dev/null @@ -1,192 +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.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.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; - - public TestWorkspaceBuilder(Context context) { - mMyUser = Process.myUserHandle(); - mContext = context; - } - - /** - * 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 createWidgetInCell( - WidgetRect widgetRect, int screenId) { - // Create the widget lazily since the appWidgetId can get lost during setup - return () -> { - LauncherAppWidgetProviderInfo info = findWidgetProvider(false); - LauncherAppWidgetInfo item = createWidgetInfo(info, getApplicationContext(), true); - item.cellX = widgetRect.getCellX(); - item.cellY = widgetRect.getCellY(); - item.spanX = widgetRect.getSpanX(); - item.spanY = widgetRect.getSpanY(); - item.screenId = screenId; - return item; - }; - } - - public FolderInfo createFolderInCell(FolderPoint folderPoint, int screenId) { - FolderInfo folderInfo = new FolderInfo(); - folderInfo.screenId = screenId; - folderInfo.container = LauncherSettings.Favorites.CONTAINER_DESKTOP; - folderInfo.cellX = folderPoint.coord.x; - folderInfo.cellY = folderPoint.coord.y; - folderInfo.minSpanY = folderInfo.minSpanX = folderInfo.spanX = folderInfo.spanY = 1; - folderInfo.setOption(FolderInfo.FLAG_MULTI_PAGE_ANIMATION, true, null); - - for (int i = 0; i < folderPoint.getNumberIconsInside(); i++) { - folderInfo.add(getDefaultWorkspaceItem(screenId), false); - } - - return folderInfo; - } - - private WorkspaceItemInfo getDefaultWorkspaceItem(int screenId) { - WorkspaceItemInfo item = new WorkspaceItemInfo(getApp()); - item.screenId = screenId; - item.minSpanY = item.minSpanX = item.spanX = item.spanY = 1; - item.container = LauncherSettings.Favorites.CONTAINER_DESKTOP; - return item; - } - - private ItemInfo createIconInCell(IconPoint iconPoint, int screenId) { - WorkspaceItemInfo item = new WorkspaceItemInfo(getApp()); - item.screenId = screenId; - item.cellX = iconPoint.getCoord().x; - item.cellY = iconPoint.getCoord().y; - item.minSpanY = item.minSpanX = item.spanX = item.spanY = 1; - item.container = LauncherSettings.Favorites.CONTAINER_DESKTOP; - return item; - } - - private ItemInfo getHotseatValues(int x) { - WorkspaceItemInfo item = new WorkspaceItemInfo(getApp()); - item.cellX = x; - item.cellY = 0; - item.minSpanY = item.minSpanX = item.spanX = item.spanY = 1; - item.rank = x; - item.screenId = x; - item.container = LauncherSettings.Favorites.CONTAINER_HOTSEAT; - return item; - } -} diff --git a/tests/multivalentTests/src/com/android/launcher3/celllayout/board/TestWorkspaceBuilder.kt b/tests/multivalentTests/src/com/android/launcher3/celllayout/board/TestWorkspaceBuilder.kt new file mode 100644 index 0000000000..8952b8542c --- /dev/null +++ b/tests/multivalentTests/src/com/android/launcher3/celllayout/board/TestWorkspaceBuilder.kt @@ -0,0 +1,191 @@ +/* + * 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.content.ComponentName +import android.content.Context +import android.graphics.Rect +import android.os.Process +import android.os.UserHandle +import android.util.Log +import androidx.test.core.app.ApplicationProvider +import androidx.test.platform.app.InstrumentationRegistry +import com.android.launcher3.InvariantDeviceProfile +import com.android.launcher3.LauncherSettings +import com.android.launcher3.celllayout.FavoriteItemsTransaction +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.WorkspaceItemInfo +import com.android.launcher3.ui.TestViewHelpers +import com.android.launcher3.util.WidgetUtils +import java.util.function.Supplier + +class TestWorkspaceBuilder(private val mContext: Context) { + + private var appComponentName = + ComponentName("com.google.android.calculator", "com.android.calculator2.Calculator") + private val myUser: UserHandle = Process.myUserHandle() + + /** Fills the given rect in WidgetRect with 1x1 widgets. This is useful to equalize cases. */ + private fun fillWithWidgets( + widgetRect: WidgetRect, + transaction: FavoriteItemsTransaction, + screenId: Int + ): FavoriteItemsTransaction { + val initX = widgetRect.cellX + val initY = widgetRect.cellY + for (x in initX until initX + widgetRect.spanX) { + for (y in initY until initY + widgetRect.spanY) { + try { + // this widgets are filling, we don't care if we can't place them + transaction.addItem( + createWidgetInCell(WidgetRect(CellType.IGNORE, Rect(x, y, x, y)), screenId) + ) + } catch (e: Exception) { + Log.d(TAG, "Unable to place filling widget at $x,$y") + } + } + } + return transaction + } + + private fun app() = + AppInfo(appComponentName, "test icon", myUser, AppInfo.makeLaunchIntent(appComponentName)) + + /** + * 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 + */ + fun setTestAppActivityAlias(testAppName: String) { + appComponentName = + ComponentName( + InstrumentationRegistry.getInstrumentation().context.packageName, + TEST_ACTIVITY_PACKAGE_PREFIX + testAppName + ) + } + + private fun addCorrespondingWidgetRect( + widgetRect: WidgetRect, + transaction: FavoriteItemsTransaction, + screenId: Int + ) { + if (widgetRect.type == 'x') { + fillWithWidgets(widgetRect, transaction, screenId) + } else { + transaction.addItem(createWidgetInCell(widgetRect, screenId)) + } + } + + /** Builds the given board into the transaction */ + fun buildFromBoard( + board: CellLayoutBoard, + transaction: FavoriteItemsTransaction, + screenId: Int + ): FavoriteItemsTransaction { + board.widgets.forEach { addCorrespondingWidgetRect(it, transaction, screenId) } + board.icons.forEach { transaction.addItem { createIconInCell(it, screenId) } } + board.folders.forEach { transaction.addItem { createFolderInCell(it, 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. + */ + fun fillHotseatIcons(transaction: FavoriteItemsTransaction): FavoriteItemsTransaction { + for (i in 0.. { + // Create the widget lazily since the appWidgetId can get lost during setup + return Supplier { + WidgetUtils.createWidgetInfo( + TestViewHelpers.findWidgetProvider(false), + ApplicationProvider.getApplicationContext(), + true + ) + .apply { + cellX = widgetRect.cellX + cellY = widgetRect.cellY + spanX = widgetRect.spanX + spanY = widgetRect.spanY + screenId = paramScreenId + } + } + } + + fun createFolderInCell(folderPoint: FolderPoint, paramScreenId: Int): FolderInfo = + FolderInfo().apply { + screenId = paramScreenId + container = LauncherSettings.Favorites.CONTAINER_DESKTOP + cellX = folderPoint.coord.x + cellY = folderPoint.coord.y + spanY = 1 + spanX = 1 + minSpanX = 1 + minSpanY = 1 + setOption(FolderInfo.FLAG_MULTI_PAGE_ANIMATION, true, null) + for (i in 0 until folderPoint.numberIconsInside) { + add(getDefaultWorkspaceItem(paramScreenId), false) + } + } + + private fun getDefaultWorkspaceItem(paramScreenId: Int): WorkspaceItemInfo = + WorkspaceItemInfo(app()).apply { + screenId = paramScreenId + spanY = 1 + spanX = 1 + minSpanX = 1 + minSpanY = 1 + container = LauncherSettings.Favorites.CONTAINER_DESKTOP + } + + private fun createIconInCell(iconPoint: IconPoint, paramScreenId: Int) = + WorkspaceItemInfo(app()).apply { + screenId = paramScreenId + cellX = iconPoint.coord.x + cellY = iconPoint.coord.y + spanY = 1 + spanX = 1 + minSpanX = 1 + minSpanY = 1 + container = LauncherSettings.Favorites.CONTAINER_DESKTOP + } + + private fun getHotseatValues(x: Int) = + WorkspaceItemInfo(app()).apply { + cellX = x + cellY = 0 + spanY = 1 + spanX = 1 + minSpanX = 1 + minSpanY = 1 + rank = x + screenId = x + container = LauncherSettings.Favorites.CONTAINER_HOTSEAT + } + + companion object { + private const val TAG = "CellLayoutBoardBuilder" + private const val TEST_ACTIVITY_PACKAGE_PREFIX = "com.android.launcher3.tests." + } +} diff --git a/tests/multivalentTests/src/com/android/launcher3/celllayout/board/WidgetRect.java b/tests/multivalentTests/src/com/android/launcher3/celllayout/board/WidgetRect.java deleted file mode 100644 index c90ce8504f..0000000000 --- a/tests/multivalentTests/src/com/android/launcher3/celllayout/board/WidgetRect.java +++ /dev/null @@ -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(); - } -} From ce638d69b7af749b34cf8da2751a3f3a8d45ab97 Mon Sep 17 00:00:00 2001 From: Fengjiang Li Date: Mon, 5 Aug 2024 09:35:21 -0700 Subject: [PATCH 3/4] Fix jank regression from AllAppsRecyclerViewPoolTest Unit test ag/28323761 delays the preInflationCount check after creating ActivityContext on main thread, thus causing jank regression. This CL is a forward fix. Fix: 354560500 Flag: NONE - jank fix Test: Presubmit Change-Id: I0e91dd765f1805b98895ce90804ec187e50285b4 --- .../recyclerview/AllAppsRecyclerViewPool.kt | 21 +++++++++++++------ .../AllAppsRecyclerViewPoolTest.kt | 8 +++---- 2 files changed, 19 insertions(+), 10 deletions(-) diff --git a/src/com/android/launcher3/recyclerview/AllAppsRecyclerViewPool.kt b/src/com/android/launcher3/recyclerview/AllAppsRecyclerViewPool.kt index f895b302c5..f231b92494 100644 --- a/src/com/android/launcher3/recyclerview/AllAppsRecyclerViewPool.kt +++ b/src/com/android/launcher3/recyclerview/AllAppsRecyclerViewPool.kt @@ -53,6 +53,10 @@ class AllAppsRecyclerViewPool : RecycledViewPool() where T : Context, T : Act fun preInflateAllAppsViewHolders(context: T) { val appsView = context.appsView ?: return val activeRv: RecyclerView = appsView.activeRecyclerView ?: return + val preInflateCount = getPreinflateCount(context) + if (preInflateCount <= 0) { + return + } // Create a separate context dedicated for all apps preinflation thread. The goal is to // create a separate AssetManager obj internally to avoid lock contention with @@ -81,7 +85,12 @@ class AllAppsRecyclerViewPool : RecycledViewPool() where T : Context, T : Act override fun getLayoutManager(): RecyclerView.LayoutManager? = null } - preInflateAllAppsViewHolders(adapter, BaseAllAppsAdapter.VIEW_TYPE_ICON, activeRv) { + preInflateAllAppsViewHolders( + adapter, + BaseAllAppsAdapter.VIEW_TYPE_ICON, + activeRv, + preInflateCount + ) { getPreinflateCount(context) } } @@ -91,10 +100,10 @@ class AllAppsRecyclerViewPool : RecycledViewPool() where T : Context, T : Act adapter: RecyclerView.Adapter<*>, viewType: Int, parent: ViewGroup, + preInflationCount: Int, preInflationCountProvider: () -> Int ) { - val preinflationCount = preInflationCountProvider.invoke() - if (preinflationCount <= 0) { + if (preInflationCount <= 0) { return } mCancellableTask?.cancel() @@ -103,7 +112,7 @@ class AllAppsRecyclerViewPool : RecycledViewPool() where T : Context, T : Act CancellableTask( { val list: ArrayList = ArrayList() - for (i in 0 until preinflationCount) { + for (i in 0 until preInflationCount) { if (task?.canceled == true) { break } @@ -114,8 +123,8 @@ class AllAppsRecyclerViewPool : RecycledViewPool() where T : Context, T : Act MAIN_EXECUTOR, { viewHolders -> // Run preInflationCountProvider again as the needed VH might have changed - val newPreinflationCount = preInflationCountProvider.invoke() - for (i in 0 until minOf(viewHolders.size, newPreinflationCount)) { + val newPreInflationCount = preInflationCountProvider.invoke() + for (i in 0 until minOf(viewHolders.size, newPreInflationCount)) { putRecycledView(viewHolders[i]) } } diff --git a/tests/multivalentTests/src/com/android/launcher3/recyclerview/AllAppsRecyclerViewPoolTest.kt b/tests/multivalentTests/src/com/android/launcher3/recyclerview/AllAppsRecyclerViewPoolTest.kt index 82043130a9..3e6aae2897 100644 --- a/tests/multivalentTests/src/com/android/launcher3/recyclerview/AllAppsRecyclerViewPoolTest.kt +++ b/tests/multivalentTests/src/com/android/launcher3/recyclerview/AllAppsRecyclerViewPoolTest.kt @@ -65,7 +65,7 @@ class AllAppsRecyclerViewPoolTest where T : Context, T : ActivityContext { @Test fun preinflate_success() { - underTest.preInflateAllAppsViewHolders(adapter, VIEW_TYPE, parent) { 10 } + underTest.preInflateAllAppsViewHolders(adapter, VIEW_TYPE, parent, 10) { 10 } awaitTasksCompleted() assertThat(underTest.getRecycledViewCount(VIEW_TYPE)).isEqualTo(10) @@ -73,7 +73,7 @@ class AllAppsRecyclerViewPoolTest where T : Context, T : ActivityContext { @Test fun preinflate_not_triggered() { - underTest.preInflateAllAppsViewHolders(adapter, VIEW_TYPE, parent) { 0 } + underTest.preInflateAllAppsViewHolders(adapter, VIEW_TYPE, parent, 0) { 0 } awaitTasksCompleted() assertThat(underTest.getRecycledViewCount(VIEW_TYPE)).isEqualTo(0) @@ -81,7 +81,7 @@ class AllAppsRecyclerViewPoolTest where T : Context, T : ActivityContext { @Test fun preinflate_cancel_before_runOnMainThread() { - underTest.preInflateAllAppsViewHolders(adapter, VIEW_TYPE, parent) { 10 } + underTest.preInflateAllAppsViewHolders(adapter, VIEW_TYPE, parent, 10) { 10 } assertThat(underTest.mCancellableTask!!.canceled).isFalse() underTest.clear() @@ -94,7 +94,7 @@ class AllAppsRecyclerViewPoolTest where T : Context, T : ActivityContext { @Test fun preinflate_cancel_after_run() { - underTest.preInflateAllAppsViewHolders(adapter, VIEW_TYPE, parent) { 10 } + underTest.preInflateAllAppsViewHolders(adapter, VIEW_TYPE, parent, 10) { 10 } assertThat(underTest.mCancellableTask!!.canceled).isFalse() awaitTasksCompleted() From 8c629fd8b56e76fc2eed1b0702a1cb8d32b7e7d6 Mon Sep 17 00:00:00 2001 From: Mady Mellor Date: Thu, 1 Aug 2024 13:36:39 -0700 Subject: [PATCH 4/4] Support optional bubble overflow in bubble bar This is similar to the animations that add / remove a bubble at the same time -- the overflow is generally added when a bubble is removed. The overflow is generally removed when a bubble is added (i.e. user promotes a bubble out of the overflow). There are a couple of additional cases: - when bubbles are first added to the bar -- if there were saved bubbles in the overflow, the view should be added - an app could cancel its bubbles / remove its shortcuts and not have any in the stack but could have some in the overflow & it could become empty without an addition. Flag: com.android.wm.shell.enable_optional_bubble_overflow Flag: com.android.wm.shell.enable_bubble_bar Test: manual - add bubbles to the bubble bar for first time => observe there is no overflow - dismiss a bubble => observe the overflow is added, tap on it, tap on the bubble in it => observe that bubble is added & the overflow disappears - dismiss all the bubbles - add a bubble => observe the overflow is there & has the previously dismissed bubbles - cancel all the bubbles that are in the overflow via adb => observe the overflow is remvoed Bug: 334175587 Change-Id: I2b6e855e65520b4b2b1fde7757d46f00a468b4a6 --- .../taskbar/bubbles/BubbleBarController.java | 28 ++++++- .../taskbar/bubbles/BubbleBarView.java | 82 +++++++++++-------- .../bubbles/BubbleBarViewController.java | 44 ++++++++-- .../launcher3/taskbar/bubbles/BubbleView.java | 7 ++ 4 files changed, 121 insertions(+), 40 deletions(-) diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java index fbee0802e2..33d8a8430f 100644 --- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java +++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java @@ -138,6 +138,8 @@ public class BubbleBarController extends IBubblesListener.Stub { List removedBubbles; List bubbleKeysInOrder; Point expandedViewDropTargetSize; + boolean showOverflow; + boolean showOverflowChanged; // These need to be loaded in the background BubbleBarBubble addedBubble; @@ -156,6 +158,8 @@ public class BubbleBarController extends IBubblesListener.Stub { removedBubbles = update.removedBubbles; bubbleKeysInOrder = update.bubbleKeysInOrder; expandedViewDropTargetSize = update.expandedViewDropTargetSize; + showOverflow = update.showOverflow; + showOverflowChanged = update.showOverflowChanged; } } @@ -271,7 +275,13 @@ public class BubbleBarController extends IBubblesListener.Stub { BubbleBarBubble bubbleToSelect = null; - if (update.addedBubble != null && update.removedBubbles.size() == 1) { + if (Flags.enableOptionalBubbleOverflow() + && update.showOverflowChanged && !update.showOverflow && update.addedBubble != null + && update.removedBubbles.isEmpty()) { + // A bubble was added from the overflow (& now it's empty / not showing) + mBubbles.put(update.addedBubble.getKey(), update.addedBubble); + mBubbleBarViewController.removeOverflowAndAddBubble(update.addedBubble); + } else if (update.addedBubble != null && update.removedBubbles.size() == 1) { // we're adding and removing a bubble at the same time. handle this as a single update. RemovedBubble removedBubble = update.removedBubbles.get(0); BubbleBarBubble bubbleToRemove = mBubbles.remove(removedBubble.getKey()); @@ -285,11 +295,17 @@ public class BubbleBarController extends IBubblesListener.Stub { Log.w(TAG, "trying to remove bubble that doesn't exist: " + removedBubble.getKey()); } } else { + boolean overflowNeedsToBeAdded = Flags.enableOptionalBubbleOverflow() + && update.showOverflowChanged && update.showOverflow; if (!update.removedBubbles.isEmpty()) { for (int i = 0; i < update.removedBubbles.size(); i++) { RemovedBubble removedBubble = update.removedBubbles.get(i); BubbleBarBubble bubble = mBubbles.remove(removedBubble.getKey()); - if (bubble != null) { + if (bubble != null && overflowNeedsToBeAdded) { + // First removal, show the overflow + overflowNeedsToBeAdded = false; + mBubbleBarViewController.addOverflowAndRemoveBubble(bubble); + } else if (bubble != null) { mBubbleBarViewController.removeBubble(bubble); } else { Log.w(TAG, "trying to remove bubble that doesn't exist: " @@ -302,6 +318,11 @@ public class BubbleBarController extends IBubblesListener.Stub { mBubbleBarViewController.addBubble(update.addedBubble, isExpanding, suppressAnimation); } + if (Flags.enableOptionalBubbleOverflow() + && update.showOverflowChanged + && update.showOverflow != mBubbleBarViewController.isOverflowAdded()) { + mBubbleBarViewController.showOverflow(update.showOverflow); + } } // if a bubble was updated upstream, but removed before the update was received, add it back @@ -333,6 +354,9 @@ public class BubbleBarController extends IBubblesListener.Stub { } } } + if (Flags.enableOptionalBubbleOverflow() && update.initialState && update.showOverflow) { + mBubbleBarViewController.showOverflow(true); + } // Adds and removals have happened, update visibility before any other visual changes. mBubbleBarViewController.setHiddenForBubbles(mBubbles.isEmpty()); diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java index 7d27a90420..32ca9f2f7c 100644 --- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java +++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java @@ -715,11 +715,13 @@ public class BubbleBarView extends FrameLayout { public void addBubble(BubbleView bubble) { FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams((int) mIconSize, (int) mIconSize, Gravity.LEFT); + final int index = bubble.isOverflow() ? getChildCount() : 0; + if (isExpanded()) { // if we're expanded scale the new bubble in bubble.setScaleX(0f); bubble.setScaleY(0f); - addView(bubble, 0, lp); + addView(bubble, index, lp); bubble.showDotIfNeeded(/* animate= */ false); mBubbleAnimator = new BubbleAnimator(mIconSize, mExpandedBarIconsSpacing, @@ -748,23 +750,33 @@ public class BubbleBarView extends FrameLayout { }; mBubbleAnimator.animateNewBubble(indexOfChild(mSelectedBubbleView), listener); } else { - addView(bubble, 0, lp); + addView(bubble, index, lp); } } /** Add a new bubble and remove an old bubble from the bubble bar. */ - public void addBubbleAndRemoveBubble(View addedBubble, View removedBubble) { + public void addBubbleAndRemoveBubble(BubbleView addedBubble, BubbleView removedBubble) { FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams((int) mIconSize, (int) mIconSize, Gravity.LEFT); + boolean isOverflowSelected = mSelectedBubbleView.isOverflow(); + boolean removingOverflow = removedBubble.isOverflow(); + boolean addingOverflow = addedBubble.isOverflow(); + if (!isExpanded()) { removeView(removedBubble); - addView(addedBubble, 0, lp); + int index = addingOverflow ? getChildCount() : 0; + addView(addedBubble, index, lp); return; } + int index = addingOverflow ? getChildCount() : 0; addedBubble.setScaleX(0f); addedBubble.setScaleY(0f); - addView(addedBubble, 0, lp); + addView(addedBubble, index, lp); + if (isOverflowSelected && removingOverflow) { + // The added bubble will be selected + mSelectedBubbleView = addedBubble; + } int indexOfSelectedBubble = indexOfChild(mSelectedBubbleView); int indexOfBubbleToRemove = indexOfChild(removedBubble); @@ -924,7 +936,7 @@ public class BubbleBarView extends FrameLayout { final float currentWidth = getWidth(); final float expandedWidth = expandedWidth(); final float collapsedWidth = collapsedWidth(); - int bubbleCount = getChildCount(); + int childCount = getChildCount(); float viewBottom = mBubbleBarBounds.height() + (isExpanded() ? mPointerSize : 0); float bubbleBarAnimatedTop = viewBottom - getBubbleBarHeight(); // When translating X & Y the scale is ignored, so need to deduct it from the translations @@ -932,7 +944,7 @@ public class BubbleBarView extends FrameLayout { final boolean onLeft = bubbleBarLocation.isOnLeft(isLayoutRtl()); // elevation state is opposite to widthState - when expanded all icons are flat float elevationState = (1 - widthState); - for (int i = 0; i < bubbleCount; i++) { + for (int i = 0; i < childCount; i++) { BubbleView bv = (BubbleView) getChildAt(i); if (bv == mDraggedBubbleView || bv == mDismissedByDragBubbleView) { // Skip the dragged bubble. Its translation is managed by the drag controller. @@ -951,9 +963,9 @@ public class BubbleBarView extends FrameLayout { bv.setTranslationY(ty); // the position of the bubble when the bar is fully expanded - final float expandedX = getExpandedBubbleTranslationX(i, bubbleCount, onLeft); + final float expandedX = getExpandedBubbleTranslationX(i, childCount, onLeft); // the position of the bubble when the bar is fully collapsed - final float collapsedX = getCollapsedBubbleTranslationX(i, bubbleCount, onLeft); + final float collapsedX = getCollapsedBubbleTranslationX(i, childCount, onLeft); // slowly animate elevation while keeping correct Z ordering float fullElevationForChild = (MAX_BUBBLES * mBubbleElevation) - i; @@ -981,13 +993,10 @@ public class BubbleBarView extends FrameLayout { final float collapsedBarShift = onLeft ? 0 : currentWidth - collapsedWidth; final float targetX = collapsedX + collapsedBarShift; bv.setTranslationX(widthState * (expandedX - targetX) + targetX); - // If we're fully collapsed, hide all bubbles except for the first 2. If there are - // only 2 bubbles, hide the second bubble as well because it's the overflow. + // If we're fully collapsed, hide all bubbles except for the first 2, excluding + // the overflow. if (widthState == 0) { - if (i > MAX_VISIBLE_BUBBLES_COLLAPSED - 1) { - bv.setAlpha(0); - } else if (i == MAX_VISIBLE_BUBBLES_COLLAPSED - 1 - && bubbleCount == MAX_VISIBLE_BUBBLES_COLLAPSED) { + if (bv.isOverflow() || i > MAX_VISIBLE_BUBBLES_COLLAPSED - 1) { bv.setAlpha(0); } else { bv.setAlpha(1); @@ -1043,22 +1052,26 @@ public class BubbleBarView extends FrameLayout { return translationX - getScaleIconShift(); } - private float getCollapsedBubbleTranslationX(int bubbleIndex, int bubbleCount, boolean onLeft) { - if (bubbleIndex < 0 || bubbleIndex >= bubbleCount) { + private float getCollapsedBubbleTranslationX(int bubbleIndex, int childCount, boolean onLeft) { + if (bubbleIndex < 0 || bubbleIndex >= childCount) { return 0; } float translationX; if (onLeft) { - // Shift the first bubble only if there are more bubbles in addition to overflow - translationX = mBubbleBarPadding + ( - bubbleIndex == 0 && bubbleCount > MAX_VISIBLE_BUBBLES_COLLAPSED - ? mIconOverlapAmount : 0); + // Shift the first bubble only if there are more bubbles + if (bubbleIndex == 0 && getBubbleChildCount() >= MAX_VISIBLE_BUBBLES_COLLAPSED) { + translationX = mIconOverlapAmount; + } else { + translationX = 0f; + } } else { - translationX = mBubbleBarPadding + ( - bubbleIndex == 0 || bubbleCount <= MAX_VISIBLE_BUBBLES_COLLAPSED - ? 0 : mIconOverlapAmount); + if (bubbleIndex == 1 && getBubbleChildCount() >= MAX_VISIBLE_BUBBLES_COLLAPSED) { + translationX = mIconOverlapAmount; + } else { + translationX = 0f; + } } - return translationX - getScaleIconShift(); + return mBubbleBarPadding + translationX - getScaleIconShift(); } /** @@ -1256,15 +1269,20 @@ public class BubbleBarView extends FrameLayout { } private float collapsedWidth() { - final int childCount = getChildCount(); + final int bubbleChildCount = getBubbleChildCount(); final float horizontalPadding = 2 * mBubbleBarPadding; - // If there are more than 2 bubbles, the first 2 should be visible when collapsed. - // Otherwise just the first bubble should be visible because we don't show the overflow. - return childCount > MAX_VISIBLE_BUBBLES_COLLAPSED + // If there are more than 2 bubbles, the first 2 should be visible when collapsed, + // excluding the overflow. + return bubbleChildCount >= MAX_VISIBLE_BUBBLES_COLLAPSED ? getScaledIconSize() + mIconOverlapAmount + horizontalPadding : getScaledIconSize() + horizontalPadding; } + /** Returns the child count excluding the overflow if it's present. */ + private int getBubbleChildCount() { + return hasOverflow() ? getChildCount() - 1 : getChildCount(); + } + private float getBubbleBarExpandedHeight() { return getBubbleBarCollapsedHeight() + mPointerSize; } @@ -1303,8 +1321,8 @@ public class BubbleBarView extends FrameLayout { return mIsAnimatingNewBubble; } - private boolean hasOverview() { - // Overview is always the last bubble + private boolean hasOverflow() { + // Overflow is always the last bubble View lastChild = getChildAt(getChildCount() - 1); if (lastChild instanceof BubbleView bubbleView) { return bubbleView.getBubble() instanceof BubbleBarOverflow; @@ -1336,7 +1354,7 @@ public class BubbleBarView extends FrameLayout { CharSequence contentDesc = firstChild != null ? firstChild.getContentDescription() : ""; // Don't count overflow if it exists - int bubbleCount = getChildCount() - (hasOverview() ? 1 : 0); + int bubbleCount = getChildCount() - (hasOverflow() ? 1 : 0); if (bubbleCount > 1) { contentDesc = getResources().getString(R.string.bubble_bar_description_multiple_bubbles, contentDesc, bubbleCount - 1); diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java index 590916edd0..2cdc0ced86 100644 --- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java +++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java @@ -92,6 +92,8 @@ public class BubbleBarViewController { private boolean mHiddenForNoBubbles = true; private boolean mShouldShowEducation; + public boolean mOverflowAdded; + private BubbleBarViewAnimator mBubbleBarViewAnimator; private final TimeSource mTimeSource = System::currentTimeMillis; @@ -123,7 +125,6 @@ public class BubbleBarViewController { mBubbleBarClickListener = v -> expandBubbleBar(); mBubbleDragController.setupBubbleBarView(mBarView); mOverflowBubble = bubbleControllers.bubbleCreator.createOverflow(mBarView); - addOverflow(); mBarView.setOnClickListener(mBubbleBarClickListener); mBarView.addOnLayoutChangeListener( (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> { @@ -494,13 +495,44 @@ public class BubbleBarViewController { } } - /** - * Adds the overflow view to the bubble bar. - */ - public void addOverflow() { - mBarView.addBubble(mOverflowBubble.getView()); + /** Whether the overflow view is added to the bubble bar. */ + public boolean isOverflowAdded() { + return mOverflowAdded; + } + + /** Shows or hides the overflow view. */ + public void showOverflow(boolean showOverflow) { + if (mOverflowAdded == showOverflow) return; + mOverflowAdded = showOverflow; + if (mOverflowAdded) { + mBarView.addBubble(mOverflowBubble.getView()); + mOverflowBubble.getView().setOnClickListener(mBubbleClickListener); + mOverflowBubble.getView().setController(mBubbleViewController); + } else { + mBarView.removeBubble(mOverflowBubble.getView()); + mOverflowBubble.getView().setOnClickListener(null); + mOverflowBubble.getView().setController(null); + } + } + + /** Adds the overflow view to the bubble bar while animating a view away. */ + public void addOverflowAndRemoveBubble(BubbleBarBubble removedBubble) { + if (mOverflowAdded) return; + mOverflowAdded = true; + mBarView.addBubbleAndRemoveBubble(mOverflowBubble.getView(), removedBubble.getView()); mOverflowBubble.getView().setOnClickListener(mBubbleClickListener); mOverflowBubble.getView().setController(mBubbleViewController); + removedBubble.getView().setController(null); + } + + /** Removes the overflow view to the bubble bar while animating a view in. */ + public void removeOverflowAndAddBubble(BubbleBarBubble addedBubble) { + if (!mOverflowAdded) return; + mOverflowAdded = false; + mBarView.addBubbleAndRemoveBubble(addedBubble.getView(), mOverflowBubble.getView()); + addedBubble.getView().setOnClickListener(mBubbleClickListener); + addedBubble.getView().setController(mBubbleViewController); + mOverflowBubble.getView().setController(null); } /** diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleView.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleView.java index 09da3e0176..f0f28729a6 100644 --- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleView.java +++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleView.java @@ -74,6 +74,7 @@ public class BubbleView extends ConstraintLayout { private boolean mOnLeft = false; private BubbleBarItem mBubble; + private boolean mIsOverflow; private Bitmap mIcon; @@ -271,12 +272,18 @@ public class BubbleView extends ConstraintLayout { */ public void setOverflow(BubbleBarOverflow overflow, Bitmap bitmap) { mBubble = overflow; + mIsOverflow = true; mIcon = bitmap; updateBubbleIcon(); mAppIcon.setVisibility(GONE); // Overflow doesn't show the app badge setContentDescription(getResources().getString(R.string.bubble_bar_overflow_description)); } + /** Whether this view represents the overflow button. */ + public boolean isOverflow() { + return mIsOverflow; + } + /** Returns the bubble being rendered in this view. */ @Nullable public BubbleBarItem getBubble() {