mirror of
https://github.com/kavishdevar/librepods.git
synced 2026-04-28 09:07:14 +00:00
move files across computers
This commit is contained in:
@@ -25,11 +25,11 @@ To develop for the Android App, Android Studio is the preferred IDE. And you can
|
|||||||
|
|
||||||
#### Create a new issue
|
#### Create a new issue
|
||||||
|
|
||||||
If you find a bug or want to suggest a feature, check if an issue already exists by searching through our [existing issues](https://github.com/username/AirPods-Like-Normal/issues). If no relevant issue exists, open a new one and fill in the details.
|
If you find a bug or want to suggest a feature, check if an issue already exists by searching through our [existing issues](https://github.com/kavishdevar/aln/issues). If no relevant issue exists, open a new one and fill in the details.
|
||||||
|
|
||||||
#### Solve an issue
|
#### Solve an issue
|
||||||
|
|
||||||
Browse our [issues list](https://github.com/username/AirPods-Like-Normal/issues) to find an interesting issue to work on. Use labels to filter issues and pick one that matches your expertise. If you’d like to work on an issue, open a PR with your solution.
|
Browse our [issues list](https://github.com/kavishdevar/aln/issues) to find an interesting issue to work on. Use labels to filter issues and pick one that matches your expertise. If you’d like to work on an issue, open a PR with your solution.
|
||||||
|
|
||||||
### Make Changes
|
### Make Changes
|
||||||
|
|
||||||
@@ -37,7 +37,7 @@ Browse our [issues list](https://github.com/username/AirPods-Like-Normal/issues)
|
|||||||
|
|
||||||
1. Fork the repository and clone it to your local environment.
|
1. Fork the repository and clone it to your local environment.
|
||||||
```
|
```
|
||||||
git clone https://github.com/your-username/AirPods-Like-Normal.git
|
git clone https://github.com/kavishdevar/aln.git
|
||||||
cd AirPods-Like-Normal
|
cd AirPods-Like-Normal
|
||||||
```
|
```
|
||||||
2. Create a working branch to start your changes.
|
2. Create a working branch to start your changes.
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
<uses-permission android:name="android.permission.BLUETOOTH_SCAN"
|
<uses-permission android:name="android.permission.BLUETOOTH_SCAN"
|
||||||
android:usesPermissionFlags="neverForLocation"
|
android:usesPermissionFlags="neverForLocation"
|
||||||
tools:ignore="UnusedAttribute" />
|
tools:ignore="UnusedAttribute" />
|
||||||
|
<uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE" />
|
||||||
<application
|
<application
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||||
|
|||||||
@@ -146,6 +146,15 @@ fun Main() {
|
|||||||
isRemotelyConnected.value = CrossDevice.isAvailable
|
isRemotelyConnected.value = CrossDevice.isAvailable
|
||||||
isConnected.value = false
|
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)
|
sharedPreferences.registerOnSharedPreferenceChangeListener(isAvailableChangeListener)
|
||||||
Log.d("MainActivity", "CrossDeviceIsAvailable: ${sharedPreferences.getBoolean("CrossDeviceIsAvailable", false)} | isAvailable: ${CrossDevice.isAvailable}")
|
Log.d("MainActivity", "CrossDeviceIsAvailable: ${sharedPreferences.getBoolean("CrossDeviceIsAvailable", false)} | isAvailable: ${CrossDevice.isAvailable}")
|
||||||
isRemotelyConnected.value = sharedPreferences.getBoolean("CrossDeviceIsAvailable", false) || CrossDevice.isAvailable
|
isRemotelyConnected.value = sharedPreferences.getBoolean("CrossDeviceIsAvailable", false) || CrossDevice.isAvailable
|
||||||
|
Log.d("MainActivity", "isRemotelyConnected: ${isRemotelyConnected.value}")
|
||||||
val filter = IntentFilter().apply {
|
val filter = IntentFilter().apply {
|
||||||
addAction(AirPodsNotifications.AIRPODS_CONNECTED)
|
addAction(AirPodsNotifications.AIRPODS_CONNECTED)
|
||||||
addAction(AirPodsNotifications.AIRPODS_DISCONNECTED)
|
addAction(AirPodsNotifications.AIRPODS_DISCONNECTED)
|
||||||
addAction(AirPodsNotifications.AIRPODS_CONNECTION_DETECTED)
|
addAction(AirPodsNotifications.AIRPODS_CONNECTION_DETECTED)
|
||||||
|
addAction(AirPodsNotifications.DISCONNECT_RECEIVERS)
|
||||||
}
|
}
|
||||||
Log.d("MainActivity", "Registering Receiver")
|
Log.d("MainActivity", "Registering Receiver")
|
||||||
|
|
||||||
|
|||||||
@@ -148,7 +148,7 @@ fun DropdownMenuComponent(
|
|||||||
Column (
|
Column (
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(horizontal = 8.dp)
|
.padding(horizontal = 12.dp)
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = label,
|
text = label,
|
||||||
|
|||||||
@@ -82,11 +82,11 @@ fun AdaptiveStrengthSlider(service: AirPodsService, sharedPreferences: SharedPre
|
|||||||
value = sliderValue.floatValue,
|
value = sliderValue.floatValue,
|
||||||
onValueChange = {
|
onValueChange = {
|
||||||
sliderValue.floatValue = it
|
sliderValue.floatValue = it
|
||||||
service.setAdaptiveStrength(100 - it.toInt())
|
|
||||||
},
|
},
|
||||||
valueRange = 0f..100f,
|
valueRange = 0f..100f,
|
||||||
onValueChangeFinished = {
|
onValueChangeFinished = {
|
||||||
sliderValue.floatValue = sliderValue.floatValue.roundToInt().toFloat()
|
sliderValue.floatValue = sliderValue.floatValue.roundToInt().toFloat()
|
||||||
|
service.setAdaptiveStrength(100 - sliderValue.floatValue.toInt())
|
||||||
},
|
},
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
|
|||||||
@@ -18,7 +18,6 @@
|
|||||||
|
|
||||||
package me.kavishdevar.aln.composables
|
package me.kavishdevar.aln.composables
|
||||||
|
|
||||||
import androidx.compose.foundation.background
|
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
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.fillMaxHeight
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
@@ -44,18 +42,18 @@ fun NoiseControlButton(
|
|||||||
icon: ImageBitmap,
|
icon: ImageBitmap,
|
||||||
onClick: () -> Unit,
|
onClick: () -> Unit,
|
||||||
textColor: Color,
|
textColor: Color,
|
||||||
backgroundColor: Color,
|
modifier: Modifier = Modifier,
|
||||||
modifier: Modifier = Modifier
|
usePadding: Boolean = true
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.fillMaxHeight()
|
.fillMaxHeight()
|
||||||
.padding(horizontal = 4.dp, vertical = 4.dp)
|
.then(if (usePadding) Modifier.padding(horizontal = 4.dp, vertical = 4.dp) else Modifier)
|
||||||
.background(color = backgroundColor, shape = RoundedCornerShape(11.dp))
|
|
||||||
.clickable(
|
.clickable(
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
indication = null,
|
indication = null,
|
||||||
interactionSource = remember { MutableInteractionSource() }),
|
interactionSource = remember { MutableInteractionSource() }
|
||||||
|
),
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
verticalArrangement = Arrangement.Center
|
verticalArrangement = Arrangement.Center
|
||||||
) {
|
) {
|
||||||
@@ -75,6 +73,5 @@ fun NoiseControlButtonPreview() {
|
|||||||
icon = ImageBitmap.imageResource(R.drawable.noise_cancellation),
|
icon = ImageBitmap.imageResource(R.drawable.noise_cancellation),
|
||||||
onClick = {},
|
onClick = {},
|
||||||
textColor = Color.White,
|
textColor = Color.White,
|
||||||
backgroundColor = Color.Black
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -25,42 +25,60 @@ import android.content.Intent
|
|||||||
import android.content.IntentFilter
|
import android.content.IntentFilter
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
import android.os.Build
|
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.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.isSystemInDarkTheme
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
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.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.offset
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.VerticalDivider
|
import androidx.compose.material3.VerticalDivider
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.DisposableEffect
|
import androidx.compose.runtime.DisposableEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableFloatStateOf
|
import androidx.compose.runtime.mutableFloatStateOf
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.alpha
|
import androidx.compose.ui.draw.alpha
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.ImageBitmap
|
import androidx.compose.ui.graphics.ImageBitmap
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
import androidx.compose.ui.res.imageResource
|
import androidx.compose.ui.res.imageResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.TextStyle
|
import androidx.compose.ui.text.TextStyle
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.IntOffset
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
|
import androidx.compose.ui.zIndex
|
||||||
import me.kavishdevar.aln.R
|
import me.kavishdevar.aln.R
|
||||||
import me.kavishdevar.aln.services.AirPodsService
|
import me.kavishdevar.aln.services.AirPodsService
|
||||||
import me.kavishdevar.aln.utils.AirPodsNotifications
|
import me.kavishdevar.aln.utils.AirPodsNotifications
|
||||||
import me.kavishdevar.aln.utils.NoiseControlMode
|
import me.kavishdevar.aln.utils.NoiseControlMode
|
||||||
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
@SuppressLint("UnspecifiedRegisterReceiverFlag")
|
@SuppressLint("UnspecifiedRegisterReceiverFlag", "UnusedBoxWithConstraintsScope")
|
||||||
@Composable
|
@Composable
|
||||||
fun NoiseControlSettings(service: AirPodsService) {
|
fun NoiseControlSettings(service: AirPodsService) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
@@ -86,7 +104,7 @@ fun NoiseControlSettings(service: AirPodsService) {
|
|||||||
val backgroundColor = if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFE3E3E8)
|
val backgroundColor = if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFE3E3E8)
|
||||||
val textColor = if (isDarkTheme) Color.White else Color.Black
|
val textColor = if (isDarkTheme) Color.White else Color.Black
|
||||||
val textColorSelected = 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) }
|
val noiseControlMode = remember { mutableStateOf(NoiseControlMode.OFF) }
|
||||||
|
|
||||||
@@ -161,118 +179,258 @@ fun NoiseControlSettings(service: AirPodsService) {
|
|||||||
),
|
),
|
||||||
modifier = Modifier.padding(8.dp, bottom = 2.dp)
|
modifier = Modifier.padding(8.dp, bottom = 2.dp)
|
||||||
)
|
)
|
||||||
|
BoxWithConstraints(
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(vertical = 16.dp)
|
.padding(vertical = 8.dp) // Adjusted padding
|
||||||
) {
|
) {
|
||||||
Box(
|
val density = LocalDensity.current
|
||||||
modifier = Modifier
|
val buttonCount = if (offListeningMode.value) 4 else 3
|
||||||
.fillMaxWidth()
|
val buttonWidth = maxWidth / buttonCount
|
||||||
.height(75.dp)
|
|
||||||
.padding(8.dp)
|
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
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
|
.height(60.dp) // Adjusted height
|
||||||
.background(backgroundColor, RoundedCornerShape(14.dp))
|
.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(
|
NoiseControlButton(
|
||||||
icon = ImageBitmap.imageResource(R.drawable.noise_cancellation),
|
icon = ImageBitmap.imageResource(R.drawable.transparency),
|
||||||
onClick = { onModeSelected(NoiseControlMode.OFF) },
|
onClick = { onModeSelected(NoiseControlMode.TRANSPARENCY) },
|
||||||
textColor = if (noiseControlMode.value == NoiseControlMode.OFF) textColorSelected else textColor,
|
textColor = if (noiseControlMode.value == NoiseControlMode.TRANSPARENCY) textColorSelected else textColor,
|
||||||
backgroundColor = if (noiseControlMode.value == NoiseControlMode.OFF) selectedBackground else Color.Transparent,
|
modifier = Modifier.weight(1f),
|
||||||
modifier = Modifier.weight(1f)
|
usePadding = false
|
||||||
)
|
)
|
||||||
VerticalDivider(
|
VerticalDivider(
|
||||||
thickness = 1.dp,
|
thickness = 1.dp,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(vertical = 10.dp)
|
.padding(vertical = 10.dp)
|
||||||
.alpha(d1a.floatValue),
|
.alpha(d2a.floatValue),
|
||||||
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.12f),
|
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),
|
Box(
|
||||||
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
|
modifier = Modifier
|
||||||
.padding(vertical = 10.dp)
|
.width(buttonWidth)
|
||||||
.alpha(d2a.floatValue),
|
.fillMaxHeight()
|
||||||
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.12f),
|
.offset { IntOffset(animatedOffset.roundToInt(), 0) }
|
||||||
)
|
.zIndex(0f)
|
||||||
NoiseControlButton(
|
.draggable(
|
||||||
icon = ImageBitmap.imageResource(R.drawable.adaptive),
|
orientation = Orientation.Horizontal,
|
||||||
onClick = { onModeSelected(NoiseControlMode.ADAPTIVE) },
|
state = rememberDraggableState { delta ->
|
||||||
textColor = if (noiseControlMode.value == NoiseControlMode.ADAPTIVE) textColorSelected else textColor,
|
dragOffset = (dragOffset + delta).coerceIn(
|
||||||
backgroundColor = if (noiseControlMode.value == NoiseControlMode.ADAPTIVE) selectedBackground else Color.Transparent,
|
0f,
|
||||||
modifier = Modifier.weight(1f)
|
with(density) { (buttonWidth * (buttonCount - 1)).toPx() }
|
||||||
)
|
)
|
||||||
VerticalDivider(
|
},
|
||||||
thickness = 1.dp,
|
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
|
modifier = Modifier
|
||||||
.padding(vertical = 10.dp)
|
.fillMaxWidth()
|
||||||
.alpha(d3a.floatValue),
|
.zIndex(1f)
|
||||||
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.12f),
|
) {
|
||||||
)
|
if (offListeningMode.value) {
|
||||||
NoiseControlButton(
|
NoiseControlButton(
|
||||||
icon = ImageBitmap.imageResource(R.drawable.noise_cancellation),
|
icon = ImageBitmap.imageResource(R.drawable.noise_cancellation),
|
||||||
onClick = { onModeSelected(NoiseControlMode.NOISE_CANCELLATION) },
|
onClick = { onModeSelected(NoiseControlMode.OFF) },
|
||||||
textColor = if (noiseControlMode.value == NoiseControlMode.NOISE_CANCELLATION) textColorSelected else textColor,
|
textColor = if (noiseControlMode.value == NoiseControlMode.OFF) textColorSelected else textColor,
|
||||||
backgroundColor = if (noiseControlMode.value == NoiseControlMode.NOISE_CANCELLATION) selectedBackground else Color.Transparent,
|
modifier = Modifier.weight(1f),
|
||||||
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(
|
// Labels row
|
||||||
modifier = Modifier
|
Row(
|
||||||
.fillMaxWidth()
|
modifier = Modifier
|
||||||
.padding(horizontal = 8.dp)
|
.fillMaxWidth()
|
||||||
.padding(top = 1.dp)
|
.padding(horizontal = 8.dp)
|
||||||
) {
|
.padding(top = 2.dp)
|
||||||
if (offListeningMode.value) {
|
) {
|
||||||
|
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(
|
||||||
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),
|
style = TextStyle(fontSize = 12.sp, color = textColor),
|
||||||
textAlign = TextAlign.Center,
|
textAlign = TextAlign.Center,
|
||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
modifier = Modifier.weight(1f)
|
modifier = Modifier.weight(1f)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Text(
|
|
||||||
text = 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
|
@Preview@Composable
|
||||||
@Composable
|
|
||||||
fun NoiseControlSettingsPreview() {
|
fun NoiseControlSettingsPreview() {
|
||||||
NoiseControlSettings(AirPodsService())
|
NoiseControlSettings(AirPodsService())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,8 +18,10 @@
|
|||||||
|
|
||||||
package me.kavishdevar.aln.composables
|
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.background
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.gestures.detectTapGestures
|
||||||
import androidx.compose.foundation.isSystemInDarkTheme
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
@@ -36,9 +38,14 @@ import androidx.compose.material3.Icon
|
|||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.input.pointer.pointerInput
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.TextStyle
|
import androidx.compose.ui.text.TextStyle
|
||||||
@@ -55,6 +62,13 @@ import me.kavishdevar.aln.R
|
|||||||
fun PressAndHoldSettings(navController: NavController) {
|
fun PressAndHoldSettings(navController: NavController) {
|
||||||
val isDarkTheme = isSystemInDarkTheme()
|
val isDarkTheme = isSystemInDarkTheme()
|
||||||
val textColor = if (isDarkTheme) Color.White else Color.Black
|
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(
|
||||||
text = stringResource(R.string.press_and_hold_airpods).uppercase(),
|
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)
|
modifier = Modifier.padding(8.dp, bottom = 2.dp)
|
||||||
)
|
)
|
||||||
|
|
||||||
val backgroundColor = if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFFFFFFF)
|
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.background(backgroundColor, RoundedCornerShape(14.dp))
|
.background(if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFFFFFFF), RoundedCornerShape(14.dp))
|
||||||
.padding(top = 2.dp)
|
|
||||||
) {
|
) {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.height(55.dp)
|
.height(55.dp)
|
||||||
.background(
|
.background(animatedLeftBackgroundColor, RoundedCornerShape(topStart = 14.dp, topEnd = 14.dp))
|
||||||
backgroundColor,
|
.pointerInput(Unit) {
|
||||||
RoundedCornerShape(14.dp)
|
detectTapGestures(
|
||||||
)
|
onPress = {
|
||||||
.clickable(
|
leftBackgroundColor = dividerColor
|
||||||
onClick = {
|
tryAwaitRelease()
|
||||||
navController.navigate("long_press/Left")
|
leftBackgroundColor = if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFFFFFFF)
|
||||||
}
|
},
|
||||||
),
|
onTap = {
|
||||||
|
navController.navigate("long_press/Left")
|
||||||
|
}
|
||||||
|
)
|
||||||
|
},
|
||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
@@ -105,7 +120,6 @@ fun PressAndHoldSettings(navController: NavController) {
|
|||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.weight(1f))
|
Spacer(modifier = Modifier.weight(1f))
|
||||||
Text(
|
Text(
|
||||||
// TODO: Implement voice assistant on long press; for now, it's noise control
|
|
||||||
text = stringResource(R.string.noise_control),
|
text = stringResource(R.string.noise_control),
|
||||||
style = TextStyle(
|
style = TextStyle(
|
||||||
fontSize = 18.sp,
|
fontSize = 18.sp,
|
||||||
@@ -128,7 +142,7 @@ fun PressAndHoldSettings(navController: NavController) {
|
|||||||
}
|
}
|
||||||
HorizontalDivider(
|
HorizontalDivider(
|
||||||
thickness = 1.5.dp,
|
thickness = 1.5.dp,
|
||||||
color = Color(0x40888888),
|
color = dividerColor,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(start = 16.dp)
|
.padding(start = 16.dp)
|
||||||
)
|
)
|
||||||
@@ -136,15 +150,19 @@ fun PressAndHoldSettings(navController: NavController) {
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.height(55.dp)
|
.height(55.dp)
|
||||||
.background(
|
.background(animatedRightBackgroundColor, RoundedCornerShape(bottomEnd = 14.dp, bottomStart = 14.dp))
|
||||||
backgroundColor,
|
.pointerInput(Unit) {
|
||||||
RoundedCornerShape(18.dp)
|
detectTapGestures(
|
||||||
)
|
onPress = {
|
||||||
.clickable(
|
rightBackgroundColor = dividerColor
|
||||||
onClick = {
|
tryAwaitRelease()
|
||||||
navController.navigate("long_press/Right")
|
rightBackgroundColor = if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFFFFFFF)
|
||||||
}
|
},
|
||||||
),
|
onTap = {
|
||||||
|
navController.navigate("long_press/Right")
|
||||||
|
}
|
||||||
|
)
|
||||||
|
},
|
||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
@@ -162,7 +180,6 @@ fun PressAndHoldSettings(navController: NavController) {
|
|||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.weight(1f))
|
Spacer(modifier = Modifier.weight(1f))
|
||||||
Text(
|
Text(
|
||||||
// TODO: Implement voice assistant on long press; for now, it's noise control
|
|
||||||
text = stringResource(R.string.noise_control),
|
text = stringResource(R.string.noise_control),
|
||||||
style = TextStyle(
|
style = TextStyle(
|
||||||
fontSize = 18.sp,
|
fontSize = 18.sp,
|
||||||
|
|||||||
@@ -21,19 +21,15 @@
|
|||||||
package me.kavishdevar.aln.screens
|
package me.kavishdevar.aln.screens
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.content.BroadcastReceiver
|
|
||||||
import android.content.ComponentName
|
import android.content.ComponentName
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.IntentFilter
|
|
||||||
import android.content.ServiceConnection
|
import android.content.ServiceConnection
|
||||||
import android.os.Build
|
|
||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.isSystemInDarkTheme
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
|
||||||
import androidx.compose.foundation.layout.Box
|
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||||
import androidx.compose.foundation.layout.Row
|
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.height
|
||||||
import androidx.compose.foundation.layout.imePadding
|
import androidx.compose.foundation.layout.imePadding
|
||||||
import androidx.compose.foundation.layout.padding
|
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.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.automirrored.filled.KeyboardArrowLeft
|
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.material.icons.filled.Send
|
||||||
|
import androidx.compose.material3.Card
|
||||||
|
import androidx.compose.material3.CardDefaults
|
||||||
import androidx.compose.material3.CenterAlignedTopAppBar
|
import androidx.compose.material3.CenterAlignedTopAppBar
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.HorizontalDivider
|
import androidx.compose.material3.HorizontalDivider
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextButton
|
import androidx.compose.material3.TextButton
|
||||||
@@ -62,7 +64,7 @@ import androidx.compose.material3.TextFieldDefaults
|
|||||||
import androidx.compose.material3.TopAppBarDefaults
|
import androidx.compose.material3.TopAppBarDefaults
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.mutableStateListOf
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
@@ -83,18 +85,36 @@ import dev.chrisbanes.haze.haze
|
|||||||
import dev.chrisbanes.haze.hazeChild
|
import dev.chrisbanes.haze.hazeChild
|
||||||
import dev.chrisbanes.haze.materials.CupertinoMaterials
|
import dev.chrisbanes.haze.materials.CupertinoMaterials
|
||||||
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
|
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import me.kavishdevar.aln.R
|
import me.kavishdevar.aln.R
|
||||||
import me.kavishdevar.aln.services.AirPodsService
|
import me.kavishdevar.aln.services.AirPodsService
|
||||||
import me.kavishdevar.aln.utils.AirPodsNotifications
|
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class)
|
@OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class)
|
||||||
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter", "UnspecifiedRegisterReceiverFlag")
|
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter", "UnspecifiedRegisterReceiverFlag")
|
||||||
@Composable
|
@Composable
|
||||||
fun DebugScreen(navController: NavController) {
|
fun DebugScreen(navController: NavController) {
|
||||||
val hazeState = remember { HazeState() }
|
val hazeState = remember { HazeState() }
|
||||||
val text = remember { mutableStateListOf<String>("Log Start") }
|
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val listState = rememberLazyListState()
|
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(
|
Scaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
@@ -145,29 +165,6 @@ fun DebugScreen(navController: NavController) {
|
|||||||
containerColor = if (isSystemInDarkTheme()) Color(0xFF000000)
|
containerColor = if (isSystemInDarkTheme()) Color(0xFF000000)
|
||||||
else Color(0xFFF2F2F7),
|
else Color(0xFFF2F2F7),
|
||||||
) { paddingValues ->
|
) { 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(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
@@ -181,44 +178,53 @@ fun DebugScreen(navController: NavController) {
|
|||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.weight(1f),
|
.weight(1f),
|
||||||
content = {
|
content = {
|
||||||
items(text.size) { index ->
|
items(packetLogs.size) { index ->
|
||||||
val message = text[index]
|
val message = packetLogs.elementAt(index)
|
||||||
val isSent = message.startsWith(">")
|
val isSent = message.startsWith("Sent")
|
||||||
val backgroundColor =
|
val isExpanded = expandedItems.value.contains(index)
|
||||||
if (isSent) Color(0xFFE1FFC7) else Color(0xFFD1D1D1)
|
|
||||||
|
|
||||||
if (message == "Log Start") {
|
Card(
|
||||||
Spacer(modifier = Modifier.height(115.dp))
|
|
||||||
}
|
|
||||||
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(8.dp)
|
.padding(vertical = 2.dp, horizontal = 4.dp) // Reduced padding
|
||||||
.background(backgroundColor, RoundedCornerShape(12.dp))
|
.clickable {
|
||||||
.padding(12.dp),
|
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(
|
Column(modifier = Modifier.padding(8.dp)) { // Reduced padding
|
||||||
modifier = Modifier.fillMaxWidth(),
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
Icon(
|
||||||
horizontalArrangement = Arrangement.SpaceBetween
|
imageVector = if (isSent) Icons.AutoMirrored.Filled.KeyboardArrowLeft else Icons.AutoMirrored.Filled.KeyboardArrowRight,
|
||||||
) {
|
contentDescription = null,
|
||||||
if (!isSent) {
|
tint = if (isSent) Color.Green else Color.Red,
|
||||||
Text("<", color = Color(0xFF00796B), fontSize = 16.sp)
|
modifier = Modifier.size(24.dp) // Reduced icon size
|
||||||
}
|
|
||||||
|
|
||||||
Text(
|
|
||||||
text = if (isSent) message.substring(1) else message,
|
|
||||||
fontFamily = FontFamily(Font(R.font.hack)),
|
|
||||||
color = if (isSystemInDarkTheme()) Color(
|
|
||||||
0xFF000000
|
|
||||||
)
|
)
|
||||||
else Color(0xFF000000),
|
Spacer(modifier = Modifier.width(4.dp)) // Reduced spacing
|
||||||
modifier = Modifier.weight(1f)
|
Column {
|
||||||
)
|
Text(
|
||||||
|
text =
|
||||||
if (isSent) {
|
if (isSent) message.substring(5).take(60) + (if (message.substring(5).length > 60) "..." else "")
|
||||||
Text(">", color = Color(0xFF00796B), fontSize = 16.sp)
|
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(
|
IconButton(
|
||||||
onClick = {
|
onClick = {
|
||||||
airPodsService.value?.sendPacket(packet.value.text)
|
airPodsService.value?.sendPacket(packet.value.text)
|
||||||
text.add(packet.value.text)
|
|
||||||
packet.value = TextFieldValue("")
|
packet.value = TextFieldValue("")
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
@@ -282,23 +287,6 @@ fun DebugScreen(navController: NavController) {
|
|||||||
),
|
),
|
||||||
shape = RoundedCornerShape(12.dp)
|
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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,16 +41,18 @@ import androidx.compose.material3.Text
|
|||||||
import androidx.compose.material3.TextButton
|
import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.material3.TopAppBarDefaults
|
import androidx.compose.material3.TopAppBarDefaults
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.scale
|
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.Color
|
||||||
import androidx.compose.ui.graphics.SolidColor
|
import androidx.compose.ui.graphics.SolidColor
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.TextStyle
|
import androidx.compose.ui.text.TextStyle
|
||||||
import androidx.compose.ui.text.font.Font
|
import androidx.compose.ui.text.font.Font
|
||||||
@@ -69,6 +71,14 @@ fun RenameScreen(navController: NavController) {
|
|||||||
val sharedPreferences = LocalContext.current.getSharedPreferences("settings", Context.MODE_PRIVATE)
|
val sharedPreferences = LocalContext.current.getSharedPreferences("settings", Context.MODE_PRIVATE)
|
||||||
val isDarkTheme = isSystemInDarkTheme()
|
val isDarkTheme = isSystemInDarkTheme()
|
||||||
val name = remember { mutableStateOf(sharedPreferences.getString("name", "") ?: "") }
|
val name = remember { mutableStateOf(sharedPreferences.getString("name", "") ?: "") }
|
||||||
|
val focusRequester = remember { FocusRequester() }
|
||||||
|
val keyboardController = LocalSoftwareKeyboardController.current
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
focusRequester.requestFocus()
|
||||||
|
keyboardController?.show()
|
||||||
|
}
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
CenterAlignedTopAppBar(
|
CenterAlignedTopAppBar(
|
||||||
@@ -117,17 +127,10 @@ fun RenameScreen(navController: NavController) {
|
|||||||
.padding(horizontal = 16.dp)
|
.padding(horizontal = 16.dp)
|
||||||
.padding(top = 8.dp)
|
.padding(top = 8.dp)
|
||||||
) {
|
) {
|
||||||
var isFocused by remember { mutableStateOf(false) }
|
|
||||||
val isDarkTheme = isSystemInDarkTheme()
|
val isDarkTheme = isSystemInDarkTheme()
|
||||||
|
|
||||||
val backgroundColor = if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFFFFFFF)
|
val backgroundColor = if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFFFFFFF)
|
||||||
val textColor = if (isDarkTheme) Color.White else Color.Black
|
val textColor = if (isDarkTheme) Color.White else Color.Black
|
||||||
val cursorColor = if (isFocused) { // Show cursor only when focused
|
val cursorColor = if (isDarkTheme) Color.White else Color.Black
|
||||||
if (isDarkTheme) Color.White else Color.Black
|
|
||||||
} else {
|
|
||||||
Color.Transparent
|
|
||||||
}
|
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -180,6 +183,7 @@ fun RenameScreen(navController: NavController) {
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(start = 8.dp)
|
.padding(start = 8.dp)
|
||||||
|
.focusRequester(focusRequester)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ import android.content.ComponentName
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.IntentFilter
|
import android.content.IntentFilter
|
||||||
|
import android.content.SharedPreferences
|
||||||
import android.media.AudioManager
|
import android.media.AudioManager
|
||||||
import android.os.Binder
|
import android.os.Binder
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
@@ -95,6 +96,7 @@ object ServiceManager {
|
|||||||
delay(1000)
|
delay(1000)
|
||||||
context.startService(intent)
|
context.startService(intent)
|
||||||
context.startActivity(Intent(context, MainActivity::class.java))
|
context.startActivity(Intent(context, MainActivity::class.java))
|
||||||
|
service?.clearLogs()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -106,6 +108,34 @@ class AirPodsService: Service() {
|
|||||||
fun getService(): AirPodsService = this@AirPodsService
|
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 {
|
override fun onBind(intent: Intent?): IBinder {
|
||||||
return LocalBinder()
|
return LocalBinder()
|
||||||
}
|
}
|
||||||
@@ -133,7 +163,9 @@ class AirPodsService: Service() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
private fun forwardPacket(packet: String, outputStream: OutputStream) {
|
private fun forwardPacket(packet: String, outputStream: OutputStream) {
|
||||||
outputStream.write(packet.toByteArray())
|
val byteArray = packet.toByteArray()
|
||||||
|
outputStream.write(byteArray)
|
||||||
|
logPacket(byteArray, "Sent")
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun connectToAirPods() {
|
private fun connectToAirPods() {
|
||||||
@@ -400,7 +432,7 @@ class AirPodsService: Service() {
|
|||||||
.putString("name", name).apply()
|
.putString("name", name).apply()
|
||||||
}
|
}
|
||||||
Log.d("AirPodsQuickSwitchServices", CrossDevice.isAvailable.toString())
|
Log.d("AirPodsQuickSwitchServices", CrossDevice.isAvailable.toString())
|
||||||
if (!CrossDevice.isAvailable) {
|
if (!CrossDevice.checkAirPodsConnectionStatus()) {
|
||||||
Log.d("AirPodsService", "$name connected")
|
Log.d("AirPodsService", "$name connected")
|
||||||
showPopup(this@AirPodsService, name.toString())
|
showPopup(this@AirPodsService, name.toString())
|
||||||
connectToSocket(device!!)
|
connectToSocket(device!!)
|
||||||
@@ -463,7 +495,7 @@ class AirPodsService: Service() {
|
|||||||
if (profile == BluetoothProfile.A2DP) {
|
if (profile == BluetoothProfile.A2DP) {
|
||||||
val connectedDevices = proxy.connectedDevices
|
val connectedDevices = proxy.connectedDevices
|
||||||
if (connectedDevices.isNotEmpty()) {
|
if (connectedDevices.isNotEmpty()) {
|
||||||
if (!CrossDevice.isAvailable) {
|
if (!CrossDevice.checkAirPodsConnectionStatus()) {
|
||||||
connectToSocket(device)
|
connectToSocket(device)
|
||||||
}
|
}
|
||||||
this@AirPodsService.sendBroadcast(
|
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
|
return START_STICKY
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -581,6 +617,7 @@ class AirPodsService: Service() {
|
|||||||
var data: ByteArray = byteArrayOf()
|
var data: ByteArray = byteArrayOf()
|
||||||
if (bytesRead > 0) {
|
if (bytesRead > 0) {
|
||||||
data = buffer.copyOfRange(0, bytesRead)
|
data = buffer.copyOfRange(0, bytesRead)
|
||||||
|
logPacket(data, "AirPods")
|
||||||
sendBroadcast(Intent(AirPodsNotifications.Companion.AIRPODS_DATA).apply {
|
sendBroadcast(Intent(AirPodsNotifications.Companion.AIRPODS_DATA).apply {
|
||||||
putExtra("data", buffer.copyOfRange(0, bytesRead))
|
putExtra("data", buffer.copyOfRange(0, bytesRead))
|
||||||
})
|
})
|
||||||
@@ -824,8 +861,10 @@ class AirPodsService: Service() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (this::socket.isInitialized) {
|
if (this::socket.isInitialized) {
|
||||||
socket.outputStream?.write(fromHex.toByteArray())
|
val byteArray = fromHex.toByteArray()
|
||||||
|
socket.outputStream?.write(byteArray)
|
||||||
socket.outputStream?.flush()
|
socket.outputStream?.flush()
|
||||||
|
logPacket(byteArray, "Sent")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -837,6 +876,7 @@ class AirPodsService: Service() {
|
|||||||
if (this::socket.isInitialized) {
|
if (this::socket.isInitialized) {
|
||||||
socket.outputStream?.write(packet)
|
socket.outputStream?.write(packet)
|
||||||
socket.outputStream?.flush()
|
socket.outputStream?.flush()
|
||||||
|
logPacket(packet, "Sent")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1015,7 +1055,6 @@ class AirPodsService: Service() {
|
|||||||
val bytes = byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x1a, 0x00, 0x01,
|
val bytes = byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x1a, 0x00, 0x01,
|
||||||
nameBytes.size.toByte(), 0x00) + nameBytes
|
nameBytes.size.toByte(), 0x00) + nameBytes
|
||||||
sendPacket(bytes)
|
sendPacket(bytes)
|
||||||
socket.outputStream?.flush()
|
|
||||||
val hex = bytes.joinToString(" ") { "%02X".format(it) }
|
val hex = bytes.joinToString(" ") { "%02X".format(it) }
|
||||||
updateNotificationContent(true, name, batteryNotification.getBattery())
|
updateNotificationContent(true, name, batteryNotification.getBattery())
|
||||||
Log.d("AirPodsService", "setName: $name, sent packet: $hex")
|
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 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()
|
var bytes = hex.split(" ").map { it.toInt(16).toByte() }.toByteArray()
|
||||||
sendPacket(bytes)
|
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"
|
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()
|
bytes = hex.split(" ").map { it.toInt(16).toByte() }.toByteArray()
|
||||||
sendPacket(bytes)
|
sendPacket(bytes)
|
||||||
socket.outputStream?.flush()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setLoudSoundReduction(enabled: Boolean) {
|
fun setLoudSoundReduction(enabled: Boolean) {
|
||||||
val hex = "52 1B 00 0${if (enabled) "1" else "0"}"
|
val hex = "52 1B 00 0${if (enabled) "1" else "0"}"
|
||||||
val bytes = hex.split(" ").map { it.toInt(16).toByte() }.toByteArray()
|
val bytes = hex.split(" ").map { it.toInt(16).toByte() }.toByteArray()
|
||||||
sendPacket(bytes)
|
sendPacket(bytes)
|
||||||
socket.outputStream?.flush()
|
|
||||||
}
|
}
|
||||||
fun findChangedIndex(oldArray: BooleanArray, newArray: BooleanArray): Int {
|
fun findChangedIndex(oldArray: BooleanArray, newArray: BooleanArray): Int {
|
||||||
for (i in oldArray.indices) {
|
for (i in oldArray.indices) {
|
||||||
@@ -1175,11 +1211,12 @@ class AirPodsService: Service() {
|
|||||||
}
|
}
|
||||||
packet?.let {
|
packet?.let {
|
||||||
Log.d("AirPodsService", "Sending packet: ${it.joinToString(" ") { "%02X".format(it) }}")
|
Log.d("AirPodsService", "Sending packet: ${it.joinToString(" ") { "%02X".format(it) }}")
|
||||||
socket.outputStream.write(it)
|
sendPacket(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
|
clearPacketLogs()
|
||||||
Log.d("AirPodsService", "Service stopped is being destroyed for some reason!")
|
Log.d("AirPodsService", "Service stopped is being destroyed for some reason!")
|
||||||
try {
|
try {
|
||||||
unregisterReceiver(bluetoothReceiver)
|
unregisterReceiver(bluetoothReceiver)
|
||||||
|
|||||||
@@ -5,8 +5,13 @@ import android.bluetooth.BluetoothAdapter
|
|||||||
import android.bluetooth.BluetoothManager
|
import android.bluetooth.BluetoothManager
|
||||||
import android.bluetooth.BluetoothServerSocket
|
import android.bluetooth.BluetoothServerSocket
|
||||||
import android.bluetooth.BluetoothSocket
|
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.Context
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
|
import android.os.ParcelUuid
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@@ -21,26 +26,36 @@ enum class CrossDevicePackets(val packet: ByteArray) {
|
|||||||
REQUEST_DISCONNECT(byteArrayOf(0x00, 0x02, 0x00, 0x00)),
|
REQUEST_DISCONNECT(byteArrayOf(0x00, 0x02, 0x00, 0x00)),
|
||||||
REQUEST_BATTERY_BYTES(byteArrayOf(0x00, 0x02, 0x00, 0x01)),
|
REQUEST_BATTERY_BYTES(byteArrayOf(0x00, 0x02, 0x00, 0x01)),
|
||||||
REQUEST_ANC_BYTES(byteArrayOf(0x00, 0x02, 0x00, 0x02)),
|
REQUEST_ANC_BYTES(byteArrayOf(0x00, 0x02, 0x00, 0x02)),
|
||||||
|
REQUEST_CONNECTION_STATUS(byteArrayOf(0x00, 0x02, 0x00, 0x03)),
|
||||||
AIRPODS_DATA_HEADER(byteArrayOf(0x00, 0x04, 0x00, 0x01)),
|
AIRPODS_DATA_HEADER(byteArrayOf(0x00, 0x04, 0x00, 0x01)),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
object CrossDevice {
|
object CrossDevice {
|
||||||
|
var initialized = false
|
||||||
private val uuid = UUID.fromString("1abbb9a4-10e4-4000-a75c-8953c5471342")
|
private val uuid = UUID.fromString("1abbb9a4-10e4-4000-a75c-8953c5471342")
|
||||||
private var serverSocket: BluetoothServerSocket? = null
|
private var serverSocket: BluetoothServerSocket? = null
|
||||||
private var clientSocket: BluetoothSocket? = null
|
private var clientSocket: BluetoothSocket? = null
|
||||||
private lateinit var bluetoothAdapter: BluetoothAdapter
|
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 isAvailable: Boolean = false // set to true when airpods are connected to another device
|
||||||
var batteryBytes: ByteArray = byteArrayOf()
|
var batteryBytes: ByteArray = byteArrayOf()
|
||||||
var ancBytes: ByteArray = byteArrayOf()
|
var ancBytes: ByteArray = byteArrayOf()
|
||||||
private lateinit var sharedPreferences: SharedPreferences
|
private lateinit var sharedPreferences: SharedPreferences
|
||||||
|
private const val packetLogKey = "packet_log"
|
||||||
|
|
||||||
@SuppressLint("MissingPermission")
|
@SuppressLint("MissingPermission")
|
||||||
fun init(context: Context) {
|
fun init(context: Context) {
|
||||||
Log.d("AirPodsQuickSwitchService", "Initializing CrossDevice")
|
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()
|
sharedPreferences.edit().putBoolean("CrossDeviceIsAvailable", false).apply()
|
||||||
this.bluetoothAdapter = context.getSystemService(BluetoothManager::class.java).adapter
|
this.bluetoothAdapter = context.getSystemService(BluetoothManager::class.java).adapter
|
||||||
|
this.bluetoothLeAdvertiser = bluetoothAdapter.bluetoothLeAdvertiser
|
||||||
|
startAdvertising()
|
||||||
startServer()
|
startServer()
|
||||||
|
initialized = true
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("MissingPermission")
|
@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) {
|
fun setAirPodsConnected(connected: Boolean) {
|
||||||
if (connected) {
|
if (connected) {
|
||||||
isAvailable = false
|
isAvailable = false
|
||||||
@@ -71,9 +114,21 @@ object CrossDevice {
|
|||||||
|
|
||||||
fun sendReceivedPacket(packet: ByteArray) {
|
fun sendReceivedPacket(packet: ByteArray) {
|
||||||
Log.d("AirPodsQuickSwitchService", "Sending packet to remote device")
|
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)
|
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")
|
@SuppressLint("MissingPermission")
|
||||||
private fun handleClientConnection(socket: BluetoothSocket) {
|
private fun handleClientConnection(socket: BluetoothSocket) {
|
||||||
Log.d("AirPodsQuickSwitchService", "Client connected")
|
Log.d("AirPodsQuickSwitchService", "Client connected")
|
||||||
@@ -85,6 +140,7 @@ object CrossDevice {
|
|||||||
while (true) {
|
while (true) {
|
||||||
bytes = inputStream.read(buffer)
|
bytes = inputStream.read(buffer)
|
||||||
val packet = buffer.copyOf(bytes)
|
val packet = buffer.copyOf(bytes)
|
||||||
|
logPacket(packet, "Relay")
|
||||||
Log.d("AirPodsQuickSwitchService", "Received packet: ${packet.joinToString("") { "%02x".format(it) }}")
|
Log.d("AirPodsQuickSwitchService", "Received packet: ${packet.joinToString("") { "%02x".format(it) }}")
|
||||||
if (bytes == -1) {
|
if (bytes == -1) {
|
||||||
break
|
break
|
||||||
@@ -102,19 +158,17 @@ object CrossDevice {
|
|||||||
} else if (packet.contentEquals(CrossDevicePackets.REQUEST_ANC_BYTES.packet)) {
|
} else if (packet.contentEquals(CrossDevicePackets.REQUEST_ANC_BYTES.packet)) {
|
||||||
Log.d("AirPodsQuickSwitchService", "Received ANC request")
|
Log.d("AirPodsQuickSwitchService", "Received ANC request")
|
||||||
sendRemotePacket(ancBytes)
|
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 {
|
else {
|
||||||
if (packet.sliceArray(0..3).contentEquals(CrossDevicePackets.AIRPODS_DATA_HEADER.packet)) {
|
if (packet.sliceArray(0..3).contentEquals(CrossDevicePackets.AIRPODS_DATA_HEADER.packet)) {
|
||||||
// the AIRPODS_CONNECTED wasn't sent before
|
|
||||||
isAvailable = true
|
isAvailable = true
|
||||||
sharedPreferences.edit().putBoolean("CrossDeviceIsAvailable", true).apply()
|
sharedPreferences.edit().putBoolean("CrossDeviceIsAvailable", true).apply()
|
||||||
val trimmedPacket =
|
val trimmedPacket = packet.drop(CrossDevicePackets.AIRPODS_DATA_HEADER.packet.size).toByteArray()
|
||||||
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", "Received relayed packet, with ${sharedPreferences.getBoolean("CrossDeviceIsAvailable", false)} | ${ServiceManager.getService()?.batteryNotification?.isBatteryData(trimmedPacket)}")
|
||||||
Log.d(
|
Log.d("AirPodsQuickSwitchService", "Received relayed packet: ${trimmedPacket.joinToString("") { "%02x".format(it) }}")
|
||||||
"AirPodsQuickSwitchService",
|
|
||||||
"Relayed packet: ${trimmedPacket.joinToString("") { "%02x".format(it) }}"
|
|
||||||
)
|
|
||||||
if (ServiceManager.getService()?.isConnectedLocally == true) {
|
if (ServiceManager.getService()?.isConnectedLocally == true) {
|
||||||
val packetInHex = trimmedPacket.joinToString("") { "%02x".format(it) }
|
val packetInHex = trimmedPacket.joinToString("") { "%02x".format(it) }
|
||||||
ServiceManager.getService()?.sendPacket(packetInHex)
|
ServiceManager.getService()?.sendPacket(packetInHex)
|
||||||
@@ -142,6 +196,25 @@ object CrossDevice {
|
|||||||
}
|
}
|
||||||
clientSocket?.outputStream?.write(byteArray)
|
clientSocket?.outputStream?.write(byteArray)
|
||||||
clientSocket?.outputStream?.flush()
|
clientSocket?.outputStream?.flush()
|
||||||
|
logPacket(byteArray, "Sent")
|
||||||
Log.d("AirPodsQuickSwitchService", "Sent packet to remote device")
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -157,8 +157,13 @@ class AirPodsNotifications {
|
|||||||
private var case: Battery = Battery(BatteryComponent.CASE, 0, BatteryStatus.DISCONNECTED)
|
private var case: Battery = Battery(BatteryComponent.CASE, 0, BatteryStatus.DISCONNECTED)
|
||||||
|
|
||||||
fun isBatteryData(data: ByteArray): Boolean {
|
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) {
|
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
|
return false
|
||||||
}
|
}
|
||||||
Log.d("BatteryNotification", data.joinToString("") { "%02x".format(it) }.startsWith("040004000400").toString())
|
Log.d("BatteryNotification", data.joinToString("") { "%02x".format(it) }.startsWith("040004000400").toString())
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[versions]
|
[versions]
|
||||||
accompanistPermissions = "0.36.0"
|
accompanistPermissions = "0.36.0"
|
||||||
agp = "8.7.3"
|
agp = "8.8.0"
|
||||||
hiddenapibypass = "4.3"
|
hiddenapibypass = "4.3"
|
||||||
kotlin = "2.0.0"
|
kotlin = "2.0.0"
|
||||||
coreKtx = "1.15.0"
|
coreKtx = "1.15.0"
|
||||||
|
|||||||
143
linux/main.cpp
143
linux/main.cpp
@@ -39,6 +39,10 @@ Q_LOGGING_CATEGORY(airpodsApp, "airpodsApp")
|
|||||||
|
|
||||||
#define PHONE_MAC_ADDRESS "22:22:F5:BB:1C:A0"
|
#define PHONE_MAC_ADDRESS "22:22:F5:BB:1C:A0"
|
||||||
|
|
||||||
|
// Define Manufacturer Specific Data Identifier
|
||||||
|
#define MANUFACTURER_ID 0x1234
|
||||||
|
#define MANUFACTURER_DATA "ALN_AirPods"
|
||||||
|
|
||||||
class AirPodsTrayApp : public QObject {
|
class AirPodsTrayApp : public QObject {
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
||||||
@@ -88,6 +92,8 @@ public:
|
|||||||
connect(trayIcon, &QSystemTrayIcon::activated, this, &AirPodsTrayApp::onTrayIconActivated);
|
connect(trayIcon, &QSystemTrayIcon::activated, this, &AirPodsTrayApp::onTrayIconActivated);
|
||||||
|
|
||||||
discoveryAgent = new QBluetoothDeviceDiscoveryAgent();
|
discoveryAgent = new QBluetoothDeviceDiscoveryAgent();
|
||||||
|
discoveryAgent->setLowEnergyDiscoveryTimeout(5000);
|
||||||
|
|
||||||
connect(discoveryAgent, &QBluetoothDeviceDiscoveryAgent::deviceDiscovered, this, &AirPodsTrayApp::onDeviceDiscovered);
|
connect(discoveryAgent, &QBluetoothDeviceDiscoveryAgent::deviceDiscovered, this, &AirPodsTrayApp::onDeviceDiscovered);
|
||||||
connect(discoveryAgent, &QBluetoothDeviceDiscoveryAgent::finished, this, &AirPodsTrayApp::onDiscoveryFinished);
|
connect(discoveryAgent, &QBluetoothDeviceDiscoveryAgent::finished, this, &AirPodsTrayApp::onDiscoveryFinished);
|
||||||
discoveryAgent->start();
|
discoveryAgent->start();
|
||||||
@@ -107,6 +113,16 @@ public:
|
|||||||
}
|
}
|
||||||
initializeMprisInterface();
|
initializeMprisInterface();
|
||||||
connect(phoneSocket, &QBluetoothSocket::readyRead, this, &AirPodsTrayApp::onPhoneDataReceived);
|
connect(phoneSocket, &QBluetoothSocket::readyRead, this, &AirPodsTrayApp::onPhoneDataReceived);
|
||||||
|
|
||||||
|
// After starting discovery, check if service record exists
|
||||||
|
QDBusInterface iface("org.bluez", "/org/bluez", "org.bluez.Adapter1");
|
||||||
|
QDBusReply<QVariant> reply = iface.call("GetServiceRecords", QString::fromUtf8("74ec2172-0bad-4d01-8f77-997b2be0722a"));
|
||||||
|
if (reply.isValid()) {
|
||||||
|
LOG_INFO("Service record found, proceeding with connection");
|
||||||
|
// Proceed with existing connection logic
|
||||||
|
} else {
|
||||||
|
LOG_WARN("Service record not found, waiting for BLE broadcast");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public slots:
|
public slots:
|
||||||
@@ -288,6 +304,12 @@ public slots:
|
|||||||
}
|
}
|
||||||
|
|
||||||
void onDeviceDiscovered(const QBluetoothDeviceInfo &device) {
|
void onDeviceDiscovered(const QBluetoothDeviceInfo &device) {
|
||||||
|
QByteArray manufacturerData = device.manufacturerData(MANUFACTURER_ID);
|
||||||
|
if (manufacturerData.startsWith(MANUFACTURER_DATA)) {
|
||||||
|
LOG_INFO("Detected AirPods via BLE manufacturer data");
|
||||||
|
// Initiate RFComm connection
|
||||||
|
connectToDevice(device.address().toString());
|
||||||
|
}
|
||||||
LOG_INFO("Device discovered: " << device.name() << " (" << device.address().toString() << ")");
|
LOG_INFO("Device discovered: " << device.name() << " (" << device.address().toString() << ")");
|
||||||
if (device.serviceUuids().contains(QBluetoothUuid("74ec2172-0bad-4d01-8f77-997b2be0722a"))) {
|
if (device.serviceUuids().contains(QBluetoothUuid("74ec2172-0bad-4d01-8f77-997b2be0722a"))) {
|
||||||
LOG_DEBUG("Found AirPods device" + device.name());
|
LOG_DEBUG("Found AirPods device" + device.name());
|
||||||
@@ -297,6 +319,8 @@ public slots:
|
|||||||
|
|
||||||
void onDiscoveryFinished() {
|
void onDiscoveryFinished() {
|
||||||
LOG_INFO("Device discovery finished");
|
LOG_INFO("Device discovery finished");
|
||||||
|
// Restart discovery to continuously listen for broadcasts
|
||||||
|
discoveryAgent->start();
|
||||||
const QList<QBluetoothDeviceInfo> discoveredDevices = discoveryAgent->discoveredDevices();
|
const QList<QBluetoothDeviceInfo> discoveredDevices = discoveryAgent->discoveredDevices();
|
||||||
for (const QBluetoothDeviceInfo &device : discoveredDevices) {
|
for (const QBluetoothDeviceInfo &device : discoveredDevices) {
|
||||||
if (device.serviceUuids().contains(QBluetoothUuid("74ec2172-0bad-4d01-8f77-997b2be0722a"))) {
|
if (device.serviceUuids().contains(QBluetoothUuid("74ec2172-0bad-4d01-8f77-997b2be0722a"))) {
|
||||||
@@ -335,57 +359,76 @@ public slots:
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
LOG_INFO("Connecting to device: " << device.name() << " (" << device.address().toString() << ")");
|
LOG_INFO("Checking connection status with phone before connecting to device: " << device.name());
|
||||||
QBluetoothSocket *localSocket = new QBluetoothSocket(QBluetoothServiceInfo::L2capProtocol);
|
QByteArray connectionStatusRequest = QByteArray::fromHex("00020003");
|
||||||
connect(localSocket, &QBluetoothSocket::connected, this, [this, localSocket]() {
|
if (phoneSocket && phoneSocket->isOpen()) {
|
||||||
LOG_INFO("Connected to device, sending initial packets");
|
phoneSocket->write(connectionStatusRequest);
|
||||||
discoveryAgent->stop();
|
LOG_DEBUG("Connection status request packet written: " << connectionStatusRequest.toHex());
|
||||||
|
connect(phoneSocket, &QBluetoothSocket::readyRead, this, [this, device]() {
|
||||||
|
QByteArray data = phoneSocket->read(4);
|
||||||
|
LOG_DEBUG("Data received from phone: " << data.toHex());
|
||||||
|
if (data == QByteArray::fromHex("00010001")) {
|
||||||
|
LOG_INFO("AirPods are already connected");
|
||||||
|
disconnect(phoneSocket, &QBluetoothSocket::readyRead, nullptr, nullptr);
|
||||||
|
} else if (data == QByteArray::fromHex("00010000")) {
|
||||||
|
LOG_INFO("AirPods are disconnected, proceeding with connection");
|
||||||
|
disconnect(phoneSocket, &QBluetoothSocket::readyRead, nullptr, nullptr);
|
||||||
|
|
||||||
QByteArray handshakePacket = QByteArray::fromHex("00000400010002000000000000000000");
|
QBluetoothSocket *localSocket = new QBluetoothSocket(QBluetoothServiceInfo::L2capProtocol);
|
||||||
QByteArray setSpecificFeaturesPacket = QByteArray::fromHex("040004004d00ff00000000000000");
|
connect(localSocket, &QBluetoothSocket::connected, this, [this, localSocket]() {
|
||||||
QByteArray requestNotificationsPacket = QByteArray::fromHex("040004000f00ffffffffff");
|
LOG_INFO("Connected to device, sending initial packets");
|
||||||
|
discoveryAgent->stop();
|
||||||
|
|
||||||
qint64 bytesWritten = localSocket->write(handshakePacket);
|
QByteArray handshakePacket = QByteArray::fromHex("00000400010002000000000000000000");
|
||||||
LOG_DEBUG("Handshake packet written: " << handshakePacket.toHex() << ", bytes written: " << bytesWritten);
|
QByteArray setSpecificFeaturesPacket = QByteArray::fromHex("040004004d00ff00000000000000");
|
||||||
|
QByteArray requestNotificationsPacket = QByteArray::fromHex("040004000f00ffffffffff");
|
||||||
|
|
||||||
QByteArray airpodsConnectedPacket = QByteArray::fromHex("000400010001");
|
qint64 bytesWritten = localSocket->write(handshakePacket);
|
||||||
phoneSocket->write(airpodsConnectedPacket);
|
LOG_DEBUG("Handshake packet written: " << handshakePacket.toHex() << ", bytes written: " << bytesWritten);
|
||||||
LOG_DEBUG("AIRPODS_CONNECTED packet written: " << airpodsConnectedPacket.toHex());
|
|
||||||
|
|
||||||
connect(localSocket, &QBluetoothSocket::bytesWritten, this, [this, localSocket, setSpecificFeaturesPacket, requestNotificationsPacket](qint64 bytes) {
|
QByteArray airpodsConnectedPacket = QByteArray::fromHex("000400010001");
|
||||||
LOG_INFO("Bytes written: " << bytes);
|
phoneSocket->write(airpodsConnectedPacket);
|
||||||
if (bytes > 0) {
|
LOG_DEBUG("AIRPODS_CONNECTED packet written: " << airpodsConnectedPacket.toHex());
|
||||||
static int step = 0;
|
|
||||||
switch (step) {
|
connect(localSocket, &QBluetoothSocket::bytesWritten, this, [this, localSocket, setSpecificFeaturesPacket, requestNotificationsPacket](qint64 bytes) {
|
||||||
case 0:
|
LOG_INFO("Bytes written: " << bytes);
|
||||||
localSocket->write(setSpecificFeaturesPacket);
|
if (bytes > 0) {
|
||||||
LOG_DEBUG("Set specific features packet written: " << setSpecificFeaturesPacket.toHex());
|
static int step = 0;
|
||||||
step++;
|
switch (step) {
|
||||||
break;
|
case 0:
|
||||||
case 1:
|
localSocket->write(setSpecificFeaturesPacket);
|
||||||
localSocket->write(requestNotificationsPacket);
|
LOG_DEBUG("Set specific features packet written: " << setSpecificFeaturesPacket.toHex());
|
||||||
LOG_DEBUG("Request notifications packet written: " << requestNotificationsPacket.toHex());
|
step++;
|
||||||
step++;
|
break;
|
||||||
break;
|
case 1:
|
||||||
}
|
localSocket->write(requestNotificationsPacket);
|
||||||
|
LOG_DEBUG("Request notifications packet written: " << requestNotificationsPacket.toHex());
|
||||||
|
step++;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
connect(localSocket, &QBluetoothSocket::readyRead, this, [this, localSocket]() {
|
||||||
|
QByteArray data = localSocket->readAll();
|
||||||
|
LOG_DEBUG("Data received: " << data.toHex());
|
||||||
|
parseData(data);
|
||||||
|
relayPacketToPhone(data);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
connect(localSocket, QOverload<QBluetoothSocket::SocketError>::of(&QBluetoothSocket::errorOccurred), this, [this, localSocket](QBluetoothSocket::SocketError error) {
|
||||||
|
LOG_ERROR("Socket error: " << error << ", " << localSocket->errorString());
|
||||||
|
});
|
||||||
|
|
||||||
|
localSocket->connectToService(device.address(), QBluetoothUuid("74ec2172-0bad-4d01-8f77-997b2be0722a"));
|
||||||
|
socket = localSocket;
|
||||||
|
connectedDeviceMacAddress = device.address().toString().replace(":", "_");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
connect(localSocket, &QBluetoothSocket::readyRead, this, [this, localSocket]() {
|
LOG_ERROR("Phone socket is not open, cannot send connection status request");
|
||||||
QByteArray data = localSocket->readAll();
|
}
|
||||||
LOG_DEBUG("Data received: " << data.toHex());
|
|
||||||
parseData(data);
|
|
||||||
relayPacketToPhone(data);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
connect(localSocket, QOverload<QBluetoothSocket::SocketError>::of(&QBluetoothSocket::errorOccurred), this, [this, localSocket](QBluetoothSocket::SocketError error) {
|
|
||||||
LOG_ERROR("Socket error: " << error << ", " << localSocket->errorString());
|
|
||||||
});
|
|
||||||
|
|
||||||
localSocket->connectToService(device.address(), QBluetoothUuid("74ec2172-0bad-4d01-8f77-997b2be0722a"));
|
|
||||||
socket = localSocket;
|
|
||||||
connectedDeviceMacAddress = device.address().toString().replace(":", "_");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void parseData(const QByteArray &data) {
|
void parseData(const QByteArray &data) {
|
||||||
@@ -416,7 +459,6 @@ public slots:
|
|||||||
.arg(caseLevel);
|
.arg(caseLevel);
|
||||||
LOG_INFO("Battery status: " << batteryStatus);
|
LOG_INFO("Battery status: " << batteryStatus);
|
||||||
emit batteryStatusChanged(batteryStatus);
|
emit batteryStatusChanged(batteryStatus);
|
||||||
relayPacketToPhone(data);
|
|
||||||
|
|
||||||
} else if (data.size() == 10 && data.startsWith(QByteArray::fromHex("040004004B00020001"))) {
|
} else if (data.size() == 10 && data.startsWith(QByteArray::fromHex("040004004B00020001"))) {
|
||||||
LOG_INFO("Received conversational awareness data");
|
LOG_INFO("Received conversational awareness data");
|
||||||
@@ -436,9 +478,6 @@ public slots:
|
|||||||
process.start("pactl", QStringList() << "get-sink-volume" << "@DEFAULT_SINK@");
|
process.start("pactl", QStringList() << "get-sink-volume" << "@DEFAULT_SINK@");
|
||||||
process.waitForFinished();
|
process.waitForFinished();
|
||||||
QString output = process.readAllStandardOutput();
|
QString output = process.readAllStandardOutput();
|
||||||
// Volume: front-left: 12843 / 20% / -42.47 dB, front-right: 12843 / 20% / -42.47 dB
|
|
||||||
// balance 0.00
|
|
||||||
|
|
||||||
QRegularExpression re("front-left: \\d+ /\\s*(\\d+)%");
|
QRegularExpression re("front-left: \\d+ /\\s*(\\d+)%");
|
||||||
QRegularExpressionMatch match = re.match(output);
|
QRegularExpressionMatch match = re.match(output);
|
||||||
if (match.hasMatch()) {
|
if (match.hasMatch()) {
|
||||||
@@ -513,6 +552,7 @@ public slots:
|
|||||||
phoneSocket->write(header + packet);
|
phoneSocket->write(header + packet);
|
||||||
LOG_DEBUG("Relayed packet to phone with header: " << (header + packet).toHex());
|
LOG_DEBUG("Relayed packet to phone with header: " << (header + packet).toHex());
|
||||||
} else {
|
} else {
|
||||||
|
connectToPhone();
|
||||||
LOG_WARN("Phone socket is not open, cannot relay packet");
|
LOG_WARN("Phone socket is not open, cannot relay packet");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -532,6 +572,11 @@ public slots:
|
|||||||
} else if (packet.startsWith(QByteArray::fromHex("00010000"))) {
|
} else if (packet.startsWith(QByteArray::fromHex("00010000"))) {
|
||||||
LOG_INFO("AirPods disconnected");
|
LOG_INFO("AirPods disconnected");
|
||||||
// Handle AirPods disconnected
|
// Handle AirPods disconnected
|
||||||
|
} else if (packet.startsWith(QByteArray::fromHex("00020003"))) {
|
||||||
|
LOG_INFO("Connection status request received");
|
||||||
|
QByteArray response = (socket && socket->isOpen()) ? QByteArray::fromHex("00010001") : QByteArray::fromHex("00010000");
|
||||||
|
phoneSocket->write(response);
|
||||||
|
LOG_DEBUG("Sent connection status response: " << response.toHex());
|
||||||
} else {
|
} else {
|
||||||
if (socket && socket->isOpen()) {
|
if (socket && socket->isOpen()) {
|
||||||
socket->write(packet);
|
socket->write(packet);
|
||||||
|
|||||||
Binary file not shown.
2
root-module-manual/post-data-fs.sh
Normal file
2
root-module-manual/post-data-fs.sh
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
#!/system/bin/sh
|
||||||
|
mount -t overlay overlay -o lowerdir=/apex/com.android.btservices/lib64/,upperdir=/data/adb/modules/btl2capfix/apex/com.android.btservices/lib64,workdir=/data/adb/modules/btl2capfix/apex/com.android.btservices/work Zapex/com.android.btservices/lib64
|
||||||
Reference in New Issue
Block a user