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
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ import androidx.compose.ui.test.printToLog
import org.junit.Rule
import org.junit.Test
import tech.capullo.radio.snapcast.Client
import tech.capullo.radio.ui.RadioBroadcasterScreenContent
import tech.capullo.radio.ui.BroadcasterScreenContent
import tech.capullo.radio.ui.model.AudioChannel
import tech.capullo.radio.viewmodels.RadioBroadcasterUiState

class RadioBroadcasterEspotiConnectTest {
Expand All @@ -29,7 +30,10 @@ class RadioBroadcasterEspotiConnectTest {

// When: RadioBroadcasterScreen is displayed
composeTestRule.setContent {
RadioBroadcasterScreenContent(uiState)
BroadcasterScreenContent(
uiState,
onAudioChannelChange = { },
)
}
composeTestRule.onRoot().printToLog("TAG")

Expand Down Expand Up @@ -57,7 +61,10 @@ class RadioBroadcasterEspotiConnectTest {

// When: RadioBroadcasterScreen is displayed
composeTestRule.setContent {
RadioBroadcasterScreenContent(uiState)
BroadcasterScreenContent(
uiState,
onAudioChannelChange = { },
)
}
composeTestRule.onRoot().printToLog("TAG")

Expand All @@ -76,11 +83,15 @@ class RadioBroadcasterEspotiConnectTest {
val uiState = RadioBroadcasterUiState.EspotiPlayerReady(
hostAddresses = hostAddresses,
snapcastClients = mockClients,
audioChannel = AudioChannel.STEREO,
)

// When: Composable is displayed
composeTestRule.setContent {
RadioBroadcasterScreenContent(uiState)
BroadcasterScreenContent(
uiState,
onAudioChannelChange = { },
)
}

// Then: Broadcaster host addresses are displayed
Expand Down
25 changes: 14 additions & 11 deletions app/src/androidTest/java/tech/capullo/radio/TuneInScreen.kt
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import tech.capullo.radio.ui.AudioChannel
import tech.capullo.radio.ui.RadioTuneInScreenContent
import tech.capullo.radio.ui.TuneInScreenContent
import tech.capullo.radio.ui.model.AudioChannel

@RunWith(AndroidJUnit4::class)
class TuneInScreen {
Expand All @@ -24,13 +24,14 @@ class TuneInScreen {
@Test
fun testDefaultChannelSelection() {
val lastServerText = "192.168.0.1"
val isTunedIn = false
composeTestRule.setContent {
RadioTuneInScreenContent(
TuneInScreenContent(
lastServerText = lastServerText,
isTunedIn = isTunedIn,
onTextChange = {},
onTuneInClick = {},
isButtonEnabled = true,
selectedChannel = AudioChannel.STEREO,
onChannelChange = {},
)
}

Expand All @@ -51,13 +52,14 @@ class TuneInScreen {
@Test
fun testSameChannelSelection() {
val lastServerText = "192.168.0.1"
val isTunedIn = false
composeTestRule.setContent {
RadioTuneInScreenContent(
TuneInScreenContent(
lastServerText = lastServerText,
isTunedIn = isTunedIn,
onTextChange = {},
onTuneInClick = {},
isButtonEnabled = false,
selectedChannel = AudioChannel.STEREO,
onChannelChange = { },
)
}

Expand All @@ -82,15 +84,16 @@ class TuneInScreen {
@Test
fun testChannelSelectionRestoration() {
val lastServerText = "192.168.0.1"
val isTunedIn = false

val restorationTester = StateRestorationTester(composeTestRule)
restorationTester.setContent {
RadioTuneInScreenContent(
TuneInScreenContent(
lastServerText = lastServerText,
isTunedIn = isTunedIn,
onTextChange = {},
onTuneInClick = {},
isButtonEnabled = false,
selectedChannel = AudioChannel.RIGHT,
onChannelChange = { },
)
}

Expand Down
16 changes: 7 additions & 9 deletions app/src/main/java/tech/capullo/radio/RadioNavHost.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@ import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.remember
import androidx.navigation3.runtime.NavEntry
import androidx.navigation3.ui.NavDisplay
import tech.capullo.radio.ui.BroadcasterScreen
import tech.capullo.radio.ui.NowPlayingScreen
import tech.capullo.radio.ui.RadioApp
import tech.capullo.radio.ui.RadioBroadcasterScreen
import tech.capullo.radio.ui.RadioTuneInScreen
import tech.capullo.radio.ui.RadioHomeScreen
import tech.capullo.radio.ui.TuneInScreen
import tech.capullo.radio.ui.theme.RadioTheme
import tech.capullo.radio.ui.theme.SchemeChoice

Expand All @@ -26,7 +26,7 @@ fun RadioCapulloNavHost() {
entryProvider = { key ->
when (key) {
is Home -> NavEntry(key) {
RadioApp(
RadioHomeScreen(
onStartBroadcastingClicked = { backStack.add(Broadcast) },
onTuneInClicked = { backStack.add(TuneIn) },
)
Expand All @@ -36,18 +36,16 @@ fun RadioCapulloNavHost() {
RadioTheme(
schemeChoice = SchemeChoice.GREEN,
) {
RadioBroadcasterScreen()
BroadcasterScreen()
}
}

is TuneIn -> NavEntry(key) {
RadioTheme(
schemeChoice = SchemeChoice.ORANGE,
) {
RadioTuneInScreen(
onConnected = { serverIp, channel ->
backStack.add(NowPlaying)
},
TuneInScreen(
onConnected = { backStack.add(NowPlaying) },
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,8 @@ import androidx.core.app.NotificationCompat
import androidx.core.app.ServiceCompat
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
Expand All @@ -29,6 +26,7 @@ import tech.capullo.radio.espoti.EspotiPlayerManager
import tech.capullo.radio.espoti.EspotiSessionRepository
import tech.capullo.radio.snapcast.SnapclientProcess
import tech.capullo.radio.snapcast.SnapserverProcess
import tech.capullo.radio.ui.model.AudioChannel
import xyz.gianlu.librespot.audio.MetadataWrapper
import xyz.gianlu.librespot.core.Session
import xyz.gianlu.librespot.metadata.PlayableId
Expand All @@ -53,16 +51,21 @@ class RadioBroadcasterService : Service() {
private val playbackExecutor: ExecutorService = Executors.newSingleThreadExecutor()
private var player: Player? = null
private var session: Session? = null
private val _isPlayerLoading = MutableStateFlow(true)
val isPlayerLoading = _isPlayerLoading.asStateFlow()
private val _isPlayerLoadingFlow = MutableStateFlow(true)
val isPlayerLoadingFlow = _isPlayerLoadingFlow.asStateFlow()

private val scope = CoroutineScope(Dispatchers.IO + Job())
private lateinit var snapserverJob: Deferred<Unit>
private lateinit var snapclientJob: Deferred<Unit>

private var snapserverJob: Job? = null
private var snapclientJob: Job? = null

private var currentAudioChannel = AudioChannel.STEREO

private val binder = LocalBinder()
inner class LocalBinder : Binder() {
fun getService(): RadioBroadcasterService = this@RadioBroadcasterService
fun getIsPlayerLoadingFlow() = this@RadioBroadcasterService.isPlayerLoadingFlow
fun updateAudioChannel(channel: AudioChannel) =
this@RadioBroadcasterService.updateAudioChannel(channel)
}

private fun runOnPlayback(func: () -> Unit): Future<*>? = playbackExecutor.submit(func)
Expand Down Expand Up @@ -207,12 +210,12 @@ class RadioBroadcasterService : Service() {
}

override fun onStartedLoading(player: Player) {
_isPlayerLoading.value = true
_isPlayerLoadingFlow.value = true
println("started loading")
}

override fun onFinishedLoading(player: Player) {
_isPlayerLoading.value = false
_isPlayerLoadingFlow.value = false
println("finished loading")
}
}
Expand Down Expand Up @@ -253,11 +256,16 @@ class RadioBroadcasterService : Service() {
}

fun startSnapcast() {
scope.launch {
snapserverJob = async { snapserverProcess.start() }
snapclientJob = async { snapclientProcess.start() }
awaitAll(snapclientJob, snapserverJob)
}
snapserverJob = scope.launch { snapserverProcess.start() }
snapclientJob = scope.launch { snapclientProcess.start() }
}

fun updateAudioChannel(channel: AudioChannel) {
currentAudioChannel = channel
// Restart snapclient with new channel
snapclientJob?.cancel()
snapclientJob =
scope.launch { snapclientProcess.start(audioChannel = currentAudioChannel.ordinal) }
}

companion object {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import tech.capullo.radio.snapcast.SnapclientProcess
import tech.capullo.radio.ui.AudioChannel
import tech.capullo.radio.ui.model.AudioChannel
import javax.inject.Inject

@AndroidEntryPoint
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.ensureActive
import tech.capullo.radio.data.RadioRepository
import tech.capullo.radio.ui.AudioChannel
import tech.capullo.radio.ui.model.AudioChannel
import java.io.BufferedReader
import java.io.InputStreamReader
import java.util.UUID
Expand Down
80 changes: 80 additions & 0 deletions app/src/main/java/tech/capullo/radio/ui/AudioSettingsDialog.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package tech.capullo.radio.ui

import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.size
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.ButtonGroupDefaults
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.ToggleButton
import androidx.compose.material3.ToggleButtonDefaults
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import tech.capullo.radio.ui.model.AudioChannel

@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
fun AudioSettingsDialog(
onDismissRequest: () -> Unit,
selectedChannel: AudioChannel,
onCheckedChanged: (Boolean, AudioChannel) -> Unit,
) {
AlertDialog(
onDismissRequest = onDismissRequest,
title = { Text("Audio Channel Settings") },
text = {
Row(
horizontalArrangement = Arrangement.spacedBy(
ButtonGroupDefaults.ConnectedSpaceBetween,
),
) {
AudioChannel.entries.forEach { channel ->
ToggleButton(
checked = selectedChannel == channel,
onCheckedChange = { isChecked -> onCheckedChanged(isChecked, channel) },
modifier = Modifier.Companion.weight(channel.modifierWeight),
shapes =
when (channel) {
AudioChannel.LEFT -> {
ButtonGroupDefaults.connectedLeadingButtonShapes()
}

AudioChannel.RIGHT -> {
ButtonGroupDefaults.connectedTrailingButtonShapes()
}

AudioChannel.STEREO -> {
ButtonGroupDefaults.connectedMiddleButtonShapes()
}
},
contentPadding = PaddingValues(0.dp),
) {
Icon(
if (selectedChannel == channel) {
channel.selectedIcon
} else {
channel.unselectedIcon
},
contentDescription = channel.label,
)
Spacer(Modifier.Companion.size(ToggleButtonDefaults.IconSpacing))
Text(
text = channel.label,
)
}
}
}
},
confirmButton = {
TextButton(onClick = onDismissRequest) {
Text("Dismiss")
}
},
)
}
Loading