feat: reply from notifications
This commit is contained in:
@@ -11,6 +11,7 @@ import android.widget.Toast
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.GsonBuilder
|
||||
import me.rhunk.snapenhance.bridge.AbstractBridgeClient
|
||||
import me.rhunk.snapenhance.data.MessageSender
|
||||
import me.rhunk.snapenhance.database.DatabaseAccess
|
||||
import me.rhunk.snapenhance.features.Feature
|
||||
import me.rhunk.snapenhance.manager.impl.ActionManager
|
||||
@@ -40,6 +41,7 @@ class ModContext {
|
||||
val actionManager = ActionManager(this)
|
||||
val database = DatabaseAccess(this)
|
||||
val downloadServer = DownloadServer(this)
|
||||
val messageSender = MessageSender(this)
|
||||
val classCache get() = SnapEnhance.classCache
|
||||
val resources: Resources get() = androidContext.resources
|
||||
|
||||
|
||||
@@ -169,7 +169,7 @@ enum class ConfigProperty(
|
||||
"description.gallery_media_send_override",
|
||||
ConfigCategory.EXTRAS,
|
||||
ConfigStateSelection(
|
||||
listOf("OFF", "NOTE", "SNAP"),
|
||||
listOf("OFF", "NOTE", "SNAP", "LIVE_SNAP"),
|
||||
"OFF"
|
||||
)
|
||||
),
|
||||
|
||||
164
app/src/main/kotlin/me/rhunk/snapenhance/data/MessageSender.kt
Normal file
164
app/src/main/kotlin/me/rhunk/snapenhance/data/MessageSender.kt
Normal file
@@ -0,0 +1,164 @@
|
||||
package me.rhunk.snapenhance.data
|
||||
|
||||
import me.rhunk.snapenhance.ModContext
|
||||
import me.rhunk.snapenhance.data.wrapper.AbstractWrapper
|
||||
import me.rhunk.snapenhance.data.wrapper.impl.MessageDestinations
|
||||
import me.rhunk.snapenhance.data.wrapper.impl.SnapUUID
|
||||
import me.rhunk.snapenhance.features.impl.Messaging
|
||||
import me.rhunk.snapenhance.util.CallbackBuilder
|
||||
import me.rhunk.snapenhance.util.protobuf.ProtoWriter
|
||||
|
||||
class MessageSender(
|
||||
private val context: ModContext,
|
||||
) {
|
||||
companion object {
|
||||
val redSnapProto: (Boolean) -> ByteArray = {hasAudio ->
|
||||
ProtoWriter().apply {
|
||||
write(11, 5) {
|
||||
write(1) {
|
||||
write(1) {
|
||||
writeConstant(2, 0)
|
||||
writeConstant(12, 0)
|
||||
writeConstant(15, 0)
|
||||
}
|
||||
writeConstant(6, 0)
|
||||
}
|
||||
write(2) {
|
||||
writeConstant(5, if (hasAudio) 1 else 0)
|
||||
writeBuffer(6, byteArrayOf())
|
||||
}
|
||||
}
|
||||
}.toByteArray()
|
||||
}
|
||||
|
||||
val audioNoteProto: (Int) -> ByteArray = { duration ->
|
||||
ProtoWriter().apply {
|
||||
write(6, 1) {
|
||||
write(1) {
|
||||
writeConstant(2, 4)
|
||||
write(5) {
|
||||
writeConstant(1, 0)
|
||||
writeConstant(2, 0)
|
||||
}
|
||||
writeConstant(7, 0)
|
||||
writeConstant(13, duration)
|
||||
}
|
||||
}
|
||||
}.toByteArray()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private val sendMessageCallback by lazy { context.mappings.getMappedClass("callbacks", "SendMessageCallback") }
|
||||
|
||||
private val platformAnalyticsCreatorClass by lazy {
|
||||
context.mappings.getMappedClass("PlatformAnalyticsCreator")
|
||||
}
|
||||
|
||||
private fun defaultPlatformAnalytics(): ByteArray {
|
||||
val analyticsSource = platformAnalyticsCreatorClass.constructors[0].parameterTypes[0]
|
||||
val chatAnalyticsSource = analyticsSource.enumConstants.first { it.toString() == "CHAT" }
|
||||
|
||||
val platformAnalyticsDefaultArgs = arrayOf(chatAnalyticsSource, null, null, null, null, null, null, null, null, null, 0L, 0L,
|
||||
null, null, false, null, null, 0L, null, null, false, null, null,
|
||||
null, null, null, null, null, null, null, null, null, null, null,
|
||||
null, null, null, null, null, null, false, null, null, false, 0L, -2, 8191)
|
||||
|
||||
val platformAnalyticsInstance = platformAnalyticsCreatorClass.constructors[0].newInstance(
|
||||
*platformAnalyticsDefaultArgs
|
||||
) ?: throw Exception("Failed to create platform analytics instance")
|
||||
|
||||
return platformAnalyticsInstance.javaClass.declaredMethods.first { it.returnType == ByteArray::class.java }
|
||||
.invoke(platformAnalyticsInstance) as ByteArray?
|
||||
?: throw Exception("Failed to get platform analytics content")
|
||||
}
|
||||
|
||||
private fun createLocalMessageContentTemplate(
|
||||
contentType: ContentType,
|
||||
messageContent: ByteArray,
|
||||
localMediaReference: ByteArray? = null,
|
||||
metricMessageMediaType: MetricsMessageMediaType = MetricsMessageMediaType.DERIVED_FROM_MESSAGE_TYPE,
|
||||
metricsMediaType: MetricsMessageType = MetricsMessageType.TEXT,
|
||||
savePolicy: String = "PROHIBITED",
|
||||
): String {
|
||||
return """
|
||||
{
|
||||
"mAllowsTranscription": false,
|
||||
"mBotMention": false,
|
||||
"mContent": [${messageContent.joinToString(",")}],
|
||||
"mContentType": "${contentType.name}",
|
||||
"mIncidentalAttachments": [],
|
||||
"mLocalMediaReferences": [${
|
||||
if (localMediaReference != null) {
|
||||
"{\"mId\": [${localMediaReference.joinToString(",")}]}"
|
||||
} else {
|
||||
""
|
||||
}
|
||||
}],
|
||||
"mPlatformAnalytics": {
|
||||
"mAttemptId": {
|
||||
"mId": [${(1..16).map { (-127 ..127).random() }.joinToString(",")}]
|
||||
},
|
||||
"mContent": [${defaultPlatformAnalytics().joinToString(",")}],
|
||||
"mMetricsMessageMediaType": "${metricMessageMediaType.name}",
|
||||
"mMetricsMessageType": "${metricsMediaType.name}",
|
||||
"mReactionSource": "NONE"
|
||||
},
|
||||
"mSavePolicy": "$savePolicy"
|
||||
}
|
||||
""".trimIndent()
|
||||
}
|
||||
|
||||
private fun internalSendMessage(conversations: List<SnapUUID>, localMessageContentTemplate: String, callback: Any) {
|
||||
val sendMessageWithContentMethod = context.classCache.conversationManager.declaredMethods.first { it.name == "sendMessageWithContent" }
|
||||
|
||||
val localMessageContent = context.gson.fromJson(localMessageContentTemplate, context.classCache.localMessageContent)
|
||||
val messageDestinations = MessageDestinations(AbstractWrapper.newEmptyInstance(context.classCache.messageDestinations)).also {
|
||||
it.conversations = conversations
|
||||
it.mPhoneNumbers = arrayListOf()
|
||||
it.stories = arrayListOf()
|
||||
}
|
||||
|
||||
sendMessageWithContentMethod.invoke(context.feature(Messaging::class).conversationManager, messageDestinations.instanceNonNull(), localMessageContent, callback)
|
||||
}
|
||||
|
||||
//TODO: implement sendSnapMessage
|
||||
/*
|
||||
fun sendSnapMessage(conversations: List<SnapUUID>, chatMediaType: ChatMediaType, uri: Uri, onError: (Any) -> Unit = {}, onSuccess: () -> Unit = {}) {
|
||||
val mediaReferenceBuffer = FlatBufferBuilder(0).apply {
|
||||
val uriOffset = createString(uri.toString())
|
||||
forceDefaults(true)
|
||||
startTable(2)
|
||||
addOffset(1, uriOffset, 0)
|
||||
addInt(0, chatMediaType.value, 0)
|
||||
finish(endTable())
|
||||
finished()
|
||||
}.sizedByteArray()
|
||||
|
||||
internalSendMessage(conversations, createLocalMessageContentTemplate(
|
||||
contentType = ContentType.SNAP,
|
||||
messageContent = redSnapProto(chatMediaType == ChatMediaType.AUDIO || chatMediaType == ChatMediaType.VIDEO),
|
||||
localMediaReference = mediaReferenceBuffer,
|
||||
metricMessageMediaType = MetricsMessageMediaType.IMAGE,
|
||||
metricsMediaType = MetricsMessageType.SNAP
|
||||
), CallbackBuilder(sendMessageCallback)
|
||||
.override("onSuccess") {
|
||||
onSuccess()
|
||||
}
|
||||
.override("onError") {
|
||||
onError(it.arg(0))
|
||||
}
|
||||
.build())
|
||||
}*/
|
||||
|
||||
fun sendChatMessage(conversations: List<SnapUUID>, message: String, onError: (Any) -> Unit = {}, onSuccess: () -> Unit = {}) {
|
||||
internalSendMessage(conversations, createLocalMessageContentTemplate(ContentType.CHAT, ProtoWriter().apply {
|
||||
write(2) {
|
||||
writeString(1, message)
|
||||
}
|
||||
}.toByteArray(), savePolicy = "LIFETIME"), CallbackBuilder(sendMessageCallback)
|
||||
.override("onSuccess", callback = { onSuccess() })
|
||||
.override("onError", callback = { onError(it.arg(0)) })
|
||||
.build())
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,8 @@ class SnapClassCache (
|
||||
val messageUpdateEnum by lazy { findClass("com.snapchat.client.messaging.MessageUpdate") }
|
||||
val unifiedGrpcService by lazy { findClass("com.snapchat.client.grpc.UnifiedGrpcService\$CppProxy") }
|
||||
val networkApi by lazy { findClass("com.snapchat.client.network_api.NetworkApi\$CppProxy") }
|
||||
val messageDestinations by lazy { findClass("com.snapchat.client.messaging.MessageDestinations") }
|
||||
val localMessageContent by lazy { findClass("com.snapchat.client.messaging.LocalMessageContent") }
|
||||
|
||||
private fun findClass(className: String): Class<*> {
|
||||
return try {
|
||||
|
||||
@@ -4,6 +4,39 @@ enum class MessageState {
|
||||
PREPARING, SENDING, COMMITTED, FAILED, CANCELING
|
||||
}
|
||||
|
||||
enum class ChatMediaType (
|
||||
val value: Int
|
||||
) {
|
||||
IMAGE(0),
|
||||
VIDEO(1),
|
||||
VIDEO_NO_SOUND(2),
|
||||
FRIEND_DEPRECATED(3),
|
||||
BLOB(4),
|
||||
LAGUNA_SOUND(5),
|
||||
LAGUNA_NO_SOUND(6),
|
||||
GIF(7),
|
||||
FINGERPRINT_HEADER_SIZE(8),
|
||||
AUDIO_STITCH(9),
|
||||
PSYCHOMANTIS(10),
|
||||
SCREAMINGMANTIS(11),
|
||||
MALIBU_SOUND(12),
|
||||
MALIBU_NO_SOUND(13),
|
||||
LAGUNAHD_SOUND(14),
|
||||
LAGUNAHD_NO_SOUND(15),
|
||||
GHOSTMANTIS(16),
|
||||
NEWPORT_SOUND(17),
|
||||
NEWPORT_NO_SOUND(18),
|
||||
AUDIO(19),
|
||||
BLOOP(20),
|
||||
SPECTACLES_IMAGE(21),
|
||||
SPECTACLES_VIDEO(22),
|
||||
SPECTACLES_VIDEO_NO_SOUND(23),
|
||||
CHEERIOS_IMAGE(24),
|
||||
CHEERIOS_VIDEO_SOUND(25),
|
||||
CHEERIOS_VIDEO_NO_SOUND(26),
|
||||
WEB(27),
|
||||
UNRECOGNIZED_VALUE(-9999);
|
||||
}
|
||||
enum class ContentType(val id: Int) {
|
||||
UNKNOWN(-1),
|
||||
SNAP(0),
|
||||
@@ -36,6 +69,66 @@ enum class PlayableSnapState {
|
||||
NOTDOWNLOADED, DOWNLOADING, DOWNLOADFAILED, PLAYABLE, VIEWEDREPLAYABLE, PLAYING, VIEWEDNOTREPLAYABLE
|
||||
}
|
||||
|
||||
enum class MetricsMessageMediaType {
|
||||
NO_MEDIA,
|
||||
IMAGE,
|
||||
VIDEO,
|
||||
VIDEO_NO_SOUND,
|
||||
GIF,
|
||||
DERIVED_FROM_MESSAGE_TYPE,
|
||||
REACTION
|
||||
}
|
||||
|
||||
enum class MetricsMessageType {
|
||||
TEXT,
|
||||
STICKER,
|
||||
CUSTOM_STICKER,
|
||||
SNAP,
|
||||
AUDIO_NOTE,
|
||||
MEDIA,
|
||||
BATCHED_MEDIA,
|
||||
MISSED_AUDIO_CALL,
|
||||
MISSED_VIDEO_CALL,
|
||||
JOINED_CALL,
|
||||
LEFT_CALL,
|
||||
SNAPCHATTER,
|
||||
LOCATION_SHARE,
|
||||
LOCATION_REQUEST,
|
||||
SCREENSHOT,
|
||||
SCREEN_RECORDING,
|
||||
GAME_CLOSED,
|
||||
STORY_SHARE,
|
||||
MAP_DROP_SHARE,
|
||||
MAP_STORY_SHARE,
|
||||
MAP_STORY_SNAP_SHARE,
|
||||
MAP_HEAT_SNAP_SHARE,
|
||||
MAP_SCREENSHOT_SHARE,
|
||||
MEMORIES_STORY,
|
||||
SEARCH_STORY_SHARE,
|
||||
SEARCH_STORY_SNAP_SHARE,
|
||||
DISCOVER_SHARE,
|
||||
SHAZAM_SHARE,
|
||||
SAVE_TO_CAMERA_ROLL,
|
||||
GAME_SCORE_SHARE,
|
||||
SNAP_PRO_PROFILE_SHARE,
|
||||
SNAP_PRO_SNAP_SHARE,
|
||||
CANVAS_APP_SHARE,
|
||||
AD_SHARE,
|
||||
STORY_REPLY,
|
||||
SPOTLIGHT_STORY_SHARE,
|
||||
CAMEO,
|
||||
MEMOJI,
|
||||
BITMOJI_OUTFIT_SHARE,
|
||||
LIVE_LOCATION_SHARE,
|
||||
CREATIVE_TOOL_ITEM,
|
||||
SNAP_KIT_INVITE_SHARE,
|
||||
QUOTE_REPLY_SHARE,
|
||||
BLOOPS_STORY_SHARE,
|
||||
SNAP_PRO_SAVED_STORY_SHARE,
|
||||
PLACE_PROFILE_SHARE,
|
||||
PLACE_STORY_SHARE,
|
||||
SAVED_STORY_SHARE
|
||||
}
|
||||
enum class MediaReferenceType {
|
||||
UNASSIGNED, OVERLAY, IMAGE, VIDEO, ASSET_BUNDLE, AUDIO, ANIMATED_IMAGE, FONT, WEB_VIEW_CONTENT, VIDEO_NO_AUDIO
|
||||
}
|
||||
|
||||
@@ -1,10 +1,17 @@
|
||||
package me.rhunk.snapenhance.data.wrapper
|
||||
|
||||
import de.robv.android.xposed.XposedHelpers
|
||||
import me.rhunk.snapenhance.util.CallbackBuilder
|
||||
|
||||
abstract class AbstractWrapper(
|
||||
protected var instance: Any?
|
||||
) {
|
||||
companion object {
|
||||
fun newEmptyInstance(clazz: Class<*>): Any {
|
||||
return CallbackBuilder.createEmptyObject(clazz.constructors[0]) ?: throw NullPointerException()
|
||||
}
|
||||
}
|
||||
|
||||
fun instanceNonNull(): Any = instance!!
|
||||
fun isPresent(): Boolean = instance == null
|
||||
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
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
|
||||
|
||||
class MessageDestinations(obj: Any) : AbstractWrapper(obj){
|
||||
var conversations get() = (instanceNonNull().getObjectField("mConversations") as ArrayList<*>).map { SnapUUID(it) }
|
||||
set(value) = instanceNonNull().setObjectField("mConversations", value.map { it.instanceNonNull() }.toCollection(ArrayList()))
|
||||
var stories get() = instanceNonNull().getObjectField("mStories") as ArrayList<Any>
|
||||
set(value) = instanceNonNull().setObjectField("mStories", value)
|
||||
var mPhoneNumbers get() = instanceNonNull().getObjectField("mPhoneNumbers") as ArrayList<Any>
|
||||
set(value) = instanceNonNull().setObjectField("mPhoneNumbers", value)
|
||||
}
|
||||
@@ -1,51 +1,16 @@
|
||||
package me.rhunk.snapenhance.features.impl.extras
|
||||
|
||||
import me.rhunk.snapenhance.Logger
|
||||
import me.rhunk.snapenhance.config.ConfigProperty
|
||||
import me.rhunk.snapenhance.data.ContentType
|
||||
import me.rhunk.snapenhance.data.MessageSender
|
||||
import me.rhunk.snapenhance.data.wrapper.impl.MessageContent
|
||||
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.protobuf.ProtoReader
|
||||
import me.rhunk.snapenhance.util.protobuf.ProtoWriter
|
||||
|
||||
class GalleryMediaSendOverride : Feature("Gallery Media Send Override", loadParams = FeatureLoadParams.INIT_SYNC) {
|
||||
private val redSnapProto: ByteArray by lazy {
|
||||
ProtoWriter().apply {
|
||||
write(11, 5) {
|
||||
write(1) {
|
||||
write(1) {
|
||||
writeConstant(2, 0)
|
||||
writeConstant(12, 0)
|
||||
writeConstant(15, 0)
|
||||
}
|
||||
writeConstant(6, 0)
|
||||
}
|
||||
write(2) {
|
||||
writeConstant(5, 0)
|
||||
writeBuffer(6, byteArrayOf())
|
||||
}
|
||||
}
|
||||
}.toByteArray()
|
||||
}
|
||||
|
||||
private val audioNoteProto: (Int) -> ByteArray = { duration ->
|
||||
ProtoWriter().apply {
|
||||
write(6, 1) {
|
||||
write(1) {
|
||||
writeConstant(2, 4)
|
||||
write(5) {
|
||||
writeConstant(1, 0)
|
||||
writeConstant(2, 0)
|
||||
}
|
||||
writeConstant(7, 0)
|
||||
writeConstant(13, duration)
|
||||
}
|
||||
}
|
||||
}.toByteArray()
|
||||
}
|
||||
|
||||
override fun init() {
|
||||
Hooker.hook(context.classCache.conversationManager, "sendMessageWithContent", HookStage.BEFORE) { param ->
|
||||
@@ -56,15 +21,15 @@ class GalleryMediaSendOverride : Feature("Gallery Media Send Override", loadPara
|
||||
val messageProtoReader = ProtoReader(localMessageContent.content)
|
||||
if (messageProtoReader.exists(7)) return@hook
|
||||
|
||||
when (context.config.state(ConfigProperty.GALLERY_MEDIA_SEND_OVERRIDE)) {
|
||||
"SNAP" -> {
|
||||
when (val overrideType = context.config.state(ConfigProperty.GALLERY_MEDIA_SEND_OVERRIDE)) {
|
||||
"SNAP", "LIVE_SNAP" -> {
|
||||
localMessageContent.contentType = ContentType.SNAP
|
||||
localMessageContent.content = redSnapProto
|
||||
localMessageContent.content = MessageSender.redSnapProto(overrideType == "LIVE_SNAP")
|
||||
}
|
||||
"NOTE" -> {
|
||||
localMessageContent.contentType = ContentType.NOTE
|
||||
val mediaDuration = messageProtoReader.getInt(3, 3, 5, 1, 1, 15) ?: 0
|
||||
localMessageContent.content = audioNoteProto(mediaDuration)
|
||||
localMessageContent.content = MessageSender.audioNoteProto(mediaDuration)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,10 @@ package me.rhunk.snapenhance.features.impl.extras
|
||||
|
||||
import android.app.Notification
|
||||
import android.app.NotificationManager
|
||||
import android.app.PendingIntent
|
||||
import android.app.RemoteInput
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.graphics.Bitmap
|
||||
import android.os.Bundle
|
||||
import android.os.UserHandle
|
||||
@@ -28,10 +31,18 @@ import me.rhunk.snapenhance.util.PreviewUtils
|
||||
import me.rhunk.snapenhance.util.protobuf.ProtoReader
|
||||
|
||||
class Notifications : Feature("Notifications", loadParams = FeatureLoadParams.INIT_SYNC) {
|
||||
companion object{
|
||||
const val ACTION_REPLY = "me.rhunk.snapenhance.action.REPLY"
|
||||
}
|
||||
|
||||
private val notificationDataQueue = mutableMapOf<Long, NotificationData>() // messageId => notification
|
||||
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",
|
||||
@@ -54,8 +65,8 @@ class Notifications : Feature("Notifications", loadParams = FeatureLoadParams.IN
|
||||
context.androidContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||
}
|
||||
|
||||
private fun setNotificationText(notification: NotificationData, text: String) {
|
||||
with(notification.notification.extras) {
|
||||
private fun setNotificationText(notification: Notification, text: String) {
|
||||
with(notification.extras) {
|
||||
putString("android.text", text)
|
||||
putString("android.bigText", text)
|
||||
}
|
||||
@@ -70,6 +81,74 @@ class Notifications : Feature("Notifications", loadParams = FeatureLoadParams.IN
|
||||
return messageBuilder.toString()
|
||||
}
|
||||
|
||||
private fun setupNotificationActionButtons(conversationId: String, notificationData: NotificationData) {
|
||||
val notificationBuilder = XposedHelpers.newInstance(
|
||||
Notification.Builder::class.java,
|
||||
context.androidContext,
|
||||
notificationData.notification
|
||||
) as Notification.Builder
|
||||
|
||||
val chatReplyInput = RemoteInput.Builder("chat_reply_input")
|
||||
.setLabel("Reply")
|
||||
.build()
|
||||
|
||||
val replyIntent = Intent()
|
||||
.setClassName(Constants.SNAPCHAT_PACKAGE_NAME, broadcastReceiverClass.name)
|
||||
.putExtra("conversation_id", conversationId)
|
||||
.putExtra("notification_id", notificationData.id)
|
||||
.setAction(ACTION_REPLY)
|
||||
|
||||
val action = Notification.Action.Builder(
|
||||
null,
|
||||
"Reply",
|
||||
PendingIntent.getBroadcast(
|
||||
context.androidContext,
|
||||
0,
|
||||
replyIntent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE
|
||||
)
|
||||
).addRemoteInput(chatReplyInput).build()
|
||||
|
||||
notificationBuilder.setActions(action)
|
||||
notificationData.notification = notificationBuilder.build()
|
||||
}
|
||||
|
||||
private fun setupBroadcastReceiverHook() {
|
||||
Hooker.hook(broadcastReceiverClass, "onReceive", HookStage.BEFORE) { param ->
|
||||
val androidContext = param.arg<Context>(0)
|
||||
val intent = param.arg<Intent>(1)
|
||||
if (intent.action != ACTION_REPLY) return@hook
|
||||
param.setResult(null)
|
||||
|
||||
val notificationManager = androidContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||
val updateNotification: (Int, (Notification) -> Unit) -> Unit = { notificationId, notificationBuilder ->
|
||||
notificationManager.activeNotifications.firstOrNull { it.id == notificationId }?.let {
|
||||
notificationBuilder(it.notification)
|
||||
XposedBridge.invokeOriginalMethod(notifyAsUserMethod, notificationManager, arrayOf(
|
||||
it.tag, it.id, it.notification, it.user
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
val input = RemoteInput.getResultsFromIntent(intent).getCharSequence("chat_reply_input")
|
||||
.toString()
|
||||
val conversationId = intent.getStringExtra("conversation_id")!!
|
||||
val notificationId = intent.getIntExtra("notification_id", -1)
|
||||
|
||||
context.database.getMyUserId()?.let { context.database.getFriendInfo(it) }?.let { myUser ->
|
||||
cachedMessages.computeIfAbsent(conversationId) { mutableListOf() }.add("${myUser.displayName}: $input")
|
||||
|
||||
updateNotification(notificationId) { notification ->
|
||||
setNotificationText(notification, computeNotificationText(conversationId))
|
||||
}
|
||||
|
||||
context.messageSender.sendChatMessage(listOf(SnapUUID.fromString(conversationId)), input, onError = {
|
||||
context.longToast("Failed to send message: $it")
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun fetchMessagesResult(conversationId: String, messages: List<Message>) {
|
||||
val sendNotificationData = { notificationData: NotificationData, forceCreate: Boolean ->
|
||||
val notificationId = if (forceCreate) System.nanoTime().toInt() else notificationData.id
|
||||
@@ -89,7 +168,7 @@ class Notifications : Feature("Notifications", loadParams = FeatureLoadParams.IN
|
||||
|
||||
val formatUsername: (String) -> String = { "$senderUsername: $it" }
|
||||
val notificationCache = cachedMessages.let { it.computeIfAbsent(conversationId) { mutableListOf() } }
|
||||
val appendNotifications: () -> Unit = { setNotificationText(notificationData, computeNotificationText(conversationId))}
|
||||
val appendNotifications: () -> Unit = { setNotificationText(notificationData.notification, computeNotificationText(conversationId))}
|
||||
|
||||
when (contentType) {
|
||||
ContentType.NOTE -> {
|
||||
@@ -150,6 +229,10 @@ class Notifications : Feature("Notifications", loadParams = FeatureLoadParams.IN
|
||||
}
|
||||
}
|
||||
|
||||
if (contentType == ContentType.CHAT) {
|
||||
setupNotificationActionButtons(conversationId, notificationData)
|
||||
}
|
||||
|
||||
sendNotificationData(notificationData, false)
|
||||
}.clear()
|
||||
}
|
||||
@@ -165,6 +248,8 @@ class Notifications : Feature("Notifications", loadParams = FeatureLoadParams.IN
|
||||
}
|
||||
|
||||
override fun init() {
|
||||
setupBroadcastReceiverHook()
|
||||
|
||||
val fetchConversationWithMessagesCallback = context.mappings.getMappedClass("callbacks", "FetchConversationWithMessagesCallback")
|
||||
|
||||
Hooker.hook(notifyAsUserMethod, HookStage.BEFORE) { param ->
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
package me.rhunk.snapenhance.features.impl.privacy
|
||||
|
||||
import de.robv.android.xposed.XposedHelpers
|
||||
import me.rhunk.snapenhance.Logger.debug
|
||||
import me.rhunk.snapenhance.config.ConfigProperty
|
||||
import me.rhunk.snapenhance.features.Feature
|
||||
import me.rhunk.snapenhance.features.FeatureLoadParams
|
||||
import me.rhunk.snapenhance.hook.HookAdapter
|
||||
import me.rhunk.snapenhance.hook.HookStage
|
||||
import me.rhunk.snapenhance.hook.Hooker
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.util.Base64
|
||||
|
||||
class DisableMetrics : Feature("DisableMetrics", loadParams = FeatureLoadParams.INIT_SYNC) {
|
||||
override fun init() {
|
||||
|
||||
@@ -17,6 +17,7 @@ import me.rhunk.snapenhance.mapping.impl.CallbackMapper
|
||||
import me.rhunk.snapenhance.mapping.impl.EnumMapper
|
||||
import me.rhunk.snapenhance.mapping.impl.GridMediaItemMapper
|
||||
import me.rhunk.snapenhance.mapping.impl.OperaPageViewControllerMapper
|
||||
import me.rhunk.snapenhance.mapping.impl.PlatformAnalyticsCreatorMapper
|
||||
import me.rhunk.snapenhance.mapping.impl.PlusSubscriptionMapper
|
||||
import me.rhunk.snapenhance.util.getObjectField
|
||||
import java.nio.charset.StandardCharsets
|
||||
@@ -31,6 +32,7 @@ class MappingManager(private val context: ModContext) : Manager {
|
||||
add(PlusSubscriptionMapper())
|
||||
add(GridMediaItemMapper())
|
||||
add(BCryptClassMapper())
|
||||
add(PlatformAnalyticsCreatorMapper())
|
||||
}
|
||||
|
||||
private val mappings = ConcurrentHashMap<String, Any>()
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
package me.rhunk.snapenhance.mapping.impl
|
||||
|
||||
import me.rhunk.snapenhance.mapping.Mapper
|
||||
|
||||
class PlatformAnalyticsCreatorMapper : Mapper() {
|
||||
override fun useClasses(
|
||||
classLoader: ClassLoader,
|
||||
classes: List<Class<*>>,
|
||||
mappings: MutableMap<String, Any>
|
||||
) {
|
||||
for (clazz in classes) {
|
||||
if (clazz.isEnum || clazz.isInterface) continue
|
||||
val constructors = clazz.constructors
|
||||
if (constructors.isEmpty()) continue
|
||||
val firstConstructor = constructors[0]
|
||||
// 47 is the number of parameters of the constructor
|
||||
// can change in future versions
|
||||
if (firstConstructor.parameterCount != 47) continue
|
||||
if (!firstConstructor.parameterTypes[0].isEnum) continue
|
||||
if (firstConstructor.parameterTypes[0].enumConstants.none { it.toString() == "IN_APP_NOTIFICATION" }) continue
|
||||
|
||||
mappings["PlatformAnalyticsCreator"] = clazz.name
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -69,7 +69,7 @@ class CallbackBuilder(
|
||||
}
|
||||
|
||||
companion object {
|
||||
private fun createEmptyObject(constructor: Constructor<*>): Any? {
|
||||
fun createEmptyObject(constructor: Constructor<*>): Any? {
|
||||
//compute the args for the constructor with null or default primitive values
|
||||
val args = constructor.parameterTypes.map { type: Class<*> ->
|
||||
if (type.isPrimitive) {
|
||||
|
||||
Reference in New Issue
Block a user