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.app.Service
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
|
import me.rhunk.snapenhance.Logger
|
||||||
import me.rhunk.snapenhance.RemoteSideContext
|
import me.rhunk.snapenhance.RemoteSideContext
|
||||||
import me.rhunk.snapenhance.SharedContextHolder
|
import me.rhunk.snapenhance.SharedContextHolder
|
||||||
import me.rhunk.snapenhance.bridge.types.BridgeFileType
|
import me.rhunk.snapenhance.bridge.types.BridgeFileType
|
||||||
import me.rhunk.snapenhance.bridge.types.FileActionType
|
import me.rhunk.snapenhance.bridge.types.FileActionType
|
||||||
import me.rhunk.snapenhance.bridge.wrapper.LocaleWrapper
|
import me.rhunk.snapenhance.bridge.wrapper.LocaleWrapper
|
||||||
import me.rhunk.snapenhance.bridge.wrapper.MessageLoggerWrapper
|
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.download.DownloadProcessor
|
||||||
|
import me.rhunk.snapenhance.util.SerializableDataObject
|
||||||
|
import kotlin.system.measureTimeMillis
|
||||||
|
|
||||||
class BridgeService : Service() {
|
class BridgeService : Service() {
|
||||||
private lateinit var messageLoggerWrapper: MessageLoggerWrapper
|
private lateinit var messageLoggerWrapper: MessageLoggerWrapper
|
||||||
private lateinit var remoteSideContext: RemoteSideContext
|
private lateinit var remoteSideContext: RemoteSideContext
|
||||||
|
private lateinit var syncCallback: SyncCallback
|
||||||
|
|
||||||
override fun onBind(intent: Intent): IBinder {
|
override fun onBind(intent: Intent): IBinder {
|
||||||
remoteSideContext = SharedContextHolder.remote(this).apply {
|
remoteSideContext = SharedContextHolder.remote(this).apply {
|
||||||
@ -25,28 +33,36 @@ class BridgeService : Service() {
|
|||||||
|
|
||||||
inner class BridgeBinder : BridgeInterface.Stub() {
|
inner class BridgeBinder : BridgeInterface.Stub() {
|
||||||
override fun fileOperation(action: Int, fileType: Int, content: ByteArray?): ByteArray {
|
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]) {
|
return when (FileActionType.values()[action]) {
|
||||||
FileActionType.CREATE_AND_READ -> {
|
FileActionType.CREATE_AND_READ -> {
|
||||||
resolvedFile?.let {
|
resolvedFile?.let {
|
||||||
if (!it.exists()) {
|
if (!it.exists()) {
|
||||||
return content?.also { content -> it.writeBytes(content) } ?: ByteArray(0)
|
return content?.also { content -> it.writeBytes(content) } ?: ByteArray(
|
||||||
|
0
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
it.readBytes()
|
it.readBytes()
|
||||||
} ?: ByteArray(0)
|
} ?: ByteArray(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
FileActionType.READ -> {
|
FileActionType.READ -> {
|
||||||
resolvedFile?.takeIf { it.exists() }?.readBytes() ?: ByteArray(0)
|
resolvedFile?.takeIf { it.exists() }?.readBytes() ?: ByteArray(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
FileActionType.WRITE -> {
|
FileActionType.WRITE -> {
|
||||||
content?.also { resolvedFile?.writeBytes(content) } ?: ByteArray(0)
|
content?.also { resolvedFile?.writeBytes(content) } ?: ByteArray(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
FileActionType.DELETE -> {
|
FileActionType.DELETE -> {
|
||||||
resolvedFile?.takeIf { it.exists() }?.delete()
|
resolvedFile?.takeIf { it.exists() }?.delete()
|
||||||
ByteArray(0)
|
ByteArray(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
FileActionType.EXISTS -> {
|
FileActionType.EXISTS -> {
|
||||||
if (resolvedFile?.exists() == true)
|
if (resolvedFile?.exists() == true)
|
||||||
ByteArray(1)
|
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) {
|
override fun addMessageLoggerMessage(conversationId: String, id: Long, message: ByteArray) {
|
||||||
messageLoggerWrapper.addMessage(conversationId, id, message)
|
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 clearMessageLogger() = messageLoggerWrapper.clearMessages()
|
||||||
|
|
||||||
override fun fetchLocales(userLocale: String) = LocaleWrapper.fetchLocales(context = this@BridgeService, userLocale).associate {
|
override fun fetchLocales(userLocale: String) =
|
||||||
it.locale to it.content
|
LocaleWrapper.fetchLocales(context = this@BridgeService, userLocale).associate {
|
||||||
}
|
it.locale to it.content
|
||||||
|
}
|
||||||
|
|
||||||
override fun enqueueDownload(intent: Intent, callback: DownloadCallback) {
|
override fun enqueueDownload(intent: Intent, callback: DownloadCallback) {
|
||||||
DownloadProcessor(
|
DownloadProcessor(
|
||||||
remoteSideContext = SharedContextHolder.remote(this@BridgeService),
|
remoteSideContext = remoteSideContext,
|
||||||
callback = callback
|
callback = callback
|
||||||
).onReceive(intent)
|
).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
|
package me.rhunk.snapenhance.messaging
|
||||||
|
|
||||||
import android.database.sqlite.SQLiteDatabase
|
import android.database.sqlite.SQLiteDatabase
|
||||||
|
import me.rhunk.snapenhance.Logger
|
||||||
import me.rhunk.snapenhance.RemoteSideContext
|
import me.rhunk.snapenhance.RemoteSideContext
|
||||||
import me.rhunk.snapenhance.core.messaging.FriendStreaks
|
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.MessagingRule
|
||||||
import me.rhunk.snapenhance.core.messaging.Mode
|
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.database.objects.FriendInfo
|
||||||
import me.rhunk.snapenhance.util.SQLiteDatabaseHelper
|
import me.rhunk.snapenhance.util.SQLiteDatabaseHelper
|
||||||
import me.rhunk.snapenhance.util.getInteger
|
import me.rhunk.snapenhance.util.ktx.getInteger
|
||||||
import me.rhunk.snapenhance.util.getLongOrNull
|
import me.rhunk.snapenhance.util.ktx.getLongOrNull
|
||||||
import me.rhunk.snapenhance.util.getStringOrNull
|
import me.rhunk.snapenhance.util.ktx.getStringOrNull
|
||||||
import java.util.concurrent.Executors
|
import java.util.concurrent.Executors
|
||||||
|
|
||||||
|
|
||||||
@ -20,19 +23,27 @@ class ModDatabase(
|
|||||||
private val executor = Executors.newSingleThreadExecutor()
|
private val executor = Executors.newSingleThreadExecutor()
|
||||||
private lateinit var database: SQLiteDatabase
|
private lateinit var database: SQLiteDatabase
|
||||||
|
|
||||||
|
var receiveMessagingDataCallback: (friends: List<MessagingFriendInfo>, groups: List<MessagingGroupInfo>) -> Unit = { _, _ -> }
|
||||||
|
|
||||||
|
|
||||||
fun init() {
|
fun init() {
|
||||||
database = context.androidContext.openOrCreateDatabase("main.db", 0, null)
|
database = context.androidContext.openOrCreateDatabase("main.db", 0, null)
|
||||||
SQLiteDatabaseHelper.createTablesFromSchema(database, mapOf(
|
SQLiteDatabaseHelper.createTablesFromSchema(database, mapOf(
|
||||||
"friends" to listOf(
|
"friends" to listOf(
|
||||||
"userId VARCHAR PRIMARY KEY",
|
"userId VARCHAR PRIMARY KEY",
|
||||||
"displayName VARCHAR",
|
"displayName VARCHAR",
|
||||||
"mutable_username VARCHAR",
|
"mutableUsername VARCHAR",
|
||||||
"bitmojiId VARCHAR",
|
"bitmojiId VARCHAR",
|
||||||
"selfieId VARCHAR"
|
"selfieId VARCHAR"
|
||||||
),
|
),
|
||||||
|
"groups" to listOf(
|
||||||
|
"conversationId VARCHAR PRIMARY KEY",
|
||||||
|
"name VARCHAR",
|
||||||
|
"participantsCount INTEGER"
|
||||||
|
),
|
||||||
"rules" to listOf(
|
"rules" to listOf(
|
||||||
"id INTEGER PRIMARY KEY AUTOINCREMENT",
|
"id INTEGER PRIMARY KEY AUTOINCREMENT",
|
||||||
"objectType VARCHAR",
|
"scope VARCHAR",
|
||||||
"targetUuid VARCHAR",
|
"targetUuid VARCHAR",
|
||||||
"enabled BOOLEAN",
|
"enabled BOOLEAN",
|
||||||
"mode VARCHAR",
|
"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 {
|
executor.execute {
|
||||||
try {
|
try {
|
||||||
friends.forEach { friend ->
|
database.execSQL("INSERT OR REPLACE INTO groups VALUES (?, ?, ?)", arrayOf(
|
||||||
database.execSQL("INSERT OR REPLACE INTO friends 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.userId,
|
||||||
friend.displayName,
|
friend.streakExpirationTimestamp,
|
||||||
friend.username,
|
friend.streakLength
|
||||||
friend.bitmojiAvatarId,
|
|
||||||
friend.bitmojiSelfieId
|
|
||||||
))
|
))
|
||||||
//sync streaks
|
} else {
|
||||||
if (friend.streakLength > 0) {
|
database.execSQL("DELETE FROM streaks WHERE userId = ?", arrayOf(friend.userId))
|
||||||
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))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
throw e
|
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 ->
|
return database.rawQuery("SELECT * FROM rules WHERE objectType = ? AND targetUuid = ?", arrayOf(type.name, targetUuid)).use { cursor ->
|
||||||
val rules = mutableListOf<MessagingRule>()
|
val rules = mutableListOf<MessagingRule>()
|
||||||
while (cursor.moveToNext()) {
|
while (cursor.moveToNext()) {
|
||||||
rules.add(MessagingRule(
|
rules.add(MessagingRule(
|
||||||
id = cursor.getInteger("id"),
|
id = cursor.getInteger("id"),
|
||||||
objectType = ObjectType.valueOf(cursor.getStringOrNull("objectType")!!),
|
ruleScope = RuleScope.valueOf(cursor.getStringOrNull("scope")!!),
|
||||||
targetUuid = cursor.getStringOrNull("targetUuid")!!,
|
targetUuid = cursor.getStringOrNull("targetUuid")!!,
|
||||||
enabled = cursor.getInteger("enabled") == 1,
|
enabled = cursor.getInteger("enabled") == 1,
|
||||||
mode = Mode.valueOf(cursor.getStringOrNull("mode")!!),
|
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.NotImplemented
|
||||||
import me.rhunk.snapenhance.ui.manager.sections.downloads.DownloadsSection
|
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.features.FeaturesSection
|
||||||
|
import me.rhunk.snapenhance.ui.manager.sections.social.SocialSection
|
||||||
import kotlin.reflect.KClass
|
import kotlin.reflect.KClass
|
||||||
|
|
||||||
enum class EnumSection(
|
enum class EnumSection(
|
||||||
@ -39,9 +40,10 @@ enum class EnumSection(
|
|||||||
icon = Icons.Filled.Home,
|
icon = Icons.Filled.Home,
|
||||||
section = HomeSection::class
|
section = HomeSection::class
|
||||||
),
|
),
|
||||||
FRIENDS(
|
SOCIAL(
|
||||||
route = "friends",
|
route = "social",
|
||||||
icon = Icons.Filled.Group
|
icon = Icons.Filled.Group,
|
||||||
|
section = SocialSection::class
|
||||||
),
|
),
|
||||||
PLUGINS(
|
PLUGINS(
|
||||||
route = "plugins",
|
route = "plugins",
|
||||||
|
@ -34,9 +34,11 @@ import androidx.compose.material3.RadioButton
|
|||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.blur
|
import androidx.compose.ui.draw.blur
|
||||||
@ -102,15 +104,15 @@ class DownloadsSection : Section() {
|
|||||||
@Composable
|
@Composable
|
||||||
private fun FilterList() {
|
private fun FilterList() {
|
||||||
val coroutineScope = rememberCoroutineScope()
|
val coroutineScope = rememberCoroutineScope()
|
||||||
val showMenu = remember { mutableStateOf(false) }
|
var showMenu by remember { mutableStateOf(false) }
|
||||||
IconButton(onClick = { showMenu.value = !showMenu.value}) {
|
IconButton(onClick = { showMenu = !showMenu}) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Default.FilterList,
|
imageVector = Icons.Default.FilterList,
|
||||||
contentDescription = null
|
contentDescription = null
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
DropdownMenu(expanded = showMenu.value, onDismissRequest = { showMenu.value = false }) {
|
DropdownMenu(expanded = showMenu, onDismissRequest = { showMenu = false }) {
|
||||||
MediaFilter.values().toList().forEach { filter ->
|
MediaFilter.values().toList().forEach { filter ->
|
||||||
DropdownMenuItem(
|
DropdownMenuItem(
|
||||||
text = {
|
text = {
|
||||||
@ -130,7 +132,7 @@ class DownloadsSection : Section() {
|
|||||||
onClick = {
|
onClick = {
|
||||||
coroutineScope.launch {
|
coroutineScope.launch {
|
||||||
loadByFilter(filter)
|
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.Text
|
||||||
import androidx.compose.material3.TextField
|
import androidx.compose.material3.TextField
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.focus.FocusRequester
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
@ -72,14 +74,14 @@ class Dialogs(
|
|||||||
add(0, "null")
|
add(0, "null")
|
||||||
}
|
}
|
||||||
|
|
||||||
val selectedValue = remember {
|
var selectedValue by remember {
|
||||||
mutableStateOf(property.value.getNullable()?.toString() ?: "null")
|
mutableStateOf(property.value.getNullable()?.toString() ?: "null")
|
||||||
}
|
}
|
||||||
|
|
||||||
DefaultDialogCard {
|
DefaultDialogCard {
|
||||||
keys.forEachIndexed { index, item ->
|
keys.forEachIndexed { index, item ->
|
||||||
fun select() {
|
fun select() {
|
||||||
selectedValue.value = item
|
selectedValue = item
|
||||||
property.value.setAny(if (index == 0) {
|
property.value.setAny(if (index == 0) {
|
||||||
null
|
null
|
||||||
} else {
|
} else {
|
||||||
@ -97,7 +99,7 @@ class Dialogs(
|
|||||||
modifier = Modifier.weight(1f)
|
modifier = Modifier.weight(1f)
|
||||||
)
|
)
|
||||||
RadioButton(
|
RadioButton(
|
||||||
selected = selectedValue.value == item,
|
selected = selectedValue == item,
|
||||||
onClick = { select() }
|
onClick = { select() }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -179,13 +181,11 @@ class Dialogs(
|
|||||||
val toggledStates = property.value.get() as MutableList<String>
|
val toggledStates = property.value.get() as MutableList<String>
|
||||||
DefaultDialogCard {
|
DefaultDialogCard {
|
||||||
defaultItems.forEach { key ->
|
defaultItems.forEach { key ->
|
||||||
val state = remember {
|
var state by remember { mutableStateOf(toggledStates.contains(key)) }
|
||||||
mutableStateOf(toggledStates.contains(key))
|
|
||||||
}
|
|
||||||
|
|
||||||
fun toggle(value: Boolean? = null) {
|
fun toggle(value: Boolean? = null) {
|
||||||
state.value = value ?: !state.value
|
state = value ?: !state
|
||||||
if (state.value) {
|
if (state) {
|
||||||
toggledStates.add(key)
|
toggledStates.add(key)
|
||||||
} else {
|
} else {
|
||||||
toggledStates.remove(key)
|
toggledStates.remove(key)
|
||||||
@ -203,7 +203,7 @@ class Dialogs(
|
|||||||
.weight(1f)
|
.weight(1f)
|
||||||
)
|
)
|
||||||
Switch(
|
Switch(
|
||||||
checked = state.value,
|
checked = state,
|
||||||
onCheckedChange = {
|
onCheckedChange = {
|
||||||
toggle(it)
|
toggle(it)
|
||||||
}
|
}
|
||||||
|
@ -44,9 +44,11 @@ import androidx.compose.material3.TextFieldDefaults
|
|||||||
import androidx.compose.material3.rememberBottomSheetScaffoldState
|
import androidx.compose.material3.rememberBottomSheetScaffoldState
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.focus.FocusRequester
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
@ -172,18 +174,16 @@ class FeaturesSection : Section() {
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun PropertyAction(property: PropertyPair<*>, registerClickCallback: RegisterClickCallback) {
|
private fun PropertyAction(property: PropertyPair<*>, registerClickCallback: RegisterClickCallback) {
|
||||||
val showDialog = remember { mutableStateOf(false) }
|
var showDialog by remember { mutableStateOf(false) }
|
||||||
val dialogComposable = remember { mutableStateOf<@Composable () -> Unit>({}) }
|
var dialogComposable by remember { mutableStateOf<@Composable () -> Unit>({}) }
|
||||||
|
|
||||||
fun registerDialogOnClickCallback() = registerClickCallback {
|
fun registerDialogOnClickCallback() = registerClickCallback { showDialog = true }
|
||||||
showDialog.value = true
|
|
||||||
}
|
|
||||||
|
|
||||||
if (showDialog.value) {
|
if (showDialog) {
|
||||||
Dialog(
|
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 }) {
|
when (val dataType = remember { property.key.dataType.type }) {
|
||||||
DataProcessors.Type.BOOLEAN -> {
|
DataProcessors.Type.BOOLEAN -> {
|
||||||
val state = remember { mutableStateOf(propertyValue.get() as Boolean) }
|
var state by remember { mutableStateOf(propertyValue.get() as Boolean) }
|
||||||
Switch(
|
Switch(
|
||||||
checked = state.value,
|
checked = state,
|
||||||
onCheckedChange = registerClickCallback {
|
onCheckedChange = registerClickCallback {
|
||||||
state.value = state.value.not()
|
state = state.not()
|
||||||
propertyValue.setAny(state.value)
|
propertyValue.setAny(state)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -216,7 +216,7 @@ class FeaturesSection : Section() {
|
|||||||
DataProcessors.Type.STRING_UNIQUE_SELECTION -> {
|
DataProcessors.Type.STRING_UNIQUE_SELECTION -> {
|
||||||
registerDialogOnClickCallback()
|
registerDialogOnClickCallback()
|
||||||
|
|
||||||
dialogComposable.value = {
|
dialogComposable = {
|
||||||
dialogs.UniqueSelectionDialog(property)
|
dialogs.UniqueSelectionDialog(property)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -233,13 +233,13 @@ class FeaturesSection : Section() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
DataProcessors.Type.STRING_MULTIPLE_SELECTION, DataProcessors.Type.STRING, DataProcessors.Type.INTEGER, DataProcessors.Type.FLOAT -> {
|
DataProcessors.Type.STRING_MULTIPLE_SELECTION, DataProcessors.Type.STRING, DataProcessors.Type.INTEGER, DataProcessors.Type.FLOAT -> {
|
||||||
dialogComposable.value = {
|
dialogComposable = {
|
||||||
when (dataType) {
|
when (dataType) {
|
||||||
DataProcessors.Type.STRING_MULTIPLE_SELECTION -> {
|
DataProcessors.Type.STRING_MULTIPLE_SELECTION -> {
|
||||||
dialogs.MultipleSelectionDialog(property)
|
dialogs.MultipleSelectionDialog(property)
|
||||||
}
|
}
|
||||||
DataProcessors.Type.STRING, DataProcessors.Type.INTEGER, DataProcessors.Type.FLOAT -> {
|
DataProcessors.Type.STRING, DataProcessors.Type.INTEGER, DataProcessors.Type.FLOAT -> {
|
||||||
dialogs.KeyboardInputDialog(property) { showDialog.value = false }
|
dialogs.KeyboardInputDialog(property) { showDialog = false }
|
||||||
}
|
}
|
||||||
else -> {}
|
else -> {}
|
||||||
}
|
}
|
||||||
@ -271,7 +271,7 @@ class FeaturesSection : Section() {
|
|||||||
|
|
||||||
if (container.globalState == null) return
|
if (container.globalState == null) return
|
||||||
|
|
||||||
val state = remember { mutableStateOf(container.globalState!!) }
|
var state by remember { mutableStateOf(container.globalState!!) }
|
||||||
|
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@ -288,10 +288,10 @@ class FeaturesSection : Section() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Switch(
|
Switch(
|
||||||
checked = state.value,
|
checked = state,
|
||||||
onCheckedChange = {
|
onCheckedChange = {
|
||||||
state.value = state.value.not()
|
state = state.not()
|
||||||
container.globalState = state.value
|
container.globalState = state
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -301,7 +301,7 @@ class FeaturesSection : Section() {
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun PropertyCard(property: PropertyPair<*>) {
|
private fun PropertyCard(property: PropertyPair<*>) {
|
||||||
val clickCallback = remember { mutableStateOf<ClickCallback?>(null) }
|
var clickCallback by remember { mutableStateOf<ClickCallback?>(null) }
|
||||||
Card(
|
Card(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
@ -311,7 +311,7 @@ class FeaturesSection : Section() {
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.clickable {
|
.clickable {
|
||||||
clickCallback.value?.invoke(true)
|
clickCallback?.invoke(true)
|
||||||
}
|
}
|
||||||
.padding(all = 4.dp),
|
.padding(all = 4.dp),
|
||||||
horizontalArrangement = Arrangement.SpaceBetween
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
@ -371,7 +371,7 @@ class FeaturesSection : Section() {
|
|||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
PropertyAction(property, registerClickCallback = { callback ->
|
PropertyAction(property, registerClickCallback = { callback ->
|
||||||
clickCallback.value = callback
|
clickCallback = callback
|
||||||
callback
|
callback
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -381,20 +381,20 @@ class FeaturesSection : Section() {
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun FeatureSearchBar(rowScope: RowScope, focusRequester: FocusRequester) {
|
private fun FeatureSearchBar(rowScope: RowScope, focusRequester: FocusRequester) {
|
||||||
val searchValue = remember { mutableStateOf("") }
|
var searchValue by remember { mutableStateOf("") }
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
val currentSearchJob = remember { mutableStateOf<Job?>(null) }
|
var currentSearchJob by remember { mutableStateOf<Job?>(null) }
|
||||||
|
|
||||||
rowScope.apply {
|
rowScope.apply {
|
||||||
TextField(
|
TextField(
|
||||||
value = searchValue.value,
|
value = searchValue,
|
||||||
onValueChange = { keyword ->
|
onValueChange = { keyword ->
|
||||||
searchValue.value = keyword
|
searchValue = keyword
|
||||||
if (keyword.isEmpty()) {
|
if (keyword.isEmpty()) {
|
||||||
navController.navigate(MAIN_ROUTE)
|
navController.navigate(MAIN_ROUTE)
|
||||||
return@TextField
|
return@TextField
|
||||||
}
|
}
|
||||||
currentSearchJob.value?.cancel()
|
currentSearchJob?.cancel()
|
||||||
scope.launch {
|
scope.launch {
|
||||||
delay(300)
|
delay(300)
|
||||||
navController.navigate(SEARCH_FEATURE_ROUTE.replace("{keyword}", keyword), NavOptions.Builder()
|
navController.navigate(SEARCH_FEATURE_ROUTE.replace("{keyword}", keyword), NavOptions.Builder()
|
||||||
@ -402,7 +402,7 @@ class FeaturesSection : Section() {
|
|||||||
.setPopUpTo(MAIN_ROUTE, false)
|
.setPopUpTo(MAIN_ROUTE, false)
|
||||||
.build()
|
.build()
|
||||||
)
|
)
|
||||||
}.also { currentSearchJob.value = it }
|
}.also { currentSearchJob = it }
|
||||||
},
|
},
|
||||||
|
|
||||||
keyboardActions = KeyboardActions(onDone = {
|
keyboardActions = KeyboardActions(onDone = {
|
||||||
@ -428,10 +428,10 @@ class FeaturesSection : Section() {
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
override fun TopBarActions(rowScope: RowScope) {
|
override fun TopBarActions(rowScope: RowScope) {
|
||||||
val showSearchBar = remember { mutableStateOf(false) }
|
var showSearchBar by remember { mutableStateOf(false) }
|
||||||
val focusRequester = remember { FocusRequester() }
|
val focusRequester = remember { FocusRequester() }
|
||||||
|
|
||||||
if (showSearchBar.value) {
|
if (showSearchBar) {
|
||||||
FeatureSearchBar(rowScope, focusRequester)
|
FeatureSearchBar(rowScope, focusRequester)
|
||||||
LaunchedEffect(true) {
|
LaunchedEffect(true) {
|
||||||
focusRequester.requestFocus()
|
focusRequester.requestFocus()
|
||||||
@ -439,13 +439,13 @@ class FeaturesSection : Section() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
IconButton(onClick = {
|
IconButton(onClick = {
|
||||||
showSearchBar.value = showSearchBar.value.not()
|
showSearchBar = showSearchBar.not()
|
||||||
if (!showSearchBar.value && navController.currentBackStackEntry?.destination?.route == SEARCH_FEATURE_ROUTE) {
|
if (!showSearchBar && navController.currentBackStackEntry?.destination?.route == SEARCH_FEATURE_ROUTE) {
|
||||||
navController.navigate(MAIN_ROUTE)
|
navController.navigate(MAIN_ROUTE)
|
||||||
}
|
}
|
||||||
}) {
|
}) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = if (showSearchBar.value) Icons.Filled.Close
|
imageVector = if (showSearchBar) Icons.Filled.Close
|
||||||
else Icons.Filled.Search,
|
else Icons.Filled.Search,
|
||||||
contentDescription = null
|
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.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.alpha
|
import androidx.compose.ui.draw.alpha
|
||||||
@ -79,13 +80,13 @@ class SetupActivity : ComponentActivity() {
|
|||||||
|
|
||||||
setContent {
|
setContent {
|
||||||
val navController = rememberNavController()
|
val navController = rememberNavController()
|
||||||
val canGoNext = remember { mutableStateOf(false) }
|
var canGoNext by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
fun nextScreen() {
|
fun nextScreen() {
|
||||||
if (!canGoNext.value) return
|
if (!canGoNext) return
|
||||||
requiredScreens.firstOrNull()?.onLeave()
|
requiredScreens.firstOrNull()?.onLeave()
|
||||||
if (requiredScreens.size > 1) {
|
if (requiredScreens.size > 1) {
|
||||||
canGoNext.value = false
|
canGoNext = false
|
||||||
requiredScreens.removeFirst()
|
requiredScreens.removeFirst()
|
||||||
navController.navigate(requiredScreens.first().route)
|
navController.navigate(requiredScreens.first().route)
|
||||||
} else {
|
} else {
|
||||||
@ -102,7 +103,7 @@ class SetupActivity : ComponentActivity() {
|
|||||||
.fillMaxWidth(),
|
.fillMaxWidth(),
|
||||||
horizontalAlignment = Alignment.CenterHorizontally
|
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"
|
label = "NextButton"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -114,7 +115,7 @@ class SetupActivity : ComponentActivity() {
|
|||||||
.alpha(alpha)
|
.alpha(alpha)
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = if (requiredScreens.size <= 1 && canGoNext.value) {
|
imageVector = if (requiredScreens.size <= 1 && canGoNext) {
|
||||||
Icons.Default.Check
|
Icons.Default.Check
|
||||||
} else {
|
} else {
|
||||||
Icons.Default.ArrowForwardIos
|
Icons.Default.ArrowForwardIos
|
||||||
@ -135,7 +136,7 @@ class SetupActivity : ComponentActivity() {
|
|||||||
startDestination = requiredScreens.first().route
|
startDestination = requiredScreens.first().route
|
||||||
) {
|
) {
|
||||||
requiredScreens.forEach { screen ->
|
requiredScreens.forEach { screen ->
|
||||||
screen.allowNext = { canGoNext.value = it }
|
screen.allowNext = { canGoNext = it }
|
||||||
composable(screen.route) {
|
composable(screen.route) {
|
||||||
BackHandler(true) {}
|
BackHandler(true) {}
|
||||||
Column(
|
Column(
|
||||||
|
@ -11,9 +11,11 @@ import androidx.compose.material3.MaterialTheme
|
|||||||
import androidx.compose.material3.Surface
|
import androidx.compose.material3.Surface
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.window.Dialog
|
import androidx.compose.ui.window.Dialog
|
||||||
@ -26,12 +28,12 @@ class MappingsScreen : SetupScreen() {
|
|||||||
@Composable
|
@Composable
|
||||||
override fun Content() {
|
override fun Content() {
|
||||||
val coroutineScope = rememberCoroutineScope()
|
val coroutineScope = rememberCoroutineScope()
|
||||||
val infoText = remember { mutableStateOf(null as String?) }
|
var infoText by remember { mutableStateOf(null as String?) }
|
||||||
val isGenerating = remember { mutableStateOf(false) }
|
var isGenerating by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
if (infoText.value != null) {
|
if (infoText != null) {
|
||||||
Dialog(onDismissRequest = {
|
Dialog(onDismissRequest = {
|
||||||
infoText.value = null
|
infoText = null
|
||||||
}) {
|
}) {
|
||||||
Surface(
|
Surface(
|
||||||
modifier = Modifier.padding(16.dp).fillMaxWidth(),
|
modifier = Modifier.padding(16.dp).fillMaxWidth(),
|
||||||
@ -40,9 +42,9 @@ class MappingsScreen : SetupScreen() {
|
|||||||
Column(
|
Column(
|
||||||
modifier = Modifier.padding(16.dp)
|
modifier = Modifier.padding(16.dp)
|
||||||
) {
|
) {
|
||||||
Text(text = infoText.value!!)
|
Text(text = infoText!!)
|
||||||
Button(onClick = {
|
Button(onClick = {
|
||||||
infoText.value = null
|
infoText = null
|
||||||
},
|
},
|
||||||
modifier = Modifier.padding(top = 5.dp).align(alignment = androidx.compose.ui.Alignment.End)) {
|
modifier = Modifier.padding(top = 5.dp).align(alignment = androidx.compose.ui.Alignment.End)) {
|
||||||
Text(text = "OK")
|
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"])
|
DialogText(text = context.translation["setup.mappings.dialog"])
|
||||||
if (hasMappings.value) return
|
if (hasMappings) return
|
||||||
Button(onClick = {
|
Button(onClick = {
|
||||||
if (isGenerating.value) return@Button
|
if (isGenerating) return@Button
|
||||||
isGenerating.value = true
|
isGenerating = true
|
||||||
coroutineScope.launch(Dispatchers.IO) {
|
coroutineScope.launch(Dispatchers.IO) {
|
||||||
runCatching {
|
runCatching {
|
||||||
tryToGenerateMappings()
|
tryToGenerateMappings()
|
||||||
allowNext(true)
|
allowNext(true)
|
||||||
infoText.value = context.translation["setup.mappings.generate_success"]
|
infoText = context.translation["setup.mappings.generate_success"]
|
||||||
hasMappings.value = true
|
hasMappings = true
|
||||||
}.onFailure {
|
}.onFailure {
|
||||||
isGenerating.value = false
|
isGenerating = false
|
||||||
infoText.value = context.translation["setup.mappings.generate_failure"] + "\n\n" + it.message
|
infoText = context.translation["setup.mappings.generate_failure"] + "\n\n" + it.message
|
||||||
Logger.error("Failed to generate mappings", it)
|
Logger.error("Failed to generate mappings", it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}) {
|
}) {
|
||||||
if (isGenerating.value) {
|
if (isGenerating) {
|
||||||
CircularProgressIndicator(
|
CircularProgressIndicator(
|
||||||
modifier = Modifier.padding().size(30.dp),
|
modifier = Modifier.padding().size(30.dp),
|
||||||
strokeWidth = 3.dp,
|
strokeWidth = 3.dp,
|
||||||
|
@ -15,8 +15,10 @@ import androidx.compose.material3.MaterialTheme
|
|||||||
import androidx.compose.material3.Surface
|
import androidx.compose.material3.Surface
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
@ -78,10 +80,10 @@ class PickLanguageScreen : SetupScreen(){
|
|||||||
|
|
||||||
DialogText(text = context.translation["setup.dialogs.select_language"])
|
DialogText(text = context.translation["setup.dialogs.select_language"])
|
||||||
|
|
||||||
val isDialog = remember { mutableStateOf(false) }
|
var isDialog by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
if (isDialog.value) {
|
if (isDialog) {
|
||||||
Dialog(onDismissRequest = { isDialog.value = false }) {
|
Dialog(onDismissRequest = { isDialog = false }) {
|
||||||
Surface(
|
Surface(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(10.dp)
|
.padding(10.dp)
|
||||||
@ -98,7 +100,7 @@ class PickLanguageScreen : SetupScreen(){
|
|||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.clickable {
|
.clickable {
|
||||||
selectedLocale.value = locale
|
selectedLocale.value = locale
|
||||||
isDialog.value = false
|
isDialog = false
|
||||||
},
|
},
|
||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
) {
|
) {
|
||||||
@ -121,7 +123,7 @@ class PickLanguageScreen : SetupScreen(){
|
|||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
) {
|
) {
|
||||||
Button(onClick = {
|
Button(onClick = {
|
||||||
isDialog.value = true
|
isDialog = true
|
||||||
}) {
|
}) {
|
||||||
Text(text = getLocaleDisplayName(selectedLocale.value), fontSize = 16.sp,
|
Text(text = getLocaleDisplayName(selectedLocale.value), fontSize = 16.sp,
|
||||||
fontWeight = FontWeight.Normal)
|
fontWeight = FontWeight.Normal)
|
||||||
|
@ -9,9 +9,9 @@ import androidx.compose.runtime.Composable
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import me.rhunk.snapenhance.Logger
|
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.setup.screens.SetupScreen
|
||||||
import me.rhunk.snapenhance.ui.util.ChooseFolderHelper
|
import me.rhunk.snapenhance.ui.util.ChooseFolderHelper
|
||||||
|
import me.rhunk.snapenhance.ui.util.ObservableMutableState
|
||||||
|
|
||||||
class SaveFolderScreen : SetupScreen() {
|
class SaveFolderScreen : SetupScreen() {
|
||||||
private lateinit var saveFolder: ObservableMutableState<String>
|
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 java.util.List;
|
||||||
import me.rhunk.snapenhance.bridge.DownloadCallback;
|
import me.rhunk.snapenhance.bridge.DownloadCallback;
|
||||||
|
import me.rhunk.snapenhance.bridge.SyncCallback;
|
||||||
|
|
||||||
interface BridgeInterface {
|
interface BridgeInterface {
|
||||||
/**
|
/**
|
||||||
* Execute a file operation
|
* Execute a file operation
|
||||||
*/
|
*/
|
||||||
byte[] fileOperation(int action, int fileType, in @nullable byte[] content);
|
byte[] fileOperation(int action, int fileType, in @nullable byte[] content);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the content of a logged message from the database
|
* Get the content of a logged message from the database
|
||||||
*
|
*
|
||||||
* @param conversationId the ID of the conversation
|
* @param conversationId the ID of the conversation
|
||||||
* @return the content of the message
|
* @return the content of the message
|
||||||
*/
|
*/
|
||||||
long[] getLoggedMessageIds(String conversationId, int limit);
|
long[] getLoggedMessageIds(String conversationId, int limit);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the content of a logged message from the database
|
* Get the content of a logged message from the database
|
||||||
*
|
*
|
||||||
* @param id the ID of the message logger message
|
* @param id the ID of the message logger message
|
||||||
* @return the content of the message
|
* @return the content of the message
|
||||||
*/
|
*/
|
||||||
@nullable byte[] getMessageLoggerMessage(String conversationId, long id);
|
@nullable byte[] getMessageLoggerMessage(String conversationId, long id);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add a message to the message logger database
|
* Add a message to the message logger database
|
||||||
*
|
*
|
||||||
* @param id the ID of the message logger message
|
* @param id the ID of the message logger message
|
||||||
* @param message the content of the message
|
* @param message the content of the message
|
||||||
*/
|
*/
|
||||||
void addMessageLoggerMessage(String conversationId, long id, in byte[] message);
|
void addMessageLoggerMessage(String conversationId, long id, in byte[] message);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Delete a message from the message logger database
|
* Delete a message from the message logger database
|
||||||
*
|
*
|
||||||
* @param id the ID of the message logger message
|
* @param id the ID of the message logger message
|
||||||
*/
|
*/
|
||||||
void deleteMessageLoggerMessage(String conversationId, long id);
|
void deleteMessageLoggerMessage(String conversationId, long id);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Clear the message logger database
|
* Clear the message logger database
|
||||||
*/
|
*/
|
||||||
void clearMessageLogger();
|
void clearMessageLogger();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetch the locales
|
* Fetch the locales
|
||||||
*
|
*
|
||||||
* @return the locale result
|
* @return the locale result
|
||||||
*/
|
*/
|
||||||
Map<String, String> fetchLocales(String userLocale);
|
Map<String, String> fetchLocales(String userLocale);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Enqueue a download
|
* Enqueue a download
|
||||||
*/
|
*/
|
||||||
void enqueueDownload(in Intent intent, DownloadCallback callback);
|
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",
|
"downloads": "Downloads",
|
||||||
"features": "Features",
|
"features": "Features",
|
||||||
"home": "Home",
|
"home": "Home",
|
||||||
"friends": "Friends",
|
"social": "Social",
|
||||||
"plugins": "Plugins"
|
"plugins": "Plugins"
|
||||||
},
|
},
|
||||||
"features": {
|
"features": {
|
||||||
|
@ -1,12 +1,15 @@
|
|||||||
package me.rhunk.snapenhance
|
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.OnSnapInteractionEvent
|
||||||
import me.rhunk.snapenhance.core.eventbus.events.impl.SendMessageWithContentEvent
|
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.MessageContent
|
||||||
import me.rhunk.snapenhance.data.wrapper.impl.SnapUUID
|
import me.rhunk.snapenhance.data.wrapper.impl.SnapUUID
|
||||||
import me.rhunk.snapenhance.hook.HookStage
|
import me.rhunk.snapenhance.hook.HookStage
|
||||||
import me.rhunk.snapenhance.hook.hook
|
import me.rhunk.snapenhance.hook.hook
|
||||||
import me.rhunk.snapenhance.manager.Manager
|
import me.rhunk.snapenhance.manager.Manager
|
||||||
|
import me.rhunk.snapenhance.util.snap.SnapWidgetBroadcastReceiverHelper
|
||||||
|
|
||||||
class EventDispatcher(
|
class EventDispatcher(
|
||||||
private val context: ModContext
|
private val context: ModContext
|
||||||
@ -14,7 +17,7 @@ class EventDispatcher(
|
|||||||
override fun init() {
|
override fun init() {
|
||||||
context.classCache.conversationManager.hook("sendMessageWithContent", HookStage.BEFORE) { param ->
|
context.classCache.conversationManager.hook("sendMessageWithContent", HookStage.BEFORE) { param ->
|
||||||
val messageContent = MessageContent(param.arg(1))
|
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) {
|
if (it.canceled) {
|
||||||
param.setResult(null)
|
param.setResult(null)
|
||||||
}
|
}
|
||||||
@ -29,7 +32,26 @@ class EventDispatcher(
|
|||||||
conversationId = conversationId,
|
conversationId = conversationId,
|
||||||
messageId = messageId
|
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) {
|
if (it.canceled) {
|
||||||
param.setResult(null)
|
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.runBlocking
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import me.rhunk.snapenhance.bridge.BridgeClient
|
import me.rhunk.snapenhance.bridge.BridgeClient
|
||||||
|
import me.rhunk.snapenhance.bridge.SyncCallback
|
||||||
import me.rhunk.snapenhance.core.BuildConfig
|
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.data.SnapClassCache
|
||||||
import me.rhunk.snapenhance.hook.HookStage
|
import me.rhunk.snapenhance.hook.HookStage
|
||||||
import me.rhunk.snapenhance.hook.Hooker
|
import me.rhunk.snapenhance.hook.Hooker
|
||||||
import me.rhunk.snapenhance.hook.hook
|
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.ExperimentalTime
|
||||||
import kotlin.time.measureTime
|
import kotlin.time.measureTime
|
||||||
|
|
||||||
@ -104,6 +108,7 @@ class SnapEnhance {
|
|||||||
//if mappings aren't loaded, we can't initialize features
|
//if mappings aren't loaded, we can't initialize features
|
||||||
if (!mappings.isMappingsLoaded()) return
|
if (!mappings.isMappingsLoaded()) return
|
||||||
features.init()
|
features.init()
|
||||||
|
syncRemote()
|
||||||
}
|
}
|
||||||
}.also { time ->
|
}.also { time ->
|
||||||
Logger.debug("init took $time")
|
Logger.debug("init took $time")
|
||||||
@ -121,4 +126,53 @@ class SnapEnhance {
|
|||||||
Logger.debug("onActivityCreate took $time")
|
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.ContentType
|
||||||
import me.rhunk.snapenhance.data.wrapper.impl.Message
|
import me.rhunk.snapenhance.data.wrapper.impl.Message
|
||||||
import me.rhunk.snapenhance.data.wrapper.impl.SnapUUID
|
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.features.impl.Messaging
|
||||||
import me.rhunk.snapenhance.ui.ViewAppearanceHelper
|
import me.rhunk.snapenhance.ui.ViewAppearanceHelper
|
||||||
import me.rhunk.snapenhance.util.CallbackBuilder
|
import me.rhunk.snapenhance.util.CallbackBuilder
|
||||||
@ -108,8 +108,8 @@ class ExportChatMessages : AbstractAction("action.export_chat_messages") {
|
|||||||
exportType = askExportType() ?: return@launch
|
exportType = askExportType() ?: return@launch
|
||||||
mediaToDownload = if (exportType == ExportFormat.HTML) askMediaToDownload() else null
|
mediaToDownload = if (exportType == ExportFormat.HTML) askMediaToDownload() else null
|
||||||
|
|
||||||
val friendFeedEntries = context.database.getFriendFeed(20)
|
val friendFeedEntries = context.database.getFeedEntries(20)
|
||||||
val selectedConversations = mutableListOf<FriendFeedInfo>()
|
val selectedConversations = mutableListOf<FriendFeedEntry>()
|
||||||
|
|
||||||
ViewAppearanceHelper.newAlertDialogBuilder(context.mainActivity)
|
ViewAppearanceHelper.newAlertDialogBuilder(context.mainActivity)
|
||||||
.setTitle(context.translation["chat_export.select_conversation"])
|
.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
|
//first fetch the first message
|
||||||
val conversationId = friendFeedInfo.key!!
|
val conversationId = friendFeedEntry.key!!
|
||||||
val conversationName = friendFeedInfo.feedDisplayName ?: friendFeedInfo.friendDisplayName!!.split("|").lastOrNull() ?: "unknown"
|
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))
|
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"])
|
logDialog(context.translation["chat_export.writing_output"])
|
||||||
MessageExporter(
|
MessageExporter(
|
||||||
context = context,
|
context = context,
|
||||||
friendFeedInfo = friendFeedInfo,
|
friendFeedEntry = friendFeedEntry,
|
||||||
outputFile = outputFile,
|
outputFile = outputFile,
|
||||||
mediaToDownload = mediaToDownload,
|
mediaToDownload = mediaToDownload,
|
||||||
printLog = ::logDialog
|
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()
|
dialogLogs.clear()
|
||||||
val jobs = mutableListOf<Job>()
|
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.BridgeFileType
|
||||||
import me.rhunk.snapenhance.bridge.types.FileActionType
|
import me.rhunk.snapenhance.bridge.types.FileActionType
|
||||||
import me.rhunk.snapenhance.core.BuildConfig
|
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.data.LocalePair
|
||||||
|
import me.rhunk.snapenhance.util.SerializableDataObject
|
||||||
import java.util.concurrent.CompletableFuture
|
import java.util.concurrent.CompletableFuture
|
||||||
import java.util.concurrent.Executors
|
import java.util.concurrent.Executors
|
||||||
import kotlin.system.exitProcess
|
import kotlin.system.exitProcess
|
||||||
@ -27,6 +30,10 @@ class BridgeClient(
|
|||||||
private lateinit var future: CompletableFuture<Boolean>
|
private lateinit var future: CompletableFuture<Boolean>
|
||||||
private lateinit var service: BridgeInterface
|
private lateinit var service: BridgeInterface
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val BRIDGE_SYNC_ACTION = "me.rhunk.snapenhance.bridge.SYNC"
|
||||||
|
}
|
||||||
|
|
||||||
fun start(callback: (Boolean) -> Unit) {
|
fun start(callback: (Boolean) -> Unit) {
|
||||||
this.future = CompletableFuture()
|
this.future = CompletableFuture()
|
||||||
|
|
||||||
@ -124,4 +131,14 @@ class BridgeClient(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun enqueueDownload(intent: Intent, callback: DownloadCallback) = service.enqueueDownload(intent, callback)
|
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> {
|
val obj = object : IListener<T> {
|
||||||
override fun handle(event: T) {
|
override fun handle(event: T) {
|
||||||
if (!filter(event)) return
|
if (!filter(event)) return
|
||||||
listener(event)
|
runCatching {
|
||||||
|
listener(event)
|
||||||
|
}.onFailure {
|
||||||
|
Logger.error("Error while handling event ${event::class.simpleName}", it)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
subscribe(event, obj)
|
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.MessageState
|
||||||
import me.rhunk.snapenhance.data.wrapper.AbstractWrapper
|
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) {
|
class Message(obj: Any?) : AbstractWrapper(obj) {
|
||||||
val orderKey get() = instanceNonNull().getObjectField("mOrderKey") as Long
|
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.ContentType
|
||||||
import me.rhunk.snapenhance.data.wrapper.AbstractWrapper
|
import me.rhunk.snapenhance.data.wrapper.AbstractWrapper
|
||||||
import me.rhunk.snapenhance.util.getObjectField
|
import me.rhunk.snapenhance.util.ktx.getObjectField
|
||||||
import me.rhunk.snapenhance.util.setObjectField
|
import me.rhunk.snapenhance.util.ktx.setObjectField
|
||||||
|
|
||||||
class MessageContent(obj: Any?) : AbstractWrapper(obj) {
|
class MessageContent(obj: Any?) : AbstractWrapper(obj) {
|
||||||
var content
|
var content
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
package me.rhunk.snapenhance.data.wrapper.impl
|
package me.rhunk.snapenhance.data.wrapper.impl
|
||||||
|
|
||||||
import me.rhunk.snapenhance.data.wrapper.AbstractWrapper
|
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) {
|
class MessageDescriptor(obj: Any?) : AbstractWrapper(obj) {
|
||||||
val messageId: Long get() = instanceNonNull().getObjectField("mMessageId") as Long
|
val messageId: Long get() = instanceNonNull().getObjectField("mMessageId") as Long
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
package me.rhunk.snapenhance.data.wrapper.impl
|
package me.rhunk.snapenhance.data.wrapper.impl
|
||||||
|
|
||||||
import me.rhunk.snapenhance.data.wrapper.AbstractWrapper
|
import me.rhunk.snapenhance.data.wrapper.AbstractWrapper
|
||||||
import me.rhunk.snapenhance.util.getObjectField
|
import me.rhunk.snapenhance.util.ktx.getObjectField
|
||||||
import me.rhunk.snapenhance.util.setObjectField
|
import me.rhunk.snapenhance.util.ktx.setObjectField
|
||||||
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
@Suppress("UNCHECKED_CAST")
|
||||||
class MessageDestinations(obj: Any) : AbstractWrapper(obj){
|
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.PlayableSnapState
|
||||||
import me.rhunk.snapenhance.data.wrapper.AbstractWrapper
|
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){
|
class MessageMetadata(obj: Any?) : AbstractWrapper(obj){
|
||||||
val createdAt: Long get() = instanceNonNull().getObjectField("mCreatedAt") as Long
|
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.SnapEnhance
|
||||||
import me.rhunk.snapenhance.data.wrapper.AbstractWrapper
|
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.nio.ByteBuffer
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
package me.rhunk.snapenhance.data.wrapper.impl
|
package me.rhunk.snapenhance.data.wrapper.impl
|
||||||
|
|
||||||
import me.rhunk.snapenhance.data.wrapper.AbstractWrapper
|
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) {
|
class UserIdToReaction(obj: Any?) : AbstractWrapper(obj) {
|
||||||
val userId = SnapUUID(instanceNonNull().getObjectField("mUserId"))
|
val userId = SnapUUID(instanceNonNull().getObjectField("mUserId"))
|
||||||
|
@ -2,7 +2,7 @@ package me.rhunk.snapenhance.data.wrapper.impl.media
|
|||||||
|
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import me.rhunk.snapenhance.data.wrapper.AbstractWrapper
|
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
|
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.data.wrapper.AbstractWrapper
|
||||||
import me.rhunk.snapenhance.util.ReflectionHelper
|
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.lang.reflect.Field
|
||||||
import java.util.concurrent.ConcurrentHashMap
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
|
|
||||||
|
@ -74,10 +74,10 @@ class DatabaseAccess(private val context: ModContext) : Manager {
|
|||||||
return obj
|
return obj
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getFriendFeedInfoByUserId(userId: String): FriendFeedInfo? {
|
fun getFeedEntryByUserId(userId: String): FriendFeedEntry? {
|
||||||
return safeDatabaseOperation(openMain()) { database ->
|
return safeDatabaseOperation(openMain()) { database ->
|
||||||
readDatabaseObject(
|
readDatabaseObject(
|
||||||
FriendFeedInfo(),
|
FriendFeedEntry(),
|
||||||
database,
|
database,
|
||||||
"FriendsFeedView",
|
"FriendsFeedView",
|
||||||
"friendUserId = ?",
|
"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()) {
|
return safeDatabaseOperation(openMain()) {
|
||||||
readDatabaseObject(
|
readDatabaseObject(
|
||||||
FriendFeedInfo(),
|
FriendFeedEntry(),
|
||||||
it,
|
it,
|
||||||
"FriendsFeedView",
|
"FriendsFeedView",
|
||||||
"key = ?",
|
"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 ->
|
return safeDatabaseOperation(openMain()) { database ->
|
||||||
val cursor = database.rawQuery(
|
val cursor = database.rawQuery(
|
||||||
"SELECT * FROM FriendsFeedView ORDER BY _id LIMIT ?",
|
"SELECT * FROM FriendsFeedView ORDER BY _id LIMIT ?",
|
||||||
arrayOf(limit.toString())
|
arrayOf(limit.toString())
|
||||||
)
|
)
|
||||||
val list = mutableListOf<FriendFeedInfo>()
|
val list = mutableListOf<FriendFeedEntry>()
|
||||||
while (cursor.moveToNext()) {
|
while (cursor.moveToNext()) {
|
||||||
val friendFeedInfo = FriendFeedInfo()
|
val friendFeedEntry = FriendFeedEntry()
|
||||||
try {
|
try {
|
||||||
friendFeedInfo.write(cursor)
|
friendFeedEntry.write(cursor)
|
||||||
} catch (_: Throwable) {}
|
} catch (_: Throwable) {}
|
||||||
list.add(friendFeedInfo)
|
list.add(friendFeedEntry)
|
||||||
}
|
}
|
||||||
cursor.close()
|
cursor.close()
|
||||||
list
|
list
|
||||||
|
@ -5,10 +5,10 @@ import android.database.Cursor
|
|||||||
import me.rhunk.snapenhance.Constants
|
import me.rhunk.snapenhance.Constants
|
||||||
import me.rhunk.snapenhance.data.ContentType
|
import me.rhunk.snapenhance.data.ContentType
|
||||||
import me.rhunk.snapenhance.database.DatabaseObject
|
import me.rhunk.snapenhance.database.DatabaseObject
|
||||||
import me.rhunk.snapenhance.util.getBlobOrNull
|
import me.rhunk.snapenhance.util.ktx.getBlobOrNull
|
||||||
import me.rhunk.snapenhance.util.getInteger
|
import me.rhunk.snapenhance.util.ktx.getInteger
|
||||||
import me.rhunk.snapenhance.util.getLong
|
import me.rhunk.snapenhance.util.ktx.getLong
|
||||||
import me.rhunk.snapenhance.util.getStringOrNull
|
import me.rhunk.snapenhance.util.ktx.getStringOrNull
|
||||||
import me.rhunk.snapenhance.util.protobuf.ProtoReader
|
import me.rhunk.snapenhance.util.protobuf.ProtoReader
|
||||||
|
|
||||||
@Suppress("ArrayInDataClass")
|
@Suppress("ArrayInDataClass")
|
||||||
|
@ -3,21 +3,26 @@ package me.rhunk.snapenhance.database.objects
|
|||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.database.Cursor
|
import android.database.Cursor
|
||||||
import me.rhunk.snapenhance.database.DatabaseObject
|
import me.rhunk.snapenhance.database.DatabaseObject
|
||||||
import me.rhunk.snapenhance.util.getInteger
|
import me.rhunk.snapenhance.util.ktx.getIntOrNull
|
||||||
import me.rhunk.snapenhance.util.getLong
|
import me.rhunk.snapenhance.util.ktx.getInteger
|
||||||
import me.rhunk.snapenhance.util.getStringOrNull
|
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 id: Int = 0,
|
||||||
var feedDisplayName: String? = null,
|
var feedDisplayName: String? = null,
|
||||||
var participantsSize: Int = 0,
|
var participantsSize: Int = 0,
|
||||||
var lastInteractionTimestamp: Long = 0,
|
var lastInteractionTimestamp: Long = 0,
|
||||||
var displayTimestamp: Long = 0,
|
var displayTimestamp: Long = 0,
|
||||||
var displayInteractionType: String? = null,
|
var displayInteractionType: String? = null,
|
||||||
var lastInteractionUserId: Int = 0,
|
var lastInteractionUserId: Int? = null,
|
||||||
var key: String? = null,
|
var key: String? = null,
|
||||||
var friendUserId: String? = null,
|
var friendUserId: String? = null,
|
||||||
var friendDisplayName: String? = null,
|
var friendDisplayName: String? = null,
|
||||||
|
var friendDisplayUsername: String? = null,
|
||||||
|
var friendLinkType: Int? = null,
|
||||||
|
var bitmojiAvatarId: String? = null,
|
||||||
|
var bitmojiSelfieId: String? = null,
|
||||||
) : DatabaseObject {
|
) : DatabaseObject {
|
||||||
|
|
||||||
@SuppressLint("Range")
|
@SuppressLint("Range")
|
||||||
@ -29,10 +34,14 @@ data class FriendFeedInfo(
|
|||||||
lastInteractionTimestamp = getLong("lastInteractionTimestamp")
|
lastInteractionTimestamp = getLong("lastInteractionTimestamp")
|
||||||
displayTimestamp = getLong("displayTimestamp")
|
displayTimestamp = getLong("displayTimestamp")
|
||||||
displayInteractionType = getStringOrNull("displayInteractionType")
|
displayInteractionType = getStringOrNull("displayInteractionType")
|
||||||
lastInteractionUserId = getInteger("lastInteractionUserId")
|
lastInteractionUserId = getIntOrNull("lastInteractionUserId")
|
||||||
key = getStringOrNull("key")
|
key = getStringOrNull("key")
|
||||||
friendUserId = getStringOrNull("friendUserId")
|
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.annotation.SuppressLint
|
||||||
import android.database.Cursor
|
import android.database.Cursor
|
||||||
import me.rhunk.snapenhance.database.DatabaseObject
|
import me.rhunk.snapenhance.database.DatabaseObject
|
||||||
import me.rhunk.snapenhance.util.getInteger
|
import me.rhunk.snapenhance.util.SerializableDataObject
|
||||||
import me.rhunk.snapenhance.util.getLong
|
import me.rhunk.snapenhance.util.ktx.getInteger
|
||||||
import me.rhunk.snapenhance.util.getStringOrNull
|
import me.rhunk.snapenhance.util.ktx.getLong
|
||||||
|
import me.rhunk.snapenhance.util.ktx.getStringOrNull
|
||||||
|
|
||||||
data class FriendInfo(
|
data class FriendInfo(
|
||||||
var id: Int = 0,
|
var id: Int = 0,
|
||||||
@ -30,7 +31,7 @@ data class FriendInfo(
|
|||||||
var isPinnedBestFriend: Int = 0,
|
var isPinnedBestFriend: Int = 0,
|
||||||
var plusBadgeVisibility: Int = 0,
|
var plusBadgeVisibility: Int = 0,
|
||||||
var usernameForSorting: String? = null
|
var usernameForSorting: String? = null
|
||||||
) : DatabaseObject {
|
) : DatabaseObject, SerializableDataObject() {
|
||||||
@SuppressLint("Range")
|
@SuppressLint("Range")
|
||||||
override fun write(cursor: Cursor) {
|
override fun write(cursor: Cursor) {
|
||||||
with(cursor) {
|
with(cursor) {
|
||||||
|
@ -3,8 +3,8 @@ package me.rhunk.snapenhance.database.objects
|
|||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.database.Cursor
|
import android.database.Cursor
|
||||||
import me.rhunk.snapenhance.database.DatabaseObject
|
import me.rhunk.snapenhance.database.DatabaseObject
|
||||||
import me.rhunk.snapenhance.util.getInteger
|
import me.rhunk.snapenhance.util.ktx.getInteger
|
||||||
import me.rhunk.snapenhance.util.getStringOrNull
|
import me.rhunk.snapenhance.util.ktx.getStringOrNull
|
||||||
|
|
||||||
data class StoryEntry(
|
data class StoryEntry(
|
||||||
var id: Int = 0,
|
var id: Int = 0,
|
||||||
|
@ -3,8 +3,8 @@ package me.rhunk.snapenhance.database.objects
|
|||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.database.Cursor
|
import android.database.Cursor
|
||||||
import me.rhunk.snapenhance.database.DatabaseObject
|
import me.rhunk.snapenhance.database.DatabaseObject
|
||||||
import me.rhunk.snapenhance.util.getInteger
|
import me.rhunk.snapenhance.util.ktx.getInteger
|
||||||
import me.rhunk.snapenhance.util.getStringOrNull
|
import me.rhunk.snapenhance.util.ktx.getStringOrNull
|
||||||
|
|
||||||
class UserConversationLink(
|
class UserConversationLink(
|
||||||
var userId: String? = null,
|
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.DownloadStage
|
||||||
import me.rhunk.snapenhance.download.data.MediaFilter
|
import me.rhunk.snapenhance.download.data.MediaFilter
|
||||||
import me.rhunk.snapenhance.util.SQLiteDatabaseHelper
|
import me.rhunk.snapenhance.util.SQLiteDatabaseHelper
|
||||||
import me.rhunk.snapenhance.util.getIntOrNull
|
import me.rhunk.snapenhance.util.ktx.getIntOrNull
|
||||||
import me.rhunk.snapenhance.util.getStringOrNull
|
import me.rhunk.snapenhance.util.ktx.getStringOrNull
|
||||||
|
|
||||||
class DownloadTaskManager {
|
class DownloadTaskManager {
|
||||||
private lateinit var taskDatabase: SQLiteDatabase
|
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.features.FeatureLoadParams
|
||||||
import me.rhunk.snapenhance.hook.HookStage
|
import me.rhunk.snapenhance.hook.HookStage
|
||||||
import me.rhunk.snapenhance.hook.hook
|
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) {
|
class ConfigurationOverride : Feature("Configuration Override", loadParams = FeatureLoadParams.INIT_SYNC) {
|
||||||
override fun init() {
|
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.HookStage
|
||||||
import me.rhunk.snapenhance.hook.Hooker
|
import me.rhunk.snapenhance.hook.Hooker
|
||||||
import me.rhunk.snapenhance.hook.hook
|
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) {
|
class Messaging : Feature("Messaging", loadParams = FeatureLoadParams.ACTIVITY_CREATE_SYNC or FeatureLoadParams.INIT_ASYNC or FeatureLoadParams.INIT_SYNC) {
|
||||||
lateinit var conversationManager: Any
|
lateinit var conversationManager: Any
|
||||||
|
@ -6,7 +6,6 @@ import android.graphics.BitmapFactory
|
|||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import me.rhunk.snapenhance.Constants.ARROYO_URL_KEY_PROTO_PATH
|
|
||||||
import me.rhunk.snapenhance.Logger
|
import me.rhunk.snapenhance.Logger
|
||||||
import me.rhunk.snapenhance.Logger.xposedLog
|
import me.rhunk.snapenhance.Logger.xposedLog
|
||||||
import me.rhunk.snapenhance.bridge.DownloadCallback
|
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.DownloadMediaType
|
||||||
import me.rhunk.snapenhance.download.data.DownloadMetadata
|
import me.rhunk.snapenhance.download.data.DownloadMetadata
|
||||||
import me.rhunk.snapenhance.download.data.InputMedia
|
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.SplitMediaAssetType
|
||||||
import me.rhunk.snapenhance.download.data.toKeyPair
|
import me.rhunk.snapenhance.download.data.toKeyPair
|
||||||
import me.rhunk.snapenhance.features.Feature
|
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.HookStage
|
||||||
import me.rhunk.snapenhance.hook.Hooker
|
import me.rhunk.snapenhance.hook.Hooker
|
||||||
import me.rhunk.snapenhance.ui.ViewAppearanceHelper
|
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.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.protobuf.ProtoReader
|
||||||
import me.rhunk.snapenhance.util.snap.BitmojiSelfie
|
import me.rhunk.snapenhance.util.snap.BitmojiSelfie
|
||||||
import me.rhunk.snapenhance.util.snap.EncryptionHelper
|
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.features.FeatureLoadParams
|
||||||
import me.rhunk.snapenhance.hook.HookStage
|
import me.rhunk.snapenhance.hook.HookStage
|
||||||
import me.rhunk.snapenhance.hook.hookConstructor
|
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) {
|
class UnlimitedMultiSnap : Feature("UnlimitedMultiSnap", loadParams = FeatureLoadParams.ACTIVITY_CREATE_ASYNC) {
|
||||||
override fun asyncOnActivityCreate() {
|
override fun asyncOnActivityCreate() {
|
||||||
|
@ -4,7 +4,7 @@ import me.rhunk.snapenhance.features.Feature
|
|||||||
import me.rhunk.snapenhance.features.FeatureLoadParams
|
import me.rhunk.snapenhance.features.FeatureLoadParams
|
||||||
import me.rhunk.snapenhance.hook.HookStage
|
import me.rhunk.snapenhance.hook.HookStage
|
||||||
import me.rhunk.snapenhance.hook.Hooker
|
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) {
|
class AnonymousStoryViewing : Feature("Anonymous Story Viewing", loadParams = FeatureLoadParams.ACTIVITY_CREATE_ASYNC) {
|
||||||
override fun asyncOnActivityCreate() {
|
override fun asyncOnActivityCreate() {
|
||||||
|
@ -71,7 +71,7 @@ class MessageLogger : Feature("MessageLogger",
|
|||||||
}
|
}
|
||||||
|
|
||||||
measureTime {
|
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())
|
fetchedMessages.addAll(context.bridgeClient.getLoggedMessageIds(friendFeedInfo.key!!, PREFETCH_MESSAGE_COUNT).toList())
|
||||||
}
|
}
|
||||||
}.also { Logger.debug("Loaded ${fetchedMessages.size} cached messages in $it") }
|
}.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.HookStage
|
||||||
import me.rhunk.snapenhance.hook.Hooker
|
import me.rhunk.snapenhance.hook.Hooker
|
||||||
import me.rhunk.snapenhance.util.CallbackBuilder
|
import me.rhunk.snapenhance.util.CallbackBuilder
|
||||||
import me.rhunk.snapenhance.util.getObjectField
|
import me.rhunk.snapenhance.util.ktx.getObjectField
|
||||||
import java.util.concurrent.Executors
|
import java.util.concurrent.Executors
|
||||||
|
|
||||||
class AutoSave : Feature("Auto Save", loadParams = FeatureLoadParams.ACTIVITY_CREATE_ASYNC) {
|
class AutoSave : Feature("Auto Save", loadParams = FeatureLoadParams.ACTIVITY_CREATE_ASYNC) {
|
||||||
|
@ -12,8 +12,8 @@ import android.os.Bundle
|
|||||||
import android.os.UserHandle
|
import android.os.UserHandle
|
||||||
import de.robv.android.xposed.XposedBridge
|
import de.robv.android.xposed.XposedBridge
|
||||||
import de.robv.android.xposed.XposedHelpers
|
import de.robv.android.xposed.XposedHelpers
|
||||||
import me.rhunk.snapenhance.Constants
|
|
||||||
import me.rhunk.snapenhance.Logger
|
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.ContentType
|
||||||
import me.rhunk.snapenhance.data.MediaReferenceType
|
import me.rhunk.snapenhance.data.MediaReferenceType
|
||||||
import me.rhunk.snapenhance.data.wrapper.impl.Message
|
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.EncryptionHelper
|
||||||
import me.rhunk.snapenhance.util.snap.MediaDownloaderHelper
|
import me.rhunk.snapenhance.util.snap.MediaDownloaderHelper
|
||||||
import me.rhunk.snapenhance.util.snap.PreviewUtils
|
import me.rhunk.snapenhance.util.snap.PreviewUtils
|
||||||
|
import me.rhunk.snapenhance.util.snap.SnapWidgetBroadcastReceiverHelper
|
||||||
|
|
||||||
class Notifications : Feature("Notifications", loadParams = FeatureLoadParams.INIT_SYNC) {
|
class Notifications : Feature("Notifications", loadParams = FeatureLoadParams.INIT_SYNC) {
|
||||||
companion object{
|
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 cachedMessages = mutableMapOf<String, MutableList<String>>() // conversationId => cached messages
|
||||||
private val notificationIdMap = mutableMapOf<Int, String>() // notificationId => conversationId
|
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 {
|
private val notifyAsUserMethod by lazy {
|
||||||
XposedHelpers.findMethodExact(
|
XposedHelpers.findMethodExact(
|
||||||
NotificationManager::class.java, "notifyAsUser",
|
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) {
|
fun newAction(title: String, remoteAction: String, filter: (() -> Boolean), builder: (Notification.Action.Builder) -> Unit) {
|
||||||
if (!filter()) return
|
if (!filter()) return
|
||||||
val intent = Intent().setClassName(Constants.SNAPCHAT_PACKAGE_NAME, broadcastReceiverClass.name)
|
|
||||||
.putExtra("conversation_id", conversationId)
|
val intent = SnapWidgetBroadcastReceiverHelper.create(remoteAction) {
|
||||||
.putExtra("notification_id", notificationData.id)
|
putExtra("conversation_id", conversationId)
|
||||||
.putExtra("message_id", messageId)
|
putExtra("notification_id", notificationData.id)
|
||||||
.setAction(remoteAction)
|
putExtra("message_id", messageId)
|
||||||
|
}
|
||||||
|
|
||||||
val action = Notification.Action.Builder(null, title, PendingIntent.getBroadcast(
|
val action = Notification.Action.Builder(null, title, PendingIntent.getBroadcast(
|
||||||
context.androidContext,
|
context.androidContext,
|
||||||
System.nanoTime().toInt(),
|
System.nanoTime().toInt(),
|
||||||
intent,
|
intent,
|
||||||
PendingIntent.FLAG_CANCEL_CURRENT or PendingIntent.FLAG_MUTABLE
|
PendingIntent.FLAG_MUTABLE
|
||||||
)).apply(builder).build()
|
)).apply(builder).build()
|
||||||
actions.add(action)
|
actions.add(action)
|
||||||
}
|
}
|
||||||
@ -134,14 +133,12 @@ class Notifications : Feature("Notifications", loadParams = FeatureLoadParams.IN
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun setupBroadcastReceiverHook() {
|
private fun setupBroadcastReceiverHook() {
|
||||||
Hooker.hook(broadcastReceiverClass, "onReceive", HookStage.BEFORE) { param ->
|
context.event.subscribe(SnapWidgetBroadcastReceiveEvent::class) { event ->
|
||||||
val androidContext = param.arg<Context>(0)
|
val intent = event.intent ?: return@subscribe
|
||||||
val intent = param.arg<Intent>(1)
|
val conversationId = intent.getStringExtra("conversation_id") ?: return@subscribe
|
||||||
|
|
||||||
val conversationId = intent.getStringExtra("conversation_id") ?: return@hook
|
|
||||||
val messageId = intent.getLongExtra("message_id", -1)
|
val messageId = intent.getLongExtra("message_id", -1)
|
||||||
val notificationId = intent.getIntExtra("notification_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 ->
|
val updateNotification: (Int, (Notification) -> Unit) -> Unit = { id, notificationBuilder ->
|
||||||
notificationManager.activeNotifications.firstOrNull { it.id == id }?.let {
|
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 -> {
|
ACTION_REPLY -> {
|
||||||
val input = RemoteInput.getResultsFromIntent(intent).getCharSequence("chat_reply_input")
|
val input = RemoteInput.getResultsFromIntent(intent).getCharSequence("chat_reply_input")
|
||||||
.toString()
|
.toString()
|
||||||
@ -177,10 +174,10 @@ class Notifications : Feature("Notifications", loadParams = FeatureLoadParams.IN
|
|||||||
context.longToast(it)
|
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.HookStage
|
||||||
import me.rhunk.snapenhance.hook.hook
|
import me.rhunk.snapenhance.hook.hook
|
||||||
import me.rhunk.snapenhance.hook.hookConstructor
|
import me.rhunk.snapenhance.hook.hookConstructor
|
||||||
import me.rhunk.snapenhance.util.getObjectField
|
import me.rhunk.snapenhance.util.ktx.getObjectField
|
||||||
import me.rhunk.snapenhance.util.setObjectField
|
import me.rhunk.snapenhance.util.ktx.setObjectField
|
||||||
|
|
||||||
class PinConversations : BridgeFileFeature("PinConversations", BridgeFileType.PINNED_CONVERSATIONS, loadParams = FeatureLoadParams.ACTIVITY_CREATE_SYNC) {
|
class PinConversations : BridgeFileFeature("PinConversations", BridgeFileType.PINNED_CONVERSATIONS, loadParams = FeatureLoadParams.ACTIVITY_CREATE_SYNC) {
|
||||||
override fun onActivityCreate() {
|
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)
|
//mapped conversation fetch (may not work with legacy sc versions)
|
||||||
messaging.lastFetchGroupConversationUUID?.let {
|
messaging.lastFetchGroupConversationUUID?.let {
|
||||||
context.database.getFriendFeedInfoByConversationId(it.toString())?.let { friendFeedInfo ->
|
context.database.getFeedEntryByConversationId(it.toString())?.let { friendFeedInfo ->
|
||||||
val participantSize = friendFeedInfo.participantsSize
|
val participantSize = friendFeedInfo.participantsSize
|
||||||
return it.toString() to if (participantSize == 1) focusedConversationTargetUser else null
|
return it.toString() to if (participantSize == 1) focusedConversationTargetUser else null
|
||||||
}
|
}
|
||||||
@ -280,7 +280,7 @@ class FriendFeedInfoMenu : AbstractMenu() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
run {
|
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")) {
|
if (friendFeedMenuOptions.contains("auto_download_blacklist")) {
|
||||||
createToggleFeature(viewConsumer,
|
createToggleFeature(viewConsumer,
|
||||||
"friend_menu_option.auto_download_blacklist",
|
"friend_menu_option.auto_download_blacklist",
|
||||||
@ -340,7 +340,7 @@ class FriendFeedInfoMenu : AbstractMenu() {
|
|||||||
if (friendFeedMenuOptions.contains("auto_download_blacklist")) {
|
if (friendFeedMenuOptions.contains("auto_download_blacklist")) {
|
||||||
run {
|
run {
|
||||||
val userId =
|
val userId =
|
||||||
context.database.getFriendFeedInfoByConversationId(conversationId)?.friendUserId
|
context.database.getFeedEntryByConversationId(conversationId)?.friendUserId
|
||||||
?: return@run
|
?: return@run
|
||||||
createActionButton(
|
createActionButton(
|
||||||
"\u2B07\uFE0F",
|
"\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.MediaReferenceType
|
||||||
import me.rhunk.snapenhance.data.wrapper.impl.Message
|
import me.rhunk.snapenhance.data.wrapper.impl.Message
|
||||||
import me.rhunk.snapenhance.data.wrapper.impl.SnapUUID
|
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.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.protobuf.ProtoReader
|
||||||
import me.rhunk.snapenhance.util.snap.EncryptionHelper
|
import me.rhunk.snapenhance.util.snap.EncryptionHelper
|
||||||
import me.rhunk.snapenhance.util.snap.MediaDownloaderHelper
|
import me.rhunk.snapenhance.util.snap.MediaDownloaderHelper
|
||||||
@ -50,7 +50,7 @@ enum class ExportFormat(
|
|||||||
class MessageExporter(
|
class MessageExporter(
|
||||||
private val context: ModContext,
|
private val context: ModContext,
|
||||||
private val outputFile: File,
|
private val outputFile: File,
|
||||||
private val friendFeedInfo: FriendFeedInfo,
|
private val friendFeedEntry: FriendFeedEntry,
|
||||||
private val mediaToDownload: List<ContentType>? = null,
|
private val mediaToDownload: List<ContentType>? = null,
|
||||||
private val printLog: (String) -> Unit = {},
|
private val printLog: (String) -> Unit = {},
|
||||||
) {
|
) {
|
||||||
@ -59,13 +59,13 @@ class MessageExporter(
|
|||||||
|
|
||||||
fun readMessages(messages: List<Message>) {
|
fun readMessages(messages: List<Message>) {
|
||||||
conversationParticipants =
|
conversationParticipants =
|
||||||
context.database.getConversationParticipants(friendFeedInfo.key!!)
|
context.database.getConversationParticipants(friendFeedEntry.key!!)
|
||||||
?.mapNotNull {
|
?.mapNotNull {
|
||||||
context.database.getFriendInfo(it)
|
context.database.getFriendInfo(it)
|
||||||
}?.associateBy { it.userId!! } ?: emptyMap()
|
}?.associateBy { it.userId!! } ?: emptyMap()
|
||||||
|
|
||||||
if (conversationParticipants.isEmpty())
|
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 }
|
this.messages = messages.sortedBy { it.orderKey }
|
||||||
}
|
}
|
||||||
@ -78,8 +78,8 @@ class MessageExporter(
|
|||||||
|
|
||||||
private fun exportText(output: OutputStream) {
|
private fun exportText(output: OutputStream) {
|
||||||
val writer = output.bufferedWriter()
|
val writer = output.bufferedWriter()
|
||||||
writer.write("Conversation key: ${friendFeedInfo.key}\n")
|
writer.write("Conversation key: ${friendFeedEntry.key}\n")
|
||||||
writer.write("Conversation Name: ${friendFeedInfo.feedDisplayName}\n")
|
writer.write("Conversation Name: ${friendFeedEntry.feedDisplayName}\n")
|
||||||
writer.write("Participants:\n")
|
writer.write("Participants:\n")
|
||||||
conversationParticipants.forEach { (userId, friendInfo) ->
|
conversationParticipants.forEach { (userId, friendInfo) ->
|
||||||
writer.write(" $userId: ${friendInfo.displayName}\n")
|
writer.write(" $userId: ${friendInfo.displayName}\n")
|
||||||
@ -233,8 +233,8 @@ class MessageExporter(
|
|||||||
|
|
||||||
private fun exportJson(output: OutputStream) {
|
private fun exportJson(output: OutputStream) {
|
||||||
val rootObject = JsonObject().apply {
|
val rootObject = JsonObject().apply {
|
||||||
addProperty("conversationId", friendFeedInfo.key)
|
addProperty("conversationId", friendFeedEntry.key)
|
||||||
addProperty("conversationName", friendFeedInfo.feedDisplayName)
|
addProperty("conversationName", friendFeedEntry.feedDisplayName)
|
||||||
|
|
||||||
var index = 0
|
var index = 0
|
||||||
val participants = mutableMapOf<String, Int>()
|
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
|
||||||
import android.content.pm.PackageManager.ApplicationInfoFlags
|
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
|
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
|
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