diff --git a/android/app/src/main/java/me/kavishdevar/aln/MainActivity.kt b/android/app/src/main/java/me/kavishdevar/aln/MainActivity.kt index f819939..d9692f7 100644 --- a/android/app/src/main/java/me/kavishdevar/aln/MainActivity.kt +++ b/android/app/src/main/java/me/kavishdevar/aln/MainActivity.kt @@ -55,8 +55,10 @@ import androidx.navigation.compose.rememberNavController import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.google.accompanist.permissions.rememberMultiplePermissionsState import me.kavishdevar.aln.screens.AirPodsSettingsScreen +import me.kavishdevar.aln.screens.AppSettingsScreen import me.kavishdevar.aln.screens.DebugScreen import me.kavishdevar.aln.screens.LongPress +import me.kavishdevar.aln.screens.RenameScreen import me.kavishdevar.aln.services.AirPodsService import me.kavishdevar.aln.ui.theme.ALNTheme import me.kavishdevar.aln.utils.AirPodsNotifications @@ -158,7 +160,7 @@ fun Main() { NavHost( navController = navController, - startDestination = "settings", + startDestination = "app_settings", enterTransition = { slideInHorizontally(initialOffsetX = { it }, animationSpec = tween(300)) }, exitTransition = { slideOutHorizontally(targetOffsetX = { -it }, animationSpec = tween(300)) }, popEnterTransition = { slideInHorizontally(initialOffsetX = { -it }, animationSpec = tween(300)) }, @@ -183,6 +185,12 @@ fun Main() { name = navBackStackEntry.arguments?.getString("bud")!! ) } + composable("rename") { navBackStackEntry -> + RenameScreen(navController) + } + composable("app_settings") { + AppSettingsScreen(navController) + } } serviceConnection = remember { 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 c437dcb..1ec51dc 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 @@ -31,11 +31,13 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import me.kavishdevar.aln.R import me.kavishdevar.aln.services.AirPodsService @Composable @@ -44,7 +46,7 @@ fun AccessibilitySettings(service: AirPodsService, sharedPreferences: SharedPref val textColor = if (isDarkTheme) Color.White else Color.Black Text( - text = "ACCESSIBILITY", + text = stringResource(R.string.accessibility).uppercase(), style = TextStyle( fontSize = 14.sp, fontWeight = FontWeight.Light, @@ -68,7 +70,7 @@ fun AccessibilitySettings(service: AirPodsService, sharedPreferences: SharedPref .padding(12.dp) ) { Text( - text = "Tone Volume", + text = stringResource(R.string.tone_volume), modifier = Modifier .padding(end = 8.dp, bottom = 2.dp, start = 2.dp) .fillMaxWidth(), @@ -95,4 +97,4 @@ fun AccessibilitySettings(service: AirPodsService, sharedPreferences: SharedPref @Composable fun AccessibilitySettingsPreview() { AccessibilitySettings(service = AirPodsService(), sharedPreferences = LocalContext.current.getSharedPreferences("preview", Context.MODE_PRIVATE)) -} \ No newline at end of file +} diff --git a/android/app/src/main/java/me/kavishdevar/aln/composables/AudioSettings.kt b/android/app/src/main/java/me/kavishdevar/aln/composables/AudioSettings.kt index e85f460..4c9afa9 100644 --- a/android/app/src/main/java/me/kavishdevar/aln/composables/AudioSettings.kt +++ b/android/app/src/main/java/me/kavishdevar/aln/composables/AudioSettings.kt @@ -31,11 +31,13 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import me.kavishdevar.aln.R import me.kavishdevar.aln.services.AirPodsService @Composable @@ -44,7 +46,7 @@ fun AudioSettings(service: AirPodsService, sharedPreferences: SharedPreferences) val textColor = if (isDarkTheme) Color.White else Color.Black Text( - text = "AUDIO", + text = stringResource(R.string.audio).uppercase(), style = TextStyle( fontSize = 14.sp, fontWeight = FontWeight.Light, @@ -72,7 +74,7 @@ fun AudioSettings(service: AirPodsService, sharedPreferences: SharedPreferences) .padding(horizontal = 8.dp, vertical = 10.dp) ) { Text( - text = "Adaptive Audio", + text = stringResource(R.string.adaptive_audio), modifier = Modifier .padding(end = 8.dp, bottom = 2.dp, start = 2.dp) .fillMaxWidth(), @@ -82,7 +84,7 @@ fun AudioSettings(service: AirPodsService, sharedPreferences: SharedPreferences) ) ) Text( - text = "Adaptive audio dynamically responds to your environment and cancels or allows external noise. You can customize Adaptive Audio to allow more or less noise.", + text = stringResource(R.string.adaptive_audio_description), modifier = Modifier .padding(bottom = 8.dp, top = 2.dp) .padding(end = 2.dp, start = 2.dp) @@ -102,4 +104,4 @@ fun AudioSettings(service: AirPodsService, sharedPreferences: SharedPreferences) @Composable fun AudioSettingsPreview() { AudioSettings(service = AirPodsService(), sharedPreferences = LocalContext.current.getSharedPreferences("preview", Context.MODE_PRIVATE)) -} \ No newline at end of file +} diff --git a/android/app/src/main/java/me/kavishdevar/aln/composables/BatteryView.kt b/android/app/src/main/java/me/kavishdevar/aln/composables/BatteryView.kt index d5c925c..8f66bbf 100644 --- a/android/app/src/main/java/me/kavishdevar/aln/composables/BatteryView.kt +++ b/android/app/src/main/java/me/kavishdevar/aln/composables/BatteryView.kt @@ -41,6 +41,7 @@ import androidx.compose.ui.draw.scale import androidx.compose.ui.graphics.ImageBitmap import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.imageResource +import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import me.kavishdevar.aln.R @@ -110,7 +111,7 @@ fun BatteryView(service: AirPodsService, preview: Boolean = false) { ) { Image ( bitmap = ImageBitmap.imageResource(R.drawable.pro_2_buds), - contentDescription = "Buds", + contentDescription = stringResource(R.string.buds), modifier = Modifier .fillMaxWidth() .scale(0.80f) @@ -163,7 +164,7 @@ fun BatteryView(service: AirPodsService, preview: Boolean = false) { Image( bitmap = ImageBitmap.imageResource(R.drawable.pro_2_case), - contentDescription = "Case", + contentDescription = stringResource(R.string.case_alt), modifier = Modifier .fillMaxWidth() .scale(1.25f) @@ -181,4 +182,4 @@ fun BatteryView(service: AirPodsService, preview: Boolean = false) { @Composable fun BatteryViewPreview() { BatteryView(AirPodsService(), preview = true) -} \ No newline at end of file +} diff --git a/android/app/src/main/java/me/kavishdevar/aln/composables/StyledTextField.kt b/android/app/src/main/java/me/kavishdevar/aln/composables/NameField.kt similarity index 51% rename from android/app/src/main/java/me/kavishdevar/aln/composables/StyledTextField.kt rename to android/app/src/main/java/me/kavishdevar/aln/composables/NameField.kt index c66b0d8..1bfc321 100644 --- a/android/app/src/main/java/me/kavishdevar/aln/composables/StyledTextField.kt +++ b/android/app/src/main/java/me/kavishdevar/aln/composables/NameField.kt @@ -19,14 +19,20 @@ package me.kavishdevar.aln.composables import androidx.compose.foundation.background +import androidx.compose.foundation.clickable import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Row 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.RoundedCornerShape import androidx.compose.foundation.text.BasicTextField +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight +import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue @@ -39,15 +45,18 @@ import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import androidx.navigation.NavController +import androidx.navigation.compose.rememberNavController @Composable -fun StyledTextField( +fun NameField( name: String, value: String, - onValueChange: (String) -> Unit + navController: NavController ) { var isFocused by remember { mutableStateOf(false) } @@ -61,53 +70,72 @@ fun StyledTextField( Color.Transparent } - Row( - verticalAlignment = Alignment.CenterVertically, + Box ( modifier = Modifier - .fillMaxWidth() - .height(55.dp) - .background( - backgroundColor, - RoundedCornerShape(14.dp) - ) - .padding(horizontal = 16.dp, vertical = 8.dp) - ) { - Text( - text = name, - style = TextStyle( - fontSize = 16.sp, - color = textColor - ) - ) - BasicTextField( - value = value, - onValueChange = onValueChange, - textStyle = TextStyle( - color = textColor, - fontSize = 16.sp, - ), - singleLine = true, - cursorBrush = SolidColor(cursorColor), // Dynamic cursor color based on focus - decorationBox = { innerTextField -> - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.End - ) { - innerTextField() + .clickable( + onClick = { + navController.navigate("rename") } - }, + ) + ) { + Row( + verticalAlignment = Alignment.CenterVertically, modifier = Modifier .fillMaxWidth() - .padding(start = 8.dp) - .onFocusChanged { focusState -> - isFocused = focusState.isFocused // Update focus state + .height(55.dp) + .background( + backgroundColor, + RoundedCornerShape(14.dp) + ) + .padding(horizontal = 16.dp, vertical = 8.dp) + + ) { + Text( + text = name, + style = TextStyle( + fontSize = 16.sp, + color = textColor + ) + ) + BasicTextField( + value = value, + textStyle = TextStyle( + color = textColor.copy(alpha = 0.75f), + fontSize = 16.sp, + textAlign = TextAlign.End + ), + onValueChange = {}, + singleLine = true, + enabled = false, + cursorBrush = SolidColor(cursorColor), + modifier = Modifier + .fillMaxWidth() + .padding(start = 8.dp) + .onFocusChanged { focusState -> + isFocused = focusState.isFocused + }, + decorationBox = { innerTextField -> + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.End + ) { + innerTextField() + Icon( + imageVector = Icons.AutoMirrored.Filled.KeyboardArrowRight, + contentDescription = "Edit name", + tint = textColor.copy(alpha = 0.75f), + modifier = Modifier + .size(32.dp) + ) + } } - ) + ) + } } } @Preview @Composable fun StyledTextFieldPreview() { - StyledTextField(name = "Name", value = "AirPods Pro", onValueChange = {}) + NameField(name = "Name", value = "AirPods Pro", rememberNavController()) } \ No newline at end of file diff --git a/android/app/src/main/java/me/kavishdevar/aln/composables/NoiseControlSettings.kt b/android/app/src/main/java/me/kavishdevar/aln/composables/NoiseControlSettings.kt index 08f83da..aa9edbd 100644 --- a/android/app/src/main/java/me/kavishdevar/aln/composables/NoiseControlSettings.kt +++ b/android/app/src/main/java/me/kavishdevar/aln/composables/NoiseControlSettings.kt @@ -39,7 +39,6 @@ import androidx.compose.material3.Text import androidx.compose.material3.VerticalDivider import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -49,6 +48,7 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.ImageBitmap import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.imageResource +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign @@ -81,6 +81,7 @@ fun NoiseControlSettings(service: AirPodsService) { sharedPreferences.unregisterOnSharedPreferenceChangeListener(preferenceChangeListener) } } + val isDarkTheme = isSystemInDarkTheme() val backgroundColor = if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFE3E3E8) val textColor = if (isDarkTheme) Color.White else Color.Black @@ -151,12 +152,12 @@ fun NoiseControlSettings(service: AirPodsService) { context.registerReceiver(noiseControlReceiver, noiseControlIntentFilter) } - Text( - text = "NOISE CONTROL", + Text(// all caps + text = stringResource(R.string.noise_control).uppercase(), style = TextStyle( fontSize = 14.sp, fontWeight = FontWeight.Light, - color = textColor.copy(alpha = 0.6f) + color = textColor.copy(alpha = 0.6f), ), modifier = Modifier.padding(8.dp, bottom = 2.dp) ) @@ -238,7 +239,7 @@ fun NoiseControlSettings(service: AirPodsService) { ) { if (offListeningMode.value) { Text( - text = "Off", + text = stringResource(R.string.off), style = TextStyle(fontSize = 12.sp, color = textColor), textAlign = TextAlign.Center, fontWeight = FontWeight.Bold, @@ -246,21 +247,21 @@ fun NoiseControlSettings(service: AirPodsService) { ) } Text( - text = "Transparency", + text = stringResource(R.string.transparency), style = TextStyle(fontSize = 12.sp, color = textColor), textAlign = TextAlign.Center, fontWeight = FontWeight.Bold, modifier = Modifier.weight(1f) ) Text( - text = "Adaptive", + text = stringResource(R.string.adaptive), style = TextStyle(fontSize = 12.sp, color = textColor), textAlign = TextAlign.Center, fontWeight = FontWeight.Bold, modifier = Modifier.weight(1f) ) Text( - text = "Noise Cancellation", + text = stringResource(R.string.noise_cancellation), style = TextStyle(fontSize = 12.sp, color = textColor), textAlign = TextAlign.Center, fontWeight = FontWeight.Bold, diff --git a/android/app/src/main/java/me/kavishdevar/aln/composables/PressAndHoldSettings.kt b/android/app/src/main/java/me/kavishdevar/aln/composables/PressAndHoldSettings.kt index d91e05e..8f89570 100644 --- a/android/app/src/main/java/me/kavishdevar/aln/composables/PressAndHoldSettings.kt +++ b/android/app/src/main/java/me/kavishdevar/aln/composables/PressAndHoldSettings.kt @@ -40,6 +40,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.Font import androidx.compose.ui.text.font.FontFamily @@ -56,7 +57,7 @@ fun PressAndHoldSettings(navController: NavController) { val textColor = if (isDarkTheme) Color.White else Color.Black Text( - text = "PRESS AND HOLD AIRPODS", + text = stringResource(R.string.press_and_hold_airpods).uppercase(), style = TextStyle( fontSize = 14.sp, fontWeight = FontWeight.Light, @@ -95,7 +96,7 @@ fun PressAndHoldSettings(navController: NavController) { verticalAlignment = Alignment.CenterVertically ) { Text( - text = "Left", + text = stringResource(R.string.left), style = TextStyle( fontSize = 18.sp, color = textColor, @@ -105,7 +106,7 @@ fun PressAndHoldSettings(navController: NavController) { Spacer(modifier = Modifier.weight(1f)) Text( // TODO: Implement voice assistant on long press; for now, it's noise control - text = "Noise Control", + text = stringResource(R.string.noise_control), style = TextStyle( fontSize = 18.sp, color = textColor.copy(alpha = 0.6f), @@ -152,7 +153,7 @@ fun PressAndHoldSettings(navController: NavController) { verticalAlignment = Alignment.CenterVertically ) { Text( - text = "Right", + text = stringResource(R.string.right), style = TextStyle( fontSize = 18.sp, color = textColor, @@ -162,7 +163,7 @@ fun PressAndHoldSettings(navController: NavController) { Spacer(modifier = Modifier.weight(1f)) Text( // TODO: Implement voice assistant on long press; for now, it's noise control - text = "Noise Control", + text = stringResource(R.string.noise_control), style = TextStyle( fontSize = 18.sp, color = textColor.copy(alpha = 0.6f), @@ -189,4 +190,4 @@ fun PressAndHoldSettings(navController: NavController) { @Composable fun PressAndHoldSettingsPreview() { PressAndHoldSettings(navController = NavController(LocalContext.current)) -} \ No newline at end of file +} diff --git a/android/app/src/main/java/me/kavishdevar/aln/composables/StyledSwitch.kt b/android/app/src/main/java/me/kavishdevar/aln/composables/StyledSwitch.kt index c583d20..a631bcc 100644 --- a/android/app/src/main/java/me/kavishdevar/aln/composables/StyledSwitch.kt +++ b/android/app/src/main/java/me/kavishdevar/aln/composables/StyledSwitch.kt @@ -42,14 +42,23 @@ import androidx.compose.ui.unit.dp @Composable fun StyledSwitch( checked: Boolean, - onCheckedChange: (Boolean) -> Unit + onCheckedChange: (Boolean) -> Unit, + enabled: Boolean = true, ) { val isDarkTheme = isSystemInDarkTheme() val thumbColor = Color.White - val trackColor = if (checked) Color(0xFF34C759) else if (isDarkTheme) Color(0xFF5B5B5E) else Color(0xFFD1D1D6) + val trackColor = if (enabled) ( + if (isDarkTheme) { + if (checked) Color(0xFF34C759) else Color(0xFF5B5B5E) + } else { + if (checked) Color(0xFF34C759) else Color(0xFFD1D1D6) + } + ) else { + if (isDarkTheme) Color(0xFF5B5B5E) else Color(0xFFD1D1D6) + } + - // Animate the horizontal offset of the thumb val thumbOffsetX by animateDpAsState(targetValue = if (checked) 20.dp else 0.dp, label = "Test") Box( @@ -63,11 +72,11 @@ fun StyledSwitch( ) { Box( modifier = Modifier - .offset(x = thumbOffsetX) // Animate the offset for smooth transition + .offset(x = thumbOffsetX) .size(27.dp) .clip(CircleShape) - .background(thumbColor) // Dynamic thumb color - .clickable { onCheckedChange(!checked) } // Make the switch clickable + .background(thumbColor) + .clickable { if (enabled) onCheckedChange(!checked) } ) } } diff --git a/android/app/src/main/java/me/kavishdevar/aln/screens/AirPodsSettingsScreen.kt b/android/app/src/main/java/me/kavishdevar/aln/screens/AirPodsSettingsScreen.kt index bfec98a..3ec6116 100644 --- a/android/app/src/main/java/me/kavishdevar/aln/screens/AirPodsSettingsScreen.kt +++ b/android/app/src/main/java/me/kavishdevar/aln/screens/AirPodsSettingsScreen.kt @@ -22,6 +22,7 @@ import android.annotation.SuppressLint import android.bluetooth.BluetoothDevice import android.content.Context.MODE_PRIVATE import android.content.Intent +import android.content.SharedPreferences import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -33,7 +34,7 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Refresh +import androidx.compose.material.icons.filled.Settings import androidx.compose.material3.CenterAlignedTopAppBar import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon @@ -43,6 +44,7 @@ import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.TopAppBarDefaults 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 @@ -55,6 +57,7 @@ import androidx.compose.ui.draw.drawBehind import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.Font import androidx.compose.ui.text.font.FontFamily @@ -76,12 +79,11 @@ import me.kavishdevar.aln.composables.AccessibilitySettings import me.kavishdevar.aln.composables.AudioSettings import me.kavishdevar.aln.composables.BatteryView import me.kavishdevar.aln.composables.IndependentToggle +import me.kavishdevar.aln.composables.NameField import me.kavishdevar.aln.composables.NavigationButton import me.kavishdevar.aln.composables.NoiseControlSettings import me.kavishdevar.aln.composables.PressAndHoldSettings -import me.kavishdevar.aln.composables.StyledTextField import me.kavishdevar.aln.services.AirPodsService -import me.kavishdevar.aln.services.ServiceManager import me.kavishdevar.aln.ui.theme.ALNTheme import me.kavishdevar.aln.utils.AirPodsNotifications @@ -99,6 +101,23 @@ fun AirPodsSettingsScreen(dev: BluetoothDevice?, service: AirPodsService, ) ) } + + val nameChangeListener = remember { + SharedPreferences.OnSharedPreferenceChangeListener { _, key -> + if (key == "name") { + deviceName = TextFieldValue(sharedPreferences.getString("name", "AirPods Pro").toString()) + } + } + } + + DisposableEffect(Unit) { + sharedPreferences.registerOnSharedPreferenceChangeListener(nameChangeListener) + onDispose { + sharedPreferences.unregisterOnSharedPreferenceChangeListener(nameChangeListener) + } + } + + val verticalScrollState = rememberScrollState() val hazeState = remember { HazeState() } @@ -150,10 +169,9 @@ fun AirPodsSettingsScreen(dev: BluetoothDevice?, service: AirPodsService, containerColor = Color.Transparent ), actions = { - val context = LocalContext.current IconButton( onClick = { - ServiceManager.restartService(context) + navController.navigate("app_settings") }, colors = IconButtonDefaults.iconButtonColors( containerColor = Color.Transparent, @@ -161,7 +179,7 @@ fun AirPodsSettingsScreen(dev: BluetoothDevice?, service: AirPodsService, ) ) { Icon( - imageVector = Icons.Default.Refresh, + imageVector = Icons.Default.Settings, contentDescription = "Settings", ) } @@ -199,14 +217,10 @@ fun AirPodsSettingsScreen(dev: BluetoothDevice?, service: AirPodsService, Spacer(modifier = Modifier.height(32.dp)) - StyledTextField( - name = "Name", + NameField( + name = stringResource(R.string.name), value = deviceName.text, - onValueChange = { - deviceName = TextFieldValue(it) - sharedPreferences.edit().putString("name", it).apply() - service.setName(it) - } + navController = navController ) Spacer(modifier = Modifier.height(32.dp)) diff --git a/android/app/src/main/java/me/kavishdevar/aln/screens/AppSettingsScreen.kt b/android/app/src/main/java/me/kavishdevar/aln/screens/AppSettingsScreen.kt new file mode 100644 index 0000000..499813c --- /dev/null +++ b/android/app/src/main/java/me/kavishdevar/aln/screens/AppSettingsScreen.kt @@ -0,0 +1,359 @@ +/* + * AirPods like Normal (ALN) - Bringing Apple-only features to Linux and Android for seamless AirPods functionality! + * + * Copyright (C) 2024 Kavish Devar + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package me.kavishdevar.aln.screens + +import android.content.Context +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.fillMaxSize +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.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.KeyboardArrowLeft +import androidx.compose.material3.CenterAlignedTopAppBar +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Slider +import androidx.compose.material3.SliderDefaults +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +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.draw.scale +import androidx.compose.ui.draw.shadow +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.Font +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.navigation.NavController +import me.kavishdevar.aln.R +import me.kavishdevar.aln.composables.StyledSwitch +import kotlin.math.roundToInt + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun AppSettingsScreen(navController: NavController) { + val sharedPreferences = LocalContext.current.getSharedPreferences("settings", Context.MODE_PRIVATE) + val name = remember { mutableStateOf(sharedPreferences.getString("name", "") ?: "") } + val isDarkTheme = isSystemInDarkTheme() + Scaffold( + topBar = { + CenterAlignedTopAppBar( + title = { + Text( + text = stringResource(R.string.app_settings), + fontFamily = FontFamily(Font(R.font.sf_pro)), + ) + }, + navigationIcon = { + TextButton( + onClick = { + navController.popBackStack() + }, + shape = RoundedCornerShape(8.dp), + ) { + Icon( + Icons.AutoMirrored.Filled.KeyboardArrowLeft, + contentDescription = "Back", + tint = if (isDarkTheme) Color(0xFF007AFF) else Color(0xFF3C6DF5), + modifier = Modifier.scale(1.5f) + ) + Text( + text = name.value, + style = TextStyle( + fontSize = 18.sp, + fontWeight = FontWeight.Medium, + color = if (isDarkTheme) Color(0xFF007AFF) else Color(0xFF3C6DF5), + fontFamily = FontFamily(Font(R.font.sf_pro)) + ), + ) + } + }, + colors = TopAppBarDefaults.topAppBarColors( + containerColor = Color.Transparent + ) + ) + }, + containerColor = if (isSystemInDarkTheme()) Color(0xFF000000) + else Color(0xFFF2F2F7), + ) { paddingValues -> + Column ( + modifier = Modifier + .fillMaxSize() + .padding(paddingValues) + .padding(horizontal = 12.dp) + ) { + val isDarkTheme = isSystemInDarkTheme() + + val backgroundColor = if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFFFFFFF) + val textColor = if (isDarkTheme) Color.White else Color.Black + + Column ( + modifier = Modifier + .fillMaxWidth() + .height(275.sp.value.dp) + .background( + backgroundColor, + RoundedCornerShape(14.dp) + ) + .padding(horizontal = 16.dp, vertical = 8.dp) + ) { + val sliderValue = remember { mutableFloatStateOf(0f) } + LaunchedEffect(sliderValue) { + if (sharedPreferences.contains("conversational_awareness_volume")) { + sliderValue.floatValue = sharedPreferences.getInt("conversational_awareness_volume", 0).toFloat() + } + } + LaunchedEffect(sliderValue.floatValue) { + sharedPreferences.edit().putInt("conversational_awareness_volume", sliderValue.floatValue.toInt()).apply() + } + + val trackColor = if (isDarkTheme) Color(0xFFB3B3B3) else Color(0xFFD9D9D9) + val thumbColor = if (isDarkTheme) Color(0xFFFFFFFF) else Color(0xFFFFFFFF) + val labelTextColor = if (isDarkTheme) Color.White else Color.Black + + Text( + text = stringResource(R.string.conversational_awareness_customization), + style = TextStyle( + fontSize = 20.sp, + color = textColor + ), + modifier = Modifier + .padding(top = 12.dp, bottom = 4.dp) + ) + + + var conversationalAwarenessPauseMusicEnabled by remember { + mutableStateOf( + sharedPreferences.getBoolean("conversational_awareness_pause_music", true) + ) + } + + fun updateConversationalAwarenessPauseMusic(enabled: Boolean) { + conversationalAwarenessPauseMusicEnabled = enabled + sharedPreferences.edit().putBoolean("conversational_awareness_pause_music", enabled).apply() + } + + var relativeConversationalAwarenessVolumeEnabled by remember { + mutableStateOf( + sharedPreferences.getBoolean("relative_conversational_awareness_volume", true) + ) + } + + fun updateRelativeConversationalAwarenessVolume(enabled: Boolean) { + relativeConversationalAwarenessVolumeEnabled = enabled + sharedPreferences.edit().putBoolean("relative_conversational_awareness_volume", enabled).apply() + } + + Row( + modifier = Modifier + .fillMaxWidth() + .height(85.sp.value.dp) + .background( + shape = RoundedCornerShape(14.dp), + color = Color.Transparent + ) + .clickable( + indication = null, + interactionSource = remember { MutableInteractionSource() } + ) { + updateConversationalAwarenessPauseMusic(!conversationalAwarenessPauseMusicEnabled) + }, + verticalAlignment = Alignment.CenterVertically + ) { + Column( + modifier = Modifier + .weight(1f) + .padding(end = 4.dp) + ) { + Text( + text = stringResource(R.string.conversational_awareness_pause_music), + fontSize = 16.sp, + color = textColor + ) + Spacer(modifier = Modifier.height(4.dp)) + Text( + text = stringResource(R.string.conversational_awareness_pause_music_description), + fontSize = 14.sp, + color = textColor.copy(0.6f), + lineHeight = 16.sp, + ) + } + + StyledSwitch( + checked = conversationalAwarenessPauseMusicEnabled, + onCheckedChange = { + updateConversationalAwarenessPauseMusic(it) + }, + ) + } + + Row( + modifier = Modifier + .fillMaxWidth() + .height(85.sp.value.dp) + .background( + shape = RoundedCornerShape(14.dp), + color = Color.Transparent + ) + .clickable( + indication = null, + interactionSource = remember { MutableInteractionSource() } + ) { + updateRelativeConversationalAwarenessVolume(!relativeConversationalAwarenessVolumeEnabled) + }, + verticalAlignment = Alignment.CenterVertically + ) { + Column( + modifier = Modifier + .weight(1f) + .padding(end = 4.dp) + ) { + Text( + text = stringResource(R.string.relative_conversational_awareness_volume), + fontSize = 16.sp, + color = textColor + ) + Spacer(modifier = Modifier.height(4.dp)) + Text( + text = stringResource(R.string.relative_conversational_awareness_volume_description), + fontSize = 14.sp, + color = textColor.copy(0.6f), + lineHeight = 16.sp, + ) + } + + StyledSwitch( + checked = relativeConversationalAwarenessVolumeEnabled, + onCheckedChange = { + updateRelativeConversationalAwarenessVolume(it) + } + ) + } + + val activeTrackColor = if (isDarkTheme) Color(0xFF007AFF) else Color(0xFF3C6DF5) + + Slider( + value = sliderValue.floatValue, + onValueChange = { + sliderValue.floatValue = it + }, + valueRange = 10f..85f, + onValueChangeFinished = { + sliderValue.floatValue = sliderValue.floatValue.roundToInt().toFloat() + }, + 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(((sliderValue.floatValue - 10) * 100) /7500) + .height(4.dp) + .background(if (conversationalAwarenessPauseMusicEnabled) trackColor else activeTrackColor, RoundedCornerShape(4.dp)) + ) + } + } + ) + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text( + text = "10%", + style = TextStyle( + fontSize = 14.sp, + fontWeight = FontWeight.Light, + color = labelTextColor + ), + modifier = Modifier.padding(start = 4.dp) + ) + Text( + text = "85%", + style = TextStyle( + fontSize = 14.sp, + fontWeight = FontWeight.Light, + color = labelTextColor + ), + modifier = Modifier.padding(end = 4.dp) + ) + } + } + } + } +} + +@Preview +@Composable +fun AppSettingsScreenPreview() { + AppSettingsScreen(navController = NavController(LocalContext.current)) +} \ No newline at end of file diff --git a/android/app/src/main/java/me/kavishdevar/aln/screens/RenameScreen.kt b/android/app/src/main/java/me/kavishdevar/aln/screens/RenameScreen.kt new file mode 100644 index 0000000..40be85a --- /dev/null +++ b/android/app/src/main/java/me/kavishdevar/aln/screens/RenameScreen.kt @@ -0,0 +1,193 @@ +/* + * AirPods like Normal (ALN) - Bringing Apple-only features to Linux and Android for seamless AirPods functionality! + * + * Copyright (C) 2024 Kavish Devar + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package me.kavishdevar.aln.screens + +import android.content.Context +import androidx.compose.foundation.background +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.BasicTextField +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.KeyboardArrowLeft +import androidx.compose.material.icons.filled.Clear +import androidx.compose.material3.CenterAlignedTopAppBar +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.material3.TopAppBarDefaults +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.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.scale +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.Font +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.navigation.NavController +import me.kavishdevar.aln.R +import me.kavishdevar.aln.services.ServiceManager + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun RenameScreen(navController: NavController) { + val sharedPreferences = LocalContext.current.getSharedPreferences("settings", Context.MODE_PRIVATE) + val isDarkTheme = isSystemInDarkTheme() + val name = remember { mutableStateOf(sharedPreferences.getString("name", "") ?: "") } + Scaffold( + topBar = { + CenterAlignedTopAppBar( + title = { + Text( + text = stringResource(R.string.name), + fontFamily = FontFamily(Font(R.font.sf_pro)), + ) + }, + navigationIcon = { + TextButton( + onClick = { + navController.popBackStack() + }, + shape = RoundedCornerShape(8.dp), + ) { + Icon( + Icons.AutoMirrored.Filled.KeyboardArrowLeft, + contentDescription = "Back", + tint = if (isDarkTheme) Color(0xFF007AFF) else Color(0xFF3C6DF5), + modifier = Modifier.scale(1.5f) + ) + Text( + text = name.value, + style = TextStyle( + fontSize = 18.sp, + fontWeight = FontWeight.Medium, + color = if (isDarkTheme) Color(0xFF007AFF) else Color(0xFF3C6DF5), + fontFamily = FontFamily(Font(R.font.sf_pro)) + ), + ) + } + }, + colors = TopAppBarDefaults.topAppBarColors( + containerColor = Color.Transparent + ) + ) + }, + containerColor = if (isSystemInDarkTheme()) Color(0xFF000000) + else Color(0xFFF2F2F7), + ) { paddingValues -> + Column ( + modifier = Modifier + .fillMaxSize() + .padding(paddingValues = paddingValues) + .padding(horizontal = 16.dp) + .padding(top = 8.dp) + ) { + var isFocused by remember { mutableStateOf(false) } + val isDarkTheme = isSystemInDarkTheme() + + val backgroundColor = if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFFFFFFF) + val textColor = if (isDarkTheme) Color.White else Color.Black + val cursorColor = if (isFocused) { // Show cursor only when focused + if (isDarkTheme) Color.White else Color.Black + } else { + Color.Transparent + } + + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxWidth() + .height(55.dp) + .background( + backgroundColor, + RoundedCornerShape(14.dp) + ) + .padding(horizontal = 16.dp, vertical = 8.dp) + ) { + BasicTextField( + value = name.value, + onValueChange = { + name.value = it + sharedPreferences.edit().putString("name", it).apply() + ServiceManager.getService()?.setName(it) + }, + textStyle = TextStyle( + color = textColor, + fontSize = 16.sp, + ), + singleLine = true, + cursorBrush = SolidColor(cursorColor), + decorationBox = { innerTextField -> + Row( + verticalAlignment = Alignment.CenterVertically, + ) { + Row( + modifier = Modifier + .weight(1f) + ) { + innerTextField() + } + IconButton( + onClick = { + name.value = "" + sharedPreferences.edit().putString("name", "").apply() + ServiceManager.getService()?.setName("") + } + ) { + Icon( + Icons.Default.Clear, + contentDescription = "Clear", + tint = if (isDarkTheme) Color.White else Color.Black + ) + } + } + }, + modifier = Modifier + .fillMaxWidth() + .padding(start = 8.dp) + ) + } + } + } +} + +@Preview +@Composable +fun RenameScreenPreview() { + RenameScreen(navController = NavController(LocalContext.current)) +} \ 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 1e8dcb1..e31a8ce 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 @@ -36,7 +36,9 @@ import android.content.IntentFilter import android.media.AudioManager import android.os.Binder import android.os.Build +import android.os.Handler import android.os.IBinder +import android.os.Looper import android.os.ParcelUuid import android.util.Log import android.widget.RemoteViews @@ -473,6 +475,15 @@ class AirPodsService: Service() { it.outputStream.write(Enums.REQUEST_NOTIFICATIONS.value) it.outputStream.flush() delay(200) + // just in case this doesn't work, send all three after 5 seconds again + Handler(Looper.getMainLooper()).postDelayed({ + it.outputStream.write(Enums.HANDSHAKE.value) + it.outputStream.flush() + it.outputStream.write(Enums.SET_SPECIFIC_FEATURES.value) + it.outputStream.flush() + it.outputStream.write(Enums.REQUEST_NOTIFICATIONS.value) + it.outputStream.flush() + }, 5000) sendBroadcast( Intent(AirPodsNotifications.Companion.AIRPODS_CONNECTED) .putExtra("device", device) @@ -482,7 +493,7 @@ class AirPodsService: Service() { socket.let { val audioManager = this@AirPodsService.getSystemService(AUDIO_SERVICE) as AudioManager - MediaController.initialize(audioManager) + MediaController.initialize(audioManager, this@AirPodsService.getSharedPreferences("settings", MODE_PRIVATE)) val buffer = ByteArray(1024) val bytesRead = it.inputStream.read(buffer) var data: ByteArray = byteArrayOf() @@ -889,6 +900,7 @@ class AirPodsService: Service() { socket.outputStream?.write(bytes) socket.outputStream?.flush() val hex = bytes.joinToString(" ") { "%02X".format(it) } + updateNotificationContent(true, name, batteryNotification.getBattery()) Log.d("AirPodsService", "setName: $name, sent packet: $hex") } diff --git a/android/app/src/main/java/me/kavishdevar/aln/utils/MediaController.kt b/android/app/src/main/java/me/kavishdevar/aln/utils/MediaController.kt index 1dd4ad5..cfb44ff 100644 --- a/android/app/src/main/java/me/kavishdevar/aln/utils/MediaController.kt +++ b/android/app/src/main/java/me/kavishdevar/aln/utils/MediaController.kt @@ -18,6 +18,7 @@ package me.kavishdevar.aln.utils +import android.content.SharedPreferences import android.media.AudioManager import android.media.AudioPlaybackConfiguration import android.os.Handler @@ -30,10 +31,35 @@ object MediaController { private lateinit var audioManager: AudioManager var iPausedTheMedia = false var userPlayedTheMedia = false + private lateinit var sharedPreferences: SharedPreferences private val handler = Handler(Looper.getMainLooper()) - fun initialize(audioManager: AudioManager) { + private var relativeVolume: Boolean = false + private var conversationalAwarenessVolume: Int = 1/12 + private var conversationalAwarenessPauseMusic: Boolean = false + + fun initialize(audioManager: AudioManager, sharedPreferences: SharedPreferences) { this.audioManager = audioManager + this.sharedPreferences = sharedPreferences + + relativeVolume = sharedPreferences.getBoolean("relative_conversational_awareness_volume", false) + conversationalAwarenessVolume = sharedPreferences.getInt("conversational_awareness_volume", 100/12) + conversationalAwarenessPauseMusic = sharedPreferences.getBoolean("conversational_awareness_pause_music", false) + + sharedPreferences.registerOnSharedPreferenceChangeListener { _, key -> + when (key) { + "relative_conversational_awareness_volume" -> { + relativeVolume = sharedPreferences.getBoolean("relative_conversational_awareness_volume", false) + } + "conversational_awareness_volume" -> { + conversationalAwarenessVolume = sharedPreferences.getInt("conversational_awareness_volume", 100/12) + } + "conversational_awareness_pause_music" -> { + conversationalAwarenessPauseMusic = sharedPreferences.getBoolean("conversational_awareness_pause_music", false) + } + } + } + audioManager.registerAudioPlaybackCallback(cb, null) } @@ -46,15 +72,15 @@ object MediaController { handler.postDelayed({ iPausedTheMedia = !audioManager.isMusicActive userPlayedTheMedia = audioManager.isMusicActive - }, 7) // i have no idea why, but android sends a pause event a hundred times after the user does something. + }, 7) // i have no idea why android sends an event a hundred times after the user does something. } } } @Synchronized - fun sendPause() { + fun sendPause(force: Boolean = false) { Log.d("MediaController", "Sending pause with iPausedTheMedia: $iPausedTheMedia, userPlayedTheMedia: $userPlayedTheMedia") - if (audioManager.isMusicActive && !userPlayedTheMedia) { + if ((audioManager.isMusicActive && !userPlayedTheMedia) || force) { iPausedTheMedia = true userPlayedTheMedia = false audioManager.dispatchMediaKeyEvent( @@ -99,8 +125,11 @@ object MediaController { if (initialVolume == null) { initialVolume = audioManager.getStreamVolume(AudioManager.STREAM_MUSIC) Log.d("MediaController", "Initial Volume Set: $initialVolume") - val targetVolume = audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC) * 1 / 12 - smoothVolumeTransition(initialVolume!!, targetVolume) + val targetVolume = if (relativeVolume) initialVolume!! * conversationalAwarenessVolume * 1/100 else if ( initialVolume!! > audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC) * conversationalAwarenessVolume * 1/100) audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC) * conversationalAwarenessVolume * 1/100 else initialVolume!! + smoothVolumeTransition(initialVolume!!, targetVolume.toInt()) + if (conversationalAwarenessPauseMusic) { + sendPause(force = true) + } } Log.d("MediaController", "Initial Volume: $initialVolume") } @@ -110,13 +139,16 @@ object MediaController { Log.d("MediaController", "Stopping speaking, initialVolume: $initialVolume") if (initialVolume != null) { smoothVolumeTransition(audioManager.getStreamVolume(AudioManager.STREAM_MUSIC), initialVolume!!) + if (conversationalAwarenessPauseMusic) { + sendPlay() + } initialVolume = null } } private fun smoothVolumeTransition(fromVolume: Int, toVolume: Int) { val step = if (fromVolume < toVolume) 1 else -1 - val delay = 50L // 50 milliseconds delay between each step + val delay = 50L var currentVolume = fromVolume handler.post(object : Runnable { diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml index 3b28a1d..8128cdf 100644 --- a/android/app/src/main/res/values/strings.xml +++ b/android/app/src/main/res/values/strings.xml @@ -1,5 +1,42 @@ - ALN - GATT Testing + ALN + GATT Testing See your AirPods battery status right from your home screen! - \ No newline at end of file + Accessibility + Tone Volume + Audio + Adaptive Audio + Adaptive audio dynamically responds to your environment and cancels or allows external noise. You can customize Adaptive Audio to allow more or less noise. + Buds + Case + Test + Name + Noise Control + Off + Transparency + Adaptive + Noise Cancellation + Press and Hold AirPods + Left + Right + Adjusts the volume of media in response to your environment + Conversational Awareness + Lowers media volume and reduces background noise when you start speaking to other people. + Personalized Volume + Adjusts the volume of media in response to your environment. + Less Noise + More Noise + Noise Cancellation with Single AirPod + Allow AirPods to be put in noise cancellation mode when only one AirPods is in your ear. + Volume Control + Adjust the volume by swiping up or down on the sensor located on the AirPods Pro stem. + AirPods not connected + Please connect your AirPods to access settings. If you\'re stuck here, then try reopening the app again after closing it from the recents.\n(DO NOT KILL THE APP!) + Back + App Settings + Conversational Awareness + Relative volume + Reduces to a percentage of the current volume instead of the maximum volume. + Pause Music + When you start speaking, music will be paused. +