Spoof Backup bug fixes
This commit is contained in:
@@ -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.")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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}")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user