move files across computers

This commit is contained in:
Kavish Devar
2025-01-25 00:00:43 +05:30
parent 938278b0b5
commit a6d7bd704a
17 changed files with 628 additions and 290 deletions

View File

@@ -18,6 +18,7 @@
<uses-permission android:name="android.permission.BLUETOOTH_SCAN"
android:usesPermissionFlags="neverForLocation"
tools:ignore="UnusedAttribute" />
<uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE" />
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"

View File

@@ -146,6 +146,15 @@ fun Main() {
isRemotelyConnected.value = CrossDevice.isAvailable
isConnected.value = false
}
else if (intent.action == AirPodsNotifications.DISCONNECT_RECEIVERS) {
Log.d("MainActivity", "Disconnect Receivers intent received")
try {
context.unregisterReceiver(this)
}
catch (e: Exception) {
Log.e("MainActivity", "Error while unregistering receiver: $e")
}
}
}
}
@@ -159,10 +168,12 @@ fun Main() {
sharedPreferences.registerOnSharedPreferenceChangeListener(isAvailableChangeListener)
Log.d("MainActivity", "CrossDeviceIsAvailable: ${sharedPreferences.getBoolean("CrossDeviceIsAvailable", false)} | isAvailable: ${CrossDevice.isAvailable}")
isRemotelyConnected.value = sharedPreferences.getBoolean("CrossDeviceIsAvailable", false) || CrossDevice.isAvailable
Log.d("MainActivity", "isRemotelyConnected: ${isRemotelyConnected.value}")
val filter = IntentFilter().apply {
addAction(AirPodsNotifications.AIRPODS_CONNECTED)
addAction(AirPodsNotifications.AIRPODS_DISCONNECTED)
addAction(AirPodsNotifications.AIRPODS_CONNECTION_DETECTED)
addAction(AirPodsNotifications.DISCONNECT_RECEIVERS)
}
Log.d("MainActivity", "Registering Receiver")

View File

@@ -148,7 +148,7 @@ fun DropdownMenuComponent(
Column (
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 8.dp)
.padding(horizontal = 12.dp)
) {
Text(
text = label,

View File

@@ -82,11 +82,11 @@ fun AdaptiveStrengthSlider(service: AirPodsService, sharedPreferences: SharedPre
value = sliderValue.floatValue,
onValueChange = {
sliderValue.floatValue = it
service.setAdaptiveStrength(100 - it.toInt())
},
valueRange = 0f..100f,
onValueChangeFinished = {
sliderValue.floatValue = sliderValue.floatValue.roundToInt().toFloat()
service.setAdaptiveStrength(100 - sliderValue.floatValue.toInt())
},
modifier = Modifier
.fillMaxWidth()

View File

@@ -18,7 +18,6 @@
package me.kavishdevar.aln.composables
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
@@ -26,7 +25,6 @@ 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
@@ -44,18 +42,18 @@ fun NoiseControlButton(
icon: ImageBitmap,
onClick: () -> Unit,
textColor: Color,
backgroundColor: Color,
modifier: Modifier = Modifier
modifier: Modifier = Modifier,
usePadding: Boolean = true
) {
Column(
modifier = modifier
.fillMaxHeight()
.padding(horizontal = 4.dp, vertical = 4.dp)
.background(color = backgroundColor, shape = RoundedCornerShape(11.dp))
.then(if (usePadding) Modifier.padding(horizontal = 4.dp, vertical = 4.dp) else Modifier)
.clickable(
onClick = onClick,
indication = null,
interactionSource = remember { MutableInteractionSource() }),
interactionSource = remember { MutableInteractionSource() }
),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
@@ -75,6 +73,5 @@ fun NoiseControlButtonPreview() {
icon = ImageBitmap.imageResource(R.drawable.noise_cancellation),
onClick = {},
textColor = Color.White,
backgroundColor = Color.Black
)
}

View File

@@ -25,42 +25,60 @@ import android.content.Intent
import android.content.IntentFilter
import android.content.SharedPreferences
import android.os.Build
import androidx.compose.animation.core.AnimationSpec
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.SpringSpec
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.gestures.draggable
import androidx.compose.foundation.gestures.rememberDraggableState
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
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.DisposableEffect
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.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.platform.LocalDensity
import androidx.compose.ui.res.imageResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.zIndex
import me.kavishdevar.aln.R
import me.kavishdevar.aln.services.AirPodsService
import me.kavishdevar.aln.utils.AirPodsNotifications
import me.kavishdevar.aln.utils.NoiseControlMode
import kotlin.math.roundToInt
@SuppressLint("UnspecifiedRegisterReceiverFlag")
@SuppressLint("UnspecifiedRegisterReceiverFlag", "UnusedBoxWithConstraintsScope")
@Composable
fun NoiseControlSettings(service: AirPodsService) {
val context = LocalContext.current
@@ -86,7 +104,7 @@ fun NoiseControlSettings(service: AirPodsService) {
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 selectedBackground = if (isDarkTheme) Color(0xBF5C5A5F) else Color(0xFFFFFFFF)
val noiseControlMode = remember { mutableStateOf(NoiseControlMode.OFF) }
@@ -161,118 +179,258 @@ fun NoiseControlSettings(service: AirPodsService) {
),
modifier = Modifier.padding(8.dp, bottom = 2.dp)
)
Column(
BoxWithConstraints(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 16.dp)
.padding(vertical = 8.dp) // Adjusted padding
) {
Box(
modifier = Modifier
.fillMaxWidth()
.height(75.dp)
.padding(8.dp)
val density = LocalDensity.current
val buttonCount = if (offListeningMode.value) 4 else 3
val buttonWidth = maxWidth / buttonCount
val isDragging = remember { mutableStateOf(false) }
var dragOffset by remember {
mutableFloatStateOf(
with(density) {
when(noiseControlMode.value) {
NoiseControlMode.OFF -> if (offListeningMode.value) 0f else buttonWidth.toPx()
NoiseControlMode.TRANSPARENCY -> if (offListeningMode.value) buttonWidth.toPx() else 0f
NoiseControlMode.ADAPTIVE -> if (offListeningMode.value) (buttonWidth * 2).toPx() else buttonWidth.toPx()
NoiseControlMode.NOISE_CANCELLATION -> if (offListeningMode.value) (buttonWidth * 3).toPx() else (buttonWidth * 2).toPx()
}
}
)
}
val animationSpec: AnimationSpec<Float> = SpringSpec(
dampingRatio = Spring.DampingRatioLowBouncy,
stiffness = Spring.StiffnessMediumLow,
visibilityThreshold = 0.01f
)
val targetOffset = buttonWidth * when(noiseControlMode.value) {
NoiseControlMode.OFF -> if (offListeningMode.value) 0 else 1
NoiseControlMode.TRANSPARENCY -> if (offListeningMode.value) 1 else 0
NoiseControlMode.ADAPTIVE -> if (offListeningMode.value) 2 else 1
NoiseControlMode.NOISE_CANCELLATION -> if (offListeningMode.value) 3 else 2
}
val animatedOffset by animateFloatAsState(
targetValue = with(density) {
if (isDragging.value) dragOffset else targetOffset.toPx()
},
animationSpec = animationSpec,
label = "selector"
)
Column(
modifier = Modifier.fillMaxWidth()
) {
Row(
Box(
modifier = Modifier
.fillMaxWidth()
.height(60.dp) // Adjusted height
.background(backgroundColor, RoundedCornerShape(14.dp))
) {
if (offListeningMode.value) {
// First: Background Row (just for visual)
Row(
modifier = Modifier.fillMaxWidth()
) {
if (offListeningMode.value) {
NoiseControlButton(
icon = ImageBitmap.imageResource(R.drawable.noise_cancellation),
onClick = { onModeSelected(NoiseControlMode.OFF) },
textColor = if (noiseControlMode.value == NoiseControlMode.OFF) textColorSelected else textColor,
modifier = Modifier.weight(1f),
usePadding = false
)
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.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)
icon = ImageBitmap.imageResource(R.drawable.transparency),
onClick = { onModeSelected(NoiseControlMode.TRANSPARENCY) },
textColor = if (noiseControlMode.value == NoiseControlMode.TRANSPARENCY) textColorSelected else textColor,
modifier = Modifier.weight(1f),
usePadding = false
)
VerticalDivider(
thickness = 1.dp,
modifier = Modifier
.padding(vertical = 10.dp)
.alpha(d1a.floatValue),
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.12f),
.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,
modifier = Modifier.weight(1f),
usePadding = false
)
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,
modifier = Modifier.weight(1f),
usePadding = false
)
}
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,
Box(
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,
.width(buttonWidth)
.fillMaxHeight()
.offset { IntOffset(animatedOffset.roundToInt(), 0) }
.zIndex(0f)
.draggable(
orientation = Orientation.Horizontal,
state = rememberDraggableState { delta ->
dragOffset = (dragOffset + delta).coerceIn(
0f,
with(density) { (buttonWidth * (buttonCount - 1)).toPx() }
)
},
onDragStarted = { isDragging.value = true },
onDragStopped = {
isDragging.value = false
val position = dragOffset / with(density) { buttonWidth.toPx() }
val newIndex = position.roundToInt()
val newMode = when(newIndex) {
0 -> if (offListeningMode.value) NoiseControlMode.OFF else NoiseControlMode.TRANSPARENCY
1 -> if (offListeningMode.value) NoiseControlMode.TRANSPARENCY else NoiseControlMode.ADAPTIVE
2 -> if (offListeningMode.value) NoiseControlMode.ADAPTIVE else NoiseControlMode.NOISE_CANCELLATION
3 -> NoiseControlMode.NOISE_CANCELLATION
else -> null
}
newMode?.let { onModeSelected(it) }
}
)
) {
Box(
modifier = Modifier
.fillMaxSize()
.padding(4.dp)
.background(selectedBackground, RoundedCornerShape(11.dp))
)
}
// Button row (top layer)
Row(
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)
)
.fillMaxWidth()
.zIndex(1f)
) {
if (offListeningMode.value) {
NoiseControlButton(
icon = ImageBitmap.imageResource(R.drawable.noise_cancellation),
onClick = { onModeSelected(NoiseControlMode.OFF) },
textColor = if (noiseControlMode.value == NoiseControlMode.OFF) textColorSelected else textColor,
modifier = Modifier.weight(1f),
usePadding = false
)
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,
modifier = Modifier.weight(1f),
usePadding = false
)
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,
modifier = Modifier.weight(1f),
usePadding = false
)
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,
modifier = Modifier.weight(1f),
usePadding = false
)
}
}
}
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 8.dp)
.padding(top = 1.dp)
) {
if (offListeningMode.value) {
// Labels row
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 8.dp)
.padding(top = 2.dp)
) {
if (offListeningMode.value) {
Text(
text = stringResource(R.string.off),
style = TextStyle(fontSize = 12.sp, color = textColor),
textAlign = TextAlign.Center,
fontWeight = FontWeight.Bold,
modifier = Modifier.weight(1f)
)
}
Text(
text = stringResource(R.string.off),
text = stringResource(R.string.transparency),
style = TextStyle(fontSize = 12.sp, color = textColor),
textAlign = TextAlign.Center,
fontWeight = FontWeight.Bold,
modifier = Modifier.weight(1f)
)
Text(
text = stringResource(R.string.adaptive),
style = TextStyle(fontSize = 12.sp, color = textColor),
textAlign = TextAlign.Center,
fontWeight = FontWeight.Bold,
modifier = Modifier.weight(1f)
)
Text(
text = stringResource(R.string.noise_cancellation),
style = TextStyle(fontSize = 12.sp, color = textColor),
textAlign = TextAlign.Center,
fontWeight = FontWeight.Bold,
modifier = Modifier.weight(1f)
)
}
Text(
text = stringResource(R.string.transparency),
style = TextStyle(fontSize = 12.sp, color = textColor),
textAlign = TextAlign.Center,
fontWeight = FontWeight.Bold,
modifier = Modifier.weight(1f)
)
Text(
text = stringResource(R.string.adaptive),
style = TextStyle(fontSize = 12.sp, color = textColor),
textAlign = TextAlign.Center,
fontWeight = FontWeight.Bold,
modifier = Modifier.weight(1f)
)
Text(
text = stringResource(R.string.noise_cancellation),
style = TextStyle(fontSize = 12.sp, color = textColor),
textAlign = TextAlign.Center,
fontWeight = FontWeight.Bold,
modifier = Modifier.weight(1f)
)
}
}
}
@Preview
@Composable
@Preview@Composable
fun NoiseControlSettingsPreview() {
NoiseControlSettings(AirPodsService())
}

View File

@@ -18,8 +18,10 @@
package me.kavishdevar.aln.composables
import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.tween
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
@@ -36,9 +38,14 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
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.res.stringResource
import androidx.compose.ui.text.TextStyle
@@ -55,6 +62,13 @@ import me.kavishdevar.aln.R
fun PressAndHoldSettings(navController: NavController) {
val isDarkTheme = isSystemInDarkTheme()
val textColor = if (isDarkTheme) Color.White else Color.Black
val dividerColor = Color(0x40888888)
var leftBackgroundColor by remember { mutableStateOf(if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFFFFFFF)) }
var rightBackgroundColor by remember { mutableStateOf(if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFFFFFFF)) }
val animationSpec = tween<Color>(durationMillis = 500)
val animatedLeftBackgroundColor by animateColorAsState(targetValue = leftBackgroundColor, animationSpec = animationSpec)
val animatedRightBackgroundColor by animateColorAsState(targetValue = rightBackgroundColor, animationSpec = animationSpec)
Text(
text = stringResource(R.string.press_and_hold_airpods).uppercase(),
@@ -67,27 +81,28 @@ fun PressAndHoldSettings(navController: NavController) {
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)
.background(if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFFFFFFF), RoundedCornerShape(14.dp))
) {
Box(
modifier = Modifier
.fillMaxWidth()
.height(55.dp)
.background(
backgroundColor,
RoundedCornerShape(14.dp)
)
.clickable(
onClick = {
navController.navigate("long_press/Left")
}
),
.background(animatedLeftBackgroundColor, RoundedCornerShape(topStart = 14.dp, topEnd = 14.dp))
.pointerInput(Unit) {
detectTapGestures(
onPress = {
leftBackgroundColor = dividerColor
tryAwaitRelease()
leftBackgroundColor = if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFFFFFFF)
},
onTap = {
navController.navigate("long_press/Left")
}
)
},
contentAlignment = Alignment.Center
) {
Row(
@@ -105,7 +120,6 @@ fun PressAndHoldSettings(navController: NavController) {
)
Spacer(modifier = Modifier.weight(1f))
Text(
// TODO: Implement voice assistant on long press; for now, it's noise control
text = stringResource(R.string.noise_control),
style = TextStyle(
fontSize = 18.sp,
@@ -128,7 +142,7 @@ fun PressAndHoldSettings(navController: NavController) {
}
HorizontalDivider(
thickness = 1.5.dp,
color = Color(0x40888888),
color = dividerColor,
modifier = Modifier
.padding(start = 16.dp)
)
@@ -136,15 +150,19 @@ fun PressAndHoldSettings(navController: NavController) {
modifier = Modifier
.fillMaxWidth()
.height(55.dp)
.background(
backgroundColor,
RoundedCornerShape(18.dp)
)
.clickable(
onClick = {
navController.navigate("long_press/Right")
}
),
.background(animatedRightBackgroundColor, RoundedCornerShape(bottomEnd = 14.dp, bottomStart = 14.dp))
.pointerInput(Unit) {
detectTapGestures(
onPress = {
rightBackgroundColor = dividerColor
tryAwaitRelease()
rightBackgroundColor = if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFFFFFFF)
},
onTap = {
navController.navigate("long_press/Right")
}
)
},
contentAlignment = Alignment.Center
) {
Row(
@@ -162,7 +180,6 @@ fun PressAndHoldSettings(navController: NavController) {
)
Spacer(modifier = Modifier.weight(1f))
Text(
// TODO: Implement voice assistant on long press; for now, it's noise control
text = stringResource(R.string.noise_control),
style = TextStyle(
fontSize = 18.sp,

View File

@@ -21,19 +21,15 @@
package me.kavishdevar.aln.screens
import android.annotation.SuppressLint
import android.content.BroadcastReceiver
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.content.ServiceConnection
import android.os.Build
import android.os.IBinder
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.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.Row
@@ -43,17 +39,23 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.KeyboardArrowLeft
import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight
import androidx.compose.material.icons.filled.Send
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
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.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
@@ -62,7 +64,7 @@ import androidx.compose.material3.TextFieldDefaults
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
@@ -83,18 +85,36 @@ import dev.chrisbanes.haze.haze
import dev.chrisbanes.haze.hazeChild
import dev.chrisbanes.haze.materials.CupertinoMaterials
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
import kotlinx.coroutines.flow.MutableStateFlow
import me.kavishdevar.aln.R
import me.kavishdevar.aln.services.AirPodsService
import me.kavishdevar.aln.utils.AirPodsNotifications
@OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class)
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter", "UnspecifiedRegisterReceiverFlag")
@Composable
fun DebugScreen(navController: NavController) {
val hazeState = remember { HazeState() }
val text = remember { mutableStateListOf<String>("Log Start") }
val context = LocalContext.current
val listState = rememberLazyListState()
val packetLogsFlow = remember { MutableStateFlow(emptySet<String>()) }
val expandedItems = remember { mutableStateOf(setOf<Int>()) }
LaunchedEffect(context) {
val serviceConnection = object : ServiceConnection {
override fun onServiceConnected(name: ComponentName, service: IBinder) {
val binder = service as AirPodsService.LocalBinder
val airPodsService = binder.getService()
packetLogsFlow.value = airPodsService.getPacketLogs()
}
override fun onServiceDisconnected(name: ComponentName) {}
}
val intent = Intent(context, AirPodsService::class.java)
context.bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE)
}
val packetLogs = packetLogsFlow.collectAsState(setOf()).value
Scaffold(
topBar = {
@@ -145,29 +165,6 @@ fun DebugScreen(navController: NavController) {
containerColor = if (isSystemInDarkTheme()) Color(0xFF000000)
else Color(0xFFF2F2F7),
) { paddingValues ->
val receiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
val data = intent.getByteArrayExtra("data")
data?.let {
text.add(">" + it.joinToString(" ") { byte -> "%02X".format(byte) })
}
}
}
LaunchedEffect(context) {
val intentFilter = IntentFilter(AirPodsNotifications.Companion.AIRPODS_DATA)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
context.registerReceiver(receiver, intentFilter, Context.RECEIVER_EXPORTED)
} else {
context.registerReceiver(receiver, intentFilter)
}
}
LaunchedEffect(text.size) {
if (text.isNotEmpty()) {
listState.animateScrollToItem(text.size - 1)
}
}
Column(
modifier = Modifier
.fillMaxSize()
@@ -181,44 +178,53 @@ fun DebugScreen(navController: NavController) {
.fillMaxWidth()
.weight(1f),
content = {
items(text.size) { index ->
val message = text[index]
val isSent = message.startsWith(">")
val backgroundColor =
if (isSent) Color(0xFFE1FFC7) else Color(0xFFD1D1D1)
items(packetLogs.size) { index ->
val message = packetLogs.elementAt(index)
val isSent = message.startsWith("Sent")
val isExpanded = expandedItems.value.contains(index)
if (message == "Log Start") {
Spacer(modifier = Modifier.height(115.dp))
}
Box(
Card(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp)
.background(backgroundColor, RoundedCornerShape(12.dp))
.padding(12.dp),
.padding(vertical = 2.dp, horizontal = 4.dp) // Reduced padding
.clickable {
expandedItems.value = if (isExpanded) {
expandedItems.value - index
} else {
expandedItems.value + index
}
},
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp), // Reduced elevation
shape = RoundedCornerShape(4.dp), // Reduced corner radius
colors = CardDefaults.cardColors(
containerColor = Color.Transparent
)
) {
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
if (!isSent) {
Text("<", color = Color(0xFF00796B), fontSize = 16.sp)
}
Text(
text = if (isSent) message.substring(1) else message,
fontFamily = FontFamily(Font(R.font.hack)),
color = if (isSystemInDarkTheme()) Color(
0xFF000000
Column(modifier = Modifier.padding(8.dp)) { // Reduced padding
Row(verticalAlignment = Alignment.CenterVertically) {
Icon(
imageVector = if (isSent) Icons.AutoMirrored.Filled.KeyboardArrowLeft else Icons.AutoMirrored.Filled.KeyboardArrowRight,
contentDescription = null,
tint = if (isSent) Color.Green else Color.Red,
modifier = Modifier.size(24.dp) // Reduced icon size
)
else Color(0xFF000000),
modifier = Modifier.weight(1f)
)
if (isSent) {
Text(">", color = Color(0xFF00796B), fontSize = 16.sp)
Spacer(modifier = Modifier.width(4.dp)) // Reduced spacing
Column {
Text(
text =
if (isSent) message.substring(5).take(60) + (if (message.substring(5).length > 60) "..." else "")
else message.substring(9).take(60) + (if (message.substring(9).length > 60) "..." else ""),
style = MaterialTheme.typography.bodySmall,
)
if (isExpanded) {
Spacer(modifier = Modifier.height(4.dp)) // Reduced spacing
Text(
text = message.substring(if (isSent) 5 else 9),
style = MaterialTheme.typography.bodySmall,
color = Color.Gray
)
}
}
}
}
}
@@ -262,7 +268,6 @@ fun DebugScreen(navController: NavController) {
IconButton(
onClick = {
airPodsService.value?.sendPacket(packet.value.text)
text.add(packet.value.text)
packet.value = TextFieldValue("")
}
) {
@@ -282,23 +287,6 @@ fun DebugScreen(navController: NavController) {
),
shape = RoundedCornerShape(12.dp)
)
val airPodsService = remember { mutableStateOf<AirPodsService?>(null) }
val serviceConnection = object : ServiceConnection {
override fun onServiceConnected(name: ComponentName, service: IBinder) {
val binder = service as AirPodsService.LocalBinder
airPodsService.value = binder.getService()
Log.d("AirPodsService", "Service connected")
}
override fun onServiceDisconnected(name: ComponentName) {
airPodsService.value = null
}
}
val intent = Intent(context, AirPodsService::class.java)
context.bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE)
}
}
}

View File

@@ -41,16 +41,18 @@ import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.scale
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.Font
@@ -69,6 +71,14 @@ fun RenameScreen(navController: NavController) {
val sharedPreferences = LocalContext.current.getSharedPreferences("settings", Context.MODE_PRIVATE)
val isDarkTheme = isSystemInDarkTheme()
val name = remember { mutableStateOf(sharedPreferences.getString("name", "") ?: "") }
val focusRequester = remember { FocusRequester() }
val keyboardController = LocalSoftwareKeyboardController.current
LaunchedEffect(Unit) {
focusRequester.requestFocus()
keyboardController?.show()
}
Scaffold(
topBar = {
CenterAlignedTopAppBar(
@@ -117,17 +127,10 @@ fun RenameScreen(navController: NavController) {
.padding(horizontal = 16.dp)
.padding(top = 8.dp)
) {
var isFocused by remember { mutableStateOf(false) }
val isDarkTheme = isSystemInDarkTheme()
val backgroundColor = if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFFFFFFF)
val textColor = if (isDarkTheme) Color.White else Color.Black
val cursorColor = if (isFocused) { // Show cursor only when focused
if (isDarkTheme) Color.White else Color.Black
} else {
Color.Transparent
}
val cursorColor = if (isDarkTheme) Color.White else Color.Black
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
@@ -180,6 +183,7 @@ fun RenameScreen(navController: NavController) {
modifier = Modifier
.fillMaxWidth()
.padding(start = 8.dp)
.focusRequester(focusRequester)
)
}
}

View File

@@ -39,6 +39,7 @@ import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.content.SharedPreferences
import android.media.AudioManager
import android.os.Binder
import android.os.Build
@@ -95,6 +96,7 @@ object ServiceManager {
delay(1000)
context.startService(intent)
context.startActivity(Intent(context, MainActivity::class.java))
service?.clearLogs()
}
}
}
@@ -106,6 +108,34 @@ class AirPodsService: Service() {
fun getService(): AirPodsService = this@AirPodsService
}
private lateinit var sharedPreferences: SharedPreferences
private val packetLogKey = "packet_log"
override fun onCreate() {
super.onCreate()
sharedPreferences = getSharedPreferences("packet_logs", Context.MODE_PRIVATE)
}
private fun logPacket(packet: ByteArray, source: String) {
val packetHex = packet.joinToString(" ") { "%02X".format(it) }
val logEntry = "$source: $packetHex"
val logs = sharedPreferences.getStringSet(packetLogKey, mutableSetOf())?.toMutableSet() ?: mutableSetOf()
logs.add(logEntry)
sharedPreferences.edit().putStringSet(packetLogKey, logs).apply()
}
fun getPacketLogs(): Set<String> {
return sharedPreferences.getStringSet(packetLogKey, emptySet()) ?: emptySet()
}
private fun clearPacketLogs() {
sharedPreferences.edit().remove(packetLogKey).apply()
}
fun clearLogs() {
clearPacketLogs() // Expose a method to clear logs
}
override fun onBind(intent: Intent?): IBinder {
return LocalBinder()
}
@@ -133,7 +163,9 @@ class AirPodsService: Service() {
}
}
private fun forwardPacket(packet: String, outputStream: OutputStream) {
outputStream.write(packet.toByteArray())
val byteArray = packet.toByteArray()
outputStream.write(byteArray)
logPacket(byteArray, "Sent")
}
private fun connectToAirPods() {
@@ -400,7 +432,7 @@ class AirPodsService: Service() {
.putString("name", name).apply()
}
Log.d("AirPodsQuickSwitchServices", CrossDevice.isAvailable.toString())
if (!CrossDevice.isAvailable) {
if (!CrossDevice.checkAirPodsConnectionStatus()) {
Log.d("AirPodsService", "$name connected")
showPopup(this@AirPodsService, name.toString())
connectToSocket(device!!)
@@ -463,7 +495,7 @@ class AirPodsService: Service() {
if (profile == BluetoothProfile.A2DP) {
val connectedDevices = proxy.connectedDevices
if (connectedDevices.isNotEmpty()) {
if (!CrossDevice.isAvailable) {
if (!CrossDevice.checkAirPodsConnectionStatus()) {
connectToSocket(device)
}
this@AirPodsService.sendBroadcast(
@@ -482,6 +514,10 @@ class AirPodsService: Service() {
}
}
if (!isConnectedLocally && !CrossDevice.isAvailable) {
clearPacketLogs() // Clear logs when device is not available
}
return START_STICKY
}
@@ -581,6 +617,7 @@ class AirPodsService: Service() {
var data: ByteArray = byteArrayOf()
if (bytesRead > 0) {
data = buffer.copyOfRange(0, bytesRead)
logPacket(data, "AirPods")
sendBroadcast(Intent(AirPodsNotifications.Companion.AIRPODS_DATA).apply {
putExtra("data", buffer.copyOfRange(0, bytesRead))
})
@@ -824,8 +861,10 @@ class AirPodsService: Service() {
return
}
if (this::socket.isInitialized) {
socket.outputStream?.write(fromHex.toByteArray())
val byteArray = fromHex.toByteArray()
socket.outputStream?.write(byteArray)
socket.outputStream?.flush()
logPacket(byteArray, "Sent")
}
}
@@ -837,6 +876,7 @@ class AirPodsService: Service() {
if (this::socket.isInitialized) {
socket.outputStream?.write(packet)
socket.outputStream?.flush()
logPacket(packet, "Sent")
}
}
@@ -1015,7 +1055,6 @@ class AirPodsService: Service() {
val bytes = byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x1a, 0x00, 0x01,
nameBytes.size.toByte(), 0x00) + nameBytes
sendPacket(bytes)
socket.outputStream?.flush()
val hex = bytes.joinToString(" ") { "%02X".format(it) }
updateNotificationContent(true, name, batteryNotification.getBattery())
Log.d("AirPodsService", "setName: $name, sent packet: $hex")
@@ -1025,18 +1064,15 @@ class AirPodsService: Service() {
var hex = "04 00 04 00 09 00 26 ${if (enabled) "01" else "02"} 00 00 00"
var bytes = hex.split(" ").map { it.toInt(16).toByte() }.toByteArray()
sendPacket(bytes)
socket.outputStream?.flush()
hex = "04 00 04 00 17 00 00 00 10 00 12 00 08 E${if (enabled) "6" else "5"} 05 10 02 42 0B 08 50 10 02 1A 05 02 ${if (enabled) "32" else "00"} 00 00 00"
bytes = hex.split(" ").map { it.toInt(16).toByte() }.toByteArray()
sendPacket(bytes)
socket.outputStream?.flush()
}
fun setLoudSoundReduction(enabled: Boolean) {
val hex = "52 1B 00 0${if (enabled) "1" else "0"}"
val bytes = hex.split(" ").map { it.toInt(16).toByte() }.toByteArray()
sendPacket(bytes)
socket.outputStream?.flush()
}
fun findChangedIndex(oldArray: BooleanArray, newArray: BooleanArray): Int {
for (i in oldArray.indices) {
@@ -1175,11 +1211,12 @@ class AirPodsService: Service() {
}
packet?.let {
Log.d("AirPodsService", "Sending packet: ${it.joinToString(" ") { "%02X".format(it) }}")
socket.outputStream.write(it)
sendPacket(it)
}
}
override fun onDestroy() {
clearPacketLogs()
Log.d("AirPodsService", "Service stopped is being destroyed for some reason!")
try {
unregisterReceiver(bluetoothReceiver)

View File

@@ -5,8 +5,13 @@ import android.bluetooth.BluetoothAdapter
import android.bluetooth.BluetoothManager
import android.bluetooth.BluetoothServerSocket
import android.bluetooth.BluetoothSocket
import android.bluetooth.le.AdvertiseCallback
import android.bluetooth.le.AdvertiseData
import android.bluetooth.le.AdvertiseSettings
import android.bluetooth.le.BluetoothLeAdvertiser
import android.content.Context
import android.content.SharedPreferences
import android.os.ParcelUuid
import android.util.Log
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@@ -21,26 +26,36 @@ enum class CrossDevicePackets(val packet: ByteArray) {
REQUEST_DISCONNECT(byteArrayOf(0x00, 0x02, 0x00, 0x00)),
REQUEST_BATTERY_BYTES(byteArrayOf(0x00, 0x02, 0x00, 0x01)),
REQUEST_ANC_BYTES(byteArrayOf(0x00, 0x02, 0x00, 0x02)),
REQUEST_CONNECTION_STATUS(byteArrayOf(0x00, 0x02, 0x00, 0x03)),
AIRPODS_DATA_HEADER(byteArrayOf(0x00, 0x04, 0x00, 0x01)),
}
object CrossDevice {
var initialized = false
private val uuid = UUID.fromString("1abbb9a4-10e4-4000-a75c-8953c5471342")
private var serverSocket: BluetoothServerSocket? = null
private var clientSocket: BluetoothSocket? = null
private lateinit var bluetoothAdapter: BluetoothAdapter
private lateinit var bluetoothLeAdvertiser: BluetoothLeAdvertiser
private const val MANUFACTURER_ID = 0x1234
private const val MANUFACTURER_DATA = "ALN_AirPods"
var isAvailable: Boolean = false // set to true when airpods are connected to another device
var batteryBytes: ByteArray = byteArrayOf()
var ancBytes: ByteArray = byteArrayOf()
private lateinit var sharedPreferences: SharedPreferences
private const val packetLogKey = "packet_log"
@SuppressLint("MissingPermission")
fun init(context: Context) {
Log.d("AirPodsQuickSwitchService", "Initializing CrossDevice")
sharedPreferences = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
sharedPreferences = context.getSharedPreferences("packet_logs", Context.MODE_PRIVATE)
sharedPreferences.edit().putBoolean("CrossDeviceIsAvailable", false).apply()
this.bluetoothAdapter = context.getSystemService(BluetoothManager::class.java).adapter
this.bluetoothLeAdvertiser = bluetoothAdapter.bluetoothLeAdvertiser
startAdvertising()
startServer()
initialized = true
}
@SuppressLint("MissingPermission")
@@ -59,6 +74,34 @@ object CrossDevice {
}
}
@SuppressLint("MissingPermission")
private fun startAdvertising() {
val settings = AdvertiseSettings.Builder()
.setAdvertiseMode(AdvertiseSettings.ADVERTISE_MODE_LOW_LATENCY)
.setTxPowerLevel(AdvertiseSettings.ADVERTISE_TX_POWER_HIGH)
.setConnectable(true)
.build()
val data = AdvertiseData.Builder()
.setIncludeDeviceName(true)
.addManufacturerData(MANUFACTURER_ID, MANUFACTURER_DATA.toByteArray())
.addServiceUuid(ParcelUuid(uuid))
.build()
bluetoothLeAdvertiser.startAdvertising(settings, data, advertiseCallback)
Log.d("AirPodsQuickSwitchService", "BLE Advertising started")
}
private val advertiseCallback = object : AdvertiseCallback() {
override fun onStartSuccess(settingsInEffect: AdvertiseSettings) {
Log.d("AirPodsQuickSwitchService", "BLE Advertising started successfully")
}
override fun onStartFailure(errorCode: Int) {
Log.e("AirPodsQuickSwitchService", "BLE Advertising failed with error code: $errorCode")
}
}
fun setAirPodsConnected(connected: Boolean) {
if (connected) {
isAvailable = false
@@ -71,9 +114,21 @@ object CrossDevice {
fun sendReceivedPacket(packet: ByteArray) {
Log.d("AirPodsQuickSwitchService", "Sending packet to remote device")
if (clientSocket == null) {
Log.d("AirPodsQuickSwitchService", "Client socket is null")
return
}
clientSocket?.outputStream?.write(CrossDevicePackets.AIRPODS_DATA_HEADER.packet + packet)
}
private fun logPacket(packet: ByteArray, source: String) {
val packetHex = packet.joinToString(" ") { "%02X".format(it) }
val logEntry = "$source: $packetHex"
val logs = sharedPreferences.getStringSet(packetLogKey, mutableSetOf())?.toMutableSet() ?: mutableSetOf()
logs.add(logEntry)
sharedPreferences.edit().putStringSet(packetLogKey, logs).apply()
}
@SuppressLint("MissingPermission")
private fun handleClientConnection(socket: BluetoothSocket) {
Log.d("AirPodsQuickSwitchService", "Client connected")
@@ -85,6 +140,7 @@ object CrossDevice {
while (true) {
bytes = inputStream.read(buffer)
val packet = buffer.copyOf(bytes)
logPacket(packet, "Relay")
Log.d("AirPodsQuickSwitchService", "Received packet: ${packet.joinToString("") { "%02x".format(it) }}")
if (bytes == -1) {
break
@@ -102,19 +158,17 @@ object CrossDevice {
} else if (packet.contentEquals(CrossDevicePackets.REQUEST_ANC_BYTES.packet)) {
Log.d("AirPodsQuickSwitchService", "Received ANC request")
sendRemotePacket(ancBytes)
} else if (packet.contentEquals(CrossDevicePackets.REQUEST_CONNECTION_STATUS.packet)) {
Log.d("AirPodsQuickSwitchService", "Received connection status request")
sendRemotePacket(if (ServiceManager.getService()?.isConnectedLocally == true) CrossDevicePackets.AIRPODS_CONNECTED.packet else CrossDevicePackets.AIRPODS_DISCONNECTED.packet)
}
else {
if (packet.sliceArray(0..3).contentEquals(CrossDevicePackets.AIRPODS_DATA_HEADER.packet)) {
// the AIRPODS_CONNECTED wasn't sent before
isAvailable = true
sharedPreferences.edit().putBoolean("CrossDeviceIsAvailable", true).apply()
val trimmedPacket =
packet.drop(CrossDevicePackets.AIRPODS_DATA_HEADER.packet.size).toByteArray()
val trimmedPacket = packet.drop(CrossDevicePackets.AIRPODS_DATA_HEADER.packet.size).toByteArray()
Log.d("AirPodsQuickSwitchService", "Received relayed packet, with ${sharedPreferences.getBoolean("CrossDeviceIsAvailable", false)} | ${ServiceManager.getService()?.batteryNotification?.isBatteryData(trimmedPacket)}")
Log.d(
"AirPodsQuickSwitchService",
"Relayed packet: ${trimmedPacket.joinToString("") { "%02x".format(it) }}"
)
Log.d("AirPodsQuickSwitchService", "Received relayed packet: ${trimmedPacket.joinToString("") { "%02x".format(it) }}")
if (ServiceManager.getService()?.isConnectedLocally == true) {
val packetInHex = trimmedPacket.joinToString("") { "%02x".format(it) }
ServiceManager.getService()?.sendPacket(packetInHex)
@@ -142,6 +196,25 @@ object CrossDevice {
}
clientSocket?.outputStream?.write(byteArray)
clientSocket?.outputStream?.flush()
logPacket(byteArray, "Sent")
Log.d("AirPodsQuickSwitchService", "Sent packet to remote device")
}
fun checkAirPodsConnectionStatus(): Boolean {
Log.d("AirPodsQuickSwitchService", "Checking AirPods connection status")
if (clientSocket == null) {
Log.d("AirPodsQuickSwitchService", "Client socket is null - linux probably not connected.")
return false
}
return try {
clientSocket?.outputStream?.write(CrossDevicePackets.REQUEST_CONNECTION_STATUS.packet)
val buffer = ByteArray(1024)
val bytes = clientSocket?.inputStream?.read(buffer) ?: -1
val packet = buffer.copyOf(bytes)
packet.contentEquals(CrossDevicePackets.AIRPODS_CONNECTED.packet)
} catch (e: IOException) {
Log.e("AirPodsQuickSwitchService", "Error checking connection status", e)
false
}
}
}

View File

@@ -157,8 +157,13 @@ class AirPodsNotifications {
private var case: Battery = Battery(BatteryComponent.CASE, 0, BatteryStatus.DISCONNECTED)
fun isBatteryData(data: ByteArray): Boolean {
if (data.joinToString("") { "%02x".format(it) }.startsWith("040004000400")) {
Log.d("BatteryNotification", "Battery data starts with 040004000400. Most likely is a battery packet.")
} else {
return false
}
if (data.size != 22) {
Log.d("BatteryNotification", "Battery data size is not 22")
Log.d("BatteryNotification", "Battery data size is not 22, probably being used with Airpods with fewer or more battery count.")
return false
}
Log.d("BatteryNotification", data.joinToString("") { "%02x".format(it) }.startsWith("040004000400").toString())

View File

@@ -1,6 +1,6 @@
[versions]
accompanistPermissions = "0.36.0"
agp = "8.7.3"
agp = "8.8.0"
hiddenapibypass = "4.3"
kotlin = "2.0.0"
coreKtx = "1.15.0"