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