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:
rhunk
2023-08-19 11:58:41 +02:00
parent 11b7119f8b
commit 8a19f27d96
61 changed files with 972 additions and 522 deletions

View File

@ -2,58 +2,77 @@ package me.rhunk.snapenhance.bridge;
import java.util.List;
import me.rhunk.snapenhance.bridge.DownloadCallback;
import me.rhunk.snapenhance.bridge.SyncCallback;
interface BridgeInterface {
/**
* Execute a file operation
*/
byte[] fileOperation(int action, int fileType, in @nullable byte[] content);
/**
* Execute a file operation
*/
byte[] fileOperation(int action, int fileType, in @nullable byte[] content);
/**
* Get the content of a logged message from the database
*
* @param conversationId the ID of the conversation
* @return the content of the message
*/
long[] getLoggedMessageIds(String conversationId, int limit);
/**
* Get the content of a logged message from the database
*
* @param conversationId the ID of the conversation
* @return the content of the message
*/
long[] getLoggedMessageIds(String conversationId, int limit);
/**
* Get the content of a logged message from the database
*
* @param id the ID of the message logger message
* @return the content of the message
*/
@nullable byte[] getMessageLoggerMessage(String conversationId, long id);
/**
* Get the content of a logged message from the database
*
* @param id the ID of the message logger message
* @return the content of the message
*/
@nullable byte[] getMessageLoggerMessage(String conversationId, long id);
/**
* Add a message to the message logger database
*
* @param id the ID of the message logger message
* @param message the content of the message
*/
void addMessageLoggerMessage(String conversationId, long id, in byte[] message);
/**
* Add a message to the message logger database
*
* @param id the ID of the message logger message
* @param message the content of the message
*/
void addMessageLoggerMessage(String conversationId, long id, in byte[] message);
/**
* Delete a message from the message logger database
*
* @param id the ID of the message logger message
*/
void deleteMessageLoggerMessage(String conversationId, long id);
/**
* Delete a message from the message logger database
*
* @param id the ID of the message logger message
*/
void deleteMessageLoggerMessage(String conversationId, long id);
/**
* Clear the message logger database
*/
void clearMessageLogger();
/**
* Clear the message logger database
*/
void clearMessageLogger();
/**
* Fetch the locales
*
* @return the locale result
*/
Map<String, String> fetchLocales(String userLocale);
/**
* Fetch the locales
*
* @return the locale result
*/
Map<String, String> fetchLocales(String userLocale);
/**
* Enqueue a download
*/
void enqueueDownload(in Intent intent, DownloadCallback callback);
/**
* Enqueue a download
*/
void enqueueDownload(in Intent intent, DownloadCallback callback);
/**
* Get rules for a given user or conversation
*/
List<String> getRules(String objectType, String uuid);
/**
* Sync groups and friends
*/
oneway void sync(SyncCallback callback);
/**
* Pass all groups and friends to be able to add them to the database
* @param groups serialized groups
* @param friends serialized friends
*/
oneway void passGroupsAndFriends(in List<String> groups, in List<String> friends);
}

View File

@ -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);
}

View File

@ -20,7 +20,7 @@
"downloads": "Downloads",
"features": "Features",
"home": "Home",
"friends": "Friends",
"social": "Social",
"plugins": "Plugins"
},
"features": {

View File

@ -1,12 +1,15 @@
package me.rhunk.snapenhance
import android.content.Intent
import me.rhunk.snapenhance.core.eventbus.events.impl.OnSnapInteractionEvent
import me.rhunk.snapenhance.core.eventbus.events.impl.SendMessageWithContentEvent
import me.rhunk.snapenhance.core.eventbus.events.impl.SnapWidgetBroadcastReceiveEvent
import me.rhunk.snapenhance.data.wrapper.impl.MessageContent
import me.rhunk.snapenhance.data.wrapper.impl.SnapUUID
import me.rhunk.snapenhance.hook.HookStage
import me.rhunk.snapenhance.hook.hook
import me.rhunk.snapenhance.manager.Manager
import me.rhunk.snapenhance.util.snap.SnapWidgetBroadcastReceiverHelper
class EventDispatcher(
private val context: ModContext
@ -14,7 +17,7 @@ class EventDispatcher(
override fun init() {
context.classCache.conversationManager.hook("sendMessageWithContent", HookStage.BEFORE) { param ->
val messageContent = MessageContent(param.arg(1))
context.event.post(SendMessageWithContentEvent(messageContent).apply { adapter = param })?.let {
context.event.post(SendMessageWithContentEvent(messageContent).apply { adapter = param })?.also {
if (it.canceled) {
param.setResult(null)
}
@ -29,7 +32,26 @@ class EventDispatcher(
conversationId = conversationId,
messageId = messageId
)
)?.let {
)?.also {
if (it.canceled) {
param.setResult(null)
}
}
}
context.androidContext.classLoader.loadClass(SnapWidgetBroadcastReceiverHelper.CLASS_NAME)
.hook("onReceive", HookStage.BEFORE) { param ->
val intent = param.arg(1) as? Intent ?: return@hook
if (!SnapWidgetBroadcastReceiverHelper.isIncomingIntentValid(intent)) return@hook
val action = intent.getStringExtra("action") ?: return@hook
context.event.post(
SnapWidgetBroadcastReceiveEvent(
androidContext = context.androidContext,
intent = intent,
action = action
)
)?.also {
if (it.canceled) {
param.setResult(null)
}

View File

@ -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)
}
}

View File

@ -7,12 +7,16 @@ import android.content.pm.PackageManager
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import me.rhunk.snapenhance.bridge.BridgeClient
import me.rhunk.snapenhance.bridge.SyncCallback
import me.rhunk.snapenhance.core.BuildConfig
import me.rhunk.snapenhance.core.eventbus.events.impl.SnapWidgetBroadcastReceiveEvent
import me.rhunk.snapenhance.core.messaging.MessagingFriendInfo
import me.rhunk.snapenhance.core.messaging.MessagingGroupInfo
import me.rhunk.snapenhance.data.SnapClassCache
import me.rhunk.snapenhance.hook.HookStage
import me.rhunk.snapenhance.hook.Hooker
import me.rhunk.snapenhance.hook.hook
import me.rhunk.snapenhance.util.getApplicationInfoCompat
import me.rhunk.snapenhance.util.ktx.getApplicationInfoCompat
import kotlin.time.ExperimentalTime
import kotlin.time.measureTime
@ -104,6 +108,7 @@ class SnapEnhance {
//if mappings aren't loaded, we can't initialize features
if (!mappings.isMappingsLoaded()) return
features.init()
syncRemote()
}
}.also { time ->
Logger.debug("init took $time")
@ -121,4 +126,53 @@ class SnapEnhance {
Logger.debug("onActivityCreate took $time")
}
}
private fun syncRemote() {
val database = appContext.database
appContext.bridgeClient.sync(object : SyncCallback.Stub() {
override fun syncFriend(uuid: String): String? {
return database.getFriendInfo(uuid)?.toJson()
}
override fun syncGroup(uuid: String): String? {
return database.getFeedEntryByConversationId(uuid)?.let {
MessagingGroupInfo(
it.key!!,
it.feedDisplayName!!,
it.participantsSize
).toJson()
}
}
})
appContext.event.subscribe(SnapWidgetBroadcastReceiveEvent::class) { event ->
if (event.action != BridgeClient.BRIDGE_SYNC_ACTION) return@subscribe
event.canceled = true
val feedEntries = appContext.database.getFeedEntries(Int.MAX_VALUE)
val groups = feedEntries.filter { it.friendUserId == null }.map {
MessagingGroupInfo(
it.key!!,
it.feedDisplayName!!,
it.participantsSize
)
}
val friends = feedEntries.filter { it.friendUserId != null }.map {
MessagingFriendInfo(
it.friendUserId!!,
it.friendDisplayName,
it.friendDisplayUsername!!.split("|")[1],
it.bitmojiAvatarId,
it.bitmojiSelfieId
)
}
appContext.bridgeClient.passGroupsAndFriends(
groups.map { it.toJson() },
friends.map { it.toJson() }
)
}
}
}

View File

@ -17,7 +17,7 @@ import me.rhunk.snapenhance.action.AbstractAction
import me.rhunk.snapenhance.data.ContentType
import me.rhunk.snapenhance.data.wrapper.impl.Message
import me.rhunk.snapenhance.data.wrapper.impl.SnapUUID
import me.rhunk.snapenhance.database.objects.FriendFeedInfo
import me.rhunk.snapenhance.database.objects.FriendFeedEntry
import me.rhunk.snapenhance.features.impl.Messaging
import me.rhunk.snapenhance.ui.ViewAppearanceHelper
import me.rhunk.snapenhance.util.CallbackBuilder
@ -108,8 +108,8 @@ class ExportChatMessages : AbstractAction("action.export_chat_messages") {
exportType = askExportType() ?: return@launch
mediaToDownload = if (exportType == ExportFormat.HTML) askMediaToDownload() else null
val friendFeedEntries = context.database.getFriendFeed(20)
val selectedConversations = mutableListOf<FriendFeedInfo>()
val friendFeedEntries = context.database.getFeedEntries(20)
val selectedConversations = mutableListOf<FriendFeedEntry>()
ViewAppearanceHelper.newAlertDialogBuilder(context.mainActivity)
.setTitle(context.translation["chat_export.select_conversation"])
@ -182,12 +182,12 @@ class ExportChatMessages : AbstractAction("action.export_chat_messages") {
)
}
private suspend fun exportFullConversation(friendFeedInfo: FriendFeedInfo) {
private suspend fun exportFullConversation(friendFeedEntry: FriendFeedEntry) {
//first fetch the first message
val conversationId = friendFeedInfo.key!!
val conversationName = friendFeedInfo.feedDisplayName ?: friendFeedInfo.friendDisplayName!!.split("|").lastOrNull() ?: "unknown"
val conversationId = friendFeedEntry.key!!
val conversationName = friendFeedEntry.feedDisplayName ?: friendFeedEntry.friendDisplayName!!.split("|").lastOrNull() ?: "unknown"
conversationAction(true, conversationId, if (friendFeedInfo.feedDisplayName != null) "USERCREATEDGROUP" else "ONEONONE")
conversationAction(true, conversationId, if (friendFeedEntry.feedDisplayName != null) "USERCREATEDGROUP" else "ONEONONE")
logDialog(context.translation.format("chat_export.exporting_message", "conversation" to conversationName))
@ -215,7 +215,7 @@ class ExportChatMessages : AbstractAction("action.export_chat_messages") {
logDialog(context.translation["chat_export.writing_output"])
MessageExporter(
context = context,
friendFeedInfo = friendFeedInfo,
friendFeedEntry = friendFeedEntry,
outputFile = outputFile,
mediaToDownload = mediaToDownload,
printLog = ::logDialog
@ -245,7 +245,7 @@ class ExportChatMessages : AbstractAction("action.export_chat_messages") {
}
}
private fun exportChatForConversations(conversations: List<FriendFeedInfo>) {
private fun exportChatForConversations(conversations: List<FriendFeedEntry>) {
dialogLogs.clear()
val jobs = mutableListOf<Job>()

View File

@ -15,7 +15,10 @@ import me.rhunk.snapenhance.ModContext
import me.rhunk.snapenhance.bridge.types.BridgeFileType
import me.rhunk.snapenhance.bridge.types.FileActionType
import me.rhunk.snapenhance.core.BuildConfig
import me.rhunk.snapenhance.core.messaging.MessagingRule
import me.rhunk.snapenhance.core.messaging.RuleScope
import me.rhunk.snapenhance.data.LocalePair
import me.rhunk.snapenhance.util.SerializableDataObject
import java.util.concurrent.CompletableFuture
import java.util.concurrent.Executors
import kotlin.system.exitProcess
@ -27,6 +30,10 @@ class BridgeClient(
private lateinit var future: CompletableFuture<Boolean>
private lateinit var service: BridgeInterface
companion object {
const val BRIDGE_SYNC_ACTION = "me.rhunk.snapenhance.bridge.SYNC"
}
fun start(callback: (Boolean) -> Unit) {
this.future = CompletableFuture()
@ -124,4 +131,14 @@ class BridgeClient(
}
fun enqueueDownload(intent: Intent, callback: DownloadCallback) = service.enqueueDownload(intent, callback)
fun sync(callback: SyncCallback) = service.sync(callback)
fun passGroupsAndFriends(groups: List<String>, friends: List<String>) = service.passGroupsAndFriends(groups, friends)
fun getRulesFromId(type: RuleScope, targetUuid: String): List<MessagingRule> {
return service.getRules(type.name, targetUuid).map {
SerializableDataObject.fromJson(it, MessagingRule::class.java)
}.toList()
}
}

View File

@ -31,7 +31,11 @@ class EventBus(
val obj = object : IListener<T> {
override fun handle(event: T) {
if (!filter(event)) return
listener(event)
runCatching {
listener(event)
}.onFailure {
Logger.error("Error while handling event ${event::class.simpleName}", it)
}
}
}
subscribe(event, obj)

View File

@ -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()

View File

@ -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);
}

View File

@ -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
)

View File

@ -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()

View File

@ -2,7 +2,7 @@ package me.rhunk.snapenhance.data.wrapper.impl
import me.rhunk.snapenhance.data.MessageState
import me.rhunk.snapenhance.data.wrapper.AbstractWrapper
import me.rhunk.snapenhance.util.getObjectField
import me.rhunk.snapenhance.util.ktx.getObjectField
class Message(obj: Any?) : AbstractWrapper(obj) {
val orderKey get() = instanceNonNull().getObjectField("mOrderKey") as Long

View File

@ -2,8 +2,8 @@ package me.rhunk.snapenhance.data.wrapper.impl
import me.rhunk.snapenhance.data.ContentType
import me.rhunk.snapenhance.data.wrapper.AbstractWrapper
import me.rhunk.snapenhance.util.getObjectField
import me.rhunk.snapenhance.util.setObjectField
import me.rhunk.snapenhance.util.ktx.getObjectField
import me.rhunk.snapenhance.util.ktx.setObjectField
class MessageContent(obj: Any?) : AbstractWrapper(obj) {
var content

View File

@ -1,7 +1,7 @@
package me.rhunk.snapenhance.data.wrapper.impl
import me.rhunk.snapenhance.data.wrapper.AbstractWrapper
import me.rhunk.snapenhance.util.getObjectField
import me.rhunk.snapenhance.util.ktx.getObjectField
class MessageDescriptor(obj: Any?) : AbstractWrapper(obj) {
val messageId: Long get() = instanceNonNull().getObjectField("mMessageId") as Long

View File

@ -1,8 +1,8 @@
package me.rhunk.snapenhance.data.wrapper.impl
import me.rhunk.snapenhance.data.wrapper.AbstractWrapper
import me.rhunk.snapenhance.util.getObjectField
import me.rhunk.snapenhance.util.setObjectField
import me.rhunk.snapenhance.util.ktx.getObjectField
import me.rhunk.snapenhance.util.ktx.setObjectField
@Suppress("UNCHECKED_CAST")
class MessageDestinations(obj: Any) : AbstractWrapper(obj){

View File

@ -2,7 +2,7 @@ package me.rhunk.snapenhance.data.wrapper.impl
import me.rhunk.snapenhance.data.PlayableSnapState
import me.rhunk.snapenhance.data.wrapper.AbstractWrapper
import me.rhunk.snapenhance.util.getObjectField
import me.rhunk.snapenhance.util.ktx.getObjectField
class MessageMetadata(obj: Any?) : AbstractWrapper(obj){
val createdAt: Long get() = instanceNonNull().getObjectField("mCreatedAt") as Long

View File

@ -2,7 +2,7 @@ package me.rhunk.snapenhance.data.wrapper.impl
import me.rhunk.snapenhance.SnapEnhance
import me.rhunk.snapenhance.data.wrapper.AbstractWrapper
import me.rhunk.snapenhance.util.getObjectField
import me.rhunk.snapenhance.util.ktx.getObjectField
import java.nio.ByteBuffer
import java.util.UUID

View File

@ -1,7 +1,7 @@
package me.rhunk.snapenhance.data.wrapper.impl
import me.rhunk.snapenhance.data.wrapper.AbstractWrapper
import me.rhunk.snapenhance.util.getObjectField
import me.rhunk.snapenhance.util.ktx.getObjectField
class UserIdToReaction(obj: Any?) : AbstractWrapper(obj) {
val userId = SnapUUID(instanceNonNull().getObjectField("mUserId"))

View File

@ -2,7 +2,7 @@ package me.rhunk.snapenhance.data.wrapper.impl.media
import android.os.Parcelable
import me.rhunk.snapenhance.data.wrapper.AbstractWrapper
import me.rhunk.snapenhance.util.getObjectField
import me.rhunk.snapenhance.util.ktx.getObjectField
import java.lang.reflect.Field

View File

@ -2,7 +2,7 @@ package me.rhunk.snapenhance.data.wrapper.impl.media.opera
import me.rhunk.snapenhance.data.wrapper.AbstractWrapper
import me.rhunk.snapenhance.util.ReflectionHelper
import me.rhunk.snapenhance.util.getObjectField
import me.rhunk.snapenhance.util.ktx.getObjectField
import java.lang.reflect.Field
import java.util.concurrent.ConcurrentHashMap

View File

@ -74,10 +74,10 @@ class DatabaseAccess(private val context: ModContext) : Manager {
return obj
}
fun getFriendFeedInfoByUserId(userId: String): FriendFeedInfo? {
fun getFeedEntryByUserId(userId: String): FriendFeedEntry? {
return safeDatabaseOperation(openMain()) { database ->
readDatabaseObject(
FriendFeedInfo(),
FriendFeedEntry(),
database,
"FriendsFeedView",
"friendUserId = ?",
@ -86,10 +86,10 @@ class DatabaseAccess(private val context: ModContext) : Manager {
}
}
fun getFriendFeedInfoByConversationId(conversationId: String): FriendFeedInfo? {
fun getFeedEntryByConversationId(conversationId: String): FriendFeedEntry? {
return safeDatabaseOperation(openMain()) {
readDatabaseObject(
FriendFeedInfo(),
FriendFeedEntry(),
it,
"FriendsFeedView",
"key = ?",
@ -110,19 +110,19 @@ class DatabaseAccess(private val context: ModContext) : Manager {
}
}
fun getFriendFeed(limit: Int): List<FriendFeedInfo> {
fun getFeedEntries(limit: Int): List<FriendFeedEntry> {
return safeDatabaseOperation(openMain()) { database ->
val cursor = database.rawQuery(
"SELECT * FROM FriendsFeedView ORDER BY _id LIMIT ?",
arrayOf(limit.toString())
)
val list = mutableListOf<FriendFeedInfo>()
val list = mutableListOf<FriendFeedEntry>()
while (cursor.moveToNext()) {
val friendFeedInfo = FriendFeedInfo()
val friendFeedEntry = FriendFeedEntry()
try {
friendFeedInfo.write(cursor)
friendFeedEntry.write(cursor)
} catch (_: Throwable) {}
list.add(friendFeedInfo)
list.add(friendFeedEntry)
}
cursor.close()
list

View File

@ -5,10 +5,10 @@ import android.database.Cursor
import me.rhunk.snapenhance.Constants
import me.rhunk.snapenhance.data.ContentType
import me.rhunk.snapenhance.database.DatabaseObject
import me.rhunk.snapenhance.util.getBlobOrNull
import me.rhunk.snapenhance.util.getInteger
import me.rhunk.snapenhance.util.getLong
import me.rhunk.snapenhance.util.getStringOrNull
import me.rhunk.snapenhance.util.ktx.getBlobOrNull
import me.rhunk.snapenhance.util.ktx.getInteger
import me.rhunk.snapenhance.util.ktx.getLong
import me.rhunk.snapenhance.util.ktx.getStringOrNull
import me.rhunk.snapenhance.util.protobuf.ProtoReader
@Suppress("ArrayInDataClass")

View File

@ -3,21 +3,26 @@ package me.rhunk.snapenhance.database.objects
import android.annotation.SuppressLint
import android.database.Cursor
import me.rhunk.snapenhance.database.DatabaseObject
import me.rhunk.snapenhance.util.getInteger
import me.rhunk.snapenhance.util.getLong
import me.rhunk.snapenhance.util.getStringOrNull
import me.rhunk.snapenhance.util.ktx.getIntOrNull
import me.rhunk.snapenhance.util.ktx.getInteger
import me.rhunk.snapenhance.util.ktx.getLong
import me.rhunk.snapenhance.util.ktx.getStringOrNull
data class FriendFeedInfo(
data class FriendFeedEntry(
var id: Int = 0,
var feedDisplayName: String? = null,
var participantsSize: Int = 0,
var lastInteractionTimestamp: Long = 0,
var displayTimestamp: Long = 0,
var displayInteractionType: String? = null,
var lastInteractionUserId: Int = 0,
var lastInteractionUserId: Int? = null,
var key: String? = null,
var friendUserId: String? = null,
var friendDisplayName: String? = null,
var friendDisplayUsername: String? = null,
var friendLinkType: Int? = null,
var bitmojiAvatarId: String? = null,
var bitmojiSelfieId: String? = null,
) : DatabaseObject {
@SuppressLint("Range")
@ -29,10 +34,14 @@ data class FriendFeedInfo(
lastInteractionTimestamp = getLong("lastInteractionTimestamp")
displayTimestamp = getLong("displayTimestamp")
displayInteractionType = getStringOrNull("displayInteractionType")
lastInteractionUserId = getInteger("lastInteractionUserId")
lastInteractionUserId = getIntOrNull("lastInteractionUserId")
key = getStringOrNull("key")
friendUserId = getStringOrNull("friendUserId")
friendDisplayName = getStringOrNull("friendDisplayUsername")
friendDisplayName = getStringOrNull("friendDisplayName")
friendDisplayUsername = getStringOrNull("friendDisplayUsername")
friendLinkType = getIntOrNull("friendLinkType")
bitmojiAvatarId = getStringOrNull("bitmojiAvatarId")
bitmojiSelfieId = getStringOrNull("bitmojiSelfieId")
}
}
}

View File

@ -3,9 +3,10 @@ package me.rhunk.snapenhance.database.objects
import android.annotation.SuppressLint
import android.database.Cursor
import me.rhunk.snapenhance.database.DatabaseObject
import me.rhunk.snapenhance.util.getInteger
import me.rhunk.snapenhance.util.getLong
import me.rhunk.snapenhance.util.getStringOrNull
import me.rhunk.snapenhance.util.SerializableDataObject
import me.rhunk.snapenhance.util.ktx.getInteger
import me.rhunk.snapenhance.util.ktx.getLong
import me.rhunk.snapenhance.util.ktx.getStringOrNull
data class FriendInfo(
var id: Int = 0,
@ -30,7 +31,7 @@ data class FriendInfo(
var isPinnedBestFriend: Int = 0,
var plusBadgeVisibility: Int = 0,
var usernameForSorting: String? = null
) : DatabaseObject {
) : DatabaseObject, SerializableDataObject() {
@SuppressLint("Range")
override fun write(cursor: Cursor) {
with(cursor) {

View File

@ -3,8 +3,8 @@ package me.rhunk.snapenhance.database.objects
import android.annotation.SuppressLint
import android.database.Cursor
import me.rhunk.snapenhance.database.DatabaseObject
import me.rhunk.snapenhance.util.getInteger
import me.rhunk.snapenhance.util.getStringOrNull
import me.rhunk.snapenhance.util.ktx.getInteger
import me.rhunk.snapenhance.util.ktx.getStringOrNull
data class StoryEntry(
var id: Int = 0,

View File

@ -3,8 +3,8 @@ package me.rhunk.snapenhance.database.objects
import android.annotation.SuppressLint
import android.database.Cursor
import me.rhunk.snapenhance.database.DatabaseObject
import me.rhunk.snapenhance.util.getInteger
import me.rhunk.snapenhance.util.getStringOrNull
import me.rhunk.snapenhance.util.ktx.getInteger
import me.rhunk.snapenhance.util.ktx.getStringOrNull
class UserConversationLink(
var userId: String? = null,

View File

@ -8,8 +8,8 @@ import me.rhunk.snapenhance.download.data.DownloadObject
import me.rhunk.snapenhance.download.data.DownloadStage
import me.rhunk.snapenhance.download.data.MediaFilter
import me.rhunk.snapenhance.util.SQLiteDatabaseHelper
import me.rhunk.snapenhance.util.getIntOrNull
import me.rhunk.snapenhance.util.getStringOrNull
import me.rhunk.snapenhance.util.ktx.getIntOrNull
import me.rhunk.snapenhance.util.ktx.getStringOrNull
class DownloadTaskManager {
private lateinit var taskDatabase: SQLiteDatabase

View File

@ -5,7 +5,7 @@ import me.rhunk.snapenhance.features.Feature
import me.rhunk.snapenhance.features.FeatureLoadParams
import me.rhunk.snapenhance.hook.HookStage
import me.rhunk.snapenhance.hook.hook
import me.rhunk.snapenhance.util.setObjectField
import me.rhunk.snapenhance.util.ktx.setObjectField
class ConfigurationOverride : Feature("Configuration Override", loadParams = FeatureLoadParams.INIT_SYNC) {
override fun init() {

View File

@ -8,7 +8,7 @@ import me.rhunk.snapenhance.features.impl.spying.StealthMode
import me.rhunk.snapenhance.hook.HookStage
import me.rhunk.snapenhance.hook.Hooker
import me.rhunk.snapenhance.hook.hook
import me.rhunk.snapenhance.util.getObjectField
import me.rhunk.snapenhance.util.ktx.getObjectField
class Messaging : Feature("Messaging", loadParams = FeatureLoadParams.ACTIVITY_CREATE_SYNC or FeatureLoadParams.INIT_ASYNC or FeatureLoadParams.INIT_SYNC) {
lateinit var conversationManager: Any

View File

@ -6,7 +6,6 @@ import android.graphics.BitmapFactory
import android.net.Uri
import android.widget.ImageView
import kotlinx.coroutines.runBlocking
import me.rhunk.snapenhance.Constants.ARROYO_URL_KEY_PROTO_PATH
import me.rhunk.snapenhance.Logger
import me.rhunk.snapenhance.Logger.xposedLog
import me.rhunk.snapenhance.bridge.DownloadCallback
@ -22,6 +21,7 @@ import me.rhunk.snapenhance.download.DownloadManagerClient
import me.rhunk.snapenhance.download.data.DownloadMediaType
import me.rhunk.snapenhance.download.data.DownloadMetadata
import me.rhunk.snapenhance.download.data.InputMedia
import me.rhunk.snapenhance.download.data.MediaFilter
import me.rhunk.snapenhance.download.data.SplitMediaAssetType
import me.rhunk.snapenhance.download.data.toKeyPair
import me.rhunk.snapenhance.features.Feature
@ -32,9 +32,8 @@ import me.rhunk.snapenhance.hook.HookAdapter
import me.rhunk.snapenhance.hook.HookStage
import me.rhunk.snapenhance.hook.Hooker
import me.rhunk.snapenhance.ui.ViewAppearanceHelper
import me.rhunk.snapenhance.download.data.MediaFilter
import me.rhunk.snapenhance.util.download.RemoteMediaResolver
import me.rhunk.snapenhance.util.getObjectField
import me.rhunk.snapenhance.util.ktx.getObjectField
import me.rhunk.snapenhance.util.protobuf.ProtoReader
import me.rhunk.snapenhance.util.snap.BitmojiSelfie
import me.rhunk.snapenhance.util.snap.EncryptionHelper

View File

@ -4,7 +4,7 @@ import me.rhunk.snapenhance.features.Feature
import me.rhunk.snapenhance.features.FeatureLoadParams
import me.rhunk.snapenhance.hook.HookStage
import me.rhunk.snapenhance.hook.hookConstructor
import me.rhunk.snapenhance.util.setObjectField
import me.rhunk.snapenhance.util.ktx.setObjectField
class UnlimitedMultiSnap : Feature("UnlimitedMultiSnap", loadParams = FeatureLoadParams.ACTIVITY_CREATE_ASYNC) {
override fun asyncOnActivityCreate() {

View File

@ -4,7 +4,7 @@ import me.rhunk.snapenhance.features.Feature
import me.rhunk.snapenhance.features.FeatureLoadParams
import me.rhunk.snapenhance.hook.HookStage
import me.rhunk.snapenhance.hook.Hooker
import me.rhunk.snapenhance.util.getObjectField
import me.rhunk.snapenhance.util.ktx.getObjectField
class AnonymousStoryViewing : Feature("Anonymous Story Viewing", loadParams = FeatureLoadParams.ACTIVITY_CREATE_ASYNC) {
override fun asyncOnActivityCreate() {

View File

@ -71,7 +71,7 @@ class MessageLogger : Feature("MessageLogger",
}
measureTime {
context.database.getFriendFeed(PREFETCH_FEED_COUNT).forEach { friendFeedInfo ->
context.database.getFeedEntries(PREFETCH_FEED_COUNT).forEach { friendFeedInfo ->
fetchedMessages.addAll(context.bridgeClient.getLoggedMessageIds(friendFeedInfo.key!!, PREFETCH_MESSAGE_COUNT).toList())
}
}.also { Logger.debug("Loaded ${fetchedMessages.size} cached messages in $it") }

View File

@ -12,7 +12,7 @@ import me.rhunk.snapenhance.features.impl.spying.StealthMode
import me.rhunk.snapenhance.hook.HookStage
import me.rhunk.snapenhance.hook.Hooker
import me.rhunk.snapenhance.util.CallbackBuilder
import me.rhunk.snapenhance.util.getObjectField
import me.rhunk.snapenhance.util.ktx.getObjectField
import java.util.concurrent.Executors
class AutoSave : Feature("Auto Save", loadParams = FeatureLoadParams.ACTIVITY_CREATE_ASYNC) {

View File

@ -12,8 +12,8 @@ import android.os.Bundle
import android.os.UserHandle
import de.robv.android.xposed.XposedBridge
import de.robv.android.xposed.XposedHelpers
import me.rhunk.snapenhance.Constants
import me.rhunk.snapenhance.Logger
import me.rhunk.snapenhance.core.eventbus.events.impl.SnapWidgetBroadcastReceiveEvent
import me.rhunk.snapenhance.data.ContentType
import me.rhunk.snapenhance.data.MediaReferenceType
import me.rhunk.snapenhance.data.wrapper.impl.Message
@ -31,6 +31,7 @@ import me.rhunk.snapenhance.util.protobuf.ProtoReader
import me.rhunk.snapenhance.util.snap.EncryptionHelper
import me.rhunk.snapenhance.util.snap.MediaDownloaderHelper
import me.rhunk.snapenhance.util.snap.PreviewUtils
import me.rhunk.snapenhance.util.snap.SnapWidgetBroadcastReceiverHelper
class Notifications : Feature("Notifications", loadParams = FeatureLoadParams.INIT_SYNC) {
companion object{
@ -42,10 +43,6 @@ class Notifications : Feature("Notifications", loadParams = FeatureLoadParams.IN
private val cachedMessages = mutableMapOf<String, MutableList<String>>() // conversationId => cached messages
private val notificationIdMap = mutableMapOf<Int, String>() // notificationId => conversationId
private val broadcastReceiverClass by lazy {
context.androidContext.classLoader.loadClass("com.snap.widgets.core.BestFriendsWidgetProvider")
}
private val notifyAsUserMethod by lazy {
XposedHelpers.findMethodExact(
NotificationManager::class.java, "notifyAsUser",
@ -102,16 +99,18 @@ class Notifications : Feature("Notifications", loadParams = FeatureLoadParams.IN
fun newAction(title: String, remoteAction: String, filter: (() -> Boolean), builder: (Notification.Action.Builder) -> Unit) {
if (!filter()) return
val intent = Intent().setClassName(Constants.SNAPCHAT_PACKAGE_NAME, broadcastReceiverClass.name)
.putExtra("conversation_id", conversationId)
.putExtra("notification_id", notificationData.id)
.putExtra("message_id", messageId)
.setAction(remoteAction)
val intent = SnapWidgetBroadcastReceiverHelper.create(remoteAction) {
putExtra("conversation_id", conversationId)
putExtra("notification_id", notificationData.id)
putExtra("message_id", messageId)
}
val action = Notification.Action.Builder(null, title, PendingIntent.getBroadcast(
context.androidContext,
System.nanoTime().toInt(),
intent,
PendingIntent.FLAG_CANCEL_CURRENT or PendingIntent.FLAG_MUTABLE
PendingIntent.FLAG_MUTABLE
)).apply(builder).build()
actions.add(action)
}
@ -134,14 +133,12 @@ class Notifications : Feature("Notifications", loadParams = FeatureLoadParams.IN
}
private fun setupBroadcastReceiverHook() {
Hooker.hook(broadcastReceiverClass, "onReceive", HookStage.BEFORE) { param ->
val androidContext = param.arg<Context>(0)
val intent = param.arg<Intent>(1)
val conversationId = intent.getStringExtra("conversation_id") ?: return@hook
context.event.subscribe(SnapWidgetBroadcastReceiveEvent::class) { event ->
val intent = event.intent ?: return@subscribe
val conversationId = intent.getStringExtra("conversation_id") ?: return@subscribe
val messageId = intent.getLongExtra("message_id", -1)
val notificationId = intent.getIntExtra("notification_id", -1)
val notificationManager = androidContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
val notificationManager = event.androidContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
val updateNotification: (Int, (Notification) -> Unit) -> Unit = { id, notificationBuilder ->
notificationManager.activeNotifications.firstOrNull { it.id == id }?.let {
@ -152,7 +149,7 @@ class Notifications : Feature("Notifications", loadParams = FeatureLoadParams.IN
}
}
when (intent.action) {
when (event.action) {
ACTION_REPLY -> {
val input = RemoteInput.getResultsFromIntent(intent).getCharSequence("chat_reply_input")
.toString()
@ -177,10 +174,10 @@ class Notifications : Feature("Notifications", loadParams = FeatureLoadParams.IN
context.longToast(it)
}
}
else -> return@hook
else -> return@subscribe
}
param.setResult(null)
event.canceled = true
}
}

View File

@ -7,8 +7,8 @@ import me.rhunk.snapenhance.features.FeatureLoadParams
import me.rhunk.snapenhance.hook.HookStage
import me.rhunk.snapenhance.hook.hook
import me.rhunk.snapenhance.hook.hookConstructor
import me.rhunk.snapenhance.util.getObjectField
import me.rhunk.snapenhance.util.setObjectField
import me.rhunk.snapenhance.util.ktx.getObjectField
import me.rhunk.snapenhance.util.ktx.setObjectField
class PinConversations : BridgeFileFeature("PinConversations", BridgeFileType.PINNED_CONVERSATIONS, loadParams = FeatureLoadParams.ACTIVITY_CREATE_SYNC) {
override fun onActivityCreate() {

View File

@ -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, *>
}
}

View File

@ -204,7 +204,7 @@ class FriendFeedInfoMenu : AbstractMenu() {
//mapped conversation fetch (may not work with legacy sc versions)
messaging.lastFetchGroupConversationUUID?.let {
context.database.getFriendFeedInfoByConversationId(it.toString())?.let { friendFeedInfo ->
context.database.getFeedEntryByConversationId(it.toString())?.let { friendFeedInfo ->
val participantSize = friendFeedInfo.participantsSize
return it.toString() to if (participantSize == 1) focusedConversationTargetUser else null
}
@ -280,7 +280,7 @@ class FriendFeedInfoMenu : AbstractMenu() {
}
run {
val userId = context.database.getFriendFeedInfoByConversationId(conversationId)?.friendUserId ?: return@run
val userId = context.database.getFeedEntryByConversationId(conversationId)?.friendUserId ?: return@run
if (friendFeedMenuOptions.contains("auto_download_blacklist")) {
createToggleFeature(viewConsumer,
"friend_menu_option.auto_download_blacklist",
@ -340,7 +340,7 @@ class FriendFeedInfoMenu : AbstractMenu() {
if (friendFeedMenuOptions.contains("auto_download_blacklist")) {
run {
val userId =
context.database.getFriendFeedInfoByConversationId(conversationId)?.friendUserId
context.database.getFeedEntryByConversationId(conversationId)?.friendUserId
?: return@run
createActionButton(
"\u2B07\uFE0F",

View File

@ -1,5 +0,0 @@
package me.rhunk.snapenhance.util
import android.content.Intent
typealias ActivityResultCallback = (requestCode: Int, resultCode: Int, data: Intent?) -> Unit

View File

@ -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)
}
}

View File

@ -18,9 +18,9 @@ import me.rhunk.snapenhance.data.FileType
import me.rhunk.snapenhance.data.MediaReferenceType
import me.rhunk.snapenhance.data.wrapper.impl.Message
import me.rhunk.snapenhance.data.wrapper.impl.SnapUUID
import me.rhunk.snapenhance.database.objects.FriendFeedInfo
import me.rhunk.snapenhance.database.objects.FriendFeedEntry
import me.rhunk.snapenhance.database.objects.FriendInfo
import me.rhunk.snapenhance.util.getApplicationInfoCompat
import me.rhunk.snapenhance.util.ktx.getApplicationInfoCompat
import me.rhunk.snapenhance.util.protobuf.ProtoReader
import me.rhunk.snapenhance.util.snap.EncryptionHelper
import me.rhunk.snapenhance.util.snap.MediaDownloaderHelper
@ -50,7 +50,7 @@ enum class ExportFormat(
class MessageExporter(
private val context: ModContext,
private val outputFile: File,
private val friendFeedInfo: FriendFeedInfo,
private val friendFeedEntry: FriendFeedEntry,
private val mediaToDownload: List<ContentType>? = null,
private val printLog: (String) -> Unit = {},
) {
@ -59,13 +59,13 @@ class MessageExporter(
fun readMessages(messages: List<Message>) {
conversationParticipants =
context.database.getConversationParticipants(friendFeedInfo.key!!)
context.database.getConversationParticipants(friendFeedEntry.key!!)
?.mapNotNull {
context.database.getFriendInfo(it)
}?.associateBy { it.userId!! } ?: emptyMap()
if (conversationParticipants.isEmpty())
throw Throwable("Failed to get conversation participants for ${friendFeedInfo.key}")
throw Throwable("Failed to get conversation participants for ${friendFeedEntry.key}")
this.messages = messages.sortedBy { it.orderKey }
}
@ -78,8 +78,8 @@ class MessageExporter(
private fun exportText(output: OutputStream) {
val writer = output.bufferedWriter()
writer.write("Conversation key: ${friendFeedInfo.key}\n")
writer.write("Conversation Name: ${friendFeedInfo.feedDisplayName}\n")
writer.write("Conversation key: ${friendFeedEntry.key}\n")
writer.write("Conversation Name: ${friendFeedEntry.feedDisplayName}\n")
writer.write("Participants:\n")
conversationParticipants.forEach { (userId, friendInfo) ->
writer.write(" $userId: ${friendInfo.displayName}\n")
@ -233,8 +233,8 @@ class MessageExporter(
private fun exportJson(output: OutputStream) {
val rootObject = JsonObject().apply {
addProperty("conversationId", friendFeedInfo.key)
addProperty("conversationName", friendFeedInfo.feedDisplayName)
addProperty("conversationId", friendFeedEntry.key)
addProperty("conversationName", friendFeedEntry.feedDisplayName)
var index = 0
val participants = mutableMapOf<String, Int>()

View File

@ -1,4 +1,4 @@
package me.rhunk.snapenhance.util
package me.rhunk.snapenhance.util.ktx
import android.content.pm.PackageManager
import android.content.pm.PackageManager.ApplicationInfoFlags

View File

@ -1,4 +1,4 @@
package me.rhunk.snapenhance.util
package me.rhunk.snapenhance.util.ktx
import android.database.Cursor

View File

@ -1,4 +1,4 @@
package me.rhunk.snapenhance.util
package me.rhunk.snapenhance.util.ktx
import de.robv.android.xposed.XposedHelpers

View File

@ -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)
}
}