commit d31591fd4753f9611b98e1bd1b68d8b908736f47
parent bf49bcbe11bb2bbbcb445ce6447e1cf16e663b7d
Author: rhunk <101876869+rhunk@users.noreply.github.com>
Date:   Sun, 30 Jun 2024 23:58:28 +0200

refactor: feature load system

Signed-off-by: rhunk <101876869+rhunk@users.noreply.github.com>

Diffstat:
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/ModContext.kt | 4++--
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/SnapEnhance.kt | 10+++++-----
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/features/BridgeFileFeature.kt | 2+-
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/features/Feature.kt | 46++++++++++++++++++++++++++--------------------
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/features/FeatureManager.kt | 79++++++++++++++++++++++++++-----------------------------------------------------
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/features/MessagingRuleFeature.kt | 2+-
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/COFOverride.kt | 4++--
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/ConfigurationOverride.kt | 4++--
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/FriendMutationObserver.kt | 4++--
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/MixerStories.kt | 4++--
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/OperaViewerParamsOverride.kt | 57+++++++++++++++++++++++++++++----------------------------
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/ScopeSync.kt | 3+--
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/downloader/MediaDownloader.kt | 76++++++++++++++++++++++++++++++++++++++--------------------------------------
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/downloader/ProfilePictureDownloader.kt | 83++++++++++++++++++++++++++++++++++++++++---------------------------------------
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/experiments/AccountSwitcher.kt | 29+++++++++++++----------------
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/experiments/AddFriendSourceSpoof.kt | 95++++++++++++++++++++++++++++++++++++++++---------------------------------------
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/experiments/AppLock.kt | 45+++++++++++++++++++++++----------------------
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/experiments/AutoOpenSnaps.kt | 3+--
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/experiments/BestFriendPinning.kt | 3+--
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/experiments/BetterLocation.kt | 3+--
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/experiments/BetterTranscript.kt | 98++++++++++++++++++++++++++++++++++++++++---------------------------------------
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/experiments/CallRecorder.kt | 3+--
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/experiments/ComposerHooks.kt | 3+--
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/experiments/ContextMenuFix.kt | 3+--
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/experiments/ConvertMessageLocally.kt | 15++++++++-------
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/experiments/DeviceSpooferHook.kt | 3+--
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/experiments/EndToEndEncryption.kt | 401+++++++++++++++++++++++++++++++++++++++----------------------------------------
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/experiments/InfiniteStoryBoost.kt | 21+++++++++++----------
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/experiments/MediaFilePicker.kt | 321++++++++++++++++++++++++++++++++++++++++---------------------------------------
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/experiments/MeoPasscodeBypass.kt | 21+++++++++++----------
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/experiments/NoFriendScoreDelay.kt | 21+++++++++++----------
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/experiments/PreventForcedLogout.kt | 3+--
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/global/BypassVideoLengthRestriction.kt | 89++++++++++++++++++++++++++++++++++++++++---------------------------------------
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/global/DisableCustomTabs.kt | 15++++++++-------
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/global/DisableMemoriesSnapFeed.kt | 22+++++++++++-----------
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/global/DisableMetrics.kt | 3+--
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/global/DisableTelecomFramework.kt | 3+--
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/global/GooglePlayServicesDialogs.kt | 19++++++++++---------
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/global/MediaUploadQualityOverride.kt | 3+--
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/global/SnapchatPlus.kt | 3+--
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/messaging/AutoMarkAsRead.kt | 3+--
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/messaging/AutoSave.kt | 57+++++++++++++++++++++++++++++----------------------------
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/messaging/BypassMessageActionRestrictions.kt | 15++++++++-------
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/messaging/CallStartConfirmation.kt | 53+++++++++++++++++++++++++++--------------------------
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/messaging/DisableReplayInFF.kt | 19++++++++++---------
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/messaging/Messaging.kt | 154+++++++++++++++++++++++++++++++++++++++----------------------------------------
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/messaging/Notifications.kt | 5++---
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/messaging/PreventMessageSending.kt | 5++---
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/messaging/SendOverride.kt | 3+--
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/messaging/UnlimitedSnapViewTime.kt | 34+++++++++++++++++-----------------
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/spying/FriendTracker.kt | 3+--
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/spying/HalfSwipeNotifier.kt | 3+--
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/spying/MessageLogger.kt | 32++++++++++----------------------
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/spying/StealthMode.kt | 3+--
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/tweaks/BypassScreenshotDetection.kt | 5++---
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/tweaks/CameraTweaks.kt | 133++++++++++++++++++++++++++++++++++++++++---------------------------------------
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/tweaks/DisablePermissionRequests.kt | 3+--
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/tweaks/HideActiveMusic.kt | 11++++++-----
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/tweaks/PreventMessageListAutoScroll.kt | 89++++++++++++++++++++++++++++++++++++++++---------------------------------------
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/tweaks/RemoveGroupsLockedStatus.kt | 13+++++++------
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/tweaks/UnsaveableMessages.kt | 4+---
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/ui/ClientBootstrapOverride.kt | 3+--
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/ui/ConversationToolbox.kt | 77+++++++++++++++++++++++++++++++++++++++--------------------------------------
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/ui/CustomStreaksExpirationFormat.kt | 61+++++++++++++++++++++++++++++++------------------------------
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/ui/CustomizeUI.kt | 5++---
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/ui/DefaultVolumeControls.kt | 15++++++++-------
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/ui/DisableConfirmationDialogs.kt | 83++++++++++++++++++++++++++++++++++++++++---------------------------------------
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/ui/EditTextOverride.kt | 33+++++++++++++++++----------------
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/ui/FriendFeedMessagePreview.kt | 132++++++++++++++++++++++++++++++++++++++++---------------------------------------
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/ui/HideFriendFeedEntry.kt | 4++--
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/ui/HideQuickAddFriendFeed.kt | 19++++++++++---------
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/ui/HideStreakRestore.kt | 3+--
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/ui/MessageIndicators.kt | 187++++++++++++++++++++++++++++++++++++++++---------------------------------------
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/ui/OldBitmojiSelfie.kt | 3+--
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/ui/PinConversations.kt | 5++---
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/ui/SnapPreview.kt | 69+++++++++++++++++++++++++++++++++------------------------------------
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/ui/SpotlightCommentsUsername.kt | 53+++++++++++++++++++++++++++--------------------------
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/ui/StealthModeIndicator.kt | 74++++++++++++++++++++++++++++++++++++++------------------------------------
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/ui/UITweaks.kt | 11++++++++---
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/ui/ViewAppearanceHelper.kt | 5+++--
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/ui/menu/MenuViewInjector.kt | 187++++++++++++++++++++++++++++++++++++++++---------------------------------------
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/ui/menu/impl/FriendFeedInfoMenu.kt | 5+++--
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/ui/menu/impl/OperaContextActionMenu.kt | 3++-
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/ui/menu/impl/SettingsGearInjector.kt | 5+++--
84 files changed, 1642 insertions(+), 1673 deletions(-)

diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/ModContext.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/ModContext.kt @@ -79,7 +79,7 @@ class ModContext( } fun runOnUiThread(runnable: () -> Unit) { - if (Looper.myLooper() == Looper.getMainLooper()) { + if (Looper.getMainLooper().isCurrentThread) { runnable() return } @@ -95,7 +95,7 @@ class ModContext( runCatching { runnable() }.onFailure { - longToast("Async task failed " + it.message) + longToast("Async task failed: " + it.message) log.error("Async task failed", it) } } diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/SnapEnhance.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/SnapEnhance.kt @@ -106,7 +106,7 @@ class SnapEnhance { appContext.mainActivity = this if (!appContext.mappings.isMappingsLoaded) return@hookMainActivity appContext.isMainActivityPaused = false - onActivityCreate() + onActivityCreate(this) appContext.actionManager.onNewIntent(intent) } @@ -166,12 +166,12 @@ class SnapEnhance { } } - private fun onActivityCreate() { + private fun onActivityCreate(activity: Activity) { measureTimeMillis { with(appContext) { - features.onActivityCreate() - inAppOverlay.onActivityCreate(mainActivity!!) - scriptRuntime.eachModule { callFunction("module.onSnapMainActivityCreate", mainActivity!!) } + features.onActivityCreate(activity) + inAppOverlay.onActivityCreate(activity) + scriptRuntime.eachModule { callFunction("module.onSnapMainActivityCreate", activity) } actionManager.onActivityCreate() } }.also { time -> diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/BridgeFileFeature.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/BridgeFileFeature.kt @@ -9,7 +9,7 @@ import java.io.BufferedReader import java.io.InputStreamReader import java.nio.charset.StandardCharsets -abstract class BridgeFileFeature(name: String, private val bridgeFileType: InternalFileHandleType, loadParams: Int) : Feature(name, loadParams) { +abstract class BridgeFileFeature(name: String, private val bridgeFileType: InternalFileHandleType) : Feature(name) { private val fileLines = mutableListOf<String>() private val fileWrapper by mappedLazyBridge(LazyBridgeValue({ context.fileHandlerManager.getFileHandle(FileHandleScope.INTERNAL.key, bridgeFileType.key)!! }), map = { it.toWrapper() }) diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/Feature.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/Feature.kt @@ -1,33 +1,39 @@ package me.rhunk.snapenhance.core.features +import android.app.Activity +import kotlinx.coroutines.launch import me.rhunk.snapenhance.core.ModContext abstract class Feature( - val featureKey: String, - val loadParams: Int = FeatureLoadParams.INIT_SYNC + val key: String ) { lateinit var context: ModContext + lateinit var registerNextActivityCallback: ((Activity) -> Unit) -> Unit + + protected fun defer(block: () -> Unit) { + context.coroutineScope.launch { + runCatching { + block() + }.onFailure { + context.log.error("Failed to run onNextActivityCreate callback", it) + } + } + } - /** - * called on the main thread when the mod initialize - */ - open fun init() {} - - /** - * called on a dedicated thread when the mod initialize - */ - open fun asyncInit() {} - - /** - * called when the Snapchat Activity is created - */ - open fun onActivityCreate() {} + protected fun onNextActivityCreate(defer: Boolean = false, block: (Activity) -> Unit) { + if (defer) { + registerNextActivityCallback { + defer { + block(it) + } + } + return + } + registerNextActivityCallback(block) + } + open fun init() {} - /** - * called on a dedicated thread when the Snapchat Activity is created - */ - open fun asyncOnActivityCreate() {} protected fun findClass(name: String): Class<*> { return context.androidContext.classLoader.loadClass(name) diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/FeatureManager.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/FeatureManager.kt @@ -1,5 +1,6 @@ package me.rhunk.snapenhance.core.features +import android.app.Activity import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking @@ -25,6 +26,7 @@ class FeatureManager( private val context: ModContext ) { private val features = mutableMapOf<KClass<out Feature>, Feature>() + private val onActivityCreateListeners = mutableListOf<(Activity) -> Unit>() private fun register(vararg featureList: Feature) { if (context.bridgeClient.getDebugProp("disable_feature_loading") == "true") { @@ -37,11 +39,12 @@ class FeatureManager( launch(Dispatchers.IO) { runCatching { feature.context = context + feature.registerNextActivityCallback = { block -> onActivityCreateListeners.add(block) } synchronized(features) { features[feature::class] = feature } }.onFailure { - CoreLogger.xposedLog("Failed to register feature ${feature.featureKey}", it) + CoreLogger.xposedLog("Failed to register feature ${feature.key}", it) } } } @@ -132,65 +135,35 @@ class FeatureManager( DisableTelecomFramework(), BetterTranscript(), ) - initializeFeatures() - } - - private inline fun tryInit(feature: Feature, crossinline block: () -> Unit) { - runCatching { - block() - }.onFailure { - context.log.error("Failed to init feature ${feature.featureKey}", it) - context.longToast("Failed to init feature ${feature.featureKey}! Check logcat for more details.") - } - } - - private fun initFeatures( - syncParam: Int, - asyncParam: Int, - syncAction: (Feature) -> Unit, - asyncAction: (Feature) -> Unit - ) { features.values.toList().forEach { feature -> - if (feature.loadParams and syncParam != 0) { - tryInit(feature) { - syncAction(feature) + runCatching { + measureTimeMillis { + feature.init() + }.also { + context.log.verbose("Feature ${feature.key} initialized in $it ms") } + }.onFailure { + context.log.error("Failed to init feature ${feature.key}", it) + context.longToast("Failed to init feature ${feature.key}! Check logcat for more details.") } - if (feature.loadParams and asyncParam != 0) { - context.coroutineScope.launch { - tryInit(feature) { - asyncAction(feature) - } - } - } - } - } - - private fun initializeFeatures() { - //TODO: async called when all features are initiated ? - measureTimeMillis { - initFeatures( - FeatureLoadParams.INIT_SYNC, - FeatureLoadParams.INIT_ASYNC, - Feature::init, - Feature::asyncInit - ) - }.also { - context.log.verbose("feature manager init took $it ms") } } - fun onActivityCreate() { - measureTimeMillis { - initFeatures( - FeatureLoadParams.ACTIVITY_CREATE_SYNC, - FeatureLoadParams.ACTIVITY_CREATE_ASYNC, - Feature::onActivityCreate, - Feature::asyncOnActivityCreate - ) - }.also { - context.log.verbose("feature manager onActivityCreate took $it ms") + fun onActivityCreate(activity: Activity) { + context.log.verbose("Activity created: ${activity.javaClass.simpleName}") + onActivityCreateListeners.toList().also { + onActivityCreateListeners.clear() + }.forEach { activityListener -> + measureTimeMillis { + runCatching { + activityListener(activity) + }.onFailure { + context.log.error("Failed to run activity listener ${activityListener::class.simpleName}", it) + } + }.also { + context.log.verbose("Activity listener ${activityListener::class.simpleName} executed in $it ms") + } } } } diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/MessagingRuleFeature.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/MessagingRuleFeature.kt @@ -3,7 +3,7 @@ package me.rhunk.snapenhance.core.features import me.rhunk.snapenhance.common.data.MessagingRuleType import me.rhunk.snapenhance.common.data.RuleState -abstract class MessagingRuleFeature(name: String, val ruleType: MessagingRuleType, loadParams: Int = 0) : Feature(name, loadParams) { +abstract class MessagingRuleFeature(name: String, val ruleType: MessagingRuleType) : Feature(name) { private val listeners = mutableListOf<(String, Boolean) -> Unit>() fun addStateListener(listener: (conversationId: String, newState: Boolean) -> Unit) { diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/COFOverride.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/COFOverride.kt @@ -1,7 +1,7 @@ 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.dataBuilder import me.rhunk.snapenhance.core.util.hook.HookStage import me.rhunk.snapenhance.core.util.hook.hook @@ -9,7 +9,7 @@ import me.rhunk.snapenhance.core.util.ktx.getObjectField import me.rhunk.snapenhance.mapper.impl.COFObservableMapper import java.lang.reflect.Method -class COFOverride : Feature("COF Override", loadParams = FeatureLoadParams.INIT_SYNC) { +class COFOverride : Feature("COF Override") { var hasActionMenuV2 = false override fun init() { diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/ConfigurationOverride.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/ConfigurationOverride.kt @@ -2,7 +2,7 @@ package me.rhunk.snapenhance.core.features.impl import de.robv.android.xposed.XposedHelpers 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.Hooker import me.rhunk.snapenhance.core.util.hook.hook @@ -22,7 +22,7 @@ data class ConfigFilter( val isAppExperiment: Boolean? ) -class ConfigurationOverride : Feature("Configuration Override", loadParams = FeatureLoadParams.INIT_SYNC) { +class ConfigurationOverride : Feature("Configuration Override") { override fun init() { context.mappings.useMapper(CompositeConfigurationProviderMapper::class) { fun getConfigKeyInfo(key: Any?) = runCatching { diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/FriendMutationObserver.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/FriendMutationObserver.kt @@ -10,12 +10,12 @@ import me.rhunk.snapenhance.common.data.FriendLinkType import me.rhunk.snapenhance.common.database.impl.FriendInfo import me.rhunk.snapenhance.core.event.events.impl.NetworkApiRequestEvent import me.rhunk.snapenhance.core.features.Feature -import me.rhunk.snapenhance.core.features.FeatureLoadParams + import me.rhunk.snapenhance.core.util.EvictingMap import java.io.InputStreamReader import java.util.Calendar -class FriendMutationObserver: Feature("FriendMutationObserver", loadParams = FeatureLoadParams.INIT_SYNC) { +class FriendMutationObserver: Feature("FriendMutationObserver") { private val translation by lazy { context.translation.getCategory("friend_mutation_observer") } private val addSourceCache = EvictingMap<String, String>(500) diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/MixerStories.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/MixerStories.kt @@ -7,13 +7,13 @@ import me.rhunk.snapenhance.common.data.StoryData import me.rhunk.snapenhance.common.util.protobuf.ProtoEditor import me.rhunk.snapenhance.core.event.events.impl.NetworkApiRequestEvent import me.rhunk.snapenhance.core.features.Feature -import me.rhunk.snapenhance.core.features.FeatureLoadParams + import java.nio.ByteBuffer import kotlin.coroutines.suspendCoroutine import kotlin.io.encoding.Base64 import kotlin.io.encoding.ExperimentalEncodingApi -class MixerStories : Feature("MixerStories", loadParams = FeatureLoadParams.INIT_SYNC) { +class MixerStories : Feature("MixerStories") { @OptIn(ExperimentalEncodingApi::class) override fun init() { val disableDiscoverSections by context.config.global.disableStorySections diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/OperaViewerParamsOverride.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/OperaViewerParamsOverride.kt @@ -1,12 +1,11 @@ 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 import me.rhunk.snapenhance.mapper.impl.OperaViewerParamsMapper -class OperaViewerParamsOverride : Feature("OperaViewerParamsOverride", loadParams = FeatureLoadParams.ACTIVITY_CREATE_SYNC) { +class OperaViewerParamsOverride : Feature("OperaViewerParamsOverride") { var currentPlaybackRate = 1.0F data class OverrideKey( @@ -19,7 +18,7 @@ class OperaViewerParamsOverride : Feature("OperaViewerParamsOverride", loadParam val value: (key: OverrideKey, value: Any?) -> Any? ) - override fun onActivityCreate() { + override fun init() { val overrideMap = mutableMapOf<String, Override>() fun overrideParam(key: String, filter: (value: Any?) -> Boolean, value: (overrideKey: OverrideKey, value: Any?) -> Any?) { @@ -44,37 +43,39 @@ class OperaViewerParamsOverride : Feature("OperaViewerParamsOverride", loadParam }) } - context.mappings.useMapper(OperaViewerParamsMapper::class) { - fun overrideParamResult(paramKey: Any, value: Any?): Any? { - val fields = paramKey::class.java.fields - val key = OverrideKey( - name = fields.firstOrNull { - it.type == String::class.java - }?.get(paramKey)?.toString() ?: return value, - defaultValue = fields.firstOrNull { - it.type == Object::class.java - }?.get(paramKey) - ) + onNextActivityCreate { + context.mappings.useMapper(OperaViewerParamsMapper::class) { + fun overrideParamResult(paramKey: Any, value: Any?): Any? { + val fields = paramKey::class.java.fields + val key = OverrideKey( + name = fields.firstOrNull { + it.type == String::class.java + }?.get(paramKey)?.toString() ?: return value, + defaultValue = fields.firstOrNull { + it.type == Object::class.java + }?.get(paramKey) + ) - overrideMap[key.name]?.let { override -> - if (override.filter(value)) { - runCatching { - return override.value(key, value) - }.onFailure { - context.log.error("Failed to override param $key", it) + overrideMap[key.name]?.let { override -> + if (override.filter(value)) { + runCatching { + return override.value(key, value) + }.onFailure { + context.log.error("Failed to override param $key", it) + } } } - } - return value - } + return value + } - classReference.get()?.hook(getMethod.get()!!, HookStage.AFTER) { param -> - param.setResult(overrideParamResult(param.arg(0), param.getResult())) - } + classReference.get()?.hook(getMethod.get()!!, HookStage.AFTER) { param -> + param.setResult(overrideParamResult(param.arg(0), param.getResult())) + } - classReference.get()?.hook(getOrDefaultMethod.get()!!, HookStage.AFTER) { param -> - param.setResult(overrideParamResult(param.arg(0), param.getResult())) + classReference.get()?.hook(getOrDefaultMethod.get()!!, HookStage.AFTER) { param -> + param.setResult(overrideParamResult(param.arg(0), param.getResult())) + } } } } diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/ScopeSync.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/ScopeSync.kt @@ -7,9 +7,8 @@ import me.rhunk.snapenhance.common.data.ContentType import me.rhunk.snapenhance.common.data.SocialScope import me.rhunk.snapenhance.core.event.events.impl.SendMessageWithContentEvent import me.rhunk.snapenhance.core.features.Feature -import me.rhunk.snapenhance.core.features.FeatureLoadParams -class ScopeSync : Feature("Scope Sync", loadParams = FeatureLoadParams.INIT_SYNC) { +class ScopeSync : Feature("Scope Sync") { companion object { private const val DELAY_BEFORE_SYNC = 2000L } diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/downloader/MediaDownloader.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/downloader/MediaDownloader.kt @@ -31,7 +31,6 @@ import me.rhunk.snapenhance.common.util.snap.MediaDownloaderHelper import me.rhunk.snapenhance.common.util.snap.RemoteMediaResolver import me.rhunk.snapenhance.core.DownloadManagerClient import me.rhunk.snapenhance.core.SnapEnhance -import me.rhunk.snapenhance.core.features.FeatureLoadParams import me.rhunk.snapenhance.core.features.MessagingRuleFeature import me.rhunk.snapenhance.core.features.impl.downloader.decoder.DecodedAttachment import me.rhunk.snapenhance.core.features.impl.downloader.decoder.MessageDecoder @@ -64,8 +63,7 @@ class SnapChapterInfo( ) -@OptIn(ExperimentalEncodingApi::class) -class MediaDownloader : MessagingRuleFeature("MediaDownloader", MessagingRuleType.AUTO_DOWNLOAD, loadParams = FeatureLoadParams.ACTIVITY_CREATE_ASYNC) { +class MediaDownloader : MessagingRuleFeature("MediaDownloader", MessagingRuleType.AUTO_DOWNLOAD) { private var lastSeenMediaInfoMap: MutableMap<SplitMediaAssetType, MediaInfo>? = null var lastSeenMapParams: ParamMap? = null private set @@ -487,50 +485,52 @@ class MediaDownloader : MessagingRuleFeature("MediaDownloader", MessagingRuleTyp return options.any { keyFilter == null || it.contains(keyFilter, true) } } - override fun asyncOnActivityCreate() { - context.mappings.useMapper(OperaPageViewControllerMapper::class) { - val onOperaViewStateCallback: (HookAdapter) -> Unit = onOperaViewStateCallback@{ param -> - val viewState = (param.thisObject() as Any).getObjectField(viewStateField.get()!!).toString() - if (viewState != "FULLY_DISPLAYED") { - return@onOperaViewStateCallback - } - val operaLayerList = (param.thisObject() as Any).getObjectField(layerListField.get()!!) as ArrayList<*> - val mediaParamMap: ParamMap = operaLayerList.map { Layer(it) }.first().paramMap + override fun init() { + onNextActivityCreate { + context.mappings.useMapper(OperaPageViewControllerMapper::class) { + val onOperaViewStateCallback: (HookAdapter) -> Unit = onOperaViewStateCallback@{ param -> + val viewState = (param.thisObject() as Any).getObjectField(viewStateField.get()!!).toString() + if (viewState != "FULLY_DISPLAYED") { + return@onOperaViewStateCallback + } + val operaLayerList = (param.thisObject() as Any).getObjectField(layerListField.get()!!) as ArrayList<*> + val mediaParamMap: ParamMap = operaLayerList.map { Layer(it) }.first().paramMap - if (!mediaParamMap.containsKey("image_media_info") && !mediaParamMap.containsKey("video_media_info_list")) - return@onOperaViewStateCallback + if (!mediaParamMap.containsKey("image_media_info") && !mediaParamMap.containsKey("video_media_info_list")) + return@onOperaViewStateCallback - val mediaInfoMap = mutableMapOf<SplitMediaAssetType, MediaInfo>() - val isVideo = mediaParamMap.containsKey("video_media_info_list") + val mediaInfoMap = mutableMapOf<SplitMediaAssetType, MediaInfo>() + val isVideo = mediaParamMap.containsKey("video_media_info_list") - mediaInfoMap[SplitMediaAssetType.ORIGINAL] = MediaInfo( - (if (isVideo) mediaParamMap["video_media_info_list"] else mediaParamMap["image_media_info"])!! - ) + mediaInfoMap[SplitMediaAssetType.ORIGINAL] = MediaInfo( + (if (isVideo) mediaParamMap["video_media_info_list"] else mediaParamMap["image_media_info"])!! + ) - if (context.config.downloader.mergeOverlays.get() && mediaParamMap.containsKey("overlay_image_media_info")) { - mediaInfoMap[SplitMediaAssetType.OVERLAY] = - MediaInfo(mediaParamMap["overlay_image_media_info"]!!) - } - lastSeenMapParams = mediaParamMap - lastSeenMediaInfoMap = mediaInfoMap + if (context.config.downloader.mergeOverlays.get() && mediaParamMap.containsKey("overlay_image_media_info")) { + mediaInfoMap[SplitMediaAssetType.OVERLAY] = + MediaInfo(mediaParamMap["overlay_image_media_info"]!!) + } + lastSeenMapParams = mediaParamMap + lastSeenMediaInfoMap = mediaInfoMap - if (!canAutoDownload()) return@onOperaViewStateCallback + if (!canAutoDownload()) return@onOperaViewStateCallback - context.executeAsync { - runCatching { - handleOperaMedia(mediaParamMap, mediaInfoMap, false) - }.onFailure { - context.log.error("Failed to handle opera media", it) - context.longToast(it.message) + context.executeAsync { + runCatching { + handleOperaMedia(mediaParamMap, mediaInfoMap, false) + }.onFailure { + context.log.error("Failed to handle opera media", it) + context.longToast(it.message) + } } } - } - arrayOf(onDisplayStateChange, onDisplayStateChangeGesture).forEach { methodName -> - classReference.get()?.hook( - methodName.get() ?: return@forEach, - HookStage.AFTER, onOperaViewStateCallback - ) + arrayOf(onDisplayStateChange, onDisplayStateChangeGesture).forEach { methodName -> + classReference.get()?.hook( + methodName.get() ?: return@forEach, + HookStage.AFTER, onOperaViewStateCallback + ) + } } } } diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/downloader/ProfilePictureDownloader.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/downloader/ProfilePictureDownloader.kt @@ -7,61 +7,62 @@ import me.rhunk.snapenhance.common.util.protobuf.ProtoReader import me.rhunk.snapenhance.core.event.events.impl.AddViewEvent import me.rhunk.snapenhance.core.event.events.impl.NetworkApiRequestEvent import me.rhunk.snapenhance.core.features.Feature -import me.rhunk.snapenhance.core.features.FeatureLoadParams import me.rhunk.snapenhance.core.ui.ViewAppearanceHelper -class ProfilePictureDownloader : Feature("ProfilePictureDownloader", loadParams = FeatureLoadParams.ACTIVITY_CREATE_ASYNC) { +class ProfilePictureDownloader : Feature("ProfilePictureDownloader") { @SuppressLint("SetTextI18n") - override fun asyncOnActivityCreate() { + override fun init() { if (!context.config.downloader.downloadProfilePictures.get()) return 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 + onNextActivityCreate(defer = true) { + 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 = this@ProfilePictureDownloader.context.translation["profile_picture_downloader.button"] - 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(this@ProfilePictureDownloader.context.translation["profile_picture_downloader.title"]) - val choices = mutableMapOf<String, String>() - backgroundUrl?.let { choices["background_option"] = it } - avatarUrl?.let { choices["avatar_option"] = it } + event.parent.addView(Button(event.parent.context).apply { + text = this@ProfilePictureDownloader.context.translation["profile_picture_downloader.button"] + 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(this@ProfilePictureDownloader.context.translation["profile_picture_downloader.title"]) + val choices = mutableMapOf<String, String>() + backgroundUrl?.let { choices["background_option"] = it } + avatarUrl?.let { choices["avatar_option"] = it } - setItems(choices.keys.map { - this@ProfilePictureDownloader.context.translation["profile_picture_downloader.$it"] - }.toTypedArray()) { _, which -> - runCatching { - this@ProfilePictureDownloader.context.feature(MediaDownloader::class).downloadProfilePicture( - choices.values.elementAt(which), - friendUsername!! - ) - }.onFailure { - this@ProfilePictureDownloader.context.log.error("Failed to download profile picture", it) + setItems(choices.keys.map { + this@ProfilePictureDownloader.context.translation["profile_picture_downloader.$it"] + }.toTypedArray()) { _, which -> + runCatching { + this@ProfilePictureDownloader.context.feature(MediaDownloader::class).downloadProfilePicture( + choices.values.elementAt(which), + friendUsername!! + ) + }.onFailure { + this@ProfilePictureDownloader.context.log.error("Failed to download profile picture", it) + } } - } - }.show() - } - }) - } + }.show() + } + }) + } - context.event.subscribe(NetworkApiRequestEvent::class) { event -> - if (!event.url.endsWith("/rpc/getPublicProfile")) return@subscribe - event.onSuccess { buffer -> - ProtoReader(buffer ?: return@onSuccess).followPath(1, 1, 2) { - friendUsername = getString(2) ?: return@followPath - followPath(4) { - backgroundUrl = getString(2) - avatarUrl = getString(100) + context.event.subscribe(NetworkApiRequestEvent::class) { event -> + if (!event.url.endsWith("/rpc/getPublicProfile")) return@subscribe + event.onSuccess { buffer -> + ProtoReader(buffer ?: return@onSuccess).followPath(1, 1, 2) { + friendUsername = getString(2) ?: return@followPath + followPath(4) { + backgroundUrl = getString(2) + avatarUrl = getString(100) + } } } } diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/experiments/AccountSwitcher.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/experiments/AccountSwitcher.kt @@ -36,7 +36,6 @@ import me.rhunk.snapenhance.common.util.snap.MediaDownloaderHelper import me.rhunk.snapenhance.core.event.events.impl.ActivityResultEvent import me.rhunk.snapenhance.core.event.events.impl.AddViewEvent 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 import me.rhunk.snapenhance.core.util.ktx.getId @@ -47,7 +46,7 @@ import java.util.zip.ZipInputStream import java.util.zip.ZipOutputStream import kotlin.random.Random -class AccountSwitcher: Feature("Account Switcher", loadParams = FeatureLoadParams.INIT_SYNC or FeatureLoadParams.ACTIVITY_CREATE_SYNC) { +class AccountSwitcher: Feature("Account Switcher") { private var exportCallback: Pair<Int, String>? = null // requestCode -> userId private var importRequestCode: Int? = null @@ -425,25 +424,23 @@ class AccountSwitcher: Feature("Account Switcher", loadParams = FeatureLoadParam mainDbShmFile?.delete() } - override fun onActivityCreate() { + @SuppressLint("SetTextI18n") + override fun init() { if (context.config.experimental.accountSwitcher.globalState != true) return - val hovaHeaderSearchIcon = context.mainActivity!!.resources.getId("hova_header_search_icon") + onNextActivityCreate { + val hovaHeaderSearchIcon = context.resources.getId("hova_header_search_icon") - context.event.subscribe(AddViewEvent::class) { event -> - if (event.view.id != hovaHeaderSearchIcon) return@subscribe + context.event.subscribe(AddViewEvent::class) { event -> + if (event.view.id != hovaHeaderSearchIcon) return@subscribe - event.view.setOnLongClickListener { - context.mainActivity!!.vibrateLongPress() - showManagementPopup() - false + event.view.setOnLongClickListener { + context.mainActivity!!.vibrateLongPress() + showManagementPopup() + false + } } } - } - - @SuppressLint("SetTextI18n") - override fun init() { - if (context.config.experimental.accountSwitcher.globalState != true) return context.event.subscribe(ActivityResultEvent::class) { event -> if (importRequestCode == event.requestCode) { @@ -501,7 +498,7 @@ class AccountSwitcher: Feature("Account Switcher", loadParams = FeatureLoadParam } findClass("com.snap.identity.loginsignup.ui.LoginSignupActivity").apply { - hook("onPostCreate", HookStage.AFTER) { param -> + hook("onPostCreate", HookStage.AFTER) { context.mainActivity!!.findViewById<FrameLayout>(android.R.id.content).addView(createComposeView(context.mainActivity!!) { Row( modifier = Modifier.fillMaxWidth(), diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/experiments/AddFriendSourceSpoof.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/experiments/AddFriendSourceSpoof.kt @@ -4,68 +4,69 @@ import me.rhunk.snapenhance.common.data.FriendAddSource import me.rhunk.snapenhance.common.util.protobuf.ProtoEditor import me.rhunk.snapenhance.core.event.events.impl.UnaryCallEvent 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.hookConstructor import me.rhunk.snapenhance.mapper.impl.FriendRelationshipChangerMapper -class AddFriendSourceSpoof : Feature("AddFriendSourceSpoof", loadParams = FeatureLoadParams.ACTIVITY_CREATE_SYNC) { +class AddFriendSourceSpoof : Feature("AddFriendSourceSpoof") { var friendRelationshipChangerInstance: Any? = null private set - override fun onActivityCreate() { - context.mappings.useMapper(FriendRelationshipChangerMapper::class) { - classReference.get()?.hookConstructor(HookStage.AFTER) { param -> - friendRelationshipChangerInstance = param.thisObject() + override fun init() { + onNextActivityCreate { + context.mappings.useMapper(FriendRelationshipChangerMapper::class) { + classReference.get()?.hookConstructor(HookStage.AFTER) { param -> + friendRelationshipChangerInstance = param.thisObject() + } } - } - - context.event.subscribe(UnaryCallEvent::class) { event -> - if (event.uri != "/snapchat.friending.server.FriendAction/AddFriends") return@subscribe - val spoofedSource = context.config.experimental.addFriendSourceSpoof.getNullable() ?: return@subscribe - event.buffer = ProtoEditor(event.buffer).apply { - edit { - fun setPage(value: String) { - remove(1) - addString(1, value) - } - editEach(2) { - remove(3) // remove suggestion token - fun setSource(source: FriendAddSource) { - remove(2) - addVarInt(2, source.id) + context.event.subscribe(UnaryCallEvent::class) { event -> + if (event.uri != "/snapchat.friending.server.FriendAction/AddFriends") return@subscribe + val spoofedSource = context.config.experimental.addFriendSourceSpoof.getNullable() ?: return@subscribe + event.buffer = ProtoEditor(event.buffer).apply { + edit { + fun setPage(value: String) { + remove(1) + addString(1, value) } - when (spoofedSource) { - "added_by_group_chat" -> { - setPage("group_profile") - setSource(FriendAddSource.GROUP_CHAT) - } - "added_by_username" -> { - setPage("search") - setSource(FriendAddSource.USERNAME) - } - "added_by_qr_code" -> { - setPage("scan_snapcode") - setSource(FriendAddSource.QR_CODE) + editEach(2) { + remove(3) // remove suggestion token + fun setSource(source: FriendAddSource) { + remove(2) + addVarInt(2, source.id) } - "added_by_mention" -> { - setPage("context_card") - setSource(FriendAddSource.MENTION) - } - "added_by_community" -> { - setPage("profile") - setSource(FriendAddSource.COMMUNITY) - } - "added_by_quick_add" -> { - setPage("add_friends_button_on_top_bar_on_friends_feed") - setSource(FriendAddSource.SUGGESTED) + + when (spoofedSource) { + "added_by_group_chat" -> { + setPage("group_profile") + setSource(FriendAddSource.GROUP_CHAT) + } + "added_by_username" -> { + setPage("search") + setSource(FriendAddSource.USERNAME) + } + "added_by_qr_code" -> { + setPage("scan_snapcode") + setSource(FriendAddSource.QR_CODE) + } + "added_by_mention" -> { + setPage("context_card") + setSource(FriendAddSource.MENTION) + } + "added_by_community" -> { + setPage("profile") + setSource(FriendAddSource.COMMUNITY) + } + "added_by_quick_add" -> { + setPage("add_friends_button_on_top_bar_on_friends_feed") + setSource(FriendAddSource.SUGGESTED) + } } } } - } - }.toByteArray() + }.toByteArray() + } } } } \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/experiments/AppLock.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/experiments/AppLock.kt @@ -31,7 +31,6 @@ import me.rhunk.snapenhance.common.ui.AppMaterialTheme import me.rhunk.snapenhance.common.ui.createComposeView import me.rhunk.snapenhance.core.event.events.impl.ActivityResultEvent import me.rhunk.snapenhance.core.features.Feature -import me.rhunk.snapenhance.core.features.FeatureLoadParams import me.rhunk.snapenhance.core.ui.addForegroundDrawable import me.rhunk.snapenhance.core.ui.children import me.rhunk.snapenhance.core.ui.removeForegroundDrawable @@ -39,7 +38,7 @@ import me.rhunk.snapenhance.core.util.hook.HookStage import me.rhunk.snapenhance.core.util.hook.hook import kotlin.random.Random -class AppLock : Feature("AppLock", loadParams = FeatureLoadParams.ACTIVITY_CREATE_SYNC) { +class AppLock : Feature("AppLock") { private var isUnlockRequested = false private val rootContentView get() = context.mainActivity!!.findViewById<FrameLayout>(android.R.id.content) @@ -122,32 +121,34 @@ class AppLock : Feature("AppLock", loadParams = FeatureLoadParams.ACTIVITY_CREAT } } - override fun onActivityCreate() { + override fun init() { if (context.config.experimental.appLock.globalState != true) return - Activity::class.java.apply { - if (context.config.experimental.appLock.lockOnResume.get()) { - hook("onResume", HookStage.BEFORE) { param -> - if (param.thisObject<Activity>().packageName != Constants.SNAPCHAT_PACKAGE_NAME) return@hook - if (isUnlockRequested) return@hook - lock(prompt = true) - } - hook("onPause", HookStage.BEFORE) { param -> - if (param.thisObject<Activity>().packageName != Constants.SNAPCHAT_PACKAGE_NAME) return@hook - if (isUnlockRequested) return@hook - hideRootView() + onNextActivityCreate { + Activity::class.java.apply { + if (context.config.experimental.appLock.lockOnResume.get()) { + hook("onResume", HookStage.BEFORE) { param -> + if (param.thisObject<Activity>().packageName != Constants.SNAPCHAT_PACKAGE_NAME) return@hook + if (isUnlockRequested) return@hook + lock(prompt = true) + } + hook("onPause", HookStage.BEFORE) { param -> + if (param.thisObject<Activity>().packageName != Constants.SNAPCHAT_PACKAGE_NAME) return@hook + if (isUnlockRequested) return@hook + hideRootView() + } } } - } - context.event.subscribe(ActivityResultEvent::class) { event -> - if (event.requestCode != requestCode) return@subscribe - if (event.resultCode == Activity.RESULT_OK) { - unlock() - return@subscribe + context.event.subscribe(ActivityResultEvent::class) { event -> + if (event.requestCode != requestCode) return@subscribe + if (event.resultCode == Activity.RESULT_OK) { + unlock() + return@subscribe + } + lock(prompt = false) } - lock(prompt = false) + lock() } - lock() } } \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/experiments/AutoOpenSnaps.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/experiments/AutoOpenSnaps.kt @@ -11,7 +11,6 @@ import me.rhunk.snapenhance.common.data.ContentType import me.rhunk.snapenhance.common.data.MessageUpdate import me.rhunk.snapenhance.common.data.MessagingRuleType import me.rhunk.snapenhance.core.event.events.impl.BuildMessageEvent -import me.rhunk.snapenhance.core.features.FeatureLoadParams import me.rhunk.snapenhance.core.features.MessagingRuleFeature import me.rhunk.snapenhance.core.features.impl.messaging.Messaging import java.util.concurrent.atomic.AtomicInteger @@ -19,7 +18,7 @@ import kotlin.coroutines.resume import kotlin.coroutines.suspendCoroutine import kotlin.random.Random -class AutoOpenSnaps: MessagingRuleFeature("Auto Open Snaps", MessagingRuleType.AUTO_OPEN_SNAPS, loadParams = FeatureLoadParams.INIT_SYNC) { +class AutoOpenSnaps: MessagingRuleFeature("Auto Open Snaps", MessagingRuleType.AUTO_OPEN_SNAPS) { private val snapQueue = MutableSharedFlow<Pair<String, Long>>() private var snapQueueSize = AtomicInteger(0) private val openedSnaps = mutableListOf<Long>() diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/experiments/BestFriendPinning.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/experiments/BestFriendPinning.kt @@ -12,13 +12,12 @@ import me.rhunk.snapenhance.common.util.protobuf.ProtoReader import me.rhunk.snapenhance.core.event.events.impl.NetworkApiRequestEvent import me.rhunk.snapenhance.core.event.events.impl.UnaryCallEvent import me.rhunk.snapenhance.core.features.BridgeFileFeature -import me.rhunk.snapenhance.core.features.FeatureLoadParams import me.rhunk.snapenhance.core.ui.triggerRootCloseTouchEvent import java.io.InputStreamReader import java.nio.ByteBuffer import java.util.UUID -class BestFriendPinning: BridgeFileFeature("Best Friend Pinning", InternalFileHandleType.PINNED_BEST_FRIEND, loadParams = FeatureLoadParams.INIT_SYNC) { +class BestFriendPinning: BridgeFileFeature("Best Friend Pinning", InternalFileHandleType.PINNED_BEST_FRIEND) { private fun updatePinnedBestFriendStatus() { lines().firstOrNull()?.trim()?.let { context.database.updatePinnedBestFriendStatus(it.substring(0, 36), "number_one_bf_for_two_months") diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/experiments/BetterLocation.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/experiments/BetterLocation.kt @@ -25,7 +25,6 @@ import me.rhunk.snapenhance.common.util.protobuf.ProtoReader import me.rhunk.snapenhance.core.event.events.impl.AddViewEvent import me.rhunk.snapenhance.core.event.events.impl.UnaryCallEvent import me.rhunk.snapenhance.core.features.Feature -import me.rhunk.snapenhance.core.features.FeatureLoadParams import me.rhunk.snapenhance.core.util.RandomWalking import me.rhunk.snapenhance.core.util.hook.HookStage import me.rhunk.snapenhance.core.util.hook.hook @@ -44,7 +43,7 @@ data class FriendLocation( val localityPieces: List<String> ) -class BetterLocation : Feature("Better Location", loadParams = FeatureLoadParams.INIT_SYNC) { +class BetterLocation : Feature("Better Location") { private val locationHistory = mutableMapOf<String, FriendLocation>() private val walkRadius by lazy { diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/experiments/BetterTranscript.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/experiments/BetterTranscript.kt @@ -5,7 +5,6 @@ import me.rhunk.snapenhance.common.util.TranscriptApi import me.rhunk.snapenhance.common.util.protobuf.ProtoEditor import me.rhunk.snapenhance.core.event.events.impl.BuildMessageEvent import me.rhunk.snapenhance.core.features.Feature -import me.rhunk.snapenhance.core.features.FeatureLoadParams import me.rhunk.snapenhance.core.util.dataBuilder import me.rhunk.snapenhance.core.util.hook.HookStage import me.rhunk.snapenhance.core.util.hook.hook @@ -15,62 +14,65 @@ import okhttp3.RequestBody.Companion.toRequestBody import java.lang.reflect.Method import java.nio.ByteBuffer -class BetterTranscript: Feature("Better Transcript", loadParams = FeatureLoadParams.ACTIVITY_CREATE_SYNC) { +class BetterTranscript: Feature("Better Transcript") { val transcriptApi by lazy { TranscriptApi() } - override fun onActivityCreate() { + override fun init() { if (context.config.experimental.betterTranscript.globalState != true) return - val config = context.config.experimental.betterTranscript - val preferredTranscriptionLang = config.preferredTranscriptionLang.getNullable()?.takeIf { - it.isNotBlank() - } - if (config.forceTranscription.get()) { - context.event.subscribe(BuildMessageEvent::class, priority = 104) { event -> - if (event.message.messageContent?.contentType != ContentType.NOTE) return@subscribe - event.message.messageContent!!.content = ProtoEditor(event.message.messageContent!!.content!!).apply { - edit(6, 1) { - if (firstOrNull(3) == null) { - addString(3, context.getConfigLocale()) - } - } - }.toByteArray() + onNextActivityCreate { + val config = context.config.experimental.betterTranscript + val preferredTranscriptionLang = config.preferredTranscriptionLang.getNullable()?.takeIf { + it.isNotBlank() } - } - findClass("com.snapchat.client.voiceml.IVoiceMLSDK\$CppProxy").hook("asrTranscribe", HookStage.BEFORE) { param -> - if (config.enhancedTranscript.get()) { - val buffer = param.arg<ByteBuffer>(2).let { - it.rewind() - ByteArray(it.remaining()).also { it1 -> it.get(it1); it.rewind() } + if (config.forceTranscription.get()) { + context.event.subscribe(BuildMessageEvent::class, priority = 104) { event -> + if (event.message.messageContent?.contentType != ContentType.NOTE) return@subscribe + event.message.messageContent!!.content = ProtoEditor(event.message.messageContent!!.content!!).apply { + edit(6, 1) { + if (firstOrNull(3) == null) { + addString(3, context.getConfigLocale()) + } + } + }.toByteArray() } - val result = runCatching { - transcriptApi.transcribe( - buffer.toRequestBody(), - lang = config.preferredTranscriptionLang.getNullable()?.takeIf { - it.isNotBlank() - }?.uppercase() - ) - }.onFailure { - context.log.error("Failed to transcribe audio", it) - context.shortToast("Failed to transcribe audio! Check logcat for more details.") - }.getOrNull() + } - param.setResult( - (param.method() as Method).returnType.dataBuilder { - set("mError", result == null) - set("mNlpResponses", ArrayList<Any>()) - set("mWordInfo", ArrayList<Any>()) - set("mTranscription", result) + findClass("com.snapchat.client.voiceml.IVoiceMLSDK\$CppProxy").hook("asrTranscribe", HookStage.BEFORE) { param -> + if (config.enhancedTranscript.get()) { + val buffer = param.arg<ByteBuffer>(2).let { + it.rewind() + ByteArray(it.remaining()).also { it1 -> it.get(it1); it.rewind() } + } + val result = runCatching { + transcriptApi.transcribe( + buffer.toRequestBody(), + lang = config.preferredTranscriptionLang.getNullable()?.takeIf { + it.isNotBlank() + }?.uppercase() + ) + }.onFailure { + context.log.error("Failed to transcribe audio", it) + context.shortToast("Failed to transcribe audio! Check logcat for more details.") + }.getOrNull() + + param.setResult( + (param.method() as Method).returnType.dataBuilder { + set("mError", result == null) + set("mNlpResponses", ArrayList<Any>()) + set("mWordInfo", ArrayList<Any>()) + set("mTranscription", result) + } + ) + return@hook + } + preferredTranscriptionLang?.lowercase()?.let { + val asrConfig = param.arg<Any>(1) + asrConfig.getObjectFieldOrNull("mBaseConfig")?.apply { + setObjectField("mLanguageModel", it) + setObjectField("mUiLanguage", it) } - ) - return@hook - } - preferredTranscriptionLang?.lowercase()?.let { - val asrConfig = param.arg<Any>(1) - asrConfig.getObjectFieldOrNull("mBaseConfig")?.apply { - setObjectField("mLanguageModel", it) - setObjectField("mUiLanguage", it) } } } diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/experiments/CallRecorder.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/experiments/CallRecorder.kt @@ -8,7 +8,6 @@ import kotlinx.coroutines.withTimeoutOrNull import me.rhunk.snapenhance.common.data.download.AudioStreamFormat import me.rhunk.snapenhance.common.data.download.MediaDownloadSource import me.rhunk.snapenhance.core.features.Feature -import me.rhunk.snapenhance.core.features.FeatureLoadParams import me.rhunk.snapenhance.core.features.impl.downloader.MediaDownloader import me.rhunk.snapenhance.core.util.hook.HookStage import me.rhunk.snapenhance.core.util.hook.hook @@ -23,7 +22,7 @@ import java.util.UUID import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.CopyOnWriteArrayList -class CallRecorder : Feature("Call Recorder", loadParams = FeatureLoadParams.INIT_SYNC) { +class CallRecorder : Feature("Call Recorder") { private val httpServer = HttpServer( timeout = Integer.MAX_VALUE ) diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/experiments/ComposerHooks.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/experiments/ComposerHooks.kt @@ -25,7 +25,6 @@ import me.rhunk.snapenhance.common.ui.AppMaterialTheme import me.rhunk.snapenhance.common.ui.createComposeAlertDialog import me.rhunk.snapenhance.common.ui.createComposeView import me.rhunk.snapenhance.core.features.Feature -import me.rhunk.snapenhance.core.features.FeatureLoadParams import me.rhunk.snapenhance.core.features.impl.downloader.MediaDownloader import me.rhunk.snapenhance.core.util.hook.HookStage import me.rhunk.snapenhance.core.util.hook.Hooker @@ -36,7 +35,7 @@ import java.lang.reflect.Proxy import kotlin.math.absoluteValue import kotlin.random.Random -class ComposerHooks: Feature("ComposerHooks", loadParams = FeatureLoadParams.INIT_SYNC) { +class ComposerHooks: Feature("ComposerHooks") { private val config by lazy { context.config.experimental.nativeHooks.composerHooks } private val getImportsFunctionName = Random.nextLong().absoluteValue.toString(16) diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/experiments/ContextMenuFix.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/experiments/ContextMenuFix.kt @@ -2,10 +2,9 @@ package me.rhunk.snapenhance.core.features.impl.experiments import me.rhunk.snapenhance.core.event.events.impl.UnaryCallEvent import me.rhunk.snapenhance.core.features.Feature -import me.rhunk.snapenhance.core.features.FeatureLoadParams import java.nio.ByteBuffer -class ContextMenuFix: Feature("Context Menu Fix", loadParams = FeatureLoadParams.INIT_SYNC) { +class ContextMenuFix: Feature("Context Menu Fix") { override fun init() { if (!context.config.experimental.contextMenuFix.get()) return context.event.subscribe(UnaryCallEvent::class) { event -> diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/experiments/ConvertMessageLocally.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/experiments/ConvertMessageLocally.kt @@ -5,13 +5,12 @@ import me.rhunk.snapenhance.common.util.protobuf.ProtoReader import me.rhunk.snapenhance.common.util.protobuf.ProtoWriter import me.rhunk.snapenhance.core.event.events.impl.BuildMessageEvent import me.rhunk.snapenhance.core.features.Feature -import me.rhunk.snapenhance.core.features.FeatureLoadParams import me.rhunk.snapenhance.core.features.impl.messaging.Messaging import me.rhunk.snapenhance.core.ui.ViewAppearanceHelper import me.rhunk.snapenhance.core.wrapper.impl.Message import me.rhunk.snapenhance.core.wrapper.impl.MessageContent -class ConvertMessageLocally : Feature("Convert Message Edit", loadParams = FeatureLoadParams.ACTIVITY_CREATE_SYNC) { +class ConvertMessageLocally : Feature("Convert Message Edit") { private val messageCache = mutableMapOf<Long, MessageContent>() private fun dispatchMessageEdit(message: Message, restore: Boolean = false) { @@ -64,11 +63,13 @@ class ConvertMessageLocally : Feature("Convert Message Edit", loadParams = Featu }.show() } - override fun onActivityCreate() { - context.event.subscribe(BuildMessageEvent::class, priority = 2) { - val clientMessageId = it.message.messageDescriptor?.messageId ?: return@subscribe - if (!messageCache.containsKey(clientMessageId)) return@subscribe - it.message.messageContent = messageCache[clientMessageId] + override fun init() { + onNextActivityCreate { + context.event.subscribe(BuildMessageEvent::class, priority = 2) { + val clientMessageId = it.message.messageDescriptor?.messageId ?: return@subscribe + if (!messageCache.containsKey(clientMessageId)) return@subscribe + it.message.messageContent = messageCache[clientMessageId] + } } } } \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/experiments/DeviceSpooferHook.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/experiments/DeviceSpooferHook.kt @@ -7,12 +7,11 @@ import android.net.Network import android.net.NetworkCapabilities import android.os.Build import me.rhunk.snapenhance.core.features.Feature -import me.rhunk.snapenhance.core.features.FeatureLoadParams import me.rhunk.snapenhance.core.util.LSPatchUpdater import me.rhunk.snapenhance.core.util.hook.HookStage import me.rhunk.snapenhance.core.util.hook.hook -class DeviceSpooferHook: Feature("device_spoofer", loadParams = FeatureLoadParams.INIT_SYNC) { +class DeviceSpooferHook: Feature("device_spoofer") { private fun hookInstallerPackageName() { context.androidContext.packageManager::class.java.hook("getInstallerPackageName", HookStage.BEFORE) { param -> param.setResult("com.android.vending") diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/experiments/EndToEndEncryption.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/experiments/EndToEndEncryption.kt @@ -33,7 +33,6 @@ import me.rhunk.snapenhance.core.event.events.impl.BindViewEvent import me.rhunk.snapenhance.core.event.events.impl.BuildMessageEvent import me.rhunk.snapenhance.core.event.events.impl.NativeUnaryCallEvent import me.rhunk.snapenhance.core.event.events.impl.SendMessageWithContentEvent -import me.rhunk.snapenhance.core.features.FeatureLoadParams import me.rhunk.snapenhance.core.features.MessagingRuleFeature import me.rhunk.snapenhance.core.features.impl.ui.ConversationToolbox import me.rhunk.snapenhance.core.ui.ViewAppearanceHelper @@ -55,8 +54,7 @@ import kotlin.random.Random class EndToEndEncryption : MessagingRuleFeature( "EndToEndEncryption", - MessagingRuleType.E2E_ENCRYPTION, - loadParams = FeatureLoadParams.ACTIVITY_CREATE_SYNC or FeatureLoadParams.INIT_SYNC or FeatureLoadParams.INIT_ASYNC + MessagingRuleType.E2E_ENCRYPTION ) { val isEnabled get() = context.config.experimental.e2eEncryption.globalState == true private val e2eeInterface by lazyBridge { context.bridgeClient.getE2eeInterface() } @@ -174,96 +172,223 @@ class EndToEndEncryption : MessagingRuleFeature( } @SuppressLint("SetTextI18n", "DiscouragedApi") - override fun onActivityCreate() { + override fun init() { if (!isEnabled) return - context.feature(ConversationToolbox::class).addComposable(translation["confirmation_dialogs.title"], filter = { - context.database.getDMOtherParticipant(it) != null - }) { dialog, conversationId -> - val friendId = remember { - context.database.getDMOtherParticipant(conversationId) - } ?: return@addComposable - val fingerprint = remember { - runCatching { - e2eeInterface.getSecretFingerprint(friendId) - }.getOrNull() - } - if (fingerprint != null) { - Text(translation.format("toolbox.shared_key_fingerprint", "fingerprint" to fingerprint)) - } else { - Text(translation["toolbox.no_shared_key"]) - } - Spacer(modifier = Modifier.height(10.dp)) - Button(onClick = { - dialog.dismiss() - warnKeyOverwrite(friendId) { - askForKeys(conversationId) + context.mappings.useMapper(CallbackMapper::class) { + callbacks.getClass("ConversationManagerDelegate")?.hook("onSendComplete", HookStage.BEFORE) { param -> + val sendMessageResult = param.arg<Any>(0) + val messageDestinations = MessageDestinations(sendMessageResult.getObjectField("mCompletedDestinations") ?: return@hook) + if (messageDestinations.mPhoneNumbers?.isNotEmpty() == true || messageDestinations.stories?.isNotEmpty() == true) return@hook + + val completedConversationDestinations = sendMessageResult.getObjectField("mCompletedConversationDestinations") as? ArrayList<*> ?: return@hook + val messageIds = completedConversationDestinations.filter { getState(SnapUUID(it.getObjectField("mConversationId")).toString()) }.mapNotNull { + it.getObjectFieldOrNull("mMessageId") as? Long } - }) { - Text(translation["toolbox.initiate_exchange_button"]) + + encryptedMessages.addAll(messageIds) } } - val encryptedMessageIndicator by context.config.experimental.e2eEncryption.encryptedMessageIndicator - - val specialCard = Random.nextLong().toString(16) + context.event.subscribe(BuildMessageEvent::class, priority = 0) { event -> + val message = event.message + val conversationId = message.messageDescriptor!!.conversationId.toString() + val isMessageCommitted = message.messageState == MessageState.COMMITTED + messageHook( + conversationId = conversationId, + messageId = message.messageDescriptor!!.messageId!!, + senderId = message.senderId.toString(), + messageContent = message.messageContent!!, + committed = isMessageCommitted + ) - context.event.subscribe(BindViewEvent::class) { event -> - event.chatMessage { conversationId, messageId -> - val viewGroup = event.view.parent as? ViewGroup ?: return@subscribe + message.messageContent!!.instanceNonNull() + .getObjectField("mQuotedMessage") + ?.getObjectField("mContent") + ?.also { quotedMessage -> + messageHook( + conversationId = conversationId, + messageId = quotedMessage.getObjectField("mMessageId")?.toString()?.toLong() ?: return@also, + senderId = SnapUUID(quotedMessage.getObjectField("mSenderId")).toString(), + messageContent = MessageContent(quotedMessage), + committed = isMessageCommitted + ) + } + } - viewGroup.findViewWithTag<View>(specialCard)?.also { - viewGroup.removeView(it) + onNextActivityCreate(defer = true) { + context.feature(ConversationToolbox::class).addComposable(translation["confirmation_dialogs.title"], filter = { + context.database.getDMOtherParticipant(it) != null + }) { dialog, conversationId -> + val friendId = remember { + context.database.getDMOtherParticipant(conversationId) + } ?: return@addComposable + val fingerprint = remember { + runCatching { + e2eeInterface.getSecretFingerprint(friendId) + }.getOrNull() + } + if (fingerprint != null) { + Text(translation.format("toolbox.shared_key_fingerprint", "fingerprint" to fingerprint)) + } else { + Text(translation["toolbox.no_shared_key"]) + } + Spacer(modifier = Modifier.height(10.dp)) + Button(onClick = { + dialog.dismiss() + warnKeyOverwrite(friendId) { + askForKeys(conversationId) + } + }) { + Text(translation["toolbox.initiate_exchange_button"]) } + } - if (encryptedMessageIndicator) { - viewGroup.removeForegroundDrawable("encryptedMessage") + val encryptedMessageIndicator by context.config.experimental.e2eEncryption.encryptedMessageIndicator - if (encryptedMessages.contains(messageId.toLong())) { - viewGroup.addForegroundDrawable("encryptedMessage", ShapeDrawable(object: Shape() { - override fun draw(canvas: Canvas, paint: Paint) { - paint.textSize = 20f - canvas.drawText("\uD83D\uDD12", 0f, canvas.height / 2f, paint) - } - })) + val specialCard = Random.nextLong().toString(16) + + context.event.subscribe(BindViewEvent::class) { event -> + event.chatMessage { conversationId, messageId -> + val viewGroup = event.view.parent as? ViewGroup ?: return@subscribe + + viewGroup.findViewWithTag<View>(specialCard)?.also { + viewGroup.removeView(it) } - } - val secret = secretResponses[messageId.toLong()] - val publicKey = pkRequests[messageId.toLong()] + if (encryptedMessageIndicator) { + viewGroup.removeForegroundDrawable("encryptedMessage") - if (publicKey != null || secret != null) { - viewGroup.addView(createComposeView(context.mainActivity!!) { - Card( - modifier = Modifier.fillMaxWidth().padding(8.dp), - onClick = { - if (publicKey != null) { - handlePublicKeyRequest(conversationId, publicKey) + if (encryptedMessages.contains(messageId.toLong())) { + viewGroup.addForegroundDrawable("encryptedMessage", ShapeDrawable(object: Shape() { + override fun draw(canvas: Canvas, paint: Paint) { + paint.textSize = 20f + canvas.drawText("\uD83D\uDD12", 0f, canvas.height / 2f, paint) } - if (secret != null) { - handleSecretResponse(conversationId, secret) + })) + } + } + + val secret = secretResponses[messageId.toLong()] + val publicKey = pkRequests[messageId.toLong()] + + if (publicKey != null || secret != null) { + viewGroup.addView(createComposeView(context.mainActivity!!) { + Card( + modifier = Modifier.fillMaxWidth().padding(8.dp), + onClick = { + if (publicKey != null) { + handlePublicKeyRequest(conversationId, publicKey) + } + if (secret != null) { + handleSecretResponse(conversationId, secret) + } } - } - ) { - Box( - modifier = Modifier.fillMaxWidth().padding(5.dp), - contentAlignment = Alignment.Center ) { - if (publicKey != null) { - Text(translation["accept_public_key_button"]) - } - if (secret != null) { - Text(translation["accept_secret_button"]) + Box( + modifier = Modifier.fillMaxWidth().padding(5.dp), + contentAlignment = Alignment.Center + ) { + if (publicKey != null) { + Text(translation["accept_public_key_button"]) + } + if (secret != null) { + Text(translation["accept_secret_button"]) + } } } + }.apply { + tag = specialCard + layoutParams = ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT, + ) + }) + } + } + } + } + + defer { + val forceMessageEncryption by context.config.experimental.e2eEncryption.forceMessageEncryption + + context.mappings.useMapper(CallbackMapper::class) { + callbacks.getClass("UploadDelegate")?.hook("uploadMedia", HookStage.BEFORE) { param -> + val messageDestinations = MessageDestinations(param.arg(1)) + val uploadCallback = param.arg<Any>(2) + val e2eeConversations = messageDestinations.getEndToEndConversations() + if (e2eeConversations.isEmpty()) return@hook + + if (messageDestinations.conversations!!.size != e2eeConversations.size || messageDestinations.stories?.isNotEmpty() == true) { + context.log.debug("skipping encryption") + return@hook + } + + Hooker.hookObjectMethod(uploadCallback::class.java, uploadCallback, "onUploadFinished", HookStage.BEFORE) { methodParam -> + val messageContent = MessageContent(methodParam.arg(1)) + runCatching { + messageContent.content = ProtoWriter().apply { + writeEncryptedMessage(e2eeConversations.map { getE2EParticipants(it) }.flatten().distinct(), messageContent.content!!) + }.toByteArray() + }.onFailure { + context.log.error("Failed to encrypt message", it) + context.longToast(translation["encryption_failed_toast"]) + } + } + } + } + + // trick to disable fidelius encryption + context.event.subscribe(SendMessageWithContentEvent::class) { event -> + val messageContent = event.messageContent + val destinations = event.destinations + + val e2eeConversations = destinations.getEndToEndConversations().takeIf { it.isNotEmpty() } ?: return@subscribe + + if (e2eeConversations.size != destinations.conversations!!.size || destinations.stories?.isNotEmpty() == true) { + if (!forceMessageEncryption) return@subscribe + context.longToast(translation["unencrypted_conversation_send_failure_toast"]) + event.canceled = true + return@subscribe + } + + if (!NativeLib.initialized) { + context.longToast(translation["native_hooks_send_failure_toast"]) + event.canceled = true + return@subscribe + } + + event.addInvokeLater { + if (event.messageContent.localMediaReferences?.isEmpty() == true) { + runCatching { + event.messageContent.content = ProtoWriter().apply { + writeEncryptedMessage(e2eeConversations.map { getE2EParticipants(it) }.flatten().distinct(), messageContent.content!!) + }.toByteArray() + }.onFailure { + context.log.error("Failed to encrypt message", it) + context.longToast(translation["encryption_failed_toast"]) } - }.apply { - tag = specialCard - layoutParams = ViewGroup.LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, - ViewGroup.LayoutParams.WRAP_CONTENT, - ) - }) + } + + if (event.messageContent.contentType == ContentType.SNAP) { + event.messageContent.contentType = ContentType.EXTERNAL_MEDIA + } + } + } + + context.event.subscribe(NativeUnaryCallEvent::class) { event -> + if (event.uri != "/messagingcoreservice.MessagingCoreService/CreateContentMessage") return@subscribe + val protoReader = ProtoReader(event.buffer) + val messageReader = protoReader.followPath(4) ?: return@subscribe + + if (messageReader.getVarInt(4, 2, 1, 5) == 1L) { + event.buffer = ProtoEditor(event.buffer).apply { + edit(4) { + remove(2) + addVarInt(2, ContentType.SNAP.id) + context.log.verbose("fixed snap content type") + } + }.toByteArray() } } } @@ -440,135 +565,5 @@ class EndToEndEncryption : MessagingRuleFeature( return conversations!!.filter { getState(it.toString()) && getE2EParticipants(it.toString()).isNotEmpty() }.map { it.toString() } } - override fun asyncInit() { - if (!isEnabled) return - val forceMessageEncryption by context.config.experimental.e2eEncryption.forceMessageEncryption - - context.mappings.useMapper(CallbackMapper::class) { - callbacks.getClass("UploadDelegate")?.hook("uploadMedia", HookStage.BEFORE) { param -> - val messageDestinations = MessageDestinations(param.arg(1)) - val uploadCallback = param.arg<Any>(2) - val e2eeConversations = messageDestinations.getEndToEndConversations() - if (e2eeConversations.isEmpty()) return@hook - - if (messageDestinations.conversations!!.size != e2eeConversations.size || messageDestinations.stories?.isNotEmpty() == true) { - context.log.debug("skipping encryption") - return@hook - } - - Hooker.hookObjectMethod(uploadCallback::class.java, uploadCallback, "onUploadFinished", HookStage.BEFORE) { methodParam -> - val messageContent = MessageContent(methodParam.arg(1)) - runCatching { - messageContent.content = ProtoWriter().apply { - writeEncryptedMessage(e2eeConversations.map { getE2EParticipants(it) }.flatten().distinct(), messageContent.content!!) - }.toByteArray() - }.onFailure { - context.log.error("Failed to encrypt message", it) - context.longToast(translation["encryption_failed_toast"]) - } - } - } - } - - // trick to disable fidelius encryption - context.event.subscribe(SendMessageWithContentEvent::class) { event -> - val messageContent = event.messageContent - val destinations = event.destinations - - val e2eeConversations = destinations.getEndToEndConversations().takeIf { it.isNotEmpty() } ?: return@subscribe - - if (e2eeConversations.size != destinations.conversations!!.size || destinations.stories?.isNotEmpty() == true) { - if (!forceMessageEncryption) return@subscribe - context.longToast(translation["unencrypted_conversation_send_failure_toast"]) - event.canceled = true - return@subscribe - } - - if (!NativeLib.initialized) { - context.longToast(translation["native_hooks_send_failure_toast"]) - event.canceled = true - return@subscribe - } - - event.addInvokeLater { - if (event.messageContent.localMediaReferences?.isEmpty() == true) { - runCatching { - event.messageContent.content = ProtoWriter().apply { - writeEncryptedMessage(e2eeConversations.map { getE2EParticipants(it) }.flatten().distinct(), messageContent.content!!) - }.toByteArray() - }.onFailure { - context.log.error("Failed to encrypt message", it) - context.longToast(translation["encryption_failed_toast"]) - } - } - - if (event.messageContent.contentType == ContentType.SNAP) { - event.messageContent.contentType = ContentType.EXTERNAL_MEDIA - } - } - } - - context.event.subscribe(NativeUnaryCallEvent::class) { event -> - if (event.uri != "/messagingcoreservice.MessagingCoreService/CreateContentMessage") return@subscribe - val protoReader = ProtoReader(event.buffer) - val messageReader = protoReader.followPath(4) ?: return@subscribe - - if (messageReader.getVarInt(4, 2, 1, 5) == 1L) { - event.buffer = ProtoEditor(event.buffer).apply { - edit(4) { - remove(2) - addVarInt(2, ContentType.SNAP.id) - context.log.verbose("fixed snap content type") - } - }.toByteArray() - } - } - } - - override fun init() { - if (!isEnabled) return - - context.mappings.useMapper(CallbackMapper::class) { - callbacks.getClass("ConversationManagerDelegate")?.hook("onSendComplete", HookStage.BEFORE) { param -> - val sendMessageResult = param.arg<Any>(0) - val messageDestinations = MessageDestinations(sendMessageResult.getObjectField("mCompletedDestinations") ?: return@hook) - if (messageDestinations.mPhoneNumbers?.isNotEmpty() == true || messageDestinations.stories?.isNotEmpty() == true) return@hook - - val completedConversationDestinations = sendMessageResult.getObjectField("mCompletedConversationDestinations") as? ArrayList<*> ?: return@hook - val messageIds = completedConversationDestinations.filter { getState(SnapUUID(it.getObjectField("mConversationId")).toString()) }.mapNotNull { - it.getObjectFieldOrNull("mMessageId") as? Long - } - - encryptedMessages.addAll(messageIds) - } - } - - context.event.subscribe(BuildMessageEvent::class, priority = 0) { event -> - val message = event.message - val conversationId = message.messageDescriptor!!.conversationId.toString() - val isMessageCommitted = message.messageState == MessageState.COMMITTED - messageHook( - conversationId = conversationId, - messageId = message.messageDescriptor!!.messageId!!, - senderId = message.senderId.toString(), - messageContent = message.messageContent!!, - committed = isMessageCommitted - ) - - message.messageContent!!.instanceNonNull() - .getObjectField("mQuotedMessage") - ?.getObjectField("mContent") - ?.also { quotedMessage -> - messageHook( - conversationId = conversationId, - messageId = quotedMessage.getObjectField("mMessageId")?.toString()?.toLong() ?: return@also, - senderId = SnapUUID(quotedMessage.getObjectField("mSenderId")).toString(), - messageContent = MessageContent(quotedMessage), - committed = isMessageCommitted - ) - } - } - } - override fun getRuleState() = RuleState.WHITELIST } \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/experiments/InfiniteStoryBoost.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/experiments/InfiniteStoryBoost.kt @@ -1,22 +1,23 @@ package me.rhunk.snapenhance.core.features.impl.experiments 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.hookConstructor import me.rhunk.snapenhance.mapper.impl.StoryBoostStateMapper -class InfiniteStoryBoost : Feature("InfiniteStoryBoost", loadParams = FeatureLoadParams.ACTIVITY_CREATE_ASYNC) { - override fun asyncOnActivityCreate() { +class InfiniteStoryBoost : Feature("InfiniteStoryBoost") { + override fun init() { if (!context.config.experimental.infiniteStoryBoost.get()) return - context.mappings.useMapper(StoryBoostStateMapper::class) { - classReference.get()?.hookConstructor(HookStage.BEFORE) { param -> - val startTimeMillis = param.arg<Long>(1) - //reset timestamp if it's more than 24 hours - if (System.currentTimeMillis() - startTimeMillis > 86400000) { - param.setArg(1, 0) - param.setArg(2, 0) + onNextActivityCreate(defer = true) { + context.mappings.useMapper(StoryBoostStateMapper::class) { + classReference.get()?.hookConstructor(HookStage.BEFORE) { param -> + val startTimeMillis = param.arg<Long>(1) + //reset timestamp if it's more than 24 hours + if (System.currentTimeMillis() - startTimeMillis > 86400000) { + param.setArg(1, 0) + param.setArg(2, 0) + } } } } diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/experiments/MediaFilePicker.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/experiments/MediaFilePicker.kt @@ -31,7 +31,6 @@ import me.rhunk.snapenhance.common.util.ktx.getTypeArguments import me.rhunk.snapenhance.core.event.events.impl.ActivityResultEvent import me.rhunk.snapenhance.core.event.events.impl.AddViewEvent import me.rhunk.snapenhance.core.features.Feature -import me.rhunk.snapenhance.core.features.FeatureLoadParams import me.rhunk.snapenhance.core.ui.ViewAppearanceHelper import me.rhunk.snapenhance.core.util.dataBuilder import me.rhunk.snapenhance.core.util.hook.HookStage @@ -41,198 +40,200 @@ import java.io.InputStream import java.lang.reflect.Method import kotlin.random.Random -class MediaFilePicker : Feature("Media File Picker", loadParams = FeatureLoadParams.ACTIVITY_CREATE_SYNC) { +class MediaFilePicker : Feature("Media File Picker") { var lastMediaDuration: Long? = null private set @SuppressLint("Recycle") - override fun onActivityCreate() { + override fun init() { if (!context.config.experimental.mediaFilePicker.get()) return - lateinit var chatMediaDrawerActionHandler: Any - lateinit var sendItemsMethod: Method - - findClass("com.snap.composer.memories.ChatMediaDrawer").genericSuperclass?.getTypeArguments()?.getOrNull(1)?.apply { - methods.first { - it.parameterTypes.size == 1 && it.parameterTypes[0].name.endsWith("ChatMediaDrawerActionHandler") - }.also { method -> - sendItemsMethod = method.parameterTypes[0].methods.first { it.name == "sendItems" } - }.hook(HookStage.AFTER) { - chatMediaDrawerActionHandler = it.arg(0) + onNextActivityCreate(defer = true) { + lateinit var chatMediaDrawerActionHandler: Any + lateinit var sendItemsMethod: Method + + findClass("com.snap.composer.memories.ChatMediaDrawer").genericSuperclass?.getTypeArguments()?.getOrNull(1)?.apply { + methods.first { + it.parameterTypes.size == 1 && it.parameterTypes[0].name.endsWith("ChatMediaDrawerActionHandler") + }.also { method -> + sendItemsMethod = method.parameterTypes[0].methods.first { it.name == "sendItems" } + }.hook(HookStage.AFTER) { + chatMediaDrawerActionHandler = it.arg(0) + } } - } - var requestCode: Int? = null - var firstVideoId: Long? = null - var mediaInputStream: InputStream? = null + var requestCode: Int? = null + var firstVideoId: Long? = null + var mediaInputStream: InputStream? = null - ContentResolver::class.java.apply { - hook("query", HookStage.AFTER) { param -> - val uri = param.arg<Uri>(0) - if (!uri.toString().endsWith(firstVideoId.toString())) return@hook + ContentResolver::class.java.apply { + hook("query", HookStage.AFTER) { param -> + val uri = param.arg<Uri>(0) + if (!uri.toString().endsWith(firstVideoId.toString())) return@hook - param.setResult(object: CursorWrapper(param.getResult() as Cursor) { - override fun getLong(columnIndex: Int): Long { - if (getColumnName(columnIndex) == "duration") { - return lastMediaDuration ?: -1 + param.setResult(object: CursorWrapper(param.getResult() as Cursor) { + override fun getLong(columnIndex: Int): Long { + if (getColumnName(columnIndex) == "duration") { + return lastMediaDuration ?: -1 + } + return super.getLong(columnIndex) } - return super.getLong(columnIndex) + }) + } + hook("openInputStream", HookStage.BEFORE) { param -> + val uri = param.arg<Uri>(0) + if (uri.toString().endsWith(firstVideoId.toString())) { + param.setResult(mediaInputStream) + mediaInputStream = null } - }) - } - hook("openInputStream", HookStage.BEFORE) { param -> - val uri = param.arg<Uri>(0) - if (uri.toString().endsWith(firstVideoId.toString())) { - param.setResult(mediaInputStream) - mediaInputStream = null } } - } - context.event.subscribe(ActivityResultEvent::class) { event -> - if (event.requestCode != requestCode || event.resultCode != Activity.RESULT_OK) return@subscribe - requestCode = null - - firstVideoId = context.androidContext.contentResolver.query( - MediaStore.Video.Media.EXTERNAL_CONTENT_URI, - arrayOf(MediaStore.Video.Media._ID), - null, - null, - "${MediaStore.Video.Media.DATE_TAKEN} DESC" - )?.use { cursor -> - if (cursor.moveToFirst()) { - cursor.getLongOrNull("_id") - } else { - null + context.event.subscribe(ActivityResultEvent::class) { event -> + if (event.requestCode != requestCode || event.resultCode != Activity.RESULT_OK) return@subscribe + requestCode = null + + firstVideoId = context.androidContext.contentResolver.query( + MediaStore.Video.Media.EXTERNAL_CONTENT_URI, + arrayOf(MediaStore.Video.Media._ID), + null, + null, + "${MediaStore.Video.Media.DATE_TAKEN} DESC" + )?.use { cursor -> + if (cursor.moveToFirst()) { + cursor.getLongOrNull("_id") + } else { + null + } } - } - if (firstVideoId == null) { - context.inAppOverlay.showStatusToast( - Icons.Default.Upload, - "Must have a video in gallery to upload." - ) - return@subscribe - } + if (firstVideoId == null) { + context.inAppOverlay.showStatusToast( + Icons.Default.Upload, + "Must have a video in gallery to upload." + ) + return@subscribe + } - fun sendMedia() { - sendItemsMethod.invoke(chatMediaDrawerActionHandler, listOf<Any>(), listOf( - sendItemsMethod.genericParameterTypes[1].getTypeArguments().first().dataBuilder { - from("_item") { - set("_cameraRollSource", "Snapchat") - set("_contentUri", "") - set("_durationMs", 0.0) - set("_disabled", false) - set("_imageRotation", 0.0) - set("_width", 1080.0) - set("_height", 1920.0) - set("_timestampMs", System.currentTimeMillis().toDouble()) - from("_itemId") { - set("_itemId", firstVideoId.toString()) - set("_type", "VIDEO") + fun sendMedia() { + sendItemsMethod.invoke(chatMediaDrawerActionHandler, listOf<Any>(), listOf( + sendItemsMethod.genericParameterTypes[1].getTypeArguments().first().dataBuilder { + from("_item") { + set("_cameraRollSource", "Snapchat") + set("_contentUri", "") + set("_durationMs", 0.0) + set("_disabled", false) + set("_imageRotation", 0.0) + set("_width", 1080.0) + set("_height", 1920.0) + set("_timestampMs", System.currentTimeMillis().toDouble()) + from("_itemId") { + set("_itemId", firstVideoId.toString()) + set("_type", "VIDEO") + } } + set("_order", 0.0) } - set("_order", 0.0) - } - )) - } + )) + } - fun startConversation(audioOnly: Boolean) { - context.coroutineScope.launch { - lastMediaDuration = MediaPlayer().run { - setDataSource(context.androidContext, event.intent.data!!) - prepare() - duration.toLong().also { - release() + fun startConversation(audioOnly: Boolean) { + context.coroutineScope.launch { + lastMediaDuration = MediaPlayer().run { + setDataSource(context.androidContext, event.intent.data!!) + prepare() + duration.toLong().also { + release() + } } - } - context.inAppOverlay.showStatusToast(Icons.Default.Crop, "Converting media...", durationMs = 3000) - val pfd = context.bridgeClient.convertMedia( - context.androidContext.contentResolver.openFileDescriptor(event.intent.data!!, "r")!!, - "m4a", - "m4a", - "aac", - if (!audioOnly) "libx264" else null - ) - - if (pfd == null) { - context.inAppOverlay.showStatusToast(Icons.Default.Error, "Failed to convert media.") - return@launch - } + context.inAppOverlay.showStatusToast(Icons.Default.Crop, "Converting media...", durationMs = 3000) + val pfd = context.bridgeClient.convertMedia( + context.androidContext.contentResolver.openFileDescriptor(event.intent.data!!, "r")!!, + "m4a", + "m4a", + "aac", + if (!audioOnly) "libx264" else null + ) + + if (pfd == null) { + context.inAppOverlay.showStatusToast(Icons.Default.Error, "Failed to convert media.") + return@launch + } - context.inAppOverlay.showStatusToast(Icons.Default.CheckCircleOutline, "Media converted successfully.") + context.inAppOverlay.showStatusToast(Icons.Default.CheckCircleOutline, "Media converted successfully.") - runCatching { - mediaInputStream = ParcelFileDescriptor.AutoCloseInputStream(pfd) - sendMedia() - }.onFailure { - mediaInputStream = null - context.log.error(it) - context.inAppOverlay.showStatusToast(Icons.Default.Error, "Failed to send media.") + runCatching { + mediaInputStream = ParcelFileDescriptor.AutoCloseInputStream(pfd) + sendMedia() + }.onFailure { + mediaInputStream = null + context.log.error(it) + context.inAppOverlay.showStatusToast(Icons.Default.Error, "Failed to send media.") + } } } - } - - val isAudio = context.androidContext.contentResolver.getType(event.intent.data!!)!!.startsWith("audio/") - if (isAudio || context.config.messaging.galleryMediaSendOverride.getNullable() == null) { - startConversation(isAudio) - return@subscribe - } + val isAudio = context.androidContext.contentResolver.getType(event.intent.data!!)!!.startsWith("audio/") - ViewAppearanceHelper.newAlertDialogBuilder(context.mainActivity!!) - .setTitle("Convert video file") - .setItems(arrayOf("Send as video/audio", "Send as audio only")) { _, which -> - startConversation(which == 1) + if (isAudio || context.config.messaging.galleryMediaSendOverride.getNullable() == null) { + startConversation(isAudio) + return@subscribe } - .setNegativeButton("Cancel") { dialog, _ -> dialog.dismiss() }.show() - } - val buttonTag = Random.nextInt(0, 65535) - - context.event.subscribe(AddViewEvent::class) { event -> - if (event.parent.id != context.resources.getId("chat_drawer_container") || !event.view::class.java.name.endsWith("ChatMediaDrawer")) return@subscribe - - event.view.addOnAttachStateChangeListener(object: View.OnAttachStateChangeListener { - override fun onViewAttachedToWindow(v: View) { - if (event.parent.findViewWithTag<View>(buttonTag)?.run { - visibility = View.VISIBLE - bringToFront() - } != null) return - event.parent.addView( - createComposeView(context.mainActivity!!) { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.End - ) { - FilledIconButton(onClick = { - requestCode = Random.nextInt(0, 65535) - this@MediaFilePicker.context.mainActivity!!.startActivityForResult( - Intent(Intent.ACTION_OPEN_DOCUMENT).apply { - addCategory(Intent.CATEGORY_OPENABLE) - type = "video/*" - putExtra(Intent.EXTRA_MIME_TYPES, arrayOf("video/*", "audio/*")) - }, - requestCode!! - ) - }) { - Icon(Icons.Default.Upload, "Upload media") + ViewAppearanceHelper.newAlertDialogBuilder(context.mainActivity!!) + .setTitle("Convert video file") + .setItems(arrayOf("Send as video/audio", "Send as audio only")) { _, which -> + startConversation(which == 1) + } + .setNegativeButton("Cancel") { dialog, _ -> dialog.dismiss() }.show() + } + + val buttonTag = Random.nextInt(0, 65535) + + context.event.subscribe(AddViewEvent::class) { event -> + if (event.parent.id != context.resources.getId("chat_drawer_container") || !event.view::class.java.name.endsWith("ChatMediaDrawer")) return@subscribe + + event.view.addOnAttachStateChangeListener(object: View.OnAttachStateChangeListener { + override fun onViewAttachedToWindow(v: View) { + if (event.parent.findViewWithTag<View>(buttonTag)?.run { + visibility = View.VISIBLE + bringToFront() + } != null) return + event.parent.addView( + createComposeView(context.mainActivity!!) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.End + ) { + FilledIconButton(onClick = { + requestCode = Random.nextInt(0, 65535) + this@MediaFilePicker.context.mainActivity!!.startActivityForResult( + Intent(Intent.ACTION_OPEN_DOCUMENT).apply { + addCategory(Intent.CATEGORY_OPENABLE) + type = "video/*" + putExtra(Intent.EXTRA_MIME_TYPES, arrayOf("video/*", "audio/*")) + }, + requestCode!! + ) + }) { + Icon(Icons.Default.Upload, "Upload media") + } } + }.apply { + tag = buttonTag + layoutParams = FrameLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT + ) } - }.apply { - tag = buttonTag - layoutParams = FrameLayout.LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, - ViewGroup.LayoutParams.WRAP_CONTENT - ) - } - ) - } - override fun onViewDetachedFromWindow(v: View) { - event.parent.findViewWithTag<View>(buttonTag)?.visibility = View.GONE - } - }) + ) + } + override fun onViewDetachedFromWindow(v: View) { + event.parent.findViewWithTag<View>(buttonTag)?.visibility = View.GONE + } + }) + } } } } \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/experiments/MeoPasscodeBypass.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/experiments/MeoPasscodeBypass.kt @@ -1,22 +1,23 @@ package me.rhunk.snapenhance.core.features.impl.experiments 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 import me.rhunk.snapenhance.mapper.impl.BCryptClassMapper -class MeoPasscodeBypass : Feature("Meo Passcode Bypass", loadParams = FeatureLoadParams.ACTIVITY_CREATE_ASYNC) { - override fun asyncOnActivityCreate() { +class MeoPasscodeBypass : Feature("Meo Passcode Bypass") { + override fun init() { if (!context.config.experimental.meoPasscodeBypass.get()) return - context.mappings.useMapper(BCryptClassMapper::class) { - classReference.get()?.hook( - hashMethod.get()!!, - HookStage.BEFORE, - ) { param -> - //set the hash to the result of the method - param.setResult(param.arg(1)) + onNextActivityCreate(defer = true) { + context.mappings.useMapper(BCryptClassMapper::class) { + classReference.get()?.hook( + hashMethod.get()!!, + HookStage.BEFORE, + ) { param -> + //set the hash to the result of the method + param.setResult(param.arg(1)) + } } } } diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/experiments/NoFriendScoreDelay.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/experiments/NoFriendScoreDelay.kt @@ -1,24 +1,25 @@ package me.rhunk.snapenhance.core.features.impl.experiments 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.hookConstructor import me.rhunk.snapenhance.mapper.impl.ScoreUpdateMapper import kotlin.time.Duration.Companion.days import kotlin.time.Duration.Companion.minutes -class NoFriendScoreDelay : Feature("NoFriendScoreDelay", loadParams = FeatureLoadParams.ACTIVITY_CREATE_SYNC) { - override fun onActivityCreate() { +class NoFriendScoreDelay : Feature("NoFriendScoreDelay") { + override fun init() { if (!context.config.experimental.noFriendScoreDelay.get()) return - context.mappings.useMapper(ScoreUpdateMapper::class) { - classReference.get()?.hookConstructor(HookStage.BEFORE) { param -> - param.args().indexOfFirst { - val longValue = it.toString().toLongOrNull() ?: return@indexOfFirst false - longValue > 30.minutes.inWholeMilliseconds && longValue < 10.days.inWholeMilliseconds - }.takeIf { it != -1 }?.let { index -> - param.setArg(index, 0) + onNextActivityCreate { + context.mappings.useMapper(ScoreUpdateMapper::class) { + classReference.get()?.hookConstructor(HookStage.BEFORE) { param -> + param.args().indexOfFirst { + val longValue = it.toString().toLongOrNull() ?: return@indexOfFirst false + longValue > 30.minutes.inWholeMilliseconds && longValue < 10.days.inWholeMilliseconds + }.takeIf { it != -1 }?.let { index -> + param.setArg(index, 0) + } } } } diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/experiments/PreventForcedLogout.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/experiments/PreventForcedLogout.kt @@ -2,11 +2,10 @@ package me.rhunk.snapenhance.core.features.impl.experiments import android.content.Intent 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 PreventForcedLogout : Feature("Prevent Forced Logout", loadParams = FeatureLoadParams.INIT_SYNC) { +class PreventForcedLogout : Feature("Prevent Forced Logout") { override fun init() { if (!context.config.experimental.preventForcedLogout.get()) return findClass("com.snap.identity.service.ForcedLogoutBroadcastReceiver").hook("onReceive", HookStage.BEFORE) { param -> diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/global/BypassVideoLengthRestriction.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/global/BypassVideoLengthRestriction.kt @@ -5,7 +5,6 @@ import android.os.FileObserver import com.google.gson.JsonParser import me.rhunk.snapenhance.core.event.events.impl.SendMessageWithContentEvent 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.hookConstructor import me.rhunk.snapenhance.core.util.ktx.setObjectField @@ -13,63 +12,65 @@ import me.rhunk.snapenhance.mapper.impl.DefaultMediaItemMapper import java.io.File class BypassVideoLengthRestriction : - Feature("BypassVideoLengthRestriction", loadParams = FeatureLoadParams.ACTIVITY_CREATE_ASYNC) { + Feature("BypassVideoLengthRestriction") { private lateinit var fileObserver: FileObserver - override fun asyncOnActivityCreate() { - val mode = context.config.global.bypassVideoLengthRestriction.getNullable() + override fun init() { + onNextActivityCreate(defer = true) { + val mode = context.config.global.bypassVideoLengthRestriction.getNullable() - if (mode == "single") { - //fix black videos when story is posted - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - val postedStorySnapFolder = - File(context.androidContext.filesDir, "file_manager/posted_story_snap") + if (mode == "single") { + //fix black videos when story is posted + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + val postedStorySnapFolder = + File(context.androidContext.filesDir, "file_manager/posted_story_snap") - fileObserver = (object : FileObserver(postedStorySnapFolder, MOVED_TO) { - override fun onEvent(event: Int, path: String?) { - if (event != MOVED_TO || path?.endsWith("posted_story_snap.2") != true) return - fileObserver.stopWatching() + fileObserver = (object : FileObserver(postedStorySnapFolder, MOVED_TO) { + override fun onEvent(event: Int, path: String?) { + if (event != MOVED_TO || path?.endsWith("posted_story_snap.2") != true) return + fileObserver.stopWatching() - val file = File(postedStorySnapFolder, path) - runCatching { - val fileContent = JsonParser.parseReader(file.reader()).asJsonObject - if (fileContent["timerOrDuration"].asLong < 0) file.delete() - }.onFailure { - context.log.error("Failed to read story metadata file", it) + val file = File(postedStorySnapFolder, path) + runCatching { + val fileContent = JsonParser.parseReader(file.reader()).asJsonObject + if (fileContent["timerOrDuration"].asLong < 0) file.delete() + }.onFailure { + context.log.error("Failed to read story metadata file", it) + } } - } - }) + }) - context.event.subscribe(SendMessageWithContentEvent::class) { event -> - if (event.destinations.stories!!.isEmpty()) return@subscribe - fileObserver.startWatching() + context.event.subscribe(SendMessageWithContentEvent::class) { event -> + if (event.destinations.stories!!.isEmpty()) return@subscribe + fileObserver.startWatching() + } } - } - context.mappings.useMapper(DefaultMediaItemMapper::class) { - defaultMediaItem.getAsClass()?.hookConstructor(HookStage.BEFORE) { param -> - //set the video length argument - param.setArg(5, -1L) + context.mappings.useMapper(DefaultMediaItemMapper::class) { + defaultMediaItem.getAsClass()?.hookConstructor(HookStage.BEFORE) { param -> + //set the video length argument + param.setArg(5, -1L) + } } } - } - //TODO: allow split from any source - if (mode == "split") { - // memories grid - context.mappings.useMapper(DefaultMediaItemMapper::class) { - cameraRollMediaId.getAsClass()?.hookConstructor(HookStage.AFTER) { param -> - //set the durationMs field - param.thisObject<Any>() - .setObjectField(durationMsField.get()!!, -1L) + //TODO: allow split from any source + if (mode == "split") { + // memories grid + context.mappings.useMapper(DefaultMediaItemMapper::class) { + cameraRollMediaId.getAsClass()?.hookConstructor(HookStage.AFTER) { param -> + //set the durationMs field + param.thisObject<Any>() + .setObjectField(durationMsField.get()!!, -1L) + } } - } - // chat camera roll grid - findClass("com.snap.composer.memories.MemoriesPickerVideoDurationConfig").hookConstructor(HookStage.AFTER) { param -> - param.thisObject<Any>().apply { - setObjectField("_maxSingleItemDurationMs", null) - setObjectField("_maxTotalDurationMs", null) + // chat camera roll grid + findClass("com.snap.composer.memories.MemoriesPickerVideoDurationConfig").hookConstructor(HookStage.AFTER) { param -> + param.thisObject<Any>().apply { + setObjectField("_maxSingleItemDurationMs", null) + setObjectField("_maxTotalDurationMs", null) + } } } } diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/global/DisableCustomTabs.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/global/DisableCustomTabs.kt @@ -2,17 +2,18 @@ package me.rhunk.snapenhance.core.features.impl.global import android.content.Intent 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 DisableCustomTabs: Feature("Disable Custom Tabs", loadParams = FeatureLoadParams.ACTIVITY_CREATE_SYNC) { - override fun onActivityCreate() { +class DisableCustomTabs: Feature("Disable Custom Tabs") { + override fun init() { if (!context.config.global.disableCustomTabs.get()) return - context.mainActivity!!.packageManager.javaClass.hook("resolveService", HookStage.BEFORE) { param -> - val intent = param.arg<Intent>(0) - if (intent.action == "android.support.customtabs.action.CustomTabsService") { - param.setResult(null) + onNextActivityCreate { activity -> + activity.packageManager.javaClass.hook("resolveService", HookStage.BEFORE) { param -> + val intent = param.arg<Intent>(0) + if (intent.action == "android.support.customtabs.action.CustomTabsService") { + param.setResult(null) + } } } } diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/global/DisableMemoriesSnapFeed.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/global/DisableMemoriesSnapFeed.kt @@ -1,24 +1,24 @@ package me.rhunk.snapenhance.core.features.impl.global 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 import me.rhunk.snapenhance.mapper.impl.MemoriesPresenterMapper -class DisableMemoriesSnapFeed : Feature("Disable Memories Snap Feed", loadParams = FeatureLoadParams.ACTIVITY_CREATE_SYNC) { - override fun onActivityCreate() { +class DisableMemoriesSnapFeed : Feature("Disable Memories Snap Feed") { + override fun init() { if (!context.config.global.disableMemoriesSnapFeed.get()) return + onNextActivityCreate { + context.mappings.useMapper(MemoriesPresenterMapper::class) { + classReference.get()?.apply { + val getNameMethod = getMethod("getName") ?: return@apply - context.mappings.useMapper(MemoriesPresenterMapper::class) { - classReference.get()?.apply { - val getNameMethod = getMethod("getName") ?: return@apply + hook(onNavigationEventMethod.get()!!, HookStage.BEFORE) { param -> + val instance = param.thisObject<Any>() - hook(onNavigationEventMethod.get()!!, HookStage.BEFORE) { param -> - val instance = param.thisObject<Any>() - - if (getNameMethod.invoke(instance) == "MemoriesAsyncPresenterFragmentSubscriber") { - param.setResult(null) + if (getNameMethod.invoke(instance) == "MemoriesAsyncPresenterFragmentSubscriber") { + param.setResult(null) + } } } } diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/global/DisableMetrics.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/global/DisableMetrics.kt @@ -4,9 +4,8 @@ import me.rhunk.snapenhance.common.util.protobuf.ProtoReader import me.rhunk.snapenhance.core.event.events.impl.NetworkApiRequestEvent import me.rhunk.snapenhance.core.event.events.impl.UnaryCallEvent import me.rhunk.snapenhance.core.features.Feature -import me.rhunk.snapenhance.core.features.FeatureLoadParams -class DisableMetrics : Feature("DisableMetrics", loadParams = FeatureLoadParams.INIT_SYNC) { +class DisableMetrics : Feature("DisableMetrics") { override fun init() { if (!context.config.global.disableMetrics.get()) return diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/global/DisableTelecomFramework.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/global/DisableTelecomFramework.kt @@ -2,11 +2,10 @@ package me.rhunk.snapenhance.core.features.impl.global import android.content.ContextWrapper 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 DisableTelecomFramework: Feature("Disable Telecom Framework", loadParams = FeatureLoadParams.INIT_SYNC) { +class DisableTelecomFramework: Feature("Disable Telecom Framework") { override fun init() { if (!context.config.global.disableTelecomFramework.get()) return diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/global/GooglePlayServicesDialogs.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/global/GooglePlayServicesDialogs.kt @@ -2,21 +2,22 @@ package me.rhunk.snapenhance.core.features.impl.global import android.app.AlertDialog 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 import java.lang.reflect.Modifier -class GooglePlayServicesDialogs : Feature("Disable GMS Dialogs", loadParams = FeatureLoadParams.ACTIVITY_CREATE_ASYNC) { - override fun asyncOnActivityCreate() { +class GooglePlayServicesDialogs : Feature("Disable GMS Dialogs") { + override fun init() { if (!context.config.global.disableGooglePlayDialogs.get()) return - findClass("com.google.android.gms.common.GoogleApiAvailability").methods - .first { Modifier.isStatic(it.modifiers) && it.returnType == AlertDialog::class.java }.let { method -> - method.hook(HookStage.BEFORE) { param -> - context.log.verbose("GoogleApiAvailability.showErrorDialogFragment() called, returning null") - param.setResult(null) - } + onNextActivityCreate(defer = true) { + findClass("com.google.android.gms.common.GoogleApiAvailability").methods + .first { Modifier.isStatic(it.modifiers) && it.returnType == AlertDialog::class.java }.let { method -> + method.hook(HookStage.BEFORE) { param -> + context.log.verbose("GoogleApiAvailability.showErrorDialogFragment() called, returning null") + param.setResult(null) + } + } } } } \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/global/MediaUploadQualityOverride.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/global/MediaUploadQualityOverride.kt @@ -2,13 +2,12 @@ package me.rhunk.snapenhance.core.features.impl.global import android.graphics.Bitmap 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 import me.rhunk.snapenhance.mapper.impl.MediaQualityLevelProviderMapper import java.lang.reflect.Method -class MediaUploadQualityOverride : Feature("Media Upload Quality Override", loadParams = FeatureLoadParams.INIT_SYNC) { +class MediaUploadQualityOverride : Feature("Media Upload Quality Override") { override fun init() { if (context.config.global.mediaUploadQualityConfig.forceVideoUploadSourceQuality.get()) { context.mappings.useMapper(MediaQualityLevelProviderMapper::class) { diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/global/SnapchatPlus.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/global/SnapchatPlus.kt @@ -1,7 +1,6 @@ package me.rhunk.snapenhance.core.features.impl.global 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 import me.rhunk.snapenhance.core.util.hook.hookConstructor @@ -9,7 +8,7 @@ import me.rhunk.snapenhance.core.util.ktx.getObjectField import me.rhunk.snapenhance.core.util.ktx.setObjectField import me.rhunk.snapenhance.mapper.impl.PlusSubscriptionMapper -class SnapchatPlus: Feature("SnapchatPlus", loadParams = FeatureLoadParams.INIT_SYNC) { +class SnapchatPlus: Feature("SnapchatPlus") { private val originalSubscriptionTime = (System.currentTimeMillis() - 7776000000L) private val expirationTimeMillis = (System.currentTimeMillis() + 15552000000L) diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/messaging/AutoMarkAsRead.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/messaging/AutoMarkAsRead.kt @@ -11,7 +11,6 @@ import me.rhunk.snapenhance.common.data.ContentType import me.rhunk.snapenhance.common.data.MessageUpdate import me.rhunk.snapenhance.core.event.events.impl.SendMessageWithContentEvent import me.rhunk.snapenhance.core.features.Feature -import me.rhunk.snapenhance.core.features.FeatureLoadParams import me.rhunk.snapenhance.core.features.impl.spying.StealthMode import me.rhunk.snapenhance.core.ui.ViewAppearanceHelper import me.rhunk.snapenhance.core.util.ktx.getObjectFieldOrNull @@ -19,7 +18,7 @@ import kotlin.coroutines.resume import kotlin.coroutines.suspendCoroutine import kotlin.random.Random -class AutoMarkAsRead : Feature("Auto Mark As Read", loadParams = FeatureLoadParams.INIT_SYNC) { +class AutoMarkAsRead : Feature("Auto Mark As Read") { val canMarkConversationAsRead by lazy { context.config.messaging.autoMarkAsRead.get().contains("conversation_read") } fun markConversationsAsRead(conversationIds: List<String>) { diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/messaging/AutoSave.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/messaging/AutoSave.kt @@ -4,7 +4,6 @@ import me.rhunk.snapenhance.common.data.MessageState import me.rhunk.snapenhance.common.data.MessageUpdate import me.rhunk.snapenhance.common.data.MessagingRuleType import me.rhunk.snapenhance.core.event.events.impl.ConversationUpdateEvent -import me.rhunk.snapenhance.core.features.FeatureLoadParams import me.rhunk.snapenhance.core.features.MessagingRuleFeature import me.rhunk.snapenhance.core.features.impl.spying.MessageLogger import me.rhunk.snapenhance.core.features.impl.spying.StealthMode @@ -16,7 +15,7 @@ import me.rhunk.snapenhance.core.wrapper.impl.SnapUUID import me.rhunk.snapenhance.mapper.impl.CallbackMapper import java.util.concurrent.Executors -class AutoSave : MessagingRuleFeature("Auto Save", MessagingRuleType.AUTO_SAVE, loadParams = FeatureLoadParams.ACTIVITY_CREATE_ASYNC) { +class AutoSave : MessagingRuleFeature("Auto Save", MessagingRuleType.AUTO_SAVE) { private val asyncSaveExecutorService = Executors.newSingleThreadExecutor() private val messageLogger by lazy { context.feature(MessageLogger::class) } @@ -68,37 +67,39 @@ class AutoSave : MessagingRuleFeature("Auto Save", MessagingRuleType.AUTO_SAVE, return canUseRule(targetConversationId) } - override fun asyncOnActivityCreate() { - // called when enter in a conversation - context.mappings.useMapper(CallbackMapper::class) { - callbacks.getClass("FetchConversationWithMessagesCallback")?.hook( - "onFetchConversationWithMessagesComplete", - HookStage.BEFORE, - { autoSaveFilter.isNotEmpty() } - ) { param -> - val conversationId = SnapUUID(param.arg<Any>(0).getObjectField("mConversationId")!!) - if (!canSaveInConversation(conversationId.toString())) return@hook - - val messages = param.arg<List<Any>>(1).map { Message(it) } - messages.forEach { - if (!canSaveMessage(it)) return@forEach - asyncSaveExecutorService.submit { - saveMessage(conversationId.toString(), it) + override fun init() { + onNextActivityCreate(defer = true) { + // called when enter in a conversation + context.mappings.useMapper(CallbackMapper::class) { + callbacks.getClass("FetchConversationWithMessagesCallback")?.hook( + "onFetchConversationWithMessagesComplete", + HookStage.BEFORE, + { autoSaveFilter.isNotEmpty() } + ) { param -> + val conversationId = SnapUUID(param.arg<Any>(0).getObjectField("mConversationId")!!) + if (!canSaveInConversation(conversationId.toString())) return@hook + + val messages = param.arg<List<Any>>(1).map { Message(it) } + messages.forEach { + if (!canSaveMessage(it)) return@forEach + asyncSaveExecutorService.submit { + saveMessage(conversationId.toString(), it) + } } } } - } - context.event.subscribe( - ConversationUpdateEvent::class, - { autoSaveFilter.isNotEmpty() } - ) { event -> - if (!canSaveInConversation(event.conversationId)) return@subscribe + context.event.subscribe( + ConversationUpdateEvent::class, + { autoSaveFilter.isNotEmpty() } + ) { event -> + if (!canSaveInConversation(event.conversationId)) return@subscribe - event.messages.forEach { message -> - if (!canSaveMessage(message)) return@forEach - asyncSaveExecutorService.submit { - saveMessage(event.conversationId, message) + event.messages.forEach { message -> + if (!canSaveMessage(message)) return@forEach + asyncSaveExecutorService.submit { + saveMessage(event.conversationId, message) + } } } } diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/messaging/BypassMessageActionRestrictions.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/messaging/BypassMessageActionRestrictions.kt @@ -2,15 +2,16 @@ package me.rhunk.snapenhance.core.features.impl.messaging import me.rhunk.snapenhance.core.event.events.impl.BuildMessageEvent import me.rhunk.snapenhance.core.features.Feature -import me.rhunk.snapenhance.core.features.FeatureLoadParams -class BypassMessageActionRestrictions : Feature("Bypass Message Action Restrictions", loadParams = FeatureLoadParams.ACTIVITY_CREATE_SYNC) { - override fun onActivityCreate() { +class BypassMessageActionRestrictions : Feature("Bypass Message Action Restrictions") { + override fun init() { if (!context.config.messaging.bypassMessageActionRestrictions.get()) return - context.event.subscribe(BuildMessageEvent::class, priority = 102) { event -> - event.message.messageMetadata?.apply { - isSaveable = true - isReactable = true + onNextActivityCreate { + context.event.subscribe(BuildMessageEvent::class, priority = 102) { event -> + event.message.messageMetadata?.apply { + isSaveable = true + isReactable = true + } } } } diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/messaging/CallStartConfirmation.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/messaging/CallStartConfirmation.kt @@ -5,7 +5,6 @@ import android.view.MotionEvent import android.view.View import android.view.ViewGroup import me.rhunk.snapenhance.core.features.Feature -import me.rhunk.snapenhance.core.features.FeatureLoadParams import me.rhunk.snapenhance.core.ui.ViewAppearanceHelper import me.rhunk.snapenhance.core.ui.children import me.rhunk.snapenhance.core.util.hook.HookAdapter @@ -13,7 +12,7 @@ import me.rhunk.snapenhance.core.util.hook.HookStage import me.rhunk.snapenhance.core.util.hook.hook import me.rhunk.snapenhance.core.util.ktx.getId -class CallStartConfirmation : Feature("CallStartConfirmation", loadParams = FeatureLoadParams.ACTIVITY_CREATE_SYNC) { +class CallStartConfirmation : Feature("CallStartConfirmation") { private fun hookTouchEvent(param: HookAdapter, motionEvent: MotionEvent, onConfirm: () -> Unit) { if (motionEvent.action != MotionEvent.ACTION_UP) return param.setResult(true) @@ -26,37 +25,39 @@ class CallStartConfirmation : Feature("CallStartConfirmation", loadParams = Feat } @SuppressLint("DiscouragedApi") - override fun onActivityCreate() { + override fun init() { if (!context.config.messaging.callStartConfirmation.get()) return - val callButtonsStub = context.resources.getId("call_buttons_stub") + onNextActivityCreate { + val callButtonsStub = context.resources.getId("call_buttons_stub") - findClass("com.snap.composer.views.ComposerRootView").hook("dispatchTouchEvent", HookStage.BEFORE) { param -> - val view = param.thisObject() as? ViewGroup ?: return@hook - if (view.id != callButtonsStub) return@hook - val childComposerView = view.getChildAt(0) as? ViewGroup ?: return@hook - // check if the child composer view contains 2 call buttons - if (childComposerView.children().count { - it::class.java == childComposerView::class.java - } != 2) return@hook - hookTouchEvent(param, param.arg(0)) { - param.invokeOriginal() + findClass("com.snap.composer.views.ComposerRootView").hook("dispatchTouchEvent", HookStage.BEFORE) { param -> + val view = param.thisObject() as? ViewGroup ?: return@hook + if (view.id != callButtonsStub) return@hook + val childComposerView = view.getChildAt(0) as? ViewGroup ?: return@hook + // check if the child composer view contains 2 call buttons + if (childComposerView.children().count { + it::class.java == childComposerView::class.java + } != 2) return@hook + hookTouchEvent(param, param.arg(0)) { + param.invokeOriginal() + } } - } - val callButton1 = context.resources.getId("friend_action_button3") - val callButton2 = context.resources.getId("friend_action_button4") + val callButton1 = context.resources.getId("friend_action_button3") + val callButton2 = context.resources.getId("friend_action_button4") - findClass("com.snap.ui.view.stackdraw.StackDrawLayout").hook("onTouchEvent", HookStage.BEFORE) { param -> - val view = param.thisObject<View>() - if (view.id != callButton1 && view.id != callButton2) return@hook + findClass("com.snap.ui.view.stackdraw.StackDrawLayout").hook("onTouchEvent", HookStage.BEFORE) { param -> + val view = param.thisObject<View>() + if (view.id != callButton1 && view.id != callButton2) return@hook - hookTouchEvent(param, param.arg(0)) { - arrayOf( - MotionEvent.obtain(0, 0, MotionEvent.ACTION_DOWN, 0f, 0f, 0), - MotionEvent.obtain(0, 0, MotionEvent.ACTION_UP, 0f, 0f, 0) - ).forEach { - param.invokeOriginal(arrayOf(it)) + hookTouchEvent(param, param.arg(0)) { + arrayOf( + MotionEvent.obtain(0, 0, MotionEvent.ACTION_DOWN, 0f, 0f, 0), + MotionEvent.obtain(0, 0, MotionEvent.ACTION_UP, 0f, 0f, 0) + ).forEach { + param.invokeOriginal(arrayOf(it)) + } } } } diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/messaging/DisableReplayInFF.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/messaging/DisableReplayInFF.kt @@ -1,22 +1,23 @@ package me.rhunk.snapenhance.core.features.impl.messaging 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.hookConstructor import me.rhunk.snapenhance.core.util.ktx.getObjectField import me.rhunk.snapenhance.core.util.ktx.setEnumField -class DisableReplayInFF : Feature("DisableReplayInFF", loadParams = FeatureLoadParams.ACTIVITY_CREATE_ASYNC) { - override fun asyncOnActivityCreate() { +class DisableReplayInFF : Feature("DisableReplayInFF") { + override fun init() { val state by context.config.messaging.disableReplayInFF - findClass("com.snapchat.client.messaging.InteractionInfo") - .hookConstructor(HookStage.AFTER, { state }) { param -> - val instance = param.thisObject<Any>() - if (instance.getObjectField("mLongPressActionState").toString() == "REQUEST_SNAP_REPLAY") { - instance.setEnumField("mLongPressActionState", "SHOW_CONVERSATION_ACTION_MENU") - } + onNextActivityCreate(defer = true) { + findClass("com.snapchat.client.messaging.InteractionInfo") + .hookConstructor(HookStage.AFTER, { state }) { param -> + val instance = param.thisObject<Any>() + if (instance.getObjectField("mLongPressActionState").toString() == "REQUEST_SNAP_REPLAY") { + instance.setEnumField("mLongPressActionState", "SHOW_CONVERSATION_ACTION_MENU") + } + } } } } \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/messaging/Messaging.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/messaging/Messaging.kt @@ -7,7 +7,6 @@ import me.rhunk.snapenhance.common.ReceiversConfig import me.rhunk.snapenhance.core.event.events.impl.ConversationUpdateEvent import me.rhunk.snapenhance.core.event.events.impl.OnSnapInteractionEvent import me.rhunk.snapenhance.core.features.Feature -import me.rhunk.snapenhance.core.features.FeatureLoadParams import me.rhunk.snapenhance.core.features.impl.spying.StealthMode import me.rhunk.snapenhance.core.util.EvictingMap import me.rhunk.snapenhance.core.util.hook.HookStage @@ -25,7 +24,7 @@ import me.rhunk.snapenhance.mapper.impl.FriendsFeedEventDispatcherMapper import java.util.UUID import java.util.concurrent.Future -class Messaging : Feature("Messaging", loadParams = FeatureLoadParams.ACTIVITY_CREATE_SYNC or FeatureLoadParams.INIT_ASYNC or FeatureLoadParams.INIT_SYNC) { +class Messaging : Feature("Messaging") { var conversationManager: ConversationManager? = null private set private var conversationManagerDelegate: Any? = null @@ -48,6 +47,7 @@ class Messaging : Feature("Messaging", loadParams = FeatureLoadParams.ACTIVITY_C } override fun init() { + val stealthMode = context.feature(StealthMode::class) context.classCache.conversationManager.hookConstructor(HookStage.BEFORE) { param -> conversationManager = ConversationManager(context, param.thisObject()) context.messagingBridge.triggerSessionStart() @@ -83,6 +83,80 @@ class Messaging : Feature("Messaging", loadParams = FeatureLoadParams.ACTIVITY_C } } } + + defer { + arrayOf("activate", "deactivate", "processTypingActivity").forEach { hook -> + context.classCache.presenceSession.hook(hook, HookStage.BEFORE, { + context.config.messaging.hideBitmojiPresence.get() || stealthMode.canUseRule(openedConversationUUID.toString()) + }) { + it.setResult(null) + } + } + + context.classCache.presenceSession.hook("startPeeking", HookStage.BEFORE, { + context.config.messaging.hidePeekAPeek.get() || stealthMode.canUseRule(openedConversationUUID.toString()) + }) { it.setResult(null) } + + //get last opened snap for media downloader + context.event.subscribe(OnSnapInteractionEvent::class) { event -> + openedConversationUUID = event.conversationId + lastFocusedMessageId = event.messageId + } + + context.classCache.conversationManager.hook("fetchMessage", HookStage.BEFORE) { param -> + val conversationId = SnapUUID(param.arg(0)).toString() + if (openedConversationUUID?.toString() == conversationId) { + lastFocusedMessageId = param.arg(1) + } + } + + context.classCache.conversationManager.hook("sendTypingNotification", HookStage.BEFORE, { + context.config.messaging.hideTypingNotifications.get() || stealthMode.canUseRule(openedConversationUUID.toString()) + }) { + it.setResult(null) + } + } + + onNextActivityCreate { + context.mappings.useMapper(FriendsFeedEventDispatcherMapper::class) { + classReference.getAsClass()?.hook("onItemLongPress", HookStage.BEFORE) { param -> + val viewItemContainer = param.arg<Any>(0) + val viewItem = viewItemContainer.getObjectField(viewModelField.get()!!).toString() + val conversationId = viewItem.substringAfter("conversationId: ").substring(0, 36).also { + if (it.startsWith("null")) return@hook + } + lastFocusedConversationId = conversationId + lastFocusedConversationType = context.database.getConversationType(conversationId) ?: 0 + } + } + + context.classCache.feedEntry.hookConstructor(HookStage.AFTER) { param -> + val instance = param.thisObject<Any>() + val interactionInfo = instance.getObjectFieldOrNull("mInteractionInfo") ?: return@hookConstructor + val messages = (interactionInfo.getObjectFieldOrNull("mMessages") as? List<*>)?.map { Message(it) } ?: return@hookConstructor + val conversationId = SnapUUID(instance.getObjectFieldOrNull("mConversationId") ?: return@hookConstructor).toString() + val myUserId = context.database.myUserId + + feedCachedSnapMessages[conversationId] = messages.filter { msg -> + msg.messageMetadata?.openedBy?.none { it.toString() == myUserId } == true + }.sortedBy { it.orderKey }.mapNotNull { it.messageDescriptor?.messageId } + } + + context.classCache.conversationManager.apply { + hook("enterConversation", HookStage.BEFORE) { param -> + openedConversationUUID = SnapUUID(param.arg(0)) + if (context.config.messaging.bypassMessageRetentionPolicy.get()) { + val callback = param.argNullable<Any>(2) ?: return@hook + callback::class.java.methods.firstOrNull { it.name == "onSuccess" }?.invoke(callback) + param.setResult(null) + } + } + + hook("exitConversation", HookStage.BEFORE) { + openedConversationUUID = null + } + } + } } fun getFeedCachedMessageIds(conversationId: String) = feedCachedSnapMessages[conversationId] @@ -116,82 +190,6 @@ class Messaging : Feature("Messaging", loadParams = FeatureLoadParams.ACTIVITY_C } } - override fun onActivityCreate() { - context.mappings.useMapper(FriendsFeedEventDispatcherMapper::class) { - classReference.getAsClass()?.hook("onItemLongPress", HookStage.BEFORE) { param -> - val viewItemContainer = param.arg<Any>(0) - val viewItem = viewItemContainer.getObjectField(viewModelField.get()!!).toString() - val conversationId = viewItem.substringAfter("conversationId: ").substring(0, 36).also { - if (it.startsWith("null")) return@hook - } - lastFocusedConversationId = conversationId - lastFocusedConversationType = context.database.getConversationType(conversationId) ?: 0 - } - } - - context.classCache.feedEntry.hookConstructor(HookStage.AFTER) { param -> - val instance = param.thisObject<Any>() - val interactionInfo = instance.getObjectFieldOrNull("mInteractionInfo") ?: return@hookConstructor - val messages = (interactionInfo.getObjectFieldOrNull("mMessages") as? List<*>)?.map { Message(it) } ?: return@hookConstructor - val conversationId = SnapUUID(instance.getObjectFieldOrNull("mConversationId") ?: return@hookConstructor).toString() - val myUserId = context.database.myUserId - - feedCachedSnapMessages[conversationId] = messages.filter { msg -> - msg.messageMetadata?.openedBy?.none { it.toString() == myUserId } == true - }.sortedBy { it.orderKey }.mapNotNull { it.messageDescriptor?.messageId } - } - - context.classCache.conversationManager.apply { - hook("enterConversation", HookStage.BEFORE) { param -> - openedConversationUUID = SnapUUID(param.arg(0)) - if (context.config.messaging.bypassMessageRetentionPolicy.get()) { - val callback = param.argNullable<Any>(2) ?: return@hook - callback::class.java.methods.firstOrNull { it.name == "onSuccess" }?.invoke(callback) - param.setResult(null) - } - } - - hook("exitConversation", HookStage.BEFORE) { - openedConversationUUID = null - } - } - } - - override fun asyncInit() { - val stealthMode = context.feature(StealthMode::class) - - arrayOf("activate", "deactivate", "processTypingActivity").forEach { hook -> - context.classCache.presenceSession.hook(hook, HookStage.BEFORE, { - context.config.messaging.hideBitmojiPresence.get() || stealthMode.canUseRule(openedConversationUUID.toString()) - }) { - it.setResult(null) - } - } - - context.classCache.presenceSession.hook("startPeeking", HookStage.BEFORE, { - context.config.messaging.hidePeekAPeek.get() || stealthMode.canUseRule(openedConversationUUID.toString()) - }) { it.setResult(null) } - - //get last opened snap for media downloader - context.event.subscribe(OnSnapInteractionEvent::class) { event -> - openedConversationUUID = event.conversationId - lastFocusedMessageId = event.messageId - } - - context.classCache.conversationManager.hook("fetchMessage", HookStage.BEFORE) { param -> - val conversationId = SnapUUID(param.arg(0)).toString() - if (openedConversationUUID?.toString() == conversationId) { - lastFocusedMessageId = param.arg(1) - } - } - - context.classCache.conversationManager.hook("sendTypingNotification", HookStage.BEFORE, { - context.config.messaging.hideTypingNotifications.get() || stealthMode.canUseRule(openedConversationUUID.toString()) - }) { - it.setResult(null) - } - } - fun fetchSnapchatterInfos(userIds: List<String>): List<Snapchatter> { val identity = identityDelegate ?: return emptyList() val snapUUIDs = userIds.map { diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/messaging/Notifications.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/messaging/Notifications.kt @@ -23,7 +23,6 @@ import me.rhunk.snapenhance.common.util.snap.MediaDownloaderHelper import me.rhunk.snapenhance.common.util.snap.SnapWidgetBroadcastReceiverHelper import me.rhunk.snapenhance.core.event.events.impl.SnapWidgetBroadcastReceiveEvent import me.rhunk.snapenhance.core.features.Feature -import me.rhunk.snapenhance.core.features.FeatureLoadParams import me.rhunk.snapenhance.core.features.impl.FriendMutationObserver import me.rhunk.snapenhance.core.features.impl.downloader.MediaDownloader import me.rhunk.snapenhance.core.features.impl.downloader.decoder.AttachmentType @@ -39,7 +38,7 @@ import me.rhunk.snapenhance.core.wrapper.impl.SnapUUID import okhttp3.RequestBody.Companion.toRequestBody import kotlin.coroutines.suspendCoroutine -class Notifications : Feature("Notifications", loadParams = FeatureLoadParams.INIT_SYNC) { +class Notifications : Feature("Notifications") { inner class NotificationData( val tag: String?, val id: Int, @@ -301,7 +300,7 @@ class Notifications : Feature("Notifications", loadParams = FeatureLoadParams.IN ) } }.onFailure { - context.log.warn("Failed to set notification group key: ${it.stackTraceToString()}", featureKey) + context.log.warn("Failed to set notification group key: ${it.stackTraceToString()}", key) } } diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/messaging/PreventMessageSending.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/messaging/PreventMessageSending.kt @@ -5,12 +5,11 @@ import me.rhunk.snapenhance.common.util.protobuf.ProtoEditor import me.rhunk.snapenhance.core.event.events.impl.NativeUnaryCallEvent import me.rhunk.snapenhance.core.event.events.impl.SendMessageWithContentEvent 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 PreventMessageSending : Feature("Prevent message sending", loadParams = FeatureLoadParams.ACTIVITY_CREATE_ASYNC) { - override fun asyncOnActivityCreate() { +class PreventMessageSending : Feature("Prevent message sending") { + override fun init() { val preventMessageSending by context.config.messaging.preventMessageSending context.event.subscribe(NativeUnaryCallEvent::class, { preventMessageSending.contains("snap_replay") }) { event -> diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/messaging/SendOverride.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/messaging/SendOverride.kt @@ -8,14 +8,13 @@ import me.rhunk.snapenhance.common.util.protobuf.ProtoReader import me.rhunk.snapenhance.core.event.events.impl.NativeUnaryCallEvent import me.rhunk.snapenhance.core.event.events.impl.SendMessageWithContentEvent import me.rhunk.snapenhance.core.features.Feature -import me.rhunk.snapenhance.core.features.FeatureLoadParams import me.rhunk.snapenhance.core.features.impl.experiments.MediaFilePicker import me.rhunk.snapenhance.core.messaging.MessageSender import me.rhunk.snapenhance.core.ui.ViewAppearanceHelper import me.rhunk.snapenhance.nativelib.NativeLib import java.util.Locale -class SendOverride : Feature("Send Override", loadParams = FeatureLoadParams.INIT_SYNC) { +class SendOverride : Feature("Send Override") { private var isLastSnapSavable = false private val typeNames by lazy { mutableListOf("ORIGINAL", "SNAP", "NOTE").also { diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/messaging/UnlimitedSnapViewTime.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/messaging/UnlimitedSnapViewTime.kt @@ -6,27 +6,27 @@ import me.rhunk.snapenhance.common.util.protobuf.ProtoEditor import me.rhunk.snapenhance.common.util.protobuf.ProtoReader import me.rhunk.snapenhance.core.event.events.impl.BuildMessageEvent import me.rhunk.snapenhance.core.features.Feature -import me.rhunk.snapenhance.core.features.FeatureLoadParams -class UnlimitedSnapViewTime : - Feature("UnlimitedSnapViewTime", loadParams = FeatureLoadParams.ACTIVITY_CREATE_SYNC) { - override fun onActivityCreate() { - val state by context.config.messaging.unlimitedSnapViewTime +class UnlimitedSnapViewTime : Feature("UnlimitedSnapViewTime") { + override fun init() { + onNextActivityCreate { + val state by context.config.messaging.unlimitedSnapViewTime - context.event.subscribe(BuildMessageEvent::class, { state }, priority = 101) { event -> - if (event.message.messageState != MessageState.COMMITTED) return@subscribe - if (event.message.messageContent!!.contentType != ContentType.SNAP) return@subscribe + context.event.subscribe(BuildMessageEvent::class, { state }, priority = 101) { event -> + if (event.message.messageState != MessageState.COMMITTED) return@subscribe + if (event.message.messageContent!!.contentType != ContentType.SNAP) return@subscribe - val messageContent = event.message.messageContent + val messageContent = event.message.messageContent - val mediaAttributes = ProtoReader(messageContent!!.content!!).followPath(11, 5, 2) ?: return@subscribe - if (mediaAttributes.contains(6)) return@subscribe - messageContent.content = ProtoEditor(messageContent.content!!).apply { - edit(11, 5, 2) { - remove(8) - addBuffer(6, byteArrayOf()) - } - }.toByteArray() + val mediaAttributes = ProtoReader(messageContent!!.content!!).followPath(11, 5, 2) ?: return@subscribe + if (mediaAttributes.contains(6)) return@subscribe + messageContent.content = ProtoEditor(messageContent.content!!).apply { + edit(11, 5, 2) { + remove(8) + addBuffer(6, byteArrayOf()) + } + }.toByteArray() + } } } } diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/spying/FriendTracker.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/spying/FriendTracker.kt @@ -12,7 +12,6 @@ import me.rhunk.snapenhance.common.util.lazyBridge import me.rhunk.snapenhance.common.util.protobuf.ProtoReader import me.rhunk.snapenhance.common.util.toParcelable import me.rhunk.snapenhance.core.features.Feature -import me.rhunk.snapenhance.core.features.FeatureLoadParams import me.rhunk.snapenhance.core.features.impl.messaging.Messaging import me.rhunk.snapenhance.core.util.hook.HookStage import me.rhunk.snapenhance.core.util.hook.hook @@ -23,7 +22,7 @@ import me.rhunk.snapenhance.nativelib.NativeLib import java.lang.reflect.Method import java.nio.ByteBuffer -class FriendTracker : Feature("Friend Tracker", loadParams = FeatureLoadParams.INIT_SYNC) { +class FriendTracker : Feature("Friend Tracker") { private val conversationPresenceState = mutableMapOf<String, MutableMap<String, FriendPresenceState?>>() // conversationId -> (userId -> state) private val tracker by lazyBridge { context.bridgeClient.getTracker() } private val notificationManager by lazy { context.androidContext.getSystemService(NotificationManager::class.java).apply { diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/spying/HalfSwipeNotifier.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/spying/HalfSwipeNotifier.kt @@ -6,7 +6,6 @@ import android.app.NotificationManager import android.app.PendingIntent import me.rhunk.snapenhance.common.Constants 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 import me.rhunk.snapenhance.core.util.hook.hookConstructor @@ -16,7 +15,7 @@ import me.rhunk.snapenhance.mapper.impl.CallbackMapper import java.util.concurrent.ConcurrentHashMap import kotlin.time.Duration.Companion.milliseconds -class HalfSwipeNotifier : Feature("Half Swipe Notifier", loadParams = FeatureLoadParams.INIT_SYNC) { +class HalfSwipeNotifier : Feature("Half Swipe Notifier") { private val peekingConversations = ConcurrentHashMap<String, List<String>>() private val startPeekingTimestamps = ConcurrentHashMap<String, Long>() diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/spying/MessageLogger.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/spying/MessageLogger.kt @@ -18,18 +18,13 @@ import me.rhunk.snapenhance.common.util.protobuf.ProtoReader import me.rhunk.snapenhance.core.event.events.impl.BindViewEvent import me.rhunk.snapenhance.core.event.events.impl.BuildMessageEvent import me.rhunk.snapenhance.core.features.Feature -import me.rhunk.snapenhance.core.features.FeatureLoadParams import me.rhunk.snapenhance.core.ui.addForegroundDrawable import me.rhunk.snapenhance.core.ui.removeForegroundDrawable import me.rhunk.snapenhance.core.util.EvictingMap import java.util.concurrent.Executors import kotlin.system.measureTimeMillis -class MessageLogger : Feature("MessageLogger", - loadParams = FeatureLoadParams.INIT_SYNC or - FeatureLoadParams.ACTIVITY_CREATE_SYNC or - FeatureLoadParams.ACTIVITY_CREATE_ASYNC -) { +class MessageLogger : Feature("MessageLogger") { companion object { const val PREFETCH_MESSAGE_COUNT = 20 const val PREFETCH_FEED_COUNT = 20 @@ -96,23 +91,20 @@ class MessageLogger : Feature("MessageLogger", return computeMessageIdentifier(conversationId, serverMessageId) } - override fun asyncOnActivityCreate() { - if (!isEnabled || !context.database.hasArroyo()) { - return - } - - measureTimeMillis { - val conversationIds = context.database.getFeedEntries(PREFETCH_FEED_COUNT).map { it.key!! } - if (conversationIds.isEmpty()) return@measureTimeMillis - fetchedMessages.addAll(loggerInterface.getLoggedIds(conversationIds.toTypedArray(), PREFETCH_MESSAGE_COUNT).toList()) - }.also { context.log.verbose("Loaded ${fetchedMessages.size} cached messages in ${it}ms") } - } - override fun init() { if (!isEnabled) return val keepMyOwnMessages = context.config.messaging.messageLogger.keepMyOwnMessages.get() val messageFilter by context.config.messaging.messageLogger.messageFilter + onNextActivityCreate(defer = true) { + if (!context.database.hasArroyo()) return@onNextActivityCreate + measureTimeMillis { + val conversationIds = context.database.getFeedEntries(PREFETCH_FEED_COUNT).map { it.key!! } + if (conversationIds.isEmpty()) return@measureTimeMillis + fetchedMessages.addAll(loggerInterface.getLoggedIds(conversationIds.toTypedArray(), PREFETCH_MESSAGE_COUNT).toList()) + }.also { context.log.verbose("Loaded ${fetchedMessages.size} cached messages in ${it}ms") } + } + context.event.subscribe(BuildMessageEvent::class, priority = 1) { event -> val messageInstance = event.message.instanceNonNull() if (event.message.messageState != MessageState.COMMITTED) return@subscribe @@ -183,10 +175,6 @@ class MessageLogger : Feature("MessageLogger", deletedMessageCache[uniqueMessageIdentifier] = deletedMessageObject } - } - - override fun onActivityCreate() { - if (!isEnabled) return context.event.subscribe(BindViewEvent::class) { event -> event.chatMessage { conversationId, messageId -> diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/spying/StealthMode.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/spying/StealthMode.kt @@ -2,14 +2,13 @@ package me.rhunk.snapenhance.core.features.impl.spying import me.rhunk.snapenhance.common.data.MessagingRuleType import me.rhunk.snapenhance.core.event.events.impl.OnSnapInteractionEvent -import me.rhunk.snapenhance.core.features.FeatureLoadParams import me.rhunk.snapenhance.core.features.MessagingRuleFeature import me.rhunk.snapenhance.core.util.hook.HookStage import me.rhunk.snapenhance.core.util.hook.hook import me.rhunk.snapenhance.core.wrapper.impl.SnapUUID import java.util.concurrent.CopyOnWriteArraySet -class StealthMode : MessagingRuleFeature("StealthMode", MessagingRuleType.STEALTH, loadParams = FeatureLoadParams.INIT_SYNC) { +class StealthMode : MessagingRuleFeature("StealthMode", MessagingRuleType.STEALTH) { private val displayedMessageQueue = CopyOnWriteArraySet<Long>() fun addDisplayedMessageException(clientMessageId: Long) { diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/tweaks/BypassScreenshotDetection.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/tweaks/BypassScreenshotDetection.kt @@ -5,12 +5,11 @@ import android.content.ContentResolver import android.database.ContentObserver import android.net.Uri 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 BypassScreenshotDetection : Feature("BypassScreenshotDetection", loadParams = FeatureLoadParams.ACTIVITY_CREATE_SYNC) { - override fun onActivityCreate() { +class BypassScreenshotDetection : Feature("BypassScreenshotDetection") { + override fun init() { if (!context.config.messaging.bypassScreenshotDetection.get()) return Activity::class.java.hook("registerScreenCaptureCallback", HookStage.BEFORE) { param -> param.setResult(null) diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/tweaks/CameraTweaks.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/tweaks/CameraTweaks.kt @@ -12,100 +12,101 @@ import android.media.Image import android.media.ImageReader import android.util.Range 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 import me.rhunk.snapenhance.core.util.ktx.setObjectField import java.io.ByteArrayOutputStream import java.nio.ByteBuffer -class CameraTweaks : Feature("Camera Tweaks", loadParams = FeatureLoadParams.ACTIVITY_CREATE_SYNC) { +class CameraTweaks : Feature("Camera Tweaks") { private fun parseResolution(resolution: String): IntArray? { return runCatching { resolution.split("x").map { it.toInt() }.toIntArray() }.getOrNull() } @SuppressLint("MissingPermission", "DiscouragedApi") - override fun onActivityCreate() { - val config = context.config.camera - - val frontCameraId = runCatching { context.androidContext.getSystemService(CameraManager::class.java).run { - cameraIdList.firstOrNull { getCameraCharacteristics(it).get(CameraCharacteristics.LENS_FACING) == CameraCharacteristics.LENS_FACING_FRONT } - } }.getOrNull() - - if (config.disableCameras.get().isNotEmpty() && frontCameraId != null) { - ContextWrapper::class.java.hook("checkPermission", HookStage.BEFORE) { param -> - val permission = param.arg<String>(0) - if (permission == Manifest.permission.CAMERA) { - param.setResult(PackageManager.PERMISSION_GRANTED) + override fun init() { + onNextActivityCreate { + val config = context.config.camera + + val frontCameraId = runCatching { context.androidContext.getSystemService(CameraManager::class.java).run { + cameraIdList.firstOrNull { getCameraCharacteristics(it).get(CameraCharacteristics.LENS_FACING) == CameraCharacteristics.LENS_FACING_FRONT } + } }.getOrNull() + + if (config.disableCameras.get().isNotEmpty() && frontCameraId != null) { + ContextWrapper::class.java.hook("checkPermission", HookStage.BEFORE) { param -> + val permission = param.arg<String>(0) + if (permission == Manifest.permission.CAMERA) { + param.setResult(PackageManager.PERMISSION_GRANTED) + } } } - } - var isLastCameraFront = false + var isLastCameraFront = false - CameraManager::class.java.hook("openCamera", HookStage.BEFORE) { param -> - val cameraManager = param.thisObject() as? CameraManager ?: return@hook - val cameraId = param.arg<String>(0) - val disabledCameras = config.disableCameras.get() + CameraManager::class.java.hook("openCamera", HookStage.BEFORE) { param -> + val cameraManager = param.thisObject() as? CameraManager ?: return@hook + val cameraId = param.arg<String>(0) + val disabledCameras = config.disableCameras.get() - if (disabledCameras.size >= 2) { - param.setResult(null) - return@hook - } + if (disabledCameras.size >= 2) { + param.setResult(null) + return@hook + } - isLastCameraFront = cameraId == frontCameraId + isLastCameraFront = cameraId == frontCameraId - if (disabledCameras.size != 1) return@hook + if (disabledCameras.size != 1) return@hook - // trick to replace unwanted camera with another one - if ((disabledCameras.contains("front") && isLastCameraFront) || (disabledCameras.contains("back") && !isLastCameraFront)) { - param.setArg(0, cameraManager.cameraIdList.filterNot { it == cameraId }.firstOrNull() ?: return@hook) - isLastCameraFront = !isLastCameraFront + // trick to replace unwanted camera with another one + if ((disabledCameras.contains("front") && isLastCameraFront) || (disabledCameras.contains("back") && !isLastCameraFront)) { + param.setArg(0, cameraManager.cameraIdList.filterNot { it == cameraId }.firstOrNull() ?: return@hook) + isLastCameraFront = !isLastCameraFront + } } - } - - ImageReader::class.java.hook("newInstance", HookStage.BEFORE) { param -> - val captureResolutionConfig = config.customResolution.getNullable()?.takeIf { it.isNotEmpty() }?.let { parseResolution(it) } - ?: (if (isLastCameraFront) config.overrideFrontResolution.getNullable() else config.overrideBackResolution.getNullable())?.let { parseResolution(it) } ?: return@hook - param.setArg(0, captureResolutionConfig[0]) - param.setArg(1, captureResolutionConfig[1]) - } - CameraCharacteristics::class.java.hook("get", HookStage.AFTER) { param -> - val key = param.argNullable<Key<*>>(0) ?: return@hook + ImageReader::class.java.hook("newInstance", HookStage.BEFORE) { param -> + val captureResolutionConfig = config.customResolution.getNullable()?.takeIf { it.isNotEmpty() }?.let { parseResolution(it) } + ?: (if (isLastCameraFront) config.overrideFrontResolution.getNullable() else config.overrideBackResolution.getNullable())?.let { parseResolution(it) } ?: return@hook + param.setArg(0, captureResolutionConfig[0]) + param.setArg(1, captureResolutionConfig[1]) + } - if (key == CameraCharacteristics.LENS_FACING) { - val disabledCameras = config.disableCameras.get() - //FIXME: unexpected behavior when app is resumed - if (disabledCameras.size == 1) { - val isFrontCamera = param.getResult() as? Int == CameraCharacteristics.LENS_FACING_FRONT - if ((disabledCameras.contains("front") && isFrontCamera) || (disabledCameras.contains("back") && !isFrontCamera)) { - param.setResult(if (isFrontCamera) CameraCharacteristics.LENS_FACING_BACK else CameraCharacteristics.LENS_FACING_FRONT) + CameraCharacteristics::class.java.hook("get", HookStage.AFTER) { param -> + val key = param.argNullable<Key<*>>(0) ?: return@hook + + if (key == CameraCharacteristics.LENS_FACING) { + val disabledCameras = config.disableCameras.get() + //FIXME: unexpected behavior when app is resumed + if (disabledCameras.size == 1) { + val isFrontCamera = param.getResult() as? Int == CameraCharacteristics.LENS_FACING_FRONT + if ((disabledCameras.contains("front") && isFrontCamera) || (disabledCameras.contains("back") && !isFrontCamera)) { + param.setResult(if (isFrontCamera) CameraCharacteristics.LENS_FACING_BACK else CameraCharacteristics.LENS_FACING_FRONT) + } } } - } - if (key == CameraCharacteristics.CONTROL_AE_AVAILABLE_TARGET_FPS_RANGES) { - val isFrontCamera = param.invokeOriginal( - arrayOf(CameraCharacteristics.LENS_FACING) - ) == CameraCharacteristics.LENS_FACING_FRONT - val customFrameRate = (if (isFrontCamera) config.frontCustomFrameRate.getNullable() else config.backCustomFrameRate.getNullable())?.toIntOrNull() ?: return@hook - param.setResult(arrayOf(Range(customFrameRate, customFrameRate))) + if (key == CameraCharacteristics.CONTROL_AE_AVAILABLE_TARGET_FPS_RANGES) { + val isFrontCamera = param.invokeOriginal( + arrayOf(CameraCharacteristics.LENS_FACING) + ) == CameraCharacteristics.LENS_FACING_FRONT + val customFrameRate = (if (isFrontCamera) config.frontCustomFrameRate.getNullable() else config.backCustomFrameRate.getNullable())?.toIntOrNull() ?: return@hook + param.setResult(arrayOf(Range(customFrameRate, customFrameRate))) + } } - } - if (config.blackPhotos.get()) { - findClass("android.media.ImageReader\$SurfaceImage").hook("getPlanes", HookStage.AFTER) { param -> - val image = param.thisObject() as? Image ?: return@hook - val planes = param.getResult() as? Array<*> ?: return@hook - val output = ByteArrayOutputStream() - Bitmap.createBitmap(image.width, image.height, Bitmap.Config.ARGB_8888).apply { - compress(Bitmap.CompressFormat.JPEG, 100, output) - recycle() - } - planes.filterNotNull().forEach { plane -> - plane.setObjectField("mBuffer", ByteBuffer.wrap(output.toByteArray())) + if (config.blackPhotos.get()) { + findClass("android.media.ImageReader\$SurfaceImage").hook("getPlanes", HookStage.AFTER) { param -> + val image = param.thisObject() as? Image ?: return@hook + val planes = param.getResult() as? Array<*> ?: return@hook + val output = ByteArrayOutputStream() + Bitmap.createBitmap(image.width, image.height, Bitmap.Config.ARGB_8888).apply { + compress(Bitmap.CompressFormat.JPEG, 100, output) + recycle() + } + planes.filterNotNull().forEach { plane -> + plane.setObjectField("mBuffer", ByteBuffer.wrap(output.toByteArray())) + } } } } diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/tweaks/DisablePermissionRequests.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/tweaks/DisablePermissionRequests.kt @@ -4,11 +4,10 @@ import android.content.ContextWrapper import android.content.pm.PackageManager import me.rhunk.snapenhance.common.config.impl.Global 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 DisablePermissionRequests : Feature("Disable Permission Requests", loadParams = FeatureLoadParams.INIT_SYNC) { +class DisablePermissionRequests : Feature("Disable Permission Requests") { override fun init() { val deniedPermissions by context.config.global.disablePermissionRequests if (deniedPermissions.isEmpty()) return diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/tweaks/HideActiveMusic.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/tweaks/HideActiveMusic.kt @@ -2,15 +2,16 @@ package me.rhunk.snapenhance.core.features.impl.tweaks import android.media.AudioManager 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 HideActiveMusic: Feature("Hide Active Music", loadParams = FeatureLoadParams.ACTIVITY_CREATE_SYNC) { - override fun onActivityCreate() { +class HideActiveMusic: Feature("Hide Active Music") { + override fun init() { if (!context.config.global.hideActiveMusic.get()) return - AudioManager::class.java.hook("isMusicActive", HookStage.BEFORE) { - it.setResult(false) + onNextActivityCreate { + AudioManager::class.java.hook("isMusicActive", HookStage.BEFORE) { + it.setResult(false) + } } } } \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/tweaks/PreventMessageListAutoScroll.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/tweaks/PreventMessageListAutoScroll.kt @@ -4,73 +4,74 @@ import android.view.View import me.rhunk.snapenhance.core.event.events.impl.BindViewEvent import me.rhunk.snapenhance.core.event.events.impl.ConversationUpdateEvent 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 import me.rhunk.snapenhance.core.wrapper.impl.SnapUUID -class PreventMessageListAutoScroll : Feature("PreventMessageListAutoScroll", loadParams = FeatureLoadParams.ACTIVITY_CREATE_SYNC) { +class PreventMessageListAutoScroll : Feature("PreventMessageListAutoScroll") { private var openedConversationId: String? = null private val focusedMessages = mutableMapOf<View, Long>() private var firstFocusedMessageId: Long? = null private val delayedMessageUpdates = mutableListOf<() -> Unit>() - override fun onActivityCreate() { + override fun init() { if (!context.config.userInterface.preventMessageListAutoScroll.get()) return - context.event.subscribe(ConversationUpdateEvent::class) { event -> - val updatedMessage = event.messages.firstOrNull() ?: return@subscribe - if (openedConversationId != updatedMessage.messageDescriptor?.conversationId.toString()) return@subscribe + onNextActivityCreate { + context.event.subscribe(ConversationUpdateEvent::class) { event -> + val updatedMessage = event.messages.firstOrNull() ?: return@subscribe + if (openedConversationId != updatedMessage.messageDescriptor?.conversationId.toString()) return@subscribe - // cancel if the message is already in focus - if (focusedMessages.entries.any { entry -> entry.value == updatedMessage.messageDescriptor?.messageId && entry.key.isAttachedToWindow }) return@subscribe + // cancel if the message is already in focus + if (focusedMessages.entries.any { entry -> entry.value == updatedMessage.messageDescriptor?.messageId && entry.key.isAttachedToWindow }) return@subscribe - val conversationLastMessages = context.database.getMessagesFromConversationId( - openedConversationId.toString(), - 4 - ) ?: return@subscribe + val conversationLastMessages = context.database.getMessagesFromConversationId( + openedConversationId.toString(), + 4 + ) ?: return@subscribe - if (conversationLastMessages.none { - focusedMessages.entries.any { entry -> entry.value == it.clientMessageId.toLong() && entry.key.isAttachedToWindow } - }) { - synchronized(delayedMessageUpdates) { - if (firstFocusedMessageId == null) firstFocusedMessageId = conversationLastMessages.lastOrNull()?.clientMessageId?.toLong() - delayedMessageUpdates.add { - event.adapter.invokeOriginal() + if (conversationLastMessages.none { + focusedMessages.entries.any { entry -> entry.value == it.clientMessageId.toLong() && entry.key.isAttachedToWindow } + }) { + synchronized(delayedMessageUpdates) { + if (firstFocusedMessageId == null) firstFocusedMessageId = conversationLastMessages.lastOrNull()?.clientMessageId?.toLong() + delayedMessageUpdates.add { + event.adapter.invokeOriginal() + } } + event.adapter.setResult(null) } - event.adapter.setResult(null) } - } - context.classCache.conversationManager.apply { - hook("enterConversation", HookStage.BEFORE) { param -> - openedConversationId = SnapUUID(param.arg(0)).toString() - } - hook("exitConversation", HookStage.BEFORE) { - openedConversationId = null - firstFocusedMessageId = null - synchronized(focusedMessages) { - focusedMessages.clear() + context.classCache.conversationManager.apply { + hook("enterConversation", HookStage.BEFORE) { param -> + openedConversationId = SnapUUID(param.arg(0)).toString() } - synchronized(delayedMessageUpdates) { - delayedMessageUpdates.clear() + hook("exitConversation", HookStage.BEFORE) { + openedConversationId = null + firstFocusedMessageId = null + synchronized(focusedMessages) { + focusedMessages.clear() + } + synchronized(delayedMessageUpdates) { + delayedMessageUpdates.clear() + } } } - } - context.event.subscribe(BindViewEvent::class) { event -> - event.chatMessage { conversationId, messageId -> - if (conversationId != openedConversationId) return@chatMessage - synchronized(focusedMessages) { - focusedMessages[event.view] = messageId.toLong() - } + context.event.subscribe(BindViewEvent::class) { event -> + event.chatMessage { conversationId, messageId -> + if (conversationId != openedConversationId) return@chatMessage + synchronized(focusedMessages) { + focusedMessages[event.view] = messageId.toLong() + } - if (delayedMessageUpdates.isNotEmpty() && focusedMessages.entries.any { entry -> entry.value == firstFocusedMessageId && entry.key.isAttachedToWindow }) { - delayedMessageUpdates.apply { - synchronized(this) { - removeIf { it(); true } - firstFocusedMessageId = null + if (delayedMessageUpdates.isNotEmpty() && focusedMessages.entries.any { entry -> entry.value == firstFocusedMessageId && entry.key.isAttachedToWindow }) { + delayedMessageUpdates.apply { + synchronized(this) { + removeIf { it(); true } + firstFocusedMessageId = null + } } } } diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/tweaks/RemoveGroupsLockedStatus.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/tweaks/RemoveGroupsLockedStatus.kt @@ -1,17 +1,18 @@ package me.rhunk.snapenhance.core.features.impl.tweaks import me.rhunk.snapenhance.core.features.Feature -import me.rhunk.snapenhance.core.features.FeatureLoadParams import me.rhunk.snapenhance.core.util.dataBuilder import me.rhunk.snapenhance.core.util.hook.HookStage import me.rhunk.snapenhance.core.util.hook.hookConstructor -class RemoveGroupsLockedStatus : Feature("Remove Groups Locked Status", loadParams = FeatureLoadParams.ACTIVITY_CREATE_SYNC) { - override fun onActivityCreate() { +class RemoveGroupsLockedStatus : Feature("Remove Groups Locked Status") { + override fun init() { if (!context.config.messaging.removeGroupsLockedStatus.get()) return - context.classCache.conversation.hookConstructor(HookStage.AFTER) { param -> - param.thisObject<Any>().dataBuilder { - set("mLockedState", "UNLOCKED") + onNextActivityCreate(defer = true) { + context.classCache.conversation.hookConstructor(HookStage.AFTER) { param -> + param.thisObject<Any>().dataBuilder { + set("mLockedState", "UNLOCKED") + } } } } diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/tweaks/UnsaveableMessages.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/tweaks/UnsaveableMessages.kt @@ -5,14 +5,12 @@ import me.rhunk.snapenhance.common.data.MessagingRuleType import me.rhunk.snapenhance.common.util.protobuf.ProtoEditor import me.rhunk.snapenhance.common.util.protobuf.ProtoReader import me.rhunk.snapenhance.core.event.events.impl.NativeUnaryCallEvent -import me.rhunk.snapenhance.core.features.FeatureLoadParams import me.rhunk.snapenhance.core.features.MessagingRuleFeature import me.rhunk.snapenhance.core.wrapper.impl.SnapUUID class UnsaveableMessages : MessagingRuleFeature( "Unsaveable Messages", - MessagingRuleType.UNSAVEABLE_MESSAGES, - loadParams = FeatureLoadParams.INIT_SYNC + MessagingRuleType.UNSAVEABLE_MESSAGES ) { override fun init() { if (context.config.rules.getRuleState(MessagingRuleType.UNSAVEABLE_MESSAGES) == null) return diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/ui/ClientBootstrapOverride.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/ui/ClientBootstrapOverride.kt @@ -2,11 +2,10 @@ package me.rhunk.snapenhance.core.features.impl.ui import me.rhunk.snapenhance.common.config.impl.UserInterfaceTweaks import me.rhunk.snapenhance.core.features.Feature -import me.rhunk.snapenhance.core.features.FeatureLoadParams import java.io.File -class ClientBootstrapOverride: Feature("ClientBootstrapOverride", loadParams = FeatureLoadParams.INIT_SYNC) { +class ClientBootstrapOverride: Feature("ClientBootstrapOverride") { private val clientBootstrapFolder by lazy { File(context.androidContext.filesDir, "client-bootstrap") } diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/ui/ConversationToolbox.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/ui/ConversationToolbox.kt @@ -34,7 +34,6 @@ import me.rhunk.snapenhance.common.scripting.ui.ScriptInterface import me.rhunk.snapenhance.common.ui.createComposeAlertDialog import me.rhunk.snapenhance.core.event.events.impl.AddViewEvent import me.rhunk.snapenhance.core.features.Feature -import me.rhunk.snapenhance.core.features.FeatureLoadParams import me.rhunk.snapenhance.core.features.impl.messaging.Messaging import me.rhunk.snapenhance.core.util.ktx.getId @@ -45,7 +44,7 @@ data class ComposableMenu( val composable: @Composable (alertDialog: AlertDialog, conversationId: String) -> Unit, ) -class ConversationToolbox : Feature("Conversation Toolbox", loadParams = FeatureLoadParams.ACTIVITY_CREATE_SYNC) { +class ConversationToolbox : Feature("Conversation Toolbox") { private val composableList = mutableListOf<ComposableMenu>() private val expandedComposableCache = mutableStateMapOf<String, Boolean>() @@ -56,49 +55,51 @@ class ConversationToolbox : Feature("Conversation Toolbox", loadParams = Feature } @SuppressLint("SetTextI18n") - override fun onActivityCreate() { - val defaultInputBarId = context.resources.getId("default_input_bar") + override fun init() { + onNextActivityCreate { + val defaultInputBarId = context.resources.getId("default_input_bar") - context.event.subscribe(AddViewEvent::class) { event -> - if (event.view.id != defaultInputBarId) return@subscribe - if (composableList.isEmpty()) return@subscribe + context.event.subscribe(AddViewEvent::class) { event -> + if (event.view.id != defaultInputBarId) return@subscribe + if (composableList.isEmpty()) return@subscribe - (event.view as ViewGroup).addView(FrameLayout(event.view.context).apply { - layoutParams = LinearLayout.LayoutParams( - ViewGroup.LayoutParams.WRAP_CONTENT, - (52 * context.resources.displayMetrics.density).toInt(), - ).apply { - gravity = Gravity.BOTTOM - } - setPadding(25, 0, 25, 0) - - addView(TextView(event.view.context).apply { - layoutParams = FrameLayout.LayoutParams( - ViewGroup.LayoutParams.WRAP_CONTENT, + (event.view as ViewGroup).addView(FrameLayout(event.view.context).apply { + layoutParams = LinearLayout.LayoutParams( ViewGroup.LayoutParams.WRAP_CONTENT, + (52 * context.resources.displayMetrics.density).toInt(), ).apply { - gravity = Gravity.CENTER_VERTICAL + gravity = Gravity.BOTTOM } - setOnClickListener { - openToolbox() - } - textSize = 21f - text = "\uD83E\uDDF0" + setPadding(25, 0, 25, 0) + + addView(TextView(event.view.context).apply { + layoutParams = FrameLayout.LayoutParams( + ViewGroup.LayoutParams.WRAP_CONTENT, + ViewGroup.LayoutParams.WRAP_CONTENT, + ).apply { + gravity = Gravity.CENTER_VERTICAL + } + setOnClickListener { + openToolbox() + } + textSize = 21f + text = "\uD83E\uDDF0" + }) }) - }) - } + } - context.scriptRuntime.eachModule { - val interfaceManager = getBinding(InterfaceManager::class)?.takeIf { - it.hasInterface(EnumScriptInterface.CONVERSATION_TOOLBOX) - } ?: return@eachModule - addComposable("\uD83D\uDCDC ${moduleInfo.displayName}") { alertDialog, conversationId -> - ScriptInterface(remember { - interfaceManager.buildInterface(EnumScriptInterface.CONVERSATION_TOOLBOX, mapOf( - "alertDialog" to alertDialog, - "conversationId" to conversationId, - )) - } ?: return@addComposable) + context.scriptRuntime.eachModule { + val interfaceManager = getBinding(InterfaceManager::class)?.takeIf { + it.hasInterface(EnumScriptInterface.CONVERSATION_TOOLBOX) + } ?: return@eachModule + addComposable("\uD83D\uDCDC ${moduleInfo.displayName}") { alertDialog, conversationId -> + ScriptInterface(remember { + interfaceManager.buildInterface(EnumScriptInterface.CONVERSATION_TOOLBOX, mapOf( + "alertDialog" to alertDialog, + "conversationId" to conversationId, + )) + } ?: return@addComposable) + } } } } diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/ui/CustomStreaksExpirationFormat.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/ui/CustomStreaksExpirationFormat.kt @@ -1,53 +1,54 @@ package me.rhunk.snapenhance.core.features.impl.ui 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 import me.rhunk.snapenhance.core.util.ktx.getObjectField import me.rhunk.snapenhance.mapper.impl.StreaksExpirationMapper import kotlin.time.Duration.Companion.milliseconds -class CustomStreaksExpirationFormat: Feature("CustomStreaksExpirationFormat", loadParams = FeatureLoadParams.ACTIVITY_CREATE_SYNC) { +class CustomStreaksExpirationFormat: Feature("CustomStreaksExpirationFormat") { private fun Long.padZero(): String { return this.toString().padStart(2, '0') } - override fun onActivityCreate() { - val expirationFormat by context.config.experimental.customStreaksExpirationFormat - if (expirationFormat.isNotEmpty() || context.config.userInterface.streakExpirationInfo.get()) { - context.mappings.useMapper(StreaksExpirationMapper::class) { - runCatching { - simpleStreaksFormatterClass.getAsClass()?.hook(formatSimpleStreaksTextMethod.get() ?: return@useMapper, HookStage.BEFORE) { param -> - param.setResult(null) + override fun init() { + onNextActivityCreate { + val expirationFormat by context.config.experimental.customStreaksExpirationFormat + if (expirationFormat.isNotEmpty() || context.config.userInterface.streakExpirationInfo.get()) { + context.mappings.useMapper(StreaksExpirationMapper::class) { + runCatching { + simpleStreaksFormatterClass.getAsClass()?.hook(formatSimpleStreaksTextMethod.get() ?: return@useMapper, HookStage.BEFORE) { param -> + param.setResult(null) + } + }.onFailure { + context.log.warn("Failed to hook simpleStreaksFormatterClass : " + it.message) } - }.onFailure { - context.log.warn("Failed to hook simpleStreaksFormatterClass : " + it.message) } } - } - if (expirationFormat.isEmpty()) return + if (expirationFormat.isEmpty()) return@onNextActivityCreate - context.mappings.useMapper(StreaksExpirationMapper::class) { - streaksFormatterClass.getAsClass()?.hook(formatStreaksTextMethod.get() ?: return@useMapper, HookStage.AFTER) { param -> - val streaksCount = param.argNullable(2) ?: 0 - val streaksExpiration = param.argNullable<Any>(3) ?: return@hook + context.mappings.useMapper(StreaksExpirationMapper::class) { + streaksFormatterClass.getAsClass()?.hook(formatStreaksTextMethod.get() ?: return@useMapper, HookStage.AFTER) { param -> + val streaksCount = param.argNullable(2) ?: 0 + val streaksExpiration = param.argNullable<Any>(3) ?: return@hook - val hourGlassTimeRemaining = streaksExpiration.getObjectField(hourGlassTimeRemainingField.get() ?: return@hook) as? Long ?: return@hook - val expirationTime = streaksExpiration.getObjectField(expirationTimeField.get() ?: return@hook) as? Long ?: return@hook - val delta = (expirationTime - System.currentTimeMillis()).milliseconds + val hourGlassTimeRemaining = streaksExpiration.getObjectField(hourGlassTimeRemainingField.get() ?: return@hook) as? Long ?: return@hook + val expirationTime = streaksExpiration.getObjectField(expirationTimeField.get() ?: return@hook) as? Long ?: return@hook + val delta = (expirationTime - System.currentTimeMillis()).milliseconds - val hourGlassEmoji = if (delta.inWholeMilliseconds in 1..hourGlassTimeRemaining) if (expirationTime % 2 == 0L) "\u23F3" else "\u231B" else "" + val hourGlassEmoji = if (delta.inWholeMilliseconds in 1..hourGlassTimeRemaining) if (expirationTime % 2 == 0L) "\u23F3" else "\u231B" else "" - param.setResult(expirationFormat - .replace("%c", streaksCount.toString()) - .replace("%e", hourGlassEmoji) - .replace("%d", delta.inWholeDays.toString()) - .replace("%h", (delta.inWholeHours % 24).padZero()) - .replace("%m", (delta.inWholeMinutes % 60).padZero()) - .replace("%s", (delta.inWholeSeconds % 60).padZero()) - .replace("%w", delta.toString()) - ) + param.setResult(expirationFormat + .replace("%c", streaksCount.toString()) + .replace("%e", hourGlassEmoji) + .replace("%d", delta.inWholeDays.toString()) + .replace("%h", (delta.inWholeHours % 24).padZero()) + .replace("%m", (delta.inWholeMinutes % 60).padZero()) + .replace("%s", (delta.inWholeSeconds % 60).padZero()) + .replace("%w", delta.toString()) + ) + } } } } diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/ui/CustomizeUI.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/ui/CustomizeUI.kt @@ -7,18 +7,17 @@ import android.util.TypedValue import androidx.compose.material3.dynamicDarkColorScheme import androidx.compose.ui.graphics.toArgb 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.Hooker import me.rhunk.snapenhance.core.util.hook.hook import me.rhunk.snapenhance.core.util.ktx.getIdentifier -class CustomizeUI: Feature("Customize UI", loadParams = FeatureLoadParams.ACTIVITY_CREATE_SYNC) { +class CustomizeUI: Feature("Customize UI") { private fun getAttribute(name: String): Int { return context.resources.getIdentifier(name, "attr") } - override fun onActivityCreate() { + override fun init() { val customizeUIConfig = context.config.userInterface.customizeUi val themePicker = customizeUIConfig.themePicker.getNullable() ?: return val colorsConfig = context.config.userInterface.customizeUi.colors diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/ui/DefaultVolumeControls.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/ui/DefaultVolumeControls.kt @@ -2,17 +2,18 @@ package me.rhunk.snapenhance.core.features.impl.ui import android.view.KeyEvent 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 DefaultVolumeControls : Feature("Default Volume Controls", loadParams = FeatureLoadParams.ACTIVITY_CREATE_SYNC) { - override fun onActivityCreate() { +class DefaultVolumeControls : Feature("Default Volume Controls") { + override fun init() { if (!context.config.global.defaultVolumeControls.get()) return - context.mainActivity!!::class.java.hook("onKeyDown", HookStage.BEFORE) { param -> - val keyCode = param.arg<Int>(0) - if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN || keyCode == KeyEvent.KEYCODE_VOLUME_UP) { - param.setResult(false) + onNextActivityCreate { activity -> + activity::class.java.hook("onKeyDown", HookStage.BEFORE) { param -> + val keyCode = param.arg<Int>(0) + if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN || keyCode == KeyEvent.KEYCODE_VOLUME_UP) { + param.setResult(false) + } } } } diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/ui/DisableConfirmationDialogs.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/ui/DisableConfirmationDialogs.kt @@ -4,59 +4,60 @@ import android.view.View import android.widget.TextView import me.rhunk.snapenhance.core.event.events.impl.AddViewEvent import me.rhunk.snapenhance.core.features.Feature -import me.rhunk.snapenhance.core.features.FeatureLoadParams import me.rhunk.snapenhance.core.ui.children import me.rhunk.snapenhance.core.ui.triggerRootCloseTouchEvent import me.rhunk.snapenhance.core.util.ktx.getId import me.rhunk.snapenhance.core.util.ktx.getIdentifier import java.util.regex.Pattern -class DisableConfirmationDialogs : Feature("Disable Confirmation Dialogs", loadParams = FeatureLoadParams.ACTIVITY_CREATE_SYNC) { - override fun onActivityCreate() { - val disableConfirmationDialogs = context.config.global.disableConfirmationDialogs.get().takeIf { it.isNotEmpty() } ?: return - val dialogContent = context.resources.getId("dialog_content") - val alertDialogTitle = context.resources.getId("alert_dialog_title") +class DisableConfirmationDialogs : Feature("Disable Confirmation Dialogs") { + override fun init() { + onNextActivityCreate { + val disableConfirmationDialogs = context.config.global.disableConfirmationDialogs.get().takeIf { it.isNotEmpty() } ?: return@onNextActivityCreate + val dialogContent = context.resources.getId("dialog_content") + val alertDialogTitle = context.resources.getId("alert_dialog_title") - val questions = listOf( - "erase_message" to "erase_learn_more_dialog_title", - "erase_message" to "erase_dialog_title", - "erase_message" to "snap_erase_dialog_title", - "erase_message" to "snap_erase_learn_more_dialog_title", - "remove_friend" to "action_menu_remove_friend_question", - "block_friend" to "action_menu_block_friend_question", - "ignore_friend" to "action_menu_ignore_friend_question", - "hide_friend" to "action_menu_hide_friend_question", - "hide_conversation" to "hide_or_block_clear_conversation_dialog_title", - "clear_conversation" to "action_menu_clear_conversation_dialog_title" - ).map { pair -> - pair.first to runCatching { - Pattern.compile( - context.resources.getString(context.resources.getIdentifier(pair.second, "string")) - .split("%s").joinToString(".*") { - Pattern.quote(it) - }, Pattern.CASE_INSENSITIVE) - }.onFailure { - context.log.error("Failed to compile regex for ${pair.second}", it) - }.getOrNull() - } + val questions = listOf( + "erase_message" to "erase_learn_more_dialog_title", + "erase_message" to "erase_dialog_title", + "erase_message" to "snap_erase_dialog_title", + "erase_message" to "snap_erase_learn_more_dialog_title", + "remove_friend" to "action_menu_remove_friend_question", + "block_friend" to "action_menu_block_friend_question", + "ignore_friend" to "action_menu_ignore_friend_question", + "hide_friend" to "action_menu_hide_friend_question", + "hide_conversation" to "hide_or_block_clear_conversation_dialog_title", + "clear_conversation" to "action_menu_clear_conversation_dialog_title" + ).map { pair -> + pair.first to runCatching { + Pattern.compile( + context.resources.getString(context.resources.getIdentifier(pair.second, "string")) + .split("%s").joinToString(".*") { + Pattern.quote(it) + }, Pattern.CASE_INSENSITIVE) + }.onFailure { + context.log.error("Failed to compile regex for ${pair.second}", it) + }.getOrNull() + } - context.event.subscribe(AddViewEvent::class) { event -> - if (event.parent.id != dialogContent || !event.view::class.java.name.endsWith("SnapButtonView")) return@subscribe + context.event.subscribe(AddViewEvent::class) { event -> + if (event.parent.id != dialogContent || !event.view::class.java.name.endsWith("SnapButtonView")) return@subscribe - val dialogTitle = event.parent.findViewById<TextView>(alertDialogTitle)?.text?.toString() ?: return@subscribe - if (event.parent.children().count { it::class.java.name.endsWith("SnapButtonView") } != 0) return@subscribe + val dialogTitle = event.parent.findViewById<TextView>(alertDialogTitle)?.text?.toString() ?: return@subscribe + if (event.parent.children().count { it::class.java.name.endsWith("SnapButtonView") } != 0) return@subscribe - questions.forEach { (key, value) -> - if (!disableConfirmationDialogs.contains(key)) return@forEach + questions.forEach { (key, value) -> + if (!disableConfirmationDialogs.contains(key)) return@forEach - if (value?.matcher(dialogTitle)?.matches() == true) { - event.parent.visibility = View.INVISIBLE - event.parent.post { - event.view.callOnClick() + if (value?.matcher(dialogTitle)?.matches() == true) { + event.parent.visibility = View.INVISIBLE + event.parent.post { + event.view.callOnClick() + } + event.parent.postDelayed({ + context.mainActivity!!.triggerRootCloseTouchEvent() + }, 400) } - event.parent.postDelayed({ - context.mainActivity!!.triggerRootCloseTouchEvent() - }, 400) } } } diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/ui/EditTextOverride.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/ui/EditTextOverride.kt @@ -5,30 +5,31 @@ import android.text.InputType import android.widget.EditText import android.widget.TextView 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 import me.rhunk.snapenhance.core.util.hook.hookConstructor -class EditTextOverride : Feature("Edit Text Override", loadParams = FeatureLoadParams.ACTIVITY_CREATE_SYNC) { - override fun onActivityCreate() { +class EditTextOverride : Feature("Edit Text Override") { + override fun init() { val editTextOverride by context.config.userInterface.editTextOverride if (editTextOverride.isEmpty()) return - if (editTextOverride.contains("bypass_text_input_limit")) { - TextView::class.java.getMethod("setFilters", Array<InputFilter>::class.java) - .hook(HookStage.BEFORE) { param -> - param.setArg(0, param.arg<Array<InputFilter>>(0).filter { - it !is InputFilter.LengthFilter - }.toTypedArray()) - } - } + onNextActivityCreate { + if (editTextOverride.contains("bypass_text_input_limit")) { + TextView::class.java.getMethod("setFilters", Array<InputFilter>::class.java) + .hook(HookStage.BEFORE) { param -> + param.setArg(0, param.arg<Array<InputFilter>>(0).filter { + it !is InputFilter.LengthFilter + }.toTypedArray()) + } + } - if (editTextOverride.contains("multi_line_chat_input")) { - findClass("com.snap.messaging.chat.features.input.InputBarEditText").apply { - hookConstructor(HookStage.AFTER) { param -> - val editText = param.thisObject<EditText>() - editText.inputType = editText.inputType or InputType.TYPE_TEXT_FLAG_MULTI_LINE + if (editTextOverride.contains("multi_line_chat_input")) { + findClass("com.snap.messaging.chat.features.input.InputBarEditText").apply { + hookConstructor(HookStage.AFTER) { param -> + val editText = param.thisObject<EditText>() + editText.inputType = editText.inputType or InputType.TYPE_TEXT_FLAG_MULTI_LINE + } } } } diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/ui/FriendFeedMessagePreview.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/ui/FriendFeedMessagePreview.kt @@ -8,6 +8,7 @@ import android.graphics.drawable.shapes.Shape import android.text.TextPaint import android.view.View import android.view.ViewGroup +import androidx.core.content.res.use import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.launch @@ -17,7 +18,6 @@ import me.rhunk.snapenhance.common.util.protobuf.ProtoReader import me.rhunk.snapenhance.core.event.events.impl.BindViewEvent import me.rhunk.snapenhance.core.event.events.impl.BuildMessageEvent import me.rhunk.snapenhance.core.features.Feature -import me.rhunk.snapenhance.core.features.FeatureLoadParams import me.rhunk.snapenhance.core.features.impl.experiments.EndToEndEncryption import me.rhunk.snapenhance.core.ui.addForegroundDrawable import me.rhunk.snapenhance.core.ui.removeForegroundDrawable @@ -29,7 +29,7 @@ import me.rhunk.snapenhance.core.wrapper.impl.getMessageText import java.util.WeakHashMap import kotlin.math.absoluteValue -class FriendFeedMessagePreview : Feature("FriendFeedMessagePreview", loadParams = FeatureLoadParams.ACTIVITY_CREATE_SYNC) { +class FriendFeedMessagePreview : Feature("FriendFeedMessagePreview") { private val endToEndEncryption by lazy { context.feature(EndToEndEncryption::class) } @OptIn(ExperimentalCoroutinesApi::class) private val coroutineDispatcher = Dispatchers.IO.limitedParallelism(1) @@ -39,7 +39,7 @@ class FriendFeedMessagePreview : Feature("FriendFeedMessagePreview", loadParams private val sigColorTextPrimary by lazy { context.mainActivity!!.theme.obtainStyledAttributes( intArrayOf(context.resources.getIdentifier("sigColorTextPrimary", "attr")) - ).getColor(0, 0) + ).use { it.getColor(0, 0) } } private val cachedLayouts = WeakHashMap<String, View>() @@ -73,82 +73,84 @@ class FriendFeedMessagePreview : Feature("FriendFeedMessagePreview", loadParams } } - override fun onActivityCreate() { + override fun init() { if (setting.globalState != true) return - val ffItemId = context.resources.getId("ff_item") + onNextActivityCreate { + val ffItemId = context.resources.getId("ff_item") - val secondaryTextSize = context.resources.getDimens("ff_feed_cell_secondary_text_size").toFloat() - val ffSdlAvatarMargin = context.resources.getDimens("ff_sdl_avatar_margin") - val ffSdlAvatarSize = context.resources.getDimens("ff_sdl_avatar_size") - val ffSdlPrimaryTextStartMargin = context.resources.getDimens("ff_sdl_primary_text_start_margin").toFloat() + val secondaryTextSize = context.resources.getDimens("ff_feed_cell_secondary_text_size").toFloat() + val ffSdlAvatarMargin = context.resources.getDimens("ff_sdl_avatar_margin") + val ffSdlAvatarSize = context.resources.getDimens("ff_sdl_avatar_size") + val ffSdlPrimaryTextStartMargin = context.resources.getDimens("ff_sdl_primary_text_start_margin").toFloat() - val feedEntryHeight = ffSdlAvatarSize + ffSdlAvatarMargin * 2 + (4 * context.resources.displayMetrics.density).toInt() - val separatorHeight = (context.resources.displayMetrics.density * 2).toInt() - val avenirNextMedium = context.resources.getFont(context.resources.getIdentifier("avenir_next_medium", "font")) - val textPaint = TextPaint().apply { - textSize = secondaryTextSize - typeface = avenirNextMedium - } + val feedEntryHeight = ffSdlAvatarSize + ffSdlAvatarMargin * 2 + (4 * context.resources.displayMetrics.density).toInt() + val separatorHeight = (context.resources.displayMetrics.density * 2).toInt() + val avenirNextMedium = context.resources.getFont(context.resources.getIdentifier("avenir_next_medium", "font")) + val textPaint = TextPaint().apply { + textSize = secondaryTextSize + typeface = avenirNextMedium + } - context.event.subscribe(BuildMessageEvent::class) { param -> - val conversationId = param.message.messageDescriptor?.conversationId?.toString() ?: return@subscribe - val cachedView = cachedLayouts[conversationId] ?: return@subscribe - context.coroutineScope.launch { - fetchMessages(conversationId) { - cachedView.postInvalidateDelayed(100L) + context.event.subscribe(BuildMessageEvent::class) { param -> + val conversationId = param.message.messageDescriptor?.conversationId?.toString() ?: return@subscribe + val cachedView = cachedLayouts[conversationId] ?: return@subscribe + context.coroutineScope.launch { + fetchMessages(conversationId) { + cachedView.postInvalidateDelayed(100L) + } } } - } - context.event.subscribe(BindViewEvent::class) { param -> - param.friendFeedItem { conversationId -> - val frameLayout = param.view as ViewGroup - val ffItem = frameLayout.findViewById<View>(ffItemId) + context.event.subscribe(BindViewEvent::class) { param -> + param.friendFeedItem { conversationId -> + val frameLayout = param.view as ViewGroup + val ffItem = frameLayout.findViewById<View>(ffItemId) - context.coroutineScope.launch(coroutineDispatcher) { - withContext(Dispatchers.Main) { - cachedLayouts.remove(conversationId) - frameLayout.removeForegroundDrawable("ffItem") - } + context.coroutineScope.launch(coroutineDispatcher) { + withContext(Dispatchers.Main) { + cachedLayouts.remove(conversationId) + frameLayout.removeForegroundDrawable("ffItem") + } - fetchMessages(conversationId) { - var maxTextHeight = 0 - val previewContainerHeight = messageCache[conversationId]?.sumOf { msg -> - val rect = Rect() - textPaint.getTextBounds(msg, 0, msg.length, rect) - rect.height().also { - if (it > maxTextHeight) maxTextHeight = it - }.plus(separatorHeight) - } ?: run { - ffItem.layoutParams = ffItem.layoutParams.apply { - height = ViewGroup.LayoutParams.MATCH_PARENT + fetchMessages(conversationId) { + var maxTextHeight = 0 + val previewContainerHeight = messageCache[conversationId]?.sumOf { msg -> + val rect = Rect() + textPaint.getTextBounds(msg, 0, msg.length, rect) + rect.height().also { + if (it > maxTextHeight) maxTextHeight = it + }.plus(separatorHeight) + } ?: run { + ffItem.layoutParams = ffItem.layoutParams.apply { + height = ViewGroup.LayoutParams.MATCH_PARENT + } + return@fetchMessages } - return@fetchMessages - } - ffItem.layoutParams = ffItem.layoutParams.apply { - height = feedEntryHeight + previewContainerHeight + separatorHeight - } + ffItem.layoutParams = ffItem.layoutParams.apply { + height = feedEntryHeight + previewContainerHeight + separatorHeight + } - cachedLayouts[conversationId] = frameLayout - - frameLayout.addForegroundDrawable("ffItem", ShapeDrawable(object: Shape() { - override fun draw(canvas: Canvas, paint: Paint) { - val offsetY = canvas.height.toFloat() - previewContainerHeight - paint.textSize = secondaryTextSize - paint.color = sigColorTextPrimary - paint.typeface = avenirNextMedium - - messageCache[conversationId]?.forEachIndexed { index, messageString -> - canvas.drawText(messageString, - feedEntryHeight + ffSdlPrimaryTextStartMargin, - offsetY + index * maxTextHeight, - paint - ) + cachedLayouts[conversationId] = frameLayout + + frameLayout.addForegroundDrawable("ffItem", ShapeDrawable(object: Shape() { + override fun draw(canvas: Canvas, paint: Paint) { + val offsetY = canvas.height.toFloat() - previewContainerHeight + paint.textSize = secondaryTextSize + paint.color = sigColorTextPrimary + paint.typeface = avenirNextMedium + + messageCache[conversationId]?.forEachIndexed { index, messageString -> + canvas.drawText(messageString, + feedEntryHeight + ffSdlPrimaryTextStartMargin, + offsetY + index * maxTextHeight, + paint + ) + } } - } - })) + })) + } } } } diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/ui/HideFriendFeedEntry.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/ui/HideFriendFeedEntry.kt @@ -2,7 +2,7 @@ package me.rhunk.snapenhance.core.features.impl.ui import me.rhunk.snapenhance.common.data.MessagingRuleType import me.rhunk.snapenhance.common.data.RuleState -import me.rhunk.snapenhance.core.features.FeatureLoadParams + import me.rhunk.snapenhance.core.features.MessagingRuleFeature import me.rhunk.snapenhance.core.util.dataBuilder import me.rhunk.snapenhance.core.util.hook.HookStage @@ -11,7 +11,7 @@ import me.rhunk.snapenhance.core.util.ktx.getObjectField import me.rhunk.snapenhance.core.wrapper.impl.SnapUUID import me.rhunk.snapenhance.mapper.impl.CallbackMapper -class HideFriendFeedEntry : MessagingRuleFeature("HideFriendFeedEntry", ruleType = MessagingRuleType.HIDE_FRIEND_FEED, loadParams = FeatureLoadParams.INIT_SYNC) { +class HideFriendFeedEntry : MessagingRuleFeature("HideFriendFeedEntry", ruleType = MessagingRuleType.HIDE_FRIEND_FEED) { private fun createDeletedFeedEntry(conversationIdInstance: Any) = findClass("com.snapchat.client.messaging.DeletedFeedEntry").dataBuilder { from("mFeedEntryIdentifier") { set("mConversationId", conversationIdInstance) diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/ui/HideQuickAddFriendFeed.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/ui/HideQuickAddFriendFeed.kt @@ -1,22 +1,23 @@ package me.rhunk.snapenhance.core.features.impl.ui 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.hookConstructor import me.rhunk.snapenhance.core.util.ktx.setObjectField import me.rhunk.snapenhance.mapper.impl.FriendingDataSourcesMapper -class HideQuickAddFriendFeed : Feature("HideQuickAddFriendFeed", loadParams = FeatureLoadParams.ACTIVITY_CREATE_SYNC) { - override fun onActivityCreate() { +class HideQuickAddFriendFeed : Feature("HideQuickAddFriendFeed") { + override fun init() { if (!context.config.userInterface.hideQuickAddFriendFeed.get()) return - context.mappings.useMapper(FriendingDataSourcesMapper::class) { - classReference.getAsClass()?.hookConstructor(HookStage.AFTER) { param -> - param.thisObject<Any>().setObjectField( - quickAddSourceListField.get()!!, - arrayListOf<Any>() - ) + onNextActivityCreate { + context.mappings.useMapper(FriendingDataSourcesMapper::class) { + classReference.getAsClass()?.hookConstructor(HookStage.AFTER) { param -> + param.thisObject<Any>().setObjectField( + quickAddSourceListField.get()!!, + arrayListOf<Any>() + ) + } } } } diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/ui/HideStreakRestore.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/ui/HideStreakRestore.kt @@ -1,7 +1,6 @@ package me.rhunk.snapenhance.core.features.impl.ui import me.rhunk.snapenhance.core.features.Feature -import me.rhunk.snapenhance.core.features.FeatureLoadParams import me.rhunk.snapenhance.core.features.impl.messaging.Messaging import me.rhunk.snapenhance.core.util.dataBuilder import me.rhunk.snapenhance.core.util.hook.HookStage @@ -11,7 +10,7 @@ import me.rhunk.snapenhance.core.util.ktx.getObjectFieldOrNull import me.rhunk.snapenhance.core.util.ktx.setObjectField import me.rhunk.snapenhance.core.wrapper.impl.SnapUUID -class HideStreakRestore : Feature("HideStreakRestore", loadParams = FeatureLoadParams.INIT_SYNC) { +class HideStreakRestore : Feature("HideStreakRestore") { override fun init() { if (!context.config.userInterface.hideStreakRestore.get()) return diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/ui/MessageIndicators.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/ui/MessageIndicators.kt @@ -26,119 +26,120 @@ import me.rhunk.snapenhance.common.ui.rememberAsyncMutableState import me.rhunk.snapenhance.common.util.protobuf.ProtoReader import me.rhunk.snapenhance.core.event.events.impl.BindViewEvent import me.rhunk.snapenhance.core.features.Feature -import me.rhunk.snapenhance.core.features.FeatureLoadParams import me.rhunk.snapenhance.core.ui.AppleLogo import me.rhunk.snapenhance.core.ui.removeForegroundDrawable import kotlin.random.Random -class MessageIndicators : Feature("Message Indicators", loadParams = FeatureLoadParams.ACTIVITY_CREATE_SYNC) { - override fun onActivityCreate() { +class MessageIndicators : Feature("Message Indicators") { + override fun init() { val messageIndicatorsConfig = context.config.userInterface.messageIndicators.getNullable() ?: return if (messageIndicatorsConfig.isEmpty()) return val messageInfoTag = Random.nextLong().toString() - val appleLogo = AppleLogo + onNextActivityCreate { + val appleLogo = AppleLogo - context.event.subscribe(BindViewEvent::class) { event -> - event.chatMessage { _, messageId -> - val parentLinearLayout = event.view.parent as? ViewGroup ?: return@subscribe - parentLinearLayout.findViewWithTag<View>(messageInfoTag)?.let { parentLinearLayout.removeView(it) } + context.event.subscribe(BindViewEvent::class) { event -> + event.chatMessage { _, messageId -> + val parentLinearLayout = event.view.parent as? ViewGroup ?: return@subscribe + parentLinearLayout.findViewWithTag<View>(messageInfoTag)?.let { parentLinearLayout.removeView(it) } - event.view.removeForegroundDrawable("messageIndicators") + event.view.removeForegroundDrawable("messageIndicators") - val message = context.database.getConversationMessageFromId(messageId.toLong()) ?: return@chatMessage - if (message.contentType != ContentType.SNAP.id && message.contentType != ContentType.EXTERNAL_MEDIA.id) return@chatMessage - val reader = ProtoReader(message.messageContent ?: return@chatMessage) + val message = context.database.getConversationMessageFromId(messageId.toLong()) ?: return@chatMessage + if (message.contentType != ContentType.SNAP.id && message.contentType != ContentType.EXTERNAL_MEDIA.id) return@chatMessage + val reader = ProtoReader(message.messageContent ?: return@chatMessage) - createComposeView(event.view.context) { - Box( - modifier = Modifier - .fillMaxWidth() - .height(50.dp) - .padding(top = 4.dp, end = 1.dp), - contentAlignment = Alignment.TopEnd - ) { - val hasEncryption by rememberAsyncMutableState(defaultValue = false) { - reader.getByteArray(4, 3, 3) != null || reader.containsPath(3, 99, 3) - } - val sentFromIosDevice by rememberAsyncMutableState(defaultValue = false) { - if (reader.containsPath(4, 4, 3)) !reader.containsPath(4, 4, 3, 3, 17) else reader.getVarInt(4, 4, 11, 17, 7) != null - } - val sentFromWebApp by rememberAsyncMutableState(defaultValue = false) { - reader.getVarInt(4, 4, *(if (reader.containsPath(4, 4, 3)) intArrayOf(3, 3, 22, 1) else intArrayOf(11, 22, 1))) == 7L - } - val sentWithLocation by rememberAsyncMutableState(defaultValue = false) { - reader.getVarInt(4, 4, 11, 17, 5) != null - } - val sentUsingOvfEditor by rememberAsyncMutableState(defaultValue = false) { - (reader.getString(4, 4, 11, 12, 1) ?: reader.getString(4, 4, 11, 13, 4, 1, 2, 12, 20, 1)) == "c13129f7-fe4a-44c4-9b9d-e0b26fee8f82" - } - val sentUsingDirectorMode by rememberAsyncMutableState(defaultValue = false) { - reader.followPath(4, 4, 11, 28)?.let { - (it.getVarInt(1) to it.getVarInt(2)) == (0L to 0L) - } == true || reader.getByteArray(4, 4, 11, 13, 4, 1, 2, 12, 27, 1) != null - } - - Row( - verticalAlignment = Alignment.CenterVertically + createComposeView(event.view.context) { + Box( + modifier = Modifier + .fillMaxWidth() + .height(50.dp) + .padding(top = 4.dp, end = 1.dp), + contentAlignment = Alignment.TopEnd ) { - if (sentWithLocation && messageIndicatorsConfig.contains("location_indicator")) { - Image( - imageVector = Icons.Default.LocationOn, - colorFilter = ColorFilter.tint(Color.Green), - contentDescription = null, - modifier = Modifier.size(15.dp) - ) + val hasEncryption by rememberAsyncMutableState(defaultValue = false) { + reader.getByteArray(4, 3, 3) != null || reader.containsPath(3, 99, 3) + } + val sentFromIosDevice by rememberAsyncMutableState(defaultValue = false) { + if (reader.containsPath(4, 4, 3)) !reader.containsPath(4, 4, 3, 3, 17) else reader.getVarInt(4, 4, 11, 17, 7) != null + } + val sentFromWebApp by rememberAsyncMutableState(defaultValue = false) { + reader.getVarInt(4, 4, *(if (reader.containsPath(4, 4, 3)) intArrayOf(3, 3, 22, 1) else intArrayOf(11, 22, 1))) == 7L } - if (messageIndicatorsConfig.contains("platform_indicator")) { - Image( - imageVector = when { - sentFromWebApp -> Icons.Default.Laptop - sentFromIosDevice -> appleLogo - else -> Icons.Default.Android - }, - colorFilter = ColorFilter.tint(Color.Green), - contentDescription = null, - modifier = Modifier.size(15.dp) - ) + val sentWithLocation by rememberAsyncMutableState(defaultValue = false) { + reader.getVarInt(4, 4, 11, 17, 5) != null } - if (hasEncryption && messageIndicatorsConfig.contains("encryption_indicator")) { - Image( - imageVector = Icons.Default.Lock, - colorFilter = ColorFilter.tint(Color.Green), - contentDescription = null, - modifier = Modifier.size(15.dp) - ) + val sentUsingOvfEditor by rememberAsyncMutableState(defaultValue = false) { + (reader.getString(4, 4, 11, 12, 1) ?: reader.getString(4, 4, 11, 13, 4, 1, 2, 12, 20, 1)) == "c13129f7-fe4a-44c4-9b9d-e0b26fee8f82" } - if (sentUsingDirectorMode && messageIndicatorsConfig.contains("director_mode_indicator")) { - Image( - imageVector = Icons.Default.Edit, - colorFilter = ColorFilter.tint(Color.Red), - contentDescription = null, - modifier = Modifier.size(15.dp) - ) + val sentUsingDirectorMode by rememberAsyncMutableState(defaultValue = false) { + reader.followPath(4, 4, 11, 28)?.let { + (it.getVarInt(1) to it.getVarInt(2)) == (0L to 0L) + } == true || reader.getByteArray(4, 4, 11, 13, 4, 1, 2, 12, 27, 1) != null } - if (sentUsingOvfEditor && messageIndicatorsConfig.contains("ovf_editor_indicator")) { - Text( - text = "OVF", - color = Color.Red, - fontWeight = FontWeight.ExtraBold, - fontSize = 10.sp, - ) + + Row( + verticalAlignment = Alignment.CenterVertically + ) { + if (sentWithLocation && messageIndicatorsConfig.contains("location_indicator")) { + Image( + imageVector = Icons.Default.LocationOn, + colorFilter = ColorFilter.tint(Color.Green), + contentDescription = null, + modifier = Modifier.size(15.dp) + ) + } + if (messageIndicatorsConfig.contains("platform_indicator")) { + Image( + imageVector = when { + sentFromWebApp -> Icons.Default.Laptop + sentFromIosDevice -> appleLogo + else -> Icons.Default.Android + }, + colorFilter = ColorFilter.tint(Color.Green), + contentDescription = null, + modifier = Modifier.size(15.dp) + ) + } + if (hasEncryption && messageIndicatorsConfig.contains("encryption_indicator")) { + Image( + imageVector = Icons.Default.Lock, + colorFilter = ColorFilter.tint(Color.Green), + contentDescription = null, + modifier = Modifier.size(15.dp) + ) + } + if (sentUsingDirectorMode && messageIndicatorsConfig.contains("director_mode_indicator")) { + Image( + imageVector = Icons.Default.Edit, + colorFilter = ColorFilter.tint(Color.Red), + contentDescription = null, + modifier = Modifier.size(15.dp) + ) + } + if (sentUsingOvfEditor && messageIndicatorsConfig.contains("ovf_editor_indicator")) { + Text( + text = "OVF", + color = Color.Red, + fontWeight = FontWeight.ExtraBold, + fontSize = 10.sp, + ) + } } } + }.apply { + tag = messageInfoTag + addOnLayoutChangeListener { _, left, _, right, _, _, _, _, _ -> + layout(left, 0, right, 0) + } + setPadding(0, 0, 0, -(50 * event.view.resources.displayMetrics.density).toInt()) + layoutParams = LinearLayout.LayoutParams( + LinearLayout.LayoutParams.MATCH_PARENT, + LinearLayout.LayoutParams.WRAP_CONTENT + ) + parentLinearLayout.addView(this) } - }.apply { - tag = messageInfoTag - addOnLayoutChangeListener { _, left, _, right, _, _, _, _, _ -> - layout(left, 0, right, 0) - } - setPadding(0, 0, 0, -(50 * event.view.resources.displayMetrics.density).toInt()) - layoutParams = LinearLayout.LayoutParams( - LinearLayout.LayoutParams.MATCH_PARENT, - LinearLayout.LayoutParams.WRAP_CONTENT - ) - parentLinearLayout.addView(this) } } } diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/ui/OldBitmojiSelfie.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/ui/OldBitmojiSelfie.kt @@ -4,9 +4,8 @@ import android.net.Uri import me.rhunk.snapenhance.common.util.snap.BitmojiSelfie import me.rhunk.snapenhance.core.event.events.impl.NetworkApiRequestEvent import me.rhunk.snapenhance.core.features.Feature -import me.rhunk.snapenhance.core.features.FeatureLoadParams -class OldBitmojiSelfie : Feature("OldBitmojiSelfie", loadParams = FeatureLoadParams.INIT_SYNC) { +class OldBitmojiSelfie : Feature("OldBitmojiSelfie") { override fun init() { val urlPrefixes = arrayOf("https://images.bitmoji.com/3d/render/", "https://cf-st.sc-cdn.net/3d/render/") val oldBitmojiSelfie = context.config.userInterface.oldBitmojiSelfie.getNullable() ?: return diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/ui/PinConversations.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/ui/PinConversations.kt @@ -2,7 +2,6 @@ package me.rhunk.snapenhance.core.features.impl.ui import me.rhunk.snapenhance.common.data.MessagingRuleType import me.rhunk.snapenhance.common.data.RuleState -import me.rhunk.snapenhance.core.features.FeatureLoadParams import me.rhunk.snapenhance.core.features.MessagingRuleFeature import me.rhunk.snapenhance.core.util.hook.HookStage import me.rhunk.snapenhance.core.util.hook.Hooker @@ -12,8 +11,8 @@ import me.rhunk.snapenhance.core.util.ktx.getObjectField import me.rhunk.snapenhance.core.util.ktx.setObjectField import me.rhunk.snapenhance.core.wrapper.impl.SnapUUID -class PinConversations : MessagingRuleFeature("PinConversations", MessagingRuleType.PIN_CONVERSATION, loadParams = FeatureLoadParams.ACTIVITY_CREATE_SYNC) { - override fun onActivityCreate() { +class PinConversations : MessagingRuleFeature("PinConversations", MessagingRuleType.PIN_CONVERSATION) { + override fun init() { if (!context.config.messaging.unlimitedConversationPinning.get()) return context.classCache.feedManager.hook("setPinnedConversationStatus", HookStage.BEFORE) { param -> diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/ui/SnapPreview.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/ui/SnapPreview.kt @@ -10,7 +10,6 @@ import me.rhunk.snapenhance.common.data.ContentType import me.rhunk.snapenhance.common.util.protobuf.ProtoReader import me.rhunk.snapenhance.core.event.events.impl.BindViewEvent import me.rhunk.snapenhance.core.features.Feature -import me.rhunk.snapenhance.core.features.FeatureLoadParams import me.rhunk.snapenhance.core.ui.addForegroundDrawable import me.rhunk.snapenhance.core.ui.removeForegroundDrawable import me.rhunk.snapenhance.core.util.EvictingMap @@ -22,7 +21,7 @@ import me.rhunk.snapenhance.core.util.media.PreviewUtils import me.rhunk.snapenhance.mapper.impl.CallbackMapper import java.io.File -class SnapPreview : Feature("SnapPreview", loadParams = FeatureLoadParams.INIT_SYNC or FeatureLoadParams.ACTIVITY_CREATE_SYNC) { +class SnapPreview : Feature("SnapPreview") { private val mediaFileCache = EvictingMap<String, File>(500) // mMediaId => mediaFile private val bitmapCache = EvictingMap<String, Bitmap>(50) // filePath => bitmap @@ -44,48 +43,46 @@ class SnapPreview : Feature("SnapPreview", loadParams = FeatureLoadParams.INIT_S mediaFileCache[mediaId.substringAfter("-")] = File(filePath.toString()) } } - } - @SuppressLint("DiscouragedApi") - override fun onActivityCreate() { - if (!isEnabled) return - val chatMediaCardHeight = context.resources.getDimens("chat_media_card_height") - val chatMediaCardSnapMargin = context.resources.getDimens("chat_media_card_snap_margin") - val chatMediaCardSnapMarginStartSdl = context.resources.getDimens("chat_media_card_snap_margin_start_sdl") + onNextActivityCreate { + val chatMediaCardHeight = context.resources.getDimens("chat_media_card_height") + val chatMediaCardSnapMargin = context.resources.getDimens("chat_media_card_snap_margin") + val chatMediaCardSnapMarginStartSdl = context.resources.getDimens("chat_media_card_snap_margin_start_sdl") - fun decodeMedia(file: File) = runCatching { - bitmapCache.getOrPut(file.absolutePath) { - PreviewUtils.resizeBitmap( - PreviewUtils.createPreviewFromFile(file) ?: return@runCatching null, - chatMediaCardHeight - chatMediaCardSnapMargin, - chatMediaCardHeight - chatMediaCardSnapMargin - ) - } - }.getOrNull() + fun decodeMedia(file: File) = runCatching { + bitmapCache.getOrPut(file.absolutePath) { + PreviewUtils.resizeBitmap( + PreviewUtils.createPreviewFromFile(file) ?: return@runCatching null, + chatMediaCardHeight - chatMediaCardSnapMargin, + chatMediaCardHeight - chatMediaCardSnapMargin + ) + } + }.getOrNull() - context.event.subscribe(BindViewEvent::class) { event -> - event.chatMessage { _, messageId -> - event.view.removeForegroundDrawable("snapPreview") + context.event.subscribe(BindViewEvent::class) { event -> + event.chatMessage { _, messageId -> + event.view.removeForegroundDrawable("snapPreview") - val message = context.database.getConversationMessageFromId(messageId.toLong()) ?: return@chatMessage - val messageReader = ProtoReader(message.messageContent ?: return@chatMessage) - val contentType = ContentType.fromMessageContainer(messageReader.followPath(4, 4)) + val message = context.database.getConversationMessageFromId(messageId.toLong()) ?: return@chatMessage + val messageReader = ProtoReader(message.messageContent ?: return@chatMessage) + val contentType = ContentType.fromMessageContainer(messageReader.followPath(4, 4)) - if (contentType != ContentType.SNAP || message.isSaved == 1) return@chatMessage + if (contentType != ContentType.SNAP || message.isSaved == 1) return@chatMessage - val mediaIdKey = messageReader.getString(4, 5, 1, 3, 2, 2) ?: return@chatMessage + val mediaIdKey = messageReader.getString(4, 5, 1, 3, 2, 2) ?: return@chatMessage - event.view.addForegroundDrawable("snapPreview", ShapeDrawable(object: Shape() { - override fun draw(canvas: Canvas, paint: Paint) { - val bitmap = mediaFileCache[mediaIdKey]?.let { decodeMedia(it) } ?: return + event.view.addForegroundDrawable("snapPreview", ShapeDrawable(object: Shape() { + override fun draw(canvas: Canvas, paint: Paint) { + val bitmap = mediaFileCache[mediaIdKey]?.let { decodeMedia(it) } ?: return - canvas.drawBitmap(bitmap, - canvas.width.toFloat() - bitmap.width - chatMediaCardSnapMarginStartSdl.toFloat() - chatMediaCardSnapMargin.toFloat(), - (canvas.height - bitmap.height) / 2f, - null - ) - } - })) + canvas.drawBitmap(bitmap, + canvas.width.toFloat() - bitmap.width - chatMediaCardSnapMarginStartSdl.toFloat() - chatMediaCardSnapMargin.toFloat(), + (canvas.height - bitmap.height) / 2f, + null + ) + } + })) + } } } } diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/ui/SpotlightCommentsUsername.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/ui/SpotlightCommentsUsername.kt @@ -7,47 +7,48 @@ 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.features.FeatureLoadParams import me.rhunk.snapenhance.core.features.impl.messaging.Messaging import me.rhunk.snapenhance.core.util.EvictingMap import me.rhunk.snapenhance.core.util.ktx.getId -class SpotlightCommentsUsername : Feature("SpotlightCommentsUsername", loadParams = FeatureLoadParams.ACTIVITY_CREATE_SYNC) { +class SpotlightCommentsUsername : Feature("SpotlightCommentsUsername") { private val usernameCache = EvictingMap<String, String>(150) @SuppressLint("SetTextI18n") - override fun onActivityCreate() { + override fun init() { if (!context.config.global.spotlightCommentsUsername.get()) return - val messaging = context.feature(Messaging::class) - val commentsCreatorBadgeTimestampId = context.resources.getId("comments_creator_badge_timestamp") + onNextActivityCreate(defer = true) { + val messaging = context.feature(Messaging::class) + val commentsCreatorBadgeTimestampId = context.resources.getId("comments_creator_badge_timestamp") - context.event.subscribe(BindViewEvent::class) { event -> - val commentsCreatorBadgeTimestamp = event.view.findViewById<TextView>(commentsCreatorBadgeTimestampId) ?: return@subscribe + context.event.subscribe(BindViewEvent::class) { event -> + val commentsCreatorBadgeTimestamp = event.view.findViewById<TextView>(commentsCreatorBadgeTimestampId) ?: return@subscribe - val posterUserId = event.prevModel.toString().takeIf { it.startsWith("Comment") } - ?.substringAfter("posterUserId=")?.substringBefore(",")?.substringBefore(")") ?: return@subscribe + val posterUserId = event.prevModel.toString().takeIf { it.startsWith("Comment") } + ?.substringAfter("posterUserId=")?.substringBefore(",")?.substringBefore(")") ?: return@subscribe - fun setUsername(username: String) { - usernameCache[posterUserId] = username - if (commentsCreatorBadgeTimestamp.text.contains(username)) return - commentsCreatorBadgeTimestamp.text = " (${username})" + commentsCreatorBadgeTimestamp.text.toString() - } + fun setUsername(username: String) { + usernameCache[posterUserId] = username + if (commentsCreatorBadgeTimestamp.text.contains(username)) return + commentsCreatorBadgeTimestamp.text = " (${username})" + commentsCreatorBadgeTimestamp.text.toString() + } - usernameCache[posterUserId]?.let { - setUsername(it) - return@subscribe - } + usernameCache[posterUserId]?.let { + setUsername(it) + return@subscribe + } - context.coroutineScope.launch { - val username = runCatching { - messaging.fetchSnapchatterInfos(listOf(posterUserId)).firstOrNull() - }.onFailure { - context.log.error("Failed to fetch snapchatter info for user $posterUserId", it) - }.getOrNull()?.username ?: return@launch + context.coroutineScope.launch { + val username = runCatching { + messaging.fetchSnapchatterInfos(listOf(posterUserId)).firstOrNull() + }.onFailure { + context.log.error("Failed to fetch snapchatter info for user $posterUserId", it) + }.getOrNull()?.username ?: return@launch - withContext(Dispatchers.Main) { - setUsername(username) + withContext(Dispatchers.Main) { + setUsername(username) + } } } } diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/ui/StealthModeIndicator.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/ui/StealthModeIndicator.kt @@ -4,12 +4,12 @@ import android.graphics.Canvas import android.graphics.Paint import android.graphics.drawable.ShapeDrawable import android.graphics.drawable.shapes.Shape +import androidx.core.content.res.use 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.features.FeatureLoadParams import me.rhunk.snapenhance.core.features.impl.spying.StealthMode import me.rhunk.snapenhance.core.ui.addForegroundDrawable import me.rhunk.snapenhance.core.ui.removeForegroundDrawable @@ -17,7 +17,7 @@ import me.rhunk.snapenhance.core.util.EvictingMap import me.rhunk.snapenhance.core.util.ktx.getDimens import me.rhunk.snapenhance.core.util.ktx.getIdentifier -class StealthModeIndicator : Feature("StealthModeIndicator", loadParams = FeatureLoadParams.ACTIVITY_CREATE_SYNC) { +class StealthModeIndicator : Feature("StealthModeIndicator") { private val stealthMode by lazy { context.feature(StealthMode::class) } private val listeners = EvictingMap<String, (Boolean) -> Unit>(100) @@ -30,46 +30,48 @@ class StealthModeIndicator : Feature("StealthModeIndicator", loadParams = Featur } } - override fun onActivityCreate() { + override fun init() { if (!context.config.userInterface.stealthModeIndicator.get()) return - val secondaryTextSize = context.resources.getDimens("ff_feed_cell_secondary_text_size").toFloat() - val sigColorTextPrimary = context.mainActivity!!.obtainStyledAttributes( - intArrayOf(context.resources.getIdentifier("sigColorTextPrimary", "attr")) - ).use { it.getColor(0, 0) } + onNextActivityCreate { + val secondaryTextSize = context.resources.getDimens("ff_feed_cell_secondary_text_size").toFloat() + val sigColorTextPrimary = context.mainActivity!!.obtainStyledAttributes( + intArrayOf(context.resources.getIdentifier("sigColorTextPrimary", "attr")) + ).use { it.getColor(0, 0) } - stealthMode.addStateListener { conversationId, state -> - runCatching { - listeners[conversationId]?.invoke(state) - }.onFailure { - context.log.error("Failed to update stealth mode indicator", it) - } - } - - context.event.subscribe(BindViewEvent::class) { event -> - fun updateStealthIndicator(isStealth: Boolean = true) { - event.view.removeForegroundDrawable("stealthModeIndicator") - if (!isStealth || !event.view.isAttachedToWindow) return - event.view.addForegroundDrawable("stealthModeIndicator", ShapeDrawable(object : Shape() { - override fun draw(canvas: Canvas, paint: Paint) { - paint.textSize = secondaryTextSize - paint.color = sigColorTextPrimary - canvas.drawText( - "\uD83D\uDC7B", - 0f, - canvas.height.toFloat() - secondaryTextSize / 2, - paint - ) - } - })) + stealthMode.addStateListener { conversationId, state -> + runCatching { + listeners[conversationId]?.invoke(state) + }.onFailure { + context.log.error("Failed to update stealth mode indicator", it) + } } - event.friendFeedItem { conversationId -> - listeners[conversationId] = addStateListener@{ stealth -> - updateStealthIndicator(stealth) + context.event.subscribe(BindViewEvent::class) { event -> + fun updateStealthIndicator(isStealth: Boolean = true) { + event.view.removeForegroundDrawable("stealthModeIndicator") + if (!isStealth || !event.view.isAttachedToWindow) return + event.view.addForegroundDrawable("stealthModeIndicator", ShapeDrawable(object : Shape() { + override fun draw(canvas: Canvas, paint: Paint) { + paint.textSize = secondaryTextSize + paint.color = sigColorTextPrimary + canvas.drawText( + "\uD83D\uDC7B", + 0f, + canvas.height.toFloat() - secondaryTextSize / 2, + paint + ) + } + })) } - fetchStealthState(conversationId) { isStealth -> - updateStealthIndicator(isStealth) + + event.friendFeedItem { conversationId -> + listeners[conversationId] = addStateListener@{ stealth -> + updateStealthIndicator(stealth) + } + fetchStealthState(conversationId) { isStealth -> + updateStealthIndicator(isStealth) + } } } } diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/ui/UITweaks.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/ui/UITweaks.kt @@ -10,13 +10,12 @@ import me.rhunk.snapenhance.core.event.events.impl.AddViewEvent import me.rhunk.snapenhance.core.event.events.impl.BindViewEvent import me.rhunk.snapenhance.core.event.events.impl.LayoutInflateEvent 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.Hooker import me.rhunk.snapenhance.core.util.hook.hook import me.rhunk.snapenhance.core.util.ktx.getIdentifier -class UITweaks : Feature("UITweaks", loadParams = FeatureLoadParams.ACTIVITY_CREATE_SYNC) { +class UITweaks : Feature("UITweaks") { private val identifierCache = mutableMapOf<String, Int>() fun getId(name: String, defType: String): Int { @@ -47,7 +46,7 @@ class UITweaks : Feature("UITweaks", loadParams = FeatureLoadParams.ACTIVITY_CRE } } - override fun onActivityCreate() { + private fun onActivityCreate() { val blockAds by context.config.global.blockAds val hiddenElements by context.config.userInterface.hideUiComponents val hideStorySuggestions by context.config.userInterface.hideStorySuggestions @@ -172,4 +171,10 @@ class UITweaks : Feature("UITweaks", loadParams = FeatureLoadParams.ACTIVITY_CRE } } } + + override fun init() { + onNextActivityCreate { + onActivityCreate() + } + } } \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/ui/ViewAppearanceHelper.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/ui/ViewAppearanceHelper.kt @@ -19,6 +19,7 @@ import android.view.View import android.view.ViewGroup import android.widget.Switch import android.widget.TextView +import androidx.core.content.res.use import me.rhunk.snapenhance.core.SnapEnhance import me.rhunk.snapenhance.core.util.ktx.getDimens import me.rhunk.snapenhance.core.util.ktx.getDimensFloat @@ -136,10 +137,10 @@ object ViewAppearanceHelper { val sigColorTextPrimary = component.context.theme.obtainStyledAttributes( intArrayOf(resources.getIdentifier("sigColorTextPrimary", "attr")) - ).getColor(0, 0) + ).use { it.getColor(0, 0) } val sigColorBackgroundSurface = component.context.theme.obtainStyledAttributes( intArrayOf(resources.getIdentifier("sigColorBackgroundSurface", "attr")) - ).getColor(0, 0) + ).use { it.getColor(0, 0) } val actionSheetDefaultCellHeight = resources.getDimens("action_sheet_default_cell_height") val actionSheetCornerRadius = resources.getDimensFloat("action_sheet_corner_radius") diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/ui/menu/MenuViewInjector.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/ui/menu/MenuViewInjector.kt @@ -9,7 +9,6 @@ import android.widget.LinearLayout import android.widget.ScrollView import me.rhunk.snapenhance.core.event.events.impl.AddViewEvent import me.rhunk.snapenhance.core.features.Feature -import me.rhunk.snapenhance.core.features.FeatureLoadParams import me.rhunk.snapenhance.core.features.impl.COFOverride import me.rhunk.snapenhance.core.features.impl.messaging.Messaging import me.rhunk.snapenhance.core.ui.findParent @@ -18,7 +17,7 @@ import me.rhunk.snapenhance.core.util.ktx.getIdentifier import kotlin.reflect.KClass @SuppressLint("DiscouragedApi") -class MenuViewInjector : Feature("MenuViewInjector", loadParams = FeatureLoadParams.ACTIVITY_CREATE_ASYNC) { +class MenuViewInjector : Feature("MenuViewInjector") { private val menuMap by lazy { arrayOf( NewChatActionMenu(), @@ -40,113 +39,115 @@ class MenuViewInjector : Feature("MenuViewInjector", loadParams = FeatureLoadPar return menuMap[menuClass] as? T } - override fun asyncOnActivityCreate() { - menuMap.forEach { it.value.init() } - - val messaging = context.feature(Messaging::class) - - val actionSheetItemsContainerLayoutId = context.resources.getIdentifier("action_sheet_items_container", "id") - val actionMenuTitle = context.resources.getIdentifier("action_menu_title", "id") - val actionMenu = context.resources.getIdentifier("action_menu", "id") - val componentsHolder = context.resources.getIdentifier("components_holder", "id") - val feedNewChat = context.resources.getIdentifier("feed_new_chat", "id") - val contextMenuButtonIconView = context.resources.getIdentifier("context_menu_button_icon_view", "id") - val chatActionMenu = context.resources.getIdentifier("chat_action_menu", "id") - - val hasV2ActionMenu = { context.feature(COFOverride::class).hasActionMenuV2 } - - context.event.subscribe(AddViewEvent::class) { event -> - val originalAddView: (View) -> Unit = { - event.adapter.invokeOriginal(arrayOf(it, -1, - FrameLayout.LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, - ViewGroup.LayoutParams.MATCH_PARENT - )) - ) - } + override fun init() { + onNextActivityCreate(defer = true) { + menuMap.forEach { it.value.init() } - val viewGroup: ViewGroup = event.parent - val childView: View = event.view - menuMap[OperaContextActionMenu::class]!!.inject(viewGroup, childView, originalAddView) + val messaging = context.feature(Messaging::class) - if (event.view.id == actionSheetItemsContainerLayoutId) { - event.view.post { - if (event.parent.findParent(4) { - it.findViewById<View>(actionMenuTitle) != null - } == null) return@post + val actionSheetItemsContainerLayoutId = context.resources.getIdentifier("action_sheet_items_container", "id") + val actionMenuTitle = context.resources.getIdentifier("action_menu_title", "id") + val actionMenu = context.resources.getIdentifier("action_menu", "id") + val componentsHolder = context.resources.getIdentifier("components_holder", "id") + val feedNewChat = context.resources.getIdentifier("feed_new_chat", "id") + val contextMenuButtonIconView = context.resources.getIdentifier("context_menu_button_icon_view", "id") + val chatActionMenu = context.resources.getIdentifier("chat_action_menu", "id") - val views = mutableListOf<View>() - menuMap[FriendFeedInfoMenu::class]?.inject(event.parent, event.view) { - views.add(it) - } - views.reversed().forEach { (event.view as ViewGroup).addView(it, 0) } + val hasV2ActionMenu = { context.feature(COFOverride::class).hasActionMenuV2 } + + context.event.subscribe(AddViewEvent::class) { event -> + val originalAddView: (View) -> Unit = { + event.adapter.invokeOriginal(arrayOf(it, -1, + FrameLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT + )) + ) } - } - if (childView.id == contextMenuButtonIconView) { - menuMap[OperaViewerIcons::class]!!.inject(viewGroup, childView, originalAddView) - } + val viewGroup: ViewGroup = event.parent + val childView: View = event.view + menuMap[OperaContextActionMenu::class]!!.inject(viewGroup, childView, originalAddView) - if (event.parent.id == componentsHolder && childView.id == feedNewChat) { - menuMap[SettingsGearInjector::class]!!.inject(viewGroup, childView, originalAddView) - return@subscribe - } + if (event.view.id == actionSheetItemsContainerLayoutId) { + event.view.post { + if (event.parent.findParent(4) { + it.findViewById<View>(actionMenuTitle) != null + } == null) return@post - if (viewGroup !is LinearLayout && childView.id == chatActionMenu && context.isDeveloper) { - event.view = LinearLayout(childView.context).apply { - orientation = LinearLayout.VERTICAL - addView( - (menuMap[NewChatActionMenu::class]!! as NewChatActionMenu).createDebugInfoView(childView.context) - ) - addView(event.view) + val views = mutableListOf<View>() + menuMap[FriendFeedInfoMenu::class]?.inject(event.parent, event.view) { + views.add(it) + } + views.reversed().forEach { (event.view as ViewGroup).addView(it, 0) } + } } - } - if (childView.javaClass.name.endsWith("ChatActionMenuComponent") && hasV2ActionMenu()) { - (menuMap[NewChatActionMenu::class]!! as NewChatActionMenu).handle(event) - return@subscribe - } + if (childView.id == contextMenuButtonIconView) { + menuMap[OperaViewerIcons::class]!!.inject(viewGroup, childView, originalAddView) + } - if (viewGroup.javaClass.name.endsWith("ActionMenuChatItemContainer") && !hasV2ActionMenu()) { - if (viewGroup.parent == null || viewGroup.parent.parent == null) return@subscribe - menuMap[ChatActionMenu::class]!!.inject(viewGroup, childView, originalAddView) - return@subscribe - } + if (event.parent.id == componentsHolder && childView.id == feedNewChat) { + menuMap[SettingsGearInjector::class]!!.inject(viewGroup, childView, originalAddView) + return@subscribe + } - if (viewGroup !is LinearLayout && childView.id == actionMenu && messaging.lastFocusedConversationType == 1) { - val injectedLayout = LinearLayout(childView.context).apply { - orientation = LinearLayout.VERTICAL - gravity = Gravity.BOTTOM - layoutParams = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT) - addView(childView) + if (viewGroup !is LinearLayout && childView.id == chatActionMenu && context.isDeveloper) { + event.view = LinearLayout(childView.context).apply { + orientation = LinearLayout.VERTICAL + addView( + (menuMap[NewChatActionMenu::class]!! as NewChatActionMenu).createDebugInfoView(childView.context) + ) + addView(event.view) + } } - event.parent.post { - injectedLayout.addView(ScrollView(injectedLayout.context).apply { - layoutParams = LinearLayout.LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, - ViewGroup.LayoutParams.WRAP_CONTENT - ).apply { - weight = 1f; - setMargins(0, 100, 0, 0) - } + if (childView.javaClass.name.endsWith("ChatActionMenuComponent") && hasV2ActionMenu()) { + (menuMap[NewChatActionMenu::class]!! as NewChatActionMenu).handle(event) + return@subscribe + } - addView(LinearLayout(context).apply { - orientation = LinearLayout.VERTICAL - menuMap[FriendFeedInfoMenu::class]?.inject(event.parent, injectedLayout) { view -> - view.layoutParams = LinearLayout.LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, - ViewGroup.LayoutParams.WRAP_CONTENT - ).apply { - setMargins(0, 5, 0, 5) - } - addView(view) - } - }) - }, 0) + if (viewGroup.javaClass.name.endsWith("ActionMenuChatItemContainer") && !hasV2ActionMenu()) { + if (viewGroup.parent == null || viewGroup.parent.parent == null) return@subscribe + menuMap[ChatActionMenu::class]!!.inject(viewGroup, childView, originalAddView) + return@subscribe } - event.view = injectedLayout + if (viewGroup !is LinearLayout && childView.id == actionMenu && messaging.lastFocusedConversationType == 1) { + val injectedLayout = LinearLayout(childView.context).apply { + orientation = LinearLayout.VERTICAL + gravity = Gravity.BOTTOM + layoutParams = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT) + addView(childView) + } + + event.parent.post { + injectedLayout.addView(ScrollView(injectedLayout.context).apply { + layoutParams = LinearLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT + ).apply { + weight = 1f; + setMargins(0, 100, 0, 0) + } + + addView(LinearLayout(context).apply { + orientation = LinearLayout.VERTICAL + menuMap[FriendFeedInfoMenu::class]?.inject(event.parent, injectedLayout) { view -> + view.layoutParams = LinearLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT + ).apply { + setMargins(0, 5, 0, 5) + } + addView(view) + } + }) + }, 0) + } + + event.view = injectedLayout + } } } } diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/ui/menu/impl/FriendFeedInfoMenu.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/ui/menu/impl/FriendFeedInfoMenu.kt @@ -33,6 +33,7 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import androidx.core.content.res.use import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -74,12 +75,12 @@ class FriendFeedInfoMenu : AbstractMenu() { private val sigColorTextPrimary by lazy { context.androidContext.theme.obtainStyledAttributes( intArrayOf(context.resources.getIdentifier("sigColorTextPrimary", "attr")) - ).getColor(0, 0) + ).use { it.getColor(0, 0) } } private val sigColorBackgroundSurface by lazy { context.androidContext.theme.obtainStyledAttributes( intArrayOf(context.resources.getIdentifier("sigColorBackgroundSurface", "attr")) - ).getColor(0, 0) + ).use { it.getColor(0, 0) } } private fun getImageDrawable(url: String): Drawable { diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/ui/menu/impl/OperaContextActionMenu.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/ui/menu/impl/OperaContextActionMenu.kt @@ -21,6 +21,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp +import androidx.core.content.res.use import me.rhunk.snapenhance.common.ui.createComposeView import me.rhunk.snapenhance.core.features.impl.OperaViewerParamsOverride import me.rhunk.snapenhance.core.features.impl.downloader.MediaDownloader @@ -170,7 +171,7 @@ class OperaContextActionMenu : AbstractMenu() { color = remember { view.context.theme.obtainStyledAttributes( intArrayOf(view.context.resources.getIdentifier("sigColorTextPrimary", "attr")) - ).getColor(0, 0).let { Color(it) } + ).use { Color(it.getColor(0, 0)) } }, textAlign = TextAlign.Center, modifier = Modifier.fillMaxWidth() diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/ui/menu/impl/SettingsGearInjector.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/ui/menu/impl/SettingsGearInjector.kt @@ -5,6 +5,7 @@ import android.view.View import android.view.ViewGroup import android.widget.FrameLayout import android.widget.ImageView +import androidx.core.content.res.use import me.rhunk.snapenhance.common.ui.OverlayType import me.rhunk.snapenhance.core.ui.menu.AbstractMenu import me.rhunk.snapenhance.core.util.ktx.getDimens @@ -65,8 +66,8 @@ class SettingsGearInjector : AbstractMenu() { gravity = android.view.Gravity.CENTER } setImageDrawable(context.resources.getDrawable("svg_settings_32x32", context.theme)) - context.resources.getStyledAttributes("headerButtonOpaqueIconTint", context.theme).getColorStateList(0)?.let { - imageTintList = it + context.resources.getStyledAttributes("headerButtonOpaqueIconTint", context.theme).use { + imageTintList = it.getColorStateList(0) } }) })