diff --git a/app/src/main/kotlin/me/eternal/purrfectsnap/download/DownloadProcessor.kt b/app/src/main/kotlin/me/eternal/purrfectsnap/download/DownloadProcessor.kt index 6a1a7d84..dd0dc591 100644 --- a/app/src/main/kotlin/me/eternal/purrfectsnap/download/DownloadProcessor.kt +++ b/app/src/main/kotlin/me/eternal/purrfectsnap/download/DownloadProcessor.kt @@ -121,6 +121,7 @@ class DownloadProcessor ( inputFile.outputStream().use { bitmap.compress(compressFormat, 100, it) } + bitmap.recycle() fileType = FileType.fromFile(inputFile) } } @@ -673,6 +674,10 @@ class DownloadProcessor ( mergedBitmap.compress(compressFormat, 100, it) } + originalBitmap.recycle() + overlayBitmap.recycle() + mergedBitmap.recycle() + saveMediaToGallery(pendingTask, mergedImage, downloadMetadata) mergedImage.delete() renamedOverlayMedia.delete() diff --git a/app/src/main/kotlin/me/eternal/purrfectsnap/download/FFMpegProcessor.kt b/app/src/main/kotlin/me/eternal/purrfectsnap/download/FFMpegProcessor.kt index 86c0b053..5e4b9dd0 100644 --- a/app/src/main/kotlin/me/eternal/purrfectsnap/download/FFMpegProcessor.kt +++ b/app/src/main/kotlin/me/eternal/purrfectsnap/download/FFMpegProcessor.kt @@ -92,6 +92,10 @@ class FFMpegProcessor( private val sharedExecutor = Executors.newSingleThreadExecutor() + protected fun finalize() { + runCatching { sharedExecutor.shutdown() } + } + private suspend fun newFFMpegTask(globalArguments: ArgumentList, inputArguments: ArgumentList, outputArguments: ArgumentList) = suspendCancellableCoroutine { val stringBuilder = StringBuilder() arrayOf(globalArguments, inputArguments, outputArguments).forEach { argumentList -> diff --git a/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/impl/downloader/MediaDownloader.kt b/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/impl/downloader/MediaDownloader.kt index 9d0b76b5..9302bba7 100644 --- a/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/impl/downloader/MediaDownloader.kt +++ b/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/impl/downloader/MediaDownloader.kt @@ -76,6 +76,7 @@ import me.eternal.purrfectsnap.core.wrapper.impl.media.opera.ParamMap import me.eternal.purrfectsnap.core.wrapper.impl.media.toKeyPair import me.eternal.purrfectsnap.mapper.impl.OperaPageViewControllerMapper import java.util.UUID +import java.util.concurrent.atomic.AtomicInteger import kotlin.coroutines.resume import kotlin.coroutines.suspendCoroutine import kotlin.math.absoluteValue @@ -91,6 +92,8 @@ class MediaDownloader : MessagingRuleFeature("MediaDownloader", MessagingRuleTyp private set @Volatile private var pendingBatchDownloadIndices: MutableList? = null + private val batchLock = Any() + @Volatile private var batchForceAllowDuplicate: Boolean = false private val translations by lazy { @@ -107,12 +110,9 @@ class MediaDownloader : MessagingRuleFeature("MediaDownloader", MessagingRuleTyp private fun logVerbose(msg: String) = this@MediaDownloader.context.log.verbose("[MediaDownloader] $msg") private fun logError(msg: String, e: Throwable? = null) = if (e != null) this@MediaDownloader.context.log.error("[MediaDownloader] $msg", e) else this@MediaDownloader.context.log.error("[MediaDownloader] $msg") - @Volatile - private var batchTotalCount: Int = 0 - @Volatile - private var batchSuccessCount: Int = 0 - @Volatile - private var batchFailureCount: Int = 0 + private val batchTotalCount = AtomicInteger(0) + private val batchSuccessCount = AtomicInteger(0) + private val batchFailureCount = AtomicInteger(0) @Volatile private var initialBatchStoryIdentity: String? = null @@ -179,11 +179,11 @@ class MediaDownloader : MessagingRuleFeature("MediaDownloader", MessagingRuleTyp }.onFailure { logError("Post-Processing Logic Failed for $outputFile", it) } if (isBatch) { - batchSuccessCount++ + batchSuccessCount.incrementAndGet() if (downloadLogging.contains("success")) { modCtx.inAppOverlay.showStatusToast( icon = Icons.Outlined.DownloadDone, - text = translations.format("batch_progress_toast", "current" to (batchSuccessCount + batchFailureCount).toString(), "total" to batchTotalCount.toString()), + text = translations.format("batch_progress_toast", "current" to (batchSuccessCount.get() + batchFailureCount.get()).toString(), "total" to batchTotalCount.get().toString()), durationMs = 1300 ) } @@ -207,7 +207,7 @@ class MediaDownloader : MessagingRuleFeature("MediaDownloader", MessagingRuleTyp override fun onFailure(message: String, throwable: String?) { if (!downloadLogging.contains("failure")) return val errorText = translations[if (message == "Failed to download") "failed_generic_toast" else message] ?: message - if (isBatch) { batchFailureCount++; return } + if (isBatch) { batchFailureCount.incrementAndGet(); return } if (modCtx.isMainActivityPaused) modCtx.shortToast(errorText) modCtx.inAppOverlay.showStatusToast(Icons.Outlined.ErrorOutline, errorText, 1300) } @@ -283,9 +283,9 @@ class MediaDownloader : MessagingRuleFeature("MediaDownloader", MessagingRuleTyp val mediaInfoMap = lastSeenMediaInfoMap ?: return val modCtx = this@MediaDownloader.context - batchTotalCount = indices.size - batchSuccessCount = 0; batchFailureCount = 0 - pendingBatchDownloadIndices = indices + batchTotalCount.set(indices.size) + batchSuccessCount.set(0); batchFailureCount.set(0) + synchronized(batchLock) { pendingBatchDownloadIndices = indices } batchForceAllowDuplicate = allowDuplicate initialBatchStoryIdentity = paramMap.getStoryIdentity() @@ -297,7 +297,7 @@ class MediaDownloader : MessagingRuleFeature("MediaDownloader", MessagingRuleTyp modCtx.coroutineScope.launch { processNextBatchDownload(paramMap, mediaInfoMap) } } else { val jumped = modCtx.feature(OperaStoryOverlay::class).requestJumpToSnap(targetIndex, totalCount) - if (!jumped) { pendingBatchDownloadIndices = null; modCtx.shortToast(translations["batch_download_jump_failed_toast"] ?: "Jump Failed") } + if (!jumped) { synchronized(batchLock) { pendingBatchDownloadIndices = null }; modCtx.shortToast(translations["batch_download_jump_failed_toast"] ?: "Jump Failed") } } } @@ -305,13 +305,13 @@ class MediaDownloader : MessagingRuleFeature("MediaDownloader", MessagingRuleTyp runCatching { handleOperaMedia(paramMap, mediaInfoMap, forceDownload = true, forceAllowDuplicate = batchForceAllowDuplicate, isBatch = true) }.onFailure { - batchFailureCount++ - if (batchSuccessCount + batchFailureCount == batchTotalCount) flushPendingMergeAndComplete() + batchFailureCount.incrementAndGet() + if (batchSuccessCount.get() + batchFailureCount.get() == batchTotalCount.get()) flushPendingMergeAndComplete() } } private suspend fun processNextBatchDownload(paramMap: ParamMap, mediaInfoMap: Map) { - val queue = pendingBatchDownloadIndices ?: return + val queue = synchronized(batchLock) { pendingBatchDownloadIndices } ?: return if (queue.isEmpty()) return val modCtx = this@MediaDownloader.context @@ -323,7 +323,7 @@ class MediaDownloader : MessagingRuleFeature("MediaDownloader", MessagingRuleTyp val currentIndex = paramMap.getStorySnapIndex() ?: -1 if (currentIndex != queue.first()) return - queue.removeAt(0) + synchronized(batchLock) { queue.removeAt(0) } downloadSingleSnap(paramMap, mediaInfoMap) if (queue.isNotEmpty()) { @@ -333,7 +333,7 @@ class MediaDownloader : MessagingRuleFeature("MediaDownloader", MessagingRuleTyp android.os.Handler(android.os.Looper.getMainLooper()).postDelayed({ val jumped = runCatching { modCtx.feature(OperaStoryOverlay::class).requestJumpToSnap(queue.first(), totalCount) }.getOrNull() == true if (!jumped && retryCount < 1) tryJump(retryCount + 1) - else if (!jumped) { pendingBatchDownloadIndices = null; modCtx.shortToast(translations["batch_download_jump_failed_toast"] ?: "Jump Failed") } + else if (!jumped) { synchronized(batchLock) { pendingBatchDownloadIndices = null }; modCtx.shortToast(translations["batch_download_jump_failed_toast"] ?: "Jump Failed") } }, if (retryCount == 0) 120L else 220L) } tryJump() @@ -343,8 +343,8 @@ class MediaDownloader : MessagingRuleFeature("MediaDownloader", MessagingRuleTyp private fun flushPendingMergeAndComplete() { val modCtx = this@MediaDownloader.context - pendingBatchDownloadIndices = null - modCtx.shortToast(if (batchFailureCount == 0) translations["batch_download_complete_toast"] ?: "Batch Complete" else "Batch complete: $batchSuccessCount succeeded, $batchFailureCount failed") + synchronized(batchLock) { pendingBatchDownloadIndices = null } + modCtx.shortToast(if (batchFailureCount.get() == 0) translations["batch_download_complete_toast"] ?: "Batch Complete" else "Batch complete: ${batchSuccessCount.get()} succeeded, ${batchFailureCount.get()} failed") } fun showLastOperaDebugMediaInfo() { diff --git a/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/impl/experiments/AutoOpenSnaps.kt b/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/impl/experiments/AutoOpenSnaps.kt index e2837409..f413b786 100644 --- a/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/impl/experiments/AutoOpenSnaps.kt +++ b/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/impl/experiments/AutoOpenSnaps.kt @@ -373,7 +373,7 @@ class AutoOpenSnaps: MessagingRuleFeature("Auto Open Snaps", MessagingRuleType.A if (!isCompact) { val recentSnaps = synchronized(queuedSnaps) { queuedSnaps.takeLast(5) } - val bigTextStyle = Notification.BigTextStyle().setSummaryText("") + val bigTextStyle = Notification.BigTextStyle().setSummaryText(null) val detailText = buildString { append("QUEUE STATISTICS\n") append("├─ Opened: $processed snaps\n")