Merge changes I8a889842,I07d46bd9,I7e8ce558,I45ab6ad5 into main
* changes:
Convert NotificationActivityStarter to kotlin
Make empty shade text react to Modes changes
Make EmptyShadeView a LaunchableView
Migrate EmptyShadeView to recommended architecture
diff --git a/core/java/android/app/notification.aconfig b/core/java/android/app/notification.aconfig
index 108b5f4..b139017 100644
--- a/core/java/android/app/notification.aconfig
+++ b/core/java/android/app/notification.aconfig
@@ -31,6 +31,16 @@
}
flag {
+ name: "modes_ui_empty_shade"
+ namespace: "systemui"
+ description: "Shows mode that is currently blocking notifications in the empty shade; dependent on flags modes_api and modes_ui"
+ bug: "366003631"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
+
+flag {
name: "modes_ui_test"
namespace: "systemui"
description: "Guards new CTS tests for Modes; dependent on flags modes_api and modes_ui"
diff --git a/packages/SettingsLib/src/com/android/settingslib/notification/modes/TestModeBuilder.java b/packages/SettingsLib/src/com/android/settingslib/notification/modes/TestModeBuilder.java
index 712ddc8..5eeb49a 100644
--- a/packages/SettingsLib/src/com/android/settingslib/notification/modes/TestModeBuilder.java
+++ b/packages/SettingsLib/src/com/android/settingslib/notification/modes/TestModeBuilder.java
@@ -167,6 +167,13 @@
return this;
}
+ public TestModeBuilder setVisualEffect(int effect, boolean allowed) {
+ ZenPolicy newPolicy = new ZenPolicy.Builder(mRule.getZenPolicy())
+ .showVisualEffect(effect, allowed).build();
+ setZenPolicy(newPolicy);
+ return this;
+ }
+
public TestModeBuilder setEnabled(boolean enabled) {
return setEnabled(enabled, /* byUser= */ false);
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/emptyshade/ui/viewmodel/EmptyShadeViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/emptyshade/ui/viewmodel/EmptyShadeViewModelTest.kt
new file mode 100644
index 0000000..f9b7769
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/emptyshade/ui/viewmodel/EmptyShadeViewModelTest.kt
@@ -0,0 +1,208 @@
+/*
+ * 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.systemui.statusbar.notification.emptyshade.ui.viewmodel
+
+import android.app.Flags
+import android.app.NotificationManager.Policy
+import android.platform.test.annotations.DisableFlags
+import android.platform.test.annotations.EnableFlags
+import android.platform.test.flag.junit.FlagsParameterization
+import android.provider.Settings
+import android.service.notification.ZenPolicy.VISUAL_EFFECT_NOTIFICATION_LIST
+import androidx.test.filters.SmallTest
+import com.android.settingslib.notification.data.repository.updateNotificationPolicy
+import com.android.settingslib.notification.modes.TestModeBuilder
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.flags.andSceneContainer
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.statusbar.notification.data.repository.activeNotificationListRepository
+import com.android.systemui.statusbar.notification.emptyshade.shared.ModesEmptyShadeFix
+import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefactor
+import com.android.systemui.statusbar.policy.data.repository.zenModeRepository
+import com.android.systemui.testKosmos
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+import platform.test.runner.parameterized.ParameterizedAndroidJunit4
+import platform.test.runner.parameterized.Parameters
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@RunWith(ParameterizedAndroidJunit4::class)
+@SmallTest
+@EnableFlags(FooterViewRefactor.FLAG_NAME)
+class EmptyShadeViewModelTest(flags: FlagsParameterization) : SysuiTestCase() {
+ private val kosmos = testKosmos()
+ private val testScope = kosmos.testScope
+ private val zenModeRepository = kosmos.zenModeRepository
+ private val activeNotificationListRepository = kosmos.activeNotificationListRepository
+
+ private val underTest = kosmos.emptyShadeViewModel
+
+ companion object {
+ @JvmStatic
+ @Parameters(name = "{0}")
+ fun getParams(): List<FlagsParameterization> {
+ return FlagsParameterization.allCombinationsOf().andSceneContainer()
+ }
+ }
+
+ init {
+ mSetFlagsRule.setFlagsParameterization(flags)
+ }
+
+ @Test
+ fun areNotificationsHiddenInShade_true() =
+ testScope.runTest {
+ val hidden by collectLastValue(underTest.areNotificationsHiddenInShade)
+
+ zenModeRepository.updateNotificationPolicy(
+ suppressedVisualEffects = Policy.SUPPRESSED_EFFECT_NOTIFICATION_LIST
+ )
+ zenModeRepository.updateZenMode(Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS)
+ runCurrent()
+
+ assertThat(hidden).isTrue()
+ }
+
+ @Test
+ fun areNotificationsHiddenInShade_false() =
+ testScope.runTest {
+ val hidden by collectLastValue(underTest.areNotificationsHiddenInShade)
+
+ zenModeRepository.updateNotificationPolicy(
+ suppressedVisualEffects = Policy.SUPPRESSED_EFFECT_NOTIFICATION_LIST
+ )
+ zenModeRepository.updateZenMode(Settings.Global.ZEN_MODE_OFF)
+ runCurrent()
+
+ assertThat(hidden).isFalse()
+ }
+
+ @Test
+ fun hasFilteredOutSeenNotifications_true() =
+ testScope.runTest {
+ val hasFilteredNotifs by collectLastValue(underTest.hasFilteredOutSeenNotifications)
+
+ activeNotificationListRepository.hasFilteredOutSeenNotifications.value = true
+ runCurrent()
+
+ assertThat(hasFilteredNotifs).isTrue()
+ }
+
+ @Test
+ fun hasFilteredOutSeenNotifications_false() =
+ testScope.runTest {
+ val hasFilteredNotifs by collectLastValue(underTest.hasFilteredOutSeenNotifications)
+
+ activeNotificationListRepository.hasFilteredOutSeenNotifications.value = false
+ runCurrent()
+
+ assertThat(hasFilteredNotifs).isFalse()
+ }
+
+ @Test
+ @EnableFlags(ModesEmptyShadeFix.FLAG_NAME)
+ @DisableFlags(Flags.FLAG_MODES_UI, Flags.FLAG_MODES_API)
+ fun text_changesWhenNotifsHiddenInShade() =
+ testScope.runTest {
+ val text by collectLastValue(underTest.text)
+
+ zenModeRepository.updateNotificationPolicy(
+ suppressedVisualEffects = Policy.SUPPRESSED_EFFECT_NOTIFICATION_LIST
+ )
+ zenModeRepository.updateZenMode(Settings.Global.ZEN_MODE_OFF)
+ runCurrent()
+
+ assertThat(text).isEqualTo("No notifications")
+
+ zenModeRepository.updateNotificationPolicy(
+ suppressedVisualEffects = Policy.SUPPRESSED_EFFECT_NOTIFICATION_LIST
+ )
+ zenModeRepository.updateZenMode(Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS)
+ runCurrent()
+
+ assertThat(text).isEqualTo("Notifications paused by Do Not Disturb")
+ }
+
+ @Test
+ @EnableFlags(ModesEmptyShadeFix.FLAG_NAME, Flags.FLAG_MODES_UI, Flags.FLAG_MODES_API)
+ fun text_reflectsModesHidingNotifications() =
+ testScope.runTest {
+ val text by collectLastValue(underTest.text)
+
+ assertThat(text).isEqualTo("No notifications")
+
+ zenModeRepository.addMode(
+ TestModeBuilder()
+ .setId("Do not disturb")
+ .setName("Do not disturb")
+ .setActive(true)
+ .setVisualEffect(VISUAL_EFFECT_NOTIFICATION_LIST, /* allowed= */ false)
+ .build()
+ )
+ runCurrent()
+ assertThat(text).isEqualTo("Notifications paused by Do not disturb")
+
+ zenModeRepository.addMode(
+ TestModeBuilder()
+ .setId("Work")
+ .setName("Work")
+ .setActive(true)
+ .setVisualEffect(VISUAL_EFFECT_NOTIFICATION_LIST, /* allowed= */ false)
+ .build()
+ )
+ runCurrent()
+ assertThat(text).isEqualTo("Notifications paused by Do not disturb and one other mode")
+
+ zenModeRepository.addMode(
+ TestModeBuilder()
+ .setId("Gym")
+ .setName("Gym")
+ .setActive(true)
+ .setVisualEffect(VISUAL_EFFECT_NOTIFICATION_LIST, /* allowed= */ false)
+ .build()
+ )
+ runCurrent()
+ assertThat(text).isEqualTo("Notifications paused by Do not disturb and 2 other modes")
+
+ zenModeRepository.deactivateMode("Do not disturb")
+ zenModeRepository.deactivateMode("Work")
+ runCurrent()
+ assertThat(text).isEqualTo("Notifications paused by Gym")
+ }
+
+ @Test
+ @EnableFlags(ModesEmptyShadeFix.FLAG_NAME)
+ fun footer_isVisibleWhenSeenNotifsAreFilteredOut() =
+ testScope.runTest {
+ val footerVisible by collectLastValue(underTest.footer.isVisible)
+
+ activeNotificationListRepository.hasFilteredOutSeenNotifications.value = false
+ runCurrent()
+
+ assertThat(footerVisible).isFalse()
+
+ activeNotificationListRepository.hasFilteredOutSeenNotifications.value = true
+ runCurrent()
+
+ assertThat(footerVisible).isTrue()
+ }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelTest.kt
index 26e1a4d..d12d6f6 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelTest.kt
@@ -18,12 +18,9 @@
package com.android.systemui.statusbar.notification.stack.ui.viewmodel
-import android.app.NotificationManager.Policy
import android.platform.test.annotations.EnableFlags
import android.platform.test.flag.junit.FlagsParameterization
-import android.provider.Settings
import androidx.test.filters.SmallTest
-import com.android.settingslib.notification.data.repository.updateNotificationPolicy
import com.android.systemui.SysuiTestCase
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.flags.DisableSceneContainer
@@ -46,7 +43,6 @@
import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefactor
import com.android.systemui.statusbar.notification.stack.data.repository.headsUpNotificationRepository
import com.android.systemui.statusbar.policy.data.repository.fakeUserSetupRepository
-import com.android.systemui.statusbar.policy.data.repository.zenModeRepository
import com.android.systemui.statusbar.policy.fakeConfigurationController
import com.android.systemui.testKosmos
import com.android.systemui.util.ui.isAnimating
@@ -79,7 +75,6 @@
private val fakeRemoteInputRepository = kosmos.fakeRemoteInputRepository
private val fakeUserSetupRepository = kosmos.fakeUserSetupRepository
private val headsUpRepository = kosmos.headsUpNotificationRepository
- private val zenModeRepository = kosmos.zenModeRepository
private val shadeTestUtil by lazy { kosmos.shadeTestUtil }
@@ -266,56 +261,6 @@
}
@Test
- fun areNotificationsHiddenInShade_true() =
- testScope.runTest {
- val hidden by collectLastValue(underTest.areNotificationsHiddenInShade)
-
- zenModeRepository.updateNotificationPolicy(
- suppressedVisualEffects = Policy.SUPPRESSED_EFFECT_NOTIFICATION_LIST
- )
- zenModeRepository.updateZenMode(Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS)
- runCurrent()
-
- assertThat(hidden).isTrue()
- }
-
- @Test
- fun areNotificationsHiddenInShade_false() =
- testScope.runTest {
- val hidden by collectLastValue(underTest.areNotificationsHiddenInShade)
-
- zenModeRepository.updateNotificationPolicy(
- suppressedVisualEffects = Policy.SUPPRESSED_EFFECT_NOTIFICATION_LIST
- )
- zenModeRepository.updateZenMode(Settings.Global.ZEN_MODE_OFF)
- runCurrent()
-
- assertThat(hidden).isFalse()
- }
-
- @Test
- fun hasFilteredOutSeenNotifications_true() =
- testScope.runTest {
- val hasFilteredNotifs by collectLastValue(underTest.hasFilteredOutSeenNotifications)
-
- activeNotificationListRepository.hasFilteredOutSeenNotifications.value = true
- runCurrent()
-
- assertThat(hasFilteredNotifs).isTrue()
- }
-
- @Test
- fun hasFilteredOutSeenNotifications_false() =
- testScope.runTest {
- val hasFilteredNotifs by collectLastValue(underTest.hasFilteredOutSeenNotifications)
-
- activeNotificationListRepository.hasFilteredOutSeenNotifications.value = false
- runCurrent()
-
- assertThat(hasFilteredNotifs).isFalse()
- }
-
- @Test
fun shouldIncludeFooterView_trueWhenShade() =
testScope.runTest {
val shouldIncludeFooterView by collectFooterViewVisibility()
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractorTest.kt
index 0f6dc07..c5ccf9e 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractorTest.kt
@@ -25,6 +25,7 @@
import android.provider.Settings.Secure.ZEN_DURATION_FOREVER
import android.provider.Settings.Secure.ZEN_DURATION_PROMPT
import android.service.notification.SystemZenRules
+import android.service.notification.ZenPolicy.VISUAL_EFFECT_NOTIFICATION_LIST
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.internal.R
@@ -34,6 +35,7 @@
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.kosmos.testScope
import com.android.systemui.shared.settings.data.repository.secureSettingsRepository
+import com.android.systemui.statusbar.notification.emptyshade.shared.ModesEmptyShadeFix
import com.android.systemui.statusbar.policy.data.repository.fakeDeviceProvisioningRepository
import com.android.systemui.statusbar.policy.data.repository.fakeZenModeRepository
import com.android.systemui.testKosmos
@@ -379,4 +381,46 @@
assertThat(dndMode!!.isActive).isTrue()
}
+
+ @Test
+ @EnableFlags(ModesEmptyShadeFix.FLAG_NAME, Flags.FLAG_MODES_UI, Flags.FLAG_MODES_API)
+ fun modesHidingNotifications_onlyIncludesModesWithNotifListSuppression() =
+ testScope.runTest {
+ val modesHidingNotifications by collectLastValue(underTest.modesHidingNotifications)
+
+ zenModeRepository.addModes(
+ listOf(
+ TestModeBuilder()
+ .setName("Not active, no list suppression")
+ .setActive(false)
+ .setVisualEffect(VISUAL_EFFECT_NOTIFICATION_LIST, /* allowed= */ true)
+ .build(),
+ TestModeBuilder()
+ .setName("Not active, has list suppression")
+ .setActive(false)
+ .setVisualEffect(VISUAL_EFFECT_NOTIFICATION_LIST, /* allowed= */ false)
+ .build(),
+ TestModeBuilder()
+ .setName("No list suppression")
+ .setActive(true)
+ .setVisualEffect(VISUAL_EFFECT_NOTIFICATION_LIST, /* allowed= */ true)
+ .build(),
+ TestModeBuilder()
+ .setName("Has list suppression 1")
+ .setActive(true)
+ .setVisualEffect(VISUAL_EFFECT_NOTIFICATION_LIST, /* allowed= */ false)
+ .build(),
+ TestModeBuilder()
+ .setName("Has list suppression 2")
+ .setActive(true)
+ .setVisualEffect(VISUAL_EFFECT_NOTIFICATION_LIST, /* allowed= */ false)
+ .build(),
+ )
+ )
+ runCurrent()
+
+ assertThat(modesHidingNotifications?.map { it.name })
+ .containsExactly("Has list suppression 1", "Has list suppression 2")
+ .inOrder()
+ }
}
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index 24b6579..75389b1 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -1473,6 +1473,16 @@
<!-- The text to show in the notifications shade when dnd is suppressing notifications. [CHAR LIMIT=100] -->
<string name="dnd_suppressing_shade_text">Notifications paused by Do Not Disturb</string>
+ <!-- The text to show in the notifications shade when a mode is suppressing notifications. [CHAR LIMIT=100] -->
+ <string name="modes_suppressing_shade_text">
+ {count, plural, offset:1
+ =0 {No notifications}
+ =1 {Notifications paused by {mode}}
+ =2 {Notifications paused by {mode} and one other mode}
+ other {Notifications paused by {mode} and # other modes}
+ }
+ </string>
+
<!-- Media projection permission dialog action text. [CHAR LIMIT=60] -->
<string name="media_projection_action_text">Start now</string>
diff --git a/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt b/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt
index 0b775ab..820c102 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt
+++ b/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt
@@ -35,6 +35,8 @@
import com.android.systemui.scene.shared.flag.SceneContainerFlag
import com.android.systemui.shade.shared.flag.DualShade
import com.android.systemui.statusbar.notification.collection.SortBySectionTimeFlag
+import com.android.systemui.statusbar.notification.emptyshade.shared.ModesEmptyShadeFix
+import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefactor
import com.android.systemui.statusbar.notification.interruption.VisualInterruptionRefactor
import com.android.systemui.statusbar.notification.shared.NotificationAvalancheSuppression
import com.android.systemui.statusbar.notification.shared.NotificationMinimalismPrototype
@@ -56,6 +58,8 @@
NotificationAvalancheSuppression.token dependsOn VisualInterruptionRefactor.token
PriorityPeopleSection.token dependsOn SortBySectionTimeFlag.token
NotificationMinimalismPrototype.token dependsOn NotificationThrottleHun.token
+ ModesEmptyShadeFix.token dependsOn FooterViewRefactor.token
+ ModesEmptyShadeFix.token dependsOn modesUi
// SceneContainer dependencies
SceneContainerFlag.getFlagDependencies().forEach { (alpha, beta) -> alpha dependsOn beta }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationActivityStarter.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationActivityStarter.kt
similarity index 60%
rename from packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationActivityStarter.java
rename to packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationActivityStarter.kt
index ec3c7d0..0f93b5d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationActivityStarter.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationActivityStarter.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2018 The Android Open Source Project
+ * 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.
@@ -13,37 +13,36 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+package com.android.systemui.statusbar.notification
-package com.android.systemui.statusbar.notification;
-
-import android.content.Intent;
-import android.view.View;
-
-import com.android.systemui.statusbar.notification.collection.NotificationEntry;
-import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
+import android.content.Intent
+import android.view.View
+import com.android.systemui.statusbar.notification.collection.NotificationEntry
+import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow
/**
* Component responsible for handling actions on a notification which cause activites to start.
* (e.g. clicking on a notification, tapping on the settings icon in the notification guts)
*/
-public interface NotificationActivityStarter {
+interface NotificationActivityStarter {
/** Called when the user clicks on the notification bubble icon. */
- void onNotificationBubbleIconClicked(NotificationEntry entry);
+ fun onNotificationBubbleIconClicked(entry: NotificationEntry?)
/** Called when the user clicks on the surface of a notification. */
- void onNotificationClicked(NotificationEntry entry, ExpandableNotificationRow row);
+ fun onNotificationClicked(entry: NotificationEntry?, row: ExpandableNotificationRow?)
/** Called when the user clicks on a button in the notification guts which fires an intent. */
- void startNotificationGutsIntent(Intent intent, int appUid,
- ExpandableNotificationRow row);
+ fun startNotificationGutsIntent(intent: Intent?, appUid: Int, row: ExpandableNotificationRow?)
- /** Called when the user clicks "Manage" or "History" in the Shade. */
- void startHistoryIntent(View view, boolean showHistory);
+ /**
+ * Called when the user clicks "Manage" or "History" in the Shade, or the "No notifications"
+ * text.
+ */
+ fun startHistoryIntent(view: View?, showHistory: Boolean)
/** Called when the user succeed to drop notification to proper target view. */
- void onDragSuccess(NotificationEntry entry);
+ fun onDragSuccess(entry: NotificationEntry?)
- default boolean isCollapsingToShowActivityOverLockscreen() {
- return false;
- }
+ val isCollapsingToShowActivityOverLockscreen: Boolean
+ get() = false
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/emptyshade/shared/ModesEmptyShadeFix.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/emptyshade/shared/ModesEmptyShadeFix.kt
new file mode 100644
index 0000000..f1fc275
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/emptyshade/shared/ModesEmptyShadeFix.kt
@@ -0,0 +1,61 @@
+/*
+ * 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.systemui.statusbar.notification.emptyshade.shared
+
+import android.app.Flags
+import com.android.systemui.flags.FlagToken
+import com.android.systemui.flags.RefactorFlagUtils
+
+/** Helper for reading or using the modes_ui_empty_shade flag state. */
+@Suppress("NOTHING_TO_INLINE")
+object ModesEmptyShadeFix {
+ /** The aconfig flag name */
+ const val FLAG_NAME = Flags.FLAG_MODES_UI_EMPTY_SHADE
+
+ /** A token used for dependency declaration */
+ val token: FlagToken
+ get() = FlagToken(FLAG_NAME, isEnabled)
+
+ /** Is the refactor enabled */
+ @JvmStatic
+ inline val isEnabled
+ get() = Flags.modesUiEmptyShade()
+
+ /**
+ * Called to ensure code is only run when the flag is enabled. This protects users from the
+ * unintended behaviors caused by accidentally running new logic, while also crashing on an eng
+ * build to ensure that the refactor author catches issues in testing.
+ */
+ @JvmStatic
+ inline fun isUnexpectedlyInLegacyMode() =
+ RefactorFlagUtils.isUnexpectedlyInLegacyMode(isEnabled, FLAG_NAME)
+
+ /**
+ * Called to ensure code is only run when the flag is disabled. This will throw an exception if
+ * the flag is not enabled to ensure that the refactor author catches issues in testing.
+ * Caution!! Using this check incorrectly will cause crashes in nextfood builds!
+ */
+ @JvmStatic
+ inline fun assertInNewMode() = RefactorFlagUtils.assertInNewMode(isEnabled, FLAG_NAME)
+
+ /**
+ * Called to ensure code is only run when the flag is disabled. This will throw an exception if
+ * the flag is enabled to ensure that the refactor author catches issues in testing.
+ */
+ @JvmStatic
+ inline fun assertInLegacyMode() = RefactorFlagUtils.assertInLegacyMode(isEnabled, FLAG_NAME)
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/emptyshade/ui/view/EmptyShadeView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/emptyshade/ui/view/EmptyShadeView.java
index 850e944..73477da 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/emptyshade/ui/view/EmptyShadeView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/emptyshade/ui/view/EmptyShadeView.java
@@ -22,6 +22,7 @@
import android.content.Context;
import android.content.res.ColorStateList;
import android.content.res.Configuration;
+import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.view.View;
@@ -29,26 +30,71 @@
import androidx.annotation.NonNull;
+import com.android.systemui.animation.LaunchableView;
+import com.android.systemui.animation.LaunchableViewDelegate;
import com.android.systemui.res.R;
+import com.android.systemui.statusbar.notification.emptyshade.shared.ModesEmptyShadeFix;
import com.android.systemui.statusbar.notification.row.StackScrollerDecorView;
import com.android.systemui.statusbar.notification.stack.ExpandableViewState;
-public class EmptyShadeView extends StackScrollerDecorView {
+import kotlin.Unit;
+
+import java.util.Objects;
+
+public class EmptyShadeView extends StackScrollerDecorView implements LaunchableView {
private TextView mEmptyText;
private TextView mEmptyFooterText;
- private @StringRes int mText = R.string.empty_shade_text;
+ private @StringRes int mTextId = R.string.empty_shade_text;
+ private String mTextString;
- private @DrawableRes int mFooterIcon = R.drawable.ic_friction_lock_closed;
- private @StringRes int mFooterText = R.string.unlock_to_see_notif_text;
+ private @DrawableRes int mFooterIcon;
+ private @StringRes int mFooterText;
+ // This view is initially gone in the xml.
private @Visibility int mFooterVisibility = View.GONE;
private int mSize;
+ private LaunchableViewDelegate mLaunchableViewDelegate = new LaunchableViewDelegate(this,
+ visibility -> {
+ super.setVisibility(visibility);
+ return Unit.INSTANCE;
+ });
+
public EmptyShadeView(Context context, AttributeSet attrs) {
super(context, attrs);
mSize = getResources().getDimensionPixelSize(
R.dimen.notifications_unseen_footer_icon_size);
+ if (ModesEmptyShadeFix.isEnabled()) {
+ mTextString = getContext().getString(R.string.empty_shade_text);
+ } else {
+ // These will be set by the binder when appropriate if ModesEmptyShadeFix is on.
+ mFooterIcon = R.drawable.ic_friction_lock_closed;
+ mFooterText = R.string.unlock_to_see_notif_text;
+ }
+ }
+
+ @Override
+ public void setVisibility(int visibility) {
+ mLaunchableViewDelegate.setVisibility(visibility);
+ }
+
+ @Override
+ public void setShouldBlockVisibilityChanges(boolean block) {
+ /* check if */ ModesEmptyShadeFix.isUnexpectedlyInLegacyMode();
+ mLaunchableViewDelegate.setShouldBlockVisibilityChanges(block);
+ }
+
+ @Override
+ public void onActivityLaunchAnimationEnd() {
+ /* check if */ ModesEmptyShadeFix.isUnexpectedlyInLegacyMode();
+ }
+
+ @Override
+ @NonNull
+ public Rect getPaddingForLaunchAnimation() {
+ /* check if */ ModesEmptyShadeFix.isUnexpectedlyInLegacyMode();
+ return new Rect();
}
@Override
@@ -56,7 +102,11 @@
super.onConfigurationChanged(newConfig);
mSize = getResources().getDimensionPixelSize(
R.dimen.notifications_unseen_footer_icon_size);
- mEmptyText.setText(mText);
+ if (ModesEmptyShadeFix.isEnabled()) {
+ mEmptyText.setText(mTextString);
+ } else {
+ mEmptyText.setText(mTextId);
+ }
mEmptyFooterText.setVisibility(mFooterVisibility);
setFooterText(mFooterText);
setFooterIcon(mFooterIcon);
@@ -72,25 +122,45 @@
return findViewById(R.id.no_notifications_footer);
}
+ /** Update view colors. */
public void setTextColors(@ColorInt int onSurface, @ColorInt int onSurfaceVariant) {
mEmptyText.setTextColor(onSurfaceVariant);
mEmptyFooterText.setTextColor(onSurface);
mEmptyFooterText.setCompoundDrawableTintList(ColorStateList.valueOf(onSurface));
}
+ /** Set the resource ID for the main text shown by the view. */
public void setText(@StringRes int text) {
- mText = text;
- mEmptyText.setText(mText);
+ ModesEmptyShadeFix.assertInLegacyMode();
+ mTextId = text;
+ mEmptyText.setText(mTextId);
}
+ /** Set the string for the main text shown by the view. */
+ public void setText(String text) {
+ if (ModesEmptyShadeFix.isUnexpectedlyInLegacyMode() || Objects.equals(mTextString, text)) {
+ return;
+ }
+ mTextString = text;
+ mEmptyText.setText(text);
+ }
+
+ /** Visibility for the footer (the additional icon+text shown below the main text). */
public void setFooterVisibility(@Visibility int visibility) {
+ if (ModesEmptyShadeFix.isEnabled() && mFooterVisibility == visibility) {
+ return; // nothing to change
+ }
mFooterVisibility = visibility;
setSecondaryVisible(/* visible = */ visibility == View.VISIBLE,
/* animate = */false,
/* onAnimationEnded = */ null);
}
+ /** Text resource ID for the footer (the additional icon+text shown below the main text). */
public void setFooterText(@StringRes int text) {
+ if (ModesEmptyShadeFix.isEnabled() && mFooterText == text) {
+ return; // nothing to change
+ }
mFooterText = text;
if (text != 0) {
mEmptyFooterText.setText(mFooterText);
@@ -99,7 +169,11 @@
}
}
+ /** Icon resource ID for the footer (the additional icon+text shown below the main text). */
public void setFooterIcon(@DrawableRes int icon) {
+ if (ModesEmptyShadeFix.isEnabled() && mFooterIcon == icon) {
+ return; // nothing to change
+ }
mFooterIcon = icon;
Drawable drawable;
if (icon == 0) {
@@ -111,18 +185,24 @@
mEmptyFooterText.setCompoundDrawablesRelative(drawable, null, null, null);
}
+ /** Get resource ID for main text. */
@StringRes
public int getTextResource() {
- return mText;
+ ModesEmptyShadeFix.assertInLegacyMode();
+ return mTextId;
}
+ /** Get resource ID for footer text. */
@StringRes
public int getFooterTextResource() {
+ ModesEmptyShadeFix.assertInLegacyMode();
return mFooterText;
}
+ /** Get resource ID for footer icon. */
@DrawableRes
public int getFooterIconResource() {
+ ModesEmptyShadeFix.assertInLegacyMode();
return mFooterIcon;
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/emptyshade/ui/viewbinder/EmptyShadeViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/emptyshade/ui/viewbinder/EmptyShadeViewBinder.kt
new file mode 100644
index 0000000..102a11c
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/emptyshade/ui/viewbinder/EmptyShadeViewBinder.kt
@@ -0,0 +1,59 @@
+/*
+ * 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.systemui.statusbar.notification.emptyshade.ui.viewbinder
+
+import android.view.View
+import com.android.systemui.statusbar.notification.emptyshade.ui.view.EmptyShadeView
+import com.android.systemui.statusbar.notification.emptyshade.ui.viewmodel.EmptyShadeViewModel
+import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.launch
+
+object EmptyShadeViewBinder {
+ suspend fun bind(
+ view: EmptyShadeView,
+ viewModel: EmptyShadeViewModel,
+ launchNotificationSettings: View.OnClickListener,
+ launchNotificationHistory: View.OnClickListener,
+ ) = coroutineScope {
+ launch { viewModel.text.collect { view.setText(it) } }
+
+ launch {
+ viewModel.tappingShouldLaunchHistory.collect { shouldLaunchHistory ->
+ if (shouldLaunchHistory) {
+ view.setOnClickListener(launchNotificationHistory)
+ } else {
+ view.setOnClickListener(launchNotificationSettings)
+ }
+ }
+ }
+
+ launch { bindFooter(view, viewModel) }
+ }
+
+ private suspend fun bindFooter(view: EmptyShadeView, viewModel: EmptyShadeViewModel) =
+ coroutineScope {
+ // Bind the resource IDs
+ view.setFooterText(viewModel.footer.messageId)
+ view.setFooterIcon(viewModel.footer.iconId)
+
+ launch {
+ viewModel.footer.isVisible.collect { visible ->
+ view.setFooterVisibility(if (visible) View.VISIBLE else View.GONE)
+ }
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/emptyshade/ui/viewmodel/EmptyShadeViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/emptyshade/ui/viewmodel/EmptyShadeViewModel.kt
new file mode 100644
index 0000000..d5417e7
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/emptyshade/ui/viewmodel/EmptyShadeViewModel.kt
@@ -0,0 +1,128 @@
+/*
+ * 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.systemui.statusbar.notification.emptyshade.ui.viewmodel
+
+import android.content.Context
+import android.icu.text.MessageFormat
+import com.android.systemui.dump.DumpManager
+import com.android.systemui.modes.shared.ModesUi
+import com.android.systemui.res.R
+import com.android.systemui.shared.notifications.domain.interactor.NotificationSettingsInteractor
+import com.android.systemui.statusbar.notification.domain.interactor.SeenNotificationsInteractor
+import com.android.systemui.statusbar.notification.emptyshade.shared.ModesEmptyShadeFix
+import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefactor
+import com.android.systemui.statusbar.notification.footer.ui.viewmodel.FooterMessageViewModel
+import com.android.systemui.statusbar.policy.domain.interactor.ZenModeInteractor
+import com.android.systemui.util.kotlin.FlowDumperImpl
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+import java.util.Locale
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.map
+
+/**
+ * ViewModel for the empty shade (aka the "No notifications" text shown when there are no
+ * notifications.
+ */
+class EmptyShadeViewModel
+@AssistedInject
+constructor(
+ private val context: Context,
+ zenModeInteractor: ZenModeInteractor,
+ seenNotificationsInteractor: SeenNotificationsInteractor,
+ notificationSettingsInteractor: NotificationSettingsInteractor,
+ dumpManager: DumpManager,
+) : FlowDumperImpl(dumpManager) {
+ val areNotificationsHiddenInShade: Flow<Boolean> by lazy {
+ if (FooterViewRefactor.isUnexpectedlyInLegacyMode()) {
+ flowOf(false)
+ } else {
+ zenModeInteractor.areNotificationsHiddenInShade.dumpWhileCollecting(
+ "areNotificationsHiddenInShade"
+ )
+ }
+ }
+
+ val hasFilteredOutSeenNotifications: StateFlow<Boolean> by lazy {
+ if (FooterViewRefactor.isUnexpectedlyInLegacyMode()) {
+ MutableStateFlow(false)
+ } else {
+ seenNotificationsInteractor.hasFilteredOutSeenNotifications.dumpValue(
+ "hasFilteredOutSeenNotifications"
+ )
+ }
+ }
+
+ val text: Flow<String> by lazy {
+ if (ModesEmptyShadeFix.isUnexpectedlyInLegacyMode()) {
+ flowOf(context.getString(R.string.empty_shade_text))
+ } else {
+ // Note: Flag modes_ui_empty_shade includes two pieces: refactoring the empty shade to
+ // recommended architecture, and making it so it reacts to changes for the new Modes.
+ // The former does not depend on the modes flags being on, but the latter does.
+ if (ModesUi.isEnabled) {
+ zenModeInteractor.modesHidingNotifications.map { modes ->
+ // Create a string that is either "No notifications" if no modes are filtering
+ // them
+ // out, or something like "Notifications paused by SomeMode" otherwise.
+ val msgFormat =
+ MessageFormat(
+ context.getString(R.string.modes_suppressing_shade_text),
+ Locale.getDefault(),
+ )
+ val count = modes.count()
+ val args: MutableMap<String, Any> = HashMap()
+ args["count"] = count
+ if (count >= 1) {
+ args["mode"] = modes[0].name
+ }
+ msgFormat.format(args)
+ }
+ } else {
+ areNotificationsHiddenInShade.map { areNotificationsHiddenInShade ->
+ if (areNotificationsHiddenInShade) {
+ context.getString(R.string.dnd_suppressing_shade_text)
+ } else {
+ context.getString(R.string.empty_shade_text)
+ }
+ }
+ }
+ }
+ }
+
+ val footer: FooterMessageViewModel by lazy {
+ ModesEmptyShadeFix.assertInNewMode()
+ FooterMessageViewModel(
+ messageId = R.string.unlock_to_see_notif_text,
+ iconId = R.drawable.ic_friction_lock_closed,
+ isVisible = hasFilteredOutSeenNotifications,
+ )
+ }
+
+ val tappingShouldLaunchHistory by lazy {
+ ModesEmptyShadeFix.assertInNewMode()
+ notificationSettingsInteractor.isNotificationHistoryEnabled
+ }
+
+ @AssistedFactory
+ interface Factory {
+ fun create(): EmptyShadeViewModel
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/StackScrollerDecorView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/StackScrollerDecorView.java
index 291dc13..cd228e7 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/StackScrollerDecorView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/StackScrollerDecorView.java
@@ -72,14 +72,24 @@
}
/**
+ * See {@link #setVisible(boolean, boolean, Consumer)}.
+ */
+ public void setVisible(boolean visible, boolean animate) {
+ setVisible(visible, animate, null /* onAnimationEnded */);
+ }
+
+ /**
* Make this view visible. If {@code false} is passed, the view will fade out its content
* and set the view Visibility to GONE. If only the content should be changed,
* {@link #setContentVisibleAnimated(boolean)} can be used.
*
* @param visible True if the contents should be visible.
* @param animate True if we should fade to new visibility.
+ * @param onAnimationEnded Callback to run after visibility updates, takes a boolean as a
+ * parameter that represents whether the animation was cancelled.
*/
- public void setVisible(boolean visible, boolean animate) {
+ public void setVisible(boolean visible, boolean animate,
+ Consumer<Boolean> onAnimationEnded) {
if (mIsVisible != visible) {
mIsVisible = visible;
if (animate) {
@@ -90,10 +100,10 @@
} else {
setWillBeGone(true);
}
- setContentVisible(visible, true /* animate */, null /* onAnimationEnded */);
+ setContentVisible(visible, true /* animate */, onAnimationEnded);
} else {
setVisibility(visible ? VISIBLE : GONE);
- setContentVisible(visible, false /* animate */, null /* onAnimationEnded */);
+ setContentVisible(visible, false /* animate */, onAnimationEnded);
setWillBeGone(false);
notifyHeightChanged(false /* needsAnimation */);
}
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 cd3516d..b2b2c2a 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
@@ -104,6 +104,7 @@
import com.android.systemui.statusbar.notification.collection.NotificationEntry;
import com.android.systemui.statusbar.notification.collection.render.GroupExpansionManager;
import com.android.systemui.statusbar.notification.collection.render.GroupMembershipManager;
+import com.android.systemui.statusbar.notification.emptyshade.shared.ModesEmptyShadeFix;
import com.android.systemui.statusbar.notification.emptyshade.ui.view.EmptyShadeView;
import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefactor;
import com.android.systemui.statusbar.notification.footer.ui.view.FooterView;
@@ -686,7 +687,9 @@
protected void onFinishInflate() {
super.onFinishInflate();
- inflateEmptyShadeView();
+ if (!ModesEmptyShadeFix.isEnabled()) {
+ inflateEmptyShadeView();
+ }
if (!FooterViewRefactor.isEnabled()) {
inflateFooterView();
}
@@ -729,7 +732,9 @@
inflateFooterView();
updateFooter();
}
- inflateEmptyShadeView();
+ if (!ModesEmptyShadeFix.isEnabled()) {
+ inflateEmptyShadeView();
+ }
mSectionsManager.reinflateViews();
}
@@ -4835,6 +4840,8 @@
/** Trigger an update for the empty shade resources and visibility. */
public void updateEmptyShadeView(boolean visible, boolean areNotificationsHiddenInShade,
boolean hasFilteredOutSeenNotifications) {
+ ModesEmptyShadeFix.assertInLegacyMode();
+
mEmptyShadeView.setVisible(visible, mIsExpanded && mAnimationsEnabled);
if (areNotificationsHiddenInShade) {
@@ -4853,6 +4860,8 @@
@StringRes int newTextRes,
@StringRes int newFooterTextRes,
@DrawableRes int newFooterIconRes) {
+ ModesEmptyShadeFix.assertInLegacyMode();
+
int oldTextRes = mEmptyShadeView.getTextResource();
if (oldTextRes != newTextRes) {
mEmptyShadeView.setText(newTextRes);
@@ -4874,6 +4883,9 @@
public boolean isEmptyShadeViewVisible() {
SceneContainerFlag.assertInLegacyMode();
+ if (mEmptyShadeView == null) {
+ return false;
+ }
return mEmptyShadeView.isVisible();
}
@@ -5361,7 +5373,7 @@
public float getOpeningHeight() {
SceneContainerFlag.assertInLegacyMode();
- if (mEmptyShadeView.getVisibility() == GONE) {
+ if (mEmptyShadeView == null || mEmptyShadeView.getVisibility() == GONE) {
return getMinExpansionHeight();
} else {
return FooterViewRefactor.isEnabled() ? getAppearEndPosition()
@@ -5710,6 +5722,8 @@
}
private void inflateEmptyShadeView() {
+ ModesEmptyShadeFix.assertInLegacyMode();
+
EmptyShadeView oldView = mEmptyShadeView;
EmptyShadeView view = (EmptyShadeView) LayoutInflater.from(mContext).inflate(
R.layout.status_bar_no_notifications, this, false);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt
index dc9615c..3dad326 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt
@@ -17,6 +17,7 @@
package com.android.systemui.statusbar.notification.stack.ui.viewbinder
import android.view.LayoutInflater
+import android.view.View
import androidx.lifecycle.lifecycleScope
import com.android.app.tracing.TraceUtils.traceAsync
import com.android.internal.logging.MetricsLogger
@@ -25,6 +26,7 @@
import com.android.systemui.common.ui.view.setImportantForAccessibilityYesNo
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.lifecycle.repeatWhenAttached
+import com.android.systemui.lifecycle.repeatWhenAttachedToWindow
import com.android.systemui.plugins.FalsingManager
import com.android.systemui.res.R
import com.android.systemui.scene.shared.flag.SceneContainerFlag
@@ -32,6 +34,10 @@
import com.android.systemui.statusbar.notification.NotificationActivityStarter
import com.android.systemui.statusbar.notification.collection.render.SectionHeaderController
import com.android.systemui.statusbar.notification.dagger.SilentHeader
+import com.android.systemui.statusbar.notification.emptyshade.shared.ModesEmptyShadeFix
+import com.android.systemui.statusbar.notification.emptyshade.ui.view.EmptyShadeView
+import com.android.systemui.statusbar.notification.emptyshade.ui.viewbinder.EmptyShadeViewBinder
+import com.android.systemui.statusbar.notification.emptyshade.ui.viewmodel.EmptyShadeViewModel
import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefactor
import com.android.systemui.statusbar.notification.footer.ui.view.FooterView
import com.android.systemui.statusbar.notification.footer.ui.viewbinder.FooterViewBinder
@@ -49,6 +55,7 @@
import com.android.systemui.util.kotlin.awaitCancellationThenDispose
import com.android.systemui.util.kotlin.getOrNull
import com.android.systemui.util.ui.isAnimating
+import com.android.systemui.util.ui.stopAnimating
import com.android.systemui.util.ui.value
import java.util.Optional
import javax.inject.Inject
@@ -84,7 +91,7 @@
fun bindWhileAttached(
view: NotificationStackScrollLayout,
- viewController: NotificationStackScrollLayoutController
+ viewController: NotificationStackScrollLayoutController,
) {
val shelf =
LayoutInflater.from(view.context)
@@ -103,7 +110,13 @@
val hasNonClearableSilentNotifications: StateFlow<Boolean> =
viewModel.hasNonClearableSilentNotifications.stateIn(this)
launch { reinflateAndBindFooter(view, hasNonClearableSilentNotifications) }
- launch { bindEmptyShade(view) }
+ launch {
+ if (ModesEmptyShadeFix.isEnabled) {
+ reinflateAndBindEmptyShade(view)
+ } else {
+ bindEmptyShadeLegacy(viewModel.emptyShadeViewFactory.create(), view)
+ }
+ }
launch {
bindSilentHeaderClickListener(view, hasNonClearableSilentNotifications)
}
@@ -121,17 +134,12 @@
}
private suspend fun bindShelf(shelf: NotificationShelf) {
- NotificationShelfViewBinder.bind(
- shelf,
- viewModel.shelf,
- falsingManager,
- nicBinder,
- )
+ NotificationShelfViewBinder.bind(shelf, viewModel.shelf, falsingManager, nicBinder)
}
private suspend fun reinflateAndBindFooter(
parentView: NotificationStackScrollLayout,
- hasNonClearableSilentNotifications: StateFlow<Boolean>
+ hasNonClearableSilentNotifications: StateFlow<Boolean>,
) {
viewModel.footer.getOrNull()?.let { footerViewModel ->
// The footer needs to be re-inflated every time the theme or the font size changes.
@@ -149,7 +157,7 @@
footerView,
footerViewModel,
parentView,
- hasNonClearableSilentNotifications
+ hasNonClearableSilentNotifications,
)
}
}
@@ -163,13 +171,13 @@
footerView: FooterView,
footerViewModel: FooterViewModel,
parentView: NotificationStackScrollLayout,
- hasNonClearableSilentNotifications: StateFlow<Boolean>
+ hasNonClearableSilentNotifications: StateFlow<Boolean>,
): Unit = coroutineScope {
val disposableHandle =
FooterViewBinder.bindWhileAttached(
footerView,
footerViewModel,
- clearAllNotifications = {
+ {
clearAllNotifications(
parentView,
// Hide the silent section header (if present) if there will be
@@ -177,16 +185,8 @@
hideSilentSection = !hasNonClearableSilentNotifications.value,
)
},
- launchNotificationSettings = { view ->
- notificationActivityStarter
- .get()
- .startHistoryIntent(view, /* showHistory= */ false)
- },
- launchNotificationHistory = { view ->
- notificationActivityStarter
- .get()
- .startHistoryIntent(view, /* showHistory= */ true)
- },
+ launchNotificationSettings,
+ launchNotificationHistory,
)
if (SceneContainerFlag.isEnabled) {
launch {
@@ -194,7 +194,9 @@
footerView.setVisible(
/* visible = */ animatedVisibility.value,
/* animate = */ animatedVisibility.isAnimating,
- )
+ ) {
+ animatedVisibility.stopAnimating()
+ }
}
}
} else {
@@ -211,20 +213,71 @@
disposableHandle.awaitCancellationThenDispose()
}
- private suspend fun bindEmptyShade(parentView: NotificationStackScrollLayout) {
+ private val launchNotificationSettings: (View) -> Unit = { view: View ->
+ notificationActivityStarter.get().startHistoryIntent(view, /* showHistory= */ false)
+ }
+
+ private val launchNotificationHistory: (View) -> Unit = { view ->
+ notificationActivityStarter.get().startHistoryIntent(view, /* showHistory= */ true)
+ }
+
+ private suspend fun reinflateAndBindEmptyShade(parentView: NotificationStackScrollLayout) {
+ ModesEmptyShadeFix.assertInNewMode()
+ // The empty shade needs to be re-inflated every time the theme or the font size
+ // changes.
+ configuration
+ .inflateLayout<EmptyShadeView>(
+ R.layout.status_bar_no_notifications,
+ parentView,
+ attachToRoot = false,
+ )
+ .flowOn(backgroundDispatcher)
+ .collectLatest { emptyShadeView: EmptyShadeView ->
+ traceAsync("bind EmptyShadeView") {
+ parentView.setEmptyShadeView(emptyShadeView)
+ bindEmptyShade(emptyShadeView, viewModel.emptyShadeViewFactory.create())
+ }
+ }
+ }
+
+ private suspend fun bindEmptyShadeLegacy(
+ emptyShadeViewModel: EmptyShadeViewModel,
+ parentView: NotificationStackScrollLayout,
+ ) {
+ ModesEmptyShadeFix.assertInLegacyMode()
combine(
viewModel.shouldShowEmptyShadeView,
- viewModel.areNotificationsHiddenInShade,
- viewModel.hasFilteredOutSeenNotifications,
- ::Triple
+ emptyShadeViewModel.areNotificationsHiddenInShade,
+ emptyShadeViewModel.hasFilteredOutSeenNotifications,
+ ::Triple,
)
.collect { (shouldShow, areNotifsHidden, hasFilteredNotifs) ->
- parentView.updateEmptyShadeView(
- shouldShow,
- areNotifsHidden,
- hasFilteredNotifs,
+ parentView.updateEmptyShadeView(shouldShow, areNotifsHidden, hasFilteredNotifs)
+ }
+ }
+
+ private suspend fun bindEmptyShade(
+ emptyShadeView: EmptyShadeView,
+ emptyShadeViewModel: EmptyShadeViewModel,
+ ): Unit = coroutineScope {
+ ModesEmptyShadeFix.assertInNewMode()
+ launch {
+ emptyShadeView.repeatWhenAttachedToWindow {
+ EmptyShadeViewBinder.bind(
+ emptyShadeView,
+ emptyShadeViewModel,
+ launchNotificationSettings,
+ launchNotificationHistory,
)
}
+ }
+ launch {
+ viewModel.shouldShowEmptyShadeViewAnimated.collect { shouldShow ->
+ emptyShadeView.setVisible(shouldShow.value, shouldShow.isAnimating) {
+ shouldShow.stopAnimating()
+ }
+ }
+ }
}
private suspend fun bindSilentHeaderClickListener(
@@ -261,7 +314,7 @@
private fun clearSilentNotifications(
view: NotificationStackScrollLayout,
closeShade: Boolean,
- hideSilentSection: Boolean
+ hideSilentSection: Boolean,
) {
view.clearSilentNotifications(closeShade, hideSilentSection)
}
@@ -270,11 +323,7 @@
if (NotificationsLiveDataStoreRefactor.isEnabled) {
viewModel.logger.getOrNull()?.let { viewModel ->
loggerOptional.getOrNull()?.let { logger ->
- NotificationStatsLoggerBinder.bindLogger(
- view,
- logger,
- viewModel,
- )
+ NotificationStatsLoggerBinder.bindLogger(view, logger, viewModel)
}
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModel.kt
index 4e2a46d..935e2a3 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModel.kt
@@ -23,14 +23,14 @@
import com.android.systemui.statusbar.domain.interactor.RemoteInputInteractor
import com.android.systemui.statusbar.notification.domain.interactor.ActiveNotificationsInteractor
import com.android.systemui.statusbar.notification.domain.interactor.HeadsUpNotificationInteractor
-import com.android.systemui.statusbar.notification.domain.interactor.SeenNotificationsInteractor
+import com.android.systemui.statusbar.notification.emptyshade.shared.ModesEmptyShadeFix
+import com.android.systemui.statusbar.notification.emptyshade.ui.viewmodel.EmptyShadeViewModel
import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefactor
import com.android.systemui.statusbar.notification.footer.ui.viewmodel.FooterViewModel
import com.android.systemui.statusbar.notification.shared.HeadsUpRowKey
import com.android.systemui.statusbar.notification.shelf.ui.viewmodel.NotificationShelfViewModel
import com.android.systemui.statusbar.notification.stack.domain.interactor.NotificationStackInteractor
import com.android.systemui.statusbar.policy.domain.interactor.UserSetupInteractor
-import com.android.systemui.statusbar.policy.domain.interactor.ZenModeInteractor
import com.android.systemui.util.kotlin.FlowDumperImpl
import com.android.systemui.util.kotlin.combine
import com.android.systemui.util.kotlin.sample
@@ -48,22 +48,24 @@
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onStart
-/** ViewModel for the list of notifications. */
+/**
+ * ViewModel for the list of notifications, including child elements like the Clear all/Manage
+ * button at the bottom (the footer) and the "No notifications" text (the empty shade).
+ */
class NotificationListViewModel
@Inject
constructor(
val shelf: NotificationShelfViewModel,
val hideListViewModel: HideListViewModel,
val footer: Optional<FooterViewModel>,
+ val emptyShadeViewFactory: EmptyShadeViewModel.Factory,
val logger: Optional<NotificationLoggerViewModel>,
activeNotificationsInteractor: ActiveNotificationsInteractor,
notificationStackInteractor: NotificationStackInteractor,
private val headsUpNotificationInteractor: HeadsUpNotificationInteractor,
remoteInputInteractor: RemoteInputInteractor,
- seenNotificationsInteractor: SeenNotificationsInteractor,
shadeInteractor: ShadeInteractor,
userSetupInteractor: UserSetupInteractor,
- zenModeInteractor: ZenModeInteractor,
@Background bgDispatcher: CoroutineDispatcher,
dumpManager: DumpManager,
) : FlowDumperImpl(dumpManager) {
@@ -90,6 +92,7 @@
}
val shouldShowEmptyShadeView: Flow<Boolean> by lazy {
+ ModesEmptyShadeFix.assertInLegacyMode()
if (FooterViewRefactor.isUnexpectedlyInLegacyMode()) {
flowOf(false)
} else {
@@ -114,6 +117,45 @@
}
}
+ val shouldShowEmptyShadeViewAnimated: Flow<AnimatedValue<Boolean>> by lazy {
+ if (ModesEmptyShadeFix.isUnexpectedlyInLegacyMode()) {
+ flowOf(AnimatedValue.NotAnimating(false))
+ } else {
+ combine(
+ activeNotificationsInteractor.areAnyNotificationsPresent,
+ shadeInteractor.isQsFullscreen,
+ notificationStackInteractor.isShowingOnLockscreen,
+ ) { hasNotifications, isQsFullScreen, isShowingOnLockscreen ->
+ when {
+ hasNotifications -> false
+ isQsFullScreen -> false
+ // Do not show the empty shade if the lockscreen is visible (including AOD
+ // b/228790482 and bouncer b/267060171), except if the shade is opened on
+ // top.
+ isShowingOnLockscreen -> false
+ else -> true
+ }
+ }
+ .distinctUntilChanged()
+ .sample(
+ // TODO(b/322167853): This check is currently duplicated in FooterViewModel
+ // but instead it should be a field in ShadeAnimationInteractor.
+ combine(
+ shadeInteractor.isShadeFullyExpanded,
+ shadeInteractor.isShadeTouchable,
+ ::Pair,
+ )
+ .onStart { emit(Pair(false, false)) }
+ ) { visible, (isShadeFullyExpanded, animationsEnabled) ->
+ val shouldAnimate = isShadeFullyExpanded && animationsEnabled
+ AnimatableEvent(visible, shouldAnimate)
+ }
+ .toAnimatedValueFlow()
+ .dumpWhileCollecting("shouldShowEmptyShadeViewAnimated")
+ .flowOn(bgDispatcher)
+ }
+ }
+
/**
* Whether the footer should not be visible for the user, even if it's present in the list (as
* per [shouldIncludeFooterView] below).
@@ -154,7 +196,7 @@
userSetupInteractor.isUserSetUp,
notificationStackInteractor.isShowingOnLockscreen,
shadeInteractor.isQsFullscreen,
- remoteInputInteractor.isRemoteInputActive
+ remoteInputInteractor.isRemoteInputActive,
) {
hasNotifications,
isUserSetUp,
@@ -193,7 +235,7 @@
combine(
shadeInteractor.isShadeFullyExpanded,
shadeInteractor.isShadeTouchable,
- ::Pair
+ ::Pair,
)
.onStart { emit(Pair(false, false)) }
) { visibilityChange, (isShadeFullyExpanded, animationsEnabled) ->
@@ -263,7 +305,7 @@
combine(
shadeInteractor.isShadeFullyExpanded,
shadeInteractor.isShadeTouchable,
- ::Pair
+ ::Pair,
)
.onStart { emit(Pair(false, false)) }
) { visibilityChange, (isShadeFullyExpanded, animationsEnabled) ->
@@ -283,29 +325,7 @@
enum class VisibilityChange(val visible: Boolean, val canAnimate: Boolean) {
DISAPPEAR_WITHOUT_ANIMATION(visible = false, canAnimate = false),
DISAPPEAR_WITH_ANIMATION(visible = false, canAnimate = true),
- APPEAR_WITH_ANIMATION(visible = true, canAnimate = true)
- }
-
- // TODO(b/308591475): This should be tracked separately by the empty shade.
- val areNotificationsHiddenInShade: Flow<Boolean> by lazy {
- if (FooterViewRefactor.isUnexpectedlyInLegacyMode()) {
- flowOf(false)
- } else {
- zenModeInteractor.areNotificationsHiddenInShade.dumpWhileCollecting(
- "areNotificationsHiddenInShade"
- )
- }
- }
-
- // TODO(b/308591475): This should be tracked separately by the empty shade.
- val hasFilteredOutSeenNotifications: Flow<Boolean> by lazy {
- if (FooterViewRefactor.isUnexpectedlyInLegacyMode()) {
- flowOf(false)
- } else {
- seenNotificationsInteractor.hasFilteredOutSeenNotifications.dumpWhileCollecting(
- "hasFilteredOutSeenNotifications"
- )
- }
+ APPEAR_WITH_ANIMATION(visible = true, canAnimate = true),
}
val hasClearableAlertingNotifications: Flow<Boolean> by lazy {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractor.kt
index ba45942..daba109 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractor.kt
@@ -20,6 +20,7 @@
import android.provider.Settings
import android.provider.Settings.Secure.ZEN_DURATION_FOREVER
import android.provider.Settings.Secure.ZEN_DURATION_PROMPT
+import android.service.notification.ZenPolicy.VISUAL_EFFECT_NOTIFICATION_LIST
import android.util.Log
import androidx.concurrent.futures.await
import com.android.settingslib.notification.data.repository.ZenModeRepository
@@ -29,6 +30,7 @@
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.modes.shared.ModesUi
import com.android.systemui.shared.notifications.data.repository.NotificationSettingsRepository
+import com.android.systemui.statusbar.notification.emptyshade.shared.ModesEmptyShadeFix
import com.android.systemui.statusbar.policy.data.repository.DeviceProvisioningRepository
import com.android.systemui.statusbar.policy.data.repository.UserSetupRepository
import com.android.systemui.statusbar.policy.domain.model.ActiveZenModes
@@ -39,6 +41,7 @@
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
@@ -54,8 +57,8 @@
private val notificationSettingsRepository: NotificationSettingsRepository,
@Background private val bgDispatcher: CoroutineDispatcher,
private val iconLoader: ZenIconLoader,
- private val deviceProvisioningRepository: DeviceProvisioningRepository,
- private val userSetupRepository: UserSetupRepository,
+ deviceProvisioningRepository: DeviceProvisioningRepository,
+ userSetupRepository: UserSetupRepository,
) {
val isZenAvailable: Flow<Boolean> =
combine(
@@ -126,6 +129,25 @@
val mainActiveMode: Flow<ZenModeInfo?> =
activeModes.map { a -> a.mainMode }.distinctUntilChanged()
+ val modesHidingNotifications: Flow<List<ZenMode>> by lazy {
+ if (ModesEmptyShadeFix.isUnexpectedlyInLegacyMode() || !ModesUi.isEnabled) {
+ flowOf(listOf())
+ } else {
+ modes
+ .map { modes ->
+ modes.filter { mode ->
+ mode.isActive &&
+ !mode.policy.isVisualEffectAllowed(
+ /* effect = */ VISUAL_EFFECT_NOTIFICATION_LIST,
+ /* defaultVal = */ true,
+ )
+ }
+ }
+ .flowOn(bgDispatcher)
+ .distinctUntilChanged()
+ }
+ }
+
suspend fun getModeIcon(mode: ZenMode): ZenIcon {
return iconLoader.getIcon(context, mode).await()
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/emptyshade/ui/viewmodel/EmptyShadeViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/emptyshade/ui/viewmodel/EmptyShadeViewModelKosmos.kt
new file mode 100644
index 0000000..8fdb948
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/emptyshade/ui/viewmodel/EmptyShadeViewModelKosmos.kt
@@ -0,0 +1,42 @@
+/*
+ * 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.systemui.statusbar.notification.emptyshade.ui.viewmodel
+
+import android.content.applicationContext
+import com.android.systemui.dump.dumpManager
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.shared.notifications.domain.interactor.notificationSettingsInteractor
+import com.android.systemui.statusbar.notification.domain.interactor.seenNotificationsInteractor
+import com.android.systemui.statusbar.policy.domain.interactor.zenModeInteractor
+
+val Kosmos.emptyShadeViewModel by
+ Kosmos.Fixture {
+ EmptyShadeViewModel(
+ applicationContext,
+ zenModeInteractor,
+ seenNotificationsInteractor,
+ notificationSettingsInteractor,
+ dumpManager,
+ )
+ }
+
+val Kosmos.emptyShadeViewModelFactory: EmptyShadeViewModel.Factory by
+ Kosmos.Fixture {
+ object : EmptyShadeViewModel.Factory {
+ override fun create() = emptyShadeViewModel
+ }
+ }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelKosmos.kt
index de8b350..c3bc744 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelKosmos.kt
@@ -23,13 +23,12 @@
import com.android.systemui.shade.domain.interactor.shadeInteractor
import com.android.systemui.statusbar.domain.interactor.remoteInputInteractor
import com.android.systemui.statusbar.notification.domain.interactor.activeNotificationsInteractor
-import com.android.systemui.statusbar.notification.domain.interactor.seenNotificationsInteractor
+import com.android.systemui.statusbar.notification.emptyshade.ui.viewmodel.emptyShadeViewModelFactory
import com.android.systemui.statusbar.notification.footer.ui.viewmodel.footerViewModel
import com.android.systemui.statusbar.notification.shelf.ui.viewmodel.notificationShelfViewModel
import com.android.systemui.statusbar.notification.stack.domain.interactor.headsUpNotificationInteractor
import com.android.systemui.statusbar.notification.stack.domain.interactor.notificationStackInteractor
import com.android.systemui.statusbar.policy.domain.interactor.userSetupInteractor
-import com.android.systemui.statusbar.policy.domain.interactor.zenModeInteractor
import java.util.Optional
val Kosmos.notificationListViewModel by Fixture {
@@ -37,15 +36,14 @@
notificationShelfViewModel,
hideListViewModel,
Optional.of(footerViewModel),
+ emptyShadeViewModelFactory,
Optional.of(notificationListLoggerViewModel),
activeNotificationsInteractor,
notificationStackInteractor,
headsUpNotificationInteractor,
remoteInputInteractor,
- seenNotificationsInteractor,
shadeInteractor,
userSetupInteractor,
- zenModeInteractor,
testDispatcher,
dumpManager,
)