diff --git a/Android.bp b/Android.bp index 78db013d3a..cdada0a87f 100644 --- a/Android.bp +++ b/Android.bp @@ -201,6 +201,8 @@ android_library { "Launcher3ResLib", "launcher-testing-shared", "animationlib", + "kotlinx_coroutines_android", + "kotlinx_coroutines", "com_android_launcher3_flags_lib", "com_android_wm_shell_flags_lib", "android.appwidget.flags-aconfig-java", diff --git a/aconfig/launcher.aconfig b/aconfig/launcher.aconfig index a724602e7b..634caa705c 100644 --- a/aconfig/launcher.aconfig +++ b/aconfig/launcher.aconfig @@ -173,6 +173,13 @@ flag { bug: "318410881" } +flag { + name: "force_monochrome_app_icons" + namespace: "launcher" + description: "Enable the ability to generate monochromatic icons, if it is not provided by the app" + bug: "270396209" +} + flag { name: "enable_add_app_widget_via_config_activity_v2" namespace: "launcher" diff --git a/quickstep/src/com/android/quickstep/BaseActivityInterface.java b/quickstep/src/com/android/quickstep/BaseActivityInterface.java index 2341e4ccbd..59302b7afa 100644 --- a/quickstep/src/com/android/quickstep/BaseActivityInterface.java +++ b/quickstep/src/com/android/quickstep/BaseActivityInterface.java @@ -379,13 +379,6 @@ public abstract class BaseActivityInterface { val spaceAboveSnapshot = dp.overviewTaskThumbnailTopMarginPx val totalThumbnailHeight = parentHeight - spaceAboveSnapshot - val dividerBar = - Math.round( - totalThumbnailHeight * - if (splitBoundsConfig.appsStackedVertically) - splitBoundsConfig.dividerHeightPercent - else splitBoundsConfig.dividerWidthPercent - ) + val dividerBar = getDividerBarSize(totalThumbnailHeight, splitBoundsConfig) + val taskPercent = if (splitBoundsConfig.appsStackedVertically) { splitBoundsConfig.topTaskPercent @@ -569,64 +565,22 @@ open class LandscapePagedViewHandler : RecentsPagedOrientationHandler { deviceProfile: DeviceProfile, splitConfig: SplitBounds ) { - val primaryIconParams = primaryIconView.layoutParams as FrameLayout.LayoutParams - val secondaryIconParams = - if (Flags.enableOverviewIconMenu()) - secondaryIconView.layoutParams as FrameLayout.LayoutParams - else FrameLayout.LayoutParams(primaryIconParams) + val spaceAboveSnapshot = deviceProfile.overviewTaskThumbnailTopMarginPx + val totalThumbnailHeight = groupedTaskViewHeight - spaceAboveSnapshot + val dividerBar: Int = getDividerBarSize(totalThumbnailHeight, splitConfig) - // We calculate the "midpoint" of the thumbnail area, and place the icons there. - // This is the place where the thumbnail area splits by default, in a near-50/50 split. - // It is usually not exactly 50/50, due to insets/screen cutouts. - val fullscreenInsetThickness = (deviceProfile.insets.top - deviceProfile.insets.bottom) - val fullscreenMidpointFromBottom = ((deviceProfile.heightPx - fullscreenInsetThickness) / 2) - val midpointFromBottomPct = fullscreenMidpointFromBottom.toFloat() / deviceProfile.heightPx - val insetPct = fullscreenInsetThickness.toFloat() / deviceProfile.heightPx - val spaceAboveSnapshots = deviceProfile.overviewTaskThumbnailTopMarginPx - val overviewThumbnailAreaThickness = groupedTaskViewHeight - spaceAboveSnapshots - val bottomToMidpointOffset = - (overviewThumbnailAreaThickness * midpointFromBottomPct).toInt() - val insetOffset = (overviewThumbnailAreaThickness * insetPct).toInt() - if (Flags.enableOverviewIconMenu()) { - val gravity = if (isRtl) Gravity.BOTTOM or Gravity.START else Gravity.TOP or Gravity.END - primaryIconParams.gravity = gravity - secondaryIconParams.gravity = gravity - } else { - primaryIconParams.gravity = Gravity.BOTTOM or if (isRtl) Gravity.START else Gravity.END - secondaryIconParams.gravity = - Gravity.BOTTOM or if (isRtl) Gravity.START else Gravity.END - } - primaryIconView.translationX = 0f - secondaryIconView.translationX = 0f - when { - Flags.enableOverviewIconMenu() -> { - val primaryAppChipView = primaryIconView as IconAppChipView - val secondaryAppChipView = secondaryIconView as IconAppChipView - if (primaryIconView.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL) { - secondaryAppChipView.setSplitTranslationY(-primarySnapshotHeight.toFloat()) - primaryAppChipView.setSplitTranslationY(0f) - } else { - val secondarySnapshotHeight = groupedTaskViewHeight - primarySnapshotHeight - primaryAppChipView.setSplitTranslationY(secondarySnapshotHeight.toFloat()) - } - } - splitConfig.initiatedFromSeascape -> { - // if the split was initiated from seascape, - // the task on the right (secondary) is slightly larger - primaryIconView.translationY = (-bottomToMidpointOffset - insetOffset).toFloat() - secondaryIconView.translationY = - (-bottomToMidpointOffset - insetOffset + taskIconHeight).toFloat() - } - else -> { - // if not, - // the task on the left (primary) is slightly larger - primaryIconView.translationY = -bottomToMidpointOffset.toFloat() - secondaryIconView.translationY = - (-bottomToMidpointOffset + taskIconHeight).toFloat() - } - } - primaryIconView.layoutParams = primaryIconParams - secondaryIconView.layoutParams = secondaryIconParams + val (topLeftY, bottomRightY) = + getSplitIconsPosition( + taskIconHeight, + primarySnapshotHeight, + totalThumbnailHeight, + isRtl, + deviceProfile.overviewTaskMarginPx, + dividerBar + ) + + updateSplitIconsPosition(primaryIconView, topLeftY, isRtl) + updateSplitIconsPosition(secondaryIconView, bottomRightY, isRtl) } override fun getDefaultSplitPosition(deviceProfile: DeviceProfile): Int { @@ -656,4 +610,91 @@ open class LandscapePagedViewHandler : RecentsPagedOrientationHandler { override fun getFloatingTaskPrimaryTranslation(floatingTask: View, dp: DeviceProfile): Float = floatingTask.translationY + + /** + * Retrieves split icons position + * + * @param taskIconHeight The height of the task icon. + * @param primarySnapshotHeight The height for the primary snapshot (i.e., top-left snapshot). + * @param totalThumbnailHeight The total height for the group task view. + * @param isRtl Whether the layout direction is RTL (or false for LTR). + * @param overviewTaskMarginPx The space under the focused task icon provided by Device Profile. + * @param dividerSize The size of the divider for the group task view. + * @return The top-left and right-bottom positions for the icon views. + */ + @VisibleForTesting + open fun getSplitIconsPosition( + taskIconHeight: Int, + primarySnapshotHeight: Int, + totalThumbnailHeight: Int, + isRtl: Boolean, + overviewTaskMarginPx: Int, + dividerSize: Int, + ): SplitIconPositions { + return if (Flags.enableOverviewIconMenu()) { + if (isRtl) { + SplitIconPositions(0, -(totalThumbnailHeight - primarySnapshotHeight)) + } else { + SplitIconPositions(0, primarySnapshotHeight + dividerSize) + } + } else { + val topLeftY = primarySnapshotHeight + overviewTaskMarginPx + SplitIconPositions( + topLeftY = topLeftY, + bottomRightY = topLeftY + dividerSize + taskIconHeight + ) + } + } + + /** + * Updates icon view gravity and translation for split tasks + * + * @param iconView View to be updated + * @param translationY the translationY that should be applied + * @param isRtl Whether the layout direction is RTL (or false for LTR). + */ + @SuppressLint("RtlHardcoded") + @VisibleForTesting + open fun updateSplitIconsPosition(iconView: View, translationY: Int, isRtl: Boolean) { + val layoutParams = iconView.layoutParams as FrameLayout.LayoutParams + + if (Flags.enableOverviewIconMenu()) { + val appChipView = iconView as IconAppChipView + layoutParams.gravity = + if (isRtl) Gravity.BOTTOM or Gravity.START else Gravity.TOP or Gravity.END + appChipView.layoutParams = layoutParams + appChipView.setSplitTranslationX(0f) + appChipView.setSplitTranslationY(translationY.toFloat()) + } else { + layoutParams.gravity = Gravity.TOP or Gravity.RIGHT + layoutParams.topMargin = translationY + iconView.translationX = 0f + iconView.translationY = 0f + iconView.layoutParams = layoutParams + } + } + + /** + * It calculates the divider's size in the group task view. + * + * @param totalThumbnailHeight The total height for the group task view + * @param splitConfig Contains information about sizes and proportions for split task. + * @return The divider size for the group task view. + */ + protected fun getDividerBarSize(totalThumbnailHeight: Int, splitConfig: SplitBounds): Int { + return Math.round( + totalThumbnailHeight * + if (splitConfig.appsStackedVertically) splitConfig.dividerHeightPercent + else splitConfig.dividerWidthPercent + ) + } + + /** + * Data structure to keep the y position to be used for the split task icon views translation. + * + * @param topLeftY The y-axis position for the task view position on the Top or Left side. + * @param bottomRightY The y-axis position for the task view position on the Bottom or Right + * side. + */ + data class SplitIconPositions(val topLeftY: Int, val bottomRightY: Int) } diff --git a/quickstep/src/com/android/quickstep/orientation/PortraitPagedViewHandler.java b/quickstep/src/com/android/quickstep/orientation/PortraitPagedViewHandler.java index 62dfd82c9f..0476fe80d6 100644 --- a/quickstep/src/com/android/quickstep/orientation/PortraitPagedViewHandler.java +++ b/quickstep/src/com/android/quickstep/orientation/PortraitPagedViewHandler.java @@ -75,6 +75,7 @@ public class PortraitPagedViewHandler extends DefaultPagedViewHandler implements public T getSecondaryValue(T x, T y) { return y; } + @Override public boolean isLayoutNaturalToLauncher() { return true; diff --git a/quickstep/src/com/android/quickstep/orientation/SeascapePagedViewHandler.kt b/quickstep/src/com/android/quickstep/orientation/SeascapePagedViewHandler.kt index 0c78b8f9cf..5bebf8c8be 100644 --- a/quickstep/src/com/android/quickstep/orientation/SeascapePagedViewHandler.kt +++ b/quickstep/src/com/android/quickstep/orientation/SeascapePagedViewHandler.kt @@ -15,6 +15,7 @@ */ package com.android.quickstep.orientation +import android.annotation.SuppressLint import android.content.res.Resources import android.graphics.Point import android.graphics.PointF @@ -32,7 +33,12 @@ import com.android.launcher3.Flags import com.android.launcher3.R import com.android.launcher3.Utilities import com.android.launcher3.touch.SingleAxisSwipeDetector -import com.android.launcher3.util.SplitConfigurationOptions.* +import com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_BOTTOM_OR_RIGHT +import com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_TOP_OR_LEFT +import com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_UNDEFINED +import com.android.launcher3.util.SplitConfigurationOptions.STAGE_TYPE_MAIN +import com.android.launcher3.util.SplitConfigurationOptions.SplitBounds +import com.android.launcher3.util.SplitConfigurationOptions.SplitPositionOption import com.android.launcher3.views.BaseDragLayer import com.android.quickstep.views.IconAppChipView @@ -255,97 +261,6 @@ class SeascapePagedViewHandler : LandscapePagedViewHandler() { iconAppChipView.setRotation(degreesRotated) } - override fun setSplitIconParams( - primaryIconView: View, - secondaryIconView: View, - taskIconHeight: Int, - primarySnapshotWidth: Int, - primarySnapshotHeight: Int, - groupedTaskViewHeight: Int, - groupedTaskViewWidth: Int, - isRtl: Boolean, - deviceProfile: DeviceProfile, - splitConfig: SplitBounds - ) { - super.setSplitIconParams( - primaryIconView, - secondaryIconView, - taskIconHeight, - primarySnapshotWidth, - primarySnapshotHeight, - groupedTaskViewHeight, - groupedTaskViewWidth, - isRtl, - deviceProfile, - splitConfig - ) - val primaryIconParams = primaryIconView.layoutParams as FrameLayout.LayoutParams - val secondaryIconParams = secondaryIconView.layoutParams as FrameLayout.LayoutParams - - // We calculate the "midpoint" of the thumbnail area, and place the icons there. - // This is the place where the thumbnail area splits by default, in a near-50/50 split. - // It is usually not exactly 50/50, due to insets/screen cutouts. - val fullscreenInsetThickness = (deviceProfile.insets.top - deviceProfile.insets.bottom) - val fullscreenMidpointFromBottom = (deviceProfile.heightPx - fullscreenInsetThickness) / 2 - val midpointFromBottomPct = fullscreenMidpointFromBottom.toFloat() / deviceProfile.heightPx - val insetPct = fullscreenInsetThickness.toFloat() / deviceProfile.heightPx - val spaceAboveSnapshots = deviceProfile.overviewTaskThumbnailTopMarginPx - val overviewThumbnailAreaThickness = groupedTaskViewHeight - spaceAboveSnapshots - val bottomToMidpointOffset = - (overviewThumbnailAreaThickness * midpointFromBottomPct).toInt() - val insetOffset = (overviewThumbnailAreaThickness * insetPct).toInt() - val gravity = if (isRtl) Gravity.TOP or Gravity.END else Gravity.BOTTOM or Gravity.START - primaryIconParams.gravity = gravity - secondaryIconParams.gravity = gravity - primaryIconView.translationX = 0f - secondaryIconView.translationX = 0f - when { - Flags.enableOverviewIconMenu() -> { - val primaryAppChipView = primaryIconView as IconAppChipView - val secondaryAppChipView = secondaryIconView as IconAppChipView - if (isRtl) { - primaryAppChipView.setSplitTranslationY( - (groupedTaskViewHeight - primarySnapshotHeight).toFloat() - ) - secondaryAppChipView.setSplitTranslationY(0f) - } else { - secondaryAppChipView.setSplitTranslationY(-primarySnapshotHeight.toFloat()) - primaryAppChipView.setSplitTranslationY(0f) - } - } - splitConfig.initiatedFromSeascape -> { - // if the split was initiated from seascape, - // the task on the right (secondary) is slightly larger - if (isRtl) { - primaryIconView.translationY = - (bottomToMidpointOffset - insetOffset + taskIconHeight).toFloat() - secondaryIconView.translationY = - (bottomToMidpointOffset - insetOffset).toFloat() - } else { - primaryIconView.translationY = - (-bottomToMidpointOffset - insetOffset + taskIconHeight).toFloat() - secondaryIconView.translationY = - (-bottomToMidpointOffset - insetOffset).toFloat() - } - } - else -> { - // if not, - // the task on the left (primary) is slightly larger - if (isRtl) { - primaryIconView.translationY = - (bottomToMidpointOffset + taskIconHeight).toFloat() - secondaryIconView.translationY = bottomToMidpointOffset.toFloat() - } else { - primaryIconView.translationY = - (-bottomToMidpointOffset + taskIconHeight).toFloat() - secondaryIconView.translationY = -bottomToMidpointOffset.toFloat() - } - } - } - primaryIconView.layoutParams = primaryIconParams - secondaryIconView.layoutParams = secondaryIconParams - } - override fun measureGroupedTaskViewThumbnailBounds( primarySnapshot: View, secondarySnapshot: View, @@ -366,13 +281,8 @@ class SeascapePagedViewHandler : LandscapePagedViewHandler() { // (portrait bottom) and secondary is on the right (portrait top) val spaceAboveSnapshot = dp.overviewTaskThumbnailTopMarginPx val totalThumbnailHeight = parentHeight - spaceAboveSnapshot - val dividerBar = - Math.round( - totalThumbnailHeight * - if (splitBoundsConfig.appsStackedVertically) - splitBoundsConfig.dividerHeightPercent - else splitBoundsConfig.dividerWidthPercent - ) + val dividerBar = getDividerBarSize(totalThumbnailHeight, splitBoundsConfig) + val (taskViewFirst, taskViewSecond) = getGroupedTaskViewSizes(dp, splitBoundsConfig, parentWidth, parentHeight) secondarySnapshot.translationY = 0f @@ -398,13 +308,8 @@ class SeascapePagedViewHandler : LandscapePagedViewHandler() { // (portrait bottom) and secondary is on the right (portrait top) val spaceAboveSnapshot = dp.overviewTaskThumbnailTopMarginPx val totalThumbnailHeight = parentHeight - spaceAboveSnapshot - val dividerBar = - Math.round( - totalThumbnailHeight * - if (splitBoundsConfig.appsStackedVertically) - splitBoundsConfig.dividerHeightPercent - else splitBoundsConfig.dividerWidthPercent - ) + val dividerBar = getDividerBarSize(totalThumbnailHeight, splitBoundsConfig) + val taskPercent = if (splitBoundsConfig.appsStackedVertically) { splitBoundsConfig.topTaskPercent @@ -430,4 +335,64 @@ class SeascapePagedViewHandler : LandscapePagedViewHandler() { override fun getTaskDragDisplacementFactor(isRtl: Boolean): Int = if (isRtl) -1 else 1 /* -------------------- */ + + override fun getSplitIconsPosition( + taskIconHeight: Int, + primarySnapshotHeight: Int, + totalThumbnailHeight: Int, + isRtl: Boolean, + overviewTaskMarginPx: Int, + dividerSize: Int, + ): SplitIconPositions { + return if (Flags.enableOverviewIconMenu()) { + if (isRtl) { + SplitIconPositions( + topLeftY = totalThumbnailHeight - primarySnapshotHeight, + bottomRightY = 0 + ) + } else { + SplitIconPositions( + topLeftY = 0, + bottomRightY = -(primarySnapshotHeight + dividerSize) + ) + } + } else { + // In seascape, the icons are initially placed at the bottom start of the + // display (portrait locked). The values defined here are used to translate the icons + // from the bottom to the almost-center of the screen using the bottom margin. + // The primary snapshot is placed at the bottom, thus we translate the icons using + // the size of the primary snapshot minus the icon size for the top-left icon. + SplitIconPositions( + topLeftY = primarySnapshotHeight - taskIconHeight, + bottomRightY = primarySnapshotHeight + dividerSize + ) + } + } + + /** + * Updates icon view gravity and translation for split tasks + * + * @param iconView View to be updated + * @param translationY the translationY that should be applied + * @param isRtl Whether the layout direction is RTL (or false for LTR). + */ + @SuppressLint("RtlHardcoded") + override fun updateSplitIconsPosition(iconView: View, translationY: Int, isRtl: Boolean) { + val layoutParams = iconView.layoutParams as FrameLayout.LayoutParams + + if (Flags.enableOverviewIconMenu()) { + val appChipView = iconView as IconAppChipView + layoutParams.gravity = + if (isRtl) Gravity.TOP or Gravity.END else Gravity.BOTTOM or Gravity.START + appChipView.layoutParams = layoutParams + appChipView.setSplitTranslationX(0f) + appChipView.setSplitTranslationY(translationY.toFloat()) + } else { + layoutParams.gravity = Gravity.BOTTOM or Gravity.LEFT + iconView.translationX = 0f + iconView.translationY = 0f + layoutParams.bottomMargin = translationY + iconView.layoutParams = layoutParams + } + } } diff --git a/quickstep/src/com/android/quickstep/views/RecentsView.java b/quickstep/src/com/android/quickstep/views/RecentsView.java index 4985f0ba5e..2ead7a6f52 100644 --- a/quickstep/src/com/android/quickstep/views/RecentsView.java +++ b/quickstep/src/com/android/quickstep/views/RecentsView.java @@ -463,7 +463,6 @@ public abstract class RecentsView 0) { newFocusedTaskView = getTaskViewAt(0); - // Check if the first task is the desktop. - // If first task is desktop, try to find another task to set as the focused task - if (newFocusedTaskView != null && newFocusedTaskView.isDesktopTask() - && getTaskViewCount() > 1) { - newFocusedTaskView = getTaskViewAt(1); - } } mFocusedTaskViewId = newFocusedTaskView != null && !enableGridOnlyOverview() ? newFocusedTaskView.getTaskViewId() : INVALID_TASK_ID; @@ -1869,12 +1839,7 @@ public abstract class RecentsView 0) { - TaskView taskView = requireTaskViewAt(0); - // If first task id desktop, try to find another task to set the target page - if (taskView.isDesktopTask() && getTaskViewCount() > 1) { - taskView = requireTaskViewAt(1); - } - targetPage = indexOfChild(taskView); + targetPage = indexOfChild(requireTaskViewAt(0)); } } if (targetPage != -1 && mCurrentPage != targetPage) { @@ -2119,9 +2084,6 @@ public abstract class RecentsView focusedTaskIndex) { // For tasks after the focused task, shift by focused task's width and spacing. @@ -3060,7 +3000,7 @@ public abstract class RecentsView= 0; j--) { - if (j == focusedTaskIndex || j == desktopTaskIndex) { + if (j == focusedTaskIndex) { continue; } widthOffset += requireTaskViewAt(j).getLayoutParams().width + mPageSpacing; @@ -3079,7 +3019,7 @@ public abstract class RecentsView= 0; j--) { - if (j == focusedTaskIndex || j == desktopTaskIndex) { + if (j == focusedTaskIndex) { continue; } widthOffset += requireTaskViewAt(j).getLayoutParams().width + mPageSpacing; @@ -5545,10 +5485,6 @@ public abstract class RecentsView() + val frameLayout = FrameLayout.LayoutParams(100, 100) + `when`(iconView.layoutParams).thenReturn(frameLayout) + + sut.updateSplitIconsPosition(iconView, expectedTranslationY, false) + assertThat(frameLayout.gravity).isEqualTo(expectedGravity) + assertThat(frameLayout.topMargin).isEqualTo(expectedTranslationY) + verify(iconView).translationX = 0f + verify(iconView).translationY = 0f + } + + @Test + fun testIcon_updateSplitIconsPosition_isRTL() { + enableGridOnlyOverview(false) + + val expectedTranslationY = 250 + val expectedGravity = Gravity.TOP or Gravity.RIGHT + + val iconView = mock() + val frameLayout = FrameLayout.LayoutParams(100, 100) + `when`(iconView.layoutParams).thenReturn(frameLayout) + + sut.updateSplitIconsPosition(iconView, expectedTranslationY, true) + assertThat(frameLayout.gravity).isEqualTo(expectedGravity) + assertThat(frameLayout.topMargin).isEqualTo(expectedTranslationY) + verify(iconView).translationX = 0f + verify(iconView).translationY = 0f + } + + @Test + fun testChip_updateSplitIconsPosition() { + enableGridOnlyOverview(true) + + val expectedTranslationY = 250 + val frameLayout = FrameLayout.LayoutParams(100, 100) + val iconView = mock() + `when`(iconView.layoutParams).thenReturn(frameLayout) + + sut.updateSplitIconsPosition(iconView, expectedTranslationY, false) + assertThat(frameLayout.gravity).isEqualTo(Gravity.TOP or Gravity.END) + verify(iconView).setSplitTranslationX(0f) + verify(iconView).setSplitTranslationY(expectedTranslationY.toFloat()) + } + + @Test + fun testChip_updateSplitIconsPosition_isRTL() { + enableGridOnlyOverview(true) + + val expectedTranslationY = 250 + val frameLayout = FrameLayout.LayoutParams(100, 100) + val iconView = mock() + `when`(iconView.layoutParams).thenReturn(frameLayout) + + sut.updateSplitIconsPosition(iconView, expectedTranslationY, true) + assertThat(frameLayout.gravity).isEqualTo(Gravity.BOTTOM or Gravity.START) + verify(iconView).setSplitTranslationX(0f) + verify(iconView).setSplitTranslationY(expectedTranslationY.toFloat()) + } + + private companion object { + const val TASK_ICON_HEIGHT_PX = 108 + const val OVERVIEW_TASK_MARGIN_PX = 0 + const val DIVIDER_SIZE_PX = 16 + const val PRIMARY_SNAPSHOT = 250 + const val SECONDARY_SNAPSHOT = 300 + const val TOTAL_THUMBNAIL_HEIGHT = PRIMARY_SNAPSHOT + SECONDARY_SNAPSHOT + DIVIDER_SIZE_PX + } +} diff --git a/quickstep/tests/src/com/android/quickstep/orientation/SeascapePagedViewHandlerTest.kt b/quickstep/tests/src/com/android/quickstep/orientation/SeascapePagedViewHandlerTest.kt new file mode 100644 index 0000000000..2bc182c02d --- /dev/null +++ b/quickstep/tests/src/com/android/quickstep/orientation/SeascapePagedViewHandlerTest.kt @@ -0,0 +1,197 @@ +/* + * 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.quickstep.orientation + +import android.platform.test.flag.junit.SetFlagsRule +import android.view.Gravity +import android.view.View +import android.widget.FrameLayout +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.android.launcher3.Flags +import com.android.quickstep.orientation.LandscapePagedViewHandler.SplitIconPositions +import com.android.quickstep.views.IconAppChipView +import com.google.common.truth.Truth.assertThat +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mockito.`when` +import org.mockito.kotlin.mock +import org.mockito.kotlin.verify + +@RunWith(AndroidJUnit4::class) +class SeascapePagedViewHandlerTest { + + @get:Rule val setFlagsRule = SetFlagsRule() + + private val sut = SeascapePagedViewHandler() + + private fun enableGridOnlyOverview(isEnabled: Boolean) { + if (isEnabled) { + setFlagsRule.enableFlags( + Flags.FLAG_ENABLE_GRID_ONLY_OVERVIEW, + Flags.FLAG_ENABLE_OVERVIEW_ICON_MENU + ) + } else { + setFlagsRule.disableFlags( + Flags.FLAG_ENABLE_GRID_ONLY_OVERVIEW, + Flags.FLAG_ENABLE_OVERVIEW_ICON_MENU + ) + } + } + + /** [ Test getSplitIconsPosition ] */ + private fun getSplitIconsPosition(isRTL: Boolean): SplitIconPositions { + return sut.getSplitIconsPosition( + TASK_ICON_HEIGHT_PX, + PRIMARY_SNAPSHOT, + TOTAL_THUMBNAIL_HEIGHT, + isRTL, + OVERVIEW_TASK_MARGIN_PX, + DIVIDER_SIZE_PX, + ) + } + + @Test + fun testIcon_getSplitIconsPositions() { + enableGridOnlyOverview(false) + + val (topLeftY, bottomRightY) = getSplitIconsPosition(isRTL = false) + + // The top-left icon is translated from the bottom of the screen to the end of + // the primary snapshot minus the icon size. + assertThat(topLeftY).isEqualTo(142) + // The bottom-right icon is placed at the end of the primary snapshot plus the divider. + assertThat(bottomRightY).isEqualTo(266) + } + + @Test + fun testIcon_getSplitIconsPositions_isRTL() { + enableGridOnlyOverview(false) + + val (topLeftY, bottomRightY) = getSplitIconsPosition(isRTL = true) + + // The top-left icon is translated from the bottom of the screen to the end of + // the primary snapshot minus the icon size. + assertThat(topLeftY).isEqualTo(142) + // The bottom-right icon is placed at the end of the primary snapshot plus the divider. + assertThat(bottomRightY).isEqualTo(266) + } + + @Test + fun testChip_getSplitIconsPositions() { + enableGridOnlyOverview(true) + + val (topLeftY, bottomRightY) = getSplitIconsPosition(isRTL = false) + + // Top-Left app chip should always be at the initial position of the first snapshot + assertThat(topLeftY).isEqualTo(0) + // Bottom-Right app chip should be at the end of the primary height + divider + assertThat(bottomRightY).isEqualTo(-266) + } + + @Test + fun testChip_getSplitIconsPositions_isRTL() { + enableGridOnlyOverview(true) + + val (topLeftY, bottomRightY) = getSplitIconsPosition(isRTL = true) + + // TODO(b/326377497): When started in fake seascape and rotated to landscape, + // the icon chips are in RTL and wrongly positioned at the right side of the snapshot. + // Top-Left app chip should be placed at the top left of the first snapshot, but because + // this issue, it's displayed at the top-right of the second snapshot. + // The Bottom-Right app chip is displayed at the top-right of the first snapshot because + // of this issue. + assertThat(topLeftY).isEqualTo(316) + assertThat(bottomRightY).isEqualTo(0) + } + + /** Test updateSplitIconsPosition */ + @Test + fun testIcon_updateSplitIconsPosition() { + enableGridOnlyOverview(false) + + val expectedTranslationY = 250 + val expectedGravity = Gravity.BOTTOM or Gravity.LEFT + + val iconView = mock() + val frameLayout = FrameLayout.LayoutParams(100, 100) + `when`(iconView.layoutParams).thenReturn(frameLayout) + + sut.updateSplitIconsPosition(iconView, expectedTranslationY, false) + assertThat(frameLayout.gravity).isEqualTo(expectedGravity) + assertThat(frameLayout.bottomMargin).isEqualTo(expectedTranslationY) + verify(iconView).translationX = 0f + verify(iconView).translationY = 0f + } + + @Test + fun testIcon_updateSplitIconsPosition_isRTL() { + enableGridOnlyOverview(false) + + val expectedTranslationY = 250 + val expectedGravity = Gravity.BOTTOM or Gravity.LEFT + + val iconView = mock() + val frameLayout = FrameLayout.LayoutParams(100, 100) + `when`(iconView.layoutParams).thenReturn(frameLayout) + + sut.updateSplitIconsPosition(iconView, expectedTranslationY, true) + assertThat(frameLayout.gravity).isEqualTo(expectedGravity) + assertThat(frameLayout.bottomMargin).isEqualTo(expectedTranslationY) + verify(iconView).translationX = 0f + verify(iconView).translationY = 0f + } + + @Test + fun testChip_updateSplitIconsPosition() { + enableGridOnlyOverview(true) + + val expectedTranslationY = 250 + val frameLayout = FrameLayout.LayoutParams(100, 100) + val iconView = mock() + `when`(iconView.layoutParams).thenReturn(frameLayout) + + sut.updateSplitIconsPosition(iconView, expectedTranslationY, false) + assertThat(frameLayout.gravity).isEqualTo(Gravity.BOTTOM or Gravity.START) + verify(iconView).setSplitTranslationX(0f) + verify(iconView).setSplitTranslationY(expectedTranslationY.toFloat()) + } + + @Test + fun testChip_updateSplitIconsPosition_isRTL() { + enableGridOnlyOverview(true) + + val expectedTranslationY = 250 + val frameLayout = FrameLayout.LayoutParams(100, 100) + val iconView = mock() + `when`(iconView.layoutParams).thenReturn(frameLayout) + + sut.updateSplitIconsPosition(iconView, expectedTranslationY, true) + assertThat(frameLayout.gravity).isEqualTo(Gravity.TOP or Gravity.END) + verify(iconView).setSplitTranslationX(0f) + verify(iconView).setSplitTranslationY(expectedTranslationY.toFloat()) + } + + private companion object { + const val TASK_ICON_HEIGHT_PX = 108 + const val OVERVIEW_TASK_MARGIN_PX = 0 + const val DIVIDER_SIZE_PX = 16 + const val PRIMARY_SNAPSHOT = 250 + const val SECONDARY_SNAPSHOT = 300 + const val TOTAL_THUMBNAIL_HEIGHT = PRIMARY_SNAPSHOT + SECONDARY_SNAPSHOT + DIVIDER_SIZE_PX + } +} diff --git a/src/com/android/launcher3/LauncherModel.java b/src/com/android/launcher3/LauncherModel.java index 99fca62ac1..be01d633bc 100644 --- a/src/com/android/launcher3/LauncherModel.java +++ b/src/com/android/launcher3/LauncherModel.java @@ -20,6 +20,7 @@ import static android.app.admin.DevicePolicyManager.ACTION_DEVICE_POLICY_RESOURC import static com.android.launcher3.LauncherAppState.ACTION_FORCE_ROLOAD; import static com.android.launcher3.config.FeatureFlags.IS_STUDIO_BUILD; +import static com.android.launcher3.icons.cache.BaseIconCache.EMPTY_CLASS_NAME; import static com.android.launcher3.model.PackageUpdatedTask.OP_UPDATE; import static com.android.launcher3.pm.UserCache.ACTION_PROFILE_AVAILABLE; import static com.android.launcher3.pm.UserCache.ACTION_PROFILE_UNAVAILABLE; @@ -27,6 +28,7 @@ import static com.android.launcher3.testing.shared.TestProtocol.sDebugTracing; import static com.android.launcher3.util.Executors.MAIN_EXECUTOR; import static com.android.launcher3.util.Executors.MODEL_EXECUTOR; +import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.pm.PackageInstaller; @@ -70,6 +72,7 @@ import com.android.launcher3.pm.UserCache; import com.android.launcher3.shortcuts.ShortcutRequest; import com.android.launcher3.util.IntSet; import com.android.launcher3.util.ItemInfoMatcher; +import com.android.launcher3.util.PackageManagerHelper; import com.android.launcher3.util.PackageUserKey; import com.android.launcher3.util.Preconditions; @@ -443,9 +446,18 @@ public class LauncherModel implements InstallSessionTracker.Callback { @NonNull final BgDataModel dataModel, @NonNull final AllAppsList apps) { IconCache iconCache = app.getIconCache(); final IntSet removedIds = new IntSet(); - HashSet archivedItemsToCacheRefresh = new HashSet<>(); - HashSet archivedPackagesToCacheRefresh = new HashSet<>(); + HashSet archivedWorkspaceItemsToCacheRefresh = new HashSet<>(); + boolean isAppArchived = new PackageManagerHelper( + mApp.getContext()).isAppArchivedForUser(packageName, user); synchronized (dataModel) { + if (isAppArchived) { + // Remove package icon cache entry for archived app in case of a session + // failure. + mApp.getIconCache().remove( + new ComponentName(packageName, packageName + EMPTY_CLASS_NAME), + user); + } + for (ItemInfo info : dataModel.itemsIdMap) { if (info instanceof WorkspaceItemInfo && ((WorkspaceItemInfo) info).hasPromiseIconUi() @@ -456,19 +468,16 @@ public class LauncherModel implements InstallSessionTracker.Callback { } if (((WorkspaceItemInfo) info).isArchived()) { WorkspaceItemInfo workspaceItem = (WorkspaceItemInfo) info; - // Remove package cache icon for archived app in case of a session - // failure. - mApp.getIconCache().removeIconsForPkg(packageName, user); // Refresh icons on the workspace for archived apps. iconCache.getTitleAndIcon(workspaceItem, workspaceItem.usingLowResIcon()); - archivedPackagesToCacheRefresh.add(packageName); - archivedItemsToCacheRefresh.add(workspaceItem); + archivedWorkspaceItemsToCacheRefresh.add(workspaceItem); } } } - if (!archivedPackagesToCacheRefresh.isEmpty()) { - apps.updateIconsAndLabels(archivedPackagesToCacheRefresh, user); + + if (isAppArchived) { + apps.updateIconsAndLabels(new HashSet<>(List.of(packageName)), user); } } @@ -477,8 +486,11 @@ public class LauncherModel implements InstallSessionTracker.Callback { ItemInfoMatcher.ofItemIds(removedIds), "removed because install session failed"); } - if (!archivedItemsToCacheRefresh.isEmpty()) { - bindUpdatedWorkspaceItems(archivedItemsToCacheRefresh.stream().toList()); + if (!archivedWorkspaceItemsToCacheRefresh.isEmpty()) { + bindUpdatedWorkspaceItems( + archivedWorkspaceItemsToCacheRefresh.stream().toList()); + } + if (isAppArchived) { bindApplicationsIfNeeded(); } } diff --git a/src/com/android/launcher3/accessibility/LauncherAccessibilityDelegate.java b/src/com/android/launcher3/accessibility/LauncherAccessibilityDelegate.java index e861d38733..f130b89be2 100644 --- a/src/com/android/launcher3/accessibility/LauncherAccessibilityDelegate.java +++ b/src/com/android/launcher3/accessibility/LauncherAccessibilityDelegate.java @@ -405,7 +405,9 @@ public class LauncherAccessibilityDelegate extends BaseAccessibilityDelegate workspace = mContext.getWorkspace(); - workspace.snapToPage(workspace.getPageIndexForScreenId(screenId)); + workspace.post( + () -> workspace.snapToPage(workspace.getPageIndexForScreenId(screenId)) + ); mContext.addPendingItem(info, LauncherSettings.Favorites.CONTAINER_DESKTOP, screenId, coordinates, info.spanX, info.spanY); } else if (item instanceof WorkspaceItemInfo) { diff --git a/src/com/android/launcher3/allapps/AppInfoComparator.java b/src/com/android/launcher3/allapps/AppInfoComparator.java index a0867dbaf1..bbf8e5a5fb 100644 --- a/src/com/android/launcher3/allapps/AppInfoComparator.java +++ b/src/com/android/launcher3/allapps/AppInfoComparator.java @@ -18,6 +18,7 @@ package com.android.launcher3.allapps; import android.content.Context; import android.os.Process; import android.os.UserHandle; +import android.text.TextUtils; import com.android.launcher3.model.data.AppInfo; import com.android.launcher3.pm.UserCache; @@ -64,7 +65,7 @@ public class AppInfoComparator implements Comparator { } private String getSortingTitle(AppInfo info) { - if (info.appTitle != null) { + if (!TextUtils.isEmpty(info.appTitle)) { return info.appTitle.toString(); } if (info.title != null) { diff --git a/src/com/android/launcher3/config/FeatureFlags.java b/src/com/android/launcher3/config/FeatureFlags.java index b28964ab36..6d64c22547 100644 --- a/src/com/android/launcher3/config/FeatureFlags.java +++ b/src/com/android/launcher3/config/FeatureFlags.java @@ -184,11 +184,6 @@ public final class FeatureFlags { "SECONDARY_DRAG_N_DROP_TO_PIN", DISABLED, "Enable dragging and dropping to pin apps within secondary display"); - // TODO(Block 7): Clean up flags - public static final BooleanFlag ENABLE_FORCED_MONO_ICON = getDebugFlag(270396209, - "ENABLE_FORCED_MONO_ICON", DISABLED, - "Enable the ability to generate monochromatic icons, if it is not provided by the app"); - // TODO(Block 8): Clean up flags // TODO(Block 9): Clean up flags diff --git a/src/com/android/launcher3/icons/IconCache.java b/src/com/android/launcher3/icons/IconCache.java index 41e3ef026b..1633eba5e1 100644 --- a/src/com/android/launcher3/icons/IconCache.java +++ b/src/com/android/launcher3/icons/IconCache.java @@ -222,6 +222,7 @@ public class IconCache extends BaseIconCache { * Updates {@param application} only if a valid entry is found. */ public synchronized void updateTitleAndIcon(AppInfo application) { + boolean preferPackageIcon = application.isArchived(); CacheEntry entry = cacheLocked(application.componentName, application.user, () -> null, mLauncherActivityInfoCachingLogic, false, application.usingLowResIcon()); @@ -229,13 +230,12 @@ public class IconCache extends BaseIconCache { return; } - boolean preferPackageIcon = application.isArchived(); if (preferPackageIcon) { String packageName = application.getTargetPackage(); CacheEntry packageEntry = cacheLocked(new ComponentName(packageName, packageName + EMPTY_CLASS_NAME), application.user, () -> null, mLauncherActivityInfoCachingLogic, - false, application.usingLowResIcon()); + true, application.usingLowResIcon()); applyPackageEntry(packageEntry, application, entry); } else { applyCacheEntry(entry, application); diff --git a/src/com/android/launcher3/icons/LauncherIcons.java b/src/com/android/launcher3/icons/LauncherIcons.java index a15348bb57..513377ae61 100644 --- a/src/com/android/launcher3/icons/LauncherIcons.java +++ b/src/com/android/launcher3/icons/LauncherIcons.java @@ -16,14 +16,13 @@ package com.android.launcher3.icons; -import static com.android.launcher3.config.FeatureFlags.ENABLE_FORCED_MONO_ICON; - import android.content.Context; import android.graphics.drawable.Drawable; import android.os.UserHandle; import androidx.annotation.NonNull; +import com.android.launcher3.Flags; import com.android.launcher3.InvariantDeviceProfile; import com.android.launcher3.graphics.IconShape; import com.android.launcher3.graphics.LauncherPreviewRenderer; @@ -103,7 +102,7 @@ public class LauncherIcons extends BaseIconFactory implements AutoCloseable { @Override protected Drawable getMonochromeDrawable(Drawable base) { Drawable mono = super.getMonochromeDrawable(base); - if (mono != null || !ENABLE_FORCED_MONO_ICON.get()) { + if (mono != null || !Flags.forceMonochromeAppIcons()) { return mono; } if (mMonochromeIconFactory == null) {