v1.6.4
This commit is contained in:
@@ -33,8 +33,8 @@ tasks.register<GetVersionTask>("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.
|
||||
|
||||
@@ -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!
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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() }
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 = ?",
|
||||
|
||||
@@ -99,6 +99,7 @@ class FeatureManager(
|
||||
MeoPasscodeBypass(),
|
||||
AppLock(),
|
||||
CameraTweaks(),
|
||||
PerformanceMode(),
|
||||
InfiniteStoryBoost(),
|
||||
PinConversations(),
|
||||
DeviceSpooferHook(),
|
||||
|
||||
@@ -37,11 +37,32 @@ class ConfigurationOverride : Feature("Configuration Override") {
|
||||
}.getOrNull()
|
||||
|
||||
val propertyOverrides = mutableMapOf<String, ConfigFilter>()
|
||||
val loggedOverrides = mutableSetOf<String>()
|
||||
|
||||
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 {
|
||||
|
||||
@@ -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<Int>? = 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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<String>(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<Any>() 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<Thread>()
|
||||
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<ThreadPoolExecutor>()
|
||||
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<Dispatcher>()
|
||||
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<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
|
||||
}
|
||||
recyclerCtorLog("cache=$recyclerViewCacheSize max=$isMaxProfile class=${recyclerView::class.java.name}")
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
recyclerAdapterLog("cache=$recyclerViewCacheSize adapter=${param.argNullable<Any>(0)?.javaClass?.name}")
|
||||
}
|
||||
|
||||
RecyclerView::class.java.hook("setLayoutManager", HookStage.AFTER) { param ->
|
||||
val recyclerView = param.thisObject<RecyclerView>()
|
||||
val layoutManager = param.argNullable<Any>(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<Any>(0)}")
|
||||
}
|
||||
}
|
||||
|
||||
SQLiteDatabase::class.java.hook("openOrCreateDatabase", HookStage.AFTER) { param ->
|
||||
(param.getResult() as? SQLiteDatabase)?.also {
|
||||
it.applyPerformancePragmas()
|
||||
sqliteCreateLog("path=${param.argNullable<Any>(0)}")
|
||||
}
|
||||
}
|
||||
|
||||
MediaRecorder::class.java.hook("setVideoFrameRate", HookStage.BEFORE) { param ->
|
||||
val currentRate = param.arg<Int>(0)
|
||||
if (currentRate < minimumFrameRate) {
|
||||
param.setArg(0, minimumFrameRate)
|
||||
}
|
||||
mediaRecorderLog("requested=$currentRate applied=${param.arg<Int>(0)}")
|
||||
}
|
||||
|
||||
OverScroller::class.java.hook("startScroll", HookStage.BEFORE) { param ->
|
||||
if (param.args().size >= 5) {
|
||||
val original = param.arg<Int>(4)
|
||||
val updated = original.coerceAtMost(maxScrollDurationMs)
|
||||
if (updated != original) {
|
||||
param.setArg(4, updated)
|
||||
}
|
||||
overScrollerLog("requested=$original applied=${param.arg<Int>(4)}")
|
||||
}
|
||||
}
|
||||
|
||||
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.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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user