Skip to content

Commit

Permalink
AddShowTask: use coroutines, enforce queue with semaphore
Browse files Browse the repository at this point in the history
Also use the semaphore for import task.
  • Loading branch information
UweTrottmann committed Oct 17, 2024
1 parent 7af9144 commit ce65dcc
Show file tree
Hide file tree
Showing 4 changed files with 72 additions and 104 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ import com.battlelancer.seriesguide.shows.database.SgSeason2Helper
import com.battlelancer.seriesguide.shows.database.SgShow2Helper
import com.battlelancer.seriesguide.shows.episodes.EpisodeTools
import com.battlelancer.seriesguide.util.Errors
import com.battlelancer.seriesguide.util.TaskManager
import com.google.gson.Gson
import com.google.gson.JsonParseException
import com.google.gson.stream.JsonWriter
Expand All @@ -44,6 +43,7 @@ import java.io.IOException
import java.io.OutputStream
import java.io.OutputStreamWriter
import java.nio.charset.StandardCharsets
import kotlin.collections.List
import com.battlelancer.seriesguide.dataliberation.model.List as ExportList

/**
Expand Down Expand Up @@ -174,8 +174,6 @@ class JsonExportTask(
}

private fun onPostExecute(result: Int) {
TaskManager.releaseBackupTaskRef()

if (!isAutoBackupMode) {
val messageId: Int
val showIndefinite: Boolean
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,11 @@ class JsonImportTask(

private fun doInBackground(coroutineScope: CoroutineScope): Int {
// Ensure no large database ops are running
if (SgSyncAdapter.isSyncActive(context, false) || TaskManager.isAddTaskRunning) {
if (SgSyncAdapter.isSyncActive(context, false)) {
return ERROR_LARGE_DB_OP
}
// Do not import if an add or backup task is running
if (!TaskManager.addShowOrBackupSemaphore.tryAcquire()) {
return ERROR_LARGE_DB_OP
}

Expand Down Expand Up @@ -163,6 +167,9 @@ class JsonImportTask(
// Renew search table
SeriesGuideDatabase.rebuildFtsTable(context)

// allow other tasks to resume
TaskManager.addShowOrBackupSemaphore.release()

return SUCCESS
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,7 @@

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
Expand All @@ -20,23 +18,22 @@ 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
* 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<Show>,
isSilentMode: Boolean,
isMergingShows: Boolean
) : AsyncTask<Void?, String, Void?>() {
private val isSilentMode: Boolean,
private val isMergingShows: Boolean
) {

/**
* [tmdbId] and [languageCode] are passed to [AddUpdateShowTools.addShow]. The [title] is only
Expand Down Expand Up @@ -105,61 +102,30 @@ class AddShowTask(
}
}

@SuppressLint("StaticFieldLeak")
private val context: Context = context.applicationContext
private val addQueue = LinkedList<Show>()

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<Show>,
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
}
fun run() {
doInBackground()
}

@Deprecated("Deprecated in Java")
override fun doInBackground(vararg params: Void?): Void? {
private fun doInBackground() {
Timber.d("Starting to add shows...")

val firstShow = addQueue.peek()
if (firstShow == null) {
Timber.d("Finished. Queue was empty.")
return null
return
}

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
return
}

// if not connected to Hexagon, get episodes from trakt
Expand All @@ -169,10 +135,10 @@ class AddShowTask(
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.
?: return // 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.
?: return // can not get watched state from trakt, give up.
}

val services = SgApp.getServicesComponent(context)
Expand All @@ -184,13 +150,6 @@ class AddShowTask(
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
Expand Down Expand Up @@ -250,8 +209,6 @@ class AddShowTask(
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)
Expand All @@ -270,20 +227,19 @@ class AddShowTask(
}

Timber.d("Finished adding shows.")
return null
}

@Deprecated("Deprecated in Java")
override fun onProgressUpdate(vararg values: String) {
private fun publishProgress(
result: Int,
showTmdbId: Int,
showTitle: 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
Expand All @@ -305,7 +261,10 @@ class AddShowTask(

PROGRESS_ERROR_HEXAGON -> OnShowAddedEvent.failedDetails(
context, showTmdbId, showTitle,
context.getString(R.string.api_error_generic, context.getString(R.string.hexagon))
context.getString(
R.string.api_error_generic,
context.getString(R.string.hexagon)
)
)

PROGRESS_ERROR_DATA -> OnShowAddedEvent.failedDetails(
Expand All @@ -326,21 +285,15 @@ class AddShowTask(
}

if (event != null) {
// TODO Is this necessary?
// withContext(Dispatchers.Main) {
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)
publishProgress(result, 0, "")
}

private fun getTraktShows(isCollectionNotWatched: Boolean): Map<Int, BaseShow>? {
Expand Down
68 changes: 39 additions & 29 deletions app/src/main/java/com/battlelancer/seriesguide/util/TaskManager.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,25 +3,34 @@

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.SgApp
import com.battlelancer.seriesguide.dataliberation.JsonExportTask
import com.battlelancer.seriesguide.shows.tools.AddShowTask
import com.battlelancer.seriesguide.shows.tools.LatestEpisodeUpdateTask
import kotlinx.coroutines.Job
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Semaphore
import kotlinx.coroutines.sync.withPermit

/**
* 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
/**
* Ensures that only one task that
* - adds shows,
* - runs a backup
* - runs an import
* runs at a time.
*/
val addShowOrBackupSemaphore = Semaphore(1)
private var hasBackupTask: Boolean = false
private var nextEpisodeUpdateTask: LatestEpisodeUpdateTask? = null

@MainThread
Expand Down Expand Up @@ -60,42 +69,43 @@ object TaskManager {
}
}

// 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)
// Queue another add task
SgApp.coroutineScope.launch(Dispatchers.IO) {
addShowOrBackupSemaphore.withPermit {
AddShowTask(context, shows, isSilentMode, isMergingShows).run()
}
}
}

@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
if (hasBackupTask) {
return false
}
return false
}
hasBackupTask = true

@Synchronized
fun releaseBackupTaskRef() {
backupTask = null // clear reference to avoid holding on to task context
// Queue backup task
SgApp.coroutineScope.launch(Dispatchers.IO) {
addShowOrBackupSemaphore.withPermit {
try {
JsonExportTask(
context, null,
isFullDump = false,
isAutoBackupMode = true,
type = null
).run()
} finally {
// If backup task gets cancelled for any reason, ensure flag is reset
hasBackupTask = false
}
}
}
return true
}

/**
Expand Down

0 comments on commit ce65dcc

Please sign in to comment.