commit 1e02e0a61cd8db04fc729840c22c33ae1f0c188d parent 5d4e2aacb1dfc682e8fa0f9adbd9183c7b023104 Author: rhunk <101876869+rhunk@users.noreply.github.com> Date: Tue, 1 Aug 2023 01:14:04 +0200 refactor: config system Diffstat:
54 files changed, 722 insertions(+), 240 deletions(-)
diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/manager/MainActivity.kt b/app/src/main/kotlin/me/rhunk/snapenhance/manager/MainActivity.kt @@ -1,21 +1,37 @@ package me.rhunk.snapenhance.manager import android.annotation.SuppressLint +import android.content.Intent import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent +import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.navigation.compose.rememberNavController +import me.rhunk.snapenhance.Logger import me.rhunk.snapenhance.manager.data.ManagerContext +import me.rhunk.snapenhance.manager.util.SaveFolderChecker +import me.rhunk.snapenhance.util.ActivityResultCallback class MainActivity : ComponentActivity() { + private val activityResultCallbacks = mutableMapOf<Int, ActivityResultCallback>() + @SuppressLint("UnusedMaterialScaffoldPaddingParameter") override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + val startDestination = intent.getStringExtra("route")?.let { EnumSection.fromRoute(it) } ?: EnumSection.HOME val managerContext = ManagerContext(this) + //FIXME: temporary save folder + SaveFolderChecker.askForFolder( + this, + managerContext.config.root.downloader.saveFolder) + { + managerContext.config.writeConfig() + } + setContent { val navController = rememberNavController() val navigation = Navigation(managerContext) diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/manager/data/ManagerContext.kt b/app/src/main/kotlin/me/rhunk/snapenhance/manager/data/ManagerContext.kt @@ -4,11 +4,12 @@ import android.content.Context import me.rhunk.snapenhance.bridge.wrapper.ConfigWrapper import me.rhunk.snapenhance.bridge.wrapper.MappingsWrapper import me.rhunk.snapenhance.bridge.wrapper.TranslationWrapper +import me.rhunk.snapenhance.core.config.ModConfig class ManagerContext( private val context: Context ) { - val config = ConfigWrapper() + val config = ModConfig() val translation = TranslationWrapper() val mappings = MappingsWrapper(context) diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/manager/sections/FeaturesSection.kt b/app/src/main/kotlin/me/rhunk/snapenhance/manager/sections/FeaturesSection.kt @@ -41,6 +41,7 @@ import androidx.compose.ui.unit.sp import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.DialogProperties import kotlinx.coroutines.launch +import me.rhunk.snapenhance.Logger import me.rhunk.snapenhance.config.ConfigProperty import me.rhunk.snapenhance.config.impl.ConfigIntegerValue import me.rhunk.snapenhance.config.impl.ConfigStateListValue @@ -170,13 +171,28 @@ class FeaturesSection : Section() { } } + @Composable + private fun PropertyContainer() { + val properties = remember { + val items by manager.config + items.properties.map { it.key to it.value } + } + + LazyColumn( + modifier = Modifier + .fillMaxHeight(), + verticalArrangement = Arrangement.Center + ) { + items(properties) { (key, value) -> + // Logger.debug("key: $key, value: $value") + } + } + } + @Composable @Preview override fun Content() { - val configItems = remember { - ConfigProperty.sortedByCategory() - } val scope = rememberCoroutineScope() val scaffoldState = rememberScaffoldState() Scaffold( @@ -184,7 +200,7 @@ class FeaturesSection : Section() { floatingActionButton = { FloatingActionButton( onClick = { - manager.config.save() + //manager.config.writeConfig() scope.launch { scaffoldState.snackbarHostState.showSnackbar("Saved") } @@ -211,16 +227,7 @@ class FeaturesSection : Section() { modifier = Modifier.padding(all = 10.dp), fontSize = 20.sp ) - LazyColumn( - modifier = Modifier - .fillMaxHeight(), - verticalArrangement = Arrangement.Center - ) { - items(configItems) { item -> - if (item.shouldAppearInSettings.not()) return@items - PropertyCard(item) - } - } + PropertyContainer() } } ) diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/manager/util/SaveFolderChecker.kt b/app/src/main/kotlin/me/rhunk/snapenhance/manager/util/SaveFolderChecker.kt @@ -0,0 +1,43 @@ +package me.rhunk.snapenhance.manager.util + +import android.app.Activity +import android.app.AlertDialog +import android.content.Intent +import android.widget.Toast +import androidx.activity.ComponentActivity +import androidx.activity.result.contract.ActivityResultContracts +import me.rhunk.snapenhance.Logger +import me.rhunk.snapenhance.core.config.PropertyValue +import kotlin.system.exitProcess + +object SaveFolderChecker { + fun askForFolder(activity: ComponentActivity, property: PropertyValue<String>, saveConfig: () -> Unit) { + if (property.get().isEmpty() || !property.get().startsWith("content://")) { + val startActivity = activity.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) result@{ + if (it.resultCode != Activity.RESULT_OK) return@result + val uri = it.data?.data ?: return@result + val value = uri.toString() + activity.contentResolver.takePersistableUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION) + property.set(value) + saveConfig() + Toast.makeText(activity, "save folder set!", Toast.LENGTH_SHORT).show() + activity.finish() + } + + AlertDialog.Builder(activity) + .setTitle("Save folder") + .setMessage("Please select a folder where you want to save downloaded files.") + .setPositiveButton("Select") { _, _ -> + startActivity.launch( + Intent(Intent.ACTION_OPEN_DOCUMENT_TREE) + .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + .addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION) + ) + } + .setNegativeButton("Cancel") { _, _ -> + exitProcess(0) + } + .show() + } + } +}+ \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts @@ -2,6 +2,7 @@ @Suppress("DSL_SCOPE_VIOLATION") // TODO: Remove once KTIJ-19369 is fixed plugins { alias(libs.plugins.androidApplication) apply false + alias(libs.plugins.androidLibrary) apply false alias(libs.plugins.kotlinAndroid) apply false } diff --git a/core/build.gradle.kts b/core/build.gradle.kts @@ -1,6 +1,6 @@ @Suppress("DSL_SCOPE_VIOLATION") // TODO: Remove once KTIJ-19369 is fixed plugins { - id("com.android.library") + alias(libs.plugins.androidLibrary) alias(libs.plugins.kotlinAndroid) } android { diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/ModContext.kt b/core/src/main/kotlin/me/rhunk/snapenhance/ModContext.kt @@ -12,8 +12,9 @@ import com.google.gson.Gson import com.google.gson.GsonBuilder import kotlinx.coroutines.asCoroutineDispatcher import me.rhunk.snapenhance.bridge.BridgeClient -import me.rhunk.snapenhance.bridge.wrapper.ConfigWrapper import me.rhunk.snapenhance.bridge.wrapper.TranslationWrapper +import me.rhunk.snapenhance.core.config.ModConfig +import me.rhunk.snapenhance.core.config.impl.RootConfig import me.rhunk.snapenhance.data.MessageSender import me.rhunk.snapenhance.database.DatabaseAccess import me.rhunk.snapenhance.features.Feature @@ -39,8 +40,10 @@ class ModContext { val gson: Gson = GsonBuilder().create() + private val modConfig = ModConfig() + val config by modConfig + val translation = TranslationWrapper() - val config = ConfigWrapper() val features = FeatureManager(this) val mappings = MappingManager(this) val actionManager = ActionManager(this) @@ -87,7 +90,7 @@ class ModContext { fun softRestartApp(saveSettings: Boolean = false) { if (saveSettings) { - config.writeConfig() + modConfig.writeConfig() } val intent: Intent? = androidContext.packageManager.getLaunchIntentForPackage( Constants.SNAPCHAT_PACKAGE_NAME @@ -113,4 +116,8 @@ class ModContext { Process.killProcess(Process.myPid()) exitProcess(1) } + + fun reloadConfig() { + modConfig.loadFromBridge(bridgeClient) + } } \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/SnapEnhance.kt b/core/src/main/kotlin/me/rhunk/snapenhance/SnapEnhance.kt @@ -84,7 +84,7 @@ class SnapEnhance { } Logger.debug("Reloading config") - appContext.config.loadFromBridge(appContext.bridgeClient) + appContext.reloadConfig() } } @@ -97,7 +97,7 @@ class SnapEnhance { measureTime { with(appContext) { - config.loadFromBridge(bridgeClient) + reloadConfig() mappings.init() //if mappings aren't loaded, we can't initialize features if (!mappings.areMappingsLoaded) return diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/action/impl/OpenMap.kt b/core/src/main/kotlin/me/rhunk/snapenhance/action/impl/OpenMap.kt @@ -13,8 +13,8 @@ class OpenMap: AbstractAction("action.open_map", dependsOnProperty = ConfigPrope val mapActivityIntent = Intent() mapActivityIntent.setClassName(BuildConfig.APPLICATION_ID, MapActivity::class.java.name) mapActivityIntent.putExtra("location", Bundle().apply { - putDouble("latitude", context.config.string(ConfigProperty.LATITUDE).toDouble()) - putDouble("longitude", context.config.string(ConfigProperty.LONGITUDE).toDouble()) + putDouble("latitude", context.config.spoof.location.latitude.get().toDouble()) + putDouble("longitude", context.config.spoof.location.longitude.get().toDouble()) }) context.mainActivity!!.startActivityForResult(mapActivityIntent, 0x1337) diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/config/ConfigContainer.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/config/ConfigContainer.kt @@ -0,0 +1,78 @@ +package me.rhunk.snapenhance.core.config + +import com.google.gson.JsonObject +import me.rhunk.snapenhance.Logger +import kotlin.reflect.KProperty + +typealias ConfigParamsBuilder = ConfigParams.() -> Unit + +open class ConfigContainer( + var globalState: Boolean? = null +) { + val properties = mutableMapOf<PropertyKey<*>, PropertyValue<*>>() + + private inline fun <T> registerProperty( + key: String, + type: DataProcessors.PropertyDataProcessor<*>, + defaultValue: PropertyValue<T>, + params: ConfigParams.() -> Unit = {} + ): PropertyValue<T> { + val propertyKey = PropertyKey(key, type, ConfigParams().also { it.params() }) + properties[propertyKey] = defaultValue + return defaultValue + } + + protected fun boolean(key: String, defaultValue: Boolean = false, params: ConfigParamsBuilder = {}) = + registerProperty(key, DataProcessors.BOOLEAN, PropertyValue(defaultValue), params) + + protected fun integer(key: String, defaultValue: Int = 0, params: ConfigParamsBuilder = {}) = + registerProperty(key, DataProcessors.INTEGER, PropertyValue(defaultValue), params) + + protected fun float(key: String, defaultValue: Float = 0f, params: ConfigParamsBuilder = {}) = + registerProperty(key, DataProcessors.FLOAT, PropertyValue(defaultValue), params) + + protected fun string(key: String, defaultValue: String = "", params: ConfigParamsBuilder = {}) = + registerProperty(key, DataProcessors.STRING, PropertyValue(defaultValue), params) + + protected fun multiple( + key: String, + vararg values: String = emptyArray(), + params: ConfigParamsBuilder = {} + ) = registerProperty(key, + DataProcessors.STRING_MULTIPLE_SELECTION, PropertyValue(emptyList<String>(), defaultValues = values.toList()), params) + + //null value is considered as Off/Disabled + protected fun unique( + key: String, + vararg values: String = emptyArray(), + params: ConfigParamsBuilder = {} + ) = registerProperty(key, + DataProcessors.STRING_UNIQUE_SELECTION, PropertyValue("null", defaultValues = values.toList()), params) + + protected fun <T : ConfigContainer> container( + key: String, + container: T + ) = registerProperty(key, DataProcessors.container(container), PropertyValue(container)).get() + + fun toJson(): JsonObject { + val json = JsonObject() + properties.forEach { (propertyKey, propertyValue) -> + Logger.debug("${propertyKey.name} => $propertyValue") + val serializedValue = propertyValue.getNullable()?.let { propertyKey.dataProcessor.serializeAny(it) } + json.add(propertyKey.name, serializedValue) + } + return json + } + + fun fromJson(json: JsonObject) { + properties.forEach { (key, _) -> + val jsonElement = json.get(key.name) ?: return@forEach + key.dataProcessor.deserializeAny(jsonElement)?.let { + properties[key]?.setAny(it) + } + } + } + + operator fun getValue(t: Any?, property: KProperty<*>) = this.globalState + operator fun setValue(t: Any?, property: KProperty<*>, t1: Boolean?) { this.globalState = t1 } +}+ \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/config/ConfigObjects.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/config/ConfigObjects.kt @@ -0,0 +1,40 @@ +package me.rhunk.snapenhance.core.config + +import me.rhunk.snapenhance.Logger +import kotlin.reflect.KProperty + +class ConfigParams( + var shouldTranslate: Boolean = false, + var hidden: Boolean = false, + var isFolder: Boolean = false +) + +class PropertyValue<T>( + private var value: T? = null, + val defaultValues: List<*>? = null +) { + inner class PropertyValueNullable { + fun get() = value + operator fun getValue(t: Any?, property: KProperty<*>): T? = getNullable() + operator fun setValue(t: Any?, property: KProperty<*>, t1: T?) = set(t1) + } + + fun nullable() = PropertyValueNullable() + + fun isSet() = value != null + fun getNullable() = value?.takeIf { it != "null" } + fun get() = getNullable() ?: throw IllegalStateException("Property is not set") + fun set(value: T?) { this.value = value } + @Suppress("UNCHECKED_CAST") + fun setAny(value: Any?) { this.value = value as T? } + + operator fun getValue(t: Any?, property: KProperty<*>): T = get() + operator fun setValue(t: Any?, property: KProperty<*>, t1: T?) = set(t1) +} + +class PropertyKey<T>( + val name: String, + val dataProcessor: DataProcessors.PropertyDataProcessor<T>, + val params: ConfigParams = ConfigParams(), +) + diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/config/DataProcessors.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/config/DataProcessors.kt @@ -0,0 +1,94 @@ +package me.rhunk.snapenhance.core.config + +import com.google.gson.JsonArray +import com.google.gson.JsonElement +import com.google.gson.JsonNull +import com.google.gson.JsonObject +import com.google.gson.JsonPrimitive + +object DataProcessors { + enum class Type { + STRING, + BOOLEAN, + INTEGER, + FLOAT, + STRING_MULTIPLE_SELECTION, + STRING_UNIQUE_SELECTION, + CONTAINER, + } + + data class PropertyDataProcessor<T> + internal constructor( + val type: Type, + private val serialize: (T) -> JsonElement, + private val deserialize: (JsonElement) -> T + ) { + @Suppress("UNCHECKED_CAST") + fun serializeAny(value: Any) = serialize(value as T) + fun deserializeAny(value: JsonElement) = deserialize(value) + } + + val STRING = PropertyDataProcessor( + type = Type.STRING, + serialize = { + if (it != null) JsonPrimitive(it) + else JsonNull.INSTANCE + }, + deserialize = { + if (it.isJsonNull) null + else it.asString + }, + ) + + val BOOLEAN = PropertyDataProcessor( + type = Type.BOOLEAN, + serialize = { + if (it) JsonPrimitive(true) + else JsonPrimitive(false) + }, + deserialize = { it.asBoolean }, + ) + + val INTEGER = PropertyDataProcessor( + type = Type.INTEGER, + serialize = { JsonPrimitive(it) }, + deserialize = { it.asInt }, + ) + + val FLOAT = PropertyDataProcessor( + type = Type.FLOAT, + serialize = { JsonPrimitive(it) }, + deserialize = { it.asFloat }, + ) + + val STRING_MULTIPLE_SELECTION = PropertyDataProcessor( + type = Type.STRING_MULTIPLE_SELECTION, + serialize = { JsonArray().apply { it.forEach { add(it) } } }, + deserialize = { obj -> + obj.asJsonArray.map { it.asString } + }, + ) + + val STRING_UNIQUE_SELECTION = PropertyDataProcessor( + type = Type.STRING_UNIQUE_SELECTION, + serialize = { JsonPrimitive(it) }, + deserialize = { obj -> obj.takeIf { !it.isJsonNull }?.asString } + ) + + fun <T : ConfigContainer> container(container: T) = PropertyDataProcessor( + type = Type.CONTAINER, + serialize = { + JsonObject().apply { + addProperty("state", it.globalState) + add("properties", it.toJson()) + } + }, + deserialize = { obj -> + val jsonObject = obj.asJsonObject + container.apply { + globalState = jsonObject["state"].takeIf { !it.isJsonNull }?.asBoolean + fromJson(jsonObject["properties"].asJsonObject) + } + }, + ) +}+ \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/config/ModConfig.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/config/ModConfig.kt @@ -0,0 +1,57 @@ +package me.rhunk.snapenhance.core.config + +import android.content.Context +import com.google.gson.Gson +import com.google.gson.GsonBuilder +import com.google.gson.JsonObject +import me.rhunk.snapenhance.Logger +import me.rhunk.snapenhance.bridge.BridgeClient +import me.rhunk.snapenhance.bridge.FileLoaderWrapper +import me.rhunk.snapenhance.bridge.types.BridgeFileType +import me.rhunk.snapenhance.core.config.impl.RootConfig + +class ModConfig() { + val root = RootConfig() + + companion object { + val gson: Gson = GsonBuilder().setPrettyPrinting().create() + } + + private val file = FileLoaderWrapper(BridgeFileType.CONFIG, "{}".toByteArray(Charsets.UTF_8)) + + operator fun getValue(thisRef: Any?, property: Any?) = root + + private fun load() { + if (!file.isFileExists()) { + writeConfig() + return + } + + runCatching { + loadConfig() + }.onFailure { + Logger.error("Failed to load config", it) + writeConfig() + } + } + + private fun loadConfig() { + val configContent = file.read() + root.fromJson(gson.fromJson(configContent.toString(Charsets.UTF_8), JsonObject::class.java)) + } + + fun writeConfig() { + val configObject = root.toJson() + file.write(configObject.toString().toByteArray(Charsets.UTF_8)) + } + + fun loadFromContext(context: Context) { + file.loadFromContext(context) + load() + } + + fun loadFromBridge(bridgeClient: BridgeClient) { + file.loadFromBridge(bridgeClient) + load() + } +}+ \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/config/impl/Camera.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/config/impl/Camera.kt @@ -0,0 +1,15 @@ +package me.rhunk.snapenhance.core.config.impl + +import me.rhunk.snapenhance.core.config.ConfigContainer +import me.rhunk.snapenhance.features.impl.tweaks.CameraTweaks + +class Camera : ConfigContainer() { + val disable = boolean("disable_camera") + val immersiveCameraPreview = boolean("immersive_camera_preview") + val overridePreviewResolution = unique("override_preview_resolution", *CameraTweaks.resolutions.toTypedArray()) + { shouldTranslate = false } + val overridePictureResolution = unique("override_picture_resolution", *CameraTweaks.resolutions.toTypedArray()) + { shouldTranslate = false } + val forceHighestFrameRate = boolean("force_highest_frame_rate") + val forceCameraSourceEncoding = boolean("force_camera_source_encoding") +} diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/config/impl/DownloaderConfig.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/config/impl/DownloaderConfig.kt @@ -0,0 +1,24 @@ +package me.rhunk.snapenhance.core.config.impl + +import me.rhunk.snapenhance.core.config.ConfigContainer + +class DownloaderConfig : ConfigContainer() { + val saveFolder = string("save_folder") { isFolder = true } + val autoDownloadOptions = multiple("auto_download_options", + "friend_snaps", + "friend_stories", + "public_stories", + "spotlight" + ) + val pathFormat = multiple("path_format", + "create_user_folder", + "append_hash", + "append_date_time", + "append_type", + "append_username" + ) + val allowDuplicate = boolean("allow_duplicate") + val mergeOverlays = boolean("merge_overlays") + val chatDownloadContextMenu = boolean("chat_download_context_menu") + val logging = multiple("logging", "started", "success", "progress", "failure") +}+ \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/config/impl/Experimental.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/config/impl/Experimental.kt @@ -0,0 +1,11 @@ +package me.rhunk.snapenhance.core.config.impl + +import me.rhunk.snapenhance.core.config.ConfigContainer + +class Experimental : ConfigContainer() { + val appPasscode = string("app_passcode") + val appLockOnResume = boolean("app_lock_on_resume") + val infiniteStoryBoost = boolean("infinite_story_boost") + val meoPasscodeBypass = boolean("meo_passcode_bypass") + val unlimitedMultiSnap = boolean("unlimited_multi_snap") +}+ \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/config/impl/Global.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/config/impl/Global.kt @@ -0,0 +1,16 @@ +package me.rhunk.snapenhance.core.config.impl + +import me.rhunk.snapenhance.core.config.ConfigContainer +import me.rhunk.snapenhance.data.NotificationType + +class Global : ConfigContainer() { + val snapchatPlus = boolean("snapchat_plus") + val autoUpdater = unique("auto_updater", "DAILY","EVERY_LAUNCH", "DAILY", "WEEKLY") + val disableMetrics = boolean("disable_metrics") + val disableVideoLengthRestrictions = boolean("disable_video_length_restrictions") + val disableGooglePlayDialogs = boolean("disable_google_play_dialogs") + val forceMediaSourceQuality = boolean("force_media_source_quality") + val betterNotifications = multiple("better_notifications", "snap", "chat", "reply_button", "download_button") + val notificationBlacklist = multiple("notification_blacklist", *NotificationType.getIncomingValues().map { it.key }.toTypedArray()) + val disableSnapSplitting = boolean("disable_snap_splitting") +}+ \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/config/impl/MessagingTweaks.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/config/impl/MessagingTweaks.kt @@ -0,0 +1,25 @@ +package me.rhunk.snapenhance.core.config.impl + +import me.rhunk.snapenhance.core.config.ConfigContainer +import me.rhunk.snapenhance.data.NotificationType + +class MessagingTweaks : ConfigContainer() { + val anonymousStoryViewing = boolean("annonymous_story_viewing") + val preventReadReceipts = boolean("prevent_read_receipts") + val hideBitmojiPresence = boolean("hide_bitmoji_presence") + val hideTypingNotifications = boolean("hide_typing_notifications") + val unlimitedSnapViewTime = boolean("unlimited_snap_view_time") + val preventMessageSending = multiple("prevent_message_sending", *NotificationType.getOutgoingValues().map { it.key }.toTypedArray()) + val messageLogger = boolean("message_logger") + val autoSaveMessagesInConversations = multiple("auto_save_messages_in_conversations", + "CHAT", + "SNAP", + "NOTE", + "EXTERNAL_MEDIA", + "STICKER" + ) + + val galleryMediaSendOverride = unique("gallery_media_send_override", "NOTE", "SNAP", "LIVE_SNAP") + val messagePreviewLength = integer("message_preview_length", defaultValue = 20) + +}+ \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/config/impl/RootConfig.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/config/impl/RootConfig.kt @@ -0,0 +1,15 @@ +package me.rhunk.snapenhance.core.config.impl + +import me.rhunk.snapenhance.core.config.ConfigContainer +import me.rhunk.snapenhance.data.NotificationType +import me.rhunk.snapenhance.features.impl.tweaks.CameraTweaks + +class RootConfig : ConfigContainer() { + val downloader = container("downloader", DownloaderConfig()) + val userInterface = container("user_interface", UserInterfaceTweaks()) + val messaging = container("messaging", MessagingTweaks()) + val global = container("global", Global()) + val camera = container("camera", Camera()) + val experimental = container("experimental", Experimental()) + val spoof = container("spoof", Spoof()) +}+ \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/config/impl/Spoof.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/config/impl/Spoof.kt @@ -0,0 +1,17 @@ +package me.rhunk.snapenhance.core.config.impl + +import me.rhunk.snapenhance.core.config.ConfigContainer + +class Spoof : ConfigContainer() { + inner class Location : ConfigContainer(globalState = false) { + val latitude = float("location_latitude") + val longitude = float("location_longitude") + } + val location = container("location", Location()) + + inner class Device : ConfigContainer(globalState = false) { + val fingerprint = string("device_fingerprint") + val androidId = string("device_android_id") + } + val device = container("device", Device()) +}+ \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/config/impl/UserInterfaceTweaks.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/config/impl/UserInterfaceTweaks.kt @@ -0,0 +1,34 @@ +package me.rhunk.snapenhance.core.config.impl + +import me.rhunk.snapenhance.core.config.ConfigContainer + +class UserInterfaceTweaks : ConfigContainer() { + val enableAppAppearance = boolean("enable_app_appearance") + val amoledDarkMode = boolean("amoled_dark_mode") + val blockAds = boolean("block_ads") + val mapFriendNameTags = boolean("map_friend_nametags") + val streakExpirationInfo = boolean("streak_expiration_info") + val hideStorySections = multiple("hide_story_sections", "hide_friend_suggestions", "hide_friends", "hide_following", "hide_for_you") + val hideUiComponents = multiple( + "hide_ui_components", + "hide_voice_record_button", + "hide_stickers_button", + "hide_cognac_button", + "hide_live_location_share_button", + "hide_call_buttons" + ) + val disableSpotlight = boolean("disable_spotlight") + val startupTab = unique("startup_tab", "ngs_map_icon_container", + "ngs_map_icon_container", + "ngs_chat_icon_container", + "ngs_camera_icon_container", + "ngs_community_icon_container", + "ngs_spotlight_icon_container", + "ngs_search_icon_container" + ) + val storyViewerOverride = unique("story_viewer_override", "DISCOVER_PLAYBACK_SEEKBAR", "VERTICAL_STORY_VIEWER") + + val friendFeedMenuButtons = multiple("friend_feed_menu_buttons", "auto_download_blacklist", "anti_auto_save", "stealth_mode", "conversation_info") + val enableFriendFeedMenuBar = boolean("enable_friend_feed_menu_bar") + val friendFeedMenuPosition = integer("friend_feed_menu_position", defaultValue = 1) +} diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/download/DownloadProcessor.kt b/core/src/main/kotlin/me/rhunk/snapenhance/download/DownloadProcessor.kt @@ -20,6 +20,7 @@ import me.rhunk.snapenhance.SharedContext import me.rhunk.snapenhance.bridge.DownloadCallback import me.rhunk.snapenhance.bridge.wrapper.ConfigWrapper import me.rhunk.snapenhance.config.ConfigProperty +import me.rhunk.snapenhance.core.config.ModConfig import me.rhunk.snapenhance.data.FileType import me.rhunk.snapenhance.download.data.DownloadMetadata import me.rhunk.snapenhance.download.data.DownloadRequest @@ -117,7 +118,7 @@ class DownloadProcessor ( private suspend fun saveMediaToGallery(inputFile: File, pendingDownload: PendingDownload) { if (coroutineContext.job.isCancelled) return - val config = ConfigWrapper().apply { loadFromContext(context) } + val config by ModConfig().apply { loadFromContext(context) } runCatching { val fileType = FileType.fromFile(inputFile) @@ -128,7 +129,7 @@ class DownloadProcessor ( val fileName = pendingDownload.metadata.outputPath.substringAfterLast("/") + "." + fileType.fileExtension - val outputFolder = DocumentFile.fromTreeUri(context, Uri.parse(config.string(ConfigProperty.SAVE_FOLDER))) + val outputFolder = DocumentFile.fromTreeUri(context, Uri.parse(config.downloader.saveFolder.get())) ?: throw Exception("Failed to open output folder") val outputFileFolder = pendingDownload.metadata.outputPath.let { diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/AutoUpdater.kt b/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/AutoUpdater.kt @@ -1,7 +1,6 @@ package me.rhunk.snapenhance.features.impl import android.annotation.SuppressLint -import android.app.AlertDialog import android.app.DownloadManager import android.content.BroadcastReceiver import android.content.Context @@ -9,23 +8,22 @@ import android.content.Intent import android.content.IntentFilter import android.net.Uri import android.os.Environment -import me.rhunk.snapenhance.core.BuildConfig +import com.google.gson.JsonParser import me.rhunk.snapenhance.Logger -import me.rhunk.snapenhance.config.ConfigProperty +import me.rhunk.snapenhance.core.BuildConfig import me.rhunk.snapenhance.features.Feature import me.rhunk.snapenhance.features.FeatureLoadParams import me.rhunk.snapenhance.ui.ViewAppearanceHelper import okhttp3.OkHttpClient import okhttp3.Request -import org.json.JSONArray class AutoUpdater : Feature("AutoUpdater", loadParams = FeatureLoadParams.ACTIVITY_CREATE_ASYNC) { override fun asyncOnActivityCreate() { - val checkForUpdateMode = context.config.state(ConfigProperty.AUTO_UPDATER) + val autoUpdaterTime = context.config.global.autoUpdater.getNullable() ?: return val currentTimeMillis = System.currentTimeMillis() val checkForUpdatesTimestamp = context.bridgeClient.getAutoUpdaterTime() - val delayTimestamp = when (checkForUpdateMode) { + val delayTimestamp = when (autoUpdaterTime) { "EVERY_LAUNCH" -> currentTimeMillis - checkForUpdatesTimestamp "DAILY" -> 86400000L "WEEKLY" -> 604800000L @@ -50,16 +48,16 @@ class AutoUpdater : Feature("AutoUpdater", loadParams = FeatureLoadParams.ACTIVI if (!response.isSuccessful) throw Throwable("Failed to fetch releases: ${response.code}") - val releases = JSONArray(response.body.string()).also { - if (it.length() == 0) throw Throwable("No releases found") + val releases = JsonParser.parseString(response.body.string()).asJsonArray.also { + if (it.size() == 0) throw Throwable("No releases found") } - val latestRelease = releases.getJSONObject(0) - val latestVersion = latestRelease.getString("tag_name") + val latestRelease = releases.get(0).asJsonObject + val latestVersion = latestRelease.getAsJsonPrimitive("tag_name").asString if (latestVersion.removePrefix("v") == BuildConfig.VERSION_NAME) return null - val releaseContentBody = latestRelease.getString("body") - val downloadEndpoint = latestRelease.getJSONArray("assets").getJSONObject(0).getString("browser_download_url") + val releaseContentBody = latestRelease.getAsJsonPrimitive("body").asString + val downloadEndpoint = latestRelease.getAsJsonArray("assets").get(0).asJsonObject.getAsJsonPrimitive("browser_download_url").asString context.runOnUiThread { ViewAppearanceHelper.newAlertDialogBuilder(context.mainActivity) diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/ConfigEnumKeys.kt b/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/ConfigEnumKeys.kt @@ -55,7 +55,7 @@ class ConfigEnumKeys : Feature("Config enum keys", loadParams = FeatureLoadParam @SuppressLint("PrivateApi") override fun onActivityCreate() { - if (context.config.bool(ConfigProperty.NEW_MAP_UI)) { + if (context.config.userInterface.mapFriendNameTags.get()) { hookAllEnums(context.mappings.getMappedClass("enums", "PLUS")) { if (key == "REDUCE_MY_PROFILE_UI_COMPLEXITY") set(true) } @@ -63,17 +63,17 @@ class ConfigEnumKeys : Feature("Config enum keys", loadParams = FeatureLoadParam hookAllEnums(context.mappings.getMappedClass("enums", "ARROYO")) { if (key == "ENABLE_LONG_SNAP_SENDING") { - if (context.config.bool(ConfigProperty.DISABLE_SNAP_SPLITTING)) set(true) + if (context.config.global.disableSnapSplitting.get()) set(true) } } - if (context.config.bool(ConfigProperty.STREAK_EXPIRATION_INFO)) { + if (context.config.userInterface.streakExpirationInfo.get()) { hookAllEnums(context.mappings.getMappedClass("enums", "FRIENDS_FEED")) { if (key == "STREAK_EXPIRATION_INFO") set(true) } } - if (context.config.bool(ConfigProperty.BLOCK_ADS)) { + if (context.config.userInterface.blockAds.get()) { hookAllEnums(context.mappings.getMappedClass("enums", "SNAPADS")) { if (key == "BYPASS_AD_FEATURE_GATE") { set(true) @@ -84,14 +84,12 @@ class ConfigEnumKeys : Feature("Config enum keys", loadParams = FeatureLoadParam } } - context.config.state(ConfigProperty.STORY_VIEWER_OVERRIDE).let { state -> - if (state == "OFF") return@let - + context.config.userInterface.storyViewerOverride.getNullable()?.let { value -> hookAllEnums(context.mappings.getMappedClass("enums", "DISCOVER_FEED")) { - if (key == "DF_ENABLE_SHOWS_PAGE_CONTROLS" && state == "DISCOVER_PLAYBACK_SEEKBAR") { + if (key == "DF_ENABLE_SHOWS_PAGE_CONTROLS" && value == "DISCOVER_PLAYBACK_SEEKBAR") { set(true) } - if (key == "DF_VOPERA_FOR_STORIES" && state == "VERTICAL_STORY_VIEWER") { + if (key == "DF_VOPERA_FOR_STORIES" && value == "VERTICAL_STORY_VIEWER") { set(true) } } @@ -105,8 +103,8 @@ class ConfigEnumKeys : Feature("Config enum keys", loadParams = FeatureLoadParam sharedPreferencesImpl.methods.first { it.name == "getBoolean" }.hook(HookStage.BEFORE) { param -> when (param.arg<String>(0)) { - "SIG_APP_APPEARANCE_SETTING" -> if (context.config.bool(ConfigProperty.ENABLE_APP_APPEARANCE)) param.setResult(true) - "SPOTLIGHT_5TH_TAB_ENABLED" -> if (context.config.bool(ConfigProperty.DISABLE_SPOTLIGHT)) param.setResult(false) + "SIG_APP_APPEARANCE_SETTING" -> if (context.config.userInterface.enableAppAppearance.get()) param.setResult(true) + "SPOTLIGHT_5TH_TAB_ENABLED" -> if (context.config.userInterface.disableSpotlight.get()) param.setResult(false) } } } diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/Messaging.kt b/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/Messaging.kt @@ -61,9 +61,12 @@ class Messaging : Feature("Messaging", loadParams = FeatureLoadParams.ACTIVITY_C override fun asyncInit() { val stealthMode = context.feature(StealthMode::class) + val hideBitmojiPresence by context.config.messaging.hideBitmojiPresence + val hideTypingNotification by context.config.messaging.hideTypingNotifications + arrayOf("activate", "deactivate", "processTypingActivity").forEach { hook -> Hooker.hook(context.classCache.presenceSession, hook, HookStage.BEFORE, { - context.config.bool(ConfigProperty.HIDE_BITMOJI_PRESENCE) || stealthMode.isStealth(openedConversationUUID.toString()) + hideBitmojiPresence || stealthMode.isStealth(openedConversationUUID.toString()) }) { it.setResult(null) } @@ -81,7 +84,7 @@ class Messaging : Feature("Messaging", loadParams = FeatureLoadParams.ACTIVITY_C } Hooker.hook(context.classCache.conversationManager, "sendTypingNotification", HookStage.BEFORE, { - context.config.bool(ConfigProperty.HIDE_TYPING_NOTIFICATION) || stealthMode.isStealth(openedConversationUUID.toString()) + hideTypingNotification || stealthMode.isStealth(openedConversationUUID.toString()) }) { it.setResult(null) } diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/downloader/MediaDownloader.kt b/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/downloader/MediaDownloader.kt @@ -14,7 +14,6 @@ import me.rhunk.snapenhance.bridge.DownloadCallback import me.rhunk.snapenhance.config.ConfigProperty import me.rhunk.snapenhance.data.ContentType import me.rhunk.snapenhance.data.FileType -import me.rhunk.snapenhance.data.wrapper.impl.SnapUUID import me.rhunk.snapenhance.data.wrapper.impl.media.MediaInfo import me.rhunk.snapenhance.data.wrapper.impl.media.dash.LongformVideoPlaylistItem import me.rhunk.snapenhance.data.wrapper.impl.media.dash.SnapPlaylistItem @@ -73,8 +72,8 @@ class MediaDownloader : Feature("MediaDownloader", loadParams = FeatureLoadParam BitmojiSelfie.getBitmojiSelfie(it.bitmojiSelfieId!!, it.bitmojiAvatarId!!, BitmojiSelfie.BitmojiSelfieType.THREE_D) } - val downloadLogging = context.config.options(ConfigProperty.DOWNLOAD_LOGGING) - if (downloadLogging["started"] == true) { + val downloadLogging by context.config.downloader.logging + if (downloadLogging.contains("started")) { context.shortToast(context.translation["download_processor.download_started_toast"]) } @@ -83,7 +82,7 @@ class MediaDownloader : Feature("MediaDownloader", loadParams = FeatureLoadParam return DownloadManagerClient( context = context, metadata = DownloadMetadata( - mediaIdentifier = if (context.config.options(ConfigProperty.DOWNLOAD_OPTIONS)["allow_duplicate"] == false) { + mediaIdentifier = if (!context.config.downloader.allowDuplicate.get()) { generatedHash } else null, mediaDisplaySource = mediaDisplaySource, @@ -93,19 +92,19 @@ class MediaDownloader : Feature("MediaDownloader", loadParams = FeatureLoadParam ), callback = object: DownloadCallback.Stub() { override fun onSuccess(outputFile: String) { - if (downloadLogging["success"] != true) return + if (!downloadLogging.contains("success")) return Logger.debug("onSuccess: outputFile=$outputFile") context.shortToast(context.translation.format("download_processor.saved_toast", "path" to outputFile.split("/").takeLast(2).joinToString("/"))) } override fun onProgress(message: String) { - if (downloadLogging["progress"] != true) return + if (!downloadLogging.contains("progress")) return Logger.debug("onProgress: message=$message") context.shortToast(message) } override fun onFailure(message: String, throwable: String?) { - if (downloadLogging["failure"] != true) return + if (!downloadLogging.contains("failure")) return Logger.debug("onFailure: message=$message, throwable=$throwable") throwable?.let { context.longToast((message + it.takeIf { it.isNotEmpty() }.orEmpty())) @@ -118,13 +117,13 @@ class MediaDownloader : Feature("MediaDownloader", loadParams = FeatureLoadParam } private fun canMergeOverlay(): Boolean { - if (context.config.options(ConfigProperty.DOWNLOAD_OPTIONS)["merge_overlay"] == false) return false + if (!context.config.downloader.autoDownloadOptions.get().contains("merge_overlay")) return false return isFFmpegPresent } //TODO: implement subfolder argument private fun createNewFilePath(hexHash: String, mediaDisplayType: String?, pathPrefix: String): String { - val downloadOptions = context.config.options(ConfigProperty.DOWNLOAD_OPTIONS) + val pathFormat by context.config.downloader.pathFormat val sanitizedPathPrefix = pathPrefix .replace(" ", "_") .replace(Regex("[\\\\:*?\"<>|]"), "") @@ -142,21 +141,21 @@ class MediaDownloader : Feature("MediaDownloader", loadParams = FeatureLoadParam } } - if (downloadOptions["create_user_folder"] == true) { + if (pathFormat.contains("create_user_folder")) { finalPath.append(sanitizedPathPrefix).append("/") } - if (downloadOptions["append_hash"] == true) { + if (pathFormat.contains("append_hash")) { appendFileName(hexHash) } mediaDisplayType?.let { - if (downloadOptions["append_type"] == true) { + if (pathFormat.contains("append_type")) { appendFileName(it.lowercase().replace(" ", "-")) } } - if (downloadOptions["append_username"] == true) { + if (pathFormat.contains("append_username")) { appendFileName(sanitizedPathPrefix) } - if (downloadOptions["append_date_time"] == true) { + if (pathFormat.contains("append_date_time")) { appendFileName(currentDateTime) } @@ -377,8 +376,8 @@ class MediaDownloader : Feature("MediaDownloader", loadParams = FeatureLoadParam } private fun canAutoDownload(keyFilter: String? = null): Boolean { - val options = context.config.options(ConfigProperty.AUTO_DOWNLOAD_OPTIONS) - return options.filter { it.value }.any { keyFilter == null || it.key.contains(keyFilter, true) } + val options by context.config.downloader.autoDownloadOptions + return options.any { keyFilter == null || it.contains(keyFilter, true) } } override fun asyncOnActivityCreate() { diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/experiments/AmoledDarkMode.kt b/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/experiments/AmoledDarkMode.kt @@ -14,7 +14,7 @@ import me.rhunk.snapenhance.hook.hook class AmoledDarkMode : Feature("Amoled Dark Mode", loadParams = FeatureLoadParams.ACTIVITY_CREATE_SYNC) { @SuppressLint("DiscouragedApi") override fun onActivityCreate() { - if (!context.config.bool(ConfigProperty.AMOLED_DARK_MODE)) return + if (!context.config.userInterface.amoledDarkMode.get()) return val attributeCache = mutableMapOf<String, Int>() fun getAttribute(name: String): Int { diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/experiments/AppPasscode.kt b/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/experiments/AppPasscode.kt @@ -27,7 +27,9 @@ class AppPasscode : Feature("App Passcode", loadParams = FeatureLoadParams.ACTIV fun lock() { if (isLocked) return isLocked = true - val passcode = context.config.string(ConfigProperty.APP_PASSCODE).also { if (it.isEmpty()) return } + val passcode by context.config.experimental.appPasscode.also { + if (it.getNullable()?.isEmpty() != false) return + } val isDigitPasscode = passcode.all { it.isDigit() } val mainActivity = context.mainActivity!! @@ -89,7 +91,7 @@ class AppPasscode : Feature("App Passcode", loadParams = FeatureLoadParams.ACTIV lock() } - if (!context.config.bool(ConfigProperty.APP_LOCK_ON_RESUME)) return + if (!context.config.experimental.appLockOnResume.get()) return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { context.mainActivity?.registerActivityLifecycleCallbacks(object: android.app.Application.ActivityLifecycleCallbacks { diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/experiments/DeviceSpooferHook.kt b/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/experiments/DeviceSpooferHook.kt @@ -9,39 +9,29 @@ import me.rhunk.snapenhance.hook.Hooker class DeviceSpooferHook: Feature("device_spoofer", loadParams = FeatureLoadParams.ACTIVITY_CREATE_ASYNC) { override fun asyncOnActivityCreate() { - //FINGERPRINT - if(getFingerprint().isNotEmpty()) { + val fingerprint by context.config.spoof.device.fingerprint + val androidId by context.config.spoof.device.androidId + + if (fingerprint.isNotEmpty()) { val fingerprintClass = android.os.Build::class.java Hooker.hook(fingerprintClass, "FINGERPRINT", HookStage.BEFORE) { hookAdapter -> - hookAdapter.setResult(getFingerprint()) + hookAdapter.setResult(fingerprint) + Logger.debug("Fingerprint spoofed to $fingerprint") } Hooker.hook(fingerprintClass, "deriveFingerprint", HookStage.BEFORE) { hookAdapter -> - hookAdapter.setResult(getFingerprint()) + hookAdapter.setResult(fingerprint) + Logger.debug("Fingerprint spoofed to $fingerprint") } } - else { - Logger.xposedLog("Fingerprint is null, not spoofing") - } - - //ANDROID ID - if(getAndroidId().isNotEmpty()) { + + if (androidId.isNotEmpty()) { val settingsSecureClass = android.provider.Settings.Secure::class.java Hooker.hook(settingsSecureClass, "getString", HookStage.BEFORE) { hookAdapter -> if(hookAdapter.args()[1] == "android_id") { - hookAdapter.setResult(getAndroidId()) + hookAdapter.setResult(androidId) + Logger.debug("Android ID spoofed to $androidId") } } } - else { - Logger.xposedLog("Android ID is null, not spoofing") - } - } - private fun getFingerprint():String { - return context.config.string(ConfigProperty.FINGERPRINT) - } - - private fun getAndroidId():String { - return context.config.string(ConfigProperty.ANDROID_ID) } - } \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/experiments/InfiniteStoryBoost.kt b/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/experiments/InfiniteStoryBoost.kt @@ -11,7 +11,7 @@ class InfiniteStoryBoost : Feature("InfiniteStoryBoost", loadParams = FeatureLoa val storyBoostStateClass = context.mappings.getMappedClass("StoryBoostStateClass") storyBoostStateClass.hookConstructor(HookStage.BEFORE, { - context.config.bool(ConfigProperty.INFINITE_STORY_BOOST) + context.config.experimental.infiniteStoryBoost.get() }) { param -> val startTimeMillis = param.arg<Long>(1) //reset timestamp if it's more than 24 hours diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/experiments/MeoPasscodeBypass.kt b/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/experiments/MeoPasscodeBypass.kt @@ -14,7 +14,7 @@ class MeoPasscodeBypass : Feature("Meo Passcode Bypass", loadParams = FeatureLoa context.androidContext.classLoader.loadClass(bcrypt["class"].toString()), bcrypt["hashMethod"].toString(), HookStage.BEFORE, - { context.config.bool(ConfigProperty.MEO_PASSCODE_BYPASS) }, + { context.config.experimental.meoPasscodeBypass.get() }, ) { 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/features/impl/experiments/UnlimitedMultiSnap.kt b/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/experiments/UnlimitedMultiSnap.kt @@ -10,7 +10,7 @@ import me.rhunk.snapenhance.util.setObjectField class UnlimitedMultiSnap : Feature("UnlimitedMultiSnap", loadParams = FeatureLoadParams.ACTIVITY_CREATE_ASYNC) { override fun asyncOnActivityCreate() { android.util.Pair::class.java.hookConstructor(HookStage.AFTER, { - context.config.bool(ConfigProperty.UNLIMITED_MULTI_SNAP) + context.config.experimental.unlimitedMultiSnap.get() }) { param -> val first = param.arg<Any>(0) val second = param.arg<Any>(1) diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/privacy/DisableMetrics.kt b/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/privacy/DisableMetrics.kt @@ -10,11 +10,10 @@ import me.rhunk.snapenhance.hook.Hooker class DisableMetrics : Feature("DisableMetrics", loadParams = FeatureLoadParams.INIT_SYNC) { override fun init() { - val disableMetricsFilter: (HookAdapter) -> Boolean = { - context.config.bool(ConfigProperty.DISABLE_METRICS) - } + val disableMetrics by context.config.global.disableMetrics - Hooker.hook(context.classCache.unifiedGrpcService, "unaryCall", HookStage.BEFORE, disableMetricsFilter) { param -> + Hooker.hook(context.classCache.unifiedGrpcService, "unaryCall", HookStage.BEFORE, + { disableMetrics }) { param -> val url: String = param.arg(0) if (url.endsWith("snapchat.valis.Valis/SendClientUpdate") || url.endsWith("targetingQuery") @@ -23,7 +22,8 @@ class DisableMetrics : Feature("DisableMetrics", loadParams = FeatureLoadParams. } } - Hooker.hook(context.classCache.networkApi, "submit", HookStage.BEFORE, disableMetricsFilter) { param -> + Hooker.hook(context.classCache.networkApi, "submit", HookStage.BEFORE, + { disableMetrics }) { param -> val httpRequest: Any = param.arg(0) val url = XposedHelpers.getObjectField(httpRequest, "mUrl").toString() /*if (url.contains("resolve?co=")) { diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/privacy/PreventMessageSending.kt b/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/privacy/PreventMessageSending.kt @@ -11,16 +11,15 @@ import me.rhunk.snapenhance.hook.hook class PreventMessageSending : Feature("Prevent message sending", loadParams = FeatureLoadParams.ACTIVITY_CREATE_ASYNC) { override fun asyncOnActivityCreate() { + val preventMessageSending by context.config.messaging.preventMessageSending + context.classCache.conversationManager.hook("updateMessage", HookStage.BEFORE) { param -> val messageUpdate = param.arg<Any>(2).toString(); - val options by lazy { - context.config.options(ConfigProperty.PREVENT_SENDING_MESSAGES) - } - if (messageUpdate == "SCREENSHOT" && options["chat_screenshot"] == true) { + if (messageUpdate == "SCREENSHOT" && preventMessageSending.contains("chat_screenshot")) { param.setResult(null) } - if (messageUpdate == "SCREEN_RECORD" && options["chat_screen_record"] == true) { + if (messageUpdate == "SCREEN_RECORD" && preventMessageSending.contains("chat_screen_record")) { param.setResult(null) } } @@ -29,9 +28,8 @@ class PreventMessageSending : Feature("Prevent message sending", loadParams = Fe val message = MessageContent(param.arg(1)) val contentType = message.contentType val associatedType = NotificationType.fromContentType(contentType) ?: return@hook - val options = context.config.options(ConfigProperty.PREVENT_SENDING_MESSAGES) - if (options[associatedType.key] == true) { + if (preventMessageSending.contains(associatedType.key)) { Logger.debug("Preventing message sending for $associatedType") param.setResult(null) } diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/spying/AnonymousStoryViewing.kt b/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/spying/AnonymousStoryViewing.kt @@ -9,7 +9,8 @@ import me.rhunk.snapenhance.util.getObjectField class AnonymousStoryViewing : Feature("Anonymous Story Viewing", loadParams = FeatureLoadParams.ACTIVITY_CREATE_ASYNC) { override fun asyncOnActivityCreate() { - Hooker.hook(context.classCache.networkApi,"submit", HookStage.BEFORE, { context.config.bool(ConfigProperty.ANONYMOUS_STORY_VIEW) }) { + val anonymousStoryViewProperty by context.config.messaging.anonymousStoryViewing + Hooker.hook(context.classCache.networkApi,"submit", HookStage.BEFORE, { anonymousStoryViewProperty }) { val httpRequest: Any = it.arg(0) val url = httpRequest.getObjectField("mUrl") as String if (url.endsWith("readreceipt-indexer/batchuploadreadreceipts") || url.endsWith("v2/batch_cta")) { diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/spying/MessageLogger.kt b/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/spying/MessageLogger.kt @@ -52,11 +52,6 @@ class MessageLogger : Feature("MessageLogger", @OptIn(ExperimentalTime::class) override fun asyncOnActivityCreate() { - ConfigProperty.MESSAGE_LOGGER.valueContainer.addPropertyChangeListener { - context.config.writeConfig() - context.softRestartApp() - } - if (!context.database.hasArroyo()) { return } @@ -130,9 +125,8 @@ class MessageLogger : Feature("MessageLogger", } override fun init() { - Hooker.hookConstructor(context.classCache.message, HookStage.AFTER, { - context.config.bool(ConfigProperty.MESSAGE_LOGGER) - }) { param -> + val messageLogger by context.config.messaging.messageLogger + Hooker.hookConstructor(context.classCache.message, HookStage.AFTER, { messageLogger }) { param -> processSnapMessage(param.thisObject()) } } diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/spying/PreventReadReceipts.kt b/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/spying/PreventReadReceipts.kt @@ -9,8 +9,9 @@ import me.rhunk.snapenhance.hook.Hooker class PreventReadReceipts : Feature("PreventReadReceipts", loadParams = FeatureLoadParams.ACTIVITY_CREATE_SYNC) { override fun onActivityCreate() { + val preventReadReceipts by context.config.messaging.preventReadReceipts val isConversationInStealthMode: (SnapUUID) -> Boolean = hook@{ - if (context.config.bool(ConfigProperty.PREVENT_READ_RECEIPTS)) return@hook true + if (preventReadReceipts) return@hook true context.feature(StealthMode::class).isStealth(it.toString()) } diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/tweaks/AutoSave.kt b/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/tweaks/AutoSave.kt @@ -32,6 +32,10 @@ class AutoSave : Feature("Auto Save", loadParams = FeatureLoadParams.ACTIVITY_CR context.classCache.conversationManager.methods.first { it.name == "fetchConversationWithMessagesPaginated" } } + private val autoSaveFilter by lazy { + context.config.messaging.autoSaveMessagesInConversations.get() + } + private fun saveMessage(conversationId: SnapUUID, message: Message) { val messageId = message.messageDescriptor.messageId if (messageLogger.isMessageRemoved(messageId)) return @@ -62,11 +66,11 @@ class AutoSave : Feature("Auto Save", loadParams = FeatureLoadParams.ACTIVITY_CR if (message.messageMetadata.savedBy.any { uuid -> uuid.toString() == myUserId }) return false val contentType = message.messageContent.contentType.toString() - return context.config.options(ConfigProperty.AUTO_SAVE_MESSAGES).filter { it.value }.any { it.key == contentType } + return autoSaveFilter.any { it == contentType } } private fun canSave(): Boolean { - if (context.config.options(ConfigProperty.AUTO_SAVE_MESSAGES).none { it.value }) return false + if (autoSaveFilter.isEmpty()) return false with(context.feature(Messaging::class)) { if (openedConversationUUID == null) return@canSave false diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/tweaks/CameraTweaks.kt b/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/tweaks/CameraTweaks.kt @@ -16,16 +16,16 @@ import me.rhunk.snapenhance.hook.hookConstructor class CameraTweaks : Feature("Camera Tweaks", loadParams = FeatureLoadParams.ACTIVITY_CREATE_SYNC) { companion object { - val resolutions = listOf("OFF", "3264x2448", "3264x1840", "3264x1504", "2688x1512", "2560x1920", "2448x2448", "2340x1080", "2160x1080", "1920x1440", "1920x1080", "1600x1200", "1600x960", "1600x900", "1600x736", "1600x720", "1560x720", "1520x720", "1440x1080", "1440x720", "1280x720", "1080x1080", "1080x720", "960x720", "720x720", "720x480", "640x480", "352x288", "320x240", "176x144") + val resolutions = listOf("3264x2448", "3264x1840", "3264x1504", "2688x1512", "2560x1920", "2448x2448", "2340x1080", "2160x1080", "1920x1440", "1920x1080", "1600x1200", "1600x960", "1600x900", "1600x736", "1600x720", "1560x720", "1520x720", "1440x1080", "1440x720", "1280x720", "1080x1080", "1080x720", "960x720", "720x720", "720x480", "640x480", "352x288", "320x240", "176x144") } - private fun parseResolution(resolution: String): IntArray? { - return resolution.takeIf { resolution != "OFF" }?.split("x")?.map { it.toInt() }?.toIntArray() + private fun parseResolution(resolution: String): IntArray { + return resolution.split("x").map { it.toInt() }.toIntArray() } @SuppressLint("MissingPermission", "DiscouragedApi") override fun onActivityCreate() { - if (context.config.bool(ConfigProperty.CAMERA_DISABLE)) { + if (context.config.camera.disable.get()) { ContextWrapper::class.java.hook("checkPermission", HookStage.BEFORE) { param -> val permission = param.arg<String>(0) if (permission == Manifest.permission.CAMERA) { @@ -39,16 +39,16 @@ class CameraTweaks : Feature("Camera Tweaks", loadParams = FeatureLoadParams.ACT } ConfigEnumKeys.hookAllEnums(context.mappings.getMappedClass("enums", "CAMERA")) { - if (key == "FORCE_CAMERA_HIGHEST_FPS" && context.config.bool(ConfigProperty.FORCE_HIGHEST_FRAME_RATE)) { + if (key == "FORCE_CAMERA_HIGHEST_FPS" && context.config.camera.forceHighestFrameRate.get()) { set(true) } - if (key == "MEDIA_RECORDER_MAX_QUALITY_LEVEL" && context.config.bool(ConfigProperty.FORCE_CAMERA_SOURCE_ENCODING)) { + if (key == "MEDIA_RECORDER_MAX_QUALITY_LEVEL" && context.config.camera.forceCameraSourceEncoding.get()) { value!!.javaClass.enumConstants?.let { enumData -> set(enumData.filter { it.toString() == "LEVEL_MAX" }) } } } - val previewResolutionConfig = parseResolution(context.config.state(ConfigProperty.OVERRIDE_PREVIEW_RESOLUTION)) - val captureResolutionConfig = parseResolution(context.config.state(ConfigProperty.OVERRIDE_PICTURE_RESOLUTION)) + val previewResolutionConfig = context.config.camera.overridePreviewResolution.getNullable()?.let { parseResolution(it) } + val captureResolutionConfig = context.config.camera.overridePictureResolution.getNullable()?.let { parseResolution(it) } context.mappings.getMappedClass("ScCameraSettings").hookConstructor(HookStage.BEFORE) { param -> val previewResolution = ScSize(param.argNullable(2)) diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/tweaks/DisableVideoLengthRestriction.kt b/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/tweaks/DisableVideoLengthRestriction.kt @@ -9,10 +9,9 @@ import me.rhunk.snapenhance.hook.Hooker class DisableVideoLengthRestriction : Feature("DisableVideoLengthRestriction", loadParams = FeatureLoadParams.ACTIVITY_CREATE_ASYNC) { override fun asyncOnActivityCreate() { val defaultMediaItem = context.mappings.getMappedClass("DefaultMediaItem") + val isState by context.config.global.disableVideoLengthRestrictions - Hooker.hookConstructor(defaultMediaItem, HookStage.BEFORE, { - context.config.bool(ConfigProperty.DISABLE_VIDEO_LENGTH_RESTRICTION) - }) { param -> + Hooker.hookConstructor(defaultMediaItem, HookStage.BEFORE, { isState }) { param -> //set the video length argument param.setArg(5, -1L) } diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/tweaks/GalleryMediaSendOverride.kt b/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/tweaks/GalleryMediaSendOverride.kt @@ -15,7 +15,7 @@ import me.rhunk.snapenhance.util.protobuf.ProtoReader class GalleryMediaSendOverride : Feature("Gallery Media Send Override", loadParams = FeatureLoadParams.INIT_SYNC) { override fun init() { Hooker.hook(context.classCache.conversationManager, "sendMessageWithContent", HookStage.BEFORE) { param -> - val overrideType = context.config.state(ConfigProperty.GALLERY_MEDIA_SEND_OVERRIDE).also { if (it == "OFF") return@hook } + val overrideType = context.config.messaging.galleryMediaSendOverride.getNullable() ?: return@hook val localMessageContent = MessageContent(param.arg(1)) if (localMessageContent.contentType != ContentType.EXTERNAL_MEDIA) return@hook diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/tweaks/GooglePlayServicesDialogs.kt b/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/tweaks/GooglePlayServicesDialogs.kt @@ -11,7 +11,7 @@ import java.lang.reflect.Modifier class GooglePlayServicesDialogs : Feature("Disable GMS Dialogs", loadParams = FeatureLoadParams.ACTIVITY_CREATE_ASYNC) { override fun asyncOnActivityCreate() { - if (!context.config.bool(ConfigProperty.DISABLE_GOOGLE_PLAY_DIALOGS)) return + 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 -> diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/tweaks/LocationSpoofer.kt b/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/tweaks/LocationSpoofer.kt @@ -6,6 +6,7 @@ import me.rhunk.snapenhance.features.Feature import me.rhunk.snapenhance.features.FeatureLoadParams import me.rhunk.snapenhance.hook.HookStage import me.rhunk.snapenhance.hook.Hooker +import me.rhunk.snapenhance.hook.hook class LocationSpoofer: Feature("LocationSpoof", loadParams = FeatureLoadParams.ACTIVITY_CREATE_ASYNC) { override fun asyncOnActivityCreate() { @@ -13,52 +14,29 @@ class LocationSpoofer: Feature("LocationSpoof", loadParams = FeatureLoadParams.A val intent = param.argNullable<Intent>(2) ?: return@hook val bundle = intent.getBundleExtra("location") ?: return@hook param.setResult(null) - val latitude = bundle.getFloat("latitude") - val longitude = bundle.getFloat("longitude") - with(context.config) { - get(ConfigProperty.LATITUDE).writeFrom(latitude.toString()) - get(ConfigProperty.LONGITUDE).writeFrom(longitude.toString()) - writeConfig() + with(context.config.spoof.location) { + latitude.set(bundle.getFloat("latitude")) + longitude.set(bundle.getFloat("longitude")) + + context.longToast("Location set to $latitude, $longitude") } - context.longToast("Location set to $latitude, $longitude") } - if (!context.config.bool(ConfigProperty.LOCATION_SPOOF)) return - val locationClass = android.location.Location::class.java - val locationManagerClass = android.location.LocationManager::class.java + if (context.config.spoof.location.globalState != true) return - Hooker.hook(locationClass, "getLatitude", HookStage.BEFORE) { hookAdapter -> - hookAdapter.setResult(getLatitude()) - } + val latitude by context.config.spoof.location.latitude + val longitude by context.config.spoof.location.longitude - Hooker.hook(locationClass, "getLongitude", HookStage.BEFORE) { hookAdapter -> - hookAdapter.setResult(getLongitude()) - } + val locationClass = android.location.Location::class.java + val locationManagerClass = android.location.LocationManager::class.java - Hooker.hook(locationClass, "getAccuracy", HookStage.BEFORE) { hookAdapter -> - hookAdapter.setResult(getAccuracy()) - } + locationClass.hook("getLatitude", HookStage.BEFORE) { it.setResult(latitude) } + locationClass.hook("getLongitude", HookStage.BEFORE) { it.setResult(longitude) } + locationClass.hook("getAccuracy", HookStage.BEFORE) { it.setResult(0.0F) } //Might be redundant because it calls isProviderEnabledForUser which we also hook, meaning if isProviderEnabledForUser returns true this will also return true - Hooker.hook(locationManagerClass, "isProviderEnabled", HookStage.BEFORE) { hookAdapter -> - hookAdapter.setResult(true) - } - - Hooker.hook(locationManagerClass, "isProviderEnabledForUser", HookStage.BEFORE) {hookAdapter -> - hookAdapter.setResult(true) - } - } - - private fun getLatitude():Double { - return context.config.string(ConfigProperty.LATITUDE).toDouble() - } - - private fun getLongitude():Double { - return context.config.string(ConfigProperty.LONGITUDE).toDouble() - } - - private fun getAccuracy():Float { - return 0.0f + locationManagerClass.hook("isProviderEnabled", HookStage.BEFORE) { it.setResult(true) } + locationManagerClass.hook("isProviderEnabledForUser", HookStage.BEFORE) { it.setResult(true) } } } \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/tweaks/MediaQualityLevelOverride.kt b/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/tweaks/MediaQualityLevelOverride.kt @@ -11,10 +11,12 @@ class MediaQualityLevelOverride : Feature("MediaQualityLevelOverride", loadParam val enumQualityLevel = context.mappings.getMappedClass("EnumQualityLevel") val mediaQualityLevelProvider = context.mappings.getMappedMap("MediaQualityLevelProvider") + val forceMediaSourceQuality by context.config.global.forceMediaSourceQuality + context.androidContext.classLoader.loadClass(mediaQualityLevelProvider["class"].toString()).hook( mediaQualityLevelProvider["method"].toString(), HookStage.BEFORE, - { context.config.bool(ConfigProperty.FORCE_MEDIA_SOURCE_QUALITY) } + { forceMediaSourceQuality } ) { param -> param.setResult(enumQualityLevel.enumConstants.firstOrNull { it.toString() == "LEVEL_MAX" } ) } diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/tweaks/Notifications.kt b/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/tweaks/Notifications.kt @@ -65,6 +65,10 @@ class Notifications : Feature("Notifications", loadParams = FeatureLoadParams.IN context.androidContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager } + private val betterNotificationFilter by lazy { + context.config.global.betterNotifications.get() + } + private fun setNotificationText(notification: Notification, conversationId: String) { val messageText = StringBuilder().apply { cachedMessages.computeIfAbsent(conversationId) { mutableListOf() }.forEach { @@ -87,7 +91,6 @@ class Notifications : Feature("Notifications", loadParams = FeatureLoadParams.IN } private fun setupNotificationActionButtons(contentType: ContentType, conversationId: String, messageId: Long, notificationData: NotificationData) { - val betterNotifications = context.config.options(ConfigProperty.BETTER_NOTIFICATIONS) val notificationBuilder = XposedHelpers.newInstance( Notification.Builder::class.java, @@ -115,7 +118,7 @@ class Notifications : Feature("Notifications", loadParams = FeatureLoadParams.IN } newAction("Reply", ACTION_REPLY, { - betterNotifications["reply_button"] == true && contentType == ContentType.CHAT + betterNotificationFilter.contains("reply_button") && contentType == ContentType.CHAT }) { val chatReplyInput = RemoteInput.Builder("chat_reply_input") .setLabel("Reply") @@ -124,7 +127,7 @@ class Notifications : Feature("Notifications", loadParams = FeatureLoadParams.IN } newAction("Download", ACTION_DOWNLOAD, { - betterNotifications["download_button"] == true && (contentType == ContentType.EXTERNAL_MEDIA || contentType == ContentType.SNAP) + betterNotificationFilter.contains("download_button") && (contentType == ContentType.EXTERNAL_MEDIA || contentType == ContentType.SNAP) }) {} notificationBuilder.setActions(*actions.toTypedArray()) @@ -282,8 +285,7 @@ class Notifications : Feature("Notifications", loadParams = FeatureLoadParams.IN val notificationType = extras.getString("notification_type") ?: return@hook val conversationId = extras.getString("conversation_id") ?: return@hook - if (context.config.options(ConfigProperty.BETTER_NOTIFICATIONS) - .filter { it.value }.map { it.key.uppercase() }.none { + if (betterNotificationFilter.map { it.uppercase() }.none { notificationType.contains(it) }) return@hook @@ -319,15 +321,15 @@ class Notifications : Feature("Notifications", loadParams = FeatureLoadParams.IN } findClass("com.google.firebase.messaging.FirebaseMessagingService").run { + val states by context.config.global.notificationBlacklist methods.first { it.declaringClass == this && it.returnType == Void::class.javaPrimitiveType && it.parameterCount == 1 && it.parameterTypes[0] == Intent::class.java } .hook(HookStage.BEFORE) { param -> val intent = param.argNullable<Intent>(0) ?: return@hook val messageType = intent.getStringExtra("type") ?: return@hook - val states = context.config.options(ConfigProperty.NOTIFICATION_BLACKLIST) Logger.xposedLog("received message type: $messageType") - if (states[messageType.replaceFirst("mischief_", "")] == true) { + if (states.contains(messageType.replaceFirst("mischief_", ""))) { param.setResult(null) } } diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/tweaks/SnapchatPlus.kt b/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/tweaks/SnapchatPlus.kt @@ -11,7 +11,7 @@ class SnapchatPlus: Feature("SnapchatPlus", loadParams = FeatureLoadParams.ACTIV private val expirationTimeMillis = (System.currentTimeMillis() + 15552000000L) override fun asyncOnActivityCreate() { - if (!context.config.bool(ConfigProperty.SNAPCHAT_PLUS)) return + if (!context.config.global.snapchatPlus.get()) return val subscriptionInfoClass = context.mappings.getMappedClass("SubscriptionInfoClass") diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/tweaks/UnlimitedSnapViewTime.kt b/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/tweaks/UnlimitedSnapViewTime.kt @@ -14,9 +14,8 @@ import me.rhunk.snapenhance.util.protobuf.ProtoReader class UnlimitedSnapViewTime : Feature("UnlimitedSnapViewTime", loadParams = FeatureLoadParams.ACTIVITY_CREATE_SYNC) { override fun onActivityCreate() { - Hooker.hookConstructor(context.classCache.message, HookStage.AFTER, { - context.config.bool(ConfigProperty.UNLIMITED_SNAP_VIEW_TIME) - }) { param -> + val state by context.config.messaging.unlimitedSnapViewTime + Hooker.hookConstructor(context.classCache.message, HookStage.AFTER, { state }) { param -> val message = Message(param.thisObject()) if (message.messageState != MessageState.COMMITTED) return@hookConstructor if (message.messageContent.contentType != ContentType.SNAP) return@hookConstructor diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/ui/StartupPageOverride.kt b/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/ui/StartupPageOverride.kt @@ -25,9 +25,7 @@ class StartupPageOverride : Feature("StartupPageOverride", loadParams = FeatureL */ override fun onActivityCreate() { - val ngsIconName = context.config.state(ConfigProperty.STARTUP_PAGE_OVERRIDE).also { - if (it == "OFF") return - } + val ngsIconName = context.config.userInterface.startupTab.getNullable() ?: return context.androidContext.classLoader.loadClass("com.snap.mushroom.MainActivity").apply { hook("onPostCreate", HookStage.AFTER) { diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/ui/UITweaks.kt b/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/ui/UITweaks.kt @@ -36,10 +36,10 @@ class UITweaks : Feature("UITweaks", loadParams = FeatureLoadParams.ACTIVITY_CRE @SuppressLint("DiscouragedApi", "InternalInsetResource") override fun onActivityCreate() { - val blockAds = context.config.bool(ConfigProperty.BLOCK_ADS) - val hiddenElements = context.config.options(ConfigProperty.HIDE_UI_ELEMENTS) - val hideStorySection = context.config.options(ConfigProperty.HIDE_STORY_SECTION) - val isImmersiveCamera = context.config.bool(ConfigProperty.IMMERSIVE_CAMERA_PREVIEW) + val blockAds by context.config.userInterface.blockAds + val hiddenElements by context.config.userInterface.hideUiComponents + val hideStorySections by context.config.userInterface.hideStorySections + val isImmersiveCamera by context.config.camera.immersiveCameraPreview val displayMetrics = context.resources.displayMetrics @@ -61,11 +61,11 @@ class UITweaks : Feature("UITweaks", loadParams = FeatureLoadParams.ACTIVITY_CRE Hooker.hook(View::class.java, "setVisibility", HookStage.BEFORE) { methodParam -> val viewId = (methodParam.thisObject() as View).id - if (viewId == chatNoteRecordButton && hiddenElements["remove_voice_record_button"] == true) { + if (viewId == chatNoteRecordButton && hiddenElements.contains("hide_voice_record_button")) { methodParam.setArg(0, View.GONE) } if (viewId == callButton1 || viewId == callButton2) { - if (hiddenElements["remove_call_buttons"] == false) return@hook + if (!hiddenElements.contains("hide_call_buttons")) return@hook methodParam.setArg(0, View.GONE) } } @@ -79,7 +79,7 @@ class UITweaks : Feature("UITweaks", loadParams = FeatureLoadParams.ACTIVITY_CRE val view: View = param.arg(0) val viewId = view.id - if (hideStorySection["hide_for_you"] == true) { + if (hideStorySections.contains("hide_for_you")) { if (viewId == getIdentifier("df_large_story", "id") || viewId == getIdentifier("df_promoted_story", "id")) { hideStorySection(param) @@ -90,12 +90,12 @@ class UITweaks : Feature("UITweaks", loadParams = FeatureLoadParams.ACTIVITY_CRE } } - if (hideStorySection["hide_friends"] == true && viewId == getIdentifier("friend_card_frame", "id")) { + if (hideStorySections.contains("hide_friends") && viewId == getIdentifier("friend_card_frame", "id")) { hideStorySection(param) } //mappings? - if (hideStorySection["hide_friend_suggestions"] == true && view.javaClass.superclass?.name?.endsWith("StackDrawLayout") == true) { + if (hideStorySections.contains("hide_friend_suggestions") && view.javaClass.superclass?.name?.endsWith("StackDrawLayout") == true) { val layoutParams = view.layoutParams as? FrameLayout.LayoutParams ?: return@hook if (layoutParams.width == -1 && layoutParams.height == -2 && @@ -108,7 +108,7 @@ class UITweaks : Feature("UITweaks", loadParams = FeatureLoadParams.ACTIVITY_CRE } } - if (hideStorySection["hide_following"] == true && (viewId == getIdentifier("df_small_story", "id")) + if (hideStorySections.contains("hide_following") && (viewId == getIdentifier("df_small_story", "id")) ) { hideStorySection(param) } @@ -134,26 +134,26 @@ class UITweaks : Feature("UITweaks", loadParams = FeatureLoadParams.ACTIVITY_CRE } } - if (viewId == chatNoteRecordButton && hiddenElements["remove_voice_record_button"] == true) { + if (viewId == chatNoteRecordButton && hiddenElements.contains("hide_voice_record_button")) { view.isEnabled = false view.setWillNotDraw(true) } - if (getIdentifier("chat_input_bar_cognac", "id") == viewId && hiddenElements["remove_cognac_button"] == true) { + if (getIdentifier("chat_input_bar_cognac", "id") == viewId && hiddenElements.contains("hide_cognac_button")) { view.visibility = View.GONE } - if (getIdentifier("chat_input_bar_sticker", "id") == viewId && hiddenElements["remove_stickers_button"] == true) { + if (getIdentifier("chat_input_bar_sticker", "id") == viewId && hiddenElements.contains("hide_stickers_button")) { view.visibility = View.GONE } - if (getIdentifier("chat_input_bar_sharing_drawer_button", "id") == viewId && hiddenElements["remove_live_location_share_button"] == true) { + if (getIdentifier("chat_input_bar_sharing_drawer_button", "id") == viewId && hiddenElements.contains("hide_live_location_share_button")) { param.setResult(null) } if (viewId == callButton1 || viewId == callButton2) { - if (hiddenElements["remove_call_buttons"] == false) return@hook + if (!hiddenElements.contains("hide_call_buttons")) return@hook if (view.visibility == View.GONE) return@hook } if (viewId == callButtonsStub) { - if (hiddenElements["remove_call_buttons"] == false) return@hook + if (!hiddenElements.contains("hide_call_buttons")) return@hook param.setResult(null) } } diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/hook/Hooker.kt b/core/src/main/kotlin/me/rhunk/snapenhance/hook/Hooker.kt @@ -4,11 +4,14 @@ import de.robv.android.xposed.XC_MethodHook import de.robv.android.xposed.XposedBridge import java.lang.reflect.Member +typealias HookFilter = (HookAdapter) -> Boolean +typealias HookConsumer = (HookAdapter) -> Unit + object Hooker { inline fun newMethodHook( stage: HookStage, - crossinline consumer: (HookAdapter) -> Unit, - crossinline filter: ((HookAdapter) -> Boolean) = { true } + crossinline consumer: HookConsumer, + crossinline filter: HookFilter = { true } ): XC_MethodHook { return if (stage == HookStage.BEFORE) object : XC_MethodHook() { override fun beforeHookedMethod(param: MethodHookParam<*>) { @@ -25,21 +28,21 @@ object Hooker { clazz: Class<*>, methodName: String, stage: HookStage, - crossinline consumer: (HookAdapter) -> Unit + crossinline consumer: HookConsumer ): Set<XC_MethodHook.Unhook> = hook(clazz, methodName, stage, { true }, consumer) inline fun hook( clazz: Class<*>, methodName: String, stage: HookStage, - crossinline filter: (HookAdapter) -> Boolean, - crossinline consumer: (HookAdapter) -> Unit + crossinline filter: HookFilter, + crossinline consumer: HookConsumer ): Set<XC_MethodHook.Unhook> = XposedBridge.hookAllMethods(clazz, methodName, newMethodHook(stage, consumer, filter)) inline fun hook( member: Member, stage: HookStage, - crossinline consumer: (HookAdapter) -> Unit + crossinline consumer: HookConsumer ): XC_MethodHook.Unhook { return hook(member, stage, { true }, consumer) } @@ -47,8 +50,8 @@ object Hooker { inline fun hook( member: Member, stage: HookStage, - crossinline filter: ((HookAdapter) -> Boolean), - crossinline consumer: (HookAdapter) -> Unit + crossinline filter: HookFilter, + crossinline consumer: HookConsumer ): XC_MethodHook.Unhook { return XposedBridge.hookMethod(member, newMethodHook(stage, consumer, filter)) } @@ -57,7 +60,7 @@ object Hooker { inline fun hookConstructor( clazz: Class<*>, stage: HookStage, - crossinline consumer: (HookAdapter) -> Unit + crossinline consumer: HookConsumer ) { XposedBridge.hookAllConstructors(clazz, newMethodHook(stage, consumer)) } @@ -65,8 +68,8 @@ object Hooker { inline fun hookConstructor( clazz: Class<*>, stage: HookStage, - crossinline filter: ((HookAdapter) -> Boolean), - crossinline consumer: (HookAdapter) -> Unit + crossinline filter: HookFilter, + crossinline consumer: HookConsumer ) { XposedBridge.hookAllConstructors(clazz, newMethodHook(stage, consumer, filter)) } @@ -76,7 +79,7 @@ object Hooker { instance: Any, methodName: String, stage: HookStage, - crossinline hookConsumer: (HookAdapter) -> Unit + crossinline hookConsumer: HookConsumer ) { val unhooks: MutableSet<XC_MethodHook.Unhook> = HashSet() hook(clazz, methodName, stage) { param-> @@ -92,7 +95,7 @@ object Hooker { clazz: Class<*>, methodName: String, stage: HookStage, - crossinline hookConsumer: (HookAdapter) -> Unit + crossinline hookConsumer: HookConsumer ) { val unhooks: MutableSet<XC_MethodHook.Unhook> = HashSet() hook(clazz, methodName, stage) { param-> @@ -106,7 +109,7 @@ object Hooker { instance: Any, methodName: String, stage: HookStage, - crossinline hookConsumer: (HookAdapter) -> Unit + crossinline hookConsumer: HookConsumer ) { val unhooks: MutableSet<XC_MethodHook.Unhook> = HashSet() hook(clazz, methodName, stage) { param-> @@ -119,35 +122,35 @@ object Hooker { inline fun Class<*>.hookConstructor( stage: HookStage, - crossinline consumer: (HookAdapter) -> Unit + crossinline consumer: HookConsumer ) = Hooker.hookConstructor(this, stage, consumer) inline fun Class<*>.hookConstructor( stage: HookStage, - crossinline filter: ((HookAdapter) -> Boolean), - crossinline consumer: (HookAdapter) -> Unit + crossinline filter: HookFilter, + crossinline consumer: HookConsumer ) = Hooker.hookConstructor(this, stage, filter, consumer) inline fun Class<*>.hook( methodName: String, stage: HookStage, - crossinline consumer: (HookAdapter) -> Unit + crossinline consumer: HookConsumer ): Set<XC_MethodHook.Unhook> = Hooker.hook(this, methodName, stage, consumer) inline fun Class<*>.hook( methodName: String, stage: HookStage, - crossinline filter: (HookAdapter) -> Boolean, - crossinline consumer: (HookAdapter) -> Unit + crossinline filter: HookFilter, + crossinline consumer: HookConsumer ): Set<XC_MethodHook.Unhook> = Hooker.hook(this, methodName, stage, filter, consumer) inline fun Member.hook( stage: HookStage, - crossinline consumer: (HookAdapter) -> Unit + crossinline consumer: HookConsumer ): XC_MethodHook.Unhook = Hooker.hook(this, stage, consumer) inline fun Member.hook( stage: HookStage, - crossinline filter: ((HookAdapter) -> Boolean), - crossinline consumer: (HookAdapter) -> Unit + crossinline filter: HookFilter, + crossinline consumer: HookConsumer ): XC_MethodHook.Unhook = Hooker.hook(this, stage, filter, consumer) \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/ui/ItemHelper.kt b/core/src/main/kotlin/me/rhunk/snapenhance/ui/ItemHelper.kt @@ -82,4 +82,6 @@ class ItemHelper( callback(value) } } + + } diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/ui/menu/impl/ChatActionMenu.kt b/core/src/main/kotlin/me/rhunk/snapenhance/ui/menu/impl/ChatActionMenu.kt @@ -104,7 +104,7 @@ class ChatActionMenu : AbstractMenu() { } } - if (context.config.bool(ConfigProperty.CHAT_DOWNLOAD_CONTEXT_MENU)) { + if (context.config.downloader.chatDownloadContextMenu.get()) { injectButton(Button(viewGroup.context).apply { text = this@ChatActionMenu.context.translation["chat_action_menu.preview_button"] setOnClickListener { @@ -127,7 +127,7 @@ class ChatActionMenu : AbstractMenu() { } //delete logged message button - if (context.config.bool(ConfigProperty.MESSAGE_LOGGER)) { + if (context.config.messaging.messageLogger.get()) { injectButton(Button(viewGroup.context).apply { text = this@ChatActionMenu.context.translation["chat_action_menu.delete_logged_message_button"] setOnClickListener { diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/ui/menu/impl/FriendFeedInfoMenu.kt b/core/src/main/kotlin/me/rhunk/snapenhance/ui/menu/impl/FriendFeedInfoMenu.kt @@ -104,7 +104,7 @@ class FriendFeedInfoMenu : AbstractMenu() { //query message val messages: List<ConversationMessage>? = context.database.getMessagesFromConversationId( conversationId, - context.config.int(ConfigProperty.MESSAGE_PREVIEW_LENGTH) + context.config.messaging.messagePreviewLength.get() )?.reversed() if (messages.isNullOrEmpty()) { @@ -240,12 +240,12 @@ class FriendFeedInfoMenu : AbstractMenu() { fun inject(viewModel: View, viewConsumer: ((View) -> Unit)) { val modContext = context - val friendFeedMenuOptions = context.config.options(ConfigProperty.FRIEND_FEED_MENU_BUTTONS) - if (friendFeedMenuOptions.none { it.value }) return + val friendFeedMenuOptions by context.config.userInterface.friendFeedMenuButtons + if (friendFeedMenuOptions.isEmpty()) return val (conversationId, targetUser) = getCurrentConversationInfo() - if (!context.config.bool(ConfigProperty.ENABLE_FRIEND_FEED_MENU_BAR)) { + if (!context.config.userInterface.enableFriendFeedMenuBar.get()) { //preview button val previewButton = Button(viewModel.context).apply { text = modContext.translation["friend_menu_option.preview"] @@ -272,7 +272,7 @@ class FriendFeedInfoMenu : AbstractMenu() { } } - if (friendFeedMenuOptions["anti_auto_save"] == true) { + if (friendFeedMenuOptions.contains("anti_auto_save")) { createToggleFeature(viewConsumer, "friend_menu_option.anti_auto_save", { context.feature(AntiAutoSave::class).isConversationIgnored(conversationId) }, @@ -282,7 +282,7 @@ class FriendFeedInfoMenu : AbstractMenu() { run { val userId = context.database.getFriendFeedInfoByConversationId(conversationId)?.friendUserId ?: return@run - if (friendFeedMenuOptions["auto_download_blacklist"] == true) { + if (friendFeedMenuOptions.contains("auto_download_blacklist")) { createToggleFeature(viewConsumer, "friend_menu_option.auto_download_blacklist", { context.feature(AntiAutoDownload::class).isUserIgnored(userId) }, @@ -291,10 +291,10 @@ class FriendFeedInfoMenu : AbstractMenu() { } } - if (friendFeedMenuOptions["stealth_mode"] == true) { + if (friendFeedMenuOptions.contains("stealth_mode")) { viewConsumer(stealthSwitch) } - if (friendFeedMenuOptions["conversation_info"] == true) { + if (friendFeedMenuOptions.contains("conversation_info")) { viewConsumer(previewButton) } return @@ -338,7 +338,7 @@ class FriendFeedInfoMenu : AbstractMenu() { }) } - if (friendFeedMenuOptions["auto_download_blacklist"] == true) { + if (friendFeedMenuOptions.contains("auto_download_blacklist")) { run { val userId = context.database.getFriendFeedInfoByConversationId(conversationId)?.friendUserId @@ -352,7 +352,7 @@ class FriendFeedInfoMenu : AbstractMenu() { } } - if (friendFeedMenuOptions["anti_auto_save"] == true) { + if (friendFeedMenuOptions.contains("anti_auto_save")) { //diskette createActionButton("\uD83D\uDCAC", isDisabled = !context.feature(AntiAutoSave::class) @@ -363,7 +363,7 @@ class FriendFeedInfoMenu : AbstractMenu() { } - if (friendFeedMenuOptions["stealth_mode"] == true) { + if (friendFeedMenuOptions.contains("stealth_mode")) { //eyes createActionButton( "\uD83D\uDC7B", @@ -376,7 +376,7 @@ class FriendFeedInfoMenu : AbstractMenu() { } } - if (friendFeedMenuOptions["conversation_info"] == true) { + if (friendFeedMenuOptions.contains("conversation_info")) { //user createActionButton("\uD83D\uDC64") { showPreview( diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/ui/menu/impl/MenuViewInjector.kt b/core/src/main/kotlin/me/rhunk/snapenhance/ui/menu/impl/MenuViewInjector.kt @@ -124,7 +124,7 @@ class MenuViewInjector : Feature("MenuViewInjector", loadParams = FeatureLoadPar viewGroup.addOnAttachStateChangeListener(object: View.OnAttachStateChangeListener { override fun onViewAttachedToWindow(v: View) {} override fun onViewDetachedFromWindow(v: View) { - context.config.writeConfig() + //context.config.writeConfig() } }) return@hook @@ -132,10 +132,9 @@ class MenuViewInjector : Feature("MenuViewInjector", loadParams = FeatureLoadPar if (messaging.lastFetchConversationUUID == null || messaging.lastFetchConversationUserUUID == null) return@hook //filter by the slot index - if (viewGroup.getChildCount() != context.config.int(ConfigProperty.FRIEND_FEED_MENU_POSITION)) return@hook + if (viewGroup.getChildCount() != context.config.userInterface.friendFeedMenuPosition.get()) return@hook friendFeedInfoMenu.inject(viewGroup, originalAddView) } } } - } \ No newline at end of file