This commit is contained in:
ΞTΞRNAL
2026-04-05 15:35:39 +05:30
parent f01b6c2f9a
commit e97a37d18e
13 changed files with 603 additions and 128 deletions

View File

@@ -466,22 +466,26 @@ private class DialogWrapper(
this.onDismissRequest = onDismissRequest this.onDismissRequest = onDismissRequest
this.properties = properties this.properties = properties
setLayoutDirection(layoutDirection) setLayoutDirection(layoutDirection)
if (properties.usePlatformDefaultWidth && !dialogLayout.usePlatformDefaultWidth) { val dialogWindow = window
val canUpdateWindowLayout = dialogWindow?.decorView?.let { decorView ->
isShowing && decorView.isAttachedToWindow && decorView.windowToken != null
} == true
if (canUpdateWindowLayout && properties.usePlatformDefaultWidth && !dialogLayout.usePlatformDefaultWidth) {
// Undo fixed size in internalOnLayout, which would suppress size changes when // Undo fixed size in internalOnLayout, which would suppress size changes when
// usePlatformDefaultWidth is true. // usePlatformDefaultWidth is true.
window?.setLayout( dialogWindow.setLayout(
WindowManager.LayoutParams.WRAP_CONTENT, WindowManager.LayoutParams.WRAP_CONTENT,
WindowManager.LayoutParams.WRAP_CONTENT WindowManager.LayoutParams.WRAP_CONTENT
) )
} }
dialogLayout.usePlatformDefaultWidth = properties.usePlatformDefaultWidth dialogLayout.usePlatformDefaultWidth = properties.usePlatformDefaultWidth
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) { if (canUpdateWindowLayout && Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
@OptIn(ExperimentalComposeUiApi::class) @OptIn(ExperimentalComposeUiApi::class)
if (properties.decorFitsSystemWindows) { if (properties.decorFitsSystemWindows) {
window?.setSoftInputMode(defaultSoftInputMode) dialogWindow?.setSoftInputMode(defaultSoftInputMode)
} else { } else {
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE) dialogWindow?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE)
} }
} }
} }

View File

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

View File

@@ -1,3 +1,18 @@
## v1.6.8
- Fix: Many improvements to the Performance Mode feature(Max, turned on by Default), changes are pretty noticeable: faster loading of chats, long group messages optimizations, many snapmap optimizations
- Fix: Crash issues for some devices
- Fix: Block Ads
- Fix: Snapchat automatically restarting if kept idle
- Fix: Feed Flickering issue for some devices
- Fix: Implemented Dual-Thread Sync Architecture with a 1,000 depth that proactively scans the database for missed history, thus resolving the "failed to open..." and "Tap to load" snaps before marking them, clearing historical gaps automatically(tq to Kaladin)
- New: Implemented Screen Guard where Auto Open Engine now detects screen state and stops all notification work while the device is locked, thus reducing excessive battery drain and heat spike during background processing(tq to Kaladin)
- Fix: Disk read/write frequency increased from 1s to 5 minutes, to eliminate constant background disk friction, thus reducing overheating of the device while processing(tq to Kaladin)
- Fix: Slid chat switching delays from 2.0s down to 40ms, thus achieving near-instant processing of 1000+ snap bursts(tq to Kaladin)
- Fix: Removed "Auto Open pause while Gaming" toggle(tq to Kaladin)
- Fix: Reworked "Snap Pre-Fetch" toggle to the global Messaging settings page(tq to Kaladin)
- Fix: Refactored Auto Open engine notification cards to declutter it and removed redundant stats(tq to Kaladin)
- Fix: Corrected speed labels in the Auto Open notification to show "Idle" when monitoring and accurate notions (Full Speed/Throttled) when active(tq to Kaladin)
## v1.6.6 ## v1.6.6
- Fix: Persistent auto open snap disabled notification(tq to Kaladin) - Fix: Persistent auto open snap disabled notification(tq to Kaladin)
- Fix: Crash issues for some devices - Fix: Crash issues for some devices

View File

@@ -168,11 +168,10 @@ class BridgeClient(
Log.d("BridgeClient", "service is dead, restarting") Log.d("BridgeClient", "service is dead, restarting")
val canLoad = connect { val canLoad = connect {
Log.e("BridgeClient", "connection failed", it) Log.e("BridgeClient", "connection failed", it)
context.softRestartApp()
} }
if (canLoad != true) { if (canLoad != true) {
Log.e("BridgeClient", "failed to reconnect to service, result=$canLoad") Log.e("BridgeClient", "failed to reconnect to service, result=$canLoad")
context.softRestartApp() return@runBlocking
} }
} }
} }
@@ -188,9 +187,6 @@ class BridgeClient(
block() block()
}.getOrElse { }.getOrElse {
Log.e("BridgeClient", "service call failed", it) Log.e("BridgeClient", "service call failed", it)
if (it is DeadObjectException) {
context.softRestartApp()
}
throw it throw it
} }
} }

View File

@@ -80,6 +80,7 @@ class FeatureManager(
MessageLogger(), MessageLogger(),
ConvertMessageLocally(), ConvertMessageLocally(),
SnapchatPlus(), SnapchatPlus(),
AdBlockFix(),
DisableMetrics(), DisableMetrics(),
EndpointsBlocker(), EndpointsBlocker(),
PreventMessageSending(), PreventMessageSending(),

View File

@@ -109,6 +109,10 @@ class ConfigurationOverride : Feature("Configuration Override") {
{ true }) { true })
overrideProperty("MEDIA_RECORDER_MAX_QUALITY_LEVEL", { context.config.camera.forceCameraSourceEncoding.get() }, overrideProperty("MEDIA_RECORDER_MAX_QUALITY_LEVEL", { context.config.camera.forceCameraSourceEncoding.get() },
{ true }) { true })
overrideProperty("ENABLE_MESSAGE_WINDOW_MANAGER", { context.config.global.performanceMode.profile.getNullable() != null },
{ true })
overrideProperty("ENABLE_SIMPLE_CONVERSATION_RESET", { context.config.global.performanceMode.profile.getNullable() == "max" },
{ false })
overrideProperty("PREVIEW_PRELOAD_ACTIVATOR", { context.config.global.performanceMode.profile.getNullable() != null }, overrideProperty("PREVIEW_PRELOAD_ACTIVATOR", { context.config.global.performanceMode.profile.getNullable() != null },
{ true }) { true })
overrideProperty("BUFFERED_VIDEO_RECORDING_ACTIVATOR", { context.config.global.performanceMode.profile.getNullable() != null }, overrideProperty("BUFFERED_VIDEO_RECORDING_ACTIVATOR", { context.config.global.performanceMode.profile.getNullable() != null },
@@ -120,27 +124,32 @@ class ConfigurationOverride : Feature("Configuration Override") {
arrayOf( arrayOf(
"FEATURE_PRELOADER", "FEATURE_PRELOADER",
"USER_STORY_PRELOAD",
"STARTUP_LENS_ACTIVATOR",
"LENSES_PREVIEW_ACTIVATOR",
"THUMBNAIL_PRESENTER_ACTIVATOR",
"SINGLE_SEGMENT_THUMBNAIL_ACTIVATOR",
"SERVER_PREFETCH", "SERVER_PREFETCH",
"SERVER_PREFETCH_WITH_COF", "SERVER_PREFETCH_WITH_COF",
"DISCOVER_FEED_PERFORMANCE", "DISCOVER_FEED_PERFORMANCE",
"DISCOVER_FEED_STORY_PREFETCH",
"DISCOVER_FEED_THUMBNAILS",
"LOGIN_PRELOAD", "LOGIN_PRELOAD",
"PREFETCH_REPO_SUBSCRIBE_ON_CPU", "PREFETCH_REPO_SUBSCRIBE_ON_CPU",
"COMPUTE_FEED_CACHE_WITH_TTL", "COMPUTE_FEED_CACHE_WITH_TTL",
"COMPUTE_FEED_NETWORK_WITH_CACHE", "COMPUTE_FEED_NETWORK_WITH_CACHE",
"OPERA_WARMUP", "OPERA_WARMUP",
"REFACTORED_WITH_WARMUP_LENS",
"SHOW_PREFETCH", "SHOW_PREFETCH",
).forEach { key -> ).forEach { key ->
overrideProperty(key, { context.config.global.performanceMode.profile.getNullable() != null }, { true }) overrideProperty(key, { context.config.global.performanceMode.profile.getNullable() != null }, { true })
} }
arrayOf(
"USER_STORY_PRELOAD",
"STARTUP_LENS_ACTIVATOR",
"LENSES_PREVIEW_ACTIVATOR",
"THUMBNAIL_PRESENTER_ACTIVATOR",
"SINGLE_SEGMENT_THUMBNAIL_ACTIVATOR",
"DISCOVER_FEED_STORY_PREFETCH",
"DISCOVER_FEED_THUMBNAILS",
"REFACTORED_WITH_WARMUP_LENS",
).forEach { key ->
overrideProperty(key, { context.config.global.performanceMode.profile.getNullable() == "max" }, { false })
}
overrideProperty("LOAD_LATENCY_TRACKER_ACTIVATOR", { context.config.global.performanceMode.profile.getNullable() != null }, overrideProperty("LOAD_LATENCY_TRACKER_ACTIVATOR", { context.config.global.performanceMode.profile.getNullable() != null },
{ false }) { false })
overrideProperty("ANALYTICS_ACTIVATOR", { context.config.global.performanceMode.profile.getNullable() != null }, overrideProperty("ANALYTICS_ACTIVATOR", { context.config.global.performanceMode.profile.getNullable() != null },

View File

@@ -0,0 +1,254 @@
package me.eternal.purrfectsnap.core.features.impl.global
import android.os.SystemClock
import android.view.View
import me.eternal.purrfectsnap.core.event.events.impl.BindViewEvent
import me.eternal.purrfectsnap.core.features.Feature
import me.eternal.purrfectsnap.core.ui.hideViewCompletely
import me.eternal.purrfectsnap.core.ui.dispatchSyntheticTap
import me.eternal.purrfectsnap.core.util.dataBuilder
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 me.eternal.purrfectsnap.core.util.ktx.getObjectField
import me.eternal.purrfectsnap.core.util.ktx.getObjectFieldOrNull
import me.eternal.purrfectsnap.core.wrapper.impl.SnapUUID
import me.eternal.purrfectsnap.core.wrapper.impl.media.opera.Layer
import me.eternal.purrfectsnap.core.wrapper.impl.media.opera.ParamMap
import me.eternal.purrfectsnap.mapper.impl.CallbackMapper
import me.eternal.purrfectsnap.mapper.impl.OperaPageViewControllerMapper
import java.util.ArrayList
import java.util.Collections
import java.util.concurrent.ConcurrentHashMap
class AdBlockFix : Feature("AdBlockFix") {
private val adConversationIds = Collections.newSetFromMap(ConcurrentHashMap<String, Boolean>())
@Volatile
private var lastAutoSkippedOperaFingerprint: String? = null
@Volatile
private var lastAutoSkippedOperaAt = 0L
override fun init() {
if (!context.config.global.blockAds.get()) return
hookFeedEntryTracking()
hookMessagingFeedCallbacks()
hookChatFeedRowSuppression()
hookOperaAutoSkip()
}
private fun hookFeedEntryTracking() {
findClass("com.snapchat.client.messaging.FeedEntry").hookConstructor(HookStage.AFTER) { param ->
val feedEntry = param.thisObject<Any>()
val conversationId = feedEntry.getObjectFieldOrNull("mConversationId")?.let(::SnapUUID)?.toString()
?: return@hookConstructor
if (isCampaignFeedEntry(feedEntry) || isChatAdShareFeedEntry(feedEntry)) {
adConversationIds.add(conversationId)
}
}
}
private fun hookMessagingFeedCallbacks() {
context.mappings.useMapper(CallbackMapper::class) {
classLoader = context.androidContext.classLoader
val callbackMap = callbacks.getAsMap().orEmpty()
val hookedCallbacks = mutableSetOf<String>()
fun hookOnce(
callbackClassName: String,
methodName: String,
block: (param: me.eternal.purrfectsnap.core.util.hook.HookAdapter) -> Unit
) {
val hookKey = "$callbackClassName#$methodName"
if (!hookedCallbacks.add(hookKey)) return
runCatching {
findClass(callbackClassName).hook(methodName, HookStage.BEFORE) { param ->
block(param)
}
}.onFailure {
context.log.warn("Failed to hook $methodName on $callbackClassName")
}
}
callbackMap.entries.forEach { (callbackName, callbackClassName) ->
val className = callbackClassName ?: return@forEach
when {
callbackName.startsWith("FetchAndSyncFeed") && callbackName.endsWith("Callback") -> {
hookOnce(className, "onFetchAndSyncFeedComplete") { param ->
val deletedEntries = param.argNullable<ArrayList<Any>>(2)
filterCampaignFeed(param.arg(0), deletedEntries)
if (deletedEntries?.isNotEmpty() == true) {
param.setArg(4, true)
}
}
}
callbackName.contains("SyncFeed") && callbackName.endsWith("Callback") -> {
hookOnce(className, "onSyncFeedComplete") { param ->
filterCampaignFeed(param.arg(0), param.argNullable(2))
}
}
callbackName == "FetchFeedCallback" || callbackName.contains("FetchFeedCallback") -> {
hookOnce(className, "onFetchFeedComplete") { param ->
filterCampaignFeed(param.arg(0))
}
}
callbackName == "FetchFeedEntriesCallback" || callbackName.contains("FetchFeedEntriesCallback") -> {
hookOnce(className, "onFetchFeedEntriesComplete") { param ->
filterCampaignFeed(param.arg(0))
}
}
callbackName == "QueryFeedCallback" || callbackName.contains("QueryFeedCallback") -> {
hookOnce(className, "onQueryFeedComplete") { param ->
filterCampaignFeed(param.arg(0))
}
}
callbackName == "FeedManagerDelegate" -> {
hookOnce(className, "onFeedEntriesUpdated") { param ->
filterCampaignFeed(param.arg(0))
}
hookOnce(className, "onInternalSyncFeed") { param ->
filterCampaignFeed(param.arg(0))
}
}
}
}
}
}
private fun hookChatFeedRowSuppression() {
context.event.subscribe(BindViewEvent::class) { event ->
val modelDump = event.prevModel.toString()
event.friendFeedItem { conversationId ->
if (adConversationIds.contains(conversationId) || isChatAdShareModel(modelDump)) {
hideBoundChatFeedRow(event.view)
}
}
}
}
private fun hideBoundChatFeedRow(view: View) {
view.hideViewCompletely()
(view.parent as? View)?.hideViewCompletely()
(view.parent?.parent as? View)?.hideViewCompletely()
}
private fun hookOperaAutoSkip() {
onNextActivityCreate {
context.mappings.useMapper(OperaPageViewControllerMapper::class) {
arrayOf(onDisplayStateChange, onDisplayStateChangeGesture).forEach { methodName ->
val resolvedMethod = methodName.get() ?: return@forEach
classReference.get()?.hook(resolvedMethod, HookStage.AFTER) { param ->
val viewState = runCatching {
param.thisObject<Any>().getObjectField(viewStateField.get()!!)?.toString()
}.getOrNull() ?: return@hook
if (viewState != "FULLY_DISPLAYED") return@hook
val layerList = runCatching {
param.thisObject<Any>().getObjectField(layerListField.get()!!) as? ArrayList<*>
}.getOrNull() ?: return@hook
val paramMap = runCatching {
layerList.map { Layer(it).paramMap }.firstOrNull()
}.getOrNull() ?: return@hook
if (!isSpotlightCommercialPage(paramMap)) return@hook
val fingerprint = buildOperaFingerprint(paramMap)
val now = SystemClock.elapsedRealtime()
if (fingerprint == lastAutoSkippedOperaFingerprint && now - lastAutoSkippedOperaAt < 1_500L) {
return@hook
}
lastAutoSkippedOperaFingerprint = fingerprint
lastAutoSkippedOperaAt = now
runOnUiThread {
context.mainActivity?.window?.decorView?.postDelayed({
context.mainActivity?.window?.decorView?.let { decorView ->
val x = decorView.width * 0.88f
val y = decorView.height * 0.5f
decorView.dispatchSyntheticTap(x, y)
}
}, 70L)
}
}
}
}
}
}
private fun filterCampaignFeed(entries: ArrayList<Any>, deletedEntries: ArrayList<Any>? = null) {
entries.removeIf { feedEntry ->
if (!isCampaignFeedEntry(feedEntry)) return@removeIf false
val conversationIdInstance = feedEntry.getObjectFieldOrNull("mConversationId") ?: return@removeIf true
deletedEntries?.add(createDeletedFeedEntry(conversationIdInstance))
true
}
}
private fun createDeletedFeedEntry(conversationIdInstance: Any) =
findClass("com.snapchat.client.messaging.DeletedFeedEntry").dataBuilder {
from("mFeedEntryIdentifier") {
set("mConversationId", conversationIdInstance)
}
set("mReason", "AD_CAMPAIGN_COMPLETE")
}!!
private fun isCampaignFeedEntry(feedEntry: Any?): Boolean {
if (feedEntry == null) return false
if (feedEntry.getObjectFieldOrNull("mConversationSubType")?.toString() == "CAMPAIGN") {
return true
}
return feedEntry.getObjectFieldOrNull("mConversationSubTypeMetadata")
?.getObjectFieldOrNull("mCampaignMetadata") != null
}
private fun isChatAdShareFeedEntry(feedEntry: Any): Boolean {
val interactionDump = feedEntry.getObjectFieldOrNull("mInteractionInfo")?.toString().orEmpty()
val displayDump = feedEntry.getObjectFieldOrNull("mDisplayInfo")?.toString().orEmpty()
val combined = "$interactionDump $displayDump"
return isChatAdShareModel(combined)
}
private fun isChatAdShareModel(modelDump: String): Boolean {
if (modelDump.isBlank()) return false
return modelDump.contains("CHAT_AD_SHARE") ||
modelDump.contains("AD_SHARE") ||
modelDump.contains("ChatAd") ||
modelDump.contains("chat_ad_share") ||
modelDump.contains("chat_sponsored_snap") ||
modelDump.contains("CommonAttachmentViewModel") ||
modelDump.contains("visibilityFeedbackURL") ||
modelDump.contains("pageLoadPingURL")
}
private fun isSpotlightCommercialPage(paramMap: ParamMap): Boolean {
val snapSource = paramMap["SNAP_SOURCE"]?.toString()
if (snapSource != "SINGLE_SNAP_STORY" && snapSource != "SPOTLIGHT" && snapSource != "PUBLIC_STORY") {
return false
}
val adProductType = paramMap["ad_product_type"]?.toString()
if (!adProductType.isNullOrBlank() && adProductType != "UNKNOWN" && adProductType != "null") {
return true
}
val pageDump = paramMap.toString().uppercase()
return pageDump.contains("COMMERCIAL") || pageDump.contains("PROMOTED_STORY")
}
private fun buildOperaFingerprint(paramMap: ParamMap): String {
val storyId = paramMap["STORY_ID"]?.toString()
val snapId = paramMap["SNAP_ID"]?.toString()
?: paramMap["snap_id"]?.toString()
val index = paramMap["snap_index_in_story"]?.toString()
?: paramMap["SNAP_POSITION_IN_STORY"]?.toString()
return listOfNotNull(storyId, snapId, index, paramMap["ad_product_type"]?.toString()).joinToString("|")
}
}

View File

@@ -12,6 +12,7 @@ import android.hardware.camera2.CameraCharacteristics.Key
import android.hardware.camera2.CameraManager import android.hardware.camera2.CameraManager
import android.media.Image import android.media.Image
import android.media.ImageReader import android.media.ImageReader
import android.os.Build
import android.util.Range import android.util.Range
import me.eternal.purrfectsnap.core.features.Feature import me.eternal.purrfectsnap.core.features.Feature
import me.eternal.purrfectsnap.core.util.hook.HookStage import me.eternal.purrfectsnap.core.util.hook.HookStage
@@ -28,6 +29,9 @@ class CameraTweaks : Feature("Camera Tweaks") {
@SuppressLint("MissingPermission", "DiscouragedApi") @SuppressLint("MissingPermission", "DiscouragedApi")
override fun init() { override fun init() {
val config = context.config.camera val config = context.config.camera
val skipUnstableStillCaptureTweaks = Build.MANUFACTURER.equals("samsung", ignoreCase = true) ||
Build.HARDWARE.contains("exynos", ignoreCase = true) ||
Build.BRAND.equals("samsung", ignoreCase = true)
// Toggle A: Audio & Video Optimizations (Bitrates) // Toggle A: Audio & Video Optimizations (Bitrates)
if (config.audioVideoOptimizations.get()) { if (config.audioVideoOptimizations.get()) {
@@ -44,7 +48,7 @@ class CameraTweaks : Feature("Camera Tweaks") {
} }
// Toggle B: Camera Optimizations (Hardware ISP - UNSTABLE) // Toggle B: Camera Optimizations (Hardware ISP - UNSTABLE)
if (config.cameraOptimizations.get()) { if (config.cameraOptimizations.get() && !skipUnstableStillCaptureTweaks) {
CaptureRequest.Builder::class.java.hook("set", HookStage.BEFORE) { param -> CaptureRequest.Builder::class.java.hook("set", HookStage.BEFORE) { param ->
val key = param.arg<CaptureRequest.Key<*>>(0) val key = param.arg<CaptureRequest.Key<*>>(0)
when (key) { when (key) {

View File

@@ -3,37 +3,55 @@ package me.eternal.purrfectsnap.core.features.impl.tweaks
import android.animation.ValueAnimator import android.animation.ValueAnimator
import android.app.Activity import android.app.Activity
import android.app.Dialog import android.app.Dialog
import android.content.Context
import android.database.Cursor import android.database.Cursor
import android.database.MatrixCursor import android.database.MatrixCursor
import android.database.sqlite.SQLiteDatabase import android.database.sqlite.SQLiteDatabase
import android.hardware.camera2.CaptureRequest
import android.media.MediaRecorder import android.media.MediaRecorder
import android.os.Build
import android.transition.Transition
import android.os.HandlerThread import android.os.HandlerThread
import android.os.Process import android.os.Process
import android.util.Base64 import android.util.Base64
import android.util.Range import android.util.Range
import android.view.View import android.view.View
import android.view.ViewPropertyAnimator
import android.view.WindowManager
import android.view.animation.Animation
import android.widget.OverScroller import android.widget.OverScroller
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.StaggeredGridLayoutManager import androidx.recyclerview.widget.StaggeredGridLayoutManager
import java.io.File import java.io.File
import java.lang.Thread import java.lang.Thread
import java.lang.reflect.Method
import java.util.LinkedHashMap
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicLong
import java.util.concurrent.ThreadPoolExecutor import java.util.concurrent.ThreadPoolExecutor
import com.google.gson.reflect.TypeToken
import me.eternal.purrfectsnap.core.features.Feature import me.eternal.purrfectsnap.core.features.Feature
import me.eternal.purrfectsnap.core.features.impl.messaging.Messaging
import me.eternal.purrfectsnap.core.event.events.impl.NetworkApiRequestEvent import me.eternal.purrfectsnap.core.event.events.impl.NetworkApiRequestEvent
import me.eternal.purrfectsnap.core.wrapper.impl.SnapUUID
import me.eternal.purrfectsnap.mapper.impl.CallbackMapper
import me.eternal.purrfectsnap.core.util.hook.HookStage import me.eternal.purrfectsnap.core.util.hook.HookStage
import me.eternal.purrfectsnap.core.util.hook.findRestrictedMethod
import me.eternal.purrfectsnap.core.util.hook.hook import me.eternal.purrfectsnap.core.util.hook.hook
import me.eternal.purrfectsnap.core.util.hook.hookConstructor import me.eternal.purrfectsnap.core.util.hook.hookConstructor
import me.eternal.purrfectsnap.core.util.ktx.getObjectField
import me.eternal.purrfectsnap.core.util.ktx.setObjectField
import okhttp3.Dispatcher import okhttp3.Dispatcher
class PerformanceMode : Feature("Performance Mode") { class PerformanceMode : Feature("Performance Mode") {
companion object {
private const val CHAT_FEED_CACHE_MAX_ROWS = 400
private const val CHAT_FEED_CACHE_MAX_BLOB_BYTES = 512
private const val CHAT_FEED_CACHE_MIN_REFRESH_INTERVAL_MS = 15_000L
private const val MESSAGE_WINDOW_STATE_MAX_CONVERSATIONS = 64
private const val MESSAGE_WINDOW_STATE_MAX_AGE_MS = 7L * 24L * 60L * 60L * 1000L
private const val SNAP_PREFETCH_GROUP_MESSAGES = 48
private const val SNAP_PREFETCH_DM_MESSAGES = 24
private const val REOPEN_WARMUP_GROUP_MESSAGES = 160
private const val REOPEN_WARMUP_DM_MESSAGES = 96
}
private data class SnapshotCell( private data class SnapshotCell(
val type: Int, val type: Int,
val stringValue: String? = null, val stringValue: String? = null,
@@ -47,6 +65,15 @@ class PerformanceMode : Feature("Performance Mode") {
val rows: List<List<SnapshotCell>>, val rows: List<List<SnapshotCell>>,
) )
private data class MessageWindowState(
val conversationId: String,
val currentSize: Int,
val oldestOrderKey: Long?,
val newestOrderKey: Long?,
val updatedAt: Long,
val isGroup: Boolean,
)
override fun init() { override fun init() {
val profile = context.config.global.performanceMode.profile.getNullable() ?: return val profile = context.config.global.performanceMode.profile.getNullable() ?: return
val isMaxProfile = profile == "max" val isMaxProfile = profile == "max"
@@ -56,6 +83,7 @@ class PerformanceMode : Feature("Performance Mode") {
Process.THREAD_PRIORITY_MORE_FAVORABLE Process.THREAD_PRIORITY_MORE_FAVORABLE
} }
val minimumFrameRate = if (isMaxProfile) 60 else 45 val minimumFrameRate = if (isMaxProfile) 60 else 45
val minimumRecordingFrameRate = if (isMaxProfile) 30 else 24
val durationScale = if (isMaxProfile) 0.35f else 0.55f val durationScale = if (isMaxProfile) 0.35f else 0.55f
val recyclerViewCacheSize = if (isMaxProfile) 64 else 32 val recyclerViewCacheSize = if (isMaxProfile) 64 else 32
val maxRequests = if (isMaxProfile) 192 else 96 val maxRequests = if (isMaxProfile) 192 else 96
@@ -63,11 +91,16 @@ class PerformanceMode : Feature("Performance Mode") {
val minimumCoreThreads = if (isMaxProfile) 16 else 8 val minimumCoreThreads = if (isMaxProfile) 16 else 8
val prefetchItemCount = if (isMaxProfile) 24 else 12 val prefetchItemCount = if (isMaxProfile) 24 else 12
val maxAnimationDurationMs = if (isMaxProfile) 90L else 140L val maxAnimationDurationMs = if (isMaxProfile) 90L else 140L
val maxScrollDurationMs = if (isMaxProfile) 120 else 180 val maxScrollDurationMs = if (isMaxProfile) 72 else 180
val preferredRefreshRate = if (isMaxProfile) 120f else 90f val preferredRefreshRate = if (isMaxProfile) 120f else 90f
val snapMapTransitionDurationMs = if (isMaxProfile) 0L else 24L
val snapMapCameraDurationMs = if (isMaxProfile) 16L else 64L
val snapMapMoveDurationMs = if (isMaxProfile) 8L else 40L
val snapMapPrefetchZoomDelta = if (isMaxProfile) 6 else 3
val preferredJavaThreadPriority = if (isMaxProfile) Thread.NORM_PRIORITY + 2 else Thread.NORM_PRIORITY + 1
context.log.info( context.log.info(
"Performance mode enabled: profile=$profile, threadPriority=$threadPriority, minFps=$minimumFrameRate, durationScale=$durationScale, rvCache=$recyclerViewCacheSize, maxRequests=$maxRequests/$maxRequestsPerHost, minCoreThreads=$minimumCoreThreads, prefetch=$prefetchItemCount, maxAnimMs=$maxAnimationDurationMs, maxScrollMs=$maxScrollDurationMs, preferredRefreshRate=$preferredRefreshRate", "Performance mode enabled: profile=$profile, threadPriority=$threadPriority, minFps=$minimumFrameRate, minRecordingFps=$minimumRecordingFrameRate, durationScale=$durationScale, rvCache=$recyclerViewCacheSize, maxRequests=$maxRequests/$maxRequestsPerHost, minCoreThreads=$minimumCoreThreads, prefetch=$prefetchItemCount, maxAnimMs=$maxAnimationDurationMs, maxScrollMs=$maxScrollDurationMs, preferredRefreshRate=$preferredRefreshRate, snapMapTransitionMs=$snapMapTransitionDurationMs, snapMapCameraMs=$snapMapCameraDurationMs, snapMapMoveMs=$snapMapMoveDurationMs, snapMapPrefetchZoomDelta=$snapMapPrefetchZoomDelta, javaThreadPriority=$preferredJavaThreadPriority",
"PerformanceMode" "PerformanceMode"
) )
@@ -91,38 +124,58 @@ class PerformanceMode : Feature("Performance Mode") {
val executorLog = firstHitLogger("ThreadPoolExecutor.constructor") val executorLog = firstHitLogger("ThreadPoolExecutor.constructor")
val dispatcherLog = firstHitLogger("OkHttp.Dispatcher.constructor") val dispatcherLog = firstHitLogger("OkHttp.Dispatcher.constructor")
val animatorLog = firstHitLogger("ValueAnimator.getDurationScale") val animatorLog = firstHitLogger("ValueAnimator.getDurationScale")
val animatorDurationLog = firstHitLogger("ValueAnimator.setDuration")
val viewAnimatorDurationLog = firstHitLogger("ViewPropertyAnimator.setDuration")
val transitionDurationLog = firstHitLogger("Transition.setDuration")
val animationDurationLog = firstHitLogger("Animation.setDuration")
val recyclerCtorLog = firstHitLogger("RecyclerView.constructor") val recyclerCtorLog = firstHitLogger("RecyclerView.constructor")
val recyclerAdapterLog = firstHitLogger("RecyclerView.setAdapter") val recyclerAdapterLog = firstHitLogger("RecyclerView.setAdapter")
val recyclerLayoutManagerLog = firstHitLogger("RecyclerView.setLayoutManager") val recyclerLayoutManagerLog = firstHitLogger("RecyclerView.setLayoutManager")
val sqliteOpenLog = firstHitLogger("SQLiteDatabase.openDatabase") val sqliteOpenLog = firstHitLogger("SQLiteDatabase.openDatabase")
val sqliteCreateLog = firstHitLogger("SQLiteDatabase.openOrCreateDatabase") val sqliteCreateLog = firstHitLogger("SQLiteDatabase.openOrCreateDatabase")
val mediaRecorderLog = firstHitLogger("MediaRecorder.setVideoFrameRate") val mediaRecorderLog = firstHitLogger("MediaRecorder.setVideoFrameRate")
val captureRequestLog = firstHitLogger("CaptureRequest.Builder.set")
val sustainedModeLog = firstHitLogger("Window.setSustainedPerformanceMode")
val refreshRateLog = firstHitLogger("Activity.preferredRefreshRate")
val overScrollerLog = firstHitLogger("OverScroller.startScroll") val overScrollerLog = firstHitLogger("OverScroller.startScroll")
val chatFeedCacheServeLog = firstHitLogger("ChatFeed.cacheServe")
val chatFeedCacheRefreshLog = firstHitLogger("ChatFeed.cacheRefresh")
val mapDialogLog = firstHitLogger("Dialog.show") val mapDialogLog = firstHitLogger("Dialog.show")
val mapViewLog = firstHitLogger("MapView.constructor") val mapViewLog = firstHitLogger("MapView.constructor")
val mapboxNetworkBlockLog = firstHitLogger("SnapMap.telemetryBlock") val mapboxNetworkBlockLog = firstHitLogger("SnapMap.telemetryBlock")
val mapCameraAnimLog = firstHitLogger("SnapMap.mapAnimatorDuration") val mapCameraAnimLog = firstHitLogger("SnapMap.mapAnimatorDuration")
val mapThreadLog = firstHitLogger("SnapMap.mapThread") val mapThreadLog = firstHitLogger("SnapMap.mapThread")
val mapRendererFpsLog = firstHitLogger("SnapMap.mapRendererFps") val mapRendererFpsLog = firstHitLogger("SnapMap.mapRendererFps")
val mapTransitionLog = firstHitLogger("SnapMap.transitionOptions")
val mapMoveLog = firstHitLogger("SnapMap.moveDuration")
fun isPerformanceSensitiveThread(name: String?): Boolean { fun isPerformanceSensitiveThread(name: String?): Boolean {
val normalizedName = name?.lowercase() ?: return false val normalizedName = name?.lowercase() ?: return false
return listOf("camera", "preview", "codec", "render", "gl", "transcod", "lens", "feed", "story", "opera", "messag", "network", "db", "disk", "map", "mapbox", "snapmap", "viewport").any { return listOf("codec", "transcod", "feed", "story", "opera", "messag", "network", "db", "disk", "map", "mapbox", "snapmap", "viewport").any {
normalizedName.contains(it) normalizedName.contains(it)
} }
} }
fun clampPositiveDuration(durationMs: Long, maxDurationMs: Long): Long {
if (durationMs <= 0L) return durationMs
return durationMs.coerceAtMost(maxDurationMs)
}
val performanceCacheDir = File(context.androidContext.filesDir, "performance_mode_cache").apply { mkdirs() } val performanceCacheDir = File(context.androidContext.filesDir, "performance_mode_cache").apply { mkdirs() }
val chatFeedSnapshotFile = File(performanceCacheDir, "chat_feed_snapshot.json") val chatFeedSnapshotFile = File(performanceCacheDir, "chat_feed_snapshot.json")
val lastChatFeedSnapshotWrite = AtomicLong(0L)
val chatFeedSnapshotServedThisProcess = AtomicBoolean(false)
val windowStatePrefs = context.androidContext.getSharedPreferences("purrfectsnap_perf_message_windows", Context.MODE_PRIVATE)
val messageWindowStates = runCatching {
val raw = windowStatePrefs.getString("states", null).orEmpty()
if (raw.isBlank()) {
LinkedHashMap<String, MessageWindowState>()
} else {
context.gson.fromJson<LinkedHashMap<String, MessageWindowState>>(
raw,
object : TypeToken<LinkedHashMap<String, MessageWindowState>>() {}.type
) ?: LinkedHashMap()
}
}.getOrElse { LinkedHashMap() }
fun persistMessageWindowStates() {
runCatching {
windowStatePrefs.edit().putString("states", context.gson.toJson(messageWindowStates)).apply()
}.onFailure {
context.log.error("Failed to persist message window states", it, "PerformanceMode")
}
}
fun isChatFeedQuery(sql: String): Boolean { fun isChatFeedQuery(sql: String): Boolean {
val normalized = sql.uppercase() val normalized = sql.uppercase()
@@ -144,7 +197,9 @@ class PerformanceMode : Feature("Performance Mode") {
Cursor.FIELD_TYPE_STRING -> SnapshotCell(Cursor.FIELD_TYPE_STRING, stringValue = cursor.getString(index)) Cursor.FIELD_TYPE_STRING -> SnapshotCell(Cursor.FIELD_TYPE_STRING, stringValue = cursor.getString(index))
Cursor.FIELD_TYPE_BLOB -> SnapshotCell( Cursor.FIELD_TYPE_BLOB -> SnapshotCell(
Cursor.FIELD_TYPE_BLOB, Cursor.FIELD_TYPE_BLOB,
blobValue = Base64.encodeToString(cursor.getBlob(index), Base64.NO_WRAP) blobValue = cursor.getBlob(index)
?.takeIf { it.size <= CHAT_FEED_CACHE_MAX_BLOB_BYTES }
?.let { Base64.encodeToString(it, Base64.NO_WRAP) }
) )
else -> SnapshotCell(Cursor.FIELD_TYPE_STRING, stringValue = cursor.getString(index)) else -> SnapshotCell(Cursor.FIELD_TYPE_STRING, stringValue = cursor.getString(index))
} }
@@ -154,9 +209,11 @@ class PerformanceMode : Feature("Performance Mode") {
val columns = cursor.columnNames.toList() val columns = cursor.columnNames.toList()
val rows = mutableListOf<List<SnapshotCell>>() val rows = mutableListOf<List<SnapshotCell>>()
if (cursor.moveToFirst()) { if (cursor.moveToFirst()) {
var rowCount = 0
do { do {
rows += columns.indices.map { index -> cursorCell(cursor, index) } rows += columns.indices.map { index -> cursorCell(cursor, index) }
} while (cursor.moveToNext()) rowCount++
} while (rowCount < CHAT_FEED_CACHE_MAX_ROWS && cursor.moveToNext())
} }
return CursorSnapshot(columns, rows) return CursorSnapshot(columns, rows)
} }
@@ -201,12 +258,6 @@ class PerformanceMode : Feature("Performance Mode") {
context.log.info("Invalidated chat feed snapshot after friends mutation sync", "PerformanceMode") 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"))) { if (url.contains("mapbox") && (url.contains("events.") || url.contains("telemetry"))) {
event.canceled = true event.canceled = true
mapboxNetworkBlockLog("url=$url") mapboxNetworkBlockLog("url=$url")
@@ -237,7 +288,7 @@ class PerformanceMode : Feature("Performance Mode") {
val thread = param.thisObject<Thread>() val thread = param.thisObject<Thread>()
if (!isPerformanceSensitiveThread(thread.name)) return@hook if (!isPerformanceSensitiveThread(thread.name)) return@hook
runCatching { runCatching {
thread.priority = Thread.MAX_PRIORITY thread.priority = preferredJavaThreadPriority
} }
threadStartLog("name=${thread.name} priority=${thread.priority}") threadStartLog("name=${thread.name} priority=${thread.priority}")
if ((thread.name ?: "").contains("map", ignoreCase = true) || (thread.name ?: "").contains("mapbox", ignoreCase = true)) { if ((thread.name ?: "").contains("map", ignoreCase = true) || (thread.name ?: "").contains("mapbox", ignoreCase = true)) {
@@ -272,51 +323,10 @@ class PerformanceMode : Feature("Performance Mode") {
animatorLog("durationScale=$durationScale") animatorLog("durationScale=$durationScale")
} }
ValueAnimator::class.java.hook("setDuration", HookStage.BEFORE) { param ->
val original = param.arg<Long>(0)
val updated = original.coerceAtMost(maxAnimationDurationMs)
if (updated != original) {
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 ->
val original = param.arg<Long>(0)
val updated = original.coerceAtMost(maxAnimationDurationMs)
if (updated != original) {
param.setArg(0, updated)
}
viewAnimatorDurationLog("requested=$original applied=${param.arg<Long>(0)}")
}
Transition::class.java.hook("setDuration", HookStage.BEFORE) { param ->
val original = param.arg<Long>(0)
val updated = original.coerceAtMost(maxAnimationDurationMs)
if (updated != original) {
param.setArg(0, updated)
}
transitionDurationLog("requested=$original applied=${param.arg<Long>(0)}")
}
Animation::class.java.hook("setDuration", HookStage.BEFORE) { param ->
val original = param.arg<Long>(0)
val updated = original.coerceAtMost(maxAnimationDurationMs)
if (updated != original) {
param.setArg(0, updated)
}
animationDurationLog("requested=$original applied=${param.arg<Long>(0)}")
}
RecyclerView::class.java.hookConstructor(HookStage.AFTER) { param -> RecyclerView::class.java.hookConstructor(HookStage.AFTER) { param ->
val recyclerView = param.thisObject<RecyclerView>() val recyclerView = param.thisObject<RecyclerView>()
recyclerView.setItemViewCacheSize(recyclerViewCacheSize) recyclerView.setItemViewCacheSize(recyclerViewCacheSize)
recyclerView.overScrollMode = View.OVER_SCROLL_NEVER recyclerView.overScrollMode = View.OVER_SCROLL_NEVER
recyclerView.setLayerType(View.LAYER_TYPE_HARDWARE, null)
if (isMaxProfile) { if (isMaxProfile) {
recyclerView.itemAnimator = null recyclerView.itemAnimator = null
} }
@@ -326,7 +336,6 @@ class PerformanceMode : Feature("Performance Mode") {
RecyclerView::class.java.hook("setAdapter", HookStage.AFTER) { param -> RecyclerView::class.java.hook("setAdapter", HookStage.AFTER) { param ->
val recyclerView = param.thisObject<RecyclerView>() val recyclerView = param.thisObject<RecyclerView>()
recyclerView.setItemViewCacheSize(recyclerViewCacheSize) recyclerView.setItemViewCacheSize(recyclerViewCacheSize)
recyclerView.setLayerType(View.LAYER_TYPE_HARDWARE, null)
if (isMaxProfile) { if (isMaxProfile) {
recyclerView.itemAnimator = null recyclerView.itemAnimator = null
} }
@@ -346,7 +355,6 @@ class PerformanceMode : Feature("Performance Mode") {
layoutManager.gapStrategy = StaggeredGridLayoutManager.GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS layoutManager.gapStrategy = StaggeredGridLayoutManager.GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS
} }
} }
recyclerView.setLayerType(View.LAYER_TYPE_HARDWARE, null)
recyclerLayoutManagerLog("layoutManager=${layoutManager?.javaClass?.name} prefetch=$prefetchItemCount") recyclerLayoutManagerLog("layoutManager=${layoutManager?.javaClass?.name} prefetch=$prefetchItemCount")
} }
@@ -375,8 +383,11 @@ class PerformanceMode : Feature("Performance Mode") {
MediaRecorder::class.java.hook("setVideoFrameRate", HookStage.BEFORE) { param -> MediaRecorder::class.java.hook("setVideoFrameRate", HookStage.BEFORE) { param ->
val currentRate = param.arg<Int>(0) val currentRate = param.arg<Int>(0)
if (currentRate < minimumFrameRate) { val applied = currentRate
param.setArg(0, minimumFrameRate) .coerceAtLeast(minimumRecordingFrameRate)
.coerceAtMost(if (isMaxProfile) 60 else 45)
if (applied != currentRate) {
param.setArg(0, applied)
} }
mediaRecorderLog("requested=$currentRate applied=${param.arg<Int>(0)}") mediaRecorderLog("requested=$currentRate applied=${param.arg<Int>(0)}")
} }
@@ -401,27 +412,9 @@ class PerformanceMode : Feature("Performance Mode") {
} }
} }
CaptureRequest.Builder::class.java.hook("set", HookStage.BEFORE) { param ->
val key = param.arg<CaptureRequest.Key<*>>(0)
captureRequestLog("key=${key.name} value=${param.argNullable<Any>(1)}")
}
fun applyActivityPerformanceTuning(activity: Activity) { fun applyActivityPerformanceTuning(activity: Activity) {
runCatching { runCatching {
activity.window.decorView.setLayerType(View.LAYER_TYPE_HARDWARE, null) activity.window.setWindowAnimations(0)
val display = activity.display
val targetRefreshRate = display?.supportedModes?.maxByOrNull { it.refreshRate }?.refreshRate
?.coerceAtLeast(preferredRefreshRate) ?: preferredRefreshRate
activity.window.attributes = activity.window.attributes.apply {
this.preferredRefreshRate = targetRefreshRate
}
refreshRateLog("activity=${activity::class.java.name} refreshRate=$targetRefreshRate")
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && isMaxProfile) {
runCatching {
activity.window.setSustainedPerformanceMode(true)
sustainedModeLog("activity=${activity::class.java.name}")
}
} }
} }
@@ -434,10 +427,6 @@ class PerformanceMode : Feature("Performance Mode") {
val window = dialog.window ?: return@hook val window = dialog.window ?: return@hook
runCatching { runCatching {
window.setWindowAnimations(0) 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)) { if (dialog::class.java.name.contains("map", ignoreCase = true) || dialog::class.java.name.contains("snap", ignoreCase = true)) {
mapDialogLog("class=${dialog::class.java.name}") mapDialogLog("class=${dialog::class.java.name}")
} }
@@ -447,7 +436,6 @@ class PerformanceMode : Feature("Performance Mode") {
runCatching { runCatching {
findClass("com.mapbox.mapboxsdk.maps.MapView").hookConstructor(HookStage.AFTER) { param -> findClass("com.mapbox.mapboxsdk.maps.MapView").hookConstructor(HookStage.AFTER) { param ->
val mapView = param.nullableThisObject<Any>() as? View ?: return@hookConstructor val mapView = param.nullableThisObject<Any>() as? View ?: return@hookConstructor
mapView.setLayerType(View.LAYER_TYPE_HARDWARE, null)
mapView.overScrollMode = View.OVER_SCROLL_NEVER mapView.overScrollMode = View.OVER_SCROLL_NEVER
mapViewLog("class=${mapView::class.java.name}") mapViewLog("class=${mapView::class.java.name}")
} }
@@ -464,14 +452,165 @@ class PerformanceMode : Feature("Performance Mode") {
} }
} }
runCatching {
val nativeMapViewClass = findClass("com.mapbox.mapboxsdk.maps.NativeMapView")
val transitionOptionsClass = findClass("com.mapbox.mapboxsdk.style.layers.TransitionOptions")
val transitionOptionsCtor = transitionOptionsClass.getDeclaredConstructor(Long::class.javaPrimitiveType, Long::class.javaPrimitiveType, Boolean::class.javaPrimitiveType).apply {
isAccessible = true
}
fun findNativeMapMethod(name: String, predicate: (Method) -> Boolean): Method? {
return nativeMapViewClass.findRestrictedMethod { method ->
method.name == name && predicate(method)
}?.apply {
isAccessible = true
}
}
val nativeCancelTransitions = findNativeMapMethod("nativeCancelTransitions") { it.parameterCount == 0 }
val nativeSetPrefetchTiles = findNativeMapMethod("nativeSetPrefetchTiles") { it.parameterCount == 1 && it.parameterTypes[0] == Boolean::class.javaPrimitiveType }
val nativeSetPrefetchZoomDelta = findNativeMapMethod("nativeSetPrefetchZoomDelta") { it.parameterCount == 1 && it.parameterTypes[0] == Int::class.javaPrimitiveType }
val nativeSetTransitionDelay = findNativeMapMethod("nativeSetTransitionDelay") { it.parameterCount == 1 && it.parameterTypes[0] == Long::class.javaPrimitiveType }
val nativeSetTransitionDuration = findNativeMapMethod("nativeSetTransitionDuration") { it.parameterCount == 1 && it.parameterTypes[0] == Long::class.javaPrimitiveType }
val nativeSetTransitionOptions = findNativeMapMethod("nativeSetTransitionOptions") { it.parameterCount == 1 && it.parameterTypes[0].name == transitionOptionsClass.name }
nativeMapViewClass.hookConstructor(HookStage.AFTER) { param ->
val nativeMapView = param.thisObject<Any>()
runCatching {
nativeSetPrefetchTiles?.invoke(nativeMapView, true)
nativeSetPrefetchZoomDelta?.invoke(nativeMapView, snapMapPrefetchZoomDelta)
nativeSetTransitionDelay?.invoke(nativeMapView, 0L)
nativeSetTransitionDuration?.invoke(nativeMapView, snapMapTransitionDurationMs)
nativeSetTransitionOptions?.invoke(
nativeMapView,
transitionOptionsCtor.newInstance(snapMapTransitionDurationMs, 0L, false)
)
nativeCancelTransitions?.invoke(nativeMapView)
mapTransitionLog("transitionMs=$snapMapTransitionDurationMs prefetchZoomDelta=$snapMapPrefetchZoomDelta placementTransitions=false")
}
}
nativeMapViewClass.findRestrictedMethod { method ->
method.name == "g" &&
method.parameterCount == 6 &&
method.parameterTypes.last() == Long::class.javaPrimitiveType
}?.hook(HookStage.BEFORE) { param ->
val original = param.arg<Long>(5)
val applied = clampPositiveDuration(original, snapMapCameraDurationMs)
if (applied != original) {
param.setArg(5, applied)
}
runCatching { nativeCancelTransitions?.invoke(param.thisObject<Any>()) }
mapCameraAnimLog("requested=$original applied=${param.arg<Long>(5)}")
}
nativeMapViewClass.findRestrictedMethod { method ->
method.name == "v" &&
method.parameterCount == 3 &&
method.parameterTypes[0] == Double::class.javaPrimitiveType &&
method.parameterTypes[1] == Double::class.javaPrimitiveType &&
method.parameterTypes[2] == Long::class.javaPrimitiveType
}?.hook(HookStage.BEFORE) { param ->
val original = param.arg<Long>(2)
val applied = clampPositiveDuration(original, snapMapMoveDurationMs)
if (applied != original) {
param.setArg(2, applied)
}
runCatching { nativeCancelTransitions?.invoke(param.thisObject<Any>()) }
mapMoveLog("requested=$original applied=${param.arg<Long>(2)}")
}
}.onFailure {
context.log.error("Failed to install Snap Map transition hooks", it, "PerformanceMode")
}
runCatching {
findClass("com.snapchat.client.messaging.MessageWindowManager\$CppProxy").hook("initWindow", HookStage.BEFORE) { param ->
if (!isMaxProfile) return@hook
val conversationId = runCatching {
SnapUUID(param.arg(0)).toString()
}.getOrNull()?.takeIf { it.isNotBlank() } ?: return@hook
val initParams = param.arg<Any>(1)
val conversationType = context.database.getConversationType(conversationId) ?: return@hook
val isGroup = conversationType == 1
val savedState = synchronized(messageWindowStates) {
messageWindowStates[conversationId]
?.takeIf { System.currentTimeMillis() - it.updatedAt <= MESSAGE_WINDOW_STATE_MAX_AGE_MS }
}
val enumConstants = initParams.getObjectField("mStartingType")?.javaClass?.enumConstants ?: return@hook
if (savedState != null) {
val restoredMaxSize = if (savedState.isGroup) {
savedState.currentSize.coerceAtLeast(220).coerceAtMost(520)
} else {
savedState.currentSize.coerceAtLeast(140).coerceAtMost(320)
}
val restoredForward = (savedState.currentSize + if (savedState.isGroup) 24 else 16).coerceAtMost(restoredMaxSize)
val restoredBack = if (savedState.isGroup) 180 else 120
initParams.setObjectField("mStartingType", enumConstants.firstOrNull { it.toString() == "MESSAGE" } ?: return@hook)
initParams.setObjectField("mStartingOrderKey", savedState.oldestOrderKey ?: savedState.newestOrderKey)
initParams.setObjectField("mMaxSize", restoredMaxSize)
initParams.setObjectField("mNumMessagesForward", restoredForward)
initParams.setObjectField("mNumMessagesBack", restoredBack)
val warmupAmount = if (savedState.isGroup) REOPEN_WARMUP_GROUP_MESSAGES else REOPEN_WARMUP_DM_MESSAGES
val oldestKey = savedState.oldestOrderKey
if (oldestKey != null) {
context.feature(Messaging::class).conversationManager?.fetchConversationWithMessagesPaginated(
conversationId = conversationId,
lastMessageId = oldestKey,
amount = warmupAmount,
onSuccess = {},
onError = {}
)
}
}
}
}.onFailure {
context.log.error("Failed to install saved message window restore hooks", it, "PerformanceMode")
}
context.mappings.useMapper(CallbackMapper::class) {
callbacks.getClass("MessageWindowManagerDelegate")?.hook("onWindowUpdated", HookStage.AFTER) { param ->
if (!isMaxProfile) return@hook
val conversationId = runCatching { SnapUUID(param.arg(0)).toString() }.getOrNull() ?: return@hook
val update = param.arg<Any>(2)
val pagination = update.getObjectField("mPagination") ?: return@hook
val currentSize = pagination.getObjectField("mCurrentSize") as? Int ?: return@hook
val oldestOrderKey = pagination.getObjectField("mOldestOrderKey") as? Long
val newestOrderKey = pagination.getObjectField("mNewestOrderKey") as? Long
val conversationType = context.database.getConversationType(conversationId) ?: 0
val isGroup = conversationType == 1
synchronized(messageWindowStates) {
messageWindowStates[conversationId] = MessageWindowState(
conversationId = conversationId,
currentSize = currentSize.coerceAtMost(if (isGroup) 420 else 260),
oldestOrderKey = oldestOrderKey,
newestOrderKey = newestOrderKey,
updatedAt = System.currentTimeMillis(),
isGroup = isGroup
)
while (messageWindowStates.size > MESSAGE_WINDOW_STATE_MAX_CONVERSATIONS) {
val eldestKey = messageWindowStates.entries.minByOrNull { it.value.updatedAt }?.key ?: break
messageWindowStates.remove(eldestKey)
}
persistMessageWindowStates()
}
}
}
runCatching { runCatching {
findClass("io.requery.android.database.sqlite.SQLiteDatabase").hook("rawQueryWithFactory", HookStage.BEFORE) { param -> findClass("io.requery.android.database.sqlite.SQLiteDatabase").hook("rawQueryWithFactory", HookStage.BEFORE) { param ->
if (!isMaxProfile) return@hook if (!isMaxProfile) return@hook
val sql = param.argNullable<String>(1) ?: return@hook val sql = param.argNullable<String>(1) ?: return@hook
if (!isChatFeedQuery(sql)) return@hook if (!isChatFeedQuery(sql)) return@hook
if (chatFeedSnapshotServedThisProcess.get()) return@hook
readSnapshot(chatFeedSnapshotFile)?.let { snapshot -> readSnapshot(chatFeedSnapshotFile)?.let { snapshot ->
param.setResult(snapshotToMatrixCursor(snapshot)) param.setResult(snapshotToMatrixCursor(snapshot))
chatFeedCacheServeLog("rows=${snapshot.rows.size} file=${chatFeedSnapshotFile.name}") chatFeedSnapshotServedThisProcess.set(true)
} }
} }
@@ -479,15 +618,18 @@ class PerformanceMode : Feature("Performance Mode") {
if (!isMaxProfile) return@hook if (!isMaxProfile) return@hook
val sql = param.argNullable<String>(1) ?: return@hook val sql = param.argNullable<String>(1) ?: return@hook
if (!isChatFeedQuery(sql)) return@hook if (!isChatFeedQuery(sql)) return@hook
if (chatFeedSnapshotFile.exists()) return@hook
val cursor = param.getResult() as? Cursor ?: return@hook val cursor = param.getResult() as? Cursor ?: return@hook
val now = System.currentTimeMillis()
if (now - lastChatFeedSnapshotWrite.get() < CHAT_FEED_CACHE_MIN_REFRESH_INTERVAL_MS) return@hook
val snapshot = snapshotFromCursor(cursor) val snapshot = snapshotFromCursor(cursor)
if (snapshot.rows.isEmpty()) return@hook
writeSnapshot(chatFeedSnapshotFile, snapshot) writeSnapshot(chatFeedSnapshotFile, snapshot)
param.setResult(snapshotToMatrixCursor(snapshot)) lastChatFeedSnapshotWrite.set(now)
runCatching { cursor.close() }
chatFeedCacheRefreshLog("rows=${snapshot.rows.size} file=${chatFeedSnapshotFile.name}")
} }
}.onFailure { }.onFailure {
context.log.error("Failed to install chat feed cache hooks", it, "PerformanceMode") context.log.error("Failed to install chat feed cache hooks", it, "PerformanceMode")
} }
} }
} }

View File

@@ -30,6 +30,7 @@ class ConversationManager(
private val fetchConversationWithMessagesMethod by lazy { findMethodByName("fetchConversationWithMessages") } private val fetchConversationWithMessagesMethod by lazy { findMethodByName("fetchConversationWithMessages") }
private val fetchMessageByServerId by lazy { findMethodByName("fetchMessageByServerId") } private val fetchMessageByServerId by lazy { findMethodByName("fetchMessageByServerId") }
private val fetchMessagesByServerIds by lazy { findMethodByName("fetchMessagesByServerIds") } private val fetchMessagesByServerIds by lazy { findMethodByName("fetchMessagesByServerIds") }
private val fetchPrefetchableMessagesForConversationsMethod by lazy { findMethodByName("fetchPrefetchableMessagesForConversations") }
private val displayedMessagesMethod by lazy { findMethodByName("displayedMessages") } private val displayedMessagesMethod by lazy { findMethodByName("displayedMessages") }
private val fetchMessage by lazy { findMethodByName("fetchMessage") } private val fetchMessage by lazy { findMethodByName("fetchMessage") }
private val clearConversation by lazy { findMethodByName("clearConversation") } private val clearConversation by lazy { findMethodByName("clearConversation") }
@@ -163,6 +164,37 @@ class ConversationManager(
) )
} }
fun fetchPrefetchableMessagesForConversations(
conversationIds: List<String>,
strategyName: String,
messagesPerConversation: Int,
onSuccess: (List<Message>) -> Unit = {},
onError: (error: String) -> Unit = {}
) {
val prefetchRequestClass = fetchPrefetchableMessagesForConversationsMethod.parameterTypes.firstOrNull {
it.name == "com.snapchat.client.messaging.PrefetchRequest"
} ?: error("PrefetchRequest parameter type not found")
val strategyClass = context.androidContext.classLoader.loadClass("com.snapchat.client.messaging.PrefetchStrategy")
val strategy = strategyClass.enumConstants?.firstOrNull { it.toString() == strategyName }
?: error("PrefetchStrategy $strategyName not found")
val prefetchRequest = prefetchRequestClass
.getConstructor(strategyClass, Int::class.javaPrimitiveType)
.newInstance(strategy, messagesPerConversation)
fetchPrefetchableMessagesForConversationsMethod.invoke(
instanceNonNull(),
conversationIds.map { it.toSnapUUID().instanceNonNull() }.toCollection(ArrayList()),
prefetchRequest,
CallbackBuilder(getCallbackClass("FetchMessagesCallback"))
.override("onFetchMessagesComplete") { param ->
onSuccess(param.arg<List<*>>(0).map { Message(it) })
}
.override("onError") {
onError(it.arg<Any>(0).toString())
}.build()
)
}
fun clearConversation(conversationId: String, onSuccess: () -> Unit, onError: (error: String) -> Unit) { fun clearConversation(conversationId: String, onSuccess: () -> Unit, onError: (error: String) -> Unit) {
val callback = CallbackBuilder(getCallbackClass("Callback")) val callback = CallbackBuilder(getCallbackClass("Callback"))
.override("onSuccess") { onSuccess() } .override("onSuccess") { onSuccess() }

View File

@@ -19,6 +19,14 @@ fun UUID.toBytes(): ByteArray =
class SnapUUID( class SnapUUID(
private val obj: Any? private val obj: Any?
) : AbstractWrapper(obj) { ) : AbstractWrapper(obj) {
private fun extractUuidBytesFromObject(any: Any): ByteArray? {
runCatching { any.getObjectField("mId") as? ByteArray }.getOrNull()?.let { return it }
runCatching { any.javaClass.getMethod("getId").invoke(any) as? ByteArray }.getOrNull()?.let { return it }
runCatching { any.javaClass.getMethod("getUuid").invoke(any) as? ByteArray }.getOrNull()?.let { return it }
runCatching { any.javaClass.getMethod("uuid").invoke(any) as? ByteArray }.getOrNull()?.let { return it }
return null
}
private val uuidBytes by lazy { private val uuidBytes by lazy {
when { when {
obj is String -> { obj is String -> {
@@ -38,6 +46,10 @@ class SnapUUID(
any.getObjectField("mId") as ByteArray any.getObjectField("mId") as ByteArray
} }
} }
obj is Any -> {
extractUuidBytesFromObject(obj)
?: runCatching { UUID.fromString(obj.toString()).toBytes() }.getOrElse { ByteArray(16) }
}
else -> ByteArray(16) else -> ByteArray(16)
} }
} }

View File

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

View File

@@ -5,10 +5,16 @@ declare var _runtimeName: string;
export const runtimeName = _runtimeName; export const runtimeName = _runtimeName;
let remoteImports: any = null; let remoteImports: any = null;
try { for (const moduleName of ["DeviceBridge", "Device"]) {
remoteImports = require(_runtimeName + "_core/DeviceBridge")?.[_getImportsFunctionName]?.(); try {
} catch { const imports = require(_runtimeName + "_core/" + moduleName)?.[_getImportsFunctionName]?.();
remoteImports = null; if (imports != null) {
remoteImports = imports;
break;
}
} catch {
// Some Snapchat builds expose DeviceBridge, others only Device.
}
} }
function callRemoteFunction(method: string, ...args: any[]): any | null { function callRemoteFunction(method: string, ...args: any[]): any | null {