commit 0b80489a1d068cf576ee84bea073dfa1e144432b parent 9e9d82ab06c9ce63207f44c9cde071bbe9b57569 Author: auth <64337177+authorisation@users.noreply.github.com> Date: Fri, 9 Jun 2023 02:45:41 +0200 feat: camera settings override (#51) * feat: camera tweaks * refact: force media source quality --------- Co-authored-by: rhunk <101876869+rhunk@users.noreply.github.com> Diffstat:
11 files changed, 223 insertions(+), 53 deletions(-)
diff --git a/app/src/main/assets/lang/en_US.json b/app/src/main/assets/lang/en_US.json @@ -3,6 +3,7 @@ "spying_privacy": "Spying & Privacy", "media_manager": "Media Manager", "ui_tweaks": "UI & Tweaks", + "camera": "Camera", "updates": "Updates", "experimental_debugging": "Experimental" }, @@ -41,7 +42,7 @@ "snapchat_plus": "Snapchat Plus", "disable_snap_splitting": "Disable Snap Splitting", "disable_video_length_restriction": "Disable Video Length Restriction", - "override_media_quality": "Override Media Quality", + "force_media_source_quality": "Force Media Source Quality", "media_quality_level": "Media Quality Level", "remove_voice_record_button": "Remove Voice Record Button", "remove_stickers_button": "Remove Stickers Button", @@ -61,7 +62,11 @@ "auto_updater": "Auto Updater", "infinite_story_boost": "Infinite Story Boost", "enable_app_appearance": "Enable App Appearance Settings", - "disable_spotlight": "Disable Spotlight" + "disable_spotlight": "Disable Spotlight", + "preview_resolution": "Override Preview Resolution", + "picture_resolution": "Override Picture Resolution", + "force_highest_frame_rate": "Force Highest Frame Rate", + "force_camera_source_encoding": "Force Camera Source Encoding" }, "option": { diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/config/ConfigCategory.kt b/app/src/main/kotlin/me/rhunk/snapenhance/config/ConfigCategory.kt @@ -7,5 +7,6 @@ enum class ConfigCategory( MEDIA_MANAGEMENT("category.media_manager"), UI_TWEAKS("category.ui_tweaks"), UPDATES("category.updates"), + CAMERA("category.camera"), EXPERIMENTAL_DEBUGGING("category.experimental_debugging"); } diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/config/ConfigProperty.kt b/app/src/main/kotlin/me/rhunk/snapenhance/config/ConfigProperty.kt @@ -6,6 +6,7 @@ import me.rhunk.snapenhance.config.impl.ConfigStateListValue import me.rhunk.snapenhance.config.impl.ConfigStateSelection import me.rhunk.snapenhance.config.impl.ConfigStateValue import me.rhunk.snapenhance.config.impl.ConfigStringValue +import me.rhunk.snapenhance.features.impl.tweaks.CameraTweaks import java.io.File enum class ConfigProperty( @@ -13,7 +14,8 @@ enum class ConfigProperty( val descriptionKey: String, val category: ConfigCategory, val valueContainer: ConfigValue<*>, - val shouldAppearInSettings: Boolean = true + val shouldAppearInSettings: Boolean = true, + val disableValueLocalization: Boolean = false ) { //SPYING AND PRIVACY @@ -166,22 +168,13 @@ enum class ConfigProperty( ConfigCategory.MEDIA_MANAGEMENT, ConfigStateValue(false) ), - - OVERRIDE_MEDIA_QUALITY( - "property.override_media_quality", - "description.override_media_quality", + + FORCE_MEDIA_SOURCE_QUALITY( + "property.force_media_source_quality", + "description.force_media_source_quality", ConfigCategory.MEDIA_MANAGEMENT, ConfigStateValue(false) ), - MEDIA_QUALITY_LEVEL( - "property.media_quality_level", - "description.media_quality_level", - ConfigCategory.MEDIA_MANAGEMENT, - ConfigStateSelection( - listOf("LEVEL_NONE", "LEVEL_1", "LEVEL_2", "LEVEL_3", "LEVEL_4", "LEVEL_5", "LEVEL_6", "LEVEL_7", "LEVEL_MAX"), - "LEVEL_NONE" - ) - ), //UI AND TWEAKS HIDE_UI_ELEMENTS( @@ -272,6 +265,40 @@ enum class ConfigProperty( ), + //CAMERA + OVERRIDE_PREVIEW_RESOLUTION( + "property.preview_resolution", + "description.preview_resolution", + ConfigCategory.CAMERA, + ConfigStateSelection( + CameraTweaks.resolutions, + "OFF" + ), + disableValueLocalization = true + ), + OVERRIDE_PICTURE_RESOLUTION( + "property.picture_resolution", + "description.picture_resolution", + ConfigCategory.CAMERA, + ConfigStateSelection( + CameraTweaks.resolutions, + "OFF" + ), + disableValueLocalization = true + ), + FORCE_HIGHEST_FRAME_RATE( + "property.force_highest_frame_rate", + "description.force_highest_frame_rate", + ConfigCategory.CAMERA, + ConfigStateValue(false) + ), + FORCE_CAMERA_SOURCE_ENCODING( + "property.force_camera_source_encoding", + "description.force_camera_source_encoding", + ConfigCategory.CAMERA, + ConfigStateValue(false) + ), + // UPDATES AUTO_UPDATER( "property.auto_updater", diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/data/wrapper/impl/ScSize.kt b/app/src/main/kotlin/me/rhunk/snapenhance/data/wrapper/impl/ScSize.kt @@ -0,0 +1,26 @@ +package me.rhunk.snapenhance.data.wrapper.impl + +import me.rhunk.snapenhance.data.wrapper.AbstractWrapper + +class ScSize( + obj: Any? +) : AbstractWrapper(obj) { + private val firstField by lazy { + instanceNonNull().javaClass.declaredFields.first { it.type == Int::class.javaPrimitiveType }.also { it.isAccessible = true } + } + + private val secondField by lazy { + instanceNonNull().javaClass.declaredFields.last { it.type == Int::class.javaPrimitiveType }.also { it.isAccessible = true } + } + + + var first: Int get() = firstField.getInt(instanceNonNull()) + set(value) { + firstField.setInt(instanceNonNull(), value) + } + + var second: Int get() = secondField.getInt(instanceNonNull()) + set(value) { + secondField.setInt(instanceNonNull(), value) + } +}+ \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/ConfigEnumKeys.kt b/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/ConfigEnumKeys.kt @@ -6,31 +6,48 @@ 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) { - private fun hookAllEnums(enumClass: Class<*>, callback: (String, (Any) -> Unit) -> 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 } + data class HookEnumContext( + val key: String, + val type: Type?, + val value: Any?, + val set: (Any) -> Unit + ) - //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) - } + 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() - callback(key) { newValue -> - enumData.setObjectField(objectDataField.name, newValue) + 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)) } } } @@ -39,25 +56,25 @@ class ConfigEnumKeys : Feature("Config enum keys", loadParams = FeatureLoadParam @SuppressLint("PrivateApi") override fun onActivityCreate() { if (context.config.bool(ConfigProperty.NEW_MAP_UI)) { - hookAllEnums(context.mappings.getMappedClass("enums", "PLUS")) { key, set -> + hookAllEnums(context.mappings.getMappedClass("enums", "PLUS")) { if (key == "REDUCE_MY_PROFILE_UI_COMPLEXITY") set(true) } } - hookAllEnums(context.mappings.getMappedClass("enums", "ARROYO")) { key, set -> + hookAllEnums(context.mappings.getMappedClass("enums", "ARROYO")) { if (key == "ENABLE_LONG_SNAP_SENDING") { if (context.config.bool(ConfigProperty.DISABLE_SNAP_SPLITTING)) set(true) } } if (context.config.bool(ConfigProperty.STREAK_EXPIRATION_INFO)) { - hookAllEnums(context.mappings.getMappedClass("enums", "FRIENDS_FEED")) { key, set -> + hookAllEnums(context.mappings.getMappedClass("enums", "FRIENDS_FEED")) { if (key == "STREAK_EXPIRATION_INFO") set(true) } } if (context.config.bool(ConfigProperty.BLOCK_ADS)) { - hookAllEnums(context.mappings.getMappedClass("enums", "SNAPADS")) { key, set -> + hookAllEnums(context.mappings.getMappedClass("enums", "SNAPADS")) { if (key == "BYPASS_AD_FEATURE_GATE") { set(true) } diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/tweaks/CameraTweaks.kt b/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/tweaks/CameraTweaks.kt @@ -0,0 +1,52 @@ +package me.rhunk.snapenhance.features.impl.tweaks + +import android.annotation.SuppressLint +import me.rhunk.snapenhance.config.ConfigProperty +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.hookConstructor + +class CameraTweaks : Feature("Camera Tweaks", loadParams = FeatureLoadParams.ACTIVITY_CREATE_SYNC) { + companion object { + val resolutions = listOf("OFF", "3264x2448", "3264x1840", "3264x1504", "2688x1512", "2560x1920", "2448x2448", "2340x1080", "2160x1080", "1920x1440", "1920x1080", "1600x1200", "1600x960", "1600x900", "1600x736", "1600x720", "1560x720", "1520x720", "1440x1080", "1440x720", "1280x720", "1080x1080", "1080x720", "960x720", "720x720", "720x480", "640x480", "352x288", "320x240", "176x144") + } + + private fun parseResolution(resolution: String): IntArray? { + return resolution.takeIf { resolution != "OFF" }?.split("x")?.map { it.toInt() }?.toIntArray() + } + + @SuppressLint("MissingPermission", "DiscouragedApi") + override fun onActivityCreate() { + ConfigEnumKeys.hookAllEnums(context.mappings.getMappedClass("enums", "CAMERA")) { + if (key == "FORCE_CAMERA_HIGHEST_FPS" && context.config.bool(ConfigProperty.FORCE_HIGHEST_FRAME_RATE)) { + set(true) + } + if (key == "MEDIA_RECORDER_MAX_QUALITY_LEVEL" && context.config.bool(ConfigProperty.FORCE_CAMERA_SOURCE_ENCODING)) { + value!!.javaClass.enumConstants?.let { enumData -> set(enumData.filter { it.toString() == "LEVEL_MAX" }) } + } + } + + val previewResolutionConfig = parseResolution(context.config.state(ConfigProperty.OVERRIDE_PREVIEW_RESOLUTION)) + val captureResolutionConfig = parseResolution(context.config.state(ConfigProperty.OVERRIDE_PICTURE_RESOLUTION)) + + context.mappings.getMappedClass("ScCameraSettings").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] + } + + captureResolutionConfig?.let { + captureResolution.first = it[0] + captureResolution.second = it[1] + } + } + } + } +}+ \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/tweaks/MediaQualityLevelOverride.kt b/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/tweaks/MediaQualityLevelOverride.kt @@ -13,14 +13,9 @@ class MediaQualityLevelOverride : Feature("MediaQualityLevelOverride", loadParam Hooker.hook(context.mappings.getMappedClass("MediaQualityLevelProvider"), context.mappings.getMappedValue("MediaQualityLevelProviderMethod"), HookStage.BEFORE, - {context.config.bool(ConfigProperty.OVERRIDE_MEDIA_QUALITY)} + { context.config.bool(ConfigProperty.FORCE_MEDIA_SOURCE_QUALITY) } ) { param -> - val currentCompressionLevel = enumQualityLevel.enumConstants - .firstOrNull { it.toString() == context.config.state(ConfigProperty.MEDIA_QUALITY_LEVEL)} ?: run { - context.longToast("Invalid media quality level: ${context.config.state(ConfigProperty.MEDIA_QUALITY_LEVEL)}") - return@hook - } - param.setResult(currentCompressionLevel) + param.setResult(enumQualityLevel.enumConstants.firstOrNull { it.toString() == "LEVEL_MAX" } ) } } } \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/ui/menus/impl/SettingsMenu.kt b/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/ui/menus/impl/SettingsMenu.kt @@ -36,17 +36,29 @@ class SettingsMenu : AbstractMenu() { @SuppressLint("SetTextI18n") private fun createPropertyView(viewModel: View, property: ConfigProperty): View { + val propertyName = context.translation.get(property.nameKey) val updateButtonText: (TextView, String) -> Unit = { textView, text -> - textView.text = "${context.translation.get(property.nameKey)}${if (text.isEmpty()) "" else ": $text"}" + textView.text = "$propertyName${if (text.isEmpty()) "" else ": $text"}" } val updateLocalizedText: (TextView, String) -> Unit = { textView, value -> - updateButtonText(textView, value.let { if (it.isEmpty()) "(empty)" else context.translation.get("option." + property.nameKey + "." + it) }) + updateButtonText(textView, value.let { + if (it.isEmpty()) { + "(empty)" + } + else { + if (property.disableValueLocalization) { + it + } else { + context.translation.get("option." + property.nameKey + "." + it) + } + } + }) } val textEditor: ((String) -> Unit) -> Unit = { updateValue -> val builder = AlertDialog.Builder(viewModel.context) - builder.setTitle(context.translation.get(property.nameKey)) + builder.setTitle(propertyName) val input = EditText(viewModel.context) input.inputType = InputType.TYPE_CLASS_TEXT @@ -98,7 +110,7 @@ class SettingsMenu : AbstractMenu() { } is ConfigStateValue -> { val switch = Switch(viewModel.context) - switch.text = context.translation.get(property.nameKey) + switch.text = propertyName switch.isChecked = property.valueContainer.value() switch.setOnCheckedChangeListener { _, isChecked -> property.valueContainer.writeFrom(isChecked.toString()) @@ -112,10 +124,13 @@ class SettingsMenu : AbstractMenu() { button.setOnClickListener {_ -> val builder = AlertDialog.Builder(viewModel.context) - builder.setTitle(context.translation.get(property.nameKey)) + builder.setTitle(propertyName) builder.setSingleChoiceItems( - property.valueContainer.keys().toTypedArray().map { context.translation.get("option." + property.nameKey + "." + it) }.toTypedArray(), + property.valueContainer.keys().toTypedArray().map { + if (property.disableValueLocalization) it + else context.translation.get("option." + property.nameKey + "." + it) + }.toTypedArray(), property.valueContainer.keys().indexOf(property.valueContainer.value()) ) { _, which -> property.valueContainer.writeFrom(property.valueContainer.keys()[which]) @@ -136,12 +151,15 @@ class SettingsMenu : AbstractMenu() { button.setOnClickListener {_ -> val builder = AlertDialog.Builder(viewModel.context) - builder.setTitle(context.translation.get(property.nameKey)) + builder.setTitle(propertyName) val sortedStates = property.valueContainer.value().toSortedMap() builder.setMultiChoiceItems( - sortedStates.toSortedMap().map { context.translation.get("option." + property.nameKey + "." +it.key) }.toTypedArray(), + sortedStates.toSortedMap().map { + if (property.disableValueLocalization) it.key + else context.translation.get("option." + property.nameKey + "." + it.key) + }.toTypedArray(), sortedStates.map { it.value }.toBooleanArray() ) { _, which, isChecked -> sortedStates.keys.toList()[which].let { key -> diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/manager/impl/FeatureManager.kt b/app/src/main/kotlin/me/rhunk/snapenhance/manager/impl/FeatureManager.kt @@ -27,6 +27,7 @@ import me.rhunk.snapenhance.features.impl.spying.AnonymousStoryViewing import me.rhunk.snapenhance.features.impl.spying.MessageLogger import me.rhunk.snapenhance.features.impl.spying.PreventReadReceipts import me.rhunk.snapenhance.features.impl.spying.StealthMode +import me.rhunk.snapenhance.features.impl.tweaks.CameraTweaks import me.rhunk.snapenhance.features.impl.ui.UITweaks import me.rhunk.snapenhance.features.impl.ui.menus.MenuViewInjector import me.rhunk.snapenhance.manager.Manager @@ -78,6 +79,7 @@ class FeatureManager(private val context: ModContext) : Manager { register(AppPasscode::class) register(LocationSpoofer::class) register(AutoUpdater::class) + register(CameraTweaks::class) register(InfiniteStoryBoost::class) initializeFeatures() diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/manager/impl/MappingManager.kt b/app/src/main/kotlin/me/rhunk/snapenhance/manager/impl/MappingManager.kt @@ -16,15 +16,17 @@ import me.rhunk.snapenhance.manager.Manager import me.rhunk.snapenhance.mapping.Mapper import me.rhunk.snapenhance.mapping.impl.BCryptClassMapper import me.rhunk.snapenhance.mapping.impl.CallbackMapper -import me.rhunk.snapenhance.mapping.impl.DefaultMediaItemMapper import me.rhunk.snapenhance.mapping.impl.EnumMapper +import me.rhunk.snapenhance.mapping.impl.DefaultMediaItemMapper import me.rhunk.snapenhance.mapping.impl.OperaPageViewControllerMapper import me.rhunk.snapenhance.mapping.impl.PlatformAnalyticsCreatorMapper import me.rhunk.snapenhance.mapping.impl.PlusSubscriptionMapper +import me.rhunk.snapenhance.mapping.impl.ScCameraSettingsMapper import me.rhunk.snapenhance.mapping.impl.StoryBoostStateMapper import me.rhunk.snapenhance.util.getObjectField import java.nio.charset.StandardCharsets import java.util.concurrent.ConcurrentHashMap +import kotlin.concurrent.thread @Suppress("UNCHECKED_CAST") class MappingManager(private val context: ModContext) : Manager { @@ -36,6 +38,7 @@ class MappingManager(private val context: ModContext) : Manager { add(DefaultMediaItemMapper()) add(BCryptClassMapper()) add(PlatformAnalyticsCreatorMapper()) + add(ScCameraSettingsMapper()) add(StoryBoostStateMapper()) } diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/mapping/impl/ScCameraSettingsMapper.kt b/app/src/main/kotlin/me/rhunk/snapenhance/mapping/impl/ScCameraSettingsMapper.kt @@ -0,0 +1,21 @@ +package me.rhunk.snapenhance.mapping.impl + +import me.rhunk.snapenhance.mapping.Mapper + +class ScCameraSettingsMapper : Mapper() { + override fun useClasses( + classLoader: ClassLoader, + classes: List<Class<*>>, + mappings: MutableMap<String, Any> + ) { + for (clazz in classes) { + if (clazz.constructors.isEmpty()) continue + val parameters = clazz.constructors.first().parameterTypes + if (parameters.size < 27) continue + val firstParameter = parameters[0] + if (!firstParameter.isEnum || firstParameter.enumConstants.find { it.toString() == "CONTINUOUS_PICTURE" } == null) continue + mappings["ScCameraSettings"] = clazz.name + return + } + } +}+ \ No newline at end of file