diff --git a/build.gradle.kts b/build.gradle.kts index baa81d55..963248c5 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -33,8 +33,8 @@ tasks.register("getVersion") { } // You can still set these for legacy use by submodules or scripts: -rootProject.ext.set("appVersionName", providers.gradleProperty("APP_VERSION_NAME").orElse("1.6.3").get()) -rootProject.ext.set("appVersionCode", providers.gradleProperty("APP_VERSION_CODE").orElse("316").get().toInt()) +rootProject.ext.set("appVersionName", providers.gradleProperty("APP_VERSION_NAME").orElse("1.6.4").get()) +rootProject.ext.set("appVersionCode", providers.gradleProperty("APP_VERSION_CODE").orElse("318").get().toInt()) rootProject.ext.set("applicationId", "me.eternal.purrfectsnap") // 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. diff --git a/changelogs-stable.txt b/changelogs-stable.txt index d7b2ddac..238336c7 100644 --- a/changelogs-stable.txt +++ b/changelogs-stable.txt @@ -1,3 +1,9 @@ +## v1.6.4 +- New: Performance mode feature!(Smooth & Max) +- Fix: Failed to init feature Device Spoofer for some devices +- Fix: Increase the default queue of auto open snaps +- Fix: user_conversation error spam + ## v1.6.3 - Fix: Blank Global Settings page - New: Backup & Restore option for Randomized Device Profile feature! diff --git a/common/src/main/assets/lang/en_US.json b/common/src/main/assets/lang/en_US.json index d5dd5452..27fbcfa4 100644 --- a/common/src/main/assets/lang/en_US.json +++ b/common/src/main/assets/lang/en_US.json @@ -1909,6 +1909,16 @@ "name": "Disable Metrics", "description": "Blocks sending specific analytic data to Snapchat" }, + "performance_mode": { + "name": "Performance Mode", + "description": "Applies an app-wide speed profile for navigation, preview loading, background work, and camera responsiveness", + "properties": { + "performance_profile": { + "name": "Performance Profile", + "description": "Select how aggressively PurrfectSnap pushes app-wide performance tuning" + } + } + }, "disable_story_sections": { "name": "Disable Story Sections", "description": "Removes sections from the Stories page\nMay require a refresh to work properly" @@ -3065,6 +3075,11 @@ "custom_image_upload_format": { "null": "Automatic" }, + "performance_profile": { + "smooth": "Smooth", + "max": "Max", + "null": "Disabled" + }, "update_check_frequency": { "daily": "Daily", "weekly": "Weekly", diff --git a/common/src/main/kotlin/me/eternal/purrfectsnap/common/config/impl/Global.kt b/common/src/main/kotlin/me/eternal/purrfectsnap/common/config/impl/Global.kt index 3b3f4ab7..26518d8b 100644 --- a/common/src/main/kotlin/me/eternal/purrfectsnap/common/config/impl/Global.kt +++ b/common/src/main/kotlin/me/eternal/purrfectsnap/common/config/impl/Global.kt @@ -38,9 +38,16 @@ class Global : ConfigContainer() { val customUploadImageFormat = unique("custom_image_upload_format", "jpeg", "png", "webp") { requireRestart(); addFlags(ConfigFlag.NO_TRANSLATE) } } + inner class PerformanceModeConfig : ConfigContainer() { + val profile = unique("performance_profile", "smooth", "max") { + requireRestart() + } + } + val betterLocation = container("better_location", BetterLocationConfig()) val snapchatPlus = unique("snapchat_plus", "not_subscribed", "basic", "ad_free") { requireRestart() } val mediaUploadQualityConfig = container("media_upload_quality", MediaUploadQualityConfig()) + val performanceMode = container("performance_mode", PerformanceModeConfig()) { requireRestart() } val disableConfirmationDialogs = multiple("disable_confirmation_dialogs", "erase_message", "remove_friend", "block_friend", "ignore_friend", "hide_friend", "hide_conversation", "clear_conversation") { requireRestart() } val disableMetrics = boolean("disable_metrics") { requireRestart() } val disableStorySections = multiple("disable_story_sections", "friends", "suggested_stories", "following", "discover") { requireRestart(); requireCleanCache() } diff --git a/common/src/main/kotlin/me/eternal/purrfectsnap/common/config/impl/MessagingTweaks.kt b/common/src/main/kotlin/me/eternal/purrfectsnap/common/config/impl/MessagingTweaks.kt index b5c06188..9b0f83e0 100644 --- a/common/src/main/kotlin/me/eternal/purrfectsnap/common/config/impl/MessagingTweaks.kt +++ b/common/src/main/kotlin/me/eternal/purrfectsnap/common/config/impl/MessagingTweaks.kt @@ -166,7 +166,7 @@ class MessagingTweaks : ConfigContainer() { val maxDelayMs = integer("max_delay_ms", defaultValue = 100) { inputCheck = { it.toIntOrNull()?.coerceAtLeast(0) != null && it.toInt() > minDelay.get() } } - val queueSize = integer("queue_size", defaultValue = 10) { + val queueSize = integer("queue_size", defaultValue = 1000) { inputCheck = { it.toIntOrNull()?.coerceAtLeast(1) != null } } val retryAttempts = integer("retry_attempts", defaultValue = 5) { diff --git a/core/src/main/kotlin/me/eternal/purrfectsnap/core/database/DatabaseAccess.kt b/core/src/main/kotlin/me/eternal/purrfectsnap/core/database/DatabaseAccess.kt index f81413f1..28a6e4ee 100644 --- a/core/src/main/kotlin/me/eternal/purrfectsnap/core/database/DatabaseAccess.kt +++ b/core/src/main/kotlin/me/eternal/purrfectsnap/core/database/DatabaseAccess.kt @@ -47,6 +47,14 @@ class DatabaseAccess( } == true } + private val hasArroyoUserConversationTable by lazy { + useDatabase(DatabaseType.ARROYO)?.performOperation { + safeRawQuery("SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'user_conversation'")?.use { query -> + query.moveToFirst() && query.getStringOrNull("name") == "user_conversation" + } + } == true + } + private fun useDatabase(database: DatabaseType, writeMode: Boolean = false): SQLiteDatabase? { // only cache read-only databases if (!writeMode && openedDatabases.containsKey(database) && openedDatabases[database]?.isOpen == true) { @@ -148,6 +156,10 @@ class DatabaseAccess( }?.toMutableMap() ?: mutableMapOf() } + if (!hasArroyoUserConversationTable) { + return@lazy mutableMapOf() + } + (useDatabase(DatabaseType.ARROYO)?.performOperation { safeRawQuery( "SELECT client_conversation_id, conversation_type, user_id FROM user_conversation WHERE user_id != ?", @@ -354,7 +366,7 @@ class DatabaseAccess( } fun getConversationType(conversationId: String): Int? { - if (hasArroyoConversationTable) { + if (hasArroyoConversationTable || !hasArroyoUserConversationTable) { return getFeedEntryByConversationId(conversationId)?.conversationType } @@ -372,7 +384,9 @@ class DatabaseAccess( } fun getDMConversationId(userId: String): String? { - if (hasArroyoConversationTable) { + friendDMsCache[userId]?.let { return it } + + if (hasArroyoConversationTable || !hasArroyoUserConversationTable) { return friendDMsCache[userId] } @@ -408,6 +422,10 @@ class DatabaseAccess( } } + if (!hasArroyoUserConversationTable) { + return getFeedEntryByConversationId(conversationId)?.participants + } + return useDatabase(DatabaseType.ARROYO)?.performOperation { safeRawQuery( "SELECT user_id FROM user_conversation WHERE client_conversation_id = ?", diff --git a/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/FeatureManager.kt b/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/FeatureManager.kt index f58f97f3..53bd9661 100644 --- a/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/FeatureManager.kt +++ b/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/FeatureManager.kt @@ -99,6 +99,7 @@ class FeatureManager( MeoPasscodeBypass(), AppLock(), CameraTweaks(), + PerformanceMode(), InfiniteStoryBoost(), PinConversations(), DeviceSpooferHook(), diff --git a/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/impl/ConfigurationOverride.kt b/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/impl/ConfigurationOverride.kt index e1b660e6..0d77ca14 100644 --- a/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/impl/ConfigurationOverride.kt +++ b/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/impl/ConfigurationOverride.kt @@ -37,11 +37,32 @@ class ConfigurationOverride : Feature("Configuration Override") { }.getOrNull() val propertyOverrides = mutableMapOf() + val loggedOverrides = mutableSetOf() fun overrideProperty(key: String, filter: (ConfigKeyInfo) -> Boolean, value: (ConfigKeyInfo) -> Any?, isAppExperiment: Boolean = false) { propertyOverrides[key] = ConfigFilter(filter, value, isAppExperiment) } + fun logPerformanceOverride(key: String, value: Any?) { + if (!key.contains("PRELOAD") && + !key.contains("PERFORMANCE") && + !key.contains("WARM") && + !key.contains("PREFETCH") && + !key.contains("LATENCY") && + !key.contains("ANALYTICS") && + !key.contains("THREAD_PRIORITY") && + !key.contains("HD_MODE") && + !key.contains("LENS") && + !key.contains("THUMBNAIL") + ) { + return + } + synchronized(loggedOverrides) { + if (!loggedOverrides.add(key)) return + } + context.log.info("Performance override applied: $key=$value", "PerformanceMode") + } + overrideProperty("STREAK_EXPIRATION_INFO", { context.config.userInterface.streakExpirationInfo.get() }, { true }) overrideProperty("TRANSCODING_MAX_QUALITY", { context.config.global.mediaUploadQualityConfig.forceVideoUploadSourceQuality.get() }, @@ -84,6 +105,44 @@ class ConfigurationOverride : Feature("Configuration Override") { { true }) overrideProperty("MEDIA_RECORDER_MAX_QUALITY_LEVEL", { context.config.camera.forceCameraSourceEncoding.get() }, { true }) + overrideProperty("PREVIEW_PRELOAD_ACTIVATOR", { context.config.global.performanceMode.profile.getNullable() != null }, + { true }) + overrideProperty("BUFFERED_VIDEO_RECORDING_ACTIVATOR", { context.config.global.performanceMode.profile.getNullable() != null }, + { true }) + overrideProperty("CAMERA_THREAD_PRIORITY", { context.config.global.performanceMode.profile.getNullable() != null }, + { true }) + overrideProperty("HD_MODE_ACTIVATOR", { context.config.global.performanceMode.profile.getNullable() == "max" }, + { true }) + + arrayOf( + "FEATURE_PRELOADER", + "USER_STORY_PRELOAD", + "STARTUP_LENS_ACTIVATOR", + "LENSES_PREVIEW_ACTIVATOR", + "THUMBNAIL_PRESENTER_ACTIVATOR", + "SINGLE_SEGMENT_THUMBNAIL_ACTIVATOR", + "SERVER_PREFETCH", + "SERVER_PREFETCH_WITH_COF", + "DISCOVER_FEED_PERFORMANCE", + "DISCOVER_FEED_STORY_PREFETCH", + "DISCOVER_FEED_THUMBNAILS", + "LOGIN_PRELOAD", + "PREFETCH_REPO_SUBSCRIBE_ON_CPU", + "COMPUTE_FEED_CACHE_WITH_TTL", + "COMPUTE_FEED_NETWORK_WITH_CACHE", + "OPERA_WARMUP", + "REFACTORED_WITH_WARMUP_LENS", + "SHOW_PREFETCH", + ).forEach { key -> + overrideProperty(key, { context.config.global.performanceMode.profile.getNullable() != null }, { true }) + } + + overrideProperty("LOAD_LATENCY_TRACKER_ACTIVATOR", { context.config.global.performanceMode.profile.getNullable() != null }, + { false }) + overrideProperty("ANALYTICS_ACTIVATOR", { context.config.global.performanceMode.profile.getNullable() != null }, + { false }) + overrideProperty("LOCK_SCREEN_ANALYTICS_ACTIVATOR", { context.config.global.performanceMode.profile.getNullable() != null }, + { false }) overrideProperty("REDUCE_MY_PROFILE_UI_COMPLEXITY", { context.config.userInterface.mapFriendNameTags.get() }, { true }) @@ -115,7 +174,10 @@ class ConfigurationOverride : Feature("Configuration Override") { propertyOverrides[propertyKey.name]?.let { (filter, value) -> if (!filter(propertyKey)) return@let - param.setResult(value(propertyKey)) + value(propertyKey).also { + logPerformanceOverride(propertyKey.name ?: return@also, it) + param.setResult(it) + } } } @@ -135,7 +197,10 @@ class ConfigurationOverride : Feature("Configuration Override") { propertyOverrides[key]?.let { (filter, value) -> val keyInfo = getConfigKeyInfo(enumData) ?: return@let if (!filter(keyInfo)) return@let - setValue(value(keyInfo)) + value(keyInfo).also { + logPerformanceOverride(key, it) + setValue(it) + } } } @@ -151,7 +216,10 @@ class ConfigurationOverride : Feature("Configuration Override") { } propertyOverrides[keyInfo.name]?.let { (filter, value, isAppExperiment) -> if (isAppExperiment != true || !filter(keyInfo)) return@let - param.setResult(value(keyInfo)) + value(keyInfo).also { + logPerformanceOverride(keyInfo.name ?: return@also, it) + param.setResult(it) + } } } @@ -174,7 +242,10 @@ class ConfigurationOverride : Feature("Configuration Override") { } val propertyOverride = propertyOverrides[keyInfo.name] ?: return@hook - propertyOverride.isAppExperiment.takeIf { propertyOverride.filter(keyInfo) }?.let { param.setResult(it) } + propertyOverride.isAppExperiment.takeIf { propertyOverride.filter(keyInfo) }?.let { + logPerformanceOverride(keyInfo.name ?: return@let, it) + param.setResult(it) + } } } }.onFailure { diff --git a/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/impl/experiments/RandomizedDeviceProfile.kt b/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/impl/experiments/RandomizedDeviceProfile.kt index 0120fa74..3ba5b1f1 100644 --- a/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/impl/experiments/RandomizedDeviceProfile.kt +++ b/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/impl/experiments/RandomizedDeviceProfile.kt @@ -385,7 +385,7 @@ object RandomizedDeviceProfileStore { bluetoothMacAddress = bluetoothMac, ipAddress = ipAddress, wifiSsid = region.randomWifiSsid(), - wifiRssi = random.nextInt(-72, -36), + wifiRssi = randomInt(-72, -36), localeTag = locale.toLanguageTag(), countryIso = region.countryIso, timeZoneId = region.timeZoneId, @@ -450,7 +450,7 @@ object RandomizedDeviceProfileStore { private fun randomPublicIpv4(prefixes: List? = null): String { val firstOctet = prefixes?.takeIf { it.isNotEmpty() }?.let { pick(it) } ?: run { - generateSequence { random.nextInt(1, 224) } + generateSequence { randomInt(1, 224) } .first { candidate -> candidate != 10 && candidate != 127 && @@ -459,13 +459,18 @@ object RandomizedDeviceProfileStore { candidate != 192 } } - val secondOctet = random.nextInt(1, 255) - val thirdOctet = random.nextInt(1, 255) - val fourthOctet = random.nextInt(2, 255) + val secondOctet = randomInt(1, 255) + val thirdOctet = randomInt(1, 255) + val fourthOctet = randomInt(2, 255) val candidate = "$firstOctet.$secondOctet.$thirdOctet.$fourthOctet" return runCatching { InetAddress.getByName(candidate).hostAddress }.getOrDefault(candidate) } + private fun randomInt(minInclusive: Int, maxExclusive: Int): Int { + require(maxExclusive > minInclusive) + return minInclusive + random.nextInt(maxExclusive - minInclusive) + } + private fun randomLong(minInclusive: Long, maxExclusive: Long): Long { require(maxExclusive > minInclusive) val bound = maxExclusive - minInclusive diff --git a/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/impl/global/DisableMetrics.kt b/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/impl/global/DisableMetrics.kt index 0546dd3c..025fda6a 100644 --- a/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/impl/global/DisableMetrics.kt +++ b/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/impl/global/DisableMetrics.kt @@ -7,7 +7,7 @@ import me.eternal.purrfectsnap.core.features.Feature class DisableMetrics : Feature("DisableMetrics") { override fun init() { - if (!context.config.global.disableMetrics.get()) return + if (!context.config.global.disableMetrics.get() && context.config.global.performanceMode.profile.getNullable() == null) return context.event.subscribe(NetworkApiRequestEvent::class) { param -> val url = param.url diff --git a/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/impl/tweaks/PerformanceMode.kt b/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/impl/tweaks/PerformanceMode.kt new file mode 100644 index 00000000..5f798a23 --- /dev/null +++ b/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/impl/tweaks/PerformanceMode.kt @@ -0,0 +1,312 @@ +package me.eternal.purrfectsnap.core.features.impl.tweaks + +import android.animation.ValueAnimator +import android.app.Activity +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.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.lang.Thread +import java.util.concurrent.atomic.AtomicBoolean +import java.util.concurrent.ThreadPoolExecutor +import me.eternal.purrfectsnap.core.features.Feature +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 okhttp3.Dispatcher + +class PerformanceMode : Feature("Performance Mode") { + override fun init() { + val profile = context.config.global.performanceMode.profile.getNullable() ?: return + val isMaxProfile = profile == "max" + val threadPriority = if (isMaxProfile) { + Process.THREAD_PRIORITY_DISPLAY + } else { + Process.THREAD_PRIORITY_MORE_FAVORABLE + } + val minimumFrameRate = if (isMaxProfile) 60 else 45 + val durationScale = if (isMaxProfile) 0.35f else 0.55f + val recyclerViewCacheSize = if (isMaxProfile) 64 else 32 + val maxRequests = if (isMaxProfile) 192 else 96 + val maxRequestsPerHost = if (isMaxProfile) 32 else 16 + 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) 120 else 180 + val preferredRefreshRate = if (isMaxProfile) 120f else 90f + + 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", + "PerformanceMode" + ) + + runCatching { + ValueAnimator.setFrameDelay(0L) + context.log.info("Applied ValueAnimator frame delay override: 0ms", "PerformanceMode") + } + + fun firstHitLogger(name: String): (String) -> Unit { + val didLog = AtomicBoolean(false) + return { details -> + if (didLog.compareAndSet(false, true)) { + context.log.info("First hit: $name | $details", "PerformanceMode") + } + } + } + + val handlerThreadConstructorLog = firstHitLogger("HandlerThread.constructor") + val handlerThreadStartLog = firstHitLogger("HandlerThread.start") + val threadStartLog = firstHitLogger("Thread.start") + 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") + + fun isPerformanceSensitiveThread(name: String?): Boolean { + val normalizedName = name?.lowercase() ?: return false + return listOf("camera", "preview", "codec", "render", "gl", "transcod", "lens", "feed", "story", "opera", "messag", "network", "db", "disk").any { + normalizedName.contains(it) + } + } + + HandlerThread::class.java.hookConstructor(HookStage.BEFORE) { param -> + if (param.args().size < 2) return@hookConstructor + val threadName = param.argNullable(0) + if (!isPerformanceSensitiveThread(threadName)) return@hookConstructor + param.setArg(1, threadPriority) + handlerThreadConstructorLog("name=$threadName priority=$threadPriority") + } + + HandlerThread::class.java.hook("start", HookStage.AFTER) { param -> + val thread = param.nullableThisObject() as? HandlerThread ?: return@hook + if (!isPerformanceSensitiveThread(thread.name)) return@hook + runCatching { + val tid = thread.threadId + if (tid > 0) { + Process.setThreadPriority(tid, threadPriority) + } + } + handlerThreadStartLog("name=${thread.name} tid=${thread.threadId} priority=$threadPriority") + } + + Thread::class.java.hook("start", HookStage.AFTER) { param -> + val thread = param.thisObject() + if (!isPerformanceSensitiveThread(thread.name)) return@hook + runCatching { + thread.priority = Thread.MAX_PRIORITY + } + threadStartLog("name=${thread.name} priority=${thread.priority}") + } + + ThreadPoolExecutor::class.java.hookConstructor(HookStage.AFTER) { param -> + val executor = param.thisObject() + runCatching { + val targetCorePoolSize = executor.maximumPoolSize.coerceAtLeast(1).coerceAtMost(minimumCoreThreads.coerceAtLeast(executor.corePoolSize)) + if (executor.corePoolSize < targetCorePoolSize) { + executor.corePoolSize = targetCorePoolSize + } + executor.allowCoreThreadTimeOut(false) + executor.prestartAllCoreThreads() + executorLog("core=${executor.corePoolSize} max=${executor.maximumPoolSize} active=${executor.activeCount}") + } + } + + Dispatcher::class.java.hookConstructor(HookStage.AFTER) { param -> + val dispatcher = param.thisObject() + runCatching { + dispatcher.maxRequests = maxRequests + dispatcher.maxRequestsPerHost = maxRequestsPerHost + dispatcherLog("maxRequests=${dispatcher.maxRequests} maxRequestsPerHost=${dispatcher.maxRequestsPerHost}") + } + } + + ValueAnimator::class.java.hook("getDurationScale", HookStage.AFTER) { param -> + param.setResult(durationScale) + animatorLog("durationScale=$durationScale") + } + + ValueAnimator::class.java.hook("setDuration", HookStage.BEFORE) { param -> + val original = param.arg(0) + val updated = original.coerceAtMost(maxAnimationDurationMs) + if (updated != original) { + param.setArg(0, updated) + } + animatorDurationLog("requested=$original applied=${param.arg(0)}") + } + + ViewPropertyAnimator::class.java.hook("setDuration", HookStage.BEFORE) { param -> + val original = param.arg(0) + val updated = original.coerceAtMost(maxAnimationDurationMs) + if (updated != original) { + param.setArg(0, updated) + } + viewAnimatorDurationLog("requested=$original applied=${param.arg(0)}") + } + + Transition::class.java.hook("setDuration", HookStage.BEFORE) { param -> + val original = param.arg(0) + val updated = original.coerceAtMost(maxAnimationDurationMs) + if (updated != original) { + param.setArg(0, updated) + } + transitionDurationLog("requested=$original applied=${param.arg(0)}") + } + + Animation::class.java.hook("setDuration", HookStage.BEFORE) { param -> + val original = param.arg(0) + val updated = original.coerceAtMost(maxAnimationDurationMs) + if (updated != original) { + param.setArg(0, updated) + } + animationDurationLog("requested=$original applied=${param.arg(0)}") + } + + RecyclerView::class.java.hookConstructor(HookStage.AFTER) { param -> + val recyclerView = param.thisObject() + recyclerView.setItemViewCacheSize(recyclerViewCacheSize) + recyclerView.overScrollMode = View.OVER_SCROLL_NEVER + recyclerView.setLayerType(View.LAYER_TYPE_HARDWARE, null) + if (isMaxProfile) { + recyclerView.itemAnimator = null + } + recyclerCtorLog("cache=$recyclerViewCacheSize max=$isMaxProfile class=${recyclerView::class.java.name}") + } + + RecyclerView::class.java.hook("setAdapter", HookStage.AFTER) { param -> + val recyclerView = param.thisObject() + recyclerView.setItemViewCacheSize(recyclerViewCacheSize) + recyclerView.setLayerType(View.LAYER_TYPE_HARDWARE, null) + if (isMaxProfile) { + recyclerView.itemAnimator = null + } + recyclerAdapterLog("cache=$recyclerViewCacheSize adapter=${param.argNullable(0)?.javaClass?.name}") + } + + RecyclerView::class.java.hook("setLayoutManager", HookStage.AFTER) { param -> + val recyclerView = param.thisObject() + val layoutManager = param.argNullable(0) + when (layoutManager) { + is LinearLayoutManager -> { + layoutManager.isItemPrefetchEnabled = true + layoutManager.initialPrefetchItemCount = prefetchItemCount + } + is StaggeredGridLayoutManager -> { + layoutManager.isItemPrefetchEnabled = true + layoutManager.gapStrategy = StaggeredGridLayoutManager.GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS + } + } + recyclerView.setLayerType(View.LAYER_TYPE_HARDWARE, null) + recyclerLayoutManagerLog("layoutManager=${layoutManager?.javaClass?.name} prefetch=$prefetchItemCount") + } + + fun SQLiteDatabase.applyPerformancePragmas() { + runCatching { execSQL("PRAGMA synchronous = NORMAL") } + runCatching { execSQL("PRAGMA temp_store = MEMORY") } + runCatching { execSQL("PRAGMA cache_size = -32768") } + runCatching { execSQL("PRAGMA mmap_size = 268435456") } + runCatching { execSQL("PRAGMA journal_size_limit = 1048576") } + runCatching { execSQL("PRAGMA optimize") } + } + + SQLiteDatabase::class.java.hook("openDatabase", HookStage.AFTER) { param -> + (param.getResult() as? SQLiteDatabase)?.also { + it.applyPerformancePragmas() + sqliteOpenLog("path=${param.argNullable(0)}") + } + } + + SQLiteDatabase::class.java.hook("openOrCreateDatabase", HookStage.AFTER) { param -> + (param.getResult() as? SQLiteDatabase)?.also { + it.applyPerformancePragmas() + sqliteCreateLog("path=${param.argNullable(0)}") + } + } + + MediaRecorder::class.java.hook("setVideoFrameRate", HookStage.BEFORE) { param -> + val currentRate = param.arg(0) + if (currentRate < minimumFrameRate) { + param.setArg(0, minimumFrameRate) + } + mediaRecorderLog("requested=$currentRate applied=${param.arg(0)}") + } + + OverScroller::class.java.hook("startScroll", HookStage.BEFORE) { param -> + if (param.args().size >= 5) { + val original = param.arg(4) + val updated = original.coerceAtMost(maxScrollDurationMs) + if (updated != original) { + param.setArg(4, updated) + } + overScrollerLog("requested=$original applied=${param.arg(4)}") + } + } + + CaptureRequest.Builder::class.java.hook("set", HookStage.BEFORE) { param -> + val key = param.arg>(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(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(1)}") + } + + fun applyActivityPerformanceTuning(activity: Activity) { + runCatching { + 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) + } + } +} diff --git a/gradle.properties b/gradle.properties index 31065f04..d413d851 100644 --- a/gradle.properties +++ b/gradle.properties @@ -7,8 +7,8 @@ org.gradle.configuration-cache=true org.gradle.configuration-cache.problems=warn nativeAbis=arm64-v8a -APP_VERSION_NAME=1.6.3 -APP_VERSION_CODE=316 +APP_VERSION_NAME=1.6.4 +APP_VERSION_CODE=318 debug_build_hash=18fe2a814d0e2eb5 psIntegrityPinnedSha256= EXPECTED_CERT_SHA256=0f188cb17d8ea4902ee15ce98f4928ba4226a4790fa3c52f62d776b08e46ca1c