diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index ddad160..83ebc1b 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -67,7 +67,7 @@ android:foregroundServiceType="connectedDevice" android:permission="android.permission.BLUETOOTH_CONNECT" /> diff --git a/android/app/src/main/java/me/kavishdevar/aln/AirPodsService.kt b/android/app/src/main/java/me/kavishdevar/aln/AirPodsService.kt index c07cc5d..52d621d 100644 --- a/android/app/src/main/java/me/kavishdevar/aln/AirPodsService.kt +++ b/android/app/src/main/java/me/kavishdevar/aln/AirPodsService.kt @@ -23,7 +23,9 @@ import android.widget.RemoteViews import androidx.core.app.NotificationCompat import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay import kotlinx.coroutines.launch +import me.kavishdevar.aln.utils.MediaController import org.lsposed.hiddenapibypass.HiddenApiBypass object ServiceManager { @@ -36,11 +38,11 @@ object ServiceManager { fun setService(service: AirPodsService?) { this.service = service } - @Synchronized - fun restartService(context: Context) { - service?.stopSelf() - context.startService(Intent(context, AirPodsService::class.java)) - } +// @Synchronized +// fun restartService(context: Context) { +// service?.stopSelf() +// context.startService(Intent(context, AirPodsService::class.java)) +// } } @Suppress("unused") @@ -76,6 +78,10 @@ class AirPodsService: Service() { if (bluetoothDevice != null && action != null && !action.isEmpty()) { Log.d("AirPodsService", "Received bluetooth connection broadcast") if (BluetoothDevice.ACTION_ACL_CONNECTED == action) { + if (ServiceManager.getService()?.isConnected == true) { + ServiceManager.getService()?.manuallyCheckForAudioSource() + return + } val uuid = ParcelUuid.fromString("74ec2172-0bad-4d01-8f77-997b2be0722a") if (bluetoothDevice.uuids.contains(uuid)) { val intent = Intent(AirPodsNotifications.AIRPODS_CONNECTION_DETECTED) @@ -84,8 +90,6 @@ class AirPodsService: Service() { context?.sendBroadcast(intent) } } - - // Airpods disconnected, remove notification but leave the scanner going. if (BluetoothDevice.ACTION_ACL_DISCONNECTED == action || BluetoothDevice.ACTION_ACL_DISCONNECT_REQUESTED == action ) { @@ -203,7 +207,19 @@ class AirPodsService: Service() { Log.d("AirPodsService", "Service started") ServiceManager.setService(this) startForegroundNotification() - registerReceiver(bluetoothReceiver, BluetoothReceiver.buildFilter(), RECEIVER_EXPORTED) + val serviceIntentFilter = IntentFilter().apply { + addAction("android.bluetooth.device.action.ACL_CONNECTED") + addAction("android.bluetooth.device.action.ACL_DISCONNECTED") + addAction("android.bluetooth.device.action.BOND_STATE_CHANGED") + addAction("android.bluetooth.device.action.NAME_CHANGED") + addAction("android.bluetooth.adapter.action.CONNECTION_STATE_CHANGED") + addAction("android.bluetooth.adapter.action.STATE_CHANGED") + addAction("android.bluetooth.headset.profile.action.CONNECTION_STATE_CHANGED") + addAction("android.bluetooth.headset.action.VENDOR_SPECIFIC_HEADSET_EVENT") + addAction("android.bluetooth.a2dp.profile.action.CONNECTION_STATE_CHANGED") + addAction("android.bluetooth.a2dp.profile.action.PLAYING_STATE_CHANGED") + } + registerReceiver(bluetoothReceiver, serviceIntentFilter, RECEIVER_EXPORTED) connectionReceiver = object: BroadcastReceiver() { override fun onReceive(context: Context?, intent: Intent?) { @@ -225,11 +241,11 @@ class AirPodsService: Service() { } } - val intentFilter = IntentFilter().apply { + val deviceIntentFilter = IntentFilter().apply { addAction(AirPodsNotifications.AIRPODS_CONNECTION_DETECTED) addAction(AirPodsNotifications.AIRPODS_DISCONNECTED) } - registerReceiver(connectionReceiver, intentFilter, RECEIVER_EXPORTED) + registerReceiver(connectionReceiver, deviceIntentFilter, RECEIVER_EXPORTED) val bluetoothAdapter = getSystemService(BluetoothManager::class.java).adapter bluetoothAdapter.bondedDevices.forEach { device -> @@ -258,6 +274,13 @@ class AirPodsService: Service() { private lateinit var socket: BluetoothSocket + fun manuallyCheckForAudioSource() { + if (earDetectionNotification.status[0] != 0.toByte() && earDetectionNotification.status[1] != 0.toByte()) { + Log.d("AirPodsService", "For some reason, Android connected to the audio profile itself even after disconnecting. Disconnecting audio profile again!") + disconnectAudio(this, device) + } + } + @SuppressLint("MissingPermission", "UnspecifiedRegisterReceiverFlag") fun connectToSocket(device: BluetoothDevice) { HiddenApiBypass.addHiddenApiExemptions("Landroid/bluetooth/BluetoothSocket;") @@ -303,18 +326,32 @@ class AirPodsService: Service() { this@AirPodsService.device = device isConnected = true socket.let { it -> + // sometimes doesn't work ;-; + // i though i move it to the coroutine + // but, the socket sometimes disconnects if i don't send a packet outside of the routine first + // so, sending *again*, with a delay, in the coroutine 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() - sendBroadcast( - Intent(AirPodsNotifications.AIRPODS_CONNECTED) - .putExtra("device", device) - ) - CoroutineScope(Dispatchers.IO).launch { + // this is so stupid, why does it disconnect if i don't send a packet outside of the coroutine first + it.outputStream.write(Enums.HANDSHAKE.value) + it.outputStream.flush() + delay(200) + it.outputStream.write(Enums.SET_SPECIFIC_FEATURES.value) + it.outputStream.flush() + delay(200) + it.outputStream.write(Enums.REQUEST_NOTIFICATIONS.value) + it.outputStream.flush() + delay(200) + sendBroadcast( + Intent(AirPodsNotifications.AIRPODS_CONNECTED) + .putExtra("device", device) + ) + while (socket.isConnected == true) { socket.let { val audioManager = @@ -516,6 +553,7 @@ class AirPodsService: Service() { } fun setANCMode(mode: Int) { + Log.d("AirPodsService", "setANCMode: $mode") when (mode) { 1 -> { socket.outputStream?.write(Enums.NOISE_CANCELLATION_OFF.value) diff --git a/android/app/src/main/java/me/kavishdevar/aln/AirPodsSettingsScreen.kt b/android/app/src/main/java/me/kavishdevar/aln/AirPodsSettingsScreen.kt deleted file mode 100644 index 1bd5b03..0000000 --- a/android/app/src/main/java/me/kavishdevar/aln/AirPodsSettingsScreen.kt +++ /dev/null @@ -1,1126 +0,0 @@ -package me.kavishdevar.aln - -import android.annotation.SuppressLint -import android.bluetooth.BluetoothDevice -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 androidx.annotation.RequiresApi -import androidx.compose.foundation.background -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.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.foundation.verticalScroll -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight -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.HorizontalDivider -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.drawBehind -import androidx.compose.ui.draw.shadow -import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.ImageBitmap -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.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 androidx.navigation.compose.rememberNavController -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 me.kavishdevar.aln.composables.BatteryView -import me.kavishdevar.aln.composables.SinglePodANCSwitch -import me.kavishdevar.aln.composables.StyledSwitch -import me.kavishdevar.aln.composables.StyledTextField -import me.kavishdevar.aln.composables.ToneVolumeSlider -import me.kavishdevar.aln.composables.VolumeControlSwitch -import me.kavishdevar.aln.ui.theme.ALNTheme -import kotlin.math.roundToInt - - -@Preview(showBackground = true) -@Composable -fun BatteryViewPreview() { - ALNTheme (darkTheme = false) { - BatteryView(AirPodsService(), true) - } -} - -@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 -// Dropdown menu with 3 options, Default, Slower, Slowest – Volume Swipe Speed - - SinglePodANCSwitch(service = service, sharedPreferences = sharedPreferences) - VolumeControlSwitch(service = service, sharedPreferences = sharedPreferences) - } -} - - -@Composable -fun PressAndHoldSettings(navController: NavController) { - val isDarkTheme = MaterialTheme.colorScheme.surface.luminance() < 0.5 - val textColor = if (isDarkTheme) Color.White else Color.Black - - Text( - text = "PRESS AND HOLD AIRPODS", - 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) - ) { - Box( - modifier = Modifier - .fillMaxWidth() - .height(55.dp) - .background( - backgroundColor, - RoundedCornerShape(14.dp) - ) - .clickable( - onClick = { - navController.navigate("long_press/Left") - } - ), - contentAlignment = Alignment.Center - ) { - Row( - modifier = Modifier - .padding(start = 16.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Text( - text = "Left", - style = TextStyle( - fontSize = 16.sp, - color = textColor - ) - ) - Spacer(modifier = Modifier.weight(1f)) - IconButton( - onClick = { - navController.navigate("long_press/Left") - } - ) { - Icon( - imageVector = Icons.AutoMirrored.Filled.KeyboardArrowRight, - contentDescription = "go", - tint = textColor - ) - } - } - } - HorizontalDivider( - thickness = 2.dp, - color = Color(0xFF4D4D4D).copy(alpha = 0.4f), - modifier = Modifier - .padding(start = 16.dp) - ) - Box( - modifier = Modifier - .fillMaxWidth() - .height(55.dp) - .background( - backgroundColor, - RoundedCornerShape(14.dp) - ) - .clickable( - onClick = { - navController.navigate("long_press/Right") - } - ), - contentAlignment = Alignment.Center - ) { - Row( - modifier = Modifier - .padding(start = 16.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Text( - text = "Right", - style = TextStyle( - fontSize = 16.sp, - color = textColor - ) - ) - Spacer(modifier = Modifier.weight(1f)) - IconButton( - onClick = { - navController.navigate("long_press/Right") - } - ) { - Icon( - imageVector = Icons.AutoMirrored.Filled.KeyboardArrowRight, - contentDescription = "go", - tint = textColor - ) - } - } - } - } -} - -@Composable -fun NavigationButton(to: String, name: String, navController: NavController) { - 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(to) - } - ) { - Text( - text = name, - 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(to) }, - 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 = name - ) - } - } -} - -@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 ?: "AirPods Pro").toString() - ) - ) - } - 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.regular(), - block = { - alpha = - if (verticalScrollState.value > 55.dp.value * mDensity.floatValue) 1f else 0f - } - ) - .drawBehind { - mDensity.floatValue = 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 = { - ServiceManager.restartService(context) - }, - 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)) - PressAndHoldSettings(navController = navController) - - 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)) - NavigationButton("debug", "Debug", navController) - 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) - ) - } - } -} - -@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) - ) - .clickable { - checked = !checked - sharedPreferences - .edit() - .putBoolean(snakeCasedName, checked) - .apply() - - val method = service::class.java.getMethod(functionName, Boolean::class.java) - method.invoke(service, checked) - }, - ) - { - Row( - modifier = Modifier - .fillMaxWidth() - .height(55.dp) - .padding(horizontal = 12.dp), - 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 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 - } - } - } - - val noiseControlReceiver = remember { - object : BroadcastReceiver() { - override fun onReceive(context: Context, intent: Intent) { - noiseControlMode.value = NoiseControlMode.entries.toTypedArray()[intent.getIntExtra("data", 3) - 1] - onModeSelected(noiseControlMode.value) - } - } - } - - val context = LocalContext.current - val noiseControlIntentFilter = IntentFilter(AirPodsNotifications.ANC_DATA) - context.registerReceiver(noiseControlReceiver, noiseControlIntentFilter, Context.RECEIVER_EXPORTED) - - 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) - ) { - 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) - ) - } -} - -@Preview -@Composable -fun AirPodsSettingsScreenPreview() { - ALNTheme ( - darkTheme = true - ) { - AirPodsSettingsScreen(dev = null, service = AirPodsService(), navController = rememberNavController(), isConnected = true) - } -} \ No newline at end of file diff --git a/android/app/src/main/java/me/kavishdevar/aln/BootReceiver.kt b/android/app/src/main/java/me/kavishdevar/aln/BootReceiver.kt deleted file mode 100644 index df514ac..0000000 --- a/android/app/src/main/java/me/kavishdevar/aln/BootReceiver.kt +++ /dev/null @@ -1,14 +0,0 @@ -package me.kavishdevar.aln - -import android.content.BroadcastReceiver -import android.content.Context -import android.content.Intent - -class BootReceiver: BroadcastReceiver() { - override fun onReceive(context: Context?, intent: Intent?) { - when (intent?.action) { - Intent.ACTION_MY_PACKAGE_REPLACED -> try { context?.startForegroundService(Intent(context, AirPodsService::class.java)) } catch (e: Exception) { e.printStackTrace() } - Intent.ACTION_BOOT_COMPLETED -> try { context?.startForegroundService(Intent(context, AirPodsService::class.java)) } catch (e: Exception) { e.printStackTrace() } - } - } -} \ No newline at end of file diff --git a/android/app/src/main/java/me/kavishdevar/aln/CustomDevice.kt b/android/app/src/main/java/me/kavishdevar/aln/CustomDeviceActivity.kt similarity index 100% rename from android/app/src/main/java/me/kavishdevar/aln/CustomDevice.kt rename to android/app/src/main/java/me/kavishdevar/aln/CustomDeviceActivity.kt diff --git a/android/app/src/main/java/me/kavishdevar/aln/MainActivity.kt b/android/app/src/main/java/me/kavishdevar/aln/MainActivity.kt index c75b780..cac5126 100644 --- a/android/app/src/main/java/me/kavishdevar/aln/MainActivity.kt +++ b/android/app/src/main/java/me/kavishdevar/aln/MainActivity.kt @@ -34,6 +34,9 @@ import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.google.accompanist.permissions.rememberMultiplePermissionsState +import me.kavishdevar.aln.screens.AirPodsSettingsScreen +import me.kavishdevar.aln.screens.DebugScreen +import me.kavishdevar.aln.screens.LongPress import me.kavishdevar.aln.ui.theme.ALNTheme lateinit var serviceConnection: ServiceConnection @@ -66,6 +69,7 @@ class MainActivity : ComponentActivity() { } catch (e: Exception) { Log.e("MainActivity", "Error while unregistering receiver: $e") } + sendBroadcast(Intent(AirPodsNotifications.DISCONNECT_RECEIVERS)) super.onDestroy() } @@ -147,7 +151,10 @@ fun Main() { DebugScreen(navController = navController) } composable("long_press/{bud}") { navBackStackEntry -> - LongPress(navController = navController, name = navBackStackEntry.arguments?.getString("bud")!!) + LongPress( + navController = navController, + name = navBackStackEntry.arguments?.getString("bud")!! + ) } } diff --git a/android/app/src/main/java/me/kavishdevar/aln/Packets.kt b/android/app/src/main/java/me/kavishdevar/aln/Packets.kt index 22f197b..a0a4154 100644 --- a/android/app/src/main/java/me/kavishdevar/aln/Packets.kt +++ b/android/app/src/main/java/me/kavishdevar/aln/Packets.kt @@ -75,6 +75,7 @@ class AirPodsNotifications { const val CA_DATA = "me.kavishdevar.aln.CA_DATA" const val AIRPODS_DISCONNECTED = "me.kavishdevar.aln.AIRPODS_DISCONNECTED" const val AIRPODS_CONNECTION_DETECTED = "me.kavishdevar.aln.AIRPODS_CONNECTION_DETECTED" + const val DISCONNECT_RECEIVERS = "me.kavishdevar.aln.DISCONNECT_RECEIVERS" } class EarDetection { diff --git a/android/app/src/main/java/me/kavishdevar/aln/composables/AccessibilitySettings.kt b/android/app/src/main/java/me/kavishdevar/aln/composables/AccessibilitySettings.kt new file mode 100644 index 0000000..eaae5ce --- /dev/null +++ b/android/app/src/main/java/me/kavishdevar/aln/composables/AccessibilitySettings.kt @@ -0,0 +1,80 @@ +package me.kavishdevar.aln.composables + +import android.content.Context +import android.content.SharedPreferences +import androidx.compose.foundation.background +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import me.kavishdevar.aln.AirPodsService + +@Composable +fun AccessibilitySettings(service: AirPodsService, sharedPreferences: SharedPreferences) { + val isDarkTheme = isSystemInDarkTheme() + 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) + } + +// TODO: Dropdown menu with 3 options, Default, Slower, Slowest – Press speed +// TODO: Dropdown menu with 3 options, Default, Slower, Slowest – Press and hold duration +// TODO: Dropdown menu with 3 options, Default, Slower, Slowest – Volume Swipe Speed + + SinglePodANCSwitch(service = service, sharedPreferences = sharedPreferences) + VolumeControlSwitch(service = service, sharedPreferences = sharedPreferences) + } +} + +@Preview +@Composable +fun AccessibilitySettingsPreview() { + AccessibilitySettings(service = AirPodsService(), sharedPreferences = LocalContext.current.getSharedPreferences("preview", Context.MODE_PRIVATE)) +} \ No newline at end of file diff --git a/android/app/src/main/java/me/kavishdevar/aln/composables/AdaptiveStrengthSlider.kt b/android/app/src/main/java/me/kavishdevar/aln/composables/AdaptiveStrengthSlider.kt new file mode 100644 index 0000000..7552f23 --- /dev/null +++ b/android/app/src/main/java/me/kavishdevar/aln/composables/AdaptiveStrengthSlider.kt @@ -0,0 +1,137 @@ +package me.kavishdevar.aln.composables + +import android.content.Context +import android.content.SharedPreferences +import androidx.compose.foundation.background +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.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Slider +import androidx.compose.material3.SliderDefaults +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.mutableFloatStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.shadow +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import me.kavishdevar.aln.AirPodsService +import kotlin.math.roundToInt + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun AdaptiveStrengthSlider(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 = isSystemInDarkTheme() + + 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 AdaptiveStrengthSliderPreview() { + AdaptiveStrengthSlider(service = AirPodsService(), sharedPreferences = LocalContext.current.getSharedPreferences("preview", Context.MODE_PRIVATE)) +} \ No newline at end of file diff --git a/android/app/src/main/java/me/kavishdevar/aln/composables/AudioSettings.kt b/android/app/src/main/java/me/kavishdevar/aln/composables/AudioSettings.kt new file mode 100644 index 0000000..a8c06df --- /dev/null +++ b/android/app/src/main/java/me/kavishdevar/aln/composables/AudioSettings.kt @@ -0,0 +1,87 @@ +package me.kavishdevar.aln.composables + +import android.content.Context +import android.content.SharedPreferences +import androidx.compose.foundation.background +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import me.kavishdevar.aln.AirPodsService + +@Composable +fun AudioSettings(service: AirPodsService, sharedPreferences: SharedPreferences) { + val isDarkTheme = isSystemInDarkTheme() + 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) + ) + ) + + AdaptiveStrengthSlider(service = service, sharedPreferences = sharedPreferences) + } + } +} + +@Preview +@Composable +fun AudioSettingsPreview() { + AudioSettings(service = AirPodsService(), sharedPreferences = LocalContext.current.getSharedPreferences("preview", Context.MODE_PRIVATE)) +} \ No newline at end of file diff --git a/android/app/src/main/java/me/kavishdevar/aln/composables/BatteryIndicator.kt b/android/app/src/main/java/me/kavishdevar/aln/composables/BatteryIndicator.kt index 7cf095c..eb55cf4 100644 --- a/android/app/src/main/java/me/kavishdevar/aln/composables/BatteryIndicator.kt +++ b/android/app/src/main/java/me/kavishdevar/aln/composables/BatteryIndicator.kt @@ -22,6 +22,7 @@ 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 me.kavishdevar.aln.R @@ -105,4 +106,10 @@ fun BatteryIndicator(batteryPercentage: Int, charging: Boolean = false) { style = TextStyle(fontSize = 16.sp, fontWeight = FontWeight.Bold) ) } +} + +@Preview +@Composable +fun BatteryIndicatorPreview() { + BatteryIndicator(batteryPercentage = 48, charging = true) } \ No newline at end of file diff --git a/android/app/src/main/java/me/kavishdevar/aln/composables/BatteryView.kt b/android/app/src/main/java/me/kavishdevar/aln/composables/BatteryView.kt index 30fb4d5..056b5a7 100644 --- a/android/app/src/main/java/me/kavishdevar/aln/composables/BatteryView.kt +++ b/android/app/src/main/java/me/kavishdevar/aln/composables/BatteryView.kt @@ -22,12 +22,12 @@ import androidx.compose.ui.draw.scale import androidx.compose.ui.graphics.ImageBitmap import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.imageResource +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import me.kavishdevar.aln.AirPodsNotifications import me.kavishdevar.aln.AirPodsService import me.kavishdevar.aln.Battery import me.kavishdevar.aln.BatteryComponent -import me.kavishdevar.aln.composables.BatteryIndicator import me.kavishdevar.aln.BatteryStatus import me.kavishdevar.aln.R @@ -37,19 +37,28 @@ fun BatteryView(service: AirPodsService, preview: Boolean = false) { @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() + if (intent.action == AirPodsNotifications.BATTERY_DATA) { + batteryStatus.value = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + intent.getParcelableArrayListExtra("data", Battery::class.java) + } else { + intent.getParcelableArrayListExtra("data") + }?.toList() ?: listOf() + } + else if (intent.action == AirPodsNotifications.DISCONNECT_RECEIVERS) { + context.unregisterReceiver(this) + } } } } val context = LocalContext.current LaunchedEffect(context) { - val batteryIntentFilter = IntentFilter(AirPodsNotifications.BATTERY_DATA) + val batteryIntentFilter = IntentFilter() + .apply { + addAction(AirPodsNotifications.BATTERY_DATA) + addAction(AirPodsNotifications.DISCONNECT_RECEIVERS) + } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { context.registerReceiver( batteryReceiver, @@ -132,4 +141,10 @@ fun BatteryView(service: AirPodsService, preview: Boolean = false) { } } } +} + +@Preview +@Composable +fun BatteryViewPreview() { + BatteryView(AirPodsService(), preview = true) } \ No newline at end of file diff --git a/android/app/src/main/java/me/kavishdevar/aln/composables/ConversationalAwarenessSwitch.kt b/android/app/src/main/java/me/kavishdevar/aln/composables/ConversationalAwarenessSwitch.kt new file mode 100644 index 0000000..b151cea --- /dev/null +++ b/android/app/src/main/java/me/kavishdevar/aln/composables/ConversationalAwarenessSwitch.kt @@ -0,0 +1,107 @@ +package me.kavishdevar.aln.composables + +import android.content.SharedPreferences +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.gestures.detectTapGestures +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import me.kavishdevar.aln.AirPodsService + +@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 = isSystemInDarkTheme() + 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) + }, + ) + } +} + +@Preview +@Composable +fun ConversationalAwarenessSwitchPreview() { + ConversationalAwarenessSwitch(AirPodsService(), LocalContext.current.getSharedPreferences("preview", 0)) +} \ No newline at end of file diff --git a/android/app/src/main/java/me/kavishdevar/aln/composables/IndependentToggle.kt b/android/app/src/main/java/me/kavishdevar/aln/composables/IndependentToggle.kt new file mode 100644 index 0000000..56cb5e2 --- /dev/null +++ b/android/app/src/main/java/me/kavishdevar/aln/composables/IndependentToggle.kt @@ -0,0 +1,84 @@ +package me.kavishdevar.aln.composables + +import android.content.SharedPreferences +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +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.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +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.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import me.kavishdevar.aln.AirPodsService + +@Composable +fun IndependentToggle(name: String, service: AirPodsService, functionName: String, sharedPreferences: SharedPreferences, default: Boolean = false) { + val isDarkTheme = isSystemInDarkTheme() + 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) + ) + .clickable { + checked = !checked + sharedPreferences + .edit() + .putBoolean(snakeCasedName, checked) + .apply() + + val method = service::class.java.getMethod(functionName, Boolean::class.java) + method.invoke(service, checked) + }, + ) + { + Row( + modifier = Modifier + .fillMaxWidth() + .height(55.dp) + .padding(horizontal = 12.dp), + 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) + }, + ) + } + } +} + +@Preview +@Composable +fun IndependentTogglePreview() { + IndependentToggle("Test", AirPodsService(), "test", LocalContext.current.getSharedPreferences("preview", 0), true) +} \ No newline at end of file diff --git a/android/app/src/main/java/me/kavishdevar/aln/composables/LoudSoundReductionSwitch.kt b/android/app/src/main/java/me/kavishdevar/aln/composables/LoudSoundReductionSwitch.kt new file mode 100644 index 0000000..269467b --- /dev/null +++ b/android/app/src/main/java/me/kavishdevar/aln/composables/LoudSoundReductionSwitch.kt @@ -0,0 +1,108 @@ +package me.kavishdevar.aln.composables + +import android.content.SharedPreferences +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.gestures.detectTapGestures +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import me.kavishdevar.aln.AirPodsService + +@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 = isSystemInDarkTheme() + 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) + }, + ) + } +} + +@Preview +@Composable +fun LoudSoundReductionSwitchPreview() { + LoudSoundReductionSwitch(AirPodsService(), LocalContext.current.getSharedPreferences("preview", 0)) +} \ No newline at end of file diff --git a/android/app/src/main/java/me/kavishdevar/aln/composables/NavigationButton.kt b/android/app/src/main/java/me/kavishdevar/aln/composables/NavigationButton.kt new file mode 100644 index 0000000..44f5090 --- /dev/null +++ b/android/app/src/main/java/me/kavishdevar/aln/composables/NavigationButton.kt @@ -0,0 +1,70 @@ +package me.kavishdevar.aln.composables + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.KeyboardArrowRight +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.IconButtonDefaults +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.navigation.NavController + + +@Composable +fun NavigationButton(to: String, name: String, navController: NavController) { + Row( + modifier = Modifier + .background( + if (isSystemInDarkTheme()) Color( + 0xFF1C1C1E + ) else Color(0xFFFFFFFF), RoundedCornerShape(14.dp) + ) + .height(55.dp) + .clickable { + navController.navigate(to) + } + ) { + Text( + text = name, + modifier = Modifier.padding(16.dp), + color = if (isSystemInDarkTheme()) Color.White else Color.Black + ) + Spacer(modifier = Modifier.weight(1f)) + IconButton( + onClick = { navController.navigate(to) }, + colors = IconButtonDefaults.iconButtonColors( + containerColor = Color.Transparent, + contentColor = if (isSystemInDarkTheme()) Color.White else Color.Black + ), + modifier = Modifier + .padding(start = 16.dp) + .fillMaxHeight() + ) { + @Suppress("DEPRECATION") + Icon( + imageVector = Icons.Default.KeyboardArrowRight, + contentDescription = name + ) + } + } +} + +@Preview +@Composable +fun NavigationButtonPreview() { + NavigationButton("to", "Name", NavController(LocalContext.current)) +} \ No newline at end of file diff --git a/android/app/src/main/java/me/kavishdevar/aln/composables/NoiseControlButton.kt b/android/app/src/main/java/me/kavishdevar/aln/composables/NoiseControlButton.kt new file mode 100644 index 0000000..34ddb24 --- /dev/null +++ b/android/app/src/main/java/me/kavishdevar/aln/composables/NoiseControlButton.kt @@ -0,0 +1,62 @@ +package me.kavishdevar.aln.composables + +import me.kavishdevar.aln.R +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Icon +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.ImageBitmap +import androidx.compose.ui.res.imageResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp + +@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) + ) + } +} + +@Preview +@Composable +fun NoiseControlButtonPreview() { + NoiseControlButton( + icon = ImageBitmap.imageResource(R.drawable.noise_cancellation), + onClick = {}, + textColor = Color.White, + backgroundColor = Color.Black + ) +} \ No newline at end of file diff --git a/android/app/src/main/java/me/kavishdevar/aln/composables/NoiseControlSettings.kt b/android/app/src/main/java/me/kavishdevar/aln/composables/NoiseControlSettings.kt new file mode 100644 index 0000000..e9389ce --- /dev/null +++ b/android/app/src/main/java/me/kavishdevar/aln/composables/NoiseControlSettings.kt @@ -0,0 +1,231 @@ +package me.kavishdevar.aln.composables + +import android.annotation.SuppressLint +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.os.Build +import androidx.compose.foundation.background +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.VerticalDivider +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableFloatStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.ImageBitmap +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.imageResource +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight +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 me.kavishdevar.aln.AirPodsNotifications +import me.kavishdevar.aln.AirPodsService +import me.kavishdevar.aln.NoiseControlMode +import me.kavishdevar.aln.R + +@SuppressLint("UnspecifiedRegisterReceiverFlag") +@Composable +fun NoiseControlSettings(service: AirPodsService) { + val isDarkTheme = isSystemInDarkTheme() + 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 d1a = remember { mutableFloatStateOf(0f) } + val d2a = remember { mutableFloatStateOf(0f) } + val d3a = remember { mutableFloatStateOf(0f) } + + fun onModeSelected(mode: NoiseControlMode, received: Boolean = false) { + noiseControlMode.value = mode + if (!received) 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 + } + } + } + + val noiseControlReceiver = remember { + object : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + if (intent.action == AirPodsNotifications.ANC_DATA) { + noiseControlMode.value = NoiseControlMode.entries.toTypedArray()[intent.getIntExtra("data", 3) - 1] + onModeSelected(noiseControlMode.value, true) + } + else if (intent.action == AirPodsNotifications.DISCONNECT_RECEIVERS) { + context.unregisterReceiver(this) + } + } + } + } + + val context = LocalContext.current + val noiseControlIntentFilter = IntentFilter() + .apply { + addAction(AirPodsNotifications.ANC_DATA) + addAction(AirPodsNotifications.DISCONNECT_RECEIVERS) + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + context.registerReceiver(noiseControlReceiver, noiseControlIntentFilter, Context.RECEIVER_EXPORTED) + } + else { + context.registerReceiver(noiseControlReceiver, noiseControlIntentFilter) + } + + 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) + ) { + 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) + ) + } + } +} + +@Preview +@Composable +fun NoiseControlSettingsPreview() { + NoiseControlSettings(AirPodsService()) +} \ No newline at end of file diff --git a/android/app/src/main/java/me/kavishdevar/aln/composables/PersonalizedVolumeSwitch.kt b/android/app/src/main/java/me/kavishdevar/aln/composables/PersonalizedVolumeSwitch.kt new file mode 100644 index 0000000..26f3699 --- /dev/null +++ b/android/app/src/main/java/me/kavishdevar/aln/composables/PersonalizedVolumeSwitch.kt @@ -0,0 +1,108 @@ +package me.kavishdevar.aln.composables + +import android.content.SharedPreferences +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.gestures.detectTapGestures +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import me.kavishdevar.aln.AirPodsService + +@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 = isSystemInDarkTheme() + 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) + }, + ) + } +} + +@Preview +@Composable +fun PersonalizedVolumeSwitchPreview() { + PersonalizedVolumeSwitch(service = AirPodsService(), sharedPreferences = LocalContext.current.getSharedPreferences("preview", 0)) +} \ No newline at end of file diff --git a/android/app/src/main/java/me/kavishdevar/aln/composables/PressAndHoldSettings.kt b/android/app/src/main/java/me/kavishdevar/aln/composables/PressAndHoldSettings.kt new file mode 100644 index 0000000..62a4db9 --- /dev/null +++ b/android/app/src/main/java/me/kavishdevar/aln/composables/PressAndHoldSettings.kt @@ -0,0 +1,174 @@ +package me.kavishdevar.aln.composables + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.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 + +@Composable +fun PressAndHoldSettings(navController: NavController) { + val isDarkTheme = isSystemInDarkTheme() + val textColor = if (isDarkTheme) Color.White else Color.Black + + Text( + text = "PRESS AND HOLD AIRPODS", + style = TextStyle( + fontSize = 14.sp, + fontWeight = FontWeight.Light, + color = textColor.copy(alpha = 0.6f), + fontFamily = FontFamily(Font(R.font.sf_pro)) + ), + 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) + ) { + Box( + modifier = Modifier + .fillMaxWidth() + .height(55.dp) + .background( + backgroundColor, + RoundedCornerShape(14.dp) + ) + .clickable( + onClick = { + navController.navigate("long_press/Left") + } + ), + contentAlignment = Alignment.Center + ) { + Row( + modifier = Modifier + .padding(start = 16.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = "Left", + style = TextStyle( + fontSize = 18.sp, + color = textColor, + fontFamily = FontFamily(Font(R.font.sf_pro)) + ), + ) + Spacer(modifier = Modifier.weight(1f)) + Text( + // TODO: Implement voice assistant on long press; for now, it's noise control + text = "Noise Control", + style = TextStyle( + fontSize = 18.sp, + color = textColor.copy(alpha = 0.6f), + fontFamily = FontFamily(Font(R.font.sf_pro)) + ), + ) + IconButton( + onClick = { + navController.navigate("long_press/Left") + } + ) { + Icon( + imageVector = Icons.AutoMirrored.Filled.KeyboardArrowRight, + contentDescription = "go", + tint = textColor + ) + } + } + } + HorizontalDivider( + thickness = 2.dp, + color = Color(0xFF4D4D4D).copy(alpha = 0.4f), + modifier = Modifier + .padding(start = 16.dp) + ) + Box( + modifier = Modifier + .fillMaxWidth() + .height(55.dp) + .background( + backgroundColor, + RoundedCornerShape(18.dp) + ) + .clickable( + onClick = { + navController.navigate("long_press/Right") + } + ), + contentAlignment = Alignment.Center + ) { + Row( + modifier = Modifier + .padding(start = 16.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = "Right", + style = TextStyle( + fontSize = 18.sp, + color = textColor, + fontFamily = FontFamily(Font(R.font.sf_pro)) + ), + ) + Spacer(modifier = Modifier.weight(1f)) + Text( + // TODO: Implement voice assistant on long press; for now, it's noise control + text = "Noise Control", + style = TextStyle( + fontSize = 18.sp, + color = textColor.copy(alpha = 0.6f), + fontFamily = FontFamily(Font(R.font.sf_pro)) + ), + ) + IconButton( + onClick = { + navController.navigate("long_press/Right") + } + ) { + Icon( + imageVector = Icons.AutoMirrored.Filled.KeyboardArrowRight, + contentDescription = "go", + tint = textColor + ) + } + } + } + } +} + +@Preview +@Composable +fun PressAndHoldSettingsPreview() { + PressAndHoldSettings(navController = NavController(LocalContext.current)) +} \ No newline at end of file diff --git a/android/app/src/main/java/me/kavishdevar/aln/composables/SinglePodANCSwitch.kt b/android/app/src/main/java/me/kavishdevar/aln/composables/SinglePodANCSwitch.kt index ba712eb..b726cc9 100644 --- a/android/app/src/main/java/me/kavishdevar/aln/composables/SinglePodANCSwitch.kt +++ b/android/app/src/main/java/me/kavishdevar/aln/composables/SinglePodANCSwitch.kt @@ -5,6 +5,7 @@ import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer @@ -12,7 +13,6 @@ 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.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue @@ -22,8 +22,9 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.luminance import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import me.kavishdevar.aln.AirPodsService @@ -42,7 +43,7 @@ fun SinglePodANCSwitch(service: AirPodsService, sharedPreferences: SharedPrefere service.setNoiseCancellationWithOnePod(enabled) } - val isDarkTheme = MaterialTheme.colorScheme.surface.luminance() < 0.5 + val isDarkTheme = isSystemInDarkTheme() val textColor = if (isDarkTheme) Color.White else Color.Black val isPressed = remember { mutableStateOf(false) } @@ -97,4 +98,10 @@ fun SinglePodANCSwitch(service: AirPodsService, sharedPreferences: SharedPrefere }, ) } +} + +@Preview +@Composable +fun SinglePodANCSwitchPreview() { + SinglePodANCSwitch(service = AirPodsService(), sharedPreferences = LocalContext.current.getSharedPreferences("preview", 0)) } \ No newline at end of file diff --git a/android/app/src/main/java/me/kavishdevar/aln/composables/StyledSwitch.kt b/android/app/src/main/java/me/kavishdevar/aln/composables/StyledSwitch.kt index 070a950..e0c423e 100644 --- a/android/app/src/main/java/me/kavishdevar/aln/composables/StyledSwitch.kt +++ b/android/app/src/main/java/me/kavishdevar/aln/composables/StyledSwitch.kt @@ -3,6 +3,7 @@ package me.kavishdevar.aln.composables import androidx.compose.animation.core.animateDpAsState import androidx.compose.foundation.background import androidx.compose.foundation.clickable +import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.offset @@ -11,14 +12,13 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.luminance +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @Composable @@ -26,7 +26,7 @@ fun StyledSwitch( checked: Boolean, onCheckedChange: (Boolean) -> Unit ) { - val isDarkTheme = MaterialTheme.colorScheme.surface.luminance() < 0.5 + val isDarkTheme = isSystemInDarkTheme() val thumbColor = Color.White val trackColor = if (checked) Color(0xFF34C759) else if (isDarkTheme) Color(0xFF5B5B5E) else Color(0xFFD1D1D6) @@ -53,3 +53,9 @@ fun StyledSwitch( ) } } + +@Preview +@Composable +fun StyledSwitchPreview() { + StyledSwitch(checked = true, onCheckedChange = {}) +} \ No newline at end of file diff --git a/android/app/src/main/java/me/kavishdevar/aln/composables/StyledTextField.kt b/android/app/src/main/java/me/kavishdevar/aln/composables/StyledTextField.kt index ced1929..df329d5 100644 --- a/android/app/src/main/java/me/kavishdevar/aln/composables/StyledTextField.kt +++ b/android/app/src/main/java/me/kavishdevar/aln/composables/StyledTextField.kt @@ -1,6 +1,7 @@ package me.kavishdevar.aln.composables import androidx.compose.foundation.background +import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth @@ -8,7 +9,6 @@ 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.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue @@ -20,8 +20,8 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.SolidColor -import androidx.compose.ui.graphics.luminance import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp @@ -33,7 +33,7 @@ fun StyledTextField( ) { var isFocused by remember { mutableStateOf(false) } - val isDarkTheme = MaterialTheme.colorScheme.surface.luminance() < 0.5 + val isDarkTheme = isSystemInDarkTheme() val backgroundColor = if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFFFFFFF) val textColor = if (isDarkTheme) Color.White else Color.Black @@ -86,4 +86,10 @@ fun StyledTextField( } ) } +} + +@Preview +@Composable +fun StyledTextFieldPreview() { + StyledTextField(name = "Name", value = "AirPods Pro", onValueChange = {}) } \ No newline at end of file diff --git a/android/app/src/main/java/me/kavishdevar/aln/composables/ToneVolumeSlider.kt b/android/app/src/main/java/me/kavishdevar/aln/composables/ToneVolumeSlider.kt index 1940527..8b9ad21 100644 --- a/android/app/src/main/java/me/kavishdevar/aln/composables/ToneVolumeSlider.kt +++ b/android/app/src/main/java/me/kavishdevar/aln/composables/ToneVolumeSlider.kt @@ -2,6 +2,7 @@ package me.kavishdevar.aln.composables import android.content.SharedPreferences import androidx.compose.foundation.background +import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Row @@ -12,7 +13,6 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Slider import androidx.compose.material3.SliderDefaults import androidx.compose.material3.Text @@ -24,11 +24,12 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.shadow import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.luminance +import androidx.compose.ui.platform.LocalContext 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 me.kavishdevar.aln.AirPodsService @@ -48,7 +49,7 @@ fun ToneVolumeSlider(service: AirPodsService, sharedPreferences: SharedPreferenc sharedPreferences.edit().putInt("tone_volume", sliderValue.floatValue.toInt()).apply() } - val isDarkTheme = MaterialTheme.colorScheme.surface.luminance() < 0.5 + val isDarkTheme = isSystemInDarkTheme() val trackColor = if (isDarkTheme) Color(0xFFB3B3B3) else Color(0xFF929491) val activeTrackColor = if (isDarkTheme) Color(0xFF007AFF) else Color(0xFF3C6DF5) @@ -132,4 +133,10 @@ fun ToneVolumeSlider(service: AirPodsService, sharedPreferences: SharedPreferenc modifier = Modifier.padding(end = 4.dp) ) } +} + +@Preview +@Composable +fun ToneVolumeSliderPreview() { + ToneVolumeSlider(AirPodsService(), sharedPreferences = LocalContext.current.getSharedPreferences("preview", 0)) } \ No newline at end of file diff --git a/android/app/src/main/java/me/kavishdevar/aln/composables/VolumeControlSwitch.kt b/android/app/src/main/java/me/kavishdevar/aln/composables/VolumeControlSwitch.kt index b352d20..5199e50 100644 --- a/android/app/src/main/java/me/kavishdevar/aln/composables/VolumeControlSwitch.kt +++ b/android/app/src/main/java/me/kavishdevar/aln/composables/VolumeControlSwitch.kt @@ -5,6 +5,7 @@ import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer @@ -12,7 +13,6 @@ 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.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue @@ -22,8 +22,9 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.luminance import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import me.kavishdevar.aln.AirPodsService @@ -41,7 +42,7 @@ fun VolumeControlSwitch(service: AirPodsService, sharedPreferences: SharedPrefer service.setVolumeControl(enabled) } - val isDarkTheme = MaterialTheme.colorScheme.surface.luminance() < 0.5 + val isDarkTheme = isSystemInDarkTheme() val textColor = if (isDarkTheme) Color.White else Color.Black val isPressed = remember { mutableStateOf(false) } @@ -96,4 +97,10 @@ fun VolumeControlSwitch(service: AirPodsService, sharedPreferences: SharedPrefer }, ) } +} + +@Preview +@Composable +fun VolumeControlSwitchPreview() { + VolumeControlSwitch(service = AirPodsService(), sharedPreferences = LocalContext.current.getSharedPreferences("preview", 0)) } \ No newline at end of file diff --git a/android/app/src/main/java/me/kavishdevar/aln/receiver.kt b/android/app/src/main/java/me/kavishdevar/aln/receiver.kt deleted file mode 100644 index ab96720..0000000 --- a/android/app/src/main/java/me/kavishdevar/aln/receiver.kt +++ /dev/null @@ -1,62 +0,0 @@ -package me.kavishdevar.aln - -import android.annotation.SuppressLint -import android.bluetooth.BluetoothDevice -import android.content.BroadcastReceiver -import android.content.Context -import android.content.Intent -import android.content.IntentFilter - -class BluetoothReceiver : BroadcastReceiver() { - fun onConnect(bluetoothDevice: BluetoothDevice?) { - - } - - fun onDisconnect(bluetoothDevice: BluetoothDevice?) { - - } - - @SuppressLint("NewApi") - override fun onReceive(context: Context?, intent: Intent) { - val bluetoothDevice = - intent.getParcelableExtra("android.bluetooth.device.extra.DEVICE", BluetoothDevice::class.java) - val action = intent.action - - // Airpods filter - if (bluetoothDevice != null && action != null && !action.isEmpty()) { - // Airpods connected, show notification. - if (BluetoothDevice.ACTION_ACL_CONNECTED == action) { - onConnect(bluetoothDevice) - } - - // Airpods disconnected, remove notification but leave the scanner going. - if (BluetoothDevice.ACTION_ACL_DISCONNECTED == action - || BluetoothDevice.ACTION_ACL_DISCONNECT_REQUESTED == action - ) { - onDisconnect(bluetoothDevice) - } - } - } - - companion object { - /** - * When the service is created, we register to get as many bluetooth and airpods related events as possible. - * ACL_CONNECTED and ACL_DISCONNECTED should have been enough, but you never know with android these days. - */ - fun buildFilter(): IntentFilter { - val intentFilter = IntentFilter() - intentFilter.addAction("android.bluetooth.device.action.ACL_CONNECTED") - intentFilter.addAction("android.bluetooth.device.action.ACL_DISCONNECTED") - intentFilter.addAction("android.bluetooth.device.action.BOND_STATE_CHANGED") - intentFilter.addAction("android.bluetooth.device.action.NAME_CHANGED") - intentFilter.addAction("android.bluetooth.adapter.action.CONNECTION_STATE_CHANGED") - intentFilter.addAction("android.bluetooth.adapter.action.STATE_CHANGED") - intentFilter.addAction("android.bluetooth.headset.profile.action.CONNECTION_STATE_CHANGED") - intentFilter.addAction("android.bluetooth.headset.action.VENDOR_SPECIFIC_HEADSET_EVENT") - intentFilter.addAction("android.bluetooth.a2dp.profile.action.CONNECTION_STATE_CHANGED") - intentFilter.addAction("android.bluetooth.a2dp.profile.action.PLAYING_STATE_CHANGED") - intentFilter.addCategory("android.bluetooth.headset.intent.category.companyid.76") - return intentFilter - } - } -} \ No newline at end of file diff --git a/android/app/src/main/java/me/kavishdevar/aln/receivers/BootReceiver.kt b/android/app/src/main/java/me/kavishdevar/aln/receivers/BootReceiver.kt new file mode 100644 index 0000000..ac67456 --- /dev/null +++ b/android/app/src/main/java/me/kavishdevar/aln/receivers/BootReceiver.kt @@ -0,0 +1,25 @@ +package me.kavishdevar.aln.receivers + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import me.kavishdevar.aln.AirPodsService + +class BootReceiver: BroadcastReceiver() { + override fun onReceive(context: Context?, intent: Intent?) { + when (intent?.action) { + Intent.ACTION_MY_PACKAGE_REPLACED -> try { context?.startForegroundService( + Intent( + context, + AirPodsService::class.java + ) + ) } catch (e: Exception) { e.printStackTrace() } + Intent.ACTION_BOOT_COMPLETED -> try { context?.startForegroundService( + Intent( + context, + AirPodsService::class.java + ) + ) } catch (e: Exception) { e.printStackTrace() } + } + } +} \ No newline at end of file diff --git a/android/app/src/main/java/me/kavishdevar/aln/screens/AirPodsSettingsScreen.kt b/android/app/src/main/java/me/kavishdevar/aln/screens/AirPodsSettingsScreen.kt new file mode 100644 index 0000000..d232681 --- /dev/null +++ b/android/app/src/main/java/me/kavishdevar/aln/screens/AirPodsSettingsScreen.kt @@ -0,0 +1,261 @@ +package me.kavishdevar.aln.screens + +import android.annotation.SuppressLint +import android.bluetooth.BluetoothDevice +import android.content.Context.MODE_PRIVATE +import android.content.Intent +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +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.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.CenterAlignedTopAppBar +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +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.drawBehind +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.TextStyle +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 androidx.navigation.compose.rememberNavController +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 me.kavishdevar.aln.AirPodsNotifications +import me.kavishdevar.aln.AirPodsService +import me.kavishdevar.aln.composables.AccessibilitySettings +import me.kavishdevar.aln.composables.AudioSettings +import me.kavishdevar.aln.composables.BatteryView +import me.kavishdevar.aln.composables.IndependentToggle +import me.kavishdevar.aln.composables.NavigationButton +import me.kavishdevar.aln.composables.NoiseControlSettings +import me.kavishdevar.aln.composables.PressAndHoldSettings +import me.kavishdevar.aln.composables.StyledTextField +import me.kavishdevar.aln.ui.theme.ALNTheme + +@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 ?: "AirPods Pro").toString() + ) + ) + } + val verticalScrollState = rememberScrollState() + val hazeState = remember { HazeState() } + + @SuppressLint("UnusedMaterial3ScaffoldPaddingParameter") + Scaffold( + containerColor = if (isSystemInDarkTheme()) Color( + 0xFF000000 + ) else Color( + 0xFFF2F2F7 + ), + topBar = { + val darkMode = isSystemInDarkTheme() + val mDensity = remember { mutableFloatStateOf(1f) } + CenterAlignedTopAppBar( + title = { + Text( + text = deviceName.text + ) + }, + modifier = Modifier + .hazeChild( + state = hazeState, + style = CupertinoMaterials.regular(), + block = { + alpha = + if (verticalScrollState.value > 55.dp.value * mDensity.floatValue) 1f else 0f + } + ) + .drawBehind { + mDensity.floatValue = 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 = { +// ServiceManager.restartService(context) +// }, +// colors = IconButtonDefaults.iconButtonColors( +// containerColor = Color.Transparent, +// contentColor = if (isSystemInDarkTheme()) 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.Companion.BATTERY_DATA).apply { + putParcelableArrayListExtra("data", ArrayList(it.getBattery())) + }) + it.sendBroadcast(Intent(AirPodsNotifications.Companion.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)) + PressAndHoldSettings(navController = navController) + + 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)) + NavigationButton("debug", "Debug", navController) + 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 (isSystemInDarkTheme()) 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 (isSystemInDarkTheme()) Color.White else Color.Black + ), + textAlign = TextAlign.Center, + modifier = Modifier.fillMaxWidth() + ) + } + } + } +} + + +@Preview +@Composable +fun AirPodsSettingsScreenPreview() { + ALNTheme ( + darkTheme = true + ) { + AirPodsSettingsScreen(dev = null, service = AirPodsService(), navController = rememberNavController(), isConnected = true) + } +} \ No newline at end of file diff --git a/android/app/src/main/java/me/kavishdevar/aln/DebugScreen.kt b/android/app/src/main/java/me/kavishdevar/aln/screens/DebugScreen.kt similarity index 88% rename from android/app/src/main/java/me/kavishdevar/aln/DebugScreen.kt rename to android/app/src/main/java/me/kavishdevar/aln/screens/DebugScreen.kt index a825446..5afdda5 100644 --- a/android/app/src/main/java/me/kavishdevar/aln/DebugScreen.kt +++ b/android/app/src/main/java/me/kavishdevar/aln/screens/DebugScreen.kt @@ -1,6 +1,6 @@ @file:OptIn(ExperimentalHazeMaterialsApi::class) -package me.kavishdevar.aln +package me.kavishdevar.aln.screens import android.annotation.SuppressLint import android.content.BroadcastReceiver @@ -13,6 +13,7 @@ import android.os.Build import android.os.IBinder import android.util.Log import androidx.compose.foundation.background +import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -34,7 +35,6 @@ import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.IconButton -import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.TextField @@ -49,7 +49,6 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.luminance import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.font.Font import androidx.compose.ui.text.font.FontFamily @@ -62,6 +61,9 @@ import dev.chrisbanes.haze.haze import dev.chrisbanes.haze.hazeChild import dev.chrisbanes.haze.materials.CupertinoMaterials import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi +import me.kavishdevar.aln.AirPodsNotifications +import me.kavishdevar.aln.AirPodsService +import me.kavishdevar.aln.R @OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class) @SuppressLint("UnusedMaterial3ScaffoldPaddingParameter") @@ -97,10 +99,10 @@ fun DebugScreen(navController: NavController) { ), colors = TopAppBarDefaults.topAppBarColors( containerColor = Color.Transparent - ) + ), ) }, - containerColor = if (MaterialTheme.colorScheme.surface.luminance() < 0.5) Color(0xFF000000) + containerColor = if (isSystemInDarkTheme()) Color(0xFF000000) else Color(0xFFF2F2F7), ) { paddingValues -> val receiver = object : BroadcastReceiver() { @@ -113,7 +115,7 @@ fun DebugScreen(navController: NavController) { } LaunchedEffect(context) { - val intentFilter = IntentFilter(AirPodsNotifications.AIRPODS_DATA) + val intentFilter = IntentFilter(AirPodsNotifications.Companion.AIRPODS_DATA) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { context.registerReceiver(receiver, intentFilter, Context.RECEIVER_EXPORTED) } @@ -166,7 +168,7 @@ fun DebugScreen(navController: NavController) { Text( text = if (isSent) message.substring(1) else message, fontFamily = FontFamily(Font(R.font.hack)), - color = if (MaterialTheme.colorScheme.surface.luminance() < 0.5) Color( + color = if (isSystemInDarkTheme()) Color( 0xFF000000 ) else Color(0xFF000000), @@ -202,7 +204,7 @@ fun DebugScreen(navController: NavController) { Row( modifier = Modifier .fillMaxWidth() - .background(if (MaterialTheme.colorScheme.surface.luminance() < 0.5) Color(0xFF1C1B20) else Color(0xFFF2F2F7)), + .background(if (isSystemInDarkTheme()) Color(0xFF1C1B20) else Color(0xFFF2F2F7)), verticalAlignment = Alignment.CenterVertically ) { val packet = remember { mutableStateOf(TextFieldValue("")) } @@ -227,14 +229,14 @@ fun DebugScreen(navController: NavController) { } }, colors = TextFieldDefaults.colors( - focusedContainerColor = if (MaterialTheme.colorScheme.surface.luminance() < 0.5) Color(0xFF1C1B20) else Color(0xFFF2F2F7), - unfocusedContainerColor = if (MaterialTheme.colorScheme.surface.luminance() < 0.5) Color(0xFF1C1B20) else Color(0xFFF2F2F7), + focusedContainerColor = if (isSystemInDarkTheme()) Color(0xFF1C1B20) else Color(0xFFF2F2F7), + unfocusedContainerColor = if (isSystemInDarkTheme()) Color(0xFF1C1B20) else Color(0xFFF2F2F7), focusedIndicatorColor = Color.Transparent, unfocusedIndicatorColor = Color.Transparent, - focusedTextColor = if (MaterialTheme.colorScheme.surface.luminance() < 0.5) Color.White else Color.Black, - unfocusedTextColor = if (MaterialTheme.colorScheme.surface.luminance() < 0.5) Color.White else Color.Black.copy(alpha = 0.6f), - focusedLabelColor = if (MaterialTheme.colorScheme.surface.luminance() < 0.5) Color.White.copy(alpha = 0.6f) else Color.Black, - unfocusedLabelColor = if (MaterialTheme.colorScheme.surface.luminance() < 0.5) Color.White.copy(alpha = 0.6f) else Color.Black.copy(alpha = 0.6f), + focusedTextColor = if (isSystemInDarkTheme()) Color.White else Color.Black, + unfocusedTextColor = if (isSystemInDarkTheme()) Color.White else Color.Black.copy(alpha = 0.6f), + focusedLabelColor = if (isSystemInDarkTheme()) Color.White.copy(alpha = 0.6f) else Color.Black, + unfocusedLabelColor = if (isSystemInDarkTheme()) Color.White.copy(alpha = 0.6f) else Color.Black.copy(alpha = 0.6f), ), shape = RoundedCornerShape(12.dp) ) diff --git a/android/app/src/main/java/me/kavishdevar/aln/LongPress.kt b/android/app/src/main/java/me/kavishdevar/aln/screens/PressAndHoldSettingsScreen.kt similarity index 96% rename from android/app/src/main/java/me/kavishdevar/aln/LongPress.kt rename to android/app/src/main/java/me/kavishdevar/aln/screens/PressAndHoldSettingsScreen.kt index 407d0c3..3526286 100644 --- a/android/app/src/main/java/me/kavishdevar/aln/LongPress.kt +++ b/android/app/src/main/java/me/kavishdevar/aln/screens/PressAndHoldSettingsScreen.kt @@ -1,9 +1,10 @@ -package me.kavishdevar.aln +package me.kavishdevar.aln.screens import android.content.Context import android.util.Log import androidx.compose.foundation.background import androidx.compose.foundation.clickable +import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -21,7 +22,6 @@ import androidx.compose.material3.CheckboxDefaults import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon -import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.TextButton @@ -35,7 +35,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.scale import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.ImageBitmap -import androidx.compose.ui.graphics.luminance import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.imageResource import androidx.compose.ui.text.TextStyle @@ -45,6 +44,8 @@ import androidx.compose.ui.text.font.FontWeight 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.ServiceManager @Composable() fun RightDivider() { @@ -64,7 +65,7 @@ fun LongPress(navController: NavController, name: String) { val transparencyChecked = remember { mutableStateOf(sharedPreferences.getBoolean("long_press_transparency", false)) } val adaptiveChecked = remember { mutableStateOf(sharedPreferences.getBoolean("long_press_adaptive", false)) } Log.d("LongPress", "offChecked: ${offChecked.value}, ncChecked: ${ncChecked.value}, transparencyChecked: ${transparencyChecked.value}, adaptiveChecked: ${adaptiveChecked.value}") - val isDarkTheme = MaterialTheme.colorScheme.surface.luminance() < 0.5 + val isDarkTheme = isSystemInDarkTheme() val textColor = if (isDarkTheme) Color.White else Color.Black Scaffold( @@ -81,7 +82,7 @@ fun LongPress(navController: NavController, name: String) { onClick = { navController.popBackStack() }, - shape = RoundedCornerShape(24.dp), + shape = RoundedCornerShape(8.dp), ) { Icon( Icons.AutoMirrored.Filled.KeyboardArrowLeft, @@ -105,7 +106,7 @@ fun LongPress(navController: NavController, name: String) { ) ) }, - containerColor = if (MaterialTheme.colorScheme.surface.luminance() < 0.5) Color(0xFF000000) + containerColor = if (isSystemInDarkTheme()) Color(0xFF000000) else Color(0xFFF2F2F7), ) { paddingValues -> val backgroundColor = if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFFFFFFF) @@ -160,7 +161,7 @@ fun LongPressElement (name: String, checked: MutableState, id: String, val sharedPreferences = LocalContext.current.getSharedPreferences("settings", Context.MODE_PRIVATE) val offListeningMode = sharedPreferences.getBoolean("off_listening_mode", false) - val darkMode = MaterialTheme.colorScheme.surface.luminance() < 0.5 + val darkMode = isSystemInDarkTheme() val textColor = if (darkMode) Color.White else Color.Black val desc = when (name) { "Off" -> "Turns off noise management" diff --git a/android/app/src/main/java/me/kavishdevar/aln/AirPodsQSService.kt b/android/app/src/main/java/me/kavishdevar/aln/services/AirPodsQSService.kt similarity index 89% rename from android/app/src/main/java/me/kavishdevar/aln/AirPodsQSService.kt rename to android/app/src/main/java/me/kavishdevar/aln/services/AirPodsQSService.kt index 6247a3c..89ce7e1 100644 --- a/android/app/src/main/java/me/kavishdevar/aln/AirPodsQSService.kt +++ b/android/app/src/main/java/me/kavishdevar/aln/services/AirPodsQSService.kt @@ -1,4 +1,4 @@ -package me.kavishdevar.aln +package me.kavishdevar.aln.services import android.annotation.SuppressLint import android.content.BroadcastReceiver @@ -8,6 +8,9 @@ import android.content.IntentFilter import android.service.quicksettings.Tile import android.service.quicksettings.TileService import android.util.Log +import me.kavishdevar.aln.AirPodsNotifications +import me.kavishdevar.aln.NoiseControlMode +import me.kavishdevar.aln.ServiceManager class AirPodsQSService: TileService() { private val ancModes = listOf(NoiseControlMode.NOISE_CANCELLATION.name, NoiseControlMode.TRANSPARENCY.name, NoiseControlMode.ADAPTIVE.name) @@ -46,18 +49,19 @@ class AirPodsQSService: TileService() { availabilityReceiver = object : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { - if (intent.action == AirPodsNotifications.AIRPODS_CONNECTED) { + if (intent.action == AirPodsNotifications.Companion.AIRPODS_CONNECTED) { qsTile.state = Tile.STATE_ACTIVE qsTile.updateTile() } - else if (intent.action == AirPodsNotifications.AIRPODS_DISCONNECTED) { + else if (intent.action == AirPodsNotifications.Companion.AIRPODS_DISCONNECTED) { qsTile.state = Tile.STATE_UNAVAILABLE qsTile.updateTile() } } } - registerReceiver(ancStatusReceiver, IntentFilter(AirPodsNotifications.ANC_DATA), RECEIVER_EXPORTED) + registerReceiver(ancStatusReceiver, + IntentFilter(AirPodsNotifications.Companion.ANC_DATA), RECEIVER_EXPORTED) qsTile.state = if (ServiceManager.getService()?.isConnected == true) Tile.STATE_ACTIVE else Tile.STATE_UNAVAILABLE val ancIndex = ServiceManager.getService()?.getANC() diff --git a/android/app/src/main/java/me/kavishdevar/aln/MediaController.kt b/android/app/src/main/java/me/kavishdevar/aln/utils/MediaController.kt similarity index 65% rename from android/app/src/main/java/me/kavishdevar/aln/MediaController.kt rename to android/app/src/main/java/me/kavishdevar/aln/utils/MediaController.kt index c0e0064..26bbe3b 100644 --- a/android/app/src/main/java/me/kavishdevar/aln/MediaController.kt +++ b/android/app/src/main/java/me/kavishdevar/aln/utils/MediaController.kt @@ -1,4 +1,4 @@ -package me.kavishdevar.aln +package me.kavishdevar.aln.utils import android.media.AudioManager import android.util.Log @@ -16,16 +16,36 @@ object MediaController { @Synchronized fun sendPause() { if (audioManager.isMusicActive) { - audioManager.dispatchMediaKeyEvent(KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_PAUSE)) - audioManager.dispatchMediaKeyEvent(KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_MEDIA_PAUSE)) + audioManager.dispatchMediaKeyEvent( + KeyEvent( + KeyEvent.ACTION_DOWN, + KeyEvent.KEYCODE_MEDIA_PAUSE + ) + ) + audioManager.dispatchMediaKeyEvent( + KeyEvent( + KeyEvent.ACTION_UP, + KeyEvent.KEYCODE_MEDIA_PAUSE + ) + ) } } @Synchronized fun sendPlay() { if (!audioManager.isMusicActive) { - audioManager.dispatchMediaKeyEvent(KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_PLAY)) - audioManager.dispatchMediaKeyEvent(KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_MEDIA_PLAY)) + audioManager.dispatchMediaKeyEvent( + KeyEvent( + KeyEvent.ACTION_DOWN, + KeyEvent.KEYCODE_MEDIA_PLAY + ) + ) + audioManager.dispatchMediaKeyEvent( + KeyEvent( + KeyEvent.ACTION_UP, + KeyEvent.KEYCODE_MEDIA_PLAY + ) + ) } }