android: refactor... stuff (3, see description)

implement head gestures for calls, clean up code ig
This commit is contained in:
Kavish Devar
2026-08-16 21:02:11 +05:30
parent ddd1a9c5ce
commit e950aa010d
72 changed files with 801 additions and 1179 deletions
+1 -1
View File
@@ -77,7 +77,7 @@ android {
defaultConfig { defaultConfig {
applicationId = "me.kavishdevar.librepods" applicationId = "me.kavishdevar.librepods"
targetSdk = 37 targetSdk = 37
versionCode = 65 versionCode = 80
versionName = appVersionName versionName = appVersionName
} }
buildTypes { buildTypes {
@@ -2,7 +2,7 @@
"formatVersion": 1, "formatVersion": 1,
"database": { "database": {
"version": 1, "version": 1,
"identityHash": "7a502eac34ab57e1c12ba8f849e172cf", "identityHash": "c547604d916583c3b94c7bf1ac787fdf",
"entities": [ "entities": [
{ {
"tableName": "AppleEntity", "tableName": "AppleEntity",
@@ -102,7 +102,7 @@
}, },
{ {
"tableName": "AppStateEntity", "tableName": "AppStateEntity",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `hasCompletedOnboarding` INTEGER NOT NULL, `lastVersionShown` TEXT, `hasConnectedToAACP` INTEGER NOT NULL, `firstSuccessfulConnectionTime` INTEGER, `reviewPrompted` INTEGER NOT NULL, `timeUntilFOSSPremiumExpiry` INTEGER NOT NULL, PRIMARY KEY(`id`))", "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `isPremium` INTEGER NOT NULL, `hasCompletedOnboarding` INTEGER NOT NULL, `lastVersionShown` TEXT, `hasConnectedToAACP` INTEGER NOT NULL, `firstSuccessfulConnectionTime` INTEGER, `reviewPrompted` INTEGER NOT NULL, `timeUntilFOSSPremiumExpiry` INTEGER NOT NULL, PRIMARY KEY(`id`))",
"fields": [ "fields": [
{ {
"fieldPath": "id", "fieldPath": "id",
@@ -110,6 +110,12 @@
"affinity": "INTEGER", "affinity": "INTEGER",
"notNull": true "notNull": true
}, },
{
"fieldPath": "isPremium",
"columnName": "isPremium",
"affinity": "INTEGER",
"notNull": true
},
{ {
"fieldPath": "hasCompletedOnboarding", "fieldPath": "hasCompletedOnboarding",
"columnName": "hasCompletedOnboarding", "columnName": "hasCompletedOnboarding",
@@ -209,7 +215,7 @@
], ],
"setupQueries": [ "setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '7a502eac34ab57e1c12ba8f849e172cf')" "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'c547604d916583c3b94c7bf1ac787fdf')"
] ]
} }
} }
@@ -8,6 +8,8 @@ import androidx.lifecycle.ProcessLifecycleOwner
import androidx.room3.Room import androidx.room3.Room
import io.github.libxposed.service.XposedService import io.github.libxposed.service.XposedService
import io.github.libxposed.service.XposedServiceHelper import io.github.libxposed.service.XposedServiceHelper
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
import me.kavishdevar.librepods.billing.BillingManager import me.kavishdevar.librepods.billing.BillingManager
import me.kavishdevar.librepods.billing.BillingProviderFactory import me.kavishdevar.librepods.billing.BillingProviderFactory
import me.kavishdevar.librepods.database.LibrePodsDatabase import me.kavishdevar.librepods.database.LibrePodsDatabase
@@ -50,6 +52,11 @@ class LibrePodsApplication: Application(), XposedServiceHelper.OnServiceListener
).build() ).build()
XposedServiceHelper.registerListener(this) XposedServiceHelper.registerListener(this)
runBlocking(Dispatchers.IO) {
appDataRepository.awaitInitialized()
}
BillingManager.provider = BillingProviderFactory.create(this) BillingManager.provider = BillingProviderFactory.create(this)
ProcessLifecycleOwner.get().lifecycle.addObserver(this) ProcessLifecycleOwner.get().lifecycle.addObserver(this)
@@ -21,7 +21,6 @@ package me.kavishdevar.librepods.billing
import android.app.Activity import android.app.Activity
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import androidx.core.content.edit
import androidx.core.net.toUri import androidx.core.net.toUri
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@@ -31,17 +30,19 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import me.kavishdevar.librepods.LibrePodsApplication
import me.kavishdevar.librepods.R import me.kavishdevar.librepods.R
import kotlin.time.Duration.Companion.seconds
class FOSSBillingProvider(context: Context): BillingProvider { class FOSSBillingProvider(context: Context): BillingProvider {
private val _isPremium = MutableStateFlow(false) private val appDataRepository = (context.applicationContext as LibrePodsApplication).appDataRepository
private val _isPremium = MutableStateFlow(appDataRepository.state.value.isPremium)
override val isPremium: StateFlow<Boolean> = _isPremium override val isPremium: StateFlow<Boolean> = _isPremium
private val _price = MutableStateFlow(context.getString(R.string.name_your_own_price)) private val _price = MutableStateFlow(context.getString(R.string.name_your_own_price))
override val price: StateFlow<String> = _price override val price: StateFlow<String> = _price
private val sharedPreferences = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO) private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
private var purchaseJob: Job? = null private var purchaseJob: Job? = null
@@ -57,21 +58,18 @@ class FOSSBillingProvider(context: Context): BillingProvider {
purchaseJob?.cancel() purchaseJob?.cancel()
purchaseJob = scope.launch { purchaseJob = scope.launch {
delay(5_000) delay(5.seconds)
_isPremium.value = true _isPremium.value = true
sharedPreferences.edit { putBoolean("foss_upgraded", true) } appDataRepository.updateState { it.copy(isPremium = true) }
} }
} }
override fun queryPurchases() { override fun queryPurchases() {
val stored = sharedPreferences.getBoolean("foss_upgraded", false) _isPremium.value = appDataRepository.state.value.isPremium
if (stored != _isPremium.value) {
_isPremium.value = stored
}
} }
override fun restorePurchases() { override fun restorePurchases() {
_isPremium.value = true _isPremium.value = true
sharedPreferences.edit { putBoolean("foss_upgraded", true) } appDataRepository.updateState { it.copy(isPremium = true) }
} }
} }
@@ -41,6 +41,7 @@ import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import me.kavishdevar.librepods.LibrePodsApplication
const val TAG = "PlayBillingProvider" const val TAG = "PlayBillingProvider"
@@ -49,10 +50,11 @@ private const val PREMIUM_PRODUCT_ID = "librepods.advanced_features.v2"
class PlayBillingProvider( class PlayBillingProvider(
context: Context context: Context
) : BillingProvider, PurchasesUpdatedListener { ) : BillingProvider, PurchasesUpdatedListener {
private val appDataRepository = (context.applicationContext as LibrePodsApplication).appDataRepository
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO) private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
private val _isPremium = MutableStateFlow(false) private val _isPremium = MutableStateFlow(appDataRepository.state.value.isPremium)
override val isPremium: StateFlow<Boolean> = _isPremium override val isPremium: StateFlow<Boolean> = _isPremium
private val _price = MutableStateFlow("unknown") private val _price = MutableStateFlow("unknown")
@@ -177,6 +179,8 @@ class PlayBillingProvider(
_isPremium.value = hasPremium _isPremium.value = hasPremium
appDataRepository.updateState { it.copy(isPremium = hasPremium) }
scope.launch { scope.launch {
purchases purchases
.filter { it.purchaseState == Purchase.PurchaseState.PURCHASED && !it.isAcknowledged } .filter { it.purchaseState == Purchase.PurchaseState.PURCHASED && !it.isAcknowledged }
@@ -637,8 +637,9 @@ class AACPManager(private val device: AppleDevice) {
return sendPacket(packet) return sendPacket(packet)
} }
// todo: implement adaptive volume
fun sendSourceFeatureCapabilities(): Boolean { fun sendSourceFeatureCapabilities(): Boolean {
val payload = byteArrayOf(0xFF.toByte(), 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00) val payload = byteArrayOf(0xd7.toByte(), 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00)
val packet = AACPPacket.createUnknownPacket( val packet = AACPPacket.createUnknownPacket(
opcode = MessageOpcode.SOURCE_FEATURE_CAPABILITIES, opcode = MessageOpcode.SOURCE_FEATURE_CAPABILITIES,
payload payload
@@ -8,6 +8,8 @@ data class AppStateEntity(
@PrimaryKey @PrimaryKey
val id: Int = 0, val id: Int = 0,
val isPremium: Boolean = false,
val hasCompletedOnboarding: Boolean = false, val hasCompletedOnboarding: Boolean = false,
val lastVersionShown: String? = null, val lastVersionShown: String? = null,
@@ -75,6 +75,12 @@ class AppleDevice(
_metadata.update(transform) _metadata.update(transform)
} }
internal inline fun updateSettings(
transform: (AppleSettings) -> AppleSettings
) {
_settings.update(transform)
}
internal suspend fun emitEvent(event: AppleEvent) { internal suspend fun emitEvent(event: AppleEvent) {
_events.emit(event) _events.emit(event)
} }
@@ -197,12 +203,9 @@ class AppleDevice(
currentByte or modeBit currentByte or modeBit
} }
setControlCommand(ControlCommandIdentifier.LISTENING_MODE_CONFIGS, newValue) setControlCommand(ControlCommandIdentifier.LISTENING_MODE_CONFIGS, newValue)
// sharedPreferences.edit { putInt("long_press_byte", newValue) }
} }
fun setLongPressAction(side: String, action: StemAction) { fun setLongPressAction(side: String, action: StemAction) {
// val prefKey = if (side.lowercase() == "left") "left_long_press_action" else "right_long_press_action"
// sharedPreferences.edit { putString(prefKey, action.name) }
_settings.update { _settings.update {
if (side.lowercase() == "left") it.copy(leftLongPressAction = action) else it.copy(rightLongPressAction = action) if (side.lowercase() == "left") it.copy(leftLongPressAction = action) else it.copy(rightLongPressAction = action)
} }
@@ -9,6 +9,8 @@ import kotlin.time.Duration.Companion.milliseconds
data class AppleSettings( data class AppleSettings(
val disconnectWhenNotWearing: Boolean = true, // disconnect_when_not_wearing val disconnectWhenNotWearing: Boolean = true, // disconnect_when_not_wearing
val earDetectionEnabled: Boolean = true,
val cacheDisconnectedComponentBattery: Boolean = true, val cacheDisconnectedComponentBattery: Boolean = true,
val headGesturesEnabled: Boolean = true, // head_gestures_enabled val headGesturesEnabled: Boolean = true, // head_gestures_enabled
@@ -33,6 +35,9 @@ data class AppleSettings(
val conversationalAwarenessPauseMusicEnabled: Boolean = false, // conversational_awareness_pause_music val conversationalAwarenessPauseMusicEnabled: Boolean = false, // conversational_awareness_pause_music
val relativeConversationalAwarenessVolumeEnabled: Boolean = true, // relative_conversational_awareness_volume val relativeConversationalAwarenessVolumeEnabled: Boolean = true, // relative_conversational_awareness_volume
val conversationalAwarenessVolume: Float = 43f, // conversational_awareness_volume val conversationalAwarenessVolume: Float = 43f,
val conversationalAwarenessReducedVolume: Float = 20f,
val hrAlertEnabled: Boolean = true,
val hrmAlertThreshold: Int = 120,
): DeviceSettings ): DeviceSettings
@@ -25,7 +25,6 @@ import android.app.Activity
import android.content.BroadcastReceiver import android.content.BroadcastReceiver
import android.content.ComponentName import android.content.ComponentName
import android.content.Context import android.content.Context
import android.content.Context.MODE_PRIVATE
import android.content.Intent import android.content.Intent
import android.content.ServiceConnection import android.content.ServiceConnection
import android.hardware.display.DisplayManager import android.hardware.display.DisplayManager
@@ -46,7 +45,6 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalView import androidx.compose.ui.platform.LocalView
import androidx.core.content.edit
import androidx.core.view.WindowCompat import androidx.core.view.WindowCompat
import com.google.android.play.core.review.ReviewManagerFactory import com.google.android.play.core.review.ReviewManagerFactory
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
@@ -56,9 +54,11 @@ import me.kavishdevar.librepods.LibrePodsApplication
import me.kavishdevar.librepods.presentation.navigation.NavigationRoot import me.kavishdevar.librepods.presentation.navigation.NavigationRoot
import me.kavishdevar.librepods.presentation.theme.LibrePodsTheme import me.kavishdevar.librepods.presentation.theme.LibrePodsTheme
import me.kavishdevar.librepods.presentation.theme.NightTheme import me.kavishdevar.librepods.presentation.theme.NightTheme
import me.kavishdevar.librepods.repository.AppDataRepository
import me.kavishdevar.librepods.services.LibrePodsService import me.kavishdevar.librepods.services.LibrePodsService
import me.kavishdevar.librepods.utils.XposedState import me.kavishdevar.librepods.utils.XposedState
import kotlin.io.encoding.ExperimentalEncodingApi import kotlin.io.encoding.ExperimentalEncodingApi
import kotlin.time.Duration.Companion.hours
private lateinit var serviceConnection: ServiceConnection private lateinit var serviceConnection: ServiceConnection
private lateinit var connectionStatusReceiver: BroadcastReceiver private lateinit var connectionStatusReceiver: BroadcastReceiver
@@ -128,13 +128,13 @@ class MainActivity : ComponentActivity() {
} }
override fun onDestroy() { override fun onDestroy() {
try { if (::serviceConnection.isInitialized) try {
unbindService(serviceConnection) unbindService(serviceConnection)
Log.d("MainActivity", "Unbound service") Log.d("MainActivity", "Unbound service")
} catch (e: Exception) { } catch (e: Exception) {
Log.e("MainActivity", "Error while unbinding service: $e") Log.e("MainActivity", "Error while unbinding service: $e")
} }
try { if (::connectionStatusReceiver.isInitialized) try {
unregisterReceiver(connectionStatusReceiver) unregisterReceiver(connectionStatusReceiver)
Log.d("MainActivity", "Unregistered receiver") Log.d("MainActivity", "Unregistered receiver")
} catch (e: Exception) { } catch (e: Exception) {
@@ -142,41 +142,24 @@ class MainActivity : ComponentActivity() {
} }
super.onDestroy() super.onDestroy()
} }
override fun onStop() {
try {
unbindService(serviceConnection)
Log.d("MainActivity", "Unbound service")
} catch (e: Exception) {
Log.e("MainActivity", "Error while unbinding service: $e")
}
try {
unregisterReceiver(connectionStatusReceiver)
Log.d("MainActivity", "Unregistered receiver")
} catch (e: Exception) {
Log.e("MainActivity", "Error while unregistering receiver: $e")
}
super.onStop()
}
} }
@Composable @Composable
fun Main() { fun Main() {
val context = LocalContext.current val context = LocalContext.current
val sharedPreferences = context.getSharedPreferences("settings", MODE_PRIVATE)
val librepodsService = remember { mutableStateOf<LibrePodsService?>(null) } val librepodsService = remember { mutableStateOf<LibrePodsService?>(null) }
val appDataRepository: AppDataRepository = (LocalContext.current.applicationContext as LibrePodsApplication).appDataRepository
val appState by appDataRepository.state.collectAsState()
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
if (BuildConfig.PLAY_BUILD) { if (BuildConfig.PLAY_BUILD) {
val now = System.currentTimeMillis() val now = System.currentTimeMillis()
val firstConn = val firstConn = appState.firstSuccessfulConnectionTime?: 0L
sharedPreferences.getLong("first_connection_successful_time", 0L)
val alreadyPrompted = val alreadyPrompted = appState.reviewPrompted
sharedPreferences.getBoolean("review_prompted", false)
val oneDay = 24 * 60 * 60 * 1000L val oneDay = 24.hours.inWholeMilliseconds
if ( if (
firstConn != 0L && firstConn != 0L &&
@@ -185,17 +168,19 @@ fun Main() {
) { ) {
triggerReviewFlow(context as? Activity ?: return@LaunchedEffect) triggerReviewFlow(context as? Activity ?: return@LaunchedEffect)
sharedPreferences.edit { appDataRepository.updateState {
putBoolean("review_prompted", true) it.copy(reviewPrompted = true)
} }
} }
} }
} }
val onboardingComplete = sharedPreferences.getBoolean("onboarding_complete", false) val onboardingComplete = appState.hasCompletedOnboarding
val releaseNotesShownPrefKey = "release_notes_shown_${BuildConfig.VERSION_NAME.removeSuffix("-debug").removeSuffix("-play")}" val currentVersion = BuildConfig.VERSION_NAME.removeSuffix("-debug").removeSuffix("-play")
val releaseNotesShown = sharedPreferences.getBoolean(releaseNotesShownPrefKey, false) val lastVersionShown = appState.lastVersionShown
val releaseNotesShown = lastVersionShown == currentVersion
val devicesState = remember(librepodsService.value) { val devicesState = remember(librepodsService.value) {
librepodsService.value?.devices ?: MutableStateFlow(emptyMap()) librepodsService.value?.devices ?: MutableStateFlow(emptyMap())
@@ -241,12 +226,6 @@ fun Main() {
val binder = service as LibrePodsService.LocalBinder val binder = service as LibrePodsService.LocalBinder
val service = binder.getService() val service = binder.getService()
librepodsService.value = service librepodsService.value = service
if (!sharedPreferences.contains("first_connection_successful_time")) {
sharedPreferences.edit {
putLong("first_connection_successful_time", System.currentTimeMillis())
}
}
} }
override fun onServiceDisconnected(name: ComponentName?) { override fun onServiceDisconnected(name: ComponentName?) {
@@ -263,10 +242,16 @@ fun Main() {
NavigationRoot( NavigationRoot(
showReleaseNotes = !releaseNotesShown, showReleaseNotes = !releaseNotesShown,
updatesShown = { sharedPreferences.edit { putBoolean(releaseNotesShownPrefKey, true) } }, updatesShown = {
appDataRepository.updateState {
it.copy(lastVersionShown = currentVersion)
}
},
showOnboarding = !onboardingComplete, showOnboarding = !onboardingComplete,
onboardingComplete = { onboardingComplete = {
sharedPreferences.edit { putBoolean("onboarding_complete", true) } appDataRepository.updateState {
it.copy(hasCompletedOnboarding = true)
}
bindService() bindService()
}, },
devicesState = devicesState devicesState = devicesState
@@ -37,10 +37,10 @@ import me.kavishdevar.librepods.R
import me.kavishdevar.librepods.bluetooth.MacAddress import me.kavishdevar.librepods.bluetooth.MacAddress
import me.kavishdevar.librepods.database.widget.WidgetConfigEntity import me.kavishdevar.librepods.database.widget.WidgetConfigEntity
import me.kavishdevar.librepods.devices.Device import me.kavishdevar.librepods.devices.Device
import me.kavishdevar.librepods.presentation.components.StyledButton import me.kavishdevar.librepods.presentation.components.primitives.StyledButton
import me.kavishdevar.librepods.presentation.components.StyledList import me.kavishdevar.librepods.presentation.components.primitives.StyledList
import me.kavishdevar.librepods.presentation.components.StyledListItem import me.kavishdevar.librepods.presentation.components.primitives.StyledListItem
import me.kavishdevar.librepods.presentation.components.StyledScaffold import me.kavishdevar.librepods.presentation.components.primitives.StyledScaffold
import me.kavishdevar.librepods.presentation.icons.LocalIcons import me.kavishdevar.librepods.presentation.icons.LocalIcons
import me.kavishdevar.librepods.presentation.theme.LibrePodsTheme import me.kavishdevar.librepods.presentation.theme.LibrePodsTheme
import me.kavishdevar.librepods.presentation.theme.NightTheme import me.kavishdevar.librepods.presentation.theme.NightTheme
@@ -25,7 +25,7 @@ import androidx.compose.ui.unit.dp
import androidx.core.view.WindowCompat import androidx.core.view.WindowCompat
import me.kavishdevar.librepods.LibrePodsApplication import me.kavishdevar.librepods.LibrePodsApplication
import me.kavishdevar.librepods.R import me.kavishdevar.librepods.R
import me.kavishdevar.librepods.presentation.components.StyledScaffold import me.kavishdevar.librepods.presentation.components.primitives.StyledScaffold
import me.kavishdevar.librepods.presentation.screens.onboarding.PrivacyPolicyPage import me.kavishdevar.librepods.presentation.screens.onboarding.PrivacyPolicyPage
import me.kavishdevar.librepods.presentation.theme.LibrePodsTheme import me.kavishdevar.librepods.presentation.theme.LibrePodsTheme
import me.kavishdevar.librepods.presentation.theme.NightTheme import me.kavishdevar.librepods.presentation.theme.NightTheme
@@ -1,49 +0,0 @@
/*
LibrePods - AirPods liberated from Apples ecosystem
Copyright (C) 2025 LibrePods contributors
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
@file:OptIn(ExperimentalEncodingApi::class)
package me.kavishdevar.librepods.presentation.components
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import me.kavishdevar.librepods.R
import kotlin.io.encoding.ExperimentalEncodingApi
@Composable
fun ConnectionSettings(
automaticEarDetectionEnabled: Boolean,
onAutomaticEarDetectionChanged: (Boolean) -> Unit,
automaticConnectionEnabled: Boolean,
onAutomaticConnectionChanged: (Boolean) -> Unit,
) {
StyledList {
StyledToggle(
label = stringResource(R.string.ear_detection),
checked = automaticEarDetectionEnabled,
onCheckedChange = onAutomaticEarDetectionChanged
)
StyledToggle(
label = stringResource(R.string.automatically_connect),
description = stringResource(R.string.automatically_connect_description),
checked = automaticConnectionEnabled,
onCheckedChange = onAutomaticConnectionChanged
)
}
}
@@ -1,106 +0,0 @@
/*
LibrePods - AirPods liberated from Apples ecosystem
Copyright (C) 2025 LibrePods contributors
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
@file:Suppress("unused")
package me.kavishdevar.librepods.presentation.components
import androidx.compose.animation.animateColorAsState
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
private val SelectedColorBlue = Color(0xFF0A84FF)
private val UnselectedColor = Color(0x593C3C3E)
private val TextColor = Color.White
private val IconTint = Color.White
@Composable
fun ControlCenterButton(
label: String,
icon: Painter,
onClick: () -> Unit,
modifier: Modifier = Modifier,
iconAreaSize: Dp,
isSelected: Boolean,
backgroundBrush: Brush? = null
) {
val targetBackgroundColor = if (isSelected) SelectedColorBlue else UnselectedColor
val backgroundColor by animateColorAsState(
targetValue = targetBackgroundColor,
label = "ButtonBackground"
)
Column(
modifier = modifier,
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Box(
modifier = Modifier
.size(iconAreaSize)
.clip(CircleShape)
.background(backgroundBrush ?: Brush.linearGradient(colors=listOf(backgroundColor, backgroundColor)))
.clickable(
onClick = onClick,
indication = null,
interactionSource = remember { MutableInteractionSource() }
),
contentAlignment = Alignment.Center
) {
Icon(
painter = icon,
contentDescription = null,
tint = IconTint,
modifier = Modifier.size(32.dp)
)
}
Spacer(modifier = Modifier.height(8.dp))
Text(
text = label,
color = TextColor,
fontSize = 12.sp,
fontWeight = FontWeight.Medium,
textAlign = TextAlign.Center,
maxLines = 2
)
}
}
@@ -1,242 +0,0 @@
/*
LibrePods - AirPods liberated from Apples ecosystem
Copyright (C) 2025 LibrePods contributors
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package me.kavishdevar.librepods.presentation.components
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.animateDpAsState
import androidx.compose.animation.core.spring
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
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.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import me.kavishdevar.librepods.R
import me.kavishdevar.librepods.devices.NoiseControlMode
private val ContainerColor = Color(0x593C3C3E)
private val SelectedIndicatorColorGray = Color(0xFF6C6C6E)
private val SelectedIndicatorColorBlue = Color(0xFF0A84FF)
private val TextColor = Color.White
private val IconTintUnselected = Color.White
private val IconTintSelected = Color.White
internal val AdaptiveRainbowBrush = Brush.sweepGradient(
colors = listOf(
Color(0xFFB03A2F), Color(0xFFB07A2F), Color(0xFFB0A22F), Color(0xFF6AB02F),
Color(0xFF2FAAB0), Color(0xFF2F5EB0), Color(0xFF7D2FB0), Color(0xFFB02F7D),
Color(0xFFB03A2F)
)
)
internal val IconAreaSize = 72.dp
private val IconSize = 42.dp
private val IconRowHeight = IconAreaSize + 12.dp
private val TextRowHeight = 24.dp
private val TextSize = 12.sp
@Composable
fun ControlCenterNoiseControlSegmentedButton(
modifier: Modifier = Modifier,
availableModes: List<NoiseControlMode>,
selectedMode: NoiseControlMode,
onModeSelected: (NoiseControlMode) -> Unit
) {
val selectedIndex = availableModes.indexOf(selectedMode).coerceAtLeast(0)
val density = LocalDensity.current
var iconRowWidthPx by remember { mutableFloatStateOf(0f) }
val itemCount = availableModes.size
val itemSlotWidthPx = remember(iconRowWidthPx, itemCount) {
if (itemCount > 0 && iconRowWidthPx > 0) {
iconRowWidthPx / itemCount
} else {
0f
}
}
val itemSlotWidthDp = remember(itemSlotWidthPx) { with(density) { itemSlotWidthPx.toDp() } }
val iconAreaSizePx = remember { with(density) { IconAreaSize.toPx() } }
val targetIndicatorStartPx = remember(selectedIndex, itemSlotWidthPx, iconAreaSizePx) {
if (itemSlotWidthPx > 0) {
val slotCenterPx = (selectedIndex + 0.5f) * itemSlotWidthPx
slotCenterPx - (iconAreaSizePx / 2f)
} else {
0f
}
}
val indicatorOffset: Dp by animateDpAsState(
targetValue = with(density) { targetIndicatorStartPx.toDp() },
animationSpec = spring(
dampingRatio = Spring.DampingRatioLowBouncy,
stiffness = Spring.StiffnessMedium
),
label = "IndicatorOffset"
)
val indicatorBackground = remember(selectedMode) {
when (selectedMode) {
NoiseControlMode.ADAPTIVE -> AdaptiveRainbowBrush
NoiseControlMode.OFF -> Brush.linearGradient(colors=listOf(SelectedIndicatorColorGray, SelectedIndicatorColorGray))
NoiseControlMode.TRANSPARENCY,
NoiseControlMode.NOISE_CANCELLATION -> Brush.linearGradient(colors=listOf(SelectedIndicatorColorBlue, SelectedIndicatorColorBlue))
}
}
Column(
modifier = modifier,
horizontalAlignment = Alignment.CenterHorizontally
) {
Box(
modifier = Modifier
.fillMaxWidth()
.height(IconRowHeight)
.clip(CircleShape)
.background(ContainerColor)
.onSizeChanged { iconRowWidthPx = it.width.toFloat() },
contentAlignment = Alignment.Center
) {
Box(
Modifier
.align(Alignment.CenterStart)
.offset(x = indicatorOffset)
.size(IconAreaSize)
.clip(CircleShape)
.background(indicatorBackground)
)
Row(
modifier = Modifier.fillMaxWidth().align(Alignment.Center),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceAround
) {
availableModes.forEach { mode ->
val isSelected = selectedMode == mode
NoiseControlIconItem(
modifier = Modifier.size(IconAreaSize),
mode = mode,
isSelected = isSelected,
onClick = { onModeSelected(mode) }
)
}
}
}
Spacer(modifier = Modifier.height(4.dp))
Row(
modifier = Modifier
.fillMaxWidth()
.height(TextRowHeight),
horizontalArrangement = Arrangement.SpaceAround,
verticalAlignment = Alignment.CenterVertically
) {
availableModes.forEach { mode ->
val isSelected = selectedMode == mode
Text(
text = getModeLabel(mode),
color = TextColor,
fontSize = TextSize,
fontWeight = if (isSelected) FontWeight.SemiBold else FontWeight.Normal,
textAlign = TextAlign.Center,
modifier = Modifier.width(itemSlotWidthDp.coerceAtLeast(1.dp))
)
}
}
}
}
@Composable
private fun NoiseControlIconItem(
modifier: Modifier = Modifier,
mode: NoiseControlMode,
isSelected: Boolean,
onClick: () -> Unit
) {
val iconRes = remember(mode) { getModeIconRes(mode) }
val tint = IconTintUnselected
Box(
modifier = modifier
.clip(CircleShape)
.clickable(
onClick = onClick,
indication = null,
interactionSource = remember { MutableInteractionSource() }
),
contentAlignment = Alignment.Center
) {
Icon(
painter = painterResource(id = iconRes),
contentDescription = getModeLabel(mode),
tint = if (isSelected && mode == NoiseControlMode.ADAPTIVE) IconTintSelected else tint,
modifier = Modifier.size(IconSize)
)
}
}
private fun getModeIconRes(mode: NoiseControlMode): Int {
return when (mode) {
NoiseControlMode.OFF -> R.drawable.ic_noise_cancellation
NoiseControlMode.TRANSPARENCY -> R.drawable.ic_transparency
NoiseControlMode.ADAPTIVE -> R.drawable.ic_adaptive
NoiseControlMode.NOISE_CANCELLATION -> R.drawable.ic_noise_cancellation
}
}
private fun getModeLabel(mode: NoiseControlMode): String {
return when (mode) {
NoiseControlMode.OFF -> "Off"
NoiseControlMode.TRANSPARENCY -> "Transparency"
NoiseControlMode.ADAPTIVE -> "Adaptive"
NoiseControlMode.NOISE_CANCELLATION -> "Noise Cancellation"
}
}
@@ -1,56 +0,0 @@
package me.kavishdevar.librepods.presentation.components
import android.os.Build
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import me.kavishdevar.librepods.R
import me.kavishdevar.librepods.utils.XposedState
@Composable
fun DeviceInfoCard() {
StyledList(title = stringResource(R.string.device_info)) {
StyledListItem(
contentText = stringResource(R.string.manufacturer),
supportingText = Build.MANUFACTURER,
enabled = false
)
StyledListItem(
contentText = stringResource(R.string.model_number),
supportingText = Build.MODEL,
enabled = false
)
StyledListItem(
contentText = stringResource(R.string.build_id),
supportingText = Build.DISPLAY,
enabled = false
)
StyledListItem(
contentText = stringResource(R.string.version),
supportingText = "${Build.ID} (${Build.VERSION.SDK_INT_FULL})",
enabled = false
)
StyledListItem(
contentText = stringResource(R.string.xposed_available),
supportingText = if (XposedState.isAvailable) {
stringResource(R.string.yes)
} else {
stringResource(R.string.no)
},
enabled = false
)
StyledListItem(
contentText = stringResource(R.string.app_enabled_in_xposed),
supportingText = if (XposedState.bluetoothScopeEnabled) {
stringResource(R.string.yes)
} else {
stringResource(R.string.no)
},
enabled = false
)
}
}
@@ -1,190 +0,0 @@
/*
LibrePods - AirPods liberated from Apples ecosystem
Copyright (C) 2025 LibrePods contributors
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package me.kavishdevar.librepods.presentation.components
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.spring
import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.gestures.draggable
import androidx.compose.foundation.gestures.rememberDraggableState
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
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.graphics.Color
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import kotlin.math.abs
import kotlin.math.max
import kotlin.math.min
import kotlin.math.roundToInt
import kotlin.math.sign
@Composable
fun VerticalVolumeSlider(
displayFraction: Float,
maxVolume: Int,
onVolumeChange: (Int) -> Unit,
initialFraction: Float,
onDragStateChange: (Boolean) -> Unit,
modifier: Modifier = Modifier,
baseSliderHeight: Dp = 400.dp,
baseSliderWidth: Dp = 145.dp,
baseCornerRadius: Dp = 45.dp,
maxStretchFactor: Float = 1.15f,
minCompressionFactor: Float = 0.875f,
stretchSensitivity: Float = 1.0f,
compressionSensitivity: Float = 1.0f,
cornerRadiusChangeFactor: Float = 0.2f,
directionalStretchRatio: Float = 0.75f
) {
val trackColor = Color(0x593C3C3E)
val progressColor = Color.White
var dragFraction by remember { mutableFloatStateOf(initialFraction) }
var isDragging by remember { mutableStateOf(false) }
var rawDragPosition by remember { mutableFloatStateOf(initialFraction) }
var overscrollAmount by remember { mutableFloatStateOf(0f) }
val baseHeightPx = with(LocalDensity.current) { baseSliderHeight.toPx() }
val animatedProgress by animateFloatAsState(
targetValue = dragFraction.coerceIn(0f, 1f),
animationSpec = spring(
dampingRatio = Spring.DampingRatioLowBouncy,
stiffness = Spring.StiffnessMedium
),
label = "ProgressAnimation"
)
val animatedOverscroll by animateFloatAsState(
targetValue = overscrollAmount,
animationSpec = spring(
dampingRatio = Spring.DampingRatioMediumBouncy,
stiffness = Spring.StiffnessMediumLow
),
label = "OverscrollAnimation"
)
val maxOverscrollEffect = (maxStretchFactor - 1f).coerceAtLeast(0f)
val stretchMultiplier = stretchSensitivity
val compressionMultiplier = compressionSensitivity
val overscrollDirection = sign(animatedOverscroll)
val totalStretchAmount = (min(maxOverscrollEffect, abs(animatedOverscroll) * stretchMultiplier) * baseSliderHeight.value).dp
val offsetY = if (abs(animatedOverscroll) > 0.001f) {
val asymmetricOffset = totalStretchAmount * (directionalStretchRatio - 0.5f)
(-overscrollDirection * asymmetricOffset.value).dp
} else {
0.dp
}
val heightStretch = baseSliderHeight + totalStretchAmount
val widthCompression = baseSliderWidth * max(
minCompressionFactor,
1f - min(1f - minCompressionFactor, abs(animatedOverscroll) * compressionMultiplier)
)
val dynamicCornerRadius = baseCornerRadius * (1f - min(cornerRadiusChangeFactor, abs(animatedOverscroll) * cornerRadiusChangeFactor * 2f))
Box(
modifier = modifier,
contentAlignment = Alignment.Center
) {
Box(
modifier = Modifier
.height(heightStretch)
.width(widthCompression)
.offset(y = offsetY)
.clip(RoundedCornerShape(dynamicCornerRadius))
.background(trackColor)
.pointerInput(Unit) {
detectTapGestures { offset ->
val newFraction = 1f - (offset.y / size.height).coerceIn(0f, 1f)
dragFraction = newFraction
rawDragPosition = newFraction
overscrollAmount = 0f
val newVolume = (newFraction * maxVolume).roundToInt()
onVolumeChange(newVolume)
}
}
.draggable(
orientation = Orientation.Vertical,
state = rememberDraggableState { delta ->
rawDragPosition -= (delta / baseHeightPx)
dragFraction = rawDragPosition.coerceIn(0f, 1f)
overscrollAmount = when {
rawDragPosition > 1f -> min(1.0f, (rawDragPosition - 1f) * 2.0f)
rawDragPosition < 0f -> max(-1.0f, rawDragPosition * 2.0f)
else -> 0f
}
val newVolume = (dragFraction * maxVolume).roundToInt()
onVolumeChange(newVolume)
},
onDragStarted = {
isDragging = true
dragFraction = displayFraction
rawDragPosition = displayFraction
overscrollAmount = 0f
onDragStateChange(true)
},
onDragStopped = {
isDragging = false
overscrollAmount = 0f
rawDragPosition = dragFraction
onDragStateChange(false)
}
),
contentAlignment = Alignment.BottomCenter
) {
Box(
modifier = Modifier
.fillMaxWidth()
.fillMaxHeight(animatedProgress)
.background(progressColor)
)
}
}
}
@@ -18,7 +18,7 @@
@file:OptIn(ExperimentalEncodingApi::class) @file:OptIn(ExperimentalEncodingApi::class)
package me.kavishdevar.librepods.presentation.components package me.kavishdevar.librepods.presentation.components.apple
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
@@ -28,6 +28,8 @@ import androidx.compose.runtime.remember
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
import me.kavishdevar.librepods.R import me.kavishdevar.librepods.R
import me.kavishdevar.librepods.presentation.components.primitives.StyledList
import me.kavishdevar.librepods.presentation.components.primitives.StyledListItem
import me.kavishdevar.librepods.presentation.icons.richText import me.kavishdevar.librepods.presentation.icons.richText
import me.kavishdevar.librepods.presentation.theme.DesignSystem import me.kavishdevar.librepods.presentation.theme.DesignSystem
import me.kavishdevar.librepods.presentation.theme.LibrePodsTheme import me.kavishdevar.librepods.presentation.theme.LibrePodsTheme
@@ -50,7 +52,7 @@ fun AboutCard(
val serialNumber = remember { mutableIntStateOf(0) } val serialNumber = remember { mutableIntStateOf(0) }
StyledList (title = stringResource(R.string.about)) { StyledList(title = stringResource(R.string.about)) {
StyledListItem( StyledListItem(
contentText = stringResource(R.string.model_name), contentText = stringResource(R.string.model_name),
supportingText = modelName supportingText = modelName
@@ -61,7 +63,7 @@ fun AboutCard(
supportingText = actualModel supportingText = actualModel
) )
StyledListItem ( StyledListItem(
contentText = stringResource(R.string.serial_number), contentText = stringResource(R.string.serial_number),
supportingContent = { supportingContent = {
Text( Text(
@@ -18,11 +18,14 @@
@file:OptIn(ExperimentalEncodingApi::class) @file:OptIn(ExperimentalEncodingApi::class)
package me.kavishdevar.librepods.presentation.components package me.kavishdevar.librepods.presentation.components.apple
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import me.kavishdevar.librepods.R import me.kavishdevar.librepods.R
import me.kavishdevar.librepods.presentation.components.primitives.StyledList
import me.kavishdevar.librepods.presentation.components.primitives.StyledListItem
import me.kavishdevar.librepods.presentation.components.primitives.StyledToggle
import kotlin.io.encoding.ExperimentalEncodingApi import kotlin.io.encoding.ExperimentalEncodingApi
@Composable @Composable
@@ -30,7 +33,6 @@ fun AudioSettings(
adaptiveVolumeCapability: Boolean, adaptiveVolumeCapability: Boolean,
conversationalAwarenessCapability: Boolean, conversationalAwarenessCapability: Boolean,
loudSoundReductionCapability: Boolean, loudSoundReductionCapability: Boolean,
adaptiveAudioCapability: Boolean,
customEqCapability: Boolean, customEqCapability: Boolean,
adaptiveVolumeChecked: Boolean, adaptiveVolumeChecked: Boolean,
@@ -42,13 +44,12 @@ fun AudioSettings(
loudSoundReductionChecked: Boolean, loudSoundReductionChecked: Boolean,
onLoudSoundReductionCheckedChange: (Boolean) -> Unit, onLoudSoundReductionCheckedChange: (Boolean) -> Unit,
navigateToAdaptiveStrength: () -> Unit,
navigateToEqualizer: () -> Unit, navigateToEqualizer: () -> Unit,
vendorIdHook: Boolean, vendorIdHook: Boolean,
isPremium: Boolean isPremium: Boolean
) { ) {
if (adaptiveVolumeCapability || conversationalAwarenessCapability || loudSoundReductionCapability || adaptiveAudioCapability) { if (adaptiveVolumeCapability || conversationalAwarenessCapability || loudSoundReductionCapability) {
StyledList(title = stringResource(R.string.audio)) { StyledList(title = stringResource(R.string.audio)) {
if (adaptiveVolumeCapability) { if (adaptiveVolumeCapability) {
StyledToggle( StyledToggle(
@@ -80,13 +81,6 @@ fun AudioSettings(
) )
} }
if (adaptiveAudioCapability) {
StyledListItem(
contentText = stringResource(R.string.adaptive_audio),
onClick = navigateToAdaptiveStrength,
)
}
if (customEqCapability) { if (customEqCapability) {
StyledListItem( StyledListItem(
contentText = stringResource(R.string.equalizer), contentText = stringResource(R.string.equalizer),
@@ -16,7 +16,7 @@
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package me.kavishdevar.librepods.presentation.components package me.kavishdevar.librepods.presentation.components.apple
import android.content.res.Configuration import android.content.res.Configuration
@@ -18,7 +18,7 @@
@file:OptIn(ExperimentalEncodingApi::class) @file:OptIn(ExperimentalEncodingApi::class)
package me.kavishdevar.librepods.presentation.components package me.kavishdevar.librepods.presentation.components.apple
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
import androidx.compose.foundation.background import androidx.compose.foundation.background
@@ -18,7 +18,7 @@
@file:OptIn(ExperimentalEncodingApi::class) @file:OptIn(ExperimentalEncodingApi::class)
package me.kavishdevar.librepods.presentation.components package me.kavishdevar.librepods.presentation.components.apple
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
@@ -28,6 +28,8 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
import me.kavishdevar.librepods.R import me.kavishdevar.librepods.R
import me.kavishdevar.librepods.presentation.components.primitives.StyledList
import me.kavishdevar.librepods.presentation.components.primitives.StyledListItem
import kotlin.io.encoding.ExperimentalEncodingApi import kotlin.io.encoding.ExperimentalEncodingApi
@ExperimentalHazeMaterialsApi @ExperimentalHazeMaterialsApi
@@ -47,20 +49,20 @@ fun CallControlSettings(
StyledList(title = stringResource(R.string.call_controls)) { StyledList(title = stringResource(R.string.call_controls)) {
StyledListItem( StyledListItem(
contentText = stringResource(R.string.answer_call), contentText = stringResource(R.string.answer_call),
supportingText = stringResource(R.string.press_once), supportingText = stringResource(R.string.press_once),
enabled = false enabled = false
) )
StyledListItem( StyledListItem(
contentText = muteUnmuteText, contentText = muteUnmuteText,
supportingText = singlePressAction, supportingText = singlePressAction,
onClick = { navigateToCallControlScreen(muteUnmuteText) } , onClick = { navigateToCallControlScreen(muteUnmuteText) },
) )
StyledListItem( StyledListItem(
contentText = hangUpText, contentText = hangUpText,
supportingText = doublePressAction, supportingText = doublePressAction,
onClick = { navigateToCallControlScreen(hangUpText) } onClick = { navigateToCallControlScreen(hangUpText) }
) )
@@ -0,0 +1,125 @@
/*
LibrePods - AirPods liberated from Apples ecosystem
Copyright (C) 2025 LibrePods contributors
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
@file:OptIn(ExperimentalEncodingApi::class)
package me.kavishdevar.librepods.presentation.components.apple
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import me.kavishdevar.librepods.R
import me.kavishdevar.librepods.presentation.components.primitives.StyledList
import me.kavishdevar.librepods.presentation.components.primitives.StyledToggle
import kotlin.io.encoding.ExperimentalEncodingApi
@Composable
fun ConnectionSettings(
automaticEarDetectionEnabled: Boolean,
onAutomaticEarDetectionChanged: (Boolean) -> Unit,
automaticConnectionEnabled: Boolean,
onAutomaticConnectionChanged: (Boolean) -> Unit,
disconnectWhenNotWearing: Boolean,
onDisconnectWhenNotWearingChanged: (Boolean) -> Unit,
takeoverWhenDisconnected: Boolean,
onTakeoverWhenDisconnectedChanged: (Boolean) -> Unit,
takeoverWhenIdle: Boolean,
onTakeoverWhenIdleChanged: (Boolean) -> Unit,
takeoverWhenMusic: Boolean,
onTakeoverWhenMusicChanged: (Boolean) -> Unit,
takeoverWhenCall: Boolean,
onTakeoverWhenCallChanged: (Boolean) -> Unit,
takeoverWhenRingingCall: Boolean,
onTakeoverWhenRingingCallChanged: (Boolean) -> Unit,
takeoverWhenMediaStart: Boolean,
onTakeoverWhenMediaStartChanged: (Boolean) -> Unit,
isPremium: Boolean
) {
StyledList {
StyledToggle(
label = stringResource(R.string.ear_detection),
checked = automaticEarDetectionEnabled,
onCheckedChange = onAutomaticEarDetectionChanged
)
StyledToggle(
label = stringResource(R.string.automatically_connect),
description = stringResource(R.string.automatically_connect_description),
checked = automaticConnectionEnabled,
onCheckedChange = onAutomaticConnectionChanged
)
StyledToggle(
label = stringResource(R.string.disconnect_when_not_wearing),
description = stringResource(R.string.disconnect_when_not_wearing_description),
checked = disconnectWhenNotWearing,
onCheckedChange = onDisconnectWhenNotWearingChanged
)
}
// StyledList(title = stringResource(R.string.takeover_airpods_state)) {
// StyledToggle(
// label = stringResource(R.string.takeover_disconnected),
// description = stringResource(R.string.takeover_disconnected_desc),
// checked = takeoverWhenDisconnected,
// onCheckedChange = onTakeoverWhenDisconnectedChanged,
// enabled = isPremium
// )
// StyledToggle(
// label = stringResource(R.string.takeover_idle),
// description = stringResource(R.string.takeover_idle_desc),
// checked = takeoverWhenIdle,
// onCheckedChange = onTakeoverWhenIdleChanged,
// enabled = isPremium
// )
// StyledToggle(
// label = stringResource(R.string.takeover_music),
// description = stringResource(R.string.takeover_music_desc),
// checked = takeoverWhenMusic,
// onCheckedChange = onTakeoverWhenMusicChanged,
// enabled = isPremium
// )
//
// StyledToggle(
// label = stringResource(R.string.takeover_call),
// description = stringResource(R.string.takeover_call_desc),
// checked = takeoverWhenCall,
// onCheckedChange = onTakeoverWhenCallChanged,
// enabled = isPremium
// )
// }
//
// StyledList(title = stringResource(R.string.takeover_phone_state)) {
// StyledToggle(
// label = stringResource(R.string.takeover_ringing_call),
// description = stringResource(R.string.takeover_ringing_call_desc),
// checked = takeoverWhenRingingCall,
// onCheckedChange = onTakeoverWhenRingingCallChanged,
// enabled = isPremium
// )
// StyledToggle(
// label = stringResource(R.string.takeover_media_start),
// description = stringResource(R.string.takeover_media_start_desc),
// checked = takeoverWhenMediaStart,
// onCheckedChange = onTakeoverWhenMediaStartChanged,
// enabled = isPremium
// )
// }
}
@@ -18,11 +18,13 @@
@file:OptIn(ExperimentalEncodingApi::class) @file:OptIn(ExperimentalEncodingApi::class)
package me.kavishdevar.librepods.presentation.components package me.kavishdevar.librepods.presentation.components.apple
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import me.kavishdevar.librepods.R import me.kavishdevar.librepods.R
import me.kavishdevar.librepods.presentation.components.primitives.StyledList
import me.kavishdevar.librepods.presentation.components.primitives.StyledListItem
import kotlin.io.encoding.ExperimentalEncodingApi import kotlin.io.encoding.ExperimentalEncodingApi
@Composable @Composable
@@ -38,12 +40,12 @@ fun HearingHealthSettings(
if (hasPPECapability && shouldShowHearingAid) { if (hasPPECapability && shouldShowHearingAid) {
StyledList(title = stringResource(R.string.hearing_health)) { StyledList(title = stringResource(R.string.hearing_health)) {
StyledListItem( StyledListItem(
contentText = stringResource(R.string.hearing_protection), contentText = stringResource(R.string.hearing_protection),
onClick = navigateToHearingProtection onClick = navigateToHearingProtection
) )
StyledListItem( StyledListItem(
contentText = stringResource(R.string.hearing_aid), contentText = stringResource(R.string.hearing_aid),
onClick = navigateToHearingAid onClick = navigateToHearingAid
) )
} }
@@ -16,7 +16,7 @@
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package me.kavishdevar.librepods.presentation.components package me.kavishdevar.librepods.presentation.components.apple
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.MutableInteractionSource
@@ -18,7 +18,7 @@
@file:OptIn(ExperimentalEncodingApi::class) @file:OptIn(ExperimentalEncodingApi::class)
package me.kavishdevar.librepods.presentation.components package me.kavishdevar.librepods.presentation.components.apple
import android.annotation.SuppressLint import android.annotation.SuppressLint
import androidx.compose.animation.core.AnimationSpec import androidx.compose.animation.core.AnimationSpec
@@ -34,6 +34,7 @@ import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
@@ -174,6 +175,7 @@ fun NoiseControlSettings(
} }
if (showLabels) { if (showLabels) {
Spacer(modifier = Modifier.height(2.dp))
Text( Text(
text = stringResource(labelRes), text = stringResource(labelRes),
style = MaterialTheme.typography.labelSmall, style = MaterialTheme.typography.labelSmall,
@@ -181,6 +183,7 @@ fun NoiseControlSettings(
maxLines = 2, maxLines = 2,
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth()
) )
Spacer(modifier = Modifier.height(8.dp))
} }
} }
} }
@@ -16,12 +16,14 @@
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package me.kavishdevar.librepods.presentation.components package me.kavishdevar.librepods.presentation.components.apple
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import me.kavishdevar.librepods.R import me.kavishdevar.librepods.R
import me.kavishdevar.librepods.data.StemAction import me.kavishdevar.librepods.data.StemAction
import me.kavishdevar.librepods.presentation.components.primitives.StyledList
import me.kavishdevar.librepods.presentation.components.primitives.StyledListItem
@Composable @Composable
fun PressAndHoldSettings( fun PressAndHoldSettings(
@@ -46,12 +48,12 @@ fun PressAndHoldSettings(
title = stringResource(R.string.press_and_hold_airpods) title = stringResource(R.string.press_and_hold_airpods)
) { ) {
StyledListItem( StyledListItem(
contentText = stringResource(R.string.left), contentText = stringResource(R.string.left),
supportingText = leftActionText, supportingText = leftActionText,
onClick = navigateToLeftLongPress onClick = navigateToLeftLongPress
) )
StyledListItem( StyledListItem(
contentText = stringResource(R.string.right), contentText = stringResource(R.string.right),
supportingText = rightActionText, supportingText = rightActionText,
onClick = navigateToRightLongPress, onClick = navigateToRightLongPress,
) )
@@ -16,12 +16,14 @@
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package me.kavishdevar.librepods.presentation.components package me.kavishdevar.librepods.presentation.components.common
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import me.kavishdevar.librepods.BuildConfig import me.kavishdevar.librepods.BuildConfig
import me.kavishdevar.librepods.R import me.kavishdevar.librepods.R
import me.kavishdevar.librepods.presentation.components.primitives.StyledList
import me.kavishdevar.librepods.presentation.components.primitives.StyledListItem
@Composable @Composable
fun AppInfoCard( fun AppInfoCard(
@@ -29,24 +31,24 @@ fun AppInfoCard(
) { ) {
StyledList(title = stringResource(R.string.about)) { StyledList(title = stringResource(R.string.about)) {
StyledListItem( StyledListItem(
contentText = stringResource(R.string.version), contentText = stringResource(R.string.version),
supportingText = BuildConfig.VERSION_NAME, supportingText = BuildConfig.VERSION_NAME,
onClick = navigateToReleaseNotesScreen onClick = navigateToReleaseNotesScreen
) )
StyledListItem( StyledListItem(
contentText = stringResource(R.string.version_code), contentText = stringResource(R.string.version_code),
supportingText = BuildConfig.VERSION_CODE.toString(), supportingText = BuildConfig.VERSION_CODE.toString(),
) )
StyledListItem( StyledListItem(
contentText = stringResource(R.string.flavor), contentText = stringResource(R.string.flavor),
supportingText = BuildConfig.FLAVOR, supportingText = BuildConfig.FLAVOR,
) )
StyledListItem( StyledListItem(
contentText = stringResource(R.string.build_type), contentText = stringResource(R.string.build_type),
supportingText = BuildConfig.BUILD_TYPE, supportingText = BuildConfig.BUILD_TYPE,
) )
} }
} }
@@ -0,0 +1,58 @@
package me.kavishdevar.librepods.presentation.components.common
import android.os.Build
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import me.kavishdevar.librepods.R
import me.kavishdevar.librepods.presentation.components.primitives.StyledList
import me.kavishdevar.librepods.presentation.components.primitives.StyledListItem
import me.kavishdevar.librepods.utils.XposedState
@Composable
fun DeviceInfoCard() {
StyledList(title = stringResource(R.string.device_info)) {
StyledListItem(
contentText = stringResource(R.string.manufacturer),
supportingText = Build.MANUFACTURER,
enabled = false
)
StyledListItem(
contentText = stringResource(R.string.model_number),
supportingText = Build.MODEL,
enabled = false
)
StyledListItem(
contentText = stringResource(R.string.build_id),
supportingText = Build.DISPLAY,
enabled = false
)
StyledListItem(
contentText = stringResource(R.string.version),
supportingText = "${Build.ID} (${Build.VERSION.SDK_INT_FULL})",
enabled = false
)
StyledListItem(
contentText = stringResource(R.string.xposed_available),
supportingText = if (XposedState.isAvailable) {
stringResource(R.string.yes)
} else {
stringResource(R.string.no)
},
enabled = false
)
StyledListItem(
contentText = stringResource(R.string.app_enabled_in_xposed),
supportingText = if (XposedState.bluetoothScopeEnabled) {
stringResource(R.string.yes)
} else {
stringResource(R.string.no)
},
enabled = false
)
}
}
@@ -1,4 +1,4 @@
package me.kavishdevar.librepods.presentation.components package me.kavishdevar.librepods.presentation.components.primitives
import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
@@ -16,7 +16,7 @@
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package me.kavishdevar.librepods.presentation.components package me.kavishdevar.librepods.presentation.components.primitives
import android.graphics.RuntimeShader import android.graphics.RuntimeShader
import android.os.Build import android.os.Build
@@ -16,7 +16,7 @@
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package me.kavishdevar.librepods.presentation.components package me.kavishdevar.librepods.presentation.components.primitives
import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeIn
@@ -62,7 +62,7 @@ import me.kavishdevar.librepods.presentation.theme.LocalDesignSystem
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun ConfirmationDialog( fun StyledConfirmationDialog(
showDialog: MutableState<Boolean>, showDialog: MutableState<Boolean>,
title: String, title: String,
message: String, message: String,
@@ -16,7 +16,7 @@
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package me.kavishdevar.librepods.presentation.components package me.kavishdevar.librepods.presentation.components.primitives
import android.content.res.Configuration.UI_MODE_NIGHT_NO import android.content.res.Configuration.UI_MODE_NIGHT_NO
import android.content.res.Configuration.UI_MODE_NIGHT_YES import android.content.res.Configuration.UI_MODE_NIGHT_YES
@@ -1,4 +1,4 @@
package me.kavishdevar.librepods.presentation.components package me.kavishdevar.librepods.presentation.components.primitives
import androidx.compose.animation.core.animateDp import androidx.compose.animation.core.animateDp
import androidx.compose.animation.core.animateDpAsState import androidx.compose.animation.core.animateDpAsState
@@ -1,4 +1,4 @@
package me.kavishdevar.librepods.presentation.components package me.kavishdevar.librepods.presentation.components.primitives
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
@@ -16,7 +16,7 @@
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package me.kavishdevar.librepods.presentation.components package me.kavishdevar.librepods.presentation.components.primitives
import androidx.compose.animation.animateColorAsState import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.animateFloatAsState
@@ -16,7 +16,7 @@
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package me.kavishdevar.librepods.presentation.components package me.kavishdevar.librepods.presentation.components.primitives
import android.graphics.RenderEffect import android.graphics.RenderEffect
import android.graphics.Shader import android.graphics.Shader
@@ -16,7 +16,7 @@
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package me.kavishdevar.librepods.presentation.components package me.kavishdevar.librepods.presentation.components.primitives
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.res.Configuration import android.content.res.Configuration
@@ -16,7 +16,7 @@
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package me.kavishdevar.librepods.presentation.components package me.kavishdevar.librepods.presentation.components.primitives
import android.content.res.Configuration import android.content.res.Configuration
import androidx.compose.animation.animateColorAsState import androidx.compose.animation.animateColorAsState
@@ -18,7 +18,7 @@
@file:OptIn(ExperimentalEncodingApi::class) @file:OptIn(ExperimentalEncodingApi::class)
package me.kavishdevar.librepods.presentation.components package me.kavishdevar.librepods.presentation.components.primitives
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
@@ -22,7 +22,6 @@ import me.kavishdevar.librepods.presentation.screens.OpenSourceLicensesScreen
import me.kavishdevar.librepods.presentation.screens.PurchaseScreen import me.kavishdevar.librepods.presentation.screens.PurchaseScreen
import me.kavishdevar.librepods.presentation.screens.ReleaseNotesScreen import me.kavishdevar.librepods.presentation.screens.ReleaseNotesScreen
import me.kavishdevar.librepods.presentation.screens.apple.AccessibilitySettingsScreen import me.kavishdevar.librepods.presentation.screens.apple.AccessibilitySettingsScreen
import me.kavishdevar.librepods.presentation.screens.apple.AdaptiveStrengthScreen
import me.kavishdevar.librepods.presentation.screens.apple.AppleSettingsRoute import me.kavishdevar.librepods.presentation.screens.apple.AppleSettingsRoute
import me.kavishdevar.librepods.presentation.screens.apple.CallControlScreen import me.kavishdevar.librepods.presentation.screens.apple.CallControlScreen
import me.kavishdevar.librepods.presentation.screens.apple.DebugRoute import me.kavishdevar.librepods.presentation.screens.apple.DebugRoute
@@ -77,7 +76,7 @@ fun RenderScreenContent(
updatesShown: () -> Unit, updatesShown: () -> Unit,
onboardingComplete: () -> Unit onboardingComplete: () -> Unit
) { ) {
val navigate: (Screen) -> Unit = { target -> backStack.add(target) } val navigate: (Screen) -> Unit = { target -> if (target !in backStack) backStack.add(target) } // prevents multiple clicks while transitioning
fun navigateToPurchase() = navigate(Screen.Purchase) fun navigateToPurchase() = navigate(Screen.Purchase)
val navigateBack: (() -> Unit)? = if (backStack.size > 1) { val navigateBack: (() -> Unit)? = if (backStack.size > 1) {
@@ -129,7 +128,6 @@ fun RenderScreenContent(
navigateToLeftLongPress = { navigate(Screen.LongPress(screen.macAddress, left)) }, navigateToLeftLongPress = { navigate(Screen.LongPress(screen.macAddress, left)) },
navigateToRightLongPress = { navigate(Screen.LongPress(screen.macAddress, right)) }, navigateToRightLongPress = { navigate(Screen.LongPress(screen.macAddress, right)) },
navigateToPurchase = ::navigateToPurchase, navigateToPurchase = ::navigateToPurchase,
navigateToAdaptiveStrength = { navigate(Screen.AdaptiveStrength(screen.macAddress)) },
navigateToEqualizer = { navigate(Screen.Equalizer(screen.macAddress)) }, navigateToEqualizer = { navigate(Screen.Equalizer(screen.macAddress)) },
navigateToHeadTracking = { navigate(Screen.HeadTracking(screen.macAddress)) }, navigateToHeadTracking = { navigate(Screen.HeadTracking(screen.macAddress)) },
navigateToAccessibility = { navigate(Screen.Accessibility(screen.macAddress)) }, navigateToAccessibility = { navigate(Screen.Accessibility(screen.macAddress)) },
@@ -245,20 +243,6 @@ fun RenderScreenContent(
) )
} }
is Screen.AdaptiveStrength -> {
val device = devices[screen.macAddress] as? AppleDevice ?: return
val factory = createAppleViewModelFactory(device, appDataRepository, recordingRepository, heartRateRepository)
val appleViewModel: AppleViewModel = viewModel(
key = "${screen.macAddress.value}:${device.connectionNumber}",
factory = factory
)
AdaptiveStrengthScreen(
viewModel = appleViewModel,
navigateBack = navigateBack,
navigateToPurchase = ::navigateToPurchase
)
}
Screen.OpenSourceLicenses -> OpenSourceLicensesScreen(navigateBack) Screen.OpenSourceLicenses -> OpenSourceLicensesScreen(navigateBack)
is Screen.UpdateHearingTest -> { is Screen.UpdateHearingTest -> {
@@ -55,11 +55,6 @@ sealed interface Screen: NavKey {
override val macAddress: MacAddress override val macAddress: MacAddress
): DeviceScreen ): DeviceScreen
@Serializable
data class AdaptiveStrength(
override val macAddress: MacAddress
): DeviceScreen
// @Serializable // @Serializable
// data object CameraControl: Screen // data object CameraControl: Screen
@@ -38,7 +38,6 @@ import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.input.TextFieldState import androidx.compose.foundation.text.input.TextFieldState
import androidx.compose.foundation.text.input.clearText import androidx.compose.foundation.text.input.clearText
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
@@ -63,17 +62,17 @@ import com.kyant.backdrop.backdrops.layerBackdrop
import com.kyant.backdrop.backdrops.rememberLayerBackdrop import com.kyant.backdrop.backdrops.rememberLayerBackdrop
import me.kavishdevar.librepods.BuildConfig import me.kavishdevar.librepods.BuildConfig
import me.kavishdevar.librepods.R import me.kavishdevar.librepods.R
import me.kavishdevar.librepods.presentation.components.AppInfoCard import me.kavishdevar.librepods.presentation.components.common.AppInfoCard
import me.kavishdevar.librepods.presentation.components.DeviceInfoCard import me.kavishdevar.librepods.presentation.components.common.DeviceInfoCard
import me.kavishdevar.librepods.presentation.components.StyledBottomSheet import me.kavishdevar.librepods.presentation.components.primitives.StyledBottomSheet
import me.kavishdevar.librepods.presentation.components.StyledButton import me.kavishdevar.librepods.presentation.components.primitives.StyledButton
import me.kavishdevar.librepods.presentation.components.StyledIconButton import me.kavishdevar.librepods.presentation.components.primitives.StyledIconButton
import me.kavishdevar.librepods.presentation.components.StyledInputField import me.kavishdevar.librepods.presentation.components.primitives.StyledInputField
import me.kavishdevar.librepods.presentation.components.StyledList import me.kavishdevar.librepods.presentation.components.primitives.StyledList
import me.kavishdevar.librepods.presentation.components.StyledListItem import me.kavishdevar.librepods.presentation.components.primitives.StyledListItem
import me.kavishdevar.librepods.presentation.components.StyledListItemOrientation import me.kavishdevar.librepods.presentation.components.primitives.StyledListItemOrientation
import me.kavishdevar.librepods.presentation.components.StyledScaffold import me.kavishdevar.librepods.presentation.components.primitives.StyledScaffold
import me.kavishdevar.librepods.presentation.components.StyledToggle import me.kavishdevar.librepods.presentation.components.primitives.StyledToggle
import me.kavishdevar.librepods.presentation.icons.LocalIcons import me.kavishdevar.librepods.presentation.icons.LocalIcons
import me.kavishdevar.librepods.presentation.theme.DesignSystem import me.kavishdevar.librepods.presentation.theme.DesignSystem
import me.kavishdevar.librepods.presentation.theme.NightTheme import me.kavishdevar.librepods.presentation.theme.NightTheme
@@ -81,7 +80,6 @@ import me.kavishdevar.librepods.presentation.viewmodel.AppSettingsViewModel
import me.kavishdevar.librepods.utils.XposedState import me.kavishdevar.librepods.utils.XposedState
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
@OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun AppSettingsScreen( fun AppSettingsScreen(
viewModel: AppSettingsViewModel = viewModel(), viewModel: AppSettingsViewModel = viewModel(),
@@ -20,11 +20,11 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.debounce
import me.kavishdevar.librepods.R import me.kavishdevar.librepods.R
import me.kavishdevar.librepods.presentation.components.StyledList import me.kavishdevar.librepods.presentation.components.primitives.StyledList
import me.kavishdevar.librepods.presentation.components.StyledListItem import me.kavishdevar.librepods.presentation.components.primitives.StyledListItem
import me.kavishdevar.librepods.presentation.components.StyledListItemOrientation import me.kavishdevar.librepods.presentation.components.primitives.StyledListItemOrientation
import me.kavishdevar.librepods.presentation.components.StyledScaffold import me.kavishdevar.librepods.presentation.components.primitives.StyledScaffold
import me.kavishdevar.librepods.presentation.components.StyledSlider import me.kavishdevar.librepods.presentation.components.primitives.StyledSlider
import me.kavishdevar.librepods.presentation.viewmodel.AppSettingsViewModel import me.kavishdevar.librepods.presentation.viewmodel.AppSettingsViewModel
import kotlin.time.Duration.Companion.seconds import kotlin.time.Duration.Companion.seconds
@@ -66,12 +66,12 @@ import me.kavishdevar.librepods.devices.AppleState
import me.kavishdevar.librepods.devices.BaseCapability import me.kavishdevar.librepods.devices.BaseCapability
import me.kavishdevar.librepods.devices.ConnectionState import me.kavishdevar.librepods.devices.ConnectionState
import me.kavishdevar.librepods.devices.Device import me.kavishdevar.librepods.devices.Device
import me.kavishdevar.librepods.presentation.components.NoiseControlSettings import me.kavishdevar.librepods.presentation.components.apple.NoiseControlSettings
import me.kavishdevar.librepods.presentation.components.StyledIconButton import me.kavishdevar.librepods.presentation.components.primitives.StyledIconButton
import me.kavishdevar.librepods.presentation.components.StyledList import me.kavishdevar.librepods.presentation.components.primitives.StyledList
import me.kavishdevar.librepods.presentation.components.StyledListItem import me.kavishdevar.librepods.presentation.components.primitives.StyledListItem
import me.kavishdevar.librepods.presentation.components.StyledListItemOrientation import me.kavishdevar.librepods.presentation.components.primitives.StyledListItemOrientation
import me.kavishdevar.librepods.presentation.components.StyledScaffold import me.kavishdevar.librepods.presentation.components.primitives.StyledScaffold
import me.kavishdevar.librepods.presentation.icons.LocalIcons import me.kavishdevar.librepods.presentation.icons.LocalIcons
import me.kavishdevar.librepods.presentation.theme.DesignSystem import me.kavishdevar.librepods.presentation.theme.DesignSystem
import me.kavishdevar.librepods.presentation.theme.LocalDesignSystem import me.kavishdevar.librepods.presentation.theme.LocalDesignSystem
@@ -30,13 +30,9 @@ import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.asPaddingValues
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.statusBars
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@@ -58,9 +54,7 @@ import com.mikepenz.aboutlibraries.ui.compose.variant.LibraryDetailMode
import com.mikepenz.aboutlibraries.ui.compose.variant.LibraryInlineDetail import com.mikepenz.aboutlibraries.ui.compose.variant.LibraryInlineDetail
import com.mikepenz.aboutlibraries.ui.compose.variant.LibraryRow import com.mikepenz.aboutlibraries.ui.compose.variant.LibraryRow
import me.kavishdevar.librepods.R import me.kavishdevar.librepods.R
import me.kavishdevar.librepods.presentation.components.StyledScaffold import me.kavishdevar.librepods.presentation.components.primitives.StyledScaffold
import me.kavishdevar.librepods.presentation.theme.DesignSystem
import me.kavishdevar.librepods.presentation.theme.LocalDesignSystem
@Composable @Composable
fun OpenSourceLicensesScreen( fun OpenSourceLicensesScreen(
@@ -21,13 +21,9 @@ package me.kavishdevar.librepods.presentation.screens
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.asPaddingValues
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.statusBars
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
@@ -44,14 +40,12 @@ import androidx.lifecycle.viewmodel.compose.viewModel
import com.kyant.backdrop.backdrops.layerBackdrop import com.kyant.backdrop.backdrops.layerBackdrop
import com.kyant.backdrop.backdrops.rememberLayerBackdrop import com.kyant.backdrop.backdrops.rememberLayerBackdrop
import me.kavishdevar.librepods.R import me.kavishdevar.librepods.R
import me.kavishdevar.librepods.presentation.components.MaterialButtonStyle import me.kavishdevar.librepods.presentation.components.primitives.MaterialButtonStyle
import me.kavishdevar.librepods.presentation.components.StyledButton import me.kavishdevar.librepods.presentation.components.primitives.StyledButton
import me.kavishdevar.librepods.presentation.components.StyledList import me.kavishdevar.librepods.presentation.components.primitives.StyledList
import me.kavishdevar.librepods.presentation.components.StyledListItem import me.kavishdevar.librepods.presentation.components.primitives.StyledListItem
import me.kavishdevar.librepods.presentation.components.StyledListItemOrientation import me.kavishdevar.librepods.presentation.components.primitives.StyledListItemOrientation
import me.kavishdevar.librepods.presentation.components.StyledScaffold import me.kavishdevar.librepods.presentation.components.primitives.StyledScaffold
import me.kavishdevar.librepods.presentation.theme.DesignSystem
import me.kavishdevar.librepods.presentation.theme.LocalDesignSystem
import me.kavishdevar.librepods.presentation.viewmodel.PurchaseViewModel import me.kavishdevar.librepods.presentation.viewmodel.PurchaseViewModel
import me.kavishdevar.librepods.utils.XposedState import me.kavishdevar.librepods.utils.XposedState
@@ -21,14 +21,10 @@ package me.kavishdevar.librepods.presentation.screens.apple
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.asPaddingValues
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.statusBars
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
@@ -52,15 +48,13 @@ import me.kavishdevar.librepods.bluetooth.aacp.types.ControlCommandIdentifier
import me.kavishdevar.librepods.bluetooth.att.ATTHandle import me.kavishdevar.librepods.bluetooth.att.ATTHandle
import me.kavishdevar.librepods.devices.AirPodsSpecs import me.kavishdevar.librepods.devices.AirPodsSpecs
import me.kavishdevar.librepods.devices.BaseCapability import me.kavishdevar.librepods.devices.BaseCapability
import me.kavishdevar.librepods.presentation.components.StyledButton import me.kavishdevar.librepods.presentation.components.primitives.StyledButton
import me.kavishdevar.librepods.presentation.components.StyledList import me.kavishdevar.librepods.presentation.components.primitives.StyledList
import me.kavishdevar.librepods.presentation.components.StyledListItem import me.kavishdevar.librepods.presentation.components.primitives.StyledListItem
import me.kavishdevar.librepods.presentation.components.StyledScaffold import me.kavishdevar.librepods.presentation.components.primitives.StyledScaffold
import me.kavishdevar.librepods.presentation.components.StyledSlider import me.kavishdevar.librepods.presentation.components.primitives.StyledSlider
import me.kavishdevar.librepods.presentation.components.StyledToggle import me.kavishdevar.librepods.presentation.components.primitives.StyledToggle
import me.kavishdevar.librepods.presentation.icons.LocalIcons import me.kavishdevar.librepods.presentation.icons.LocalIcons
import me.kavishdevar.librepods.presentation.theme.DesignSystem
import me.kavishdevar.librepods.presentation.theme.LocalDesignSystem
import me.kavishdevar.librepods.presentation.viewmodel.AppleViewModel import me.kavishdevar.librepods.presentation.viewmodel.AppleViewModel
import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.milliseconds
@@ -1,126 +0,0 @@
/*
LibrePods - AirPods liberated from Apples ecosystem
Copyright (C) 2025 LibrePods contributors
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package me.kavishdevar.librepods.presentation.screens.apple
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.asPaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.statusBars
import androidx.compose.material3.MaterialTheme
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.mutableFloatStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.kyant.backdrop.backdrops.layerBackdrop
import com.kyant.backdrop.backdrops.rememberLayerBackdrop
import kotlinx.coroutines.flow.debounce
import me.kavishdevar.librepods.R
import me.kavishdevar.librepods.bluetooth.aacp.types.ControlCommandIdentifier
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.icons.LocalIcons
import me.kavishdevar.librepods.presentation.theme.DesignSystem
import me.kavishdevar.librepods.presentation.theme.LocalDesignSystem
import me.kavishdevar.librepods.presentation.viewmodel.AppleViewModel
import kotlin.time.Duration.Companion.milliseconds
@Composable
fun AdaptiveStrengthScreen(
viewModel: AppleViewModel,
navigateBack: (() -> Unit)?,
navigateToPurchase: () -> Unit
) {
val uiState by viewModel.uiState.collectAsState()
val state = uiState.state
val backdrop = rememberLayerBackdrop()
StyledScaffold(
title = stringResource(R.string.customize_adaptive_audio),
navigateBack = navigateBack
) { topPadding, bottomPadding ->
Column(
modifier = Modifier
.fillMaxSize()
.layerBackdrop(backdrop)
.padding(horizontal = 16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
Spacer(modifier = Modifier.height(topPadding))
if (!uiState.isPremium) {
StyledButton(
onClick = navigateToPurchase,
backdrop = rememberLayerBackdrop(),
modifier = Modifier.fillMaxWidth(),
maxScale = 0.05f,
surfaceColor = MaterialTheme.colorScheme.primary
) {
Text(
stringResource(R.string.unlock_advanced_features),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onPrimary
)
}
Spacer(modifier = Modifier.height(16.dp))
}
val sliderValue = remember {
mutableFloatStateOf(100f - (state.controlStates[ControlCommandIdentifier.AUTO_ANC_STRENGTH]?.getOrNull(0)?.toFloat() ?: 50f))
}
LaunchedEffect(sliderValue) {
snapshotFlow { sliderValue.floatValue }
.debounce(100.milliseconds)
.collect { value ->
viewModel.setControlCommand(
ControlCommandIdentifier.AUTO_ANC_STRENGTH,
byteArrayOf((100 - value).toInt().toByte())
)
}
}
StyledSlider(
value = sliderValue.floatValue,
onValueChange = { sliderValue.floatValue = it },
valueRange = 0f..100f,
snapPoints = listOf(0f, 50f, 100f),
startImageVector = LocalIcons.current.SpeakerMin,
endImageVector = LocalIcons.current.SpeakerMax,
independent = true,
description = stringResource(R.string.adaptive_audio_description),
enabled = uiState.isPremium
)
Spacer(modifier = Modifier.height(bottomPadding))
}
}
}
@@ -19,6 +19,11 @@
package me.kavishdevar.librepods.presentation.screens.apple package me.kavishdevar.librepods.presentation.screens.apple
import android.annotation.SuppressLint import android.annotation.SuppressLint
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
@@ -30,36 +35,45 @@ import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
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.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.kyant.backdrop.backdrops.rememberLayerBackdrop import com.kyant.backdrop.backdrops.rememberLayerBackdrop
import kotlinx.coroutines.flow.debounce
import me.kavishdevar.librepods.R import me.kavishdevar.librepods.R
import me.kavishdevar.librepods.bluetooth.aacp.types.ControlCommandIdentifier import me.kavishdevar.librepods.bluetooth.aacp.types.ControlCommandIdentifier
import me.kavishdevar.librepods.bluetooth.att.ATTHandle import me.kavishdevar.librepods.bluetooth.att.ATTHandle
import me.kavishdevar.librepods.devices.AirPodsSpecs import me.kavishdevar.librepods.devices.AirPodsSpecs
import me.kavishdevar.librepods.devices.AppleSettings
import me.kavishdevar.librepods.devices.BaseCapability import me.kavishdevar.librepods.devices.BaseCapability
import me.kavishdevar.librepods.presentation.components.AboutCard import me.kavishdevar.librepods.presentation.components.apple.AboutCard
import me.kavishdevar.librepods.presentation.components.AudioSettings import me.kavishdevar.librepods.presentation.components.apple.AudioSettings
import me.kavishdevar.librepods.presentation.components.BatteryView import me.kavishdevar.librepods.presentation.components.apple.BatteryView
import me.kavishdevar.librepods.presentation.components.CallControlSettings import me.kavishdevar.librepods.presentation.components.apple.CallControlSettings
import me.kavishdevar.librepods.presentation.components.ConnectionSettings import me.kavishdevar.librepods.presentation.components.apple.ConnectionSettings
import me.kavishdevar.librepods.presentation.components.HearingHealthSettings import me.kavishdevar.librepods.presentation.components.apple.HearingHealthSettings
import me.kavishdevar.librepods.presentation.components.NoiseControlSettings import me.kavishdevar.librepods.presentation.components.apple.NoiseControlSettings
import me.kavishdevar.librepods.presentation.components.PressAndHoldSettings import me.kavishdevar.librepods.presentation.components.apple.PressAndHoldSettings
import me.kavishdevar.librepods.presentation.components.StyledButton import me.kavishdevar.librepods.presentation.components.primitives.StyledButton
import me.kavishdevar.librepods.presentation.components.StyledListItem import me.kavishdevar.librepods.presentation.components.primitives.StyledListItem
import me.kavishdevar.librepods.presentation.components.StyledListItemOrientation import me.kavishdevar.librepods.presentation.components.primitives.StyledListItemOrientation
import me.kavishdevar.librepods.presentation.components.StyledScaffold import me.kavishdevar.librepods.presentation.components.primitives.StyledScaffold
import me.kavishdevar.librepods.presentation.components.StyledToggle import me.kavishdevar.librepods.presentation.components.primitives.StyledSlider
import me.kavishdevar.librepods.presentation.components.primitives.StyledToggle
import me.kavishdevar.librepods.presentation.icons.LocalIcons
import me.kavishdevar.librepods.presentation.theme.DesignSystem import me.kavishdevar.librepods.presentation.theme.DesignSystem
import me.kavishdevar.librepods.presentation.theme.LibrePodsTheme import me.kavishdevar.librepods.presentation.theme.LibrePodsTheme
import me.kavishdevar.librepods.presentation.viewmodel.AppleUiState import me.kavishdevar.librepods.presentation.viewmodel.AppleUiState
import me.kavishdevar.librepods.presentation.viewmodel.AppleViewModel import me.kavishdevar.librepods.presentation.viewmodel.AppleViewModel
import kotlin.time.Duration.Companion.milliseconds
@Composable @Composable
fun AppleSettingsRoute( fun AppleSettingsRoute(
@@ -71,7 +85,6 @@ fun AppleSettingsRoute(
navigateToLeftLongPress: () -> Unit, navigateToLeftLongPress: () -> Unit,
navigateToRightLongPress: () -> Unit, navigateToRightLongPress: () -> Unit,
navigateToPurchase: () -> Unit, navigateToPurchase: () -> Unit,
navigateToAdaptiveStrength: () -> Unit,
navigateToEqualizer: () -> Unit, navigateToEqualizer: () -> Unit,
navigateToHeadTracking: () -> Unit, navigateToHeadTracking: () -> Unit,
navigateToAccessibility: () -> Unit, navigateToAccessibility: () -> Unit,
@@ -94,7 +107,8 @@ fun AppleSettingsRoute(
writeATTCharacteristic = viewModel::writeATTCharacteristic, writeATTCharacteristic = viewModel::writeATTCharacteristic,
// onAutomaticEarDetectionChanged = viewModel::setAutomaticEarDetectionEnabled, updateSettings = viewModel::updateSettings,
// onAutomaticConnectionChanged = viewModel::setAutomaticConnectionEnabled, // onAutomaticConnectionChanged = viewModel::setAutomaticConnectionEnabled,
disconnect = viewModel::disconnect, disconnect = viewModel::disconnect,
@@ -105,7 +119,6 @@ fun AppleSettingsRoute(
navigateToLeftLongPress = navigateToLeftLongPress, navigateToLeftLongPress = navigateToLeftLongPress,
navigateToRightLongPress = navigateToRightLongPress, navigateToRightLongPress = navigateToRightLongPress,
navigateToPurchase = navigateToPurchase, navigateToPurchase = navigateToPurchase,
navigateToAdaptiveStrength = navigateToAdaptiveStrength,
navigateToEqualizer = navigateToEqualizer, navigateToEqualizer = navigateToEqualizer,
navigateToHeadTracking = navigateToHeadTracking, navigateToHeadTracking = navigateToHeadTracking,
navigateToAccessibility = navigateToAccessibility, navigateToAccessibility = navigateToAccessibility,
@@ -130,6 +143,8 @@ fun AppleSettingsScreen(
writeATTCharacteristic: (ATTHandle, ByteArray) -> Unit, writeATTCharacteristic: (ATTHandle, ByteArray) -> Unit,
updateSettings: (transform: (AppleSettings) -> AppleSettings) -> Unit,
// onAutomaticEarDetectionChanged: (Boolean) -> Unit, // onAutomaticEarDetectionChanged: (Boolean) -> Unit,
// onAutomaticConnectionChanged: (Boolean) -> Unit, // onAutomaticConnectionChanged: (Boolean) -> Unit,
@@ -142,7 +157,6 @@ fun AppleSettingsScreen(
navigateToLeftLongPress: () -> Unit, navigateToLeftLongPress: () -> Unit,
navigateToRightLongPress: () -> Unit, navigateToRightLongPress: () -> Unit,
navigateToPurchase: () -> Unit, navigateToPurchase: () -> Unit,
navigateToAdaptiveStrength: () -> Unit,
navigateToEqualizer: () -> Unit, navigateToEqualizer: () -> Unit,
navigateToHeadTracking: () -> Unit, navigateToHeadTracking: () -> Unit,
navigateToAccessibility: () -> Unit, navigateToAccessibility: () -> Unit,
@@ -209,20 +223,6 @@ fun AppleSettingsScreen(
} }
} }
if (metadata.version3.startsWith("8") || metadata.version3.startsWith("9")) {
item(key = "spacer_recording") {
Spacer(modifier = Modifier.height(16.dp))
}
item(key = "recording") {
StyledListItem(
contentText = stringResource(R.string.recorder),
supportingText = stringResource(R.string.recorder_description),
onClick = navigateToRecordingScreen,
orientation = StyledListItemOrientation.Vertical
)
}
}
if (baseCapabilities.contains(BaseCapability.LISTENING_MODE)) { if (baseCapabilities.contains(BaseCapability.LISTENING_MODE)) {
item(key = "spacer_noise") { item(key = "spacer_noise") {
Spacer(modifier = Modifier.height(16.dp)) Spacer(modifier = Modifier.height(16.dp))
@@ -238,6 +238,67 @@ fun AppleSettingsScreen(
}, },
) )
} }
if (baseCapabilities.contains(BaseCapability.ADAPTIVE_AUDIO)) {
item(key = "adaptive_strength") {
AnimatedVisibility(
visible = state.controlStates[ControlCommandIdentifier.LISTENING_MODE]?.getOrNull(0)?.toInt() == 4,
enter = remember {
fadeIn() + slideInVertically()
},
exit = remember {
fadeOut() + slideOutVertically()
}
) {
val sliderValue = remember {
mutableFloatStateOf(
100f - (state.controlStates[ControlCommandIdentifier.AUTO_ANC_STRENGTH]?.getOrNull(
0
)?.toFloat() ?: 50f)
)
}
LaunchedEffect(sliderValue) {
snapshotFlow { sliderValue.floatValue }
.debounce(100.milliseconds)
.collect { value ->
setControlCommandInt(
ControlCommandIdentifier.AUTO_ANC_STRENGTH,
(100 - value).toInt()
)
}
}
StyledSlider(
value = sliderValue.floatValue,
onValueChange = { sliderValue.floatValue = it },
valueRange = 0f..100f,
snapPoints = listOf(0f, 50f, 100f),
startImageVector = LocalIcons.current.SpeakerMin,
endImageVector = LocalIcons.current.SpeakerMax,
independent = true,
description = stringResource(R.string.adaptive_audio_description),
enabled = uiState.isPremium
)
Spacer(modifier = Modifier.height(16.dp))
}
}
}
}
if (metadata.version3.isNotBlank() && metadata.version3.first().digitToInt() >= 8) {
item(key = "spacer_recording") {
Spacer(modifier = Modifier.height(16.dp))
}
item(key = "recording") {
StyledListItem(
contentText = stringResource(R.string.recorder),
supportingText = stringResource(R.string.recorder_description),
onClick = navigateToRecordingScreen,
orientation = StyledListItemOrientation.Vertical
)
}
} }
if (baseCapabilities.contains(BaseCapability.HRM)) { if (baseCapabilities.contains(BaseCapability.HRM)) {
@@ -319,30 +380,18 @@ fun AppleSettingsScreen(
item(key = "spacer_audio") { Spacer(modifier = Modifier.height(16.dp)) } item(key = "spacer_audio") { Spacer(modifier = Modifier.height(16.dp)) }
item(key = "audio") { item(key = "audio") {
val adaptiveVolumeCapability = val adaptiveVolumeCapability = baseCapabilities.contains(BaseCapability.ADAPTIVE_VOLUME)
baseCapabilities.contains(BaseCapability.ADAPTIVE_VOLUME) val conversationalAwarenessCapability = baseCapabilities.contains(BaseCapability.CONVERSATION_AWARENESS)
val conversationalAwarenessCapability = val loudSoundReductionCapability = baseCapabilities.contains(BaseCapability.LOUD_SOUND_REDUCTION)
baseCapabilities.contains(BaseCapability.CONVERSATION_AWARENESS)
val loudSoundReductionCapability =
baseCapabilities.contains(BaseCapability.LOUD_SOUND_REDUCTION)
val adaptiveAudioCapability =
baseCapabilities.contains(BaseCapability.ADAPTIVE_VOLUME)
val adaptiveVolumeChecked = val adaptiveVolumeChecked = state.controlStates[ControlCommandIdentifier.ADAPTIVE_VOLUME_CONFIG]?.getOrNull(0) == 0x01.toByte()
state.controlStates[ControlCommandIdentifier.ADAPTIVE_VOLUME_CONFIG]?.getOrNull( val conversationalAwarenessChecked = state.controlStates[ControlCommandIdentifier.CONVERSATION_DETECT_CONFIG]?.getOrNull(0) == 0x01.toByte()
0
) == 0x01.toByte()
val conversationalAwarenessChecked =
state.controlStates[ControlCommandIdentifier.CONVERSATION_DETECT_CONFIG]?.getOrNull(
0
) == 0x01.toByte()
AudioSettings( AudioSettings(
adaptiveVolumeCapability = adaptiveVolumeCapability, adaptiveVolumeCapability = adaptiveVolumeCapability,
conversationalAwarenessCapability = conversationalAwarenessCapability, conversationalAwarenessCapability = conversationalAwarenessCapability,
loudSoundReductionCapability = loudSoundReductionCapability, loudSoundReductionCapability = loudSoundReductionCapability,
adaptiveAudioCapability = adaptiveAudioCapability, customEqCapability = metadata.version3.isNotBlank() && metadata.version3.first().digitToInt() >= 9,
customEqCapability = metadata.version3.startsWith("9"),
adaptiveVolumeChecked = adaptiveVolumeChecked, adaptiveVolumeChecked = adaptiveVolumeChecked,
onAdaptiveVolumeCheckedChange = { checked -> onAdaptiveVolumeCheckedChange = { checked ->
setControlCommandBoolean( setControlCommandBoolean(
@@ -364,7 +413,6 @@ fun AppleSettingsScreen(
byteArrayOf(if (checked) 0x01.toByte() else 0x00.toByte()) byteArrayOf(if (checked) 0x01.toByte() else 0x00.toByte())
) )
}, },
navigateToAdaptiveStrength = navigateToAdaptiveStrength,
navigateToEqualizer = navigateToEqualizer, navigateToEqualizer = navigateToEqualizer,
vendorIdHook = uiState.vendorIdHook, vendorIdHook = uiState.vendorIdHook,
isPremium = uiState.isPremium isPremium = uiState.isPremium
@@ -374,10 +422,28 @@ fun AppleSettingsScreen(
item(key = "spacer_connection") { Spacer(modifier = Modifier.height(16.dp)) } item(key = "spacer_connection") { Spacer(modifier = Modifier.height(16.dp)) }
item(key = "connection") { item(key = "connection") {
ConnectionSettings( ConnectionSettings(
automaticEarDetectionEnabled = state.controlStates[ControlCommandIdentifier.EAR_DETECTION_CONFIG]?.getOrNull(0) == 0x01.toByte(), automaticEarDetectionEnabled = state.controlStates[ControlCommandIdentifier.EAR_DETECTION_CONFIG]?.getOrNull(0) == 0x01.toByte() || settings.earDetectionEnabled,
onAutomaticEarDetectionChanged = { setControlCommandBoolean(ControlCommandIdentifier.EAR_DETECTION_CONFIG, it) }, onAutomaticEarDetectionChanged = { enabled -> setControlCommandBoolean(ControlCommandIdentifier.EAR_DETECTION_CONFIG, enabled); updateSettings { it.copy(earDetectionEnabled = enabled) } },
automaticConnectionEnabled = state.controlStates[ControlCommandIdentifier.SMART_ROUTING_MODE]?.getOrNull(0) == 0x01.toByte(), automaticConnectionEnabled = state.controlStates[ControlCommandIdentifier.SMART_ROUTING_MODE]?.getOrNull(0) == 0x01.toByte(),
onAutomaticConnectionChanged = { setControlCommandBoolean(ControlCommandIdentifier.SMART_ROUTING_MODE, it) } onAutomaticConnectionChanged = { setControlCommandBoolean(ControlCommandIdentifier.SMART_ROUTING_MODE, it) },
disconnectWhenNotWearing = settings.disconnectWhenNotWearing,
onDisconnectWhenNotWearingChanged = { enabled -> updateSettings { it.copy(disconnectWhenNotWearing = enabled) } },
takeoverWhenDisconnected = settings.takeoverWhenDisconnected,
onTakeoverWhenDisconnectedChanged = { enabled -> updateSettings { it.copy(takeoverWhenDisconnected = enabled) } },
takeoverWhenIdle = settings.takeoverWhenIdle,
onTakeoverWhenIdleChanged = { enabled -> updateSettings { it.copy(takeoverWhenIdle = enabled) } },
takeoverWhenMusic = settings.takeoverWhenMusic,
onTakeoverWhenMusicChanged = { enabled -> updateSettings { it.copy(takeoverWhenMusic = enabled) } },
takeoverWhenCall = settings.takeoverWhenCall,
onTakeoverWhenCallChanged = { enabled -> updateSettings { it.copy(takeoverWhenCall = enabled) } },
takeoverWhenRingingCall = settings.takeoverWhenRingingCall,
onTakeoverWhenRingingCallChanged = { enabled -> updateSettings { it.copy(takeoverWhenRingingCall = enabled) } },
takeoverWhenMediaStart = settings.takeoverWhenMediaStart,
onTakeoverWhenMediaStartChanged = { enabled -> updateSettings { it.copy(takeoverWhenMediaStart = enabled) } },
isPremium = uiState.isPremium
) )
} }
@@ -441,7 +507,7 @@ fun AppleSettingsScreen(
) )
} }
if (baseCapabilities.contains(BaseCapability.LOUD_SOUND_REDUCTION) && (metadata.version3.startsWith("8") || metadata.version3.startsWith("9"))) { if (baseCapabilities.contains(BaseCapability.LOUD_SOUND_REDUCTION) && (metadata.version3.isNotBlank() && metadata.version3.first().digitToInt() >= 8)) {
item(key = "spacer_off_listening") { Spacer(modifier = Modifier.height(16.dp)) } item(key = "spacer_off_listening") { Spacer(modifier = Modifier.height(16.dp)) }
item(key = "off_listening") { item(key = "off_listening") {
val id = ControlCommandIdentifier.ALLOW_OFF_OPTION val id = ControlCommandIdentifier.ALLOW_OFF_OPTION
@@ -487,7 +553,16 @@ fun AppleSettingsScreen(
item(key = "spacer_debug") { Spacer(modifier = Modifier.height(16.dp)) } item(key = "spacer_debug") { Spacer(modifier = Modifier.height(16.dp)) }
if (uiState.appSettings.debugMode) { if (uiState.appSettings.debugMode) {
item(key = "debug") { item(key = "show_cached_battery") {
StyledToggle(
label = "show cached battery",
checked = settings.cacheDisconnectedComponentBattery,
onCheckedChange = { enabled ->
updateSettings { it.copy(cacheDisconnectedComponentBattery = enabled) }
}
)
}
item(key = "debug_button") {
StyledListItem( StyledListItem(
contentText = "debug", contentText = "debug",
onClick = navigateToDebugScreen onClick = navigateToDebugScreen
@@ -517,6 +592,8 @@ fun AppleSettingsScreenPreviewApple() {
setControlCommandBoolean = { _, _ -> }, setControlCommandBoolean = { _, _ -> },
writeATTCharacteristic = { _, _ -> }, writeATTCharacteristic = { _, _ -> },
updateSettings = { _ -> },
disconnect = {}, disconnect = {},
navigateBack = null, navigateBack = null,
@@ -526,7 +603,6 @@ fun AppleSettingsScreenPreviewApple() {
navigateToLeftLongPress = {}, navigateToLeftLongPress = {},
navigateToRightLongPress = {}, navigateToRightLongPress = {},
navigateToPurchase = {}, navigateToPurchase = {},
navigateToAdaptiveStrength = {},
navigateToEqualizer = {}, navigateToEqualizer = {},
navigateToHeadTracking = {}, navigateToHeadTracking = {},
navigateToAccessibility = {}, navigateToAccessibility = {},
@@ -559,6 +635,8 @@ fun AppleSettingsScreenPreviewMaterial() {
setControlCommandBoolean = { _, _ -> }, setControlCommandBoolean = { _, _ -> },
writeATTCharacteristic = { _, _ -> }, writeATTCharacteristic = { _, _ -> },
updateSettings = { _ -> },
disconnect = {}, disconnect = {},
navigateBack = null, navigateBack = null,
@@ -568,7 +646,6 @@ fun AppleSettingsScreenPreviewMaterial() {
navigateToLeftLongPress = {}, navigateToLeftLongPress = {},
navigateToRightLongPress = {}, navigateToRightLongPress = {},
navigateToPurchase = {}, navigateToPurchase = {},
navigateToAdaptiveStrength = {},
navigateToEqualizer = {}, navigateToEqualizer = {},
navigateToHeadTracking = {}, navigateToHeadTracking = {},
navigateToAccessibility = {}, navigateToAccessibility = {},
@@ -19,9 +19,9 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import me.kavishdevar.librepods.R import me.kavishdevar.librepods.R
import me.kavishdevar.librepods.bluetooth.aacp.types.ControlCommandIdentifier import me.kavishdevar.librepods.bluetooth.aacp.types.ControlCommandIdentifier
import me.kavishdevar.librepods.presentation.components.StyledList import me.kavishdevar.librepods.presentation.components.primitives.StyledList
import me.kavishdevar.librepods.presentation.components.StyledListItem import me.kavishdevar.librepods.presentation.components.primitives.StyledListItem
import me.kavishdevar.librepods.presentation.components.StyledScaffold import me.kavishdevar.librepods.presentation.components.primitives.StyledScaffold
import me.kavishdevar.librepods.presentation.viewmodel.AppleViewModel import me.kavishdevar.librepods.presentation.viewmodel.AppleViewModel
@Composable @Composable
@@ -34,12 +34,12 @@ import me.kavishdevar.librepods.bluetooth.aacp.types.RTBuddyDescriptor
import me.kavishdevar.librepods.bluetooth.aacp.types.SensorDataWxBuddyPayload import me.kavishdevar.librepods.bluetooth.aacp.types.SensorDataWxBuddyPayload
import me.kavishdevar.librepods.devices.DeviceComponent import me.kavishdevar.librepods.devices.DeviceComponent
import me.kavishdevar.librepods.devices.PacketDestination import me.kavishdevar.librepods.devices.PacketDestination
import me.kavishdevar.librepods.presentation.components.StyledButton import me.kavishdevar.librepods.presentation.components.primitives.StyledButton
import me.kavishdevar.librepods.presentation.components.StyledInputField import me.kavishdevar.librepods.presentation.components.primitives.StyledInputField
import me.kavishdevar.librepods.presentation.components.StyledList import me.kavishdevar.librepods.presentation.components.primitives.StyledList
import me.kavishdevar.librepods.presentation.components.StyledListItem import me.kavishdevar.librepods.presentation.components.primitives.StyledListItem
import me.kavishdevar.librepods.presentation.components.StyledListItemOrientation import me.kavishdevar.librepods.presentation.components.primitives.StyledListItemOrientation
import me.kavishdevar.librepods.presentation.components.StyledScaffold import me.kavishdevar.librepods.presentation.components.primitives.StyledScaffold
import me.kavishdevar.librepods.presentation.icons.richText import me.kavishdevar.librepods.presentation.icons.richText
import me.kavishdevar.librepods.presentation.viewmodel.AppleUiState import me.kavishdevar.librepods.presentation.viewmodel.AppleUiState
import me.kavishdevar.librepods.presentation.viewmodel.AppleViewModel import me.kavishdevar.librepods.presentation.viewmodel.AppleViewModel
@@ -77,10 +77,10 @@ import com.kyant.backdrop.effects.lens
import com.kyant.backdrop.highlight.Highlight import com.kyant.backdrop.highlight.Highlight
import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.debounce
import me.kavishdevar.librepods.R import me.kavishdevar.librepods.R
import me.kavishdevar.librepods.presentation.components.StyledButton import me.kavishdevar.librepods.presentation.components.primitives.StyledButton
import me.kavishdevar.librepods.presentation.components.StyledList import me.kavishdevar.librepods.presentation.components.primitives.StyledList
import me.kavishdevar.librepods.presentation.components.StyledListItem import me.kavishdevar.librepods.presentation.components.primitives.StyledListItem
import me.kavishdevar.librepods.presentation.components.StyledScaffold import me.kavishdevar.librepods.presentation.components.primitives.StyledScaffold
import me.kavishdevar.librepods.presentation.theme.DesignSystem import me.kavishdevar.librepods.presentation.theme.DesignSystem
import me.kavishdevar.librepods.presentation.theme.LibrePodsTheme import me.kavishdevar.librepods.presentation.theme.LibrePodsTheme
import me.kavishdevar.librepods.presentation.viewmodel.AppleUiState import me.kavishdevar.librepods.presentation.viewmodel.AppleUiState
@@ -84,12 +84,12 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import me.kavishdevar.librepods.R import me.kavishdevar.librepods.R
import me.kavishdevar.librepods.presentation.components.MaterialButtonStyle import me.kavishdevar.librepods.presentation.components.primitives.MaterialButtonStyle
import me.kavishdevar.librepods.presentation.components.StyledButton import me.kavishdevar.librepods.presentation.components.primitives.StyledButton
import me.kavishdevar.librepods.presentation.components.StyledIconButton import me.kavishdevar.librepods.presentation.components.primitives.StyledIconButton
import me.kavishdevar.librepods.presentation.components.StyledScaffold import me.kavishdevar.librepods.presentation.components.primitives.StyledScaffold
import me.kavishdevar.librepods.presentation.components.StyledSlider import me.kavishdevar.librepods.presentation.components.primitives.StyledSlider
import me.kavishdevar.librepods.presentation.components.StyledToggle import me.kavishdevar.librepods.presentation.components.primitives.StyledToggle
import me.kavishdevar.librepods.presentation.icons.LocalIcons import me.kavishdevar.librepods.presentation.icons.LocalIcons
import me.kavishdevar.librepods.presentation.icons.MaterialIcons import me.kavishdevar.librepods.presentation.icons.MaterialIcons
import me.kavishdevar.librepods.presentation.theme.DesignSystem import me.kavishdevar.librepods.presentation.theme.DesignSystem
@@ -48,9 +48,9 @@ import me.kavishdevar.librepods.bluetooth.att.ATTHandle
import me.kavishdevar.librepods.bluetooth.att.types.HearingAidSettings import me.kavishdevar.librepods.bluetooth.att.types.HearingAidSettings
import me.kavishdevar.librepods.bluetooth.att.types.parseHearingAidSettingsResponse import me.kavishdevar.librepods.bluetooth.att.types.parseHearingAidSettingsResponse
import me.kavishdevar.librepods.bluetooth.att.types.sendHearingAidSettings import me.kavishdevar.librepods.bluetooth.att.types.sendHearingAidSettings
import me.kavishdevar.librepods.presentation.components.StyledScaffold import me.kavishdevar.librepods.presentation.components.primitives.StyledScaffold
import me.kavishdevar.librepods.presentation.components.StyledSlider import me.kavishdevar.librepods.presentation.components.primitives.StyledSlider
import me.kavishdevar.librepods.presentation.components.StyledToggle import me.kavishdevar.librepods.presentation.components.primitives.StyledToggle
import me.kavishdevar.librepods.presentation.icons.LocalIcons import me.kavishdevar.librepods.presentation.icons.LocalIcons
import me.kavishdevar.librepods.presentation.viewmodel.AppleViewModel import me.kavishdevar.librepods.presentation.viewmodel.AppleViewModel
@@ -50,11 +50,11 @@ import me.kavishdevar.librepods.R
import me.kavishdevar.librepods.bluetooth.aacp.types.ControlCommandIdentifier import me.kavishdevar.librepods.bluetooth.aacp.types.ControlCommandIdentifier
import me.kavishdevar.librepods.bluetooth.att.types.parseTransparencySettingsResponse import me.kavishdevar.librepods.bluetooth.att.types.parseTransparencySettingsResponse
import me.kavishdevar.librepods.bluetooth.att.types.sendTransparencySettings import me.kavishdevar.librepods.bluetooth.att.types.sendTransparencySettings
import me.kavishdevar.librepods.presentation.components.ConfirmationDialog import me.kavishdevar.librepods.presentation.components.primitives.StyledConfirmationDialog
import me.kavishdevar.librepods.presentation.components.StyledList import me.kavishdevar.librepods.presentation.components.primitives.StyledList
import me.kavishdevar.librepods.presentation.components.StyledListItem import me.kavishdevar.librepods.presentation.components.primitives.StyledListItem
import me.kavishdevar.librepods.presentation.components.StyledScaffold import me.kavishdevar.librepods.presentation.components.primitives.StyledScaffold
import me.kavishdevar.librepods.presentation.components.StyledToggle import me.kavishdevar.librepods.presentation.components.primitives.StyledToggle
import me.kavishdevar.librepods.presentation.viewmodel.AppleViewModel import me.kavishdevar.librepods.presentation.viewmodel.AppleViewModel
private const val TAG = "HearingAidScreen" private const val TAG = "HearingAidScreen"
@@ -187,7 +187,7 @@ fun HearingAidScreen(
} }
} }
ConfirmationDialog( StyledConfirmationDialog(
showDialog = showDialog, showDialog = showDialog,
title = "Enable Hearing Aid", title = "Enable Hearing Aid",
message = "Enabling Hearing Aid will disable Headphone Accommodation and Customized Transparency Mode.", message = "Enabling Hearing Aid will disable Headphone Accommodation and Customized Transparency Mode.",
@@ -38,9 +38,9 @@ import com.kyant.backdrop.backdrops.rememberLayerBackdrop
import me.kavishdevar.librepods.R import me.kavishdevar.librepods.R
import me.kavishdevar.librepods.bluetooth.aacp.types.ControlCommandIdentifier import me.kavishdevar.librepods.bluetooth.aacp.types.ControlCommandIdentifier
import me.kavishdevar.librepods.bluetooth.att.ATTHandle import me.kavishdevar.librepods.bluetooth.att.ATTHandle
import me.kavishdevar.librepods.presentation.components.StyledButton import me.kavishdevar.librepods.presentation.components.primitives.StyledButton
import me.kavishdevar.librepods.presentation.components.StyledScaffold import me.kavishdevar.librepods.presentation.components.primitives.StyledScaffold
import me.kavishdevar.librepods.presentation.components.StyledToggle import me.kavishdevar.librepods.presentation.components.primitives.StyledToggle
import me.kavishdevar.librepods.presentation.viewmodel.AppleViewModel import me.kavishdevar.librepods.presentation.viewmodel.AppleViewModel
@Composable @Composable
@@ -27,10 +27,12 @@ import androidx.compose.material3.Text
import androidx.compose.material3.minimumInteractiveComponentSize import androidx.compose.material3.minimumInteractiveComponentSize
import androidx.compose.material3.toShape import androidx.compose.material3.toShape
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalLocale import androidx.compose.ui.platform.LocalLocale
@@ -41,11 +43,15 @@ import androidx.health.connect.client.records.HeartRateRecord
import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.isGranted import com.google.accompanist.permissions.isGranted
import com.google.accompanist.permissions.rememberPermissionState import com.google.accompanist.permissions.rememberPermissionState
import kotlinx.coroutines.flow.debounce
import me.kavishdevar.librepods.R import me.kavishdevar.librepods.R
import me.kavishdevar.librepods.presentation.components.StyledIconButton import me.kavishdevar.librepods.devices.AppleSettings
import me.kavishdevar.librepods.presentation.components.StyledListItem import me.kavishdevar.librepods.presentation.components.primitives.StyledIconButton
import me.kavishdevar.librepods.presentation.components.StyledListItemOrientation import me.kavishdevar.librepods.presentation.components.primitives.StyledListItem
import me.kavishdevar.librepods.presentation.components.StyledScaffold import me.kavishdevar.librepods.presentation.components.primitives.StyledListItemOrientation
import me.kavishdevar.librepods.presentation.components.primitives.StyledScaffold
import me.kavishdevar.librepods.presentation.components.primitives.StyledSlider
import me.kavishdevar.librepods.presentation.components.primitives.StyledToggle
import me.kavishdevar.librepods.presentation.icons.LocalIcons import me.kavishdevar.librepods.presentation.icons.LocalIcons
import me.kavishdevar.librepods.presentation.icons.MaterialIcons import me.kavishdevar.librepods.presentation.icons.MaterialIcons
import me.kavishdevar.librepods.presentation.theme.DesignSystem import me.kavishdevar.librepods.presentation.theme.DesignSystem
@@ -55,6 +61,8 @@ import me.kavishdevar.librepods.presentation.viewmodel.AppleViewModel
import java.time.Instant import java.time.Instant
import java.time.ZoneId import java.time.ZoneId
import java.time.format.DateTimeFormatter import java.time.format.DateTimeFormatter
import kotlin.math.roundToInt
import kotlin.time.Duration.Companion.milliseconds
@Composable @Composable
fun HeartRateRoute( fun HeartRateRoute(
@@ -68,7 +76,8 @@ fun HeartRateRoute(
navigateBack = navigateBack, navigateBack = navigateBack,
startHr = viewModel::startHr, startHr = viewModel::startHr,
stopHr = viewModel::stopHr, stopHr = viewModel::stopHr,
setHrRange = viewModel::setHrRange setHrRange = viewModel::setHrRange,
updateSettings = viewModel::updateSettings
) )
} }
@@ -79,9 +88,11 @@ fun HeartRateScreen(
navigateBack: (() -> Unit)?, navigateBack: (() -> Unit)?,
startHr: () -> Unit, startHr: () -> Unit,
stopHr: () -> Unit, stopHr: () -> Unit,
setHrRange: (ClosedRange<Long>) -> Unit setHrRange: (ClosedRange<Long>) -> Unit,
updateSettings: (transform: (AppleSettings) -> AppleSettings) -> Unit,
) { ) {
val state = uiState.state val state = uiState.state
val settings = uiState.settings
val scrollState = rememberScrollState() val scrollState = rememberScrollState()
@@ -129,59 +140,7 @@ fun HeartRateScreen(
) { ) {
Spacer(modifier = Modifier.height(topPadding)) Spacer(modifier = Modifier.height(topPadding))
val sliderValue = remember { val healthPermissions = rememberPermissionState(HealthPermission.getWritePermission(HeartRateRecord::class))
mutableFloatStateOf(state.heartRateInterval.inWholeMilliseconds.toFloat())
}
val healthPermissions = rememberPermissionState(
HealthPermission.getWritePermission(HeartRateRecord::class)
)
AnimatedVisibility(visible = !healthPermissions.status.isGranted) {
if (healthPermissions.status.isGranted) return@AnimatedVisibility
StyledListItem(
contentText = stringResource(R.string.permission_healthconnect),
onClick = { healthPermissions.launchPermissionRequest() },
supportingText = stringResource(R.string.permission_description_healthconnect),
leadingContent = {
Box(
modifier = Modifier
.size(48.dp)
.background(
MaterialTheme.colorScheme.surfaceContainerLow,
MaterialShapes.SoftBurst.normalized()
.toShape()
),
contentAlignment = Alignment.Center
) {
Icon(
imageVector = LocalIcons.current.VitalSigns,
contentDescription = "vital signs",
modifier = Modifier.size(24.dp),
tint = MaterialTheme.colorScheme.onSurface
)
}
},
orientation = StyledListItemOrientation.Vertical
)
}
// saw this while using one of the forks. 1 minute doesn't seem to work for me
// StyledList(
// title = stringResource(R.string.interval),
// description = stringResource(R.string.heart_rate_interval_description),
// ) {
// StyledListItem(
// onClick = { setHrInterval(1.seconds) },
// contentText = stringResource(R.string.one_second),
// selected = state.heartRateInterval == 1.seconds
// )
// StyledListItem(
// onClick = { setHrInterval(1.minutes) },
// contentText = stringResource(R.string.one_minute),
// selected = state.heartRateInterval == 1.minutes
// )
// }
AnimatedVisibility(state.currentHeartRate != null) { AnimatedVisibility(state.currentHeartRate != null) {
if (state.currentHeartRate == null) return@AnimatedVisibility if (state.currentHeartRate == null) return@AnimatedVisibility
@@ -235,6 +194,66 @@ fun HeartRateScreen(
) )
} }
AnimatedVisibility(visible = !healthPermissions.status.isGranted) {
StyledListItem(
contentText = stringResource(R.string.permission_healthconnect),
onClick = { healthPermissions.launchPermissionRequest() },
supportingText = stringResource(R.string.permission_description_healthconnect),
leadingContent = {
Box(
modifier = Modifier
.size(48.dp)
.background(
MaterialTheme.colorScheme.surfaceContainerLow,
MaterialShapes.SoftBurst.normalized()
.toShape()
),
contentAlignment = Alignment.Center
) {
Icon(
imageVector = LocalIcons.current.VitalSigns,
contentDescription = "vital signs",
modifier = Modifier.size(24.dp),
tint = MaterialTheme.colorScheme.onSurface
)
}
},
orientation = StyledListItemOrientation.Vertical
)
}
StyledToggle(
label = stringResource(R.string.heart_rate_alert),
description = stringResource(R.string.hrm_alert_description),
checked = settings.hrAlertEnabled,
onCheckedChange = { enabled ->
updateSettings {
it.copy(hrAlertEnabled = enabled)
}
}
)
val sliderValue = remember { mutableFloatStateOf(settings.hrmAlertThreshold.toFloat()) }
LaunchedEffect(sliderValue) {
snapshotFlow { sliderValue.floatValue }
.debounce(250.milliseconds)
.collect { value ->
updateSettings {
it.copy(hrmAlertThreshold = value.toInt())
}
}
}
StyledSlider(
label = stringResource(R.string.heart_rate_alert_threshold),
value = sliderValue.floatValue,
onValueChange = { sliderValue.floatValue = it },
valueRange = 120f..180f,
description = "${sliderValue.floatValue.roundToInt()} bpm",
independent = true
)
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
@@ -17,9 +17,9 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import me.kavishdevar.librepods.R import me.kavishdevar.librepods.R
import me.kavishdevar.librepods.bluetooth.aacp.types.ControlCommandIdentifier import me.kavishdevar.librepods.bluetooth.aacp.types.ControlCommandIdentifier
import me.kavishdevar.librepods.presentation.components.StyledList import me.kavishdevar.librepods.presentation.components.primitives.StyledList
import me.kavishdevar.librepods.presentation.components.StyledListItem import me.kavishdevar.librepods.presentation.components.primitives.StyledListItem
import me.kavishdevar.librepods.presentation.components.StyledScaffold import me.kavishdevar.librepods.presentation.components.primitives.StyledScaffold
import me.kavishdevar.librepods.presentation.viewmodel.AppleViewModel import me.kavishdevar.librepods.presentation.viewmodel.AppleViewModel
@Composable @Composable
@@ -45,11 +45,11 @@ import com.kyant.backdrop.backdrops.rememberLayerBackdrop
import me.kavishdevar.librepods.R import me.kavishdevar.librepods.R
import me.kavishdevar.librepods.bluetooth.aacp.types.ControlCommandIdentifier import me.kavishdevar.librepods.bluetooth.aacp.types.ControlCommandIdentifier
import me.kavishdevar.librepods.data.StemAction import me.kavishdevar.librepods.data.StemAction
import me.kavishdevar.librepods.presentation.components.StyledButton import me.kavishdevar.librepods.presentation.components.primitives.StyledButton
import me.kavishdevar.librepods.presentation.components.StyledList import me.kavishdevar.librepods.presentation.components.primitives.StyledList
import me.kavishdevar.librepods.presentation.components.StyledListItem import me.kavishdevar.librepods.presentation.components.primitives.StyledListItem
import me.kavishdevar.librepods.presentation.components.StyledListItemOrientation import me.kavishdevar.librepods.presentation.components.primitives.StyledListItemOrientation
import me.kavishdevar.librepods.presentation.components.StyledScaffold import me.kavishdevar.librepods.presentation.components.primitives.StyledScaffold
import me.kavishdevar.librepods.presentation.viewmodel.AppleViewModel import me.kavishdevar.librepods.presentation.viewmodel.AppleViewModel
import kotlin.experimental.and import kotlin.experimental.and
import kotlin.io.encoding.ExperimentalEncodingApi import kotlin.io.encoding.ExperimentalEncodingApi
@@ -78,6 +78,9 @@ fun LongPress(
val scrollState = rememberScrollState() val scrollState = rememberScrollState()
// ik enum would take like 1 minute to implement, idc
val side = if (name == stringResource(R.string.left)) "left" else "right"
StyledScaffold( StyledScaffold(
title = name, title = name,
navigateBack = navigateBack navigateBack = navigateBack
@@ -98,7 +101,7 @@ fun LongPress(
selected = longPressAction == StemAction.CYCLE_NOISE_CONTROL_MODES, selected = longPressAction == StemAction.CYCLE_NOISE_CONTROL_MODES,
onClick = { onClick = {
viewModel.setLongPressAction( viewModel.setLongPressAction(
name, side,
StemAction.CYCLE_NOISE_CONTROL_MODES StemAction.CYCLE_NOISE_CONTROL_MODES
) )
} }
@@ -109,7 +112,7 @@ fun LongPress(
selected = longPressAction == StemAction.DIGITAL_ASSISTANT, selected = longPressAction == StemAction.DIGITAL_ASSISTANT,
onClick = { onClick = {
viewModel.setLongPressAction( viewModel.setLongPressAction(
name, side,
StemAction.DIGITAL_ASSISTANT StemAction.DIGITAL_ASSISTANT
) )
}, },
@@ -35,17 +35,16 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalLocale import androidx.compose.ui.platform.LocalLocale
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.core.content.FileProvider import androidx.core.content.FileProvider
import me.kavishdevar.librepods.BuildConfig import me.kavishdevar.librepods.BuildConfig
import me.kavishdevar.librepods.R import me.kavishdevar.librepods.R
import me.kavishdevar.librepods.data.recording.Recording import me.kavishdevar.librepods.data.recording.Recording
import me.kavishdevar.librepods.presentation.components.StyledButton import me.kavishdevar.librepods.presentation.components.primitives.StyledButton
import me.kavishdevar.librepods.presentation.components.StyledList import me.kavishdevar.librepods.presentation.components.primitives.StyledList
import me.kavishdevar.librepods.presentation.components.StyledListItem import me.kavishdevar.librepods.presentation.components.primitives.StyledListItem
import me.kavishdevar.librepods.presentation.components.StyledListItemOrientation import me.kavishdevar.librepods.presentation.components.primitives.StyledListItemOrientation
import me.kavishdevar.librepods.presentation.components.StyledScaffold import me.kavishdevar.librepods.presentation.components.primitives.StyledScaffold
import me.kavishdevar.librepods.presentation.viewmodel.AppleUiState import me.kavishdevar.librepods.presentation.viewmodel.AppleUiState
import me.kavishdevar.librepods.presentation.viewmodel.AppleViewModel import me.kavishdevar.librepods.presentation.viewmodel.AppleViewModel
import java.time.Instant import java.time.Instant
@@ -39,8 +39,8 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
import me.kavishdevar.librepods.R import me.kavishdevar.librepods.R
import me.kavishdevar.librepods.presentation.components.StyledInputField import me.kavishdevar.librepods.presentation.components.primitives.StyledInputField
import me.kavishdevar.librepods.presentation.components.StyledScaffold import me.kavishdevar.librepods.presentation.components.primitives.StyledScaffold
import me.kavishdevar.librepods.presentation.viewmodel.AppleViewModel import me.kavishdevar.librepods.presentation.viewmodel.AppleViewModel
import kotlin.io.encoding.ExperimentalEncodingApi import kotlin.io.encoding.ExperimentalEncodingApi
@@ -60,9 +60,9 @@ import me.kavishdevar.librepods.R
import me.kavishdevar.librepods.bluetooth.att.types.TransparencySettings import me.kavishdevar.librepods.bluetooth.att.types.TransparencySettings
import me.kavishdevar.librepods.bluetooth.att.types.parseTransparencySettingsResponse import me.kavishdevar.librepods.bluetooth.att.types.parseTransparencySettingsResponse
import me.kavishdevar.librepods.bluetooth.att.types.sendTransparencySettings import me.kavishdevar.librepods.bluetooth.att.types.sendTransparencySettings
import me.kavishdevar.librepods.presentation.components.StyledScaffold import me.kavishdevar.librepods.presentation.components.primitives.StyledScaffold
import me.kavishdevar.librepods.presentation.components.StyledSlider import me.kavishdevar.librepods.presentation.components.primitives.StyledSlider
import me.kavishdevar.librepods.presentation.components.StyledToggle import me.kavishdevar.librepods.presentation.components.primitives.StyledToggle
import me.kavishdevar.librepods.presentation.icons.LocalIcons import me.kavishdevar.librepods.presentation.icons.LocalIcons
import me.kavishdevar.librepods.presentation.theme.DesignSystem import me.kavishdevar.librepods.presentation.theme.DesignSystem
import me.kavishdevar.librepods.presentation.theme.LocalDesignSystem import me.kavishdevar.librepods.presentation.theme.LocalDesignSystem
@@ -58,7 +58,7 @@ import me.kavishdevar.librepods.bluetooth.att.ATTHandle
import me.kavishdevar.librepods.bluetooth.att.types.HearingAidSettings import me.kavishdevar.librepods.bluetooth.att.types.HearingAidSettings
import me.kavishdevar.librepods.bluetooth.att.types.parseHearingAidSettingsResponse import me.kavishdevar.librepods.bluetooth.att.types.parseHearingAidSettingsResponse
import me.kavishdevar.librepods.bluetooth.att.types.sendHearingAidSettings import me.kavishdevar.librepods.bluetooth.att.types.sendHearingAidSettings
import me.kavishdevar.librepods.presentation.components.StyledScaffold import me.kavishdevar.librepods.presentation.components.primitives.StyledScaffold
import me.kavishdevar.librepods.presentation.theme.DesignSystem import me.kavishdevar.librepods.presentation.theme.DesignSystem
import me.kavishdevar.librepods.presentation.theme.LibrePodsTheme import me.kavishdevar.librepods.presentation.theme.LibrePodsTheme
import me.kavishdevar.librepods.presentation.viewmodel.AppleUiState import me.kavishdevar.librepods.presentation.viewmodel.AppleUiState
@@ -30,9 +30,9 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import me.kavishdevar.librepods.R import me.kavishdevar.librepods.R
import me.kavishdevar.librepods.presentation.components.StyledList import me.kavishdevar.librepods.presentation.components.primitives.StyledList
import me.kavishdevar.librepods.presentation.components.StyledListItem import me.kavishdevar.librepods.presentation.components.primitives.StyledListItem
import me.kavishdevar.librepods.presentation.components.StyledScaffold import me.kavishdevar.librepods.presentation.components.primitives.StyledScaffold
import me.kavishdevar.librepods.presentation.viewmodel.AppleViewModel import me.kavishdevar.librepods.presentation.viewmodel.AppleViewModel
@Composable @Composable
@@ -18,9 +18,9 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import me.kavishdevar.librepods.R import me.kavishdevar.librepods.R
import me.kavishdevar.librepods.presentation.components.AppInfoCard import me.kavishdevar.librepods.presentation.components.common.AppInfoCard
import me.kavishdevar.librepods.presentation.components.DeviceInfoCard import me.kavishdevar.librepods.presentation.components.common.DeviceInfoCard
import me.kavishdevar.librepods.presentation.components.StyledListItem import me.kavishdevar.librepods.presentation.components.primitives.StyledListItem
@Composable @Composable
fun NotSupportedPage( fun NotSupportedPage(
@@ -50,9 +50,9 @@ import com.google.accompanist.permissions.isGranted
import com.google.accompanist.permissions.rememberMultiplePermissionsState import com.google.accompanist.permissions.rememberMultiplePermissionsState
import com.google.accompanist.permissions.rememberPermissionState import com.google.accompanist.permissions.rememberPermissionState
import me.kavishdevar.librepods.R import me.kavishdevar.librepods.R
import me.kavishdevar.librepods.presentation.components.StyledList import me.kavishdevar.librepods.presentation.components.primitives.StyledList
import me.kavishdevar.librepods.presentation.components.StyledListItem import me.kavishdevar.librepods.presentation.components.primitives.StyledListItem
import me.kavishdevar.librepods.presentation.components.StyledListItemOrientation import me.kavishdevar.librepods.presentation.components.primitives.StyledListItemOrientation
import me.kavishdevar.librepods.presentation.icons.MaterialIcons import me.kavishdevar.librepods.presentation.icons.MaterialIcons
@OptIn(ExperimentalPermissionsApi::class) @OptIn(ExperimentalPermissionsApi::class)
@@ -198,5 +198,9 @@ class AppleViewModel(
_heartRateRange.value = range _heartRateRange.value = range
} }
fun updateSettings( transform : (AppleSettings) -> AppleSettings) {
device.updateSettings(transform)
}
fun sendRawPacket(data: ByteArray): Boolean = device.sendRawPacket(data) fun sendRawPacket(data: ByteArray): Boolean = device.sendRawPacket(data)
} }
@@ -1,5 +1,6 @@
package me.kavishdevar.librepods.repository package me.kavishdevar.librepods.repository
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.SupervisorJob
@@ -16,6 +17,12 @@ class AppDataRepository(
private val settingsDao: AppSettingsDao, private val settingsDao: AppSettingsDao,
private val stateDao: AppStateDao, private val stateDao: AppStateDao,
) { ) {
private val initialized = CompletableDeferred<Unit>()
suspend fun awaitInitialized() {
initialized.await()
}
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO) private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
private val _settings = MutableStateFlow(AppSettingsEntity()) private val _settings = MutableStateFlow(AppSettingsEntity())
@@ -28,6 +35,7 @@ class AppDataRepository(
scope.launch { scope.launch {
_settings.value = settingsDao.get() ?: AppSettingsEntity() _settings.value = settingsDao.get() ?: AppSettingsEntity()
_state.value = stateDao.get() ?: AppStateEntity() _state.value = stateDao.get() ?: AppStateEntity()
initialized.complete(Unit)
} }
} }
@@ -27,9 +27,14 @@ import android.os.IBinder
import android.os.ParcelUuid import android.os.ParcelUuid
import android.os.ext.SdkExtensions import android.os.ext.SdkExtensions
import android.provider.Settings import android.provider.Settings
import android.telecom.TelecomManager
import android.telecom.VideoProfile
import android.telephony.TelephonyCallback
import android.telephony.TelephonyManager
import android.util.Log import android.util.Log
import android.view.View import android.view.View
import android.widget.RemoteViews import android.widget.RemoteViews
import android.widget.Toast
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import androidx.health.connect.client.permission.HealthPermission import androidx.health.connect.client.permission.HealthPermission
import androidx.health.connect.client.records.HeartRateRecord import androidx.health.connect.client.records.HeartRateRecord
@@ -111,8 +116,53 @@ class LibrePodsService : Service() {
(application as LibrePodsApplication).healthConnectClient (application as LibrePodsApplication).healthConnectClient
} }
private val hasConnectedToAACP by lazy { private var isCallRinging = false
appDataRepository.state.value.hasConnectedToAACP
private val telephonyCallback = object: TelephonyCallback(), TelephonyCallback.CallStateListener {
override fun onCallStateChanged(state: Int) {
isCallRinging = state == TelephonyManager.CALL_STATE_RINGING
if (state == TelephonyManager.CALL_STATE_RINGING) {
_devices.value.values.firstOrNull { device ->
device is AppleDevice &&
device.connectionState.value == ConnectionState.CONNECTED &&
device.state.value.componentState.any { it.status == ComponentStatus.IN_EAR }
}?.let { device ->
(device as AppleDevice).detectHeadGestures { accept ->
if (!isCallRinging) return@detectHeadGestures
try {
val telecomManager = getSystemService(TelecomManager::class.java)
@Suppress("DEPRECATION")
if (accept) {
telecomManager?.acceptRingingCall(
VideoProfile.STATE_AUDIO_ONLY
)
Toast.makeText(
this@LibrePodsService,
getString(R.string.call_accepted),
Toast.LENGTH_SHORT
).show()
} else {
Toast.makeText(
this@LibrePodsService,
getString(R.string.call_rejected),
Toast.LENGTH_SHORT
).show()
telecomManager?.endCall()
}
isCallRinging = false
device.stopHeadTracking()
} catch (e: Exception) {
Log.e(TAG, "Error accepting call", e)
}
}
}
}
}
} }
override fun onCreate() { override fun onCreate() {
@@ -132,6 +182,14 @@ class LibrePodsService : Service() {
localMac = null // TODO: smart routing. MAC_ADDRESS message gives host mac? localMac = null // TODO: smart routing. MAC_ADDRESS message gives host mac?
) )
val telephonyManager = getSystemService(TelephonyManager::class.java)
telephonyManager.registerTelephonyCallback(
mainExecutor,
telephonyCallback
)
startForegroundNotification() startForegroundNotification()
} }
@@ -538,7 +596,6 @@ class LibrePodsService : Service() {
return device return device
} }
fun observeAppSettings(): Job { fun observeAppSettings(): Job {
var oldAppSettings: AppSettingsEntity = appDataRepository.settings.value var oldAppSettings: AppSettingsEntity = appDataRepository.settings.value
@@ -562,9 +619,12 @@ class LibrePodsService : Service() {
if (state.aacpPackets != previousState.aacpPackets) { if (state.aacpPackets != previousState.aacpPackets) {
if (!hasConnectedToAACP) { if (!appDataRepository.state.value.hasConnectedToAACP) {
appDataRepository.updateState { it.copy(hasConnectedToAACP = true) } appDataRepository.updateState { it.copy(hasConnectedToAACP = true) }
} }
if (appDataRepository.state.value.firstSuccessfulConnectionTime == null) {
appDataRepository.updateState { it.copy(firstSuccessfulConnectionTime = System.currentTimeMillis()) }
}
} }
when { when {
@@ -655,7 +715,7 @@ class LibrePodsService : Service() {
TAG, TAG,
"ear detection control command value: ${earDetectionCtrlCmdValue.toHexString()}" "ear detection control command value: ${earDetectionCtrlCmdValue.toHexString()}"
) )
val earDetectionEnabled = earDetectionCtrlCmdValue[0] == 0x01.toByte() val earDetectionEnabled = earDetectionCtrlCmdValue[0] == 0x01.toByte() || deviceSettings.earDetectionEnabled // temporary, cache broken (?)
Log.d(TAG, "ear detection enabled: $earDetectionEnabled") Log.d(TAG, "ear detection enabled: $earDetectionEnabled")
if (earDetectionEnabled) { if (earDetectionEnabled) {
processComponentStateChange( processComponentStateChange(
@@ -695,10 +755,29 @@ class LibrePodsService : Service() {
"conversational awareness state: ${state.conversationalAwarenessState}" "conversational awareness state: ${state.conversationalAwarenessState}"
) )
// TODO: multi-step volume change. implementation exists in linux rewrite; copy from there
when (state.conversationalAwarenessState) { when (state.conversationalAwarenessState) {
1, 2 -> MediaController.startSpeaking() 1 -> {
6, 8, 9 -> MediaController.stopSpeaking() MediaController.startSpeaking()
MediaController.setVolume(
deviceSettings.conversationalAwarenessVolume.toInt()
)
}
2 -> {
MediaController.setVolume(
deviceSettings.conversationalAwarenessReducedVolume.toInt()
)
}
3 -> {
MediaController.setVolume(
deviceSettings.conversationalAwarenessVolume.toInt()
)
}
6, 7, 8, 9 -> {
MediaController.stopSpeaking()
}
} }
} }
@@ -311,38 +311,74 @@ object MediaController {
} }
} }
@Synchronized // @Synchronized
fun startSpeaking() { // fun startSpeaking() {
Log.d("MediaController", "Starting speaking max vol: ${audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC)}, current vol: ${audioManager.getStreamVolume(AudioManager.STREAM_MUSIC)}, conversationalAwarenessVolume: $conversationalAwarenessVolume, relativeVolume: $relativeVolume") // Log.d("MediaController", "Starting speaking max vol: ${audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC)}, current vol: ${audioManager.getStreamVolume(AudioManager.STREAM_MUSIC)}, conversationalAwarenessVolume: $conversationalAwarenessVolume, relativeVolume: $relativeVolume")
//
// if (initialVolume == null) {
// initialVolume = audioManager.getStreamVolume(AudioManager.STREAM_MUSIC)
// Log.d("MediaController", "Initial Volume: $initialVolume")
// val targetVolume = if (relativeVolume) {
// (initialVolume!! * conversationalAwarenessVolume / 100)
// } else if (initialVolume!! > (audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC) * conversationalAwarenessVolume / 100)) {
// (audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC) * conversationalAwarenessVolume / 100)
// } else {
// initialVolume!!
// }
// smoothVolumeTransition(initialVolume!!, targetVolume)
// if (conversationalAwarenessPauseMusic) {
// sendPause(force = true)
// }
// }
// Log.d("MediaController", "Initial Volume: $initialVolume")
// }
if (initialVolume == null) { // @Synchronized
initialVolume = audioManager.getStreamVolume(AudioManager.STREAM_MUSIC) // fun stopSpeaking() {
Log.d("MediaController", "Initial Volume: $initialVolume") // Log.d("MediaController", "Stopping speaking, initialVolume: $initialVolume")
val targetVolume = if (relativeVolume) { // if (initialVolume != null) {
(initialVolume!! * conversationalAwarenessVolume / 100) // smoothVolumeTransition(audioManager.getStreamVolume(AudioManager.STREAM_MUSIC), initialVolume!!)
} else if (initialVolume!! > (audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC) * conversationalAwarenessVolume / 100)) { // if (conversationalAwarenessPauseMusic) {
(audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC) * conversationalAwarenessVolume / 100) // sendPlay()
} else { // }
initialVolume!! // initialVolume = null
} // }
smoothVolumeTransition(initialVolume!!, targetVolume) // }
if (conversationalAwarenessPauseMusic) {
sendPause(force = true) @Synchronized
} fun setVolume(volumePercent: Int) {
} val maxVolume = audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC)
Log.d("MediaController", "Initial Volume: $initialVolume") val targetVolume = maxVolume * volumePercent / 100
smoothVolumeTransition(
audioManager.getStreamVolume(AudioManager.STREAM_MUSIC),
targetVolume,
)
} }
@Synchronized @Synchronized
fun stopSpeaking() { fun startSpeaking() {
Log.d("MediaController", "Stopping speaking, initialVolume: $initialVolume") if (initialVolume == null) {
if (initialVolume != null) { initialVolume = audioManager.getStreamVolume(AudioManager.STREAM_MUSIC)
smoothVolumeTransition(audioManager.getStreamVolume(AudioManager.STREAM_MUSIC), initialVolume!!)
if (conversationalAwarenessPauseMusic) {
sendPlay()
}
initialVolume = null
} }
Log.d(
"MediaController",
"Starting speaking, initial volume: $initialVolume",
)
}
@Synchronized
fun stopSpeaking() {
val original = initialVolume ?: return
smoothVolumeTransition(
audioManager.getStreamVolume(AudioManager.STREAM_MUSIC),
original,
)
initialVolume = null
} }
private fun smoothVolumeTransition(fromVolume: Int, toVolume: Int) { private fun smoothVolumeTransition(fromVolume: Int, toVolume: Int) {
+7 -1
View File
@@ -150,7 +150,7 @@
<string name="open_dialog_for_controlling">Open dialog for controlling</string> <string name="open_dialog_for_controlling">Open dialog for controlling</string>
<string name="open_dialog_for_controlling_description">If disabled, clicking on the QS will cycle through modes. If enabled, it will show a dialog for controlling noise control mode and conversational awareness</string> <string name="open_dialog_for_controlling_description">If disabled, clicking on the QS will cycle through modes. If enabled, it will show a dialog for controlling noise control mode and conversational awareness</string>
<string name="disconnect_when_not_wearing">Disconnect AirPods when not wearing</string> <string name="disconnect_when_not_wearing">Disconnect AirPods when not wearing</string>
<string name="disconnect_when_not_wearing_description">You will still be able to control them with the app - this just disconnects the audio.</string> <string name="disconnect_when_not_wearing_description">You will still be able to control them with the app; this just disconnects the audio.</string>
<string name="advanced_options">Advanced Options</string> <string name="advanced_options">Advanced Options</string>
<string name="set_identity_resolving_key">Set Identity Resolving Key (IRK)</string> <string name="set_identity_resolving_key">Set Identity Resolving Key (IRK)</string>
<string name="set_identity_resolving_key_description">Manually set the IRK value used for resolving BLE random addresses</string> <string name="set_identity_resolving_key_description">Manually set the IRK value used for resolving BLE random addresses</string>
@@ -340,4 +340,10 @@
<string name="use_highest_refresh_rate">Use highest refresh rate available</string> <string name="use_highest_refresh_rate">Use highest refresh rate available</string>
<string name="debug_mode_description">Shows extra information and settings.</string> <string name="debug_mode_description">Shows extra information and settings.</string>
<string name="interaction">Interaction</string> <string name="interaction">Interaction</string>
<string name="heart_rate_monitoring">Heart Rate Monitoring</string>
<string name="hrm_alert_description">Sends a notification when your heart rate is above the threshold. Consumes more battery.</string>
<string name="heart_rate_alert">Heart rate alert</string>
<string name="heart_rate_alert_threshold">Heart rate threshold</string>
<string name="call_rejected">Call rejected</string>
<string name="call_accepted">Call accepted</string>
</resources> </resources>