commit d0668b67d4c64b2b70d5e9ae9c88af8161abe8e9 parent 05990a4b72ec318bab1079e5dc5e6df749627b54 Author: rhunk <101876869+rhunk@users.noreply.github.com> Date: Fri, 25 Aug 2023 02:50:41 +0200 feat: profile picture downloader - new events: add view, network request - fix anonymous story viewing Diffstat:
18 files changed, 387 insertions(+), 240 deletions(-)
diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/EventDispatcher.kt b/core/src/main/kotlin/me/rhunk/snapenhance/EventDispatcher.kt @@ -1,6 +1,11 @@ package me.rhunk.snapenhance import android.content.Intent +import android.view.View +import android.view.ViewGroup +import android.view.ViewGroup.LayoutParams +import me.rhunk.snapenhance.core.eventbus.events.impl.AddViewEvent +import me.rhunk.snapenhance.core.eventbus.events.impl.NetworkApiRequestEvent import me.rhunk.snapenhance.core.eventbus.events.impl.OnSnapInteractionEvent import me.rhunk.snapenhance.core.eventbus.events.impl.SendMessageWithContentEvent import me.rhunk.snapenhance.core.eventbus.events.impl.SnapWidgetBroadcastReceiveEvent @@ -9,6 +14,8 @@ import me.rhunk.snapenhance.data.wrapper.impl.SnapUUID import me.rhunk.snapenhance.hook.HookStage import me.rhunk.snapenhance.hook.hook import me.rhunk.snapenhance.manager.Manager +import me.rhunk.snapenhance.util.ktx.getObjectField +import me.rhunk.snapenhance.util.ktx.setObjectField import me.rhunk.snapenhance.util.snap.SnapWidgetBroadcastReceiverHelper class EventDispatcher( @@ -57,5 +64,48 @@ class EventDispatcher( } } } + + ViewGroup::class.java.getMethod( + "addView", + View::class.java, + Int::class.javaPrimitiveType, + LayoutParams::class.java + ).hook(HookStage.BEFORE) { param -> + context.event.post( + AddViewEvent( + parent = param.thisObject(), + view = param.arg(0), + index = param.arg(1), + layoutParams = param.arg(2) + ).apply { + adapter = param + } + )?.also { event -> + with(param) { + setArg(0, event.view) + setArg(1, event.index) + setArg(2, event.layoutParams) + } + if (event.canceled) param.setResult(null) + } + } + + context.classCache.networkApi.hook("submit", HookStage.BEFORE) { param -> + val request = param.arg<Any>(0) + + context.event.post( + NetworkApiRequestEvent( + url = request.getObjectField("mUrl") as String, + callback = param.arg(4), + request = request, + ).apply { + adapter = param + } + )?.also { event -> + event.request.setObjectField("mUrl", event.url) + if (event.canceled) param.setResult(null) + } + } + } } \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/ModContext.kt b/core/src/main/kotlin/me/rhunk/snapenhance/ModContext.kt @@ -21,7 +21,7 @@ import me.rhunk.snapenhance.database.DatabaseAccess import me.rhunk.snapenhance.features.Feature import me.rhunk.snapenhance.manager.impl.ActionManager import me.rhunk.snapenhance.manager.impl.FeatureManager -import me.rhunk.snapenhance.util.download.DownloadServer +import me.rhunk.snapenhance.util.download.HttpServer import java.util.concurrent.ExecutorService import java.util.concurrent.Executors import kotlin.reflect.KClass @@ -50,7 +50,7 @@ class ModContext { val mappings = MappingsWrapper() val actionManager = ActionManager(this) val database = DatabaseAccess(this) - val downloadServer = DownloadServer() + val httpServer = HttpServer() val messageSender = MessageSender(this) val classCache get() = SnapEnhance.classCache val resources: Resources get() = androidContext.resources diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/config/impl/UserInterfaceTweaks.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/config/impl/UserInterfaceTweaks.kt @@ -25,13 +25,13 @@ class UserInterfaceTweaks : ConfigContainer() { "hide_call_buttons" ) val disableSpotlight = boolean("disable_spotlight") - val startupTab = unique("startup_tab", "ngs_map_icon_container", + val startupTab = unique("startup_tab", "ngs_map_icon_container", "ngs_chat_icon_container", "ngs_camera_icon_container", "ngs_community_icon_container", "ngs_spotlight_icon_container", "ngs_search_icon_container" - ) + ) { addNotices(FeatureNotice.MAY_BREAK_INTERNAL_BEHAVIOR) } val storyViewerOverride = unique("story_viewer_override", "DISCOVER_PLAYBACK_SEEKBAR", "VERTICAL_STORY_VIEWER") { addNotices(FeatureNotice.UNSTABLE) } } diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/eventbus/events/impl/AddViewEvent.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/eventbus/events/impl/AddViewEvent.kt @@ -0,0 +1,12 @@ +package me.rhunk.snapenhance.core.eventbus.events.impl + +import android.view.View +import android.view.ViewGroup +import me.rhunk.snapenhance.core.eventbus.events.AbstractHookEvent + +class AddViewEvent( + val parent: ViewGroup, + var view: View, + var index: Int, + var layoutParams: ViewGroup.LayoutParams +) : AbstractHookEvent()+ \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/eventbus/events/impl/NetworkApiRequestEvent.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/eventbus/events/impl/NetworkApiRequestEvent.kt @@ -0,0 +1,9 @@ +package me.rhunk.snapenhance.core.eventbus.events.impl + +import me.rhunk.snapenhance.core.eventbus.events.AbstractHookEvent + +class NetworkApiRequestEvent( + val request: Any, + val callback: Any, + var url: String, +) : AbstractHookEvent()+ \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/data/FileType.kt b/core/src/main/kotlin/me/rhunk/snapenhance/data/FileType.kt @@ -21,17 +21,15 @@ enum class FileType( UNKNOWN("dat", "application/octet-stream", false, false, false); companion object { - private val fileSignatures = HashMap<String, FileType>() - - init { - fileSignatures["52494646"] = WEBP - fileSignatures["504b0304"] = ZIP - fileSignatures["89504e47"] = PNG - fileSignatures["00000020"] = MP4 - fileSignatures["00000018"] = MP4 - fileSignatures["0000001c"] = MP4 - fileSignatures["ffd8ffe0"] = JPG - } + private val fileSignatures = mapOf( + "52494646" to WEBP, + "504b0304" to ZIP, + "89504e47" to PNG, + "00000020" to MP4, + "00000018" to MP4, + "0000001c" to MP4, + "ffd8ff" to JPG, + ) fun fromString(string: String?): FileType { return values().firstOrNull { it.fileExtension.equals(string, ignoreCase = true) } ?: UNKNOWN diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/download/data/MediaFilter.kt b/core/src/main/kotlin/me/rhunk/snapenhance/download/data/MediaFilter.kt @@ -8,7 +8,8 @@ enum class MediaFilter( PENDING("pending", true), CHAT_MEDIA("chat_media"), STORY("story"), - SPOTLIGHT("spotlight"); + SPOTLIGHT("spotlight"), + PROFILE_PICTURE("profile_picture"); fun matches(source: String?): Boolean { if (source == null) return false diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/downloader/MediaDownloader.kt b/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/downloader/MediaDownloader.kt @@ -165,7 +165,7 @@ class MediaDownloader : MessagingRuleFeature("MediaDownloader", MessagingRuleTyp Uri.parse(path).let { uri -> if (uri.scheme == "file") { return@let suspendCoroutine<String> { continuation -> - context.downloadServer.ensureServerStarted { + context.httpServer.ensureServerStarted { val file = Paths.get(uri.path).toFile() val url = putDownloadableContent(file.inputStream(), file.length()) continuation.resumeWith(Result.success(url)) @@ -532,6 +532,18 @@ class MediaDownloader : MessagingRuleFeature("MediaDownloader", MessagingRuleTyp } } + fun downloadProfilePicture(url: String, author: String) { + provideDownloadManagerClient( + pathSuffix = "Profile Pictures", + mediaIdentifier = url.hashCode().toString(16).replaceFirst("-", ""), + mediaDisplaySource = author, + mediaDisplayType = MediaFilter.PROFILE_PICTURE.key + ).downloadSingleMedia( + url, + DownloadMediaType.REMOTE_MEDIA + ) + } + /** * Called when a message is focused in chat */ diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/downloader/ProfilePictureDownloader.kt b/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/downloader/ProfilePictureDownloader.kt @@ -0,0 +1,77 @@ +package me.rhunk.snapenhance.features.impl.downloader + +import android.annotation.SuppressLint +import android.widget.Button +import android.widget.RelativeLayout +import me.rhunk.snapenhance.Logger +import me.rhunk.snapenhance.core.eventbus.events.impl.AddViewEvent +import me.rhunk.snapenhance.core.eventbus.events.impl.NetworkApiRequestEvent +import me.rhunk.snapenhance.features.Feature +import me.rhunk.snapenhance.features.FeatureLoadParams +import me.rhunk.snapenhance.hook.HookStage +import me.rhunk.snapenhance.hook.Hooker +import me.rhunk.snapenhance.ui.ViewAppearanceHelper +import me.rhunk.snapenhance.util.protobuf.ProtoReader +import java.nio.ByteBuffer + +class ProfilePictureDownloader : Feature("ProfilePictureDownloader", loadParams = FeatureLoadParams.ACTIVITY_CREATE_ASYNC) { + @SuppressLint("SetTextI18n") + override fun asyncOnActivityCreate() { + var friendUsername: String? = null + var backgroundUrl: String? = null + var avatarUrl: String? = null + + context.event.subscribe(AddViewEvent::class) { event -> + if (event.view::class.java.name != "com.snap.unifiedpublicprofile.UnifiedPublicProfileView") return@subscribe + + event.parent.addView(Button(event.parent.context).apply { + text = "Download" + layoutParams = RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.WRAP_CONTENT, RelativeLayout.LayoutParams.WRAP_CONTENT).apply { + setMargins(0, 200, 0, 0) + } + setOnClickListener { + ViewAppearanceHelper.newAlertDialogBuilder( + this@ProfilePictureDownloader.context.mainActivity!! + ).apply { + setTitle("Download profile picture") + val choices = mutableMapOf<String, String>() + backgroundUrl?.let { choices["Background"] = it } + avatarUrl?.let { choices["Avatar"] = it } + + setItems(choices.keys.toTypedArray()) { _, which -> + runCatching { + this@ProfilePictureDownloader.context.feature(MediaDownloader::class).downloadProfilePicture( + choices.values.elementAt(which), + friendUsername!! + ) + }.onFailure { + Logger.error("Failed to download profile picture", it) + } + } + }.show() + } + }) + } + + + context.event.subscribe(NetworkApiRequestEvent::class) { event -> + if (!event.url.endsWith("/rpc/getPublicProfile")) return@subscribe + Hooker.ephemeralHookObjectMethod(event.callback::class.java, event.callback, "onSucceeded", HookStage.BEFORE) { methodParams -> + val content = methodParams.arg<ByteBuffer>(2).run { + ByteArray(capacity()).also { + get(it) + position(0) + } + } + + ProtoReader(content).readPath(1, 1, 2) { + friendUsername = getString(2) ?: return@readPath + readPath(4) { + backgroundUrl = getString(2) + avatarUrl = getString(100) + } + } + } + } + } +}+ \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/privacy/DisableMetrics.kt b/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/privacy/DisableMetrics.kt @@ -1,6 +1,6 @@ package me.rhunk.snapenhance.features.impl.privacy -import de.robv.android.xposed.XposedHelpers +import me.rhunk.snapenhance.core.eventbus.events.impl.NetworkApiRequestEvent import me.rhunk.snapenhance.features.Feature import me.rhunk.snapenhance.features.FeatureLoadParams import me.rhunk.snapenhance.hook.HookStage @@ -20,20 +20,10 @@ class DisableMetrics : Feature("DisableMetrics", loadParams = FeatureLoadParams. } } - Hooker.hook(context.classCache.networkApi, "submit", HookStage.BEFORE, - { disableMetrics }) { param -> - val httpRequest: Any = param.arg(0) - val url = XposedHelpers.getObjectField(httpRequest, "mUrl").toString() - /*if (url.contains("resolve?co=")) { - val index = url.indexOf("co=") - val end = url.lastIndexOf("&") - val co = url.substring(index + 3, end) - val decoded = Base64.getDecoder().decode(co.toByteArray(StandardCharsets.UTF_8)) - debug("decoded : " + decoded.toString(Charsets.UTF_8)) - debug("content: $co") - }*/ + context.event.subscribe(NetworkApiRequestEvent::class, { disableMetrics }) { param -> + val url = param.url if (url.contains("app-analytics") || url.endsWith("v1/metrics")) { - param.setResult(null) + param.canceled = true } } } diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/spying/AnonymousStoryViewing.kt b/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/spying/AnonymousStoryViewing.kt @@ -1,20 +1,27 @@ package me.rhunk.snapenhance.features.impl.spying +import kotlinx.coroutines.runBlocking +import me.rhunk.snapenhance.Logger +import me.rhunk.snapenhance.core.eventbus.events.impl.NetworkApiRequestEvent import me.rhunk.snapenhance.features.Feature import me.rhunk.snapenhance.features.FeatureLoadParams -import me.rhunk.snapenhance.hook.HookStage -import me.rhunk.snapenhance.hook.Hooker -import me.rhunk.snapenhance.util.ktx.getObjectField -import me.rhunk.snapenhance.util.ktx.setObjectField +import me.rhunk.snapenhance.util.download.HttpServer +import kotlin.coroutines.suspendCoroutine class AnonymousStoryViewing : Feature("Anonymous Story Viewing", loadParams = FeatureLoadParams.ACTIVITY_CREATE_ASYNC) { override fun asyncOnActivityCreate() { val anonymousStoryViewProperty by context.config.messaging.anonymousStoryViewing - Hooker.hook(context.classCache.networkApi,"submit", HookStage.BEFORE, { anonymousStoryViewProperty }) { - val httpRequest: Any = it.arg(0) - val url = httpRequest.getObjectField("mUrl") as String - if (url.endsWith("readreceipt-indexer/batchuploadreadreceipts")) { - httpRequest.setObjectField("mUrl", "http://127.0.0.1") + val httpServer = HttpServer() + + context.event.subscribe(NetworkApiRequestEvent::class, { anonymousStoryViewProperty }) { event -> + if (!event.url.endsWith("readreceipt-indexer/batchuploadreadreceipts")) return@subscribe + runBlocking { + suspendCoroutine { + httpServer.ensureServerStarted { + event.url = "http://127.0.0.1:${httpServer.port}" + it.resumeWith(Result.success(Unit)) + } + } } } } diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/ui/StartupPageOverride.kt b/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/ui/StartupPageOverride.kt @@ -3,9 +3,9 @@ package me.rhunk.snapenhance.features.impl.ui import android.annotation.SuppressLint import android.os.Handler import android.view.View -import android.view.ViewGroup import android.widget.LinearLayout import me.rhunk.snapenhance.Constants +import me.rhunk.snapenhance.core.eventbus.events.impl.AddViewEvent import me.rhunk.snapenhance.features.Feature import me.rhunk.snapenhance.features.FeatureLoadParams import me.rhunk.snapenhance.hook.HookStage @@ -38,21 +38,17 @@ class StartupPageOverride : Feature("StartupPageOverride", loadParams = FeatureL } val ngsIconId = context.androidContext.resources.getIdentifier(ngsIconName, "id", Constants.SNAPCHAT_PACKAGE_NAME) - val unhooks = mutableListOf<() -> Unit>() - - ViewGroup::class.java.getMethod( - "addView", - View::class.java, - Int::class.javaPrimitiveType, - ViewGroup.LayoutParams::class.java - ).hook(HookStage.AFTER) { param -> - if (param.thisObject<ViewGroup>() !is LinearLayout) return@hook - with(param.arg<View>(0)) { + + lateinit var unhook: () -> Unit + + context.event.subscribe(AddViewEvent::class) { event -> + if (event.parent !is LinearLayout) return@subscribe + with(event.view) { if (id == ngsIconId) { ngsIcon = this - unhooks.forEach { it() } + unhook() } } - }.also { unhooks.add(it::unhook) } + }.also { unhook = it } } } \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/ui/UITweaks.kt b/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/ui/UITweaks.kt @@ -8,9 +8,9 @@ import android.view.View import android.view.ViewGroup import android.widget.FrameLayout import me.rhunk.snapenhance.Constants +import me.rhunk.snapenhance.core.eventbus.events.impl.AddViewEvent import me.rhunk.snapenhance.features.Feature import me.rhunk.snapenhance.features.FeatureLoadParams -import me.rhunk.snapenhance.hook.HookAdapter import me.rhunk.snapenhance.hook.HookStage import me.rhunk.snapenhance.hook.Hooker import me.rhunk.snapenhance.hook.hook @@ -25,12 +25,12 @@ class UITweaks : Feature("UITweaks", loadParams = FeatureLoadParams.ACTIVITY_CRE } } - private fun hideStorySection(param: HookAdapter) { - val parent = param.thisObject() as ViewGroup + private fun hideStorySection(event: AddViewEvent) { + val parent = event.parent parent.visibility = View.GONE val marginLayoutParams = parent.layoutParams as ViewGroup.MarginLayoutParams marginLayoutParams.setMargins(-99999, -99999, -99999, -99999) - param.setResult(null) + event.canceled = true } @SuppressLint("DiscouragedApi", "InternalInsetResource") @@ -69,33 +69,29 @@ class UITweaks : Feature("UITweaks", loadParams = FeatureLoadParams.ACTIVITY_CRE } } - ViewGroup::class.java.getMethod( - "addView", - View::class.java, - Int::class.javaPrimitiveType, - ViewGroup.LayoutParams::class.java - ).hook(HookStage.BEFORE) { param -> - val view: View = param.arg(0) - val viewId = view.id + + context.event.subscribe(AddViewEvent::class) { event -> + val viewId = event.view.id + val view = event.view if (hideStorySections.contains("hide_for_you")) { if (viewId == getIdentifier("df_large_story", "id") || viewId == getIdentifier("df_promoted_story", "id")) { - hideStorySection(param) - return@hook + hideStorySection(event) + return@subscribe } if (viewId == getIdentifier("stories_load_progress_layout", "id")) { - param.setResult(null) + event.canceled = true } } if (hideStorySections.contains("hide_friends") && viewId == getIdentifier("friend_card_frame", "id")) { - hideStorySection(param) + hideStorySection(event) } //mappings? if (hideStorySections.contains("hide_friend_suggestions") && view.javaClass.superclass?.name?.endsWith("StackDrawLayout") == true) { - val layoutParams = view.layoutParams as? FrameLayout.LayoutParams ?: return@hook + val layoutParams = view.layoutParams as? FrameLayout.LayoutParams ?: return@subscribe if (layoutParams.width == -1 && layoutParams.height == -2 && view.javaClass.let { clazz -> @@ -103,17 +99,17 @@ class UITweaks : Feature("UITweaks", loadParams = FeatureLoadParams.ACTIVITY_CRE clazz.constructors.any { it.parameterCount == 1 && it.parameterTypes[0] == Context::class.java } } ) { - hideStorySection(param) + hideStorySection(event) } } if (hideStorySections.contains("hide_following") && (viewId == getIdentifier("df_small_story", "id")) ) { - hideStorySection(param) + hideStorySection(event) } if (blockAds && viewId == getIdentifier("df_promoted_story", "id")) { - hideStorySection(param) + hideStorySection(event) } if (isImmersiveCamera) { @@ -145,15 +141,15 @@ class UITweaks : Feature("UITweaks", loadParams = FeatureLoadParams.ACTIVITY_CRE view.visibility = View.GONE } if (getIdentifier("chat_input_bar_sharing_drawer_button", "id") == viewId && hiddenElements.contains("hide_live_location_share_button")) { - param.setResult(null) + event.canceled = true } if (viewId == callButton1 || viewId == callButton2) { - if (!hiddenElements.contains("hide_call_buttons")) return@hook - if (view.visibility == View.GONE) return@hook + if (!hiddenElements.contains("hide_call_buttons")) return@subscribe + if (view.visibility == View.GONE) return@subscribe } if (viewId == callButtonsStub) { - if (!hiddenElements.contains("hide_call_buttons")) return@hook - param.setResult(null) + if (!hiddenElements.contains("hide_call_buttons")) return@subscribe + event.canceled = true } } } diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/hook/Hooker.kt b/core/src/main/kotlin/me/rhunk/snapenhance/hook/Hooker.kt @@ -111,8 +111,8 @@ object Hooker { val unhooks: MutableSet<XC_MethodHook.Unhook> = HashSet() hook(clazz, methodName, stage) { param-> if (param.nullableThisObject<Any>() != instance) return@hook + unhooks.forEach { it.unhook() } hookConsumer(param) - unhooks.forEach{ it.unhook() } }.also { unhooks.addAll(it) } } } diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/manager/impl/FeatureManager.kt b/core/src/main/kotlin/me/rhunk/snapenhance/manager/impl/FeatureManager.kt @@ -8,6 +8,7 @@ import me.rhunk.snapenhance.features.impl.AutoUpdater import me.rhunk.snapenhance.features.impl.ConfigurationOverride import me.rhunk.snapenhance.features.impl.Messaging import me.rhunk.snapenhance.features.impl.downloader.MediaDownloader +import me.rhunk.snapenhance.features.impl.downloader.ProfilePictureDownloader import me.rhunk.snapenhance.features.impl.experiments.AmoledDarkMode import me.rhunk.snapenhance.features.impl.experiments.AppPasscode import me.rhunk.snapenhance.features.impl.experiments.DeviceSpooferHook @@ -91,6 +92,7 @@ class FeatureManager(private val context: ModContext) : Manager { register(StartupPageOverride::class) register(GooglePlayServicesDialogs::class) register(NoFriendScoreDelay::class) + register(ProfilePictureDownloader::class) initializeFeatures() } diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/ui/menu/impl/MenuViewInjector.kt b/core/src/main/kotlin/me/rhunk/snapenhance/ui/menu/impl/MenuViewInjector.kt @@ -7,11 +7,10 @@ import android.view.ViewGroup import android.widget.FrameLayout import android.widget.LinearLayout import me.rhunk.snapenhance.Constants +import me.rhunk.snapenhance.core.eventbus.events.impl.AddViewEvent import me.rhunk.snapenhance.features.Feature import me.rhunk.snapenhance.features.FeatureLoadParams import me.rhunk.snapenhance.features.impl.Messaging -import me.rhunk.snapenhance.hook.HookStage -import me.rhunk.snapenhance.hook.Hooker import java.lang.reflect.Modifier @SuppressLint("DiscouragedApi") @@ -42,17 +41,9 @@ class MenuViewInjector : Feature("MenuViewInjector", loadParams = FeatureLoadPar val componentsHolder = context.resources.getIdentifier("components_holder", "id", Constants.SNAPCHAT_PACKAGE_NAME) val feedNewChat = context.resources.getIdentifier("feed_new_chat", "id", Constants.SNAPCHAT_PACKAGE_NAME) - val addViewMethod = ViewGroup::class.java.getMethod( - "addView", - View::class.java, - Int::class.javaPrimitiveType, - ViewGroup.LayoutParams::class.java - ) - - Hooker.hook(addViewMethod, HookStage.BEFORE) { param -> - val viewGroup: ViewGroup = param.thisObject() + context.event.subscribe(AddViewEvent::class) { event -> val originalAddView: (View) -> Unit = { - param.invokeOriginal(arrayOf(it, -1, + event.adapter.invokeOriginal(arrayOf(it, -1, FrameLayout.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT @@ -60,19 +51,20 @@ class MenuViewInjector : Feature("MenuViewInjector", loadParams = FeatureLoadPar ) } - val childView: View = param.arg(0) - operaContextActionMenu.inject(viewGroup, childView) + val viewGroup: ViewGroup = event.parent + val childView: View = event.view + operaContextActionMenu.inject(event.parent, childView) - if (viewGroup.id == componentsHolder && childView.id == feedNewChat) { - settingsGearInjector.inject(viewGroup, childView) - return@hook + if (event.parent.id == componentsHolder && childView.id == feedNewChat) { + settingsGearInjector.inject(event.parent, childView) + return@subscribe } //download in chat snaps and notes from the chat action menu if (viewGroup.javaClass.name.endsWith("ActionMenuChatItemContainer")) { - if (viewGroup.parent == null || viewGroup.parent.parent == null) return@hook + if (viewGroup.parent == null || viewGroup.parent.parent == null) return@subscribe chatActionMenu.inject(viewGroup) - return@hook + return@subscribe } //TODO: inject in group chat menus @@ -101,7 +93,7 @@ class MenuViewInjector : Feature("MenuViewInjector", loadParams = FeatureLoadPar viewList.reversed().forEach { injectedLayout.addView(it, 0) } } - param.setArg(0, injectedLayout) + event.view = injectedLayout } if (viewGroup is LinearLayout && viewGroup.id == actionSheetItemsContainerLayoutId) { @@ -125,12 +117,12 @@ class MenuViewInjector : Feature("MenuViewInjector", loadParams = FeatureLoadPar //context.config.writeConfig() } }) - return@hook + return@subscribe } - if (messaging.lastFetchConversationUUID == null || messaging.lastFetchConversationUserUUID == null) return@hook + if (messaging.lastFetchConversationUUID == null || messaging.lastFetchConversationUserUUID == null) return@subscribe //filter by the slot index - if (viewGroup.getChildCount() != context.config.userInterface.friendFeedMenuPosition.get()) return@hook + if (viewGroup.getChildCount() != context.config.userInterface.friendFeedMenuPosition.get()) return@subscribe friendFeedInfoMenu.inject(viewGroup, originalAddView) } } diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/util/download/DownloadServer.kt b/core/src/main/kotlin/me/rhunk/snapenhance/util/download/DownloadServer.kt @@ -1,137 +0,0 @@ -package me.rhunk.snapenhance.util.download - -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch -import me.rhunk.snapenhance.Logger -import java.io.BufferedReader -import java.io.InputStream -import java.io.InputStreamReader -import java.io.PrintWriter -import java.net.ServerSocket -import java.net.Socket -import java.net.SocketException -import java.util.Locale -import java.util.StringTokenizer -import java.util.concurrent.ConcurrentHashMap -import kotlin.random.Random - -class DownloadServer( - private val timeout: Int = 10000 -) { - private val port = Random.nextInt(10000, 65535) - - private val coroutineScope = CoroutineScope(Dispatchers.IO) - private var timeoutJob: Job? = null - private var socketJob: Job? = null - - private val cachedData = ConcurrentHashMap<String, Pair<InputStream, Long>>() - private var serverSocket: ServerSocket? = null - - fun ensureServerStarted(callback: DownloadServer.() -> Unit) { - if (serverSocket != null && !serverSocket!!.isClosed) { - callback(this) - return - } - - coroutineScope.launch(Dispatchers.IO) { - Logger.debug("starting download server on port $port") - serverSocket = ServerSocket(port) - callback(this@DownloadServer) - while (!serverSocket!!.isClosed) { - try { - val socket = serverSocket!!.accept() - timeoutJob?.cancel() - launch { - handleRequest(socket) - timeoutJob = launch { - delay(timeout.toLong()) - Logger.debug("download server closed due to timeout") - runCatching { - socketJob?.cancel() - socket.close() - serverSocket?.close() - }.onFailure { - Logger.error(it) - } - } - } - } catch (e: SocketException) { - Logger.debug("download server timed out") - break; - } catch (e: Throwable) { - Logger.error("failed to handle request", e) - } - } - }.also { socketJob = it } - } - - fun close() { - serverSocket?.close() - } - - fun putDownloadableContent(inputStream: InputStream, size: Long): String { - val key = System.nanoTime().toString(16) - cachedData[key] = inputStream to size - return "http://127.0.0.1:$port/$key" - } - - private fun handleRequest(socket: Socket) { - val reader = BufferedReader(InputStreamReader(socket.getInputStream())) - val outputStream = socket.getOutputStream() - val writer = PrintWriter(outputStream) - val line = reader.readLine() ?: return - fun close() { - runCatching { - reader.close() - writer.close() - outputStream.close() - socket.close() - }.onFailure { - Logger.error("failed to close socket", it) - } - } - val parse = StringTokenizer(line) - val method = parse.nextToken().uppercase(Locale.getDefault()) - var fileRequested = parse.nextToken().lowercase(Locale.getDefault()) - if (method != "GET") { - with(writer) { - println("HTTP/1.1 501 Not Implemented") - println("Content-type: " + "application/octet-stream") - println("Content-length: " + 0) - println() - flush() - } - close() - return - } - if (fileRequested.startsWith("/")) { - fileRequested = fileRequested.substring(1) - } - if (!cachedData.containsKey(fileRequested)) { - with(writer) { - println("HTTP/1.1 404 Not Found") - println("Content-type: " + "application/octet-stream") - println("Content-length: " + 0) - println() - flush() - } - close() - return - } - val requestedData = cachedData[fileRequested]!! - with(writer) { - println("HTTP/1.1 200 OK") - println("Content-type: " + "application/octet-stream") - println("Content-length: " + requestedData.second) - println() - flush() - } - requestedData.first.copyTo(outputStream) - outputStream.flush() - cachedData.remove(fileRequested) - close() - } -}- \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/util/download/HttpServer.kt b/core/src/main/kotlin/me/rhunk/snapenhance/util/download/HttpServer.kt @@ -0,0 +1,139 @@ +package me.rhunk.snapenhance.util.download + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import me.rhunk.snapenhance.Logger +import java.io.BufferedReader +import java.io.InputStream +import java.io.InputStreamReader +import java.io.PrintWriter +import java.net.ServerSocket +import java.net.Socket +import java.net.SocketException +import java.util.Locale +import java.util.StringTokenizer +import java.util.concurrent.ConcurrentHashMap +import kotlin.random.Random + +class HttpServer( + private val timeout: Int = 10000 +) { + val port = Random.nextInt(10000, 65535) + + private val coroutineScope = CoroutineScope(Dispatchers.IO) + private var timeoutJob: Job? = null + private var socketJob: Job? = null + + private val cachedData = ConcurrentHashMap<String, Pair<InputStream, Long>>() + private var serverSocket: ServerSocket? = null + + fun ensureServerStarted(callback: HttpServer.() -> Unit) { + if (serverSocket != null && !serverSocket!!.isClosed) { + callback(this) + return + } + + coroutineScope.launch(Dispatchers.IO) { + Logger.debug("starting http server on port $port") + serverSocket = ServerSocket(port) + callback(this@HttpServer) + while (!serverSocket!!.isClosed) { + try { + val socket = serverSocket!!.accept() + timeoutJob?.cancel() + launch { + handleRequest(socket) + timeoutJob = launch { + delay(timeout.toLong()) + Logger.debug("http server closed due to timeout") + runCatching { + socketJob?.cancel() + socket.close() + serverSocket?.close() + }.onFailure { + Logger.error(it) + } + } + } + } catch (e: SocketException) { + Logger.debug("http server timed out") + break; + } catch (e: Throwable) { + Logger.error("failed to handle request", e) + } + } + }.also { socketJob = it } + } + + fun close() { + serverSocket?.close() + } + + fun putDownloadableContent(inputStream: InputStream, size: Long): String { + val key = System.nanoTime().toString(16) + cachedData[key] = inputStream to size + return "http://127.0.0.1:$port/$key" + } + + private fun handleRequest(socket: Socket) { + val reader = BufferedReader(InputStreamReader(socket.getInputStream())) + val outputStream = socket.getOutputStream() + val writer = PrintWriter(outputStream) + val line = reader.readLine() ?: return + fun close() { + runCatching { + reader.close() + writer.close() + outputStream.close() + socket.close() + }.onFailure { + Logger.error("failed to close socket", it) + } + } + val parse = StringTokenizer(line) + val method = parse.nextToken().uppercase(Locale.getDefault()) + var fileRequested = parse.nextToken().lowercase(Locale.getDefault()) + Logger.debug("[http-server:${port}] $method $fileRequested") + + if (method != "GET") { + with(writer) { + println("HTTP/1.1 501 Not Implemented") + println("Content-type: " + "application/octet-stream") + println("Content-length: " + 0) + println() + flush() + } + close() + return + } + if (fileRequested.startsWith("/")) { + fileRequested = fileRequested.substring(1) + } + if (!cachedData.containsKey(fileRequested)) { + with(writer) { + println("HTTP/1.1 404 Not Found") + println("Content-type: " + "application/octet-stream") + println("Content-length: " + 0) + println() + flush() + } + close() + return + } + val requestedData = cachedData[fileRequested]!! + with(writer) { + println("HTTP/1.1 200 OK") + println("Content-type: " + "application/octet-stream") + println("Content-length: " + requestedData.second) + println() + flush() + } + requestedData.first.copyTo(outputStream) + outputStream.flush() + cachedData.remove(fileRequested) + close() + } +}+ \ No newline at end of file