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 dd0dc591..b4520b63 100644 --- a/app/src/main/kotlin/me/eternal/purrfectsnap/download/DownloadProcessor.kt +++ b/app/src/main/kotlin/me/eternal/purrfectsnap/download/DownloadProcessor.kt @@ -283,7 +283,7 @@ class DownloadProcessor ( while (true) { val existingFile = outputFileFolder.findFile(finalFileName) ?: break - if (existingFile.length() == inputFile.length()) { + if (existingFile.length() == inputFile.length() && !remoteSideContext.config.root.downloader.allowDuplicate.get()) { val existingInputStream = remoteSideContext.androidContext.contentResolver.openInputStream(existingFile.uri) if (existingInputStream != null && streamsMatch(existingInputStream, inputFile.inputStream())) { return GallerySaveResult(existingFile.uri, alreadyDownloaded = true) @@ -376,7 +376,7 @@ class DownloadProcessor ( var destFile = File(destDir, fileName) var suffix = 1 while (destFile.exists()) { - if (destFile.length() == inputFile.length() && streamsMatch(destFile.inputStream(), inputFile.inputStream())) { + if (destFile.length() == inputFile.length() && !remoteSideContext.config.root.downloader.allowDuplicate.get() && streamsMatch(destFile.inputStream(), inputFile.inputStream())) { return GallerySaveResult(Uri.fromFile(destFile), alreadyDownloaded = true) } destFile = File(destDir, appendNameSuffix(fileName, suffix++)) 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 3a622360..4778968b 100644 --- a/app/src/main/kotlin/me/eternal/purrfectsnap/download/FFMpegProcessor.kt +++ b/app/src/main/kotlin/me/eternal/purrfectsnap/download/FFMpegProcessor.kt @@ -168,10 +168,7 @@ class FFMpegProcessor( } Action.MERGE_OVERLAY -> { inputArguments += "-i" to args.overlay!!.absolutePath - outputArguments += "-filter_complex" to "\"[1:v][0:v]scale2ref=w=iw:h=ih[ovrl][main];[main][ovrl]overlay=(W-w)/2:(H-h)/2,scale=2*trunc(iw/2):2*trunc(ih/2)[vout]\"" - outputArguments += "-map" to "\"[vout]\"" - outputArguments += "-map" to "\"0:a?\"" - outputArguments += "-shortest" to "" + outputArguments += "-filter_complex" to "\"[0]scale2ref[img][vid];[img]setsar=1[img];[vid]nullsink;[img][1]overlay=(W-w)/2:(H-h)/2,scale=2*trunc(iw*sar/2):2*trunc(ih/2)\"" } Action.CONVERSION -> { if (ffmpegOptions.customAudioCodec.isEmpty()) { 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 3711eee2..65d4401e 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 @@ -189,19 +189,23 @@ class MediaDownloader : MessagingRuleFeature("MediaDownloader", MessagingRuleTyp if (isBatch) { batchSuccessCount.incrementAndGet() if (downloadLogging.contains("success")) { - modCtx.inAppOverlay.showStatusToast( - icon = Icons.Outlined.DownloadDone, - text = translations.format("batch_progress_toast", "current" to (batchSuccessCount.get() + batchFailureCount.get()).toString(), "total" to batchTotalCount.get().toString()), - durationMs = 1300 - ) + modCtx.runOnUiThread { + modCtx.inAppOverlay.showStatusToast( + icon = Icons.Outlined.DownloadDone, + text = translations.format("batch_progress_toast", "current" to (batchSuccessCount.get() + batchFailureCount.get()).toString(), "total" to batchTotalCount.get().toString()), + durationMs = 1300 + ) + } } return@launch } if (downloadLogging.contains("success")) { val toastText = translations.format("content_saved_toast", "path" to java.io.File(finalOutputFile).name) - if (modCtx.isMainActivityPaused) modCtx.shortToast(toastText) - modCtx.inAppOverlay.showStatusToast(Icons.Outlined.DownloadDone, toastText, 1300) + modCtx.runOnUiThread { + if (modCtx.isMainActivityPaused) modCtx.shortToast(toastText) + modCtx.inAppOverlay.showStatusToast(Icons.Outlined.DownloadDone, toastText, 1300) + } } } } @@ -209,16 +213,20 @@ class MediaDownloader : MessagingRuleFeature("MediaDownloader", MessagingRuleTyp override fun onProgress(message: String) { if (isBatch || !downloadLogging.contains("progress")) return val toastText = message.ifBlank { translations["download_started_toast"] ?: "Started" } - if (modCtx.isMainActivityPaused) modCtx.shortToast(toastText) - modCtx.inAppOverlay.showStatusToast(Icons.Outlined.Info, toastText, 1300) + modCtx.runOnUiThread { + if (modCtx.isMainActivityPaused) modCtx.shortToast(toastText) + modCtx.inAppOverlay.showStatusToast(Icons.Outlined.Info, toastText, 1300) + } } 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.incrementAndGet(); return } - if (modCtx.isMainActivityPaused) modCtx.shortToast(errorText) - modCtx.inAppOverlay.showStatusToast(Icons.Outlined.ErrorOutline, errorText, 1300) + modCtx.runOnUiThread { + if (modCtx.isMainActivityPaused) modCtx.shortToast(errorText) + modCtx.inAppOverlay.showStatusToast(Icons.Outlined.ErrorOutline, errorText, 1300) + } } } ) 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 1cf39601..4f45efe4 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 @@ -54,7 +54,6 @@ class AutoOpenSnaps: MessagingRuleFeature("Auto Open Snaps", MessagingRuleType.A private const val NOTIFICATION_GROUP_KEY = "purrfectsnap.AUTO_OPEN" private const val PREF_TOTAL_OPENED = "auto_open_total_opened" private const val PREF_SESSION_START = "auto_open_session_start" - private const val PREF_PROCESSED_IDS = "auto_open_processed_ids" private const val LAZY_SAVE_INTERVAL_MS = 600_000L } @@ -79,12 +78,6 @@ class AutoOpenSnaps: MessagingRuleFeature("Auto Open Snaps", MessagingRuleType.A private val prefs by lazy { this@AutoOpenSnaps.context.androidContext.getSharedPreferences("me.eternal.purrfectsnap_preferences", Context.MODE_PRIVATE) } private val messaging by lazy { this@AutoOpenSnaps.context.feature(Messaging::class) } private var wakeLock: PowerManager.WakeLock? = null - private var wakeLockCooldownJob: Job? = null - - // Optimized Metadata Cache: 500 entries limit to prevent OOM crashes - private val metadataCache = Collections.synchronizedMap(object : LinkedHashMap(100, 0.75f, true) { - override fun removeEldestEntry(eldest: MutableMap.MutableEntry?): Boolean = size > 500 - }) private var currentStatusText = "Monitoring..." private var currentSpeedText = "Full Speed" @@ -117,6 +110,7 @@ class AutoOpenSnaps: MessagingRuleFeature("Auto Open Snaps", MessagingRuleType.A } override fun init() { + restorePersistence() createNotificationChannels() // NATIVE HOOKS: Ensuring Snapchat never sees the app as "In Background" @@ -127,6 +121,14 @@ class AutoOpenSnaps: MessagingRuleFeature("Auto Open Snaps", MessagingRuleType.A val state = param.arg(0).toString() if (state == "INACTIVE" || state == "BACKGROUND") param.setResult(null) } + // INDUSTRIAL FIX: Restoring the universal v1.6.8 background wake-up hook + hookConstructor(HookStage.AFTER) { param -> + methods.firstOrNull { it.name == "appStateChanged" }?.let { method -> + val enumClass = method.parameterTypes[0] + val activeState = enumClass.enumConstants?.firstOrNull { it.toString() == "ACTIVE" || it.toString() == "FOREGROUND" } + if (activeState != null) method.invoke(param.thisObject(), activeState) + } + } } findClass("com.snapchat.client.network_manager.NetworkManager\$CppProxy").apply { hook("onAppForegrounded", HookStage.BEFORE) { param -> param.setResult(null) } @@ -201,7 +203,7 @@ class AutoOpenSnaps: MessagingRuleFeature("Auto Open Snaps", MessagingRuleType.A sessionProcessed.incrementAndGet(); totalProcessed.incrementAndGet(); recordSpeedTimestamp() val duration = System.currentTimeMillis() - startTime averageProcessingTime.set((averageProcessingTime.get() * 0.7 + duration * 0.3).toLong()) - break + triggerLazySave(); break } delay((autoOpenConfig.retryDelay as PropertyValue).get().toLong()) } @@ -265,18 +267,7 @@ class AutoOpenSnaps: MessagingRuleFeature("Auto Open Snaps", MessagingRuleType.A val contentType = message.messageContent?.contentType if (contentType != ContentType.SNAP && contentType != ContentType.EXTERNAL_MEDIA) return@subscribe - - // Prevent processing of snaps already viewed manually by the local user - if (message.messageMetadata?.openedBy?.any { it.toString() == this@AutoOpenSnaps.context.database.myUserId } == true) { - openedSnapsIds.add(clientMessageId) - return@subscribe - } - if (!canUseRule(conversationId)) return@subscribe - - val currentQueueSize = synchronized(queuedSnaps) { queuedSnaps.size } - if (currentQueueSize >= (autoOpenConfig.queueSize as PropertyValue).get()) return@subscribe - if (openedSnapsIds.contains(clientMessageId)) return@subscribe openedSnapsIds.add(clientMessageId) @@ -286,10 +277,32 @@ class AutoOpenSnaps: MessagingRuleFeature("Auto Open Snaps", MessagingRuleType.A synchronized(queuedSnaps) { queuedSnaps.add(item) } snapChannel.trySend(item) - acquireWakeLock(); updateStatusNotification() + acquireWakeLock(); updateStatusNotification(); triggerLazySave() } } + private fun triggerLazySave() { + needsSaving.set(true) + if (isSaving.compareAndSet(false, true)) { + this@AutoOpenSnaps.context.coroutineScope.launch(Dispatchers.IO) { + while (needsSaving.get() && engineActive.get()) { + needsSaving.set(false); saveQueueToDisk(); delay(LAZY_SAVE_INTERVAL_MS) + } + isSaving.set(false) + } + } + } + + private fun saveQueueToDisk() { + prefs.edit { putInt(PREF_TOTAL_OPENED, totalProcessed.get()); putLong(PREF_SESSION_START, sessionStartTime.get()) } + } + + private fun restorePersistence() { + val savedStartTime = prefs.getLong(PREF_SESSION_START, 0) + if (System.currentTimeMillis() - savedStartTime > 21600000) return + totalProcessed.set(prefs.getInt(PREF_TOTAL_OPENED, 0)); sessionStartTime.set(savedStartTime) + } + private fun isWifiConnected(): Boolean { val cm = this@AutoOpenSnaps.context.androidContext.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager return cm.getNetworkCapabilities(cm.activeNetwork)?.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) == true @@ -452,4 +465,4 @@ class AutoOpenSnaps: MessagingRuleFeature("Auto Open Snaps", MessagingRuleType.A private fun getSnapContentType(type: ContentType?): String = when (type) { ContentType.SNAP -> "Photo/Video"; ContentType.EXTERNAL_MEDIA -> "Media"; else -> "Message" } } -data class SnapQueueItem(val conversationId: String, val messageId: Long, val serverMessageId: Long, val senderId: String, val senderName: String, val conversationType: String, val contentType: String, val timestamp: Long = System.currentTimeMillis()) +data class SnapQueueItem(val conversationId: String, val messageId: Long, val serverMessageId: Long, val senderId: String, val senderName: String, val conversationType: String, val contentType: String)