diff --git a/app/src/main/java/com/lagradost/cloudstream3/services/SubscriptionWorkManager.kt b/app/src/main/java/com/lagradost/cloudstream3/services/SubscriptionWorkManager.kt index fc31c1f3e0d..426bd199288 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/services/SubscriptionWorkManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/services/SubscriptionWorkManager.kt @@ -6,11 +6,13 @@ import android.content.Intent import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC import android.os.Build.VERSION.SDK_INT import androidx.core.app.NotificationCompat +import androidx.core.app.NotificationManagerCompat import androidx.core.app.PendingIntentCompat import androidx.core.net.toUri import androidx.work.* import com.lagradost.cloudstream3.* import com.lagradost.cloudstream3.APIHolder.getApiFromNameNull +import com.lagradost.cloudstream3.APIHolder.unixTime import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.plugins.PluginManager @@ -78,15 +80,6 @@ class SubscriptionWorkManager(val context: Context, workerParams: WorkerParamete .setSmallIcon(com.google.android.gms.cast.framework.R.drawable.quantum_ic_refresh_white_24) .setProgress(0, 0, true) - private val updateNotificationBuilder = - NotificationCompat.Builder(context, SUBSCRIPTION_CHANNEL_ID) - .setColorized(true) - .setOnlyAlertOnce(true) - .setAutoCancel(true) - .setPriority(NotificationCompat.PRIORITY_DEFAULT) - .setColor(context.colorFromAttribute(R.attr.colorPrimary)) - .setSmallIcon(R.drawable.ic_cloudstream_monochrome_big) - private val notificationManager: NotificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager @@ -137,8 +130,8 @@ class SubscriptionWorkManager(val context: Context, workerParams: WorkerParamete val api = getApiFromNameNull(savedData.apiName) ?: return@amap null // Reasonable timeout to prevent having this worker run forever. - val response = withTimeoutOrNull(60_000) { - api.load(savedData.url) as? EpisodeResponse + val loadResponse = withTimeoutOrNull(60_000) { + api.load(savedData.url) } ?: return@amap null val dubPreference = @@ -150,8 +143,38 @@ class SubscriptionWorkManager(val context: Context, workerParams: WorkerParamete DubStatus.Subbed } + var season = 0 + val response = loadResponse as? EpisodeResponse ?: return@amap null val latestEpisodes = response.getLatestEpisodes() val latestPreferredEpisode = latestEpisodes[dubPreference] + val nextAiring = response.nextAiring + + if (nextAiring != null && nextAiring.unixTime > unixTime) { + EpisodeAlertManager.scheduleEpisodeAlert( + subscribedData = savedData, + nextAiring = nextAiring, + episodeResponse = response, + apiName = api.name, + context = applicationContext, + ) + updateProgress(max, ++progress, false) + + // Early return to prevent notifying users of unavailable episodes + // on the rare occasion latestPreferredEpisode changes for meta providers + return@amap Unit + } + + when (loadResponse) { + is TvSeriesLoadResponse -> { + season = loadResponse.episodes.maxOf { it.season ?: Int.MIN_VALUE } + } + + is AnimeLoadResponse -> { + loadResponse.episodes[dubPreference]?.let { episodes -> + season = episodes.maxOf { it.season ?: Int.MIN_VALUE } + } + } + } val (shouldUpdate, latestEpisode) = if (latestPreferredEpisode != null) { val latestSeenEpisode = @@ -173,38 +196,14 @@ class SubscriptionWorkManager(val context: Context, workerParams: WorkerParamete ) if (shouldUpdate) { - val updateHeader = savedData.name - val updateDescription = txt( - R.string.subscription_episode_released, - latestEpisode, - savedData.name - ).asString(context) - - val intent = Intent(context, MainActivity::class.java).apply { - data = savedData.url.toUri() - flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK - }.putExtra(MainActivity.API_NAME_EXTRA_KEY, api.name) - - val pendingIntent = - PendingIntentCompat.getActivity(context, 0, intent, 0, false) - - val poster = ioWork { - savedData.posterUrl?.let { url -> - context.getImageBitmapFromUrl( - url, - savedData.posterHeaders - ) - } - } - - val updateNotification = - updateNotificationBuilder.setContentTitle(updateHeader) - .setContentText(updateDescription) - .setContentIntent(pendingIntent) - .setLargeIcon(poster) - .build() - - notificationManager.notify(id, updateNotification) + EpisodeAlertManager.showEpisodeNotification( + id = id, + season = season, + episode = latestEpisode, + savedData = savedData, + apiName = api.name, + context = context + ) } // You can probably get some issues here since this is async but it does not matter much. @@ -223,4 +222,175 @@ class SubscriptionWorkManager(val context: Context, workerParams: WorkerParamete return Result.success() } } +} + +class EpisodeAlertWorker( + val context: Context, + workerParams: WorkerParameters +) : CoroutineWorker(context, workerParams) { + companion object { + const val SUBSCRIPTION_TAG = "SUBSCRIPTION_TAG" + const val SUBSCRIPTION_ID = "subscription_id" + const val EPISODE_NO = "episode_no" + const val SEASON_NO = "season_no" + const val API_NAME = "api_name" + } + + override suspend fun doWork(): Result { + + val subscriptionId = inputData.getInt(SUBSCRIPTION_ID, -1) + val episode = inputData.getInt(EPISODE_NO, -1) + val season = inputData.getInt(SEASON_NO, -1) + val apiName = inputData.getString(API_NAME) ?: return Result.success() + + // Final check to ensure user is still subscribed before notifying + val savedData = DataStoreHelper.getSubscribedData(subscriptionId) ?: return Result.success() + val id = savedData.id ?: return Result.success() + + EpisodeAlertManager.showEpisodeNotification( + id = id, + season = season, + episode = episode, + savedData = savedData, + apiName = apiName, + context = applicationContext + ) + + return Result.success() + } +} + +class EpisodeAlertManager { + companion object { + fun scheduleEpisodeAlert( + subscribedData: DataStoreHelper.SubscribedData, + nextAiring: NextAiring?, + episodeResponse: EpisodeResponse?, + apiName: String, + context: Context + ) { + val now = unixTime + + when { + nextAiring == null -> { + DataStoreHelper.updateSubscribedData( + subscribedData.id, + subscribedData, + episodeResponse + ) + } + + nextAiring.unixTime > now && nextAiring.unixTime < now + 31_556_926L -> { + val episodeKey = "${nextAiring.season ?: ""}_${nextAiring.episode}" + val uniqueWorkName = "${apiName}_${subscribedData.id}_$episodeKey" + val delay = nextAiring.unixTime - now + + // Work manager's replace policy will take care of rescheduling if the air time changes + enqueueEpisodeAlertWorker( + subscribedData = subscribedData, + nextAiring = nextAiring, + apiName = apiName, + uniqueWorkName = uniqueWorkName, + delay = delay, + context = context, + ) + + DataStoreHelper.updateSubscribedData( + subscribedData.id, + subscribedData, + episodeResponse + ) + } + } + } + + private fun enqueueEpisodeAlertWorker( + subscribedData: DataStoreHelper.SubscribedData, + nextAiring: NextAiring, + apiName: String, + uniqueWorkName: String, + delay: Long, + context: Context + ) { + val inputData = workDataOf( + EpisodeAlertWorker.SUBSCRIPTION_ID to subscribedData.id, + EpisodeAlertWorker.EPISODE_NO to nextAiring.episode, + EpisodeAlertWorker.SEASON_NO to nextAiring.season, + EpisodeAlertWorker.API_NAME to apiName, + ) + + val constraints = Constraints.Builder() + .setRequiredNetworkType(NetworkType.CONNECTED) + .build() + + val request = OneTimeWorkRequestBuilder() + .setInitialDelay(delay, TimeUnit.SECONDS) + .addTag(EpisodeAlertWorker.SUBSCRIPTION_TAG) + .setInputData(inputData) + .setConstraints(constraints) + .build() + + WorkManager.getInstance(context).enqueueUniqueWork( + uniqueWorkName, + ExistingWorkPolicy.REPLACE, + request + ) + } + + suspend fun showEpisodeNotification( + id: Int, + season: Int, + episode: Int, + savedData: DataStoreHelper.SubscribedData, + apiName: String, + context: Context + ) { + val updateHeader = savedData.name + val posterUrl = savedData.posterUrl + val posterHeaders = savedData.posterHeaders + val intent = Intent(context, MainActivity::class.java).apply { + data = savedData.url.toUri() + flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK + putExtra(MainActivity.API_NAME_EXTRA_KEY, apiName) + } + + val pendingIntent = + PendingIntentCompat.getActivity(context, 0, intent, 0, false) + + val updateDescription = if (season > 0) { + txt( + R.string.subscription_season_episode_released, + season, + episode, + ).asString(context) + } else { + txt( + R.string.subscription_episode_released, + episode, + ).asString(context) + } + + val notificationBuilder = + NotificationCompat.Builder(context, SUBSCRIPTION_CHANNEL_ID) + .setColorized(true) + .setOnlyAlertOnce(true) + .setAutoCancel(true) + .setPriority(NotificationCompat.PRIORITY_DEFAULT) + .setColor(context.colorFromAttribute(R.attr.colorPrimary)) + .setSmallIcon(R.drawable.ic_cloudstream_monochrome_big) + .setContentTitle(updateHeader) + .setContentText(updateDescription) + .setContentIntent(pendingIntent) + + val poster = ioWork { + posterUrl?.let { url -> + context.getImageBitmapFromUrl(url, posterHeaders) + } + } + notificationBuilder.setLargeIcon(poster) + + NotificationManagerCompat.from(context) + .notify(id, notificationBuilder.build()) + } + } } \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index b623820e415..f2eff15ce36 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -741,6 +741,7 @@ Subscribed to %s Unsubscribed from %s Episode %d released! + Season %d Episode %d released! Subscribe Unsubscribe Profile %d