fix(PR): Restore Auto Open stability by Kaladin
Restore Auto Open stability
This commit is contained in:
@@ -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)
|
||||||
|
|||||||
@@ -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."
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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 }
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user