summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--apex/jobscheduler/service/java/com/android/server/alarm/UserWakeupStore.java6
-rw-r--r--api/Android.bp17
-rw-r--r--core/java/android/service/notification/ZenModeConfig.java49
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt11
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt22
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java4
-rw-r--r--libs/WindowManager/Shell/tests/unittest/Android.bp1
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepositoryTest.kt107
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt193
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/BiometricSettingsRepositoryTest.kt24
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/data/repository/BiometricSettingsRepository.kt50
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/NotificationMediaManager.java13
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AmbientState.java17
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java12
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ScrollViewFields.kt3
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java21
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java2
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithmTest.kt29
-rw-r--r--services/core/java/com/android/server/notification/ZenModeHelper.java12
-rw-r--r--services/core/java/com/android/server/wm/AppCompatOrientationCapability.java12
-rw-r--r--services/core/java/com/android/server/wm/Transition.java5
-rw-r--r--services/core/java/com/android/server/wm/WallpaperController.java4
-rw-r--r--services/tests/uiservicestests/src/com/android/server/notification/ZenModeConfigTest.java12
-rw-r--r--services/tests/uiservicestests/src/com/android/server/notification/ZenModeDiffTest.java11
-rw-r--r--services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java95
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/AppCompatOrientationCapabilityTest.java433
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/LetterboxUiControllerTest.java187
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/TransparentPolicyTest.java66
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/utils/TestComponentStack.java110
29 files changed, 1112 insertions, 416 deletions
diff --git a/apex/jobscheduler/service/java/com/android/server/alarm/UserWakeupStore.java b/apex/jobscheduler/service/java/com/android/server/alarm/UserWakeupStore.java
index 6840877369f5..dc5e3414a819 100644
--- a/apex/jobscheduler/service/java/com/android/server/alarm/UserWakeupStore.java
+++ b/apex/jobscheduler/service/java/com/android/server/alarm/UserWakeupStore.java
@@ -206,14 +206,14 @@ public class UserWakeupStore {
* @return true if an entry is found and the list of wakeups changes.
*/
private boolean deleteWakeupFromUserStarts(int userId) {
- int index;
synchronized (mUserWakeupLock) {
- index = mUserStarts.indexOfKey(userId);
+ final int index = mUserStarts.indexOfKey(userId);
if (index >= 0) {
mUserStarts.removeAt(index);
+ return true;
}
+ return false;
}
- return index >= 0;
}
/**
diff --git a/api/Android.bp b/api/Android.bp
index 3fa9c600741e..26899eaf872d 100644
--- a/api/Android.bp
+++ b/api/Android.bp
@@ -157,6 +157,7 @@ genrule {
genrule {
name: "frameworks-base-api-system-current-compat",
srcs: [
+ ":android.api.public.latest",
":android.api.system.latest",
":android-incompatibilities.api.system.latest",
":frameworks-base-api-current.txt",
@@ -165,33 +166,35 @@ genrule {
out: ["updated-baseline.txt"],
tools: ["metalava"],
cmd: metalava_cmd +
+ "--check-compatibility:api:released $(location :android.api.public.latest) " +
"--check-compatibility:api:released $(location :android.api.system.latest) " +
- "--check-compatibility:base $(location :frameworks-base-api-current.txt) " +
"--baseline:compatibility:released $(location :android-incompatibilities.api.system.latest) " +
"--update-baseline:compatibility:released $(genDir)/updated-baseline.txt " +
+ "$(location :frameworks-base-api-current.txt) " +
"$(location :frameworks-base-api-system-current.txt)",
}
genrule {
name: "frameworks-base-api-module-lib-current-compat",
srcs: [
+ ":android.api.public.latest",
+ ":android.api.system.latest",
":android.api.module-lib.latest",
":android-incompatibilities.api.module-lib.latest",
":frameworks-base-api-current.txt",
+ ":frameworks-base-api-system-current.txt",
":frameworks-base-api-module-lib-current.txt",
],
out: ["updated-baseline.txt"],
tools: ["metalava"],
cmd: metalava_cmd +
+ "--check-compatibility:api:released $(location :android.api.public.latest) " +
+ "--check-compatibility:api:released $(location :android.api.system.latest) " +
"--check-compatibility:api:released $(location :android.api.module-lib.latest) " +
- // Note: having "public" be the base of module-lib is not perfect -- it should
- // ideally be a merged public+system (which metalava is not currently able to generate).
- // This "base" will help when migrating from MODULE_LIBS -> public, but not when
- // migrating from MODULE_LIBS -> system (where it needs to instead be listed as
- // an incompatibility).
- "--check-compatibility:base $(location :frameworks-base-api-current.txt) " +
"--baseline:compatibility:released $(location :android-incompatibilities.api.module-lib.latest) " +
"--update-baseline:compatibility:released $(genDir)/updated-baseline.txt " +
+ "$(location :frameworks-base-api-current.txt) " +
+ "$(location :frameworks-base-api-system-current.txt) " +
"$(location :frameworks-base-api-module-lib-current.txt)",
}
diff --git a/core/java/android/service/notification/ZenModeConfig.java b/core/java/android/service/notification/ZenModeConfig.java
index a3afcce5fe36..863a99adacca 100644
--- a/core/java/android/service/notification/ZenModeConfig.java
+++ b/core/java/android/service/notification/ZenModeConfig.java
@@ -262,15 +262,12 @@ public class ZenModeConfig implements Parcelable {
private static final String CONDITION_ATT_SOURCE = "source";
private static final String CONDITION_ATT_FLAGS = "flags";
- private static final String ZEN_POLICY_TAG = "zen_policy";
-
private static final String MANUAL_TAG = "manual";
private static final String AUTOMATIC_TAG = "automatic";
private static final String AUTOMATIC_DELETED_TAG = "deleted";
private static final String RULE_ATT_ID = "ruleId";
private static final String RULE_ATT_ENABLED = "enabled";
- private static final String RULE_ATT_SNOOZING = "snoozing";
private static final String RULE_ATT_NAME = "name";
private static final String RULE_ATT_PKG = "pkg";
private static final String RULE_ATT_COMPONENT = "component";
@@ -286,6 +283,7 @@ public class ZenModeConfig implements Parcelable {
private static final String RULE_ATT_ICON = "rule_icon";
private static final String RULE_ATT_TRIGGER_DESC = "triggerDesc";
private static final String RULE_ATT_DELETION_INSTANT = "deletionInstant";
+ private static final String RULE_ATT_DISABLED_ORIGIN = "disabledOrigin";
private static final String DEVICE_EFFECT_DISPLAY_GRAYSCALE = "zdeDisplayGrayscale";
private static final String DEVICE_EFFECT_SUPPRESS_AMBIENT_DISPLAY =
@@ -1170,6 +1168,10 @@ public class ZenModeConfig implements Parcelable {
if (deletionInstant != null) {
rt.deletionInstant = Instant.ofEpochMilli(deletionInstant);
}
+ if (Flags.modesUi()) {
+ rt.disabledOrigin = safeInt(parser, RULE_ATT_DISABLED_ORIGIN,
+ UPDATE_ORIGIN_UNKNOWN);
+ }
}
return rt;
}
@@ -1224,6 +1226,9 @@ public class ZenModeConfig implements Parcelable {
out.attributeLong(null, RULE_ATT_DELETION_INSTANT,
rule.deletionInstant.toEpochMilli());
}
+ if (Flags.modesUi()) {
+ out.attributeInt(null, RULE_ATT_DISABLED_ORIGIN, rule.disabledOrigin);
+ }
}
}
@@ -2514,6 +2519,8 @@ public class ZenModeConfig implements Parcelable {
@ZenPolicy.ModifiableField public int zenPolicyUserModifiedFields;
@ZenDeviceEffects.ModifiableField public int zenDeviceEffectsUserModifiedFields;
@Nullable public Instant deletionInstant; // Only set on deleted rules.
+ @FlaggedApi(Flags.FLAG_MODES_UI)
+ @ConfigChangeOrigin public int disabledOrigin = UPDATE_ORIGIN_UNKNOWN;
public ZenRule() { }
@@ -2552,6 +2559,9 @@ public class ZenModeConfig implements Parcelable {
if (source.readInt() == 1) {
deletionInstant = Instant.ofEpochMilli(source.readLong());
}
+ if (Flags.modesUi()) {
+ disabledOrigin = source.readInt();
+ }
}
}
@@ -2626,6 +2636,9 @@ public class ZenModeConfig implements Parcelable {
} else {
dest.writeInt(0);
}
+ if (Flags.modesUi()) {
+ dest.writeInt(disabledOrigin);
+ }
}
}
@@ -2671,6 +2684,9 @@ public class ZenModeConfig implements Parcelable {
if (deletionInstant != null) {
sb.append(",deletionInstant=").append(deletionInstant);
}
+ if (Flags.modesUi()) {
+ sb.append(",disabledOrigin=").append(disabledOrigin);
+ }
}
return sb.append(']').toString();
@@ -2724,7 +2740,7 @@ public class ZenModeConfig implements Parcelable {
&& other.modified == modified;
if (Flags.modesApi()) {
- return finalEquals
+ finalEquals = finalEquals
&& Objects.equals(other.zenDeviceEffects, zenDeviceEffects)
&& other.allowManualInvocation == allowManualInvocation
&& Objects.equals(other.iconResName, iconResName)
@@ -2735,6 +2751,11 @@ public class ZenModeConfig implements Parcelable {
&& other.zenDeviceEffectsUserModifiedFields
== zenDeviceEffectsUserModifiedFields
&& Objects.equals(other.deletionInstant, deletionInstant);
+
+ if (Flags.modesUi()) {
+ finalEquals = finalEquals
+ && other.disabledOrigin == disabledOrigin;
+ }
}
return finalEquals;
@@ -2743,11 +2764,21 @@ public class ZenModeConfig implements Parcelable {
@Override
public int hashCode() {
if (Flags.modesApi()) {
- return Objects.hash(enabled, snoozing, name, zenMode, conditionId, condition,
- component, configurationActivity, pkg, id, enabler, zenPolicy,
- zenDeviceEffects, modified, allowManualInvocation, iconResName,
- triggerDescription, type, userModifiedFields, zenPolicyUserModifiedFields,
- zenDeviceEffectsUserModifiedFields, deletionInstant);
+ if (Flags.modesUi()) {
+ return Objects.hash(enabled, snoozing, name, zenMode, conditionId, condition,
+ component, configurationActivity, pkg, id, enabler, zenPolicy,
+ zenDeviceEffects, modified, allowManualInvocation, iconResName,
+ triggerDescription, type, userModifiedFields,
+ zenPolicyUserModifiedFields,
+ zenDeviceEffectsUserModifiedFields, deletionInstant, disabledOrigin);
+ } else {
+ return Objects.hash(enabled, snoozing, name, zenMode, conditionId, condition,
+ component, configurationActivity, pkg, id, enabler, zenPolicy,
+ zenDeviceEffects, modified, allowManualInvocation, iconResName,
+ triggerDescription, type, userModifiedFields,
+ zenPolicyUserModifiedFields,
+ zenDeviceEffectsUserModifiedFields, deletionInstant);
+ }
}
return Objects.hash(enabled, snoozing, name, zenMode, conditionId, condition,
component, configurationActivity, pkg, id, enabler, zenPolicy, modified);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt
index 9bf244e1af1c..81891ce91e04 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt
@@ -47,7 +47,7 @@ class DesktopModeTaskRepository {
val visibleTasks: ArraySet<Int> = ArraySet(),
val minimizedTasks: ArraySet<Int> = ArraySet(),
// Tasks that are closing, but are still visible
- // TODO(b/332682201): Remove when the repository state is updated via TransitionObserver
+ // TODO(b/332682201): Remove when the repository state is updated via TransitionObserver
val closingTasks: ArraySet<Int> = ArraySet(),
// Tasks currently in freeform mode, ordered from top to bottom (top is at index 0).
val freeformTasksInZOrder: ArrayList<Int> = ArrayList(),
@@ -234,17 +234,16 @@ class DesktopModeTaskRepository {
}
/**
- * Check if a task with the given [taskId] is the only active, non-closing, not-minimized task
+ * Check if a task with the given [taskId] is the only visible, non-closing, not-minimized task
* on its display
*/
- fun isOnlyActiveNonClosingTask(taskId: Int): Boolean {
- return displayData.valueIterator().asSequence().any { data ->
- data.activeTasks
+ fun isOnlyVisibleNonClosingTask(taskId: Int): Boolean =
+ displayData.valueIterator().asSequence().any { data ->
+ data.visibleTasks
.subtract(data.closingTasks)
.subtract(data.minimizedTasks)
.singleOrNull() == taskId
}
- }
/** Get a set of the active tasks for given [displayId] */
fun getActiveTasks(displayId: Int): ArraySet<Int> {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
index d28cda33a569..14ae3a7b9b27 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
@@ -40,7 +40,6 @@ import android.view.SurfaceControl
import android.view.WindowManager.TRANSIT_CHANGE
import android.view.WindowManager.TRANSIT_NONE
import android.view.WindowManager.TRANSIT_OPEN
-import android.view.WindowManager.TRANSIT_TO_BACK
import android.view.WindowManager.TRANSIT_TO_FRONT
import android.window.RemoteTransition
import android.window.TransitionInfo
@@ -76,6 +75,7 @@ import com.android.wm.shell.recents.RecentsTransitionStateListener
import com.android.wm.shell.shared.DesktopModeStatus
import com.android.wm.shell.shared.DesktopModeStatus.DESKTOP_DENSITY_OVERRIDE
import com.android.wm.shell.shared.DesktopModeStatus.useDesktopOverrideDensity
+import com.android.wm.shell.shared.TransitionUtil
import com.android.wm.shell.shared.annotations.ExternalThread
import com.android.wm.shell.shared.annotations.ShellMainThread
import com.android.wm.shell.splitscreen.SplitScreenController
@@ -446,7 +446,7 @@ class DesktopTasksController(
* @param taskId task id of the window that's being closed
*/
fun onDesktopWindowClose(wct: WindowContainerTransaction, displayId: Int, taskId: Int) {
- if (desktopModeTaskRepository.isOnlyActiveNonClosingTask(taskId)) {
+ if (desktopModeTaskRepository.isOnlyVisibleNonClosingTask(taskId)) {
removeWallpaperActivity(wct)
}
if (!desktopModeTaskRepository.addClosingTask(displayId, taskId)) {
@@ -879,8 +879,8 @@ class DesktopTasksController(
reason = "recents animation is running"
false
}
- // Handle back navigation for the last window if wallpaper available
- shouldHandleBackNavigation(request) -> true
+ // Handle task closing for the last window if wallpaper is available
+ shouldHandleTaskClosing(request) -> true
// Only handle open or to front transitions
request.type != TRANSIT_OPEN && request.type != TRANSIT_TO_FRONT -> {
reason = "transition type not handled (${request.type})"
@@ -918,7 +918,8 @@ class DesktopTasksController(
val result =
triggerTask?.let { task ->
when {
- request.type == TRANSIT_TO_BACK -> handleBackNavigation(task)
+ // Check if the closing task needs to be handled
+ TransitionUtil.isClosingType(request.type) -> handleTaskClosing(task)
// Check if the task has a top transparent activity
shouldLaunchAsModal(task) -> handleIncompatibleTaskLaunch(task)
// Check if the task has a top systemUI activity
@@ -960,9 +961,10 @@ class DesktopTasksController(
private fun shouldLaunchAsModal(task: TaskInfo) =
Flags.enableDesktopWindowingModalsPolicy() && isSingleTopActivityTranslucent(task)
- private fun shouldHandleBackNavigation(request: TransitionRequestInfo): Boolean {
+ private fun shouldHandleTaskClosing(request: TransitionRequestInfo): Boolean {
return Flags.enableDesktopWindowingWallpaperActivity() &&
- request.type == TRANSIT_TO_BACK
+ TransitionUtil.isClosingType(request.type) &&
+ request.triggerTask != null
}
private fun handleFreeformTaskLaunch(
@@ -1029,10 +1031,10 @@ class DesktopTasksController(
return WindowContainerTransaction().also { wct -> addMoveToFullscreenChanges(wct, task) }
}
- /** Handle back navigation by removing wallpaper activity if it's the last active task */
- private fun handleBackNavigation(task: RunningTaskInfo): WindowContainerTransaction? {
+ /** Handle task closing by removing wallpaper activity if it's the last active task */
+ private fun handleTaskClosing(task: RunningTaskInfo): WindowContainerTransaction? {
val wct = if (
- desktopModeTaskRepository.isOnlyActiveNonClosingTask(task.taskId) &&
+ desktopModeTaskRepository.isOnlyVisibleNonClosingTask(task.taskId) &&
desktopModeTaskRepository.wallpaperActivityToken != null
) {
// Remove wallpaper activity when the last active task is removed
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java
index b88afb9e7197..b48aee5ccd5e 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java
@@ -122,10 +122,6 @@ public class FreeformTaskListener implements ShellTaskOrganizer.TaskListener,
mDesktopModeTaskRepository.ifPresent(repository -> {
repository.removeFreeformTask(taskInfo.displayId, taskInfo.taskId);
repository.unminimizeTask(taskInfo.displayId, taskInfo.taskId);
- if (repository.removeClosingTask(taskInfo.taskId)) {
- ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE,
- "Removing closing freeform task: #%d", taskInfo.taskId);
- }
if (repository.removeActiveTask(taskInfo.taskId)) {
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE,
"Removing active freeform task: #%d", taskInfo.taskId);
diff --git a/libs/WindowManager/Shell/tests/unittest/Android.bp b/libs/WindowManager/Shell/tests/unittest/Android.bp
index 13f95ccea640..92be4f9f0374 100644
--- a/libs/WindowManager/Shell/tests/unittest/Android.bp
+++ b/libs/WindowManager/Shell/tests/unittest/Android.bp
@@ -46,6 +46,7 @@ android_test {
"androidx.dynamicanimation_dynamicanimation",
"dagger2",
"frameworks-base-testutils",
+ "kotlin-test",
"kotlinx-coroutines-android",
"kotlinx-coroutines-core",
"mockito-kotlin2",
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepositoryTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepositoryTest.kt
index 193d61437b2b..6612aee0cd12 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepositoryTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepositoryTest.kt
@@ -119,86 +119,91 @@ class DesktopModeTaskRepositoryTest : ShellTestCase() {
}
@Test
- fun isOnlyActiveNonClosingTask_noActiveNonClosingTasks() {
- // Not an active task
- assertThat(repo.isOnlyActiveNonClosingTask(1)).isFalse()
+ fun isOnlyVisibleNonClosingTask_noTasks() {
+ // No visible tasks
+ assertThat(repo.isOnlyVisibleNonClosingTask(1)).isFalse()
assertThat(repo.isClosingTask(1)).isFalse()
}
@Test
- fun isOnlyActiveNonClosingTask_singleActiveNonClosingTask() {
- repo.addActiveTask(DEFAULT_DISPLAY, 1)
- // The only active task
- assertThat(repo.isActiveTask(1)).isTrue()
+ fun isOnlyVisibleNonClosingTask_singleVisibleNonClosingTask() {
+ repo.updateVisibleFreeformTasks(DEFAULT_DISPLAY, taskId = 1, visible = true)
+
+ // The only visible task
+ assertThat(repo.isVisibleTask(1)).isTrue()
assertThat(repo.isClosingTask(1)).isFalse()
- assertThat(repo.isOnlyActiveNonClosingTask(1)).isTrue()
- // Not an active task
- assertThat(repo.isActiveTask(99)).isFalse()
+ assertThat(repo.isOnlyVisibleNonClosingTask(1)).isTrue()
+ // Not a visible task
+ assertThat(repo.isVisibleTask(99)).isFalse()
assertThat(repo.isClosingTask(99)).isFalse()
- assertThat(repo.isOnlyActiveNonClosingTask(99)).isFalse()
+ assertThat(repo.isOnlyVisibleNonClosingTask(99)).isFalse()
}
@Test
- fun isOnlyActiveNonClosingTask_singleActiveClosingTask() {
- repo.addActiveTask(DEFAULT_DISPLAY, 1)
+ fun isOnlyVisibleNonClosingTask_singleVisibleClosingTask() {
+ repo.updateVisibleFreeformTasks(DEFAULT_DISPLAY, taskId = 1, visible = true)
repo.addClosingTask(DEFAULT_DISPLAY, 1)
- // The active task that's closing
- assertThat(repo.isActiveTask(1)).isTrue()
+
+ // A visible task that's closing
+ assertThat(repo.isVisibleTask(1)).isTrue()
assertThat(repo.isClosingTask(1)).isTrue()
- assertThat(repo.isOnlyActiveNonClosingTask(1)).isFalse()
- // Not an active task
- assertThat(repo.isActiveTask(99)).isFalse()
- assertThat(repo.isOnlyActiveNonClosingTask(99)).isFalse()
+ assertThat(repo.isOnlyVisibleNonClosingTask(1)).isFalse()
+ // Not a visible task
+ assertThat(repo.isVisibleTask(99)).isFalse()
+ assertThat(repo.isOnlyVisibleNonClosingTask(99)).isFalse()
}
@Test
- fun isOnlyActiveNonClosingTask_singleActiveMinimizedTask() {
- repo.addActiveTask(DEFAULT_DISPLAY, 1)
+ fun isOnlyVisibleNonClosingTask_singleVisibleMinimizedTask() {
+ repo.updateVisibleFreeformTasks(DEFAULT_DISPLAY, taskId = 1, visible = true)
repo.minimizeTask(DEFAULT_DISPLAY, 1)
- // The active task that's closing
- assertThat(repo.isActiveTask(1)).isTrue()
+
+ // The visible task that's closing
+ assertThat(repo.isVisibleTask(1)).isTrue()
assertThat(repo.isMinimizedTask(1)).isTrue()
- assertThat(repo.isOnlyActiveNonClosingTask(1)).isFalse()
- // Not an active task
- assertThat(repo.isActiveTask(99)).isFalse()
- assertThat(repo.isOnlyActiveNonClosingTask(99)).isFalse()
+ assertThat(repo.isOnlyVisibleNonClosingTask(1)).isFalse()
+ // Not a visible task
+ assertThat(repo.isVisibleTask(99)).isFalse()
+ assertThat(repo.isOnlyVisibleNonClosingTask(99)).isFalse()
}
@Test
- fun isOnlyActiveNonClosingTask_multipleActiveNonClosingTasks() {
- repo.addActiveTask(DEFAULT_DISPLAY, 1)
- repo.addActiveTask(DEFAULT_DISPLAY, 2)
+ fun isOnlyVisibleNonClosingTask_multipleVisibleNonClosingTasks() {
+ repo.updateVisibleFreeformTasks(DEFAULT_DISPLAY, taskId = 1, visible = true)
+ repo.updateVisibleFreeformTasks(DEFAULT_DISPLAY, taskId = 2, visible = true)
+
// Not the only task
- assertThat(repo.isActiveTask(1)).isTrue()
+ assertThat(repo.isVisibleTask(1)).isTrue()
assertThat(repo.isClosingTask(1)).isFalse()
- assertThat(repo.isOnlyActiveNonClosingTask(1)).isFalse()
+ assertThat(repo.isOnlyVisibleNonClosingTask(1)).isFalse()
// Not the only task
- assertThat(repo.isActiveTask(2)).isTrue()
+ assertThat(repo.isVisibleTask(2)).isTrue()
assertThat(repo.isClosingTask(2)).isFalse()
- assertThat(repo.isOnlyActiveNonClosingTask(2)).isFalse()
- // Not an active task
- assertThat(repo.isActiveTask(99)).isFalse()
+ assertThat(repo.isOnlyVisibleNonClosingTask(2)).isFalse()
+ // Not a visible task
+ assertThat(repo.isVisibleTask(99)).isFalse()
assertThat(repo.isClosingTask(99)).isFalse()
- assertThat(repo.isOnlyActiveNonClosingTask(99)).isFalse()
+ assertThat(repo.isOnlyVisibleNonClosingTask(99)).isFalse()
}
@Test
- fun isOnlyActiveNonClosingTask_multipleDisplays() {
- repo.addActiveTask(DEFAULT_DISPLAY, 1)
- repo.addActiveTask(DEFAULT_DISPLAY, 2)
- repo.addActiveTask(SECOND_DISPLAY, 3)
+ fun isOnlyVisibleNonClosingTask_multipleDisplays() {
+ repo.updateVisibleFreeformTasks(DEFAULT_DISPLAY, taskId = 1, visible = true)
+ repo.updateVisibleFreeformTasks(DEFAULT_DISPLAY, taskId = 2, visible = true)
+ repo.updateVisibleFreeformTasks(SECOND_DISPLAY, taskId = 3, visible = true)
+
// Not the only task on DEFAULT_DISPLAY
- assertThat(repo.isActiveTask(1)).isTrue()
- assertThat(repo.isOnlyActiveNonClosingTask(1)).isFalse()
+ assertThat(repo.isVisibleTask(1)).isTrue()
+ assertThat(repo.isOnlyVisibleNonClosingTask(1)).isFalse()
// Not the only task on DEFAULT_DISPLAY
- assertThat(repo.isActiveTask(2)).isTrue()
- assertThat(repo.isOnlyActiveNonClosingTask(2)).isFalse()
- // The only active task on SECOND_DISPLAY
- assertThat(repo.isActiveTask(3)).isTrue()
- assertThat(repo.isOnlyActiveNonClosingTask(3)).isTrue()
- // Not an active task
- assertThat(repo.isActiveTask(99)).isFalse()
- assertThat(repo.isOnlyActiveNonClosingTask(99)).isFalse()
+ assertThat(repo.isVisibleTask(2)).isTrue()
+ assertThat(repo.isOnlyVisibleNonClosingTask(2)).isFalse()
+ // The only visible task on SECOND_DISPLAY
+ assertThat(repo.isVisibleTask(3)).isTrue()
+ assertThat(repo.isOnlyVisibleNonClosingTask(3)).isTrue()
+ // Not a visible task
+ assertThat(repo.isVisibleTask(99)).isFalse()
+ assertThat(repo.isOnlyVisibleNonClosingTask(99)).isFalse()
}
@Test
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt
index 392161fd8e66..a1a18a9b7c9d 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt
@@ -45,6 +45,7 @@ import android.view.Display.DEFAULT_DISPLAY
import android.view.SurfaceControl
import android.view.WindowManager
import android.view.WindowManager.TRANSIT_CHANGE
+import android.view.WindowManager.TRANSIT_CLOSE
import android.view.WindowManager.TRANSIT_OPEN
import android.view.WindowManager.TRANSIT_TO_BACK
import android.view.WindowManager.TRANSIT_TO_FRONT
@@ -102,6 +103,8 @@ import com.google.common.truth.Truth.assertWithMessage
import java.util.Optional
import junit.framework.Assert.assertFalse
import junit.framework.Assert.assertTrue
+import kotlin.test.assertNotNull
+import kotlin.test.assertNull
import org.junit.After
import org.junit.Assume.assumeTrue
import org.junit.Before
@@ -1254,72 +1257,205 @@ class DesktopTasksControllerTest : ShellTestCase() {
}
@Test
+ @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
+ fun handleRequest_backTransition_singleActiveTaskNoTokenFlagDisabled_doesNotHandle() {
+ val task = setUpFreeformTask()
+
+ val result = controller.handleRequest(Binder(), createTransition(task, type = TRANSIT_TO_BACK))
+
+ assertNull(result, "Should not handle request")
+ }
+
+ @Test
@EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
- fun handleRequest_backTransition_singleActiveTask_noToken() {
+ fun handleRequest_backTransition_singleActiveTaskNoTokenFlagEnabled_doesNotHandle() {
val task = setUpFreeformTask()
+
val result = controller.handleRequest(Binder(), createTransition(task, type = TRANSIT_TO_BACK))
- // Doesn't handle request
- assertThat(result).isNull()
+
+ assertNull(result, "Should not handle request")
}
@Test
@DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
- fun handleRequest_backTransition_singleActiveTask_hasToken_desktopWallpaperDisabled() {
- desktopModeTaskRepository.wallpaperActivityToken = MockToken().token()
-
+ fun handleRequest_backTransition_singleActiveTaskWithTokenFlagDisabled_doesNotHandle() {
val task = setUpFreeformTask()
+
+ desktopModeTaskRepository.wallpaperActivityToken = MockToken().token()
val result = controller.handleRequest(Binder(), createTransition(task, type = TRANSIT_TO_BACK))
- // Doesn't handle request
- assertThat(result).isNull()
+
+ assertNull(result, "Should not handle request")
}
@Test
@EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
- fun handleRequest_backTransition_singleActiveTask_hasToken_desktopWallpaperEnabled() {
+ fun handleRequest_backTransition_singleActiveTaskWithTokenFlagEnabled_handlesRequest() {
+ val task = setUpFreeformTask()
val wallpaperToken = MockToken().token()
- desktopModeTaskRepository.wallpaperActivityToken = wallpaperToken
- val task = setUpFreeformTask()
+ desktopModeTaskRepository.wallpaperActivityToken = wallpaperToken
val result = controller.handleRequest(Binder(), createTransition(task, type = TRANSIT_TO_BACK))
- assertThat(result).isNotNull()
- // Creates remove wallpaper transaction
- result!!.assertRemoveAt(index = 0, wallpaperToken)
+
+ assertNotNull(result, "Should handle request")
+ // Should create remove wallpaper transaction
+ .assertRemoveAt(index = 0, wallpaperToken)
}
@Test
- @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
- fun handleRequest_backTransition_multipleActiveTasks() {
+ @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
+ fun handleRequest_backTransition_multipleActiveTasksFlagDisabled_doesNotHandle() {
+ val task1 = setUpFreeformTask()
+ setUpFreeformTask()
+
desktopModeTaskRepository.wallpaperActivityToken = MockToken().token()
+ val result = controller.handleRequest(Binder(), createTransition(task1, type = TRANSIT_TO_BACK))
+ assertNull(result, "Should not handle request")
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
+ fun handleRequest_backTransition_multipleActiveTasksFlagEnabled_doesNotHandle() {
val task1 = setUpFreeformTask()
setUpFreeformTask()
+
+ desktopModeTaskRepository.wallpaperActivityToken = MockToken().token()
val result = controller.handleRequest(Binder(), createTransition(task1, type = TRANSIT_TO_BACK))
- // Doesn't handle request
- assertThat(result).isNull()
+
+ assertNull(result, "Should not handle request")
}
@Test
@EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
- fun handleRequest_backTransition_multipleActiveTasks_singleNonClosing() {
- desktopModeTaskRepository.wallpaperActivityToken = MockToken().token()
+ fun handleRequest_backTransition_multipleActiveTasksSingleNonClosing_handlesRequest() {
+ val task1 = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
+ val task2 = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
+ val wallpaperToken = MockToken().token()
- val task1 = setUpFreeformTask()
- setUpFreeformTask()
+ desktopModeTaskRepository.wallpaperActivityToken = wallpaperToken
+ desktopModeTaskRepository.addClosingTask(displayId = DEFAULT_DISPLAY, taskId = task2.taskId)
val result = controller.handleRequest(Binder(), createTransition(task1, type = TRANSIT_TO_BACK))
- // Doesn't handle request
- assertThat(result).isNull()
+
+ assertNotNull(result, "Should handle request")
+ // Should create remove wallpaper transaction
+ .assertRemoveAt(index = 0, wallpaperToken)
}
@Test
@EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
- fun handleRequest_backTransition_multipleActiveTasks_singleNonMinimized() {
+ fun handleRequest_backTransition_multipleActiveTasksSingleNonMinimized_handlesRequest() {
+ val task1 = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
+ val task2 = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
+ val wallpaperToken = MockToken().token()
+
+ desktopModeTaskRepository.wallpaperActivityToken = wallpaperToken
+ desktopModeTaskRepository.minimizeTask(displayId = DEFAULT_DISPLAY, taskId = task2.taskId)
+ val result = controller.handleRequest(Binder(), createTransition(task1, type = TRANSIT_TO_BACK))
+
+ assertNotNull(result, "Should handle request")
+ // Should create remove wallpaper transaction
+ .assertRemoveAt(index = 0, wallpaperToken)
+ }
+
+ @Test
+ @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
+ fun handleRequest_closeTransition_singleActiveTaskNoTokenFlagDisabled_doesNotHandle() {
+ val task = setUpFreeformTask()
+
+ val result = controller.handleRequest(Binder(), createTransition(task, type = TRANSIT_CLOSE))
+
+ assertNull(result, "Should not handle request")
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
+ fun handleRequest_closeTransition_singleActiveTaskNoTokenFlagEnabled_doesNotHandle() {
+ val task = setUpFreeformTask()
+
+ val result = controller.handleRequest(Binder(), createTransition(task, type = TRANSIT_CLOSE))
+
+ assertNull(result, "Should not handle request")
+ }
+
+ @Test
+ @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
+ fun handleRequest_closeTransition_singleActiveTaskWithTokenFlagDisabled_doesNotHandle() {
+ val task = setUpFreeformTask()
+
desktopModeTaskRepository.wallpaperActivityToken = MockToken().token()
+ val result = controller.handleRequest(Binder(), createTransition(task, type = TRANSIT_CLOSE))
+
+ assertNull(result, "Should not handle request")
+ }
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
+ fun handleRequest_closeTransition_singleActiveTaskWithTokenFlagEnabled_handlesRequest() {
+ val task = setUpFreeformTask()
+ val wallpaperToken = MockToken().token()
+
+ desktopModeTaskRepository.wallpaperActivityToken = wallpaperToken
+ val result = controller.handleRequest(Binder(), createTransition(task, type = TRANSIT_CLOSE))
+
+ assertNotNull(result, "Should handle request")
+ // Should create remove wallpaper transaction
+ .assertRemoveAt(index = 0, wallpaperToken)
+ }
+
+ @Test
+ @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
+ fun handleRequest_closeTransition_multipleActiveTasksFlagDisabled_doesNotHandle() {
val task1 = setUpFreeformTask()
setUpFreeformTask()
- val result = controller.handleRequest(Binder(), createTransition(task1, type = TRANSIT_TO_BACK))
- // Doesn't handle request
- assertThat(result).isNull()
+
+ desktopModeTaskRepository.wallpaperActivityToken = MockToken().token()
+ val result = controller.handleRequest(Binder(), createTransition(task1, type = TRANSIT_CLOSE))
+
+ assertNull(result, "Should not handle request")
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
+ fun handleRequest_closeTransition_multipleActiveTasksFlagEnabled_doesNotHandle() {
+ val task1 = setUpFreeformTask()
+ setUpFreeformTask()
+
+ desktopModeTaskRepository.wallpaperActivityToken = MockToken().token()
+ val result = controller.handleRequest(Binder(), createTransition(task1, type = TRANSIT_CLOSE))
+
+ assertNull(result, "Should not handle request")
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
+ fun handleRequest_closeTransition_multipleActiveTasksSingleNonClosing_handlesRequest() {
+ val task1 = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
+ val task2 = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
+ val wallpaperToken = MockToken().token()
+
+ desktopModeTaskRepository.wallpaperActivityToken = wallpaperToken
+ desktopModeTaskRepository.addClosingTask(displayId = DEFAULT_DISPLAY, taskId = task2.taskId)
+ val result = controller.handleRequest(Binder(), createTransition(task1, type = TRANSIT_CLOSE))
+
+ assertNotNull(result, "Should handle request")
+ // Should create remove wallpaper transaction
+ .assertRemoveAt(index = 0, wallpaperToken)
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
+ fun handleRequest_closeTransition_multipleActiveTasksSingleNonMinimized_handlesRequest() {
+ val task1 = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
+ val task2 = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
+ val wallpaperToken = MockToken().token()
+
+ desktopModeTaskRepository.wallpaperActivityToken = wallpaperToken
+ desktopModeTaskRepository.minimizeTask(displayId = DEFAULT_DISPLAY, taskId = task2.taskId)
+ val result = controller.handleRequest(Binder(), createTransition(task1, type = TRANSIT_CLOSE))
+
+ assertNotNull(result, "Should handle request")
+ // Should create remove wallpaper transaction
+ .assertRemoveAt(index = 0, wallpaperToken)
}
@Test
@@ -1693,6 +1829,7 @@ class DesktopTasksControllerTest : ShellTestCase() {
task.topActivityInfo = activityInfo
whenever(shellTaskOrganizer.getRunningTaskInfo(task.taskId)).thenReturn(task)
desktopModeTaskRepository.addActiveTask(displayId, task.taskId)
+ desktopModeTaskRepository.updateVisibleFreeformTasks(displayId, task.taskId, visible = true)
desktopModeTaskRepository.addOrMoveFreeformTaskToTop(displayId, task.taskId)
runningTasks.add(task)
return task
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/BiometricSettingsRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/BiometricSettingsRepositoryTest.kt
index 4587ea6dbdc8..c5ba02d0773a 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/BiometricSettingsRepositoryTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/BiometricSettingsRepositoryTest.kt
@@ -528,6 +528,30 @@ class BiometricSettingsRepositoryTest : SysuiTestCase() {
}
@Test
+ fun userChange_isFingerprintEnrolledAndEnabledUpdated() =
+ testScope.runTest {
+ createBiometricSettingsRepository()
+ whenever(authController.isFingerprintEnrolled(ANOTHER_USER_ID)).thenReturn(false)
+ whenever(authController.isFingerprintEnrolled(PRIMARY_USER_ID)).thenReturn(true)
+
+ verify(biometricManager)
+ .registerEnabledOnKeyguardCallback(biometricManagerCallback.capture())
+ val isFingerprintEnrolledAndEnabled =
+ collectLastValue(underTest.isFingerprintEnrolledAndEnabled)
+ biometricManagerCallback.value.onChanged(true, ANOTHER_USER_ID)
+ runCurrent()
+ userRepository.setSelectedUserInfo(ANOTHER_USER)
+ runCurrent()
+ assertThat(isFingerprintEnrolledAndEnabled()).isFalse()
+
+ biometricManagerCallback.value.onChanged(true, PRIMARY_USER_ID)
+ runCurrent()
+ userRepository.setSelectedUserInfo(PRIMARY_USER)
+ runCurrent()
+ assertThat(isFingerprintEnrolledAndEnabled()).isTrue()
+ }
+
+ @Test
fun userChange_biometricEnabledChange_handlesRaceCondition() =
testScope.runTest {
createBiometricSettingsRepository()
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/BiometricSettingsRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/BiometricSettingsRepository.kt
index 882f2315f6e8..dd3e6190ceaf 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/BiometricSettingsRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/BiometricSettingsRepository.kt
@@ -188,35 +188,33 @@ constructor(
)
private val isFingerprintEnrolled: Flow<Boolean> =
- selectedUserId
- .flatMapLatest { currentUserId ->
- conflatedCallbackFlow {
- val callback =
- object : AuthController.Callback {
- override fun onEnrollmentsChanged(
- sensorBiometricType: BiometricType,
- userId: Int,
- hasEnrollments: Boolean
- ) {
- if (sensorBiometricType.isFingerprint && userId == currentUserId) {
- trySendWithFailureLogging(
- hasEnrollments,
- TAG,
- "update fpEnrollment"
- )
- }
+ selectedUserId.flatMapLatest { currentUserId ->
+ conflatedCallbackFlow {
+ val callback =
+ object : AuthController.Callback {
+ override fun onEnrollmentsChanged(
+ sensorBiometricType: BiometricType,
+ userId: Int,
+ hasEnrollments: Boolean
+ ) {
+ if (sensorBiometricType.isFingerprint && userId == currentUserId) {
+ trySendWithFailureLogging(
+ hasEnrollments,
+ TAG,
+ "update fpEnrollment"
+ )
}
}
- authController.addCallback(callback)
- awaitClose { authController.removeCallback(callback) }
- }
+ }
+ authController.addCallback(callback)
+ trySendWithFailureLogging(
+ authController.isFingerprintEnrolled(currentUserId),
+ TAG,
+ "Initial value of fingerprint enrollment"
+ )
+ awaitClose { authController.removeCallback(callback) }
}
- .stateIn(
- scope,
- started = SharingStarted.Eagerly,
- initialValue =
- authController.isFingerprintEnrolled(userRepository.getSelectedUserInfo().id)
- )
+ }
private val isFaceEnrolled: Flow<Boolean> =
selectedUserId.flatMapLatest { selectedUserId: Int ->
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMediaManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMediaManager.java
index 04a413acdf0b..240953d6bfcd 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMediaManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMediaManager.java
@@ -291,8 +291,9 @@ public class NotificationMediaManager implements Dumpable {
}
private void updateMediaMetaData(MediaListener callback) {
- callback.onPrimaryMetadataOrStateChanged(mMediaMetadata,
- getMediaControllerPlaybackState(mMediaController));
+ int playbackState = getMediaControllerPlaybackState(mMediaController);
+ mHandler.post(
+ () -> callback.onPrimaryMetadataOrStateChanged(mMediaMetadata, playbackState));
}
public void removeCallback(MediaListener callback) {
@@ -437,9 +438,11 @@ public class NotificationMediaManager implements Dumpable {
private void updateMediaMetaData(List<MediaListener> callbacks) {
@PlaybackState.State int state = getMediaControllerPlaybackState(mMediaController);
- for (int i = 0; i < callbacks.size(); i++) {
- callbacks.get(i).onPrimaryMetadataOrStateChanged(mMediaMetadata, state);
- }
+ mHandler.post(() -> {
+ for (int i = 0; i < callbacks.size(); i++) {
+ callbacks.get(i).onPrimaryMetadataOrStateChanged(mMediaMetadata, state);
+ }
+ });
}
@Override
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AmbientState.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AmbientState.java
index 456c321a0c14..2cbb6aea53a4 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AmbientState.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AmbientState.java
@@ -64,6 +64,7 @@ public class AmbientState implements Dumpable {
* Used to read bouncer states.
*/
private StatusBarKeyguardViewManager mStatusBarKeyguardViewManager;
+ private float mStackTop;
private float mStackCutoff;
private int mScrollY;
private float mOverScrollTopAmount;
@@ -186,6 +187,7 @@ public class AmbientState implements Dumpable {
* @param stackY Distance of top of notifications panel from top of screen.
*/
public void setStackY(float stackY) {
+ SceneContainerFlag.assertInLegacyMode();
mStackY = stackY;
}
@@ -193,6 +195,7 @@ public class AmbientState implements Dumpable {
* @return Distance of top of notifications panel from top of screen.
*/
public float getStackY() {
+ SceneContainerFlag.assertInLegacyMode();
return mStackY;
}
@@ -348,6 +351,18 @@ public class AmbientState implements Dumpable {
return mZDistanceBetweenElements;
}
+ /** Y coordinate in view pixels of the top of the notification stack */
+ public float getStackTop() {
+ if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) return 0f;
+ return mStackTop;
+ }
+
+ /** @see #getStackTop() */
+ public void setStackTop(float mStackTop) {
+ if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) return;
+ this.mStackTop = mStackTop;
+ }
+
/**
* Y coordinate in view pixels above which the bottom of the notification stack / shelf / footer
* must be.
@@ -769,6 +784,8 @@ public class AmbientState implements Dumpable {
@Override
public void dump(PrintWriter pw, String[] args) {
+ pw.println("mStackTop=" + mStackTop);
+ pw.println("mStackCutoff" + mStackCutoff);
pw.println("mTopPadding=" + mTopPadding);
pw.println("mStackTopMargin=" + mStackTopMargin);
pw.println("mStackTranslation=" + mStackTranslation);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
index 38bcc0b98e59..1eee466f0d56 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
@@ -834,7 +834,7 @@ public class NotificationStackScrollLayout
drawDebugInfo(canvas, y, Color.RED, /* label= */ "y = " + y);
if (SceneContainerFlag.isEnabled()) {
- y = (int) mScrollViewFields.getStackTop();
+ y = (int) mAmbientState.getStackTop();
drawDebugInfo(canvas, y, Color.RED, /* label= */ "getStackTop() = " + y);
y = (int) mAmbientState.getStackCutoff();
@@ -1216,7 +1216,7 @@ public class NotificationStackScrollLayout
@Override
public void setStackTop(float stackTop) {
- mScrollViewFields.setStackTop(stackTop);
+ mAmbientState.setStackTop(stackTop);
// TODO(b/332574413): replace the following with using stackTop
updateTopPadding(stackTop, isAddOrRemoveAnimationPending());
}
@@ -1426,11 +1426,7 @@ public class NotificationStackScrollLayout
if (mAmbientState.isBouncerInTransit() && mQsExpansionFraction > 0f) {
fraction = BouncerPanelExpansionCalculator.aboutToShowBouncerProgress(fraction);
}
- // TODO(b/322228881): Clean up scene container vs legacy behavior in NSSL
- if (SceneContainerFlag.isEnabled()) {
- // stackY should be driven by scene container, not NSSL
- mAmbientState.setStackY(getTopPadding());
- } else {
+ if (!SceneContainerFlag.isEnabled()) {
final float stackY = MathUtils.lerp(0, endTopPosition, fraction);
mAmbientState.setStackY(stackY);
}
@@ -3726,7 +3722,7 @@ public class NotificationStackScrollLayout
protected boolean isInsideQsHeader(MotionEvent ev) {
if (SceneContainerFlag.isEnabled()) {
- return ev.getY() < mScrollViewFields.getStackTop();
+ return ev.getY() < mAmbientState.getStackTop();
}
mQsHeader.getBoundsOnScreen(mQsHeaderBound);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ScrollViewFields.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ScrollViewFields.kt
index 2e86ad97a5f4..97ec39192cc8 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ScrollViewFields.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ScrollViewFields.kt
@@ -32,8 +32,6 @@ import java.util.function.Consumer
class ScrollViewFields {
/** Used to produce the clipping path */
var scrimClippingShape: ShadeScrimShape? = null
- /** Y coordinate in view pixels of the top of the notification stack */
- var stackTop: Float = 0f
/** Y coordinate in view pixels of the top of the HUN */
var headsUpTop: Float = 0f
/** Whether the notifications are scrolled all the way to the top (i.e. when freshly opened) */
@@ -76,7 +74,6 @@ class ScrollViewFields {
fun dump(pw: IndentingPrintWriter) {
pw.printSection("StackViewStates") {
pw.println("scrimClippingShape", scrimClippingShape)
- pw.println("stackTop", stackTop)
pw.println("headsUpTop", headsUpTop)
pw.println("isScrolledToTop", isScrolledToTop)
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java
index f9efc07c67fb..ca74c0e6f9e1 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java
@@ -29,6 +29,7 @@ import com.android.internal.policy.SystemBarUtils;
import com.android.keyguard.BouncerPanelExpansionCalculator;
import com.android.systemui.animation.ShadeInterpolation;
import com.android.systemui.res.R;
+import com.android.systemui.scene.shared.flag.SceneContainerFlag;
import com.android.systemui.shade.transition.LargeScreenShadeInterpolator;
import com.android.systemui.statusbar.EmptyShadeView;
import com.android.systemui.statusbar.NotificationShelf;
@@ -332,8 +333,10 @@ public class StackScrollAlgorithm {
private void updateClipping(StackScrollAlgorithmState algorithmState,
AmbientState ambientState) {
- float drawStart = ambientState.isOnKeyguard() ? 0
+ float stackTop = SceneContainerFlag.isEnabled() ? ambientState.getStackTop()
: ambientState.getStackY() - ambientState.getScrollY();
+ float drawStart = ambientState.isOnKeyguard() ? 0
+ : stackTop;
float clipStart = 0;
int childCount = algorithmState.visibleChildren.size();
boolean firstHeadsUp = true;
@@ -641,7 +644,10 @@ public class StackScrollAlgorithm {
// Incoming views have yTranslation=0 by default.
viewState.setYTranslation(algorithmState.mCurrentYPosition);
- float viewEnd = viewState.getYTranslation() + viewState.height + ambientState.getStackY();
+ float stackTop = SceneContainerFlag.isEnabled()
+ ? ambientState.getStackTop()
+ : ambientState.getStackY();
+ float viewEnd = stackTop + viewState.getYTranslation() + viewState.height;
maybeUpdateHeadsUpIsVisible(viewState, ambientState.isShadeExpanded(),
view.mustStayOnScreen(),
/* topVisible= */ viewState.getYTranslation() >= mNotificationScrimPadding,
@@ -681,7 +687,9 @@ public class StackScrollAlgorithm {
}
} else {
if (view instanceof EmptyShadeView) {
- float fullHeight = ambientState.getLayoutMaxHeight() + mMarginBottom
+ float fullHeight = SceneContainerFlag.isEnabled()
+ ? ambientState.getStackCutoff() - ambientState.getStackTop()
+ : ambientState.getLayoutMaxHeight() + mMarginBottom
- ambientState.getStackY();
viewState.setYTranslation((fullHeight - getMaxAllowedChildHeight(view)) / 2f);
} else if (view != ambientState.getTrackedHeadsUpRow()) {
@@ -726,7 +734,7 @@ public class StackScrollAlgorithm {
+ mPaddingBetweenElements;
setLocation(view.getViewState(), algorithmState.mCurrentYPosition, i);
- viewState.setYTranslation(viewState.getYTranslation() + ambientState.getStackY());
+ viewState.setYTranslation(viewState.getYTranslation() + stackTop);
}
@VisibleForTesting
@@ -1002,8 +1010,11 @@ public class StackScrollAlgorithm {
// Animate pinned HUN bottom corners to and from original roundness.
final float originalCornerRadius =
row.isLastInSection() ? 1f : (mSmallCornerRadius / mLargeCornerRadius);
+ final float stackTop = SceneContainerFlag.isEnabled()
+ ? ambientState.getStackTop()
+ : ambientState.getStackY();
final float bottomValue = computeCornerRoundnessForPinnedHun(mHostView.getHeight(),
- ambientState.getStackY(), getMaxAllowedChildHeight(row), originalCornerRadius);
+ stackTop, getMaxAllowedChildHeight(row), originalCornerRadius);
row.requestBottomRoundness(bottomValue, STACK_SCROLL_ALGO);
row.addOnDetachResetRoundness(STACK_SCROLL_ALGO);
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java
index 12f3ef3cf553..967f95eb0ef6 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java
@@ -214,6 +214,7 @@ public class NotificationStackScrollLayoutTest extends SysuiTestCase {
}
@Test
+ @DisableSceneContainer // TODO(b/332574413) cover stack bounds integration with tests
public void testUpdateStackHeight_qsExpansionGreaterThanZero() {
final float expansionFraction = 0.2f;
final float overExpansion = 50f;
@@ -891,6 +892,7 @@ public class NotificationStackScrollLayoutTest extends SysuiTestCase {
}
@Test
+ @DisableSceneContainer // NSSL has no more scroll logic when SceneContainer is on
public void testNormalShade_hasNoTopOverscroll() {
mTestableResources
.addOverride(R.bool.config_use_split_notification_shade, /* value= */ false);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithmTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithmTest.kt
index d28e0c17c5c9..8401a19f075d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithmTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithmTest.kt
@@ -11,6 +11,8 @@ import com.android.keyguard.BouncerPanelExpansionCalculator.aboutToShowBouncerPr
import com.android.systemui.SysuiTestCase
import com.android.systemui.animation.ShadeInterpolation.getContentAlpha
import com.android.systemui.dump.DumpManager
+import com.android.systemui.flags.DisableSceneContainer
+import com.android.systemui.flags.EnableSceneContainer
import com.android.systemui.flags.FeatureFlags
import com.android.systemui.flags.FeatureFlagsClassic
import com.android.systemui.res.R
@@ -66,7 +68,7 @@ class StackScrollAlgorithmTest : SysuiTestCase() {
EmptyShadeView(context, /* attrs= */ null).apply {
layout(/* l= */ 0, /* t= */ 0, /* r= */ 100, /* b= */ 100)
}
- private val footerView = FooterView(context, /*attrs=*/ null)
+ private val footerView = FooterView(context, /* attrs= */ null)
@OptIn(ExperimentalCoroutinesApi::class)
private val ambientState =
AmbientState(
@@ -126,6 +128,7 @@ class StackScrollAlgorithmTest : SysuiTestCase() {
}
@Test
+ @DisableSceneContainer // TODO(b/332574413) cover hun bounds integration with tests
fun resetViewStates_defaultHunWhenShadeIsOpening_yTranslationIsInset() {
whenever(notificationRow.isPinned).thenReturn(true)
whenever(notificationRow.isHeadsUp).thenReturn(true)
@@ -168,6 +171,7 @@ class StackScrollAlgorithmTest : SysuiTestCase() {
}
@Test
+ @DisableSceneContainer // TODO(b/332574413) cover hun bounds integration with tests
@EnableFlags(NotificationsImprovedHunAnimation.FLAG_NAME)
fun resetViewStates_defaultHun_showingQS_newHeadsUpAnim_hunTranslatedToMax() {
// Given: the shade is open and scrolled to the bottom to show the QuickSettings
@@ -184,6 +188,7 @@ class StackScrollAlgorithmTest : SysuiTestCase() {
}
@Test
+ @DisableSceneContainer // TODO(b/332574413) cover hun bounds integration with tests
@EnableFlags(NotificationsImprovedHunAnimation.FLAG_NAME)
fun resetViewStates_hunAnimatingAway_showingQS_newHeadsUpAnim_hunTranslatedToBottomOfScreen() {
// Given: the shade is open and scrolled to the bottom to show the QuickSettings
@@ -272,6 +277,27 @@ class StackScrollAlgorithmTest : SysuiTestCase() {
}
@Test
+ @EnableSceneContainer
+ fun resetViewStates_emptyShadeView_isCenteredVertically_withSceneContainer() {
+ stackScrollAlgorithm.initView(context)
+ hostView.removeAllViews()
+ hostView.addView(emptyShadeView)
+ ambientState.layoutMaxHeight = maxPanelHeight.toInt()
+
+ val stackTop = 200f
+ val stackBottom = 2000f
+ val stackHeight = stackBottom - stackTop
+ ambientState.stackTop = stackTop
+ ambientState.stackCutoff = stackBottom
+
+ stackScrollAlgorithm.resetViewStates(ambientState, /* speedBumpIndex= */ 0)
+
+ val centeredY = stackTop + stackHeight / 2f - emptyShadeView.height / 2f
+ assertThat(emptyShadeView.viewState.yTranslation).isEqualTo(centeredY)
+ }
+
+ @Test
+ @DisableSceneContainer
fun resetViewStates_emptyShadeView_isCenteredVertically() {
stackScrollAlgorithm.initView(context)
hostView.removeAllViews()
@@ -1157,6 +1183,7 @@ class StackScrollAlgorithmTest : SysuiTestCase() {
assertFalse(stackScrollAlgorithm.shouldHunAppearFromBottom(ambientState, viewState))
}
+
// endregion
private fun createHunViewMock(
diff --git a/services/core/java/com/android/server/notification/ZenModeHelper.java b/services/core/java/com/android/server/notification/ZenModeHelper.java
index 2e7295eca1a2..c078409f468c 100644
--- a/services/core/java/com/android/server/notification/ZenModeHelper.java
+++ b/services/core/java/com/android/server/notification/ZenModeHelper.java
@@ -34,6 +34,7 @@ import static android.service.notification.ZenModeConfig.UPDATE_ORIGIN_INIT;
import static android.service.notification.ZenModeConfig.UPDATE_ORIGIN_INIT_USER;
import static android.service.notification.ZenModeConfig.UPDATE_ORIGIN_RESTORE_BACKUP;
import static android.service.notification.ZenModeConfig.UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI;
+import static android.service.notification.ZenModeConfig.UPDATE_ORIGIN_UNKNOWN;
import static android.service.notification.ZenModeConfig.UPDATE_ORIGIN_USER;
import static com.android.internal.util.FrameworkStatsLog.DND_MODE_RULE;
@@ -1143,6 +1144,17 @@ public class ZenModeHelper {
modified = true;
}
+ if (Flags.modesUi()) {
+ if (!azr.isEnabled() && (isNew || rule.enabled)) {
+ // Creating a rule as disabled, or disabling a previously enabled rule.
+ // Record whodunit.
+ rule.disabledOrigin = origin;
+ } else if (azr.isEnabled()) {
+ // Enabling or previously enabled. Clear disabler.
+ rule.disabledOrigin = UPDATE_ORIGIN_UNKNOWN;
+ }
+ }
+
if (!Objects.equals(rule.conditionId, azr.getConditionId())) {
rule.conditionId = azr.getConditionId();
modified = true;
diff --git a/services/core/java/com/android/server/wm/AppCompatOrientationCapability.java b/services/core/java/com/android/server/wm/AppCompatOrientationCapability.java
index fbe90a2cecda..10f3e833f78a 100644
--- a/services/core/java/com/android/server/wm/AppCompatOrientationCapability.java
+++ b/services/core/java/com/android/server/wm/AppCompatOrientationCapability.java
@@ -38,6 +38,7 @@ import com.android.internal.annotations.VisibleForTesting;
import com.android.server.wm.utils.OptPropFactory;
import java.util.function.BooleanSupplier;
+import java.util.function.LongSupplier;
class AppCompatOrientationCapability {
@@ -58,7 +59,8 @@ class AppCompatOrientationCapability {
@NonNull LetterboxConfiguration letterboxConfiguration,
@NonNull ActivityRecord activityRecord) {
mActivityRecord = activityRecord;
- mOrientationCapabilityState = new OrientationCapabilityState(mActivityRecord);
+ mOrientationCapabilityState = new OrientationCapabilityState(mActivityRecord,
+ System::currentTimeMillis);
final BooleanSupplier isPolicyForIgnoringRequestedOrientationEnabled = asLazy(
letterboxConfiguration::isPolicyForIgnoringRequestedOrientationEnabled);
mIgnoreRequestedOrientationOptProp = optPropBuilder.create(
@@ -214,8 +216,12 @@ class AppCompatOrientationCapability {
private long mTimeMsLastSetOrientationRequest = 0;
// Counter for ActivityRecord#setRequestedOrientation
private int mSetOrientationRequestCounter = 0;
+ @VisibleForTesting
+ LongSupplier mCurrentTimeMillisSupplier;
- OrientationCapabilityState(@NonNull ActivityRecord activityRecord) {
+ OrientationCapabilityState(@NonNull ActivityRecord activityRecord,
+ @NonNull LongSupplier currentTimeMillisSupplier) {
+ mCurrentTimeMillisSupplier = currentTimeMillisSupplier;
mIsOverrideToNosensorOrientationEnabled =
activityRecord.info.isChangeEnabled(OVERRIDE_UNDEFINED_ORIENTATION_TO_NOSENSOR);
mIsOverrideToPortraitOrientationEnabled =
@@ -238,7 +244,7 @@ class AppCompatOrientationCapability {
* Updates the orientation request counter using a specific timeout.
*/
void updateOrientationRequestLoopState() {
- final long currTimeMs = System.currentTimeMillis();
+ final long currTimeMs = mCurrentTimeMillisSupplier.getAsLong();
final long elapsedTime = currTimeMs - mTimeMsLastSetOrientationRequest;
if (elapsedTime < SET_ORIENTATION_REQUEST_COUNTER_TIMEOUT_MS) {
mSetOrientationRequestCounter++;
diff --git a/services/core/java/com/android/server/wm/Transition.java b/services/core/java/com/android/server/wm/Transition.java
index 7e61023e28fc..a029f38de128 100644
--- a/services/core/java/com/android/server/wm/Transition.java
+++ b/services/core/java/com/android/server/wm/Transition.java
@@ -2544,9 +2544,8 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener {
if (wc.asWindowState() != null) continue;
final ChangeInfo changeInfo = changes.get(wc);
-
- // Reject no-ops
- if (!changeInfo.hasChanged()) {
+ // Reject no-ops, unless wallpaper
+ if (!changeInfo.hasChanged() && wc.asWallpaperToken() == null) {
ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS,
" Rejecting as no-op: %s", wc);
continue;
diff --git a/services/core/java/com/android/server/wm/WallpaperController.java b/services/core/java/com/android/server/wm/WallpaperController.java
index 3e43f5a2da66..a3e03029c095 100644
--- a/services/core/java/com/android/server/wm/WallpaperController.java
+++ b/services/core/java/com/android/server/wm/WallpaperController.java
@@ -764,10 +764,10 @@ class WallpaperController {
void collectTopWallpapers(Transition transition) {
if (mFindResults.hasTopShowWhenLockedWallpaper()) {
- transition.collect(mFindResults.mTopWallpaper.mTopShowWhenLockedWallpaper);
+ transition.collect(mFindResults.mTopWallpaper.mTopShowWhenLockedWallpaper.mToken);
}
if (mFindResults.hasTopHideWhenLockedWallpaper()) {
- transition.collect(mFindResults.mTopWallpaper.mTopHideWhenLockedWallpaper);
+ transition.collect(mFindResults.mTopWallpaper.mTopHideWhenLockedWallpaper.mToken);
}
}
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeConfigTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeConfigTest.java
index 9352c1287ee1..f5ab95c1ab4c 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeConfigTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeConfigTest.java
@@ -511,6 +511,9 @@ public class ZenModeConfigTest extends UiServiceTestCase {
rule.iconResName = ICON_RES_NAME;
rule.triggerDescription = TRIGGER_DESC;
rule.deletionInstant = Instant.ofEpochMilli(1701790147000L);
+ if (Flags.modesUi()) {
+ rule.disabledOrigin = ZenModeConfig.UPDATE_ORIGIN_USER;
+ }
Parcel parcel = Parcel.obtain();
rule.writeToParcel(parcel, 0);
@@ -540,6 +543,9 @@ public class ZenModeConfigTest extends UiServiceTestCase {
assertEquals(rule.triggerDescription, parceled.triggerDescription);
assertEquals(rule.zenPolicy, parceled.zenPolicy);
assertEquals(rule.deletionInstant, parceled.deletionInstant);
+ if (Flags.modesUi()) {
+ assertEquals(rule.disabledOrigin, parceled.disabledOrigin);
+ }
assertEquals(rule, parceled);
assertEquals(rule.hashCode(), parceled.hashCode());
@@ -620,6 +626,9 @@ public class ZenModeConfigTest extends UiServiceTestCase {
rule.iconResName = ICON_RES_NAME;
rule.triggerDescription = TRIGGER_DESC;
rule.deletionInstant = Instant.ofEpochMilli(1701790147000L);
+ if (Flags.modesUi()) {
+ rule.disabledOrigin = ZenModeConfig.UPDATE_ORIGIN_APP;
+ }
ByteArrayOutputStream baos = new ByteArrayOutputStream();
writeRuleXml(rule, baos);
@@ -653,6 +662,9 @@ public class ZenModeConfigTest extends UiServiceTestCase {
assertEquals(rule.triggerDescription, fromXml.triggerDescription);
assertEquals(rule.iconResName, fromXml.iconResName);
assertEquals(rule.deletionInstant, fromXml.deletionInstant);
+ if (Flags.modesUi()) {
+ assertEquals(rule.disabledOrigin, fromXml.disabledOrigin);
+ }
}
@Test
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeDiffTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeDiffTest.java
index 26a13cb47563..57587f7dc38a 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeDiffTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeDiffTest.java
@@ -16,6 +16,7 @@
package com.android.server.notification;
+import static android.app.Flags.FLAG_MODES_API;
import static android.app.Flags.FLAG_MODES_UI;
import static com.google.common.truth.Truth.assertThat;
@@ -32,6 +33,7 @@ import android.app.Flags;
import android.app.NotificationManager;
import android.content.ComponentName;
import android.net.Uri;
+import android.platform.test.annotations.EnableFlags;
import android.platform.test.flag.junit.FlagsParameterization;
import android.platform.test.flag.junit.SetFlagsRule;
import android.provider.Settings;
@@ -79,16 +81,17 @@ public class ZenModeDiffTest extends UiServiceTestCase {
: Set.of("version", "manualRule", "automaticRules");
// Differences for flagged fields are only generated if the flag is enabled.
- // "Metadata" fields (userModifiedFields & co, deletionInstant) are not compared.
+ // "Metadata" fields (userModifiedFields, deletionInstant, disabledOrigin) are not compared.
private static final Set<String> ZEN_RULE_EXEMPT_FIELDS =
android.app.Flags.modesApi()
? Set.of("userModifiedFields", "zenPolicyUserModifiedFields",
- "zenDeviceEffectsUserModifiedFields", "deletionInstant")
+ "zenDeviceEffectsUserModifiedFields", "deletionInstant",
+ "disabledOrigin")
: Set.of(RuleDiff.FIELD_TYPE, RuleDiff.FIELD_TRIGGER_DESCRIPTION,
RuleDiff.FIELD_ICON_RES, RuleDiff.FIELD_ALLOW_MANUAL,
RuleDiff.FIELD_ZEN_DEVICE_EFFECTS, "userModifiedFields",
"zenPolicyUserModifiedFields", "zenDeviceEffectsUserModifiedFields",
- "deletionInstant");
+ "deletionInstant", "disabledOrigin");
// allowPriorityChannels is flagged by android.app.modes_api
public static final Set<String> ZEN_MODE_CONFIG_FLAGGED_FIELDS =
@@ -201,8 +204,8 @@ public class ZenModeDiffTest extends UiServiceTestCase {
}
@Test
+ @EnableFlags(FLAG_MODES_API)
public void testConfigDiff_fieldDiffs_flagOn() throws Exception {
- mSetFlagsRule.enableFlags(Flags.FLAG_MODES_API);
// these two start the same
ZenModeConfig c1 = new ZenModeConfig();
ZenModeConfig c2 = new ZenModeConfig();
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java
index 40b0d78f0640..7bb633e6e1e0 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java
@@ -6226,6 +6226,101 @@ public class ZenModeHelperTest extends UiServiceTestCase {
assertThat(mZenModeHelper.getConfig().manualRule.zenDeviceEffects).isEqualTo(effects);
}
+ @Test
+ @EnableFlags({FLAG_MODES_API, FLAG_MODES_UI})
+ public void addAutomaticZenRule_startsDisabled_recordsDisabledOrigin() {
+ AutomaticZenRule startsDisabled = new AutomaticZenRule.Builder("Rule", Uri.EMPTY)
+ .setOwner(new ComponentName(mPkg, "SomeProvider"))
+ .setEnabled(false)
+ .build();
+
+ String ruleId = mZenModeHelper.addAutomaticZenRule(mPkg, startsDisabled, UPDATE_ORIGIN_APP,
+ "new", CUSTOM_PKG_UID);
+
+ assertThat(mZenModeHelper.mConfig.automaticRules.get(ruleId).disabledOrigin).isEqualTo(
+ UPDATE_ORIGIN_APP);
+ }
+
+ @Test
+ @EnableFlags({FLAG_MODES_API, FLAG_MODES_UI})
+ public void updateAutomaticZenRule_disabling_recordsDisabledOrigin() {
+ AutomaticZenRule startsEnabled = new AutomaticZenRule.Builder("Rule", Uri.EMPTY)
+ .setOwner(new ComponentName(mPkg, "SomeProvider"))
+ .setEnabled(true)
+ .build();
+ String ruleId = mZenModeHelper.addAutomaticZenRule(mPkg, startsEnabled, UPDATE_ORIGIN_APP,
+ "new", CUSTOM_PKG_UID);
+ assertThat(mZenModeHelper.mConfig.automaticRules.get(ruleId).disabledOrigin).isEqualTo(
+ UPDATE_ORIGIN_UNKNOWN);
+
+ AutomaticZenRule nowDisabled = new AutomaticZenRule.Builder(startsEnabled)
+ .setEnabled(false)
+ .build();
+ mZenModeHelper.updateAutomaticZenRule(ruleId, nowDisabled, UPDATE_ORIGIN_USER, "off",
+ Process.SYSTEM_UID);
+
+ assertThat(mZenModeHelper.mConfig.automaticRules.get(ruleId).disabledOrigin).isEqualTo(
+ UPDATE_ORIGIN_USER);
+ }
+
+ @Test
+ @EnableFlags({FLAG_MODES_API, FLAG_MODES_UI})
+ public void updateAutomaticZenRule_keepingDisabled_preservesPreviousDisabledOrigin() {
+ AutomaticZenRule startsEnabled = new AutomaticZenRule.Builder("Rule", Uri.EMPTY)
+ .setOwner(new ComponentName(mPkg, "SomeProvider"))
+ .setEnabled(true)
+ .build();
+ String ruleId = mZenModeHelper.addAutomaticZenRule(mPkg, startsEnabled, UPDATE_ORIGIN_APP,
+ "new", CUSTOM_PKG_UID);
+ AutomaticZenRule nowDisabled = new AutomaticZenRule.Builder(startsEnabled)
+ .setEnabled(false)
+ .build();
+ mZenModeHelper.updateAutomaticZenRule(ruleId, nowDisabled, UPDATE_ORIGIN_USER, "off",
+ Process.SYSTEM_UID);
+ assertThat(mZenModeHelper.mConfig.automaticRules.get(ruleId).disabledOrigin).isEqualTo(
+ UPDATE_ORIGIN_USER);
+
+ // Now update it again, for an unrelated reason with a different origin.
+ AutomaticZenRule nowRenamed = new AutomaticZenRule.Builder(nowDisabled)
+ .setName("Fancy pants rule")
+ .build();
+ mZenModeHelper.updateAutomaticZenRule(ruleId, nowRenamed, UPDATE_ORIGIN_APP, "update",
+ CUSTOM_PKG_UID);
+
+ // Identity of the disabler is preserved.
+ assertThat(mZenModeHelper.mConfig.automaticRules.get(ruleId).disabledOrigin).isEqualTo(
+ UPDATE_ORIGIN_USER);
+ }
+
+ @Test
+ @EnableFlags({FLAG_MODES_API, FLAG_MODES_UI})
+ public void updateAutomaticZenRule_enabling_clearsDisabledOrigin() {
+ AutomaticZenRule startsEnabled = new AutomaticZenRule.Builder("Rule", Uri.EMPTY)
+ .setOwner(new ComponentName(mPkg, "SomeProvider"))
+ .setEnabled(true)
+ .build();
+ String ruleId = mZenModeHelper.addAutomaticZenRule(mPkg, startsEnabled, UPDATE_ORIGIN_APP,
+ "new", CUSTOM_PKG_UID);
+ AutomaticZenRule nowDisabled = new AutomaticZenRule.Builder(startsEnabled)
+ .setEnabled(false)
+ .build();
+ mZenModeHelper.updateAutomaticZenRule(ruleId, nowDisabled, UPDATE_ORIGIN_USER, "off",
+ Process.SYSTEM_UID);
+ assertThat(mZenModeHelper.mConfig.automaticRules.get(ruleId).disabledOrigin).isEqualTo(
+ UPDATE_ORIGIN_USER);
+
+ // Now enable it again
+ AutomaticZenRule nowEnabled = new AutomaticZenRule.Builder(nowDisabled)
+ .setEnabled(true)
+ .build();
+ mZenModeHelper.updateAutomaticZenRule(ruleId, nowEnabled, UPDATE_ORIGIN_APP, "on",
+ CUSTOM_PKG_UID);
+
+ // Identity of the disabler was cleared.
+ assertThat(mZenModeHelper.mConfig.automaticRules.get(ruleId).disabledOrigin).isEqualTo(
+ UPDATE_ORIGIN_UNKNOWN);
+ }
+
private static void addZenRule(ZenModeConfig config, String id, String ownerPkg, int zenMode,
@Nullable ZenPolicy zenPolicy) {
ZenRule rule = new ZenRule();
diff --git a/services/tests/wmtests/src/com/android/server/wm/AppCompatOrientationCapabilityTest.java b/services/tests/wmtests/src/com/android/server/wm/AppCompatOrientationCapabilityTest.java
new file mode 100644
index 000000000000..f1cf866df4a5
--- /dev/null
+++ b/services/tests/wmtests/src/com/android/server/wm/AppCompatOrientationCapabilityTest.java
@@ -0,0 +1,433 @@
+/*
+ * 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.server.wm;
+
+import static android.content.pm.ActivityInfo.OVERRIDE_CAMERA_COMPAT_DISABLE_REFRESH;
+import static android.content.pm.ActivityInfo.OVERRIDE_ENABLE_COMPAT_IGNORE_ORIENTATION_REQUEST_WHEN_LOOP_DETECTED;
+import static android.content.pm.ActivityInfo.OVERRIDE_ENABLE_COMPAT_IGNORE_REQUESTED_ORIENTATION;
+import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED;
+import static android.view.WindowManager.PROPERTY_COMPAT_ALLOW_IGNORING_ORIENTATION_REQUEST_WHEN_LOOP_DETECTED;
+import static android.view.WindowManager.PROPERTY_COMPAT_IGNORE_REQUESTED_ORIENTATION;
+
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
+import static com.android.server.wm.AppCompatOrientationCapability.OrientationCapabilityState.MIN_COUNT_TO_IGNORE_REQUEST_IN_LOOP;
+import static com.android.server.wm.AppCompatOrientationCapability.OrientationCapabilityState.SET_ORIENTATION_REQUEST_COUNTER_TIMEOUT_MS;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+
+import android.compat.testing.PlatformCompatChangeRule;
+import android.content.ComponentName;
+import android.content.pm.PackageManager;
+import android.content.res.Configuration;
+import android.platform.test.annotations.Presubmit;
+
+import androidx.annotation.NonNull;
+
+import com.android.server.wm.utils.TestComponentStack;
+
+import libcore.junit.util.compat.CoreCompatChangeRule.EnableCompatChanges;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TestRule;
+import org.junit.runner.RunWith;
+
+import java.util.function.Consumer;
+import java.util.function.IntConsumer;
+import java.util.function.LongSupplier;
+
+/**
+ * Test class for {@link AppCompatOrientationCapability}.
+ * <p>
+ * Build/Install/Run:
+ * atest WmTests:AppCompatOrientationCapabilityTest
+ */
+@Presubmit
+@RunWith(WindowTestRunner.class)
+public class AppCompatOrientationCapabilityTest extends WindowTestsBase {
+
+ @Rule
+ public TestRule compatChangeRule = new PlatformCompatChangeRule();
+
+ @Test
+ @EnableCompatChanges({OVERRIDE_ENABLE_COMPAT_IGNORE_REQUESTED_ORIENTATION})
+ public void testShouldIgnoreRequestedOrientation_activityRelaunching_returnsTrue() {
+ runTestScenario((robot) -> {
+ robot.prepareIsPolicyForIgnoringRequestedOrientationEnabled(true);
+ robot.createActivityWithComponent();
+ robot.prepareRelaunchingAfterRequestedOrientationChanged(true);
+
+ robot.checkShouldIgnoreRequestedOrientation(SCREEN_ORIENTATION_UNSPECIFIED);
+ });
+ }
+
+ @Test
+ @EnableCompatChanges({OVERRIDE_ENABLE_COMPAT_IGNORE_REQUESTED_ORIENTATION})
+ public void testShouldIgnoreRequestedOrientation_cameraCompatTreatment_returnsTrue() {
+ runTestScenario((robot) -> {
+ robot.prepareIsCameraCompatTreatmentEnabled(true);
+ robot.prepareIsCameraCompatTreatmentEnabledAtBuildTime(true);
+ robot.prepareIsPolicyForIgnoringRequestedOrientationEnabled(true);
+
+ robot.createActivityWithComponentInNewTask();
+ robot.prepareRelaunchingAfterRequestedOrientationChanged(false);
+ robot.prepareIsTreatmentEnabledForTopActivity(true);
+
+ robot.checkShouldIgnoreRequestedOrientation(SCREEN_ORIENTATION_UNSPECIFIED);
+ });
+ }
+
+ @Test
+ public void testShouldIgnoreRequestedOrientation_overrideDisabled_returnsFalse() {
+ runTestScenario((robot) -> {
+ robot.prepareIsPolicyForIgnoringRequestedOrientationEnabled(true);
+
+ robot.createActivityWithComponent();
+ robot.prepareRelaunchingAfterRequestedOrientationChanged(true);
+
+ robot.checkShouldNotIgnoreRequestedOrientation(SCREEN_ORIENTATION_UNSPECIFIED);
+ });
+ }
+
+ @Test
+ public void testShouldIgnoreRequestedOrientation_propertyIsTrue_returnsTrue() {
+ runTestScenario((robot) -> {
+ robot.prepareIsPolicyForIgnoringRequestedOrientationEnabled(true);
+ robot.enableProperty(PROPERTY_COMPAT_IGNORE_REQUESTED_ORIENTATION);
+
+ robot.createActivityWithComponent();
+ robot.prepareRelaunchingAfterRequestedOrientationChanged(true);
+
+ robot.checkShouldIgnoreRequestedOrientation(SCREEN_ORIENTATION_UNSPECIFIED);
+ });
+ }
+
+ @Test
+ @EnableCompatChanges({OVERRIDE_ENABLE_COMPAT_IGNORE_REQUESTED_ORIENTATION})
+ public void testShouldIgnoreRequestedOrientation_propertyIsFalseAndOverride_returnsFalse()
+ throws Exception {
+ runTestScenario((robot) -> {
+ robot.prepareIsPolicyForIgnoringRequestedOrientationEnabled(true);
+ robot.disableProperty(PROPERTY_COMPAT_IGNORE_REQUESTED_ORIENTATION);
+
+ robot.createActivityWithComponent();
+ robot.prepareRelaunchingAfterRequestedOrientationChanged(true);
+
+ robot.checkShouldNotIgnoreRequestedOrientation(SCREEN_ORIENTATION_UNSPECIFIED);
+ });
+ }
+
+ @Test
+ public void testShouldIgnoreOrientationRequestLoop_overrideDisabled_returnsFalse() {
+ runTestScenario((robot) -> {
+ robot.prepareIsPolicyForIgnoringRequestedOrientationEnabled(true);
+ robot.createActivityWithComponent();
+ robot.prepareIsLetterboxedForFixedOrientationAndAspectRatio(false);
+
+ robot.checkRequestLoopExtended((i) -> {
+ robot.checkShouldNotIgnoreOrientationLoop();
+ robot.checkExpectedLoopCount(/* expectedCount */ 0);
+ });
+ });
+ }
+
+ @Test
+ @EnableCompatChanges({OVERRIDE_ENABLE_COMPAT_IGNORE_ORIENTATION_REQUEST_WHEN_LOOP_DETECTED})
+ public void testShouldIgnoreOrientationRequestLoop_propertyIsFalseAndOverride_returnsFalse() {
+ runTestScenario((robot) -> {
+ robot.prepareIsPolicyForIgnoringRequestedOrientationEnabled(true);
+ robot.disableProperty(
+ PROPERTY_COMPAT_ALLOW_IGNORING_ORIENTATION_REQUEST_WHEN_LOOP_DETECTED);
+ robot.createActivityWithComponent();
+ robot.prepareIsLetterboxedForFixedOrientationAndAspectRatio(false);
+
+ robot.checkRequestLoopExtended((i) -> {
+ robot.checkShouldNotIgnoreOrientationLoop();
+ robot.checkExpectedLoopCount(/* expectedCount */ 0);
+ });
+ });
+ }
+
+ @Test
+ @EnableCompatChanges({OVERRIDE_ENABLE_COMPAT_IGNORE_ORIENTATION_REQUEST_WHEN_LOOP_DETECTED})
+ public void testShouldIgnoreOrientationRequestLoop_isLetterboxed_returnsFalse() {
+ runTestScenario((robot) -> {
+ robot.prepareIsPolicyForIgnoringRequestedOrientationEnabled(true);
+ robot.createActivityWithComponent();
+ robot.prepareIsLetterboxedForFixedOrientationAndAspectRatio(true);
+
+ robot.checkRequestLoopExtended((i) -> {
+ robot.checkShouldNotIgnoreOrientationLoop();
+ robot.checkExpectedLoopCount(/* expectedCount */ i);
+ });
+ });
+ }
+
+ @Test
+ @EnableCompatChanges({OVERRIDE_ENABLE_COMPAT_IGNORE_ORIENTATION_REQUEST_WHEN_LOOP_DETECTED})
+ public void testShouldIgnoreOrientationRequestLoop_noLoop_returnsFalse() {
+ runTestScenario((robot) -> {
+ robot.prepareIsPolicyForIgnoringRequestedOrientationEnabled(true);
+ robot.createActivityWithComponent();
+ robot.prepareIsLetterboxedForFixedOrientationAndAspectRatio(false);
+
+ robot.checkShouldNotIgnoreOrientationLoop();
+ robot.checkExpectedLoopCount(/* expectedCount */ 0);
+ });
+ }
+
+ @Test
+ @EnableCompatChanges({OVERRIDE_ENABLE_COMPAT_IGNORE_ORIENTATION_REQUEST_WHEN_LOOP_DETECTED})
+ public void testShouldIgnoreOrientationRequestLoop_timeout_returnsFalse() {
+ runTestScenario((robot) -> {
+ robot.prepareIsPolicyForIgnoringRequestedOrientationEnabled(true);
+ robot.createActivityWithComponent();
+ robot.prepareIsLetterboxedForFixedOrientationAndAspectRatio(false);
+
+ robot.prepareMockedTime();
+ robot.checkRequestLoopExtended((i) -> {
+ robot.checkShouldNotIgnoreOrientationLoop();
+ robot.checkExpectedLoopCount(/* expectedCount */ 0);
+ robot.delay();
+ });
+ });
+ }
+
+ @Test
+ @EnableCompatChanges({OVERRIDE_ENABLE_COMPAT_IGNORE_ORIENTATION_REQUEST_WHEN_LOOP_DETECTED})
+ public void testShouldIgnoreOrientationRequestLoop_returnsTrue() {
+ runTestScenario((robot) -> {
+ robot.prepareIsPolicyForIgnoringRequestedOrientationEnabled(true);
+ robot.createActivityWithComponent();
+ robot.prepareIsLetterboxedForFixedOrientationAndAspectRatio(false);
+
+ robot.checkRequestLoop((i) -> {
+ robot.checkShouldNotIgnoreOrientationLoop();
+ robot.checkExpectedLoopCount(/* expectedCount */ i);
+ });
+ robot.checkShouldIgnoreOrientationLoop();
+ robot.checkExpectedLoopCount(/* expectedCount */ MIN_COUNT_TO_IGNORE_REQUEST_IN_LOOP);
+ });
+ }
+
+ @Test
+ @EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_DISABLE_REFRESH})
+ public void testShouldIgnoreRequestedOrientation_flagIsDisabled_returnsFalse() {
+ runTestScenario((robot) -> {
+ robot.prepareIsPolicyForIgnoringRequestedOrientationEnabled(true);
+ robot.createActivityWithComponent();
+ robot.prepareIsLetterboxedForFixedOrientationAndAspectRatio(false);
+
+ robot.checkShouldNotIgnoreRequestedOrientation(SCREEN_ORIENTATION_UNSPECIFIED);
+ });
+ }
+
+ /**
+ * Runs a test scenario providing a Robot.
+ */
+ void runTestScenario(@NonNull Consumer<OrientationCapabilityRobotTest> consumer) {
+ spyOn(mWm.mLetterboxConfiguration);
+ final OrientationCapabilityRobotTest robot =
+ new OrientationCapabilityRobotTest(mWm, mAtm, mSupervisor);
+ consumer.accept(robot);
+ }
+
+ private static class OrientationCapabilityRobotTest {
+
+ @NonNull
+ private final ActivityTaskManagerService mAtm;
+ @NonNull
+ private final WindowManagerService mWm;
+ @NonNull
+ private final ActivityTaskSupervisor mSupervisor;
+ @NonNull
+ private final LetterboxConfiguration mLetterboxConfiguration;
+ @NonNull
+ private final TestComponentStack<ActivityRecord> mActivityStack;
+ @NonNull
+ private final TestComponentStack<Task> mTaskStack;
+ @NonNull
+ private final CurrentTimeMillisSupplierTest mTestCurrentTimeMillisSupplier;
+
+
+ OrientationCapabilityRobotTest(@NonNull WindowManagerService wm,
+ @NonNull ActivityTaskManagerService atm,
+ @NonNull ActivityTaskSupervisor supervisor) {
+ mAtm = atm;
+ mWm = wm;
+ mSupervisor = supervisor;
+ mActivityStack = new TestComponentStack<>();
+ mTaskStack = new TestComponentStack<>();
+ mLetterboxConfiguration = mWm.mLetterboxConfiguration;
+ mTestCurrentTimeMillisSupplier = new CurrentTimeMillisSupplierTest();
+ }
+
+ void prepareRelaunchingAfterRequestedOrientationChanged(boolean enabled) {
+ getTopOrientationCapability().setRelaunchingAfterRequestedOrientationChanged(enabled);
+ }
+
+ void prepareIsPolicyForIgnoringRequestedOrientationEnabled(boolean enabled) {
+ doReturn(enabled).when(mLetterboxConfiguration)
+ .isPolicyForIgnoringRequestedOrientationEnabled();
+ }
+
+ void prepareIsCameraCompatTreatmentEnabled(boolean enabled) {
+ doReturn(enabled).when(mLetterboxConfiguration).isCameraCompatTreatmentEnabled();
+ }
+
+ void prepareIsCameraCompatTreatmentEnabledAtBuildTime(boolean enabled) {
+ doReturn(enabled).when(mLetterboxConfiguration)
+ .isCameraCompatTreatmentEnabledAtBuildTime();
+ }
+
+ void prepareIsTreatmentEnabledForTopActivity(boolean enabled) {
+ final DisplayRotationCompatPolicy displayPolicy = mActivityStack.top()
+ .mDisplayContent.mDisplayRotationCompatPolicy;
+ spyOn(displayPolicy);
+ doReturn(enabled).when(displayPolicy)
+ .isTreatmentEnabledForActivity(eq(mActivityStack.top()));
+ }
+
+ // Useful to reduce timeout during tests
+ void prepareMockedTime() {
+ getTopOrientationCapability().mOrientationCapabilityState.mCurrentTimeMillisSupplier =
+ mTestCurrentTimeMillisSupplier;
+ }
+
+ void delay() {
+ mTestCurrentTimeMillisSupplier.delay(SET_ORIENTATION_REQUEST_COUNTER_TIMEOUT_MS);
+ }
+
+ void enableProperty(@NonNull String propertyName) {
+ setPropertyValue(propertyName, /* enabled */ true);
+ }
+
+ void disableProperty(@NonNull String propertyName) {
+ setPropertyValue(propertyName, /* enabled */ false);
+ }
+
+ void prepareIsLetterboxedForFixedOrientationAndAspectRatio(boolean enabled) {
+ spyOn(mActivityStack.top());
+ doReturn(enabled).when(mActivityStack.top())
+ .isLetterboxedForFixedOrientationAndAspectRatio();
+ }
+
+ void createActivityWithComponent() {
+ createActivityWithComponentInNewTask(/* inNewTask */ mTaskStack.isEmpty());
+ }
+
+ void createActivityWithComponentInNewTask() {
+ createActivityWithComponentInNewTask(/* inNewTask */ true);
+ }
+
+ private void createActivityWithComponentInNewTask(boolean inNewTask) {
+ if (inNewTask) {
+ createNewTask();
+ }
+ final ActivityRecord activity = new ActivityBuilder(mAtm)
+ .setOnTop(true)
+ .setTask(mTaskStack.top())
+ // Set the component to be that of the test class in order
+ // to enable compat changes
+ .setComponent(ComponentName.createRelative(mAtm.mContext,
+ com.android.server.wm.LetterboxUiControllerTest.class.getName()))
+ .build();
+ mActivityStack.push(activity);
+ }
+
+ void checkShouldIgnoreRequestedOrientation(
+ @Configuration.Orientation int expectedOrientation) {
+ assertTrue(getTopOrientationCapability()
+ .shouldIgnoreRequestedOrientation(expectedOrientation));
+ }
+
+ void checkShouldNotIgnoreRequestedOrientation(
+ @Configuration.Orientation int expectedOrientation) {
+ assertFalse(getTopOrientationCapability()
+ .shouldIgnoreRequestedOrientation(expectedOrientation));
+ }
+
+ void checkExpectedLoopCount(int expectedCount) {
+ assertEquals(expectedCount, getTopOrientationCapability()
+ .getSetOrientationRequestCounter());
+ }
+
+ void checkShouldNotIgnoreOrientationLoop() {
+ assertFalse(getTopOrientationCapability().shouldIgnoreOrientationRequestLoop());
+ }
+
+ void checkShouldIgnoreOrientationLoop() {
+ assertTrue(getTopOrientationCapability().shouldIgnoreOrientationRequestLoop());
+ }
+
+ void checkRequestLoop(IntConsumer consumer) {
+ for (int i = 0; i < MIN_COUNT_TO_IGNORE_REQUEST_IN_LOOP; i++) {
+ consumer.accept(i);
+ }
+ }
+
+ void checkRequestLoopExtended(IntConsumer consumer) {
+ for (int i = 0; i <= MIN_COUNT_TO_IGNORE_REQUEST_IN_LOOP; i++) {
+ consumer.accept(i);
+ }
+ }
+
+ private AppCompatOrientationCapability getTopOrientationCapability() {
+ return mActivityStack.top().mAppCompatController.getAppCompatCapability()
+ .getAppCompatOrientationCapability();
+ }
+
+ private void createNewTask() {
+ final DisplayContent displayContent = new TestDisplayContent
+ .Builder(mAtm, /* dw */ 1000, /* dh */ 2000).build();
+ final Task newTask = new TaskBuilder(mSupervisor).setDisplay(displayContent).build();
+ mTaskStack.push(newTask);
+ }
+
+ private void setPropertyValue(@NonNull String propertyName, boolean enabled) {
+ PackageManager.Property property = new PackageManager.Property(propertyName,
+ /* value */ enabled, /* packageName */ "",
+ /* className */ "");
+ PackageManager pm = mWm.mContext.getPackageManager();
+ spyOn(pm);
+ try {
+ doReturn(property).when(pm).getProperty(eq(propertyName), anyString());
+ } catch (PackageManager.NameNotFoundException e) {
+ fail(e.getLocalizedMessage());
+ }
+ }
+
+ private static class CurrentTimeMillisSupplierTest implements LongSupplier {
+
+ private long mCurrenTimeMillis = System.currentTimeMillis();
+
+ @Override
+ public long getAsLong() {
+ return mCurrenTimeMillis;
+ }
+
+ public void delay(long delay) {
+ mCurrenTimeMillis += delay;
+ }
+ }
+ }
+}
diff --git a/services/tests/wmtests/src/com/android/server/wm/LetterboxUiControllerTest.java b/services/tests/wmtests/src/com/android/server/wm/LetterboxUiControllerTest.java
index 07870527c8fd..b1200bcdd4b7 100644
--- a/services/tests/wmtests/src/com/android/server/wm/LetterboxUiControllerTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/LetterboxUiControllerTest.java
@@ -25,8 +25,6 @@ import static android.content.pm.ActivityInfo.OVERRIDE_CAMERA_COMPAT_DISABLE_FRE
import static android.content.pm.ActivityInfo.OVERRIDE_CAMERA_COMPAT_DISABLE_REFRESH;
import static android.content.pm.ActivityInfo.OVERRIDE_CAMERA_COMPAT_ENABLE_REFRESH_VIA_PAUSE;
import static android.content.pm.ActivityInfo.OVERRIDE_ENABLE_COMPAT_FAKE_FOCUS;
-import static android.content.pm.ActivityInfo.OVERRIDE_ENABLE_COMPAT_IGNORE_ORIENTATION_REQUEST_WHEN_LOOP_DETECTED;
-import static android.content.pm.ActivityInfo.OVERRIDE_ENABLE_COMPAT_IGNORE_REQUESTED_ORIENTATION;
import static android.content.pm.ActivityInfo.OVERRIDE_LANDSCAPE_ORIENTATION_TO_REVERSE_LANDSCAPE;
import static android.content.pm.ActivityInfo.OVERRIDE_MIN_ASPECT_RATIO;
import static android.content.pm.ActivityInfo.OVERRIDE_MIN_ASPECT_RATIO_ONLY_FOR_CAMERA;
@@ -50,21 +48,17 @@ import static android.view.WindowManager.PROPERTY_CAMERA_COMPAT_ALLOW_FORCE_ROTA
import static android.view.WindowManager.PROPERTY_CAMERA_COMPAT_ALLOW_REFRESH;
import static android.view.WindowManager.PROPERTY_CAMERA_COMPAT_ENABLE_REFRESH_VIA_PAUSE;
import static android.view.WindowManager.PROPERTY_COMPAT_ALLOW_DISPLAY_ORIENTATION_OVERRIDE;
-import static android.view.WindowManager.PROPERTY_COMPAT_ALLOW_IGNORING_ORIENTATION_REQUEST_WHEN_LOOP_DETECTED;
import static android.view.WindowManager.PROPERTY_COMPAT_ALLOW_MIN_ASPECT_RATIO_OVERRIDE;
import static android.view.WindowManager.PROPERTY_COMPAT_ALLOW_ORIENTATION_OVERRIDE;
import static android.view.WindowManager.PROPERTY_COMPAT_ALLOW_RESIZEABLE_ACTIVITY_OVERRIDES;
import static android.view.WindowManager.PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_FULLSCREEN_OVERRIDE;
import static android.view.WindowManager.PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_OVERRIDE;
import static android.view.WindowManager.PROPERTY_COMPAT_ENABLE_FAKE_FOCUS;
-import static android.view.WindowManager.PROPERTY_COMPAT_IGNORE_REQUESTED_ORIENTATION;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.eq;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.mock;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
-import static com.android.server.wm.AppCompatOrientationCapability.OrientationCapabilityState.MIN_COUNT_TO_IGNORE_REQUEST_IN_LOOP;
-import static com.android.server.wm.AppCompatOrientationCapability.OrientationCapabilityState.SET_ORIENTATION_REQUEST_COUNTER_TIMEOUT_MS;
import static com.android.window.flags.Flags.FLAG_CAMERA_COMPAT_FOR_FREEFORM;
import static org.junit.Assert.assertEquals;
@@ -149,187 +143,6 @@ public class LetterboxUiControllerTest extends WindowTestsBase {
mController = new LetterboxUiController(mWm, mActivity);
}
- // shouldIgnoreRequestedOrientation
-
- @Test
- @EnableCompatChanges({OVERRIDE_ENABLE_COMPAT_IGNORE_REQUESTED_ORIENTATION})
- public void testShouldIgnoreRequestedOrientation_activityRelaunching_returnsTrue() {
- prepareActivityThatShouldIgnoreRequestedOrientationDuringRelaunch();
-
- assertTrue(mActivity.mAppCompatController.getAppCompatCapability()
- .getAppCompatOrientationCapability()
- .shouldIgnoreRequestedOrientation(SCREEN_ORIENTATION_UNSPECIFIED));
- }
-
- @Test
- @EnableCompatChanges({OVERRIDE_ENABLE_COMPAT_IGNORE_REQUESTED_ORIENTATION})
- public void testShouldIgnoreRequestedOrientation_cameraCompatTreatment_returnsTrue() {
- doReturn(true).when(mLetterboxConfiguration).isCameraCompatTreatmentEnabled();
- doReturn(true).when(mLetterboxConfiguration)
- .isCameraCompatTreatmentEnabledAtBuildTime();
-
- // Recreate DisplayContent with DisplayRotationCompatPolicy
- mActivity = setUpActivityWithComponent();
- mController = new LetterboxUiController(mWm, mActivity);
- prepareActivityThatShouldIgnoreRequestedOrientationDuringRelaunch();
- mController.setRelaunchingAfterRequestedOrientationChanged(false);
-
- spyOn(mDisplayContent.mDisplayRotationCompatPolicy);
- doReturn(true).when(mDisplayContent.mDisplayRotationCompatPolicy)
- .isTreatmentEnabledForActivity(eq(mActivity));
-
- assertTrue(mActivity.mAppCompatController.getAppCompatCapability()
- .getAppCompatOrientationCapability()
- .shouldIgnoreRequestedOrientation(SCREEN_ORIENTATION_UNSPECIFIED));
- }
-
- @Test
- public void testShouldIgnoreRequestedOrientation_overrideDisabled_returnsFalse() {
- prepareActivityThatShouldIgnoreRequestedOrientationDuringRelaunch();
-
- assertFalse(mActivity.mAppCompatController.getAppCompatCapability()
- .getAppCompatOrientationCapability()
- .shouldIgnoreRequestedOrientation(SCREEN_ORIENTATION_UNSPECIFIED));
- }
-
- @Test
- public void testShouldIgnoreRequestedOrientation_propertyIsTrue_returnsTrue()
- throws Exception {
- doReturn(true).when(mLetterboxConfiguration)
- .isPolicyForIgnoringRequestedOrientationEnabled();
- mockThatProperty(PROPERTY_COMPAT_IGNORE_REQUESTED_ORIENTATION, /* value */ true);
- mController = new LetterboxUiController(mWm, mActivity);
- prepareActivityThatShouldIgnoreRequestedOrientationDuringRelaunch();
-
- assertTrue(mActivity.mAppCompatController.getAppCompatCapability()
- .getAppCompatOrientationCapability()
- .shouldIgnoreRequestedOrientation(SCREEN_ORIENTATION_UNSPECIFIED));
- }
-
- @Test
- @EnableCompatChanges({OVERRIDE_ENABLE_COMPAT_IGNORE_REQUESTED_ORIENTATION})
- public void testShouldIgnoreRequestedOrientation_propertyIsFalseAndOverride_returnsFalse()
- throws Exception {
- doReturn(true).when(mLetterboxConfiguration)
- .isPolicyForIgnoringRequestedOrientationEnabled();
- mockThatProperty(PROPERTY_COMPAT_IGNORE_REQUESTED_ORIENTATION, /* value */ false);
-
- mController = new LetterboxUiController(mWm, mActivity);
- prepareActivityThatShouldIgnoreRequestedOrientationDuringRelaunch();
-
- assertFalse(mActivity.mAppCompatController.getAppCompatCapability()
- .getAppCompatOrientationCapability()
- .shouldIgnoreRequestedOrientation(SCREEN_ORIENTATION_UNSPECIFIED));
- }
-
- @Test
- public void testShouldIgnoreOrientationRequestLoop_overrideDisabled_returnsFalse() {
- doReturn(true).when(mLetterboxConfiguration)
- .isPolicyForIgnoringRequestedOrientationEnabled();
- doReturn(false).when(mActivity).isLetterboxedForFixedOrientationAndAspectRatio();
- // Request 3 times to simulate orientation request loop
- for (int i = 0; i <= MIN_COUNT_TO_IGNORE_REQUEST_IN_LOOP; i++) {
- assertShouldIgnoreOrientationRequestLoop(/* shouldIgnore */ false,
- /* expectedCount */ 0);
- }
- }
-
- @Test
- @EnableCompatChanges({OVERRIDE_ENABLE_COMPAT_IGNORE_ORIENTATION_REQUEST_WHEN_LOOP_DETECTED})
- public void testShouldIgnoreOrientationRequestLoop_propertyIsFalseAndOverride_returnsFalse()
- throws Exception {
- doReturn(true).when(mLetterboxConfiguration)
- .isPolicyForIgnoringRequestedOrientationEnabled();
- mockThatProperty(PROPERTY_COMPAT_ALLOW_IGNORING_ORIENTATION_REQUEST_WHEN_LOOP_DETECTED,
- /* value */ false);
- doReturn(false).when(mActivity).isLetterboxedForFixedOrientationAndAspectRatio();
-
- mController = new LetterboxUiController(mWm, mActivity);
-
- // Request 3 times to simulate orientation request loop
- for (int i = 0; i <= MIN_COUNT_TO_IGNORE_REQUEST_IN_LOOP; i++) {
- assertShouldIgnoreOrientationRequestLoop(/* shouldIgnore */ false,
- /* expectedCount */ 0);
- }
- }
-
- @Test
- @EnableCompatChanges({OVERRIDE_ENABLE_COMPAT_IGNORE_ORIENTATION_REQUEST_WHEN_LOOP_DETECTED})
- public void testShouldIgnoreOrientationRequestLoop_isLetterboxed_returnsFalse() {
- doReturn(true).when(mLetterboxConfiguration)
- .isPolicyForIgnoringRequestedOrientationEnabled();
- doReturn(true).when(mActivity).isLetterboxedForFixedOrientationAndAspectRatio();
-
- // Request 3 times to simulate orientation request loop
- for (int i = 0; i <= MIN_COUNT_TO_IGNORE_REQUEST_IN_LOOP; i++) {
- assertShouldIgnoreOrientationRequestLoop(/* shouldIgnore */ false,
- /* expectedCount */ i);
- }
- }
-
- @Test
- @EnableCompatChanges({OVERRIDE_ENABLE_COMPAT_IGNORE_ORIENTATION_REQUEST_WHEN_LOOP_DETECTED})
- public void testShouldIgnoreOrientationRequestLoop_noLoop_returnsFalse() {
- doReturn(true).when(mLetterboxConfiguration)
- .isPolicyForIgnoringRequestedOrientationEnabled();
- doReturn(false).when(mActivity).isLetterboxedForFixedOrientationAndAspectRatio();
-
- // No orientation request loop
- assertShouldIgnoreOrientationRequestLoop(/* shouldIgnore */ false,
- /* expectedCount */ 0);
- }
-
- @Test
- @EnableCompatChanges({OVERRIDE_ENABLE_COMPAT_IGNORE_ORIENTATION_REQUEST_WHEN_LOOP_DETECTED})
- public void testShouldIgnoreOrientationRequestLoop_timeout_returnsFalse()
- throws InterruptedException {
- doReturn(true).when(mLetterboxConfiguration)
- .isPolicyForIgnoringRequestedOrientationEnabled();
- doReturn(false).when(mActivity).isLetterboxedForFixedOrientationAndAspectRatio();
-
- for (int i = MIN_COUNT_TO_IGNORE_REQUEST_IN_LOOP; i > 0; i--) {
- assertShouldIgnoreOrientationRequestLoop(/* shouldIgnore */ false,
- /* expectedCount */ 0);
- Thread.sleep(SET_ORIENTATION_REQUEST_COUNTER_TIMEOUT_MS);
- }
- }
-
- @Test
- @EnableCompatChanges({OVERRIDE_ENABLE_COMPAT_IGNORE_ORIENTATION_REQUEST_WHEN_LOOP_DETECTED})
- public void testShouldIgnoreOrientationRequestLoop_returnsTrue() {
- doReturn(true).when(mLetterboxConfiguration)
- .isPolicyForIgnoringRequestedOrientationEnabled();
- doReturn(false).when(mActivity).isLetterboxedForFixedOrientationAndAspectRatio();
-
- for (int i = 0; i < MIN_COUNT_TO_IGNORE_REQUEST_IN_LOOP; i++) {
- assertShouldIgnoreOrientationRequestLoop(/* shouldIgnore */ false,
- /* expectedCount */ i);
- }
- assertShouldIgnoreOrientationRequestLoop(/* shouldIgnore */ true,
- /* expectedCount */ MIN_COUNT_TO_IGNORE_REQUEST_IN_LOOP);
- }
-
- private void assertShouldIgnoreOrientationRequestLoop(boolean shouldIgnore, int expectedCount) {
- if (shouldIgnore) {
- assertTrue(mController.shouldIgnoreOrientationRequestLoop());
- } else {
- assertFalse(mController.shouldIgnoreOrientationRequestLoop());
- }
- assertEquals(expectedCount, mController.getSetOrientationRequestCounter());
- }
-
- @Test
- @EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_DISABLE_REFRESH})
- public void testShouldIgnoreRequestedOrientation_flagIsDisabled_returnsFalse() {
- prepareActivityThatShouldIgnoreRequestedOrientationDuringRelaunch();
- doReturn(false).when(mLetterboxConfiguration)
- .isPolicyForIgnoringRequestedOrientationEnabled();
-
- assertFalse(mActivity.mAppCompatController.getAppCompatCapability()
- .getAppCompatOrientationCapability()
- .shouldIgnoreRequestedOrientation(SCREEN_ORIENTATION_UNSPECIFIED));
- }
-
// shouldRefreshActivityForCameraCompat
@Test
diff --git a/services/tests/wmtests/src/com/android/server/wm/TransparentPolicyTest.java b/services/tests/wmtests/src/com/android/server/wm/TransparentPolicyTest.java
index 78cea954eceb..628c65e992fd 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TransparentPolicyTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TransparentPolicyTest.java
@@ -46,12 +46,14 @@ import android.content.res.Configuration;
import android.graphics.Rect;
import android.platform.test.annotations.Presubmit;
+import androidx.annotation.NonNull;
+
+import com.android.server.wm.utils.TestComponentStack;
+
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
-import java.util.ArrayList;
-import java.util.List;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
@@ -318,20 +320,22 @@ public class TransparentPolicyTest extends WindowTestsBase {
*/
private static class TransparentPolicyRobotTest {
+ @NonNull
private final ActivityTaskManagerService mAtm;
-
+ @NonNull
private final Task mTask;
-
- private final ActivityStackTest mActivityStack;
-
+ @NonNull
+ private final TestComponentStack<ActivityRecord> mActivityStack;
+ @NonNull
private WindowConfiguration mTopActivityWindowConfiguration;
- private TransparentPolicyRobotTest(ActivityTaskManagerService atm, Task task,
- ActivityRecord opaqueActivity) {
+ private TransparentPolicyRobotTest(@NonNull ActivityTaskManagerService atm,
+ @NonNull Task task,
+ @NonNull ActivityRecord opaqueActivity) {
mAtm = atm;
mTask = task;
- mActivityStack = new ActivityStackTest();
- mActivityStack.pushActivity(opaqueActivity);
+ mActivityStack = new TestComponentStack<>();
+ mActivityStack.push(opaqueActivity);
spyOn(opaqueActivity.mAppCompatController.getTransparentPolicy());
}
@@ -361,7 +365,7 @@ public class TransparentPolicyTest extends WindowTestsBase {
mTask.addChild(newActivity);
}
spyOn(newActivity.mAppCompatController.getTransparentPolicy());
- mActivityStack.pushActivity(newActivity);
+ mActivityStack.push(newActivity);
}
void attachTopActivityToTask() {
@@ -596,45 +600,5 @@ public class TransparentPolicyTest extends WindowTestsBase {
display.computeScreenConfiguration(c);
display.onRequestedOverrideConfigurationChanged(c);
}
-
- /**
- * Contains all the ActivityRecord launched in the test. This is different from what's in
- * the Task because activities are added here even if not added to tasks.
- */
- private static class ActivityStackTest {
- private final List<ActivityRecord> mActivities = new ArrayList<>();
-
- void pushActivity(ActivityRecord activityRecord) {
- mActivities.add(activityRecord);
- }
-
- void applyToTop(Consumer<ActivityRecord> consumer) {
- consumer.accept(top());
- }
-
- ActivityRecord getFromTop(int fromTop) {
- return mActivities.get(mActivities.size() - fromTop - 1);
- }
-
- ActivityRecord base() {
- return mActivities.get(0);
- }
-
- ActivityRecord top() {
- return mActivities.get(mActivities.size() - 1);
- }
-
- // Allows access to the activity at position beforeLast from the top.
- // If fromTop = 0 the activity used is the top one.
- void applyTo(int fromTop, Consumer<ActivityRecord> consumer) {
- consumer.accept(getFromTop(fromTop));
- }
-
- void applyToAll(Consumer<ActivityRecord> consumer) {
- for (int i = mActivities.size() - 1; i >= 0; i--) {
- consumer.accept(mActivities.get(i));
- }
- }
- }
}
}
diff --git a/services/tests/wmtests/src/com/android/server/wm/utils/TestComponentStack.java b/services/tests/wmtests/src/com/android/server/wm/utils/TestComponentStack.java
new file mode 100644
index 000000000000..a06c0a216d9e
--- /dev/null
+++ b/services/tests/wmtests/src/com/android/server/wm/utils/TestComponentStack.java
@@ -0,0 +1,110 @@
+/*
+ * 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.server.wm.utils;
+
+import androidx.annotation.NonNull;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.Consumer;
+
+/**
+ * Contains all the component created in the test.
+ */
+public class TestComponentStack<T> {
+
+ @NonNull
+ private final List<T> mItems = new ArrayList<>();
+
+ /**
+ * Adds an item to the stack.
+ *
+ * @param item The item to add.
+ */
+ public void push(@NonNull T item) {
+ mItems.add(item);
+ }
+
+ /**
+ * Consumes the top element of the stack.
+ *
+ * @param consumer Consumer for the optional top element.
+ * @throws IndexOutOfBoundsException In case that stack is empty.
+ */
+ public void applyToTop(@NonNull Consumer<T> consumer) {
+ consumer.accept(top());
+ }
+
+ /**
+ * Returns the item at fromTop position from the top one if present or it throws an
+ * exception if not present.
+ *
+ * @param fromTop The position from the top of the item to return.
+ * @return The returned item.
+ * @throws IndexOutOfBoundsException In case that position doesn't exist.
+ */
+ @NonNull
+ public T getFromTop(int fromTop) {
+ return mItems.get(mItems.size() - fromTop - 1);
+ }
+
+ /**
+ * @return The item at the base of the stack if present.
+ * @throws IndexOutOfBoundsException In case that stack is empty.
+ */
+ @NonNull
+ public T base() {
+ return mItems.get(0);
+ }
+
+ /**
+ * @return The item at the top of the stack if present.
+ * @throws IndexOutOfBoundsException In case that stack is empty.
+ */
+ @NonNull
+ public T top() {
+ return mItems.get(mItems.size() - 1);
+ }
+
+ /**
+ * @return {@code true} if the stack is empty.
+ */
+ public boolean isEmpty() {
+ return mItems.isEmpty();
+ }
+
+ /**
+ * Allows access to the item at position beforeLast from the top.
+ *
+ * @param fromTop The position from the top of the item to return.
+ * @param consumer Consumer for the optional returned element.
+ */
+ public void applyTo(int fromTop, @NonNull Consumer<T> consumer) {
+ consumer.accept(getFromTop(fromTop));
+ }
+
+ /**
+ * Invoked the consumer iterating over all the elements in the stack.
+ *
+ * @param consumer Consumer for the elements.
+ */
+ public void applyToAll(@NonNull Consumer<T> consumer) {
+ for (int i = mItems.size() - 1; i >= 0; i--) {
+ consumer.accept(mItems.get(i));
+ }
+ }
+}