Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f01b6c2f9a | ||
|
|
98f8d6ebea | ||
|
|
015226cb40 | ||
|
|
ad06b0dffb | ||
|
|
4b4d64e8eb |
@@ -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.
|
||||
|
||||
@@ -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!
|
||||
|
||||
@@ -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."
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 })
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user