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 package me.kavishdevar.librepods.composables
import android.content.res.Configuration import android.content.res.Configuration
import android.util.Log
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.isSystemInDarkTheme 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.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.widthIn
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
@@ -39,6 +41,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ImageBitmap import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.res.imageResource import androidx.compose.ui.res.imageResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
@@ -74,76 +77,84 @@ fun BatteryView(
val singleDisplayed = remember { mutableStateOf(false) } val singleDisplayed = remember { mutableStateOf(false) }
Row { Box(
Column( modifier = Modifier.fillMaxWidth(),
modifier = Modifier.fillMaxWidth(0.5f), contentAlignment = Alignment.Center
horizontalAlignment = Alignment.CenterHorizontally ) {
Row(
modifier = Modifier.widthIn(max = 500.dp),
horizontalArrangement = Arrangement.Center
) { ) {
Image( Column(
bitmap = ImageBitmap.imageResource(budsRes), modifier = Modifier.weight(1f),
contentDescription = stringResource(R.string.buds), horizontalAlignment = Alignment.CenterHorizontally
modifier = Modifier
.fillMaxWidth()
.padding(8.dp)
)
if (
leftCharging == rightCharging &&
(leftLevel - rightLevel) in -3..3
) { ) {
BatteryIndicator( Image(
leftLevel.coerceAtMost(rightLevel), bitmap = ImageBitmap.imageResource(budsRes),
leftCharging contentDescription = stringResource(R.string.buds),
modifier = Modifier
.fillMaxWidth()
.padding(8.dp)
) )
singleDisplayed.value = true
} else {
singleDisplayed.value = false
Row( if (
modifier = Modifier.fillMaxWidth(), leftCharging == rightCharging &&
horizontalArrangement = Arrangement.Center (leftLevel - rightLevel) in -3..3
) { ) {
if (leftLevel > 0 || left?.status != BatteryStatus.DISCONNECTED) { BatteryIndicator(
BatteryIndicator( leftLevel.coerceAtMost(rightLevel),
leftLevel, leftCharging
leftCharging, )
"\uDBC6\uDCE5" singleDisplayed.value = true
) } else {
} singleDisplayed.value = false
if (leftLevel > 0 && rightLevel > 0) { Row(
Spacer(modifier = Modifier.width(16.dp)) 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) { if (leftLevel > 0 && rightLevel > 0) {
BatteryIndicator( Spacer(modifier = Modifier.width(16.dp))
rightLevel, }
rightCharging,
"\uDBC6\uDCE8" if (rightLevel > 0 || right?.status != BatteryStatus.DISCONNECTED) {
) BatteryIndicator(
rightLevel,
rightCharging,
"\uDBC6\uDCE8"
)
}
} }
} }
} }
}
Column( Column(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.weight(1f),
horizontalAlignment = Alignment.CenterHorizontally horizontalAlignment = Alignment.CenterHorizontally
) { ) {
Image( Image(
bitmap = ImageBitmap.imageResource(caseRes), bitmap = ImageBitmap.imageResource(caseRes),
contentDescription = stringResource(R.string.case_alt), contentDescription = stringResource(R.string.case_alt),
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(8.dp) .padding(8.dp)
)
if (caseLevel > 0 || case?.status != BatteryStatus.DISCONNECTED) {
BatteryIndicator(
caseLevel,
caseCharging,
prefix = if (!singleDisplayed.value) "\uDBC3\uDE6C" else ""
) )
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.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.compose.ui.zIndex 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.layerBackdrop import com.kyant.backdrop.backdrops.layerBackdrop
import com.kyant.backdrop.backdrops.rememberLayerBackdrop import com.kyant.backdrop.backdrops.rememberLayerBackdrop
@@ -57,7 +56,6 @@ import dev.chrisbanes.haze.HazeProgressive
import dev.chrisbanes.haze.HazeState import dev.chrisbanes.haze.HazeState
import dev.chrisbanes.haze.HazeTint import dev.chrisbanes.haze.HazeTint
import dev.chrisbanes.haze.hazeEffect import dev.chrisbanes.haze.hazeEffect
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
import dev.chrisbanes.haze.rememberHazeState import dev.chrisbanes.haze.rememberHazeState
import me.kavishdevar.librepods.R import me.kavishdevar.librepods.R
@@ -66,7 +64,7 @@ fun StyledScaffold(
title: String, title: String,
actionButtons: List<@Composable (backdrop: LayerBackdrop) -> Unit> = emptyList(), actionButtons: List<@Composable (backdrop: LayerBackdrop) -> Unit> = emptyList(),
snackbarHostState: SnackbarHostState = remember { SnackbarHostState() }, snackbarHostState: SnackbarHostState = remember { SnackbarHostState() },
content: @Composable (spacerValue: Dp, hazeState: HazeState) -> Unit content: @Composable (spacerValue: Dp, hazeState: HazeState, bottomPadding: Dp) -> Unit
) { ) {
val isDarkTheme = isSystemInDarkTheme() val isDarkTheme = isSystemInDarkTheme()
val hazeState = rememberHazeState(blurEnabled = true) val hazeState = rememberHazeState(blurEnabled = true)
@@ -86,7 +84,7 @@ fun StyledScaffold(
Box( Box(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.padding(start = startPadding, end = endPadding, bottom = bottomPadding) .padding(start = startPadding, end = endPadding)
) { ) {
val backdrop = rememberLayerBackdrop() val backdrop = rememberLayerBackdrop()
Box( 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, title = title,
actionButtons = actionButtons, actionButtons = actionButtons,
snackbarHostState = snackbarHostState, snackbarHostState = snackbarHostState,
) { _, _ -> ) { _, _, _->
content() content()
} }
} }
@@ -159,7 +157,7 @@ fun StyledScaffold(
title = title, title = title,
actionButtons = actionButtons, actionButtons = actionButtons,
snackbarHostState = snackbarHostState, snackbarHostState = snackbarHostState,
) { spacerValue, _ -> ) { spacerValue, _, _ ->
content(spacerValue) content(spacerValue)
} }
} }

View File

@@ -19,6 +19,7 @@
package me.kavishdevar.librepods.data package me.kavishdevar.librepods.data
import me.kavishdevar.librepods.utils.AACPManager import me.kavishdevar.librepods.utils.AACPManager
import me.kavishdevar.librepods.utils.AACPManager.Companion.ControlCommandIdentifiers
class ControlCommandRepository( class ControlCommandRepository(
private val aacpManager: AACPManager private val aacpManager: AACPManager
@@ -60,4 +61,10 @@ class ControlCommandRepository(
) { ) {
aacpManager.unregisterControlCommandListener(identifier, listener) 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( StyledScaffold(
title = stringResource(R.string.accessibility) title = stringResource(R.string.accessibility)
) { spacerHeight, hazeState -> ) { topPadding, hazeState, bottomPadding ->
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
@@ -111,7 +111,7 @@ fun AccessibilitySettingsScreen(viewModel: AirPodsViewModel, navController: NavC
.padding(horizontal = 16.dp), .padding(horizontal = 16.dp),
verticalArrangement = Arrangement.spacedBy(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 phoneMediaEQ = remember { mutableStateOf(FloatArray(8) { 0.5f }) }
// val phoneEQEnabled = remember { mutableStateOf(false) } // 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.annotation.SuppressLint
import android.content.Context.MODE_PRIVATE import android.content.Context.MODE_PRIVATE
import android.content.SharedPreferences import android.content.SharedPreferences
import android.util.Log
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
@@ -42,12 +44,16 @@ import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableLongStateOf
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color 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.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextStyle 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.font.FontWeight
import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.text.style.TextAlign 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.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.navigation.NavController import androidx.navigation.NavController
@@ -66,6 +71,7 @@ import com.kyant.backdrop.highlight.Highlight
import dev.chrisbanes.haze.HazeState import dev.chrisbanes.haze.HazeState
import dev.chrisbanes.haze.hazeSource import dev.chrisbanes.haze.hazeSource
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
import kotlinx.coroutines.delay
import me.kavishdevar.librepods.BuildConfig import me.kavishdevar.librepods.BuildConfig
import me.kavishdevar.librepods.R import me.kavishdevar.librepods.R
import me.kavishdevar.librepods.composables.AboutCard 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.StyledIconButton
import me.kavishdevar.librepods.composables.StyledScaffold import me.kavishdevar.librepods.composables.StyledScaffold
import me.kavishdevar.librepods.composables.StyledToggle import me.kavishdevar.librepods.composables.StyledToggle
import me.kavishdevar.librepods.ui.theme.LibrePodsTheme
import me.kavishdevar.librepods.utils.AACPManager import me.kavishdevar.librepods.utils.AACPManager
import me.kavishdevar.librepods.utils.ATTHandles import me.kavishdevar.librepods.utils.ATTHandles
import me.kavishdevar.librepods.utils.AirPodsPro3 import me.kavishdevar.librepods.utils.AirPodsPro3
@@ -131,15 +136,24 @@ fun AirPodsSettingsScreen(viewModel: AirPodsViewModel, navController: NavControl
StyledScaffold( StyledScaffold(
title = deviceName.text, actionButtons = listOf( title = deviceName.text, actionButtons = listOf(
{ scaffoldBackdrop -> { scaffoldBackdrop ->
StyledIconButton( StyledIconButton(
onClick = { navController.navigate("app_settings") }, onClick = { navController.navigate("app_settings") },
icon = "􀍟", icon = "􀍟",
backdrop = scaffoldBackdrop backdrop = scaffoldBackdrop
) )
}), snackbarHostState = snackbarHostState }), snackbarHostState = snackbarHostState
) { spacerHeight, hazeState -> ) { topPadding, hazeState, bottomPadding ->
hazeStateS.value = hazeState hazeStateS.value = hazeState
var blockTouches by remember { mutableStateOf(false) }
LaunchedEffect(Unit) {
viewModel.demoActivated.collect {
blockTouches = true
delay(1000)
blockTouches = false
}
}
if (state.isLocallyConnected) { if (state.isLocallyConnected) {
val capabilities = state.capabilities val capabilities = state.capabilities
@@ -148,8 +162,18 @@ fun AirPodsSettingsScreen(viewModel: AirPodsViewModel, navController: NavControl
.fillMaxSize() .fillMaxSize()
.hazeSource(hazeState) .hazeSource(hazeState)
.padding(horizontal = 16.dp) .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") { item(key = "battery") {
BatteryView( BatteryView(
batteryList = state.battery, batteryList = state.battery,
@@ -341,8 +365,7 @@ fun AirPodsSettingsScreen(viewModel: AirPodsViewModel, navController: NavControl
viewModel.setAutomaticEarDetectionEnabled(it) viewModel.setAutomaticEarDetectionEnabled(it)
}, },
automaticConnectionEnabled = state.automaticConnectionEnabled, automaticConnectionEnabled = state.automaticConnectionEnabled,
onAutomaticConnectionChanged = { viewModel.setAutomaticConnectionEnabled(it) } onAutomaticConnectionChanged = { viewModel.setAutomaticConnectionEnabled(it) })
)
} }
item(key = "spacer_microphone") { Spacer(modifier = Modifier.height(16.dp)) } 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 = "spacer_debug") { Spacer(modifier = Modifier.height(16.dp)) }
// item(key = "debug") { NavigationButton("debug", "Debug", navController) } // 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 { } else {
val backdrop = rememberLayerBackdrop() val backdrop = rememberLayerBackdrop()
@@ -440,26 +463,54 @@ fun AirPodsSettingsScreen(viewModel: AirPodsViewModel, navController: NavControl
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center verticalArrangement = Arrangement.Center
) { ) {
Text( val tapCount = remember { mutableIntStateOf(0) }
text = stringResource(R.string.airpods_not_connected), style = TextStyle( val lastTapTime = remember { mutableLongStateOf(0L) }
fontSize = 24.sp, Column(
fontWeight = FontWeight.Medium, modifier = Modifier
color = if (isSystemInDarkTheme()) Color.White else Color.Black, .fillMaxWidth()
fontFamily = FontFamily(Font(R.font.sf_pro)) .pointerInput(Unit) {
), textAlign = TextAlign.Center, modifier = Modifier.fillMaxWidth() detectTapGestures(
) onTap = {
Spacer(Modifier.height(24.dp)) val now = System.currentTimeMillis()
Text(
text = stringResource(R.string.airpods_not_connected_description), if (now - lastTapTime.longValue > 400) {
style = TextStyle( tapCount.intValue = 0
fontSize = 16.sp, }
fontWeight = FontWeight.Light,
color = if (isSystemInDarkTheme()) Color.White else Color.Black, tapCount.intValue++
fontFamily = FontFamily(Font(R.font.sf_pro)) lastTapTime.longValue = now
),
textAlign = TextAlign.Center, if (tapCount.intValue >= 5) {
modifier = Modifier.fillMaxWidth() 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)) Spacer(Modifier.height(32.dp))
// StyledButton( // StyledButton(
// onClick = { navController.navigate("troubleshooting") }, // 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( StyledScaffold(
title = stringResource(R.string.app_settings) title = stringResource(R.string.app_settings)
) { spacerHeight, hazeState -> ) { topPadding, hazeState, bottomPadding ->
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
@@ -90,7 +90,7 @@ fun AppSettingsScreen(
.verticalScroll(scrollState) .verticalScroll(scrollState)
.padding(horizontal = 16.dp) .padding(horizontal = 16.dp)
) { ) {
Spacer(modifier = Modifier.height(spacerHeight)) Spacer(modifier = Modifier.height(topPadding))
val isDarkTheme = isSystemInDarkTheme() val isDarkTheme = isSystemInDarkTheme()
val backgroundColor = if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFFFFFFF) val backgroundColor = if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFFFFFFF)
@@ -189,19 +189,21 @@ fun AppSettingsScreen(
enabled = uiState.isPremium enabled = uiState.isPremium
) )
Spacer(modifier = Modifier.height(16.dp)) if (!BuildConfig.PLAY_BUILD) {
Spacer(modifier = Modifier.height(16.dp))
NavigationButton( NavigationButton(
to = "", to = "",
title = stringResource(R.string.camera_control), title = stringResource(R.string.camera_control),
name = stringResource(R.string.set_custom_camera_package), name = stringResource(R.string.set_custom_camera_package),
navController = navController, navController = navController,
onClick = { onClick = {
if (uiState.isPremium) viewModel.setShowCameraDialog(true) if (uiState.isPremium) viewModel.setShowCameraDialog(true)
}, },
independent = true, independent = true,
description = stringResource(R.string.camera_control_app_description) description = stringResource(R.string.camera_control_app_description)
) )
}
Spacer(modifier = Modifier.height(16.dp)) Spacer(modifier = Modifier.height(16.dp))
if (BuildConfig.FLAVOR == "xposed") { 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( Column(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
@@ -348,7 +348,7 @@ fun DebugScreen(navController: NavController) {
.layerBackdrop(backdrop) .layerBackdrop(backdrop)
.padding(horizontal = 16.dp) .padding(horizontal = 16.dp)
) { ) {
Spacer(modifier = Modifier.height(spacerHeight)) Spacer(modifier = Modifier.height(topPadding))
LazyColumn( LazyColumn(
state = listState, state = listState,
modifier = Modifier 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.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Card import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults import androidx.compose.material3.CardDefaults
@@ -139,7 +140,7 @@ fun HeadTrackingScreen(viewModel: AirPodsViewModel) {
) )
} }
), ),
) { spacerHeight, hazeState -> ) { topPadding, hazeState, _ ->
var gestureText by remember { mutableStateOf("") } var gestureText by remember { mutableStateOf("") }
val coroutineScope = rememberCoroutineScope() val coroutineScope = rememberCoroutineScope()
@@ -160,7 +161,7 @@ fun HeadTrackingScreen(viewModel: AirPodsViewModel) {
.padding(horizontal = 16.dp) .padding(horizontal = 16.dp)
.verticalScroll(scrollState) .verticalScroll(scrollState)
) { ) {
Spacer(modifier = Modifier.height(spacerHeight)) Spacer(modifier = Modifier.height(topPadding))
val context = LocalContext.current val context = LocalContext.current
@@ -404,7 +405,8 @@ private fun HeadVisualization() {
.aspectRatio(2f), .aspectRatio(2f),
colors = CardDefaults.cardColors( colors = CardDefaults.cardColors(
containerColor = backgroundColor containerColor = backgroundColor
) ),
shape = RoundedCornerShape(28.dp)
) { ) {
Box( Box(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
@@ -655,7 +657,8 @@ private fun AccelerationPlot() {
.height(300.dp), .height(300.dp),
colors = CardDefaults.cardColors( colors = CardDefaults.cardColors(
containerColor = if (darkTheme) Color(0xFF1C1C1E) else Color.White containerColor = if (darkTheme) Color(0xFF1C1C1E) else Color.White
) ),
shape = RoundedCornerShape(28.dp)
) { ) {
Box( Box(
modifier = Modifier modifier = Modifier

View File

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

View File

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

View File

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

View File

@@ -18,7 +18,6 @@
package me.kavishdevar.librepods.screens package me.kavishdevar.librepods.screens
import android.annotation.SuppressLint
import android.util.Log import android.util.Log
import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Arrangement 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.rememberScrollState
import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable 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.layerBackdrop
import com.kyant.backdrop.backdrops.rememberLayerBackdrop import com.kyant.backdrop.backdrops.rememberLayerBackdrop
import dev.chrisbanes.haze.hazeSource import dev.chrisbanes.haze.hazeSource
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import me.kavishdevar.librepods.R 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.parseHearingAidSettingsResponse
import me.kavishdevar.librepods.utils.sendHearingAidSettings import me.kavishdevar.librepods.utils.sendHearingAidSettings
import java.io.IOException import java.io.IOException
import kotlin.io.encoding.ExperimentalEncodingApi
private var debounceJob: MutableState<Job?> = mutableStateOf(null) private var debounceJob: MutableState<Job?> = mutableStateOf(null)
private const val TAG = "HearingAidAdjustments" private const val TAG = "HearingAidAdjustments"
@@ -92,7 +88,7 @@ fun UpdateHearingTestScreen() {
val backdrop = rememberLayerBackdrop() val backdrop = rememberLayerBackdrop()
StyledScaffold( StyledScaffold(
title = stringResource(R.string.hearing_test) title = stringResource(R.string.hearing_test)
) { spacerHeight, hazeState -> ) { topPadding, hazeState, bottomPadding ->
Column( Column(
modifier = Modifier modifier = Modifier
.hazeSource(hazeState) .hazeSource(hazeState)
@@ -104,7 +100,7 @@ fun UpdateHearingTestScreen() {
) { ) {
val textColor = if (isSystemInDarkTheme()) Color.White else Color.Black val textColor = if (isSystemInDarkTheme()) Color.White else Color.Black
Spacer(modifier = Modifier.height(spacerHeight)) Spacer(modifier = Modifier.height(topPadding))
Text( Text(
text = stringResource(R.string.hearing_test_value_instruction), 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.core.content.edit
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
@@ -35,6 +36,8 @@ import kotlinx.coroutines.launch
import me.kavishdevar.librepods.billing.BillingManager import me.kavishdevar.librepods.billing.BillingManager
import me.kavishdevar.librepods.constants.AirPodsNotifications import me.kavishdevar.librepods.constants.AirPodsNotifications
import me.kavishdevar.librepods.constants.Battery import me.kavishdevar.librepods.constants.Battery
import me.kavishdevar.librepods.constants.BatteryComponent
import me.kavishdevar.librepods.constants.BatteryStatus
import me.kavishdevar.librepods.constants.StemAction import me.kavishdevar.librepods.constants.StemAction
import me.kavishdevar.librepods.data.ControlCommandRepository import me.kavishdevar.librepods.data.ControlCommandRepository
import me.kavishdevar.librepods.services.AirPodsService 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")) private val _uiState = MutableStateFlow(AirPodsUiState(deviceName = sharedPreferences.getString("name", "AirPods Pro") ?: "AirPods Pro"))
val uiState: StateFlow<AirPodsUiState> = _uiState val uiState: StateFlow<AirPodsUiState> = _uiState
private var isDemoMode = false
val demoActivated = MutableSharedFlow<Unit>()
private var billingFirstCollectDone = false
private val listeners = mutableMapOf< private val listeners = mutableMapOf<
ControlCommandIdentifiers, ControlCommandIdentifiers,
AACPManager.ControlCommandListener AACPManager.ControlCommandListener
@@ -120,6 +128,8 @@ class AirPodsViewModel(
loadSharedPreferences() loadSharedPreferences()
setupControlObservers() setupControlObservers()
observeBilling() observeBilling()
loadControlList()
if (isDemoMode) activateDemoMode()
} }
override fun onCleared() { override fun onCleared() {
@@ -138,14 +148,19 @@ class AirPodsViewModel(
} }
private fun observeBilling() { private fun observeBilling() {
viewModelScope.launch { if (!isDemoMode) viewModelScope.launch {
BillingManager.provider.isPremium.collect { premium -> BillingManager.provider.isPremium.collect { premium ->
if (!billingFirstCollectDone) {
billingFirstCollectDone = true
return@collect
}
if (!premium) { if (!premium) {
Log.d("AirPodsViewModel", "we are not premium")
setControlCommandBoolean(ControlCommandIdentifiers.CONVERSATION_DETECT_CONFIG, false) setControlCommandBoolean(ControlCommandIdentifiers.CONVERSATION_DETECT_CONFIG, false)
setHeadGesturesEnabled(false) setHeadGesturesEnabled(false)
} else {
Log.d("AirPodsViewModel", "we are premium")
} }
_uiState.update { it.copy(isPremium = premium) } _uiState.update { it.copy(isPremium = premium) }
} }
} }
@@ -154,7 +169,7 @@ class AirPodsViewModel(
private fun observeBroadcasts() { private fun observeBroadcasts() {
broadcastReceiver = object : BroadcastReceiver() { broadcastReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) { override fun onReceive(context: Context?, intent: Intent?) {
when (intent?.action) { if (!isDemoMode) when (intent?.action) {
AirPodsNotifications.AIRPODS_CONNECTED -> { AirPodsNotifications.AIRPODS_CONNECTED -> {
_uiState.update { _uiState.update {
it.copy(isLocallyConnected = true) it.copy(isLocallyConnected = true)
@@ -208,7 +223,7 @@ class AirPodsViewModel(
identifier: ControlCommandIdentifiers, identifier: ControlCommandIdentifiers,
value: ByteArray value: ByteArray
) { ) {
controlRepo.setValue(identifier, value) if (!isDemoMode) controlRepo.setValue(identifier, value)
_uiState.update { _uiState.update {
it.copy( it.copy(
controlStates = it.controlStates + (identifier to value) controlStates = it.controlStates + (identifier to value)
@@ -290,6 +305,7 @@ class AirPodsViewModel(
} }
fun refreshInitialData() { fun refreshInitialData() {
if (isDemoMode) return
service.let { service -> service.let { service ->
_uiState.update { _uiState.update {
it.copy( it.copy(
@@ -336,6 +352,14 @@ class AirPodsViewModel(
} }
} }
private fun loadControlList() {
_uiState.update {
it.copy(
controlStates = controlRepo.getMap()
)
}
}
private fun loadInstance() { private fun loadInstance() {
val instance = service.airpodsInstance ?: AirPodsInstance( val instance = service.airpodsInstance ?: AirPodsInstance(
name = "AirPods", name = "AirPods",
@@ -414,4 +438,43 @@ class AirPodsViewModel(
fun purchase(context: Context) { fun purchase(context: Context) {
BillingManager.provider.purchase(context as Activity) 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"
)
}
}
} }