EARHORNMUSIC UPnP · Android TV · Phones · Android Auto · GNU/Linux
Release 0.5.0

CastIntegration.kt

package dev.stave.cast

import android.content.Context
import dagger.hilt.android.qualifiers.ApplicationContext
import dev.stave.domain.model.DidlItem
import dev.stave.domain.renderer.RepeatMode
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import javax.inject.Inject
import javax.inject.Singleton

/**
 * Libre flavor: Google Cast is compiled out. This class exists so common code
 * can depend on [CastIntegration] unconditionally. All methods are no-ops;
 * [isCasting] is always false.
 */
@Singleton
class CastIntegration @Inject constructor(
    @Suppress("UnusedPrivateProperty") @ApplicationContext private val context: Context,
) {
    val available: Boolean = false

    private val _devices = MutableStateFlow<List<CastDevice>>(emptyList())
    val devices: StateFlow<List<CastDevice>> = _devices.asStateFlow()

    private val _session = MutableStateFlow<CastSession?>(null)
    val session: StateFlow<CastSession?> = _session.asStateFlow()

    val isCasting: StateFlow<Boolean> = MutableStateFlow(false).asStateFlow()

    // Match play-flavor surface so common code (LocalRenderer) can reference
    // these flows unconditionally without a flavor check. All three are
    // permanently zero/false in libre since no session can ever start.
    val castIsPlaying: StateFlow<Boolean> = MutableStateFlow(false).asStateFlow()
    val castPositionMs: StateFlow<Long> = MutableStateFlow(0L).asStateFlow()
    val castDurationMs: StateFlow<Long> = MutableStateFlow(0L).asStateFlow()
    val castCurrentItemIndex: StateFlow<Int> = MutableStateFlow(-1).asStateFlow()
    val userInitiatedDisconnect: StateFlow<Boolean> = MutableStateFlow(false).asStateFlow()
    fun consumeDisconnectIntent(): Boolean = false

    // Diagnostic mirror of the play-flavor surface so shared code (CastPickerSheet)
    // can render the same diagnostic UI in both flavors. In libre, the diagnostic
    // always reports "compiled out" — which at least makes it clear to the user
    // why the picker is empty.
    val diagnostic: StateFlow<DiagnosticState> = MutableStateFlow(
        DiagnosticState(
            castContextOk = false,
            mediaRouterOk = false,
            playServices = "libre build — Cast compiled out",
            rawRouteCount = 0,
            filteredDeviceCount = 0,
            tick = 0,
            rawRouteNames = emptyList(),
        )
    ).asStateFlow()

    fun loadTrack(track: DidlItem.Track, autoplay: Boolean = true) = Unit
    fun loadQueue(
        tracks: List<DidlItem.Track>,
        startIndex: Int = 0,
        autoplay: Boolean = true,
        repeat: RepeatMode = RepeatMode.OFF,
        shuffle: Boolean = false,
    ) = Unit
    fun setRepeatAndShuffle(repeat: RepeatMode, shuffle: Boolean) = Unit
    fun queueAppend(tracks: List<DidlItem.Track>, fullQueue: List<DidlItem.Track> = tracks) = Unit
    fun queueInsertAfterCurrent(tracks: List<DidlItem.Track>) = Unit
    fun queueRemoveAt(index: Int) = Unit
    fun queueMove(from: Int, to: Int) = Unit
    fun consumeAttachWasResume(): Boolean = false
    fun currentContentId(): String? = null
    fun detachWithoutStopping() = Unit
    fun play() = Unit
    fun pause() = Unit
    fun stop() = Unit
    fun seekTo(positionMs: Long) = Unit
    fun queueNext() = Unit
    fun queuePrev() = Unit
    fun queueJumpTo(index: Int) = Unit
    // Maintainer, 2026-10-29: mirror of the play flavour's wait-then-jump (D4); no-op here.
    suspend fun queueJumpToWhenReady(index: Int) = Unit
    fun setVolume(volume: Double) = Unit
    fun getDeviceVolume(): Double = 1.0
    fun setDeviceVolume(volume: Double) = Unit

    fun startDiscovery() = Unit
    fun stopDiscovery() = Unit
    fun connect(deviceId: String) = Unit
    fun disconnect() = Unit
    fun requestStatusRefresh() = Unit
}

data class CastDevice(
    val id: String,
    val friendlyName: String,
    val isGroup: Boolean,
)

data class CastSession(
    val deviceId: String,
    val deviceName: String,
)

data class DiagnosticState(
    val castContextOk: Boolean = false,
    val castInitError: String? = null,
    val mediaRouterOk: Boolean = false,
    val playServices: String = "unknown",
    val rawRouteCount: Int = 0,
    val filteredDeviceCount: Int = 0,
    val tick: Int = 0,
    val rawRouteNames: List<String> = emptyList(),
)