mirror of
https://github.com/kavishdevar/librepods.git
synced 2026-02-01 07:39:11 +00:00
Compare commits
24 Commits
v0.1.0-rc.
...
v0.1.0-rc.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
69439257ce | ||
|
|
810a3c90e4 | ||
|
|
0611509782 | ||
|
|
116f7dda92 | ||
|
|
51ca4c12d1 | ||
|
|
8e670c2481 | ||
|
|
aec9c7192e | ||
|
|
01432ce9c7 | ||
|
|
9baa3c9b60 | ||
|
|
364a6f4b64 | ||
|
|
9b96218fa9 | ||
|
|
98aef13395 | ||
|
|
42e0f48b8b | ||
|
|
4c73200f35 | ||
|
|
06de276dca | ||
|
|
7ffcd68ad9 | ||
|
|
295c49fdc6 | ||
|
|
b95962d722 | ||
|
|
45ed8a3a88 | ||
|
|
d381adaa09 | ||
|
|
58dfed97b3 | ||
|
|
48e2899564 | ||
|
|
7f7b439746 | ||
|
|
0b4030dd9f |
@@ -13,8 +13,8 @@ android {
|
||||
applicationId = "me.kavishdevar.librepods"
|
||||
minSdk = 28
|
||||
targetSdk = 35
|
||||
versionCode = 4
|
||||
versionName = "0.1.0"
|
||||
versionCode = 6
|
||||
versionName = "0.1.0-rc.3"
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
|
||||
@@ -1,24 +1,26 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:sharedUserId="android.uid.system"
|
||||
android:sharedUserMaxSdkVersion="32"
|
||||
tools:targetApi="33">
|
||||
|
||||
<uses-feature
|
||||
android:name="android.hardware.telephony"
|
||||
android:required="false" />
|
||||
|
||||
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_CONNECTED_DEVICE" />
|
||||
<uses-permission android:name="android.permission.INTERACT_ACROSS_USERS"
|
||||
tools:ignore="ProtectedPermissions" />
|
||||
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
|
||||
<uses-permission
|
||||
android:name="android.permission.BLUETOOTH_PRIVILEGED"
|
||||
tools:ignore="ProtectedPermissions" />
|
||||
<uses-permission
|
||||
android:name="android.permission.BATTERY_STATS"
|
||||
tools:ignore="ProtectedPermissions" />
|
||||
<uses-permission
|
||||
android:name="android.permission.UPDATE_DEVICE_STATS"
|
||||
tools:ignore="ProtectedPermissions" />
|
||||
<uses-permission android:name="android.permission.BLUETOOTH" />
|
||||
<uses-permission
|
||||
android:name="android.permission.BLUETOOTH_SCAN"
|
||||
@@ -29,6 +31,9 @@
|
||||
<uses-permission android:name="android.permission.ANSWER_PHONE_CALLS" />
|
||||
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||
|
||||
<protected-broadcast android:name="batterywidget.impl.action.update_bluetooth_data" />
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
@@ -40,6 +45,7 @@
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.LibrePods"
|
||||
android:description="@string/app_description"
|
||||
tools:ignore="UnusedAttribute"
|
||||
tools:targetApi="31">
|
||||
<receiver
|
||||
@@ -121,6 +127,16 @@
|
||||
<action android:name="android.intent.action.MY_PACKAGE_REPLACED" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<provider
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
android:authorities="${applicationId}.provider"
|
||||
android:exported="false"
|
||||
android:grantUriPermissions="true">
|
||||
<meta-data
|
||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||
android:resource="@xml/file_paths" />
|
||||
</provider>
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
|
||||
@@ -32,9 +32,7 @@
|
||||
static HookFunType hook_func = nullptr;
|
||||
#define L2CEVT_L2CAP_CONFIG_REQ 4
|
||||
#define L2CEVT_L2CAP_CONFIG_RSP 15
|
||||
// Define all necessary structures for the L2CAP stack
|
||||
|
||||
// Forward declarations for types needed by the new hook
|
||||
struct t_l2c_lcb;
|
||||
typedef struct _BT_HDR {
|
||||
uint16_t event;
|
||||
@@ -44,7 +42,6 @@ typedef struct _BT_HDR {
|
||||
uint8_t data[];
|
||||
} BT_HDR;
|
||||
|
||||
// Define base FCR structures
|
||||
typedef struct {
|
||||
uint8_t mode;
|
||||
uint8_t tx_win_sz;
|
||||
@@ -130,17 +127,7 @@ static void (*original_l2c_csm_config)(tL2C_CCB* p_ccb, uint8_t event, void* p_d
|
||||
static void (*original_l2cu_send_peer_info_req)(tL2C_LCB* p_lcb, uint16_t info_type) = nullptr;
|
||||
|
||||
uint8_t fake_l2c_fcr_chk_chan_modes(void* p_ccb) {
|
||||
LOGI("l2c_fcr_chk_chan_modes hooked");
|
||||
auto* ccb = static_cast<tL2C_CCB*>(p_ccb);
|
||||
LOGI("Original FCR mode: 0x%02x", ccb->our_cfg.fcr.mode);
|
||||
|
||||
ccb->our_cfg.fcr.mode = 0;
|
||||
ccb->our_cfg.fcr_present = true;
|
||||
ccb->peer_cfg.fcr.mode = 0;
|
||||
ccb->peer_cfg.fcr_present = true;
|
||||
|
||||
LOGI("FCR mode set to Basic Mode (0) for both local and peer config, here's the new desired FCR mode: 0x%02x, and the peer's FCR mode: 0x%02x", ccb->our_cfg.fcr.mode, ccb->peer_cfg.fcr.mode);
|
||||
|
||||
LOGI("l2c_fcr_chk_chan_modes hooked, returning true.");
|
||||
return 1;
|
||||
}
|
||||
|
||||
|
||||
@@ -107,6 +107,7 @@ import me.kavishdevar.librepods.screens.HeadTrackingScreen
|
||||
import me.kavishdevar.librepods.screens.LongPress
|
||||
import me.kavishdevar.librepods.screens.Onboarding
|
||||
import me.kavishdevar.librepods.screens.RenameScreen
|
||||
import me.kavishdevar.librepods.screens.TroubleshootingScreen
|
||||
import me.kavishdevar.librepods.services.AirPodsService
|
||||
import me.kavishdevar.librepods.ui.theme.LibrePodsTheme
|
||||
import me.kavishdevar.librepods.utils.AirPodsNotifications
|
||||
@@ -186,6 +187,8 @@ fun Main() {
|
||||
permissions = listOf(
|
||||
"android.permission.BLUETOOTH_CONNECT",
|
||||
"android.permission.BLUETOOTH_SCAN",
|
||||
"android.permission.BLUETOOTH",
|
||||
"android.permission.BLUETOOTH_ADMIN",
|
||||
"android.permission.BLUETOOTH_ADVERTISE",
|
||||
"android.permission.POST_NOTIFICATIONS",
|
||||
"android.permission.READ_PHONE_STATE",
|
||||
@@ -275,6 +278,9 @@ fun Main() {
|
||||
composable("app_settings") {
|
||||
AppSettingsScreen(navController)
|
||||
}
|
||||
composable("troubleshooting") {
|
||||
TroubleshootingScreen(navController)
|
||||
}
|
||||
composable("head_tracking") {
|
||||
HeadTrackingScreen(navController)
|
||||
}
|
||||
@@ -405,7 +411,7 @@ fun PermissionsScreen(
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
Text(
|
||||
text = "To provide the best AirPods experience, we need a few permissions",
|
||||
text = "The following permissions are required to use the app. Please grant them to continue.",
|
||||
style = TextStyle(
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.Normal,
|
||||
@@ -517,16 +523,16 @@ fun PermissionsScreen(
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
if (!canDrawOverlays && basicPermissionsGranted) {
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
|
||||
|
||||
Button(
|
||||
onClick = {
|
||||
val editor = context.getSharedPreferences("settings", MODE_PRIVATE).edit()
|
||||
editor.putBoolean("overlay_permission_skipped", true)
|
||||
editor.apply()
|
||||
|
||||
|
||||
val intent = Intent(context, MainActivity::class.java)
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
|
||||
context.startActivity(intent)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/*
|
||||
* LibrePods - AirPods liberated from Apple’s ecosystem
|
||||
*
|
||||
* Copyright (C) 2025 Kavish Devar
|
||||
* Copyright (C) 2025 LibrePods Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/*
|
||||
* LibrePods - AirPods liberated from Apple’s ecosystem
|
||||
*
|
||||
* Copyright (C) 2025 Kavish Devar
|
||||
* Copyright (C) 2025 LibrePods Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/*
|
||||
* LibrePods - AirPods liberated from Apple’s ecosystem
|
||||
*
|
||||
* Copyright (C) 2025 Kavish Devar
|
||||
* Copyright (C) 2025 LibrePods Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
|
||||
@@ -36,11 +36,15 @@ import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Info
|
||||
import androidx.compose.material.icons.filled.Settings
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.CenterAlignedTopAppBar
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
@@ -78,9 +82,11 @@ import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import dev.chrisbanes.haze.HazeEffectScope
|
||||
import dev.chrisbanes.haze.HazeState
|
||||
import dev.chrisbanes.haze.haze
|
||||
import dev.chrisbanes.haze.hazeChild
|
||||
import dev.chrisbanes.haze.hazeEffect
|
||||
import dev.chrisbanes.haze.hazeSource
|
||||
import dev.chrisbanes.haze.materials.CupertinoMaterials
|
||||
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -102,6 +108,7 @@ import me.kavishdevar.librepods.utils.AirPodsNotifications
|
||||
@Composable
|
||||
fun AirPodsSettingsScreen(dev: BluetoothDevice?, service: AirPodsService,
|
||||
navController: NavController, isConnected: Boolean, isRemotelyConnected: Boolean) {
|
||||
var isLocallyConnected by remember { mutableStateOf(isConnected) }
|
||||
var isRemotelyConnected by remember { mutableStateOf(isRemotelyConnected) }
|
||||
val sharedPreferences = LocalContext.current.getSharedPreferences("settings", MODE_PRIVATE)
|
||||
var device by remember { mutableStateOf(dev) }
|
||||
@@ -113,6 +120,10 @@ fun AirPodsSettingsScreen(dev: BluetoothDevice?, service: AirPodsService,
|
||||
)
|
||||
}
|
||||
|
||||
LaunchedEffect(service) {
|
||||
isLocallyConnected = service.isConnectedLocally
|
||||
}
|
||||
|
||||
val nameChangeListener = remember {
|
||||
SharedPreferences.OnSharedPreferenceChangeListener { _, key ->
|
||||
if (key == "name") {
|
||||
@@ -144,22 +155,37 @@ fun AirPodsSettingsScreen(dev: BluetoothDevice?, service: AirPodsService,
|
||||
}
|
||||
|
||||
val context = LocalContext.current
|
||||
val bluetoothReceiver = remember {
|
||||
|
||||
val connectionReceiver = remember {
|
||||
object : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context?, intent: Intent?) {
|
||||
if (intent?.action == "me.kavishdevar.librepods.AIRPODS_CONNECTED_REMOTELY") {
|
||||
coroutineScope.launch {
|
||||
handleRemoteConnection(true)
|
||||
when (intent?.action) {
|
||||
"me.kavishdevar.librepods.AIRPODS_CONNECTED_REMOTELY" -> {
|
||||
coroutineScope.launch {
|
||||
handleRemoteConnection(true)
|
||||
}
|
||||
}
|
||||
} else if (intent?.action == "me.kavishdevar.librepods.AIRPODS_DISCONNECTED_REMOTELY") {
|
||||
coroutineScope.launch {
|
||||
handleRemoteConnection(false)
|
||||
"me.kavishdevar.librepods.AIRPODS_DISCONNECTED_REMOTELY" -> {
|
||||
coroutineScope.launch {
|
||||
handleRemoteConnection(false)
|
||||
}
|
||||
}
|
||||
} else if (intent?.action == AirPodsNotifications.DISCONNECT_RECEIVERS) {
|
||||
try {
|
||||
context?.unregisterReceiver(this)
|
||||
} catch (e: IllegalArgumentException) {
|
||||
e.printStackTrace()
|
||||
AirPodsNotifications.AIRPODS_CONNECTED -> {
|
||||
coroutineScope.launch {
|
||||
isLocallyConnected = true
|
||||
}
|
||||
}
|
||||
AirPodsNotifications.AIRPODS_DISCONNECTED -> {
|
||||
coroutineScope.launch {
|
||||
isLocallyConnected = false
|
||||
}
|
||||
}
|
||||
AirPodsNotifications.DISCONNECT_RECEIVERS -> {
|
||||
try {
|
||||
context?.unregisterReceiver(this)
|
||||
} catch (e: IllegalArgumentException) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -170,16 +196,22 @@ fun AirPodsSettingsScreen(dev: BluetoothDevice?, service: AirPodsService,
|
||||
val filter = IntentFilter().apply {
|
||||
addAction("me.kavishdevar.librepods.AIRPODS_CONNECTED_REMOTELY")
|
||||
addAction("me.kavishdevar.librepods.AIRPODS_DISCONNECTED_REMOTELY")
|
||||
addAction(AirPodsNotifications.AIRPODS_CONNECTED)
|
||||
addAction(AirPodsNotifications.AIRPODS_DISCONNECTED)
|
||||
addAction(AirPodsNotifications.DISCONNECT_RECEIVERS)
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
context.registerReceiver(bluetoothReceiver, filter, RECEIVER_EXPORTED)
|
||||
context.registerReceiver(connectionReceiver, filter, RECEIVER_EXPORTED)
|
||||
} else {
|
||||
context.registerReceiver(bluetoothReceiver, filter)
|
||||
context.registerReceiver(connectionReceiver, filter)
|
||||
}
|
||||
onDispose {
|
||||
context.unregisterReceiver(bluetoothReceiver)
|
||||
try {
|
||||
context.unregisterReceiver(connectionReceiver)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -206,14 +238,13 @@ fun AirPodsSettingsScreen(dev: BluetoothDevice?, service: AirPodsService,
|
||||
)
|
||||
},
|
||||
modifier = Modifier
|
||||
.hazeChild(
|
||||
.hazeEffect(
|
||||
state = hazeState,
|
||||
style = CupertinoMaterials.thick(),
|
||||
block = {
|
||||
block = fun HazeEffectScope.() {
|
||||
alpha =
|
||||
if (verticalScrollState.value > 60.dp.value * mDensity.floatValue) 1f else 0f
|
||||
}
|
||||
)
|
||||
})
|
||||
.drawBehind {
|
||||
mDensity.floatValue = density
|
||||
val strokeWidth = 0.7.dp.value * density
|
||||
@@ -266,10 +297,10 @@ fun AirPodsSettingsScreen(dev: BluetoothDevice?, service: AirPodsService,
|
||||
},
|
||||
snackbarHost = { SnackbarHost(snackbarHostState) }
|
||||
) { paddingValues ->
|
||||
if (isConnected == true || isRemotelyConnected == true) {
|
||||
if (isLocallyConnected || isRemotelyConnected) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.haze(hazeState)
|
||||
.hazeSource(hazeState)
|
||||
.fillMaxSize()
|
||||
.padding(horizontal = 16.dp)
|
||||
.verticalScroll(
|
||||
@@ -387,6 +418,24 @@ fun AirPodsSettingsScreen(dev: BluetoothDevice?, service: AirPodsService,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
Spacer(Modifier.height(32.dp))
|
||||
Button(
|
||||
onClick = { navController.navigate("troubleshooting") },
|
||||
shape = RoundedCornerShape(10.dp),
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = if (isSystemInDarkTheme()) Color(0xFF1C1C1E) else Color(0xFFF2F2F7),
|
||||
contentColor = if (isSystemInDarkTheme()) Color.White else Color.Black,
|
||||
)
|
||||
) {
|
||||
Text(
|
||||
text = "Troubleshoot Connection",
|
||||
style = TextStyle(
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.Medium,
|
||||
fontFamily = FontFamily(Font(R.font.sf_pro))
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,8 +35,10 @@ import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.KeyboardArrowLeft
|
||||
import androidx.compose.material.icons.filled.Refresh
|
||||
@@ -62,38 +64,86 @@ import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.drawBehind
|
||||
import androidx.compose.ui.draw.scale
|
||||
import androidx.compose.ui.draw.shadow
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.Font
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.navigation.NavController
|
||||
import dev.chrisbanes.haze.HazeEffectScope
|
||||
import dev.chrisbanes.haze.HazeState
|
||||
import dev.chrisbanes.haze.hazeEffect
|
||||
import dev.chrisbanes.haze.hazeSource
|
||||
import dev.chrisbanes.haze.materials.CupertinoMaterials
|
||||
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
|
||||
import me.kavishdevar.librepods.R
|
||||
import me.kavishdevar.librepods.composables.IndependentToggle
|
||||
import me.kavishdevar.librepods.composables.StyledSwitch
|
||||
import me.kavishdevar.librepods.services.ServiceManager
|
||||
import me.kavishdevar.librepods.utils.RadareOffsetFinder
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalHazeMaterialsApi::class)
|
||||
@Composable
|
||||
fun AppSettingsScreen(navController: NavController) {
|
||||
val sharedPreferences = LocalContext.current.getSharedPreferences("settings", Context.MODE_PRIVATE)
|
||||
val name = remember { mutableStateOf(sharedPreferences.getString("name", "") ?: "") }
|
||||
val isDarkTheme = isSystemInDarkTheme()
|
||||
val context = LocalContext.current
|
||||
val scrollState = rememberScrollState()
|
||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
|
||||
val hazeState = remember { HazeState() }
|
||||
|
||||
var showResetDialog by remember { mutableStateOf(false) }
|
||||
|
||||
var showPhoneBatteryInWidget by remember {
|
||||
mutableStateOf(sharedPreferences.getBoolean("show_phone_battery_in_widget", true))
|
||||
}
|
||||
var conversationalAwarenessPauseMusicEnabled by remember {
|
||||
mutableStateOf(sharedPreferences.getBoolean("conversational_awareness_pause_music", false))
|
||||
}
|
||||
var relativeConversationalAwarenessVolumeEnabled by remember {
|
||||
mutableStateOf(sharedPreferences.getBoolean("relative_conversational_awareness_volume", true))
|
||||
}
|
||||
var openDialogForControlling by remember {
|
||||
mutableStateOf(sharedPreferences.getString("qs_click_behavior", "dialog") == "dialog")
|
||||
}
|
||||
var disconnectWhenNotWearing by remember {
|
||||
mutableStateOf(sharedPreferences.getBoolean("disconnect_when_not_wearing", false))
|
||||
}
|
||||
var mDensity by remember { mutableFloatStateOf(0f) }
|
||||
Scaffold(
|
||||
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||
topBar = {
|
||||
CenterAlignedTopAppBar(
|
||||
modifier = Modifier.hazeEffect(
|
||||
state = hazeState,
|
||||
style = CupertinoMaterials.thick(),
|
||||
block = fun HazeEffectScope.() {
|
||||
alpha =
|
||||
if (scrollState.value > 60.dp.value * mDensity) 1f else 0f
|
||||
})
|
||||
.drawBehind {
|
||||
mDensity = density
|
||||
val strokeWidth = 0.7.dp.value * density
|
||||
val y = size.height - strokeWidth / 2
|
||||
if (scrollState.value > 60.dp.value * density) {
|
||||
drawLine(
|
||||
if (isDarkTheme) Color.DarkGray else Color.LightGray,
|
||||
Offset(0f, y),
|
||||
Offset(size.width, y),
|
||||
strokeWidth
|
||||
)
|
||||
}
|
||||
},
|
||||
title = {
|
||||
Text(
|
||||
text = stringResource(R.string.app_settings),
|
||||
@@ -106,6 +156,7 @@ fun AppSettingsScreen(navController: NavController) {
|
||||
navController.popBackStack()
|
||||
},
|
||||
shape = RoundedCornerShape(8.dp),
|
||||
modifier = Modifier.width(180.dp)
|
||||
) {
|
||||
Icon(
|
||||
Icons.AutoMirrored.Filled.KeyboardArrowLeft,
|
||||
@@ -121,12 +172,16 @@ fun AppSettingsScreen(navController: NavController) {
|
||||
color = if (isDarkTheme) Color(0xFF007AFF) else Color(0xFF3C6DF5),
|
||||
fontFamily = FontFamily(Font(R.font.sf_pro))
|
||||
),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
}
|
||||
},
|
||||
colors = TopAppBarDefaults.topAppBarColors(
|
||||
colors = TopAppBarDefaults.centerAlignedTopAppBarColors(
|
||||
containerColor = Color.Transparent
|
||||
)
|
||||
),
|
||||
scrollBehavior = scrollBehavior
|
||||
)
|
||||
},
|
||||
containerColor = if (isSystemInDarkTheme()) Color(0xFF000000)
|
||||
@@ -136,68 +191,114 @@ fun AppSettingsScreen(navController: NavController) {
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues)
|
||||
.padding(horizontal = 12.dp)
|
||||
.padding(horizontal = 16.dp)
|
||||
.verticalScroll(scrollState)
|
||||
.hazeSource(state = hazeState)
|
||||
) {
|
||||
val isDarkTheme = isSystemInDarkTheme()
|
||||
|
||||
val backgroundColor = if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFFFFFFF)
|
||||
val textColor = if (isDarkTheme) Color.White else Color.Black
|
||||
val accentColor = if (isDarkTheme) Color(0xFF007AFF) else Color(0xFF3C6DF5)
|
||||
|
||||
IndependentToggle("Show phone battery in widget", ServiceManager.getService()!!, "setPhoneBatteryInWidget", sharedPreferences)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
Text(
|
||||
text = "Widget".uppercase(),
|
||||
style = TextStyle(
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.Light,
|
||||
color = textColor.copy(alpha = 0.6f),
|
||||
fontFamily = FontFamily(Font(R.font.sf_pro))
|
||||
),
|
||||
modifier = Modifier.padding(8.dp, bottom = 2.dp, top = 8.dp)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(2.dp))
|
||||
|
||||
Column (
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(275.sp.value.dp)
|
||||
.background(
|
||||
backgroundColor,
|
||||
RoundedCornerShape(14.dp)
|
||||
)
|
||||
.padding(horizontal = 16.dp, vertical = 8.dp)
|
||||
.padding(horizontal = 16.dp, vertical = 4.dp)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable(
|
||||
indication = null,
|
||||
interactionSource = remember { MutableInteractionSource() }
|
||||
) {
|
||||
showPhoneBatteryInWidget = !showPhoneBatteryInWidget
|
||||
sharedPreferences.edit().putBoolean("show_phone_battery_in_widget", showPhoneBatteryInWidget).apply()
|
||||
},
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(vertical = 8.dp)
|
||||
.padding(end = 4.dp)
|
||||
) {
|
||||
Text(
|
||||
text = "Show phone battery in widget",
|
||||
fontSize = 16.sp,
|
||||
color = textColor
|
||||
)
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Text(
|
||||
text = "Display your phone's battery level in the widget alongside AirPods battery",
|
||||
fontSize = 14.sp,
|
||||
color = textColor.copy(0.6f),
|
||||
lineHeight = 16.sp,
|
||||
)
|
||||
}
|
||||
|
||||
StyledSwitch(
|
||||
checked = showPhoneBatteryInWidget,
|
||||
onCheckedChange = {
|
||||
showPhoneBatteryInWidget = it
|
||||
sharedPreferences.edit().putBoolean("show_phone_battery_in_widget", it).apply()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Text(
|
||||
text = "Conversational Awareness".uppercase(),
|
||||
style = TextStyle(
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.Light,
|
||||
color = textColor.copy(alpha = 0.6f),
|
||||
fontFamily = FontFamily(Font(R.font.sf_pro))
|
||||
),
|
||||
modifier = Modifier.padding(8.dp, bottom = 2.dp, top = 24.dp)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(2.dp))
|
||||
|
||||
Column (
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(
|
||||
backgroundColor,
|
||||
RoundedCornerShape(14.dp)
|
||||
)
|
||||
.padding(horizontal = 16.dp, vertical = 4.dp)
|
||||
) {
|
||||
val sliderValue = remember { mutableFloatStateOf(0f) }
|
||||
LaunchedEffect(sliderValue) {
|
||||
if (sharedPreferences.contains("conversational_awareness_volume")) {
|
||||
sliderValue.floatValue = sharedPreferences.getInt("conversational_awareness_volume", 0).toFloat()
|
||||
sliderValue.floatValue = sharedPreferences.getInt("conversational_awareness_volume", 43).toFloat()
|
||||
}
|
||||
}
|
||||
LaunchedEffect(sliderValue.floatValue) {
|
||||
sharedPreferences.edit().putInt("conversational_awareness_volume", sliderValue.floatValue.toInt()).apply()
|
||||
}
|
||||
|
||||
val trackColor = if (isDarkTheme) Color(0xFFB3B3B3) else Color(0xFFD9D9D9)
|
||||
val thumbColor = if (isDarkTheme) Color(0xFFFFFFFF) else Color(0xFFFFFFFF)
|
||||
val labelTextColor = if (isDarkTheme) Color.White else Color.Black
|
||||
|
||||
Text(
|
||||
text = stringResource(R.string.conversational_awareness_customization),
|
||||
style = TextStyle(
|
||||
fontSize = 20.sp,
|
||||
color = textColor
|
||||
),
|
||||
modifier = Modifier
|
||||
.padding(top = 12.dp, bottom = 4.dp)
|
||||
)
|
||||
|
||||
|
||||
var conversationalAwarenessPauseMusicEnabled by remember {
|
||||
mutableStateOf(
|
||||
sharedPreferences.getBoolean("conversational_awareness_pause_music", true)
|
||||
)
|
||||
}
|
||||
|
||||
fun updateConversationalAwarenessPauseMusic(enabled: Boolean) {
|
||||
conversationalAwarenessPauseMusicEnabled = enabled
|
||||
sharedPreferences.edit().putBoolean("conversational_awareness_pause_music", enabled).apply()
|
||||
}
|
||||
|
||||
var relativeConversationalAwarenessVolumeEnabled by remember {
|
||||
mutableStateOf(
|
||||
sharedPreferences.getBoolean("relative_conversational_awareness_volume", true)
|
||||
)
|
||||
}
|
||||
|
||||
fun updateRelativeConversationalAwarenessVolume(enabled: Boolean) {
|
||||
relativeConversationalAwarenessVolumeEnabled = enabled
|
||||
sharedPreferences.edit().putBoolean("relative_conversational_awareness_volume", enabled).apply()
|
||||
@@ -206,11 +307,6 @@ fun AppSettingsScreen(navController: NavController) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(85.sp.value.dp)
|
||||
.background(
|
||||
shape = RoundedCornerShape(14.dp),
|
||||
color = Color.Transparent
|
||||
)
|
||||
.clickable(
|
||||
indication = null,
|
||||
interactionSource = remember { MutableInteractionSource() }
|
||||
@@ -222,6 +318,7 @@ fun AppSettingsScreen(navController: NavController) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(vertical = 8.dp)
|
||||
.padding(end = 4.dp)
|
||||
) {
|
||||
Text(
|
||||
@@ -249,11 +346,6 @@ fun AppSettingsScreen(navController: NavController) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(85.sp.value.dp)
|
||||
.background(
|
||||
shape = RoundedCornerShape(14.dp),
|
||||
color = Color.Transparent
|
||||
)
|
||||
.clickable(
|
||||
indication = null,
|
||||
interactionSource = remember { MutableInteractionSource() }
|
||||
@@ -265,6 +357,7 @@ fun AppSettingsScreen(navController: NavController) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(vertical = 8.dp)
|
||||
.padding(end = 4.dp)
|
||||
) {
|
||||
Text(
|
||||
@@ -289,20 +382,31 @@ fun AppSettingsScreen(navController: NavController) {
|
||||
)
|
||||
}
|
||||
|
||||
Text(
|
||||
text = "Conversational Awareness Volume",
|
||||
fontSize = 16.sp,
|
||||
color = textColor,
|
||||
modifier = Modifier.padding(top = 8.dp, bottom = 4.dp)
|
||||
)
|
||||
|
||||
val trackColor = if (isDarkTheme) Color(0xFFB3B3B3) else Color(0xFFD9D9D9)
|
||||
val activeTrackColor = if (isDarkTheme) Color(0xFF007AFF) else Color(0xFF3C6DF5)
|
||||
val thumbColor = if (isDarkTheme) Color(0xFFFFFFFF) else Color(0xFFFFFFFF)
|
||||
|
||||
Slider(
|
||||
value = sliderValue.floatValue,
|
||||
onValueChange = {
|
||||
sliderValue.floatValue = it
|
||||
sharedPreferences.edit().putInt("conversational_awareness_volume", it.toInt()).apply()
|
||||
},
|
||||
valueRange = 10f..85f,
|
||||
onValueChangeFinished = {
|
||||
sliderValue.floatValue = sliderValue.floatValue.roundToInt().toFloat()
|
||||
},
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.height(36.dp),
|
||||
.fillMaxWidth()
|
||||
.height(36.dp)
|
||||
.padding(vertical = 4.dp),
|
||||
colors = SliderDefaults.colors(
|
||||
thumbColor = thumbColor,
|
||||
activeTrackColor = activeTrackColor,
|
||||
@@ -341,7 +445,9 @@ fun AppSettingsScreen(navController: NavController) {
|
||||
)
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(bottom = 8.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Text(
|
||||
@@ -349,7 +455,7 @@ fun AppSettingsScreen(navController: NavController) {
|
||||
style = TextStyle(
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.Light,
|
||||
color = labelTextColor
|
||||
color = textColor.copy(alpha = 0.7f)
|
||||
),
|
||||
modifier = Modifier.padding(start = 4.dp)
|
||||
)
|
||||
@@ -358,14 +464,202 @@ fun AppSettingsScreen(navController: NavController) {
|
||||
style = TextStyle(
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.Light,
|
||||
color = labelTextColor
|
||||
color = textColor.copy(alpha = 0.7f)
|
||||
),
|
||||
modifier = Modifier.padding(end = 4.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
Text(
|
||||
text = "Quick Settings Tile".uppercase(),
|
||||
style = TextStyle(
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.Light,
|
||||
color = textColor.copy(alpha = 0.6f),
|
||||
fontFamily = FontFamily(Font(R.font.sf_pro))
|
||||
),
|
||||
modifier = Modifier.padding(8.dp, bottom = 2.dp, top = 24.dp)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(2.dp))
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(
|
||||
backgroundColor,
|
||||
RoundedCornerShape(14.dp)
|
||||
)
|
||||
.padding(horizontal = 16.dp, vertical = 4.dp)
|
||||
) {
|
||||
fun updateQsClickBehavior(enabled: Boolean) {
|
||||
openDialogForControlling = enabled
|
||||
sharedPreferences.edit().putString("qs_click_behavior", if (enabled) "dialog" else "cycle").apply()
|
||||
}
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable(
|
||||
indication = null,
|
||||
interactionSource = remember { MutableInteractionSource() }
|
||||
) {
|
||||
updateQsClickBehavior(!openDialogForControlling)
|
||||
},
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(vertical = 8.dp)
|
||||
.padding(end = 4.dp)
|
||||
) {
|
||||
Text(
|
||||
text = "Open dialog for controlling",
|
||||
fontSize = 16.sp,
|
||||
color = textColor
|
||||
)
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Text(
|
||||
text = if (openDialogForControlling)
|
||||
"If disabled, clicking on the QS will cycle through modes"
|
||||
else "If enabled, it will show a dialog for controlling noise control mode and conversational awareness",
|
||||
fontSize = 14.sp,
|
||||
color = textColor.copy(0.6f),
|
||||
lineHeight = 16.sp,
|
||||
)
|
||||
}
|
||||
|
||||
StyledSwitch(
|
||||
checked = openDialogForControlling,
|
||||
onCheckedChange = {
|
||||
updateQsClickBehavior(it)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Text(
|
||||
text = "Ear Detection".uppercase(),
|
||||
style = TextStyle(
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.Light,
|
||||
color = textColor.copy(alpha = 0.6f),
|
||||
fontFamily = FontFamily(Font(R.font.sf_pro))
|
||||
),
|
||||
modifier = Modifier.padding(8.dp, bottom = 2.dp, top = 24.dp)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(2.dp))
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(
|
||||
backgroundColor,
|
||||
RoundedCornerShape(14.dp)
|
||||
)
|
||||
.padding(horizontal = 16.dp, vertical = 4.dp)
|
||||
) {
|
||||
fun updateDisconnectWhenNotWearing(enabled: Boolean) {
|
||||
disconnectWhenNotWearing = enabled
|
||||
sharedPreferences.edit().putBoolean("disconnect_when_not_wearing", enabled).apply()
|
||||
}
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable(
|
||||
indication = null,
|
||||
interactionSource = remember { MutableInteractionSource() }
|
||||
) {
|
||||
updateDisconnectWhenNotWearing(!disconnectWhenNotWearing)
|
||||
},
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(vertical = 8.dp)
|
||||
.padding(end = 4.dp)
|
||||
) {
|
||||
Text(
|
||||
text = "Disconnect AirPods when not wearing",
|
||||
fontSize = 16.sp,
|
||||
color = textColor
|
||||
)
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Text(
|
||||
text = "You will still be able to control them with the app - this just disconnects the audio.",
|
||||
fontSize = 14.sp,
|
||||
color = textColor.copy(0.6f),
|
||||
lineHeight = 16.sp,
|
||||
)
|
||||
}
|
||||
|
||||
StyledSwitch(
|
||||
checked = disconnectWhenNotWearing,
|
||||
onCheckedChange = {
|
||||
updateDisconnectWhenNotWearing(it)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Text(
|
||||
text = "Advanced Options".uppercase(),
|
||||
style = TextStyle(
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.Light,
|
||||
color = textColor.copy(alpha = 0.6f),
|
||||
fontFamily = FontFamily(Font(R.font.sf_pro))
|
||||
),
|
||||
modifier = Modifier.padding(8.dp, bottom = 2.dp, top = 24.dp)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(2.dp))
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(
|
||||
backgroundColor,
|
||||
RoundedCornerShape(14.dp)
|
||||
)
|
||||
.padding(horizontal = 16.dp, vertical = 4.dp)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable {
|
||||
navController.navigate("troubleshooting")
|
||||
},
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(vertical = 8.dp)
|
||||
.padding(end = 4.dp)
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.troubleshooting),
|
||||
fontSize = 16.sp,
|
||||
color = textColor
|
||||
)
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Text(
|
||||
text = stringResource(R.string.troubleshooting_description),
|
||||
fontSize = 14.sp,
|
||||
color = textColor.copy(0.6f),
|
||||
lineHeight = 16.sp,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
Button(
|
||||
onClick = { showResetDialog = true },
|
||||
@@ -400,6 +694,8 @@ fun AppSettingsScreen(navController: NavController) {
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(32.dp))
|
||||
|
||||
if (showResetDialog) {
|
||||
AlertDialog(
|
||||
onDismissRequest = { showResetDialog = false },
|
||||
|
||||
@@ -40,6 +40,7 @@ import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
@@ -49,6 +50,7 @@ import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.KeyboardArrowLeft
|
||||
import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight
|
||||
import androidx.compose.material.icons.filled.Check
|
||||
import androidx.compose.material.icons.filled.Delete
|
||||
import androidx.compose.material.icons.filled.MoreVert
|
||||
import androidx.compose.material.icons.filled.Send
|
||||
@@ -89,33 +91,18 @@ import androidx.compose.ui.text.input.TextFieldValue
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.navigation.NavController
|
||||
import dev.chrisbanes.haze.HazeEffectScope
|
||||
import dev.chrisbanes.haze.HazeState
|
||||
import dev.chrisbanes.haze.haze
|
||||
import dev.chrisbanes.haze.hazeChild
|
||||
import dev.chrisbanes.haze.hazeEffect
|
||||
import dev.chrisbanes.haze.hazeSource
|
||||
import dev.chrisbanes.haze.materials.CupertinoMaterials
|
||||
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import me.kavishdevar.librepods.R
|
||||
import me.kavishdevar.librepods.services.ServiceManager
|
||||
import me.kavishdevar.librepods.utils.BatteryStatus
|
||||
import me.kavishdevar.librepods.utils.isHeadTrackingData
|
||||
import me.kavishdevar.librepods.composables.StyledSwitch
|
||||
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||
import androidx.compose.foundation.layout.imePadding
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.layout.onGloballyPositioned
|
||||
import androidx.compose.ui.layout.positionInRoot
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.foundation.gestures.detectTapGestures
|
||||
import androidx.compose.foundation.gestures.detectDragGestures
|
||||
import androidx.compose.material.icons.filled.Check
|
||||
import androidx.compose.ui.input.pointer.PointerInputChange
|
||||
|
||||
data class PacketInfo(
|
||||
val type: String,
|
||||
@@ -349,13 +336,13 @@ fun DebugScreen(navController: NavController) {
|
||||
val scrollOffset by remember { derivedStateOf { listState.firstVisibleItemScrollOffset } }
|
||||
val focusManager = LocalFocusManager.current
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
|
||||
|
||||
val showMenu = remember { mutableStateOf(false) }
|
||||
|
||||
|
||||
val airPodsService = remember { ServiceManager.getService() }
|
||||
val packetLogs = airPodsService?.packetLogsFlow?.collectAsState(emptySet())?.value ?: emptySet()
|
||||
val shouldScrollToBottom = remember { mutableStateOf(true) }
|
||||
|
||||
|
||||
val refreshTrigger = remember { mutableStateOf(0) }
|
||||
LaunchedEffect(refreshTrigger.value) {
|
||||
while(true) {
|
||||
@@ -363,16 +350,16 @@ fun DebugScreen(navController: NavController) {
|
||||
refreshTrigger.value = refreshTrigger.value + 1
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
val expandedItems = remember { mutableStateOf(setOf<Int>()) }
|
||||
|
||||
|
||||
fun copyToClipboard(text: String) {
|
||||
val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||
val clip = ClipData.newPlainText("Packet Data", text)
|
||||
clipboard.setPrimaryClip(clip)
|
||||
Toast.makeText(context, "Packet copied to clipboard", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
|
||||
LaunchedEffect(packetLogs.size, refreshTrigger.value) {
|
||||
if (shouldScrollToBottom.value && packetLogs.isNotEmpty()) {
|
||||
listState.animateScrollToItem(packetLogs.size - 1)
|
||||
@@ -415,7 +402,7 @@ fun DebugScreen(navController: NavController) {
|
||||
tint = if (isSystemInDarkTheme()) Color.White else Color.Black
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
DropdownMenu(
|
||||
expanded = showMenu.value,
|
||||
onDismissRequest = { showMenu.value = false },
|
||||
@@ -446,17 +433,17 @@ fun DebugScreen(navController: NavController) {
|
||||
)
|
||||
}
|
||||
},
|
||||
onClick = {
|
||||
onClick = {
|
||||
shouldScrollToBottom.value = !shouldScrollToBottom.value
|
||||
showMenu.value = false
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
HorizontalDivider(
|
||||
color = if (isSystemInDarkTheme()) Color(0xFF3A3A3C) else Color(0xFFE5E5EA),
|
||||
thickness = 0.5.dp
|
||||
)
|
||||
|
||||
|
||||
DropdownMenuItem(
|
||||
text = {
|
||||
Row(
|
||||
@@ -478,7 +465,7 @@ fun DebugScreen(navController: NavController) {
|
||||
)
|
||||
}
|
||||
},
|
||||
onClick = {
|
||||
onClick = {
|
||||
ServiceManager.getService()?.clearLogs()
|
||||
expandedItems.value = emptySet()
|
||||
showMenu.value = false
|
||||
@@ -487,13 +474,12 @@ fun DebugScreen(navController: NavController) {
|
||||
}
|
||||
}
|
||||
},
|
||||
modifier = Modifier.hazeChild(
|
||||
modifier = Modifier.hazeEffect(
|
||||
state = hazeState,
|
||||
style = CupertinoMaterials.thick(),
|
||||
block = {
|
||||
block = fun HazeEffectScope.() {
|
||||
alpha = if (scrollOffset > 0) 1f else 0f
|
||||
}
|
||||
),
|
||||
}),
|
||||
colors = TopAppBarDefaults.topAppBarColors(containerColor = Color.Transparent),
|
||||
)
|
||||
},
|
||||
@@ -502,7 +488,7 @@ fun DebugScreen(navController: NavController) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.haze(hazeState)
|
||||
.hazeSource(hazeState)
|
||||
.padding(top = paddingValues.calculateTopPadding())
|
||||
.navigationBarsPadding()
|
||||
) {
|
||||
@@ -633,7 +619,7 @@ fun DebugScreen(navController: NavController) {
|
||||
airPodsService?.value?.sendPacket(packet.value.text)
|
||||
packet.value = TextFieldValue("")
|
||||
focusManager.clearFocus()
|
||||
|
||||
|
||||
if (shouldScrollToBottom.value && packetLogs.isNotEmpty()) {
|
||||
coroutineScope.launch {
|
||||
try {
|
||||
@@ -643,6 +629,7 @@ fun DebugScreen(navController: NavController) {
|
||||
scrollOffset = 0
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
listState.scrollToItem(
|
||||
index = (packetLogs.size - 1).coerceAtLeast(0)
|
||||
)
|
||||
|
||||
@@ -39,7 +39,10 @@ import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.KeyboardArrowLeft
|
||||
import androidx.compose.material.icons.filled.PlayArrow
|
||||
@@ -69,6 +72,7 @@ import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.drawBehind
|
||||
import androidx.compose.ui.draw.scale
|
||||
import androidx.compose.ui.geometry.CornerRadius
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
@@ -83,6 +87,7 @@ import androidx.compose.ui.graphics.nativeCanvas
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.graphics.vector.path
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
@@ -92,10 +97,17 @@ import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.rememberTextMeasurer
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.navigation.NavController
|
||||
import dev.chrisbanes.haze.HazeEffectScope
|
||||
import dev.chrisbanes.haze.HazeState
|
||||
import dev.chrisbanes.haze.hazeEffect
|
||||
import dev.chrisbanes.haze.hazeSource
|
||||
import dev.chrisbanes.haze.materials.CupertinoMaterials
|
||||
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -108,6 +120,7 @@ import kotlin.math.cos
|
||||
import kotlin.math.sin
|
||||
import kotlin.random.Random
|
||||
|
||||
@ExperimentalHazeMaterialsApi
|
||||
@RequiresApi(Build.VERSION_CODES.Q)
|
||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalAnimationApi::class)
|
||||
@Composable
|
||||
@@ -122,9 +135,36 @@ fun HeadTrackingScreen(navController: NavController) {
|
||||
val isDarkTheme = isSystemInDarkTheme()
|
||||
val backgroundColor = if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFFFFFFF)
|
||||
val textColor = if (isDarkTheme) Color.White else Color.Black
|
||||
|
||||
val scrollState = rememberScrollState()
|
||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
|
||||
val hazeState = remember { HazeState() }
|
||||
|
||||
var mDensity by remember { mutableFloatStateOf(0f) }
|
||||
Scaffold(
|
||||
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||
topBar = {
|
||||
CenterAlignedTopAppBar(
|
||||
modifier = Modifier.hazeEffect(
|
||||
state = hazeState,
|
||||
style = CupertinoMaterials.thick(),
|
||||
block = fun HazeEffectScope.() {
|
||||
alpha =
|
||||
if (scrollState.value > 60.dp.value * mDensity) 1f else 0f
|
||||
})
|
||||
.drawBehind {
|
||||
mDensity = density
|
||||
val strokeWidth = 0.7.dp.value * density
|
||||
val y = size.height - strokeWidth / 2
|
||||
if (scrollState.value > 60.dp.value * density) {
|
||||
drawLine(
|
||||
if (isDarkTheme) Color.DarkGray else Color.LightGray,
|
||||
Offset(0f, y),
|
||||
Offset(size.width, y),
|
||||
strokeWidth
|
||||
)
|
||||
}
|
||||
},
|
||||
title = {
|
||||
Text(
|
||||
stringResource(R.string.head_tracking),
|
||||
@@ -138,6 +178,7 @@ fun HeadTrackingScreen(navController: NavController) {
|
||||
if (ServiceManager.getService()?.isHeadTrackingActive == true) ServiceManager.getService()?.stopHeadTracking()
|
||||
},
|
||||
shape = RoundedCornerShape(8.dp),
|
||||
modifier = Modifier.width(180.dp)
|
||||
) {
|
||||
Icon(
|
||||
Icons.AutoMirrored.Filled.KeyboardArrowLeft,
|
||||
@@ -153,10 +194,13 @@ fun HeadTrackingScreen(navController: NavController) {
|
||||
color = if (isDarkTheme) Color(0xFF007AFF) else Color(0xFF3C6DF5),
|
||||
fontFamily = FontFamily(Font(R.font.sf_pro))
|
||||
),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
}
|
||||
},
|
||||
colors = TopAppBarDefaults.topAppBarColors(
|
||||
colors = TopAppBarDefaults.centerAlignedTopAppBarColors(
|
||||
containerColor = Color.Transparent
|
||||
),
|
||||
actions = {
|
||||
@@ -205,7 +249,8 @@ fun HeadTrackingScreen(navController: NavController) {
|
||||
modifier = Modifier.scale(1.5f)
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
scrollBehavior = scrollBehavior
|
||||
)
|
||||
},
|
||||
containerColor = if (isSystemInDarkTheme()) Color(0xFF000000)
|
||||
@@ -217,6 +262,8 @@ fun HeadTrackingScreen(navController: NavController) {
|
||||
.padding(paddingValues = paddingValues)
|
||||
.padding(horizontal = 16.dp)
|
||||
.padding(top = 8.dp)
|
||||
.verticalScroll(scrollState)
|
||||
.hazeSource(state = hazeState)
|
||||
) {
|
||||
val sharedPreferences =
|
||||
LocalContext.current.getSharedPreferences("settings", Context.MODE_PRIVATE)
|
||||
@@ -305,7 +352,7 @@ fun HeadTrackingScreen(navController: NavController) {
|
||||
|
||||
Box(
|
||||
contentAlignment = Alignment.Center,
|
||||
modifier = Modifier.padding(top = 12.dp)
|
||||
modifier = Modifier.padding(top = 12.dp, bottom = 24.dp)
|
||||
) {
|
||||
AnimatedContent(
|
||||
targetState = gestureText,
|
||||
@@ -800,6 +847,7 @@ private fun AccelerationPlot() {
|
||||
}
|
||||
}
|
||||
|
||||
@ExperimentalHazeMaterialsApi
|
||||
@RequiresApi(Build.VERSION_CODES.Q)
|
||||
@Preview
|
||||
@Composable
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,7 @@
|
||||
/*
|
||||
* LibrePods - AirPods liberated from Apple’s ecosystem
|
||||
*
|
||||
* Copyright (C) 2025 Kavish Devar
|
||||
* Copyright (C) 2025 LibrePods Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
@@ -31,6 +31,8 @@ import android.service.quicksettings.Tile
|
||||
import android.service.quicksettings.TileService
|
||||
import android.util.Log
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import me.kavishdevar.librepods.MainActivity
|
||||
import me.kavishdevar.librepods.QuickSettingsDialogActivity
|
||||
import me.kavishdevar.librepods.R
|
||||
import me.kavishdevar.librepods.utils.AirPodsNotifications
|
||||
@@ -260,4 +262,42 @@ class AirPodsQSService : TileService() {
|
||||
else -> R.drawable.airpods
|
||||
}
|
||||
}
|
||||
|
||||
@ExperimentalMaterial3Api
|
||||
override fun onTileAdded() {
|
||||
super.onTileAdded()
|
||||
Log.d("AirPodsQSService", "Tile added")
|
||||
|
||||
val intent = Intent(this, MainActivity::class.java).apply {
|
||||
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
}
|
||||
}
|
||||
|
||||
@ExperimentalMaterial3Api
|
||||
fun openMainActivity() {
|
||||
Log.d("AirPodsQSService", "Opening MainActivity")
|
||||
|
||||
try {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||
val pendingIntent = PendingIntent.getActivity(
|
||||
this,
|
||||
0,
|
||||
Intent(this, MainActivity::class.java).apply {
|
||||
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
},
|
||||
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
|
||||
)
|
||||
startActivityAndCollapse(pendingIntent)
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
val intent = Intent(this, MainActivity::class.java).apply {
|
||||
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
}
|
||||
startActivityAndCollapse(intent)
|
||||
}
|
||||
Log.d("AirPodsQSService", "Called startActivityAndCollapse for MainActivity")
|
||||
} catch (e: Exception) {
|
||||
Log.e("AirPodsQSService", "Error launching MainActivity: $e")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -238,7 +238,7 @@ object CrossDevice {
|
||||
batteryBytes = trimmedPacket
|
||||
ServiceManager.getService()?.batteryNotification?.setBattery(trimmedPacket)
|
||||
Log.d("CrossDevice", "Battery data: ${ServiceManager.getService()?.batteryNotification?.getBattery()[0]?.level}")
|
||||
ServiceManager.getService()?.updateBatteryWidget()
|
||||
ServiceManager.getService()?.updateBattery()
|
||||
ServiceManager.getService()?.sendBatteryBroadcast()
|
||||
ServiceManager.getService()?.sendBatteryNotification()
|
||||
} else if (ServiceManager.getService()?.ancNotification?.isANCData(trimmedPacket) == true) {
|
||||
|
||||
@@ -0,0 +1,215 @@
|
||||
/*
|
||||
* LibrePods - AirPods liberated from Apple's ecosystem
|
||||
*
|
||||
* Copyright (C) 2025 LibrePods contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package me.kavishdevar.librepods.utils
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.BufferedReader
|
||||
import java.io.File
|
||||
import java.io.InputStreamReader
|
||||
|
||||
class LogCollector(private val context: Context) {
|
||||
private var isCollecting = false
|
||||
private var logProcess: Process? = null
|
||||
|
||||
suspend fun openXposedSettings(context: Context) {
|
||||
withContext(Dispatchers.IO) {
|
||||
val command = if (android.os.Build.VERSION.SDK_INT >= 29) {
|
||||
"am broadcast -a android.telephony.action.SECRET_CODE -d android_secret_code://5776733 android"
|
||||
} else {
|
||||
"am broadcast -a android.provider.Telephony.SECRET_CODE -d android_secret_code://5776733 android"
|
||||
}
|
||||
|
||||
executeRootCommand(command)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun clearLogs() {
|
||||
withContext(Dispatchers.IO) {
|
||||
executeRootCommand("logcat -c")
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun killBluetoothService() {
|
||||
withContext(Dispatchers.IO) {
|
||||
executeRootCommand("killall com.android.bluetooth")
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getPackageUIDs(): Pair<String?, String?> {
|
||||
return withContext(Dispatchers.IO) {
|
||||
val btUid = executeRootCommand("dumpsys package com.android.bluetooth | grep -m 1 \"uid=\" | sed -E 's/.*uid=([0-9]+).*/\\1/'")
|
||||
.trim()
|
||||
.takeIf { it.isNotEmpty() }
|
||||
|
||||
val appUid = executeRootCommand("dumpsys package me.kavishdevar.librepods | grep -m 1 \"uid=\" | sed -E 's/.*uid=([0-9]+).*/\\1/'")
|
||||
.trim()
|
||||
.takeIf { it.isNotEmpty() }
|
||||
|
||||
Pair(btUid, appUid)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun startLogCollection(listener: (String) -> Unit, connectionDetectedCallback: () -> Unit): String {
|
||||
return withContext(Dispatchers.IO) {
|
||||
isCollecting = true
|
||||
val (btUid, appUid) = getPackageUIDs()
|
||||
|
||||
val uidFilter = buildString {
|
||||
if (!btUid.isNullOrEmpty() && !appUid.isNullOrEmpty()) {
|
||||
append("$btUid,$appUid")
|
||||
} else if (!btUid.isNullOrEmpty()) {
|
||||
append(btUid)
|
||||
} else if (!appUid.isNullOrEmpty()) {
|
||||
append(appUid)
|
||||
}
|
||||
}
|
||||
|
||||
val command = if (uidFilter.isNotEmpty()) {
|
||||
"su -c logcat --uid=$uidFilter -v threadtime"
|
||||
} else {
|
||||
"su -c logcat -v threadtime"
|
||||
}
|
||||
|
||||
val logs = StringBuilder()
|
||||
try {
|
||||
logProcess = Runtime.getRuntime().exec(command)
|
||||
val reader = BufferedReader(InputStreamReader(logProcess!!.inputStream))
|
||||
var line: String? = null
|
||||
var connectionDetected = false
|
||||
|
||||
while (isCollecting && reader.readLine().also { line = it } != null) {
|
||||
line?.let {
|
||||
if (it.contains("<LogCollector:")) {
|
||||
logs.append("\n=============\n")
|
||||
}
|
||||
|
||||
logs.append(it).append("\n")
|
||||
listener(it)
|
||||
|
||||
if (it.contains("<LogCollector:")) {
|
||||
logs.append("=============\n\n")
|
||||
}
|
||||
|
||||
if (!connectionDetected) {
|
||||
if (it.contains("<LogCollector:Complete:Success>")) {
|
||||
connectionDetected = true
|
||||
connectionDetectedCallback()
|
||||
} else if (it.contains("<LogCollector:Complete:Failed>")) {
|
||||
connectionDetected = true
|
||||
connectionDetectedCallback()
|
||||
} else if (it.contains("<LogCollector:Start>")) {
|
||||
}
|
||||
else if (it.contains("AirPodsService") && it.contains("Connected to device")) {
|
||||
connectionDetected = true
|
||||
connectionDetectedCallback()
|
||||
} else if (it.contains("AirPodsService") && it.contains("Connection failed")) {
|
||||
connectionDetected = true
|
||||
connectionDetectedCallback()
|
||||
} else if (it.contains("AirPodsService") && it.contains("Device disconnected")) {
|
||||
}
|
||||
else if (it.contains("BluetoothService") && it.contains("CONNECTION_STATE_CONNECTED")) {
|
||||
connectionDetected = true
|
||||
connectionDetectedCallback()
|
||||
} else if (it.contains("BluetoothService") && it.contains("CONNECTION_STATE_DISCONNECTED")) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
logs.append("Error collecting logs: ${e.message}").append("\n")
|
||||
e.printStackTrace()
|
||||
}
|
||||
|
||||
logs.toString()
|
||||
}
|
||||
}
|
||||
|
||||
fun stopLogCollection() {
|
||||
isCollecting = false
|
||||
logProcess?.destroy()
|
||||
logProcess = null
|
||||
}
|
||||
|
||||
suspend fun saveLogToInternalStorage(fileName: String, content: String): File? {
|
||||
return withContext(Dispatchers.IO) {
|
||||
try {
|
||||
val logsDir = File(context.filesDir, "logs")
|
||||
if (!logsDir.exists()) {
|
||||
logsDir.mkdir()
|
||||
}
|
||||
|
||||
val file = File(logsDir, fileName)
|
||||
file.writeText(content)
|
||||
return@withContext file
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
return@withContext null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun addLogMarker(markerType: LogMarkerType, details: String = "") {
|
||||
withContext(Dispatchers.IO) {
|
||||
val timestamp = java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS", java.util.Locale.US)
|
||||
.format(java.util.Date())
|
||||
|
||||
val marker = when (markerType) {
|
||||
LogMarkerType.START -> "<LogCollector:Start> [$timestamp] Beginning connection test"
|
||||
LogMarkerType.SUCCESS -> "<LogCollector:Complete:Success> [$timestamp] Connection test completed successfully"
|
||||
LogMarkerType.FAILURE -> "<LogCollector:Complete:Failed> [$timestamp] Connection test failed"
|
||||
LogMarkerType.CUSTOM -> "<LogCollector:Custom:$details> [$timestamp]"
|
||||
}
|
||||
|
||||
val command = "log -t AirPodsService \"$marker\""
|
||||
executeRootCommand(command)
|
||||
}
|
||||
}
|
||||
|
||||
enum class LogMarkerType {
|
||||
START,
|
||||
SUCCESS,
|
||||
FAILURE,
|
||||
CUSTOM
|
||||
}
|
||||
|
||||
private suspend fun executeRootCommand(command: String): String {
|
||||
return withContext(Dispatchers.IO) {
|
||||
try {
|
||||
val process = Runtime.getRuntime().exec("su -c $command")
|
||||
val reader = BufferedReader(InputStreamReader(process.inputStream))
|
||||
val output = StringBuilder()
|
||||
var line: String?
|
||||
|
||||
while (reader.readLine().also { line = it } != null) {
|
||||
output.append(line).append("\n")
|
||||
}
|
||||
|
||||
process.waitFor()
|
||||
output.toString()
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -21,10 +21,12 @@ package me.kavishdevar.librepods.utils
|
||||
import android.content.SharedPreferences
|
||||
import android.media.AudioManager
|
||||
import android.media.AudioPlaybackConfiguration
|
||||
import android.os.Build
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.util.Log
|
||||
import android.view.KeyEvent
|
||||
import androidx.annotation.RequiresApi
|
||||
import me.kavishdevar.librepods.services.ServiceManager
|
||||
|
||||
object MediaController {
|
||||
@@ -34,11 +36,12 @@ object MediaController {
|
||||
var userPlayedTheMedia = false
|
||||
private lateinit var sharedPreferences: SharedPreferences
|
||||
private val handler = Handler(Looper.getMainLooper())
|
||||
private lateinit var preferenceChangeListener: SharedPreferences.OnSharedPreferenceChangeListener
|
||||
|
||||
var pausedForCrossDevice = false
|
||||
|
||||
private var relativeVolume: Boolean = false
|
||||
private var conversationalAwarenessVolume: Int = 1/12
|
||||
private var conversationalAwarenessVolume: Int = 2
|
||||
private var conversationalAwarenessPauseMusic: Boolean = false
|
||||
|
||||
fun initialize(audioManager: AudioManager, sharedPreferences: SharedPreferences) {
|
||||
@@ -49,16 +52,16 @@ object MediaController {
|
||||
this.sharedPreferences = sharedPreferences
|
||||
Log.d("MediaController", "Initializing MediaController")
|
||||
relativeVolume = sharedPreferences.getBoolean("relative_conversational_awareness_volume", false)
|
||||
conversationalAwarenessVolume = sharedPreferences.getInt("conversational_awareness_volume", 100/12)
|
||||
conversationalAwarenessVolume = sharedPreferences.getInt("conversational_awareness_volume", (audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC) / 0.4).toInt())
|
||||
conversationalAwarenessPauseMusic = sharedPreferences.getBoolean("conversational_awareness_pause_music", false)
|
||||
|
||||
sharedPreferences.registerOnSharedPreferenceChangeListener { _, key ->
|
||||
preferenceChangeListener = SharedPreferences.OnSharedPreferenceChangeListener { _, key ->
|
||||
when (key) {
|
||||
"relative_conversational_awareness_volume" -> {
|
||||
relativeVolume = sharedPreferences.getBoolean("relative_conversational_awareness_volume", false)
|
||||
}
|
||||
"conversational_awareness_volume" -> {
|
||||
conversationalAwarenessVolume = sharedPreferences.getInt("conversational_awareness_volume", 100/12)
|
||||
conversationalAwarenessVolume = sharedPreferences.getInt("conversational_awareness_volume", (audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC) * 0.4).toInt())
|
||||
}
|
||||
"conversational_awareness_pause_music" -> {
|
||||
conversationalAwarenessPauseMusic = sharedPreferences.getBoolean("conversational_awareness_pause_music", false)
|
||||
@@ -66,17 +69,19 @@ object MediaController {
|
||||
}
|
||||
}
|
||||
|
||||
sharedPreferences.registerOnSharedPreferenceChangeListener(preferenceChangeListener)
|
||||
|
||||
audioManager.registerAudioPlaybackCallback(cb, null)
|
||||
}
|
||||
|
||||
val cb = object : AudioManager.AudioPlaybackCallback() {
|
||||
@RequiresApi(Build.VERSION_CODES.R)
|
||||
override fun onPlaybackConfigChanged(configs: MutableList<AudioPlaybackConfiguration>?) {
|
||||
super.onPlaybackConfigChanged(configs)
|
||||
Log.d("MediaController", "Playback config changed, iPausedTheMedia: $iPausedTheMedia")
|
||||
if (configs != null && !iPausedTheMedia) {
|
||||
Log.d("MediaController", "Seems like the user changed the state of media themselves, now I won't play until the ear detection pauses it.")
|
||||
handler.postDelayed({
|
||||
iPausedTheMedia = !audioManager.isMusicActive
|
||||
userPlayedTheMedia = audioManager.isMusicActive
|
||||
}, 7) // i have no idea why android sends an event a hundred times after the user does something.
|
||||
}
|
||||
@@ -92,9 +97,9 @@ object MediaController {
|
||||
|
||||
@Synchronized
|
||||
fun sendPause(force: Boolean = false) {
|
||||
Log.d("MediaController", "Sending pause with iPausedTheMedia: $iPausedTheMedia, userPlayedTheMedia: $userPlayedTheMedia")
|
||||
if ((audioManager.isMusicActive && !userPlayedTheMedia) || force) {
|
||||
iPausedTheMedia = true
|
||||
Log.d("MediaController", "Sending pause with iPausedTheMedia: $iPausedTheMedia, userPlayedTheMedia: $userPlayedTheMedia, isMusicActive: ${audioManager.isMusicActive}, force: $force")
|
||||
if ((audioManager.isMusicActive) && (!userPlayedTheMedia || force)) {
|
||||
iPausedTheMedia = if (force) audioManager.isMusicActive else true
|
||||
userPlayedTheMedia = false
|
||||
audioManager.dispatchMediaKeyEvent(
|
||||
KeyEvent(
|
||||
@@ -134,11 +139,18 @@ object MediaController {
|
||||
|
||||
@Synchronized
|
||||
fun startSpeaking() {
|
||||
Log.d("MediaController", "Starting speaking")
|
||||
Log.d("MediaController", "Starting speaking max vol: ${audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC)}, current vol: ${audioManager.getStreamVolume(AudioManager.STREAM_MUSIC)}, conversationalAwarenessVolume: $conversationalAwarenessVolume, relativeVolume: $relativeVolume")
|
||||
|
||||
if (initialVolume == null) {
|
||||
initialVolume = audioManager.getStreamVolume(AudioManager.STREAM_MUSIC)
|
||||
Log.d("MediaController", "Initial Volume Set: $initialVolume")
|
||||
val targetVolume = if (relativeVolume) initialVolume!! * conversationalAwarenessVolume * 1/100 else if ( initialVolume!! > audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC) * conversationalAwarenessVolume * 1/100) audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC) * conversationalAwarenessVolume * 1/100 else initialVolume!!
|
||||
Log.d("MediaController", "Initial Volume: $initialVolume")
|
||||
val targetVolume = if (relativeVolume) {
|
||||
(initialVolume!! * conversationalAwarenessVolume / 100)
|
||||
} else if (initialVolume!! > (audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC) * conversationalAwarenessVolume / 100)) {
|
||||
(audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC) * conversationalAwarenessVolume / 100)
|
||||
} else {
|
||||
initialVolume!!
|
||||
}
|
||||
smoothVolumeTransition(initialVolume!!, targetVolume.toInt())
|
||||
if (conversationalAwarenessPauseMusic) {
|
||||
sendPause(force = true)
|
||||
@@ -160,6 +172,7 @@ object MediaController {
|
||||
}
|
||||
|
||||
private fun smoothVolumeTransition(fromVolume: Int, toVolume: Int) {
|
||||
Log.d("MediaController", "Smooth volume transition from $fromVolume to $toVolume")
|
||||
val step = if (fromVolume < toVolume) 1 else -1
|
||||
val delay = 50L
|
||||
var currentVolume = fromVolume
|
||||
|
||||
@@ -24,8 +24,12 @@ import android.animation.AnimatorListenerAdapter
|
||||
import android.animation.ObjectAnimator
|
||||
import android.animation.PropertyValuesHolder
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.graphics.PixelFormat
|
||||
import android.os.Build
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.util.Log
|
||||
@@ -51,6 +55,7 @@ class PopupWindow(
|
||||
private var isClosing = false
|
||||
private var autoCloseHandler = Handler(Looper.getMainLooper())
|
||||
private var autoCloseRunnable: Runnable? = null
|
||||
private var batteryUpdateReceiver: BroadcastReceiver? = null
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
private val mParams: WindowManager.LayoutParams = WindowManager.LayoutParams().apply {
|
||||
@@ -145,6 +150,8 @@ class PopupWindow(
|
||||
interpolator = DecelerateInterpolator()
|
||||
start()
|
||||
}
|
||||
|
||||
registerBatteryUpdateReceiver()
|
||||
|
||||
autoCloseRunnable = Runnable { close() }
|
||||
autoCloseHandler.postDelayed(autoCloseRunnable!!, 12000)
|
||||
@@ -155,15 +162,43 @@ class PopupWindow(
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
fun updateBatteryStatus(batteryNotification: AirPodsNotifications.BatteryNotification) {
|
||||
val batteryStatus = batteryNotification.getBattery()
|
||||
private fun registerBatteryUpdateReceiver() {
|
||||
batteryUpdateReceiver = object : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context?, intent: Intent?) {
|
||||
if (intent?.action == AirPodsNotifications.BATTERY_DATA) {
|
||||
val batteryList = intent.getParcelableArrayListExtra<Battery>("data")
|
||||
if (batteryList != null) {
|
||||
updateBatteryStatusFromList(batteryList)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val filter = IntentFilter(AirPodsNotifications.BATTERY_DATA)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
context.registerReceiver(batteryUpdateReceiver, filter, Context.RECEIVER_EXPORTED)
|
||||
} else {
|
||||
context.registerReceiver(batteryUpdateReceiver, filter)
|
||||
}
|
||||
}
|
||||
|
||||
private fun unregisterBatteryUpdateReceiver() {
|
||||
batteryUpdateReceiver?.let {
|
||||
try {
|
||||
context.unregisterReceiver(it)
|
||||
batteryUpdateReceiver = null
|
||||
} catch (e: Exception) {
|
||||
Log.e("PopupWindow", "Error unregistering battery receiver: ${e.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateBatteryStatusFromList(batteryList: List<Battery>) {
|
||||
val batteryLeftText = mView.findViewById<TextView>(R.id.left_battery)
|
||||
val batteryRightText = mView.findViewById<TextView>(R.id.right_battery)
|
||||
val batteryCaseText = mView.findViewById<TextView>(R.id.case_battery)
|
||||
|
||||
batteryLeftText.text = batteryStatus.find { it.component == BatteryComponent.LEFT }?.let {
|
||||
batteryLeftText.text = batteryList.find { it.component == BatteryComponent.LEFT }?.let {
|
||||
if (it.status != BatteryStatus.DISCONNECTED) {
|
||||
"\uDBC3\uDC8E ${it.level}%"
|
||||
} else {
|
||||
@@ -171,7 +206,7 @@ class PopupWindow(
|
||||
}
|
||||
} ?: ""
|
||||
|
||||
batteryRightText.text = batteryStatus.find { it.component == BatteryComponent.RIGHT }?.let {
|
||||
batteryRightText.text = batteryList.find { it.component == BatteryComponent.RIGHT }?.let {
|
||||
if (it.status != BatteryStatus.DISCONNECTED) {
|
||||
"\uDBC3\uDC8D ${it.level}%"
|
||||
} else {
|
||||
@@ -179,7 +214,7 @@ class PopupWindow(
|
||||
}
|
||||
} ?: ""
|
||||
|
||||
batteryCaseText.text = batteryStatus.find { it.component == BatteryComponent.CASE }?.let {
|
||||
batteryCaseText.text = batteryList.find { it.component == BatteryComponent.CASE }?.let {
|
||||
if (it.status != BatteryStatus.DISCONNECTED) {
|
||||
"\uDBC3\uDE6C ${it.level}%"
|
||||
} else {
|
||||
@@ -188,12 +223,19 @@ class PopupWindow(
|
||||
} ?: ""
|
||||
}
|
||||
|
||||
@SuppressLint("SetTextI18s")
|
||||
fun updateBatteryStatus(batteryNotification: AirPodsNotifications.BatteryNotification) {
|
||||
val batteryStatus = batteryNotification.getBattery()
|
||||
updateBatteryStatusFromList(batteryStatus)
|
||||
}
|
||||
|
||||
fun close() {
|
||||
try {
|
||||
if (isClosing) return
|
||||
isClosing = true
|
||||
|
||||
autoCloseRunnable?.let { autoCloseHandler.removeCallbacks(it) }
|
||||
unregisterBatteryUpdateReceiver()
|
||||
|
||||
val vid = mView.findViewById<VideoView>(R.id.video)
|
||||
vid.stopPlayback()
|
||||
|
||||
@@ -416,9 +416,9 @@ class RadareOffsetFinder(context: Context) {
|
||||
Log.e(TAG, "rabin2 command failed with exit code $exitCode")
|
||||
}
|
||||
|
||||
// findAndSaveL2cuProcessCfgReqOffset(libraryPath, envSetup)
|
||||
// findAndSaveL2cCsmConfigOffset(libraryPath, envSetup)
|
||||
findAndSaveL2cuSendPeerInfoReqOffset(libraryPath, envSetup)
|
||||
// findAndSaveL2cuProcessCfgReqOffset(libraryPath, envSetup)
|
||||
// findAndSaveL2cCsmConfigOffset(libraryPath, envSetup)
|
||||
// findAndSaveL2cuSendPeerInfoReqOffset(libraryPath, envSetup)
|
||||
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to find function offset", e)
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package me.kavishdevar.librepods.utils
|
||||
|
||||
import android.bluetooth.BluetoothDevice
|
||||
import android.util.Log
|
||||
import org.lsposed.hiddenapibypass.HiddenApiBypass
|
||||
|
||||
object SystemApisUtils {
|
||||
|
||||
@@ -282,4 +284,23 @@ object SystemApisUtils {
|
||||
const val ACTION_BLUETOOTH_HANDSFREE_BATTERY_CHANGED = "android.intent.action.BLUETOOTH_HANDSFREE_BATTERY_CHANGED"
|
||||
const val EXTRA_SHOW_BT_HANDSFREE_BATTERY = "android.intent.extra.show_bluetooth_handsfree_battery"
|
||||
const val EXTRA_BT_HANDSFREE_BATTERY_LEVEL = "android.intent.extra.bluetooth_handsfree_battery_level"
|
||||
|
||||
/**
|
||||
* Helper method to set metadata using HiddenApiBypass
|
||||
*/
|
||||
fun setMetadata(device: BluetoothDevice, key: Int, value: ByteArray): Boolean {
|
||||
return try {
|
||||
val result = HiddenApiBypass.invoke(
|
||||
BluetoothDevice::class.java,
|
||||
device,
|
||||
"setMetadata",
|
||||
key,
|
||||
value
|
||||
) as Boolean
|
||||
result
|
||||
} catch (e: Exception) {
|
||||
Log.e("SystemApisUtils", "Failed to set metadata for key $key", e)
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,15 +19,9 @@
|
||||
|
||||
package me.kavishdevar.librepods.widgets
|
||||
|
||||
import android.app.PendingIntent
|
||||
import android.appwidget.AppWidgetManager
|
||||
import android.appwidget.AppWidgetProvider
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.widget.RemoteViews
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import me.kavishdevar.librepods.MainActivity
|
||||
import me.kavishdevar.librepods.R
|
||||
import me.kavishdevar.librepods.services.ServiceManager
|
||||
|
||||
class BatteryWidget : AppWidgetProvider() {
|
||||
@@ -36,6 +30,6 @@ class BatteryWidget : AppWidgetProvider() {
|
||||
appWidgetManager: AppWidgetManager,
|
||||
appWidgetIds: IntArray
|
||||
) {
|
||||
ServiceManager.getService()?.updateBatteryWidget()
|
||||
ServiceManager.getService()?.updateBattery()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ import android.appwidget.AppWidgetManager
|
||||
import android.appwidget.AppWidgetProvider
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.util.Log
|
||||
import android.widget.RemoteViews
|
||||
import me.kavishdevar.librepods.R
|
||||
import me.kavishdevar.librepods.services.ServiceManager
|
||||
@@ -77,6 +78,7 @@ class NoiseControlWidget : AppWidgetProvider() {
|
||||
super.onReceive(context, intent)
|
||||
if (intent.action == "ACTION_SET_ANC_MODE") {
|
||||
val mode = intent.getIntExtra("ANC_MODE", 1)
|
||||
Log.d("NoiseControlWidget", "Setting ANC mode to $mode")
|
||||
ServiceManager.getService()?.setANCMode(mode)
|
||||
}
|
||||
}
|
||||
|
||||
10
android/app/src/main/res/drawable/ic_save.xml
Normal file
10
android/app/src/main/res/drawable/ic_save.xml
Normal file
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:pathData="M17,3L5,3c-1.11,0 -2,0.9 -2,2v14c0,1.1 0.89,2 2,2h14c1.1,0 2,-0.9 2,-2L21,7l-4,-4zM19,19L5,19L5,5h11.17L19,7.83L19,19zM12,12c-1.66,0 -3,1.34 -3,3s1.34,3 3,3 3,-1.34 3,-3 -1.34,-3 -3,-3zM6,6h9v4L6,10z" />
|
||||
</vector>
|
||||
BIN
android/app/src/main/res/drawable/pro_2_left.png
Normal file
BIN
android/app/src/main/res/drawable/pro_2_left.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 35 KiB |
BIN
android/app/src/main/res/drawable/pro_2_right.png
Normal file
BIN
android/app/src/main/res/drawable/pro_2_right.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 36 KiB |
@@ -1,5 +1,6 @@
|
||||
<resources>
|
||||
<string name="app_name" translatable="false">LibrePods</string>
|
||||
<string name="app_description" translatable="false">Liberate your AirPods from Apple\'s ecosystem.</string>
|
||||
<string name="title_activity_custom_device" translatable="false">GATT Testing</string>
|
||||
<string name="app_widget_description">See your AirPods battery status right from your home screen!</string>
|
||||
<string name="accessibility">Accessibility</string>
|
||||
@@ -34,8 +35,7 @@
|
||||
<string name="airpods_not_connected">AirPods not connected</string>
|
||||
<string name="airpods_not_connected_description">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!)</string>
|
||||
<string name="back">Back</string>
|
||||
<string name="app_settings">App Settings</string>
|
||||
<string name="conversational_awareness_customization">Conversational Awareness</string>
|
||||
<string name="app_settings">Customizations</string>
|
||||
<string name="relative_conversational_awareness_volume">Relative volume</string>
|
||||
<string name="relative_conversational_awareness_volume_description">Reduces to a percentage of the current volume instead of the maximum volume.</string>
|
||||
<string name="conversational_awareness_pause_music">Pause Music</string>
|
||||
@@ -59,4 +59,9 @@
|
||||
<string name="ear_detection">Automatic Ear Detection</string>
|
||||
<string name="auto_play">Auto Play</string>
|
||||
<string name="auto_pause">Auto Pause</string>
|
||||
<string name="troubleshooting">Troubleshooting</string>
|
||||
<string name="troubleshooting_description">Collect logs to diagnose issues with AirPods connection</string>
|
||||
<string name="collect_logs">Collect Logs</string>
|
||||
<string name="saved_logs">Saved Logs</string>
|
||||
<string name="no_logs_found">No saved logs found</string>
|
||||
</resources>
|
||||
|
||||
4
android/app/src/main/res/xml/file_paths.xml
Normal file
4
android/app/src/main/res/xml/file_paths.xml
Normal file
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<paths>
|
||||
<files-path name="logs" path="logs/"/>
|
||||
</paths>
|
||||
@@ -4,9 +4,9 @@ project(linux VERSION 0.1 LANGUAGES CXX)
|
||||
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
|
||||
find_package(Qt6 6.5 REQUIRED COMPONENTS Quick Widgets Bluetooth DBus)
|
||||
find_package(Qt6 6.4 REQUIRED COMPONENTS Quick Widgets Bluetooth DBus)
|
||||
|
||||
qt_standard_project_setup(REQUIRES 6.5)
|
||||
qt_standard_project_setup(REQUIRES 6.4)
|
||||
|
||||
qt_add_executable(applinux
|
||||
main.cpp
|
||||
|
||||
@@ -14,7 +14,13 @@ A native Linux application to control your AirPods, with support for:
|
||||
2. Qt6 packages
|
||||
|
||||
```bash
|
||||
sudo pacman -S qt6-base qt6-connectivity qt6-multimedia-ffmpeg qt6-multimedia # Arch Linux / EndeavourOS
|
||||
# For Arch Linux / EndeavourOS
|
||||
sudo pacman -S qt6-base qt6-connectivity qt6-multimedia-ffmpeg qt6-multimedia
|
||||
|
||||
# For Debian
|
||||
sudo apt-get install qt6-base-dev qt6-declarative-dev qt6-connectivity-dev qt6-multimedia-dev \
|
||||
qml6-module-qtquick-controls qml6-module-qtqml-workerscript qml6-module-qtquick-templates \
|
||||
qml6-module-qtquick-window qml6-module-qtquick-layouts
|
||||
```
|
||||
|
||||
## Setup
|
||||
|
||||
@@ -8,7 +8,7 @@ set(CMAKE_AUTOMOC ON)
|
||||
set(CMAKE_AUTORCC ON)
|
||||
set(CMAKE_AUTOUIC ON)
|
||||
|
||||
find_package(Qt6 6.5 REQUIRED COMPONENTS Core Bluetooth Widgets)
|
||||
find_package(Qt6 6.4 REQUIRED COMPONENTS Core Bluetooth Widgets)
|
||||
|
||||
qt_add_executable(ble_monitor
|
||||
main.cpp
|
||||
@@ -26,4 +26,4 @@ install(TARGETS ble_monitor
|
||||
BUNDLE DESTINATION .
|
||||
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
|
||||
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
|
||||
)
|
||||
)
|
||||
|
||||
@@ -367,7 +367,7 @@ private slots:
|
||||
}
|
||||
else
|
||||
{
|
||||
parent->loadFromModule("linux", "Main");
|
||||
loadMainModule();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -379,7 +379,7 @@ private slots:
|
||||
}
|
||||
else
|
||||
{
|
||||
parent->loadFromModule("linux", "Main");
|
||||
loadMainModule();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -908,6 +908,10 @@ private slots:
|
||||
connectToPhone();
|
||||
}
|
||||
|
||||
void loadMainModule() {
|
||||
parent->load(QUrl(QStringLiteral("qrc:/linux/Main.qml")));
|
||||
}
|
||||
|
||||
signals:
|
||||
void noiseControlModeChanged(NoiseControlMode mode);
|
||||
void earDetectionStatusChanged(const QString &status);
|
||||
@@ -995,7 +999,7 @@ int main(int argc, char *argv[]) {
|
||||
qmlRegisterType<Battery>("me.kavishdevar.Battery", 1, 0, "Battery");
|
||||
AirPodsTrayApp *trayApp = new AirPodsTrayApp(debugMode, hideOnStart, &engine);
|
||||
engine.rootContext()->setContextProperty("airPodsTrayApp", trayApp);
|
||||
engine.loadFromModule("linux", "Main");
|
||||
trayApp->loadMainModule();
|
||||
|
||||
QLocalServer server;
|
||||
QLocalServer::removeServer("app_server");
|
||||
@@ -1012,7 +1016,7 @@ int main(int argc, char *argv[]) {
|
||||
QObject::connect(&server, &QLocalServer::newConnection, [&]() {
|
||||
QLocalSocket* socket = server.nextPendingConnection();
|
||||
// Handles Proper Connection
|
||||
QObject::connect(socket, &QLocalSocket::readyRead, [socket, &engine]() {
|
||||
QObject::connect(socket, &QLocalSocket::readyRead, [socket, &engine, &trayApp]() {
|
||||
QString msg = socket->readAll();
|
||||
// Check if the message is "reopen", if so, trigger onOpenApp function
|
||||
if (msg == "reopen") {
|
||||
@@ -1023,7 +1027,7 @@ int main(int argc, char *argv[]) {
|
||||
}
|
||||
else
|
||||
{
|
||||
engine.loadFromModule("linux", "Main");
|
||||
trayApp->loadMainModule();
|
||||
}
|
||||
}
|
||||
else
|
||||
|
||||
Reference in New Issue
Block a user