Unlimited Local pins bug fix
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user