Domain Layer

April 4, 2026 · View on GitHub

Business logic, domain models, repository contracts, and services. Pure Kotlin — no Android framework dependencies.

Package: com.cadnative.firevisioniptv.domain

Package Structure

domain/
├── model/          # Domain models (pure data classes and enums)
├── repository/     # Repository interfaces (contracts for data layer)
├── service/        # Long-running background services
└── usecase/        # Single-responsibility business operations

Domain Models

Channel

domain/model/Channel.kt

Core business entity representing a TV channel.

FieldTypeDescription
idStringUnique channel identifier
nameStringDisplay name
streamUrlStringHLS/DASH streaming URL
logoUrlString?Channel logo image URL
categoryStringCategory name (e.g., "Sports", "News")
languageString?Language code
countryString?Country code
isFavoriteBooleanWhether the user has favorited this channel (default: false)

Category

domain/model/Category.kt

FieldTypeDescription
idStringCategory identifier
nameStringDisplay name
channelCountIntNumber of channels in this category

ChannelHealthStatus

domain/model/ChannelHealthStatus.kt

Enum representing channel availability:

ValueDescription
UNKNOWNNot yet checked
CHECKINGCurrently being scanned
ONLINEStream is reachable
OFFLINEStream is unreachable

PlaybackState

domain/model/PlaybackState.kt

FieldTypeDescription
channelIdStringAssociated channel
positionLongCurrent position in milliseconds
durationLongTotal duration in milliseconds
isPlayingBooleanWhether playback is active

EpgProgram

domain/model/EpgProgram.kt

EPG (Electronic Program Guide) entry for a channel.

FieldTypeDescription
channelEpgIdStringTVG ID matching the channel
titleStringProgram title (e.g., "News at 9")
descriptionString?Program description
startTimeInstantProgram start time
endTimeInstantProgram end time
iconString?Program artwork URL

SearchFilter

domain/model/SearchFilter.kt

Sealed class for composable search filters:

SubclassFieldDescription
ByCategorycategory: StringFilter by category name
ByLanguagelanguage: StringFilter by language code
ByCountrycountry: StringFilter by country code
Combinedfilters: List<SearchFilter>AND-combine multiple filters

Repository Interfaces

All repository interfaces define the contract that the data layer must implement. They use Flow<Result<T>> for reactive reads and suspend fun returning Result<T> for writes.

ChannelRepository

domain/repository/ChannelRepository.kt

fun getChannels(): Flow<Result<List<Channel>>>
fun getChannelById(id: String): Flow<Result<Channel>>
fun getChannelsByCategory(category: String): Flow<Result<List<Channel>>>
fun searchChannels(query: String): Flow<Result<List<Channel>>>
suspend fun refreshChannels(): Result<Unit>
suspend fun addToFavorites(channelId: String): Result<Unit>
suspend fun removeFromFavorites(channelId: String): Result<Unit>
fun getFavoriteChannels(): Flow<Result<List<Channel>>>

CategoryRepository

domain/repository/CategoryRepository.kt

fun getCategories(): Flow<Result<List<Category>>>
fun getCategoryById(id: String): Flow<Result<Category>>
suspend fun refreshCategories(): Result<Unit>

FavoriteRepository

domain/repository/FavoriteRepository.kt

fun getFavoriteChannels(): Flow<Result<List<Channel>>>
fun isFavorite(channelId: String): Flow<Result<Boolean>>
suspend fun addFavorite(channelId: String): Result<Unit>
suspend fun removeFavorite(channelId: String): Result<Unit>
suspend fun toggleFavorite(channelId: String): Result<Unit>
suspend fun updateFavoriteOrder(channelId: String, newOrder: Int): Result<Unit>
suspend fun syncFavorites(): Result<Unit>

PlaybackRepository

domain/repository/PlaybackRepository.kt

fun getPlaybackPosition(channelId: String): Flow<Result<Long?>>
suspend fun savePlaybackPosition(channelId: String, position: Long, duration: Long): Result<Unit>
suspend fun deletePlaybackPosition(channelId: String): Result<Unit>
fun getAllPlaybackPositions(): Flow<Result<Map<String, Long>>>
suspend fun clearOldPositions(keepCount: Int = 100): Result<Unit>

PlaylistRepository

domain/repository/PlaylistRepository.kt

suspend fun parsePlaylistFromUrl(url: String): Result<List<Channel>>
suspend fun parsePlaylistFromString(content: String): Result<List<Channel>>
suspend fun importChannels(channels: List<Channel>): Result<Unit>

SearchHistoryRepository

domain/repository/SearchHistoryRepository.kt

fun getRecentSearches(limit: Int = 10): Flow<Result<List<String>>>
suspend fun saveSearch(query: String): Result<Unit>
suspend fun clearHistory(): Result<Unit>
suspend fun removeSearch(query: String): Result<Unit>

EpgRepository

domain/repository/EpgRepository.kt

Provides EPG (Electronic Program Guide) data — "Now" and "Next" program info for channels.

suspend fun ensureLoaded()
suspend fun getNowNext(tvgId: String): Pair<EpgProgram?, EpgProgram?>
fun getNowNextIfCached(tvgId: String): Pair<EpgProgram?, EpgProgram?>?
  • ensureLoaded() — Fetches EPG data from the server if not already cached. Called once during ViewModel init.
  • getNowNext(tvgId) — Suspend. Triggers load if needed, then returns current and next program.
  • getNowNextIfCached(tvgId)Non-suspend, non-blocking. Returns null if EPG not yet loaded (caller skips enrichment). Returns Pair(null, null) if loaded but no programs found for this tvgId. Used by ChannelsViewModel.enrichWithEpgIfReady() to avoid blocking channel display while EPG loads.

UserPreferencesRepository

domain/repository/UserPreferencesRepository.kt

fun getTheme(): Flow<String>
suspend fun setTheme(theme: String): Result<Unit>
fun getGridSize(): Flow<Int>
suspend fun setGridSize(size: Int): Result<Unit>
fun getFontSize(): Flow<Float>
suspend fun setFontSize(scale: Float): Result<Unit>
fun getAnimationSpeed(): Flow<Float>
suspend fun setAnimationSpeed(speed: Float): Result<Unit>
fun getLayoutDensity(): Flow<String>
suspend fun setLayoutDensity(density: String): Result<Unit>
suspend fun clearCache(): Result<Unit>

Use Cases

All use cases follow one of two base class patterns:

Base Classes

UseCase<P, R> — For one-shot suspend operations:

abstract class UseCase<in P, out R> {
    suspend operator fun invoke(params: P): R
    protected abstract suspend fun execute(params: P): R
}

FlowUseCase<P, R> — For reactive streaming operations:

abstract class FlowUseCase<in P, out R> {
    operator fun invoke(params: P): Flow<R>
    protected abstract fun execute(params: P): Flow<R>
}

Concrete Use Cases

Use CaseBaseInputOutputDescription
GetChannelsUseCaseFlowUseCaseUnitFlow<Result<List<Channel>>>Get all channels reactively
GetChannelByIdUseCaseFlowUseCaseStringFlow<Result<Channel>>Get single channel by ID
GetChannelsByCategoryUseCaseFlowUseCaseStringFlow<Result<List<Channel>>>Get channels filtered by category
RefreshChannelsUseCaseUseCaseUnitResult<Unit>Trigger remote refresh
SearchChannelsUseCaseFlowUseCaseParams(query, filters)Flow<Result<List<Channel>>>Search with filters (AND logic)
GetRecentSearchesUseCaseFlowUseCaseIntFlow<Result<List<String>>>Get recent search queries
SaveSearchQueryUseCaseUseCaseStringResult<Unit>Save query to history
ClearSearchHistoryUseCaseUseCaseUnitResult<Unit>Clear all search history
GetFavoriteChannelsUseCaseFlowUseCaseUnitFlow<Result<List<Channel>>>Get favorites reactively
ToggleFavoriteUseCaseUseCaseStringResult<Unit>Toggle channel favorite status
ReorderFavoritesUseCaseUseCaseParams(channelId, newOrder)Result<Unit>Update favorite display order
GetPlaybackPositionUseCaseFlowUseCaseStringFlow<Result<PlaybackState?>>Get saved playback position
SavePlaybackPositionUseCaseUseCaseParams(channelId, position, duration)Result<Unit>Save playback position

Services

ChannelHealthScanner

domain/service/ChannelHealthScanner.kt

Long-running service that periodically checks whether channel streams are reachable.

Scan cycle:

  1. Full health scan of all channels (batches of 4, 6s timeout per stream)
  2. 5-minute delay
  3. Thumbnail extraction for ONLINE channels
  4. 30-minute cooldown
  5. Repeat

Public API:

val scanProgress: StateFlow<ScanProgress>  // scanned, total, isScanning
fun startAutoScan()
fun triggerManualScan()
fun stopScan()

Stream checking strategy:

  • HLS streams: Validates response headers for #EXTM3U, #EXT-X-STREAM-INF, or #EXTINF
  • Generic streams: Tries HEAD request, falls back to GET with Range header

ChannelThumbnailExtractor

domain/service/ChannelThumbnailExtractor.kt

Extracts preview frames from online channel streams and caches them as JPEG thumbnails.

Configuration: 320x180 resolution, 70% JPEG quality, batches of 3.

Public API:

suspend fun extractThumbnails(): Int   // Returns count of successful extractions
suspend fun clearThumbnails()          // Clears all cached thumbnails

Thumbnails are stored in cacheDir/thumbnails/{channelId}.jpg and referenced via ChannelHealthEntity.thumbnailPath.