From dd177c52fa25498d1331efc59087ed0efa52020a Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 12 May 2026 04:19:16 +0000
Subject: [PATCH 01/22] Initial plan
From c45d9cd83799ae23adb284c622194a32e2537fb1 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 12 May 2026 04:23:30 +0000
Subject: [PATCH 02/22] Add system stream controls and brightness panel updates
Agent-Logs-Url: https://github.com/yume-chan/VolumeManager/sessions/0dfe35c2-a621-4609-8a1b-327139ed70d2
Co-authored-by: yume-chan <1330321+yume-chan@users.noreply.github.com>
---
app/build.gradle.kts | 4 +-
app/src/main/AndroidManifest.xml | 2 +
.../java/moe/chensi/volume/MainActivity.kt | 14 +-
.../main/java/moe/chensi/volume/Manager.kt | 13 +
.../main/java/moe/chensi/volume/Service.kt | 26 +-
.../volume/compose/StreamVolumeSlider.kt | 66 ++--
.../volume/compose/SystemVolumePanel.kt | 300 ++++++++++++++++++
.../chensi/volume/data/AppPreferencesStore.kt | 22 +-
app/src/main/res/values-zh-rCN/strings.xml | 14 +-
app/src/main/res/values/strings.xml | 14 +-
10 files changed, 419 insertions(+), 56 deletions(-)
create mode 100644 app/src/main/java/moe/chensi/volume/compose/SystemVolumePanel.kt
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index ab580c6..b5fb4d4 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -45,8 +45,8 @@ android {
applicationId = "moe.chensi.volume"
minSdk = 33
targetSdk = 35
- versionCode = 19
- versionName = "0.3-beta.15"
+ versionCode = 20
+ versionName = "0.3-beta.16"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 00afe26..4d6c076 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -5,6 +5,8 @@
+
+
diff --git a/app/src/main/java/moe/chensi/volume/MainActivity.kt b/app/src/main/java/moe/chensi/volume/MainActivity.kt
index 67fd1f9..41a9ae3 100644
--- a/app/src/main/java/moe/chensi/volume/MainActivity.kt
+++ b/app/src/main/java/moe/chensi/volume/MainActivity.kt
@@ -60,6 +60,7 @@ import androidx.core.net.toUri
import moe.chensi.volume.compose.AboutDialog
import moe.chensi.volume.compose.AppVolumeList
import moe.chensi.volume.compose.CrashReportDialog
+import moe.chensi.volume.compose.SystemVolumePanel
import moe.chensi.volume.compose.ToggleButton
import moe.chensi.volume.ui.theme.VolumeManagerTheme
import org.joor.Reflect
@@ -326,7 +327,18 @@ class MainActivity : ComponentActivity() {
apps = manager.apps.values,
showEmpty = true,
showAll = showAll,
- onShowAll = { showAll = true })
+ onShowAll = { showAll = true },
+ content = {
+ item("system_volume_panel_main") {
+ SystemVolumePanel(
+ audioManager = manager.audioManager,
+ showCallVolumeAlways = true,
+ showHideButton = false,
+ showSliders = true,
+ onShowSlidersChange = { },
+ )
+ }
+ })
}
}
}
diff --git a/app/src/main/java/moe/chensi/volume/Manager.kt b/app/src/main/java/moe/chensi/volume/Manager.kt
index d1fb18f..ff0206f 100644
--- a/app/src/main/java/moe/chensi/volume/Manager.kt
+++ b/app/src/main/java/moe/chensi/volume/Manager.kt
@@ -46,6 +46,17 @@ class Manager(context: Context, dataStore: DataStore) {
private val packageManager by lazy { PackageManagerProxy.get(context) }
private val appPreferencesStore = AppPreferencesStore(dataStore)
+ private var _showSystemSlidersInPopup by mutableStateOf(appPreferencesStore.showSystemSlidersInPopup)
+ var showSystemSlidersInPopup: Boolean
+ get() = _showSystemSlidersInPopup
+ set(value) {
+ if (_showSystemSlidersInPopup == value) {
+ return
+ }
+
+ _showSystemSlidersInPopup = value
+ appPreferencesStore.showSystemSlidersInPopup = value
+ }
val apps = mutableStateMapOf()
@@ -162,6 +173,8 @@ class Manager(context: Context, dataStore: DataStore) {
}
}
+ _showSystemSlidersInPopup = appPreferencesStore.showSystemSlidersInPopup
+
if (first) {
initialize()
}
diff --git a/app/src/main/java/moe/chensi/volume/Service.kt b/app/src/main/java/moe/chensi/volume/Service.kt
index 5478eee..8ebb214 100644
--- a/app/src/main/java/moe/chensi/volume/Service.kt
+++ b/app/src/main/java/moe/chensi/volume/Service.kt
@@ -28,9 +28,6 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.filled.MusicNote
-import androidx.compose.material.icons.filled.Notifications
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
@@ -45,7 +42,7 @@ import androidx.savedstate.SavedStateRegistryController
import androidx.savedstate.SavedStateRegistryOwner
import androidx.savedstate.setViewTreeSavedStateRegistryOwner
import moe.chensi.volume.compose.AppVolumeList
-import moe.chensi.volume.compose.StreamVolumeSlider
+import moe.chensi.volume.compose.SystemVolumePanel
import moe.chensi.volume.compose.VolumeChangeObserver
import moe.chensi.volume.system.ActivityTaskManagerProxy
import moe.chensi.volume.ui.theme.VolumeManagerTheme
@@ -203,22 +200,13 @@ class Service : AccessibilityService() {
showAll = false,
onChange = this@Service.handler::startIdleTimer
) {
- item(AudioManager.STREAM_MUSIC) {
- StreamVolumeSlider(
- AudioManager.STREAM_MUSIC,
- Icons.Default.MusicNote,
- "Music",
- manager.audioManager,
- onChange = this@Service.handler::startIdleTimer
- )
- }
-
- item(AudioManager.STREAM_NOTIFICATION) {
- StreamVolumeSlider(
- AudioManager.STREAM_NOTIFICATION,
- Icons.Default.Notifications,
- "Notifications",
+ item("system_volume_panel") {
+ SystemVolumePanel(
manager.audioManager,
+ showCallVolumeAlways = false,
+ showHideButton = true,
+ showSliders = manager.showSystemSlidersInPopup,
+ onShowSlidersChange = { manager.showSystemSlidersInPopup = it },
onChange = this@Service.handler::startIdleTimer
)
}
diff --git a/app/src/main/java/moe/chensi/volume/compose/StreamVolumeSlider.kt b/app/src/main/java/moe/chensi/volume/compose/StreamVolumeSlider.kt
index cdb0a0f..c8c899f 100644
--- a/app/src/main/java/moe/chensi/volume/compose/StreamVolumeSlider.kt
+++ b/app/src/main/java/moe/chensi/volume/compose/StreamVolumeSlider.kt
@@ -6,6 +6,7 @@ import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.media.AudioManager
+import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
@@ -80,6 +81,7 @@ fun StreamVolumeSlider(
icon: ImageVector,
name: String,
audioManager: AudioManager,
+ footer: (@Composable () -> Unit)? = null,
onChange: (() -> Unit)? = null
) {
val context = LocalContext.current
@@ -103,39 +105,43 @@ fun StreamVolumeSlider(
volume = audioManager.getStreamVolume(streamType)
}
- TrackSlider(
- cornerRadius = 20.dp,
- value = volume.toFloat(),
- valueRange = 0f..maxVolume,
- onValueChange = { value ->
- volume = value.toInt()
- audioManager.setStreamVolume(streamType, value.toInt(), 0)
- onChange?.invoke()
- },
- ) {
- Row(
- horizontalArrangement = Arrangement.spacedBy(8.dp),
- verticalAlignment = Alignment.CenterVertically,
- modifier = Modifier.padding(12.dp, 8.dp)
+ Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
+ TrackSlider(
+ cornerRadius = 20.dp,
+ value = volume.toFloat(),
+ valueRange = 0f..maxVolume,
+ onValueChange = { value ->
+ volume = value.toInt()
+ audioManager.setStreamVolume(streamType, value.toInt(), 0)
+ onChange?.invoke()
+ },
) {
- Icon(
- imageVector = icon,
- contentDescription = name,
- modifier = Modifier.size(32.dp),
- )
+ Row(
+ horizontalArrangement = Arrangement.spacedBy(8.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ modifier = Modifier.padding(12.dp, 8.dp)
+ ) {
+ Icon(
+ imageVector = icon,
+ contentDescription = name,
+ modifier = Modifier.size(32.dp),
+ )
- Text(
- text = name,
- modifier = Modifier.weight(1f),
- maxLines = 1,
- overflow = TextOverflow.Ellipsis
- )
+ Text(
+ text = name,
+ modifier = Modifier.weight(1f),
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis
+ )
- Text(
- text = "$volume/${maxVolume.toInt()}",
- style = Typography.bodySmall,
- maxLines = 1,
- )
+ Text(
+ text = "$volume/${maxVolume.toInt()}",
+ style = Typography.bodySmall,
+ maxLines = 1,
+ )
+ }
}
+
+ footer?.invoke()
}
}
diff --git a/app/src/main/java/moe/chensi/volume/compose/SystemVolumePanel.kt b/app/src/main/java/moe/chensi/volume/compose/SystemVolumePanel.kt
new file mode 100644
index 0000000..895f200
--- /dev/null
+++ b/app/src/main/java/moe/chensi/volume/compose/SystemVolumePanel.kt
@@ -0,0 +1,300 @@
+package moe.chensi.volume.compose
+
+import android.app.NotificationManager
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import android.database.ContentObserver
+import android.media.AudioManager
+import android.os.Handler
+import android.os.Looper
+import android.provider.Settings
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.weight
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Alarm
+import androidx.compose.material.icons.filled.Brightness6
+import androidx.compose.material.icons.filled.DoNotDisturbOn
+import androidx.compose.material.icons.filled.NotificationsActive
+import androidx.compose.material.icons.filled.NotificationsNone
+import androidx.compose.material.icons.filled.PhoneInTalk
+import androidx.compose.material.icons.filled.Vibration
+import androidx.compose.material.icons.filled.Visibility
+import androidx.compose.material.icons.filled.VisibilityOff
+import androidx.compose.material.icons.filled.VolumeUp
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Icon
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableIntStateOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.unit.dp
+import moe.chensi.volume.R
+import moe.chensi.volume.ui.theme.Typography
+import kotlinx.coroutines.delay
+
+private fun isCallMode(mode: Int): Boolean {
+ return mode == AudioManager.MODE_IN_CALL || mode == AudioManager.MODE_IN_COMMUNICATION
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun SystemVolumePanel(
+ audioManager: AudioManager,
+ showCallVolumeAlways: Boolean,
+ showHideButton: Boolean,
+ showSliders: Boolean,
+ onShowSlidersChange: (Boolean) -> Unit,
+ onChange: (() -> Unit)? = null
+) {
+ var inCallMode by remember { mutableStateOf(isCallMode(audioManager.mode)) }
+
+ LaunchedEffect(showCallVolumeAlways) {
+ if (!showCallVolumeAlways) {
+ while (true) {
+ inCallMode = isCallMode(audioManager.mode)
+ delay(500)
+ }
+ }
+ }
+
+ Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
+ if (showHideButton) {
+ Row(
+ horizontalArrangement = Arrangement.End,
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 4.dp)
+ ) {
+ ToggleButton(
+ checked = showSliders,
+ checkedDescription = stringResource(R.string.hide_system_sliders_in_popup),
+ checkedIcon = Icons.Default.Visibility,
+ uncheckedDescription = stringResource(R.string.show_system_sliders_in_popup),
+ uncheckedIcon = Icons.Default.VisibilityOff,
+ onCheckedChange = onShowSlidersChange
+ )
+ }
+ }
+
+ if (!showSliders) {
+ return@Column
+ }
+
+ if (!showCallVolumeAlways && inCallMode) {
+ StreamVolumeSlider(
+ streamType = AudioManager.STREAM_VOICE_CALL,
+ icon = Icons.Default.PhoneInTalk,
+ name = stringResource(R.string.stream_call),
+ audioManager = audioManager,
+ onChange = onChange
+ )
+ }
+
+ StreamVolumeSlider(
+ streamType = AudioManager.STREAM_MUSIC,
+ icon = Icons.Default.VolumeUp,
+ name = stringResource(R.string.stream_media),
+ audioManager = audioManager,
+ onChange = onChange
+ )
+
+ StreamVolumeSlider(
+ streamType = AudioManager.STREAM_RING,
+ icon = Icons.Default.NotificationsActive,
+ name = stringResource(R.string.stream_ring),
+ audioManager = audioManager,
+ onChange = onChange
+ )
+
+ if (showCallVolumeAlways) {
+ StreamVolumeSlider(
+ streamType = AudioManager.STREAM_VOICE_CALL,
+ icon = Icons.Default.PhoneInTalk,
+ name = stringResource(R.string.stream_call),
+ audioManager = audioManager,
+ onChange = onChange
+ )
+ }
+
+ StreamVolumeSlider(
+ streamType = AudioManager.STREAM_ALARM,
+ icon = Icons.Default.Alarm,
+ name = stringResource(R.string.stream_alarm),
+ audioManager = audioManager,
+ onChange = onChange
+ )
+
+ StreamVolumeSlider(
+ streamType = AudioManager.STREAM_NOTIFICATION,
+ icon = Icons.Default.NotificationsNone,
+ name = stringResource(R.string.stream_notification),
+ audioManager = audioManager,
+ footer = { NotificationModeToggles(audioManager = audioManager, onChange = onChange) },
+ onChange = onChange
+ )
+
+ BrightnessSlider(onChange = onChange)
+ }
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+private fun NotificationModeToggles(audioManager: AudioManager, onChange: (() -> Unit)? = null) {
+ val context = LocalContext.current
+ val notificationManager = context.getSystemService(NotificationManager::class.java)!!
+
+ var ringerMode by remember { mutableIntStateOf(audioManager.ringerMode) }
+ var interruptionFilter by remember { mutableIntStateOf(notificationManager.currentInterruptionFilter) }
+
+ DisposableEffect(context) {
+ val receiver = object : BroadcastReceiver() {
+ override fun onReceive(context: Context?, intent: Intent?) {
+ ringerMode = audioManager.ringerMode
+ interruptionFilter = notificationManager.currentInterruptionFilter
+ }
+ }
+
+ val filter = IntentFilter().apply {
+ addAction(AudioManager.RINGER_MODE_CHANGED_ACTION)
+ addAction(NotificationManager.ACTION_INTERRUPTION_FILTER_CHANGED)
+ }
+ context.registerReceiver(receiver, filter, Context.RECEIVER_NOT_EXPORTED)
+ onDispose {
+ context.unregisterReceiver(receiver)
+ }
+ }
+
+ Row(horizontalArrangement = Arrangement.spacedBy(4.dp), modifier = Modifier.padding(start = 4.dp)) {
+ ToggleButton(
+ checked = ringerMode == AudioManager.RINGER_MODE_VIBRATE,
+ checkedDescription = stringResource(R.string.disable_vibrate_mode),
+ checkedIcon = Icons.Default.Vibration,
+ uncheckedDescription = stringResource(R.string.enable_vibrate_mode),
+ uncheckedIcon = Icons.Default.VolumeUp
+ ) {
+ audioManager.ringerMode =
+ if (it) AudioManager.RINGER_MODE_VIBRATE else AudioManager.RINGER_MODE_NORMAL
+ ringerMode = audioManager.ringerMode
+ onChange?.invoke()
+ }
+
+ ToggleButton(
+ checked = interruptionFilter != NotificationManager.INTERRUPTION_FILTER_ALL,
+ checkedDescription = stringResource(R.string.disable_do_not_disturb),
+ checkedIcon = Icons.Default.DoNotDisturbOn,
+ uncheckedDescription = stringResource(R.string.enable_do_not_disturb),
+ uncheckedIcon = Icons.Default.NotificationsActive
+ ) {
+ try {
+ notificationManager.setInterruptionFilter(
+ if (it) NotificationManager.INTERRUPTION_FILTER_NONE else NotificationManager.INTERRUPTION_FILTER_ALL
+ )
+ } catch (_: SecurityException) {
+ }
+ interruptionFilter = notificationManager.currentInterruptionFilter
+ onChange?.invoke()
+ }
+ }
+}
+
+@Composable
+private fun BrightnessSlider(onChange: (() -> Unit)? = null) {
+ val context = LocalContext.current
+ val contentResolver = context.contentResolver
+
+ fun getBrightness(): Int {
+ return try {
+ Settings.System.getInt(contentResolver, Settings.System.SCREEN_BRIGHTNESS)
+ } catch (_: Settings.SettingNotFoundException) {
+ 127
+ }
+ }
+
+ var brightness by remember { mutableIntStateOf(getBrightness()) }
+ val canWrite = Settings.System.canWrite(context)
+
+ DisposableEffect(contentResolver) {
+ val observer = object : ContentObserver(Handler(Looper.getMainLooper())) {
+ override fun onChange(selfChange: Boolean) {
+ brightness = getBrightness()
+ }
+ }
+
+ contentResolver.registerContentObserver(
+ Settings.System.getUriFor(Settings.System.SCREEN_BRIGHTNESS),
+ false,
+ observer
+ )
+
+ onDispose {
+ contentResolver.unregisterContentObserver(observer)
+ }
+ }
+
+ TrackSlider(
+ cornerRadius = 20.dp,
+ value = brightness.toFloat(),
+ valueRange = 0f..255f,
+ enabled = canWrite,
+ onValueChange = { value ->
+ brightness = value.toInt()
+ if (canWrite) {
+ Settings.System.putInt(contentResolver, Settings.System.SCREEN_BRIGHTNESS, brightness)
+ onChange?.invoke()
+ }
+ }
+ ) {
+ StreamSliderLabelRow(
+ icon = Icons.Default.Brightness6,
+ name = stringResource(R.string.brightness),
+ value = brightness,
+ max = 255
+ )
+ }
+}
+
+@Composable
+private fun StreamSliderLabelRow(icon: ImageVector, name: String, value: Int, max: Int) {
+ Row(
+ horizontalArrangement = Arrangement.spacedBy(8.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ modifier = Modifier.padding(12.dp, 8.dp)
+ ) {
+ Icon(
+ imageVector = icon,
+ contentDescription = name,
+ modifier = Modifier.size(32.dp),
+ )
+
+ Text(
+ text = name,
+ modifier = Modifier.weight(1f),
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis
+ )
+
+ Text(
+ text = "$value/$max",
+ style = Typography.bodySmall,
+ maxLines = 1,
+ )
+ }
+}
diff --git a/app/src/main/java/moe/chensi/volume/data/AppPreferencesStore.kt b/app/src/main/java/moe/chensi/volume/data/AppPreferencesStore.kt
index 389cef7..d8fc181 100644
--- a/app/src/main/java/moe/chensi/volume/data/AppPreferencesStore.kt
+++ b/app/src/main/java/moe/chensi/volume/data/AppPreferencesStore.kt
@@ -21,7 +21,9 @@ class AppPreferencesStore(private val dataStore: DataStore) {
@Serializable
private data class SerializedState(
- val values: MutableList, val indices: MutableMap
+ val values: MutableList,
+ val indices: MutableMap,
+ val showSystemSlidersInPopup: Boolean = true
)
private val lock = Any()
@@ -30,6 +32,22 @@ class AppPreferencesStore(private val dataStore: DataStore) {
get() = state.values
val indices: Map
get() = synchronized(lock) { state.indices.toMap() }
+ var showSystemSlidersInPopup: Boolean
+ get() = synchronized(lock) { state.showSystemSlidersInPopup }
+ set(value) {
+ val changed = synchronized(lock) {
+ if (state.showSystemSlidersInPopup == value) {
+ return@synchronized false
+ }
+
+ state = state.copy(showSystemSlidersInPopup = value)
+ true
+ }
+
+ if (changed) {
+ save()
+ }
+ }
fun track(onChange: (first: Boolean) -> Unit) {
var first = true
@@ -71,4 +89,4 @@ class AppPreferencesStore(private val dataStore: DataStore) {
}
}
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml
index 2f4aada..d0ccbd4 100644
--- a/app/src/main/res/values-zh-rCN/strings.xml
+++ b/app/src/main/res/values-zh-rCN/strings.xml
@@ -14,4 +14,16 @@
作者:%s
在 GitHub 上查看
跳转到分组
-
\ No newline at end of file
+ 媒体
+ 铃声
+ 通话
+ 闹钟
+ 通知
+ 亮度
+ 在弹窗中显示系统滑块
+ 在弹窗中隐藏系统滑块
+ 开启振动模式
+ 关闭振动模式
+ 开启勿扰模式
+ 关闭勿扰模式
+
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 68d0462..3b156c8 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -13,4 +13,16 @@
Author: %s
View on GitHub
Jump to group
-
\ No newline at end of file
+ Media
+ Ring
+ Call
+ Alarm
+ Notification
+ Brightness
+ Show system sliders in popup
+ Hide system sliders in popup
+ Enable vibrate mode
+ Disable vibrate mode
+ Enable Do Not Disturb
+ Disable Do Not Disturb
+
From ba0112699661291f362fe2840af99963ec3c5b05 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 12 May 2026 04:24:06 +0000
Subject: [PATCH 03/22] Refine constants in system volume panel
Agent-Logs-Url: https://github.com/yume-chan/VolumeManager/sessions/0dfe35c2-a621-4609-8a1b-327139ed70d2
Co-authored-by: yume-chan <1330321+yume-chan@users.noreply.github.com>
---
.../java/moe/chensi/volume/compose/SystemVolumePanel.kt | 7 +++++--
1 file changed, 5 insertions(+), 2 deletions(-)
diff --git a/app/src/main/java/moe/chensi/volume/compose/SystemVolumePanel.kt b/app/src/main/java/moe/chensi/volume/compose/SystemVolumePanel.kt
index 895f200..7bedae4 100644
--- a/app/src/main/java/moe/chensi/volume/compose/SystemVolumePanel.kt
+++ b/app/src/main/java/moe/chensi/volume/compose/SystemVolumePanel.kt
@@ -50,6 +50,9 @@ import moe.chensi.volume.R
import moe.chensi.volume.ui.theme.Typography
import kotlinx.coroutines.delay
+private const val CALL_MODE_POLL_INTERVAL_MS = 500L
+private const val DEFAULT_BRIGHTNESS = 127
+
private fun isCallMode(mode: Int): Boolean {
return mode == AudioManager.MODE_IN_CALL || mode == AudioManager.MODE_IN_COMMUNICATION
}
@@ -70,7 +73,7 @@ fun SystemVolumePanel(
if (!showCallVolumeAlways) {
while (true) {
inCallMode = isCallMode(audioManager.mode)
- delay(500)
+ delay(CALL_MODE_POLL_INTERVAL_MS)
}
}
}
@@ -224,7 +227,7 @@ private fun BrightnessSlider(onChange: (() -> Unit)? = null) {
return try {
Settings.System.getInt(contentResolver, Settings.System.SCREEN_BRIGHTNESS)
} catch (_: Settings.SettingNotFoundException) {
- 127
+ DEFAULT_BRIGHTNESS
}
}
From 2f877a904e5f72d47beddd22138a8290207d6d5c Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 12 May 2026 04:24:43 +0000
Subject: [PATCH 04/22] Log DND permission failures in system panel
Agent-Logs-Url: https://github.com/yume-chan/VolumeManager/sessions/0dfe35c2-a621-4609-8a1b-327139ed70d2
Co-authored-by: yume-chan <1330321+yume-chan@users.noreply.github.com>
---
.../main/java/moe/chensi/volume/compose/SystemVolumePanel.kt | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/app/src/main/java/moe/chensi/volume/compose/SystemVolumePanel.kt b/app/src/main/java/moe/chensi/volume/compose/SystemVolumePanel.kt
index 7bedae4..d289fb6 100644
--- a/app/src/main/java/moe/chensi/volume/compose/SystemVolumePanel.kt
+++ b/app/src/main/java/moe/chensi/volume/compose/SystemVolumePanel.kt
@@ -10,6 +10,7 @@ import android.media.AudioManager
import android.os.Handler
import android.os.Looper
import android.provider.Settings
+import android.util.Log
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
@@ -52,6 +53,7 @@ import kotlinx.coroutines.delay
private const val CALL_MODE_POLL_INTERVAL_MS = 500L
private const val DEFAULT_BRIGHTNESS = 127
+private const val TAG = "SystemVolumePanel"
private fun isCallMode(mode: Int): Boolean {
return mode == AudioManager.MODE_IN_CALL || mode == AudioManager.MODE_IN_COMMUNICATION
@@ -210,7 +212,8 @@ private fun NotificationModeToggles(audioManager: AudioManager, onChange: (() ->
notificationManager.setInterruptionFilter(
if (it) NotificationManager.INTERRUPTION_FILTER_NONE else NotificationManager.INTERRUPTION_FILTER_ALL
)
- } catch (_: SecurityException) {
+ } catch (e: SecurityException) {
+ Log.w(TAG, "Can't change interruption filter", e)
}
interruptionFilter = notificationManager.currentInterruptionFilter
onChange?.invoke()
From 80217a5f5e84220a2faeaac7b1a8a345e2532fe9 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 12 May 2026 04:25:17 +0000
Subject: [PATCH 05/22] Guard notification manager lookup in toggles
Agent-Logs-Url: https://github.com/yume-chan/VolumeManager/sessions/0dfe35c2-a621-4609-8a1b-327139ed70d2
Co-authored-by: yume-chan <1330321+yume-chan@users.noreply.github.com>
---
.../main/java/moe/chensi/volume/compose/SystemVolumePanel.kt | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/app/src/main/java/moe/chensi/volume/compose/SystemVolumePanel.kt b/app/src/main/java/moe/chensi/volume/compose/SystemVolumePanel.kt
index d289fb6..3d1ca7e 100644
--- a/app/src/main/java/moe/chensi/volume/compose/SystemVolumePanel.kt
+++ b/app/src/main/java/moe/chensi/volume/compose/SystemVolumePanel.kt
@@ -164,7 +164,7 @@ fun SystemVolumePanel(
@Composable
private fun NotificationModeToggles(audioManager: AudioManager, onChange: (() -> Unit)? = null) {
val context = LocalContext.current
- val notificationManager = context.getSystemService(NotificationManager::class.java)!!
+ val notificationManager = context.getSystemService(NotificationManager::class.java) ?: return
var ringerMode by remember { mutableIntStateOf(audioManager.ringerMode) }
var interruptionFilter by remember { mutableIntStateOf(notificationManager.currentInterruptionFilter) }
From 15fa497e78ee613e3f47ad146496a83d8d0ccbc7 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 12 May 2026 04:26:16 +0000
Subject: [PATCH 06/22] Reuse slider value label component
Agent-Logs-Url: https://github.com/yume-chan/VolumeManager/sessions/0dfe35c2-a621-4609-8a1b-327139ed70d2
Co-authored-by: yume-chan <1330321+yume-chan@users.noreply.github.com>
---
.../volume/compose/StreamVolumeSlider.kt | 31 +++++++++++--------
.../volume/compose/SystemVolumePanel.kt | 18 +----------
2 files changed, 19 insertions(+), 30 deletions(-)
diff --git a/app/src/main/java/moe/chensi/volume/compose/StreamVolumeSlider.kt b/app/src/main/java/moe/chensi/volume/compose/StreamVolumeSlider.kt
index c8c899f..80ecc1e 100644
--- a/app/src/main/java/moe/chensi/volume/compose/StreamVolumeSlider.kt
+++ b/app/src/main/java/moe/chensi/volume/compose/StreamVolumeSlider.kt
@@ -11,6 +11,7 @@ import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.weight
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
@@ -126,22 +127,26 @@ fun StreamVolumeSlider(
contentDescription = name,
modifier = Modifier.size(32.dp),
)
-
- Text(
- text = name,
- modifier = Modifier.weight(1f),
- maxLines = 1,
- overflow = TextOverflow.Ellipsis
- )
-
- Text(
- text = "$volume/${maxVolume.toInt()}",
- style = Typography.bodySmall,
- maxLines = 1,
- )
+ StreamSliderValueLabel(name = name, valueText = "$volume/${maxVolume.toInt()}")
}
}
footer?.invoke()
}
}
+
+@Composable
+internal fun StreamSliderValueLabel(name: String, valueText: String) {
+ Text(
+ text = name,
+ modifier = Modifier.weight(1f),
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis
+ )
+
+ Text(
+ text = valueText,
+ style = Typography.bodySmall,
+ maxLines = 1,
+ )
+}
diff --git a/app/src/main/java/moe/chensi/volume/compose/SystemVolumePanel.kt b/app/src/main/java/moe/chensi/volume/compose/SystemVolumePanel.kt
index 3d1ca7e..398db56 100644
--- a/app/src/main/java/moe/chensi/volume/compose/SystemVolumePanel.kt
+++ b/app/src/main/java/moe/chensi/volume/compose/SystemVolumePanel.kt
@@ -17,7 +17,6 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.layout.weight
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Alarm
import androidx.compose.material.icons.filled.Brightness6
@@ -31,7 +30,6 @@ import androidx.compose.material.icons.filled.VisibilityOff
import androidx.compose.material.icons.filled.VolumeUp
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
-import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
@@ -45,10 +43,8 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import moe.chensi.volume.R
-import moe.chensi.volume.ui.theme.Typography
import kotlinx.coroutines.delay
private const val CALL_MODE_POLL_INTERVAL_MS = 500L
@@ -289,18 +285,6 @@ private fun StreamSliderLabelRow(icon: ImageVector, name: String, value: Int, ma
contentDescription = name,
modifier = Modifier.size(32.dp),
)
-
- Text(
- text = name,
- modifier = Modifier.weight(1f),
- maxLines = 1,
- overflow = TextOverflow.Ellipsis
- )
-
- Text(
- text = "$value/$max",
- style = Typography.bodySmall,
- maxLines = 1,
- )
+ StreamSliderValueLabel(name = name, valueText = "$value/$max")
}
}
From 6e1fb02ad9a196883fb7d92ac6a73c812af918dd Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 12 May 2026 04:26:50 +0000
Subject: [PATCH 07/22] Reuse main-thread handler for brightness observer
Agent-Logs-Url: https://github.com/yume-chan/VolumeManager/sessions/0dfe35c2-a621-4609-8a1b-327139ed70d2
Co-authored-by: yume-chan <1330321+yume-chan@users.noreply.github.com>
---
.../main/java/moe/chensi/volume/compose/SystemVolumePanel.kt | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/app/src/main/java/moe/chensi/volume/compose/SystemVolumePanel.kt b/app/src/main/java/moe/chensi/volume/compose/SystemVolumePanel.kt
index 398db56..3f516da 100644
--- a/app/src/main/java/moe/chensi/volume/compose/SystemVolumePanel.kt
+++ b/app/src/main/java/moe/chensi/volume/compose/SystemVolumePanel.kt
@@ -232,9 +232,10 @@ private fun BrightnessSlider(onChange: (() -> Unit)? = null) {
var brightness by remember { mutableIntStateOf(getBrightness()) }
val canWrite = Settings.System.canWrite(context)
+ val mainThreadHandler = remember { Handler(Looper.getMainLooper()) }
DisposableEffect(contentResolver) {
- val observer = object : ContentObserver(Handler(Looper.getMainLooper())) {
+ val observer = object : ContentObserver(mainThreadHandler) {
override fun onChange(selfChange: Boolean) {
brightness = getBrightness()
}
From 967fd6dcc610a6d4ed0b84b8eadc2817b68a7dbe Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 12 May 2026 04:27:27 +0000
Subject: [PATCH 08/22] Rename shared slider text helper
Agent-Logs-Url: https://github.com/yume-chan/VolumeManager/sessions/0dfe35c2-a621-4609-8a1b-327139ed70d2
Co-authored-by: yume-chan <1330321+yume-chan@users.noreply.github.com>
---
.../main/java/moe/chensi/volume/compose/StreamVolumeSlider.kt | 4 ++--
.../main/java/moe/chensi/volume/compose/SystemVolumePanel.kt | 2 +-
2 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/app/src/main/java/moe/chensi/volume/compose/StreamVolumeSlider.kt b/app/src/main/java/moe/chensi/volume/compose/StreamVolumeSlider.kt
index 80ecc1e..8493139 100644
--- a/app/src/main/java/moe/chensi/volume/compose/StreamVolumeSlider.kt
+++ b/app/src/main/java/moe/chensi/volume/compose/StreamVolumeSlider.kt
@@ -127,7 +127,7 @@ fun StreamVolumeSlider(
contentDescription = name,
modifier = Modifier.size(32.dp),
)
- StreamSliderValueLabel(name = name, valueText = "$volume/${maxVolume.toInt()}")
+ StreamSliderTextContent(name = name, valueText = "$volume/${maxVolume.toInt()}")
}
}
@@ -136,7 +136,7 @@ fun StreamVolumeSlider(
}
@Composable
-internal fun StreamSliderValueLabel(name: String, valueText: String) {
+internal fun StreamSliderTextContent(name: String, valueText: String) {
Text(
text = name,
modifier = Modifier.weight(1f),
diff --git a/app/src/main/java/moe/chensi/volume/compose/SystemVolumePanel.kt b/app/src/main/java/moe/chensi/volume/compose/SystemVolumePanel.kt
index 3f516da..9b7d1ad 100644
--- a/app/src/main/java/moe/chensi/volume/compose/SystemVolumePanel.kt
+++ b/app/src/main/java/moe/chensi/volume/compose/SystemVolumePanel.kt
@@ -286,6 +286,6 @@ private fun StreamSliderLabelRow(icon: ImageVector, name: String, value: Int, ma
contentDescription = name,
modifier = Modifier.size(32.dp),
)
- StreamSliderValueLabel(name = name, valueText = "$value/$max")
+ StreamSliderTextContent(name = name, valueText = "$value/$max")
}
}
From 685617635711bcac2ae449da6aeaf3820008cc55 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 12 May 2026 04:28:04 +0000
Subject: [PATCH 09/22] Document call modes and log brightness read failure
Agent-Logs-Url: https://github.com/yume-chan/VolumeManager/sessions/0dfe35c2-a621-4609-8a1b-327139ed70d2
Co-authored-by: yume-chan <1330321+yume-chan@users.noreply.github.com>
---
.../java/moe/chensi/volume/compose/SystemVolumePanel.kt | 6 +++++-
1 file changed, 5 insertions(+), 1 deletion(-)
diff --git a/app/src/main/java/moe/chensi/volume/compose/SystemVolumePanel.kt b/app/src/main/java/moe/chensi/volume/compose/SystemVolumePanel.kt
index 9b7d1ad..7f079c5 100644
--- a/app/src/main/java/moe/chensi/volume/compose/SystemVolumePanel.kt
+++ b/app/src/main/java/moe/chensi/volume/compose/SystemVolumePanel.kt
@@ -51,6 +51,9 @@ private const val CALL_MODE_POLL_INTERVAL_MS = 500L
private const val DEFAULT_BRIGHTNESS = 127
private const val TAG = "SystemVolumePanel"
+/**
+ * MODE_IN_CALL is for cellular calls, MODE_IN_COMMUNICATION is for VoIP/communication apps.
+ */
private fun isCallMode(mode: Int): Boolean {
return mode == AudioManager.MODE_IN_CALL || mode == AudioManager.MODE_IN_COMMUNICATION
}
@@ -225,7 +228,8 @@ private fun BrightnessSlider(onChange: (() -> Unit)? = null) {
fun getBrightness(): Int {
return try {
Settings.System.getInt(contentResolver, Settings.System.SCREEN_BRIGHTNESS)
- } catch (_: Settings.SettingNotFoundException) {
+ } catch (e: Settings.SettingNotFoundException) {
+ Log.w(TAG, "Can't read system brightness", e)
DEFAULT_BRIGHTNESS
}
}
From e049c4a85308d20bfdfaae40c0122e0548b872c1 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 12 May 2026 05:51:20 +0000
Subject: [PATCH 10/22] Address PR review feedback for system sliders panel
Agent-Logs-Url: https://github.com/yume-chan/VolumeManager/sessions/ea154d5a-d158-408f-ade6-cc145e8a4b7e
Co-authored-by: yume-chan <1330321+yume-chan@users.noreply.github.com>
---
app/src/main/AndroidManifest.xml | 2 -
.../java/moe/chensi/volume/MainActivity.kt | 9 +-
.../main/java/moe/chensi/volume/Manager.kt | 30 +-
.../main/java/moe/chensi/volume/Service.kt | 11 +-
.../chensi/volume/compose/AppVolumeList.kt | 5 +-
.../volume/compose/StreamVolumeSlider.kt | 12 +-
.../volume/compose/SystemVolumePanel.kt | 371 +++++++++++-------
.../chensi/volume/data/AppPreferencesStore.kt | 32 +-
.../volume/system/DisplayManagerProxy.kt | 46 +++
.../volume/system/NotificationManagerProxy.kt | 35 ++
app/src/main/res/values-zh-rCN/strings.xml | 4 +-
app/src/main/res/values/strings.xml | 4 +-
12 files changed, 397 insertions(+), 164 deletions(-)
create mode 100644 app/src/main/java/moe/chensi/volume/system/DisplayManagerProxy.kt
create mode 100644 app/src/main/java/moe/chensi/volume/system/NotificationManagerProxy.kt
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 4d6c076..00afe26 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -5,8 +5,6 @@
-
-
diff --git a/app/src/main/java/moe/chensi/volume/MainActivity.kt b/app/src/main/java/moe/chensi/volume/MainActivity.kt
index 41a9ae3..e049be1 100644
--- a/app/src/main/java/moe/chensi/volume/MainActivity.kt
+++ b/app/src/main/java/moe/chensi/volume/MainActivity.kt
@@ -332,10 +332,13 @@ class MainActivity : ComponentActivity() {
item("system_volume_panel_main") {
SystemVolumePanel(
audioManager = manager.audioManager,
+ notificationManagerProxy = manager.notificationManagerProxy,
+ displayManagerProxy = manager.displayManagerProxy,
showCallVolumeAlways = true,
- showHideButton = false,
- showSliders = true,
- onShowSlidersChange = { },
+ applyVisibilityFilter = false,
+ allowVisibilityConfig = true,
+ isSliderVisible = manager::isSystemSliderVisible,
+ onSliderVisibilityChange = manager::setSystemSliderVisible,
)
}
})
diff --git a/app/src/main/java/moe/chensi/volume/Manager.kt b/app/src/main/java/moe/chensi/volume/Manager.kt
index ff0206f..76d6e7f 100644
--- a/app/src/main/java/moe/chensi/volume/Manager.kt
+++ b/app/src/main/java/moe/chensi/volume/Manager.kt
@@ -15,6 +15,8 @@ import androidx.datastore.preferences.core.Preferences
import moe.chensi.volume.data.App
import moe.chensi.volume.data.AppPreferencesStore
import moe.chensi.volume.system.AudioPlaybackConfigurationProxy
+import moe.chensi.volume.system.DisplayManagerProxy
+import moe.chensi.volume.system.NotificationManagerProxy
import moe.chensi.volume.system.PackageManagerProxy
import org.joor.Reflect
import rikka.shizuku.Shizuku
@@ -44,20 +46,27 @@ class Manager(context: Context, dataStore: DataStore) {
.apply { ToggleableBinderProxy.wrap(this) }
}
private val packageManager by lazy { PackageManagerProxy.get(context) }
+ val notificationManagerProxy = NotificationManagerProxy(context)
+ val displayManagerProxy = DisplayManagerProxy(context)
private val appPreferencesStore = AppPreferencesStore(dataStore)
- private var _showSystemSlidersInPopup by mutableStateOf(appPreferencesStore.showSystemSlidersInPopup)
- var showSystemSlidersInPopup: Boolean
- get() = _showSystemSlidersInPopup
- set(value) {
- if (_showSystemSlidersInPopup == value) {
- return
- }
+ private val _systemSliderVisibility = mutableStateMapOf()
+ val systemSliderVisibility: Map
+ get() = _systemSliderVisibility
+
+ fun isSystemSliderVisible(id: String): Boolean {
+ return _systemSliderVisibility[id] ?: true
+ }
- _showSystemSlidersInPopup = value
- appPreferencesStore.showSystemSlidersInPopup = value
+ fun setSystemSliderVisible(id: String, visible: Boolean) {
+ if ((_systemSliderVisibility[id] ?: true) == visible) {
+ return
}
+ _systemSliderVisibility[id] = visible
+ appPreferencesStore.setSystemSliderVisible(id, visible)
+ }
+
val apps = mutableStateMapOf()
private fun reloadApps() {
@@ -173,7 +182,8 @@ class Manager(context: Context, dataStore: DataStore) {
}
}
- _showSystemSlidersInPopup = appPreferencesStore.showSystemSlidersInPopup
+ _systemSliderVisibility.clear()
+ _systemSliderVisibility.putAll(appPreferencesStore.systemSliderVisibility)
if (first) {
initialize()
diff --git a/app/src/main/java/moe/chensi/volume/Service.kt b/app/src/main/java/moe/chensi/volume/Service.kt
index 8ebb214..9d58e37 100644
--- a/app/src/main/java/moe/chensi/volume/Service.kt
+++ b/app/src/main/java/moe/chensi/volume/Service.kt
@@ -202,11 +202,14 @@ class Service : AccessibilityService() {
) {
item("system_volume_panel") {
SystemVolumePanel(
- manager.audioManager,
+ audioManager = manager.audioManager,
+ notificationManagerProxy = manager.notificationManagerProxy,
+ displayManagerProxy = manager.displayManagerProxy,
showCallVolumeAlways = false,
- showHideButton = true,
- showSliders = manager.showSystemSlidersInPopup,
- onShowSlidersChange = { manager.showSystemSlidersInPopup = it },
+ applyVisibilityFilter = true,
+ allowVisibilityConfig = true,
+ isSliderVisible = manager::isSystemSliderVisible,
+ onSliderVisibilityChange = manager::setSystemSliderVisible,
onChange = this@Service.handler::startIdleTimer
)
}
diff --git a/app/src/main/java/moe/chensi/volume/compose/AppVolumeList.kt b/app/src/main/java/moe/chensi/volume/compose/AppVolumeList.kt
index 4a7c4d1..260cc2c 100644
--- a/app/src/main/java/moe/chensi/volume/compose/AppVolumeList.kt
+++ b/app/src/main/java/moe/chensi/volume/compose/AppVolumeList.kt
@@ -5,7 +5,6 @@ import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
-import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
@@ -154,7 +153,9 @@ fun AppVolumeList(
} else if (showEmpty) {
item {
Column(
- modifier = Modifier.fillParentMaxSize(),
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(vertical = 48.dp),
verticalArrangement = Arrangement.spacedBy(
12.dp, Alignment.CenterVertically
),
diff --git a/app/src/main/java/moe/chensi/volume/compose/StreamVolumeSlider.kt b/app/src/main/java/moe/chensi/volume/compose/StreamVolumeSlider.kt
index 8493139..0e36e9b 100644
--- a/app/src/main/java/moe/chensi/volume/compose/StreamVolumeSlider.kt
+++ b/app/src/main/java/moe/chensi/volume/compose/StreamVolumeSlider.kt
@@ -9,6 +9,7 @@ import android.media.AudioManager
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.weight
@@ -112,8 +113,13 @@ fun StreamVolumeSlider(
value = volume.toFloat(),
valueRange = 0f..maxVolume,
onValueChange = { value ->
- volume = value.toInt()
- audioManager.setStreamVolume(streamType, value.toInt(), 0)
+ val target = value.toInt()
+ if (volume == target) {
+ return@TrackSlider
+ }
+
+ volume = target
+ audioManager.setStreamVolume(streamType, target, 0)
onChange?.invoke()
},
) {
@@ -136,7 +142,7 @@ fun StreamVolumeSlider(
}
@Composable
-internal fun StreamSliderTextContent(name: String, valueText: String) {
+internal fun RowScope.StreamSliderTextContent(name: String, valueText: String) {
Text(
text = name,
modifier = Modifier.weight(1f),
diff --git a/app/src/main/java/moe/chensi/volume/compose/SystemVolumePanel.kt b/app/src/main/java/moe/chensi/volume/compose/SystemVolumePanel.kt
index 7f079c5..7a0bd87 100644
--- a/app/src/main/java/moe/chensi/volume/compose/SystemVolumePanel.kt
+++ b/app/src/main/java/moe/chensi/volume/compose/SystemVolumePanel.kt
@@ -10,12 +10,9 @@ import android.media.AudioManager
import android.os.Handler
import android.os.Looper
import android.provider.Settings
-import android.util.Log
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Alarm
@@ -24,6 +21,7 @@ import androidx.compose.material.icons.filled.DoNotDisturbOn
import androidx.compose.material.icons.filled.NotificationsActive
import androidx.compose.material.icons.filled.NotificationsNone
import androidx.compose.material.icons.filled.PhoneInTalk
+import androidx.compose.material.icons.filled.RingVolume
import androidx.compose.material.icons.filled.Vibration
import androidx.compose.material.icons.filled.Visibility
import androidx.compose.material.icons.filled.VisibilityOff
@@ -32,28 +30,32 @@ import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
-import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
-import androidx.compose.ui.Modifier
import androidx.compose.ui.Alignment
-import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
+import androidx.core.content.ContextCompat
import moe.chensi.volume.R
-import kotlinx.coroutines.delay
+import moe.chensi.volume.system.DisplayManagerProxy
+import moe.chensi.volume.system.NotificationManagerProxy
+import kotlin.math.roundToInt
-private const val CALL_MODE_POLL_INTERVAL_MS = 500L
-private const val DEFAULT_BRIGHTNESS = 127
-private const val TAG = "SystemVolumePanel"
+object SystemSliderIds {
+ const val Media = "media"
+ const val Ring = "ring"
+ const val Call = "call"
+ const val Alarm = "alarm"
+ const val Notification = "notification"
+ const val Brightness = "brightness"
+}
-/**
- * MODE_IN_CALL is for cellular calls, MODE_IN_COMMUNICATION is for VoIP/communication apps.
- */
private fun isCallMode(mode: Int): Boolean {
return mode == AudioManager.MODE_IN_CALL || mode == AudioManager.MODE_IN_COMMUNICATION
}
@@ -62,117 +64,163 @@ private fun isCallMode(mode: Int): Boolean {
@Composable
fun SystemVolumePanel(
audioManager: AudioManager,
+ notificationManagerProxy: NotificationManagerProxy,
+ displayManagerProxy: DisplayManagerProxy,
showCallVolumeAlways: Boolean,
- showHideButton: Boolean,
- showSliders: Boolean,
- onShowSlidersChange: (Boolean) -> Unit,
+ applyVisibilityFilter: Boolean,
+ allowVisibilityConfig: Boolean,
+ isSliderVisible: (String) -> Boolean,
+ onSliderVisibilityChange: (String, Boolean) -> Unit,
onChange: (() -> Unit)? = null
) {
+ val context = LocalContext.current
+ val executor = remember(context) { ContextCompat.getMainExecutor(context) }
var inCallMode by remember { mutableStateOf(isCallMode(audioManager.mode)) }
- LaunchedEffect(showCallVolumeAlways) {
- if (!showCallVolumeAlways) {
- while (true) {
- inCallMode = isCallMode(audioManager.mode)
- delay(CALL_MODE_POLL_INTERVAL_MS)
+ DisposableEffect(audioManager, showCallVolumeAlways) {
+ if (showCallVolumeAlways) {
+ onDispose { }
+ } else {
+ val listener = AudioManager.OnModeChangedListener { mode ->
+ inCallMode = isCallMode(mode)
+ }
+ audioManager.addOnModeChangedListener(executor, listener)
+ onDispose {
+ audioManager.removeOnModeChangedListener(listener)
}
}
}
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
- if (showHideButton) {
- Row(
- horizontalArrangement = Arrangement.End,
- modifier = Modifier
- .fillMaxWidth()
- .padding(horizontal = 4.dp)
- ) {
- ToggleButton(
- checked = showSliders,
- checkedDescription = stringResource(R.string.hide_system_sliders_in_popup),
- checkedIcon = Icons.Default.Visibility,
- uncheckedDescription = stringResource(R.string.show_system_sliders_in_popup),
- uncheckedIcon = Icons.Default.VisibilityOff,
- onCheckedChange = onShowSlidersChange
+ if (!applyVisibilityFilter || isSliderVisible(SystemSliderIds.Call)) {
+ if (!showCallVolumeAlways && inCallMode || showCallVolumeAlways) {
+ StreamVolumeSlider(
+ streamType = AudioManager.STREAM_VOICE_CALL,
+ icon = Icons.Default.PhoneInTalk,
+ name = stringResource(R.string.stream_call),
+ audioManager = audioManager,
+ footer = {
+ SliderVisibilityFooter(
+ sliderId = SystemSliderIds.Call,
+ sliderName = stringResource(R.string.stream_call),
+ allowVisibilityConfig = allowVisibilityConfig,
+ isVisible = isSliderVisible(SystemSliderIds.Call),
+ onSliderVisibilityChange = onSliderVisibilityChange
+ )
+ },
+ onChange = onChange
)
}
}
- if (!showSliders) {
- return@Column
+ if (!applyVisibilityFilter || isSliderVisible(SystemSliderIds.Media)) {
+ StreamVolumeSlider(
+ streamType = AudioManager.STREAM_MUSIC,
+ icon = Icons.Default.VolumeUp,
+ name = stringResource(R.string.stream_media),
+ audioManager = audioManager,
+ footer = {
+ SliderVisibilityFooter(
+ sliderId = SystemSliderIds.Media,
+ sliderName = stringResource(R.string.stream_media),
+ allowVisibilityConfig = allowVisibilityConfig,
+ isVisible = isSliderVisible(SystemSliderIds.Media),
+ onSliderVisibilityChange = onSliderVisibilityChange
+ )
+ },
+ onChange = onChange
+ )
}
- if (!showCallVolumeAlways && inCallMode) {
+ if (!applyVisibilityFilter || isSliderVisible(SystemSliderIds.Ring)) {
StreamVolumeSlider(
- streamType = AudioManager.STREAM_VOICE_CALL,
- icon = Icons.Default.PhoneInTalk,
- name = stringResource(R.string.stream_call),
+ streamType = AudioManager.STREAM_RING,
+ icon = Icons.Default.RingVolume,
+ name = stringResource(R.string.stream_ring),
audioManager = audioManager,
+ footer = {
+ RingFooter(
+ audioManager = audioManager,
+ notificationManagerProxy = notificationManagerProxy,
+ sliderVisible = isSliderVisible(SystemSliderIds.Ring),
+ allowVisibilityConfig = allowVisibilityConfig,
+ onSliderVisibilityChange = onSliderVisibilityChange,
+ onChange = onChange
+ )
+ },
onChange = onChange
)
}
- StreamVolumeSlider(
- streamType = AudioManager.STREAM_MUSIC,
- icon = Icons.Default.VolumeUp,
- name = stringResource(R.string.stream_media),
- audioManager = audioManager,
- onChange = onChange
- )
-
- StreamVolumeSlider(
- streamType = AudioManager.STREAM_RING,
- icon = Icons.Default.NotificationsActive,
- name = stringResource(R.string.stream_ring),
- audioManager = audioManager,
- onChange = onChange
- )
-
- if (showCallVolumeAlways) {
+ if (!applyVisibilityFilter || isSliderVisible(SystemSliderIds.Alarm)) {
StreamVolumeSlider(
- streamType = AudioManager.STREAM_VOICE_CALL,
- icon = Icons.Default.PhoneInTalk,
- name = stringResource(R.string.stream_call),
+ streamType = AudioManager.STREAM_ALARM,
+ icon = Icons.Default.Alarm,
+ name = stringResource(R.string.stream_alarm),
audioManager = audioManager,
+ footer = {
+ SliderVisibilityFooter(
+ sliderId = SystemSliderIds.Alarm,
+ sliderName = stringResource(R.string.stream_alarm),
+ allowVisibilityConfig = allowVisibilityConfig,
+ isVisible = isSliderVisible(SystemSliderIds.Alarm),
+ onSliderVisibilityChange = onSliderVisibilityChange
+ )
+ },
onChange = onChange
)
}
- StreamVolumeSlider(
- streamType = AudioManager.STREAM_ALARM,
- icon = Icons.Default.Alarm,
- name = stringResource(R.string.stream_alarm),
- audioManager = audioManager,
- onChange = onChange
- )
-
- StreamVolumeSlider(
- streamType = AudioManager.STREAM_NOTIFICATION,
- icon = Icons.Default.NotificationsNone,
- name = stringResource(R.string.stream_notification),
- audioManager = audioManager,
- footer = { NotificationModeToggles(audioManager = audioManager, onChange = onChange) },
- onChange = onChange
- )
+ if (!applyVisibilityFilter || isSliderVisible(SystemSliderIds.Notification)) {
+ StreamVolumeSlider(
+ streamType = AudioManager.STREAM_NOTIFICATION,
+ icon = Icons.Default.NotificationsNone,
+ name = stringResource(R.string.stream_notification),
+ audioManager = audioManager,
+ footer = {
+ SliderVisibilityFooter(
+ sliderId = SystemSliderIds.Notification,
+ sliderName = stringResource(R.string.stream_notification),
+ allowVisibilityConfig = allowVisibilityConfig,
+ isVisible = isSliderVisible(SystemSliderIds.Notification),
+ onSliderVisibilityChange = onSliderVisibilityChange
+ )
+ },
+ onChange = onChange
+ )
+ }
- BrightnessSlider(onChange = onChange)
+ if (!applyVisibilityFilter || isSliderVisible(SystemSliderIds.Brightness)) {
+ BrightnessSlider(
+ displayManagerProxy = displayManagerProxy,
+ sliderVisible = isSliderVisible(SystemSliderIds.Brightness),
+ allowVisibilityConfig = allowVisibilityConfig,
+ onSliderVisibilityChange = onSliderVisibilityChange,
+ onChange = onChange
+ )
+ }
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
-private fun NotificationModeToggles(audioManager: AudioManager, onChange: (() -> Unit)? = null) {
+private fun RingFooter(
+ audioManager: AudioManager,
+ notificationManagerProxy: NotificationManagerProxy,
+ sliderVisible: Boolean,
+ allowVisibilityConfig: Boolean,
+ onSliderVisibilityChange: (String, Boolean) -> Unit,
+ onChange: (() -> Unit)? = null
+) {
val context = LocalContext.current
- val notificationManager = context.getSystemService(NotificationManager::class.java) ?: return
-
var ringerMode by remember { mutableIntStateOf(audioManager.ringerMode) }
- var interruptionFilter by remember { mutableIntStateOf(notificationManager.currentInterruptionFilter) }
+ var interruptionFilter by remember { mutableIntStateOf(notificationManagerProxy.getCurrentInterruptionFilter()) }
DisposableEffect(context) {
val receiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
ringerMode = audioManager.ringerMode
- interruptionFilter = notificationManager.currentInterruptionFilter
+ interruptionFilter = notificationManagerProxy.getCurrentInterruptionFilter()
}
}
@@ -186,7 +234,10 @@ private fun NotificationModeToggles(audioManager: AudioManager, onChange: (() ->
}
}
- Row(horizontalArrangement = Arrangement.spacedBy(4.dp), modifier = Modifier.padding(start = 4.dp)) {
+ Row(
+ horizontalArrangement = Arrangement.spacedBy(4.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
ToggleButton(
checked = ringerMode == AudioManager.RINGER_MODE_VIBRATE,
checkedDescription = stringResource(R.string.disable_vibrate_mode),
@@ -207,41 +258,99 @@ private fun NotificationModeToggles(audioManager: AudioManager, onChange: (() ->
uncheckedDescription = stringResource(R.string.enable_do_not_disturb),
uncheckedIcon = Icons.Default.NotificationsActive
) {
- try {
- notificationManager.setInterruptionFilter(
- if (it) NotificationManager.INTERRUPTION_FILTER_NONE else NotificationManager.INTERRUPTION_FILTER_ALL
- )
- } catch (e: SecurityException) {
- Log.w(TAG, "Can't change interruption filter", e)
- }
- interruptionFilter = notificationManager.currentInterruptionFilter
+ notificationManagerProxy.setInterruptionFilter(
+ if (it) NotificationManager.INTERRUPTION_FILTER_NONE else NotificationManager.INTERRUPTION_FILTER_ALL
+ )
+ interruptionFilter = notificationManagerProxy.getCurrentInterruptionFilter()
onChange?.invoke()
}
+
+ SliderVisibilityToggle(
+ sliderId = SystemSliderIds.Ring,
+ sliderName = stringResource(R.string.stream_ring),
+ allowVisibilityConfig = allowVisibilityConfig,
+ isVisible = sliderVisible,
+ onSliderVisibilityChange = onSliderVisibilityChange
+ )
+ }
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+private fun SliderVisibilityFooter(
+ sliderId: String,
+ sliderName: String,
+ allowVisibilityConfig: Boolean,
+ isVisible: Boolean,
+ onSliderVisibilityChange: (String, Boolean) -> Unit
+) {
+ if (!allowVisibilityConfig) {
+ return
+ }
+
+ Row(horizontalArrangement = Arrangement.End, modifier = Modifier) {
+ SliderVisibilityToggle(
+ sliderId = sliderId,
+ sliderName = sliderName,
+ allowVisibilityConfig = allowVisibilityConfig,
+ isVisible = isVisible,
+ onSliderVisibilityChange = onSliderVisibilityChange
+ )
+ }
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+private fun SliderVisibilityToggle(
+ sliderId: String,
+ sliderName: String,
+ allowVisibilityConfig: Boolean,
+ isVisible: Boolean,
+ onSliderVisibilityChange: (String, Boolean) -> Unit
+) {
+ if (!allowVisibilityConfig) {
+ return
+ }
+
+ ToggleButton(
+ checked = isVisible,
+ checkedDescription = stringResource(R.string.hide_slider, sliderName),
+ checkedIcon = Icons.Default.Visibility,
+ uncheckedDescription = stringResource(R.string.show_slider, sliderName),
+ uncheckedIcon = Icons.Default.VisibilityOff
+ ) {
+ onSliderVisibilityChange(sliderId, it)
}
}
@Composable
-private fun BrightnessSlider(onChange: (() -> Unit)? = null) {
+private fun BrightnessSlider(
+ displayManagerProxy: DisplayManagerProxy,
+ sliderVisible: Boolean,
+ allowVisibilityConfig: Boolean,
+ onSliderVisibilityChange: (String, Boolean) -> Unit,
+ onChange: (() -> Unit)? = null
+) {
val context = LocalContext.current
val contentResolver = context.contentResolver
- fun getBrightness(): Int {
- return try {
- Settings.System.getInt(contentResolver, Settings.System.SCREEN_BRIGHTNESS)
- } catch (e: Settings.SettingNotFoundException) {
- Log.w(TAG, "Can't read system brightness", e)
- DEFAULT_BRIGHTNESS
- }
+ fun readBrightness(): Float {
+ return displayManagerProxy.getDefaultDisplayBrightness()
}
- var brightness by remember { mutableIntStateOf(getBrightness()) }
- val canWrite = Settings.System.canWrite(context)
+ fun readMaxBrightness(): Float {
+ return displayManagerProxy.getDefaultDisplayMaxBrightness().coerceAtLeast(1f)
+ }
+
+ var maxBrightness by remember { mutableFloatStateOf(readMaxBrightness()) }
+ var brightness by remember { mutableFloatStateOf(readBrightness()) }
val mainThreadHandler = remember { Handler(Looper.getMainLooper()) }
DisposableEffect(contentResolver) {
val observer = object : ContentObserver(mainThreadHandler) {
override fun onChange(selfChange: Boolean) {
- brightness = getBrightness()
+ maxBrightness = readMaxBrightness()
+ brightness = readBrightness()
}
}
@@ -258,38 +367,38 @@ private fun BrightnessSlider(onChange: (() -> Unit)? = null) {
TrackSlider(
cornerRadius = 20.dp,
- value = brightness.toFloat(),
- valueRange = 0f..255f,
- enabled = canWrite,
+ value = brightness,
+ valueRange = 0f..maxBrightness,
onValueChange = { value ->
- brightness = value.toInt()
- if (canWrite) {
- Settings.System.putInt(contentResolver, Settings.System.SCREEN_BRIGHTNESS, brightness)
- onChange?.invoke()
+ if (brightness == value) {
+ return@TrackSlider
}
+ brightness = value
+ displayManagerProxy.setDefaultDisplayBrightness(value)
+ onChange?.invoke()
}
) {
- StreamSliderLabelRow(
- icon = Icons.Default.Brightness6,
- name = stringResource(R.string.brightness),
- value = brightness,
- max = 255
- )
+ Row(
+ horizontalArrangement = Arrangement.spacedBy(8.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ Icon(
+ imageVector = Icons.Default.Brightness6,
+ contentDescription = stringResource(R.string.brightness),
+ modifier = Modifier.size(32.dp),
+ )
+ StreamSliderTextContent(
+ name = stringResource(R.string.brightness),
+ valueText = "${brightness.roundToInt()}/${maxBrightness.roundToInt()}"
+ )
+ }
}
-}
-@Composable
-private fun StreamSliderLabelRow(icon: ImageVector, name: String, value: Int, max: Int) {
- Row(
- horizontalArrangement = Arrangement.spacedBy(8.dp),
- verticalAlignment = Alignment.CenterVertically,
- modifier = Modifier.padding(12.dp, 8.dp)
- ) {
- Icon(
- imageVector = icon,
- contentDescription = name,
- modifier = Modifier.size(32.dp),
- )
- StreamSliderTextContent(name = name, valueText = "$value/$max")
- }
+ SliderVisibilityFooter(
+ sliderId = SystemSliderIds.Brightness,
+ sliderName = stringResource(R.string.brightness),
+ allowVisibilityConfig = allowVisibilityConfig,
+ isVisible = sliderVisible,
+ onSliderVisibilityChange = onSliderVisibilityChange
+ )
}
diff --git a/app/src/main/java/moe/chensi/volume/data/AppPreferencesStore.kt b/app/src/main/java/moe/chensi/volume/data/AppPreferencesStore.kt
index d8fc181..b5816aa 100644
--- a/app/src/main/java/moe/chensi/volume/data/AppPreferencesStore.kt
+++ b/app/src/main/java/moe/chensi/volume/data/AppPreferencesStore.kt
@@ -23,7 +23,7 @@ class AppPreferencesStore(private val dataStore: DataStore) {
private data class SerializedState(
val values: MutableList,
val indices: MutableMap,
- val showSystemSlidersInPopup: Boolean = true
+ val systemSliderVisibility: MutableMap = mutableMapOf()
)
private val lock = Any()
@@ -32,15 +32,37 @@ class AppPreferencesStore(private val dataStore: DataStore) {
get() = state.values
val indices: Map
get() = synchronized(lock) { state.indices.toMap() }
- var showSystemSlidersInPopup: Boolean
- get() = synchronized(lock) { state.showSystemSlidersInPopup }
+ fun getSystemSliderVisible(id: String): Boolean {
+ return synchronized(lock) { state.systemSliderVisibility[id] ?: true }
+ }
+
+ fun setSystemSliderVisible(id: String, value: Boolean) {
+ val changed = synchronized(lock) {
+ val oldValue = state.systemSliderVisibility[id] ?: true
+ if (oldValue == value) {
+ return@synchronized false
+ }
+
+ val updated = state.systemSliderVisibility.toMutableMap()
+ updated[id] = value
+ state = state.copy(systemSliderVisibility = updated)
+ true
+ }
+
+ if (changed) {
+ save()
+ }
+ }
+
+ var systemSliderVisibility: Map
+ get() = synchronized(lock) { state.systemSliderVisibility.toMap() }
set(value) {
val changed = synchronized(lock) {
- if (state.showSystemSlidersInPopup == value) {
+ if (state.systemSliderVisibility == value) {
return@synchronized false
}
- state = state.copy(showSystemSlidersInPopup = value)
+ state = state.copy(systemSliderVisibility = value.toMutableMap())
true
}
diff --git a/app/src/main/java/moe/chensi/volume/system/DisplayManagerProxy.kt b/app/src/main/java/moe/chensi/volume/system/DisplayManagerProxy.kt
new file mode 100644
index 0000000..36530b4
--- /dev/null
+++ b/app/src/main/java/moe/chensi/volume/system/DisplayManagerProxy.kt
@@ -0,0 +1,46 @@
+package moe.chensi.volume.system
+
+import android.content.Context
+import android.hardware.display.DisplayManager
+import android.view.Display
+import moe.chensi.volume.EnableBinderProxy
+import moe.chensi.volume.ToggleableBinderProxy
+import org.joor.Reflect
+import java.util.WeakHashMap
+
+class DisplayManagerProxy private constructor(context: Context) {
+ companion object {
+ private val cache = WeakHashMap()
+
+ operator fun invoke(context: Context): DisplayManagerProxy {
+ return cache.getOrPut(context) { DisplayManagerProxy(context) }
+ }
+ }
+
+ private val displayManager = context.getSystemService(DisplayManager::class.java)!!
+ private val displayManagerReflect = Reflect.on(displayManager)
+
+ init {
+ val service = Reflect.onClass("android.hardware.display.DisplayManagerGlobal")
+ .call("getInstance")
+ .get()
+ .run(Reflect::on)
+ .get("mDm")
+ ToggleableBinderProxy.wrap(service)
+ }
+
+ @EnableBinderProxy
+ fun getDefaultDisplayBrightness(): Float {
+ return displayManagerReflect.call("getBrightness", Display.DEFAULT_DISPLAY).get()
+ }
+
+ @EnableBinderProxy
+ fun getDefaultDisplayMaxBrightness(): Float {
+ return displayManager.getBrightnessInfo(Display.DEFAULT_DISPLAY)?.brightnessMaximum ?: 1f
+ }
+
+ @EnableBinderProxy
+ fun setDefaultDisplayBrightness(value: Float) {
+ displayManagerReflect.call("setBrightness", Display.DEFAULT_DISPLAY, value)
+ }
+}
diff --git a/app/src/main/java/moe/chensi/volume/system/NotificationManagerProxy.kt b/app/src/main/java/moe/chensi/volume/system/NotificationManagerProxy.kt
new file mode 100644
index 0000000..9c06e17
--- /dev/null
+++ b/app/src/main/java/moe/chensi/volume/system/NotificationManagerProxy.kt
@@ -0,0 +1,35 @@
+package moe.chensi.volume.system
+
+import android.app.NotificationManager
+import android.content.Context
+import moe.chensi.volume.EnableBinderProxy
+import moe.chensi.volume.ToggleableBinderProxy
+import org.joor.Reflect
+import java.util.WeakHashMap
+
+class NotificationManagerProxy private constructor(context: Context) {
+ companion object {
+ private val cache = WeakHashMap()
+
+ operator fun invoke(context: Context): NotificationManagerProxy {
+ return cache.getOrPut(context) { NotificationManagerProxy(context) }
+ }
+ }
+
+ private val notificationManager = context.getSystemService(NotificationManager::class.java)!!
+
+ init {
+ val service = Reflect.onClass(NotificationManager::class.java).call("getService").get()
+ ToggleableBinderProxy.wrap(service)
+ }
+
+ @EnableBinderProxy
+ fun getCurrentInterruptionFilter(): Int {
+ return notificationManager.currentInterruptionFilter
+ }
+
+ @EnableBinderProxy
+ fun setInterruptionFilter(filter: Int) {
+ notificationManager.setInterruptionFilter(filter)
+ }
+}
diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml
index d0ccbd4..5934a77 100644
--- a/app/src/main/res/values-zh-rCN/strings.xml
+++ b/app/src/main/res/values-zh-rCN/strings.xml
@@ -20,8 +20,8 @@
闹钟
通知
亮度
- 在弹窗中显示系统滑块
- 在弹窗中隐藏系统滑块
+ 显示%1$s滑块
+ 隐藏%1$s滑块
开启振动模式
关闭振动模式
开启勿扰模式
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 3b156c8..fa74c48 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -19,8 +19,8 @@
Alarm
Notification
Brightness
- Show system sliders in popup
- Hide system sliders in popup
+ Show %1$s slider
+ Hide %1$s slider
Enable vibrate mode
Disable vibrate mode
Enable Do Not Disturb
From ce4fa96f8954d74435e634abcef4c872f72be56b Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 12 May 2026 05:52:00 +0000
Subject: [PATCH 11/22] Apply final review cleanup in system volume panel
Agent-Logs-Url: https://github.com/yume-chan/VolumeManager/sessions/ea154d5a-d158-408f-ade6-cc145e8a4b7e
Co-authored-by: yume-chan <1330321+yume-chan@users.noreply.github.com>
---
.../volume/compose/SystemVolumePanel.kt | 20 +++++++++----------
1 file changed, 10 insertions(+), 10 deletions(-)
diff --git a/app/src/main/java/moe/chensi/volume/compose/SystemVolumePanel.kt b/app/src/main/java/moe/chensi/volume/compose/SystemVolumePanel.kt
index 7a0bd87..8d42c89 100644
--- a/app/src/main/java/moe/chensi/volume/compose/SystemVolumePanel.kt
+++ b/app/src/main/java/moe/chensi/volume/compose/SystemVolumePanel.kt
@@ -79,15 +79,15 @@ fun SystemVolumePanel(
DisposableEffect(audioManager, showCallVolumeAlways) {
if (showCallVolumeAlways) {
- onDispose { }
- } else {
- val listener = AudioManager.OnModeChangedListener { mode ->
- inCallMode = isCallMode(mode)
- }
- audioManager.addOnModeChangedListener(executor, listener)
- onDispose {
- audioManager.removeOnModeChangedListener(listener)
- }
+ return@DisposableEffect onDispose { }
+ }
+
+ val listener = AudioManager.OnModeChangedListener { mode ->
+ inCallMode = isCallMode(mode)
+ }
+ audioManager.addOnModeChangedListener(executor, listener)
+ onDispose {
+ audioManager.removeOnModeChangedListener(listener)
}
}
@@ -288,7 +288,7 @@ private fun SliderVisibilityFooter(
return
}
- Row(horizontalArrangement = Arrangement.End, modifier = Modifier) {
+ Row(horizontalArrangement = Arrangement.End) {
SliderVisibilityToggle(
sliderId = sliderId,
sliderName = sliderName,
From d5e6d27491226d66920781ad61b6d25fd64d975e Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 12 May 2026 05:52:36 +0000
Subject: [PATCH 12/22] Use tolerance for brightness float comparison
Agent-Logs-Url: https://github.com/yume-chan/VolumeManager/sessions/ea154d5a-d158-408f-ade6-cc145e8a4b7e
Co-authored-by: yume-chan <1330321+yume-chan@users.noreply.github.com>
---
.../main/java/moe/chensi/volume/compose/SystemVolumePanel.kt | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/app/src/main/java/moe/chensi/volume/compose/SystemVolumePanel.kt b/app/src/main/java/moe/chensi/volume/compose/SystemVolumePanel.kt
index 8d42c89..ea59a7c 100644
--- a/app/src/main/java/moe/chensi/volume/compose/SystemVolumePanel.kt
+++ b/app/src/main/java/moe/chensi/volume/compose/SystemVolumePanel.kt
@@ -45,6 +45,7 @@ import androidx.core.content.ContextCompat
import moe.chensi.volume.R
import moe.chensi.volume.system.DisplayManagerProxy
import moe.chensi.volume.system.NotificationManagerProxy
+import kotlin.math.abs
import kotlin.math.roundToInt
object SystemSliderIds {
@@ -370,7 +371,7 @@ private fun BrightnessSlider(
value = brightness,
valueRange = 0f..maxBrightness,
onValueChange = { value ->
- if (brightness == value) {
+ if (abs(brightness - value) < 0.001f) {
return@TrackSlider
}
brightness = value
From 07f62923aafd53329ea41cf05995e435afade701 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 12 May 2026 05:53:14 +0000
Subject: [PATCH 13/22] Extract brightness change tolerance constant
Agent-Logs-Url: https://github.com/yume-chan/VolumeManager/sessions/ea154d5a-d158-408f-ade6-cc145e8a4b7e
Co-authored-by: yume-chan <1330321+yume-chan@users.noreply.github.com>
---
.../main/java/moe/chensi/volume/compose/SystemVolumePanel.kt | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/app/src/main/java/moe/chensi/volume/compose/SystemVolumePanel.kt b/app/src/main/java/moe/chensi/volume/compose/SystemVolumePanel.kt
index ea59a7c..f9b887f 100644
--- a/app/src/main/java/moe/chensi/volume/compose/SystemVolumePanel.kt
+++ b/app/src/main/java/moe/chensi/volume/compose/SystemVolumePanel.kt
@@ -57,6 +57,8 @@ object SystemSliderIds {
const val Brightness = "brightness"
}
+private const val BRIGHTNESS_CHANGE_TOLERANCE = 0.001f
+
private fun isCallMode(mode: Int): Boolean {
return mode == AudioManager.MODE_IN_CALL || mode == AudioManager.MODE_IN_COMMUNICATION
}
@@ -371,7 +373,7 @@ private fun BrightnessSlider(
value = brightness,
valueRange = 0f..maxBrightness,
onValueChange = { value ->
- if (abs(brightness - value) < 0.001f) {
+ if (abs(brightness - value) < BRIGHTNESS_CHANGE_TOLERANCE) {
return@TrackSlider
}
brightness = value
From 6e2e38eceabff27f3813ce038d19d934f76a8cb4 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 12 May 2026 06:21:41 +0000
Subject: [PATCH 14/22] Fix follow-up review issues for slider layout and
brightness
Agent-Logs-Url: https://github.com/yume-chan/VolumeManager/sessions/b84fe74d-6791-40c7-a7a2-9ea10e21da98
Co-authored-by: yume-chan <1330321+yume-chan@users.noreply.github.com>
---
.../main/java/moe/chensi/volume/Service.kt | 2 +-
.../chensi/volume/compose/BrightnessSlider.kt | 103 +++++++++++++++
.../volume/compose/StreamVolumeSlider.kt | 8 +-
.../volume/compose/SystemVolumePanel.kt | 119 +++---------------
.../volume/system/DisplayManagerProxy.kt | 14 ++-
5 files changed, 135 insertions(+), 111 deletions(-)
create mode 100644 app/src/main/java/moe/chensi/volume/compose/BrightnessSlider.kt
diff --git a/app/src/main/java/moe/chensi/volume/Service.kt b/app/src/main/java/moe/chensi/volume/Service.kt
index 9d58e37..d70af13 100644
--- a/app/src/main/java/moe/chensi/volume/Service.kt
+++ b/app/src/main/java/moe/chensi/volume/Service.kt
@@ -207,7 +207,7 @@ class Service : AccessibilityService() {
displayManagerProxy = manager.displayManagerProxy,
showCallVolumeAlways = false,
applyVisibilityFilter = true,
- allowVisibilityConfig = true,
+ allowVisibilityConfig = false,
isSliderVisible = manager::isSystemSliderVisible,
onSliderVisibilityChange = manager::setSystemSliderVisible,
onChange = this@Service.handler::startIdleTimer
diff --git a/app/src/main/java/moe/chensi/volume/compose/BrightnessSlider.kt b/app/src/main/java/moe/chensi/volume/compose/BrightnessSlider.kt
new file mode 100644
index 0000000..d716d6b
--- /dev/null
+++ b/app/src/main/java/moe/chensi/volume/compose/BrightnessSlider.kt
@@ -0,0 +1,103 @@
+package moe.chensi.volume.compose
+
+import android.hardware.display.DisplayManager
+import android.os.Handler
+import android.os.Looper
+import android.view.Display
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Brightness6
+import androidx.compose.material3.Icon
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableFloatStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import moe.chensi.volume.R
+import moe.chensi.volume.system.DisplayManagerProxy
+import kotlin.math.abs
+import kotlin.math.roundToInt
+
+private const val BRIGHTNESS_SLIDER_MAX = 100f
+private const val BRIGHTNESS_CHANGE_TOLERANCE = 0.001f
+
+@Composable
+fun BrightnessSlider(
+ displayManagerProxy: DisplayManagerProxy,
+ footer: (@Composable () -> Unit)? = null,
+ onChange: (() -> Unit)? = null
+) {
+ fun readBrightnessPercent(): Float {
+ return (displayManagerProxy.getDefaultDisplayBrightness().coerceIn(0f, 1f) * BRIGHTNESS_SLIDER_MAX)
+ }
+
+ var brightnessPercent by remember { mutableFloatStateOf(readBrightnessPercent()) }
+ val mainThreadHandler = remember { Handler(Looper.getMainLooper()) }
+
+ DisposableEffect(displayManagerProxy) {
+ val listener = object : DisplayManager.DisplayListener {
+ override fun onDisplayAdded(displayId: Int) = Unit
+
+ override fun onDisplayRemoved(displayId: Int) = Unit
+
+ override fun onDisplayChanged(displayId: Int) {
+ if (displayId == Display.DEFAULT_DISPLAY) {
+ brightnessPercent = readBrightnessPercent()
+ }
+ }
+ }
+
+ displayManagerProxy.registerDisplayListener(listener, mainThreadHandler)
+
+ onDispose {
+ displayManagerProxy.unregisterDisplayListener(listener)
+ }
+ }
+
+ Row(
+ horizontalArrangement = Arrangement.spacedBy(4.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ TrackSlider(
+ modifier = Modifier.weight(1f),
+ cornerRadius = 20.dp,
+ value = brightnessPercent,
+ valueRange = 0f..BRIGHTNESS_SLIDER_MAX,
+ onValueChange = { value ->
+ if (abs(brightnessPercent - value) < BRIGHTNESS_CHANGE_TOLERANCE) {
+ return@TrackSlider
+ }
+
+ brightnessPercent = value
+ displayManagerProxy.setDefaultDisplayBrightness((value / BRIGHTNESS_SLIDER_MAX).coerceIn(0f, 1f))
+ onChange?.invoke()
+ }
+ ) {
+ Row(
+ horizontalArrangement = Arrangement.spacedBy(8.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ modifier = Modifier.padding(12.dp, 8.dp)
+ ) {
+ Icon(
+ imageVector = Icons.Default.Brightness6,
+ contentDescription = stringResource(R.string.brightness),
+ modifier = Modifier.size(32.dp),
+ )
+ StreamSliderTextContent(
+ name = stringResource(R.string.brightness),
+ valueText = "${brightnessPercent.roundToInt()}/${BRIGHTNESS_SLIDER_MAX.roundToInt()}"
+ )
+ }
+ }
+
+ footer?.invoke()
+ }
+}
diff --git a/app/src/main/java/moe/chensi/volume/compose/StreamVolumeSlider.kt b/app/src/main/java/moe/chensi/volume/compose/StreamVolumeSlider.kt
index 0e36e9b..b21bb4e 100644
--- a/app/src/main/java/moe/chensi/volume/compose/StreamVolumeSlider.kt
+++ b/app/src/main/java/moe/chensi/volume/compose/StreamVolumeSlider.kt
@@ -6,13 +6,11 @@ import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.media.AudioManager
-import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.layout.weight
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
@@ -107,8 +105,12 @@ fun StreamVolumeSlider(
volume = audioManager.getStreamVolume(streamType)
}
- Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
+ Row(
+ horizontalArrangement = Arrangement.spacedBy(4.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
TrackSlider(
+ modifier = Modifier.weight(1f),
cornerRadius = 20.dp,
value = volume.toFloat(),
valueRange = 0f..maxVolume,
diff --git a/app/src/main/java/moe/chensi/volume/compose/SystemVolumePanel.kt b/app/src/main/java/moe/chensi/volume/compose/SystemVolumePanel.kt
index f9b887f..ea790ea 100644
--- a/app/src/main/java/moe/chensi/volume/compose/SystemVolumePanel.kt
+++ b/app/src/main/java/moe/chensi/volume/compose/SystemVolumePanel.kt
@@ -5,18 +5,12 @@ import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
-import android.database.ContentObserver
import android.media.AudioManager
-import android.os.Handler
-import android.os.Looper
-import android.provider.Settings
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Alarm
-import androidx.compose.material.icons.filled.Brightness6
import androidx.compose.material.icons.filled.DoNotDisturbOn
import androidx.compose.material.icons.filled.NotificationsActive
import androidx.compose.material.icons.filled.NotificationsNone
@@ -31,7 +25,6 @@ import androidx.compose.material3.Icon
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
@@ -45,8 +38,6 @@ import androidx.core.content.ContextCompat
import moe.chensi.volume.R
import moe.chensi.volume.system.DisplayManagerProxy
import moe.chensi.volume.system.NotificationManagerProxy
-import kotlin.math.abs
-import kotlin.math.roundToInt
object SystemSliderIds {
const val Media = "media"
@@ -57,8 +48,6 @@ object SystemSliderIds {
const val Brightness = "brightness"
}
-private const val BRIGHTNESS_CHANGE_TOLERANCE = 0.001f
-
private fun isCallMode(mode: Int): Boolean {
return mode == AudioManager.MODE_IN_CALL || mode == AudioManager.MODE_IN_COMMUNICATION
}
@@ -196,9 +185,15 @@ fun SystemVolumePanel(
if (!applyVisibilityFilter || isSliderVisible(SystemSliderIds.Brightness)) {
BrightnessSlider(
displayManagerProxy = displayManagerProxy,
- sliderVisible = isSliderVisible(SystemSliderIds.Brightness),
- allowVisibilityConfig = allowVisibilityConfig,
- onSliderVisibilityChange = onSliderVisibilityChange,
+ footer = {
+ SliderVisibilityToggle(
+ sliderId = SystemSliderIds.Brightness,
+ sliderName = stringResource(R.string.brightness),
+ allowVisibilityConfig = allowVisibilityConfig,
+ isVisible = isSliderVisible(SystemSliderIds.Brightness),
+ onSliderVisibilityChange = onSliderVisibilityChange
+ )
+ },
onChange = onChange
)
}
@@ -291,15 +286,13 @@ private fun SliderVisibilityFooter(
return
}
- Row(horizontalArrangement = Arrangement.End) {
- SliderVisibilityToggle(
- sliderId = sliderId,
- sliderName = sliderName,
- allowVisibilityConfig = allowVisibilityConfig,
- isVisible = isVisible,
- onSliderVisibilityChange = onSliderVisibilityChange
- )
- }
+ SliderVisibilityToggle(
+ sliderId = sliderId,
+ sliderName = sliderName,
+ allowVisibilityConfig = allowVisibilityConfig,
+ isVisible = isVisible,
+ onSliderVisibilityChange = onSliderVisibilityChange
+ )
}
@OptIn(ExperimentalMaterial3Api::class)
@@ -325,83 +318,3 @@ private fun SliderVisibilityToggle(
onSliderVisibilityChange(sliderId, it)
}
}
-
-@Composable
-private fun BrightnessSlider(
- displayManagerProxy: DisplayManagerProxy,
- sliderVisible: Boolean,
- allowVisibilityConfig: Boolean,
- onSliderVisibilityChange: (String, Boolean) -> Unit,
- onChange: (() -> Unit)? = null
-) {
- val context = LocalContext.current
- val contentResolver = context.contentResolver
-
- fun readBrightness(): Float {
- return displayManagerProxy.getDefaultDisplayBrightness()
- }
-
- fun readMaxBrightness(): Float {
- return displayManagerProxy.getDefaultDisplayMaxBrightness().coerceAtLeast(1f)
- }
-
- var maxBrightness by remember { mutableFloatStateOf(readMaxBrightness()) }
- var brightness by remember { mutableFloatStateOf(readBrightness()) }
- val mainThreadHandler = remember { Handler(Looper.getMainLooper()) }
-
- DisposableEffect(contentResolver) {
- val observer = object : ContentObserver(mainThreadHandler) {
- override fun onChange(selfChange: Boolean) {
- maxBrightness = readMaxBrightness()
- brightness = readBrightness()
- }
- }
-
- contentResolver.registerContentObserver(
- Settings.System.getUriFor(Settings.System.SCREEN_BRIGHTNESS),
- false,
- observer
- )
-
- onDispose {
- contentResolver.unregisterContentObserver(observer)
- }
- }
-
- TrackSlider(
- cornerRadius = 20.dp,
- value = brightness,
- valueRange = 0f..maxBrightness,
- onValueChange = { value ->
- if (abs(brightness - value) < BRIGHTNESS_CHANGE_TOLERANCE) {
- return@TrackSlider
- }
- brightness = value
- displayManagerProxy.setDefaultDisplayBrightness(value)
- onChange?.invoke()
- }
- ) {
- Row(
- horizontalArrangement = Arrangement.spacedBy(8.dp),
- verticalAlignment = Alignment.CenterVertically,
- ) {
- Icon(
- imageVector = Icons.Default.Brightness6,
- contentDescription = stringResource(R.string.brightness),
- modifier = Modifier.size(32.dp),
- )
- StreamSliderTextContent(
- name = stringResource(R.string.brightness),
- valueText = "${brightness.roundToInt()}/${maxBrightness.roundToInt()}"
- )
- }
- }
-
- SliderVisibilityFooter(
- sliderId = SystemSliderIds.Brightness,
- sliderName = stringResource(R.string.brightness),
- allowVisibilityConfig = allowVisibilityConfig,
- isVisible = sliderVisible,
- onSliderVisibilityChange = onSliderVisibilityChange
- )
-}
diff --git a/app/src/main/java/moe/chensi/volume/system/DisplayManagerProxy.kt b/app/src/main/java/moe/chensi/volume/system/DisplayManagerProxy.kt
index 36530b4..d056954 100644
--- a/app/src/main/java/moe/chensi/volume/system/DisplayManagerProxy.kt
+++ b/app/src/main/java/moe/chensi/volume/system/DisplayManagerProxy.kt
@@ -2,6 +2,7 @@ package moe.chensi.volume.system
import android.content.Context
import android.hardware.display.DisplayManager
+import android.os.Handler
import android.view.Display
import moe.chensi.volume.EnableBinderProxy
import moe.chensi.volume.ToggleableBinderProxy
@@ -35,12 +36,17 @@ class DisplayManagerProxy private constructor(context: Context) {
}
@EnableBinderProxy
- fun getDefaultDisplayMaxBrightness(): Float {
- return displayManager.getBrightnessInfo(Display.DEFAULT_DISPLAY)?.brightnessMaximum ?: 1f
+ fun setDefaultDisplayBrightness(value: Float) {
+ displayManagerReflect.call("setBrightness", Display.DEFAULT_DISPLAY, value)
}
@EnableBinderProxy
- fun setDefaultDisplayBrightness(value: Float) {
- displayManagerReflect.call("setBrightness", Display.DEFAULT_DISPLAY, value)
+ fun registerDisplayListener(listener: DisplayManager.DisplayListener, handler: Handler?) {
+ displayManager.registerDisplayListener(listener, handler)
+ }
+
+ @EnableBinderProxy
+ fun unregisterDisplayListener(listener: DisplayManager.DisplayListener) {
+ displayManager.unregisterDisplayListener(listener)
}
}
From f06ae0b804d44cc743765c86bde3cdd203e0cca7 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 12 May 2026 06:22:41 +0000
Subject: [PATCH 15/22] Document DisplayManagerProxy brightness API contract
Agent-Logs-Url: https://github.com/yume-chan/VolumeManager/sessions/b84fe74d-6791-40c7-a7a2-9ea10e21da98
Co-authored-by: yume-chan <1330321+yume-chan@users.noreply.github.com>
---
.../java/moe/chensi/volume/system/DisplayManagerProxy.kt | 6 ++++++
1 file changed, 6 insertions(+)
diff --git a/app/src/main/java/moe/chensi/volume/system/DisplayManagerProxy.kt b/app/src/main/java/moe/chensi/volume/system/DisplayManagerProxy.kt
index d056954..b7f582b 100644
--- a/app/src/main/java/moe/chensi/volume/system/DisplayManagerProxy.kt
+++ b/app/src/main/java/moe/chensi/volume/system/DisplayManagerProxy.kt
@@ -36,6 +36,12 @@ class DisplayManagerProxy private constructor(context: Context) {
}
@EnableBinderProxy
+ /**
+ * Sets default display brightness.
+ *
+ * @param value Brightness in [0f, 1f].
+ * Uses reflection to call DisplayManager hidden setBrightness API.
+ */
fun setDefaultDisplayBrightness(value: Float) {
displayManagerReflect.call("setBrightness", Display.DEFAULT_DISPLAY, value)
}
From 65131cfcd4de68c5f67de22c73b03c5979bc47a4 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 12 May 2026 06:23:48 +0000
Subject: [PATCH 16/22] Apply final review nits for brightness slider and proxy
docs
Agent-Logs-Url: https://github.com/yume-chan/VolumeManager/sessions/b84fe74d-6791-40c7-a7a2-9ea10e21da98
Co-authored-by: yume-chan <1330321+yume-chan@users.noreply.github.com>
---
app/src/main/java/moe/chensi/volume/compose/BrightnessSlider.kt | 2 +-
.../main/java/moe/chensi/volume/system/DisplayManagerProxy.kt | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/app/src/main/java/moe/chensi/volume/compose/BrightnessSlider.kt b/app/src/main/java/moe/chensi/volume/compose/BrightnessSlider.kt
index d716d6b..2f04438 100644
--- a/app/src/main/java/moe/chensi/volume/compose/BrightnessSlider.kt
+++ b/app/src/main/java/moe/chensi/volume/compose/BrightnessSlider.kt
@@ -93,7 +93,7 @@ fun BrightnessSlider(
)
StreamSliderTextContent(
name = stringResource(R.string.brightness),
- valueText = "${brightnessPercent.roundToInt()}/${BRIGHTNESS_SLIDER_MAX.roundToInt()}"
+ valueText = "${brightnessPercent.roundToInt()}/100"
)
}
}
diff --git a/app/src/main/java/moe/chensi/volume/system/DisplayManagerProxy.kt b/app/src/main/java/moe/chensi/volume/system/DisplayManagerProxy.kt
index b7f582b..c71688e 100644
--- a/app/src/main/java/moe/chensi/volume/system/DisplayManagerProxy.kt
+++ b/app/src/main/java/moe/chensi/volume/system/DisplayManagerProxy.kt
@@ -35,13 +35,13 @@ class DisplayManagerProxy private constructor(context: Context) {
return displayManagerReflect.call("getBrightness", Display.DEFAULT_DISPLAY).get()
}
- @EnableBinderProxy
/**
* Sets default display brightness.
*
* @param value Brightness in [0f, 1f].
* Uses reflection to call DisplayManager hidden setBrightness API.
*/
+ @EnableBinderProxy
fun setDefaultDisplayBrightness(value: Float) {
displayManagerReflect.call("setBrightness", Display.DEFAULT_DISPLAY, value)
}
From 5d989dad03f829d4d262d25000644b4fcb4e0d74 Mon Sep 17 00:00:00 2001
From: Simon Chan <1330321+yume-chan@users.noreply.github.com>
Date: Tue, 12 May 2026 23:16:57 +0800
Subject: [PATCH 17/22] Fix display brightness gamma conversion on Xiaomi
devices
---
.../chensi/volume/compose/BrightnessSlider.kt | 18 +++--
.../chensi/volume/system/BrightnessUtils.kt | 71 +++++++++++++++++++
.../volume/system/DisplayManagerProxy.kt | 51 ++++++++++---
3 files changed, 121 insertions(+), 19 deletions(-)
create mode 100644 app/src/main/java/moe/chensi/volume/system/BrightnessUtils.kt
diff --git a/app/src/main/java/moe/chensi/volume/compose/BrightnessSlider.kt b/app/src/main/java/moe/chensi/volume/compose/BrightnessSlider.kt
index 2f04438..f277298 100644
--- a/app/src/main/java/moe/chensi/volume/compose/BrightnessSlider.kt
+++ b/app/src/main/java/moe/chensi/volume/compose/BrightnessSlider.kt
@@ -3,6 +3,7 @@ package moe.chensi.volume.compose
import android.hardware.display.DisplayManager
import android.os.Handler
import android.os.Looper
+import android.util.Log
import android.view.Display
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
@@ -26,7 +27,6 @@ import moe.chensi.volume.system.DisplayManagerProxy
import kotlin.math.abs
import kotlin.math.roundToInt
-private const val BRIGHTNESS_SLIDER_MAX = 100f
private const val BRIGHTNESS_CHANGE_TOLERANCE = 0.001f
@Composable
@@ -35,22 +35,20 @@ fun BrightnessSlider(
footer: (@Composable () -> Unit)? = null,
onChange: (() -> Unit)? = null
) {
- fun readBrightnessPercent(): Float {
- return (displayManagerProxy.getDefaultDisplayBrightness().coerceIn(0f, 1f) * BRIGHTNESS_SLIDER_MAX)
- }
-
- var brightnessPercent by remember { mutableFloatStateOf(readBrightnessPercent()) }
+ var brightnessPercent by remember { mutableFloatStateOf(displayManagerProxy.getDefaultDisplayBrightnessGammaPercentage()) }
val mainThreadHandler = remember { Handler(Looper.getMainLooper()) }
DisposableEffect(displayManagerProxy) {
+
val listener = object : DisplayManager.DisplayListener {
override fun onDisplayAdded(displayId: Int) = Unit
override fun onDisplayRemoved(displayId: Int) = Unit
override fun onDisplayChanged(displayId: Int) {
+ Log.d("BrightnessSlider", "onDisplayChanged: $displayId")
if (displayId == Display.DEFAULT_DISPLAY) {
- brightnessPercent = readBrightnessPercent()
+ brightnessPercent = displayManagerProxy.getDefaultDisplayBrightnessGammaPercentage()
}
}
}
@@ -70,14 +68,14 @@ fun BrightnessSlider(
modifier = Modifier.weight(1f),
cornerRadius = 20.dp,
value = brightnessPercent,
- valueRange = 0f..BRIGHTNESS_SLIDER_MAX,
+ valueRange = 0f..1f,
onValueChange = { value ->
if (abs(brightnessPercent - value) < BRIGHTNESS_CHANGE_TOLERANCE) {
return@TrackSlider
}
brightnessPercent = value
- displayManagerProxy.setDefaultDisplayBrightness((value / BRIGHTNESS_SLIDER_MAX).coerceIn(0f, 1f))
+ displayManagerProxy.setDefaultDisplayBrightnessGammaPercentage(value)
onChange?.invoke()
}
) {
@@ -93,7 +91,7 @@ fun BrightnessSlider(
)
StreamSliderTextContent(
name = stringResource(R.string.brightness),
- valueText = "${brightnessPercent.roundToInt()}/100"
+ valueText = "${(brightnessPercent * 100).roundToInt()}%"
)
}
}
diff --git a/app/src/main/java/moe/chensi/volume/system/BrightnessUtils.kt b/app/src/main/java/moe/chensi/volume/system/BrightnessUtils.kt
new file mode 100644
index 0000000..cffce18
--- /dev/null
+++ b/app/src/main/java/moe/chensi/volume/system/BrightnessUtils.kt
@@ -0,0 +1,71 @@
+package moe.chensi.volume.system
+
+import android.annotation.SuppressLint
+import android.app.ActivityThread
+import android.os.Build
+import android.util.Log
+import android.util.MathUtils
+
+@SuppressLint("ResourceType")
+object BrightnessUtils {
+ var R: Float = 0.5f
+ var A: Float = 0.17883277f
+ var B: Float = 0.28466892f
+ var C: Float = 0.5599107f
+
+ init {
+ Log.d("BrightnessUtils", "MANUFACTURER: ${Build.MANUFACTURER}")
+
+ if (Build.MANUFACTURER == "Xiaomi") {
+ val app = ActivityThread.currentApplication()
+ val resources = app.resources
+
+ val rId =
+ resources.getIdentifier("config_GammaLinearConvertRValue", "dimen", "android.miui")
+ val aId =
+ resources.getIdentifier("config_GammaLinearConvertAValue", "dimen", "android.miui")
+ val bId =
+ resources.getIdentifier("config_GammaLinearConvertBValue", "dimen", "android.miui")
+ val cId =
+ resources.getIdentifier("config_GammaLinearConvertCValue", "dimen", "android.miui")
+
+ if (rId != 0 && aId != 0 && bId != 0 && cId != 0) {
+ R = resources.getFloat(rId)
+ A = resources.getFloat(aId)
+ B = resources.getFloat(bId)
+ C = resources.getFloat(cId)
+ }
+ }
+
+ Log.d("BrightnessUtils", "R: $R")
+ Log.d("BrightnessUtils", "A: $A")
+ Log.d("BrightnessUtils", "B: $B")
+ Log.d("BrightnessUtils", "C: $C")
+ }
+
+ fun convertGammaPercentageToLinear(v: Float, min: Float, max: Float): Float {
+ val ret: Float = if (v <= R) {
+ MathUtils.sq(v / R)
+ } else {
+ MathUtils.exp((v - C) / A) + B
+ }
+
+ // HLG is normalized to the range [0, 12], ensure that value is within that range,
+ // it shouldn't be out of bounds.
+ val normalizedRet = MathUtils.constrain(ret, 0f, 12f)
+
+ // Re-normalize to the range [0, 1]
+ // in order to derive the correct setting value.
+ return MathUtils.lerp(min, max, normalizedRet / 12)
+ }
+
+ fun convertLinearToGammaPercentage(v: Float, min: Float, max: Float): Float {
+ // For some reason, HLG normalizes to the range [0, 12] rather than [0, 1]
+ val normalizedVal: Float = MathUtils.norm(min, max, v) * 12
+ return if (normalizedVal <= 1f) {
+ MathUtils.sqrt(normalizedVal) * R
+ } else {
+ A * MathUtils.log(normalizedVal - B) + C
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/moe/chensi/volume/system/DisplayManagerProxy.kt b/app/src/main/java/moe/chensi/volume/system/DisplayManagerProxy.kt
index c71688e..b8a35ff 100644
--- a/app/src/main/java/moe/chensi/volume/system/DisplayManagerProxy.kt
+++ b/app/src/main/java/moe/chensi/volume/system/DisplayManagerProxy.kt
@@ -1,8 +1,10 @@
package moe.chensi.volume.system
+import android.annotation.SuppressLint
import android.content.Context
import android.hardware.display.DisplayManager
import android.os.Handler
+import android.util.Log
import android.view.Display
import moe.chensi.volume.EnableBinderProxy
import moe.chensi.volume.ToggleableBinderProxy
@@ -19,20 +21,40 @@ class DisplayManagerProxy private constructor(context: Context) {
}
private val displayManager = context.getSystemService(DisplayManager::class.java)!!
+ private val displayManagerGlobalReflect =
+ Reflect.onClass("android.hardware.display.DisplayManagerGlobal").call("getInstance")
private val displayManagerReflect = Reflect.on(displayManager)
init {
- val service = Reflect.onClass("android.hardware.display.DisplayManagerGlobal")
- .call("getInstance")
- .get()
- .run(Reflect::on)
- .get("mDm")
+ val service = displayManagerGlobalReflect.get("mDm")
ToggleableBinderProxy.wrap(service)
}
+ @SuppressLint("MissingPermission")
@EnableBinderProxy
- fun getDefaultDisplayBrightness(): Float {
- return displayManagerReflect.call("getBrightness", Display.DEFAULT_DISPLAY).get()
+ fun getDefaultDisplayBrightnessGammaPercentage(): Float {
+ val display = displayManager.getDisplay(Display.DEFAULT_DISPLAY)
+ val brightnessInfo = display.brightnessInfo
+ if (brightnessInfo == null) {
+ Log.w(
+ "DisplayManagerProxy",
+ "getDefaultDisplayBrightnessPercentage: brightnessInfo is null"
+ )
+ return 0f
+ }
+ Log.d(
+ "DisplayManagerProxy",
+ "getDefaultDisplayBrightnessPercentage: ${brightnessInfo.brightness} ${brightnessInfo.brightnessMinimum} ${brightnessInfo.brightnessMaximum}"
+ )
+
+ val gamma = BrightnessUtils.convertLinearToGammaPercentage(
+ brightnessInfo.brightness,
+ brightnessInfo.brightnessMinimum,
+ brightnessInfo.brightnessMaximum
+ )
+ Log.d("DisplayManagerProxy", "getDefaultDisplayBrightnessPercentage: $gamma")
+
+ return gamma
}
/**
@@ -41,9 +63,20 @@ class DisplayManagerProxy private constructor(context: Context) {
* @param value Brightness in [0f, 1f].
* Uses reflection to call DisplayManager hidden setBrightness API.
*/
+ @SuppressLint("MissingPermission")
@EnableBinderProxy
- fun setDefaultDisplayBrightness(value: Float) {
- displayManagerReflect.call("setBrightness", Display.DEFAULT_DISPLAY, value)
+ fun setDefaultDisplayBrightnessGammaPercentage(value: Float) {
+ Log.d("DisplayManagerProxy", "setDefaultDisplayBrightnessPercentage: $value")
+
+ val display = displayManager.getDisplay(Display.DEFAULT_DISPLAY)
+ val brightnessInfo = display.brightnessInfo ?: return
+
+ val brightness = BrightnessUtils.convertGammaPercentageToLinear(
+ value, brightnessInfo.brightnessMinimum, brightnessInfo.brightnessMaximum
+ )
+ Log.d("DisplayManagerProxy", "setDefaultDisplayBrightnessPercentage: $brightness")
+
+ displayManagerReflect.call("setBrightness", display.displayId, brightness)
}
@EnableBinderProxy
From d2cd0f63ed5a7ec5a8c4dfa131bc02397f9612a6 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 12 May 2026 15:39:27 +0000
Subject: [PATCH 18/22] Fix brightness proxy reflection and add auto-brightness
controls
Agent-Logs-Url: https://github.com/yume-chan/VolumeManager/sessions/29309638-aa08-48d7-acc8-ef0bad929682
Co-authored-by: yume-chan <1330321+yume-chan@users.noreply.github.com>
---
.../chensi/volume/ToggleableBinderProxy.kt | 25 ++++-
.../chensi/volume/compose/BrightnessSlider.kt | 43 +++++++--
.../chensi/volume/system/BrightnessUtils.kt | 92 ++++++++++++++-----
.../volume/system/DisplayManagerProxy.kt | 88 +++++++++++++++++-
app/src/main/res/values-zh-rCN/strings.xml | 2 +
app/src/main/res/values/strings.xml | 2 +
6 files changed, 215 insertions(+), 37 deletions(-)
diff --git a/app/src/main/java/moe/chensi/volume/ToggleableBinderProxy.kt b/app/src/main/java/moe/chensi/volume/ToggleableBinderProxy.kt
index 8225ace..5d8b3b0 100644
--- a/app/src/main/java/moe/chensi/volume/ToggleableBinderProxy.kt
+++ b/app/src/main/java/moe/chensi/volume/ToggleableBinderProxy.kt
@@ -24,6 +24,7 @@ class EnableBinderProxyCut : BasePointCut {
class ToggleableBinderProxy(private val base: IBinder) : IBinder by base {
companion object {
+ private const val SHELL_PACKAGE_NAME = "com.android.shell"
private val _enabled: ThreadLocal = ThreadLocal.withInitial { false }
var enabled: Boolean
@@ -32,14 +33,36 @@ class ToggleableBinderProxy(private val base: IBinder) : IBinder by base {
fun withEnabled(block: () -> T): T {
val prev = enabled
+ val restoreActivityThreadPackageName = patchActivityThreadPackageName()
try {
enabled = true
return block()
} finally {
+ restoreActivityThreadPackageName()
enabled = prev
}
}
+ private fun patchActivityThreadPackageName(): () -> Unit {
+ val field = runCatching {
+ Class.forName("android.app.ActivityThread").getDeclaredField("sCurrentPackageName")
+ .apply { isAccessible = true }
+ }.getOrNull() ?: return {}
+
+ val holder = runCatching { field.get(null) }.getOrNull()
+ if (holder is ThreadLocal<*>) {
+ @Suppress("UNCHECKED_CAST")
+ val packageNameHolder = holder as ThreadLocal
+ val previous = packageNameHolder.get()
+ packageNameHolder.set(SHELL_PACKAGE_NAME)
+ return { packageNameHolder.set(previous) }
+ }
+
+ val previous = holder
+ runCatching { field.set(null, SHELL_PACKAGE_NAME) }
+ return { runCatching { field.set(null, previous) } }
+ }
+
fun wrap(proxy: Any) {
Reflect.on(proxy).apply {
set("mRemote", get("mRemote").run {
@@ -69,4 +92,4 @@ class ToggleableBinderProxy(private val base: IBinder) : IBinder by base {
): Boolean {
return (if (enabled) shizukuWrapper else base).transact(code, data, reply, flags)
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/moe/chensi/volume/compose/BrightnessSlider.kt b/app/src/main/java/moe/chensi/volume/compose/BrightnessSlider.kt
index f277298..53aeb94 100644
--- a/app/src/main/java/moe/chensi/volume/compose/BrightnessSlider.kt
+++ b/app/src/main/java/moe/chensi/volume/compose/BrightnessSlider.kt
@@ -10,12 +10,14 @@ import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.BrightnessAuto
import androidx.compose.material.icons.filled.Brightness6
import androidx.compose.material3.Icon
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
+import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
@@ -36,6 +38,8 @@ fun BrightnessSlider(
onChange: (() -> Unit)? = null
) {
var brightnessPercent by remember { mutableFloatStateOf(displayManagerProxy.getDefaultDisplayBrightnessGammaPercentage()) }
+ var autoBrightnessEnabled by remember { mutableStateOf(displayManagerProxy.isAutoBrightnessEnabled()) }
+ var autoBrightnessBias by remember { mutableFloatStateOf(displayManagerProxy.getAutoBrightnessBias()) }
val mainThreadHandler = remember { Handler(Looper.getMainLooper()) }
DisposableEffect(displayManagerProxy) {
@@ -49,6 +53,8 @@ fun BrightnessSlider(
Log.d("BrightnessSlider", "onDisplayChanged: $displayId")
if (displayId == Display.DEFAULT_DISPLAY) {
brightnessPercent = displayManagerProxy.getDefaultDisplayBrightnessGammaPercentage()
+ autoBrightnessEnabled = displayManagerProxy.isAutoBrightnessEnabled()
+ autoBrightnessBias = displayManagerProxy.getAutoBrightnessBias()
}
}
}
@@ -67,15 +73,23 @@ fun BrightnessSlider(
TrackSlider(
modifier = Modifier.weight(1f),
cornerRadius = 20.dp,
- value = brightnessPercent,
- valueRange = 0f..1f,
+ value = if (autoBrightnessEnabled) autoBrightnessBias else brightnessPercent,
+ valueRange = if (autoBrightnessEnabled) -1f..1f else 0f..1f,
onValueChange = { value ->
- if (abs(brightnessPercent - value) < BRIGHTNESS_CHANGE_TOLERANCE) {
+ if (autoBrightnessEnabled && abs(autoBrightnessBias - value) < BRIGHTNESS_CHANGE_TOLERANCE) {
+ return@TrackSlider
+ }
+ if (!autoBrightnessEnabled && abs(brightnessPercent - value) < BRIGHTNESS_CHANGE_TOLERANCE) {
return@TrackSlider
}
- brightnessPercent = value
- displayManagerProxy.setDefaultDisplayBrightnessGammaPercentage(value)
+ if (autoBrightnessEnabled) {
+ autoBrightnessBias = value
+ displayManagerProxy.setAutoBrightnessBias(value)
+ } else {
+ brightnessPercent = value
+ displayManagerProxy.setDefaultDisplayBrightnessGammaPercentage(value)
+ }
onChange?.invoke()
}
) {
@@ -91,11 +105,28 @@ fun BrightnessSlider(
)
StreamSliderTextContent(
name = stringResource(R.string.brightness),
- valueText = "${(brightnessPercent * 100).roundToInt()}%"
+ valueText = if (autoBrightnessEnabled) {
+ "${(autoBrightnessBias * 100).roundToInt()}%"
+ } else {
+ "${(brightnessPercent * 100).roundToInt()}%"
+ }
)
}
}
+ ToggleButton(
+ checked = autoBrightnessEnabled,
+ checkedDescription = stringResource(R.string.disable_auto_brightness),
+ checkedIcon = Icons.Default.BrightnessAuto,
+ uncheckedDescription = stringResource(R.string.enable_auto_brightness),
+ uncheckedIcon = Icons.Default.Brightness6
+ ) {
+ displayManagerProxy.setAutoBrightnessEnabled(it)
+ autoBrightnessEnabled = displayManagerProxy.isAutoBrightnessEnabled()
+ autoBrightnessBias = displayManagerProxy.getAutoBrightnessBias()
+ onChange?.invoke()
+ }
+
footer?.invoke()
}
}
diff --git a/app/src/main/java/moe/chensi/volume/system/BrightnessUtils.kt b/app/src/main/java/moe/chensi/volume/system/BrightnessUtils.kt
index cffce18..7686de4 100644
--- a/app/src/main/java/moe/chensi/volume/system/BrightnessUtils.kt
+++ b/app/src/main/java/moe/chensi/volume/system/BrightnessUtils.kt
@@ -1,10 +1,12 @@
package moe.chensi.volume.system
import android.annotation.SuppressLint
-import android.app.ActivityThread
import android.os.Build
import android.util.Log
-import android.util.MathUtils
+import org.joor.Reflect
+import kotlin.math.exp
+import kotlin.math.ln
+import kotlin.math.sqrt
@SuppressLint("ResourceType")
object BrightnessUtils {
@@ -17,23 +19,45 @@ object BrightnessUtils {
Log.d("BrightnessUtils", "MANUFACTURER: ${Build.MANUFACTURER}")
if (Build.MANUFACTURER == "Xiaomi") {
- val app = ActivityThread.currentApplication()
- val resources = app.resources
+ val app = runCatching {
+ Reflect.onClass("android.app.ActivityThread")
+ .call("currentApplication")
+ .get()
+ }.getOrNull()
+ val resources = app?.let {
+ runCatching {
+ Reflect.on(it).call("getResources").get()
+ }.getOrNull()
+ }
- val rId =
- resources.getIdentifier("config_GammaLinearConvertRValue", "dimen", "android.miui")
- val aId =
- resources.getIdentifier("config_GammaLinearConvertAValue", "dimen", "android.miui")
- val bId =
- resources.getIdentifier("config_GammaLinearConvertBValue", "dimen", "android.miui")
- val cId =
- resources.getIdentifier("config_GammaLinearConvertCValue", "dimen", "android.miui")
+ if (resources != null) {
+ val rId = resources.getIdentifier(
+ "config_GammaLinearConvertRValue",
+ "dimen",
+ "android.miui"
+ )
+ val aId = resources.getIdentifier(
+ "config_GammaLinearConvertAValue",
+ "dimen",
+ "android.miui"
+ )
+ val bId = resources.getIdentifier(
+ "config_GammaLinearConvertBValue",
+ "dimen",
+ "android.miui"
+ )
+ val cId = resources.getIdentifier(
+ "config_GammaLinearConvertCValue",
+ "dimen",
+ "android.miui"
+ )
- if (rId != 0 && aId != 0 && bId != 0 && cId != 0) {
- R = resources.getFloat(rId)
- A = resources.getFloat(aId)
- B = resources.getFloat(bId)
- C = resources.getFloat(cId)
+ if (rId != 0 && aId != 0 && bId != 0 && cId != 0) {
+ R = resources.getFloat(rId)
+ A = resources.getFloat(aId)
+ B = resources.getFloat(bId)
+ C = resources.getFloat(cId)
+ }
}
}
@@ -45,27 +69,45 @@ object BrightnessUtils {
fun convertGammaPercentageToLinear(v: Float, min: Float, max: Float): Float {
val ret: Float = if (v <= R) {
- MathUtils.sq(v / R)
+ sq(v / R)
} else {
- MathUtils.exp((v - C) / A) + B
+ exp((v - C) / A) + B
}
// HLG is normalized to the range [0, 12], ensure that value is within that range,
// it shouldn't be out of bounds.
- val normalizedRet = MathUtils.constrain(ret, 0f, 12f)
+ val normalizedRet = constrain(ret, 0f, 12f)
// Re-normalize to the range [0, 1]
// in order to derive the correct setting value.
- return MathUtils.lerp(min, max, normalizedRet / 12)
+ return lerp(min, max, normalizedRet / 12)
}
fun convertLinearToGammaPercentage(v: Float, min: Float, max: Float): Float {
// For some reason, HLG normalizes to the range [0, 12] rather than [0, 1]
- val normalizedVal: Float = MathUtils.norm(min, max, v) * 12
+ val normalizedVal: Float = norm(min, max, v) * 12
return if (normalizedVal <= 1f) {
- MathUtils.sqrt(normalizedVal) * R
+ sqrt(normalizedVal) * R
} else {
- A * MathUtils.log(normalizedVal - B) + C
+ A * ln(normalizedVal - B) + C
+ }
+ }
+
+ private fun sq(value: Float): Float = value * value
+
+ private fun constrain(value: Float, min: Float, max: Float): Float {
+ return value.coerceIn(min, max)
+ }
+
+ private fun lerp(start: Float, stop: Float, amount: Float): Float {
+ return start + (stop - start) * amount
+ }
+
+ private fun norm(start: Float, stop: Float, value: Float): Float {
+ val range = stop - start
+ if (range == 0f) {
+ return 0f
}
+ return (value - start) / range
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/moe/chensi/volume/system/DisplayManagerProxy.kt b/app/src/main/java/moe/chensi/volume/system/DisplayManagerProxy.kt
index b8a35ff..3228d59 100644
--- a/app/src/main/java/moe/chensi/volume/system/DisplayManagerProxy.kt
+++ b/app/src/main/java/moe/chensi/volume/system/DisplayManagerProxy.kt
@@ -4,6 +4,7 @@ import android.annotation.SuppressLint
import android.content.Context
import android.hardware.display.DisplayManager
import android.os.Handler
+import android.provider.Settings
import android.util.Log
import android.view.Display
import moe.chensi.volume.EnableBinderProxy
@@ -13,6 +14,7 @@ import java.util.WeakHashMap
class DisplayManagerProxy private constructor(context: Context) {
companion object {
+ private const val AUTO_BRIGHTNESS_ADJ_KEY = "screen_auto_brightness_adj"
private val cache = WeakHashMap()
operator fun invoke(context: Context): DisplayManagerProxy {
@@ -21,6 +23,7 @@ class DisplayManagerProxy private constructor(context: Context) {
}
private val displayManager = context.getSystemService(DisplayManager::class.java)!!
+ private val contentResolver = context.contentResolver
private val displayManagerGlobalReflect =
Reflect.onClass("android.hardware.display.DisplayManagerGlobal").call("getInstance")
private val displayManagerReflect = Reflect.on(displayManager)
@@ -30,11 +33,35 @@ class DisplayManagerProxy private constructor(context: Context) {
ToggleableBinderProxy.wrap(service)
}
+ private data class BrightnessInfoValue(
+ val brightness: Float,
+ val brightnessMinimum: Float,
+ val brightnessMaximum: Float
+ )
+
+ @EnableBinderProxy
+ private fun getDefaultDisplayBrightnessInfo(): BrightnessInfoValue? {
+ val display = runCatching {
+ displayManagerReflect.call("getDisplay", Display.DEFAULT_DISPLAY).get()
+ }.getOrNull() ?: return null
+
+ val brightnessInfo = runCatching {
+ Reflect.on(display).call("getBrightnessInfo").get()
+ }.getOrNull() ?: return null
+
+ return runCatching {
+ BrightnessInfoValue(
+ brightness = Reflect.on(brightnessInfo).get("brightness"),
+ brightnessMinimum = Reflect.on(brightnessInfo).get("brightnessMinimum"),
+ brightnessMaximum = Reflect.on(brightnessInfo).get("brightnessMaximum")
+ )
+ }.getOrNull()
+ }
+
@SuppressLint("MissingPermission")
@EnableBinderProxy
fun getDefaultDisplayBrightnessGammaPercentage(): Float {
- val display = displayManager.getDisplay(Display.DEFAULT_DISPLAY)
- val brightnessInfo = display.brightnessInfo
+ val brightnessInfo = getDefaultDisplayBrightnessInfo()
if (brightnessInfo == null) {
Log.w(
"DisplayManagerProxy",
@@ -68,15 +95,66 @@ class DisplayManagerProxy private constructor(context: Context) {
fun setDefaultDisplayBrightnessGammaPercentage(value: Float) {
Log.d("DisplayManagerProxy", "setDefaultDisplayBrightnessPercentage: $value")
- val display = displayManager.getDisplay(Display.DEFAULT_DISPLAY)
- val brightnessInfo = display.brightnessInfo ?: return
+ val brightnessInfo = getDefaultDisplayBrightnessInfo() ?: return
val brightness = BrightnessUtils.convertGammaPercentageToLinear(
value, brightnessInfo.brightnessMinimum, brightnessInfo.brightnessMaximum
)
Log.d("DisplayManagerProxy", "setDefaultDisplayBrightnessPercentage: $brightness")
- displayManagerReflect.call("setBrightness", display.displayId, brightness)
+ displayManagerReflect.call("setBrightness", Display.DEFAULT_DISPLAY, brightness)
+ }
+
+ @EnableBinderProxy
+ fun isAutoBrightnessEnabled(): Boolean {
+ val mode = Settings.System.getInt(
+ contentResolver,
+ Settings.System.SCREEN_BRIGHTNESS_MODE,
+ Settings.System.SCREEN_BRIGHTNESS_MODE_MANUAL
+ )
+ return mode == Settings.System.SCREEN_BRIGHTNESS_MODE_AUTOMATIC
+ }
+
+ @EnableBinderProxy
+ fun setAutoBrightnessEnabled(enabled: Boolean) {
+ val mode = if (enabled) {
+ Settings.System.SCREEN_BRIGHTNESS_MODE_AUTOMATIC
+ } else {
+ Settings.System.SCREEN_BRIGHTNESS_MODE_MANUAL
+ }
+
+ val updated = Settings.System.putInt(
+ contentResolver,
+ Settings.System.SCREEN_BRIGHTNESS_MODE,
+ mode
+ )
+ if (!updated) {
+ Log.w("DisplayManagerProxy", "setAutoBrightnessEnabled failed")
+ }
+ }
+
+ @EnableBinderProxy
+ fun getAutoBrightnessBias(): Float {
+ return Settings.System.getFloat(
+ contentResolver,
+ AUTO_BRIGHTNESS_ADJ_KEY,
+ 0f
+ ).coerceIn(-1f, 1f)
+ }
+
+ @EnableBinderProxy
+ fun setAutoBrightnessBias(value: Float) {
+ val adjusted = value.coerceIn(-1f, 1f)
+ displayManagerReflect.call("setTemporaryAutoBrightnessAdjustment", adjusted)
+
+ val updated = Settings.System.putFloat(
+ contentResolver,
+ AUTO_BRIGHTNESS_ADJ_KEY,
+ adjusted
+ )
+ if (!updated) {
+ Log.w("DisplayManagerProxy", "setAutoBrightnessBias failed")
+ }
}
@EnableBinderProxy
diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml
index 5934a77..caf076b 100644
--- a/app/src/main/res/values-zh-rCN/strings.xml
+++ b/app/src/main/res/values-zh-rCN/strings.xml
@@ -26,4 +26,6 @@
关闭振动模式
开启勿扰模式
关闭勿扰模式
+ 开启自动亮度
+ 关闭自动亮度
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index fa74c48..d0fef71 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -25,4 +25,6 @@
Disable vibrate mode
Enable Do Not Disturb
Disable Do Not Disturb
+ Enable auto brightness
+ Disable auto brightness
From f4e50fa629e7571735766fb078c1625001484a83 Mon Sep 17 00:00:00 2001
From: Simon Chan <1330321+yume-chan@users.noreply.github.com>
Date: Thu, 14 May 2026 00:24:48 +0800
Subject: [PATCH 19/22] Convert BrightnessUtils to a class
---
.../chensi/volume/system/BrightnessUtils.kt | 70 +++++++------------
.../volume/system/DisplayManagerProxy.kt | 6 +-
2 files changed, 28 insertions(+), 48 deletions(-)
diff --git a/app/src/main/java/moe/chensi/volume/system/BrightnessUtils.kt b/app/src/main/java/moe/chensi/volume/system/BrightnessUtils.kt
index 7686de4..57147ef 100644
--- a/app/src/main/java/moe/chensi/volume/system/BrightnessUtils.kt
+++ b/app/src/main/java/moe/chensi/volume/system/BrightnessUtils.kt
@@ -1,15 +1,18 @@
package moe.chensi.volume.system
import android.annotation.SuppressLint
+import android.content.Context
import android.os.Build
import android.util.Log
import org.joor.Reflect
import kotlin.math.exp
import kotlin.math.ln
+import kotlin.math.pow
import kotlin.math.sqrt
+@Suppress("PropertyName")
@SuppressLint("ResourceType")
-object BrightnessUtils {
+class BrightnessUtils(context: Context) {
var R: Float = 0.5f
var A: Float = 0.17883277f
var B: Float = 0.28466892f
@@ -19,45 +22,26 @@ object BrightnessUtils {
Log.d("BrightnessUtils", "MANUFACTURER: ${Build.MANUFACTURER}")
if (Build.MANUFACTURER == "Xiaomi") {
- val app = runCatching {
- Reflect.onClass("android.app.ActivityThread")
- .call("currentApplication")
- .get()
- }.getOrNull()
- val resources = app?.let {
- runCatching {
- Reflect.on(it).call("getResources").get()
- }.getOrNull()
- }
+ val resources = context.resources
- if (resources != null) {
- val rId = resources.getIdentifier(
- "config_GammaLinearConvertRValue",
- "dimen",
- "android.miui"
- )
- val aId = resources.getIdentifier(
- "config_GammaLinearConvertAValue",
- "dimen",
- "android.miui"
- )
- val bId = resources.getIdentifier(
- "config_GammaLinearConvertBValue",
- "dimen",
- "android.miui"
- )
- val cId = resources.getIdentifier(
- "config_GammaLinearConvertCValue",
- "dimen",
- "android.miui"
- )
+ val rId = resources.getIdentifier(
+ "config_GammaLinearConvertRValue", "dimen", "android.miui"
+ )
+ val aId = resources.getIdentifier(
+ "config_GammaLinearConvertAValue", "dimen", "android.miui"
+ )
+ val bId = resources.getIdentifier(
+ "config_GammaLinearConvertBValue", "dimen", "android.miui"
+ )
+ val cId = resources.getIdentifier(
+ "config_GammaLinearConvertCValue", "dimen", "android.miui"
+ )
- if (rId != 0 && aId != 0 && bId != 0 && cId != 0) {
- R = resources.getFloat(rId)
- A = resources.getFloat(aId)
- B = resources.getFloat(bId)
- C = resources.getFloat(cId)
- }
+ if (rId != 0 && aId != 0 && bId != 0 && cId != 0) {
+ R = resources.getFloat(rId)
+ A = resources.getFloat(aId)
+ B = resources.getFloat(bId)
+ C = resources.getFloat(cId)
}
}
@@ -69,14 +53,14 @@ object BrightnessUtils {
fun convertGammaPercentageToLinear(v: Float, min: Float, max: Float): Float {
val ret: Float = if (v <= R) {
- sq(v / R)
+ (v / R).pow(2)
} else {
exp((v - C) / A) + B
}
// HLG is normalized to the range [0, 12], ensure that value is within that range,
// it shouldn't be out of bounds.
- val normalizedRet = constrain(ret, 0f, 12f)
+ val normalizedRet = ret.coerceIn(0f, 12f)
// Re-normalize to the range [0, 1]
// in order to derive the correct setting value.
@@ -93,12 +77,6 @@ object BrightnessUtils {
}
}
- private fun sq(value: Float): Float = value * value
-
- private fun constrain(value: Float, min: Float, max: Float): Float {
- return value.coerceIn(min, max)
- }
-
private fun lerp(start: Float, stop: Float, amount: Float): Float {
return start + (stop - start) * amount
}
diff --git a/app/src/main/java/moe/chensi/volume/system/DisplayManagerProxy.kt b/app/src/main/java/moe/chensi/volume/system/DisplayManagerProxy.kt
index 3228d59..3816e0e 100644
--- a/app/src/main/java/moe/chensi/volume/system/DisplayManagerProxy.kt
+++ b/app/src/main/java/moe/chensi/volume/system/DisplayManagerProxy.kt
@@ -28,6 +28,8 @@ class DisplayManagerProxy private constructor(context: Context) {
Reflect.onClass("android.hardware.display.DisplayManagerGlobal").call("getInstance")
private val displayManagerReflect = Reflect.on(displayManager)
+ private val brightnessUtils = BrightnessUtils(context)
+
init {
val service = displayManagerGlobalReflect.get("mDm")
ToggleableBinderProxy.wrap(service)
@@ -74,7 +76,7 @@ class DisplayManagerProxy private constructor(context: Context) {
"getDefaultDisplayBrightnessPercentage: ${brightnessInfo.brightness} ${brightnessInfo.brightnessMinimum} ${brightnessInfo.brightnessMaximum}"
)
- val gamma = BrightnessUtils.convertLinearToGammaPercentage(
+ val gamma = brightnessUtils.convertLinearToGammaPercentage(
brightnessInfo.brightness,
brightnessInfo.brightnessMinimum,
brightnessInfo.brightnessMaximum
@@ -97,7 +99,7 @@ class DisplayManagerProxy private constructor(context: Context) {
val brightnessInfo = getDefaultDisplayBrightnessInfo() ?: return
- val brightness = BrightnessUtils.convertGammaPercentageToLinear(
+ val brightness = brightnessUtils.convertGammaPercentageToLinear(
value, brightnessInfo.brightnessMinimum, brightnessInfo.brightnessMaximum
)
Log.d("DisplayManagerProxy", "setDefaultDisplayBrightnessPercentage: $brightness")
From e3b057c632566c4165143a67a9b9ff84bcb14d1d Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sun, 17 May 2026 14:47:18 +0000
Subject: [PATCH 20/22] Remove brightness controls while keeping system volume
controls
Agent-Logs-Url: https://github.com/yume-chan/VolumeManager/sessions/190b9278-708e-4c01-9f8e-25423bcc0e6d
Co-authored-by: yume-chan <1330321+yume-chan@users.noreply.github.com>
---
.../java/moe/chensi/volume/MainActivity.kt | 1 -
.../main/java/moe/chensi/volume/Manager.kt | 2 -
.../main/java/moe/chensi/volume/Service.kt | 1 -
.../chensi/volume/ToggleableBinderProxy.kt | 23 ---
.../chensi/volume/compose/BrightnessSlider.kt | 132 --------------
.../volume/compose/SystemVolumePanel.kt | 18 --
.../chensi/volume/system/BrightnessUtils.kt | 91 ----------
.../volume/system/DisplayManagerProxy.kt | 171 ------------------
app/src/main/res/values-zh-rCN/strings.xml | 3 -
app/src/main/res/values/strings.xml | 3 -
10 files changed, 445 deletions(-)
delete mode 100644 app/src/main/java/moe/chensi/volume/compose/BrightnessSlider.kt
delete mode 100644 app/src/main/java/moe/chensi/volume/system/BrightnessUtils.kt
delete mode 100644 app/src/main/java/moe/chensi/volume/system/DisplayManagerProxy.kt
diff --git a/app/src/main/java/moe/chensi/volume/MainActivity.kt b/app/src/main/java/moe/chensi/volume/MainActivity.kt
index e049be1..5f30d2b 100644
--- a/app/src/main/java/moe/chensi/volume/MainActivity.kt
+++ b/app/src/main/java/moe/chensi/volume/MainActivity.kt
@@ -333,7 +333,6 @@ class MainActivity : ComponentActivity() {
SystemVolumePanel(
audioManager = manager.audioManager,
notificationManagerProxy = manager.notificationManagerProxy,
- displayManagerProxy = manager.displayManagerProxy,
showCallVolumeAlways = true,
applyVisibilityFilter = false,
allowVisibilityConfig = true,
diff --git a/app/src/main/java/moe/chensi/volume/Manager.kt b/app/src/main/java/moe/chensi/volume/Manager.kt
index 76d6e7f..55c6ac0 100644
--- a/app/src/main/java/moe/chensi/volume/Manager.kt
+++ b/app/src/main/java/moe/chensi/volume/Manager.kt
@@ -15,7 +15,6 @@ import androidx.datastore.preferences.core.Preferences
import moe.chensi.volume.data.App
import moe.chensi.volume.data.AppPreferencesStore
import moe.chensi.volume.system.AudioPlaybackConfigurationProxy
-import moe.chensi.volume.system.DisplayManagerProxy
import moe.chensi.volume.system.NotificationManagerProxy
import moe.chensi.volume.system.PackageManagerProxy
import org.joor.Reflect
@@ -47,7 +46,6 @@ class Manager(context: Context, dataStore: DataStore) {
}
private val packageManager by lazy { PackageManagerProxy.get(context) }
val notificationManagerProxy = NotificationManagerProxy(context)
- val displayManagerProxy = DisplayManagerProxy(context)
private val appPreferencesStore = AppPreferencesStore(dataStore)
private val _systemSliderVisibility = mutableStateMapOf()
diff --git a/app/src/main/java/moe/chensi/volume/Service.kt b/app/src/main/java/moe/chensi/volume/Service.kt
index d70af13..3417d69 100644
--- a/app/src/main/java/moe/chensi/volume/Service.kt
+++ b/app/src/main/java/moe/chensi/volume/Service.kt
@@ -204,7 +204,6 @@ class Service : AccessibilityService() {
SystemVolumePanel(
audioManager = manager.audioManager,
notificationManagerProxy = manager.notificationManagerProxy,
- displayManagerProxy = manager.displayManagerProxy,
showCallVolumeAlways = false,
applyVisibilityFilter = true,
allowVisibilityConfig = false,
diff --git a/app/src/main/java/moe/chensi/volume/ToggleableBinderProxy.kt b/app/src/main/java/moe/chensi/volume/ToggleableBinderProxy.kt
index 5d8b3b0..33af958 100644
--- a/app/src/main/java/moe/chensi/volume/ToggleableBinderProxy.kt
+++ b/app/src/main/java/moe/chensi/volume/ToggleableBinderProxy.kt
@@ -24,7 +24,6 @@ class EnableBinderProxyCut : BasePointCut {
class ToggleableBinderProxy(private val base: IBinder) : IBinder by base {
companion object {
- private const val SHELL_PACKAGE_NAME = "com.android.shell"
private val _enabled: ThreadLocal = ThreadLocal.withInitial { false }
var enabled: Boolean
@@ -33,36 +32,14 @@ class ToggleableBinderProxy(private val base: IBinder) : IBinder by base {
fun withEnabled(block: () -> T): T {
val prev = enabled
- val restoreActivityThreadPackageName = patchActivityThreadPackageName()
try {
enabled = true
return block()
} finally {
- restoreActivityThreadPackageName()
enabled = prev
}
}
- private fun patchActivityThreadPackageName(): () -> Unit {
- val field = runCatching {
- Class.forName("android.app.ActivityThread").getDeclaredField("sCurrentPackageName")
- .apply { isAccessible = true }
- }.getOrNull() ?: return {}
-
- val holder = runCatching { field.get(null) }.getOrNull()
- if (holder is ThreadLocal<*>) {
- @Suppress("UNCHECKED_CAST")
- val packageNameHolder = holder as ThreadLocal
- val previous = packageNameHolder.get()
- packageNameHolder.set(SHELL_PACKAGE_NAME)
- return { packageNameHolder.set(previous) }
- }
-
- val previous = holder
- runCatching { field.set(null, SHELL_PACKAGE_NAME) }
- return { runCatching { field.set(null, previous) } }
- }
-
fun wrap(proxy: Any) {
Reflect.on(proxy).apply {
set("mRemote", get("mRemote").run {
diff --git a/app/src/main/java/moe/chensi/volume/compose/BrightnessSlider.kt b/app/src/main/java/moe/chensi/volume/compose/BrightnessSlider.kt
deleted file mode 100644
index 53aeb94..0000000
--- a/app/src/main/java/moe/chensi/volume/compose/BrightnessSlider.kt
+++ /dev/null
@@ -1,132 +0,0 @@
-package moe.chensi.volume.compose
-
-import android.hardware.display.DisplayManager
-import android.os.Handler
-import android.os.Looper
-import android.util.Log
-import android.view.Display
-import androidx.compose.foundation.layout.Arrangement
-import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.size
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.filled.BrightnessAuto
-import androidx.compose.material.icons.filled.Brightness6
-import androidx.compose.material3.Icon
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.DisposableEffect
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableFloatStateOf
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.setValue
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.unit.dp
-import moe.chensi.volume.R
-import moe.chensi.volume.system.DisplayManagerProxy
-import kotlin.math.abs
-import kotlin.math.roundToInt
-
-private const val BRIGHTNESS_CHANGE_TOLERANCE = 0.001f
-
-@Composable
-fun BrightnessSlider(
- displayManagerProxy: DisplayManagerProxy,
- footer: (@Composable () -> Unit)? = null,
- onChange: (() -> Unit)? = null
-) {
- var brightnessPercent by remember { mutableFloatStateOf(displayManagerProxy.getDefaultDisplayBrightnessGammaPercentage()) }
- var autoBrightnessEnabled by remember { mutableStateOf(displayManagerProxy.isAutoBrightnessEnabled()) }
- var autoBrightnessBias by remember { mutableFloatStateOf(displayManagerProxy.getAutoBrightnessBias()) }
- val mainThreadHandler = remember { Handler(Looper.getMainLooper()) }
-
- DisposableEffect(displayManagerProxy) {
-
- val listener = object : DisplayManager.DisplayListener {
- override fun onDisplayAdded(displayId: Int) = Unit
-
- override fun onDisplayRemoved(displayId: Int) = Unit
-
- override fun onDisplayChanged(displayId: Int) {
- Log.d("BrightnessSlider", "onDisplayChanged: $displayId")
- if (displayId == Display.DEFAULT_DISPLAY) {
- brightnessPercent = displayManagerProxy.getDefaultDisplayBrightnessGammaPercentage()
- autoBrightnessEnabled = displayManagerProxy.isAutoBrightnessEnabled()
- autoBrightnessBias = displayManagerProxy.getAutoBrightnessBias()
- }
- }
- }
-
- displayManagerProxy.registerDisplayListener(listener, mainThreadHandler)
-
- onDispose {
- displayManagerProxy.unregisterDisplayListener(listener)
- }
- }
-
- Row(
- horizontalArrangement = Arrangement.spacedBy(4.dp),
- verticalAlignment = Alignment.CenterVertically
- ) {
- TrackSlider(
- modifier = Modifier.weight(1f),
- cornerRadius = 20.dp,
- value = if (autoBrightnessEnabled) autoBrightnessBias else brightnessPercent,
- valueRange = if (autoBrightnessEnabled) -1f..1f else 0f..1f,
- onValueChange = { value ->
- if (autoBrightnessEnabled && abs(autoBrightnessBias - value) < BRIGHTNESS_CHANGE_TOLERANCE) {
- return@TrackSlider
- }
- if (!autoBrightnessEnabled && abs(brightnessPercent - value) < BRIGHTNESS_CHANGE_TOLERANCE) {
- return@TrackSlider
- }
-
- if (autoBrightnessEnabled) {
- autoBrightnessBias = value
- displayManagerProxy.setAutoBrightnessBias(value)
- } else {
- brightnessPercent = value
- displayManagerProxy.setDefaultDisplayBrightnessGammaPercentage(value)
- }
- onChange?.invoke()
- }
- ) {
- Row(
- horizontalArrangement = Arrangement.spacedBy(8.dp),
- verticalAlignment = Alignment.CenterVertically,
- modifier = Modifier.padding(12.dp, 8.dp)
- ) {
- Icon(
- imageVector = Icons.Default.Brightness6,
- contentDescription = stringResource(R.string.brightness),
- modifier = Modifier.size(32.dp),
- )
- StreamSliderTextContent(
- name = stringResource(R.string.brightness),
- valueText = if (autoBrightnessEnabled) {
- "${(autoBrightnessBias * 100).roundToInt()}%"
- } else {
- "${(brightnessPercent * 100).roundToInt()}%"
- }
- )
- }
- }
-
- ToggleButton(
- checked = autoBrightnessEnabled,
- checkedDescription = stringResource(R.string.disable_auto_brightness),
- checkedIcon = Icons.Default.BrightnessAuto,
- uncheckedDescription = stringResource(R.string.enable_auto_brightness),
- uncheckedIcon = Icons.Default.Brightness6
- ) {
- displayManagerProxy.setAutoBrightnessEnabled(it)
- autoBrightnessEnabled = displayManagerProxy.isAutoBrightnessEnabled()
- autoBrightnessBias = displayManagerProxy.getAutoBrightnessBias()
- onChange?.invoke()
- }
-
- footer?.invoke()
- }
-}
diff --git a/app/src/main/java/moe/chensi/volume/compose/SystemVolumePanel.kt b/app/src/main/java/moe/chensi/volume/compose/SystemVolumePanel.kt
index ea790ea..1dc176b 100644
--- a/app/src/main/java/moe/chensi/volume/compose/SystemVolumePanel.kt
+++ b/app/src/main/java/moe/chensi/volume/compose/SystemVolumePanel.kt
@@ -36,7 +36,6 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.core.content.ContextCompat
import moe.chensi.volume.R
-import moe.chensi.volume.system.DisplayManagerProxy
import moe.chensi.volume.system.NotificationManagerProxy
object SystemSliderIds {
@@ -45,7 +44,6 @@ object SystemSliderIds {
const val Call = "call"
const val Alarm = "alarm"
const val Notification = "notification"
- const val Brightness = "brightness"
}
private fun isCallMode(mode: Int): Boolean {
@@ -57,7 +55,6 @@ private fun isCallMode(mode: Int): Boolean {
fun SystemVolumePanel(
audioManager: AudioManager,
notificationManagerProxy: NotificationManagerProxy,
- displayManagerProxy: DisplayManagerProxy,
showCallVolumeAlways: Boolean,
applyVisibilityFilter: Boolean,
allowVisibilityConfig: Boolean,
@@ -182,21 +179,6 @@ fun SystemVolumePanel(
)
}
- if (!applyVisibilityFilter || isSliderVisible(SystemSliderIds.Brightness)) {
- BrightnessSlider(
- displayManagerProxy = displayManagerProxy,
- footer = {
- SliderVisibilityToggle(
- sliderId = SystemSliderIds.Brightness,
- sliderName = stringResource(R.string.brightness),
- allowVisibilityConfig = allowVisibilityConfig,
- isVisible = isSliderVisible(SystemSliderIds.Brightness),
- onSliderVisibilityChange = onSliderVisibilityChange
- )
- },
- onChange = onChange
- )
- }
}
}
diff --git a/app/src/main/java/moe/chensi/volume/system/BrightnessUtils.kt b/app/src/main/java/moe/chensi/volume/system/BrightnessUtils.kt
deleted file mode 100644
index 57147ef..0000000
--- a/app/src/main/java/moe/chensi/volume/system/BrightnessUtils.kt
+++ /dev/null
@@ -1,91 +0,0 @@
-package moe.chensi.volume.system
-
-import android.annotation.SuppressLint
-import android.content.Context
-import android.os.Build
-import android.util.Log
-import org.joor.Reflect
-import kotlin.math.exp
-import kotlin.math.ln
-import kotlin.math.pow
-import kotlin.math.sqrt
-
-@Suppress("PropertyName")
-@SuppressLint("ResourceType")
-class BrightnessUtils(context: Context) {
- var R: Float = 0.5f
- var A: Float = 0.17883277f
- var B: Float = 0.28466892f
- var C: Float = 0.5599107f
-
- init {
- Log.d("BrightnessUtils", "MANUFACTURER: ${Build.MANUFACTURER}")
-
- if (Build.MANUFACTURER == "Xiaomi") {
- val resources = context.resources
-
- val rId = resources.getIdentifier(
- "config_GammaLinearConvertRValue", "dimen", "android.miui"
- )
- val aId = resources.getIdentifier(
- "config_GammaLinearConvertAValue", "dimen", "android.miui"
- )
- val bId = resources.getIdentifier(
- "config_GammaLinearConvertBValue", "dimen", "android.miui"
- )
- val cId = resources.getIdentifier(
- "config_GammaLinearConvertCValue", "dimen", "android.miui"
- )
-
- if (rId != 0 && aId != 0 && bId != 0 && cId != 0) {
- R = resources.getFloat(rId)
- A = resources.getFloat(aId)
- B = resources.getFloat(bId)
- C = resources.getFloat(cId)
- }
- }
-
- Log.d("BrightnessUtils", "R: $R")
- Log.d("BrightnessUtils", "A: $A")
- Log.d("BrightnessUtils", "B: $B")
- Log.d("BrightnessUtils", "C: $C")
- }
-
- fun convertGammaPercentageToLinear(v: Float, min: Float, max: Float): Float {
- val ret: Float = if (v <= R) {
- (v / R).pow(2)
- } else {
- exp((v - C) / A) + B
- }
-
- // HLG is normalized to the range [0, 12], ensure that value is within that range,
- // it shouldn't be out of bounds.
- val normalizedRet = ret.coerceIn(0f, 12f)
-
- // Re-normalize to the range [0, 1]
- // in order to derive the correct setting value.
- return lerp(min, max, normalizedRet / 12)
- }
-
- fun convertLinearToGammaPercentage(v: Float, min: Float, max: Float): Float {
- // For some reason, HLG normalizes to the range [0, 12] rather than [0, 1]
- val normalizedVal: Float = norm(min, max, v) * 12
- return if (normalizedVal <= 1f) {
- sqrt(normalizedVal) * R
- } else {
- A * ln(normalizedVal - B) + C
- }
- }
-
- private fun lerp(start: Float, stop: Float, amount: Float): Float {
- return start + (stop - start) * amount
- }
-
- private fun norm(start: Float, stop: Float, value: Float): Float {
- val range = stop - start
- if (range == 0f) {
- return 0f
- }
- return (value - start) / range
- }
-}
diff --git a/app/src/main/java/moe/chensi/volume/system/DisplayManagerProxy.kt b/app/src/main/java/moe/chensi/volume/system/DisplayManagerProxy.kt
deleted file mode 100644
index 3816e0e..0000000
--- a/app/src/main/java/moe/chensi/volume/system/DisplayManagerProxy.kt
+++ /dev/null
@@ -1,171 +0,0 @@
-package moe.chensi.volume.system
-
-import android.annotation.SuppressLint
-import android.content.Context
-import android.hardware.display.DisplayManager
-import android.os.Handler
-import android.provider.Settings
-import android.util.Log
-import android.view.Display
-import moe.chensi.volume.EnableBinderProxy
-import moe.chensi.volume.ToggleableBinderProxy
-import org.joor.Reflect
-import java.util.WeakHashMap
-
-class DisplayManagerProxy private constructor(context: Context) {
- companion object {
- private const val AUTO_BRIGHTNESS_ADJ_KEY = "screen_auto_brightness_adj"
- private val cache = WeakHashMap()
-
- operator fun invoke(context: Context): DisplayManagerProxy {
- return cache.getOrPut(context) { DisplayManagerProxy(context) }
- }
- }
-
- private val displayManager = context.getSystemService(DisplayManager::class.java)!!
- private val contentResolver = context.contentResolver
- private val displayManagerGlobalReflect =
- Reflect.onClass("android.hardware.display.DisplayManagerGlobal").call("getInstance")
- private val displayManagerReflect = Reflect.on(displayManager)
-
- private val brightnessUtils = BrightnessUtils(context)
-
- init {
- val service = displayManagerGlobalReflect.get("mDm")
- ToggleableBinderProxy.wrap(service)
- }
-
- private data class BrightnessInfoValue(
- val brightness: Float,
- val brightnessMinimum: Float,
- val brightnessMaximum: Float
- )
-
- @EnableBinderProxy
- private fun getDefaultDisplayBrightnessInfo(): BrightnessInfoValue? {
- val display = runCatching {
- displayManagerReflect.call("getDisplay", Display.DEFAULT_DISPLAY).get()
- }.getOrNull() ?: return null
-
- val brightnessInfo = runCatching {
- Reflect.on(display).call("getBrightnessInfo").get()
- }.getOrNull() ?: return null
-
- return runCatching {
- BrightnessInfoValue(
- brightness = Reflect.on(brightnessInfo).get("brightness"),
- brightnessMinimum = Reflect.on(brightnessInfo).get("brightnessMinimum"),
- brightnessMaximum = Reflect.on(brightnessInfo).get("brightnessMaximum")
- )
- }.getOrNull()
- }
-
- @SuppressLint("MissingPermission")
- @EnableBinderProxy
- fun getDefaultDisplayBrightnessGammaPercentage(): Float {
- val brightnessInfo = getDefaultDisplayBrightnessInfo()
- if (brightnessInfo == null) {
- Log.w(
- "DisplayManagerProxy",
- "getDefaultDisplayBrightnessPercentage: brightnessInfo is null"
- )
- return 0f
- }
- Log.d(
- "DisplayManagerProxy",
- "getDefaultDisplayBrightnessPercentage: ${brightnessInfo.brightness} ${brightnessInfo.brightnessMinimum} ${brightnessInfo.brightnessMaximum}"
- )
-
- val gamma = brightnessUtils.convertLinearToGammaPercentage(
- brightnessInfo.brightness,
- brightnessInfo.brightnessMinimum,
- brightnessInfo.brightnessMaximum
- )
- Log.d("DisplayManagerProxy", "getDefaultDisplayBrightnessPercentage: $gamma")
-
- return gamma
- }
-
- /**
- * Sets default display brightness.
- *
- * @param value Brightness in [0f, 1f].
- * Uses reflection to call DisplayManager hidden setBrightness API.
- */
- @SuppressLint("MissingPermission")
- @EnableBinderProxy
- fun setDefaultDisplayBrightnessGammaPercentage(value: Float) {
- Log.d("DisplayManagerProxy", "setDefaultDisplayBrightnessPercentage: $value")
-
- val brightnessInfo = getDefaultDisplayBrightnessInfo() ?: return
-
- val brightness = brightnessUtils.convertGammaPercentageToLinear(
- value, brightnessInfo.brightnessMinimum, brightnessInfo.brightnessMaximum
- )
- Log.d("DisplayManagerProxy", "setDefaultDisplayBrightnessPercentage: $brightness")
-
- displayManagerReflect.call("setBrightness", Display.DEFAULT_DISPLAY, brightness)
- }
-
- @EnableBinderProxy
- fun isAutoBrightnessEnabled(): Boolean {
- val mode = Settings.System.getInt(
- contentResolver,
- Settings.System.SCREEN_BRIGHTNESS_MODE,
- Settings.System.SCREEN_BRIGHTNESS_MODE_MANUAL
- )
- return mode == Settings.System.SCREEN_BRIGHTNESS_MODE_AUTOMATIC
- }
-
- @EnableBinderProxy
- fun setAutoBrightnessEnabled(enabled: Boolean) {
- val mode = if (enabled) {
- Settings.System.SCREEN_BRIGHTNESS_MODE_AUTOMATIC
- } else {
- Settings.System.SCREEN_BRIGHTNESS_MODE_MANUAL
- }
-
- val updated = Settings.System.putInt(
- contentResolver,
- Settings.System.SCREEN_BRIGHTNESS_MODE,
- mode
- )
- if (!updated) {
- Log.w("DisplayManagerProxy", "setAutoBrightnessEnabled failed")
- }
- }
-
- @EnableBinderProxy
- fun getAutoBrightnessBias(): Float {
- return Settings.System.getFloat(
- contentResolver,
- AUTO_BRIGHTNESS_ADJ_KEY,
- 0f
- ).coerceIn(-1f, 1f)
- }
-
- @EnableBinderProxy
- fun setAutoBrightnessBias(value: Float) {
- val adjusted = value.coerceIn(-1f, 1f)
- displayManagerReflect.call("setTemporaryAutoBrightnessAdjustment", adjusted)
-
- val updated = Settings.System.putFloat(
- contentResolver,
- AUTO_BRIGHTNESS_ADJ_KEY,
- adjusted
- )
- if (!updated) {
- Log.w("DisplayManagerProxy", "setAutoBrightnessBias failed")
- }
- }
-
- @EnableBinderProxy
- fun registerDisplayListener(listener: DisplayManager.DisplayListener, handler: Handler?) {
- displayManager.registerDisplayListener(listener, handler)
- }
-
- @EnableBinderProxy
- fun unregisterDisplayListener(listener: DisplayManager.DisplayListener) {
- displayManager.unregisterDisplayListener(listener)
- }
-}
diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml
index caf076b..df7dc73 100644
--- a/app/src/main/res/values-zh-rCN/strings.xml
+++ b/app/src/main/res/values-zh-rCN/strings.xml
@@ -19,13 +19,10 @@
通话
闹钟
通知
- 亮度
显示%1$s滑块
隐藏%1$s滑块
开启振动模式
关闭振动模式
开启勿扰模式
关闭勿扰模式
- 开启自动亮度
- 关闭自动亮度
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index d0fef71..9bc6fa8 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -18,13 +18,10 @@
Call
Alarm
Notification
- Brightness
Show %1$s slider
Hide %1$s slider
Enable vibrate mode
Disable vibrate mode
Enable Do Not Disturb
Disable Do Not Disturb
- Enable auto brightness
- Disable auto brightness
From 87e7f00e063c33be4851f1b3c76bf21aebdc28c9 Mon Sep 17 00:00:00 2001
From: Simon Chan <1330321+yume-chan@users.noreply.github.com>
Date: Sun, 17 May 2026 23:28:45 +0800
Subject: [PATCH 21/22] Change action bar icon to settings
---
.../java/moe/chensi/volume/MainActivity.kt | 21 ++++++-------------
1 file changed, 6 insertions(+), 15 deletions(-)
diff --git a/app/src/main/java/moe/chensi/volume/MainActivity.kt b/app/src/main/java/moe/chensi/volume/MainActivity.kt
index 5f30d2b..1ee631b 100644
--- a/app/src/main/java/moe/chensi/volume/MainActivity.kt
+++ b/app/src/main/java/moe/chensi/volume/MainActivity.kt
@@ -25,9 +25,9 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.BugReport
+import androidx.compose.material.icons.filled.Check
import androidx.compose.material.icons.filled.Info
-import androidx.compose.material.icons.filled.Visibility
-import androidx.compose.material.icons.filled.VisibilityOff
+import androidx.compose.material.icons.filled.Settings
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.ExperimentalMaterial3Api
@@ -198,10 +198,10 @@ class MainActivity : ComponentActivity() {
if (manager.shizukuStatus == Manager.ShizukuStatus.Connected) {
ToggleButton(
checked = showAll,
- checkedIcon = Icons.Default.Visibility,
- checkedDescription = "Hide inactive or hidden apps",
- uncheckedIcon = Icons.Default.VisibilityOff,
- uncheckedDescription = "Show all apps"
+ checkedIcon = Icons.Default.Check,
+ checkedDescription = "Save",
+ uncheckedIcon = Icons.Default.Settings,
+ uncheckedDescription = "Settings"
) {
showAll = it
}
@@ -370,14 +370,11 @@ class MainActivity : ComponentActivity() {
@Composable
fun ServiceStatus() {
- var permissionGranted by remember { mutableStateOf(false) }
- var serviceEnabled by remember { mutableStateOf(false) }
var errorInfo by remember { mutableStateOf(null) }
LaunchedEffect(0) {
try {
grantSelfPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS)
- permissionGranted = true
} catch (e: Exception) {
Log.e(TAG, "Can't add WRITE_SECURE_SETTINGS permission", e)
errorInfo = ErrorInfo(e.message!!, e.stackTraceToString())
@@ -388,7 +385,6 @@ class MainActivity : ComponentActivity() {
enableAccessibilityService(
ComponentName(this@MainActivity, Service::class.java).flattenToString()
)
- serviceEnabled = true
} catch (e: Exception) {
Log.e(TAG, "Can't enable accessibility service", e)
}
@@ -418,11 +414,6 @@ class MainActivity : ComponentActivity() {
})
}
- Column {
- Text(text = "Permission granted: ${if (permissionGranted) "Yes" else "No"}")
- Text(text = "Service enabled: ${if (serviceEnabled) "Yes" else "No"}")
- }
-
Log.i(TAG, "Manufacturer: ${Build.MANUFACTURER}")
if (!isIgnoringBatteryOptimization) {
From b953389dd6b65028133400e0691725d862163da0 Mon Sep 17 00:00:00 2001
From: Simon Chan <1330321+yume-chan@users.noreply.github.com>
Date: Sun, 17 May 2026 23:50:41 +0800
Subject: [PATCH 22/22] Polish the system volume sliders
---
.../main/java/moe/chensi/volume/MainActivity.kt | 4 ++--
app/src/main/java/moe/chensi/volume/Service.kt | 15 ++++++---------
.../chensi/volume/compose/StreamVolumeSlider.kt | 2 +-
3 files changed, 9 insertions(+), 12 deletions(-)
diff --git a/app/src/main/java/moe/chensi/volume/MainActivity.kt b/app/src/main/java/moe/chensi/volume/MainActivity.kt
index 1ee631b..80762c3 100644
--- a/app/src/main/java/moe/chensi/volume/MainActivity.kt
+++ b/app/src/main/java/moe/chensi/volume/MainActivity.kt
@@ -334,8 +334,8 @@ class MainActivity : ComponentActivity() {
audioManager = manager.audioManager,
notificationManagerProxy = manager.notificationManagerProxy,
showCallVolumeAlways = true,
- applyVisibilityFilter = false,
- allowVisibilityConfig = true,
+ applyVisibilityFilter = !showAll,
+ allowVisibilityConfig = showAll,
isSliderVisible = manager::isSystemSliderVisible,
onSliderVisibilityChange = manager::setSystemSliderVisible,
)
diff --git a/app/src/main/java/moe/chensi/volume/Service.kt b/app/src/main/java/moe/chensi/volume/Service.kt
index 3417d69..dfe0a8b 100644
--- a/app/src/main/java/moe/chensi/volume/Service.kt
+++ b/app/src/main/java/moe/chensi/volume/Service.kt
@@ -28,6 +28,7 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
@@ -157,8 +158,7 @@ class Service : AccessibilityService() {
Log.i(TAG, "onAttachedToWindow manufacturer: ${Build.MANUFACTURER}")
- @Suppress("SpellCheckingInspection")
- if (windowManager.isCrossWindowBlurEnabled && isHardwareAccelerated && Build.MANUFACTURER != "realme") {
+ @Suppress("SpellCheckingInspection") if (windowManager.isCrossWindowBlurEnabled && isHardwareAccelerated && Build.MANUFACTURER != "realme") {
background =
Reflect.on(rootSurfaceControl).call("createBackgroundBlurDrawable").apply {
call("setBlurRadius", 200)
@@ -185,15 +185,12 @@ class Service : AccessibilityService() {
override fun Content() {
return VolumeManagerTheme {
Surface(
- color = Color.Transparent,
- contentColor = Color.White,
+ color = Color(1f, 1f, 1f, 0.3f),
+ contentColor = MaterialTheme.colorScheme.onSurface,
+ shape = RoundedCornerShape(40f)
) {
Column(
- modifier = Modifier
- .background(
- Color(1f, 1f, 1f, 0.3f), RoundedCornerShape(40f)
- )
- .padding(20.dp, 16.dp)
+ modifier = Modifier.padding(20.dp, 16.dp)
) {
AppVolumeList(
apps = manager.apps.values,
diff --git a/app/src/main/java/moe/chensi/volume/compose/StreamVolumeSlider.kt b/app/src/main/java/moe/chensi/volume/compose/StreamVolumeSlider.kt
index b21bb4e..c36abb6 100644
--- a/app/src/main/java/moe/chensi/volume/compose/StreamVolumeSlider.kt
+++ b/app/src/main/java/moe/chensi/volume/compose/StreamVolumeSlider.kt
@@ -128,7 +128,7 @@ fun StreamVolumeSlider(
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = Alignment.CenterVertically,
- modifier = Modifier.padding(12.dp, 8.dp)
+ modifier = Modifier.padding(16.dp, 8.dp)
) {
Icon(
imageVector = icon,