Reorganize SpiderQueen code

This commit is contained in:
tarsin committed 2023-02-17 14:21:24 +08:00
1 parent 64e26a6b8b
commit 72a6419aff
2 files changed
+336 -353

No files matched your search

@@ -1,5 +1,6 @@
/*
* Copyright 2016 Hippo Seven
* Rewrite with Kotlin coroutines, Tarsin Norbin 2023
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -15,13 +16,17 @@
*/
package com.hippo.ehviewer.spider
import android.util.Log
import androidx.annotation.IntDef
import androidx.collection.LongSparseArray
import androidx.collection.set
import com.hippo.ehviewer.EhApplication.Companion.okHttpClient
import com.hippo.ehviewer.GetText
import com.hippo.ehviewer.R
import com.hippo.ehviewer.Settings
import com.hippo.ehviewer.client.EhEngine
import com.hippo.ehviewer.client.EhRequestBuilder
import com.hippo.ehviewer.client.EhUrl
import com.hippo.ehviewer.client.EhUrl.getGalleryDetailUrl
import com.hippo.ehviewer.client.EhUrl.getGalleryMultiPageViewerUrl
import com.hippo.ehviewer.client.EhUrl.referer
@@ -35,12 +40,21 @@ import com.hippo.ehviewer.client.parser.GalleryPageUrlParser
import com.hippo.image.Image
import com.hippo.unifile.UniFile
import com.hippo.util.ExceptionUtils
import com.hippo.yorozuya.StringUtils
import eu.kanade.tachiyomi.util.lang.launchIO
import eu.kanade.tachiyomi.util.lang.launchNonCancellable
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.cancel
import kotlinx.coroutines.currentCoroutineContext
import kotlinx.coroutines.delay
import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.Semaphore
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.sync.withPermit
import moe.tarsin.coroutines.runSuspendCatching
import okhttp3.executeAsync
import java.util.concurrent.ConcurrentHashMap
@@ -48,8 +62,11 @@ import java.util.concurrent.atomic.AtomicInteger
import kotlin.coroutines.CoroutineContext
class SpiderQueen private constructor(val galleryInfo: GalleryInfo) : CoroutineScope {
val mSpiderDen: SpiderDen = SpiderDen(galleryInfo)
@Volatile
lateinit var mPageStateArray: IntArray
lateinit var mSpiderInfo: SpiderInfo
val mSpiderDen: SpiderDen = SpiderDen(galleryInfo)
val mPagePercentMap = ConcurrentHashMap<Int, Float>()
private val mPageStateLock = Any()
private val mDownloadedPages = AtomicInteger(0)
@@ -57,12 +74,10 @@ class SpiderQueen private constructor(val galleryInfo: GalleryInfo) : CoroutineS
private val mPageErrorMap = ConcurrentHashMap<Int, String>()
private val mSpiderListeners: MutableList<OnSpiderListener> = ArrayList()
@Volatile
lateinit var mPageStateArray: IntArray
private var mReadReference = 0
private var mDownloadReference = 0
private val mWorkerScope = SpiderQueenWorker(this)
private val mWorkerScope = SpiderQueenWorker()
fun addOnSpiderListener(listener: OnSpiderListener) {
synchronized(mSpiderListeners) { mSpiderListeners.add(listener) }
@@ -74,32 +89,33 @@ class SpiderQueen private constructor(val galleryInfo: GalleryInfo) : CoroutineS
private fun notifyGetPages(pages: Int) {
synchronized(mSpiderListeners) {
for (listener in mSpiderListeners) {
listener.onGetPages(pages)
}
mSpiderListeners.forEach { it.onGetPages(pages) }
}
}
fun notifyGet509(index: Int) {
synchronized(mSpiderListeners) {
for (listener in mSpiderListeners) {
listener.onGet509(index)
}
mSpiderListeners.forEach { it.onGet509(index) }
}
}
fun notifyPageDownload(index: Int, contentLength: Long, receivedSize: Long, bytesRead: Int) {
synchronized(mSpiderListeners) {
for (listener in mSpiderListeners) {
listener.onPageDownload(index, contentLength, receivedSize, bytesRead)
mSpiderListeners.forEach {
it.onPageDownload(
index,
contentLength,
receivedSize,
bytesRead
)
}
}
}
private fun notifyPageSuccess(index: Int) {
synchronized(mSpiderListeners) {
for (listener in mSpiderListeners) {
listener.onPageSuccess(
mSpiderListeners.forEach {
it.onPageSuccess(
index,
mFinishedPages.get(),
mDownloadedPages.get(),
@@ -111,8 +127,8 @@ class SpiderQueen private constructor(val galleryInfo: GalleryInfo) : CoroutineS
private fun notifyPageFailure(index: Int, error: String?) {
synchronized(mSpiderListeners) {
for (listener in mSpiderListeners) {
listener.onPageFailure(
mSpiderListeners.forEach {
it.onPageFailure(
index,
error,
mFinishedPages.get(),
@@ -125,8 +141,8 @@ class SpiderQueen private constructor(val galleryInfo: GalleryInfo) : CoroutineS
private fun notifyAllPageDownloaded() {
synchronized(mSpiderListeners) {
for (listener in mSpiderListeners) {
listener.onFinish(
mSpiderListeners.forEach {
it.onFinish(
mFinishedPages.get(),
mDownloadedPages.get(),
mPageStateArray.size
@@ -137,21 +153,21 @@ class SpiderQueen private constructor(val galleryInfo: GalleryInfo) : CoroutineS
fun notifyGetImageSuccess(index: Int, image: Image) {
synchronized(mSpiderListeners) {
for (listener in mSpiderListeners) {
listener.onGetImageSuccess(index, image)
mSpiderListeners.forEach {
it.onGetImageSuccess(index, image)
}
}
}
fun notifyGetImageFailure(index: Int, error: String) {
synchronized(mSpiderListeners) {
for (listener in mSpiderListeners) {
listener.onGetImageFailure(index, error)
mSpiderListeners.forEach {
it.onGetImageFailure(index, error)
}
}
}
private var mInDownloadMode = false
private var downloadMode = false
@Synchronized
private fun updateMode() {
@@ -164,7 +180,7 @@ class SpiderQueen private constructor(val galleryInfo: GalleryInfo) : CoroutineS
// Update download page
val intoDownloadMode = mode == MODE_DOWNLOAD
if (intoDownloadMode && !mInDownloadMode) {
if (intoDownloadMode && !downloadMode) {
// Clear download state
synchronized(mPageStateLock) {
val temp: IntArray = mPageStateArray
@@ -184,7 +200,7 @@ class SpiderQueen private constructor(val galleryInfo: GalleryInfo) : CoroutineS
}
mWorkerScope.enterDownloadMode()
}
mInDownloadMode = intoDownloadMode
downloadMode = intoDownloadMode
}
private fun setMode(@Mode mode: Int) {
@@ -211,14 +227,14 @@ class SpiderQueen private constructor(val galleryInfo: GalleryInfo) : CoroutineS
}
}
private var prepareJob = launchIO { doPrepare() }
private val prepareJob = launchIO { doPrepare() }
private suspend fun doPrepare() {
mSpiderInfo = readSpiderInfoFromLocal() ?: readSpiderInfoFromInternet() ?: return
mPageStateArray = IntArray(mSpiderInfo.pages)
}
suspend fun awaitReady() {
private suspend fun awaitReady() {
prepareJob.join()
notifyGetPages(mSpiderInfo.pages)
}
@@ -520,9 +536,298 @@ class SpiderQueen private constructor(val galleryInfo: GalleryInfo) : CoroutineS
override val coroutineContext: CoroutineContext
get() = Dispatchers.IO + Job()
inner class SpiderQueenWorker {
private val mFetcherJobMap = hashMapOf<Int, Job>()
private val mSemaphore = Semaphore(Settings.getMultiThreadDownload())
private val pTokenLock = Mutex()
private var showKey: String? = null
private val showKeyLock = Mutex()
private val mDownloadDelay = Settings.getDownloadDelay()
private var isDownloadMode = false
fun cancelDecode(index: Int) {
decoder.cancel(index)
}
@Synchronized
fun enterDownloadMode() {
if (isDownloadMode) return
updateRAList((0 until size).toList())
isDownloadMode = true
}
fun updateRAList(list: List<Int>, cancelBounds: Pair<Int, Int> = 0 to Int.MAX_VALUE) {
if (isDownloadMode) return
synchronized(mFetcherJobMap) {
mFetcherJobMap.forEach { (i, job) ->
if (i < cancelBounds.first || i > cancelBounds.second)
job.cancel()
}
list.forEach {
if (mFetcherJobMap[it]?.isActive != true)
doLaunchDownloadJob(it, false)
}
}
}
private fun doLaunchDownloadJob(index: Int, force: Boolean) {
val state = mPageStateArray[index]
if (!force && state == STATE_FINISHED) return
val currentJob = mFetcherJobMap[index]
if (force) currentJob?.cancel()
if (currentJob?.isActive != true) {
mFetcherJobMap[index] = launch {
mSemaphore.withPermit {
doInJob(index, force)
}
}
}
}
@JvmOverloads
fun launch(index: Int, force: Boolean = false) {
check(index in 0 until size)
if (!isDownloadMode) synchronized(mFetcherJobMap) { doLaunchDownloadJob(index, force) }
if (force) decoder.cancel(index)
decoder.launch(index)
}
private suspend fun doInJob(index: Int, force: Boolean) {
fun getPToken(index: Int): String? {
if (index !in 0 until size) return null
return mSpiderInfo.pTokenMap[index].takeIf { it != TOKEN_FAILED }
?: getPTokenFromInternet(index)
?: getPTokenFromInternet(index)
?: getPTokenFromMultiPageViewer(index)
}
updatePageState(index, STATE_DOWNLOADING)
if (!force && index in mSpiderDen) {
return updatePageState(index, STATE_FINISHED)
}
if (force) {
pTokenLock.withLock {
val pToken = mSpiderInfo.pTokenMap[index]
if (pToken == TOKEN_FAILED) mSpiderInfo.pTokenMap.remove(index)
}
}
val previousPToken: String?
val pToken: String
pTokenLock.withLock {
pToken = getPToken(index) ?: return updatePageState(
index,
STATE_FAILED,
PTOKEN_FAILED_MESSAGE
).also {
mSpiderInfo.pTokenMap[index] = TOKEN_FAILED
}
previousPToken = getPToken(index - 1)
}
var skipHathKey: String? = null
val skipHathKeys = mutableListOf<String>()
var originImageUrl: String? = null
var error: String? = null
var forceHtml = false
var leakSkipHathKey = false
repeat(3) {
var imageUrl: String? = null
var localShowKey: String?
showKeyLock.withLock {
localShowKey = showKey
if (localShowKey == null || forceHtml) {
if (leakSkipHathKey) return@repeat
var pageUrl = EhUrl.getPageUrl(mSpiderInfo.gid, index, pToken)
// Add skipHathKey
if (skipHathKey != null) {
pageUrl += if ("?" in pageUrl) {
"&nl=$skipHathKey"
} else {
"?nl=$skipHathKey"
}
}
runSuspendCatching {
EhEngine.getGalleryPage(pageUrl, mSpiderInfo.gid, mSpiderInfo.token)
.also {
if (StringUtils.endsWith(it.imageUrl, URL_509_SUFFIX_ARRAY)) {
// Get 509
notifyGet509(index)
error = ERROR_509
return@repeat
}
}
}.onSuccess { result ->
imageUrl = result.imageUrl
skipHathKey = result.skipHathKey.takeIf { it.isNotBlank() }
originImageUrl = result.originImageUrl
localShowKey = result.showKey
if (skipHathKey != null) {
if (skipHathKey in skipHathKeys) {
// Duplicate skip hath key
leakSkipHathKey = true
} else {
skipHathKeys.add(skipHathKey!!)
}
} else {
leakSkipHathKey = true
}
showKey = result.showKey
}.onFailure {
if (it is ParseException && "Key mismatch" == it.message) {
// Show key is wrong, enter a new loop to get the new show key
if (showKey == localShowKey) showKey = null
return@repeat
} else {
error = ExceptionUtils.getReadableString(it)
return@repeat
}
}
}
}
if (imageUrl == null) {
if (localShowKey == null) {
error = "ShowKey error"
return@repeat
}
runSuspendCatching {
EhEngine.getGalleryPageApi(
mSpiderInfo.gid,
index,
pToken,
localShowKey,
previousPToken
).also {
if (StringUtils.endsWith(it.imageUrl, URL_509_SUFFIX_ARRAY)) {
// Get 509
notifyGet509(index)
error = ERROR_509
return@repeat
}
}
}.onFailure {
if (it is ParseException && "Key mismatch" == it.message) {
// Show key is wrong, enter a new loop to get the new show key
if (showKey == localShowKey) showKey = null
} else {
error = ExceptionUtils.getReadableString(it)
}
return@repeat
}.onSuccess {
imageUrl = it.imageUrl
skipHathKey = it.skipHathKey
originImageUrl = it.originImageUrl
}
}
val targetImageUrl: String?
val referer: String?
if (Settings.getDownloadOriginImage() && !originImageUrl.isNullOrBlank()) {
targetImageUrl = originImageUrl
referer = EhUrl.getPageUrl(mSpiderInfo.gid, index, pToken)
} else {
targetImageUrl = imageUrl
referer = null
}
if (targetImageUrl == null) {
error = "TargetImageUrl error"
return@repeat
}
Log.d(WORKER_DEBUG_TAG, targetImageUrl)
runSuspendCatching {
Log.d(WORKER_DEBUG_TAG, "Start download image $index")
val success: Boolean = mSpiderDen.makeHttpCallAndSaveImage(
index,
targetImageUrl,
referer
) { contentLength: Long, receivedSize: Long, bytesRead: Int ->
mPagePercentMap[index] = receivedSize.toFloat() / contentLength
notifyPageDownload(index, contentLength, receivedSize, bytesRead)
}
if (!success) {
Log.e(WORKER_DEBUG_TAG, "Can't download all of image data")
error = "Incomplete"
forceHtml = true
return@repeat
}
if (mSpiderDen.checkPlainText(index)) {
error = ""
forceHtml = true
return@repeat
}
Log.d(WORKER_DEBUG_TAG, "Download image succeed $index")
updatePageState(index, STATE_FINISHED)
delay(mDownloadDelay.toLong())
return
}.onFailure {
it.printStackTrace()
error = NETWORK_ERROR
}
Log.d(WORKER_DEBUG_TAG, "End download image $index")
}
mSpiderDen.remove(index)
updatePageState(index, STATE_FAILED, error)
}
private val decoder = Decoder()
inner class Decoder {
private val mSemaphore = Semaphore(4)
private val mDecodeJobMap = hashMapOf<Int, Job>()
fun cancel(index: Int) {
synchronized(mDecodeJobMap) {
mDecodeJobMap.remove(index)?.cancel()
}
}
fun launch(index: Int) {
synchronized(mDecodeJobMap) {
val currentJob = mDecodeJobMap[index]
if (currentJob?.isActive != true) {
mDecodeJobMap[index] = launch {
doInJob(index)
}
}
}
}
private suspend fun doInJob(index: Int) {
mFetcherJobMap[index]?.takeIf { it.isActive }?.join()
val src = mSpiderDen.getImageSource(index) ?: return
val image = mSemaphore.withPermit { Image.decode(src) }
runCatching {
currentCoroutineContext().ensureActive()
}.onFailure {
image?.recycle()
throw it
}
if (image == null) {
notifyGetImageFailure(index, DECODE_ERROR)
} else {
notifyGetImageSuccess(index, image)
}
}
}
}
}
var PTOKEN_FAILED_MESSAGE = GetText.getString(R.string.error_get_ptoken_error)
var ERROR_509 = GetText.getString(R.string.error_509)
var NETWORK_ERROR = GetText.getString(R.string.error_socket)
var DECODE_ERROR = GetText.getString(R.string.error_decoding_failed)
private val PTOKEN_FAILED_MESSAGE = GetText.getString(R.string.error_get_ptoken_error)
private val ERROR_509 = GetText.getString(R.string.error_509)
private val NETWORK_ERROR = GetText.getString(R.string.error_socket)
private val DECODE_ERROR = GetText.getString(R.string.error_decoding_failed)
private val URL_509_SUFFIX_ARRAY = arrayOf(
"/509.gif",
"/509s.gif"
)
private const val WORKER_DEBUG_TAG = "SpiderQueenWorker"
@@ -1,322 +0,0 @@
package com.hippo.ehviewer.spider
import android.util.Log
import com.hippo.ehviewer.Settings
import com.hippo.ehviewer.client.EhEngine.getGalleryPage
import com.hippo.ehviewer.client.EhEngine.getGalleryPageApi
import com.hippo.ehviewer.client.EhUrl.getPageUrl
import com.hippo.ehviewer.client.exception.ParseException
import com.hippo.ehviewer.spider.SpiderQueen.Companion.STATE_FAILED
import com.hippo.ehviewer.spider.SpiderQueen.Companion.STATE_FINISHED
import com.hippo.image.Image.Companion.decode
import com.hippo.util.ExceptionUtils
import com.hippo.yorozuya.StringUtils
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.currentCoroutineContext
import kotlinx.coroutines.delay
import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.Semaphore
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.sync.withPermit
import moe.tarsin.coroutines.runSuspendCatching
class SpiderQueenWorker(private val queen: SpiderQueen) : CoroutineScope by queen {
private val spiderDen
get() = queen.mSpiderDen
private val spiderInfo
get() = queen.mSpiderInfo
private val mFetcherJobMap = hashMapOf<Int, Job>()
private val mSemaphore = Semaphore(Settings.getMultiThreadDownload())
private val pTokenLock = Mutex()
private var showKey: String? = null
private val showKeyLock = Mutex()
private val mDownloadDelay = Settings.getDownloadDelay()
private var isDownloadMode = false
private val size
get() = queen.size
fun cancelDecode(index: Int) {
decoder.cancel(index)
}
@Synchronized
fun enterDownloadMode() {
if (isDownloadMode) return
updateRAList((0 until size).toList())
isDownloadMode = true
}
fun updateRAList(list: List<Int>, cancelBounds: Pair<Int, Int> = 0 to Int.MAX_VALUE) {
if (isDownloadMode) return
synchronized(mFetcherJobMap) {
mFetcherJobMap.forEach { (i, job) ->
if (i < cancelBounds.first || i > cancelBounds.second)
job.cancel()
}
list.forEach {
if (mFetcherJobMap[it]?.isActive != true)
doLaunchDownloadJob(it, false)
}
}
}
private fun doLaunchDownloadJob(index: Int, force: Boolean) {
val state = queen.mPageStateArray[index]
if (!force && state == STATE_FINISHED) return
val currentJob = mFetcherJobMap[index]
if (force) currentJob?.cancel()
if (currentJob?.isActive != true) {
mFetcherJobMap[index] = launch {
mSemaphore.withPermit {
doInJob(index, force)
}
}
}
}
@JvmOverloads
fun launch(index: Int, force: Boolean = false) {
check(index in 0 until size)
if (!isDownloadMode) synchronized(mFetcherJobMap) { doLaunchDownloadJob(index, force) }
if (force) decoder.cancel(index)
decoder.launch(index)
}
private suspend fun doInJob(index: Int, force: Boolean) {
fun getPToken(index: Int): String? {
if (index !in 0 until size) return null
return spiderInfo.pTokenMap[index].takeIf { it != TOKEN_FAILED }
?: queen.getPTokenFromInternet(index)
?: queen.getPTokenFromInternet(index)
?: queen.getPTokenFromMultiPageViewer(index)
}
queen.updatePageState(index, SpiderQueen.STATE_DOWNLOADING)
if (!force && index in spiderDen) {
return queen.updatePageState(index, STATE_FINISHED)
}
if (force) {
pTokenLock.withLock {
val pToken = spiderInfo.pTokenMap[index]
if (pToken == TOKEN_FAILED) spiderInfo.pTokenMap.remove(index)
}
}
val previousPToken: String?
val pToken: String
pTokenLock.withLock {
pToken = getPToken(index) ?: return queen.updatePageState(
index,
STATE_FAILED,
PTOKEN_FAILED_MESSAGE
).also {
spiderInfo.pTokenMap[index] = TOKEN_FAILED
}
previousPToken = getPToken(index - 1)
}
var skipHathKey: String? = null
val skipHathKeys = mutableListOf<String>()
var originImageUrl: String? = null
var error: String? = null
var forceHtml = false
var leakSkipHathKey = false
repeat(3) {
var imageUrl: String? = null
var localShowKey: String?
showKeyLock.withLock {
localShowKey = showKey
if (localShowKey == null || forceHtml) {
if (leakSkipHathKey) return@repeat
var pageUrl = getPageUrl(spiderInfo.gid, index, pToken)
// Add skipHathKey
if (skipHathKey != null) {
pageUrl += if ("?" in pageUrl) {
"&nl=$skipHathKey"
} else {
"?nl=$skipHathKey"
}
}
runSuspendCatching {
getGalleryPage(pageUrl, spiderInfo.gid, spiderInfo.token).also {
if (StringUtils.endsWith(it.imageUrl, URL_509_SUFFIX_ARRAY)) {
// Get 509
queen.notifyGet509(index)
error = ERROR_509
return@repeat
}
}
}.onSuccess { result ->
imageUrl = result.imageUrl
skipHathKey = result.skipHathKey.takeIf { it.isNotBlank() }
originImageUrl = result.originImageUrl
localShowKey = result.showKey
if (skipHathKey != null) {
if (skipHathKey in skipHathKeys) {
// Duplicate skip hath key
leakSkipHathKey = true
} else {
skipHathKeys.add(skipHathKey!!)
}
} else {
leakSkipHathKey = true
}
showKey = result.showKey
}.onFailure {
if (it is ParseException && "Key mismatch" == it.message) {
// Show key is wrong, enter a new loop to get the new show key
if (showKey == localShowKey) showKey = null
return@repeat
} else {
error = ExceptionUtils.getReadableString(it)
return@repeat
}
}
}
}
if (imageUrl == null) {
if (localShowKey == null) {
error = "ShowKey error"
return@repeat
}
runSuspendCatching {
getGalleryPageApi(
spiderInfo.gid,
index,
pToken,
localShowKey,
previousPToken
).also {
if (StringUtils.endsWith(it.imageUrl, URL_509_SUFFIX_ARRAY)) {
// Get 509
queen.notifyGet509(index)
error = ERROR_509
return@repeat
}
}
}.onFailure {
if (it is ParseException && "Key mismatch" == it.message) {
// Show key is wrong, enter a new loop to get the new show key
if (showKey == localShowKey) showKey = null
} else {
error = ExceptionUtils.getReadableString(it)
}
return@repeat
}.onSuccess {
imageUrl = it.imageUrl
skipHathKey = it.skipHathKey
originImageUrl = it.originImageUrl
}
}
val targetImageUrl: String?
val referer: String?
if (Settings.getDownloadOriginImage() && !originImageUrl.isNullOrBlank()) {
targetImageUrl = originImageUrl
referer = getPageUrl(spiderInfo.gid, index, pToken)
} else {
targetImageUrl = imageUrl
referer = null
}
if (targetImageUrl == null) {
error = "TargetImageUrl error"
return@repeat
}
Log.d(DEBUG_TAG, targetImageUrl)
runSuspendCatching {
Log.d(DEBUG_TAG, "Start download image $index")
val success: Boolean = spiderDen.makeHttpCallAndSaveImage(
index,
targetImageUrl,
referer
) { contentLength: Long, receivedSize: Long, bytesRead: Int ->
queen.mPagePercentMap[index] = receivedSize.toFloat() / contentLength
queen.notifyPageDownload(index, contentLength, receivedSize, bytesRead)
}
if (!success) {
Log.e(DEBUG_TAG, "Can't download all of image data")
error = "Incomplete"
forceHtml = true
return@repeat
}
if (spiderDen.checkPlainText(index)) {
error = ""
forceHtml = true
return@repeat
}
Log.d(DEBUG_TAG, "Download image succeed $index")
queen.updatePageState(index, STATE_FINISHED)
delay(mDownloadDelay.toLong())
return
}.onFailure {
it.printStackTrace()
error = NETWORK_ERROR
}
Log.d(DEBUG_TAG, "End download image $index")
}
spiderDen.remove(index)
queen.updatePageState(index, STATE_FAILED, error)
}
companion object {
private val URL_509_SUFFIX_ARRAY = arrayOf(
"/509.gif",
"/509s.gif"
)
private const val DEBUG_TAG = "SpiderQueenWorker"
}
private val decoder = Decoder()
inner class Decoder {
private val mSemaphore = Semaphore(4)
private val mDecodeJobMap = hashMapOf<Int, Job>()
fun cancel(index: Int) {
synchronized(mDecodeJobMap) {
mDecodeJobMap.remove(index)?.cancel()
}
}
fun launch(index: Int) {
synchronized(mDecodeJobMap) {
val currentJob = mDecodeJobMap[index]
if (currentJob?.isActive != true) {
mDecodeJobMap[index] = launch {
doInJob(index)
}
}
}
}
private suspend fun doInJob(index: Int) {
mFetcherJobMap[index]?.takeIf { it.isActive }?.join()
val src = spiderDen.getImageSource(index) ?: return
val image = mSemaphore.withPermit { decode(src) }
runCatching {
currentCoroutineContext().ensureActive()
}.onFailure {
image?.recycle()
throw it
}
if (image == null) {
queen.notifyGetImageFailure(index, DECODE_ERROR)
} else {
queen.notifyGetImageSuccess(index, image)
}
}
}
}