Data Layer

April 4, 2026 · View on GitHub

Implements domain repository interfaces. Manages local persistence (Room), remote API (Retrofit), and data mapping between DTOs, entities, and domain models.

flowchart LR
    API["Retrofit API"] --> DTO
    DTO --> Mapper
    Mapper --> Entity["Room Entity"]
    Entity --> Room["Room DB"]
    Room --> Flow["Flow<Entity>"]
    Flow --> Mapper2["Domain Mapper"]
    Mapper2 --> Domain["Domain Model"]
    Domain --> ViewModel

Package: com.cadnative.firevisioniptv.data

Package Structure

data/
├── mapper/                  # Bidirectional data mappers
│   ├── CategoryMapper.kt
│   └── ChannelMapper.kt
├── model/
│   ├── dto/                 # API response/request DTOs
│   │   ├── CategoriesResponse.kt
│   │   ├── CategoryDto.kt
│   │   ├── ChannelDto.kt
│   │   ├── ChannelsResponse.kt
│   │   └── FavoritesRequest.kt
│   └── Result.kt            # Sealed Result<T> wrapper
├── repository/              # Repository implementations
│   ├── CategoryRepositoryImpl.kt
│   ├── ChannelRepositoryImpl.kt
│   ├── EpgRepositoryImpl.kt
│   ├── FavoriteRepositoryImpl.kt
│   ├── PlaybackRepositoryImpl.kt
│   ├── SearchHistoryRepositoryImpl.kt
│   └── UserPreferencesRepositoryImpl.kt
└── source/
    ├── local/
    │   ├── dao/             # Room DAOs
    │   │   ├── CategoryDao.kt
    │   │   ├── ChannelDao.kt
    │   │   ├── ChannelHealthDao.kt
    │   │   ├── FavoriteDao.kt
    │   │   ├── PlaybackPositionDao.kt
    │   │   └── SearchHistoryDao.kt
    │   ├── entity/          # Room entities
    │   │   ├── CategoryEntity.kt
    │   │   ├── ChannelEntity.kt
    │   │   ├── ChannelHealthEntity.kt
    │   │   ├── FavoriteEntity.kt
    │   │   ├── PlaybackPositionEntity.kt
    │   │   └── SearchHistoryEntity.kt
    │   ├── FireVisionDatabase.kt
    │   ├── CategoryLocalDataSource.kt
    │   ├── ChannelLocalDataSource.kt
    │   ├── FavoriteLocalDataSource.kt
    │   ├── PlaybackLocalDataSource.kt
    │   └── SearchHistoryLocalDataSource.kt
    └── remote/
        ├── FireVisionApiService.kt
        ├── CategoryRemoteDataSource.kt
        └── ChannelRemoteDataSource.kt

Room Database

FireVisionDatabase

data/source/local/FireVisionDatabase.kt

  • Version: 3
  • Schema export: Enabled (schemas stored in app/schemas/)
  • Migration strategy: fallbackToDestructiveMigration() (development mode)

Provides 6 DAOs:

abstract fun channelDao(): ChannelDao
abstract fun categoryDao(): CategoryDao
abstract fun favoriteDao(): FavoriteDao
abstract fun searchHistoryDao(): SearchHistoryDao
abstract fun playbackPositionDao(): PlaybackPositionDao
abstract fun channelHealthDao(): ChannelHealthDao

Entities

channels

ColumnTypeConstraintsDescription
idTEXTPRIMARY KEYChannel identifier
nameTEXTNOT NULL, INDEXEDDisplay name
streamUrlTEXTNOT NULLStreaming URL
logoUrlTEXTnullableLogo image URL
categoryIdTEXTNOT NULL, INDEXEDCategory reference
languageTEXTnullableLanguage code
countryTEXTnullableCountry code
groupTitleTEXTnullableM3U group title
tvgIdTEXTnullableTVG identifier
tvgNameTEXTnullableTVG name
isActiveINTEGERNOT NULL, INDEXED, default 1Active flag
lastUpdatedINTEGERNOT NULLTimestamp

categories

ColumnTypeConstraints
idTEXTPRIMARY KEY
nameTEXTNOT NULL
displayOrderINTEGERNOT NULL, default 0
channelCountINTEGERNOT NULL, default 0

channel_health

ColumnTypeConstraints
channelIdTEXTPRIMARY KEY, FK → channels(id) ON DELETE CASCADE
statusTEXTNOT NULL
lastCheckedAtINTEGERNOT NULL, INDEXED, default 0
responseTimeMsINTEGERnullable
errorMessageTEXTnullable
thumbnailPathTEXTnullable

Unique index on channelId. Additional index on status.

favorites

ColumnTypeConstraints
idINTEGERPRIMARY KEY AUTOINCREMENT
channelIdTEXTNOT NULL, INDEXED, FK → channels(id) ON DELETE CASCADE
addedAtINTEGERNOT NULL
displayOrderINTEGERNOT NULL, default 0

playback_positions

ColumnTypeConstraints
channelIdTEXTPRIMARY KEY
positionINTEGERNOT NULL
durationINTEGERNOT NULL
lastPlayedINTEGERNOT NULL, INDEXED

search_history

ColumnTypeConstraints
idINTEGERPRIMARY KEY AUTOINCREMENT
queryTEXTNOT NULL
timestampINTEGERNOT NULL, INDEXED

Key DAO Queries

ChannelDao:

  • getAllChannels() — Active channels ordered by name, returns Flow
  • searchChannels(query) — Case-insensitive LIKE search on name and groupTitle
  • replaceAllChannels(channels)@Transaction: atomic delete-all + insert-all

ChannelHealthDao:

  • upsertPreservingThumbnail(...) — Updates health status while preserving existing thumbnailPath
  • getOnlineChannelIdsWithoutThumbnail() — Returns ONLINE channels with NULL thumbnailPath
  • getAllChannelIdsByPriority() — Ordered by lastCheckedAt ASC (oldest first)
  • cleanupOrphaned() — Deletes health records with no matching channel

FavoriteDao:

  • getFavoriteChannels() — JOIN query: favorites INNER JOIN channels, ordered by displayOrder ASC, addedAt DESC
  • isFavorite(channelId) — EXISTS subquery returning Flow<Boolean>

Remote API

FireVisionApiService

data/source/remote/FireVisionApiService.kt

Retrofit interface:

MethodEndpointRequestResponse
GET/api/v1/channelsResponse<ChannelsResponse>
GET/api/v1/channels/{id}@Path("id")Response<ChannelDto>
GET/api/v1/categoriesResponse<CategoriesResponse>
POST/api/v1/favorites@Body FavoritesRequestResponse<Unit>
GET/api/v1/playlist.m3uResponse<ResponseBody>

DTOs

ChannelsResponse:

data class ChannelsResponse(
    @SerializedName("success") val success: Boolean,
    @SerializedName("data") val data: List<ChannelDto>
)

ChannelDto:

data class ChannelDto(
    @SerializedName("channelId") val id: String,
    @SerializedName("channelName") val name: String,
    @SerializedName("channelUrl") val url: String,
    @SerializedName("channelImg") val channelImg: String?,
    val tvgLogo: String?,
    @SerializedName("channelGroup") val groupTitle: String?,
    @SerializedName("channelDrmKey") val drmKey: String?,
    @SerializedName("channelDrmType") val drmType: String?,
    val tvgLanguage: String?,
    val tvgCountry: String?,
    val tvgId: String?,
    val tvgName: String?,
    val isActive: Boolean,
    val metadata: ChannelMetadataDto?
)
// Computed: logoUrl = tvgLogo ?: channelImg

CategoriesResponse:

data class CategoriesResponse(
    @SerializedName("categories") val categories: List<CategoryDto>,
    @SerializedName("total") val total: Int?
)

FavoritesRequest:

data class FavoritesRequest(
    @SerializedName("channel_ids") val channelIds: List<String>,
    @SerializedName("device_id") val deviceId: String?,
    @SerializedName("timestamp") val timestamp: Long
)

Error Handling

Remote data sources map HTTP errors to typed exceptions:

HTTP CodeException
Network failureNetworkException
400BadRequestException
401UnauthorizedException
403ForbiddenException
404NotFoundException
500ServerException
503ServiceUnavailableException
OtherUnknownException

Data Mappers

ChannelMapper

data/mapper/ChannelMapper.kt@Singleton

MethodFromToNotes
toDomain(entity, isFavorite)ChannelEntityChannelAccepts optional favorite flag
toEntity(dto)ChannelDtoChannelEntityResolves logo from tvgLogo or channelImg, defaults category to "uncategorized"
fromDomain(channel)ChannelChannelEntityReverse mapping

CategoryMapper

data/mapper/CategoryMapper.kt@Singleton

MethodFromTo
toDomain(entity)CategoryEntityCategory
toEntity(dto)CategoryDtoCategoryEntity
fromDomain(category)CategoryCategoryEntity

Result Wrapper

data/model/Result.kt

sealed class Result<out T> {
    data class Success<T>(val data: T) : Result<T>()
    data class Error(val exception: Exception) : Result<Nothing>()

    val isSuccess: Boolean
    val isError: Boolean
    fun getOrNull(): T?
    fun exceptionOrNull(): Exception?

    companion object {
        fun <T> success(data: T): Result<T>
        fun error(exception: Exception): Result<Nothing>
    }
}

Repository Implementations

All repositories are @Singleton and follow the offline-first pattern: local database is the single source of truth, remote data refreshes the cache.

ChannelRepositoryImpl

  • getChannels() — Combines ChannelDao.getAllChannels() with FavoriteDao.getAllFavorites() using flow.combine() to enrich channels with favorite status
  • refreshChannels() — Fetches from API, maps DTOs → entities, atomically replaces all local channels via @Transaction

FavoriteRepositoryImpl

  • addFavorite() / removeFavorite() — Updates local DB, then fires background sync to POST /api/v1/favorites
  • syncFavorites() — Collects all local favorite channel IDs and sends to server

EpgRepositoryImpl

data/repository/EpgRepositoryImpl.kt@Singleton

In-memory EPG cache. Fetches guide data from GET /api/v1/epg/guide on first access, then serves from memory.

  • ensureLoaded() — Double-checked locking with Mutex. Fetches EPG once, maps EpgProgramDtoEpgProgram, stores in Map<tvgId, List<EpgProgram>>.
  • getNowNext(tvgId) — Suspend. Calls ensureLoaded() if needed, then finds current/next programs by comparing Instant.now() against start/end times.
  • getNowNextIfCached(tvgId)Synchronous, non-blocking. Returns null when !cacheLoaded (EPG not fetched yet). This is the key performance optimization: ChannelsViewModel calls this during channel rendering so channels appear instantly without waiting for EPG network call.

Network failures are silently swallowed — EPG is non-critical, the app degrades gracefully (no "Now Playing" titles).

UserPreferencesRepositoryImpl

  • Uses SharedPreferences (not Room) with MutableStateFlow for reactive preference updates
  • clearCache() — Recursively deletes context.cacheDir