diff --git a/app/src/main/java/com/battlelancer/seriesguide/dataliberation/AutoBackupFragment.kt b/app/src/main/java/com/battlelancer/seriesguide/dataliberation/AutoBackupFragment.kt index 4027462fe3..d9e554a988 100644 --- a/app/src/main/java/com/battlelancer/seriesguide/dataliberation/AutoBackupFragment.kt +++ b/app/src/main/java/com/battlelancer/seriesguide/dataliberation/AutoBackupFragment.kt @@ -1,5 +1,5 @@ -// Copyright 2023 Uwe Trottmann // SPDX-License-Identifier: Apache-2.0 +// Copyright 2013-2024 Uwe Trottmann package com.battlelancer.seriesguide.dataliberation @@ -63,7 +63,7 @@ class AutoBackupFragment : Fragment() { } binding.buttonAutoBackupNow.setOnClickListener { - if (TaskManager.getInstance().tryBackupTask(requireContext())) { + if (TaskManager.tryBackupTask(requireContext())) { setProgressLock(true) } } diff --git a/app/src/main/java/com/battlelancer/seriesguide/dataliberation/JsonExportTask.kt b/app/src/main/java/com/battlelancer/seriesguide/dataliberation/JsonExportTask.kt index a9fcdf83a9..650bc4036c 100644 --- a/app/src/main/java/com/battlelancer/seriesguide/dataliberation/JsonExportTask.kt +++ b/app/src/main/java/com/battlelancer/seriesguide/dataliberation/JsonExportTask.kt @@ -174,7 +174,7 @@ class JsonExportTask( } private fun onPostExecute(result: Int) { - TaskManager.getInstance().releaseBackupTaskRef() + TaskManager.releaseBackupTaskRef() if (!isAutoBackupMode) { val messageId: Int diff --git a/app/src/main/java/com/battlelancer/seriesguide/dataliberation/JsonImportTask.kt b/app/src/main/java/com/battlelancer/seriesguide/dataliberation/JsonImportTask.kt index 72b79335f0..a62e5959be 100644 --- a/app/src/main/java/com/battlelancer/seriesguide/dataliberation/JsonImportTask.kt +++ b/app/src/main/java/com/battlelancer/seriesguide/dataliberation/JsonImportTask.kt @@ -1,5 +1,5 @@ -// Copyright 2023 Uwe Trottmann // SPDX-License-Identifier: Apache-2.0 +// Copyright 2013-2024 Uwe Trottmann package com.battlelancer.seriesguide.dataliberation @@ -120,8 +120,7 @@ class JsonImportTask( private fun doInBackground(coroutineScope: CoroutineScope): Int { // Ensure no large database ops are running - val tm = TaskManager.getInstance() - if (SgSyncAdapter.isSyncActive(context, false) || tm.isAddTaskRunning) { + if (SgSyncAdapter.isSyncActive(context, false) || TaskManager.isAddTaskRunning) { return ERROR_LARGE_DB_OP } diff --git a/app/src/main/java/com/battlelancer/seriesguide/history/HistoryActivity.kt b/app/src/main/java/com/battlelancer/seriesguide/history/HistoryActivity.kt index f79d8a9e5c..f6d0af2530 100644 --- a/app/src/main/java/com/battlelancer/seriesguide/history/HistoryActivity.kt +++ b/app/src/main/java/com/battlelancer/seriesguide/history/HistoryActivity.kt @@ -7,18 +7,15 @@ import android.os.Bundle import androidx.fragment.app.Fragment import com.battlelancer.seriesguide.BuildConfig import com.battlelancer.seriesguide.R -import com.battlelancer.seriesguide.shows.search.discover.AddShowDialogFragment.OnAddShowListener -import com.battlelancer.seriesguide.shows.search.discover.SearchResult import com.battlelancer.seriesguide.ui.BaseActivity import com.battlelancer.seriesguide.ui.SinglePaneActivity -import com.battlelancer.seriesguide.util.TaskManager import com.battlelancer.seriesguide.util.commitReorderingAllowed import timber.log.Timber /** * Displays history of watched episodes or movies. */ -class HistoryActivity : BaseActivity(), OnAddShowListener { +class HistoryActivity : BaseActivity() { interface InitBundle { companion object { @@ -60,13 +57,6 @@ class HistoryActivity : BaseActivity(), OnAddShowListener { supportActionBar?.setDisplayHomeAsUpEnabled(true) } - /** - * Called if the user adds a show from a trakt stream fragment. - */ - override fun onAddShow(show: SearchResult) { - TaskManager.getInstance().performAddTask(this, show) - } - companion object { const val EPISODES_LOADER_ID = 100 const val MOVIES_LOADER_ID = 101 diff --git a/app/src/main/java/com/battlelancer/seriesguide/history/UserEpisodeStreamFragment.kt b/app/src/main/java/com/battlelancer/seriesguide/history/UserEpisodeStreamFragment.kt index ec4539ad45..27276d64c4 100644 --- a/app/src/main/java/com/battlelancer/seriesguide/history/UserEpisodeStreamFragment.kt +++ b/app/src/main/java/com/battlelancer/seriesguide/history/UserEpisodeStreamFragment.kt @@ -81,7 +81,11 @@ class UserEpisodeStreamFragment : StreamFragment() { ) } else { // Offer to add the show if not in database. - AddShowDialogFragment.show(parentFragmentManager, showTmdbId) + AddShowDialogFragment.show( + requireContext(), + parentFragmentManager, + showTmdbId + ) } } } diff --git a/app/src/main/java/com/battlelancer/seriesguide/movies/similar/SimilarMoviesActivity.kt b/app/src/main/java/com/battlelancer/seriesguide/movies/similar/SimilarMoviesActivity.kt index 02ef160fd2..fff9ed5656 100644 --- a/app/src/main/java/com/battlelancer/seriesguide/movies/similar/SimilarMoviesActivity.kt +++ b/app/src/main/java/com/battlelancer/seriesguide/movies/similar/SimilarMoviesActivity.kt @@ -1,5 +1,5 @@ -// Copyright 2023 Uwe Trottmann // SPDX-License-Identifier: Apache-2.0 +// Copyright 2023-2024 Uwe Trottmann package com.battlelancer.seriesguide.movies.similar @@ -14,11 +14,11 @@ class SimilarMoviesActivity : BaseSimilarActivity() { override val liftOnScrollTargetViewId: Int = SimilarShowsFragment.liftOnScrollTargetViewId override val titleStringRes: Int = R.string.title_similar_movies - override fun createFragment(tmdbId: Int, title: String?): Fragment = + override fun createFragment(tmdbId: Int, title: String): Fragment = SimilarMoviesFragment.newInstance(tmdbId, title) companion object { - fun intent(context: Context, movieTmdbId: Int, title: String?): Intent { + fun intent(context: Context, movieTmdbId: Int, title: String): Intent { return Intent(context, SimilarMoviesActivity::class.java) .putExtras(movieTmdbId, title) } diff --git a/app/src/main/java/com/battlelancer/seriesguide/movies/similar/SimilarMoviesFragment.kt b/app/src/main/java/com/battlelancer/seriesguide/movies/similar/SimilarMoviesFragment.kt index f628f13008..87e2f994f7 100644 --- a/app/src/main/java/com/battlelancer/seriesguide/movies/similar/SimilarMoviesFragment.kt +++ b/app/src/main/java/com/battlelancer/seriesguide/movies/similar/SimilarMoviesFragment.kt @@ -1,5 +1,5 @@ // SPDX-License-Identifier: Apache-2.0 -// Copyright 2023-2024 Uwe Trottmann +// Copyright 2019-2024 Uwe Trottmann package com.battlelancer.seriesguide.movies.similar @@ -119,7 +119,7 @@ class SimilarMoviesFragment : Fragment() { private const val ARG_TMDB_ID = "ARG_TMDB_ID" private const val ARG_TITLE = "ARG_TITLE" - fun newInstance(tmdbId: Int, title: String?): SimilarMoviesFragment { + fun newInstance(tmdbId: Int, title: String): SimilarMoviesFragment { return SimilarMoviesFragment().apply { arguments = Bundle().apply { putInt(ARG_TMDB_ID, tmdbId) diff --git a/app/src/main/java/com/battlelancer/seriesguide/shows/FirstRunView.kt b/app/src/main/java/com/battlelancer/seriesguide/shows/FirstRunView.kt index 14cff4578c..3d2e205603 100644 --- a/app/src/main/java/com/battlelancer/seriesguide/shows/FirstRunView.kt +++ b/app/src/main/java/com/battlelancer/seriesguide/shows/FirstRunView.kt @@ -1,5 +1,5 @@ -// Copyright 2023 Uwe Trottmann // SPDX-License-Identifier: Apache-2.0 +// Copyright 2018-2024 Uwe Trottmann package com.battlelancer.seriesguide.shows @@ -63,7 +63,7 @@ class FirstRunView @JvmOverloads constructor(context: Context, attrs: AttributeS putBoolean(DisplaySettings.KEY_PREVENT_SPOILERS, noSpoilers) } // update next episode strings right away - TaskManager.getInstance().tryNextEpisodeUpdateTask(v.context) + TaskManager.tryNextEpisodeUpdateTask(v.context) // show binding.checkboxNoSpoilers.isChecked = noSpoilers } diff --git a/app/src/main/java/com/battlelancer/seriesguide/shows/ShowsActivityImpl.kt b/app/src/main/java/com/battlelancer/seriesguide/shows/ShowsActivityImpl.kt index 43f3488f51..4520f96a7b 100644 --- a/app/src/main/java/com/battlelancer/seriesguide/shows/ShowsActivityImpl.kt +++ b/app/src/main/java/com/battlelancer/seriesguide/shows/ShowsActivityImpl.kt @@ -23,7 +23,6 @@ import com.battlelancer.seriesguide.shows.calendar.UpcomingFragment import com.battlelancer.seriesguide.shows.episodes.EpisodesActivity import com.battlelancer.seriesguide.shows.history.ShowsHistoryFragment import com.battlelancer.seriesguide.shows.search.discover.AddShowDialogFragment -import com.battlelancer.seriesguide.shows.search.discover.SearchResult import com.battlelancer.seriesguide.shows.search.discover.ShowsDiscoverFragment import com.battlelancer.seriesguide.shows.search.discover.ShowsDiscoverPagingActivity import com.battlelancer.seriesguide.sync.AccountUtils @@ -47,7 +46,7 @@ import kotlinx.coroutines.launch * Provides the apps main screen, displays tabs for shows, discover, history, * recent and upcoming episodes. Runs upgrade code and checks billing state. */ -open class ShowsActivityImpl : BaseTopActivity(), AddShowDialogFragment.OnAddShowListener { +open class ShowsActivityImpl : BaseTopActivity() { private lateinit var tabsAdapter: TabStripAdapter private lateinit var viewPager: ViewPager2 @@ -154,7 +153,7 @@ open class ShowsActivityImpl : BaseTopActivity(), AddShowDialogFragment.OnAddSho } } else { // Show not added, offer to. - AddShowDialogFragment.show(supportFragmentManager, showTmdbId) + AddShowDialogFragment.show(this, supportFragmentManager, showTmdbId) } } } else if (Intents.ACTION_VIEW_SHOW == action) { @@ -170,7 +169,7 @@ open class ShowsActivityImpl : BaseTopActivity(), AddShowDialogFragment.OnAddSho viewIntent = OverviewActivity.intentShow(this, showId) } else { // no such show, offer to add it - AddShowDialogFragment.show(supportFragmentManager, showTmdbId) + AddShowDialogFragment.show(this, supportFragmentManager, showTmdbId) } } @@ -312,7 +311,7 @@ open class ShowsActivityImpl : BaseTopActivity(), AddShowDialogFragment.OnAddSho } // update next episodes - TaskManager.getInstance().tryNextEpisodeUpdateTask(this) + TaskManager.tryNextEpisodeUpdateTask(this) } override fun onPause() { @@ -332,13 +331,6 @@ open class ShowsActivityImpl : BaseTopActivity(), AddShowDialogFragment.OnAddSho return keyCode == KeyEvent.KEYCODE_BACK } - /** - * Called if the user adds a show from a trakt stream fragment. - */ - override fun onAddShow(show: SearchResult) { - TaskManager.getInstance().performAddTask(this, show) - } - override val snackbarParentView: View get() = findViewById(R.id.coordinatorLayoutShows) diff --git a/app/src/main/java/com/battlelancer/seriesguide/shows/ShowsDistillationFragment.kt b/app/src/main/java/com/battlelancer/seriesguide/shows/ShowsDistillationFragment.kt index 8655309747..771deb5760 100644 --- a/app/src/main/java/com/battlelancer/seriesguide/shows/ShowsDistillationFragment.kt +++ b/app/src/main/java/com/battlelancer/seriesguide/shows/ShowsDistillationFragment.kt @@ -147,7 +147,7 @@ class ShowsDistillationFragment : AppCompatDialogFragment() { override fun onNoReleasedChanged(value: Boolean) { DisplaySettings.setNoReleasedEpisodes(requireContext(), value) - TaskManager.getInstance().tryNextEpisodeUpdateTask(requireContext()) + TaskManager.tryNextEpisodeUpdateTask(requireContext()) } } diff --git a/app/src/main/java/com/battlelancer/seriesguide/shows/history/ShowsHistoryFragment.kt b/app/src/main/java/com/battlelancer/seriesguide/shows/history/ShowsHistoryFragment.kt index f2d9e42189..0a0cce89a4 100644 --- a/app/src/main/java/com/battlelancer/seriesguide/shows/history/ShowsHistoryFragment.kt +++ b/app/src/main/java/com/battlelancer/seriesguide/shows/history/ShowsHistoryFragment.kt @@ -361,7 +361,7 @@ class ShowsHistoryFragment : Fragment() { showDetails(view, episodeRowId) } else if (showTmdbId != null && showTmdbId > 0) { // episode missing: show likely not in database, suggest adding it - AddShowDialogFragment.show(parentFragmentManager, showTmdbId) + AddShowDialogFragment.show(requireContext(), parentFragmentManager, showTmdbId) } } } diff --git a/app/src/main/java/com/battlelancer/seriesguide/shows/search/SearchActivityImpl.kt b/app/src/main/java/com/battlelancer/seriesguide/shows/search/SearchActivityImpl.kt index 8c950b6e32..56d051caf3 100644 --- a/app/src/main/java/com/battlelancer/seriesguide/shows/search/SearchActivityImpl.kt +++ b/app/src/main/java/com/battlelancer/seriesguide/shows/search/SearchActivityImpl.kt @@ -14,12 +14,9 @@ import android.view.inputmethod.EditorInfo import androidx.viewpager2.widget.ViewPager2 import com.battlelancer.seriesguide.R import com.battlelancer.seriesguide.databinding.ActivitySearchBinding -import com.battlelancer.seriesguide.shows.search.discover.AddShowDialogFragment -import com.battlelancer.seriesguide.shows.search.discover.SearchResult import com.battlelancer.seriesguide.ui.BaseMessageActivity import com.battlelancer.seriesguide.ui.TabStripAdapter import com.battlelancer.seriesguide.util.TabClickEvent -import com.battlelancer.seriesguide.util.TaskManager import com.battlelancer.seriesguide.util.ThemeUtils import com.battlelancer.seriesguide.util.ViewTools import com.google.android.gms.actions.SearchIntents @@ -33,7 +30,7 @@ import org.greenrobot.eventbus.EventBus * When [SearchManager.APP_DATA] contains a [EpisodeSearchFragment.ARG_SHOW_TITLE] switches to the * episodes tab. */ -open class SearchActivityImpl : BaseMessageActivity(), AddShowDialogFragment.OnAddShowListener { +open class SearchActivityImpl : BaseMessageActivity() { private lateinit var binding: ActivitySearchBinding @@ -193,11 +190,6 @@ open class SearchActivityImpl : BaseMessageActivity(), AddShowDialogFragment.OnA EventBus.getDefault().removeStickyEvent(SearchQueryEvent::class.java) } - override fun onAddShow(show: SearchResult) { - TaskManager.getInstance().performAddTask(this, show) - } - - override val snackbarParentView: View get() = findViewById(R.id.coordinatorLayoutSearch) diff --git a/app/src/main/java/com/battlelancer/seriesguide/shows/search/discover/AddShowDialogFragment.kt b/app/src/main/java/com/battlelancer/seriesguide/shows/search/discover/AddShowDialogFragment.kt index e262322fea..5d2d9c9141 100644 --- a/app/src/main/java/com/battlelancer/seriesguide/shows/search/discover/AddShowDialogFragment.kt +++ b/app/src/main/java/com/battlelancer/seriesguide/shows/search/discover/AddShowDialogFragment.kt @@ -19,6 +19,7 @@ import com.battlelancer.seriesguide.R import com.battlelancer.seriesguide.databinding.DialogAddshowBinding import com.battlelancer.seriesguide.shows.ShowsSettings import com.battlelancer.seriesguide.shows.search.similar.SimilarShowsFragment +import com.battlelancer.seriesguide.shows.tools.AddShowTask import com.battlelancer.seriesguide.shows.tools.ShowStatus import com.battlelancer.seriesguide.streaming.StreamingSearch import com.battlelancer.seriesguide.ui.OverviewActivity @@ -28,6 +29,7 @@ import com.battlelancer.seriesguide.util.LanguageTools import com.battlelancer.seriesguide.util.RatingsTools.initialize import com.battlelancer.seriesguide.util.RatingsTools.setRatingValues import com.battlelancer.seriesguide.util.ServiceUtils +import com.battlelancer.seriesguide.util.TaskManager import com.battlelancer.seriesguide.util.TextTools import com.battlelancer.seriesguide.util.TimeTools import com.battlelancer.seriesguide.util.ViewTools @@ -47,12 +49,7 @@ import timber.log.Timber */ class AddShowDialogFragment : AppCompatDialogFragment() { - interface OnAddShowListener { - fun onAddShow(show: SearchResult) - } - private var binding: DialogAddshowBinding? = null - private lateinit var addShowListener: OnAddShowListener private var showTmdbId: Int = 0 private lateinit var languageCode: String private val model by viewModels { @@ -61,20 +58,9 @@ class AddShowDialogFragment : AppCompatDialogFragment() { override fun onAttach(context: Context) { super.onAttach(context) - try { - addShowListener = context as OnAddShowListener - } catch (e: ClassCastException) { - throw ClassCastException("$context must implement OnAddShowListener") - } - showTmdbId = requireArguments().getInt(ARG_INT_SHOW_TMDBID) - val languageCodeOrNull = requireArguments().getString(ARG_STRING_LANGUAGE_CODE) - if (languageCodeOrNull.isNullOrEmpty()) { - // Use search language. - this.languageCode = ShowsSettings.getShowsSearchLanguage(context) - } else { - this.languageCode = languageCodeOrNull - } + languageCode = requireArguments().getString(ARG_STRING_LANGUAGE_CODE) + ?: throw IllegalArgumentException("Language code must not be null") } override fun onCreate(savedInstanceState: Bundle?) { @@ -132,10 +118,12 @@ class AddShowDialogFragment : AppCompatDialogFragment() { val details = model.showDetails.value if (details?.show != null) { dismissAllowingStateLoss() - SimilarShowsFragment.displaySimilarShowsEventLiveData.postValue(SearchResult().also { - it.tmdbId = showTmdbId - it.title = details.show.title - }) + SimilarShowsFragment.displaySimilarShowsEventLiveData.postValue( + SimilarShowsFragment.SimilarShowEvent( + tmdbId = showTmdbId, + title = details.show.title + ) + ) } } } @@ -236,11 +224,10 @@ class AddShowDialogFragment : AppCompatDialogFragment() { binding.buttonPositive.setText(R.string.action_shows_add) binding.buttonPositive.setOnClickListener { EventBus.getDefault().post(OnAddingShowEvent(showTmdbId)) - addShowListener.onAddShow(SearchResult().also { - it.tmdbId = showTmdbId - it.title = show.title - it.language = languageCode - }) + TaskManager.performAddTask( + requireContext(), + AddShowTask.Show(showTmdbId, languageCode, show.title) + ) dismiss() } } @@ -308,32 +295,27 @@ class AddShowDialogFragment : AppCompatDialogFragment() { private const val ARG_STRING_LANGUAGE_CODE = "language" /** - * Display a [AddShowDialogFragment] for the given show. The language of the show should - * be set. + * Display an [AddShowDialogFragment] for the given show. The language of the show should + * be set, otherwise uses [ShowsSettings.getShowsSearchLanguage]. */ - @JvmStatic - fun show(fm: FragmentManager, show: SearchResult) { + fun show(fm: FragmentManager, showTmdbId: Int, languageCode: String) { // Replace any currently showing add dialog (do not add it to the back stack). val ft = fm.beginTransaction() val prev = fm.findFragmentByTag(TAG) if (prev != null) { ft.remove(prev) } - newInstance(show.tmdbId, show.language).safeShow(fm, ft, TAG) + newInstance(showTmdbId, languageCode).safeShow(fm, ft, TAG) } /** - * Display a [AddShowDialogFragment] for the given show. + * Display an [AddShowDialogFragment] for the given show. * - * Use if there is no actual search result but just an id available. Uses the search - * or fall back language. + * Use if there is just an id available. The language code is always + * [ShowsSettings.getShowsSearchLanguage]. */ - @JvmStatic - fun show(fm: FragmentManager, showTmdbId: Int) { - val fakeResult = SearchResult().apply { - tmdbId = showTmdbId - } - show(fm, fakeResult) + fun show(context: Context, fm: FragmentManager, showTmdbId: Int) { + show(fm, showTmdbId, ShowsSettings.getShowsSearchLanguage(context)) } private fun newInstance(showTmdbId: Int, languageCode: String?): AddShowDialogFragment { diff --git a/app/src/main/java/com/battlelancer/seriesguide/shows/search/discover/AddShowPopupMenu.kt b/app/src/main/java/com/battlelancer/seriesguide/shows/search/discover/AddShowPopupMenu.kt index edcf744c65..695ae4110a 100644 --- a/app/src/main/java/com/battlelancer/seriesguide/shows/search/discover/AddShowPopupMenu.kt +++ b/app/src/main/java/com/battlelancer/seriesguide/shows/search/discover/AddShowPopupMenu.kt @@ -9,6 +9,7 @@ import android.view.MenuItem import android.view.View import androidx.appcompat.widget.PopupMenu import com.battlelancer.seriesguide.R +import com.battlelancer.seriesguide.shows.tools.AddShowTask import com.battlelancer.seriesguide.util.TaskManager import com.battlelancer.seriesguide.util.tasks.AddShowToWatchlistTask import com.battlelancer.seriesguide.util.tasks.RemoveShowFromWatchlistTask @@ -39,7 +40,9 @@ class AddShowPopupMenu( R.id.menu_action_add_show_add -> { // post so other fragments can display a progress indicator for that show EventBus.getDefault().post(OnAddingShowEvent(show.tmdbId)) - TaskManager.getInstance().performAddTask(context, show) + TaskManager.performAddTask( + context, AddShowTask.Show(show.tmdbId, show.languageCode, show.title) + ) true } diff --git a/app/src/main/java/com/battlelancer/seriesguide/shows/search/discover/BaseShowResultsDataSource.kt b/app/src/main/java/com/battlelancer/seriesguide/shows/search/discover/BaseShowResultsDataSource.kt index e01955272b..3662769805 100644 --- a/app/src/main/java/com/battlelancer/seriesguide/shows/search/discover/BaseShowResultsDataSource.kt +++ b/app/src/main/java/com/battlelancer/seriesguide/shows/search/discover/BaseShowResultsDataSource.kt @@ -58,8 +58,8 @@ abstract class BaseShowResultsDataSource( nextKey = null ) } else { - val searchResults = SearchTools.mapTvShowsToSearchResults(languageCode, shows) - SearchTools.markLocalShowsAsAddedAndPreferLocalPoster(context, searchResults) + val searchResults = TmdbSearchResultMapper(context, languageCode) + .mapToSearchResults(shows) LoadResult.Page( data = searchResults, prevKey = null, // Only paging forward. diff --git a/app/src/main/java/com/battlelancer/seriesguide/shows/search/discover/ItemAddShowClickListener.kt b/app/src/main/java/com/battlelancer/seriesguide/shows/search/discover/ItemAddShowClickListener.kt index f778500737..77c5ccb115 100644 --- a/app/src/main/java/com/battlelancer/seriesguide/shows/search/discover/ItemAddShowClickListener.kt +++ b/app/src/main/java/com/battlelancer/seriesguide/shows/search/discover/ItemAddShowClickListener.kt @@ -9,6 +9,7 @@ import androidx.fragment.app.FragmentManager import androidx.lifecycle.Lifecycle import androidx.lifecycle.coroutineScope import com.battlelancer.seriesguide.provider.SgRoomDatabase +import com.battlelancer.seriesguide.shows.tools.AddShowTask import com.battlelancer.seriesguide.traktapi.TraktCredentials import com.battlelancer.seriesguide.ui.OverviewActivity import com.battlelancer.seriesguide.util.TaskManager @@ -38,14 +39,16 @@ open class ItemAddShowClickListener( } } else { // Display more details in a dialog. - AddShowDialogFragment.show(fragmentManager, item) + AddShowDialogFragment.show(fragmentManager, item.tmdbId, item.languageCode) } } } override fun onAddClick(item: SearchResult) { EventBus.getDefault().post(OnAddingShowEvent(item.tmdbId)) - TaskManager.getInstance().performAddTask(context, item) + TaskManager.performAddTask( + context, AddShowTask.Show(item.tmdbId, item.languageCode, item.title) + ) } override fun onMoreOptionsClick(view: View, show: SearchResult) { diff --git a/app/src/main/java/com/battlelancer/seriesguide/shows/search/discover/ItemAddShowViewHolder.kt b/app/src/main/java/com/battlelancer/seriesguide/shows/search/discover/ItemAddShowViewHolder.kt index 5ec1a1a207..bc17ba0a57 100644 --- a/app/src/main/java/com/battlelancer/seriesguide/shows/search/discover/ItemAddShowViewHolder.kt +++ b/app/src/main/java/com/battlelancer/seriesguide/shows/search/discover/ItemAddShowViewHolder.kt @@ -73,19 +73,8 @@ class ItemAddShowViewHolder( isVisible = true } - // image - // If item is provided from Trakt source, it does not provide images, - // so need to resolve them. - val posterUrl = ImageTools.posterUrlOrResolve( - item.posterPath, - item.tmdbId, - item.language, - context - ) - ImageTools.loadShowPosterUrlResizeCrop( - context, binding.imageViewAddPoster, - posterUrl - ) + // poster + ImageTools.loadShowPosterUrlResizeCrop(context, binding.imageViewAddPoster, item.posterUrl) // context/long press listener and more options button val canBeAdded = item.state == SearchResult.STATE_ADD diff --git a/app/src/main/java/com/battlelancer/seriesguide/shows/search/discover/SearchResult.java b/app/src/main/java/com/battlelancer/seriesguide/shows/search/discover/SearchResult.java deleted file mode 100644 index 211901a047..0000000000 --- a/app/src/main/java/com/battlelancer/seriesguide/shows/search/discover/SearchResult.java +++ /dev/null @@ -1,140 +0,0 @@ -// Copyright 2023 Uwe Trottmann -// SPDX-License-Identifier: Apache-2.0 - -package com.battlelancer.seriesguide.shows.search.discover; - -import android.os.Parcel; -import android.os.Parcelable; - -/** - * Holds a search result, used later for adding this show. Supplying a poster URL is optional. - */ -public class SearchResult implements Parcelable { - - public static final int STATE_ADD = 0; - public static final int STATE_ADDING = 1; - public static final int STATE_ADDED = 2; - - private int tvdbid; - private int tmdbId; - private String language; - private String title; - private String overview; - private String posterPath; - private int state; - - public static final Parcelable.Creator CREATOR - = new Parcelable.Creator() { - public SearchResult createFromParcel(Parcel in) { - return new SearchResult(in); - } - - public SearchResult[] newArray(int size) { - return new SearchResult[size]; - } - }; - - public SearchResult() { - } - - public SearchResult(Parcel in) { - setTvdbid(in.readInt()); - setTmdbId(in.readInt()); - setLanguage(in.readString()); - setTitle(in.readString()); - setOverview(in.readString()); - setPosterPath(in.readString()); - setState(in.readInt()); - } - - public SearchResult copy() { - SearchResult copy = new SearchResult(); - copy.setTvdbid(this.getTvdbid()); - copy.setTmdbId(this.getTmdbId()); - copy.setLanguage(this.getLanguage()); - copy.setTitle(this.getTitle()); - copy.setOverview(this.getOverview()); - copy.setPosterPath(this.getPosterPath()); - copy.setState(this.getState()); - return copy; - } - - @Override - public int describeContents() { - return hashCode(); - } - - @Override - public void writeToParcel(Parcel dest, int flags) { - dest.writeInt(getTvdbid()); - dest.writeInt(getTmdbId()); - dest.writeString(getLanguage()); - dest.writeString(getTitle()); - dest.writeString(getOverview()); - dest.writeString(getPosterPath()); - dest.writeInt(getState()); - } - - /** - * @deprecated Use {@link #getTmdbId()} instead. - */ - public int getTvdbid() { - return tvdbid; - } - - /** - * @deprecated Use {@link #setTmdbId(int)} instead. - */ - public void setTvdbid(int tvdbid) { - this.tvdbid = tvdbid; - } - - public int getTmdbId() { - return tmdbId; - } - - public void setTmdbId(int tmdbId) { - this.tmdbId = tmdbId; - } - - /** Two-letter ISO 639-1 language code plus ISO-3166-1 region tag. */ - public String getLanguage() { - return language; - } - - public void setLanguage(String language) { - this.language = language; - } - - public String getTitle() { - return title; - } - - public void setTitle(String title) { - this.title = title; - } - - public String getOverview() { - return overview; - } - - public void setOverview(String overview) { - this.overview = overview; - } - - public String getPosterPath() { - return posterPath; - } - - public void setPosterPath(String posterPath) { - this.posterPath = posterPath; - } - - public int getState() { - return state; - } - - public void setState(int state) { - this.state = state; - } -} diff --git a/app/src/main/java/com/battlelancer/seriesguide/shows/search/discover/SearchResult.kt b/app/src/main/java/com/battlelancer/seriesguide/shows/search/discover/SearchResult.kt new file mode 100644 index 0000000000..3b4bad7ba6 --- /dev/null +++ b/app/src/main/java/com/battlelancer/seriesguide/shows/search/discover/SearchResult.kt @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2018-2024 Uwe Trottmann + +package com.battlelancer.seriesguide.shows.search.discover + +/** + * Holds a search result. Supplying a poster URL is optional. + */ +data class SearchResult( + val tmdbId: Int, + /** Two-letter ISO 639-1 language code plus ISO-3166-1 region tag. */ + val languageCode: String, + val title: String, + val overview: String, + var posterUrl: String?, + var state: Int = STATE_ADD +) { + companion object { + const val STATE_ADD: Int = 0 + const val STATE_ADDING: Int = 1 + const val STATE_ADDED: Int = 2 + } +} diff --git a/app/src/main/java/com/battlelancer/seriesguide/shows/search/discover/SearchResultDiffCallback.kt b/app/src/main/java/com/battlelancer/seriesguide/shows/search/discover/SearchResultDiffCallback.kt index eaaca86437..176c1f3bd4 100644 --- a/app/src/main/java/com/battlelancer/seriesguide/shows/search/discover/SearchResultDiffCallback.kt +++ b/app/src/main/java/com/battlelancer/seriesguide/shows/search/discover/SearchResultDiffCallback.kt @@ -14,8 +14,8 @@ class SearchResultDiffCallback : DiffUtil.ItemCallback() { override fun areContentsTheSame(oldItem: SearchResult, newItem: SearchResult): Boolean = oldItem.state == newItem.state - && oldItem.language == newItem.language - && oldItem.posterPath == newItem.posterPath + && oldItem.languageCode == newItem.languageCode + && oldItem.posterUrl == newItem.posterUrl && oldItem.overview == newItem.overview } \ No newline at end of file diff --git a/app/src/main/java/com/battlelancer/seriesguide/shows/search/discover/SearchResultMapper.kt b/app/src/main/java/com/battlelancer/seriesguide/shows/search/discover/SearchResultMapper.kt new file mode 100644 index 0000000000..a26865e1cb --- /dev/null +++ b/app/src/main/java/com/battlelancer/seriesguide/shows/search/discover/SearchResultMapper.kt @@ -0,0 +1,125 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2019-2024 Uwe Trottmann + +package com.battlelancer.seriesguide.shows.search.discover + +import android.content.Context +import com.battlelancer.seriesguide.SgApp +import com.battlelancer.seriesguide.util.ImageTools +import com.uwetrottmann.tmdb2.entities.BaseTvShow +import com.uwetrottmann.trakt5.entities.BaseShow + +/** + * See [mapToSearchResults]. + */ +abstract class SearchResultMapper( + private val context: Context +) { + + /** + * Maps to a list of [SearchResult] with [mapToSearchResult]. + * + * For added shows, changes [SearchResult.state] to [SearchResult.STATE_ADDED] and if available + * uses the poster path of that show (which might differ if the added show is set to a different + * language). + * + * Builds the final poster URL with [buildPosterUrl]. + */ + fun mapToSearchResults(shows: List): List { + val localShowsToPoster = + SgApp.getServicesComponent(context).showTools().getTmdbIdsToPoster() + return shows.mapNotNull { show -> + val searchResult = mapToSearchResult(show) + ?: return@mapNotNull null + + if (localShowsToPoster.indexOfKey(searchResult.tmdbId) >= 0) { + // Is already in local database. + searchResult.state = SearchResult.STATE_ADDED + // Use the poster already fetched for it. + val posterPathOrNull = localShowsToPoster[searchResult.tmdbId] + if (posterPathOrNull != null) { + searchResult.posterUrl = posterPathOrNull + } + } + + // It may take some time to build the image cache URL, so do this here instead of when + // binding to the view. + searchResult.posterUrl = buildPosterUrl(searchResult) + + searchResult + } + } + + abstract fun mapToSearchResult(show: SHOW): SearchResult? + + abstract fun buildPosterUrl(searchResult: SearchResult): String? + +} + +class TmdbSearchResultMapper( + private val context: Context, + private val languageCode: String +) : SearchResultMapper(context) { + + override fun mapToSearchResult(show: BaseTvShow): SearchResult? { + val tmdbId = show.id ?: return null + val name = show.name ?: return null + return SearchResult( + tmdbId = tmdbId, + title = name, + overview = show.overview ?: "", + languageCode = languageCode, + posterUrl = show.poster_path // temporarily store path + ) + } + + override fun buildPosterUrl(searchResult: SearchResult): String? { + return ImageTools.tmdbOrTvdbPosterUrl(searchResult.posterUrl, context) + } + +} + +/** + * Maps Trakt shows to a list of [SearchResult]. + */ +class TraktSearchResultMapper( + private val context: Context, + private val languageCode: String +) : SearchResultMapper(context) { + + override fun mapToSearchResult(show: BaseShow): SearchResult? { + val traktShow = show.show + val tmdbId = traktShow?.ids?.tmdb ?: return null // has no TMDB id + val title = traktShow.title ?: return null + return SearchResult( + tmdbId = tmdbId, + title = title, + // Trakt might not return an overview, so use the year if available + overview = if (!traktShow.overview.isNullOrEmpty()) { + traktShow.overview + } else if (traktShow.year != null) { + traktShow.year!!.toString() + } else { + "" + }, + languageCode = languageCode, + posterUrl = null // Trakt does not supply poster URLs + ) + } + + /** + * Uses [ImageTools.posterUrlOrResolve] to build the final poster URL or a special resolve URL. + * + * Trakt does not return posters, so sets a special URL that makes the image loader resolve + * them. But for added shows uses their TMDB poster path to build a regular URL. + */ + override fun buildPosterUrl(searchResult: SearchResult): String? { + return ImageTools.posterUrlOrResolve( + searchResult.posterUrl, + searchResult.tmdbId, + searchResult.languageCode, + context + ) + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/battlelancer/seriesguide/shows/search/discover/SearchTools.kt b/app/src/main/java/com/battlelancer/seriesguide/shows/search/discover/SearchTools.kt deleted file mode 100644 index c38110f3f5..0000000000 --- a/app/src/main/java/com/battlelancer/seriesguide/shows/search/discover/SearchTools.kt +++ /dev/null @@ -1,55 +0,0 @@ -// Copyright 2023 Uwe Trottmann -// SPDX-License-Identifier: Apache-2.0 - -package com.battlelancer.seriesguide.shows.search.discover - -import android.content.Context -import com.battlelancer.seriesguide.SgApp -import com.uwetrottmann.tmdb2.entities.BaseTvShow - -object SearchTools { - - /** - * Maps TMDB TV shows to search results. - */ - fun mapTvShowsToSearchResults( - languageCode: String, - results: List - ): List { - return results.mapNotNull { tvShow -> - val tmdbId = tvShow.id ?: return@mapNotNull null - SearchResult().also { - it.tmdbId = tmdbId - it.title = tvShow.name - it.overview = tvShow.overview - it.language = languageCode - it.posterPath = tvShow.poster_path - } - } - } - - /** - * Replaces with local poster (e.g. if the user added the show in a different language to - * ensure it shows up with the same poster and to avoid fetching another image). - */ - fun markLocalShowsAsAddedAndPreferLocalPoster(context: Context, results: List?) { - if (results == null) { - return - } - - val localShowsToPoster = - SgApp.getServicesComponent(context).showTools().getTmdbIdsToPoster() - for (result in results) { - if (localShowsToPoster.indexOfKey(result.tmdbId) >= 0) { - // Is already in local database. - result.state = SearchResult.STATE_ADDED - // Use the poster already fetched for it. - val posterOrNull = localShowsToPoster[result.tmdbId] - if (posterOrNull != null) { - result.posterPath = posterOrNull - } - } - } - } - -} \ No newline at end of file diff --git a/app/src/main/java/com/battlelancer/seriesguide/shows/search/discover/ShowsDiscoverLiveData.kt b/app/src/main/java/com/battlelancer/seriesguide/shows/search/discover/ShowsDiscoverLiveData.kt index d3499eeb1a..75f583eadf 100644 --- a/app/src/main/java/com/battlelancer/seriesguide/shows/search/discover/ShowsDiscoverLiveData.kt +++ b/app/src/main/java/com/battlelancer/seriesguide/shows/search/discover/ShowsDiscoverLiveData.kt @@ -67,7 +67,7 @@ class ShowsDiscoverLiveData( } private suspend fun getShowsWithNewEpisodes( - language: String, + languageCode: String, watchProviderIds: List?, watchRegion: String?, firstReleaseYear: Int?, @@ -76,7 +76,7 @@ class ShowsDiscoverLiveData( val tmdb = SgApp.getServicesComponent(context.applicationContext).tmdb() val results = TmdbTools2().getShowsWithNewEpisodes( tmdb = tmdb, - language = language, + language = languageCode, page = 1, firstReleaseYear = firstReleaseYear, originalLanguage = originalLanguage, @@ -85,8 +85,8 @@ class ShowsDiscoverLiveData( )?.results val result = if (results != null) { - val searchResults = SearchTools.mapTvShowsToSearchResults(language, results) - SearchTools.markLocalShowsAsAddedAndPreferLocalPoster(context, searchResults) + val searchResults = TmdbSearchResultMapper(context, languageCode) + .mapToSearchResults(results) buildResultSuccess(searchResults, R.string.add_empty) } else { buildResultFailure() diff --git a/app/src/main/java/com/battlelancer/seriesguide/shows/search/discover/ShowsDiscoverPagingActivity.kt b/app/src/main/java/com/battlelancer/seriesguide/shows/search/discover/ShowsDiscoverPagingActivity.kt index 183b4c244e..b47045e116 100644 --- a/app/src/main/java/com/battlelancer/seriesguide/shows/search/discover/ShowsDiscoverPagingActivity.kt +++ b/app/src/main/java/com/battlelancer/seriesguide/shows/search/discover/ShowsDiscoverPagingActivity.kt @@ -15,7 +15,6 @@ import com.battlelancer.seriesguide.shows.search.discover.ShowsDiscoverPagingAct import com.battlelancer.seriesguide.shows.search.similar.SimilarShowsActivity import com.battlelancer.seriesguide.shows.search.similar.SimilarShowsFragment import com.battlelancer.seriesguide.ui.BaseMessageActivity -import com.battlelancer.seriesguide.util.TaskManager import com.battlelancer.seriesguide.util.ThemeUtils import com.battlelancer.seriesguide.util.commitReorderingAllowed @@ -26,7 +25,7 @@ import com.battlelancer.seriesguide.util.commitReorderingAllowed * If launched with [intentLink] the search bar can be shown with a menu item and hidden by * going up or back. */ -class ShowsDiscoverPagingActivity : BaseMessageActivity(), AddShowDialogFragment.OnAddShowListener { +class ShowsDiscoverPagingActivity : BaseMessageActivity() { // Re-using layout of movies as filter chips are currently identical lateinit var binding: ActivityMoviesSearchBinding @@ -73,10 +72,6 @@ class ShowsDiscoverPagingActivity : BaseMessageActivity(), AddShowDialogFragment return intent.getStringExtra(EXTRA_QUERY) } - override fun onAddShow(show: SearchResult) { - TaskManager.getInstance().performAddTask(this, show) - } - companion object { private const val EXTRA_LINK = "link" private const val EXTRA_QUERY = "query" diff --git a/app/src/main/java/com/battlelancer/seriesguide/shows/search/discover/ShowsDiscoverPagingFragment.kt b/app/src/main/java/com/battlelancer/seriesguide/shows/search/discover/ShowsDiscoverPagingFragment.kt index e16757e313..f4d8a3be05 100644 --- a/app/src/main/java/com/battlelancer/seriesguide/shows/search/discover/ShowsDiscoverPagingFragment.kt +++ b/app/src/main/java/com/battlelancer/seriesguide/shows/search/discover/ShowsDiscoverPagingFragment.kt @@ -311,7 +311,7 @@ class ShowsDiscoverPagingFragment : BaseAddShowsFragment() { val showTmdbId = TmdbIdExtractor(requireContext(), query).tryToExtract() if (showTmdbId > 0) { // found an id, display the add dialog - AddShowDialogFragment.show(parentFragmentManager, showTmdbId) + AddShowDialogFragment.show(requireContext(), parentFragmentManager, showTmdbId) } else { // no id, do a search instead searchEditText.setText(query) diff --git a/app/src/main/java/com/battlelancer/seriesguide/shows/search/discover/ShowsTraktActivity.kt b/app/src/main/java/com/battlelancer/seriesguide/shows/search/discover/ShowsTraktActivity.kt index 3b0231c202..eb19ce3c5c 100644 --- a/app/src/main/java/com/battlelancer/seriesguide/shows/search/discover/ShowsTraktActivity.kt +++ b/app/src/main/java/com/battlelancer/seriesguide/shows/search/discover/ShowsTraktActivity.kt @@ -11,14 +11,13 @@ import com.battlelancer.seriesguide.databinding.ActivityTraktShowsBinding import com.battlelancer.seriesguide.shows.search.similar.SimilarShowsActivity import com.battlelancer.seriesguide.shows.search.similar.SimilarShowsFragment import com.battlelancer.seriesguide.ui.BaseMessageActivity -import com.battlelancer.seriesguide.util.TaskManager import com.battlelancer.seriesguide.util.ThemeUtils import com.battlelancer.seriesguide.util.commitReorderingAllowed /** * Hosts [TraktAddFragment] configured by [DiscoverShowsLink]. */ -class ShowsTraktActivity : BaseMessageActivity(), AddShowDialogFragment.OnAddShowListener { +class ShowsTraktActivity : BaseMessageActivity() { lateinit var binding: ActivityTraktShowsBinding @@ -61,10 +60,6 @@ class ShowsTraktActivity : BaseMessageActivity(), AddShowDialogFragment.OnAddSho setTitle(link.titleRes) } - override fun onAddShow(show: SearchResult) { - TaskManager.getInstance().performAddTask(this, show) - } - companion object { const val EXTRA_LINK = "LINK" const val TRAKT_BASE_LOADER_ID = 200 diff --git a/app/src/main/java/com/battlelancer/seriesguide/shows/search/discover/TraktAddFragment.kt b/app/src/main/java/com/battlelancer/seriesguide/shows/search/discover/TraktAddFragment.kt index 39f4fd09c8..b11e42804b 100644 --- a/app/src/main/java/com/battlelancer/seriesguide/shows/search/discover/TraktAddFragment.kt +++ b/app/src/main/java/com/battlelancer/seriesguide/shows/search/discover/TraktAddFragment.kt @@ -169,17 +169,26 @@ class TraktAddFragment : Fragment() { if (itemId == R.id.menu_add_all) { val searchResults = searchResults if (searchResults != null) { - val showsToAdd = LinkedList() + val showsToAdd = LinkedList() // only include shows not already added for (result in searchResults) { if (result.state == SearchResult.STATE_ADD) { - showsToAdd.add(result) + showsToAdd.add( + AddShowTask.Show( + result.tmdbId, + result.languageCode, + result.title + ) + ) result.state = SearchResult.STATE_ADDING } } EventBus.getDefault().post(OnAddingShowEvent()) - TaskManager.getInstance() - .performAddTask(context, showsToAdd, false, false) + TaskManager.performAddTask( + requireContext(), showsToAdd, + isSilentMode = false, + isMergingShows = false + ) } // disable the item so the user has to come back menuItem.isEnabled = false diff --git a/app/src/main/java/com/battlelancer/seriesguide/shows/search/discover/TraktAddLoader.kt b/app/src/main/java/com/battlelancer/seriesguide/shows/search/discover/TraktAddLoader.kt index b9f34cde2c..6c75127cba 100644 --- a/app/src/main/java/com/battlelancer/seriesguide/shows/search/discover/TraktAddLoader.kt +++ b/app/src/main/java/com/battlelancer/seriesguide/shows/search/discover/TraktAddLoader.kt @@ -5,7 +5,6 @@ package com.battlelancer.seriesguide.shows.search.discover import android.content.Context import androidx.annotation.StringRes -import androidx.collection.SparseArrayCompat import com.battlelancer.seriesguide.R import com.battlelancer.seriesguide.SgApp import com.battlelancer.seriesguide.shows.ShowsSettings @@ -97,13 +96,11 @@ class TraktAddLoader( return buildResultSuccess(emptyList()) } - return buildResultSuccess( - parseTraktShowsToSearchResults( - shows, - SgApp.getServicesComponent(context).showTools().getTmdbIdsToPoster(), - ShowsSettings.getShowsSearchLanguage(context) - ) - ) + + val searchResults = + TraktSearchResultMapper(context, ShowsSettings.getShowsSearchLanguage(context)) + .mapToSearchResults(shows) + return buildResultSuccess(searchResults) } private fun buildResultSuccess(results: List): Result { @@ -124,45 +121,4 @@ class TraktAddLoader( return Result(LinkedList(), context, errorResId) } - - /** - * Transforms a list of Trakt shows to a list of [SearchResult], marks shows already in - * the local database as added. - */ - private fun parseTraktShowsToSearchResults( - traktShows: List, - existingPosterPaths: SparseArrayCompat, - overrideLanguage: String - ): List { - val results: MutableList = ArrayList() - - // build list - for (baseShow in traktShows) { - val show = baseShow.show - val tmdbId = show?.ids?.tmdb - ?: continue // has no TMDB id - - val result = SearchResult().also { - it.tmdbId = tmdbId - it.title = show.title - // Trakt might not return an overview, so use the year if available - it.overview = if (!show.overview.isNullOrEmpty()) { - show.overview - } else if (show.year != null) { - show.year!!.toString() - } else { - "" - } - if (existingPosterPaths.indexOfKey(tmdbId) >= 0) { - // is already in local database - it.state = SearchResult.STATE_ADDED - // use the poster fetched for it (or null if there is none) - it.posterPath = existingPosterPaths[tmdbId] - } - it.language = overrideLanguage - } - results.add(result) - } - return results - } } diff --git a/app/src/main/java/com/battlelancer/seriesguide/shows/search/similar/SimilarShowsActivity.kt b/app/src/main/java/com/battlelancer/seriesguide/shows/search/similar/SimilarShowsActivity.kt index 5ca52fb936..965037a47a 100644 --- a/app/src/main/java/com/battlelancer/seriesguide/shows/search/similar/SimilarShowsActivity.kt +++ b/app/src/main/java/com/battlelancer/seriesguide/shows/search/similar/SimilarShowsActivity.kt @@ -1,5 +1,5 @@ -// Copyright 2023 Uwe Trottmann // SPDX-License-Identifier: Apache-2.0 +// Copyright 2019-2024 Uwe Trottmann package com.battlelancer.seriesguide.shows.search.similar @@ -8,16 +8,13 @@ import android.content.Intent import android.os.Bundle import androidx.fragment.app.Fragment import com.battlelancer.seriesguide.R -import com.battlelancer.seriesguide.shows.search.discover.AddShowDialogFragment -import com.battlelancer.seriesguide.shows.search.discover.SearchResult import com.battlelancer.seriesguide.ui.BaseSimilarActivity -import com.battlelancer.seriesguide.util.TaskManager -class SimilarShowsActivity : BaseSimilarActivity(), AddShowDialogFragment.OnAddShowListener { +class SimilarShowsActivity : BaseSimilarActivity() { override val liftOnScrollTargetViewId: Int = SimilarShowsFragment.liftOnScrollTargetViewId override val titleStringRes: Int = R.string.title_similar_shows - override fun createFragment(tmdbId: Int, title: String?): Fragment = + override fun createFragment(tmdbId: Int, title: String): Fragment = SimilarShowsFragment.newInstance(tmdbId, title) override fun onCreate(savedInstanceState: Bundle?) { @@ -29,12 +26,8 @@ class SimilarShowsActivity : BaseSimilarActivity(), AddShowDialogFragment.OnAddS } } - override fun onAddShow(show: SearchResult) { - TaskManager.getInstance().performAddTask(this, show) - } - companion object { - fun intent(context: Context, showTmdbId: Int, showTitle: String?): Intent { + fun intent(context: Context, showTmdbId: Int, showTitle: String): Intent { return Intent(context, SimilarShowsActivity::class.java) .putExtras(showTmdbId, showTitle) } diff --git a/app/src/main/java/com/battlelancer/seriesguide/shows/search/similar/SimilarShowsFragment.kt b/app/src/main/java/com/battlelancer/seriesguide/shows/search/similar/SimilarShowsFragment.kt index 8969297ce3..a4389433d5 100644 --- a/app/src/main/java/com/battlelancer/seriesguide/shows/search/similar/SimilarShowsFragment.kt +++ b/app/src/main/java/com/battlelancer/seriesguide/shows/search/similar/SimilarShowsFragment.kt @@ -18,7 +18,6 @@ import androidx.lifecycle.Lifecycle import androidx.recyclerview.widget.RecyclerView import com.battlelancer.seriesguide.R import com.battlelancer.seriesguide.shows.search.discover.BaseAddShowsFragment -import com.battlelancer.seriesguide.shows.search.discover.SearchResult import com.battlelancer.seriesguide.shows.search.discover.ShowsDiscoverPagingActivity import com.battlelancer.seriesguide.traktapi.TraktCredentials import com.battlelancer.seriesguide.ui.AutoGridLayoutManager @@ -151,6 +150,11 @@ class SimilarShowsFragment : BaseAddShowsFragment() { similarShowsViewModel.setStateForTmdbId(showTmdbId, newState) } + data class SimilarShowEvent( + val tmdbId: Int, + val title: String + ) + companion object { val liftOnScrollTargetViewId = R.id.recyclerViewShowsSimilar @@ -159,9 +163,9 @@ class SimilarShowsFragment : BaseAddShowsFragment() { private const val MENU_ITEM_SEARCH_ID = 1 @JvmStatic - val displaySimilarShowsEventLiveData = SingleLiveEvent() + val displaySimilarShowsEventLiveData = SingleLiveEvent() - fun newInstance(showTmdbId: Int, showTitle: String?): SimilarShowsFragment { + fun newInstance(showTmdbId: Int, showTitle: String): SimilarShowsFragment { return SimilarShowsFragment().apply { arguments = Bundle().apply { putInt(ARG_SHOW_TMDB_ID, showTmdbId) diff --git a/app/src/main/java/com/battlelancer/seriesguide/shows/search/similar/SimilarShowsViewModel.kt b/app/src/main/java/com/battlelancer/seriesguide/shows/search/similar/SimilarShowsViewModel.kt index a3663acf94..be1ee64c23 100644 --- a/app/src/main/java/com/battlelancer/seriesguide/shows/search/similar/SimilarShowsViewModel.kt +++ b/app/src/main/java/com/battlelancer/seriesguide/shows/search/similar/SimilarShowsViewModel.kt @@ -11,7 +11,7 @@ import com.battlelancer.seriesguide.R import com.battlelancer.seriesguide.SgApp import com.battlelancer.seriesguide.shows.ShowsSettings import com.battlelancer.seriesguide.shows.search.discover.SearchResult -import com.battlelancer.seriesguide.shows.search.discover.SearchTools +import com.battlelancer.seriesguide.shows.search.discover.TmdbSearchResultMapper import com.battlelancer.seriesguide.util.Errors import com.uwetrottmann.androidutils.AndroidUtils import kotlinx.coroutines.Dispatchers @@ -63,10 +63,8 @@ class SimilarShowsViewModel( page.results } - val searchResults = SearchTools.mapTvShowsToSearchResults(languageCode, results) - // Mark local shows and use existing posters. - SearchTools.markLocalShowsAsAddedAndPreferLocalPoster(context, searchResults) - + val searchResults = TmdbSearchResultMapper(context, languageCode) + .mapToSearchResults(results) postSuccessfulResult(searchResults) } } diff --git a/app/src/main/java/com/battlelancer/seriesguide/shows/tools/AddShowTask.java b/app/src/main/java/com/battlelancer/seriesguide/shows/tools/AddShowTask.java deleted file mode 100644 index d176f6b51e..0000000000 --- a/app/src/main/java/com/battlelancer/seriesguide/shows/tools/AddShowTask.java +++ /dev/null @@ -1,361 +0,0 @@ -// Copyright 2023 Uwe Trottmann -// SPDX-License-Identifier: Apache-2.0 - -package com.battlelancer.seriesguide.shows.tools; - -import android.annotation.SuppressLint; -import android.content.Context; -import android.os.AsyncTask; -import android.widget.Toast; -import androidx.annotation.Nullable; -import androidx.preference.PreferenceManager; -import com.battlelancer.seriesguide.R; -import com.battlelancer.seriesguide.SgApp; -import com.battlelancer.seriesguide.backend.settings.HexagonSettings; -import com.battlelancer.seriesguide.modules.ServicesComponent; -import com.battlelancer.seriesguide.provider.SeriesGuideDatabase; -import com.battlelancer.seriesguide.shows.search.discover.SearchResult; -import com.battlelancer.seriesguide.shows.tools.AddUpdateShowTools.ShowResult; -import com.battlelancer.seriesguide.sync.HexagonEpisodeSync; -import com.battlelancer.seriesguide.traktapi.TraktCredentials; -import com.battlelancer.seriesguide.traktapi.TraktSettings; -import com.battlelancer.seriesguide.traktapi.TraktTools2; -import com.battlelancer.seriesguide.util.Errors; -import com.battlelancer.seriesguide.util.TaskManager; -import com.uwetrottmann.androidutils.AndroidUtils; -import com.uwetrottmann.trakt5.entities.BaseShow; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; -import kotlin.Pair; -import org.greenrobot.eventbus.EventBus; -import timber.log.Timber; - -/** - * Adds shows to the local database, tries to get watched and collected episodes if a trakt account - * is connected. - */ -public class AddShowTask extends AsyncTask { - - public static class OnShowAddedEvent { - - public final boolean successful; - /** - * Is -1 if add task was aborted. - */ - public final int showTmdbId; - private final String message; - - private OnShowAddedEvent(int showTmdbId, String message, boolean successful) { - this.showTmdbId = showTmdbId; - this.message = message; - this.successful = successful; - } - - public void handle(Context context) { - if (message != null) { - Toast.makeText(context, message, Toast.LENGTH_LONG).show(); - } - } - - public static OnShowAddedEvent successful(int showTmdbId) { - return new OnShowAddedEvent(showTmdbId, null, true); - } - - public static OnShowAddedEvent exists(Context context, int showTmdbId, String showTitle) { - return new OnShowAddedEvent(showTmdbId, - context.getString(R.string.add_already_exists, showTitle), true); - } - - public static OnShowAddedEvent failed(Context context, int showTmdbId, String showTitle) { - return new OnShowAddedEvent(showTmdbId, - context.getString(R.string.add_error, showTitle), - false); - } - - public static OnShowAddedEvent failedDetails(Context context, int showTmdbId, - String showTitle, String details) { - return new OnShowAddedEvent(showTmdbId, - String.format("%s %s", context.getString(R.string.add_error, showTitle), - details), - false); - } - - public static OnShowAddedEvent aborted(String message) { - return new OnShowAddedEvent(-1, message, false); - } - } - - private static final int PROGRESS_EXISTS = 0; - private static final int PROGRESS_SUCCESS = 1; - private static final int PROGRESS_ERROR = 2; - private static final int PROGRESS_ERROR_TMDB = 3; - private static final int PROGRESS_ERROR_DOES_NOT_EXIST = 4; - private static final int PROGRESS_ERROR_HEXAGON = 6; - private static final int PROGRESS_ERROR_DATA = 7; - private static final int RESULT_OFFLINE = 8; - private static final int RESULT_TRAKT_API_ERROR = 9; - private static final int RESULT_TRAKT_AUTH_ERROR = 10; - - @SuppressLint("StaticFieldLeak") private final Context context; - private final LinkedList addQueue = new LinkedList<>(); - - private boolean isFinishedAddingShows = false; - private boolean isSilentMode; - private boolean isMergingShows; - - public AddShowTask(Context context, List shows, boolean isSilentMode, - boolean isMergingShows) { - this.context = context.getApplicationContext(); - this.addQueue.addAll(shows); - this.isSilentMode = isSilentMode; - this.isMergingShows = isMergingShows; - } - - /** - * Adds shows to the add queue. If this returns false, the shows were not added because the task - * is finishing up. Create a new one instead. - */ - public boolean addShows(List show, boolean isSilentMode, boolean isMergingShows) { - if (isFinishedAddingShows) { - Timber.d("addShows: failed, already finishing up."); - return false; - } else { - this.isSilentMode = isSilentMode; - // never reset isMergingShows once true, so merged flag is correctly set on completion - this.isMergingShows = this.isMergingShows || isMergingShows; - addQueue.addAll(show); - Timber.d("addShows: added shows to queue."); - return true; - } - } - - @Override - protected Void doInBackground(Void... params) { - Timber.d("Starting to add shows..."); - - SearchResult firstShow = addQueue.peek(); - if (firstShow == null) { - Timber.d("Finished. Queue was empty."); - return null; - } - - if (!AndroidUtils.isNetworkConnected(context)) { - Timber.d("Finished. No internet connection."); - publishProgress(RESULT_OFFLINE, firstShow.getTmdbId(), firstShow.getTitle()); - return null; - } - - if (isCancelled()) { - Timber.d("Finished. Cancelled."); - return null; - } - - // if not connected to Hexagon, get episodes from trakt - Map traktCollection = null; - Map traktWatched = null; - if (!HexagonSettings.isEnabled(context) && TraktCredentials.get(context).hasCredentials()) { - Timber.d("Getting watched and collected episodes from trakt."); - // get collection - Map traktShows = getTraktShows(true); - if (traktShows == null) { - return null; // can not get collected state from trakt, give up. - } - traktCollection = traktShows; - // get watched - traktShows = getTraktShows(false); - if (traktShows == null) { - return null; // can not get watched state from trakt, give up. - } - traktWatched = traktShows; - } - - ServicesComponent services = SgApp.getServicesComponent(context); - HexagonEpisodeSync hexagonEpisodeSync = new HexagonEpisodeSync(context, - services.hexagonTools()); - AddUpdateShowTools showTools = services.addUpdateShowTools(); - - int result; - boolean addedAtLeastOneShow = false; - boolean failedMergingShows = false; - while (!addQueue.isEmpty()) { - Timber.d("Starting to add next show..."); - if (isCancelled()) { - Timber.d("Finished. Cancelled."); - // only cancelled on config change, so don't rebuild fts - // table yet - return null; - } - - SearchResult nextShow = addQueue.removeFirst(); - // set values required for progress update - String currentShowName = nextShow.getTitle(); - int currentShowTmdbId = nextShow.getTmdbId(); - - if (currentShowTmdbId <= 0) { - // Invalid ID, should never have been passed, report. - // Background: Hexagon gets requests with ID 0. - IllegalStateException invalidIdException = new IllegalStateException( - "Show id invalid: " + currentShowTmdbId - + ", silentMode=" + isSilentMode - + ", merging=" + isMergingShows - ); - Errors.logAndReport("Add show", invalidIdException); - continue; - } - - if (!AndroidUtils.isNetworkConnected(context)) { - Timber.d("Finished. No connection."); - publishProgress(RESULT_OFFLINE, currentShowTmdbId, currentShowName); - failedMergingShows = true; - break; - } - - ShowResult addResult = showTools.addShow(nextShow.getTmdbId(), nextShow.getLanguage(), - traktCollection, traktWatched, hexagonEpisodeSync); - if (addResult == ShowResult.SUCCESS) { - result = PROGRESS_SUCCESS; - addedAtLeastOneShow = true; - } else if (addResult == ShowResult.IN_DATABASE) { - result = PROGRESS_EXISTS; - } else { - Timber.e("Adding show failed: %s", addResult); - - // Only fail a hexagon merge if show can not be added due to network error, - // not because it does not (longer) exist. - if (isMergingShows && addResult != ShowResult.DOES_NOT_EXIST) { - failedMergingShows = true; - } - - switch (addResult) { - case DOES_NOT_EXIST: - result = PROGRESS_ERROR_DOES_NOT_EXIST; - break; - case TMDB_ERROR: - result = PROGRESS_ERROR_TMDB; - break; - case HEXAGON_ERROR: - result = PROGRESS_ERROR_HEXAGON; - break; - case DATABASE_ERROR: - result = PROGRESS_ERROR_DATA; - break; - default: - result = PROGRESS_ERROR; - break; - } - } - publishProgress(result, currentShowTmdbId, currentShowName); - Timber.d("Finished adding show. (Result code: %s)", result); - } - - isFinishedAddingShows = true; - - // when merging shows down from Hexagon, set success flag - if (isMergingShows && !failedMergingShows) { - HexagonSettings.setHasMergedShows(context, true); - } - - if (addedAtLeastOneShow) { - // make sure the next sync will download all ratings - PreferenceManager.getDefaultSharedPreferences(context).edit() - .putLong(TraktSettings.KEY_LAST_SHOWS_RATED_AT, 0) - .putLong(TraktSettings.KEY_LAST_EPISODES_RATED_AT, 0) - .apply(); - - // renew FTS3 table - Timber.d("Renewing search table."); - SeriesGuideDatabase.rebuildFtsTable(context); - } - - Timber.d("Finished adding shows."); - return null; - } - - @Override - protected void onProgressUpdate(String... values) { - if (isSilentMode) { - Timber.d("SILENT MODE: do not show progress toast"); - return; - } - - // passing tvdb id and show name through values as fields may already have been - // overwritten on processing thread - - OnShowAddedEvent event = null; - // not catching format/null exceptions, if they happen we made a mistake passing values - int result = Integer.parseInt(values[0]); - int showTmdbId = Integer.parseInt(values[1]); - String showTitle = values[2]; - switch (result) { - case PROGRESS_SUCCESS: - // do nothing, user will see show added to show list - event = OnShowAddedEvent.successful(showTmdbId); - break; - case PROGRESS_EXISTS: - event = OnShowAddedEvent.exists(context, showTmdbId, showTitle); - break; - case PROGRESS_ERROR: - event = OnShowAddedEvent.failed(context, showTmdbId, showTitle); - break; - case PROGRESS_ERROR_TMDB: - event = OnShowAddedEvent.failedDetails(context, showTmdbId, showTitle, - context.getString(R.string.api_error_generic, - context.getString(R.string.tmdb))); - break; - case PROGRESS_ERROR_DOES_NOT_EXIST: - event = OnShowAddedEvent.failedDetails(context, showTmdbId, showTitle, - context.getString(R.string.tvdb_error_does_not_exist)); - break; - case PROGRESS_ERROR_HEXAGON: - event = OnShowAddedEvent.failedDetails(context, showTmdbId, showTitle, - context.getString(R.string.api_error_generic, - context.getString(R.string.hexagon))); - break; - case PROGRESS_ERROR_DATA: - event = OnShowAddedEvent.failedDetails(context, showTmdbId, showTitle, - context.getString(R.string.database_error)); - break; - case RESULT_OFFLINE: - event = OnShowAddedEvent.aborted(context.getString(R.string.offline)); - break; - case RESULT_TRAKT_API_ERROR: - event = OnShowAddedEvent.aborted(context.getString(R.string.api_error_generic, - context.getString(R.string.trakt))); - break; - case RESULT_TRAKT_AUTH_ERROR: - event = OnShowAddedEvent - .aborted(context.getString(R.string.trakt_error_credentials)); - break; - } - - if (event != null) { - EventBus.getDefault().post(event); - } - } - - @Override - protected void onPostExecute(Void aVoid) { - TaskManager.getInstance().releaseAddTaskRef(); - } - - private void publishProgress(int result) { - publishProgress(String.valueOf(result), "0", ""); - } - - private void publishProgress(int result, int showTmdbId, String showTitle) { - publishProgress(String.valueOf(result), String.valueOf(showTmdbId), showTitle); - } - - @Nullable - private Map getTraktShows(boolean isCollectionNotWatched) { - Pair, TraktTools2.ServiceResult> result = TraktTools2 - .getCollectedOrWatchedShows(isCollectionNotWatched, context); - if (result.getSecond() == TraktTools2.ServiceResult.AUTH_ERROR) { - publishProgress(RESULT_TRAKT_AUTH_ERROR); - } else if (result.getSecond() == TraktTools2.ServiceResult.API_ERROR) { - publishProgress(RESULT_TRAKT_API_ERROR); - } - return result.getFirst(); - } -} diff --git a/app/src/main/java/com/battlelancer/seriesguide/shows/tools/AddShowTask.kt b/app/src/main/java/com/battlelancer/seriesguide/shows/tools/AddShowTask.kt new file mode 100644 index 0000000000..236e80f13a --- /dev/null +++ b/app/src/main/java/com/battlelancer/seriesguide/shows/tools/AddShowTask.kt @@ -0,0 +1,369 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2011-2024 Uwe Trottmann + +package com.battlelancer.seriesguide.shows.tools + +import android.annotation.SuppressLint +import android.content.Context +import android.os.AsyncTask +import android.widget.Toast +import androidx.preference.PreferenceManager +import com.battlelancer.seriesguide.R +import com.battlelancer.seriesguide.SgApp +import com.battlelancer.seriesguide.backend.settings.HexagonSettings +import com.battlelancer.seriesguide.backend.settings.HexagonSettings.isEnabled +import com.battlelancer.seriesguide.provider.SeriesGuideDatabase +import com.battlelancer.seriesguide.shows.tools.AddUpdateShowTools.ShowResult +import com.battlelancer.seriesguide.sync.HexagonEpisodeSync +import com.battlelancer.seriesguide.traktapi.TraktCredentials.Companion.get +import com.battlelancer.seriesguide.traktapi.TraktSettings +import com.battlelancer.seriesguide.traktapi.TraktTools2 +import com.battlelancer.seriesguide.traktapi.TraktTools2.ServiceResult +import com.battlelancer.seriesguide.util.Errors +import com.battlelancer.seriesguide.util.TaskManager +import com.uwetrottmann.androidutils.AndroidUtils +import com.uwetrottmann.trakt5.entities.BaseShow +import org.greenrobot.eventbus.EventBus +import timber.log.Timber +import java.util.LinkedList + +/** + * Adds shows to the local database, tries to get watched and collected episodes if a trakt account + * is connected. + */ +class AddShowTask( + context: Context, + shows: List, + isSilentMode: Boolean, + isMergingShows: Boolean +) : AsyncTask() { + + /** + * [tmdbId] and [languageCode] are passed to [AddUpdateShowTools.addShow]. The [title] is only + * used for notifying the user, so it can be empty if the task is running in silent mode. + */ + data class Show( + val tmdbId: Int, + val languageCode: String, + val title: String + ) + + class OnShowAddedEvent private constructor( + /** + * Is -1 if add task was aborted. + */ + val showTmdbId: Int, + private val message: String?, + val successful: Boolean + ) { + fun handle(context: Context?) { + if (message != null) { + Toast.makeText(context, message, Toast.LENGTH_LONG).show() + } + } + + companion object { + fun successful(showTmdbId: Int): OnShowAddedEvent { + return OnShowAddedEvent(showTmdbId, null, true) + } + + fun exists(context: Context, showTmdbId: Int, showTitle: String?): OnShowAddedEvent { + return OnShowAddedEvent( + showTmdbId, + context.getString(R.string.add_already_exists, showTitle), + true + ) + } + + fun failed(context: Context, showTmdbId: Int, showTitle: String?): OnShowAddedEvent { + return OnShowAddedEvent( + showTmdbId, + context.getString(R.string.add_error, showTitle), + false + ) + } + + fun failedDetails( + context: Context, + showTmdbId: Int, + showTitle: String?, + details: String? + ): OnShowAddedEvent { + return OnShowAddedEvent( + showTmdbId, + String.format( + "%s %s", context.getString(R.string.add_error, showTitle), + details + ), + false + ) + } + + fun aborted(message: String): OnShowAddedEvent { + return OnShowAddedEvent(-1, message, false) + } + } + } + + @SuppressLint("StaticFieldLeak") + private val context: Context = context.applicationContext + private val addQueue = LinkedList() + + private var isFinishedAddingShows = false + private var isSilentMode: Boolean + private var isMergingShows: Boolean + + init { + addQueue.addAll(shows) + this.isSilentMode = isSilentMode + this.isMergingShows = isMergingShows + } + + /** + * Adds shows to the add queue. If this returns false, the shows were not added because the task + * is finishing up. Create a new one instead. + */ + fun addShows( + shows: List, + isSilentMode: Boolean, + isMergingShows: Boolean + ): Boolean { + if (isFinishedAddingShows) { + Timber.d("addShows: failed, already finishing up.") + return false + } else { + this.isSilentMode = isSilentMode + // never reset isMergingShows once true, so merged flag is correctly set on completion + this.isMergingShows = this.isMergingShows || isMergingShows + addQueue.addAll(shows) + Timber.d("addShows: added shows to queue.") + return true + } + } + + @Deprecated("Deprecated in Java") + override fun doInBackground(vararg params: Void?): Void? { + Timber.d("Starting to add shows...") + + val firstShow = addQueue.peek() + if (firstShow == null) { + Timber.d("Finished. Queue was empty.") + return null + } + + if (!AndroidUtils.isNetworkConnected(context)) { + Timber.d("Finished. No internet connection.") + publishProgress(RESULT_OFFLINE, firstShow.tmdbId, firstShow.title) + return null + } + + if (isCancelled) { + Timber.d("Finished. Cancelled.") + return null + } + + // if not connected to Hexagon, get episodes from trakt + var traktCollection: Map? = null + var traktWatched: Map? = null + if (!isEnabled(context) && get(context).hasCredentials()) { + Timber.d("Getting watched and collected episodes from trakt.") + // get collection + traktCollection = getTraktShows(true) + ?: return null // can not get collected state from trakt, give up. + // get watched + traktWatched = getTraktShows(false) + ?: return null // can not get watched state from trakt, give up. + } + + val services = SgApp.getServicesComponent(context) + val hexagonEpisodeSync = HexagonEpisodeSync(context, services.hexagonTools()) + val showTools = services.addUpdateShowTools() + + var result: Int + var addedAtLeastOneShow = false + var failedMergingShows = false + while (!addQueue.isEmpty()) { + Timber.d("Starting to add next show...") + if (isCancelled) { + Timber.d("Finished. Cancelled.") + // only cancelled on config change, so don't rebuild fts + // table yet + return null + } + + val nextShow = addQueue.removeFirst() + // set values required for progress update + val currentShowName = nextShow.title + val currentShowTmdbId = nextShow.tmdbId + + if (currentShowTmdbId <= 0) { + // Invalid ID, should never have been passed, report. + // Background: Hexagon gets requests with ID 0. + val invalidIdException = + IllegalStateException("Show id invalid: $currentShowTmdbId, silentMode=$isSilentMode, merging=$isMergingShows") + Errors.logAndReport("Add show", invalidIdException) + continue + } + + if (!AndroidUtils.isNetworkConnected(context)) { + Timber.d("Finished. No connection.") + publishProgress(RESULT_OFFLINE, currentShowTmdbId, currentShowName) + failedMergingShows = true + break + } + + val addResult = showTools.addShow( + nextShow.tmdbId, + nextShow.languageCode, + traktCollection, traktWatched, + hexagonEpisodeSync + ) + when (addResult) { + ShowResult.SUCCESS -> { + result = PROGRESS_SUCCESS + addedAtLeastOneShow = true + } + + ShowResult.IN_DATABASE -> { + result = PROGRESS_EXISTS + } + + else -> { + Timber.e("Adding show failed: %s", addResult) + + // Only fail a hexagon merge if show can not be added due to network error, + // not because it does not (longer) exist. + if (isMergingShows && addResult != ShowResult.DOES_NOT_EXIST) { + failedMergingShows = true + } + + result = when (addResult) { + ShowResult.DOES_NOT_EXIST -> PROGRESS_ERROR_DOES_NOT_EXIST + ShowResult.TMDB_ERROR -> PROGRESS_ERROR_TMDB + ShowResult.HEXAGON_ERROR -> PROGRESS_ERROR_HEXAGON + ShowResult.DATABASE_ERROR -> PROGRESS_ERROR_DATA + else -> PROGRESS_ERROR + } + } + } + publishProgress(result, currentShowTmdbId, currentShowName) + Timber.d("Finished adding show. (Result code: %s)", result) + } + + isFinishedAddingShows = true + + // when merging shows down from Hexagon, set success flag + if (isMergingShows && !failedMergingShows) { + HexagonSettings.setHasMergedShows(context, true) + } + + if (addedAtLeastOneShow) { + // make sure the next sync will download all ratings + PreferenceManager.getDefaultSharedPreferences(context).edit() + .putLong(TraktSettings.KEY_LAST_SHOWS_RATED_AT, 0) + .putLong(TraktSettings.KEY_LAST_EPISODES_RATED_AT, 0) + .apply() + + // renew FTS3 table + Timber.d("Renewing search table.") + SeriesGuideDatabase.rebuildFtsTable(context) + } + + Timber.d("Finished adding shows.") + return null + } + + @Deprecated("Deprecated in Java") + override fun onProgressUpdate(vararg values: String) { + if (isSilentMode) { + Timber.d("SILENT MODE: do not show progress toast") + return + } + + // Not catching format/null exceptions, should not occur if values correctly passed. + val result = values[0].toInt() + val showTmdbId = values[1].toInt() + val showTitle = values[2] + val event = when (result) { + PROGRESS_SUCCESS -> + // do nothing, user will see show added to show list + OnShowAddedEvent.successful(showTmdbId) + + PROGRESS_EXISTS -> OnShowAddedEvent.exists(context, showTmdbId, showTitle) + + PROGRESS_ERROR -> OnShowAddedEvent.failed(context, showTmdbId, showTitle) + + PROGRESS_ERROR_TMDB -> OnShowAddedEvent.failedDetails( + context, showTmdbId, showTitle, + context.getString(R.string.api_error_generic, context.getString(R.string.tmdb)) + ) + + PROGRESS_ERROR_DOES_NOT_EXIST -> OnShowAddedEvent.failedDetails( + context, showTmdbId, showTitle, + context.getString(R.string.tvdb_error_does_not_exist) + ) + + PROGRESS_ERROR_HEXAGON -> OnShowAddedEvent.failedDetails( + context, showTmdbId, showTitle, + context.getString(R.string.api_error_generic, context.getString(R.string.hexagon)) + ) + + PROGRESS_ERROR_DATA -> OnShowAddedEvent.failedDetails( + context, showTmdbId, showTitle, context.getString(R.string.database_error) + ) + + RESULT_OFFLINE -> OnShowAddedEvent.aborted(context.getString(R.string.offline)) + + RESULT_TRAKT_API_ERROR -> OnShowAddedEvent.aborted( + context.getString(R.string.api_error_generic, context.getString(R.string.trakt)) + ) + + RESULT_TRAKT_AUTH_ERROR -> OnShowAddedEvent.aborted( + context.getString(R.string.trakt_error_credentials) + ) + + else -> null + } + + if (event != null) { + EventBus.getDefault().post(event) + } + } + + @Deprecated("Deprecated in Java") + override fun onPostExecute(aVoid: Void?) { + TaskManager.releaseAddTaskRef() + } + + private fun publishProgress(result: Int) { + publishProgress(result.toString(), "0", "") + } + + private fun publishProgress(result: Int, showTmdbId: Int, showTitle: String) { + publishProgress(result.toString(), showTmdbId.toString(), showTitle) + } + + private fun getTraktShows(isCollectionNotWatched: Boolean): Map? { + val result: Pair?, ServiceResult> = + TraktTools2.getCollectedOrWatchedShows(isCollectionNotWatched, context) + if (result.second == ServiceResult.AUTH_ERROR) { + publishProgress(RESULT_TRAKT_AUTH_ERROR) + } else if (result.second == ServiceResult.API_ERROR) { + publishProgress(RESULT_TRAKT_API_ERROR) + } + return result.first + } + + companion object { + private const val PROGRESS_EXISTS = 0 + private const val PROGRESS_SUCCESS = 1 + private const val PROGRESS_ERROR = 2 + private const val PROGRESS_ERROR_TMDB = 3 + private const val PROGRESS_ERROR_DOES_NOT_EXIST = 4 + private const val PROGRESS_ERROR_HEXAGON = 6 + private const val PROGRESS_ERROR_DATA = 7 + private const val RESULT_OFFLINE = 8 + private const val RESULT_TRAKT_API_ERROR = 9 + private const val RESULT_TRAKT_AUTH_ERROR = 10 + } +} diff --git a/app/src/main/java/com/battlelancer/seriesguide/shows/tools/AddUpdateShowTools.kt b/app/src/main/java/com/battlelancer/seriesguide/shows/tools/AddUpdateShowTools.kt index bfa320381d..048a9d6364 100644 --- a/app/src/main/java/com/battlelancer/seriesguide/shows/tools/AddUpdateShowTools.kt +++ b/app/src/main/java/com/battlelancer/seriesguide/shows/tools/AddUpdateShowTools.kt @@ -79,7 +79,7 @@ class AddUpdateShowTools @Inject constructor( fun addShow( showTmdbId: Int, - desiredLanguage: String?, + languageCode: String, traktCollection: Map?, traktWatched: Map?, hexagonEpisodeSync: HexagonEpisodeSync @@ -89,9 +89,7 @@ class AddUpdateShowTools @Inject constructor( return ShowResult.IN_DATABASE } - val language = desiredLanguage ?: LanguageTools.LANGUAGE_EN - - val showDetails = getShowTools.getShowDetails(showTmdbId, language) + val showDetails = getShowTools.getShowDetails(showTmdbId, languageCode) .getOrElse { return it.toShowResult() } val show = showDetails.show!! @@ -160,7 +158,7 @@ class AddUpdateShowTools @Inject constructor( showId, season.number, seasonId, - language, + languageCode, null, null ).getOrElse { return@runInTransaction ShowResult.TMDB_ERROR } @@ -185,7 +183,7 @@ class AddUpdateShowTools @Inject constructor( // updates the language, so the show will be auto-added on other connected devices. val cloudShow = SgCloudShow() cloudShow.tmdbId = showTmdbId - cloudShow.language = language + cloudShow.language = languageCode cloudShow.isRemoved = false // Prevent losing restored properties from a legacy Cloud show (see // hexagonTools.get().getShow used above) by always sending them. diff --git a/app/src/main/java/com/battlelancer/seriesguide/shows/tools/LatestEpisodeUpdateTask.java b/app/src/main/java/com/battlelancer/seriesguide/shows/tools/LatestEpisodeUpdateTask.java index e14980aeff..83062deb8b 100644 --- a/app/src/main/java/com/battlelancer/seriesguide/shows/tools/LatestEpisodeUpdateTask.java +++ b/app/src/main/java/com/battlelancer/seriesguide/shows/tools/LatestEpisodeUpdateTask.java @@ -1,5 +1,5 @@ -// Copyright 2023 Uwe Trottmann // SPDX-License-Identifier: Apache-2.0 +// Copyright 2014-2024 Uwe Trottmann package com.battlelancer.seriesguide.shows.tools; @@ -32,7 +32,7 @@ protected Void doInBackground(Integer... params) { @Override protected void onPostExecute(Void aVoid) { - TaskManager.getInstance().releaseNextEpisodeUpdateTaskRef(); + TaskManager.releaseNextEpisodeUpdateTaskRef(); } public static void updateLatestEpisodeFor(Context context, Long showId) { diff --git a/app/src/main/java/com/battlelancer/seriesguide/sync/HexagonShowSync.kt b/app/src/main/java/com/battlelancer/seriesguide/sync/HexagonShowSync.kt index f23b62cc12..fcf97891cd 100644 --- a/app/src/main/java/com/battlelancer/seriesguide/sync/HexagonShowSync.kt +++ b/app/src/main/java/com/battlelancer/seriesguide/sync/HexagonShowSync.kt @@ -1,5 +1,5 @@ // SPDX-License-Identifier: Apache-2.0 -// Copyright 2017-2023 Uwe Trottmann +// Copyright 2017-2024 Uwe Trottmann package com.battlelancer.seriesguide.sync @@ -9,8 +9,9 @@ import com.battlelancer.seriesguide.backend.HexagonTools import com.battlelancer.seriesguide.backend.settings.HexagonSettings import com.battlelancer.seriesguide.modules.ApplicationContext import com.battlelancer.seriesguide.provider.SgRoomDatabase +import com.battlelancer.seriesguide.shows.ShowsSettings import com.battlelancer.seriesguide.shows.database.SgShow2CloudUpdate -import com.battlelancer.seriesguide.shows.search.discover.SearchResult +import com.battlelancer.seriesguide.shows.tools.AddShowTask import com.battlelancer.seriesguide.tmdbapi.TmdbTools2 import com.battlelancer.seriesguide.util.Errors.Companion.logAndReportHexagon import com.battlelancer.seriesguide.util.LanguageTools @@ -42,7 +43,7 @@ class HexagonShowSync @Inject constructor( */ fun download( tmdbIdsToShowIds: Map, - toAdd: HashMap, + toAdd: HashMap, hasMergedShows: Boolean ): Boolean { val updates: MutableList = ArrayList() @@ -86,7 +87,7 @@ class HexagonShowSync @Inject constructor( updates: MutableList, toUpdate: MutableSet, removed: MutableSet, - toAdd: HashMap, + toAdd: HashMap, tmdbIdsToShowIds: Map, hasMergedShows: Boolean, lastSyncTime: DateTime @@ -158,7 +159,7 @@ class HexagonShowSync @Inject constructor( updates: MutableList, toUpdate: MutableSet, removed: MutableSet, - toAdd: HashMap, + toAdd: HashMap, tmdbIdsToShowIds: Map ): Boolean { var cursor: String? = null @@ -255,11 +256,12 @@ class HexagonShowSync @Inject constructor( updates: MutableList, toUpdate: MutableSet, removed: MutableSet, - toAdd: MutableMap, + toAdd: MutableMap, shows: List, tmdbIdsToShowIds: Map, mergeValues: Boolean ) { + val defaultLanguageCode = ShowsSettings.getShowsSearchLanguage(context) for (show in shows) { // schedule to add shows not in local database val showTmdbId = show.tmdbId ?: continue // Invalid data. @@ -275,10 +277,12 @@ class HexagonShowSync @Inject constructor( continue } if (!toAdd.containsKey(showTmdbId)) { - val item = SearchResult() - item.tmdbId = showTmdbId - item.language = show.language?.let { LanguageTools.mapLegacyShowCode(it) } - item.title = "" + val item = AddShowTask.Show( + tmdbId = showTmdbId, + languageCode = show.language?.let { LanguageTools.mapLegacyShowCode(it) } + ?: defaultLanguageCode, + title = "" + ) toAdd[showTmdbId] = item } } else if (!toUpdate.contains(showIdOrNull)) { diff --git a/app/src/main/java/com/battlelancer/seriesguide/sync/HexagonSync.java b/app/src/main/java/com/battlelancer/seriesguide/sync/HexagonSync.java index cf7a89404e..bc9f4880ee 100644 --- a/app/src/main/java/com/battlelancer/seriesguide/sync/HexagonSync.java +++ b/app/src/main/java/com/battlelancer/seriesguide/sync/HexagonSync.java @@ -1,5 +1,5 @@ -// Copyright 2023 Uwe Trottmann // SPDX-License-Identifier: Apache-2.0 +// Copyright 2017-2024 Uwe Trottmann package com.battlelancer.seriesguide.sync; @@ -9,11 +9,11 @@ import com.battlelancer.seriesguide.SgApp; import com.battlelancer.seriesguide.backend.HexagonTools; import com.battlelancer.seriesguide.backend.settings.HexagonSettings; +import com.battlelancer.seriesguide.movies.tools.MovieTools; import com.battlelancer.seriesguide.provider.SgRoomDatabase; import com.battlelancer.seriesguide.shows.database.SgShow2Helper; import com.battlelancer.seriesguide.shows.database.SgShow2Ids; -import com.battlelancer.seriesguide.movies.tools.MovieTools; -import com.battlelancer.seriesguide.shows.search.discover.SearchResult; +import com.battlelancer.seriesguide.shows.tools.AddShowTask; import com.battlelancer.seriesguide.util.TaskManager; import com.uwetrottmann.androidutils.AndroidUtils; import java.util.HashMap; @@ -138,7 +138,7 @@ private HexagonResult syncShows(Map tmdbIdsToShowIds) { // download shows and apply property changes (if merging only overwrite some properties) HexagonShowSync showSync = new HexagonShowSync(context, hexagonTools); - HashMap newShows = new HashMap<>(); + HashMap newShows = new HashMap<>(); boolean downloadSuccessful = showSync.download(tmdbIdsToShowIds, newShows, hasMergedShows); if (!downloadSuccessful) { return new HexagonResult(false, false); @@ -155,8 +155,8 @@ private HexagonResult syncShows(Map tmdbIdsToShowIds) { // add new shows boolean addNewShows = !newShows.isEmpty(); if (addNewShows) { - List newShowsList = new LinkedList<>(newShows.values()); - TaskManager.getInstance().performAddTask(context, newShowsList, true, !hasMergedShows); + List newShowsList = new LinkedList<>(newShows.values()); + TaskManager.performAddTask(context, newShowsList, true, !hasMergedShows); } else if (!hasMergedShows) { // set shows as merged HexagonSettings.setHasMergedShows(context, true); diff --git a/app/src/main/java/com/battlelancer/seriesguide/sync/SgSyncAdapter.kt b/app/src/main/java/com/battlelancer/seriesguide/sync/SgSyncAdapter.kt index 683c1930e1..c5641008ac 100644 --- a/app/src/main/java/com/battlelancer/seriesguide/sync/SgSyncAdapter.kt +++ b/app/src/main/java/com/battlelancer/seriesguide/sync/SgSyncAdapter.kt @@ -204,7 +204,7 @@ class SgSyncAdapter(context: Context) : AbstractThreadedSyncAdapter(context, tru if (Thread.interrupted()) throw InterruptedException() // update next episodes for all shows - TaskManager.getInstance().tryNextEpisodeUpdateTask(context) + TaskManager.tryNextEpisodeUpdateTask(context) updateTimeAndFailedCounter(prefs, resultCode) } diff --git a/app/src/main/java/com/battlelancer/seriesguide/ui/BaseActivity.kt b/app/src/main/java/com/battlelancer/seriesguide/ui/BaseActivity.kt index f5bbbd7e77..705db1e827 100644 --- a/app/src/main/java/com/battlelancer/seriesguide/ui/BaseActivity.kt +++ b/app/src/main/java/com/battlelancer/seriesguide/ui/BaseActivity.kt @@ -1,5 +1,5 @@ -// Copyright 2023 Uwe Trottmann // SPDX-License-Identifier: Apache-2.0 +// Copyright 2011-2024 Uwe Trottmann package com.battlelancer.seriesguide.ui @@ -93,7 +93,7 @@ abstract class BaseActivity : BaseThemeActivity() { if (!BackupSettings.isTimeForAutoBackup(this)) { return false } - TaskManager.getInstance().tryBackupTask(this) + TaskManager.tryBackupTask(this) return true } diff --git a/app/src/main/java/com/battlelancer/seriesguide/ui/BaseSimilarActivity.kt b/app/src/main/java/com/battlelancer/seriesguide/ui/BaseSimilarActivity.kt index 901c7767d6..2f01c98bb3 100644 --- a/app/src/main/java/com/battlelancer/seriesguide/ui/BaseSimilarActivity.kt +++ b/app/src/main/java/com/battlelancer/seriesguide/ui/BaseSimilarActivity.kt @@ -18,7 +18,7 @@ abstract class BaseSimilarActivity : BaseMessageActivity() { abstract val liftOnScrollTargetViewId: Int abstract val titleStringRes: Int - abstract fun createFragment(tmdbId: Int, title: String?): Fragment + abstract fun createFragment(tmdbId: Int, title: String): Fragment private lateinit var binding: ActivitySinglepaneBinding override fun onCreate(savedInstanceState: Bundle?) { @@ -28,14 +28,13 @@ abstract class BaseSimilarActivity : BaseMessageActivity() { liftOnScrollTargetViewId setupActionBar() - val tmdbId = intent.getIntExtra(EXTRA_TMDB_ID, 0) - if (tmdbId <= 0) { - finish() - return - } - - val title = intent.getStringExtra(EXTRA_TITLE) if (savedInstanceState == null) { + val tmdbId = intent.getIntExtra(EXTRA_TMDB_ID, 0) + val title = intent.getStringExtra(EXTRA_TITLE) + if (tmdbId <= 0 || title == null) { + finish() + return + } addFragment(tmdbId, title) } } @@ -50,7 +49,7 @@ abstract class BaseSimilarActivity : BaseMessageActivity() { fun addFragment( tmdbId: Int, - title: String?, + title: String, addToBackStack: Boolean = false ) { val fragment = createFragment(tmdbId, title) @@ -71,7 +70,7 @@ abstract class BaseSimilarActivity : BaseMessageActivity() { private const val EXTRA_TMDB_ID = "EXTRA_TMDB_ID" private const val EXTRA_TITLE = "EXTRA_TITLE" - fun Intent.putExtras(showTmdbId: Int, title: String?): Intent { + fun Intent.putExtras(showTmdbId: Int, title: String): Intent { return this .putExtra(EXTRA_TMDB_ID, showTmdbId) .putExtra(EXTRA_TITLE, title) diff --git a/app/src/main/java/com/battlelancer/seriesguide/util/TaskManager.java b/app/src/main/java/com/battlelancer/seriesguide/util/TaskManager.java deleted file mode 100644 index c2d1a5dc62..0000000000 --- a/app/src/main/java/com/battlelancer/seriesguide/util/TaskManager.java +++ /dev/null @@ -1,122 +0,0 @@ -// Copyright 2023 Uwe Trottmann -// SPDX-License-Identifier: Apache-2.0 - -package com.battlelancer.seriesguide.util; - -import android.content.Context; -import android.os.AsyncTask; -import android.widget.Toast; -import androidx.annotation.MainThread; -import androidx.annotation.Nullable; -import com.battlelancer.seriesguide.R; -import com.battlelancer.seriesguide.dataliberation.JsonExportTask; -import com.battlelancer.seriesguide.shows.tools.AddShowTask; -import com.battlelancer.seriesguide.shows.search.discover.SearchResult; -import com.battlelancer.seriesguide.shows.tools.LatestEpisodeUpdateTask; -import java.util.ArrayList; -import java.util.List; -import kotlinx.coroutines.Job; - -/** - * Hold some {@link AsyncTask} instances while running to ensure only one is executing at a time. - */ -public class TaskManager { - - private static TaskManager _instance; - - @Nullable private AddShowTask addShowTask; - @Nullable private Job backupTask; - @Nullable private LatestEpisodeUpdateTask nextEpisodeUpdateTask; - - private TaskManager() { - } - - public static synchronized TaskManager getInstance() { - if (_instance == null) { - _instance = new TaskManager(); - } - return _instance; - } - - @MainThread - public synchronized void performAddTask(Context context, SearchResult show) { - List wrapper = new ArrayList<>(); - wrapper.add(show); - performAddTask(context, wrapper, false, false); - } - - /** - * Schedule shows to be added to the database. - * - * @param isSilentMode Whether to display status toasts if a show could not be added. - * @param isMergingShows Whether to set the Hexagon show merged flag to true if all shows were - */ - @MainThread - public synchronized void performAddTask(final Context context, final List shows, - final boolean isSilentMode, final boolean isMergingShows) { - if (!isSilentMode) { - // notify user here already - if (shows.size() == 1) { - // say title of show - SearchResult show = shows.get(0); - Toast.makeText(context, context.getString(R.string.add_started, show.getTitle()), - Toast.LENGTH_SHORT).show(); - } else { - // generic adding multiple message - Toast.makeText(context, R.string.add_multiple, Toast.LENGTH_SHORT).show(); - } - } - - // add the show(s) to a running add task or create a new one - //noinspection ConstantConditions: null check in isAddTaskRunning - if (!isAddTaskRunning() || !addShowTask.addShows(shows, isSilentMode, isMergingShows)) { - addShowTask = new AddShowTask(context, shows, isSilentMode, isMergingShows); - addShowTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); - } - } - - public synchronized void releaseAddTaskRef() { - addShowTask = null; // clear reference to avoid holding on to task context - } - - public boolean isAddTaskRunning() { - return !(addShowTask == null || addShowTask.getStatus() == AsyncTask.Status.FINISHED); - } - - /** - * If no {@link AddShowTask} or {@link JsonExportTask} created by this {@link - * com.battlelancer.seriesguide.util.TaskManager} is running a - * {@link JsonExportTask} is scheduled in silent mode. - */ - @MainThread - public synchronized boolean tryBackupTask(Context context) { - if (!isAddTaskRunning() - && (backupTask == null || backupTask.isCompleted())) { - JsonExportTask exportTask = new JsonExportTask(context, null, false, true, null); - backupTask = exportTask.launch(); - return true; - } - return false; - } - - public synchronized void releaseBackupTaskRef() { - backupTask = null; // clear reference to avoid holding on to task context - } - - /** - * Schedules a {@link LatestEpisodeUpdateTask} for all shows - * if no other one of this type is currently running. - */ - @MainThread - public synchronized void tryNextEpisodeUpdateTask(Context context) { - if (nextEpisodeUpdateTask == null - || nextEpisodeUpdateTask.getStatus() == AsyncTask.Status.FINISHED) { - nextEpisodeUpdateTask = new LatestEpisodeUpdateTask(context); - nextEpisodeUpdateTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); - } - } - - public synchronized void releaseNextEpisodeUpdateTaskRef() { - nextEpisodeUpdateTask = null; // clear reference to avoid holding on to task context - } -} diff --git a/app/src/main/java/com/battlelancer/seriesguide/util/TaskManager.kt b/app/src/main/java/com/battlelancer/seriesguide/util/TaskManager.kt new file mode 100644 index 0000000000..2d1c95ce47 --- /dev/null +++ b/app/src/main/java/com/battlelancer/seriesguide/util/TaskManager.kt @@ -0,0 +1,123 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2011-2024 Uwe Trottmann + +package com.battlelancer.seriesguide.util + +import android.annotation.SuppressLint +import android.content.Context +import android.os.AsyncTask +import android.widget.Toast +import androidx.annotation.MainThread +import com.battlelancer.seriesguide.R +import com.battlelancer.seriesguide.dataliberation.JsonExportTask +import com.battlelancer.seriesguide.shows.tools.AddShowTask +import com.battlelancer.seriesguide.shows.tools.LatestEpisodeUpdateTask +import kotlinx.coroutines.Job + +/** + * Holds on to task instances while they are running to ensure only one is executing at a time. + */ +object TaskManager { + + @SuppressLint("StaticFieldLeak") // AddShowTask holds an application context + private var addShowTask: AddShowTask? = null + private var backupTask: Job? = null + private var nextEpisodeUpdateTask: LatestEpisodeUpdateTask? = null + + @MainThread + @Synchronized + fun performAddTask(context: Context, show: AddShowTask.Show) { + performAddTask(context, listOf(show), isSilentMode = false, isMergingShows = false) + } + + /** + * Schedule shows to be added to the database. + * + * @param isSilentMode Whether to display status toasts if a show could not be added. + * @param isMergingShows Whether to set the Hexagon show merged flag to true if all shows were + */ + @JvmStatic + @MainThread + @Synchronized + fun performAddTask( + context: Context, + shows: List, + isSilentMode: Boolean, + isMergingShows: Boolean + ) { + if (!isSilentMode) { + // notify user here already + if (shows.size == 1) { + // say title of show + val show = shows[0] + Toast.makeText( + context, context.getString(R.string.add_started, show.title), + Toast.LENGTH_SHORT + ).show() + } else { + // generic adding multiple message + Toast.makeText(context, R.string.add_multiple, Toast.LENGTH_SHORT).show() + } + } + + // add the show(s) to a running add task or create a new one + if (!isAddTaskRunning || !addShowTask!!.addShows(shows, isSilentMode, isMergingShows)) { + AddShowTask(context, shows, isSilentMode, isMergingShows) + .also { this.addShowTask = it } + .executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR) + } + } + + @Synchronized + fun releaseAddTaskRef() { + addShowTask = null // clear reference to avoid holding on to task context + } + + val isAddTaskRunning: Boolean + get() = !(addShowTask == null || addShowTask!!.status == AsyncTask.Status.FINISHED) + + /** + * If no [AddShowTask] or [JsonExportTask] created by this [TaskManager] is running a + * [JsonExportTask] is scheduled in silent mode. + */ + @MainThread + @Synchronized + fun tryBackupTask(context: Context): Boolean { + val backupTask = backupTask + if (!isAddTaskRunning + && (backupTask == null || backupTask.isCompleted)) { + val exportTask = JsonExportTask(context, null, false, true, null) + this.backupTask = exportTask.launch() + return true + } + return false + } + + @Synchronized + fun releaseBackupTaskRef() { + backupTask = null // clear reference to avoid holding on to task context + } + + /** + * Schedules a [LatestEpisodeUpdateTask] for all shows + * if no other one of this type is currently running. + */ + @MainThread + @Synchronized + fun tryNextEpisodeUpdateTask(context: Context) { + val nextEpisodeUpdateTask = nextEpisodeUpdateTask + if (nextEpisodeUpdateTask == null + || nextEpisodeUpdateTask.status == AsyncTask.Status.FINISHED) { + LatestEpisodeUpdateTask(context) + .also { this.nextEpisodeUpdateTask = it } + .executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR) + } + } + + @JvmStatic + @Synchronized + fun releaseNextEpisodeUpdateTaskRef() { + nextEpisodeUpdateTask = null // clear reference to avoid holding on to task context + } + +}