5 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
7 changed files with 159 additions and 39 deletions

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,12 @@
## 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!

View File

@@ -1696,6 +1696,8 @@
"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."

View File

@@ -182,6 +182,7 @@ class MessagingTweaks : ConfigContainer() {
// 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

@@ -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

@@ -22,9 +22,11 @@ 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
@@ -95,6 +97,14 @@ class AutoOpenSnaps: MessagingRuleFeature("Auto Open Snaps", MessagingRuleType.A
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,
val messageId: Long,
@@ -155,8 +165,18 @@ class AutoOpenSnaps: MessagingRuleFeature("Auto Open Snaps", MessagingRuleType.A
updateStatusNotification()
}
ACTION_CLEAR_QUEUE -> {
synchronized(queuedSnaps) { queuedSnaps.clear() }
synchronized(deadLetterQueue) { deadLetterQueue.clear() }
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()
}
@@ -167,7 +187,14 @@ class AutoOpenSnaps: MessagingRuleFeature("Auto Open Snaps", MessagingRuleType.A
override fun init() {
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()
@@ -418,32 +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()
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()
}
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) {
@@ -490,10 +568,19 @@ class AutoOpenSnaps: MessagingRuleFeature("Auto Open Snaps", MessagingRuleType.A
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
@@ -509,19 +596,21 @@ class AutoOpenSnaps: MessagingRuleFeature("Auto Open Snaps", MessagingRuleType.A
val eta = if (isWorking && !isCurrentlyWaiting && !isPaused.get()) formatDuration(remaining * averageProcessingTime.get()) else null
// Refined Collapsed Logic
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 {
// Idle stats for collapsed view
// Static status for monitoring stage to save battery
builder.setContentText("$processed Opened Today │ $total Lifetime")
builder.setSubText("Monitoring Snaps...")
// 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.setProgress(if (isWorking) sessionTotal else 0, if (isWorking) processed else 0, !isWorking)
builder.addAction(Notification.Action.Builder(null, if (isPaused.get()) "Resume" else "Pause", createPendingIntent(ACTION_PAUSE_RESUME)).build())
builder.addAction(Notification.Action.Builder(null, "Clear Queue", createPendingIntent(ACTION_CLEAR_QUEUE)).build())
@@ -530,6 +619,9 @@ class AutoOpenSnaps: MessagingRuleFeature("Auto Open Snaps", MessagingRuleType.A
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 {
append("QUEUE STATISTICS\n")
append("├─ Opened: $processed snaps\n")
@@ -548,7 +640,6 @@ class AutoOpenSnaps: MessagingRuleFeature("Auto Open Snaps", MessagingRuleType.A
}
}
bigTextStyle.bigText(detailText)
bigTextStyle.setSummaryText(null)
builder.setStyle(bigTextStyle)
}
@@ -556,7 +647,7 @@ class AutoOpenSnaps: MessagingRuleFeature("Auto Open Snaps", MessagingRuleType.A
}
private fun shutdownFeature() {
notificationManager.cancel(STATUS_NOTIFICATION_ID)
cancelStatusNotification()
val finalCount = totalProcessed.get()
if (hasBeenActive.get()) {
val elapsedMillis = System.currentTimeMillis() - sessionStartTime.get() - totalPausedDuration.get()
@@ -567,7 +658,8 @@ class AutoOpenSnaps: MessagingRuleFeature("Auto Open Snaps", MessagingRuleType.A
.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)
}
triggerLazySave()
@@ -677,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

@@ -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