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/java/moe/chensi/volume/MainActivity.kt b/app/src/main/java/moe/chensi/volume/MainActivity.kt index 67fd1f9..80762c3 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 @@ -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 @@ -197,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 } @@ -326,7 +327,20 @@ 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, + notificationManagerProxy = manager.notificationManagerProxy, + showCallVolumeAlways = true, + applyVisibilityFilter = !showAll, + allowVisibilityConfig = showAll, + isSliderVisible = manager::isSystemSliderVisible, + onSliderVisibilityChange = manager::setSystemSliderVisible, + ) + } + }) } } } @@ -356,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()) @@ -374,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) } @@ -404,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) { diff --git a/app/src/main/java/moe/chensi/volume/Manager.kt b/app/src/main/java/moe/chensi/volume/Manager.kt index d1fb18f..55c6ac0 100644 --- a/app/src/main/java/moe/chensi/volume/Manager.kt +++ b/app/src/main/java/moe/chensi/volume/Manager.kt @@ -15,6 +15,7 @@ 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.NotificationManagerProxy import moe.chensi.volume.system.PackageManagerProxy import org.joor.Reflect import rikka.shizuku.Shizuku @@ -44,8 +45,25 @@ class Manager(context: Context, dataStore: DataStore) { .apply { ToggleableBinderProxy.wrap(this) } } private val packageManager by lazy { PackageManagerProxy.get(context) } + val notificationManagerProxy = NotificationManagerProxy(context) private val appPreferencesStore = AppPreferencesStore(dataStore) + private val _systemSliderVisibility = mutableStateMapOf() + val systemSliderVisibility: Map + get() = _systemSliderVisibility + + fun isSystemSliderVisible(id: String): Boolean { + return _systemSliderVisibility[id] ?: true + } + + fun setSystemSliderVisible(id: String, visible: Boolean) { + if ((_systemSliderVisibility[id] ?: true) == visible) { + return + } + + _systemSliderVisibility[id] = visible + appPreferencesStore.setSystemSliderVisible(id, visible) + } val apps = mutableStateMapOf() @@ -162,6 +180,9 @@ class Manager(context: Context, dataStore: DataStore) { } } + _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 5478eee..dfe0a8b 100644 --- a/app/src/main/java/moe/chensi/volume/Service.kt +++ b/app/src/main/java/moe/chensi/volume/Service.kt @@ -28,9 +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.material.icons.Icons -import androidx.compose.material.icons.filled.MusicNote -import androidx.compose.material.icons.filled.Notifications +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier @@ -45,7 +43,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 @@ -160,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) @@ -188,37 +185,27 @@ 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, 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", - manager.audioManager, + item("system_volume_panel") { + SystemVolumePanel( + audioManager = manager.audioManager, + notificationManagerProxy = manager.notificationManagerProxy, + showCallVolumeAlways = false, + applyVisibilityFilter = true, + allowVisibilityConfig = false, + isSliderVisible = manager::isSystemSliderVisible, + onSliderVisibilityChange = manager::setSystemSliderVisible, onChange = this@Service.handler::startIdleTimer ) } diff --git a/app/src/main/java/moe/chensi/volume/ToggleableBinderProxy.kt b/app/src/main/java/moe/chensi/volume/ToggleableBinderProxy.kt index 8225ace..33af958 100644 --- a/app/src/main/java/moe/chensi/volume/ToggleableBinderProxy.kt +++ b/app/src/main/java/moe/chensi/volume/ToggleableBinderProxy.kt @@ -69,4 +69,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/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 cdb0a0f..c36abb6 100644 --- a/app/src/main/java/moe/chensi/volume/compose/StreamVolumeSlider.kt +++ b/app/src/main/java/moe/chensi/volume/compose/StreamVolumeSlider.kt @@ -8,6 +8,7 @@ import android.content.IntentFilter import android.media.AudioManager 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.material3.ExperimentalMaterial3Api @@ -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,56 @@ 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(4.dp), + verticalAlignment = Alignment.CenterVertically ) { - 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 - ) + TrackSlider( + modifier = Modifier.weight(1f), + cornerRadius = 20.dp, + value = volume.toFloat(), + valueRange = 0f..maxVolume, + onValueChange = { value -> + val target = value.toInt() + if (volume == target) { + return@TrackSlider + } - Text( - text = "$volume/${maxVolume.toInt()}", - style = Typography.bodySmall, - maxLines = 1, - ) + volume = target + audioManager.setStreamVolume(streamType, target, 0) + onChange?.invoke() + }, + ) { + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.padding(16.dp, 8.dp) + ) { + Icon( + imageVector = icon, + contentDescription = name, + modifier = Modifier.size(32.dp), + ) + StreamSliderTextContent(name = name, valueText = "$volume/${maxVolume.toInt()}") + } } + + footer?.invoke() } } + +@Composable +internal fun RowScope.StreamSliderTextContent(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 new file mode 100644 index 0000000..1dc176b --- /dev/null +++ b/app/src/main/java/moe/chensi/volume/compose/SystemVolumePanel.kt @@ -0,0 +1,302 @@ +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.media.AudioManager +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Alarm +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 +import androidx.compose.material.icons.filled.VolumeUp +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +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.Alignment +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 moe.chensi.volume.system.NotificationManagerProxy + +object SystemSliderIds { + const val Media = "media" + const val Ring = "ring" + const val Call = "call" + const val Alarm = "alarm" + const val Notification = "notification" +} + +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, + notificationManagerProxy: NotificationManagerProxy, + showCallVolumeAlways: Boolean, + 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)) } + + DisposableEffect(audioManager, showCallVolumeAlways) { + if (showCallVolumeAlways) { + return@DisposableEffect onDispose { } + } + + val listener = AudioManager.OnModeChangedListener { mode -> + inCallMode = isCallMode(mode) + } + audioManager.addOnModeChangedListener(executor, listener) + onDispose { + audioManager.removeOnModeChangedListener(listener) + } + } + + Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { + 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 (!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 (!applyVisibilityFilter || isSliderVisible(SystemSliderIds.Ring)) { + StreamVolumeSlider( + 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 + ) + } + + if (!applyVisibilityFilter || isSliderVisible(SystemSliderIds.Alarm)) { + StreamVolumeSlider( + 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 + ) + } + + 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 + ) + } + + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun RingFooter( + audioManager: AudioManager, + notificationManagerProxy: NotificationManagerProxy, + sliderVisible: Boolean, + allowVisibilityConfig: Boolean, + onSliderVisibilityChange: (String, Boolean) -> Unit, + onChange: (() -> Unit)? = null +) { + val context = LocalContext.current + var ringerMode by remember { mutableIntStateOf(audioManager.ringerMode) } + var interruptionFilter by remember { mutableIntStateOf(notificationManagerProxy.getCurrentInterruptionFilter()) } + + DisposableEffect(context) { + val receiver = object : BroadcastReceiver() { + override fun onReceive(context: Context?, intent: Intent?) { + ringerMode = audioManager.ringerMode + interruptionFilter = notificationManagerProxy.getCurrentInterruptionFilter() + } + } + + 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), + verticalAlignment = Alignment.CenterVertically + ) { + 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 + ) { + 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 + } + + 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) + } +} 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..b5816aa 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 systemSliderVisibility: MutableMap = mutableMapOf() ) private val lock = Any() @@ -30,6 +32,44 @@ class AppPreferencesStore(private val dataStore: DataStore) { get() = state.values val indices: Map get() = synchronized(lock) { state.indices.toMap() } + 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.systemSliderVisibility == value) { + return@synchronized false + } + + state = state.copy(systemSliderVisibility = value.toMutableMap()) + true + } + + if (changed) { + save() + } + } fun track(onChange: (first: Boolean) -> Unit) { var first = true @@ -71,4 +111,4 @@ class AppPreferencesStore(private val dataStore: DataStore) { } } } -} \ No newline at end of file +} 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 2f4aada..df7dc73 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -14,4 +14,15 @@ 作者:%s 在 GitHub 上查看 跳转到分组 - \ No newline at end of file + 媒体 + 铃声 + 通话 + 闹钟 + 通知 + 显示%1$s滑块 + 隐藏%1$s滑块 + 开启振动模式 + 关闭振动模式 + 开启勿扰模式 + 关闭勿扰模式 + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 68d0462..9bc6fa8 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -13,4 +13,15 @@ Author: %s View on GitHub Jump to group - \ No newline at end of file + Media + Ring + Call + Alarm + Notification + Show %1$s slider + Hide %1$s slider + Enable vibrate mode + Disable vibrate mode + Enable Do Not Disturb + Disable Do Not Disturb +