Skip to content
This repository has been archived by the owner on Nov 12, 2024. It is now read-only.

Properly enable/disable episode airing notifications #1952

Merged
merged 1 commit into from
Jul 19, 2024
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 @@ -4,12 +4,23 @@
package app.tivi.core.notifications

import app.tivi.data.models.Notification
import app.tivi.data.models.NotificationChannel

interface NotificationManager {

suspend fun schedule(notification: Notification) = Unit

suspend fun cancel(notification: Notification) = Unit

suspend fun cancelAll(notifications: List<Notification>) {
notifications.forEach { cancel(it) }
}

suspend fun cancelAllInChannel(channel: NotificationChannel) {
cancelAll(
getPendingNotifications().filter { it.channel == channel },
)
}

suspend fun getPendingNotifications(): List<Notification> = emptyList()
}
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,20 @@ class IosNotificationManager(
}
}

override suspend fun cancelAll(notifications: List<Notification>) {
val ids = notifications.map { it.id }

UNUserNotificationCenter
.currentNotificationCenter()
.removePendingNotificationRequestsWithIdentifiers(ids)
}

override suspend fun cancel(notification: Notification) {
UNUserNotificationCenter
.currentNotificationCenter()
.removePendingNotificationRequestsWithIdentifiers(listOf(notification.id))
}

private companion object {
const val USER_INFO_DEEPLINK = "deeplink_uri"
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@

package app.tivi.settings

import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.Flow

interface Preference<T> {
val defaultValue: T

val flow: StateFlow<T>
val flow: Flow<T>
suspend fun set(value: T)
suspend fun get(): T
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ interface TiviPreferences {

val developerHideArtwork: Preference<Boolean>

val notificationsEnabled: Preference<Boolean>
val episodeAiringNotificationsEnabled: Preference<Boolean>

enum class Theme {
LIGHT,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,12 @@ import com.russhwolf.settings.ObservableSettings
import com.russhwolf.settings.coroutines.toFlowSettings
import com.russhwolf.settings.set
import kotlin.time.Duration.Companion.seconds
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.WhileSubscribed
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.shareIn
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.withContext
import me.tatarka.inject.annotations.Inject
Expand Down Expand Up @@ -57,7 +59,7 @@ class TiviPreferencesImpl(
override val developerHideArtwork: Preference<Boolean> by lazy {
BooleanPreference(KEY_DEV_HIDE_ARTWORK)
}
override val notificationsEnabled: Preference<Boolean> by lazy {
override val episodeAiringNotificationsEnabled: Preference<Boolean> by lazy {
BooleanPreference(KEY_NOTIFICATIONS)
}

Expand Down Expand Up @@ -98,13 +100,12 @@ class TiviPreferencesImpl(
settings.getStringOrNull(key)?.let(toValue) ?: defaultValue
}

override val flow: StateFlow<V> by lazy {
override val flow: Flow<V> by lazy {
flowSettings.getStringOrNullFlow(key)
.map { it?.let(toValue) ?: defaultValue }
.stateIn(
.shareIn(
scope = coroutineScope,
started = SharingStarted.WhileSubscribed(SUBSCRIBED_TIMEOUT),
initialValue = defaultValue,
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,11 @@ package app.tivi.domain.interactors
import app.tivi.app.ApplicationInfo
import app.tivi.core.notifications.NotificationManager
import app.tivi.data.episodes.SeasonsEpisodesRepository
import app.tivi.data.models.NotificationChannel
import app.tivi.domain.Interactor
import app.tivi.settings.TiviPreferences
import app.tivi.util.AppCoroutineDispatchers
import app.tivi.util.Logger
import app.tivi.util.TiviDateFormatter
import kotlin.time.Duration
import kotlin.time.Duration.Companion.days
Expand All @@ -22,12 +25,28 @@ class ScheduleDebugEpisodeNotification(
dateTimeFormatter: Lazy<TiviDateFormatter>,
private val dispatchers: AppCoroutineDispatchers,
private val applicationInfo: ApplicationInfo,
private val preferences: Lazy<TiviPreferences>,
private val logger: Logger,
) : Interactor<ScheduleDebugEpisodeNotification.Params, Unit>() {
private val seasonsEpisodesRepository by seasonsEpisodesRepository
private val notificationManager by notificationManager
private val dateTimeFormatter by dateTimeFormatter

override suspend fun doWork(params: Params) {
val notificationsEnabled = withContext(dispatchers.io) {
preferences.value.episodeAiringNotificationsEnabled.get()
}

if (!notificationsEnabled) {
logger.d {
"ScheduleDebugEpisodeNotification. " +
"Notifications not enabled. " +
"Cancelling all EPISODES_AIRING notifications"
}
notificationManager.cancelAllInChannel(NotificationChannel.EPISODES_AIRING)
return
}

val episode = withContext(dispatchers.io) {
seasonsEpisodesRepository
.getUpcomingEpisodesFromFollowedShows(Clock.System.now() + 365.days)
Expand Down
30 changes: 23 additions & 7 deletions tasks/src/androidMain/kotlin/app/tivi/tasks/AndroidTasks.kt
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,21 @@ import androidx.work.NetworkType
import androidx.work.OneTimeWorkRequest
import androidx.work.PeriodicWorkRequestBuilder
import androidx.work.WorkManager
import app.tivi.util.Logger
import kotlin.time.Duration.Companion.hours
import kotlin.time.toJavaDuration
import me.tatarka.inject.annotations.Inject

@Inject
class AndroidTasks(
workManager: Lazy<WorkManager>,
private val logger: Logger,
) : Tasks {
private val workManager by workManager

override fun registerPeriodicTasks() {
override fun scheduleLibrarySync() {
logger.d { "Tasks.scheduleLibrarySync()" }

val nightlyConstraints = Constraints.Builder()
.setRequiredNetworkType(NetworkType.UNMETERED)
.setRequiresCharging(true)
Expand All @@ -33,6 +37,21 @@ class AndroidTasks(
.setConstraints(nightlyConstraints)
.build(),
)
}

override fun cancelLibrarySync() {
logger.d { "Tasks.cancelLibrarySync()" }
workManager.cancelUniqueWork(SyncLibraryShowsWorker.NAME)
}

override fun scheduleEpisodeNotifications() {
logger.d { "Tasks.scheduleEpisodeNotifications()" }

workManager.enqueueUniqueWork(
"tasks_startup",
ExistingWorkPolicy.KEEP,
OneTimeWorkRequest.from(ScheduleEpisodeNotificationsWorker::class.java),
)

workManager.enqueueUniquePeriodicWork(
ScheduleEpisodeNotificationsWorker.NAME,
Expand All @@ -43,12 +62,9 @@ class AndroidTasks(
)
}

override fun enqueueStartupTasks() {
workManager.enqueueUniqueWork(
"tasks_startup",
ExistingWorkPolicy.KEEP,
OneTimeWorkRequest.from(ScheduleEpisodeNotificationsWorker::class.java),
)
override fun cancelEpisodeNotifications() {
logger.d { "Tasks.cancelEpisodeNotifications()" }
workManager.cancelUniqueWork(ScheduleEpisodeNotificationsWorker.NAME)
}

internal companion object {
Expand Down
6 changes: 4 additions & 2 deletions tasks/src/commonMain/kotlin/app/tivi/tasks/Tasks.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@
package app.tivi.tasks

interface Tasks {
fun registerPeriodicTasks() = Unit
fun scheduleEpisodeNotifications()
fun cancelEpisodeNotifications()

fun enqueueStartupTasks() = Unit
fun scheduleLibrarySync()
fun cancelLibrarySync()
}
19 changes: 17 additions & 2 deletions tasks/src/commonMain/kotlin/app/tivi/tasks/TasksInitializer.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,31 @@
package app.tivi.tasks

import app.tivi.appinitializers.AppInitializer
import app.tivi.inject.ApplicationCoroutineScope
import app.tivi.settings.TiviPreferences
import kotlinx.coroutines.launch
import me.tatarka.inject.annotations.Inject

@Inject
class TasksInitializer(
tasks: Lazy<Tasks>,
preferences: Lazy<TiviPreferences>,
private val coroutineScope: ApplicationCoroutineScope,
) : AppInitializer {
private val tasks by tasks
private val preferences by preferences

override fun initialize() {
tasks.registerPeriodicTasks()
tasks.enqueueStartupTasks()
tasks.scheduleLibrarySync()

coroutineScope.launch {
preferences.episodeAiringNotificationsEnabled.flow
.collect { enabled ->
when {
enabled -> tasks.scheduleEpisodeNotifications()
else -> tasks.cancelEpisodeNotifications()
}
}
}
}
}
18 changes: 13 additions & 5 deletions tasks/src/iosMain/kotlin/app/tivi/tasks/IosTasks.kt
Original file line number Diff line number Diff line change
Expand Up @@ -35,26 +35,34 @@ class IosTasks(
private val updateLibraryShows by updateLibraryShows
private val scheduleEpisodeNotifications by scheduleEpisodeNotifications

override fun registerPeriodicTasks() {
override fun scheduleLibrarySync() {
registerTaskAndSchedule(
id = ID_LIBRARY_SHOWS_NIGHTLY,
type = TaskType.Refresh,
firstSync = nextEarliestNightlySyncDate(),
)
}

override fun cancelLibrarySync() {
taskScheduler.cancelTaskRequestWithIdentifier(ID_LIBRARY_SHOWS_NIGHTLY)
}

override fun scheduleEpisodeNotifications() {
registerTaskAndSchedule(
id = ID_SCHEDULE_EPISODE_NOTIFICATIONS,
id = ID_LIBRARY_SHOWS_NIGHTLY,
type = TaskType.Refresh,
firstSync = (Clock.System.now() + SCHEDULE_EPISODE_NOTIFICATIONS_INTERVAL).toNSDate(),
firstSync = nextEarliestNightlySyncDate(),
)
}

override fun enqueueStartupTasks() {
// iOS has no concept of running tasks while the app is open, so we'll just run them
// manually now
scope.launch { runScheduleEpisodeNotifications() }
}

override fun cancelEpisodeNotifications() {
taskScheduler.cancelTaskRequestWithIdentifier(ID_SCHEDULE_EPISODE_NOTIFICATIONS)
}

private fun registerTaskAndSchedule(id: String, type: TaskType, firstSync: NSDate) {
taskScheduler.registerForTaskWithIdentifier(
identifier = id,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,8 @@ actual interface TasksPlatformComponent {
}

object EmptyShowTasks : Tasks {
override fun registerPeriodicTasks() = Unit
override fun scheduleEpisodeNotifications() = Unit
override fun cancelEpisodeNotifications() = Unit
override fun scheduleLibrarySync() = Unit
override fun cancelLibrarySync() = Unit
}
Original file line number Diff line number Diff line change
Expand Up @@ -119,10 +119,7 @@ internal fun DevNotifications(
items(state.pendingNotifications) { notification ->
ListItem(
overlineContent = {
val date = notification.date
if (date != null) {
Text(LocalTiviDateFormatter.current.formatMediumDateTime(date))
}
Text(LocalTiviDateFormatter.current.formatMediumDateTime(notification.date))
},
headlineContent = { Text(notification.title) },
supportingContent = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ class SettingsPresenter(
val ignoreSpecials by preferences.ignoreSpecials.collectAsState()
val crashDataReportingEnabled by preferences.reportAppCrashes.collectAsState()
val analyticsDataReportingEnabled by preferences.reportAnalytics.collectAsState()
val notificationsEnabled by preferences.notificationsEnabled.collectAsState()
val notificationsEnabled by preferences.episodeAiringNotificationsEnabled.collectAsState()

val coroutineScope = rememberCoroutineScope()

Expand Down Expand Up @@ -87,14 +87,14 @@ class SettingsPresenter(
}
SettingsUiEvent.ToggleAiringEpisodeNotificationsEnabled -> {
coroutineScope.launch {
if (preferences.notificationsEnabled.get()) {
if (preferences.episodeAiringNotificationsEnabled.get()) {
// If we're enabled, and being turned off, we don't need to mess with permissions
preferences.notificationsEnabled.toggle()
preferences.episodeAiringNotificationsEnabled.toggle()
} else {
// If we're disabled, and being turned on, we need to check our permissions
permissionsController.performPermissionedAction(REMOTE_NOTIFICATION) { state ->
if (state == PermissionState.Granted) {
preferences.notificationsEnabled.toggle()
preferences.episodeAiringNotificationsEnabled.toggle()
} else {
permissionsController.openAppSettings()
}
Expand Down
Loading