Spoof Backup bug fixes

This commit is contained in:
DarkKnight2122
2026-04-25 00:40:15 +05:30
parent 5978717065
commit d19257869b
6 changed files with 161 additions and 101 deletions

View File

@@ -231,9 +231,12 @@ class FeaturesRootSection : Routes.Route() {
?: error("Failed to read randomized profile backup")
val profile = RandomizedDeviceProfile.fromJson(importedJson)
val generationToken = UUID.randomUUID().toString()
val profileJson = profile.toJson().toString()
// Save to local prefs for legacy compatibility
context.androidContext.getSharedPreferences("purrfectsnap_spoof", 0)
.edit()
.putString("randomized_device_profile", profile.toJson().toString())
.putString("randomized_device_profile", profileJson)
.putString("randomized_device_profile_token", generationToken)
.putString("android_id", profile.androidId)
.putString("advertising_id", profile.advertisingId)
@@ -246,6 +249,8 @@ class FeaturesRootSection : Routes.Route() {
val randomizeConfig = context.config.root.experimental.spoof.randomizeDeviceProfile
randomizeConfig.profileGenerationToken.set(generationToken)
randomizeConfig.currentProfileSnapshot.set(profile.toJson().toString(2))
randomizeConfig.profileData.set(profileJson) // Shared storage fix
context.config.writeConfig()
onConfigChanged()
context.shortToast("Randomized profile restored. Restart Snapchat to apply it.")

View File

@@ -64,7 +64,6 @@ class SocialRootSection : Routes.Route() {
}
// Real-time synchronization from the bridge
context.requestSocialSnapshotRefresh()
context.database.messagingDataFlow.collect { (friends, groups) ->
friendList = context.sortSocialFriends(friends)
groupList = groups

View File

@@ -186,6 +186,9 @@ class Spoof : ConfigContainer(hasGlobalState = true) {
val currentProfileSnapshot = string("current_profile_snapshot") {
addFlags(ConfigFlag.HIDDEN)
}
val profileData = string("profile_data") {
addFlags(ConfigFlag.HIDDEN)
}
}
inner class SpoofDeviceIdConfig : ConfigContainer() {

View File

@@ -14,10 +14,10 @@ import android.os.Build
import android.os.PowerManager
import androidx.core.content.edit
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.MutableSharedFlow
import me.eternal.purrfectsnap.bridge.AutoOpenInterface
import me.eternal.purrfectsnap.common.config.PropertyValue
import me.eternal.purrfectsnap.common.data.ContentType
@@ -31,6 +31,7 @@ import me.eternal.purrfectsnap.core.util.hook.HookStage
import me.eternal.purrfectsnap.core.util.hook.hook
import me.eternal.purrfectsnap.core.util.hook.hookConstructor
import java.util.*
import java.util.Objects
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicInteger
@@ -52,6 +53,7 @@ class AutoOpenSnaps: MessagingRuleFeature("Auto Open Snaps", MessagingRuleType.A
private const val NOTIFICATION_GROUP_KEY = "purrfectsnap.AUTO_OPEN"
private const val PREF_TOTAL_OPENED = "auto_open_total_opened"
private const val PREF_SESSION_START = "auto_open_session_start"
private const val PREF_SAVED_QUEUE = "auto_open_saved_queue"
}
private val gson = Gson()
@@ -64,9 +66,10 @@ class AutoOpenSnaps: MessagingRuleFeature("Auto Open Snaps", MessagingRuleType.A
private val averageProcessingTime = AtomicLong(800)
private val lastSnapProcessedAt = AtomicLong(0)
private val snapChannel = Channel<SnapQueueItem>(Channel.UNLIMITED)
private val snapQueue = MutableSharedFlow<Long>(extraBufferCapacity = 100, onBufferOverflow = BufferOverflow.DROP_OLDEST)
private val openedSnapsIds = ConcurrentHashMap.newKeySet<Long>()
private val queuedSnaps = LinkedList<SnapQueueItem>()
private val deadLetterQueue = mutableListOf<SnapQueueItem>()
private var engineJob: Job? = null
private val engineDispatcher = Dispatchers.Default.limitedParallelism(1)
@@ -85,6 +88,7 @@ class AutoOpenSnaps: MessagingRuleFeature("Auto Open Snaps", MessagingRuleType.A
private val pendingNotificationUpdate = AtomicBoolean(false)
private val snapTimestamps = LinkedList<Long>()
private var lastConversationId: String? = null
private var lastQueueActivity = System.currentTimeMillis()
private val lastSaveTime = AtomicLong(System.currentTimeMillis())
private var isThermalThrottled = false
@@ -135,10 +139,21 @@ class AutoOpenSnaps: MessagingRuleFeature("Auto Open Snaps", MessagingRuleType.A
// Background Watchdog: Periodically verifies engine health
this@AutoOpenSnaps.context.coroutineScope.launch(Dispatchers.Default) {
while (isActive && engineActive.get()) {
if (autoOpenConfig.globalState == true && synchronized(queuedSnaps) { queuedSnaps.isNotEmpty() }) {
updateStatusNotification()
val remainingCount = synchronized(queuedSnaps) { queuedSnaps.size }
if (remainingCount > 0) {
lastQueueActivity = System.currentTimeMillis(); acquireWakeLock()
if (!isPaused.get()) snapQueue.tryEmit(System.currentTimeMillis())
} else {
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 > 300000) {
startWakeLockCooldown()
}
}
delay(300000)
updateStatusNotification()
delay(30000) // 30s watchdog cycle
}
}
@@ -149,42 +164,44 @@ class AutoOpenSnaps: MessagingRuleFeature("Auto Open Snaps", MessagingRuleType.A
private fun startEngineWorker() {
engineJob = this@AutoOpenSnaps.context.coroutineScope.launch(engineDispatcher) {
while (engineActive.get()) {
val item = try { snapChannel.receive() } catch (e: Exception) { break }
while (isPaused.get() && engineActive.get()) {
currentStatusText = "Paused"; updateStatusNotification(); delay(500)
}
if (!engineActive.get()) break
snapQueue.collect {
while (engineActive.get()) {
val item = synchronized(queuedSnaps) { if (queuedSnaps.isNotEmpty()) queuedSnaps.removeAt(0) else null } ?: break
updateStatusNotification()
if (!validateEnvironmentalConstraints()) {
synchronized(queuedSnaps) { queuedSnaps.remove(item) }
continue
}
while (isPaused.get() && engineActive.get()) {
currentStatusText = "Paused"; updateStatusNotification(); delay(500)
}
if (!engineActive.get()) break
val isSafe = (autoOpenConfig.safeProcessing as PropertyValue<Boolean>).get()
if (lastConversationId != null && lastConversationId != item.conversationId) {
delay(if (isSafe) (autoOpenConfig.delayBetweenConversations as PropertyValue<Int>).get().toLong() else 40L)
}
lastConversationId = item.conversationId
processSnapItem(item)
lastSnapProcessedAt.set(System.currentTimeMillis())
// Process at natural network speed when safety is disabled
val baseDelay = if (currentSpeedText == "Throttled") 3000L else (autoOpenConfig.delayBetweenSnaps as PropertyValue<Int>).get().toLong()
if (isSafe) {
delay(Random.nextLong(baseDelay, baseDelay + 200))
} else {
if (baseDelay > 0) delay(baseDelay)
}
if (synchronized(queuedSnaps) { queuedSnaps.isEmpty() }) {
currentStatusText = "Monitoring..."
updateStatusNotification()
saveQueueToDisk() // Batch complete save
startWakeLockCooldown()
if (!validateEnvironmentalConstraints()) {
synchronized(queuedSnaps) { queuedSnaps.add(0, item) }
continue
}
val isSafe = (autoOpenConfig.safeProcessing as PropertyValue<Boolean>).get()
if (lastConversationId != null && lastConversationId != item.conversationId) {
delay(if (isSafe) (autoOpenConfig.delayBetweenConversations as PropertyValue<Int>).get().toLong() else 40L)
}
lastConversationId = item.conversationId
processSnapItem(item)
lastSnapProcessedAt.set(System.currentTimeMillis())
// Process at natural network speed when safety is disabled
val baseDelay = if (currentSpeedText == "Throttled") 3000L else (autoOpenConfig.delayBetweenSnaps as PropertyValue<Int>).get().toLong()
if (isSafe) {
delay(Random.nextLong(baseDelay, baseDelay + 200))
} else {
if (baseDelay > 0) delay(baseDelay)
}
if (synchronized(queuedSnaps) { queuedSnaps.isEmpty() }) {
currentStatusText = "Monitoring..."
updateStatusNotification()
saveQueueToDisk() // Batch complete save
startWakeLockCooldown()
}
}
}
}
@@ -194,7 +211,6 @@ class AutoOpenSnaps: MessagingRuleFeature("Auto Open Snaps", MessagingRuleType.A
// Verify database state on background thread before processing
val dbMessage = withContext(Dispatchers.IO) { this@AutoOpenSnaps.context.database.getConversationMessageFromId(item.messageId) }
if (dbMessage?.isViewedByUser == 1) {
synchronized(queuedSnaps) { queuedSnaps.remove(item) }
return
}
@@ -203,17 +219,16 @@ class AutoOpenSnaps: MessagingRuleFeature("Auto Open Snaps", MessagingRuleType.A
val startTime = System.currentTimeMillis()
for (i in 0 until (autoOpenConfig.retryAttempts as PropertyValue<Int>).get()) {
if (isPaused.get() || !engineActive.get() || autoOpenConfig.globalState == false) break
if (messaging.conversationManager == null) {
if (messaging.conversationManager == null) {
runCatching { this@AutoOpenSnaps.context.messagingBridge.triggerSessionStart() }
delay(1000)
delay(1000)
}
success = performOpen(item)
if (success) {
synchronized(queuedSnaps) { queuedSnaps.remove(item) }
sessionProcessed.incrementAndGet(); totalProcessed.incrementAndGet(); recordSpeedTimestamp()
// Industrial Interval Check: Only write to disk once every 10 minutes during floods
if (System.currentTimeMillis() - lastSaveTime.get() > 600000) {
saveQueueToDisk()
@@ -228,7 +243,8 @@ class AutoOpenSnaps: MessagingRuleFeature("Auto Open Snaps", MessagingRuleType.A
}
if (!success && !isPaused.get() && engineActive.get()) {
logError("Engine failed to open Snap: ${item.messageId}")
synchronized(queuedSnaps) { queuedSnaps.remove(item) }
synchronized(openedSnapsIds) { openedSnapsIds.remove(item.messageId) }
synchronized(deadLetterQueue) { if (deadLetterQueue.size < 100) deadLetterQueue.add(item) else { deadLetterQueue.removeAt(0); deadLetterQueue.add(item) } }
currentStatusText = "Failed: ${item.senderName}"; updateStatusNotification()
}
}
@@ -239,7 +255,7 @@ class AutoOpenSnaps: MessagingRuleFeature("Auto Open Snaps", MessagingRuleType.A
suspendCancellableCoroutine { cont ->
runCatching {
manager.updateMessage(item.conversationId, item.messageId, MessageUpdate.READ) { result ->
if (result == null || result == "DUPLICATEREQUEST") { cont.resume(true) }
if (result == null || result == "DUPLICATEREQUEST") { cont.resume(true) }
else if (item.serverMessageId != 0L) {
manager.updateMessage(item.conversationId, item.serverMessageId, MessageUpdate.READ) { serverResult ->
cont.resume(serverResult == null || serverResult == "DUPLICATEREQUEST")
@@ -258,17 +274,17 @@ class AutoOpenSnaps: MessagingRuleFeature("Auto Open Snaps", MessagingRuleType.A
val isIdle = isDeviceIdle()
val onlyIdle = (autoOpenConfig.onlyWhenIdle as PropertyValue<Boolean>).get()
val inSleepWindow = if (onlyIdle) isInsideSleepWindow() else false
val wifiStop = (autoOpenConfig.onlyOnWifi as PropertyValue<Boolean>).get() && !isWifi
val idleStop = onlyIdle && !isIdle && !inSleepWindow
when {
wifiStop -> { currentStatusText = "Waiting for WiFi..."; delay(5000) }
idleStop -> { currentStatusText = "Waiting for Idle..."; delay(5000) }
else -> {
else -> {
val thermalActive = (autoOpenConfig.thermalProtection as PropertyValue<Boolean>).get() && isThermalThrottled
currentSpeedText = if (inSleepWindow || thermalActive) "Throttled" else "Full Speed"
return true
return true
}
}
updateStatusNotification()
@@ -281,47 +297,58 @@ class AutoOpenSnaps: MessagingRuleFeature("Auto Open Snaps", MessagingRuleType.A
if (autoOpenConfig.globalState == false || !engineActive.get()) return@subscribe
val message = event.message
if (message.messageState != MessageState.COMMITTED || message.senderId?.toString() == this@AutoOpenSnaps.context.database.myUserId) return@subscribe
val clientMessageId = message.messageDescriptor?.messageId ?: return@subscribe
val conversationId = message.messageDescriptor?.conversationId?.toString() ?: return@subscribe
val serverMessageId = message.orderKey ?: 0L
val contentType = message.messageContent?.contentType
if (contentType != ContentType.SNAP && contentType != ContentType.EXTERNAL_MEDIA) return@subscribe
if (!canUseRule(conversationId)) return@subscribe
// Prevent re-queueing the same message while it is currently being processed
if (openedSnapsIds.contains(clientMessageId)) return@subscribe
openedSnapsIds.add(clientMessageId)
val senderId = message.senderId?.toString() ?: "unknown"
val item = SnapQueueItem(conversationId, clientMessageId, serverMessageId, senderId, getSenderDisplayName(senderId), getConversationType(conversationId, senderId), getSnapContentType(contentType))
synchronized(queuedSnaps) { queuedSnaps.add(item) }
snapChannel.trySend(item)
acquireWakeLock(); updateStatusNotification(); saveQueueToDisk()
snapQueue.tryEmit(System.currentTimeMillis())
acquireWakeLock(); updateStatusNotification()
}
}
private fun saveQueueToDisk() {
prefs.edit { putInt(PREF_TOTAL_OPENED, totalProcessed.get()); putLong(PREF_SESSION_START, sessionStartTime.get()) }
prefs.edit {
putInt(PREF_TOTAL_OPENED, totalProcessed.get())
putLong(PREF_SESSION_START, sessionStartTime.get())
synchronized(queuedSnaps) { putString(PREF_SAVED_QUEUE, gson.toJson(queuedSnaps)) }
}
}
private fun restorePersistence() {
val savedStartTime = prefs.getLong(PREF_SESSION_START, 0)
if (System.currentTimeMillis() - savedStartTime > 21600000) return
val now = System.currentTimeMillis()
if (now - savedStartTime > 21600000) return
totalProcessed.set(prefs.getInt(PREF_TOTAL_OPENED, 0)); sessionStartTime.set(savedStartTime)
val savedQueueJson = prefs.getString(PREF_SAVED_QUEUE, null)
if (!savedQueueJson.isNullOrBlank()) {
runCatching {
val restored: List<SnapQueueItem> = gson.fromJson(savedQueueJson, object : TypeToken<List<SnapQueueItem>>() {}.type)
synchronized(queuedSnaps) { queuedSnaps.addAll(restored.filter { (now - it.timestamp) < 3600000 }) }
}
}
}
private fun isWifiConnected(): Boolean {
val cm = this@AutoOpenSnaps.context.androidContext.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
// Check for any available network with a WiFi or Ethernet transport
return cm.allNetworks.any { network ->
cm.getNetworkCapabilities(network)?.let { caps ->
caps.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) ||
caps.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) ||
caps.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET)
} == true
}
@@ -360,7 +387,7 @@ class AutoOpenSnaps: MessagingRuleFeature("Auto Open Snaps", MessagingRuleType.A
if (!isScreenOn.get() && !force) return
if (!force && (now - lastNotificationUpdate) < notificationUpdateDelay) {
if (pendingNotificationUpdate.compareAndSet(false, true)) {
this@AutoOpenSnaps.context.coroutineScope.launch { delay(notificationUpdateDelay - (now - lastNotificationUpdate)); updateStatusNotificationInternal() }
this@AutoOpenSnaps.context.coroutineScope.launch { delay(notificationUpdateDelay - (now - lastNotificationUpdate)); updateStatusNotificationInternal() }
}
return
}
@@ -372,7 +399,7 @@ class AutoOpenSnaps: MessagingRuleFeature("Auto Open Snaps", MessagingRuleType.A
val processed = sessionProcessed.get()
val total = totalProcessed.get()
val remaining = synchronized(queuedSnaps) { queuedSnaps.size }
// Industrial State Hashing: Prevent redundant redraws and CPU wakeups
val currentStateHash = Objects.hash(processed, total, remaining, currentStatusText, isPaused.get())
if (currentStateHash == lastNotificationStateHash && remaining == 0) return
@@ -380,24 +407,19 @@ class AutoOpenSnaps: MessagingRuleFeature("Auto Open Snaps", MessagingRuleType.A
val isWorking = remaining > 0
val speed = if (isWorking) getSnapsPerSecond() else 0.0
lastNotificationUpdate = System.currentTimeMillis(); pendingNotificationUpdate.set(false)
val sessionTotal = processed + remaining
val progressPercent = if (sessionTotal > 0) (processed * 100) / sessionTotal else 0
val eta = if (isWorking && !isPaused.get()) formatDuration(remaining * averageProcessingTime.get()) else "..."
val builder = Notification.Builder(this@AutoOpenSnaps.context.androidContext, "auto_open_status")
.setOngoing(isWorking).setOnlyAlertOnce(true).setGroup(NOTIFICATION_GROUP_KEY)
val iconRes = when {
isPaused.get() -> android.R.drawable.ic_media_pause
!isWorking -> android.R.drawable.ic_popup_sync
else -> android.R.drawable.ic_media_play
}
builder.setSmallIcon(iconRes)
builder.setSmallIcon(if (isPaused.get()) android.R.drawable.ic_media_pause else if (!isWorking) android.R.drawable.ic_popup_sync else android.R.drawable.ic_media_play)
builder.setContentTitle("Auto-Open: $currentStatusText")
val isCompact = (autoOpenConfig.compactNotification as PropertyValue<Boolean>).get()
if (isWorking) {
builder.setContentText("Opened: $processed │ Queue: $remaining ($progressPercent%)")
@@ -471,28 +493,28 @@ class AutoOpenSnaps: MessagingRuleFeature("Auto Open Snaps", MessagingRuleType.A
}
}
}
val filter = IntentFilter().apply {
val filter = IntentFilter().apply {
addAction(ACTION_PAUSE_RESUME)
addAction(ACTION_CLEAR_QUEUE)
addAction(ACTION_STOP_ENGINE)
addAction(Intent.ACTION_SCREEN_ON)
addAction(Intent.ACTION_SCREEN_OFF)
addAction(Intent.ACTION_BATTERY_CHANGED)
addAction(Intent.ACTION_BATTERY_CHANGED)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) this@AutoOpenSnaps.context.androidContext.registerReceiver(actionReceiver, filter, Context.RECEIVER_NOT_EXPORTED)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) this@AutoOpenSnaps.context.androidContext.registerReceiver(actionReceiver, filter, Context.RECEIVER_NOT_EXPORTED)
else this@AutoOpenSnaps.context.androidContext.registerReceiver(actionReceiver, filter)
}
private fun recordSpeedTimestamp() { synchronized(snapTimestamps) { snapTimestamps.addLast(System.currentTimeMillis()); if (snapTimestamps.size > 250) snapTimestamps.removeFirst() } }
private fun shutdownFeature() {
private fun shutdownFeature() {
engineActive.set(false)
snapChannel.close()
engineJob?.cancel()
releaseWakeLock()
cancelStatusNotification()
cancelStatusNotification()
saveQueueToDisk()
}
private fun cancelStatusNotification() = notificationManager.cancel(STATUS_NOTIFICATION_ID)
fun getInterface(): AutoOpenInterface {
@@ -505,7 +527,7 @@ class AutoOpenSnaps: MessagingRuleFeature("Auto Open Snaps", MessagingRuleType.A
private fun getSenderDisplayName(userId: String): String = this@AutoOpenSnaps.context.database.getFriendInfo(userId)?.displayName ?: "Unknown"
private fun getConversationType(convId: String, senderId: String): String = if (this@AutoOpenSnaps.context.database.getDMOtherParticipant(convId) != null) "Friend DM" else this@AutoOpenSnaps.context.database.getFeedEntryByConversationId(convId)?.feedDisplayName ?: "Group Chat"
private fun getSnapContentType(type: ContentType?): String = when (type) { ContentType.SNAP -> "Photo/Video"; ContentType.EXTERNAL_MEDIA -> "Media"; else -> "Message" }
private fun getSnapContentType(type: ContentType?): String = when (type) { ContentType.SNAP -> "Photo/Video"; ContentType.EXTERNAL_MEDIA -> "Media"; else -> "Message" }
}
data class SnapQueueItem(val conversationId: String, val messageId: Long, val serverMessageId: Long, val senderId: String, val senderName: String, val conversationType: String, val contentType: String)
data class SnapQueueItem(val conversationId: String, val messageId: Long, val serverMessageId: Long, val senderId: String, val senderName: String, val conversationType: String, val contentType: String, val timestamp: Long = System.currentTimeMillis())

View File

@@ -144,8 +144,24 @@ class DeviceSpooferHook : Feature("Device Spoofer") {
}
private fun getRandomizedProfile(): RandomizedDeviceProfile {
val generationToken = context.config.experimental.spoof.randomizeDeviceProfile.profileGenerationToken.getNullable()
return randomizedProfile ?: RandomizedDeviceProfileStore
if (randomizedProfile != null) return randomizedProfile!!
val spoofConfig = context.config.experimental.spoof.randomizeDeviceProfile
val configProfileJson = spoofConfig.profileData.getNullable()
if (!configProfileJson.isNullOrBlank()) {
runCatching {
val profile = RandomizedDeviceProfile.fromJson(configProfileJson)
randomizedProfile = profile
context.log.verbose("Using restored randomized device profile from config")
return profile
}.onFailure {
context.log.warn("Failed to parse restored device profile from config, generating fresh one: ${it.message}")
}
}
val generationToken = spoofConfig.profileGenerationToken.getNullable()
return RandomizedDeviceProfileStore
.getOrCreate(context.androidContext, context.log, generationToken)
.also { profile ->
randomizedProfile = profile
@@ -155,18 +171,23 @@ class DeviceSpooferHook : Feature("Device Spoofer") {
private fun persistRandomizedProfileSnapshot(profile: RandomizedDeviceProfile) {
val spoofConfig = context.config.experimental.spoof.randomizeDeviceProfile
val profileJson = profile.toJson().toString()
val snapshot = profile.toJson().toString(2)
if (spoofConfig.currentProfileSnapshot.getNullable() == snapshot) return
if (spoofConfig.currentProfileSnapshot.getNullable() == snapshot && spoofConfig.profileData.getNullable() == profileJson) return
spoofConfig.currentProfileSnapshot.set(snapshot)
spoofConfig.profileData.set(profileJson) // Synchronize raw profile data for multi-process persistence
runCatching {
val field = context.javaClass.getDeclaredField("_config\$delegate")
field.isAccessible = true
val lazyConfig = field.get(context) as Lazy<*>
val modConfig = lazyConfig.value as? ModConfig ?: return@runCatching
modConfig.writeConfig(dispatchConfigListener = false)
context.log.verbose("Persisted randomized device profile snapshot to config")
context.log.verbose("Persisted randomized device profile data to config")
}.onFailure {
context.log.warn("Failed to persist randomized device profile snapshot: ${it.message}")
context.log.warn("Failed to persist randomized device profile: ${it.message}")
}
}

View File

@@ -103,7 +103,7 @@ class SendOverride : Feature("Send Override") {
}
private fun handleQueuedOriginalItemRepeatSuccess(): Boolean {
if (queuedOriginalItemRepeatCount <= 0) {
if (isStopped.get() || queuedOriginalItemRepeatCount <= 0) {
clearQueuedOriginalItemRepeats()
return false
}
@@ -152,13 +152,13 @@ class SendOverride : Feature("Send Override") {
}
private fun updateContinuousSendNotification() {
if (!engineActive.get() || isStopped.get()) return
if (!engineActive.get()) return
val notificationManager = context.androidContext.getSystemService(NotificationManager::class.java)
val remaining = queuedOriginalItemRepeatCount
val processed = processedRepeatCount
val total = totalRepeatCount
val isWorking = (remaining > 0 || (total > 0 && processed < total)) && !isStopped.get()
val isWorking = remaining > 0 && !isStopped.get() && engineActive.get()
if (!isWorking) {
notificationManager.cancel(STATUS_NOTIFICATION_ID)
@@ -186,13 +186,18 @@ class SendOverride : Feature("Send Override") {
}
private fun showCompletionNotification(sent: Int, total: Int) {
val title = if (isStopped.get()) "Continuous Send Stopped" else "Continuous Send Finished"
val content = "Successfully sent $sent / $total snaps to $currentRecipientName"
val isError = sent < total && !isStopped.get()
val title = when {
isStopped.get() -> "Continuous Send Stopped"
isError -> "Continuous Send Failed"
else -> "Continuous Send Finished"
}
val content = "Sent $sent / $total snaps to $currentRecipientName"
val notificationManager = context.androidContext.getSystemService(NotificationManager::class.java)
val builder = Notification.Builder(context.androidContext, CONTINUOUS_SEND_CHANNEL_ID)
.setSmallIcon(android.R.drawable.checkbox_on_background) // Emerald Green Checkmark
.setColor(0xFF2ECC71.toInt()) // Emerald Green tint
.setSmallIcon(if (isError) android.R.drawable.stat_notify_error else android.R.drawable.checkbox_on_background)
.setColor(if (isError) 0xFFE74C3C.toInt() else 0xFF2ECC71.toInt())
.setContentTitle(title)
.setContentText(content)
.setAutoCancel(true)
@@ -928,6 +933,11 @@ class SendOverride : Feature("Send Override") {
if (repeatCount <= 0) return false
fun sendIteration(index: Int) {
if (isStopped.get()) {
clearQueuedOriginalItemRepeats()
updateContinuousSendNotification()
return
}
val callback = if (index == repeatCount - 1) {
originalCallback
} else {