Social Tab bug fixes
This commit is contained in:
@@ -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? {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user