mirror of
https://github.com/rhunk/SnapEnhance.git
synced 2025-06-13 05:37:48 +02:00
feat: social section
- bridge: rules, sync - move extensions to a new package - snap widget broadcast receiver (SnapWidgetBroadcastReceiverHelper) - refactor compose remember delegates
This commit is contained in:
@ -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) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
friends.forEach { friend ->
|
||||
database.execSQL("INSERT OR REPLACE INTO friends VALUES (?, ?, ?, ?, ?)", arrayOf(
|
||||
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 {
|
||||
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")!!),
|
||||
|
@ -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",
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
)
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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
|
||||
)
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,4 @@
|
||||
package me.rhunk.snapenhance.ui.manager.sections.social
|
||||
|
||||
class FriendTab {
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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(
|
||||
|
@ -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,
|
||||
|
@ -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)
|
||||
|
@ -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>
|
||||
|
@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
/**
|
||||
* 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 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);
|
||||
/**
|
||||
* 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);
|
||||
/**
|
||||
* 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);
|
||||
/**
|
||||
* 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();
|
||||
/**
|
||||
* Clear the message logger database
|
||||
*/
|
||||
void clearMessageLogger();
|
||||
|
||||
/**
|
||||
* Fetch the locales
|
||||
*
|
||||
* @return the locale result
|
||||
*/
|
||||
Map<String, String> fetchLocales(String userLocale);
|
||||
/**
|
||||
* Fetch the locales
|
||||
*
|
||||
* @return the locale result
|
||||
*/
|
||||
Map<String, String> fetchLocales(String userLocale);
|
||||
|
||||
/**
|
||||
* Enqueue a download
|
||||
*/
|
||||
void enqueueDownload(in Intent intent, DownloadCallback callback);
|
||||
/**
|
||||
* 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);
|
||||
}
|
@ -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);
|
||||
}
|
@ -20,7 +20,7 @@
|
||||
"downloads": "Downloads",
|
||||
"features": "Features",
|
||||
"home": "Home",
|
||||
"friends": "Friends",
|
||||
"social": "Social",
|
||||
"plugins": "Plugins"
|
||||
},
|
||||
"features": {
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
@ -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() }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
@ -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>()
|
||||
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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()
|
@ -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);
|
||||
}
|
@ -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
|
||||
)
|
@ -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()
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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){
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
@ -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"))
|
||||
|
@ -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
|
||||
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
@ -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")
|
||||
|
@ -3,21 +3,26 @@ 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.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 FriendFeedInfo(
|
||||
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 = 0,
|
||||
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")
|
||||
@ -29,10 +34,14 @@ data class FriendFeedInfo(
|
||||
lastInteractionTimestamp = getLong("lastInteractionTimestamp")
|
||||
displayTimestamp = getLong("displayTimestamp")
|
||||
displayInteractionType = getStringOrNull("displayInteractionType")
|
||||
lastInteractionUserId = getInteger("lastInteractionUserId")
|
||||
lastInteractionUserId = getIntOrNull("lastInteractionUserId")
|
||||
key = getStringOrNull("key")
|
||||
friendUserId = getStringOrNull("friendUserId")
|
||||
friendDisplayName = getStringOrNull("friendDisplayUsername")
|
||||
friendDisplayName = getStringOrNull("friendDisplayName")
|
||||
friendDisplayUsername = getStringOrNull("friendDisplayUsername")
|
||||
friendLinkType = getIntOrNull("friendLinkType")
|
||||
bitmojiAvatarId = getStringOrNull("bitmojiAvatarId")
|
||||
bitmojiSelfieId = getStringOrNull("bitmojiSelfieId")
|
||||
}
|
||||
}
|
||||
}
|
@ -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) {
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
@ -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() {
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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() {
|
||||
|
@ -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() {
|
||||
|
@ -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") }
|
||||
|
@ -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) {
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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() {
|
||||
|
@ -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, *>
|
||||
}
|
||||
}
|
@ -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",
|
||||
|
@ -1,5 +0,0 @@
|
||||
package me.rhunk.snapenhance.util
|
||||
|
||||
import android.content.Intent
|
||||
|
||||
typealias ActivityResultCallback = (requestCode: Int, resultCode: Int, data: Intent?) -> Unit
|
@ -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)
|
||||
}
|
||||
}
|
@ -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>()
|
||||
|
@ -1,4 +1,4 @@
|
||||
package me.rhunk.snapenhance.util
|
||||
package me.rhunk.snapenhance.util.ktx
|
||||
|
||||
import android.content.pm.PackageManager
|
||||
import android.content.pm.PackageManager.ApplicationInfoFlags
|
@ -1,4 +1,4 @@
|
||||
package me.rhunk.snapenhance.util
|
||||
package me.rhunk.snapenhance.util.ktx
|
||||
|
||||
import android.database.Cursor
|
||||
|
@ -1,4 +1,4 @@
|
||||
package me.rhunk.snapenhance.util
|
||||
package me.rhunk.snapenhance.util.ktx
|
||||
|
||||
import de.robv.android.xposed.XposedHelpers
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user