Skip to content

Commit

Permalink
Programme Reminder.
Browse files Browse the repository at this point in the history
  • Loading branch information
oxyroid committed Jul 20, 2024
1 parent ab8d00e commit df5c908
Show file tree
Hide file tree
Showing 8 changed files with 203 additions and 15 deletions.
3 changes: 3 additions & 0 deletions data/src/main/java/com/m3u/data/database/dao/ProgrammeDao.kt
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ interface ProgrammeDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertOrReplace(programme: Programme)

@Query("SELECT * FROM programmes WHERE id = :id")
suspend fun getById(id: Int): Programme

@Query("""SELECT MAX("end") from programmes WHERE epg_url = :epgUrl""")
suspend fun getMaxEndByEpgUrl(epgUrl: String): Long?

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,6 @@ interface ProgrammeRepository {
vararg playlistUrls: String,
ignoreCache: Boolean
): Flow<Int>

suspend fun getById(id: Int): Programme?
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import com.m3u.core.architecture.dispatcher.Dispatcher
import com.m3u.core.architecture.dispatcher.M3uDispatchers.IO
import com.m3u.core.architecture.logger.Logger
import com.m3u.core.architecture.logger.Profiles
import com.m3u.core.architecture.logger.execute
import com.m3u.core.architecture.logger.install
import com.m3u.core.architecture.logger.post
import com.m3u.core.util.basic.letIf
Expand Down Expand Up @@ -91,6 +92,10 @@ internal class ProgrammeRepositoryImpl @Inject constructor(
}
}

override suspend fun getById(id: Int): Programme? = logger.execute {
programmeDao.getById(id)
}

private fun checkOrRefreshProgrammesOrThrowImpl(
epgUrls: List<String>,
ignoreCache: Boolean
Expand Down
109 changes: 109 additions & 0 deletions data/src/main/java/com/m3u/data/worker/ProgrammeReminder.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
package com.m3u.data.worker

import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.content.Context
import android.media.AudioAttributes
import android.media.RingtoneManager
import androidx.core.graphics.drawable.toBitmapOrNull
import androidx.hilt.work.HiltWorker
import androidx.work.CoroutineWorker
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkManager
import androidx.work.WorkerParameters
import androidx.work.workDataOf
import com.m3u.data.R
import com.m3u.data.repository.media.MediaRepository
import com.m3u.data.repository.programme.ProgrammeRepository
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import kotlinx.datetime.Clock
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicInteger

@HiltWorker
class ProgrammeReminder @AssistedInject constructor(
@Assisted private val context: Context,
@Assisted params: WorkerParameters,
private val programmeRepository: ProgrammeRepository,
private val notificationManager: NotificationManager,
private val mediaRepository: MediaRepository
) : CoroutineWorker(context, params) {
private val programmeId = inputData.getInt(PROGRAMME_ID, -1)
private val notificationId: Int by lazy(LazyThreadSafetyMode.SYNCHRONIZED) {
ATOMIC_NOTIFICATION_ID.incrementAndGet()
}

override suspend fun doWork(): Result {
createChannel()
if (programmeId == -1) {
return Result.failure()
}
val programme = programmeRepository.getById(programmeId) ?: return Result.failure()
val drawable = mediaRepository.loadDrawable(programme.icon.orEmpty())
val builder = Notification.Builder(context, CHANNEL_ID)
.setContentTitle(programme.title)
.setContentText(programme.description)
.setSmallIcon(R.drawable.baseline_notifications_none_24)
if (drawable != null) {
builder.setLargeIcon(drawable.toBitmapOrNull())
}
notificationManager.notify(
notificationId,
builder.build()
)
return Result.success()
}

private fun createChannel() {
val channel = NotificationChannel(
CHANNEL_ID,
NOTIFICATION_NAME,
NotificationManager.IMPORTANCE_HIGH
)
channel.description = "Receive programme notifications"
channel.setSound(
RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION),
AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_MEDIA)
.setContentType(AudioAttributes.CONTENT_TYPE_MOVIE)
.build()
)
notificationManager.createNotificationChannel(channel)
}


companion object {
private const val CHANNEL_ID = "reminder_channel"
private const val NOTIFICATION_NAME = "programme_reminder"
private const val PROGRAMME_ID = "programme_id"
fun readProgrammeId(tags: Collection<String>): Int? = tags.find {
it.startsWith("id=")
}
?.drop(3)
?.toIntOrNull()

const val TAG = CHANNEL_ID
private val ATOMIC_NOTIFICATION_ID = AtomicInteger()

operator fun invoke(
workManager: WorkManager,
programmeId: Int,
programmeStart: Long
) {
val now = Clock.System.now().toEpochMilliseconds()
if (now > programmeStart) return
val data = workDataOf(
PROGRAMME_ID to programmeId
)
val request = OneTimeWorkRequestBuilder<ProgrammeReminder>()
.addTag(TAG)
.addTag("id=$programmeId")
.setInputData(data)
.setInitialDelay(programmeStart - now, TimeUnit.MILLISECONDS)
.build()
workManager.enqueue(request)
}
}
}
5 changes: 5 additions & 0 deletions data/src/main/res/drawable/baseline_notifications_none_24.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">

<path android:fillColor="@android:color/white" android:pathData="M12,22c1.1,0 2,-0.9 2,-2h-4c0,1.1 0.9,2 2,2zM18,16v-5c0,-3.07 -1.63,-5.64 -4.5,-6.32L13.5,4c0,-0.83 -0.67,-1.5 -1.5,-1.5s-1.5,0.67 -1.5,1.5v0.68C7.64,5.36 6,7.92 6,11v5l-2,2v1h16v-1l-2,-2zM16,17L8,17v-6c0,-2.48 1.51,-4.5 4,-4.5s4,2.02 4,4.5v6z"/>

</vector>
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,8 @@ fun ChannelRoute(
val programmes = viewModel.programmes.collectAsLazyPagingItems()
val programmeRange by viewModel.programmeRange.collectAsStateWithLifecycle()

val programmeIdsInReminder by viewModel.programmeIdsInReminder.collectAsStateWithLifecycle()

var brightness by remember { mutableFloatStateOf(helper.brightness) }
var isPipMode by remember { mutableStateOf(false) }
var isAutoZappingMode by remember { mutableStateOf(true) }
Expand Down Expand Up @@ -177,7 +179,10 @@ fun ChannelRoute(
isProgrammeSupported = isProgrammeSupported,
channels = channels,
programmes = programmes,
programmeRange = programmeRange
programmeRange = programmeRange,
programmeIdsInReminder = programmeIdsInReminder,
onRemindProgramme = viewModel::onRemindProgramme,
onCancelRemindProgramme = viewModel::onCancelRemindProgramme
)
},
content = {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.m3u.feature.channel

import android.annotation.SuppressLint
import android.media.AudioManager
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
Expand All @@ -12,27 +13,29 @@ import androidx.paging.Pager
import androidx.paging.PagingConfig
import androidx.paging.PagingData
import androidx.paging.cachedIn
import com.m3u.core.architecture.dispatcher.Dispatcher
import com.m3u.core.architecture.dispatcher.M3uDispatchers.Main
import androidx.work.WorkInfo
import androidx.work.WorkManager
import androidx.work.WorkQuery
import androidx.work.await
import com.m3u.core.architecture.logger.Logger
import com.m3u.core.architecture.logger.Profiles
import com.m3u.core.architecture.logger.install
import com.m3u.data.database.model.Channel
import com.m3u.data.database.model.DataSource
import com.m3u.data.database.model.Playlist
import com.m3u.data.database.model.Programme
import com.m3u.data.database.model.ProgrammeRange
import com.m3u.data.database.model.Channel
import com.m3u.data.database.model.epgUrlsOrXtreamXmlUrl
import com.m3u.data.database.model.isSeries
import com.m3u.data.database.model.isVod
import com.m3u.data.repository.channel.ChannelRepository
import com.m3u.data.repository.playlist.PlaylistRepository
import com.m3u.data.repository.programme.ProgrammeRepository
import com.m3u.data.repository.channel.ChannelRepository
import com.m3u.data.service.PlayerManager
import com.m3u.data.service.currentTracks
import com.m3u.data.service.tracks
import com.m3u.data.worker.ProgrammeReminder
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
Expand Down Expand Up @@ -63,8 +66,8 @@ class ChannelViewModel @Inject constructor(
private val playerManager: PlayerManager,
private val audioManager: AudioManager,
private val programmeRepository: ProgrammeRepository,
private val workManager: WorkManager,
delegate: Logger,
@Dispatcher(Main) private val mainDispatcher: CoroutineDispatcher
) : ViewModel(), ControlPoint.DiscoveryListener {
private val logger = delegate.install(Profiles.VIEWMODEL_CHANNEL)

Expand Down Expand Up @@ -237,6 +240,44 @@ class ChannelViewModel @Inject constructor(
playerManager.pauseOrContinue(isContinued)
}

internal val programmeIdsInReminder: StateFlow<List<Int>> = workManager.getWorkInfosFlow(
WorkQuery.fromStates(
WorkInfo.State.ENQUEUED
)
)
.map { infos: List<WorkInfo> ->
infos
.filter { ProgrammeReminder.TAG in it.tags }
.mapNotNull { info -> ProgrammeReminder.readProgrammeId(info.tags) }
}
.stateIn(
scope = viewModelScope,
initialValue = emptyList(),
started = SharingStarted.Lazily
)

fun onRemindProgramme(programme: Programme) {
ProgrammeReminder(
workManager = workManager,
programmeId = programme.id,
programmeStart = programme.start
)
}

@SuppressLint("RestrictedApi")
fun onCancelRemindProgramme(programme: Programme) {
viewModelScope.launch {
val infos = workManager
.getWorkInfos(WorkQuery.fromStates(WorkInfo.State.ENQUEUED))
.await()
.filter { ProgrammeReminder.TAG in it.tags }
.filter { info -> ProgrammeReminder.readProgrammeId(info.tags) != null }
infos.forEach {
workManager.cancelWorkById(it.id)
}
}
}

// the channels which is in the same category with the current channel
// or the episodes which is in the same series.
internal val channels: Flow<PagingData<Channel>> = playlist.flatMapLatest { playlist ->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.AbsoluteRoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Notifications
import androidx.compose.material.icons.rounded.Close
import androidx.compose.material.icons.rounded.NotificationsActive
import androidx.compose.material3.Card
Expand Down Expand Up @@ -84,6 +85,7 @@ import com.m3u.ui.helper.LocalHelper
import com.m3u.ui.util.TimeUtils.formatEOrSh
import com.m3u.ui.util.TimeUtils.toEOrSh
import kotlinx.coroutines.launch
import kotlinx.datetime.Clock
import kotlinx.datetime.Instant
import kotlinx.datetime.TimeZone
import kotlinx.datetime.toLocalDateTime
Expand All @@ -103,7 +105,10 @@ internal fun PlayerPanel(
channels: LazyPagingItems<Channel>,
programmes: LazyPagingItems<Programme>,
programmeRange: ProgrammeRange,
modifier: Modifier = Modifier
modifier: Modifier = Modifier,
programmeIdsInReminder: List<Int>,
onRemindProgramme: (Programme) -> Unit,
onCancelRemindProgramme: (Programme) -> Unit,
) {
val configuration = LocalConfiguration.current
val spacing = LocalSpacing.current
Expand Down Expand Up @@ -197,7 +202,11 @@ internal fun PlayerPanel(
modifier = Modifier.weight(1f)
) {
Text(
text = "${start.formatEOrSh(false)} - ${end.formatEOrSh(false)}",
text = "${start.formatEOrSh(false)} - ${
end.formatEOrSh(
false
)
}",
maxLines = 1,
overflow = TextOverflow.Ellipsis,
style = MaterialTheme.typography.bodyMedium,
Expand All @@ -212,12 +221,21 @@ internal fun PlayerPanel(
fontFamily = FontFamilies.LexendExa
)
}
IconButton(
icon = Icons.Rounded.NotificationsActive,
contentDescription = null,
onClick = { /*TODO*/ },
modifier = Modifier.align(Alignment.CenterVertically)
)
val isReminderShowing = Clock.System.now()
.toEpochMilliseconds() < currentProgramme.start
if (isReminderShowing) {
val inReminder = currentProgramme.id in programmeIdsInReminder
IconButton(
icon = if (inReminder) Icons.Outlined.Notifications
else Icons.Rounded.NotificationsActive,
contentDescription = null,
onClick = {
if (inReminder) onCancelRemindProgramme(currentProgramme)
else onRemindProgramme(currentProgramme)
},
modifier = Modifier.align(Alignment.CenterVertically)
)
}
}

Column(
Expand Down

0 comments on commit df5c908

Please sign in to comment.