refactor: config system
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
@@ -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(),
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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=")) {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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")) {
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 ->
|
||||
|
||||
@@ -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) }
|
||||
}
|
||||
}
|
||||
@@ -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" } )
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
@@ -82,4 +82,6 @@ class ItemHelper(
|
||||
callback(value)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user