Reorganize notification visibility settings
This commit is contained in:
@@ -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),
|
||||
|
||||
+116
-51
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
+3
-3
@@ -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 app’s 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>
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">    Profiles</string>
|
||||
<string name="navigation_indented_rules">    Rules</string>
|
||||
<string name="navigation_indented_apps">    Apps</string>
|
||||
<string name="navigation_indented_notification_visibility">    Visibility</string>
|
||||
<string name="navigation_indented_cards_layout">    Cards layout</string>
|
||||
<string name="navigation_indented_debug_notifications">    Debug icons</string>
|
||||
<string name="navigation_indented_filter_logging">    Filter logging</string>
|
||||
<string name="navigation_indented_filter_apps">    Filter apps</string>
|
||||
</resources>
|
||||
|
||||
Reference in New Issue
Block a user