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:
@@ -35,10 +35,17 @@ import android.util.Log
|
|||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
import androidx.activity.enableEdgeToEdge
|
import androidx.activity.enableEdgeToEdge
|
||||||
import androidx.compose.animation.core.tween
|
import androidx.compose.animation.core.Spring
|
||||||
|
import androidx.compose.animation.core.spring
|
||||||
|
import androidx.compose.animation.scaleIn
|
||||||
|
import androidx.compose.animation.scaleOut
|
||||||
import androidx.compose.animation.slideInHorizontally
|
import androidx.compose.animation.slideInHorizontally
|
||||||
import androidx.compose.animation.slideOutHorizontally
|
import androidx.compose.animation.slideOutHorizontally
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.material3.Button
|
import androidx.compose.material3.Button
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
@@ -48,6 +55,7 @@ import androidx.compose.runtime.Composable
|
|||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.toArgb
|
import androidx.compose.ui.graphics.toArgb
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
@@ -183,14 +191,75 @@ fun Main() {
|
|||||||
context.registerReceiver(connectionStatusReceiver, filter)
|
context.registerReceiver(connectionStatusReceiver, filter)
|
||||||
}
|
}
|
||||||
Log.d("MainActivity", "Registered Receiver")
|
Log.d("MainActivity", "Registered Receiver")
|
||||||
|
Box (
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(0.dp)
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(if (isSystemInDarkTheme()) Color.Black else Color(0xFFF2F2F7))
|
||||||
|
) {
|
||||||
NavHost(
|
NavHost(
|
||||||
navController = navController,
|
navController = navController,
|
||||||
startDestination = "settings",
|
startDestination = "settings",
|
||||||
enterTransition = { slideInHorizontally(initialOffsetX = { it }, animationSpec = tween(300)) },
|
enterTransition = {
|
||||||
exitTransition = { slideOutHorizontally(targetOffsetX = { -it }, animationSpec = tween(300)) },
|
slideInHorizontally(
|
||||||
popEnterTransition = { slideInHorizontally(initialOffsetX = { -it }, animationSpec = tween(300)) },
|
initialOffsetX = { it },
|
||||||
popExitTransition = { slideOutHorizontally(targetOffsetX = { it }, animationSpec = tween(300)) }
|
animationSpec = spring(
|
||||||
|
dampingRatio = Spring.DampingRatioLowBouncy,
|
||||||
|
stiffness = Spring.StiffnessLow
|
||||||
|
)
|
||||||
|
) + scaleIn(
|
||||||
|
initialScale = 0.85f,
|
||||||
|
animationSpec = spring(
|
||||||
|
dampingRatio = Spring.DampingRatioLowBouncy,
|
||||||
|
stiffness = Spring.StiffnessLow
|
||||||
|
)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
exitTransition = {
|
||||||
|
slideOutHorizontally(
|
||||||
|
targetOffsetX = { -it },
|
||||||
|
animationSpec = spring(
|
||||||
|
dampingRatio = Spring.DampingRatioLowBouncy,
|
||||||
|
stiffness = Spring.StiffnessLow
|
||||||
|
)
|
||||||
|
) + scaleOut(
|
||||||
|
targetScale = 0.85f,
|
||||||
|
animationSpec = spring(
|
||||||
|
dampingRatio = Spring.DampingRatioLowBouncy,
|
||||||
|
stiffness = Spring.StiffnessLow
|
||||||
|
)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
popEnterTransition = {
|
||||||
|
slideInHorizontally(
|
||||||
|
initialOffsetX = { -it },
|
||||||
|
animationSpec = spring(
|
||||||
|
dampingRatio = Spring.DampingRatioLowBouncy,
|
||||||
|
stiffness = Spring.StiffnessLow
|
||||||
|
)
|
||||||
|
) + scaleIn(
|
||||||
|
initialScale = 0.85f,
|
||||||
|
animationSpec = spring(
|
||||||
|
dampingRatio = Spring.DampingRatioLowBouncy,
|
||||||
|
stiffness = Spring.StiffnessLow
|
||||||
|
)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
popExitTransition = {
|
||||||
|
slideOutHorizontally(
|
||||||
|
targetOffsetX = { it },
|
||||||
|
animationSpec = spring(
|
||||||
|
dampingRatio = Spring.DampingRatioLowBouncy,
|
||||||
|
stiffness = Spring.StiffnessLow
|
||||||
|
)
|
||||||
|
) + scaleOut(
|
||||||
|
targetScale = 0.85f,
|
||||||
|
animationSpec = spring(
|
||||||
|
dampingRatio = Spring.DampingRatioLowBouncy,
|
||||||
|
stiffness = Spring.StiffnessLow
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
) {
|
) {
|
||||||
composable("settings") {
|
composable("settings") {
|
||||||
if (airPodsService.value != null) {
|
if (airPodsService.value != null) {
|
||||||
@@ -219,7 +288,7 @@ fun Main() {
|
|||||||
AppSettingsScreen(navController)
|
AppSettingsScreen(navController)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
serviceConnection = remember {
|
serviceConnection = remember {
|
||||||
object : ServiceConnection {
|
object : ServiceConnection {
|
||||||
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
|
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
|
||||||
|
|||||||
@@ -131,7 +131,7 @@ fun AccessibilitySettings(service: AirPodsService, sharedPreferences: SharedPref
|
|||||||
|
|
||||||
SinglePodANCSwitch(service = service, sharedPreferences = sharedPreferences)
|
SinglePodANCSwitch(service = service, sharedPreferences = sharedPreferences)
|
||||||
VolumeControlSwitch(service = service, sharedPreferences = sharedPreferences)
|
VolumeControlSwitch(service = service, sharedPreferences = sharedPreferences)
|
||||||
TransparencySettings(service = service, sharedPreferences = sharedPreferences)
|
// TransparencySettings(service = service, sharedPreferences = sharedPreferences)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,8 +19,10 @@
|
|||||||
package me.kavishdevar.aln.composables
|
package me.kavishdevar.aln.composables
|
||||||
|
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
|
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.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
@@ -38,6 +40,7 @@ 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.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
@@ -51,6 +54,8 @@ fun IndependentToggle(name: String, service: AirPodsService, functionName: Strin
|
|||||||
|
|
||||||
val snakeCasedName = name.replace(Regex("[\\W\\s]+"), "_").lowercase()
|
val snakeCasedName = name.replace(Regex("[\\W\\s]+"), "_").lowercase()
|
||||||
var checked by remember { mutableStateOf(default) }
|
var checked by remember { mutableStateOf(default) }
|
||||||
|
var backgroundColor by remember { mutableStateOf(if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFFFFFFF)) }
|
||||||
|
val animatedBackgroundColor by animateColorAsState(targetValue = backgroundColor, animationSpec = tween(durationMillis = 500))
|
||||||
|
|
||||||
LaunchedEffect(sharedPreferences) {
|
LaunchedEffect(sharedPreferences) {
|
||||||
checked = sharedPreferences.getBoolean(snakeCasedName, true)
|
checked = sharedPreferences.getBoolean(snakeCasedName, true)
|
||||||
@@ -58,11 +63,15 @@ fun IndependentToggle(name: String, service: AirPodsService, functionName: Strin
|
|||||||
Box (
|
Box (
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(vertical = 8.dp)
|
.padding(vertical = 8.dp)
|
||||||
.background(
|
.background(animatedBackgroundColor, RoundedCornerShape(14.dp))
|
||||||
if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFFFFFFF),
|
.pointerInput(Unit) {
|
||||||
RoundedCornerShape(14.dp)
|
detectTapGestures(
|
||||||
)
|
onPress = {
|
||||||
.clickable {
|
backgroundColor = if (isDarkTheme) Color(0x40888888) else Color(0x40D9D9D9)
|
||||||
|
tryAwaitRelease()
|
||||||
|
backgroundColor = if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFFFFFFF)
|
||||||
|
},
|
||||||
|
onTap = {
|
||||||
checked = !checked
|
checked = !checked
|
||||||
sharedPreferences
|
sharedPreferences
|
||||||
.edit()
|
.edit()
|
||||||
@@ -71,6 +80,8 @@ fun IndependentToggle(name: String, service: AirPodsService, functionName: Strin
|
|||||||
|
|
||||||
val method = service::class.java.getMethod(functionName, Boolean::class.java)
|
val method = service::class.java.getMethod(functionName, Boolean::class.java)
|
||||||
method.invoke(service, checked)
|
method.invoke(service, checked)
|
||||||
|
}
|
||||||
|
)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
@@ -44,6 +46,7 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.compose.ui.focus.onFocusChanged
|
import androidx.compose.ui.focus.onFocusChanged
|
||||||
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.input.pointer.pointerInput
|
||||||
import androidx.compose.ui.text.TextStyle
|
import androidx.compose.ui.text.TextStyle
|
||||||
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
|
||||||
@@ -62,9 +65,11 @@ fun NameField(
|
|||||||
|
|
||||||
val isDarkTheme = isSystemInDarkTheme()
|
val isDarkTheme = isSystemInDarkTheme()
|
||||||
|
|
||||||
val backgroundColor = if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFFFFFFF)
|
var backgroundColor by remember { mutableStateOf(if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFFFFFFF)) }
|
||||||
|
val animatedBackgroundColor by animateColorAsState(targetValue = backgroundColor, animationSpec = tween(durationMillis = 500))
|
||||||
|
|
||||||
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 (isFocused) {
|
||||||
if (isDarkTheme) Color.White else Color.Black
|
if (isDarkTheme) Color.White else Color.Black
|
||||||
} else {
|
} else {
|
||||||
Color.Transparent
|
Color.Transparent
|
||||||
@@ -72,11 +77,19 @@ fun NameField(
|
|||||||
|
|
||||||
Box (
|
Box (
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.clickable(
|
.background(animatedBackgroundColor, RoundedCornerShape(14.dp))
|
||||||
onClick = {
|
.pointerInput(Unit) {
|
||||||
|
detectTapGestures(
|
||||||
|
onPress = {
|
||||||
|
backgroundColor = if (isDarkTheme) Color(0x40888888) else Color(0x40D9D9D9)
|
||||||
|
tryAwaitRelease()
|
||||||
|
backgroundColor = if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFFFFFFF)
|
||||||
|
},
|
||||||
|
onTap = {
|
||||||
navController.navigate("rename")
|
navController.navigate("rename")
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
}
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
@@ -84,7 +97,7 @@ fun NameField(
|
|||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.height(55.dp)
|
.height(55.dp)
|
||||||
.background(
|
.background(
|
||||||
backgroundColor,
|
animatedBackgroundColor,
|
||||||
RoundedCornerShape(14.dp)
|
RoundedCornerShape(14.dp)
|
||||||
)
|
)
|
||||||
.padding(horizontal = 16.dp, vertical = 8.dp)
|
.padding(horizontal = 16.dp, vertical = 8.dp)
|
||||||
|
|||||||
@@ -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.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
@@ -34,8 +36,13 @@ import androidx.compose.material3.IconButton
|
|||||||
import androidx.compose.material3.IconButtonDefaults
|
import androidx.compose.material3.IconButtonDefaults
|
||||||
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.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.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
@@ -44,29 +51,38 @@ import androidx.navigation.NavController
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun NavigationButton(to: String, name: String, navController: NavController) {
|
fun NavigationButton(to: String, name: String, navController: NavController) {
|
||||||
|
val isDarkTheme = isSystemInDarkTheme()
|
||||||
|
var backgroundColor by remember { mutableStateOf(if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFFFFFFF)) }
|
||||||
|
val animatedBackgroundColor by animateColorAsState(targetValue = backgroundColor, animationSpec = tween(durationMillis = 500))
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.background(
|
.background(animatedBackgroundColor, RoundedCornerShape(14.dp))
|
||||||
if (isSystemInDarkTheme()) Color(
|
|
||||||
0xFF1C1C1E
|
|
||||||
) else Color(0xFFFFFFFF), RoundedCornerShape(14.dp)
|
|
||||||
)
|
|
||||||
.height(55.dp)
|
.height(55.dp)
|
||||||
.clickable {
|
.pointerInput(Unit) {
|
||||||
|
detectTapGestures(
|
||||||
|
onPress = {
|
||||||
|
backgroundColor = if (isDarkTheme) Color(0x40888888) else Color(0x40D9D9D9)
|
||||||
|
tryAwaitRelease()
|
||||||
|
backgroundColor = if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFFFFFFF)
|
||||||
|
},
|
||||||
|
onTap = {
|
||||||
navController.navigate(to)
|
navController.navigate(to)
|
||||||
}
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = name,
|
text = name,
|
||||||
modifier = Modifier.padding(16.dp),
|
modifier = Modifier.padding(16.dp),
|
||||||
color = if (isSystemInDarkTheme()) Color.White else Color.Black
|
color = if (isDarkTheme) Color.White else Color.Black
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.weight(1f))
|
Spacer(modifier = Modifier.weight(1f))
|
||||||
IconButton(
|
IconButton(
|
||||||
onClick = { navController.navigate(to) },
|
onClick = { navController.navigate(to) },
|
||||||
colors = IconButtonDefaults.iconButtonColors(
|
colors = IconButtonDefaults.iconButtonColors(
|
||||||
containerColor = Color.Transparent,
|
containerColor = Color.Transparent,
|
||||||
contentColor = if (isSystemInDarkTheme()) Color.White else Color.Black
|
contentColor = if (isDarkTheme) Color.White else Color.Black
|
||||||
),
|
),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(start = 16.dp)
|
.padding(start = 16.dp)
|
||||||
|
|||||||
@@ -170,7 +170,7 @@ fun NoiseControlSettings(service: AirPodsService) {
|
|||||||
context.registerReceiver(noiseControlReceiver, noiseControlIntentFilter)
|
context.registerReceiver(noiseControlReceiver, noiseControlIntentFilter)
|
||||||
}
|
}
|
||||||
|
|
||||||
Text(// all caps
|
Text(
|
||||||
text = stringResource(R.string.noise_control).uppercase(),
|
text = stringResource(R.string.noise_control).uppercase(),
|
||||||
style = TextStyle(
|
style = TextStyle(
|
||||||
fontSize = 14.sp,
|
fontSize = 14.sp,
|
||||||
@@ -182,7 +182,7 @@ fun NoiseControlSettings(service: AirPodsService) {
|
|||||||
BoxWithConstraints(
|
BoxWithConstraints(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(vertical = 8.dp) // Adjusted padding
|
.padding(vertical = 8.dp)
|
||||||
) {
|
) {
|
||||||
val density = LocalDensity.current
|
val density = LocalDensity.current
|
||||||
val buttonCount = if (offListeningMode.value) 4 else 3
|
val buttonCount = if (offListeningMode.value) 4 else 3
|
||||||
@@ -229,10 +229,9 @@ fun NoiseControlSettings(service: AirPodsService) {
|
|||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.height(60.dp) // Adjusted height
|
.height(60.dp)
|
||||||
.background(backgroundColor, RoundedCornerShape(14.dp))
|
.background(backgroundColor, RoundedCornerShape(14.dp))
|
||||||
) {
|
) {
|
||||||
// First: Background Row (just for visual)
|
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth()
|
modifier = Modifier.fillMaxWidth()
|
||||||
) {
|
) {
|
||||||
@@ -327,7 +326,6 @@ fun NoiseControlSettings(service: AirPodsService) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Button row (top layer)
|
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
@@ -387,12 +385,11 @@ fun NoiseControlSettings(service: AirPodsService) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Labels row
|
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(horizontal = 8.dp)
|
.padding(horizontal = 4.dp)
|
||||||
.padding(top = 2.dp)
|
.padding(top = 4.dp)
|
||||||
) {
|
) {
|
||||||
if (offListeningMode.value) {
|
if (offListeningMode.value) {
|
||||||
Text(
|
Text(
|
||||||
@@ -429,8 +426,8 @@ fun NoiseControlSettings(service: AirPodsService) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Preview@Composable
|
@Preview()
|
||||||
|
@Composable
|
||||||
fun NoiseControlSettingsPreview() {
|
fun NoiseControlSettingsPreview() {
|
||||||
NoiseControlSettings(AirPodsService())
|
NoiseControlSettings(AirPodsService())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -283,7 +283,7 @@ fun AirPodsSettingsScreen(dev: BluetoothDevice?, service: AirPodsService,
|
|||||||
)
|
)
|
||||||
Spacer(Modifier.height(24.dp))
|
Spacer(Modifier.height(24.dp))
|
||||||
Text(
|
Text(
|
||||||
text = "Please connect your AirPods to access settings. If you're stuck here, then try reopening the app again after closing it from the recents.\n(DO NOT KILL THE APP!)",
|
text = "Please connect your AirPods to access settings.",
|
||||||
style = TextStyle(
|
style = TextStyle(
|
||||||
fontSize = 16.sp,
|
fontSize = 16.sp,
|
||||||
fontWeight = FontWeight.Light,
|
fontWeight = FontWeight.Light,
|
||||||
|
|||||||
@@ -21,12 +21,7 @@
|
|||||||
package me.kavishdevar.aln.screens
|
package me.kavishdevar.aln.screens
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.content.ComponentName
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
|
||||||
import android.content.ServiceConnection
|
|
||||||
import android.os.IBinder
|
|
||||||
import android.util.Log
|
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.isSystemInDarkTheme
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
@@ -65,6 +60,8 @@ 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.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.derivedStateOf
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
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
|
||||||
@@ -87,7 +84,7 @@ import dev.chrisbanes.haze.materials.CupertinoMaterials
|
|||||||
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
|
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
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.ServiceManager
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class)
|
@OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class)
|
||||||
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter", "UnspecifiedRegisterReceiverFlag")
|
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter", "UnspecifiedRegisterReceiverFlag")
|
||||||
@@ -96,24 +93,13 @@ fun DebugScreen(navController: NavController) {
|
|||||||
val hazeState = remember { HazeState() }
|
val hazeState = remember { HazeState() }
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val listState = rememberLazyListState()
|
val listState = rememberLazyListState()
|
||||||
|
val scrollOffset by remember { derivedStateOf { listState.firstVisibleItemScrollOffset } }
|
||||||
val packetLogsFlow = remember { MutableStateFlow(emptySet<String>()) }
|
val packetLogsFlow = remember { MutableStateFlow(emptySet<String>()) }
|
||||||
val expandedItems = remember { mutableStateOf(setOf<Int>()) }
|
val expandedItems = remember { mutableStateOf(setOf<Int>()) }
|
||||||
|
|
||||||
LaunchedEffect(context) {
|
LaunchedEffect(Unit) {
|
||||||
val serviceConnection = object : ServiceConnection {
|
ServiceManager.getService()?.packetLogsFlow?.collect { packetLogsFlow.value = it }
|
||||||
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
|
val packetLogs = packetLogsFlow.collectAsState(setOf()).value
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
@@ -150,7 +136,7 @@ fun DebugScreen(navController: NavController) {
|
|||||||
state = hazeState,
|
state = hazeState,
|
||||||
style = CupertinoMaterials.thick(),
|
style = CupertinoMaterials.thick(),
|
||||||
block = {
|
block = {
|
||||||
alpha = if (listState.firstVisibleItemIndex > 0) {
|
alpha = if (scrollOffset > 0) {
|
||||||
1f
|
1f
|
||||||
} else {
|
} else {
|
||||||
0f
|
0f
|
||||||
@@ -170,7 +156,7 @@ fun DebugScreen(navController: NavController) {
|
|||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.imePadding()
|
.imePadding()
|
||||||
.haze(hazeState)
|
.haze(hazeState)
|
||||||
.padding(top = 0.dp)
|
.padding(top = paddingValues.calculateTopPadding())
|
||||||
) {
|
) {
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
state = listState,
|
state = listState,
|
||||||
@@ -186,7 +172,7 @@ fun DebugScreen(navController: NavController) {
|
|||||||
Card(
|
Card(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(vertical = 2.dp, horizontal = 4.dp) // Reduced padding
|
.padding(vertical = 2.dp, horizontal = 4.dp)
|
||||||
.clickable {
|
.clickable {
|
||||||
expandedItems.value = if (isExpanded) {
|
expandedItems.value = if (isExpanded) {
|
||||||
expandedItems.value - index
|
expandedItems.value - index
|
||||||
@@ -194,21 +180,21 @@ fun DebugScreen(navController: NavController) {
|
|||||||
expandedItems.value + index
|
expandedItems.value + index
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp), // Reduced elevation
|
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp),
|
||||||
shape = RoundedCornerShape(4.dp), // Reduced corner radius
|
shape = RoundedCornerShape(4.dp),
|
||||||
colors = CardDefaults.cardColors(
|
colors = CardDefaults.cardColors(
|
||||||
containerColor = Color.Transparent
|
containerColor = Color.Transparent
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
Column(modifier = Modifier.padding(8.dp)) { // Reduced padding
|
Column(modifier = Modifier.padding(8.dp)) {
|
||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = if (isSent) Icons.AutoMirrored.Filled.KeyboardArrowLeft else Icons.AutoMirrored.Filled.KeyboardArrowRight,
|
imageVector = if (isSent) Icons.AutoMirrored.Filled.KeyboardArrowLeft else Icons.AutoMirrored.Filled.KeyboardArrowRight,
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
tint = if (isSent) Color.Green else Color.Red,
|
tint = if (isSent) Color.Green else Color.Red,
|
||||||
modifier = Modifier.size(24.dp) // Reduced icon size
|
modifier = Modifier.size(24.dp)
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.width(4.dp)) // Reduced spacing
|
Spacer(modifier = Modifier.width(4.dp))
|
||||||
Column {
|
Column {
|
||||||
Text(
|
Text(
|
||||||
text =
|
text =
|
||||||
@@ -217,7 +203,7 @@ fun DebugScreen(navController: NavController) {
|
|||||||
style = MaterialTheme.typography.bodySmall,
|
style = MaterialTheme.typography.bodySmall,
|
||||||
)
|
)
|
||||||
if (isExpanded) {
|
if (isExpanded) {
|
||||||
Spacer(modifier = Modifier.height(4.dp)) // Reduced spacing
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
Text(
|
Text(
|
||||||
text = message.substring(if (isSent) 5 else 9),
|
text = message.substring(if (isSent) 5 else 9),
|
||||||
style = MaterialTheme.typography.bodySmall,
|
style = MaterialTheme.typography.bodySmall,
|
||||||
@@ -232,22 +218,7 @@ fun DebugScreen(navController: NavController) {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
val airPodsService = remember { mutableStateOf<AirPodsService?>(null) }
|
val airPodsService = ServiceManager.getService()?.let { mutableStateOf(it) }
|
||||||
|
|
||||||
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)
|
|
||||||
HorizontalDivider()
|
HorizontalDivider()
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -267,7 +238,7 @@ fun DebugScreen(navController: NavController) {
|
|||||||
trailingIcon = {
|
trailingIcon = {
|
||||||
IconButton(
|
IconButton(
|
||||||
onClick = {
|
onClick = {
|
||||||
airPodsService.value?.sendPacket(packet.value.text)
|
airPodsService?.value?.sendPacket(packet.value.text)
|
||||||
packet.value = TextFieldValue("")
|
packet.value = TextFieldValue("")
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -20,8 +20,10 @@ package me.kavishdevar.aln.screens
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
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.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
@@ -46,13 +48,16 @@ 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.MutableState
|
import androidx.compose.runtime.MutableState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
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.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.ImageBitmap
|
import androidx.compose.ui.graphics.ImageBitmap
|
||||||
|
import androidx.compose.ui.input.pointer.pointerInput
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.imageResource
|
import androidx.compose.ui.res.imageResource
|
||||||
import androidx.compose.ui.text.TextStyle
|
import androidx.compose.ui.text.TextStyle
|
||||||
@@ -155,13 +160,13 @@ fun LongPress(navController: NavController, name: String) {
|
|||||||
horizontalAlignment = Alignment.CenterHorizontally
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
) {
|
) {
|
||||||
val offListeningMode = sharedPreferences.getBoolean("off_listening_mode", false)
|
val offListeningMode = sharedPreferences.getBoolean("off_listening_mode", false)
|
||||||
LongPressElement("Off", offChecked, "long_press_off", offListeningMode, R.drawable.noise_cancellation)
|
LongPressElement("Off", offChecked, "long_press_off", offListeningMode, R.drawable.noise_cancellation, isFirst = true)
|
||||||
if (offListeningMode) RightDivider()
|
if (offListeningMode) RightDivider()
|
||||||
LongPressElement("Transparency", transparencyChecked, "long_press_transparency", resourceId = R.drawable.transparency)
|
LongPressElement("Transparency", transparencyChecked, "long_press_transparency", resourceId = R.drawable.transparency, isFirst = !offListeningMode)
|
||||||
RightDivider()
|
RightDivider()
|
||||||
LongPressElement("Adaptive", adaptiveChecked, "long_press_adaptive", resourceId = R.drawable.adaptive)
|
LongPressElement("Adaptive", adaptiveChecked, "long_press_adaptive", resourceId = R.drawable.adaptive)
|
||||||
RightDivider()
|
RightDivider()
|
||||||
LongPressElement("Noise Cancellation", ncChecked, "long_press_nc", resourceId = R.drawable.noise_cancellation)
|
LongPressElement("Noise Cancellation", ncChecked, "long_press_nc", resourceId = R.drawable.noise_cancellation, isLast = true)
|
||||||
}
|
}
|
||||||
Text(
|
Text(
|
||||||
"Press and hold the stem to cycle between the selected noise control modes.",
|
"Press and hold the stem to cycle between the selected noise control modes.",
|
||||||
@@ -176,7 +181,7 @@ fun LongPress(navController: NavController, name: String) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun LongPressElement (name: String, checked: MutableState<Boolean>, id: String, enabled: Boolean = true, resourceId: Int) {
|
fun LongPressElement(name: String, checked: MutableState<Boolean>, id: String, enabled: Boolean = true, resourceId: Int, isFirst: Boolean = false, isLast: Boolean = false) {
|
||||||
val sharedPreferences =
|
val sharedPreferences =
|
||||||
LocalContext.current.getSharedPreferences("settings", Context.MODE_PRIVATE)
|
LocalContext.current.getSharedPreferences("settings", Context.MODE_PRIVATE)
|
||||||
val offListeningMode = sharedPreferences.getBoolean("off_listening_mode", false)
|
val offListeningMode = sharedPreferences.getBoolean("off_listening_mode", false)
|
||||||
@@ -213,15 +218,30 @@ fun LongPressElement (name: String, checked: MutableState<Boolean>, id: String,
|
|||||||
ServiceManager.getService()
|
ServiceManager.getService()
|
||||||
?.updateLongPress(originalLongPressArray, newLongPressArray, offListeningMode)
|
?.updateLongPress(originalLongPressArray, newLongPressArray, offListeningMode)
|
||||||
}
|
}
|
||||||
|
val shape = when {
|
||||||
|
isFirst -> RoundedCornerShape(topStart = 14.dp, topEnd = 14.dp)
|
||||||
|
isLast -> RoundedCornerShape(bottomStart = 14.dp, bottomEnd = 14.dp)
|
||||||
|
else -> RoundedCornerShape(0.dp)
|
||||||
|
}
|
||||||
|
var backgroundColor by remember { mutableStateOf(if (darkMode) Color(0xFF1C1C1E) else Color(0xFFFFFFFF)) }
|
||||||
|
val animatedBackgroundColor by animateColorAsState(targetValue = backgroundColor, animationSpec = tween(durationMillis = 500))
|
||||||
if (!enabled) {
|
if (!enabled) {
|
||||||
valueChanged(false)
|
valueChanged(false)
|
||||||
} else {
|
} else {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.height(72.dp)
|
.height(72.dp)
|
||||||
.clickable(
|
.background(animatedBackgroundColor, shape)
|
||||||
onClick = { valueChanged() }
|
.pointerInput(Unit) {
|
||||||
|
detectTapGestures(
|
||||||
|
onPress = {
|
||||||
|
backgroundColor = if (darkMode) Color(0x40888888) else Color(0x40D9D9D9)
|
||||||
|
tryAwaitRelease()
|
||||||
|
backgroundColor = if (darkMode) Color(0xFF1C1C1E) else Color(0xFFFFFFFF)
|
||||||
|
},
|
||||||
|
onTap = { valueChanged() }
|
||||||
)
|
)
|
||||||
|
}
|
||||||
.padding(horizontal = 16.dp, vertical = 0.dp),
|
.padding(horizontal = 16.dp, vertical = 0.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.SpaceBetween
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
|
|||||||
@@ -54,10 +54,12 @@ 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.platform.LocalSoftwareKeyboardController
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.TextRange
|
||||||
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
|
||||||
import androidx.compose.ui.text.font.FontFamily
|
import androidx.compose.ui.text.font.FontFamily
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.input.TextFieldValue
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
@@ -65,18 +67,20 @@ import androidx.navigation.NavController
|
|||||||
import me.kavishdevar.aln.R
|
import me.kavishdevar.aln.R
|
||||||
import me.kavishdevar.aln.services.ServiceManager
|
import me.kavishdevar.aln.services.ServiceManager
|
||||||
|
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun RenameScreen(navController: NavController) {
|
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(TextFieldValue(sharedPreferences.getString("name", "") ?: "")) }
|
||||||
val focusRequester = remember { FocusRequester() }
|
val focusRequester = remember { FocusRequester() }
|
||||||
val keyboardController = LocalSoftwareKeyboardController.current
|
val keyboardController = LocalSoftwareKeyboardController.current
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
focusRequester.requestFocus()
|
focusRequester.requestFocus()
|
||||||
keyboardController?.show()
|
keyboardController?.show()
|
||||||
|
name.value = name.value.copy(selection = TextRange(name.value.text.length))
|
||||||
}
|
}
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
@@ -102,7 +106,7 @@ fun RenameScreen(navController: NavController) {
|
|||||||
modifier = Modifier.scale(1.5f)
|
modifier = Modifier.scale(1.5f)
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
text = name.value,
|
text = name.value.text,
|
||||||
style = TextStyle(
|
style = TextStyle(
|
||||||
fontSize = 18.sp,
|
fontSize = 18.sp,
|
||||||
fontWeight = FontWeight.Medium,
|
fontWeight = FontWeight.Medium,
|
||||||
@@ -146,8 +150,8 @@ fun RenameScreen(navController: NavController) {
|
|||||||
value = name.value,
|
value = name.value,
|
||||||
onValueChange = {
|
onValueChange = {
|
||||||
name.value = it
|
name.value = it
|
||||||
sharedPreferences.edit().putString("name", it).apply()
|
sharedPreferences.edit().putString("name", it.text).apply()
|
||||||
ServiceManager.getService()?.setName(it)
|
ServiceManager.getService()?.setName(it.text)
|
||||||
},
|
},
|
||||||
textStyle = TextStyle(
|
textStyle = TextStyle(
|
||||||
color = textColor,
|
color = textColor,
|
||||||
@@ -167,7 +171,7 @@ fun RenameScreen(navController: NavController) {
|
|||||||
}
|
}
|
||||||
IconButton(
|
IconButton(
|
||||||
onClick = {
|
onClick = {
|
||||||
name.value = ""
|
name.value = TextFieldValue("")
|
||||||
sharedPreferences.edit().putString("name", "").apply()
|
sharedPreferences.edit().putString("name", "").apply()
|
||||||
ServiceManager.getService()?.setName("")
|
ServiceManager.getService()?.setName("")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import android.annotation.SuppressLint
|
|||||||
import android.app.Notification
|
import android.app.Notification
|
||||||
import android.app.NotificationChannel
|
import android.app.NotificationChannel
|
||||||
import android.app.NotificationManager
|
import android.app.NotificationManager
|
||||||
|
import android.app.PendingIntent
|
||||||
import android.app.Service
|
import android.app.Service
|
||||||
import android.appwidget.AppWidgetManager
|
import android.appwidget.AppWidgetManager
|
||||||
import android.bluetooth.BluetoothAdapter
|
import android.bluetooth.BluetoothAdapter
|
||||||
@@ -57,6 +58,8 @@ import kotlinx.coroutines.Dispatchers
|
|||||||
import kotlinx.coroutines.channels.awaitClose
|
import kotlinx.coroutines.channels.awaitClose
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.callbackFlow
|
import kotlinx.coroutines.flow.callbackFlow
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import me.kavishdevar.aln.BatteryWidget
|
import me.kavishdevar.aln.BatteryWidget
|
||||||
@@ -73,7 +76,6 @@ import me.kavishdevar.aln.utils.LongPressPackets
|
|||||||
import me.kavishdevar.aln.utils.MediaController
|
import me.kavishdevar.aln.utils.MediaController
|
||||||
import me.kavishdevar.aln.utils.Window
|
import me.kavishdevar.aln.utils.Window
|
||||||
import org.lsposed.hiddenapibypass.HiddenApiBypass
|
import org.lsposed.hiddenapibypass.HiddenApiBypass
|
||||||
import java.io.OutputStream
|
|
||||||
|
|
||||||
object ServiceManager {
|
object ServiceManager {
|
||||||
private var service: AirPodsService? = null
|
private var service: AirPodsService? = null
|
||||||
@@ -110,10 +112,13 @@ class AirPodsService: Service() {
|
|||||||
|
|
||||||
private lateinit var sharedPreferences: SharedPreferences
|
private lateinit var sharedPreferences: SharedPreferences
|
||||||
private val packetLogKey = "packet_log"
|
private val packetLogKey = "packet_log"
|
||||||
|
private val _packetLogsFlow = MutableStateFlow<Set<String>>(emptySet())
|
||||||
|
val packetLogsFlow: StateFlow<Set<String>> get() = _packetLogsFlow
|
||||||
|
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
sharedPreferences = getSharedPreferences("packet_logs", Context.MODE_PRIVATE)
|
sharedPreferences = getSharedPreferences("packet_logs", MODE_PRIVATE)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun logPacket(packet: ByteArray, source: String) {
|
private fun logPacket(packet: ByteArray, source: String) {
|
||||||
@@ -121,6 +126,7 @@ class AirPodsService: Service() {
|
|||||||
val logEntry = "$source: $packetHex"
|
val logEntry = "$source: $packetHex"
|
||||||
val logs = sharedPreferences.getStringSet(packetLogKey, mutableSetOf())?.toMutableSet() ?: mutableSetOf()
|
val logs = sharedPreferences.getStringSet(packetLogKey, mutableSetOf())?.toMutableSet() ?: mutableSetOf()
|
||||||
logs.add(logEntry)
|
logs.add(logEntry)
|
||||||
|
_packetLogsFlow.value = logs
|
||||||
sharedPreferences.edit().putStringSet(packetLogKey, logs).apply()
|
sharedPreferences.edit().putStringSet(packetLogKey, logs).apply()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -134,6 +140,7 @@ class AirPodsService: Service() {
|
|||||||
|
|
||||||
fun clearLogs() {
|
fun clearLogs() {
|
||||||
clearPacketLogs() // Expose a method to clear logs
|
clearPacketLogs() // Expose a method to clear logs
|
||||||
|
_packetLogsFlow.value = emptySet()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onBind(intent: Intent?): IBinder {
|
override fun onBind(intent: Intent?): IBinder {
|
||||||
@@ -151,32 +158,6 @@ class AirPodsService: Service() {
|
|||||||
popupShown = true
|
popupShown = true
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleMessage(message: String, outputStream: OutputStream) {
|
|
||||||
when (message) {
|
|
||||||
"PAUSE_MEDIA" -> MediaController.sendPause()
|
|
||||||
"PLAY_MEDIA" -> MediaController.sendPlay()
|
|
||||||
"CONNECT_AIRPODS" -> connectToAirPods()
|
|
||||||
"DISCONNECT_AIRPODS" -> disconnectFromAirPods()
|
|
||||||
else -> {
|
|
||||||
forwardPacket(message, outputStream)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
private fun forwardPacket(packet: String, outputStream: OutputStream) {
|
|
||||||
val byteArray = packet.toByteArray()
|
|
||||||
outputStream.write(byteArray)
|
|
||||||
logPacket(byteArray, "Sent")
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun connectToAirPods() {
|
|
||||||
Log.d("AirPodsQuickSwitchService", "Should connect to AirPods now.")
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun disconnectFromAirPods() {
|
|
||||||
Log.d("AirPodsQuickSwitchService", "Should disconnect from AirPods now.")
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Suppress("ClassName")
|
@Suppress("ClassName")
|
||||||
private object bluetoothReceiver: BroadcastReceiver() {
|
private object bluetoothReceiver: BroadcastReceiver() {
|
||||||
@SuppressLint("MissingPermission")
|
@SuppressLint("MissingPermission")
|
||||||
@@ -254,6 +235,7 @@ class AirPodsService: Service() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
fun startForegroundNotification() {
|
fun startForegroundNotification() {
|
||||||
val notificationChannel = NotificationChannel(
|
val notificationChannel = NotificationChannel(
|
||||||
"background_service_status",
|
"background_service_status",
|
||||||
@@ -262,9 +244,17 @@ class AirPodsService: Service() {
|
|||||||
)
|
)
|
||||||
val notificationManager = getSystemService(NotificationManager::class.java)
|
val notificationManager = getSystemService(NotificationManager::class.java)
|
||||||
notificationManager.createNotificationChannel(notificationChannel)
|
notificationManager.createNotificationChannel(notificationChannel)
|
||||||
|
|
||||||
|
val notificationIntent = Intent(this, MainActivity::class.java)
|
||||||
|
val pendingIntent = PendingIntent.getActivity(
|
||||||
|
this, 0, notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
||||||
|
)
|
||||||
|
|
||||||
val notification = NotificationCompat.Builder(this, "background_service_status")
|
val notification = NotificationCompat.Builder(this, "background_service_status")
|
||||||
.setSmallIcon(R.drawable.airpods)
|
.setSmallIcon(R.drawable.airpods)
|
||||||
.setContentTitle("AirPods not connected")
|
.setContentTitle("AirPods not connected")
|
||||||
|
.setContentText("Tap to open app")
|
||||||
|
.setContentIntent(pendingIntent)
|
||||||
.setCategory(Notification.CATEGORY_SERVICE)
|
.setCategory(Notification.CATEGORY_SERVICE)
|
||||||
.setPriority(NotificationCompat.PRIORITY_LOW)
|
.setPriority(NotificationCompat.PRIORITY_LOW)
|
||||||
.setOngoing(true)
|
.setOngoing(true)
|
||||||
@@ -272,8 +262,7 @@ class AirPodsService: Service() {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
startForeground(1, notification)
|
startForeground(1, notification)
|
||||||
}
|
} catch (e: Exception) {
|
||||||
catch (e: Exception) {
|
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -309,46 +298,59 @@ class AirPodsService: Service() {
|
|||||||
it.setTextViewText(
|
it.setTextViewText(
|
||||||
R.id.left_battery_widget,
|
R.id.left_battery_widget,
|
||||||
batteryNotification.getBattery().find { it.component == BatteryComponent.LEFT }?.let {
|
batteryNotification.getBattery().find { it.component == BatteryComponent.LEFT }?.let {
|
||||||
// if (it.status != BatteryStatus.DISCONNECTED) {
|
|
||||||
"${if (it.status == BatteryStatus.CHARGING) "⚡" else ""} ${it.level}%"
|
"${if (it.status == BatteryStatus.CHARGING) "⚡" else ""} ${it.level}%"
|
||||||
// } else {
|
|
||||||
// ""
|
|
||||||
// }
|
|
||||||
} ?: ""
|
} ?: ""
|
||||||
)
|
)
|
||||||
|
it.setProgressBar(
|
||||||
|
R.id.left_battery_progress,
|
||||||
|
100,
|
||||||
|
batteryNotification.getBattery().find { it.component == BatteryComponent.LEFT }?.level ?: 0,
|
||||||
|
false
|
||||||
|
)
|
||||||
it.setTextViewText(
|
it.setTextViewText(
|
||||||
R.id.right_battery_widget,
|
R.id.right_battery_widget,
|
||||||
batteryNotification.getBattery().find { it.component == BatteryComponent.RIGHT }?.let {
|
batteryNotification.getBattery().find { it.component == BatteryComponent.RIGHT }?.let {
|
||||||
// if (it.status != BatteryStatus.DISCONNECTED) {
|
|
||||||
"${if (it.status == BatteryStatus.CHARGING) "⚡" else ""} ${it.level}%"
|
"${if (it.status == BatteryStatus.CHARGING) "⚡" else ""} ${it.level}%"
|
||||||
// } else {
|
|
||||||
// ""
|
|
||||||
// }
|
|
||||||
} ?: ""
|
} ?: ""
|
||||||
)
|
)
|
||||||
|
it.setProgressBar(
|
||||||
|
R.id.right_battery_progress,
|
||||||
|
100,
|
||||||
|
batteryNotification.getBattery().find { it.component == BatteryComponent.RIGHT }?.level ?: 0,
|
||||||
|
false
|
||||||
|
)
|
||||||
it.setTextViewText(
|
it.setTextViewText(
|
||||||
R.id.case_battery_widget,
|
R.id.case_battery_widget,
|
||||||
batteryNotification.getBattery().find { it.component == BatteryComponent.CASE }?.let {
|
batteryNotification.getBattery().find { it.component == BatteryComponent.CASE }?.let {
|
||||||
// if (it.status != BatteryStatus.DISCONNECTED) {
|
|
||||||
"${if (it.status == BatteryStatus.CHARGING) "⚡" else ""} ${it.level}%"
|
"${if (it.status == BatteryStatus.CHARGING) "⚡" else ""} ${it.level}%"
|
||||||
// } else {
|
|
||||||
// ""
|
|
||||||
// }
|
|
||||||
} ?: ""
|
} ?: ""
|
||||||
)
|
)
|
||||||
|
it.setProgressBar(
|
||||||
|
R.id.case_battery_progress,
|
||||||
|
100,
|
||||||
|
batteryNotification.getBattery().find { it.component == BatteryComponent.CASE }?.level ?: 0,
|
||||||
|
false
|
||||||
|
)
|
||||||
}
|
}
|
||||||
Log.d("AirPodsService", "Updating battery widget")
|
Log.d("AirPodsService", "Updating battery widget")
|
||||||
appWidgetManager.updateAppWidget(widgetIds, remoteViews)
|
appWidgetManager.updateAppWidget(widgetIds, remoteViews)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
fun updateNotificationContent(connected: Boolean, airpodsName: String? = null, batteryList: List<Battery>? = null) {
|
fun updateNotificationContent(connected: Boolean, airpodsName: String? = null, batteryList: List<Battery>? = null) {
|
||||||
val notificationManager = getSystemService(NotificationManager::class.java)
|
val notificationManager = getSystemService(NotificationManager::class.java)
|
||||||
var updatedNotification: Notification? = null
|
var updatedNotification: Notification? = null
|
||||||
|
|
||||||
|
val notificationIntent = Intent(this, MainActivity::class.java)
|
||||||
|
val pendingIntent = PendingIntent.getActivity(
|
||||||
|
this, 0, notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
||||||
|
)
|
||||||
|
|
||||||
if (connected) {
|
if (connected) {
|
||||||
updatedNotification = NotificationCompat.Builder(this, "background_service_status")
|
updatedNotification = NotificationCompat.Builder(this, "background_service_status")
|
||||||
.setSmallIcon(R.drawable.airpods)
|
.setSmallIcon(R.drawable.airpods)
|
||||||
.setContentTitle("""$airpodsName –${batteryList?.find { it.component == BatteryComponent.LEFT }?.let {
|
.setContentTitle(airpodsName)
|
||||||
|
.setContentText("""${batteryList?.find { it.component == BatteryComponent.LEFT }?.let {
|
||||||
if (it.status != BatteryStatus.DISCONNECTED) {
|
if (it.status != BatteryStatus.DISCONNECTED) {
|
||||||
"L: ${if (it.status == BatteryStatus.CHARGING) "⚡" else ""} ${it.level}%"
|
"L: ${if (it.status == BatteryStatus.CHARGING) "⚡" else ""} ${it.level}%"
|
||||||
} else {
|
} else {
|
||||||
@@ -362,20 +364,22 @@ class AirPodsService: Service() {
|
|||||||
}
|
}
|
||||||
} ?: ""} ${batteryList?.find { it.component == BatteryComponent.CASE }?.let {
|
} ?: ""} ${batteryList?.find { it.component == BatteryComponent.CASE }?.let {
|
||||||
if (it.status != BatteryStatus.DISCONNECTED) {
|
if (it.status != BatteryStatus.DISCONNECTED) {
|
||||||
" C:${if (it.status == BatteryStatus.CHARGING) "⚡" else ""} ${it.level}%"
|
"Case: ${if (it.status == BatteryStatus.CHARGING) "⚡" else ""} ${it.level}%"
|
||||||
} else {
|
} else {
|
||||||
""
|
""
|
||||||
}
|
}
|
||||||
} ?: ""}""")
|
} ?: ""}""")
|
||||||
|
.setContentIntent(pendingIntent)
|
||||||
.setCategory(Notification.CATEGORY_SERVICE)
|
.setCategory(Notification.CATEGORY_SERVICE)
|
||||||
.setPriority(NotificationCompat.PRIORITY_LOW)
|
.setPriority(NotificationCompat.PRIORITY_LOW)
|
||||||
.setOngoing(true)
|
.setOngoing(true)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
updatedNotification = NotificationCompat.Builder(this, "background_service_status")
|
updatedNotification = NotificationCompat.Builder(this, "background_service_status")
|
||||||
.setSmallIcon(R.drawable.airpods)
|
.setSmallIcon(R.drawable.airpods)
|
||||||
.setContentTitle("AirPods not connected")
|
.setContentTitle("AirPods not connected")
|
||||||
|
.setContentText("Tap to open app")
|
||||||
|
.setContentIntent(pendingIntent)
|
||||||
.setCategory(Notification.CATEGORY_SERVICE)
|
.setCategory(Notification.CATEGORY_SERVICE)
|
||||||
.setPriority(NotificationCompat.PRIORITY_LOW)
|
.setPriority(NotificationCompat.PRIORITY_LOW)
|
||||||
.setOngoing(true)
|
.setOngoing(true)
|
||||||
@@ -952,11 +956,6 @@ class AirPodsService: Service() {
|
|||||||
|
|
||||||
var earDetectionEnabled = true
|
var earDetectionEnabled = true
|
||||||
|
|
||||||
fun setCaseChargingSounds(enabled: Boolean) {
|
|
||||||
val bytes = byteArrayOf(0x12, 0x3a, 0x00, 0x01, 0x00, 0x08, if (enabled) 0x00 else 0x01)
|
|
||||||
sendPacket(bytes)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setEarDetection(enabled: Boolean) {
|
fun setEarDetection(enabled: Boolean) {
|
||||||
earDetectionEnabled = enabled
|
earDetectionEnabled = enabled
|
||||||
}
|
}
|
||||||
|
|||||||
7
android/app/src/main/res/drawable/blur_background.xml
Normal file
7
android/app/src/main/res/drawable/blur_background.xml
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item>
|
||||||
|
<shape android:shape="rectangle">
|
||||||
|
<solid android:color="#DA000000" />
|
||||||
|
</shape>
|
||||||
|
</item>
|
||||||
|
</layer-list>
|
||||||
15
android/app/src/main/res/drawable/circular_progress_bar.xml
Normal file
15
android/app/src/main/res/drawable/circular_progress_bar.xml
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<rotate xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:fromDegrees="-90"
|
||||||
|
android:toDegrees="270">
|
||||||
|
<shape
|
||||||
|
android:shape="ring"
|
||||||
|
android:innerRadiusRatio="3"
|
||||||
|
android:thicknessRatio="8"
|
||||||
|
android:useLevel="true">
|
||||||
|
<gradient
|
||||||
|
android:type="sweep"
|
||||||
|
android:useLevel="true"
|
||||||
|
android:startColor="#00ff00"
|
||||||
|
android:endColor="#00ff00" />
|
||||||
|
</shape>
|
||||||
|
</rotate>
|
||||||
10
android/app/src/main/res/drawable/ring_background.xml
Normal file
10
android/app/src/main/res/drawable/ring_background.xml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item>
|
||||||
|
<shape android:shape="oval">
|
||||||
|
<solid android:color="@android:color/transparent" />
|
||||||
|
<stroke
|
||||||
|
android:width="6dp"
|
||||||
|
android:color="#00ff00" />
|
||||||
|
</shape>
|
||||||
|
</item>
|
||||||
|
</layer-list>
|
||||||
@@ -4,70 +4,104 @@
|
|||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:id="@+id/battery_widget"
|
android:id="@+id/battery_widget"
|
||||||
android:theme="@style/Theme.ALN.AppWidgetContainer">
|
android:theme="@style/Theme.ALN.AppWidgetContainer"
|
||||||
|
android:background="@drawable/blur_background">
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="100dp"
|
android:layout_height="wrap_content"
|
||||||
android:layout_margin="0dp"
|
android:layout_margin="0dp"
|
||||||
android:gravity="center">
|
android:gravity="center"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:gravity="center"
|
||||||
|
android:orientation="vertical">
|
||||||
<ImageView
|
<ImageView
|
||||||
android:layout_width="64dp"
|
android:layout_width="64dp"
|
||||||
android:layout_height="64dp"
|
android:layout_height="64dp"
|
||||||
android:contentDescription="something"
|
android:src="@drawable/ring_background"
|
||||||
android:gravity="center_vertical"
|
android:layout_gravity="center">
|
||||||
android:layout_marginEnd="0dp"
|
<ImageView
|
||||||
|
android:layout_width="32dp"
|
||||||
|
android:layout_height="32dp"
|
||||||
android:src="@drawable/airpods_pro_left_notification"
|
android:src="@drawable/airpods_pro_left_notification"
|
||||||
android:tint="@android:color/system_accent2_400"
|
android:tint="@android:color/system_accent2_400"
|
||||||
|
android:layout_gravity="center"
|
||||||
tools:ignore="HardcodedText" />
|
tools:ignore="HardcodedText" />
|
||||||
|
</ImageView>
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/left_battery_widget"
|
android:id="@+id/left_battery_widget"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:textSize="24sp"
|
android:textSize="16sp"
|
||||||
android:textColor="@android:color/system_accent2_400"
|
android:textColor="@android:color/system_accent2_400"
|
||||||
android:layout_marginEnd="8dp"
|
android:gravity="center"
|
||||||
android:gravity="center_vertical"
|
|
||||||
android:text="Left"
|
android:text="Left"
|
||||||
tools:ignore="HardcodedText" />
|
tools:ignore="HardcodedText" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:gravity="center"
|
||||||
|
android:orientation="vertical">
|
||||||
<ImageView
|
<ImageView
|
||||||
android:layout_width="64dp"
|
android:layout_width="64dp"
|
||||||
android:layout_height="64dp"
|
android:layout_height="64dp"
|
||||||
|
android:src="@drawable/ring_background"
|
||||||
|
android:layout_gravity="center">
|
||||||
|
<ImageView
|
||||||
|
android:layout_width="32dp"
|
||||||
|
android:layout_height="32dp"
|
||||||
android:src="@drawable/airpods_pro_right_notification"
|
android:src="@drawable/airpods_pro_right_notification"
|
||||||
android:tint="@android:color/system_accent2_400"
|
android:tint="@android:color/system_accent2_400"
|
||||||
android:layout_margin="0dp"
|
android:layout_gravity="center"
|
||||||
android:contentDescription="something"
|
|
||||||
android:gravity="center_vertical"
|
|
||||||
tools:ignore="HardcodedText" />
|
tools:ignore="HardcodedText" />
|
||||||
|
</ImageView>
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/right_battery_widget"
|
android:id="@+id/right_battery_widget"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:textSize="24sp"
|
android:textSize="16sp"
|
||||||
android:textColor="@android:color/system_accent2_400"
|
android:textColor="@android:color/system_accent2_400"
|
||||||
android:layout_marginEnd="8dp"
|
android:gravity="center"
|
||||||
android:gravity="center_vertical"
|
|
||||||
android:text="Right"
|
android:text="Right"
|
||||||
tools:ignore="HardcodedText" />
|
tools:ignore="HardcodedText" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:gravity="center"
|
||||||
|
android:orientation="vertical">
|
||||||
<ImageView
|
<ImageView
|
||||||
android:layout_width="64dp"
|
android:layout_width="64dp"
|
||||||
android:layout_height="64dp"
|
android:layout_height="64dp"
|
||||||
|
android:src="@drawable/ring_background"
|
||||||
|
android:layout_gravity="center">
|
||||||
|
<ImageView
|
||||||
|
android:layout_width="32dp"
|
||||||
|
android:layout_height="32dp"
|
||||||
android:src="@drawable/airpods_pro_case_notification"
|
android:src="@drawable/airpods_pro_case_notification"
|
||||||
android:tint="@android:color/system_accent2_400"
|
android:tint="@android:color/system_accent2_400"
|
||||||
android:contentDescription="something"
|
android:layout_gravity="center"
|
||||||
android:layout_margin="0dp"
|
|
||||||
android:gravity="center_vertical"
|
|
||||||
tools:ignore="HardcodedText" />
|
tools:ignore="HardcodedText" />
|
||||||
|
</ImageView>
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/case_battery_widget"
|
android:id="@+id/case_battery_widget"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:textSize="24sp"
|
android:textSize="16sp"
|
||||||
android:textColor="@android:color/system_accent2_400"
|
android:textColor="@android:color/system_accent2_400"
|
||||||
android:layout_marginEnd="8dp"
|
android:gravity="center"
|
||||||
android:gravity="center_vertical"
|
|
||||||
android:text="Case"
|
android:text="Case"
|
||||||
tools:ignore="HardcodedText" />
|
tools:ignore="HardcodedText" />
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
</LinearLayout>
|
||||||
</RelativeLayout>
|
</RelativeLayout>
|
||||||
@@ -4,72 +4,125 @@
|
|||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:id="@+id/battery_widget"
|
android:id="@+id/battery_widget"
|
||||||
android:theme="@style/Theme.ALN.AppWidgetContainer">
|
android:theme="@style/Theme.ALN.AppWidgetContainer"
|
||||||
|
android:background="@drawable/blur_background">
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="100dp"
|
android:layout_height="wrap_content"
|
||||||
android:layout_margin="0dp"
|
android:layout_margin="0dp"
|
||||||
android:gravity="center">
|
android:gravity="center"
|
||||||
<ImageView
|
android:orientation="horizontal">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:gravity="center"
|
||||||
|
android:orientation="vertical">
|
||||||
|
<FrameLayout
|
||||||
android:layout_width="64dp"
|
android:layout_width="64dp"
|
||||||
android:layout_height="64dp"
|
android:layout_height="64dp"
|
||||||
android:contentDescription="something"
|
android:layout_gravity="center">
|
||||||
android:gravity="center_vertical"
|
<ProgressBar
|
||||||
android:layout_marginEnd="0dp"
|
android:id="@+id/left_battery_progress"
|
||||||
|
style="@style/Widget.AppCompat.ProgressBar.Horizontal"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:indeterminate="false"
|
||||||
|
android:max="100"
|
||||||
|
android:progressDrawable="@drawable/circular_progress_bar" />
|
||||||
|
<ImageView
|
||||||
|
android:layout_width="32dp"
|
||||||
|
android:layout_height="32dp"
|
||||||
android:src="@drawable/airpods_pro_left_notification"
|
android:src="@drawable/airpods_pro_left_notification"
|
||||||
android:tint="@color/popup_text"
|
android:tint="@color/popup_text"
|
||||||
|
android:layout_gravity="center"
|
||||||
tools:ignore="HardcodedText" />
|
tools:ignore="HardcodedText" />
|
||||||
|
</FrameLayout>
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/left_battery_widget"
|
android:id="@+id/left_battery_widget"
|
||||||
android:layout_width="0dp"
|
android:layout_width="wrap_content"
|
||||||
android:layout_weight="1"
|
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:textSize="24sp"
|
android:textSize="16sp"
|
||||||
android:textColor="@color/popup_text"
|
android:textColor="@color/popup_text"
|
||||||
android:layout_marginHorizontal="8dp"
|
android:gravity="center"
|
||||||
android:gravity="center_vertical"
|
|
||||||
android:text="Left"
|
android:text="Left"
|
||||||
tools:ignore="HardcodedText" />
|
tools:ignore="HardcodedText" />
|
||||||
<ImageView
|
</LinearLayout>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:gravity="center"
|
||||||
|
android:orientation="vertical">
|
||||||
|
<FrameLayout
|
||||||
android:layout_width="64dp"
|
android:layout_width="64dp"
|
||||||
android:layout_height="64dp"
|
android:layout_height="64dp"
|
||||||
|
android:layout_gravity="center">
|
||||||
|
<ProgressBar
|
||||||
|
android:id="@+id/right_battery_progress"
|
||||||
|
style="@style/Widget.AppCompat.ProgressBar.Horizontal"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:indeterminate="false"
|
||||||
|
android:max="100"
|
||||||
|
android:progressDrawable="@drawable/circular_progress_bar" />
|
||||||
|
<ImageView
|
||||||
|
android:layout_width="32dp"
|
||||||
|
android:layout_height="32dp"
|
||||||
android:src="@drawable/airpods_pro_right_notification"
|
android:src="@drawable/airpods_pro_right_notification"
|
||||||
android:tint="@color/popup_text"
|
android:tint="@color/popup_text"
|
||||||
android:layout_margin="0dp"
|
android:layout_gravity="center"
|
||||||
android:contentDescription="something"
|
|
||||||
android:gravity="center_vertical"
|
|
||||||
tools:ignore="HardcodedText" />
|
tools:ignore="HardcodedText" />
|
||||||
|
</FrameLayout>
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/right_battery_widget"
|
android:id="@+id/right_battery_widget"
|
||||||
android:layout_width="0dp"
|
android:layout_width="wrap_content"
|
||||||
android:layout_weight="1"
|
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:textSize="24sp"
|
android:textSize="16sp"
|
||||||
android:textColor="@color/popup_text"
|
android:textColor="@color/popup_text"
|
||||||
android:layout_marginHorizontal="8dp"
|
android:gravity="center"
|
||||||
android:gravity="center_vertical"
|
|
||||||
android:text="Right"
|
android:text="Right"
|
||||||
tools:ignore="HardcodedText" />
|
tools:ignore="HardcodedText" />
|
||||||
<ImageView
|
</LinearLayout>
|
||||||
android:layout_width="64dp"
|
|
||||||
android:layout_height="64dp"
|
<LinearLayout
|
||||||
android:src="@drawable/airpods_pro_case_notification"
|
|
||||||
android:tint="@color/popup_text"
|
|
||||||
android:contentDescription="something"
|
|
||||||
android:layout_margin="0dp"
|
|
||||||
android:gravity="center_vertical"
|
|
||||||
tools:ignore="HardcodedText" />
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/case_battery_widget"
|
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_weight="1"
|
android:layout_weight="1"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:textSize="24sp"
|
android:gravity="center"
|
||||||
|
android:orientation="vertical">
|
||||||
|
<FrameLayout
|
||||||
|
android:layout_width="64dp"
|
||||||
|
android:layout_height="64dp"
|
||||||
|
android:layout_gravity="center">
|
||||||
|
<ProgressBar
|
||||||
|
android:id="@+id/case_battery_progress"
|
||||||
|
style="@style/Widget.AppCompat.ProgressBar.Horizontal"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:indeterminate="false"
|
||||||
|
android:max="100"
|
||||||
|
android:progressDrawable="@drawable/circular_progress_bar" />
|
||||||
|
<ImageView
|
||||||
|
android:layout_width="32dp"
|
||||||
|
android:layout_height="32dp"
|
||||||
|
android:src="@drawable/airpods_pro_case_notification"
|
||||||
|
android:tint="@color/popup_text"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
tools:ignore="HardcodedText" />
|
||||||
|
</FrameLayout>
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/case_battery_widget"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textSize="16sp"
|
||||||
android:textColor="@color/popup_text"
|
android:textColor="@color/popup_text"
|
||||||
android:layout_marginHorizontal="8dp"
|
android:gravity="center"
|
||||||
android:gravity="center_vertical"
|
|
||||||
android:text="Case"
|
android:text="Case"
|
||||||
tools:ignore="HardcodedText" />
|
tools:ignore="HardcodedText" />
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
</LinearLayout>
|
||||||
</RelativeLayout>
|
</RelativeLayout>
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:description="@string/app_widget_description"
|
|
||||||
android:initialKeyguardLayout="@layout/battery_widget"
|
|
||||||
android:initialLayout="@layout/battery_widget"
|
|
||||||
android:minWidth="40dp"
|
|
||||||
android:minHeight="40dp"
|
|
||||||
android:previewImage="@drawable/example_appwidget_preview"
|
|
||||||
android:previewLayout="@layout/battery_widget"
|
|
||||||
android:resizeMode="horizontal|vertical"
|
|
||||||
android:targetCellWidth="1"
|
|
||||||
android:targetCellHeight="1"
|
|
||||||
android:updatePeriodMillis="86400000"
|
|
||||||
android:widgetCategory="home_screen|keyguard" />
|
|
||||||
@@ -10,5 +10,5 @@
|
|||||||
android:resizeMode="horizontal|vertical"
|
android:resizeMode="horizontal|vertical"
|
||||||
android:targetCellWidth="1"
|
android:targetCellWidth="1"
|
||||||
android:targetCellHeight="1"
|
android:targetCellHeight="1"
|
||||||
android:updatePeriodMillis="86400000"
|
android:updatePeriodMillis="300000"
|
||||||
android:widgetCategory="home_screen|keyguard" />
|
android:widgetCategory="home_screen|keyguard" />
|
||||||
53
linux/AirPodsTrayApp.h
Normal file
53
linux/AirPodsTrayApp.h
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QObject>
|
||||||
|
#include <QSystemTrayIcon>
|
||||||
|
#include <QMenu>
|
||||||
|
#include <QBluetoothDeviceDiscoveryAgent>
|
||||||
|
#include <QBluetoothSocket>
|
||||||
|
#include <QDBusInterface>
|
||||||
|
#include "BluetoothHandler.h"
|
||||||
|
|
||||||
|
class AirPodsTrayApp : public QObject {
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
AirPodsTrayApp();
|
||||||
|
|
||||||
|
public slots:
|
||||||
|
void connectToDevice(const QString &address);
|
||||||
|
void showAvailableDevices();
|
||||||
|
void setNoiseControlMode(int mode);
|
||||||
|
void setConversationalAwareness(bool enabled);
|
||||||
|
void updateNoiseControlMenu(int mode);
|
||||||
|
void updateBatteryTooltip(const QString &status);
|
||||||
|
void updateTrayIcon(const QString &status);
|
||||||
|
void handleEarDetection(const QString &status);
|
||||||
|
|
||||||
|
private slots:
|
||||||
|
void onTrayIconActivated(QSystemTrayIcon::ActivationReason reason);
|
||||||
|
void onDeviceDiscovered(const QBluetoothDeviceInfo &device);
|
||||||
|
void onDiscoveryFinished();
|
||||||
|
void onDeviceConnected(const QBluetoothAddress &address);
|
||||||
|
void onDeviceDisconnected(const QBluetoothAddress &address);
|
||||||
|
void onPhoneDataReceived();
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void noiseControlModeChanged(int mode);
|
||||||
|
void earDetectionStatusChanged(const QString &status);
|
||||||
|
void batteryStatusChanged(const QString &status);
|
||||||
|
|
||||||
|
private:
|
||||||
|
void initializeMprisInterface();
|
||||||
|
void connectToPhone();
|
||||||
|
void relayPacketToPhone(const QByteArray &packet);
|
||||||
|
void handlePhonePacket(const QByteArray &packet);
|
||||||
|
|
||||||
|
QSystemTrayIcon *trayIcon;
|
||||||
|
QMenu *trayMenu;
|
||||||
|
QBluetoothDeviceDiscoveryAgent *discoveryAgent;
|
||||||
|
QBluetoothSocket *socket = nullptr;
|
||||||
|
QBluetoothSocket *phoneSocket = nullptr;
|
||||||
|
QDBusInterface *mprisInterface;
|
||||||
|
QString connectedDeviceMacAddress;
|
||||||
|
};
|
||||||
109
linux/BluetoothHandler.cpp
Normal file
109
linux/BluetoothHandler.cpp
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
#include "BluetoothHandler.h"
|
||||||
|
#include "PacketDefinitions.h"
|
||||||
|
#include <QLoggingCategory>
|
||||||
|
|
||||||
|
Q_LOGGING_CATEGORY(bluetoothHandler, "bluetoothHandler")
|
||||||
|
|
||||||
|
#define LOG_INFO(msg) qCInfo(bluetoothHandler) << "\033[32m" << msg << "\033[0m"
|
||||||
|
#define LOG_WARN(msg) qCWarning(bluetoothHandler) << "\033[33m" << msg << "\033[0m"
|
||||||
|
#define LOG_ERROR(msg) qCCritical(bluetoothHandler) << "\033[31m" << msg << "\033[0m"
|
||||||
|
#define LOG_DEBUG(msg) qCDebug(bluetoothHandler) << "\033[34m" << msg << "\033[0m"
|
||||||
|
|
||||||
|
BluetoothHandler::BluetoothHandler() {
|
||||||
|
discoveryAgent = new QBluetoothDeviceDiscoveryAgent();
|
||||||
|
discoveryAgent->setLowEnergyDiscoveryTimeout(5000);
|
||||||
|
|
||||||
|
connect(discoveryAgent, &QBluetoothDeviceDiscoveryAgent::deviceDiscovered, this, &BluetoothHandler::onDeviceDiscovered);
|
||||||
|
connect(discoveryAgent, &QBluetoothDeviceDiscoveryAgent::finished, this, &BluetoothHandler::onDiscoveryFinished);
|
||||||
|
discoveryAgent->start();
|
||||||
|
LOG_INFO("BluetoothHandler initialized and started device discovery");
|
||||||
|
}
|
||||||
|
|
||||||
|
void BluetoothHandler::connectToDevice(const QBluetoothDeviceInfo &device) {
|
||||||
|
if (socket && socket->isOpen() && socket->peerAddress() == device.address()) {
|
||||||
|
LOG_INFO("Already connected to the device: " << device.name());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG_INFO("Connecting to device: " << device.name());
|
||||||
|
QBluetoothSocket *localSocket = new QBluetoothSocket(QBluetoothServiceInfo::L2capProtocol);
|
||||||
|
connect(localSocket, &QBluetoothSocket::connected, this, [this, localSocket]() {
|
||||||
|
LOG_INFO("Connected to device, sending initial packets");
|
||||||
|
discoveryAgent->stop();
|
||||||
|
|
||||||
|
QByteArray handshakePacket = QByteArray::fromHex("00000400010002000000000000000000");
|
||||||
|
QByteArray setSpecificFeaturesPacket = QByteArray::fromHex("040004004d00ff00000000000000");
|
||||||
|
QByteArray requestNotificationsPacket = QByteArray::fromHex("040004000f00ffffffffff");
|
||||||
|
|
||||||
|
qint64 bytesWritten = localSocket->write(handshakePacket);
|
||||||
|
LOG_DEBUG("Handshake packet written: " << handshakePacket.toHex() << ", bytes written: " << bytesWritten);
|
||||||
|
|
||||||
|
QByteArray airpodsConnectedPacket = QByteArray::fromHex("000400010001");
|
||||||
|
phoneSocket->write(airpodsConnectedPacket);
|
||||||
|
LOG_DEBUG("AIRPODS_CONNECTED packet written: " << airpodsConnectedPacket.toHex());
|
||||||
|
|
||||||
|
connect(localSocket, &QBluetoothSocket::bytesWritten, this, [this, localSocket, setSpecificFeaturesPacket, requestNotificationsPacket](qint64 bytes) {
|
||||||
|
LOG_INFO("Bytes written: " << bytes);
|
||||||
|
if (bytes > 0) {
|
||||||
|
static int step = 0;
|
||||||
|
switch (step) {
|
||||||
|
case 0:
|
||||||
|
localSocket->write(setSpecificFeaturesPacket);
|
||||||
|
LOG_DEBUG("Set specific features packet written: " << setSpecificFeaturesPacket.toHex());
|
||||||
|
step++;
|
||||||
|
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(":", "_");
|
||||||
|
}
|
||||||
|
|
||||||
|
void BluetoothHandler::parseData(const QByteArray &data) {
|
||||||
|
LOG_DEBUG("Parsing data: " << data.toHex() << "Size: " << data.size());
|
||||||
|
if (data.size() == 11 && data.startsWith(QByteArray::fromHex("0400040009000D"))) {
|
||||||
|
int mode = data[7] - 1;
|
||||||
|
LOG_INFO("Noise control mode: " << mode);
|
||||||
|
if (mode >= 0 && mode <= 3) {
|
||||||
|
emit noiseControlModeChanged(mode);
|
||||||
|
} else {
|
||||||
|
LOG_ERROR("Invalid noise control mode value received: " << mode);
|
||||||
|
}
|
||||||
|
} else if (data.size() == 8 && data.startsWith(QByteArray::fromHex("040004000600"))) {
|
||||||
|
bool primaryInEar = data[6] == 0x00;
|
||||||
|
bool secondaryInEar = data[7] == 0x00;
|
||||||
|
QString earDetectionStatus = QString("Primary: %1, Secondary: %2")
|
||||||
|
.arg(primaryInEar ? "In Ear" : "Out of Ear")
|
||||||
|
.arg(secondaryInEar ? "In Ear" : "Out of Ear");
|
||||||
|
LOG_INFO("Ear detection status: " << earDetectionStatus);
|
||||||
|
emit earDetectionStatusChanged(earDetectionStatus);
|
||||||
|
} else if (data.size() == 22 && data.startsWith(QByteArray::fromHex("040004000400"))) {
|
||||||
|
int leftLevel = data[9];
|
||||||
|
int rightLevel = data[14];
|
||||||
|
int caseLevel = data[19];
|
||||||
|
QString batteryStatus = QString("Left: %1%, Right: %2%, Case: %3%")
|
||||||
|
.arg(leftLevel)
|
||||||
|
.arg(rightLevel)
|
||||||
|
.arg(caseLevel);
|
||||||
|
LOG_INFO("Battery status: " << batteryStatus);
|
||||||
|
emit batteryStatusChanged(batteryStatus);
|
||||||
|
} else if (data.size() == 10 &&
|
||||||
23
linux/BluetoothHandler.h
Normal file
23
linux/BluetoothHandler.h
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QBluetoothDeviceInfo>
|
||||||
|
#include <QBluetoothSocket>
|
||||||
|
#include <QBluetoothDeviceDiscoveryAgent>
|
||||||
|
|
||||||
|
class BluetoothHandler : public QObject {
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
BluetoothHandler();
|
||||||
|
void connectToDevice(const QBluetoothDeviceInfo &device);
|
||||||
|
void parseData(const QByteArray &data);
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void noiseControlModeChanged(int mode);
|
||||||
|
void earDetectionStatusChanged(const QString &status);
|
||||||
|
void batteryStatusChanged(const QString &status);
|
||||||
|
|
||||||
|
private:
|
||||||
|
QBluetoothSocket *socket = nullptr;
|
||||||
|
QBluetoothDeviceDiscoveryAgent *discoveryAgent;
|
||||||
|
};
|
||||||
@@ -10,6 +10,9 @@ qt_standard_project_setup(REQUIRES 6.5)
|
|||||||
|
|
||||||
qt_add_executable(applinux
|
qt_add_executable(applinux
|
||||||
main.cpp
|
main.cpp
|
||||||
|
AirPodsTrayApp.cpp
|
||||||
|
BluetoothHandler.cpp
|
||||||
|
PacketDefinitions.cpp
|
||||||
)
|
)
|
||||||
|
|
||||||
qt_add_qml_module(applinux
|
qt_add_qml_module(applinux
|
||||||
|
|||||||
Reference in New Issue
Block a user