From e1c66777539aa59ee008b59c1d232ce18428889e Mon Sep 17 00:00:00 2001 From: Kavish Devar Date: Fri, 17 Jan 2025 03:20:34 +0530 Subject: [PATCH] add ui for accessibility settings --- .../aln/composables/AccessibilitySettings.kt | 108 ++++++- .../aln/composables/TransparencySettings.kt | 270 ++++++++++++++++++ .../aln/services/AirPodsService.kt | 69 +---- 3 files changed, 383 insertions(+), 64 deletions(-) create mode 100644 android/app/src/main/java/me/kavishdevar/aln/composables/TransparencySettings.kt diff --git a/android/app/src/main/java/me/kavishdevar/aln/composables/AccessibilitySettings.kt b/android/app/src/main/java/me/kavishdevar/aln/composables/AccessibilitySettings.kt index 1ec51dc..03da60a 100644 --- a/android/app/src/main/java/me/kavishdevar/aln/composables/AccessibilitySettings.kt +++ b/android/app/src/main/java/me/kavishdevar/aln/composables/AccessibilitySettings.kt @@ -21,13 +21,21 @@ package me.kavishdevar.aln.composables import android.content.Context import android.content.SharedPreferences import androidx.compose.foundation.background +import androidx.compose.foundation.clickable import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext @@ -44,6 +52,7 @@ import me.kavishdevar.aln.services.AirPodsService fun AccessibilitySettings(service: AirPodsService, sharedPreferences: SharedPreferences) { val isDarkTheme = isSystemInDarkTheme() val textColor = if (isDarkTheme) Color.White else Color.Black + val backgroundColor = if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFFFFFFF) Text( text = stringResource(R.string.accessibility).uppercase(), @@ -55,15 +64,12 @@ fun AccessibilitySettings(service: AirPodsService, sharedPreferences: SharedPref modifier = Modifier.padding(8.dp, bottom = 2.dp) ) - val backgroundColor = if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFFFFFFF) - Column( modifier = Modifier .fillMaxWidth() .background(backgroundColor, RoundedCornerShape(14.dp)) .padding(top = 2.dp) ) { - Column( modifier = Modifier .fillMaxWidth() @@ -84,12 +90,102 @@ fun AccessibilitySettings(service: AirPodsService, sharedPreferences: SharedPref ToneVolumeSlider(service = service, sharedPreferences = sharedPreferences) } -// TODO: Dropdown menu with 3 options, Default, Slower, Slowest – Press speed -// TODO: Dropdown menu with 3 options, Default, Slower, Slowest – Press and hold duration -// TODO: Dropdown menu with 3 options, Default, Slower, Slowest – Volume Swipe Speed + val pressSpeedOptions = listOf("Default", "Slower", "Slowest") + var selectedPressSpeed by remember { mutableStateOf(pressSpeedOptions[0]) } + DropdownMenuComponent( + label = "Press Speed", + options = pressSpeedOptions, + selectedOption = selectedPressSpeed, + onOptionSelected = { + selectedPressSpeed = it + service.setPressSpeed(pressSpeedOptions.indexOf(it)) + }, + textColor = textColor + ) + + val pressAndHoldDurationOptions = listOf("Default", "Slower", "Slowest") + var selectedPressAndHoldDuration by remember { mutableStateOf(pressAndHoldDurationOptions[0]) } + DropdownMenuComponent( + label = "Press and Hold Duration", + options = pressAndHoldDurationOptions, + selectedOption = selectedPressAndHoldDuration, + onOptionSelected = { + selectedPressAndHoldDuration = it + service.setPressAndHoldDuration(pressAndHoldDurationOptions.indexOf(it)) + }, + textColor = textColor + ) + + val volumeSwipeSpeedOptions = listOf("Default", "Longer", "Longest") + var selectedVolumeSwipeSpeed by remember { mutableStateOf(volumeSwipeSpeedOptions[0]) } + DropdownMenuComponent( + label = "Volume Swipe Speed", + options = volumeSwipeSpeedOptions, + selectedOption = selectedVolumeSwipeSpeed, + onOptionSelected = { + selectedVolumeSwipeSpeed = it + service.setVolumeSwipeSpeed(volumeSwipeSpeedOptions.indexOf(it)) + }, + textColor = textColor + ) SinglePodANCSwitch(service = service, sharedPreferences = sharedPreferences) VolumeControlSwitch(service = service, sharedPreferences = sharedPreferences) + TransparencySettings(service = service, sharedPreferences = sharedPreferences) + } +} + +@Composable +fun DropdownMenuComponent( + label: String, + options: List, + selectedOption: String, + onOptionSelected: (String) -> Unit, + textColor: Color +) { + var expanded by remember { mutableStateOf(false) } + + Column ( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 8.dp) + ) { + Text( + text = label, + style = TextStyle( + fontSize = 16.sp, + fontWeight = FontWeight.Medium, + color = textColor + ) + ) + + Box( + modifier = Modifier + .fillMaxWidth() + .clickable { expanded = true } + .padding(8.dp) + ) { + Text( + text = selectedOption, + modifier = Modifier.padding(16.dp), + color = textColor + ) + } + + DropdownMenu( + expanded = expanded, + onDismissRequest = { expanded = false } + ) { + options.forEach { option -> + DropdownMenuItem( + onClick = { + onOptionSelected(option) + expanded = false + }, + text = { Text(text = option) } + ) + } + } } } diff --git a/android/app/src/main/java/me/kavishdevar/aln/composables/TransparencySettings.kt b/android/app/src/main/java/me/kavishdevar/aln/composables/TransparencySettings.kt new file mode 100644 index 0000000..4a1d7f4 --- /dev/null +++ b/android/app/src/main/java/me/kavishdevar/aln/composables/TransparencySettings.kt @@ -0,0 +1,270 @@ +package me.kavishdevar.aln.composables + +import android.content.SharedPreferences +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Slider +import androidx.compose.material3.SliderDefaults +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +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.draw.shadow +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.Font +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import me.kavishdevar.aln.R +import me.kavishdevar.aln.services.AirPodsService + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun TransparencySettings(service: AirPodsService, sharedPreferences: SharedPreferences) { + val isDarkTheme = isSystemInDarkTheme() + val textColor = if (isDarkTheme) Color.White else Color.Black + + var transparencyModeCustomizationEnabled by remember { mutableStateOf(sharedPreferences.getBoolean("transparency_mode_customization", false)) } + var amplification by remember { mutableIntStateOf(sharedPreferences.getInt("transparency_amplification", 0)) } + var balance by remember { mutableIntStateOf(sharedPreferences.getInt("transparency_balance", 0)) } + var tone by remember { mutableIntStateOf(sharedPreferences.getInt("transparency_tone", 0)) } + var ambientNoise by remember { mutableIntStateOf(sharedPreferences.getInt("transparency_ambient_noise", 0)) } + var conversationBoostEnabled by remember { mutableStateOf(sharedPreferences.getBoolean("transparency_conversation_boost", false)) } + + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 12.dp, vertical = 12.dp) + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .clickable( + indication = null, + interactionSource = remember { MutableInteractionSource() } + ) { + transparencyModeCustomizationEnabled = !transparencyModeCustomizationEnabled + }, + verticalAlignment = Alignment.CenterVertically + ) { + Column( + modifier = Modifier + .weight(1f) + .padding(end = 4.dp) + ) { + Text( + text = "Transparency Mode", + fontSize = 16.sp, + color = textColor + ) + Spacer(modifier = Modifier.height(4.dp)) + Text( + text = "You can customize Transparency mode for your AirPods Pro.", + fontSize = 12.sp, + color = textColor.copy(0.6f), + lineHeight = 14.sp, + ) + } + StyledSwitch( + checked = transparencyModeCustomizationEnabled, + onCheckedChange = { + transparencyModeCustomizationEnabled = it + }, + ) + } + if (transparencyModeCustomizationEnabled) { + Spacer(modifier = Modifier.height(8.dp)) + SliderRow( + label = "Amplification", + value = amplification, + onValueChange = { + amplification = it + sharedPreferences.edit().putInt("transparency_amplification", it).apply() + }, + isDarkTheme = isDarkTheme + ) + Spacer(modifier = Modifier.height(8.dp)) + SliderRow( + label = "Balance", + value = balance, + onValueChange = { + balance = it + sharedPreferences.edit().putInt("transparency_balance", it).apply() + }, + isDarkTheme = isDarkTheme + ) + Spacer(modifier = Modifier.height(8.dp)) + SliderRow( + label = "Tone", + value = tone, + onValueChange = { + tone = it + sharedPreferences.edit().putInt("transparency_tone", it).apply() + }, + isDarkTheme = isDarkTheme + ) + Spacer(modifier = Modifier.height(8.dp)) + SliderRow( + label = "Ambient Noise", + value = ambientNoise, + onValueChange = { + ambientNoise = it + sharedPreferences.edit().putInt("transparency_ambient_noise", it).apply() + }, + isDarkTheme = isDarkTheme + ) + Spacer(modifier = Modifier.height(8.dp)) + Row( + modifier = Modifier + .fillMaxWidth() + .clickable( + indication = null, + interactionSource = remember { MutableInteractionSource() } + ) { + conversationBoostEnabled = !conversationBoostEnabled + }, + verticalAlignment = Alignment.CenterVertically + ) { + Column( + modifier = Modifier + .weight(1f) + .padding(end = 4.dp) + ) { + Text( + text = "Conversation Boost", + fontSize = 16.sp, + color = textColor + ) + Spacer(modifier = Modifier.height(4.dp)) + Text( + text = "Conversation Boost focuses your AirPods on the person in front of you, making it easier to hear in a face-to-face conversation.", + fontSize = 12.sp, + color = textColor.copy(0.6f), + lineHeight = 14.sp, + ) + } + StyledSwitch( + checked = conversationBoostEnabled, + onCheckedChange = { + conversationBoostEnabled = it + }, + ) + } + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun SliderRow( + label: String, + value: Int, + onValueChange: (Int) -> Unit, + isDarkTheme: Boolean +) { + val trackColor = if (isDarkTheme) Color(0xFFB3B3B3) else Color(0xFF929491) + val activeTrackColor = if (isDarkTheme) Color(0xFF007AFF) else Color(0xFF3C6DF5) + val thumbColor = Color(0xFFFFFFFF) + val labelTextColor = if (isDarkTheme) Color.White else Color.Black + + Row( + modifier = Modifier + .fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = label, + style = TextStyle( + fontSize = 16.sp, + color = labelTextColor, + fontFamily = FontFamily(Font(R.font.sf_pro)) + ) + ) + Text( + text = "\uDBC0\uDEA1", + style = TextStyle( + fontSize = 16.sp, + color = labelTextColor, + fontFamily = FontFamily(Font(R.font.sf_pro)) + ), + modifier = Modifier.padding(start = 4.dp) + ) + Slider( + value = value.toFloat(), + onValueChange = { + onValueChange(it.toInt()) + }, + valueRange = 0f..100f, + onValueChangeFinished = { + onValueChange(value) + }, + modifier = Modifier + .weight(1f) + .height(36.dp), + colors = SliderDefaults.colors( + thumbColor = thumbColor, + activeTrackColor = activeTrackColor, + inactiveTrackColor = trackColor + ), + thumb = { + Box( + modifier = Modifier + .size(24.dp) + .shadow(4.dp, CircleShape) + .background(thumbColor, CircleShape) + ) + }, + track = { + Box( + modifier = Modifier + .fillMaxWidth() + .height(12.dp), + contentAlignment = Alignment.CenterStart + ) { + Box( + modifier = Modifier + .fillMaxWidth() + .height(4.dp) + .background(trackColor, RoundedCornerShape(4.dp)) + ) + Box( + modifier = Modifier + .fillMaxWidth(value.toFloat() / 100) + .height(4.dp) + .background(activeTrackColor, RoundedCornerShape(4.dp)) + ) + } + } + ) + Text( + text = "\uDBC0\uDEA9", + style = TextStyle( + fontSize = 16.sp, + color = labelTextColor, + fontFamily = FontFamily(Font(R.font.sf_pro)) + ), + modifier = Modifier.padding(end = 4.dp) + ) + } +} \ No newline at end of file diff --git a/android/app/src/main/java/me/kavishdevar/aln/services/AirPodsService.kt b/android/app/src/main/java/me/kavishdevar/aln/services/AirPodsService.kt index e31a8ce..baa3757 100644 --- a/android/app/src/main/java/me/kavishdevar/aln/services/AirPodsService.kt +++ b/android/app/src/main/java/me/kavishdevar/aln/services/AirPodsService.kt @@ -86,7 +86,7 @@ object ServiceManager { } } -@Suppress("unused") +//@Suppress("unused") class AirPodsService: Service() { inner class LocalBinder : Binder() { fun getService(): AirPodsService = this@AirPodsService @@ -211,60 +211,9 @@ class AirPodsService: Service() { fun updateNotificationContent(connected: Boolean, airpodsName: String? = null, batteryList: List? = null) { val notificationManager = getSystemService(NotificationManager::class.java) -// val textColor = this.getSharedPreferences("settings", MODE_PRIVATE).getLong("textColor", 0) var updatedNotification: Notification? = null if (connected) { -// val collapsedRemoteViews = RemoteViews(packageName, R.layout.notification) -// val expandedRemoteViews = RemoteViews(packageName, R.layout.notification_expanded) -// collapsedRemoteViews.setTextColor(R.id.notification_title, textColor.toInt()) -// -// collapsedRemoteViews.setTextViewText(R.id.notification_title, "Connected to $airpodsName") -// expandedRemoteViews.setTextViewText( -// R.id.notification_title, -// "Connected to $airpodsName" -// ) -// expandedRemoteViews.setTextViewText( -// R.id.left_battery_notification, -// batteryList?.find { it.component == BatteryComponent.LEFT }?.let { -// if (it.status != BatteryStatus.DISCONNECTED) { -// "Left ${if (it.status == BatteryStatus.CHARGING) "⚡" else ""} ${it.level}%" -// } else { -// "" -// } -// } ?: "") -// expandedRemoteViews.setTextViewText( -// R.id.right_battery_notification, -// batteryList?.find { it.component == BatteryComponent.RIGHT }?.let { -// if (it.status != BatteryStatus.DISCONNECTED) { -// "Right ${if (it.status == BatteryStatus.CHARGING) "⚡" else ""} ${it.level}%" -// } else { -// "" -// } -// } ?: "") -// expandedRemoteViews.setTextViewText( -// R.id.case_battery_notification, -// batteryList?.find { it.component == BatteryComponent.CASE }?.let { -// if (it.status != BatteryStatus.DISCONNECTED) { -// "Case ${if (it.status == BatteryStatus.CHARGING) "⚡" else ""} ${it.level}%" -// } else { -// "" -// } -// } ?: "") -// expandedRemoteViews.setTextColor(R.id.notification_title, textColor.toInt()) -// expandedRemoteViews.setTextColor(R.id.left_battery_notification, textColor.toInt()) -// expandedRemoteViews.setTextColor(R.id.right_battery_notification, textColor.toInt()) -// expandedRemoteViews.setTextColor(R.id.case_battery_notification, textColor.toInt()) -// updatedNotification = NotificationCompat.Builder(this, "background_service_status") -// .setSmallIcon(R.drawable.airpods) -// .setStyle(NotificationCompat.DecoratedCustomViewStyle()) -// .setCustomContentView(collapsedRemoteViews) -// .setCustomBigContentView(expandedRemoteViews) -// .setPriority(NotificationCompat.PRIORITY_LOW) -// .setCategory(Notification.CATEGORY_SERVICE) -// .setOngoing(true) -// .build() - updatedNotification = NotificationCompat.Builder(this, "background_service_status") .setSmallIcon(R.drawable.airpods) .setContentTitle("""$airpodsName –${batteryList?.find { it.component == BatteryComponent.LEFT }?.let { @@ -757,17 +706,27 @@ class AirPodsService: Service() { } fun setPressSpeed(speed: Int) { + // 0x00 = default, 0x01 = slower, 0x02 = slowest val bytes = byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x09, 0x00, 0x17, speed.toByte(), 0x00, 0x00, 0x00) socket.outputStream?.write(bytes) socket.outputStream?.flush() } fun setPressAndHoldDuration(speed: Int) { + // 0 - default, 1 - slower, 2 - slowest val bytes = byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x09, 0x00, 0x18, speed.toByte(), 0x00, 0x00, 0x00) socket.outputStream?.write(bytes) socket.outputStream?.flush() } + fun setVolumeSwipeSpeed(speed: Int) { + // 0 - default, 1 - longer, 2 - longest + val bytes = byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x09, 0x00, 0x23, speed.toByte(), 0x00, 0x00, 0x00) + Log.d("AirPodsService", "Setting volume swipe speed to $speed by packet ${bytes.joinToString(" ") { "%02X".format(it) }}") + socket.outputStream?.write(bytes) + socket.outputStream?.flush() + } + fun setNoiseCancellationWithOnePod(enabled: Boolean) { val bytes = byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x09, 0x00, 0x1B, if (enabled) 0x01 else 0x02, 0x00, 0x00, 0x00) socket.outputStream?.write(bytes) @@ -780,12 +739,6 @@ class AirPodsService: Service() { socket.outputStream?.flush() } - fun setVolumeSwipeSpeed(speed: Int) { - val bytes = byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x09, 0x00, 0x23, speed.toByte(), 0x00, 0x00, 0x00) - socket.outputStream?.write(bytes) - socket.outputStream?.flush() - } - fun setToneVolume(volume: Int) { val bytes = byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x09, 0x00, 0x1F, volume.toByte(), 0x50, 0x00, 0x00) socket.outputStream?.write(bytes)