Add notification visibility settings
This commit is contained in:
@@ -0,0 +1,157 @@
|
||||
package se.ajpanton.notificationsmaster
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
import androidx.core.widget.doAfterTextChanged
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.commit
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import se.ajpanton.notificationsmaster.databinding.FragmentNotificationVisibilityBinding
|
||||
import se.ajpanton.notificationsmaster.visibility.AppVisibilityPolicy
|
||||
import se.ajpanton.notificationsmaster.visibility.NotificationSurface
|
||||
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 var appPolicies = emptyMap<String, AppVisibilityPolicy>()
|
||||
private var loadGeneration = 0
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
binding = FragmentNotificationVisibilityBinding.bind(view)
|
||||
store = VisibilityPolicyStore(requireContext())
|
||||
binding!!.appList.layoutManager = LinearLayoutManager(requireContext())
|
||||
binding!!.appList.adapter = adapter
|
||||
binding!!.appSearch.doAfterTextChanged { showApps() }
|
||||
bindEnabled()
|
||||
loadApps()
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
activity?.title = getString(R.string.page_notification_visibility)
|
||||
if (binding != null) {
|
||||
bindEnabled()
|
||||
showApps()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
loadGeneration++
|
||||
binding = null
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
private fun bindEnabled() {
|
||||
val toggle = binding?.visibilityEnabled ?: return
|
||||
val policy = store.load()
|
||||
appPolicies = policy.apps.associateBy(AppVisibilityPolicy::packageName)
|
||||
toggle.setOnCheckedChangeListener(null)
|
||||
toggle.isChecked = policy.enabled
|
||||
toggle.setOnCheckedChangeListener { _, enabled -> store.update { it.copy(enabled = enabled) } }
|
||||
}
|
||||
|
||||
private fun loadApps() {
|
||||
val currentBinding = binding ?: return
|
||||
val generation = ++loadGeneration
|
||||
val context = requireContext().applicationContext
|
||||
currentBinding.appListLoading.visibility = View.VISIBLE
|
||||
viewLifecycleOwner.lifecycleScope.launch {
|
||||
val loaded = withContext(Dispatchers.IO) { InstalledApps.all(context) }
|
||||
if (binding === currentBinding && generation == loadGeneration) {
|
||||
apps = loaded
|
||||
currentBinding.appListLoading.visibility = View.GONE
|
||||
showApps()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun showApps() {
|
||||
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()
|
||||
terms.all(searchable::contains)
|
||||
}.map { app -> AppRow(app, appPolicies[app.packageName]) })
|
||||
}
|
||||
|
||||
private fun openApp(app: InstalledApp) = parentFragmentManager.commit {
|
||||
replace(R.id.content_frame, AppVisibilityFragment.newInstance(app.packageName))
|
||||
addToBackStack(null)
|
||||
}
|
||||
|
||||
private data class AppRow(val app: InstalledApp, val policy: AppVisibilityPolicy?)
|
||||
|
||||
private class AppAdapter(private val open: (InstalledApp) -> Unit) : RecyclerView.Adapter<AppHolder>() {
|
||||
private var rows = emptyList<AppRow>()
|
||||
|
||||
fun submit(newRows: List<AppRow>) {
|
||||
rows = newRows
|
||||
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)
|
||||
}
|
||||
|
||||
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) }
|
||||
}
|
||||
|
||||
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 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)
|
||||
}
|
||||
Reference in New Issue
Block a user