Social Tab bug fixes

This commit is contained in:
DarkKnight2122
2026-04-24 21:51:20 +05:30
parent c988c489e7
commit 5978717065
12 changed files with 237 additions and 155 deletions

View File

@@ -219,19 +219,38 @@ class BridgeService : Service() {
triggerScopeSync(SocialScope.getByName(scope), id, true)
}
private val friendAccumulator = mutableListOf<MessagingFriendInfo>()
private val groupAccumulator = mutableListOf<MessagingGroupInfo>()
override fun passGroupsAndFriends(
groups: List<String>,
friends: List<String>
friends: List<String>,
chunkIndex: Int,
totalChunks: Int
) {
remoteSideContext.log.verbose("Received ${groups.size} groups and ${friends.size} friends")
val parsedFriends = friends.mapNotNull { toParcelable<MessagingFriendInfo>(it) }
val parsedGroups = groups.mapNotNull { toParcelable<MessagingGroupInfo>(it) }
pendingSocialSnapshotCallback?.let { callback ->
pendingSocialSnapshotCallback = null
callback(parsedFriends, parsedGroups)
if (chunkIndex == 0) {
friendAccumulator.clear()
groupAccumulator.clear()
}
remoteSideContext.log.verbose("Received chunk $chunkIndex/$totalChunks: ${groups.size} groups, ${friends.size} friends")
friendAccumulator.addAll(friends.mapNotNull { toParcelable<MessagingFriendInfo>(it) })
groupAccumulator.addAll(groups.mapNotNull { toParcelable<MessagingGroupInfo>(it) })
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.database.replaceMessagingData(parsedFriends, parsedGroups)
remoteSideContext.database.receiveMessagingDataCallback(parsedFriends, parsedGroups)
}
override fun getScopeNotes(id: String): String? {

View File

@@ -5,6 +5,8 @@ import me.eternal.purrfectsnap.RemoteSideContext
import me.eternal.purrfectsnap.common.data.MessagingFriendInfo
import me.eternal.purrfectsnap.common.data.MessagingGroupInfo
import me.eternal.purrfectsnap.common.util.SQLiteDatabaseHelper
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.MutableSharedFlow
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
@@ -15,7 +17,11 @@ class AppDatabase(
val executor: ExecutorService = Executors.newSingleThreadExecutor()
lateinit var database: SQLiteDatabase
var receiveMessagingDataCallback: (friends: List<MessagingFriendInfo>, groups: List<MessagingGroupInfo>) -> Unit = { _, _ -> }
// Multi-subscriber event stream for messaging data updates
val messagingDataFlow = MutableSharedFlow<Pair<List<MessagingFriendInfo>, List<MessagingGroupInfo>>>(
extraBufferCapacity = 1,
onBufferOverflow = BufferOverflow.DROP_OLDEST
)
fun executeAsync(block: () -> Unit) {
executor.execute {

View File

@@ -139,10 +139,10 @@ fun AppDatabase.replaceMessagingData(
database.endTransaction()
}
// Notify with the full updated list from the DB
// Notify all observers with the updated data from the database
val allFriends = getFriends(descOrder = true)
val allGroups = getGroups()
receiveMessagingDataCallback(allFriends, allGroups)
messagingDataFlow.tryEmit(allFriends to allGroups)
}
}

View File

@@ -26,9 +26,11 @@ import kotlinx.coroutines.*
import me.eternal.purrfectsnap.RemoteSideContext
import me.eternal.purrfectsnap.common.data.MessagingFriendInfo
import me.eternal.purrfectsnap.common.data.MessagingGroupInfo
import me.eternal.purrfectsnap.common.data.MessagingRuleType
import me.eternal.purrfectsnap.common.util.snap.BitmojiSelfie
import me.eternal.purrfectsnap.storage.getFriends
import me.eternal.purrfectsnap.storage.getGroups
import me.eternal.purrfectsnap.storage.getRuleIds
import me.eternal.purrfectsnap.ui.manager.theme.PurrfectPalette
import me.eternal.purrfectsnap.ui.util.coil.BitmojiImage
@@ -228,7 +230,14 @@ class AddFriendDialog(
if (pinnedIds != null) {
sortedBy { -pinnedIds.indexOf(it.conversationId) }
} else {
this
// 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)
}
}
}
if (friends.isNotEmpty() || groups.isNotEmpty()) {
@@ -237,12 +246,7 @@ class AddFriendDialog(
}
}
val updateSnapshot: (List<MessagingFriendInfo>, List<MessagingGroupInfo>) -> Unit = { friends, groups ->
coroutineScope.launch {
applySnapshot(friends, groups)
}
}
// Initial database load
withContext(Dispatchers.IO) {
applySnapshot(
context.database.getFriends(descOrder = true),
@@ -250,20 +254,11 @@ class AddFriendDialog(
)
}
context.database.receiveMessagingDataCallback = updateSnapshot
// Real-time synchronization flow
context.requestSocialSnapshotRefresh()
coroutineScope.launch(Dispatchers.IO) {
repeat(25) {
delay(1000)
val dbFriends = context.database.getFriends(descOrder = true)
val dbGroups = context.database.getGroups()
if (dbFriends.isNotEmpty() || dbGroups.isNotEmpty()) {
withContext(Dispatchers.Main) {
applySnapshot(dbFriends, dbGroups)
}
return@launch
}
coroutineScope.launch {
context.database.messagingDataFlow.collect { (friends, groups) ->
applySnapshot(friends, groups)
}
}
@@ -280,7 +275,6 @@ class AddFriendDialog(
onDispose {
timeoutJob?.cancel()
context.bridgeService?.clearEphemeralSocialSnapshotRequest()
context.database.receiveMessagingDataCallback = { _, _ -> }
}
}
@@ -340,6 +334,7 @@ class AddFriendDialog(
it.mutableUsername.contains(searchKeyword.value, ignoreCase = true) ||
it.displayName?.contains(searchKeyword.value, ignoreCase = true) == true
} ?: cachedFriends!!
val selectedFriendCount by remember(filteredFriends) {
derivedStateOf {
filteredFriends.count { friend ->
@@ -350,6 +345,16 @@ class AddFriendDialog(
val hasFriendsSelected = selectedFriendCount > 0
val allFriendsSelected = filteredFriends.isNotEmpty() && selectedFriendCount == filteredFriends.size
val selectedGroupCount by remember(filteredGroups) {
derivedStateOf {
filteredGroups.count { group ->
stateCache[group.conversationId] ?: actionHandler.getGroupState(group)
}
}
}
val hasGroupsSelected = selectedGroupCount > 0
val allGroupsSelected = filteredGroups.isNotEmpty() && selectedGroupCount == filteredGroups.size
DialogHeader(searchKeyword)
LazyColumn(
@@ -359,14 +364,54 @@ class AddFriendDialog(
) {
item {
if (filteredGroups.isNotEmpty()) {
Text(
text = translation["category_groups"],
fontSize = 16.sp,
fontWeight = FontWeight.SemiBold,
Row(
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 8.dp, top = 8.dp),
color = Color.White
)
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = translation["category_groups"],
fontSize = 16.sp,
fontWeight = FontWeight.SemiBold,
color = Color.White
)
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
TextButton(
onClick = {
coroutineScope.launch(Dispatchers.IO) {
filteredGroups.forEach { group ->
stateCache[group.conversationId] = true
actionHandler.onGroupState(group, true)
}
}
},
enabled = !allGroupsSelected
) {
Text(
text = context.translation["manager.dialogs.messaging_action.select_all_button"],
color = if (allGroupsSelected) Color.White.copy(alpha = 0.45f) else PurrfectPalette.glowSecondary
)
}
TextButton(
onClick = {
coroutineScope.launch(Dispatchers.IO) {
filteredGroups.forEach { group ->
stateCache[group.conversationId] = false
actionHandler.onGroupState(group, false)
}
}
},
enabled = hasGroupsSelected
) {
Text(
text = translation["unselect_all_button"],
color = if (hasGroupsSelected) PurrfectPalette.glowPrimary else Color.White.copy(alpha = 0.45f)
)
}
}
}
}
}
@@ -411,11 +456,7 @@ class AddFriendDialog(
) {
Text(
text = context.translation["manager.dialogs.messaging_action.select_all_button"],
color = if (allFriendsSelected) {
Color.White.copy(alpha = 0.45f)
} else {
PurrfectPalette.glowSecondary
}
color = if (allFriendsSelected) Color.White.copy(alpha = 0.45f) else PurrfectPalette.glowSecondary
)
}
TextButton(
@@ -431,11 +472,7 @@ class AddFriendDialog(
) {
Text(
text = translation["unselect_all_button"],
color = if (hasFriendsSelected) {
PurrfectPalette.glowPrimary
} else {
Color.White.copy(alpha = 0.45f)
}
color = if (hasFriendsSelected) PurrfectPalette.glowPrimary else Color.White.copy(alpha = 0.45f)
)
}
}

View File

@@ -2,21 +2,29 @@ package me.eternal.purrfectsnap.ui.manager.pages.social
import me.eternal.purrfectsnap.RemoteSideContext
import me.eternal.purrfectsnap.common.data.MessagingFriendInfo
import me.eternal.purrfectsnap.storage.getFriends
internal fun RemoteSideContext.sortSocialFriends(
friends: List<MessagingFriendInfo>,
pinnedIds: List<String>? = null
): List<MessagingFriendInfo> {
if (config.root.userInterface.sortSocialTabByStreakLength.get()) {
return friends.sortedWith(
compareByDescending<MessagingFriendInfo> { (it.streaks?.length ?: 0) > 0 }
.thenByDescending { it.streaks?.length ?: 0 }
)
}
val whitelistedIds = pinnedIds?.toSet() ?: database.getFriends().map { it.userId }.toSet()
val sortByStreakLength = config.root.userInterface.sortSocialTabByStreakLength.get()
return if (pinnedIds != null) {
friends.sortedBy { -pinnedIds.indexOf(it.userId) }
} else {
friends
return friends.sortedWith { a, b ->
val aSelected = whitelistedIds.contains(a.userId)
val bSelected = whitelistedIds.contains(b.userId)
if (aSelected != bSelected) {
return@sortedWith if (aSelected) -1 else 1
}
if (sortByStreakLength) {
val aStreak = a.streaks?.length ?: 0
val bStreak = b.streaks?.length ?: 0
if (aStreak != bStreak) return@sortedWith bStreak.compareTo(aStreak)
}
(a.displayName ?: a.mutableUsername).compareTo(b.displayName ?: b.mutableUsername, ignoreCase = true)
}
}

View File

@@ -35,8 +35,7 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.navigation.NavBackStackEntry
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.*
import me.eternal.purrfectsnap.R
import me.eternal.purrfectsnap.common.data.MessagingFriendInfo
import me.eternal.purrfectsnap.common.data.MessagingGroupInfo
@@ -53,10 +52,23 @@ class SocialRootSection : Routes.Route() {
internal var friendList: List<MessagingFriendInfo> by mutableStateOf(emptyList())
internal var groupList: List<MessagingGroupInfo> by mutableStateOf(emptyList())
internal fun updateScopeLists() {
context.coroutineScope.launch {
friendList = context.database.getFriends(descOrder = true)
groupList = context.database.getGroups()
@Composable
fun SocialDataController() {
LaunchedEffect(Unit) {
// Initial data fetch from the database
withContext(Dispatchers.IO) {
val dbFriends = context.database.getFriends(descOrder = true)
val dbGroups = context.database.getGroups()
friendList = context.sortSocialFriends(dbFriends)
groupList = dbGroups
}
// Real-time synchronization from the bridge
context.requestSocialSnapshotRefresh()
context.database.messagingDataFlow.collect { (friends, groups) ->
friendList = context.sortSocialFriends(friends)
groupList = groups
}
}
}
@@ -124,11 +136,6 @@ class SocialRootSection : Routes.Route() {
addFriendDialog?.Content {
addFriendDialog = null
}
DisposableEffect(Unit) {
onDispose {
updateScopeLists()
}
}
}
FloatingActionButton(

View File

@@ -50,6 +50,9 @@ import me.eternal.purrfectsnap.ui.manager.theme.PurrfectPalette
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun SocialRootSection.AphelionSocialScreen(nav: NavBackStackEntry) {
// Controller handles data loading and synchronization
SocialDataController()
val titles = remember {
listOf(translation["friends_tab"], translation["groups_tab"])
}
@@ -58,27 +61,11 @@ fun SocialRootSection.AphelionSocialScreen(nav: NavBackStackEntry) {
var searchQuery by rememberSaveable { mutableStateOf("") }
var searchActive by rememberSaveable { mutableStateOf(false) }
LaunchedEffect(Unit) {
context.database.receiveMessagingDataCallback = { friends, groups ->
friendList = friends
groupList = groups
}
updateScopeLists()
}
DisposableEffect(Unit) {
onDispose {
context.database.receiveMessagingDataCallback = { _, _ -> }
}
}
val sortByStreakLength by produceState(initialValue = context.config.root.userInterface.sortSocialTabByStreakLength.get()) {
while (true) {
delay(300)
value = context.config.root.userInterface.sortSocialTabByStreakLength.get()
}
}
val normalizedQuery = remember(searchQuery) { searchQuery.trim() }
val filteredFriends = remember(friendList, normalizedQuery, sortByStreakLength) {
val matchingFriends = if (normalizedQuery.isBlank()) {
// Filter logic based on the parent's synchronized data lists
val filteredFriends = remember(friendList, normalizedQuery) {
if (normalizedQuery.isBlank()) {
friendList
} else {
friendList.filter {
@@ -86,8 +73,6 @@ fun SocialRootSection.AphelionSocialScreen(nav: NavBackStackEntry) {
it.displayName?.contains(normalizedQuery, ignoreCase = true) == true
}
}
context.sortSocialFriends(matchingFriends)
}
val filteredGroups = remember(groupList, normalizedQuery) {
if (normalizedQuery.isBlank()) {

View File

@@ -2015,6 +2015,9 @@ object LegacyTheme : ThemeContract {
}
@Composable override fun SocialRootSection.SocialScreen(nav: NavBackStackEntry) {
// Controller handles data loading and synchronization
SocialDataController()
val titles = remember {
listOf(translation["friends_tab"], translation["groups_tab"])
}
@@ -2023,27 +2026,11 @@ object LegacyTheme : ThemeContract {
var searchQuery by rememberSaveable { mutableStateOf("") }
var searchActive by rememberSaveable { mutableStateOf(false) }
LaunchedEffect(Unit) {
context.database.receiveMessagingDataCallback = { friends, groups ->
friendList = friends
groupList = groups
}
updateScopeLists()
}
DisposableEffect(Unit) {
onDispose {
context.database.receiveMessagingDataCallback = { _, _ -> }
}
}
val sortByStreakLength by produceState(initialValue = context.config.root.userInterface.sortSocialTabByStreakLength.get()) {
while (true) {
delay(300)
value = context.config.root.userInterface.sortSocialTabByStreakLength.get()
}
}
val normalizedQuery = remember(searchQuery) { searchQuery.trim() }
val filteredFriends = remember(friendList, normalizedQuery, sortByStreakLength) {
val matchingFriends = if (normalizedQuery.isBlank()) {
// Filter logic based on the parent's synchronized data lists
val filteredFriends = remember(friendList, normalizedQuery) {
if (normalizedQuery.isBlank()) {
friendList
} else {
friendList.filter {
@@ -2051,8 +2038,6 @@ object LegacyTheme : ThemeContract {
it.displayName?.contains(normalizedQuery, ignoreCase = true) == true
}
}
context.sortSocialFriends(matchingFriends)
}
val filteredGroups = remember(groupList, normalizedQuery) {
if (normalizedQuery.isBlank()) {

View File

@@ -71,7 +71,7 @@ interface BridgeInterface {
* @param groups list of groups (MessagingGroupInfo as parcelable)
* @param friends list of friends (MessagingFriendInfo as parcelable)
*/
oneway void passGroupsAndFriends(in List<String> groups, in List<String> friends);
oneway void passGroupsAndFriends(in List<String> groups, in List<String> friends, int chunkIndex, int totalChunks);
@nullable String getScopeNotes(String id);

View File

@@ -348,22 +348,24 @@ class BridgeClient(
safeServiceCall {
val serializedGroups = groups.mapNotNull { it.toSerialized() }
val serializedFriends = friends.mapNotNull { it.toSerialized() }
// Binder transaction limit is 1MB. Use 128KB chunks to avoid TransactionTooLargeException.
val maxChunkBytes = 128 * 1024
fun chunkSerialized(values: List<String>): List<List<String>> {
fun calculateParts(values: List<String>): List<List<String>> {
if (values.isEmpty()) return listOf(emptyList())
val result = mutableListOf<List<String>>()
val currentChunk = mutableListOf<String>()
var currentChunk = mutableListOf<String>()
var currentSize = 0
values.forEach { value ->
val valueSize = value.toByteArray(StandardCharsets.UTF_8).size + 32
val valueSize = value.toByteArray(Charsets.UTF_8).size + 32
if (currentChunk.isNotEmpty() && currentSize + valueSize > maxChunkBytes) {
result += currentChunk.toList()
currentChunk.clear()
currentChunk = mutableListOf()
currentSize = 0
}
currentChunk += value
currentChunk.add(value)
currentSize += valueSize
}
@@ -373,19 +375,18 @@ class BridgeClient(
return result
}
val groupChunks = chunkSerialized(serializedGroups)
val friendChunks = chunkSerialized(serializedFriends)
val chunkCount = maxOf(groupChunks.size, friendChunks.size)
val groupParts = calculateParts(serializedGroups)
val friendParts = calculateParts(serializedFriends)
val totalParts = maxOf(groupParts.size, friendParts.size)
context.log.info(
"Sending social snapshot in $chunkCount chunk(s): " +
"${serializedGroups.size} groups, ${serializedFriends.size} friends"
)
context.log.info("Synchronizing social data in $totalParts part(s): ${serializedGroups.size} groups, ${serializedFriends.size} friends")
repeat(chunkCount) { index ->
repeat(totalParts) { index ->
connectedService.passGroupsAndFriends(
groupChunks.getOrElse(index) { emptyList() },
friendChunks.getOrElse(index) { emptyList() }
groupParts.getOrElse(index) { emptyList() },
friendParts.getOrElse(index) { emptyList() },
index,
totalParts
)
}
}

View File

@@ -52,12 +52,11 @@ class AutoOpenSnaps: MessagingRuleFeature("Auto Open Snaps", MessagingRuleType.A
private const val NOTIFICATION_GROUP_KEY = "purrfectsnap.AUTO_OPEN"
private const val PREF_TOTAL_OPENED = "auto_open_total_opened"
private const val PREF_SESSION_START = "auto_open_session_start"
private const val LAZY_SAVE_INTERVAL_MS = 600_000L
}
private val gson = Gson()
private val isPaused = AtomicBoolean(false)
private val isScreenOn = AtomicBoolean(true)
private val engineActive = AtomicBoolean(true)
private val totalProcessed = AtomicInteger(0)
private val sessionProcessed = AtomicInteger(0)
@@ -76,17 +75,18 @@ class AutoOpenSnaps: MessagingRuleFeature("Auto Open Snaps", MessagingRuleType.A
private val prefs by lazy { this@AutoOpenSnaps.context.androidContext.getSharedPreferences("me.eternal.purrfectsnap_preferences", Context.MODE_PRIVATE) }
private val messaging by lazy { this@AutoOpenSnaps.context.feature(Messaging::class) }
private var wakeLock: PowerManager.WakeLock? = null
private var wakeLockCooldownJob: Job? = null
private var currentStatusText = "Monitoring..."
private var currentSpeedText = "Full Speed"
private var lastNotificationUpdate = 0L
private var lastNotificationStateHash = 0
private val notificationUpdateDelay = 1000L
private val pendingNotificationUpdate = AtomicBoolean(false)
private val snapTimestamps = LinkedList<Long>()
private var lastConversationId: String? = null
private val isSaving = AtomicBoolean(false)
private val needsSaving = AtomicBoolean(false)
private val lastSaveTime = AtomicLong(System.currentTimeMillis())
private var isThermalThrottled = false
private var lastThermalThrottleAt = 0L
@@ -125,16 +125,20 @@ class AutoOpenSnaps: MessagingRuleFeature("Auto Open Snaps", MessagingRuleType.A
}
}
}
findClass("com.snapchat.client.network_manager.NetworkManager\$CppProxy").apply {
hook("onAppForegrounded", HookStage.BEFORE) { param -> param.setResult(null) }
hook("onAppBackgrounded", HookStage.BEFORE) { param -> param.setResult(null) }
}
}
}
// Background Watchdog: Periodically refreshes UI and verifies engine health
// Background Watchdog: Periodically verifies engine health
this@AutoOpenSnaps.context.coroutineScope.launch(Dispatchers.Default) {
while (isActive && engineActive.get()) {
if (autoOpenConfig.globalState == true) {
if (autoOpenConfig.globalState == true && synchronized(queuedSnaps) { queuedSnaps.isNotEmpty() }) {
updateStatusNotification()
}
delay(5000)
delay(300000)
}
}
@@ -179,6 +183,8 @@ class AutoOpenSnaps: MessagingRuleFeature("Auto Open Snaps", MessagingRuleType.A
if (synchronized(queuedSnaps) { queuedSnaps.isEmpty() }) {
currentStatusText = "Monitoring..."
updateStatusNotification()
saveQueueToDisk() // Batch complete save
startWakeLockCooldown()
}
}
}
@@ -207,9 +213,16 @@ class AutoOpenSnaps: MessagingRuleFeature("Auto Open Snaps", MessagingRuleType.A
if (success) {
synchronized(queuedSnaps) { queuedSnaps.remove(item) }
sessionProcessed.incrementAndGet(); totalProcessed.incrementAndGet(); recordSpeedTimestamp()
// Industrial Interval Check: Only write to disk once every 10 minutes during floods
if (System.currentTimeMillis() - lastSaveTime.get() > 600000) {
saveQueueToDisk()
lastSaveTime.set(System.currentTimeMillis())
}
val duration = System.currentTimeMillis() - startTime
averageProcessingTime.set((averageProcessingTime.get() * 0.7 + duration * 0.3).toLong())
triggerLazySave(); break
break
}
delay((autoOpenConfig.retryDelay as PropertyValue<Int>).get().toLong())
}
@@ -288,19 +301,7 @@ class AutoOpenSnaps: MessagingRuleFeature("Auto Open Snaps", MessagingRuleType.A
synchronized(queuedSnaps) { queuedSnaps.add(item) }
snapChannel.trySend(item)
acquireWakeLock(); updateStatusNotification(); triggerLazySave()
}
}
private fun triggerLazySave() {
needsSaving.set(true)
if (isSaving.compareAndSet(false, true)) {
this@AutoOpenSnaps.context.coroutineScope.launch(Dispatchers.IO) {
while (needsSaving.get() && engineActive.get()) {
needsSaving.set(false); saveQueueToDisk(); delay(LAZY_SAVE_INTERVAL_MS)
}
isSaving.set(false)
}
acquireWakeLock(); updateStatusNotification(); saveQueueToDisk()
}
}
@@ -333,12 +334,21 @@ class AutoOpenSnaps: MessagingRuleFeature("Auto Open Snaps", MessagingRuleType.A
}
private fun acquireWakeLock() {
wakeLockCooldownJob?.cancel()
if (wakeLock?.isHeld == true) return
wakeLock = (this@AutoOpenSnaps.context.androidContext.getSystemService(Context.POWER_SERVICE) as PowerManager).newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "PurrfectSnap:AutoOpen").apply { acquire(8 * 60 * 60 * 1000L) }
}
private fun releaseWakeLock() { if (wakeLock?.isHeld == true) wakeLock?.release(); wakeLock = null }
private fun startWakeLockCooldown() {
wakeLockCooldownJob?.cancel()
wakeLockCooldownJob = this@AutoOpenSnaps.context.coroutineScope.launch {
delay(30000)
releaseWakeLock()
}
}
private fun createNotificationChannels() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
notificationManager.createNotificationChannel(NotificationChannel("auto_open_status", "Auto-Open Status", NotificationManager.IMPORTANCE_LOW).apply { enableVibration(false); setSound(null, null) })
@@ -347,6 +357,7 @@ class AutoOpenSnaps: MessagingRuleFeature("Auto Open Snaps", MessagingRuleType.A
private fun updateStatusNotification(force: Boolean = false) {
val now = System.currentTimeMillis()
if (!isScreenOn.get() && !force) return
if (!force && (now - lastNotificationUpdate) < notificationUpdateDelay) {
if (pendingNotificationUpdate.compareAndSet(false, true)) {
this@AutoOpenSnaps.context.coroutineScope.launch { delay(notificationUpdateDelay - (now - lastNotificationUpdate)); updateStatusNotificationInternal() }
@@ -361,6 +372,12 @@ class AutoOpenSnaps: MessagingRuleFeature("Auto Open Snaps", MessagingRuleType.A
val processed = sessionProcessed.get()
val total = totalProcessed.get()
val remaining = synchronized(queuedSnaps) { queuedSnaps.size }
// Industrial State Hashing: Prevent redundant redraws and CPU wakeups
val currentStateHash = Objects.hash(processed, total, remaining, currentStatusText, isPaused.get())
if (currentStateHash == lastNotificationStateHash && remaining == 0) return
lastNotificationStateHash = currentStateHash
val isWorking = remaining > 0
val speed = if (isWorking) getSnapsPerSecond() else 0.0
@@ -444,6 +461,8 @@ class AutoOpenSnaps: MessagingRuleFeature("Auto Open Snaps", MessagingRuleType.A
ACTION_PAUSE_RESUME -> { isPaused.set(!isPaused.get()); updateStatusNotification(force = true) }
ACTION_CLEAR_QUEUE -> { sessionProcessed.set(0); synchronized(queuedSnaps) { queuedSnaps.clear() }; updateStatusNotification(force = true) }
ACTION_STOP_ENGINE -> shutdownFeature()
Intent.ACTION_SCREEN_ON -> { isScreenOn.set(true); updateStatusNotification(force = true) }
Intent.ACTION_SCREEN_OFF -> { isScreenOn.set(false) }
Intent.ACTION_BATTERY_CHANGED -> {
val temp = intent.getIntExtra("temperature", 0) / 10f
if (temp >= 40f && !isThermalThrottled) { isThermalThrottled = true; lastThermalThrottleAt = System.currentTimeMillis() }
@@ -452,7 +471,14 @@ class AutoOpenSnaps: MessagingRuleFeature("Auto Open Snaps", MessagingRuleType.A
}
}
}
val filter = IntentFilter().apply { addAction(ACTION_PAUSE_RESUME); addAction(ACTION_CLEAR_QUEUE); addAction(ACTION_STOP_ENGINE); addAction(Intent.ACTION_BATTERY_CHANGED) }
val filter = IntentFilter().apply {
addAction(ACTION_PAUSE_RESUME)
addAction(ACTION_CLEAR_QUEUE)
addAction(ACTION_STOP_ENGINE)
addAction(Intent.ACTION_SCREEN_ON)
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)
}

View File

@@ -78,9 +78,17 @@ class HideFriendFeedEntry : MessagingRuleFeature("HideFriendFeedEntry", ruleType
}
private fun hideBoundChatFeedRow(view: View) {
view.hideViewCompletely()
(view.parent as? View)?.hideViewCompletely()
(view.parent?.parent as? View)?.hideViewCompletely()
var current: View? = view
repeat(4) {
val parent = current?.parent as? View
// Safety: Never hide the actual list container
if (parent?.javaClass?.name?.contains("RecyclerView") == true) {
current?.hideViewCompletely()
return
}
current?.hideViewCompletely()
current = parent
}
}
private fun hookCallbackMethod(