add an app settings page for customizing functions and un-hardcode strings

This commit is contained in:
Kavish Devar
2025-01-09 03:25:32 +05:30
parent fc0475e2c0
commit f5cc47b53c
14 changed files with 794 additions and 95 deletions

View File

@@ -55,8 +55,10 @@ import androidx.navigation.compose.rememberNavController
import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.rememberMultiplePermissionsState import com.google.accompanist.permissions.rememberMultiplePermissionsState
import me.kavishdevar.aln.screens.AirPodsSettingsScreen import me.kavishdevar.aln.screens.AirPodsSettingsScreen
import me.kavishdevar.aln.screens.AppSettingsScreen
import me.kavishdevar.aln.screens.DebugScreen import me.kavishdevar.aln.screens.DebugScreen
import me.kavishdevar.aln.screens.LongPress import me.kavishdevar.aln.screens.LongPress
import me.kavishdevar.aln.screens.RenameScreen
import me.kavishdevar.aln.services.AirPodsService import me.kavishdevar.aln.services.AirPodsService
import me.kavishdevar.aln.ui.theme.ALNTheme import me.kavishdevar.aln.ui.theme.ALNTheme
import me.kavishdevar.aln.utils.AirPodsNotifications import me.kavishdevar.aln.utils.AirPodsNotifications
@@ -158,7 +160,7 @@ fun Main() {
NavHost( NavHost(
navController = navController, navController = navController,
startDestination = "settings", startDestination = "app_settings",
enterTransition = { slideInHorizontally(initialOffsetX = { it }, animationSpec = tween(300)) }, enterTransition = { slideInHorizontally(initialOffsetX = { it }, animationSpec = tween(300)) },
exitTransition = { slideOutHorizontally(targetOffsetX = { -it }, animationSpec = tween(300)) }, exitTransition = { slideOutHorizontally(targetOffsetX = { -it }, animationSpec = tween(300)) },
popEnterTransition = { slideInHorizontally(initialOffsetX = { -it }, animationSpec = tween(300)) }, popEnterTransition = { slideInHorizontally(initialOffsetX = { -it }, animationSpec = tween(300)) },
@@ -183,6 +185,12 @@ fun Main() {
name = navBackStackEntry.arguments?.getString("bud")!! name = navBackStackEntry.arguments?.getString("bud")!!
) )
} }
composable("rename") { navBackStackEntry ->
RenameScreen(navController)
}
composable("app_settings") {
AppSettingsScreen(navController)
}
} }
serviceConnection = remember { serviceConnection = remember {

View File

@@ -31,11 +31,13 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import me.kavishdevar.aln.R
import me.kavishdevar.aln.services.AirPodsService import me.kavishdevar.aln.services.AirPodsService
@Composable @Composable
@@ -44,7 +46,7 @@ fun AccessibilitySettings(service: AirPodsService, sharedPreferences: SharedPref
val textColor = if (isDarkTheme) Color.White else Color.Black val textColor = if (isDarkTheme) Color.White else Color.Black
Text( Text(
text = "ACCESSIBILITY", text = stringResource(R.string.accessibility).uppercase(),
style = TextStyle( style = TextStyle(
fontSize = 14.sp, fontSize = 14.sp,
fontWeight = FontWeight.Light, fontWeight = FontWeight.Light,
@@ -68,7 +70,7 @@ fun AccessibilitySettings(service: AirPodsService, sharedPreferences: SharedPref
.padding(12.dp) .padding(12.dp)
) { ) {
Text( Text(
text = "Tone Volume", text = stringResource(R.string.tone_volume),
modifier = Modifier modifier = Modifier
.padding(end = 8.dp, bottom = 2.dp, start = 2.dp) .padding(end = 8.dp, bottom = 2.dp, start = 2.dp)
.fillMaxWidth(), .fillMaxWidth(),
@@ -95,4 +97,4 @@ fun AccessibilitySettings(service: AirPodsService, sharedPreferences: SharedPref
@Composable @Composable
fun AccessibilitySettingsPreview() { fun AccessibilitySettingsPreview() {
AccessibilitySettings(service = AirPodsService(), sharedPreferences = LocalContext.current.getSharedPreferences("preview", Context.MODE_PRIVATE)) AccessibilitySettings(service = AirPodsService(), sharedPreferences = LocalContext.current.getSharedPreferences("preview", Context.MODE_PRIVATE))
} }

View File

@@ -31,11 +31,13 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import me.kavishdevar.aln.R
import me.kavishdevar.aln.services.AirPodsService import me.kavishdevar.aln.services.AirPodsService
@Composable @Composable
@@ -44,7 +46,7 @@ fun AudioSettings(service: AirPodsService, sharedPreferences: SharedPreferences)
val textColor = if (isDarkTheme) Color.White else Color.Black val textColor = if (isDarkTheme) Color.White else Color.Black
Text( Text(
text = "AUDIO", text = stringResource(R.string.audio).uppercase(),
style = TextStyle( style = TextStyle(
fontSize = 14.sp, fontSize = 14.sp,
fontWeight = FontWeight.Light, fontWeight = FontWeight.Light,
@@ -72,7 +74,7 @@ fun AudioSettings(service: AirPodsService, sharedPreferences: SharedPreferences)
.padding(horizontal = 8.dp, vertical = 10.dp) .padding(horizontal = 8.dp, vertical = 10.dp)
) { ) {
Text( Text(
text = "Adaptive Audio", text = stringResource(R.string.adaptive_audio),
modifier = Modifier modifier = Modifier
.padding(end = 8.dp, bottom = 2.dp, start = 2.dp) .padding(end = 8.dp, bottom = 2.dp, start = 2.dp)
.fillMaxWidth(), .fillMaxWidth(),
@@ -82,7 +84,7 @@ fun AudioSettings(service: AirPodsService, sharedPreferences: SharedPreferences)
) )
) )
Text( 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 modifier = Modifier
.padding(bottom = 8.dp, top = 2.dp) .padding(bottom = 8.dp, top = 2.dp)
.padding(end = 2.dp, start = 2.dp) .padding(end = 2.dp, start = 2.dp)
@@ -102,4 +104,4 @@ fun AudioSettings(service: AirPodsService, sharedPreferences: SharedPreferences)
@Composable @Composable
fun AudioSettingsPreview() { fun AudioSettingsPreview() {
AudioSettings(service = AirPodsService(), sharedPreferences = LocalContext.current.getSharedPreferences("preview", Context.MODE_PRIVATE)) AudioSettings(service = AirPodsService(), sharedPreferences = LocalContext.current.getSharedPreferences("preview", Context.MODE_PRIVATE))
} }

View File

@@ -41,6 +41,7 @@ import androidx.compose.ui.draw.scale
import androidx.compose.ui.graphics.ImageBitmap import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.imageResource import androidx.compose.ui.res.imageResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import me.kavishdevar.aln.R import me.kavishdevar.aln.R
@@ -110,7 +111,7 @@ fun BatteryView(service: AirPodsService, preview: Boolean = false) {
) { ) {
Image ( Image (
bitmap = ImageBitmap.imageResource(R.drawable.pro_2_buds), bitmap = ImageBitmap.imageResource(R.drawable.pro_2_buds),
contentDescription = "Buds", contentDescription = stringResource(R.string.buds),
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.scale(0.80f) .scale(0.80f)
@@ -163,7 +164,7 @@ fun BatteryView(service: AirPodsService, preview: Boolean = false) {
Image( Image(
bitmap = ImageBitmap.imageResource(R.drawable.pro_2_case), bitmap = ImageBitmap.imageResource(R.drawable.pro_2_case),
contentDescription = "Case", contentDescription = stringResource(R.string.case_alt),
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.scale(1.25f) .scale(1.25f)
@@ -181,4 +182,4 @@ fun BatteryView(service: AirPodsService, preview: Boolean = false) {
@Composable @Composable
fun BatteryViewPreview() { fun BatteryViewPreview() {
BatteryView(AirPodsService(), preview = true) BatteryView(AirPodsService(), preview = true)
} }

View File

@@ -19,14 +19,20 @@
package me.kavishdevar.aln.composables package me.kavishdevar.aln.composables
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicTextField 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.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue 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.Color
import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.navigation.NavController
import androidx.navigation.compose.rememberNavController
@Composable @Composable
fun StyledTextField( fun NameField(
name: String, name: String,
value: String, value: String,
onValueChange: (String) -> Unit navController: NavController
) { ) {
var isFocused by remember { mutableStateOf(false) } var isFocused by remember { mutableStateOf(false) }
@@ -61,53 +70,72 @@ fun StyledTextField(
Color.Transparent Color.Transparent
} }
Row( Box (
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier modifier = Modifier
.fillMaxWidth() .clickable(
.height(55.dp) onClick = {
.background( navController.navigate("rename")
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()
} }
}, )
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(start = 8.dp) .height(55.dp)
.onFocusChanged { focusState -> .background(
isFocused = focusState.isFocused // Update focus state 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 @Preview
@Composable @Composable
fun StyledTextFieldPreview() { fun StyledTextFieldPreview() {
StyledTextField(name = "Name", value = "AirPods Pro", onValueChange = {}) NameField(name = "Name", value = "AirPods Pro", rememberNavController())
} }

View File

@@ -39,7 +39,6 @@ import androidx.compose.material3.Text
import androidx.compose.material3.VerticalDivider import androidx.compose.material3.VerticalDivider
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember 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.graphics.ImageBitmap
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.imageResource import androidx.compose.ui.res.imageResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
@@ -81,6 +81,7 @@ fun NoiseControlSettings(service: AirPodsService) {
sharedPreferences.unregisterOnSharedPreferenceChangeListener(preferenceChangeListener) sharedPreferences.unregisterOnSharedPreferenceChangeListener(preferenceChangeListener)
} }
} }
val isDarkTheme = isSystemInDarkTheme() val isDarkTheme = isSystemInDarkTheme()
val backgroundColor = if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFE3E3E8) val backgroundColor = if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFE3E3E8)
val textColor = if (isDarkTheme) Color.White else Color.Black val textColor = if (isDarkTheme) Color.White else Color.Black
@@ -151,12 +152,12 @@ fun NoiseControlSettings(service: AirPodsService) {
context.registerReceiver(noiseControlReceiver, noiseControlIntentFilter) context.registerReceiver(noiseControlReceiver, noiseControlIntentFilter)
} }
Text( Text(// all caps
text = "NOISE CONTROL", text = stringResource(R.string.noise_control).uppercase(),
style = TextStyle( style = TextStyle(
fontSize = 14.sp, fontSize = 14.sp,
fontWeight = FontWeight.Light, fontWeight = FontWeight.Light,
color = textColor.copy(alpha = 0.6f) color = textColor.copy(alpha = 0.6f),
), ),
modifier = Modifier.padding(8.dp, bottom = 2.dp) modifier = Modifier.padding(8.dp, bottom = 2.dp)
) )
@@ -238,7 +239,7 @@ fun NoiseControlSettings(service: AirPodsService) {
) { ) {
if (offListeningMode.value) { if (offListeningMode.value) {
Text( Text(
text = "Off", text = stringResource(R.string.off),
style = TextStyle(fontSize = 12.sp, color = textColor), style = TextStyle(fontSize = 12.sp, color = textColor),
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
@@ -246,21 +247,21 @@ fun NoiseControlSettings(service: AirPodsService) {
) )
} }
Text( Text(
text = "Transparency", text = stringResource(R.string.transparency),
style = TextStyle(fontSize = 12.sp, color = textColor), style = TextStyle(fontSize = 12.sp, color = textColor),
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
modifier = Modifier.weight(1f) modifier = Modifier.weight(1f)
) )
Text( Text(
text = "Adaptive", text = stringResource(R.string.adaptive),
style = TextStyle(fontSize = 12.sp, color = textColor), style = TextStyle(fontSize = 12.sp, color = textColor),
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
modifier = Modifier.weight(1f) modifier = Modifier.weight(1f)
) )
Text( Text(
text = "Noise Cancellation", text = stringResource(R.string.noise_cancellation),
style = TextStyle(fontSize = 12.sp, color = textColor), style = TextStyle(fontSize = 12.sp, color = textColor),
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,

View File

@@ -40,6 +40,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.Font import androidx.compose.ui.text.font.Font
import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontFamily
@@ -56,7 +57,7 @@ fun PressAndHoldSettings(navController: NavController) {
val textColor = if (isDarkTheme) Color.White else Color.Black val textColor = if (isDarkTheme) Color.White else Color.Black
Text( Text(
text = "PRESS AND HOLD AIRPODS", text = stringResource(R.string.press_and_hold_airpods).uppercase(),
style = TextStyle( style = TextStyle(
fontSize = 14.sp, fontSize = 14.sp,
fontWeight = FontWeight.Light, fontWeight = FontWeight.Light,
@@ -95,7 +96,7 @@ fun PressAndHoldSettings(navController: NavController) {
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
Text( Text(
text = "Left", text = stringResource(R.string.left),
style = TextStyle( style = TextStyle(
fontSize = 18.sp, fontSize = 18.sp,
color = textColor, color = textColor,
@@ -105,7 +106,7 @@ fun PressAndHoldSettings(navController: NavController) {
Spacer(modifier = Modifier.weight(1f)) Spacer(modifier = Modifier.weight(1f))
Text( Text(
// TODO: Implement voice assistant on long press; for now, it's noise control // TODO: Implement voice assistant on long press; for now, it's noise control
text = "Noise Control", text = stringResource(R.string.noise_control),
style = TextStyle( style = TextStyle(
fontSize = 18.sp, fontSize = 18.sp,
color = textColor.copy(alpha = 0.6f), color = textColor.copy(alpha = 0.6f),
@@ -152,7 +153,7 @@ fun PressAndHoldSettings(navController: NavController) {
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
Text( Text(
text = "Right", text = stringResource(R.string.right),
style = TextStyle( style = TextStyle(
fontSize = 18.sp, fontSize = 18.sp,
color = textColor, color = textColor,
@@ -162,7 +163,7 @@ fun PressAndHoldSettings(navController: NavController) {
Spacer(modifier = Modifier.weight(1f)) Spacer(modifier = Modifier.weight(1f))
Text( Text(
// TODO: Implement voice assistant on long press; for now, it's noise control // TODO: Implement voice assistant on long press; for now, it's noise control
text = "Noise Control", text = stringResource(R.string.noise_control),
style = TextStyle( style = TextStyle(
fontSize = 18.sp, fontSize = 18.sp,
color = textColor.copy(alpha = 0.6f), color = textColor.copy(alpha = 0.6f),
@@ -189,4 +190,4 @@ fun PressAndHoldSettings(navController: NavController) {
@Composable @Composable
fun PressAndHoldSettingsPreview() { fun PressAndHoldSettingsPreview() {
PressAndHoldSettings(navController = NavController(LocalContext.current)) PressAndHoldSettings(navController = NavController(LocalContext.current))
} }

View File

@@ -42,14 +42,23 @@ import androidx.compose.ui.unit.dp
@Composable @Composable
fun StyledSwitch( fun StyledSwitch(
checked: Boolean, checked: Boolean,
onCheckedChange: (Boolean) -> Unit onCheckedChange: (Boolean) -> Unit,
enabled: Boolean = true,
) { ) {
val isDarkTheme = isSystemInDarkTheme() val isDarkTheme = isSystemInDarkTheme()
val thumbColor = Color.White 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") val thumbOffsetX by animateDpAsState(targetValue = if (checked) 20.dp else 0.dp, label = "Test")
Box( Box(
@@ -63,11 +72,11 @@ fun StyledSwitch(
) { ) {
Box( Box(
modifier = Modifier modifier = Modifier
.offset(x = thumbOffsetX) // Animate the offset for smooth transition .offset(x = thumbOffsetX)
.size(27.dp) .size(27.dp)
.clip(CircleShape) .clip(CircleShape)
.background(thumbColor) // Dynamic thumb color .background(thumbColor)
.clickable { onCheckedChange(!checked) } // Make the switch clickable .clickable { if (enabled) onCheckedChange(!checked) }
) )
} }
} }

View File

@@ -22,6 +22,7 @@ import android.annotation.SuppressLint
import android.bluetooth.BluetoothDevice import android.bluetooth.BluetoothDevice
import android.content.Context.MODE_PRIVATE import android.content.Context.MODE_PRIVATE
import android.content.Intent import android.content.Intent
import android.content.SharedPreferences
import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column 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.rememberScrollState
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons 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.CenterAlignedTopAppBar
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
@@ -43,6 +44,7 @@ import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf 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.geometry.Offset
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.Font import androidx.compose.ui.text.font.Font
import androidx.compose.ui.text.font.FontFamily 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.AudioSettings
import me.kavishdevar.aln.composables.BatteryView import me.kavishdevar.aln.composables.BatteryView
import me.kavishdevar.aln.composables.IndependentToggle import me.kavishdevar.aln.composables.IndependentToggle
import me.kavishdevar.aln.composables.NameField
import me.kavishdevar.aln.composables.NavigationButton import me.kavishdevar.aln.composables.NavigationButton
import me.kavishdevar.aln.composables.NoiseControlSettings import me.kavishdevar.aln.composables.NoiseControlSettings
import me.kavishdevar.aln.composables.PressAndHoldSettings import me.kavishdevar.aln.composables.PressAndHoldSettings
import me.kavishdevar.aln.composables.StyledTextField
import me.kavishdevar.aln.services.AirPodsService import me.kavishdevar.aln.services.AirPodsService
import me.kavishdevar.aln.services.ServiceManager
import me.kavishdevar.aln.ui.theme.ALNTheme import me.kavishdevar.aln.ui.theme.ALNTheme
import me.kavishdevar.aln.utils.AirPodsNotifications 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 verticalScrollState = rememberScrollState()
val hazeState = remember { HazeState() } val hazeState = remember { HazeState() }
@@ -150,10 +169,9 @@ fun AirPodsSettingsScreen(dev: BluetoothDevice?, service: AirPodsService,
containerColor = Color.Transparent containerColor = Color.Transparent
), ),
actions = { actions = {
val context = LocalContext.current
IconButton( IconButton(
onClick = { onClick = {
ServiceManager.restartService(context) navController.navigate("app_settings")
}, },
colors = IconButtonDefaults.iconButtonColors( colors = IconButtonDefaults.iconButtonColors(
containerColor = Color.Transparent, containerColor = Color.Transparent,
@@ -161,7 +179,7 @@ fun AirPodsSettingsScreen(dev: BluetoothDevice?, service: AirPodsService,
) )
) { ) {
Icon( Icon(
imageVector = Icons.Default.Refresh, imageVector = Icons.Default.Settings,
contentDescription = "Settings", contentDescription = "Settings",
) )
} }
@@ -199,14 +217,10 @@ fun AirPodsSettingsScreen(dev: BluetoothDevice?, service: AirPodsService,
Spacer(modifier = Modifier.height(32.dp)) Spacer(modifier = Modifier.height(32.dp))
StyledTextField( NameField(
name = "Name", name = stringResource(R.string.name),
value = deviceName.text, value = deviceName.text,
onValueChange = { navController = navController
deviceName = TextFieldValue(it)
sharedPreferences.edit().putString("name", it).apply()
service.setName(it)
}
) )
Spacer(modifier = Modifier.height(32.dp)) Spacer(modifier = Modifier.height(32.dp))

View File

@@ -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 <https://www.gnu.org/licenses/>.
*/
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))
}

View File

@@ -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 <https://www.gnu.org/licenses/>.
*/
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))
}

View File

@@ -36,7 +36,9 @@ import android.content.IntentFilter
import android.media.AudioManager import android.media.AudioManager
import android.os.Binder import android.os.Binder
import android.os.Build import android.os.Build
import android.os.Handler
import android.os.IBinder import android.os.IBinder
import android.os.Looper
import android.os.ParcelUuid import android.os.ParcelUuid
import android.util.Log import android.util.Log
import android.widget.RemoteViews import android.widget.RemoteViews
@@ -473,6 +475,15 @@ class AirPodsService: Service() {
it.outputStream.write(Enums.REQUEST_NOTIFICATIONS.value) it.outputStream.write(Enums.REQUEST_NOTIFICATIONS.value)
it.outputStream.flush() it.outputStream.flush()
delay(200) 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( sendBroadcast(
Intent(AirPodsNotifications.Companion.AIRPODS_CONNECTED) Intent(AirPodsNotifications.Companion.AIRPODS_CONNECTED)
.putExtra("device", device) .putExtra("device", device)
@@ -482,7 +493,7 @@ class AirPodsService: Service() {
socket.let { socket.let {
val audioManager = val audioManager =
this@AirPodsService.getSystemService(AUDIO_SERVICE) as 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 buffer = ByteArray(1024)
val bytesRead = it.inputStream.read(buffer) val bytesRead = it.inputStream.read(buffer)
var data: ByteArray = byteArrayOf() var data: ByteArray = byteArrayOf()
@@ -889,6 +900,7 @@ class AirPodsService: Service() {
socket.outputStream?.write(bytes) socket.outputStream?.write(bytes)
socket.outputStream?.flush() socket.outputStream?.flush()
val hex = bytes.joinToString(" ") { "%02X".format(it) } val hex = bytes.joinToString(" ") { "%02X".format(it) }
updateNotificationContent(true, name, batteryNotification.getBattery())
Log.d("AirPodsService", "setName: $name, sent packet: $hex") Log.d("AirPodsService", "setName: $name, sent packet: $hex")
} }

View File

@@ -18,6 +18,7 @@
package me.kavishdevar.aln.utils package me.kavishdevar.aln.utils
import android.content.SharedPreferences
import android.media.AudioManager import android.media.AudioManager
import android.media.AudioPlaybackConfiguration import android.media.AudioPlaybackConfiguration
import android.os.Handler import android.os.Handler
@@ -30,10 +31,35 @@ object MediaController {
private lateinit var audioManager: AudioManager private lateinit var audioManager: AudioManager
var iPausedTheMedia = false var iPausedTheMedia = false
var userPlayedTheMedia = false var userPlayedTheMedia = false
private lateinit var sharedPreferences: SharedPreferences
private val handler = Handler(Looper.getMainLooper()) 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.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) audioManager.registerAudioPlaybackCallback(cb, null)
} }
@@ -46,15 +72,15 @@ object MediaController {
handler.postDelayed({ handler.postDelayed({
iPausedTheMedia = !audioManager.isMusicActive iPausedTheMedia = !audioManager.isMusicActive
userPlayedTheMedia = 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 @Synchronized
fun sendPause() { fun sendPause(force: Boolean = false) {
Log.d("MediaController", "Sending pause with iPausedTheMedia: $iPausedTheMedia, userPlayedTheMedia: $userPlayedTheMedia") Log.d("MediaController", "Sending pause with iPausedTheMedia: $iPausedTheMedia, userPlayedTheMedia: $userPlayedTheMedia")
if (audioManager.isMusicActive && !userPlayedTheMedia) { if ((audioManager.isMusicActive && !userPlayedTheMedia) || force) {
iPausedTheMedia = true iPausedTheMedia = true
userPlayedTheMedia = false userPlayedTheMedia = false
audioManager.dispatchMediaKeyEvent( audioManager.dispatchMediaKeyEvent(
@@ -99,8 +125,11 @@ object MediaController {
if (initialVolume == null) { if (initialVolume == null) {
initialVolume = audioManager.getStreamVolume(AudioManager.STREAM_MUSIC) initialVolume = audioManager.getStreamVolume(AudioManager.STREAM_MUSIC)
Log.d("MediaController", "Initial Volume Set: $initialVolume") Log.d("MediaController", "Initial Volume Set: $initialVolume")
val targetVolume = audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC) * 1 / 12 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) smoothVolumeTransition(initialVolume!!, targetVolume.toInt())
if (conversationalAwarenessPauseMusic) {
sendPause(force = true)
}
} }
Log.d("MediaController", "Initial Volume: $initialVolume") Log.d("MediaController", "Initial Volume: $initialVolume")
} }
@@ -110,13 +139,16 @@ object MediaController {
Log.d("MediaController", "Stopping speaking, initialVolume: $initialVolume") Log.d("MediaController", "Stopping speaking, initialVolume: $initialVolume")
if (initialVolume != null) { if (initialVolume != null) {
smoothVolumeTransition(audioManager.getStreamVolume(AudioManager.STREAM_MUSIC), initialVolume!!) smoothVolumeTransition(audioManager.getStreamVolume(AudioManager.STREAM_MUSIC), initialVolume!!)
if (conversationalAwarenessPauseMusic) {
sendPlay()
}
initialVolume = null initialVolume = null
} }
} }
private fun smoothVolumeTransition(fromVolume: Int, toVolume: Int) { private fun smoothVolumeTransition(fromVolume: Int, toVolume: Int) {
val step = if (fromVolume < toVolume) 1 else -1 val step = if (fromVolume < toVolume) 1 else -1
val delay = 50L // 50 milliseconds delay between each step val delay = 50L
var currentVolume = fromVolume var currentVolume = fromVolume
handler.post(object : Runnable { handler.post(object : Runnable {

View File

@@ -1,5 +1,42 @@
<resources> <resources>
<string name="app_name">ALN</string> <string name="app_name" translatable="false">ALN</string>
<string name="title_activity_custom_device">GATT Testing</string> <string name="title_activity_custom_device" translatable="false">GATT Testing</string>
<string name="app_widget_description">See your AirPods battery status right from your home screen!</string> <string name="app_widget_description">See your AirPods battery status right from your home screen!</string>
</resources> <string name="accessibility">Accessibility</string>
<string name="tone_volume">Tone Volume</string>
<string name="audio">Audio</string>
<string name="adaptive_audio">Adaptive Audio</string>
<string name="adaptive_audio_description">Adaptive audio dynamically responds to your environment and cancels or allows external noise. You can customize Adaptive Audio to allow more or less noise.</string>
<string name="buds">Buds</string>
<string name="case_alt">Case</string>
<string name="test">Test</string>
<string name="name">Name</string>
<string name="noise_control">Noise Control</string>
<string name="off">Off</string>
<string name="transparency">Transparency</string>
<string name="adaptive">Adaptive</string>
<string name="noise_cancellation">Noise Cancellation</string>
<string name="press_and_hold_airpods">Press and Hold AirPods</string>
<string name="left">Left</string>
<string name="right">Right</string>
<string name="adjusts_volume">Adjusts the volume of media in response to your environment</string>
<string name="conversational_awareness">Conversational Awareness</string>
<string name="conversational_awareness_description">Lowers media volume and reduces background noise when you start speaking to other people.</string>
<string name="personalized_volume">Personalized Volume</string>
<string name="personalized_volume_description">Adjusts the volume of media in response to your environment.</string>
<string name="less_noise">Less Noise</string>
<string name="more_noise">More Noise</string>
<string name="noise_cancellation_single_airpod">Noise Cancellation with Single AirPod</string>
<string name="noise_cancellation_single_airpod_description">Allow AirPods to be put in noise cancellation mode when only one AirPods is in your ear.</string>
<string name="volume_control">Volume Control</string>
<string name="volume_control_description">Adjust the volume by swiping up or down on the sensor located on the AirPods Pro stem.</string>
<string name="airpods_not_connected">AirPods not connected</string>
<string name="airpods_not_connected_description">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!)</string>
<string name="back">Back</string>
<string name="app_settings">App Settings</string>
<string name="conversational_awareness_customization">Conversational Awareness</string>
<string name="relative_conversational_awareness_volume">Relative volume</string>
<string name="relative_conversational_awareness_volume_description">Reduces to a percentage of the current volume instead of the maximum volume.</string>
<string name="conversational_awareness_pause_music">Pause Music</string>
<string name="conversational_awareness_pause_music_description">When you start speaking, music will be paused.</string>
</resources>