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.
+