feat: camera settings override (#51)

* feat: camera tweaks

* refact: force media source quality

---------

Co-authored-by: rhunk <101876869+rhunk@users.noreply.github.com>
This commit is contained in:
auth
2023-06-09 02:45:41 +02:00
committed by GitHub
parent 9e9d82ab06
commit 0b80489a1d
11 changed files with 220 additions and 53 deletions

View File

@@ -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": {

View File

@@ -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");
}

View File

@@ -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",

View File

@@ -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)
}
}

View File

@@ -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 }
enumClass.enumConstants.forEach { enum ->
enumDataField.get(enum)?.let { enumData ->
val key = enum.toString()
callback(key) { newValue ->
enumData.setObjectField(objectDataField.name, newValue)
//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))
}
}
}
@@ -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)
}

View File

@@ -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]
}
}
}
}
}

View File

@@ -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" } )
}
}
}

View File

@@ -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 ->

View File

@@ -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()

View File

@@ -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())
}

View File

@@ -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
}
}
}