Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
3.2.5-rc0 30205000
3.3.0-rc0 30300000
Original file line number Diff line number Diff line change
Expand Up @@ -198,9 +198,14 @@ class BluetoothRepo @Inject constructor(
fun renameDevice(address: DeviceAddr, newName: String): Boolean {
val realDevice = bluetoothAdapter.getRemoteDevice(address) ?: return false
return if (hasApiLevel(31)) {
@Suppress("NewApi")
realDevice.alias = newName
true
try {
@Suppress("NewApi")
realDevice.alias = newName
true
} catch (e: SecurityException) {
log(TAG, ERROR) { "Failed to set alias (no CDM association): ${e.asLog()}" }
false
}
} else {
try {
val method = realDevice.javaClass.getMethod("setAlias", String::class.java)
Expand Down
11 changes: 10 additions & 1 deletion app/src/main/java/eu/darken/bluemusic/devices/core/DeviceRepo.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import eu.darken.bluemusic.bluetooth.core.BluetoothRepo
import eu.darken.bluemusic.bluetooth.core.SourceDevice
import eu.darken.bluemusic.common.coroutine.AppScope
import eu.darken.bluemusic.common.coroutine.DispatcherProvider
import eu.darken.bluemusic.common.debug.logging.Logging.Priority.INFO
import eu.darken.bluemusic.common.debug.logging.Logging.Priority.VERBOSE
import eu.darken.bluemusic.common.debug.logging.log
import eu.darken.bluemusic.common.debug.logging.logTag
import eu.darken.bluemusic.common.flow.replayingShare
Expand Down Expand Up @@ -72,6 +74,7 @@ class DeviceRepo @Inject constructor(
suspend fun updateDevice(address: DeviceAddr, update: (DeviceConfigEntity) -> DeviceConfigEntity) {
withContext(dispatcherProvider.IO) {
var before = deviceDatabase.devices.getDevice(address)
val isNew = before == null

if (before == null) {
log(TAG) { "Device not found for update: $address. Creating new." }
Expand All @@ -81,7 +84,13 @@ class DeviceRepo @Inject constructor(
val updated = update(before)
deviceDatabase.devices.updateDevice(updated)

log(TAG) { "Updated device config: $address" }
if (isNew) {
log(TAG, INFO) { "New device config: $updated" }
} else if (before != updated) {
log(TAG) { "Updated device config $address, before: $before" }
} else {
log(TAG, VERBOSE) { "Device config unchanged: $address" }
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,9 @@ data class ManagedDevice(
private val defaultMonitoringDuration: Duration = Duration.ofSeconds(4)
private val defaultAdjustmentDelay: Duration = Duration.ofMillis(250)

fun toCompactString(): String =
"ManagedDevice($address/$label, active=$isActive, connected=$isConnected)"

override fun toString(): String {
return "ManagedDevice(isActive=$isActive, isConnected=$isConnected, isEnabled=$isEnabled, address=$address, last=$lastConnected, config=$config)"
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,4 +93,29 @@ data class DeviceConfigEntity(

@ColumnInfo(name = "connection_alert_sound_uri")
val connectionAlertSoundUri: String? = null,
)
) {

fun toCompactString(): String = buildString {
append("Config($address")
if (customName != null) append(", name=$customName")
if (musicVolume != null) append(", music=$musicVolume")
if (callVolume != null) append(", call=$callVolume")
if (ringVolume != null) append(", ring=$ringVolume")
if (notificationVolume != null) append(", notif=$notificationVolume")
if (alarmVolume != null) append(", alarm=$alarmVolume")
if (volumeLock) append(", lock")
if (volumeObserving) append(", observing")
if (volumeRateLimiter) append(", rateLimiter")
if (volumeSaveOnDisconnect) append(", saveOnDisconnect")
if (keepAwake) append(", keepAwake")
if (nudgeVolume) append(", nudge")
if (autoplay) append(", autoplay")
if (launchPkgs.isNotEmpty()) append(", launch=$launchPkgs")
if (showHomeScreen) append(", showHome")
if (!isEnabled) append(", DISABLED")
if (dndMode != null) append(", dnd=$dndMode")
if (connectionAlertType != AlertType.NONE) append(", alert=$connectionAlertType")
append(")")
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.material3.AlertDialog
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
Expand All @@ -16,6 +18,7 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import eu.darken.bluemusic.R
Expand All @@ -38,6 +41,13 @@ fun RenameDialog(
value = name,
onValueChange = { name = it },
singleLine = true,
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
keyboardActions = KeyboardActions(onDone = {
if (name.isNotBlank()) {
onConfirm(name)
onDismiss()
}
}),
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(16.dp))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,12 @@ fun VolumeInputDialog(
modifier = Modifier.padding(top = 4.dp),
)
}
Text(
text = stringResource(R.string.devices_volume_input_caption),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(top = 8.dp),
)
}
},
confirmButton = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import eu.darken.bluemusic.common.navigation.NavigationController
import eu.darken.bluemusic.common.ui.ViewModel4
import eu.darken.bluemusic.common.upgrade.UpgradeRepo
import eu.darken.bluemusic.devices.core.DevicesSettings
import eu.darken.bluemusic.monitor.core.service.MonitorControl
import kotlinx.coroutines.flow.combine
import javax.inject.Inject

Expand All @@ -20,6 +21,7 @@ constructor(
dispatcherProvider: DispatcherProvider,
navCtrl: NavigationController,
private val devicesSettings: DevicesSettings,
private val monitorControl: MonitorControl,
upgradeRepo: UpgradeRepo,
) : ViewModel4(dispatcherProvider, logTag("Settings", "Devices", "ViewModel"), navCtrl) {

Expand All @@ -43,6 +45,9 @@ constructor(
fun onToggleEnabled(enabled: Boolean) = launch {
log(tag) { "onToggleEnabled($enabled)" }
devicesSettings.setEnabled(enabled)
if (enabled) {
monitorControl.startMonitor(forceStart = true)
}
}

fun onToggleRestoreOnBoot(enabled: Boolean) = launch {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ class VolumeTool @Inject constructor(

internal var clock: () -> Long = System::currentTimeMillis

@Volatile private var adjusting = false
@Volatile private var adjustingStream: AudioStream.Id? = null
private val lock = Mutex()
private val lastUs = ConcurrentHashMap<AudioStream.Id, RecentWrite>()

Expand Down Expand Up @@ -72,7 +72,7 @@ class VolumeTool @Inject constructor(
private suspend fun setVolume(streamId: AudioStream.Id, volume: Int, flags: Int) = lock.withLock {
log(TAG, VERBOSE) { "setVolume(streamId=$streamId, volume=$volume, flags=$flags)." }
try {
adjusting = true
adjustingStream = streamId
val now = clock()
val write = RecentWrite(volume, now)
lastUs[streamId] = write
Expand All @@ -85,12 +85,15 @@ class VolumeTool @Inject constructor(

delay(10)
} finally {
adjusting = false
adjustingStream = null
}
}

fun wasUs(id: AudioStream.Id, volume: Int): Boolean {
if (adjusting) return true
val currentlyAdjusting = adjustingStream
if (currentlyAdjusting != null) {
if (currentlyAdjusting == id || mirroredPeer(currentlyAdjusting) == id) return true
}
val entry = lastUs[id] ?: return false
return entry.volume == volume && (clock() - entry.timestamp) < WRITE_TTL_MS
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,7 @@ package eu.darken.bluemusic.monitor.core.modules

interface ConnectionModule : EventModule {
suspend fun handle(event: DeviceEvent)

val cancellable: Boolean
get() = true
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package eu.darken.bluemusic.monitor.core.modules

import eu.darken.bluemusic.devices.core.DeviceAddr
import eu.darken.bluemusic.devices.core.ManagedDevice
import eu.darken.bluemusic.monitor.core.ownership.DisconnectResult
import eu.darken.bluemusic.monitor.core.service.BluetoothEventQueue.VolumeSnapshot

interface DeviceEvent {
Expand All @@ -13,10 +14,15 @@ interface DeviceEvent {

data class Connected(
override val device: ManagedDevice,
) : DeviceEvent
) : DeviceEvent {
override fun toString(): String = "Connected(${device.toCompactString()})"
}

data class Disconnected(
override val device: ManagedDevice,
val volumeSnapshot: VolumeSnapshot? = null,
) : DeviceEvent
val disconnectResult: DisconnectResult? = null,
) : DeviceEvent {
override fun toString(): String = "Disconnected(${device.toCompactString()}, disconnectResult=$disconnectResult)"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import eu.darken.bluemusic.monitor.core.audio.VolumeObserver
import eu.darken.bluemusic.monitor.core.audio.VolumeTool
import eu.darken.bluemusic.monitor.core.modules.ConnectionModule
import eu.darken.bluemusic.monitor.core.modules.volume.VolumeObservationGate
import eu.darken.bluemusic.devices.core.DeviceRepo
import eu.darken.bluemusic.monitor.core.ownership.AudioStreamOwnerRegistry
import javax.inject.Inject
import javax.inject.Singleton

Expand All @@ -18,7 +20,9 @@ class AlarmVolumeModule @Inject constructor(
volumeTool: VolumeTool,
volumeObserver: VolumeObserver,
observationGate: VolumeObservationGate,
) : BaseVolumeModule(volumeTool, volumeObserver, observationGate) {
ownerRegistry: AudioStreamOwnerRegistry,
deviceRepo: DeviceRepo,
) : BaseVolumeModule(volumeTool, volumeObserver, observationGate, ownerRegistry, deviceRepo) {

override val type: AudioStream.Type = AudioStream.Type.ALARM

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ import eu.darken.bluemusic.common.debug.logging.Logging.Priority.INFO
import eu.darken.bluemusic.common.debug.logging.Logging.Priority.VERBOSE
import eu.darken.bluemusic.common.debug.logging.log
import eu.darken.bluemusic.common.debug.logging.logTag
import eu.darken.bluemusic.devices.core.DeviceRepo
import eu.darken.bluemusic.devices.core.ManagedDevice
import eu.darken.bluemusic.devices.core.getDevice
import eu.darken.bluemusic.monitor.core.audio.AudioStream
import eu.darken.bluemusic.monitor.core.audio.VolumeMode
import eu.darken.bluemusic.monitor.core.audio.VolumeMode.Companion.fromFloat
Expand All @@ -15,6 +17,7 @@ import eu.darken.bluemusic.monitor.core.modules.ConnectionModule
import eu.darken.bluemusic.monitor.core.modules.DeviceEvent
import eu.darken.bluemusic.monitor.core.modules.delayForReactionDelay
import eu.darken.bluemusic.monitor.core.modules.volume.VolumeObservationGate
import eu.darken.bluemusic.monitor.core.ownership.AudioStreamOwnerRegistry
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.takeWhile
Expand All @@ -24,6 +27,8 @@ abstract class BaseVolumeModule(
private val volumeTool: VolumeTool,
private val volumeObserver: VolumeObserver,
private val observationGate: VolumeObservationGate,
private val ownerRegistry: AudioStreamOwnerRegistry,
private val deviceRepo: DeviceRepo,
) : ConnectionModule {

abstract val type: AudioStream.Type
Expand All @@ -46,21 +51,37 @@ abstract class BaseVolumeModule(
return
}

val generationAtStart = ownerRegistry.ownershipGeneration()

val streamId = device.getStreamId(type)
observationGate.suppress(streamId)
val token = observationGate.suppress(streamId)
try {
delayForReactionDelay(event)

setInitial(device, volumeMode)
if (ownerRegistry.ownershipGeneration() != generationAtStart) {
log(tag, INFO) { "Ownership changed during actionDelay, yielding" }
return
}

val freshDevice = deviceRepo.getDevice(device.address) ?: run {
log(tag, INFO) { "Device ${device.address} no longer exists after delay, yielding" }
return
}
val freshVolumeMode = fromFloat(freshDevice.getVolume(type)) ?: run {
log(tag, INFO) { "Device ${device.address} volume no longer configured after delay, yielding" }
return
}

setInitial(freshDevice, freshVolumeMode)

monitor(device, volumeMode)
monitor(freshDevice, freshVolumeMode, generationAtStart)
} finally {
observationGate.unsuppress(streamId)
observationGate.unsuppress(token)
}
}

protected open suspend fun setInitial(device: ManagedDevice, volumeMode: VolumeMode) {
log(tag, INFO) { "Setting initial volume ($volumeMode) for $device" }
log(tag, INFO) { "Setting initial volume ($volumeMode) for ${device.address}/${device.label}" }

// Default implementation only handles normal volumes
if (volumeMode !is VolumeMode.Normal) {
Expand Down Expand Up @@ -111,7 +132,11 @@ abstract class BaseVolumeModule(
* setInitial even runs), setInitial will overwrite them with the connect-time
* snapshot. Fixing that would require re-reading DeviceRepo after the delay.
*/
protected open suspend fun monitor(device: ManagedDevice, volumeMode: VolumeMode) {
protected open suspend fun monitor(
device: ManagedDevice,
volumeMode: VolumeMode,
generationAtStart: Long = -1L,
) {
if (volumeMode !is VolumeMode.Normal) {
log(tag) { "Special volume mode $volumeMode not supported in base monitoring" }
return
Expand All @@ -121,16 +146,21 @@ abstract class BaseVolumeModule(
val targetPercentage = volumeMode.percentage
val targetLevel = percentageToLevel(targetPercentage, volumeTool.getMinVolume(streamId), volumeTool.getMaxVolume(streamId))

log(tag, INFO) { "Monitoring volume (target=$volumeMode, level=$targetLevel) for $device" }
log(tag, INFO) { "Monitoring volume (target=$volumeMode, level=$targetLevel) for ${device.address}/${device.label}" }

// Set to true inside collect to exit cleanly via takeWhile on the next element.
var yielded = false
withTimeoutOrNull(device.monitoringDuration.toMillis()) {
volumeObserver.volumes
.filter { it.streamId == streamId }
.filter { it.newVolume != targetLevel }
.takeWhile { !yielded }
.collect { event ->
if (generationAtStart >= 0 && ownerRegistry.ownershipGeneration() != generationAtStart) {
log(tag, INFO) { "Monitor($type) yielding, ownership changed" }
yielded = true
return@collect
}

if (!volumeTool.wasUs(streamId, targetLevel)) {
log(tag, INFO) {
"Monitor($type) yielding to external VolumeTool write on $device"
Expand Down
Loading