diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/EventDispatcher.kt b/core/src/main/kotlin/me/rhunk/snapenhance/EventDispatcher.kt index 2c19a744..d06f8bae 100644 --- 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(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 index 025cc1f8..14579d82 100644 --- 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 index 54da7c1e..6a3cb91b 100644 --- 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 new file mode 100644 index 00000000..aff213cb --- /dev/null +++ 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 new file mode 100644 index 00000000..a64461c7 --- /dev/null +++ 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 index 33d8f114..b84096b1 100644 --- 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() - - 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 index f3be03d1..1233b1ad 100644 --- 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 index 288ab333..c4d11366 100644 --- 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 { 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 new file mode 100644 index 00000000..a0c06a61 --- /dev/null +++ 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() + 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(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 index 257e69d5..c5566e22 100644 --- 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 index bc5211f8..7cf52d10 100644 --- 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 index 11ae48dc..4c4cc6aa 100644 --- 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() !is LinearLayout) return@hook - with(param.arg(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 index dee2ec67..474290e0 100644 --- 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 index 052d4e0e..ed3c5cfc 100644 --- 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 = HashSet() hook(clazz, methodName, stage) { param-> if (param.nullableThisObject() != 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 index 2e877021..cf463bb7 100644 --- 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 index d5191de0..bd2002ea 100644 --- 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/HttpServer.kt similarity index 91% rename from core/src/main/kotlin/me/rhunk/snapenhance/util/download/DownloadServer.kt rename to core/src/main/kotlin/me/rhunk/snapenhance/util/download/HttpServer.kt index 509d0074..02e663ed 100644 --- a/core/src/main/kotlin/me/rhunk/snapenhance/util/download/DownloadServer.kt +++ b/core/src/main/kotlin/me/rhunk/snapenhance/util/download/HttpServer.kt @@ -18,10 +18,10 @@ import java.util.StringTokenizer import java.util.concurrent.ConcurrentHashMap import kotlin.random.Random -class DownloadServer( +class HttpServer( private val timeout: Int = 10000 ) { - private val port = Random.nextInt(10000, 65535) + val port = Random.nextInt(10000, 65535) private val coroutineScope = CoroutineScope(Dispatchers.IO) private var timeoutJob: Job? = null @@ -30,16 +30,16 @@ class DownloadServer( private val cachedData = ConcurrentHashMap>() private var serverSocket: ServerSocket? = null - fun ensureServerStarted(callback: DownloadServer.() -> Unit) { + fun ensureServerStarted(callback: HttpServer.() -> Unit) { if (serverSocket != null && !serverSocket!!.isClosed) { callback(this) return } coroutineScope.launch(Dispatchers.IO) { - Logger.debug("starting download server on port $port") + Logger.debug("starting http server on port $port") serverSocket = ServerSocket(port) - callback(this@DownloadServer) + callback(this@HttpServer) while (!serverSocket!!.isClosed) { try { val socket = serverSocket!!.accept() @@ -48,7 +48,7 @@ class DownloadServer( handleRequest(socket) timeoutJob = launch { delay(timeout.toLong()) - Logger.debug("download server closed due to timeout") + Logger.debug("http server closed due to timeout") runCatching { socketJob?.cancel() socket.close() @@ -59,7 +59,7 @@ class DownloadServer( } } } catch (e: SocketException) { - Logger.debug("download server timed out") + Logger.debug("http server timed out") break; } catch (e: Throwable) { Logger.error("failed to handle request", e) @@ -96,6 +96,8 @@ class DownloadServer( 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")