split compose functions, and organize stuff

This commit is contained in:
Kavish Devar
2024-12-13 01:41:37 +05:30
parent 7b03a7e9f0
commit 809818f60d
32 changed files with 1733 additions and 1273 deletions

View File

@@ -67,7 +67,7 @@
android:foregroundServiceType="connectedDevice"
android:permission="android.permission.BLUETOOTH_CONNECT" />
<service
android:name=".AirPodsQSService"
android:name=".services.AirPodsQSService"
android:exported="true"
android:icon="@drawable/airpods"
android:label="ANC Mode"
@@ -78,7 +78,7 @@
</service>
<receiver
android:name=".BootReceiver"
android:name=".receivers.BootReceiver"
android:enabled="true"
android:exported="true"
android:label="@string/app_name">

View File

@@ -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)

View File

@@ -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() }
}
}
}

View File

@@ -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")!!
)
}
}

View File

@@ -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 {

View File

@@ -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))
}

View File

@@ -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))
}

View File

@@ -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))
}

View File

@@ -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)
}

View File

@@ -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)
}

View File

@@ -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))
}

View File

@@ -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)
}

View File

@@ -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))
}

View File

@@ -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))
}

View File

@@ -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
)
}

View File

@@ -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())
}

View File

@@ -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))
}

View File

@@ -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))
}

View File

@@ -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))
}

View File

@@ -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 = {})
}

View File

@@ -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 = {})
}

View File

@@ -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))
}

View File

@@ -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))
}

View File

@@ -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
}
}
}

View File

@@ -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() }
}
}
}

View File

@@ -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)
}
}

View File

@@ -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)
)

View File

@@ -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<Boolean>, 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"

View File

@@ -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()

View File

@@ -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
)
)
}
}