Unlimited Local pins bug fix

This commit is contained in:
DarkKnight2122
2026-04-20 16:47:58 +05:30
parent a20496329c
commit 335cc00dfb
2 changed files with 87 additions and 23 deletions

View File

@@ -1,6 +1,5 @@
package me.eternal.purrfectsnap.core.features.impl.experiments
import android.app.ActivityManager
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
@@ -15,7 +14,6 @@ import android.os.Build
import android.os.PowerManager
import androidx.core.content.edit
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.sync.Mutex
@@ -42,7 +40,7 @@ import kotlin.random.Random
/**
* AutoOpenSnaps: High-performance engine with real-time diagnostics.
* Optimized for 20+ snaps/s with accurate stats and background resilience.
* Optimized for background resilience and industrial stability.
*/
class AutoOpenSnaps: MessagingRuleFeature("Auto Open Snaps", MessagingRuleType.AUTO_OPEN_SNAPS) {
companion object {
@@ -99,7 +97,6 @@ class AutoOpenSnaps: MessagingRuleFeature("Auto Open Snaps", MessagingRuleType.A
val now = System.currentTimeMillis(); val window = 5000L
synchronized(snapTimestamps) {
snapTimestamps.removeIf { now - it > window }
// Smoother calculation for high-frequency bursts
return if (snapTimestamps.isEmpty()) 0.0 else (snapTimestamps.size.toDouble() / (window / 1000.0))
}
}
@@ -113,7 +110,6 @@ class AutoOpenSnaps: MessagingRuleFeature("Auto Open Snaps", MessagingRuleType.A
restorePersistence()
createNotificationChannels()
// NATIVE HOOKS: Ensuring Snapchat never sees the app as "In Background"
if ((autoOpenConfig.allowRunningInBackground as PropertyValue<Boolean>).get()) {
runCatching {
findClass("com.snapchat.client.duplex.DuplexClient\$CppProxy").apply {
@@ -121,7 +117,6 @@ class AutoOpenSnaps: MessagingRuleFeature("Auto Open Snaps", MessagingRuleType.A
val state = param.arg<Any>(0).toString()
if (state == "INACTIVE" || state == "BACKGROUND") param.setResult(null)
}
// INDUSTRIAL FIX: Restoring the universal v1.6.8 background wake-up hook
hookConstructor(HookStage.AFTER) { param ->
methods.firstOrNull { it.name == "appStateChanged" }?.let { method ->
val enumClass = method.parameterTypes[0]
@@ -158,7 +153,6 @@ class AutoOpenSnaps: MessagingRuleFeature("Auto Open Snaps", MessagingRuleType.A
continue
}
// SPEED OPTIMIZATION: Instant switch (40ms) when stealth is off
val isSafe = (autoOpenConfig.safeProcessing as PropertyValue<Boolean>).get()
if (lastConversationId != null && lastConversationId != item.conversationId) {
delay(if (isSafe) (autoOpenConfig.delayBetweenConversations as PropertyValue<Int>).get().toLong() else 40L)
@@ -198,7 +192,6 @@ class AutoOpenSnaps: MessagingRuleFeature("Auto Open Snaps", MessagingRuleType.A
success = withContext(Dispatchers.IO) { performOpen(item) }
if (success) {
// IMPORTANT: Item only removed after successful processing to ensure Stats sync
synchronized(queuedSnaps) { queuedSnaps.remove(item) }
sessionProcessed.incrementAndGet(); totalProcessed.incrementAndGet(); recordSpeedTimestamp()
val duration = System.currentTimeMillis() - startTime
@@ -305,7 +298,14 @@ class AutoOpenSnaps: MessagingRuleFeature("Auto Open Snaps", MessagingRuleType.A
private fun isWifiConnected(): Boolean {
val cm = this@AutoOpenSnaps.context.androidContext.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
return cm.getNetworkCapabilities(cm.activeNetwork)?.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) == true
// RESILIENT WIFI CHECK: Iterates through all networks to find ANY WiFi transport (VPN aware)
return cm.allNetworks.any { network ->
cm.getNetworkCapabilities(network)?.let { caps ->
caps.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) ||
caps.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET)
} == true
}
}
private fun isDeviceIdle(): Boolean = (this@AutoOpenSnaps.context.androidContext.getSystemService(Context.POWER_SERVICE) as PowerManager).isDeviceIdleMode
@@ -355,7 +355,6 @@ class AutoOpenSnaps: MessagingRuleFeature("Auto Open Snaps", MessagingRuleType.A
val builder = Notification.Builder(this@AutoOpenSnaps.context.androidContext, "auto_open_status")
.setOngoing(isWorking).setOnlyAlertOnce(true).setGroup(NOTIFICATION_GROUP_KEY)
// ICON LOGIC: Pause, Monitoring (Sync), or Active (Play)
val iconRes = when {
isPaused.get() -> android.R.drawable.ic_media_pause
!isWorking -> android.R.drawable.ic_popup_sync

View File

@@ -7,47 +7,112 @@ import me.eternal.purrfectsnap.core.util.hook.HookStage
import me.eternal.purrfectsnap.core.util.hook.Hooker
import me.eternal.purrfectsnap.core.util.hook.hook
import me.eternal.purrfectsnap.core.util.hook.hookConstructor
import me.eternal.purrfectsnap.core.util.ktx.getObjectField
import me.eternal.purrfectsnap.core.util.ktx.getObjectFieldOrNull
import me.eternal.purrfectsnap.core.util.ktx.setObjectField
import me.eternal.purrfectsnap.core.wrapper.impl.SnapUUID
import me.eternal.purrfectsnap.mapper.impl.CallbackMapper
import java.util.Collections
class PinConversations : MessagingRuleFeature("PinConversations", MessagingRuleType.PIN_CONVERSATION) {
companion object {
// 3-year offset for persistent local conversation sorting
private const val PIN_OFFSET = 100000000000L
}
private fun forcePinsInFeed(entries: ArrayList<Any>) {
val now = System.currentTimeMillis()
// Capture stable timestamp once to prevent jitter during the sweep
val stableTimestamp = now + PIN_OFFSET
entries.forEach { entry ->
val conversationIdObject = entry.getObjectFieldOrNull("mConversationId") ?: return@forEach
runCatching {
val conversationUUID = SnapUUID(conversationIdObject)
if (getState(conversationUUID.toString())) {
// Apply identical timestamp lead to all pinned items
entry.setObjectField("mPinnedTimestampMs", stableTimestamp)
} else {
// Reset timestamp if it's currently a "Future" timestamp but shouldn't be pinned
val currentTs = entry.getObjectFieldOrNull("mPinnedTimestampMs") as? Long ?: 0L
if (currentTs > now + (PIN_OFFSET / 2)) {
entry.setObjectField("mPinnedTimestampMs", now)
}
}
}
}
// Manual sort to ensure stable UI transition and prevent list jumping
runCatching {
Collections.sort(entries) { a, b ->
val tsA = a.getObjectFieldOrNull("mPinnedTimestampMs") as? Long ?: 0L
val tsB = b.getObjectFieldOrNull("mPinnedTimestampMs") as? Long ?: 0L
tsB.compareTo(tsA)
}
}
}
override fun init() {
if (!context.config.messaging.unlimitedConversationPinning.get()) return
// Intercept native pinning requests and bypass server-side limits
context.classCache.feedManager.hook("setPinnedConversationStatus", HookStage.BEFORE) { param ->
val conversationUUID = SnapUUID(param.arg(0))
val isPinned = param.arg<Any>(1).toString() == "PINNED"
setState(conversationUUID.toString(), isPinned)
// Callback forcing to suppress "Can't pin conversation" errors for both PIN and UNPIN
val callback = param.arg<Any>(2)
mutableSetOf<() -> Unit>().apply {
addAll(Hooker.ephemeralHookObjectMethod(callback::class.java, callback,"onSuccess", HookStage.BEFORE) {
addAll(Hooker.ephemeralHookObjectMethod(callback::class.java, callback, "onSuccess", HookStage.BEFORE) {
forEach { it() }
})
addAll(Hooker.ephemeralHookObjectMethod(callback::class.java, callback,"onError", HookStage.BEFORE) { methodParam ->
addAll(Hooker.ephemeralHookObjectMethod(callback::class.java, callback, "onError", HookStage.BEFORE) { methodParam ->
methodParam.setResult(null)
// Manually trigger success to bypass server-side limit rejections
callback::class.java.getDeclaredMethod("onSuccess").invoke(callback)
})
}
}
context.classCache.conversation.hookConstructor(HookStage.AFTER) { param ->
val instance = param.thisObject<Any>()
val conversationUUID = SnapUUID(instance.getObjectField("mConversationId"))
if (getState(conversationUUID.toString())) {
instance.setObjectField("mPinnedTimestampMs", 1L)
// Active feed sweep to ensure pinned conversations remain at the top
context.mappings.useMapper(CallbackMapper::class) {
val callbackMap = callbacks.getAsMap().orEmpty()
callbackMap.entries.forEach { (_, className) ->
val clazz = runCatching { findClass(className!!) }.getOrNull() ?: return@forEach
clazz.methods.forEach { method ->
if (method.name.startsWith("on") && method.name.endsWith("Complete") && method.parameterTypes.any { it == ArrayList::class.java }) {
clazz.hook(method.name, HookStage.BEFORE) { param ->
(param.args().firstOrNull { it is ArrayList<*> } as? ArrayList<Any>)?.let { forcePinsInFeed(it) }
}
}
}
}
}
// Apply pinning lead to newly created conversation objects
context.classCache.conversation.hookConstructor(HookStage.AFTER) { param ->
val instance = param.thisObject<Any>()
val conversationIdObject = instance.getObjectFieldOrNull("mConversationId") ?: return@hookConstructor
runCatching {
val conversationUUID = SnapUUID(conversationIdObject)
if (getState(conversationUUID.toString())) {
instance.setObjectField("mPinnedTimestampMs", System.currentTimeMillis() + PIN_OFFSET)
}
}
}
// Apply pinning lead to newly created feed entry objects
context.classCache.feedEntry.hookConstructor(HookStage.AFTER) { param ->
val instance = param.thisObject<Any>()
val conversationUUID = SnapUUID(instance.getObjectField("mConversationId") ?: return@hookConstructor)
val isPinned = getState(conversationUUID.toString())
if (isPinned) {
instance.setObjectField("mPinnedTimestampMs", 1L)
val conversationIdObject = instance.getObjectFieldOrNull("mConversationId") ?: return@hookConstructor
runCatching {
val conversationUUID = SnapUUID(conversationIdObject)
if (getState(conversationUUID.toString())) {
instance.setObjectField("mPinnedTimestampMs", System.currentTimeMillis() + PIN_OFFSET)
}
}
}
}
override fun getRuleState() = RuleState.WHITELIST
}
}