Add notification drawer visibility controls
This commit is contained in:
@@ -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
|
||||||
|
|||||||
+7
@@ -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) {
|
||||||
|
|||||||
+41
-19
@@ -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. Tap the app for more advanced options."
|
android:text="Icons represent AOD, Lockscreen, Unlocked and the notification drawer, respectively. Tap to toggle visibility. 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>
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
+2
-2
@@ -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)),
|
||||||
),
|
),
|
||||||
)),
|
)),
|
||||||
)
|
)
|
||||||
|
|||||||
+8
-2
@@ -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`() {
|
||||||
|
|||||||
Reference in New Issue
Block a user