feat: config & notification filter

This commit is contained in:
rhunk
2023-05-26 16:23:16 +02:00
parent 651c69d22f
commit 0389157471
12 changed files with 251 additions and 98 deletions

View File

@@ -18,7 +18,8 @@
"save_folder": "Save Folder",
"prevent_read_receipts": "Prevent Read Receipts",
"hide_bitmoji_presence": "Hide Bitmoji Presence",
"show_message_content": "Show Message Content",
"show_message_content_in_notifications": "Show Message Content In Notifications",
"notification_filter": "Notification Filter",
"message_logger": "Message Logger",
"unlimited_snap_view_time": "Unlimited Snap View Time",
"auto_download_snaps": "Auto Download Snaps",
@@ -49,7 +50,16 @@
"new_map_ui": "New Map UI",
"use_download_manager": "Use Android Download Manager"
},
"option": {
"property": {
"notification_filter": {
"chat": "Chat",
"snap": "Snap",
"typing": "Typing"
}
}
},
"friend_menu_option": {
"preview": "Preview",

View File

@@ -1,58 +1,58 @@
package me.rhunk.snapenhance.config
open class ConfigAccessor(
private val configMap: MutableMap<ConfigProperty, Any?>
private val configMap: MutableMap<ConfigProperty, ConfigValue<*>> = mutableMapOf()
) {
fun bool(key: ConfigProperty): Boolean {
return get(key) as Boolean
return get(key).value() as Boolean
}
fun int(key: ConfigProperty): Int {
return get(key) as Int
return get(key).value() as Int
}
fun string(key: ConfigProperty): String {
return get(key) as String
return get(key).value() as String
}
fun double(key: ConfigProperty): Double {
return get(key) as Double
return get(key).value() as Double
}
fun float(key: ConfigProperty): Float {
return get(key) as Float
return get(key).value() as Float
}
fun long(key: ConfigProperty): Long {
return get(key) as Long
return get(key).value() as Long
}
fun short(key: ConfigProperty): Short {
return get(key) as Short
return get(key).value() as Short
}
fun byte(key: ConfigProperty): Byte {
return get(key) as Byte
return get(key).value() as Byte
}
fun char(key: ConfigProperty): Char {
return get(key) as Char
return get(key).value() as Char
}
@Suppress("UNCHECKED_CAST")
fun <T> list(key: ConfigProperty): List<T> {
return get(key) as List<T>
fun options(key: ConfigProperty): Map<String, Boolean> {
return get(key).value() as Map<String, Boolean>
}
fun get(key: ConfigProperty): Any? {
return configMap[key]
fun get(key: ConfigProperty): ConfigValue<*> {
return configMap[key]!!
}
fun set(key: ConfigProperty, value: Any?) {
fun set(key: ConfigProperty, value: ConfigValue<*>) {
configMap[key] = value
}
fun entries(): Set<Map.Entry<ConfigProperty, Any?>> {
fun entries(): Set<Map.Entry<ConfigProperty, ConfigValue<*>>> {
return configMap.entries
}
}

View File

@@ -1,174 +1,192 @@
package me.rhunk.snapenhance.config
import android.os.Environment
import me.rhunk.snapenhance.config.impl.ConfigIntegerValue
import me.rhunk.snapenhance.config.impl.ConfigStateListValue
import me.rhunk.snapenhance.config.impl.ConfigStateValue
import me.rhunk.snapenhance.config.impl.ConfigStringValue
import java.io.File
enum class ConfigProperty(
val nameKey: String,
val descriptionKey: String,
val category: ConfigCategory,
val defaultValue: Any
val valueContainer: ConfigValue<*>
) {
SAVE_FOLDER(
"property.save_folder", "description.save_folder", ConfigCategory.GENERAL,
File(
ConfigStringValue(File(
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM).absolutePath + "/Snapchat",
"SnapEnhance"
).absolutePath
).absolutePath)
),
PREVENT_READ_RECEIPTS(
"property.prevent_read_receipts",
"description.prevent_read_receipts",
ConfigCategory.SPY,
false
ConfigStateValue(false)
),
HIDE_BITMOJI_PRESENCE(
"property.hide_bitmoji_presence",
"description.hide_bitmoji_presence",
ConfigCategory.SPY,
false
ConfigStateValue(false)
),
SHOW_MESSAGE_CONTENT(
"property.show_message_content",
"description.show_message_content",
SHOW_MESSAGE_CONTENT_IN_NOTIFICATIONS(
"property.show_message_content_in_notifications",
"description.show_message_content_in_notifications",
ConfigCategory.SPY,
false
ConfigStateValue(false)
),
MESSAGE_LOGGER("property.message_logger", "description.message_logger", ConfigCategory.SPY, false),
UNLIMITED_SNAP_VIEW_TIME("property.unlimited_snap_view_time", "description.unlimited_snap_view_time", ConfigCategory.SPY, false),
NOTIFICATION_FILTER(
"property.notification_filter",
"description.notification_filter",
ConfigCategory.SPY,
ConfigStateListValue(
listOf("snap", "chat", "typing"),
mutableMapOf(
"snap" to true,
"chat" to true,
"typing" to true
)
)
),
MESSAGE_LOGGER("property.message_logger", "description.message_logger", ConfigCategory.SPY, ConfigStateValue(false)),
UNLIMITED_SNAP_VIEW_TIME("property.unlimited_snap_view_time", "description.unlimited_snap_view_time", ConfigCategory.SPY, ConfigStateValue(false)),
AUTO_DOWNLOAD_SNAPS(
"property.auto_download_snaps",
"description.auto_download_snaps",
ConfigCategory.MEDIA_DOWNLOADER,
true
ConfigStateValue(false)
),
AUTO_DOWNLOAD_STORIES(
"property.auto_download_stories",
"description.auto_download_stories",
ConfigCategory.MEDIA_DOWNLOADER,
false
ConfigStateValue(false)
),
AUTO_DOWNLOAD_PUBLIC_STORIES(
"property.auto_download_public_stories",
"description.auto_download_public_stories",
ConfigCategory.MEDIA_DOWNLOADER,
false
ConfigStateValue(false)
),
AUTO_DOWNLOAD_SPOTLIGHT(
"property.auto_download_spotlight",
"description.auto_download_spotlight",
ConfigCategory.MEDIA_DOWNLOADER,
false
ConfigStateValue(false)
),
OVERLAY_MERGE(
"property.overlay_merge",
"description.overlay_merge",
ConfigCategory.MEDIA_DOWNLOADER,
true
ConfigStateValue(false)
),
DOWNLOAD_INCHAT_SNAPS(
"property.download_inchat_snaps",
"description.download_inchat_snaps",
ConfigCategory.MEDIA_DOWNLOADER,
true
ConfigStateValue(false)
),
ANTI_DOWNLOAD_BUTTON(
"property.anti_auto_download_button",
"description.anti_auto_download_button",
ConfigCategory.MEDIA_DOWNLOADER,
false
ConfigStateValue(false)
),
DISABLE_METRICS("property.disable_metrics", "description.disable_metrics", ConfigCategory.PRIVACY, true),
DISABLE_METRICS("property.disable_metrics", "description.disable_metrics", ConfigCategory.PRIVACY, ConfigStateValue(false)),
PREVENT_SCREENSHOT_NOTIFICATIONS(
"property.prevent_screenshot_notifications",
"description.prevent_screenshot_notifications",
ConfigCategory.PRIVACY,
true
ConfigStateValue(false)
),
PREVENT_STATUS_NOTIFICATIONS(
"property.prevent_status_notifications",
"description.prevent_status_notifications",
ConfigCategory.PRIVACY,
true
ConfigStateValue(false)
),
ANONYMOUS_STORY_VIEW(
"property.anonymous_story_view",
"description.anonymous_story_view",
ConfigCategory.PRIVACY,
false
ConfigStateValue(false)
),
HIDE_TYPING_NOTIFICATION(
"property.hide_typing_notification",
"description.hide_typing_notification",
ConfigCategory.PRIVACY,
false
ConfigStateValue(false)
),
MENU_SLOT_ID("property.menu_slot_id", "description.menu_slot_id", ConfigCategory.UI, 1),
MENU_SLOT_ID("property.menu_slot_id", "description.menu_slot_id", ConfigCategory.UI, ConfigIntegerValue(1)),
MESSAGE_PREVIEW_LENGTH(
"property.message_preview_length",
"description.message_preview_length",
ConfigCategory.UI,
20
ConfigIntegerValue(20)
),
EXTERNAL_MEDIA_AS_SNAP(
"property.external_media_as_snap",
"description.external_media_as_snap",
ConfigCategory.EXTRAS,
false
ConfigStateValue(false)
),
AUTO_SAVE("property.auto_save", "description.auto_save", ConfigCategory.EXTRAS, false),
ANTI_AUTO_SAVE("property.anti_auto_save", "description.anti_auto_save", ConfigCategory.EXTRAS, false),
SNAPCHAT_PLUS("property.snapchat_plus", "description.snapchat_plus", ConfigCategory.EXTRAS, false),
AUTO_SAVE("property.auto_save", "description.auto_save", ConfigCategory.EXTRAS, ConfigStateValue(false)),
ANTI_AUTO_SAVE("property.anti_auto_save", "description.anti_auto_save", ConfigCategory.EXTRAS, ConfigStateValue(false)),
SNAPCHAT_PLUS("property.snapchat_plus", "description.snapchat_plus", ConfigCategory.EXTRAS, ConfigStateValue(false)),
REMOVE_VOICE_RECORD_BUTTON(
"property.remove_voice_record_button",
"description.remove_voice_record_button",
ConfigCategory.TWEAKS,
false
ConfigStateValue(false)
),
REMOVE_STICKERS_BUTTON(
"property.remove_stickers_button",
"description.remove_stickers_button",
ConfigCategory.TWEAKS,
false
ConfigStateValue(false)
),
REMOVE_COGNAC_BUTTON(
"property.remove_cognac_button",
"description.remove_cognac_button",
ConfigCategory.TWEAKS,
false
ConfigStateValue(false)
),
REMOVE_CALL_BUTTONS(
"property.remove_call_buttons",
"description.remove_call_buttons",
ConfigCategory.TWEAKS,
false
ConfigStateValue(false)
),
DISABLE_SNAP_SPLITTING(
"property.disable_snap_splitting",
"description.disable_snap_splitting",
ConfigCategory.TWEAKS,
false
ConfigStateValue(false)
),
BLOCK_ADS("property.block_ads", "description.block_ads", ConfigCategory.TWEAKS, false),
BLOCK_ADS("property.block_ads", "description.block_ads", ConfigCategory.TWEAKS, ConfigStateValue(false)),
STREAK_EXPIRATION_INFO(
"property.streak_expiration_info",
"description.streakexpirationinfo",
ConfigCategory.TWEAKS,
false
ConfigStateValue(false)
),
NEW_MAP_UI("property.new_map_ui", "description.new_map_ui", ConfigCategory.TWEAKS, false),
NEW_MAP_UI("property.new_map_ui", "description.new_map_ui", ConfigCategory.TWEAKS, ConfigStateValue(false)),
USE_DOWNLOAD_MANAGER(
"property.use_download_manager",
"description.use_download_manager",
ConfigCategory.EXPERIMENTAL,
false
ConfigStateValue(false)
);
companion object {

View File

@@ -0,0 +1,7 @@
package me.rhunk.snapenhance.config
abstract class ConfigValue<T> {
abstract fun value(): T
abstract fun write(): String
abstract fun read(value: String)
}

View File

@@ -0,0 +1,17 @@
package me.rhunk.snapenhance.config.impl
import me.rhunk.snapenhance.config.ConfigValue
class ConfigIntegerValue(
var value: Int
) : ConfigValue<Int>() {
override fun value() = value
override fun write(): String {
return value.toString()
}
override fun read(value: String) {
this.value = value.toInt()
}
}

View File

@@ -0,0 +1,27 @@
package me.rhunk.snapenhance.config.impl
import me.rhunk.snapenhance.config.ConfigValue
class ConfigStateListValue(
private val keys: List<String>,
var states: MutableMap<String, Boolean> = mutableMapOf()
) : ConfigValue<Map<String, Boolean>>() {
override fun value() = states
fun value(key: String) = states[key] ?: false
override fun write(): String {
return keys.joinToString("|") { "$it:${states[it]}" }
}
override fun read(value: String) {
value.split("|").forEach {
val (key, state) = it.split(":")
states[key] = state.toBoolean()
}
}
override fun toString(): String {
return states.filter { it.value }.keys.joinToString(", ") { it }
}
}

View File

@@ -0,0 +1,17 @@
package me.rhunk.snapenhance.config.impl
import me.rhunk.snapenhance.config.ConfigValue
class ConfigStateValue(
var value: Boolean
) : ConfigValue<Boolean>() {
override fun value() = value
override fun write(): String {
return value.toString()
}
override fun read(value: String) {
this.value = value.toBoolean()
}
}

View File

@@ -0,0 +1,17 @@
package me.rhunk.snapenhance.config.impl
import me.rhunk.snapenhance.config.ConfigValue
class ConfigStringValue(
var value: String = ""
) : ConfigValue<String>() {
override fun value() = value
override fun write(): String {
return value
}
override fun read(value: String) {
this.value = value
}
}

View File

@@ -154,11 +154,22 @@ class Notifications : Feature("Notifications", loadParams = FeatureLoadParams.IN
}.clear()
}
private fun shouldIgnoreNotification(type: String): Boolean {
Logger.debug("notification type: $type")
val states = context.config.options(ConfigProperty.NOTIFICATION_FILTER)
states["snap"]?.let { if (type.endsWith("SNAP") && it) return true }
states["chat"]?.let { if (type.endsWith("CHAT") && it) return true }
states["typing"]?.let { if (type.endsWith("TYPING") && it) return true }
return false
}
override fun init() {
val fetchConversationWithMessagesCallback = context.mappings.getMappedClass("callbacks", "FetchConversationWithMessagesCallback")
Hooker.hook(notifyAsUserMethod, HookStage.BEFORE, { context.config.bool(ConfigProperty.SHOW_MESSAGE_CONTENT) }) {
val notificationData = NotificationData(it.argNullable(0), it.arg(1), it.arg(2), it.arg(3))
Hooker.hook(notifyAsUserMethod, HookStage.BEFORE) { param ->
val notificationData = NotificationData(param.argNullable(0), param.arg(1), param.arg(2), param.arg(3))
val extras: Bundle = notificationData.notification.extras.getBundle("system_notification_extras")?: return@hook
@@ -166,6 +177,13 @@ class Notifications : Feature("Notifications", loadParams = FeatureLoadParams.IN
val notificationType = extras.getString("notification_type") ?: return@hook
val conversationId = extras.getString("conversation_id") ?: return@hook
if (shouldIgnoreNotification(notificationType)) {
param.setResult(null)
return@hook
}
if (!context.config.bool(ConfigProperty.SHOW_MESSAGE_CONTENT_IN_NOTIFICATIONS)) return@hook
if (!notificationType.endsWith("CHAT") && !notificationType.endsWith("SNAP")) return@hook
val conversationManager: Any = context.feature(Messaging::class).conversationManager
@@ -181,7 +199,7 @@ class Notifications : Feature("Notifications", loadParams = FeatureLoadParams.IN
}.build()
fetchConversationWithMessagesMethod.invoke(conversationManager, SnapUUID.fromString(conversationId).instanceNonNull(), callback)
it.setResult(null)
param.setResult(null)
}
Hooker.hook(cancelAsUserMethod, HookStage.BEFORE) { param ->

View File

@@ -11,6 +11,10 @@ import android.widget.TextView
import me.rhunk.snapenhance.BuildConfig
import me.rhunk.snapenhance.Constants
import me.rhunk.snapenhance.config.ConfigProperty
import me.rhunk.snapenhance.config.impl.ConfigIntegerValue
import me.rhunk.snapenhance.config.impl.ConfigStateListValue
import me.rhunk.snapenhance.config.impl.ConfigStateValue
import me.rhunk.snapenhance.config.impl.ConfigStringValue
import me.rhunk.snapenhance.features.impl.ui.menus.AbstractMenu
import me.rhunk.snapenhance.features.impl.ui.menus.ViewAppearanceHelper
@@ -35,7 +39,7 @@ class SettingsMenu : AbstractMenu() {
val input = EditText(viewModel.context)
input.inputType = InputType.TYPE_CLASS_TEXT
input.setText(context.config.get(property).toString())
input.setText(property.valueContainer.value().toString())
builder.setView(input)
builder.setPositiveButton("OK") { _, _ ->
@@ -46,34 +50,26 @@ class SettingsMenu : AbstractMenu() {
builder.show()
}
val resultView: View = when (property.defaultValue) {
is String -> {
val resultView: View = when (property.valueContainer) {
is ConfigStringValue -> {
val textView = TextView(viewModel.context)
updateButtonText(textView, context.config.string(property))
updateButtonText(textView, property.valueContainer.value)
ViewAppearanceHelper.applyTheme(viewModel, textView)
textView.setOnClickListener {
textEditor { value ->
context.config.set(property, value)
property.valueContainer.value = value
updateButtonText(textView, value)
}
}
textView
}
is Number -> {
is ConfigIntegerValue -> {
val button = Button(viewModel.context)
updateButtonText(button, context.config.get(property).toString())
updateButtonText(button, property.valueContainer.value.toString())
button.setOnClickListener {
textEditor { value ->
runCatching {
context.config.set(property, when (property.defaultValue) {
is Int -> value.toInt()
is Double -> value.toDouble()
is Float -> value.toFloat()
is Long -> value.toLong()
is Short -> value.toShort()
is Byte -> value.toByte()
else -> throw IllegalArgumentException()
})
property.valueContainer.value = value.toInt()
updateButtonText(button, value)
}.onFailure {
context.shortToast("Invalid value")
@@ -83,16 +79,44 @@ class SettingsMenu : AbstractMenu() {
ViewAppearanceHelper.applyTheme(viewModel, button)
button
}
is Boolean -> {
is ConfigStateValue -> {
val switch = Switch(viewModel.context)
switch.text = context.translation.get(property.nameKey)
switch.isChecked = context.config.bool(property)
switch.isChecked = property.valueContainer.value
switch.setOnCheckedChangeListener { _, isChecked ->
context.config.set(property, isChecked)
property.valueContainer.value = isChecked
}
ViewAppearanceHelper.applyTheme(viewModel, switch)
switch
}
is ConfigStateListValue -> {
val button = Button(viewModel.context)
updateButtonText(button, property.valueContainer.toString())
button.setOnClickListener {_ ->
val builder = AlertDialog.Builder(viewModel.context)
builder.setTitle(context.translation.get(property.nameKey))
val sortedStates = property.valueContainer.states.toSortedMap()
builder.setMultiChoiceItems(
sortedStates.toSortedMap().map { context.translation.get("option." + property.nameKey + "." +it.key) }.toTypedArray(),
sortedStates.map { it.value }.toBooleanArray()
) { _, which, isChecked ->
sortedStates.keys.toList()[which].let { key ->
property.valueContainer.states[key] = isChecked
}
}
builder.setPositiveButton("OK") { _, _ ->
updateButtonText(button, property.valueContainer.toString())
}
builder.show()
}
ViewAppearanceHelper.applyTheme(viewModel, button)
button
}
else -> {
TextView(viewModel.context)
}

View File

@@ -10,16 +10,12 @@ import me.rhunk.snapenhance.manager.Manager
import java.nio.charset.StandardCharsets
class ConfigManager(
private val context: ModContext,
config: MutableMap<ConfigProperty, Any?> = mutableMapOf()
) : ConfigAccessor(config), Manager {
private val propertyList = ConfigProperty.sortedByCategory()
private val context: ModContext
) : ConfigAccessor(), Manager {
override fun init() {
//generate default config
propertyList.forEach { key ->
set(key, key.defaultValue)
ConfigProperty.sortedByCategory().forEach { key ->
set(key, key.valueContainer)
}
if (!context.bridgeClient.isFileExists(BridgeFileType.CONFIG)) {
@@ -44,16 +40,15 @@ class ConfigManager(
String(configContent, StandardCharsets.UTF_8),
JsonObject::class.java
)
propertyList.forEach { key ->
val value = context.gson.fromJson(configObject.get(key.name), key.defaultValue.javaClass) ?: key.defaultValue
set(key, value)
entries().forEach { (key, value) ->
value.read(configObject.get(key.name)?.asString ?: value.write())
}
}
fun writeConfig() {
val configObject = JsonObject()
propertyList.forEach { key ->
configObject.add(key.name, context.gson.toJsonTree(get(key)))
entries().forEach { (key, value) ->
configObject.addProperty(key.name, value.write())
}
context.bridgeClient.writeFile(
BridgeFileType.CONFIG,

View File

@@ -1,5 +1,6 @@
package me.rhunk.snapenhance.manager.impl
import com.google.gson.JsonObject
import com.google.gson.JsonParser
import me.rhunk.snapenhance.Logger
import me.rhunk.snapenhance.ModContext
@@ -22,15 +23,17 @@ class TranslationManager(
return
}
translations.asJsonObject.entrySet().forEach {
if (it.value.isJsonPrimitive) {
translationMap[it.key] = it.value.asString
}
if (!it.value.isJsonObject) return@forEach
it.value.asJsonObject.entrySet().forEach { entry ->
translationMap["${it.key}.${entry.key}"] = entry.value.asString
fun scanObject(jsonObject: JsonObject, prefix: String = "") {
jsonObject.entrySet().forEach {
if (it.value.isJsonPrimitive) {
translationMap["$prefix${it.key}"] = it.value.asString
}
if (!it.value.isJsonObject) return@forEach
scanObject(it.value.asJsonObject, "$prefix${it.key}.")
}
}
scanObject(translations)
}