diff --git a/app/libs/snapmod.jar b/app/libs/snapmod.jar index 0ae7dfd..8a20fa9 100644 Binary files a/app/libs/snapmod.jar and b/app/libs/snapmod.jar differ diff --git a/app/src/main/java/xyz/rodit/snapmod/SettingsActivity.kt b/app/src/main/java/xyz/rodit/snapmod/SettingsActivity.kt index 43d76ab..421320c 100644 --- a/app/src/main/java/xyz/rodit/snapmod/SettingsActivity.kt +++ b/app/src/main/java/xyz/rodit/snapmod/SettingsActivity.kt @@ -3,6 +3,9 @@ package xyz.rodit.snapmod import android.content.Intent import android.content.pm.PackageManager import android.graphics.Color +import android.graphics.ImageFormat +import android.hardware.camera2.CameraCharacteristics +import android.hardware.camera2.CameraManager import android.net.Uri import android.os.Bundle import android.text.InputType @@ -14,6 +17,7 @@ import android.view.inputmethod.EditorInfo import android.widget.EditText import androidx.appcompat.app.AlertDialog import androidx.preference.EditTextPreference +import androidx.preference.ListPreference import androidx.preference.Preference import androidx.preference.PreferenceManager import xyz.rodit.xposed.SettingsActivity @@ -76,6 +80,8 @@ class SettingsActivity : SettingsActivity(R.xml.root_preferences) { setNumericInput(fragment, "location_share_lat") setNumericInput(fragment, "location_share_long") setNumericInput(fragment, "audio_playback_speed") + setNumericInput(fragment, "custom_video_fps") + setNumericInput(fragment, "custom_video_bitrate") (fragment.findPreference("hidden_friends") as EditTextPreference?)?.apply { setOnBindEditTextListener { @@ -85,6 +91,30 @@ class SettingsActivity : SettingsActivity(R.xml.root_preferences) { it.setSelection(it.text.length) } } + + val camera = getSystemService(CAMERA_SERVICE) as CameraManager + val characteristics = camera.getCameraCharacteristics(camera.cameraIdList[0]) + val config = characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP) + config?.let { c -> + val sizes = c.getOutputSizes(ImageFormat.JPEG) + val strings = sizes.map { s -> "${s.width}x${s.height}" }.toTypedArray() + sequenceOf( + "custom_image_resolution", + "custom_video_resolution" + ).map { fragment.findPreference(it) } + .filterNotNull() + .forEach { + it.entries = strings + "Default" + it.entryValues = strings + "0" + } + } + + fragment.findPreference("camera_readme")?.apply { + onPreferenceClickListener = Preference.OnPreferenceClickListener { + showCameraReadme() + true + } + } } private fun setNumericInput(fragment: SettingsFragment, name: String) { @@ -102,6 +132,13 @@ class SettingsActivity : SettingsActivity(R.xml.root_preferences) { .show() } + private fun showCameraReadme() { + AlertDialog.Builder(this) + .setTitle(R.string.camera_title) + .setMessage(R.string.camera_dialog_description) + .show() + } + private fun getInstallationSummary(detailed: Boolean): Spannable { val builder = SpannableStringBuilder() try { diff --git a/app/src/main/java/xyz/rodit/snapmod/features/FeatureManager.kt b/app/src/main/java/xyz/rodit/snapmod/features/FeatureManager.kt index 4aecfa3..f19b864 100644 --- a/app/src/main/java/xyz/rodit/snapmod/features/FeatureManager.kt +++ b/app/src/main/java/xyz/rodit/snapmod/features/FeatureManager.kt @@ -58,6 +58,7 @@ class FeatureManager(context: FeatureContext) : Contextual(context) { // Tweaks add(::BypassVideoLength) add(::BypassVideoLengthGlobal) + add(::CameraResolution) add(::ConfigurationTweaks) add(::ConfigurationTweaks) add(::DisableBitmojis) diff --git a/app/src/main/java/xyz/rodit/snapmod/features/info/AdditionalFriendInfo.kt b/app/src/main/java/xyz/rodit/snapmod/features/info/AdditionalFriendInfo.kt index 150b9e9..5600a9a 100644 --- a/app/src/main/java/xyz/rodit/snapmod/features/info/AdditionalFriendInfo.kt +++ b/app/src/main/java/xyz/rodit/snapmod/features/info/AdditionalFriendInfo.kt @@ -17,7 +17,8 @@ class AdditionalFriendInfo(context: FeatureContext) : Feature(context) { override fun performHooks() { // Show more info in friend profile footer. FriendProfileTransformer.apply.after(context, "more_profile_info") { - if (!FriendProfilePageData.isInstance(it.args[0]) || it.result !is List<*>) return@after + val transformer = FriendProfileTransformer.wrap(it.thisObject) + if (!FriendProfilePageData.isInstance(transformer.data) || it.result !is List<*>) return@after val viewModelList = it.result as List<*> if (viewModelList.isEmpty()) return@after @@ -25,7 +26,7 @@ class AdditionalFriendInfo(context: FeatureContext) : Feature(context) { val viewModel = viewModelList[0]!! if (!FooterInfoItem.isInstance(viewModel)) return@after - val data = FriendProfilePageData.wrap(it.args[0]) + val data = FriendProfilePageData.wrap(transformer.data) val friendDate = Date(max(data.addedTimestamp, data.reverseAddedTimestamp)) val birthday = if (data.birthday.isNull) CalendarDate(13, -1) else data.birthday diff --git a/app/src/main/java/xyz/rodit/snapmod/features/tweaks/CameraResolution.kt b/app/src/main/java/xyz/rodit/snapmod/features/tweaks/CameraResolution.kt new file mode 100644 index 0000000..7ee7b1e --- /dev/null +++ b/app/src/main/java/xyz/rodit/snapmod/features/tweaks/CameraResolution.kt @@ -0,0 +1,55 @@ +package xyz.rodit.snapmod.features.tweaks + +import xyz.rodit.snapmod.features.Feature +import xyz.rodit.snapmod.features.FeatureContext +import xyz.rodit.snapmod.mappings.MediaQualityLevel +import xyz.rodit.snapmod.mappings.RecordingCodecConfiguration +import xyz.rodit.snapmod.mappings.ScCameraSettings +import xyz.rodit.snapmod.mappings.TranscodingRequest +import xyz.rodit.snapmod.util.after +import xyz.rodit.snapmod.util.getNonDefault +import xyz.rodit.snapmod.util.getResolution + +class CameraResolution(context: FeatureContext) : Feature(context) { + + override fun performHooks() { + // Override preview and picture resolution + ScCameraSettings.constructors.after { + val settings = ScCameraSettings.wrap(it.thisObject) + context.config.getResolution("custom_video_resolution")?.let { r -> + val previewResolution = settings.previewResolution + if (previewResolution.isNotNull) { + previewResolution.width = r.width + previewResolution.height = r.height + } + } + + context.config.getResolution("custom_image_resolution")?.let { r -> + val pictureResolution = settings.pictureResolution + if (pictureResolution.isNotNull) { + pictureResolution.width = r.width + pictureResolution.height = r.height + } + } + } + + // Override actual recording resolution + RecordingCodecConfiguration.constructors.after { + val config = RecordingCodecConfiguration.wrap(it.thisObject) + context.config.getResolution("custom_video_resolution")?.let { r -> + val res = config.resolution + res.width = r.width + res.height = r.height + } + + context.config.getNonDefault("custom_video_bitrate")?.let { bitrate -> + config.bitrate = bitrate + } + } + + // Override save/send quality level + TranscodingRequest.constructors.after(context, "force_source_encoding") { + TranscodingRequest.wrap(it.thisObject).qualityLevel = MediaQualityLevel.LEVEL_MAX() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/xyz/rodit/snapmod/logging/XLogUtils.kt b/app/src/main/java/xyz/rodit/snapmod/logging/XLogUtils.kt index 43ff67b..83a5134 100644 --- a/app/src/main/java/xyz/rodit/snapmod/logging/XLogUtils.kt +++ b/app/src/main/java/xyz/rodit/snapmod/logging/XLogUtils.kt @@ -2,6 +2,8 @@ package xyz.rodit.snapmod.logging import android.util.Log import de.robv.android.xposed.XC_MethodHook +import de.robv.android.xposed.XposedBridge +import de.robv.android.xposed.XposedHelpers import java.util.* private val logMap = WeakHashMap() @@ -44,4 +46,13 @@ fun XLog.dumpMethodCall(param: XC_MethodHook.MethodHookParam) { "Arguments:\n" + param.args.mapIndexed { i, o -> "$i: $o" }.joinToString("\n") ) +} + +fun XLog.dumpConstruction(className: String, classLoader: ClassLoader) { + val cls = XposedHelpers.findClass(className, classLoader) + XposedBridge.hookAllConstructors(cls, object : XC_MethodHook() { + override fun afterHookedMethod(param: MethodHookParam) { + debug("${param.thisObject}") + } + }) } \ No newline at end of file diff --git a/app/src/main/java/xyz/rodit/snapmod/util/ConfigExtensions.kt b/app/src/main/java/xyz/rodit/snapmod/util/ConfigExtensions.kt index 38c43f9..c7e68fc 100644 --- a/app/src/main/java/xyz/rodit/snapmod/util/ConfigExtensions.kt +++ b/app/src/main/java/xyz/rodit/snapmod/util/ConfigExtensions.kt @@ -9,4 +9,22 @@ fun ConfigurationClient.getList(key: String): List { .split(',') .filter(String::isNotBlank) .map(String::trim) -} \ No newline at end of file +} + +fun ConfigurationClient.getNonDefault(key: String, default: Int = 0): Int? { + val value = this.getInt(key, default) + return if (value == default) null else value +} + +fun ConfigurationClient.getResolution(key: String): Resolution? { + val parts = this.getString(key, "0").split('x') + if (parts.size != 2) return null + return try { + val dimens = parts.map { it.toInt() } + Resolution(dimens[0], dimens[1]) + } catch (ex: Exception) { + null + } +} + +data class Resolution(val width: Int, val height: Int) \ No newline at end of file diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml index 310adef..79e5704 100644 --- a/app/src/main/res/values/arrays.xml +++ b/app/src/main/res/values/arrays.xml @@ -118,6 +118,14 @@ NOTE + + Default + + + + 0 + + com.snapchat.android diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 07b2106..98fd7e9 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -4,6 +4,7 @@ Settings Privacy Tweaks + 🧪 Camera Snaps Downloads Notifications @@ -158,6 +159,23 @@ Enable New Chat Menu Enables the new chat context menu (recommended). + Enable Story List Replaces the default story carousel with a list. + + Notice + Tap to read. + Note, increasing video resolution, FPS and bitrate all have an effect on the media size. This means high quality videos will take up a lot of storage space and will take a long time to upload and download for the recipient. + + Image Resolution + Video Resolution + + Video FPS + Set to 0 for default FPS. + + Video Bitrate (bps) + Set to 0 for default bitrate. Higher bitrate leads to higher quality but also larger file sizes. For example, 4k videos typically have a bitrate of 60000000 to 100000000 bps. + + Force Source Encoding + Forces Snapchat to use the source resolution and bitrate when saving/sending videos and images. This must be enabled for custom video/image resolution to be noticeable. \ No newline at end of file diff --git a/app/src/main/res/xml/root_preferences.xml b/app/src/main/res/xml/root_preferences.xml index d2e1e77..ba4e5c9 100644 --- a/app/src/main/res/xml/root_preferences.xml +++ b/app/src/main/res/xml/root_preferences.xml @@ -192,6 +192,52 @@ app:iconSpaceReserved="false" /> + + + + + + + + + + + + + + + diff --git a/snap.ds b/snap.ds index 418e3db..0467f64 100644 --- a/snap.ds +++ b/snap.ds @@ -4,6 +4,7 @@ import android.app.Activity; import android.content.Context; import android.graphics.drawable.Drawable; import android.graphics.Matrix; +import android.media.MediaFormat; import android.net.Uri; import android.os.Bundle; import android.view.View; @@ -238,6 +239,21 @@ enum SnapMediaType { String name; } +[certain] +class MediaPackageMedia { + + @SerializedName("video_duration_ms") + Long videoDurationMs; + @SerializedName("camera_api") + String cameraApi; + @SerializedName("media_file_size_bytes") + public Long fileSize; + @SerializedName("height") + Integer height; + @SerializedName("width") + Integer width; +} + [certain] class MediaPackage { @@ -248,14 +264,7 @@ class MediaPackage { @SerializedName("mediaId") String mediaId; @SerializedName("media") - #MediaPackageMedia media; -} - -[late] -class MediaPackageMedia { - - @SerializedName("video_duration_ms") - public Long videoDurationMs; + !MediaPackageMedia media; } [certain] @@ -525,18 +534,18 @@ class FooterInfoItem { } } -class FriendProfileTransformer implements #Func2 { +class FriendProfileTransformer { [discard] int i0; - [discard] Object target; + Object section; + Object data; - void $(Object, int) + void $(Object, Object, int) - * apply(...) { + * $apply(...) { .string "Required value was null."; .string "performanceLogger"; .method Collections->singletonList; - .field this->!target; .new !FooterInfoItem; } } @@ -1874,7 +1883,7 @@ class ProfileActionSheetProvider { } } -[certain] +[certain, conserve] class ProfileActionSheetCreator { Object nestedContext; @@ -2046,6 +2055,65 @@ interface AdapterSection { [discard] void m1(View, ...) } +[certain] +class SnapSize { + + @SerializedName("width") + int width; + @SerializedName("height") + int height; + + void $(int, int) + + void $(this) + + String $toString() { + .string contains "W x H"; + } +} + +[certain] +class RecordingCodecConfiguration { + + !SnapSize resolution; + int bitrate; + + String $toString() { + .string "RecordingCodecConfiguration(resolution="; + } +} + +[certain] +class ScCameraSettings { + + !SnapSize previewResolution; + !SnapSize pictureResolution; + + String $toString() { + .string "ScCameraSettings{mScFocusMode="; + .string ", mPreviewResolution="; + .string ", mPictureResolution="; + } +} + +[certain, obfuscated] +enum MediaQualityLevel { + + [late] static this .fields { LEVEL_NONE, LEVEL_1, LEVEL_MAX, UNRECOGNIZED_VALUE } +} + +[certain] +class TranscodingRequest { + + !MediaQualityLevel qualityLevel; + + String $toString() { + .string "TranscodingRequest(caller="; + .string ", sourceInfo="; + .string ", mediaQualityLevel="; + } +} + [late] interface Func0 {