From d4a5a60cc1fb92180e38265b98fe6942f43f63df Mon Sep 17 00:00:00 2001 From: DarkKnight2122 <145188583+DarkKnight2122@users.noreply.github.com> Date: Sat, 25 Apr 2026 08:23:44 +0530 Subject: [PATCH] Manager app stutters and lag fixes --- .../purrfectsnap/bridge/BridgeService.kt | 21 +++++---- .../pages/features/FeaturesRootSection.kt | 1 - .../scripting/ManageScriptReposSection.kt | 5 ++- .../manager/pages/social/AddFriendDialog.kt | 38 +++++++++------- .../ui/manager/pages/social/LoggedStories.kt | 14 +++--- .../manager/pages/social/SocialRootSection.kt | 9 +++- .../pages/themes/aphelion/AphelionHomeView.kt | 6 ++- .../pages/themes/legacy/LegacyTheme.kt | 6 ++- .../ManageFriendTrackerReposSection.kt | 5 ++- .../impl/experiments/AutoOpenSnaps.kt | 44 ++++++++++++++++--- .../impl/ui/FriendFeedMessagePreview.kt | 13 +++--- 11 files changed, 110 insertions(+), 52 deletions(-) diff --git a/app/src/main/kotlin/me/eternal/purrfectsnap/bridge/BridgeService.kt b/app/src/main/kotlin/me/eternal/purrfectsnap/bridge/BridgeService.kt index 8cd7de09..00c36f19 100644 --- a/app/src/main/kotlin/me/eternal/purrfectsnap/bridge/BridgeService.kt +++ b/app/src/main/kotlin/me/eternal/purrfectsnap/bridge/BridgeService.kt @@ -5,6 +5,9 @@ import android.content.Intent import android.os.IBinder import android.os.ParcelFileDescriptor import android.os.RemoteException +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import me.eternal.purrfectsnap.RemoteSideContext import me.eternal.purrfectsnap.SharedContextHolder @@ -240,16 +243,18 @@ class BridgeService : Service() { if (chunkIndex == totalChunks - 1) { val finalFriends = friendAccumulator.toList() val finalGroups = groupAccumulator.toList() - - pendingSocialSnapshotCallback?.let { callback -> - pendingSocialSnapshotCallback = null - callback(finalFriends, finalGroups) - } - remoteSideContext.database.replaceMessagingData(finalFriends, finalGroups) - remoteSideContext.database.messagingDataFlow.tryEmit(finalFriends to finalGroups) - + friendAccumulator.clear() groupAccumulator.clear() + + remoteSideContext.coroutineScope.launch(Dispatchers.IO) { + pendingSocialSnapshotCallback?.let { callback -> + pendingSocialSnapshotCallback = null + callback(finalFriends, finalGroups) + } + remoteSideContext.database.replaceMessagingData(finalFriends, finalGroups) + remoteSideContext.database.messagingDataFlow.tryEmit(finalFriends to finalGroups) + } } } diff --git a/app/src/main/kotlin/me/eternal/purrfectsnap/ui/manager/pages/features/FeaturesRootSection.kt b/app/src/main/kotlin/me/eternal/purrfectsnap/ui/manager/pages/features/FeaturesRootSection.kt index fea98d07..1132d061 100644 --- a/app/src/main/kotlin/me/eternal/purrfectsnap/ui/manager/pages/features/FeaturesRootSection.kt +++ b/app/src/main/kotlin/me/eternal/purrfectsnap/ui/manager/pages/features/FeaturesRootSection.kt @@ -181,7 +181,6 @@ class FeaturesRootSection : Routes.Route() { } internal fun getRandomizedProfileSnapshot(): String { - context.config.load() return context.config.root.experimental.spoof.randomizeDeviceProfile.currentProfileSnapshot.getNullable() ?.takeIf { it.isNotBlank() } ?: (context.translation["manager.dialogs.randomize_device_profile.empty"] diff --git a/app/src/main/kotlin/me/eternal/purrfectsnap/ui/manager/pages/scripting/ManageScriptReposSection.kt b/app/src/main/kotlin/me/eternal/purrfectsnap/ui/manager/pages/scripting/ManageScriptReposSection.kt index 26107c5f..5ff97258 100644 --- a/app/src/main/kotlin/me/eternal/purrfectsnap/ui/manager/pages/scripting/ManageScriptReposSection.kt +++ b/app/src/main/kotlin/me/eternal/purrfectsnap/ui/manager/pages/scripting/ManageScriptReposSection.kt @@ -279,8 +279,9 @@ class ManageScriptReposSection : Routes.Route() { } override val content: @Composable (androidx.navigation.NavBackStackEntry) -> Unit = { - val repositories by remember(refreshTrigger.value) { - mutableStateOf>(runBlocking { context.database.getRepositories("script") }) + var repositories by remember { mutableStateOf>(emptyList()) } + LaunchedEffect(refreshTrigger.value) { + repositories = context.database.getRepositories("script") } val density = LocalDensity.current val statusBarTopPadding = WindowInsets.statusBars.asPaddingValues().calculateTopPadding() diff --git a/app/src/main/kotlin/me/eternal/purrfectsnap/ui/manager/pages/social/AddFriendDialog.kt b/app/src/main/kotlin/me/eternal/purrfectsnap/ui/manager/pages/social/AddFriendDialog.kt index 48ec4794..4bf8b687 100644 --- a/app/src/main/kotlin/me/eternal/purrfectsnap/ui/manager/pages/social/AddFriendDialog.kt +++ b/app/src/main/kotlin/me/eternal/purrfectsnap/ui/manager/pages/social/AddFriendDialog.kt @@ -225,24 +225,30 @@ class AddFriendDialog( friends: List, groups: List ) { - cachedFriends = context.sortSocialFriends(friends, pinnedIds = pinnedIds) - cachedGroups = groups.run { - if (pinnedIds != null) { - sortedBy { -pinnedIds.indexOf(it.conversationId) } - } else { - // Priority sort for whitelisted groups - val whitelistedIds = context.database.getRuleIds(MessagingRuleType.STEALTH.key).toSet() - sortedWith { a, b -> - val aSelected = whitelistedIds.contains(a.conversationId) - val bSelected = whitelistedIds.contains(b.conversationId) - if (aSelected != bSelected) if (aSelected) -1 else 1 - else a.name.compareTo(b.name, ignoreCase = true) + coroutineScope.launch(Dispatchers.IO) { + val sortedFriends = context.sortSocialFriends(friends, pinnedIds = pinnedIds) + val sortedGroups = groups.run { + if (pinnedIds != null) { + sortedBy { -pinnedIds.indexOf(it.conversationId) } + } else { + // Priority sort for whitelisted groups + val whitelistedIds = context.database.getRuleIds(MessagingRuleType.STEALTH.key).toSet() + sortedWith { a, b -> + val aSelected = whitelistedIds.contains(a.conversationId) + val bSelected = whitelistedIds.contains(b.conversationId) + if (aSelected != bSelected) if (aSelected) -1 else 1 + else a.name.compareTo(b.name, ignoreCase = true) + } + } + } + withContext(Dispatchers.Main) { + cachedFriends = sortedFriends + cachedGroups = sortedGroups + if (friends.isNotEmpty() || groups.isNotEmpty()) { + timeoutJob?.cancel() + hasFetchError = false } } - } - if (friends.isNotEmpty() || groups.isNotEmpty()) { - timeoutJob?.cancel() - hasFetchError = false } } diff --git a/app/src/main/kotlin/me/eternal/purrfectsnap/ui/manager/pages/social/LoggedStories.kt b/app/src/main/kotlin/me/eternal/purrfectsnap/ui/manager/pages/social/LoggedStories.kt index c2340729..540d56c9 100644 --- a/app/src/main/kotlin/me/eternal/purrfectsnap/ui/manager/pages/social/LoggedStories.kt +++ b/app/src/main/kotlin/me/eternal/purrfectsnap/ui/manager/pages/social/LoggedStories.kt @@ -35,6 +35,7 @@ import me.eternal.purrfectsnap.storage.getFriendInfo import me.eternal.purrfectsnap.ui.manager.Routes import me.eternal.purrfectsnap.ui.util.Dialog import me.eternal.purrfectsnap.ui.util.coil.ImageRequestHelper +import me.eternal.purrfectsnap.common.ui.rememberAsyncMutableState import java.io.File import java.text.DateFormat import java.util.Date @@ -44,12 +45,11 @@ import kotlin.math.absoluteValue class LoggedStories : Routes.Route() { override val title: @Composable () -> Unit = { val navBackStackEntry by routes.navController.currentBackStackEntryAsState() - val text = remember(navBackStackEntry) { - navBackStackEntry?.arguments?.getString("id")?.let { - context.database.getFriendInfo(it)?.displayName - } + val userId = navBackStackEntry?.arguments?.getString("id") + val displayName by rememberAsyncMutableState(defaultValue = null) { + userId?.let { context.database.getFriendInfo(it)?.displayName } } - text?.let { Text(it, maxLines = 1, overflow = TextOverflow.Ellipsis) } + displayName?.let { Text(it, maxLines = 1, overflow = TextOverflow.Ellipsis) } } @OptIn(ExperimentalCoilApi::class, ExperimentalLayoutApi::class) @@ -57,7 +57,9 @@ class LoggedStories : Routes.Route() { val userId = navBackStackEntry.arguments?.getString("id") ?: return@content val stories = remember { mutableStateListOf() } - val friendInfo = remember { context.database.getFriendInfo(userId) } + val friendInfo by rememberAsyncMutableState(defaultValue = null) { + context.database.getFriendInfo(userId) + } var lastStoryTimestamp by remember { mutableLongStateOf(Long.MAX_VALUE) } var selectedStory by remember { mutableStateOf(null) } diff --git a/app/src/main/kotlin/me/eternal/purrfectsnap/ui/manager/pages/social/SocialRootSection.kt b/app/src/main/kotlin/me/eternal/purrfectsnap/ui/manager/pages/social/SocialRootSection.kt index 7f81ad87..03551345 100644 --- a/app/src/main/kotlin/me/eternal/purrfectsnap/ui/manager/pages/social/SocialRootSection.kt +++ b/app/src/main/kotlin/me/eternal/purrfectsnap/ui/manager/pages/social/SocialRootSection.kt @@ -65,8 +65,13 @@ class SocialRootSection : Routes.Route() { // Real-time synchronization from the bridge context.database.messagingDataFlow.collect { (friends, groups) -> - friendList = context.sortSocialFriends(friends) - groupList = groups + withContext(Dispatchers.IO) { + val sortedFriends = context.sortSocialFriends(friends) + withContext(Dispatchers.Main) { + friendList = sortedFriends + groupList = groups + } + } } } } diff --git a/app/src/main/kotlin/me/eternal/purrfectsnap/ui/manager/pages/themes/aphelion/AphelionHomeView.kt b/app/src/main/kotlin/me/eternal/purrfectsnap/ui/manager/pages/themes/aphelion/AphelionHomeView.kt index 590e1732..2e8a3537 100644 --- a/app/src/main/kotlin/me/eternal/purrfectsnap/ui/manager/pages/themes/aphelion/AphelionHomeView.kt +++ b/app/src/main/kotlin/me/eternal/purrfectsnap/ui/manager/pages/themes/aphelion/AphelionHomeView.kt @@ -444,8 +444,10 @@ fun HomeRootSection.AphelionHomeScreen(nav: NavBackStackEntry) { } hasInitialized -> storedTiles else -> { - context.database.setQuickTiles(allQuickTileNames) - prefs.edit().putBoolean(HomeRootSection.QUICK_TILES_INITIALIZED_PREF, true).apply() + context.coroutineScope.launch(Dispatchers.IO) { + context.database.setQuickTiles(allQuickTileNames) + prefs.edit().putBoolean(HomeRootSection.QUICK_TILES_INITIALIZED_PREF, true).apply() + } allQuickTileNames } } diff --git a/app/src/main/kotlin/me/eternal/purrfectsnap/ui/manager/pages/themes/legacy/LegacyTheme.kt b/app/src/main/kotlin/me/eternal/purrfectsnap/ui/manager/pages/themes/legacy/LegacyTheme.kt index bd0b8914..3acda50a 100644 --- a/app/src/main/kotlin/me/eternal/purrfectsnap/ui/manager/pages/themes/legacy/LegacyTheme.kt +++ b/app/src/main/kotlin/me/eternal/purrfectsnap/ui/manager/pages/themes/legacy/LegacyTheme.kt @@ -337,8 +337,10 @@ object LegacyTheme : ThemeContract { } hasInitializedQuickTiles -> storedTiles else -> { - context.database.setQuickTiles(allQuickTileNames) - prefs.edit().putBoolean(QUICK_TILES_INITIALIZED_PREF, true).apply() + context.coroutineScope.launch(Dispatchers.IO) { + context.database.setQuickTiles(allQuickTileNames) + prefs.edit().putBoolean(QUICK_TILES_INITIALIZED_PREF, true).apply() + } allQuickTileNames } } diff --git a/app/src/main/kotlin/me/eternal/purrfectsnap/ui/manager/pages/tracker/ManageFriendTrackerReposSection.kt b/app/src/main/kotlin/me/eternal/purrfectsnap/ui/manager/pages/tracker/ManageFriendTrackerReposSection.kt index 38e1710b..c2390819 100644 --- a/app/src/main/kotlin/me/eternal/purrfectsnap/ui/manager/pages/tracker/ManageFriendTrackerReposSection.kt +++ b/app/src/main/kotlin/me/eternal/purrfectsnap/ui/manager/pages/tracker/ManageFriendTrackerReposSection.kt @@ -269,8 +269,9 @@ class ManageFriendTrackerReposSection: Routes.Route() { } override val content: @Composable (androidx.navigation.NavBackStackEntry) -> Unit = { - val repositories by remember(refreshTrigger.value) { - mutableStateOf>(runBlocking { context.database.getRepositories("friend_tracker") }) + var repositories by remember { mutableStateOf>(emptyList()) } + LaunchedEffect(refreshTrigger.value) { + repositories = context.database.getRepositories("friend_tracker") } val density = LocalDensity.current val statusBarTopPadding = WindowInsets.statusBars.asPaddingValues().calculateTopPadding() diff --git a/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/impl/experiments/AutoOpenSnaps.kt b/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/impl/experiments/AutoOpenSnaps.kt index fc36dc09..49a2a7f3 100644 --- a/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/impl/experiments/AutoOpenSnaps.kt +++ b/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/impl/experiments/AutoOpenSnaps.kt @@ -20,6 +20,7 @@ import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.flow.MutableSharedFlow import me.eternal.purrfectsnap.bridge.AutoOpenInterface import me.eternal.purrfectsnap.common.config.PropertyValue +import me.eternal.purrfectsnap.common.config.ModConfig import me.eternal.purrfectsnap.common.data.ContentType import me.eternal.purrfectsnap.common.data.MessageState import me.eternal.purrfectsnap.common.data.MessageUpdate @@ -93,6 +94,7 @@ class AutoOpenSnaps: MessagingRuleFeature("Auto Open Snaps", MessagingRuleType.A private val lastSaveTime = AtomicLong(System.currentTimeMillis()) private var isThermalThrottled = false private var lastThermalThrottleAt = 0L + private var actionReceiver: BroadcastReceiver? = null private fun logInfo(msg: String) = this@AutoOpenSnaps.context.log.info("[AutoOpenEngine] $msg") private fun logError(msg: String, e: Throwable? = null) = if (e != null) this@AutoOpenSnaps.context.log.error("[AutoOpenEngine] $msg", e) else this@AutoOpenSnaps.context.log.error("[AutoOpenEngine] $msg") @@ -111,6 +113,8 @@ class AutoOpenSnaps: MessagingRuleFeature("Auto Open Snaps", MessagingRuleType.A } override fun init() { + if (autoOpenConfig.globalState == false) return + restorePersistence() createNotificationChannels() @@ -477,7 +481,7 @@ class AutoOpenSnaps: MessagingRuleFeature("Auto Open Snaps", MessagingRuleType.A } private fun setupReceivers() { - val actionReceiver = object : BroadcastReceiver() { + actionReceiver = object : BroadcastReceiver() { override fun onReceive(ctx: Context?, intent: Intent?) { when (intent?.action) { ACTION_PAUSE_RESUME -> { isPaused.set(!isPaused.get()); updateStatusNotification(force = true) } @@ -501,8 +505,8 @@ class AutoOpenSnaps: MessagingRuleFeature("Auto Open Snaps", MessagingRuleType.A addAction(Intent.ACTION_SCREEN_OFF) addAction(Intent.ACTION_BATTERY_CHANGED) } - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) this@AutoOpenSnaps.context.androidContext.registerReceiver(actionReceiver, filter, Context.RECEIVER_NOT_EXPORTED) - else this@AutoOpenSnaps.context.androidContext.registerReceiver(actionReceiver, filter) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) this@AutoOpenSnaps.context.androidContext.registerReceiver(actionReceiver!!, filter, Context.RECEIVER_NOT_EXPORTED) + else this@AutoOpenSnaps.context.androidContext.registerReceiver(actionReceiver!!, filter) } private fun recordSpeedTimestamp() { synchronized(snapTimestamps) { snapTimestamps.addLast(System.currentTimeMillis()); if (snapTimestamps.size > 250) snapTimestamps.removeFirst() } } @@ -510,9 +514,39 @@ class AutoOpenSnaps: MessagingRuleFeature("Auto Open Snaps", MessagingRuleType.A private fun shutdownFeature() { engineActive.set(false) engineJob?.cancel() - releaseWakeLock() - cancelStatusNotification() saveQueueToDisk() + + // Permanently disable the feature in settings + autoOpenConfig.globalState = false + this@AutoOpenSnaps.context.coroutineScope.launch { + runCatching { + val field = context::class.java.getDeclaredField("_config").apply { isAccessible = true } + val modConfig = (field.get(context) as Lazy<*>).value as ModConfig + modConfig.writeConfig() + } + } + + // Surgical clean-up: release resources and listeners + actionReceiver?.let { + runCatching { this@AutoOpenSnaps.context.androidContext.unregisterReceiver(it) } + } + actionReceiver = null + wakeLockCooldownJob?.cancel() + + // Grace period for WakeLock release + this@AutoOpenSnaps.context.coroutineScope.launch { + delay(60000) + releaseWakeLock() + } + + // Show final "Stopped" notice + val builder = Notification.Builder(this@AutoOpenSnaps.context.androidContext, "auto_open_status") + .setOngoing(false) + .setSmallIcon(android.R.drawable.ic_menu_close_clear_cancel) + .setContentTitle("Auto-Open") + .setContentText("Auto-Open Engine Disabled. Re-enable in settings.") + + notificationManager.notify(STATUS_NOTIFICATION_ID, builder.build()) } private fun cancelStatusNotification() = notificationManager.cancel(STATUS_NOTIFICATION_ID) diff --git a/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/impl/ui/FriendFeedMessagePreview.kt b/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/impl/ui/FriendFeedMessagePreview.kt index 8f542f54..1fcb9fbb 100644 --- a/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/impl/ui/FriendFeedMessagePreview.kt +++ b/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/impl/ui/FriendFeedMessagePreview.kt @@ -108,9 +108,10 @@ class FriendFeedMessagePreview : Feature("FriendFeedMessagePreview") { } fetchMessages(conversationId) { - val fontMetrics = textPaint.fontMetrics + val universalTextSize = 12 * density + val fontMetrics = textPaint.apply { textSize = universalTextSize }.fontMetrics val lineHeight = (fontMetrics.descent - fontMetrics.ascent).toInt() - val spacing = (2 * density).toInt() + val spacing = (4 * density).toInt() val messages = messageCache[conversationId] val previewContainerHeight = if (messages.isNullOrEmpty()) 0 else (messages.size * (lineHeight + spacing)) @@ -123,16 +124,16 @@ class FriendFeedMessagePreview : Feature("FriendFeedMessagePreview") { } ffItem.layoutParams = ffItem.layoutParams.apply { - height = feedEntryHeight + (safetyGap * 1.5f).toInt() + previewContainerHeight + height = feedEntryHeight + (safetyGap).toInt() + previewContainerHeight } cachedLayouts[conversationId] = frameLayout frameLayout.addForegroundDrawable("ffItem", ShapeDrawable(object: Shape() { override fun draw(canvas: Canvas, paint: Paint) { - val startY = feedEntryHeight.toFloat() + (1 * density).toInt() - paint.textSize = secondaryTextSize - paint.color = Color(context.userInterface.colorPrimary).copy(alpha = 0.7f).toArgb() + val startY = feedEntryHeight.toFloat() - (9 * density) + paint.textSize = universalTextSize + paint.color = Color(context.userInterface.colorPrimary).copy(alpha = 0.85f).toArgb() paint.typeface = Typeface.DEFAULT paint.isAntiAlias = true