diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts
index eea0a24..07507d6 100644
--- a/android/app/build.gradle.kts
+++ b/android/app/build.gradle.kts
@@ -28,7 +28,7 @@ android {
applicationId = "me.kavishdevar.librepods"
minSdk = 33
targetSdk = 37
- versionCode = 34
+ versionCode = 36
versionName = "0.2.3"
}
buildTypes {
@@ -50,14 +50,17 @@ android {
debug {
buildConfigField("Boolean", "PLAY_BUILD", "false")
signingConfig = signingConfigs.getByName("release")
+ versionNameSuffix = "-debug"
}
create("playRelease") {
initWith(getByName("release"))
buildConfigField("Boolean", "PLAY_BUILD", "true")
+ versionNameSuffix = "-play"
}
create("playDebug") {
initWith(getByName("debug"))
buildConfigField("Boolean", "PLAY_BUILD", "true")
+ versionNameSuffix = "-youshouldnothavethis"
}
}
compileOptions {
@@ -104,7 +107,6 @@ android {
arguments += "-DIS_XPOSED=ON"
}
}
- versionNameSuffix = "-xposed"
}
}
}
@@ -113,6 +115,7 @@ dependencies {
implementation(platform(libs.androidx.compose.bom))
implementation(libs.accompanist.permissions)
implementation(libs.androidx.core.ktx)
+ implementation(libs.androidx.lifecycle.process)
implementation(libs.androidx.lifecycle.runtime.ktx)
implementation(libs.androidx.activity.compose)
implementation(libs.androidx.ui)
diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index 428ea3e..0474dfd 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -14,9 +14,18 @@
-
-
-
+
+
+
+
-
diff --git a/android/app/src/main/java/me/kavishdevar/librepods/MainActivity.kt b/android/app/src/main/java/me/kavishdevar/librepods/MainActivity.kt
index d3b058d..368a9cb 100644
--- a/android/app/src/main/java/me/kavishdevar/librepods/MainActivity.kt
+++ b/android/app/src/main/java/me/kavishdevar/librepods/MainActivity.kt
@@ -52,7 +52,6 @@ import androidx.compose.animation.slideInHorizontally
import androidx.compose.animation.slideOutHorizontally
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.background
-import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
@@ -64,7 +63,6 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
@@ -81,8 +79,6 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableIntStateOf
-import androidx.compose.runtime.mutableLongStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
@@ -94,8 +90,6 @@ import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.drawscope.rotate
import androidx.compose.ui.graphics.vector.ImageVector
-import androidx.compose.ui.input.pointer.PointerEventPass
-import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalWindowInfo
import androidx.compose.ui.res.stringResource
@@ -122,12 +116,11 @@ import com.kyant.backdrop.backdrops.rememberLayerBackdrop
import dev.chrisbanes.haze.hazeSource
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
import dev.chrisbanes.haze.rememberHazeState
-import kotlinx.coroutines.delay
-import me.kavishdevar.librepods.billing.BillingManager
-import me.kavishdevar.librepods.billing.BillingProviderFactory
import me.kavishdevar.librepods.data.AirPodsNotifications
import me.kavishdevar.librepods.data.ControlCommandRepository
import me.kavishdevar.librepods.presentation.components.ConfirmationDialog
+import me.kavishdevar.librepods.presentation.components.DeviceInfoCard
+import me.kavishdevar.librepods.presentation.components.PlayBypassSheet
import me.kavishdevar.librepods.presentation.components.StyledButton
import me.kavishdevar.librepods.presentation.components.StyledIconButton
import me.kavishdevar.librepods.presentation.screens.AccessibilitySettingsScreen
@@ -148,6 +141,7 @@ import me.kavishdevar.librepods.presentation.screens.TransparencySettingsScreen
import me.kavishdevar.librepods.presentation.screens.TroubleshootingScreen
import me.kavishdevar.librepods.presentation.screens.UpdateHearingTestScreen
import me.kavishdevar.librepods.presentation.screens.VersionScreen
+import me.kavishdevar.librepods.presentation.theme.LibrePodsTheme
import me.kavishdevar.librepods.presentation.viewmodel.AirPodsViewModel
import me.kavishdevar.librepods.presentation.viewmodel.AppSettingsViewModel
import me.kavishdevar.librepods.presentation.viewmodel.PurchaseViewModel
@@ -175,7 +169,7 @@ class MainActivity : ComponentActivity() {
enableEdgeToEdge()
setContent {
- _root_ide_package_.me.kavishdevar.librepods.presentation.theme.LibrePodsTheme {
+ LibrePodsTheme {
Main()
}
}
@@ -224,81 +218,72 @@ fun Main() {
val sharedPreferences = context.getSharedPreferences("settings", MODE_PRIVATE)
if (!isSupported(sharedPreferences)) {
val showDialog = remember { mutableStateOf(false) }
-
+ val showPlayBypassVisible = remember { mutableStateOf(false) }
val hazeState = rememberHazeState()
+ val backdrop = rememberLayerBackdrop()
+ val isDarkTheme = isSystemInDarkTheme()
+ val textColor = if (isDarkTheme) Color.White else Color.Black
+ val backgroundColor = if (isSystemInDarkTheme()) Color(0xFF1C1C1E) else Color(0xFFFFFFFF)
Box(
modifier = Modifier
.fillMaxSize()
.hazeSource(hazeState)
- .background(if (isSystemInDarkTheme()) Color.Black else Color(0xFFF2F2F7)),
+ .layerBackdrop(backdrop)
+ .background(if (isDarkTheme) Color.Black else Color(0xFFF2F2F7)),
contentAlignment = Alignment.Center
) {
- Box (
- modifier = Modifier
- .fillMaxSize()
- )
Column (
- verticalArrangement = Arrangement.spacedBy(8.dp)
+ modifier = Modifier.padding(horizontal = 16.dp),
+ verticalArrangement = Arrangement
+ .spacedBy(16.dp)
) {
- Text(
- text = stringResource(R.string.not_supported),
- style = TextStyle(
- fontFamily = FontFamily(Font(R.font.sf_pro)),
- fontWeight = FontWeight.SemiBold,
- color = if (isSystemInDarkTheme()) Color.White else Color.Black,
- fontSize = 20.sp
- ),
- textAlign = TextAlign.Center,
- modifier = Modifier.fillMaxWidth()
- )
- Row (
- modifier = Modifier.fillMaxWidth(),
- horizontalArrangement = Arrangement.Center
+ val innerBackdrop = rememberLayerBackdrop()
+
+ Column(
+ modifier = Modifier.layerBackdrop(innerBackdrop),
+ verticalArrangement = Arrangement
+ .spacedBy(16.dp)
) {
Text(
- text = "Device Info:",
+ text = stringResource(R.string.not_supported),
style = TextStyle(
fontFamily = FontFamily(Font(R.font.sf_pro)),
- fontWeight = FontWeight.Medium,
- color = if (isSystemInDarkTheme()) Color.White else Color.Black,
- fontSize = 16.sp
+ fontWeight = FontWeight.SemiBold,
+ color = textColor,
+ fontSize = 20.sp,
+ textAlign = TextAlign.Center
),
- textAlign = TextAlign.End,
- )
- Spacer(modifier = Modifier.width(4.dp))
- Text(
- text =
- "MANUFACTURER=${Build.MANUFACTURER}\n" +
- "MODEL=${Build.MODEL}\n" +
- "BUILD_ID=${Build.ID}\n" +
- "SDK_INT_FULL= ${Build.VERSION.SDK_INT_FULL}\n",
- style = TextStyle(
- fontFamily = FontFamily(Font(R.font.hack)),
- fontWeight = FontWeight.Medium,
- color = if (isSystemInDarkTheme()) Color.White else Color.Black,
- fontSize = 16.sp
- ),
- textAlign = TextAlign.Start,
+ modifier = Modifier.fillMaxWidth()
)
+
+ DeviceInfoCard()
+
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .background(backgroundColor, RoundedCornerShape(28.dp))
+ .clip(RoundedCornerShape(28.dp))
+ ) {
+ Text(
+ text = stringResource(R.string.check_the_repository_for_more_info),
+ style = TextStyle(
+ fontFamily = FontFamily(Font(R.font.sf_pro)),
+ fontWeight = FontWeight.Medium,
+ color = if (isSystemInDarkTheme()) Color.White else Color.Black,
+ fontSize = 16.sp
+ ),
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 12.dp, vertical = 16.dp)
+ )
+ }
}
- Text(
- text = stringResource(R.string.check_the_repository_for_more_info),
- style = TextStyle(
- fontFamily = FontFamily(Font(R.font.sf_pro)),
- fontWeight = FontWeight.Medium,
- color = if (isSystemInDarkTheme()) Color.White else Color.Black,
- fontSize = 18.sp
- ),
- textAlign = TextAlign.Center,
- modifier = Modifier.fillMaxWidth()
- )
StyledButton(
onClick = { showDialog.value = true },
- backdrop = rememberLayerBackdrop(),
+ backdrop = innerBackdrop,
modifier = Modifier
.fillMaxWidth()
- .padding(8.dp)
) {
Text(
text = stringResource(R.string.bypass_compatibility_check),
@@ -317,15 +302,19 @@ fun Main() {
showDialog = showDialog,
title = stringResource(R.string.bypass_compatibility_check),
message = stringResource(R.string.bypass_compatiblity_check_confirmation),
- confirmText = "Yes",
- dismissText = "No",
+ confirmText = stringResource(R.string.yes),
+ dismissText = stringResource(R.string.no),
onConfirm = {
showDialog.value = false
- sharedPreferences.edit {
- putBoolean("bypass_device_check", true)
- val intent = Intent(context, MainActivity::class.java)
- intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK)
- context.startActivity(intent)
+ if (BuildConfig.PLAY_BUILD) {
+ showPlayBypassVisible.value = true
+ } else {
+ sharedPreferences.edit {
+ putBoolean("bypass_device_check", true)
+ val intent = Intent(context, MainActivity::class.java)
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK)
+ context.startActivity(intent)
+ }
}
},
onDismiss = {
@@ -334,6 +323,26 @@ fun Main() {
hazeState = hazeState
)
+ if (BuildConfig.PLAY_BUILD) {
+ PlayBypassSheet(
+ visible = showPlayBypassVisible.value,
+ onDismiss = {
+ showPlayBypassVisible.value = false
+ showDialog.value = true
+ },
+ onConfirm = {
+ showPlayBypassVisible.value = false
+ sharedPreferences.edit {
+ putBoolean("bypass_device_check", true)
+ val intent = Intent(context, MainActivity::class.java)
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK)
+ context.startActivity(intent)
+ }
+ },
+ backdrop = backdrop
+ )
+ }
+
return
}
@@ -347,8 +356,6 @@ fun Main() {
)
}
- BillingManager.provider = BillingProviderFactory.create(context)
-
val bluetoothPermissions = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
listOf(
"android.permission.BLUETOOTH_CONNECT",
@@ -484,7 +491,7 @@ fun Main() {
if (airPodsViewModel != null) VersionScreen(airPodsViewModel)
}
composable("hearing_protection") {
- if (airPodsViewModel != null) HearingProtectionScreen(airPodsViewModel)
+ if (airPodsViewModel != null) HearingProtectionScreen(airPodsViewModel, navController)
}
composable("purchase_screen") {
val purchaseViewModel: PurchaseViewModel = viewModel()
diff --git a/android/app/src/main/java/me/kavishdevar/librepods/billing/BillingProvider.kt b/android/app/src/main/java/me/kavishdevar/librepods/billing/BillingProvider.kt
index 66c97bf..52a6705 100644
--- a/android/app/src/main/java/me/kavishdevar/librepods/billing/BillingProvider.kt
+++ b/android/app/src/main/java/me/kavishdevar/librepods/billing/BillingProvider.kt
@@ -26,4 +26,5 @@ interface BillingProvider {
val price: StateFlow
fun purchase(activity: Activity)
fun queryPurchases()
+ fun restorePurchases()
}
diff --git a/android/app/src/main/java/me/kavishdevar/librepods/billing/FOSSBillingProvider.kt b/android/app/src/main/java/me/kavishdevar/librepods/billing/FOSSBillingProvider.kt
index 11c8417..4f07db6 100644
--- a/android/app/src/main/java/me/kavishdevar/librepods/billing/FOSSBillingProvider.kt
+++ b/android/app/src/main/java/me/kavishdevar/librepods/billing/FOSSBillingProvider.kt
@@ -31,13 +31,13 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
-import kotlinx.coroutines.withContext
+import me.kavishdevar.librepods.R
class FOSSBillingProvider(context: Context): BillingProvider {
private val _isPremium = MutableStateFlow(false)
override val isPremium: StateFlow = _isPremium
- private val _price = MutableStateFlow("Any")
+ private val _price = MutableStateFlow(context.getString(R.string.name_your_own_price))
override val price: StateFlow = _price
private val sharedPreferences = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
@@ -69,4 +69,9 @@ class FOSSBillingProvider(context: Context): BillingProvider {
_isPremium.value = stored
}
}
+
+ override fun restorePurchases() {
+ _isPremium.value = true
+ sharedPreferences.edit { putBoolean("foss_upgraded", true) }
+ }
}
diff --git a/android/app/src/main/java/me/kavishdevar/librepods/billing/PlayBillingProvider.kt b/android/app/src/main/java/me/kavishdevar/librepods/billing/PlayBillingProvider.kt
index bc6d35d..02f17b9 100644
--- a/android/app/src/main/java/me/kavishdevar/librepods/billing/PlayBillingProvider.kt
+++ b/android/app/src/main/java/me/kavishdevar/librepods/billing/PlayBillingProvider.kt
@@ -162,21 +162,19 @@ class PlayBillingProvider(
it.purchaseState == Purchase.PurchaseState.PURCHASED
}
-
-// val navigateToPurchase = purchases.find {
+// val purchase = purchases.find {
// it.products.contains(PREMIUM_PRODUCT_ID) && it.purchaseState == Purchase.PurchaseState.PURCHASED
// }
//
-// if (navigateToPurchase != null) {
+// if (purchase != null) {
// val consumeParams = ConsumeParams.newBuilder()
-// .setPurchaseToken(navigateToPurchase.purchaseToken)
+// .setPurchaseToken(purchase.purchaseToken)
// .build()
// scope.launch {
// billingClient.consumeAsync(consumeParams) { _, _ ->}
// }
// }
-
_isPremium.value = hasPremium
scope.launch {
@@ -201,4 +199,8 @@ class PlayBillingProvider(
queryExistingPurchases()
}
}
+
+ override fun restorePurchases() {
+ queryPurchases()
+ }
}
diff --git a/android/app/src/main/java/me/kavishdevar/librepods/bluetooth/AACPManager.kt b/android/app/src/main/java/me/kavishdevar/librepods/bluetooth/AACPManager.kt
index b9c7333..659dfaf 100644
--- a/android/app/src/main/java/me/kavishdevar/librepods/bluetooth/AACPManager.kt
+++ b/android/app/src/main/java/me/kavishdevar/librepods/bluetooth/AACPManager.kt
@@ -363,7 +363,13 @@ class AACPManager {
}
val key = ByteArray(keyLength)
System.arraycopy(data, offset, key, 0, keyLength)
- keys[ProximityKeyType.fromByte(keyType)] = key
+ try {
+ keys[ProximityKeyType.fromByte(keyType)] = key
+ } catch (e: Exception) {
+ Log.e(
+ TAG, "incorrect key type received: $keyType, ${key.toHexString()}"
+ )
+ }
offset += keyLength
Log.d(
TAG, "Parsed Proximity Key: Type: ${keyType}, Length: $keyLength, Key: ${
@@ -908,7 +914,7 @@ class AACPManager {
)
buffer.put(byteArrayOf(0x01, 0xE5.toByte(), 0x4A)) // unknown, constant
buffer.put("PlayingApp".toByteArray())
- buffer.put(byteArrayOf(0x56)) // 'V', seems like a identifier or a separator
+ buffer.put(byteArrayOf(0x56)) // 'V', seems like an identifier or a separator
buffer.put("com.google.ios.youtube".toByteArray()) // package name, hardcoding for now, aforementioned reason
buffer.put(byteArrayOf(0x52)) // 'R'
buffer.put("HostStreamingState".toByteArray())
diff --git a/android/app/src/main/java/me/kavishdevar/librepods/data/Packets.kt b/android/app/src/main/java/me/kavishdevar/librepods/data/Packets.kt
index a0559db..84a3006 100644
--- a/android/app/src/main/java/me/kavishdevar/librepods/data/Packets.kt
+++ b/android/app/src/main/java/me/kavishdevar/librepods/data/Packets.kt
@@ -72,6 +72,7 @@ enum class NoiseControlMode {
class AirPodsNotifications {
companion object {
const val AIRPODS_CONNECTED = "me.kavishdevar.librepods.AIRPODS_CONNECTED"
+ const val AIRPODS_L2CAP_CONNECTED = "me.kavishdevar.librepods.AIRPODS_CONNECTED"
const val AIRPODS_DATA = "me.kavishdevar.librepods.AIRPODS_DATA"
const val EAR_DETECTION_DATA = "me.kavishdevar.librepods.EAR_DETECTION_DATA"
const val ANC_DATA = "me.kavishdevar.librepods.ANC_DATA"
diff --git a/android/app/src/main/java/me/kavishdevar/librepods/presentation/components/DeviceInfoCard.kt b/android/app/src/main/java/me/kavishdevar/librepods/presentation/components/DeviceInfoCard.kt
new file mode 100644
index 0000000..ea4f768
--- /dev/null
+++ b/android/app/src/main/java/me/kavishdevar/librepods/presentation/components/DeviceInfoCard.kt
@@ -0,0 +1,171 @@
+package me.kavishdevar.librepods.presentation.components
+
+import android.os.Build
+import androidx.compose.foundation.background
+import androidx.compose.foundation.isSystemInDarkTheme
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.HorizontalDivider
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.layout.onGloballyPositioned
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.font.Font
+import androidx.compose.ui.text.font.FontFamily
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import me.kavishdevar.librepods.R
+
+@Composable
+fun DeviceInfoCard() {
+ val isDarkTheme = isSystemInDarkTheme()
+
+ val textColor = if (isDarkTheme) Color.White else Color.Black
+ val backgroundColor = if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFFFFFFF)
+
+ val rowHeight = remember { mutableStateOf(0.dp) }
+ val density = LocalDensity.current
+
+ Column (
+ verticalArrangement = Arrangement.spacedBy(8.dp)
+ ) {
+ Text(
+ text = stringResource(R.string.device_info), style = TextStyle(
+ fontSize = 14.sp,
+ fontWeight = FontWeight.Bold,
+ color = textColor.copy(alpha = 0.6f),
+ fontFamily = FontFamily(Font(R.font.sf_pro))
+ ), modifier = Modifier.padding(start = 16.dp)
+ )
+ Column(
+ modifier = Modifier
+ .clip(RoundedCornerShape(28.dp))
+ .fillMaxWidth()
+ .background(backgroundColor, RoundedCornerShape(28.dp))
+ ) {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(16.dp)
+ .onGloballyPositioned { coordinates ->
+ rowHeight.value = with(density) { coordinates.size.height.toDp() }
+ },
+ horizontalArrangement = Arrangement.SpaceBetween,
+ ) {
+ Text(
+ text = stringResource(R.string.manufacturer), style = TextStyle(
+ fontSize = 16.sp,
+ color = textColor,
+ fontFamily = FontFamily(Font(R.font.sf_pro))
+ )
+ )
+ Text(
+ text = Build.MANUFACTURER, style = TextStyle(
+ fontSize = 16.sp,
+ color = if (isDarkTheme) Color.White.copy(alpha = 0.6f) else Color.Black.copy(
+ alpha = 0.8f
+ ),
+ fontFamily = FontFamily(Font(R.font.sf_pro))
+ )
+ )
+ }
+ HorizontalDivider(
+ thickness = 1.dp,
+ color = Color(0x40888888),
+ modifier = Modifier.padding(horizontal = 12.dp)
+ )
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(16.dp),
+ horizontalArrangement = Arrangement.SpaceBetween,
+ ) {
+ Text(
+ text = stringResource(R.string.model_number), style = TextStyle(
+ fontSize = 16.sp,
+ color = textColor,
+ fontFamily = FontFamily(Font(R.font.sf_pro))
+ )
+ )
+ Text(
+ text = Build.MODEL, style = TextStyle(
+ fontSize = 16.sp,
+ color = if (isDarkTheme) Color.White.copy(alpha = 0.6f) else Color.Black.copy(
+ alpha = 0.8f
+ ),
+ fontFamily = FontFamily(Font(R.font.sf_pro))
+ )
+ )
+ }
+ HorizontalDivider(
+ thickness = 1.dp,
+ color = Color(0x40888888),
+ modifier = Modifier.padding(horizontal = 12.dp)
+ )
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(16.dp),
+ horizontalArrangement = Arrangement.SpaceBetween,
+ ) {
+ Text(
+ text = stringResource(R.string.build_id), style = TextStyle(
+ fontSize = 16.sp,
+ color = textColor,
+ fontFamily = FontFamily(Font(R.font.sf_pro))
+ )
+ )
+ Text(
+ text = Build.DISPLAY, style = TextStyle(
+ fontSize = 16.sp,
+ color = if (isDarkTheme) Color.White.copy(alpha = 0.6f) else Color.Black.copy(
+ alpha = 0.8f
+ ),
+ fontFamily = FontFamily(Font(R.font.sf_pro))
+ )
+ )
+ }
+ HorizontalDivider(
+ thickness = 1.dp,
+ color = Color(0x40888888),
+ modifier = Modifier.padding(horizontal = 12.dp)
+ )
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(16.dp),
+ horizontalArrangement = Arrangement.SpaceBetween,
+ ) {
+ Text(
+ text = stringResource(R.string.version), style = TextStyle(
+ fontSize = 16.sp,
+ color = textColor,
+ fontFamily = FontFamily(Font(R.font.sf_pro))
+ )
+ )
+ Text(
+ text = Build.ID + " (${Build.VERSION.SDK_INT_FULL})",
+ style = TextStyle(
+ fontSize = 16.sp,
+ color = if (isDarkTheme) Color.White.copy(alpha = 0.6f) else Color.Black.copy(
+ alpha = 0.8f
+ ),
+ fontFamily = FontFamily(Font(R.font.sf_pro))
+ )
+ )
+ }
+ }
+ }
+}
diff --git a/android/app/src/main/java/me/kavishdevar/librepods/presentation/components/PlayBypassConfirmation.kt b/android/app/src/main/java/me/kavishdevar/librepods/presentation/components/PlayBypassConfirmation.kt
new file mode 100644
index 0000000..3ccac3b
--- /dev/null
+++ b/android/app/src/main/java/me/kavishdevar/librepods/presentation/components/PlayBypassConfirmation.kt
@@ -0,0 +1,254 @@
+package me.kavishdevar.librepods.presentation.components
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.isSystemInDarkTheme
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.foundation.text.BasicTextField
+import androidx.compose.foundation.text.input.clearText
+import androidx.compose.foundation.text.input.rememberTextFieldState
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.ModalBottomSheet
+import androidx.compose.material3.Text
+import androidx.compose.material3.rememberModalBottomSheetState
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.focus.FocusRequester
+import androidx.compose.ui.focus.focusRequester
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.SolidColor
+import androidx.compose.ui.platform.LocalSoftwareKeyboardController
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.font.Font
+import androidx.compose.ui.text.font.FontFamily
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import com.kyant.backdrop.backdrops.LayerBackdrop
+import com.kyant.backdrop.backdrops.rememberLayerBackdrop
+import com.kyant.backdrop.drawBackdrop
+import com.kyant.backdrop.effects.blur
+import com.kyant.backdrop.effects.lens
+import com.kyant.backdrop.effects.vibrancy
+import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
+import me.kavishdevar.librepods.R
+
+
+@ExperimentalHazeMaterialsApi
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun PlayBypassSheet(
+ visible: Boolean,
+ onDismiss: () -> Unit,
+ onConfirm: () -> Unit,
+ backdrop: LayerBackdrop
+) {
+ if (!visible) return
+
+ val dark = isSystemInDarkTheme()
+ val contentColor = if (dark) Color.White else Color.Black
+
+ val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
+
+ var acknowledged by remember { mutableStateOf(false) }
+ val inputState = rememberTextFieldState("")
+
+ val isValid = acknowledged && inputState.text.trim() == "OK"
+
+ val sfPro = FontFamily(Font(R.font.sf_pro))
+
+ ModalBottomSheet(
+ onDismissRequest = onDismiss,
+ sheetState = sheetState,
+ containerColor = Color.Transparent,
+ dragHandle = { },
+ shape = RoundedCornerShape(48.dp),
+ scrimColor = Color.Transparent,
+ modifier = Modifier.padding(16.dp)
+ ) {
+ val innerBackdrop = rememberLayerBackdrop()
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .clip(RoundedCornerShape(48.dp))
+ .drawBackdrop(
+ backdrop = backdrop,
+ exportedBackdrop = innerBackdrop,
+ shape = { RoundedCornerShape(48.dp) },
+ effects = {
+ vibrancy()
+ blur(6f.dp.toPx())
+ lens(12f.dp.toPx(), 48f.dp.toPx(), true)
+ },
+ onDrawSurface = {
+ drawRect(
+ if (dark) Color.DarkGray.copy(alpha = 0.3f) else Color.White.copy(alpha = 0.6f)
+ )
+ }
+ )
+ .padding(24.dp)
+ ) {
+ Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
+ Text(
+ text = stringResource(R.string.bypass_compatibility_check),
+ style = TextStyle(
+ fontFamily = sfPro,
+ fontWeight = FontWeight.SemiBold,
+ fontSize = 18.sp,
+ color = contentColor
+ ),
+ modifier = Modifier.padding(horizontal = 12.dp)
+ )
+
+ Text(
+ text = stringResource(R.string.compatibility_play_dialog_confirmation),
+ style = TextStyle(
+ fontFamily = sfPro,
+ fontWeight = FontWeight.Medium,
+ fontSize = 14.sp,
+ color = contentColor
+ ),
+ modifier = Modifier.padding(horizontal = 12.dp)
+ )
+
+ StyledSelectList(
+ items = listOf(
+ SelectItem(
+ name = stringResource(R.string.read_compatibility_requirements),
+ selected = acknowledged,
+ onClick = { acknowledged = !acknowledged }
+ )
+ )
+ )
+
+ val focusRequester = remember { FocusRequester() }
+ val keyboardController = LocalSoftwareKeyboardController.current
+
+ LaunchedEffect(Unit) {
+ focusRequester.requestFocus()
+ keyboardController?.show()
+ }
+ val backgroundColor = if (dark) Color(0xFF1C1C1E) else Color(0xFFFFFFFF)
+ val textColor = if (dark) Color.White else Color.Black
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(58.dp)
+ .background(
+ backgroundColor,
+ RoundedCornerShape(28.dp)
+ )
+ .padding(horizontal = 16.dp, vertical = 8.dp)
+ ) {
+ BasicTextField(
+ state = inputState,
+ textStyle = TextStyle(
+ fontSize = 16.sp,
+ color = textColor,
+ fontFamily = FontFamily(Font(R.font.sf_pro))
+ ),
+ cursorBrush = SolidColor(textColor),
+ decorator = { innerTextField ->
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ Row(
+ modifier = Modifier
+ .weight(1f)
+ ) {
+ Box {
+ if (inputState.text == "") {
+ Text(
+ text = stringResource(R.string.type_ok_to_continue, "OK"),
+ style = TextStyle(
+ fontSize = 14.sp,
+ fontWeight = FontWeight.Light,
+ fontFamily = sfPro,
+ color = textColor.copy(alpha = 0.8f)
+ )
+ )
+ }
+ innerTextField()
+ }
+ }
+ IconButton(
+ onClick = {
+ inputState.clearText()
+ }
+ ) {
+ Text(
+ text = "",
+ style = TextStyle(
+ fontSize = 16.sp,
+ fontFamily = FontFamily(Font(R.font.sf_pro)),
+ color = if (dark) Color.White.copy(alpha = 0.6f) else Color.Black.copy(alpha = 0.6f)
+ ),
+ )
+ }
+ }
+ },
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(start = 8.dp)
+ .focusRequester(focusRequester)
+ )
+ }
+
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.spacedBy(24.dp)
+ ) {
+ StyledButton(
+ onClick = onDismiss,
+ backdrop = innerBackdrop,
+ modifier = Modifier.weight(1f),
+ ) {
+ Text(
+ text = stringResource(R.string.no),
+ style = TextStyle(
+ fontFamily = sfPro,
+ fontWeight = FontWeight.Medium,
+ fontSize = 14.sp,
+ color = contentColor
+ )
+ )
+ }
+ StyledButton(
+ onClick = onConfirm,
+ backdrop = innerBackdrop,
+ isInteractive = isValid,
+ modifier = Modifier.weight(1f),
+ enabled = isValid,
+ surfaceColor = if (dark) Color(0xFF0091FF) else Color(0xFF0088FF)
+ ) {
+ Text(
+ text = stringResource(R.string.proceed),
+ style = TextStyle(
+ fontFamily = sfPro,
+ fontWeight = FontWeight.Medium,
+ fontSize = 14.sp,
+ color = if (isValid) contentColor else contentColor.copy(alpha = 0.4f)
+ )
+ )
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/android/app/src/main/java/me/kavishdevar/librepods/presentation/components/StyledButton.kt b/android/app/src/main/java/me/kavishdevar/librepods/presentation/components/StyledButton.kt
index 5012ce0..6c60148 100644
--- a/android/app/src/main/java/me/kavishdevar/librepods/presentation/components/StyledButton.kt
+++ b/android/app/src/main/java/me/kavishdevar/librepods/presentation/components/StyledButton.kt
@@ -77,8 +77,10 @@ fun StyledButton(
tint: Color = Color.Unspecified,
surfaceColor: Color = Color.Unspecified,
maxScale: Float = 0.1f,
+ enabled: Boolean = true,
content: @Composable RowScope.() -> Unit,
) {
+ val isInteractive = enabled && isInteractive
val scope = rememberCoroutineScope()
val haptics = LocalHapticFeedback.current
val progressAnimation = remember { Animatable(0f) }
@@ -125,8 +127,8 @@ half4 main(float2 coord) {
} else {
drawRect(Color.White.copy(0.1f))
}
- if (surfaceColor.isSpecified) {
- val color = if (!isInteractive && isPressed) {
+ if (surfaceColor.isSpecified && enabled) {
+ val color = if (isPressed) {
Color(
red = surfaceColor.red * 0.5f,
green = surfaceColor.green * 0.5f,
@@ -137,6 +139,11 @@ half4 main(float2 coord) {
surfaceColor
}
drawRect(color)
+ } else {
+ if (isPressed) {
+ drawRect(Color.Black.copy(alpha = 0.4f))
+ drawRect(Color.White.copy(alpha = 0.2f))
+ }
}
},
onDrawFront = null,
@@ -245,8 +252,10 @@ half4 main(float2 coord) {
indication = null,
role = Role.Button,
onClick = {
- haptics.performHapticFeedback(HapticFeedbackType.ContextClick)
- onClick()
+ if (enabled) {
+ haptics.performHapticFeedback(HapticFeedbackType.ContextClick)
+ onClick()
+ }
}
)
.then(
@@ -302,8 +311,10 @@ half4 main(float2 coord) {
isPressed = false
},
onTap = {
- haptics.performHapticFeedback(HapticFeedbackType.ContextClick)
- onClick()
+ if (enabled) {
+ haptics.performHapticFeedback(HapticFeedbackType.ContextClick)
+ onClick()
+ }
}
)
}
diff --git a/android/app/src/main/java/me/kavishdevar/librepods/presentation/components/StyledSelectList.kt b/android/app/src/main/java/me/kavishdevar/librepods/presentation/components/StyledSelectList.kt
index 1b1dd1b..b9a2246 100644
--- a/android/app/src/main/java/me/kavishdevar/librepods/presentation/components/StyledSelectList.kt
+++ b/android/app/src/main/java/me/kavishdevar/librepods/presentation/components/StyledSelectList.kt
@@ -39,7 +39,6 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
-import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@@ -74,7 +73,6 @@ fun StyledSelectList(
val backgroundColor = if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFFFFFFF)
val textColor = if (isDarkTheme) Color.White else Color.Black
- val scope = rememberCoroutineScope()
val haptics = LocalHapticFeedback.current
Column(
diff --git a/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/AccessibilitySettingsScreen.kt b/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/AccessibilitySettingsScreen.kt
index 98adcb1..22f80f4 100644
--- a/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/AccessibilitySettingsScreen.kt
+++ b/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/AccessibilitySettingsScreen.kt
@@ -20,6 +20,7 @@ package me.kavishdevar.librepods.presentation.screens
// import me.kavishdevar.librepods.utils.RadareOffsetFinder
import android.annotation.SuppressLint
+import android.util.Log
import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.detectDragGesturesAfterLongPress
import androidx.compose.foundation.gestures.detectTapGestures
@@ -39,6 +40,7 @@ import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableLongStateOf
@@ -71,6 +73,10 @@ import com.kyant.backdrop.backdrops.rememberLayerBackdrop
import dev.chrisbanes.haze.HazeState
import dev.chrisbanes.haze.hazeSource
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import me.kavishdevar.librepods.R
import me.kavishdevar.librepods.bluetooth.AACPManager
@@ -85,8 +91,8 @@ import me.kavishdevar.librepods.presentation.components.StyledToggle
import me.kavishdevar.librepods.presentation.viewmodel.AirPodsViewModel
import kotlin.io.encoding.ExperimentalEncodingApi
-//private var phoneMediaDebounceJob: Job? = null
-//private var toneVolumeDebounceJob: Job? = null
+private var phoneMediaDebounceJob: Job? = null
+private var toneVolumeDebounceJob: Job? = null
//private const val TAG = "AccessibilitySettings"
@SuppressLint("DefaultLocale")
@@ -99,7 +105,13 @@ fun AccessibilitySettingsScreen(viewModel: AirPodsViewModel, navController: NavC
val isDarkTheme = isSystemInDarkTheme()
val textColor = if (isDarkTheme) Color.White else Color.Black
- val hearingAidEnabled = state.controlStates[AACPManager.Companion.ControlCommandIdentifiers.HEARING_AID]?.getOrNull(1)?.toInt() == 1 && state.controlStates[AACPManager.Companion.ControlCommandIdentifiers.HEARING_AID]?.getOrNull(0)?.toInt() == 1
+ val hearingAidEnabled =
+ state.controlStates[AACPManager.Companion.ControlCommandIdentifiers.HEARING_AID]?.getOrNull(
+ 1
+ )
+ ?.toInt() == 1 && state.controlStates[AACPManager.Companion.ControlCommandIdentifiers.HEARING_AID]?.getOrNull(
+ 0
+ )?.toInt() == 1
val backdrop = rememberLayerBackdrop()
@@ -125,7 +137,7 @@ fun AccessibilitySettingsScreen(viewModel: AirPodsViewModel, navController: NavC
backdrop = rememberLayerBackdrop(),
modifier = Modifier.fillMaxWidth(),
maxScale = 0.05f,
- tint = if (isSystemInDarkTheme()) Color(0xFF916100) else Color(0xFFE59900)
+ surfaceColor = if (isSystemInDarkTheme()) Color(0xFF916100) else Color(0xFFE59900)
) {
Text(
stringResource(R.string.unlock_advanced_features),
@@ -149,7 +161,10 @@ fun AccessibilitySettingsScreen(viewModel: AirPodsViewModel, navController: NavC
2.toByte() to stringResource(R.string.slowest)
)
- val selectedPressSpeedValue = state.controlStates[AACPManager.Companion.ControlCommandIdentifiers.DOUBLE_CLICK_INTERVAL]?.getOrNull(0)
+ val selectedPressSpeedValue =
+ state.controlStates[AACPManager.Companion.ControlCommandIdentifiers.DOUBLE_CLICK_INTERVAL]?.getOrNull(
+ 0
+ )
var selectedPressSpeed by remember {
mutableStateOf(
pressSpeedOptions[selectedPressSpeedValue] ?: pressSpeedOptions[0]
@@ -162,7 +177,10 @@ fun AccessibilitySettingsScreen(viewModel: AirPodsViewModel, navController: NavC
2.toByte() to stringResource(R.string.slowest)
)
- val selectedPressAndHoldDurationValue = state.controlStates[AACPManager.Companion.ControlCommandIdentifiers.CLICK_HOLD_INTERVAL]?.getOrNull(0)
+ val selectedPressAndHoldDurationValue =
+ state.controlStates[AACPManager.Companion.ControlCommandIdentifiers.CLICK_HOLD_INTERVAL]?.getOrNull(
+ 0
+ )
var selectedPressAndHoldDuration by remember {
mutableStateOf(
pressAndHoldDurationOptions[selectedPressAndHoldDurationValue]
@@ -175,7 +193,10 @@ fun AccessibilitySettingsScreen(viewModel: AirPodsViewModel, navController: NavC
2.toByte() to stringResource(R.string.longer),
3.toByte() to stringResource(R.string.longest)
)
- val selectedVolumeSwipeSpeedValue = state.controlStates[AACPManager.Companion.ControlCommandIdentifiers.VOLUME_SWIPE_INTERVAL]?.getOrNull(0)
+ val selectedVolumeSwipeSpeedValue =
+ state.controlStates[AACPManager.Companion.ControlCommandIdentifiers.VOLUME_SWIPE_INTERVAL]?.getOrNull(
+ 0
+ )
var selectedVolumeSwipeSpeed by remember {
mutableStateOf(
volumeSwipeSpeedOptions[selectedVolumeSwipeSpeedValue]
@@ -183,43 +204,42 @@ fun AccessibilitySettingsScreen(viewModel: AirPodsViewModel, navController: NavC
)
}
-// LaunchedEffect(phoneMediaEQ.value, phoneEQEnabled.value, mediaEQEnabled.value) {
-// phoneMediaDebounceJob?.cancel()
-// phoneMediaDebounceJob = CoroutineScope(Dispatchers.IO).launch {
-// delay(150)
-// val manager = ServiceManager.getService()?.aacpManager
-// if (manager == null) {
-// Log.w(TAG, "Cannot write EQ: AACPManager not available")
-// return@launch
-// }
-// try {
-// val phoneByte = if (phoneEQEnabled.value) 0x01.toByte() else 0x02.toByte()
-// val mediaByte = if (mediaEQEnabled.value) 0x01.toByte() else 0x02.toByte()
-// Log.d(
-// TAG,
-// "Sending phone/media EQ (phoneEnabled=${phoneEQEnabled.value}, mediaEnabled=${mediaEQEnabled.value})"
-// )
-// manager.sendPhoneMediaEQ(phoneMediaEQ.value, phoneByte, mediaByte)
-// } catch (e: Exception) {
-// Log.w(TAG, "Error sending phone/media EQ: ${e.message}")
-// }
-// }
-// }
- Box (
+ val phoneMediaEQ = remember { mutableStateOf(FloatArray(8) { 0.5f }) }
+ val phoneEQEnabled = remember { mutableStateOf(false) }
+ val mediaEQEnabled = remember { mutableStateOf(false) }
+
+ LaunchedEffect(phoneMediaEQ.value, phoneEQEnabled.value, mediaEQEnabled.value) {
+ phoneMediaDebounceJob?.cancel()
+ phoneMediaDebounceJob = CoroutineScope(Dispatchers.IO).launch {
+ delay(150)
+ try {
+ val phoneByte = if (phoneEQEnabled.value) 0x01.toByte() else 0x02.toByte()
+ val mediaByte = if (mediaEQEnabled.value) 0x01.toByte() else 0x02.toByte()
+ Log.d(
+ "AccessibilitySettingsScreen",
+ "Sending phone/media EQ (phoneEnabled=${phoneEQEnabled.value}, mediaEnabled=${mediaEQEnabled.value})"
+ )
+ viewModel.sendPhoneMediaEQ(phoneMediaEQ.value, phoneByte, mediaByte)
+ } catch (e: Exception) {
+ Log.w(
+ "AccessibilitySettingsScreen",
+ "Error sending phone/media EQ: ${e.message}"
+ )
+ }
+ }
+ }
+ Box(
modifier = Modifier.then(
if (!state.isPremium) {
- Modifier
- .pointerInput(Unit) {
- awaitPointerEventScope {
- while (true) {
- val event = awaitPointerEvent(PointerEventPass.Initial)
- event.changes.forEach { it.consume() }
- }
- }
- }
- } else Modifier
- )
- ) {
+ Modifier.pointerInput(Unit) {
+ awaitPointerEventScope {
+ while (true) {
+ val event = awaitPointerEvent(PointerEventPass.Initial)
+ event.changes.forEach { it.consume() }
+ }
+ }
+ }
+ } else Modifier)) {
DropdownMenuComponent(
label = stringResource(R.string.press_speed),
description = stringResource(R.string.press_speed_description),
@@ -239,21 +259,18 @@ fun AccessibilitySettingsScreen(viewModel: AirPodsViewModel, navController: NavC
)
}
- Box (
+ Box(
modifier = Modifier.then(
- if (!state.isPremium) {
- Modifier
- .pointerInput(Unit) {
- awaitPointerEventScope {
- while (true) {
- val event = awaitPointerEvent(PointerEventPass.Initial)
- event.changes.forEach { it.consume() }
- }
- }
- }
- } else Modifier
- )
- ) {
+ if (!state.isPremium) {
+ Modifier.pointerInput(Unit) {
+ awaitPointerEventScope {
+ while (true) {
+ val event = awaitPointerEvent(PointerEventPass.Initial)
+ event.changes.forEach { it.consume() }
+ }
+ }
+ }
+ } else Modifier)) {
DropdownMenuComponent(
label = stringResource(R.string.press_and_hold_duration),
description = stringResource(R.string.press_and_hold_duration_description),
@@ -278,8 +295,14 @@ fun AccessibilitySettingsScreen(viewModel: AirPodsViewModel, navController: NavC
label = stringResource(R.string.noise_cancellation_single_airpod),
description = stringResource(R.string.noise_cancellation_single_airpod_description),
independent = true,
- checked = state.controlStates[AACPManager.Companion.ControlCommandIdentifiers.ONE_BUD_ANC_MODE]?.getOrNull(0) == 0x01.toByte(),
- onCheckedChange = { viewModel.setControlCommandBoolean(AACPManager.Companion.ControlCommandIdentifiers.ONE_BUD_ANC_MODE, it) },
+ checked = state.controlStates[AACPManager.Companion.ControlCommandIdentifiers.ONE_BUD_ANC_MODE]?.getOrNull(
+ 0
+ ) == 0x01.toByte(),
+ onCheckedChange = {
+ viewModel.setControlCommandBoolean(
+ AACPManager.Companion.ControlCommandIdentifiers.ONE_BUD_ANC_MODE, it
+ )
+ },
enabled = state.isPremium
)
@@ -288,7 +311,12 @@ fun AccessibilitySettingsScreen(viewModel: AirPodsViewModel, navController: NavC
label = stringResource(R.string.loud_sound_reduction),
description = stringResource(R.string.loud_sound_reduction_description),
checked = state.loudSoundReductionEnabled,
- onCheckedChange = { viewModel.setATTCharacteristicValue(ATTHandles.LOUD_SOUND_REDUCTION, if (it) byteArrayOf(0x01) else byteArrayOf(0x00)) },
+ onCheckedChange = {
+ viewModel.setATTCharacteristicValue(
+ ATTHandles.LOUD_SOUND_REDUCTION,
+ if (it) byteArrayOf(0x01) else byteArrayOf(0x00)
+ )
+ },
enabled = state.isPremium
)
}
@@ -302,13 +330,19 @@ fun AccessibilitySettingsScreen(viewModel: AirPodsViewModel, navController: NavC
)
}
- val toneVolumeValue = state.controlStates[AACPManager.Companion.ControlCommandIdentifiers.CHIME_VOLUME]?.getOrNull(0)?.toFloat() ?: 75f
+ val toneVolumeValue =
+ state.controlStates[AACPManager.Companion.ControlCommandIdentifiers.CHIME_VOLUME]?.getOrNull(
+ 0
+ )?.toFloat() ?: 75f
StyledSlider(
label = stringResource(R.string.tone_volume),
description = stringResource(R.string.tone_volume_description),
value = toneVolumeValue,
onValueChange = {
- viewModel.setControlCommandValue(AACPManager.Companion.ControlCommandIdentifiers.CHIME_VOLUME, byteArrayOf(it.toInt().toByte(), 0x50))
+ viewModel.setControlCommandValue(
+ AACPManager.Companion.ControlCommandIdentifiers.CHIME_VOLUME,
+ byteArrayOf(it.toInt().toByte(), 0x50)
+ )
},
valueRange = 0f..100f,
snapPoints = listOf(75f),
@@ -319,30 +353,34 @@ fun AccessibilitySettingsScreen(viewModel: AirPodsViewModel, navController: NavC
)
if (state.capabilities.contains(Capability.SWIPE_FOR_VOLUME)) {
- val volumeSwipeEnabled = state.controlStates[AACPManager.Companion.ControlCommandIdentifiers.VOLUME_SWIPE_MODE]?.getOrNull(0)?.toInt() == 0x01
+ val volumeSwipeEnabled =
+ state.controlStates[AACPManager.Companion.ControlCommandIdentifiers.VOLUME_SWIPE_MODE]?.getOrNull(
+ 0
+ )?.toInt() == 0x01
StyledToggle(
label = stringResource(R.string.volume_control),
description = stringResource(R.string.volume_control_description),
checked = volumeSwipeEnabled,
- onCheckedChange = { viewModel.setControlCommandBoolean(AACPManager.Companion.ControlCommandIdentifiers.VOLUME_SWIPE_MODE, it) },
+ onCheckedChange = {
+ viewModel.setControlCommandBoolean(
+ AACPManager.Companion.ControlCommandIdentifiers.VOLUME_SWIPE_MODE, it
+ )
+ },
enabled = state.isPremium
)
- Box (
+ Box(
modifier = Modifier.then(
if (!state.isPremium) {
- Modifier
- .pointerInput(Unit) {
- awaitPointerEventScope {
- while (true) {
- val event = awaitPointerEvent(PointerEventPass.Initial)
- event.changes.forEach { it.consume() }
- }
- }
- }
- } else Modifier
- )
- ) {
+ Modifier.pointerInput(Unit) {
+ awaitPointerEventScope {
+ while (true) {
+ val event = awaitPointerEvent(PointerEventPass.Initial)
+ event.changes.forEach { it.consume() }
+ }
+ }
+ }
+ } else Modifier)) {
DropdownMenuComponent(
label = stringResource(R.string.volume_swipe_speed),
description = stringResource(R.string.volume_swipe_speed_description),
@@ -364,21 +402,22 @@ fun AccessibilitySettingsScreen(viewModel: AirPodsViewModel, navController: NavC
}
}
-// if (!hearingAidEnabled.value&& BuildConfig.FLAVOR == "xposed") {
+// if (!hearingAidEnabled && BuildConfig.FLAVOR == "xposed") {
// Text(
-// text = stringResource(R.string.apply_eq_to),
-// style = TextStyle(
+// text = stringResource(R.string.apply_eq_to), style = TextStyle(
// fontSize = 14.sp,
// fontWeight = FontWeight.Bold,
// color = textColor.copy(alpha = 0.6f),
// fontFamily = FontFamily(Font(R.font.sf_pro))
-// ),
-// modifier = Modifier.padding(8.dp, bottom = 0.dp)
+// ), modifier = Modifier.padding(8.dp, bottom = 0.dp)
// )
// Column(
// modifier = Modifier
// .fillMaxWidth()
-// .background(backgroundColor, RoundedCornerShape(28.dp))
+// .background(
+// if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFFFFFFF),
+// RoundedCornerShape(28.dp)
+// )
// .padding(vertical = 0.dp)
// ) {
// val darkModeLocal = isSystemInDarkTheme()
@@ -405,17 +444,19 @@ fun AccessibilitySettingsScreen(viewModel: AirPodsViewModel, navController: NavC
// detectTapGestures(
// onPress = {
// phoneBackgroundColor =
-// if (darkModeLocal) Color(0x40888888) else Color(0x40D9D9D9)
+// if (darkModeLocal) Color(0x40888888) else Color(
+// 0x40D9D9D9
+// )
// tryAwaitRelease()
// phoneBackgroundColor =
-// if (darkModeLocal) Color(0xFF1C1C1E) else Color(0xFFFFFFFF)
+// if (darkModeLocal) Color(0xFF1C1C1E) else Color(
+// 0xFFFFFFFF
+// )
// phoneEQEnabled.value = !phoneEQEnabled.value
-// }
-// )
+// })
// }
// .padding(horizontal = 16.dp),
-// verticalAlignment = Alignment.CenterVertically
-// ) {
+// verticalAlignment = Alignment.CenterVertically) {
// Text(
// stringResource(R.string.phone),
// fontSize = 16.sp,
@@ -441,8 +482,7 @@ fun AccessibilitySettingsScreen(viewModel: AirPodsViewModel, navController: NavC
// }
//
// HorizontalDivider(
-// thickness = 1.dp,
-// color = Color(0x40888888)
+// thickness = 1.dp, color = Color(0x40888888)
// )
//
// val mediaShape = RoundedCornerShape(bottomStart = 28.dp, bottomEnd = 28.dp)
@@ -467,17 +507,19 @@ fun AccessibilitySettingsScreen(viewModel: AirPodsViewModel, navController: NavC
// detectTapGestures(
// onPress = {
// mediaBackgroundColor =
-// if (darkModeLocal) Color(0x40888888) else Color(0x40D9D9D9)
+// if (darkModeLocal) Color(0x40888888) else Color(
+// 0x40D9D9D9
+// )
// tryAwaitRelease()
// mediaBackgroundColor =
-// if (darkModeLocal) Color(0xFF1C1C1E) else Color(0xFFFFFFFF)
+// if (darkModeLocal) Color(0xFF1C1C1E) else Color(
+// 0xFFFFFFFF
+// )
// mediaEQEnabled.value = !mediaEQEnabled.value
-// }
-// )
+// })
// }
// .padding(horizontal = 16.dp),
-// verticalAlignment = Alignment.CenterVertically
-// ) {
+// verticalAlignment = Alignment.CenterVertically) {
// Text(
// stringResource(R.string.media),
// fontSize = 16.sp,
@@ -502,90 +544,97 @@ fun AccessibilitySettingsScreen(viewModel: AirPodsViewModel, navController: NavC
// )
// }
// }
-
- // EQ Settings. Don't seem to have an effect?
- // Column(
- // modifier = Modifier
- // .fillMaxWidth()
- // .background(backgroundColor, RoundedCornerShape(28.dp))
- // .padding(12.dp),
- // horizontalAlignment = Alignment.CenterHorizontally
- // ) {
- // for (i in 0 until 8) {
- // val eqPhoneValue =
- // remember(phoneMediaEQ.value[i]) { mutableFloatStateOf(phoneMediaEQ.value[i]) }
- // Row(
- // horizontalArrangement = Arrangement.SpaceBetween,
- // verticalAlignment = Alignment.CenterVertically,
- // modifier = Modifier
- // .fillMaxWidth()
- // .height(38.dp)
- // ) {
- // Text(
- // text = String.format("%.2f", eqPhoneValue.floatValue),
- // fontSize = 12.sp,
- // color = textColor,
- // modifier = Modifier.padding(bottom = 4.dp)
- // )
-
- // Slider(
- // value = eqPhoneValue.floatValue,
- // onValueChange = { newVal ->
- // eqPhoneValue.floatValue = newVal
- // val newEQ = phoneMediaEQ.value.copyOf()
- // newEQ[i] = eqPhoneValue.floatValue
- // phoneMediaEQ.value = newEQ
- // },
- // valueRange = 0f..100f,
- // modifier = Modifier
- // .fillMaxWidth(0.9f)
- // .height(36.dp),
- // colors = SliderDefaults.colors(
- // thumbColor = thumbColor,
- // activeTrackColor = activeTrackColor,
- // inactiveTrackColor = trackColor
- // ),
- // thumb = {
- // Box(
- // modifier = Modifier
- // .size(24.dp)
- // .shadow(4.dp, CircleShape)
- // .background(thumbColor, CircleShape)
- // )
- // },
- // track = {
- // Box(
- // modifier = Modifier
- // .fillMaxWidth()
- // .height(12.dp),
- // contentAlignment = Alignment.CenterStart
- // )
- // {
- // Box(
- // modifier = Modifier
- // .fillMaxWidth()
- // .height(4.dp)
- // .background(trackColor, RoundedCornerShape(4.dp))
- // )
- // Box(
- // modifier = Modifier
- // .fillMaxWidth(eqPhoneValue.floatValue / 100f)
- // .height(4.dp)
- // .background(activeTrackColor, RoundedCornerShape(4.dp))
- // )
- // }
- // }
- // )
-
- // Text(
- // text = stringResource(R.string.band_label, i + 1),
- // fontSize = 12.sp,
- // color = textColor,
- // modifier = Modifier.padding(top = 4.dp)
- // )
- // }
- // }
- // }
+//
+//// EQ Settings. Don't seem to have an effect?
+// Column(
+// modifier = Modifier
+// .fillMaxWidth()
+// .background(
+// if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFFFFFFF),
+// RoundedCornerShape(28.dp)
+// )
+// .padding(12.dp), horizontalAlignment = Alignment.CenterHorizontally
+// ) {
+// val trackColor = if (isDarkTheme) Color(0xFFB3B3B3) else Color(0xFF929491)
+// val activeTrackColor = if (isDarkTheme) Color(0xFF007AFF) else Color(0xFF3C6DF5)
+// val thumbColor = if (isDarkTheme) Color(0xFFFFFFFF) else Color(0xFFFFFFFF)
+//
+// for (i in 0 until 8) {
+// val eqPhoneValue =
+// remember(phoneMediaEQ.value[i]) { mutableFloatStateOf(phoneMediaEQ.value[i]) }
+// Row(
+// horizontalArrangement = Arrangement.SpaceBetween,
+// verticalAlignment = Alignment.CenterVertically,
+// modifier = Modifier
+// .fillMaxWidth()
+// .height(38.dp)
+// ) {
+// Text(
+// text = String.format("%.2f", eqPhoneValue.floatValue),
+// fontSize = 12.sp,
+// color = textColor,
+// modifier = Modifier.padding(bottom = 4.dp)
+// )
+//
+// Slider(
+// value = eqPhoneValue.floatValue,
+// onValueChange = { newVal ->
+// eqPhoneValue.floatValue = newVal
+// val newEQ = phoneMediaEQ.value.copyOf()
+// newEQ[i] = eqPhoneValue.floatValue
+// phoneMediaEQ.value = newEQ
+// },
+// valueRange = 0f..100f,
+// modifier = Modifier
+// .fillMaxWidth(0.9f)
+// .height(36.dp),
+// colors = SliderDefaults.colors(
+// thumbColor = thumbColor,
+// activeTrackColor = activeTrackColor,
+// inactiveTrackColor = trackColor
+// ),
+// thumb = {
+// Box(
+// modifier = Modifier
+// .size(24.dp)
+// .shadow(4.dp, CircleShape)
+// .background(thumbColor, CircleShape)
+// )
+// },
+// track = {
+// Box(
+// modifier = Modifier
+// .fillMaxWidth()
+// .height(12.dp),
+// contentAlignment = Alignment.CenterStart
+// ) {
+// Box(
+// modifier = Modifier
+// .fillMaxWidth()
+// .height(4.dp)
+// .background(trackColor, RoundedCornerShape(4.dp))
+// )
+// Box(
+// modifier = Modifier
+// .fillMaxWidth(eqPhoneValue.floatValue / 100f)
+// .height(4.dp)
+// .background(
+// activeTrackColor, RoundedCornerShape(4.dp)
+// )
+// )
+// }
+// })
+//
+// Text(
+// text = stringResource(R.string.band_label, i + 1),
+// fontSize = 12.sp,
+// color = textColor,
+// modifier = Modifier.padding(top = 4.dp)
+// )
+// }
+// }
+// }
+// }
Spacer(modifier = Modifier.height(bottomPadding))
}
}
@@ -616,7 +665,7 @@ private fun DropdownMenuComponent(
val haptics = LocalHapticFeedback.current
val scope = rememberCoroutineScope()
- Column(modifier = Modifier.fillMaxWidth()){
+ Column(modifier = Modifier.fillMaxWidth()) {
Column(
modifier = Modifier
.fillMaxWidth()
@@ -630,14 +679,14 @@ private fun DropdownMenuComponent(
} else Modifier
)
.background(
- if (independent) (if (isSystemInDarkTheme()) Color(0xFF1C1C1E) else Color(0xFFFFFFFF)) else Color.Transparent,
+ if (independent) (if (isSystemInDarkTheme()) Color(0xFF1C1C1E) else Color(
+ 0xFFFFFFFF
+ )) else Color.Transparent,
if (independent) RoundedCornerShape(28.dp) else RoundedCornerShape(0.dp)
- )
- then(
- if (independent) Modifier.padding(horizontal = 4.dp) else Modifier
- )
- .clip(if (independent) RoundedCornerShape(28.dp) else RoundedCornerShape(0.dp))
- ){
+ ) then (if (independent) Modifier.padding(horizontal = 4.dp) else Modifier).clip(
+ if (independent) RoundedCornerShape(28.dp) else RoundedCornerShape(0.dp)
+ )
+ ) {
Row(
modifier = Modifier
.fillMaxWidth()
@@ -658,98 +707,94 @@ private fun DropdownMenuComponent(
}
}
.pointerInput(Unit) {
- detectDragGesturesAfterLongPress(
- onDragStart = { offset ->
- val now = System.currentTimeMillis()
- touchOffset = offset
- if (!expanded && now - lastDismissTime > 250L) {
- expanded = true
- }
- lastDismissTime = now
- parentDragActive = true
- parentHoveredIndex = 0
- },
- onDrag = { change, _ ->
- val current = change.position
- val touch = touchOffset ?: current
- val posInPopupY = current.y - touch.y
- val idx = (posInPopupY / itemHeightPx).toInt()
- if (idx != previousIdx) {
- scope.launch { haptics.performHapticFeedback(HapticFeedbackType.SegmentTick) }
- }
- parentHoveredIndex = idx
- previousIdx = idx
- },
- onDragEnd = {
- parentDragActive = false
- parentHoveredIndex?.let { idx ->
- if (idx in options.indices) {
- onOptionSelected(options[idx])
- expanded = false
- lastDismissTime = System.currentTimeMillis()
- }
- }
- if (parentHoveredIndex != null && parentHoveredIndex in options.indices) {
- scope.launch { haptics.performHapticFeedback(HapticFeedbackType.GestureEnd) }
- }
- parentHoveredIndex = null
- },
- onDragCancel = {
- parentDragActive = false
- parentHoveredIndex = null
+ detectDragGesturesAfterLongPress(onDragStart = { offset ->
+ val now = System.currentTimeMillis()
+ touchOffset = offset
+ if (!expanded && now - lastDismissTime > 250L) {
+ expanded = true
}
- )
+ lastDismissTime = now
+ parentDragActive = true
+ parentHoveredIndex = 0
+ }, onDrag = { change, _ ->
+ val current = change.position
+ val touch = touchOffset ?: current
+ val posInPopupY = current.y - touch.y
+ val idx = (posInPopupY / itemHeightPx).toInt()
+ if (idx != previousIdx) {
+ scope.launch {
+ haptics.performHapticFeedback(
+ HapticFeedbackType.SegmentTick
+ )
+ }
+ }
+ parentHoveredIndex = idx
+ previousIdx = idx
+ }, onDragEnd = {
+ parentDragActive = false
+ parentHoveredIndex?.let { idx ->
+ if (idx in options.indices) {
+ onOptionSelected(options[idx])
+ expanded = false
+ lastDismissTime = System.currentTimeMillis()
+ }
+ }
+ if (parentHoveredIndex != null && parentHoveredIndex in options.indices) {
+ scope.launch {
+ haptics.performHapticFeedback(
+ HapticFeedbackType.GestureEnd
+ )
+ }
+ }
+ parentHoveredIndex = null
+ }, onDragCancel = {
+ parentDragActive = false
+ parentHoveredIndex = null
+ })
},
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Column(
modifier = Modifier.weight(1f)
- ){
+ ) {
Text(
text = label,
fontSize = 16.sp,
color = textColor,
modifier = Modifier.padding(bottom = 4.dp)
)
- if (!independent && description != null){
+ if (!independent && description != null) {
Text(
- text = description,
- style = TextStyle(
+ text = description, style = TextStyle(
fontSize = 12.sp,
fontWeight = FontWeight.Light,
color = textColor.copy(alpha = 0.6f),
fontFamily = FontFamily(Font(R.font.sf_pro))
- ),
- modifier = Modifier.padding(16.dp, top = 0.dp, bottom = 2.dp)
+ ), modifier = Modifier.padding(16.dp, top = 0.dp, bottom = 2.dp)
)
}
}
Box(
modifier = Modifier.onGloballyPositioned { coordinates ->
boxPosition = coordinates.positionInParent()
- }
- ) {
+ }) {
Row(
verticalAlignment = Alignment.CenterVertically
) {
Text(
- text = selectedOption,
- style = TextStyle(
+ text = selectedOption, style = TextStyle(
fontSize = 16.sp,
color = textColor.copy(alpha = 0.8f),
fontFamily = FontFamily(Font(R.font.sf_pro))
)
)
Text(
- text = "",
- style = TextStyle(
+ text = "", style = TextStyle(
fontSize = 16.sp,
color = textColor.copy(alpha = 0.6f),
fontFamily = FontFamily(Font(R.font.sf_pro))
- ),
- modifier = Modifier
- .padding(start = 6.dp)
+ ), modifier = Modifier.padding(start = 6.dp)
)
}
@@ -774,19 +819,22 @@ private fun DropdownMenuComponent(
}
}
}
- if (independent && description != null){
+ if (independent && description != null) {
Box(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp)
- .background(if (isSystemInDarkTheme()) Color(0xFF000000) else Color(0xFFF2F2F7))
- ){
+ .background(
+ if (isSystemInDarkTheme()) Color(0xFF000000) else Color(0xFFF2F2F7)
+ )
+ ) {
Text(
- text = description,
- style = TextStyle(
+ text = description, style = TextStyle(
fontSize = 12.sp,
fontWeight = FontWeight.Light,
- color = (if (isSystemInDarkTheme()) Color.White else Color.Black).copy(alpha = 0.6f),
+ color = (if (isSystemInDarkTheme()) Color.White else Color.Black).copy(
+ alpha = 0.6f
+ ),
fontFamily = FontFamily(Font(R.font.sf_pro))
)
)
diff --git a/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/AdaptiveStrengthScreen.kt b/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/AdaptiveStrengthScreen.kt
index 9a2f9ac..6ba885e 100644
--- a/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/AdaptiveStrengthScreen.kt
+++ b/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/AdaptiveStrengthScreen.kt
@@ -81,7 +81,7 @@ fun AdaptiveStrengthScreen(viewModel: AirPodsViewModel, navController: NavContro
backdrop = rememberLayerBackdrop(),
modifier = Modifier.fillMaxWidth(),
maxScale = 0.05f,
- tint = if (isSystemInDarkTheme()) Color(0xFF916100) else Color(0xFFE59900)
+ surfaceColor = if (isSystemInDarkTheme()) Color(0xFF916100) else Color(0xFFE59900)
) {
Text(
stringResource(R.string.unlock_advanced_features),
diff --git a/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/AirPodsSettingsScreen.kt b/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/AirPodsSettingsScreen.kt
index d1b9605..43d1bc9 100644
--- a/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/AirPodsSettingsScreen.kt
+++ b/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/AirPodsSettingsScreen.kt
@@ -239,13 +239,11 @@ fun AirPodsSettingsScreen(viewModel: AirPodsViewModel, navController: NavControl
item(key = "spacer_call") { Spacer(modifier = Modifier.height(16.dp)) }
item(key = "call_control") {
- val flipped =
- state.controlStates[AACPManager.Companion.ControlCommandIdentifiers.CALL_MANAGEMENT_CONFIG]?.take(
- 2
- )?.equals(byteArrayOf(0x00.toByte(), 0x02.toByte()))
+ val bytes = state.controlStates[AACPManager.Companion.ControlCommandIdentifiers.CALL_MANAGEMENT_CONFIG]?.take(2)?.toByteArray() ?: byteArrayOf(0x00, 0x00)
+ val flipped = bytes[1] == 0x02.toByte()
CallControlSettings(
hazeState = hazeState,
- flipped = flipped == true,
+ flipped = flipped,
onCallControlValueChanged = {
viewModel.setControlCommandValue(
AACPManager.Companion.ControlCommandIdentifiers.CALL_MANAGEMENT_CONFIG,
@@ -277,7 +275,7 @@ fun AirPodsSettingsScreen(viewModel: AirPodsViewModel, navController: NavControl
backdrop = rememberLayerBackdrop(),
modifier = Modifier.fillMaxWidth(),
maxScale = 0.05f,
- tint = if (isSystemInDarkTheme()) Color(0xFF916100) else Color(
+ surfaceColor = if (isSystemInDarkTheme()) Color(0xFF916100) else Color(
0xFFE59900
)
) {
diff --git a/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/AppSettingsScreen.kt b/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/AppSettingsScreen.kt
index c4e49d1..d4c195c 100644
--- a/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/AppSettingsScreen.kt
+++ b/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/AppSettingsScreen.kt
@@ -19,6 +19,7 @@
package me.kavishdevar.librepods.presentation.screens
import android.content.Intent
+import android.content.pm.PackageManager
import android.os.Build
import android.widget.Toast
import androidx.compose.foundation.background
@@ -71,13 +72,13 @@ import com.kyant.backdrop.backdrops.rememberLayerBackdrop
import dev.chrisbanes.haze.hazeSource
import me.kavishdevar.librepods.BuildConfig
import me.kavishdevar.librepods.R
+import me.kavishdevar.librepods.presentation.components.DeviceInfoCard
import me.kavishdevar.librepods.presentation.components.NavigationButton
import me.kavishdevar.librepods.presentation.components.StyledButton
import me.kavishdevar.librepods.presentation.components.StyledScaffold
import me.kavishdevar.librepods.presentation.components.StyledSlider
import me.kavishdevar.librepods.presentation.components.StyledToggle
import me.kavishdevar.librepods.presentation.viewmodel.AppSettingsViewModel
-import java.util.Locale.getDefault
@Composable
fun AppSettingsScreen(
@@ -106,7 +107,7 @@ fun AppSettingsScreen(
val backgroundColor = if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFFFFFFF)
val textColor = if (isDarkTheme) Color.White else Color.Black
- if (!state.isPremium) {
+ if (!state.isPremium && state.connectionSuccessful) {
StyledButton(
onClick = {
navController.navigate("purchase_screen")
@@ -114,7 +115,7 @@ fun AppSettingsScreen(
backdrop = rememberLayerBackdrop(),
modifier = Modifier.fillMaxWidth(),
maxScale = 0.05f,
- tint = if (isSystemInDarkTheme()) Color(0xFF916100) else Color(0xFFE59900)
+ surfaceColor = if (isSystemInDarkTheme()) Color(0xFF916100) else Color(0xFFE59900)
) {
Text(
stringResource(R.string.unlock_advanced_features),
@@ -128,246 +129,270 @@ fun AppSettingsScreen(
}
}
- StyledToggle(
- title = stringResource(R.string.widget),
- label = stringResource(R.string.show_phone_battery_in_widget),
- description = stringResource(R.string.show_phone_battery_in_widget_description),
- checked = state.showPhoneBatteryInWidget,
- onCheckedChange = viewModel::setShowPhoneBatteryInWidget,
- enabled = state.isPremium
- )
-
- Text(
- text = stringResource(R.string.conversational_awareness), style = TextStyle(
- fontSize = 14.sp,
- fontWeight = FontWeight.Bold,
- color = textColor.copy(alpha = 0.6f),
- fontFamily = FontFamily(Font(R.font.sf_pro))
- ), modifier = Modifier.padding(16.dp, bottom = 2.dp, top = 24.dp)
- )
-
- Spacer(modifier = Modifier.height(2.dp))
-
- Column(
- modifier = Modifier
- .fillMaxWidth()
- .background(
- backgroundColor, RoundedCornerShape(28.dp)
- )
- .padding(vertical = 4.dp)
- ) {
+ if (state.connectionSuccessful) {
StyledToggle(
- label = stringResource(R.string.conversational_awareness_pause_music),
- description = stringResource(R.string.conversational_awareness_pause_music_description),
- checked = state.conversationalAwarenessPauseMusicEnabled,
- onCheckedChange = viewModel::setConversationalAwarenessPauseMusicEnabled,
- independent = false,
+ title = stringResource(R.string.widget),
+ label = stringResource(R.string.show_phone_battery_in_widget),
+ description = stringResource(R.string.show_phone_battery_in_widget_description),
+ checked = state.showPhoneBatteryInWidget,
+ onCheckedChange = viewModel::setShowPhoneBatteryInWidget,
enabled = state.isPremium
)
- HorizontalDivider(
- thickness = 1.dp,
- color = Color(0x40888888),
- modifier = Modifier.padding(horizontal = 12.dp)
+ Text(
+ text = stringResource(R.string.conversational_awareness), style = TextStyle(
+ fontSize = 14.sp,
+ fontWeight = FontWeight.Bold,
+ color = textColor.copy(alpha = 0.6f),
+ fontFamily = FontFamily(Font(R.font.sf_pro))
+ ), modifier = Modifier.padding(16.dp, bottom = 2.dp, top = 24.dp)
)
- StyledToggle(
- label = stringResource(R.string.relative_conversational_awareness_volume),
- description = stringResource(R.string.relative_conversational_awareness_volume_description),
- checked = state.relativeConversationalAwarenessVolumeEnabled,
- onCheckedChange = viewModel::setRelativeConversationalAwarenessVolumeEnabled,
- independent = false,
- enabled = state.isPremium,
- )
- }
+ Spacer(modifier = Modifier.height(2.dp))
- Spacer(modifier = Modifier.height(16.dp))
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .background(
+ backgroundColor, RoundedCornerShape(28.dp)
+ )
+ .padding(vertical = 4.dp)
+ ) {
+ StyledToggle(
+ label = stringResource(R.string.conversational_awareness_pause_music),
+ description = stringResource(R.string.conversational_awareness_pause_music_description),
+ checked = state.conversationalAwarenessPauseMusicEnabled,
+ onCheckedChange = viewModel::setConversationalAwarenessPauseMusicEnabled,
+ independent = false,
+ enabled = state.isPremium
+ )
- val conversationalAwarenessVolume = state.conversationalAwarenessVolume
- LaunchedEffect(conversationalAwarenessVolume) {
- viewModel.setConversationalAwarenessVolume(conversationalAwarenessVolume)
- }
+ HorizontalDivider(
+ thickness = 1.dp,
+ color = Color(0x40888888),
+ modifier = Modifier.padding(horizontal = 12.dp)
+ )
- StyledSlider(
- label = stringResource(R.string.conversational_awareness_volume),
- value = conversationalAwarenessVolume,
- valueRange = 10f..85f,
- snapPoints = listOf(44f),
- startLabel = "10%",
- endLabel = "85%",
- onValueChange = { newValue -> viewModel.setConversationalAwarenessVolume(newValue) },
- independent = true,
- enabled = state.isPremium
- )
+ StyledToggle(
+ label = stringResource(R.string.relative_conversational_awareness_volume),
+ description = stringResource(R.string.relative_conversational_awareness_volume_description),
+ checked = state.relativeConversationalAwarenessVolumeEnabled,
+ onCheckedChange = viewModel::setRelativeConversationalAwarenessVolumeEnabled,
+ independent = false,
+ enabled = state.isPremium,
+ )
+ }
- if (!BuildConfig.PLAY_BUILD) {
Spacer(modifier = Modifier.height(16.dp))
- NavigationButton(
- to = "",
- title = stringResource(R.string.camera_control),
- name = stringResource(R.string.set_custom_camera_package),
- navController = navController,
- onClick = {
- if (state.isPremium) viewModel.setShowCameraDialog(true)
+ val conversationalAwarenessVolume = state.conversationalAwarenessVolume
+ LaunchedEffect(conversationalAwarenessVolume) {
+ viewModel.setConversationalAwarenessVolume(conversationalAwarenessVolume)
+ }
+
+ StyledSlider(
+ label = stringResource(R.string.conversational_awareness_volume),
+ value = conversationalAwarenessVolume,
+ valueRange = 10f..85f,
+ snapPoints = listOf(44f),
+ startLabel = "10%",
+ endLabel = "85%",
+ onValueChange = { newValue ->
+ viewModel.setConversationalAwarenessVolume(
+ newValue
+ )
},
independent = true,
- description = stringResource(R.string.camera_control_app_description)
- )
- }
-
- Spacer(modifier = Modifier.height(16.dp))
- if (!BuildConfig.PLAY_BUILD) {
- StyledToggle(
- title = stringResource(R.string.ear_detection),
- label = stringResource(R.string.disconnect_when_not_wearing),
- description = stringResource(R.string.disconnect_when_not_wearing_description),
- checked = state.disconnectWhenNotWearing,
- onCheckedChange = viewModel::setDisconnectWhenNotWearing,
enabled = state.isPremium
)
- }
- Text(
- text = stringResource(R.string.takeover_airpods_state), style = TextStyle(
- fontSize = 14.sp,
- fontWeight = FontWeight.Bold,
- color = textColor.copy(alpha = 0.6f),
- fontFamily = FontFamily(Font(R.font.sf_pro))
- ), modifier = Modifier.padding(16.dp, bottom = 2.dp, top = 24.dp)
- )
+// if (!BuildConfig.PLAY_BUILD) {
+// Spacer(modifier = Modifier.height(16.dp))
+//
+// NavigationButton(
+// to = "",
+// title = stringResource(R.string.camera_control),
+// name = stringResource(R.string.set_custom_camera_package),
+// navController = navController,
+// onClick = {
+// if (state.isPremium) viewModel.setShowCameraDialog(true)
+// },
+// independent = true,
+// description = stringResource(R.string.camera_control_app_description)
+// )
+// }
- Spacer(modifier = Modifier.height(4.dp))
-
- Column(
- modifier = Modifier
- .fillMaxWidth()
- .background(
- backgroundColor, RoundedCornerShape(28.dp)
+ Spacer(modifier = Modifier.height(16.dp))
+ if (context.checkSelfPermission("android.permission.BLUETOOTH_PRIVILEGED") == PackageManager.PERMISSION_GRANTED) {
+ StyledToggle(
+ title = stringResource(R.string.ear_detection),
+ label = stringResource(R.string.disconnect_when_not_wearing),
+ description = stringResource(R.string.disconnect_when_not_wearing_description),
+ checked = state.disconnectWhenNotWearing,
+ onCheckedChange = viewModel::setDisconnectWhenNotWearing,
+ enabled = state.isPremium
)
- .padding(vertical = 4.dp)
- ) {
- StyledToggle(
- label = stringResource(R.string.takeover_disconnected),
- description = stringResource(R.string.takeover_disconnected_desc),
- checked = state.takeoverWhenDisconnected,
- onCheckedChange = viewModel::setTakeoverWhenDisconnected,
- independent = false,
- enabled = state.isPremium
- )
- HorizontalDivider(
- thickness = 1.dp,
- color = Color(0x40888888),
- modifier = Modifier.padding(horizontal = 12.dp)
+ }
+
+ Text(
+ text = stringResource(R.string.takeover_airpods_state), style = TextStyle(
+ fontSize = 14.sp,
+ fontWeight = FontWeight.Bold,
+ color = textColor.copy(alpha = 0.6f),
+ fontFamily = FontFamily(Font(R.font.sf_pro))
+ ), modifier = Modifier.padding(16.dp, bottom = 2.dp, top = 24.dp)
)
- StyledToggle(
- label = stringResource(R.string.takeover_idle),
- description = stringResource(R.string.takeover_idle_desc),
- checked = state.takeoverWhenIdle,
- onCheckedChange = viewModel::setTakeoverWhenIdle,
- independent = false,
- enabled = state.isPremium
- )
- HorizontalDivider(
- thickness = 1.dp,
- color = Color(0x40888888),
- modifier = Modifier.padding(horizontal = 12.dp)
- )
+ Spacer(modifier = Modifier.height(4.dp))
- StyledToggle(
- label = stringResource(R.string.takeover_music),
- description = stringResource(R.string.takeover_music_desc),
- checked = state.takeoverWhenMusic,
- onCheckedChange = viewModel::setTakeoverWhenMusic,
- independent = false,
- enabled = state.isPremium
- )
- HorizontalDivider(
- thickness = 1.dp,
- color = Color(0x40888888),
- modifier = Modifier.padding(horizontal = 12.dp)
- )
-
- StyledToggle(
- label = stringResource(R.string.takeover_call),
- description = stringResource(R.string.takeover_call_desc),
- checked = state.takeoverWhenCall,
- onCheckedChange = viewModel::setTakeoverWhenCall,
- independent = false,
- enabled = state.isPremium
- )
- }
-
- Spacer(modifier = Modifier.height(16.dp))
-
- Text(
- text = stringResource(R.string.takeover_phone_state), style = TextStyle(
- fontSize = 14.sp,
- fontWeight = FontWeight.Bold,
- color = textColor.copy(alpha = 0.6f),
- fontFamily = FontFamily(Font(R.font.sf_pro))
- ), modifier = Modifier.padding(horizontal = 16.dp)
- )
- Spacer(modifier = Modifier.height(4.dp))
- Column(
- modifier = Modifier
- .fillMaxWidth()
- .background(
- backgroundColor, RoundedCornerShape(28.dp)
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .background(
+ backgroundColor, RoundedCornerShape(28.dp)
+ )
+ .padding(vertical = 4.dp)
+ ) {
+ StyledToggle(
+ label = stringResource(R.string.takeover_disconnected),
+ description = stringResource(R.string.takeover_disconnected_desc),
+ checked = state.takeoverWhenDisconnected,
+ onCheckedChange = viewModel::setTakeoverWhenDisconnected,
+ independent = false,
+ enabled = state.isPremium
)
- .padding(vertical = 4.dp)
- ) {
- StyledToggle(
- label = stringResource(R.string.takeover_ringing_call),
- description = stringResource(R.string.takeover_ringing_call_desc),
- checked = state.takeoverWhenRingingCall,
- onCheckedChange = viewModel::setTakeoverWhenRingingCall,
- independent = false,
- enabled = state.isPremium
+ HorizontalDivider(
+ thickness = 1.dp,
+ color = Color(0x40888888),
+ modifier = Modifier.padding(horizontal = 12.dp)
+ )
+
+ StyledToggle(
+ label = stringResource(R.string.takeover_idle),
+ description = stringResource(R.string.takeover_idle_desc),
+ checked = state.takeoverWhenIdle,
+ onCheckedChange = viewModel::setTakeoverWhenIdle,
+ independent = false,
+ enabled = state.isPremium
+ )
+ HorizontalDivider(
+ thickness = 1.dp,
+ color = Color(0x40888888),
+ modifier = Modifier.padding(horizontal = 12.dp)
+ )
+
+ StyledToggle(
+ label = stringResource(R.string.takeover_music),
+ description = stringResource(R.string.takeover_music_desc),
+ checked = state.takeoverWhenMusic,
+ onCheckedChange = viewModel::setTakeoverWhenMusic,
+ independent = false,
+ enabled = state.isPremium
+ )
+ HorizontalDivider(
+ thickness = 1.dp,
+ color = Color(0x40888888),
+ modifier = Modifier.padding(horizontal = 12.dp)
+ )
+
+ StyledToggle(
+ label = stringResource(R.string.takeover_call),
+ description = stringResource(R.string.takeover_call_desc),
+ checked = state.takeoverWhenCall,
+ onCheckedChange = viewModel::setTakeoverWhenCall,
+ independent = false,
+ enabled = state.isPremium
+ )
+ }
+
+ Spacer(modifier = Modifier.height(16.dp))
+
+ Text(
+ text = stringResource(R.string.takeover_phone_state), style = TextStyle(
+ fontSize = 14.sp,
+ fontWeight = FontWeight.Bold,
+ color = textColor.copy(alpha = 0.6f),
+ fontFamily = FontFamily(Font(R.font.sf_pro))
+ ), modifier = Modifier.padding(horizontal = 16.dp)
)
- HorizontalDivider(
- thickness = 1.dp,
- color = Color(0x40888888),
- modifier = Modifier.padding(horizontal = 12.dp)
+ Spacer(modifier = Modifier.height(4.dp))
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .background(
+ backgroundColor, RoundedCornerShape(28.dp)
+ )
+ .padding(vertical = 4.dp)
+ ) {
+ StyledToggle(
+ label = stringResource(R.string.takeover_ringing_call),
+ description = stringResource(R.string.takeover_ringing_call_desc),
+ checked = state.takeoverWhenRingingCall,
+ onCheckedChange = viewModel::setTakeoverWhenRingingCall,
+ independent = false,
+ enabled = state.isPremium
+ )
+ HorizontalDivider(
+ thickness = 1.dp,
+ color = Color(0x40888888),
+ modifier = Modifier.padding(horizontal = 12.dp)
+ )
+
+ StyledToggle(
+ label = stringResource(R.string.takeover_media_start),
+ description = stringResource(R.string.takeover_media_start_desc),
+ checked = state.takeoverWhenMediaStart,
+ onCheckedChange = viewModel::setTakeoverWhenMediaStart,
+ independent = false,
+ enabled = state.isPremium
+ )
+ }
+
+ Text(
+ text = stringResource(R.string.advanced_options), style = TextStyle(
+ fontSize = 14.sp,
+ fontWeight = FontWeight.Bold,
+ color = textColor.copy(alpha = 0.6f),
+ fontFamily = FontFamily(Font(R.font.sf_pro))
+ ), modifier = Modifier.padding(16.dp, bottom = 2.dp, top = 24.dp)
)
+ Spacer(modifier = Modifier.height(2.dp))
+
StyledToggle(
- label = stringResource(R.string.takeover_media_start),
- description = stringResource(R.string.takeover_media_start_desc),
- checked = state.takeoverWhenMediaStart,
- onCheckedChange = viewModel::setTakeoverWhenMediaStart,
- independent = false,
+ label = stringResource(R.string.use_alternate_head_tracking_packets),
+ description = stringResource(R.string.use_alternate_head_tracking_packets_description),
+ checked = state.useAlternateHeadTrackingPackets,
+ onCheckedChange = viewModel::setUseAlternateHeadTrackingPackets,
+ independent = true,
enabled = state.isPremium
)
+ } else {
+ Text(
+ text = stringResource(R.string.customizations_unavailable),
+ style = TextStyle(
+ fontSize = 16.sp,
+ fontWeight = FontWeight.Normal,
+ fontFamily = FontFamily(Font(R.font.sf_pro)),
+ color = textColor.copy(alpha = 0.6f),
+ ),
+ modifier = Modifier
+ .padding(horizontal = 16.dp)
+ .padding(top = 16.dp)
+ )
}
- Text(
- text = stringResource(R.string.advanced_options), style = TextStyle(
- fontSize = 14.sp,
- fontWeight = FontWeight.Bold,
- color = textColor.copy(alpha = 0.6f),
- fontFamily = FontFamily(Font(R.font.sf_pro))
- ), modifier = Modifier.padding(16.dp, bottom = 2.dp, top = 24.dp)
- )
-
- Spacer(modifier = Modifier.height(2.dp))
-
- StyledToggle(
- label = stringResource(R.string.use_alternate_head_tracking_packets),
- description = stringResource(R.string.use_alternate_head_tracking_packets_description),
- checked = state.useAlternateHeadTrackingPackets,
- onCheckedChange = viewModel::setUseAlternateHeadTrackingPackets,
- independent = true,
- enabled = state.isPremium
- )
-
if (BuildConfig.FLAVOR == "xposed") {
Spacer(modifier = Modifier.height(16.dp))
- val restartBluetoothText = stringResource(R.string.found_offset_restart_bluetooth)
+ val restartBluetoothText =
+ stringResource(R.string.found_offset_restart_bluetooth)
StyledToggle(
- label = stringResource(R.string.act_as_an_apple_device) + " (${stringResource(R.string.requires_xposed)})",
+ label = stringResource(R.string.act_as_an_apple_device) + " (${
+ stringResource(
+ R.string.requires_xposed
+ )
+ })",
description = stringResource(R.string.act_as_an_apple_device_description),
checked = state.vendorIdHook,
onCheckedChange = { enabled ->
@@ -379,8 +404,8 @@ fun AppSettingsScreen(
)
}
-
if (!BuildConfig.PLAY_BUILD) {
+ Spacer(modifier = Modifier.height(16.dp))
NavigationButton(
to = "troubleshooting",
name = stringResource(R.string.troubleshooting),
@@ -418,15 +443,16 @@ fun AppSettingsScreen(
val intent = Intent(Intent.ACTION_SENDTO).apply {
data = "mailto:".toUri()
putExtra(Intent.EXTRA_EMAIL, arrayOf("contact@kavish.xyz"))
- putExtra(Intent.EXTRA_SUBJECT, "LibrePods: ")
+ putExtra(Intent.EXTRA_SUBJECT, "LibrePods: ")
putExtra(
Intent.EXTRA_TEXT,
- "\n\n\n----------" +
+ "Describe your issue here:" +
+ "\n\n\n\n----------" +
"\nPhone details:" +
- "\nDEVICE: ${Build.DEVICE}" +
- "\nMANUFACTURER: ${Build.MANUFACTURER} (${Build.BRAND})" +
+ "\nMANUFACTURER: ${Build.MANUFACTURER}" +
"\nMODEL: ${Build.MODEL} (${Build.PRODUCT})" +
- "\nVERSION: ${Build.DISPLAY} (${Build.VERSION.SDK_INT_FULL})" +
+ "\nDISPLAY_VERSION: ${Build.DISPLAY} (${Build.PRODUCT})" +
+ "\nID: ${Build.ID} (SDK ${Build.VERSION.SDK_INT_FULL})" +
"\n\nApp details:" +
"\nVERSION: ${BuildConfig.VERSION_NAME}" +
"\nVERSION_CODE: ${BuildConfig.VERSION_CODE}" +
@@ -478,7 +504,8 @@ fun AppSettingsScreen(
)
}
- Spacer(modifier = Modifier.height(8.dp))
+ Spacer(modifier = Modifier.height(20.dp))
+ DeviceInfoCard()
Text(
text = stringResource(R.string.about), style = TextStyle(
@@ -486,7 +513,7 @@ fun AppSettingsScreen(
fontWeight = FontWeight.Bold,
color = textColor.copy(alpha = 0.6f),
fontFamily = FontFamily(Font(R.font.sf_pro))
- ), modifier = Modifier.padding(16.dp, bottom = 2.dp, top = 24.dp)
+ ), modifier = Modifier.padding(start = 16.dp, bottom = 2.dp, top = 24.dp)
)
val rowHeight = remember { mutableStateOf(0.dp) }
diff --git a/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/HeadTrackingScreen.kt b/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/HeadTrackingScreen.kt
index 6137ec1..b5ca451 100644
--- a/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/HeadTrackingScreen.kt
+++ b/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/HeadTrackingScreen.kt
@@ -175,7 +175,7 @@ fun HeadTrackingScreen(viewModel: AirPodsViewModel, navController: NavController
backdrop = rememberLayerBackdrop(),
modifier = Modifier.fillMaxWidth(),
maxScale = 0.05f,
- tint = if (isSystemInDarkTheme()) Color(0xFF916100) else Color(0xFFE59900)
+ surfaceColor = if (isSystemInDarkTheme()) Color(0xFF916100) else Color(0xFFE59900)
) {
Text(
stringResource(R.string.unlock_advanced_features),
diff --git a/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/HearingProtectionScreen.kt b/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/HearingProtectionScreen.kt
index 7bbbfd5..cf484b8 100644
--- a/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/HearingProtectionScreen.kt
+++ b/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/HearingProtectionScreen.kt
@@ -18,29 +18,39 @@
package me.kavishdevar.librepods.presentation.screens
+import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.font.Font
+import androidx.compose.ui.text.font.FontFamily
+import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import androidx.navigation.NavController
import com.kyant.backdrop.backdrops.layerBackdrop
import com.kyant.backdrop.backdrops.rememberLayerBackdrop
-import me.kavishdevar.librepods.BuildConfig
import me.kavishdevar.librepods.R
-import me.kavishdevar.librepods.presentation.components.StyledScaffold
-import me.kavishdevar.librepods.presentation.components.StyledToggle
import me.kavishdevar.librepods.bluetooth.AACPManager
import me.kavishdevar.librepods.bluetooth.ATTHandles
+import me.kavishdevar.librepods.presentation.components.StyledButton
+import me.kavishdevar.librepods.presentation.components.StyledScaffold
+import me.kavishdevar.librepods.presentation.components.StyledToggle
import me.kavishdevar.librepods.presentation.viewmodel.AirPodsViewModel
@Composable
-fun HearingProtectionScreen(viewModel: AirPodsViewModel) {
+fun HearingProtectionScreen(viewModel: AirPodsViewModel, navController: NavController) {
val backdrop = rememberLayerBackdrop()
val state by viewModel.uiState.collectAsState()
StyledScaffold(
@@ -53,7 +63,27 @@ fun HearingProtectionScreen(viewModel: AirPodsViewModel) {
.padding(horizontal = 16.dp)
) {
Spacer(modifier = Modifier.height(spacerHeight))
-
+ if (!state.isPremium) {
+ StyledButton(
+ onClick = {
+ navController.navigate("purchase_screen")
+ },
+ backdrop = rememberLayerBackdrop(),
+ modifier = Modifier.fillMaxWidth(),
+ maxScale = 0.05f,
+ surfaceColor = if (isSystemInDarkTheme()) Color(0xFF916100) else Color(0xFFE59900)
+ ) {
+ Text(
+ stringResource(R.string.unlock_advanced_features),
+ style = TextStyle(
+ fontSize = 16.sp,
+ fontWeight = FontWeight.Medium,
+ fontFamily = FontFamily(Font(R.font.sf_pro)),
+ color = Color.White
+ ),
+ )
+ }
+ }
if (state.vendorIdHook) {
StyledToggle(
title = stringResource(R.string.environmental_noise),
diff --git a/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/PressAndHoldSettingsScreen.kt b/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/PressAndHoldSettingsScreen.kt
index 77dd621..5140d0d 100644
--- a/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/PressAndHoldSettingsScreen.kt
+++ b/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/PressAndHoldSettingsScreen.kt
@@ -130,7 +130,7 @@ fun LongPress(viewModel: AirPodsViewModel, name: String, navController: NavContr
backdrop = rememberLayerBackdrop(),
modifier = Modifier.fillMaxWidth(),
maxScale = 0.05f,
- tint = if (isSystemInDarkTheme()) Color(0xFF916100) else Color(0xFFE59900)
+ surfaceColor = if (isSystemInDarkTheme()) Color(0xFF916100) else Color(0xFFE59900)
) {
Text(
stringResource(R.string.unlock_advanced_features),
diff --git a/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/PurchaseScreen.kt b/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/PurchaseScreen.kt
index 035dfc3..e60d38e 100644
--- a/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/PurchaseScreen.kt
+++ b/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/PurchaseScreen.kt
@@ -99,7 +99,7 @@ fun PurchaseScreen(
.padding(horizontal = 16.dp, vertical = 4.dp)
) {
Text(
- text = "Free features",
+ text = stringResource(R.string.free_features),
style = TextStyle(
fontSize = 14.sp,
fontWeight = FontWeight.Bold,
@@ -242,7 +242,7 @@ fun PurchaseScreen(
.padding(horizontal = 16.dp, vertical = 4.dp)
) {
Text(
- text = "Advanced features",
+ text = stringResource(R.string.advanced_features),
style = TextStyle(
fontSize = 14.sp,
fontWeight = FontWeight.Bold,
@@ -288,6 +288,36 @@ fun PurchaseScreen(
modifier = Modifier
.padding(horizontal = 12.dp)
)
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 12.dp, vertical = 4.dp),
+ verticalArrangement = Arrangement.spacedBy(2.dp)
+ ) {
+ Text(
+ text = stringResource(R.string.digital_assistant_on_long_press),
+ style = TextStyle(
+ fontSize = 16.sp,
+ fontWeight = FontWeight.Normal,
+ fontFamily = FontFamily(Font(R.font.sf_pro)),
+ color = textColor
+ )
+ )
+ Text(
+ text = stringResource(R.string.digital_assistant_on_long_press_description),
+ style = TextStyle(
+ fontSize = 12.sp,
+ color = textColor.copy(0.6f),
+ fontFamily = FontFamily(Font(R.font.sf_pro)),
+ )
+ )
+ }
+ HorizontalDivider(
+ thickness = 1.dp,
+ color = Color(0x40888888),
+ modifier = Modifier
+ .padding(horizontal = 12.dp)
+ )
Column(
modifier = Modifier
.fillMaxWidth()
@@ -456,7 +486,8 @@ fun PurchaseScreen(
backdrop = rememberLayerBackdrop(),
modifier = Modifier.fillMaxWidth(),
maxScale = 0.05f,
- tint = if (isSystemInDarkTheme()) Color(0xFF916100) else Color(0xFFE59900)
+ surfaceColor = if (isSystemInDarkTheme()) Color(0xFF0091FF)
+ else Color(0xFF0088FF) // if (isSystemInDarkTheme()) Color(0xFF916100) else Color(0xFFE59900)
) {
Text(
stringResource(R.string.buy_price, state.price),
@@ -478,6 +509,7 @@ fun PurchaseScreen(
backdrop = rememberLayerBackdrop(),
modifier = Modifier.fillMaxWidth(),
maxScale = 0.05f,
+ isInteractive = false
) {
Text(
stringResource(R.string.restore_purchases),
diff --git a/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/RenameScreen.kt b/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/RenameScreen.kt
index 8fc352e..95bb80c 100644
--- a/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/RenameScreen.kt
+++ b/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/RenameScreen.kt
@@ -56,8 +56,6 @@ import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.core.content.edit
-import com.kyant.backdrop.backdrops.layerBackdrop
-import com.kyant.backdrop.backdrops.rememberLayerBackdrop
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
import me.kavishdevar.librepods.R
import me.kavishdevar.librepods.presentation.components.StyledScaffold
@@ -79,15 +77,12 @@ fun RenameScreen(viewModel: AirPodsViewModel) {
name.value = name.value.copy(selection = TextRange(name.value.text.length))
}
- val backdrop = rememberLayerBackdrop()
-
StyledScaffold(
title = stringResource(R.string.name),
) { spacerHeight ->
Column(
modifier = Modifier
.fillMaxSize()
- .layerBackdrop(backdrop)
.padding(horizontal = 16.dp)
) {
Spacer(modifier = Modifier.height(spacerHeight))
diff --git a/android/app/src/main/java/me/kavishdevar/librepods/presentation/viewmodel/AirPodsViewModel.kt b/android/app/src/main/java/me/kavishdevar/librepods/presentation/viewmodel/AirPodsViewModel.kt
index 3d41110..f849ca8 100644
--- a/android/app/src/main/java/me/kavishdevar/librepods/presentation/viewmodel/AirPodsViewModel.kt
+++ b/android/app/src/main/java/me/kavishdevar/librepods/presentation/viewmodel/AirPodsViewModel.kt
@@ -33,7 +33,6 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
-import me.kavishdevar.librepods.BuildConfig
import me.kavishdevar.librepods.billing.BillingManager
import me.kavishdevar.librepods.bluetooth.AACPManager
import me.kavishdevar.librepods.bluetooth.AACPManager.Companion.ControlCommandIdentifiers
@@ -110,8 +109,6 @@ class AirPodsViewModel(
private var isDemoMode = false
val demoActivated = MutableSharedFlow()
- private var billingFirstCollectDone = false
-
private val listeners =
mutableMapOf()
@@ -163,12 +160,12 @@ class AirPodsViewModel(
private fun observeBilling() {
if (isDemoMode) return
viewModelScope.launch {
- if (!BuildConfig.PLAY_BUILD) billingFirstCollectDone = true // FOSS doesn't send multiple events
+// if (!BuildConfig.PLAY_BUILD) billingFirstCollectDone = true // FOSS doesn't send multiple events
BillingManager.provider.isPremium.collect { premium ->
- if (!billingFirstCollectDone) {
- billingFirstCollectDone = true
- return@collect
- }
+// if (!billingFirstCollectDone) {
+// billingFirstCollectDone = true
+// return@collect
+// }
if (!premium) {
setControlCommandBoolean(
ControlCommandIdentifiers.CONVERSATION_DETECT_CONFIG,
@@ -184,8 +181,9 @@ class AirPodsViewModel(
private fun observeBroadcasts() {
broadcastReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
- if (!isDemoMode) when (intent?.action) {
- AirPodsNotifications.AIRPODS_CONNECTED -> {
+ val action = intent?.action ?: return
+ if (!isDemoMode) when (action) {
+ AirPodsNotifications.AIRPODS_L2CAP_CONNECTED -> {
_uiState.update {
it.copy(isLocallyConnected = true)
}
@@ -198,10 +196,8 @@ class AirPodsViewModel(
}
AirPodsNotifications.BATTERY_DATA -> {
- val data = intent.getParcelableArrayListExtra("data", Battery::class.java)
- ?.toList() ?: emptyList()
_uiState.update {
- it.copy(battery = data)
+ it.copy(battery = service.getBattery())
}
}
@@ -276,7 +272,7 @@ class AirPodsViewModel(
}
}
- listeners[identifier] = listener as AACPManager.ControlCommandListener
+ listeners[identifier] = listener
}
// I'm lazy, sorry.
@@ -435,7 +431,15 @@ class AirPodsViewModel(
_uiState.update { it.copy(loudSoundReductionEnabled = value[0].toInt() == 0x01) }
}
viewModelScope.launch(Dispatchers.IO) {
- service.attManager?.write(handle, value)
+ try {
+ service.attManager?.connect()
+ while (service.attManager?.socket?.isConnected != true) {
+ delay(250)
+ }
+ service.attManager?.write(handle, value)
+ } catch (e: Exception) {
+ e.printStackTrace()
+ }
}
}
@@ -458,13 +462,16 @@ class AirPodsViewModel(
fun observeATT() {
viewModelScope.launch(Dispatchers.IO) {
service.attManager?.connect()
+ while (service.attManager?.socket?.isConnected != true) {
+ delay(1000)
+ }
service.attManager?.enableNotifications(ATTHandles.LOUD_SOUND_REDUCTION)
service.attManager?.enableNotifications(ATTHandles.TRANSPARENCY)
service.attManager?.enableNotifications(ATTHandles.HEARING_AID)
while (true) {
refreshATT()
- delay(10000)
+ delay(15000)
}
}
}
@@ -489,10 +496,6 @@ class AirPodsViewModel(
}
}
-// fun purchase(context: Context) {
-// BillingManager.provider.purchase(context as Activity)
-// }
-
fun activateDemoMode() {
isDemoMode = true
viewModelScope.launch {
@@ -525,8 +528,17 @@ class AirPodsViewModel(
modelName = fakeInstance.model.displayName,
actualModel = fakeInstance.actualModelNumber,
serialNumbers = listOf("DEMO", "DEMO", "DEMO"),
- version3 = "Demo Firmware"
+ version3 = "Demo Firmware",
+// isPremium = true
)
}
}
+
+ fun sendPhoneMediaEQ(eq: FloatArray, phoneByte: Byte, mediaByte: Byte) {
+ service.aacpManager.sendPhoneMediaEQ(eq, phoneByte, mediaByte)
+ }
+
+ fun disconnect() {
+ service.disconnectAirPods()
+ }
}
diff --git a/android/app/src/main/java/me/kavishdevar/librepods/presentation/viewmodel/AppSettingsViewModel.kt b/android/app/src/main/java/me/kavishdevar/librepods/presentation/viewmodel/AppSettingsViewModel.kt
index 304db98..4879fba 100644
--- a/android/app/src/main/java/me/kavishdevar/librepods/presentation/viewmodel/AppSettingsViewModel.kt
+++ b/android/app/src/main/java/me/kavishdevar/librepods/presentation/viewmodel/AppSettingsViewModel.kt
@@ -2,6 +2,7 @@ package me.kavishdevar.librepods.presentation.viewmodel
import android.app.Application
import android.content.Context
+import android.content.SharedPreferences
import androidx.core.content.edit
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
@@ -32,7 +33,8 @@ data class AppSettingsUiState(
val cameraPackageValue: String = "",
val cameraPackageError: String? = null,
val vendorIdHook: Boolean = false,
- val isPremium: Boolean = false
+ val isPremium: Boolean = false,
+ val connectionSuccessful: Boolean = false
)
class AppSettingsViewModel(application: Application) : AndroidViewModel(application) {
@@ -43,9 +45,22 @@ class AppSettingsViewModel(application: Application) : AndroidViewModel(applicat
private val xposedRemotePref = XposedRemotePrefProvider.create()
+ val sharedPrefListener = SharedPreferences.OnSharedPreferenceChangeListener { sharedPref, key ->
+ if (key == "connection_successful") {
+ _uiState.update { it.copy(connectionSuccessful = sharedPref.getBoolean(key, false)) }
+ }
+ }
+
+
init {
loadSettings()
observeBilling()
+ sharedPreferences.registerOnSharedPreferenceChangeListener(sharedPrefListener)
+ }
+
+ override fun onCleared() {
+ sharedPreferences.unregisterOnSharedPreferenceChangeListener(sharedPrefListener)
+ super.onCleared()
}
private fun observeBilling() {
@@ -72,7 +87,8 @@ class AppSettingsViewModel(application: Application) : AndroidViewModel(applicat
useAlternateHeadTrackingPackets = sharedPreferences.getBoolean("use_alternate_head_tracking_packets", true),
conversationalAwarenessVolume = sharedPreferences.getInt("conversational_awareness_volume", 43).toFloat(),
cameraPackageValue = sharedPreferences.getString("custom_camera_package", "") ?: "",
- vendorIdHook = xposedRemotePref.getBoolean("vendor_id_hook", false)
+ vendorIdHook = xposedRemotePref.getBoolean("vendor_id_hook", false),
+ connectionSuccessful = sharedPreferences.getBoolean("connection_successful", false)
)
}
if (BuildConfig.FLAVOR == "xposed") {
diff --git a/android/app/src/main/java/me/kavishdevar/librepods/presentation/viewmodel/PurchaseViewModel.kt b/android/app/src/main/java/me/kavishdevar/librepods/presentation/viewmodel/PurchaseViewModel.kt
index 3ea9649..b9e5235 100644
--- a/android/app/src/main/java/me/kavishdevar/librepods/presentation/viewmodel/PurchaseViewModel.kt
+++ b/android/app/src/main/java/me/kavishdevar/librepods/presentation/viewmodel/PurchaseViewModel.kt
@@ -42,6 +42,6 @@ class PurchaseViewModel(application: Application) : AndroidViewModel(application
}
fun restorePurchases() {
- BillingManager.provider.queryPurchases()
+ BillingManager.provider.restorePurchases()
}
}
diff --git a/android/app/src/main/java/me/kavishdevar/librepods/services/AirPodsService.kt b/android/app/src/main/java/me/kavishdevar/librepods/services/AirPodsService.kt
index 7240214..b59ebeb 100644
--- a/android/app/src/main/java/me/kavishdevar/librepods/services/AirPodsService.kt
+++ b/android/app/src/main/java/me/kavishdevar/librepods/services/AirPodsService.kt
@@ -16,7 +16,7 @@
along with this program. If not, see .
*/
-@file:OptIn(ExperimentalEncodingApi::class) @file:Suppress("DEPRECATION")
+@file:OptIn(ExperimentalEncodingApi::class)
package me.kavishdevar.librepods.services
@@ -58,7 +58,7 @@ import android.os.ParcelUuid
import android.os.UserHandle
import android.provider.Settings
import android.telecom.TelecomManager
-import android.telephony.PhoneStateListener
+import android.telephony.TelephonyCallback
import android.telephony.TelephonyManager
import android.util.Log
import android.util.TypedValue
@@ -69,14 +69,7 @@ import androidx.annotation.RequiresApi
import androidx.annotation.RequiresPermission
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.core.app.NotificationCompat
-import androidx.core.app.ServiceCompat.START_STICKY
-import androidx.core.app.ServiceCompat.startForeground
-import androidx.core.content.ContextCompat.RECEIVER_EXPORTED
-import androidx.core.content.ContextCompat.getSystemService
-import androidx.core.content.ContextCompat.registerReceiver
-import androidx.core.content.ContextCompat.startActivity
import androidx.core.content.edit
-import com.google.android.datatransport.runtime.scheduling.persistence.EventStoreModule_PackageNameFactory.packageName
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -135,7 +128,6 @@ import java.nio.ByteBuffer
import java.nio.ByteOrder
import kotlin.io.encoding.Base64
import kotlin.io.encoding.ExperimentalEncodingApi
-import kotlin.jvm.java
private const val TAG = "AirPodsService"
@@ -230,7 +222,7 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
val packetLogsFlow: StateFlow> get() = _packetLogsFlow
private lateinit var telephonyManager: TelephonyManager
- private lateinit var phoneStateListener: PhoneStateListener
+ private lateinit var phoneStateListener: TelephonyCallback
private val maxLogEntries = 1000
private val inMemoryLogs = mutableSetOf()
@@ -369,7 +361,7 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
}
- @SuppressLint("MissingPermission", "UnspecifiedRegisterReceiverFlag")
+ @SuppressLint("MissingPermission", "UnspecifiedRegisterReceiverFlag", "HardwareIds")
override fun onCreate() {
super.onCreate()
Log.i(TAG, "lib exempt worked: ${isBluetoothSocketExempted()}")
@@ -391,7 +383,11 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
localMac = config.selfMacAddress
if (localMac.isEmpty()) {
- if (BuildConfig.FLAVOR == "xposed") {
+ if (checkSelfPermission("android.permission.LOCAL_MAC_ADDRESS") == PackageManager.PERMISSION_GRANTED) {
+ val bluetoothManager = getSystemService(BluetoothManager::class.java)
+ val bluetoothAdapter = bluetoothManager.adapter
+ localMac = bluetoothAdapter.address
+ } else {
localMac = try {
val process = Runtime.getRuntime().exec(
arrayOf("su", "-c", "settings get secure bluetooth_address")
@@ -602,10 +598,8 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
macAddress = sharedPreferences.getString("mac_address", "") ?: ""
telephonyManager = getSystemService(TELEPHONY_SERVICE) as TelephonyManager
- phoneStateListener = object : PhoneStateListener() {
- @Deprecated("Deprecated in Java")
- override fun onCallStateChanged(state: Int, phoneNumber: String?) {
- super.onCallStateChanged(state, phoneNumber)
+ phoneStateListener = object: TelephonyCallback(), TelephonyCallback.CallStateListener {
+ override fun onCallStateChanged(state: Int) {
when (state) {
TelephonyManager.CALL_STATE_RINGING -> {
val leAvailableForAudio =
@@ -615,7 +609,6 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
takeOver("call")
}
if (config.headGestures) {
- callNumber = phoneNumber
handleIncomingCall()
}
}
@@ -634,13 +627,14 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
TelephonyManager.CALL_STATE_IDLE -> {
isInCall = false
- callNumber = null
gestureDetector?.stopDetection()
}
}
}
}
- telephonyManager.listen(phoneStateListener, PhoneStateListener.LISTEN_CALL_STATE)
+ if (checkSelfPermission("android.permission.READ_PHONE_STATE") == PackageManager.PERMISSION_GRANTED) {
+ telephonyManager.registerTelephonyCallback(mainExecutor, phoneStateListener)
+ }
if (config.showPhoneBatteryInWidget) {
widgetMobileBatteryEnabled = true
@@ -850,7 +844,7 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
) || (cameraActive && config.cameraAction == StemPressType.LONG_PRESS)
Log.d(
TAG,
- "Setting up stem actions: " + "Single Press Customized: $singlePressCustomized, " + "Double Press Customized: $doublePressCustomized, " + "Triple Press Customized: $triplePressCustomized, " + "Long Press Customized: $longPressCustomized"
+ "Setting up stem actions: Single Press Customized: $singlePressCustomized, Double Press Customized: $doublePressCustomized, Triple Press Customized: $triplePressCustomized, Long Press Customized: $longPressCustomized"
)
aacpManager.sendStemConfigPacket(
singlePressCustomized,
@@ -1070,6 +1064,7 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
version2 = config.airpodsVersion2,
version3 = config.airpodsVersion3,
)
+ if (device != null) setMetadatas(device!!)
}
sendBroadcast(
Intent(AirPodsNotifications.AIRPODS_INFORMATION_UPDATED).setPackage(
@@ -1722,7 +1717,7 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
val disconnectedNotificationChannel = NotificationChannel(
"background_service_status",
"Background Service Status",
- NotificationManager.IMPORTANCE_LOW
+ NotificationManager.IMPORTANCE_NONE
)
val connectedNotificationChannel = NotificationChannel(
@@ -1813,6 +1808,7 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
}
fun sendBatteryBroadcast() {
+ broadcastBatteryInformation()
sendBroadcast(Intent(AirPodsNotifications.BATTERY_DATA).apply {
putParcelableArrayListExtra("data", ArrayList(batteryNotification.getBattery()))
setPackage(packageName)
@@ -1829,47 +1825,51 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
}
fun setBatteryMetadata() {
- if (BuildConfig.FLAVOR != "xposed") return
- device?.let { it ->
- SystemApisUtils.setMetadata(
- it,
- it.METADATA_UNTETHERED_CASE_BATTERY,
- batteryNotification.getBattery()
- .find { it.component == BatteryComponent.CASE }?.level.toString().toByteArray()
- )
- SystemApisUtils.setMetadata(
- it,
- it.METADATA_UNTETHERED_CASE_CHARGING,
- (if (batteryNotification.getBattery()
- .find { it.component == BatteryComponent.CASE }?.status == BatteryStatus.CHARGING
- ) "1".toByteArray() else "0".toByteArray())
- )
- SystemApisUtils.setMetadata(
- it,
- it.METADATA_UNTETHERED_LEFT_BATTERY,
- batteryNotification.getBattery()
- .find { it.component == BatteryComponent.LEFT }?.level.toString().toByteArray()
- )
- SystemApisUtils.setMetadata(
- it,
- it.METADATA_UNTETHERED_LEFT_CHARGING,
- (if (batteryNotification.getBattery()
- .find { it.component == BatteryComponent.LEFT }?.status == BatteryStatus.CHARGING
- ) "1".toByteArray() else "0".toByteArray())
- )
- SystemApisUtils.setMetadata(
- it,
- it.METADATA_UNTETHERED_RIGHT_BATTERY,
- batteryNotification.getBattery()
- .find { it.component == BatteryComponent.RIGHT }?.level.toString().toByteArray()
- )
- SystemApisUtils.setMetadata(
- it,
- it.METADATA_UNTETHERED_RIGHT_CHARGING,
- (if (batteryNotification.getBattery()
- .find { it.component == BatteryComponent.RIGHT }?.status == BatteryStatus.CHARGING
- ) "1".toByteArray() else "0".toByteArray())
- )
+ if (checkSelfPermission("android.permission.BLUETOOTH_PRIVILEGED") != PackageManager.PERMISSION_GRANTED) {
+ device?.let { it ->
+ SystemApisUtils.setMetadata(
+ it,
+ it.METADATA_UNTETHERED_CASE_BATTERY,
+ batteryNotification.getBattery()
+ .find { it.component == BatteryComponent.CASE }?.level.toString()
+ .toByteArray()
+ )
+ SystemApisUtils.setMetadata(
+ it,
+ it.METADATA_UNTETHERED_CASE_CHARGING,
+ (if (batteryNotification.getBattery()
+ .find { it.component == BatteryComponent.CASE }?.status == BatteryStatus.CHARGING
+ ) "1".toByteArray() else "0".toByteArray())
+ )
+ SystemApisUtils.setMetadata(
+ it,
+ it.METADATA_UNTETHERED_LEFT_BATTERY,
+ batteryNotification.getBattery()
+ .find { it.component == BatteryComponent.LEFT }?.level.toString()
+ .toByteArray()
+ )
+ SystemApisUtils.setMetadata(
+ it,
+ it.METADATA_UNTETHERED_LEFT_CHARGING,
+ (if (batteryNotification.getBattery()
+ .find { it.component == BatteryComponent.LEFT }?.status == BatteryStatus.CHARGING
+ ) "1".toByteArray() else "0".toByteArray())
+ )
+ SystemApisUtils.setMetadata(
+ it,
+ it.METADATA_UNTETHERED_RIGHT_BATTERY,
+ batteryNotification.getBattery()
+ .find { it.component == BatteryComponent.RIGHT }?.level.toString()
+ .toByteArray()
+ )
+ SystemApisUtils.setMetadata(
+ it,
+ it.METADATA_UNTETHERED_RIGHT_CHARGING,
+ (if (batteryNotification.getBattery()
+ .find { it.component == BatteryComponent.RIGHT }?.status == BatteryStatus.CHARGING
+ ) "1".toByteArray() else "0".toByteArray())
+ )
+ }
}
}
@@ -2020,7 +2020,6 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
connected: Boolean, airpodsName: String? = null, batteryList: List? = null
) {
val notificationManager = getSystemService(NotificationManager::class.java)
- var updatedNotification: Notification?
val notificationIntent = Intent(this, MainActivity::class.java)
val pendingIntent = PendingIntent.getActivity(
@@ -2080,13 +2079,6 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
notificationManager.notify(2, updatedNotification)
notificationManager.cancel(1)
} else if (!connected) {
- updatedNotification = NotificationCompat.Builder(this, "background_service_status")
- .setSmallIcon(R.drawable.airpods).setContentTitle("AirPods not connected")
- .setContentText("Tap to open app").setContentIntent(pendingIntent)
- .setCategory(Notification.CATEGORY_SERVICE)
- .setPriority(NotificationCompat.PRIORITY_LOW).setOngoing(true).build()
-
- notificationManager.notify(1, updatedNotification)
notificationManager.cancel(2)
} else if (!config.bleOnlyMode && !socket.isConnected) {
showSocketConnectionFailureNotification("Socket created, but not connected. Check logs")
@@ -2116,7 +2108,7 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
return suspendCancellableCoroutine { continuation ->
gestureDetector?.startDetection(doNotStop = true) { accepted ->
if (continuation.isActive) {
- continuation.resume(accepted) {
+ continuation.resume(accepted) { _, _, _ ->
gestureDetector?.stopDetection()
}
}
@@ -2129,7 +2121,7 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
val telecomManager = getSystemService(TELECOM_SERVICE) as TelecomManager
if (checkSelfPermission(Manifest.permission.ANSWER_PHONE_CALLS) == PackageManager.PERMISSION_GRANTED) {
- telecomManager.acceptRingingCall()
+ telecomManager.acceptRingingCall() // TODO: Switch to InCallService (needs CDM association)
}
} else {
val telephonyService = getSystemService(TELEPHONY_SERVICE) as TelephonyManager
@@ -2156,7 +2148,7 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
val telecomManager = getSystemService(TELECOM_SERVICE) as TelecomManager
if (checkSelfPermission(Manifest.permission.ANSWER_PHONE_CALLS) == PackageManager.PERMISSION_GRANTED) {
- telecomManager.endCall()
+ telecomManager.endCall() // TODO: Switch to InCallService (needs CDM association)
}
} else {
val telephonyService = getSystemService(TELEPHONY_SERVICE) as TelephonyManager
@@ -2229,9 +2221,9 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
@Suppress("PrivatePropertyName")
private val ACTION_ASI_UPDATE_BLUETOOTH_DATA = "batterywidget.impl.action.update_bluetooth_data"
- @Suppress("MissingPermission", "unused")
+ @SuppressLint("MissingPermission")
fun broadcastBatteryInformation() {
- if (device == null) return
+ if (device == null || checkSelfPermission("android.permission.INTERACT_ACROSS_USERS") != PackageManager.PERMISSION_GRANTED) return
val batteryList = batteryNotification.getBattery()
val leftBattery = batteryList.find { it.component == BatteryComponent.LEFT }
@@ -2315,7 +2307,11 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
}
private fun setMetadatas(d: BluetoothDevice) {
- if (BuildConfig.FLAVOR != "xposed") return
+ if (checkSelfPermission("android.permission.BLUETOOTH_PRIVILEGED") != PackageManager.PERMISSION_GRANTED) {
+ Log.d(TAG, "no permission BLUETOOTH_PRIVILEGED, returning")
+ return
+ }
+ Log.d(TAG, "has permission BLUETOOTH_PRIVILEGED, proceeding")
d.let { device ->
val instance = airpodsInstance
if (instance != null) {
@@ -2385,7 +2381,7 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
val context = context?.applicationContext
val name = context?.getSharedPreferences("settings", MODE_PRIVATE)
?.getString("name", bluetoothDevice?.name)
- if (bluetoothDevice != null && action != null && !action.isEmpty()) {
+ if (bluetoothDevice != null && !action.isNullOrEmpty()) {
Log.d(TAG, "Received bluetooth connection broadcast: action=$action")
if (BluetoothDevice.ACTION_ACL_CONNECTED == action) {
val uuid = ParcelUuid.fromString("74ec2172-0bad-4d01-8f77-997b2be0722a")
@@ -2698,6 +2694,7 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
version2 = config.airpodsVersion2,
version3 = config.airpodsVersion3,
)
+ setMetadatas(device)
}
}
@@ -2705,7 +2702,10 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
true, config.deviceName, batteryNotification.getBattery()
)
Log.d(TAG, " Socket connected")
+ sharedPreferences.edit { putBoolean("connection_successful", true) }
+ sendBroadcast(Intent(AirPodsNotifications.AIRPODS_L2CAP_CONNECTED))
} catch (e: Exception) {
+// sharedPreferences.edit { putBoolean("connection_successful", false) }
Log.d(
TAG, " Socket not connected, ${e.message}"
)
@@ -2870,19 +2870,36 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
})
val bluetoothAdapter = getSystemService(BluetoothManager::class.java).adapter
- bluetoothAdapter.getProfileProxy(this, object : BluetoothProfile.ServiceListener {
- override fun onServiceConnected(profile: Int, proxy: BluetoothProfile) {
- if (profile == BluetoothProfile.A2DP) {
- val connectedDevices = proxy.connectedDevices
- if (connectedDevices.isNotEmpty()) {
- MediaController.sendPause()
+ if (checkSelfPermission("android.permission.BLUETOOTH_PRIVILEGED") == PackageManager.PERMISSION_GRANTED){
+ bluetoothAdapter.getProfileProxy(this, object : BluetoothProfile.ServiceListener {
+ override fun onServiceConnected(profile: Int, proxy: BluetoothProfile) {
+ if (profile == BluetoothProfile.A2DP) {
+ val connectedDevices = proxy.connectedDevices
+ if (connectedDevices.isNotEmpty()) {
+ MediaController.sendPause()
+ }
}
+ bluetoothAdapter.closeProfileProxy(profile, proxy)
}
- bluetoothAdapter.closeProfileProxy(profile, proxy)
- }
- override fun onServiceDisconnected(profile: Int) {}
- }, BluetoothProfile.A2DP)
+ override fun onServiceDisconnected(profile: Int) {}
+ }, BluetoothProfile.A2DP)
+ }
+ if (checkSelfPermission("android.permission.MODIFY_PHONE_STATE") == PackageManager.PERMISSION_GRANTED){
+ bluetoothAdapter.getProfileProxy(this, object : BluetoothProfile.ServiceListener {
+ override fun onServiceConnected(profile: Int, proxy: BluetoothProfile) {
+ if (profile == BluetoothProfile.HEADSET) {
+ val connectedDevices = proxy.connectedDevices
+ if (connectedDevices.isNotEmpty()) {
+ MediaController.sendPause()
+ }
+ }
+ bluetoothAdapter.closeProfileProxy(profile, proxy)
+ }
+
+ override fun onServiceDisconnected(profile: Int) {}
+ }, BluetoothProfile.HEADSET)
+ }
Log.d(TAG, "Disconnected AirPods upon user request")
}
@@ -2917,98 +2934,123 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
fun disconnectAudio(context: Context, device: BluetoothDevice?) {
val bluetoothAdapter = context.getSystemService(BluetoothManager::class.java).adapter
- bluetoothAdapter?.getProfileProxy(context, object : BluetoothProfile.ServiceListener {
- override fun onServiceConnected(profile: Int, proxy: BluetoothProfile) {
- if (profile == BluetoothProfile.A2DP) {
- try {
- if (proxy.getConnectionState(device) == BluetoothProfile.STATE_DISCONNECTED) {
- Log.d(TAG, "Already disconnected from A2DP")
- return
+ if (checkSelfPermission("android.permission.BLUETOOTH_PRIVILEGED") == PackageManager.PERMISSION_GRANTED) {
+ bluetoothAdapter?.getProfileProxy(context, object : BluetoothProfile.ServiceListener {
+ override fun onServiceConnected(profile: Int, proxy: BluetoothProfile) {
+ if (profile == BluetoothProfile.A2DP) {
+ try {
+ if (proxy.getConnectionState(device) == BluetoothProfile.STATE_DISCONNECTED) {
+ Log.d(TAG, "Already disconnected from A2DP")
+ return
+ }
+ val method = proxy.javaClass.getMethod(
+ "setConnectionPolicy", BluetoothDevice::class.java, Int::class.java
+ )
+ Log.d(TAG, "calling A2DP.setConnectionPolicy for ${device?.address} to 0")
+ method.invoke(proxy, device, 0)
+ } catch (e: Exception) {
+ e.printStackTrace()
+ } finally {
+ bluetoothAdapter.closeProfileProxy(BluetoothProfile.A2DP, proxy)
}
- val method = proxy.javaClass.getMethod(
- "setConnectionPolicy", BluetoothDevice::class.java, Int::class.java
- )
- method.invoke(proxy, device, 0)
- } catch (e: Exception) {
- Log.w(TAG, "we probably do not have BLUETOOTH_PRIVILEGED")
- } finally {
- bluetoothAdapter.closeProfileProxy(BluetoothProfile.A2DP, proxy)
}
}
- }
- override fun onServiceDisconnected(profile: Int) {}
- }, BluetoothProfile.A2DP)
-// requires protected permission (MODIFY_PHONE_STATE)
-// bluetoothAdapter?.getProfileProxy(context, object : BluetoothProfile.ServiceListener {
-// override fun onServiceConnected(profile: Int, proxy: BluetoothProfile) {
-// if (profile == BluetoothProfile.HEADSET) {
-// try {
-// val method =
-// proxy.javaClass.getMethod("setConnectionPolicy", BluetoothDevice::class.java, Int::class.java)
-// method.invoke(proxy, device, 0)
-// } catch (e: Exception) {
-// e.printStackTrace()
-// } finally {
-// bluetoothAdapter.closeProfileProxy(BluetoothProfile.HEADSET, proxy)
-// }
-// }
-// }
-//
-// override fun onServiceDisconnected(profile: Int) {}
-// }, BluetoothProfile.HEADSET)
+ override fun onServiceDisconnected(profile: Int) {}
+ }, BluetoothProfile.A2DP)
+ } else {
+ Log.d(TAG, "not disconnecting A2DP, no BLUETOOTH_PRIVILEGED permission")
+ }
+ if (checkSelfPermission("android.permission.MODIFY_PHONE_STATE") == PackageManager.PERMISSION_GRANTED) {
+ bluetoothAdapter?.getProfileProxy(context, object : BluetoothProfile.ServiceListener {
+ override fun onServiceConnected(profile: Int, proxy: BluetoothProfile) {
+ if (profile == BluetoothProfile.HEADSET) {
+ try {
+ val method =
+ proxy.javaClass.getMethod(
+ "setConnectionPolicy",
+ BluetoothDevice::class.java,
+ Int::class.java
+ )
+ Log.d(TAG, "calling HEADSET.setConnectionPolicy for ${device?.address} to 0")
+ method.invoke(proxy, device, 0)
+ } catch (e: Exception) {
+ e.printStackTrace()
+ } finally {
+ bluetoothAdapter.closeProfileProxy(BluetoothProfile.HEADSET, proxy)
+ }
+ }
+ }
+ override fun onServiceDisconnected(profile: Int) {}
+ }, BluetoothProfile.HEADSET)
+ } else {
+ Log.d(TAG, "not disconnecting HEADSET, no MODIFIY_PHONE_STATE permission")
+ }
}
fun connectAudio(context: Context, device: BluetoothDevice?) {
val bluetoothAdapter = context.getSystemService(BluetoothManager::class.java).adapter
+ if (context.checkSelfPermission("android.permission.BLUETOOTH_PRIVILEGED") == PackageManager.PERMISSION_GRANTED) {
- bluetoothAdapter?.getProfileProxy(context, object : BluetoothProfile.ServiceListener {
- override fun onServiceConnected(profile: Int, proxy: BluetoothProfile) {
- if (profile == BluetoothProfile.A2DP) {
- try {
- val policyMethod = proxy.javaClass.getMethod(
- "setConnectionPolicy", BluetoothDevice::class.java, Int::class.java
- )
- policyMethod.invoke(proxy, device, 100)
- val connectMethod =
- proxy.javaClass.getMethod("connect", BluetoothDevice::class.java)
- connectMethod.invoke(
- proxy, device
- ) // reduces the slight delay between allowing and actually connecting
- } catch (e: Exception) {
- Log.w(TAG, "we probably do not have BLUETOOTH_PRIVILEGED")
- } finally {
- bluetoothAdapter.closeProfileProxy(BluetoothProfile.A2DP, proxy)
- if (MediaController.pausedWhileTakingOver) {
- MediaController.sendPlay()
+ bluetoothAdapter?.getProfileProxy(context, object : BluetoothProfile.ServiceListener {
+ override fun onServiceConnected(profile: Int, proxy: BluetoothProfile) {
+ if (profile == BluetoothProfile.A2DP) {
+ try {
+ val policyMethod = proxy.javaClass.getMethod(
+ "setConnectionPolicy", BluetoothDevice::class.java, Int::class.java
+ )
+ Log.d(TAG, "calling A2DP.setConnectionPolicy for ${device?.address} to 100")
+ policyMethod.invoke(proxy, device, 100)
+ val connectMethod =
+ proxy.javaClass.getMethod("connect", BluetoothDevice::class.java)
+ connectMethod.invoke(
+ proxy, device
+ )
+ } catch (e: Exception) {
+ e.printStackTrace()
+ } finally {
+ bluetoothAdapter.closeProfileProxy(BluetoothProfile.A2DP, proxy)
+ if (MediaController.pausedWhileTakingOver) {
+ MediaController.sendPlay()
+ }
}
}
}
- }
- override fun onServiceDisconnected(profile: Int) {}
- }, BluetoothProfile.A2DP)
-// requires protected permission (MODIFY_PHONE_STATE)
-// bluetoothAdapter?.getProfileProxy(context, object : BluetoothProfile.ServiceListener {
-// override fun onServiceConnected(profile: Int, proxy: BluetoothProfile) {
-// if (profile == BluetoothProfile.HEADSET) {
-// try {
-// val policyMethod = proxy.javaClass.getMethod("setConnectionPolicy", BluetoothDevice::class.java, Int::class.java)
-// policyMethod.invoke(proxy, device, 100)
-// val connectMethod =
-// proxy.javaClass.getMethod("connect", BluetoothDevice::class.java)
-// connectMethod.invoke(proxy, device)
-// } catch (e: Exception) {
-// e.printStackTrace()
-// } finally {
-// bluetoothAdapter.closeProfileProxy(BluetoothProfile.HEADSET, proxy)
-// }
-// }
-// }
-//
-// override fun onServiceDisconnected(profile: Int) {}
-// }, BluetoothProfile.HEADSET)
+ override fun onServiceDisconnected(profile: Int) {}
+ }, BluetoothProfile.A2DP)
+ } else {
+ Log.d(TAG, "not connecting A2DP, no BLUETOOTH_PRIVILEGED permission")
+ }
+ if (checkSelfPermission("android.permission.MODIFY_PHONE_STATE") == PackageManager.PERMISSION_GRANTED) {
+ bluetoothAdapter?.getProfileProxy(context, object : BluetoothProfile.ServiceListener {
+ override fun onServiceConnected(profile: Int, proxy: BluetoothProfile) {
+ if (profile == BluetoothProfile.HEADSET) {
+ try {
+ val policyMethod = proxy.javaClass.getMethod(
+ "setConnectionPolicy",
+ BluetoothDevice::class.java,
+ Int::class.java
+ )
+ Log.d(TAG, "calling HEADSET.setConnectionPolicy for ${device?.address} to 100")
+ policyMethod.invoke(proxy, device, 100)
+ val connectMethod =
+ proxy.javaClass.getMethod("connect", BluetoothDevice::class.java)
+ connectMethod.invoke(proxy, device)
+ } catch (e: Exception) {
+ e.printStackTrace()
+ } finally {
+ bluetoothAdapter.closeProfileProxy(BluetoothProfile.HEADSET, proxy)
+ }
+ }
+ }
+
+ override fun onServiceDisconnected(profile: Int) {}
+ }, BluetoothProfile.HEADSET)
+ } else {
+ Log.d(TAG, "not connecting HEADSET, no MODIFIY_PHONE_STATE permission")
+ }
}
fun setName(name: String) {
@@ -3016,6 +3058,7 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
if (config.deviceName != name) {
config.deviceName = name
+ device?.alias = name
sharedPreferences.edit { putString("name", name) }
}
@@ -3055,7 +3098,9 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
} catch (e: Exception) {
e.printStackTrace()
}
- telephonyManager.listen(phoneStateListener, PhoneStateListener.LISTEN_NONE)
+ if (checkSelfPermission("android.permission.READ_PHONE_STATE") == PackageManager.PERMISSION_GRANTED) {
+ telephonyManager.unregisterTelephonyCallback(phoneStateListener)
+ }
// isConnectedLocally = false
// CrossDevice.isAvailable = true
super.onDestroy()
diff --git a/android/app/src/main/java/me/kavishdevar/librepods/utils/RootlessSupport.kt b/android/app/src/main/java/me/kavishdevar/librepods/utils/RootlessSupport.kt
index 129f61f..052d976 100644
--- a/android/app/src/main/java/me/kavishdevar/librepods/utils/RootlessSupport.kt
+++ b/android/app/src/main/java/me/kavishdevar/librepods/utils/RootlessSupport.kt
@@ -36,7 +36,7 @@ fun isSupported(sharedPreferences: SharedPreferences): Boolean {
}
}
} else if (isOppoOrOnePlus) {
- return Build.VERSION.SDK_INT == 36
+ return Build.VERSION.SDK_INT >= 36
}
return sharedPreferences.getBoolean("bypass_device_check", false)
}
diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml
index 67c7e04..d3f7f1b 100644
--- a/android/app/src/main/res/values/strings.xml
+++ b/android/app/src/main/res/values/strings.xml
@@ -239,5 +239,28 @@
Bypass compatibility check
Are you sure your device is supported natively/you have Xposed module enabled?
Not supported
- Check the repository for more info.
+
+ Many devices are not supported due to limitations in the Android Bluetooth stack.
+ \nOn these devices, root access with an Xposed framework is required for full functionality.
+ \n\nThis limitation has been addressed in newer Android versions. The following device configurations can run the app natively:
+ \n• Google Pixel® running Android 16 March update and later with the lateset Play system update
+ \n• Google Pixel® running 17 Beta 3 and above
+ \n• OnePlus devices running OxygenOS 16 or later
+ \n• Oppo devices running ColorOS 16 or later
+ \n\nFor details, see the project documentation.
+ (Name your own price)
+
+ This device may not be supported due to platform limitations and requires an Xposed framework. Tick the checkbox below and type OK to continue.
+
+ Type "%s" to continue
+ Proceed
+ I have read compatibility requirements.
+ Device information
+ Build ID
+ Manufacturer
+ Free features
+ Advanced features
+ Digital Assistant on Long Press
+ Invoke Digital Assistant when long pressing the AirPods Pro stem.
+ Customizations unavailable. Connect your AirPods at least once to access.
diff --git a/android/app/src/normal/java/me/kavishdevar/librepods/LibrePodsApplication.kt b/android/app/src/normal/java/me/kavishdevar/librepods/LibrePodsApplication.kt
index 0120900..95774ab 100644
--- a/android/app/src/normal/java/me/kavishdevar/librepods/LibrePodsApplication.kt
+++ b/android/app/src/normal/java/me/kavishdevar/librepods/LibrePodsApplication.kt
@@ -1,5 +1,21 @@
package me.kavishdevar.librepods
import android.app.Application
+import androidx.lifecycle.DefaultLifecycleObserver
+import androidx.lifecycle.LifecycleOwner
+import androidx.lifecycle.ProcessLifecycleOwner
+import me.kavishdevar.librepods.billing.BillingManager
+import me.kavishdevar.librepods.billing.BillingProviderFactory
-class LibrePodsApplication: Application()
+class LibrePodsApplication: Application(), DefaultLifecycleObserver {
+ override fun onCreate() {
+ BillingManager.provider = BillingProviderFactory.create(this)
+ ProcessLifecycleOwner.get().lifecycle.addObserver(this)
+
+ super.onCreate()
+ }
+
+ override fun onResume(owner: LifecycleOwner) {
+ BillingManager.provider.queryPurchases()
+ }
+}
diff --git a/android/app/src/xposed/java/me/kavishdevar/librepods/LibrePodsApplication.kt b/android/app/src/xposed/java/me/kavishdevar/librepods/LibrePodsApplication.kt
index cca90e5..8923d46 100644
--- a/android/app/src/xposed/java/me/kavishdevar/librepods/LibrePodsApplication.kt
+++ b/android/app/src/xposed/java/me/kavishdevar/librepods/LibrePodsApplication.kt
@@ -1,14 +1,27 @@
package me.kavishdevar.librepods
import android.app.Application
+import androidx.lifecycle.DefaultLifecycleObserver
+import androidx.lifecycle.LifecycleOwner
+import androidx.lifecycle.ProcessLifecycleOwner
import io.github.libxposed.service.XposedService
import io.github.libxposed.service.XposedServiceHelper
+import me.kavishdevar.librepods.billing.BillingManager
+import me.kavishdevar.librepods.billing.BillingProviderFactory
import me.kavishdevar.librepods.utils.XposedServiceHolder
-class LibrePodsApplication: Application(), XposedServiceHelper.OnServiceListener {
+class LibrePodsApplication: Application(), XposedServiceHelper.OnServiceListener, DefaultLifecycleObserver {
override fun onCreate() {
- super.onCreate()
XposedServiceHelper.registerListener(this)
+ BillingManager.provider = BillingProviderFactory.create(this)
+ ProcessLifecycleOwner.get().lifecycle.addObserver(this)
+
+ super.onCreate()
+
+ }
+
+ override fun onResume(owner: LifecycleOwner) {
+ BillingManager.provider.queryPurchases()
}
override fun onServiceBind(p0: XposedService) {
diff --git a/android/gradle/libs.versions.toml b/android/gradle/libs.versions.toml
index 964d282..3c8add6 100644
--- a/android/gradle/libs.versions.toml
+++ b/android/gradle/libs.versions.toml
@@ -18,6 +18,7 @@ backdrop = "2.0.0-alpha03"
billing = "8.3.0"
hilt = "2.59.2"
xposed = "101.0.0"
+lifecycleProcess = "2.10.0"
[libraries]
accompanist-permissions = { module = "com.google.accompanist:accompanist-permissions", version.ref = "accompanistPermissions" }
@@ -47,6 +48,7 @@ hilt = { group = "com.google.dagger", name = "hilt-android", version.ref = "hilt
hilt-compiler = { group = "com.google.dagger", name = "hilt-compiler", version.ref = "hilt" }
libxposed-api = { group = "io.github.libxposed", name = "api", version.ref = "xposed" }
libxposed-service = { group = "io.github.libxposed", name = "service", version.ref = "xposed" }
+androidx-lifecycle-process = { group = "androidx.lifecycle", name = "lifecycle-process", version.ref = "lifecycleProcess" }
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
diff --git a/update_nonpatch.json b/update_nonpatch.json
index 3e98d3e..e931cb7 100644
--- a/update_nonpatch.json
+++ b/update_nonpatch.json
@@ -1,6 +1,6 @@
{
- "version": "v0.1.0-rc.4",
- "versionCode": 3,
- "zipUrl": "https://github.com/kavishdevar/librepods/releases/download/v0.1.0-rc.4/LibrePods-v0.1.0-rc.4.zip",
+ "version": "v0.2.3",
+ "versionCode": 36,
+ "zipUrl": "https://github.com/kavishdevar/librepods/releases/download/v0.2.3/LibrePods-v0.2.3-release.zip",
"changelog": "https://raw.githubusercontent.com/kavishdevar/librepods/main/CHANGELOG.md"
}