commit 6f3251de5106ba1e1c9bd2e940a2bb996ddf4396 parent 7ff124a9eb41693f91be457c2be55f24972c5dd2 Author: rhunk <101876869+rhunk@users.noreply.github.com> Date: Mon, 29 May 2023 10:56:15 +0200 feat: reply from notifications Diffstat:
13 files changed, 408 insertions(+), 48 deletions(-)
diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ModContext.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ModContext.kt @@ -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 diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/config/ConfigProperty.kt b/app/src/main/kotlin/me/rhunk/snapenhance/config/ConfigProperty.kt @@ -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" ) ), diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/data/MessageSender.kt b/app/src/main/kotlin/me/rhunk/snapenhance/data/MessageSender.kt @@ -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()) + } +}+ \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/data/SnapClassCache.kt b/app/src/main/kotlin/me/rhunk/snapenhance/data/SnapClassCache.kt @@ -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 { diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/data/SnapEnums.kt b/app/src/main/kotlin/me/rhunk/snapenhance/data/SnapEnums.kt @@ -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 } diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/data/wrapper/AbstractWrapper.kt b/app/src/main/kotlin/me/rhunk/snapenhance/data/wrapper/AbstractWrapper.kt @@ -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 diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/data/wrapper/impl/MessageDestinations.kt b/app/src/main/kotlin/me/rhunk/snapenhance/data/wrapper/impl/MessageDestinations.kt @@ -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) +}+ \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/extras/GalleryMediaSendOverride.kt b/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/extras/GalleryMediaSendOverride.kt @@ -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) } } } diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/extras/Notifications.kt b/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/extras/Notifications.kt @@ -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 -> diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/privacy/DisableMetrics.kt b/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/privacy/DisableMetrics.kt @@ -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() { diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/manager/impl/MappingManager.kt b/app/src/main/kotlin/me/rhunk/snapenhance/manager/impl/MappingManager.kt @@ -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>() diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/mapping/impl/PlatformAnalyticsCreatorMapper.kt b/app/src/main/kotlin/me/rhunk/snapenhance/mapping/impl/PlatformAnalyticsCreatorMapper.kt @@ -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 + } + } +}+ \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/util/CallbackBuilder.kt b/app/src/main/kotlin/me/rhunk/snapenhance/util/CallbackBuilder.kt @@ -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) {