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