Port OneUI miscellaneous display settings

This commit is contained in:
ajp_anton
2026-08-30 12:40:31 +00:00
parent 192bdb5381
commit 4ff37e6699
12 changed files with 568 additions and 0 deletions
+3
View File
@@ -68,6 +68,9 @@ in Cards mode, with separate lockscreen and always-on-display controls for row
count, icons per row, balanced rows, width limits, and icon size. In Icons mode,
Android—or StatusBarTweak, when installed—continues to render the icons.
The OneUI integration can also keep AOD visible during calls and show the
battery percentage on the lockscreen or AOD while the phone is unplugged.
For visibility control, also give the module the **System UI** scope. Samsung
devices need the **Always On Display** scope for AOD filtering and layout.
Restart Android after changing module scopes.
@@ -170,6 +170,7 @@ class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelecte
page == Page.NOTIFICATION_VISIBILITY -> NotificationVisibilityFragment()
page == Page.CARDS_LAYOUT -> CardsLayoutFragment()
page == Page.DEBUG_NOTIFICATIONS -> DebugNotificationsFragment()
page == Page.MISCELLANEOUS -> MiscellaneousFragment()
page == Page.LOG_DISPLAY -> LogDisplayFragment()
page == Page.FILTER_LOGGING -> FilterLoggingFragment()
page == Page.FILTER_APPS -> FilterAppsFragment.newInstance()
@@ -377,6 +378,7 @@ class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelecte
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),
MISCELLANEOUS(R.id.nav_miscellaneous, R.string.page_miscellaneous),
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),
@@ -0,0 +1,51 @@
package se.ajpanton.notificationsmaster
import android.os.Bundle
import android.view.View
import androidx.fragment.app.Fragment
import com.google.android.material.switchmaterial.SwitchMaterial
import se.ajpanton.notificationsmaster.databinding.FragmentMiscellaneousBinding
import se.ajpanton.notificationsmaster.visibility.SystemUiMiscSettings
import se.ajpanton.notificationsmaster.visibility.VisibilityPolicyStore
class MiscellaneousFragment : Fragment(R.layout.fragment_miscellaneous) {
private var binding: FragmentMiscellaneousBinding? = null
private lateinit var store: VisibilityPolicyStore
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
binding = FragmentMiscellaneousBinding.bind(view)
store = VisibilityPolicyStore(requireContext())
val settings = store.load().systemUiMisc
bind(binding!!.keepAodVisibleDuringCalls, settings.keepAodVisibleDuringCalls) { current, checked ->
current.copy(keepAodVisibleDuringCalls = checked)
}
bind(binding!!.showLockscreenBatteryPercent, settings.showLockscreenBatteryPercentWhenUnplugged) {
current, checked ->
current.copy(showLockscreenBatteryPercentWhenUnplugged = checked)
}
bind(binding!!.showAodBatteryPercent, settings.showAodBatteryPercentWhenUnplugged) { current, checked ->
current.copy(showAodBatteryPercentWhenUnplugged = checked)
}
}
override fun onResume() {
super.onResume()
activity?.title = getString(R.string.page_miscellaneous)
}
override fun onDestroyView() {
binding = null
super.onDestroyView()
}
private fun bind(
toggle: SwitchMaterial,
initial: Boolean,
update: (SystemUiMiscSettings, Boolean) -> SystemUiMiscSettings,
) {
toggle.isChecked = initial
toggle.setOnCheckedChangeListener { _, checked ->
store.update { policy -> policy.copy(systemUiMisc = update(policy.systemUiMisc, checked)) }
}
}
}
@@ -27,6 +27,7 @@ class NotificationsMasterModule : XposedModule() {
OneUiLockscreenBackend(this),
OneUiLockscreenCardsBackend(this),
SamsungAodPluginBackend(this),
OneUiMiscBackend(this),
).filter { it.install(param.classLoader) }
ProcessVisibilityPolicyCache.installWhenReady {
systemUiBackends.orEmpty().forEach(VisibilitySurfaceBackend::onPolicyChanged)
@@ -0,0 +1,422 @@
package se.ajpanton.notificationsmaster.module
import android.Manifest
import android.annotation.SuppressLint
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.content.pm.PackageManager
import android.content.res.ColorStateList
import android.media.AudioManager
import android.os.BatteryManager
import android.os.Build
import android.telecom.TelecomManager
import android.telephony.TelephonyManager
import android.util.Log
import android.view.View
import android.view.ViewGroup
import android.widget.FrameLayout
import android.widget.TextView
import io.github.libxposed.api.XposedInterface
import java.lang.reflect.Field
import java.lang.reflect.Method
import java.text.NumberFormat
import java.util.WeakHashMap
/** OneUI information-display options ported from StatusBarTweak's Misc page. */
internal class OneUiMiscBackend(private val framework: XposedInterface) : VisibilitySurfaceBackend {
private val callVisibility = AodCallVisibilityController(framework)
private val lockscreenBattery = LockscreenBatteryController(framework)
private val aodBattery = AodBatteryController(framework)
override fun install(classLoader: ClassLoader): Boolean {
if (!Build.MANUFACTURER.equals("samsung", ignoreCase = true)) return false
val installed = listOf(
callVisibility.install(classLoader),
lockscreenBattery.install(classLoader),
aodBattery.install(classLoader),
).count { it }
if (installed > 0) Log.i(TAG, "Installed $installed OneUI miscellaneous controllers")
return installed > 0
}
override fun onPolicyChanged() = aodBattery.refresh()
private companion object { const val TAG = "NotificationsMaster" }
}
private class AodCallVisibilityController(private val framework: XposedInterface) {
fun install(classLoader: ClassLoader): Boolean {
var hooks = 0
listOf(
"com.android.systemui.statusbar.phone.CentralSurfacesCommandQueueCallbacks" to "suppressAmbientDisplay",
"com.android.systemui.statusbar.CommandQueue" to "suppressAmbientDisplay",
"com.android.systemui.statusbar.phone.SecLsScrimControlHelper" to "setFrontScrimToBlack",
"com.android.systemui.doze.DozeSuppressor\$1" to "onAlwaysOnSuppressedChanged",
"com.android.systemui.doze.AODUi\$1" to "onAlwaysOnSuppressedChanged",
).forEach { (className, methodName) ->
classLoader.findClass(className)?.methodsNamed(methodName)?.forEach { method ->
framework.intercept(method) { chain ->
val value = chain.args.firstOrNull() as? Boolean
if (value == true && shouldForceAodForCall()) {
chain.proceed(chain.args.toTypedArray().apply { this[0] = false })
} else chain.proceed()
}
hooks++
}
}
classLoader.findClass("com.android.systemui.doze.DozeMachine")?.let { type ->
listOf("requestState", "transitionTo").forEach { name ->
type.methodsNamed(name).forEach { method ->
framework.intercept(method) { chain ->
val requested = chain.args.firstOrNull() as? Enum<*>
val replacement = requested?.takeIf { shouldForceAodForCall() }?.aodReplacement()
if (replacement == null) chain.proceed() else {
chain.proceed(chain.args.toTypedArray().apply { this[0] = replacement })
}
}
hooks++
}
}
}
classLoader.findClass("com.android.systemui.plugins.aod.PluginAODSystemUIConfiguration")
?.methodsNamed("get")?.forEach { method ->
framework.intercept(method) { chain ->
val result = chain.proceed()
if (shouldForceAodForCall() && chain.args.firstOrNull() == PLUGIN_KEY_PHONE_STATE &&
(result as? Int ?: chain.args.getOrNull(1) as? Int) != TelephonyManager.CALL_STATE_IDLE
) TelephonyManager.CALL_STATE_IDLE else result
}
hooks++
}
return hooks > 0
}
private fun Enum<*>.aodReplacement(): Enum<*>? {
if (name != "DOZE" && name != "FINISH") return null
return runCatching {
@Suppress("UNCHECKED_CAST")
java.lang.Enum.valueOf(declaringJavaClass as Class<out Enum<*>>, "DOZE_AOD")
}.getOrNull()
}
private fun shouldForceAodForCall(): Boolean {
if (!ProcessVisibilityPolicyCache.systemUiMisc.keepAodVisibleDuringCalls) return false
return currentApplicationContext()?.let(::isInCallLikeState) == true
}
@SuppressLint("MissingPermission")
@Suppress("DEPRECATION")
private fun isInCallLikeState(context: Context): Boolean {
if (context.checkSelfPermission(Manifest.permission.READ_PHONE_STATE) == PackageManager.PERMISSION_GRANTED) {
if (runCatching { context.getSystemService(TelecomManager::class.java)?.isInCall == true }.getOrDefault(false)) {
return true
}
if (runCatching {
context.getSystemService(TelephonyManager::class.java)?.callState !=
TelephonyManager.CALL_STATE_IDLE
}.getOrDefault(false)
) return true
}
return runCatching {
context.getSystemService(AudioManager::class.java)?.mode in
setOf(AudioManager.MODE_IN_CALL, AudioManager.MODE_IN_COMMUNICATION)
}.getOrDefault(false)
}
private companion object { const val PLUGIN_KEY_PHONE_STATE = 0x2 }
}
private class LockscreenBatteryController(private val framework: XposedInterface) {
private var failed = false
fun install(classLoader: ClassLoader): Boolean {
val eventClass = classLoader.findClass("com.android.systemui.statusbar.IndicationEventType") ?: return false
val batteryEvent = eventClass.staticField("BATTERY") ?: return false
val restingEvent = eventClass.staticField("BATTERY_RESTING") ?: return false
val controller = classLoader.findClass(
"com.android.systemui.statusbar.KeyguardSecIndicationController",
) ?: return false
val methods = controller.methodsNamed("addInitialIndication")
methods.forEach { method ->
framework.intercept(method) { chain ->
val result = chain.proceed()
runCatching {
val target = chain.thisObject
val context = target.field("mContext") as? Context ?: return@runCatching
val state = visibleLockscreenBattery(context, target) ?: return@runCatching
val text = state.formattedPercent()
val color = target.field("mInitialTextColorState") as? ColorStateList
target.invokeNamed("addIndicationTimeout", batteryEvent, text, color, false)
target.invokeNamed("addIndication", restingEvent, text)
}.onFailure(::logFailureOnce)
result
}
}
return methods.isNotEmpty()
}
private fun visibleLockscreenBattery(context: Context, controller: Any): BatteryState? {
if (!ProcessVisibilityPolicyCache.systemUiMisc.showLockscreenBatteryPercentWhenUnplugged) return null
if (controller.field("mDozing") == true) return null
return currentBatteryState(context)?.takeUnless { it.plugged || it.charged }
}
private fun logFailureOnce(error: Throwable) {
if (failed) return
failed = true
Log.w(TAG, "Could not update the OneUI lockscreen battery indication", error)
}
}
private class AodBatteryController(private val framework: XposedInterface) {
private var manager: Any? = null
private var host: FrameLayout? = null
private val pendingRetries = mutableSetOf<Runnable>()
private val savedText = WeakHashMap<TextView, Pair<CharSequence, Int>>()
private val savedVisibility = WeakHashMap<View, Int>()
private var failed = false
fun install(classLoader: ClassLoader): Boolean {
var hooks = 0
classLoader.findClass("com.android.systemui.doze.PluginAODManager\$6")
?.methodsNamed("setBottomArea")?.forEach { method ->
framework.intercept(method) { chain ->
val result = chain.proceed()
manager = chain.thisObject.field("this\$0")
host = resolveBottomDozeArea(manager)
update(host, chain.args.firstOrNull { it is View } as? View ?: result as? View)
result
}
hooks++
}
classLoader.findClass("com.android.systemui.doze.PluginAODManager")
?.methodsNamed("setIsDozing")?.forEach { method ->
framework.intercept(method) { chain ->
val result = chain.proceed()
manager = chain.thisObject
host = resolveBottomDozeArea(manager)
val target = host?.getChildAt(0)
if (chain.args.firstOrNull() == true) update(host, target) else clear(host, target)
result
}
hooks++
}
classLoader.findClass(
"com.android.systemui.statusbar.KeyguardSecIndicationController\$SecKeyguardCallback",
)?.methodsNamed("onRefreshBatteryInfo")?.forEach { method ->
framework.intercept(method) { chain ->
val result = chain.proceed()
update(host, host?.getChildAt(0))
result
}
hooks++
}
return hooks > 0
}
fun refresh() {
host?.post { update(host, host?.getChildAt(0)) }
}
private fun update(host: FrameLayout?, source: View?) {
runCatching {
val frame = host ?: return
val state = currentBatteryState(frame.context)
if (!ProcessVisibilityPolicyCache.systemUiMisc.showAodBatteryPercentWhenUnplugged ||
state == null || state.plugged || state.charged
) return clear(frame, source)
val text = state.formattedPercent().toString()
val component = pluginRoot(source, frame).field("p") ?: return clear(frame, source)
val componentView = component.field("c") as? View ?: return clear(frame, source)
val indication = componentView.field("d") as? TextView ?: return clear(frame, source)
cancelRetries(frame)
apply(componentView, indication, text)
RETRY_DELAYS.forEach { delay ->
val retry = object : Runnable {
override fun run() {
pendingRetries -= this
if (this@AodBatteryController.host !== frame) return
val current = currentBatteryState(frame.context)
if (!ProcessVisibilityPolicyCache.systemUiMisc.showAodBatteryPercentWhenUnplugged ||
current == null || current.plugged || current.charged
) {
clear(frame, source)
return
}
val root = pluginRoot(source, frame).field("p").field("c") as? View ?: return
val textView = root.field("d") as? TextView ?: return
apply(root, textView, current.formattedPercent().toString())
}
}
pendingRetries += retry
frame.postDelayed(retry, delay)
}
}.onFailure(::logFailureOnce)
}
private fun apply(componentView: View, indication: TextView, text: String) {
savedText.putIfAbsent(indication, indication.text to indication.visibility)
indication.text = text
indication.visibility = View.VISIBLE
hide(componentView.field("k") as? View)
hide(componentView.field("c") as? View)
hide(componentView.field("l") as? View)
forceVisible(indication)
forceVisible(componentView.field("b") as? View)
forceVisible(componentView)
}
private fun clear(host: FrameLayout?, source: View?) {
cancelRetries(host)
val componentView = pluginRoot(source, host).field("p").field("c") as? View
(componentView?.field("d") as? TextView)?.let { view ->
savedText.remove(view)?.let { (text, visibility) ->
view.text = text
view.visibility = visibility
}
}
listOf("k", "c", "l").forEach { name ->
(componentView?.field(name) as? View)?.let { restoreVisibility(it) }
}
}
private fun hide(view: View?) {
view ?: return
rememberVisibility(view)
view.visibility = View.GONE
}
private fun rememberVisibility(view: View) {
savedVisibility.putIfAbsent(view, view.visibility)
}
private fun restoreVisibility(view: View) {
savedVisibility.remove(view)?.let { view.visibility = it }
}
private fun forceVisible(start: View?) {
var view = start
while (view != null) {
view.visibility = View.VISIBLE
view.alpha = 1f
view.translationX = 0f
view.translationY = 0f
view.scaleX = 1f
view.scaleY = 1f
view.requestLayout()
view.invalidate()
view = view.parent as? View
}
}
private fun pluginRoot(source: View?, host: FrameLayout?): Any? = when {
source is ViewGroup -> source
host?.childCount ?: 0 > 0 -> host?.getChildAt(0)
else -> null
}
private fun resolveBottomDozeArea(manager: Any?): FrameLayout? = runCatching {
val panel = manager.field("mPanelViewControllerLazy").invokeNamed("get")
val controller = panel.field("mKeyguardSecBottomAreaViewController")
val bottomView = controller.invokeNamed("getView")
bottomView.field("bottomDozeArea\$delegate").invokeNamed("getValue") as? FrameLayout
}.getOrNull()
private fun cancelRetries(host: FrameLayout?) {
pendingRetries.forEach { host?.removeCallbacks(it) }
pendingRetries.clear()
}
private fun logFailureOnce(error: Throwable) {
if (failed) return
failed = true
Log.w(TAG, "Could not update the OneUI AOD battery indication", error)
}
private companion object { val RETRY_DELAYS = longArrayOf(96L, 224L, 480L) }
}
private data class BatteryState(val level: Int, val plugged: Boolean, val charged: Boolean) {
fun formattedPercent(): CharSequence = NumberFormat.getPercentInstance().format(level / 100.0)
}
@Suppress("DEPRECATION")
private fun currentBatteryState(context: Context): BatteryState? {
val app = context.applicationContext
val filter = IntentFilter(Intent.ACTION_BATTERY_CHANGED)
val intent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
app.registerReceiver(null, filter, Context.RECEIVER_EXPORTED)
} else {
app.registerReceiver(null, filter)
} ?: return null
val level = intent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1)
val scale = intent.getIntExtra(BatteryManager.EXTRA_SCALE, 100)
if (level < 0 || scale <= 0) return null
val percent = Math.round((level * 100f) / scale).coerceIn(0, 100)
val status = intent.getIntExtra(BatteryManager.EXTRA_STATUS, -1)
val plugged = intent.getIntExtra(BatteryManager.EXTRA_PLUGGED, 0) != 0 ||
status == BatteryManager.BATTERY_STATUS_CHARGING || status == BatteryManager.BATTERY_STATUS_FULL
return BatteryState(percent, plugged, status == BatteryManager.BATTERY_STATUS_FULL || percent >= 100)
}
private fun currentApplicationContext(): Context? = runCatching {
Class.forName("android.app.ActivityThread").getMethod("currentApplication").invoke(null) as? Context
}.getOrNull()
private fun ClassLoader.findClass(name: String) = runCatching { Class.forName(name, false, this) }.getOrNull()
private fun Class<*>.methodsNamed(name: String): List<Method> = declaredMethods.filter { it.name == name }.onEach {
it.isAccessible = true
}
private fun Class<*>.staticField(name: String): Any? = findField(name)?.get(null)
private fun Any?.field(name: String): Any? = this?.javaClass?.findField(name)?.get(this)
private fun Class<*>.findField(name: String): Field? {
var type: Class<*>? = this
while (type != null) {
runCatching { type.getDeclaredField(name) }.getOrNull()?.let {
it.isAccessible = true
return it
}
type = type.superclass
}
return null
}
private fun Any?.invokeNamed(name: String, vararg args: Any?): Any? {
val target = this ?: return null
var type: Class<*>? = target.javaClass
while (type != null) {
type.declaredMethods.firstOrNull { method ->
method.name == name && method.parameterCount == args.size &&
method.parameterTypes.indices.all { method.parameterTypes[it].accepts(args[it]) }
}?.let { method ->
method.isAccessible = true
return method.invoke(target, *args)
}
type = type.superclass
}
error("${target.javaClass.name}.$name(${args.size}) is unavailable")
}
private fun Class<*>.accepts(value: Any?): Boolean {
if (value == null) return !isPrimitive
val boxed = when (this) {
Boolean::class.javaPrimitiveType -> Boolean::class.java
Int::class.javaPrimitiveType -> Int::class.java
Long::class.javaPrimitiveType -> Long::class.java
Float::class.javaPrimitiveType -> Float::class.java
Double::class.javaPrimitiveType -> Double::class.java
else -> this
}
return boxed.isInstance(value)
}
private fun XposedInterface.intercept(method: Method, callback: (XposedInterface.Chain) -> Any?) {
hook(method).intercept(callback)
}
private const val TAG = "NotificationsMaster"
@@ -35,6 +35,7 @@ internal object ProcessVisibilityPolicyCache {
val unlockedIconLimit get() = policy.unlockedIconLimit
val lockscreenCards get() = policy.lockscreenCards
val aodCards get() = policy.aodCards
val systemUiMisc get() = policy.systemUiMisc
private fun tryInstall() {
if (installed) return
@@ -40,6 +40,12 @@ data class AppVisibilityPolicy(
init { require(packageName.isNotBlank()) }
}
data class SystemUiMiscSettings(
val keepAodVisibleDuringCalls: Boolean = false,
val showLockscreenBatteryPercentWhenUnplugged: Boolean = true,
val showAodBatteryPercentWhenUnplugged: Boolean = true,
)
data class VisibilityPolicy(
val generation: Long = 0,
val enabled: Boolean = true,
@@ -47,6 +53,7 @@ data class VisibilityPolicy(
val unlockedIconLimit: Int? = null,
val lockscreenCards: CardsGridSettings = CardsGridSettings.LOCKSCREEN_DEFAULT,
val aodCards: CardsGridSettings = CardsGridSettings.AOD_DEFAULT,
val systemUiMisc: SystemUiMiscSettings = SystemUiMiscSettings(),
) {
init {
require(generation >= 0)
@@ -74,6 +81,7 @@ class CompiledVisibilityPolicy(policy: VisibilityPolicy) {
val unlockedIconLimit = policy.unlockedIconLimit
val lockscreenCards = policy.lockscreenCards
val aodCards = policy.aodCards
val systemUiMisc = policy.systemUiMisc
private val enabled = policy.enabled
private val apps = policy.apps.associate { app ->
app.packageName to CompiledAppPolicy(
@@ -13,6 +13,7 @@ internal object VisibilityPolicyJson {
.put("unlockedIconLimit", policy.unlockedIconLimit ?: JSONObject.NULL)
.put("lockscreenCards", policy.lockscreenCards.toJson())
.put("aodCards", policy.aodCards.toJson())
.put("systemUiMisc", policy.systemUiMisc.toJson())
.put("apps", JSONArray().apply { policy.apps.forEach { put(it.toJson()) } })
.toString().encodeToByteArray()
@@ -28,9 +29,22 @@ internal object VisibilityPolicyJson {
?: CardsGridSettings.LOCKSCREEN_DEFAULT,
aodCards = root.optJSONObject("aodCards")?.toCardsGridSettings()
?: CardsGridSettings.AOD_DEFAULT,
systemUiMisc = root.optJSONObject("systemUiMisc")?.toSystemUiMiscSettings()
?: SystemUiMiscSettings(),
)
}
private fun SystemUiMiscSettings.toJson() = JSONObject()
.put("keepAodVisibleDuringCalls", keepAodVisibleDuringCalls)
.put("showLockscreenBatteryPercentWhenUnplugged", showLockscreenBatteryPercentWhenUnplugged)
.put("showAodBatteryPercentWhenUnplugged", showAodBatteryPercentWhenUnplugged)
private fun JSONObject.toSystemUiMiscSettings() = SystemUiMiscSettings(
keepAodVisibleDuringCalls = getBoolean("keepAodVisibleDuringCalls"),
showLockscreenBatteryPercentWhenUnplugged = getBoolean("showLockscreenBatteryPercentWhenUnplugged"),
showAodBatteryPercentWhenUnplugged = getBoolean("showAodBatteryPercentWhenUnplugged"),
)
private fun CardsGridSettings.toJson() = JSONObject()
.put("maxRows", maxRows)
.put("maxIconsPerRow", maxIconsPerRow)
@@ -0,0 +1,53 @@
<?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">
<com.google.android.material.switchmaterial.SwitchMaterial
android:id="@+id/keep_aod_visible_during_calls"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/keep_aod_visible_during_calls" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:text="@string/keep_aod_visible_during_calls_hint"
android:textAppearance="?attr/textAppearanceBody2" />
<com.google.android.material.switchmaterial.SwitchMaterial
android:id="@+id/show_lockscreen_battery_percent"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:text="@string/show_lockscreen_battery_percent" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:text="@string/show_lockscreen_battery_percent_hint"
android:textAppearance="?attr/textAppearanceBody2" />
<com.google.android.material.switchmaterial.SwitchMaterial
android:id="@+id/show_aod_battery_percent"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:text="@string/show_aod_battery_percent" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:text="@string/show_aod_battery_percent_hint"
android:textAppearance="?attr/textAppearanceBody2" />
</LinearLayout>
</ScrollView>
+3
View File
@@ -32,6 +32,9 @@
<item
android:id="@+id/nav_debug_notifications"
android:title="@string/navigation_indented_debug_notifications" />
<item
android:id="@+id/nav_miscellaneous"
android:title="@string/navigation_indented_miscellaneous" />
</group>
<group android:checkableBehavior="single">
<item
+8
View File
@@ -21,6 +21,7 @@
<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_miscellaneous">Miscellaneous</string>
<string name="page_filter_apps">Filter apps</string>
<string name="page_log_display">Log display</string>
<string name="page_filter_logging">Filter logging</string>
@@ -31,6 +32,13 @@
<string name="navigation_indented_apps">&#160;&#160;&#160;&#160;Apps</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_miscellaneous">&#160;&#160;&#160;&#160;Miscellaneous</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>
<string name="keep_aod_visible_during_calls">Keep AOD visible during calls</string>
<string name="keep_aod_visible_during_calls_hint">Prevents OneUI from hiding the always-on display while a call is active.</string>
<string name="show_lockscreen_battery_percent">Show battery % on lockscreen when unplugged</string>
<string name="show_lockscreen_battery_percent_hint">Shows the battery percentage even when the phone is not charging.</string>
<string name="show_aod_battery_percent">Show battery % on AOD when unplugged</string>
<string name="show_aod_battery_percent_hint">Shows the battery percentage in the bottom AOD area even when the phone is not charging.</string>
</resources>
@@ -11,6 +11,7 @@ class VisibilityPolicyJsonTest {
unlockedIconLimit = 7,
lockscreenCards = CardsGridSettings(2, 7, false, false, 72),
aodCards = CardsGridSettings(3, 6, true, true, 48),
systemUiMisc = SystemUiMiscSettings(true, false, true),
apps = listOf(AppVisibilityPolicy(
packageName = "example.app",
blockedSurfaces = setOf(NotificationSurface.AOD),
@@ -37,6 +38,7 @@ class VisibilityPolicyJsonTest {
assertEquals(CardsGridSettings.LOCKSCREEN_DEFAULT, decoded.lockscreenCards)
assertEquals(CardsGridSettings.AOD_DEFAULT, decoded.aodCards)
assertEquals(SystemUiMiscSettings(), decoded.systemUiMisc)
}
@Test(expected = IllegalArgumentException::class)