feat: loop media playback

- refactor mappings wrapper
This commit is contained in:
rhunk
2024-01-01 21:41:12 +01:00
parent 1b566db184
commit f1b0bc41f2
13 changed files with 121 additions and 23 deletions

View File

@ -378,6 +378,10 @@
"name": "Unlimited Snap View Time", "name": "Unlimited Snap View Time",
"description": "Removes the Time Limit for viewing Snaps" "description": "Removes the Time Limit for viewing Snaps"
}, },
"loop_media_playback": {
"name": "Loop Media Playback",
"description": "Loops media playback when viewing Snaps / Stories"
},
"disable_replay_in_ff": { "disable_replay_in_ff": {
"name": "Disable Replay in FF", "name": "Disable Replay in FF",
"description": "Disables the ability to replay with a long press from the Friend Feed" "description": "Disables the ability to replay with a long press from the Friend Feed"

View File

@ -31,6 +31,7 @@ class MappingsWrapper : FileLoaderWrapper(BridgeFileType.MAPPINGS, "{}".toByteAr
FriendRelationshipChangerMapper::class, FriendRelationshipChangerMapper::class,
ViewBinderMapper::class, ViewBinderMapper::class,
FriendingDataSourcesMapper::class, FriendingDataSourcesMapper::class,
OperaViewerParamsMapper::class,
) )
} }
@ -115,29 +116,22 @@ class MappingsWrapper : FileLoaderWrapper(BridgeFileType.MAPPINGS, "{}".toByteAr
return mappings[key] return mappings[key]
} }
fun getMappedClass(className: String): Class<*> { fun getMappedClass(className: String): Class<*>? {
return context.classLoader.loadClass(getMappedObject(className) as String) return runCatching {
context.classLoader.loadClass(getMappedObject(className) as? String)
}.getOrNull()
} }
fun getMappedClass(key: String, subKey: String): Class<*> { fun getMappedClass(key: String, subKey: String): Class<*> {
return context.classLoader.loadClass(getMappedValue(key, subKey)) return context.classLoader.loadClass(getMappedValue(key, subKey))
} }
fun getMappedValue(key: String): String { fun getMappedValue(key: String, subKey: String): String? {
return getMappedObject(key) as String return getMappedMap(key)?.get(subKey) as? String
} }
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
fun <T : Any> getMappedList(key: String): List<T> { fun getMappedMap(key: String): Map<String, *>? {
return listOf(getMappedObject(key) as List<T>).flatten() return getMappedObjectNullable(key) as? Map<String, *>
}
fun getMappedValue(key: String, subKey: String): String {
return getMappedMap(key)[subKey] as String
}
@Suppress("UNCHECKED_CAST")
fun getMappedMap(key: String): Map<String, *> {
return getMappedObject(key) as Map<String, *>
} }
} }

View File

@ -55,6 +55,7 @@ class MessagingTweaks : ConfigContainer() {
val hideBitmojiPresence = boolean("hide_bitmoji_presence") val hideBitmojiPresence = boolean("hide_bitmoji_presence")
val hideTypingNotifications = boolean("hide_typing_notifications") val hideTypingNotifications = boolean("hide_typing_notifications")
val unlimitedSnapViewTime = boolean("unlimited_snap_view_time") val unlimitedSnapViewTime = boolean("unlimited_snap_view_time")
val loopMediaPlayback = boolean("loop_media_playback") { requireRestart() }
val disableReplayInFF = boolean("disable_replay_in_ff") val disableReplayInFF = boolean("disable_replay_in_ff")
val halfSwipeNotifier = container("half_swipe_notifier", HalfSwipeNotifierConfig()) { requireRestart()} val halfSwipeNotifier = container("half_swipe_notifier", HalfSwipeNotifierConfig()) { requireRestart()}
val messagePreviewLength = integer("message_preview_length", defaultValue = 20) val messagePreviewLength = integer("message_preview_length", defaultValue = 20)

View File

@ -145,7 +145,7 @@ class BulkMessagingAction : AbstractAction() {
} }
private fun removeFriend(userId: String) { private fun removeFriend(userId: String) {
val friendRelationshipChangerMapping = context.mappings.getMappedMap("FriendRelationshipChanger") val friendRelationshipChangerMapping = context.mappings.getMappedMap("FriendRelationshipChanger") ?: throw Exception("Failed to get FriendRelationshipChanger mapping")
val friendRelationshipChangerInstance = context.feature(AddFriendSourceSpoof::class).friendRelationshipChangerInstance!! val friendRelationshipChangerInstance = context.feature(AddFriendSourceSpoof::class).friendRelationshipChangerInstance!!
val removeFriendMethod = friendRelationshipChangerInstance::class.java.methods.first { val removeFriendMethod = friendRelationshipChangerInstance::class.java.methods.first {

View File

@ -23,7 +23,7 @@ data class ConfigFilter(
class ConfigurationOverride : Feature("Configuration Override", loadParams = FeatureLoadParams.INIT_SYNC) { class ConfigurationOverride : Feature("Configuration Override", loadParams = FeatureLoadParams.INIT_SYNC) {
override fun init() { override fun init() {
val compositeConfigurationProviderMappings = context.mappings.getMappedMap("CompositeConfigurationProvider") val compositeConfigurationProviderMappings = context.mappings.getMappedMap("CompositeConfigurationProvider") ?: throw Exception("Failed to get compositeConfigurationProviderMappings")
val enumMappings = compositeConfigurationProviderMappings["enum"] as Map<*, *> val enumMappings = compositeConfigurationProviderMappings["enum"] as Map<*, *>
fun getConfigKeyInfo(key: Any?) = runCatching { fun getConfigKeyInfo(key: Any?) = runCatching {

View File

@ -0,0 +1,64 @@
package me.rhunk.snapenhance.core.features.impl
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
class OperaViewerParamsOverride : Feature("OperaViewerParamsOverride", loadParams = FeatureLoadParams.ACTIVITY_CREATE_SYNC) {
data class OverrideKey(
val name: String,
val defaultValue: Any?
)
data class Override(
val filter: (value: Any?) -> Boolean,
val value: (key: OverrideKey, value: Any?) -> Any?
)
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?) {
overrideMap[key] = Override(filter, value)
}
if (context.config.messaging.loopMediaPlayback.get()) {
//https://github.com/rodit/SnapMod/blob/master/app/src/main/java/xyz/rodit/snapmod/features/opera/SnapDurationModifier.kt
overrideParam("auto_advance_mode", { true }, { key, _ -> key.defaultValue })
overrideParam("auto_advance_max_loop_number", { true }, { _, _ -> Int.MAX_VALUE })
overrideParam("media_playback_mode", { true }, { _, value ->
val playbackMode = value ?: return@overrideParam null
playbackMode::class.java.enumConstants.firstOrNull {
it.toString() == "LOOPING"
} ?: return@overrideParam value
})
}
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
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)
}
}
}
}
}
}

View File

@ -7,7 +7,7 @@ import me.rhunk.snapenhance.core.util.hook.Hooker
class MeoPasscodeBypass : Feature("Meo Passcode Bypass", loadParams = FeatureLoadParams.ACTIVITY_CREATE_ASYNC) { class MeoPasscodeBypass : Feature("Meo Passcode Bypass", loadParams = FeatureLoadParams.ACTIVITY_CREATE_ASYNC) {
override fun asyncOnActivityCreate() { override fun asyncOnActivityCreate() {
val bcrypt = context.mappings.getMappedMap("BCrypt") val bcrypt = context.mappings.getMappedMap("BCrypt") ?: throw Exception("Failed to get bcrypt mappings")
Hooker.hook( Hooker.hook(
context.androidContext.classLoader.loadClass(bcrypt["class"].toString()), context.androidContext.classLoader.loadClass(bcrypt["class"].toString()),

View File

@ -46,7 +46,7 @@ class BypassVideoLengthRestriction :
} }
context.mappings.getMappedClass("DefaultMediaItem") context.mappings.getMappedClass("DefaultMediaItem")
.hookConstructor(HookStage.BEFORE) { param -> ?.hookConstructor(HookStage.BEFORE) { param ->
//set the video length argument //set the video length argument
param.setArg(5, -1L) param.setArg(5, -1L)
} }
@ -54,7 +54,7 @@ class BypassVideoLengthRestriction :
//TODO: allow split from any source //TODO: allow split from any source
if (mode == "split") { if (mode == "split") {
val cameraRollId = context.mappings.getMappedMap("CameraRollMediaId") val cameraRollId = context.mappings.getMappedMap("CameraRollMediaId") ?: throw Exception("Failed to get cameraRollId mappings")
// memories grid // memories grid
findClass(cameraRollId["class"].toString()).hookConstructor(HookStage.AFTER) { param -> findClass(cameraRollId["class"].toString()).hookConstructor(HookStage.AFTER) { param ->
//set the durationMs field //set the durationMs field

View File

@ -7,8 +7,8 @@ import me.rhunk.snapenhance.core.util.hook.hook
class MediaQualityLevelOverride : Feature("MediaQualityLevelOverride", loadParams = FeatureLoadParams.INIT_SYNC) { class MediaQualityLevelOverride : Feature("MediaQualityLevelOverride", loadParams = FeatureLoadParams.INIT_SYNC) {
override fun init() { override fun init() {
val enumQualityLevel = context.mappings.getMappedClass("EnumQualityLevel") val enumQualityLevel = context.mappings.getMappedClass("EnumQualityLevel") ?: throw Exception("Failed to get enumQualityLevelMappings")
val mediaQualityLevelProvider = context.mappings.getMappedMap("MediaQualityLevelProvider") val mediaQualityLevelProvider = context.mappings.getMappedMap("MediaQualityLevelProvider") ?: throw Exception("Failed to get mediaQualityLevelProviderMappings")
val forceMediaSourceQuality by context.config.global.forceUploadSourceQuality val forceMediaSourceQuality by context.config.global.forceUploadSourceQuality

View File

@ -10,7 +10,7 @@ class HideQuickAddFriendFeed : Feature("HideQuickAddFriendFeed", loadParams = Fe
override fun onActivityCreate() { override fun onActivityCreate() {
if (!context.config.userInterface.hideQuickAddFriendFeed.get()) return if (!context.config.userInterface.hideQuickAddFriendFeed.get()) return
val friendingDataSource = context.mappings.getMappedMap("FriendingDataSources") val friendingDataSource = context.mappings.getMappedMap("FriendingDataSources") ?: throw Exception("Failed to get friendingDataSourceMappings")
findClass(friendingDataSource["class"].toString()).hookConstructor(HookStage.AFTER) { param -> findClass(friendingDataSource["class"].toString()).hookConstructor(HookStage.AFTER) { param ->
param.thisObject<Any>().setObjectField( param.thisObject<Any>().setObjectField(
friendingDataSource["quickAddSourceListField"].toString(), friendingDataSource["quickAddSourceListField"].toString(),

View File

@ -8,6 +8,7 @@ import me.rhunk.snapenhance.core.features.Feature
import me.rhunk.snapenhance.core.features.FeatureLoadParams import me.rhunk.snapenhance.core.features.FeatureLoadParams
import me.rhunk.snapenhance.core.features.MessagingRuleFeature import me.rhunk.snapenhance.core.features.MessagingRuleFeature
import me.rhunk.snapenhance.core.features.impl.ConfigurationOverride import me.rhunk.snapenhance.core.features.impl.ConfigurationOverride
import me.rhunk.snapenhance.core.features.impl.OperaViewerParamsOverride
import me.rhunk.snapenhance.core.features.impl.ScopeSync import me.rhunk.snapenhance.core.features.impl.ScopeSync
import me.rhunk.snapenhance.core.features.impl.Stories import me.rhunk.snapenhance.core.features.impl.Stories
import me.rhunk.snapenhance.core.features.impl.downloader.MediaDownloader import me.rhunk.snapenhance.core.features.impl.downloader.MediaDownloader
@ -120,6 +121,7 @@ class FeatureManager(
SuspendLocationUpdates::class, SuspendLocationUpdates::class,
ConversationToolbox::class, ConversationToolbox::class,
SpotlightCommentsUsername::class, SpotlightCommentsUsername::class,
OperaViewerParamsOverride::class,
) )
initializeFeatures() initializeFeatures()

View File

@ -0,0 +1,32 @@
package me.rhunk.snapenhance.mapper.impl
import me.rhunk.snapenhance.mapper.AbstractClassMapper
import me.rhunk.snapenhance.mapper.ext.findConstString
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() {
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 ->
method.implementation?.instructions?.any {
val instruction = it as? Instruction35c ?: return@any false
val reference = instruction.reference as? MethodReference ?: return@any false
reference.name == "put" && reference.definingClass == "Ljava/util/concurrent/ConcurrentHashMap;"
} == true
} ?: return@mapper
addMapping("OperaViewerParams",
"class" to classDef.getClassName(),
"putMethod" to putMethod.name
)
return@mapper
}
}
}
}

View File

@ -25,6 +25,7 @@ class TestMappings {
FriendRelationshipChangerMapper::class, FriendRelationshipChangerMapper::class,
ViewBinderMapper::class, ViewBinderMapper::class,
FriendingDataSourcesMapper::class, FriendingDataSourcesMapper::class,
OperaViewerParamsMapper::class,
) )
val gson = GsonBuilder().setPrettyPrinting().create() val gson = GsonBuilder().setPrettyPrinting().create()