feat(ui): setup activity

- remote side context
- fix float dialogs
- fix choose folder
This commit is contained in:
rhunk
2023-08-03 21:48:23 +02:00
parent 853ceec290
commit 641d66b208
39 changed files with 648 additions and 248 deletions

View File

@ -83,11 +83,11 @@ interface BridgeInterface {
void clearMessageLogger();
/**
* Fetch the translations
* Fetch the locales
*
* @return the translations result
* @return the locale result
*/
Map<String, String> fetchTranslations();
Map<String, String> fetchLocales(String userLocale);
/**
* Get check for updates last time

View File

@ -1,4 +1,21 @@
{
"setup": {
"dialogs": {
"select_language": "Select Language",
"save_folder": "For downloading snapchat media, you'll need to choose a save location. This can be changed later in the application settings.",
"select_save_folder_button": "Select Save Folder",
"mappings": "To support a wide range of versions, mappings need to be generated for the current snapchat version."
},
"mappings": {
"dialog": "To support a wide range of versions, mappings need to be generated for the current snapchat version.",
"snapchat_not_found": "Snapchat could not be found on your device. Please install Snapchat and try again.",
"snapchat_not_supported": "Snapchat is not supported. Please update Snapchat and try again.",
"generate_button": "Generate",
"generate_error": "An error occurred while generating mappings. Please try again.",
"generate_success": "Mappings generated successfully."
}
},
"category": {
"spying_privacy": "Spying & Privacy",
"media_manager": "Media Manager",

View File

@ -1,4 +1,12 @@
{
"setup": {
"dialogs": {
"select_language": "Selectionner une langue",
"save_folder": "Pour télécharger les médias Snapchat, vous devez choisir un emplacement de sauvegarde. Cela peut être modifié plus tard dans les paramètres de l'application.",
"select_save_folder_button": "Choisir un emplacement de sauvegarde"
}
},
"category": {
"spying_privacy": "Espionnage et vie privée",
"media_manager": "Gestionnaire de média",

View File

@ -16,6 +16,11 @@ object Logger {
Log.d(TAG, message.toString())
}
fun debug(tag: String, message: Any?) {
if (!BuildConfig.DEBUG) return
Log.d(tag, message.toString())
}
fun error(throwable: Throwable) {
Log.e(TAG, "", throwable)
}

View File

@ -122,4 +122,8 @@ class ModContext {
fun reloadConfig() {
modConfig.loadFromBridge(bridgeClient)
}
fun getConfigLocale(): String {
return modConfig.locale
}
}

View File

@ -90,14 +90,14 @@ class SnapEnhance {
@OptIn(ExperimentalTime::class)
private suspend fun init() {
//load translations in a coroutine to speed up initialization
withContext(appContext.coroutineDispatcher) {
appContext.translation.loadFromBridge(appContext.bridgeClient)
}
measureTime {
with(appContext) {
reloadConfig()
withContext(appContext.coroutineDispatcher) {
translation.userLocale = getConfigLocale()
translation.loadFromBridge(appContext.bridgeClient)
}
mappings.init()
eventDispatcher.init()
//if mappings aren't loaded, we can't initialize features

View File

@ -36,8 +36,9 @@ class BridgeClient(
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_MULTIPLE_TASK)
)
//TODO: randomize package name
val intent = Intent()
.setClassName(BuildConfig.APPLICATION_ID, BridgeService::class.java.name)
.setClassName(BuildConfig.APPLICATION_ID, "me.rhunk.snapenhance.bridge.BridgeService")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
bindService(
intent,
@ -103,7 +104,7 @@ class BridgeClient(
fun clearMessageLogger() = service.clearMessageLogger()
fun fetchTranslations() = service.fetchTranslations().map {
fun fetchLocales(userLocale: String) = service.fetchLocales(userLocale).map {
LocalePair(it.key, it.value)
}

View File

@ -1,105 +0,0 @@
package me.rhunk.snapenhance.bridge
import android.app.Service
import android.content.Intent
import android.os.IBinder
import me.rhunk.snapenhance.SharedContext
import me.rhunk.snapenhance.bridge.types.BridgeFileType
import me.rhunk.snapenhance.bridge.wrapper.MessageLoggerWrapper
import me.rhunk.snapenhance.bridge.wrapper.LocaleWrapper
import me.rhunk.snapenhance.download.DownloadProcessor
class BridgeService : Service() {
private lateinit var messageLoggerWrapper: MessageLoggerWrapper
override fun onBind(intent: Intent): IBinder {
messageLoggerWrapper = MessageLoggerWrapper(getDatabasePath(BridgeFileType.MESSAGE_LOGGER_DATABASE.fileName)).also { it.init() }
return BridgeBinder()
}
inner class BridgeBinder : BridgeInterface.Stub() {
override fun createAndReadFile(fileType: Int, defaultContent: ByteArray?): ByteArray {
val file = BridgeFileType.fromValue(fileType)?.resolve(this@BridgeService)
?: return defaultContent ?: ByteArray(0)
if (!file.exists()) {
if (defaultContent == null) {
return ByteArray(0)
}
file.writeBytes(defaultContent)
}
return file.readBytes()
}
override fun readFile(fileType: Int): ByteArray {
val file = BridgeFileType.fromValue(fileType)?.resolve(this@BridgeService)
?: return ByteArray(0)
if (!file.exists()) {
return ByteArray(0)
}
return file.readBytes()
}
override fun writeFile(fileType: Int, content: ByteArray?): Boolean {
val file = BridgeFileType.fromValue(fileType)?.resolve(this@BridgeService)
?: return false
if (content == null) {
return false
}
file.writeBytes(content)
return true
}
override fun deleteFile(fileType: Int): Boolean {
val file = BridgeFileType.fromValue(fileType)?.resolve(this@BridgeService)
?: return false
if (!file.exists()) {
return false
}
return file.delete()
}
override fun isFileExists(fileType: Int): Boolean {
val file = BridgeFileType.fromValue(fileType)?.resolve(this@BridgeService)
?: return false
return file.exists()
}
override fun getLoggedMessageIds(conversationId: String, limit: Int) = messageLoggerWrapper.getMessageIds(conversationId, limit).toLongArray()
override fun getMessageLoggerMessage(conversationId: String, id: Long) = messageLoggerWrapper.getMessage(conversationId, id).second
override fun addMessageLoggerMessage(conversationId: String, id: Long, message: ByteArray) {
messageLoggerWrapper.addMessage(conversationId, id, message)
}
override fun deleteMessageLoggerMessage(conversationId: String, id: Long) = messageLoggerWrapper.deleteMessage(conversationId, id)
override fun clearMessageLogger() = messageLoggerWrapper.clearMessages()
override fun fetchTranslations() = LocaleWrapper.fetchLocales(context = this@BridgeService).associate {
it.locale to it.content
}
override fun getAutoUpdaterTime(): Long {
throw UnsupportedOperationException()
}
override fun setAutoUpdaterTime(time: Long) {
throw UnsupportedOperationException()
}
override fun enqueueDownload(intent: Intent, callback: DownloadCallback) {
SharedContext.ensureInitialized(this@BridgeService)
DownloadProcessor(this@BridgeService, callback).onReceive(intent)
}
}
}

View File

@ -13,15 +13,14 @@ class LocaleWrapper {
companion object {
const val DEFAULT_LOCALE = "en_US"
fun fetchLocales(context: Context): List<LocalePair> {
val deviceLocale = Locale.getDefault().toString()
val locales = mutableListOf<LocalePair>()
fun fetchLocales(context: Context, locale: String = DEFAULT_LOCALE): List<LocalePair> {
val locales = mutableListOf<LocalePair>().apply {
add(LocalePair(DEFAULT_LOCALE, context.resources.assets.open("lang/$DEFAULT_LOCALE.json").bufferedReader().use { it.readText() }))
}
locales.add(LocalePair(DEFAULT_LOCALE, context.resources.assets.open("lang/$DEFAULT_LOCALE.json").bufferedReader().use { it.readText() }))
if (locale == DEFAULT_LOCALE) return locales
if (deviceLocale == DEFAULT_LOCALE) return locales
val compatibleLocale = context.resources.assets.list("lang")?.firstOrNull { it.startsWith(deviceLocale) }?.substring(0, 5) ?: return locales
val compatibleLocale = context.resources.assets.list("lang")?.firstOrNull { it.startsWith(locale) }?.substring(0, 5) ?: return locales
context.resources.assets.open("lang/$compatibleLocale.json").use { inputStream ->
locales.add(LocalePair(compatibleLocale, inputStream.bufferedReader().use { it.readText() }))
@ -29,19 +28,24 @@ class LocaleWrapper {
return locales
}
fun fetchAvailableLocales(context: Context): List<String> {
return context.resources.assets.list("lang")?.map { it.substring(0, 5) } ?: listOf()
}
}
var userLocale = DEFAULT_LOCALE
private val translationMap = linkedMapOf<String, String>()
private lateinit var _locale: String
private lateinit var _loadedLocaleString: String
val locale by lazy {
Locale(_locale.substring(0, 2), _locale.substring(3, 5))
val loadedLocale by lazy {
Locale(_loadedLocaleString.substring(0, 2), _loadedLocaleString.substring(3, 5))
}
private fun load(localePair: LocalePair) {
if (!::_locale.isInitialized) {
_locale = localePair.locale
if (!::_loadedLocaleString.isInitialized) {
_loadedLocaleString = localePair.locale
}
val translations = JsonParser.parseString(localePair.content).asJsonObject
@ -64,17 +68,23 @@ class LocaleWrapper {
}
fun loadFromBridge(bridgeClient: BridgeClient) {
bridgeClient.fetchTranslations().forEach {
bridgeClient.fetchLocales(userLocale).forEach {
load(it)
}
}
fun loadFromContext(context: Context) {
fetchLocales(context).forEach {
fetchLocales(context, userLocale).forEach {
load(it)
}
}
fun reloadFromContext(context: Context, locale: String) {
userLocale = locale
translationMap.clear()
loadFromContext(context)
}
operator fun get(key: String): String {
return translationMap[key] ?: key.also { Logger.debug("Missing translation for $key") }
}

View File

@ -46,7 +46,6 @@ class MappingsWrapper(
private val mappings = ConcurrentHashMap<String, Any>()
private var snapBuildNumber: Long = 0
@Suppress("deprecation")
fun init() {
snapBuildNumber = getSnapchatVersionCode()
@ -54,6 +53,7 @@ class MappingsWrapper(
runCatching {
loadCached()
}.onFailure {
Logger.error("Failed to load cached mappings", it)
delete()
}
}
@ -100,6 +100,7 @@ class MappingsWrapper(
}
fun refresh() {
snapBuildNumber = getSnapchatVersionCode()
val mapper = Mapper(*mappers)
runCatching {
@ -114,7 +115,7 @@ class MappingsWrapper(
}
write(result.toString().toByteArray())
}.also {
Logger.xposedLog("Generated mappings in $it ms")
Logger.debug("Generated mappings in $it ms")
}
}

View File

@ -38,7 +38,7 @@ open class ConfigContainer(
vararg values: String = emptyArray(),
params: ConfigParamsBuilder = {}
) = registerProperty(key,
DataProcessors.STRING_MULTIPLE_SELECTION, PropertyValue(emptyList<String>(), defaultValues = values.toList()), params)
DataProcessors.STRING_MULTIPLE_SELECTION, PropertyValue(mutableListOf<String>(), defaultValues = values.toList()), params)
//null value is considered as Off/Disabled
protected fun unique(

View File

@ -10,22 +10,20 @@ import me.rhunk.snapenhance.bridge.FileLoaderWrapper
import me.rhunk.snapenhance.bridge.types.BridgeFileType
import me.rhunk.snapenhance.bridge.wrapper.LocaleWrapper
import me.rhunk.snapenhance.core.config.impl.RootConfig
import kotlin.properties.Delegates
class ModConfig {
var locale: String = LocaleWrapper.DEFAULT_LOCALE
set(value) {
field = value
writeConfig()
}
private val gson: Gson = GsonBuilder().setPrettyPrinting().create()
private val file = FileLoaderWrapper(BridgeFileType.CONFIG, "{}".toByteArray(Charsets.UTF_8))
var wasPresent by Delegates.notNull<Boolean>()
val root = RootConfig()
operator fun getValue(thisRef: Any?, property: Any?) = root
private fun load() {
wasPresent = file.isFileExists()
if (!file.isFileExists()) {
writeConfig()
return
@ -42,12 +40,13 @@ class ModConfig {
private fun loadConfig() {
val configFileContent = file.read()
val configObject = gson.fromJson(configFileContent.toString(Charsets.UTF_8), JsonObject::class.java)
locale = configObject.get("language")?.asString ?: LocaleWrapper.DEFAULT_LOCALE
locale = configObject.get("_locale")?.asString ?: LocaleWrapper.DEFAULT_LOCALE
root.fromJson(configObject)
}
fun writeConfig() {
val configObject = root.toJson()
configObject.addProperty("language", locale)
configObject.addProperty("_locale", locale)
file.write(configObject.toString().toByteArray(Charsets.UTF_8))
}

View File

@ -16,11 +16,16 @@ class DownloadManagerClient (
private val metadata: DownloadMetadata,
private val callback: DownloadCallback
) {
companion object {
const val DOWNLOAD_REQUEST_EXTRA = "request"
const val DOWNLOAD_METADATA_EXTRA = "metadata"
}
private fun enqueueDownloadRequest(request: DownloadRequest) {
context.bridgeClient.enqueueDownload(Intent().apply {
putExtras(Bundle().apply {
putString(DownloadProcessor.DOWNLOAD_REQUEST_EXTRA, context.gson.toJson(request))
putString(DownloadProcessor.DOWNLOAD_METADATA_EXTRA, context.gson.toJson(metadata))
putString(DOWNLOAD_REQUEST_EXTRA, context.gson.toJson(request))
putString(DOWNLOAD_METADATA_EXTRA, context.gson.toJson(metadata))
})
}, callback)
}

View File

@ -1,386 +0,0 @@
package me.rhunk.snapenhance.download
import android.annotation.SuppressLint
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.widget.Toast
import androidx.documentfile.provider.DocumentFile
import com.google.gson.GsonBuilder
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.job
import kotlinx.coroutines.joinAll
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import me.rhunk.snapenhance.Constants
import me.rhunk.snapenhance.Logger
import me.rhunk.snapenhance.SharedContext
import me.rhunk.snapenhance.bridge.DownloadCallback
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
import me.rhunk.snapenhance.download.data.InputMedia
import me.rhunk.snapenhance.download.data.MediaEncryptionKeyPair
import me.rhunk.snapenhance.download.data.PendingDownload
import me.rhunk.snapenhance.download.enums.DownloadMediaType
import me.rhunk.snapenhance.download.enums.DownloadStage
import me.rhunk.snapenhance.util.download.RemoteMediaResolver
import me.rhunk.snapenhance.util.snap.MediaDownloaderHelper
import java.io.File
import java.io.InputStream
import java.net.HttpURLConnection
import java.net.URL
import java.util.zip.ZipInputStream
import javax.crypto.Cipher
import javax.crypto.CipherInputStream
import javax.crypto.spec.IvParameterSpec
import javax.crypto.spec.SecretKeySpec
import javax.xml.parsers.DocumentBuilderFactory
import javax.xml.transform.TransformerFactory
import javax.xml.transform.dom.DOMSource
import javax.xml.transform.stream.StreamResult
import kotlin.coroutines.coroutineContext
import kotlin.io.encoding.Base64
import kotlin.io.encoding.ExperimentalEncodingApi
data class DownloadedFile(
val file: File,
val fileType: FileType
)
/**
* DownloadProcessor handles the download requests of the user
*/
@OptIn(ExperimentalEncodingApi::class)
class DownloadProcessor (
private val context: Context,
private val callback: DownloadCallback
) {
companion object {
const val DOWNLOAD_REQUEST_EXTRA = "request"
const val DOWNLOAD_METADATA_EXTRA = "metadata"
}
private val translation by lazy {
SharedContext.translation.getCategory("download_processor")
}
private val gson by lazy {
GsonBuilder().setPrettyPrinting().create()
}
private fun fallbackToast(message: Any) {
android.os.Handler(context.mainLooper).post {
Toast.makeText(context, message.toString(), Toast.LENGTH_SHORT).show()
}
}
private fun extractZip(inputStream: InputStream): List<File> {
val files = mutableListOf<File>()
val zipInputStream = ZipInputStream(inputStream)
var entry = zipInputStream.nextEntry
while (entry != null) {
createMediaTempFile().also { file ->
file.outputStream().use { outputStream ->
zipInputStream.copyTo(outputStream)
}
files += file
}
entry = zipInputStream.nextEntry
}
return files
}
private fun decryptInputStream(inputStream: InputStream, encryption: MediaEncryptionKeyPair): InputStream {
val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding")
val key = Base64.UrlSafe.decode(encryption.key)
val iv = Base64.UrlSafe.decode(encryption.iv)
cipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(key, "AES"), IvParameterSpec(iv))
return CipherInputStream(inputStream, cipher)
}
private fun createNeededDirectories(file: File): File {
val directory = file.parentFile ?: return file
if (!directory.exists()) {
directory.mkdirs()
}
return file
}
@SuppressLint("UnspecifiedRegisterReceiverFlag")
private suspend fun saveMediaToGallery(inputFile: File, pendingDownload: PendingDownload) {
if (coroutineContext.job.isCancelled) return
val config by ModConfig().apply { loadFromContext(context) }
runCatching {
val fileType = FileType.fromFile(inputFile)
if (fileType == FileType.UNKNOWN) {
callback.onFailure(translation.format("failed_gallery_toast", "error" to "Unknown media type"), null)
return
}
val fileName = pendingDownload.metadata.outputPath.substringAfterLast("/") + "." + fileType.fileExtension
val outputFolder = DocumentFile.fromTreeUri(context, Uri.parse(config.downloader.saveFolder.get()))
?: throw Exception("Failed to open output folder")
val outputFileFolder = pendingDownload.metadata.outputPath.let {
if (it.contains("/")) {
it.substringBeforeLast("/").split("/").fold(outputFolder) { folder, name ->
folder.findFile(name) ?: folder.createDirectory(name)!!
}
} else {
outputFolder
}
}
val outputFile = outputFileFolder.createFile(fileType.mimeType, fileName)!!
val outputStream = context.contentResolver.openOutputStream(outputFile.uri)!!
inputFile.inputStream().use { inputStream ->
inputStream.copyTo(outputStream)
}
pendingDownload.outputFile = outputFile.uri.toString()
pendingDownload.downloadStage = DownloadStage.SAVED
runCatching {
val mediaScanIntent = Intent("android.intent.action.MEDIA_SCANNER_SCAN_FILE")
mediaScanIntent.setData(outputFile.uri)
context.sendBroadcast(mediaScanIntent)
}.onFailure {
Logger.error("Failed to scan media file", it)
callback.onFailure(translation.format("failed_gallery_toast", "error" to it.toString()), it.message)
}
Logger.debug("download complete")
fileName.let {
runCatching { callback.onSuccess(it) }.onFailure { fallbackToast(it) }
}
}.onFailure { exception ->
Logger.error(exception)
translation.format("failed_gallery_toast", "error" to exception.toString()).let {
runCatching { callback.onFailure(it, exception.message) }.onFailure { fallbackToast(it) }
}
pendingDownload.downloadStage = DownloadStage.FAILED
}
}
private fun createMediaTempFile(): File {
return File.createTempFile("media", ".tmp")
}
private fun downloadInputMedias(downloadRequest: DownloadRequest) = runBlocking {
val jobs = mutableListOf<Job>()
val downloadedMedias = mutableMapOf<InputMedia, File>()
downloadRequest.inputMedias.forEach { inputMedia ->
fun handleInputStream(inputStream: InputStream) {
createMediaTempFile().apply {
if (inputMedia.encryption != null) {
decryptInputStream(inputStream, inputMedia.encryption).use { decryptedInputStream ->
decryptedInputStream.copyTo(outputStream())
}
} else {
inputStream.copyTo(outputStream())
}
}.also { downloadedMedias[inputMedia] = it }
}
launch {
when (inputMedia.type) {
DownloadMediaType.PROTO_MEDIA -> {
RemoteMediaResolver.downloadBoltMedia(Base64.UrlSafe.decode(inputMedia.content))?.let { inputStream ->
handleInputStream(inputStream)
}
}
DownloadMediaType.DIRECT_MEDIA -> {
val decoded = Base64.UrlSafe.decode(inputMedia.content)
createMediaTempFile().apply {
writeBytes(decoded)
}.also { downloadedMedias[inputMedia] = it }
}
DownloadMediaType.REMOTE_MEDIA -> {
with(URL(inputMedia.content).openConnection() as HttpURLConnection) {
requestMethod = "GET"
setRequestProperty("User-Agent", Constants.USER_AGENT)
connect()
handleInputStream(inputStream)
}
}
else -> {
downloadedMedias[inputMedia] = File(inputMedia.content)
}
}
}.also { jobs.add(it) }
}
jobs.joinAll()
downloadedMedias
}
private suspend fun downloadRemoteMedia(pendingDownloadObject: PendingDownload, downloadedMedias: Map<InputMedia, DownloadedFile>, downloadRequest: DownloadRequest) {
downloadRequest.inputMedias.first().let { inputMedia ->
val mediaType = inputMedia.type
val media = downloadedMedias[inputMedia]!!
if (!downloadRequest.isDashPlaylist) {
saveMediaToGallery(media.file, pendingDownloadObject)
media.file.delete()
return
}
assert(mediaType == DownloadMediaType.REMOTE_MEDIA)
val playlistXml = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(media.file)
val baseUrlNodeList = playlistXml.getElementsByTagName("BaseURL")
for (i in 0 until baseUrlNodeList.length) {
val baseUrlNode = baseUrlNodeList.item(i)
val baseUrl = baseUrlNode.textContent
baseUrlNode.textContent = "${RemoteMediaResolver.CF_ST_CDN_D}$baseUrl"
}
val dashOptions = downloadRequest.dashOptions!!
val dashPlaylistFile = renameFromFileType(media.file, FileType.MPD)
val xmlData = dashPlaylistFile.outputStream()
TransformerFactory.newInstance().newTransformer().transform(DOMSource(playlistXml), StreamResult(xmlData))
translation.format("download_toast", "path" to dashPlaylistFile.nameWithoutExtension).let {
runCatching { callback.onProgress(it) }.onFailure { fallbackToast(it) }
}
val outputFile = File.createTempFile("dash", ".mp4")
runCatching {
MediaDownloaderHelper.downloadDashChapterFile(
dashPlaylist = dashPlaylistFile,
output = outputFile,
startTime = dashOptions.offsetTime,
duration = dashOptions.duration)
saveMediaToGallery(outputFile, pendingDownloadObject)
}.onFailure { exception ->
if (coroutineContext.job.isCancelled) return@onFailure
Logger.error(exception)
translation.format("failed_processing_toast", "error" to exception.toString()).let {
runCatching { callback.onFailure(it, exception.message) }.onFailure { fallbackToast(it) }
}
pendingDownloadObject.downloadStage = DownloadStage.FAILED
}
dashPlaylistFile.delete()
outputFile.delete()
media.file.delete()
}
}
private fun renameFromFileType(file: File, fileType: FileType): File {
val newFile = File(file.parentFile, file.nameWithoutExtension + "." + fileType.fileExtension)
file.renameTo(newFile)
return newFile
}
fun onReceive(intent: Intent) {
CoroutineScope(Dispatchers.IO).launch {
val downloadMetadata = gson.fromJson(intent.getStringExtra(DOWNLOAD_METADATA_EXTRA)!!, DownloadMetadata::class.java)
val downloadRequest = gson.fromJson(intent.getStringExtra(DOWNLOAD_REQUEST_EXTRA)!!, DownloadRequest::class.java)
SharedContext.downloadTaskManager.canDownloadMedia(downloadMetadata.mediaIdentifier)?.let { downloadStage ->
translation[if (downloadStage.isFinalStage) {
"already_downloaded_toast"
} else {
"already_queued_toast"
}].let {
runCatching { callback.onFailure(it, null) }.onFailure { fallbackToast(it) }
}
return@launch
}
val pendingDownloadObject = PendingDownload(
metadata = downloadMetadata
)
SharedContext.downloadTaskManager.addTask(pendingDownloadObject)
pendingDownloadObject.apply {
job = coroutineContext.job
downloadStage = DownloadStage.DOWNLOADING
}
runCatching {
//first download all input medias into cache
val downloadedMedias = downloadInputMedias(downloadRequest).map {
it.key to DownloadedFile(it.value, FileType.fromFile(it.value))
}.toMap().toMutableMap()
Logger.debug("downloaded ${downloadedMedias.size} medias")
var shouldMergeOverlay = downloadRequest.shouldMergeOverlay
//if there is a zip file, extract it and replace the downloaded media with the extracted ones
downloadedMedias.values.find { it.fileType == FileType.ZIP }?.let { entry ->
val extractedMedias = extractZip(entry.file.inputStream()).map {
InputMedia(
type = DownloadMediaType.LOCAL_MEDIA,
content = it.absolutePath
) to DownloadedFile(it, FileType.fromFile(it))
}
downloadedMedias.values.removeIf {
it.file.delete()
true
}
downloadedMedias.putAll(extractedMedias)
shouldMergeOverlay = true
}
if (shouldMergeOverlay) {
assert(downloadedMedias.size == 2)
val media = downloadedMedias.values.first { it.fileType.isVideo }
val overlayMedia = downloadedMedias.values.first { it.fileType.isImage }
val renamedMedia = renameFromFileType(media.file, media.fileType)
val renamedOverlayMedia = renameFromFileType(overlayMedia.file, overlayMedia.fileType)
val mergedOverlay: File = File.createTempFile("merged", "." + media.fileType.fileExtension)
runCatching {
translation.format("download_toast", "path" to media.file.nameWithoutExtension).let {
runCatching { callback.onProgress(it) }.onFailure { fallbackToast(it) }
}
pendingDownloadObject.downloadStage = DownloadStage.MERGING
MediaDownloaderHelper.mergeOverlayFile(
media = renamedMedia,
overlay = renamedOverlayMedia,
output = mergedOverlay
)
saveMediaToGallery(mergedOverlay, pendingDownloadObject)
}.onFailure { exception ->
if (coroutineContext.job.isCancelled) return@onFailure
Logger.error(exception)
translation.format("failed_processing_toast", "error" to exception.toString()).let {
runCatching { callback.onFailure(it, exception.message) }.onFailure { fallbackToast(it) }
}
pendingDownloadObject.downloadStage = DownloadStage.MERGE_FAILED
}
mergedOverlay.delete()
renamedOverlayMedia.delete()
renamedMedia.delete()
return@launch
}
downloadRemoteMedia(pendingDownloadObject, downloadedMedias, downloadRequest)
}.onFailure { exception ->
pendingDownloadObject.downloadStage = DownloadStage.FAILED
Logger.error(exception)
translation["failed_generic_toast"].let {
runCatching { callback.onFailure(it, exception.message) }.onFailure { fallbackToast(it) }
}
}
}
}
}

View File

@ -3,9 +3,9 @@ package me.rhunk.snapenhance.features
object FeatureLoadParams {
const val NO_INIT = 0
const val INIT_SYNC = 1
const val ACTIVITY_CREATE_SYNC = 2
const val INIT_SYNC = 0b0001
const val ACTIVITY_CREATE_SYNC = 0b0010
const val INIT_ASYNC = 3
const val ACTIVITY_CREATE_ASYNC = 4
const val INIT_ASYNC = 0b0100
const val ACTIVITY_CREATE_ASYNC = 0b1000
}

View File

@ -84,7 +84,7 @@ class FriendFeedInfoMenu : AbstractMenu() {
${birthday.getDisplayName(
Calendar.MONTH,
Calendar.LONG,
context.translation.locale
context.translation.loadedLocale
)?.let {
context.translation.format("profile_info.birthday",
"month" to it,

View File

@ -47,9 +47,8 @@ class SettingsGearInjector : AbstractMenu() {
setOnClickListener {
val intent = Intent().apply {
setClassName(BuildConfig.APPLICATION_ID, "me.rhunk.snapenhance.manager.MainActivity")
setClassName(BuildConfig.APPLICATION_ID, "me.rhunk.snapenhance.ui.manager.MainActivity")
putExtra("route", "features")
putExtra("lspatched", File(context.cacheDir, "lspatch/origin").exists())
}
context.startActivity(intent)
}