Add OneUI cards layout settings

This commit is contained in:
ajp_anton
2026-08-29 22:26:25 +00:00
parent c14b2c5e61
commit ae1bf8e62b
13 changed files with 346 additions and 0 deletions
@@ -0,0 +1,130 @@
package se.ajpanton.notificationsmaster
import android.os.Bundle
import android.view.View
import android.widget.AdapterView
import android.widget.ArrayAdapter
import android.widget.SeekBar
import android.widget.Spinner
import android.widget.TextView
import androidx.fragment.app.Fragment
import com.google.android.material.switchmaterial.SwitchMaterial
import se.ajpanton.notificationsmaster.databinding.FragmentCardsLayoutBinding
import se.ajpanton.notificationsmaster.visibility.CardsGridSettings
import se.ajpanton.notificationsmaster.visibility.VisibilityPolicy
import se.ajpanton.notificationsmaster.visibility.VisibilityPolicyStore
class CardsLayoutFragment : Fragment(R.layout.fragment_cards_layout) {
private var binding: FragmentCardsLayoutBinding? = null
private lateinit var store: VisibilityPolicyStore
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
binding = FragmentCardsLayoutBinding.bind(view)
store = VisibilityPolicyStore(requireContext())
val policy = store.load()
bind(
controls(lockscreen = true),
policy.lockscreenCards,
VisibilityPolicy::lockscreenCards,
) { current, settings -> current.copy(lockscreenCards = settings) }
bind(
controls(lockscreen = false),
policy.aodCards,
VisibilityPolicy::aodCards,
) { current, settings -> current.copy(aodCards = settings) }
}
override fun onResume() {
super.onResume()
activity?.title = getString(R.string.page_cards_layout)
}
override fun onDestroyView() {
binding = null
super.onDestroyView()
}
private fun bind(
controls: Controls,
initial: CardsGridSettings,
currentSettings: (VisibilityPolicy) -> CardsGridSettings,
update: (VisibilityPolicy, CardsGridSettings) -> VisibilityPolicy,
) {
controls.rows.bindNumbers(0..5, initial.maxRows) { value ->
save(currentSettings, update) { it.copy(maxRows = value) }
}
controls.icons.bindNumbers(1..15, initial.maxIconsPerRow) { value ->
save(currentSettings, update) { it.copy(maxIconsPerRow = value) }
}
controls.even.isChecked = initial.evenDistribution
controls.even.setOnCheckedChangeListener { _, checked ->
save(currentSettings, update) { it.copy(evenDistribution = checked) }
}
controls.limit.isChecked = initial.limitToScreenWidth
controls.limit.setOnCheckedChangeListener { _, checked ->
save(currentSettings, update) { it.copy(limitToScreenWidth = checked) }
}
controls.height.max = 90
controls.height.progress = initial.iconHeightPercent - 10
controls.heightValue.text = "${initial.iconHeightPercent}%"
controls.height.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {
controls.heightValue.text = "${progress + 10}%"
}
override fun onStartTrackingTouch(seekBar: SeekBar) = Unit
override fun onStopTrackingTouch(seekBar: SeekBar) {
save(currentSettings, update) { it.copy(iconHeightPercent = seekBar.progress + 10) }
}
})
}
private fun save(
currentSettings: (VisibilityPolicy) -> CardsGridSettings,
update: (VisibilityPolicy, CardsGridSettings) -> VisibilityPolicy,
change: (CardsGridSettings) -> CardsGridSettings,
) {
store.update { current ->
update(current, change(currentSettings(current)))
}
}
private fun Spinner.bindNumbers(range: IntRange, selected: Int, changed: (Int) -> Unit) {
var current = selected
adapter = ArrayAdapter(
requireContext(),
android.R.layout.simple_spinner_dropdown_item,
range.map(Int::toString),
)
setSelection(selected - range.first, false)
onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onNothingSelected(parent: AdapterView<*>?) = Unit
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
val value = range.first + position
if (value != current) {
current = value
changed(value)
}
}
}
}
private fun controls(lockscreen: Boolean): Controls {
val b = binding!!
return if (lockscreen) b.lockscreenSettings.run {
Controls(maxRows, maxIcons, evenDistribution, limitWidth, iconHeight, iconHeightValue)
} else b.aodSettings.run {
Controls(maxRows, maxIcons, evenDistribution, limitWidth, iconHeight, iconHeightValue)
}
}
private data class Controls(
val rows: Spinner,
val icons: Spinner,
val even: SwitchMaterial,
val limit: SwitchMaterial,
val height: SeekBar,
val heightValue: TextView,
)
}
@@ -168,6 +168,7 @@ class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelecte
page == Page.RULES -> RulesFragment()
page == Page.APPS -> AppsFragment()
page == Page.NOTIFICATION_VISIBILITY -> NotificationVisibilityFragment()
page == Page.CARDS_LAYOUT -> CardsLayoutFragment()
page == Page.LOG_DISPLAY -> LogDisplayFragment()
page == Page.FILTER_LOGGING -> FilterLoggingFragment()
page == Page.FILTER_APPS -> FilterAppsFragment.newInstance()
@@ -373,6 +374,7 @@ class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelecte
RULES(R.id.nav_rules, R.string.page_rules),
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),
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),
@@ -33,6 +33,8 @@ internal object ProcessVisibilityPolicyCache {
fun hasExceptions(packageName: String) = policy.hasExceptions(packageName)
val unlockedIconLimit get() = policy.unlockedIconLimit
val lockscreenCards get() = policy.lockscreenCards
val aodCards get() = policy.aodCards
private fun tryInstall() {
if (installed) return
@@ -0,0 +1,34 @@
package se.ajpanton.notificationsmaster.visibility
data class CardsGridRows(
val rowSizes: List<Int>,
val showsOverflow: Boolean,
) {
val visibleSlots get() = rowSizes.sum()
}
object CardsGridLayout {
fun arrange(itemCount: Int, settings: CardsGridSettings, widthCapacity: Int? = null): CardsGridRows {
require(itemCount >= 0)
val perRow = if (settings.limitToScreenWidth && widthCapacity != null) {
minOf(settings.maxIconsPerRow, widthCapacity.coerceAtLeast(0))
} else {
settings.maxIconsPerRow
}
val capacity = perRow * settings.maxRows
if (itemCount == 0 || capacity == 0) return CardsGridRows(emptyList(), itemCount > 0)
val overflow = itemCount > capacity
val slots = minOf(itemCount, capacity)
val rows = minOf(settings.maxRows, (slots + perRow - 1) / perRow)
if (!settings.evenDistribution || rows == 1) {
var remaining = slots
return CardsGridRows(List(rows) {
minOf(perRow, remaining).also { remaining -= it }
}, overflow)
}
val base = slots / rows
val remainder = slots % rows
return CardsGridRows(List(rows) { base + if (it < remainder) 1 else 0 }, overflow)
}
}
@@ -4,6 +4,25 @@ import com.google.re2j.Pattern
enum class NotificationSurface { AOD, LOCKSCREEN_COLLAPSED, UNLOCKED_STATUSBAR }
data class CardsGridSettings(
val maxRows: Int = 1,
val maxIconsPerRow: Int = 5,
val evenDistribution: Boolean = true,
val limitToScreenWidth: Boolean = true,
val iconHeightPercent: Int,
) {
init {
require(maxRows in 0..5)
require(maxIconsPerRow in 1..15)
require(iconHeightPercent in 10..100)
}
companion object {
val LOCKSCREEN_DEFAULT = CardsGridSettings(iconHeightPercent = 59)
val AOD_DEFAULT = CardsGridSettings(iconHeightPercent = 65)
}
}
data class VisibilityExceptionRule(
val pattern: String,
val blockedSurfaces: Set<NotificationSurface>,
@@ -26,6 +45,8 @@ data class VisibilityPolicy(
val enabled: Boolean = true,
val apps: List<AppVisibilityPolicy> = emptyList(),
val unlockedIconLimit: Int? = null,
val lockscreenCards: CardsGridSettings = CardsGridSettings.LOCKSCREEN_DEFAULT,
val aodCards: CardsGridSettings = CardsGridSettings.AOD_DEFAULT,
) {
init {
require(generation >= 0)
@@ -51,6 +72,8 @@ data class VisibilityNotification(
class CompiledVisibilityPolicy(policy: VisibilityPolicy) {
val generation = policy.generation
val unlockedIconLimit = policy.unlockedIconLimit
val lockscreenCards = policy.lockscreenCards
val aodCards = policy.aodCards
private val enabled = policy.enabled
private val apps = policy.apps.associate { app ->
app.packageName to CompiledAppPolicy(
@@ -11,6 +11,8 @@ internal object VisibilityPolicyJson {
.put("generation", policy.generation)
.put("enabled", policy.enabled)
.put("unlockedIconLimit", policy.unlockedIconLimit ?: JSONObject.NULL)
.put("lockscreenCards", policy.lockscreenCards.toJson())
.put("aodCards", policy.aodCards.toJson())
.put("apps", JSONArray().apply { policy.apps.forEach { put(it.toJson()) } })
.toString().encodeToByteArray()
@@ -22,9 +24,28 @@ internal object VisibilityPolicyJson {
enabled = root.getBoolean("enabled"),
apps = root.getJSONArray("apps").map { (it as JSONObject).toAppPolicy() },
unlockedIconLimit = if (root.isNull("unlockedIconLimit")) null else root.getInt("unlockedIconLimit"),
lockscreenCards = root.optJSONObject("lockscreenCards")?.toCardsGridSettings()
?: CardsGridSettings.LOCKSCREEN_DEFAULT,
aodCards = root.optJSONObject("aodCards")?.toCardsGridSettings()
?: CardsGridSettings.AOD_DEFAULT,
)
}
private fun CardsGridSettings.toJson() = JSONObject()
.put("maxRows", maxRows)
.put("maxIconsPerRow", maxIconsPerRow)
.put("evenDistribution", evenDistribution)
.put("limitToScreenWidth", limitToScreenWidth)
.put("iconHeightPercent", iconHeightPercent)
private fun JSONObject.toCardsGridSettings() = CardsGridSettings(
maxRows = getInt("maxRows"),
maxIconsPerRow = getInt("maxIconsPerRow"),
evenDistribution = getBoolean("evenDistribution"),
limitToScreenWidth = getBoolean("limitToScreenWidth"),
iconHeightPercent = getInt("iconHeightPercent"),
)
private fun AppVisibilityPolicy.toJson() = JSONObject()
.put("packageName", packageName)
.put("blockedSurfaces", JSONArray(blockedSurfaces.map { it.name }))