diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index 14814371..78e82816 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -77,7 +77,7 @@ android { defaultConfig { applicationId = "me.kavishdevar.librepods" targetSdk = 37 - versionCode = 65 + versionCode = 80 versionName = appVersionName } buildTypes { diff --git a/android/app/schemas/me.kavishdevar.librepods.database.LibrePodsDatabase/1.json b/android/app/schemas/me.kavishdevar.librepods.database.LibrePodsDatabase/1.json index e22a1ab7..3aca3dc3 100644 --- a/android/app/schemas/me.kavishdevar.librepods.database.LibrePodsDatabase/1.json +++ b/android/app/schemas/me.kavishdevar.librepods.database.LibrePodsDatabase/1.json @@ -2,7 +2,7 @@ "formatVersion": 1, "database": { "version": 1, - "identityHash": "7a502eac34ab57e1c12ba8f849e172cf", + "identityHash": "c547604d916583c3b94c7bf1ac787fdf", "entities": [ { "tableName": "AppleEntity", @@ -102,7 +102,7 @@ }, { "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": [ { "fieldPath": "id", @@ -110,6 +110,12 @@ "affinity": "INTEGER", "notNull": true }, + { + "fieldPath": "isPremium", + "columnName": "isPremium", + "affinity": "INTEGER", + "notNull": true + }, { "fieldPath": "hasCompletedOnboarding", "columnName": "hasCompletedOnboarding", @@ -209,7 +215,7 @@ ], "setupQueries": [ "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')" ] } } \ No newline at end of file diff --git a/android/app/src/main/kotlin/me/kavishdevar/librepods/LibrePodsApplication.kt b/android/app/src/main/kotlin/me/kavishdevar/librepods/LibrePodsApplication.kt index b5000707..e285c725 100644 --- a/android/app/src/main/kotlin/me/kavishdevar/librepods/LibrePodsApplication.kt +++ b/android/app/src/main/kotlin/me/kavishdevar/librepods/LibrePodsApplication.kt @@ -8,6 +8,8 @@ import androidx.lifecycle.ProcessLifecycleOwner import androidx.room3.Room import io.github.libxposed.service.XposedService 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.BillingProviderFactory import me.kavishdevar.librepods.database.LibrePodsDatabase @@ -50,6 +52,11 @@ class LibrePodsApplication: Application(), XposedServiceHelper.OnServiceListener ).build() XposedServiceHelper.registerListener(this) + + runBlocking(Dispatchers.IO) { + appDataRepository.awaitInitialized() + } + BillingManager.provider = BillingProviderFactory.create(this) ProcessLifecycleOwner.get().lifecycle.addObserver(this) diff --git a/android/app/src/main/kotlin/me/kavishdevar/librepods/billing/FOSSBillingProvider.kt b/android/app/src/main/kotlin/me/kavishdevar/librepods/billing/FOSSBillingProvider.kt index 4f07db66..e8c64a83 100644 --- a/android/app/src/main/kotlin/me/kavishdevar/librepods/billing/FOSSBillingProvider.kt +++ b/android/app/src/main/kotlin/me/kavishdevar/librepods/billing/FOSSBillingProvider.kt @@ -21,7 +21,6 @@ package me.kavishdevar.librepods.billing import android.app.Activity import android.content.Context import android.content.Intent -import androidx.core.content.edit import androidx.core.net.toUri import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -31,17 +30,19 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch +import me.kavishdevar.librepods.LibrePodsApplication import me.kavishdevar.librepods.R +import kotlin.time.Duration.Companion.seconds 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 = _isPremium private val _price = MutableStateFlow(context.getString(R.string.name_your_own_price)) override val price: StateFlow = _price - private val sharedPreferences = context.getSharedPreferences("settings", Context.MODE_PRIVATE) - private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO) private var purchaseJob: Job? = null @@ -57,21 +58,18 @@ class FOSSBillingProvider(context: Context): BillingProvider { purchaseJob?.cancel() purchaseJob = scope.launch { - delay(5_000) + delay(5.seconds) _isPremium.value = true - sharedPreferences.edit { putBoolean("foss_upgraded", true) } + appDataRepository.updateState { it.copy(isPremium = true) } } } override fun queryPurchases() { - val stored = sharedPreferences.getBoolean("foss_upgraded", false) - if (stored != _isPremium.value) { - _isPremium.value = stored - } + _isPremium.value = appDataRepository.state.value.isPremium } override fun restorePurchases() { _isPremium.value = true - sharedPreferences.edit { putBoolean("foss_upgraded", true) } + appDataRepository.updateState { it.copy(isPremium = true) } } } diff --git a/android/app/src/main/kotlin/me/kavishdevar/librepods/billing/PlayBillingProvider.kt b/android/app/src/main/kotlin/me/kavishdevar/librepods/billing/PlayBillingProvider.kt index 02f17b95..962bd191 100644 --- a/android/app/src/main/kotlin/me/kavishdevar/librepods/billing/PlayBillingProvider.kt +++ b/android/app/src/main/kotlin/me/kavishdevar/librepods/billing/PlayBillingProvider.kt @@ -41,6 +41,7 @@ import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch +import me.kavishdevar.librepods.LibrePodsApplication const val TAG = "PlayBillingProvider" @@ -49,10 +50,11 @@ private const val PREMIUM_PRODUCT_ID = "librepods.advanced_features.v2" class PlayBillingProvider( context: Context ) : BillingProvider, PurchasesUpdatedListener { + private val appDataRepository = (context.applicationContext as LibrePodsApplication).appDataRepository private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO) - private val _isPremium = MutableStateFlow(false) + private val _isPremium = MutableStateFlow(appDataRepository.state.value.isPremium) override val isPremium: StateFlow = _isPremium private val _price = MutableStateFlow("unknown") @@ -177,6 +179,8 @@ class PlayBillingProvider( _isPremium.value = hasPremium + appDataRepository.updateState { it.copy(isPremium = hasPremium) } + scope.launch { purchases .filter { it.purchaseState == Purchase.PurchaseState.PURCHASED && !it.isAcknowledged } diff --git a/android/app/src/main/kotlin/me/kavishdevar/librepods/bluetooth/aacp/AACPManager.kt b/android/app/src/main/kotlin/me/kavishdevar/librepods/bluetooth/aacp/AACPManager.kt index 2d3322eb..1600007e 100644 --- a/android/app/src/main/kotlin/me/kavishdevar/librepods/bluetooth/aacp/AACPManager.kt +++ b/android/app/src/main/kotlin/me/kavishdevar/librepods/bluetooth/aacp/AACPManager.kt @@ -637,8 +637,9 @@ class AACPManager(private val device: AppleDevice) { return sendPacket(packet) } + // todo: implement adaptive volume 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( opcode = MessageOpcode.SOURCE_FEATURE_CAPABILITIES, payload diff --git a/android/app/src/main/kotlin/me/kavishdevar/librepods/database/app/AppStateEntiy.kt b/android/app/src/main/kotlin/me/kavishdevar/librepods/database/app/AppStateEntiy.kt index 96de2635..4bae5bb4 100644 --- a/android/app/src/main/kotlin/me/kavishdevar/librepods/database/app/AppStateEntiy.kt +++ b/android/app/src/main/kotlin/me/kavishdevar/librepods/database/app/AppStateEntiy.kt @@ -8,6 +8,8 @@ data class AppStateEntity( @PrimaryKey val id: Int = 0, + val isPremium: Boolean = false, + val hasCompletedOnboarding: Boolean = false, val lastVersionShown: String? = null, diff --git a/android/app/src/main/kotlin/me/kavishdevar/librepods/devices/AppleDevice.kt b/android/app/src/main/kotlin/me/kavishdevar/librepods/devices/AppleDevice.kt index 5267dc63..af96deac 100644 --- a/android/app/src/main/kotlin/me/kavishdevar/librepods/devices/AppleDevice.kt +++ b/android/app/src/main/kotlin/me/kavishdevar/librepods/devices/AppleDevice.kt @@ -75,6 +75,12 @@ class AppleDevice( _metadata.update(transform) } + internal inline fun updateSettings( + transform: (AppleSettings) -> AppleSettings + ) { + _settings.update(transform) + } + internal suspend fun emitEvent(event: AppleEvent) { _events.emit(event) } @@ -197,12 +203,9 @@ class AppleDevice( currentByte or modeBit } setControlCommand(ControlCommandIdentifier.LISTENING_MODE_CONFIGS, newValue) -// sharedPreferences.edit { putInt("long_press_byte", newValue) } } 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 { if (side.lowercase() == "left") it.copy(leftLongPressAction = action) else it.copy(rightLongPressAction = action) } diff --git a/android/app/src/main/kotlin/me/kavishdevar/librepods/devices/AppleSettings.kt b/android/app/src/main/kotlin/me/kavishdevar/librepods/devices/AppleSettings.kt index a1893466..cf2a16fc 100644 --- a/android/app/src/main/kotlin/me/kavishdevar/librepods/devices/AppleSettings.kt +++ b/android/app/src/main/kotlin/me/kavishdevar/librepods/devices/AppleSettings.kt @@ -9,6 +9,8 @@ import kotlin.time.Duration.Companion.milliseconds data class AppleSettings( val disconnectWhenNotWearing: Boolean = true, // disconnect_when_not_wearing + val earDetectionEnabled: Boolean = true, + val cacheDisconnectedComponentBattery: Boolean = true, val headGesturesEnabled: Boolean = true, // head_gestures_enabled @@ -33,6 +35,9 @@ data class AppleSettings( val conversationalAwarenessPauseMusicEnabled: Boolean = false, // conversational_awareness_pause_music 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 diff --git a/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/activities/MainActivity.kt b/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/activities/MainActivity.kt index 9d5d8ac3..942556b2 100644 --- a/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/activities/MainActivity.kt +++ b/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/activities/MainActivity.kt @@ -25,7 +25,6 @@ import android.app.Activity import android.content.BroadcastReceiver import android.content.ComponentName import android.content.Context -import android.content.Context.MODE_PRIVATE import android.content.Intent import android.content.ServiceConnection import android.hardware.display.DisplayManager @@ -46,7 +45,6 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalView -import androidx.core.content.edit import androidx.core.view.WindowCompat import com.google.android.play.core.review.ReviewManagerFactory 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.theme.LibrePodsTheme import me.kavishdevar.librepods.presentation.theme.NightTheme +import me.kavishdevar.librepods.repository.AppDataRepository import me.kavishdevar.librepods.services.LibrePodsService import me.kavishdevar.librepods.utils.XposedState import kotlin.io.encoding.ExperimentalEncodingApi +import kotlin.time.Duration.Companion.hours private lateinit var serviceConnection: ServiceConnection private lateinit var connectionStatusReceiver: BroadcastReceiver @@ -128,13 +128,13 @@ class MainActivity : ComponentActivity() { } override fun onDestroy() { - try { + if (::serviceConnection.isInitialized) try { unbindService(serviceConnection) Log.d("MainActivity", "Unbound service") } catch (e: Exception) { Log.e("MainActivity", "Error while unbinding service: $e") } - try { + if (::connectionStatusReceiver.isInitialized) try { unregisterReceiver(connectionStatusReceiver) Log.d("MainActivity", "Unregistered receiver") } catch (e: Exception) { @@ -142,41 +142,24 @@ class MainActivity : ComponentActivity() { } 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 fun Main() { val context = LocalContext.current - val sharedPreferences = context.getSharedPreferences("settings", MODE_PRIVATE) - val librepodsService = remember { mutableStateOf(null) } + val appDataRepository: AppDataRepository = (LocalContext.current.applicationContext as LibrePodsApplication).appDataRepository + val appState by appDataRepository.state.collectAsState() + LaunchedEffect(Unit) { if (BuildConfig.PLAY_BUILD) { val now = System.currentTimeMillis() - val firstConn = - sharedPreferences.getLong("first_connection_successful_time", 0L) + val firstConn = appState.firstSuccessfulConnectionTime?: 0L - val alreadyPrompted = - sharedPreferences.getBoolean("review_prompted", false) + val alreadyPrompted = appState.reviewPrompted - val oneDay = 24 * 60 * 60 * 1000L + val oneDay = 24.hours.inWholeMilliseconds if ( firstConn != 0L && @@ -185,17 +168,19 @@ fun Main() { ) { triggerReviewFlow(context as? Activity ?: return@LaunchedEffect) - sharedPreferences.edit { - putBoolean("review_prompted", true) + appDataRepository.updateState { + 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 releaseNotesShown = sharedPreferences.getBoolean(releaseNotesShownPrefKey, false) + val currentVersion = BuildConfig.VERSION_NAME.removeSuffix("-debug").removeSuffix("-play") + val lastVersionShown = appState.lastVersionShown + + val releaseNotesShown = lastVersionShown == currentVersion val devicesState = remember(librepodsService.value) { librepodsService.value?.devices ?: MutableStateFlow(emptyMap()) @@ -241,12 +226,6 @@ fun Main() { val binder = service as LibrePodsService.LocalBinder val service = binder.getService() librepodsService.value = service - - if (!sharedPreferences.contains("first_connection_successful_time")) { - sharedPreferences.edit { - putLong("first_connection_successful_time", System.currentTimeMillis()) - } - } } override fun onServiceDisconnected(name: ComponentName?) { @@ -263,10 +242,16 @@ fun Main() { NavigationRoot( showReleaseNotes = !releaseNotesShown, - updatesShown = { sharedPreferences.edit { putBoolean(releaseNotesShownPrefKey, true) } }, + updatesShown = { + appDataRepository.updateState { + it.copy(lastVersionShown = currentVersion) + } + }, showOnboarding = !onboardingComplete, onboardingComplete = { - sharedPreferences.edit { putBoolean("onboarding_complete", true) } + appDataRepository.updateState { + it.copy(hasCompletedOnboarding = true) + } bindService() }, devicesState = devicesState diff --git a/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/activities/NoiseControlWidgetConfigurationActivity.kt b/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/activities/NoiseControlWidgetConfigurationActivity.kt index d1af77a6..e6d9a1ce 100644 --- a/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/activities/NoiseControlWidgetConfigurationActivity.kt +++ b/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/activities/NoiseControlWidgetConfigurationActivity.kt @@ -37,10 +37,10 @@ import me.kavishdevar.librepods.R import me.kavishdevar.librepods.bluetooth.MacAddress import me.kavishdevar.librepods.database.widget.WidgetConfigEntity import me.kavishdevar.librepods.devices.Device -import me.kavishdevar.librepods.presentation.components.StyledButton -import me.kavishdevar.librepods.presentation.components.StyledList -import me.kavishdevar.librepods.presentation.components.StyledListItem -import me.kavishdevar.librepods.presentation.components.StyledScaffold +import me.kavishdevar.librepods.presentation.components.primitives.StyledButton +import me.kavishdevar.librepods.presentation.components.primitives.StyledList +import me.kavishdevar.librepods.presentation.components.primitives.StyledListItem +import me.kavishdevar.librepods.presentation.components.primitives.StyledScaffold import me.kavishdevar.librepods.presentation.icons.LocalIcons import me.kavishdevar.librepods.presentation.theme.LibrePodsTheme import me.kavishdevar.librepods.presentation.theme.NightTheme diff --git a/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/activities/PrivacyPolicyActivity.kt b/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/activities/PrivacyPolicyActivity.kt index 7ec7b838..b55bb364 100644 --- a/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/activities/PrivacyPolicyActivity.kt +++ b/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/activities/PrivacyPolicyActivity.kt @@ -25,7 +25,7 @@ import androidx.compose.ui.unit.dp import androidx.core.view.WindowCompat import me.kavishdevar.librepods.LibrePodsApplication 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.theme.LibrePodsTheme import me.kavishdevar.librepods.presentation.theme.NightTheme diff --git a/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/components/ConnectionSettings.kt b/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/components/ConnectionSettings.kt deleted file mode 100644 index 72f36952..00000000 --- a/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/components/ConnectionSettings.kt +++ /dev/null @@ -1,49 +0,0 @@ -/* - LibrePods - AirPods liberated from Apple’s 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 . -*/ - -@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 - ) - } -} diff --git a/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/components/ControlCenterButton.kt b/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/components/ControlCenterButton.kt deleted file mode 100644 index 5340b0ca..00000000 --- a/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/components/ControlCenterButton.kt +++ /dev/null @@ -1,106 +0,0 @@ -/* - LibrePods - AirPods liberated from Apple’s 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 . -*/ - -@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 - ) - } -} diff --git a/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/components/ControlCenterNoiseControlSegmentedButton.kt b/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/components/ControlCenterNoiseControlSegmentedButton.kt deleted file mode 100644 index e1042fdb..00000000 --- a/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/components/ControlCenterNoiseControlSegmentedButton.kt +++ /dev/null @@ -1,242 +0,0 @@ -/* - LibrePods - AirPods liberated from Apple’s 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 . -*/ - -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, - 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" - } -} - diff --git a/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/components/DeviceInfoCard.kt b/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/components/DeviceInfoCard.kt deleted file mode 100644 index 70cd33b2..00000000 --- a/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/components/DeviceInfoCard.kt +++ /dev/null @@ -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 - ) - } -} diff --git a/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/components/VerticalVolumeSlider.kt b/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/components/VerticalVolumeSlider.kt deleted file mode 100644 index 37bddd8e..00000000 --- a/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/components/VerticalVolumeSlider.kt +++ /dev/null @@ -1,190 +0,0 @@ -/* - LibrePods - AirPods liberated from Apple’s 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 . -*/ - -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) - ) - } - } -} diff --git a/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/components/AboutCard.kt b/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/components/apple/AboutCard.kt similarity index 92% rename from android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/components/AboutCard.kt rename to android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/components/apple/AboutCard.kt index 4fc9de3e..d5bb44b9 100644 --- a/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/components/AboutCard.kt +++ b/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/components/apple/AboutCard.kt @@ -18,7 +18,7 @@ @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.Text @@ -28,6 +28,8 @@ import androidx.compose.runtime.remember import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview 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.theme.DesignSystem import me.kavishdevar.librepods.presentation.theme.LibrePodsTheme @@ -50,7 +52,7 @@ fun AboutCard( val serialNumber = remember { mutableIntStateOf(0) } - StyledList (title = stringResource(R.string.about)) { + StyledList(title = stringResource(R.string.about)) { StyledListItem( contentText = stringResource(R.string.model_name), supportingText = modelName @@ -61,7 +63,7 @@ fun AboutCard( supportingText = actualModel ) - StyledListItem ( + StyledListItem( contentText = stringResource(R.string.serial_number), supportingContent = { Text( diff --git a/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/components/AudioSettings.kt b/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/components/apple/AudioSettings.kt similarity index 88% rename from android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/components/AudioSettings.kt rename to android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/components/apple/AudioSettings.kt index e70708e0..b075c944 100644 --- a/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/components/AudioSettings.kt +++ b/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/components/apple/AudioSettings.kt @@ -18,11 +18,14 @@ @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.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.presentation.components.primitives.StyledToggle import kotlin.io.encoding.ExperimentalEncodingApi @Composable @@ -30,7 +33,6 @@ fun AudioSettings( adaptiveVolumeCapability: Boolean, conversationalAwarenessCapability: Boolean, loudSoundReductionCapability: Boolean, - adaptiveAudioCapability: Boolean, customEqCapability: Boolean, adaptiveVolumeChecked: Boolean, @@ -42,13 +44,12 @@ fun AudioSettings( loudSoundReductionChecked: Boolean, onLoudSoundReductionCheckedChange: (Boolean) -> Unit, - navigateToAdaptiveStrength: () -> Unit, navigateToEqualizer: () -> Unit, vendorIdHook: Boolean, isPremium: Boolean ) { - if (adaptiveVolumeCapability || conversationalAwarenessCapability || loudSoundReductionCapability || adaptiveAudioCapability) { + if (adaptiveVolumeCapability || conversationalAwarenessCapability || loudSoundReductionCapability) { StyledList(title = stringResource(R.string.audio)) { if (adaptiveVolumeCapability) { StyledToggle( @@ -80,13 +81,6 @@ fun AudioSettings( ) } - if (adaptiveAudioCapability) { - StyledListItem( - contentText = stringResource(R.string.adaptive_audio), - onClick = navigateToAdaptiveStrength, - ) - } - if (customEqCapability) { StyledListItem( contentText = stringResource(R.string.equalizer), diff --git a/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/components/BatteryIndicator.kt b/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/components/apple/BatteryIndicator.kt similarity index 99% rename from android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/components/BatteryIndicator.kt rename to android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/components/apple/BatteryIndicator.kt index 2e7efc32..eeb9fb2a 100644 --- a/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/components/BatteryIndicator.kt +++ b/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/components/apple/BatteryIndicator.kt @@ -16,7 +16,7 @@ along with this program. If not, see . */ -package me.kavishdevar.librepods.presentation.components +package me.kavishdevar.librepods.presentation.components.apple import android.content.res.Configuration diff --git a/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/components/BatteryView.kt b/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/components/apple/BatteryView.kt similarity index 99% rename from android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/components/BatteryView.kt rename to android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/components/apple/BatteryView.kt index 5d6d62ad..d57b1475 100644 --- a/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/components/BatteryView.kt +++ b/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/components/apple/BatteryView.kt @@ -18,7 +18,7 @@ @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.background diff --git a/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/components/CallControlSettings.kt b/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/components/apple/CallControlSettings.kt similarity index 84% rename from android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/components/CallControlSettings.kt rename to android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/components/apple/CallControlSettings.kt index 40278d24..eba3513a 100644 --- a/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/components/CallControlSettings.kt +++ b/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/components/apple/CallControlSettings.kt @@ -18,7 +18,7 @@ @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.getValue @@ -28,6 +28,8 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.res.stringResource import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi 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 @ExperimentalHazeMaterialsApi @@ -47,20 +49,20 @@ fun CallControlSettings( StyledList(title = stringResource(R.string.call_controls)) { StyledListItem( - contentText = stringResource(R.string.answer_call), - supportingText = stringResource(R.string.press_once), + contentText = stringResource(R.string.answer_call), + supportingText = stringResource(R.string.press_once), enabled = false ) StyledListItem( - contentText = muteUnmuteText, - supportingText = singlePressAction, - onClick = { navigateToCallControlScreen(muteUnmuteText) } , + contentText = muteUnmuteText, + supportingText = singlePressAction, + onClick = { navigateToCallControlScreen(muteUnmuteText) }, ) StyledListItem( - contentText = hangUpText, - supportingText = doublePressAction, + contentText = hangUpText, + supportingText = doublePressAction, onClick = { navigateToCallControlScreen(hangUpText) } ) diff --git a/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/components/apple/ConnectionSettings.kt b/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/components/apple/ConnectionSettings.kt new file mode 100644 index 00000000..a96ebfdc --- /dev/null +++ b/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/components/apple/ConnectionSettings.kt @@ -0,0 +1,125 @@ +/* + LibrePods - AirPods liberated from Apple’s 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 . +*/ + +@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 +// ) +// } +} diff --git a/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/components/HearingHealthSettings.kt b/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/components/apple/HearingHealthSettings.kt similarity index 84% rename from android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/components/HearingHealthSettings.kt rename to android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/components/apple/HearingHealthSettings.kt index 4288f6a6..9a1fe47b 100644 --- a/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/components/HearingHealthSettings.kt +++ b/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/components/apple/HearingHealthSettings.kt @@ -18,11 +18,13 @@ @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.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 kotlin.io.encoding.ExperimentalEncodingApi @Composable @@ -38,12 +40,12 @@ fun HearingHealthSettings( if (hasPPECapability && shouldShowHearingAid) { StyledList(title = stringResource(R.string.hearing_health)) { StyledListItem( - contentText = stringResource(R.string.hearing_protection), + contentText = stringResource(R.string.hearing_protection), onClick = navigateToHearingProtection ) StyledListItem( - contentText = stringResource(R.string.hearing_aid), + contentText = stringResource(R.string.hearing_aid), onClick = navigateToHearingAid ) } diff --git a/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/components/NoiseControlButton.kt b/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/components/apple/NoiseControlButton.kt similarity index 97% rename from android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/components/NoiseControlButton.kt rename to android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/components/apple/NoiseControlButton.kt index 97b4724f..3a931d3a 100644 --- a/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/components/NoiseControlButton.kt +++ b/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/components/apple/NoiseControlButton.kt @@ -16,7 +16,7 @@ along with this program. If not, see . */ -package me.kavishdevar.librepods.presentation.components +package me.kavishdevar.librepods.presentation.components.apple import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource diff --git a/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/components/NoiseControlSettings.kt b/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/components/apple/NoiseControlSettings.kt similarity index 99% rename from android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/components/NoiseControlSettings.kt rename to android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/components/apple/NoiseControlSettings.kt index bc965791..865e6aad 100644 --- a/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/components/NoiseControlSettings.kt +++ b/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/components/apple/NoiseControlSettings.kt @@ -18,7 +18,7 @@ @file:OptIn(ExperimentalEncodingApi::class) -package me.kavishdevar.librepods.presentation.components +package me.kavishdevar.librepods.presentation.components.apple import android.annotation.SuppressLint 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.Column import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth @@ -174,6 +175,7 @@ fun NoiseControlSettings( } if (showLabels) { + Spacer(modifier = Modifier.height(2.dp)) Text( text = stringResource(labelRes), style = MaterialTheme.typography.labelSmall, @@ -181,6 +183,7 @@ fun NoiseControlSettings( maxLines = 2, modifier = Modifier.fillMaxWidth() ) + Spacer(modifier = Modifier.height(8.dp)) } } } diff --git a/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/components/PressAndHoldSettings.kt b/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/components/apple/PressAndHoldSettings.kt similarity index 84% rename from android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/components/PressAndHoldSettings.kt rename to android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/components/apple/PressAndHoldSettings.kt index 28f60df9..ce739072 100644 --- a/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/components/PressAndHoldSettings.kt +++ b/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/components/apple/PressAndHoldSettings.kt @@ -16,12 +16,14 @@ along with this program. If not, see . */ -package me.kavishdevar.librepods.presentation.components +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.data.StemAction +import me.kavishdevar.librepods.presentation.components.primitives.StyledList +import me.kavishdevar.librepods.presentation.components.primitives.StyledListItem @Composable fun PressAndHoldSettings( @@ -46,12 +48,12 @@ fun PressAndHoldSettings( title = stringResource(R.string.press_and_hold_airpods) ) { StyledListItem( - contentText = stringResource(R.string.left), + contentText = stringResource(R.string.left), supportingText = leftActionText, onClick = navigateToLeftLongPress ) StyledListItem( - contentText = stringResource(R.string.right), + contentText = stringResource(R.string.right), supportingText = rightActionText, onClick = navigateToRightLongPress, ) diff --git a/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/components/AppInfoCard.kt b/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/components/common/AppInfoCard.kt similarity index 64% rename from android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/components/AppInfoCard.kt rename to android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/components/common/AppInfoCard.kt index 23ee690d..05aca73a 100644 --- a/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/components/AppInfoCard.kt +++ b/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/components/common/AppInfoCard.kt @@ -16,12 +16,14 @@ along with this program. If not, see . */ -package me.kavishdevar.librepods.presentation.components +package me.kavishdevar.librepods.presentation.components.common import androidx.compose.runtime.Composable import androidx.compose.ui.res.stringResource import me.kavishdevar.librepods.BuildConfig import me.kavishdevar.librepods.R +import me.kavishdevar.librepods.presentation.components.primitives.StyledList +import me.kavishdevar.librepods.presentation.components.primitives.StyledListItem @Composable fun AppInfoCard( @@ -29,24 +31,24 @@ fun AppInfoCard( ) { StyledList(title = stringResource(R.string.about)) { StyledListItem( - contentText = stringResource(R.string.version), - supportingText = BuildConfig.VERSION_NAME, + contentText = stringResource(R.string.version), + supportingText = BuildConfig.VERSION_NAME, onClick = navigateToReleaseNotesScreen ) StyledListItem( - contentText = stringResource(R.string.version_code), - supportingText = BuildConfig.VERSION_CODE.toString(), + contentText = stringResource(R.string.version_code), + supportingText = BuildConfig.VERSION_CODE.toString(), ) StyledListItem( - contentText = stringResource(R.string.flavor), - supportingText = BuildConfig.FLAVOR, + contentText = stringResource(R.string.flavor), + supportingText = BuildConfig.FLAVOR, ) StyledListItem( - contentText = stringResource(R.string.build_type), - supportingText = BuildConfig.BUILD_TYPE, + contentText = stringResource(R.string.build_type), + supportingText = BuildConfig.BUILD_TYPE, ) } } diff --git a/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/components/common/DeviceInfoCard.kt b/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/components/common/DeviceInfoCard.kt new file mode 100644 index 00000000..b31985fd --- /dev/null +++ b/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/components/common/DeviceInfoCard.kt @@ -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 + ) + } +} diff --git a/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/components/StyledBottomSheet.kt b/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/components/primitives/StyledBottomSheet.kt similarity index 97% rename from android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/components/StyledBottomSheet.kt rename to android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/components/primitives/StyledBottomSheet.kt index 8e46fc20..386d8044 100644 --- a/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/components/StyledBottomSheet.kt +++ b/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/components/primitives/StyledBottomSheet.kt @@ -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.foundation.layout.Box diff --git a/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/components/StyledButton.kt b/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/components/primitives/StyledButton.kt similarity index 99% rename from android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/components/StyledButton.kt rename to android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/components/primitives/StyledButton.kt index 8842316c..584f855e 100644 --- a/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/components/StyledButton.kt +++ b/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/components/primitives/StyledButton.kt @@ -16,7 +16,7 @@ along with this program. If not, see . */ -package me.kavishdevar.librepods.presentation.components +package me.kavishdevar.librepods.presentation.components.primitives import android.graphics.RuntimeShader import android.os.Build diff --git a/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/components/ConfirmationDialog.kt b/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/components/primitives/StyledConfirmationDialog.kt similarity index 99% rename from android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/components/ConfirmationDialog.kt rename to android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/components/primitives/StyledConfirmationDialog.kt index 5def1f5d..4f3c9c95 100644 --- a/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/components/ConfirmationDialog.kt +++ b/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/components/primitives/StyledConfirmationDialog.kt @@ -16,7 +16,7 @@ along with this program. If not, see . */ -package me.kavishdevar.librepods.presentation.components +package me.kavishdevar.librepods.presentation.components.primitives import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.fadeIn @@ -62,7 +62,7 @@ import me.kavishdevar.librepods.presentation.theme.LocalDesignSystem @OptIn(ExperimentalMaterial3Api::class) @Composable -fun ConfirmationDialog( +fun StyledConfirmationDialog( showDialog: MutableState, title: String, message: String, diff --git a/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/components/StyledIconButton.kt b/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/components/primitives/StyledIconButton.kt similarity index 99% rename from android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/components/StyledIconButton.kt rename to android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/components/primitives/StyledIconButton.kt index e3a80b8c..5ca3a269 100644 --- a/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/components/StyledIconButton.kt +++ b/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/components/primitives/StyledIconButton.kt @@ -16,7 +16,7 @@ along with this program. If not, see . */ -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_YES diff --git a/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/components/StyledInputField.kt b/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/components/primitives/StyledInputField.kt similarity index 99% rename from android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/components/StyledInputField.kt rename to android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/components/primitives/StyledInputField.kt index e6d297ed..8c7bd152 100644 --- a/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/components/StyledInputField.kt +++ b/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/components/primitives/StyledInputField.kt @@ -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.animateDpAsState diff --git a/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/components/StyledList.kt b/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/components/primitives/StyledList.kt similarity index 98% rename from android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/components/StyledList.kt rename to android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/components/primitives/StyledList.kt index d3433f49..67033908 100644 --- a/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/components/StyledList.kt +++ b/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/components/primitives/StyledList.kt @@ -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.layout.Box diff --git a/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/components/StyledListItem.kt b/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/components/primitives/StyledListItem.kt similarity index 99% rename from android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/components/StyledListItem.kt rename to android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/components/primitives/StyledListItem.kt index 39b4ad65..b402653d 100644 --- a/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/components/StyledListItem.kt +++ b/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/components/primitives/StyledListItem.kt @@ -16,7 +16,7 @@ along with this program. If not, see . */ -package me.kavishdevar.librepods.presentation.components +package me.kavishdevar.librepods.presentation.components.primitives import androidx.compose.animation.animateColorAsState import androidx.compose.animation.core.animateFloatAsState diff --git a/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/components/StyledScaffold.kt b/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/components/primitives/StyledScaffold.kt similarity index 99% rename from android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/components/StyledScaffold.kt rename to android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/components/primitives/StyledScaffold.kt index 6d205f2e..10d4608c 100644 --- a/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/components/StyledScaffold.kt +++ b/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/components/primitives/StyledScaffold.kt @@ -16,7 +16,7 @@ along with this program. If not, see . */ -package me.kavishdevar.librepods.presentation.components +package me.kavishdevar.librepods.presentation.components.primitives import android.graphics.RenderEffect import android.graphics.Shader diff --git a/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/components/StyledSlider.kt b/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/components/primitives/StyledSlider.kt similarity index 99% rename from android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/components/StyledSlider.kt rename to android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/components/primitives/StyledSlider.kt index 6e7ddcbc..0c038f0f 100644 --- a/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/components/StyledSlider.kt +++ b/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/components/primitives/StyledSlider.kt @@ -16,7 +16,7 @@ along with this program. If not, see . */ -package me.kavishdevar.librepods.presentation.components +package me.kavishdevar.librepods.presentation.components.primitives import android.annotation.SuppressLint import android.content.res.Configuration diff --git a/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/components/StyledSwitch.kt b/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/components/primitives/StyledSwitch.kt similarity index 99% rename from android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/components/StyledSwitch.kt rename to android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/components/primitives/StyledSwitch.kt index 2e116962..2f2bc794 100644 --- a/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/components/StyledSwitch.kt +++ b/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/components/primitives/StyledSwitch.kt @@ -16,7 +16,7 @@ along with this program. If not, see . */ -package me.kavishdevar.librepods.presentation.components +package me.kavishdevar.librepods.presentation.components.primitives import android.content.res.Configuration import androidx.compose.animation.animateColorAsState diff --git a/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/components/StyledToggle.kt b/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/components/primitives/StyledToggle.kt similarity index 99% rename from android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/components/StyledToggle.kt rename to android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/components/primitives/StyledToggle.kt index b3e2cd92..81becdeb 100644 --- a/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/components/StyledToggle.kt +++ b/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/components/primitives/StyledToggle.kt @@ -18,7 +18,7 @@ @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.clickable diff --git a/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/navigation/RenderScreenContent.kt b/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/navigation/RenderScreenContent.kt index 3cf9a16a..a33b9a3e 100644 --- a/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/navigation/RenderScreenContent.kt +++ b/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/navigation/RenderScreenContent.kt @@ -22,7 +22,6 @@ import me.kavishdevar.librepods.presentation.screens.OpenSourceLicensesScreen import me.kavishdevar.librepods.presentation.screens.PurchaseScreen import me.kavishdevar.librepods.presentation.screens.ReleaseNotesScreen 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.CallControlScreen import me.kavishdevar.librepods.presentation.screens.apple.DebugRoute @@ -77,7 +76,7 @@ fun RenderScreenContent( updatesShown: () -> 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) val navigateBack: (() -> Unit)? = if (backStack.size > 1) { @@ -129,7 +128,6 @@ fun RenderScreenContent( navigateToLeftLongPress = { navigate(Screen.LongPress(screen.macAddress, left)) }, navigateToRightLongPress = { navigate(Screen.LongPress(screen.macAddress, right)) }, navigateToPurchase = ::navigateToPurchase, - navigateToAdaptiveStrength = { navigate(Screen.AdaptiveStrength(screen.macAddress)) }, navigateToEqualizer = { navigate(Screen.Equalizer(screen.macAddress)) }, navigateToHeadTracking = { navigate(Screen.HeadTracking(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) is Screen.UpdateHearingTest -> { diff --git a/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/navigation/Screen.kt b/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/navigation/Screen.kt index 321f9794..119dac03 100644 --- a/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/navigation/Screen.kt +++ b/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/navigation/Screen.kt @@ -55,11 +55,6 @@ sealed interface Screen: NavKey { override val macAddress: MacAddress ): DeviceScreen - @Serializable - data class AdaptiveStrength( - override val macAddress: MacAddress - ): DeviceScreen - // @Serializable // data object CameraControl: Screen diff --git a/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/screens/AppSettingsScreen.kt b/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/screens/AppSettingsScreen.kt index 56a9d698..1437a4c7 100644 --- a/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/screens/AppSettingsScreen.kt +++ b/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/screens/AppSettingsScreen.kt @@ -38,7 +38,6 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.input.TextFieldState import androidx.compose.foundation.text.input.clearText import androidx.compose.foundation.verticalScroll -import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text @@ -63,17 +62,17 @@ import com.kyant.backdrop.backdrops.layerBackdrop import com.kyant.backdrop.backdrops.rememberLayerBackdrop import me.kavishdevar.librepods.BuildConfig import me.kavishdevar.librepods.R -import me.kavishdevar.librepods.presentation.components.AppInfoCard -import me.kavishdevar.librepods.presentation.components.DeviceInfoCard -import me.kavishdevar.librepods.presentation.components.StyledBottomSheet -import me.kavishdevar.librepods.presentation.components.StyledButton -import me.kavishdevar.librepods.presentation.components.StyledIconButton -import me.kavishdevar.librepods.presentation.components.StyledInputField -import me.kavishdevar.librepods.presentation.components.StyledList -import me.kavishdevar.librepods.presentation.components.StyledListItem -import me.kavishdevar.librepods.presentation.components.StyledListItemOrientation -import me.kavishdevar.librepods.presentation.components.StyledScaffold -import me.kavishdevar.librepods.presentation.components.StyledToggle +import me.kavishdevar.librepods.presentation.components.common.AppInfoCard +import me.kavishdevar.librepods.presentation.components.common.DeviceInfoCard +import me.kavishdevar.librepods.presentation.components.primitives.StyledBottomSheet +import me.kavishdevar.librepods.presentation.components.primitives.StyledButton +import me.kavishdevar.librepods.presentation.components.primitives.StyledIconButton +import me.kavishdevar.librepods.presentation.components.primitives.StyledInputField +import me.kavishdevar.librepods.presentation.components.primitives.StyledList +import me.kavishdevar.librepods.presentation.components.primitives.StyledListItem +import me.kavishdevar.librepods.presentation.components.primitives.StyledListItemOrientation +import me.kavishdevar.librepods.presentation.components.primitives.StyledScaffold +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.NightTheme @@ -81,7 +80,6 @@ import me.kavishdevar.librepods.presentation.viewmodel.AppSettingsViewModel import me.kavishdevar.librepods.utils.XposedState import java.util.concurrent.TimeUnit -@OptIn(ExperimentalMaterial3Api::class) @Composable fun AppSettingsScreen( viewModel: AppSettingsViewModel = viewModel(), diff --git a/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/screens/BLESettingsScreen.kt b/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/screens/BLESettingsScreen.kt index b9dcc228..f845f70a 100644 --- a/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/screens/BLESettingsScreen.kt +++ b/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/screens/BLESettingsScreen.kt @@ -20,11 +20,11 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import kotlinx.coroutines.flow.debounce import me.kavishdevar.librepods.R -import me.kavishdevar.librepods.presentation.components.StyledList -import me.kavishdevar.librepods.presentation.components.StyledListItem -import me.kavishdevar.librepods.presentation.components.StyledListItemOrientation -import me.kavishdevar.librepods.presentation.components.StyledScaffold -import me.kavishdevar.librepods.presentation.components.StyledSlider +import me.kavishdevar.librepods.presentation.components.primitives.StyledList +import me.kavishdevar.librepods.presentation.components.primitives.StyledListItem +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.viewmodel.AppSettingsViewModel import kotlin.time.Duration.Companion.seconds diff --git a/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/screens/DeviceListScreen.kt b/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/screens/DeviceListScreen.kt index 1ddaca81..98cbbec9 100644 --- a/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/screens/DeviceListScreen.kt +++ b/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/screens/DeviceListScreen.kt @@ -66,12 +66,12 @@ import me.kavishdevar.librepods.devices.AppleState import me.kavishdevar.librepods.devices.BaseCapability import me.kavishdevar.librepods.devices.ConnectionState import me.kavishdevar.librepods.devices.Device -import me.kavishdevar.librepods.presentation.components.NoiseControlSettings -import me.kavishdevar.librepods.presentation.components.StyledIconButton -import me.kavishdevar.librepods.presentation.components.StyledList -import me.kavishdevar.librepods.presentation.components.StyledListItem -import me.kavishdevar.librepods.presentation.components.StyledListItemOrientation -import me.kavishdevar.librepods.presentation.components.StyledScaffold +import me.kavishdevar.librepods.presentation.components.apple.NoiseControlSettings +import me.kavishdevar.librepods.presentation.components.primitives.StyledIconButton +import me.kavishdevar.librepods.presentation.components.primitives.StyledList +import me.kavishdevar.librepods.presentation.components.primitives.StyledListItem +import me.kavishdevar.librepods.presentation.components.primitives.StyledListItemOrientation +import me.kavishdevar.librepods.presentation.components.primitives.StyledScaffold import me.kavishdevar.librepods.presentation.icons.LocalIcons import me.kavishdevar.librepods.presentation.theme.DesignSystem import me.kavishdevar.librepods.presentation.theme.LocalDesignSystem diff --git a/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/screens/OpenSourceLicensesScreen.kt b/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/screens/OpenSourceLicensesScreen.kt index 00abb461..c4f17e20 100644 --- a/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/screens/OpenSourceLicensesScreen.kt +++ b/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/screens/OpenSourceLicensesScreen.kt @@ -30,13 +30,9 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues 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.height -import androidx.compose.foundation.layout.navigationBars import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.statusBars import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.MaterialTheme 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.LibraryRow import me.kavishdevar.librepods.R -import me.kavishdevar.librepods.presentation.components.StyledScaffold -import me.kavishdevar.librepods.presentation.theme.DesignSystem -import me.kavishdevar.librepods.presentation.theme.LocalDesignSystem +import me.kavishdevar.librepods.presentation.components.primitives.StyledScaffold @Composable fun OpenSourceLicensesScreen( diff --git a/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/screens/PurchaseScreen.kt b/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/screens/PurchaseScreen.kt index 5b0a15c5..f4afcab5 100644 --- a/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/screens/PurchaseScreen.kt +++ b/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/screens/PurchaseScreen.kt @@ -21,13 +21,9 @@ package me.kavishdevar.librepods.presentation.screens import androidx.compose.foundation.background 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.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.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll 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.rememberLayerBackdrop import me.kavishdevar.librepods.R -import me.kavishdevar.librepods.presentation.components.MaterialButtonStyle -import me.kavishdevar.librepods.presentation.components.StyledButton -import me.kavishdevar.librepods.presentation.components.StyledList -import me.kavishdevar.librepods.presentation.components.StyledListItem -import me.kavishdevar.librepods.presentation.components.StyledListItemOrientation -import me.kavishdevar.librepods.presentation.components.StyledScaffold -import me.kavishdevar.librepods.presentation.theme.DesignSystem -import me.kavishdevar.librepods.presentation.theme.LocalDesignSystem +import me.kavishdevar.librepods.presentation.components.primitives.MaterialButtonStyle +import me.kavishdevar.librepods.presentation.components.primitives.StyledButton +import me.kavishdevar.librepods.presentation.components.primitives.StyledList +import me.kavishdevar.librepods.presentation.components.primitives.StyledListItem +import me.kavishdevar.librepods.presentation.components.primitives.StyledListItemOrientation +import me.kavishdevar.librepods.presentation.components.primitives.StyledScaffold import me.kavishdevar.librepods.presentation.viewmodel.PurchaseViewModel import me.kavishdevar.librepods.utils.XposedState diff --git a/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/screens/apple/AccessibilitySettingsScreen.kt b/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/screens/apple/AccessibilitySettingsScreen.kt index 486802c0..a4f582b0 100644 --- a/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/screens/apple/AccessibilitySettingsScreen.kt +++ b/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/screens/apple/AccessibilitySettingsScreen.kt @@ -21,14 +21,10 @@ 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.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll 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.devices.AirPodsSpecs import me.kavishdevar.librepods.devices.BaseCapability -import me.kavishdevar.librepods.presentation.components.StyledButton -import me.kavishdevar.librepods.presentation.components.StyledList -import me.kavishdevar.librepods.presentation.components.StyledListItem -import me.kavishdevar.librepods.presentation.components.StyledScaffold -import me.kavishdevar.librepods.presentation.components.StyledSlider -import me.kavishdevar.librepods.presentation.components.StyledToggle +import me.kavishdevar.librepods.presentation.components.primitives.StyledButton +import me.kavishdevar.librepods.presentation.components.primitives.StyledList +import me.kavishdevar.librepods.presentation.components.primitives.StyledListItem +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.theme.DesignSystem -import me.kavishdevar.librepods.presentation.theme.LocalDesignSystem import me.kavishdevar.librepods.presentation.viewmodel.AppleViewModel import kotlin.time.Duration.Companion.milliseconds diff --git a/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/screens/apple/AdaptiveStrengthScreen.kt b/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/screens/apple/AdaptiveStrengthScreen.kt deleted file mode 100644 index 62d42e28..00000000 --- a/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/screens/apple/AdaptiveStrengthScreen.kt +++ /dev/null @@ -1,126 +0,0 @@ -/* - LibrePods - AirPods liberated from Apple’s 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 . -*/ - -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)) - } - } -} diff --git a/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/screens/apple/AppleSettingsScreen.kt b/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/screens/apple/AppleSettingsScreen.kt index c621e881..6e7a5580 100644 --- a/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/screens/apple/AppleSettingsScreen.kt +++ b/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/screens/apple/AppleSettingsScreen.kt @@ -19,6 +19,11 @@ package me.kavishdevar.librepods.presentation.screens.apple 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.layout.Box 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.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.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp 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.bluetooth.att.ATTHandle import me.kavishdevar.librepods.devices.AirPodsSpecs +import me.kavishdevar.librepods.devices.AppleSettings import me.kavishdevar.librepods.devices.BaseCapability -import me.kavishdevar.librepods.presentation.components.AboutCard -import me.kavishdevar.librepods.presentation.components.AudioSettings -import me.kavishdevar.librepods.presentation.components.BatteryView -import me.kavishdevar.librepods.presentation.components.CallControlSettings -import me.kavishdevar.librepods.presentation.components.ConnectionSettings -import me.kavishdevar.librepods.presentation.components.HearingHealthSettings -import me.kavishdevar.librepods.presentation.components.NoiseControlSettings -import me.kavishdevar.librepods.presentation.components.PressAndHoldSettings -import me.kavishdevar.librepods.presentation.components.StyledButton -import me.kavishdevar.librepods.presentation.components.StyledListItem -import me.kavishdevar.librepods.presentation.components.StyledListItemOrientation -import me.kavishdevar.librepods.presentation.components.StyledScaffold -import me.kavishdevar.librepods.presentation.components.StyledToggle +import me.kavishdevar.librepods.presentation.components.apple.AboutCard +import me.kavishdevar.librepods.presentation.components.apple.AudioSettings +import me.kavishdevar.librepods.presentation.components.apple.BatteryView +import me.kavishdevar.librepods.presentation.components.apple.CallControlSettings +import me.kavishdevar.librepods.presentation.components.apple.ConnectionSettings +import me.kavishdevar.librepods.presentation.components.apple.HearingHealthSettings +import me.kavishdevar.librepods.presentation.components.apple.NoiseControlSettings +import me.kavishdevar.librepods.presentation.components.apple.PressAndHoldSettings +import me.kavishdevar.librepods.presentation.components.primitives.StyledButton +import me.kavishdevar.librepods.presentation.components.primitives.StyledListItem +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.theme.DesignSystem import me.kavishdevar.librepods.presentation.theme.LibrePodsTheme import me.kavishdevar.librepods.presentation.viewmodel.AppleUiState import me.kavishdevar.librepods.presentation.viewmodel.AppleViewModel +import kotlin.time.Duration.Companion.milliseconds @Composable fun AppleSettingsRoute( @@ -71,7 +85,6 @@ fun AppleSettingsRoute( navigateToLeftLongPress: () -> Unit, navigateToRightLongPress: () -> Unit, navigateToPurchase: () -> Unit, - navigateToAdaptiveStrength: () -> Unit, navigateToEqualizer: () -> Unit, navigateToHeadTracking: () -> Unit, navigateToAccessibility: () -> Unit, @@ -94,7 +107,8 @@ fun AppleSettingsRoute( writeATTCharacteristic = viewModel::writeATTCharacteristic, -// onAutomaticEarDetectionChanged = viewModel::setAutomaticEarDetectionEnabled, + updateSettings = viewModel::updateSettings, + // onAutomaticConnectionChanged = viewModel::setAutomaticConnectionEnabled, disconnect = viewModel::disconnect, @@ -105,7 +119,6 @@ fun AppleSettingsRoute( navigateToLeftLongPress = navigateToLeftLongPress, navigateToRightLongPress = navigateToRightLongPress, navigateToPurchase = navigateToPurchase, - navigateToAdaptiveStrength = navigateToAdaptiveStrength, navigateToEqualizer = navigateToEqualizer, navigateToHeadTracking = navigateToHeadTracking, navigateToAccessibility = navigateToAccessibility, @@ -130,6 +143,8 @@ fun AppleSettingsScreen( writeATTCharacteristic: (ATTHandle, ByteArray) -> Unit, + updateSettings: (transform: (AppleSettings) -> AppleSettings) -> Unit, + // onAutomaticEarDetectionChanged: (Boolean) -> Unit, // onAutomaticConnectionChanged: (Boolean) -> Unit, @@ -142,7 +157,6 @@ fun AppleSettingsScreen( navigateToLeftLongPress: () -> Unit, navigateToRightLongPress: () -> Unit, navigateToPurchase: () -> Unit, - navigateToAdaptiveStrength: () -> Unit, navigateToEqualizer: () -> Unit, navigateToHeadTracking: () -> 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)) { item(key = "spacer_noise") { 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)) { @@ -319,30 +380,18 @@ fun AppleSettingsScreen( item(key = "spacer_audio") { Spacer(modifier = Modifier.height(16.dp)) } item(key = "audio") { - val adaptiveVolumeCapability = - baseCapabilities.contains(BaseCapability.ADAPTIVE_VOLUME) - val conversationalAwarenessCapability = - baseCapabilities.contains(BaseCapability.CONVERSATION_AWARENESS) - val loudSoundReductionCapability = - baseCapabilities.contains(BaseCapability.LOUD_SOUND_REDUCTION) - val adaptiveAudioCapability = - baseCapabilities.contains(BaseCapability.ADAPTIVE_VOLUME) + val adaptiveVolumeCapability = baseCapabilities.contains(BaseCapability.ADAPTIVE_VOLUME) + val conversationalAwarenessCapability = baseCapabilities.contains(BaseCapability.CONVERSATION_AWARENESS) + val loudSoundReductionCapability = baseCapabilities.contains(BaseCapability.LOUD_SOUND_REDUCTION) - val adaptiveVolumeChecked = - state.controlStates[ControlCommandIdentifier.ADAPTIVE_VOLUME_CONFIG]?.getOrNull( - 0 - ) == 0x01.toByte() - val conversationalAwarenessChecked = - state.controlStates[ControlCommandIdentifier.CONVERSATION_DETECT_CONFIG]?.getOrNull( - 0 - ) == 0x01.toByte() + val adaptiveVolumeChecked = state.controlStates[ControlCommandIdentifier.ADAPTIVE_VOLUME_CONFIG]?.getOrNull(0) == 0x01.toByte() + val conversationalAwarenessChecked = state.controlStates[ControlCommandIdentifier.CONVERSATION_DETECT_CONFIG]?.getOrNull(0) == 0x01.toByte() AudioSettings( adaptiveVolumeCapability = adaptiveVolumeCapability, conversationalAwarenessCapability = conversationalAwarenessCapability, loudSoundReductionCapability = loudSoundReductionCapability, - adaptiveAudioCapability = adaptiveAudioCapability, - customEqCapability = metadata.version3.startsWith("9"), + customEqCapability = metadata.version3.isNotBlank() && metadata.version3.first().digitToInt() >= 9, adaptiveVolumeChecked = adaptiveVolumeChecked, onAdaptiveVolumeCheckedChange = { checked -> setControlCommandBoolean( @@ -364,7 +413,6 @@ fun AppleSettingsScreen( byteArrayOf(if (checked) 0x01.toByte() else 0x00.toByte()) ) }, - navigateToAdaptiveStrength = navigateToAdaptiveStrength, navigateToEqualizer = navigateToEqualizer, vendorIdHook = uiState.vendorIdHook, isPremium = uiState.isPremium @@ -374,10 +422,28 @@ fun AppleSettingsScreen( item(key = "spacer_connection") { Spacer(modifier = Modifier.height(16.dp)) } item(key = "connection") { ConnectionSettings( - automaticEarDetectionEnabled = state.controlStates[ControlCommandIdentifier.EAR_DETECTION_CONFIG]?.getOrNull(0) == 0x01.toByte(), - onAutomaticEarDetectionChanged = { setControlCommandBoolean(ControlCommandIdentifier.EAR_DETECTION_CONFIG, it) }, + automaticEarDetectionEnabled = state.controlStates[ControlCommandIdentifier.EAR_DETECTION_CONFIG]?.getOrNull(0) == 0x01.toByte() || settings.earDetectionEnabled, + onAutomaticEarDetectionChanged = { enabled -> setControlCommandBoolean(ControlCommandIdentifier.EAR_DETECTION_CONFIG, enabled); updateSettings { it.copy(earDetectionEnabled = enabled) } }, 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 = "off_listening") { val id = ControlCommandIdentifier.ALLOW_OFF_OPTION @@ -487,7 +553,16 @@ fun AppleSettingsScreen( item(key = "spacer_debug") { Spacer(modifier = Modifier.height(16.dp)) } 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( contentText = "debug", onClick = navigateToDebugScreen @@ -517,6 +592,8 @@ fun AppleSettingsScreenPreviewApple() { setControlCommandBoolean = { _, _ -> }, writeATTCharacteristic = { _, _ -> }, + updateSettings = { _ -> }, + disconnect = {}, navigateBack = null, @@ -526,7 +603,6 @@ fun AppleSettingsScreenPreviewApple() { navigateToLeftLongPress = {}, navigateToRightLongPress = {}, navigateToPurchase = {}, - navigateToAdaptiveStrength = {}, navigateToEqualizer = {}, navigateToHeadTracking = {}, navigateToAccessibility = {}, @@ -559,6 +635,8 @@ fun AppleSettingsScreenPreviewMaterial() { setControlCommandBoolean = { _, _ -> }, writeATTCharacteristic = { _, _ -> }, + updateSettings = { _ -> }, + disconnect = {}, navigateBack = null, @@ -568,7 +646,6 @@ fun AppleSettingsScreenPreviewMaterial() { navigateToLeftLongPress = {}, navigateToRightLongPress = {}, navigateToPurchase = {}, - navigateToAdaptiveStrength = {}, navigateToEqualizer = {}, navigateToHeadTracking = {}, navigateToAccessibility = {}, diff --git a/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/screens/apple/CallControlScreen.kt b/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/screens/apple/CallControlScreen.kt index 7a0241b9..6e13f35f 100644 --- a/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/screens/apple/CallControlScreen.kt +++ b/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/screens/apple/CallControlScreen.kt @@ -19,9 +19,9 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import me.kavishdevar.librepods.R import me.kavishdevar.librepods.bluetooth.aacp.types.ControlCommandIdentifier -import me.kavishdevar.librepods.presentation.components.StyledList -import me.kavishdevar.librepods.presentation.components.StyledListItem -import me.kavishdevar.librepods.presentation.components.StyledScaffold +import me.kavishdevar.librepods.presentation.components.primitives.StyledList +import me.kavishdevar.librepods.presentation.components.primitives.StyledListItem +import me.kavishdevar.librepods.presentation.components.primitives.StyledScaffold import me.kavishdevar.librepods.presentation.viewmodel.AppleViewModel @Composable diff --git a/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/screens/apple/DebugScreen.kt b/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/screens/apple/DebugScreen.kt index e6e13c02..44787b6f 100644 --- a/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/screens/apple/DebugScreen.kt +++ b/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/screens/apple/DebugScreen.kt @@ -34,12 +34,12 @@ import me.kavishdevar.librepods.bluetooth.aacp.types.RTBuddyDescriptor import me.kavishdevar.librepods.bluetooth.aacp.types.SensorDataWxBuddyPayload import me.kavishdevar.librepods.devices.DeviceComponent import me.kavishdevar.librepods.devices.PacketDestination -import me.kavishdevar.librepods.presentation.components.StyledButton -import me.kavishdevar.librepods.presentation.components.StyledInputField -import me.kavishdevar.librepods.presentation.components.StyledList -import me.kavishdevar.librepods.presentation.components.StyledListItem -import me.kavishdevar.librepods.presentation.components.StyledListItemOrientation -import me.kavishdevar.librepods.presentation.components.StyledScaffold +import me.kavishdevar.librepods.presentation.components.primitives.StyledButton +import me.kavishdevar.librepods.presentation.components.primitives.StyledInputField +import me.kavishdevar.librepods.presentation.components.primitives.StyledList +import me.kavishdevar.librepods.presentation.components.primitives.StyledListItem +import me.kavishdevar.librepods.presentation.components.primitives.StyledListItemOrientation +import me.kavishdevar.librepods.presentation.components.primitives.StyledScaffold import me.kavishdevar.librepods.presentation.icons.richText import me.kavishdevar.librepods.presentation.viewmodel.AppleUiState import me.kavishdevar.librepods.presentation.viewmodel.AppleViewModel diff --git a/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/screens/apple/EqualizerScreen.kt b/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/screens/apple/EqualizerScreen.kt index ebefb033..87bd5119 100644 --- a/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/screens/apple/EqualizerScreen.kt +++ b/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/screens/apple/EqualizerScreen.kt @@ -77,10 +77,10 @@ import com.kyant.backdrop.effects.lens import com.kyant.backdrop.highlight.Highlight import kotlinx.coroutines.flow.debounce import me.kavishdevar.librepods.R -import me.kavishdevar.librepods.presentation.components.StyledButton -import me.kavishdevar.librepods.presentation.components.StyledList -import me.kavishdevar.librepods.presentation.components.StyledListItem -import me.kavishdevar.librepods.presentation.components.StyledScaffold +import me.kavishdevar.librepods.presentation.components.primitives.StyledButton +import me.kavishdevar.librepods.presentation.components.primitives.StyledList +import me.kavishdevar.librepods.presentation.components.primitives.StyledListItem +import me.kavishdevar.librepods.presentation.components.primitives.StyledScaffold import me.kavishdevar.librepods.presentation.theme.DesignSystem import me.kavishdevar.librepods.presentation.theme.LibrePodsTheme import me.kavishdevar.librepods.presentation.viewmodel.AppleUiState diff --git a/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/screens/apple/HeadTrackingScreen.kt b/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/screens/apple/HeadTrackingScreen.kt index d473d3c2..cbfc715f 100644 --- a/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/screens/apple/HeadTrackingScreen.kt +++ b/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/screens/apple/HeadTrackingScreen.kt @@ -84,12 +84,12 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.launch import me.kavishdevar.librepods.R -import me.kavishdevar.librepods.presentation.components.MaterialButtonStyle -import me.kavishdevar.librepods.presentation.components.StyledButton -import me.kavishdevar.librepods.presentation.components.StyledIconButton -import me.kavishdevar.librepods.presentation.components.StyledScaffold -import me.kavishdevar.librepods.presentation.components.StyledSlider -import me.kavishdevar.librepods.presentation.components.StyledToggle +import me.kavishdevar.librepods.presentation.components.primitives.MaterialButtonStyle +import me.kavishdevar.librepods.presentation.components.primitives.StyledButton +import me.kavishdevar.librepods.presentation.components.primitives.StyledIconButton +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.MaterialIcons import me.kavishdevar.librepods.presentation.theme.DesignSystem diff --git a/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/screens/apple/HearingAidAdjustmentsScreen.kt b/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/screens/apple/HearingAidAdjustmentsScreen.kt index 0851e7d1..19bbc88f 100644 --- a/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/screens/apple/HearingAidAdjustmentsScreen.kt +++ b/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/screens/apple/HearingAidAdjustmentsScreen.kt @@ -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.parseHearingAidSettingsResponse import me.kavishdevar.librepods.bluetooth.att.types.sendHearingAidSettings -import me.kavishdevar.librepods.presentation.components.StyledScaffold -import me.kavishdevar.librepods.presentation.components.StyledSlider -import me.kavishdevar.librepods.presentation.components.StyledToggle +import me.kavishdevar.librepods.presentation.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.viewmodel.AppleViewModel diff --git a/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/screens/apple/HearingAidScreen.kt b/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/screens/apple/HearingAidScreen.kt index efc63384..e90df8a2 100644 --- a/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/screens/apple/HearingAidScreen.kt +++ b/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/screens/apple/HearingAidScreen.kt @@ -50,11 +50,11 @@ import me.kavishdevar.librepods.R import me.kavishdevar.librepods.bluetooth.aacp.types.ControlCommandIdentifier import me.kavishdevar.librepods.bluetooth.att.types.parseTransparencySettingsResponse import me.kavishdevar.librepods.bluetooth.att.types.sendTransparencySettings -import me.kavishdevar.librepods.presentation.components.ConfirmationDialog -import me.kavishdevar.librepods.presentation.components.StyledList -import me.kavishdevar.librepods.presentation.components.StyledListItem -import me.kavishdevar.librepods.presentation.components.StyledScaffold -import me.kavishdevar.librepods.presentation.components.StyledToggle +import me.kavishdevar.librepods.presentation.components.primitives.StyledConfirmationDialog +import me.kavishdevar.librepods.presentation.components.primitives.StyledList +import me.kavishdevar.librepods.presentation.components.primitives.StyledListItem +import me.kavishdevar.librepods.presentation.components.primitives.StyledScaffold +import me.kavishdevar.librepods.presentation.components.primitives.StyledToggle import me.kavishdevar.librepods.presentation.viewmodel.AppleViewModel private const val TAG = "HearingAidScreen" @@ -187,7 +187,7 @@ fun HearingAidScreen( } } - ConfirmationDialog( + StyledConfirmationDialog( showDialog = showDialog, title = "Enable Hearing Aid", message = "Enabling Hearing Aid will disable Headphone Accommodation and Customized Transparency Mode.", diff --git a/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/screens/apple/HearingProtectionScreen.kt b/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/screens/apple/HearingProtectionScreen.kt index fbd02ac1..46382e85 100644 --- a/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/screens/apple/HearingProtectionScreen.kt +++ b/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/screens/apple/HearingProtectionScreen.kt @@ -38,9 +38,9 @@ import com.kyant.backdrop.backdrops.rememberLayerBackdrop import me.kavishdevar.librepods.R import me.kavishdevar.librepods.bluetooth.aacp.types.ControlCommandIdentifier import me.kavishdevar.librepods.bluetooth.att.ATTHandle -import me.kavishdevar.librepods.presentation.components.StyledButton -import me.kavishdevar.librepods.presentation.components.StyledScaffold -import me.kavishdevar.librepods.presentation.components.StyledToggle +import me.kavishdevar.librepods.presentation.components.primitives.StyledButton +import me.kavishdevar.librepods.presentation.components.primitives.StyledScaffold +import me.kavishdevar.librepods.presentation.components.primitives.StyledToggle import me.kavishdevar.librepods.presentation.viewmodel.AppleViewModel @Composable diff --git a/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/screens/apple/HeartRateScreen.kt b/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/screens/apple/HeartRateScreen.kt index db1ea9b0..4dd56859 100644 --- a/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/screens/apple/HeartRateScreen.kt +++ b/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/screens/apple/HeartRateScreen.kt @@ -27,10 +27,12 @@ import androidx.compose.material3.Text import androidx.compose.material3.minimumInteractiveComponentSize import androidx.compose.material3.toShape 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.Alignment import androidx.compose.ui.Modifier 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.isGranted import com.google.accompanist.permissions.rememberPermissionState +import kotlinx.coroutines.flow.debounce import me.kavishdevar.librepods.R -import me.kavishdevar.librepods.presentation.components.StyledIconButton -import me.kavishdevar.librepods.presentation.components.StyledListItem -import me.kavishdevar.librepods.presentation.components.StyledListItemOrientation -import me.kavishdevar.librepods.presentation.components.StyledScaffold +import me.kavishdevar.librepods.devices.AppleSettings +import me.kavishdevar.librepods.presentation.components.primitives.StyledIconButton +import me.kavishdevar.librepods.presentation.components.primitives.StyledListItem +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.MaterialIcons 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.ZoneId import java.time.format.DateTimeFormatter +import kotlin.math.roundToInt +import kotlin.time.Duration.Companion.milliseconds @Composable fun HeartRateRoute( @@ -68,7 +76,8 @@ fun HeartRateRoute( navigateBack = navigateBack, startHr = viewModel::startHr, stopHr = viewModel::stopHr, - setHrRange = viewModel::setHrRange + setHrRange = viewModel::setHrRange, + updateSettings = viewModel::updateSettings ) } @@ -79,9 +88,11 @@ fun HeartRateScreen( navigateBack: (() -> Unit)?, startHr: () -> Unit, stopHr: () -> Unit, - setHrRange: (ClosedRange) -> Unit + setHrRange: (ClosedRange) -> Unit, + updateSettings: (transform: (AppleSettings) -> AppleSettings) -> Unit, ) { val state = uiState.state + val settings = uiState.settings val scrollState = rememberScrollState() @@ -129,59 +140,7 @@ fun HeartRateScreen( ) { Spacer(modifier = Modifier.height(topPadding)) - val sliderValue = remember { - 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 -// ) -// } + val healthPermissions = rememberPermissionState(HealthPermission.getWritePermission(HeartRateRecord::class)) AnimatedVisibility(state.currentHeartRate != null) { 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( modifier = Modifier .fillMaxWidth() diff --git a/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/screens/apple/MicrophoneSettingsScreen.kt b/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/screens/apple/MicrophoneSettingsScreen.kt index 81cc5b70..2a59dfd5 100644 --- a/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/screens/apple/MicrophoneSettingsScreen.kt +++ b/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/screens/apple/MicrophoneSettingsScreen.kt @@ -17,9 +17,9 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import me.kavishdevar.librepods.R import me.kavishdevar.librepods.bluetooth.aacp.types.ControlCommandIdentifier -import me.kavishdevar.librepods.presentation.components.StyledList -import me.kavishdevar.librepods.presentation.components.StyledListItem -import me.kavishdevar.librepods.presentation.components.StyledScaffold +import me.kavishdevar.librepods.presentation.components.primitives.StyledList +import me.kavishdevar.librepods.presentation.components.primitives.StyledListItem +import me.kavishdevar.librepods.presentation.components.primitives.StyledScaffold import me.kavishdevar.librepods.presentation.viewmodel.AppleViewModel @Composable diff --git a/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/screens/apple/PressAndHoldSettingsScreen.kt b/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/screens/apple/PressAndHoldSettingsScreen.kt index 526aae10..ecfa646d 100644 --- a/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/screens/apple/PressAndHoldSettingsScreen.kt +++ b/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/screens/apple/PressAndHoldSettingsScreen.kt @@ -45,11 +45,11 @@ import com.kyant.backdrop.backdrops.rememberLayerBackdrop import me.kavishdevar.librepods.R import me.kavishdevar.librepods.bluetooth.aacp.types.ControlCommandIdentifier import me.kavishdevar.librepods.data.StemAction -import me.kavishdevar.librepods.presentation.components.StyledButton -import me.kavishdevar.librepods.presentation.components.StyledList -import me.kavishdevar.librepods.presentation.components.StyledListItem -import me.kavishdevar.librepods.presentation.components.StyledListItemOrientation -import me.kavishdevar.librepods.presentation.components.StyledScaffold +import me.kavishdevar.librepods.presentation.components.primitives.StyledButton +import me.kavishdevar.librepods.presentation.components.primitives.StyledList +import me.kavishdevar.librepods.presentation.components.primitives.StyledListItem +import me.kavishdevar.librepods.presentation.components.primitives.StyledListItemOrientation +import me.kavishdevar.librepods.presentation.components.primitives.StyledScaffold import me.kavishdevar.librepods.presentation.viewmodel.AppleViewModel import kotlin.experimental.and import kotlin.io.encoding.ExperimentalEncodingApi @@ -78,6 +78,9 @@ fun LongPress( 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( title = name, navigateBack = navigateBack @@ -98,7 +101,7 @@ fun LongPress( selected = longPressAction == StemAction.CYCLE_NOISE_CONTROL_MODES, onClick = { viewModel.setLongPressAction( - name, + side, StemAction.CYCLE_NOISE_CONTROL_MODES ) } @@ -109,7 +112,7 @@ fun LongPress( selected = longPressAction == StemAction.DIGITAL_ASSISTANT, onClick = { viewModel.setLongPressAction( - name, + side, StemAction.DIGITAL_ASSISTANT ) }, diff --git a/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/screens/apple/RecordingScreen.kt b/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/screens/apple/RecordingScreen.kt index e85be9f9..56d2f339 100644 --- a/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/screens/apple/RecordingScreen.kt +++ b/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/screens/apple/RecordingScreen.kt @@ -35,17 +35,16 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalLocale import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.core.content.FileProvider import me.kavishdevar.librepods.BuildConfig import me.kavishdevar.librepods.R import me.kavishdevar.librepods.data.recording.Recording -import me.kavishdevar.librepods.presentation.components.StyledButton -import me.kavishdevar.librepods.presentation.components.StyledList -import me.kavishdevar.librepods.presentation.components.StyledListItem -import me.kavishdevar.librepods.presentation.components.StyledListItemOrientation -import me.kavishdevar.librepods.presentation.components.StyledScaffold +import me.kavishdevar.librepods.presentation.components.primitives.StyledButton +import me.kavishdevar.librepods.presentation.components.primitives.StyledList +import me.kavishdevar.librepods.presentation.components.primitives.StyledListItem +import me.kavishdevar.librepods.presentation.components.primitives.StyledListItemOrientation +import me.kavishdevar.librepods.presentation.components.primitives.StyledScaffold import me.kavishdevar.librepods.presentation.viewmodel.AppleUiState import me.kavishdevar.librepods.presentation.viewmodel.AppleViewModel import java.time.Instant diff --git a/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/screens/apple/RenameScreen.kt b/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/screens/apple/RenameScreen.kt index 31e70aff..90088af8 100644 --- a/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/screens/apple/RenameScreen.kt +++ b/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/screens/apple/RenameScreen.kt @@ -39,8 +39,8 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi import me.kavishdevar.librepods.R -import me.kavishdevar.librepods.presentation.components.StyledInputField -import me.kavishdevar.librepods.presentation.components.StyledScaffold +import me.kavishdevar.librepods.presentation.components.primitives.StyledInputField +import me.kavishdevar.librepods.presentation.components.primitives.StyledScaffold import me.kavishdevar.librepods.presentation.viewmodel.AppleViewModel import kotlin.io.encoding.ExperimentalEncodingApi diff --git a/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/screens/apple/TransparencySettingsScreen.kt b/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/screens/apple/TransparencySettingsScreen.kt index e9c6e523..dba58087 100644 --- a/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/screens/apple/TransparencySettingsScreen.kt +++ b/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/screens/apple/TransparencySettingsScreen.kt @@ -60,9 +60,9 @@ import me.kavishdevar.librepods.R import me.kavishdevar.librepods.bluetooth.att.types.TransparencySettings import me.kavishdevar.librepods.bluetooth.att.types.parseTransparencySettingsResponse import me.kavishdevar.librepods.bluetooth.att.types.sendTransparencySettings -import me.kavishdevar.librepods.presentation.components.StyledScaffold -import me.kavishdevar.librepods.presentation.components.StyledSlider -import me.kavishdevar.librepods.presentation.components.StyledToggle +import me.kavishdevar.librepods.presentation.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.theme.DesignSystem import me.kavishdevar.librepods.presentation.theme.LocalDesignSystem diff --git a/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/screens/apple/UpdateHearingTestScreen.kt b/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/screens/apple/UpdateHearingTestScreen.kt index dc9ed046..a6ed378f 100644 --- a/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/screens/apple/UpdateHearingTestScreen.kt +++ b/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/screens/apple/UpdateHearingTestScreen.kt @@ -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.parseHearingAidSettingsResponse 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.LibrePodsTheme import me.kavishdevar.librepods.presentation.viewmodel.AppleUiState diff --git a/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/screens/apple/VersionInfoScreen.kt b/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/screens/apple/VersionInfoScreen.kt index 61d47c87..37838b2c 100644 --- a/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/screens/apple/VersionInfoScreen.kt +++ b/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/screens/apple/VersionInfoScreen.kt @@ -30,9 +30,9 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import me.kavishdevar.librepods.R -import me.kavishdevar.librepods.presentation.components.StyledList -import me.kavishdevar.librepods.presentation.components.StyledListItem -import me.kavishdevar.librepods.presentation.components.StyledScaffold +import me.kavishdevar.librepods.presentation.components.primitives.StyledList +import me.kavishdevar.librepods.presentation.components.primitives.StyledListItem +import me.kavishdevar.librepods.presentation.components.primitives.StyledScaffold import me.kavishdevar.librepods.presentation.viewmodel.AppleViewModel @Composable diff --git a/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/screens/onboarding/NotSupportedPage.kt b/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/screens/onboarding/NotSupportedPage.kt index 2ea14862..d272f908 100644 --- a/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/screens/onboarding/NotSupportedPage.kt +++ b/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/screens/onboarding/NotSupportedPage.kt @@ -18,9 +18,9 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import me.kavishdevar.librepods.R -import me.kavishdevar.librepods.presentation.components.AppInfoCard -import me.kavishdevar.librepods.presentation.components.DeviceInfoCard -import me.kavishdevar.librepods.presentation.components.StyledListItem +import me.kavishdevar.librepods.presentation.components.common.AppInfoCard +import me.kavishdevar.librepods.presentation.components.common.DeviceInfoCard +import me.kavishdevar.librepods.presentation.components.primitives.StyledListItem @Composable fun NotSupportedPage( diff --git a/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/screens/onboarding/PermissionsPage.kt b/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/screens/onboarding/PermissionsPage.kt index a5f84d92..3785922a 100644 --- a/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/screens/onboarding/PermissionsPage.kt +++ b/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/screens/onboarding/PermissionsPage.kt @@ -50,9 +50,9 @@ import com.google.accompanist.permissions.isGranted import com.google.accompanist.permissions.rememberMultiplePermissionsState import com.google.accompanist.permissions.rememberPermissionState import me.kavishdevar.librepods.R -import me.kavishdevar.librepods.presentation.components.StyledList -import me.kavishdevar.librepods.presentation.components.StyledListItem -import me.kavishdevar.librepods.presentation.components.StyledListItemOrientation +import me.kavishdevar.librepods.presentation.components.primitives.StyledList +import me.kavishdevar.librepods.presentation.components.primitives.StyledListItem +import me.kavishdevar.librepods.presentation.components.primitives.StyledListItemOrientation import me.kavishdevar.librepods.presentation.icons.MaterialIcons @OptIn(ExperimentalPermissionsApi::class) diff --git a/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/viewmodel/AppleViewModel.kt b/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/viewmodel/AppleViewModel.kt index 2dc126b4..4ee2c42b 100644 --- a/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/viewmodel/AppleViewModel.kt +++ b/android/app/src/main/kotlin/me/kavishdevar/librepods/presentation/viewmodel/AppleViewModel.kt @@ -198,5 +198,9 @@ class AppleViewModel( _heartRateRange.value = range } + fun updateSettings( transform : (AppleSettings) -> AppleSettings) { + device.updateSettings(transform) + } + fun sendRawPacket(data: ByteArray): Boolean = device.sendRawPacket(data) } diff --git a/android/app/src/main/kotlin/me/kavishdevar/librepods/repository/AppDataRepository.kt b/android/app/src/main/kotlin/me/kavishdevar/librepods/repository/AppDataRepository.kt index 9bfdb49c..5927888b 100644 --- a/android/app/src/main/kotlin/me/kavishdevar/librepods/repository/AppDataRepository.kt +++ b/android/app/src/main/kotlin/me/kavishdevar/librepods/repository/AppDataRepository.kt @@ -1,5 +1,6 @@ package me.kavishdevar.librepods.repository +import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob @@ -16,6 +17,12 @@ class AppDataRepository( private val settingsDao: AppSettingsDao, private val stateDao: AppStateDao, ) { + private val initialized = CompletableDeferred() + + suspend fun awaitInitialized() { + initialized.await() + } + private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO) private val _settings = MutableStateFlow(AppSettingsEntity()) @@ -28,6 +35,7 @@ class AppDataRepository( scope.launch { _settings.value = settingsDao.get() ?: AppSettingsEntity() _state.value = stateDao.get() ?: AppStateEntity() + initialized.complete(Unit) } } diff --git a/android/app/src/main/kotlin/me/kavishdevar/librepods/services/LibrePodsService.kt b/android/app/src/main/kotlin/me/kavishdevar/librepods/services/LibrePodsService.kt index c9bf9408..d03b0440 100644 --- a/android/app/src/main/kotlin/me/kavishdevar/librepods/services/LibrePodsService.kt +++ b/android/app/src/main/kotlin/me/kavishdevar/librepods/services/LibrePodsService.kt @@ -27,9 +27,14 @@ import android.os.IBinder import android.os.ParcelUuid import android.os.ext.SdkExtensions 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.view.View import android.widget.RemoteViews +import android.widget.Toast import androidx.core.app.NotificationCompat import androidx.health.connect.client.permission.HealthPermission import androidx.health.connect.client.records.HeartRateRecord @@ -111,8 +116,53 @@ class LibrePodsService : Service() { (application as LibrePodsApplication).healthConnectClient } - private val hasConnectedToAACP by lazy { - appDataRepository.state.value.hasConnectedToAACP + private var isCallRinging = false + + 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() { @@ -132,6 +182,14 @@ class LibrePodsService : Service() { localMac = null // TODO: smart routing. MAC_ADDRESS message gives host mac? ) + val telephonyManager = getSystemService(TelephonyManager::class.java) + + telephonyManager.registerTelephonyCallback( + mainExecutor, + telephonyCallback + ) + + startForegroundNotification() } @@ -538,7 +596,6 @@ class LibrePodsService : Service() { return device } - fun observeAppSettings(): Job { var oldAppSettings: AppSettingsEntity = appDataRepository.settings.value @@ -562,9 +619,12 @@ class LibrePodsService : Service() { if (state.aacpPackets != previousState.aacpPackets) { - if (!hasConnectedToAACP) { + if (!appDataRepository.state.value.hasConnectedToAACP) { appDataRepository.updateState { it.copy(hasConnectedToAACP = true) } } + if (appDataRepository.state.value.firstSuccessfulConnectionTime == null) { + appDataRepository.updateState { it.copy(firstSuccessfulConnectionTime = System.currentTimeMillis()) } + } } when { @@ -655,7 +715,7 @@ class LibrePodsService : Service() { TAG, "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") if (earDetectionEnabled) { processComponentStateChange( @@ -695,10 +755,29 @@ class LibrePodsService : Service() { "conversational awareness state: ${state.conversationalAwarenessState}" ) - // TODO: multi-step volume change. implementation exists in linux rewrite; copy from there when (state.conversationalAwarenessState) { - 1, 2 -> MediaController.startSpeaking() - 6, 8, 9 -> MediaController.stopSpeaking() + 1 -> { + 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() + } } } diff --git a/android/app/src/main/kotlin/me/kavishdevar/librepods/utils/MediaController.kt b/android/app/src/main/kotlin/me/kavishdevar/librepods/utils/MediaController.kt index 42cd9510..ccf82284 100644 --- a/android/app/src/main/kotlin/me/kavishdevar/librepods/utils/MediaController.kt +++ b/android/app/src/main/kotlin/me/kavishdevar/librepods/utils/MediaController.kt @@ -311,38 +311,74 @@ object MediaController { } } - @Synchronized - 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") +// @Synchronized +// 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") +// +// 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) { - 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") +// @Synchronized +// fun stopSpeaking() { +// Log.d("MediaController", "Stopping speaking, initialVolume: $initialVolume") +// if (initialVolume != null) { +// smoothVolumeTransition(audioManager.getStreamVolume(AudioManager.STREAM_MUSIC), initialVolume!!) +// if (conversationalAwarenessPauseMusic) { +// sendPlay() +// } +// initialVolume = null +// } +// } + + @Synchronized + fun setVolume(volumePercent: Int) { + val maxVolume = audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC) + val targetVolume = maxVolume * volumePercent / 100 + + smoothVolumeTransition( + audioManager.getStreamVolume(AudioManager.STREAM_MUSIC), + targetVolume, + ) } @Synchronized - fun stopSpeaking() { - Log.d("MediaController", "Stopping speaking, initialVolume: $initialVolume") - if (initialVolume != null) { - smoothVolumeTransition(audioManager.getStreamVolume(AudioManager.STREAM_MUSIC), initialVolume!!) - if (conversationalAwarenessPauseMusic) { - sendPlay() - } - initialVolume = null + fun startSpeaking() { + if (initialVolume == null) { + initialVolume = audioManager.getStreamVolume(AudioManager.STREAM_MUSIC) } + + 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) { diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml index ad0244e4..103699e2 100644 --- a/android/app/src/main/res/values/strings.xml +++ b/android/app/src/main/res/values/strings.xml @@ -150,7 +150,7 @@ Open dialog for controlling 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 Disconnect AirPods when not wearing - You will still be able to control them with the app - this just disconnects the audio. + You will still be able to control them with the app; this just disconnects the audio. Advanced Options Set Identity Resolving Key (IRK) Manually set the IRK value used for resolving BLE random addresses @@ -340,4 +340,10 @@ Use highest refresh rate available Shows extra information and settings. Interaction + Heart Rate Monitoring + Sends a notification when your heart rate is above the threshold. Consumes more battery. + Heart rate alert + Heart rate threshold + Call rejected + Call accepted