feat: config export

- activity launcher helper
This commit is contained in:
rhunk
2023-08-23 00:44:05 +02:00
parent d12a5d689e
commit ae70b29180
13 changed files with 198 additions and 109 deletions

View File

@ -4,6 +4,8 @@ import android.app.Activity
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.documentfile.provider.DocumentFile
import coil.ImageLoader
import coil.decode.VideoFrameDecoder
@ -20,6 +22,7 @@ import me.rhunk.snapenhance.ui.manager.data.ModMappingsInfo
import me.rhunk.snapenhance.ui.manager.data.SnapchatAppInfo
import me.rhunk.snapenhance.ui.setup.Requirements
import me.rhunk.snapenhance.ui.setup.SetupActivity
import me.rhunk.snapenhance.ui.util.ActivityLauncherHelper
import java.lang.ref.WeakReference
class RemoteSideContext(
@ -28,9 +31,14 @@ class RemoteSideContext(
private var _activity: WeakReference<Activity>? = null
lateinit var bridgeService: BridgeService
lateinit var activityLauncherHelper: ActivityLauncherHelper
var activity: Activity?
get() = _activity?.get()
set(value) { _activity?.clear(); _activity = WeakReference(value) }
set(value) {
_activity?.clear();
_activity = WeakReference(value)
activityLauncherHelper = ActivityLauncherHelper(value as ComponentActivity)
}
val config = ModConfig()
val translation = LocaleWrapper()
@ -84,6 +92,20 @@ class RemoteSideContext(
} else null
)
fun longToast(message: Any) {
activity?.runOnUiThread {
Toast.makeText(activity, message.toString(), Toast.LENGTH_LONG).show()
}
Logger.debug(message.toString())
}
fun shortToast(message: Any) {
activity?.runOnUiThread {
Toast.makeText(activity, message.toString(), Toast.LENGTH_SHORT).show()
}
Logger.debug(message.toString())
}
fun checkForRequirements(overrideRequirements: Int? = null): Boolean {
var requirements = overrideRequirements ?: 0

View File

@ -1,6 +1,6 @@
package me.rhunk.snapenhance.ui.manager.sections.features
import androidx.activity.ComponentActivity
import android.net.Uri
import androidx.compose.animation.AnimatedContentTransitionScope
import androidx.compose.animation.core.tween
import androidx.compose.foundation.background
@ -27,12 +27,14 @@ import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.FolderOpen
import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material.icons.filled.OpenInNew
import androidx.compose.material.icons.filled.Rule
import androidx.compose.material.icons.filled.Search
import androidx.compose.material.icons.rounded.Save
import androidx.compose.material3.BottomSheetScaffoldState
import androidx.compose.material3.Card
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.FilledIconButton
import androidx.compose.material3.FloatingActionButton
@ -78,7 +80,9 @@ import me.rhunk.snapenhance.core.config.PropertyKey
import me.rhunk.snapenhance.core.config.PropertyPair
import me.rhunk.snapenhance.core.config.PropertyValue
import me.rhunk.snapenhance.ui.manager.Section
import me.rhunk.snapenhance.ui.util.ChooseFolderHelper
import me.rhunk.snapenhance.ui.util.chooseFolder
import me.rhunk.snapenhance.ui.util.openFile
import me.rhunk.snapenhance.ui.util.saveFile
@OptIn(ExperimentalMaterial3Api::class)
class FeaturesSection : Section() {
@ -90,8 +94,6 @@ class FeaturesSection : Section() {
const val SEARCH_FEATURE_ROUTE = "search_feature/{keyword}"
}
private lateinit var openFolderCallback: (uri: String) -> Unit
private lateinit var openFolderLauncher: () -> Unit
private val featuresRouteName by lazy { context.translation["manager.routes.features"] }
@ -122,32 +124,13 @@ class FeaturesSection : Section() {
properties
}
override fun init() {
openFolderLauncher = ChooseFolderHelper.createChooseFolder(context.activity!! as ComponentActivity) {
openFolderCallback(it)
}
}
override fun canGoBack() = sectionTopBarName() != featuresRouteName
override fun sectionTopBarName(): String {
navController.currentBackStackEntry?.arguments?.getString("name")?.let { routeName ->
val currentContainerPair = allContainers[routeName]
val propertyTree = run {
var key = currentContainerPair?.key
val tree = mutableListOf<String>()
while (key != null) {
tree.add(key.propertyTranslationPath())
key = key.parentKey
}
tree
}
val translatedKey = propertyTree.reversed().joinToString(" > ") {
context.translation["$it.name"]
}
return "$featuresRouteName > $translatedKey"
return context.translation["${currentContainerPair?.key?.propertyTranslationPath()}.name"]
}
return featuresRouteName
}
@ -203,10 +186,9 @@ class FeaturesSection : Section() {
if (property.key.params.flags.contains(ConfigFlag.FOLDER)) {
IconButton(onClick = registerClickCallback {
openFolderCallback = { uri ->
context.activityLauncherHelper.chooseFolder { uri ->
propertyValue.setAny(uri)
}
openFolderLauncher()
}.let { { it.invoke(true) } }) {
Icon(Icons.Filled.FolderOpen, contentDescription = null)
}
@ -460,6 +442,63 @@ class FeaturesSection : Section() {
contentDescription = null
)
}
if (showSearchBar) return
var showExportDropdownMenu by remember { mutableStateOf(false) }
val actions = remember {
mapOf(
"Export" to {
context.activityLauncherHelper.saveFile("config.json", "application/json") { uri ->
context.androidContext.contentResolver.openOutputStream(Uri.parse(uri))?.use {
context.config.writeConfig()
context.config.exportToString().byteInputStream().copyTo(it)
context.shortToast("Config exported successfully!")
}
}
},
"Import" to {
context.activityLauncherHelper.openFile("application/json") { uri ->
context.androidContext.contentResolver.openInputStream(Uri.parse(uri))?.use {
runCatching {
context.config.loadFromString(it.readBytes().toString(Charsets.UTF_8))
}.onFailure {
context.longToast("Failed to import config ${it.message}")
return@use
}
context.shortToast("Config successfully loaded!")
}
}
},
"Reset" to {
context.config.reset()
context.shortToast("Config successfully reset!")
}
)
}
IconButton(onClick = { showExportDropdownMenu = !showExportDropdownMenu}) {
Icon(
imageVector = Icons.Filled.MoreVert,
contentDescription = null
)
}
if (showExportDropdownMenu) {
DropdownMenu(expanded = showExportDropdownMenu, onDismissRequest = { showExportDropdownMenu = false }) {
actions.forEach { (name, action) ->
DropdownMenuItem(
text = {
Text(text = name)
},
onClick = {
action()
showExportDropdownMenu = false
}
)
}
}
}
}
@Composable

View File

@ -1,6 +1,5 @@
package me.rhunk.snapenhance.ui.setup.screens.impl
import androidx.activity.ComponentActivity
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.material3.Button
@ -10,12 +9,11 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import me.rhunk.snapenhance.Logger
import me.rhunk.snapenhance.ui.setup.screens.SetupScreen
import me.rhunk.snapenhance.ui.util.ChooseFolderHelper
import me.rhunk.snapenhance.ui.util.ObservableMutableState
import me.rhunk.snapenhance.ui.util.chooseFolder
class SaveFolderScreen : SetupScreen() {
private lateinit var saveFolder: ObservableMutableState<String>
private lateinit var openFolderLauncher: () -> Unit
override fun init() {
saveFolder = ObservableMutableState(
@ -29,9 +27,6 @@ class SaveFolderScreen : SetupScreen() {
}
}
)
openFolderLauncher = ChooseFolderHelper.createChooseFolder(context.activity as ComponentActivity) { uri ->
saveFolder.value = uri
}
}
@Composable
@ -39,7 +34,9 @@ class SaveFolderScreen : SetupScreen() {
DialogText(text = context.translation["setup.dialogs.save_folder"])
Spacer(modifier = Modifier.height(16.dp))
Button(onClick = {
openFolderLauncher()
context.activityLauncherHelper.chooseFolder {
saveFolder.value = it
}
}) {
Text(text = context.translation["setup.dialogs.select_save_folder_button"])
}

View File

@ -0,0 +1,81 @@
package me.rhunk.snapenhance.ui.util
import android.content.Intent
import androidx.activity.ComponentActivity
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import me.rhunk.snapenhance.Logger
class ActivityLauncherHelper(
val activity: ComponentActivity
) {
private var callback: ((Intent) -> Unit)? = null
private var activityResultLauncher: ActivityResultLauncher<Intent> =
activity.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
if (result.resultCode == ComponentActivity.RESULT_OK) {
runCatching {
callback?.let { it(result.data!!) }
}.onFailure {
Logger.error("Failed to process activity result", it)
}
}
callback = null
}
fun launch(intent: Intent, callback: (Intent) -> Unit) {
if (this.callback != null) {
throw IllegalStateException("Already launching an activity")
}
this.callback = callback
activityResultLauncher.launch(intent)
}
}
fun ActivityLauncherHelper.chooseFolder(callback: (uri: String) -> Unit) {
launch(
Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
) {
val uri = it.data ?: return@launch
val value = uri.toString()
this.activity.contentResolver.takePersistableUriPermission(
uri,
Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
)
callback(value)
}
}
fun ActivityLauncherHelper.saveFile(name: String, type: String = "*/*", callback: (uri: String) -> Unit) {
launch(
Intent(Intent.ACTION_CREATE_DOCUMENT)
.addCategory(Intent.CATEGORY_OPENABLE)
.setType(type)
.putExtra(Intent.EXTRA_TITLE, name)
.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
) {
val uri = it.data ?: return@launch
val value = uri.toString()
this.activity.contentResolver.takePersistableUriPermission(
uri,
Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
)
callback(value)
}
}
fun ActivityLauncherHelper.openFile(type: String = "*/*", callback: (uri: String) -> Unit) {
launch(
Intent(Intent.ACTION_OPEN_DOCUMENT)
.addCategory(Intent.CATEGORY_OPENABLE)
.setType(type)
.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
) {
val uri = it.data ?: return@launch
val value = uri.toString()
this.activity.contentResolver.takePersistableUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
callback(value)
}
}

View File

@ -1,26 +0,0 @@
package me.rhunk.snapenhance.ui.util
import android.app.Activity
import android.content.Intent
import androidx.activity.ComponentActivity
import androidx.activity.result.contract.ActivityResultContracts
object ChooseFolderHelper {
fun createChooseFolder(activity: ComponentActivity, callback: (uri: String) -> Unit): () -> Unit {
val activityResultLauncher = 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)
callback(value)
}
return {
activityResultLauncher.launch(
Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
)
}
}
}

View File

@ -191,10 +191,6 @@
"name": "Anonymous Story Viewing",
"description": "Prevents anyone from knowing you've seen their story"
},
"prevent_read_receipts": {
"name": "Prevent Read Receipts",
"description": "Prevent anyone from knowing you've opened their Snaps/Chats"
},
"hide_bitmoji_presence": {
"name": "Hide Bitmoji Presence",
"description": "Hides your Bitmoji presence from the chat"

View File

@ -8,11 +8,8 @@ enum class BridgeFileType(val value: Int, val fileName: String, val displayName:
CONFIG(0, "config.json", "Config"),
MAPPINGS(1, "mappings.json", "Mappings"),
MESSAGE_LOGGER_DATABASE(2, "message_logger.db", "Message Logger",true),
STEALTH(3, "stealth.txt", "Stealth Conversations"),
ANTI_AUTO_DOWNLOAD(4, "anti_auto_download.txt", "Anti Auto Download"),
ANTI_AUTO_SAVE(5, "anti_auto_save.txt", "Anti Auto Save"),
AUTO_UPDATER_TIMESTAMP(6, "auto_updater_timestamp.txt", "Auto Updater Timestamp"),
PINNED_CONVERSATIONS(7, "pinned_conversations.txt", "Pinned Conversations");
AUTO_UPDATER_TIMESTAMP(3, "auto_updater_timestamp.txt", "Auto Updater Timestamp"),
PINNED_CONVERSATIONS(4, "pinned_conversations.txt", "Pinned Conversations");
fun resolve(context: Context): File = if (isDatabase) {
context.getDatabasePath(fileName)

View File

@ -19,10 +19,11 @@ class ModConfig {
private val file = FileLoaderWrapper(BridgeFileType.CONFIG, "{}".toByteArray(Charsets.UTF_8))
var wasPresent by Delegates.notNull<Boolean>()
val root = RootConfig()
lateinit var root: RootConfig
operator fun getValue(thisRef: Any?, property: Any?) = root
private fun load() {
root = RootConfig()
wasPresent = file.isFileExists()
if (!file.isFileExists()) {
writeConfig()
@ -44,10 +45,26 @@ class ModConfig {
root.fromJson(configObject)
}
fun writeConfig() {
fun exportToString(): String {
val configObject = root.toJson()
configObject.addProperty("_locale", locale)
file.write(configObject.toString().toByteArray(Charsets.UTF_8))
return configObject.toString()
}
fun reset() {
root = RootConfig()
writeConfig()
}
fun writeConfig() {
file.write(exportToString().toByteArray(Charsets.UTF_8))
}
fun loadFromString(string: String) {
val configObject = gson.fromJson(string, JsonObject::class.java)
locale = configObject.get("_locale")?.asString ?: LocaleWrapper.DEFAULT_LOCALE
root.fromJson(configObject)
writeConfig()
}
fun loadFromContext(context: Context) {

View File

@ -6,12 +6,9 @@ import me.rhunk.snapenhance.data.NotificationType
class MessagingTweaks : ConfigContainer() {
val anonymousStoryViewing = boolean("anonymous_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") { addNotices(FeatureNotice.MAY_CAUSE_CRASHES) }
val autoSaveMessagesInConversations = multiple("auto_save_messages_in_conversations",
"CHAT",
"SNAP",
@ -19,7 +16,8 @@ class MessagingTweaks : ConfigContainer() {
"EXTERNAL_MEDIA",
"STICKER"
)
val preventMessageSending = multiple("prevent_message_sending", *NotificationType.getOutgoingValues().map { it.key }.toTypedArray())
val messageLogger = boolean("message_logger") { addNotices(FeatureNotice.MAY_CAUSE_CRASHES) }
val galleryMediaSendOverride = boolean("gallery_media_send_override")
val messagePreviewLength = integer("message_preview_length", defaultValue = 20)
}

View File

@ -61,12 +61,3 @@ data class MessagingFriendInfo(
val bitmojiId: String?,
val selfieId: String?
) : SerializableDataObject()
data class MessagingRule(
val id: Int,
val type: MessagingRuleType,
val socialScope: SocialScope,
val targetUuid: String,
//val mode: Mode?,
) : SerializableDataObject()

View File

@ -9,9 +9,7 @@ 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 (preventReadReceipts) return@hook true
context.feature(StealthMode::class).canUseRule(it.toString())
}

View File

@ -1,19 +0,0 @@
package me.rhunk.snapenhance.features.impl.tweaks
import me.rhunk.snapenhance.bridge.types.BridgeFileType
import me.rhunk.snapenhance.features.BridgeFileFeature
import me.rhunk.snapenhance.features.FeatureLoadParams
class AntiAutoSave : BridgeFileFeature("AntiAutoSave", BridgeFileType.ANTI_AUTO_SAVE, loadParams = FeatureLoadParams.ACTIVITY_CREATE_SYNC) {
override fun onActivityCreate() {
readFile()
}
fun setConversationIgnored(userId: String, state: Boolean) {
setState(userId.hashCode().toLong().toString(16), state)
}
fun isConversationIgnored(userId: String): Boolean {
return exists(userId.hashCode().toLong().toString(16))
}
}

View File

@ -21,7 +21,6 @@ import me.rhunk.snapenhance.features.impl.spying.AnonymousStoryViewing
import me.rhunk.snapenhance.features.impl.spying.MessageLogger
import me.rhunk.snapenhance.features.impl.spying.PreventReadReceipts
import me.rhunk.snapenhance.features.impl.spying.StealthMode
import me.rhunk.snapenhance.features.impl.tweaks.AntiAutoSave
import me.rhunk.snapenhance.features.impl.tweaks.AutoSave
import me.rhunk.snapenhance.features.impl.tweaks.CameraTweaks
import me.rhunk.snapenhance.features.impl.tweaks.DisableVideoLengthRestriction
@ -76,7 +75,6 @@ class FeatureManager(private val context: ModContext) : Manager {
register(UITweaks::class)
register(ConfigurationOverride::class)
register(GalleryMediaSendOverride::class)
register(AntiAutoSave::class)
register(UnlimitedSnapViewTime::class)
register(DisableVideoLengthRestriction::class)
register(MediaQualityLevelOverride::class)