feat(core): auto download voice notes
This commit is contained in:
@@ -365,6 +365,10 @@
|
||||
"name": "Force Voice Note Format",
|
||||
"description": "Forces Voice Notes to be saved in a specified Format"
|
||||
},
|
||||
"auto_download_voice_notes": {
|
||||
"name": "Auto Download Voice Notes",
|
||||
"description": "Automatically downloads voice notes when playing them"
|
||||
},
|
||||
"download_profile_pictures": {
|
||||
"name": "Download Profile Pictures",
|
||||
"description": "Allows you to download Profile Pictures from the profile page"
|
||||
|
||||
@@ -41,6 +41,7 @@ class DownloaderConfig : ConfigContainer() {
|
||||
val forceVoiceNoteFormat = unique("force_voice_note_format", "aac", "mp3", "opus") {
|
||||
addFlags(ConfigFlag.NO_TRANSLATE)
|
||||
}
|
||||
val autoDownloadVoiceNotes = boolean("auto_download_voice_notes") { requireRestart(); addNotices(FeatureNotice.UNSTABLE) }
|
||||
val downloadProfilePictures = boolean("download_profile_pictures") { requireRestart() }
|
||||
val operaDownloadButton = boolean("opera_download_button") { requireRestart() }
|
||||
val downloadContextMenu = boolean("download_context_menu")
|
||||
|
||||
@@ -135,7 +135,7 @@ class FeatureManager(
|
||||
ContextMenuFix(),
|
||||
DisableTelecomFramework(),
|
||||
BetterTranscript(),
|
||||
VoiceNoteAutoPlay(),
|
||||
VoiceNoteOverride(),
|
||||
)
|
||||
|
||||
features.values.toList().forEach { feature ->
|
||||
|
||||
@@ -54,7 +54,6 @@ import me.rhunk.snapenhance.mapper.impl.OperaPageViewControllerMapper
|
||||
import java.nio.file.Paths
|
||||
import java.util.UUID
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
import kotlin.io.encoding.ExperimentalEncodingApi
|
||||
import kotlin.math.absoluteValue
|
||||
|
||||
class SnapChapterInfo(
|
||||
@@ -241,6 +240,11 @@ class MediaDownloader : MessagingRuleFeature("MediaDownloader", MessagingRuleTyp
|
||||
)
|
||||
}
|
||||
|
||||
fun canAutoDownloadMessage(databaseMessage: ConversationMessage): Boolean {
|
||||
if (context.config.downloader.preventSelfAutoDownload.get() && databaseMessage.senderId == context.database.myUserId) return false
|
||||
return canUseRule(databaseMessage.clientConversationId!!)
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the media from the opera viewer
|
||||
*
|
||||
@@ -653,7 +657,7 @@ class MediaDownloader : MessagingRuleFeature("MediaDownloader", MessagingRuleTyp
|
||||
}
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
fun downloadMessageId(messageId: Long, forceAllowDuplicate: Boolean = false, isPreview: Boolean = false) {
|
||||
fun downloadMessageId(messageId: Long, forceAllowDuplicate: Boolean = false, isPreview: Boolean = false, forceDownloadFirst: Boolean = false) {
|
||||
val messageLogger = context.feature(MessageLogger::class)
|
||||
val message = context.database.getConversationMessageFromId(messageId) ?: throw Exception("Message not found in database")
|
||||
|
||||
@@ -696,7 +700,8 @@ class MediaDownloader : MessagingRuleFeature("MediaDownloader", MessagingRuleTyp
|
||||
}
|
||||
|
||||
if (!isPreview) {
|
||||
if (decodedAttachments.size == 1 ||
|
||||
if (forceDownloadFirst ||
|
||||
decodedAttachments.size == 1 ||
|
||||
context.isMainActivityPaused // we can't show alert dialogs when it downloads from a notification, so it downloads the first one
|
||||
) {
|
||||
downloadMessageAttachments(friendInfo, message, authorName,
|
||||
|
||||
@@ -1,156 +0,0 @@
|
||||
package me.rhunk.snapenhance.core.features.impl.tweaks
|
||||
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import me.rhunk.snapenhance.core.event.events.impl.BindViewEvent
|
||||
import me.rhunk.snapenhance.core.features.Feature
|
||||
import me.rhunk.snapenhance.core.util.dataBuilder
|
||||
import me.rhunk.snapenhance.core.util.hook.HookStage
|
||||
import me.rhunk.snapenhance.core.util.hook.hook
|
||||
import me.rhunk.snapenhance.core.util.ktx.getObjectField
|
||||
import me.rhunk.snapenhance.mapper.impl.PlaybackViewContextMapper
|
||||
import java.lang.reflect.Proxy
|
||||
|
||||
class VoiceNoteAutoPlay: Feature("Voice Note Auto Play") {
|
||||
override fun init() {
|
||||
if (!context.config.experimental.voiceNoteAutoPlay.get()) return
|
||||
|
||||
val playbackMap = sortedMapOf<Long, MutableList<Any>>()
|
||||
|
||||
fun setPlaybackState(componentContext: Any, state: String): Boolean {
|
||||
val seek = componentContext.getObjectField("_seek") ?: return false
|
||||
seek.javaClass.getMethod("invoke", Any::class.java).invoke(seek, 0)
|
||||
|
||||
val onPlayButtonTapped = componentContext.getObjectField("_onPlayButtonTapped") ?: return false
|
||||
onPlayButtonTapped.javaClass.getMethod("invoke", Any::class.java).invoke(
|
||||
onPlayButtonTapped,
|
||||
findClass("com.snap.voicenotes.PlaybackState").enumConstants?.first {
|
||||
it.toString() == state
|
||||
}
|
||||
)
|
||||
return true
|
||||
}
|
||||
|
||||
fun playNextVoiceNote(currentContext: Any) {
|
||||
val currentContextMessageId = playbackMap.entries.firstOrNull { entry -> entry.value.any { it.hashCode() == currentContext.hashCode() } }?.key ?: return
|
||||
|
||||
context.log.verbose("messageId=$currentContextMessageId")
|
||||
|
||||
val nextPlayback = playbackMap.entries.firstOrNull { it.key > currentContextMessageId }
|
||||
|
||||
if (nextPlayback == null) {
|
||||
context.log.verbose("No more voice notes to play")
|
||||
return
|
||||
}
|
||||
nextPlayback.value.forEach { setPlaybackState(it, "PLAYING") }
|
||||
}
|
||||
|
||||
context.classCache.conversationManager.apply {
|
||||
arrayOf("enterConversation", "exitConversation").forEach {
|
||||
hook(it, HookStage.BEFORE) {
|
||||
context.coroutineScope.launch(Dispatchers.Main) {
|
||||
playbackMap.clear()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
context.mappings.useMapper(PlaybackViewContextMapper::class) {
|
||||
componentContextClass.getAsClass()?.hook(setOnPlayButtonTapedMethod.get() ?: return@useMapper, HookStage.AFTER) { param ->
|
||||
val instance = param.thisObject<Any>()
|
||||
var lastPlayerState: String? = null
|
||||
|
||||
instance.dataBuilder {
|
||||
val onPlayButtonTapped = get("_onPlayButtonTapped") as? Any ?: return@dataBuilder
|
||||
|
||||
set("_onPlayButtonTapped", Proxy.newProxyInstance(
|
||||
context.androidContext.classLoader,
|
||||
arrayOf(findClass("kotlin.jvm.functions.Function1"))
|
||||
) { _, _, args ->
|
||||
lastPlayerState = null
|
||||
context.log.verbose("onPlayButtonTapped ${args.contentToString()}")
|
||||
onPlayButtonTapped.javaClass.getMethod("invoke", Any::class.java).invoke(onPlayButtonTapped, args[0])
|
||||
})
|
||||
|
||||
from("_playbackStateObservable") {
|
||||
val oldSubscribe = get("_subscribe") as? Any ?: return@from
|
||||
|
||||
fun subscribe(listener: Any): Any? {
|
||||
return oldSubscribe.javaClass.getMethod("invoke", Any::class.java).invoke(oldSubscribe, listener)
|
||||
}
|
||||
|
||||
set("_subscribe", Proxy.newProxyInstance(
|
||||
context.androidContext.classLoader,
|
||||
arrayOf(findClass("kotlin.jvm.functions.Function1"))
|
||||
) proxy@{ _, _, args ->
|
||||
val function4 = args[0]
|
||||
|
||||
subscribe(
|
||||
Proxy.newProxyInstance(
|
||||
context.androidContext.classLoader,
|
||||
arrayOf(findClass("kotlin.jvm.functions.Function4"))
|
||||
) { _, _, listenerArgs ->
|
||||
val state = listenerArgs[2]?.toString()
|
||||
|
||||
if (state == "PAUSED" && lastPlayerState == "PLAYING") {
|
||||
lastPlayerState = null
|
||||
context.log.verbose("playback finished. playing next voice note")
|
||||
runCatching {
|
||||
context.coroutineScope.launch(Dispatchers.Main) {
|
||||
playNextVoiceNote(instance)
|
||||
}
|
||||
}.onFailure {
|
||||
context.log.error("Failed to play next voice note", it)
|
||||
}
|
||||
}
|
||||
|
||||
lastPlayerState = state
|
||||
function4.javaClass.methods.first { it.parameterCount == 4 }.invoke(function4, *listenerArgs)
|
||||
}
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onNextActivityCreate {
|
||||
context.event.subscribe(BindViewEvent::class) { event ->
|
||||
if (!event.prevModel.toString().contains("audio_note")) return@subscribe
|
||||
event.chatMessage { _, _ ->
|
||||
// find view model of the audio note
|
||||
val viewModelField = event.prevModel.javaClass.fields.firstOrNull { field ->
|
||||
field.type.constructors.firstOrNull()?.parameterTypes?.takeIf { it.size == 3 }?.let { args ->
|
||||
args[1].interfaces.any { it.name == "com.snap.composer.utils.ComposerMarshallable" }
|
||||
} == true
|
||||
} ?: return@subscribe
|
||||
|
||||
val viewModel = viewModelField.get(event.prevModel)
|
||||
var playbackViewComponentContext: Any? = null
|
||||
|
||||
for (field in viewModel.javaClass.fields) {
|
||||
val fieldContent = runCatching { field.get(viewModel) }.getOrNull() ?: continue
|
||||
if (fieldContent.javaClass.declaredFields.any { it.name == "_onPlayButtonTapped" }) {
|
||||
playbackViewComponentContext = fieldContent
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (playbackViewComponentContext == null) {
|
||||
context.log.warn("Failed to find playback view component context")
|
||||
return@subscribe
|
||||
}
|
||||
|
||||
context.coroutineScope.launch {
|
||||
val serverMessageId = event.databaseMessage?.serverMessageId?.toLong() ?: return@launch
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
playbackMap.computeIfAbsent(serverMessageId) { mutableListOf() }.add(playbackViewComponentContext)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
package me.rhunk.snapenhance.core.features.impl.tweaks
|
||||
|
||||
import android.view.ViewGroup
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import me.rhunk.snapenhance.core.SnapEnhance
|
||||
import me.rhunk.snapenhance.core.event.events.impl.BindViewEvent
|
||||
import me.rhunk.snapenhance.core.features.Feature
|
||||
import me.rhunk.snapenhance.core.features.impl.downloader.MediaDownloader
|
||||
import me.rhunk.snapenhance.core.features.impl.messaging.Messaging
|
||||
import me.rhunk.snapenhance.core.ui.getComposerContext
|
||||
import me.rhunk.snapenhance.core.util.dataBuilder
|
||||
import me.rhunk.snapenhance.core.util.hook.HookStage
|
||||
import me.rhunk.snapenhance.core.util.hook.hook
|
||||
import me.rhunk.snapenhance.core.util.ktx.getId
|
||||
import me.rhunk.snapenhance.core.util.ktx.getObjectField
|
||||
import me.rhunk.snapenhance.core.util.makeFunctionProxy
|
||||
|
||||
class VoiceNoteOverride: Feature("Voice Note Override") {
|
||||
override fun init() {
|
||||
val voiceNoteAutoPlay = context.config.experimental.voiceNoteAutoPlay.get()
|
||||
val autoDownloadVoiceNotes = context.config.downloader.autoDownloadVoiceNotes.get()
|
||||
|
||||
if (!autoDownloadVoiceNotes && !voiceNoteAutoPlay) return
|
||||
|
||||
val playbackMap = sortedMapOf<Long, MutableList<Any>>()
|
||||
|
||||
fun setPlaybackState(componentContext: Any, state: String): Boolean {
|
||||
val seek = componentContext.getObjectField("_seek") ?: return false
|
||||
seek.javaClass.getMethod("invoke", Any::class.java).invoke(seek, 0)
|
||||
|
||||
val onPlayButtonTapped = componentContext.getObjectField("_onPlayButtonTapped") ?: return false
|
||||
onPlayButtonTapped.javaClass.getMethod("invoke", Any::class.java).invoke(
|
||||
onPlayButtonTapped,
|
||||
findClass("com.snap.voicenotes.PlaybackState").enumConstants?.first {
|
||||
it.toString() == state
|
||||
}
|
||||
)
|
||||
return true
|
||||
}
|
||||
|
||||
fun getCurrentContextMessageId(currentContext: Any): Long? {
|
||||
return synchronized(playbackMap) {
|
||||
playbackMap.entries.firstOrNull { entry -> entry.value.any { it.hashCode() == currentContext.hashCode() } }?.key
|
||||
}
|
||||
}
|
||||
|
||||
fun playNextVoiceNote(currentContext: Any) {
|
||||
val currentContextMessageId = getCurrentContextMessageId(currentContext) ?: return
|
||||
|
||||
context.log.verbose("messageId=$currentContextMessageId")
|
||||
|
||||
val nextPlayback = synchronized(playbackMap) {
|
||||
playbackMap.entries.firstOrNull { it.key > currentContextMessageId }
|
||||
}
|
||||
|
||||
if (nextPlayback == null) {
|
||||
context.log.verbose("No more voice notes to play")
|
||||
return
|
||||
}
|
||||
nextPlayback.value.toList().forEach { setPlaybackState(it, "PLAYING") }
|
||||
}
|
||||
|
||||
context.classCache.conversationManager.apply {
|
||||
arrayOf("enterConversation", "exitConversation").forEach {
|
||||
hook(it, HookStage.BEFORE) {
|
||||
synchronized(playbackMap) {
|
||||
playbackMap.clear()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SnapEnhance.classCache.nativeBridge.hook("createContext", HookStage.BEFORE) { param ->
|
||||
val componentPath = param.arg<String>(1)
|
||||
val componentContext = param.argNullable<Any>(3)
|
||||
|
||||
if (componentPath != "PlaybackView@voice_notes/src/PlaybackView") return@hook
|
||||
|
||||
var lastPlayerState: String? = null
|
||||
|
||||
componentContext.dataBuilder {
|
||||
interceptFieldInterface("_onPlayButtonTapped") { args, originalCall ->
|
||||
lastPlayerState = null
|
||||
context.log.verbose("onPlayButtonTapped ${args.contentToString()}")
|
||||
originalCall(args)
|
||||
}
|
||||
|
||||
from("_playbackStateObservable") {
|
||||
interceptFieldInterface("_subscribe") { subscribeArgs, originalSubscribe ->
|
||||
originalSubscribe(
|
||||
arrayOf(
|
||||
makeFunctionProxy(
|
||||
subscribeArgs[0]!!
|
||||
) { args, originalCall ->
|
||||
val state = args[2]?.toString()
|
||||
|
||||
if (autoDownloadVoiceNotes && state != lastPlayerState && state == "PLAYING") {
|
||||
val currentConversationId = context.feature(Messaging::class).openedConversationUUID.toString()
|
||||
val currentMessageId = getCurrentContextMessageId(componentContext!!)
|
||||
val mediaDownloader = context.feature(MediaDownloader::class)
|
||||
|
||||
context.coroutineScope.launch {
|
||||
val databaseMessage = context.database.getConversationServerMessage(currentConversationId, currentMessageId ?: return@launch) ?: throw IllegalStateException("Failed to get database message")
|
||||
|
||||
if (mediaDownloader.canAutoDownloadMessage(databaseMessage)) {
|
||||
mediaDownloader.downloadMessageId(databaseMessage.clientMessageId.toLong(), forceDownloadFirst = true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (voiceNoteAutoPlay && state == "PAUSED" && lastPlayerState == "PLAYING") {
|
||||
lastPlayerState = null
|
||||
context.log.verbose("playback finished. playing next voice note")
|
||||
runCatching {
|
||||
context.coroutineScope.launch(Dispatchers.Main) {
|
||||
playNextVoiceNote(componentContext!!)
|
||||
}
|
||||
}.onFailure {
|
||||
context.log.error("Failed to play next voice note", it)
|
||||
}
|
||||
}
|
||||
|
||||
lastPlayerState = state
|
||||
originalCall(args)
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onNextActivityCreate {
|
||||
context.event.subscribe(BindViewEvent::class) { event ->
|
||||
event.chatMessage { _, _ ->
|
||||
val messagePluginContentHolder = event.view.findViewById<ViewGroup>(context.resources.getId("plugin_content_holder")) ?: return@subscribe
|
||||
val composerRootView = messagePluginContentHolder.getChildAt(0) ?: return@subscribe
|
||||
|
||||
val composerContext = composerRootView.getComposerContext() ?: return@subscribe
|
||||
val playbackViewComponentContext = composerContext.componentContext?.get() ?: return@subscribe
|
||||
|
||||
val serverMessageId = event.databaseMessage?.serverMessageId?.toLong() ?: return@subscribe
|
||||
|
||||
synchronized(playbackMap) {
|
||||
playbackMap.computeIfAbsent(serverMessageId) { mutableListOf() }.add(playbackViewComponentContext)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -108,18 +108,17 @@ fun View.findParent(maxIteration: Int = Int.MAX_VALUE, predicate: (View) -> Bool
|
||||
|
||||
|
||||
fun View.getComposerViewNode(): ComposerViewNode? {
|
||||
if (!this::class.java.isAssignableFrom(SnapEnhance.classCache.composerView)) return null
|
||||
if (!SnapEnhance.classCache.composerView.isInstance(this)) return null
|
||||
|
||||
val composerViewNode = this::class.java.methods.firstOrNull {
|
||||
it.name == "getComposerViewNode"
|
||||
}?.invoke(this) ?: return null
|
||||
|
||||
return ComposerViewNode(composerViewNode::class.java.methods.firstOrNull {
|
||||
it.name == "getNativeHandle"
|
||||
}?.invoke(composerViewNode) as? Long ?: return null)
|
||||
return ComposerViewNode.fromNode(composerViewNode)
|
||||
}
|
||||
|
||||
fun View.getComposerContext(): ComposerContext? {
|
||||
if (!this::class.java.isAssignableFrom(SnapEnhance.classCache.composerView)) return null
|
||||
if (!SnapEnhance.classCache.composerView.isInstance(this)) return null
|
||||
|
||||
return ComposerContext(this::class.java.methods.firstOrNull {
|
||||
it.name == "getComposerContext"
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package me.rhunk.snapenhance.core.util
|
||||
|
||||
import java.lang.reflect.Proxy
|
||||
|
||||
|
||||
inline fun Any?.dataBuilder(dataClassBuilder: DataClassBuilder.() -> Unit): Any? {
|
||||
return DataClassBuilder(
|
||||
@@ -12,18 +14,34 @@ inline fun Any?.dataBuilder(dataClassBuilder: DataClassBuilder.() -> Unit): Any?
|
||||
).apply(dataClassBuilder).build()
|
||||
}
|
||||
|
||||
fun makeFunctionProxy(function: Any, handler: (args: Array<Any?>, originalCall: (Array<Any?>) -> Any?) -> Any?): Any {
|
||||
val type = function.javaClass.interfaces.firstOrNull() ?: function.javaClass
|
||||
val method = type.methods.firstOrNull {
|
||||
it.declaringClass == type
|
||||
}
|
||||
|
||||
return Proxy.newProxyInstance(
|
||||
function::class.java.classLoader,
|
||||
arrayOf(type)
|
||||
) { _, _, args ->
|
||||
handler(args) { newArgs ->
|
||||
method?.invoke(function, *newArgs)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Util for building/editing data classes
|
||||
class DataClassBuilder(
|
||||
private val instance: Any,
|
||||
) {
|
||||
fun set(fieldName: String, value: Any?) {
|
||||
val field = instance::class.java.declaredFields.firstOrNull { it.name == fieldName } ?: return
|
||||
val fieldType = field.type
|
||||
val fieldType = field.type ?: return
|
||||
field.isAccessible = true
|
||||
|
||||
when {
|
||||
fieldType.isEnum -> {
|
||||
val enumValue = fieldType.enumConstants.firstOrNull { it.toString() == value } ?: return
|
||||
val enumValue = fieldType.enumConstants?.firstOrNull { it.toString() == value } ?: return
|
||||
field.set(instance, enumValue)
|
||||
}
|
||||
fieldType.isPrimitive -> {
|
||||
@@ -69,5 +87,11 @@ class DataClassBuilder(
|
||||
type.cast(instance)?.let { callback(it) }
|
||||
}
|
||||
|
||||
fun interceptFieldInterface(fieldName: String, callback: (args: Array<Any?>, originalCall: (Array<Any?>) -> Any?) -> Any?) {
|
||||
val field = instance.javaClass.declaredFields.firstOrNull { it.name == fieldName } ?: return
|
||||
field.isAccessible = true
|
||||
set(fieldName, field.get(instance)?.let { makeFunctionProxy(it, callback) } ?: return)
|
||||
}
|
||||
|
||||
fun build() = instance
|
||||
}
|
||||
@@ -16,6 +16,14 @@ fun createComposerFunction(block: (args: Array<*>) -> Any?): Any {
|
||||
}
|
||||
|
||||
class ComposerViewNode(obj: Long) : AbstractWrapper(obj) {
|
||||
companion object {
|
||||
fun fromNode(composerViewNode: Any?): ComposerViewNode? {
|
||||
return (composerViewNode?.javaClass?.methods?.firstOrNull {
|
||||
it.name == "getNativeHandle"
|
||||
}?.invoke(composerViewNode) as? Long)?.let { ComposerViewNode(it) } ?: return null
|
||||
}
|
||||
}
|
||||
|
||||
fun getAttribute(name: String): Any? {
|
||||
return SnapEnhance.classCache.nativeBridge.methods.firstOrNull {
|
||||
it.name == "getValueForAttribute"
|
||||
|
||||
@@ -37,7 +37,6 @@ class ClassMapper(
|
||||
MemoriesPresenterMapper(),
|
||||
StreaksExpirationMapper(),
|
||||
COFObservableMapper(),
|
||||
PlaybackViewContextMapper(),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
package me.rhunk.snapenhance.mapper.impl
|
||||
|
||||
import com.android.tools.smali.dexlib2.iface.instruction.formats.Instruction22c
|
||||
import com.android.tools.smali.dexlib2.iface.reference.FieldReference
|
||||
import me.rhunk.snapenhance.mapper.AbstractClassMapper
|
||||
import me.rhunk.snapenhance.mapper.ext.getClassName
|
||||
|
||||
class PlaybackViewContextMapper: AbstractClassMapper("Playback View Context Mapper") {
|
||||
val componentContextClass = classReference("componentContextClass")
|
||||
val setOnPlayButtonTapedMethod = string("setOnPlayButtonTapedMethod")
|
||||
|
||||
init {
|
||||
mapper {
|
||||
val playbackViewClass = getClass("Lcom/snap/voicenotes/PlaybackView;") ?: return@mapper
|
||||
|
||||
val componentContextDexClass = getClass(playbackViewClass.methods.firstOrNull {
|
||||
it.name == "create" && it.parameters.size > 3
|
||||
}?.parameterTypes?.get(2)) ?: return@mapper
|
||||
|
||||
componentContextClass.set(componentContextDexClass.getClassName())
|
||||
|
||||
val setOnPlayButtonTapedDexMethod = componentContextDexClass.methods.firstOrNull { method ->
|
||||
method.name != "<init>" && method.implementation?.instructions?.any {
|
||||
if (it is Instruction22c && it.reference is FieldReference) {
|
||||
(it.reference as FieldReference).name == "_onPlayButtonTapped"
|
||||
} else false
|
||||
} == true
|
||||
} ?: return@mapper
|
||||
|
||||
setOnPlayButtonTapedMethod.set(setOnPlayButtonTapedDexMethod.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user