mirror of
https://github.com/kavishdevar/librepods.git
synced 2026-05-25 02:18:45 +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"
|
applicationId = "me.kavishdevar.librepods"
|
||||||
minSdk = 28
|
minSdk = 28
|
||||||
targetSdk = 35
|
targetSdk = 35
|
||||||
versionCode = 4
|
versionCode = 6
|
||||||
versionName = "0.1.0"
|
versionName = "0.1.0-rc.3"
|
||||||
}
|
}
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
|
|||||||
@@ -1,24 +1,26 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<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.BLUETOOTH_CONNECT" />
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_CONNECTED_DEVICE" />
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_CONNECTED_DEVICE" />
|
||||||
<uses-permission android:name="android.permission.INTERACT_ACROSS_USERS"
|
<uses-permission android:name="android.permission.INTERACT_ACROSS_USERS"
|
||||||
tools:ignore="ProtectedPermissions" />
|
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.POST_NOTIFICATIONS" />
|
||||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||||
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
|
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
|
||||||
<uses-permission
|
<uses-permission
|
||||||
android:name="android.permission.BLUETOOTH_PRIVILEGED"
|
android:name="android.permission.BLUETOOTH_PRIVILEGED"
|
||||||
tools:ignore="ProtectedPermissions" />
|
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" />
|
||||||
<uses-permission
|
<uses-permission
|
||||||
android:name="android.permission.BLUETOOTH_SCAN"
|
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.ANSWER_PHONE_CALLS" />
|
||||||
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
|
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<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
|
<application
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
@@ -40,6 +45,7 @@
|
|||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:theme="@style/Theme.LibrePods"
|
android:theme="@style/Theme.LibrePods"
|
||||||
|
android:description="@string/app_description"
|
||||||
tools:ignore="UnusedAttribute"
|
tools:ignore="UnusedAttribute"
|
||||||
tools:targetApi="31">
|
tools:targetApi="31">
|
||||||
<receiver
|
<receiver
|
||||||
@@ -121,6 +127,16 @@
|
|||||||
<action android:name="android.intent.action.MY_PACKAGE_REPLACED" />
|
<action android:name="android.intent.action.MY_PACKAGE_REPLACED" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</receiver>
|
</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>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
||||||
|
|||||||
@@ -32,9 +32,7 @@
|
|||||||
static HookFunType hook_func = nullptr;
|
static HookFunType hook_func = nullptr;
|
||||||
#define L2CEVT_L2CAP_CONFIG_REQ 4
|
#define L2CEVT_L2CAP_CONFIG_REQ 4
|
||||||
#define L2CEVT_L2CAP_CONFIG_RSP 15
|
#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;
|
struct t_l2c_lcb;
|
||||||
typedef struct _BT_HDR {
|
typedef struct _BT_HDR {
|
||||||
uint16_t event;
|
uint16_t event;
|
||||||
@@ -44,7 +42,6 @@ typedef struct _BT_HDR {
|
|||||||
uint8_t data[];
|
uint8_t data[];
|
||||||
} BT_HDR;
|
} BT_HDR;
|
||||||
|
|
||||||
// Define base FCR structures
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
uint8_t mode;
|
uint8_t mode;
|
||||||
uint8_t tx_win_sz;
|
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;
|
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) {
|
uint8_t fake_l2c_fcr_chk_chan_modes(void* p_ccb) {
|
||||||
LOGI("l2c_fcr_chk_chan_modes hooked");
|
LOGI("l2c_fcr_chk_chan_modes hooked, returning true.");
|
||||||
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);
|
|
||||||
|
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -107,6 +107,7 @@ import me.kavishdevar.librepods.screens.HeadTrackingScreen
|
|||||||
import me.kavishdevar.librepods.screens.LongPress
|
import me.kavishdevar.librepods.screens.LongPress
|
||||||
import me.kavishdevar.librepods.screens.Onboarding
|
import me.kavishdevar.librepods.screens.Onboarding
|
||||||
import me.kavishdevar.librepods.screens.RenameScreen
|
import me.kavishdevar.librepods.screens.RenameScreen
|
||||||
|
import me.kavishdevar.librepods.screens.TroubleshootingScreen
|
||||||
import me.kavishdevar.librepods.services.AirPodsService
|
import me.kavishdevar.librepods.services.AirPodsService
|
||||||
import me.kavishdevar.librepods.ui.theme.LibrePodsTheme
|
import me.kavishdevar.librepods.ui.theme.LibrePodsTheme
|
||||||
import me.kavishdevar.librepods.utils.AirPodsNotifications
|
import me.kavishdevar.librepods.utils.AirPodsNotifications
|
||||||
@@ -186,6 +187,8 @@ fun Main() {
|
|||||||
permissions = listOf(
|
permissions = listOf(
|
||||||
"android.permission.BLUETOOTH_CONNECT",
|
"android.permission.BLUETOOTH_CONNECT",
|
||||||
"android.permission.BLUETOOTH_SCAN",
|
"android.permission.BLUETOOTH_SCAN",
|
||||||
|
"android.permission.BLUETOOTH",
|
||||||
|
"android.permission.BLUETOOTH_ADMIN",
|
||||||
"android.permission.BLUETOOTH_ADVERTISE",
|
"android.permission.BLUETOOTH_ADVERTISE",
|
||||||
"android.permission.POST_NOTIFICATIONS",
|
"android.permission.POST_NOTIFICATIONS",
|
||||||
"android.permission.READ_PHONE_STATE",
|
"android.permission.READ_PHONE_STATE",
|
||||||
@@ -275,6 +278,9 @@ fun Main() {
|
|||||||
composable("app_settings") {
|
composable("app_settings") {
|
||||||
AppSettingsScreen(navController)
|
AppSettingsScreen(navController)
|
||||||
}
|
}
|
||||||
|
composable("troubleshooting") {
|
||||||
|
TroubleshootingScreen(navController)
|
||||||
|
}
|
||||||
composable("head_tracking") {
|
composable("head_tracking") {
|
||||||
HeadTrackingScreen(navController)
|
HeadTrackingScreen(navController)
|
||||||
}
|
}
|
||||||
@@ -405,7 +411,7 @@ fun PermissionsScreen(
|
|||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
|
||||||
Text(
|
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(
|
style = TextStyle(
|
||||||
fontSize = 16.sp,
|
fontSize = 16.sp,
|
||||||
fontWeight = FontWeight.Normal,
|
fontWeight = FontWeight.Normal,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
/*
|
/*
|
||||||
* LibrePods - AirPods liberated from Apple’s ecosystem
|
* 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
|
* 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
|
* it under the terms of the GNU Affero General Public License as published
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
/*
|
/*
|
||||||
* LibrePods - AirPods liberated from Apple’s ecosystem
|
* 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
|
* 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
|
* it under the terms of the GNU Affero General Public License as published
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
/*
|
/*
|
||||||
* LibrePods - AirPods liberated from Apple’s ecosystem
|
* 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
|
* 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
|
* 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.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.Info
|
import androidx.compose.material.icons.filled.Info
|
||||||
import androidx.compose.material.icons.filled.Settings
|
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.CenterAlignedTopAppBar
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
@@ -78,9 +82,11 @@ import androidx.compose.ui.unit.dp
|
|||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavController
|
||||||
import androidx.navigation.compose.rememberNavController
|
import androidx.navigation.compose.rememberNavController
|
||||||
|
import dev.chrisbanes.haze.HazeEffectScope
|
||||||
import dev.chrisbanes.haze.HazeState
|
import dev.chrisbanes.haze.HazeState
|
||||||
import dev.chrisbanes.haze.haze
|
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.CupertinoMaterials
|
||||||
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
|
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@@ -102,6 +108,7 @@ import me.kavishdevar.librepods.utils.AirPodsNotifications
|
|||||||
@Composable
|
@Composable
|
||||||
fun AirPodsSettingsScreen(dev: BluetoothDevice?, service: AirPodsService,
|
fun AirPodsSettingsScreen(dev: BluetoothDevice?, service: AirPodsService,
|
||||||
navController: NavController, isConnected: Boolean, isRemotelyConnected: Boolean) {
|
navController: NavController, isConnected: Boolean, isRemotelyConnected: Boolean) {
|
||||||
|
var isLocallyConnected by remember { mutableStateOf(isConnected) }
|
||||||
var isRemotelyConnected by remember { mutableStateOf(isRemotelyConnected) }
|
var isRemotelyConnected by remember { mutableStateOf(isRemotelyConnected) }
|
||||||
val sharedPreferences = LocalContext.current.getSharedPreferences("settings", MODE_PRIVATE)
|
val sharedPreferences = LocalContext.current.getSharedPreferences("settings", MODE_PRIVATE)
|
||||||
var device by remember { mutableStateOf(dev) }
|
var device by remember { mutableStateOf(dev) }
|
||||||
@@ -113,6 +120,10 @@ fun AirPodsSettingsScreen(dev: BluetoothDevice?, service: AirPodsService,
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(service) {
|
||||||
|
isLocallyConnected = service.isConnectedLocally
|
||||||
|
}
|
||||||
|
|
||||||
val nameChangeListener = remember {
|
val nameChangeListener = remember {
|
||||||
SharedPreferences.OnSharedPreferenceChangeListener { _, key ->
|
SharedPreferences.OnSharedPreferenceChangeListener { _, key ->
|
||||||
if (key == "name") {
|
if (key == "name") {
|
||||||
@@ -144,22 +155,37 @@ fun AirPodsSettingsScreen(dev: BluetoothDevice?, service: AirPodsService,
|
|||||||
}
|
}
|
||||||
|
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val bluetoothReceiver = remember {
|
|
||||||
|
val connectionReceiver = remember {
|
||||||
object : BroadcastReceiver() {
|
object : BroadcastReceiver() {
|
||||||
override fun onReceive(context: Context?, intent: Intent?) {
|
override fun onReceive(context: Context?, intent: Intent?) {
|
||||||
if (intent?.action == "me.kavishdevar.librepods.AIRPODS_CONNECTED_REMOTELY") {
|
when (intent?.action) {
|
||||||
coroutineScope.launch {
|
"me.kavishdevar.librepods.AIRPODS_CONNECTED_REMOTELY" -> {
|
||||||
handleRemoteConnection(true)
|
coroutineScope.launch {
|
||||||
|
handleRemoteConnection(true)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else if (intent?.action == "me.kavishdevar.librepods.AIRPODS_DISCONNECTED_REMOTELY") {
|
"me.kavishdevar.librepods.AIRPODS_DISCONNECTED_REMOTELY" -> {
|
||||||
coroutineScope.launch {
|
coroutineScope.launch {
|
||||||
handleRemoteConnection(false)
|
handleRemoteConnection(false)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else if (intent?.action == AirPodsNotifications.DISCONNECT_RECEIVERS) {
|
AirPodsNotifications.AIRPODS_CONNECTED -> {
|
||||||
try {
|
coroutineScope.launch {
|
||||||
context?.unregisterReceiver(this)
|
isLocallyConnected = true
|
||||||
} catch (e: IllegalArgumentException) {
|
}
|
||||||
e.printStackTrace()
|
}
|
||||||
|
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 {
|
val filter = IntentFilter().apply {
|
||||||
addAction("me.kavishdevar.librepods.AIRPODS_CONNECTED_REMOTELY")
|
addAction("me.kavishdevar.librepods.AIRPODS_CONNECTED_REMOTELY")
|
||||||
addAction("me.kavishdevar.librepods.AIRPODS_DISCONNECTED_REMOTELY")
|
addAction("me.kavishdevar.librepods.AIRPODS_DISCONNECTED_REMOTELY")
|
||||||
|
addAction(AirPodsNotifications.AIRPODS_CONNECTED)
|
||||||
|
addAction(AirPodsNotifications.AIRPODS_DISCONNECTED)
|
||||||
addAction(AirPodsNotifications.DISCONNECT_RECEIVERS)
|
addAction(AirPodsNotifications.DISCONNECT_RECEIVERS)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
context.registerReceiver(bluetoothReceiver, filter, RECEIVER_EXPORTED)
|
context.registerReceiver(connectionReceiver, filter, RECEIVER_EXPORTED)
|
||||||
} else {
|
} else {
|
||||||
context.registerReceiver(bluetoothReceiver, filter)
|
context.registerReceiver(connectionReceiver, filter)
|
||||||
}
|
}
|
||||||
onDispose {
|
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
|
modifier = Modifier
|
||||||
.hazeChild(
|
.hazeEffect(
|
||||||
state = hazeState,
|
state = hazeState,
|
||||||
style = CupertinoMaterials.thick(),
|
style = CupertinoMaterials.thick(),
|
||||||
block = {
|
block = fun HazeEffectScope.() {
|
||||||
alpha =
|
alpha =
|
||||||
if (verticalScrollState.value > 60.dp.value * mDensity.floatValue) 1f else 0f
|
if (verticalScrollState.value > 60.dp.value * mDensity.floatValue) 1f else 0f
|
||||||
}
|
})
|
||||||
)
|
|
||||||
.drawBehind {
|
.drawBehind {
|
||||||
mDensity.floatValue = density
|
mDensity.floatValue = density
|
||||||
val strokeWidth = 0.7.dp.value * density
|
val strokeWidth = 0.7.dp.value * density
|
||||||
@@ -266,10 +297,10 @@ fun AirPodsSettingsScreen(dev: BluetoothDevice?, service: AirPodsService,
|
|||||||
},
|
},
|
||||||
snackbarHost = { SnackbarHost(snackbarHostState) }
|
snackbarHost = { SnackbarHost(snackbarHostState) }
|
||||||
) { paddingValues ->
|
) { paddingValues ->
|
||||||
if (isConnected == true || isRemotelyConnected == true) {
|
if (isLocallyConnected || isRemotelyConnected) {
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.haze(hazeState)
|
.hazeSource(hazeState)
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.padding(horizontal = 16.dp)
|
.padding(horizontal = 16.dp)
|
||||||
.verticalScroll(
|
.verticalScroll(
|
||||||
@@ -387,6 +418,24 @@ fun AirPodsSettingsScreen(dev: BluetoothDevice?, service: AirPodsService,
|
|||||||
textAlign = TextAlign.Center,
|
textAlign = TextAlign.Center,
|
||||||
modifier = Modifier.fillMaxWidth()
|
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.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.automirrored.filled.KeyboardArrowLeft
|
import androidx.compose.material.icons.automirrored.filled.KeyboardArrowLeft
|
||||||
import androidx.compose.material.icons.filled.Refresh
|
import androidx.compose.material.icons.filled.Refresh
|
||||||
@@ -62,38 +64,86 @@ import androidx.compose.runtime.remember
|
|||||||
import androidx.compose.runtime.setValue
|
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.drawBehind
|
||||||
import androidx.compose.ui.draw.scale
|
import androidx.compose.ui.draw.scale
|
||||||
import androidx.compose.ui.draw.shadow
|
import androidx.compose.ui.draw.shadow
|
||||||
|
import androidx.compose.ui.geometry.Offset
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.TextStyle
|
import androidx.compose.ui.text.TextStyle
|
||||||
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.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.navigation.NavController
|
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.R
|
||||||
import me.kavishdevar.librepods.composables.IndependentToggle
|
|
||||||
import me.kavishdevar.librepods.composables.StyledSwitch
|
import me.kavishdevar.librepods.composables.StyledSwitch
|
||||||
import me.kavishdevar.librepods.services.ServiceManager
|
|
||||||
import me.kavishdevar.librepods.utils.RadareOffsetFinder
|
import me.kavishdevar.librepods.utils.RadareOffsetFinder
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class, ExperimentalHazeMaterialsApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun AppSettingsScreen(navController: NavController) {
|
fun AppSettingsScreen(navController: NavController) {
|
||||||
val sharedPreferences = LocalContext.current.getSharedPreferences("settings", Context.MODE_PRIVATE)
|
val sharedPreferences = LocalContext.current.getSharedPreferences("settings", Context.MODE_PRIVATE)
|
||||||
val name = remember { mutableStateOf(sharedPreferences.getString("name", "") ?: "") }
|
val name = remember { mutableStateOf(sharedPreferences.getString("name", "") ?: "") }
|
||||||
val isDarkTheme = isSystemInDarkTheme()
|
val isDarkTheme = isSystemInDarkTheme()
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
val scrollState = rememberScrollState()
|
||||||
|
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
|
||||||
|
val hazeState = remember { HazeState() }
|
||||||
|
|
||||||
var showResetDialog by remember { mutableStateOf(false) }
|
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(
|
Scaffold(
|
||||||
|
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||||
topBar = {
|
topBar = {
|
||||||
CenterAlignedTopAppBar(
|
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 = {
|
title = {
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(R.string.app_settings),
|
text = stringResource(R.string.app_settings),
|
||||||
@@ -106,6 +156,7 @@ fun AppSettingsScreen(navController: NavController) {
|
|||||||
navController.popBackStack()
|
navController.popBackStack()
|
||||||
},
|
},
|
||||||
shape = RoundedCornerShape(8.dp),
|
shape = RoundedCornerShape(8.dp),
|
||||||
|
modifier = Modifier.width(180.dp)
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
Icons.AutoMirrored.Filled.KeyboardArrowLeft,
|
Icons.AutoMirrored.Filled.KeyboardArrowLeft,
|
||||||
@@ -121,12 +172,16 @@ fun AppSettingsScreen(navController: NavController) {
|
|||||||
color = if (isDarkTheme) Color(0xFF007AFF) else Color(0xFF3C6DF5),
|
color = if (isDarkTheme) Color(0xFF007AFF) else Color(0xFF3C6DF5),
|
||||||
fontFamily = FontFamily(Font(R.font.sf_pro))
|
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
|
containerColor = Color.Transparent
|
||||||
)
|
),
|
||||||
|
scrollBehavior = scrollBehavior
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
containerColor = if (isSystemInDarkTheme()) Color(0xFF000000)
|
containerColor = if (isSystemInDarkTheme()) Color(0xFF000000)
|
||||||
@@ -136,68 +191,114 @@ fun AppSettingsScreen(navController: NavController) {
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.padding(paddingValues)
|
.padding(paddingValues)
|
||||||
.padding(horizontal = 12.dp)
|
.padding(horizontal = 16.dp)
|
||||||
|
.verticalScroll(scrollState)
|
||||||
|
.hazeSource(state = hazeState)
|
||||||
) {
|
) {
|
||||||
val isDarkTheme = isSystemInDarkTheme()
|
val isDarkTheme = isSystemInDarkTheme()
|
||||||
|
|
||||||
val backgroundColor = if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFFFFFFF)
|
val backgroundColor = if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFFFFFFF)
|
||||||
val textColor = if (isDarkTheme) Color.White else Color.Black
|
val textColor = if (isDarkTheme) Color.White else Color.Black
|
||||||
val 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 (
|
Column (
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.height(275.sp.value.dp)
|
|
||||||
.background(
|
.background(
|
||||||
backgroundColor,
|
backgroundColor,
|
||||||
RoundedCornerShape(14.dp)
|
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) }
|
val sliderValue = remember { mutableFloatStateOf(0f) }
|
||||||
LaunchedEffect(sliderValue) {
|
LaunchedEffect(sliderValue) {
|
||||||
if (sharedPreferences.contains("conversational_awareness_volume")) {
|
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) {
|
fun updateConversationalAwarenessPauseMusic(enabled: Boolean) {
|
||||||
conversationalAwarenessPauseMusicEnabled = enabled
|
conversationalAwarenessPauseMusicEnabled = enabled
|
||||||
sharedPreferences.edit().putBoolean("conversational_awareness_pause_music", enabled).apply()
|
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) {
|
fun updateRelativeConversationalAwarenessVolume(enabled: Boolean) {
|
||||||
relativeConversationalAwarenessVolumeEnabled = enabled
|
relativeConversationalAwarenessVolumeEnabled = enabled
|
||||||
sharedPreferences.edit().putBoolean("relative_conversational_awareness_volume", enabled).apply()
|
sharedPreferences.edit().putBoolean("relative_conversational_awareness_volume", enabled).apply()
|
||||||
@@ -206,11 +307,6 @@ fun AppSettingsScreen(navController: NavController) {
|
|||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.height(85.sp.value.dp)
|
|
||||||
.background(
|
|
||||||
shape = RoundedCornerShape(14.dp),
|
|
||||||
color = Color.Transparent
|
|
||||||
)
|
|
||||||
.clickable(
|
.clickable(
|
||||||
indication = null,
|
indication = null,
|
||||||
interactionSource = remember { MutableInteractionSource() }
|
interactionSource = remember { MutableInteractionSource() }
|
||||||
@@ -222,6 +318,7 @@ fun AppSettingsScreen(navController: NavController) {
|
|||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.weight(1f)
|
.weight(1f)
|
||||||
|
.padding(vertical = 8.dp)
|
||||||
.padding(end = 4.dp)
|
.padding(end = 4.dp)
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
@@ -249,11 +346,6 @@ fun AppSettingsScreen(navController: NavController) {
|
|||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.height(85.sp.value.dp)
|
|
||||||
.background(
|
|
||||||
shape = RoundedCornerShape(14.dp),
|
|
||||||
color = Color.Transparent
|
|
||||||
)
|
|
||||||
.clickable(
|
.clickable(
|
||||||
indication = null,
|
indication = null,
|
||||||
interactionSource = remember { MutableInteractionSource() }
|
interactionSource = remember { MutableInteractionSource() }
|
||||||
@@ -265,6 +357,7 @@ fun AppSettingsScreen(navController: NavController) {
|
|||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.weight(1f)
|
.weight(1f)
|
||||||
|
.padding(vertical = 8.dp)
|
||||||
.padding(end = 4.dp)
|
.padding(end = 4.dp)
|
||||||
) {
|
) {
|
||||||
Text(
|
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 activeTrackColor = if (isDarkTheme) Color(0xFF007AFF) else Color(0xFF3C6DF5)
|
||||||
|
val thumbColor = if (isDarkTheme) Color(0xFFFFFFFF) else Color(0xFFFFFFFF)
|
||||||
|
|
||||||
Slider(
|
Slider(
|
||||||
value = sliderValue.floatValue,
|
value = sliderValue.floatValue,
|
||||||
onValueChange = {
|
onValueChange = {
|
||||||
sliderValue.floatValue = it
|
sliderValue.floatValue = it
|
||||||
|
sharedPreferences.edit().putInt("conversational_awareness_volume", it.toInt()).apply()
|
||||||
},
|
},
|
||||||
valueRange = 10f..85f,
|
valueRange = 10f..85f,
|
||||||
onValueChangeFinished = {
|
onValueChangeFinished = {
|
||||||
sliderValue.floatValue = sliderValue.floatValue.roundToInt().toFloat()
|
sliderValue.floatValue = sliderValue.floatValue.roundToInt().toFloat()
|
||||||
},
|
},
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.weight(1f)
|
.fillMaxWidth()
|
||||||
.height(36.dp),
|
.height(36.dp)
|
||||||
|
.padding(vertical = 4.dp),
|
||||||
colors = SliderDefaults.colors(
|
colors = SliderDefaults.colors(
|
||||||
thumbColor = thumbColor,
|
thumbColor = thumbColor,
|
||||||
activeTrackColor = activeTrackColor,
|
activeTrackColor = activeTrackColor,
|
||||||
@@ -341,7 +445,9 @@ fun AppSettingsScreen(navController: NavController) {
|
|||||||
)
|
)
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(bottom = 8.dp),
|
||||||
horizontalArrangement = Arrangement.SpaceBetween
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
@@ -349,7 +455,7 @@ fun AppSettingsScreen(navController: NavController) {
|
|||||||
style = TextStyle(
|
style = TextStyle(
|
||||||
fontSize = 14.sp,
|
fontSize = 14.sp,
|
||||||
fontWeight = FontWeight.Light,
|
fontWeight = FontWeight.Light,
|
||||||
color = labelTextColor
|
color = textColor.copy(alpha = 0.7f)
|
||||||
),
|
),
|
||||||
modifier = Modifier.padding(start = 4.dp)
|
modifier = Modifier.padding(start = 4.dp)
|
||||||
)
|
)
|
||||||
@@ -358,14 +464,202 @@ fun AppSettingsScreen(navController: NavController) {
|
|||||||
style = TextStyle(
|
style = TextStyle(
|
||||||
fontSize = 14.sp,
|
fontSize = 14.sp,
|
||||||
fontWeight = FontWeight.Light,
|
fontWeight = FontWeight.Light,
|
||||||
color = labelTextColor
|
color = textColor.copy(alpha = 0.7f)
|
||||||
),
|
),
|
||||||
modifier = Modifier.padding(end = 4.dp)
|
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(
|
Button(
|
||||||
onClick = { showResetDialog = true },
|
onClick = { showResetDialog = true },
|
||||||
@@ -400,6 +694,8 @@ fun AppSettingsScreen(navController: NavController) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(32.dp))
|
||||||
|
|
||||||
if (showResetDialog) {
|
if (showResetDialog) {
|
||||||
AlertDialog(
|
AlertDialog(
|
||||||
onDismissRequest = { showResetDialog = false },
|
onDismissRequest = { showResetDialog = false },
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ import androidx.compose.foundation.layout.Spacer
|
|||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.layout.width
|
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.Icons
|
||||||
import androidx.compose.material.icons.automirrored.filled.KeyboardArrowLeft
|
import androidx.compose.material.icons.automirrored.filled.KeyboardArrowLeft
|
||||||
import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight
|
import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight
|
||||||
|
import androidx.compose.material.icons.filled.Check
|
||||||
import androidx.compose.material.icons.filled.Delete
|
import androidx.compose.material.icons.filled.Delete
|
||||||
import androidx.compose.material.icons.filled.MoreVert
|
import androidx.compose.material.icons.filled.MoreVert
|
||||||
import androidx.compose.material.icons.filled.Send
|
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.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavController
|
||||||
|
import dev.chrisbanes.haze.HazeEffectScope
|
||||||
import dev.chrisbanes.haze.HazeState
|
import dev.chrisbanes.haze.HazeState
|
||||||
import dev.chrisbanes.haze.haze
|
import dev.chrisbanes.haze.hazeEffect
|
||||||
import dev.chrisbanes.haze.hazeChild
|
import dev.chrisbanes.haze.hazeSource
|
||||||
import dev.chrisbanes.haze.materials.CupertinoMaterials
|
import dev.chrisbanes.haze.materials.CupertinoMaterials
|
||||||
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
|
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import me.kavishdevar.librepods.R
|
import me.kavishdevar.librepods.R
|
||||||
import me.kavishdevar.librepods.services.ServiceManager
|
import me.kavishdevar.librepods.services.ServiceManager
|
||||||
import me.kavishdevar.librepods.utils.BatteryStatus
|
import me.kavishdevar.librepods.utils.BatteryStatus
|
||||||
import me.kavishdevar.librepods.utils.isHeadTrackingData
|
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(
|
data class PacketInfo(
|
||||||
val type: String,
|
val type: String,
|
||||||
@@ -487,13 +474,12 @@ fun DebugScreen(navController: NavController) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
modifier = Modifier.hazeChild(
|
modifier = Modifier.hazeEffect(
|
||||||
state = hazeState,
|
state = hazeState,
|
||||||
style = CupertinoMaterials.thick(),
|
style = CupertinoMaterials.thick(),
|
||||||
block = {
|
block = fun HazeEffectScope.() {
|
||||||
alpha = if (scrollOffset > 0) 1f else 0f
|
alpha = if (scrollOffset > 0) 1f else 0f
|
||||||
}
|
}),
|
||||||
),
|
|
||||||
colors = TopAppBarDefaults.topAppBarColors(containerColor = Color.Transparent),
|
colors = TopAppBarDefaults.topAppBarColors(containerColor = Color.Transparent),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
@@ -502,7 +488,7 @@ fun DebugScreen(navController: NavController) {
|
|||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.haze(hazeState)
|
.hazeSource(hazeState)
|
||||||
.padding(top = paddingValues.calculateTopPadding())
|
.padding(top = paddingValues.calculateTopPadding())
|
||||||
.navigationBarsPadding()
|
.navigationBarsPadding()
|
||||||
) {
|
) {
|
||||||
@@ -643,6 +629,7 @@ fun DebugScreen(navController: NavController) {
|
|||||||
scrollOffset = 0
|
scrollOffset = 0
|
||||||
)
|
)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
listState.scrollToItem(
|
listState.scrollToItem(
|
||||||
index = (packetLogs.size - 1).coerceAtLeast(0)
|
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.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
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.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.automirrored.filled.KeyboardArrowLeft
|
import androidx.compose.material.icons.automirrored.filled.KeyboardArrowLeft
|
||||||
import androidx.compose.material.icons.filled.PlayArrow
|
import androidx.compose.material.icons.filled.PlayArrow
|
||||||
@@ -69,6 +72,7 @@ import androidx.compose.runtime.rememberCoroutineScope
|
|||||||
import androidx.compose.runtime.setValue
|
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.drawBehind
|
||||||
import androidx.compose.ui.draw.scale
|
import androidx.compose.ui.draw.scale
|
||||||
import androidx.compose.ui.geometry.CornerRadius
|
import androidx.compose.ui.geometry.CornerRadius
|
||||||
import androidx.compose.ui.geometry.Offset
|
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.toArgb
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
import androidx.compose.ui.graphics.vector.path
|
import androidx.compose.ui.graphics.vector.path
|
||||||
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.TextStyle
|
import androidx.compose.ui.text.TextStyle
|
||||||
@@ -92,10 +97,17 @@ 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.rememberTextMeasurer
|
import androidx.compose.ui.text.rememberTextMeasurer
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
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.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
|
||||||
import androidx.navigation.NavController
|
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.CoroutineScope
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@@ -108,6 +120,7 @@ import kotlin.math.cos
|
|||||||
import kotlin.math.sin
|
import kotlin.math.sin
|
||||||
import kotlin.random.Random
|
import kotlin.random.Random
|
||||||
|
|
||||||
|
@ExperimentalHazeMaterialsApi
|
||||||
@RequiresApi(Build.VERSION_CODES.Q)
|
@RequiresApi(Build.VERSION_CODES.Q)
|
||||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalAnimationApi::class)
|
@OptIn(ExperimentalMaterial3Api::class, ExperimentalAnimationApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
@@ -122,9 +135,36 @@ fun HeadTrackingScreen(navController: NavController) {
|
|||||||
val isDarkTheme = isSystemInDarkTheme()
|
val isDarkTheme = isSystemInDarkTheme()
|
||||||
val backgroundColor = if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFFFFFFF)
|
val backgroundColor = if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFFFFFFF)
|
||||||
val textColor = if (isDarkTheme) Color.White else Color.Black
|
val textColor = if (isDarkTheme) Color.White else Color.Black
|
||||||
|
|
||||||
|
val scrollState = rememberScrollState()
|
||||||
|
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
|
||||||
|
val hazeState = remember { HazeState() }
|
||||||
|
|
||||||
|
var mDensity by remember { mutableFloatStateOf(0f) }
|
||||||
Scaffold(
|
Scaffold(
|
||||||
|
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||||
topBar = {
|
topBar = {
|
||||||
CenterAlignedTopAppBar(
|
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 = {
|
title = {
|
||||||
Text(
|
Text(
|
||||||
stringResource(R.string.head_tracking),
|
stringResource(R.string.head_tracking),
|
||||||
@@ -138,6 +178,7 @@ fun HeadTrackingScreen(navController: NavController) {
|
|||||||
if (ServiceManager.getService()?.isHeadTrackingActive == true) ServiceManager.getService()?.stopHeadTracking()
|
if (ServiceManager.getService()?.isHeadTrackingActive == true) ServiceManager.getService()?.stopHeadTracking()
|
||||||
},
|
},
|
||||||
shape = RoundedCornerShape(8.dp),
|
shape = RoundedCornerShape(8.dp),
|
||||||
|
modifier = Modifier.width(180.dp)
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
Icons.AutoMirrored.Filled.KeyboardArrowLeft,
|
Icons.AutoMirrored.Filled.KeyboardArrowLeft,
|
||||||
@@ -153,10 +194,13 @@ fun HeadTrackingScreen(navController: NavController) {
|
|||||||
color = if (isDarkTheme) Color(0xFF007AFF) else Color(0xFF3C6DF5),
|
color = if (isDarkTheme) Color(0xFF007AFF) else Color(0xFF3C6DF5),
|
||||||
fontFamily = FontFamily(Font(R.font.sf_pro))
|
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
|
containerColor = Color.Transparent
|
||||||
),
|
),
|
||||||
actions = {
|
actions = {
|
||||||
@@ -205,7 +249,8 @@ fun HeadTrackingScreen(navController: NavController) {
|
|||||||
modifier = Modifier.scale(1.5f)
|
modifier = Modifier.scale(1.5f)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
scrollBehavior = scrollBehavior
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
containerColor = if (isSystemInDarkTheme()) Color(0xFF000000)
|
containerColor = if (isSystemInDarkTheme()) Color(0xFF000000)
|
||||||
@@ -217,6 +262,8 @@ fun HeadTrackingScreen(navController: NavController) {
|
|||||||
.padding(paddingValues = paddingValues)
|
.padding(paddingValues = paddingValues)
|
||||||
.padding(horizontal = 16.dp)
|
.padding(horizontal = 16.dp)
|
||||||
.padding(top = 8.dp)
|
.padding(top = 8.dp)
|
||||||
|
.verticalScroll(scrollState)
|
||||||
|
.hazeSource(state = hazeState)
|
||||||
) {
|
) {
|
||||||
val sharedPreferences =
|
val sharedPreferences =
|
||||||
LocalContext.current.getSharedPreferences("settings", Context.MODE_PRIVATE)
|
LocalContext.current.getSharedPreferences("settings", Context.MODE_PRIVATE)
|
||||||
@@ -305,7 +352,7 @@ fun HeadTrackingScreen(navController: NavController) {
|
|||||||
|
|
||||||
Box(
|
Box(
|
||||||
contentAlignment = Alignment.Center,
|
contentAlignment = Alignment.Center,
|
||||||
modifier = Modifier.padding(top = 12.dp)
|
modifier = Modifier.padding(top = 12.dp, bottom = 24.dp)
|
||||||
) {
|
) {
|
||||||
AnimatedContent(
|
AnimatedContent(
|
||||||
targetState = gestureText,
|
targetState = gestureText,
|
||||||
@@ -800,6 +847,7 @@ private fun AccelerationPlot() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ExperimentalHazeMaterialsApi
|
||||||
@RequiresApi(Build.VERSION_CODES.Q)
|
@RequiresApi(Build.VERSION_CODES.Q)
|
||||||
@Preview
|
@Preview
|
||||||
@Composable
|
@Composable
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,7 @@
|
|||||||
/*
|
/*
|
||||||
* LibrePods - AirPods liberated from Apple’s ecosystem
|
* 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
|
* 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
|
* 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.service.quicksettings.TileService
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import me.kavishdevar.librepods.MainActivity
|
||||||
import me.kavishdevar.librepods.QuickSettingsDialogActivity
|
import me.kavishdevar.librepods.QuickSettingsDialogActivity
|
||||||
import me.kavishdevar.librepods.R
|
import me.kavishdevar.librepods.R
|
||||||
import me.kavishdevar.librepods.utils.AirPodsNotifications
|
import me.kavishdevar.librepods.utils.AirPodsNotifications
|
||||||
@@ -260,4 +262,42 @@ class AirPodsQSService : TileService() {
|
|||||||
else -> R.drawable.airpods
|
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
|
batteryBytes = trimmedPacket
|
||||||
ServiceManager.getService()?.batteryNotification?.setBattery(trimmedPacket)
|
ServiceManager.getService()?.batteryNotification?.setBattery(trimmedPacket)
|
||||||
Log.d("CrossDevice", "Battery data: ${ServiceManager.getService()?.batteryNotification?.getBattery()[0]?.level}")
|
Log.d("CrossDevice", "Battery data: ${ServiceManager.getService()?.batteryNotification?.getBattery()[0]?.level}")
|
||||||
ServiceManager.getService()?.updateBatteryWidget()
|
ServiceManager.getService()?.updateBattery()
|
||||||
ServiceManager.getService()?.sendBatteryBroadcast()
|
ServiceManager.getService()?.sendBatteryBroadcast()
|
||||||
ServiceManager.getService()?.sendBatteryNotification()
|
ServiceManager.getService()?.sendBatteryNotification()
|
||||||
} else if (ServiceManager.getService()?.ancNotification?.isANCData(trimmedPacket) == true) {
|
} 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.content.SharedPreferences
|
||||||
import android.media.AudioManager
|
import android.media.AudioManager
|
||||||
import android.media.AudioPlaybackConfiguration
|
import android.media.AudioPlaybackConfiguration
|
||||||
|
import android.os.Build
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.view.KeyEvent
|
import android.view.KeyEvent
|
||||||
|
import androidx.annotation.RequiresApi
|
||||||
import me.kavishdevar.librepods.services.ServiceManager
|
import me.kavishdevar.librepods.services.ServiceManager
|
||||||
|
|
||||||
object MediaController {
|
object MediaController {
|
||||||
@@ -34,11 +36,12 @@ object MediaController {
|
|||||||
var userPlayedTheMedia = false
|
var userPlayedTheMedia = false
|
||||||
private lateinit var sharedPreferences: SharedPreferences
|
private lateinit var sharedPreferences: SharedPreferences
|
||||||
private val handler = Handler(Looper.getMainLooper())
|
private val handler = Handler(Looper.getMainLooper())
|
||||||
|
private lateinit var preferenceChangeListener: SharedPreferences.OnSharedPreferenceChangeListener
|
||||||
|
|
||||||
var pausedForCrossDevice = false
|
var pausedForCrossDevice = false
|
||||||
|
|
||||||
private var relativeVolume: Boolean = false
|
private var relativeVolume: Boolean = false
|
||||||
private var conversationalAwarenessVolume: Int = 1/12
|
private var conversationalAwarenessVolume: Int = 2
|
||||||
private var conversationalAwarenessPauseMusic: Boolean = false
|
private var conversationalAwarenessPauseMusic: Boolean = false
|
||||||
|
|
||||||
fun initialize(audioManager: AudioManager, sharedPreferences: SharedPreferences) {
|
fun initialize(audioManager: AudioManager, sharedPreferences: SharedPreferences) {
|
||||||
@@ -49,16 +52,16 @@ object MediaController {
|
|||||||
this.sharedPreferences = sharedPreferences
|
this.sharedPreferences = sharedPreferences
|
||||||
Log.d("MediaController", "Initializing MediaController")
|
Log.d("MediaController", "Initializing MediaController")
|
||||||
relativeVolume = sharedPreferences.getBoolean("relative_conversational_awareness_volume", false)
|
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)
|
conversationalAwarenessPauseMusic = sharedPreferences.getBoolean("conversational_awareness_pause_music", false)
|
||||||
|
|
||||||
sharedPreferences.registerOnSharedPreferenceChangeListener { _, key ->
|
preferenceChangeListener = SharedPreferences.OnSharedPreferenceChangeListener { _, key ->
|
||||||
when (key) {
|
when (key) {
|
||||||
"relative_conversational_awareness_volume" -> {
|
"relative_conversational_awareness_volume" -> {
|
||||||
relativeVolume = sharedPreferences.getBoolean("relative_conversational_awareness_volume", false)
|
relativeVolume = sharedPreferences.getBoolean("relative_conversational_awareness_volume", false)
|
||||||
}
|
}
|
||||||
"conversational_awareness_volume" -> {
|
"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" -> {
|
"conversational_awareness_pause_music" -> {
|
||||||
conversationalAwarenessPauseMusic = sharedPreferences.getBoolean("conversational_awareness_pause_music", false)
|
conversationalAwarenessPauseMusic = sharedPreferences.getBoolean("conversational_awareness_pause_music", false)
|
||||||
@@ -66,17 +69,19 @@ object MediaController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sharedPreferences.registerOnSharedPreferenceChangeListener(preferenceChangeListener)
|
||||||
|
|
||||||
audioManager.registerAudioPlaybackCallback(cb, null)
|
audioManager.registerAudioPlaybackCallback(cb, null)
|
||||||
}
|
}
|
||||||
|
|
||||||
val cb = object : AudioManager.AudioPlaybackCallback() {
|
val cb = object : AudioManager.AudioPlaybackCallback() {
|
||||||
|
@RequiresApi(Build.VERSION_CODES.R)
|
||||||
override fun onPlaybackConfigChanged(configs: MutableList<AudioPlaybackConfiguration>?) {
|
override fun onPlaybackConfigChanged(configs: MutableList<AudioPlaybackConfiguration>?) {
|
||||||
super.onPlaybackConfigChanged(configs)
|
super.onPlaybackConfigChanged(configs)
|
||||||
Log.d("MediaController", "Playback config changed, iPausedTheMedia: $iPausedTheMedia")
|
Log.d("MediaController", "Playback config changed, iPausedTheMedia: $iPausedTheMedia")
|
||||||
if (configs != null && !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.")
|
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({
|
handler.postDelayed({
|
||||||
iPausedTheMedia = !audioManager.isMusicActive
|
|
||||||
userPlayedTheMedia = audioManager.isMusicActive
|
userPlayedTheMedia = audioManager.isMusicActive
|
||||||
}, 7) // i have no idea why android sends an event a hundred times after the user does something.
|
}, 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
|
@Synchronized
|
||||||
fun sendPause(force: Boolean = false) {
|
fun sendPause(force: Boolean = false) {
|
||||||
Log.d("MediaController", "Sending pause with iPausedTheMedia: $iPausedTheMedia, userPlayedTheMedia: $userPlayedTheMedia")
|
Log.d("MediaController", "Sending pause with iPausedTheMedia: $iPausedTheMedia, userPlayedTheMedia: $userPlayedTheMedia, isMusicActive: ${audioManager.isMusicActive}, force: $force")
|
||||||
if ((audioManager.isMusicActive && !userPlayedTheMedia) || force) {
|
if ((audioManager.isMusicActive) && (!userPlayedTheMedia || force)) {
|
||||||
iPausedTheMedia = true
|
iPausedTheMedia = if (force) audioManager.isMusicActive else true
|
||||||
userPlayedTheMedia = false
|
userPlayedTheMedia = false
|
||||||
audioManager.dispatchMediaKeyEvent(
|
audioManager.dispatchMediaKeyEvent(
|
||||||
KeyEvent(
|
KeyEvent(
|
||||||
@@ -134,11 +139,18 @@ object MediaController {
|
|||||||
|
|
||||||
@Synchronized
|
@Synchronized
|
||||||
fun startSpeaking() {
|
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) {
|
if (initialVolume == null) {
|
||||||
initialVolume = audioManager.getStreamVolume(AudioManager.STREAM_MUSIC)
|
initialVolume = audioManager.getStreamVolume(AudioManager.STREAM_MUSIC)
|
||||||
Log.d("MediaController", "Initial Volume Set: $initialVolume")
|
Log.d("MediaController", "Initial Volume: $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!!
|
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())
|
smoothVolumeTransition(initialVolume!!, targetVolume.toInt())
|
||||||
if (conversationalAwarenessPauseMusic) {
|
if (conversationalAwarenessPauseMusic) {
|
||||||
sendPause(force = true)
|
sendPause(force = true)
|
||||||
@@ -160,6 +172,7 @@ object MediaController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun smoothVolumeTransition(fromVolume: Int, toVolume: Int) {
|
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 step = if (fromVolume < toVolume) 1 else -1
|
||||||
val delay = 50L
|
val delay = 50L
|
||||||
var currentVolume = fromVolume
|
var currentVolume = fromVolume
|
||||||
|
|||||||
@@ -24,8 +24,12 @@ import android.animation.AnimatorListenerAdapter
|
|||||||
import android.animation.ObjectAnimator
|
import android.animation.ObjectAnimator
|
||||||
import android.animation.PropertyValuesHolder
|
import android.animation.PropertyValuesHolder
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
|
import android.content.BroadcastReceiver
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.IntentFilter
|
||||||
import android.graphics.PixelFormat
|
import android.graphics.PixelFormat
|
||||||
|
import android.os.Build
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
@@ -51,6 +55,7 @@ class PopupWindow(
|
|||||||
private var isClosing = false
|
private var isClosing = false
|
||||||
private var autoCloseHandler = Handler(Looper.getMainLooper())
|
private var autoCloseHandler = Handler(Looper.getMainLooper())
|
||||||
private var autoCloseRunnable: Runnable? = null
|
private var autoCloseRunnable: Runnable? = null
|
||||||
|
private var batteryUpdateReceiver: BroadcastReceiver? = null
|
||||||
|
|
||||||
@Suppress("DEPRECATION")
|
@Suppress("DEPRECATION")
|
||||||
private val mParams: WindowManager.LayoutParams = WindowManager.LayoutParams().apply {
|
private val mParams: WindowManager.LayoutParams = WindowManager.LayoutParams().apply {
|
||||||
@@ -146,6 +151,8 @@ class PopupWindow(
|
|||||||
start()
|
start()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
registerBatteryUpdateReceiver()
|
||||||
|
|
||||||
autoCloseRunnable = Runnable { close() }
|
autoCloseRunnable = Runnable { close() }
|
||||||
autoCloseHandler.postDelayed(autoCloseRunnable!!, 12000)
|
autoCloseHandler.postDelayed(autoCloseRunnable!!, 12000)
|
||||||
}
|
}
|
||||||
@@ -155,15 +162,43 @@ class PopupWindow(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("SetTextI18n")
|
private fun registerBatteryUpdateReceiver() {
|
||||||
fun updateBatteryStatus(batteryNotification: AirPodsNotifications.BatteryNotification) {
|
batteryUpdateReceiver = object : BroadcastReceiver() {
|
||||||
val batteryStatus = batteryNotification.getBattery()
|
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 batteryLeftText = mView.findViewById<TextView>(R.id.left_battery)
|
||||||
val batteryRightText = mView.findViewById<TextView>(R.id.right_battery)
|
val batteryRightText = mView.findViewById<TextView>(R.id.right_battery)
|
||||||
val batteryCaseText = mView.findViewById<TextView>(R.id.case_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) {
|
if (it.status != BatteryStatus.DISCONNECTED) {
|
||||||
"\uDBC3\uDC8E ${it.level}%"
|
"\uDBC3\uDC8E ${it.level}%"
|
||||||
} else {
|
} 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) {
|
if (it.status != BatteryStatus.DISCONNECTED) {
|
||||||
"\uDBC3\uDC8D ${it.level}%"
|
"\uDBC3\uDC8D ${it.level}%"
|
||||||
} else {
|
} 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) {
|
if (it.status != BatteryStatus.DISCONNECTED) {
|
||||||
"\uDBC3\uDE6C ${it.level}%"
|
"\uDBC3\uDE6C ${it.level}%"
|
||||||
} else {
|
} else {
|
||||||
@@ -188,12 +223,19 @@ class PopupWindow(
|
|||||||
} ?: ""
|
} ?: ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressLint("SetTextI18s")
|
||||||
|
fun updateBatteryStatus(batteryNotification: AirPodsNotifications.BatteryNotification) {
|
||||||
|
val batteryStatus = batteryNotification.getBattery()
|
||||||
|
updateBatteryStatusFromList(batteryStatus)
|
||||||
|
}
|
||||||
|
|
||||||
fun close() {
|
fun close() {
|
||||||
try {
|
try {
|
||||||
if (isClosing) return
|
if (isClosing) return
|
||||||
isClosing = true
|
isClosing = true
|
||||||
|
|
||||||
autoCloseRunnable?.let { autoCloseHandler.removeCallbacks(it) }
|
autoCloseRunnable?.let { autoCloseHandler.removeCallbacks(it) }
|
||||||
|
unregisterBatteryUpdateReceiver()
|
||||||
|
|
||||||
val vid = mView.findViewById<VideoView>(R.id.video)
|
val vid = mView.findViewById<VideoView>(R.id.video)
|
||||||
vid.stopPlayback()
|
vid.stopPlayback()
|
||||||
|
|||||||
@@ -416,9 +416,9 @@ class RadareOffsetFinder(context: Context) {
|
|||||||
Log.e(TAG, "rabin2 command failed with exit code $exitCode")
|
Log.e(TAG, "rabin2 command failed with exit code $exitCode")
|
||||||
}
|
}
|
||||||
|
|
||||||
// findAndSaveL2cuProcessCfgReqOffset(libraryPath, envSetup)
|
// findAndSaveL2cuProcessCfgReqOffset(libraryPath, envSetup)
|
||||||
// findAndSaveL2cCsmConfigOffset(libraryPath, envSetup)
|
// findAndSaveL2cCsmConfigOffset(libraryPath, envSetup)
|
||||||
findAndSaveL2cuSendPeerInfoReqOffset(libraryPath, envSetup)
|
// findAndSaveL2cuSendPeerInfoReqOffset(libraryPath, envSetup)
|
||||||
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Failed to find function offset", e)
|
Log.e(TAG, "Failed to find function offset", e)
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
package me.kavishdevar.librepods.utils
|
package me.kavishdevar.librepods.utils
|
||||||
|
|
||||||
import android.bluetooth.BluetoothDevice
|
import android.bluetooth.BluetoothDevice
|
||||||
|
import android.util.Log
|
||||||
|
import org.lsposed.hiddenapibypass.HiddenApiBypass
|
||||||
|
|
||||||
object SystemApisUtils {
|
object SystemApisUtils {
|
||||||
|
|
||||||
@@ -282,4 +284,23 @@ object SystemApisUtils {
|
|||||||
const val ACTION_BLUETOOTH_HANDSFREE_BATTERY_CHANGED = "android.intent.action.BLUETOOTH_HANDSFREE_BATTERY_CHANGED"
|
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_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"
|
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
|
package me.kavishdevar.librepods.widgets
|
||||||
|
|
||||||
import android.app.PendingIntent
|
|
||||||
import android.appwidget.AppWidgetManager
|
import android.appwidget.AppWidgetManager
|
||||||
import android.appwidget.AppWidgetProvider
|
import android.appwidget.AppWidgetProvider
|
||||||
import android.content.Context
|
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
|
import me.kavishdevar.librepods.services.ServiceManager
|
||||||
|
|
||||||
class BatteryWidget : AppWidgetProvider() {
|
class BatteryWidget : AppWidgetProvider() {
|
||||||
@@ -36,6 +30,6 @@ class BatteryWidget : AppWidgetProvider() {
|
|||||||
appWidgetManager: AppWidgetManager,
|
appWidgetManager: AppWidgetManager,
|
||||||
appWidgetIds: IntArray
|
appWidgetIds: IntArray
|
||||||
) {
|
) {
|
||||||
ServiceManager.getService()?.updateBatteryWidget()
|
ServiceManager.getService()?.updateBattery()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import android.appwidget.AppWidgetManager
|
|||||||
import android.appwidget.AppWidgetProvider
|
import android.appwidget.AppWidgetProvider
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.util.Log
|
||||||
import android.widget.RemoteViews
|
import android.widget.RemoteViews
|
||||||
import me.kavishdevar.librepods.R
|
import me.kavishdevar.librepods.R
|
||||||
import me.kavishdevar.librepods.services.ServiceManager
|
import me.kavishdevar.librepods.services.ServiceManager
|
||||||
@@ -77,6 +78,7 @@ class NoiseControlWidget : AppWidgetProvider() {
|
|||||||
super.onReceive(context, intent)
|
super.onReceive(context, intent)
|
||||||
if (intent.action == "ACTION_SET_ANC_MODE") {
|
if (intent.action == "ACTION_SET_ANC_MODE") {
|
||||||
val mode = intent.getIntExtra("ANC_MODE", 1)
|
val mode = intent.getIntExtra("ANC_MODE", 1)
|
||||||
|
Log.d("NoiseControlWidget", "Setting ANC mode to $mode")
|
||||||
ServiceManager.getService()?.setANCMode(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>
|
<resources>
|
||||||
<string name="app_name" translatable="false">LibrePods</string>
|
<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="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="app_widget_description">See your AirPods battery status right from your home screen!</string>
|
||||||
<string name="accessibility">Accessibility</string>
|
<string name="accessibility">Accessibility</string>
|
||||||
@@ -34,8 +35,7 @@
|
|||||||
<string name="airpods_not_connected">AirPods not connected</string>
|
<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="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="back">Back</string>
|
||||||
<string name="app_settings">App Settings</string>
|
<string name="app_settings">Customizations</string>
|
||||||
<string name="conversational_awareness_customization">Conversational Awareness</string>
|
|
||||||
<string name="relative_conversational_awareness_volume">Relative volume</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="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>
|
<string name="conversational_awareness_pause_music">Pause Music</string>
|
||||||
@@ -59,4 +59,9 @@
|
|||||||
<string name="ear_detection">Automatic Ear Detection</string>
|
<string name="ear_detection">Automatic Ear Detection</string>
|
||||||
<string name="auto_play">Auto Play</string>
|
<string name="auto_play">Auto Play</string>
|
||||||
<string name="auto_pause">Auto Pause</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>
|
</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)
|
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
|
qt_add_executable(applinux
|
||||||
main.cpp
|
main.cpp
|
||||||
|
|||||||
@@ -14,7 +14,13 @@ A native Linux application to control your AirPods, with support for:
|
|||||||
2. Qt6 packages
|
2. Qt6 packages
|
||||||
|
|
||||||
```bash
|
```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
|
## Setup
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ set(CMAKE_AUTOMOC ON)
|
|||||||
set(CMAKE_AUTORCC ON)
|
set(CMAKE_AUTORCC ON)
|
||||||
set(CMAKE_AUTOUIC 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
|
qt_add_executable(ble_monitor
|
||||||
main.cpp
|
main.cpp
|
||||||
|
|||||||
@@ -367,7 +367,7 @@ private slots:
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
parent->loadFromModule("linux", "Main");
|
loadMainModule();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -379,7 +379,7 @@ private slots:
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
parent->loadFromModule("linux", "Main");
|
loadMainModule();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -908,6 +908,10 @@ private slots:
|
|||||||
connectToPhone();
|
connectToPhone();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void loadMainModule() {
|
||||||
|
parent->load(QUrl(QStringLiteral("qrc:/linux/Main.qml")));
|
||||||
|
}
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void noiseControlModeChanged(NoiseControlMode mode);
|
void noiseControlModeChanged(NoiseControlMode mode);
|
||||||
void earDetectionStatusChanged(const QString &status);
|
void earDetectionStatusChanged(const QString &status);
|
||||||
@@ -995,7 +999,7 @@ int main(int argc, char *argv[]) {
|
|||||||
qmlRegisterType<Battery>("me.kavishdevar.Battery", 1, 0, "Battery");
|
qmlRegisterType<Battery>("me.kavishdevar.Battery", 1, 0, "Battery");
|
||||||
AirPodsTrayApp *trayApp = new AirPodsTrayApp(debugMode, hideOnStart, &engine);
|
AirPodsTrayApp *trayApp = new AirPodsTrayApp(debugMode, hideOnStart, &engine);
|
||||||
engine.rootContext()->setContextProperty("airPodsTrayApp", trayApp);
|
engine.rootContext()->setContextProperty("airPodsTrayApp", trayApp);
|
||||||
engine.loadFromModule("linux", "Main");
|
trayApp->loadMainModule();
|
||||||
|
|
||||||
QLocalServer server;
|
QLocalServer server;
|
||||||
QLocalServer::removeServer("app_server");
|
QLocalServer::removeServer("app_server");
|
||||||
@@ -1012,7 +1016,7 @@ int main(int argc, char *argv[]) {
|
|||||||
QObject::connect(&server, &QLocalServer::newConnection, [&]() {
|
QObject::connect(&server, &QLocalServer::newConnection, [&]() {
|
||||||
QLocalSocket* socket = server.nextPendingConnection();
|
QLocalSocket* socket = server.nextPendingConnection();
|
||||||
// Handles Proper Connection
|
// Handles Proper Connection
|
||||||
QObject::connect(socket, &QLocalSocket::readyRead, [socket, &engine]() {
|
QObject::connect(socket, &QLocalSocket::readyRead, [socket, &engine, &trayApp]() {
|
||||||
QString msg = socket->readAll();
|
QString msg = socket->readAll();
|
||||||
// Check if the message is "reopen", if so, trigger onOpenApp function
|
// Check if the message is "reopen", if so, trigger onOpenApp function
|
||||||
if (msg == "reopen") {
|
if (msg == "reopen") {
|
||||||
@@ -1023,7 +1027,7 @@ int main(int argc, char *argv[]) {
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
engine.loadFromModule("linux", "Main");
|
trayApp->loadMainModule();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
|||||||
Reference in New Issue
Block a user