From 77282d6ac254947317c27c0e5baa8300938790ba Mon Sep 17 00:00:00 2001 From: Uwe Trottmann Date: Thu, 7 Mar 2024 12:27:21 +0100 Subject: [PATCH] Local history: add/remove entries when marking multiple episodes --- .../jobs/episodes/BaseEpisodesJob.kt | 38 +++++-- .../jobs/episodes/EpisodeBaseJob.java | 2 +- .../jobs/episodes/EpisodeCollectedJob.kt | 6 +- .../jobs/episodes/EpisodeWatchedJob.kt | 46 ++++---- .../jobs/episodes/EpisodeWatchedUpToJob.kt | 33 +++--- .../jobs/episodes/SeasonCollectedJob.kt | 4 +- .../jobs/episodes/SeasonWatchedJob.kt | 24 +++-- .../jobs/episodes/ShowCollectedJob.kt | 7 +- .../jobs/episodes/ShowWatchedJob.kt | 38 ++++--- .../shows/database/SgEpisode2Helper.kt | 19 ++-- .../seriesguide/shows/history/SgActivity.kt | 5 + .../shows/history/SgActivityHelper.kt | 101 ++++++++---------- 12 files changed, 177 insertions(+), 146 deletions(-) diff --git a/app/src/main/java/com/battlelancer/seriesguide/jobs/episodes/BaseEpisodesJob.kt b/app/src/main/java/com/battlelancer/seriesguide/jobs/episodes/BaseEpisodesJob.kt index 81ce726508..0f8b96ba83 100644 --- a/app/src/main/java/com/battlelancer/seriesguide/jobs/episodes/BaseEpisodesJob.kt +++ b/app/src/main/java/com/battlelancer/seriesguide/jobs/episodes/BaseEpisodesJob.kt @@ -11,6 +11,7 @@ import com.battlelancer.seriesguide.provider.SeriesGuideContract import com.battlelancer.seriesguide.provider.SgRoomDatabase import com.battlelancer.seriesguide.shows.database.SgEpisode2Numbers import com.battlelancer.seriesguide.shows.episodes.EpisodeTools +import com.battlelancer.seriesguide.shows.history.SgActivityHelper import com.battlelancer.seriesguide.shows.tools.LatestEpisodeUpdateTask import com.google.flatbuffers.FlatBufferBuilder @@ -37,17 +38,19 @@ abstract class BaseEpisodesJob( */ @CallSuper override fun applyLocalChanges(context: Context, requiresNetworkJob: Boolean): Boolean { + val episodes: List = getAffectedEpisodes(context) + // prepare network job var networkJobInfo: ByteArray? = null if (requiresNetworkJob) { - networkJobInfo = prepareNetworkJob(context) + networkJobInfo = prepareNetworkJob(episodes) if (networkJobInfo == null) { return false } } // apply local updates - val updated = applyDatabaseChanges(context) + val updated = applyDatabaseChanges(context, episodes) if (!updated) { return false } @@ -66,13 +69,16 @@ abstract class BaseEpisodesJob( return true } - protected abstract fun applyDatabaseChanges(context: Context): Boolean + protected abstract fun applyDatabaseChanges( + context: Context, + episodes: List + ): Boolean /** * Note: Ensure episodes are ordered by season number (lowest first), * then episode number (lowest first). */ - protected abstract fun getEpisodesForNetworkJob(context: Context): List + protected abstract fun getAffectedEpisodes(context: Context): List /** * Returns the number of plays to upload to Cloud (Trakt currently not supported) @@ -80,10 +86,10 @@ abstract class BaseEpisodesJob( */ protected abstract fun getPlaysForNetworkJob(plays: Int): Int - private fun prepareNetworkJob(context: Context): ByteArray? { - // store affected episodes for network part - val episodes = getEpisodesForNetworkJob(context) - + /** + * Store affected episodes for network job. + */ + private fun prepareNetworkJob(episodes: List): ByteArray? { val builder = FlatBufferBuilder(0) val episodeInfos = IntArray(episodes.size) @@ -122,4 +128,20 @@ abstract class BaseEpisodesJob( } LatestEpisodeUpdateTask.updateLatestEpisodeFor(context, showId) } + + /** + * Add or remove watch activity entries for episodes. Only used for watch jobs. + */ + protected fun updateActivity(context: Context, episodes: List) { + val showTmdbIdOrZero = + SgRoomDatabase.getInstance(context).sgShow2Helper().getShowTmdbId(showId) + val episodeTmdbIds = episodes.mapNotNull { it.tmdbId } + if (showTmdbIdOrZero != 0 && episodeTmdbIds.isNotEmpty()) return + + if (EpisodeTools.isWatched(flagValue)) { + SgActivityHelper.addActivitiesForEpisodes(context, showTmdbIdOrZero, episodeTmdbIds) + } else if (EpisodeTools.isUnwatched(flagValue)) { + SgActivityHelper.removeActivitiesForEpisodes(context, episodeTmdbIds) + } + } } diff --git a/app/src/main/java/com/battlelancer/seriesguide/jobs/episodes/EpisodeBaseJob.java b/app/src/main/java/com/battlelancer/seriesguide/jobs/episodes/EpisodeBaseJob.java index c8d7cd8f12..f922f9e4d5 100644 --- a/app/src/main/java/com/battlelancer/seriesguide/jobs/episodes/EpisodeBaseJob.java +++ b/app/src/main/java/com/battlelancer/seriesguide/jobs/episodes/EpisodeBaseJob.java @@ -49,7 +49,7 @@ protected long getShowId() { @NonNull @Override - protected List getEpisodesForNetworkJob(@NonNull Context context) { + protected List getAffectedEpisodes(@NonNull Context context) { List list = new ArrayList<>(); list.add(episode); return list; diff --git a/app/src/main/java/com/battlelancer/seriesguide/jobs/episodes/EpisodeCollectedJob.kt b/app/src/main/java/com/battlelancer/seriesguide/jobs/episodes/EpisodeCollectedJob.kt index 63a25031b2..daebf44e67 100644 --- a/app/src/main/java/com/battlelancer/seriesguide/jobs/episodes/EpisodeCollectedJob.kt +++ b/app/src/main/java/com/battlelancer/seriesguide/jobs/episodes/EpisodeCollectedJob.kt @@ -4,11 +4,15 @@ package com.battlelancer.seriesguide.jobs.episodes import android.content.Context import com.battlelancer.seriesguide.provider.SgRoomDatabase +import com.battlelancer.seriesguide.shows.database.SgEpisode2Numbers class EpisodeCollectedJob( episodeId: Long, private val isCollected: Boolean ) : EpisodeBaseJob(episodeId, if (isCollected) 1 else 0, JobAction.EPISODE_COLLECTION) { - override fun applyDatabaseChanges(context: Context): Boolean { + override fun applyDatabaseChanges( + context: Context, + episodes: List + ): Boolean { val updated = SgRoomDatabase.getInstance(context).sgEpisode2Helper() .updateCollected(episodeId, isCollected) return updated == 1 diff --git a/app/src/main/java/com/battlelancer/seriesguide/jobs/episodes/EpisodeWatchedJob.kt b/app/src/main/java/com/battlelancer/seriesguide/jobs/episodes/EpisodeWatchedJob.kt index 5ddd33b9c8..31bd962076 100644 --- a/app/src/main/java/com/battlelancer/seriesguide/jobs/episodes/EpisodeWatchedJob.kt +++ b/app/src/main/java/com/battlelancer/seriesguide/jobs/episodes/EpisodeWatchedJob.kt @@ -5,9 +5,9 @@ package com.battlelancer.seriesguide.jobs.episodes import android.content.Context import com.battlelancer.seriesguide.appwidget.ListWidgetProvider import com.battlelancer.seriesguide.provider.SgRoomDatabase +import com.battlelancer.seriesguide.shows.database.SgEpisode2Numbers import com.battlelancer.seriesguide.shows.episodes.EpisodeFlags import com.battlelancer.seriesguide.shows.episodes.EpisodeTools -import com.battlelancer.seriesguide.shows.history.SgActivityHelper class EpisodeWatchedJob( episodeId: Long, @@ -49,31 +49,10 @@ class EpisodeWatchedJob( } } - override fun applyLocalChanges(context: Context, requiresNetworkJob: Boolean): Boolean { - if (!super.applyLocalChanges(context, requiresNetworkJob)) { - return false - } - - // set a new last watched episode - // set last watched time to now if marking as watched or skipped - val unwatched = EpisodeTools.isUnwatched(flagValue) - updateLastWatched(context, getLastWatchedEpisodeId(context), !unwatched) - - if (EpisodeTools.isWatched(flagValue)) { - // create activity entry for watched episode - SgActivityHelper.addActivity(context, episodeId, showId) - } else if (unwatched) { - // remove any previous activity entries for this episode - // use case: user accidentally toggled watched flag - SgActivityHelper.removeActivity(context, episodeId) - } - - ListWidgetProvider.notifyDataChanged(context) - - return true - } - - override fun applyDatabaseChanges(context: Context): Boolean { + override fun applyDatabaseChanges( + context: Context, + episodes: List + ): Boolean { val episodeHelper = SgRoomDatabase.getInstance(context).sgEpisode2Helper() val flagValue = flagValue val rowsUpdated: Int = when (flagValue) { @@ -82,7 +61,20 @@ class EpisodeWatchedJob( EpisodeFlags.UNWATCHED -> episodeHelper.setNotWatchedAndRemovePlays(episodeId) else -> throw IllegalArgumentException("Flag value not supported") } - return rowsUpdated == 1 + val isSuccessful = rowsUpdated == 1 + + if (isSuccessful) { + // set a new last watched episode + // set last watched time to now if marking as watched or skipped + val unwatched = EpisodeTools.isUnwatched(flagValue) + updateLastWatched(context, getLastWatchedEpisodeId(context), !unwatched) + + // Add or remove activity entry + updateActivity(context, episodes) + + ListWidgetProvider.notifyDataChanged(context) + } + return isSuccessful } /** diff --git a/app/src/main/java/com/battlelancer/seriesguide/jobs/episodes/EpisodeWatchedUpToJob.kt b/app/src/main/java/com/battlelancer/seriesguide/jobs/episodes/EpisodeWatchedUpToJob.kt index d36e0e3afe..6475a8da88 100644 --- a/app/src/main/java/com/battlelancer/seriesguide/jobs/episodes/EpisodeWatchedUpToJob.kt +++ b/app/src/main/java/com/battlelancer/seriesguide/jobs/episodes/EpisodeWatchedUpToJob.kt @@ -18,27 +18,28 @@ class EpisodeWatchedUpToJob( private val episodeNumber: Int ) : BaseEpisodesJob(EpisodeFlags.WATCHED, JobAction.EPISODE_WATCHED_FLAG) { - override fun applyLocalChanges(context: Context, requiresNetworkJob: Boolean): Boolean { - if (!super.applyLocalChanges(context, requiresNetworkJob)) { - return false - } - - // we don't care about the last watched episode value - // always update last watched time, this type only marks as watched - updateLastWatched(context, -1, true) + override fun applyDatabaseChanges( + context: Context, + episodes: List + ): Boolean { + val rowsUpdated = SgRoomDatabase.getInstance(context).sgEpisode2Helper() + .setWatchedUpToAndAddPlay(showId, episodeFirstAired, episodeNumber) + val isSuccessful = rowsUpdated >= 0 // -1 means error - ListWidgetProvider.notifyDataChanged(context) + if (isSuccessful) { + // we don't care about the last watched episode value + // always update last watched time, this type only marks as watched + updateLastWatched(context, -1, true) - return true - } + // Add or remove activity entries + updateActivity(context, episodes) - override fun applyDatabaseChanges(context: Context): Boolean { - val rowsUpdated = SgRoomDatabase.getInstance(context).sgEpisode2Helper() - .setWatchedUpToAndAddPlay(showId, episodeFirstAired, episodeNumber) - return rowsUpdated >= 0 // -1 means error + ListWidgetProvider.notifyDataChanged(context) + } + return isSuccessful } - override fun getEpisodesForNetworkJob(context: Context): List { + override fun getAffectedEpisodes(context: Context): List { return SgRoomDatabase.getInstance(context).sgEpisode2Helper() .getEpisodeNumbersForWatchedUpTo(showId, episodeFirstAired, episodeNumber) } diff --git a/app/src/main/java/com/battlelancer/seriesguide/jobs/episodes/SeasonCollectedJob.kt b/app/src/main/java/com/battlelancer/seriesguide/jobs/episodes/SeasonCollectedJob.kt index 9494c88647..081f03f4d7 100644 --- a/app/src/main/java/com/battlelancer/seriesguide/jobs/episodes/SeasonCollectedJob.kt +++ b/app/src/main/java/com/battlelancer/seriesguide/jobs/episodes/SeasonCollectedJob.kt @@ -10,13 +10,13 @@ class SeasonCollectedJob( seasonId: Long, private val isCollected: Boolean ) : SeasonBaseJob(seasonId, if (isCollected) 1 else 0, JobAction.EPISODE_COLLECTION) { - override fun applyDatabaseChanges(context: Context): Boolean { + override fun applyDatabaseChanges(context: Context, episodes: List): Boolean { val rowsUpdated = SgRoomDatabase.getInstance(context).sgEpisode2Helper() .updateCollectedOfSeason(seasonId, isCollected) return rowsUpdated >= 0 // -1 means error. } - override fun getEpisodesForNetworkJob(context: Context): List { + override fun getAffectedEpisodes(context: Context): List { return SgRoomDatabase.getInstance(context).sgEpisode2Helper() .getEpisodeNumbersOfSeason(seasonId) } diff --git a/app/src/main/java/com/battlelancer/seriesguide/jobs/episodes/SeasonWatchedJob.kt b/app/src/main/java/com/battlelancer/seriesguide/jobs/episodes/SeasonWatchedJob.kt index bc81476693..0e1fff0abb 100644 --- a/app/src/main/java/com/battlelancer/seriesguide/jobs/episodes/SeasonWatchedJob.kt +++ b/app/src/main/java/com/battlelancer/seriesguide/jobs/episodes/SeasonWatchedJob.kt @@ -36,7 +36,10 @@ class SeasonWatchedJob(seasonId: Long, episodeFlags: Int) : } } - override fun applyDatabaseChanges(context: Context): Boolean { + override fun applyDatabaseChanges( + context: Context, + episodes: List + ): Boolean { val database = SgRoomDatabase.getInstance(context) val helper = database.sgEpisode2Helper() val rowsUpdated = when (flagValue) { @@ -45,18 +48,23 @@ class SeasonWatchedJob(seasonId: Long, episodeFlags: Int) : EpisodeFlags.UNWATCHED -> helper.setSeasonNotWatchedAndRemovePlays(seasonId) else -> throw IllegalArgumentException("Flag value not supported") } + val isSuccessful = rowsUpdated >= 0 // -1 means error. - // set a new last watched episode - // set last watched time to now if marking as watched or skipped - val unwatched = EpisodeTools.isUnwatched(flagValue) - updateLastWatched(context, getLastWatchedEpisodeId(context), !unwatched) + if (isSuccessful) { + // set a new last watched episode + // set last watched time to now if marking as watched or skipped + val unwatched = EpisodeTools.isUnwatched(flagValue) + updateLastWatched(context, getLastWatchedEpisodeId(context), !unwatched) - ListWidgetProvider.notifyDataChanged(context) + // Add or remove activity entries + updateActivity(context, episodes) - return rowsUpdated >= 0 // -1 means error. + ListWidgetProvider.notifyDataChanged(context) + } + return isSuccessful } - override fun getEpisodesForNetworkJob(context: Context): List { + override fun getAffectedEpisodes(context: Context): List { val helper = SgRoomDatabase.getInstance(context).sgEpisode2Helper() return if (EpisodeTools.isUnwatched(flagValue)) { // set unwatched diff --git a/app/src/main/java/com/battlelancer/seriesguide/jobs/episodes/ShowCollectedJob.kt b/app/src/main/java/com/battlelancer/seriesguide/jobs/episodes/ShowCollectedJob.kt index a8bf630601..dee1e2bf4a 100644 --- a/app/src/main/java/com/battlelancer/seriesguide/jobs/episodes/ShowCollectedJob.kt +++ b/app/src/main/java/com/battlelancer/seriesguide/jobs/episodes/ShowCollectedJob.kt @@ -11,13 +11,16 @@ class ShowCollectedJob( private val isCollected: Boolean ) : ShowBaseJob(showId, if (isCollected) 1 else 0, JobAction.EPISODE_COLLECTION) { - override fun applyDatabaseChanges(context: Context): Boolean { + override fun applyDatabaseChanges( + context: Context, + episodes: List + ): Boolean { val rowsUpdated = SgRoomDatabase.getInstance(context).sgEpisode2Helper() .updateCollectedOfShowExcludeSpecials(showId, isCollected) return rowsUpdated >= 0 // -1 means error. } - override fun getEpisodesForNetworkJob(context: Context): List { + override fun getAffectedEpisodes(context: Context): List { return SgRoomDatabase.getInstance(context).sgEpisode2Helper() .getEpisodeNumbersOfShow(showId) } diff --git a/app/src/main/java/com/battlelancer/seriesguide/jobs/episodes/ShowWatchedJob.kt b/app/src/main/java/com/battlelancer/seriesguide/jobs/episodes/ShowWatchedJob.kt index 76374bb825..06bf280e27 100644 --- a/app/src/main/java/com/battlelancer/seriesguide/jobs/episodes/ShowWatchedJob.kt +++ b/app/src/main/java/com/battlelancer/seriesguide/jobs/episodes/ShowWatchedJob.kt @@ -25,23 +25,16 @@ class ShowWatchedJob( if (!super.applyLocalChanges(context, requiresNetworkJob)) { return false } - val lastWatchedEpisodeId = - if (EpisodeTools.isUnwatched(flagValue)) { - 0L /* just reset */ - } else { - -1L /* we don't care */ - } - // set a new last watched episode - // set last watched time to now if marking as watched or skipped - updateLastWatched(context, lastWatchedEpisodeId, !EpisodeTools.isUnwatched(flagValue)) - ListWidgetProvider.notifyDataChanged(context) return true } - override fun applyDatabaseChanges(context: Context): Boolean { + override fun applyDatabaseChanges( + context: Context, + episodes: List + ): Boolean { val helper = SgRoomDatabase.getInstance(context).sgEpisode2Helper() val rowsUpdated: Int = when (flagValue) { EpisodeFlags.UNWATCHED -> helper.setShowNotWatchedAndRemovePlays(showId) @@ -51,10 +44,29 @@ class ShowWatchedJob( throw IllegalArgumentException("Flag value not supported") } } - return rowsUpdated >= 0 // -1 means error. + val isSuccessful = rowsUpdated >= 0 // -1 means error + + if (isSuccessful) { + val lastWatchedEpisodeId = + if (EpisodeTools.isUnwatched(flagValue)) { + 0L /* just reset */ + } else { + -1L /* we don't care */ + } + + // set a new last watched episode + // set last watched time to now if marking as watched or skipped + updateLastWatched(context, lastWatchedEpisodeId, !EpisodeTools.isUnwatched(flagValue)) + + // Add or remove activity entries + updateActivity(context, episodes) + + ListWidgetProvider.notifyDataChanged(context) + } + return isSuccessful } - override fun getEpisodesForNetworkJob(context: Context): List { + override fun getAffectedEpisodes(context: Context): List { val helper = SgRoomDatabase.getInstance(context).sgEpisode2Helper() return if (EpisodeTools.isUnwatched(flagValue)) { // set unwatched diff --git a/app/src/main/java/com/battlelancer/seriesguide/shows/database/SgEpisode2Helper.kt b/app/src/main/java/com/battlelancer/seriesguide/shows/database/SgEpisode2Helper.kt index 73df5ac84d..ce7a9bc4a6 100644 --- a/app/src/main/java/com/battlelancer/seriesguide/shows/database/SgEpisode2Helper.kt +++ b/app/src/main/java/com/battlelancer/seriesguide/shows/database/SgEpisode2Helper.kt @@ -90,7 +90,7 @@ interface SgEpisode2Helper { @Query("SELECT episode_tmdb_id FROM sg_episode WHERE _id = :episodeId") fun getEpisodeTmdbId(episodeId: Long): Int - @Query("SELECT _id, season_id, series_id, episode_number, episode_season_number, episode_plays FROM sg_episode WHERE _id = :episodeId") + @Query("SELECT _id, episode_tmdb_id, season_id, series_id, episode_number, episode_season_number, episode_plays FROM sg_episode WHERE _id = :episodeId") fun getEpisodeNumbers(episodeId: Long): SgEpisode2Numbers? @Query("SELECT _id, season_id, series_id, episode_tvdb_id, episode_title, episode_number, episode_absolute_number, episode_season_number, episode_dvd_number, episode_firstairedms, episode_watched, episode_collected FROM sg_episode WHERE _id = :episodeId") @@ -166,7 +166,7 @@ interface SgEpisode2Helper { /** * Also serves as compile time validation of [SgEpisode2Numbers.buildQuery] */ - @Query("SELECT _id, season_id, series_id, episode_number, episode_season_number, episode_plays FROM sg_episode WHERE season_id = :seasonId ORDER BY episode_season_number ASC, episode_number ASC") + @Query("SELECT _id, episode_tmdb_id, season_id, series_id, episode_number, episode_season_number, episode_plays FROM sg_episode WHERE season_id = :seasonId ORDER BY episode_season_number ASC, episode_number ASC") fun getEpisodeNumbersOfSeason(seasonId: Long): List @RawQuery(observedEntities = [SgEpisode2::class]) @@ -175,7 +175,7 @@ interface SgEpisode2Helper { /** * Excludes specials. */ - @Query("SELECT _id, season_id, series_id, episode_number, episode_season_number, episode_plays FROM sg_episode WHERE series_id = :showId AND episode_season_number != 0 ORDER BY episode_season_number ASC, episode_number ASC") + @Query("SELECT _id, episode_tmdb_id, season_id, series_id, episode_number, episode_season_number, episode_plays FROM sg_episode WHERE series_id = :showId AND episode_season_number != 0 ORDER BY episode_season_number ASC, episode_number ASC") fun getEpisodeNumbersOfShow(showId: Long): List @Query("SELECT _id, episode_number, episode_season_number, episode_watched, episode_plays, episode_collected FROM sg_episode WHERE series_id = :showId AND episode_tmdb_id > 0 AND (episode_watched != ${EpisodeFlags.UNWATCHED} OR episode_collected = 1)") @@ -305,7 +305,7 @@ interface SgEpisode2Helper { /** * See [setWatchedUpToAndAddPlay] for which episodes are returned. */ - @Query("""SELECT _id, season_id, series_id, episode_number, episode_season_number, episode_plays FROM sg_episode WHERE series_id = :showId + @Query("""SELECT _id, episode_tmdb_id, season_id, series_id, episode_number, episode_season_number, episode_plays FROM sg_episode WHERE series_id = :showId AND ( episode_firstairedms < :episodeFirstAired OR (episode_firstairedms = :episodeFirstAired AND episode_number <= :episodeNumber) @@ -319,7 +319,7 @@ interface SgEpisode2Helper { * Note: keep in sync with [setSeasonNotWatchedAndRemovePlays]. */ @Query( - """SELECT _id, season_id, series_id, episode_number, episode_season_number, episode_plays FROM sg_episode + """SELECT _id, episode_tmdb_id, season_id, series_id, episode_number, episode_season_number, episode_plays FROM sg_episode WHERE season_id = :seasonId AND episode_watched != ${EpisodeFlags.UNWATCHED} ORDER BY episode_season_number ASC, episode_number ASC""" ) @@ -368,7 +368,7 @@ interface SgEpisode2Helper { * Note: keep in sync with [setSeasonSkipped] and [setSeasonWatchedAndAddPlay]. */ @Query( - """SELECT _id, season_id, series_id, episode_number, episode_season_number, episode_plays FROM sg_episode + """SELECT _id, episode_tmdb_id, season_id, series_id, episode_number, episode_season_number, episode_plays FROM sg_episode WHERE season_id = :seasonId AND episode_watched != ${EpisodeFlags.WATCHED} ORDER BY episode_season_number ASC, episode_number ASC""" ) @@ -405,7 +405,7 @@ interface SgEpisode2Helper { * Note: keep in sync with [setShowNotWatchedAndRemovePlays]. */ @Query( - """SELECT _id, season_id, series_id, episode_number, episode_season_number, episode_plays FROM sg_episode + """SELECT _id, episode_tmdb_id, season_id, series_id, episode_number, episode_season_number, episode_plays FROM sg_episode WHERE series_id = :showId AND episode_watched != ${EpisodeFlags.UNWATCHED} AND episode_season_number != 0 ORDER BY episode_season_number ASC, episode_number ASC""" @@ -428,7 +428,7 @@ interface SgEpisode2Helper { * Note: keep in sync with [setShowWatchedAndAddPlay]. */ @Query( - """SELECT _id, season_id, series_id, episode_number, episode_season_number, episode_plays FROM sg_episode + """SELECT _id, episode_tmdb_id, season_id, series_id, episode_number, episode_season_number, episode_plays FROM sg_episode WHERE series_id = :showId AND episode_watched != ${EpisodeFlags.WATCHED} AND episode_firstairedms <= :currentTimePlusOneHour AND episode_firstairedms != -1 AND episode_season_number != 0 @@ -687,6 +687,7 @@ data class SgEpisode2Ids( data class SgEpisode2Numbers( @ColumnInfo(name = SgEpisode2Columns._ID) val id: Long, + @ColumnInfo(name = SgEpisode2Columns.TMDB_ID) val tmdbId: Int?, @ColumnInfo(name = SgSeason2Columns.REF_SEASON_ID) val seasonId: Long, @ColumnInfo(name = SgShow2Columns.REF_SHOW_ID) val showId: Long, @ColumnInfo(name = SgEpisode2Columns.NUMBER) val episodenumber: Int, @@ -701,7 +702,7 @@ data class SgEpisode2Numbers( fun buildQuery(seasonId: Long, order: EpisodesSettings.EpisodeSorting): SimpleSQLiteQuery { val orderClause = order.query() return SimpleSQLiteQuery( - "SELECT _id, season_id, series_id, episode_number, episode_season_number, episode_plays FROM sg_episode WHERE season_id = $seasonId ORDER BY $orderClause" + "SELECT _id, episode_tmdb_id, season_id, series_id, episode_number, episode_season_number, episode_plays FROM sg_episode WHERE season_id = $seasonId ORDER BY $orderClause" ) } } diff --git a/app/src/main/java/com/battlelancer/seriesguide/shows/history/SgActivity.kt b/app/src/main/java/com/battlelancer/seriesguide/shows/history/SgActivity.kt index 5693211288..13b454bef7 100644 --- a/app/src/main/java/com/battlelancer/seriesguide/shows/history/SgActivity.kt +++ b/app/src/main/java/com/battlelancer/seriesguide/shows/history/SgActivity.kt @@ -11,6 +11,8 @@ import com.battlelancer.seriesguide.provider.SeriesGuideContract.ActivityColumns import com.battlelancer.seriesguide.provider.SeriesGuideDatabase.Tables /** + * Episode watched activity. Uses stable TMDB IDs to work when a show is removed and re-added. + * * Note: ensure to use CONFLICT_REPLACE when inserting to mimic SQLite UNIQUE x ON CONFLICT REPLACE. */ @Entity( @@ -32,6 +34,9 @@ data class SgActivity ( ) object ActivityType { + /** + * Only used for reading, new entries only added if a TMDB ID exists. + */ const val TVDB_ID = 1 const val TMDB_ID = 2 } diff --git a/app/src/main/java/com/battlelancer/seriesguide/shows/history/SgActivityHelper.kt b/app/src/main/java/com/battlelancer/seriesguide/shows/history/SgActivityHelper.kt index 5c65d91c37..4dac74d698 100644 --- a/app/src/main/java/com/battlelancer/seriesguide/shows/history/SgActivityHelper.kt +++ b/app/src/main/java/com/battlelancer/seriesguide/shows/history/SgActivityHelper.kt @@ -1,5 +1,5 @@ -// Copyright 2023 Uwe Trottmann // SPDX-License-Identifier: Apache-2.0 +// Copyright 2021-2024 Uwe Trottmann package com.battlelancer.seriesguide.shows.history @@ -9,6 +9,7 @@ import androidx.room.Dao import androidx.room.Insert import androidx.room.OnConflictStrategy import androidx.room.Query +import androidx.room.Transaction import com.battlelancer.seriesguide.provider.SgRoomDatabase import timber.log.Timber @@ -19,7 +20,7 @@ import timber.log.Timber interface SgActivityHelper { @Insert(onConflict = OnConflictStrategy.REPLACE) - fun insertActivity(activity: SgActivity) + fun insertActivities(activities: List) @Query("DELETE FROM activity WHERE activity_time < :deleteOlderThanMs") fun deleteOldActivity(deleteOlderThanMs: Long): Int @@ -27,86 +28,68 @@ interface SgActivityHelper { @Query("DELETE FROM activity WHERE activity_episode = :episodeStableId AND activity_type = :type") fun deleteActivity(episodeStableId: String, type: Int): Int + @Transaction + fun deleteActivities(episodeStableId: List, type: Int): Int { + var deleted = 0 + episodeStableId.forEach { + deleted += deleteActivity(it, type) + } + return deleted + } + @Query("SELECT * FROM activity ORDER BY activity_time DESC") fun getActivityByLatest(): List companion object { - private const val HISTORY_THRESHOLD = 30 * DateUtils.DAY_IN_MILLIS + private const val HISTORY_THRESHOLD = 90 * DateUtils.DAY_IN_MILLIS /** - * Adds an activity entry for the given episode with the current time as timestamp. + * Adds activity entries for the given episode TMDB IDs with the current time as timestamp. * If an entry already exists it is replaced. * * Also cleans up old entries. */ - @JvmStatic - fun addActivity(context: Context, episodeId: Long, showId: Long) { - // Need to use global IDs (in case a show is removed and added again). + fun addActivitiesForEpisodes( + context: Context, + showTmdbId: Int, + episodeTmdbIds: List + ) { val database = SgRoomDatabase.getInstance(context) - - // Try using TMDB ID - var type = ActivityType.TMDB_ID - var showStableIdOrZero = database.sgShow2Helper().getShowTmdbId(showId) - var episodeStableIdOrZero = database.sgEpisode2Helper().getEpisodeTmdbId(episodeId) - - // Fall back to TVDB ID - if (showStableIdOrZero == 0 || episodeStableIdOrZero == 0) { - type = ActivityType.TVDB_ID - showStableIdOrZero = database.sgShow2Helper().getShowTvdbId(showId) - episodeStableIdOrZero = database.sgEpisode2Helper().getEpisodeTvdbId(episodeId) - if (showStableIdOrZero == 0 || episodeStableIdOrZero == 0) { - // Should never happen: have neither TMDB or TVDB ID. - Timber.e( - "Failed to add activity, no TMDB or TVDB ID for show %d episode %d", - showId, - episodeId - ) - return - } - } - - val timeMonthAgo = System.currentTimeMillis() - HISTORY_THRESHOLD val helper = database.sgActivityHelper() // delete all entries older than 30 days + val timeMonthAgo = System.currentTimeMillis() - HISTORY_THRESHOLD val deleted = helper.deleteOldActivity(timeMonthAgo) Timber.d("addActivity: removed %d outdated activities", deleted) // add new entry val currentTime = System.currentTimeMillis() - val activity = SgActivity( - null, - episodeStableIdOrZero.toString(), - showStableIdOrZero.toString(), - currentTime, - type - ) - helper.insertActivity(activity) - Timber.d("addActivity: episode: %d timestamp: %d", episodeId, currentTime) + episodeTmdbIds.map { + SgActivity( + null, + it.toString(), + showTmdbId.toString(), + currentTime, + ActivityType.TMDB_ID + ) + }.also { + helper.insertActivities(it) + Timber.d("Added %d activities with time %d", it.size, currentTime) + } } /** - * Tries to remove any activity with the given episode id. + * Tries to remove any activity with the given episode TMDB IDs. */ - @JvmStatic - fun removeActivity(context: Context, episodeId: Long) { - // Need to use global IDs (in case a show is removed and added again). - val database = SgRoomDatabase.getInstance(context) - - // Try removal using TMDB ID. - var deleted = 0 - val episodeTmdbIdOrZero = database.sgEpisode2Helper().getEpisodeTmdbId(episodeId) - if (episodeTmdbIdOrZero != 0) { - deleted += database.sgActivityHelper() - .deleteActivity(episodeTmdbIdOrZero.toString(), ActivityType.TMDB_ID) - } - // Try removal using TVDB ID. - val episodeTvdbIdOrZero = database.sgEpisode2Helper().getEpisodeTvdbId(episodeId) - if (episodeTvdbIdOrZero != 0) { - deleted += database.sgActivityHelper() - .deleteActivity(episodeTvdbIdOrZero.toString(), ActivityType.TVDB_ID) - } - Timber.d("removeActivity: deleted %d activity entries", deleted) + fun removeActivitiesForEpisodes( + context: Context, + episodeTmdbIds: List + ) { + SgRoomDatabase.getInstance(context).sgActivityHelper() + .deleteActivities(episodeTmdbIds.map { it.toString() }, ActivityType.TMDB_ID) + .also { + Timber.d("Deleted %d activity entries for %d episodes", it, episodeTmdbIds.size) + } } }