commit 8a19f27d96ecf51d559225c40b32e89846451d1a
parent 11b7119f8b5478adcadb013112da67044dafa6c7
Author: rhunk <101876869+rhunk@users.noreply.github.com>
Date:   Sat, 19 Aug 2023 11:58:41 +0200

feat: social section
- bridge: rules, sync
- move extensions to a new package
- snap widget broadcast receiver (SnapWidgetBroadcastReceiverHelper)
- refactor compose remember delegates

Diffstat:
Mapp/src/main/kotlin/me/rhunk/snapenhance/bridge/BridgeService.kt | 83++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------
Mapp/src/main/kotlin/me/rhunk/snapenhance/messaging/ModDatabase.kt | 128+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------------
Mapp/src/main/kotlin/me/rhunk/snapenhance/ui/manager/Section.kt | 8+++++---
Mapp/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/downloads/DownloadsSection.kt | 10++++++----
Mapp/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/features/Dialogs.kt | 18+++++++++---------
Mapp/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/features/FeaturesSection.kt | 66+++++++++++++++++++++++++++++++++---------------------------------
Aapp/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/social/AddFriendDialog.kt | 124+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aapp/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/social/FriendTab.kt | 5+++++
Aapp/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/social/SocialSection.kt | 131+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mapp/src/main/kotlin/me/rhunk/snapenhance/ui/setup/SetupActivity.kt | 13+++++++------
Mapp/src/main/kotlin/me/rhunk/snapenhance/ui/setup/screens/impl/MappingsScreen.kt | 32+++++++++++++++++---------------
Mapp/src/main/kotlin/me/rhunk/snapenhance/ui/setup/screens/impl/PickLanguageScreen.kt | 12+++++++-----
Mapp/src/main/kotlin/me/rhunk/snapenhance/ui/setup/screens/impl/SaveFolderScreen.kt | 2+-
Aapp/src/main/kotlin/me/rhunk/snapenhance/ui/util/Accompagnist.kt | 57+++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcore/src/main/aidl/me/rhunk/snapenhance/bridge/BridgeInterface.aidl | 123++++++++++++++++++++++++++++++++++++++++++++++---------------------------------
Acore/src/main/aidl/me/rhunk/snapenhance/bridge/SyncCallback.aidl | 18++++++++++++++++++
Mcore/src/main/assets/lang/en_US.json | 2+-
Mcore/src/main/kotlin/me/rhunk/snapenhance/EventDispatcher.kt | 26++++++++++++++++++++++++--
Dcore/src/main/kotlin/me/rhunk/snapenhance/SharedContext.kt | 35-----------------------------------
Mcore/src/main/kotlin/me/rhunk/snapenhance/SnapEnhance.kt | 56+++++++++++++++++++++++++++++++++++++++++++++++++++++++-
Mcore/src/main/kotlin/me/rhunk/snapenhance/action/impl/ExportChatMessages.kt | 18+++++++++---------
Mcore/src/main/kotlin/me/rhunk/snapenhance/bridge/BridgeClient.kt | 17+++++++++++++++++
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/eventbus/EventBus.kt | 6+++++-
Acore/src/main/kotlin/me/rhunk/snapenhance/core/eventbus/events/impl/SnapWidgetBroadcastReceiveEvent.kt | 12++++++++++++
Dcore/src/main/kotlin/me/rhunk/snapenhance/core/messaging/EnumConversationFeature.kt | 11-----------
Dcore/src/main/kotlin/me/rhunk/snapenhance/core/messaging/ManagementObjects.kt | 29-----------------------------
Acore/src/main/kotlin/me/rhunk/snapenhance/core/messaging/MessagingCoreObjects.kt | 56++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcore/src/main/kotlin/me/rhunk/snapenhance/data/wrapper/impl/Message.kt | 2+-
Mcore/src/main/kotlin/me/rhunk/snapenhance/data/wrapper/impl/MessageContent.kt | 4++--
Mcore/src/main/kotlin/me/rhunk/snapenhance/data/wrapper/impl/MessageDescriptor.kt | 2+-
Mcore/src/main/kotlin/me/rhunk/snapenhance/data/wrapper/impl/MessageDestinations.kt | 4++--
Mcore/src/main/kotlin/me/rhunk/snapenhance/data/wrapper/impl/MessageMetadata.kt | 2+-
Mcore/src/main/kotlin/me/rhunk/snapenhance/data/wrapper/impl/SnapUUID.kt | 2+-
Mcore/src/main/kotlin/me/rhunk/snapenhance/data/wrapper/impl/UserIdToReaction.kt | 2+-
Mcore/src/main/kotlin/me/rhunk/snapenhance/data/wrapper/impl/media/MediaInfo.kt | 2+-
Mcore/src/main/kotlin/me/rhunk/snapenhance/data/wrapper/impl/media/opera/ParamMap.kt | 2+-
Mcore/src/main/kotlin/me/rhunk/snapenhance/database/DatabaseAccess.kt | 18+++++++++---------
Mcore/src/main/kotlin/me/rhunk/snapenhance/database/objects/ConversationMessage.kt | 8++++----
Acore/src/main/kotlin/me/rhunk/snapenhance/database/objects/FriendFeedEntry.kt | 47+++++++++++++++++++++++++++++++++++++++++++++++
Dcore/src/main/kotlin/me/rhunk/snapenhance/database/objects/FriendFeedInfo.kt | 38--------------------------------------
Mcore/src/main/kotlin/me/rhunk/snapenhance/database/objects/FriendInfo.kt | 9+++++----
Mcore/src/main/kotlin/me/rhunk/snapenhance/database/objects/StoryEntry.kt | 4++--
Mcore/src/main/kotlin/me/rhunk/snapenhance/database/objects/UserConversationLink.kt | 4++--
Mcore/src/main/kotlin/me/rhunk/snapenhance/download/DownloadTaskManager.kt | 4++--
Mcore/src/main/kotlin/me/rhunk/snapenhance/features/impl/ConfigurationOverride.kt | 2+-
Mcore/src/main/kotlin/me/rhunk/snapenhance/features/impl/Messaging.kt | 2+-
Mcore/src/main/kotlin/me/rhunk/snapenhance/features/impl/downloader/MediaDownloader.kt | 5++---
Mcore/src/main/kotlin/me/rhunk/snapenhance/features/impl/experiments/UnlimitedMultiSnap.kt | 2+-
Mcore/src/main/kotlin/me/rhunk/snapenhance/features/impl/spying/AnonymousStoryViewing.kt | 2+-
Mcore/src/main/kotlin/me/rhunk/snapenhance/features/impl/spying/MessageLogger.kt | 2+-
Mcore/src/main/kotlin/me/rhunk/snapenhance/features/impl/tweaks/AutoSave.kt | 2+-
Mcore/src/main/kotlin/me/rhunk/snapenhance/features/impl/tweaks/Notifications.kt | 37+++++++++++++++++--------------------
Mcore/src/main/kotlin/me/rhunk/snapenhance/features/impl/ui/PinConversations.kt | 4++--
Dcore/src/main/kotlin/me/rhunk/snapenhance/manager/impl/MappingManager.kt | 190-------------------------------------------------------------------------------
Mcore/src/main/kotlin/me/rhunk/snapenhance/ui/menu/impl/FriendFeedInfoMenu.kt | 6+++---
Dcore/src/main/kotlin/me/rhunk/snapenhance/util/ActivityResultCallback.kt | 6------
Dcore/src/main/kotlin/me/rhunk/snapenhance/util/AndroidCompatExtensions.kt | 13-------------
Dcore/src/main/kotlin/me/rhunk/snapenhance/util/DbCursorExt.kt | 38--------------------------------------
Acore/src/main/kotlin/me/rhunk/snapenhance/util/SerializableDataObject.kt | 23+++++++++++++++++++++++
Dcore/src/main/kotlin/me/rhunk/snapenhance/util/XposedHelperExt.kt | 20--------------------
Mcore/src/main/kotlin/me/rhunk/snapenhance/util/export/MessageExporter.kt | 18+++++++++---------
Acore/src/main/kotlin/me/rhunk/snapenhance/util/ktx/AndroidCompatExtensions.kt | 13+++++++++++++
Acore/src/main/kotlin/me/rhunk/snapenhance/util/ktx/DbCursorExt.kt | 38++++++++++++++++++++++++++++++++++++++
Acore/src/main/kotlin/me/rhunk/snapenhance/util/ktx/XposedHelperExt.kt | 20++++++++++++++++++++
Acore/src/main/kotlin/me/rhunk/snapenhance/util/snap/SnapWidgetBroadcastReceiverHelper.kt | 25+++++++++++++++++++++++++
65 files changed, 1087 insertions(+), 633 deletions(-)

diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/bridge/BridgeService.kt b/app/src/main/kotlin/me/rhunk/snapenhance/bridge/BridgeService.kt @@ -3,17 +3,25 @@ package me.rhunk.snapenhance.bridge import android.app.Service import android.content.Intent import android.os.IBinder +import me.rhunk.snapenhance.Logger import me.rhunk.snapenhance.RemoteSideContext import me.rhunk.snapenhance.SharedContextHolder import me.rhunk.snapenhance.bridge.types.BridgeFileType import me.rhunk.snapenhance.bridge.types.FileActionType import me.rhunk.snapenhance.bridge.wrapper.LocaleWrapper import me.rhunk.snapenhance.bridge.wrapper.MessageLoggerWrapper +import me.rhunk.snapenhance.core.messaging.MessagingFriendInfo +import me.rhunk.snapenhance.core.messaging.MessagingGroupInfo +import me.rhunk.snapenhance.core.messaging.RuleScope +import me.rhunk.snapenhance.database.objects.FriendInfo import me.rhunk.snapenhance.download.DownloadProcessor +import me.rhunk.snapenhance.util.SerializableDataObject +import kotlin.system.measureTimeMillis class BridgeService : Service() { private lateinit var messageLoggerWrapper: MessageLoggerWrapper private lateinit var remoteSideContext: RemoteSideContext + private lateinit var syncCallback: SyncCallback override fun onBind(intent: Intent): IBinder { remoteSideContext = SharedContextHolder.remote(this).apply { @@ -25,28 +33,36 @@ class BridgeService : Service() { inner class BridgeBinder : BridgeInterface.Stub() { override fun fileOperation(action: Int, fileType: Int, content: ByteArray?): ByteArray { - val resolvedFile by lazy { BridgeFileType.fromValue(fileType)?.resolve(this@BridgeService) } + val resolvedFile by lazy { + BridgeFileType.fromValue(fileType)?.resolve(this@BridgeService) + } return when (FileActionType.values()[action]) { FileActionType.CREATE_AND_READ -> { resolvedFile?.let { if (!it.exists()) { - return content?.also { content -> it.writeBytes(content) } ?: ByteArray(0) + return content?.also { content -> it.writeBytes(content) } ?: ByteArray( + 0 + ) } it.readBytes() } ?: ByteArray(0) } + FileActionType.READ -> { resolvedFile?.takeIf { it.exists() }?.readBytes() ?: ByteArray(0) } + FileActionType.WRITE -> { content?.also { resolvedFile?.writeBytes(content) } ?: ByteArray(0) } + FileActionType.DELETE -> { resolvedFile?.takeIf { it.exists() }?.delete() ByteArray(0) } + FileActionType.EXISTS -> { if (resolvedFile?.exists() == true) ByteArray(1) @@ -55,27 +71,76 @@ class BridgeService : Service() { } } - override fun getLoggedMessageIds(conversationId: String, limit: Int) = messageLoggerWrapper.getMessageIds(conversationId, limit).toLongArray() + override fun getLoggedMessageIds(conversationId: String, limit: Int) = + messageLoggerWrapper.getMessageIds(conversationId, limit).toLongArray() - override fun getMessageLoggerMessage(conversationId: String, id: Long) = messageLoggerWrapper.getMessage(conversationId, id).second + override fun getMessageLoggerMessage(conversationId: String, id: Long) = + messageLoggerWrapper.getMessage(conversationId, id).second override fun addMessageLoggerMessage(conversationId: String, id: Long, message: ByteArray) { messageLoggerWrapper.addMessage(conversationId, id, message) } - override fun deleteMessageLoggerMessage(conversationId: String, id: Long) = messageLoggerWrapper.deleteMessage(conversationId, id) + override fun deleteMessageLoggerMessage(conversationId: String, id: Long) = + messageLoggerWrapper.deleteMessage(conversationId, id) override fun clearMessageLogger() = messageLoggerWrapper.clearMessages() - override fun fetchLocales(userLocale: String) = LocaleWrapper.fetchLocales(context = this@BridgeService, userLocale).associate { - it.locale to it.content - } + override fun fetchLocales(userLocale: String) = + LocaleWrapper.fetchLocales(context = this@BridgeService, userLocale).associate { + it.locale to it.content + } override fun enqueueDownload(intent: Intent, callback: DownloadCallback) { DownloadProcessor( - remoteSideContext = SharedContextHolder.remote(this@BridgeService), + remoteSideContext = remoteSideContext, callback = callback ).onReceive(intent) } + + override fun getRules(objectType: String, uuid: String): MutableList<String> { + remoteSideContext.modDatabase.getRulesFromId(RuleScope.valueOf(objectType), uuid) + .let { rules -> + return rules.map { it.toJson() }.toMutableList() + } + } + + override fun sync(callback: SyncCallback) { + Logger.debug("Syncing remote") + syncCallback = callback + measureTimeMillis { + remoteSideContext.modDatabase.getFriendsIds().forEach { friendId -> + runCatching { + SerializableDataObject.fromJson<FriendInfo>(callback.syncFriend(friendId)).let { + remoteSideContext.modDatabase.syncFriend(it) + } + }.onFailure { + Logger.error("Failed to sync friend $friendId", it) + } + } + remoteSideContext.modDatabase.getGroupsIds().forEach { groupId -> + runCatching { + SerializableDataObject.fromJson<MessagingGroupInfo>(callback.syncGroup(groupId)).let { + remoteSideContext.modDatabase.syncGroupInfo(it) + } + }.onFailure { + Logger.error("Failed to sync group $groupId", it) + } + } + }.also { + Logger.debug("Syncing remote took $it ms") + } + } + + override fun passGroupsAndFriends( + groups: List<String>, + friends: List<String> + ) { + Logger.debug("Received ${groups.size} groups and ${friends.size} friends") + remoteSideContext.modDatabase.receiveMessagingDataCallback( + friends.map { SerializableDataObject.fromJson<MessagingFriendInfo>(it) }, + groups.map { SerializableDataObject.fromJson<MessagingGroupInfo>(it) } + ) + } } } diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/messaging/ModDatabase.kt b/app/src/main/kotlin/me/rhunk/snapenhance/messaging/ModDatabase.kt @@ -1,16 +1,19 @@ package me.rhunk.snapenhance.messaging import android.database.sqlite.SQLiteDatabase +import me.rhunk.snapenhance.Logger import me.rhunk.snapenhance.RemoteSideContext import me.rhunk.snapenhance.core.messaging.FriendStreaks +import me.rhunk.snapenhance.core.messaging.MessagingFriendInfo +import me.rhunk.snapenhance.core.messaging.MessagingGroupInfo import me.rhunk.snapenhance.core.messaging.MessagingRule import me.rhunk.snapenhance.core.messaging.Mode -import me.rhunk.snapenhance.core.messaging.ObjectType +import me.rhunk.snapenhance.core.messaging.RuleScope import me.rhunk.snapenhance.database.objects.FriendInfo import me.rhunk.snapenhance.util.SQLiteDatabaseHelper -import me.rhunk.snapenhance.util.getInteger -import me.rhunk.snapenhance.util.getLongOrNull -import me.rhunk.snapenhance.util.getStringOrNull +import me.rhunk.snapenhance.util.ktx.getInteger +import me.rhunk.snapenhance.util.ktx.getLongOrNull +import me.rhunk.snapenhance.util.ktx.getStringOrNull import java.util.concurrent.Executors @@ -20,19 +23,27 @@ class ModDatabase( private val executor = Executors.newSingleThreadExecutor() private lateinit var database: SQLiteDatabase + var receiveMessagingDataCallback: (friends: List<MessagingFriendInfo>, groups: List<MessagingGroupInfo>) -> Unit = { _, _ -> } + + fun init() { database = context.androidContext.openOrCreateDatabase("main.db", 0, null) SQLiteDatabaseHelper.createTablesFromSchema(database, mapOf( "friends" to listOf( "userId VARCHAR PRIMARY KEY", "displayName VARCHAR", - "mutable_username VARCHAR", + "mutableUsername VARCHAR", "bitmojiId VARCHAR", "selfieId VARCHAR" ), + "groups" to listOf( + "conversationId VARCHAR PRIMARY KEY", + "name VARCHAR", + "participantsCount INTEGER" + ), "rules" to listOf( "id INTEGER PRIMARY KEY AUTOINCREMENT", - "objectType VARCHAR", + "scope VARCHAR", "targetUuid VARCHAR", "enabled BOOLEAN", "mode VARCHAR", @@ -59,27 +70,94 @@ class ModDatabase( )) } - fun syncFriends(friends: List<FriendInfo>) { + fun getFriendsIds(): List<String> { + return database.rawQuery("SELECT userId FROM friends", null).use { cursor -> + val ids = mutableListOf<String>() + while (cursor.moveToNext()) { + ids.add(cursor.getString(0)) + } + ids + } + } + + fun getGroupsIds(): List<String> { + return database.rawQuery("SELECT conversationId FROM groups", null).use { cursor -> + val ids = mutableListOf<String>() + while (cursor.moveToNext()) { + ids.add(cursor.getString(0)) + } + ids + } + } + + fun getGroups(): List<MessagingGroupInfo> { + return database.rawQuery("SELECT * FROM groups", null).use { cursor -> + val groups = mutableListOf<MessagingGroupInfo>() + while (cursor.moveToNext()) { + groups.add(MessagingGroupInfo( + conversationId = cursor.getStringOrNull("conversationId")!!, + name = cursor.getStringOrNull("name")!!, + participantsCount = cursor.getInteger("participantsCount") + )) + } + groups + } + } + + fun getFriends(): List<MessagingFriendInfo> { + return database.rawQuery("SELECT * FROM friends", null).use { cursor -> + val friends = mutableListOf<MessagingFriendInfo>() + while (cursor.moveToNext()) { + runCatching { + friends.add(MessagingFriendInfo( + userId = cursor.getStringOrNull("userId")!!, + displayName = cursor.getStringOrNull("displayName"), + mutableUsername = cursor.getStringOrNull("mutableUsername")!!, + bitmojiId = cursor.getStringOrNull("bitmojiId"), + selfieId = cursor.getStringOrNull("selfieId") + )) + }.onFailure { + Logger.error("Failed to parse friend", it) + } + } + friends + } + } + + + fun syncGroupInfo(conversationInfo: MessagingGroupInfo) { + executor.execute { + try { + database.execSQL("INSERT OR REPLACE INTO groups VALUES (?, ?, ?)", arrayOf( + conversationInfo.conversationId, + conversationInfo.name, + conversationInfo.participantsCount + )) + } catch (e: Exception) { + throw e + } + } + } + + fun syncFriend(friend: FriendInfo) { executor.execute { try { - friends.forEach { friend -> - database.execSQL("INSERT OR REPLACE INTO friends VALUES (?, ?, ?, ?, ?)", arrayOf( + database.execSQL("INSERT OR REPLACE INTO friends VALUES (?, ?, ?, ?, ?)", arrayOf( + friend.userId, + friend.displayName, + friend.usernameForSorting!!.split("|")[1], + friend.bitmojiAvatarId, + friend.bitmojiSelfieId + )) + //sync streaks + if (friend.streakLength > 0) { + database.execSQL("INSERT OR REPLACE INTO streaks (userId, expirationTimestamp, count) VALUES (?, ?, ?)", arrayOf( friend.userId, - friend.displayName, - friend.username, - friend.bitmojiAvatarId, - friend.bitmojiSelfieId + friend.streakExpirationTimestamp, + friend.streakLength )) - //sync streaks - if (friend.streakLength > 0) { - database.execSQL("INSERT OR REPLACE INTO streaks (userId, expirationTimestamp, count) VALUES (?, ?, ?)", arrayOf( - friend.userId, - friend.streakExpirationTimestamp, - friend.streakLength - )) - } else { - database.execSQL("DELETE FROM streaks WHERE userId = ?", arrayOf(friend.userId)) - } + } else { + database.execSQL("DELETE FROM streaks WHERE userId = ?", arrayOf(friend.userId)) } } catch (e: Exception) { throw e @@ -87,13 +165,13 @@ class ModDatabase( } } - fun getRulesFromId(type: ObjectType, targetUuid: String): List<MessagingRule> { + fun getRulesFromId(type: RuleScope, targetUuid: String): List<MessagingRule> { return database.rawQuery("SELECT * FROM rules WHERE objectType = ? AND targetUuid = ?", arrayOf(type.name, targetUuid)).use { cursor -> val rules = mutableListOf<MessagingRule>() while (cursor.moveToNext()) { rules.add(MessagingRule( id = cursor.getInteger("id"), - objectType = ObjectType.valueOf(cursor.getStringOrNull("objectType")!!), + ruleScope = RuleScope.valueOf(cursor.getStringOrNull("scope")!!), targetUuid = cursor.getStringOrNull("targetUuid")!!, enabled = cursor.getInteger("enabled") == 1, mode = Mode.valueOf(cursor.getStringOrNull("mode")!!), diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/Section.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/Section.kt @@ -17,6 +17,7 @@ import me.rhunk.snapenhance.ui.manager.sections.HomeSection import me.rhunk.snapenhance.ui.manager.sections.NotImplemented import me.rhunk.snapenhance.ui.manager.sections.downloads.DownloadsSection import me.rhunk.snapenhance.ui.manager.sections.features.FeaturesSection +import me.rhunk.snapenhance.ui.manager.sections.social.SocialSection import kotlin.reflect.KClass enum class EnumSection( @@ -39,9 +40,10 @@ enum class EnumSection( icon = Icons.Filled.Home, section = HomeSection::class ), - FRIENDS( - route = "friends", - icon = Icons.Filled.Group + SOCIAL( + route = "social", + icon = Icons.Filled.Group, + section = SocialSection::class ), PLUGINS( route = "plugins", diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/downloads/DownloadsSection.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/downloads/DownloadsSection.kt @@ -34,9 +34,11 @@ import androidx.compose.material3.RadioButton import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.blur @@ -102,15 +104,15 @@ class DownloadsSection : Section() { @Composable private fun FilterList() { val coroutineScope = rememberCoroutineScope() - val showMenu = remember { mutableStateOf(false) } - IconButton(onClick = { showMenu.value = !showMenu.value}) { + var showMenu by remember { mutableStateOf(false) } + IconButton(onClick = { showMenu = !showMenu}) { Icon( imageVector = Icons.Default.FilterList, contentDescription = null ) } - DropdownMenu(expanded = showMenu.value, onDismissRequest = { showMenu.value = false }) { + DropdownMenu(expanded = showMenu, onDismissRequest = { showMenu = false }) { MediaFilter.values().toList().forEach { filter -> DropdownMenuItem( text = { @@ -130,7 +132,7 @@ class DownloadsSection : Section() { onClick = { coroutineScope.launch { loadByFilter(filter) - showMenu.value = false + showMenu = false } } ) diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/features/Dialogs.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/features/Dialogs.kt @@ -18,8 +18,10 @@ import androidx.compose.material3.Switch import androidx.compose.material3.Text import androidx.compose.material3.TextField import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester @@ -72,14 +74,14 @@ class Dialogs( add(0, "null") } - val selectedValue = remember { + var selectedValue by remember { mutableStateOf(property.value.getNullable()?.toString() ?: "null") } DefaultDialogCard { keys.forEachIndexed { index, item -> fun select() { - selectedValue.value = item + selectedValue = item property.value.setAny(if (index == 0) { null } else { @@ -97,7 +99,7 @@ class Dialogs( modifier = Modifier.weight(1f) ) RadioButton( - selected = selectedValue.value == item, + selected = selectedValue == item, onClick = { select() } ) } @@ -179,13 +181,11 @@ class Dialogs( val toggledStates = property.value.get() as MutableList<String> DefaultDialogCard { defaultItems.forEach { key -> - val state = remember { - mutableStateOf(toggledStates.contains(key)) - } + var state by remember { mutableStateOf(toggledStates.contains(key)) } fun toggle(value: Boolean? = null) { - state.value = value ?: !state.value - if (state.value) { + state = value ?: !state + if (state) { toggledStates.add(key) } else { toggledStates.remove(key) @@ -203,7 +203,7 @@ class Dialogs( .weight(1f) ) Switch( - checked = state.value, + checked = state, onCheckedChange = { toggle(it) } diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/features/FeaturesSection.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/features/FeaturesSection.kt @@ -44,9 +44,11 @@ import androidx.compose.material3.TextFieldDefaults import androidx.compose.material3.rememberBottomSheetScaffoldState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester @@ -172,18 +174,16 @@ class FeaturesSection : Section() { @Composable private fun PropertyAction(property: PropertyPair<*>, registerClickCallback: RegisterClickCallback) { - val showDialog = remember { mutableStateOf(false) } - val dialogComposable = remember { mutableStateOf<@Composable () -> Unit>({}) } + var showDialog by remember { mutableStateOf(false) } + var dialogComposable by remember { mutableStateOf<@Composable () -> Unit>({}) } - fun registerDialogOnClickCallback() = registerClickCallback { - showDialog.value = true - } + fun registerDialogOnClickCallback() = registerClickCallback { showDialog = true } - if (showDialog.value) { + if (showDialog) { Dialog( - onDismissRequest = { showDialog.value = false } + onDismissRequest = { showDialog = false } ) { - dialogComposable.value() + dialogComposable() } } @@ -203,12 +203,12 @@ class FeaturesSection : Section() { when (val dataType = remember { property.key.dataType.type }) { DataProcessors.Type.BOOLEAN -> { - val state = remember { mutableStateOf(propertyValue.get() as Boolean) } + var state by remember { mutableStateOf(propertyValue.get() as Boolean) } Switch( - checked = state.value, + checked = state, onCheckedChange = registerClickCallback { - state.value = state.value.not() - propertyValue.setAny(state.value) + state = state.not() + propertyValue.setAny(state) } ) } @@ -216,7 +216,7 @@ class FeaturesSection : Section() { DataProcessors.Type.STRING_UNIQUE_SELECTION -> { registerDialogOnClickCallback() - dialogComposable.value = { + dialogComposable = { dialogs.UniqueSelectionDialog(property) } @@ -233,13 +233,13 @@ class FeaturesSection : Section() { } DataProcessors.Type.STRING_MULTIPLE_SELECTION, DataProcessors.Type.STRING, DataProcessors.Type.INTEGER, DataProcessors.Type.FLOAT -> { - dialogComposable.value = { + dialogComposable = { when (dataType) { DataProcessors.Type.STRING_MULTIPLE_SELECTION -> { dialogs.MultipleSelectionDialog(property) } DataProcessors.Type.STRING, DataProcessors.Type.INTEGER, DataProcessors.Type.FLOAT -> { - dialogs.KeyboardInputDialog(property) { showDialog.value = false } + dialogs.KeyboardInputDialog(property) { showDialog = false } } else -> {} } @@ -271,7 +271,7 @@ class FeaturesSection : Section() { if (container.globalState == null) return - val state = remember { mutableStateOf(container.globalState!!) } + var state by remember { mutableStateOf(container.globalState!!) } Box( modifier = Modifier @@ -288,10 +288,10 @@ class FeaturesSection : Section() { } Switch( - checked = state.value, + checked = state, onCheckedChange = { - state.value = state.value.not() - container.globalState = state.value + state = state.not() + container.globalState = state } ) } @@ -301,7 +301,7 @@ class FeaturesSection : Section() { @Composable private fun PropertyCard(property: PropertyPair<*>) { - val clickCallback = remember { mutableStateOf<ClickCallback?>(null) } + var clickCallback by remember { mutableStateOf<ClickCallback?>(null) } Card( modifier = Modifier .fillMaxWidth() @@ -311,7 +311,7 @@ class FeaturesSection : Section() { modifier = Modifier .fillMaxSize() .clickable { - clickCallback.value?.invoke(true) + clickCallback?.invoke(true) } .padding(all = 4.dp), horizontalArrangement = Arrangement.SpaceBetween @@ -371,7 +371,7 @@ class FeaturesSection : Section() { verticalAlignment = Alignment.CenterVertically ) { PropertyAction(property, registerClickCallback = { callback -> - clickCallback.value = callback + clickCallback = callback callback }) } @@ -381,20 +381,20 @@ class FeaturesSection : Section() { @Composable private fun FeatureSearchBar(rowScope: RowScope, focusRequester: FocusRequester) { - val searchValue = remember { mutableStateOf("") } + var searchValue by remember { mutableStateOf("") } val scope = rememberCoroutineScope() - val currentSearchJob = remember { mutableStateOf<Job?>(null) } + var currentSearchJob by remember { mutableStateOf<Job?>(null) } rowScope.apply { TextField( - value = searchValue.value, + value = searchValue, onValueChange = { keyword -> - searchValue.value = keyword + searchValue = keyword if (keyword.isEmpty()) { navController.navigate(MAIN_ROUTE) return@TextField } - currentSearchJob.value?.cancel() + currentSearchJob?.cancel() scope.launch { delay(300) navController.navigate(SEARCH_FEATURE_ROUTE.replace("{keyword}", keyword), NavOptions.Builder() @@ -402,7 +402,7 @@ class FeaturesSection : Section() { .setPopUpTo(MAIN_ROUTE, false) .build() ) - }.also { currentSearchJob.value = it } + }.also { currentSearchJob = it } }, keyboardActions = KeyboardActions(onDone = { @@ -428,10 +428,10 @@ class FeaturesSection : Section() { @Composable override fun TopBarActions(rowScope: RowScope) { - val showSearchBar = remember { mutableStateOf(false) } + var showSearchBar by remember { mutableStateOf(false) } val focusRequester = remember { FocusRequester() } - if (showSearchBar.value) { + if (showSearchBar) { FeatureSearchBar(rowScope, focusRequester) LaunchedEffect(true) { focusRequester.requestFocus() @@ -439,13 +439,13 @@ class FeaturesSection : Section() { } IconButton(onClick = { - showSearchBar.value = showSearchBar.value.not() - if (!showSearchBar.value && navController.currentBackStackEntry?.destination?.route == SEARCH_FEATURE_ROUTE) { + showSearchBar = showSearchBar.not() + if (!showSearchBar && navController.currentBackStackEntry?.destination?.route == SEARCH_FEATURE_ROUTE) { navController.navigate(MAIN_ROUTE) } }) { Icon( - imageVector = if (showSearchBar.value) Icons.Filled.Close + imageVector = if (showSearchBar) Icons.Filled.Close else Icons.Filled.Search, contentDescription = null ) diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/social/AddFriendDialog.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/social/AddFriendDialog.kt @@ -0,0 +1,123 @@ +package me.rhunk.snapenhance.ui.manager.sections.social + +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material3.Card +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.compose.ui.window.Dialog +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import me.rhunk.snapenhance.Logger +import me.rhunk.snapenhance.RemoteSideContext +import me.rhunk.snapenhance.bridge.BridgeClient +import me.rhunk.snapenhance.core.messaging.MessagingFriendInfo +import me.rhunk.snapenhance.core.messaging.MessagingGroupInfo +import me.rhunk.snapenhance.util.snap.SnapWidgetBroadcastReceiverHelper + +class AddFriendDialog( + private val context: RemoteSideContext, + private val section: SocialSection, +) { + + + @Composable + private fun ListCardEntry(name: String) { + Card( + modifier = Modifier.padding(5.dp), + ) { + Text(text = name, modifier = Modifier.padding(10.dp)) + } + } + + + @Composable + fun Content(dismiss: () -> Unit = { }) { + var cachedFriends by remember { mutableStateOf(null as List<MessagingFriendInfo>?) } + var cachedGroups by remember { mutableStateOf(null as List<MessagingGroupInfo>?) } + + val coroutineScope = rememberCoroutineScope() + + var timeoutJob: Job? = null + var hasFetchError by remember { mutableStateOf(false) } + + LaunchedEffect(Unit) { + context.modDatabase.receiveMessagingDataCallback = { friends, groups -> + cachedFriends = friends + cachedGroups = groups + timeoutJob?.cancel() + hasFetchError = false + } + SnapWidgetBroadcastReceiverHelper.create(BridgeClient.BRIDGE_SYNC_ACTION) {}.also { + runCatching { + context.androidContext.sendBroadcast(it) + }.onFailure { + Logger.error("Failed to send broadcast", it) + hasFetchError = true + } + } + timeoutJob = coroutineScope.launch { + withContext(Dispatchers.IO) { + delay(10000) + hasFetchError = true + } + } + } + + Dialog(onDismissRequest = { + timeoutJob?.cancel() + dismiss() + }) { + if (hasFetchError) { + Text(text = "Failed to load friends and groups. Make sure Snapchat is installed and logged in.") + return@Dialog + } + if (cachedGroups == null || cachedFriends == null) { + CircularProgressIndicator( + modifier = Modifier + .padding() + .size(30.dp), + strokeWidth = 3.dp, + color = MaterialTheme.colorScheme.onPrimary + ) + return@Dialog + } + + LazyColumn( + modifier = Modifier.fillMaxSize() + ) { + item { + Text(text = "Groups", fontSize = 20.sp) + Spacer(modifier = Modifier.padding(5.dp)) + } + items(cachedGroups!!.size) { + ListCardEntry(name = cachedGroups!![it].name) + } + item { + Text(text = "Friends", fontSize = 20.sp) + Spacer(modifier = Modifier.padding(5.dp)) + } + items(cachedFriends!!.size) { + ListCardEntry(name = cachedFriends!![it].displayName ?: cachedFriends!![it].mutableUsername) + } + } + } + } +}+ \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/social/FriendTab.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/social/FriendTab.kt @@ -0,0 +1,4 @@ +package me.rhunk.snapenhance.ui.manager.sections.social + +class FriendTab { +}+ \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/social/SocialSection.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/social/SocialSection.kt @@ -0,0 +1,130 @@ +package me.rhunk.snapenhance.ui.manager.sections.social + +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.pager.HorizontalPager +import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.Add +import androidx.compose.material3.FloatingActionButton +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Tab +import androidx.compose.material3.TabRow +import androidx.compose.material3.TabRowDefaults +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import kotlinx.coroutines.launch +import me.rhunk.snapenhance.core.messaging.MessagingFriendInfo +import me.rhunk.snapenhance.core.messaging.MessagingGroupInfo +import me.rhunk.snapenhance.ui.manager.Section +import me.rhunk.snapenhance.ui.util.pagerTabIndicatorOffset + +class SocialSection : Section() { + private lateinit var friendList: List<MessagingFriendInfo> + private lateinit var groupList: List<MessagingGroupInfo> + + private val addFriendDialog by lazy { + AddFriendDialog(context, this) + } + + override fun onResumed() { + friendList = context.modDatabase.getFriends() + groupList = context.modDatabase.getGroups() + } + + + @OptIn(ExperimentalFoundationApi::class) + @Composable + override fun Content() { + val titles = listOf("Friends", "Groups") + val coroutineScope = rememberCoroutineScope() + val pagerState = rememberPagerState { titles.size } + var showAddFriendDialog by remember { mutableStateOf(false) } + + if (showAddFriendDialog) { + addFriendDialog.Content { + showAddFriendDialog = false + } + } + + Scaffold( + floatingActionButton = { + FloatingActionButton( + onClick = { + showAddFriendDialog = true + }, + modifier = Modifier.padding(10.dp), + containerColor = MaterialTheme.colorScheme.primary, + contentColor = MaterialTheme.colorScheme.onPrimary, + shape = RoundedCornerShape(16.dp), + ) { + Icon( + imageVector = Icons.Rounded.Add, + contentDescription = null + ) + } + } + ) { paddingValues -> + Column(modifier = Modifier.padding(paddingValues)) { + TabRow(selectedTabIndex = pagerState.currentPage, indicator = { tabPositions -> + TabRowDefaults.Indicator( + Modifier.pagerTabIndicatorOffset( + pagerState = pagerState, + tabPositions = tabPositions + ) + ) + }) { + titles.forEachIndexed { index, title -> + Tab( + selected = pagerState.currentPage == index, + onClick = { + coroutineScope.launch { + pagerState.animateScrollToPage( index ) + } + }, + text = { Text(text = title, maxLines = 2, overflow = TextOverflow.Ellipsis) } + ) + } + } + + HorizontalPager(modifier = Modifier.padding(paddingValues), state = pagerState) { page -> + Column( + modifier = Modifier.fillMaxSize(), + ) { + when (page) { + 0 -> { + Text(text = "Friends") + Column { + friendList.forEach { + Text(text = it.displayName ?: it.mutableUsername) + } + } + } + 1 -> { + Text(text = "Groups") + Column { + groupList.forEach { + Text(text = it.name) + } + } + } + } + } + } + } + } + } +}+ \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/setup/SetupActivity.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/setup/SetupActivity.kt @@ -24,6 +24,7 @@ import androidx.compose.material3.Scaffold import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha @@ -79,13 +80,13 @@ class SetupActivity : ComponentActivity() { setContent { val navController = rememberNavController() - val canGoNext = remember { mutableStateOf(false) } + var canGoNext by remember { mutableStateOf(false) } fun nextScreen() { - if (!canGoNext.value) return + if (!canGoNext) return requiredScreens.firstOrNull()?.onLeave() if (requiredScreens.size > 1) { - canGoNext.value = false + canGoNext = false requiredScreens.removeFirst() navController.navigate(requiredScreens.first().route) } else { @@ -102,7 +103,7 @@ class SetupActivity : ComponentActivity() { .fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally ) { - val alpha: Float by animateFloatAsState(if (canGoNext.value) 1f else 0f, + val alpha: Float by animateFloatAsState(if (canGoNext) 1f else 0f, label = "NextButton" ) @@ -114,7 +115,7 @@ class SetupActivity : ComponentActivity() { .alpha(alpha) ) { Icon( - imageVector = if (requiredScreens.size <= 1 && canGoNext.value) { + imageVector = if (requiredScreens.size <= 1 && canGoNext) { Icons.Default.Check } else { Icons.Default.ArrowForwardIos @@ -135,7 +136,7 @@ class SetupActivity : ComponentActivity() { startDestination = requiredScreens.first().route ) { requiredScreens.forEach { screen -> - screen.allowNext = { canGoNext.value = it } + screen.allowNext = { canGoNext = it } composable(screen.route) { BackHandler(true) {} Column( diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/setup/screens/impl/MappingsScreen.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/setup/screens/impl/MappingsScreen.kt @@ -11,9 +11,11 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Dialog @@ -26,12 +28,12 @@ class MappingsScreen : SetupScreen() { @Composable override fun Content() { val coroutineScope = rememberCoroutineScope() - val infoText = remember { mutableStateOf(null as String?) } - val isGenerating = remember { mutableStateOf(false) } + var infoText by remember { mutableStateOf(null as String?) } + var isGenerating by remember { mutableStateOf(false) } - if (infoText.value != null) { + if (infoText != null) { Dialog(onDismissRequest = { - infoText.value = null + infoText = null }) { Surface( modifier = Modifier.padding(16.dp).fillMaxWidth(), @@ -40,9 +42,9 @@ class MappingsScreen : SetupScreen() { Column( modifier = Modifier.padding(16.dp) ) { - Text(text = infoText.value!!) + Text(text = infoText!!) Button(onClick = { - infoText.value = null + infoText = null }, modifier = Modifier.padding(top = 5.dp).align(alignment = androidx.compose.ui.Alignment.End)) { Text(text = "OK") @@ -63,27 +65,27 @@ class MappingsScreen : SetupScreen() { } } - val hasMappings = remember { mutableStateOf(false) } + var hasMappings by remember { mutableStateOf(false) } DialogText(text = context.translation["setup.mappings.dialog"]) - if (hasMappings.value) return + if (hasMappings) return Button(onClick = { - if (isGenerating.value) return@Button - isGenerating.value = true + if (isGenerating) return@Button + isGenerating = true coroutineScope.launch(Dispatchers.IO) { runCatching { tryToGenerateMappings() allowNext(true) - infoText.value = context.translation["setup.mappings.generate_success"] - hasMappings.value = true + infoText = context.translation["setup.mappings.generate_success"] + hasMappings = true }.onFailure { - isGenerating.value = false - infoText.value = context.translation["setup.mappings.generate_failure"] + "\n\n" + it.message + isGenerating = false + infoText = context.translation["setup.mappings.generate_failure"] + "\n\n" + it.message Logger.error("Failed to generate mappings", it) } } }) { - if (isGenerating.value) { + if (isGenerating) { CircularProgressIndicator( modifier = Modifier.padding().size(30.dp), strokeWidth = 3.dp, diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/setup/screens/impl/PickLanguageScreen.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/setup/screens/impl/PickLanguageScreen.kt @@ -15,8 +15,10 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.text.font.FontWeight @@ -78,10 +80,10 @@ class PickLanguageScreen : SetupScreen(){ DialogText(text = context.translation["setup.dialogs.select_language"]) - val isDialog = remember { mutableStateOf(false) } + var isDialog by remember { mutableStateOf(false) } - if (isDialog.value) { - Dialog(onDismissRequest = { isDialog.value = false }) { + if (isDialog) { + Dialog(onDismissRequest = { isDialog = false }) { Surface( modifier = Modifier .padding(10.dp) @@ -98,7 +100,7 @@ class PickLanguageScreen : SetupScreen(){ .fillMaxWidth() .clickable { selectedLocale.value = locale - isDialog.value = false + isDialog = false }, contentAlignment = Alignment.Center ) { @@ -121,7 +123,7 @@ class PickLanguageScreen : SetupScreen(){ contentAlignment = Alignment.Center ) { Button(onClick = { - isDialog.value = true + isDialog = true }) { Text(text = getLocaleDisplayName(selectedLocale.value), fontSize = 16.sp, fontWeight = FontWeight.Normal) diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/setup/screens/impl/SaveFolderScreen.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/setup/screens/impl/SaveFolderScreen.kt @@ -9,9 +9,9 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import me.rhunk.snapenhance.Logger -import me.rhunk.snapenhance.ui.util.ObservableMutableState import me.rhunk.snapenhance.ui.setup.screens.SetupScreen import me.rhunk.snapenhance.ui.util.ChooseFolderHelper +import me.rhunk.snapenhance.ui.util.ObservableMutableState class SaveFolderScreen : SetupScreen() { private lateinit var saveFolder: ObservableMutableState<String> diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/util/Accompagnist.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/util/Accompagnist.kt @@ -0,0 +1,56 @@ +package me.rhunk.snapenhance.ui.util + +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.pager.PagerState +import androidx.compose.material3.TabPosition +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.layout +import androidx.compose.ui.unit.Constraints +import androidx.compose.ui.unit.lerp + +//https://github.com/google/accompanist/blob/main/pager-indicators/src/main/java/com/google/accompanist/pager/PagerTab.kt#L78 +@OptIn(ExperimentalFoundationApi::class) +fun Modifier.pagerTabIndicatorOffset( + pagerState: PagerState, + tabPositions: List<TabPosition>, + pageIndexMapping: (Int) -> Int = { it }, +): Modifier = layout { measurable, constraints -> + if (tabPositions.isEmpty()) { + // If there are no pages, nothing to show + layout(constraints.maxWidth, 0) {} + } else { + val currentPage = minOf(tabPositions.lastIndex, pageIndexMapping(pagerState.currentPage)) + val currentTab = tabPositions[currentPage] + val previousTab = tabPositions.getOrNull(currentPage - 1) + val nextTab = tabPositions.getOrNull(currentPage + 1) + val fraction = pagerState.currentPageOffsetFraction + val indicatorWidth = if (fraction > 0 && nextTab != null) { + lerp(currentTab.width, nextTab.width, fraction).roundToPx() + } else if (fraction < 0 && previousTab != null) { + lerp(currentTab.width, previousTab.width, -fraction).roundToPx() + } else { + currentTab.width.roundToPx() + } + val indicatorOffset = if (fraction > 0 && nextTab != null) { + lerp(currentTab.left, nextTab.left, fraction).roundToPx() + } else if (fraction < 0 && previousTab != null) { + lerp(currentTab.left, previousTab.left, -fraction).roundToPx() + } else { + currentTab.left.roundToPx() + } + val placeable = measurable.measure( + Constraints( + minWidth = indicatorWidth, + maxWidth = indicatorWidth, + minHeight = 0, + maxHeight = constraints.maxHeight + ) + ) + layout(constraints.maxWidth, maxOf(placeable.height, constraints.minHeight)) { + placeable.placeRelative( + indicatorOffset, + maxOf(constraints.minHeight - placeable.height, 0) + ) + } + } +}+ \ No newline at end of file diff --git a/core/src/main/aidl/me/rhunk/snapenhance/bridge/BridgeInterface.aidl b/core/src/main/aidl/me/rhunk/snapenhance/bridge/BridgeInterface.aidl @@ -2,58 +2,77 @@ package me.rhunk.snapenhance.bridge; import java.util.List; import me.rhunk.snapenhance.bridge.DownloadCallback; +import me.rhunk.snapenhance.bridge.SyncCallback; interface BridgeInterface { - /** - * Execute a file operation - */ - byte[] fileOperation(int action, int fileType, in @nullable byte[] content); - - /** - * Get the content of a logged message from the database - * - * @param conversationId the ID of the conversation - * @return the content of the message - */ - long[] getLoggedMessageIds(String conversationId, int limit); - - /** - * Get the content of a logged message from the database - * - * @param id the ID of the message logger message - * @return the content of the message - */ - @nullable byte[] getMessageLoggerMessage(String conversationId, long id); - - /** - * Add a message to the message logger database - * - * @param id the ID of the message logger message - * @param message the content of the message - */ - void addMessageLoggerMessage(String conversationId, long id, in byte[] message); - - /** - * Delete a message from the message logger database - * - * @param id the ID of the message logger message - */ - void deleteMessageLoggerMessage(String conversationId, long id); - - /** - * Clear the message logger database - */ - void clearMessageLogger(); - - /** - * Fetch the locales - * - * @return the locale result - */ - Map<String, String> fetchLocales(String userLocale); - - /** - * Enqueue a download - */ - void enqueueDownload(in Intent intent, DownloadCallback callback); + /** + * Execute a file operation + */ + byte[] fileOperation(int action, int fileType, in @nullable byte[] content); + + /** + * Get the content of a logged message from the database + * + * @param conversationId the ID of the conversation + * @return the content of the message + */ + long[] getLoggedMessageIds(String conversationId, int limit); + + /** + * Get the content of a logged message from the database + * + * @param id the ID of the message logger message + * @return the content of the message + */ + @nullable byte[] getMessageLoggerMessage(String conversationId, long id); + + /** + * Add a message to the message logger database + * + * @param id the ID of the message logger message + * @param message the content of the message + */ + void addMessageLoggerMessage(String conversationId, long id, in byte[] message); + + /** + * Delete a message from the message logger database + * + * @param id the ID of the message logger message + */ + void deleteMessageLoggerMessage(String conversationId, long id); + + /** + * Clear the message logger database + */ + void clearMessageLogger(); + + /** + * Fetch the locales + * + * @return the locale result + */ + Map<String, String> fetchLocales(String userLocale); + + /** + * Enqueue a download + */ + void enqueueDownload(in Intent intent, DownloadCallback callback); + + /** + * Get rules for a given user or conversation + */ + + List<String> getRules(String objectType, String uuid); + + /** + * Sync groups and friends + */ + oneway void sync(SyncCallback callback); + + /** + * Pass all groups and friends to be able to add them to the database + * @param groups serialized groups + * @param friends serialized friends + */ + oneway void passGroupsAndFriends(in List<String> groups, in List<String> friends); } \ No newline at end of file diff --git a/core/src/main/aidl/me/rhunk/snapenhance/bridge/SyncCallback.aidl b/core/src/main/aidl/me/rhunk/snapenhance/bridge/SyncCallback.aidl @@ -0,0 +1,17 @@ +package me.rhunk.snapenhance.bridge; + +interface SyncCallback { + /** + * Called when the friend data has been synced + * @param uuid The uuid of the friend to sync + * @return The serialized friend data + */ + String syncFriend(String uuid); + + /** + * Called when the conversation data has been synced + * @param uuid The uuid of the conversation to sync + * @return The serialized conversation data + */ + String syncGroup(String uuid); +}+ \ No newline at end of file diff --git a/core/src/main/assets/lang/en_US.json b/core/src/main/assets/lang/en_US.json @@ -20,7 +20,7 @@ "downloads": "Downloads", "features": "Features", "home": "Home", - "friends": "Friends", + "social": "Social", "plugins": "Plugins" }, "features": { diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/EventDispatcher.kt b/core/src/main/kotlin/me/rhunk/snapenhance/EventDispatcher.kt @@ -1,12 +1,15 @@ package me.rhunk.snapenhance +import android.content.Intent import me.rhunk.snapenhance.core.eventbus.events.impl.OnSnapInteractionEvent import me.rhunk.snapenhance.core.eventbus.events.impl.SendMessageWithContentEvent +import me.rhunk.snapenhance.core.eventbus.events.impl.SnapWidgetBroadcastReceiveEvent import me.rhunk.snapenhance.data.wrapper.impl.MessageContent import me.rhunk.snapenhance.data.wrapper.impl.SnapUUID import me.rhunk.snapenhance.hook.HookStage import me.rhunk.snapenhance.hook.hook import me.rhunk.snapenhance.manager.Manager +import me.rhunk.snapenhance.util.snap.SnapWidgetBroadcastReceiverHelper class EventDispatcher( private val context: ModContext @@ -14,7 +17,7 @@ class EventDispatcher( override fun init() { context.classCache.conversationManager.hook("sendMessageWithContent", HookStage.BEFORE) { param -> val messageContent = MessageContent(param.arg(1)) - context.event.post(SendMessageWithContentEvent(messageContent).apply { adapter = param })?.let { + context.event.post(SendMessageWithContentEvent(messageContent).apply { adapter = param })?.also { if (it.canceled) { param.setResult(null) } @@ -29,7 +32,26 @@ class EventDispatcher( conversationId = conversationId, messageId = messageId ) - )?.let { + )?.also { + if (it.canceled) { + param.setResult(null) + } + } + } + + context.androidContext.classLoader.loadClass(SnapWidgetBroadcastReceiverHelper.CLASS_NAME) + .hook("onReceive", HookStage.BEFORE) { param -> + val intent = param.arg(1) as? Intent ?: return@hook + if (!SnapWidgetBroadcastReceiverHelper.isIncomingIntentValid(intent)) return@hook + val action = intent.getStringExtra("action") ?: return@hook + + context.event.post( + SnapWidgetBroadcastReceiveEvent( + androidContext = context.androidContext, + intent = intent, + action = action + ) + )?.also { if (it.canceled) { param.setResult(null) } diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/SharedContext.kt b/core/src/main/kotlin/me/rhunk/snapenhance/SharedContext.kt @@ -1,34 +0,0 @@ -package me.rhunk.snapenhance - -import android.app.Activity -import android.app.AlertDialog -import android.content.Context -import android.content.Intent -import android.os.Build -import android.os.Environment -import android.provider.Settings -import me.rhunk.snapenhance.bridge.wrapper.LocaleWrapper -import me.rhunk.snapenhance.download.DownloadTaskManager -import kotlin.system.exitProcess - -/** - * Used to store objects between activities and receivers - */ -object SharedContext { - lateinit var downloadTaskManager: DownloadTaskManager - lateinit var translation: LocaleWrapper - - fun ensureInitialized(context: Context) { - if (!this::downloadTaskManager.isInitialized) { - downloadTaskManager = DownloadTaskManager().apply { - init(context) - } - } - if (!this::translation.isInitialized) { - translation = LocaleWrapper().apply { - loadFromContext(context) - } - } - //askForPermissions(context) - } -}- \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/SnapEnhance.kt b/core/src/main/kotlin/me/rhunk/snapenhance/SnapEnhance.kt @@ -7,12 +7,16 @@ import android.content.pm.PackageManager import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withContext import me.rhunk.snapenhance.bridge.BridgeClient +import me.rhunk.snapenhance.bridge.SyncCallback import me.rhunk.snapenhance.core.BuildConfig +import me.rhunk.snapenhance.core.eventbus.events.impl.SnapWidgetBroadcastReceiveEvent +import me.rhunk.snapenhance.core.messaging.MessagingFriendInfo +import me.rhunk.snapenhance.core.messaging.MessagingGroupInfo import me.rhunk.snapenhance.data.SnapClassCache import me.rhunk.snapenhance.hook.HookStage import me.rhunk.snapenhance.hook.Hooker import me.rhunk.snapenhance.hook.hook -import me.rhunk.snapenhance.util.getApplicationInfoCompat +import me.rhunk.snapenhance.util.ktx.getApplicationInfoCompat import kotlin.time.ExperimentalTime import kotlin.time.measureTime @@ -104,6 +108,7 @@ class SnapEnhance { //if mappings aren't loaded, we can't initialize features if (!mappings.isMappingsLoaded()) return features.init() + syncRemote() } }.also { time -> Logger.debug("init took $time") @@ -121,4 +126,53 @@ class SnapEnhance { Logger.debug("onActivityCreate took $time") } } + + private fun syncRemote() { + val database = appContext.database + + appContext.bridgeClient.sync(object : SyncCallback.Stub() { + override fun syncFriend(uuid: String): String? { + return database.getFriendInfo(uuid)?.toJson() + } + + override fun syncGroup(uuid: String): String? { + return database.getFeedEntryByConversationId(uuid)?.let { + MessagingGroupInfo( + it.key!!, + it.feedDisplayName!!, + it.participantsSize + ).toJson() + } + } + }) + + appContext.event.subscribe(SnapWidgetBroadcastReceiveEvent::class) { event -> + if (event.action != BridgeClient.BRIDGE_SYNC_ACTION) return@subscribe + event.canceled = true + val feedEntries = appContext.database.getFeedEntries(Int.MAX_VALUE) + + val groups = feedEntries.filter { it.friendUserId == null }.map { + MessagingGroupInfo( + it.key!!, + it.feedDisplayName!!, + it.participantsSize + ) + } + + val friends = feedEntries.filter { it.friendUserId != null }.map { + MessagingFriendInfo( + it.friendUserId!!, + it.friendDisplayName, + it.friendDisplayUsername!!.split("|")[1], + it.bitmojiAvatarId, + it.bitmojiSelfieId + ) + } + + appContext.bridgeClient.passGroupsAndFriends( + groups.map { it.toJson() }, + friends.map { it.toJson() } + ) + } + } } \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/action/impl/ExportChatMessages.kt b/core/src/main/kotlin/me/rhunk/snapenhance/action/impl/ExportChatMessages.kt @@ -17,7 +17,7 @@ import me.rhunk.snapenhance.action.AbstractAction import me.rhunk.snapenhance.data.ContentType import me.rhunk.snapenhance.data.wrapper.impl.Message import me.rhunk.snapenhance.data.wrapper.impl.SnapUUID -import me.rhunk.snapenhance.database.objects.FriendFeedInfo +import me.rhunk.snapenhance.database.objects.FriendFeedEntry import me.rhunk.snapenhance.features.impl.Messaging import me.rhunk.snapenhance.ui.ViewAppearanceHelper import me.rhunk.snapenhance.util.CallbackBuilder @@ -108,8 +108,8 @@ class ExportChatMessages : AbstractAction("action.export_chat_messages") { exportType = askExportType() ?: return@launch mediaToDownload = if (exportType == ExportFormat.HTML) askMediaToDownload() else null - val friendFeedEntries = context.database.getFriendFeed(20) - val selectedConversations = mutableListOf<FriendFeedInfo>() + val friendFeedEntries = context.database.getFeedEntries(20) + val selectedConversations = mutableListOf<FriendFeedEntry>() ViewAppearanceHelper.newAlertDialogBuilder(context.mainActivity) .setTitle(context.translation["chat_export.select_conversation"]) @@ -182,12 +182,12 @@ class ExportChatMessages : AbstractAction("action.export_chat_messages") { ) } - private suspend fun exportFullConversation(friendFeedInfo: FriendFeedInfo) { + private suspend fun exportFullConversation(friendFeedEntry: FriendFeedEntry) { //first fetch the first message - val conversationId = friendFeedInfo.key!! - val conversationName = friendFeedInfo.feedDisplayName ?: friendFeedInfo.friendDisplayName!!.split("|").lastOrNull() ?: "unknown" + val conversationId = friendFeedEntry.key!! + val conversationName = friendFeedEntry.feedDisplayName ?: friendFeedEntry.friendDisplayName!!.split("|").lastOrNull() ?: "unknown" - conversationAction(true, conversationId, if (friendFeedInfo.feedDisplayName != null) "USERCREATEDGROUP" else "ONEONONE") + conversationAction(true, conversationId, if (friendFeedEntry.feedDisplayName != null) "USERCREATEDGROUP" else "ONEONONE") logDialog(context.translation.format("chat_export.exporting_message", "conversation" to conversationName)) @@ -215,7 +215,7 @@ class ExportChatMessages : AbstractAction("action.export_chat_messages") { logDialog(context.translation["chat_export.writing_output"]) MessageExporter( context = context, - friendFeedInfo = friendFeedInfo, + friendFeedEntry = friendFeedEntry, outputFile = outputFile, mediaToDownload = mediaToDownload, printLog = ::logDialog @@ -245,7 +245,7 @@ class ExportChatMessages : AbstractAction("action.export_chat_messages") { } } - private fun exportChatForConversations(conversations: List<FriendFeedInfo>) { + private fun exportChatForConversations(conversations: List<FriendFeedEntry>) { dialogLogs.clear() val jobs = mutableListOf<Job>() diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/bridge/BridgeClient.kt b/core/src/main/kotlin/me/rhunk/snapenhance/bridge/BridgeClient.kt @@ -15,7 +15,10 @@ import me.rhunk.snapenhance.ModContext import me.rhunk.snapenhance.bridge.types.BridgeFileType import me.rhunk.snapenhance.bridge.types.FileActionType import me.rhunk.snapenhance.core.BuildConfig +import me.rhunk.snapenhance.core.messaging.MessagingRule +import me.rhunk.snapenhance.core.messaging.RuleScope import me.rhunk.snapenhance.data.LocalePair +import me.rhunk.snapenhance.util.SerializableDataObject import java.util.concurrent.CompletableFuture import java.util.concurrent.Executors import kotlin.system.exitProcess @@ -27,6 +30,10 @@ class BridgeClient( private lateinit var future: CompletableFuture<Boolean> private lateinit var service: BridgeInterface + companion object { + const val BRIDGE_SYNC_ACTION = "me.rhunk.snapenhance.bridge.SYNC" + } + fun start(callback: (Boolean) -> Unit) { this.future = CompletableFuture() @@ -124,4 +131,14 @@ class BridgeClient( } fun enqueueDownload(intent: Intent, callback: DownloadCallback) = service.enqueueDownload(intent, callback) + + fun sync(callback: SyncCallback) = service.sync(callback) + + fun passGroupsAndFriends(groups: List<String>, friends: List<String>) = service.passGroupsAndFriends(groups, friends) + + fun getRulesFromId(type: RuleScope, targetUuid: String): List<MessagingRule> { + return service.getRules(type.name, targetUuid).map { + SerializableDataObject.fromJson(it, MessagingRule::class.java) + }.toList() + } } diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/eventbus/EventBus.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/eventbus/EventBus.kt @@ -31,7 +31,11 @@ class EventBus( val obj = object : IListener<T> { override fun handle(event: T) { if (!filter(event)) return - listener(event) + runCatching { + listener(event) + }.onFailure { + Logger.error("Error while handling event ${event::class.simpleName}", it) + } } } subscribe(event, obj) diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/eventbus/events/impl/SnapWidgetBroadcastReceiveEvent.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/eventbus/events/impl/SnapWidgetBroadcastReceiveEvent.kt @@ -0,0 +1,11 @@ +package me.rhunk.snapenhance.core.eventbus.events.impl + +import android.content.Context +import android.content.Intent +import me.rhunk.snapenhance.core.eventbus.events.AbstractHookEvent + +class SnapWidgetBroadcastReceiveEvent( + val androidContext: Context, + val intent: Intent?, + val action: String +) : AbstractHookEvent()+ \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/messaging/EnumConversationFeature.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/messaging/EnumConversationFeature.kt @@ -1,10 +0,0 @@ -package me.rhunk.snapenhance.core.messaging - -enum class EnumConversationFeature( - val value: String, - val objectType: ObjectType, -) { - DOWNLOAD("download", ObjectType.USER), - STEALTH("stealth", ObjectType.CONVERSATION), - AUTO_SAVE("auto_save", ObjectType.CONVERSATION); -}- \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/messaging/ManagementObjects.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/messaging/ManagementObjects.kt @@ -1,28 +0,0 @@ -package me.rhunk.snapenhance.core.messaging - - -enum class Mode { - BLACKLIST, - WHITELIST -} - -enum class ObjectType { - USER, - CONVERSATION -} - -data class FriendStreaks( - val userId: String, - val notify: Boolean, - val expirationTimestamp: Long, - val count: Int -) - -data class MessagingRule( - val id: Int, - val objectType: ObjectType, - val targetUuid: String, - val enabled: Boolean, - val mode: Mode?, - val subject: String -)- \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/messaging/MessagingCoreObjects.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/messaging/MessagingCoreObjects.kt @@ -0,0 +1,55 @@ +package me.rhunk.snapenhance.core.messaging + +import me.rhunk.snapenhance.util.SerializableDataObject + + +enum class Mode { + BLACKLIST, + WHITELIST +} + +enum class RuleScope { + FRIEND, + GROUP +} + +enum class ConversationFeature( + val value: String, + val ruleScope: RuleScope, +) { + DOWNLOAD("download", RuleScope.FRIEND), + STEALTH("stealth", RuleScope.GROUP), + AUTO_SAVE("auto_save", RuleScope.GROUP); +} + +data class FriendStreaks( + val userId: String, + val notify: Boolean, + val expirationTimestamp: Long, + val count: Int +) : SerializableDataObject() + + +data class MessagingGroupInfo( + val conversationId: String, + val name: String, + val participantsCount: Int +) : SerializableDataObject() + +data class MessagingFriendInfo( + val userId: String, + val displayName: String?, + val mutableUsername: String, + val bitmojiId: String?, + val selfieId: String? +) : SerializableDataObject() + + +data class MessagingRule( + val id: Int, + val ruleScope: RuleScope, + val targetUuid: String, + val enabled: Boolean, + val mode: Mode?, + val subject: String +) : SerializableDataObject()+ \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/data/wrapper/impl/Message.kt b/core/src/main/kotlin/me/rhunk/snapenhance/data/wrapper/impl/Message.kt @@ -2,7 +2,7 @@ package me.rhunk.snapenhance.data.wrapper.impl import me.rhunk.snapenhance.data.MessageState import me.rhunk.snapenhance.data.wrapper.AbstractWrapper -import me.rhunk.snapenhance.util.getObjectField +import me.rhunk.snapenhance.util.ktx.getObjectField class Message(obj: Any?) : AbstractWrapper(obj) { val orderKey get() = instanceNonNull().getObjectField("mOrderKey") as Long diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/data/wrapper/impl/MessageContent.kt b/core/src/main/kotlin/me/rhunk/snapenhance/data/wrapper/impl/MessageContent.kt @@ -2,8 +2,8 @@ package me.rhunk.snapenhance.data.wrapper.impl import me.rhunk.snapenhance.data.ContentType import me.rhunk.snapenhance.data.wrapper.AbstractWrapper -import me.rhunk.snapenhance.util.getObjectField -import me.rhunk.snapenhance.util.setObjectField +import me.rhunk.snapenhance.util.ktx.getObjectField +import me.rhunk.snapenhance.util.ktx.setObjectField class MessageContent(obj: Any?) : AbstractWrapper(obj) { var content diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/data/wrapper/impl/MessageDescriptor.kt b/core/src/main/kotlin/me/rhunk/snapenhance/data/wrapper/impl/MessageDescriptor.kt @@ -1,7 +1,7 @@ package me.rhunk.snapenhance.data.wrapper.impl import me.rhunk.snapenhance.data.wrapper.AbstractWrapper -import me.rhunk.snapenhance.util.getObjectField +import me.rhunk.snapenhance.util.ktx.getObjectField class MessageDescriptor(obj: Any?) : AbstractWrapper(obj) { val messageId: Long get() = instanceNonNull().getObjectField("mMessageId") as Long diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/data/wrapper/impl/MessageDestinations.kt b/core/src/main/kotlin/me/rhunk/snapenhance/data/wrapper/impl/MessageDestinations.kt @@ -1,8 +1,8 @@ package me.rhunk.snapenhance.data.wrapper.impl import me.rhunk.snapenhance.data.wrapper.AbstractWrapper -import me.rhunk.snapenhance.util.getObjectField -import me.rhunk.snapenhance.util.setObjectField +import me.rhunk.snapenhance.util.ktx.getObjectField +import me.rhunk.snapenhance.util.ktx.setObjectField @Suppress("UNCHECKED_CAST") class MessageDestinations(obj: Any) : AbstractWrapper(obj){ diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/data/wrapper/impl/MessageMetadata.kt b/core/src/main/kotlin/me/rhunk/snapenhance/data/wrapper/impl/MessageMetadata.kt @@ -2,7 +2,7 @@ package me.rhunk.snapenhance.data.wrapper.impl import me.rhunk.snapenhance.data.PlayableSnapState import me.rhunk.snapenhance.data.wrapper.AbstractWrapper -import me.rhunk.snapenhance.util.getObjectField +import me.rhunk.snapenhance.util.ktx.getObjectField class MessageMetadata(obj: Any?) : AbstractWrapper(obj){ val createdAt: Long get() = instanceNonNull().getObjectField("mCreatedAt") as Long diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/data/wrapper/impl/SnapUUID.kt b/core/src/main/kotlin/me/rhunk/snapenhance/data/wrapper/impl/SnapUUID.kt @@ -2,7 +2,7 @@ package me.rhunk.snapenhance.data.wrapper.impl import me.rhunk.snapenhance.SnapEnhance import me.rhunk.snapenhance.data.wrapper.AbstractWrapper -import me.rhunk.snapenhance.util.getObjectField +import me.rhunk.snapenhance.util.ktx.getObjectField import java.nio.ByteBuffer import java.util.UUID diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/data/wrapper/impl/UserIdToReaction.kt b/core/src/main/kotlin/me/rhunk/snapenhance/data/wrapper/impl/UserIdToReaction.kt @@ -1,7 +1,7 @@ package me.rhunk.snapenhance.data.wrapper.impl import me.rhunk.snapenhance.data.wrapper.AbstractWrapper -import me.rhunk.snapenhance.util.getObjectField +import me.rhunk.snapenhance.util.ktx.getObjectField class UserIdToReaction(obj: Any?) : AbstractWrapper(obj) { val userId = SnapUUID(instanceNonNull().getObjectField("mUserId")) diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/data/wrapper/impl/media/MediaInfo.kt b/core/src/main/kotlin/me/rhunk/snapenhance/data/wrapper/impl/media/MediaInfo.kt @@ -2,7 +2,7 @@ package me.rhunk.snapenhance.data.wrapper.impl.media import android.os.Parcelable import me.rhunk.snapenhance.data.wrapper.AbstractWrapper -import me.rhunk.snapenhance.util.getObjectField +import me.rhunk.snapenhance.util.ktx.getObjectField import java.lang.reflect.Field diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/data/wrapper/impl/media/opera/ParamMap.kt b/core/src/main/kotlin/me/rhunk/snapenhance/data/wrapper/impl/media/opera/ParamMap.kt @@ -2,7 +2,7 @@ package me.rhunk.snapenhance.data.wrapper.impl.media.opera import me.rhunk.snapenhance.data.wrapper.AbstractWrapper import me.rhunk.snapenhance.util.ReflectionHelper -import me.rhunk.snapenhance.util.getObjectField +import me.rhunk.snapenhance.util.ktx.getObjectField import java.lang.reflect.Field import java.util.concurrent.ConcurrentHashMap diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/database/DatabaseAccess.kt b/core/src/main/kotlin/me/rhunk/snapenhance/database/DatabaseAccess.kt @@ -74,10 +74,10 @@ class DatabaseAccess(private val context: ModContext) : Manager { return obj } - fun getFriendFeedInfoByUserId(userId: String): FriendFeedInfo? { + fun getFeedEntryByUserId(userId: String): FriendFeedEntry? { return safeDatabaseOperation(openMain()) { database -> readDatabaseObject( - FriendFeedInfo(), + FriendFeedEntry(), database, "FriendsFeedView", "friendUserId = ?", @@ -86,10 +86,10 @@ class DatabaseAccess(private val context: ModContext) : Manager { } } - fun getFriendFeedInfoByConversationId(conversationId: String): FriendFeedInfo? { + fun getFeedEntryByConversationId(conversationId: String): FriendFeedEntry? { return safeDatabaseOperation(openMain()) { readDatabaseObject( - FriendFeedInfo(), + FriendFeedEntry(), it, "FriendsFeedView", "key = ?", @@ -110,19 +110,19 @@ class DatabaseAccess(private val context: ModContext) : Manager { } } - fun getFriendFeed(limit: Int): List<FriendFeedInfo> { + fun getFeedEntries(limit: Int): List<FriendFeedEntry> { return safeDatabaseOperation(openMain()) { database -> val cursor = database.rawQuery( "SELECT * FROM FriendsFeedView ORDER BY _id LIMIT ?", arrayOf(limit.toString()) ) - val list = mutableListOf<FriendFeedInfo>() + val list = mutableListOf<FriendFeedEntry>() while (cursor.moveToNext()) { - val friendFeedInfo = FriendFeedInfo() + val friendFeedEntry = FriendFeedEntry() try { - friendFeedInfo.write(cursor) + friendFeedEntry.write(cursor) } catch (_: Throwable) {} - list.add(friendFeedInfo) + list.add(friendFeedEntry) } cursor.close() list diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/database/objects/ConversationMessage.kt b/core/src/main/kotlin/me/rhunk/snapenhance/database/objects/ConversationMessage.kt @@ -5,10 +5,10 @@ import android.database.Cursor import me.rhunk.snapenhance.Constants import me.rhunk.snapenhance.data.ContentType import me.rhunk.snapenhance.database.DatabaseObject -import me.rhunk.snapenhance.util.getBlobOrNull -import me.rhunk.snapenhance.util.getInteger -import me.rhunk.snapenhance.util.getLong -import me.rhunk.snapenhance.util.getStringOrNull +import me.rhunk.snapenhance.util.ktx.getBlobOrNull +import me.rhunk.snapenhance.util.ktx.getInteger +import me.rhunk.snapenhance.util.ktx.getLong +import me.rhunk.snapenhance.util.ktx.getStringOrNull import me.rhunk.snapenhance.util.protobuf.ProtoReader @Suppress("ArrayInDataClass") diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/database/objects/FriendFeedEntry.kt b/core/src/main/kotlin/me/rhunk/snapenhance/database/objects/FriendFeedEntry.kt @@ -0,0 +1,47 @@ +package me.rhunk.snapenhance.database.objects + +import android.annotation.SuppressLint +import android.database.Cursor +import me.rhunk.snapenhance.database.DatabaseObject +import me.rhunk.snapenhance.util.ktx.getIntOrNull +import me.rhunk.snapenhance.util.ktx.getInteger +import me.rhunk.snapenhance.util.ktx.getLong +import me.rhunk.snapenhance.util.ktx.getStringOrNull + +data class FriendFeedEntry( + var id: Int = 0, + var feedDisplayName: String? = null, + var participantsSize: Int = 0, + var lastInteractionTimestamp: Long = 0, + var displayTimestamp: Long = 0, + var displayInteractionType: String? = null, + var lastInteractionUserId: Int? = null, + var key: String? = null, + var friendUserId: String? = null, + var friendDisplayName: String? = null, + var friendDisplayUsername: String? = null, + var friendLinkType: Int? = null, + var bitmojiAvatarId: String? = null, + var bitmojiSelfieId: String? = null, +) : DatabaseObject { + + @SuppressLint("Range") + override fun write(cursor: Cursor) { + with(cursor) { + id = getInteger("_id") + feedDisplayName = getStringOrNull("feedDisplayName") + participantsSize = getInteger("participantsSize") + lastInteractionTimestamp = getLong("lastInteractionTimestamp") + displayTimestamp = getLong("displayTimestamp") + displayInteractionType = getStringOrNull("displayInteractionType") + lastInteractionUserId = getIntOrNull("lastInteractionUserId") + key = getStringOrNull("key") + friendUserId = getStringOrNull("friendUserId") + friendDisplayName = getStringOrNull("friendDisplayName") + friendDisplayUsername = getStringOrNull("friendDisplayUsername") + friendLinkType = getIntOrNull("friendLinkType") + bitmojiAvatarId = getStringOrNull("bitmojiAvatarId") + bitmojiSelfieId = getStringOrNull("bitmojiSelfieId") + } + } +} diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/database/objects/FriendFeedInfo.kt b/core/src/main/kotlin/me/rhunk/snapenhance/database/objects/FriendFeedInfo.kt @@ -1,38 +0,0 @@ -package me.rhunk.snapenhance.database.objects - -import android.annotation.SuppressLint -import android.database.Cursor -import me.rhunk.snapenhance.database.DatabaseObject -import me.rhunk.snapenhance.util.getInteger -import me.rhunk.snapenhance.util.getLong -import me.rhunk.snapenhance.util.getStringOrNull - -data class FriendFeedInfo( - var id: Int = 0, - var feedDisplayName: String? = null, - var participantsSize: Int = 0, - var lastInteractionTimestamp: Long = 0, - var displayTimestamp: Long = 0, - var displayInteractionType: String? = null, - var lastInteractionUserId: Int = 0, - var key: String? = null, - var friendUserId: String? = null, - var friendDisplayName: String? = null, -) : DatabaseObject { - - @SuppressLint("Range") - override fun write(cursor: Cursor) { - with(cursor) { - id = getInteger("_id") - feedDisplayName = getStringOrNull("feedDisplayName") - participantsSize = getInteger("participantsSize") - lastInteractionTimestamp = getLong("lastInteractionTimestamp") - displayTimestamp = getLong("displayTimestamp") - displayInteractionType = getStringOrNull("displayInteractionType") - lastInteractionUserId = getInteger("lastInteractionUserId") - key = getStringOrNull("key") - friendUserId = getStringOrNull("friendUserId") - friendDisplayName = getStringOrNull("friendDisplayUsername") - } - } -} diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/database/objects/FriendInfo.kt b/core/src/main/kotlin/me/rhunk/snapenhance/database/objects/FriendInfo.kt @@ -3,9 +3,10 @@ package me.rhunk.snapenhance.database.objects import android.annotation.SuppressLint import android.database.Cursor import me.rhunk.snapenhance.database.DatabaseObject -import me.rhunk.snapenhance.util.getInteger -import me.rhunk.snapenhance.util.getLong -import me.rhunk.snapenhance.util.getStringOrNull +import me.rhunk.snapenhance.util.SerializableDataObject +import me.rhunk.snapenhance.util.ktx.getInteger +import me.rhunk.snapenhance.util.ktx.getLong +import me.rhunk.snapenhance.util.ktx.getStringOrNull data class FriendInfo( var id: Int = 0, @@ -30,7 +31,7 @@ data class FriendInfo( var isPinnedBestFriend: Int = 0, var plusBadgeVisibility: Int = 0, var usernameForSorting: String? = null -) : DatabaseObject { +) : DatabaseObject, SerializableDataObject() { @SuppressLint("Range") override fun write(cursor: Cursor) { with(cursor) { diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/database/objects/StoryEntry.kt b/core/src/main/kotlin/me/rhunk/snapenhance/database/objects/StoryEntry.kt @@ -3,8 +3,8 @@ package me.rhunk.snapenhance.database.objects import android.annotation.SuppressLint import android.database.Cursor import me.rhunk.snapenhance.database.DatabaseObject -import me.rhunk.snapenhance.util.getInteger -import me.rhunk.snapenhance.util.getStringOrNull +import me.rhunk.snapenhance.util.ktx.getInteger +import me.rhunk.snapenhance.util.ktx.getStringOrNull data class StoryEntry( var id: Int = 0, diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/database/objects/UserConversationLink.kt b/core/src/main/kotlin/me/rhunk/snapenhance/database/objects/UserConversationLink.kt @@ -3,8 +3,8 @@ package me.rhunk.snapenhance.database.objects import android.annotation.SuppressLint import android.database.Cursor import me.rhunk.snapenhance.database.DatabaseObject -import me.rhunk.snapenhance.util.getInteger -import me.rhunk.snapenhance.util.getStringOrNull +import me.rhunk.snapenhance.util.ktx.getInteger +import me.rhunk.snapenhance.util.ktx.getStringOrNull class UserConversationLink( var userId: String? = null, diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/download/DownloadTaskManager.kt b/core/src/main/kotlin/me/rhunk/snapenhance/download/DownloadTaskManager.kt @@ -8,8 +8,8 @@ import me.rhunk.snapenhance.download.data.DownloadObject import me.rhunk.snapenhance.download.data.DownloadStage import me.rhunk.snapenhance.download.data.MediaFilter import me.rhunk.snapenhance.util.SQLiteDatabaseHelper -import me.rhunk.snapenhance.util.getIntOrNull -import me.rhunk.snapenhance.util.getStringOrNull +import me.rhunk.snapenhance.util.ktx.getIntOrNull +import me.rhunk.snapenhance.util.ktx.getStringOrNull class DownloadTaskManager { private lateinit var taskDatabase: SQLiteDatabase diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/ConfigurationOverride.kt b/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/ConfigurationOverride.kt @@ -5,7 +5,7 @@ import me.rhunk.snapenhance.features.Feature import me.rhunk.snapenhance.features.FeatureLoadParams import me.rhunk.snapenhance.hook.HookStage import me.rhunk.snapenhance.hook.hook -import me.rhunk.snapenhance.util.setObjectField +import me.rhunk.snapenhance.util.ktx.setObjectField class ConfigurationOverride : Feature("Configuration Override", loadParams = FeatureLoadParams.INIT_SYNC) { override fun init() { diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/Messaging.kt b/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/Messaging.kt @@ -8,7 +8,7 @@ import me.rhunk.snapenhance.features.impl.spying.StealthMode import me.rhunk.snapenhance.hook.HookStage import me.rhunk.snapenhance.hook.Hooker import me.rhunk.snapenhance.hook.hook -import me.rhunk.snapenhance.util.getObjectField +import me.rhunk.snapenhance.util.ktx.getObjectField class Messaging : Feature("Messaging", loadParams = FeatureLoadParams.ACTIVITY_CREATE_SYNC or FeatureLoadParams.INIT_ASYNC or FeatureLoadParams.INIT_SYNC) { lateinit var conversationManager: Any diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/downloader/MediaDownloader.kt b/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/downloader/MediaDownloader.kt @@ -6,7 +6,6 @@ import android.graphics.BitmapFactory import android.net.Uri import android.widget.ImageView import kotlinx.coroutines.runBlocking -import me.rhunk.snapenhance.Constants.ARROYO_URL_KEY_PROTO_PATH import me.rhunk.snapenhance.Logger import me.rhunk.snapenhance.Logger.xposedLog import me.rhunk.snapenhance.bridge.DownloadCallback @@ -22,6 +21,7 @@ import me.rhunk.snapenhance.download.DownloadManagerClient import me.rhunk.snapenhance.download.data.DownloadMediaType import me.rhunk.snapenhance.download.data.DownloadMetadata import me.rhunk.snapenhance.download.data.InputMedia +import me.rhunk.snapenhance.download.data.MediaFilter import me.rhunk.snapenhance.download.data.SplitMediaAssetType import me.rhunk.snapenhance.download.data.toKeyPair import me.rhunk.snapenhance.features.Feature @@ -32,9 +32,8 @@ import me.rhunk.snapenhance.hook.HookAdapter import me.rhunk.snapenhance.hook.HookStage import me.rhunk.snapenhance.hook.Hooker import me.rhunk.snapenhance.ui.ViewAppearanceHelper -import me.rhunk.snapenhance.download.data.MediaFilter import me.rhunk.snapenhance.util.download.RemoteMediaResolver -import me.rhunk.snapenhance.util.getObjectField +import me.rhunk.snapenhance.util.ktx.getObjectField import me.rhunk.snapenhance.util.protobuf.ProtoReader import me.rhunk.snapenhance.util.snap.BitmojiSelfie import me.rhunk.snapenhance.util.snap.EncryptionHelper diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/experiments/UnlimitedMultiSnap.kt b/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/experiments/UnlimitedMultiSnap.kt @@ -4,7 +4,7 @@ import me.rhunk.snapenhance.features.Feature import me.rhunk.snapenhance.features.FeatureLoadParams import me.rhunk.snapenhance.hook.HookStage import me.rhunk.snapenhance.hook.hookConstructor -import me.rhunk.snapenhance.util.setObjectField +import me.rhunk.snapenhance.util.ktx.setObjectField class UnlimitedMultiSnap : Feature("UnlimitedMultiSnap", loadParams = FeatureLoadParams.ACTIVITY_CREATE_ASYNC) { override fun asyncOnActivityCreate() { diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/spying/AnonymousStoryViewing.kt b/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/spying/AnonymousStoryViewing.kt @@ -4,7 +4,7 @@ import me.rhunk.snapenhance.features.Feature import me.rhunk.snapenhance.features.FeatureLoadParams import me.rhunk.snapenhance.hook.HookStage import me.rhunk.snapenhance.hook.Hooker -import me.rhunk.snapenhance.util.getObjectField +import me.rhunk.snapenhance.util.ktx.getObjectField class AnonymousStoryViewing : Feature("Anonymous Story Viewing", loadParams = FeatureLoadParams.ACTIVITY_CREATE_ASYNC) { override fun asyncOnActivityCreate() { diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/spying/MessageLogger.kt b/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/spying/MessageLogger.kt @@ -71,7 +71,7 @@ class MessageLogger : Feature("MessageLogger", } measureTime { - context.database.getFriendFeed(PREFETCH_FEED_COUNT).forEach { friendFeedInfo -> + context.database.getFeedEntries(PREFETCH_FEED_COUNT).forEach { friendFeedInfo -> fetchedMessages.addAll(context.bridgeClient.getLoggedMessageIds(friendFeedInfo.key!!, PREFETCH_MESSAGE_COUNT).toList()) } }.also { Logger.debug("Loaded ${fetchedMessages.size} cached messages in $it") } diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/tweaks/AutoSave.kt b/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/tweaks/AutoSave.kt @@ -12,7 +12,7 @@ import me.rhunk.snapenhance.features.impl.spying.StealthMode import me.rhunk.snapenhance.hook.HookStage import me.rhunk.snapenhance.hook.Hooker import me.rhunk.snapenhance.util.CallbackBuilder -import me.rhunk.snapenhance.util.getObjectField +import me.rhunk.snapenhance.util.ktx.getObjectField import java.util.concurrent.Executors class AutoSave : Feature("Auto Save", loadParams = FeatureLoadParams.ACTIVITY_CREATE_ASYNC) { diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/tweaks/Notifications.kt b/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/tweaks/Notifications.kt @@ -12,8 +12,8 @@ import android.os.Bundle import android.os.UserHandle import de.robv.android.xposed.XposedBridge import de.robv.android.xposed.XposedHelpers -import me.rhunk.snapenhance.Constants import me.rhunk.snapenhance.Logger +import me.rhunk.snapenhance.core.eventbus.events.impl.SnapWidgetBroadcastReceiveEvent import me.rhunk.snapenhance.data.ContentType import me.rhunk.snapenhance.data.MediaReferenceType import me.rhunk.snapenhance.data.wrapper.impl.Message @@ -31,6 +31,7 @@ import me.rhunk.snapenhance.util.protobuf.ProtoReader import me.rhunk.snapenhance.util.snap.EncryptionHelper import me.rhunk.snapenhance.util.snap.MediaDownloaderHelper import me.rhunk.snapenhance.util.snap.PreviewUtils +import me.rhunk.snapenhance.util.snap.SnapWidgetBroadcastReceiverHelper class Notifications : Feature("Notifications", loadParams = FeatureLoadParams.INIT_SYNC) { companion object{ @@ -42,10 +43,6 @@ class Notifications : Feature("Notifications", loadParams = FeatureLoadParams.IN private val cachedMessages = mutableMapOf<String, MutableList<String>>() // conversationId => cached messages private val notificationIdMap = mutableMapOf<Int, String>() // notificationId => conversationId - private val broadcastReceiverClass by lazy { - context.androidContext.classLoader.loadClass("com.snap.widgets.core.BestFriendsWidgetProvider") - } - private val notifyAsUserMethod by lazy { XposedHelpers.findMethodExact( NotificationManager::class.java, "notifyAsUser", @@ -102,16 +99,18 @@ class Notifications : Feature("Notifications", loadParams = FeatureLoadParams.IN fun newAction(title: String, remoteAction: String, filter: (() -> Boolean), builder: (Notification.Action.Builder) -> Unit) { if (!filter()) return - val intent = Intent().setClassName(Constants.SNAPCHAT_PACKAGE_NAME, broadcastReceiverClass.name) - .putExtra("conversation_id", conversationId) - .putExtra("notification_id", notificationData.id) - .putExtra("message_id", messageId) - .setAction(remoteAction) + + val intent = SnapWidgetBroadcastReceiverHelper.create(remoteAction) { + putExtra("conversation_id", conversationId) + putExtra("notification_id", notificationData.id) + putExtra("message_id", messageId) + } + val action = Notification.Action.Builder(null, title, PendingIntent.getBroadcast( context.androidContext, System.nanoTime().toInt(), intent, - PendingIntent.FLAG_CANCEL_CURRENT or PendingIntent.FLAG_MUTABLE + PendingIntent.FLAG_MUTABLE )).apply(builder).build() actions.add(action) } @@ -134,14 +133,12 @@ class Notifications : Feature("Notifications", loadParams = FeatureLoadParams.IN } private fun setupBroadcastReceiverHook() { - Hooker.hook(broadcastReceiverClass, "onReceive", HookStage.BEFORE) { param -> - val androidContext = param.arg<Context>(0) - val intent = param.arg<Intent>(1) - - val conversationId = intent.getStringExtra("conversation_id") ?: return@hook + context.event.subscribe(SnapWidgetBroadcastReceiveEvent::class) { event -> + val intent = event.intent ?: return@subscribe + val conversationId = intent.getStringExtra("conversation_id") ?: return@subscribe val messageId = intent.getLongExtra("message_id", -1) val notificationId = intent.getIntExtra("notification_id", -1) - val notificationManager = androidContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + val notificationManager = event.androidContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager val updateNotification: (Int, (Notification) -> Unit) -> Unit = { id, notificationBuilder -> notificationManager.activeNotifications.firstOrNull { it.id == id }?.let { @@ -152,7 +149,7 @@ class Notifications : Feature("Notifications", loadParams = FeatureLoadParams.IN } } - when (intent.action) { + when (event.action) { ACTION_REPLY -> { val input = RemoteInput.getResultsFromIntent(intent).getCharSequence("chat_reply_input") .toString() @@ -177,10 +174,10 @@ class Notifications : Feature("Notifications", loadParams = FeatureLoadParams.IN context.longToast(it) } } - else -> return@hook + else -> return@subscribe } - param.setResult(null) + event.canceled = true } } diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/ui/PinConversations.kt b/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/ui/PinConversations.kt @@ -7,8 +7,8 @@ import me.rhunk.snapenhance.features.FeatureLoadParams import me.rhunk.snapenhance.hook.HookStage import me.rhunk.snapenhance.hook.hook import me.rhunk.snapenhance.hook.hookConstructor -import me.rhunk.snapenhance.util.getObjectField -import me.rhunk.snapenhance.util.setObjectField +import me.rhunk.snapenhance.util.ktx.getObjectField +import me.rhunk.snapenhance.util.ktx.setObjectField class PinConversations : BridgeFileFeature("PinConversations", BridgeFileType.PINNED_CONVERSATIONS, loadParams = FeatureLoadParams.ACTIVITY_CREATE_SYNC) { override fun onActivityCreate() { diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/manager/impl/MappingManager.kt b/core/src/main/kotlin/me/rhunk/snapenhance/manager/impl/MappingManager.kt @@ -1,189 +0,0 @@ -package me.rhunk.snapenhance.manager.impl - -import com.google.gson.JsonElement -import com.google.gson.JsonParser -import me.rhunk.snapenhance.Constants -import me.rhunk.snapenhance.Logger -import me.rhunk.snapenhance.ModContext -import me.rhunk.snapenhance.bridge.types.BridgeFileType -import me.rhunk.snapenhance.manager.Manager -import me.rhunk.snapenhance.ui.ViewAppearanceHelper -import me.rhunk.snapmapper.Mapper -import me.rhunk.snapmapper.impl.BCryptClassMapper -import me.rhunk.snapmapper.impl.CallbackMapper -import me.rhunk.snapmapper.impl.CompositeConfigurationProviderMapper -import me.rhunk.snapmapper.impl.DefaultMediaItemMapper -import me.rhunk.snapmapper.impl.EnumMapper -import me.rhunk.snapmapper.impl.FriendsFeedEventDispatcherMapper -import me.rhunk.snapmapper.impl.MediaQualityLevelProviderMapper -import me.rhunk.snapmapper.impl.OperaPageViewControllerMapper -import me.rhunk.snapmapper.impl.PlatformAnalyticsCreatorMapper -import me.rhunk.snapmapper.impl.PlusSubscriptionMapper -import me.rhunk.snapmapper.impl.ScCameraSettingsMapper -import me.rhunk.snapmapper.impl.ScoreUpdateMapper -import me.rhunk.snapmapper.impl.StoryBoostStateMapper -import java.nio.charset.StandardCharsets -import java.util.concurrent.ConcurrentHashMap -import kotlin.system.measureTimeMillis - -@Suppress("UNCHECKED_CAST") -class MappingManager(private val context: ModContext) : Manager { - private val mappers = arrayOf( - BCryptClassMapper::class, - CallbackMapper::class, - DefaultMediaItemMapper::class, - MediaQualityLevelProviderMapper::class, - EnumMapper::class, - OperaPageViewControllerMapper::class, - PlatformAnalyticsCreatorMapper::class, - PlusSubscriptionMapper::class, - ScCameraSettingsMapper::class, - StoryBoostStateMapper::class, - FriendsFeedEventDispatcherMapper::class, - CompositeConfigurationProviderMapper::class, - ScoreUpdateMapper::class - ) - - private val mappings = ConcurrentHashMap<String, Any>() - val areMappingsLoaded: Boolean - get() = mappings.isNotEmpty() - private var snapBuildNumber = 0 - - @Suppress("deprecation") - override fun init() { - val currentBuildNumber = context.androidContext.packageManager.getPackageInfo( - Constants.SNAPCHAT_PACKAGE_NAME, - 0 - ).longVersionCode.toInt() - snapBuildNumber = currentBuildNumber - - if (context.bridgeClient.isFileExists(BridgeFileType.MAPPINGS)) { - runCatching { - loadCached() - }.onFailure { - context.crash("Failed to load cached mappings ${it.message}", it) - } - - if (snapBuildNumber != currentBuildNumber) { - context.bridgeClient.deleteFile(BridgeFileType.MAPPINGS) - context.softRestartApp() - } - return - } - context.runOnUiThread { - val statusDialogBuilder = ViewAppearanceHelper.newAlertDialogBuilder(context.mainActivity) - .setMessage("Generating mappings, please wait...") - .setCancelable(false) - .setView(android.widget.ProgressBar(context.mainActivity).apply { - setPadding(0, 20, 0, 20) - }) - - val loadingDialog = statusDialogBuilder.show() - - context.executeAsync { - runCatching { - refresh() - }.onSuccess { - context.shortToast("Generated mappings for build $snapBuildNumber") - context.softRestartApp() - }.onFailure { - Logger.error("Failed to generate mappings", it) - context.runOnUiThread { - loadingDialog.dismiss() - statusDialogBuilder.setView(null) - statusDialogBuilder.setMessage("Failed to generate mappings: $it") - statusDialogBuilder.setNegativeButton("Close") { _, _ -> - context.mainActivity!!.finish() - } - statusDialogBuilder.show() - } - } - } - } - } - - private fun loadCached() { - if (!context.bridgeClient.isFileExists(BridgeFileType.MAPPINGS)) { - Logger.xposedLog("Mappings file does not exist") - return - } - val mappingsObject = JsonParser.parseString( - String( - context.bridgeClient.readFile(BridgeFileType.MAPPINGS), - StandardCharsets.UTF_8 - ) - ).asJsonObject.also { - snapBuildNumber = it["snap_build_number"].asInt - } - - mappingsObject.entrySet().forEach { (key, value): Map.Entry<String, JsonElement> -> - if (value.isJsonArray) { - mappings[key] = context.gson.fromJson(value, ArrayList::class.java) - return@forEach - } - if (value.isJsonObject) { - mappings[key] = context.gson.fromJson(value, ConcurrentHashMap::class.java) - return@forEach - } - mappings[key] = value.asString - } - } - - @Suppress("DEPRECATION") - private fun refresh() { - val mapper = Mapper(*mappers) - - runCatching { - mapper.loadApk(context.androidContext.packageManager.getApplicationInfo( - Constants.SNAPCHAT_PACKAGE_NAME, - 0 - ).sourceDir) - }.onFailure { - throw Exception("Failed to load APK", it) - } - - measureTimeMillis { - val result = mapper.start().apply { - addProperty("snap_build_number", snapBuildNumber) - } - context.bridgeClient.writeFile(BridgeFileType.MAPPINGS, result.toString().toByteArray()) - }.also { - Logger.xposedLog("Generated mappings in $it ms") - } - } - - fun getMappedObject(key: String): Any { - if (mappings.containsKey(key)) { - return mappings[key]!! - } - throw Exception("No mapping found for $key") - } - - fun getMappedObjectNullable(key: String): Any? { - return mappings[key] - } - - fun getMappedClass(className: String): Class<*> { - return context.androidContext.classLoader.loadClass(getMappedObject(className) as String) - } - - fun getMappedClass(key: String, subKey: String): Class<*> { - return context.androidContext.classLoader.loadClass(getMappedValue(key, subKey)) - } - - fun getMappedValue(key: String): String { - return getMappedObject(key) as String - } - - fun <T : Any> getMappedList(key: String): List<T> { - return listOf(getMappedObject(key) as List<T>).flatten() - } - - fun getMappedValue(key: String, subKey: String): String { - return getMappedMap(key)[subKey] as String - } - - fun getMappedMap(key: String): Map<String, *> { - return getMappedObject(key) as Map<String, *> - } -}- \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/ui/menu/impl/FriendFeedInfoMenu.kt b/core/src/main/kotlin/me/rhunk/snapenhance/ui/menu/impl/FriendFeedInfoMenu.kt @@ -204,7 +204,7 @@ class FriendFeedInfoMenu : AbstractMenu() { //mapped conversation fetch (may not work with legacy sc versions) messaging.lastFetchGroupConversationUUID?.let { - context.database.getFriendFeedInfoByConversationId(it.toString())?.let { friendFeedInfo -> + context.database.getFeedEntryByConversationId(it.toString())?.let { friendFeedInfo -> val participantSize = friendFeedInfo.participantsSize return it.toString() to if (participantSize == 1) focusedConversationTargetUser else null } @@ -280,7 +280,7 @@ class FriendFeedInfoMenu : AbstractMenu() { } run { - val userId = context.database.getFriendFeedInfoByConversationId(conversationId)?.friendUserId ?: return@run + val userId = context.database.getFeedEntryByConversationId(conversationId)?.friendUserId ?: return@run if (friendFeedMenuOptions.contains("auto_download_blacklist")) { createToggleFeature(viewConsumer, "friend_menu_option.auto_download_blacklist", @@ -340,7 +340,7 @@ class FriendFeedInfoMenu : AbstractMenu() { if (friendFeedMenuOptions.contains("auto_download_blacklist")) { run { val userId = - context.database.getFriendFeedInfoByConversationId(conversationId)?.friendUserId + context.database.getFeedEntryByConversationId(conversationId)?.friendUserId ?: return@run createActionButton( "\u2B07\uFE0F", diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/util/ActivityResultCallback.kt b/core/src/main/kotlin/me/rhunk/snapenhance/util/ActivityResultCallback.kt @@ -1,5 +0,0 @@ -package me.rhunk.snapenhance.util - -import android.content.Intent - -typealias ActivityResultCallback = (requestCode: Int, resultCode: Int, data: Intent?) -> Unit- \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/util/AndroidCompatExtensions.kt b/core/src/main/kotlin/me/rhunk/snapenhance/util/AndroidCompatExtensions.kt @@ -1,13 +0,0 @@ -package me.rhunk.snapenhance.util - -import android.content.pm.PackageManager -import android.content.pm.PackageManager.ApplicationInfoFlags -import android.os.Build - -fun PackageManager.getApplicationInfoCompat(packageName: String, flags: Int) = - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - getApplicationInfo(packageName, ApplicationInfoFlags.of(flags.toLong())) - } else { - @Suppress("DEPRECATION") - getApplicationInfo(packageName, flags) - } diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/util/DbCursorExt.kt b/core/src/main/kotlin/me/rhunk/snapenhance/util/DbCursorExt.kt @@ -1,37 +0,0 @@ -package me.rhunk.snapenhance.util - -import android.database.Cursor - -fun Cursor.getStringOrNull(columnName: String): String? { - val columnIndex = getColumnIndex(columnName) - return if (columnIndex == -1) null else getString(columnIndex) -} - -fun Cursor.getIntOrNull(columnName: String): Int? { - val columnIndex = getColumnIndex(columnName) - return if (columnIndex == -1) null else getInt(columnIndex) -} - -fun Cursor.getInteger(columnName: String) = getIntOrNull(columnName) ?: throw NullPointerException("Column $columnName is null") -fun Cursor.getLong(columnName: String) = getLongOrNull(columnName) ?: throw NullPointerException("Column $columnName is null") - -fun Cursor.getBlobOrNull(columnName: String): ByteArray? { - val columnIndex = getColumnIndex(columnName) - return if (columnIndex == -1) null else getBlob(columnIndex) -} - - -fun Cursor.getLongOrNull(columnName: String): Long? { - val columnIndex = getColumnIndex(columnName) - return if (columnIndex == -1) null else getLong(columnIndex) -} - -fun Cursor.getDoubleOrNull(columnName: String): Double? { - val columnIndex = getColumnIndex(columnName) - return if (columnIndex == -1) null else getDouble(columnIndex) -} - -fun Cursor.getFloatOrNull(columnName: String): Float? { - val columnIndex = getColumnIndex(columnName) - return if (columnIndex == -1) null else getFloat(columnIndex) -}- \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/util/SerializableDataObject.kt b/core/src/main/kotlin/me/rhunk/snapenhance/util/SerializableDataObject.kt @@ -0,0 +1,22 @@ +package me.rhunk.snapenhance.util + +import com.google.gson.Gson +import com.google.gson.GsonBuilder + +open class SerializableDataObject { + companion object { + val gson: Gson = GsonBuilder().create() + + inline fun <reified T : SerializableDataObject> fromJson(json: String): T { + return gson.fromJson(json, T::class.java) + } + + inline fun <reified T : SerializableDataObject> fromJson(json: String, type: Class<T>): T { + return gson.fromJson(json, type) + } + } + + fun toJson(): String { + return gson.toJson(this) + } +}+ \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/util/XposedHelperExt.kt b/core/src/main/kotlin/me/rhunk/snapenhance/util/XposedHelperExt.kt @@ -1,20 +0,0 @@ -package me.rhunk.snapenhance.util - -import de.robv.android.xposed.XposedHelpers - -fun Any.getObjectField(fieldName: String): Any? { - return XposedHelpers.getObjectField(this, fieldName) -} - -fun Any.setObjectField(fieldName: String, value: Any?) { - XposedHelpers.setObjectField(this, fieldName, value) -} - -fun Any.getObjectFieldOrNull(fieldName: String): Any? { - return try { - getObjectField(fieldName) - } catch (e: Exception) { - null - } -} - diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/util/export/MessageExporter.kt b/core/src/main/kotlin/me/rhunk/snapenhance/util/export/MessageExporter.kt @@ -18,9 +18,9 @@ import me.rhunk.snapenhance.data.FileType import me.rhunk.snapenhance.data.MediaReferenceType import me.rhunk.snapenhance.data.wrapper.impl.Message import me.rhunk.snapenhance.data.wrapper.impl.SnapUUID -import me.rhunk.snapenhance.database.objects.FriendFeedInfo +import me.rhunk.snapenhance.database.objects.FriendFeedEntry import me.rhunk.snapenhance.database.objects.FriendInfo -import me.rhunk.snapenhance.util.getApplicationInfoCompat +import me.rhunk.snapenhance.util.ktx.getApplicationInfoCompat import me.rhunk.snapenhance.util.protobuf.ProtoReader import me.rhunk.snapenhance.util.snap.EncryptionHelper import me.rhunk.snapenhance.util.snap.MediaDownloaderHelper @@ -50,7 +50,7 @@ enum class ExportFormat( class MessageExporter( private val context: ModContext, private val outputFile: File, - private val friendFeedInfo: FriendFeedInfo, + private val friendFeedEntry: FriendFeedEntry, private val mediaToDownload: List<ContentType>? = null, private val printLog: (String) -> Unit = {}, ) { @@ -59,13 +59,13 @@ class MessageExporter( fun readMessages(messages: List<Message>) { conversationParticipants = - context.database.getConversationParticipants(friendFeedInfo.key!!) + context.database.getConversationParticipants(friendFeedEntry.key!!) ?.mapNotNull { context.database.getFriendInfo(it) }?.associateBy { it.userId!! } ?: emptyMap() if (conversationParticipants.isEmpty()) - throw Throwable("Failed to get conversation participants for ${friendFeedInfo.key}") + throw Throwable("Failed to get conversation participants for ${friendFeedEntry.key}") this.messages = messages.sortedBy { it.orderKey } } @@ -78,8 +78,8 @@ class MessageExporter( private fun exportText(output: OutputStream) { val writer = output.bufferedWriter() - writer.write("Conversation key: ${friendFeedInfo.key}\n") - writer.write("Conversation Name: ${friendFeedInfo.feedDisplayName}\n") + writer.write("Conversation key: ${friendFeedEntry.key}\n") + writer.write("Conversation Name: ${friendFeedEntry.feedDisplayName}\n") writer.write("Participants:\n") conversationParticipants.forEach { (userId, friendInfo) -> writer.write(" $userId: ${friendInfo.displayName}\n") @@ -233,8 +233,8 @@ class MessageExporter( private fun exportJson(output: OutputStream) { val rootObject = JsonObject().apply { - addProperty("conversationId", friendFeedInfo.key) - addProperty("conversationName", friendFeedInfo.feedDisplayName) + addProperty("conversationId", friendFeedEntry.key) + addProperty("conversationName", friendFeedEntry.feedDisplayName) var index = 0 val participants = mutableMapOf<String, Int>() diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/util/ktx/AndroidCompatExtensions.kt b/core/src/main/kotlin/me/rhunk/snapenhance/util/ktx/AndroidCompatExtensions.kt @@ -0,0 +1,13 @@ +package me.rhunk.snapenhance.util.ktx + +import android.content.pm.PackageManager +import android.content.pm.PackageManager.ApplicationInfoFlags +import android.os.Build + +fun PackageManager.getApplicationInfoCompat(packageName: String, flags: Int) = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + getApplicationInfo(packageName, ApplicationInfoFlags.of(flags.toLong())) + } else { + @Suppress("DEPRECATION") + getApplicationInfo(packageName, flags) + } diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/util/ktx/DbCursorExt.kt b/core/src/main/kotlin/me/rhunk/snapenhance/util/ktx/DbCursorExt.kt @@ -0,0 +1,37 @@ +package me.rhunk.snapenhance.util.ktx + +import android.database.Cursor + +fun Cursor.getStringOrNull(columnName: String): String? { + val columnIndex = getColumnIndex(columnName) + return if (columnIndex == -1) null else getString(columnIndex) +} + +fun Cursor.getIntOrNull(columnName: String): Int? { + val columnIndex = getColumnIndex(columnName) + return if (columnIndex == -1) null else getInt(columnIndex) +} + +fun Cursor.getInteger(columnName: String) = getIntOrNull(columnName) ?: throw NullPointerException("Column $columnName is null") +fun Cursor.getLong(columnName: String) = getLongOrNull(columnName) ?: throw NullPointerException("Column $columnName is null") + +fun Cursor.getBlobOrNull(columnName: String): ByteArray? { + val columnIndex = getColumnIndex(columnName) + return if (columnIndex == -1) null else getBlob(columnIndex) +} + + +fun Cursor.getLongOrNull(columnName: String): Long? { + val columnIndex = getColumnIndex(columnName) + return if (columnIndex == -1) null else getLong(columnIndex) +} + +fun Cursor.getDoubleOrNull(columnName: String): Double? { + val columnIndex = getColumnIndex(columnName) + return if (columnIndex == -1) null else getDouble(columnIndex) +} + +fun Cursor.getFloatOrNull(columnName: String): Float? { + val columnIndex = getColumnIndex(columnName) + return if (columnIndex == -1) null else getFloat(columnIndex) +}+ \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/util/ktx/XposedHelperExt.kt b/core/src/main/kotlin/me/rhunk/snapenhance/util/ktx/XposedHelperExt.kt @@ -0,0 +1,20 @@ +package me.rhunk.snapenhance.util.ktx + +import de.robv.android.xposed.XposedHelpers + +fun Any.getObjectField(fieldName: String): Any? { + return XposedHelpers.getObjectField(this, fieldName) +} + +fun Any.setObjectField(fieldName: String, value: Any?) { + XposedHelpers.setObjectField(this, fieldName, value) +} + +fun Any.getObjectFieldOrNull(fieldName: String): Any? { + return try { + getObjectField(fieldName) + } catch (e: Exception) { + null + } +} + diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/util/snap/SnapWidgetBroadcastReceiverHelper.kt b/core/src/main/kotlin/me/rhunk/snapenhance/util/snap/SnapWidgetBroadcastReceiverHelper.kt @@ -0,0 +1,24 @@ +package me.rhunk.snapenhance.util.snap + +import android.content.Intent +import me.rhunk.snapenhance.Constants + +object SnapWidgetBroadcastReceiverHelper { + private const val ACTION_WIDGET_UPDATE = "com.snap.android.WIDGET_APP_START_UPDATE_ACTION" + const val CLASS_NAME = "com.snap.widgets.core.BestFriendsWidgetProvider" + + fun create(targetAction: String, callback: Intent.() -> Unit): Intent { + with(Intent()) { + callback(this) + action = ACTION_WIDGET_UPDATE + putExtra(":)", true) + putExtra("action", targetAction) + setClassName(Constants.SNAPCHAT_PACKAGE_NAME, CLASS_NAME) + return this + } + } + + fun isIncomingIntentValid(intent: Intent): Boolean { + return intent.action == ACTION_WIDGET_UPDATE && intent.getBooleanExtra(":)", false) + } +}+ \ No newline at end of file