android: add demo mode; fix issues on UI start

This commit is contained in:
Kavish Devar
2026-04-20 16:21:06 +05:30
parent 78920ef486
commit 45915ca560
13 changed files with 274 additions and 151 deletions

View File

@@ -21,6 +21,7 @@
package me.kavishdevar.librepods.composables
import android.content.res.Configuration
import android.util.Log
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.isSystemInDarkTheme
@@ -32,6 +33,7 @@ import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.widthIn
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
@@ -39,6 +41,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.res.imageResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
@@ -74,76 +77,84 @@ fun BatteryView(
val singleDisplayed = remember { mutableStateOf(false) }
Row {
Column(
modifier = Modifier.fillMaxWidth(0.5f),
horizontalAlignment = Alignment.CenterHorizontally
Box(
modifier = Modifier.fillMaxWidth(),
contentAlignment = Alignment.Center
) {
Row(
modifier = Modifier.widthIn(max = 500.dp),
horizontalArrangement = Arrangement.Center
) {
Image(
bitmap = ImageBitmap.imageResource(budsRes),
contentDescription = stringResource(R.string.buds),
modifier = Modifier
.fillMaxWidth()
.padding(8.dp)
)
if (
leftCharging == rightCharging &&
(leftLevel - rightLevel) in -3..3
Column(
modifier = Modifier.weight(1f),
horizontalAlignment = Alignment.CenterHorizontally
) {
BatteryIndicator(
leftLevel.coerceAtMost(rightLevel),
leftCharging
Image(
bitmap = ImageBitmap.imageResource(budsRes),
contentDescription = stringResource(R.string.buds),
modifier = Modifier
.fillMaxWidth()
.padding(8.dp)
)
singleDisplayed.value = true
} else {
singleDisplayed.value = false
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.Center
if (
leftCharging == rightCharging &&
(leftLevel - rightLevel) in -3..3
) {
if (leftLevel > 0 || left?.status != BatteryStatus.DISCONNECTED) {
BatteryIndicator(
leftLevel,
leftCharging,
"\uDBC6\uDCE5"
)
}
BatteryIndicator(
leftLevel.coerceAtMost(rightLevel),
leftCharging
)
singleDisplayed.value = true
} else {
singleDisplayed.value = false
if (leftLevel > 0 && rightLevel > 0) {
Spacer(modifier = Modifier.width(16.dp))
}
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.Center
) {
if (leftLevel > 0 || left?.status != BatteryStatus.DISCONNECTED) {
BatteryIndicator(
leftLevel,
leftCharging,
"\uDBC6\uDCE5"
)
}
if (rightLevel > 0 || right?.status != BatteryStatus.DISCONNECTED) {
BatteryIndicator(
rightLevel,
rightCharging,
"\uDBC6\uDCE8"
)
if (leftLevel > 0 && rightLevel > 0) {
Spacer(modifier = Modifier.width(16.dp))
}
if (rightLevel > 0 || right?.status != BatteryStatus.DISCONNECTED) {
BatteryIndicator(
rightLevel,
rightCharging,
"\uDBC6\uDCE8"
)
}
}
}
}
}
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally
) {
Image(
bitmap = ImageBitmap.imageResource(caseRes),
contentDescription = stringResource(R.string.case_alt),
modifier = Modifier
.fillMaxWidth()
.padding(8.dp)
)
if (caseLevel > 0 || case?.status != BatteryStatus.DISCONNECTED) {
BatteryIndicator(
caseLevel,
caseCharging,
prefix = if (!singleDisplayed.value) "\uDBC3\uDE6C" else ""
Column(
modifier = Modifier.weight(1f),
horizontalAlignment = Alignment.CenterHorizontally
) {
Image(
bitmap = ImageBitmap.imageResource(caseRes),
contentDescription = stringResource(R.string.case_alt),
modifier = Modifier
.fillMaxWidth()
.padding(8.dp)
)
if (caseLevel > 0 || case?.status != BatteryStatus.DISCONNECTED) {
BatteryIndicator(
caseLevel,
caseCharging,
prefix = if (!singleDisplayed.value) "\uDBC3\uDE6C" else ""
)
}
}
}
}

View File

@@ -49,7 +49,6 @@ import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.zIndex
import androidx.navigation.NavController
import com.kyant.backdrop.backdrops.LayerBackdrop
import com.kyant.backdrop.backdrops.layerBackdrop
import com.kyant.backdrop.backdrops.rememberLayerBackdrop
@@ -57,7 +56,6 @@ import dev.chrisbanes.haze.HazeProgressive
import dev.chrisbanes.haze.HazeState
import dev.chrisbanes.haze.HazeTint
import dev.chrisbanes.haze.hazeEffect
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
import dev.chrisbanes.haze.rememberHazeState
import me.kavishdevar.librepods.R
@@ -66,7 +64,7 @@ fun StyledScaffold(
title: String,
actionButtons: List<@Composable (backdrop: LayerBackdrop) -> Unit> = emptyList(),
snackbarHostState: SnackbarHostState = remember { SnackbarHostState() },
content: @Composable (spacerValue: Dp, hazeState: HazeState) -> Unit
content: @Composable (spacerValue: Dp, hazeState: HazeState, bottomPadding: Dp) -> Unit
) {
val isDarkTheme = isSystemInDarkTheme()
val hazeState = rememberHazeState(blurEnabled = true)
@@ -86,7 +84,7 @@ fun StyledScaffold(
Box(
modifier = Modifier
.fillMaxSize()
.padding(start = startPadding, end = endPadding, bottom = bottomPadding)
.padding(start = startPadding, end = endPadding)
) {
val backdrop = rememberLayerBackdrop()
Box(
@@ -126,7 +124,7 @@ fun StyledScaffold(
}
}
content(topPadding + 64.dp, hazeState)
content(topPadding + 64.dp, hazeState, bottomPadding + 12.dp)
}
}
}
@@ -143,7 +141,7 @@ fun StyledScaffold(
title = title,
actionButtons = actionButtons,
snackbarHostState = snackbarHostState,
) { _, _ ->
) { _, _, _->
content()
}
}
@@ -159,7 +157,7 @@ fun StyledScaffold(
title = title,
actionButtons = actionButtons,
snackbarHostState = snackbarHostState,
) { spacerValue, _ ->
) { spacerValue, _, _ ->
content(spacerValue)
}
}

View File

@@ -19,6 +19,7 @@
package me.kavishdevar.librepods.data
import me.kavishdevar.librepods.utils.AACPManager
import me.kavishdevar.librepods.utils.AACPManager.Companion.ControlCommandIdentifiers
class ControlCommandRepository(
private val aacpManager: AACPManager
@@ -60,4 +61,10 @@ class ControlCommandRepository(
) {
aacpManager.unregisterControlCommandListener(identifier, listener)
}
fun getMap(): Map<ControlCommandIdentifiers, ByteArray> {
return aacpManager.controlCommandStatusList.associate {
it.identifier to it.value
}
}
}

View File

@@ -101,7 +101,7 @@ fun AccessibilitySettingsScreen(viewModel: AirPodsViewModel, navController: NavC
StyledScaffold(
title = stringResource(R.string.accessibility)
) { spacerHeight, hazeState ->
) { topPadding, hazeState, bottomPadding ->
Column(
modifier = Modifier
.fillMaxSize()
@@ -111,7 +111,7 @@ fun AccessibilitySettingsScreen(viewModel: AirPodsViewModel, navController: NavC
.padding(horizontal = 16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
Spacer(modifier = Modifier.height(spacerHeight))
Spacer(modifier = Modifier.height(topPadding))
// val phoneMediaEQ = remember { mutableStateOf(FloatArray(8) { 0.5f }) }
// val phoneEQEnabled = remember { mutableStateOf(false) }
@@ -508,6 +508,7 @@ fun AccessibilitySettingsScreen(viewModel: AirPodsViewModel, navController: NavC
// }
// }
// }
Spacer(modifier = Modifier.height(bottomPadding))
}
}
}

View File

@@ -24,6 +24,8 @@ package me.kavishdevar.librepods.screens
import android.annotation.SuppressLint
import android.content.Context.MODE_PRIVATE
import android.content.SharedPreferences
import android.util.Log
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
@@ -42,12 +44,16 @@ import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
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
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.pointer.PointerEventPass
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextStyle
@@ -56,7 +62,6 @@ import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.navigation.NavController
@@ -66,6 +71,7 @@ import com.kyant.backdrop.highlight.Highlight
import dev.chrisbanes.haze.HazeState
import dev.chrisbanes.haze.hazeSource
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
import kotlinx.coroutines.delay
import me.kavishdevar.librepods.BuildConfig
import me.kavishdevar.librepods.R
import me.kavishdevar.librepods.composables.AboutCard
@@ -82,7 +88,6 @@ import me.kavishdevar.librepods.composables.StyledButton
import me.kavishdevar.librepods.composables.StyledIconButton
import me.kavishdevar.librepods.composables.StyledScaffold
import me.kavishdevar.librepods.composables.StyledToggle
import me.kavishdevar.librepods.ui.theme.LibrePodsTheme
import me.kavishdevar.librepods.utils.AACPManager
import me.kavishdevar.librepods.utils.ATTHandles
import me.kavishdevar.librepods.utils.AirPodsPro3
@@ -131,15 +136,24 @@ fun AirPodsSettingsScreen(viewModel: AirPodsViewModel, navController: NavControl
StyledScaffold(
title = deviceName.text, actionButtons = listOf(
{ scaffoldBackdrop ->
StyledIconButton(
onClick = { navController.navigate("app_settings") },
icon = "􀍟",
backdrop = scaffoldBackdrop
)
}), snackbarHostState = snackbarHostState
) { spacerHeight, hazeState ->
{ scaffoldBackdrop ->
StyledIconButton(
onClick = { navController.navigate("app_settings") },
icon = "􀍟",
backdrop = scaffoldBackdrop
)
}), snackbarHostState = snackbarHostState
) { topPadding, hazeState, bottomPadding ->
hazeStateS.value = hazeState
var blockTouches by remember { mutableStateOf(false) }
LaunchedEffect(Unit) {
viewModel.demoActivated.collect {
blockTouches = true
delay(1000)
blockTouches = false
}
}
if (state.isLocallyConnected) {
val capabilities = state.capabilities
@@ -148,8 +162,18 @@ fun AirPodsSettingsScreen(viewModel: AirPodsViewModel, navController: NavControl
.fillMaxSize()
.hazeSource(hazeState)
.padding(horizontal = 16.dp)
.then(
if (blockTouches) Modifier.pointerInput(Unit) {
awaitPointerEventScope {
while (true) {
val event = awaitPointerEvent(PointerEventPass.Initial)
event.changes.forEach { it.consume() }
}
}
} else Modifier
)
) {
item(key = "spacer_top") { Spacer(modifier = Modifier.height(spacerHeight)) }
item(key = "spacer_top") { Spacer(modifier = Modifier.height(topPadding)) }
item(key = "battery") {
BatteryView(
batteryList = state.battery,
@@ -341,8 +365,7 @@ fun AirPodsSettingsScreen(viewModel: AirPodsViewModel, navController: NavControl
viewModel.setAutomaticEarDetectionEnabled(it)
},
automaticConnectionEnabled = state.automaticConnectionEnabled,
onAutomaticConnectionChanged = { viewModel.setAutomaticConnectionEnabled(it) }
)
onAutomaticConnectionChanged = { viewModel.setAutomaticConnectionEnabled(it) })
}
item(key = "spacer_microphone") { Spacer(modifier = Modifier.height(16.dp)) }
@@ -420,7 +443,7 @@ fun AirPodsSettingsScreen(viewModel: AirPodsViewModel, navController: NavControl
// item(key = "spacer_debug") { Spacer(modifier = Modifier.height(16.dp)) }
// item(key = "debug") { NavigationButton("debug", "Debug", navController) }
item(key = "spacer_bottom") { Spacer(Modifier.height(24.dp)) }
item(key = "spacer_bottom") { Spacer(Modifier.height(bottomPadding)) }
}
} else {
val backdrop = rememberLayerBackdrop()
@@ -440,26 +463,54 @@ fun AirPodsSettingsScreen(viewModel: AirPodsViewModel, navController: NavControl
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Text(
text = stringResource(R.string.airpods_not_connected), style = TextStyle(
fontSize = 24.sp,
fontWeight = FontWeight.Medium,
color = if (isSystemInDarkTheme()) Color.White else Color.Black,
fontFamily = FontFamily(Font(R.font.sf_pro))
), textAlign = TextAlign.Center, modifier = Modifier.fillMaxWidth()
)
Spacer(Modifier.height(24.dp))
Text(
text = stringResource(R.string.airpods_not_connected_description),
style = TextStyle(
fontSize = 16.sp,
fontWeight = FontWeight.Light,
color = if (isSystemInDarkTheme()) Color.White else Color.Black,
fontFamily = FontFamily(Font(R.font.sf_pro))
),
textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth()
val tapCount = remember { mutableIntStateOf(0) }
val lastTapTime = remember { mutableLongStateOf(0L) }
Column(
modifier = Modifier
.fillMaxWidth()
.pointerInput(Unit) {
detectTapGestures(
onTap = {
val now = System.currentTimeMillis()
if (now - lastTapTime.longValue > 400) {
tapCount.intValue = 0
}
tapCount.intValue++
lastTapTime.longValue = now
if (tapCount.intValue >= 5) {
tapCount.intValue = 0
viewModel.activateDemoMode()
}
}
)
}
)
{
Text(
text = stringResource(R.string.airpods_not_connected), style = TextStyle(
fontSize = 24.sp,
fontWeight = FontWeight.Medium,
color = if (isSystemInDarkTheme()) Color.White else Color.Black,
fontFamily = FontFamily(Font(R.font.sf_pro))
), textAlign = TextAlign.Center, modifier = Modifier.fillMaxWidth()
)
Spacer(Modifier.height(24.dp))
Text(
text = stringResource(R.string.airpods_not_connected_description),
style = TextStyle(
fontSize = 16.sp,
fontWeight = FontWeight.Light,
color = if (isSystemInDarkTheme()) Color.White else Color.Black,
fontFamily = FontFamily(Font(R.font.sf_pro))
),
textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth(),
)
}
Spacer(Modifier.height(32.dp))
// StyledButton(
// onClick = { navController.navigate("troubleshooting") },
@@ -496,17 +547,3 @@ fun AirPodsSettingsScreen(viewModel: AirPodsViewModel, navController: NavControl
}
}
}
@Preview
@Composable
fun AirPodsSettingsScreenPreview() {
Column(
modifier = Modifier.height(2000.dp)
) {
LibrePodsTheme(
darkTheme = true
) {
// AirPodsSettingsScreen(dev = null, service = AirPodsService(), navController = rememberNavController(), isConnected = true, isRemotelyConnected = false)
}
}
}

View File

@@ -81,7 +81,7 @@ fun AppSettingsScreen(
StyledScaffold(
title = stringResource(R.string.app_settings)
) { spacerHeight, hazeState ->
) { topPadding, hazeState, bottomPadding ->
Column(
modifier = Modifier
.fillMaxSize()
@@ -90,7 +90,7 @@ fun AppSettingsScreen(
.verticalScroll(scrollState)
.padding(horizontal = 16.dp)
) {
Spacer(modifier = Modifier.height(spacerHeight))
Spacer(modifier = Modifier.height(topPadding))
val isDarkTheme = isSystemInDarkTheme()
val backgroundColor = if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFFFFFFF)
@@ -189,19 +189,21 @@ fun AppSettingsScreen(
enabled = uiState.isPremium
)
Spacer(modifier = Modifier.height(16.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 (uiState.isPremium) viewModel.setShowCameraDialog(true)
},
independent = true,
description = stringResource(R.string.camera_control_app_description)
)
NavigationButton(
to = "",
title = stringResource(R.string.camera_control),
name = stringResource(R.string.set_custom_camera_package),
navController = navController,
onClick = {
if (uiState.isPremium) viewModel.setShowCameraDialog(true)
},
independent = true,
description = stringResource(R.string.camera_control_app_description)
)
}
Spacer(modifier = Modifier.height(16.dp))
if (BuildConfig.FLAVOR == "xposed") {
@@ -437,6 +439,7 @@ fun AppSettingsScreen(
}
})
}
Spacer(modifier = Modifier.height(bottomPadding))
}
}
}

View File

@@ -339,7 +339,7 @@ fun DebugScreen(navController: NavController) {
)
}
),
) { spacerHeight, hazeState ->
) { topPadding, hazeState, bottomPadding ->
Column(
modifier = Modifier
.fillMaxSize()
@@ -348,7 +348,7 @@ fun DebugScreen(navController: NavController) {
.layerBackdrop(backdrop)
.padding(horizontal = 16.dp)
) {
Spacer(modifier = Modifier.height(spacerHeight))
Spacer(modifier = Modifier.height(topPadding))
LazyColumn(
state = listState,
modifier = Modifier

View File

@@ -42,6 +42,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
@@ -139,7 +140,7 @@ fun HeadTrackingScreen(viewModel: AirPodsViewModel) {
)
}
),
) { spacerHeight, hazeState ->
) { topPadding, hazeState, _ ->
var gestureText by remember { mutableStateOf("") }
val coroutineScope = rememberCoroutineScope()
@@ -160,7 +161,7 @@ fun HeadTrackingScreen(viewModel: AirPodsViewModel) {
.padding(horizontal = 16.dp)
.verticalScroll(scrollState)
) {
Spacer(modifier = Modifier.height(spacerHeight))
Spacer(modifier = Modifier.height(topPadding))
val context = LocalContext.current
@@ -404,7 +405,8 @@ private fun HeadVisualization() {
.aspectRatio(2f),
colors = CardDefaults.cardColors(
containerColor = backgroundColor
)
),
shape = RoundedCornerShape(28.dp)
) {
Box(
modifier = Modifier.fillMaxSize(),
@@ -655,7 +657,8 @@ private fun AccelerationPlot() {
.height(300.dp),
colors = CardDefaults.cardColors(
containerColor = if (darkTheme) Color(0xFF1C1C1E) else Color.White
)
),
shape = RoundedCornerShape(28.dp)
) {
Box(
modifier = Modifier

View File

@@ -102,7 +102,7 @@ fun HearingAidScreen(viewModel: AirPodsViewModel, navController: NavController)
StyledScaffold(
title = stringResource(R.string.hearing_aid),
snackbarHostState = snackbarHostState,
) { spacerHeight, hazeState ->
) { topPadding, hazeState, bottomPadding ->
Column(
modifier = Modifier
.layerBackdrop(backdrop)
@@ -113,7 +113,7 @@ fun HearingAidScreen(viewModel: AirPodsViewModel, navController: NavController)
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
hazeStateS.value = hazeState
Spacer(modifier = Modifier.height(spacerHeight))
Spacer(modifier = Modifier.height(topPadding))
// val mediaAssistEnabled = remember { mutableStateOf(false) }
// val adjustMediaEnabled = remember { mutableStateOf(false) }
@@ -234,6 +234,7 @@ fun HearingAidScreen(viewModel: AirPodsViewModel, navController: NavController)
// independent = false
// )
// }
Spacer(modifier = Modifier.height(bottomPadding))
}
}

View File

@@ -104,7 +104,7 @@ fun TransparencySettingsScreen(viewModel: AirPodsViewModel) {
StyledScaffold(
title = stringResource(R.string.customize_transparency_mode)
){ spacerHeight, hazeState ->
){ topPadding, hazeState, bottomPadding ->
Column(
modifier = Modifier
.hazeSource(hazeState)
@@ -114,7 +114,7 @@ fun TransparencySettingsScreen(viewModel: AirPodsViewModel) {
.padding(horizontal = 16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
Spacer(modifier = Modifier.height(spacerHeight))
Spacer(modifier = Modifier.height(topPadding))
val backgroundColor = if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFFFFFFF)
val enabled = remember { mutableStateOf(false) }
@@ -441,6 +441,8 @@ fun TransparencySettingsScreen(viewModel: AirPodsViewModel) {
Spacer(modifier = Modifier.height(16.dp))
}
Spacer(modifier = Modifier.height(bottomPadding))
}
}
}

View File

@@ -216,7 +216,7 @@ fun TroubleshootingScreen(navController: NavController) {
) {
StyledScaffold(
title = stringResource(R.string.troubleshooting)
){ spacerHeight, hazeState ->
){ topPadding, hazeState, bottomPadding ->
Column(
modifier = Modifier
.fillMaxSize()
@@ -225,7 +225,7 @@ fun TroubleshootingScreen(navController: NavController) {
.verticalScroll(scrollState)
.padding(horizontal = 16.dp)
) {
Spacer(modifier = Modifier.height(spacerHeight))
Spacer(modifier = Modifier.height(topPadding))
Text(
text = stringResource(R.string.saved_logs),

View File

@@ -18,7 +18,6 @@
package me.kavishdevar.librepods.screens
import android.annotation.SuppressLint
import android.util.Log
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Arrangement
@@ -33,7 +32,6 @@ import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
@@ -58,7 +56,6 @@ import androidx.compose.ui.unit.sp
import com.kyant.backdrop.backdrops.layerBackdrop
import com.kyant.backdrop.backdrops.rememberLayerBackdrop
import dev.chrisbanes.haze.hazeSource
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import me.kavishdevar.librepods.R
@@ -69,7 +66,6 @@ import me.kavishdevar.librepods.utils.HearingAidSettings
import me.kavishdevar.librepods.utils.parseHearingAidSettingsResponse
import me.kavishdevar.librepods.utils.sendHearingAidSettings
import java.io.IOException
import kotlin.io.encoding.ExperimentalEncodingApi
private var debounceJob: MutableState<Job?> = mutableStateOf(null)
private const val TAG = "HearingAidAdjustments"
@@ -92,7 +88,7 @@ fun UpdateHearingTestScreen() {
val backdrop = rememberLayerBackdrop()
StyledScaffold(
title = stringResource(R.string.hearing_test)
) { spacerHeight, hazeState ->
) { topPadding, hazeState, bottomPadding ->
Column(
modifier = Modifier
.hazeSource(hazeState)
@@ -104,7 +100,7 @@ fun UpdateHearingTestScreen() {
) {
val textColor = if (isSystemInDarkTheme()) Color.White else Color.Black
Spacer(modifier = Modifier.height(spacerHeight))
Spacer(modifier = Modifier.height(topPadding))
Text(
text = stringResource(R.string.hearing_test_value_instruction),
@@ -356,6 +352,7 @@ fun UpdateHearingTestScreen() {
)
}
}
Spacer(modifier = Modifier.height(bottomPadding))
}
}
}

View File

@@ -28,6 +28,7 @@ import android.util.Log
import androidx.core.content.edit
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
@@ -35,6 +36,8 @@ import kotlinx.coroutines.launch
import me.kavishdevar.librepods.billing.BillingManager
import me.kavishdevar.librepods.constants.AirPodsNotifications
import me.kavishdevar.librepods.constants.Battery
import me.kavishdevar.librepods.constants.BatteryComponent
import me.kavishdevar.librepods.constants.BatteryStatus
import me.kavishdevar.librepods.constants.StemAction
import me.kavishdevar.librepods.data.ControlCommandRepository
import me.kavishdevar.librepods.services.AirPodsService
@@ -91,6 +94,11 @@ class AirPodsViewModel(
private val _uiState = MutableStateFlow(AirPodsUiState(deviceName = sharedPreferences.getString("name", "AirPods Pro") ?: "AirPods Pro"))
val uiState: StateFlow<AirPodsUiState> = _uiState
private var isDemoMode = false
val demoActivated = MutableSharedFlow<Unit>()
private var billingFirstCollectDone = false
private val listeners = mutableMapOf<
ControlCommandIdentifiers,
AACPManager.ControlCommandListener
@@ -120,6 +128,8 @@ class AirPodsViewModel(
loadSharedPreferences()
setupControlObservers()
observeBilling()
loadControlList()
if (isDemoMode) activateDemoMode()
}
override fun onCleared() {
@@ -138,14 +148,19 @@ class AirPodsViewModel(
}
private fun observeBilling() {
viewModelScope.launch {
if (!isDemoMode) viewModelScope.launch {
BillingManager.provider.isPremium.collect { premium ->
if (!billingFirstCollectDone) {
billingFirstCollectDone = true
return@collect
}
if (!premium) {
Log.d("AirPodsViewModel", "we are not premium")
setControlCommandBoolean(ControlCommandIdentifiers.CONVERSATION_DETECT_CONFIG, false)
setHeadGesturesEnabled(false)
} else {
Log.d("AirPodsViewModel", "we are premium")
}
_uiState.update { it.copy(isPremium = premium) }
}
}
@@ -154,7 +169,7 @@ class AirPodsViewModel(
private fun observeBroadcasts() {
broadcastReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
when (intent?.action) {
if (!isDemoMode) when (intent?.action) {
AirPodsNotifications.AIRPODS_CONNECTED -> {
_uiState.update {
it.copy(isLocallyConnected = true)
@@ -208,7 +223,7 @@ class AirPodsViewModel(
identifier: ControlCommandIdentifiers,
value: ByteArray
) {
controlRepo.setValue(identifier, value)
if (!isDemoMode) controlRepo.setValue(identifier, value)
_uiState.update {
it.copy(
controlStates = it.controlStates + (identifier to value)
@@ -290,6 +305,7 @@ class AirPodsViewModel(
}
fun refreshInitialData() {
if (isDemoMode) return
service.let { service ->
_uiState.update {
it.copy(
@@ -336,6 +352,14 @@ class AirPodsViewModel(
}
}
private fun loadControlList() {
_uiState.update {
it.copy(
controlStates = controlRepo.getMap()
)
}
}
private fun loadInstance() {
val instance = service.airpodsInstance ?: AirPodsInstance(
name = "AirPods",
@@ -414,4 +438,43 @@ class AirPodsViewModel(
fun purchase(context: Context) {
BillingManager.provider.purchase(context as Activity)
}
fun activateDemoMode() {
isDemoMode = true
viewModelScope.launch {
demoActivated.emit(Unit)
}
val fakeInstance = AirPodsInstance(
name = "AirPods Pro (Demo)",
model = AirPodsModels.getModelByModelNumber("A3049")!!,
actualModelNumber = "A3049",
aacpManager = service.aacpManager,
serialNumber = "DEMO123",
leftSerialNumber = "L-DEMO",
rightSerialNumber = "R-DEMO",
version1 = "1.0",
version2 = "1.0",
version3 = "1.0",
attManager = null
)
_uiState.update {
it.copy(
isLocallyConnected = true,
instance = fakeInstance,
capabilities = fakeInstance.model.capabilities,
battery = listOf(
Battery(BatteryComponent.LEFT, 85, BatteryStatus.CHARGING),
Battery(BatteryComponent.RIGHT, 25, BatteryStatus.NOT_CHARGING),
Battery(BatteryComponent.CASE, 85, BatteryStatus.CHARGING),
),
modelName = fakeInstance.model.displayName,
actualModel = fakeInstance.actualModelNumber,
serialNumbers = listOf("DEMO", "DEMO", "DEMO"),
version3 = "Demo Firmware"
)
}
}
}