Revert: Performance Mode PR #126
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user