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

implement head gestures for calls, clean up code ig
This commit is contained in:
Kavish Devar
2026-08-16 21:02:11 +05:30
parent ddd1a9c5ce
commit e950aa010d
72 changed files with 801 additions and 1179 deletions
+1 -1
View File
@@ -77,7 +77,7 @@ android {
defaultConfig {
applicationId = "me.kavishdevar.librepods"
targetSdk = 37
versionCode = 65
versionCode = 80
versionName = appVersionName
}
buildTypes {
@@ -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')"
]
}
}
@@ -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)
@@ -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<Boolean> = _isPremium
private val _price = MutableStateFlow(context.getString(R.string.name_your_own_price))
override val price: StateFlow<String> = _price
private val sharedPreferences = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
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) }
}
}
@@ -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<Boolean> = _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 }
@@ -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
@@ -8,6 +8,8 @@ data class AppStateEntity(
@PrimaryKey
val id: Int = 0,
val isPremium: Boolean = false,
val hasCompletedOnboarding: Boolean = false,
val lastVersionShown: String? = null,
@@ -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)
}
@@ -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
@@ -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<LibrePodsService?>(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
@@ -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
@@ -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
@@ -1,49 +0,0 @@
/*
LibrePods - AirPods liberated from Apples ecosystem
Copyright (C) 2025 LibrePods contributors
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
@file:OptIn(ExperimentalEncodingApi::class)
package me.kavishdevar.librepods.presentation.components
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import me.kavishdevar.librepods.R
import kotlin.io.encoding.ExperimentalEncodingApi
@Composable
fun ConnectionSettings(
automaticEarDetectionEnabled: Boolean,
onAutomaticEarDetectionChanged: (Boolean) -> Unit,
automaticConnectionEnabled: Boolean,
onAutomaticConnectionChanged: (Boolean) -> Unit,
) {
StyledList {
StyledToggle(
label = stringResource(R.string.ear_detection),
checked = automaticEarDetectionEnabled,
onCheckedChange = onAutomaticEarDetectionChanged
)
StyledToggle(
label = stringResource(R.string.automatically_connect),
description = stringResource(R.string.automatically_connect_description),
checked = automaticConnectionEnabled,
onCheckedChange = onAutomaticConnectionChanged
)
}
}
@@ -1,106 +0,0 @@
/*
LibrePods - AirPods liberated from Apples ecosystem
Copyright (C) 2025 LibrePods contributors
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
@file:Suppress("unused")
package me.kavishdevar.librepods.presentation.components
import androidx.compose.animation.animateColorAsState
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
private val SelectedColorBlue = Color(0xFF0A84FF)
private val UnselectedColor = Color(0x593C3C3E)
private val TextColor = Color.White
private val IconTint = Color.White
@Composable
fun ControlCenterButton(
label: String,
icon: Painter,
onClick: () -> Unit,
modifier: Modifier = Modifier,
iconAreaSize: Dp,
isSelected: Boolean,
backgroundBrush: Brush? = null
) {
val targetBackgroundColor = if (isSelected) SelectedColorBlue else UnselectedColor
val backgroundColor by animateColorAsState(
targetValue = targetBackgroundColor,
label = "ButtonBackground"
)
Column(
modifier = modifier,
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Box(
modifier = Modifier
.size(iconAreaSize)
.clip(CircleShape)
.background(backgroundBrush ?: Brush.linearGradient(colors=listOf(backgroundColor, backgroundColor)))
.clickable(
onClick = onClick,
indication = null,
interactionSource = remember { MutableInteractionSource() }
),
contentAlignment = Alignment.Center
) {
Icon(
painter = icon,
contentDescription = null,
tint = IconTint,
modifier = Modifier.size(32.dp)
)
}
Spacer(modifier = Modifier.height(8.dp))
Text(
text = label,
color = TextColor,
fontSize = 12.sp,
fontWeight = FontWeight.Medium,
textAlign = TextAlign.Center,
maxLines = 2
)
}
}
@@ -1,242 +0,0 @@
/*
LibrePods - AirPods liberated from Apples ecosystem
Copyright (C) 2025 LibrePods contributors
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package me.kavishdevar.librepods.presentation.components
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.animateDpAsState
import androidx.compose.animation.core.spring
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import me.kavishdevar.librepods.R
import me.kavishdevar.librepods.devices.NoiseControlMode
private val ContainerColor = Color(0x593C3C3E)
private val SelectedIndicatorColorGray = Color(0xFF6C6C6E)
private val SelectedIndicatorColorBlue = Color(0xFF0A84FF)
private val TextColor = Color.White
private val IconTintUnselected = Color.White
private val IconTintSelected = Color.White
internal val AdaptiveRainbowBrush = Brush.sweepGradient(
colors = listOf(
Color(0xFFB03A2F), Color(0xFFB07A2F), Color(0xFFB0A22F), Color(0xFF6AB02F),
Color(0xFF2FAAB0), Color(0xFF2F5EB0), Color(0xFF7D2FB0), Color(0xFFB02F7D),
Color(0xFFB03A2F)
)
)
internal val IconAreaSize = 72.dp
private val IconSize = 42.dp
private val IconRowHeight = IconAreaSize + 12.dp
private val TextRowHeight = 24.dp
private val TextSize = 12.sp
@Composable
fun ControlCenterNoiseControlSegmentedButton(
modifier: Modifier = Modifier,
availableModes: List<NoiseControlMode>,
selectedMode: NoiseControlMode,
onModeSelected: (NoiseControlMode) -> Unit
) {
val selectedIndex = availableModes.indexOf(selectedMode).coerceAtLeast(0)
val density = LocalDensity.current
var iconRowWidthPx by remember { mutableFloatStateOf(0f) }
val itemCount = availableModes.size
val itemSlotWidthPx = remember(iconRowWidthPx, itemCount) {
if (itemCount > 0 && iconRowWidthPx > 0) {
iconRowWidthPx / itemCount
} else {
0f
}
}
val itemSlotWidthDp = remember(itemSlotWidthPx) { with(density) { itemSlotWidthPx.toDp() } }
val iconAreaSizePx = remember { with(density) { IconAreaSize.toPx() } }
val targetIndicatorStartPx = remember(selectedIndex, itemSlotWidthPx, iconAreaSizePx) {
if (itemSlotWidthPx > 0) {
val slotCenterPx = (selectedIndex + 0.5f) * itemSlotWidthPx
slotCenterPx - (iconAreaSizePx / 2f)
} else {
0f
}
}
val indicatorOffset: Dp by animateDpAsState(
targetValue = with(density) { targetIndicatorStartPx.toDp() },
animationSpec = spring(
dampingRatio = Spring.DampingRatioLowBouncy,
stiffness = Spring.StiffnessMedium
),
label = "IndicatorOffset"
)
val indicatorBackground = remember(selectedMode) {
when (selectedMode) {
NoiseControlMode.ADAPTIVE -> AdaptiveRainbowBrush
NoiseControlMode.OFF -> Brush.linearGradient(colors=listOf(SelectedIndicatorColorGray, SelectedIndicatorColorGray))
NoiseControlMode.TRANSPARENCY,
NoiseControlMode.NOISE_CANCELLATION -> Brush.linearGradient(colors=listOf(SelectedIndicatorColorBlue, SelectedIndicatorColorBlue))
}
}
Column(
modifier = modifier,
horizontalAlignment = Alignment.CenterHorizontally
) {
Box(
modifier = Modifier
.fillMaxWidth()
.height(IconRowHeight)
.clip(CircleShape)
.background(ContainerColor)
.onSizeChanged { iconRowWidthPx = it.width.toFloat() },
contentAlignment = Alignment.Center
) {
Box(
Modifier
.align(Alignment.CenterStart)
.offset(x = indicatorOffset)
.size(IconAreaSize)
.clip(CircleShape)
.background(indicatorBackground)
)
Row(
modifier = Modifier.fillMaxWidth().align(Alignment.Center),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceAround
) {
availableModes.forEach { mode ->
val isSelected = selectedMode == mode
NoiseControlIconItem(
modifier = Modifier.size(IconAreaSize),
mode = mode,
isSelected = isSelected,
onClick = { onModeSelected(mode) }
)
}
}
}
Spacer(modifier = Modifier.height(4.dp))
Row(
modifier = Modifier
.fillMaxWidth()
.height(TextRowHeight),
horizontalArrangement = Arrangement.SpaceAround,
verticalAlignment = Alignment.CenterVertically
) {
availableModes.forEach { mode ->
val isSelected = selectedMode == mode
Text(
text = getModeLabel(mode),
color = TextColor,
fontSize = TextSize,
fontWeight = if (isSelected) FontWeight.SemiBold else FontWeight.Normal,
textAlign = TextAlign.Center,
modifier = Modifier.width(itemSlotWidthDp.coerceAtLeast(1.dp))
)
}
}
}
}
@Composable
private fun NoiseControlIconItem(
modifier: Modifier = Modifier,
mode: NoiseControlMode,
isSelected: Boolean,
onClick: () -> Unit
) {
val iconRes = remember(mode) { getModeIconRes(mode) }
val tint = IconTintUnselected
Box(
modifier = modifier
.clip(CircleShape)
.clickable(
onClick = onClick,
indication = null,
interactionSource = remember { MutableInteractionSource() }
),
contentAlignment = Alignment.Center
) {
Icon(
painter = painterResource(id = iconRes),
contentDescription = getModeLabel(mode),
tint = if (isSelected && mode == NoiseControlMode.ADAPTIVE) IconTintSelected else tint,
modifier = Modifier.size(IconSize)
)
}
}
private fun getModeIconRes(mode: NoiseControlMode): Int {
return when (mode) {
NoiseControlMode.OFF -> R.drawable.ic_noise_cancellation
NoiseControlMode.TRANSPARENCY -> R.drawable.ic_transparency
NoiseControlMode.ADAPTIVE -> R.drawable.ic_adaptive
NoiseControlMode.NOISE_CANCELLATION -> R.drawable.ic_noise_cancellation
}
}
private fun getModeLabel(mode: NoiseControlMode): String {
return when (mode) {
NoiseControlMode.OFF -> "Off"
NoiseControlMode.TRANSPARENCY -> "Transparency"
NoiseControlMode.ADAPTIVE -> "Adaptive"
NoiseControlMode.NOISE_CANCELLATION -> "Noise Cancellation"
}
}
@@ -1,56 +0,0 @@
package me.kavishdevar.librepods.presentation.components
import android.os.Build
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import me.kavishdevar.librepods.R
import me.kavishdevar.librepods.utils.XposedState
@Composable
fun DeviceInfoCard() {
StyledList(title = stringResource(R.string.device_info)) {
StyledListItem(
contentText = stringResource(R.string.manufacturer),
supportingText = Build.MANUFACTURER,
enabled = false
)
StyledListItem(
contentText = stringResource(R.string.model_number),
supportingText = Build.MODEL,
enabled = false
)
StyledListItem(
contentText = stringResource(R.string.build_id),
supportingText = Build.DISPLAY,
enabled = false
)
StyledListItem(
contentText = stringResource(R.string.version),
supportingText = "${Build.ID} (${Build.VERSION.SDK_INT_FULL})",
enabled = false
)
StyledListItem(
contentText = stringResource(R.string.xposed_available),
supportingText = if (XposedState.isAvailable) {
stringResource(R.string.yes)
} else {
stringResource(R.string.no)
},
enabled = false
)
StyledListItem(
contentText = stringResource(R.string.app_enabled_in_xposed),
supportingText = if (XposedState.bluetoothScopeEnabled) {
stringResource(R.string.yes)
} else {
stringResource(R.string.no)
},
enabled = false
)
}
}
@@ -1,190 +0,0 @@
/*
LibrePods - AirPods liberated from Apples ecosystem
Copyright (C) 2025 LibrePods contributors
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package me.kavishdevar.librepods.presentation.components
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.spring
import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.gestures.draggable
import androidx.compose.foundation.gestures.rememberDraggableState
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import kotlin.math.abs
import kotlin.math.max
import kotlin.math.min
import kotlin.math.roundToInt
import kotlin.math.sign
@Composable
fun VerticalVolumeSlider(
displayFraction: Float,
maxVolume: Int,
onVolumeChange: (Int) -> Unit,
initialFraction: Float,
onDragStateChange: (Boolean) -> Unit,
modifier: Modifier = Modifier,
baseSliderHeight: Dp = 400.dp,
baseSliderWidth: Dp = 145.dp,
baseCornerRadius: Dp = 45.dp,
maxStretchFactor: Float = 1.15f,
minCompressionFactor: Float = 0.875f,
stretchSensitivity: Float = 1.0f,
compressionSensitivity: Float = 1.0f,
cornerRadiusChangeFactor: Float = 0.2f,
directionalStretchRatio: Float = 0.75f
) {
val trackColor = Color(0x593C3C3E)
val progressColor = Color.White
var dragFraction by remember { mutableFloatStateOf(initialFraction) }
var isDragging by remember { mutableStateOf(false) }
var rawDragPosition by remember { mutableFloatStateOf(initialFraction) }
var overscrollAmount by remember { mutableFloatStateOf(0f) }
val baseHeightPx = with(LocalDensity.current) { baseSliderHeight.toPx() }
val animatedProgress by animateFloatAsState(
targetValue = dragFraction.coerceIn(0f, 1f),
animationSpec = spring(
dampingRatio = Spring.DampingRatioLowBouncy,
stiffness = Spring.StiffnessMedium
),
label = "ProgressAnimation"
)
val animatedOverscroll by animateFloatAsState(
targetValue = overscrollAmount,
animationSpec = spring(
dampingRatio = Spring.DampingRatioMediumBouncy,
stiffness = Spring.StiffnessMediumLow
),
label = "OverscrollAnimation"
)
val maxOverscrollEffect = (maxStretchFactor - 1f).coerceAtLeast(0f)
val stretchMultiplier = stretchSensitivity
val compressionMultiplier = compressionSensitivity
val overscrollDirection = sign(animatedOverscroll)
val totalStretchAmount = (min(maxOverscrollEffect, abs(animatedOverscroll) * stretchMultiplier) * baseSliderHeight.value).dp
val offsetY = if (abs(animatedOverscroll) > 0.001f) {
val asymmetricOffset = totalStretchAmount * (directionalStretchRatio - 0.5f)
(-overscrollDirection * asymmetricOffset.value).dp
} else {
0.dp
}
val heightStretch = baseSliderHeight + totalStretchAmount
val widthCompression = baseSliderWidth * max(
minCompressionFactor,
1f - min(1f - minCompressionFactor, abs(animatedOverscroll) * compressionMultiplier)
)
val dynamicCornerRadius = baseCornerRadius * (1f - min(cornerRadiusChangeFactor, abs(animatedOverscroll) * cornerRadiusChangeFactor * 2f))
Box(
modifier = modifier,
contentAlignment = Alignment.Center
) {
Box(
modifier = Modifier
.height(heightStretch)
.width(widthCompression)
.offset(y = offsetY)
.clip(RoundedCornerShape(dynamicCornerRadius))
.background(trackColor)
.pointerInput(Unit) {
detectTapGestures { offset ->
val newFraction = 1f - (offset.y / size.height).coerceIn(0f, 1f)
dragFraction = newFraction
rawDragPosition = newFraction
overscrollAmount = 0f
val newVolume = (newFraction * maxVolume).roundToInt()
onVolumeChange(newVolume)
}
}
.draggable(
orientation = Orientation.Vertical,
state = rememberDraggableState { delta ->
rawDragPosition -= (delta / baseHeightPx)
dragFraction = rawDragPosition.coerceIn(0f, 1f)
overscrollAmount = when {
rawDragPosition > 1f -> min(1.0f, (rawDragPosition - 1f) * 2.0f)
rawDragPosition < 0f -> max(-1.0f, rawDragPosition * 2.0f)
else -> 0f
}
val newVolume = (dragFraction * maxVolume).roundToInt()
onVolumeChange(newVolume)
},
onDragStarted = {
isDragging = true
dragFraction = displayFraction
rawDragPosition = displayFraction
overscrollAmount = 0f
onDragStateChange(true)
},
onDragStopped = {
isDragging = false
overscrollAmount = 0f
rawDragPosition = dragFraction
onDragStateChange(false)
}
),
contentAlignment = Alignment.BottomCenter
) {
Box(
modifier = Modifier
.fillMaxWidth()
.fillMaxHeight(animatedProgress)
.background(progressColor)
)
}
}
}
@@ -18,7 +18,7 @@
@file:OptIn(ExperimentalEncodingApi::class)
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(
@@ -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),
@@ -16,7 +16,7 @@
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package me.kavishdevar.librepods.presentation.components
package me.kavishdevar.librepods.presentation.components.apple
import android.content.res.Configuration
@@ -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
@@ -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) }
)
@@ -0,0 +1,125 @@
/*
LibrePods - AirPods liberated from Apples ecosystem
Copyright (C) 2025 LibrePods contributors
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
@file:OptIn(ExperimentalEncodingApi::class)
package me.kavishdevar.librepods.presentation.components.apple
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import me.kavishdevar.librepods.R
import me.kavishdevar.librepods.presentation.components.primitives.StyledList
import me.kavishdevar.librepods.presentation.components.primitives.StyledToggle
import kotlin.io.encoding.ExperimentalEncodingApi
@Composable
fun ConnectionSettings(
automaticEarDetectionEnabled: Boolean,
onAutomaticEarDetectionChanged: (Boolean) -> Unit,
automaticConnectionEnabled: Boolean,
onAutomaticConnectionChanged: (Boolean) -> Unit,
disconnectWhenNotWearing: Boolean,
onDisconnectWhenNotWearingChanged: (Boolean) -> Unit,
takeoverWhenDisconnected: Boolean,
onTakeoverWhenDisconnectedChanged: (Boolean) -> Unit,
takeoverWhenIdle: Boolean,
onTakeoverWhenIdleChanged: (Boolean) -> Unit,
takeoverWhenMusic: Boolean,
onTakeoverWhenMusicChanged: (Boolean) -> Unit,
takeoverWhenCall: Boolean,
onTakeoverWhenCallChanged: (Boolean) -> Unit,
takeoverWhenRingingCall: Boolean,
onTakeoverWhenRingingCallChanged: (Boolean) -> Unit,
takeoverWhenMediaStart: Boolean,
onTakeoverWhenMediaStartChanged: (Boolean) -> Unit,
isPremium: Boolean
) {
StyledList {
StyledToggle(
label = stringResource(R.string.ear_detection),
checked = automaticEarDetectionEnabled,
onCheckedChange = onAutomaticEarDetectionChanged
)
StyledToggle(
label = stringResource(R.string.automatically_connect),
description = stringResource(R.string.automatically_connect_description),
checked = automaticConnectionEnabled,
onCheckedChange = onAutomaticConnectionChanged
)
StyledToggle(
label = stringResource(R.string.disconnect_when_not_wearing),
description = stringResource(R.string.disconnect_when_not_wearing_description),
checked = disconnectWhenNotWearing,
onCheckedChange = onDisconnectWhenNotWearingChanged
)
}
// StyledList(title = stringResource(R.string.takeover_airpods_state)) {
// StyledToggle(
// label = stringResource(R.string.takeover_disconnected),
// description = stringResource(R.string.takeover_disconnected_desc),
// checked = takeoverWhenDisconnected,
// onCheckedChange = onTakeoverWhenDisconnectedChanged,
// enabled = isPremium
// )
// StyledToggle(
// label = stringResource(R.string.takeover_idle),
// description = stringResource(R.string.takeover_idle_desc),
// checked = takeoverWhenIdle,
// onCheckedChange = onTakeoverWhenIdleChanged,
// enabled = isPremium
// )
// StyledToggle(
// label = stringResource(R.string.takeover_music),
// description = stringResource(R.string.takeover_music_desc),
// checked = takeoverWhenMusic,
// onCheckedChange = onTakeoverWhenMusicChanged,
// enabled = isPremium
// )
//
// StyledToggle(
// label = stringResource(R.string.takeover_call),
// description = stringResource(R.string.takeover_call_desc),
// checked = takeoverWhenCall,
// onCheckedChange = onTakeoverWhenCallChanged,
// enabled = isPremium
// )
// }
//
// StyledList(title = stringResource(R.string.takeover_phone_state)) {
// StyledToggle(
// label = stringResource(R.string.takeover_ringing_call),
// description = stringResource(R.string.takeover_ringing_call_desc),
// checked = takeoverWhenRingingCall,
// onCheckedChange = onTakeoverWhenRingingCallChanged,
// enabled = isPremium
// )
// StyledToggle(
// label = stringResource(R.string.takeover_media_start),
// description = stringResource(R.string.takeover_media_start_desc),
// checked = takeoverWhenMediaStart,
// onCheckedChange = onTakeoverWhenMediaStartChanged,
// enabled = isPremium
// )
// }
}
@@ -18,11 +18,13 @@
@file:OptIn(ExperimentalEncodingApi::class)
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
)
}
@@ -16,7 +16,7 @@
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package me.kavishdevar.librepods.presentation.components
package me.kavishdevar.librepods.presentation.components.apple
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
@@ -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))
}
}
}
@@ -16,12 +16,14 @@
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package me.kavishdevar.librepods.presentation.components
package me.kavishdevar.librepods.presentation.components.apple
import androidx.compose.runtime.Composable
import androidx.compose.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,
)
@@ -16,12 +16,14 @@
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package me.kavishdevar.librepods.presentation.components
package me.kavishdevar.librepods.presentation.components.common
import androidx.compose.runtime.Composable
import androidx.compose.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,
)
}
}
@@ -0,0 +1,58 @@
package me.kavishdevar.librepods.presentation.components.common
import android.os.Build
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import me.kavishdevar.librepods.R
import me.kavishdevar.librepods.presentation.components.primitives.StyledList
import me.kavishdevar.librepods.presentation.components.primitives.StyledListItem
import me.kavishdevar.librepods.utils.XposedState
@Composable
fun DeviceInfoCard() {
StyledList(title = stringResource(R.string.device_info)) {
StyledListItem(
contentText = stringResource(R.string.manufacturer),
supportingText = Build.MANUFACTURER,
enabled = false
)
StyledListItem(
contentText = stringResource(R.string.model_number),
supportingText = Build.MODEL,
enabled = false
)
StyledListItem(
contentText = stringResource(R.string.build_id),
supportingText = Build.DISPLAY,
enabled = false
)
StyledListItem(
contentText = stringResource(R.string.version),
supportingText = "${Build.ID} (${Build.VERSION.SDK_INT_FULL})",
enabled = false
)
StyledListItem(
contentText = stringResource(R.string.xposed_available),
supportingText = if (XposedState.isAvailable) {
stringResource(R.string.yes)
} else {
stringResource(R.string.no)
},
enabled = false
)
StyledListItem(
contentText = stringResource(R.string.app_enabled_in_xposed),
supportingText = if (XposedState.bluetoothScopeEnabled) {
stringResource(R.string.yes)
} else {
stringResource(R.string.no)
},
enabled = false
)
}
}
@@ -1,4 +1,4 @@
package me.kavishdevar.librepods.presentation.components
package me.kavishdevar.librepods.presentation.components.primitives
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.foundation.layout.Box
@@ -16,7 +16,7 @@
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package me.kavishdevar.librepods.presentation.components
package me.kavishdevar.librepods.presentation.components.primitives
import android.graphics.RuntimeShader
import android.os.Build
@@ -16,7 +16,7 @@
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package me.kavishdevar.librepods.presentation.components
package me.kavishdevar.librepods.presentation.components.primitives
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.fadeIn
@@ -62,7 +62,7 @@ import me.kavishdevar.librepods.presentation.theme.LocalDesignSystem
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ConfirmationDialog(
fun StyledConfirmationDialog(
showDialog: MutableState<Boolean>,
title: String,
message: String,
@@ -16,7 +16,7 @@
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package me.kavishdevar.librepods.presentation.components
package me.kavishdevar.librepods.presentation.components.primitives
import android.content.res.Configuration.UI_MODE_NIGHT_NO
import android.content.res.Configuration.UI_MODE_NIGHT_YES
@@ -1,4 +1,4 @@
package me.kavishdevar.librepods.presentation.components
package me.kavishdevar.librepods.presentation.components.primitives
import androidx.compose.animation.core.animateDp
import androidx.compose.animation.core.animateDpAsState
@@ -1,4 +1,4 @@
package me.kavishdevar.librepods.presentation.components
package me.kavishdevar.librepods.presentation.components.primitives
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
@@ -16,7 +16,7 @@
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package me.kavishdevar.librepods.presentation.components
package me.kavishdevar.librepods.presentation.components.primitives
import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.animateFloatAsState
@@ -16,7 +16,7 @@
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package me.kavishdevar.librepods.presentation.components
package me.kavishdevar.librepods.presentation.components.primitives
import android.graphics.RenderEffect
import android.graphics.Shader
@@ -16,7 +16,7 @@
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package me.kavishdevar.librepods.presentation.components
package me.kavishdevar.librepods.presentation.components.primitives
import android.annotation.SuppressLint
import android.content.res.Configuration
@@ -16,7 +16,7 @@
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package me.kavishdevar.librepods.presentation.components
package me.kavishdevar.librepods.presentation.components.primitives
import android.content.res.Configuration
import androidx.compose.animation.animateColorAsState
@@ -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
@@ -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 -> {
@@ -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
@@ -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(),
@@ -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
@@ -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
@@ -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(
@@ -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
@@ -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
@@ -1,126 +0,0 @@
/*
LibrePods - AirPods liberated from Apples ecosystem
Copyright (C) 2025 LibrePods contributors
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package me.kavishdevar.librepods.presentation.screens.apple
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.asPaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.statusBars
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.kyant.backdrop.backdrops.layerBackdrop
import com.kyant.backdrop.backdrops.rememberLayerBackdrop
import kotlinx.coroutines.flow.debounce
import me.kavishdevar.librepods.R
import me.kavishdevar.librepods.bluetooth.aacp.types.ControlCommandIdentifier
import me.kavishdevar.librepods.presentation.components.StyledButton
import me.kavishdevar.librepods.presentation.components.StyledScaffold
import me.kavishdevar.librepods.presentation.components.StyledSlider
import me.kavishdevar.librepods.presentation.icons.LocalIcons
import me.kavishdevar.librepods.presentation.theme.DesignSystem
import me.kavishdevar.librepods.presentation.theme.LocalDesignSystem
import me.kavishdevar.librepods.presentation.viewmodel.AppleViewModel
import kotlin.time.Duration.Companion.milliseconds
@Composable
fun AdaptiveStrengthScreen(
viewModel: AppleViewModel,
navigateBack: (() -> Unit)?,
navigateToPurchase: () -> Unit
) {
val uiState by viewModel.uiState.collectAsState()
val state = uiState.state
val backdrop = rememberLayerBackdrop()
StyledScaffold(
title = stringResource(R.string.customize_adaptive_audio),
navigateBack = navigateBack
) { topPadding, bottomPadding ->
Column(
modifier = Modifier
.fillMaxSize()
.layerBackdrop(backdrop)
.padding(horizontal = 16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
Spacer(modifier = Modifier.height(topPadding))
if (!uiState.isPremium) {
StyledButton(
onClick = navigateToPurchase,
backdrop = rememberLayerBackdrop(),
modifier = Modifier.fillMaxWidth(),
maxScale = 0.05f,
surfaceColor = MaterialTheme.colorScheme.primary
) {
Text(
stringResource(R.string.unlock_advanced_features),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onPrimary
)
}
Spacer(modifier = Modifier.height(16.dp))
}
val sliderValue = remember {
mutableFloatStateOf(100f - (state.controlStates[ControlCommandIdentifier.AUTO_ANC_STRENGTH]?.getOrNull(0)?.toFloat() ?: 50f))
}
LaunchedEffect(sliderValue) {
snapshotFlow { sliderValue.floatValue }
.debounce(100.milliseconds)
.collect { value ->
viewModel.setControlCommand(
ControlCommandIdentifier.AUTO_ANC_STRENGTH,
byteArrayOf((100 - value).toInt().toByte())
)
}
}
StyledSlider(
value = sliderValue.floatValue,
onValueChange = { sliderValue.floatValue = it },
valueRange = 0f..100f,
snapPoints = listOf(0f, 50f, 100f),
startImageVector = LocalIcons.current.SpeakerMin,
endImageVector = LocalIcons.current.SpeakerMax,
independent = true,
description = stringResource(R.string.adaptive_audio_description),
enabled = uiState.isPremium
)
Spacer(modifier = Modifier.height(bottomPadding))
}
}
}
@@ -19,6 +19,11 @@
package me.kavishdevar.librepods.presentation.screens.apple
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 = {},
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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.",
@@ -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
@@ -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<Long>) -> Unit
setHrRange: (ClosedRange<Long>) -> 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()
@@ -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
@@ -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
)
},
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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(
@@ -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)
@@ -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)
}
@@ -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<Unit>()
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)
}
}
@@ -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()
}
}
}
@@ -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) {
+7 -1
View File
@@ -150,7 +150,7 @@
<string name="open_dialog_for_controlling">Open dialog for controlling</string>
<string name="open_dialog_for_controlling_description">If disabled, clicking on the QS will cycle through modes. If enabled, it will show a dialog for controlling noise control mode and conversational awareness</string>
<string name="disconnect_when_not_wearing">Disconnect AirPods when not wearing</string>
<string name="disconnect_when_not_wearing_description">You will still be able to control them with the app - this just disconnects the audio.</string>
<string name="disconnect_when_not_wearing_description">You will still be able to control them with the app; this just disconnects the audio.</string>
<string name="advanced_options">Advanced Options</string>
<string name="set_identity_resolving_key">Set Identity Resolving Key (IRK)</string>
<string name="set_identity_resolving_key_description">Manually set the IRK value used for resolving BLE random addresses</string>
@@ -340,4 +340,10 @@
<string name="use_highest_refresh_rate">Use highest refresh rate available</string>
<string name="debug_mode_description">Shows extra information and settings.</string>
<string name="interaction">Interaction</string>
<string name="heart_rate_monitoring">Heart Rate Monitoring</string>
<string name="hrm_alert_description">Sends a notification when your heart rate is above the threshold. Consumes more battery.</string>
<string name="heart_rate_alert">Heart rate alert</string>
<string name="heart_rate_alert_threshold">Heart rate threshold</string>
<string name="call_rejected">Call rejected</string>
<string name="call_accepted">Call accepted</string>
</resources>