commit ed4334c429cc27633c1ad389ab8701eb50ab60b8
parent 1f7f27076687166e5c608101cbbbde64ca2c6e6e
Author: rhunk <101876869+rhunk@users.noreply.github.com>
Date:   Thu, 11 Jan 2024 23:10:09 +0100

refactor: mapper

Diffstat:
Mapp/src/main/kotlin/me/rhunk/snapenhance/RemoteSideContext.kt | 2+-
Mapp/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/home/HomeSection.kt | 2+-
Mcommon/src/main/kotlin/me/rhunk/snapenhance/common/bridge/wrapper/MappingsWrapper.kt | 96+++++++++++++++++++++----------------------------------------------------------
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/SnapEnhance.kt | 4++--
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/action/impl/BulkMessagingAction.kt | 30++++++++++++++++--------------
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/event/EventDispatcher.kt | 52++++++++++++++++++++++++++--------------------------
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/ConfigurationOverride.kt | 214++++++++++++++++++++++++++++++++++++++++---------------------------------------
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/OperaViewerParamsOverride.kt | 42++++++++++++++++++++++--------------------
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/downloader/MediaDownloader.kt | 72++++++++++++++++++++++++++++++++++++------------------------------------
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/experiments/AddFriendSourceSpoof.kt | 88++++++++++++++++++++++++++++++++++++++++----------------------------------------
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/experiments/InfiniteStoryBoost.kt | 16+++++++++-------
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/experiments/MeoPasscodeBypass.kt | 21+++++++++++----------
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/experiments/NoFriendScoreDelay.kt | 12+++++++-----
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/global/BypassVideoLengthRestriction.kt | 17++++++++++-------
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/global/MediaQualityLevelOverride.kt | 20++++++++++----------
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/global/SnapchatPlus.kt | 23++++++++++++-----------
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/messaging/AutoSave.kt | 29++++++++++++++++-------------
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/messaging/Messaging.kt | 67++++++++++++++++++++++++++++++++++++++-----------------------------
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/spying/HalfSwipeNotifier.kt | 6++++--
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/tweaks/CameraTweaks.kt | 25++++++++++++++-----------
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/ui/HideFriendFeedEntry.kt | 40+++++++++++++++++++++-------------------
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/ui/HideQuickAddFriendFeed.kt | 14++++++++------
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/ui/SnapPreview.kt | 19+++++++++++--------
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/messaging/MessageSender.kt | 9++++++++-
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/wrapper/impl/ConversationManager.kt | 28+++++++++++++++++++---------
Mmapper/src/main/kotlin/me/rhunk/snapenhance/mapper/AbstractClassMapper.kt | 97++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
Amapper/src/main/kotlin/me/rhunk/snapenhance/mapper/ClassMapper.kt | 94+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dmapper/src/main/kotlin/me/rhunk/snapenhance/mapper/Mapper.kt | 70----------------------------------------------------------------------
Mmapper/src/main/kotlin/me/rhunk/snapenhance/mapper/MapperContext.kt | 38--------------------------------------
Mmapper/src/main/kotlin/me/rhunk/snapenhance/mapper/impl/BCryptClassMapper.kt | 13+++++++------
Mmapper/src/main/kotlin/me/rhunk/snapenhance/mapper/impl/CallbackMapper.kt | 7++++---
Mmapper/src/main/kotlin/me/rhunk/snapenhance/mapper/impl/CompositeConfigurationProviderMapper.kt | 55+++++++++++++++++++++++++++++++++++--------------------
Mmapper/src/main/kotlin/me/rhunk/snapenhance/mapper/impl/DefaultMediaItemMapper.kt | 13+++++++++----
Mmapper/src/main/kotlin/me/rhunk/snapenhance/mapper/impl/FriendRelationshipChangerMapper.kt | 21+++++++++++++--------
Mmapper/src/main/kotlin/me/rhunk/snapenhance/mapper/impl/FriendingDataSourcesMapper.kt | 13+++++++------
Mmapper/src/main/kotlin/me/rhunk/snapenhance/mapper/impl/FriendsFeedEventDispatcherMapper.kt | 13+++++++------
Mmapper/src/main/kotlin/me/rhunk/snapenhance/mapper/impl/MediaQualityLevelProviderMapper.kt | 12++++++------
Mmapper/src/main/kotlin/me/rhunk/snapenhance/mapper/impl/OperaPageViewControllerMapper.kt | 30+++++++++++++++++-------------
Mmapper/src/main/kotlin/me/rhunk/snapenhance/mapper/impl/OperaViewerParamsMapper.kt | 14++++++++------
Mmapper/src/main/kotlin/me/rhunk/snapenhance/mapper/impl/PlusSubscriptionMapper.kt | 18++++++++++--------
Mmapper/src/main/kotlin/me/rhunk/snapenhance/mapper/impl/ScCameraSettingsMapper.kt | 6++++--
Mmapper/src/main/kotlin/me/rhunk/snapenhance/mapper/impl/ScoreUpdateMapper.kt | 6++++--
Mmapper/src/main/kotlin/me/rhunk/snapenhance/mapper/impl/StoryBoostStateMapper.kt | 6++++--
Mmapper/src/main/kotlin/me/rhunk/snapenhance/mapper/impl/ViewBinderMapper.kt | 18++++++++++--------
Mmapper/src/test/kotlin/me/rhunk/snapenhance/mapper/tests/TestMappings.kt | 29++++++++---------------------
45 files changed, 822 insertions(+), 699 deletions(-)

diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/RemoteSideContext.kt b/app/src/main/kotlin/me/rhunk/snapenhance/RemoteSideContext.kt @@ -195,7 +195,7 @@ class RemoteSideContext( } } - if (mappings.isMappingsOutdated() || !mappings.isMappingsLoaded()) { + if (mappings.isMappingsOutdated() || !mappings.isMappingsLoaded) { requirements = requirements or Requirements.MAPPINGS } diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/home/HomeSection.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/home/HomeSection.kt @@ -110,7 +110,7 @@ class HomeSection : Section() { } override fun onResumed() { - if (!context.mappings.isMappingsLoaded()) { + if (!context.mappings.isMappingsLoaded) { context.mappings.init(context.androidContext) } context.coroutineScope.launch { diff --git a/common/src/main/kotlin/me/rhunk/snapenhance/common/bridge/wrapper/MappingsWrapper.kt b/common/src/main/kotlin/me/rhunk/snapenhance/common/bridge/wrapper/MappingsWrapper.kt @@ -1,44 +1,24 @@ package me.rhunk.snapenhance.common.bridge.wrapper import android.content.Context -import com.google.gson.GsonBuilder -import com.google.gson.JsonElement import com.google.gson.JsonParser +import kotlinx.coroutines.runBlocking import me.rhunk.snapenhance.common.BuildConfig import me.rhunk.snapenhance.common.Constants import me.rhunk.snapenhance.common.bridge.FileLoaderWrapper import me.rhunk.snapenhance.common.bridge.types.BridgeFileType -import me.rhunk.snapenhance.mapper.Mapper -import me.rhunk.snapenhance.mapper.impl.* -import java.util.concurrent.ConcurrentHashMap -import kotlin.system.measureTimeMillis +import me.rhunk.snapenhance.common.logger.AbstractLogger +import me.rhunk.snapenhance.mapper.AbstractClassMapper +import me.rhunk.snapenhance.mapper.ClassMapper +import kotlin.reflect.KClass class MappingsWrapper : FileLoaderWrapper(BridgeFileType.MAPPINGS, "{}".toByteArray(Charsets.UTF_8)) { - companion object { - private val gson = GsonBuilder().setPrettyPrinting().create() - private val mappers = arrayOf( - BCryptClassMapper::class, - CallbackMapper::class, - DefaultMediaItemMapper::class, - MediaQualityLevelProviderMapper::class, - OperaPageViewControllerMapper::class, - PlusSubscriptionMapper::class, - ScCameraSettingsMapper::class, - StoryBoostStateMapper::class, - FriendsFeedEventDispatcherMapper::class, - CompositeConfigurationProviderMapper::class, - ScoreUpdateMapper::class, - FriendRelationshipChangerMapper::class, - ViewBinderMapper::class, - FriendingDataSourcesMapper::class, - OperaViewerParamsMapper::class, - ) - } - private lateinit var context: Context - - private val mappings = ConcurrentHashMap<String, Any>() private var mappingUniqueHash: Long = 0 + var isMappingsLoaded = false + private set + + private val mappers = ClassMapper.DEFAULT_MAPPERS.associateBy { it::class } private fun getUniqueBuildId() = (getSnapchatPackageInfo()?.longVersionCode ?: -1) xor BuildConfig.BUILD_HASH.hashCode().toLong() @@ -63,8 +43,7 @@ class MappingsWrapper : FileLoaderWrapper(BridgeFileType.MAPPINGS, "{}".toByteAr }.getOrNull() fun getGeneratedBuildNumber() = mappingUniqueHash - fun isMappingsOutdated() = mappingUniqueHash != getUniqueBuildId() || isMappingsLoaded().not() - fun isMappingsLoaded() = mappings.isNotEmpty() + fun isMappingsOutdated() = mappingUniqueHash != getUniqueBuildId() || isMappingsLoaded.not() private fun loadCached() { if (!isFileExists()) { @@ -74,64 +53,39 @@ class MappingsWrapper : FileLoaderWrapper(BridgeFileType.MAPPINGS, "{}".toByteAr mappingUniqueHash = it["unique_hash"].asLong } - mappingsObject.entrySet().forEach { (key, value): Map.Entry<String, JsonElement> -> - if (value.isJsonArray) { - mappings[key] = gson.fromJson(value, ArrayList::class.java) - return@forEach + mappingsObject.entrySet().forEach { (key, value) -> + mappers.values.firstOrNull { it.mapperName == key }?.let { mapper -> + mapper.readFromJson(value.asJsonObject) + mapper.classLoader = context.classLoader } - if (value.isJsonObject) { - mappings[key] = gson.fromJson(value, ConcurrentHashMap::class.java) - return@forEach - } - mappings[key] = value.asString } + isMappingsLoaded = true } fun refresh() { mappingUniqueHash = getUniqueBuildId() - val mapper = Mapper(*mappers) + val classMapper = ClassMapper(*mappers.values.toTypedArray()) runCatching { - mapper.loadApk(getSnapchatPackageInfo()?.applicationInfo?.sourceDir ?: throw Exception("Failed to get APK")) + classMapper.loadApk(getSnapchatPackageInfo()?.applicationInfo?.sourceDir ?: throw Exception("Failed to get APK")) }.onFailure { throw Exception("Failed to load APK", it) } - measureTimeMillis { - val result = mapper.start().apply { + runBlocking { + val result = classMapper.run().apply { addProperty("unique_hash", mappingUniqueHash) } write(result.toString().toByteArray()) } } - 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 runCatching { - context.classLoader.loadClass(getMappedObject(className) as? String) - }.getOrNull() - } - - fun getMappedClass(key: String, subKey: String): Class<*> { - return context.classLoader.loadClass(getMappedValue(key, subKey)) - } - - fun getMappedValue(key: String, subKey: String): String? { - return getMappedMap(key)?.get(subKey) as? String - } - @Suppress("UNCHECKED_CAST") - fun getMappedMap(key: String): Map<String, *>? { - return getMappedObjectNullable(key) as? Map<String, *> + fun <T : AbstractClassMapper> useMapper(type: KClass<T>, callback: T.() -> Unit) { + mappers[type]?.let { + callback(it as? T ?: return) + } ?: run { + AbstractLogger.directError("Mapper ${type.simpleName} is not registered", Throwable()) + } } } \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/SnapEnhance.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/SnapEnhance.kt @@ -91,7 +91,7 @@ class SnapEnhance { hookMainActivity("onCreate") { val isMainActivityNotNull = appContext.mainActivity != null appContext.mainActivity = this - if (isMainActivityNotNull || !appContext.mappings.isMappingsLoaded()) return@hookMainActivity + if (isMainActivityNotNull || !appContext.mappings.isMappingsLoaded) return@hookMainActivity onActivityCreate() jetpackComposeResourceHook() appContext.actionManager.onNewIntent(intent) @@ -146,7 +146,7 @@ class SnapEnhance { database.init() eventDispatcher.init() //if mappings aren't loaded, we can't initialize features - if (!mappings.isMappingsLoaded()) return + if (!mappings.isMappingsLoaded) return bridgeClient.registerMessagingBridge(messagingBridge) features.init() scriptRuntime.connect(bridgeClient.getScriptingInterface()) diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/action/impl/BulkMessagingAction.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/action/impl/BulkMessagingAction.kt @@ -12,6 +12,7 @@ import me.rhunk.snapenhance.core.features.impl.experiments.AddFriendSourceSpoof import me.rhunk.snapenhance.core.features.impl.messaging.Messaging import me.rhunk.snapenhance.core.messaging.EnumBulkAction import me.rhunk.snapenhance.core.ui.ViewAppearanceHelper +import me.rhunk.snapenhance.mapper.impl.FriendRelationshipChangerMapper class BulkMessagingAction : AbstractAction() { private val translation by lazy { context.translation.getCategory("bulk_messaging_action") } @@ -145,22 +146,23 @@ class BulkMessagingAction : AbstractAction() { } private fun removeFriend(userId: String) { - val friendRelationshipChangerMapping = context.mappings.getMappedMap("FriendRelationshipChanger") ?: throw Exception("Failed to get FriendRelationshipChanger mapping") - val friendRelationshipChangerInstance = context.feature(AddFriendSourceSpoof::class).friendRelationshipChangerInstance!! + context.mappings.useMapper(FriendRelationshipChangerMapper::class) { + val friendRelationshipChangerInstance = context.feature(AddFriendSourceSpoof::class).friendRelationshipChangerInstance!! + val removeFriendMethod = friendRelationshipChangerInstance::class.java.methods.first { + it.name == this.removeFriendMethod.get() + } - val removeFriendMethod = friendRelationshipChangerInstance::class.java.methods.first { - it.name == friendRelationshipChangerMapping["removeFriendMethod"].toString() + val completable = removeFriendMethod.invoke(friendRelationshipChangerInstance, + userId, // userId + removeFriendMethod.parameterTypes[1].enumConstants.first { it.toString() == "DELETED_BY_MY_FRIENDS" }, // source + null, // unknown + null, // unknown + null // InteractionPlacementInfo + )!! + completable::class.java.methods.first { + it.name == "subscribe" && it.parameterTypes.isEmpty() + }.invoke(completable) } - val completable = removeFriendMethod.invoke(friendRelationshipChangerInstance, - userId, // userId - removeFriendMethod.parameterTypes[1].enumConstants.first { it.toString() == "DELETED_BY_MY_FRIENDS" }, // source - null, // unknown - null, // unknown - null // InteractionPlacementInfo - )!! - completable::class.java.methods.first { - it.name == "subscribe" && it.parameterTypes.isEmpty() - }.invoke(completable) } } \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/event/EventDispatcher.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/event/EventDispatcher.kt @@ -18,44 +18,44 @@ import me.rhunk.snapenhance.core.wrapper.impl.Message import me.rhunk.snapenhance.core.wrapper.impl.MessageContent import me.rhunk.snapenhance.core.wrapper.impl.MessageDestinations import me.rhunk.snapenhance.core.wrapper.impl.SnapUUID +import me.rhunk.snapenhance.mapper.impl.ViewBinderMapper import java.nio.ByteBuffer class EventDispatcher( private val context: ModContext ) : Manager { - private fun findClass(name: String) = context.androidContext.classLoader.loadClass(name) - private fun hookViewBinder() { - val cachedHooks = mutableListOf<String>() - val viewBinderMappings = runCatching { context.mappings.getMappedMap("ViewBinder") }.getOrNull() ?: return - - fun cacheHook(clazz: Class<*>, block: Class<*>.() -> Unit) { - if (!cachedHooks.contains(clazz.name)) { - clazz.block() - cachedHooks.add(clazz.name) + context.mappings.useMapper(ViewBinderMapper::class) { + val cachedHooks = mutableListOf<String>() + fun cacheHook(clazz: Class<*>, block: Class<*>.() -> Unit) { + if (!cachedHooks.contains(clazz.name)) { + clazz.block() + cachedHooks.add(clazz.name) + } } - } - findClass(viewBinderMappings["class"].toString()).hookConstructor(HookStage.AFTER) { methodParam -> - cacheHook( - methodParam.thisObject<Any>()::class.java - ) { - hook(viewBinderMappings["bindMethod"].toString(), HookStage.AFTER) bindViewMethod@{ param -> - val instance = param.thisObject<Any>() - val view = instance::class.java.methods.first { - it.name == viewBinderMappings["getViewMethod"].toString() - }.invoke(instance) as? View ?: return@bindViewMethod - - context.event.post( - BindViewEvent( - prevModel = param.arg(0), - nextModel = param.argNullable(1), - view = view + classReference.get()?.hookConstructor(HookStage.AFTER) { methodParam -> + cacheHook( + methodParam.thisObject<Any>()::class.java + ) { + hook(bindMethod.get().toString(), HookStage.AFTER) bindViewMethod@{ param -> + val instance = param.thisObject<Any>() + val view = instance::class.java.methods.firstOrNull { + it.name == getViewMethod.get().toString() + }?.invoke(instance) as? View ?: return@bindViewMethod + + context.event.post( + BindViewEvent( + prevModel = param.arg(0), + nextModel = param.argNullable(1), + view = view + ) ) - ) + } } } } + } diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/ConfigurationOverride.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/ConfigurationOverride.kt @@ -8,6 +8,7 @@ import me.rhunk.snapenhance.core.util.hook.Hooker import me.rhunk.snapenhance.core.util.hook.hook import me.rhunk.snapenhance.core.util.ktx.getObjectField import me.rhunk.snapenhance.core.util.ktx.setObjectField +import me.rhunk.snapenhance.mapper.impl.CompositeConfigurationProviderMapper data class ConfigKeyInfo( val category: String?, @@ -23,128 +24,129 @@ data class ConfigFilter( class ConfigurationOverride : Feature("Configuration Override", loadParams = FeatureLoadParams.INIT_SYNC) { override fun init() { - val compositeConfigurationProviderMappings = context.mappings.getMappedMap("CompositeConfigurationProvider") ?: throw Exception("Failed to get compositeConfigurationProviderMappings") - val enumMappings = compositeConfigurationProviderMappings["enum"] as Map<*, *> - - fun getConfigKeyInfo(key: Any?) = runCatching { - if (key == null) return@runCatching null - val keyClassMethods = key::class.java.methods - val keyName = keyClassMethods.firstOrNull { it.name == "getName" }?.invoke(key)?.toString() ?: key.toString() - val category = keyClassMethods.firstOrNull { it.name == enumMappings["getCategory"].toString() }?.invoke(key)?.toString() ?: return null - val valueHolder = keyClassMethods.firstOrNull { it.name == enumMappings["getValue"].toString() }?.invoke(key) ?: return null - val defaultValue = valueHolder.getObjectField(enumMappings["defaultValueField"].toString()) ?: return null - ConfigKeyInfo(category, keyName, defaultValue) - }.onFailure { - context.log.error("Failed to get config key info", it) - }.getOrNull() - - val propertyOverrides = mutableMapOf<String, ConfigFilter>() - - fun overrideProperty(key: String, filter: (ConfigKeyInfo) -> Boolean, value: (ConfigKeyInfo) -> Any?, isAppExperiment: Boolean = false) { - propertyOverrides[key] = ConfigFilter(filter, value, isAppExperiment) - } - - overrideProperty("STREAK_EXPIRATION_INFO", { context.config.userInterface.streakExpirationInfo.get() }, - { true }) - overrideProperty("TRANSCODING_MAX_QUALITY", { context.config.global.forceUploadSourceQuality.get() }, - { true }, isAppExperiment = true) - - overrideProperty("CAMERA_ME_ENABLE_HEVC_RECORDING", { context.config.camera.hevcRecording.get() }, - { true }) - overrideProperty("MEDIA_RECORDER_MAX_QUALITY_LEVEL", { context.config.camera.forceCameraSourceEncoding.get() }, - { true }) - overrideProperty("REDUCE_MY_PROFILE_UI_COMPLEXITY", { context.config.userInterface.mapFriendNameTags.get() }, - { true }) - overrideProperty("ENABLE_LONG_SNAP_SENDING", { context.config.global.disableSnapSplitting.get() }, - { true }) - - overrideProperty("DF_VOPERA_FOR_STORIES", { context.config.userInterface.verticalStoryViewer.get() }, - { true }, isAppExperiment = true) - overrideProperty("SPOTLIGHT_5TH_TAB_ENABLED", { context.config.userInterface.disableSpotlight.get() }, - { false }) - - overrideProperty("BYPASS_AD_FEATURE_GATE", { context.config.global.blockAds.get() }, - { true }) - arrayOf("CUSTOM_AD_TRACKER_URL", "CUSTOM_AD_INIT_SERVER_URL", "CUSTOM_AD_SERVER_URL", "INIT_PRIMARY_URL", "INIT_SHADOW_URL").forEach { - overrideProperty(it, { context.config.global.blockAds.get() }, { "http://127.0.0.1" }) - } - - findClass(compositeConfigurationProviderMappings["class"].toString()).hook( - compositeConfigurationProviderMappings["getProperty"].toString(), - HookStage.AFTER - ) { param -> - val propertyKey = getConfigKeyInfo(param.argNullable<Any>(0)) ?: return@hook - - propertyOverrides[propertyKey.name]?.let { (filter, value) -> - if (!filter(propertyKey)) return@let - param.setResult(value(propertyKey)) + context.mappings.useMapper(CompositeConfigurationProviderMapper::class) { + fun getConfigKeyInfo(key: Any?) = runCatching { + if (key == null) return@runCatching null + val keyClassMethods = key::class.java.methods + val keyName = keyClassMethods.firstOrNull { it.name == "getName" }?.invoke(key)?.toString() ?: key.toString() + val category = keyClassMethods.firstOrNull { it.name == configEnumMapping["getCategory"]?.get().toString() }?.invoke(key)?.toString() ?: return null + val valueHolder = keyClassMethods.firstOrNull { it.name == configEnumMapping["getValue"]?.get().toString() }?.invoke(key) ?: return null + val defaultValue = valueHolder.getObjectField(configEnumMapping["defaultValueField"]?.get().toString()) ?: return null + ConfigKeyInfo(category, keyName, defaultValue) + }.onFailure { + context.log.error("Failed to get config key info", it) + }.getOrNull() + + val propertyOverrides = mutableMapOf<String, ConfigFilter>() + + fun overrideProperty(key: String, filter: (ConfigKeyInfo) -> Boolean, value: (ConfigKeyInfo) -> Any?, isAppExperiment: Boolean = false) { + propertyOverrides[key] = ConfigFilter(filter, value, isAppExperiment) } - } - findClass(compositeConfigurationProviderMappings["class"].toString()).hook( - compositeConfigurationProviderMappings["observeProperty"].toString(), - HookStage.BEFORE - ) { param -> - val enumData = param.arg<Any>(0) - val key = enumData.toString() - val setValue: (Any?) -> Unit = { value -> - val valueHolder = XposedHelpers.callMethod(enumData, enumMappings["getValue"].toString()) - valueHolder.setObjectField(enumMappings["defaultValueField"].toString(), value) + overrideProperty("STREAK_EXPIRATION_INFO", { context.config.userInterface.streakExpirationInfo.get() }, + { true }) + overrideProperty("TRANSCODING_MAX_QUALITY", { context.config.global.forceUploadSourceQuality.get() }, + { true }, isAppExperiment = true) + + overrideProperty("CAMERA_ME_ENABLE_HEVC_RECORDING", { context.config.camera.hevcRecording.get() }, + { true }) + overrideProperty("MEDIA_RECORDER_MAX_QUALITY_LEVEL", { context.config.camera.forceCameraSourceEncoding.get() }, + { true }) + overrideProperty("REDUCE_MY_PROFILE_UI_COMPLEXITY", { context.config.userInterface.mapFriendNameTags.get() }, + { true }) + overrideProperty("ENABLE_LONG_SNAP_SENDING", { context.config.global.disableSnapSplitting.get() }, + { true }) + + overrideProperty("DF_VOPERA_FOR_STORIES", { context.config.userInterface.verticalStoryViewer.get() }, + { true }, isAppExperiment = true) + overrideProperty("SPOTLIGHT_5TH_TAB_ENABLED", { context.config.userInterface.disableSpotlight.get() }, + { false }) + + overrideProperty("BYPASS_AD_FEATURE_GATE", { context.config.global.blockAds.get() }, + { true }) + arrayOf("CUSTOM_AD_TRACKER_URL", "CUSTOM_AD_INIT_SERVER_URL", "CUSTOM_AD_SERVER_URL", "INIT_PRIMARY_URL", "INIT_SHADOW_URL").forEach { + overrideProperty(it, { context.config.global.blockAds.get() }, { "http://127.0.0.1" }) } - propertyOverrides[key]?.let { (filter, value) -> - val keyInfo = getConfigKeyInfo(enumData) ?: return@let - if (!filter(keyInfo)) return@let - setValue(value(keyInfo)) - } - } + classReference.getAsClass()?.hook( + getProperty.getAsString()!!, + HookStage.AFTER + ) { param -> + val propertyKey = getConfigKeyInfo(param.argNullable<Any>(0)) ?: return@hook - runCatching { - val appExperimentProviderMappings = compositeConfigurationProviderMappings["appExperimentProvider"] as Map<*, *> - val customBooleanPropertyRules = mutableListOf<(ConfigKeyInfo) -> Boolean>() + propertyOverrides[propertyKey.name]?.let { (filter, value) -> + if (!filter(propertyKey)) return@let + param.setResult(value(propertyKey)) + } + } - findClass(appExperimentProviderMappings["GetBooleanAppExperimentClass"].toString()).hook("invoke", HookStage.BEFORE) { param -> - val keyInfo = getConfigKeyInfo(param.arg(1)) ?: return@hook - if (customBooleanPropertyRules.any { it(keyInfo) }) { - param.setResult(true) - return@hook + classReference.get()?.hook( + observeProperty.getAsString()!!, + HookStage.BEFORE + ) { param -> + val enumData = param.arg<Any>(0) + val key = enumData.toString() + val setValue: (Any?) -> Unit = { value -> + val valueHolder = XposedHelpers.callMethod(enumData, configEnumMapping["getValue"]?.getAsString()) + valueHolder.setObjectField(configEnumMapping["defaultValueField"]?.getAsString()!!, value) } - propertyOverrides[keyInfo.name]?.let { (filter, value, isAppExperiment) -> - if (!isAppExperiment || !filter(keyInfo)) return@let - param.setResult(value(keyInfo)) + + propertyOverrides[key]?.let { (filter, value) -> + val keyInfo = getConfigKeyInfo(enumData) ?: return@let + if (!filter(keyInfo)) return@let + setValue(value(keyInfo)) } } - Hooker.ephemeralHookConstructor( - findClass(compositeConfigurationProviderMappings["class"].toString()), - HookStage.AFTER - ) { constructorParam -> - val instance = constructorParam.thisObject<Any>() - val appExperimentProviderInstance = instance::class.java.fields.firstOrNull { - findClass(appExperimentProviderMappings["class"].toString()).isAssignableFrom(it.type) - }?.get(instance) ?: return@ephemeralHookConstructor - - appExperimentProviderInstance::class.java.methods.first { - it.name == appExperimentProviderMappings["hasExperimentMethod"].toString() - }.hook(HookStage.BEFORE) { param -> - val keyInfo = getConfigKeyInfo(param.arg(0)) ?: return@hook - if (customBooleanPropertyRules.any { it(keyInfo) }) { - param.setResult(true) - return@hook + runCatching { + val customBooleanPropertyRules = mutableListOf<(ConfigKeyInfo) -> Boolean>() + + appExperimentProvider["getBooleanAppExperimentClass"]?.getAsClass() + ?.hook("invoke", HookStage.BEFORE) { param -> + val keyInfo = getConfigKeyInfo(param.arg(1)) ?: return@hook + if (customBooleanPropertyRules.any { it(keyInfo) }) { + param.setResult(true) + return@hook + } + propertyOverrides[keyInfo.name]?.let { (filter, value, isAppExperiment) -> + if (!isAppExperiment || !filter(keyInfo)) return@let + param.setResult(value(keyInfo)) + } } - val propertyOverride = propertyOverrides[keyInfo.name] ?: return@hook - if (propertyOverride.isAppExperiment && propertyOverride.filter(keyInfo)) param.setResult(true) + + + Hooker.ephemeralHookConstructor( + classReference.get()!!, + HookStage.AFTER + ) { constructorParam -> + val instance = constructorParam.thisObject<Any>() + val appExperimentProviderInstance = instance::class.java.fields.firstOrNull { + appExperimentProvider["class"]?.getAsClass()?.isAssignableFrom(it.type) == true + }?.get(instance) ?: return@ephemeralHookConstructor + + appExperimentProviderInstance::class.java.methods.first { + it.name == appExperimentProvider["hasExperimentMethod"]?.getAsString().toString() + }.hook(HookStage.BEFORE) { param -> + val keyInfo = getConfigKeyInfo(param.arg(0)) ?: return@hook + if (customBooleanPropertyRules.any { it(keyInfo) }) { + param.setResult(true) + return@hook + } + + val propertyOverride = propertyOverrides[keyInfo.name] ?: return@hook + if (propertyOverride.isAppExperiment && propertyOverride.filter(keyInfo)) param.setResult(true) + } } - } - if (context.config.experimental.hiddenSnapchatPlusFeatures.get()) { - customBooleanPropertyRules.add { key -> - key.category == "PLUS" && key.defaultValue is Boolean && key.name?.endsWith("_GATE") == true + if (context.config.experimental.hiddenSnapchatPlusFeatures.get()) { + customBooleanPropertyRules.add { key -> + key.category == "PLUS" && key.defaultValue is Boolean && key.name?.endsWith("_GATE") == true + } } + }.onFailure { + context.log.error("Failed to hook appExperimentProvider", it) } - }.onFailure { - context.log.error("Failed to hook appExperimentProvider", it) } } } \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/OperaViewerParamsOverride.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/OperaViewerParamsOverride.kt @@ -4,6 +4,7 @@ import me.rhunk.snapenhance.core.features.Feature import me.rhunk.snapenhance.core.features.FeatureLoadParams import me.rhunk.snapenhance.core.util.hook.HookStage import me.rhunk.snapenhance.core.util.hook.hook +import me.rhunk.snapenhance.mapper.impl.OperaViewerParamsMapper class OperaViewerParamsOverride : Feature("OperaViewerParamsOverride", loadParams = FeatureLoadParams.ACTIVITY_CREATE_SYNC) { data class OverrideKey( @@ -17,7 +18,6 @@ class OperaViewerParamsOverride : Feature("OperaViewerParamsOverride", loadParam ) override fun onActivityCreate() { - val operaViewerParamsMappings = context.mappings.getMappedMap("OperaViewerParams") ?: throw Exception("Failed to get operaViewerParamsMappings") val overrideMap = mutableMapOf<String, Override>() fun overrideParam(key: String, filter: (value: Any?) -> Boolean, value: (overrideKey: OverrideKey, value: Any?) -> Any?) { @@ -36,26 +36,28 @@ class OperaViewerParamsOverride : Feature("OperaViewerParamsOverride", loadParam }) } - findClass(operaViewerParamsMappings["class"].toString()).hook(operaViewerParamsMappings["putMethod"].toString(), HookStage.BEFORE) { param -> - val key = param.argNullable<Any>(0)?.let { key -> - val fields = key::class.java.fields - OverrideKey( - name = fields.firstOrNull { - it.type == String::class.java - }?.get(key)?.toString() ?: return@hook, - defaultValue = fields.firstOrNull { - it.type == Object::class.java - }?.get(key) - ) - } ?: return@hook - val value = param.argNullable<Any>(1) ?: return@hook + context.mappings.useMapper(OperaViewerParamsMapper::class) { + classReference.get()?.hook(putMethod.get()!!, HookStage.BEFORE) { param -> + val key = param.argNullable<Any>(0)?.let { key -> + val fields = key::class.java.fields + OverrideKey( + name = fields.firstOrNull { + it.type == String::class.java + }?.get(key)?.toString() ?: return@hook, + defaultValue = fields.firstOrNull { + it.type == Object::class.java + }?.get(key) + ) + } ?: return@hook + val value = param.argNullable<Any>(1) ?: return@hook - overrideMap[key.name]?.let { override -> - if (override.filter(value)) { - runCatching { - param.setArg(1, override.value(key, value)) - }.onFailure { - context.log.error("Failed to override param $key", it) + overrideMap[key.name]?.let { override -> + if (override.filter(value)) { + runCatching { + param.setArg(1, override.value(key, value)) + }.onFailure { + context.log.error("Failed to override param $key", it) + } } } } diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/downloader/MediaDownloader.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/downloader/MediaDownloader.kt @@ -47,6 +47,7 @@ import me.rhunk.snapenhance.core.wrapper.impl.media.dash.SnapPlaylistItem import me.rhunk.snapenhance.core.wrapper.impl.media.opera.Layer import me.rhunk.snapenhance.core.wrapper.impl.media.opera.ParamMap import me.rhunk.snapenhance.core.wrapper.impl.media.toKeyPair +import me.rhunk.snapenhance.mapper.impl.OperaPageViewControllerMapper import java.io.ByteArrayInputStream import java.nio.file.Paths import java.util.UUID @@ -462,51 +463,50 @@ class MediaDownloader : MessagingRuleFeature("MediaDownloader", MessagingRuleTyp } override fun asyncOnActivityCreate() { - val operaViewerControllerClass: Class<*> = context.mappings.getMappedClass("OperaPageViewController", "class") - - val onOperaViewStateCallback: (HookAdapter) -> Unit = onOperaViewStateCallback@{ param -> - - val viewState = (param.thisObject() as Any).getObjectField(context.mappings.getMappedValue("OperaPageViewController", "viewStateField")!!).toString() - if (viewState != "FULLY_DISPLAYED") { - return@onOperaViewStateCallback - } - val operaLayerList = (param.thisObject() as Any).getObjectField(context.mappings.getMappedValue("OperaPageViewController", "layerListField")!!) as ArrayList<*> - val mediaParamMap: ParamMap = operaLayerList.map { Layer(it) }.first().paramMap + context.mappings.useMapper(OperaPageViewControllerMapper::class) { + val onOperaViewStateCallback: (HookAdapter) -> Unit = onOperaViewStateCallback@{ param -> + val viewState = (param.thisObject() as Any).getObjectField(viewStateField.get()!!).toString() + if (viewState != "FULLY_DISPLAYED") { + return@onOperaViewStateCallback + } + val operaLayerList = (param.thisObject() as Any).getObjectField(layerListField.get()!!) as ArrayList<*> + val mediaParamMap: ParamMap = operaLayerList.map { Layer(it) }.first().paramMap - if (!mediaParamMap.containsKey("image_media_info") && !mediaParamMap.containsKey("video_media_info_list")) - return@onOperaViewStateCallback + if (!mediaParamMap.containsKey("image_media_info") && !mediaParamMap.containsKey("video_media_info_list")) + return@onOperaViewStateCallback - val mediaInfoMap = mutableMapOf<SplitMediaAssetType, MediaInfo>() - val isVideo = mediaParamMap.containsKey("video_media_info_list") + val mediaInfoMap = mutableMapOf<SplitMediaAssetType, MediaInfo>() + val isVideo = mediaParamMap.containsKey("video_media_info_list") - mediaInfoMap[SplitMediaAssetType.ORIGINAL] = MediaInfo( - (if (isVideo) mediaParamMap["video_media_info_list"] else mediaParamMap["image_media_info"])!! - ) + mediaInfoMap[SplitMediaAssetType.ORIGINAL] = MediaInfo( + (if (isVideo) mediaParamMap["video_media_info_list"] else mediaParamMap["image_media_info"])!! + ) - if (context.config.downloader.mergeOverlays.get() && mediaParamMap.containsKey("overlay_image_media_info")) { - mediaInfoMap[SplitMediaAssetType.OVERLAY] = - MediaInfo(mediaParamMap["overlay_image_media_info"]!!) - } - lastSeenMapParams = mediaParamMap - lastSeenMediaInfoMap = mediaInfoMap + if (context.config.downloader.mergeOverlays.get() && mediaParamMap.containsKey("overlay_image_media_info")) { + mediaInfoMap[SplitMediaAssetType.OVERLAY] = + MediaInfo(mediaParamMap["overlay_image_media_info"]!!) + } + lastSeenMapParams = mediaParamMap + lastSeenMediaInfoMap = mediaInfoMap - if (!canAutoDownload()) return@onOperaViewStateCallback + if (!canAutoDownload()) return@onOperaViewStateCallback - context.executeAsync { - runCatching { - handleOperaMedia(mediaParamMap, mediaInfoMap, false) - }.onFailure { - context.log.error("Failed to handle opera media", it) - context.longToast(it.message) + context.executeAsync { + runCatching { + handleOperaMedia(mediaParamMap, mediaInfoMap, false) + }.onFailure { + context.log.error("Failed to handle opera media", it) + context.longToast(it.message) + } } } - } - arrayOf("onDisplayStateChange", "onDisplayStateChangeGesture").forEach { methodName -> - operaViewerControllerClass.hook( - context.mappings.getMappedValue("OperaPageViewController", methodName) ?: return@forEach, - HookStage.AFTER, onOperaViewStateCallback - ) + arrayOf(onDisplayStateChange, onDisplayStateChangeGesture).forEach { methodName -> + classReference.get()?.hook( + methodName.get() ?: return@forEach, + HookStage.AFTER, onOperaViewStateCallback + ) + } } } diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/experiments/AddFriendSourceSpoof.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/experiments/AddFriendSourceSpoof.kt @@ -5,61 +5,61 @@ import me.rhunk.snapenhance.core.features.FeatureLoadParams import me.rhunk.snapenhance.core.util.hook.HookStage import me.rhunk.snapenhance.core.util.hook.hook import me.rhunk.snapenhance.core.util.hook.hookConstructor +import me.rhunk.snapenhance.mapper.impl.FriendRelationshipChangerMapper class AddFriendSourceSpoof : Feature("AddFriendSourceSpoof", loadParams = FeatureLoadParams.ACTIVITY_CREATE_SYNC) { var friendRelationshipChangerInstance: Any? = null private set override fun onActivityCreate() { - val friendRelationshipChangerMapping = context.mappings.getMappedMap("FriendRelationshipChanger") ?: throw Exception("Failed to get friendRelationshipChangerMapping") - - findClass(friendRelationshipChangerMapping["class"].toString()).hookConstructor(HookStage.AFTER) { param -> - friendRelationshipChangerInstance = param.thisObject() - } + context.mappings.useMapper(FriendRelationshipChangerMapper::class) { + classReference.get()?.hookConstructor(HookStage.AFTER) { param -> + friendRelationshipChangerInstance = param.thisObject() + } - findClass(friendRelationshipChangerMapping["class"].toString()) - .hook(friendRelationshipChangerMapping["addFriendMethod"].toString(), HookStage.BEFORE) { param -> - val spoofedSource = context.config.experimental.addFriendSourceSpoof.getNullable() ?: return@hook + classReference.get()?.hook(addFriendMethod.get()!!, HookStage.BEFORE) { param -> + val spoofedSource = context.config.experimental.addFriendSourceSpoof.getNullable() ?: return@hook - fun setEnum(index: Int, value: String) { - val enumData = param.arg<Any>(index) - enumData::class.java.enumConstants.first { it.toString() == value }.let { - param.setArg(index, it) + fun setEnum(index: Int, value: String) { + val enumData = param.arg<Any>(index) + enumData::class.java.enumConstants.first { it.toString() == value }.let { + param.setArg(index, it) + } } - } - when (spoofedSource) { - "added_by_quick_add" -> { - setEnum(1, "PROFILE") - setEnum(2, "ADD_FRIENDS_BUTTON_ON_TOP_BAR_ON_FRIENDS_FEED") - setEnum(3, "ADDED_BY_SUGGESTED") - } - "added_by_group_chat" -> { - setEnum(1, "PROFILE") - setEnum(2, "GROUP_PROFILE") - setEnum(3, "ADDED_BY_GROUP_CHAT") - } - "added_by_username" -> { - setEnum(1, "SEARCH") - setEnum(2, "SEARCH") - setEnum(3, "ADDED_BY_USERNAME") - } - "added_by_qr_code" -> { - setEnum(1, "PROFILE") - setEnum(2, "PROFILE") - setEnum(3, "ADDED_BY_QR_CODE") - } - "added_by_mention" -> { - setEnum(1, "CONTEXT_CARDS") - setEnum(2, "CONTEXT_CARD") - setEnum(3, "ADDED_BY_MENTION") - } - "added_by_community" -> { - setEnum(1, "PROFILE") - setEnum(2, "PROFILE") - setEnum(3, "ADDED_BY_COMMUNITY") + when (spoofedSource) { + "added_by_quick_add" -> { + setEnum(1, "PROFILE") + setEnum(2, "ADD_FRIENDS_BUTTON_ON_TOP_BAR_ON_FRIENDS_FEED") + setEnum(3, "ADDED_BY_SUGGESTED") + } + "added_by_group_chat" -> { + setEnum(1, "PROFILE") + setEnum(2, "GROUP_PROFILE") + setEnum(3, "ADDED_BY_GROUP_CHAT") + } + "added_by_username" -> { + setEnum(1, "SEARCH") + setEnum(2, "SEARCH") + setEnum(3, "ADDED_BY_USERNAME") + } + "added_by_qr_code" -> { + setEnum(1, "PROFILE") + setEnum(2, "PROFILE") + setEnum(3, "ADDED_BY_QR_CODE") + } + "added_by_mention" -> { + setEnum(1, "CONTEXT_CARDS") + setEnum(2, "CONTEXT_CARD") + setEnum(3, "ADDED_BY_MENTION") + } + "added_by_community" -> { + setEnum(1, "PROFILE") + setEnum(2, "PROFILE") + setEnum(3, "ADDED_BY_COMMUNITY") + } + else -> return@hook } - else -> return@hook } } } diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/experiments/InfiniteStoryBoost.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/experiments/InfiniteStoryBoost.kt @@ -4,18 +4,20 @@ import me.rhunk.snapenhance.core.features.Feature import me.rhunk.snapenhance.core.features.FeatureLoadParams import me.rhunk.snapenhance.core.util.hook.HookStage import me.rhunk.snapenhance.core.util.hook.hookConstructor +import me.rhunk.snapenhance.mapper.impl.StoryBoostStateMapper class InfiniteStoryBoost : Feature("InfiniteStoryBoost", loadParams = FeatureLoadParams.ACTIVITY_CREATE_ASYNC) { override fun asyncOnActivityCreate() { if (!context.config.experimental.infiniteStoryBoost.get()) return - val storyBoostStateClass = context.mappings.getMappedClass("StoryBoostStateClass") ?: throw Exception("Failed to get storyBoostStateClass") - storyBoostStateClass.hookConstructor(HookStage.BEFORE) { param -> - val startTimeMillis = param.arg<Long>(1) - //reset timestamp if it's more than 24 hours - if (System.currentTimeMillis() - startTimeMillis > 86400000) { - param.setArg(1, 0) - param.setArg(2, 0) + context.mappings.useMapper(StoryBoostStateMapper::class) { + classReference.get()?.hookConstructor(HookStage.BEFORE) { param -> + val startTimeMillis = param.arg<Long>(1) + //reset timestamp if it's more than 24 hours + if (System.currentTimeMillis() - startTimeMillis > 86400000) { + param.setArg(1, 0) + param.setArg(2, 0) + } } } } diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/experiments/MeoPasscodeBypass.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/experiments/MeoPasscodeBypass.kt @@ -3,20 +3,21 @@ package me.rhunk.snapenhance.core.features.impl.experiments import me.rhunk.snapenhance.core.features.Feature import me.rhunk.snapenhance.core.features.FeatureLoadParams import me.rhunk.snapenhance.core.util.hook.HookStage -import me.rhunk.snapenhance.core.util.hook.Hooker +import me.rhunk.snapenhance.core.util.hook.hook +import me.rhunk.snapenhance.mapper.impl.BCryptClassMapper class MeoPasscodeBypass : Feature("Meo Passcode Bypass", loadParams = FeatureLoadParams.ACTIVITY_CREATE_ASYNC) { override fun asyncOnActivityCreate() { - val bcrypt = context.mappings.getMappedMap("BCrypt") ?: throw Exception("Failed to get bcrypt mappings") + if (!context.config.experimental.meoPasscodeBypass.get()) return - Hooker.hook( - context.androidContext.classLoader.loadClass(bcrypt["class"].toString()), - bcrypt["hashMethod"].toString(), - HookStage.BEFORE, - { context.config.experimental.meoPasscodeBypass.get() }, - ) { param -> - //set the hash to the result of the method - param.setResult(param.arg(1)) + context.mappings.useMapper(BCryptClassMapper::class) { + classReference.get()?.hook( + hashMethod.get()!!, + HookStage.BEFORE, + ) { param -> + //set the hash to the result of the method + param.setResult(param.arg(1)) + } } } } \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/experiments/NoFriendScoreDelay.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/experiments/NoFriendScoreDelay.kt @@ -4,17 +4,19 @@ import me.rhunk.snapenhance.core.features.Feature import me.rhunk.snapenhance.core.features.FeatureLoadParams import me.rhunk.snapenhance.core.util.hook.HookStage import me.rhunk.snapenhance.core.util.hook.hookConstructor +import me.rhunk.snapenhance.mapper.impl.ScoreUpdateMapper import java.lang.reflect.Constructor class NoFriendScoreDelay : Feature("NoFriendScoreDelay", loadParams = FeatureLoadParams.ACTIVITY_CREATE_SYNC) { override fun onActivityCreate() { if (!context.config.experimental.noFriendScoreDelay.get()) return - val scoreUpdateClass = context.mappings.getMappedClass("ScoreUpdate") ?: throw Exception("Failed to get scoreUpdateClass") - scoreUpdateClass.hookConstructor(HookStage.BEFORE) { param -> - val constructor = param.method() as Constructor<*> - if (constructor.parameterTypes.size < 3 || constructor.parameterTypes[3] != java.util.Collection::class.java) return@hookConstructor - param.setArg(2, 0L) + context.mappings.useMapper(ScoreUpdateMapper::class) { + classReference.get()?.hookConstructor(HookStage.BEFORE) { param -> + val constructor = param.method() as Constructor<*> + if (constructor.parameterTypes.size < 3 || constructor.parameterTypes[3] != java.util.Collection::class.java) return@hookConstructor + param.setArg(2, 0L) + } } } } \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/global/BypassVideoLengthRestriction.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/global/BypassVideoLengthRestriction.kt @@ -9,6 +9,7 @@ import me.rhunk.snapenhance.core.features.FeatureLoadParams import me.rhunk.snapenhance.core.util.hook.HookStage import me.rhunk.snapenhance.core.util.hook.hookConstructor import me.rhunk.snapenhance.core.util.ktx.setObjectField +import me.rhunk.snapenhance.mapper.impl.DefaultMediaItemMapper import java.io.File class BypassVideoLengthRestriction : @@ -45,21 +46,23 @@ class BypassVideoLengthRestriction : } } - context.mappings.getMappedClass("DefaultMediaItem") - ?.hookConstructor(HookStage.BEFORE) { param -> + context.mappings.useMapper(DefaultMediaItemMapper::class) { + defaultMediaItem.getAsClass()?.hookConstructor(HookStage.BEFORE) { param -> //set the video length argument param.setArg(5, -1L) } + } } //TODO: allow split from any source if (mode == "split") { - val cameraRollId = context.mappings.getMappedMap("CameraRollMediaId") ?: throw Exception("Failed to get cameraRollId mappings") // memories grid - findClass(cameraRollId["class"].toString()).hookConstructor(HookStage.AFTER) { param -> - //set the durationMs field - param.thisObject<Any>() - .setObjectField(cameraRollId["durationMsField"].toString(), -1L) + context.mappings.useMapper(DefaultMediaItemMapper::class) { + cameraRollMediaId.getAsClass()?.hookConstructor(HookStage.AFTER) { param -> + //set the durationMs field + param.thisObject<Any>() + .setObjectField(durationMsField.get()!!, -1L) + } } // chat camera roll grid diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/global/MediaQualityLevelOverride.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/global/MediaQualityLevelOverride.kt @@ -4,20 +4,20 @@ import me.rhunk.snapenhance.core.features.Feature import me.rhunk.snapenhance.core.features.FeatureLoadParams import me.rhunk.snapenhance.core.util.hook.HookStage import me.rhunk.snapenhance.core.util.hook.hook +import me.rhunk.snapenhance.mapper.impl.MediaQualityLevelProviderMapper +import java.lang.reflect.Method class MediaQualityLevelOverride : Feature("MediaQualityLevelOverride", loadParams = FeatureLoadParams.INIT_SYNC) { override fun init() { - val enumQualityLevel = context.mappings.getMappedClass("EnumQualityLevel") ?: throw Exception("Failed to get enumQualityLevelMappings") - val mediaQualityLevelProvider = context.mappings.getMappedMap("MediaQualityLevelProvider") ?: throw Exception("Failed to get mediaQualityLevelProviderMappings") + if (!context.config.global.forceUploadSourceQuality.get()) return - val forceMediaSourceQuality by context.config.global.forceUploadSourceQuality - - context.androidContext.classLoader.loadClass(mediaQualityLevelProvider["class"].toString()).hook( - mediaQualityLevelProvider["method"].toString(), - HookStage.BEFORE, - { forceMediaSourceQuality } - ) { param -> - param.setResult(enumQualityLevel.enumConstants.firstOrNull { it.toString() == "LEVEL_MAX" } ) + context.mappings.useMapper(MediaQualityLevelProviderMapper::class) { + mediaQualityLevelProvider.getAsClass()?.hook( + mediaQualityLevelProviderMethod.getAsString()!!, + HookStage.BEFORE + ) { param -> + param.setResult((param.method() as Method).returnType.enumConstants.firstOrNull { it.toString() == "LEVEL_MAX" } ) + } } } } \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/global/SnapchatPlus.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/global/SnapchatPlus.kt @@ -3,8 +3,9 @@ package me.rhunk.snapenhance.core.features.impl.global import me.rhunk.snapenhance.core.features.Feature import me.rhunk.snapenhance.core.features.FeatureLoadParams import me.rhunk.snapenhance.core.util.hook.HookStage -import me.rhunk.snapenhance.core.util.hook.Hooker import me.rhunk.snapenhance.core.util.hook.hook +import me.rhunk.snapenhance.core.util.hook.hookConstructor +import me.rhunk.snapenhance.mapper.impl.PlusSubscriptionMapper class SnapchatPlus: Feature("SnapchatPlus", loadParams = FeatureLoadParams.INIT_SYNC) { private val originalSubscriptionTime = (System.currentTimeMillis() - 7776000000L) @@ -13,17 +14,17 @@ class SnapchatPlus: Feature("SnapchatPlus", loadParams = FeatureLoadParams.INIT_ override fun init() { if (!context.config.global.snapchatPlus.get()) return - val subscriptionInfoClass = context.mappings.getMappedClass("SubscriptionInfoClass") ?: throw Exception("Failed to get subscriptionInfoClass") + context.mappings.useMapper(PlusSubscriptionMapper::class) { + classReference.get()?.hookConstructor(HookStage.BEFORE) { param -> + if (param.arg<Int>(0) == 2) return@hookConstructor + //subscription tier + param.setArg(0, 2) + //subscription status + param.setArg(1, 2) - Hooker.hookConstructor(subscriptionInfoClass, HookStage.BEFORE) { param -> - if (param.arg<Int>(0) == 2) return@hookConstructor - //subscription tier - param.setArg(0, 2) - //subscription status - param.setArg(1, 2) - - param.setArg(2, originalSubscriptionTime) - param.setArg(3, expirationTimeMillis) + param.setArg(2, originalSubscriptionTime) + param.setArg(3, expirationTimeMillis) + } } // optional as ConfigurationOverride does this too diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/messaging/AutoSave.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/messaging/AutoSave.kt @@ -13,6 +13,7 @@ import me.rhunk.snapenhance.core.util.hook.hook import me.rhunk.snapenhance.core.util.ktx.getObjectField import me.rhunk.snapenhance.core.wrapper.impl.Message import me.rhunk.snapenhance.core.wrapper.impl.SnapUUID +import me.rhunk.snapenhance.mapper.impl.CallbackMapper import java.util.concurrent.Executors class AutoSave : MessagingRuleFeature("Auto Save", MessagingRuleType.AUTO_SAVE, loadParams = FeatureLoadParams.ACTIVITY_CREATE_ASYNC) { @@ -70,19 +71,21 @@ class AutoSave : MessagingRuleFeature("Auto Save", MessagingRuleType.AUTO_SAVE, override fun asyncOnActivityCreate() { // called when enter in a conversation - context.mappings.getMappedClass("callbacks", "FetchConversationWithMessagesCallback").hook( - "onFetchConversationWithMessagesComplete", - HookStage.BEFORE, - { autoSaveFilter.isNotEmpty() } - ) { param -> - val conversationId = SnapUUID(param.arg<Any>(0).getObjectField("mConversationId")!!) - if (!canSaveInConversation(conversationId.toString())) return@hook - - val messages = param.arg<List<Any>>(1).map { Message(it) } - messages.forEach { - if (!canSaveMessage(it)) return@forEach - asyncSaveExecutorService.submit { - saveMessage(conversationId.toString(), it) + context.mappings.useMapper(CallbackMapper::class) { + callbacks.getClass("FetchConversationWithMessagesCallback")?.hook( + "onFetchConversationWithMessagesComplete", + HookStage.BEFORE, + { autoSaveFilter.isNotEmpty() } + ) { param -> + val conversationId = SnapUUID(param.arg<Any>(0).getObjectField("mConversationId")!!) + if (!canSaveInConversation(conversationId.toString())) return@hook + + val messages = param.arg<List<Any>>(1).map { Message(it) } + messages.forEach { + if (!canSaveMessage(it)) return@forEach + asyncSaveExecutorService.submit { + saveMessage(conversationId.toString(), it) + } } } } diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/messaging/Messaging.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/messaging/Messaging.kt @@ -18,6 +18,8 @@ import me.rhunk.snapenhance.core.wrapper.impl.Message import me.rhunk.snapenhance.core.wrapper.impl.SnapUUID import me.rhunk.snapenhance.core.wrapper.impl.Snapchatter import me.rhunk.snapenhance.core.wrapper.impl.toSnapUUID +import me.rhunk.snapenhance.mapper.impl.CallbackMapper +import me.rhunk.snapenhance.mapper.impl.FriendsFeedEventDispatcherMapper import java.util.concurrent.Future class Messaging : Feature("Messaging", loadParams = FeatureLoadParams.ACTIVITY_CREATE_SYNC or FeatureLoadParams.INIT_ASYNC or FeatureLoadParams.INIT_SYNC) { @@ -47,24 +49,28 @@ class Messaging : Feature("Messaging", loadParams = FeatureLoadParams.ACTIVITY_C } } - context.mappings.getMappedClass("callbacks", "ConversationManagerDelegate").apply { - hookConstructor(HookStage.AFTER) { param -> - conversationManagerDelegate = param.thisObject() - } - hook("onConversationUpdated", HookStage.BEFORE) { param -> - context.event.post(ConversationUpdateEvent( - conversationId = SnapUUID(param.arg(0)).toString(), - conversation = param.argNullable(1), - messages = param.arg<ArrayList<*>>(2).map { Message(it) }, - ).apply { adapter = param }) { - param.setArg(2, messages.map { it.instanceNonNull() }.toCollection(ArrayList())) + context.mappings.useMapper(CallbackMapper::class) { + callbacks.getClass("ConversationManagerDelegate")?.apply { + hookConstructor(HookStage.AFTER) { param -> + conversationManagerDelegate = param.thisObject() + } + hook("onConversationUpdated", HookStage.BEFORE) { param -> + context.event.post(ConversationUpdateEvent( + conversationId = SnapUUID(param.arg(0)).toString(), + conversation = param.argNullable(1), + messages = param.arg<ArrayList<*>>(2).map { Message(it) }, + ).apply { adapter = param }) { + param.setArg( + 2, + messages.map { it.instanceNonNull() }.toCollection(ArrayList()) + ) + } } } - } - - context.mappings.getMappedClass("callbacks", "IdentityDelegate").apply { - hookConstructor(HookStage.AFTER) { - identityDelegate = it.thisObject() + callbacks.getClass("IdentityDelegate")?.apply { + hookConstructor(HookStage.AFTER) { + identityDelegate = it.thisObject() + } } } } @@ -96,10 +102,10 @@ class Messaging : Feature("Messaging", loadParams = FeatureLoadParams.ACTIVITY_C } override fun onActivityCreate() { - context.mappings.getMappedObjectNullable("FriendsFeedEventDispatcher").let { it as? Map<*, *> }?.let { mappings -> - findClass(mappings["class"].toString()).hook("onItemLongPress", HookStage.BEFORE) { param -> + context.mappings.useMapper(FriendsFeedEventDispatcherMapper::class) { + classReference.getAsClass()?.hook("onItemLongPress", HookStage.BEFORE) { param -> val viewItemContainer = param.arg<Any>(0) - val viewItem = viewItemContainer.getObjectField(mappings["viewModelField"].toString()).toString() + val viewItem = viewItemContainer.getObjectField(viewModelField.get()!!).toString() val conversationId = viewItem.substringAfter("conversationId: ").substring(0, 36).also { if (it.startsWith("null")) return@hook } @@ -109,7 +115,6 @@ class Messaging : Feature("Messaging", loadParams = FeatureLoadParams.ACTIVITY_C } } - context.classCache.feedEntry.hookConstructor(HookStage.AFTER) { param -> val instance = param.thisObject<Any>() val interactionInfo = instance.getObjectFieldOrNull("mInteractionInfo") ?: return@hookConstructor @@ -122,17 +127,21 @@ class Messaging : Feature("Messaging", loadParams = FeatureLoadParams.ACTIVITY_C }.sortedBy { it.orderKey }.mapNotNull { it.messageDescriptor?.messageId } } - context.mappings.getMappedClass("callbacks", "GetOneOnOneConversationIdsCallback").hook("onSuccess", HookStage.BEFORE) { param -> - val userIdToConversation = (param.arg<ArrayList<*>>(0)) - .takeIf { it.isNotEmpty() } - ?.get(0) ?: return@hook + context.mappings.useMapper(CallbackMapper::class) { + callbacks.getClass("GetOneOnOneConversationIdsCallback")?.hook("onSuccess", HookStage.BEFORE) { param -> + val userIdToConversation = (param.arg<ArrayList<*>>(0)) + .takeIf { it.isNotEmpty() } + ?.get(0) ?: return@hook - lastFetchConversationUUID = SnapUUID(userIdToConversation.getObjectField("mConversationId")) - lastFetchConversationUserUUID = SnapUUID(userIdToConversation.getObjectField("mUserId")) + lastFetchConversationUUID = + SnapUUID(userIdToConversation.getObjectField("mConversationId")) + lastFetchConversationUserUUID = + SnapUUID(userIdToConversation.getObjectField("mUserId")) + } } - with(context.classCache.conversationManager) { - Hooker.hook(this, "enterConversation", HookStage.BEFORE) { param -> + context.classCache.conversationManager.apply { + hook("enterConversation", HookStage.BEFORE) { param -> openedConversationUUID = SnapUUID(param.arg(0)) if (context.config.messaging.bypassMessageRetentionPolicy.get()) { val callback = param.argNullable<Any>(2) ?: return@hook @@ -141,7 +150,7 @@ class Messaging : Feature("Messaging", loadParams = FeatureLoadParams.ACTIVITY_C } } - Hooker.hook(this, "exitConversation", HookStage.BEFORE) { + hook("exitConversation", HookStage.BEFORE) { openedConversationUUID = null } } diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/spying/HalfSwipeNotifier.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/spying/HalfSwipeNotifier.kt @@ -12,6 +12,7 @@ import me.rhunk.snapenhance.core.util.hook.hook import me.rhunk.snapenhance.core.util.hook.hookConstructor import me.rhunk.snapenhance.core.util.ktx.getIdentifier import me.rhunk.snapenhance.core.util.ktx.getObjectField +import me.rhunk.snapenhance.mapper.impl.CallbackMapper import java.util.concurrent.ConcurrentHashMap import kotlin.time.Duration.Companion.milliseconds @@ -43,8 +44,8 @@ class HalfSwipeNotifier : Feature("Half Swipe Notifier", loadParams = FeatureLoa presenceService = it.thisObject() } - context.mappings.getMappedClass("callbacks", "PresenceServiceDelegate") - .hook("notifyActiveConversationsChanged", HookStage.BEFORE) { + context.mappings.useMapper(CallbackMapper::class) { + callbacks.getClass("PresenceServiceDelegate")?.hook("notifyActiveConversationsChanged", HookStage.BEFORE) { val activeConversations = presenceService::class.java.methods.find { it.name == "getActiveConversations" }?.invoke(presenceService) as? Map<*, *> ?: return@hook // conversationId, conversationInfo (this.mPeekingParticipants) if (activeConversations.isEmpty()) { @@ -75,6 +76,7 @@ class HalfSwipeNotifier : Feature("Half Swipe Notifier", loadParams = FeatureLoa } peekingConversations[conversationId.toString()] = peekingParticipantsIds } + } } } diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/tweaks/CameraTweaks.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/tweaks/CameraTweaks.kt @@ -17,6 +17,7 @@ import me.rhunk.snapenhance.core.util.hook.hook import me.rhunk.snapenhance.core.util.hook.hookConstructor import me.rhunk.snapenhance.core.util.ktx.setObjectField import me.rhunk.snapenhance.core.wrapper.impl.ScSize +import me.rhunk.snapenhance.mapper.impl.ScCameraSettingsMapper import java.io.ByteArrayOutputStream import java.nio.ByteBuffer @@ -62,19 +63,21 @@ class CameraTweaks : Feature("Camera Tweaks", loadParams = FeatureLoadParams.ACT } } - context.mappings.getMappedClass("ScCameraSettings")?.hookConstructor(HookStage.BEFORE) { param -> - val previewResolution = ScSize(param.argNullable(2)) - val captureResolution = ScSize(param.argNullable(3)) + context.mappings.useMapper(ScCameraSettingsMapper::class) { + classReference.get()?.hookConstructor(HookStage.BEFORE) { param -> + val previewResolution = ScSize(param.argNullable(2)) + val captureResolution = ScSize(param.argNullable(3)) - if (previewResolution.isPresent() && captureResolution.isPresent()) { - previewResolutionConfig?.let { - previewResolution.first = it[0] - previewResolution.second = it[1] - } + if (previewResolution.isPresent() && captureResolution.isPresent()) { + previewResolutionConfig?.let { + previewResolution.first = it[0] + previewResolution.second = it[1] + } - captureResolutionConfig?.let { - captureResolution.first = it[0] - captureResolution.second = it[1] + captureResolutionConfig?.let { + captureResolution.first = it[0] + captureResolution.second = it[1] + } } } } diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/ui/HideFriendFeedEntry.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/ui/HideFriendFeedEntry.kt @@ -9,6 +9,7 @@ import me.rhunk.snapenhance.core.util.hook.hook import me.rhunk.snapenhance.core.util.ktx.getObjectField import me.rhunk.snapenhance.core.util.ktx.setObjectField import me.rhunk.snapenhance.core.wrapper.impl.SnapUUID +import me.rhunk.snapenhance.mapper.impl.CallbackMapper class HideFriendFeedEntry : MessagingRuleFeature("HideFriendFeedEntry", ruleType = MessagingRuleType.HIDE_FRIEND_FEED, loadParams = FeatureLoadParams.INIT_SYNC) { private fun createDeletedFeedEntry(conversationId: String) = context.gson.fromJson( @@ -41,30 +42,31 @@ class HideFriendFeedEntry : MessagingRuleFeature("HideFriendFeedEntry", ruleType override fun init() { if (!context.config.userInterface.hideFriendFeedEntry.get()) return - arrayOf( - "QueryFeedCallback" to "onQueryFeedComplete", - "FeedManagerDelegate" to "onFeedEntriesUpdated", - "FeedManagerDelegate" to "onInternalSyncFeed", - "SyncFeedCallback" to "onSyncFeedComplete", - ).forEach { (callbackName, methodName) -> - context.mappings.getMappedClass("callbacks", callbackName) - .hook(methodName, HookStage.BEFORE) { param -> + context.mappings.useMapper(CallbackMapper::class) { + arrayOf( + "QueryFeedCallback" to "onQueryFeedComplete", + "FeedManagerDelegate" to "onFeedEntriesUpdated", + "FeedManagerDelegate" to "onInternalSyncFeed", + "SyncFeedCallback" to "onSyncFeedComplete", + ).forEach { (callbackName, methodName) -> + findClass(callbacks.get()!![callbackName] ?: return@forEach).hook(methodName, HookStage.BEFORE) { param -> filterFriendFeed(param.arg(0)) } - } + } - context.mappings.getMappedClass("callbacks", "FetchAndSyncFeedCallback") - .hook("onFetchAndSyncFeedComplete", HookStage.BEFORE) { param -> - val deletedConversations: ArrayList<Any> = param.arg(2) - filterFriendFeed(param.arg(0), deletedConversations) + callbacks.getClass("FetchAndSyncFeedCallback") + ?.hook("onFetchAndSyncFeedComplete", HookStage.BEFORE) { param -> + val deletedConversations: ArrayList<Any> = param.arg(2) + filterFriendFeed(param.arg(0), deletedConversations) - if (deletedConversations.any { - val uuid = SnapUUID(it.getObjectField("mFeedEntryIdentifier")?.getObjectField("mConversationId")).toString() - context.database.getFeedEntryByConversationId(uuid) != null - }) { - param.setArg(4, true) + if (deletedConversations.any { + val uuid = SnapUUID(it.getObjectField("mFeedEntryIdentifier")?.getObjectField("mConversationId")).toString() + context.database.getFeedEntryByConversationId(uuid) != null + }) { + param.setArg(4, true) + } } - } + } } override fun getRuleState() = RuleState.WHITELIST diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/ui/HideQuickAddFriendFeed.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/ui/HideQuickAddFriendFeed.kt @@ -5,17 +5,19 @@ import me.rhunk.snapenhance.core.features.FeatureLoadParams import me.rhunk.snapenhance.core.util.hook.HookStage import me.rhunk.snapenhance.core.util.hook.hookConstructor import me.rhunk.snapenhance.core.util.ktx.setObjectField +import me.rhunk.snapenhance.mapper.impl.FriendingDataSourcesMapper class HideQuickAddFriendFeed : Feature("HideQuickAddFriendFeed", loadParams = FeatureLoadParams.ACTIVITY_CREATE_SYNC) { override fun onActivityCreate() { if (!context.config.userInterface.hideQuickAddFriendFeed.get()) return - val friendingDataSource = context.mappings.getMappedMap("FriendingDataSources") ?: throw Exception("Failed to get friendingDataSourceMappings") - findClass(friendingDataSource["class"].toString()).hookConstructor(HookStage.AFTER) { param -> - param.thisObject<Any>().setObjectField( - friendingDataSource["quickAddSourceListField"].toString(), - arrayListOf<Any>() - ) + context.mappings.useMapper(FriendingDataSourcesMapper::class) { + classReference.getAsClass()?.hookConstructor(HookStage.AFTER) { param -> + param.thisObject<Any>().setObjectField( + quickAddSourceListField.get()!!, + arrayListOf<Any>() + ) + } } } } \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/ui/SnapPreview.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/ui/SnapPreview.kt @@ -19,6 +19,7 @@ import me.rhunk.snapenhance.core.util.hook.hook import me.rhunk.snapenhance.core.util.ktx.getDimens import me.rhunk.snapenhance.core.util.ktx.getObjectField import me.rhunk.snapenhance.core.util.media.PreviewUtils +import me.rhunk.snapenhance.mapper.impl.CallbackMapper import java.io.File class SnapPreview : Feature("SnapPreview", loadParams = FeatureLoadParams.INIT_SYNC or FeatureLoadParams.ACTIVITY_CREATE_SYNC) { @@ -29,17 +30,19 @@ class SnapPreview : Feature("SnapPreview", loadParams = FeatureLoadParams.INIT_S override fun init() { if (!isEnabled) return - context.mappings.getMappedClass("callbacks", "ContentCallback").hook("handleContentResult", HookStage.BEFORE) { param -> - val contentResult = param.arg<Any>(0) - val classMethods = contentResult::class.java.methods + context.mappings.useMapper(CallbackMapper::class) { + callbacks.getClass("ContentCallback")?.hook("handleContentResult", HookStage.BEFORE) { param -> + val contentResult = param.arg<Any>(0) + val classMethods = contentResult::class.java.methods - val contentKey = classMethods.find { it.name == "getContentKey" }?.invoke(contentResult) ?: return@hook - if (contentKey.getObjectField("mMediaContextType").toString() != "CHAT") return@hook + val contentKey = classMethods.find { it.name == "getContentKey" }?.invoke(contentResult) ?: return@hook + if (contentKey.getObjectField("mMediaContextType").toString() != "CHAT") return@hook - val filePath = classMethods.find { it.name == "getFilePath" }?.invoke(contentResult) ?: return@hook - val mediaId = contentKey.getObjectField("mMediaId").toString() + val filePath = classMethods.find { it.name == "getFilePath" }?.invoke(contentResult) ?: return@hook + val mediaId = contentKey.getObjectField("mMediaId").toString() - mediaFileCache[mediaId.substringAfter("-")] = File(filePath.toString()) + mediaFileCache[mediaId.substringAfter("-")] = File(filePath.toString()) + } } } diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/messaging/MessageSender.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/messaging/MessageSender.kt @@ -8,6 +8,7 @@ import me.rhunk.snapenhance.core.util.CallbackBuilder import me.rhunk.snapenhance.core.wrapper.AbstractWrapper import me.rhunk.snapenhance.core.wrapper.impl.MessageDestinations import me.rhunk.snapenhance.core.wrapper.impl.SnapUUID +import me.rhunk.snapenhance.mapper.impl.CallbackMapper class MessageSender( private val context: ModContext, @@ -55,7 +56,13 @@ class MessageSender( } - private val sendMessageCallback by lazy { context.mappings.getMappedClass("callbacks", "SendMessageCallback") } + private val sendMessageCallback by lazy { + lateinit var result: Class<*> + context.mappings.useMapper(CallbackMapper::class) { + result = callbacks.getClass("SendMessageCallback") ?: return@useMapper + } + result + } private fun createLocalMessageContentTemplate( contentType: ContentType, diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/wrapper/impl/ConversationManager.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/wrapper/impl/ConversationManager.kt @@ -6,6 +6,7 @@ import me.rhunk.snapenhance.core.util.CallbackBuilder import me.rhunk.snapenhance.core.util.ktx.getObjectField import me.rhunk.snapenhance.core.util.ktx.setObjectField import me.rhunk.snapenhance.core.wrapper.AbstractWrapper +import me.rhunk.snapenhance.mapper.impl.CallbackMapper typealias CallbackResult = (error: String?) -> Unit @@ -26,20 +27,29 @@ class ConversationManager( private val getOneOnOneConversationIds by lazy { findMethodByName("getOneOnOneConversationIds") } + private fun getCallbackClass(name: String): Class<*> { + lateinit var result: Class<*> + context.mappings.useMapper(CallbackMapper::class) { + result = context.androidContext.classLoader.loadClass(callbacks.get()!![name]) + } + return result + } + + fun updateMessage(conversationId: String, messageId: Long, action: MessageUpdate, onResult: CallbackResult = {}) { updateMessageMethod.invoke( instanceNonNull(), SnapUUID.fromString(conversationId).instanceNonNull(), messageId, context.classCache.messageUpdateEnum.enumConstants.first { it.toString() == action.toString() }, - CallbackBuilder(context.mappings.getMappedClass("callbacks", "Callback")) + CallbackBuilder(getCallbackClass("Callback")) .override("onSuccess") { onResult(null) } .override("onError") { onResult(it.arg<Any>(0).toString()) }.build() ) } fun fetchConversationWithMessagesPaginated(conversationId: String, lastMessageId: Long, amount: Int, onSuccess: (message: List<Message>) -> Unit, onError: (error: String) -> Unit) { - val callback = CallbackBuilder(context.mappings.getMappedClass("callbacks", "FetchConversationWithMessagesCallback")) + val callback = CallbackBuilder(getCallbackClass("FetchConversationWithMessagesCallback")) .override("onFetchConversationWithMessagesComplete") { param -> onSuccess(param.arg<List<*>>(1).map { Message(it) }) } @@ -54,7 +64,7 @@ class ConversationManager( fetchConversationWithMessagesMethod.invoke( instanceNonNull(), conversationId.toSnapUUID().instanceNonNull(), - CallbackBuilder(context.mappings.getMappedClass("callbacks", "FetchConversationWithMessagesCallback")) + CallbackBuilder(getCallbackClass("FetchConversationWithMessagesCallback")) .override("onFetchConversationWithMessagesComplete") { param -> onSuccess(param.arg<List<*>>(1).map { Message(it) }) } @@ -70,7 +80,7 @@ class ConversationManager( instanceNonNull(), conversationId.toSnapUUID().instanceNonNull(), messageId, - CallbackBuilder(context.mappings.getMappedClass("callbacks", "Callback")) + CallbackBuilder(getCallbackClass("Callback")) .override("onSuccess") { onResult(null) } .override("onError") { onResult(it.arg<Any>(0).toString()) }.build() ) @@ -81,7 +91,7 @@ class ConversationManager( instanceNonNull(), conversationId.toSnapUUID().instanceNonNull(), messageId, - CallbackBuilder(context.mappings.getMappedClass("callbacks", "FetchMessageCallback")) + CallbackBuilder(getCallbackClass("FetchMessageCallback")) .override("onFetchMessageComplete") { param -> onSuccess(Message(param.arg(0))) } @@ -100,7 +110,7 @@ class ConversationManager( fetchMessageByServerId.invoke( instanceNonNull(), serverMessageIdentifier, - CallbackBuilder(context.mappings.getMappedClass("callbacks", "FetchMessageCallback")) + CallbackBuilder(getCallbackClass("FetchMessageCallback")) .override("onFetchMessageComplete") { param -> onSuccess(Message(param.arg(0))) } @@ -119,7 +129,7 @@ class ConversationManager( setObjectField("mServerMessageId", it) } }, - CallbackBuilder(context.mappings.getMappedClass("callbacks", "FetchMessagesByServerIdsCallback")) + CallbackBuilder(getCallbackClass("FetchMessagesByServerIdsCallback")) .override("onSuccess") { param -> onSuccess(param.arg<List<*>>(0).mapNotNull { Message(it?.getObjectField("mMessage") ?: return@mapNotNull null) @@ -132,14 +142,14 @@ class ConversationManager( } fun clearConversation(conversationId: String, onSuccess: () -> Unit, onError: (error: String) -> Unit) { - val callback = CallbackBuilder(context.mappings.getMappedClass("callbacks", "Callback")) + val callback = CallbackBuilder(getCallbackClass("Callback")) .override("onSuccess") { onSuccess() } .override("onError") { onError(it.arg<Any>(0).toString()) }.build() clearConversation.invoke(instanceNonNull(), conversationId.toSnapUUID().instanceNonNull(), callback) } fun getOneOnOneConversationIds(userIds: List<String>, onSuccess: (List<Pair<String, String>>) -> Unit, onError: (error: String) -> Unit) { - val callback = CallbackBuilder(context.mappings.getMappedClass("callbacks", "GetOneOnOneConversationIdsCallback")) + val callback = CallbackBuilder(getCallbackClass("GetOneOnOneConversationIdsCallback")) .override("onSuccess") { param -> onSuccess(param.arg<ArrayList<*>>(0).map { SnapUUID(it.getObjectField("mUserId")).toString() to SnapUUID(it.getObjectField("mConversationId")).toString() diff --git a/mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/AbstractClassMapper.kt b/mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/AbstractClassMapper.kt @@ -1,8 +1,103 @@ package me.rhunk.snapenhance.mapper -abstract class AbstractClassMapper { +import android.util.Log +import com.google.gson.Gson +import com.google.gson.JsonObject + +abstract class AbstractClassMapper( + val mapperName: String +) { + lateinit var classLoader: ClassLoader + + private val gson = Gson() + private val values = mutableMapOf<String, Any?>() private val mappers = mutableListOf<MapperContext.() -> Unit>() + private fun findClassSafe(className: String?) = runCatching { + classLoader.loadClass(className) + }.onFailure { + Log.e("Mapper", it.stackTraceToString()) + }.getOrNull() + + @Suppress("UNCHECKED_CAST") + inner class PropertyDelegate<T>( + private val key: String, + defaultValue: Any? = null, + private val setter: (Any?) -> Unit = { values[key] = it }, + private val getter: (Any?) -> T? = { it as? T } + ) { + init { + values[key] = defaultValue + } + + operator fun getValue(thisRef: Any?, property: Any?): T? { + return getter(values[key]) + } + + operator fun setValue(thisRef: Any?, property: Any?, value: Any?) { + setter(value) + } + + fun set(value: String?) { + values[key] = value + } + + fun get(): T? { + return getter(values[key]) + } + + fun getAsClass(): Class<*>? { + return getter(values[key]) as? Class<*> + } + + fun getAsString(): String? { + return getter(values[key])?.toString() + } + + fun getClass(key: String): Class<*>? { + return (get() as? Map<String, String?>)?.let { + findClassSafe(it[key].toString()) + } + } + + override fun toString() = getter(values[key]).toString() + } + + fun string(key: String): PropertyDelegate<String> = PropertyDelegate(key, null) + + fun classReference(key: String): PropertyDelegate<Class<*>> = PropertyDelegate(key, getter = { findClassSafe(it as? String) }) + + fun map(key: String, value: MutableMap<String, String?> = mutableMapOf()): PropertyDelegate<MutableMap<String, String?>> = PropertyDelegate(key, value) + + fun readFromJson(json: JsonObject) { + values.forEach { (key, _) -> + runCatching { + val jsonElement = json.get(key) ?: return@forEach + when (jsonElement) { + is JsonObject -> values[key] = gson.fromJson(jsonElement, HashMap::class.java) + else -> values[key] = jsonElement.asString + } + }.onFailure { + Log.e("Mapper","Failed to deserialize property $key") + } + } + } + + fun writeFromJson(json: JsonObject) { + values.forEach { (key, value) -> + runCatching { + when (value) { + is String -> json.addProperty(key, value) + is Class<*> -> json.addProperty(key, value.name) + is Map<*, *> -> json.add(key, gson.toJsonTree(value)) + else -> json.addProperty(key, value.toString()) + } + }.onFailure { + Log.e("Mapper","Failed to serialize property $key") + } + } + } + fun mapper(task: MapperContext.() -> Unit) { mappers.add(task) } diff --git a/mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/ClassMapper.kt b/mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/ClassMapper.kt @@ -0,0 +1,93 @@ +package me.rhunk.snapenhance.mapper + +import com.google.gson.JsonObject +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import me.rhunk.snapenhance.mapper.impl.* +import org.jf.dexlib2.Opcodes +import org.jf.dexlib2.dexbacked.DexBackedDexFile +import org.jf.dexlib2.iface.ClassDef +import java.io.BufferedInputStream +import java.io.InputStream +import java.util.zip.ZipFile +import java.util.zip.ZipInputStream + +class ClassMapper( + private vararg val mappers: AbstractClassMapper = DEFAULT_MAPPERS, +) { + private val classes = mutableListOf<ClassDef>() + + companion object { + val DEFAULT_MAPPERS get() = arrayOf( + BCryptClassMapper(), + CallbackMapper(), + DefaultMediaItemMapper(), + MediaQualityLevelProviderMapper(), + OperaPageViewControllerMapper(), + PlusSubscriptionMapper(), + ScCameraSettingsMapper(), + StoryBoostStateMapper(), + FriendsFeedEventDispatcherMapper(), + CompositeConfigurationProviderMapper(), + ScoreUpdateMapper(), + FriendRelationshipChangerMapper(), + ViewBinderMapper(), + FriendingDataSourcesMapper(), + OperaViewerParamsMapper(), + ) + } + + + fun loadApk(path: String) { + val apkFile = ZipFile(path) + val apkEntries = apkFile.entries().toList() + + fun readClass(stream: InputStream) = runCatching { + classes.addAll( + DexBackedDexFile.fromInputStream(Opcodes.getDefault(), BufferedInputStream(stream)).classes + ) + }.onFailure { + throw Throwable("Failed to load dex file", it) + } + + fun filterDexClasses(name: String) = name.startsWith("classes") && name.endsWith(".dex") + + apkEntries.firstOrNull { it.name.endsWith("lspatch/origin.apk") }?.let { origin -> + val originApk = ZipInputStream(apkFile.getInputStream(origin)) + var nextEntry = originApk.nextEntry + while (nextEntry != null) { + if (filterDexClasses(nextEntry.name)) { + readClass(originApk) + } + originApk.closeEntry() + nextEntry = originApk.nextEntry + } + return + } + + apkEntries.toList().filter { filterDexClasses(it.name) }.forEach { + readClass(apkFile.getInputStream(it)) + } + } + + suspend fun run(): JsonObject { + val context = MapperContext(classes.associateBy { it.type }) + + withContext(Dispatchers.IO) { + mappers.forEach { mapper -> + launch { + mapper.run(context) + } + } + } + + val outputJson = JsonObject() + mappers.forEach { mapper -> + outputJson.add(mapper.mapperName, JsonObject().apply { + mapper.writeFromJson(this) + }) + } + return outputJson + } +}+ \ No newline at end of file diff --git a/mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/Mapper.kt b/mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/Mapper.kt @@ -1,69 +0,0 @@ -package me.rhunk.snapenhance.mapper - -import com.google.gson.JsonObject -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import kotlinx.coroutines.runBlocking -import kotlinx.coroutines.withContext -import org.jf.dexlib2.Opcodes -import org.jf.dexlib2.dexbacked.DexBackedDexFile -import org.jf.dexlib2.iface.ClassDef -import java.io.BufferedInputStream -import java.io.InputStream -import java.util.zip.ZipFile -import java.util.zip.ZipInputStream -import kotlin.reflect.KClass - -class Mapper( - private vararg val mappers: KClass<out AbstractClassMapper> = arrayOf() -) { - private val classes = mutableListOf<ClassDef>() - fun loadApk(path: String) { - val apkFile = ZipFile(path) - val apkEntries = apkFile.entries().toList() - - fun readClass(stream: InputStream) = runCatching { - classes.addAll( - DexBackedDexFile.fromInputStream(Opcodes.getDefault(), BufferedInputStream(stream)).classes - ) - }.onFailure { - throw Throwable("Failed to load dex file", it) - } - - fun filterDexClasses(name: String) = name.startsWith("classes") && name.endsWith(".dex") - - apkEntries.firstOrNull { it.name.endsWith("lspatch/origin.apk") }?.let { origin -> - val originApk = ZipInputStream(apkFile.getInputStream(origin)) - var nextEntry = originApk.nextEntry - while (nextEntry != null) { - if (filterDexClasses(nextEntry.name)) { - readClass(originApk) - } - originApk.closeEntry() - nextEntry = originApk.nextEntry - } - return - } - - apkEntries.toList().filter { filterDexClasses(it.name) }.forEach { - readClass(apkFile.getInputStream(it)) - } - } - - fun start(): JsonObject { - val mappers = mappers.map { it.java.constructors.first().newInstance() as AbstractClassMapper } - val context = MapperContext(classes.associateBy { it.type }) - - runBlocking { - withContext(Dispatchers.IO) { - mappers.forEach { mapper -> - launch { - mapper.run(context) - } - } - } - } - - return context.exportToJson() - } -}- \ No newline at end of file diff --git a/mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/MapperContext.kt b/mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/MapperContext.kt @@ -1,7 +1,5 @@ package me.rhunk.snapenhance.mapper -import com.google.gson.GsonBuilder -import com.google.gson.JsonObject import org.jf.dexlib2.iface.ClassDef class MapperContext( @@ -19,40 +17,4 @@ class MapperContext( if (name == null) return null return classMap[name.toString()] } - - private val mappings = mutableMapOf<String, Any?>() - - fun addMapping(key: String, vararg array: Pair<String, Any?>) { - mappings[key] = array.toMap() - } - - fun addMapping(key: String, value: String) { - mappings[key] = value - } - - fun getStringMapping(key: String): String? { - return mappings[key] as? String - } - - fun getMapMapping(key: String): Map<*, *>? { - return mappings[key] as? Map<*, *> - } - - fun exportToJson(): JsonObject { - val gson = GsonBuilder().setPrettyPrinting().create() - val json = JsonObject() - for ((key, value) in mappings) { - when (value) { - is String -> json.addProperty(key, value) - is Map<*, *> -> { - val obj = JsonObject() - for ((k, v) in value) { - obj.add(k.toString(), gson.toJsonTree(v)) - } - json.add(key, obj) - } - } - } - return json - } } \ No newline at end of file diff --git a/mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/impl/BCryptClassMapper.kt b/mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/impl/BCryptClassMapper.kt @@ -6,7 +6,10 @@ import me.rhunk.snapenhance.mapper.ext.getStaticConstructor import me.rhunk.snapenhance.mapper.ext.isFinal import org.jf.dexlib2.iface.instruction.formats.ArrayPayload -class BCryptClassMapper : AbstractClassMapper() { +class BCryptClassMapper : AbstractClassMapper("BCryptClass") { + val classReference = classReference("class") + val hashMethod = string("hashMethod") + init { mapper { for (clazz in classes) { @@ -17,17 +20,15 @@ class BCryptClassMapper : AbstractClassMapper() { } if (isBcryptClass == true) { - val hashMethod = clazz.methods.first { + val hashDexMethod = clazz.methods.first { it.parameterTypes.size == 2 && it.parameterTypes[0] == "Ljava/lang/String;" && it.parameterTypes[1] == "Ljava/lang/String;" && it.returnType == "Ljava/lang/String;" } - addMapping("BCrypt", - "class" to clazz.getClassName(), - "hashMethod" to hashMethod.name - ) + hashMethod.set(hashDexMethod.name) + classReference.set(clazz.getClassName()) return@mapper } } diff --git a/mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/impl/CallbackMapper.kt b/mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/impl/CallbackMapper.kt @@ -4,11 +4,12 @@ import me.rhunk.snapenhance.mapper.AbstractClassMapper import me.rhunk.snapenhance.mapper.ext.getClassName import me.rhunk.snapenhance.mapper.ext.getSuperClassName import me.rhunk.snapenhance.mapper.ext.isFinal -import org.jf.dexlib2.Opcode import org.jf.dexlib2.iface.instruction.formats.Instruction21t import org.jf.dexlib2.iface.instruction.formats.Instruction22t -class CallbackMapper : AbstractClassMapper() { +class CallbackMapper : AbstractClassMapper("Callbacks") { + val callbacks = map("callbacks") + init { mapper { val callbackClasses = classes.filter { clazz -> @@ -32,7 +33,7 @@ class CallbackMapper : AbstractClassMapper() { it.getSuperClassName()!!.substringAfterLast("/") to it.getClassName() } - addMapping("callbacks", *callbackClasses.toTypedArray()) + callbacks.get()?.putAll(callbackClasses) } } } \ No newline at end of file diff --git a/mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/impl/CompositeConfigurationProviderMapper.kt b/mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/impl/CompositeConfigurationProviderMapper.kt @@ -1,14 +1,32 @@ package me.rhunk.snapenhance.mapper.impl import me.rhunk.snapenhance.mapper.AbstractClassMapper -import me.rhunk.snapenhance.mapper.ext.* +import me.rhunk.snapenhance.mapper.ext.findConstString +import me.rhunk.snapenhance.mapper.ext.getClassName +import me.rhunk.snapenhance.mapper.ext.hasStaticConstructorString +import me.rhunk.snapenhance.mapper.ext.isEnum import org.jf.dexlib2.iface.instruction.formats.Instruction21c import org.jf.dexlib2.iface.instruction.formats.Instruction35c import org.jf.dexlib2.iface.reference.FieldReference import org.jf.dexlib2.iface.reference.MethodReference import java.lang.reflect.Modifier -class CompositeConfigurationProviderMapper : AbstractClassMapper() { +class CompositeConfigurationProviderMapper : AbstractClassMapper("CompositeConfigurationProvider") { + val classReference = classReference("class") + val observeProperty = string("observeProperty") + val getProperty = string("getProperty") + val configEnumMapping = mapOf( + "class" to classReference("enumClass"), + "getValue" to string("enumGetValue"), + "getCategory" to string("enumGetCategory"), + "defaultValueField" to string("enumDefaultValueField") + ) + val appExperimentProvider = mapOf( + "class" to classReference("appExperimentProviderClass"), + "getBooleanAppExperimentClass" to classReference("getBooleanAppExperimentClass"), + "hasExperimentMethod" to string("hasExperimentMethod") + ) + init { mapper { for (classDef in classes) { @@ -68,24 +86,21 @@ class CompositeConfigurationProviderMapper : AbstractClassMapper() { it.type == "Ljava/lang/Object;" } - addMapping("CompositeConfigurationProvider", - "class" to classDef.getClassName(), - "observeProperty" to observePropertyMethod.name, - "getProperty" to getPropertyMethod.name, - "enum" to mapOf( - "class" to configEnumInterface.getClassName(), - "getValue" to enumGetDefaultValueMethod.name, - "getCategory" to enumGetCategoryMethod.name, - "defaultValueField" to defaultValueField.name - ), - "appExperimentProvider" to (hasExperimentMethodReference?.let { - mapOf( - "class" to getClass(it.definingClass)?.getClassName(), - "GetBooleanAppExperimentClass" to getBooleanAppExperimentClass, - "hasExperimentMethod" to hasExperimentMethodReference.name - ) - }) - ) + classReference.set(classDef.getClassName()) + observeProperty.set(observePropertyMethod.name) + getProperty.set(getPropertyMethod.name) + + configEnumMapping["class"]?.set(configEnumInterface.getClassName()) + configEnumMapping["getValue"]?.set(enumGetDefaultValueMethod.name) + configEnumMapping["getCategory"]?.set(enumGetCategoryMethod.name) + configEnumMapping["defaultValueField"]?.set(defaultValueField.name) + + hasExperimentMethodReference?.let { + appExperimentProvider["class"]?.set(getClass(it.definingClass)?.getClassName()) + appExperimentProvider["getBooleanAppExperimentClass"]?.set(getBooleanAppExperimentClass) + appExperimentProvider["hasExperimentMethod"]?.set(hasExperimentMethodReference.name) + } + return@mapper } } diff --git a/mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/impl/DefaultMediaItemMapper.kt b/mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/impl/DefaultMediaItemMapper.kt @@ -5,16 +5,21 @@ import me.rhunk.snapenhance.mapper.ext.findConstString import me.rhunk.snapenhance.mapper.ext.getClassName import me.rhunk.snapenhance.mapper.ext.isAbstract -class DefaultMediaItemMapper : AbstractClassMapper() { +class DefaultMediaItemMapper : AbstractClassMapper("DefaultMediaItem") { + val cameraRollMediaId = classReference("cameraRollMediaIdClass") + val durationMsField = string("durationMsField") + val defaultMediaItem = classReference("defaultMediaItemClass") + init { mapper { for (clazz in classes) { if (clazz.methods.find { it.name == "toString" }?.implementation?.findConstString("CameraRollMediaId", contains = true) != true) { continue } - val durationMsField = clazz.fields.firstOrNull { it.type == "J" } ?: continue + val durationMsDexField = clazz.fields.firstOrNull { it.type == "J" } ?: continue - addMapping("CameraRollMediaId", "class" to clazz.getClassName(), "durationMsField" to durationMsField.name) + cameraRollMediaId.set(clazz.getClassName()) + durationMsField.set(durationMsDexField.name) return@mapper } } @@ -29,7 +34,7 @@ class DefaultMediaItemMapper : AbstractClassMapper() { val constructorParameters = clazz.directMethods.firstOrNull { it.name == "<init>" }?.parameterTypes ?: continue if (constructorParameters.size < 6 || constructorParameters[5] != "J") continue - addMapping("DefaultMediaItem", clazz.getClassName()) + defaultMediaItem.set(clazz.getClassName()) return@mapper } } diff --git a/mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/impl/FriendRelationshipChangerMapper.kt b/mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/impl/FriendRelationshipChangerMapper.kt @@ -5,12 +5,16 @@ import me.rhunk.snapenhance.mapper.ext.findConstString import me.rhunk.snapenhance.mapper.ext.getClassName import me.rhunk.snapenhance.mapper.ext.isEnum -class FriendRelationshipChangerMapper : AbstractClassMapper() { +class FriendRelationshipChangerMapper : AbstractClassMapper("FriendRelationshipChanger") { + val classReference = classReference("class") + val addFriendMethod = string("addFriendMethod") + val removeFriendMethod = string("removeFriendMethod") + init { mapper { for (classDef in classes) { classDef.methods.firstOrNull { it.name == "<init>" }?.implementation?.findConstString("FriendRelationshipChangerImpl")?.takeIf { it } ?: continue - val addFriendMethod = classDef.methods.first { + val addFriendDexMethod = classDef.methods.first { it.parameterTypes.size > 4 && getClass(it.parameterTypes[1])?.isEnum() == true && getClass(it.parameterTypes[2])?.isEnum() == true && @@ -18,7 +22,7 @@ class FriendRelationshipChangerMapper : AbstractClassMapper() { it.parameters[4].type == "Ljava/lang/String;" } - val removeFriendMethod = classDef.methods.first { + val removeFriendDexMethod = classDef.methods.firstOrNull { it.parameterTypes.size == 5 && it.parameterTypes[0] == "Ljava/lang/String;" && getClass(it.parameterTypes[1])?.isEnum() == true && @@ -26,11 +30,12 @@ class FriendRelationshipChangerMapper : AbstractClassMapper() { it.parameterTypes[3] == "Ljava/lang/String;" } - addMapping("FriendRelationshipChanger", - "class" to classDef.getClassName(), - "addFriendMethod" to addFriendMethod.name, - "removeFriendMethod" to removeFriendMethod.name - ) + this@FriendRelationshipChangerMapper.apply { + classReference.set(classDef.getClassName()) + addFriendMethod.set(addFriendDexMethod.name) + removeFriendMethod.set(removeFriendDexMethod?.name) + } + return@mapper } } diff --git a/mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/impl/FriendingDataSourcesMapper.kt b/mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/impl/FriendingDataSourcesMapper.kt @@ -5,7 +5,10 @@ import me.rhunk.snapenhance.mapper.ext.findConstString import me.rhunk.snapenhance.mapper.ext.getClassName import me.rhunk.snapenhance.mapper.ext.searchNextFieldReference -class FriendingDataSourcesMapper: AbstractClassMapper() { +class FriendingDataSourcesMapper: AbstractClassMapper("FriendingDataSources") { + val classReference = classReference("class") + val quickAddSourceListField = string("quickAddSourceListField") + init { mapper { for (classDef in classes) { @@ -15,13 +18,11 @@ class FriendingDataSourcesMapper: AbstractClassMapper() { val toStringMethod = classDef.methods.firstOrNull { it.name == "toString" } ?: continue if (toStringMethod.implementation?.findConstString("quickaddSource", contains = true) != true) continue - val quickAddSourceListField = toStringMethod.implementation?.searchNextFieldReference("quickaddSource", contains = true) + val quickAddSourceListDexField = toStringMethod.implementation?.searchNextFieldReference("quickaddSource", contains = true) ?: continue - addMapping("FriendingDataSources", - "class" to classDef.getClassName(), - "quickAddSourceListField" to quickAddSourceListField.name - ) + classReference.set(classDef.getClassName()) + quickAddSourceListField.set(quickAddSourceListDexField.name) return@mapper } } diff --git a/mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/impl/FriendsFeedEventDispatcherMapper.kt b/mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/impl/FriendsFeedEventDispatcherMapper.kt @@ -5,7 +5,10 @@ import me.rhunk.snapenhance.mapper.ext.findConstString import me.rhunk.snapenhance.mapper.ext.getClassName -class FriendsFeedEventDispatcherMapper : AbstractClassMapper() { +class FriendsFeedEventDispatcherMapper : AbstractClassMapper("FriendsFeedEventDispatcher") { + val classReference = classReference("class") + val viewModelField = string("viewModelField") + init { mapper { for (clazz in classes) { @@ -13,15 +16,13 @@ class FriendsFeedEventDispatcherMapper : AbstractClassMapper() { val onItemLongPress = clazz.methods.first { it.name == "onItemLongPress" } val viewHolderContainerClass = getClass(onItemLongPress.parameterTypes[0]) ?: continue - val viewModelField = viewHolderContainerClass.fields.firstOrNull { field -> + val viewModelDexField = viewHolderContainerClass.fields.firstOrNull { field -> val typeClass = getClass(field.type) ?: return@firstOrNull false typeClass.methods.firstOrNull {it.name == "toString"}?.implementation?.findConstString("FriendFeedItemViewModel", contains = true) == true }?.name ?: continue - addMapping("FriendsFeedEventDispatcher", - "class" to clazz.getClassName(), - "viewModelField" to viewModelField - ) + classReference.set(clazz.getClassName()) + viewModelField.set(viewModelDexField) return@mapper } } diff --git a/mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/impl/MediaQualityLevelProviderMapper.kt b/mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/impl/MediaQualityLevelProviderMapper.kt @@ -7,7 +7,10 @@ import me.rhunk.snapenhance.mapper.ext.isAbstract import me.rhunk.snapenhance.mapper.ext.isEnum import org.jf.dexlib2.AccessFlags -class MediaQualityLevelProviderMapper : AbstractClassMapper() { +class MediaQualityLevelProviderMapper : AbstractClassMapper("MediaQualityLevelProvider") { + val mediaQualityLevelProvider = classReference("mediaQualityLevelProvider") + val mediaQualityLevelProviderMethod = string("mediaQualityLevelProviderMethod") + init { var enumQualityLevel : String? = null @@ -20,7 +23,6 @@ class MediaQualityLevelProviderMapper : AbstractClassMapper() { break; } } - addMapping("EnumQualityLevel", enumQualityLevel ?: return@mapper) } mapper { @@ -31,10 +33,8 @@ class MediaQualityLevelProviderMapper : AbstractClassMapper() { if (clazz.fields.none { it.accessFlags and AccessFlags.TRANSIENT.value != 0 }) continue clazz.methods.firstOrNull { it.returnType == "L$enumQualityLevel;" }?.let { - addMapping("MediaQualityLevelProvider", - "class" to clazz.getClassName(), - "method" to it.name - ) + mediaQualityLevelProvider.set(clazz.getClassName()) + mediaQualityLevelProviderMethod.set(it.name) return@mapper } } diff --git a/mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/impl/OperaPageViewControllerMapper.kt b/mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/impl/OperaPageViewControllerMapper.kt @@ -7,7 +7,13 @@ import me.rhunk.snapenhance.mapper.ext.hasStaticConstructorString import me.rhunk.snapenhance.mapper.ext.isAbstract import me.rhunk.snapenhance.mapper.ext.isEnum -class OperaPageViewControllerMapper : AbstractClassMapper() { +class OperaPageViewControllerMapper : AbstractClassMapper("OperaPageViewController") { + val classReference = classReference("class") + val viewStateField = string("viewStateField") + val layerListField = string("layerListField") + val onDisplayStateChange = string("onDisplayStateChange") + val onDisplayStateChangeGesture = string("onDisplayStateChangeGesture") + init { mapper { for (clazz in classes) { @@ -16,37 +22,35 @@ class OperaPageViewControllerMapper : AbstractClassMapper() { continue } - val viewStateField = clazz.fields.first { field -> + val viewStateDexField = clazz.fields.first { field -> val fieldClass = getClass(field.type) ?: return@first false fieldClass.isEnum() && fieldClass.hasStaticConstructorString("FULLY_DISPLAYED") } - val layerListField = clazz.fields.first { it.type == "Ljava/util/ArrayList;" } + val layerListDexField = clazz.fields.first { it.type == "Ljava/util/ArrayList;" } - val onDisplayStateChange = clazz.methods.firstOrNull { + val onDisplayStateChangeDexMethod = clazz.methods.firstOrNull { if (it.returnType != "V" || it.parameterTypes.size != 1) return@firstOrNull false val firstParameterType = getClass(it.parameterTypes[0]) ?: return@firstOrNull false if (firstParameterType.type == clazz.type || !firstParameterType.isAbstract()) return@firstOrNull false //check if the class contains a field with the enumViewStateClass type firstParameterType.fields.any { field -> - field.type == viewStateField.type + field.type == viewStateDexField.type } } - val onDisplayStateChangeGesture = clazz.methods.first { + val onDisplayStateChangeGestureDexMethod = clazz.methods.first { if (it.returnType != "V" || it.parameterTypes.size != 2) return@first false val firstParameterType = getClass(it.parameterTypes[0]) ?: return@first false val secondParameterType = getClass(it.parameterTypes[1]) ?: return@first false firstParameterType.isEnum() && secondParameterType.isEnum() } - addMapping("OperaPageViewController", - "class" to clazz.getClassName(), - "viewStateField" to viewStateField.name, - "layerListField" to layerListField.name, - "onDisplayStateChange" to onDisplayStateChange?.name, - "onDisplayStateChangeGesture" to onDisplayStateChangeGesture.name - ) + classReference.set(clazz.getClassName()) + viewStateField.set(viewStateDexField.name) + layerListField.set(layerListDexField.name) + onDisplayStateChange.set(onDisplayStateChangeDexMethod?.name) + onDisplayStateChangeGesture.set(onDisplayStateChangeGestureDexMethod.name) return@mapper } diff --git a/mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/impl/OperaViewerParamsMapper.kt b/mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/impl/OperaViewerParamsMapper.kt @@ -6,14 +6,17 @@ import me.rhunk.snapenhance.mapper.ext.getClassName import org.jf.dexlib2.iface.instruction.formats.Instruction35c import org.jf.dexlib2.iface.reference.MethodReference -class OperaViewerParamsMapper : AbstractClassMapper() { +class OperaViewerParamsMapper : AbstractClassMapper("OperaViewerParams") { + val classReference = classReference("class") + val putMethod = string("putMethod") + init { mapper { for (classDef in classes) { classDef.fields.firstOrNull { it.type == "Ljava/util/concurrent/ConcurrentHashMap;" } ?: continue if (classDef.methods.firstOrNull { it.name == "toString" }?.implementation?.findConstString("Params") != true) continue - val putMethod = classDef.methods.firstOrNull { method -> + val putDexMethod = classDef.methods.firstOrNull { method -> method.implementation?.instructions?.any { val instruction = it as? Instruction35c ?: return@any false val reference = instruction.reference as? MethodReference ?: return@any false @@ -21,10 +24,9 @@ class OperaViewerParamsMapper : AbstractClassMapper() { } == true } ?: return@mapper - addMapping("OperaViewerParams", - "class" to classDef.getClassName(), - "putMethod" to putMethod.name - ) + classReference.set(classDef.getClassName()) + putMethod.set(putDexMethod.name) + return@mapper } } diff --git a/mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/impl/PlusSubscriptionMapper.kt b/mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/impl/PlusSubscriptionMapper.kt @@ -4,24 +4,26 @@ import me.rhunk.snapenhance.mapper.AbstractClassMapper import me.rhunk.snapenhance.mapper.ext.findConstString import me.rhunk.snapenhance.mapper.ext.getClassName -class PlusSubscriptionMapper : AbstractClassMapper(){ +class PlusSubscriptionMapper : AbstractClassMapper("PlusSubscription"){ + val classReference = classReference("class") + init { mapper { for (clazz in classes) { if (clazz.directMethods.filter { it.name == "<init>" }.none { - it.parameters.size == 4 && - it.parameterTypes[0] == "I" && - it.parameterTypes[1] == "I" && - it.parameterTypes[2] == "J" && - it.parameterTypes[3] == "J" - }) continue + it.parameterTypes.size > 3 && + it.parameterTypes[0] == "I" && + it.parameterTypes[1] == "I" && + it.parameterTypes[2] == "J" && + it.parameterTypes[3] == "J" + }) continue val isPlusSubscriptionInfoClass = clazz.virtualMethods.firstOrNull { it.name == "toString" }?.implementation?.let { it.findConstString("SubscriptionInfo", contains = true) && it.findConstString("expirationTimeMillis", contains = true) } if (isPlusSubscriptionInfoClass == true) { - addMapping("SubscriptionInfoClass", clazz.getClassName()) + classReference.set(clazz.getClassName()) return@mapper } } diff --git a/mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/impl/ScCameraSettingsMapper.kt b/mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/impl/ScCameraSettingsMapper.kt @@ -6,7 +6,9 @@ import me.rhunk.snapenhance.mapper.ext.getClassName import me.rhunk.snapenhance.mapper.ext.getStaticConstructor import me.rhunk.snapenhance.mapper.ext.isEnum -class ScCameraSettingsMapper : AbstractClassMapper() { +class ScCameraSettingsMapper : AbstractClassMapper("ScCameraSettings") { + val classReference = classReference("class") + init { mapper { for (clazz in classes) { @@ -15,7 +17,7 @@ class ScCameraSettingsMapper : AbstractClassMapper() { val firstParameter = getClass(firstConstructor.parameterTypes[0]) ?: continue if (!firstParameter.isEnum() || firstParameter.getStaticConstructor()?.implementation?.findConstString("CONTINUOUS_PICTURE") != true) continue - addMapping("ScCameraSettings", clazz.getClassName()) + classReference.set(clazz.getClassName()) return@mapper } } diff --git a/mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/impl/ScoreUpdateMapper.kt b/mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/impl/ScoreUpdateMapper.kt @@ -4,7 +4,9 @@ import me.rhunk.snapenhance.mapper.AbstractClassMapper import me.rhunk.snapenhance.mapper.ext.findConstString import me.rhunk.snapenhance.mapper.ext.getClassName -class ScoreUpdateMapper : AbstractClassMapper() { +class ScoreUpdateMapper : AbstractClassMapper("ScoreUpdate") { + val classReference = classReference("class") + init { mapper { for (classDef in classes) { @@ -18,7 +20,7 @@ class ScoreUpdateMapper : AbstractClassMapper() { it.name == "toString" }?.implementation?.findConstString("Friend.sq:selectFriendUserScoresNeedToUpdate") != true) continue - addMapping("ScoreUpdate", classDef.getClassName()) + classReference.set(classDef.getClassName()) return@mapper } } diff --git a/mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/impl/StoryBoostStateMapper.kt b/mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/impl/StoryBoostStateMapper.kt @@ -4,7 +4,9 @@ import me.rhunk.snapenhance.mapper.AbstractClassMapper import me.rhunk.snapenhance.mapper.ext.findConstString import me.rhunk.snapenhance.mapper.ext.getClassName -class StoryBoostStateMapper : AbstractClassMapper() { +class StoryBoostStateMapper : AbstractClassMapper("StoryBoostState") { + val classReference = classReference("class") + init { mapper { for (clazz in classes) { @@ -14,7 +16,7 @@ class StoryBoostStateMapper : AbstractClassMapper() { if (clazz.methods.firstOrNull { it.name == "toString" }?.implementation?.findConstString("StoryBoostState", contains = true) != true) continue - addMapping("StoryBoostStateClass", clazz.getClassName()) + classReference.set(clazz.getClassName()) return@mapper } } diff --git a/mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/impl/ViewBinderMapper.kt b/mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/impl/ViewBinderMapper.kt @@ -6,13 +6,17 @@ import me.rhunk.snapenhance.mapper.ext.isAbstract import me.rhunk.snapenhance.mapper.ext.isInterface import java.lang.reflect.Modifier -class ViewBinderMapper : AbstractClassMapper() { +class ViewBinderMapper : AbstractClassMapper("ViewBinder") { + val classReference = classReference("class") + val bindMethod = string("bindMethod") + val getViewMethod = string("getViewMethod") + init { mapper { for (clazz in classes) { if (!clazz.isAbstract() || clazz.isInterface()) continue - val getViewMethod = clazz.methods.firstOrNull { it.returnType == "Landroid/view/View;" && it.parameterTypes.size == 0 } ?: continue + val getViewDexMethod = clazz.methods.firstOrNull { it.returnType == "Landroid/view/View;" && it.parameterTypes.size == 0 } ?: continue // update view clazz.methods.filter { @@ -21,17 +25,15 @@ class ViewBinderMapper : AbstractClassMapper() { if (it.size != 1) return@also }.firstOrNull() ?: continue - val bindMethod = clazz.methods.filter { + val bindDexMethod = clazz.methods.filter { Modifier.isAbstract(it.accessFlags) && it.parameterTypes.size == 2 && it.parameterTypes[0] == it.parameterTypes[1] && it.returnType == "V" }.also { if (it.size != 1) return@also }.firstOrNull() ?: continue - addMapping("ViewBinder", - "class" to clazz.getClassName(), - "bindMethod" to bindMethod.name, - "getViewMethod" to getViewMethod.name - ) + classReference.set(clazz.getClassName()) + bindMethod.set(bindDexMethod.name) + getViewMethod.set(getViewDexMethod.name) return@mapper } } diff --git a/mapper/src/test/kotlin/me/rhunk/snapenhance/mapper/tests/TestMappings.kt b/mapper/src/test/kotlin/me/rhunk/snapenhance/mapper/tests/TestMappings.kt @@ -1,7 +1,8 @@ package me.rhunk.snapenhance.mapper.tests import com.google.gson.GsonBuilder -import me.rhunk.snapenhance.mapper.Mapper +import kotlinx.coroutines.runBlocking +import me.rhunk.snapenhance.mapper.ClassMapper import me.rhunk.snapenhance.mapper.impl.* import org.junit.Test import java.io.File @@ -10,28 +11,14 @@ import java.io.File class TestMappings { @Test fun testMappings() { - val mapper = Mapper( - BCryptClassMapper::class, - CallbackMapper::class, - DefaultMediaItemMapper::class, - MediaQualityLevelProviderMapper::class, - OperaPageViewControllerMapper::class, - PlusSubscriptionMapper::class, - ScCameraSettingsMapper::class, - StoryBoostStateMapper::class, - FriendsFeedEventDispatcherMapper::class, - CompositeConfigurationProviderMapper::class, - ScoreUpdateMapper::class, - FriendRelationshipChangerMapper::class, - ViewBinderMapper::class, - FriendingDataSourcesMapper::class, - OperaViewerParamsMapper::class, - ) + val classMapper = ClassMapper() val gson = GsonBuilder().setPrettyPrinting().create() val apkFile = File(System.getenv("SNAPCHAT_APK")!!) - mapper.loadApk(apkFile.absolutePath) - val result = mapper.start() - println("Mappings: ${gson.toJson(result)}") + classMapper.loadApk(apkFile.absolutePath) + runBlocking { + val result = classMapper.run() + println("Mappings: ${gson.toJson(result)}") + } } }