10 Commits

Author SHA1 Message Date
ΞTΞRNAL
f01b6c2f9a add fresh changelogs
Updated changelog for version 1.6.6 with multiple fixes and new features, including pre-fetch snaps toggle and disk optimization improvements.
2026-04-02 20:48:00 +05:30
ΞTΞRNAL
98f8d6ebea fix(PR): auto open notification fix by Kaladin
fix: auto open notification fix
2026-04-02 20:39:32 +05:30
DarkKnight2122
015226cb40 fix: auto open notification fix 2026-04-02 19:43:59 +05:30
ΞTΞRNAL
ad06b0dffb v1.6.6 2026-04-02 19:00:08 +05:30
ΞTΞRNAL
4b4d64e8eb fix version bump 2026-04-02 14:18:43 +05:30
ΞTΞRNAL
3eec22c615 fix(PR): Restore Auto Open stability by Kaladin
Restore Auto Open stability
2026-04-02 14:01:08 +05:30
ΞTΞRNAL
9a2e6065dc Merge branch 'dev' of https://github.com/particle-box/PurrfectSnap into dev 2026-04-02 13:59:43 +05:30
ΞTΞRNAL
8e00c29a59 v1.6.5 2026-04-02 13:57:36 +05:30
DarkKnight2122
9ec9517ec5 fix(core): Restore Auto Open stability 2026-04-02 06:45:13 +05:30
ΞTΞRNAL
e822fc20b4 New announcement!
Added recommendation for using Performance Mode feature in Snapchat.
2026-04-02 02:41:12 +05:30
13 changed files with 640 additions and 207 deletions

View File

@@ -1 +1 @@
- Test
- All users are recommended to use the new Performance Mode feature! Go to the features tab and then select global and select performance mode and set it to Max. Then force stop and reopen Snapchat and you will feel the difference i.e. Snapchat will feel a lot faster.

View File

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

View File

@@ -33,8 +33,8 @@ tasks.register<GetVersionTask>("getVersion") {
}
// You can still set these for legacy use by submodules or scripts:
rootProject.ext.set("appVersionName", providers.gradleProperty("APP_VERSION_NAME").orElse("1.6.4").get())
rootProject.ext.set("appVersionCode", providers.gradleProperty("APP_VERSION_CODE").orElse("318").get().toInt())
rootProject.ext.set("appVersionName", providers.gradleProperty("APP_VERSION_NAME").orElse("1.6.6").get())
rootProject.ext.set("appVersionCode", providers.gradleProperty("APP_VERSION_CODE").orElse("322").get().toInt())
rootProject.ext.set("applicationId", "me.eternal.purrfectsnap")
// buildHash: when PurrfectSnap or Snapchat is updated, mappings become outdated and auto-regenerate.
// Include version code so each release has a different hash; use random for uniqueness within same version.

View File

@@ -1,3 +1,22 @@
## v1.6.6
- Fix: Persistent auto open snap disabled notification(tq to Kaladin)
- Fix: Crash issues for some devices
- Fix: upload quality for snaps sent through send override
- Fix: Refactored the notification card for Auto Open to remove the looping progress bar while the engine is in Monitoring stage(tq to Kaladin)
- New: Implemented a "Pre Fetch snaps" toggle, when enabled this will pre-fetch the snaps in the auto open queue and load them for the engine to open them efficiently and thus reducing the "Failed to Open..." status(tq to Kaladin)
- Fix: The Auto Open engine now only fires the status update if the status text, processed count, or queue structural state actually changes. This reduces system wake-ups during idle periods which helps in battery drain management(tq to Kaladin)
- Fix: Disk Optimization: Previous versions wrote the auto open queue to the disk every 25 snaps, this update modified this to a 3-minute windowed save. This reduces Disk I/O by ~75%, keeping the device from overheating excessively during Auto Open processes(tq to Kaladin)
## v1.6.5
- New: Performance mode will now be set to max by default!
- Fix: Several optimizations to the performance mode feature which will make your snapchat experience more smooth!
- Fix: Crash issues for some devices after enabling spoof
- Fix: Notification Icon for the Announcements(tq to Kaladin)
- Fix: Auto Open Engine not processing, stuck on monitor/retry/failed loop causing overheating of the devices(tq to Kaladin)
- Fix: Refactor the Notification card for the Auto Open to remove dynamic progress bar to add the native progress bar(tq to Kaladin)
- Fix: Auto Open Statistics have been refactored to now show the dynamic queue status(tq to Kaladin)
- New: Auto Open Thermal Protection/Throttle toggle, when turned on the processing will be doubled down to reduce the temps of the device to maintain a study temp of 40 C and below(tq to Kaladin)
## v1.6.4
- New: Performance mode feature!(Smooth & Max)
- Fix: Failed to init feature Device Spoofer for some devices

View File

@@ -1648,6 +1648,7 @@
"status_monitoring": "Monitoring",
"status_active": "Active",
"status_paused": "Paused",
"thermal_status_title": "Thermal Cooling (Throttled)",
"processed_count": "Opened",
"queue_size": "Queue",
"action_reset": "Reset Statistics",
@@ -1682,10 +1683,6 @@
"name": "Auto Open Compact Notification",
"description": "Use a smaller, single-line notification for status updates"
},
"show_progress_bar": {
"name": "Show Progress Bar",
"description": "Display a visual progress bar in the status notification"
},
"show_lifetime_stats": {
"name": "Show Lifetime Statistics",
"description": "Include the total number of snaps opened since installation in the notification"
@@ -1694,7 +1691,13 @@
"name": "Show Queue Preview",
"description": "Show a list of the most recent snaps waiting in the queue (Expanded only)"
},
"thermal_protection": {
"name": "Thermal Protection",
"description": "Automatically throttles the engine and increases delays if the device temperature exceeds 40°C to prevent overheating"
},
"only_on_wifi": { "name": "Auto Open only on Wi-Fi", "description": "Only process queue when connected to a Wi-Fi network to save mobile data" },
"pre_fetch_snaps": { "name": "Pre-fetch Snaps", "description": "Fetches snap media into Snapchat's internal cache once the snap arrives. Note: Turning it on increases background data usage." },
"content_type_snap": "Snap",
"only_when_idle": {
"name": "Auto Open Schedule",
"description": "Configure a specific time window where the engine will throttle its speed."
@@ -3028,7 +3031,8 @@
"NOTE": "Audio Note",
"SNAP": "Snap",
"SAVEABLE_SNAP": "Saveable Snap",
"null": "Snapchat Default"
"null": "Snapchat Default",
"multiple_media_toast": "You can only send one media at a time"
},
"strip_media_metadata": {
"hide_caption_text": "Hide Caption Text",
@@ -3763,6 +3767,7 @@
"paused_message": "Processing paused. Queue preserved ({count} snaps)",
"status_paused": "Paused",
"status_monitoring": "Monitoring",
"thermal_status_title": "Thermal Cooling (Throttled)",
"status_active": "Active",
"status_failed": "Failed to open {sender}",
"status_retrying": "Retrying in background...",
@@ -3881,7 +3886,6 @@
"material3_strings": {
"date_range_picker_start_headline": "From",
"date_range_picker_end_headline": "To",
"date_range_picker_title": "Select date range",
"date_picker_switch_to_calendar_mode": "Calendar",
"date_picker_switch_to_input_mode": "Input",
"date_range_picker_scroll_to_previous_month": "Previous month",
@@ -4365,10 +4369,3 @@
"tasks_remove_selected_tasks_confirm": "Remove {count} selected tasks?",
"tasks_remove_all_tasks_confirm": "This will stop all running tasks and clear the history."
}

View File

@@ -47,7 +47,9 @@ class Global : ConfigContainer() {
val betterLocation = container("better_location", BetterLocationConfig())
val snapchatPlus = unique("snapchat_plus", "not_subscribed", "basic", "ad_free") { requireRestart() }
val mediaUploadQualityConfig = container("media_upload_quality", MediaUploadQualityConfig())
val performanceMode = container("performance_mode", PerformanceModeConfig()) { requireRestart() }
val performanceMode = container("performance_mode", PerformanceModeConfig()) { requireRestart() }.apply {
profile.set("max")
}
val disableConfirmationDialogs = multiple("disable_confirmation_dialogs", "erase_message", "remove_friend", "block_friend", "ignore_friend", "hide_friend", "hide_conversation", "clear_conversation") { requireRestart() }
val disableMetrics = boolean("disable_metrics") { requireRestart() }
val disableStorySections = multiple("disable_story_sections", "friends", "suggested_stories", "following", "discover") { requireRestart(); requireCleanCache() }

View File

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

View File

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

View File

@@ -65,11 +65,11 @@ class ConfigurationOverride : Feature("Configuration Override") {
overrideProperty("STREAK_EXPIRATION_INFO", { context.config.userInterface.streakExpirationInfo.get() },
{ true })
overrideProperty("TRANSCODING_MAX_QUALITY", { context.config.global.mediaUploadQualityConfig.forceVideoUploadSourceQuality.get() },
overrideProperty("TRANSCODING_MAX_QUALITY", { context.config.global.mediaUploadQualityConfig.forceVideoUploadSourceQuality.get() || context.config.messaging.galleryMediaSendOverride.mode.getNullable() != null },
{ true }, isAppExperiment = true)
run {
val isForceQuality = { _: ConfigKeyInfo -> context.config.global.mediaUploadQualityConfig.forceVideoUploadSourceQuality.get() }
val isForceQuality = { _: ConfigKeyInfo -> context.config.global.mediaUploadQualityConfig.forceVideoUploadSourceQuality.get() || context.config.messaging.galleryMediaSendOverride.mode.getNullable() != null }
val level7Value = { _: ConfigKeyInfo -> 700 }
arrayOf(
"MY_STORY_UPLOAD_QUALITY_LEVEL",
@@ -93,10 +93,14 @@ class ConfigurationOverride : Feature("Configuration Override") {
isForceQuality, { true })
overrideProperty("MEDIA_QUALITY_LEVEL_DOWNGRADING_PERCENTAGE",
isForceQuality, { 0.0f })
overrideProperty("CHAT_MEDIA_IMPORT_TRANSCODED_QUALITY",
isForceQuality, { 4 })
overrideProperty("POSTED_STORY_IMPORT_TRANSCODED_QUALITY",
isForceQuality, { 4 })
}
run {
val isDisableCompression = { _: ConfigKeyInfo -> context.config.global.mediaUploadQualityConfig.disableImageCompression.get() }
val isDisableCompression = { _: ConfigKeyInfo -> context.config.global.mediaUploadQualityConfig.disableImageCompression.get() || context.config.messaging.galleryMediaSendOverride.mode.getNullable() != null }
overrideProperty("LIBJPEG_IMAGE_ENCODING_QUALITY", isDisableCompression, { 100 })
overrideProperty("LIBJPEG_IMAGE_ENCODING_QUALITY_V2", isDisableCompression, { 100 })
}

View File

@@ -1,5 +1,6 @@
package me.eternal.purrfectsnap.core.features.impl.experiments
import android.app.ActivityManager
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
@@ -9,39 +10,43 @@ import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.content.SharedPreferences
import android.os.Build
import android.os.PowerManager
import android.app.ActivityManager
import android.net.ConnectivityManager
import android.net.NetworkCapabilities
import android.os.Build
import android.os.PowerManager
import androidx.core.content.edit
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.MutableSharedFlow
import me.eternal.purrfectsnap.bridge.AutoOpenInterface
import me.eternal.purrfectsnap.common.BuildConfig
import me.eternal.purrfectsnap.common.data.ContentType
import me.eternal.purrfectsnap.common.data.MessageState
import me.eternal.purrfectsnap.common.data.MessageUpdate
import me.eternal.purrfectsnap.common.data.MessagingRuleType
import me.eternal.purrfectsnap.core.event.events.impl.BuildMessageEvent
import me.eternal.purrfectsnap.core.wrapper.impl.Message
import me.eternal.purrfectsnap.core.features.MessagingRuleFeature
import me.eternal.purrfectsnap.core.features.impl.messaging.Messaging
import me.eternal.purrfectsnap.core.features.impl.tweaks.PerformanceMode
import me.eternal.purrfectsnap.core.util.hook.HookStage
import me.eternal.purrfectsnap.core.util.hook.hook
import me.eternal.purrfectsnap.core.util.hook.hookConstructor
import java.util.concurrent.atomic.AtomicInteger
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicLong
import java.util.*
import java.util.concurrent.ConcurrentHashMap
import java.util.Calendar
import kotlin.random.Random
import me.eternal.purrfectsnap.bridge.AutoOpenInterface
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicInteger
import java.util.concurrent.atomic.AtomicLong
import kotlin.coroutines.resume
import kotlin.random.Random
class AutoOpenSnaps: MessagingRuleFeature("Auto Open Snaps", MessagingRuleType.AUTO_OPEN_SNAPS) {
companion object {
const val ACTION_PAUSE_RESUME = "me.eternal.purrfectsnap.AUTO_OPEN_SNAPS_PAUSE_RESUME"
const val ACTION_CLEAR_QUEUE = "me.eternal.purrfectsnap.AUTO_OPEN_SNAPS_CLEAR_QUEUE"
private const val STATUS_NOTIFICATION_ID = 54321
private const val NOTIFICATION_GROUP_KEY = "purrfectsnap.AUTO_OPEN"
private const val PREF_TOTAL_OPENED = "auto_open_total_opened"
private const val PREF_TOTAL_DETECTED = "auto_open_total_detected"
private const val PREF_SESSION_START = "auto_open_session_start"
@@ -62,6 +67,7 @@ class AutoOpenSnaps: MessagingRuleFeature("Auto Open Snaps", MessagingRuleType.A
private val snapQueue = MutableSharedFlow<Long>(extraBufferCapacity = 100)
private val openedSnaps = ConcurrentHashMap.newKeySet<Long>()
private val queuedSnaps = mutableListOf<SnapQueueItem>()
private val deadLetterQueue = mutableListOf<SnapQueueItem>()
private val nameCache = ConcurrentHashMap<String, String>()
private val conversationTypeCache = ConcurrentHashMap<String, String>()
@@ -76,6 +82,28 @@ class AutoOpenSnaps: MessagingRuleFeature("Auto Open Snaps", MessagingRuleType.A
private var currentSpeedText = "Full Speed"
private var isCurrentlyWaiting = false
private var wakeLock: PowerManager.WakeLock? = null
private var lastQueueActivity = System.currentTimeMillis()
// Throttling & Performance fields
private val lastNotificationUpdate = AtomicLong(0)
private val notificationUpdateDelay = 1000L
private val pendingNotificationUpdate = AtomicBoolean(false)
private val processedSinceLastSave = AtomicInteger(0)
private val snapTimestamps = LinkedList<Long>()
// Safety & Synergy
private val isSaving = AtomicBoolean(false)
private val needsSaving = AtomicBoolean(false)
private var isThermalThrottled = false
private var lastThermalThrottleAt = 0L
private fun cancelStatusNotification() {
runCatching {
notificationManager.cancel(STATUS_NOTIFICATION_ID)
}.onFailure {
context.log.warn("Failed to cancel Auto Open Snaps notification: ${it.message}")
}
}
data class SnapQueueItem(
val conversationId: String,
@@ -84,7 +112,8 @@ class AutoOpenSnaps: MessagingRuleFeature("Auto Open Snaps", MessagingRuleType.A
var senderName: String = "Pending...",
var conversationType: String = "Processing",
val contentType: String,
val timestamp: Long = System.currentTimeMillis()
val timestamp: Long = System.currentTimeMillis(),
var retryCount: Int = 0
)
private val autoOpenInterface = object : AutoOpenInterface.Stub() {
@@ -104,6 +133,7 @@ class AutoOpenSnaps: MessagingRuleFeature("Auto Open Snaps", MessagingRuleType.A
lastPausedAt.set(0)
sessionStartTime.set(System.currentTimeMillis())
synchronized(queuedSnaps) { queuedSnaps.clear() }
synchronized(deadLetterQueue) { deadLetterQueue.clear() }
openedSnaps.clear()
prefs.edit()
@@ -127,32 +157,47 @@ class AutoOpenSnaps: MessagingRuleFeature("Auto Open Snaps", MessagingRuleType.A
if (paused) {
lastPausedAt.set(System.currentTimeMillis())
} else {
val pauseStarted = lastPausedAt.get()
if (pauseStarted > 0) {
totalPausedDuration.addAndGet(System.currentTimeMillis() - pauseStarted)
lastPausedAt.set(0)
if (lastPausedAt.get() > 0) {
totalPausedDuration.addAndGet(System.currentTimeMillis() - lastPausedAt.get())
}
snapQueue.tryEmit(System.currentTimeMillis())
}
updateStatusNotification()
}
ACTION_CLEAR_QUEUE -> {
clearInternalState()
this@AutoOpenSnaps.context.log.info("[AutoOpen] All statistics and queue reset.")
synchronized(queuedSnaps) {
queuedSnaps.clear()
}
synchronized(deadLetterQueue) {
deadLetterQueue.clear()
}
// Reset session and persistent counters to zero
totalProcessed.set(0)
sessionProcessed.set(0)
totalDetected.set(0)
triggerLazySave()
updateStatusNotification()
}
}
}
}
override fun init() {
if (config.globalState != true) return
context.log.info("[AutoOpen] Initializing Ultra Premium engine...")
val messaging = context.feature(Messaging::class)
restorePersistence()
hasBeenActive.set(true)
// Verify configuration state before marking as active to prevent background process notification spam
if (config.globalState == true) {
hasBeenActive.set(true)
} else {
// Feature is disabled; silent exit to avoid process-wide 'Deactivated' notices
return
}
if (config.allowRunningInBackground.get()) {
acquireWakeLock()
findClass("com.snapchat.client.duplex.DuplexClient\$CppProxy").apply {
hook("appStateChanged", HookStage.BEFORE) { param ->
if (config.allowRunningInBackground.get()) {
@@ -163,8 +208,14 @@ class AutoOpenSnaps: MessagingRuleFeature("Auto Open Snaps", MessagingRuleType.A
}
}
hookConstructor(HookStage.AFTER) { param ->
methods.first { it.name == "appStateChanged" }.let { method ->
method.invoke(param.thisObject(), method.parameterTypes[0].enumConstants!!.first { it.toString() == "ACTIVE" })
methods.firstOrNull { it.name == "appStateChanged" }?.let { method ->
val enumClass = method.parameterTypes[0]
val activeState = enumClass.enumConstants?.firstOrNull {
it.toString() == "ACTIVE" || it.toString() == "FOREGROUND"
}
if (activeState != null) {
method.invoke(param.thisObject<Any>(), activeState)
}
}
}
}
@@ -182,11 +233,35 @@ class AutoOpenSnaps: MessagingRuleFeature("Auto Open Snaps", MessagingRuleType.A
val filter = IntentFilter().apply {
addAction(ACTION_PAUSE_RESUME)
addAction(ACTION_CLEAR_QUEUE)
addAction(Intent.ACTION_BATTERY_CHANGED)
}
val batteryReceiver = object : BroadcastReceiver() {
override fun onReceive(ctx: Context?, intent: Intent?) {
if (intent?.action == Intent.ACTION_BATTERY_CHANGED && config.thermalProtection.get()) {
val temp = intent.getIntExtra("temperature", 0) / 10f
if (temp >= 40f && !isThermalThrottled) {
isThermalThrottled = true
lastThermalThrottleAt = System.currentTimeMillis()
context.log.warn("[THERMAL] Device hit ${temp}C. Throttling AutoOpen.")
} else if (isThermalThrottled && temp <= 36f && System.currentTimeMillis() - lastThermalThrottleAt > 600000) {
isThermalThrottled = false
context.log.info("[THERMAL] Device cooled to ${temp}C. Resuming full speed.")
}
}
}
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
context.androidContext.registerReceiver(actionReceiver, filter, Context.RECEIVER_NOT_EXPORTED)
context.androidContext.registerReceiver(batteryReceiver, filter, Context.RECEIVER_NOT_EXPORTED)
} else {
context.androidContext.registerReceiver(actionReceiver, filter)
context.androidContext.registerReceiver(batteryReceiver, filter)
}
if (synchronized(queuedSnaps) { queuedSnaps.isNotEmpty() }) {
snapQueue.tryEmit(System.currentTimeMillis())
}
context.coroutineScope.launch(Dispatchers.Default) {
@@ -198,9 +273,31 @@ class AutoOpenSnaps: MessagingRuleFeature("Auto Open Snaps", MessagingRuleType.A
val remainingCount = synchronized(queuedSnaps) { queuedSnaps.size }
if (remainingCount == 0 && sessionProcessed.get() > 0) {
sessionProcessed.set(0)
triggerLazySave()
}
if (remainingCount > 0) {
lastQueueActivity = System.currentTimeMillis()
acquireWakeLock()
if (!isPaused.get()) snapQueue.tryEmit(System.currentTimeMillis())
} else {
// IDLE REVIVAL: Check dead letter queue every 5 mins when idle
if (!isPaused.get() && System.currentTimeMillis() - lastQueueActivity > 300000) {
val revived = synchronized(deadLetterQueue) {
if (deadLetterQueue.isNotEmpty()) deadLetterQueue.removeAt(0) else null
}
if (revived != null) {
synchronized(queuedSnaps) { queuedSnaps.add(revived) }
snapQueue.tryEmit(System.currentTimeMillis())
}
}
if (System.currentTimeMillis() - lastQueueActivity > 600000) { // 10 mins true idle
releaseWakeLock()
}
}
updateStatusNotification()
if (remainingCount > 0) snapQueue.tryEmit(System.currentTimeMillis())
delay(5000)
}
}
@@ -208,15 +305,14 @@ class AutoOpenSnaps: MessagingRuleFeature("Auto Open Snaps", MessagingRuleType.A
context.coroutineScope.launch(Dispatchers.Default, CoroutineStart.UNDISPATCHED) {
snapQueue.collect { _ ->
while (isActive && config.globalState == true) {
val item = synchronized(queuedSnaps) { queuedSnaps.firstOrNull() } ?: break
while (isPaused.get() || config.globalState != true) {
if (config.globalState != true) return@collect
currentStatusText = context.translation["auto_open_snaps.status_paused"] ?: "Paused"
isCurrentlyWaiting = true
updateStatusNotification()
delay(2000)
if (isPaused.get()) {
delay(1000)
continue
}
val item = synchronized(queuedSnaps) {
if (queuedSnaps.isNotEmpty()) queuedSnaps.removeAt(0) else null
} ?: break
var resourceWaiting = true
while (resourceWaiting) {
@@ -229,56 +325,57 @@ class AutoOpenSnaps: MessagingRuleFeature("Auto Open Snaps", MessagingRuleType.A
when {
config.onlyOnWifi.get() && !isWifi -> {
currentStatusText = context.translation["auto_open_snaps.only_on_wifi.name"] ?: "Waiting for WiFi..."
currentSpeedText = context.translation["auto_open_snaps.paused_status"] ?: "Paused (No WiFi)"
currentSpeedText = "Throttled"
isCurrentlyWaiting = true
delay(5000)
}
config.onlyWhenIdle.get() && !isIdle && !inSleepWindow -> {
currentStatusText = context.translation["auto_open_snaps.only_when_idle.name"] ?: "Waiting for idle..."
currentSpeedText = context.translation["auto_open_snaps.paused_status"] ?: "Paused (Device Active)"
currentSpeedText = "Throttled"
isCurrentlyWaiting = true
delay(5000)
}
config.pauseDuringGaming.get() && isGaming -> {
currentStatusText = context.translation["auto_open_snaps.pause_during_gaming.name"] ?: "Paused (Gaming Mode)"
currentSpeedText = context.translation["auto_open_snaps.paused_status"] ?: "Paused (Gaming)"
currentSpeedText = "Paused"
isCurrentlyWaiting = true
delay(60000)
}
else -> {
resourceWaiting = false
currentSpeedText = if (inSleepWindow) context.translation["auto_open_snaps.speed_throttled"] ?: "Throttled" else context.translation["auto_open_snaps.processing_speed_full"] ?: "Full Speed"
currentSpeedText = if (inSleepWindow || isThermalThrottled) "Throttled" else "Full Speed"
}
}
if (resourceWaiting) updateStatusNotification()
}
if (isPaused.get() || config.globalState != true) continue
if (isPaused.get() || config.globalState != true) {
synchronized(queuedSnaps) { queuedSnaps.add(0, item) }
continue
}
isCurrentlyWaiting = false
val inSleepWindow = if (config.onlyWhenIdle.get()) isInsideSleepWindow() else false
if (inSleepWindow) {
currentStatusText = context.translation["auto_open_snaps.speed_throttled"] ?: "Throttled"
if (inSleepWindow || isThermalThrottled) {
currentStatusText = if (isThermalThrottled) context.translation["auto_open_snaps.thermal_status_title"] ?: "Thermal Cooling" else context.translation["auto_open_snaps.speed_throttled"] ?: "Throttled"
delay(Random.nextLong(3000, 5000))
} else if (lastConversationId != null && lastConversationId != item.conversationId) {
currentStatusText = "..."
currentStatusText = "Switching chats..."
delay(Random.nextLong(1500, 2500))
batchSnapCount = 0
triggerLazySave()
} else if (lastConversationId == item.conversationId) {
when {
isThermalThrottled -> delay(Random.nextLong(100, 150))
config.safeProcessing.get() -> delay(Random.nextLong(50, 150))
else -> delay(Random.nextLong(10, 40))
}
}
lastConversationId = item.conversationId
currentStatusText = if (inSleepWindow) context.translation["auto_open_snaps.status_active"] ?: "Active" else context.translation["auto_open_snaps.status_active"] ?: "Opening snap..."
currentStatusText = context.translation["auto_open_snaps.status_active"] ?: "Opening snap..."
updateStatusNotification()
if (config.safeProcessing.get() && !inSleepWindow) {
batchSnapCount++
if (batchSnapCount % 10 == 0) {
currentStatusText = "..."
updateStatusNotification()
delay(Random.nextLong(3000, 5000))
}
}
var success = false
val startTime = System.currentTimeMillis()
var currentRetryDelay = config.retryDelay.get().toLong()
@@ -287,7 +384,7 @@ class AutoOpenSnaps: MessagingRuleFeature("Auto Open Snaps", MessagingRuleType.A
if (isPaused.get() || config.globalState != true) break
while ((!config.allowRunningInBackground.get() && context.isMainActivityPaused) || (messaging.conversationManager == null && !config.allowRunningInBackground.get())) {
if (config.globalState != true || isPaused.get()) break
currentStatusText = "..."
currentStatusText = "Waiting for UI..."
isCurrentlyWaiting = true
updateStatusNotification()
delay(2000)
@@ -299,9 +396,19 @@ class AutoOpenSnaps: MessagingRuleFeature("Auto Open Snaps", MessagingRuleType.A
if (success) {
totalProcessed.incrementAndGet()
sessionProcessed.incrementAndGet()
synchronized(snapTimestamps) {
snapTimestamps.addLast(System.currentTimeMillis())
if (snapTimestamps.size > 100) snapTimestamps.removeFirst()
}
val duration = System.currentTimeMillis() - startTime
averageProcessingTime.set((averageProcessingTime.get() * 0.7 + duration * 0.3).toLong())
saveStatsToDisk()
val remaining = synchronized(queuedSnaps) { queuedSnaps.size }
val threshold = if (remaining > 100) 100 else 25
if (processedSinceLastSave.incrementAndGet() >= threshold) {
triggerLazySave()
processedSinceLastSave.set(0)
}
break
}
if (i < config.retryAttempts.get() - 1) {
@@ -311,20 +418,26 @@ class AutoOpenSnaps: MessagingRuleFeature("Auto Open Snaps", MessagingRuleType.A
}
}
if (success) {
synchronized(queuedSnaps) { queuedSnaps.removeAll { it.messageId == item.messageId }; saveQueueToDisk() }
} else if (!isPaused.get()) {
if (!success && !isPaused.get()) {
currentStatusText = context.translation["auto_open_snaps.status_failed"]?.replace("{sender}", item.senderName) ?: "Failed to open"
updateStatusNotification()
delay(5000)
synchronized(queuedSnaps) { queuedSnaps.removeAll { it.messageId == item.messageId }; saveQueueToDisk() }
// MOVE TO DEAD LETTER QUEUE (Revival Engine)
synchronized(deadLetterQueue) {
if (deadLetterQueue.size < 100) deadLetterQueue.add(item)
else { deadLetterQueue.removeAt(0); deadLetterQueue.add(item) }
}
delay(2000)
}
if (synchronized(queuedSnaps) { queuedSnaps.isEmpty() }) {
currentStatusText = context.translation["auto_open_snaps.status_monitoring"] ?: "Monitoring..."
isCurrentlyWaiting = false
updateStatusNotification()
releaseWakeLock()
delay(500)
if (synchronized(queuedSnaps) { queuedSnaps.isEmpty() }) {
currentStatusText = context.translation["auto_open_snaps.status_monitoring"] ?: "Monitoring..."
isCurrentlyWaiting = false
triggerLazySave()
updateStatusNotification()
}
}
}
}
@@ -332,29 +445,83 @@ class AutoOpenSnaps: MessagingRuleFeature("Auto Open Snaps", MessagingRuleType.A
context.event.subscribe(BuildMessageEvent::class, priority = 103) { event ->
if (config.globalState != true) return@subscribe
if (event.message.senderId?.toString() == context.database.myUserId) return@subscribe
val conversationId = event.message.messageDescriptor?.conversationId?.toString() ?: return@subscribe
val clientMessageId = event.message.messageDescriptor?.messageId ?: return@subscribe
val contentType = event.message.messageContent?.contentType
val message = event.message
// Stability: Only process committed messages to avoid ghost events during sending/failure
if (message.messageState != me.eternal.purrfectsnap.common.data.MessageState.COMMITTED) return@subscribe
if (message.senderId?.toString() == context.database.myUserId) return@subscribe
val conversationId = message.messageDescriptor?.conversationId?.toString() ?: return@subscribe
val clientMessageId = message.messageDescriptor?.messageId ?: return@subscribe
val contentType = message.messageContent?.contentType
// Validation: Only process viewable snaps and external media
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 (contentType == ContentType.SNAP_NOT_VIEWABLE) return@subscribe
if (message.messageMetadata?.openedBy?.any { it.toString() == context.database.myUserId } == true) return@subscribe
acquireWakeLock()
context.coroutineScope.launch(Dispatchers.Default) {
if (!canUseRule(conversationId)) return@launch
if (!openedSnaps.add(clientMessageId)) return@launch
if (openedSnaps.size > 15000) openedSnaps.clear()
val senderId = event.message.senderId?.toString() ?: "unknown"
val item = SnapQueueItem(conversationId, clientMessageId, senderId, getSenderDisplayName(senderId), getConversationType(conversationId, senderId), getSnapContentType(contentType))
synchronized(queuedSnaps) {
if (queuedSnaps.size >= config.queueSize.get()) queuedSnaps.removeFirstOrNull()
queuedSnaps.add(item); totalDetected.incrementAndGet(); saveQueueToDisk()
synchronized(openedSnaps) {
if (openedSnaps.contains(clientMessageId)) return@launch
openedSnaps.add(clientMessageId)
// Periodic cache maintenance to ensure O(1) performance
if (openedSnaps.size > 5000) openedSnaps.clear()
}
acquireWakeLock()
snapQueue.tryEmit(System.currentTimeMillis())
val senderId = message.senderId?.toString() ?: "unknown"
val item = SnapQueueItem(conversationId, clientMessageId, senderId, getSenderDisplayName(senderId), getConversationType(conversationId, senderId), getSnapContentType(contentType))
val currentQueueSize = synchronized(queuedSnaps) {
if (queuedSnaps.size >= config.queueSize.get()) queuedSnaps.removeFirstOrNull()
queuedSnaps.add(item)
queuedSnaps.size
}
totalDetected.incrementAndGet()
// Smart Pre-fetch Engine: Early media loading into internal cache
if (config.preFetchSnaps.get()) {
val isWifi = isWifiConnected()
val mobileLimit = 250
// Connection-Aware Limit: 1000 for WiFi, 250 for Mobile
val canFetch = if (isWifi) currentQueueSize <= 1000 else currentQueueSize <= mobileLimit
// Dynamic RAM Window (20/50/100) to prevent UI jitter on low-end devices
if (canFetch && currentQueueSize <= getFetchWindowSize()) {
runCatching {
// Stable feature access via explicit KClass resolution
context.feature(Messaging::class).conversationManager?.fetchMessage(conversationId, clientMessageId, {}, {})
}
}
}
if (!isPaused.get()) {
snapQueue.tryEmit(System.currentTimeMillis())
}
updateStatusNotification()
triggerLazySave()
}
}
}
private fun getFetchWindowSize(): Int {
val am = context.androidContext.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
val memInfo = ActivityManager.MemoryInfo()
am.getMemoryInfo(memInfo)
val totalRamGb = memInfo.totalMem / (1024 * 1024 * 1024)
return when {
totalRamGb <= 2 -> 20
totalRamGb <= 4 -> 50
else -> 100
}
}
private suspend fun performOpen(messaging: Messaging, item: SnapQueueItem): Boolean = withContext(Dispatchers.IO) {
val manager = messaging.conversationManager ?: return@withContext false
withTimeoutOrNull(5000) {
@@ -373,84 +540,114 @@ class AutoOpenSnaps: MessagingRuleFeature("Auto Open Snaps", MessagingRuleType.A
return when { h > 0 -> "${h}h ${m}m ${s}s"; m > 0 -> "${m}m ${s}s"; else -> "${s}s" }
}
private fun getSnapsPerSecond(): Double {
val now = System.currentTimeMillis()
val window = 5000L
synchronized(snapTimestamps) {
snapTimestamps.removeIf { now - it > window }
return (snapTimestamps.size.toDouble() / (window / 1000.0))
}
}
private fun updateStatusNotification() {
val currentTime = System.currentTimeMillis()
val lastUpdate = lastNotificationUpdate.get()
if ((currentTime - lastUpdate) < notificationUpdateDelay) {
if (pendingNotificationUpdate.compareAndSet(false, true)) {
context.coroutineScope.launch {
delay(notificationUpdateDelay - (currentTime - lastUpdate))
pendingNotificationUpdate.set(false)
updateStatusNotificationInternal()
}
}
return
}
lastNotificationUpdate.set(currentTime)
updateStatusNotificationInternal()
}
// Notification state cache to prevent redundant UI updates and save battery
private var lastNotificationState: String? = null
private fun updateStatusNotificationInternal() {
val processed = sessionProcessed.get()
val total = totalProcessed.get()
val remaining = synchronized(queuedSnaps) { queuedSnaps.size }
// Generate a state fingerprint to check if a notification update is actually necessary
val currentStateFingerprint = "$processed|$total|$remaining|$currentStatusText|$isPaused"
if (currentStateFingerprint == lastNotificationState && remaining == 0) return
lastNotificationState = currentStateFingerprint
if (total <= 0 && remaining <= 0 && processed <= 0) return
val isWorking = remaining > 0
val showProgressBar = config.showProgressBar.get() == true
val isCompact = config.compactNotification.get() == true
val sessionTotal = processed + remaining
val progressPercent = if (sessionTotal > 0) (processed * 100) / sessionTotal else 0
val speed = getSnapsPerSecond()
val builder = Notification.Builder(context.androidContext, "auto_open_snaps")
.setSmallIcon(if (isPaused.get()) android.R.drawable.ic_media_pause else android.R.drawable.ic_media_play)
.setOngoing(isWorking).setAutoCancel(!isWorking).setOnlyAlertOnce(true)
// Disable native progress bar to avoid "Double Bar" issue with our Premium Unicode bar
builder.setProgress(0, 0, false)
.setGroup(NOTIFICATION_GROUP_KEY).setGroupSummary(false)
val eta = if (isWorking && !isCurrentlyWaiting && !isPaused.get()) formatDuration(remaining * averageProcessingTime.get()) else null
if (config.compactNotification.get() == true) {
builder.setContentTitle("Auto-Open: $currentStatusText")
builder.setContentText("Remaining: $remaining | Opened: $processed" + (eta?.let { " | ETA: $it" } ?: ""))
builder.setContentTitle("Auto-Open: $currentStatusText")
if (isWorking) {
builder.setContentText("Opened: $processed │ ETA: ${eta ?: "..."}")
builder.setSubText("$progressPercent% • $remaining Queued")
// Show progress bar only when actively processing snaps
builder.setProgress(sessionTotal, processed, false)
} else {
builder.setContentTitle("Auto-Open: $currentStatusText")
builder.setContentText("Remaining: $remaining | Opened: $processed")
// Static status for monitoring stage to save battery
builder.setContentText("$processed Opened Today │ $total Lifetime")
// Remove subtext entirely when idle to prevent redundancy with the title
builder.setSubText(null)
// Remove progress bar entirely during idle/monitoring stage to stop animation CPU drain
builder.setProgress(0, 0, false)
}
builder.addAction(Notification.Action.Builder(null, if (isPaused.get()) "Resume" else "Pause", createPendingIntent(ACTION_PAUSE_RESUME)).build())
builder.addAction(Notification.Action.Builder(null, "Clear Queue", createPendingIntent(ACTION_CLEAR_QUEUE)).build())
if (config.compactNotification.get() != true) {
if (!isCompact) {
val recentSnaps = synchronized(queuedSnaps) { queuedSnaps.takeLast(5) }
val bigTextStyle = Notification.BigTextStyle()
// Set summary text to empty to force the header to stay clean in expanded view
bigTextStyle.setSummaryText("")
val detailText = buildString {
if (showProgressBar) append("${context.translation["auto_open_snaps.notification_statistics"] ?: "STATISTICS"} - ${drawProgressBar(progressPercent)}\n")
else append("${context.translation["auto_open_snaps.notification_statistics"] ?: "STATISTICS"}\n")
append("\u251c\u2500 ${context.translation["auto_open_snaps.processed_count"] ?: "Opened"}: $processed snaps\n")
append("\u251c\u2500 ${context.translation["auto_open_snaps.queue_size"] ?: "Remaining"}: $remaining snaps\n")
if (eta != null) {
append("\u251c\u2500 ${context.translation["auto_open_snaps.estimated_time"] ?: "Estimated time"}: $eta\n")
append("QUEUE STATISTICS\n")
append("├─ Opened: $processed snaps\n")
append("├─ Remaining: $remaining snaps\n")
if (eta != null) append("├─ Estimated time: $eta\n")
append("├─ Lifetime Opened: $total snaps\n")
append("└─ Speed: $currentSpeedText (${String.format("%.1f", speed)}/s)\n\n")
append("QUEUE PREVIEW\n")
if (isWorking) {
recentSnaps.reversed().forEach { item ->
append("${item.senderName}${item.conversationType} (${item.contentType})\n")
}
} else {
append(context.translation["auto_open_snaps.notification_no_snaps_queue"] ?: "Monitoring snaps in background...")
}
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)
bigTextStyle.bigText(detailText)
builder.setStyle(bigTextStyle)
}
notificationManager.notify(STATUS_NOTIFICATION_ID, builder.build())
}
private fun drawProgressBar(percent: Int): String {
val totalBlocks = 12; val filledBlocks = (percent * totalBlocks) / 100
return buildString {
append("[")
repeat(totalBlocks) { i ->
when { i < filledBlocks -> append("\u2501"); i == filledBlocks -> append("\u2B26"); else -> append("\u2500") }
}
append("] $percent%")
}
}
private fun shutdownFeature() {
notificationManager.cancel(STATUS_NOTIFICATION_ID)
cancelStatusNotification()
val finalCount = totalProcessed.get()
if (hasBeenActive.get()) {
val elapsedMillis = System.currentTimeMillis() - sessionStartTime.get() - totalPausedDuration.get()
@@ -459,19 +656,39 @@ class AutoOpenSnaps: MessagingRuleFeature("Auto Open Snaps", MessagingRuleType.A
.setSmallIcon(android.R.drawable.ic_dialog_info)
.setContentTitle("Auto-Open: Deactivated")
.setContentText("Opened: $finalCount snaps | Session: ${durationMins}m")
.setGroup(NOTIFICATION_GROUP_KEY)
.setAutoCancel(true).build()
notificationManager.notify(Random.nextInt(), summary)
// Use static ID to overwrite previous deactivate notice and prevent icon stacking
notificationManager.notify(STATUS_NOTIFICATION_ID + 1, summary)
hasBeenActive.set(false)
}
resetPersistence(); releaseWakeLock()
triggerLazySave()
releaseWakeLock()
}
private fun saveStatsToDisk() {
prefs.edit().putInt(PREF_TOTAL_OPENED, totalProcessed.get()).putInt(PREF_TOTAL_DETECTED, totalDetected.get()).putLong(PREF_SESSION_START, sessionStartTime.get()).apply()
private fun triggerLazySave() {
needsSaving.set(true)
if (isSaving.compareAndSet(false, true)) {
context.coroutineScope.launch(Dispatchers.IO) {
while (needsSaving.get()) {
needsSaving.set(false)
saveToDiskInternal()
delay(1000)
}
isSaving.set(false)
}
}
}
private fun saveQueueToDisk() {
synchronized(queuedSnaps) { prefs.edit().putString(PREF_SAVED_QUEUE, gson.toJson(queuedSnaps)).apply() }
private fun saveToDiskInternal() {
prefs.edit {
putInt(PREF_TOTAL_OPENED, totalProcessed.get())
putInt(PREF_TOTAL_DETECTED, totalDetected.get())
putLong(PREF_SESSION_START, sessionStartTime.get())
synchronized(queuedSnaps) {
putString(PREF_SAVED_QUEUE, gson.toJson(queuedSnaps))
}
}
}
private fun restorePersistence() {
@@ -483,8 +700,14 @@ class AutoOpenSnaps: MessagingRuleFeature("Auto Open Snaps", MessagingRuleType.A
try {
val type = object : TypeToken<List<SnapQueueItem>>() {}.type
val restored: List<SnapQueueItem> = gson.fromJson(savedQueueJson, type)
synchronized(queuedSnaps) { queuedSnaps.clear(); queuedSnaps.addAll(restored) }
} catch (e: Exception) { resetPersistence() }
val now = System.currentTimeMillis()
synchronized(queuedSnaps) {
queuedSnaps.clear()
queuedSnaps.addAll(restored.filter { (now - it.timestamp) < 3600000 })
}
} catch (e: Exception) {
prefs.edit().remove(PREF_SAVED_QUEUE).apply()
}
}
}
@@ -497,30 +720,14 @@ class AutoOpenSnaps: MessagingRuleFeature("Auto Open Snaps", MessagingRuleType.A
try {
val sleepWindow = config.sleepWindow.get()
if (!sleepWindow.contains("-") || !sleepWindow.contains(":")) return false
val window = sleepWindow.split("-")
if (window.size != 2) return false
val startStr = window[0].split(":")
val endStr = window[1].split(":")
if (startStr.size != 2 || endStr.size != 2) return false
val now = Calendar.getInstance().apply {
set(Calendar.SECOND, 0)
set(Calendar.MILLISECOND, 0)
}
val start = Calendar.getInstance().apply {
set(Calendar.HOUR_OF_DAY, startStr[0].toInt())
set(Calendar.MINUTE, startStr[1].toInt())
set(Calendar.SECOND, 0)
set(Calendar.MILLISECOND, 0)
}
val end = Calendar.getInstance().apply {
set(Calendar.HOUR_OF_DAY, endStr[0].toInt())
set(Calendar.MINUTE, endStr[1].toInt())
set(Calendar.SECOND, 0)
set(Calendar.MILLISECOND, 0)
}
val now = Calendar.getInstance().apply { set(Calendar.SECOND, 0); set(Calendar.MILLISECOND, 0) }
val start = Calendar.getInstance().apply { set(Calendar.HOUR_OF_DAY, startStr[0].toInt()); set(Calendar.MINUTE, startStr[1].toInt()); set(Calendar.SECOND, 0); set(Calendar.MILLISECOND, 0) }
val end = Calendar.getInstance().apply { set(Calendar.HOUR_OF_DAY, endStr[0].toInt()); set(Calendar.MINUTE, endStr[1].toInt()); set(Calendar.SECOND, 0); set(Calendar.MILLISECOND, 0) }
return if (end.before(start)) now.after(start) || now.before(end) else now.after(start) && now.before(end)
} catch (e: Exception) { return false }
}
@@ -562,24 +769,36 @@ class AutoOpenSnaps: MessagingRuleFeature("Auto Open Snaps", MessagingRuleType.A
}
private fun createNotificationChannels() {
val channel = NotificationChannel("auto_open_snaps", "Auto Open Snaps", NotificationManager.IMPORTANCE_LOW).apply {
enableVibration(false); setSound(null, null)
runCatching {
val channel = NotificationChannel("auto_open_snaps", "Auto Open Snaps", NotificationManager.IMPORTANCE_LOW).apply {
enableVibration(false); setSound(null, null)
}
notificationManager.createNotificationChannel(channel)
}.onFailure {
context.log.warn("Failed to create Auto Open Snaps notification channel: ${it.message}")
}
notificationManager.createNotificationChannel(channel)
}
private fun getSenderDisplayName(senderId: String): String = nameCache.getOrPut(senderId) {
context.database.getFriendInfo(senderId)?.let { it.displayName ?: it.mutableUsername } ?: "Unknown"
private fun getSenderDisplayName(senderId: String): String {
// Memory Safety: Prevent cache bloat during high-volume bursts
if (nameCache.size > 500) nameCache.clear()
return nameCache.getOrPut(senderId) {
context.database.getFriendInfo(senderId)?.let { it.displayName ?: it.mutableUsername } ?: "Unknown"
}
}
private fun getConversationType(conversationId: String, senderId: String): String = conversationTypeCache.getOrPut("$conversationId:$senderId") {
if (context.database.getDMOtherParticipant(conversationId) != null) "Friend DM"
else context.database.getFeedEntryByConversationId(conversationId)?.feedDisplayName ?: "Group Chat"
private fun getConversationType(conversationId: String, senderId: String): String {
// Memory Safety: Prevent cache bloat during high-volume bursts
if (conversationTypeCache.size > 500) conversationTypeCache.clear()
return conversationTypeCache.getOrPut("$conversationId:$senderId") {
if (context.database.getDMOtherParticipant(conversationId) != null) "Friend DM"
else context.database.getFeedEntryByConversationId(conversationId)?.feedDisplayName ?: "Group Chat"
}
}
private fun getSnapContentType(type: ContentType?): String = when (type) {
ContentType.SNAP -> "Photo/Video"
ContentType.EXTERNAL_MEDIA -> "Media"
else -> "Snap"
else -> context.translation["auto_open_snaps.content_type_snap"] ?: "Snap"
}
}

View File

@@ -357,6 +357,10 @@ class DeviceSpooferHook : Feature("Device Spoofer") {
supported32BitAbis: List<String>? = null,
supported64BitAbis: List<String>? = null
) {
val safeSupportedAbis = supportedAbis?.takeIf { it.isNotEmpty() } ?: (Build.SUPPORTED_ABIS?.toList() ?: emptyList())
val safeSupported32BitAbis = supported32BitAbis?.takeIf { it.isNotEmpty() } ?: (Build.SUPPORTED_32_BIT_ABIS?.toList() ?: emptyList())
val safeSupported64BitAbis = supported64BitAbis?.takeIf { it.isNotEmpty() } ?: (Build.SUPPORTED_64_BIT_ABIS?.toList() ?: emptyList())
Build::class.java.fields.forEach { field ->
if (!field.isAccessible) field.isAccessible = true
runCatching {
@@ -377,9 +381,9 @@ class DeviceSpooferHook : Feature("Device Spoofer") {
"DISPLAY" -> if (overrideDisplay) runCatching { field.set(null, display) }
"HOST" -> if (overrideHost) runCatching { field.set(null, host) }
"TIME" -> if (overrideBuildTime) runCatching { field.setLong(null, buildTime) }
"SUPPORTED_ABIS" -> if (overrideAbiLists) runCatching { field.set(null, supportedAbis?.toTypedArray()) }
"SUPPORTED_32_BIT_ABIS" -> if (overrideAbiLists) runCatching { field.set(null, supported32BitAbis?.toTypedArray()) }
"SUPPORTED_64_BIT_ABIS" -> if (overrideAbiLists) runCatching { field.set(null, supported64BitAbis?.toTypedArray()) }
"SUPPORTED_ABIS" -> if (overrideAbiLists) runCatching { field.set(null, safeSupportedAbis.toTypedArray()) }
"SUPPORTED_32_BIT_ABIS" -> if (overrideAbiLists) runCatching { field.set(null, safeSupported32BitAbis.toTypedArray()) }
"SUPPORTED_64_BIT_ABIS" -> if (overrideAbiLists) runCatching { field.set(null, safeSupported64BitAbis.toTypedArray()) }
}
}

View File

@@ -2,6 +2,9 @@ package me.eternal.purrfectsnap.core.features.impl.tweaks
import android.animation.ValueAnimator
import android.app.Activity
import android.app.Dialog
import android.database.Cursor
import android.database.MatrixCursor
import android.database.sqlite.SQLiteDatabase
import android.hardware.camera2.CaptureRequest
import android.media.MediaRecorder
@@ -9,24 +12,41 @@ import android.os.Build
import android.transition.Transition
import android.os.HandlerThread
import android.os.Process
import android.util.Base64
import android.util.Range
import android.view.View
import android.view.ViewPropertyAnimator
import android.view.WindowManager
import android.view.animation.Animation
import android.widget.OverScroller
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.StaggeredGridLayoutManager
import java.io.File
import java.lang.Thread
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.ThreadPoolExecutor
import me.eternal.purrfectsnap.core.features.Feature
import me.eternal.purrfectsnap.core.event.events.impl.NetworkApiRequestEvent
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 okhttp3.Dispatcher
class PerformanceMode : Feature("Performance Mode") {
private data class SnapshotCell(
val type: Int,
val stringValue: String? = null,
val longValue: Long? = null,
val doubleValue: Double? = null,
val blobValue: String? = null,
)
private data class CursorSnapshot(
val columns: List<String>,
val rows: List<List<SnapshotCell>>,
)
override fun init() {
val profile = context.config.global.performanceMode.profile.getNullable() ?: return
val isMaxProfile = profile == "max"
@@ -85,14 +105,114 @@ class PerformanceMode : Feature("Performance Mode") {
val sustainedModeLog = firstHitLogger("Window.setSustainedPerformanceMode")
val refreshRateLog = firstHitLogger("Activity.preferredRefreshRate")
val overScrollerLog = firstHitLogger("OverScroller.startScroll")
val chatFeedCacheServeLog = firstHitLogger("ChatFeed.cacheServe")
val chatFeedCacheRefreshLog = firstHitLogger("ChatFeed.cacheRefresh")
val mapDialogLog = firstHitLogger("Dialog.show")
val mapViewLog = firstHitLogger("MapView.constructor")
val mapboxNetworkBlockLog = firstHitLogger("SnapMap.telemetryBlock")
val mapCameraAnimLog = firstHitLogger("SnapMap.mapAnimatorDuration")
val mapThreadLog = firstHitLogger("SnapMap.mapThread")
val mapRendererFpsLog = firstHitLogger("SnapMap.mapRendererFps")
fun isPerformanceSensitiveThread(name: String?): Boolean {
val normalizedName = name?.lowercase() ?: return false
return listOf("camera", "preview", "codec", "render", "gl", "transcod", "lens", "feed", "story", "opera", "messag", "network", "db", "disk").any {
return listOf("camera", "preview", "codec", "render", "gl", "transcod", "lens", "feed", "story", "opera", "messag", "network", "db", "disk", "map", "mapbox", "snapmap", "viewport").any {
normalizedName.contains(it)
}
}
val performanceCacheDir = File(context.androidContext.filesDir, "performance_mode_cache").apply { mkdirs() }
val chatFeedSnapshotFile = File(performanceCacheDir, "chat_feed_snapshot.json")
fun isChatFeedQuery(sql: String): Boolean {
val normalized = sql.uppercase()
if (!normalized.startsWith("SELECT")) return false
val hitsFriendsFeedView = sql.contains("FriendsFeedView")
val hitsFeedEntry = sql.contains("feed_entry") && (sql.contains("last_updated_timestamp") || sql.contains("displayInteractionType") || sql.contains("streak_count"))
return (hitsFriendsFeedView || hitsFeedEntry) &&
!normalized.contains("COUNT(") &&
!normalized.contains("SELECT 0") &&
!normalized.contains("WHERE KEY = ?") &&
!normalized.contains("WHERE CLIENT_CONVERSATION_ID = ?")
}
fun cursorCell(cursor: Cursor, index: Int): SnapshotCell {
return when (cursor.getType(index)) {
Cursor.FIELD_TYPE_NULL -> SnapshotCell(Cursor.FIELD_TYPE_NULL)
Cursor.FIELD_TYPE_INTEGER -> SnapshotCell(Cursor.FIELD_TYPE_INTEGER, longValue = cursor.getLong(index))
Cursor.FIELD_TYPE_FLOAT -> SnapshotCell(Cursor.FIELD_TYPE_FLOAT, doubleValue = cursor.getDouble(index))
Cursor.FIELD_TYPE_STRING -> SnapshotCell(Cursor.FIELD_TYPE_STRING, stringValue = cursor.getString(index))
Cursor.FIELD_TYPE_BLOB -> SnapshotCell(
Cursor.FIELD_TYPE_BLOB,
blobValue = Base64.encodeToString(cursor.getBlob(index), Base64.NO_WRAP)
)
else -> SnapshotCell(Cursor.FIELD_TYPE_STRING, stringValue = cursor.getString(index))
}
}
fun snapshotFromCursor(cursor: Cursor): CursorSnapshot {
val columns = cursor.columnNames.toList()
val rows = mutableListOf<List<SnapshotCell>>()
if (cursor.moveToFirst()) {
do {
rows += columns.indices.map { index -> cursorCell(cursor, index) }
} while (cursor.moveToNext())
}
return CursorSnapshot(columns, rows)
}
fun snapshotToMatrixCursor(snapshot: CursorSnapshot): MatrixCursor {
return MatrixCursor(snapshot.columns.toTypedArray(), snapshot.rows.size).also { matrixCursor ->
snapshot.rows.forEach { row ->
matrixCursor.addRow(row.map { cell ->
when (cell.type) {
Cursor.FIELD_TYPE_NULL -> null
Cursor.FIELD_TYPE_INTEGER -> cell.longValue
Cursor.FIELD_TYPE_FLOAT -> cell.doubleValue
Cursor.FIELD_TYPE_BLOB -> cell.blobValue?.let { Base64.decode(it, Base64.NO_WRAP) }
else -> cell.stringValue
}
})
}
}
}
fun readSnapshot(file: File): CursorSnapshot? {
return runCatching {
if (!file.exists()) return null
context.gson.fromJson(file.readText(Charsets.UTF_8), CursorSnapshot::class.java)
}.getOrNull()
}
fun writeSnapshot(file: File, snapshot: CursorSnapshot) {
runCatching {
file.writeText(context.gson.toJson(snapshot), Charsets.UTF_8)
}.onFailure {
context.log.error("Failed to persist friend list snapshot", it, "PerformanceMode")
}
}
context.event.subscribe(NetworkApiRequestEvent::class) { event ->
if (!isMaxProfile) return@subscribe
val url = event.url
if (url.contains("ami/friends")) {
if (chatFeedSnapshotFile.exists()) {
chatFeedSnapshotFile.delete()
context.log.info("Invalidated chat feed snapshot after friends mutation sync", "PerformanceMode")
}
}
if (url.contains("messaging") || url.contains("conversation") || url.contains("feed")) {
if (chatFeedSnapshotFile.exists()) {
chatFeedSnapshotFile.delete()
context.log.info("Invalidated chat feed snapshot after messaging/feed network activity", "PerformanceMode")
}
}
if (url.contains("mapbox") && (url.contains("events.") || url.contains("telemetry"))) {
event.canceled = true
mapboxNetworkBlockLog("url=$url")
}
}
HandlerThread::class.java.hookConstructor(HookStage.BEFORE) { param ->
if (param.args().size < 2) return@hookConstructor
val threadName = param.argNullable<String>(0)
@@ -120,6 +240,9 @@ class PerformanceMode : Feature("Performance Mode") {
thread.priority = Thread.MAX_PRIORITY
}
threadStartLog("name=${thread.name} priority=${thread.priority}")
if ((thread.name ?: "").contains("map", ignoreCase = true) || (thread.name ?: "").contains("mapbox", ignoreCase = true)) {
mapThreadLog("name=${thread.name} priority=${thread.priority}")
}
}
ThreadPoolExecutor::class.java.hookConstructor(HookStage.AFTER) { param ->
@@ -156,6 +279,10 @@ class PerformanceMode : Feature("Performance Mode") {
param.setArg(0, updated)
}
animatorDurationLog("requested=$original applied=${param.arg<Long>(0)}")
val thisObject = param.nullableThisObject<Any>()?.javaClass?.name ?: ""
if (thisObject.contains("map", ignoreCase = true)) {
mapCameraAnimLog("owner=$thisObject requested=$original applied=${param.arg<Long>(0)}")
}
}
ViewPropertyAnimator::class.java.hook("setDuration", HookStage.BEFORE) { param ->
@@ -265,24 +392,17 @@ class PerformanceMode : Feature("Performance Mode") {
}
}
OverScroller::class.java.hook("fling", HookStage.BEFORE) { param ->
if (param.args().size >= 10) {
val overX = param.arg<Int>(8)
val overY = param.arg<Int>(9)
if (overX != 0) param.setArg(8, 0)
if (overY != 0) param.setArg(9, 0)
}
}
CaptureRequest.Builder::class.java.hook("set", HookStage.BEFORE) { param ->
val key = param.arg<CaptureRequest.Key<*>>(0)
when (key) {
CaptureRequest.EDGE_MODE -> param.setArg(1, CaptureRequest.EDGE_MODE_FAST)
CaptureRequest.NOISE_REDUCTION_MODE -> param.setArg(1, CaptureRequest.NOISE_REDUCTION_MODE_FAST)
CaptureRequest.HOT_PIXEL_MODE -> param.setArg(1, CaptureRequest.HOT_PIXEL_MODE_FAST)
CaptureRequest.COLOR_CORRECTION_ABERRATION_MODE -> param.setArg(1, CaptureRequest.COLOR_CORRECTION_ABERRATION_MODE_FAST)
CaptureRequest.CONTROL_AF_MODE -> param.setArg(1, CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE)
CaptureRequest.CONTROL_VIDEO_STABILIZATION_MODE -> param.setArg(1, CaptureRequest.CONTROL_VIDEO_STABILIZATION_MODE_OFF)
CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE -> {
val currentRange = param.argNullable<Any>(1) as? Range<*>
val lower = (currentRange?.lower as? Int) ?: minimumFrameRate
val upper = (currentRange?.upper as? Int) ?: minimumFrameRate
if (upper < minimumFrameRate) {
param.setArg(1, Range(lower.coerceAtMost(minimumFrameRate), minimumFrameRate))
}
}
}
captureRequestLog("key=${key.name} value=${param.argNullable<Any>(1)}")
}
@@ -308,5 +428,66 @@ class PerformanceMode : Feature("Performance Mode") {
onNextActivityCreate {
applyActivityPerformanceTuning(it)
}
Dialog::class.java.hook("show", HookStage.AFTER) { param ->
val dialog = param.nullableThisObject<Any>() as? Dialog ?: return@hook
val window = dialog.window ?: return@hook
runCatching {
window.setWindowAnimations(0)
window.decorView.setLayerType(View.LAYER_TYPE_HARDWARE, null)
window.attributes = window.attributes.apply {
flags = flags or WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED
}
if (dialog::class.java.name.contains("map", ignoreCase = true) || dialog::class.java.name.contains("snap", ignoreCase = true)) {
mapDialogLog("class=${dialog::class.java.name}")
}
}
}
runCatching {
findClass("com.mapbox.mapboxsdk.maps.MapView").hookConstructor(HookStage.AFTER) { param ->
val mapView = param.nullableThisObject<Any>() as? View ?: return@hookConstructor
mapView.setLayerType(View.LAYER_TYPE_HARDWARE, null)
mapView.overScrollMode = View.OVER_SCROLL_NEVER
mapViewLog("class=${mapView::class.java.name}")
}
}
runCatching {
findClass("com.mapbox.mapboxsdk.maps.renderer.MapRenderer").hook("setMaximumFps", HookStage.BEFORE) { param ->
val requested = param.arg<Int>(0)
val applied = requested.coerceAtLeast(120)
if (applied != requested) {
param.setArg(0, applied)
}
mapRendererFpsLog("requested=$requested applied=${param.arg<Int>(0)}")
}
}
runCatching {
findClass("io.requery.android.database.sqlite.SQLiteDatabase").hook("rawQueryWithFactory", HookStage.BEFORE) { param ->
if (!isMaxProfile) return@hook
val sql = param.argNullable<String>(1) ?: return@hook
if (!isChatFeedQuery(sql)) return@hook
readSnapshot(chatFeedSnapshotFile)?.let { snapshot ->
param.setResult(snapshotToMatrixCursor(snapshot))
chatFeedCacheServeLog("rows=${snapshot.rows.size} file=${chatFeedSnapshotFile.name}")
}
}
findClass("io.requery.android.database.sqlite.SQLiteDatabase").hook("rawQueryWithFactory", HookStage.AFTER) { param ->
if (!isMaxProfile) return@hook
val sql = param.argNullable<String>(1) ?: return@hook
if (!isChatFeedQuery(sql)) return@hook
val cursor = param.getResult() as? Cursor ?: return@hook
val snapshot = snapshotFromCursor(cursor)
writeSnapshot(chatFeedSnapshotFile, snapshot)
param.setResult(snapshotToMatrixCursor(snapshot))
runCatching { cursor.close() }
chatFeedCacheRefreshLog("rows=${snapshot.rows.size} file=${chatFeedSnapshotFile.name}")
}
}.onFailure {
context.log.error("Failed to install chat feed cache hooks", it, "PerformanceMode")
}
}
}

View File

@@ -7,8 +7,8 @@ org.gradle.configuration-cache=true
org.gradle.configuration-cache.problems=warn
nativeAbis=arm64-v8a
APP_VERSION_NAME=1.6.4
APP_VERSION_CODE=318
APP_VERSION_NAME=1.6.6
APP_VERSION_CODE=322
debug_build_hash=18fe2a814d0e2eb5
psIntegrityPinnedSha256=
EXPECTED_CERT_SHA256=0f188cb17d8ea4902ee15ce98f4928ba4226a4790fa3c52f62d776b08e46ca1c