Reorganize notification visibility settings
This commit is contained in:
+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()
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user