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,