diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts
index 6d2aa93..e39f632 100644
--- a/android/app/build.gradle.kts
+++ b/android/app/build.gradle.kts
@@ -1,4 +1,3 @@
-import org.jetbrains.kotlin.gradle.dsl.JvmTarget
import java.util.Properties
plugins {
@@ -27,10 +26,10 @@ android {
defaultConfig {
applicationId = "me.kavishdevar.librepods"
- minSdk = 36
+ minSdk = 33
targetSdk = 37
- versionCode = 21
- versionName = "0.2.0-beta.1"
+ versionCode = 27
+ versionName = "0.2.0"
}
buildTypes {
release {
@@ -45,14 +44,19 @@ android {
arguments += "-DCMAKE_BUILD_TYPE=Release"
}
}
+ buildConfigField("Boolean", "PLAY_BUILD", "false")
signingConfig = signingConfigs.getByName("release")
}
debug {
+ buildConfigField("Boolean", "PLAY_BUILD", "false")
signingConfig = signingConfigs.getByName("release")
}
create("playRelease") {
initWith(getByName("release"))
- versionNameSuffix = "-play"
+ buildConfigField("Boolean", "PLAY_BUILD", "true")
+ }
+ create("playDebug") {
+ initWith(getByName("debug"))
buildConfigField("Boolean", "PLAY_BUILD", "true")
}
}
@@ -60,11 +64,6 @@ android {
sourceCompatibility = JavaVersion.VERSION_21
targetCompatibility = JavaVersion.VERSION_21
}
- kotlin {
- compilerOptions {
- jvmTarget.set(JvmTarget.JVM_21)
- }
- }
buildFeatures {
compose = true
viewBinding = true
@@ -105,7 +104,7 @@ android {
arguments += "-DIS_XPOSED=ON"
}
}
- applicationIdSuffix = ".xposed"
+ versionNameSuffix = "-xposed"
}
}
}
@@ -134,9 +133,10 @@ dependencies {
implementation(libs.aboutlibraries)
implementation(libs.aboutlibraries.compose.m3)
implementation(libs.backdrop)
- implementation(libs.hilt)
+// implementation(libs.hilt)
// implementation(libs.hilt.compiler)
- add("xposedCompileOnly", files("libs/libxposed-api-100.aar"))
+ add("xposedCompileOnly", libs.libxposed.api)
+ add("xposedImplementation", libs.libxposed.service)
add("playReleaseImplementation", libs.billing)
}
diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index 61fc138..428ea3e 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -36,6 +36,7 @@
@@ -60,7 +61,7 @@
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 08f06d4..57c86e9 100644
--- a/android/app/src/main/java/me/kavishdevar/librepods/MainActivity.kt
+++ b/android/app/src/main/java/me/kavishdevar/librepods/MainActivity.kt
@@ -22,6 +22,7 @@ package me.kavishdevar.librepods
// import me.kavishdevar.librepods.screens.Onboarding
// import me.kavishdevar.librepods.utils.RadareOffsetFinder
+//import dagger.hilt.android.AndroidEntryPoint
import android.annotation.SuppressLint
import android.content.BroadcastReceiver
import android.content.ComponentName
@@ -51,7 +52,9 @@ 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
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
@@ -61,6 +64,7 @@ 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
@@ -77,6 +81,8 @@ 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
@@ -88,6 +94,8 @@ 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
@@ -111,40 +119,44 @@ import com.google.accompanist.permissions.isGranted
import com.google.accompanist.permissions.rememberMultiplePermissionsState
import com.kyant.backdrop.backdrops.layerBackdrop
import com.kyant.backdrop.backdrops.rememberLayerBackdrop
-import dagger.hilt.android.AndroidEntryPoint
+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.composables.StyledIconButton
-import me.kavishdevar.librepods.constants.AirPodsNotifications
+import me.kavishdevar.librepods.data.AirPodsNotifications
import me.kavishdevar.librepods.data.ControlCommandRepository
-import me.kavishdevar.librepods.screens.AccessibilitySettingsScreen
-import me.kavishdevar.librepods.screens.AdaptiveStrengthScreen
-import me.kavishdevar.librepods.screens.AirPodsSettingsScreen
-import me.kavishdevar.librepods.screens.AppSettingsScreen
-import me.kavishdevar.librepods.screens.CameraControlScreen
-import me.kavishdevar.librepods.screens.DebugScreen
-import me.kavishdevar.librepods.screens.HeadTrackingScreen
-import me.kavishdevar.librepods.screens.HearingAidAdjustmentsScreen
-import me.kavishdevar.librepods.screens.HearingAidScreen
-import me.kavishdevar.librepods.screens.HearingProtectionScreen
-import me.kavishdevar.librepods.screens.LongPress
-import me.kavishdevar.librepods.screens.OpenSourceLicensesScreen
-import me.kavishdevar.librepods.screens.RenameScreen
-import me.kavishdevar.librepods.screens.TransparencySettingsScreen
-import me.kavishdevar.librepods.screens.UpdateHearingTestScreen
-import me.kavishdevar.librepods.screens.VersionScreen
+import me.kavishdevar.librepods.presentation.components.ConfirmationDialog
+import me.kavishdevar.librepods.presentation.components.StyledIconButton
+import me.kavishdevar.librepods.presentation.screens.AccessibilitySettingsScreen
+import me.kavishdevar.librepods.presentation.screens.AdaptiveStrengthScreen
+import me.kavishdevar.librepods.presentation.screens.AirPodsSettingsScreen
+import me.kavishdevar.librepods.presentation.screens.AppSettingsScreen
+import me.kavishdevar.librepods.presentation.screens.CameraControlScreen
+import me.kavishdevar.librepods.presentation.screens.DebugScreen
+import me.kavishdevar.librepods.presentation.screens.HeadTrackingScreen
+import me.kavishdevar.librepods.presentation.screens.HearingAidAdjustmentsScreen
+import me.kavishdevar.librepods.presentation.screens.HearingAidScreen
+import me.kavishdevar.librepods.presentation.screens.HearingProtectionScreen
+import me.kavishdevar.librepods.presentation.screens.LongPress
+import me.kavishdevar.librepods.presentation.screens.OpenSourceLicensesScreen
+import me.kavishdevar.librepods.presentation.screens.PurchaseScreen
+import me.kavishdevar.librepods.presentation.screens.RenameScreen
+import me.kavishdevar.librepods.presentation.screens.TransparencySettingsScreen
+import me.kavishdevar.librepods.presentation.screens.UpdateHearingTestScreen
+import me.kavishdevar.librepods.presentation.screens.VersionScreen
+import me.kavishdevar.librepods.presentation.viewmodel.AirPodsViewModel
+import me.kavishdevar.librepods.presentation.viewmodel.AppSettingsViewModel
+import me.kavishdevar.librepods.presentation.viewmodel.PurchaseViewModel
import me.kavishdevar.librepods.services.AirPodsService
-import me.kavishdevar.librepods.ui.theme.LibrePodsTheme
import me.kavishdevar.librepods.utils.isSupported
-import me.kavishdevar.librepods.viewmodel.AirPodsViewModel
-import me.kavishdevar.librepods.viewmodel.AppSettingsViewModel
import kotlin.io.encoding.ExperimentalEncodingApi
lateinit var serviceConnection: ServiceConnection
lateinit var connectionStatusReceiver: BroadcastReceiver
-@AndroidEntryPoint
+//@AndroidEntryPoint
@ExperimentalMaterial3Api
class MainActivity : ComponentActivity() {
companion object {
@@ -161,7 +173,7 @@ class MainActivity : ComponentActivity() {
enableEdgeToEdge()
setContent {
- LibrePodsTheme {
+ _root_ide_package_.me.kavishdevar.librepods.presentation.theme.LibrePodsTheme {
Main()
}
}
@@ -203,28 +215,153 @@ class MainActivity : ComponentActivity() {
@ExperimentalHazeMaterialsApi
@SuppressLint("MissingPermission", "InlinedApi", "UnspecifiedRegisterReceiverFlag")
-@OptIn(ExperimentalPermissionsApi::class)
+@OptIn(ExperimentalPermissionsApi::class, ExperimentalMaterial3Api::class)
@Composable
fun Main() {
- if (!isSupported()) {
+ val context = LocalContext.current
+ val sharedPreferences = context.getSharedPreferences("settings", MODE_PRIVATE)
+ if (!isSupported(sharedPreferences)) {
+ val showDialog = remember { mutableStateOf(false) }
+ val blockTouches = remember { mutableStateOf(false) }
+ val tapCount = remember { mutableIntStateOf(0) }
+ val lastTapTime = remember { mutableLongStateOf(0L) }
+
+ val hazeState = rememberHazeState()
+
+ LaunchedEffect(blockTouches) {
+ if (blockTouches.value) {
+ delay(500)
+ blockTouches.value = false
+ }
+ }
Box(
modifier = Modifier
.fillMaxSize()
+ .hazeSource(hazeState)
.background(if (isSystemInDarkTheme()) Color.Black else Color(0xFFF2F2F7)),
contentAlignment = Alignment.Center
) {
- Text(
- text = "Not supported. Device Info: BUILD_ID: ${Build.ID} SDK_INT_FULL: ${Build.VERSION.SDK_INT_FULL}, MANUFACTURER: ${Build.MANUFACTURER}.\nCheck out the repository for more info.",
- color = if (isSystemInDarkTheme()) Color.White else Color.Black,
- textAlign = TextAlign.Center,
- modifier = Modifier.padding(16.dp)
+ Box (
+ modifier = Modifier
+ .fillMaxSize()
+ .then(
+ if (blockTouches.value)
+ {
+ Modifier.pointerInput(Unit) {
+ awaitPointerEventScope {
+ while (true) {
+ val event = awaitPointerEvent(PointerEventPass.Initial)
+ event.changes.forEach { it.consume() }
+ }
+ }
+ }
+ }
+ else Modifier
+ )
)
+ Column (
+ verticalArrangement = Arrangement.spacedBy(8.dp)
+ ) {
+ Text(
+ text = "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().pointerInput(Unit) {
+ detectTapGestures(
+ onTap = {
+ val now = System.currentTimeMillis()
+
+ if (now - lastTapTime.longValue > 400) {
+ tapCount.intValue = 0
+ }
+
+ tapCount.intValue++
+ lastTapTime.longValue = now
+
+ if (tapCount.intValue >= 7) {
+ showDialog.value = true
+ blockTouches.value = true
+ }
+ }
+ )
+ },
+ horizontalArrangement = Arrangement.Center
+ ) {
+ Text(
+ text = "Device Info:",
+ style = TextStyle(
+ fontFamily = FontFamily(Font(R.font.sf_pro)),
+ fontWeight = FontWeight.Medium,
+ color = if (isSystemInDarkTheme()) Color.White else Color.Black,
+ fontSize = 16.sp
+ ),
+ 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,
+ )
+ }
+ Text(
+ text = "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()
+ )
+ }
}
+
+ ConfirmationDialog(
+ showDialog = showDialog,
+ title = "Confirm device check bypass?",
+ message = "Are you sure your device is supported with LibrePods?",
+ confirmText = "Yes",
+ dismissText = "No",
+ onConfirm = {
+ showDialog.value = false
+ sharedPreferences.edit {
+ tapCount.intValue = 0
+ 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 = {
+ showDialog.value = false
+ },
+ hazeState = hazeState
+ )
+
return
}
val isConnected = remember { mutableStateOf(false) }
- val context = LocalContext.current
+
var canDrawOverlays by remember { mutableStateOf(Settings.canDrawOverlays(context)) }
val overlaySkipped = remember {
mutableStateOf(
@@ -263,7 +400,7 @@ fun Main() {
val airPodsService = remember { mutableStateOf(null) }
- val viewModel = remember(airPodsService.value) {
+ val airPodsViewModel = remember(airPodsService.value) {
airPodsService.value?.let { service ->
AirPodsViewModel(
service = service,
@@ -317,19 +454,20 @@ fun Main() {
)
}) {
composable("settings") {
- if (viewModel != null) AirPodsSettingsScreen(viewModel, navController)
+ if (airPodsViewModel != null) AirPodsSettingsScreen(airPodsViewModel, navController)
}
composable("debug") {
DebugScreen(navController = navController)
}
composable("long_press/{bud}") { navBackStackEntry ->
- if (viewModel != null) LongPress(
- viewModel = viewModel,
- name = navBackStackEntry.arguments?.getString("bud")!!
+ if (airPodsViewModel != null) LongPress(
+ viewModel = airPodsViewModel,
+ name = navBackStackEntry.arguments?.getString("bud")!!,
+ navController = navController
)
}
composable("rename") {
- if (viewModel != null) RenameScreen(viewModel)
+ if (airPodsViewModel != null) RenameScreen(airPodsViewModel)
}
composable("app_settings") {
val appSettingsViewModel: AppSettingsViewModel = viewModel()
@@ -339,37 +477,41 @@ fun Main() {
// TroubleshootingScreen(navController)
// }
composable("head_tracking") {
- if (viewModel != null) HeadTrackingScreen(viewModel)
+ if (airPodsViewModel != null) HeadTrackingScreen(airPodsViewModel, navController)
}
composable("accessibility") {
- if (viewModel != null) AccessibilitySettingsScreen(viewModel, navController)
+ if (airPodsViewModel != null) AccessibilitySettingsScreen(airPodsViewModel, navController)
}
composable("transparency_customization") {
- if (viewModel != null) TransparencySettingsScreen(viewModel)
+ if (airPodsViewModel != null) TransparencySettingsScreen(airPodsViewModel)
}
composable("hearing_aid") {
- if (viewModel != null) HearingAidScreen(viewModel, navController)
+ if (airPodsViewModel != null) HearingAidScreen(airPodsViewModel, navController)
}
composable("hearing_aid_adjustments") {
- if (viewModel != null) HearingAidAdjustmentsScreen(viewModel)
+ if (airPodsViewModel != null) HearingAidAdjustmentsScreen(airPodsViewModel)
}
composable("adaptive_strength") {
- if (viewModel != null) AdaptiveStrengthScreen(viewModel)
+ if (airPodsViewModel != null) AdaptiveStrengthScreen(airPodsViewModel, navController)
}
composable("camera_control") {
- if (viewModel != null) CameraControlScreen(viewModel)
+ if (airPodsViewModel != null) CameraControlScreen(airPodsViewModel)
}
composable("open_source_licenses") {
OpenSourceLicensesScreen(navController)
}
composable("update_hearing_test") {
- if (viewModel != null) UpdateHearingTestScreen()
+ if (airPodsViewModel != null) UpdateHearingTestScreen()
}
composable("version_info") {
- if (viewModel != null) VersionScreen(viewModel)
+ if (airPodsViewModel != null) VersionScreen(airPodsViewModel)
}
composable("hearing_protection") {
- if (viewModel != null) HearingProtectionScreen(viewModel)
+ if (airPodsViewModel != null) HearingProtectionScreen(airPodsViewModel)
+ }
+ composable("purchase_screen") {
+ val purchaseViewModel: PurchaseViewModel = viewModel()
+ PurchaseScreen(purchaseViewModel, navController)
}
}
}
diff --git a/android/app/src/main/java/me/kavishdevar/librepods/QuickSettingsDialogActivity.kt b/android/app/src/main/java/me/kavishdevar/librepods/QuickSettingsDialogActivity.kt
index 85a9573..2aa9158 100644
--- a/android/app/src/main/java/me/kavishdevar/librepods/QuickSettingsDialogActivity.kt
+++ b/android/app/src/main/java/me/kavishdevar/librepods/QuickSettingsDialogActivity.kt
@@ -85,15 +85,15 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import kotlinx.coroutines.launch
-import me.kavishdevar.librepods.composables.AdaptiveRainbowBrush
-import me.kavishdevar.librepods.composables.ControlCenterNoiseControlSegmentedButton
-import me.kavishdevar.librepods.composables.IconAreaSize
-import me.kavishdevar.librepods.composables.VerticalVolumeSlider
-import me.kavishdevar.librepods.constants.AirPodsNotifications
-import me.kavishdevar.librepods.constants.NoiseControlMode
+import me.kavishdevar.librepods.presentation.components.AdaptiveRainbowBrush
+import me.kavishdevar.librepods.presentation.components.ControlCenterNoiseControlSegmentedButton
+import me.kavishdevar.librepods.presentation.components.IconAreaSize
+import me.kavishdevar.librepods.presentation.components.VerticalVolumeSlider
+import me.kavishdevar.librepods.data.AirPodsNotifications
+import me.kavishdevar.librepods.data.NoiseControlMode
import me.kavishdevar.librepods.services.AirPodsService
-import me.kavishdevar.librepods.ui.theme.LibrePodsTheme
-import me.kavishdevar.librepods.utils.AACPManager
+import me.kavishdevar.librepods.presentation.theme.LibrePodsTheme
+import me.kavishdevar.librepods.bluetooth.AACPManager
import kotlin.io.encoding.ExperimentalEncodingApi
import kotlin.math.abs
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 027ad94..66c97bf 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
@@ -23,6 +23,7 @@ import kotlinx.coroutines.flow.StateFlow
interface BillingProvider {
val isPremium: StateFlow
-
+ val price: StateFlow
fun purchase(activity: Activity)
+ fun queryPurchases()
}
diff --git a/android/app/src/main/java/me/kavishdevar/librepods/billing/BillingProviderFactory.kt b/android/app/src/main/java/me/kavishdevar/librepods/billing/BillingProviderFactory.kt
index 2b83717..33f9b41 100644
--- a/android/app/src/main/java/me/kavishdevar/librepods/billing/BillingProviderFactory.kt
+++ b/android/app/src/main/java/me/kavishdevar/librepods/billing/BillingProviderFactory.kt
@@ -27,7 +27,7 @@ object BillingProviderFactory {
return if (BuildConfig.PLAY_BUILD) {
PlayBillingProvider(context)
} else {
- FOSSBillingProvider()
+ FOSSBillingProvider(context)
}
}
}
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 9b06964..4aef636 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
@@ -19,12 +19,56 @@
package me.kavishdevar.librepods.billing
import android.app.Activity
+import android.content.Context
+import android.content.Intent
+import androidx.core.content.edit
+import androidx.core.net.toUri
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.SupervisorJob
+import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
-class FOSSBillingProvider : BillingProvider {
- private val _isPremium = MutableStateFlow(true)
+class FOSSBillingProvider(context: Context): BillingProvider {
+ private val _isPremium = MutableStateFlow(false)
override val isPremium: StateFlow = _isPremium
- override fun purchase(activity: Activity) { }
+ private val _price = MutableStateFlow("Any")
+ override val price: StateFlow = _price
+
+ private val sharedPreferences = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
+
+ private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
+ private var purchaseJob: Job? = null
+
+ init {
+ queryPurchases()
+ }
+
+ override fun purchase(activity: Activity) {
+ activity.startActivity(
+ Intent(Intent.ACTION_VIEW, "https://github.com/sponsors/kavishdevar".toUri())
+ )
+
+ purchaseJob?.cancel()
+
+ purchaseJob = scope.launch {
+ delay(2_000)
+ withContext(Dispatchers.Main) {
+ _isPremium.value = true
+ sharedPreferences.edit { putBoolean("foss_upgraded", true) }
+ }
+ }
+ }
+
+ override fun queryPurchases() {
+ val stored = sharedPreferences.getBoolean("foss_upgraded", false)
+ if (stored != _isPremium.value) {
+ _isPremium.value = stored
+ }
+ }
}
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 790be60..bc6d35d 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
@@ -55,6 +55,10 @@ class PlayBillingProvider(
private val _isPremium = MutableStateFlow(false)
override val isPremium: StateFlow = _isPremium
+ private val _price = MutableStateFlow("unknown")
+ override val price: StateFlow = _price
+
+
private var productDetails: ProductDetails? = null
private val billingClient = BillingClient.newBuilder(context)
@@ -102,6 +106,13 @@ class PlayBillingProvider(
if (result.billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
productDetails = result.productDetailsList?.firstOrNull()
Log.d(TAG, "Product loaded: ${productDetails?.name}")
+ val priceString = productDetails
+ ?.oneTimePurchaseOfferDetails
+ ?.formattedPrice
+
+ if (priceString != null) {
+ _price.value = priceString
+ }
} else {
Log.w(TAG, "queryProductDetails failed: ${result.billingResult.debugMessage}")
}
@@ -152,13 +163,13 @@ class PlayBillingProvider(
}
-// val purchase = purchases.find {
+// val navigateToPurchase = purchases.find {
// it.products.contains(PREMIUM_PRODUCT_ID) && it.purchaseState == Purchase.PurchaseState.PURCHASED
// }
//
-// if (purchase != null) {
+// if (navigateToPurchase != null) {
// val consumeParams = ConsumeParams.newBuilder()
-// .setPurchaseToken(purchase.purchaseToken)
+// .setPurchaseToken(navigateToPurchase.purchaseToken)
// .build()
// scope.launch {
// billingClient.consumeAsync(consumeParams) { _, _ ->}
@@ -184,4 +195,10 @@ class PlayBillingProvider(
Log.e(TAG, "Acknowledgement failed: ${result.debugMessage}")
}
}
+
+ override fun queryPurchases() {
+ scope.launch {
+ queryExistingPurchases()
+ }
+ }
}
diff --git a/android/app/src/main/java/me/kavishdevar/librepods/utils/AACPManager.kt b/android/app/src/main/java/me/kavishdevar/librepods/bluetooth/AACPManager.kt
similarity index 85%
rename from android/app/src/main/java/me/kavishdevar/librepods/utils/AACPManager.kt
rename to android/app/src/main/java/me/kavishdevar/librepods/bluetooth/AACPManager.kt
index 6149666..5edee79 100644
--- a/android/app/src/main/java/me/kavishdevar/librepods/utils/AACPManager.kt
+++ b/android/app/src/main/java/me/kavishdevar/librepods/bluetooth/AACPManager.kt
@@ -18,7 +18,7 @@
@file:OptIn(ExperimentalEncodingApi::class)
-package me.kavishdevar.librepods.utils
+package me.kavishdevar.librepods.bluetooth
import android.util.Log
import java.nio.ByteBuffer
@@ -61,8 +61,7 @@ class AACPManager {
private val HEADER_BYTES = byteArrayOf(0x04, 0x00, 0x04, 0x00)
data class ControlCommandStatus(
- val identifier: ControlCommandIdentifiers,
- val value: ByteArray
+ val identifier: ControlCommandIdentifiers, val value: ByteArray
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
@@ -85,42 +84,31 @@ class AACPManager {
// @Suppress("unused")
enum class ControlCommandIdentifiers(val value: Byte) {
- MIC_MODE(0x01),
- BUTTON_SEND_MODE(0x05),
- VOICE_TRIGGER(0x12),
- SINGLE_CLICK_MODE(0x14),
- DOUBLE_CLICK_MODE(0x15),
- CLICK_HOLD_MODE(0x16),
- DOUBLE_CLICK_INTERVAL(0x17),
- CLICK_HOLD_INTERVAL(0x18),
- LISTENING_MODE_CONFIGS(0x1A),
- ONE_BUD_ANC_MODE(0x1B),
- CROWN_ROTATION_DIRECTION(0x1C),
- LISTENING_MODE(0x0D),
- AUTO_ANSWER_MODE(0x1E),
- CHIME_VOLUME(0x1F),
- VOLUME_SWIPE_INTERVAL(0x23),
- CALL_MANAGEMENT_CONFIG(0x24),
- VOLUME_SWIPE_MODE(0x25),
- ADAPTIVE_VOLUME_CONFIG(0x26),
- SOFTWARE_MUTE_CONFIG(0x27),
- CONVERSATION_DETECT_CONFIG(0x28),
- SSL(0x29),
- HEARING_AID(0x2C),
- AUTO_ANC_STRENGTH(0x2E),
- HPS_GAIN_SWIPE(0x2F),
- HRM_STATE(0x30),
- IN_CASE_TONE_CONFIG(0x31),
- SIRI_MULTITONE_CONFIG(0x32),
- HEARING_ASSIST_CONFIG(0x33),
- ALLOW_OFF_OPTION(0x34),
- STEM_CONFIG(0x39),
- SLEEP_DETECTION_CONFIG(0x35),
- ALLOW_AUTO_CONNECT(0x36), // not sure what this does, AUTOMATIC_CONNECTION is the only one used, but this is newer... so ¯\_(ツ)_/¯
- EAR_DETECTION_CONFIG(0x0A),
- AUTOMATIC_CONNECTION_CONFIG(0x20),
- OWNS_CONNECTION(0x06),
- PPE_TOGGLE_CONFIG(0x37),
+ MIC_MODE(0x01), BUTTON_SEND_MODE(0x05), VOICE_TRIGGER(0x12), SINGLE_CLICK_MODE(0x14), DOUBLE_CLICK_MODE(
+ 0x15
+ ),
+ CLICK_HOLD_MODE(0x16), DOUBLE_CLICK_INTERVAL(0x17), CLICK_HOLD_INTERVAL(0x18), LISTENING_MODE_CONFIGS(
+ 0x1A
+ ),
+ ONE_BUD_ANC_MODE(0x1B), CROWN_ROTATION_DIRECTION(0x1C), LISTENING_MODE(0x0D), AUTO_ANSWER_MODE(
+ 0x1E
+ ),
+ CHIME_VOLUME(0x1F), VOLUME_SWIPE_INTERVAL(0x23), CALL_MANAGEMENT_CONFIG(0x24), VOLUME_SWIPE_MODE(
+ 0x25
+ ),
+ ADAPTIVE_VOLUME_CONFIG(0x26), SOFTWARE_MUTE_CONFIG(0x27), CONVERSATION_DETECT_CONFIG(
+ 0x28
+ ),
+ SSL(0x29), HEARING_AID(0x2C), AUTO_ANC_STRENGTH(0x2E), HPS_GAIN_SWIPE(0x2F), HRM_STATE(
+ 0x30
+ ),
+ IN_CASE_TONE_CONFIG(0x31), SIRI_MULTITONE_CONFIG(0x32), HEARING_ASSIST_CONFIG(0x33), ALLOW_OFF_OPTION(
+ 0x34
+ ),
+ STEM_CONFIG(0x39), SLEEP_DETECTION_CONFIG(0x35), ALLOW_AUTO_CONNECT(0x36), // not sure what this does, AUTOMATIC_CONNECTION is the only one used, but this is newer... so ¯\_(ツ)_/¯
+ EAR_DETECTION_CONFIG(0x0A), AUTOMATIC_CONNECTION_CONFIG(0x20), OWNS_CONNECTION(0x06), PPE_TOGGLE_CONFIG(
+ 0x37
+ ),
PPE_CAP_LEVEL_CONFIG(0x38);
companion object {
@@ -130,59 +118,44 @@ class AACPManager {
}
enum class ProximityKeyType(val value: Byte) {
- IRK(0x01),
- ENC_KEY(0x04);
+ IRK(0x01), ENC_KEY(0x04);
companion object {
- fun fromByte(byte: Byte): ProximityKeyType =
- ProximityKeyType.entries.find { it.value == byte }
- ?: throw IllegalArgumentException("Unknown ProximityKeyType: $byte")
+ fun fromByte(byte: Byte): ProximityKeyType = entries.find { it.value == byte }
+ ?: throw IllegalArgumentException("Unknown ProximityKeyType: $byte")
}
}
enum class StemPressType(val value: Byte) {
- SINGLE_PRESS(0x05),
- DOUBLE_PRESS(0x06),
- TRIPLE_PRESS(0x07),
- LONG_PRESS(0x08);
+ SINGLE_PRESS(0x05), DOUBLE_PRESS(0x06), TRIPLE_PRESS(0x07), LONG_PRESS(0x08);
companion object {
- fun fromByte(byte: Byte): StemPressType? =
- entries.find { it.value == byte }
+ fun fromByte(byte: Byte): StemPressType? = entries.find { it.value == byte }
}
}
enum class StemPressBudType(val value: Byte) {
- LEFT(0x01),
- RIGHT(0x02);
+ LEFT(0x01), RIGHT(0x02);
companion object {
- fun fromByte(byte: Byte): StemPressBudType? =
- entries.find { it.value == byte }
+ fun fromByte(byte: Byte): StemPressBudType? = entries.find { it.value == byte }
}
}
enum class AudioSourceType(val value: Byte) {
- NONE(0x00),
- CALL(0x01),
- MEDIA(0x02);
+ NONE(0x00), CALL(0x01), MEDIA(0x02);
companion object {
- fun fromByte(byte: Byte): AudioSourceType? =
- entries.find { it.value == byte }
+ fun fromByte(byte: Byte): AudioSourceType? = entries.find { it.value == byte }
}
}
data class AudioSource(
- val mac: String,
- val type: AudioSourceType
+ val mac: String, val type: AudioSourceType
)
data class ConnectedDevice(
- val mac: String,
- val info1: Byte,
- val info2: Byte,
- var type: String?
+ val mac: String, val info1: Byte, val info2: Byte, var type: String?
)
data class AirPodsInformation(
@@ -231,8 +204,7 @@ class AACPManager {
}
private fun setControlCommandStatusValue(
- identifier: ControlCommandIdentifiers,
- value: ByteArray
+ identifier: ControlCommandIdentifiers, value: ByteArray
) {
val existingStatus = getControlCommandStatus(identifier)
if (existingStatus == value) {
@@ -289,15 +261,13 @@ class AACPManager {
}
fun registerControlCommandListener(
- identifier: ControlCommandIdentifiers,
- callback: ControlCommandListener
+ identifier: ControlCommandIdentifiers, callback: ControlCommandListener
) {
controlCommandListeners.getOrPut(identifier) { mutableListOf() }.add(callback)
}
fun unregisterControlCommandListener(
- identifier: ControlCommandIdentifiers,
- callback: ControlCommandListener
+ identifier: ControlCommandIdentifiers, callback: ControlCommandListener
) {
controlCommandListeners[identifier]?.remove(callback)
}
@@ -332,8 +302,7 @@ class AACPManager {
fun sendControlCommand(identifier: Byte, value: ByteArray): Boolean {
val controlPacket = createControlCommandPacket(identifier, value)
setControlCommandStatusValue(
- ControlCommandIdentifiers.fromByte(identifier) ?: return false,
- value
+ ControlCommandIdentifiers.fromByte(identifier) ?: return false, value
)
return sendDataPacket(controlPacket)
}
@@ -342,16 +311,14 @@ class AACPManager {
fun sendControlCommand(identifier: Byte, value: Byte): Boolean {
val controlPacket = createControlCommandPacket(identifier, byteArrayOf(value))
setControlCommandStatusValue(
- ControlCommandIdentifiers.fromByte(identifier) ?: return false,
- byteArrayOf(value)
+ ControlCommandIdentifiers.fromByte(identifier) ?: return false, byteArrayOf(value)
)
return sendDataPacket(controlPacket)
}
fun sendControlCommand(identifier: Byte, value: Boolean): Boolean {
val controlPacket = createControlCommandPacket(
- identifier,
- if (value) byteArrayOf(0x01) else byteArrayOf(0x02)
+ identifier, if (value) byteArrayOf(0x01) else byteArrayOf(0x02)
)
setControlCommandStatusValue(
ControlCommandIdentifiers.fromByte(identifier) ?: return false,
@@ -371,8 +338,7 @@ class AACPManager {
fun parseProximityKeysResponse(data: ByteArray): Map {
Log.d(
- TAG,
- "Parsing Proximity Keys Response: ${data.joinToString(" ") { "%02X".format(it) }}"
+ TAG, "Parsing Proximity Keys Response: ${data.joinToString(" ") { "%02X".format(it) }}"
)
if (data.size < 4) {
throw IllegalArgumentException("Data array too short to parse Proximity Keys Response")
@@ -400,11 +366,9 @@ class AACPManager {
keys[ProximityKeyType.fromByte(keyType)] = key
offset += keyLength
Log.d(
- TAG,
- "Parsed Proximity Key: Type: ${keyType}, Length: $keyLength, Key: ${
- key.joinToString(" ") { "%02X".format(it) }
- }"
- )
+ TAG, "Parsed Proximity Key: Type: ${keyType}, Length: $keyLength, Key: ${
+ key.joinToString(" ") { "%02X".format(it) }
+ }")
}
return keys
}
@@ -424,26 +388,21 @@ class AACPManager {
fun receivePacket(packet: ByteArray) {
if (!packet.toHexString().startsWith("04000400")) {
Log.w(
- TAG,
- "Received packet does not start with expected header: ${
- packet.joinToString(" ") {
- "%02X".format(it)
- }
- }"
- )
+ TAG, "Received packet does not start with expected header: ${
+ packet.joinToString(" ") {
+ "%02X".format(it)
+ }
+ }")
return
}
if (packet.size < 6) {
Log.w(
- TAG,
- "Received packet too short: ${packet.joinToString(" ") { "%02X".format(it) }}"
+ TAG, "Received packet too short: ${packet.joinToString(" ") { "%02X".format(it) }}"
)
return
}
- val opcode = packet[4]
-
- when (opcode) {
+ when (val opcode = packet[4]) {
Opcodes.BATTERY_INFO -> {
callback?.onBatteryInfoReceived(packet)
}
@@ -458,23 +417,23 @@ class AACPManager {
TAG,
"Control command received: ${controlCommand.identifier.toHexString()} - ${
controlCommand.value.joinToString(" ") { "%02X".format(it) }
- }"
- )
+ }")
val controlCommandListText = try {
- controlCommandStatusList.joinToString(", ") { it ->
- "${it.identifier.name} (${it.identifier.value.toHexString()}) - ${
- it.value.joinToString(
- " "
- ) { "%02X".format(it) }
- }"
- }
- } catch (e: Exception) {
- e.message
+ controlCommandStatusList.joinToString(", ") { it ->
+ "${it.identifier.name} (${it.identifier.value.toHexString()}) - ${
+ it.value.joinToString(
+ " "
+ ) { "%02X".format(it) }
+ }"
}
+ } catch (e: Exception) {
+ e.message
+ }
Log.d(
- TAG, "Control command list is now: $controlCommandListText")
+ TAG, "Control command list is now: $controlCommandListText"
+ )
val controlCommandIdentifier =
ControlCommandIdentifiers.fromByte(controlCommand.identifier)
@@ -508,13 +467,11 @@ class AACPManager {
Opcodes.HEADTRACKING -> {
if (packet.size < 70) {
Log.w(
- TAG,
- "Received HEADTRACKING packet too short: ${
- packet.joinToString(" ") {
- "%02X".format(it)
- }
- }"
- )
+ TAG, "Received HEADTRACKING packet too short: ${
+ packet.joinToString(" ") {
+ "%02X".format(it)
+ }
+ }")
return
}
callback?.onHeadTrackingReceived(packet)
@@ -546,7 +503,8 @@ class AACPManager {
Opcodes.SMART_ROUTING_RESP -> {
val packetString = packet.decodeToString()
- val sender = packet.sliceArray(6..11).reversedArray().joinToString(":") { "%02X".format(it) }
+ val sender =
+ packet.sliceArray(6..11).reversedArray().joinToString(":") { "%02X".format(it) }
// if (connectedDevices.find { it.mac == sender }?.type == null && packetString.contains("btName")) {
// val nameStartIndex = packetString.indexOf("btName") + 8
@@ -566,9 +524,15 @@ class AACPManager {
} else if ("Android" in packetString) {
connectedDevices.find { it.mac == sender }?.type = "Android"
}
- Log.d(TAG, "Smart Routing Response from $sender: $packetString, type: ${connectedDevices.find { it.mac == sender }?.type}")
+ Log.d(
+ TAG,
+ "Smart Routing Response from $sender: $packetString, type: ${connectedDevices.find { it.mac == sender }?.type}"
+ )
if (packetString.contains("SetOwnershipToFalse")) {
- callback?.onOwnershipToFalseRequest(sender, packetString.contains("ReverseBannerTapped"))
+ callback?.onOwnershipToFalseRequest(
+ sender,
+ packetString.contains("ReverseBannerTapped")
+ )
}
if (packetString.contains("ShowNearbyUI")) {
callback?.onShowNearbyUI(sender)
@@ -595,15 +559,19 @@ class AACPManager {
eqOnPhone = (packet[11] == 0x01.toByte())
// there are 4 eqs. i am not sure what those are for, maybe all 4 listening modes, or maybe phone+media left+right, but then there shouldn't be another flag for phone/media visible. just directly the EQ... weird.
// the EQs are little endian floats
- val eq1 = ByteBuffer.wrap(packet, 12, 32).order(ByteOrder.LITTLE_ENDIAN).asFloatBuffer()
- val eq2 = ByteBuffer.wrap(packet, 44, 32).order(ByteOrder.LITTLE_ENDIAN).asFloatBuffer()
- val eq3 = ByteBuffer.wrap(packet, 76, 32).order(ByteOrder.LITTLE_ENDIAN).asFloatBuffer()
- val eq4 = ByteBuffer.wrap(packet, 108, 32).order(ByteOrder.LITTLE_ENDIAN).asFloatBuffer()
+ val eq1 =
+ ByteBuffer.wrap(packet, 12, 32).order(ByteOrder.LITTLE_ENDIAN).asFloatBuffer()
+ ByteBuffer.wrap(packet, 44, 32).order(ByteOrder.LITTLE_ENDIAN).asFloatBuffer()
+ ByteBuffer.wrap(packet, 76, 32).order(ByteOrder.LITTLE_ENDIAN).asFloatBuffer()
+ ByteBuffer.wrap(packet, 108, 32).order(ByteOrder.LITTLE_ENDIAN).asFloatBuffer()
// for now, taking just the first EQ
eqData = FloatArray(8) { i -> eq1.get(i) }
- Log.d(TAG, "EQ Data set to: ${eqData.toList()}, eqOnPhone: $eqOnPhone, eqOnMedia: $eqOnMedia")
+ Log.d(
+ TAG,
+ "EQ Data set to: ${eqData.toList()}, eqOnPhone: $eqOnPhone, eqOnMedia: $eqOnMedia"
+ )
callback?.onEQPacketReceived(eqData)
}
@@ -613,8 +581,9 @@ class AACPManager {
val information = parseInformationPacket(packet)
callback?.onDeviceInformationReceived(information)
}
+
else -> {
- Log.d(TAG, "Unknown opcode received: ${opcode.toHexString()}")
+ Log.d(TAG, "Unhandled opcode received: ${opcode.toHexString()}")
callback?.onUnknownPacketReceived(packet)
}
}
@@ -644,10 +613,22 @@ class AACPManager {
fun createHandshakePacket(): ByteArray {
return byteArrayOf(
- 0x00, 0x00, 0x04, 0x00,
- 0x01, 0x00, 0x02, 0x00,
- 0x00, 0x00, 0x00, 0x00,
- 0x00, 0x00, 0x00, 0x00
+ 0x00,
+ 0x00,
+ 0x04,
+ 0x00,
+ 0x01,
+ 0x00,
+ 0x02,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x00
)
}
@@ -793,17 +774,31 @@ class AACPManager {
}
fun sendMediaInformationNewDevice(selfMacAddress: String, targetMacAddress: String): Boolean {
- if (selfMacAddress.length != 17 || !selfMacAddress.matches(Regex("([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}")) || targetMacAddress.length != 17 || !targetMacAddress.matches(Regex("([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}"))) {
+ if (selfMacAddress.length != 17 || !selfMacAddress.matches(Regex("([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}")) || targetMacAddress.length != 17 || !targetMacAddress.matches(
+ Regex("([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}")
+ )
+ ) {
// throw IllegalArgumentException("MAC address must be 6 bytes")
- Log.w(TAG, "Invalid MAC address format, got: selfMacAddress=$selfMacAddress, targetMacAddress=$targetMacAddress")
+ Log.w(
+ TAG,
+ "Invalid MAC address format, got: selfMacAddress=$selfMacAddress, targetMacAddress=$targetMacAddress"
+ )
return false
}
Log.d(TAG, "SELFMAC: ${selfMacAddress}, TARGETMAC: $targetMacAddress")
Log.d(TAG, "Sending Media Information packet to $targetMacAddress")
- return sendDataPacket(createMediaInformationNewDevicePacket(selfMacAddress, targetMacAddress))
+ return sendDataPacket(
+ createMediaInformationNewDevicePacket(
+ selfMacAddress,
+ targetMacAddress
+ )
+ )
}
- fun createMediaInformationNewDevicePacket(selfMacAddress: String, targetMacAddress: String): ByteArray {
+ fun createMediaInformationNewDevicePacket(
+ selfMacAddress: String,
+ targetMacAddress: String
+ ): ByteArray {
val opcode = byteArrayOf(Opcodes.SMART_ROUTING, 0x00)
val buffer = ByteBuffer.allocate(116)
buffer.put(
@@ -892,17 +887,13 @@ class AACPManager {
Log.d(TAG, "Sending Media Information packet to $targetMac")
return sendDataPacket(
createMediaInformationPacket(
- selfMacAddress,
- targetMac,
- streamingState
+ selfMacAddress, targetMac, streamingState
)
)
}
fun createMediaInformationPacket(
- selfMacAddress: String,
- targetMacAddress: String,
- streamingState: Boolean = true
+ selfMacAddress: String, targetMacAddress: String, streamingState: Boolean = true
): ByteArray {
val opcode = byteArrayOf(Opcodes.SMART_ROUTING, 0x00)
val buffer = ByteBuffer.allocate(138)
@@ -935,7 +926,7 @@ class AACPManager {
buffer.put("AudioCategory".toByteArray())
buffer.put(byteArrayOf(0x31, 0x2D, 0x01))
- return opcode+buffer.array()
+ return opcode + buffer.array()
}
fun sendSmartRoutingShowUI(selfMacAddress: String): Boolean {
@@ -1017,9 +1008,15 @@ class AACPManager {
fun sendAddTiPiDevice(selfMacAddress: String, targetMacAddress: String): Boolean {
- if (selfMacAddress.length != 17 || !selfMacAddress.matches(Regex("([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}")) || targetMacAddress.length != 17 || !targetMacAddress.matches(Regex("([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}"))) {
+ if (selfMacAddress.length != 17 || !selfMacAddress.matches(Regex("([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}")) || targetMacAddress.length != 17 || !targetMacAddress.matches(
+ Regex("([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}")
+ )
+ ) {
// throw IllegalArgumentException("MAC address must be 6 bytes")
- Log.w(TAG, "Invalid MAC address format, got: selfMacAddress=$selfMacAddress, targetMacAddress=$targetMacAddress")
+ Log.w(
+ TAG,
+ "Invalid MAC address format, got: selfMacAddress=$selfMacAddress, targetMacAddress=$targetMacAddress"
+ )
return false
}
Log.d(TAG, "Sending Add TiPi Device packet to $targetMacAddress")
@@ -1053,8 +1050,7 @@ class AACPManager {
}
data class ControlCommand(
- val identifier: Byte,
- val value: ByteArray
+ val identifier: Byte, val value: ByteArray
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
@@ -1106,10 +1102,8 @@ class AACPManager {
triplePressCustomized: Boolean = false,
longPressCustomized: Boolean = false
): Boolean {
- val value = ((if (singlePressCustomized) 0x01 else 0) or
- (if (doublePressCustomized) 0x02 else 0) or
- (if (triplePressCustomized) 0x04 else 0) or
- (if (longPressCustomized) 0x08 else 0)).toByte()
+ val value =
+ ((if (singlePressCustomized) 0x01 else 0) or (if (doublePressCustomized) 0x02 else 0) or (if (triplePressCustomized) 0x04 else 0) or (if (longPressCustomized) 0x08 else 0)).toByte()
Log.d(TAG, "Sending Stem Config Packet with value: ${value.toHexString()}")
return sendControlCommand(
ControlCommandIdentifiers.STEM_CONFIG.value, value
@@ -1124,19 +1118,18 @@ class AACPManager {
if (packet[4] == Opcodes.CONTROL_COMMAND) {
val controlCommand = ControlCommand.fromByteArray(packet)
Log.d(
- TAG,
- "Control command: ${controlCommand.identifier.toHexString()} - ${
- controlCommand.value.joinToString(" ") { "%02X".format(it) }
- }"
- )
+ TAG, "Control command: ${controlCommand.identifier.toHexString()} - ${
+ controlCommand.value.joinToString(" ") { "%02X".format(it) }
+ }")
setControlCommandStatusValue(
ControlCommandIdentifiers.fromByte(controlCommand.identifier) ?: return false,
controlCommand.value
)
}
- val socket = BluetoothConnectionManager.getCurrentSocket()
- if (socket?.isConnected == true) {
+ val socket = BluetoothConnectionManager.getCurrentSocket() ?: return false
+
+ if (socket.isConnected) {
socket.outputStream?.write(packet)
socket.outputStream?.flush()
return true
@@ -1213,7 +1206,10 @@ class AACPManager {
var offset = 9
for (i in 0 until deviceCount) {
if (offset + 8 > data.size) {
- Log.w(TAG, "Data array too short to parse all connected devices, returning what we have")
+ Log.w(
+ TAG,
+ "Data array too short to parse all connected devices, returning what we have"
+ )
break
}
val macBytes = data.sliceArray(offset until offset + 6)
@@ -1227,6 +1223,7 @@ class AACPManager {
return devices
}
+
fun sendSomePacketIDontKnowWhatItIs() {
// 2900 00ff ffff ffff ffff -- enables setting EQ
sendDataPacket(
@@ -1264,7 +1261,7 @@ class AACPManager {
val start = index
// find next 0x00 byte
while (index < data.size && data[index] != 0x00.toByte()) index++
- val str = data.sliceArray(start until index).decodeToString()
+ val str = data.sliceArray(start..index).decodeToString()
strings.add(str)
}
diff --git a/android/app/src/main/java/me/kavishdevar/librepods/utils/ATTManager.kt b/android/app/src/main/java/me/kavishdevar/librepods/bluetooth/ATTManager.kt
similarity index 97%
rename from android/app/src/main/java/me/kavishdevar/librepods/utils/ATTManager.kt
rename to android/app/src/main/java/me/kavishdevar/librepods/bluetooth/ATTManager.kt
index c39a113..41c1483 100644
--- a/android/app/src/main/java/me/kavishdevar/librepods/utils/ATTManager.kt
+++ b/android/app/src/main/java/me/kavishdevar/librepods/bluetooth/ATTManager.kt
@@ -21,7 +21,7 @@
* and receiving notifications. It is not a complete implementation of the ATT protocol.
*/
-package me.kavishdevar.librepods.utils
+package me.kavishdevar.librepods.bluetooth
import android.annotation.SuppressLint
import android.bluetooth.BluetoothAdapter
@@ -31,6 +31,7 @@ import android.os.ParcelUuid
import android.util.Log
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import java.io.InputStream
import java.io.OutputStream
@@ -62,7 +63,7 @@ class ATTManager(private val adapter: BluetoothAdapter, private val device: Blue
private var input: InputStream? = null
private var output: OutputStream? = null
private val listeners = mutableMapOf Unit>>()
- private var notificationJob: kotlinx.coroutines.Job? = null
+ private var notificationJob: Job? = null
// queue for non-notification PDUs (responses to requests)
private val responses = LinkedBlockingQueue()
@@ -72,7 +73,12 @@ class ATTManager(private val adapter: BluetoothAdapter, private val device: Blue
val uuid = ParcelUuid.fromString("00000000-0000-0000-0000-000000000000")
socket = createBluetoothSocket(adapter, device, uuid)
- socket!!.connect()
+ try {
+ socket!!.connect()
+ } catch (e: Exception) {
+ Log.w(TAG, "ATT socket failed to connect")
+ return
+ }
input = socket!!.inputStream
output = socket!!.outputStream
Log.d(TAG, "Connected to ATT")
diff --git a/android/app/src/main/java/me/kavishdevar/librepods/utils/BLEManager.kt b/android/app/src/main/java/me/kavishdevar/librepods/bluetooth/BLEManager.kt
similarity index 99%
rename from android/app/src/main/java/me/kavishdevar/librepods/utils/BLEManager.kt
rename to android/app/src/main/java/me/kavishdevar/librepods/bluetooth/BLEManager.kt
index 73600ee..52fa055 100644
--- a/android/app/src/main/java/me/kavishdevar/librepods/utils/BLEManager.kt
+++ b/android/app/src/main/java/me/kavishdevar/librepods/bluetooth/BLEManager.kt
@@ -16,7 +16,7 @@
along with this program. If not, see .
*/
-package me.kavishdevar.librepods.utils
+package me.kavishdevar.librepods.bluetooth
import android.annotation.SuppressLint
import android.bluetooth.BluetoothManager
@@ -30,8 +30,10 @@ import android.content.SharedPreferences
import android.os.Handler
import android.os.Looper
import android.util.Log
+import me.kavishdevar.librepods.utils.BluetoothCryptography
import javax.crypto.Cipher
import javax.crypto.spec.SecretKeySpec
+import kotlin.collections.iterator
import kotlin.io.encoding.Base64
import kotlin.io.encoding.ExperimentalEncodingApi
diff --git a/android/app/src/main/java/me/kavishdevar/librepods/utils/BluetoothConnectionManager.kt b/android/app/src/main/java/me/kavishdevar/librepods/bluetooth/BluetoothConnectionManager.kt
similarity index 96%
rename from android/app/src/main/java/me/kavishdevar/librepods/utils/BluetoothConnectionManager.kt
rename to android/app/src/main/java/me/kavishdevar/librepods/bluetooth/BluetoothConnectionManager.kt
index 249cd2d..d98050b 100644
--- a/android/app/src/main/java/me/kavishdevar/librepods/utils/BluetoothConnectionManager.kt
+++ b/android/app/src/main/java/me/kavishdevar/librepods/bluetooth/BluetoothConnectionManager.kt
@@ -16,7 +16,7 @@
along with this program. If not, see .
*/
-package me.kavishdevar.librepods.utils
+package me.kavishdevar.librepods.bluetooth
import android.bluetooth.BluetoothDevice
import android.bluetooth.BluetoothSocket
diff --git a/android/app/src/main/java/me/kavishdevar/librepods/utils/AirPods.kt b/android/app/src/main/java/me/kavishdevar/librepods/data/AirPods.kt
similarity index 98%
rename from android/app/src/main/java/me/kavishdevar/librepods/utils/AirPods.kt
rename to android/app/src/main/java/me/kavishdevar/librepods/data/AirPods.kt
index 5dd7719..9d83f05 100644
--- a/android/app/src/main/java/me/kavishdevar/librepods/utils/AirPods.kt
+++ b/android/app/src/main/java/me/kavishdevar/librepods/data/AirPods.kt
@@ -16,7 +16,7 @@
along with this program. If not, see .
*/
-package me.kavishdevar.librepods.utils
+package me.kavishdevar.librepods.data
import me.kavishdevar.librepods.R
@@ -256,8 +256,6 @@ data class AirPodsInstance(
val version1: String?,
val version2: String?,
val version3: String?,
- val aacpManager: AACPManager,
- val attManager: ATTManager?
)
object AirPodsModels {
diff --git a/android/app/src/main/java/me/kavishdevar/librepods/data/ControlCommandRepository.kt b/android/app/src/main/java/me/kavishdevar/librepods/data/ControlCommandRepository.kt
index 163c204..9097a2e 100644
--- a/android/app/src/main/java/me/kavishdevar/librepods/data/ControlCommandRepository.kt
+++ b/android/app/src/main/java/me/kavishdevar/librepods/data/ControlCommandRepository.kt
@@ -18,14 +18,14 @@
package me.kavishdevar.librepods.data
-import me.kavishdevar.librepods.utils.AACPManager
-import me.kavishdevar.librepods.utils.AACPManager.Companion.ControlCommandIdentifiers
+import me.kavishdevar.librepods.bluetooth.AACPManager
+import me.kavishdevar.librepods.bluetooth.AACPManager.Companion.ControlCommandIdentifiers
class ControlCommandRepository(
private val aacpManager: AACPManager
) {
fun getValue(
- identifier: AACPManager.Companion.ControlCommandIdentifiers
+ identifier: ControlCommandIdentifiers
): ByteArray? {
return aacpManager.controlCommandStatusList
.find { it.identifier == identifier }
@@ -33,7 +33,7 @@ class ControlCommandRepository(
}
fun setValue(
- id: AACPManager.Companion.ControlCommandIdentifiers,
+ id: ControlCommandIdentifiers,
value: ByteArray
) {
aacpManager.sendControlCommand(id.value, value)
@@ -41,7 +41,7 @@ class ControlCommandRepository(
fun observe(
- identifier: AACPManager.Companion.ControlCommandIdentifiers,
+ identifier: ControlCommandIdentifiers,
onChange: (ByteArray) -> Unit
): AACPManager.ControlCommandListener {
@@ -56,7 +56,7 @@ class ControlCommandRepository(
}
fun remove(
- identifier: AACPManager.Companion.ControlCommandIdentifiers,
+ identifier: ControlCommandIdentifiers,
listener: AACPManager.ControlCommandListener
) {
aacpManager.unregisterControlCommandListener(identifier, listener)
diff --git a/android/app/src/main/java/me/kavishdevar/librepods/utils/HearingAidEnums.kt b/android/app/src/main/java/me/kavishdevar/librepods/data/HearingAid.kt
similarity index 98%
rename from android/app/src/main/java/me/kavishdevar/librepods/utils/HearingAidEnums.kt
rename to android/app/src/main/java/me/kavishdevar/librepods/data/HearingAid.kt
index 94d1820..bf2f554 100644
--- a/android/app/src/main/java/me/kavishdevar/librepods/utils/HearingAidEnums.kt
+++ b/android/app/src/main/java/me/kavishdevar/librepods/data/HearingAid.kt
@@ -16,7 +16,7 @@
along with this program. If not, see .
*/
-package me.kavishdevar.librepods.utils
+package me.kavishdevar.librepods.data
import android.util.Log
import androidx.compose.runtime.MutableState
@@ -25,6 +25,8 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
+import me.kavishdevar.librepods.bluetooth.ATTHandles
+import me.kavishdevar.librepods.bluetooth.ATTManager
import java.io.IOException
import java.nio.ByteBuffer
import java.nio.ByteOrder
diff --git a/android/app/src/main/java/me/kavishdevar/librepods/constants/Packets.kt b/android/app/src/main/java/me/kavishdevar/librepods/data/Packets.kt
similarity index 99%
rename from android/app/src/main/java/me/kavishdevar/librepods/constants/Packets.kt
rename to android/app/src/main/java/me/kavishdevar/librepods/data/Packets.kt
index 1c179d0..a0559db 100644
--- a/android/app/src/main/java/me/kavishdevar/librepods/constants/Packets.kt
+++ b/android/app/src/main/java/me/kavishdevar/librepods/data/Packets.kt
@@ -16,7 +16,7 @@
along with this program. If not, see .
*/
-package me.kavishdevar.librepods.constants
+package me.kavishdevar.librepods.data
import android.os.Parcelable
import android.util.Log
diff --git a/android/app/src/main/java/me/kavishdevar/librepods/constants/StemAction.kt b/android/app/src/main/java/me/kavishdevar/librepods/data/StemAction.kt
similarity index 93%
rename from android/app/src/main/java/me/kavishdevar/librepods/constants/StemAction.kt
rename to android/app/src/main/java/me/kavishdevar/librepods/data/StemAction.kt
index ddf74c0..5bd9e6c 100644
--- a/android/app/src/main/java/me/kavishdevar/librepods/constants/StemAction.kt
+++ b/android/app/src/main/java/me/kavishdevar/librepods/data/StemAction.kt
@@ -16,9 +16,9 @@
along with this program. If not, see .
*/
-package me.kavishdevar.librepods.constants
+package me.kavishdevar.librepods.data
-import me.kavishdevar.librepods.utils.AACPManager
+import me.kavishdevar.librepods.bluetooth.AACPManager
enum class StemAction {
PLAY_PAUSE,
diff --git a/android/app/src/main/java/me/kavishdevar/librepods/utils/TransparencyUtils.kt b/android/app/src/main/java/me/kavishdevar/librepods/data/Transparency.kt
similarity index 98%
rename from android/app/src/main/java/me/kavishdevar/librepods/utils/TransparencyUtils.kt
rename to android/app/src/main/java/me/kavishdevar/librepods/data/Transparency.kt
index 1bbd46c..29afefb 100644
--- a/android/app/src/main/java/me/kavishdevar/librepods/utils/TransparencyUtils.kt
+++ b/android/app/src/main/java/me/kavishdevar/librepods/data/Transparency.kt
@@ -16,13 +16,14 @@
along with this program. If not, see .
*/
-package me.kavishdevar.librepods.utils
+package me.kavishdevar.librepods.data
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
+import me.kavishdevar.librepods.bluetooth.ATTHandles
import java.io.IOException
import java.nio.ByteBuffer
import java.nio.ByteOrder
diff --git a/android/app/src/main/java/me/kavishdevar/librepods/data/XposedRemotePref.kt b/android/app/src/main/java/me/kavishdevar/librepods/data/XposedRemotePref.kt
new file mode 100644
index 0000000..1977b04
--- /dev/null
+++ b/android/app/src/main/java/me/kavishdevar/librepods/data/XposedRemotePref.kt
@@ -0,0 +1,8 @@
+package me.kavishdevar.librepods.data
+
+interface XposedRemotePref {
+ fun isAvailable(): Boolean
+
+ fun getBoolean(key: String, def: Boolean): Boolean
+ fun putBoolean(key: String, value: Boolean)
+}
diff --git a/android/app/src/main/java/me/kavishdevar/librepods/data/XposedRemotePrefProvider.kt b/android/app/src/main/java/me/kavishdevar/librepods/data/XposedRemotePrefProvider.kt
new file mode 100644
index 0000000..9f18e8c
--- /dev/null
+++ b/android/app/src/main/java/me/kavishdevar/librepods/data/XposedRemotePrefProvider.kt
@@ -0,0 +1,5 @@
+package me.kavishdevar.librepods.data
+
+object XposedRemotePrefProvider {
+ fun create(): XposedRemotePref = XposedRemotePrefImpl()
+}
diff --git a/android/app/src/main/java/me/kavishdevar/librepods/composables/AboutCard.kt b/android/app/src/main/java/me/kavishdevar/librepods/presentation/components/AboutCard.kt
similarity index 97%
rename from android/app/src/main/java/me/kavishdevar/librepods/composables/AboutCard.kt
rename to android/app/src/main/java/me/kavishdevar/librepods/presentation/components/AboutCard.kt
index 416abf1..f0669ba 100644
--- a/android/app/src/main/java/me/kavishdevar/librepods/composables/AboutCard.kt
+++ b/android/app/src/main/java/me/kavishdevar/librepods/presentation/components/AboutCard.kt
@@ -18,7 +18,7 @@
@file:OptIn(ExperimentalEncodingApi::class)
-package me.kavishdevar.librepods.composables
+package me.kavishdevar.librepods.presentation.components
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
@@ -75,7 +75,8 @@ fun AboutCard(
style = TextStyle(
fontSize = 14.sp,
fontWeight = FontWeight.Bold,
- color = textColor.copy(alpha = 0.6f)
+ color = textColor.copy(alpha = 0.6f),
+ fontFamily = FontFamily(Font(R.font.sf_pro))
)
)
}
diff --git a/android/app/src/main/java/me/kavishdevar/librepods/composables/AudioSettings.kt b/android/app/src/main/java/me/kavishdevar/librepods/presentation/components/AudioSettings.kt
similarity index 93%
rename from android/app/src/main/java/me/kavishdevar/librepods/composables/AudioSettings.kt
rename to android/app/src/main/java/me/kavishdevar/librepods/presentation/components/AudioSettings.kt
index e4ead08..c7836d0 100644
--- a/android/app/src/main/java/me/kavishdevar/librepods/composables/AudioSettings.kt
+++ b/android/app/src/main/java/me/kavishdevar/librepods/presentation/components/AudioSettings.kt
@@ -18,7 +18,7 @@
@file:OptIn(ExperimentalEncodingApi::class)
-package me.kavishdevar.librepods.composables
+package me.kavishdevar.librepods.presentation.components
import androidx.compose.foundation.background
import androidx.compose.foundation.isSystemInDarkTheme
@@ -35,6 +35,8 @@ import androidx.compose.ui.draw.clip
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.tooling.preview.Preview
import androidx.compose.ui.unit.dp
@@ -61,7 +63,7 @@ fun AudioSettings(
loudSoundReductionChecked: Boolean,
onLoudSoundReductionCheckedChange: (Boolean) -> Unit,
- isXposed: Boolean,
+ vendorIdHook: Boolean,
isPremium: Boolean
) {
val isDarkTheme = isSystemInDarkTheme()
@@ -80,7 +82,8 @@ fun AudioSettings(
style = TextStyle(
fontSize = 14.sp,
fontWeight = FontWeight.Bold,
- color = textColor.copy(alpha = 0.6f)
+ color = textColor.copy(alpha = 0.6f),
+ fontFamily = FontFamily(Font(R.font.sf_pro))
)
)
}
@@ -130,13 +133,14 @@ fun AudioSettings(
)
}
- if (loudSoundReductionCapability && isXposed){
+ if (loudSoundReductionCapability && vendorIdHook){
StyledToggle(
label = stringResource(R.string.loud_sound_reduction),
description = stringResource(R.string.loud_sound_reduction_description),
independent = false,
checked = loudSoundReductionChecked,
- onCheckedChange = onLoudSoundReductionCheckedChange
+ onCheckedChange = onLoudSoundReductionCheckedChange,
+ enabled = isPremium
)
HorizontalDivider(
thickness = 1.dp,
@@ -172,7 +176,7 @@ fun AudioSettingsPreview() {
onConversationalAwarenessCheckedChange = { },
loudSoundReductionChecked = true,
onLoudSoundReductionCheckedChange = { },
- isXposed = true,
+ vendorIdHook = true,
isPremium = true
)
}
diff --git a/android/app/src/main/java/me/kavishdevar/librepods/composables/BatteryIndicator.kt b/android/app/src/main/java/me/kavishdevar/librepods/presentation/components/BatteryIndicator.kt
similarity index 68%
rename from android/app/src/main/java/me/kavishdevar/librepods/composables/BatteryIndicator.kt
rename to android/app/src/main/java/me/kavishdevar/librepods/presentation/components/BatteryIndicator.kt
index b34ffc4..c2bff84 100644
--- a/android/app/src/main/java/me/kavishdevar/librepods/composables/BatteryIndicator.kt
+++ b/android/app/src/main/java/me/kavishdevar/librepods/presentation/components/BatteryIndicator.kt
@@ -16,20 +16,21 @@
along with this program. If not, see .
*/
-package me.kavishdevar.librepods.composables
+package me.kavishdevar.librepods.presentation.components
import android.content.res.Configuration
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.tween
+import androidx.compose.foundation.Canvas
import androidx.compose.foundation.background
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
@@ -38,8 +39,10 @@ import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.scale
+import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.StrokeCap
+import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.Font
import androidx.compose.ui.text.font.FontFamily
@@ -48,11 +51,15 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import me.kavishdevar.librepods.R
+import me.kavishdevar.librepods.data.BatteryStatus
+import kotlin.math.cos
+import kotlin.math.sin
+import kotlin.math.sqrt
@Composable
fun BatteryIndicator(
batteryPercentage: Int,
- charging: Boolean = false,
+ status: Int,
prefix: String = "",
previousCharging: Boolean = false,
) {
@@ -65,6 +72,7 @@ fun BatteryIndicator(
val initialScale = if (previousCharging) 1f else 0f
val scaleAnim = remember { Animatable(initialScale) }
+ val charging = status == BatteryStatus.CHARGING || status == BatteryStatus.OPTIMIZED_CHARGING
val targetScale = if (charging) 1f else 0f
LaunchedEffect(previousCharging, charging) {
@@ -80,6 +88,43 @@ fun BatteryIndicator(
modifier = Modifier.padding(bottom = 4.dp),
contentAlignment = Alignment.Center
) {
+ val strokeWidthPx = with(LocalDensity.current) { 4.dp.toPx() }
+ val gapFromCenterPx = with(LocalDensity.current) { 8.sp.toPx() }
+
+ if (status == BatteryStatus.OPTIMIZED_CHARGING) {
+ Canvas(modifier = Modifier.size(40.dp)) {
+ val radius = size.minDimension / 2
+ val progress = batteryPercentage / 100f
+
+ val angleDeg = -90f + 360f * progress
+ val angleRad = Math.toRadians(angleDeg.toDouble())
+
+ val outerX = center.x + (radius - strokeWidthPx) * cos(angleRad).toFloat()
+ val outerY = center.y + (radius - strokeWidthPx) * sin(angleRad).toFloat()
+
+ val dirX = center.x - outerX
+ val dirY = center.y - outerY
+ val length = sqrt(dirX * dirX + dirY * dirY)
+
+ val normX = dirX / length
+ val normY = dirY / length
+
+ val startX = outerX - normX * strokeWidthPx / 2
+ val startY = outerY - normY * strokeWidthPx / 2
+
+ val endX = center.x - normX * gapFromCenterPx
+ val endY = center.y - normY * gapFromCenterPx
+
+ drawLine(
+ color = batteryFillColor,
+ start = Offset(startX, startY),
+ end = Offset(endX, endY),
+ strokeWidth = strokeWidthPx,
+ cap = StrokeCap.Round
+ )
+ }
+ }
+
CircularProgressIndicator(
progress = { batteryPercentage / 100f },
modifier = Modifier.size(40.dp),
@@ -123,6 +168,6 @@ fun BatteryIndicatorPreview() {
Box(
modifier = Modifier.background(bg)
) {
- BatteryIndicator(batteryPercentage = 24, charging = true, prefix = "\uDBC6\uDCE5", previousCharging = false)
+ BatteryIndicator(batteryPercentage = 80, status = BatteryStatus.CHARGING, prefix = "\uDBC6\uDCE5", previousCharging = false)
}
}
diff --git a/android/app/src/main/java/me/kavishdevar/librepods/composables/BatteryView.kt b/android/app/src/main/java/me/kavishdevar/librepods/presentation/components/BatteryView.kt
similarity index 87%
rename from android/app/src/main/java/me/kavishdevar/librepods/composables/BatteryView.kt
rename to android/app/src/main/java/me/kavishdevar/librepods/presentation/components/BatteryView.kt
index 4803ed3..7accabe 100644
--- a/android/app/src/main/java/me/kavishdevar/librepods/composables/BatteryView.kt
+++ b/android/app/src/main/java/me/kavishdevar/librepods/presentation/components/BatteryView.kt
@@ -18,10 +18,9 @@
@file:OptIn(ExperimentalEncodingApi::class)
-package me.kavishdevar.librepods.composables
+package me.kavishdevar.librepods.presentation.components
import android.content.res.Configuration
-import android.util.Log
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.isSystemInDarkTheme
@@ -41,15 +40,14 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ImageBitmap
-import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.res.imageResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import me.kavishdevar.librepods.R
-import me.kavishdevar.librepods.constants.Battery
-import me.kavishdevar.librepods.constants.BatteryComponent
-import me.kavishdevar.librepods.constants.BatteryStatus
+import me.kavishdevar.librepods.data.Battery
+import me.kavishdevar.librepods.data.BatteryComponent
+import me.kavishdevar.librepods.data.BatteryStatus
import kotlin.io.encoding.ExperimentalEncodingApi
@Composable
@@ -66,12 +64,6 @@ fun BatteryView(
val rightLevel = right?.level ?: 0
val caseLevel = case?.level ?: 0
- val leftCharging = left?.status == BatteryStatus.CHARGING ||
- left?.status == BatteryStatus.OPTIMIZED_CHARGING
-
- val rightCharging = right?.status == BatteryStatus.CHARGING ||
- right?.status == BatteryStatus.OPTIMIZED_CHARGING
-
val caseCharging = case?.status == BatteryStatus.CHARGING ||
case?.status == BatteryStatus.OPTIMIZED_CHARGING
@@ -98,12 +90,12 @@ fun BatteryView(
)
if (
- leftCharging == rightCharging &&
+ left?.status == right?.status &&
(leftLevel - rightLevel) in -3..3
) {
BatteryIndicator(
leftLevel.coerceAtMost(rightLevel),
- leftCharging
+ left?.status ?: BatteryStatus.NOT_CHARGING
)
singleDisplayed.value = true
} else {
@@ -116,7 +108,7 @@ fun BatteryView(
if (leftLevel > 0 || left?.status != BatteryStatus.DISCONNECTED) {
BatteryIndicator(
leftLevel,
- leftCharging,
+ left?.status ?: BatteryStatus.NOT_CHARGING,
"\uDBC6\uDCE5"
)
}
@@ -128,7 +120,7 @@ fun BatteryView(
if (rightLevel > 0 || right?.status != BatteryStatus.DISCONNECTED) {
BatteryIndicator(
rightLevel,
- rightCharging,
+ right?.status ?: BatteryStatus.NOT_CHARGING,
"\uDBC6\uDCE8"
)
}
@@ -151,7 +143,7 @@ fun BatteryView(
if (caseLevel > 0 || case?.status != BatteryStatus.DISCONNECTED) {
BatteryIndicator(
caseLevel,
- caseCharging,
+ case?.status ?: BatteryStatus.NOT_CHARGING,
prefix = if (!singleDisplayed.value) "\uDBC3\uDE6C" else ""
)
}
@@ -165,7 +157,7 @@ fun BatteryView(
fun BatteryViewPreview() {
val fakeBattery = listOf(
Battery(BatteryComponent.LEFT, 85, BatteryStatus.CHARGING),
- Battery(BatteryComponent.RIGHT, 40, BatteryStatus.CHARGING),
+ Battery(BatteryComponent.RIGHT, 40, BatteryStatus.OPTIMIZED_CHARGING),
Battery(BatteryComponent.CASE, 60, BatteryStatus.NOT_CHARGING)
)
diff --git a/android/app/src/main/java/me/kavishdevar/librepods/composables/CallControlSettings.kt b/android/app/src/main/java/me/kavishdevar/librepods/presentation/components/CallControlSettings.kt
similarity index 91%
rename from android/app/src/main/java/me/kavishdevar/librepods/composables/CallControlSettings.kt
rename to android/app/src/main/java/me/kavishdevar/librepods/presentation/components/CallControlSettings.kt
index ec8ae16..2b00c06 100644
--- a/android/app/src/main/java/me/kavishdevar/librepods/composables/CallControlSettings.kt
+++ b/android/app/src/main/java/me/kavishdevar/librepods/presentation/components/CallControlSettings.kt
@@ -18,7 +18,7 @@
@file:OptIn(ExperimentalEncodingApi::class)
-package me.kavishdevar.librepods.composables
+package me.kavishdevar.librepods.presentation.components
import android.util.Log
import androidx.compose.foundation.background
@@ -41,15 +41,18 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableLongStateOf
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
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.layout.positionInParent
import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.Font
@@ -59,6 +62,7 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import dev.chrisbanes.haze.HazeState
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
+import kotlinx.coroutines.launch
import me.kavishdevar.librepods.R
import kotlin.io.encoding.ExperimentalEncodingApi
@@ -82,7 +86,8 @@ fun CallControlSettings(
style = TextStyle(
fontSize = 14.sp,
fontWeight = FontWeight.Bold,
- color = textColor.copy(alpha = 0.6f)
+ color = textColor.copy(alpha = 0.6f),
+ fontFamily = FontFamily(Font(R.font.sf_pro))
)
)
}
@@ -93,6 +98,10 @@ fun CallControlSettings(
.background(backgroundColor, RoundedCornerShape(28.dp))
.padding(top = 2.dp)
) {
+
+ val scope = rememberCoroutineScope()
+ val haptics = LocalHapticFeedback.current
+
val pressOnceText = stringResource(R.string.press_once)
val pressTwiceText = stringResource(R.string.press_twice)
@@ -105,6 +114,7 @@ fun CallControlSettings(
var lastDismissTimeSingle by remember { mutableLongStateOf(0L) }
var parentHoveredIndexSingle by remember { mutableStateOf(null) }
var parentDragActiveSingle by remember { mutableStateOf(false) }
+ var previousIdxSingle by remember { mutableStateOf(null) }
var showDoublePressDropdown by remember { mutableStateOf(false) }
var touchOffsetDouble by remember { mutableStateOf(null) }
@@ -112,6 +122,7 @@ fun CallControlSettings(
var lastDismissTimeDouble by remember { mutableLongStateOf(0L) }
var parentHoveredIndexDouble by remember { mutableStateOf(null) }
var parentDragActiveDouble by remember { mutableStateOf(false) }
+ var previousIdxDouble by remember { mutableStateOf(null) }
LaunchedEffect(flipped) {
Log.d("CallControlSettings", "Call control flipped: $flipped")
@@ -187,7 +198,11 @@ fun CallControlSettings(
val touch = touchOffsetSingle ?: current
val posInPopupY = current.y - touch.y
val idx = (posInPopupY / itemHeightPx).toInt()
+ if (idx != previousIdxSingle) {
+ scope.launch { haptics.performHapticFeedback(HapticFeedbackType.SegmentTick) }
+ }
parentHoveredIndexSingle = idx
+ previousIdxSingle = idx
},
onDragEnd = {
parentDragActiveSingle = false
@@ -204,6 +219,9 @@ fun CallControlSettings(
}
}
+ if (parentHoveredIndexSingle != null && parentHoveredIndexSingle in 0..1) {
+ scope.launch { haptics.performHapticFeedback(HapticFeedbackType.GestureEnd) }
+ }
parentHoveredIndexSingle = null
},
onDragCancel = {
@@ -316,7 +334,11 @@ fun CallControlSettings(
val touch = touchOffsetDouble ?: current
val posInPopupY = current.y - touch.y
val idx = (posInPopupY / itemHeightPx).toInt()
+ if (idx != previousIdxDouble) {
+ scope.launch { haptics.performHapticFeedback(HapticFeedbackType.SegmentTick) }
+ }
parentHoveredIndexDouble = idx
+ previousIdxDouble = idx
},
onDragEnd = {
parentDragActiveDouble = false
@@ -330,9 +352,12 @@ fun CallControlSettings(
showDoublePressDropdown = false
lastDismissTimeDouble = System.currentTimeMillis()
val flipped = option == pressOnceText
- onCallControlValueChanged (flipped)
+ onCallControlValueChanged(flipped)
}
}
+ if (parentHoveredIndexDouble != null && parentHoveredIndexDouble in 0..1) {
+ scope.launch { haptics.performHapticFeedback(HapticFeedbackType.GestureEnd) }
+ }
parentHoveredIndexDouble = null
},
onDragCancel = {
diff --git a/android/app/src/main/java/me/kavishdevar/librepods/composables/ConfirmationDialog.kt b/android/app/src/main/java/me/kavishdevar/librepods/presentation/components/ConfirmationDialog.kt
similarity index 87%
rename from android/app/src/main/java/me/kavishdevar/librepods/composables/ConfirmationDialog.kt
rename to android/app/src/main/java/me/kavishdevar/librepods/presentation/components/ConfirmationDialog.kt
index e2c347b..337675f 100644
--- a/android/app/src/main/java/me/kavishdevar/librepods/composables/ConfirmationDialog.kt
+++ b/android/app/src/main/java/me/kavishdevar/librepods/presentation/components/ConfirmationDialog.kt
@@ -16,7 +16,7 @@
along with this program. If not, see .
*/
-package me.kavishdevar.librepods.composables
+package me.kavishdevar.librepods.presentation.components
import androidx.compose.foundation.background
import androidx.compose.foundation.isSystemInDarkTheme
@@ -24,6 +24,7 @@ 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.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
@@ -38,13 +39,16 @@ import androidx.compose.runtime.MutableState
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
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.input.pointer.PointerEventType
import androidx.compose.ui.input.pointer.pointerInput
+import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.Font
import androidx.compose.ui.text.font.FontFamily
@@ -53,10 +57,12 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.Dialog
+import androidx.compose.ui.window.DialogProperties
import dev.chrisbanes.haze.HazeState
import dev.chrisbanes.haze.hazeEffect
import dev.chrisbanes.haze.materials.CupertinoMaterials
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
+import kotlinx.coroutines.launch
import me.kavishdevar.librepods.R
@ExperimentalHazeMaterialsApi
@@ -74,8 +80,18 @@ fun ConfirmationDialog(
val isDarkTheme = isSystemInDarkTheme()
val textColor = if (isDarkTheme) Color.White else Color.Black
val accentColor = if (isDarkTheme) Color(0xFF007AFF) else Color(0xFF3C6DF5)
+
+ val haptics = LocalHapticFeedback.current
+ val scope = rememberCoroutineScope()
+
if (showDialog.value) {
- Dialog(onDismissRequest = { showDialog.value = false }) {
+ Dialog(
+ onDismissRequest = { showDialog.value = false },
+ properties = DialogProperties(
+ dismissOnBackPress = false,
+ dismissOnClickOutside = false
+ )
+ ) {
Box(
modifier = Modifier
// .fillMaxWidth(0.75f)
@@ -90,7 +106,7 @@ fun ConfirmationDialog(
)
) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
- androidx.compose.foundation.layout.Spacer(modifier = Modifier.height(24.dp))
+ Spacer(modifier = Modifier.height(24.dp))
Text(
title,
style = TextStyle(
@@ -102,7 +118,7 @@ fun ConfirmationDialog(
textAlign = TextAlign.Center,
modifier = Modifier.padding(horizontal = 16.dp)
)
- androidx.compose.foundation.layout.Spacer(modifier = Modifier.height(12.dp))
+ Spacer(modifier = Modifier.height(12.dp))
Text(
message,
style = TextStyle(
@@ -113,7 +129,7 @@ fun ConfirmationDialog(
textAlign = TextAlign.Center,
modifier = Modifier.padding(horizontal = 16.dp)
)
- androidx.compose.foundation.layout.Spacer(modifier = Modifier.height(16.dp))
+ Spacer(modifier = Modifier.height(16.dp))
HorizontalDivider(
thickness = 1.dp,
color = Color(0x40888888),
@@ -148,6 +164,8 @@ fun ConfirmationDialog(
}
PointerEventType.Move -> {
if (isWithinBounds) {
+ if (leftPressed != isLeft) scope.launch { haptics.performHapticFeedback(
+ HapticFeedbackType.SegmentTick) }
leftPressed = isLeft
rightPressed = !isLeft
} else {
@@ -158,8 +176,12 @@ fun ConfirmationDialog(
PointerEventType.Release -> {
if (isWithinBounds) {
if (leftPressed) {
+ scope.launch { haptics.performHapticFeedback(
+ HapticFeedbackType.Reject) }
onDismiss()
} else if (rightPressed) {
+ scope.launch { haptics.performHapticFeedback(
+ HapticFeedbackType.Confirm) }
onConfirm()
}
}
diff --git a/android/app/src/main/java/me/kavishdevar/librepods/composables/ConnectionSettings.kt b/android/app/src/main/java/me/kavishdevar/librepods/presentation/components/ConnectionSettings.kt
similarity index 97%
rename from android/app/src/main/java/me/kavishdevar/librepods/composables/ConnectionSettings.kt
rename to android/app/src/main/java/me/kavishdevar/librepods/presentation/components/ConnectionSettings.kt
index b95807c..ba54818 100644
--- a/android/app/src/main/java/me/kavishdevar/librepods/composables/ConnectionSettings.kt
+++ b/android/app/src/main/java/me/kavishdevar/librepods/presentation/components/ConnectionSettings.kt
@@ -18,7 +18,7 @@
@file:OptIn(ExperimentalEncodingApi::class)
-package me.kavishdevar.librepods.composables
+package me.kavishdevar.librepods.presentation.components
import androidx.compose.foundation.background
import androidx.compose.foundation.isSystemInDarkTheme
diff --git a/android/app/src/main/java/me/kavishdevar/librepods/composables/ControlCenterButton.kt b/android/app/src/main/java/me/kavishdevar/librepods/presentation/components/ControlCenterButton.kt
similarity index 98%
rename from android/app/src/main/java/me/kavishdevar/librepods/composables/ControlCenterButton.kt
rename to android/app/src/main/java/me/kavishdevar/librepods/presentation/components/ControlCenterButton.kt
index 241363b..5340b0c 100644
--- a/android/app/src/main/java/me/kavishdevar/librepods/composables/ControlCenterButton.kt
+++ b/android/app/src/main/java/me/kavishdevar/librepods/presentation/components/ControlCenterButton.kt
@@ -18,7 +18,7 @@
@file:Suppress("unused")
-package me.kavishdevar.librepods.composables
+package me.kavishdevar.librepods.presentation.components
import androidx.compose.animation.animateColorAsState
import androidx.compose.foundation.background
diff --git a/android/app/src/main/java/me/kavishdevar/librepods/composables/ControlCenterNoiseControlSegmentedButton.kt b/android/app/src/main/java/me/kavishdevar/librepods/presentation/components/ControlCenterNoiseControlSegmentedButton.kt
similarity index 98%
rename from android/app/src/main/java/me/kavishdevar/librepods/composables/ControlCenterNoiseControlSegmentedButton.kt
rename to android/app/src/main/java/me/kavishdevar/librepods/presentation/components/ControlCenterNoiseControlSegmentedButton.kt
index c41fdab..ca0d1f4 100644
--- a/android/app/src/main/java/me/kavishdevar/librepods/composables/ControlCenterNoiseControlSegmentedButton.kt
+++ b/android/app/src/main/java/me/kavishdevar/librepods/presentation/components/ControlCenterNoiseControlSegmentedButton.kt
@@ -16,7 +16,7 @@
along with this program. If not, see .
*/
-package me.kavishdevar.librepods.composables
+package me.kavishdevar.librepods.presentation.components
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.animateDpAsState
@@ -56,7 +56,7 @@ import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import me.kavishdevar.librepods.R
-import me.kavishdevar.librepods.constants.NoiseControlMode
+import me.kavishdevar.librepods.data.NoiseControlMode
private val ContainerColor = Color(0x593C3C3E)
private val SelectedIndicatorColorGray = Color(0xFF6C6C6E)
diff --git a/android/app/src/main/java/me/kavishdevar/librepods/composables/HearingHealthSettings.kt b/android/app/src/main/java/me/kavishdevar/librepods/presentation/components/HearingHealthSettings.kt
similarity index 91%
rename from android/app/src/main/java/me/kavishdevar/librepods/composables/HearingHealthSettings.kt
rename to android/app/src/main/java/me/kavishdevar/librepods/presentation/components/HearingHealthSettings.kt
index 1379d27..a444dda 100644
--- a/android/app/src/main/java/me/kavishdevar/librepods/composables/HearingHealthSettings.kt
+++ b/android/app/src/main/java/me/kavishdevar/librepods/presentation/components/HearingHealthSettings.kt
@@ -18,7 +18,7 @@
@file:OptIn(ExperimentalEncodingApi::class)
-package me.kavishdevar.librepods.composables
+package me.kavishdevar.librepods.presentation.components
import androidx.compose.foundation.background
import androidx.compose.foundation.isSystemInDarkTheme
@@ -35,6 +35,8 @@ import androidx.compose.ui.draw.clip
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
@@ -47,12 +49,12 @@ fun HearingHealthSettings(
navController: NavController,
hasPPECapability: Boolean,
hasHearingAidCapability: Boolean,
- isXposed: Boolean
+ vendorIdHook: Boolean
) {
val isDarkTheme = isSystemInDarkTheme()
val textColor = if (isDarkTheme) Color.White else Color.Black
val backgroundColor = if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFFFFFFF)
- val shouldShowHearingAid = hasHearingAidCapability && isXposed
+ val shouldShowHearingAid = hasHearingAidCapability && vendorIdHook
if (hasPPECapability && shouldShowHearingAid) {
Box(
@@ -65,7 +67,8 @@ fun HearingHealthSettings(
style = TextStyle(
fontSize = 14.sp,
fontWeight = FontWeight.Bold,
- color = textColor.copy(alpha = 0.6f)
+ color = textColor.copy(alpha = 0.6f),
+ fontFamily = FontFamily(Font(R.font.sf_pro))
)
)
}
diff --git a/android/app/src/main/java/me/kavishdevar/librepods/composables/MicrophoneSettings.kt b/android/app/src/main/java/me/kavishdevar/librepods/presentation/components/MicrophoneSettings.kt
similarity index 92%
rename from android/app/src/main/java/me/kavishdevar/librepods/composables/MicrophoneSettings.kt
rename to android/app/src/main/java/me/kavishdevar/librepods/presentation/components/MicrophoneSettings.kt
index 5ade04d..5f693e9 100644
--- a/android/app/src/main/java/me/kavishdevar/librepods/composables/MicrophoneSettings.kt
+++ b/android/app/src/main/java/me/kavishdevar/librepods/presentation/components/MicrophoneSettings.kt
@@ -18,9 +18,8 @@
@file:OptIn(ExperimentalEncodingApi::class)
-package me.kavishdevar.librepods.composables
+package me.kavishdevar.librepods.presentation.components
-import android.util.Log
import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.detectDragGesturesAfterLongPress
import androidx.compose.foundation.gestures.detectTapGestures
@@ -39,15 +38,18 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableLongStateOf
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
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.layout.positionInParent
import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.Font
@@ -56,8 +58,8 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import dev.chrisbanes.haze.HazeState
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
+import kotlinx.coroutines.launch
import me.kavishdevar.librepods.R
-import me.kavishdevar.librepods.utils.AACPManager
import kotlin.io.encoding.ExperimentalEncodingApi
@ExperimentalHazeMaterialsApi
@@ -93,26 +95,13 @@ fun MicrophoneSettings(
var lastDismissTime by remember { mutableLongStateOf(0L) }
val reopenThresholdMs = 250L
- val listener = object : AACPManager.ControlCommandListener {
- override fun onControlCommandReceived(controlCommand: AACPManager.ControlCommand) {
- if (AACPManager.Companion.ControlCommandIdentifiers.fromByte(controlCommand.identifier) ==
- AACPManager.Companion.ControlCommandIdentifiers.MIC_MODE
- ) {
- selectedMode = when (controlCommand.value[0]) {
- 0x00.toByte() -> "Automatic"
- 0x01.toByte() -> "Always Right"
- 0x02.toByte() -> "Always Left"
- else -> "Automatic"
- }
- Log.d("MicrophoneSettings", "Microphone mode received: $selectedMode")
- }
- }
- }
-
val density = LocalDensity.current
val itemHeightPx = with(density) { 48.dp.toPx() }
var parentHoveredIndex by remember { mutableStateOf(null) }
var parentDragActive by remember { mutableStateOf(false) }
+ var previousIdx by remember { mutableStateOf(null) }
+ val haptics = LocalHapticFeedback.current
+ val scope = rememberCoroutineScope()
val microphoneAutomaticText = stringResource(R.string.microphone_automatic)
val microphoneAlwaysRightText = stringResource(R.string.microphone_always_right)
val microphoneAlwaysLeftText = stringResource(R.string.microphone_always_left)
@@ -152,7 +141,11 @@ fun MicrophoneSettings(
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
@@ -180,6 +173,9 @@ fun MicrophoneSettings(
onMicModeValueChanged(byteValue.toByte())
}
}
+ if (parentHoveredIndex != null && parentHoveredIndex in 0..2) {
+ scope.launch { haptics.performHapticFeedback(HapticFeedbackType.GestureEnd) }
+ }
parentHoveredIndex = null
},
onDragCancel = {
diff --git a/android/app/src/main/java/me/kavishdevar/librepods/composables/NavigationButton.kt b/android/app/src/main/java/me/kavishdevar/librepods/presentation/components/NavigationButton.kt
similarity index 82%
rename from android/app/src/main/java/me/kavishdevar/librepods/composables/NavigationButton.kt
rename to android/app/src/main/java/me/kavishdevar/librepods/presentation/components/NavigationButton.kt
index c66f2bc..ceff731 100644
--- a/android/app/src/main/java/me/kavishdevar/librepods/composables/NavigationButton.kt
+++ b/android/app/src/main/java/me/kavishdevar/librepods/presentation/components/NavigationButton.kt
@@ -16,7 +16,7 @@
along with this program. If not, see .
*/
-package me.kavishdevar.librepods.composables
+package me.kavishdevar.librepods.presentation.components
import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.tween
@@ -35,21 +35,25 @@ 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
import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalHapticFeedback
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.tooling.preview.Preview
-import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.navigation.NavController
+import kotlinx.coroutines.launch
import me.kavishdevar.librepods.R
@Composable
@@ -62,10 +66,14 @@ fun NavigationButton(
description: String? = null,
currentState: String? = null,
height: Dp = 58.dp,
+ enabled: Boolean = true
) {
val isDarkTheme = isSystemInDarkTheme()
var backgroundColor by remember { mutableStateOf(if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFFFFFFF)) }
val animatedBackgroundColor by animateColorAsState(targetValue = backgroundColor, animationSpec = tween(durationMillis = 500))
+ val haptics = LocalHapticFeedback.current
+ val scope = rememberCoroutineScope()
+
Column {
if (title != null) {
Box(
@@ -79,23 +87,34 @@ fun NavigationButton(
fontSize = 14.sp,
fontWeight = FontWeight.Bold,
color = if (isDarkTheme) Color.White.copy(alpha = 0.6f) else Color.Black.copy(alpha = 0.6f),
+ fontFamily = FontFamily(Font(R.font.sf_pro))
)
)
}
}
Row(
modifier = Modifier
- .background(animatedBackgroundColor, RoundedCornerShape(if (independent) 28.dp else 0.dp))
+ .background(
+ animatedBackgroundColor,
+ RoundedCornerShape(if (independent) 28.dp else 0.dp)
+ )
.height(height)
.pointerInput(Unit) {
detectTapGestures(
onPress = {
- backgroundColor = if (isDarkTheme) Color(0x40888888) else Color(0x40D9D9D9)
- tryAwaitRelease()
- backgroundColor = if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFFFFFFF)
+ if (enabled) {
+ backgroundColor =
+ if (isDarkTheme) Color(0x40888888) else Color(0x40D9D9D9)
+ tryAwaitRelease()
+ backgroundColor =
+ if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFFFFFFF)
+ }
},
onTap = {
- if (onClick != null) onClick() else navController.navigate(to)
+ if (enabled) {
+ scope.launch { haptics.performHapticFeedback(HapticFeedbackType.ContextClick) }
+ if (onClick != null) onClick() else navController.navigate(to)
+ }
}
)
}
diff --git a/android/app/src/main/java/me/kavishdevar/librepods/composables/NoiseControlButton.kt b/android/app/src/main/java/me/kavishdevar/librepods/presentation/components/NoiseControlButton.kt
similarity index 97%
rename from android/app/src/main/java/me/kavishdevar/librepods/composables/NoiseControlButton.kt
rename to android/app/src/main/java/me/kavishdevar/librepods/presentation/components/NoiseControlButton.kt
index 6c7ec36..a5b880f 100644
--- a/android/app/src/main/java/me/kavishdevar/librepods/composables/NoiseControlButton.kt
+++ b/android/app/src/main/java/me/kavishdevar/librepods/presentation/components/NoiseControlButton.kt
@@ -16,7 +16,7 @@
along with this program. If not, see .
*/
-package me.kavishdevar.librepods.composables
+package me.kavishdevar.librepods.presentation.components
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
@@ -74,4 +74,4 @@ fun NoiseControlButtonPreview() {
onClick = {},
textColor = Color.White,
)
-}
\ No newline at end of file
+}
diff --git a/android/app/src/main/java/me/kavishdevar/librepods/composables/NoiseControlSettings.kt b/android/app/src/main/java/me/kavishdevar/librepods/presentation/components/NoiseControlSettings.kt
similarity index 98%
rename from android/app/src/main/java/me/kavishdevar/librepods/composables/NoiseControlSettings.kt
rename to android/app/src/main/java/me/kavishdevar/librepods/presentation/components/NoiseControlSettings.kt
index 699ed37..453c35c 100644
--- a/android/app/src/main/java/me/kavishdevar/librepods/composables/NoiseControlSettings.kt
+++ b/android/app/src/main/java/me/kavishdevar/librepods/presentation/components/NoiseControlSettings.kt
@@ -18,7 +18,7 @@
@file:OptIn(ExperimentalEncodingApi::class)
-package me.kavishdevar.librepods.composables
+package me.kavishdevar.librepods.presentation.components
import android.annotation.SuppressLint
import androidx.compose.animation.core.AnimationSpec
@@ -59,6 +59,8 @@ import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.imageResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.font.Font
+import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.IntOffset
@@ -66,7 +68,7 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.zIndex
import me.kavishdevar.librepods.R
-import me.kavishdevar.librepods.constants.NoiseControlMode
+import me.kavishdevar.librepods.data.NoiseControlMode
import kotlin.io.encoding.ExperimentalEncodingApi
import kotlin.math.roundToInt
@@ -147,6 +149,7 @@ fun NoiseControlSettings(
fontSize = 14.sp,
fontWeight = FontWeight.Bold,
color = textColor.copy(alpha = 0.6f),
+ fontFamily = FontFamily(Font(R.font.sf_pro))
)
)
}
diff --git a/android/app/src/main/java/me/kavishdevar/librepods/composables/PressAndHoldSettings.kt b/android/app/src/main/java/me/kavishdevar/librepods/presentation/components/PressAndHoldSettings.kt
similarity index 97%
rename from android/app/src/main/java/me/kavishdevar/librepods/composables/PressAndHoldSettings.kt
rename to android/app/src/main/java/me/kavishdevar/librepods/presentation/components/PressAndHoldSettings.kt
index 1861298..a29ea72 100644
--- a/android/app/src/main/java/me/kavishdevar/librepods/composables/PressAndHoldSettings.kt
+++ b/android/app/src/main/java/me/kavishdevar/librepods/presentation/components/PressAndHoldSettings.kt
@@ -16,7 +16,7 @@
along with this program. If not, see .
*/
-package me.kavishdevar.librepods.composables
+package me.kavishdevar.librepods.presentation.components
import androidx.compose.foundation.background
import androidx.compose.foundation.isSystemInDarkTheme
@@ -40,7 +40,7 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.navigation.NavController
import me.kavishdevar.librepods.R
-import me.kavishdevar.librepods.constants.StemAction
+import me.kavishdevar.librepods.data.StemAction
@Composable
fun PressAndHoldSettings(
diff --git a/android/app/src/main/java/me/kavishdevar/librepods/composables/StyledButton.kt b/android/app/src/main/java/me/kavishdevar/librepods/presentation/components/StyledButton.kt
similarity index 81%
rename from android/app/src/main/java/me/kavishdevar/librepods/composables/StyledButton.kt
rename to android/app/src/main/java/me/kavishdevar/librepods/presentation/components/StyledButton.kt
index 771dd24..5012ce0 100644
--- a/android/app/src/main/java/me/kavishdevar/librepods/composables/StyledButton.kt
+++ b/android/app/src/main/java/me/kavishdevar/librepods/presentation/components/StyledButton.kt
@@ -16,7 +16,7 @@
along with this program. If not, see .
*/
-package me.kavishdevar.librepods.composables
+package me.kavishdevar.librepods.presentation.components
import android.graphics.RuntimeShader
import android.os.Build
@@ -46,7 +46,9 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ShaderBrush
import androidx.compose.ui.graphics.isSpecified
import androidx.compose.ui.graphics.toArgb
+import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.input.pointer.pointerInput
+import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.fastCoerceAtMost
@@ -77,7 +79,8 @@ fun StyledButton(
maxScale: Float = 0.1f,
content: @Composable RowScope.() -> Unit,
) {
- val animationScope = rememberCoroutineScope()
+ val scope = rememberCoroutineScope()
+ val haptics = LocalHapticFeedback.current
val progressAnimation = remember { Animatable(0f) }
var pressStartPosition by remember { mutableStateOf(Offset.Zero) }
val offsetAnimation = remember { Animatable(Offset.Zero, Offset.VectorConverter) }
@@ -163,19 +166,21 @@ half4 main(float2 coord) {
val maxOffset = size.minDimension
val initialDerivative = 0.05f
val offset = offsetAnimation.value
- translationX = maxOffset * tanh(initialDerivative * offset.x / maxOffset)
- translationY = maxOffset * tanh(initialDerivative * offset.y / maxOffset)
+ translationX =
+ maxOffset * tanh(initialDerivative * offset.x / maxOffset)
+ translationY =
+ maxOffset * tanh(initialDerivative * offset.y / maxOffset)
val maxDragScale = 0.1f
val offsetAngle = atan2(offset.y, offset.x)
scaleX =
scale +
- maxDragScale * abs(cos(offsetAngle) * offset.x / size.maxDimension) *
- (width / height).fastCoerceAtMost(1f)
+ maxDragScale * abs(cos(offsetAngle) * offset.x / size.maxDimension) *
+ (width / height).fastCoerceAtMost(1f)
scaleY =
scale +
- maxDragScale * abs(sin(offsetAngle) * offset.y / size.maxDimension) *
- (height / width).fastCoerceAtMost(1f)
+ maxDragScale * abs(sin(offsetAngle) * offset.y / size.maxDimension) *
+ (height / width).fastCoerceAtMost(1f)
},
onDrawSurface = {
if (tint.isSpecified) {
@@ -209,7 +214,10 @@ half4 main(float2 coord) {
interactiveHighlightShader.apply {
val offset = pressStartPosition + offsetAnimation.value
setFloatUniform("size", size.width, size.height)
- setColorUniform("color", Color.White.copy(0.15f * progress).toArgb())
+ setColorUniform(
+ "color",
+ Color.White.copy(0.15f * progress).toArgb()
+ )
setFloatUniform("radius", size.maxDimension)
setFloatUniform(
"offset",
@@ -236,31 +244,51 @@ half4 main(float2 coord) {
interactionSource = null,
indication = null,
role = Role.Button,
- onClick = onClick
+ onClick = {
+ haptics.performHapticFeedback(HapticFeedbackType.ContextClick)
+ onClick()
+ }
)
.then(
if (isInteractive) {
- Modifier.pointerInput(animationScope) {
+ Modifier.pointerInput(scope) {
val progressAnimationSpec = spring(0.5f, 300f, 0.001f)
val offsetAnimationSpec = spring(1f, 300f, Offset.VisibilityThreshold)
val onDragStop: () -> Unit = {
- animationScope.launch {
+ scope.launch {
+ launch { haptics.performHapticFeedback(HapticFeedbackType.Reject) }
launch { progressAnimation.animateTo(0f, progressAnimationSpec) }
- launch { offsetAnimation.animateTo(Offset.Zero, offsetAnimationSpec) }
+ launch {
+ offsetAnimation.animateTo(
+ Offset.Zero,
+ offsetAnimationSpec
+ )
+ }
}
}
inspectDragGestures(
onDragStart = { down ->
pressStartPosition = down.position
- animationScope.launch {
- launch { progressAnimation.animateTo(1f, progressAnimationSpec) }
+ scope.launch {
+ launch { haptics.performHapticFeedback(HapticFeedbackType.SegmentFrequentTick) }
+ launch {
+ progressAnimation.animateTo(
+ 1f,
+ progressAnimationSpec
+ )
+ }
launch { offsetAnimation.snapTo(Offset.Zero) }
}
},
- onDragEnd = { onDragStop() },
+ onDragEnd = {
+ onDragStop()
+ },
onDragCancel = onDragStop
) { _, dragAmount ->
- animationScope.launch {
+ scope.launch {
+ if (dragAmount.getDistanceSquared() > 350) haptics.performHapticFeedback(
+ HapticFeedbackType.SegmentFrequentTick
+ )
offsetAnimation.snapTo(offsetAnimation.value + dragAmount)
}
}
@@ -274,6 +302,7 @@ half4 main(float2 coord) {
isPressed = false
},
onTap = {
+ haptics.performHapticFeedback(HapticFeedbackType.ContextClick)
onClick()
}
)
diff --git a/android/app/src/main/java/me/kavishdevar/librepods/composables/StyledDropdown.kt b/android/app/src/main/java/me/kavishdevar/librepods/presentation/components/StyledDropdown.kt
similarity index 91%
rename from android/app/src/main/java/me/kavishdevar/librepods/composables/StyledDropdown.kt
rename to android/app/src/main/java/me/kavishdevar/librepods/presentation/components/StyledDropdown.kt
index 394c155..4446f08 100644
--- a/android/app/src/main/java/me/kavishdevar/librepods/composables/StyledDropdown.kt
+++ b/android/app/src/main/java/me/kavishdevar/librepods/presentation/components/StyledDropdown.kt
@@ -16,7 +16,7 @@
along with this program. If not, see .
*/
-package me.kavishdevar.librepods.composables
+package me.kavishdevar.librepods.presentation.components
import android.annotation.SuppressLint
import androidx.compose.animation.AnimatedVisibility
@@ -49,14 +49,17 @@ import androidx.compose.runtime.LaunchedEffect
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
import androidx.compose.ui.draw.clip
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.layout.onGloballyPositioned
+import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.Font
import androidx.compose.ui.text.font.FontFamily
@@ -71,6 +74,7 @@ import dev.chrisbanes.haze.HazeTint
import dev.chrisbanes.haze.hazeEffect
import dev.chrisbanes.haze.materials.CupertinoMaterials
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
+import kotlinx.coroutines.launch
import me.kavishdevar.librepods.R
@ExperimentalHazeMaterialsApi
@@ -110,6 +114,9 @@ fun StyledDropdown(
var hoveredIndex by remember { mutableStateOf(null) }
val itemHeight = 48.dp
+ val scope = rememberCoroutineScope()
+ val haptics = LocalHapticFeedback.current
+
var popupSize by remember { mutableStateOf(IntSize(0, 0)) }
var lastDragPosition by remember { mutableStateOf(null) }
@@ -132,7 +139,12 @@ fun StyledDropdown(
},
onDrag = { change, _ ->
val y = change.position.y
- hoveredIndex = (y / itemHeight.toPx()).toInt()
+ val newHoveredIndex = (y / itemHeight.toPx()).toInt()
+ if (newHoveredIndex != hoveredIndex) {
+ scope.launch { haptics.performHapticFeedback(
+ HapticFeedbackType.SegmentTick) }
+ }
+ hoveredIndex = newHoveredIndex
lastDragPosition = change.position
},
onDragEnd = {
@@ -144,6 +156,8 @@ fun StyledDropdown(
if (withinBounds) {
hoveredIndex?.let { idx ->
if (idx in options.indices) {
+ scope.launch { haptics.performHapticFeedback(
+ HapticFeedbackType.GestureEnd) }
onOptionSelected(options[idx])
}
}
@@ -174,6 +188,7 @@ fun StyledDropdown(
interactionSource = remember { MutableInteractionSource() },
indication = null
) {
+ scope.launch { haptics.performHapticFeedback(HapticFeedbackType.ContextClick) }
onOptionSelected(text)
onDismissRequest()
}
diff --git a/android/app/src/main/java/me/kavishdevar/librepods/composables/StyledIconButton.kt b/android/app/src/main/java/me/kavishdevar/librepods/presentation/components/StyledIconButton.kt
similarity index 86%
rename from android/app/src/main/java/me/kavishdevar/librepods/composables/StyledIconButton.kt
rename to android/app/src/main/java/me/kavishdevar/librepods/presentation/components/StyledIconButton.kt
index 9a12561..baed724 100644
--- a/android/app/src/main/java/me/kavishdevar/librepods/composables/StyledIconButton.kt
+++ b/android/app/src/main/java/me/kavishdevar/librepods/presentation/components/StyledIconButton.kt
@@ -16,7 +16,7 @@
along with this program. If not, see .
*/
-package me.kavishdevar.librepods.composables
+package me.kavishdevar.librepods.presentation.components
import android.graphics.RuntimeShader
import android.os.Build
@@ -50,7 +50,9 @@ import androidx.compose.ui.graphics.layer.CompositingStrategy
import androidx.compose.ui.graphics.layer.drawLayer
import androidx.compose.ui.graphics.rememberGraphicsLayer
import androidx.compose.ui.graphics.toArgb
+import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.input.pointer.pointerInput
+import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.Font
import androidx.compose.ui.text.font.FontFamily
@@ -83,8 +85,9 @@ fun StyledIconButton(
backdrop: LayerBackdrop = rememberLayerBackdrop(),
onClick: () -> Unit
) {
+ val haptics = LocalHapticFeedback.current
val darkMode = isSystemInDarkTheme()
- val animationScope = rememberCoroutineScope()
+ val scope = rememberCoroutineScope()
val progressAnimationSpec = spring(0.5f, 300f, 0.001f)
val offsetAnimationSpec = spring(1f, 300f, Offset.VisibilityThreshold)
val progressAnimation = remember { Animatable(0f) }
@@ -116,7 +119,10 @@ half4 main(float2 coord) {
}
val isDarkTheme = isSystemInDarkTheme()
TextButton(
- onClick = onClick,
+ onClick = {
+ scope.launch { haptics.performHapticFeedback(HapticFeedbackType.ContextClick) }
+ onClick()
+ },
shape = RoundedCornerShape(56.dp),
modifier = modifier
.padding(horizontal = 12.dp)
@@ -147,12 +153,12 @@ half4 main(float2 coord) {
val offsetAngle = atan2(offset.y, offset.x)
scaleX =
scale +
- maxDragScale * abs(cos(offsetAngle) * offset.x / size.maxDimension) *
- (width / height).fastCoerceAtMost(1f)
+ maxDragScale * abs(cos(offsetAngle) * offset.x / size.maxDimension) *
+ (width / height).fastCoerceAtMost(1f)
scaleY =
scale +
- maxDragScale * abs(sin(offsetAngle) * offset.y / size.maxDimension) *
- (height / width).fastCoerceAtMost(1f)
+ maxDragScale * abs(sin(offsetAngle) * offset.y / size.maxDimension) *
+ (height / width).fastCoerceAtMost(1f)
},
onDrawSurface = {
val progress = progressAnimation.value.coerceIn(0f, 1f)
@@ -182,7 +188,12 @@ half4 main(float2 coord) {
drawLayer(innerShadowLayer)
drawRect(
- (if (isDarkTheme) Color(0xFFAFAFAF) else Color.White).copy(progress.coerceIn(0.15f, 0.35f))
+ (if (isDarkTheme) Color(0xFFAFAFAF) else Color.White).copy(
+ progress.coerceIn(
+ 0.15f,
+ 0.35f
+ )
+ )
)
},
onDrawFront = {
@@ -196,7 +207,10 @@ half4 main(float2 coord) {
interactiveHighlightShader.apply {
val offset = pressStartPosition + offsetAnimation.value
setFloatUniform("size", size.width, size.height)
- setColorUniform("color", Color.White.copy(0.15f * progress).toArgb())
+ setColorUniform(
+ "color",
+ Color.White.copy(0.15f * progress).toArgb()
+ )
setFloatUniform("radius", size.maxDimension)
setFloatUniform(
"offset",
@@ -225,9 +239,10 @@ half4 main(float2 coord) {
)
},
)
- .pointerInput(animationScope) {
+ .pointerInput(scope) {
val onDragStop: () -> Unit = {
- animationScope.launch {
+ scope.launch {
+ launch { haptics.performHapticFeedback(HapticFeedbackType.Reject) }
launch { progressAnimation.animateTo(0f, progressAnimationSpec) }
launch { offsetAnimation.animateTo(Offset.Zero, offsetAnimationSpec) }
}
@@ -235,7 +250,8 @@ half4 main(float2 coord) {
inspectDragGestures(
onDragStart = { down ->
pressStartPosition = down.position
- animationScope.launch {
+ scope.launch {
+ launch { haptics.performHapticFeedback(HapticFeedbackType.SegmentFrequentTick) }
launch { progressAnimation.animateTo(1f, progressAnimationSpec) }
launch { offsetAnimation.snapTo(Offset.Zero) }
}
@@ -243,7 +259,10 @@ half4 main(float2 coord) {
onDragEnd = { onDragStop() },
onDragCancel = onDragStop
) { _, dragAmount ->
- animationScope.launch {
+ scope.launch {
+ if (dragAmount.getDistanceSquared() > 350) haptics.performHapticFeedback(
+ HapticFeedbackType.SegmentFrequentTick
+ )
offsetAnimation.snapTo(offsetAnimation.value + dragAmount)
}
}
diff --git a/android/app/src/main/java/me/kavishdevar/librepods/composables/StyledScaffold.kt b/android/app/src/main/java/me/kavishdevar/librepods/presentation/components/StyledScaffold.kt
similarity index 99%
rename from android/app/src/main/java/me/kavishdevar/librepods/composables/StyledScaffold.kt
rename to android/app/src/main/java/me/kavishdevar/librepods/presentation/components/StyledScaffold.kt
index f359853..1c79f25 100644
--- a/android/app/src/main/java/me/kavishdevar/librepods/composables/StyledScaffold.kt
+++ b/android/app/src/main/java/me/kavishdevar/librepods/presentation/components/StyledScaffold.kt
@@ -16,7 +16,7 @@
along with this program. If not, see .
*/
-package me.kavishdevar.librepods.composables
+package me.kavishdevar.librepods.presentation.components
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Box
diff --git a/android/app/src/main/java/me/kavishdevar/librepods/composables/StyledSelectList.kt b/android/app/src/main/java/me/kavishdevar/librepods/presentation/components/StyledSelectList.kt
similarity index 92%
rename from android/app/src/main/java/me/kavishdevar/librepods/composables/StyledSelectList.kt
rename to android/app/src/main/java/me/kavishdevar/librepods/presentation/components/StyledSelectList.kt
index 90af965..1b1dd1b 100644
--- a/android/app/src/main/java/me/kavishdevar/librepods/composables/StyledSelectList.kt
+++ b/android/app/src/main/java/me/kavishdevar/librepods/presentation/components/StyledSelectList.kt
@@ -16,7 +16,7 @@
along with this program. If not, see .
*/
-package me.kavishdevar.librepods.composables
+package me.kavishdevar.librepods.presentation.components
import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.animateFloatAsState
@@ -39,11 +39,14 @@ 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
import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.input.pointer.pointerInput
+import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.Font
@@ -71,6 +74,9 @@ 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(
modifier = modifier
.fillMaxWidth()
@@ -104,6 +110,11 @@ fun StyledSelectList(
if (isDarkTheme) Color(0x40888888) else Color(0x40D9D9D9)
tryAwaitRelease()
itemBackgroundColor = backgroundColor
+ }
+ },
+ onTap = {
+ if (item.enabled) {
+ haptics.performHapticFeedback(HapticFeedbackType.ContextClick)
item.onClick()
}
}
diff --git a/android/app/src/main/java/me/kavishdevar/librepods/composables/StyledSlider.kt b/android/app/src/main/java/me/kavishdevar/librepods/presentation/components/StyledSlider.kt
similarity index 96%
rename from android/app/src/main/java/me/kavishdevar/librepods/composables/StyledSlider.kt
rename to android/app/src/main/java/me/kavishdevar/librepods/presentation/components/StyledSlider.kt
index 78829e2..58c9b57 100644
--- a/android/app/src/main/java/me/kavishdevar/librepods/composables/StyledSlider.kt
+++ b/android/app/src/main/java/me/kavishdevar/librepods/presentation/components/StyledSlider.kt
@@ -16,7 +16,7 @@
along with this program. If not, see .
*/
-package me.kavishdevar.librepods.composables
+package me.kavishdevar.librepods.presentation.components
import android.annotation.SuppressLint
import android.content.res.Configuration
@@ -56,6 +56,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.graphicsLayer
+import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.input.pointer.util.VelocityTracker
import androidx.compose.ui.input.pointer.util.addPointerInputChange
@@ -64,6 +65,7 @@ import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.layout.positionInParent
import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.Font
import androidx.compose.ui.text.font.FontFamily
@@ -246,6 +248,8 @@ fun StyledSlider(
val startIconWidthState = remember { mutableFloatStateOf(0f) }
val endIconWidthState = remember { mutableFloatStateOf(0f) }
val density = LocalDensity.current
+ val haptics = LocalHapticFeedback.current
+ var lastDragValue by remember { mutableFloatStateOf(value) }
val momentumAnimation = rememberMomentumAnimation(maxScale = 1.5f)
@@ -449,10 +453,17 @@ fun StyledSlider(
valueRange.start,
valueRange.endInclusive
)
+ snapPoints.forEach { snap ->
+ if ((lastDragValue < snap && targetValue >= snap) ||
+ (snap in targetValue...
*/
-package me.kavishdevar.librepods.composables
+package me.kavishdevar.librepods.presentation.components
import android.content.res.Configuration
import androidx.compose.animation.animateColorAsState
@@ -58,8 +58,10 @@ import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.graphics.layer.CompositingStrategy
import androidx.compose.ui.graphics.layer.drawLayer
import androidx.compose.ui.graphics.rememberGraphicsLayer
+import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.fastCoerceIn
@@ -73,6 +75,7 @@ import com.kyant.backdrop.highlight.Highlight
import com.kyant.backdrop.shadow.Shadow
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.launch
+import kotlin.math.abs
@Composable
fun StyledSwitch(
@@ -81,9 +84,12 @@ fun StyledSwitch(
enabled: Boolean = true,
) {
val isDarkTheme = isSystemInDarkTheme()
+ val haptics = LocalHapticFeedback.current
val onColor = if (enabled) Color(0xFF34C759) else if (isDarkTheme) Color(0xFF5B5B5E) else Color(0xFFD1D1D6)
- val offColor = if (enabled) if (isDarkTheme) Color(0xFF5B5B5E) else Color(0xFFD1D1D6) else if (isDarkTheme) Color(0xFF5B5B5E) else Color(0xFFD1D1D6)
+ val offColor = if (enabled) if (isDarkTheme) Color(0xFF5B5B5E) else Color(0xFFD1D1D6) else if (isDarkTheme) Color(
+ 0x805B5B5E
+ ) else Color(0xFFD1D1D6)
val trackWidth = 64.dp
val trackHeight = 28.dp
@@ -98,7 +104,7 @@ fun StyledSwitch(
val animatedFraction = remember { Animatable(fraction) }
val trackWidthPx = remember { mutableFloatStateOf(0f) }
val density = LocalDensity.current
- val animationScope = rememberCoroutineScope()
+ val scope = rememberCoroutineScope()
val progressAnimationSpec = spring(0.5f, 300f, 0.001f)
val progressAnimation = remember { Animatable(0f) }
val innerShadowLayer = rememberGraphicsLayer().apply {
@@ -111,6 +117,11 @@ fun StyledSwitch(
val isFirstComposition = remember { mutableStateOf(true) }
LaunchedEffect(checked) {
if (!isFirstComposition.value) {
+ if (checked) {
+ haptics.performHapticFeedback(HapticFeedbackType.ToggleOn)
+ } else {
+ haptics.performHapticFeedback(HapticFeedbackType.ToggleOff)
+ }
coroutineScope {
launch {
val targetFrac = if (checked) 1f else 0f
@@ -150,27 +161,31 @@ fun StyledSwitch(
.then(if (enabled) Modifier.draggable(
rememberDraggableState { delta ->
if (trackWidthPx.floatValue > 0f) {
+ val oldFraction = animatedFraction.value
val newFraction = (animatedFraction.value + delta / trackWidthPx.floatValue).fastCoerceIn(-0.3f, 1.3f)
- animationScope.launch {
+ scope.launch {
animatedFraction.snapTo(newFraction)
}
- totalDrag.floatValue += kotlin.math.abs(delta)
+ totalDrag.floatValue += abs(delta)
val newChecked = newFraction >= 0.5f
if (newChecked != checked) {
onCheckedChange(newChecked)
}
+ if ((oldFraction < 0.5f && newFraction >= 0.5f) || (oldFraction >= 0.5f && newFraction < 0.5f)) {
+ haptics.performHapticFeedback(HapticFeedbackType.SegmentTick)
+ }
}
},
Orientation.Horizontal,
startDragImmediately = true,
onDragStarted = {
totalDrag.floatValue = 0f
- animationScope.launch {
+ scope.launch {
progressAnimation.animateTo(1f, progressAnimationSpec)
}
},
onDragStopped = {
- animationScope.launch {
+ scope.launch {
if (totalDrag.floatValue < tapThreshold) {
val newChecked = !checked
onCheckedChange(newChecked)
diff --git a/android/app/src/main/java/me/kavishdevar/librepods/composables/StyledToggle.kt b/android/app/src/main/java/me/kavishdevar/librepods/presentation/components/StyledToggle.kt
similarity index 86%
rename from android/app/src/main/java/me/kavishdevar/librepods/composables/StyledToggle.kt
rename to android/app/src/main/java/me/kavishdevar/librepods/presentation/components/StyledToggle.kt
index b7453ae..4028dc4 100644
--- a/android/app/src/main/java/me/kavishdevar/librepods/composables/StyledToggle.kt
+++ b/android/app/src/main/java/me/kavishdevar/librepods/presentation/components/StyledToggle.kt
@@ -18,7 +18,7 @@
@file:OptIn(ExperimentalEncodingApi::class)
-package me.kavishdevar.librepods.composables
+package me.kavishdevar.librepods.presentation.components
import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.tween
@@ -40,12 +40,15 @@ 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.rememberUpdatedState
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.input.pointer.pointerInput
+import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.Font
import androidx.compose.ui.text.font.FontFamily
@@ -53,6 +56,7 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
+import kotlinx.coroutines.launch
import me.kavishdevar.librepods.R
import kotlin.io.encoding.ExperimentalEncodingApi
@@ -71,6 +75,9 @@ fun StyledToggle(
val isDarkTheme = isSystemInDarkTheme()
val textColor = if (isDarkTheme) Color.White else Color.Black
+ val haptics = LocalHapticFeedback.current
+ val scope = rememberCoroutineScope()
+
var backgroundColor by remember {
mutableStateOf(
if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFFFFFFF)
@@ -90,7 +97,8 @@ fun StyledToggle(
style = TextStyle(
fontSize = 14.sp,
fontWeight = FontWeight.Bold,
- color = textColor.copy(alpha = 0.6f)
+ color = textColor.copy(alpha = 0.6f),
+ fontFamily = FontFamily(Font(R.font.sf_pro))
),
modifier = Modifier.padding(
start = 16.dp,
@@ -108,14 +116,17 @@ fun StyledToggle(
.pointerInput(Unit) {
detectTapGestures(
onPress = {
- backgroundColor =
- if (isDarkTheme) Color(0x40888888) else Color(0x40D9D9D9)
- tryAwaitRelease()
- backgroundColor =
- if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFFFFFFF)
+ if (enabled) {
+ backgroundColor =
+ if (isDarkTheme) Color(0x40888888) else Color(0x40D9D9D9)
+ tryAwaitRelease()
+ backgroundColor =
+ if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFFFFFFF)
+ }
},
onTap = {
if (enabled) {
+ scope.launch { haptics.performHapticFeedback(if (!currentChecked) HapticFeedbackType.ToggleOn else HapticFeedbackType.ToggleOff) }
onCheckedChange(!currentChecked)
}
}
@@ -145,6 +156,7 @@ fun StyledToggle(
enabled = enabled,
onCheckedChange = {
if (enabled) {
+ scope.launch { haptics.performHapticFeedback(if (it) HapticFeedbackType.ToggleOn else HapticFeedbackType.ToggleOff) }
onCheckedChange(it)
}
}
@@ -200,6 +212,7 @@ fun StyledToggle(
interactionSource = remember { MutableInteractionSource() }
) {
if (enabled) {
+ scope.launch { haptics.performHapticFeedback(if (!currentChecked) HapticFeedbackType.ToggleOn else HapticFeedbackType.ToggleOff) }
onCheckedChange(!currentChecked)
}
},
diff --git a/android/app/src/main/java/me/kavishdevar/librepods/composables/VerticalVolumeSlider.kt b/android/app/src/main/java/me/kavishdevar/librepods/presentation/components/VerticalVolumeSlider.kt
similarity index 99%
rename from android/app/src/main/java/me/kavishdevar/librepods/composables/VerticalVolumeSlider.kt
rename to android/app/src/main/java/me/kavishdevar/librepods/presentation/components/VerticalVolumeSlider.kt
index 8c82da4..37bddd8 100644
--- a/android/app/src/main/java/me/kavishdevar/librepods/composables/VerticalVolumeSlider.kt
+++ b/android/app/src/main/java/me/kavishdevar/librepods/presentation/components/VerticalVolumeSlider.kt
@@ -16,7 +16,7 @@
along with this program. If not, see .
*/
-package me.kavishdevar.librepods.composables
+package me.kavishdevar.librepods.presentation.components
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.animateFloatAsState
diff --git a/android/app/src/main/java/me/kavishdevar/librepods/utils/IslandWindow.kt b/android/app/src/main/java/me/kavishdevar/librepods/presentation/overlays/IslandWindow.kt
similarity index 99%
rename from android/app/src/main/java/me/kavishdevar/librepods/utils/IslandWindow.kt
rename to android/app/src/main/java/me/kavishdevar/librepods/presentation/overlays/IslandWindow.kt
index c93ca15..90ad166 100644
--- a/android/app/src/main/java/me/kavishdevar/librepods/utils/IslandWindow.kt
+++ b/android/app/src/main/java/me/kavishdevar/librepods/presentation/overlays/IslandWindow.kt
@@ -18,7 +18,7 @@
@file:OptIn(ExperimentalEncodingApi::class)
-package me.kavishdevar.librepods.utils
+package me.kavishdevar.librepods.presentation.overlays
import android.animation.Animator
import android.animation.AnimatorListenerAdapter
@@ -58,10 +58,10 @@ import androidx.dynamicanimation.animation.DynamicAnimation
import androidx.dynamicanimation.animation.SpringAnimation
import androidx.dynamicanimation.animation.SpringForce
import me.kavishdevar.librepods.R
-import me.kavishdevar.librepods.constants.AirPodsNotifications
-import me.kavishdevar.librepods.constants.Battery
-import me.kavishdevar.librepods.constants.BatteryComponent
-import me.kavishdevar.librepods.constants.BatteryStatus
+import me.kavishdevar.librepods.data.AirPodsNotifications
+import me.kavishdevar.librepods.data.Battery
+import me.kavishdevar.librepods.data.BatteryComponent
+import me.kavishdevar.librepods.data.BatteryStatus
import me.kavishdevar.librepods.services.ServiceManager
import kotlin.io.encoding.ExperimentalEncodingApi
import kotlin.math.abs
diff --git a/android/app/src/main/java/me/kavishdevar/librepods/utils/PopupWindow.kt b/android/app/src/main/java/me/kavishdevar/librepods/presentation/overlays/PopupWindow.kt
similarity index 97%
rename from android/app/src/main/java/me/kavishdevar/librepods/utils/PopupWindow.kt
rename to android/app/src/main/java/me/kavishdevar/librepods/presentation/overlays/PopupWindow.kt
index e93e709..45bdf9f 100644
--- a/android/app/src/main/java/me/kavishdevar/librepods/utils/PopupWindow.kt
+++ b/android/app/src/main/java/me/kavishdevar/librepods/presentation/overlays/PopupWindow.kt
@@ -17,7 +17,7 @@
*/
-package me.kavishdevar.librepods.utils
+package me.kavishdevar.librepods.presentation.overlays
import android.animation.Animator
import android.animation.AnimatorListenerAdapter
@@ -45,10 +45,10 @@ import android.widget.LinearLayout
import android.widget.TextView
import android.widget.VideoView
import me.kavishdevar.librepods.R
-import me.kavishdevar.librepods.constants.AirPodsNotifications
-import me.kavishdevar.librepods.constants.Battery
-import me.kavishdevar.librepods.constants.BatteryComponent
-import me.kavishdevar.librepods.constants.BatteryStatus
+import me.kavishdevar.librepods.data.AirPodsNotifications
+import me.kavishdevar.librepods.data.Battery
+import me.kavishdevar.librepods.data.BatteryComponent
+import me.kavishdevar.librepods.data.BatteryStatus
@SuppressLint("InflateParams", "ClickableViewAccessibility")
class PopupWindow(
diff --git a/android/app/src/main/java/me/kavishdevar/librepods/screens/AccessibilitySettingsScreen.kt b/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/AccessibilitySettingsScreen.kt
similarity index 80%
rename from android/app/src/main/java/me/kavishdevar/librepods/screens/AccessibilitySettingsScreen.kt
rename to android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/AccessibilitySettingsScreen.kt
index 176ed77..98adcb1 100644
--- a/android/app/src/main/java/me/kavishdevar/librepods/screens/AccessibilitySettingsScreen.kt
+++ b/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/AccessibilitySettingsScreen.kt
@@ -16,7 +16,7 @@
along with this program. If not, see .
*/
-package me.kavishdevar.librepods.screens
+package me.kavishdevar.librepods.presentation.screens
// import me.kavishdevar.librepods.utils.RadareOffsetFinder
import android.annotation.SuppressLint
@@ -44,16 +44,20 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableLongStateOf
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
import androidx.compose.ui.draw.clip
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.hapticfeedback.HapticFeedbackType
+import androidx.compose.ui.input.pointer.PointerEventPass
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.layout.positionInParent
import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.Font
@@ -67,17 +71,18 @@ import com.kyant.backdrop.backdrops.rememberLayerBackdrop
import dev.chrisbanes.haze.HazeState
import dev.chrisbanes.haze.hazeSource
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
-import me.kavishdevar.librepods.BuildConfig
+import kotlinx.coroutines.launch
import me.kavishdevar.librepods.R
-import me.kavishdevar.librepods.composables.NavigationButton
-import me.kavishdevar.librepods.composables.StyledDropdown
-import me.kavishdevar.librepods.composables.StyledScaffold
-import me.kavishdevar.librepods.composables.StyledSlider
-import me.kavishdevar.librepods.composables.StyledToggle
-import me.kavishdevar.librepods.utils.AACPManager
-import me.kavishdevar.librepods.utils.ATTHandles
-import me.kavishdevar.librepods.utils.Capability
-import me.kavishdevar.librepods.viewmodel.AirPodsViewModel
+import me.kavishdevar.librepods.bluetooth.AACPManager
+import me.kavishdevar.librepods.bluetooth.ATTHandles
+import me.kavishdevar.librepods.data.Capability
+import me.kavishdevar.librepods.presentation.components.NavigationButton
+import me.kavishdevar.librepods.presentation.components.StyledButton
+import me.kavishdevar.librepods.presentation.components.StyledDropdown
+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.AirPodsViewModel
import kotlin.io.encoding.ExperimentalEncodingApi
//private var phoneMediaDebounceJob: Job? = null
@@ -96,7 +101,6 @@ fun AccessibilitySettingsScreen(viewModel: AirPodsViewModel, navController: NavC
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()
StyledScaffold(
@@ -113,6 +117,28 @@ fun AccessibilitySettingsScreen(viewModel: AirPodsViewModel, navController: NavC
) {
Spacer(modifier = Modifier.height(topPadding))
+ if (!state.isPremium) {
+ StyledButton(
+ onClick = {
+ navController.navigate("purchase_screen")
+ },
+ backdrop = rememberLayerBackdrop(),
+ modifier = Modifier.fillMaxWidth(),
+ maxScale = 0.05f,
+ tint = 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
+ ),
+ )
+ }
+ }
+
// val phoneMediaEQ = remember { mutableStateOf(FloatArray(8) { 0.5f }) }
// val phoneEQEnabled = remember { mutableStateOf(false) }
// val mediaEQEnabled = remember { mutableStateOf(false) }
@@ -179,66 +205,100 @@ fun AccessibilitySettingsScreen(viewModel: AirPodsViewModel, navController: NavC
// }
// }
// }
+ 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
+ )
+ ) {
+ DropdownMenuComponent(
+ label = stringResource(R.string.press_speed),
+ description = stringResource(R.string.press_speed_description),
+ options = pressSpeedOptions.values.toList(),
+ selectedOption = selectedPressSpeed ?: stringResource(R.string.default_option),
+ onOptionSelected = { newValue ->
+ selectedPressSpeed = newValue
+ viewModel.setControlCommandByte(
+ identifier = AACPManager.Companion.ControlCommandIdentifiers.DOUBLE_CLICK_INTERVAL,
+ value = pressSpeedOptions.filterValues { it == newValue }.keys.firstOrNull()
+ ?: 0.toByte()
+ )
+ },
+ textColor = textColor,
+ hazeState = hazeState,
+ independent = true
+ )
+ }
- DropdownMenuComponent(
- label = stringResource(R.string.press_speed),
- description = stringResource(R.string.press_speed_description),
- options = pressSpeedOptions.values.toList(),
- selectedOption = selectedPressSpeed?: stringResource(R.string.default_option),
- onOptionSelected = { newValue ->
- selectedPressSpeed = newValue
- viewModel.setControlCommandByte(
- identifier = AACPManager.Companion.ControlCommandIdentifiers.DOUBLE_CLICK_INTERVAL,
- value = pressSpeedOptions.filterValues { it == newValue }.keys.firstOrNull()
- ?: 0.toByte()
- )
- },
- textColor = textColor,
- hazeState = hazeState,
- independent = true
- )
-
- DropdownMenuComponent(
- label = stringResource(R.string.press_and_hold_duration),
- description = stringResource(R.string.press_and_hold_duration_description),
- options = pressAndHoldDurationOptions.values.toList(),
- selectedOption = selectedPressAndHoldDuration?: stringResource(R.string.default_option),
- onOptionSelected = { newValue ->
- selectedPressAndHoldDuration = newValue
- viewModel.setControlCommandByte(
- identifier = AACPManager.Companion.ControlCommandIdentifiers.CLICK_HOLD_INTERVAL,
- value = pressAndHoldDurationOptions.filterValues { it == newValue }.keys.firstOrNull()
- ?: 0.toByte()
- )
- },
- textColor = textColor,
- hazeState = hazeState,
- independent = true
- )
-
+ 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
+ )
+ ) {
+ DropdownMenuComponent(
+ label = stringResource(R.string.press_and_hold_duration),
+ description = stringResource(R.string.press_and_hold_duration_description),
+ options = pressAndHoldDurationOptions.values.toList(),
+ selectedOption = selectedPressAndHoldDuration
+ ?: stringResource(R.string.default_option),
+ onOptionSelected = { newValue ->
+ selectedPressAndHoldDuration = newValue
+ viewModel.setControlCommandByte(
+ identifier = AACPManager.Companion.ControlCommandIdentifiers.CLICK_HOLD_INTERVAL,
+ value = pressAndHoldDurationOptions.filterValues { it == newValue }.keys.firstOrNull()
+ ?: 0.toByte()
+ )
+ },
+ textColor = textColor,
+ hazeState = hazeState,
+ independent = true
+ )
+ }
StyledToggle(
title = stringResource(R.string.noise_control),
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) }
+ onCheckedChange = { viewModel.setControlCommandBoolean(AACPManager.Companion.ControlCommandIdentifiers.ONE_BUD_ANC_MODE, it) },
+ enabled = state.isPremium
)
- if (state.capabilities.contains(Capability.LOUD_SOUND_REDUCTION) && BuildConfig.FLAVOR == "xposed") {
+ if (state.capabilities.contains(Capability.LOUD_SOUND_REDUCTION) && state.vendorIdHook) {
StyledToggle(
label = stringResource(R.string.loud_sound_reduction),
description = stringResource(R.string.loud_sound_reduction_description),
- checked = viewModel.getATTCharacteristicValue(ATTHandles.LOUD_SOUND_REDUCTION)?.get(0) == 1.toByte(),
- onCheckedChange = { viewModel.setATTCharacteristicValue(ATTHandles.LOUD_SOUND_REDUCTION, if (it) byteArrayOf(0x01) else byteArrayOf(0x00)) }
+ checked = state.loudSoundReductionEnabled,
+ onCheckedChange = { viewModel.setATTCharacteristicValue(ATTHandles.LOUD_SOUND_REDUCTION, if (it) byteArrayOf(0x01) else byteArrayOf(0x00)) },
+ enabled = state.isPremium
)
}
- if (!hearingAidEnabled && BuildConfig.FLAVOR == "xposed") {
+ if (!hearingAidEnabled && state.vendorIdHook) {
NavigationButton(
to = "transparency_customization",
name = stringResource(R.string.customize_transparency_mode),
- navController = navController
+ navController = navController,
+ enabled = state.isPremium
)
}
@@ -254,7 +314,8 @@ fun AccessibilitySettingsScreen(viewModel: AirPodsViewModel, navController: NavC
snapPoints = listOf(75f),
startIcon = "\uDBC0\uDEA1",
endIcon = "\uDBC0\uDEA9",
- independent = true
+ independent = true,
+ enabled = state.isPremium
)
if (state.capabilities.contains(Capability.SWIPE_FOR_VOLUME)) {
@@ -263,26 +324,44 @@ fun AccessibilitySettingsScreen(viewModel: AirPodsViewModel, navController: NavC
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
)
- DropdownMenuComponent(
- label = stringResource(R.string.volume_swipe_speed),
- description = stringResource(R.string.volume_swipe_speed_description),
- options = volumeSwipeSpeedOptions.values.toList(),
- selectedOption = selectedVolumeSwipeSpeed?: stringResource(R.string.default_option),
- onOptionSelected = { newValue ->
- selectedVolumeSwipeSpeed = newValue
- viewModel.setControlCommandByte(
- identifier = AACPManager.Companion.ControlCommandIdentifiers.VOLUME_SWIPE_INTERVAL,
- value = volumeSwipeSpeedOptions.filterValues { it == newValue }.keys.firstOrNull()
- ?: 1.toByte()
- )
- },
- textColor = textColor,
- hazeState = hazeState,
- independent = true
- )
+ 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
+ )
+ ) {
+ DropdownMenuComponent(
+ label = stringResource(R.string.volume_swipe_speed),
+ description = stringResource(R.string.volume_swipe_speed_description),
+ options = volumeSwipeSpeedOptions.values.toList(),
+ selectedOption = selectedVolumeSwipeSpeed
+ ?: stringResource(R.string.default_option),
+ onOptionSelected = { newValue ->
+ selectedVolumeSwipeSpeed = newValue
+ viewModel.setControlCommandByte(
+ identifier = AACPManager.Companion.ControlCommandIdentifiers.VOLUME_SWIPE_INTERVAL,
+ value = volumeSwipeSpeedOptions.filterValues { it == newValue }.keys.firstOrNull()
+ ?: 1.toByte()
+ )
+ },
+ textColor = textColor,
+ hazeState = hazeState,
+ independent = true
+ )
+ }
}
// if (!hearingAidEnabled.value&& BuildConfig.FLAVOR == "xposed") {
@@ -507,7 +586,6 @@ fun AccessibilitySettingsScreen(viewModel: AirPodsViewModel, navController: NavC
// }
// }
// }
-// }
Spacer(modifier = Modifier.height(bottomPadding))
}
}
@@ -534,6 +612,9 @@ private fun DropdownMenuComponent(
var lastDismissTime by remember { mutableLongStateOf(0L) }
var parentHoveredIndex by remember { mutableStateOf(null) }
var parentDragActive by remember { mutableStateOf(false) }
+ var previousIdx by remember { mutableStateOf(null) }
+ val haptics = LocalHapticFeedback.current
+ val scope = rememberCoroutineScope()
Column(modifier = Modifier.fillMaxWidth()){
Column(
@@ -593,7 +674,11 @@ private fun DropdownMenuComponent(
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
@@ -604,6 +689,9 @@ private fun DropdownMenuComponent(
lastDismissTime = System.currentTimeMillis()
}
}
+ if (parentHoveredIndex != null && parentHoveredIndex in options.indices) {
+ scope.launch { haptics.performHapticFeedback(HapticFeedbackType.GestureEnd) }
+ }
parentHoveredIndex = null
},
onDragCancel = {
diff --git a/android/app/src/main/java/me/kavishdevar/librepods/screens/AdaptiveStrengthScreen.kt b/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/AdaptiveStrengthScreen.kt
similarity index 65%
rename from android/app/src/main/java/me/kavishdevar/librepods/screens/AdaptiveStrengthScreen.kt
rename to android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/AdaptiveStrengthScreen.kt
index 677f1b6..9a2f9ac 100644
--- a/android/app/src/main/java/me/kavishdevar/librepods/screens/AdaptiveStrengthScreen.kt
+++ b/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/AdaptiveStrengthScreen.kt
@@ -16,14 +16,17 @@
along with this program. If not, see .
*/
-package me.kavishdevar.librepods.screens
+package me.kavishdevar.librepods.presentation.screens
+import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Arrangement
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
@@ -33,21 +36,29 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
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 kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import me.kavishdevar.librepods.R
-import me.kavishdevar.librepods.composables.StyledScaffold
-import me.kavishdevar.librepods.composables.StyledSlider
-import me.kavishdevar.librepods.utils.AACPManager
-import me.kavishdevar.librepods.viewmodel.AirPodsViewModel
+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.bluetooth.AACPManager
+import me.kavishdevar.librepods.presentation.viewmodel.AirPodsViewModel
@Composable
-fun AdaptiveStrengthScreen(viewModel: AirPodsViewModel) {
+fun AdaptiveStrengthScreen(viewModel: AirPodsViewModel, navController: NavController) {
val state by viewModel.uiState.collectAsState()
val backdrop = rememberLayerBackdrop()
@@ -62,6 +73,27 @@ fun AdaptiveStrengthScreen(viewModel: AirPodsViewModel) {
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
Spacer(modifier = Modifier.height(spacerHeight))
+ if (!state.isPremium) {
+ StyledButton(
+ onClick = {
+ navController.navigate("purchase_screen")
+ },
+ backdrop = rememberLayerBackdrop(),
+ modifier = Modifier.fillMaxWidth(),
+ maxScale = 0.05f,
+ tint = 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
+ ),
+ )
+ }
+ }
val sliderValue = remember {
mutableFloatStateOf(
state.controlStates[AACPManager.Companion.ControlCommandIdentifiers.AUTO_ANC_STRENGTH]?.getOrNull(
@@ -90,7 +122,8 @@ fun AdaptiveStrengthScreen(viewModel: AirPodsViewModel) {
startIcon = "",
endIcon = "",
independent = true,
- description = stringResource(R.string.adaptive_audio_description)
+ description = stringResource(R.string.adaptive_audio_description),
+ enabled = state.isPremium
)
}
}
diff --git a/android/app/src/main/java/me/kavishdevar/librepods/screens/AirPodsSettingsScreen.kt b/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/AirPodsSettingsScreen.kt
similarity index 90%
rename from android/app/src/main/java/me/kavishdevar/librepods/screens/AirPodsSettingsScreen.kt
rename to android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/AirPodsSettingsScreen.kt
index c498313..0c64961 100644
--- a/android/app/src/main/java/me/kavishdevar/librepods/screens/AirPodsSettingsScreen.kt
+++ b/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/AirPodsSettingsScreen.kt
@@ -18,13 +18,12 @@
@file:OptIn(ExperimentalEncodingApi::class)
-package me.kavishdevar.librepods.screens
+package me.kavishdevar.librepods.presentation.screens
// import me.kavishdevar.librepods.utils.RadareOffsetFinder
import android.annotation.SuppressLint
import android.content.Context.MODE_PRIVATE
import android.content.SharedPreferences
-import android.util.Log
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Arrangement
@@ -74,25 +73,25 @@ import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
import kotlinx.coroutines.delay
import me.kavishdevar.librepods.BuildConfig
import me.kavishdevar.librepods.R
-import me.kavishdevar.librepods.composables.AboutCard
-import me.kavishdevar.librepods.composables.AudioSettings
-import me.kavishdevar.librepods.composables.BatteryView
-import me.kavishdevar.librepods.composables.CallControlSettings
-import me.kavishdevar.librepods.composables.ConnectionSettings
-import me.kavishdevar.librepods.composables.HearingHealthSettings
-import me.kavishdevar.librepods.composables.MicrophoneSettings
-import me.kavishdevar.librepods.composables.NavigationButton
-import me.kavishdevar.librepods.composables.NoiseControlSettings
-import me.kavishdevar.librepods.composables.PressAndHoldSettings
-import me.kavishdevar.librepods.composables.StyledButton
-import me.kavishdevar.librepods.composables.StyledIconButton
-import me.kavishdevar.librepods.composables.StyledScaffold
-import me.kavishdevar.librepods.composables.StyledToggle
-import me.kavishdevar.librepods.utils.AACPManager
-import me.kavishdevar.librepods.utils.ATTHandles
-import me.kavishdevar.librepods.utils.AirPodsPro3
-import me.kavishdevar.librepods.utils.Capability
-import me.kavishdevar.librepods.viewmodel.AirPodsViewModel
+import me.kavishdevar.librepods.bluetooth.AACPManager
+import me.kavishdevar.librepods.bluetooth.ATTHandles
+import me.kavishdevar.librepods.data.AirPodsPro3
+import me.kavishdevar.librepods.data.Capability
+import me.kavishdevar.librepods.presentation.components.AboutCard
+import me.kavishdevar.librepods.presentation.components.AudioSettings
+import me.kavishdevar.librepods.presentation.components.BatteryView
+import me.kavishdevar.librepods.presentation.components.CallControlSettings
+import me.kavishdevar.librepods.presentation.components.ConnectionSettings
+import me.kavishdevar.librepods.presentation.components.HearingHealthSettings
+import me.kavishdevar.librepods.presentation.components.MicrophoneSettings
+import me.kavishdevar.librepods.presentation.components.NavigationButton
+import me.kavishdevar.librepods.presentation.components.NoiseControlSettings
+import me.kavishdevar.librepods.presentation.components.PressAndHoldSettings
+import me.kavishdevar.librepods.presentation.components.StyledButton
+import me.kavishdevar.librepods.presentation.components.StyledIconButton
+import me.kavishdevar.librepods.presentation.components.StyledScaffold
+import me.kavishdevar.librepods.presentation.components.StyledToggle
+import me.kavishdevar.librepods.presentation.viewmodel.AirPodsViewModel
import kotlin.io.encoding.ExperimentalEncodingApi
@OptIn(ExperimentalMaterial3Api::class, ExperimentalHazeMaterialsApi::class)
@@ -162,17 +161,14 @@ fun AirPodsSettingsScreen(viewModel: AirPodsViewModel, navController: NavControl
.fillMaxSize()
.hazeSource(hazeState)
.padding(horizontal = 16.dp)
- .then(
- if (blockTouches) Modifier.pointerInput(Unit) {
- awaitPointerEventScope {
- while (true) {
- val event = awaitPointerEvent(PointerEventPass.Initial)
- event.changes.forEach { it.consume() }
- }
+ .then(if (blockTouches) Modifier.pointerInput(Unit) {
+ awaitPointerEventScope {
+ while (true) {
+ val event = awaitPointerEvent(PointerEventPass.Initial)
+ event.changes.forEach { it.consume() }
}
- } else Modifier
- )
- ) {
+ }
+ } else Modifier)) {
item(key = "spacer_top") { Spacer(modifier = Modifier.height(topPadding)) }
item(key = "battery") {
BatteryView(
@@ -199,7 +195,7 @@ fun AirPodsSettingsScreen(viewModel: AirPodsViewModel, navController: NavControl
state.instance?.model?.capabilities?.contains(Capability.PPE) == true
if (hasHearingAidCapability || hasPPECapability) {
- if (hasPPECapability || (BuildConfig.FLAVOR == "xposed" && hasHearingAidCapability)) item(
+ if (hasPPECapability || (state.vendorIdHook && hasHearingAidCapability)) item(
key = "spacer_hearing_health"
) { Spacer(modifier = Modifier.height(24.dp)) }
item(key = "hearing_health") {
@@ -207,7 +203,7 @@ fun AirPodsSettingsScreen(viewModel: AirPodsViewModel, navController: NavControl
navController = navController,
hasPPECapability = hasPPECapability,
hasHearingAidCapability = hasHearingAidCapability,
- isXposed = BuildConfig.FLAVOR == "xposed"
+ vendorIdHook = state.vendorIdHook
)
}
}
@@ -272,27 +268,26 @@ fun AirPodsSettingsScreen(viewModel: AirPodsViewModel, navController: NavControl
}
item(key = "upgrade_button") {
- val context = LocalContext.current
- val textColor = if (isSystemInDarkTheme()) Color.White else Color.Black
-
if (!state.isPremium) {
Spacer(modifier = Modifier.height(28.dp))
StyledButton(
onClick = {
- viewModel.purchase(context)
+ navController.navigate("purchase_screen")
},
backdrop = rememberLayerBackdrop(),
modifier = Modifier.fillMaxWidth(),
maxScale = 0.05f,
- tint = Color(0xFF916100)
+ tint = if (isSystemInDarkTheme()) Color(0xFF916100) else Color(
+ 0xFFE59900
+ )
) {
Text(
- stringResource(R.string.unlock_all_features),
+ stringResource(R.string.unlock_advanced_features),
style = TextStyle(
fontSize = 16.sp,
fontWeight = FontWeight.Medium,
fontFamily = FontFamily(Font(R.font.sf_pro)),
- color = textColor
+ color = Color.White
),
)
}
@@ -320,11 +315,7 @@ fun AirPodsSettingsScreen(viewModel: AirPodsViewModel, navController: NavControl
state.controlStates[AACPManager.Companion.ControlCommandIdentifiers.CONVERSATION_DETECT_CONFIG]?.getOrNull(
0
) == 0x01.toByte()
- val loudSoundReduction =
- viewModel.getATTCharacteristicValue(ATTHandles.LOUD_SOUND_REDUCTION)
- ?.getOrNull(0) == 0x01.toByte()
- val isXposed = BuildConfig.FLAVOR == "xposed"
AudioSettings(
navController = navController,
adaptiveVolumeCapability = adaptiveVolumeCapability,
@@ -345,14 +336,14 @@ fun AirPodsSettingsScreen(viewModel: AirPodsViewModel, navController: NavControl
checked
)
},
- loudSoundReductionChecked = loudSoundReduction,
+ loudSoundReductionChecked = state.loudSoundReductionEnabled,
onLoudSoundReductionCheckedChange = {
viewModel.setATTCharacteristicValue(
ATTHandles.LOUD_SOUND_REDUCTION,
byteArrayOf(if (it) 0x01.toByte() else 0x00.toByte())
)
},
- isXposed = isXposed,
+ vendorIdHook = state.vendorIdHook,
isPremium = state.isPremium
)
}
@@ -484,11 +475,8 @@ fun AirPodsSettingsScreen(viewModel: AirPodsViewModel, navController: NavControl
tapCount.intValue = 0
viewModel.activateDemoMode()
}
- }
- )
- }
- )
- {
+ })
+ }) {
Text(
text = stringResource(R.string.airpods_not_connected), style = TextStyle(
fontSize = 24.sp,
diff --git a/android/app/src/main/java/me/kavishdevar/librepods/screens/AppSettingsScreen.kt b/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/AppSettingsScreen.kt
similarity index 54%
rename from android/app/src/main/java/me/kavishdevar/librepods/screens/AppSettingsScreen.kt
rename to android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/AppSettingsScreen.kt
index a2cc5b3..2cb741c 100644
--- a/android/app/src/main/java/me/kavishdevar/librepods/screens/AppSettingsScreen.kt
+++ b/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/AppSettingsScreen.kt
@@ -16,12 +16,16 @@
along with this program. If not, see .
*/
-package me.kavishdevar.librepods.screens
+package me.kavishdevar.librepods.presentation.screens
+import android.content.Intent
+import android.os.Build
import android.widget.Toast
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.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
@@ -42,9 +46,14 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
+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.LocalContext
+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
@@ -54,6 +63,7 @@ import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
+import androidx.core.net.toUri
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavController
import com.kyant.backdrop.backdrops.layerBackdrop
@@ -61,26 +71,26 @@ 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.composables.NavigationButton
-import me.kavishdevar.librepods.composables.StyledButton
-import me.kavishdevar.librepods.composables.StyledScaffold
-import me.kavishdevar.librepods.composables.StyledSlider
-import me.kavishdevar.librepods.composables.StyledToggle
-import me.kavishdevar.librepods.viewmodel.AppSettingsViewModel
+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(
- navController: NavController,
- viewModel: AppSettingsViewModel = viewModel()
+ navController: NavController, viewModel: AppSettingsViewModel = viewModel()
) {
val context = LocalContext.current
val scrollState = rememberScrollState()
- val uiState by viewModel.uiState.collectAsState()
+ val state by viewModel.uiState.collectAsState()
val backdrop = rememberLayerBackdrop()
StyledScaffold(
- title = stringResource(R.string.app_settings)
+ title = stringResource(R.string.settings)
) { topPadding, hazeState, bottomPadding ->
Column(
modifier = Modifier
@@ -96,23 +106,23 @@ fun AppSettingsScreen(
val backgroundColor = if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFFFFFFF)
val textColor = if (isDarkTheme) Color.White else Color.Black
- if (!uiState.isPremium) {
+ if (!state.isPremium) {
StyledButton(
onClick = {
- viewModel.purchase(context)
+ navController.navigate("purchase_screen")
},
backdrop = rememberLayerBackdrop(),
modifier = Modifier.fillMaxWidth(),
maxScale = 0.05f,
- tint = Color(0xFF916100)
+ tint = if (isSystemInDarkTheme()) Color(0xFF916100) else Color(0xFFE59900)
) {
Text(
- stringResource(R.string.unlock_all_features),
+ stringResource(R.string.unlock_advanced_features),
style = TextStyle(
fontSize = 16.sp,
fontWeight = FontWeight.Medium,
fontFamily = FontFamily(Font(R.font.sf_pro)),
- color = textColor
+ color = Color.White
),
)
}
@@ -122,9 +132,9 @@ fun AppSettingsScreen(
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 = uiState.showPhoneBatteryInWidget,
+ checked = state.showPhoneBatteryInWidget,
onCheckedChange = viewModel::setShowPhoneBatteryInWidget,
- enabled = uiState.isPremium
+ enabled = state.isPremium
)
Text(
@@ -149,10 +159,10 @@ fun AppSettingsScreen(
StyledToggle(
label = stringResource(R.string.conversational_awareness_pause_music),
description = stringResource(R.string.conversational_awareness_pause_music_description),
- checked = uiState.conversationalAwarenessPauseMusicEnabled,
+ checked = state.conversationalAwarenessPauseMusicEnabled,
onCheckedChange = viewModel::setConversationalAwarenessPauseMusicEnabled,
independent = false,
- enabled = uiState.isPremium
+ enabled = state.isPremium
)
HorizontalDivider(
@@ -164,16 +174,16 @@ fun AppSettingsScreen(
StyledToggle(
label = stringResource(R.string.relative_conversational_awareness_volume),
description = stringResource(R.string.relative_conversational_awareness_volume_description),
- checked = uiState.relativeConversationalAwarenessVolumeEnabled,
+ checked = state.relativeConversationalAwarenessVolumeEnabled,
onCheckedChange = viewModel::setRelativeConversationalAwarenessVolumeEnabled,
independent = false,
- enabled = uiState.isPremium
+ enabled = state.isPremium,
)
}
Spacer(modifier = Modifier.height(16.dp))
- val conversationalAwarenessVolume = uiState.conversationalAwarenessVolume
+ val conversationalAwarenessVolume = state.conversationalAwarenessVolume
LaunchedEffect(conversationalAwarenessVolume) {
viewModel.setConversationalAwarenessVolume(conversationalAwarenessVolume)
}
@@ -182,11 +192,12 @@ fun AppSettingsScreen(
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 = uiState.isPremium
+ enabled = state.isPremium
)
if (!BuildConfig.PLAY_BUILD) {
@@ -198,7 +209,7 @@ fun AppSettingsScreen(
name = stringResource(R.string.set_custom_camera_package),
navController = navController,
onClick = {
- if (uiState.isPremium) viewModel.setShowCameraDialog(true)
+ if (state.isPremium) viewModel.setShowCameraDialog(true)
},
independent = true,
description = stringResource(R.string.camera_control_app_description)
@@ -211,9 +222,9 @@ fun AppSettingsScreen(
title = stringResource(R.string.ear_detection),
label = stringResource(R.string.disconnect_when_not_wearing),
description = stringResource(R.string.disconnect_when_not_wearing_description),
- checked = uiState.disconnectWhenNotWearing,
+ checked = state.disconnectWhenNotWearing,
onCheckedChange = viewModel::setDisconnectWhenNotWearing,
- enabled = uiState.isPremium
+ enabled = state.isPremium
)
}
@@ -239,10 +250,10 @@ fun AppSettingsScreen(
StyledToggle(
label = stringResource(R.string.takeover_disconnected),
description = stringResource(R.string.takeover_disconnected_desc),
- checked = uiState.takeoverWhenDisconnected,
+ checked = state.takeoverWhenDisconnected,
onCheckedChange = viewModel::setTakeoverWhenDisconnected,
independent = false,
- enabled = uiState.isPremium
+ enabled = state.isPremium
)
HorizontalDivider(
thickness = 1.dp,
@@ -253,10 +264,10 @@ fun AppSettingsScreen(
StyledToggle(
label = stringResource(R.string.takeover_idle),
description = stringResource(R.string.takeover_idle_desc),
- checked = uiState.takeoverWhenIdle,
+ checked = state.takeoverWhenIdle,
onCheckedChange = viewModel::setTakeoverWhenIdle,
independent = false,
- enabled = uiState.isPremium
+ enabled = state.isPremium
)
HorizontalDivider(
thickness = 1.dp,
@@ -267,10 +278,10 @@ fun AppSettingsScreen(
StyledToggle(
label = stringResource(R.string.takeover_music),
description = stringResource(R.string.takeover_music_desc),
- checked = uiState.takeoverWhenMusic,
+ checked = state.takeoverWhenMusic,
onCheckedChange = viewModel::setTakeoverWhenMusic,
independent = false,
- enabled = uiState.isPremium
+ enabled = state.isPremium
)
HorizontalDivider(
thickness = 1.dp,
@@ -281,10 +292,10 @@ fun AppSettingsScreen(
StyledToggle(
label = stringResource(R.string.takeover_call),
description = stringResource(R.string.takeover_call_desc),
- checked = uiState.takeoverWhenCall,
+ checked = state.takeoverWhenCall,
onCheckedChange = viewModel::setTakeoverWhenCall,
independent = false,
- enabled = uiState.isPremium
+ enabled = state.isPremium
)
}
@@ -310,10 +321,10 @@ fun AppSettingsScreen(
StyledToggle(
label = stringResource(R.string.takeover_ringing_call),
description = stringResource(R.string.takeover_ringing_call_desc),
- checked = uiState.takeoverWhenRingingCall,
+ checked = state.takeoverWhenRingingCall,
onCheckedChange = viewModel::setTakeoverWhenRingingCall,
independent = false,
- enabled = uiState.isPremium
+ enabled = state.isPremium
)
HorizontalDivider(
thickness = 1.dp,
@@ -324,10 +335,10 @@ fun AppSettingsScreen(
StyledToggle(
label = stringResource(R.string.takeover_media_start),
description = stringResource(R.string.takeover_media_start_desc),
- checked = uiState.takeoverWhenMediaStart,
+ checked = state.takeoverWhenMediaStart,
onCheckedChange = viewModel::setTakeoverWhenMediaStart,
independent = false,
- enabled = uiState.isPremium
+ enabled = state.isPremium
)
}
@@ -345,13 +356,31 @@ fun AppSettingsScreen(
StyledToggle(
label = stringResource(R.string.use_alternate_head_tracking_packets),
description = stringResource(R.string.use_alternate_head_tracking_packets_description),
- checked = uiState.useAlternateHeadTrackingPackets,
+ checked = state.useAlternateHeadTrackingPackets,
onCheckedChange = viewModel::setUseAlternateHeadTrackingPackets,
independent = true,
- enabled = uiState.isPremium
+ enabled = state.isPremium
)
- Spacer(modifier = Modifier.height(16.dp))
+
+ if (BuildConfig.FLAVOR == "xposed") {
+ Spacer(modifier = Modifier.height(16.dp))
+ val restartBluetoothText = stringResource(R.string.found_offset_restart_bluetooth)
+ StyledToggle(
+ label = stringResource(R.string.act_as_an_apple_device),
+ description = stringResource(R.string.act_as_an_apple_device_description) + "\n" + stringResource(
+ R.string.requires_xposed
+ ).replaceFirstChar { if (it.isLowerCase()) it.titlecase(getDefault()) else it.toString() },
+ checked = state.vendorIdHook,
+ onCheckedChange = { enabled ->
+ Toast.makeText(context, restartBluetoothText, Toast.LENGTH_SHORT).show()
+ viewModel.setVendorIdHook(enabled)
+ },
+ independent = true,
+ enabled = state.isPremium
+ )
+ }
+
// NavigationButton(
// to = "troubleshooting",
@@ -361,6 +390,229 @@ fun AppSettingsScreen(
// description = stringResource(R.string.troubleshooting_description)
// )
+ Spacer(modifier = Modifier.height(8.dp))
+
+ Text(
+ text = stringResource(R.string.contact), 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(4.dp))
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .background(
+ backgroundColor, RoundedCornerShape(28.dp)
+ )
+ .clip(RoundedCornerShape(28.dp))
+ ) {
+ NavigationButton(
+ to = "",
+ name = stringResource(R.string.email),
+ navController = navController,
+ onClick = {
+ 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_TEXT,
+ "\n\n\n----------" +
+ "\nPhone details:" +
+ "\nDEVICE: ${Build.DEVICE}" +
+ "\nMANUFACTURER: ${Build.MANUFACTURER} (${Build.BRAND})" +
+ "\nMODEL: ${Build.MODEL} (${Build.PRODUCT})" +
+ "\nVERSION: ${Build.DISPLAY} (${Build.VERSION.SDK_INT_FULL})" +
+ "\n\nApp details:" +
+ "\nVERSION: ${BuildConfig.VERSION_NAME}" +
+ "\nVERSION_CODE: ${BuildConfig.VERSION_CODE}" +
+ "\nFLAVOR: ${BuildConfig.FLAVOR}" +
+ "\nBUILD_TYPE: ${BuildConfig.BUILD_TYPE}"
+ )
+ }
+ context.startActivity(intent)
+ },
+ independent = false
+ )
+
+ HorizontalDivider(
+ thickness = 1.dp,
+ color = Color(0x40888888),
+ modifier = Modifier.padding(horizontal = 12.dp)
+ )
+
+ NavigationButton(
+ to = "",
+ name = stringResource(R.string.discord),
+ navController = navController,
+ onClick = {
+ val intent =
+ Intent(Intent.ACTION_VIEW, "https://discord.gg/Ts4wupXcmc".toUri())
+ context.startActivity(intent)
+ },
+ independent = false
+ )
+
+ HorizontalDivider(
+ thickness = 1.dp,
+ color = Color(0x40888888),
+ modifier = Modifier.padding(horizontal = 12.dp)
+ )
+
+ NavigationButton(
+ to = "",
+ name = stringResource(R.string.github_issues),
+ navController = navController,
+ onClick = {
+ val intent = Intent(
+ Intent.ACTION_VIEW,
+ "https://github.com/kavishdevar/librepods/issues".toUri()
+ )
+ context.startActivity(intent)
+ },
+ independent = false
+ )
+ }
+
+ Spacer(modifier = Modifier.height(8.dp))
+
+ Text(
+ text = stringResource(R.string.about), 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)
+ )
+
+ val rowHeight = remember { mutableStateOf(0.dp) }
+ val density = LocalDensity.current
+
+ Spacer(modifier = Modifier.height(4.dp))
+ Column(
+ modifier = Modifier
+ .clip(RoundedCornerShape(28.dp))
+ .fillMaxWidth()
+ .background(backgroundColor, RoundedCornerShape(28.dp))
+ .padding(top = 2.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.version), style = TextStyle(
+ fontSize = 16.sp,
+ color = textColor,
+ fontFamily = FontFamily(Font(R.font.sf_pro))
+ )
+ )
+ Text(
+ text = BuildConfig.VERSION_NAME, 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_code), style = TextStyle(
+ fontSize = 16.sp,
+ color = textColor,
+ fontFamily = FontFamily(Font(R.font.sf_pro))
+ )
+ )
+ Text(
+ text = BuildConfig.VERSION_CODE.toString(), 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.flavor), style = TextStyle(
+ fontSize = 16.sp,
+ color = textColor,
+ fontFamily = FontFamily(Font(R.font.sf_pro))
+ )
+ )
+ Text(
+ text = BuildConfig.FLAVOR, 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_type), style = TextStyle(
+ fontSize = 16.sp,
+ color = textColor,
+ fontFamily = FontFamily(Font(R.font.sf_pro))
+ )
+ )
+ Text(
+ text = BuildConfig.BUILD_TYPE,
+ 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))
+ )
+ )
+ }
+ }
+
Spacer(modifier = Modifier.height(16.dp))
NavigationButton(
@@ -370,9 +622,9 @@ fun AppSettingsScreen(
independent = true
)
- Spacer(modifier = Modifier.height(32.dp))
+ Spacer(modifier = Modifier.height(bottomPadding))
- if (uiState.showCameraDialog) {
+ if (state.showCameraDialog) {
AlertDialog(onDismissRequest = { viewModel.setShowCameraDialog(false) }, title = {
Text(
stringResource(R.string.set_custom_camera_package),
@@ -388,13 +640,13 @@ fun AppSettingsScreen(
)
OutlinedTextField(
- value = uiState.cameraPackageValue,
+ value = state.cameraPackageValue,
onValueChange = {
viewModel.setCameraPackageValue(it)
viewModel.setCameraPackageError(null)
},
modifier = Modifier.fillMaxWidth(),
- isError = uiState.cameraPackageError != null,
+ isError = state.cameraPackageError != null,
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Ascii,
capitalization = KeyboardCapitalization.None
@@ -406,9 +658,9 @@ fun AppSettingsScreen(
unfocusedBorderColor = if (isDarkTheme) Color.Gray else Color.LightGray
),
supportingText = {
- if (uiState.cameraPackageError != null) {
+ if (state.cameraPackageError != null) {
Text(
- uiState.cameraPackageError ?: "",
+ state.cameraPackageError ?: "",
color = MaterialTheme.colorScheme.error
)
}
@@ -439,7 +691,6 @@ fun AppSettingsScreen(
}
})
}
- Spacer(modifier = Modifier.height(bottomPadding))
}
}
}
diff --git a/android/app/src/main/java/me/kavishdevar/librepods/screens/CameraControlScreen.kt b/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/CameraControlScreen.kt
similarity index 90%
rename from android/app/src/main/java/me/kavishdevar/librepods/screens/CameraControlScreen.kt
rename to android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/CameraControlScreen.kt
index 4c670f5..433e7b3 100644
--- a/android/app/src/main/java/me/kavishdevar/librepods/screens/CameraControlScreen.kt
+++ b/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/CameraControlScreen.kt
@@ -16,7 +16,7 @@
along with this program. If not, see .
*/
-package me.kavishdevar.librepods.screens
+package me.kavishdevar.librepods.presentation.screens
import android.accessibilityservice.AccessibilityServiceInfo
import android.content.ComponentName
@@ -41,12 +41,12 @@ import androidx.compose.ui.unit.dp
import com.kyant.backdrop.backdrops.layerBackdrop
import com.kyant.backdrop.backdrops.rememberLayerBackdrop
import me.kavishdevar.librepods.R
-import me.kavishdevar.librepods.composables.SelectItem
-import me.kavishdevar.librepods.composables.StyledScaffold
-import me.kavishdevar.librepods.composables.StyledSelectList
+import me.kavishdevar.librepods.presentation.components.SelectItem
+import me.kavishdevar.librepods.presentation.components.StyledScaffold
+import me.kavishdevar.librepods.presentation.components.StyledSelectList
import me.kavishdevar.librepods.services.AppListenerService
-import me.kavishdevar.librepods.utils.AACPManager.Companion.StemPressType
-import me.kavishdevar.librepods.viewmodel.AirPodsViewModel
+import me.kavishdevar.librepods.bluetooth.AACPManager.Companion.StemPressType
+import me.kavishdevar.librepods.presentation.viewmodel.AirPodsViewModel
@Composable
fun CameraControlScreen(viewModel: AirPodsViewModel) {
diff --git a/android/app/src/main/java/me/kavishdevar/librepods/screens/DebugScreen.kt b/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/DebugScreen.kt
similarity index 98%
rename from android/app/src/main/java/me/kavishdevar/librepods/screens/DebugScreen.kt
rename to android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/DebugScreen.kt
index 3ca7190..75c4bc2 100644
--- a/android/app/src/main/java/me/kavishdevar/librepods/screens/DebugScreen.kt
+++ b/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/DebugScreen.kt
@@ -18,7 +18,7 @@
@file:OptIn(ExperimentalHazeMaterialsApi::class, ExperimentalEncodingApi::class)
-package me.kavishdevar.librepods.screens
+package me.kavishdevar.librepods.presentation.screens
import android.annotation.SuppressLint
import android.content.ClipData
@@ -82,10 +82,10 @@ import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import me.kavishdevar.librepods.R
-import me.kavishdevar.librepods.composables.StyledIconButton
-import me.kavishdevar.librepods.composables.StyledScaffold
-import me.kavishdevar.librepods.constants.BatteryStatus
-import me.kavishdevar.librepods.constants.isHeadTrackingData
+import me.kavishdevar.librepods.presentation.components.StyledIconButton
+import me.kavishdevar.librepods.presentation.components.StyledScaffold
+import me.kavishdevar.librepods.data.BatteryStatus
+import me.kavishdevar.librepods.data.isHeadTrackingData
import me.kavishdevar.librepods.services.ServiceManager
import kotlin.io.encoding.ExperimentalEncodingApi
diff --git a/android/app/src/main/java/me/kavishdevar/librepods/screens/HeadTrackingScreen.kt b/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/HeadTrackingScreen.kt
similarity index 89%
rename from android/app/src/main/java/me/kavishdevar/librepods/screens/HeadTrackingScreen.kt
rename to android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/HeadTrackingScreen.kt
index 5576b79..6137ec1 100644
--- a/android/app/src/main/java/me/kavishdevar/librepods/screens/HeadTrackingScreen.kt
+++ b/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/HeadTrackingScreen.kt
@@ -21,8 +21,12 @@
@file:OptIn(ExperimentalEncodingApi::class)
-package me.kavishdevar.librepods.screens
+package me.kavishdevar.librepods.presentation.screens
+import android.graphics.Paint
+import android.graphics.RadialGradient
+import android.graphics.Shader
+import android.graphics.Typeface
import android.util.Log
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.ExperimentalAnimationApi
@@ -72,7 +76,6 @@ import androidx.compose.ui.graphics.asAndroidPath
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.graphics.nativeCanvas
import androidx.compose.ui.graphics.toArgb
-import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.drawText
@@ -83,6 +86,7 @@ import androidx.compose.ui.text.rememberTextMeasurer
import androidx.compose.ui.text.style.TextAlign
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 dev.chrisbanes.haze.hazeSource
@@ -91,13 +95,13 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import me.kavishdevar.librepods.R
-import me.kavishdevar.librepods.composables.StyledButton
-import me.kavishdevar.librepods.composables.StyledIconButton
-import me.kavishdevar.librepods.composables.StyledScaffold
-import me.kavishdevar.librepods.composables.StyledToggle
+import me.kavishdevar.librepods.presentation.components.StyledButton
+import me.kavishdevar.librepods.presentation.components.StyledIconButton
+import me.kavishdevar.librepods.presentation.components.StyledScaffold
+import me.kavishdevar.librepods.presentation.components.StyledToggle
import me.kavishdevar.librepods.services.ServiceManager
import me.kavishdevar.librepods.utils.HeadTracking
-import me.kavishdevar.librepods.viewmodel.AirPodsViewModel
+import me.kavishdevar.librepods.presentation.viewmodel.AirPodsViewModel
import kotlin.io.encoding.ExperimentalEncodingApi
import kotlin.math.abs
import kotlin.math.cos
@@ -107,7 +111,7 @@ import kotlin.random.Random
@ExperimentalHazeMaterialsApi
@OptIn(ExperimentalMaterial3Api::class, ExperimentalAnimationApi::class)
@Composable
-fun HeadTrackingScreen(viewModel: AirPodsViewModel) {
+fun HeadTrackingScreen(viewModel: AirPodsViewModel, navController: NavController) {
val state by viewModel.uiState.collectAsState()
DisposableEffect(Unit) {
viewModel.startHeadTracking()
@@ -163,25 +167,23 @@ fun HeadTrackingScreen(viewModel: AirPodsViewModel) {
) {
Spacer(modifier = Modifier.height(topPadding))
- val context = LocalContext.current
-
if (!state.isPremium) {
StyledButton(
onClick = {
- viewModel.purchase(context)
+ navController.navigate("purchase_screen")
},
backdrop = rememberLayerBackdrop(),
modifier = Modifier.fillMaxWidth(),
maxScale = 0.05f,
- tint = Color(0xFF916100)
+ tint = if (isSystemInDarkTheme()) Color(0xFF916100) else Color(0xFFE59900)
) {
Text(
- stringResource(R.string.unlock_all_features),
+ stringResource(R.string.unlock_advanced_features),
style = TextStyle(
fontSize = 16.sp,
fontWeight = FontWeight.Medium,
fontFamily = FontFamily(Font(R.font.sf_pro)),
- color = textColor
+ color = Color.White
),
)
}
@@ -192,31 +194,20 @@ fun HeadTrackingScreen(viewModel: AirPodsViewModel) {
label = "Head Gestures",
checked = state.headGesturesEnabled,
onCheckedChange = { viewModel.setHeadGesturesEnabled(it) },
- enabled = state.isPremium
- )
-
- Spacer(modifier = Modifier.height(2.dp))
- Text(
- stringResource(R.string.head_gestures_details),
- style = TextStyle(
- fontSize = 14.sp,
- fontWeight = FontWeight.Normal,
- fontFamily = FontFamily(Font(R.font.sf_pro)),
- color = textColor.copy(0.6f)
- ),
- modifier = Modifier.padding(start = 4.dp)
+ enabled = state.isPremium,
+ description = stringResource(R.string.head_gestures_details)
)
Spacer(modifier = Modifier.height(16.dp))
Text(
"Head Orientation",
style = TextStyle(
- fontSize = 18.sp,
- fontWeight = FontWeight.Medium,
- fontFamily = FontFamily(Font(R.font.sf_pro)),
- color = textColor
+ fontSize = 14.sp,
+ fontWeight = FontWeight.Bold,
+ color = textColor.copy(alpha = 0.6f),
+ fontFamily = FontFamily(Font(R.font.sf_pro))
),
- modifier = Modifier.padding(start = 4.dp, bottom = 8.dp, top = 8.dp)
+ modifier = Modifier.padding(start = 16.dp, bottom = 8.dp, top = 8.dp)
)
HeadVisualization()
@@ -224,12 +215,12 @@ fun HeadTrackingScreen(viewModel: AirPodsViewModel) {
Text(
"Velocity",
style = TextStyle(
- fontSize = 18.sp,
- fontWeight = FontWeight.Medium,
- fontFamily = FontFamily(Font(R.font.sf_pro)),
- color = textColor
+ fontSize = 14.sp,
+ fontWeight = FontWeight.Bold,
+ color = textColor.copy(alpha = 0.6f),
+ fontFamily = FontFamily(Font(R.font.sf_pro))
),
- modifier = Modifier.padding(start = 4.dp, bottom = 8.dp, top = 8.dp)
+ modifier = Modifier.padding(start = 16.dp, bottom = 8.dp, top = 8.dp)
)
AccelerationPlot()
@@ -481,9 +472,9 @@ private fun HeadVisualization() {
spherePath.close()
drawContext.canvas.nativeCanvas.apply {
- val paint = android.graphics.Paint().apply {
- style = android.graphics.Paint.Style.FILL
- shader = android.graphics.RadialGradient(
+ val paint = Paint().apply {
+ style = Paint.Style.FILL
+ shader = RadialGradient(
center.x + sinY * faceRadius * 0.3f,
center.y - sinP * faceRadius * 0.3f,
faceRadius * 1.4f,
@@ -495,14 +486,14 @@ private fun HeadVisualization() {
backgroundColor.copy(alpha = 0.7f).toArgb()
),
floatArrayOf(0.3f, 0.5f, 0.7f, 0.8f, 1f),
- android.graphics.Shader.TileMode.CLAMP
+ Shader.TileMode.CLAMP
)
}
drawPath(spherePath.asAndroidPath(), paint)
- val highlightPaint = android.graphics.Paint().apply {
- style = android.graphics.Paint.Style.FILL
- shader = android.graphics.RadialGradient(
+ val highlightPaint = Paint().apply {
+ style = Paint.Style.FILL
+ shader = RadialGradient(
center.x - faceRadius * 0.4f - sinY * faceRadius * 0.5f,
center.y - faceRadius * 0.4f - sinP * faceRadius * 0.5f,
faceRadius * 0.9f,
@@ -512,15 +503,15 @@ private fun HeadVisualization() {
android.graphics.Color.TRANSPARENT
),
floatArrayOf(0f, 0.3f, 1f),
- android.graphics.Shader.TileMode.CLAMP
+ Shader.TileMode.CLAMP
)
alpha = if (darkTheme) 30 else 60
}
drawPath(spherePath.asAndroidPath(), highlightPaint)
- val secondaryHighlightPaint = android.graphics.Paint().apply {
- style = android.graphics.Paint.Style.FILL
- shader = android.graphics.RadialGradient(
+ val secondaryHighlightPaint = Paint().apply {
+ style = Paint.Style.FILL
+ shader = RadialGradient(
center.x + faceRadius * 0.3f + sinY * faceRadius * 0.3f,
center.y + faceRadius * 0.3f - sinP * faceRadius * 0.3f,
faceRadius * 0.7f,
@@ -529,15 +520,15 @@ private fun HeadVisualization() {
android.graphics.Color.TRANSPARENT
),
floatArrayOf(0f, 1f),
- android.graphics.Shader.TileMode.CLAMP
+ Shader.TileMode.CLAMP
)
alpha = if (darkTheme) 15 else 30
}
drawPath(spherePath.asAndroidPath(), secondaryHighlightPaint)
- val shadowPaint = android.graphics.Paint().apply {
- style = android.graphics.Paint.Style.FILL
- shader = android.graphics.RadialGradient(
+ val shadowPaint = Paint().apply {
+ style = Paint.Style.FILL
+ shader = RadialGradient(
center.x + sinY * faceRadius * 0.5f,
center.y - sinP * faceRadius * 0.5f,
faceRadius * 1.1f,
@@ -546,7 +537,7 @@ private fun HeadVisualization() {
android.graphics.Color.BLACK
),
floatArrayOf(0.7f, 1f),
- android.graphics.Shader.TileMode.CLAMP
+ Shader.TileMode.CLAMP
)
alpha = if (darkTheme) 40 else 20
}
@@ -606,13 +597,13 @@ private fun HeadVisualization() {
}
drawContext.canvas.nativeCanvas.apply {
- val paint = android.graphics.Paint().apply {
+ val paint = Paint().apply {
color = if (darkTheme) android.graphics.Color.WHITE else android.graphics.Color.BLACK
textSize = 12.sp.toPx()
- textAlign = android.graphics.Paint.Align.RIGHT
- typeface = android.graphics.Typeface.create(
+ textAlign = Paint.Align.RIGHT
+ typeface = Typeface.create(
"SF Pro",
- android.graphics.Typeface.NORMAL
+ Typeface.NORMAL
)
}
@@ -726,10 +717,10 @@ private fun AccelerationPlot() {
}
drawContext.canvas.nativeCanvas.apply {
- val paint = android.graphics.Paint().apply {
+ val paint = Paint().apply {
color = if (darkTheme) android.graphics.Color.WHITE else android.graphics.Color.BLACK
textSize = 12.sp.toPx()
- textAlign = android.graphics.Paint.Align.RIGHT
+ textAlign = Paint.Align.RIGHT
}
drawText("${maxAbs.toInt()}", 30.dp.toPx(), 20.dp.toPx(), paint)
@@ -742,20 +733,20 @@ private fun AccelerationPlot() {
drawCircle(Color(0xFF007AFF), 5.dp.toPx(), Offset(width - 150.dp.toPx(), legendY))
drawContext.canvas.nativeCanvas.apply {
- val paint = android.graphics.Paint().apply {
+ val paint = Paint().apply {
color = if (darkTheme) android.graphics.Color.WHITE else android.graphics.Color.BLACK
textSize = 12.sp.toPx()
- textAlign = android.graphics.Paint.Align.LEFT
+ textAlign = Paint.Align.LEFT
}
drawText("Horizontal", width - 140.dp.toPx(), textOffsetY, paint)
}
drawCircle(Color(0xFFFF3B30), 5.dp.toPx(), Offset(width - 70.dp.toPx(), legendY))
drawContext.canvas.nativeCanvas.apply {
- val paint = android.graphics.Paint().apply {
+ val paint = Paint().apply {
color = if (darkTheme) android.graphics.Color.WHITE else android.graphics.Color.BLACK
textSize = 12.sp.toPx()
- textAlign = android.graphics.Paint.Align.LEFT
+ textAlign = Paint.Align.LEFT
}
drawText("Vertical", width - 60.dp.toPx(), textOffsetY, paint)
}
diff --git a/android/app/src/main/java/me/kavishdevar/librepods/screens/HearingAidAdjustmentsScreen.kt b/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/HearingAidAdjustmentsScreen.kt
similarity index 95%
rename from android/app/src/main/java/me/kavishdevar/librepods/screens/HearingAidAdjustmentsScreen.kt
rename to android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/HearingAidAdjustmentsScreen.kt
index 8fe06b7..92dff14 100644
--- a/android/app/src/main/java/me/kavishdevar/librepods/screens/HearingAidAdjustmentsScreen.kt
+++ b/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/HearingAidAdjustmentsScreen.kt
@@ -16,7 +16,7 @@
along with this program. If not, see .
*/
-package me.kavishdevar.librepods.screens
+package me.kavishdevar.librepods.presentation.screens
import android.annotation.SuppressLint
import android.util.Log
@@ -50,16 +50,16 @@ import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import me.kavishdevar.librepods.R
-import me.kavishdevar.librepods.composables.StyledScaffold
-import me.kavishdevar.librepods.composables.StyledSlider
-import me.kavishdevar.librepods.composables.StyledToggle
+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.services.ServiceManager
-import me.kavishdevar.librepods.utils.AACPManager
-import me.kavishdevar.librepods.utils.ATTHandles
-import me.kavishdevar.librepods.utils.HearingAidSettings
-import me.kavishdevar.librepods.utils.parseHearingAidSettingsResponse
-import me.kavishdevar.librepods.utils.sendHearingAidSettings
-import me.kavishdevar.librepods.viewmodel.AirPodsViewModel
+import me.kavishdevar.librepods.bluetooth.AACPManager
+import me.kavishdevar.librepods.bluetooth.ATTHandles
+import me.kavishdevar.librepods.data.HearingAidSettings
+import me.kavishdevar.librepods.data.parseHearingAidSettingsResponse
+import me.kavishdevar.librepods.data.sendHearingAidSettings
+import me.kavishdevar.librepods.presentation.viewmodel.AirPodsViewModel
import java.io.IOException
import kotlin.io.encoding.ExperimentalEncodingApi
diff --git a/android/app/src/main/java/me/kavishdevar/librepods/screens/HearingAidScreen.kt b/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/HearingAidScreen.kt
similarity index 92%
rename from android/app/src/main/java/me/kavishdevar/librepods/screens/HearingAidScreen.kt
rename to android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/HearingAidScreen.kt
index 5c292a1..753c289 100644
--- a/android/app/src/main/java/me/kavishdevar/librepods/screens/HearingAidScreen.kt
+++ b/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/HearingAidScreen.kt
@@ -16,7 +16,7 @@
along with this program. If not, see .
*/
-package me.kavishdevar.librepods.screens
+package me.kavishdevar.librepods.presentation.screens
import android.annotation.SuppressLint
import android.util.Log
@@ -62,15 +62,14 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import me.kavishdevar.librepods.R
-import me.kavishdevar.librepods.composables.ConfirmationDialog
-import me.kavishdevar.librepods.composables.NavigationButton
-import me.kavishdevar.librepods.composables.StyledScaffold
-import me.kavishdevar.librepods.composables.StyledToggle
-import me.kavishdevar.librepods.utils.AACPManager
-import me.kavishdevar.librepods.utils.ATTHandles
-import me.kavishdevar.librepods.utils.parseTransparencySettingsResponse
-import me.kavishdevar.librepods.utils.sendTransparencySettings
-import me.kavishdevar.librepods.viewmodel.AirPodsViewModel
+import me.kavishdevar.librepods.bluetooth.AACPManager
+import me.kavishdevar.librepods.data.parseTransparencySettingsResponse
+import me.kavishdevar.librepods.data.sendTransparencySettings
+import me.kavishdevar.librepods.presentation.components.ConfirmationDialog
+import me.kavishdevar.librepods.presentation.components.NavigationButton
+import me.kavishdevar.librepods.presentation.components.StyledScaffold
+import me.kavishdevar.librepods.presentation.components.StyledToggle
+import me.kavishdevar.librepods.presentation.viewmodel.AirPodsViewModel
import kotlin.io.encoding.ExperimentalEncodingApi
private const val TAG = "AccessibilitySettings"
@@ -174,7 +173,7 @@ fun HearingAidScreen(viewModel: AirPodsViewModel, navController: NavController)
NavigationButton(
to = "hearing_aid_adjustments",
name = stringResource(R.string.adjustments),
- navController,
+ navController = navController,
independent = false
)
}
@@ -193,7 +192,7 @@ fun HearingAidScreen(viewModel: AirPodsViewModel, navController: NavController)
NavigationButton(
to = "update_hearing_test",
name = stringResource(R.string.update_hearing_test),
- navController,
+ navController = navController,
independent = true
)
@@ -237,7 +236,6 @@ fun HearingAidScreen(viewModel: AirPodsViewModel, navController: NavController)
Spacer(modifier = Modifier.height(bottomPadding))
}
}
-
ConfirmationDialog(
showDialog = showDialog,
title = "Enable Hearing Aid",
@@ -256,12 +254,11 @@ fun HearingAidScreen(viewModel: AirPodsViewModel, navController: NavController)
hearingAidEnabled.value = true
CoroutineScope(Dispatchers.IO).launch {
try {
- val data = viewModel.getATTCharacteristicValue(ATTHandles.TRANSPARENCY) ?: byteArrayOf()
- if (data.isEmpty()) {
+ if (state.hearingAidData.isEmpty()) {
Log.w(TAG, "read failed")
return@launch
}
- val parsed = parseTransparencySettingsResponse(data)
+ val parsed = parseTransparencySettingsResponse(state.hearingAidData)
val disabledSettings = parsed.copy(enabled = false)
sendTransparencySettings(viewModel::setATTCharacteristicValue, disabledSettings)
} catch (e: Exception) {
@@ -269,6 +266,10 @@ fun HearingAidScreen(viewModel: AirPodsViewModel, navController: NavController)
}
}
},
+ onDismiss = {
+ hearingAidEnabled.value = false
+ showDialog.value = false
+ },
hazeState = hazeStateS.value,
// backdrop = backdrop
)
diff --git a/android/app/src/main/java/me/kavishdevar/librepods/screens/HearingProtectionScreen.kt b/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/HearingProtectionScreen.kt
similarity index 83%
rename from android/app/src/main/java/me/kavishdevar/librepods/screens/HearingProtectionScreen.kt
rename to android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/HearingProtectionScreen.kt
index 9e6cf28..7bbbfd5 100644
--- a/android/app/src/main/java/me/kavishdevar/librepods/screens/HearingProtectionScreen.kt
+++ b/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/HearingProtectionScreen.kt
@@ -16,7 +16,7 @@
along with this program. If not, see .
*/
-package me.kavishdevar.librepods.screens
+package me.kavishdevar.librepods.presentation.screens
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
@@ -33,11 +33,11 @@ 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.composables.StyledScaffold
-import me.kavishdevar.librepods.composables.StyledToggle
-import me.kavishdevar.librepods.utils.AACPManager
-import me.kavishdevar.librepods.utils.ATTHandles
-import me.kavishdevar.librepods.viewmodel.AirPodsViewModel
+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.viewmodel.AirPodsViewModel
@Composable
fun HearingProtectionScreen(viewModel: AirPodsViewModel) {
@@ -54,20 +54,19 @@ fun HearingProtectionScreen(viewModel: AirPodsViewModel) {
) {
Spacer(modifier = Modifier.height(spacerHeight))
- if (BuildConfig.FLAVOR == "xposed") {
+ if (state.vendorIdHook) {
StyledToggle(
title = stringResource(R.string.environmental_noise),
label = stringResource(R.string.loud_sound_reduction),
description = stringResource(R.string.loud_sound_reduction_description),
- checked = viewModel.getATTCharacteristicValue(ATTHandles.LOUD_SOUND_REDUCTION)
- ?.get(0)?.toInt() == 1,
+ checked = state.loudSoundReductionEnabled,
onCheckedChange = {
viewModel.setATTCharacteristicValue(
ATTHandles.LOUD_SOUND_REDUCTION,
byteArrayOf(if (it) 1.toByte() else 0.toByte())
)
- }
-// attHandle = ATTHandles.LOUD_SOUND_REDUCTION
+ },
+ enabled = state.isPremium
)
Spacer(modifier = Modifier.height(12.dp))
@@ -83,7 +82,9 @@ fun HearingProtectionScreen(viewModel: AirPodsViewModel) {
viewModel.setControlCommandBoolean(
AACPManager.Companion.ControlCommandIdentifiers.PPE_TOGGLE_CONFIG, it
)
- })
+ },
+ enabled = state.isPremium
+ )
}
}
}
diff --git a/android/app/src/main/java/me/kavishdevar/librepods/screens/OpenSourceLicensesScreen.kt b/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/OpenSourceLicensesScreen.kt
similarity index 80%
rename from android/app/src/main/java/me/kavishdevar/librepods/screens/OpenSourceLicensesScreen.kt
rename to android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/OpenSourceLicensesScreen.kt
index 3a8aa38..8f57cdb 100644
--- a/android/app/src/main/java/me/kavishdevar/librepods/screens/OpenSourceLicensesScreen.kt
+++ b/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/OpenSourceLicensesScreen.kt
@@ -16,7 +16,7 @@
along with this program. If not, see .
*/
-package me.kavishdevar.librepods.screens
+package me.kavishdevar.librepods.presentation.screens
import android.annotation.SuppressLint
import androidx.compose.foundation.isSystemInDarkTheme
@@ -28,12 +28,9 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.DisposableEffect
-import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.runtime.mutableFloatStateOf
-import androidx.compose.runtime.remember
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.navigation.NavController
@@ -42,17 +39,9 @@ import com.kyant.backdrop.backdrops.rememberLayerBackdrop
import com.mikepenz.aboutlibraries.ui.compose.m3.LibrariesContainer
import com.mikepenz.aboutlibraries.ui.compose.produceLibraries
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.composables.StyledIconButton
-import me.kavishdevar.librepods.composables.StyledScaffold
-import me.kavishdevar.librepods.composables.StyledSlider
-import me.kavishdevar.librepods.services.ServiceManager
-import me.kavishdevar.librepods.utils.AACPManager
+import me.kavishdevar.librepods.presentation.components.StyledScaffold
import kotlin.io.encoding.ExperimentalEncodingApi
private var debounceJob: Job? = null
@@ -76,7 +65,7 @@ fun OpenSourceLicensesScreen(navController: NavController) {
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
Spacer(modifier = Modifier.height(spacerHeight))
- val context = androidx.compose.ui.platform.LocalContext.current
+ val context = LocalContext.current
val libraries by produceLibraries {
context.resources.openRawResource(R.raw.aboutlibraries)
.bufferedReader()
@@ -90,4 +79,4 @@ fun OpenSourceLicensesScreen(navController: NavController) {
)
}
}
-}
\ No newline at end of file
+}
diff --git a/android/app/src/main/java/me/kavishdevar/librepods/screens/PressAndHoldSettingsScreen.kt b/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/PressAndHoldSettingsScreen.kt
similarity index 93%
rename from android/app/src/main/java/me/kavishdevar/librepods/screens/PressAndHoldSettingsScreen.kt
rename to android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/PressAndHoldSettingsScreen.kt
index 096631c..77dd621 100644
--- a/android/app/src/main/java/me/kavishdevar/librepods/screens/PressAndHoldSettingsScreen.kt
+++ b/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/PressAndHoldSettingsScreen.kt
@@ -18,7 +18,7 @@
@file:OptIn(ExperimentalStdlibApi::class, ExperimentalEncodingApi::class)
-package me.kavishdevar.librepods.screens
+package me.kavishdevar.librepods.presentation.screens
import android.content.Context
import android.util.Log
@@ -49,25 +49,26 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.core.content.edit
+import androidx.navigation.NavController
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.composables.SelectItem
-import me.kavishdevar.librepods.composables.StyledButton
-import me.kavishdevar.librepods.composables.StyledScaffold
-import me.kavishdevar.librepods.composables.StyledSelectList
-import me.kavishdevar.librepods.constants.StemAction
+import me.kavishdevar.librepods.presentation.components.SelectItem
+import me.kavishdevar.librepods.presentation.components.StyledButton
+import me.kavishdevar.librepods.presentation.components.StyledScaffold
+import me.kavishdevar.librepods.presentation.components.StyledSelectList
+import me.kavishdevar.librepods.data.StemAction
import me.kavishdevar.librepods.services.ServiceManager
-import me.kavishdevar.librepods.utils.AACPManager
-import me.kavishdevar.librepods.viewmodel.AirPodsViewModel
+import me.kavishdevar.librepods.bluetooth.AACPManager
+import me.kavishdevar.librepods.presentation.viewmodel.AirPodsViewModel
import kotlin.experimental.and
import kotlin.io.encoding.ExperimentalEncodingApi
@ExperimentalHazeMaterialsApi
@OptIn(ExperimentalMaterial3Api::class)
@Composable
-fun LongPress(viewModel: AirPodsViewModel, name: String) {
+fun LongPress(viewModel: AirPodsViewModel, name: String, navController: NavController) {
val isDarkTheme = isSystemInDarkTheme()
val textColor = if (isDarkTheme) Color.White else Color.Black
@@ -124,20 +125,20 @@ fun LongPress(viewModel: AirPodsViewModel, name: String) {
Spacer(modifier = Modifier.height(24.dp))
StyledButton(
onClick = {
- viewModel.purchase(context)
+ navController.navigate("purchase_screen")
},
backdrop = rememberLayerBackdrop(),
modifier = Modifier.fillMaxWidth(),
maxScale = 0.05f,
- tint = Color(0xFF916100)
+ tint = if (isSystemInDarkTheme()) Color(0xFF916100) else Color(0xFFE59900)
) {
Text(
- stringResource(R.string.unlock_all_features),
+ stringResource(R.string.unlock_advanced_features),
style = TextStyle(
fontSize = 16.sp,
fontWeight = FontWeight.Medium,
fontFamily = FontFamily(Font(R.font.sf_pro)),
- color = textColor
+ color = Color.White
),
)
}
@@ -152,6 +153,7 @@ fun LongPress(viewModel: AirPodsViewModel, name: String) {
fontSize = 14.sp,
fontWeight = FontWeight.Bold,
color = textColor.copy(alpha = 0.6f),
+ fontFamily = FontFamily(Font(R.font.sf_pro))
),
fontFamily = FontFamily(Font(R.font.sf_pro)),
modifier = Modifier
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
new file mode 100644
index 0000000..5655843
--- /dev/null
+++ b/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/PurchaseScreen.kt
@@ -0,0 +1,496 @@
+/*
+ LibrePods - AirPods liberated from Apple’s ecosystem
+ Copyright (C) 2025 LibrePods contributors
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see .
+*/
+
+package me.kavishdevar.librepods.presentation.screens
+
+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.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.foundation.rememberScrollState
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material3.HorizontalDivider
+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.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.font.Font
+import androidx.compose.ui.text.font.FontFamily
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import androidx.lifecycle.viewmodel.compose.viewModel
+import androidx.navigation.NavController
+import com.kyant.backdrop.backdrops.layerBackdrop
+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.StyledButton
+import me.kavishdevar.librepods.presentation.components.StyledScaffold
+import me.kavishdevar.librepods.presentation.viewmodel.PurchaseViewModel
+
+@Composable
+fun PurchaseScreen(
+ viewModel: PurchaseViewModel = viewModel(),
+ navController: NavController
+) {
+ val context = LocalContext.current
+ val scrollState = rememberScrollState()
+ val state by viewModel.uiState.collectAsState()
+
+ val backdrop = rememberLayerBackdrop()
+
+ StyledScaffold(
+ title = stringResource(R.string.unlock_advanced_features)
+ ) { topPadding, hazeState, bottomPadding ->
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .layerBackdrop(backdrop)
+ .hazeSource(state = hazeState)
+ .verticalScroll(scrollState)
+ .padding(horizontal = 16.dp)
+ ) {
+ Spacer(modifier = Modifier.height(topPadding))
+
+ val isDarkTheme = isSystemInDarkTheme()
+ val backgroundColor = if (isDarkTheme) Color(0xFF000000) else Color(0xFFF2F2F7)
+ val cardBackgroundColor = if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFFFFFFF)
+ val textColor = if (isDarkTheme) Color.White else Color.Black
+ LaunchedEffect(state.isPremium) {
+ if (state.isPremium) {
+ navController.popBackStack()
+ }
+ }
+ if (!state.isPremium) {
+ Box(
+ modifier = Modifier
+ .background(backgroundColor)
+ .padding(horizontal = 16.dp, vertical = 4.dp)
+ ) {
+ Text(
+ text = "Free features",
+ style = TextStyle(
+ fontSize = 14.sp,
+ fontWeight = FontWeight.Bold,
+ color = textColor.copy(alpha = 0.6f),
+ fontFamily = FontFamily(Font(R.font.sf_pro))
+ )
+ )
+ }
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .background(cardBackgroundColor, RoundedCornerShape(28.dp))
+ .padding(horizontal = 8.dp, vertical = 16.dp),
+ verticalArrangement = Arrangement.spacedBy(8.dp)
+ ) {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 12.dp, vertical = 4.dp),
+ verticalArrangement = Arrangement.spacedBy(2.dp)
+ ) {
+ Text(
+ text = stringResource(R.string.ear_detection),
+ style = TextStyle(
+ fontSize = 16.sp,
+ fontWeight = FontWeight.Normal,
+ fontFamily = FontFamily(Font(R.font.sf_pro)),
+ color = textColor
+ )
+ )
+ Text(
+ text = stringResource(R.string.ear_detection_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()
+ .padding(horizontal = 12.dp, vertical = 4.dp),
+ verticalArrangement = Arrangement.spacedBy(2.dp)
+ ) {
+ Text(
+ text = stringResource(R.string.battery),
+ style = TextStyle(
+ fontSize = 16.sp,
+ fontWeight = FontWeight.Normal,
+ fontFamily = FontFamily(Font(R.font.sf_pro)),
+ color = textColor
+ )
+ )
+ Text(
+ text = stringResource(R.string.battery_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()
+ .padding(horizontal = 12.dp, vertical = 4.dp),
+ verticalArrangement = Arrangement.spacedBy(2.dp)
+ ) {
+ Text(
+ text = stringResource(R.string.noise_control),
+ style = TextStyle(
+ fontSize = 16.sp,
+ fontWeight = FontWeight.Normal,
+ fontFamily = FontFamily(Font(R.font.sf_pro)),
+ color = textColor
+ )
+ )
+ Text(
+ text = stringResource(R.string.noise_control_description),
+ style = TextStyle(
+ fontSize = 12.sp,
+ color = textColor.copy(0.6f),
+ fontFamily = FontFamily(Font(R.font.sf_pro)),
+ )
+ )
+ }
+ if (BuildConfig.FLAVOR == "xposed") {
+ HorizontalDivider(
+ thickness = 1.dp,
+ color = Color(0x40888888),
+ 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.hearing_aid) + " (" + stringResource(
+ R.string.requires_xposed
+ ) + ")",
+ style = TextStyle(
+ fontSize = 16.sp,
+ fontWeight = FontWeight.Normal,
+ fontFamily = FontFamily(Font(R.font.sf_pro)),
+ color = textColor
+ )
+ )
+ Text(
+ text = stringResource(R.string.hearing_aid_description).split("\n\n")[0],
+ style = TextStyle(
+ fontSize = 12.sp,
+ color = textColor.copy(0.6f),
+ fontFamily = FontFamily(Font(R.font.sf_pro)),
+ )
+ )
+ }
+ }
+ }
+
+ Spacer(modifier = Modifier.height(24.dp))
+ Box(
+ modifier = Modifier
+ .background(backgroundColor)
+ .padding(horizontal = 16.dp, vertical = 4.dp)
+ ) {
+ Text(
+ text = "Advanced features",
+ style = TextStyle(
+ fontSize = 14.sp,
+ fontWeight = FontWeight.Bold,
+ color = textColor.copy(alpha = 0.6f),
+ fontFamily = FontFamily(Font(R.font.sf_pro))
+ )
+ )
+ }
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .background(cardBackgroundColor, RoundedCornerShape(28.dp))
+ .padding(horizontal = 8.dp, vertical = 16.dp),
+ verticalArrangement = Arrangement.spacedBy(8.dp)
+ ) {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 12.dp, vertical = 4.dp),
+ verticalArrangement = Arrangement.spacedBy(2.dp)
+ ) {
+ Text(
+ text = stringResource(R.string.conversational_awareness),
+ style = TextStyle(
+ fontSize = 16.sp,
+ fontWeight = FontWeight.Normal,
+ fontFamily = FontFamily(Font(R.font.sf_pro)),
+ color = textColor
+ )
+ )
+ Text(
+ text = stringResource(R.string.conversational_awareness_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()
+ .padding(horizontal = 12.dp, vertical = 4.dp),
+ verticalArrangement = Arrangement.spacedBy(2.dp)
+ ) {
+ Text(
+ text = stringResource(R.string.head_gestures),
+ style = TextStyle(
+ fontSize = 16.sp,
+ fontWeight = FontWeight.Normal,
+ fontFamily = FontFamily(Font(R.font.sf_pro)),
+ color = textColor
+ )
+ )
+ Text(
+ text = stringResource(R.string.head_gestures_details),
+ 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()
+ .padding(horizontal = 12.dp, vertical = 4.dp),
+ verticalArrangement = Arrangement.spacedBy(2.dp)
+ ) {
+ Text(
+ text = stringResource(R.string.advanced_device_settings),
+ style = TextStyle(
+ fontSize = 16.sp,
+ fontWeight = FontWeight.Normal,
+ fontFamily = FontFamily(Font(R.font.sf_pro)),
+ color = textColor
+ )
+ )
+ Text(
+ text = stringResource(R.string.advanced_device_settings_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()
+ .padding(horizontal = 12.dp, vertical = 4.dp),
+ verticalArrangement = Arrangement.spacedBy(2.dp)
+ ) {
+ Text(
+ text = stringResource(R.string.automatic_connection),
+ style = TextStyle(
+ fontSize = 16.sp,
+ fontWeight = FontWeight.Normal,
+ fontFamily = FontFamily(Font(R.font.sf_pro)),
+ color = textColor
+ )
+ )
+ Text(
+ text = stringResource(R.string.automatic_connection_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()
+ .padding(horizontal = 12.dp, vertical = 4.dp),
+ verticalArrangement = Arrangement.spacedBy(2.dp)
+ ) {
+ Text(
+ text = stringResource(R.string.customizations),
+ style = TextStyle(
+ fontSize = 16.sp,
+ fontWeight = FontWeight.Normal,
+ fontFamily = FontFamily(Font(R.font.sf_pro)),
+ color = textColor
+ )
+ )
+ Text(
+ text = stringResource(R.string.customizations_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()
+ .padding(horizontal = 12.dp, vertical = 4.dp),
+ verticalArrangement = Arrangement.spacedBy(2.dp)
+ ) {
+ Text(
+ text = stringResource(R.string.support_the_development),
+ style = TextStyle(
+ fontSize = 16.sp,
+ fontWeight = FontWeight.Normal,
+ fontFamily = FontFamily(Font(R.font.sf_pro)),
+ color = textColor
+ )
+ )
+ Text(
+ text = stringResource(R.string.support_development_description),
+ style = TextStyle(
+ fontSize = 12.sp,
+ color = textColor.copy(0.6f),
+ fontFamily = FontFamily(Font(R.font.sf_pro)),
+ )
+ )
+ }
+ }
+ Spacer(modifier = Modifier.height(8.dp))
+ Text(
+ text = stringResource(R.string.feature_availability_disclaimer),
+ modifier = Modifier.fillMaxWidth(),
+ style = TextStyle(
+ fontSize = 14.sp,
+ fontWeight = FontWeight.Light,
+ fontFamily = FontFamily(Font(R.font.sf_pro)),
+ color = textColor.copy(alpha = 0.6f),
+ textAlign = TextAlign.Center
+ ),
+ )
+
+
+ Spacer(modifier = Modifier.height(24.dp))
+
+ StyledButton(
+ onClick = {
+ viewModel.purchase(context)
+ },
+ backdrop = rememberLayerBackdrop(),
+ modifier = Modifier.fillMaxWidth(),
+ maxScale = 0.05f,
+ tint = if (isSystemInDarkTheme()) Color(0xFF916100) else Color(0xFFE59900)
+ ) {
+ Text(
+ stringResource(R.string.buy),
+ style = TextStyle(
+ fontSize = 16.sp,
+ fontWeight = FontWeight.Medium,
+ fontFamily = FontFamily(Font(R.font.sf_pro)),
+ color = Color.White
+ ),
+ )
+ }
+
+ Spacer(modifier = Modifier.height(8.dp))
+
+ StyledButton(
+ onClick = {
+ viewModel.restorePurchases()
+ },
+ backdrop = rememberLayerBackdrop(),
+ modifier = Modifier.fillMaxWidth(),
+ maxScale = 0.05f,
+ ) {
+ Text(
+ stringResource(R.string.restore_purchases),
+ style = TextStyle(
+ fontSize = 16.sp,
+ fontWeight = FontWeight.Medium,
+ fontFamily = FontFamily(Font(R.font.sf_pro)),
+ color = textColor
+ ),
+ )
+ }
+ }
+ Spacer(modifier = Modifier.height(bottomPadding))
+ }
+ }
+}
diff --git a/android/app/src/main/java/me/kavishdevar/librepods/screens/RenameScreen.kt b/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/RenameScreen.kt
similarity index 97%
rename from android/app/src/main/java/me/kavishdevar/librepods/screens/RenameScreen.kt
rename to android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/RenameScreen.kt
index 829bdd6..8fc352e 100644
--- a/android/app/src/main/java/me/kavishdevar/librepods/screens/RenameScreen.kt
+++ b/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/RenameScreen.kt
@@ -18,7 +18,7 @@
@file:OptIn(ExperimentalEncodingApi::class)
-package me.kavishdevar.librepods.screens
+package me.kavishdevar.librepods.presentation.screens
import android.content.Context
import androidx.compose.foundation.background
@@ -60,8 +60,8 @@ 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.composables.StyledScaffold
-import me.kavishdevar.librepods.viewmodel.AirPodsViewModel
+import me.kavishdevar.librepods.presentation.components.StyledScaffold
+import me.kavishdevar.librepods.presentation.viewmodel.AirPodsViewModel
import kotlin.io.encoding.ExperimentalEncodingApi
diff --git a/android/app/src/main/java/me/kavishdevar/librepods/screens/TransparencySettingsScreen.kt b/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/TransparencySettingsScreen.kt
similarity index 89%
rename from android/app/src/main/java/me/kavishdevar/librepods/screens/TransparencySettingsScreen.kt
rename to android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/TransparencySettingsScreen.kt
index fedb250..fd7a893 100644
--- a/android/app/src/main/java/me/kavishdevar/librepods/screens/TransparencySettingsScreen.kt
+++ b/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/TransparencySettingsScreen.kt
@@ -16,7 +16,7 @@
along with this program. If not, see .
*/
-package me.kavishdevar.librepods.screens
+package me.kavishdevar.librepods.presentation.screens
// import me.kavishdevar.librepods.utils.RadareOffsetFinder
import android.annotation.SuppressLint
@@ -42,7 +42,6 @@ import androidx.compose.material3.Slider
import androidx.compose.material3.SliderDefaults
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
@@ -68,15 +67,13 @@ import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
import kotlinx.coroutines.delay
import me.kavishdevar.librepods.BuildConfig
import me.kavishdevar.librepods.R
-import me.kavishdevar.librepods.composables.StyledScaffold
-import me.kavishdevar.librepods.composables.StyledSlider
-import me.kavishdevar.librepods.composables.StyledToggle
-import me.kavishdevar.librepods.services.ServiceManager
-import me.kavishdevar.librepods.utils.ATTHandles
-import me.kavishdevar.librepods.utils.TransparencySettings
-import me.kavishdevar.librepods.utils.parseTransparencySettingsResponse
-import me.kavishdevar.librepods.utils.sendTransparencySettings
-import me.kavishdevar.librepods.viewmodel.AirPodsViewModel
+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.data.TransparencySettings
+import me.kavishdevar.librepods.data.parseTransparencySettingsResponse
+import me.kavishdevar.librepods.data.sendTransparencySettings
+import me.kavishdevar.librepods.presentation.viewmodel.AirPodsViewModel
import java.io.IOException
import kotlin.io.encoding.ExperimentalEncodingApi
@@ -91,8 +88,6 @@ fun TransparencySettingsScreen(viewModel: AirPodsViewModel) {
val textColor = if (isDarkTheme) Color.White else Color.Black
val verticalScrollState = rememberScrollState()
- val attManager = ServiceManager.getService()?.attManager ?: return
-
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)
@@ -151,23 +146,6 @@ fun TransparencySettingsScreen(viewModel: AirPodsViewModel) {
)
}
- val transparencyListener = remember {
- object : (ByteArray) -> Unit {
- override fun invoke(value: ByteArray) {
- val parsed = parseTransparencySettingsResponse(value)
- enabled.value = parsed.enabled
- amplificationSliderValue.floatValue = parsed.netAmplification
- balanceSliderValue.floatValue = parsed.balance
- toneSliderValue.floatValue = parsed.leftTone
- ambientNoiseReductionSliderValue.floatValue =
- parsed.leftAmbientNoiseReduction
- conversationBoostEnabled.value = parsed.leftConversationBoost
- eq.value = parsed.leftEQ.copyOf()
- Log.d(TAG, "Updated transparency settings from notification")
- }
- }
- }
-
LaunchedEffect(
enabled.value,
amplificationSliderValue.floatValue,
@@ -211,18 +189,9 @@ fun TransparencySettingsScreen(viewModel: AirPodsViewModel) {
sendTransparencySettings(viewModel::setATTCharacteristicValue, transparencySettings.value)
}
- DisposableEffect(Unit) {
- onDispose {
- attManager.unregisterListener(ATTHandles.TRANSPARENCY, transparencyListener)
- }
- }
-
LaunchedEffect(Unit) {
Log.d(TAG, "Connecting to ATT...")
try {
- attManager.enableNotifications(ATTHandles.TRANSPARENCY)
- attManager.registerListener(ATTHandles.TRANSPARENCY, transparencyListener)
-
// If we have an AACP manager, prefer its EQ data to populate EQ controls first
try {
Log.d(TAG, "Found AACPManager, reading cached EQ data")
@@ -242,7 +211,7 @@ fun TransparencySettingsScreen(viewModel: AirPodsViewModel) {
for (attempt in 1..3) {
initialReadAttempts.intValue = attempt
try {
- val data = attManager.read(ATTHandles.TRANSPARENCY)
+ val data = state.transparencyData
parsedSettings = parseTransparencySettingsResponse(data = data)
Log.d(TAG, "Parsed settings on attempt $attempt")
} catch (e: Exception) {
@@ -275,8 +244,7 @@ fun TransparencySettingsScreen(viewModel: AirPodsViewModel) {
}
}
- // Only show transparency mode section if SDP offset is available
- if (BuildConfig.FLAVOR == "xposed") {
+ if (state.vendorIdHook) {
StyledToggle(
label = stringResource(R.string.transparency_mode),
checked = enabled.value,
diff --git a/android/app/src/main/java/me/kavishdevar/librepods/screens/TroubleshootingScreen.kt b/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/TroubleshootingScreen.kt
similarity index 99%
rename from android/app/src/main/java/me/kavishdevar/librepods/screens/TroubleshootingScreen.kt
rename to android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/TroubleshootingScreen.kt
index 9bec4bb..d919571 100644
--- a/android/app/src/main/java/me/kavishdevar/librepods/screens/TroubleshootingScreen.kt
+++ b/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/TroubleshootingScreen.kt
@@ -16,7 +16,7 @@
along with this program. If not, see .
*/
-package me.kavishdevar.librepods.screens
+package me.kavishdevar.librepods.presentation.screens
import android.content.Intent
import android.widget.Toast
@@ -94,7 +94,7 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import me.kavishdevar.librepods.R
-import me.kavishdevar.librepods.composables.StyledScaffold
+import me.kavishdevar.librepods.presentation.components.StyledScaffold
import me.kavishdevar.librepods.utils.LogCollector
import java.io.File
import java.text.SimpleDateFormat
diff --git a/android/app/src/main/java/me/kavishdevar/librepods/screens/UpdateHearingTestScreen.kt b/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/UpdateHearingTestScreen.kt
similarity index 97%
rename from android/app/src/main/java/me/kavishdevar/librepods/screens/UpdateHearingTestScreen.kt
rename to android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/UpdateHearingTestScreen.kt
index e8b6898..635c75b 100644
--- a/android/app/src/main/java/me/kavishdevar/librepods/screens/UpdateHearingTestScreen.kt
+++ b/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/UpdateHearingTestScreen.kt
@@ -16,7 +16,7 @@
along with this program. If not, see .
*/
-package me.kavishdevar.librepods.screens
+package me.kavishdevar.librepods.presentation.screens
import android.util.Log
import androidx.compose.foundation.isSystemInDarkTheme
@@ -59,12 +59,12 @@ import dev.chrisbanes.haze.hazeSource
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import me.kavishdevar.librepods.R
-import me.kavishdevar.librepods.composables.StyledScaffold
+import me.kavishdevar.librepods.presentation.components.StyledScaffold
import me.kavishdevar.librepods.services.ServiceManager
-import me.kavishdevar.librepods.utils.ATTHandles
-import me.kavishdevar.librepods.utils.HearingAidSettings
-import me.kavishdevar.librepods.utils.parseHearingAidSettingsResponse
-import me.kavishdevar.librepods.utils.sendHearingAidSettings
+import me.kavishdevar.librepods.bluetooth.ATTHandles
+import me.kavishdevar.librepods.data.HearingAidSettings
+import me.kavishdevar.librepods.data.parseHearingAidSettingsResponse
+import me.kavishdevar.librepods.data.sendHearingAidSettings
import java.io.IOException
private var debounceJob: MutableState = mutableStateOf(null)
diff --git a/android/app/src/main/java/me/kavishdevar/librepods/screens/VersionInfoScreen.kt b/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/VersionInfoScreen.kt
similarity index 95%
rename from android/app/src/main/java/me/kavishdevar/librepods/screens/VersionInfoScreen.kt
rename to android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/VersionInfoScreen.kt
index feadafd..2669587 100644
--- a/android/app/src/main/java/me/kavishdevar/librepods/screens/VersionInfoScreen.kt
+++ b/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/VersionInfoScreen.kt
@@ -16,7 +16,7 @@
along with this program. If not, see .
*/
-package me.kavishdevar.librepods.screens
+package me.kavishdevar.librepods.presentation.screens
import androidx.compose.foundation.background
import androidx.compose.foundation.isSystemInDarkTheme
@@ -48,8 +48,8 @@ import androidx.compose.ui.unit.sp
import com.kyant.backdrop.backdrops.layerBackdrop
import com.kyant.backdrop.backdrops.rememberLayerBackdrop
import me.kavishdevar.librepods.R
-import me.kavishdevar.librepods.composables.StyledScaffold
-import me.kavishdevar.librepods.viewmodel.AirPodsViewModel
+import me.kavishdevar.librepods.presentation.components.StyledScaffold
+import me.kavishdevar.librepods.presentation.viewmodel.AirPodsViewModel
@Composable
fun VersionScreen(viewModel: AirPodsViewModel) {
@@ -80,7 +80,8 @@ fun VersionScreen(viewModel: AirPodsViewModel) {
style = TextStyle(
fontSize = 14.sp,
fontWeight = FontWeight.Bold,
- color = textColor.copy(alpha = 0.6f)
+ color = textColor.copy(alpha = 0.6f),
+ fontFamily = FontFamily(Font(R.font.sf_pro))
)
)
}
diff --git a/android/app/src/main/java/me/kavishdevar/librepods/ui/theme/Color.kt b/android/app/src/main/java/me/kavishdevar/librepods/presentation/theme/Color.kt
similarity index 92%
rename from android/app/src/main/java/me/kavishdevar/librepods/ui/theme/Color.kt
rename to android/app/src/main/java/me/kavishdevar/librepods/presentation/theme/Color.kt
index 808d951..f2e44c1 100644
--- a/android/app/src/main/java/me/kavishdevar/librepods/ui/theme/Color.kt
+++ b/android/app/src/main/java/me/kavishdevar/librepods/presentation/theme/Color.kt
@@ -17,7 +17,7 @@
*/
-package me.kavishdevar.librepods.ui.theme
+package me.kavishdevar.librepods.presentation.theme
import androidx.compose.ui.graphics.Color
@@ -27,4 +27,4 @@ val Pink80 = Color(0xFFEFB8C8)
val Purple40 = Color(0xFF6650a4)
val PurpleGrey40 = Color(0xFF625b71)
-val Pink40 = Color(0xFF7D5260)
\ No newline at end of file
+val Pink40 = Color(0xFF7D5260)
diff --git a/android/app/src/main/java/me/kavishdevar/librepods/ui/theme/Theme.kt b/android/app/src/main/java/me/kavishdevar/librepods/presentation/theme/Theme.kt
similarity index 97%
rename from android/app/src/main/java/me/kavishdevar/librepods/ui/theme/Theme.kt
rename to android/app/src/main/java/me/kavishdevar/librepods/presentation/theme/Theme.kt
index cd96f1f..04225cc 100644
--- a/android/app/src/main/java/me/kavishdevar/librepods/ui/theme/Theme.kt
+++ b/android/app/src/main/java/me/kavishdevar/librepods/presentation/theme/Theme.kt
@@ -16,7 +16,7 @@
along with this program. If not, see .
*/
-package me.kavishdevar.librepods.ui.theme
+package me.kavishdevar.librepods.presentation.theme
import android.os.Build
import androidx.compose.foundation.isSystemInDarkTheme
diff --git a/android/app/src/main/java/me/kavishdevar/librepods/ui/theme/Type.kt b/android/app/src/main/java/me/kavishdevar/librepods/presentation/theme/Type.kt
similarity index 96%
rename from android/app/src/main/java/me/kavishdevar/librepods/ui/theme/Type.kt
rename to android/app/src/main/java/me/kavishdevar/librepods/presentation/theme/Type.kt
index 80a67aa..72a4424 100644
--- a/android/app/src/main/java/me/kavishdevar/librepods/ui/theme/Type.kt
+++ b/android/app/src/main/java/me/kavishdevar/librepods/presentation/theme/Type.kt
@@ -16,7 +16,7 @@
along with this program. If not, see .
*/
-package me.kavishdevar.librepods.ui.theme
+package me.kavishdevar.librepods.presentation.theme
import androidx.compose.material3.Typography
import androidx.compose.ui.text.TextStyle
@@ -49,4 +49,4 @@ val Typography = Typography(
letterSpacing = 0.5.sp
)
*/
-)
\ No newline at end of file
+)
diff --git a/android/app/src/main/java/me/kavishdevar/librepods/viewmodel/AirPodsViewModel.kt b/android/app/src/main/java/me/kavishdevar/librepods/presentation/viewmodel/AirPodsViewModel.kt
similarity index 73%
rename from android/app/src/main/java/me/kavishdevar/librepods/viewmodel/AirPodsViewModel.kt
rename to android/app/src/main/java/me/kavishdevar/librepods/presentation/viewmodel/AirPodsViewModel.kt
index 3b5d34a..adc527a 100644
--- a/android/app/src/main/java/me/kavishdevar/librepods/viewmodel/AirPodsViewModel.kt
+++ b/android/app/src/main/java/me/kavishdevar/librepods/presentation/viewmodel/AirPodsViewModel.kt
@@ -16,9 +16,8 @@
along with this program. If not, see .
*/
-package me.kavishdevar.librepods.viewmodel
+package me.kavishdevar.librepods.presentation.viewmodel
-import android.app.Activity
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
@@ -28,25 +27,29 @@ import android.util.Log
import androidx.core.content.edit
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableSharedFlow
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.constants.AirPodsNotifications
-import me.kavishdevar.librepods.constants.Battery
-import me.kavishdevar.librepods.constants.BatteryComponent
-import me.kavishdevar.librepods.constants.BatteryStatus
-import me.kavishdevar.librepods.constants.StemAction
+import me.kavishdevar.librepods.bluetooth.AACPManager
+import me.kavishdevar.librepods.bluetooth.AACPManager.Companion.ControlCommandIdentifiers
+import me.kavishdevar.librepods.bluetooth.ATTHandles
+import me.kavishdevar.librepods.data.AirPodsInstance
+import me.kavishdevar.librepods.data.AirPodsModels
+import me.kavishdevar.librepods.data.AirPodsNotifications
+import me.kavishdevar.librepods.data.Battery
+import me.kavishdevar.librepods.data.BatteryComponent
+import me.kavishdevar.librepods.data.BatteryStatus
+import me.kavishdevar.librepods.data.Capability
import me.kavishdevar.librepods.data.ControlCommandRepository
+import me.kavishdevar.librepods.data.StemAction
+import me.kavishdevar.librepods.data.XposedRemotePrefProvider
import me.kavishdevar.librepods.services.AirPodsService
-import me.kavishdevar.librepods.utils.AACPManager
-import me.kavishdevar.librepods.utils.AACPManager.Companion.ControlCommandIdentifiers
-import me.kavishdevar.librepods.utils.ATTHandles
-import me.kavishdevar.librepods.utils.AirPodsInstance
-import me.kavishdevar.librepods.utils.AirPodsModels
-import me.kavishdevar.librepods.utils.Capability
@Suppress("ArrayInDataClass")
data class AirPodsUiState(
@@ -81,7 +84,12 @@ data class AirPodsUiState(
val leftAction: StemAction = StemAction.CYCLE_NOISE_CONTROL_MODES,
val rightAction: StemAction = StemAction.CYCLE_NOISE_CONTROL_MODES,
+ val loudSoundReductionEnabled: Boolean = false,
+ val transparencyData: ByteArray = byteArrayOf(),
+ val hearingAidData: ByteArray = byteArrayOf(),
+
val isPremium: Boolean = false,
+ val vendorIdHook: Boolean = false
)
class AirPodsViewModel(
@@ -90,8 +98,14 @@ class AirPodsViewModel(
private val controlRepo: ControlCommandRepository,
private val appContext: Context
) : ViewModel() {
-
- private val _uiState = MutableStateFlow(AirPodsUiState(deviceName = sharedPreferences.getString("name", "AirPods Pro") ?: "AirPods Pro"))
+ private val _uiState = MutableStateFlow(
+ AirPodsUiState(
+ deviceName = sharedPreferences.getString(
+ "name",
+ "AirPods Pro"
+ ) ?: "AirPods Pro"
+ )
+ )
val uiState: StateFlow = _uiState
private var isDemoMode = false
@@ -99,17 +113,16 @@ class AirPodsViewModel(
private var billingFirstCollectDone = false
- private val listeners = mutableMapOf<
- ControlCommandIdentifiers,
- AACPManager.ControlCommandListener
- >()
+ private val listeners =
+ mutableMapOf()
+
+ private val xposedRemotePref = XposedRemotePrefProvider.create()
private lateinit var broadcastReceiver: BroadcastReceiver
private val _cameraAction = MutableStateFlow(
sharedPreferences.getString("camera_action", null)
- ?.let { value -> AACPManager.Companion.StemPressType.entries.find { it.name == value } }
- )
+ ?.let { value -> AACPManager.Companion.StemPressType.entries.find { it.name == value } })
val cameraAction: StateFlow = _cameraAction
@@ -129,6 +142,7 @@ class AirPodsViewModel(
setupControlObservers()
observeBilling()
loadControlList()
+ observeATT()
if (isDemoMode) activateDemoMode()
}
@@ -148,7 +162,9 @@ class AirPodsViewModel(
}
private fun observeBilling() {
- if (!isDemoMode) viewModelScope.launch {
+ if (isDemoMode) return
+ viewModelScope.launch {
+ if (!BuildConfig.PLAY_BUILD) billingFirstCollectDone = true // FOSS doesn't send multiple events
BillingManager.provider.isPremium.collect { premium ->
if (!billingFirstCollectDone) {
billingFirstCollectDone = true
@@ -156,7 +172,10 @@ class AirPodsViewModel(
}
if (!premium) {
Log.d("AirPodsViewModel", "we are not premium")
- setControlCommandBoolean(ControlCommandIdentifiers.CONVERSATION_DETECT_CONFIG, false)
+ setControlCommandBoolean(
+ ControlCommandIdentifiers.CONVERSATION_DETECT_CONFIG,
+ false
+ )
setHeadGesturesEnabled(false)
} else {
Log.d("AirPodsViewModel", "we are premium")
@@ -183,7 +202,8 @@ class AirPodsViewModel(
}
AirPodsNotifications.BATTERY_DATA -> {
- val data = intent.getParcelableArrayListExtra("data", Battery::class.java)?.toList() ?: emptyList()
+ val data = intent.getParcelableArrayListExtra("data", Battery::class.java)
+ ?.toList() ?: emptyList()
_uiState.update {
it.copy(battery = data)
}
@@ -213,15 +233,12 @@ class AirPodsViewModel(
}
appContext.registerReceiver(
- broadcastReceiver,
- filter,
- Context.RECEIVER_NOT_EXPORTED
+ broadcastReceiver, filter, Context.RECEIVER_NOT_EXPORTED
)
}
fun setControlCommandValue(
- identifier: ControlCommandIdentifiers,
- value: ByteArray
+ identifier: ControlCommandIdentifiers, value: ByteArray
) {
if (!isDemoMode) controlRepo.setValue(identifier, value)
_uiState.update {
@@ -232,25 +249,21 @@ class AirPodsViewModel(
}
fun setControlCommandBoolean(
- identifier: ControlCommandIdentifiers,
- enabled: Boolean
+ identifier: ControlCommandIdentifiers, enabled: Boolean
) {
setControlCommandValue(
- identifier,
- if (enabled) byteArrayOf(0x01) else byteArrayOf(0x02)
+ identifier, if (enabled) byteArrayOf(0x01) else byteArrayOf(0x02)
)
}
fun setControlCommandInt(
- identifier: ControlCommandIdentifiers,
- value: Int
+ identifier: ControlCommandIdentifiers, value: Int
) {
setControlCommandValue(identifier, byteArrayOf(value.toByte()))
}
fun setControlCommandByte(
- identifier: ControlCommandIdentifiers,
- value: Byte
+ identifier: ControlCommandIdentifiers, value: Byte
) {
setControlCommandValue(identifier, byteArrayOf(value))
}
@@ -267,7 +280,7 @@ class AirPodsViewModel(
}
}
- listeners[identifier] = listener
+ listeners[identifier] = listener as AACPManager.ControlCommandListener
}
// I'm lazy, sorry.
@@ -309,8 +322,7 @@ class AirPodsViewModel(
service.let { service ->
_uiState.update {
it.copy(
- isLocallyConnected = service.isConnected(),
- battery = service.getBattery()
+ isLocallyConnected = service.isConnected(), battery = service.getBattery()
)
}
}
@@ -318,11 +330,24 @@ class AirPodsViewModel(
private fun loadSharedPreferences() {
val offListeningModeEnabled = sharedPreferences.getBoolean("off_listening_mode", true)
- val automaticEarDetectionEnabled = sharedPreferences.getBoolean("automatic_ear_detection", true)
- val automaticConnectionEnabled = sharedPreferences.getBoolean("automatic_connection_ctrl_cmd", true)
+ val automaticEarDetectionEnabled =
+ sharedPreferences.getBoolean("automatic_ear_detection", true)
+ val automaticConnectionEnabled =
+ sharedPreferences.getBoolean("automatic_connection_ctrl_cmd", true)
val headGesturesEnabled = sharedPreferences.getBoolean("head_gestures", true)
- val leftAction = StemAction.valueOf(sharedPreferences.getString("left_long_press_action", "CYCLE_NOISE_CONTROL_MODES") ?: "CYCLE_NOISE_CONTROL_MODES")
- val rightAction = StemAction.valueOf(sharedPreferences.getString("right_long_press_action", "CYCLE_NOISE_CONTROL_MODES") ?: "CYCLE_NOISE_CONTROL_MODES")
+ val leftAction = StemAction.valueOf(
+ sharedPreferences.getString(
+ "left_long_press_action",
+ "CYCLE_NOISE_CONTROL_MODES"
+ ) ?: "CYCLE_NOISE_CONTROL_MODES"
+ )
+ val rightAction = StemAction.valueOf(
+ sharedPreferences.getString(
+ "right_long_press_action",
+ "CYCLE_NOISE_CONTROL_MODES"
+ ) ?: "CYCLE_NOISE_CONTROL_MODES"
+ )
+ val vendorIdHook = xposedRemotePref.getBoolean("vendor_id_hook", false)
_uiState.update {
it.copy(
@@ -331,7 +356,8 @@ class AirPodsViewModel(
automaticConnectionEnabled = automaticConnectionEnabled,
headGesturesEnabled = headGesturesEnabled,
leftAction = leftAction,
- rightAction = rightAction
+ rightAction = rightAction,
+ vendorIdHook = vendorIdHook
)
}
}
@@ -365,14 +391,12 @@ class AirPodsViewModel(
name = "AirPods",
model = AirPodsModels.getModelByModelNumber("A3049")!!,
actualModelNumber = "A3049",
- aacpManager = service.aacpManager,
serialNumber = null,
leftSerialNumber = null,
rightSerialNumber = null,
version1 = null,
version2 = null,
version3 = null,
- attManager = null
)
_uiState.update {
@@ -381,7 +405,11 @@ class AirPodsViewModel(
instance = instance,
modelName = instance.model.displayName,
actualModel = instance.actualModelNumber,
- serialNumbers = listOf(instance.serialNumber ?: "", instance.leftSerialNumber ?: "", instance.rightSerialNumber ?: ""),
+ serialNumbers = listOf(
+ instance.serialNumber ?: "",
+ instance.leftSerialNumber ?: "",
+ instance.rightSerialNumber ?: ""
+ ),
version1 = instance.version1 ?: "",
version2 = instance.version2 ?: "",
version3 = instance.version3 ?: ""
@@ -408,11 +436,42 @@ class AirPodsViewModel(
}
fun setATTCharacteristicValue(handle: ATTHandles, value: ByteArray) {
- service.attManager?.write(handle, value)
+ if (handle == ATTHandles.LOUD_SOUND_REDUCTION) {
+ _uiState.update { it.copy(loudSoundReductionEnabled = value[0].toInt() == 0x01) }
+ }
+ viewModelScope.launch(Dispatchers.IO) {
+ service.attManager?.write(handle, value)
+ }
}
- fun getATTCharacteristicValue(handle: ATTHandles): ByteArray? {
- return service.attManager?.read(handle)
+ fun refreshATT() {
+ viewModelScope.launch(Dispatchers.IO) {
+ val loudSoundReduction =
+ runCatching { service.attManager?.read(ATTHandles.LOUD_SOUND_REDUCTION) }.getOrNull()
+ val transparencyData =
+ runCatching { service.attManager?.read(ATTHandles.TRANSPARENCY) }.getOrNull()?: byteArrayOf()
+ val hearingAid =
+ runCatching { service.attManager?.read(ATTHandles.HEARING_AID) }.getOrNull()?: byteArrayOf()
+ _uiState.value = _uiState.value.copy(
+ loudSoundReductionEnabled = loudSoundReduction?.get(0)?.toInt() == 0x01,
+ transparencyData = transparencyData,
+ hearingAidData = hearingAid
+ )
+ }
+ }
+
+ fun observeATT() {
+ viewModelScope.launch(Dispatchers.IO) {
+ service.attManager?.connect()
+ service.attManager?.enableNotifications(ATTHandles.LOUD_SOUND_REDUCTION)
+ service.attManager?.enableNotifications(ATTHandles.TRANSPARENCY)
+ service.attManager?.enableNotifications(ATTHandles.HEARING_AID)
+
+ while (true) {
+ refreshATT()
+ delay(10000)
+ }
+ }
}
fun setAutomaticEarDetectionEnabled(enabled: Boolean) {
@@ -435,9 +494,9 @@ class AirPodsViewModel(
}
}
- fun purchase(context: Context) {
- BillingManager.provider.purchase(context as Activity)
- }
+// fun purchase(context: Context) {
+// BillingManager.provider.purchase(context as Activity)
+// }
fun activateDemoMode() {
isDemoMode = true
@@ -448,14 +507,12 @@ class AirPodsViewModel(
name = "AirPods Pro (Demo)",
model = AirPodsModels.getModelByModelNumber("A3049")!!,
actualModelNumber = "A3049",
- aacpManager = service.aacpManager,
serialNumber = "DEMO123",
leftSerialNumber = "L-DEMO",
rightSerialNumber = "R-DEMO",
version1 = "1.0",
version2 = "1.0",
version3 = "1.0",
- attManager = null
)
_uiState.update {
diff --git a/android/app/src/main/java/me/kavishdevar/librepods/viewmodel/AppSettingsViewModel.kt b/android/app/src/main/java/me/kavishdevar/librepods/presentation/viewmodel/AppSettingsViewModel.kt
similarity index 89%
rename from android/app/src/main/java/me/kavishdevar/librepods/viewmodel/AppSettingsViewModel.kt
rename to android/app/src/main/java/me/kavishdevar/librepods/presentation/viewmodel/AppSettingsViewModel.kt
index 5508a65..304db98 100644
--- a/android/app/src/main/java/me/kavishdevar/librepods/viewmodel/AppSettingsViewModel.kt
+++ b/android/app/src/main/java/me/kavishdevar/librepods/presentation/viewmodel/AppSettingsViewModel.kt
@@ -1,6 +1,5 @@
-package me.kavishdevar.librepods.viewmodel
+package me.kavishdevar.librepods.presentation.viewmodel
-import android.app.Activity
import android.app.Application
import android.content.Context
import androidx.core.content.edit
@@ -10,7 +9,10 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
+import me.kavishdevar.librepods.BuildConfig
import me.kavishdevar.librepods.billing.BillingManager
+import me.kavishdevar.librepods.data.XposedRemotePrefProvider
+import me.kavishdevar.librepods.utils.NativeBridge
import kotlin.math.roundToInt
data class AppSettingsUiState(
@@ -29,6 +31,7 @@ data class AppSettingsUiState(
val showCameraDialog: Boolean = false,
val cameraPackageValue: String = "",
val cameraPackageError: String? = null,
+ val vendorIdHook: Boolean = false,
val isPremium: Boolean = false
)
@@ -38,6 +41,8 @@ class AppSettingsViewModel(application: Application) : AndroidViewModel(applicat
private val _uiState = MutableStateFlow(AppSettingsUiState())
val uiState = _uiState.asStateFlow()
+ private val xposedRemotePref = XposedRemotePrefProvider.create()
+
init {
loadSettings()
observeBilling()
@@ -66,9 +71,13 @@ class AppSettingsViewModel(application: Application) : AndroidViewModel(applicat
takeoverWhenMediaStart = sharedPreferences.getBoolean("takeover_when_media_start", false),
useAlternateHeadTrackingPackets = sharedPreferences.getBoolean("use_alternate_head_tracking_packets", true),
conversationalAwarenessVolume = sharedPreferences.getInt("conversational_awareness_volume", 43).toFloat(),
- cameraPackageValue = sharedPreferences.getString("custom_camera_package", "") ?: ""
+ cameraPackageValue = sharedPreferences.getString("custom_camera_package", "") ?: "",
+ vendorIdHook = xposedRemotePref.getBoolean("vendor_id_hook", false)
)
}
+ if (BuildConfig.FLAVOR == "xposed") {
+ NativeBridge.setSdpHook(_uiState.value.vendorIdHook)
+ }
}
fun setShowPhoneBatteryInWidget(enabled: Boolean) {
@@ -152,7 +161,9 @@ class AppSettingsViewModel(application: Application) : AndroidViewModel(applicat
setShowCameraDialog(false)
}
- fun purchase(context: Context) {
- BillingManager.provider.purchase(context as Activity)
+ fun setVendorIdHook(enabled: Boolean) {
+ NativeBridge.setSdpHook(enabled)
+ xposedRemotePref.putBoolean("vendor_id_hook", enabled)
+ _uiState.update { it.copy(vendorIdHook = enabled) }
}
}
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
new file mode 100644
index 0000000..3ea9649
--- /dev/null
+++ b/android/app/src/main/java/me/kavishdevar/librepods/presentation/viewmodel/PurchaseViewModel.kt
@@ -0,0 +1,47 @@
+package me.kavishdevar.librepods.presentation.viewmodel
+
+import android.app.Activity
+import android.app.Application
+import android.content.Context
+import androidx.lifecycle.AndroidViewModel
+import androidx.lifecycle.viewModelScope
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.update
+import kotlinx.coroutines.launch
+import me.kavishdevar.librepods.billing.BillingManager
+
+data class PurchaseUiState(
+ val isPremium: Boolean = false,
+ val price: String = ""
+)
+
+class PurchaseViewModel(application: Application) : AndroidViewModel(application) {
+ private val _uiState = MutableStateFlow(PurchaseUiState())
+ val uiState = _uiState.asStateFlow()
+
+ init {
+ observeBilling()
+ }
+
+ private fun observeBilling() {
+ viewModelScope.launch {
+ BillingManager.provider.isPremium.collect { premium ->
+ _uiState.update { it.copy(isPremium = premium) }
+ }
+ }
+ viewModelScope.launch {
+ BillingManager.provider.price.collect { price ->
+ _uiState.update { it.copy(price = price) }
+ }
+ }
+ }
+
+ fun purchase(context: Context) {
+ BillingManager.provider.purchase(context as Activity)
+ }
+
+ fun restorePurchases() {
+ BillingManager.provider.queryPurchases()
+ }
+}
diff --git a/android/app/src/main/java/me/kavishdevar/librepods/widgets/BatteryWidget.kt b/android/app/src/main/java/me/kavishdevar/librepods/presentation/widgets/BatteryWidget.kt
similarity index 95%
rename from android/app/src/main/java/me/kavishdevar/librepods/widgets/BatteryWidget.kt
rename to android/app/src/main/java/me/kavishdevar/librepods/presentation/widgets/BatteryWidget.kt
index a926e79..20a12d5 100644
--- a/android/app/src/main/java/me/kavishdevar/librepods/widgets/BatteryWidget.kt
+++ b/android/app/src/main/java/me/kavishdevar/librepods/presentation/widgets/BatteryWidget.kt
@@ -18,7 +18,7 @@
@file:OptIn(ExperimentalEncodingApi::class)
-package me.kavishdevar.librepods.widgets
+package me.kavishdevar.librepods.presentation.widgets
import android.appwidget.AppWidgetManager
import android.appwidget.AppWidgetProvider
diff --git a/android/app/src/main/java/me/kavishdevar/librepods/widgets/NoiseControlWidget.kt b/android/app/src/main/java/me/kavishdevar/librepods/presentation/widgets/NoiseControlWidget.kt
similarity index 97%
rename from android/app/src/main/java/me/kavishdevar/librepods/widgets/NoiseControlWidget.kt
rename to android/app/src/main/java/me/kavishdevar/librepods/presentation/widgets/NoiseControlWidget.kt
index 9d32f16..17ec190 100644
--- a/android/app/src/main/java/me/kavishdevar/librepods/widgets/NoiseControlWidget.kt
+++ b/android/app/src/main/java/me/kavishdevar/librepods/presentation/widgets/NoiseControlWidget.kt
@@ -18,7 +18,7 @@
@file:OptIn(ExperimentalEncodingApi::class)
-package me.kavishdevar.librepods.widgets
+package me.kavishdevar.librepods.presentation.widgets
import android.app.PendingIntent
import android.appwidget.AppWidgetManager
@@ -29,7 +29,7 @@ import android.util.Log
import android.widget.RemoteViews
import me.kavishdevar.librepods.R
import me.kavishdevar.librepods.services.ServiceManager
-import me.kavishdevar.librepods.utils.AACPManager
+import me.kavishdevar.librepods.bluetooth.AACPManager
import kotlin.io.encoding.ExperimentalEncodingApi
class NoiseControlWidget : AppWidgetProvider() {
diff --git a/android/app/src/main/java/me/kavishdevar/librepods/services/AirPodsQSService.kt b/android/app/src/main/java/me/kavishdevar/librepods/services/AirPodsQSService.kt
index 4d20339..8514659 100644
--- a/android/app/src/main/java/me/kavishdevar/librepods/services/AirPodsQSService.kt
+++ b/android/app/src/main/java/me/kavishdevar/librepods/services/AirPodsQSService.kt
@@ -35,9 +35,9 @@ import android.util.Log
import androidx.annotation.RequiresApi
import me.kavishdevar.librepods.QuickSettingsDialogActivity
import me.kavishdevar.librepods.R
-import me.kavishdevar.librepods.constants.AirPodsNotifications
-import me.kavishdevar.librepods.constants.NoiseControlMode
-import me.kavishdevar.librepods.utils.AACPManager
+import me.kavishdevar.librepods.data.AirPodsNotifications
+import me.kavishdevar.librepods.data.NoiseControlMode
+import me.kavishdevar.librepods.bluetooth.AACPManager
import kotlin.io.encoding.ExperimentalEncodingApi
@RequiresApi(Build.VERSION_CODES.Q)
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 b7b9b12..753e835 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
@@ -83,25 +83,28 @@ import kotlinx.coroutines.withTimeout
import me.kavishdevar.librepods.BuildConfig
import me.kavishdevar.librepods.MainActivity
import me.kavishdevar.librepods.R
-import me.kavishdevar.librepods.constants.AirPodsNotifications
-import me.kavishdevar.librepods.constants.Battery
-import me.kavishdevar.librepods.constants.BatteryComponent
-import me.kavishdevar.librepods.constants.BatteryStatus
-import me.kavishdevar.librepods.constants.StemAction
-import me.kavishdevar.librepods.constants.isHeadTrackingData
-import me.kavishdevar.librepods.utils.AACPManager
-import me.kavishdevar.librepods.utils.AACPManager.Companion.StemPressType
-import me.kavishdevar.librepods.utils.ATTManager
-import me.kavishdevar.librepods.utils.AirPodsInstance
-import me.kavishdevar.librepods.utils.AirPodsModels
-import me.kavishdevar.librepods.utils.BLEManager
-import me.kavishdevar.librepods.utils.BluetoothConnectionManager
+import me.kavishdevar.librepods.bluetooth.AACPManager
+import me.kavishdevar.librepods.bluetooth.AACPManager.Companion.StemPressType
+import me.kavishdevar.librepods.bluetooth.ATTManager
+import me.kavishdevar.librepods.bluetooth.BLEManager
+import me.kavishdevar.librepods.bluetooth.BluetoothConnectionManager
+import me.kavishdevar.librepods.data.AirPodsInstance
+import me.kavishdevar.librepods.data.AirPodsModels
+import me.kavishdevar.librepods.data.AirPodsNotifications
+import me.kavishdevar.librepods.data.Battery
+import me.kavishdevar.librepods.data.BatteryComponent
+import me.kavishdevar.librepods.data.BatteryStatus
+import me.kavishdevar.librepods.data.StemAction
+import me.kavishdevar.librepods.data.XposedRemotePrefProvider
+import me.kavishdevar.librepods.data.isHeadTrackingData
+import me.kavishdevar.librepods.presentation.overlays.IslandType
+import me.kavishdevar.librepods.presentation.overlays.IslandWindow
+import me.kavishdevar.librepods.presentation.overlays.PopupWindow
+import me.kavishdevar.librepods.presentation.widgets.BatteryWidget
+import me.kavishdevar.librepods.presentation.widgets.NoiseControlWidget
import me.kavishdevar.librepods.utils.GestureDetector
import me.kavishdevar.librepods.utils.HeadTracking
-import me.kavishdevar.librepods.utils.IslandType
-import me.kavishdevar.librepods.utils.IslandWindow
import me.kavishdevar.librepods.utils.MediaController
-import me.kavishdevar.librepods.utils.PopupWindow
import me.kavishdevar.librepods.utils.SystemApisUtils
import me.kavishdevar.librepods.utils.SystemApisUtils.DEVICE_TYPE_UNTETHERED_HEADSET
import me.kavishdevar.librepods.utils.SystemApisUtils.METADATA_COMPANION_APP
@@ -121,8 +124,6 @@ import me.kavishdevar.librepods.utils.SystemApisUtils.METADATA_UNTETHERED_RIGHT_
import me.kavishdevar.librepods.utils.SystemApisUtils.METADATA_UNTETHERED_RIGHT_CHARGING
import me.kavishdevar.librepods.utils.SystemApisUtils.METADATA_UNTETHERED_RIGHT_ICON
import me.kavishdevar.librepods.utils.SystemApisUtils.METADATA_UNTETHERED_RIGHT_LOW_BATTERY_THRESHOLD
-import me.kavishdevar.librepods.widgets.BatteryWidget
-import me.kavishdevar.librepods.widgets.NoiseControlWidget
import java.nio.ByteBuffer
import java.nio.ByteOrder
import kotlin.io.encoding.Base64
@@ -1060,8 +1061,6 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
version1 = config.airpodsVersion1,
version2 = config.airpodsVersion2,
version3 = config.airpodsVersion3,
- aacpManager = aacpManager,
- attManager = attManager
)
}
sendBroadcast(
@@ -1765,8 +1764,10 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
}
}
+ @Suppress("KotlinUnreachableCode")
@OptIn(ExperimentalMaterial3Api::class)
private fun showSocketConnectionFailureNotification(errorMessage: String) {
+ return // something causes too many notifications. turning off for now
if (BuildConfig.FLAVOR != "xposed") {
Log.w(
TAG,
@@ -1788,7 +1789,7 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
.setSmallIcon(R.drawable.airpods).setContentTitle("AirPods Connection Issue")
.setContentText("Unable to connect to AirPods over L2CAP").setStyle(
NotificationCompat.BigTextStyle().bigText(
- "Your AirPods are connected via Bluetooth, but LibrePods couldn't connect to AirPods using L2CAP. " + "Error: $errorMessage"
+ "Your AirPods are connected via Bluetooth, but LibrePods couldn't connect to AirPods using L2CAP. Error: $errorMessage"
)
).setContentIntent(pendingIntent).setCategory(Notification.CATEGORY_ERROR)
.setPriority(NotificationCompat.PRIORITY_HIGH).setAutoCancel(true).build()
@@ -2178,7 +2179,11 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
fun processHeadTrackingData(data: ByteArray) {
val horizontal = ByteBuffer.wrap(data, 51, 2).order(ByteOrder.LITTLE_ENDIAN).short.toInt()
val vertical = ByteBuffer.wrap(data, 53, 2).order(ByteOrder.LITTLE_ENDIAN).short.toInt()
- gestureDetector?.processHeadOrientation(horizontal, vertical)
+ try {
+ gestureDetector?.processHeadOrientation(horizontal, vertical)
+ } catch (e: Exception) {
+ Log.w(TAG, "gesture detector on ${data.toHexString()}: ${e.message}")
+ }
}
private lateinit var connectionReceiver: BroadcastReceiver
@@ -2666,8 +2671,8 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
this@AirPodsService.device = device
BluetoothConnectionManager.setCurrentConnection(socket, device)
-
- if (BuildConfig.FLAVOR == "xposed") {
+ val xposedRemotePref = XposedRemotePrefProvider.create()
+ if (xposedRemotePref.getBoolean("vendor_id_hook", false)) {
attManager = ATTManager(adapter, device)
attManager!!.connect()
}
@@ -2687,8 +2692,6 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
version1 = config.airpodsVersion1,
version2 = config.airpodsVersion2,
version3 = config.airpodsVersion3,
- aacpManager = aacpManager,
- attManager = attManager
)
}
}
diff --git a/android/app/src/main/java/me/kavishdevar/librepods/services/AppListenerService.kt b/android/app/src/main/java/me/kavishdevar/librepods/services/AppListenerService.kt
index 83e5b06..7eeea7c 100644
--- a/android/app/src/main/java/me/kavishdevar/librepods/services/AppListenerService.kt
+++ b/android/app/src/main/java/me/kavishdevar/librepods/services/AppListenerService.kt
@@ -40,13 +40,13 @@ val cameraPackages = mutableSetOf(
var cameraOpen = false
private var currentCustomPackage: String? = null
-class AppListenerService : AccessibilityService() {
+class AppListenerService: AccessibilityService() {
private lateinit var prefs: android.content.SharedPreferences
private val preferenceChangeListener = android.content.SharedPreferences.OnSharedPreferenceChangeListener { sharedPreferences, key ->
if (key == "custom_camera_package") {
val newPackage = sharedPreferences.getString(key, null)
currentCustomPackage?.let { cameraPackages.remove(it) }
- if (newPackage != null && newPackage.isNotBlank()) {
+ if (!newPackage.isNullOrBlank()) {
cameraPackages.add(newPackage)
}
currentCustomPackage = newPackage
@@ -57,7 +57,7 @@ class AppListenerService : AccessibilityService() {
super.onCreate()
prefs = getSharedPreferences("settings", MODE_PRIVATE)
val customPackage = prefs.getString("custom_camera_package", null)
- if (customPackage != null && customPackage.isNotBlank()) {
+ if (!customPackage.isNullOrBlank()) {
cameraPackages.add(customPackage)
currentCustomPackage = customPackage
}
@@ -95,4 +95,4 @@ class AppListenerService : AccessibilityService() {
}
override fun onInterrupt() {}
-}
\ No newline at end of file
+}
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 d45e0bb..403ba53 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
@@ -18,32 +18,27 @@
package me.kavishdevar.librepods.utils
+import android.content.SharedPreferences
import android.os.Build
import me.kavishdevar.librepods.BuildConfig
-fun isSupported(): Boolean {
- if (BuildConfig.PLAY_BUILD) {
- val isPixel = Build.MANUFACTURER.lowercase() == "google"
- val isOppoOrOnePlus = Build.MANUFACTURER.lowercase() in listOf("oneplus", "oppo")
+fun isSupported(sharedPreferences: SharedPreferences): Boolean {
+ val isPixel = Build.MANUFACTURER.lowercase() == "google"
+ val isOppoOrOnePlus = Build.MANUFACTURER.lowercase() in listOf("oneplus", "oppo")
- if (isPixel) {
- when (Build.VERSION.SDK_INT) {
- 36 -> {
- return Build.ID == "CP1A.260305.018" || Build.ID == "CP1A.260405.005"
- }
-
- 37 -> {
- return true
- }
+ if (isPixel) {
+ when (Build.VERSION.SDK_INT) {
+ 36 -> {
+ return Build.ID == "CP1A.260305.018" || Build.ID == "CP1A.260405.005"
+ }
+
+ 37 -> {
+ return true
}
- } else if (isOppoOrOnePlus) {
- return true
}
+ } else if (isOppoOrOnePlus) {
+ return true
}
- return true
+ return if (BuildConfig.FLAVOR == "xposed") true
+ else sharedPreferences.getBoolean("bypass_device_check", false)
}
-
-
-/*fun isSupported(): Boolean {
- return true
-}*/
diff --git a/android/app/src/main/java/me/kavishdevar/librepods/utils/SystemAPIUtils.kt b/android/app/src/main/java/me/kavishdevar/librepods/utils/SystemAPIUtils.kt
index 14abb76..cd91e24 100644
--- a/android/app/src/main/java/me/kavishdevar/librepods/utils/SystemAPIUtils.kt
+++ b/android/app/src/main/java/me/kavishdevar/librepods/utils/SystemAPIUtils.kt
@@ -296,7 +296,7 @@ object SystemApisUtils {
)
method.invoke(device, key, value) as Boolean
} catch (e: Exception) {
- Log.e("SystemApisUtils", "Failed to set metadata for key $key", e)
+ Log.w("SystemApisUtils", "Failed to set metadata for key $key: ${e.message}")
false
}
}
diff --git a/android/app/src/main/java/me/kavishdevar/librepods/utils/XposedServiceHolder.kt b/android/app/src/main/java/me/kavishdevar/librepods/utils/XposedServiceHolder.kt
new file mode 100644
index 0000000..27a4ffc
--- /dev/null
+++ b/android/app/src/main/java/me/kavishdevar/librepods/utils/XposedServiceHolder.kt
@@ -0,0 +1,7 @@
+package me.kavishdevar.librepods.utils
+
+import io.github.libxposed.service.XposedService
+
+object XposedServiceHolder {
+ var service: XposedService? = null
+}
diff --git a/android/app/src/main/res/values-es/strings.xml b/android/app/src/main/res/values-es/strings.xml
index 27e3510..2d7ea81 100644
--- a/android/app/src/main/res/values-es/strings.xml
+++ b/android/app/src/main/res/values-es/strings.xml
@@ -34,7 +34,7 @@
AirPods no conectados
Por favor, conecta tus AirPods para acceder a los ajustes.
Atrás
- Personalización
+ Personalización
Volumen relativo
Reduce a un porcentaje del volumen actual en vez del volumen máximo.
Pausar música
@@ -169,7 +169,7 @@
Introducir 16-byte ENC_KEY como formato hexadecimal (32 caracteres):
Debe tener exactamente 32 caracteres hexadecimales
Error convirtiendo hex:
- Offset encontrado. Por favor, reinicie el proceso Bluetooth
+ Por favor, reinicie el proceso Bluetooth
Asistente Digital
Activado
Control Remoto de Cámara
diff --git a/android/app/src/main/res/values-fr/strings.xml b/android/app/src/main/res/values-fr/strings.xml
index c87a7d4..8595b37 100644
--- a/android/app/src/main/res/values-fr/strings.xml
+++ b/android/app/src/main/res/values-fr/strings.xml
@@ -34,7 +34,7 @@
AirPods non connectés
Veuillez connecter vos AirPods pour accéder aux réglages.
Retour
- Personnalisations
+ Personnalisations
Volume relatif
Réduit à un pourcentage du volume actuel plutôt qu\'au volume maximum.
Mettre la musique en pause
diff --git a/android/app/src/main/res/values-pt/strings.xml b/android/app/src/main/res/values-pt/strings.xml
index f214026..92e969d 100644
--- a/android/app/src/main/res/values-pt/strings.xml
+++ b/android/app/src/main/res/values-pt/strings.xml
@@ -34,7 +34,7 @@
AirPods não conectados
Por favor, conecte seus AirPods para acessar as configurações.
Voltar
- Personalizações
+ Personalizações
Volume relativo
Reduz para uma porcentagem do volume atual em vez do volume máximo.
Pausar Música
diff --git a/android/app/src/main/res/values-tr/strings.xml b/android/app/src/main/res/values-tr/strings.xml
index 864920d..1e6631c 100644
--- a/android/app/src/main/res/values-tr/strings.xml
+++ b/android/app/src/main/res/values-tr/strings.xml
@@ -34,7 +34,7 @@
AirPods bağlı değil
Ayarlara erişmek için lütfen AirPods\'unuzu bağlayın.
Geri
- Özelleştirmeler
+ Özelleştirmeler
Göreceli ses
Maksimum ses yerine mevcut sesin yüzdesine göre azaltır.
Müziği Duraklat
diff --git a/android/app/src/main/res/values-uk/strings.xml b/android/app/src/main/res/values-uk/strings.xml
index 267d689..ce95694 100644
--- a/android/app/src/main/res/values-uk/strings.xml
+++ b/android/app/src/main/res/values-uk/strings.xml
@@ -34,7 +34,7 @@
AirPods не підключені
Будь ласка, підключіть ваші AirPods, щоб отримати доступ до налаштувань.
Назад
- Персоналізація
+ Персоналізація
Відносна гучність
Зменшує до відсотка від поточної гучності, а не від максимальної.
Призупинити Музику
diff --git a/android/app/src/main/res/values-vi/strings.xml b/android/app/src/main/res/values-vi/strings.xml
index 25436c5..f461c4e 100644
--- a/android/app/src/main/res/values-vi/strings.xml
+++ b/android/app/src/main/res/values-vi/strings.xml
@@ -34,7 +34,7 @@
AirPods chưa được kết nối
Vui lòng kết nối đến AirPods của bạn để truy cập cài đặt.
Quay lại
- Tùy chỉnh
+ Tùy chỉnh
Âm lượng tương đối
Giảm xuống phần trăm của âm lượng hiện tại thay vì âm lượng tối đa.
Tạm dừng nhạc
@@ -169,7 +169,7 @@
Nhập ENC_KEY 16 byte dưới dạng chuỗi hex (32 ký tự):
Phải chính xác 32 ký tự hex
Lỗi chuyển đổi hex:
- Đã tìm thấy độ lệch, vui lòng khởi động lại tiến trình Bluetooth
+ vui lòng khởi động lại tiến trình Bluetooth
Trợ lý kỹ thuật số
Bật
Điều khiển máy ảnh từ xa
diff --git a/android/app/src/main/res/values-zh-rCN/strings.xml b/android/app/src/main/res/values-zh-rCN/strings.xml
index af388da..6fccbd2 100644
--- a/android/app/src/main/res/values-zh-rCN/strings.xml
+++ b/android/app/src/main/res/values-zh-rCN/strings.xml
@@ -33,7 +33,7 @@
AirPods 未连接
请连接 AirPods 以访问设置。
返回
- 自定义
+ 自定义
相对音量
降低到当前音量的百分比,而不是最大音量。
暂停音乐
diff --git a/android/app/src/main/res/values-zh-rTW/strings.xml b/android/app/src/main/res/values-zh-rTW/strings.xml
index 3a77d69..dff0e65 100644
--- a/android/app/src/main/res/values-zh-rTW/strings.xml
+++ b/android/app/src/main/res/values-zh-rTW/strings.xml
@@ -34,7 +34,7 @@
未連接 AirPods
請連接你的 AirPods 以存取設定。
返回
- 自訂
+ 自訂
相對音量
降低至當前音量的百分比,而不是最大音量。
暫停音樂
diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml
index beedccf..a31bfc4 100644
--- a/android/app/src/main/res/values/strings.xml
+++ b/android/app/src/main/res/values/strings.xml
@@ -34,7 +34,7 @@
AirPods not connected
Please connect your AirPods to access settings.
Back
- Customizations
+ Customizations
Relative volume
Reduces to a percentage of the current volume instead of the maximum volume.
Pause Music
@@ -169,7 +169,7 @@
Enter 16-byte ENC_KEY as hex string (32 characters):
Must be exactly 32 hex characters
Error converting hex:
- Found offset please restart the Bluetooth process
+ Please restart the Bluetooth process
Digital Assistant
On
Camera Remote
@@ -210,5 +210,30 @@
Lets in external sounds
Dynamically adjust external noise
Blocks out external sounds
- Unlock all features
+ Unlock advanced features
+ Buy
+ Restore purchases
+ Automatically stop playing audio when you take them off, and resume playback when you put them back on.
+ Battery
+ View accurate battery status in the app and notification.
+ Switch between listening modes directly from the app or Quick Settings.
+ Advanced device settings
+ Customize settings like Personalized Volume, Adaptive Audio, Pause media when falling asleep, and other Accessibility settings.
+ Automatic Connection
+ Enable and customize automatic connection to AirPods.
+ Get access to app customizations, including phone battery in widget, conversational awareness volume, and many more upcoming customization features.
+ Support the development
+ LibrePods is developed by a single developer. Upgrading helps keep the app alive.
+ Feature availability depends on your AirPods model and firmware version.
+ Contact
+ E-Mail
+ Discord
+ GitHub Issues
+ Version code
+ Flavor
+ Build type
+ No
+ Yes
+ Settings
+ requires xposed
diff --git a/android/app/src/normal/java/me/kavishdevar/librepods/LibrePodsApplication.kt b/android/app/src/normal/java/me/kavishdevar/librepods/LibrePodsApplication.kt
new file mode 100644
index 0000000..0120900
--- /dev/null
+++ b/android/app/src/normal/java/me/kavishdevar/librepods/LibrePodsApplication.kt
@@ -0,0 +1,5 @@
+package me.kavishdevar.librepods
+
+import android.app.Application
+
+class LibrePodsApplication: Application()
diff --git a/android/app/src/normal/java/me/kavishdevar/librepods/data/XposedRemotePrefImpl.kt b/android/app/src/normal/java/me/kavishdevar/librepods/data/XposedRemotePrefImpl.kt
new file mode 100644
index 0000000..072d9c3
--- /dev/null
+++ b/android/app/src/normal/java/me/kavishdevar/librepods/data/XposedRemotePrefImpl.kt
@@ -0,0 +1,11 @@
+package me.kavishdevar.librepods.data
+
+class XposedRemotePrefImpl: XposedRemotePref {
+ override fun isAvailable(): Boolean { return false }
+
+ override fun getBoolean(key: String, def: Boolean): Boolean {
+ return false
+ }
+
+ override fun putBoolean(key: String, value: Boolean) { }
+}
diff --git a/android/app/src/normal/java/me/kavishdevar/librepods/utils/KotlinModule.kt b/android/app/src/normal/java/me/kavishdevar/librepods/utils/KotlinModule.kt
new file mode 100644
index 0000000..0d1c2bd
--- /dev/null
+++ b/android/app/src/normal/java/me/kavishdevar/librepods/utils/KotlinModule.kt
@@ -0,0 +1,125 @@
+package me.kavishdevar.librepods.utils
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.os.Handler
+import android.os.Looper
+import android.util.Log
+import android.widget.ImageView
+import androidx.core.net.toUri
+import io.github.libxposed.api.XposedModule
+import io.github.libxposed.api.XposedModuleInterface.ModuleLoadedParam
+import io.github.libxposed.api.XposedModuleInterface.PackageLoadedParam
+
+private const val TAG = "LibrePodsHook"
+
+@SuppressLint("DiscouragedApi", "PrivateApi")
+class KotlinModule: XposedModule() {
+ override fun onModuleLoaded(param: ModuleLoadedParam) {
+ log(Log.INFO, TAG, "module initialized at :: ${param.processName}")
+ log(Log.INFO, TAG, "framework: $frameworkName($frameworkVersionCode) API $apiVersion")
+ }
+
+ override fun onPackageLoaded(param: PackageLoadedParam) {
+ log(Log.INFO, TAG, "onPackageLoaded :: ${param.packageName}")
+
+ if (param.packageName == "com.google.android.bluetooth" || param.packageName == "com.android.bluetooth") {
+ log(Log.INFO, TAG, "Bluetooth app detected, hooking l2c_fcr_chk_chan_modes")
+ try {
+ if (param.isFirstPackage) {
+ log(Log.INFO, TAG, "Loading native library for Bluetooth hook")
+
+ NativeBridge.setSdpHook(getRemotePreferences("me.kavishdevar.librepods").getBoolean("vendor_id_hook", false))
+ System.loadLibrary("l2c_fcr_hook")
+ log(Log.INFO, TAG, "Native library loaded successfully")
+ }
+ } catch (e: Exception) {
+ log(Log.ERROR, TAG, "Failed to load native library: ${e.message}")
+ }
+ }
+
+ if (param.packageName == "com.google.android.settings") {
+ hookSettingsController(param, "com.google.android.settings.bluetooth.AdvancedBluetoothDetailsHeaderController")
+ }
+
+ if (param.packageName == "com.android.settings") {
+ hookSettingsController(param, "com.android.settings.bluetooth.AdvancedBluetoothDetailsHeaderController")
+ }
+ }
+
+ private fun hookSettingsController(param: PackageLoadedParam, className: String) {
+ log(Log.INFO, TAG, "Settings app detected, hooking Bluetooth icon handling")
+ try {
+ val headerControllerClass = Class.forName(className, false, param.defaultClassLoader)
+ val updateIconMethod = headerControllerClass.getDeclaredMethod(
+ "updateIcon",
+ ImageView::class.java,
+ String::class.java
+ )
+
+ hook(updateIconMethod).intercept { chain ->
+ try {
+ log(Log.INFO, TAG, "Bluetooth icon hook called with args: ${chain.args.joinToString(", ")}")
+ val imageView = chain.args[0] as? ImageView
+ val iconUri = chain.args[1] as? String
+
+ if (imageView == null || iconUri == null) {
+ return@intercept chain.proceed()
+ }
+
+ val uri = iconUri.toUri()
+ if (!uri.toString().startsWith("android.resource://me.kavishdevar.librepods")) {
+ return@intercept chain.proceed()
+ }
+
+ log(Log.INFO, TAG, "Handling AirPods icon URI: $uri")
+
+ Handler(Looper.getMainLooper()).post {
+ try {
+ val context = imageView.context
+ val packageName = uri.authority ?: return@post
+ val packageContext = context.createPackageContext(
+ packageName,
+ Context.CONTEXT_IGNORE_SECURITY
+ )
+
+ val resPath = uri.pathSegments
+ if (resPath.size >= 2 && resPath[0] == "drawable") {
+ val resourceName = resPath[1]
+ val resourceId = packageContext.resources.getIdentifier(
+ resourceName, "drawable", packageName
+ )
+
+ if (resourceId != 0) {
+ val drawable = packageContext.resources.getDrawable(
+ resourceId, packageContext.theme
+ )
+ imageView.setImageDrawable(drawable)
+ imageView.alpha = 1.0f
+ log(Log.INFO, TAG, "Successfully loaded icon from resource: $resourceName")
+ } else {
+ log(Log.ERROR, TAG, "Resource not found: $resourceName")
+ }
+ }
+ } catch (e: Exception) {
+ log(Log.ERROR, TAG, "Error loading resource from URI $uri: ${e.message}")
+ }
+ }
+ null
+ } catch (e: Exception) {
+ log(Log.ERROR, TAG, "Error in Bluetooth icon hook: ${e.message}")
+ chain.proceed()
+ }
+ }
+
+ log(Log.INFO, TAG, "Successfully hooked updateIcon method in Bluetooth settings")
+ } catch (e: Exception) {
+ log(Log.ERROR, TAG, "Failed to hook Bluetooth icon handler: ${e.message}")
+ }
+ }
+}
+
+
+object NativeBridge {
+ external fun setSdpHook(enabled: Boolean)
+}
diff --git a/android/app/src/normal/java/me/kavishdevar/librepods/utils/XposedServiceHolder.kt b/android/app/src/normal/java/me/kavishdevar/librepods/utils/XposedServiceHolder.kt
new file mode 100644
index 0000000..5412c5f
--- /dev/null
+++ b/android/app/src/normal/java/me/kavishdevar/librepods/utils/XposedServiceHolder.kt
@@ -0,0 +1,28 @@
+package me.kavishdevar.librepods.utils
+
+import android.content.Context
+import io.github.libxposed.service.XposedService
+import io.github.libxposed.service.XposedServiceHelper
+
+object XposedServiceHolder {
+ var service: XposedService? = null
+}
+
+
+object XposedInitializer: XposedServiceHelper.OnServiceListener {
+ private var initialized = false
+
+ fun ensureInit(context: Context) {
+ if (initialized) return
+ initialized = true
+ XposedServiceHelper.registerListener(this)
+ }
+
+ override fun onServiceBind(service: XposedService) {
+ XposedServiceHolder.service = service
+ }
+
+ override fun onServiceDied(service: XposedService) {
+ XposedServiceHolder.service = null
+ }
+}
diff --git a/android/app/src/xposed/cpp/l2c_fcr_hook.cpp b/android/app/src/xposed/cpp/l2c_fcr_hook.cpp
index 8ceb332..e663666 100644
--- a/android/app/src/xposed/cpp/l2c_fcr_hook.cpp
+++ b/android/app/src/xposed/cpp/l2c_fcr_hook.cpp
@@ -24,6 +24,8 @@
#include
#include
#include
+#include
+#include
#include "l2c_fcr_hook.h"
@@ -31,7 +33,7 @@ extern "C" {
#include "xz.h"
}
-#define LOG_TAG "LibrePods"
+#define LOG_TAG "LibrePodsHook"
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)
@@ -41,8 +43,10 @@ static uint8_t (*original_l2c_fcr_chk_chan_modes)(void*) = nullptr;
static tBTA_STATUS (*original_BTA_DmSetLocalDiRecord)(
tSDP_DI_RECORD*, uint32_t*) = nullptr;
+static std::atomic enableSdpHook(false);
+
uint8_t fake_l2c_fcr_chk_chan_modes(void* p_ccb) {
- LOGI("l2c_fcr_chk_chan_modes hooked");
+ LOGI("l2c_fcr_chk_chan_modes called");
uint8_t orig = 0;
if (original_l2c_fcr_chk_chan_modes)
orig = original_l2c_fcr_chk_chan_modes(p_ccb);
@@ -55,7 +59,11 @@ tBTA_STATUS fake_BTA_DmSetLocalDiRecord(
tSDP_DI_RECORD* p_device_info,
uint32_t* p_handle) {
- LOGI("BTA_DmSetLocalDiRecord hooked");
+ LOGI("BTA_DmSetLocalDiRecord called");
+
+ if (original_BTA_DmSetLocalDiRecord && enableSdpHook.load(std::memory_order_relaxed)) original_BTA_DmSetLocalDiRecord(p_device_info, p_handle);
+
+ LOGI("BTA_DmSetLocalDiRecord changing vendor id and source");
if (p_device_info) {
p_device_info->vendor = 0x004C;
@@ -265,9 +273,9 @@ static bool hookLibrary(const char* libname) {
findSymbolOffset(decompressed,
"l2c_fcr_chk_chan_modes");
-// uint64_t sdp_offset =
-// findSymbolOffset(decompressed,
-// "BTA_DmSetLocalDiRecord");
+ uint64_t sdp_offset =
+ findSymbolOffset(decompressed,
+ "BTA_DmSetLocalDiRecord");
if (chk_offset) {
void* target =
@@ -280,16 +288,16 @@ static bool hookLibrary(const char* libname) {
LOGI("Hooked l2c_fcr_chk_chan_modes");
}
-// if (sdp_offset) {
-// void* target =
-// reinterpret_cast(base + sdp_offset);
-//
-// hook_func(target,
-// (void*)fake_BTA_DmSetLocalDiRecord,
-// (void**)&original_BTA_DmSetLocalDiRecord);
-//
-// LOGI("Hooked BTA_DmSetLocalDiRecord");
-// }
+ if (sdp_offset) {
+ void* target =
+ reinterpret_cast(base + sdp_offset);
+
+ hook_func(target,
+ (void*)fake_BTA_DmSetLocalDiRecord,
+ (void**)&original_BTA_DmSetLocalDiRecord);
+
+ LOGI("Hooked BTA_DmSetLocalDiRecord");
+ }
return true;
}
@@ -315,10 +323,16 @@ extern "C"
[[gnu::visibility("default")]]
[[gnu::used]]
NativeOnModuleLoaded native_init(const NativeAPIEntries* entries) {
-
- LOGI("LibrePods initialized");
-
hook_func = (HookFunType)entries->hook_func;
-
+ LOGI("LibrePodsNativeHook initialized, sdp hook enabled: %d", enableSdpHook.load(std::memory_order_relaxed));
return on_library_loaded;
}
+
+extern "C"
+JNIEXPORT void JNICALL
+Java_me_kavishdevar_librepods_utils_NativeBridge_setSdpHook(
+ JNIEnv*, jobject thiz, jboolean enable) {
+ enableSdpHook.store(enable, std::memory_order_relaxed);
+
+ LOGI("sdp hook enabled: %d", enable);
+}
diff --git a/android/app/src/xposed/java/me/kavishdevar/librepods/LibrePodsApplication.kt b/android/app/src/xposed/java/me/kavishdevar/librepods/LibrePodsApplication.kt
new file mode 100644
index 0000000..cca90e5
--- /dev/null
+++ b/android/app/src/xposed/java/me/kavishdevar/librepods/LibrePodsApplication.kt
@@ -0,0 +1,21 @@
+package me.kavishdevar.librepods
+
+import android.app.Application
+import io.github.libxposed.service.XposedService
+import io.github.libxposed.service.XposedServiceHelper
+import me.kavishdevar.librepods.utils.XposedServiceHolder
+
+class LibrePodsApplication: Application(), XposedServiceHelper.OnServiceListener {
+ override fun onCreate() {
+ super.onCreate()
+ XposedServiceHelper.registerListener(this)
+ }
+
+ override fun onServiceBind(p0: XposedService) {
+ XposedServiceHolder.service = p0
+ }
+
+ override fun onServiceDied(p0: XposedService) {
+ XposedServiceHolder.service = null
+ }
+}
diff --git a/android/app/src/xposed/java/me/kavishdevar/librepods/data/XposedRemotePrefImpl.kt b/android/app/src/xposed/java/me/kavishdevar/librepods/data/XposedRemotePrefImpl.kt
new file mode 100644
index 0000000..112e752
--- /dev/null
+++ b/android/app/src/xposed/java/me/kavishdevar/librepods/data/XposedRemotePrefImpl.kt
@@ -0,0 +1,21 @@
+package me.kavishdevar.librepods.data
+
+import androidx.core.content.edit
+import me.kavishdevar.librepods.utils.XposedServiceHolder
+
+class XposedRemotePrefImpl: XposedRemotePref {
+ override fun isAvailable(): Boolean {
+ return XposedServiceHolder.service != null
+ }
+
+ override fun getBoolean(key: String, def: Boolean): Boolean {
+ val s = XposedServiceHolder.service ?: return def
+ return s.getRemotePreferences("me.kavishdevar.librepods").getBoolean(key, def)
+ }
+
+ override fun putBoolean(key: String, value: Boolean) {
+ val s = XposedServiceHolder.service ?: return
+ s.getRemotePreferences("me.kavishdevar.librepods")
+ .edit { putBoolean(key, value) }
+ }
+}
diff --git a/android/app/src/xposed/java/me/kavishdevar/librepods/utils/KotlinModule.kt b/android/app/src/xposed/java/me/kavishdevar/librepods/utils/KotlinModule.kt
index 69ad51a..e48c4f5 100644
--- a/android/app/src/xposed/java/me/kavishdevar/librepods/utils/KotlinModule.kt
+++ b/android/app/src/xposed/java/me/kavishdevar/librepods/utils/KotlinModule.kt
@@ -2,148 +2,125 @@ package me.kavishdevar.librepods.utils
import android.annotation.SuppressLint
import android.content.Context
-import android.content.pm.ApplicationInfo
+import android.os.Handler
+import android.os.Looper
import android.util.Log
import android.widget.ImageView
import androidx.core.net.toUri
-import io.github.libxposed.api.XposedInterface
-import io.github.libxposed.api.XposedInterface.AfterHookCallback
import io.github.libxposed.api.XposedModule
-import io.github.libxposed.api.XposedModuleInterface
import io.github.libxposed.api.XposedModuleInterface.ModuleLoadedParam
-import io.github.libxposed.api.annotations.AfterInvocation
-import io.github.libxposed.api.annotations.XposedHooker
-import kotlin.jvm.java
+import io.github.libxposed.api.XposedModuleInterface.PackageLoadedParam
+
+private const val TAG = "LibrePodsHook"
-private const val TAG = "AirPodsHook"
-private lateinit var module: KotlinModule
@SuppressLint("DiscouragedApi", "PrivateApi")
-class KotlinModule(base: XposedInterface, param: ModuleLoadedParam): XposedModule(base, param) {
- init {
- Log.i(TAG, "AirPodsHook module initialized at :: ${param.processName}")
- module = this
+class KotlinModule: XposedModule() {
+ override fun onModuleLoaded(param: ModuleLoadedParam) {
+ log(Log.INFO, TAG, "module initialized at :: ${param.processName}")
+ log(Log.INFO, TAG, "framework: $frameworkName($frameworkVersionCode) API $apiVersion")
}
- override fun onPackageLoaded(param: XposedModuleInterface.PackageLoadedParam) {
- super.onPackageLoaded(param)
- Log.i(TAG, "onPackageLoaded :: ${param.packageName}")
+ override fun onPackageLoaded(param: PackageLoadedParam) {
+ log(Log.INFO, TAG, "onPackageLoaded :: ${param.packageName}")
if (param.packageName == "com.google.android.bluetooth" || param.packageName == "com.android.bluetooth") {
- Log.i(TAG, "Bluetooth app detected, hooking l2c_fcr_chk_chan_modes")
-
+ log(Log.INFO, TAG, "Bluetooth app detected, hooking l2c_fcr_chk_chan_modes")
try {
if (param.isFirstPackage) {
- Log.i(TAG, "Loading native library for Bluetooth hook")
+ log(Log.INFO, TAG, "Loading native library for Bluetooth hook")
System.loadLibrary("l2c_fcr_hook")
- Log.i(TAG, "Native library loaded successfully")
+ val remotePrefValue = getRemotePreferences("me.kavishdevar.librepods").getBoolean("vendor_id_hook", false)
+ log(Log.INFO, TAG, "sdp hook enabled (remote pref): $remotePrefValue")
+ NativeBridge.setSdpHook(remotePrefValue)
+ log(Log.INFO, TAG, "Native library loaded successfully")
}
} catch (e: Exception) {
- Log.e(TAG, "Failed to load native library: ${e.message}", e)
+ log(Log.ERROR, TAG, "Failed to load native library: ${e.message}")
}
}
if (param.packageName == "com.google.android.settings") {
- Log.i(TAG, "Settings app detected, hooking Bluetooth icon handling")
- try {
- val headerControllerClass = param.classLoader.loadClass(
- "com.google.android.settings.bluetooth.AdvancedBluetoothDetailsHeaderController")
-
- val updateIconMethod = headerControllerClass.getDeclaredMethod(
- "updateIcon",
- ImageView::class.java,
- String::class.java)
-
- hook(updateIconMethod, BluetoothIconHooker::class.java)
- Log.i(TAG, "Successfully hooked updateIcon method in Bluetooth settings")
- } catch (e: Exception) {
- Log.e(TAG, "Failed to hook Bluetooth icon handler: ${e.message}", e)
- }
+ hookSettingsController(param, "com.google.android.settings.bluetooth.AdvancedBluetoothDetailsHeaderController")
}
if (param.packageName == "com.android.settings") {
- Log.i(TAG, "Settings app detected, hooking Bluetooth icon handling")
- try {
- val headerControllerClass = param.classLoader.loadClass(
- "com.android.settings.bluetooth.AdvancedBluetoothDetailsHeaderController")
-
- val updateIconMethod = headerControllerClass.getDeclaredMethod(
- "updateIcon",
- ImageView::class.java,
- String::class.java)
-
- hook(updateIconMethod, BluetoothIconHooker::class.java)
- Log.i(TAG, "Successfully hooked updateIcon method in Bluetooth settings")
- } catch (e: Exception) {
- Log.e(TAG, "Failed to hook Bluetooth icon handler: ${e.message}", e)
- }
+ hookSettingsController(param, "com.android.settings.bluetooth.AdvancedBluetoothDetailsHeaderController")
}
}
- @XposedHooker
- class BluetoothIconHooker : XposedInterface.Hooker {
- companion object {
- @JvmStatic
- @AfterInvocation
- fun afterUpdateIcon(callback: AfterHookCallback) {
- Log.i(TAG, "BluetoothIconHooker called with args: ${callback.args.joinToString(", ")}")
+ private fun hookSettingsController(param: PackageLoadedParam, className: String) {
+ log(Log.INFO, TAG, "Settings app detected, hooking Bluetooth icon handling")
+ try {
+ val headerControllerClass = Class.forName(className, false, param.defaultClassLoader)
+ val updateIconMethod = headerControllerClass.getDeclaredMethod(
+ "updateIcon",
+ ImageView::class.java,
+ String::class.java
+ )
+
+ hook(updateIconMethod).intercept { chain ->
try {
- val imageView = callback.args[0] as ImageView
- val iconUri = callback.args[1] as String
+ log(Log.INFO, TAG, "Bluetooth icon hook called with args: ${chain.args.joinToString(", ")}")
+ val imageView = chain.args[0] as? ImageView
+ val iconUri = chain.args[1] as? String
+
+ if (imageView == null || iconUri == null) {
+ return@intercept chain.proceed()
+ }
val uri = iconUri.toUri()
- if (uri.toString().startsWith("android.resource://me.kavishdevar.librepods")) {
- Log.i(TAG, "Handling AirPods icon URI: $uri")
+ if (!uri.toString().startsWith("android.resource://me.kavishdevar.librepods")) {
+ return@intercept chain.proceed()
+ }
+ log(Log.INFO, TAG, "Handling AirPods icon URI: $uri")
+
+ Handler(Looper.getMainLooper()).post {
try {
val context = imageView.context
+ val packageName = uri.authority ?: return@post
+ val packageContext = context.createPackageContext(
+ packageName,
+ Context.CONTEXT_IGNORE_SECURITY
+ )
- android.os.Handler(android.os.Looper.getMainLooper()).post {
- try {
- val packageName = uri.authority
- val packageContext = context.createPackageContext(
- packageName,
- Context.CONTEXT_IGNORE_SECURITY
+ val resPath = uri.pathSegments
+ if (resPath.size >= 2 && resPath[0] == "drawable") {
+ val resourceName = resPath[1]
+ val resourceId = packageContext.resources.getIdentifier(
+ resourceName, "drawable", packageName
+ )
+
+ if (resourceId != 0) {
+ val drawable = packageContext.resources.getDrawable(
+ resourceId, packageContext.theme
)
-
- val resPath = uri.pathSegments
- if (resPath.size >= 2 && resPath[0] == "drawable") {
- val resourceName = resPath[1]
- val resourceId = packageContext.resources.getIdentifier(
- resourceName, "drawable", packageName
- )
-
- if (resourceId != 0) {
- val drawable = packageContext.resources.getDrawable(
- resourceId, packageContext.theme
- )
-
- imageView.setImageDrawable(drawable)
- imageView.alpha = 1.0f
-
- callback.result = null
-
- Log.i(TAG, "Successfully loaded icon from resource: $resourceName")
- } else {
- Log.e(TAG, "Resource not found: $resourceName")
- }
- }
- } catch (e: Exception) {
- Log.e(TAG, "Error loading resource from URI $uri: ${e.message}")
+ imageView.setImageDrawable(drawable)
+ imageView.alpha = 1.0f
+ log(Log.INFO, TAG, "Successfully loaded icon from resource: $resourceName")
+ } else {
+ log(Log.ERROR, TAG, "Resource not found: $resourceName")
}
}
} catch (e: Exception) {
- Log.e(TAG, "Error accessing context: ${e.message}")
+ log(Log.ERROR, TAG, "Error loading resource from URI $uri: ${e.message}")
}
}
+ null
} catch (e: Exception) {
- Log.e(TAG, "Error in BluetoothIconHooker: ${e.message}")
- e.printStackTrace()
+ log(Log.ERROR, TAG, "Error in Bluetooth icon hook: ${e.message}")
+ chain.proceed()
}
}
+
+ log(Log.INFO, TAG, "Successfully hooked updateIcon method in Bluetooth settings")
+ } catch (e: Exception) {
+ log(Log.ERROR, TAG, "Failed to hook Bluetooth icon handler: ${e.message}")
}
}
-
- override fun getApplicationInfo(): ApplicationInfo {
- return super.applicationInfo
- }
+}
+
+
+object NativeBridge {
+ external fun setSdpHook(enabled: Boolean)
}
diff --git a/android/app/src/xposed/resources/META-INF/xposed/module.prop b/android/app/src/xposed/resources/META-INF/xposed/module.prop
index 8dc7ff3..c3975fe 100644
--- a/android/app/src/xposed/resources/META-INF/xposed/module.prop
+++ b/android/app/src/xposed/resources/META-INF/xposed/module.prop
@@ -1,3 +1,3 @@
-minApiVersion=100
-targetApiVersion=100
+minApiVersion=101
+targetApiVersion=101
staticScope=true
diff --git a/android/gradle/libs.versions.toml b/android/gradle/libs.versions.toml
index de2c0fa..964d282 100644
--- a/android/gradle/libs.versions.toml
+++ b/android/gradle/libs.versions.toml
@@ -17,6 +17,7 @@ materialIconsCore = "1.7.8"
backdrop = "2.0.0-alpha03"
billing = "8.3.0"
hilt = "2.59.2"
+xposed = "101.0.0"
[libraries]
accompanist-permissions = { module = "com.google.accompanist:accompanist-permissions", version.ref = "accompanistPermissions" }
@@ -44,6 +45,8 @@ backdrop = { group = "io.github.kyant0", name = "backdrop", version.ref = "backd
billing = { group = "com.android.billingclient", name = "billing-ktx", version.ref = "billing" }
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" }
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }