Add notification drawer visibility controls

This commit is contained in:
ajp_anton
2026-09-14 10:02:08 +00:00
parent eef433ce88
commit 32d0935389
16 changed files with 195 additions and 55 deletions
+5 -4
View File
@@ -58,10 +58,11 @@ force direct custom sound or vibration through Do Not Disturb.
## Notification visibility ## Notification visibility
The optional LSPosed integration can hide an app's notification icons The optional LSPosed integration can hide an app's notifications independently
independently from the unlocked status bar, collapsed lockscreen, and from the unlocked status bar, collapsed lockscreen, always-on display, and
always-on display. Text exceptions can override an app's default on individual notification drawer. The drawer choice is shared between its locked and
notifications. Pull-down notification cards remain available. unlocked forms. Text exceptions can override an app's default for individual
notifications; hiding a drawer card does not dismiss the notification.
On OneUI, Notifications Master can also lay out the extra notification icons On OneUI, Notifications Master can also lay out the extra notification icons
in Cards mode, with separate lockscreen and always-on-display controls for row in Cards mode, with separate lockscreen and always-on-display controls for row
@@ -21,6 +21,13 @@ class VisibilityModuleFixtureTest {
)))) ))))
} }
@Test fun blockHelperInDrawerOnly() {
requireFixtureMode()
store.save(VisibilityPolicy(apps = listOf(
AppVisibilityPolicy(HELPER_PACKAGE, setOf(NotificationSurface.DRAWER)),
)))
}
@Test fun allowHelper() { @Test fun allowHelper() {
requireFixtureMode() requireFixtureMode()
store.save(VisibilityPolicy()) store.save(VisibilityPolicy())
@@ -85,6 +85,7 @@ class AppVisibilityFragment : Fragment(R.layout.fragment_app_visibility) {
bindSurface(binding!!.showAod, NotificationSurface.AOD, policy) bindSurface(binding!!.showAod, NotificationSurface.AOD, policy)
bindSurface(binding!!.showLockscreen, NotificationSurface.LOCKSCREEN_COLLAPSED, policy) bindSurface(binding!!.showLockscreen, NotificationSurface.LOCKSCREEN_COLLAPSED, policy)
bindSurface(binding!!.showUnlocked, NotificationSurface.UNLOCKED_STATUSBAR, policy) bindSurface(binding!!.showUnlocked, NotificationSurface.UNLOCKED_STATUSBAR, policy)
bindSurface(binding!!.showDrawer, NotificationSurface.DRAWER, policy)
adapter.submit(policy.exceptions) adapter.submit(policy.exceptions)
} }
@@ -204,6 +205,7 @@ class AppVisibilityFragment : Fragment(R.layout.fragment_app_visibility) {
rule, rule,
NotificationSurface.UNLOCKED_STATUSBAR, NotificationSurface.UNLOCKED_STATUSBAR,
) )
bindSurface(binding.drawerIcon, binding.drawerCross, rule, NotificationSurface.DRAWER)
binding.aodCell.setOnClickListener { toggle(holder, NotificationSurface.AOD) } binding.aodCell.setOnClickListener { toggle(holder, NotificationSurface.AOD) }
binding.lockscreenCell.setOnClickListener { binding.lockscreenCell.setOnClickListener {
toggle(holder, NotificationSurface.LOCKSCREEN_COLLAPSED) toggle(holder, NotificationSurface.LOCKSCREEN_COLLAPSED)
@@ -211,6 +213,7 @@ class AppVisibilityFragment : Fragment(R.layout.fragment_app_visibility) {
binding.unlockedCell.setOnClickListener { binding.unlockedCell.setOnClickListener {
toggle(holder, NotificationSurface.UNLOCKED_STATUSBAR) toggle(holder, NotificationSurface.UNLOCKED_STATUSBAR)
} }
binding.drawerCell.setOnClickListener { toggle(holder, NotificationSurface.DRAWER) }
binding.edit.setOnClickListener { holder.positionOrNull()?.let(edit) } binding.edit.setOnClickListener { holder.positionOrNull()?.let(edit) }
binding.delete.setOnClickListener { holder.positionOrNull()?.let(delete) } binding.delete.setOnClickListener { holder.positionOrNull()?.let(delete) }
binding.dragHandle.setOnTouchListener { _, event -> binding.dragHandle.setOnTouchListener { _, event ->
@@ -188,6 +188,12 @@ class NotificationVisibilityFragment : Fragment(R.layout.fragment_notification_v
row, row,
NotificationSurface.UNLOCKED_STATUSBAR, NotificationSurface.UNLOCKED_STATUSBAR,
) )
bindSurface(
binding.visibilityDrawerIcon,
binding.visibilityDrawerCross,
row,
NotificationSurface.DRAWER,
)
binding.visibilityAodCell.setOnClickListener { toggle(row, NotificationSurface.AOD) } binding.visibilityAodCell.setOnClickListener { toggle(row, NotificationSurface.AOD) }
binding.visibilityLockscreenCell.setOnClickListener { binding.visibilityLockscreenCell.setOnClickListener {
toggle(row, NotificationSurface.LOCKSCREEN_COLLAPSED) toggle(row, NotificationSurface.LOCKSCREEN_COLLAPSED)
@@ -195,6 +201,9 @@ class NotificationVisibilityFragment : Fragment(R.layout.fragment_notification_v
binding.visibilityUnlockedCell.setOnClickListener { binding.visibilityUnlockedCell.setOnClickListener {
toggle(row, NotificationSurface.UNLOCKED_STATUSBAR) toggle(row, NotificationSurface.UNLOCKED_STATUSBAR)
} }
binding.visibilityDrawerCell.setOnClickListener {
toggle(row, NotificationSurface.DRAWER)
}
binding.visibilityAppRoot.setOnClickListener { open(row.app.app) } binding.visibilityAppRoot.setOnClickListener { open(row.app.app) }
} }
@@ -12,7 +12,7 @@ import java.lang.reflect.Method
import java.util.Collections import java.util.Collections
import java.util.WeakHashMap import java.util.WeakHashMap
/** Hides blocked rows only in AOSP's collapsed lockscreen notification stack. */ /** Filters AOSP notification rows for the collapsed lockscreen or notification drawer. */
internal class AospLockscreenBackend( internal class AospLockscreenBackend(
private val framework: XposedInterface, private val framework: XposedInterface,
) : VisibilitySurfaceBackend { ) : VisibilitySurfaceBackend {
@@ -21,6 +21,7 @@ internal class AospLockscreenBackend(
private val hiddenRows = WeakHashMap<View, RowState>() private val hiddenRows = WeakHashMap<View, RowState>()
private val groupOrders = WeakHashMap<View, List<View>>() private val groupOrders = WeakHashMap<View, List<View>>()
private lateinit var onKeyguard: Method private lateinit var onKeyguard: Method
private var stackExpanded: Field? = null
private lateinit var entryForRow: (View) -> Any? private lateinit var entryForRow: (View) -> Any?
private lateinit var sbnForEntry: (Any) -> StatusBarNotification? private lateinit var sbnForEntry: (Any) -> StatusBarNotification?
private lateinit var attachedChildrenForRow: (View) -> List<View> private lateinit var attachedChildrenForRow: (View) -> List<View>
@@ -42,6 +43,7 @@ internal class AospLockscreenBackend(
it.returnType == Boolean::class.javaPrimitiveType it.returnType == Boolean::class.javaPrimitiveType
}?.also { it.isAccessible = true } }?.also { it.isAccessible = true }
?: error("AOSP lockscreen state method not found") ?: error("AOSP lockscreen state method not found")
stackExpanded = findField(stackClass, "mIsExpanded")
prepareNotificationAccess(rowClass) prepareNotificationAccess(rowClass)
installShelfFilter(classLoader) installShelfFilter(classLoader)
@@ -73,7 +75,9 @@ internal class AospLockscreenBackend(
chain.proceed() chain.proceed()
} }
} }
stackClass.declaredMethods.filter { it.name == "setStatusBarState" }.forEach { method -> stackClass.declaredMethods.filter {
it.name == "setStatusBarState" || it.name == "setIsExpanded"
}.forEach { method ->
method.isAccessible = true method.isAccessible = true
framework.hook(method).intercept { chain -> framework.hook(method).intercept { chain ->
val result = chain.proceed() val result = chain.proceed()
@@ -85,7 +89,7 @@ internal class AospLockscreenBackend(
} }
} }
}.onSuccess { }.onSuccess {
Log.i(TAG, "Installed AOSP collapsed-lockscreen notification backend") Log.i(TAG, "Installed AOSP lockscreen/drawer notification backend")
}.onFailure { }.onFailure {
Log.w(TAG, "AOSP lockscreen shape is unsupported; leaving rows unchanged", it) Log.w(TAG, "AOSP lockscreen shape is unsupported; leaving rows unchanged", it)
}.isSuccess }.isSuccess
@@ -147,11 +151,11 @@ internal class AospLockscreenBackend(
?: return@intercept chain.proceed() ?: return@intercept chain.proceed()
if (container.parent?.javaClass?.name != SHELF) return@intercept chain.proceed() if (container.parent?.javaClass?.name != SHELF) return@intercept chain.proceed()
synchronized(shelves) { shelves += container } synchronized(shelves) { shelves += container }
if (!isCollapsedLockscreen(container)) return@intercept chain.proceed() val surface = surfaceFor(container) ?: return@intercept chain.proceed()
val children = List(container.childCount, container::getChildAt) val children = List(container.childCount, container::getChildAt)
val states = shelfIconStates.get(container) as? Map<*, *> val states = shelfIconStates.get(container) as? Map<*, *>
?: return@intercept chain.proceed() ?: return@intercept chain.proceed()
val blocked = children.filter(::isBlockedShelfIcon) val blocked = children.filter { isBlockedShelfIcon(it, surface) }
blocked.forEach { states[it]?.let(iconState::hide) } blocked.forEach { states[it]?.let(iconState::hide) }
children.filterNot(blocked::contains).forEach(container::bringChildToFront) children.filterNot(blocked::contains).forEach(container::bringChildToFront)
val result = try { val result = try {
@@ -169,22 +173,27 @@ internal class AospLockscreenBackend(
} }
private fun applyStack(stack: ViewGroup) { private fun applyStack(stack: ViewGroup) {
val active = isCollapsedLockscreen(stack) val surface = surfaceFor(stack)
repeat(stack.childCount) { applyRow(stack, stack.getChildAt(it), active) } repeat(stack.childCount) { applyRow(stack.getChildAt(it), surface) }
stack.requestLayout() stack.requestLayout()
} }
private fun applyRow(stack: ViewGroup, row: View, active: Boolean? = null) { private fun applyRow(stack: ViewGroup, row: View) = applyRow(row, surfaceFor(stack))
private fun applyRow(row: View, surface: NotificationSurface?) {
if (row.javaClass.name != ROW) return if (row.javaClass.name != ROW) return
val filter = active ?: isCollapsedLockscreen(stack) if (surface == null) {
if (!filter) restoreGroupOrder(row) restoreTree(row)
return
}
restoreGroupOrder(row)
val children = attachedChildrenForRow(row) val children = attachedChildrenForRow(row)
val parentBlocked = filter && isBlocked(row) val parentBlocked = isBlocked(row, surface)
var blockedChildren = children.map { it to (parentBlocked && isBlocked(it)) } val blockedChildren = children.map { it to isBlocked(it, surface) }
if (parentBlocked && children.isNotEmpty()) { if (blockedChildren.any { it.second }) {
groupOrders.putIfAbsent(row, children) groupOrders.putIfAbsent(row, children)
blockedChildren = blockedChildren.partition { !it.second }.let { it.first + it.second } val ordered = blockedChildren.partition { !it.second }.let { it.first + it.second }
reorderChildren(row, blockedChildren.map { it.first }) reorderChildren(row, ordered.map { it.first })
setGroupCount(row, blockedChildren.count { !it.second }) setGroupCount(row, blockedChildren.count { !it.second })
} }
blockedChildren.forEach { (child, blocked) -> blockedChildren.forEach { (child, blocked) ->
@@ -193,18 +202,19 @@ internal class AospLockscreenBackend(
if (parentBlocked && blockedChildren.all { it.second }) hideRow(row) else restoreRow(row) if (parentBlocked && blockedChildren.all { it.second }) hideRow(row) else restoreRow(row)
} }
private fun isBlocked(row: View): Boolean { private fun isBlocked(row: View, surface: NotificationSurface): Boolean {
val sbn = runCatching { entryForRow(row)?.let(sbnForEntry) }.getOrNull() ?: return false val sbn = runCatching { entryForRow(row)?.let(sbnForEntry) }.getOrNull() ?: return false
return isBlocked(sbn) return isBlocked(sbn, surface)
} }
private fun isBlockedShelfIcon(icon: View): Boolean { private fun isBlockedShelfIcon(icon: View, surface: NotificationSurface): Boolean {
if (icon.javaClass.name != STATUS_BAR_ICON_VIEW) return false if (icon.javaClass.name != STATUS_BAR_ICON_VIEW) return false
val sbn = runCatching { shelfNotification.get(icon) as? StatusBarNotification }.getOrNull() val sbn = runCatching { shelfNotification.get(icon) as? StatusBarNotification }.getOrNull()
?: return false ?: return false
val row = rowForKey(icon, sbn.key) val row = rowForKey(icon, sbn.key)
return row?.let { isBlocked(it) && attachedChildrenForRow(it).none { child -> !isBlocked(child) } } return row?.let {
?: isBlocked(sbn) isBlocked(it, surface) && attachedChildrenForRow(it).none { child -> !isBlocked(child, surface) }
} ?: isBlocked(sbn, surface)
} }
private fun rowForKey(view: View, key: String): View? { private fun rowForKey(view: View, key: String): View? {
@@ -219,21 +229,27 @@ internal class AospLockscreenBackend(
} }
} }
private fun isBlocked(sbn: StatusBarNotification): Boolean = private fun isBlocked(sbn: StatusBarNotification, surface: NotificationSurface): Boolean =
ProcessVisibilityPolicyCache.isBlocked( ProcessVisibilityPolicyCache.isBlocked(
VisibilityNotificationExtractor.from(sbn), VisibilityNotificationExtractor.from(sbn),
NotificationSurface.LOCKSCREEN_COLLAPSED, surface,
).also { blocked -> ).also { blocked ->
if (blocked && !blockedObserved) { if (blocked && !blockedObserved) {
blockedObserved = true blockedObserved = true
Log.i(TAG, "Filtered an AOSP collapsed-lockscreen notification") Log.i(TAG, "Filtered an AOSP notification from $surface")
} }
} }
private fun isCollapsedLockscreen(view: View): Boolean { private fun surfaceFor(view: View): NotificationSurface? {
val root = view.rootView val root = view.rootView
val stack = synchronized(stacks) { stacks.firstOrNull { it.rootView === root } } ?: return false val stack = synchronized(stacks) { stacks.firstOrNull { it.rootView === root } } ?: return null
return runCatching { onKeyguard.invoke(stack) as Boolean }.getOrDefault(false) return runCatching {
when {
onKeyguard.invoke(stack) as Boolean -> NotificationSurface.LOCKSCREEN_COLLAPSED
stackExpanded?.getBoolean(stack) == true -> NotificationSurface.DRAWER
else -> null
}
}.getOrNull()
} }
private fun hideRow(row: View) { private fun hideRow(row: View) {
@@ -12,7 +12,7 @@ import java.util.Collections
import java.util.WeakHashMap import java.util.WeakHashMap
import java.util.function.Function import java.util.function.Function
/** Hides blocked OneUI notification rows only on the collapsed cards lockscreen. */ /** Filters OneUI notification rows in Cards-mode lockscreen and the notification drawer. */
internal class OneUiLockscreenCardsBackend( internal class OneUiLockscreenCardsBackend(
private val framework: XposedInterface, private val framework: XposedInterface,
) : VisibilitySurfaceBackend { ) : VisibilitySurfaceBackend {
@@ -21,6 +21,7 @@ internal class OneUiLockscreenCardsBackend(
private val gridRenderer = OneUiCardsGridRenderer() private val gridRenderer = OneUiCardsGridRenderer()
private lateinit var entryForRow: (View) -> Any? private lateinit var entryForRow: (View) -> Any?
private lateinit var sbnForEntry: (Any) -> StatusBarNotification? private lateinit var sbnForEntry: (Any) -> StatusBarNotification?
private var stackExpanded: Field? = null
private lateinit var shelfNotification: Field private lateinit var shelfNotification: Field
private lateinit var shelfIconStates: Field private lateinit var shelfIconStates: Field
private lateinit var iconState: IconStateAccess private lateinit var iconState: IconStateAccess
@@ -36,6 +37,7 @@ internal class OneUiLockscreenCardsBackend(
val stackClass = Class.forName(STACK, false, classLoader) val stackClass = Class.forName(STACK, false, classLoader)
val rowClass = Class.forName(ROW, false, classLoader) val rowClass = Class.forName(ROW, false, classLoader)
val stateController = Class.forName(STATE_CONTROLLER, false, classLoader) val stateController = Class.forName(STATE_CONTROLLER, false, classLoader)
stackExpanded = findField(stackClass, "mIsExpanded")
prepareNotificationAccess(rowClass) prepareNotificationAccess(rowClass)
installShelfFilter(classLoader) installShelfFilter(classLoader)
installGridSourceHook(classLoader) installGridSourceHook(classLoader)
@@ -98,7 +100,7 @@ internal class OneUiLockscreenCardsBackend(
} }
} }
}.onSuccess { }.onSuccess {
Log.i(TAG, "Installed OneUI lockscreen-cards notification backend") Log.i(TAG, "Installed OneUI lockscreen-cards/drawer notification backend")
}.onFailure { }.onFailure {
Log.d(TAG, "OneUI lockscreen-cards boundary is unavailable", it) Log.d(TAG, "OneUI lockscreen-cards boundary is unavailable", it)
}.isSuccess }.isSuccess
@@ -141,11 +143,12 @@ internal class OneUiLockscreenCardsBackend(
if (isDozeActive() && isCardsMode(container)) { if (isDozeActive() && isCardsMode(container)) {
gridRenderer.suspend(container, true) gridRenderer.suspend(container, true)
} }
if (!shouldFilterLockscreenCards(container)) return@intercept chain.proceed() val surface = rowSurface(container) ?: return@intercept chain.proceed()
val lockscreenCards = surface == NotificationSurface.LOCKSCREEN_COLLAPSED
val children = List(container.childCount, container::getChildAt) val children = List(container.childCount, container::getChildAt)
val states = shelfIconStates.get(container) as? Map<*, *> val states = shelfIconStates.get(container) as? Map<*, *>
?: return@intercept chain.proceed() ?: return@intercept chain.proceed()
val blocked = children.filter(::isBlockedShelfIcon) val blocked = children.filter { isBlockedShelfIcon(it, surface) }
blocked.forEach { icon -> states[icon]?.let(iconState::hide) } blocked.forEach { icon -> states[icon]?.let(iconState::hide) }
children.filterNot(blocked::contains).forEach(container::bringChildToFront) children.filterNot(blocked::contains).forEach(container::bringChildToFront)
val result = try { val result = try {
@@ -153,10 +156,13 @@ internal class OneUiLockscreenCardsBackend(
} finally { } finally {
children.forEach(container::bringChildToFront) children.forEach(container::bringChildToFront)
} }
// OneUI applies these states after this calculation returns. Suppress every // OneUI applies these states after this calculation returns. In Cards mode,
// stock shelf icon in Cards mode; the renderer owns the supplemental grid. // the renderer owns the whole supplemental shelf; in the drawer, only hide
children.forEach { icon -> states[icon]?.let(iconState::hide) } // icons whose notification rows are hidden.
if (!nativeShelfSuppressedObserved) { (if (lockscreenCards) children else blocked).forEach { icon ->
states[icon]?.let(iconState::hide)
}
if (lockscreenCards && !nativeShelfSuppressedObserved) {
nativeShelfSuppressedObserved = true nativeShelfSuppressedObserved = true
Log.i(TAG, "Suppressed the native OneUI Cards shelf icon states") Log.i(TAG, "Suppressed the native OneUI Cards shelf icon states")
} }
@@ -228,19 +234,32 @@ internal class OneUiLockscreenCardsBackend(
} }
private fun applyStack(stack: ViewGroup) { private fun applyStack(stack: ViewGroup) {
val active = shouldFilterLockscreenCards(stack) val surface = rowSurface(stack)
repeat(stack.childCount) { index -> repeat(stack.childCount) { index ->
val row = stack.getChildAt(index) val row = stack.getChildAt(index)
if (row.javaClass.name == ROW) { if (row.javaClass.name == ROW) {
if (active && isBlocked(row)) hideRow(row) else restoreRow(row) if (surface != null && isBlocked(row, surface)) hideRow(row) else restoreRow(row)
} }
} }
} }
private fun applyRow(row: View) { private fun applyRow(row: View) {
val stack = row.parent as? ViewGroup ?: return val stack = row.parent as? ViewGroup ?: return
val active = shouldFilterLockscreenCards(stack) val surface = rowSurface(stack)
if (active && isBlocked(row)) hideRow(row) else restoreRow(row) if (surface != null && isBlocked(row, surface)) hideRow(row) else restoreRow(row)
}
private fun rowSurface(view: View): NotificationSurface? = when {
shouldFilterLockscreenCards(view) -> NotificationSurface.LOCKSCREEN_COLLAPSED
statusBarStateKnown && statusBarState != KEYGUARD && !isDozeActive() && isStackExpanded(view) ->
NotificationSurface.DRAWER
else -> null
}
private fun isStackExpanded(view: View): Boolean {
val root = view.rootView
val stack = synchronized(stacks) { stacks.firstOrNull { it.rootView === root } } ?: return false
return runCatching { stackExpanded?.getBoolean(stack) == true }.getOrDefault(false)
} }
private fun isLockscreenCards(view: View) = private fun isLockscreenCards(view: View) =
@@ -257,26 +276,29 @@ internal class OneUiLockscreenCardsBackend(
Settings.System.getInt(view.context.contentResolver, LOCKSCREEN_STYLE) == CARDS Settings.System.getInt(view.context.contentResolver, LOCKSCREEN_STYLE) == CARDS
}.getOrDefault(false) }.getOrDefault(false)
private fun isBlocked(row: View): Boolean { private fun isBlocked(row: View, surface: NotificationSurface): Boolean {
val sbn = runCatching { entryForRow(row)?.let(sbnForEntry) }.getOrNull() ?: return false val sbn = runCatching { entryForRow(row)?.let(sbnForEntry) }.getOrNull() ?: return false
return isBlocked(sbn) return isBlocked(sbn, surface)
} }
private fun isBlockedShelfIcon(icon: View): Boolean { private fun isBlockedShelfIcon(icon: View, surface: NotificationSurface): Boolean {
if (icon.javaClass.name != STATUS_BAR_ICON_VIEW) return false if (icon.javaClass.name != STATUS_BAR_ICON_VIEW) return false
val sbn = runCatching { shelfNotification.get(icon) as? StatusBarNotification }.getOrNull() val sbn = runCatching { shelfNotification.get(icon) as? StatusBarNotification }.getOrNull()
?: return false ?: return false
return isBlocked(sbn) return isBlocked(sbn, surface)
} }
private fun isBlocked(sbn: StatusBarNotification): Boolean { private fun isBlocked(sbn: StatusBarNotification) =
isBlocked(sbn, NotificationSurface.LOCKSCREEN_COLLAPSED)
private fun isBlocked(sbn: StatusBarNotification, surface: NotificationSurface): Boolean {
return ProcessVisibilityPolicyCache.isBlocked( return ProcessVisibilityPolicyCache.isBlocked(
VisibilityNotificationExtractor.from(sbn), VisibilityNotificationExtractor.from(sbn),
NotificationSurface.LOCKSCREEN_COLLAPSED, surface,
).also { blocked -> ).also { blocked ->
if (blocked && !blockedObserved) { if (blocked && !blockedObserved) {
blockedObserved = true blockedObserved = true
Log.i(TAG, "Filtered a OneUI lockscreen notification card") Log.i(TAG, "Filtered a OneUI notification from $surface")
} }
} }
} }
@@ -2,7 +2,7 @@ package se.ajpanton.notificationsmaster.visibility
import com.google.re2j.Pattern import com.google.re2j.Pattern
enum class NotificationSurface { AOD, LOCKSCREEN_COLLAPSED, UNLOCKED_STATUSBAR } enum class NotificationSurface { AOD, LOCKSCREEN_COLLAPSED, UNLOCKED_STATUSBAR, DRAWER }
data class CardsGridSettings( data class CardsGridSettings(
val maxRows: Int = 1, val maxRows: Int = 1,
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@color/visibility_card_background" />
<stroke
android:width="1dp"
android:color="@color/visibility_card_outline" />
<corners android:radius="6dp" />
</shape>
@@ -51,6 +51,12 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="Unlocked status bar" /> android:text="Unlocked status bar" />
<com.google.android.material.switchmaterial.SwitchMaterial
android:id="@+id/show_drawer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Notification drawer" />
<TextView <TextView
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
@@ -22,7 +22,7 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="4dp" android:layout_marginTop="4dp"
android:text="Icons represent AOD, Lockscreen and Unlocked, respectively. Tap to toggle visibility.&#10;Tap the app for more advanced options." android:text="Icons represent AOD, Lockscreen, Unlocked and the notification drawer, respectively. Tap to toggle visibility.&#10;Tap the app for more advanced options."
android:textAppearance="?attr/textAppearanceCaption" /> android:textAppearance="?attr/textAppearanceCaption" />
<EditText <EditText
@@ -92,6 +92,35 @@
android:visibility="gone" /> android:visibility="gone" />
</FrameLayout> </FrameLayout>
<FrameLayout
android:id="@+id/visibility_drawer_cell"
android:layout_width="38dp"
android:layout_height="40dp"
android:layout_marginStart="4dp"
android:foreground="?attr/selectableItemBackgroundBorderless">
<ImageView
android:id="@+id/visibility_drawer_icon"
android:layout_width="32dp"
android:layout_height="28dp"
android:layout_gravity="center"
android:background="@drawable/bg_visibility_drawer_card"
android:contentDescription="Notification drawer visibility"
android:padding="4dp"
android:scaleType="fitCenter" />
<TextView
android:id="@+id/visibility_drawer_cross"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:text="✕"
android:textColor="@color/visibility_hidden_cross"
android:textSize="16sp"
android:textStyle="bold"
android:visibility="gone" />
</FrameLayout>
<LinearLayout <LinearLayout
android:id="@+id/visibility_app_text" android:id="@+id/visibility_app_text"
android:layout_width="0dp" android:layout_width="0dp"
@@ -115,6 +115,34 @@
android:textStyle="bold" android:textStyle="bold"
android:visibility="gone" /> android:visibility="gone" />
</FrameLayout> </FrameLayout>
<FrameLayout
android:id="@+id/drawer_cell"
android:layout_width="38dp"
android:layout_height="40dp"
android:foreground="?attr/selectableItemBackgroundBorderless">
<ImageView
android:id="@+id/drawer_icon"
android:layout_width="32dp"
android:layout_height="28dp"
android:layout_gravity="center"
android:background="@drawable/bg_visibility_drawer_card"
android:contentDescription="Notification drawer visibility"
android:padding="4dp"
android:scaleType="fitCenter" />
<TextView
android:id="@+id/drawer_cross"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:text="✕"
android:textColor="@color/visibility_hidden_cross"
android:textSize="16sp"
android:textStyle="bold"
android:visibility="gone" />
</FrameLayout>
</LinearLayout> </LinearLayout>
</LinearLayout> </LinearLayout>
+2
View File
@@ -17,4 +17,6 @@
<color name="app_filter_override_icon">#B8CCFF</color> <color name="app_filter_override_icon">#B8CCFF</color>
<color name="visibility_hidden_cross">#FF8A9A</color> <color name="visibility_hidden_cross">#FF8A9A</color>
<color name="visibility_edited_background">#24294775</color> <color name="visibility_edited_background">#24294775</color>
<color name="visibility_card_background">#FF3C4043</color>
<color name="visibility_card_outline">#99FFFFFF</color>
</resources> </resources>
+2
View File
@@ -17,6 +17,8 @@
<color name="app_filter_override_icon">#34588E</color> <color name="app_filter_override_icon">#34588E</color>
<color name="visibility_hidden_cross">#B00020</color> <color name="visibility_hidden_cross">#B00020</color>
<color name="visibility_edited_background">#183B5C9C</color> <color name="visibility_edited_background">#183B5C9C</color>
<color name="visibility_card_background">#FFF3F3F3</color>
<color name="visibility_card_outline">#61000000</color>
<color name="notification_access_granted">#2E7D32</color> <color name="notification_access_granted">#2E7D32</color>
<color name="notification_access_missing">#B3261E</color> <color name="notification_access_missing">#B3261E</color>
<color name="launcher_icon_background">#D9E2FF</color> <color name="launcher_icon_background">#D9E2FF</color>
@@ -14,10 +14,10 @@ class VisibilityPolicyJsonTest {
systemUiMisc = SystemUiMiscSettings(true, false, true), systemUiMisc = SystemUiMiscSettings(true, false, true),
apps = listOf(AppVisibilityPolicy( apps = listOf(AppVisibilityPolicy(
packageName = "example.app", packageName = "example.app",
blockedSurfaces = setOf(NotificationSurface.AOD), blockedSurfaces = setOf(NotificationSurface.AOD, NotificationSurface.DRAWER),
exceptions = listOf( exceptions = listOf(
VisibilityExceptionRule("first", setOf(NotificationSurface.LOCKSCREEN_COLLAPSED)), VisibilityExceptionRule("first", setOf(NotificationSurface.LOCKSCREEN_COLLAPSED)),
VisibilityExceptionRule("second", emptySet()), VisibilityExceptionRule("second", setOf(NotificationSurface.DRAWER)),
), ),
)), )),
) )
@@ -30,16 +30,21 @@ class VisibilityPolicyTest {
} }
@Test fun `package defaults are independent by surface`() { @Test fun `package defaults are independent by surface`() {
val policy = compiled(defaults = setOf(NotificationSurface.AOD, NotificationSurface.UNLOCKED_STATUSBAR)) val policy = compiled(defaults = setOf(
NotificationSurface.AOD,
NotificationSurface.UNLOCKED_STATUSBAR,
NotificationSurface.DRAWER,
))
assertTrue(policy.isBlocked(notification, NotificationSurface.AOD)) assertTrue(policy.isBlocked(notification, NotificationSurface.AOD))
assertFalse(policy.isBlocked(notification, NotificationSurface.LOCKSCREEN_COLLAPSED)) assertFalse(policy.isBlocked(notification, NotificationSurface.LOCKSCREEN_COLLAPSED))
assertTrue(policy.isBlocked(notification, NotificationSurface.UNLOCKED_STATUSBAR)) assertTrue(policy.isBlocked(notification, NotificationSurface.UNLOCKED_STATUSBAR))
assertTrue(policy.isBlocked(notification, NotificationSurface.DRAWER))
} }
@Test fun `first matching exception replaces the package default`() { @Test fun `first matching exception replaces the package default`() {
val policy = compiled( val policy = compiled(
defaults = setOf(NotificationSurface.AOD), defaults = setOf(NotificationSurface.AOD, NotificationSurface.DRAWER),
exceptions = listOf( exceptions = listOf(
VisibilityExceptionRule("tomorrow", setOf(NotificationSurface.LOCKSCREEN_COLLAPSED)), VisibilityExceptionRule("tomorrow", setOf(NotificationSurface.LOCKSCREEN_COLLAPSED)),
VisibilityExceptionRule("Alice", setOf(NotificationSurface.UNLOCKED_STATUSBAR)), VisibilityExceptionRule("Alice", setOf(NotificationSurface.UNLOCKED_STATUSBAR)),
@@ -49,6 +54,7 @@ class VisibilityPolicyTest {
assertFalse(policy.isBlocked(notification, NotificationSurface.AOD)) assertFalse(policy.isBlocked(notification, NotificationSurface.AOD))
assertTrue(policy.isBlocked(notification, NotificationSurface.LOCKSCREEN_COLLAPSED)) assertTrue(policy.isBlocked(notification, NotificationSurface.LOCKSCREEN_COLLAPSED))
assertFalse(policy.isBlocked(notification, NotificationSurface.UNLOCKED_STATUSBAR)) assertFalse(policy.isBlocked(notification, NotificationSurface.UNLOCKED_STATUSBAR))
assertFalse(policy.isBlocked(notification, NotificationSurface.DRAWER))
} }
@Test fun `exceptions match stable notification metadata`() { @Test fun `exceptions match stable notification metadata`() {