From 9ec9517ec52f278cdd8951859d3e071e0055a04d Mon Sep 17 00:00:00 2001 From: DarkKnight2122 <145188583+DarkKnight2122@users.noreply.github.com> Date: Thu, 2 Apr 2026 01:27:28 +0530 Subject: [PATCH] fix(core): Restore Auto Open stability --- .../task/AnnouncementCheckWorker.kt | 2 +- common/src/main/assets/lang/en_US.json | 21 +- .../common/config/impl/MessagingTweaks.kt | 2 +- .../core/features/MessagingRuleFeature.kt | 16 +- .../impl/experiments/AutoOpenSnaps.kt | 387 ++++++++++++------ 5 files changed, 272 insertions(+), 156 deletions(-) diff --git a/app/src/main/kotlin/me/eternal/purrfectsnap/task/AnnouncementCheckWorker.kt b/app/src/main/kotlin/me/eternal/purrfectsnap/task/AnnouncementCheckWorker.kt index 9ea9018f..3a611432 100644 --- a/app/src/main/kotlin/me/eternal/purrfectsnap/task/AnnouncementCheckWorker.kt +++ b/app/src/main/kotlin/me/eternal/purrfectsnap/task/AnnouncementCheckWorker.kt @@ -86,7 +86,7 @@ class AnnouncementCheckWorker( val pendingIntent = PendingIntent.getActivity(appContext, 0, intent, PendingIntent.FLAG_IMMUTABLE) val builder = NotificationCompat.Builder(appContext, channelId) - .setSmallIcon(R.drawable.launcher_icon_monochrome) + .setSmallIcon(R.mipmap.ic_launcher) .setContentTitle(title) .setContentText(text) .setPriority(NotificationCompat.PRIORITY_DEFAULT) diff --git a/common/src/main/assets/lang/en_US.json b/common/src/main/assets/lang/en_US.json index 27fbcfa4..c1ea12a3 100644 --- a/common/src/main/assets/lang/en_US.json +++ b/common/src/main/assets/lang/en_US.json @@ -1648,6 +1648,7 @@ "status_monitoring": "Monitoring", "status_active": "Active", "status_paused": "Paused", + "thermal_status_title": "Thermal Cooling (Throttled)", "processed_count": "Opened", "queue_size": "Queue", "action_reset": "Reset Statistics", @@ -1682,10 +1683,6 @@ "name": "Auto Open Compact Notification", "description": "Use a smaller, single-line notification for status updates" }, - "show_progress_bar": { - "name": "Show Progress Bar", - "description": "Display a visual progress bar in the status notification" - }, "show_lifetime_stats": { "name": "Show Lifetime Statistics", "description": "Include the total number of snaps opened since installation in the notification" @@ -1694,6 +1691,10 @@ "name": "Show Queue Preview", "description": "Show a list of the most recent snaps waiting in the queue (Expanded only)" }, + "thermal_protection": { + "name": "Thermal Protection", + "description": "Automatically throttles the engine and increases delays if the device temperature exceeds 40°C to prevent overheating" + }, "only_on_wifi": { "name": "Auto Open only on Wi-Fi", "description": "Only process queue when connected to a Wi-Fi network to save mobile data" }, "only_when_idle": { "name": "Auto Open Schedule", @@ -3028,7 +3029,8 @@ "NOTE": "Audio Note", "SNAP": "Snap", "SAVEABLE_SNAP": "Saveable Snap", - "null": "Snapchat Default" + "null": "Snapchat Default", + "multiple_media_toast": "You can only send one media at a time" }, "strip_media_metadata": { "hide_caption_text": "Hide Caption Text", @@ -3763,6 +3765,7 @@ "paused_message": "Processing paused. Queue preserved ({count} snaps)", "status_paused": "Paused", "status_monitoring": "Monitoring", + "thermal_status_title": "Thermal Cooling (Throttled)", "status_active": "Active", "status_failed": "Failed to open {sender}", "status_retrying": "Retrying in background...", @@ -3881,7 +3884,6 @@ "material3_strings": { "date_range_picker_start_headline": "From", "date_range_picker_end_headline": "To", - "date_range_picker_title": "Select date range", "date_picker_switch_to_calendar_mode": "Calendar", "date_picker_switch_to_input_mode": "Input", "date_range_picker_scroll_to_previous_month": "Previous month", @@ -4365,10 +4367,3 @@ "tasks_remove_selected_tasks_confirm": "Remove {count} selected tasks?", "tasks_remove_all_tasks_confirm": "This will stop all running tasks and clear the history." } - - - - - - - diff --git a/common/src/main/kotlin/me/eternal/purrfectsnap/common/config/impl/MessagingTweaks.kt b/common/src/main/kotlin/me/eternal/purrfectsnap/common/config/impl/MessagingTweaks.kt index 9b0f83e0..af54ef09 100644 --- a/common/src/main/kotlin/me/eternal/purrfectsnap/common/config/impl/MessagingTweaks.kt +++ b/common/src/main/kotlin/me/eternal/purrfectsnap/common/config/impl/MessagingTweaks.kt @@ -176,9 +176,9 @@ class MessagingTweaks : ConfigContainer() { inputCheck = { it.toIntOrNull()?.coerceAtLeast(1000) != null } } val compactNotification = boolean("compact_notification", false) - val showProgressBar = boolean("show_progress_bar", true) val showLifetimeStats = boolean("show_lifetime_stats", false) val showQueuePreview = boolean("show_queue_preview", true) + val thermalProtection = boolean("thermal_protection", false) // Resource Intelligence: Smart triggers for battery and data safety val onlyOnWifi = boolean("only_on_wifi", false) diff --git a/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/MessagingRuleFeature.kt b/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/MessagingRuleFeature.kt index f9b41442..d26192af 100644 --- a/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/MessagingRuleFeature.kt +++ b/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/MessagingRuleFeature.kt @@ -2,9 +2,11 @@ package me.eternal.purrfectsnap.core.features import me.eternal.purrfectsnap.common.data.MessagingRuleType import me.eternal.purrfectsnap.common.data.RuleState +import java.util.concurrent.ConcurrentHashMap abstract class MessagingRuleFeature(name: String, val ruleType: MessagingRuleType) : Feature(name) { private val listeners = mutableListOf<(String, Boolean) -> Unit>() + private val ruleCache = ConcurrentHashMap() fun addStateListener(listener: (conversationId: String, newState: Boolean) -> Unit) { listeners.add(listener) @@ -13,18 +15,22 @@ abstract class MessagingRuleFeature(name: String, val ruleType: MessagingRuleTyp open fun getRuleState() = context.config.rules.getRuleState(ruleType) fun setState(conversationId: String, state: Boolean) { + val targetId = context.database.getDMOtherParticipant(conversationId) ?: conversationId context.bridgeClient.setRule( - context.database.getDMOtherParticipant(conversationId) ?: conversationId, + targetId, ruleType, state ) + ruleCache[targetId] = state listeners.forEach { it(conversationId, state) } } - fun getState(conversationId: String) = - context.bridgeClient.getRules( - context.database.getDMOtherParticipant(conversationId) ?: conversationId - ).contains(ruleType) && getRuleState() != null + fun getState(conversationId: String): Boolean { + val targetId = context.database.getDMOtherParticipant(conversationId) ?: conversationId + return ruleCache.getOrPut(targetId) { + context.bridgeClient.getRules(targetId).contains(ruleType) + } && getRuleState() != null + } fun canUseRule(conversationId: String): Boolean { if (ruleType.key == "translation" && context.config.messaging.instantTranslation.globalState != true) { 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 8d5a00b3..4fd2d71b 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 @@ -1,5 +1,6 @@ package me.eternal.purrfectsnap.core.features.impl.experiments +import android.app.ActivityManager import android.app.Notification import android.app.NotificationChannel import android.app.NotificationManager @@ -9,39 +10,41 @@ import android.content.Context import android.content.Intent import android.content.IntentFilter import android.content.SharedPreferences -import android.os.Build -import android.os.PowerManager -import android.app.ActivityManager import android.net.ConnectivityManager import android.net.NetworkCapabilities +import android.os.Build +import android.os.PowerManager import androidx.core.content.edit +import com.google.gson.Gson +import com.google.gson.reflect.TypeToken import kotlinx.coroutines.* import kotlinx.coroutines.flow.MutableSharedFlow +import me.eternal.purrfectsnap.bridge.AutoOpenInterface +import me.eternal.purrfectsnap.common.BuildConfig import me.eternal.purrfectsnap.common.data.ContentType import me.eternal.purrfectsnap.common.data.MessageUpdate import me.eternal.purrfectsnap.common.data.MessagingRuleType import me.eternal.purrfectsnap.core.event.events.impl.BuildMessageEvent import me.eternal.purrfectsnap.core.features.MessagingRuleFeature import me.eternal.purrfectsnap.core.features.impl.messaging.Messaging +import me.eternal.purrfectsnap.core.features.impl.tweaks.PerformanceMode import me.eternal.purrfectsnap.core.util.hook.HookStage import me.eternal.purrfectsnap.core.util.hook.hook import me.eternal.purrfectsnap.core.util.hook.hookConstructor -import java.util.concurrent.atomic.AtomicInteger -import java.util.concurrent.atomic.AtomicBoolean -import java.util.concurrent.atomic.AtomicLong +import java.util.* import java.util.concurrent.ConcurrentHashMap -import java.util.Calendar -import kotlin.random.Random -import me.eternal.purrfectsnap.bridge.AutoOpenInterface -import com.google.gson.Gson -import com.google.gson.reflect.TypeToken +import java.util.concurrent.atomic.AtomicBoolean +import java.util.concurrent.atomic.AtomicInteger +import java.util.concurrent.atomic.AtomicLong import kotlin.coroutines.resume +import kotlin.random.Random class AutoOpenSnaps: MessagingRuleFeature("Auto Open Snaps", MessagingRuleType.AUTO_OPEN_SNAPS) { companion object { const val ACTION_PAUSE_RESUME = "me.eternal.purrfectsnap.AUTO_OPEN_SNAPS_PAUSE_RESUME" const val ACTION_CLEAR_QUEUE = "me.eternal.purrfectsnap.AUTO_OPEN_SNAPS_CLEAR_QUEUE" private const val STATUS_NOTIFICATION_ID = 54321 + private const val NOTIFICATION_GROUP_KEY = "purrfectsnap.AUTO_OPEN" private const val PREF_TOTAL_OPENED = "auto_open_total_opened" private const val PREF_TOTAL_DETECTED = "auto_open_total_detected" private const val PREF_SESSION_START = "auto_open_session_start" @@ -62,6 +65,7 @@ class AutoOpenSnaps: MessagingRuleFeature("Auto Open Snaps", MessagingRuleType.A private val snapQueue = MutableSharedFlow(extraBufferCapacity = 100) private val openedSnaps = ConcurrentHashMap.newKeySet() private val queuedSnaps = mutableListOf() + private val deadLetterQueue = mutableListOf() private val nameCache = ConcurrentHashMap() private val conversationTypeCache = ConcurrentHashMap() @@ -76,6 +80,20 @@ class AutoOpenSnaps: MessagingRuleFeature("Auto Open Snaps", MessagingRuleType.A private var currentSpeedText = "Full Speed" private var isCurrentlyWaiting = false private var wakeLock: PowerManager.WakeLock? = null + private var lastQueueActivity = System.currentTimeMillis() + + // Throttling & Performance fields + private val lastNotificationUpdate = AtomicLong(0) + private val notificationUpdateDelay = 1000L + private val pendingNotificationUpdate = AtomicBoolean(false) + private val processedSinceLastSave = AtomicInteger(0) + private val snapTimestamps = LinkedList() + + // Safety & Synergy + private val isSaving = AtomicBoolean(false) + private val needsSaving = AtomicBoolean(false) + private var isThermalThrottled = false + private var lastThermalThrottleAt = 0L data class SnapQueueItem( val conversationId: String, @@ -84,7 +102,8 @@ class AutoOpenSnaps: MessagingRuleFeature("Auto Open Snaps", MessagingRuleType.A var senderName: String = "Pending...", var conversationType: String = "Processing", val contentType: String, - val timestamp: Long = System.currentTimeMillis() + val timestamp: Long = System.currentTimeMillis(), + var retryCount: Int = 0 ) private val autoOpenInterface = object : AutoOpenInterface.Stub() { @@ -104,6 +123,7 @@ class AutoOpenSnaps: MessagingRuleFeature("Auto Open Snaps", MessagingRuleType.A lastPausedAt.set(0) sessionStartTime.set(System.currentTimeMillis()) synchronized(queuedSnaps) { queuedSnaps.clear() } + synchronized(deadLetterQueue) { deadLetterQueue.clear() } openedSnaps.clear() prefs.edit() @@ -127,32 +147,30 @@ class AutoOpenSnaps: MessagingRuleFeature("Auto Open Snaps", MessagingRuleType.A if (paused) { lastPausedAt.set(System.currentTimeMillis()) } else { - val pauseStarted = lastPausedAt.get() - if (pauseStarted > 0) { - totalPausedDuration.addAndGet(System.currentTimeMillis() - pauseStarted) - lastPausedAt.set(0) + if (lastPausedAt.get() > 0) { + totalPausedDuration.addAndGet(System.currentTimeMillis() - lastPausedAt.get()) } snapQueue.tryEmit(System.currentTimeMillis()) } updateStatusNotification() } ACTION_CLEAR_QUEUE -> { - clearInternalState() - this@AutoOpenSnaps.context.log.info("[AutoOpen] All statistics and queue reset.") + synchronized(queuedSnaps) { queuedSnaps.clear() } + synchronized(deadLetterQueue) { deadLetterQueue.clear() } + triggerLazySave() + updateStatusNotification() } } } } override fun init() { - if (config.globalState != true) return - context.log.info("[AutoOpen] Initializing Ultra Premium engine...") - val messaging = context.feature(Messaging::class) restorePersistence() hasBeenActive.set(true) if (config.allowRunningInBackground.get()) { + acquireWakeLock() findClass("com.snapchat.client.duplex.DuplexClient\$CppProxy").apply { hook("appStateChanged", HookStage.BEFORE) { param -> if (config.allowRunningInBackground.get()) { @@ -163,8 +181,14 @@ class AutoOpenSnaps: MessagingRuleFeature("Auto Open Snaps", MessagingRuleType.A } } hookConstructor(HookStage.AFTER) { param -> - methods.first { it.name == "appStateChanged" }.let { method -> - method.invoke(param.thisObject(), method.parameterTypes[0].enumConstants!!.first { it.toString() == "ACTIVE" }) + 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) + } } } } @@ -182,11 +206,35 @@ class AutoOpenSnaps: MessagingRuleFeature("Auto Open Snaps", MessagingRuleType.A val filter = IntentFilter().apply { addAction(ACTION_PAUSE_RESUME) addAction(ACTION_CLEAR_QUEUE) + addAction(Intent.ACTION_BATTERY_CHANGED) } + + val batteryReceiver = object : BroadcastReceiver() { + override fun onReceive(ctx: Context?, intent: Intent?) { + if (intent?.action == Intent.ACTION_BATTERY_CHANGED && config.thermalProtection.get()) { + val temp = intent.getIntExtra("temperature", 0) / 10f + if (temp >= 40f && !isThermalThrottled) { + isThermalThrottled = true + lastThermalThrottleAt = System.currentTimeMillis() + context.log.warn("[THERMAL] Device hit ${temp}C. Throttling AutoOpen.") + } else if (isThermalThrottled && temp <= 36f && System.currentTimeMillis() - lastThermalThrottleAt > 600000) { + isThermalThrottled = false + context.log.info("[THERMAL] Device cooled to ${temp}C. Resuming full speed.") + } + } + } + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { context.androidContext.registerReceiver(actionReceiver, filter, Context.RECEIVER_NOT_EXPORTED) + context.androidContext.registerReceiver(batteryReceiver, filter, Context.RECEIVER_NOT_EXPORTED) } else { context.androidContext.registerReceiver(actionReceiver, filter) + context.androidContext.registerReceiver(batteryReceiver, filter) + } + + if (synchronized(queuedSnaps) { queuedSnaps.isNotEmpty() }) { + snapQueue.tryEmit(System.currentTimeMillis()) } context.coroutineScope.launch(Dispatchers.Default) { @@ -198,9 +246,31 @@ class AutoOpenSnaps: MessagingRuleFeature("Auto Open Snaps", MessagingRuleType.A val remainingCount = synchronized(queuedSnaps) { queuedSnaps.size } if (remainingCount == 0 && sessionProcessed.get() > 0) { sessionProcessed.set(0) + triggerLazySave() } + + if (remainingCount > 0) { + lastQueueActivity = System.currentTimeMillis() + acquireWakeLock() + if (!isPaused.get()) snapQueue.tryEmit(System.currentTimeMillis()) + } else { + // IDLE REVIVAL: Check dead letter queue every 5 mins when idle + if (!isPaused.get() && System.currentTimeMillis() - lastQueueActivity > 300000) { + val revived = synchronized(deadLetterQueue) { + if (deadLetterQueue.isNotEmpty()) deadLetterQueue.removeAt(0) else null + } + if (revived != null) { + synchronized(queuedSnaps) { queuedSnaps.add(revived) } + snapQueue.tryEmit(System.currentTimeMillis()) + } + } + + if (System.currentTimeMillis() - lastQueueActivity > 600000) { // 10 mins true idle + releaseWakeLock() + } + } + updateStatusNotification() - if (remainingCount > 0) snapQueue.tryEmit(System.currentTimeMillis()) delay(5000) } } @@ -208,15 +278,14 @@ class AutoOpenSnaps: MessagingRuleFeature("Auto Open Snaps", MessagingRuleType.A context.coroutineScope.launch(Dispatchers.Default, CoroutineStart.UNDISPATCHED) { snapQueue.collect { _ -> while (isActive && config.globalState == true) { - val item = synchronized(queuedSnaps) { queuedSnaps.firstOrNull() } ?: break - - while (isPaused.get() || config.globalState != true) { - if (config.globalState != true) return@collect - currentStatusText = context.translation["auto_open_snaps.status_paused"] ?: "Paused" - isCurrentlyWaiting = true - updateStatusNotification() - delay(2000) + if (isPaused.get()) { + delay(1000) + continue } + + val item = synchronized(queuedSnaps) { + if (queuedSnaps.isNotEmpty()) queuedSnaps.removeAt(0) else null + } ?: break var resourceWaiting = true while (resourceWaiting) { @@ -229,56 +298,57 @@ class AutoOpenSnaps: MessagingRuleFeature("Auto Open Snaps", MessagingRuleType.A when { config.onlyOnWifi.get() && !isWifi -> { currentStatusText = context.translation["auto_open_snaps.only_on_wifi.name"] ?: "Waiting for WiFi..." - currentSpeedText = context.translation["auto_open_snaps.paused_status"] ?: "Paused (No WiFi)" + currentSpeedText = "Throttled" isCurrentlyWaiting = true delay(5000) } config.onlyWhenIdle.get() && !isIdle && !inSleepWindow -> { currentStatusText = context.translation["auto_open_snaps.only_when_idle.name"] ?: "Waiting for idle..." - currentSpeedText = context.translation["auto_open_snaps.paused_status"] ?: "Paused (Device Active)" + currentSpeedText = "Throttled" isCurrentlyWaiting = true delay(5000) } config.pauseDuringGaming.get() && isGaming -> { currentStatusText = context.translation["auto_open_snaps.pause_during_gaming.name"] ?: "Paused (Gaming Mode)" - currentSpeedText = context.translation["auto_open_snaps.paused_status"] ?: "Paused (Gaming)" + currentSpeedText = "Paused" isCurrentlyWaiting = true delay(60000) } else -> { resourceWaiting = false - currentSpeedText = if (inSleepWindow) context.translation["auto_open_snaps.speed_throttled"] ?: "Throttled" else context.translation["auto_open_snaps.processing_speed_full"] ?: "Full Speed" + currentSpeedText = if (inSleepWindow || isThermalThrottled) "Throttled" else "Full Speed" } } if (resourceWaiting) updateStatusNotification() } - if (isPaused.get() || config.globalState != true) continue + if (isPaused.get() || config.globalState != true) { + synchronized(queuedSnaps) { queuedSnaps.add(0, item) } + continue + } isCurrentlyWaiting = false val inSleepWindow = if (config.onlyWhenIdle.get()) isInsideSleepWindow() else false - if (inSleepWindow) { - currentStatusText = context.translation["auto_open_snaps.speed_throttled"] ?: "Throttled" + if (inSleepWindow || isThermalThrottled) { + currentStatusText = if (isThermalThrottled) context.translation["auto_open_snaps.thermal_status_title"] ?: "Thermal Cooling" else context.translation["auto_open_snaps.speed_throttled"] ?: "Throttled" delay(Random.nextLong(3000, 5000)) } else if (lastConversationId != null && lastConversationId != item.conversationId) { - currentStatusText = "..." + currentStatusText = "Switching chats..." delay(Random.nextLong(1500, 2500)) batchSnapCount = 0 + triggerLazySave() + } else if (lastConversationId == item.conversationId) { + when { + isThermalThrottled -> delay(Random.nextLong(100, 150)) + config.safeProcessing.get() -> delay(Random.nextLong(50, 150)) + else -> delay(Random.nextLong(10, 40)) + } } lastConversationId = item.conversationId - currentStatusText = if (inSleepWindow) context.translation["auto_open_snaps.status_active"] ?: "Active" else context.translation["auto_open_snaps.status_active"] ?: "Opening snap..." + currentStatusText = context.translation["auto_open_snaps.status_active"] ?: "Opening snap..." updateStatusNotification() - if (config.safeProcessing.get() && !inSleepWindow) { - batchSnapCount++ - if (batchSnapCount % 10 == 0) { - currentStatusText = "..." - updateStatusNotification() - delay(Random.nextLong(3000, 5000)) - } - } - var success = false val startTime = System.currentTimeMillis() var currentRetryDelay = config.retryDelay.get().toLong() @@ -287,7 +357,7 @@ class AutoOpenSnaps: MessagingRuleFeature("Auto Open Snaps", MessagingRuleType.A if (isPaused.get() || config.globalState != true) break while ((!config.allowRunningInBackground.get() && context.isMainActivityPaused) || (messaging.conversationManager == null && !config.allowRunningInBackground.get())) { if (config.globalState != true || isPaused.get()) break - currentStatusText = "..." + currentStatusText = "Waiting for UI..." isCurrentlyWaiting = true updateStatusNotification() delay(2000) @@ -299,9 +369,19 @@ class AutoOpenSnaps: MessagingRuleFeature("Auto Open Snaps", MessagingRuleType.A if (success) { totalProcessed.incrementAndGet() sessionProcessed.incrementAndGet() + synchronized(snapTimestamps) { + snapTimestamps.addLast(System.currentTimeMillis()) + if (snapTimestamps.size > 100) snapTimestamps.removeFirst() + } val duration = System.currentTimeMillis() - startTime averageProcessingTime.set((averageProcessingTime.get() * 0.7 + duration * 0.3).toLong()) - saveStatsToDisk() + + val remaining = synchronized(queuedSnaps) { queuedSnaps.size } + val threshold = if (remaining > 100) 100 else 25 + if (processedSinceLastSave.incrementAndGet() >= threshold) { + triggerLazySave() + processedSinceLastSave.set(0) + } break } if (i < config.retryAttempts.get() - 1) { @@ -311,20 +391,26 @@ class AutoOpenSnaps: MessagingRuleFeature("Auto Open Snaps", MessagingRuleType.A } } - if (success) { - synchronized(queuedSnaps) { queuedSnaps.removeAll { it.messageId == item.messageId }; saveQueueToDisk() } - } else if (!isPaused.get()) { + if (!success && !isPaused.get()) { currentStatusText = context.translation["auto_open_snaps.status_failed"]?.replace("{sender}", item.senderName) ?: "Failed to open" updateStatusNotification() - delay(5000) - synchronized(queuedSnaps) { queuedSnaps.removeAll { it.messageId == item.messageId }; saveQueueToDisk() } + + // MOVE TO DEAD LETTER QUEUE (Revival Engine) + synchronized(deadLetterQueue) { + if (deadLetterQueue.size < 100) deadLetterQueue.add(item) + else { deadLetterQueue.removeAt(0); deadLetterQueue.add(item) } + } + delay(2000) } if (synchronized(queuedSnaps) { queuedSnaps.isEmpty() }) { - currentStatusText = context.translation["auto_open_snaps.status_monitoring"] ?: "Monitoring..." - isCurrentlyWaiting = false - updateStatusNotification() - releaseWakeLock() + delay(500) + if (synchronized(queuedSnaps) { queuedSnaps.isEmpty() }) { + currentStatusText = context.translation["auto_open_snaps.status_monitoring"] ?: "Monitoring..." + isCurrentlyWaiting = false + triggerLazySave() + updateStatusNotification() + } } } } @@ -339,6 +425,8 @@ class AutoOpenSnaps: MessagingRuleFeature("Auto Open Snaps", MessagingRuleType.A if (contentType != ContentType.SNAP && contentType != ContentType.EXTERNAL_MEDIA) return@subscribe if (event.message.messageMetadata?.openedBy?.any { it.toString() == context.database.myUserId } == true) return@subscribe + acquireWakeLock() + context.coroutineScope.launch(Dispatchers.Default) { if (!canUseRule(conversationId)) return@launch if (!openedSnaps.add(clientMessageId)) return@launch @@ -347,10 +435,11 @@ class AutoOpenSnaps: MessagingRuleFeature("Auto Open Snaps", MessagingRuleType.A val item = SnapQueueItem(conversationId, clientMessageId, senderId, getSenderDisplayName(senderId), getConversationType(conversationId, senderId), getSnapContentType(contentType)) synchronized(queuedSnaps) { if (queuedSnaps.size >= config.queueSize.get()) queuedSnaps.removeFirstOrNull() - queuedSnaps.add(item); totalDetected.incrementAndGet(); saveQueueToDisk() + queuedSnaps.add(item); totalDetected.incrementAndGet() + } + if (!isPaused.get()) { + snapQueue.tryEmit(System.currentTimeMillis()) } - acquireWakeLock() - snapQueue.tryEmit(System.currentTimeMillis()) } } } @@ -373,80 +462,97 @@ class AutoOpenSnaps: MessagingRuleFeature("Auto Open Snaps", MessagingRuleType.A return when { h > 0 -> "${h}h ${m}m ${s}s"; m > 0 -> "${m}m ${s}s"; else -> "${s}s" } } + private fun getSnapsPerSecond(): Double { + val now = System.currentTimeMillis() + val window = 5000L + synchronized(snapTimestamps) { + snapTimestamps.removeIf { now - it > window } + return (snapTimestamps.size.toDouble() / (window / 1000.0)) + } + } + private fun updateStatusNotification() { + val currentTime = System.currentTimeMillis() + val lastUpdate = lastNotificationUpdate.get() + + if ((currentTime - lastUpdate) < notificationUpdateDelay) { + if (pendingNotificationUpdate.compareAndSet(false, true)) { + context.coroutineScope.launch { + delay(notificationUpdateDelay - (currentTime - lastUpdate)) + pendingNotificationUpdate.set(false) + updateStatusNotificationInternal() + } + } + return + } + + lastNotificationUpdate.set(currentTime) + updateStatusNotificationInternal() + } + + private fun updateStatusNotificationInternal() { val processed = sessionProcessed.get() val total = totalProcessed.get() val remaining = synchronized(queuedSnaps) { queuedSnaps.size } if (total <= 0 && remaining <= 0 && processed <= 0) return val isWorking = remaining > 0 - val showProgressBar = config.showProgressBar.get() == true + val isCompact = config.compactNotification.get() == true val sessionTotal = processed + remaining val progressPercent = if (sessionTotal > 0) (processed * 100) / sessionTotal else 0 + val speed = getSnapsPerSecond() val builder = Notification.Builder(context.androidContext, "auto_open_snaps") .setSmallIcon(if (isPaused.get()) android.R.drawable.ic_media_pause else android.R.drawable.ic_media_play) .setOngoing(isWorking).setAutoCancel(!isWorking).setOnlyAlertOnce(true) - - // Disable native progress bar to avoid "Double Bar" issue with our Premium Unicode bar - builder.setProgress(0, 0, false) + .setGroup(NOTIFICATION_GROUP_KEY).setGroupSummary(false) val eta = if (isWorking && !isCurrentlyWaiting && !isPaused.get()) formatDuration(remaining * averageProcessingTime.get()) else null - if (config.compactNotification.get() == true) { - builder.setContentTitle("Auto-Open: $currentStatusText") - builder.setContentText("Remaining: $remaining | Opened: $processed" + (eta?.let { " | ETA: $it" } ?: "")) + // Refined Collapsed Logic + builder.setContentTitle("Auto-Open: $currentStatusText") + + if (isWorking) { + builder.setContentText("Opened: $processed │ ETA: ${eta ?: "..."}") + builder.setSubText("$progressPercent% • $remaining Queued") } else { - builder.setContentTitle("Auto-Open: $currentStatusText") - builder.setContentText("Remaining: $remaining | Opened: $processed") + // Idle stats for collapsed view + builder.setContentText("$processed Opened Today │ $total Lifetime") + builder.setSubText("Monitoring Snaps...") } + + builder.setProgress(if (isWorking) sessionTotal else 0, if (isWorking) processed else 0, !isWorking) builder.addAction(Notification.Action.Builder(null, if (isPaused.get()) "Resume" else "Pause", createPendingIntent(ACTION_PAUSE_RESUME)).build()) builder.addAction(Notification.Action.Builder(null, "Clear Queue", createPendingIntent(ACTION_CLEAR_QUEUE)).build()) - if (config.compactNotification.get() != true) { + if (!isCompact) { val recentSnaps = synchronized(queuedSnaps) { queuedSnaps.takeLast(5) } val bigTextStyle = Notification.BigTextStyle() + val detailText = buildString { - if (showProgressBar) append("${context.translation["auto_open_snaps.notification_statistics"] ?: "STATISTICS"} - ${drawProgressBar(progressPercent)}\n") - else append("${context.translation["auto_open_snaps.notification_statistics"] ?: "STATISTICS"}\n") - - append("\u251c\u2500 ${context.translation["auto_open_snaps.processed_count"] ?: "Opened"}: $processed snaps\n") - append("\u251c\u2500 ${context.translation["auto_open_snaps.queue_size"] ?: "Remaining"}: $remaining snaps\n") - - if (eta != null) { - append("\u251c\u2500 ${context.translation["auto_open_snaps.estimated_time"] ?: "Estimated time"}: $eta\n") - } - - if (config.showLifetimeStats.get()) { - append("\u251c\u2500 ${context.translation["auto_open_snaps.notification_total_opened"] ?: "Lifetime Opened"}: $total snaps\n") - } - - if (config.showQueuePreview.get()) { - append("\u2514\u2500 ${context.translation["auto_open_snaps.processing_speed"] ?: "Speed"}: $currentSpeedText\n\n${context.translation["auto_open_snaps.notification_queue_preview"] ?: "QUEUE PREVIEW"}\n") - if (isWorking) { - recentSnaps.reversed().forEach { item -> - append("\u2022 ${item.senderName}") - if (item.conversationType != "Friend DM" && item.conversationType != "Processing") append(" \u2502 ${item.conversationType}") - append(" (${item.contentType})\n") - } - } else append(context.translation["auto_open_snaps.notification_no_snaps_queue"] ?: "Monitoring snaps in background...") - } else append("\u2514\u2500 ${context.translation["auto_open_snaps.processing_speed"] ?: "Speed"}: $currentSpeedText") - } - bigTextStyle.bigText(detailText); builder.setStyle(bigTextStyle) - } - notificationManager.notify(STATUS_NOTIFICATION_ID, builder.build()) - } + append("QUEUE STATISTICS\n") + append("├─ Opened: $processed snaps\n") + append("├─ Remaining: $remaining snaps\n") + if (eta != null) append("├─ Estimated time: $eta\n") + append("├─ Lifetime Opened: $total snaps\n") + append("└─ Speed: $currentSpeedText (${String.format("%.1f", speed)}/s)\n\n") - private fun drawProgressBar(percent: Int): String { - val totalBlocks = 12; val filledBlocks = (percent * totalBlocks) / 100 - return buildString { - append("[") - repeat(totalBlocks) { i -> - when { i < filledBlocks -> append("\u2501"); i == filledBlocks -> append("\u2B26"); else -> append("\u2500") } + append("QUEUE PREVIEW\n") + if (isWorking) { + recentSnaps.reversed().forEach { item -> + append("• ${item.senderName} │ ${item.conversationType} (${item.contentType})\n") + } + } else { + append(context.translation["auto_open_snaps.notification_no_snaps_queue"] ?: "Monitoring snaps in background...") + } } - append("] $percent%") + bigTextStyle.bigText(detailText) + bigTextStyle.setSummaryText(null) + builder.setStyle(bigTextStyle) } + + notificationManager.notify(STATUS_NOTIFICATION_ID, builder.build()) } private fun shutdownFeature() { @@ -459,19 +565,38 @@ class AutoOpenSnaps: MessagingRuleFeature("Auto Open Snaps", MessagingRuleType.A .setSmallIcon(android.R.drawable.ic_dialog_info) .setContentTitle("Auto-Open: Deactivated") .setContentText("Opened: $finalCount snaps | Session: ${durationMins}m") + .setGroup(NOTIFICATION_GROUP_KEY) .setAutoCancel(true).build() notificationManager.notify(Random.nextInt(), summary) hasBeenActive.set(false) } - resetPersistence(); releaseWakeLock() + triggerLazySave() + releaseWakeLock() } - private fun saveStatsToDisk() { - prefs.edit().putInt(PREF_TOTAL_OPENED, totalProcessed.get()).putInt(PREF_TOTAL_DETECTED, totalDetected.get()).putLong(PREF_SESSION_START, sessionStartTime.get()).apply() + private fun triggerLazySave() { + needsSaving.set(true) + if (isSaving.compareAndSet(false, true)) { + context.coroutineScope.launch(Dispatchers.IO) { + while (needsSaving.get()) { + needsSaving.set(false) + saveToDiskInternal() + delay(1000) + } + isSaving.set(false) + } + } } - private fun saveQueueToDisk() { - synchronized(queuedSnaps) { prefs.edit().putString(PREF_SAVED_QUEUE, gson.toJson(queuedSnaps)).apply() } + private fun saveToDiskInternal() { + prefs.edit { + putInt(PREF_TOTAL_OPENED, totalProcessed.get()) + putInt(PREF_TOTAL_DETECTED, totalDetected.get()) + putLong(PREF_SESSION_START, sessionStartTime.get()) + synchronized(queuedSnaps) { + putString(PREF_SAVED_QUEUE, gson.toJson(queuedSnaps)) + } + } } private fun restorePersistence() { @@ -483,8 +608,14 @@ class AutoOpenSnaps: MessagingRuleFeature("Auto Open Snaps", MessagingRuleType.A try { val type = object : TypeToken>() {}.type val restored: List = gson.fromJson(savedQueueJson, type) - synchronized(queuedSnaps) { queuedSnaps.clear(); queuedSnaps.addAll(restored) } - } catch (e: Exception) { resetPersistence() } + val now = System.currentTimeMillis() + synchronized(queuedSnaps) { + queuedSnaps.clear() + queuedSnaps.addAll(restored.filter { (now - it.timestamp) < 3600000 }) + } + } catch (e: Exception) { + prefs.edit().remove(PREF_SAVED_QUEUE).apply() + } } } @@ -497,30 +628,14 @@ class AutoOpenSnaps: MessagingRuleFeature("Auto Open Snaps", MessagingRuleType.A try { val sleepWindow = config.sleepWindow.get() if (!sleepWindow.contains("-") || !sleepWindow.contains(":")) return false - val window = sleepWindow.split("-") if (window.size != 2) return false - val startStr = window[0].split(":") val endStr = window[1].split(":") if (startStr.size != 2 || endStr.size != 2) return false - - val now = Calendar.getInstance().apply { - set(Calendar.SECOND, 0) - set(Calendar.MILLISECOND, 0) - } - val start = Calendar.getInstance().apply { - set(Calendar.HOUR_OF_DAY, startStr[0].toInt()) - set(Calendar.MINUTE, startStr[1].toInt()) - set(Calendar.SECOND, 0) - set(Calendar.MILLISECOND, 0) - } - val end = Calendar.getInstance().apply { - set(Calendar.HOUR_OF_DAY, endStr[0].toInt()) - set(Calendar.MINUTE, endStr[1].toInt()) - set(Calendar.SECOND, 0) - set(Calendar.MILLISECOND, 0) - } + val now = Calendar.getInstance().apply { set(Calendar.SECOND, 0); set(Calendar.MILLISECOND, 0) } + val start = Calendar.getInstance().apply { set(Calendar.HOUR_OF_DAY, startStr[0].toInt()); set(Calendar.MINUTE, startStr[1].toInt()); set(Calendar.SECOND, 0); set(Calendar.MILLISECOND, 0) } + val end = Calendar.getInstance().apply { set(Calendar.HOUR_OF_DAY, endStr[0].toInt()); set(Calendar.MINUTE, endStr[1].toInt()); set(Calendar.SECOND, 0); set(Calendar.MILLISECOND, 0) } return if (end.before(start)) now.after(start) || now.before(end) else now.after(start) && now.before(end) } catch (e: Exception) { return false } }