mirror of
https://github.com/kavishdevar/librepods.git
synced 2026-02-10 19:52:24 +00:00
1582 lines
59 KiB
Kotlin
1582 lines
59 KiB
Kotlin
package me.kavishdevar.aln
|
||
|
||
import android.annotation.SuppressLint
|
||
import android.bluetooth.BluetoothDevice
|
||
import android.bluetooth.BluetoothManager
|
||
import android.bluetooth.BluetoothProfile
|
||
import android.content.BroadcastReceiver
|
||
import android.content.Context
|
||
import android.content.Context.MODE_PRIVATE
|
||
import android.content.Intent
|
||
import android.content.IntentFilter
|
||
import android.content.SharedPreferences
|
||
import android.os.Build
|
||
import android.os.ParcelUuid
|
||
import androidx.annotation.RequiresApi
|
||
import androidx.compose.animation.core.animateDpAsState
|
||
import androidx.compose.foundation.Image
|
||
import androidx.compose.foundation.background
|
||
import androidx.compose.foundation.border
|
||
import androidx.compose.foundation.clickable
|
||
import androidx.compose.foundation.gestures.detectTapGestures
|
||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||
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.fillMaxHeight
|
||
import androidx.compose.foundation.layout.fillMaxSize
|
||
import androidx.compose.foundation.layout.fillMaxWidth
|
||
import androidx.compose.foundation.layout.height
|
||
import androidx.compose.foundation.layout.offset
|
||
import androidx.compose.foundation.layout.padding
|
||
import androidx.compose.foundation.layout.size
|
||
import androidx.compose.foundation.layout.width
|
||
import androidx.compose.foundation.rememberScrollState
|
||
import androidx.compose.foundation.shape.CircleShape
|
||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||
import androidx.compose.foundation.text.BasicTextField
|
||
import androidx.compose.foundation.verticalScroll
|
||
import androidx.compose.material.icons.Icons
|
||
import androidx.compose.material.icons.filled.KeyboardArrowRight
|
||
import androidx.compose.material.icons.filled.Refresh
|
||
import androidx.compose.material3.CenterAlignedTopAppBar
|
||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||
import androidx.compose.material3.Icon
|
||
import androidx.compose.material3.IconButton
|
||
import androidx.compose.material3.IconButtonDefaults
|
||
import androidx.compose.material3.MaterialTheme
|
||
import androidx.compose.material3.Scaffold
|
||
import androidx.compose.material3.Slider
|
||
import androidx.compose.material3.SliderDefaults
|
||
import androidx.compose.material3.Text
|
||
import androidx.compose.material3.TopAppBarDefaults
|
||
import androidx.compose.material3.VerticalDivider
|
||
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.alpha
|
||
import androidx.compose.ui.draw.clip
|
||
import androidx.compose.ui.draw.drawBehind
|
||
import androidx.compose.ui.draw.scale
|
||
import androidx.compose.ui.draw.shadow
|
||
import androidx.compose.ui.focus.onFocusChanged
|
||
import androidx.compose.ui.geometry.Offset
|
||
import androidx.compose.ui.graphics.Color
|
||
import androidx.compose.ui.graphics.ImageBitmap
|
||
import androidx.compose.ui.graphics.SolidColor
|
||
import androidx.compose.ui.graphics.luminance
|
||
import androidx.compose.ui.input.pointer.pointerInput
|
||
import androidx.compose.ui.platform.LocalContext
|
||
import androidx.compose.ui.res.imageResource
|
||
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.text.input.TextFieldValue
|
||
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 dev.chrisbanes.haze.HazeState
|
||
import dev.chrisbanes.haze.haze
|
||
import dev.chrisbanes.haze.hazeChild
|
||
import dev.chrisbanes.haze.materials.CupertinoMaterials
|
||
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
|
||
import kotlin.math.roundToInt
|
||
|
||
|
||
@Preview
|
||
@Composable
|
||
fun BatteryViewPreview() {
|
||
BatteryView(AirPodsService(), true)
|
||
}
|
||
|
||
@Composable
|
||
fun BatteryView(service: AirPodsService, preview: Boolean = false) {
|
||
val batteryStatus = remember { mutableStateOf<List<Battery>>(listOf()) }
|
||
@Suppress("DEPRECATION") val batteryReceiver = remember {
|
||
object : BroadcastReceiver() {
|
||
override fun onReceive(context: Context, intent: Intent) {
|
||
batteryStatus.value =
|
||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||
intent.getParcelableArrayListExtra("data", Battery::class.java)
|
||
} else {
|
||
intent.getParcelableArrayListExtra("data")
|
||
}?.toList() ?: listOf()
|
||
}
|
||
}
|
||
}
|
||
val context = LocalContext.current
|
||
|
||
LaunchedEffect(context) {
|
||
val batteryIntentFilter = IntentFilter(AirPodsNotifications.BATTERY_DATA)
|
||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||
context.registerReceiver(
|
||
batteryReceiver,
|
||
batteryIntentFilter,
|
||
Context.RECEIVER_EXPORTED
|
||
)
|
||
}
|
||
}
|
||
|
||
batteryStatus.value = service.getBattery()
|
||
|
||
if (preview) {
|
||
batteryStatus.value = listOf<Battery>(
|
||
Battery(BatteryComponent.LEFT, 100, BatteryStatus.CHARGING),
|
||
Battery(BatteryComponent.RIGHT, 50, BatteryStatus.NOT_CHARGING),
|
||
Battery(BatteryComponent.CASE, 5, BatteryStatus.CHARGING)
|
||
)
|
||
}
|
||
|
||
Row {
|
||
Column (
|
||
modifier = Modifier
|
||
.fillMaxWidth(0.5f),
|
||
horizontalAlignment = Alignment.CenterHorizontally
|
||
) {
|
||
Image (
|
||
bitmap = ImageBitmap.imageResource(R.drawable.pro_2_buds),
|
||
contentDescription = "Buds",
|
||
modifier = Modifier
|
||
.fillMaxWidth()
|
||
.scale(0.80f)
|
||
)
|
||
val left = batteryStatus.value.find { it.component == BatteryComponent.LEFT }
|
||
val right = batteryStatus.value.find { it.component == BatteryComponent.RIGHT }
|
||
if ((right?.status == BatteryStatus.CHARGING && left?.status == BatteryStatus.CHARGING) || (left?.status == BatteryStatus.NOT_CHARGING && right?.status == BatteryStatus.NOT_CHARGING))
|
||
{
|
||
BatteryIndicator(right.level.let { left.level.coerceAtMost(it) }, left.status == BatteryStatus.CHARGING)
|
||
}
|
||
else {
|
||
Row (
|
||
modifier = Modifier
|
||
.fillMaxWidth(),
|
||
horizontalArrangement = Arrangement.Center
|
||
) {
|
||
if (left?.status != BatteryStatus.DISCONNECTED) {
|
||
BatteryIndicator(
|
||
left?.level ?: 0,
|
||
left?.status == BatteryStatus.CHARGING
|
||
)
|
||
}
|
||
if (left?.status != BatteryStatus.DISCONNECTED && right?.status != BatteryStatus.DISCONNECTED) {
|
||
Spacer(modifier = Modifier.width(16.dp))
|
||
}
|
||
if (right?.status != BatteryStatus.DISCONNECTED) {
|
||
BatteryIndicator(
|
||
right?.level ?: 0,
|
||
right?.status == BatteryStatus.CHARGING
|
||
)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
Column (
|
||
modifier = Modifier
|
||
.fillMaxWidth(),
|
||
horizontalAlignment = Alignment.CenterHorizontally
|
||
) {
|
||
val case = batteryStatus.value.find { it.component == BatteryComponent.CASE }
|
||
|
||
Image(
|
||
bitmap = ImageBitmap.imageResource(R.drawable.pro_2_case),
|
||
contentDescription = "Case",
|
||
modifier = Modifier
|
||
.fillMaxWidth()
|
||
.scale(1.25f)
|
||
)
|
||
BatteryIndicator(case?.level ?: 0)
|
||
}
|
||
}
|
||
}
|
||
|
||
@OptIn(ExperimentalMaterial3Api::class)
|
||
@Composable
|
||
fun ToneVolumeSlider(service: AirPodsService, sharedPreferences: SharedPreferences) {
|
||
val sliderValue = remember { mutableFloatStateOf(0f) }
|
||
LaunchedEffect(sliderValue) {
|
||
if (sharedPreferences.contains("tone_volume")) {
|
||
sliderValue.floatValue = sharedPreferences.getInt("tone_volume", 0).toFloat()
|
||
}
|
||
}
|
||
LaunchedEffect(sliderValue.floatValue) {
|
||
sharedPreferences.edit().putInt("tone_volume", sliderValue.floatValue.toInt()).apply()
|
||
}
|
||
|
||
val isDarkTheme = MaterialTheme.colorScheme.surface.luminance() < 0.5
|
||
|
||
val trackColor = if (isDarkTheme) Color(0xFFB3B3B3) else Color(0xFF929491)
|
||
val activeTrackColor = if (isDarkTheme) Color(0xFF007AFF) else Color(0xFF3C6DF5)
|
||
val thumbColor = if (isDarkTheme) Color(0xFFFFFFFF) else Color(0xFFFFFFFF)
|
||
val labelTextColor = if (isDarkTheme) Color.White else Color.Black
|
||
|
||
|
||
Row(
|
||
modifier = Modifier.fillMaxWidth(),
|
||
horizontalArrangement = Arrangement.SpaceBetween,
|
||
verticalAlignment = Alignment.CenterVertically
|
||
) {
|
||
Text(
|
||
text = "\uDBC0\uDEA1",
|
||
style = TextStyle(
|
||
fontSize = 16.sp,
|
||
fontFamily = FontFamily(Font(R.font.sf_pro)),
|
||
fontWeight = FontWeight.Light,
|
||
color = labelTextColor
|
||
),
|
||
modifier = Modifier.padding(start = 4.dp)
|
||
)
|
||
Slider(
|
||
value = sliderValue.floatValue,
|
||
onValueChange = {
|
||
sliderValue.floatValue = it
|
||
service.setToneVolume(volume = it.toInt())
|
||
},
|
||
valueRange = 0f..100f,
|
||
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 / 100)
|
||
.height(4.dp)
|
||
.background(activeTrackColor, RoundedCornerShape(4.dp))
|
||
)
|
||
}
|
||
}
|
||
)
|
||
Text(
|
||
text = "\uDBC0\uDEA9",
|
||
style = TextStyle(
|
||
fontSize = 16.sp,
|
||
fontFamily = FontFamily(Font(R.font.sf_pro)),
|
||
fontWeight = FontWeight.Light,
|
||
color = labelTextColor
|
||
),
|
||
modifier = Modifier.padding(end = 4.dp)
|
||
)
|
||
}
|
||
}
|
||
|
||
@Composable
|
||
fun SinglePodANCSwitch(service: AirPodsService, sharedPreferences: SharedPreferences) {
|
||
var singleANCEnabled by remember {
|
||
mutableStateOf(
|
||
sharedPreferences.getBoolean("single_anc", true)
|
||
)
|
||
}
|
||
|
||
fun updateSingleEnabled(enabled: Boolean) {
|
||
singleANCEnabled = enabled
|
||
sharedPreferences.edit().putBoolean("single_anc", enabled).apply()
|
||
service.setNoiseCancellationWithOnePod(enabled)
|
||
}
|
||
|
||
val isDarkTheme = MaterialTheme.colorScheme.surface.luminance() < 0.5
|
||
val textColor = if (isDarkTheme) Color.White else Color.Black
|
||
|
||
val isPressed = remember { mutableStateOf(false) }
|
||
|
||
Row(
|
||
modifier = Modifier
|
||
.fillMaxWidth()
|
||
.background(
|
||
shape = RoundedCornerShape(14.dp),
|
||
color = if (isPressed.value) Color(0xFFE0E0E0) else Color.Transparent
|
||
)
|
||
.padding(horizontal = 12.dp, vertical = 12.dp)
|
||
.pointerInput(Unit) {
|
||
detectTapGestures(
|
||
onPress = {
|
||
isPressed.value = true
|
||
tryAwaitRelease()
|
||
isPressed.value = false
|
||
}
|
||
)
|
||
}
|
||
.clickable(
|
||
indication = null,
|
||
interactionSource = remember { MutableInteractionSource() }
|
||
) {
|
||
updateSingleEnabled(!singleANCEnabled)
|
||
},
|
||
verticalAlignment = Alignment.CenterVertically
|
||
) {
|
||
Column(
|
||
modifier = Modifier
|
||
.weight(1f)
|
||
.padding(end = 4.dp)
|
||
) {
|
||
Text(
|
||
text = "Noise Cancellation with Single AirPod",
|
||
fontSize = 16.sp,
|
||
color = textColor
|
||
)
|
||
Spacer(modifier = Modifier.height(4.dp))
|
||
Text(
|
||
text = "Allow AirPods to be put in noise cancellation mode when only one AirPods is in your ear.",
|
||
fontSize = 12.sp,
|
||
color = textColor.copy(0.6f),
|
||
lineHeight = 14.sp,
|
||
)
|
||
}
|
||
StyledSwitch(
|
||
checked = singleANCEnabled,
|
||
onCheckedChange = {
|
||
updateSingleEnabled(it)
|
||
},
|
||
)
|
||
}
|
||
}
|
||
|
||
@Composable
|
||
fun VolumeControlSwitch(service: AirPodsService, sharedPreferences: SharedPreferences) {
|
||
var volumeControlEnabled by remember {
|
||
mutableStateOf(
|
||
sharedPreferences.getBoolean("volume_control", true)
|
||
)
|
||
}
|
||
|
||
fun updateVolumeControlEnabled(enabled: Boolean) {
|
||
volumeControlEnabled = enabled
|
||
sharedPreferences.edit().putBoolean("volume_control", enabled).apply()
|
||
service.setVolumeControl(enabled)
|
||
}
|
||
|
||
val isDarkTheme = MaterialTheme.colorScheme.surface.luminance() < 0.5
|
||
val textColor = if (isDarkTheme) Color.White else Color.Black
|
||
|
||
val isPressed = remember { mutableStateOf(false) }
|
||
|
||
Row(
|
||
modifier = Modifier
|
||
.fillMaxWidth()
|
||
.background(
|
||
shape = RoundedCornerShape(14.dp),
|
||
color = if (isPressed.value) Color(0xFFE0E0E0) else Color.Transparent
|
||
)
|
||
.padding(horizontal = 12.dp, vertical = 12.dp)
|
||
.pointerInput(Unit) {
|
||
detectTapGestures(
|
||
onPress = {
|
||
isPressed.value = true
|
||
tryAwaitRelease()
|
||
isPressed.value = false
|
||
}
|
||
)
|
||
}
|
||
.clickable(
|
||
indication = null,
|
||
interactionSource = remember { MutableInteractionSource() }
|
||
) {
|
||
updateVolumeControlEnabled(!volumeControlEnabled)
|
||
},
|
||
verticalAlignment = Alignment.CenterVertically
|
||
) {
|
||
Column(
|
||
modifier = Modifier
|
||
.weight(1f)
|
||
.padding(end = 4.dp)
|
||
) {
|
||
Text(
|
||
text = "Volume Control",
|
||
fontSize = 16.sp,
|
||
color = textColor
|
||
)
|
||
Spacer(modifier = Modifier.height(4.dp))
|
||
Text(
|
||
text = "Adjust the volume by swiping up or down on the sensor located on the AirPods Pro stem.",
|
||
fontSize = 12.sp,
|
||
color = textColor.copy(0.6f),
|
||
lineHeight = 14.sp,
|
||
)
|
||
}
|
||
StyledSwitch(
|
||
checked = volumeControlEnabled,
|
||
onCheckedChange = {
|
||
updateVolumeControlEnabled(it)
|
||
},
|
||
)
|
||
}
|
||
}
|
||
|
||
@Composable
|
||
fun AccessibilitySettings(service: AirPodsService, sharedPreferences: SharedPreferences) {
|
||
val isDarkTheme = MaterialTheme.colorScheme.surface.luminance() < 0.5
|
||
val textColor = if (isDarkTheme) Color.White else Color.Black
|
||
|
||
Text(
|
||
text = "ACCESSIBILITY",
|
||
style = TextStyle(
|
||
fontSize = 14.sp,
|
||
fontWeight = FontWeight.Light,
|
||
color = textColor.copy(alpha = 0.6f)
|
||
),
|
||
modifier = Modifier.padding(8.dp, bottom = 2.dp)
|
||
)
|
||
|
||
val backgroundColor = if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFFFFFFF)
|
||
|
||
Column(
|
||
modifier = Modifier
|
||
.fillMaxWidth()
|
||
.background(backgroundColor, RoundedCornerShape(14.dp))
|
||
.padding(top = 2.dp)
|
||
) {
|
||
|
||
Column(
|
||
modifier = Modifier
|
||
.fillMaxWidth()
|
||
.padding(12.dp)
|
||
) {
|
||
Text(
|
||
text = "Tone Volume",
|
||
modifier = Modifier
|
||
.padding(end = 8.dp, bottom = 2.dp, start = 2.dp)
|
||
.fillMaxWidth(),
|
||
style = TextStyle(
|
||
fontSize = 16.sp,
|
||
fontWeight = FontWeight.Medium,
|
||
color = textColor
|
||
)
|
||
)
|
||
|
||
ToneVolumeSlider(service = service, sharedPreferences = sharedPreferences)
|
||
}
|
||
|
||
// Dropdown menu with 3 options, Default, Slower, Slowest – Press speed
|
||
// Dropdown menu with 3 options, Default, Slower, Slowest – Press and hold duration
|
||
// IndependentToggle for Noise Cancellation with one AirPod
|
||
// IndependentToggle for Enable Volume Control
|
||
// Dropdown menu with 3 options, Default, Slower, Slowest – Volume Swipe Speed
|
||
|
||
// IndependentToggle(name = "Noise Cancellation with one AirPod", service = service, functionName = "setNoiseCancellationWithOnePod", sharedPreferences = sharedPreferences, false)
|
||
|
||
SinglePodANCSwitch(service = service, sharedPreferences = sharedPreferences)
|
||
|
||
// IndependentToggle(name = "Enable Volume Control", service = service, functionName = "setVolumeControl", sharedPreferences = sharedPreferences, true)
|
||
|
||
VolumeControlSwitch(service = service, sharedPreferences = sharedPreferences)
|
||
}
|
||
}
|
||
|
||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalHazeMaterialsApi::class)
|
||
@SuppressLint("MissingPermission", "NewApi")
|
||
@Composable
|
||
fun AirPodsSettingsScreen(dev: BluetoothDevice?, service: AirPodsService,
|
||
navController: NavController, isConnected: Boolean) {
|
||
val sharedPreferences = LocalContext.current.getSharedPreferences("settings", MODE_PRIVATE)
|
||
var device by remember { mutableStateOf(dev) }
|
||
var deviceName by remember { mutableStateOf(TextFieldValue(sharedPreferences.getString("name", device?.name ?: "") ?: "")) }
|
||
val verticalScrollState = rememberScrollState()
|
||
val hazeState = remember { HazeState() }
|
||
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
|
||
Scaffold(
|
||
containerColor = if (MaterialTheme.colorScheme.surface.luminance() < 0.5) Color(
|
||
0xFF000000
|
||
) else Color(
|
||
0xFFF2F2F7
|
||
),
|
||
topBar = {
|
||
val darkMode = MaterialTheme.colorScheme.surface.luminance() < 0.5
|
||
val mdensity = remember { mutableFloatStateOf(1f) }
|
||
CenterAlignedTopAppBar(
|
||
title = {
|
||
Text(
|
||
text = deviceName.text
|
||
)
|
||
},
|
||
modifier = Modifier
|
||
.hazeChild(
|
||
state = hazeState,
|
||
style = CupertinoMaterials.thin(),
|
||
block = {
|
||
// make the background transparent when not scrolled yet
|
||
alpha = if (verticalScrollState.value > 55.dp.value * mdensity.floatValue) 1f else 0f
|
||
}
|
||
)
|
||
.drawBehind {
|
||
mdensity.value = density
|
||
val strokeWidth = 0.7.dp.value * density
|
||
val y = size.height - strokeWidth / 2
|
||
if (verticalScrollState.value > 55.dp.value * density) {
|
||
drawLine(
|
||
if (darkMode) Color.DarkGray else Color.LightGray,
|
||
Offset(0f, y),
|
||
Offset(size.width, y),
|
||
strokeWidth
|
||
)
|
||
}
|
||
},
|
||
colors = TopAppBarDefaults.centerAlignedTopAppBarColors(
|
||
containerColor = Color.Transparent
|
||
),
|
||
actions = {
|
||
val context = LocalContext.current
|
||
IconButton(
|
||
onClick = {
|
||
val bluetoothAdapter =
|
||
context.getSystemService(BluetoothManager::class.java).adapter
|
||
bluetoothAdapter.bondedDevices.forEach { d ->
|
||
if (d.uuids.contains(ParcelUuid.fromString("74ec2172-0bad-4d01-8f77-997b2be0722a"))) {
|
||
bluetoothAdapter.getProfileProxy(
|
||
context,
|
||
object : BluetoothProfile.ServiceListener {
|
||
override fun onServiceConnected(
|
||
profile: Int,
|
||
proxy: BluetoothProfile
|
||
) {
|
||
if (profile == BluetoothProfile.A2DP) {
|
||
val connectedDevices =
|
||
proxy.connectedDevices
|
||
if (connectedDevices.isNotEmpty()) {
|
||
service.connectToSocket(d)
|
||
device = d
|
||
deviceName = TextFieldValue(d.name)
|
||
}
|
||
}
|
||
bluetoothAdapter.closeProfileProxy(
|
||
profile,
|
||
proxy
|
||
)
|
||
}
|
||
|
||
override fun onServiceDisconnected(profile: Int) {}
|
||
},
|
||
BluetoothProfile.A2DP
|
||
)
|
||
}
|
||
}
|
||
},
|
||
colors = IconButtonDefaults.iconButtonColors(
|
||
containerColor = Color.Transparent,
|
||
contentColor = if (MaterialTheme.colorScheme.surface.luminance() < 0.5) Color.White else Color.Black
|
||
)
|
||
) {
|
||
Icon(
|
||
imageVector = Icons.Default.Refresh,
|
||
contentDescription = "Settings",
|
||
)
|
||
}
|
||
}
|
||
)
|
||
}
|
||
) { paddingValues ->
|
||
if (isConnected == true) {
|
||
Column(
|
||
modifier = Modifier
|
||
.haze(hazeState)
|
||
.fillMaxSize()
|
||
.padding(horizontal = 16.dp)
|
||
.verticalScroll(
|
||
state = verticalScrollState,
|
||
enabled = true,
|
||
)
|
||
) {
|
||
Spacer(Modifier.height(75.dp))
|
||
LaunchedEffect(service) {
|
||
service.let {
|
||
it.sendBroadcast(Intent(AirPodsNotifications.BATTERY_DATA).apply {
|
||
putParcelableArrayListExtra("data", ArrayList(it.getBattery()))
|
||
})
|
||
it.sendBroadcast(Intent(AirPodsNotifications.ANC_DATA).apply {
|
||
putExtra("data", it.getANC())
|
||
})
|
||
}
|
||
}
|
||
val sharedPreferences = LocalContext.current.getSharedPreferences("settings", MODE_PRIVATE)
|
||
|
||
Spacer(modifier = Modifier.height(64.dp))
|
||
|
||
BatteryView(service = service)
|
||
|
||
Spacer(modifier = Modifier.height(32.dp))
|
||
|
||
StyledTextField(
|
||
name = "Name",
|
||
value = deviceName.text,
|
||
onValueChange = {
|
||
deviceName = TextFieldValue(it)
|
||
sharedPreferences.edit().putString("name", it).apply()
|
||
service.setName(it)
|
||
}
|
||
)
|
||
|
||
Spacer(modifier = Modifier.height(32.dp))
|
||
NoiseControlSettings(service = service)
|
||
|
||
Spacer(modifier = Modifier.height(16.dp))
|
||
AudioSettings(service = service, sharedPreferences = sharedPreferences)
|
||
|
||
Spacer(modifier = Modifier.height(16.dp))
|
||
IndependentToggle(
|
||
name = "Automatic Ear Detection",
|
||
service = service,
|
||
functionName = "setEarDetection",
|
||
sharedPreferences = sharedPreferences,
|
||
true
|
||
)
|
||
|
||
Spacer(modifier = Modifier.height(16.dp))
|
||
IndependentToggle(
|
||
name = "Off Listening Mode",
|
||
service = service,
|
||
functionName = "setOffListeningMode",
|
||
sharedPreferences = sharedPreferences,
|
||
false
|
||
)
|
||
|
||
Spacer(modifier = Modifier.height(16.dp))
|
||
AccessibilitySettings(service = service, sharedPreferences = sharedPreferences)
|
||
// Spacer(modifier = Modifier.height(16.dp))
|
||
|
||
// val isDarkTheme = MaterialTheme.colorScheme.surface.luminance() < 0.5
|
||
// val textColor = if (isDarkTheme) Color.White else Color.Black
|
||
|
||
// localstorage stuff
|
||
// TODO: localstorage and call the setButtons() with previous configuration and new configuration
|
||
// Box (
|
||
// modifier = Modifier
|
||
// .padding(vertical = 8.dp)
|
||
// .background(
|
||
// if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFFFFFFF),
|
||
// RoundedCornerShape(14.dp)
|
||
// )
|
||
// )
|
||
// {
|
||
// // TODO: A Column Rows with text at start and a check mark if ticked
|
||
// }
|
||
|
||
Spacer(modifier = Modifier.height(16.dp))
|
||
Row(
|
||
modifier = Modifier
|
||
.background(
|
||
if (MaterialTheme.colorScheme.surface.luminance() < 0.5) Color(
|
||
0xFF1C1C1E
|
||
) else Color(0xFFFFFFFF), RoundedCornerShape(14.dp)
|
||
)
|
||
.height(55.dp)
|
||
.clickable {
|
||
navController.navigate("debug")
|
||
}
|
||
) {
|
||
Text(
|
||
text = "Debug",
|
||
modifier = Modifier.padding(16.dp),
|
||
color = if (MaterialTheme.colorScheme.surface.luminance() < 0.5) Color.White else Color.Black
|
||
)
|
||
Spacer(modifier = Modifier.weight(1f))
|
||
IconButton(
|
||
onClick = { navController.navigate("debug") },
|
||
colors = IconButtonDefaults.iconButtonColors(
|
||
containerColor = Color.Transparent,
|
||
contentColor = if (MaterialTheme.colorScheme.surface.luminance() < 0.5) Color.White else Color.Black
|
||
),
|
||
modifier = Modifier
|
||
.padding(start = 16.dp)
|
||
.fillMaxHeight()
|
||
) {
|
||
@Suppress("DEPRECATION")
|
||
Icon(
|
||
imageVector = Icons.Default.KeyboardArrowRight,
|
||
contentDescription = "Debug"
|
||
)
|
||
}
|
||
}
|
||
Spacer(Modifier.height(24.dp))
|
||
}
|
||
}
|
||
else {
|
||
Column(
|
||
modifier = Modifier
|
||
.fillMaxSize()
|
||
.padding(horizontal = 8.dp)
|
||
.verticalScroll(
|
||
state = verticalScrollState,
|
||
enabled = true,
|
||
),
|
||
horizontalAlignment = Alignment.CenterHorizontally,
|
||
verticalArrangement = Arrangement.Center
|
||
) {
|
||
Text(
|
||
text = "AirPods not connected",
|
||
style = TextStyle(
|
||
fontSize = 24.sp,
|
||
fontWeight = FontWeight.Medium,
|
||
color = if (MaterialTheme.colorScheme.surface.luminance() < 0.5) Color.White else Color.Black
|
||
),
|
||
textAlign = TextAlign.Center,
|
||
modifier = Modifier.fillMaxWidth()
|
||
)
|
||
Spacer(Modifier.height(24.dp))
|
||
Text(
|
||
text = "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!)",
|
||
style = TextStyle(
|
||
fontSize = 16.sp,
|
||
fontWeight = FontWeight.Light,
|
||
color = if (MaterialTheme.colorScheme.surface.luminance() < 0.5) Color.White else Color.Black
|
||
),
|
||
textAlign = TextAlign.Center,
|
||
modifier = Modifier.fillMaxWidth()
|
||
)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
@OptIn(ExperimentalMaterial3Api::class)
|
||
@Composable
|
||
fun NoiseControlSlider(service: AirPodsService, sharedPreferences: SharedPreferences) {
|
||
val sliderValue = remember { mutableFloatStateOf(0f) }
|
||
LaunchedEffect(sliderValue) {
|
||
if (sharedPreferences.contains("adaptive_strength")) {
|
||
sliderValue.floatValue = sharedPreferences.getInt("adaptive_strength", 0).toFloat()
|
||
}
|
||
}
|
||
LaunchedEffect(sliderValue.floatValue) {
|
||
sharedPreferences.edit().putInt("adaptive_strength", sliderValue.floatValue.toInt()).apply()
|
||
}
|
||
|
||
val isDarkTheme = MaterialTheme.colorScheme.surface.luminance() < 0.5
|
||
|
||
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
|
||
|
||
Column(
|
||
modifier = Modifier
|
||
.fillMaxWidth()
|
||
.padding(horizontal = 8.dp),
|
||
horizontalAlignment = Alignment.CenterHorizontally
|
||
) {
|
||
Slider(
|
||
value = sliderValue.floatValue,
|
||
onValueChange = {
|
||
sliderValue.floatValue = it
|
||
service.setAdaptiveStrength(100 - it.toInt())
|
||
},
|
||
valueRange = 0f..100f,
|
||
onValueChangeFinished = {
|
||
sliderValue.floatValue = sliderValue.floatValue.roundToInt().toFloat()
|
||
},
|
||
modifier = Modifier
|
||
.fillMaxWidth()
|
||
.height(36.dp),
|
||
colors = SliderDefaults.colors(
|
||
thumbColor = thumbColor,
|
||
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))
|
||
)
|
||
}
|
||
|
||
}
|
||
)
|
||
|
||
Row(
|
||
modifier = Modifier.fillMaxWidth(),
|
||
horizontalArrangement = Arrangement.SpaceBetween
|
||
) {
|
||
Text(
|
||
text = "Less Noise",
|
||
style = TextStyle(
|
||
fontSize = 14.sp,
|
||
fontWeight = FontWeight.Light,
|
||
color = labelTextColor
|
||
),
|
||
modifier = Modifier.padding(start = 4.dp)
|
||
)
|
||
Text(
|
||
text = "More Noise",
|
||
style = TextStyle(
|
||
fontSize = 14.sp,
|
||
fontWeight = FontWeight.Light,
|
||
color = labelTextColor
|
||
),
|
||
modifier = Modifier.padding(end = 4.dp)
|
||
)
|
||
}
|
||
}
|
||
}
|
||
|
||
@Preview
|
||
@Composable
|
||
fun Preview() {
|
||
IndependentToggle("Case Charging Sounds", AirPodsService(), "setCaseChargingSounds", LocalContext.current.getSharedPreferences("settings", MODE_PRIVATE))
|
||
}
|
||
|
||
@Composable
|
||
fun IndependentToggle(name: String, service: AirPodsService, functionName: String, sharedPreferences: SharedPreferences, default: Boolean = false) {
|
||
val isDarkTheme = MaterialTheme.colorScheme.surface.luminance() < 0.5
|
||
val textColor = if (isDarkTheme) Color.White else Color.Black
|
||
|
||
val snakeCasedName = name.replace(Regex("[\\W\\s]+"), "_").lowercase()
|
||
var checked by remember { mutableStateOf(default) }
|
||
|
||
LaunchedEffect(sharedPreferences) {
|
||
checked = sharedPreferences.getBoolean(snakeCasedName, true)
|
||
}
|
||
Box (
|
||
modifier = Modifier
|
||
.padding(vertical = 8.dp)
|
||
.background(
|
||
if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFFFFFFF),
|
||
RoundedCornerShape(14.dp)
|
||
)
|
||
)
|
||
{
|
||
Row(
|
||
modifier = Modifier
|
||
.fillMaxWidth()
|
||
.height(55.dp)
|
||
.padding(horizontal = 12.dp)
|
||
.clickable {
|
||
checked = !checked
|
||
sharedPreferences
|
||
.edit()
|
||
.putBoolean(snakeCasedName, checked)
|
||
.apply()
|
||
|
||
val method = service::class.java.getMethod(functionName, Boolean::class.java)
|
||
method.invoke(service, checked)
|
||
},
|
||
verticalAlignment = Alignment.CenterVertically
|
||
) {
|
||
Text(text = name, modifier = Modifier.weight(1f), fontSize = 16.sp, color = textColor)
|
||
StyledSwitch(
|
||
checked = checked,
|
||
onCheckedChange = {
|
||
checked = it
|
||
sharedPreferences.edit().putBoolean(snakeCasedName, it).apply()
|
||
val method = service::class.java.getMethod(functionName, Boolean::class.java)
|
||
method.invoke(service, it)
|
||
},
|
||
)
|
||
}
|
||
}
|
||
}
|
||
|
||
@Composable
|
||
fun ConversationalAwarenessSwitch(service: AirPodsService, sharedPreferences: SharedPreferences) {
|
||
var conversationalAwarenessEnabled by remember {
|
||
mutableStateOf(
|
||
sharedPreferences.getBoolean("conversational_awareness", true)
|
||
)
|
||
}
|
||
|
||
fun updateConversationalAwareness(enabled: Boolean) {
|
||
conversationalAwarenessEnabled = enabled
|
||
sharedPreferences.edit().putBoolean("conversational_awareness", enabled).apply()
|
||
service.setCAEnabled(enabled)
|
||
}
|
||
|
||
val isDarkTheme = MaterialTheme.colorScheme.surface.luminance() < 0.5
|
||
val textColor = if (isDarkTheme) Color.White else Color.Black
|
||
|
||
val isPressed = remember { mutableStateOf(false) }
|
||
|
||
Row(
|
||
modifier = Modifier
|
||
.fillMaxWidth()
|
||
.background(
|
||
shape = RoundedCornerShape(14.dp),
|
||
color = if (isPressed.value) Color(0xFFE0E0E0) else Color.Transparent
|
||
)
|
||
.padding(horizontal = 12.dp, vertical = 12.dp)
|
||
.pointerInput(Unit) {
|
||
detectTapGestures(
|
||
onPress = {
|
||
isPressed.value = true
|
||
tryAwaitRelease()
|
||
isPressed.value = false
|
||
}
|
||
)
|
||
}
|
||
.clickable(
|
||
indication = null,
|
||
interactionSource = remember { MutableInteractionSource() }
|
||
) {
|
||
updateConversationalAwareness(!conversationalAwarenessEnabled)
|
||
},
|
||
verticalAlignment = Alignment.CenterVertically
|
||
) {
|
||
Column(
|
||
modifier = Modifier
|
||
.weight(1f)
|
||
.padding(end = 4.dp)
|
||
) {
|
||
Text(
|
||
text = "Conversational Awareness",
|
||
fontSize = 16.sp,
|
||
color = textColor
|
||
)
|
||
Spacer(modifier = Modifier.height(4.dp))
|
||
Text(
|
||
text = "Lowers media volume and reduces background noise when you start speaking to other people.",
|
||
fontSize = 12.sp,
|
||
color = textColor.copy(0.6f),
|
||
lineHeight = 14.sp,
|
||
)
|
||
}
|
||
StyledSwitch(
|
||
checked = conversationalAwarenessEnabled,
|
||
onCheckedChange = {
|
||
updateConversationalAwareness(it)
|
||
},
|
||
)
|
||
}
|
||
}
|
||
|
||
@Composable
|
||
fun PersonalizedVolumeSwitch(service: AirPodsService, sharedPreferences: SharedPreferences) {
|
||
var personalizedVolumeEnabled by remember {
|
||
mutableStateOf(
|
||
sharedPreferences.getBoolean("personalized_volume", true)
|
||
)
|
||
}
|
||
|
||
fun updatePersonalizedVolume(enabled: Boolean) {
|
||
personalizedVolumeEnabled = enabled
|
||
sharedPreferences.edit().putBoolean("personalized_volume", enabled).apply()
|
||
service.setPVEnabled(enabled)
|
||
}
|
||
|
||
val isDarkTheme = MaterialTheme.colorScheme.surface.luminance() < 0.5
|
||
val textColor = if (isDarkTheme) Color.White else Color.Black
|
||
|
||
val isPressed = remember { mutableStateOf(false) }
|
||
|
||
Row(
|
||
modifier = Modifier
|
||
.fillMaxWidth()
|
||
.background(
|
||
shape = RoundedCornerShape(14.dp),
|
||
color = if (isPressed.value) Color(0xFFE0E0E0) else Color.Transparent
|
||
)
|
||
.padding(horizontal = 12.dp, vertical = 12.dp)
|
||
.pointerInput(Unit) {
|
||
detectTapGestures(
|
||
onPress = {
|
||
isPressed.value = true
|
||
tryAwaitRelease()
|
||
isPressed.value = false
|
||
}
|
||
)
|
||
}
|
||
.clickable(
|
||
indication = null,
|
||
interactionSource = remember { MutableInteractionSource() }
|
||
) {
|
||
updatePersonalizedVolume(!personalizedVolumeEnabled)
|
||
},
|
||
verticalAlignment = Alignment.CenterVertically
|
||
) {
|
||
Column(
|
||
modifier = Modifier
|
||
.weight(1f)
|
||
.padding(end = 4.dp)
|
||
) {
|
||
Text(
|
||
text = "Personalized Volume",
|
||
fontSize = 16.sp,
|
||
color = textColor
|
||
)
|
||
Spacer(modifier = Modifier.height(4.dp))
|
||
Text(
|
||
text = "Adjusts the volume of media in response to your environment.",
|
||
fontSize = 12.sp,
|
||
color = textColor.copy(0.6f),
|
||
lineHeight = 14.sp,
|
||
)
|
||
}
|
||
|
||
StyledSwitch(
|
||
checked = personalizedVolumeEnabled,
|
||
onCheckedChange = {
|
||
updatePersonalizedVolume(it)
|
||
},
|
||
)
|
||
}
|
||
}
|
||
|
||
@Composable
|
||
fun LoudSoundReductionSwitch(service: AirPodsService, sharedPreferences: SharedPreferences) {
|
||
var loudSoundReductionEnabled by remember {
|
||
mutableStateOf(
|
||
sharedPreferences.getBoolean("loud_sound_reduction", true)
|
||
)
|
||
}
|
||
|
||
fun updateLoudSoundReduction(enabled: Boolean) {
|
||
loudSoundReductionEnabled = enabled
|
||
sharedPreferences.edit().putBoolean("loud_sound_reduction", enabled).apply()
|
||
service.setLoudSoundReduction(enabled)
|
||
}
|
||
|
||
val isDarkTheme = MaterialTheme.colorScheme.surface.luminance() < 0.5
|
||
val textColor = if (isDarkTheme) Color.White else Color.Black
|
||
|
||
val isPressed = remember { mutableStateOf(false) }
|
||
|
||
Row(
|
||
modifier = Modifier
|
||
.fillMaxWidth()
|
||
.background(
|
||
shape = RoundedCornerShape(14.dp),
|
||
color = if (isPressed.value) Color(0xFFE0E0E0) else Color.Transparent
|
||
)
|
||
.padding(horizontal = 12.dp, vertical = 12.dp)
|
||
.pointerInput(Unit) {
|
||
detectTapGestures(
|
||
onPress = {
|
||
isPressed.value = true
|
||
tryAwaitRelease()
|
||
isPressed.value = false
|
||
}
|
||
)
|
||
}
|
||
.clickable(
|
||
indication = null,
|
||
interactionSource = remember { MutableInteractionSource() }
|
||
) {
|
||
updateLoudSoundReduction(!loudSoundReductionEnabled)
|
||
},
|
||
verticalAlignment = Alignment.CenterVertically
|
||
) {
|
||
Column(
|
||
modifier = Modifier
|
||
.weight(1f)
|
||
.padding(end = 4.dp)
|
||
) {
|
||
Text(
|
||
text = "Loud Sound Reduction",
|
||
fontSize = 16.sp,
|
||
color = textColor
|
||
)
|
||
Spacer(modifier = Modifier.height(4.dp))
|
||
Text(
|
||
text = "Reduces loud sounds you are exposed to.",
|
||
fontSize = 12.sp,
|
||
color = textColor.copy(0.6f),
|
||
lineHeight = 14.sp,
|
||
)
|
||
}
|
||
|
||
StyledSwitch(
|
||
checked = loudSoundReductionEnabled,
|
||
onCheckedChange = {
|
||
updateLoudSoundReduction(it)
|
||
},
|
||
)
|
||
}
|
||
}
|
||
@Composable
|
||
fun AudioSettings(service: AirPodsService, sharedPreferences: SharedPreferences) {
|
||
val isDarkTheme = MaterialTheme.colorScheme.surface.luminance() < 0.5
|
||
val textColor = if (isDarkTheme) Color.White else Color.Black
|
||
|
||
Text(
|
||
text = "AUDIO",
|
||
style = TextStyle(
|
||
fontSize = 14.sp,
|
||
fontWeight = FontWeight.Light,
|
||
color = textColor.copy(alpha = 0.6f)
|
||
),
|
||
modifier = Modifier.padding(8.dp, bottom = 2.dp)
|
||
)
|
||
|
||
val backgroundColor = if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFFFFFFF)
|
||
|
||
Column(
|
||
modifier = Modifier
|
||
.fillMaxWidth()
|
||
.background(backgroundColor, RoundedCornerShape(14.dp))
|
||
.padding(top = 2.dp)
|
||
) {
|
||
|
||
PersonalizedVolumeSwitch(service = service, sharedPreferences = sharedPreferences)
|
||
ConversationalAwarenessSwitch(service = service, sharedPreferences = sharedPreferences)
|
||
LoudSoundReductionSwitch(service = service, sharedPreferences = sharedPreferences)
|
||
|
||
Column(
|
||
modifier = Modifier
|
||
.fillMaxWidth()
|
||
.padding(horizontal = 8.dp, vertical = 10.dp)
|
||
) {
|
||
Text(
|
||
text = "Adaptive Audio",
|
||
modifier = Modifier
|
||
.padding(end = 8.dp, bottom = 2.dp, start = 2.dp)
|
||
.fillMaxWidth(),
|
||
style = TextStyle(
|
||
fontSize = 16.sp,
|
||
color = textColor
|
||
)
|
||
)
|
||
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.",
|
||
modifier = Modifier
|
||
.padding(bottom = 8.dp, top = 2.dp)
|
||
.padding(end = 2.dp, start = 2.dp)
|
||
.fillMaxWidth(),
|
||
style = TextStyle(
|
||
fontSize = 12.sp,
|
||
color = textColor.copy(alpha = 0.6f)
|
||
)
|
||
)
|
||
|
||
NoiseControlSlider(service = service, sharedPreferences = sharedPreferences)
|
||
}
|
||
}
|
||
}
|
||
|
||
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
|
||
@Composable
|
||
fun NoiseControlSettings(service: AirPodsService) {
|
||
val isDarkTheme = MaterialTheme.colorScheme.surface.luminance() < 0.5
|
||
val backgroundColor = if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFE3E3E8)
|
||
val textColor = if (isDarkTheme) Color.White else Color.Black
|
||
val textColorSelected = if (isDarkTheme) Color.White else Color.Black
|
||
val selectedBackground = if (isDarkTheme) Color(0xFF5C5A5F) else Color(0xFFFFFFFF)
|
||
|
||
val noiseControlMode = remember { mutableStateOf(NoiseControlMode.OFF) }
|
||
|
||
val noiseControlReceiver = remember {
|
||
object : BroadcastReceiver() {
|
||
override fun onReceive(context: Context, intent: Intent) {
|
||
noiseControlMode.value = NoiseControlMode.entries.toTypedArray()[intent.getIntExtra("data", 3) - 1]
|
||
}
|
||
}
|
||
}
|
||
|
||
val context = LocalContext.current
|
||
val noiseControlIntentFilter = IntentFilter(AirPodsNotifications.ANC_DATA)
|
||
context.registerReceiver(noiseControlReceiver, noiseControlIntentFilter, Context.RECEIVER_EXPORTED)
|
||
|
||
// val paddingAnim by animateDpAsState(
|
||
// targetValue = when (noiseControlMode.value) {
|
||
// NoiseControlMode.OFF -> 0.dp
|
||
// NoiseControlMode.TRANSPARENCY -> 150.dp
|
||
// NoiseControlMode.ADAPTIVE -> 250.dp
|
||
// NoiseControlMode.NOISE_CANCELLATION -> 350.dp
|
||
// }, label = ""
|
||
// )
|
||
|
||
val d1a = remember { mutableFloatStateOf(0f) }
|
||
val d2a = remember { mutableFloatStateOf(0f) }
|
||
val d3a = remember { mutableFloatStateOf(0f) }
|
||
|
||
fun onModeSelected(mode: NoiseControlMode) {
|
||
noiseControlMode.value = mode
|
||
service.setANCMode(mode.ordinal+1)
|
||
when (mode) {
|
||
NoiseControlMode.NOISE_CANCELLATION -> {
|
||
d1a.floatValue = 1f
|
||
d2a.floatValue = 1f
|
||
d3a.floatValue = 0f
|
||
}
|
||
NoiseControlMode.OFF -> {
|
||
d1a.floatValue = 0f
|
||
d2a.floatValue = 1f
|
||
d3a.floatValue = 1f
|
||
}
|
||
NoiseControlMode.ADAPTIVE -> {
|
||
d1a.floatValue = 1f
|
||
d2a.floatValue = 0f
|
||
d3a.floatValue = 0f
|
||
}
|
||
NoiseControlMode.TRANSPARENCY -> {
|
||
d1a.floatValue = 0f
|
||
d2a.floatValue = 0f
|
||
d3a.floatValue = 1f
|
||
}
|
||
}
|
||
}
|
||
|
||
Text(
|
||
text = "NOISE CONTROL",
|
||
style = TextStyle(
|
||
fontSize = 14.sp,
|
||
fontWeight = FontWeight.Light,
|
||
color = textColor.copy(alpha = 0.6f)
|
||
),
|
||
modifier = Modifier.padding(8.dp, bottom = 2.dp)
|
||
)
|
||
|
||
Column(
|
||
modifier = Modifier
|
||
.fillMaxWidth()
|
||
.padding(vertical = 16.dp)
|
||
) {
|
||
Box(
|
||
modifier = Modifier
|
||
.fillMaxWidth()
|
||
.height(75.dp)
|
||
.padding(8.dp)
|
||
) {
|
||
// Box(
|
||
// modifier = Modifier
|
||
// .fillMaxHeight()
|
||
// .width(80.dp)
|
||
// .offset(x = paddingAnim)
|
||
// .background(selectedBackground, RoundedCornerShape(8.dp))
|
||
// )
|
||
Row(
|
||
modifier = Modifier
|
||
.fillMaxWidth()
|
||
.background(backgroundColor, RoundedCornerShape(14.dp))
|
||
) {
|
||
NoiseControlButton(
|
||
icon = ImageBitmap.imageResource(R.drawable.noise_cancellation),
|
||
onClick = { onModeSelected(NoiseControlMode.OFF) },
|
||
textColor = if (noiseControlMode.value == NoiseControlMode.OFF) textColorSelected else textColor,
|
||
backgroundColor = if (noiseControlMode.value == NoiseControlMode.OFF) selectedBackground else Color.Transparent,
|
||
modifier = Modifier.weight(1f)
|
||
)
|
||
VerticalDivider(
|
||
thickness = 1.dp,
|
||
modifier = Modifier
|
||
.padding(vertical = 10.dp)
|
||
.alpha(d1a.floatValue),
|
||
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.12f),
|
||
)
|
||
NoiseControlButton(
|
||
icon = ImageBitmap.imageResource(R.drawable.transparency),
|
||
onClick = { onModeSelected(NoiseControlMode.TRANSPARENCY) },
|
||
textColor = if (noiseControlMode.value == NoiseControlMode.TRANSPARENCY) textColorSelected else textColor,
|
||
backgroundColor = if (noiseControlMode.value == NoiseControlMode.TRANSPARENCY) selectedBackground else Color.Transparent,
|
||
modifier = Modifier.weight(1f)
|
||
)
|
||
VerticalDivider(
|
||
thickness = 1.dp,
|
||
modifier = Modifier
|
||
.padding(vertical = 10.dp)
|
||
.alpha(d2a.floatValue),
|
||
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.12f),
|
||
)
|
||
NoiseControlButton(
|
||
icon = ImageBitmap.imageResource(R.drawable.adaptive),
|
||
onClick = { onModeSelected(NoiseControlMode.ADAPTIVE) },
|
||
textColor = if (noiseControlMode.value == NoiseControlMode.ADAPTIVE) textColorSelected else textColor,
|
||
backgroundColor = if (noiseControlMode.value == NoiseControlMode.ADAPTIVE) selectedBackground else Color.Transparent,
|
||
modifier = Modifier.weight(1f)
|
||
)
|
||
VerticalDivider(
|
||
thickness = 1.dp,
|
||
modifier = Modifier
|
||
.padding(vertical = 10.dp)
|
||
.alpha(d3a.floatValue),
|
||
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.12f),
|
||
)
|
||
NoiseControlButton(
|
||
icon = ImageBitmap.imageResource(R.drawable.noise_cancellation),
|
||
onClick = { onModeSelected(NoiseControlMode.NOISE_CANCELLATION) },
|
||
textColor = if (noiseControlMode.value == NoiseControlMode.NOISE_CANCELLATION) textColorSelected else textColor,
|
||
backgroundColor = if (noiseControlMode.value == NoiseControlMode.NOISE_CANCELLATION) selectedBackground else Color.Transparent,
|
||
modifier = Modifier.weight(1f)
|
||
)
|
||
}
|
||
}
|
||
Row(
|
||
modifier = Modifier
|
||
.fillMaxWidth()
|
||
.padding(horizontal = 8.dp)
|
||
.padding(top = 1.dp)
|
||
) {
|
||
Text(
|
||
text = "Off",
|
||
style = TextStyle(fontSize = 12.sp, color = textColor),
|
||
textAlign = TextAlign.Center,
|
||
fontWeight = FontWeight.Bold,
|
||
modifier = Modifier.weight(1f)
|
||
)
|
||
Text(
|
||
text = "Transparency",
|
||
style = TextStyle(fontSize = 12.sp, color = textColor),
|
||
textAlign = TextAlign.Center,
|
||
fontWeight = FontWeight.Bold,
|
||
modifier = Modifier.weight(1f)
|
||
)
|
||
Text(
|
||
text = "Adaptive",
|
||
style = TextStyle(fontSize = 12.sp, color = textColor),
|
||
textAlign = TextAlign.Center,
|
||
fontWeight = FontWeight.Bold,
|
||
modifier = Modifier.weight(1f)
|
||
)
|
||
Text(
|
||
text = "Noise Cancellation",
|
||
style = TextStyle(fontSize = 12.sp, color = textColor),
|
||
textAlign = TextAlign.Center,
|
||
fontWeight = FontWeight.Bold,
|
||
modifier = Modifier.weight(1f)
|
||
)
|
||
}
|
||
}
|
||
}
|
||
|
||
@Composable
|
||
fun NoiseControlButton(
|
||
icon: ImageBitmap,
|
||
onClick: () -> Unit,
|
||
textColor: Color,
|
||
backgroundColor: Color,
|
||
modifier: Modifier = Modifier
|
||
) {
|
||
Column(
|
||
modifier = modifier
|
||
.fillMaxHeight()
|
||
.padding(horizontal = 4.dp, vertical = 4.dp)
|
||
.background(color = backgroundColor, shape = RoundedCornerShape(11.dp))
|
||
.clickable(
|
||
onClick = onClick,
|
||
indication = null,
|
||
interactionSource = remember { MutableInteractionSource() }),
|
||
horizontalAlignment = Alignment.CenterHorizontally,
|
||
verticalArrangement = Arrangement.Center
|
||
) {
|
||
Icon(
|
||
bitmap = icon,
|
||
contentDescription = null,
|
||
tint = textColor,
|
||
modifier = Modifier.size(40.dp)
|
||
)
|
||
}
|
||
}
|
||
|
||
@Composable
|
||
fun StyledSwitch(
|
||
checked: Boolean,
|
||
onCheckedChange: (Boolean) -> Unit
|
||
) {
|
||
val isDarkTheme = MaterialTheme.colorScheme.surface.luminance() < 0.5
|
||
|
||
val thumbColor = Color.White
|
||
val trackColor = if (checked) Color(0xFF34C759) 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(
|
||
modifier = Modifier
|
||
.width(51.dp)
|
||
.height(31.dp)
|
||
.clip(RoundedCornerShape(15.dp))
|
||
.background(trackColor) // Dynamic track background
|
||
.padding(horizontal = 3.dp),
|
||
contentAlignment = Alignment.CenterStart
|
||
) {
|
||
Box(
|
||
modifier = Modifier
|
||
.offset(x = thumbOffsetX) // Animate the offset for smooth transition
|
||
.size(27.dp)
|
||
.clip(CircleShape)
|
||
.background(thumbColor) // Dynamic thumb color
|
||
.clickable { onCheckedChange(!checked) } // Make the switch clickable
|
||
)
|
||
}
|
||
}
|
||
|
||
@Composable
|
||
fun StyledTextField(
|
||
name: String,
|
||
value: String,
|
||
onValueChange: (String) -> Unit
|
||
) {
|
||
var isFocused by remember { mutableStateOf(false) }
|
||
|
||
val isDarkTheme = MaterialTheme.colorScheme.surface.luminance() < 0.5
|
||
|
||
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 // Hide cursor when not focused
|
||
}
|
||
|
||
Row(
|
||
verticalAlignment = Alignment.CenterVertically,
|
||
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()
|
||
}
|
||
},
|
||
modifier = Modifier
|
||
.fillMaxWidth()
|
||
.padding(start = 8.dp)
|
||
.onFocusChanged { focusState ->
|
||
isFocused = focusState.isFocused // Update focus state
|
||
}
|
||
)
|
||
}
|
||
}
|
||
|
||
@Composable
|
||
fun BatteryIndicator(batteryPercentage: Int, charging: Boolean = false) {
|
||
val batteryOutlineColor = Color(0xFFBFBFBF) // Light gray outline
|
||
val batteryFillColor = if (batteryPercentage > 30) Color(0xFF30D158) else Color(0xFFFC3C3C)
|
||
val batteryTextColor = MaterialTheme.colorScheme.onSurface
|
||
|
||
// Battery indicator dimensions
|
||
val batteryWidth = 40.dp
|
||
val batteryHeight = 15.dp
|
||
val batteryCornerRadius = 4.dp
|
||
val tipWidth = 5.dp
|
||
val tipHeight = batteryHeight * 0.375f
|
||
|
||
Column(
|
||
horizontalAlignment = Alignment.CenterHorizontally
|
||
) {
|
||
Row(
|
||
verticalAlignment = Alignment.CenterVertically,
|
||
horizontalArrangement = Arrangement.spacedBy(0.dp),
|
||
modifier = Modifier.padding(bottom = 4.dp) // Padding between icon and percentage text
|
||
) {
|
||
// Battery Icon
|
||
Box(
|
||
modifier = Modifier
|
||
.width(batteryWidth)
|
||
.height(batteryHeight)
|
||
.border(1.dp, batteryOutlineColor, RoundedCornerShape(batteryCornerRadius))
|
||
) {
|
||
Box(
|
||
modifier = Modifier
|
||
.fillMaxHeight()
|
||
.padding(2.dp)
|
||
.width(batteryWidth * (batteryPercentage / 100f))
|
||
.background(batteryFillColor, RoundedCornerShape(2.dp))
|
||
)
|
||
if (charging) {
|
||
Box(
|
||
modifier = Modifier
|
||
.padding(0.dp)
|
||
.fillMaxSize(),
|
||
contentAlignment = Alignment.Center
|
||
) {
|
||
Text(
|
||
text = "\uDBC0\uDEE6",
|
||
fontSize = 15.sp,
|
||
fontFamily = FontFamily(Font(R.font.sf_pro)),
|
||
color = Color.White,
|
||
modifier = Modifier
|
||
.align(Alignment.Center)
|
||
.padding(0.dp)
|
||
)
|
||
}
|
||
}
|
||
}
|
||
|
||
Box(
|
||
modifier = Modifier
|
||
.width(tipWidth)
|
||
.height(tipHeight)
|
||
.padding(start = 1.dp)
|
||
.background(
|
||
batteryOutlineColor,
|
||
RoundedCornerShape(
|
||
topStart = 0.dp,
|
||
topEnd = 12.dp,
|
||
bottomStart = 0.dp,
|
||
bottomEnd = 12.dp
|
||
)
|
||
)
|
||
)
|
||
}
|
||
|
||
Text(
|
||
text = "$batteryPercentage%",
|
||
color = batteryTextColor,
|
||
style = TextStyle(fontSize = 16.sp, fontWeight = FontWeight.Bold)
|
||
)
|
||
}
|
||
} |