refactor: config system

This commit is contained in:
rhunk
2023-08-01 01:14:04 +02:00
parent 5d4e2aacb1
commit 1e02e0a61c
54 changed files with 712 additions and 240 deletions

View File

@@ -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)

View File

@@ -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)

View File

@@ -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()
}
}
)

View File

@@ -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()
}
}
}

View File

@@ -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
}

View File

@@ -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 {

View File

@@ -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)
}
}

View File

@@ -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

View File

@@ -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)

View File

@@ -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 }
}

View File

@@ -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(),
)

View File

@@ -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)
}
},
)
}

View File

@@ -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()
}
}

View File

@@ -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")
}

View File

@@ -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")
}

View File

@@ -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")
}

View File

@@ -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")
}

View File

@@ -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)
}

View File

@@ -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())
}

View File

@@ -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())
}

View File

@@ -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)
}

View File

@@ -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 {

View File

@@ -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)

View File

@@ -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)
}
}
}

View File

@@ -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)
}

View File

@@ -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() {

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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)
}
}

View File

@@ -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

View File

@@ -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))

View File

@@ -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)

View File

@@ -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=")) {

View File

@@ -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)
}

View File

@@ -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")) {

View File

@@ -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())
}
}

View File

@@ -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())
}

View File

@@ -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

View File

@@ -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))

View File

@@ -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)
}

View File

@@ -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

View File

@@ -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 ->

View File

@@ -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
if (context.config.spoof.location.globalState != true) return
val latitude by context.config.spoof.location.latitude
val longitude by context.config.spoof.location.longitude
val locationClass = android.location.Location::class.java
val locationManagerClass = android.location.LocationManager::class.java
Hooker.hook(locationClass, "getLatitude", HookStage.BEFORE) { hookAdapter ->
hookAdapter.setResult(getLatitude())
}
Hooker.hook(locationClass, "getLongitude", HookStage.BEFORE) { hookAdapter ->
hookAdapter.setResult(getLongitude())
}
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) }
}
}

View File

@@ -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" } )
}

View File

@@ -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)
}
}

View File

@@ -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")

View File

@@ -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

View File

@@ -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) {

View File

@@ -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)
}
}

View File

@@ -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)

View File

@@ -82,4 +82,6 @@ class ItemHelper(
callback(value)
}
}
}

View File

@@ -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 {

View File

@@ -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(

View File

@@ -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)
}
}
}
}