fix(PR): Restore Auto Open stability by Kaladin

Restore Auto Open stability
This commit is contained in:
ΞTΞRNAL
2026-04-02 14:01:08 +05:30
committed by GitHub
5 changed files with 272 additions and 156 deletions

View File

@@ -86,7 +86,7 @@ class AnnouncementCheckWorker(
val pendingIntent = PendingIntent.getActivity(appContext, 0, intent, PendingIntent.FLAG_IMMUTABLE) val pendingIntent = PendingIntent.getActivity(appContext, 0, intent, PendingIntent.FLAG_IMMUTABLE)
val builder = NotificationCompat.Builder(appContext, channelId) val builder = NotificationCompat.Builder(appContext, channelId)
.setSmallIcon(R.drawable.launcher_icon_monochrome) .setSmallIcon(R.mipmap.ic_launcher)
.setContentTitle(title) .setContentTitle(title)
.setContentText(text) .setContentText(text)
.setPriority(NotificationCompat.PRIORITY_DEFAULT) .setPriority(NotificationCompat.PRIORITY_DEFAULT)

View File

@@ -1648,6 +1648,7 @@
"status_monitoring": "Monitoring", "status_monitoring": "Monitoring",
"status_active": "Active", "status_active": "Active",
"status_paused": "Paused", "status_paused": "Paused",
"thermal_status_title": "Thermal Cooling (Throttled)",
"processed_count": "Opened", "processed_count": "Opened",
"queue_size": "Queue", "queue_size": "Queue",
"action_reset": "Reset Statistics", "action_reset": "Reset Statistics",
@@ -1682,10 +1683,6 @@
"name": "Auto Open Compact Notification", "name": "Auto Open Compact Notification",
"description": "Use a smaller, single-line notification for status updates" "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": { "show_lifetime_stats": {
"name": "Show Lifetime Statistics", "name": "Show Lifetime Statistics",
"description": "Include the total number of snaps opened since installation in the notification" "description": "Include the total number of snaps opened since installation in the notification"
@@ -1694,6 +1691,10 @@
"name": "Show Queue Preview", "name": "Show Queue Preview",
"description": "Show a list of the most recent snaps waiting in the queue (Expanded only)" "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_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": { "only_when_idle": {
"name": "Auto Open Schedule", "name": "Auto Open Schedule",
@@ -3028,7 +3029,8 @@
"NOTE": "Audio Note", "NOTE": "Audio Note",
"SNAP": "Snap", "SNAP": "Snap",
"SAVEABLE_SNAP": "Saveable 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": { "strip_media_metadata": {
"hide_caption_text": "Hide Caption Text", "hide_caption_text": "Hide Caption Text",
@@ -3763,6 +3765,7 @@
"paused_message": "Processing paused. Queue preserved ({count} snaps)", "paused_message": "Processing paused. Queue preserved ({count} snaps)",
"status_paused": "Paused", "status_paused": "Paused",
"status_monitoring": "Monitoring", "status_monitoring": "Monitoring",
"thermal_status_title": "Thermal Cooling (Throttled)",
"status_active": "Active", "status_active": "Active",
"status_failed": "Failed to open {sender}", "status_failed": "Failed to open {sender}",
"status_retrying": "Retrying in background...", "status_retrying": "Retrying in background...",
@@ -3881,7 +3884,6 @@
"material3_strings": { "material3_strings": {
"date_range_picker_start_headline": "From", "date_range_picker_start_headline": "From",
"date_range_picker_end_headline": "To", "date_range_picker_end_headline": "To",
"date_range_picker_title": "Select date range",
"date_picker_switch_to_calendar_mode": "Calendar", "date_picker_switch_to_calendar_mode": "Calendar",
"date_picker_switch_to_input_mode": "Input", "date_picker_switch_to_input_mode": "Input",
"date_range_picker_scroll_to_previous_month": "Previous month", "date_range_picker_scroll_to_previous_month": "Previous month",
@@ -4365,10 +4367,3 @@
"tasks_remove_selected_tasks_confirm": "Remove {count} selected tasks?", "tasks_remove_selected_tasks_confirm": "Remove {count} selected tasks?",
"tasks_remove_all_tasks_confirm": "This will stop all running tasks and clear the history." "tasks_remove_all_tasks_confirm": "This will stop all running tasks and clear the history."
} }

View File

@@ -176,9 +176,9 @@ class MessagingTweaks : ConfigContainer() {
inputCheck = { it.toIntOrNull()?.coerceAtLeast(1000) != null } inputCheck = { it.toIntOrNull()?.coerceAtLeast(1000) != null }
} }
val compactNotification = boolean("compact_notification", false) val compactNotification = boolean("compact_notification", false)
val showProgressBar = boolean("show_progress_bar", true)
val showLifetimeStats = boolean("show_lifetime_stats", false) val showLifetimeStats = boolean("show_lifetime_stats", false)
val showQueuePreview = boolean("show_queue_preview", true) val showQueuePreview = boolean("show_queue_preview", true)
val thermalProtection = boolean("thermal_protection", false)
// Resource Intelligence: Smart triggers for battery and data safety // Resource Intelligence: Smart triggers for battery and data safety
val onlyOnWifi = boolean("only_on_wifi", false) val onlyOnWifi = boolean("only_on_wifi", false)

View File

@@ -2,9 +2,11 @@ package me.eternal.purrfectsnap.core.features
import me.eternal.purrfectsnap.common.data.MessagingRuleType import me.eternal.purrfectsnap.common.data.MessagingRuleType
import me.eternal.purrfectsnap.common.data.RuleState import me.eternal.purrfectsnap.common.data.RuleState
import java.util.concurrent.ConcurrentHashMap
abstract class MessagingRuleFeature(name: String, val ruleType: MessagingRuleType) : Feature(name) { abstract class MessagingRuleFeature(name: String, val ruleType: MessagingRuleType) : Feature(name) {
private val listeners = mutableListOf<(String, Boolean) -> Unit>() private val listeners = mutableListOf<(String, Boolean) -> Unit>()
private val ruleCache = ConcurrentHashMap<String, Boolean>()
fun addStateListener(listener: (conversationId: String, newState: Boolean) -> Unit) { fun addStateListener(listener: (conversationId: String, newState: Boolean) -> Unit) {
listeners.add(listener) listeners.add(listener)
@@ -13,18 +15,22 @@ abstract class MessagingRuleFeature(name: String, val ruleType: MessagingRuleTyp
open fun getRuleState() = context.config.rules.getRuleState(ruleType) open fun getRuleState() = context.config.rules.getRuleState(ruleType)
fun setState(conversationId: String, state: Boolean) { fun setState(conversationId: String, state: Boolean) {
val targetId = context.database.getDMOtherParticipant(conversationId) ?: conversationId
context.bridgeClient.setRule( context.bridgeClient.setRule(
context.database.getDMOtherParticipant(conversationId) ?: conversationId, targetId,
ruleType, ruleType,
state state
) )
ruleCache[targetId] = state
listeners.forEach { it(conversationId, state) } listeners.forEach { it(conversationId, state) }
} }
fun getState(conversationId: String) = fun getState(conversationId: String): Boolean {
context.bridgeClient.getRules( val targetId = context.database.getDMOtherParticipant(conversationId) ?: conversationId
context.database.getDMOtherParticipant(conversationId) ?: conversationId return ruleCache.getOrPut(targetId) {
).contains(ruleType) && getRuleState() != null context.bridgeClient.getRules(targetId).contains(ruleType)
} && getRuleState() != null
}
fun canUseRule(conversationId: String): Boolean { fun canUseRule(conversationId: String): Boolean {
if (ruleType.key == "translation" && context.config.messaging.instantTranslation.globalState != true) { if (ruleType.key == "translation" && context.config.messaging.instantTranslation.globalState != true) {

View File

@@ -1,5 +1,6 @@
package me.eternal.purrfectsnap.core.features.impl.experiments package me.eternal.purrfectsnap.core.features.impl.experiments
import android.app.ActivityManager
import android.app.Notification import android.app.Notification
import android.app.NotificationChannel import android.app.NotificationChannel
import android.app.NotificationManager import android.app.NotificationManager
@@ -9,39 +10,41 @@ import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.IntentFilter import android.content.IntentFilter
import android.content.SharedPreferences import android.content.SharedPreferences
import android.os.Build
import android.os.PowerManager
import android.app.ActivityManager
import android.net.ConnectivityManager import android.net.ConnectivityManager
import android.net.NetworkCapabilities import android.net.NetworkCapabilities
import android.os.Build
import android.os.PowerManager
import androidx.core.content.edit import androidx.core.content.edit
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import kotlinx.coroutines.* import kotlinx.coroutines.*
import kotlinx.coroutines.flow.MutableSharedFlow 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.ContentType
import me.eternal.purrfectsnap.common.data.MessageUpdate import me.eternal.purrfectsnap.common.data.MessageUpdate
import me.eternal.purrfectsnap.common.data.MessagingRuleType import me.eternal.purrfectsnap.common.data.MessagingRuleType
import me.eternal.purrfectsnap.core.event.events.impl.BuildMessageEvent import me.eternal.purrfectsnap.core.event.events.impl.BuildMessageEvent
import me.eternal.purrfectsnap.core.features.MessagingRuleFeature import me.eternal.purrfectsnap.core.features.MessagingRuleFeature
import me.eternal.purrfectsnap.core.features.impl.messaging.Messaging 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.HookStage
import me.eternal.purrfectsnap.core.util.hook.hook import me.eternal.purrfectsnap.core.util.hook.hook
import me.eternal.purrfectsnap.core.util.hook.hookConstructor import me.eternal.purrfectsnap.core.util.hook.hookConstructor
import java.util.concurrent.atomic.AtomicInteger import java.util.*
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicLong
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
import java.util.Calendar import java.util.concurrent.atomic.AtomicBoolean
import kotlin.random.Random import java.util.concurrent.atomic.AtomicInteger
import me.eternal.purrfectsnap.bridge.AutoOpenInterface import java.util.concurrent.atomic.AtomicLong
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import kotlin.coroutines.resume import kotlin.coroutines.resume
import kotlin.random.Random
class AutoOpenSnaps: MessagingRuleFeature("Auto Open Snaps", MessagingRuleType.AUTO_OPEN_SNAPS) { class AutoOpenSnaps: MessagingRuleFeature("Auto Open Snaps", MessagingRuleType.AUTO_OPEN_SNAPS) {
companion object { companion object {
const val ACTION_PAUSE_RESUME = "me.eternal.purrfectsnap.AUTO_OPEN_SNAPS_PAUSE_RESUME" 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" const val ACTION_CLEAR_QUEUE = "me.eternal.purrfectsnap.AUTO_OPEN_SNAPS_CLEAR_QUEUE"
private const val STATUS_NOTIFICATION_ID = 54321 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_OPENED = "auto_open_total_opened"
private const val PREF_TOTAL_DETECTED = "auto_open_total_detected" private const val PREF_TOTAL_DETECTED = "auto_open_total_detected"
private const val PREF_SESSION_START = "auto_open_session_start" 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<Long>(extraBufferCapacity = 100) private val snapQueue = MutableSharedFlow<Long>(extraBufferCapacity = 100)
private val openedSnaps = ConcurrentHashMap.newKeySet<Long>() private val openedSnaps = ConcurrentHashMap.newKeySet<Long>()
private val queuedSnaps = mutableListOf<SnapQueueItem>() private val queuedSnaps = mutableListOf<SnapQueueItem>()
private val deadLetterQueue = mutableListOf<SnapQueueItem>()
private val nameCache = ConcurrentHashMap<String, String>() private val nameCache = ConcurrentHashMap<String, String>()
private val conversationTypeCache = ConcurrentHashMap<String, String>() private val conversationTypeCache = ConcurrentHashMap<String, String>()
@@ -76,6 +80,20 @@ class AutoOpenSnaps: MessagingRuleFeature("Auto Open Snaps", MessagingRuleType.A
private var currentSpeedText = "Full Speed" private var currentSpeedText = "Full Speed"
private var isCurrentlyWaiting = false private var isCurrentlyWaiting = false
private var wakeLock: PowerManager.WakeLock? = null 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<Long>()
// Safety & Synergy
private val isSaving = AtomicBoolean(false)
private val needsSaving = AtomicBoolean(false)
private var isThermalThrottled = false
private var lastThermalThrottleAt = 0L
data class SnapQueueItem( data class SnapQueueItem(
val conversationId: String, val conversationId: String,
@@ -84,7 +102,8 @@ class AutoOpenSnaps: MessagingRuleFeature("Auto Open Snaps", MessagingRuleType.A
var senderName: String = "Pending...", var senderName: String = "Pending...",
var conversationType: String = "Processing", var conversationType: String = "Processing",
val contentType: String, val contentType: String,
val timestamp: Long = System.currentTimeMillis() val timestamp: Long = System.currentTimeMillis(),
var retryCount: Int = 0
) )
private val autoOpenInterface = object : AutoOpenInterface.Stub() { private val autoOpenInterface = object : AutoOpenInterface.Stub() {
@@ -104,6 +123,7 @@ class AutoOpenSnaps: MessagingRuleFeature("Auto Open Snaps", MessagingRuleType.A
lastPausedAt.set(0) lastPausedAt.set(0)
sessionStartTime.set(System.currentTimeMillis()) sessionStartTime.set(System.currentTimeMillis())
synchronized(queuedSnaps) { queuedSnaps.clear() } synchronized(queuedSnaps) { queuedSnaps.clear() }
synchronized(deadLetterQueue) { deadLetterQueue.clear() }
openedSnaps.clear() openedSnaps.clear()
prefs.edit() prefs.edit()
@@ -127,32 +147,30 @@ class AutoOpenSnaps: MessagingRuleFeature("Auto Open Snaps", MessagingRuleType.A
if (paused) { if (paused) {
lastPausedAt.set(System.currentTimeMillis()) lastPausedAt.set(System.currentTimeMillis())
} else { } else {
val pauseStarted = lastPausedAt.get() if (lastPausedAt.get() > 0) {
if (pauseStarted > 0) { totalPausedDuration.addAndGet(System.currentTimeMillis() - lastPausedAt.get())
totalPausedDuration.addAndGet(System.currentTimeMillis() - pauseStarted)
lastPausedAt.set(0)
} }
snapQueue.tryEmit(System.currentTimeMillis()) snapQueue.tryEmit(System.currentTimeMillis())
} }
updateStatusNotification() updateStatusNotification()
} }
ACTION_CLEAR_QUEUE -> { ACTION_CLEAR_QUEUE -> {
clearInternalState() synchronized(queuedSnaps) { queuedSnaps.clear() }
this@AutoOpenSnaps.context.log.info("[AutoOpen] All statistics and queue reset.") synchronized(deadLetterQueue) { deadLetterQueue.clear() }
triggerLazySave()
updateStatusNotification()
} }
} }
} }
} }
override fun init() { override fun init() {
if (config.globalState != true) return
context.log.info("[AutoOpen] Initializing Ultra Premium engine...")
val messaging = context.feature(Messaging::class) val messaging = context.feature(Messaging::class)
restorePersistence() restorePersistence()
hasBeenActive.set(true) hasBeenActive.set(true)
if (config.allowRunningInBackground.get()) { if (config.allowRunningInBackground.get()) {
acquireWakeLock()
findClass("com.snapchat.client.duplex.DuplexClient\$CppProxy").apply { findClass("com.snapchat.client.duplex.DuplexClient\$CppProxy").apply {
hook("appStateChanged", HookStage.BEFORE) { param -> hook("appStateChanged", HookStage.BEFORE) { param ->
if (config.allowRunningInBackground.get()) { if (config.allowRunningInBackground.get()) {
@@ -163,8 +181,14 @@ class AutoOpenSnaps: MessagingRuleFeature("Auto Open Snaps", MessagingRuleType.A
} }
} }
hookConstructor(HookStage.AFTER) { param -> hookConstructor(HookStage.AFTER) { param ->
methods.first { it.name == "appStateChanged" }.let { method -> methods.firstOrNull { it.name == "appStateChanged" }?.let { method ->
method.invoke(param.thisObject(), method.parameterTypes[0].enumConstants!!.first { it.toString() == "ACTIVE" }) val enumClass = method.parameterTypes[0]
val activeState = enumClass.enumConstants?.firstOrNull {
it.toString() == "ACTIVE" || it.toString() == "FOREGROUND"
}
if (activeState != null) {
method.invoke(param.thisObject<Any>(), activeState)
}
} }
} }
} }
@@ -182,11 +206,35 @@ class AutoOpenSnaps: MessagingRuleFeature("Auto Open Snaps", MessagingRuleType.A
val filter = IntentFilter().apply { val filter = IntentFilter().apply {
addAction(ACTION_PAUSE_RESUME) addAction(ACTION_PAUSE_RESUME)
addAction(ACTION_CLEAR_QUEUE) 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) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
context.androidContext.registerReceiver(actionReceiver, filter, Context.RECEIVER_NOT_EXPORTED) context.androidContext.registerReceiver(actionReceiver, filter, Context.RECEIVER_NOT_EXPORTED)
context.androidContext.registerReceiver(batteryReceiver, filter, Context.RECEIVER_NOT_EXPORTED)
} else { } else {
context.androidContext.registerReceiver(actionReceiver, filter) context.androidContext.registerReceiver(actionReceiver, filter)
context.androidContext.registerReceiver(batteryReceiver, filter)
}
if (synchronized(queuedSnaps) { queuedSnaps.isNotEmpty() }) {
snapQueue.tryEmit(System.currentTimeMillis())
} }
context.coroutineScope.launch(Dispatchers.Default) { context.coroutineScope.launch(Dispatchers.Default) {
@@ -198,9 +246,31 @@ class AutoOpenSnaps: MessagingRuleFeature("Auto Open Snaps", MessagingRuleType.A
val remainingCount = synchronized(queuedSnaps) { queuedSnaps.size } val remainingCount = synchronized(queuedSnaps) { queuedSnaps.size }
if (remainingCount == 0 && sessionProcessed.get() > 0) { if (remainingCount == 0 && sessionProcessed.get() > 0) {
sessionProcessed.set(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() updateStatusNotification()
if (remainingCount > 0) snapQueue.tryEmit(System.currentTimeMillis())
delay(5000) delay(5000)
} }
} }
@@ -208,15 +278,14 @@ class AutoOpenSnaps: MessagingRuleFeature("Auto Open Snaps", MessagingRuleType.A
context.coroutineScope.launch(Dispatchers.Default, CoroutineStart.UNDISPATCHED) { context.coroutineScope.launch(Dispatchers.Default, CoroutineStart.UNDISPATCHED) {
snapQueue.collect { _ -> snapQueue.collect { _ ->
while (isActive && config.globalState == true) { while (isActive && config.globalState == true) {
val item = synchronized(queuedSnaps) { queuedSnaps.firstOrNull() } ?: break if (isPaused.get()) {
delay(1000)
while (isPaused.get() || config.globalState != true) { continue
if (config.globalState != true) return@collect
currentStatusText = context.translation["auto_open_snaps.status_paused"] ?: "Paused"
isCurrentlyWaiting = true
updateStatusNotification()
delay(2000)
} }
val item = synchronized(queuedSnaps) {
if (queuedSnaps.isNotEmpty()) queuedSnaps.removeAt(0) else null
} ?: break
var resourceWaiting = true var resourceWaiting = true
while (resourceWaiting) { while (resourceWaiting) {
@@ -229,56 +298,57 @@ class AutoOpenSnaps: MessagingRuleFeature("Auto Open Snaps", MessagingRuleType.A
when { when {
config.onlyOnWifi.get() && !isWifi -> { config.onlyOnWifi.get() && !isWifi -> {
currentStatusText = context.translation["auto_open_snaps.only_on_wifi.name"] ?: "Waiting for WiFi..." 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 isCurrentlyWaiting = true
delay(5000) delay(5000)
} }
config.onlyWhenIdle.get() && !isIdle && !inSleepWindow -> { config.onlyWhenIdle.get() && !isIdle && !inSleepWindow -> {
currentStatusText = context.translation["auto_open_snaps.only_when_idle.name"] ?: "Waiting for idle..." 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 isCurrentlyWaiting = true
delay(5000) delay(5000)
} }
config.pauseDuringGaming.get() && isGaming -> { config.pauseDuringGaming.get() && isGaming -> {
currentStatusText = context.translation["auto_open_snaps.pause_during_gaming.name"] ?: "Paused (Gaming Mode)" 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 isCurrentlyWaiting = true
delay(60000) delay(60000)
} }
else -> { else -> {
resourceWaiting = false 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 (resourceWaiting) updateStatusNotification()
} }
if (isPaused.get() || config.globalState != true) continue if (isPaused.get() || config.globalState != true) {
synchronized(queuedSnaps) { queuedSnaps.add(0, item) }
continue
}
isCurrentlyWaiting = false isCurrentlyWaiting = false
val inSleepWindow = if (config.onlyWhenIdle.get()) isInsideSleepWindow() else false val inSleepWindow = if (config.onlyWhenIdle.get()) isInsideSleepWindow() else false
if (inSleepWindow) { if (inSleepWindow || isThermalThrottled) {
currentStatusText = context.translation["auto_open_snaps.speed_throttled"] ?: "Throttled" 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)) delay(Random.nextLong(3000, 5000))
} else if (lastConversationId != null && lastConversationId != item.conversationId) { } else if (lastConversationId != null && lastConversationId != item.conversationId) {
currentStatusText = "..." currentStatusText = "Switching chats..."
delay(Random.nextLong(1500, 2500)) delay(Random.nextLong(1500, 2500))
batchSnapCount = 0 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 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() updateStatusNotification()
if (config.safeProcessing.get() && !inSleepWindow) {
batchSnapCount++
if (batchSnapCount % 10 == 0) {
currentStatusText = "..."
updateStatusNotification()
delay(Random.nextLong(3000, 5000))
}
}
var success = false var success = false
val startTime = System.currentTimeMillis() val startTime = System.currentTimeMillis()
var currentRetryDelay = config.retryDelay.get().toLong() 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 if (isPaused.get() || config.globalState != true) break
while ((!config.allowRunningInBackground.get() && context.isMainActivityPaused) || (messaging.conversationManager == null && !config.allowRunningInBackground.get())) { while ((!config.allowRunningInBackground.get() && context.isMainActivityPaused) || (messaging.conversationManager == null && !config.allowRunningInBackground.get())) {
if (config.globalState != true || isPaused.get()) break if (config.globalState != true || isPaused.get()) break
currentStatusText = "..." currentStatusText = "Waiting for UI..."
isCurrentlyWaiting = true isCurrentlyWaiting = true
updateStatusNotification() updateStatusNotification()
delay(2000) delay(2000)
@@ -299,9 +369,19 @@ class AutoOpenSnaps: MessagingRuleFeature("Auto Open Snaps", MessagingRuleType.A
if (success) { if (success) {
totalProcessed.incrementAndGet() totalProcessed.incrementAndGet()
sessionProcessed.incrementAndGet() sessionProcessed.incrementAndGet()
synchronized(snapTimestamps) {
snapTimestamps.addLast(System.currentTimeMillis())
if (snapTimestamps.size > 100) snapTimestamps.removeFirst()
}
val duration = System.currentTimeMillis() - startTime val duration = System.currentTimeMillis() - startTime
averageProcessingTime.set((averageProcessingTime.get() * 0.7 + duration * 0.3).toLong()) 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 break
} }
if (i < config.retryAttempts.get() - 1) { if (i < config.retryAttempts.get() - 1) {
@@ -311,20 +391,26 @@ class AutoOpenSnaps: MessagingRuleFeature("Auto Open Snaps", MessagingRuleType.A
} }
} }
if (success) { if (!success && !isPaused.get()) {
synchronized(queuedSnaps) { queuedSnaps.removeAll { it.messageId == item.messageId }; saveQueueToDisk() }
} else if (!isPaused.get()) {
currentStatusText = context.translation["auto_open_snaps.status_failed"]?.replace("{sender}", item.senderName) ?: "Failed to open" currentStatusText = context.translation["auto_open_snaps.status_failed"]?.replace("{sender}", item.senderName) ?: "Failed to open"
updateStatusNotification() 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() }) { if (synchronized(queuedSnaps) { queuedSnaps.isEmpty() }) {
currentStatusText = context.translation["auto_open_snaps.status_monitoring"] ?: "Monitoring..." delay(500)
isCurrentlyWaiting = false if (synchronized(queuedSnaps) { queuedSnaps.isEmpty() }) {
updateStatusNotification() currentStatusText = context.translation["auto_open_snaps.status_monitoring"] ?: "Monitoring..."
releaseWakeLock() 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 (contentType != ContentType.SNAP && contentType != ContentType.EXTERNAL_MEDIA) return@subscribe
if (event.message.messageMetadata?.openedBy?.any { it.toString() == context.database.myUserId } == true) return@subscribe if (event.message.messageMetadata?.openedBy?.any { it.toString() == context.database.myUserId } == true) return@subscribe
acquireWakeLock()
context.coroutineScope.launch(Dispatchers.Default) { context.coroutineScope.launch(Dispatchers.Default) {
if (!canUseRule(conversationId)) return@launch if (!canUseRule(conversationId)) return@launch
if (!openedSnaps.add(clientMessageId)) 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)) val item = SnapQueueItem(conversationId, clientMessageId, senderId, getSenderDisplayName(senderId), getConversationType(conversationId, senderId), getSnapContentType(contentType))
synchronized(queuedSnaps) { synchronized(queuedSnaps) {
if (queuedSnaps.size >= config.queueSize.get()) queuedSnaps.removeFirstOrNull() 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" } 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() { 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 processed = sessionProcessed.get()
val total = totalProcessed.get() val total = totalProcessed.get()
val remaining = synchronized(queuedSnaps) { queuedSnaps.size } val remaining = synchronized(queuedSnaps) { queuedSnaps.size }
if (total <= 0 && remaining <= 0 && processed <= 0) return if (total <= 0 && remaining <= 0 && processed <= 0) return
val isWorking = remaining > 0 val isWorking = remaining > 0
val showProgressBar = config.showProgressBar.get() == true val isCompact = config.compactNotification.get() == true
val sessionTotal = processed + remaining val sessionTotal = processed + remaining
val progressPercent = if (sessionTotal > 0) (processed * 100) / sessionTotal else 0 val progressPercent = if (sessionTotal > 0) (processed * 100) / sessionTotal else 0
val speed = getSnapsPerSecond()
val builder = Notification.Builder(context.androidContext, "auto_open_snaps") 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) .setSmallIcon(if (isPaused.get()) android.R.drawable.ic_media_pause else android.R.drawable.ic_media_play)
.setOngoing(isWorking).setAutoCancel(!isWorking).setOnlyAlertOnce(true) .setOngoing(isWorking).setAutoCancel(!isWorking).setOnlyAlertOnce(true)
.setGroup(NOTIFICATION_GROUP_KEY).setGroupSummary(false)
// Disable native progress bar to avoid "Double Bar" issue with our Premium Unicode bar
builder.setProgress(0, 0, false)
val eta = if (isWorking && !isCurrentlyWaiting && !isPaused.get()) formatDuration(remaining * averageProcessingTime.get()) else null val eta = if (isWorking && !isCurrentlyWaiting && !isPaused.get()) formatDuration(remaining * averageProcessingTime.get()) else null
if (config.compactNotification.get() == true) { // Refined Collapsed Logic
builder.setContentTitle("Auto-Open: $currentStatusText") builder.setContentTitle("Auto-Open: $currentStatusText")
builder.setContentText("Remaining: $remaining | Opened: $processed" + (eta?.let { " | ETA: $it" } ?: ""))
if (isWorking) {
builder.setContentText("Opened: $processed │ ETA: ${eta ?: "..."}")
builder.setSubText("$progressPercent% • $remaining Queued")
} else { } else {
builder.setContentTitle("Auto-Open: $currentStatusText") // Idle stats for collapsed view
builder.setContentText("Remaining: $remaining | Opened: $processed") 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, if (isPaused.get()) "Resume" else "Pause", createPendingIntent(ACTION_PAUSE_RESUME)).build())
builder.addAction(Notification.Action.Builder(null, "Clear Queue", createPendingIntent(ACTION_CLEAR_QUEUE)).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 recentSnaps = synchronized(queuedSnaps) { queuedSnaps.takeLast(5) }
val bigTextStyle = Notification.BigTextStyle() val bigTextStyle = Notification.BigTextStyle()
val detailText = buildString { val detailText = buildString {
if (showProgressBar) append("${context.translation["auto_open_snaps.notification_statistics"] ?: "STATISTICS"} - ${drawProgressBar(progressPercent)}\n") append("QUEUE STATISTICS\n")
else append("${context.translation["auto_open_snaps.notification_statistics"] ?: "STATISTICS"}\n") append("├─ Opened: $processed snaps\n")
append("├─ Remaining: $remaining snaps\n")
append("\u251c\u2500 ${context.translation["auto_open_snaps.processed_count"] ?: "Opened"}: $processed snaps\n") if (eta != null) append("├─ Estimated time: $eta\n")
append("\u251c\u2500 ${context.translation["auto_open_snaps.queue_size"] ?: "Remaining"}: $remaining snaps\n") append("├─ Lifetime Opened: $total snaps\n")
append("└─ Speed: $currentSpeedText (${String.format("%.1f", speed)}/s)\n\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())
}
private fun drawProgressBar(percent: Int): String { append("QUEUE PREVIEW\n")
val totalBlocks = 12; val filledBlocks = (percent * totalBlocks) / 100 if (isWorking) {
return buildString { recentSnaps.reversed().forEach { item ->
append("[") append("${item.senderName}${item.conversationType} (${item.contentType})\n")
repeat(totalBlocks) { i -> }
when { i < filledBlocks -> append("\u2501"); i == filledBlocks -> append("\u2B26"); else -> append("\u2500") } } 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() { private fun shutdownFeature() {
@@ -459,19 +565,38 @@ class AutoOpenSnaps: MessagingRuleFeature("Auto Open Snaps", MessagingRuleType.A
.setSmallIcon(android.R.drawable.ic_dialog_info) .setSmallIcon(android.R.drawable.ic_dialog_info)
.setContentTitle("Auto-Open: Deactivated") .setContentTitle("Auto-Open: Deactivated")
.setContentText("Opened: $finalCount snaps | Session: ${durationMins}m") .setContentText("Opened: $finalCount snaps | Session: ${durationMins}m")
.setGroup(NOTIFICATION_GROUP_KEY)
.setAutoCancel(true).build() .setAutoCancel(true).build()
notificationManager.notify(Random.nextInt(), summary) notificationManager.notify(Random.nextInt(), summary)
hasBeenActive.set(false) hasBeenActive.set(false)
} }
resetPersistence(); releaseWakeLock() triggerLazySave()
releaseWakeLock()
} }
private fun saveStatsToDisk() { private fun triggerLazySave() {
prefs.edit().putInt(PREF_TOTAL_OPENED, totalProcessed.get()).putInt(PREF_TOTAL_DETECTED, totalDetected.get()).putLong(PREF_SESSION_START, sessionStartTime.get()).apply() 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() { private fun saveToDiskInternal() {
synchronized(queuedSnaps) { prefs.edit().putString(PREF_SAVED_QUEUE, gson.toJson(queuedSnaps)).apply() } 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() { private fun restorePersistence() {
@@ -483,8 +608,14 @@ class AutoOpenSnaps: MessagingRuleFeature("Auto Open Snaps", MessagingRuleType.A
try { try {
val type = object : TypeToken<List<SnapQueueItem>>() {}.type val type = object : TypeToken<List<SnapQueueItem>>() {}.type
val restored: List<SnapQueueItem> = gson.fromJson(savedQueueJson, type) val restored: List<SnapQueueItem> = gson.fromJson(savedQueueJson, type)
synchronized(queuedSnaps) { queuedSnaps.clear(); queuedSnaps.addAll(restored) } val now = System.currentTimeMillis()
} catch (e: Exception) { resetPersistence() } 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 { try {
val sleepWindow = config.sleepWindow.get() val sleepWindow = config.sleepWindow.get()
if (!sleepWindow.contains("-") || !sleepWindow.contains(":")) return false if (!sleepWindow.contains("-") || !sleepWindow.contains(":")) return false
val window = sleepWindow.split("-") val window = sleepWindow.split("-")
if (window.size != 2) return false if (window.size != 2) return false
val startStr = window[0].split(":") val startStr = window[0].split(":")
val endStr = window[1].split(":") val endStr = window[1].split(":")
if (startStr.size != 2 || endStr.size != 2) return false if (startStr.size != 2 || endStr.size != 2) return false
val now = Calendar.getInstance().apply { set(Calendar.SECOND, 0); set(Calendar.MILLISECOND, 0) }
val now = Calendar.getInstance().apply { 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) }
set(Calendar.SECOND, 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) }
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) return if (end.before(start)) now.after(start) || now.before(end) else now.after(start) && now.before(end)
} catch (e: Exception) { return false } } catch (e: Exception) { return false }
} }