commit 8e87e7c84d100c55ef05bc475df12b0de01c6233 parent 00b6f25933373c5a4a2d093641763a8caa61ea5f Author: rhunk <101876869+rhunk@users.noreply.github.com> Date: Sun, 6 Aug 2023 17:04:18 +0200 main branch merge Diffstat:
24 files changed, 293 insertions(+), 263 deletions(-)
diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -1,64 +1,55 @@ name: "Bug report" -description: Report an issue to help the project improve. -title: "bug: <title>" -labels: [ - "bug" -] +description: "Report an issue to help the project improve." +title: "bug: TITLE" +labels: + - "bug" body: - type: textarea id: description attributes: label: "Description" - description: Please enter an explicit description of your issue - placeholder: Short and explicit description of your incident... + description: "Please enter an explicit description of your issue" + placeholder: "Short and explicit description of your incident..." validations: required: true - type: textarea id: reprod attributes: label: "Reproduction steps" - description: Steps to reproduce the behavior + description: "Steps to reproduce the behavior" value: | - 1. Go to '...' - 2. Click on '....' - 3. Scroll down to '....' - 4. See error - render: bash + 1. Go to `...` + 2. Click on `....` + 3. Scroll down to `....` + 4. See error. validations: required: true - type: textarea - id: screenshot - attributes: - label: "Screenshots" - description: If applicable, add screenshots to help explain your problem - value: | -  - render: bash - validations: - required: false - - type: textarea id: logs attributes: label: "Logs" - description: Please copy and paste any relevant log output if available - render: bash + description: "Please copy and paste any relevant log output if available" + render: markdown validations: required: false - type: input id: snapchat-version attributes: label: "Snapchat Version" - description: On which Snapchat version is this happening? - placeholder: ex. 12.35.0.45 + description: "On which Snapchat version is this happening?" + placeholder: "ex. 12.35.0.45" validations: required: true - type: checkboxes id: terms attributes: - label: Agreement - description: By creating this issue I made sure that ... + label: "Agreement" + description: "By creating this issue, I agree to the following terms:" options: - - label: I am using the latest stable SnapEnhance version. - required: true - - label: There is no issue already describing my problem. - required: true + - label: "I am using the latest stable SnapEnhance version." + - label: "This is not a bug regarding Snapchat+." + - label: "I have provided a detailed description of the issue." + - label: "I have attached a log if deemed neccessary." + - label: "This issue is not a duplicate." + validations: + required: true diff --git a/.github/ISSUE_TEMPLATE/feature_suggestion.yml b/.github/ISSUE_TEMPLATE/feature_suggestion.yml @@ -1,6 +1,6 @@ name: "Feature suggestion" description: Suggest a new feature to help the project improve. -title: "feat: <title>" +title: "feat: TITLE" labels: [ "enhancement" ] diff --git a/README.md b/README.md @@ -99,3 +99,4 @@ When redistributing the software, it must remain under the same [GPLv3](https:// - [RevealedSoulEven](https://github.com/revealedsouleven) - [iBasim](https://github.com/ibasim) - [xerta555](https://github.com/xerta555) +- [TheVisual](https://github.com/TheVisual) diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/config/impl/Experimental.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/config/impl/Experimental.kt @@ -8,4 +8,5 @@ class Experimental : ConfigContainer() { val infiniteStoryBoost = boolean("infinite_story_boost") val meoPasscodeBypass = boolean("meo_passcode_bypass") val unlimitedMultiSnap = boolean("unlimited_multi_snap") + val noFriendScoreDelay = boolean("no_friend_score_delay") } \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/ConfigEnumKeys.kt b/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/ConfigEnumKeys.kt @@ -1,106 +0,0 @@ -package me.rhunk.snapenhance.features.impl - -import android.annotation.SuppressLint -import me.rhunk.snapenhance.features.Feature -import me.rhunk.snapenhance.features.FeatureLoadParams -import me.rhunk.snapenhance.hook.HookStage -import me.rhunk.snapenhance.hook.hook -import me.rhunk.snapenhance.util.getObjectField -import me.rhunk.snapenhance.util.setObjectField -import java.lang.reflect.Field -import java.lang.reflect.Modifier -import java.lang.reflect.Type - -class ConfigEnumKeys : Feature("Config enum keys", loadParams = FeatureLoadParams.ACTIVITY_CREATE_SYNC) { - - data class HookEnumContext( - val key: String, - val type: Type?, - val value: Any?, - val set: (Any) -> Unit - ) - - companion object { - fun hookAllEnums(enumClass: Class<*>, callback: HookEnumContext.() -> Unit) { - //Enum(String, int, ?) - //or Enum(?) - val enumDataClass = enumClass.constructors[0].parameterTypes.first { clazz: Class<*> -> clazz != String::class.java && !clazz.isPrimitive } - - //get the field which contains the enum data class - val enumDataField = enumClass.declaredFields.first { field: Field -> field.type == enumDataClass } - - val typeField = enumDataClass.declaredFields.first { field: Field -> field.type == Type::class.java } - - //get the field value of the enum data class (the first field of the class with the desc Object) - val objectDataField = enumDataField.type.fields.first { field: Field -> - field.type == Any::class.java && Modifier.isPublic( - field.modifiers - ) && Modifier.isFinal(field.modifiers) - } - - enumClass.enumConstants.forEach { enum -> - enumDataField.get(enum)?.let { enumData -> - val key = enum.toString() - val type = typeField.get(enumData) as Type? - val value = enumData.getObjectField(objectDataField.name) - val set = { newValue: Any -> - enumData.setObjectField(objectDataField.name, newValue) - } - callback(HookEnumContext(key, type, value, set)) - } - } - } - } - - @SuppressLint("PrivateApi") - override fun onActivityCreate() { - if (context.config.userInterface.mapFriendNameTags.get()) { - hookAllEnums(context.mappings.getMappedClass("enums", "PLUS")) { - if (key == "REDUCE_MY_PROFILE_UI_COMPLEXITY") set(true) - } - } - - hookAllEnums(context.mappings.getMappedClass("enums", "ARROYO")) { - if (key == "ENABLE_LONG_SNAP_SENDING") { - if (context.config.global.disableSnapSplitting.get()) set(true) - } - } - - if (context.config.userInterface.streakExpirationInfo.get()) { - hookAllEnums(context.mappings.getMappedClass("enums", "FRIENDS_FEED")) { - if (key == "STREAK_EXPIRATION_INFO") set(true) - } - } - - if (context.config.userInterface.blockAds.get()) { - hookAllEnums(context.mappings.getMappedClass("enums", "SNAPADS")) { - if (key == "BYPASS_AD_FEATURE_GATE") { - set(true) - } - if (key == "CUSTOM_AD_SERVER_URL" || key == "CUSTOM_AD_INIT_SERVER_URL" || key == "CUSTOM_AD_TRACKER_URL") { - set("http://127.0.0.1") - } - } - } - - context.config.userInterface.storyViewerOverride.getNullable()?.let { value -> - hookAllEnums(context.mappings.getMappedClass("enums", "DISCOVER_FEED")) { - if (key == "DF_ENABLE_SHOWS_PAGE_CONTROLS" && value == "DISCOVER_PLAYBACK_SEEKBAR") { - set(true) - } - if (key == "DF_VOPERA_FOR_STORIES" && value == "VERTICAL_STORY_VIEWER") { - set(true) - } - } - } - - val sharedPreferencesImpl = context.androidContext.classLoader.loadClass("android.app.SharedPreferencesImpl") - - sharedPreferencesImpl.methods.first { it.name == "getBoolean" }.hook(HookStage.BEFORE) { param -> - when (param.arg<String>(0)) { - "SIG_APP_APPEARANCE_SETTING" -> if (context.config.userInterface.enableAppAppearance.get()) param.setResult(true) - "SPOTLIGHT_5TH_TAB_ENABLED" -> if (context.config.userInterface.disableSpotlight.get()) param.setResult(false) - } - } - } -}- \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/ConfigurationOverride.kt b/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/ConfigurationOverride.kt @@ -0,0 +1,83 @@ +package me.rhunk.snapenhance.features.impl + +import de.robv.android.xposed.XposedHelpers +import me.rhunk.snapenhance.features.Feature +import me.rhunk.snapenhance.features.FeatureLoadParams +import me.rhunk.snapenhance.hook.HookStage +import me.rhunk.snapenhance.hook.hook +import me.rhunk.snapenhance.util.setObjectField + +class ConfigurationOverride : Feature("Configuration Override", loadParams = FeatureLoadParams.INIT_SYNC) { + override fun init() { + val propertyOverrides = mutableMapOf<String, Pair<(() -> Boolean), Any>>() + + fun overrideProperty(key: String, filter: () -> Boolean, value: Any) { + propertyOverrides[key] = Pair(filter, value) + } + + overrideProperty("STREAK_EXPIRATION_INFO", { context.config.userInterface.streakExpirationInfo.get() }, true) + + overrideProperty("FORCE_CAMERA_HIGHEST_FPS", { context.config.camera.forceHighestFrameRate.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) + + context.config.userInterface.storyViewerOverride.getNullable()?.let { state -> + overrideProperty("DF_ENABLE_SHOWS_PAGE_CONTROLS", { state == "DISCOVER_PLAYBACK_SEEKBAR" }, true) + overrideProperty("DF_VOPERA_FOR_STORIES", { state == "VERTICAL_STORY_VIEWER" }, true) + } + + overrideProperty("SIG_APP_APPEARANCE_SETTING", { context.config.userInterface.enableAppAppearance.get() }, true) + overrideProperty("SPOTLIGHT_5TH_TAB_ENABLED", { context.config.userInterface.disableSpotlight.get() }, false) + + overrideProperty("BYPASS_AD_FEATURE_GATE", { context.config.userInterface.blockAds.get() }, true) + arrayOf("CUSTOM_AD_TRACKER_URL", "CUSTOM_AD_INIT_SERVER_URL", "CUSTOM_AD_SERVER_URL").forEach { + overrideProperty(it, { context.config.userInterface.blockAds.get() }, "http://127.0.0.1") + } + + val compositeConfigurationProviderMappings = context.mappings.getMappedMap("CompositeConfigurationProvider") + val enumMappings = compositeConfigurationProviderMappings["enum"] as Map<*, *> + + 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) + } + + propertyOverrides[key]?.let { (filter, value) -> + if (!filter()) return@let + setValue(value) + } + } + + findClass(compositeConfigurationProviderMappings["class"].toString()).hook( + compositeConfigurationProviderMappings["getProperty"].toString(), + HookStage.AFTER + ) { param -> + val propertyKey = param.arg<Any>(0).toString() + + propertyOverrides[propertyKey]?.let { (filter, value) -> + if (!filter()) return@let + param.setResult(value) + } + } + + arrayOf("getBoolean", "getInt", "getLong", "getFloat", "getString").forEach { methodName -> + findClass("android.app.SharedPreferencesImpl").hook( + methodName, + HookStage.BEFORE + ) { param -> + val key = param.argNullable<Any>(0).toString() + propertyOverrides[key]?.let { (filter, value) -> + if (!filter()) return@let + param.setResult(value) + } + } + } + } +}+ \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/downloader/MediaDownloader.kt b/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/downloader/MediaDownloader.kt @@ -117,7 +117,7 @@ class MediaDownloader : Feature("MediaDownloader", loadParams = FeatureLoadParam val pathFormat by context.config.downloader.pathFormat val sanitizedPathPrefix = pathPrefix .replace(" ", "_") - .replace(Regex("[\\\\:*?\"<>|]"), "") + .replace(Regex("[\\p{Cntrl}]"), "") .ifEmpty { hexHash } val currentDateTime = SimpleDateFormat("yyyy-MM-dd_HH-mm-ss", Locale.ENGLISH).format(System.currentTimeMillis()) @@ -294,7 +294,7 @@ class MediaDownloader : Feature("MediaDownloader", loadParams = FeatureLoadParam if ((snapSource == "PUBLIC_USER" || snapSource == "SAVED_STORY") && (forceDownload || canAutoDownload("public_stories"))) { val userDisplayName = (if (paramMap.containsKey("USER_DISPLAY_NAME")) paramMap["USER_DISPLAY_NAME"].toString() else "").replace( - "[^\\x00-\\x7F]".toRegex(), + "[\\p{Cntrl}]".toRegex(), "") downloadOperaMedia(provideDownloadManagerClient( @@ -321,7 +321,7 @@ class MediaDownloader : Feature("MediaDownloader", loadParams = FeatureLoadParam //TODO: option to download multiple chapters if (paramMap.containsKey("LONGFORM_VIDEO_PLAYLIST_ITEM") && forceDownload) { val storyName = paramMap["STORY_NAME"].toString().replace( - "[^\\x00-\\x7F]".toRegex(), + "[\\p{Cntrl}]".toRegex(), "") //get the position of the media in the playlist and the duration @@ -535,4 +535,4 @@ class MediaDownloader : Feature("MediaDownloader", loadParams = FeatureLoadParam if (messaging.openedConversationUUID == null) return downloadMessageId(messaging.lastFocusedMessageId, isPreviewMode) } -}- \ No newline at end of file +} diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/experiments/NoFriendScoreDelay.kt b/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/experiments/NoFriendScoreDelay.kt @@ -0,0 +1,20 @@ +package me.rhunk.snapenhance.features.impl.experiments + +import me.rhunk.snapenhance.features.Feature +import me.rhunk.snapenhance.features.FeatureLoadParams +import me.rhunk.snapenhance.hook.HookStage +import me.rhunk.snapenhance.hook.hookConstructor +import 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") + + 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) + } + } +}+ \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/tweaks/CameraTweaks.kt b/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/tweaks/CameraTweaks.kt @@ -8,7 +8,6 @@ import android.hardware.camera2.CameraManager import me.rhunk.snapenhance.data.wrapper.impl.ScSize import me.rhunk.snapenhance.features.Feature import me.rhunk.snapenhance.features.FeatureLoadParams -import me.rhunk.snapenhance.features.impl.ConfigEnumKeys import me.rhunk.snapenhance.hook.HookStage import me.rhunk.snapenhance.hook.hook import me.rhunk.snapenhance.hook.hookConstructor @@ -37,15 +36,6 @@ class CameraTweaks : Feature("Camera Tweaks", loadParams = FeatureLoadParams.ACT } } - ConfigEnumKeys.hookAllEnums(context.mappings.getMappedClass("enums", "CAMERA")) { - if (key == "FORCE_CAMERA_HIGHEST_FPS" && context.config.camera.forceHighestFrameRate.get()) { - set(true) - } - if (key == "MEDIA_RECORDER_MAX_QUALITY_LEVEL" && context.config.camera.forceCameraSourceEncoding.get()) { - value!!.javaClass.enumConstants?.let { enumData -> set(enumData.filter { it.toString() == "LEVEL_MAX" }) } - } - } - val previewResolutionConfig = context.config.camera.overridePreviewResolution.getNullable()?.let { parseResolution(it) } val captureResolutionConfig = context.config.camera.overridePictureResolution.getNullable()?.let { parseResolution(it) } diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/hook/Hooker.kt b/core/src/main/kotlin/me/rhunk/snapenhance/hook/Hooker.kt @@ -3,15 +3,13 @@ package me.rhunk.snapenhance.hook import de.robv.android.xposed.XC_MethodHook import de.robv.android.xposed.XposedBridge import java.lang.reflect.Member - -typealias HookFilter = (HookAdapter) -> Boolean -typealias HookConsumer = (HookAdapter) -> Unit +import java.lang.reflect.Method object Hooker { inline fun newMethodHook( stage: HookStage, - crossinline consumer: HookConsumer, - crossinline filter: HookFilter = { true } + crossinline consumer: (HookAdapter) -> Unit, + crossinline filter: ((HookAdapter) -> Boolean) = { true } ): XC_MethodHook { return if (stage == HookStage.BEFORE) object : XC_MethodHook() { override fun beforeHookedMethod(param: MethodHookParam<*>) { @@ -28,48 +26,47 @@ object Hooker { clazz: Class<*>, methodName: String, stage: HookStage, - crossinline consumer: HookConsumer - ): Set<XC_MethodHook.Unhook> = hook(clazz, methodName, stage, { true }, consumer) - - inline fun hook( - clazz: Class<*>, - methodName: String, - stage: HookStage, - crossinline filter: HookFilter, - crossinline consumer: HookConsumer + crossinline filter: (HookAdapter) -> Boolean, + noinline consumer: (HookAdapter) -> Unit ): Set<XC_MethodHook.Unhook> = XposedBridge.hookAllMethods(clazz, methodName, newMethodHook(stage, consumer, filter)) inline fun hook( member: Member, stage: HookStage, - crossinline consumer: HookConsumer + crossinline filter: ((HookAdapter) -> Boolean), + crossinline consumer: (HookAdapter) -> Unit ): XC_MethodHook.Unhook { - return hook(member, stage, { true }, consumer) + return XposedBridge.hookMethod(member, newMethodHook(stage, consumer, filter)) } - inline fun hook( + fun hook( + clazz: Class<*>, + methodName: String, + stage: HookStage, + consumer: (HookAdapter) -> Unit + ): Set<XC_MethodHook.Unhook> = hook(clazz, methodName, stage, { true }, consumer) + + fun hook( member: Member, stage: HookStage, - crossinline filter: HookFilter, - crossinline consumer: HookConsumer + consumer: (HookAdapter) -> Unit ): XC_MethodHook.Unhook { - return XposedBridge.hookMethod(member, newMethodHook(stage, consumer, filter)) + return hook(member, stage, { true }, consumer) } - - inline fun hookConstructor( + fun hookConstructor( clazz: Class<*>, stage: HookStage, - crossinline consumer: HookConsumer + consumer: (HookAdapter) -> Unit ) { XposedBridge.hookAllConstructors(clazz, newMethodHook(stage, consumer)) } - inline fun hookConstructor( + fun hookConstructor( clazz: Class<*>, stage: HookStage, - crossinline filter: HookFilter, - crossinline consumer: HookConsumer + filter: ((HookAdapter) -> Boolean), + consumer: (HookAdapter) -> Unit ) { XposedBridge.hookAllConstructors(clazz, newMethodHook(stage, consumer, filter)) } @@ -79,7 +76,7 @@ object Hooker { instance: Any, methodName: String, stage: HookStage, - crossinline hookConsumer: HookConsumer + crossinline hookConsumer: (HookAdapter) -> Unit ) { val unhooks: MutableSet<XC_MethodHook.Unhook> = HashSet() hook(clazz, methodName, stage) { param-> @@ -95,7 +92,7 @@ object Hooker { clazz: Class<*>, methodName: String, stage: HookStage, - crossinline hookConsumer: HookConsumer + crossinline hookConsumer: (HookAdapter) -> Unit ) { val unhooks: MutableSet<XC_MethodHook.Unhook> = HashSet() hook(clazz, methodName, stage) { param-> @@ -109,7 +106,7 @@ object Hooker { instance: Any, methodName: String, stage: HookStage, - crossinline hookConsumer: HookConsumer + crossinline hookConsumer: (HookAdapter) -> Unit ) { val unhooks: MutableSet<XC_MethodHook.Unhook> = HashSet() hook(clazz, methodName, stage) { param-> @@ -120,37 +117,43 @@ object Hooker { } } -inline fun Class<*>.hookConstructor( +fun Class<*>.hookConstructor( stage: HookStage, - crossinline consumer: HookConsumer + consumer: (HookAdapter) -> Unit ) = Hooker.hookConstructor(this, stage, consumer) -inline fun Class<*>.hookConstructor( +fun Class<*>.hookConstructor( stage: HookStage, - crossinline filter: HookFilter, - crossinline consumer: HookConsumer + filter: ((HookAdapter) -> Boolean), + consumer: (HookAdapter) -> Unit ) = Hooker.hookConstructor(this, stage, filter, consumer) -inline fun Class<*>.hook( +fun Class<*>.hook( methodName: String, stage: HookStage, - crossinline consumer: HookConsumer + consumer: (HookAdapter) -> Unit ): Set<XC_MethodHook.Unhook> = Hooker.hook(this, methodName, stage, consumer) -inline fun Class<*>.hook( +fun Class<*>.hook( methodName: String, stage: HookStage, - crossinline filter: HookFilter, - crossinline consumer: HookConsumer + filter: (HookAdapter) -> Boolean, + consumer: (HookAdapter) -> Unit ): Set<XC_MethodHook.Unhook> = Hooker.hook(this, methodName, stage, filter, consumer) -inline fun Member.hook( +fun Member.hook( stage: HookStage, - crossinline consumer: HookConsumer + consumer: (HookAdapter) -> Unit ): XC_MethodHook.Unhook = Hooker.hook(this, stage, consumer) -inline fun Member.hook( +fun Member.hook( stage: HookStage, - crossinline filter: HookFilter, - crossinline consumer: HookConsumer -): XC_MethodHook.Unhook = Hooker.hook(this, stage, filter, consumer)- \ No newline at end of file + filter: ((HookAdapter) -> Boolean), + consumer: (HookAdapter) -> Unit +): XC_MethodHook.Unhook = Hooker.hook(this, stage, filter, consumer) + +fun Array<Method>.hookAll(stage: HookStage, param: (HookAdapter) -> Unit) { + filter { it.declaringClass != Object::class.java }.forEach { + it.hook(stage, param) + } +} diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/manager/impl/FeatureManager.kt b/core/src/main/kotlin/me/rhunk/snapenhance/manager/impl/FeatureManager.kt @@ -5,7 +5,7 @@ import me.rhunk.snapenhance.ModContext import me.rhunk.snapenhance.features.Feature import me.rhunk.snapenhance.features.FeatureLoadParams import me.rhunk.snapenhance.features.impl.AutoUpdater -import me.rhunk.snapenhance.features.impl.ConfigEnumKeys +import me.rhunk.snapenhance.features.impl.ConfigurationOverride import me.rhunk.snapenhance.features.impl.Messaging import me.rhunk.snapenhance.features.impl.downloader.AntiAutoDownload import me.rhunk.snapenhance.features.impl.downloader.MediaDownloader @@ -14,6 +14,7 @@ import me.rhunk.snapenhance.features.impl.experiments.AppPasscode import me.rhunk.snapenhance.features.impl.experiments.DeviceSpooferHook import me.rhunk.snapenhance.features.impl.experiments.InfiniteStoryBoost import me.rhunk.snapenhance.features.impl.experiments.MeoPasscodeBypass +import me.rhunk.snapenhance.features.impl.experiments.NoFriendScoreDelay import me.rhunk.snapenhance.features.impl.experiments.UnlimitedMultiSnap import me.rhunk.snapenhance.features.impl.privacy.DisableMetrics import me.rhunk.snapenhance.features.impl.privacy.PreventMessageSending @@ -74,7 +75,7 @@ class FeatureManager(private val context: ModContext) : Manager { register(Notifications::class) register(AutoSave::class) register(UITweaks::class) - register(ConfigEnumKeys::class) + register(ConfigurationOverride::class) register(AntiAutoDownload::class) register(GalleryMediaSendOverride::class) register(AntiAutoSave::class) @@ -93,6 +94,7 @@ class FeatureManager(private val context: ModContext) : Manager { register(DeviceSpooferHook::class) register(StartupPageOverride::class) register(GooglePlayServicesDialogs::class) + register(NoFriendScoreDelay::class) initializeFeatures() } diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/manager/impl/MappingManager.kt b/core/src/main/kotlin/me/rhunk/snapenhance/manager/impl/MappingManager.kt @@ -11,6 +11,7 @@ import me.rhunk.snapenhance.ui.ViewAppearanceHelper import me.rhunk.snapmapper.Mapper import me.rhunk.snapmapper.impl.BCryptClassMapper import me.rhunk.snapmapper.impl.CallbackMapper +import me.rhunk.snapmapper.impl.CompositeConfigurationProviderMapper import me.rhunk.snapmapper.impl.DefaultMediaItemMapper import me.rhunk.snapmapper.impl.EnumMapper import me.rhunk.snapmapper.impl.FriendsFeedEventDispatcherMapper @@ -19,6 +20,7 @@ import me.rhunk.snapmapper.impl.OperaPageViewControllerMapper import me.rhunk.snapmapper.impl.PlatformAnalyticsCreatorMapper import me.rhunk.snapmapper.impl.PlusSubscriptionMapper import me.rhunk.snapmapper.impl.ScCameraSettingsMapper +import me.rhunk.snapmapper.impl.ScoreUpdateMapper import me.rhunk.snapmapper.impl.StoryBoostStateMapper import java.nio.charset.StandardCharsets import java.util.concurrent.ConcurrentHashMap @@ -37,7 +39,9 @@ class MappingManager(private val context: ModContext) : Manager { PlusSubscriptionMapper::class, ScCameraSettingsMapper::class, StoryBoostStateMapper::class, - FriendsFeedEventDispatcherMapper::class + FriendsFeedEventDispatcherMapper::class, + CompositeConfigurationProviderMapper::class, + ScoreUpdateMapper::class ) private val mappings = ConcurrentHashMap<String, Any>() @@ -94,7 +98,7 @@ class MappingManager(private val context: ModContext) : Manager { statusDialogBuilder.show() } } - } + } } } diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/util/XposedHelperMacros.kt b/core/src/main/kotlin/me/rhunk/snapenhance/util/XposedHelperExt.kt diff --git a/core/src/main/res/layout/precise_location_dialog.xml b/core/src/main/res/layout/precise_location_dialog.xml @@ -11,18 +11,18 @@ android:id="@+id/dialog_latitude" android:layout_width="match_parent" android:layout_height="wrap_content" + android:autofillHints="" android:ems="10" - android:inputType="numberDecimal" android:hint="Latitude" - android:autofillHints="" /> + android:inputType="number|numberDecimal|numberSigned" /> <EditText android:id="@+id/dialog_longitude" android:layout_width="match_parent" android:layout_height="wrap_content" + android:autofillHints="" android:ems="10" - android:inputType="numberDecimal" android:hint="Longitude" - android:autofillHints="" /> + android:inputType="number|numberDecimal|numberSigned" /> </LinearLayout> \ No newline at end of file diff --git a/mapper/src/main/kotlin/me/rhunk/snapmapper/impl/CompositeConfigurationProviderMapper.kt b/mapper/src/main/kotlin/me/rhunk/snapmapper/impl/CompositeConfigurationProviderMapper.kt @@ -0,0 +1,55 @@ +package me.rhunk.snapmapper.impl + +import me.rhunk.snapmapper.AbstractClassMapper +import me.rhunk.snapmapper.MapperContext +import me.rhunk.snapmapper.ext.findConstString +import me.rhunk.snapmapper.ext.getClassName +import me.rhunk.snapmapper.ext.hasStaticConstructorString +import me.rhunk.snapmapper.ext.isEnum +import java.lang.reflect.Modifier + +class CompositeConfigurationProviderMapper : AbstractClassMapper() { + override fun run(context: MapperContext) { + for (classDef in context.classes) { + val constructor = classDef.methods.firstOrNull { it.name == "<init>" } ?: continue + if (constructor.parameterTypes.size == 0 || constructor.parameterTypes[0] != "Ljava/util/List;") continue + if (constructor.implementation?.findConstString("CompositeConfigurationProvider") != true) continue + + val getPropertyMethod = classDef.methods.first { method -> + method.parameterTypes.size > 1 && + method.returnType == "Ljava/lang/Object;" && + context.getClass(method.parameterTypes[0])?.interfaces?.contains("Ljava/io/Serializable;") == true && + context.getClass(method.parameterTypes[1])?.let { it.isEnum() && it.hasStaticConstructorString("BOOLEAN") } == true + } + + val configEnumInterface = context.getClass(getPropertyMethod.parameterTypes[0])!! + val enumType = context.getClass(getPropertyMethod.parameterTypes[1])!! + + val observePropertyMethod = classDef.methods.first { + it.parameterTypes.size > 2 && + it.parameterTypes[0] == configEnumInterface.type && + it.parameterTypes[1] == "Ljava/lang/String;" && + it.parameterTypes[2] == enumType.type + } + + val enumGetDefaultValueMethod = configEnumInterface.methods.first { context.getClass(it.returnType)?.interfaces?.contains("Ljava/io/Serializable;") == true } + val defaultValueField = context.getClass(enumGetDefaultValueMethod.returnType)!!.fields.first { + Modifier.isFinal(it.accessFlags) && + Modifier.isPublic(it.accessFlags) && + it.type == "Ljava/lang/Object;" + } + + context.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, + "defaultValueField" to defaultValueField.name + ) + ) + return + } + } +}+ \ No newline at end of file diff --git a/mapper/src/main/kotlin/me/rhunk/snapmapper/impl/DefaultMediaItemMapper.kt b/mapper/src/main/kotlin/me/rhunk/snapmapper/impl/DefaultMediaItemMapper.kt @@ -16,6 +16,7 @@ class DefaultMediaItemMapper : AbstractClassMapper() { if (constructorParameters.size < 6 || constructorParameters[5] != "J") continue context.addMapping("DefaultMediaItem", clazz.type.replace("L", "").replace(";", "")) + return } } } \ No newline at end of file diff --git a/mapper/src/main/kotlin/me/rhunk/snapmapper/impl/EnumMapper.kt b/mapper/src/main/kotlin/me/rhunk/snapmapper/impl/EnumMapper.kt @@ -3,70 +3,22 @@ package me.rhunk.snapmapper.impl import me.rhunk.snapmapper.AbstractClassMapper import me.rhunk.snapmapper.MapperContext import me.rhunk.snapmapper.ext.getClassName -import me.rhunk.snapmapper.ext.getStaticConstructor import me.rhunk.snapmapper.ext.hasStaticConstructorString import me.rhunk.snapmapper.ext.isEnum -import org.jf.dexlib2.Opcode -import org.jf.dexlib2.iface.Method -import org.jf.dexlib2.iface.instruction.formats.Instruction21c -import org.jf.dexlib2.iface.reference.FieldReference -import org.jf.dexlib2.iface.reference.StringReference class EnumMapper : AbstractClassMapper() { override fun run(context: MapperContext) { - var enumQualityLevel : String? = null - val enums = mutableListOf<Pair<String, String>>() + lateinit var enumQualityLevel : String for (enumClass in context.classes) { if (!enumClass.isEnum()) continue - if (enumQualityLevel == null && enumClass.hasStaticConstructorString("LEVEL_MAX")) { + if (enumClass.hasStaticConstructorString("LEVEL_MAX")) { enumQualityLevel = enumClass.getClassName() - } - - if (enumClass.interfaces.isEmpty()) continue - - //check if it's a config enum - val serializableInterfaceClass = context.getClass(enumClass.interfaces.first()) ?: continue - if (serializableInterfaceClass.methods.none {it.name == "getName" && it.returnType == "Ljava/lang/String;" }) continue - - //find the method which returns the enum name - val getEnumMethod = enumClass.virtualMethods.firstOrNull { context.getClass(it.returnType)?.isEnum() == true } ?: continue - - //search for constant field instruction sget-object - - fun getFirstFieldReference21c(opcode: Opcode, method: Method) = method.implementation!!.instructions.firstOrNull { - it.opcode == opcode && it is Instruction21c - }.let { it as? Instruction21c }?.let { - it.reference as? FieldReference - } - - val fieldReference = getFirstFieldReference21c(Opcode.SGET_OBJECT, getEnumMethod) ?: - getFirstFieldReference21c(Opcode.SGET_OBJECT,enumClass.directMethods.first { it.name == "<init>" }) ?: continue - - //search field name in the <clinit> class - val enumClassListEnum = context.getClass(fieldReference.definingClass) ?: continue - - enumClassListEnum.getStaticConstructor()?.let { constructor -> - var lastEnumClassName = "" - constructor.implementation!!.instructions.forEach { - if (it.opcode == Opcode.CONST_STRING) { - lastEnumClassName = ((it as Instruction21c).reference as StringReference).string - return@forEach - } - - if (it.opcode == Opcode.SPUT_OBJECT && it is Instruction21c) { - val field = it.reference as? FieldReference ?: return@forEach - if (field.name != fieldReference.name || field.type != fieldReference.type) return@forEach - - enums.add(lastEnumClassName to enumClass.getClassName()) - } - } + break; } } - context.addMapping("EnumQualityLevel", enumQualityLevel!!) - - context.addMapping("enums", *enums.sortedBy { it.first }.toTypedArray()) + context.addMapping("EnumQualityLevel", enumQualityLevel) } } \ No newline at end of file diff --git a/mapper/src/main/kotlin/me/rhunk/snapmapper/impl/FriendsFeedEventDispatcherMapper.kt b/mapper/src/main/kotlin/me/rhunk/snapmapper/impl/FriendsFeedEventDispatcherMapper.kt @@ -22,6 +22,7 @@ class FriendsFeedEventDispatcherMapper : AbstractClassMapper() { "class" to clazz.getClassName(), "viewModelField" to viewModelField ) + return } } } \ No newline at end of file diff --git a/mapper/src/main/kotlin/me/rhunk/snapmapper/impl/MediaQualityLevelProviderMapper.kt b/mapper/src/main/kotlin/me/rhunk/snapmapper/impl/MediaQualityLevelProviderMapper.kt @@ -18,6 +18,7 @@ class MediaQualityLevelProviderMapper : AbstractClassMapper(EnumMapper::class) { "class" to clazz.type.replace("L", "").replace(";", ""), "method" to it.name ) + return } } } diff --git a/mapper/src/main/kotlin/me/rhunk/snapmapper/impl/PlatformAnalyticsCreatorMapper.kt b/mapper/src/main/kotlin/me/rhunk/snapmapper/impl/PlatformAnalyticsCreatorMapper.kt @@ -18,6 +18,7 @@ class PlatformAnalyticsCreatorMapper : AbstractClassMapper() { if (firstParameterClass.getStaticConstructor()?.implementation?.findConstString("IN_APP_NOTIFICATION") != true) continue context.addMapping("PlatformAnalyticsCreator", clazz.type.replace("L", "").replace(";", "")) + return } } } \ No newline at end of file diff --git a/mapper/src/main/kotlin/me/rhunk/snapmapper/impl/ScCameraSettingsMapper.kt b/mapper/src/main/kotlin/me/rhunk/snapmapper/impl/ScCameraSettingsMapper.kt @@ -15,6 +15,7 @@ class ScCameraSettingsMapper : AbstractClassMapper() { if (!firstParameter.isEnum() || firstParameter.getStaticConstructor()?.implementation?.findConstString("CONTINUOUS_PICTURE") != true) continue context.addMapping("ScCameraSettings", clazz.type.replace("L", "").replace(";", "")) + return } } } \ No newline at end of file diff --git a/mapper/src/main/kotlin/me/rhunk/snapmapper/impl/ScoreUpdateMapper.kt b/mapper/src/main/kotlin/me/rhunk/snapmapper/impl/ScoreUpdateMapper.kt @@ -0,0 +1,25 @@ +package me.rhunk.snapmapper.impl + +import me.rhunk.snapmapper.AbstractClassMapper +import me.rhunk.snapmapper.MapperContext +import me.rhunk.snapmapper.ext.findConstString +import me.rhunk.snapmapper.ext.getClassName + +class ScoreUpdateMapper : AbstractClassMapper() { + override fun run(context: MapperContext) { + for (classDef in context.classes) { + classDef.methods.firstOrNull { + it.name == "<init>" && + it.parameterTypes.size > 4 && + it.parameterTypes[1] == "Ljava/lang/Long;" && + it.parameterTypes[3] == "Ljava/util/Collection;" + } ?: continue + if (classDef.methods.firstOrNull { + it.name == "toString" + }?.implementation?.findConstString("Friend.sq:selectFriendUserScoresNeedToUpdate") != true) continue + + context.addMapping("ScoreUpdate", classDef.getClassName()) + return + } + } +}+ \ No newline at end of file diff --git a/mapper/src/main/kotlin/me/rhunk/snapmapper/impl/StoryBoostStateMapper.kt b/mapper/src/main/kotlin/me/rhunk/snapmapper/impl/StoryBoostStateMapper.kt @@ -18,6 +18,7 @@ class StoryBoostStateMapper : AbstractClassMapper() { if (storyBoostEnumClass.getStaticConstructor()?.implementation?.findConstString("NeedSubscriptionCannotSubscribe") != true) continue context.addMapping("StoryBoostStateClass", clazz.type.replace("L", "").replace(";", "")) + return } } } \ No newline at end of file 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 @@ -21,7 +21,9 @@ class TestMappings { PlusSubscriptionMapper::class, ScCameraSettingsMapper::class, StoryBoostStateMapper::class, - FriendsFeedEventDispatcherMapper::class + FriendsFeedEventDispatcherMapper::class, + CompositeConfigurationProviderMapper::class, + ScoreUpdateMapper::class, ) val gson = GsonBuilder().setPrettyPrinting().create()