mirror of
https://github.com/rhunk/SnapEnhance.git
synced 2025-05-16 22:27:09 +02:00
feat: export memories
This commit is contained in:
parent
04b70431c7
commit
dd755af5be
1
app/proguard-rules.pro
vendored
1
app/proguard-rules.pro
vendored
@ -6,5 +6,6 @@
|
||||
-keep class org.jf.dexlib2.** { *; }
|
||||
-keep class org.mozilla.javascript.** { *; }
|
||||
-keep class androidx.compose.material.icons.** { *; }
|
||||
-keep class androidx.compose.material3.R$* { *; }
|
||||
-keep class androidx.navigation.** { *; }
|
||||
-keep class me.rhunk.snapenhance.** { *; }
|
||||
|
@ -128,6 +128,7 @@
|
||||
"open_map": "Choose location on map",
|
||||
"check_for_updates": "Check for updates",
|
||||
"export_chat_messages": "Export Chat Messages",
|
||||
"export_memories": "Export Memories",
|
||||
"bulk_messaging_action": "Bulk Messaging Action"
|
||||
},
|
||||
|
||||
@ -1075,5 +1076,21 @@
|
||||
|
||||
"suspend_location_updates": {
|
||||
"switch_text": "Suspend Location Updates"
|
||||
},
|
||||
"material3_strings": {
|
||||
"date_range_input_title": "",
|
||||
"date_range_picker_start_headline": "From",
|
||||
"date_range_picker_end_headline": "To",
|
||||
"date_range_picker_title": "Select date range",
|
||||
"date_picker_switch_to_calendar_mode": "Calendar",
|
||||
"date_picker_switch_to_input_mode": "Input",
|
||||
"date_range_picker_scroll_to_previous_month": "Previous month",
|
||||
"date_range_picker_scroll_to_next_month": "Next month",
|
||||
"date_picker_today_description": "Today",
|
||||
"date_range_picker_day_in_range": "Selected",
|
||||
"date_input_invalid_for_pattern": "Invalid date",
|
||||
"date_input_invalid_year_range": "Invalid year",
|
||||
"date_input_invalid_not_allowed": "Invalid date",
|
||||
"date_range_input_invalid_range_input": "Invalid date range"
|
||||
}
|
||||
}
|
@ -9,6 +9,7 @@ enum class EnumAction(
|
||||
) {
|
||||
CLEAN_CACHE("clean_snapchat_cache", exitOnFinish = true),
|
||||
EXPORT_CHAT_MESSAGES("export_chat_messages"),
|
||||
EXPORT_MEMORIES("export_memories"),
|
||||
BULK_MESSAGING_ACTION("bulk_messaging_action");
|
||||
|
||||
companion object {
|
||||
|
@ -3,6 +3,7 @@ package me.rhunk.snapenhance.core
|
||||
import android.app.Activity
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import android.content.res.Resources
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
@ -20,8 +21,10 @@ import me.rhunk.snapenhance.core.data.SnapClassCache
|
||||
import me.rhunk.snapenhance.core.event.events.impl.NativeUnaryCallEvent
|
||||
import me.rhunk.snapenhance.core.event.events.impl.SnapWidgetBroadcastReceiveEvent
|
||||
import me.rhunk.snapenhance.core.util.LSPatchUpdater
|
||||
import me.rhunk.snapenhance.core.util.hook.HookAdapter
|
||||
import me.rhunk.snapenhance.core.util.hook.HookStage
|
||||
import me.rhunk.snapenhance.core.util.hook.hook
|
||||
import java.lang.reflect.Modifier
|
||||
import kotlin.system.measureTimeMillis
|
||||
|
||||
|
||||
@ -36,11 +39,11 @@ class SnapEnhance {
|
||||
private lateinit var appContext: ModContext
|
||||
private var isBridgeInitialized = false
|
||||
|
||||
private fun hookMainActivity(methodName: String, stage: HookStage = HookStage.AFTER, block: Activity.() -> Unit) {
|
||||
private fun hookMainActivity(methodName: String, stage: HookStage = HookStage.AFTER, block: Activity.(param: HookAdapter) -> Unit) {
|
||||
Activity::class.java.hook(methodName, stage, { isBridgeInitialized }) { param ->
|
||||
val activity = param.thisObject() as Activity
|
||||
if (!activity.packageName.equals(Constants.SNAPCHAT_PACKAGE_NAME)) return@hook
|
||||
block(activity)
|
||||
block(activity, param)
|
||||
}
|
||||
}
|
||||
|
||||
@ -90,6 +93,8 @@ class SnapEnhance {
|
||||
appContext.mainActivity = this
|
||||
if (isMainActivityNotNull || !appContext.mappings.isMappingsLoaded()) return@hookMainActivity
|
||||
onActivityCreate()
|
||||
jetpackComposeResourceHook()
|
||||
appContext.actionManager.onNewIntent(intent)
|
||||
}
|
||||
|
||||
hookMainActivity("onPause") {
|
||||
@ -97,6 +102,10 @@ class SnapEnhance {
|
||||
appContext.isMainActivityPaused = true
|
||||
}
|
||||
|
||||
hookMainActivity("onNewIntent") { param ->
|
||||
appContext.actionManager.onNewIntent(param.argNullable(0))
|
||||
}
|
||||
|
||||
var activityWasResumed = false
|
||||
//we need to reload the config when the app is resumed
|
||||
//FIXME: called twice at first launch
|
||||
@ -107,7 +116,6 @@ class SnapEnhance {
|
||||
return@hookMainActivity
|
||||
}
|
||||
|
||||
appContext.actionManager.onNewIntent(this.intent)
|
||||
appContext.reloadConfig()
|
||||
syncRemote()
|
||||
}
|
||||
@ -263,4 +271,22 @@ class SnapEnhance {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun jetpackComposeResourceHook() {
|
||||
val material3RString = try {
|
||||
Class.forName("androidx.compose.material3.R\$string")
|
||||
} catch (e: ClassNotFoundException) {
|
||||
return
|
||||
}
|
||||
|
||||
val stringResources = material3RString.fields.filter {
|
||||
Modifier.isStatic(it.modifiers) && it.type == Int::class.javaPrimitiveType
|
||||
}.associate { it.getInt(null) to it.name }
|
||||
|
||||
Resources::class.java.getMethod("getString", Int::class.javaPrimitiveType).hook(HookStage.BEFORE) { param ->
|
||||
val key = param.arg<Int>(0)
|
||||
val name = stringResources[key] ?: return@hook
|
||||
param.setResult(appContext.translation.getOrNull("material3_strings.$name") ?: return@hook)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,384 @@
|
||||
package me.rhunk.snapenhance.core.action.impl
|
||||
|
||||
import android.database.sqlite.SQLiteDatabase
|
||||
import android.database.sqlite.SQLiteDatabase.OpenParams
|
||||
import android.os.Environment
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import kotlinx.coroutines.*
|
||||
import me.rhunk.snapenhance.common.data.FileType
|
||||
import me.rhunk.snapenhance.common.ui.createComposeAlertDialog
|
||||
import me.rhunk.snapenhance.common.util.ktx.getLongOrNull
|
||||
import me.rhunk.snapenhance.common.util.ktx.getStringOrNull
|
||||
import me.rhunk.snapenhance.core.action.AbstractAction
|
||||
import okhttp3.OkHttpClient
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.nio.file.attribute.FileTime
|
||||
import java.time.Instant
|
||||
import java.time.OffsetDateTime
|
||||
import java.util.zip.ZipEntry
|
||||
import java.util.zip.ZipOutputStream
|
||||
import javax.crypto.Cipher
|
||||
import javax.crypto.CipherInputStream
|
||||
import javax.crypto.spec.IvParameterSpec
|
||||
import javax.crypto.spec.SecretKeySpec
|
||||
import kotlin.io.encoding.Base64
|
||||
import kotlin.io.encoding.ExperimentalEncodingApi
|
||||
import kotlin.math.absoluteValue
|
||||
|
||||
class ExportMemories : AbstractAction() {
|
||||
data class TimeRange(
|
||||
val start: Long?,
|
||||
val end: Long?,
|
||||
)
|
||||
|
||||
data class MemoriesEntry(
|
||||
val storyTitle: String,
|
||||
val createTime: Long,
|
||||
val mediaKey: String?,
|
||||
val mediaIv: String?,
|
||||
val downloadUrl: String
|
||||
) {
|
||||
val folderName: String
|
||||
get() = storyTitle.replace(Regex("[^a-zA-Z0-9\\s]"), "").trim().replace(Regex("\\s+"), "_")
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class, ExperimentalEncodingApi::class)
|
||||
private suspend fun exportMemories(
|
||||
scope: CoroutineScope = context.coroutineScope,
|
||||
database: SQLiteDatabase,
|
||||
timeRange: TimeRange?,
|
||||
includeMEO: Boolean,
|
||||
folders: Boolean,
|
||||
progress: (Int, Int) -> Unit
|
||||
) {
|
||||
val downloadContext = Dispatchers.IO.limitedParallelism(10)
|
||||
val writeToZipContext = Dispatchers.IO.limitedParallelism(1)
|
||||
val outputZip = File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS), "memories_" + System.currentTimeMillis() + ".zip").also {
|
||||
if (it.exists()) it.delete()
|
||||
}
|
||||
val okHttpClient = OkHttpClient.Builder().build()
|
||||
val outputZipFile = withContext(Dispatchers.IO) {
|
||||
ZipOutputStream(FileOutputStream(outputZip)).apply {
|
||||
setComment("Exported from SnapEnhance")
|
||||
setMethod(ZipOutputStream.DEFLATED)
|
||||
}
|
||||
}
|
||||
var totalCount = 0
|
||||
var currentCount = 0
|
||||
var failed = 0
|
||||
|
||||
fun updateProgress() {
|
||||
progress((currentCount.toFloat() / totalCount.toFloat() * 100f).toInt(), failed)
|
||||
}
|
||||
|
||||
val jobs = mutableListOf<Job>()
|
||||
|
||||
val meoMasterKeyPair = if (includeMEO) {
|
||||
runCatching {
|
||||
database.rawQuery("SELECT * FROM memories_meo_confidential", null).use { cursor ->
|
||||
if (cursor.moveToNext()) {
|
||||
cursor.getStringOrNull("master_key")!!.trim() to cursor.getStringOrNull("master_key_iv")!!.trim()
|
||||
} else null
|
||||
}
|
||||
}.getOrNull()
|
||||
} else null
|
||||
|
||||
database.rawQuery("SELECT memories_entry.title as story_title, memories_snap.create_time, " +
|
||||
"memories_snap.media_key, memories_snap.media_iv, memories_snap.encrypted_media_key, memories_snap.encrypted_media_iv, " +
|
||||
"memories_media.download_url FROM memories_snap " +
|
||||
"INNER JOIN memories_entry ON memories_snap.memories_entry_id = memories_entry._id " +
|
||||
"INNER JOIN memories_media ON memories_snap.media_id = memories_media._id " +
|
||||
"WHERE memories_snap.create_time >= ? AND memories_snap.create_time <= ? " +
|
||||
"ORDER BY memories_snap.create_time ASC", arrayOf(timeRange?.start?.toString() ?: "-1", timeRange?.end?.toString() ?: Long.MAX_VALUE.toString())
|
||||
).use { cursor ->
|
||||
while (cursor.moveToNext()) {
|
||||
val encryptedMediaKey = cursor.getStringOrNull("encrypted_media_key")?.trim()
|
||||
val encryptedMediaIv = cursor.getStringOrNull("encrypted_media_iv")?.trim()
|
||||
var mediaKey = cursor.getStringOrNull("media_key")?.trim()
|
||||
var mediaIv = cursor.getStringOrNull("media_iv")?.trim()
|
||||
|
||||
if (!includeMEO && encryptedMediaKey != null && encryptedMediaIv != null) continue
|
||||
|
||||
meoMasterKeyPair.takeIf { encryptedMediaKey != null && encryptedMediaIv != null }?.let { keyPair ->
|
||||
val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding")
|
||||
runCatching {
|
||||
cipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(Base64.decode(keyPair.first), "AES"), IvParameterSpec(Base64.decode(keyPair.second)))
|
||||
mediaKey = Base64.encode(cipher.doFinal(Base64.decode(encryptedMediaKey ?: return@let)))
|
||||
mediaIv = Base64.encode(cipher.doFinal(Base64.decode(encryptedMediaIv ?: return@let)))
|
||||
context.log.verbose("decrypted meo $mediaKey/$mediaIv")
|
||||
}.onFailure {
|
||||
context.log.error("failed to decrypt meo", it)
|
||||
}
|
||||
}
|
||||
|
||||
if (mediaKey == null || mediaIv == null) {
|
||||
context.log.error("missing media key or iv for ${cursor.getStringOrNull("download_url")}")
|
||||
failed++
|
||||
updateProgress()
|
||||
continue
|
||||
}
|
||||
|
||||
val entry = MemoriesEntry(
|
||||
storyTitle = cursor.getStringOrNull("story_title") ?: "unknown",
|
||||
createTime = cursor.getLongOrNull("create_time") ?: -1L,
|
||||
mediaKey = mediaKey,
|
||||
mediaIv = mediaIv,
|
||||
downloadUrl = cursor.getStringOrNull("download_url") ?: continue
|
||||
)
|
||||
|
||||
totalCount++
|
||||
|
||||
scope.launch(downloadContext) {
|
||||
var downloadedFile = File.createTempFile("memories", ".tmp", context.androidContext.cacheDir)
|
||||
|
||||
runCatching {
|
||||
okHttpClient.newCall(
|
||||
okhttp3.Request.Builder()
|
||||
.url(entry.downloadUrl)
|
||||
.build()
|
||||
).execute().use { response ->
|
||||
val inputStream = response.body.byteStream().let {
|
||||
if (entry.mediaKey != null && entry.mediaIv != null) {
|
||||
val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding")
|
||||
cipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(Base64.decode(entry.mediaKey), "AES"), IvParameterSpec(Base64.decode(entry.mediaIv)))
|
||||
CipherInputStream(it, cipher)
|
||||
} else it
|
||||
}
|
||||
|
||||
downloadedFile.outputStream().use { outputStream ->
|
||||
inputStream.use { inputStream ->
|
||||
inputStream.copyTo(outputStream)
|
||||
}
|
||||
}
|
||||
|
||||
val fileType = FileType.fromFile(downloadedFile)
|
||||
|
||||
downloadedFile = File(
|
||||
downloadedFile.parentFile,
|
||||
"${entry.createTime}-${entry.downloadUrl.hashCode().absoluteValue.toString(16)}.${fileType.fileExtension}"
|
||||
).also {
|
||||
downloadedFile.renameTo(it)
|
||||
}
|
||||
|
||||
withContext(writeToZipContext) {
|
||||
val zipEntry = ZipEntry("${if (folders) entry.folderName + "/" else entry.folderName}${downloadedFile.name}")
|
||||
FileTime.fromMillis(entry.createTime).let {
|
||||
zipEntry.lastModifiedTime = it
|
||||
zipEntry.lastAccessTime = it
|
||||
zipEntry.creationTime = it
|
||||
}
|
||||
outputZipFile.apply {
|
||||
putNextEntry(zipEntry)
|
||||
downloadedFile.inputStream().use { it.copyTo(outputZipFile) }
|
||||
closeEntry()
|
||||
flush()
|
||||
}
|
||||
currentCount++
|
||||
updateProgress()
|
||||
}
|
||||
}
|
||||
}.onFailure {
|
||||
context.log.error("failed to download ${entry.downloadUrl}", it)
|
||||
failed++
|
||||
updateProgress()
|
||||
}
|
||||
downloadedFile.delete()
|
||||
}.also { jobs.add(it) }
|
||||
}
|
||||
}
|
||||
|
||||
jobs.joinAll()
|
||||
withContext(Dispatchers.IO) {
|
||||
outputZipFile.close()
|
||||
}
|
||||
context.longToast("Exported to ${outputZip.absolutePath}")
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun ExporterDialog(database: SQLiteDatabase, onDismiss: () -> Unit) {
|
||||
var exportJob by remember { mutableStateOf(null as Job?) }
|
||||
var exportFinished by remember { mutableStateOf(false) }
|
||||
var exportProgress by remember { mutableStateOf(Pair(0, 0)) } // progress, failed
|
||||
|
||||
var dateRangeFilter by remember { mutableStateOf(false) }
|
||||
var sortByFolder by remember { mutableStateOf(false) }
|
||||
var includeMEO by remember { mutableStateOf(false) }
|
||||
val dateRangePickerState = rememberDateRangePickerState(
|
||||
initialSelectedStartDateMillis = OffsetDateTime.now().minusDays(8).toInstant().toEpochMilli(),
|
||||
initialSelectedEndDateMillis = Instant.now().toEpochMilli(),
|
||||
initialDisplayMode = DisplayMode.Input
|
||||
)
|
||||
|
||||
val totalCount = remember(dateRangePickerState.selectedStartDateMillis, dateRangePickerState.selectedEndDateMillis, dateRangeFilter) {
|
||||
val timeRange = dateRangePickerState.takeIf { dateRangeFilter }?.let {
|
||||
TimeRange(it.selectedStartDateMillis, it.selectedEndDateMillis)
|
||||
}
|
||||
|
||||
database.rawQuery("SELECT COUNT(*) FROM memories_snap WHERE create_time >= ? AND create_time <= ? ", arrayOf(timeRange?.start?.toString() ?: "-1", timeRange?.end?.toString() ?: Long.MAX_VALUE.toString())).use {
|
||||
it.moveToFirst()
|
||||
it.getInt(0)
|
||||
}
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier.padding(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
Text("Export memories", modifier = Modifier.fillMaxWidth(), textAlign = TextAlign.Center, fontSize = 20.sp)
|
||||
|
||||
if (exportJob != null) {
|
||||
Text(text = "Exporting memories... (${exportProgress.second} failed)", modifier = Modifier.fillMaxWidth(), textAlign = TextAlign.Center)
|
||||
LinearProgressIndicator(progress = exportProgress.first / 100f, Modifier.fillMaxWidth())
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceEvenly,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Button(onClick = {
|
||||
exportJob?.cancel()
|
||||
exportJob = null
|
||||
onDismiss()
|
||||
}) {
|
||||
Text("Quit")
|
||||
}
|
||||
if (exportFinished) {
|
||||
Button(onClick = {
|
||||
exportJob = null
|
||||
onDismiss()
|
||||
}) {
|
||||
Text("Done")
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Text("Total memories: $totalCount", modifier = Modifier.fillMaxWidth(), textAlign = TextAlign.Center)
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(5.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
var dateRangeDialog by remember { mutableStateOf(false) }
|
||||
Checkbox(checked = dateRangeFilter, onCheckedChange = { dateRangeFilter = it })
|
||||
Text("Date Range", modifier = Modifier.weight(1f))
|
||||
Button(onClick = { dateRangeDialog = true }, enabled = dateRangeFilter) {
|
||||
Text("Select")
|
||||
}
|
||||
|
||||
if (dateRangeDialog) {
|
||||
DatePickerDialog(onDismissRequest = {
|
||||
dateRangeDialog = false
|
||||
}, confirmButton = {}) {
|
||||
DateRangePicker(
|
||||
state = dateRangePickerState,
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(8.dp),
|
||||
horizontalArrangement = Arrangement.End
|
||||
) {
|
||||
Button(onClick = {
|
||||
dateRangeDialog = false
|
||||
}) {
|
||||
Text("OK")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(5.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Checkbox(checked = sortByFolder, onCheckedChange = { sortByFolder = it })
|
||||
Text("Sort by folder", modifier = Modifier.weight(1f))
|
||||
}
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(5.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Checkbox(checked = includeMEO, onCheckedChange = { includeMEO = it })
|
||||
Text("Include My Eyes Only", modifier = Modifier.weight(1f))
|
||||
}
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceEvenly,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Button(onClick = onDismiss) {
|
||||
Text("Cancel")
|
||||
}
|
||||
Button(onClick = {
|
||||
context.coroutineScope.launch {
|
||||
exportMemories(
|
||||
scope = this,
|
||||
database = database,
|
||||
timeRange = dateRangePickerState.takeIf { dateRangeFilter }?.let {
|
||||
TimeRange(it.selectedStartDateMillis, it.selectedEndDateMillis)
|
||||
},
|
||||
folders = sortByFolder,
|
||||
includeMEO = includeMEO,
|
||||
) { progress, failed ->
|
||||
exportProgress = Pair(progress, failed)
|
||||
}
|
||||
}.also { exportJob = it }.invokeOnCompletion {
|
||||
exportFinished = true
|
||||
}
|
||||
}) {
|
||||
Text("Export")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
override fun run() {
|
||||
context.coroutineScope.launch(Dispatchers.Main) {
|
||||
val database = runCatching {
|
||||
SQLiteDatabase.openDatabase(
|
||||
context.androidContext.getDatabasePath("memories.db"),
|
||||
OpenParams.Builder().build(),
|
||||
)
|
||||
}.getOrNull()
|
||||
|
||||
if (database == null) {
|
||||
context.longToast("Failed to open memories database")
|
||||
return@launch
|
||||
}
|
||||
|
||||
createComposeAlertDialog(context.mainActivity!!) { alertDialog ->
|
||||
ExporterDialog(database) { alertDialog.dismiss() }
|
||||
}.apply {
|
||||
setOnDismissListener { database.close() }
|
||||
setCanceledOnTouchOutside(false)
|
||||
show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -6,6 +6,7 @@ import me.rhunk.snapenhance.core.ModContext
|
||||
import me.rhunk.snapenhance.core.action.impl.BulkMessagingAction
|
||||
import me.rhunk.snapenhance.core.action.impl.CleanCache
|
||||
import me.rhunk.snapenhance.core.action.impl.ExportChatMessages
|
||||
import me.rhunk.snapenhance.core.action.impl.ExportMemories
|
||||
import me.rhunk.snapenhance.core.manager.Manager
|
||||
|
||||
class ActionManager(
|
||||
@ -17,6 +18,7 @@ class ActionManager(
|
||||
EnumAction.CLEAN_CACHE to CleanCache::class,
|
||||
EnumAction.EXPORT_CHAT_MESSAGES to ExportChatMessages::class,
|
||||
EnumAction.BULK_MESSAGING_ACTION to BulkMessagingAction::class,
|
||||
EnumAction.EXPORT_MEMORIES to ExportMemories::class,
|
||||
).map {
|
||||
it.key to it.value.java.getConstructor().newInstance().apply {
|
||||
this.context = modContext
|
||||
@ -29,8 +31,8 @@ class ActionManager(
|
||||
|
||||
fun onNewIntent(intent: Intent?) {
|
||||
val action = intent?.getStringExtra(EnumAction.ACTION_PARAMETER) ?: return
|
||||
execute(EnumAction.entries.find { it.key == action } ?: return)
|
||||
intent.removeExtra(EnumAction.ACTION_PARAMETER)
|
||||
execute(EnumAction.entries.find { it.key == action } ?: return)
|
||||
}
|
||||
|
||||
fun execute(enumAction: EnumAction) {
|
||||
|
Loading…
x
Reference in New Issue
Block a user