Revert: Performance Mode PR #126

This commit is contained in:
DarkKnight2122
2026-04-24 15:02:26 +05:30
parent 2be1c3258d
commit 0e549a1565
3 changed files with 119 additions and 693 deletions

View File

@@ -1,7 +1,6 @@
package me.eternal.purrfectsnap.common.config
import android.content.Context
import com.google.gson.JsonNull
import com.google.gson.JsonObject
import me.eternal.purrfectsnap.common.logger.AbstractLogger
import kotlin.reflect.KProperty
@@ -80,9 +79,7 @@ open class ConfigContainer(
properties.forEach { (propertyKey, propertyValue) ->
if (!exportSensitiveData && propertyKey.params.flags.contains(ConfigFlag.SENSITIVE)) return@forEach
if (!includeSavedLocations && propertyKey.dataType.type == DataProcessors.Type.MAP_COORDINATES) return@forEach
val serializedValue = propertyValue.getRaw()?.let {
propertyKey.dataType.serializeAny(it, exportSensitiveData, includeSavedLocations)
} ?: JsonNull.INSTANCE
val serializedValue = propertyValue.getRaw()?.let { propertyKey.dataType.serializeAny(it, exportSensitiveData, includeSavedLocations) }
json.add(propertyKey.name, serializedValue)
}
return json

View File

@@ -1,6 +1,5 @@
package me.eternal.purrfectsnap.core.features.impl.messaging
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.os.Build
@@ -65,37 +64,15 @@ import kotlin.time.toDuration
class SendOverride : Feature("Send Override") {
companion object {
private const val NOTIFICATION_CHANNEL_ID = "scheduled_send"
private const val CONTINUOUS_SEND_CHANNEL_ID = "continuous_send_status"
private const val STATUS_NOTIFICATION_ID = 54322
private const val COMPLETION_NOTIFICATION_ID = 54323
const val ACTION_PAUSE_RESUME = "me.eternal.purrfectsnap.CONTINUOUS_SEND_PAUSE_RESUME"
const val ACTION_STOP = "me.eternal.purrfectsnap.CONTINUOUS_SEND_STOP"
private val internalMultipartSend = ThreadLocal.withInitial { false }
private var queuedOriginalItemRepeatCount = 0
private var queuedOriginalItemRepeatOverrideType: String? = null
private var queuedOriginalItemRepeatSnapDurationMs: Int? = null
// Notification & Loop Tracking
private var totalRepeatCount = 0
private var processedRepeatCount = 0
private var currentRecipientName: String = "Unknown"
private val isPaused = java.util.concurrent.atomic.AtomicBoolean(false)
private val isStopped = java.util.concurrent.atomic.AtomicBoolean(false)
// State persistence for background operations
private var lastCapturedDestinationsObj: Any? = null
private var lastCapturedMessageContentJson: String? = null
private var lastCapturedOriginalCallback: Any? = null
private fun queueOriginalItemRepeats(repeatCount: Int, overrideType: String, snapDurationMs: Int?, destinations: Any, contentJson: String, originalCallback: Any?) {
private fun queueOriginalItemRepeats(repeatCount: Int, overrideType: String, snapDurationMs: Int?) {
queuedOriginalItemRepeatCount = repeatCount
queuedOriginalItemRepeatOverrideType = overrideType
queuedOriginalItemRepeatSnapDurationMs = snapDurationMs
lastCapturedDestinationsObj = destinations
lastCapturedMessageContentJson = contentJson
lastCapturedOriginalCallback = originalCallback
MediaFilePicker.setQueuedOverrideType(overrideType, snapDurationMs)
}
@@ -103,68 +80,29 @@ class SendOverride : Feature("Send Override") {
queuedOriginalItemRepeatCount = 0
queuedOriginalItemRepeatOverrideType = null
queuedOriginalItemRepeatSnapDurationMs = null
totalRepeatCount = 0
processedRepeatCount = 0
isPaused.set(false)
isStopped.set(false)
lastCapturedDestinationsObj = null
lastCapturedMessageContentJson = null
lastCapturedOriginalCallback = null
}
}
private val engineActive = java.util.concurrent.atomic.AtomicBoolean(true)
private fun updateContinuousSendNotification() {
if (!engineActive.get() || isStopped.get()) return
val notificationManager = context.androidContext.getSystemService(NotificationManager::class.java)
val remaining = queuedOriginalItemRepeatCount
val processed = processedRepeatCount
val total = totalRepeatCount
val isWorking = (remaining > 0 || (total > 0 && processed < total)) && !isStopped.get()
if (!isWorking) {
notificationManager.cancel(STATUS_NOTIFICATION_ID)
showCompletionNotification(processed, total)
return
}
val progressPercent = if (total > 0) (processed * 100) / total else 0
val builder = Notification.Builder(context.androidContext, CONTINUOUS_SEND_CHANNEL_ID)
.setOngoing(true)
.setOnlyAlertOnce(true)
.setSmallIcon(if (isPaused.get()) android.R.drawable.ic_media_pause else android.R.drawable.ic_media_play)
.setContentTitle("Sending Snaps to $currentRecipientName")
.setContentText("Progress: $processed / $total ($progressPercent%)")
.setSubText("$processed / $total")
.setProgress(total, processed, false)
private fun handleQueuedOriginalItemRepeatSuccess(): Boolean {
if (queuedOriginalItemRepeatCount <= 0) {
clearQueuedOriginalItemRepeats()
return false
}
val pauseResumeLabel = if (isPaused.get()) "Resume" else "Pause"
builder.addAction(Notification.Action.Builder(null, pauseResumeLabel, createPendingIntent(ACTION_PAUSE_RESUME)).build())
builder.addAction(Notification.Action.Builder(null, "Stop", createPendingIntent(ACTION_STOP)).build())
val overrideType = queuedOriginalItemRepeatOverrideType ?: run {
clearQueuedOriginalItemRepeats()
return false
}
val snapDurationMs = queuedOriginalItemRepeatSnapDurationMs
notificationManager.notify(STATUS_NOTIFICATION_ID, builder.build())
}
private fun showCompletionNotification(sent: Int, total: Int) {
val title = if (isStopped.get()) "Continuous Send Stopped" else "Continuous Send Finished"
val content = "Successfully sent $sent / $total snaps to $currentRecipientName"
val notificationManager = context.androidContext.getSystemService(NotificationManager::class.java)
val builder = Notification.Builder(context.androidContext, CONTINUOUS_SEND_CHANNEL_ID)
.setSmallIcon(android.R.drawable.ic_dialog_info)
.setContentTitle(title)
.setContentText(content)
.setAutoCancel(true)
notificationManager.notify(COMPLETION_NOTIFICATION_ID, builder.build())
}
private fun createPendingIntent(action: String): android.app.PendingIntent {
val intent = android.content.Intent(action).setPackage(context.androidContext.packageName)
return android.app.PendingIntent.getBroadcast(context.androidContext, action.hashCode(), intent, android.app.PendingIntent.FLAG_UPDATE_CURRENT or android.app.PendingIntent.FLAG_IMMUTABLE)
queuedOriginalItemRepeatCount--
MediaFilePicker.setQueuedOverrideType(overrideType, snapDurationMs)
val result = MediaFilePicker.sendReusableOriginalItem()
if (!result) {
queuedOriginalItemRepeatCount++
clearQueuedOriginalItemRepeats()
}
return result
}
}
private var selectedType by mutableStateOf("SNAP")
@@ -258,33 +196,6 @@ class SendOverride : Feature("Send Override") {
@OptIn(ExperimentalLayoutApi::class)
override fun init() {
createNotificationChannel()
val actionReceiver = object : android.content.BroadcastReceiver() {
override fun onReceive(ctx: android.content.Context?, intent: android.content.Intent?) {
when (intent?.action) {
ACTION_PAUSE_RESUME -> {
isPaused.set(!isPaused.get())
updateContinuousSendNotification()
}
ACTION_STOP -> {
isStopped.set(true)
if (isPaused.get()) {
isPaused.set(false)
}
updateContinuousSendNotification()
}
}
}
}
val filter = android.content.IntentFilter().apply {
addAction(ACTION_PAUSE_RESUME)
addAction(ACTION_STOP)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
context.androidContext.registerReceiver(actionReceiver, filter, android.content.Context.RECEIVER_NOT_EXPORTED)
} else {
context.androidContext.registerReceiver(actionReceiver, filter)
}
val stripMediaMetadata = context.config.messaging.stripMediaMetadata.get()
var postSavePolicy: Int? = null
@@ -798,13 +709,13 @@ class SendOverride : Feature("Send Override") {
}
}
fun invokeSendManually(destinations: MessageDestinations, messageContent: MessageContent, callback: Any?) {
fun invokeSendManually(messageContent: MessageContent, callback: Any?) {
val conversationManager = conversationManagerInstance ?: error("ConversationManager is null")
internalMultipartSend.set(true)
try {
sendMessageWithContentMethod.invoke(
conversationManager,
cloneDestinations(destinations),
cloneDestinations(event.destinations),
messageContent.instanceNonNull(),
callback
)
@@ -814,7 +725,6 @@ class SendOverride : Feature("Send Override") {
}
fun sendMediaManual(
destinations: MessageDestinations,
sourceMessageContent: MessageContent,
overrideType: String,
snapDurationMs: Int?,
@@ -874,7 +784,7 @@ class SendOverride : Feature("Send Override") {
.build()
}
invokeSendManually(destinations, partContent, callback)
invokeSendManually(partContent, callback)
}
sendPart(0)
@@ -884,12 +794,11 @@ class SendOverride : Feature("Send Override") {
postSavePolicy = null
val targetReader = ProtoReader(sourceMessageContent.content ?: return false)
if (!applyOverride(sourceMessageContent, targetReader, overrideType, snapDurationMs)) return false
invokeSendManually(destinations, sourceMessageContent, completionCallback)
invokeSendManually(sourceMessageContent, completionCallback)
return true
}
fun sendRepeatedMediaManual(
destinations: MessageDestinations,
repeatCount: Int,
overrideType: String,
snapDurationMs: Int?
@@ -911,7 +820,7 @@ class SendOverride : Feature("Send Override") {
}
val preparedContent = createMessageContentFromOriginal()
if (!sendMediaManual(destinations, preparedContent, overrideType, snapDurationMs, callback)) {
if (!sendMediaManual(preparedContent, overrideType, snapDurationMs, callback)) {
invokeCallbackError(originalCallback, "Failed to send")
}
}
@@ -934,76 +843,12 @@ class SendOverride : Feature("Send Override") {
10000
}
fun handleQueuedOriginalItemRepeatSuccess(convId: String): Boolean {
if (isStopped.get() || queuedOriginalItemRepeatCount <= 0) {
val processed = processedRepeatCount
val total = totalRepeatCount
clearQueuedOriginalItemRepeats()
context.runOnUiThread {
val notificationManager = context.androidContext.getSystemService(NotificationManager::class.java)
notificationManager.cancel(STATUS_NOTIFICATION_ID)
showCompletionNotification(processed, total)
}
return false
}
val destinations = lastCapturedDestinationsObj as? MessageDestinations ?: return false
val contentJson = lastCapturedMessageContentJson ?: return false
val originalCb = lastCapturedOriginalCallback
val overrideType = queuedOriginalItemRepeatOverrideType ?: "SNAP"
val snapDurationMs = queuedOriginalItemRepeatSnapDurationMs
context.coroutineScope.launch {
while (isPaused.get() && !isStopped.get()) {
delay(500)
}
if (isStopped.get()) {
context.runOnUiThread { handleQueuedOriginalItemRepeatSuccess(convId) }
return@launch
}
delay(1000)
context.runOnUiThread {
queuedOriginalItemRepeatCount--
processedRepeatCount++
updateContinuousSendNotification()
val repeatedContent = createMessageContentFromOriginal()
val callback = CallbackBuilder(sendMessageCallbackClass)
.override("onSuccess") {
context.runOnUiThread {
if (!handleQueuedOriginalItemRepeatSuccess(convId)) {
runCatching {
originalCb?.javaClass?.methods?.firstOrNull { it.name == "onSuccess" }?.invoke(originalCb)
}
}
}
}
.override("onError", shouldUnhook = false) {
val error = it.argNullable<Any>(0)
runCatching {
originalCb?.javaClass?.methods?.firstOrNull { it.name == "onError" && it.parameterCount == 1 }?.invoke(originalCb, error)
}
clearQueuedOriginalItemRepeats()
val notificationManager = context.androidContext.getSystemService(NotificationManager::class.java)
notificationManager.cancel(STATUS_NOTIFICATION_ID)
}
.build()
sendMediaManual(destinations, repeatedContent, overrideType, snapDurationMs, callback)
}
}
return true
}
fun attachQueuedRepeatCallbacks(sendEvent: SendMessageWithContentEvent) {
sendEvent.addCallbackResult("onSuccess") {
context.runOnUiThread {
val handledSplit = MediaFilePicker.handleCurrentQueuedItemSuccess()
val handledRepeat = if (!handledSplit) {
handleQueuedOriginalItemRepeatSuccess(conversationIds.first())
handleQueuedOriginalItemRepeatSuccess()
} else {
false
}
@@ -1529,7 +1374,6 @@ class SendOverride : Feature("Send Override") {
context.bridgeClient.getTaskInterface().updateTaskProgress(taskHash, "Sending...", 100)
if (sendRepeatedMediaManual(
MessageDestinations(cloneDestinations(event.destinations)),
repeatCount,
finalSelectedType,
selectedSnapDurationMs
@@ -1585,19 +1429,7 @@ class SendOverride : Feature("Send Override") {
invokeOriginalAndRestoreResult(event)
}
} else if (MediaFilePicker.hasReusableOriginalItem()) {
totalRepeatCount = repeatCount
processedRepeatCount = 1
currentRecipientName = recipientNameForTask
updateContinuousSendNotification()
queueOriginalItemRepeats(
repeatCount - 1,
finalSelectedType,
selectedSnapDurationMs,
MessageDestinations(cloneDestinations(event.destinations)),
originalMessageJson,
originalCallback
)
queueOriginalItemRepeats(repeatCount - 1, finalSelectedType, selectedSnapDurationMs)
attachQueuedRepeatCallbacks(event)
if (sendMedia(finalSelectedType, selectedSnapDurationMs)) {
invokeOriginalAndRestoreResult(event)
@@ -1606,7 +1438,6 @@ class SendOverride : Feature("Send Override") {
}
} else {
sendRepeatedMediaManual(
MessageDestinations(cloneDestinations(event.destinations)),
repeatCount,
finalSelectedType,
selectedSnapDurationMs

View File

@@ -2,85 +2,31 @@ package me.eternal.purrfectsnap.core.features.impl.tweaks
import android.animation.ValueAnimator
import android.app.Activity
import android.app.Dialog
import android.content.Context
import android.database.Cursor
import android.database.MatrixCursor
import android.database.sqlite.SQLiteDatabase
import android.hardware.camera2.CaptureRequest
import android.media.MediaRecorder
import android.os.Build
import android.transition.Transition
import android.os.HandlerThread
import android.os.Process
import android.util.Base64
import android.util.Range
import android.view.View
import android.view.ViewPropertyAnimator
import android.view.animation.Animation
import android.widget.OverScroller
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.StaggeredGridLayoutManager
import java.io.File
import java.lang.Thread
import java.lang.reflect.Method
import java.util.LinkedHashMap
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicLong
import java.util.concurrent.ThreadPoolExecutor
import com.google.gson.reflect.TypeToken
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.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.findRestrictedMethod
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.setObjectField
import okhttp3.Dispatcher
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 CHAT_FEED_CACHE_MAX_AGE_MS = 5L * 60L * 1000L
private const val CHAT_FEED_CACHE_SCHEMA_VERSION = 2
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(
val type: Int,
val stringValue: String? = null,
val longValue: Long? = null,
val doubleValue: Double? = null,
val blobValue: String? = null,
)
private data class CursorSnapshot(
val columns: List<String>,
val rows: List<List<SnapshotCell>>,
)
private data class ChatFeedSnapshotCache(
val schemaVersion: Int,
val queryKey: String,
val createdAt: Long,
val snapshot: CursorSnapshot,
)
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() {
val profile = context.config.global.performanceMode.profile.getNullable() ?: return
val isMaxProfile = profile == "max"
@@ -90,7 +36,6 @@ class PerformanceMode : Feature("Performance Mode") {
Process.THREAD_PRIORITY_MORE_FAVORABLE
}
val minimumFrameRate = if (isMaxProfile) 60 else 45
val minimumRecordingFrameRate = if (isMaxProfile) 30 else 24
val durationScale = if (isMaxProfile) 0.35f else 0.55f
val recyclerViewCacheSize = if (isMaxProfile) 64 else 32
val maxRequests = if (isMaxProfile) 192 else 96
@@ -98,16 +43,11 @@ class PerformanceMode : Feature("Performance Mode") {
val minimumCoreThreads = if (isMaxProfile) 16 else 8
val prefetchItemCount = if (isMaxProfile) 24 else 12
val maxAnimationDurationMs = if (isMaxProfile) 90L else 140L
val maxScrollDurationMs = if (isMaxProfile) 72 else 180
val maxScrollDurationMs = if (isMaxProfile) 120 else 180
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(
"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",
"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",
"PerformanceMode"
)
@@ -131,217 +71,28 @@ class PerformanceMode : Feature("Performance Mode") {
val executorLog = firstHitLogger("ThreadPoolExecutor.constructor")
val dispatcherLog = firstHitLogger("OkHttp.Dispatcher.constructor")
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 recyclerAdapterLog = firstHitLogger("RecyclerView.setAdapter")
val recyclerLayoutManagerLog = firstHitLogger("RecyclerView.setLayoutManager")
val sqliteOpenLog = firstHitLogger("SQLiteDatabase.openDatabase")
val sqliteCreateLog = firstHitLogger("SQLiteDatabase.openOrCreateDatabase")
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 mapDialogLog = firstHitLogger("Dialog.show")
val mapViewLog = firstHitLogger("MapView.constructor")
val mapboxNetworkBlockLog = firstHitLogger("SnapMap.telemetryBlock")
val mapCameraAnimLog = firstHitLogger("SnapMap.mapAnimatorDuration")
val mapThreadLog = firstHitLogger("SnapMap.mapThread")
val mapRendererFpsLog = firstHitLogger("SnapMap.mapRendererFps")
val mapTransitionLog = firstHitLogger("SnapMap.transitionOptions")
val mapMoveLog = firstHitLogger("SnapMap.moveDuration")
fun isPerformanceSensitiveThread(name: String?): Boolean {
val normalizedName = name?.lowercase() ?: return false
return listOf("codec", "transcod", "feed", "story", "opera", "messag", "network", "db", "disk", "map", "mapbox", "snapmap", "viewport").any {
return listOf("camera", "preview", "codec", "render", "gl", "transcod", "lens", "feed", "story", "opera", "messag", "network", "db", "disk").any {
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 chatFeedSnapshotFile = File(performanceCacheDir, "chat_feed_snapshot.json")
val lastChatFeedSnapshotWrite = AtomicLong(0L)
val chatFeedSnapshotServedThisProcess = AtomicBoolean(false)
fun invalidateChatFeedSnapshot(reason: String) {
val deleted = runCatching {
if (!chatFeedSnapshotFile.exists()) return@runCatching false
chatFeedSnapshotFile.delete()
}.getOrDefault(false)
chatFeedSnapshotServedThisProcess.set(false)
if (deleted) {
context.log.info("Invalidated chat feed snapshot ($reason)", "PerformanceMode")
}
}
Activity::class.java.hook("onResume", HookStage.AFTER) {
if (!isMaxProfile) return@hook
chatFeedSnapshotServedThisProcess.set(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")
}
}
val snapshotQueryWhitespaceRegex = Regex("\\s+")
fun buildChatFeedSnapshotQueryKey(sql: String): String {
return sql.lowercase()
.replace(snapshotQueryWhitespaceRegex, " ")
.trim()
}
fun isChatFeedQuery(sql: String): Boolean {
val normalized = buildChatFeedSnapshotQueryKey(sql)
if (!normalized.startsWith("select ")) return false
val isFriendsFeedViewQuery =
normalized.startsWith("select * from friendsfeedview ") &&
normalized.contains(" order by _id ") &&
normalized.contains(" limit ")
val isFeedEntryQuery =
normalized.startsWith("select * from feed_entry ") &&
normalized.contains(" order by last_updated_timestamp desc ") &&
normalized.contains(" limit ")
return (isFriendsFeedViewQuery || isFeedEntryQuery) &&
!normalized.contains("count(") &&
!normalized.contains("select 0") &&
!normalized.contains("where key = ?") &&
!normalized.contains("where client_conversation_id = ?")
}
fun cursorCell(cursor: Cursor, index: Int): SnapshotCell {
return when (cursor.getType(index)) {
Cursor.FIELD_TYPE_NULL -> SnapshotCell(Cursor.FIELD_TYPE_NULL)
Cursor.FIELD_TYPE_INTEGER -> SnapshotCell(Cursor.FIELD_TYPE_INTEGER, longValue = cursor.getLong(index))
Cursor.FIELD_TYPE_FLOAT -> SnapshotCell(Cursor.FIELD_TYPE_FLOAT, doubleValue = cursor.getDouble(index))
Cursor.FIELD_TYPE_STRING -> SnapshotCell(Cursor.FIELD_TYPE_STRING, stringValue = cursor.getString(index))
Cursor.FIELD_TYPE_BLOB -> SnapshotCell(
Cursor.FIELD_TYPE_BLOB,
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))
}
}
fun snapshotFromCursor(cursor: Cursor): CursorSnapshot? {
val originalPosition = cursor.position
val snapshot = runCatching {
val columns = cursor.columnNames.toList()
val rows = mutableListOf<List<SnapshotCell>>()
if (cursor.moveToFirst()) {
var rowCount = 0
do {
rows += columns.indices.map { index -> cursorCell(cursor, index) }
rowCount++
} while (rowCount < CHAT_FEED_CACHE_MAX_ROWS && cursor.moveToNext())
}
CursorSnapshot(columns, rows)
}.onFailure {
context.log.error("Failed to snapshot chat feed cursor", it, "PerformanceMode")
}.getOrNull()
runCatching { cursor.moveToPosition(originalPosition) }
val restoredPosition = runCatching { cursor.position }.getOrNull()
if (restoredPosition != originalPosition) {
context.log.warn(
"Skipping chat feed snapshot write due non-restorable cursor position (from=$originalPosition to=${restoredPosition ?: "unknown"})",
"PerformanceMode"
)
return null
}
return snapshot
}
fun snapshotToMatrixCursor(snapshot: CursorSnapshot): MatrixCursor {
return MatrixCursor(snapshot.columns.toTypedArray(), snapshot.rows.size).also { matrixCursor ->
snapshot.rows.forEach { row ->
matrixCursor.addRow(row.map { cell ->
when (cell.type) {
Cursor.FIELD_TYPE_NULL -> null
Cursor.FIELD_TYPE_INTEGER -> cell.longValue
Cursor.FIELD_TYPE_FLOAT -> cell.doubleValue
Cursor.FIELD_TYPE_BLOB -> cell.blobValue?.let { Base64.decode(it, Base64.NO_WRAP) }
else -> cell.stringValue
}
})
}
}
}
fun readSnapshot(file: File, expectedQueryKey: String): CursorSnapshot? {
return runCatching {
if (!file.exists()) return null
val cache = context.gson.fromJson(file.readText(Charsets.UTF_8), ChatFeedSnapshotCache::class.java) ?: return null
if (cache.schemaVersion != CHAT_FEED_CACHE_SCHEMA_VERSION) {
runCatching { file.delete() }
return null
}
if (cache.queryKey != expectedQueryKey) {
runCatching { file.delete() }
return null
}
if (System.currentTimeMillis() - cache.createdAt > CHAT_FEED_CACHE_MAX_AGE_MS) {
runCatching { file.delete() }
return null
}
cache.snapshot
}.getOrElse {
runCatching { file.delete() }
null
}
}
fun writeSnapshot(file: File, queryKey: String, snapshot: CursorSnapshot) {
runCatching {
file.writeText(
context.gson.toJson(
ChatFeedSnapshotCache(
schemaVersion = CHAT_FEED_CACHE_SCHEMA_VERSION,
queryKey = queryKey,
createdAt = System.currentTimeMillis(),
snapshot = snapshot,
)
),
Charsets.UTF_8
)
}.onFailure {
context.log.error("Failed to persist friend list snapshot", it, "PerformanceMode")
}
}
context.event.subscribe(NetworkApiRequestEvent::class) { event ->
if (!isMaxProfile) return@subscribe
val url = event.url
if (url.contains("ami/friends")) {
invalidateChatFeedSnapshot("friends-mutation-sync")
}
if (url.contains("mapbox") && (url.contains("events.") || url.contains("telemetry"))) {
event.canceled = true
mapboxNetworkBlockLog("url=$url")
}
}
HandlerThread::class.java.hookConstructor(HookStage.BEFORE) { param ->
if (param.args().size < 2) return@hookConstructor
val threadName = param.argNullable<String>(0)
@@ -366,12 +117,9 @@ class PerformanceMode : Feature("Performance Mode") {
val thread = param.thisObject<Thread>()
if (!isPerformanceSensitiveThread(thread.name)) return@hook
runCatching {
thread.priority = preferredJavaThreadPriority
thread.priority = Thread.MAX_PRIORITY
}
threadStartLog("name=${thread.name} priority=${thread.priority}")
if ((thread.name ?: "").contains("map", ignoreCase = true) || (thread.name ?: "").contains("mapbox", ignoreCase = true)) {
mapThreadLog("name=${thread.name} priority=${thread.priority}")
}
}
ThreadPoolExecutor::class.java.hookConstructor(HookStage.AFTER) { param ->
@@ -401,10 +149,47 @@ class PerformanceMode : Feature("Performance Mode") {
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)}")
}
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 ->
val recyclerView = param.thisObject<RecyclerView>()
recyclerView.setItemViewCacheSize(recyclerViewCacheSize)
recyclerView.overScrollMode = View.OVER_SCROLL_NEVER
recyclerView.setLayerType(View.LAYER_TYPE_HARDWARE, null)
if (isMaxProfile) {
recyclerView.itemAnimator = null
}
@@ -414,6 +199,7 @@ class PerformanceMode : Feature("Performance Mode") {
RecyclerView::class.java.hook("setAdapter", HookStage.AFTER) { param ->
val recyclerView = param.thisObject<RecyclerView>()
recyclerView.setItemViewCacheSize(recyclerViewCacheSize)
recyclerView.setLayerType(View.LAYER_TYPE_HARDWARE, null)
if (isMaxProfile) {
recyclerView.itemAnimator = null
}
@@ -433,6 +219,7 @@ class PerformanceMode : Feature("Performance Mode") {
layoutManager.gapStrategy = StaggeredGridLayoutManager.GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS
}
}
recyclerView.setLayerType(View.LAYER_TYPE_HARDWARE, null)
recyclerLayoutManagerLog("layoutManager=${layoutManager?.javaClass?.name} prefetch=$prefetchItemCount")
}
@@ -461,11 +248,8 @@ class PerformanceMode : Feature("Performance Mode") {
MediaRecorder::class.java.hook("setVideoFrameRate", HookStage.BEFORE) { param ->
val currentRate = param.arg<Int>(0)
val applied = currentRate
.coerceAtLeast(minimumRecordingFrameRate)
.coerceAtMost(if (isMaxProfile) 60 else 45)
if (applied != currentRate) {
param.setArg(0, applied)
if (currentRate < minimumFrameRate) {
param.setArg(0, minimumFrameRate)
}
mediaRecorderLog("requested=$currentRate applied=${param.arg<Int>(0)}")
}
@@ -481,234 +265,48 @@ class PerformanceMode : Feature("Performance Mode") {
}
}
OverScroller::class.java.hook("fling", HookStage.BEFORE) { param ->
if (param.args().size >= 10) {
val overX = param.arg<Int>(8)
val overY = param.arg<Int>(9)
if (overX != 0) param.setArg(8, 0)
if (overY != 0) param.setArg(9, 0)
CaptureRequest.Builder::class.java.hook("set", HookStage.BEFORE) { param ->
val key = param.arg<CaptureRequest.Key<*>>(0)
when (key) {
CaptureRequest.EDGE_MODE -> param.setArg(1, CaptureRequest.EDGE_MODE_FAST)
CaptureRequest.NOISE_REDUCTION_MODE -> param.setArg(1, CaptureRequest.NOISE_REDUCTION_MODE_FAST)
CaptureRequest.HOT_PIXEL_MODE -> param.setArg(1, CaptureRequest.HOT_PIXEL_MODE_FAST)
CaptureRequest.COLOR_CORRECTION_ABERRATION_MODE -> param.setArg(1, CaptureRequest.COLOR_CORRECTION_ABERRATION_MODE_FAST)
CaptureRequest.CONTROL_AF_MODE -> param.setArg(1, CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE)
CaptureRequest.CONTROL_VIDEO_STABILIZATION_MODE -> param.setArg(1, CaptureRequest.CONTROL_VIDEO_STABILIZATION_MODE_OFF)
CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE -> {
val currentRange = param.argNullable<Any>(1) as? Range<*>
val lower = (currentRange?.lower as? Int) ?: minimumFrameRate
val upper = (currentRange?.upper as? Int) ?: minimumFrameRate
if (upper < minimumFrameRate) {
param.setArg(1, Range(lower.coerceAtMost(minimumFrameRate), minimumFrameRate))
}
}
}
captureRequestLog("key=${key.name} value=${param.argNullable<Any>(1)}")
}
fun applyActivityPerformanceTuning(activity: Activity) {
runCatching {
activity.window.setWindowAnimations(0)
activity.window.decorView.setLayerType(View.LAYER_TYPE_HARDWARE, null)
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}")
}
}
}
onNextActivityCreate {
applyActivityPerformanceTuning(it)
}
Dialog::class.java.hook("show", HookStage.AFTER) { param ->
val dialog = param.nullableThisObject<Any>() as? Dialog ?: return@hook
val window = dialog.window ?: return@hook
runCatching {
window.setWindowAnimations(0)
if (dialog::class.java.name.contains("map", ignoreCase = true) || dialog::class.java.name.contains("snap", ignoreCase = true)) {
mapDialogLog("class=${dialog::class.java.name}")
}
}
}
runCatching {
findClass("com.mapbox.mapboxsdk.maps.MapView").hookConstructor(HookStage.AFTER) { param ->
val mapView = param.nullableThisObject<Any>() as? View ?: return@hookConstructor
mapView.overScrollMode = View.OVER_SCROLL_NEVER
mapViewLog("class=${mapView::class.java.name}")
}
}
runCatching {
findClass("com.mapbox.mapboxsdk.maps.renderer.MapRenderer").hook("setMaximumFps", HookStage.BEFORE) { param ->
val requested = param.arg<Int>(0)
val applied = requested.coerceAtLeast(120)
if (applied != requested) {
param.setArg(0, applied)
}
mapRendererFpsLog("requested=$requested applied=${param.arg<Int>(0)}")
}
}
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 {
findClass("io.requery.android.database.sqlite.SQLiteDatabase").hook("rawQueryWithFactory", HookStage.BEFORE) { param ->
if (!isMaxProfile) return@hook
val sql = param.argNullable<String>(1) ?: return@hook
if (!isChatFeedQuery(sql)) return@hook
if (chatFeedSnapshotServedThisProcess.get()) return@hook
val queryKey = buildChatFeedSnapshotQueryKey(sql)
readSnapshot(chatFeedSnapshotFile, queryKey)?.let { snapshot ->
param.setResult(snapshotToMatrixCursor(snapshot))
chatFeedSnapshotServedThisProcess.set(true)
}
}
findClass("io.requery.android.database.sqlite.SQLiteDatabase").hook("rawQueryWithFactory", HookStage.AFTER) { param ->
if (!isMaxProfile) return@hook
val sql = param.argNullable<String>(1) ?: return@hook
if (!isChatFeedQuery(sql)) 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 queryKey = buildChatFeedSnapshotQueryKey(sql)
val snapshot = snapshotFromCursor(cursor) ?: return@hook
if (snapshot.rows.isEmpty()) return@hook
writeSnapshot(chatFeedSnapshotFile, queryKey, snapshot)
lastChatFeedSnapshotWrite.set(now)
}
}.onFailure {
context.log.error("Failed to install chat feed cache hooks", it, "PerformanceMode")
}
}
}