Reorganize notification visibility settings

This commit is contained in:
ajp_anton
2026-08-30 01:40:16 +00:00
parent e814a1aab3
commit 7331ac8f01
16 changed files with 496 additions and 211 deletions
@@ -1,55 +1,21 @@
package se.ajpanton.notificationsmaster
import android.Manifest
import android.content.ComponentName
import android.content.Intent
import android.os.Bundle
import android.provider.Settings
import android.view.View
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.fragment.app.Fragment
import se.ajpanton.notificationsmaster.alerts.AlertConfigurationStore
import se.ajpanton.notificationsmaster.capture.NotificationCaptureService
import se.ajpanton.notificationsmaster.databinding.FragmentAlertStatusBinding
import se.ajpanton.notificationsmaster.debug.DebugNotifications
class AlertStatusFragment : Fragment(R.layout.fragment_alert_status) {
private var binding: FragmentAlertStatusBinding? = null
private var pendingCount: Int? = null
private var pendingCycle = false
private var updatingCycle = false
private val permissionRequest = registerForActivityResult(ActivityResultContracts.RequestPermission()) { granted ->
val context = context ?: return@registerForActivityResult
if (granted) {
pendingCount?.let { DebugNotifications.setCount(context, it) }
if (pendingCycle) DebugNotifications.setCycling(context, true)
refreshDebugControls()
} else {
Toast.makeText(context, R.string.debug_notification_permission_required, Toast.LENGTH_SHORT).show()
refreshDebugControls()
}
pendingCount = null
pendingCycle = false
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
binding = FragmentAlertStatusBinding.bind(view)
binding!!.notificationAccess.setOnClickListener { startActivity(Intent(Settings.ACTION_NOTIFICATION_LISTENER_SETTINGS)) }
binding!!.debugMinus10.setOnClickListener { changeDebugCount(-10) }
binding!!.debugMinus1.setOnClickListener { changeDebugCount(-1) }
binding!!.debugPlus1.setOnClickListener { changeDebugCount(1) }
binding!!.debugPlus10.setOnClickListener { changeDebugCount(10) }
binding!!.debugReset.setOnClickListener { setDebugCount(0) }
binding!!.debugCycle.setOnCheckedChangeListener { _, enabled ->
if (updatingCycle) return@setOnCheckedChangeListener
if (enabled && !DebugNotifications.hasPermission(requireContext())) {
pendingCycle = true
permissionRequest.launch(Manifest.permission.POST_NOTIFICATIONS)
} else {
DebugNotifications.setCycling(requireContext(), enabled)
}
}
}
override fun onResume() {
@@ -66,31 +32,6 @@ class AlertStatusFragment : Fragment(R.layout.fragment_alert_status) {
append(if (config.rules.count { it.enabled } == 1) "." else "s.")
}
binding?.notificationAccess?.text = if (access) "Notification access enabled" else "Enable notification access"
DebugNotifications.apply(context)
refreshDebugControls()
}
private fun changeDebugCount(delta: Int) = setDebugCount(DebugNotifications.count(requireContext()) + delta)
private fun setDebugCount(count: Int) {
val desired = count.coerceIn(0, DebugNotifications.MAX_COUNT)
if (desired > 0 && !DebugNotifications.hasPermission(requireContext())) {
pendingCount = desired
permissionRequest.launch(Manifest.permission.POST_NOTIFICATIONS)
return
}
DebugNotifications.setCount(requireContext(), desired)
refreshDebugControls()
}
private fun refreshDebugControls() {
val context = context ?: return
val count = DebugNotifications.count(context)
binding?.debugCount?.text = count.toString()
binding?.debugCycle?.isEnabled = count > 0
updatingCycle = true
binding?.debugCycle?.isChecked = DebugNotifications.isCycling(context)
updatingCycle = false
}
override fun onDestroyView() { binding = null; super.onDestroyView() }
@@ -0,0 +1,82 @@
package se.ajpanton.notificationsmaster
import android.Manifest
import android.os.Bundle
import android.view.View
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.fragment.app.Fragment
import se.ajpanton.notificationsmaster.databinding.FragmentDebugNotificationsBinding
import se.ajpanton.notificationsmaster.debug.DebugNotifications
class DebugNotificationsFragment : Fragment(R.layout.fragment_debug_notifications) {
private var binding: FragmentDebugNotificationsBinding? = null
private var pendingCount: Int? = null
private var pendingCycle = false
private var updatingCycle = false
private val permissionRequest = registerForActivityResult(ActivityResultContracts.RequestPermission()) { granted ->
val context = context ?: return@registerForActivityResult
if (granted) {
pendingCount?.let { DebugNotifications.setCount(context, it) }
if (pendingCycle) DebugNotifications.setCycling(context, true)
} else {
Toast.makeText(context, R.string.debug_notification_permission_required, Toast.LENGTH_SHORT).show()
}
pendingCount = null
pendingCycle = false
refresh()
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
binding = FragmentDebugNotificationsBinding.bind(view)
binding!!.debugMinus10.setOnClickListener { changeCount(-10) }
binding!!.debugMinus1.setOnClickListener { changeCount(-1) }
binding!!.debugPlus1.setOnClickListener { changeCount(1) }
binding!!.debugPlus10.setOnClickListener { changeCount(10) }
binding!!.debugReset.setOnClickListener { setCount(0) }
binding!!.debugCycle.setOnCheckedChangeListener { _, enabled ->
if (updatingCycle) return@setOnCheckedChangeListener
if (enabled && !DebugNotifications.hasPermission(requireContext())) {
pendingCycle = true
permissionRequest.launch(Manifest.permission.POST_NOTIFICATIONS)
} else {
DebugNotifications.setCycling(requireContext(), enabled)
}
}
}
override fun onResume() {
super.onResume()
activity?.title = getString(R.string.page_debug_notifications)
DebugNotifications.apply(requireContext())
refresh()
}
override fun onDestroyView() {
binding = null
super.onDestroyView()
}
private fun changeCount(delta: Int) = setCount(DebugNotifications.count(requireContext()) + delta)
private fun setCount(count: Int) {
val desired = count.coerceIn(0, DebugNotifications.MAX_COUNT)
if (desired > 0 && !DebugNotifications.hasPermission(requireContext())) {
pendingCount = desired
permissionRequest.launch(Manifest.permission.POST_NOTIFICATIONS)
return
}
DebugNotifications.setCount(requireContext(), desired)
refresh()
}
private fun refresh() {
val context = context ?: return
val count = DebugNotifications.count(context)
binding?.debugCount?.text = count.toString()
binding?.debugCycle?.isEnabled = count > 0
updatingCycle = true
binding?.debugCycle?.isChecked = DebugNotifications.isCycling(context)
updatingCycle = false
}
}
@@ -123,7 +123,7 @@ class FilterAppsFragment : Fragment(R.layout.fragment_event_settings) {
currentBinding.appListLoading.visibility = View.VISIBLE
currentBinding.appListRefresh.isRefreshing = true
val rule = store.load()
val seen = SeenApps.snapshot()
val seen = SeenApps.snapshot(requireContext())
val context = requireContext().applicationContext
viewLifecycleOwner.lifecycleScope.launch {
val apps = withContext(Dispatchers.IO) {
@@ -169,6 +169,7 @@ class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelecte
page == Page.APPS -> AppsFragment()
page == Page.NOTIFICATION_VISIBILITY -> NotificationVisibilityFragment()
page == Page.CARDS_LAYOUT -> CardsLayoutFragment()
page == Page.DEBUG_NOTIFICATIONS -> DebugNotificationsFragment()
page == Page.LOG_DISPLAY -> LogDisplayFragment()
page == Page.FILTER_LOGGING -> FilterLoggingFragment()
page == Page.FILTER_APPS -> FilterAppsFragment.newInstance()
@@ -375,6 +376,7 @@ class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelecte
APPS(R.id.nav_apps, R.string.page_apps),
NOTIFICATION_VISIBILITY(R.id.nav_notification_visibility, R.string.page_notification_visibility),
CARDS_LAYOUT(R.id.nav_cards_layout, R.string.page_cards_layout),
DEBUG_NOTIFICATIONS(R.id.nav_debug_notifications, R.string.page_debug_notifications),
LOG_DISPLAY(R.id.nav_log_display, R.string.page_log_display),
FILTER_LOGGING(R.id.nav_filter_logging, R.string.page_filter_logging),
FILTER_APPS(R.id.nav_filter_apps, R.string.page_filter_apps),
@@ -1,10 +1,14 @@
package se.ajpanton.notificationsmaster
import android.graphics.Color
import android.graphics.drawable.Drawable
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.LinearLayout
import android.widget.ImageView
import android.widget.TextView
import androidx.core.content.ContextCompat
import androidx.core.widget.doAfterTextChanged
import androidx.fragment.app.Fragment
import androidx.fragment.app.commit
@@ -14,7 +18,9 @@ import androidx.recyclerview.widget.RecyclerView
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import se.ajpanton.notificationsmaster.capture.SeenApps
import se.ajpanton.notificationsmaster.databinding.FragmentNotificationVisibilityBinding
import se.ajpanton.notificationsmaster.databinding.ItemNotificationVisibilityAppBinding
import se.ajpanton.notificationsmaster.visibility.AppVisibilityPolicy
import se.ajpanton.notificationsmaster.visibility.NotificationSurface
import se.ajpanton.notificationsmaster.visibility.VisibilityPolicyStore
@@ -22,8 +28,8 @@ import se.ajpanton.notificationsmaster.visibility.VisibilityPolicyStore
class NotificationVisibilityFragment : Fragment(R.layout.fragment_notification_visibility) {
private var binding: FragmentNotificationVisibilityBinding? = null
private lateinit var store: VisibilityPolicyStore
private val adapter = AppAdapter(::openApp)
private var apps = emptyList<InstalledApp>()
private val adapter = AppAdapter(::openApp, ::toggleSurface)
private var apps = emptyList<AppSnapshot>()
private var appPolicies = emptyMap<String, AppVisibilityPolicy>()
private var loadGeneration = 0
@@ -32,6 +38,7 @@ class NotificationVisibilityFragment : Fragment(R.layout.fragment_notification_v
store = VisibilityPolicyStore(requireContext())
binding!!.appList.layoutManager = LinearLayoutManager(requireContext())
binding!!.appList.adapter = adapter
binding!!.appListRefresh.setOnRefreshListener(::loadApps)
binding!!.appSearch.doAfterTextChanged { showApps() }
bindEnabled()
loadApps()
@@ -65,12 +72,30 @@ class NotificationVisibilityFragment : Fragment(R.layout.fragment_notification_v
val currentBinding = binding ?: return
val generation = ++loadGeneration
val context = requireContext().applicationContext
val policies = appPolicies
currentBinding.appListLoading.visibility = View.VISIBLE
currentBinding.appListRefresh.isRefreshing = true
viewLifecycleOwner.lifecycleScope.launch {
val loaded = withContext(Dispatchers.IO) { InstalledApps.all(context) }
val loaded = withContext(Dispatchers.IO) {
val manager = context.packageManager
SeenApps.snapshot(context).mapNotNull { packageName ->
runCatching {
val info = manager.getApplicationInfo(packageName, 0)
AppSnapshot(
InstalledApp(manager.getApplicationLabel(info).toString(), packageName),
manager.getApplicationIcon(info),
)
}.getOrNull()
}.sortedWith(
compareBy<AppSnapshot> { policies[it.app.packageName]?.isEdited() != true }
.thenBy { it.app.name.lowercase() }
.thenBy { it.app.packageName },
)
}
if (binding === currentBinding && generation == loadGeneration) {
apps = loaded
currentBinding.appListLoading.visibility = View.GONE
currentBinding.appListRefresh.isRefreshing = false
showApps()
}
}
@@ -80,10 +105,29 @@ class NotificationVisibilityFragment : Fragment(R.layout.fragment_notification_v
val currentBinding = binding ?: return
val terms = currentBinding.appSearch.text.toString().lowercase().trim()
.split(Regex("\\s+")).filter(String::isNotEmpty)
adapter.submit(apps.filter { app ->
val searchable = "${app.name}\n${app.packageName}".lowercase()
val rows = apps.filter { app ->
val searchable = "${app.app.name}\n${app.app.packageName}".lowercase()
terms.all(searchable::contains)
}.map { app -> AppRow(app, appPolicies[app.packageName]) })
}.map { app -> AppRow(app, appPolicies[app.app.packageName]) }
adapter.submit(rows)
currentBinding.appListEmpty.visibility = if (
rows.isEmpty() && currentBinding.appListLoading.visibility != View.VISIBLE
) View.VISIBLE else View.GONE
}
private fun toggleSurface(row: AppRow, surface: NotificationSurface) {
val saved = store.update { policy ->
val current = policy.apps.firstOrNull { it.packageName == row.app.app.packageName }
?: AppVisibilityPolicy(row.app.app.packageName)
val updated = current.copy(blockedSurfaces = current.blockedSurfaces.toMutableSet().apply {
if (surface in current.blockedSurfaces) remove(surface) else add(surface)
})
val apps = policy.apps.filterNot { it.packageName == current.packageName }.toMutableList()
if (updated.isEdited()) apps += updated
policy.copy(apps = apps.sortedBy(AppVisibilityPolicy::packageName))
}
appPolicies = saved.apps.associateBy(AppVisibilityPolicy::packageName)
adapter.updatePolicy(row.app.app.packageName, appPolicies[row.app.app.packageName])
}
private fun openApp(app: InstalledApp) = parentFragmentManager.commit {
@@ -91,9 +135,13 @@ class NotificationVisibilityFragment : Fragment(R.layout.fragment_notification_v
addToBackStack(null)
}
private data class AppRow(val app: InstalledApp, val policy: AppVisibilityPolicy?)
private data class AppSnapshot(val app: InstalledApp, val icon: Drawable)
private data class AppRow(val app: AppSnapshot, val policy: AppVisibilityPolicy?)
private class AppAdapter(private val open: (InstalledApp) -> Unit) : RecyclerView.Adapter<AppHolder>() {
private class AppAdapter(
private val open: (InstalledApp) -> Unit,
private val toggle: (AppRow, NotificationSurface) -> Unit,
) : RecyclerView.Adapter<AppHolder>() {
private var rows = emptyList<AppRow>()
fun submit(newRows: List<AppRow>) {
@@ -101,57 +149,74 @@ class NotificationVisibilityFragment : Fragment(R.layout.fragment_notification_v
notifyDataSetChanged()
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AppHolder {
val row = LinearLayout(parent.context).apply {
orientation = LinearLayout.VERTICAL
isClickable = true
isFocusable = true
setPadding(0, dp(parent, 8), 0, dp(parent, 8))
background = context.obtainStyledAttributes(intArrayOf(android.R.attr.selectableItemBackground))
.let { values -> values.getDrawable(0).also { values.recycle() } }
}
val name = TextView(parent.context).apply {
textSize = 16f
setTypeface(typeface, android.graphics.Typeface.BOLD)
}
val packageName = TextView(parent.context).apply { textSize = 12f }
val summary = TextView(parent.context).apply { textSize = 13f }
row.addView(name)
row.addView(packageName)
row.addView(summary)
return AppHolder(row, name, packageName, summary)
fun updatePolicy(packageName: String, policy: AppVisibilityPolicy?) {
val position = rows.indexOfFirst { it.app.app.packageName == packageName }
if (position < 0) return
rows = rows.toMutableList().also { it[position] = it[position].copy(policy = policy) }
notifyItemChanged(position)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = AppHolder(
ItemNotificationVisibilityAppBinding.inflate(LayoutInflater.from(parent.context), parent, false),
)
override fun onBindViewHolder(holder: AppHolder, position: Int) {
val row = rows[position]
holder.name.text = row.app.name
holder.packageName.text = row.app.packageName
holder.summary.text = summary(row.policy)
holder.itemView.setOnClickListener { open(row.app) }
val binding = holder.binding
val policy = row.policy
binding.visibilityAppName.text = row.app.app.name
binding.visibilityAppPackage.text = row.app.app.packageName
val exceptionCount = policy?.exceptions?.size ?: 0
binding.visibilityAppExceptions.visibility = if (exceptionCount > 0) View.VISIBLE else View.GONE
binding.visibilityAppExceptions.text = "$exceptionCount text exception" + if (exceptionCount == 1) "" else "s"
binding.visibilityAppRoot.setBackgroundColor(
if (policy?.isEdited() == true) ContextCompat.getColor(
binding.root.context,
R.color.visibility_edited_background,
) else Color.TRANSPARENT,
)
bindSurface(binding.visibilityAodIcon, binding.visibilityAodCross, row, NotificationSurface.AOD)
bindSurface(
binding.visibilityLockscreenIcon,
binding.visibilityLockscreenCross,
row,
NotificationSurface.LOCKSCREEN_COLLAPSED,
)
bindSurface(
binding.visibilityUnlockedIcon,
binding.visibilityUnlockedCross,
row,
NotificationSurface.UNLOCKED_STATUSBAR,
)
binding.visibilityAodCell.setOnClickListener { toggle(row, NotificationSurface.AOD) }
binding.visibilityLockscreenCell.setOnClickListener {
toggle(row, NotificationSurface.LOCKSCREEN_COLLAPSED)
}
binding.visibilityUnlockedCell.setOnClickListener {
toggle(row, NotificationSurface.UNLOCKED_STATUSBAR)
}
binding.visibilityAppRoot.setOnClickListener { open(row.app.app) }
}
override fun getItemCount() = rows.size
private fun summary(policy: AppVisibilityPolicy?): String {
if (policy == null) return "Visible on all surfaces"
val hidden = listOf(
NotificationSurface.UNLOCKED_STATUSBAR to "status bar",
NotificationSurface.LOCKSCREEN_COLLAPSED to "lockscreen",
NotificationSurface.AOD to "AOD",
).filter { it.first in policy.blockedSurfaces }.joinToString { it.second }
val defaults = if (hidden.isEmpty()) "Visible on all surfaces" else "Hidden: $hidden"
return if (policy.exceptions.isEmpty()) defaults else "$defaults${policy.exceptions.size} text exception" +
if (policy.exceptions.size == 1) "" else "s"
private fun bindSurface(
icon: ImageView,
cross: TextView,
row: AppRow,
surface: NotificationSurface,
) {
icon.setImageDrawable(row.app.icon.constantState?.newDrawable()?.mutate() ?: row.app.icon)
val hidden = surface in row.policy?.blockedSurfaces.orEmpty()
icon.alpha = if (hidden) 0.25f else 1f
cross.visibility = if (hidden) View.VISIBLE else View.GONE
}
private fun dp(parent: ViewGroup, value: Int) =
(value * parent.resources.displayMetrics.density).toInt()
}
private class AppHolder(
view: View,
val name: TextView,
val packageName: TextView,
val summary: TextView,
) : RecyclerView.ViewHolder(view)
private class AppHolder(val binding: ItemNotificationVisibilityAppBinding) :
RecyclerView.ViewHolder(binding.root)
private companion object {
fun AppVisibilityPolicy.isEdited() = blockedSurfaces.isNotEmpty() || exceptions.isNotEmpty()
}
}
@@ -62,7 +62,7 @@ class NotificationCaptureService : NotificationListenerService() {
getActiveNotifications()?.forEach { sbn ->
if (DebugNotifications.isManaged(this, sbn)) return@forEach
val snapshot = NotificationContents.snapshot(sbn)
SeenApps.markSeen(snapshot.packageName)
SeenApps.markSeen(this, snapshot.packageName)
activeNotifications[snapshot.key] = snapshot
record(
snapshot,
@@ -77,7 +77,7 @@ class NotificationCaptureService : NotificationListenerService() {
override fun onNotificationPosted(sbn: StatusBarNotification) {
if (DebugNotifications.isManaged(this, sbn)) return
val snapshot = NotificationContents.snapshot(sbn)
SeenApps.markSeen(snapshot.packageName)
SeenApps.markSeen(this, snapshot.packageName)
val previous = activeNotifications.put(snapshot.key, snapshot)
when {
previous == null -> {
@@ -113,7 +113,7 @@ class NotificationCaptureService : NotificationListenerService() {
) {
if (DebugNotifications.isManaged(this, sbn)) return
val snapshot = activeNotifications.remove(sbn.key) ?: NotificationContents.snapshot(sbn)
SeenApps.markSeen(snapshot.packageName)
SeenApps.markSeen(this, snapshot.packageName)
record(snapshot, actionForRemoval(reason), LoggingType.DISAPPEARING, includeContents = false)
}
@@ -1,10 +1,39 @@
package se.ajpanton.notificationsmaster.capture
import java.util.concurrent.ConcurrentHashMap
import android.content.Context
import android.os.SystemClock
import kotlin.math.abs
/** Process/boot-lifetime record of packages seen by the listener. */
/** Persists packages seen by the listener for the duration of the current boot. */
object SeenApps {
private val packages = ConcurrentHashMap.newKeySet<String>()
fun markSeen(packageName: String) { packages += packageName }
fun snapshot(): Set<String> = packages.toSet()
@Synchronized
fun markSeen(context: Context, packageName: String) {
val preferences = preferences(context)
resetAfterBoot(preferences)
val packages = preferences.getStringSet(KEY_PACKAGES, emptySet()).orEmpty()
if (packageName !in packages) {
preferences.edit().putStringSet(KEY_PACKAGES, packages + packageName).apply()
}
}
@Synchronized
fun snapshot(context: Context): Set<String> {
val preferences = preferences(context)
resetAfterBoot(preferences)
return preferences.getStringSet(KEY_PACKAGES, emptySet()).orEmpty().toSet()
}
private fun resetAfterBoot(preferences: android.content.SharedPreferences) {
val bootEpoch = System.currentTimeMillis() - SystemClock.elapsedRealtime()
if (abs(preferences.getLong(KEY_BOOT_EPOCH, -1) - bootEpoch) > 5_000) {
preferences.edit().putLong(KEY_BOOT_EPOCH, bootEpoch).remove(KEY_PACKAGES).commit()
}
}
private fun preferences(context: Context) =
context.applicationContext.getSharedPreferences(PREFERENCES, Context.MODE_PRIVATE)
private const val PREFERENCES = "seen_notification_apps"
private const val KEY_PACKAGES = "packages"
private const val KEY_BOOT_EPOCH = "boot_epoch"
}
@@ -28,80 +28,5 @@
android:text="Without optional system integration, Android keeps the source apps own sound and vibration. Matching custom alerts play in addition; they do not replace it."
android:textSize="14sp" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
android:text="@string/debug_notifications_title"
android:textSize="18sp"
android:textStyle="bold" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="@string/debug_notifications_description"
android:textSize="14sp" />
<com.google.android.material.switchmaterial.SwitchMaterial
android:id="@+id/debug_cycle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:text="@string/debug_cycle_notifications" />
<TextView
android:id="@+id/debug_count"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="0"
android:textSize="32sp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:orientation="horizontal">
<Button
android:id="@+id/debug_minus_10"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:minWidth="0dp"
android:text="-10" />
<Button
android:id="@+id/debug_minus_1"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:minWidth="0dp"
android:text="-1" />
<Button
android:id="@+id/debug_plus_1"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:minWidth="0dp"
android:text="+1" />
<Button
android:id="@+id/debug_plus_10"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:minWidth="0dp"
android:text="+10" />
</LinearLayout>
<Button
android:id="@+id/debug_reset"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="@string/debug_reset_notifications" />
</LinearLayout>
</ScrollView>
@@ -26,10 +26,10 @@
android:textStyle="bold" />
<com.google.android.material.switchmaterial.SwitchMaterial
android:id="@+id/hide_unlocked"
android:id="@+id/hide_aod"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Unlocked status bar" />
android:text="Always-on display" />
<com.google.android.material.switchmaterial.SwitchMaterial
android:id="@+id/hide_lockscreen"
@@ -38,10 +38,10 @@
android:text="Collapsed lockscreen" />
<com.google.android.material.switchmaterial.SwitchMaterial
android:id="@+id/hide_aod"
android:id="@+id/hide_unlocked"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Always-on display" />
android:text="Unlocked status bar" />
<LinearLayout
android:layout_width="match_parent"
@@ -0,0 +1,79 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="@dimen/page_padding">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/debug_notifications_description"
android:textSize="14sp" />
<com.google.android.material.switchmaterial.SwitchMaterial
android:id="@+id/debug_cycle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:text="@string/debug_cycle_notifications" />
<TextView
android:id="@+id/debug_count"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="0"
android:textSize="32sp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:orientation="horizontal">
<Button
android:id="@+id/debug_minus_10"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:minWidth="0dp"
android:text="-10" />
<Button
android:id="@+id/debug_minus_1"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:minWidth="0dp"
android:text="-1" />
<Button
android:id="@+id/debug_plus_1"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:minWidth="0dp"
android:text="+1" />
<Button
android:id="@+id/debug_plus_10"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:minWidth="0dp"
android:text="+10" />
</LinearLayout>
<Button
android:id="@+id/debug_reset"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="@string/debug_reset_notifications" />
</LinearLayout>
</ScrollView>
@@ -15,9 +15,16 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="Requires LSPosed. Unsupported surfaces remain unchanged."
android:text="Requires LSPosed. Only apps seen producing a notification since the last boot are listed."
android:textAppearance="?attr/textAppearanceBody2" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:text="Tap an app icon to hide or show it on AOD, the lockscreen, or the unlocked status bar. Tap the app name for text exceptions."
android:textAppearance="?attr/textAppearanceCaption" />
<EditText
android:id="@+id/app_search"
android:layout_width="match_parent"
@@ -33,14 +40,30 @@
android:layout_marginTop="8dp"
android:layout_weight="1">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/app_list"
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/app_list_refresh"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:fastScrollAlwaysVisible="true"
android:fastScrollEnabled="true"
android:paddingBottom="16dp" />
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/app_list"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:fastScrollAlwaysVisible="true"
android:fastScrollEnabled="true"
android:paddingBottom="16dp" />
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
<TextView
android:id="@+id/app_list_empty"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:gravity="center"
android:text="No notification-producing apps have been seen since boot."
android:textAppearance="?attr/textAppearanceBody2"
android:visibility="gone" />
<ProgressBar
android:id="@+id/app_list_loading"
@@ -0,0 +1,129 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/visibility_app_root"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:foreground="?attr/selectableItemBackground"
android:gravity="center_vertical"
android:minHeight="58dp"
android:orientation="horizontal"
android:paddingStart="4dp"
android:paddingTop="7dp"
android:paddingEnd="4dp"
android:paddingBottom="7dp">
<FrameLayout
android:id="@+id/visibility_aod_cell"
android:layout_width="30dp"
android:layout_height="40dp"
android:foreground="?attr/selectableItemBackgroundBorderless">
<ImageView
android:id="@+id/visibility_aod_icon"
android:layout_width="26dp"
android:layout_height="26dp"
android:layout_gravity="center"
android:contentDescription="AOD visibility"
android:scaleType="fitCenter" />
<TextView
android:id="@+id/visibility_aod_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>
<FrameLayout
android:id="@+id/visibility_lockscreen_cell"
android:layout_width="30dp"
android:layout_height="40dp"
android:layout_marginStart="4dp"
android:foreground="?attr/selectableItemBackgroundBorderless">
<ImageView
android:id="@+id/visibility_lockscreen_icon"
android:layout_width="26dp"
android:layout_height="26dp"
android:layout_gravity="center"
android:contentDescription="Lockscreen visibility"
android:scaleType="fitCenter" />
<TextView
android:id="@+id/visibility_lockscreen_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>
<FrameLayout
android:id="@+id/visibility_unlocked_cell"
android:layout_width="30dp"
android:layout_height="40dp"
android:layout_marginStart="4dp"
android:foreground="?attr/selectableItemBackgroundBorderless">
<ImageView
android:id="@+id/visibility_unlocked_icon"
android:layout_width="26dp"
android:layout_height="26dp"
android:layout_gravity="center"
android:contentDescription="Unlocked status-bar visibility"
android:scaleType="fitCenter" />
<TextView
android:id="@+id/visibility_unlocked_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
android:id="@+id/visibility_app_text"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:layout_weight="1"
android:orientation="vertical"
android:paddingTop="4dp"
android:paddingBottom="4dp">
<TextView
android:id="@+id/visibility_app_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ellipsize="end"
android:maxLines="1"
android:textAppearance="?attr/textAppearanceBody1"
android:textStyle="bold" />
<TextView
android:id="@+id/visibility_app_package"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ellipsize="end"
android:maxLines="1"
android:textAppearance="?attr/textAppearanceCaption" />
<TextView
android:id="@+id/visibility_app_exceptions"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAppearance="?attr/textAppearanceCaption"
android:visibility="gone" />
</LinearLayout>
</LinearLayout>
+6 -1
View File
@@ -21,12 +21,17 @@
<item
android:id="@+id/nav_apps"
android:title="@string/navigation_indented_apps" />
</group>
<group android:checkableBehavior="single">
<item
android:id="@+id/nav_notification_visibility"
android:title="@string/navigation_indented_notification_visibility" />
android:title="@string/navigation_notifications_header" />
<item
android:id="@+id/nav_cards_layout"
android:title="@string/navigation_indented_cards_layout" />
<item
android:id="@+id/nav_debug_notifications"
android:title="@string/navigation_indented_debug_notifications" />
</group>
<group android:checkableBehavior="single">
<item
+2
View File
@@ -15,4 +15,6 @@
<color name="app_filter_inherited_icon">#000000</color>
<color name="app_filter_override_background">#4A4B53</color>
<color name="app_filter_override_icon">#B8CCFF</color>
<color name="visibility_hidden_cross">#FF8A9A</color>
<color name="visibility_edited_background">#24294775</color>
</resources>
+2
View File
@@ -15,4 +15,6 @@
<color name="app_filter_inherited_icon">#FFFFFF</color>
<color name="app_filter_override_background">#D0D0D8</color>
<color name="app_filter_override_icon">#34588E</color>
<color name="visibility_hidden_cross">#B00020</color>
<color name="visibility_edited_background">#183B5C9C</color>
</resources>
+5 -4
View File
@@ -4,13 +4,13 @@
<string name="notification_listener_label">Notifications Master listener</string>
<string name="navigation_open">Open navigation</string>
<string name="navigation_close">Close navigation</string>
<string name="navigation_alerts_header">Notification control</string>
<string name="navigation_alerts_header">Alerts</string>
<string name="navigation_notifications_header">Notifications</string>
<string name="navigation_logging_header">Logging</string>
<string name="page_view_logs">View logs</string>
<string name="page_settings">Settings</string>
<string name="page_alerts">Alerts</string>
<string name="page_alert_status">Alert status</string>
<string name="debug_notifications_title">Debug notifications</string>
<string name="debug_notifications_description">Post up to 50 real, silent notifications to test icon filtering and layout.</string>
<string name="debug_cycle_notifications">Cycle between zero and the selected count every two seconds</string>
<string name="debug_reset_notifications">Reset</string>
@@ -18,7 +18,8 @@
<string name="page_profiles">Profiles</string>
<string name="page_rules">Rules</string>
<string name="page_apps">Apps</string>
<string name="page_notification_visibility">Notification visibility</string>
<string name="page_notification_visibility">Notifications</string>
<string name="page_debug_notifications">Debug icons</string>
<string name="page_cards_layout">Cards layout</string>
<string name="page_filter_apps">Filter apps</string>
<string name="page_log_display">Log display</string>
@@ -28,8 +29,8 @@
<string name="navigation_indented_profiles">&#160;&#160;&#160;&#160;Profiles</string>
<string name="navigation_indented_rules">&#160;&#160;&#160;&#160;Rules</string>
<string name="navigation_indented_apps">&#160;&#160;&#160;&#160;Apps</string>
<string name="navigation_indented_notification_visibility">&#160;&#160;&#160;&#160;Visibility</string>
<string name="navigation_indented_cards_layout">&#160;&#160;&#160;&#160;Cards layout</string>
<string name="navigation_indented_debug_notifications">&#160;&#160;&#160;&#160;Debug icons</string>
<string name="navigation_indented_filter_logging">&#160;&#160;&#160;&#160;Filter logging</string>
<string name="navigation_indented_filter_apps">&#160;&#160;&#160;&#160;Filter apps</string>
</resources>