commit 3df11aadb8546f5997e2f36b9790f4e96e12b1df parent 2ff8a6940364a997f6c8515dc83bb7303609a930 Author: rhunk <101876869+rhunk@users.noreply.github.com> Date: Fri, 4 Aug 2023 12:17:19 +0200 refactor: download - download task manager - fix installation summary update Diffstat:
24 files changed, 189 insertions(+), 217 deletions(-)
diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/RemoteSideContext.kt b/app/src/main/kotlin/me/rhunk/snapenhance/RemoteSideContext.kt @@ -8,6 +8,7 @@ import androidx.documentfile.provider.DocumentFile import me.rhunk.snapenhance.bridge.wrapper.LocaleWrapper import me.rhunk.snapenhance.bridge.wrapper.MappingsWrapper import me.rhunk.snapenhance.core.config.ModConfig +import me.rhunk.snapenhance.download.DownloadTaskManager import me.rhunk.snapenhance.ui.manager.data.InstallationSummary import me.rhunk.snapenhance.ui.manager.data.ModMappingsInfo import me.rhunk.snapenhance.ui.manager.data.SnapchatAppInfo @@ -17,24 +18,40 @@ import java.lang.ref.WeakReference import kotlin.system.exitProcess class RemoteSideContext( - val androidContext: Context + ctx: Context ) { + private var _context: WeakReference<Context> = WeakReference(ctx) private var _activity: WeakReference<Activity>? = null + + var androidContext: Context + get() = synchronized(this) { + _context.get() ?: error("Context is null") + } + set(value) { synchronized(this) { + _context.clear(); _context = WeakReference(value) + } } + var activity: Activity? get() = _activity?.get() - set(value) { _activity = WeakReference(value) } + set(value) { _activity?.clear(); _activity = WeakReference(value) } val config = ModConfig() val translation = LocaleWrapper() val mappings = MappingsWrapper(androidContext) + val downloadTaskManager = DownloadTaskManager() init { - config.loadFromContext(androidContext) - translation.userLocale = config.locale - translation.loadFromContext(androidContext) - mappings.apply { - loadFromContext(androidContext) - init() + runCatching { + config.loadFromContext(androidContext) + translation.userLocale = config.locale + translation.loadFromContext(androidContext) + mappings.apply { + loadFromContext(androidContext) + init() + } + downloadTaskManager.init(androidContext) + }.onFailure { + Logger.error("Failed to initialize RemoteSideContext", it) } } diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/SharedContextHolder.kt b/app/src/main/kotlin/me/rhunk/snapenhance/SharedContextHolder.kt @@ -1,15 +1,19 @@ package me.rhunk.snapenhance import android.content.Context -import java.lang.ref.WeakReference object SharedContextHolder { - private lateinit var _remoteSideContext: WeakReference<RemoteSideContext> + private lateinit var _remoteSideContext: RemoteSideContext fun remote(context: Context): RemoteSideContext { - if (!::_remoteSideContext.isInitialized || _remoteSideContext.get() == null) { - _remoteSideContext = WeakReference(RemoteSideContext(context.applicationContext)) + if (!::_remoteSideContext.isInitialized) { + _remoteSideContext = RemoteSideContext(context) } - return _remoteSideContext.get()!! + + if (_remoteSideContext.androidContext != context) { + _remoteSideContext.androidContext = context + } + + return _remoteSideContext } } \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/bridge/BridgeService.kt b/app/src/main/kotlin/me/rhunk/snapenhance/bridge/BridgeService.kt @@ -4,11 +4,10 @@ import android.app.Service import android.content.Intent import android.os.IBinder import me.rhunk.snapenhance.RemoteSideContext -import me.rhunk.snapenhance.SharedContext import me.rhunk.snapenhance.SharedContextHolder 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.bridge.wrapper.MessageLoggerWrapper import me.rhunk.snapenhance.download.DownloadProcessor class BridgeService : Service() { @@ -105,10 +104,10 @@ class BridgeService : Service() { } override fun enqueueDownload(intent: Intent, callback: DownloadCallback) { - SharedContextHolder.remote(this@BridgeService) - //TODO: refactor shared context - SharedContext.ensureInitialized(this@BridgeService) - DownloadProcessor(this@BridgeService, callback).onReceive(intent) + DownloadProcessor( + remoteSideContext = SharedContextHolder.remote(this@BridgeService), + callback = callback + ).onReceive(intent) } } } diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/download/DownloadProcessor.kt b/app/src/main/kotlin/me/rhunk/snapenhance/download/DownloadProcessor.kt @@ -1,7 +1,6 @@ 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 @@ -16,17 +15,17 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import me.rhunk.snapenhance.Constants import me.rhunk.snapenhance.Logger +import me.rhunk.snapenhance.RemoteSideContext 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.DownloadMediaType import me.rhunk.snapenhance.download.data.DownloadMetadata import me.rhunk.snapenhance.download.data.DownloadRequest +import me.rhunk.snapenhance.download.data.DownloadStage 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 @@ -56,7 +55,7 @@ data class DownloadedFile( */ @OptIn(ExperimentalEncodingApi::class) class DownloadProcessor ( - private val context: Context, + private val remoteSideContext: RemoteSideContext, private val callback: DownloadCallback ) { @@ -69,11 +68,29 @@ class DownloadProcessor ( } private fun fallbackToast(message: Any) { - android.os.Handler(context.mainLooper).post { - Toast.makeText(context, message.toString(), Toast.LENGTH_SHORT).show() + android.os.Handler(remoteSideContext.androidContext.mainLooper).post { + Toast.makeText(remoteSideContext.androidContext, message.toString(), Toast.LENGTH_SHORT).show() } } + private fun callbackOnSuccess(path: String) = runCatching { + callback.onSuccess(path) + }.onFailure { + fallbackToast(it) + } + + private fun callbackOnFailure(message: String, throwable: String? = null) = runCatching { + callback.onFailure(message, throwable) + }.onFailure { + fallbackToast("$message\n$throwable") + } + + private fun callbackOnProgress(message: String) = runCatching { + callback.onProgress(message) + }.onFailure { + fallbackToast(it) + } + private fun extractZip(inputStream: InputStream): List<File> { val files = mutableListOf<File>() val zipInputStream = ZipInputStream(inputStream) @@ -100,30 +117,20 @@ class DownloadProcessor ( 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) + callbackOnFailure(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())) + val outputFolder = DocumentFile.fromTreeUri(remoteSideContext.androidContext, Uri.parse(remoteSideContext.config.root.downloader.saveFolder.get())) ?: throw Exception("Failed to open output folder") val outputFileFolder = pendingDownload.metadata.outputPath.let { @@ -137,7 +144,7 @@ class DownloadProcessor ( } val outputFile = outputFileFolder.createFile(fileType.mimeType, fileName)!! - val outputStream = context.contentResolver.openOutputStream(outputFile.uri)!! + val outputStream = remoteSideContext.androidContext.contentResolver.openOutputStream(outputFile.uri)!! inputFile.inputStream().use { inputStream -> inputStream.copyTo(outputStream) @@ -149,21 +156,17 @@ class DownloadProcessor ( runCatching { val mediaScanIntent = Intent("android.intent.action.MEDIA_SCANNER_SCAN_FILE") mediaScanIntent.setData(outputFile.uri) - context.sendBroadcast(mediaScanIntent) + remoteSideContext.androidContext.sendBroadcast(mediaScanIntent) }.onFailure { Logger.error("Failed to scan media file", it) - callback.onFailure(translation.format("failed_gallery_toast", "error" to it.toString()), it.message) + callbackOnFailure(translation.format("failed_gallery_toast", "error" to it.toString()), it.message) } Logger.debug("download complete") - fileName.let { - runCatching { callback.onSuccess(it) }.onFailure { fallbackToast(it) } - } + callbackOnSuccess(fileName) }.onFailure { exception -> Logger.error(exception) - translation.format("failed_gallery_toast", "error" to exception.toString()).let { - runCatching { callback.onFailure(it, exception.message) }.onFailure { fallbackToast(it) } - } + callbackOnFailure(translation.format("failed_gallery_toast", "error" to exception.toString()), exception.message) pendingDownload.downloadStage = DownloadStage.FAILED } } @@ -250,9 +253,7 @@ class DownloadProcessor ( 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) } - } + callbackOnProgress(translation.format("download_toast", "path" to dashPlaylistFile.nameWithoutExtension)) val outputFile = File.createTempFile("dash", ".mp4") runCatching { MediaDownloaderHelper.downloadDashChapterFile( @@ -264,9 +265,7 @@ class DownloadProcessor ( }.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) } - } + callbackOnFailure(translation.format("failed_processing_toast", "error" to exception.toString()), exception.message) pendingDownloadObject.downloadStage = DownloadStage.FAILED } @@ -287,23 +286,24 @@ class DownloadProcessor ( val downloadMetadata = gson.fromJson(intent.getStringExtra(DownloadManagerClient.DOWNLOAD_METADATA_EXTRA)!!, DownloadMetadata::class.java) val downloadRequest = gson.fromJson(intent.getStringExtra(DownloadManagerClient.DOWNLOAD_REQUEST_EXTRA)!!, DownloadRequest::class.java) - SharedContext.downloadTaskManager.canDownloadMedia(downloadMetadata.mediaIdentifier)?.let { downloadStage -> + remoteSideContext.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) } + callbackOnFailure(it, null) } return@launch } val pendingDownloadObject = PendingDownload( metadata = downloadMetadata - ) + ).apply { downloadTaskManager = remoteSideContext.downloadTaskManager } - SharedContext.downloadTaskManager.addTask(pendingDownloadObject) - pendingDownloadObject.apply { + pendingDownloadObject.also { + remoteSideContext.downloadTaskManager.addTask(it) + }.apply { job = coroutineContext.job downloadStage = DownloadStage.DOWNLOADING } @@ -344,9 +344,7 @@ class DownloadProcessor ( 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) } - } + callbackOnProgress(translation.format("download_toast", "path" to media.file.nameWithoutExtension)) pendingDownloadObject.downloadStage = DownloadStage.MERGING MediaDownloaderHelper.mergeOverlayFile( @@ -359,9 +357,7 @@ class DownloadProcessor ( }.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) } - } + callbackOnFailure(translation.format("failed_processing_toast", "error" to exception.toString()), exception.message) pendingDownloadObject.downloadStage = DownloadStage.MERGE_FAILED } @@ -375,9 +371,7 @@ class DownloadProcessor ( }.onFailure { exception -> pendingDownloadObject.downloadStage = DownloadStage.FAILED Logger.error(exception) - translation["failed_generic_toast"].let { - runCatching { callback.onFailure(it, exception.message) }.onFailure { fallbackToast(it) } - } + callbackOnFailure(translation["failed_generic_toast"], exception.message) } } } diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/MainActivity.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/MainActivity.kt @@ -11,6 +11,13 @@ import me.rhunk.snapenhance.SharedContextHolder import me.rhunk.snapenhance.ui.AppMaterialTheme class MainActivity : ComponentActivity() { + lateinit var sections: Map<EnumSection, Section> + + override fun onPostResume() { + super.onPostResume() + sections.values.forEach { it.onResumed() } + } + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -20,8 +27,12 @@ class MainActivity : ComponentActivity() { checkForRequirements() } - val sections = EnumSection.values().toList().associateWith { - it.section.constructors.first().call() + sections = EnumSection.values().toList().associateWith { + runCatching { + it.section.constructors.first().call() + }.onFailure { + it.printStackTrace() + }.getOrThrow() }.onEach { (section, instance) -> with(instance) { enumSection = section diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/Section.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/Section.kt @@ -14,6 +14,7 @@ import androidx.navigation.compose.composable import me.rhunk.snapenhance.RemoteSideContext import me.rhunk.snapenhance.ui.manager.sections.HomeSection import me.rhunk.snapenhance.ui.manager.sections.NotImplemented +import me.rhunk.snapenhance.ui.manager.sections.download.DownloadSection import me.rhunk.snapenhance.ui.manager.sections.features.FeaturesSection import kotlin.reflect.KClass @@ -26,7 +27,8 @@ enum class EnumSection( DOWNLOADS( route = "downloads", title = "Downloads", - icon = Icons.Filled.Download + icon = Icons.Filled.Download, + section = DownloadSection::class ), FEATURES( route = "features", @@ -66,6 +68,7 @@ open class Section { lateinit var navController: NavController open fun init() {} + open fun onResumed() {} @Composable open fun Content() { NotImplemented() } diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/HomeSection.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/HomeSection.kt @@ -18,11 +18,13 @@ import androidx.compose.material3.Icon import androidx.compose.material3.OutlinedCard import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import me.rhunk.snapenhance.Logger import me.rhunk.snapenhance.ui.manager.Section import me.rhunk.snapenhance.ui.manager.data.InstallationSummary import me.rhunk.snapenhance.ui.setup.Requirements @@ -31,6 +33,7 @@ class HomeSection : Section() { companion object { val cardMargin = 10.dp } + private val installationSummary = mutableStateOf(null as InstallationSummary?) @OptIn(ExperimentalLayoutApi::class) @Composable @@ -72,7 +75,8 @@ class HomeSection : Section() { "Mappings ${if (installationSummary.mappingsInfo == null) "not generated" else "outdated"}" } else { "Mappings version ${installationSummary.mappingsInfo.generatedSnapchatVersion}" - }, modifier = Modifier.weight(1f) + }, modifier = Modifier + .weight(1f) .align(Alignment.CenterVertically) ) @@ -86,6 +90,14 @@ class HomeSection : Section() { } } + override fun onResumed() { + Logger.debug("HomeSection resumed") + if (!context.mappings.isMappingsLoaded()) { + context.mappings.init() + } + installationSummary.value = context.getInstallationSummary() + } + @Composable @Preview override fun Content() { @@ -105,7 +117,7 @@ class HomeSection : Section() { modifier = Modifier.padding(16.dp) ) - SummaryCards(context.getInstallationSummary()) + SummaryCards(installationSummary = installationSummary.value ?: return) } } } \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/download/DownloadSection.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/download/DownloadSection.kt @@ -0,0 +1,11 @@ +package me.rhunk.snapenhance.ui.manager.sections.download + +import androidx.compose.runtime.Composable +import me.rhunk.snapenhance.ui.manager.Section + +class DownloadSection : Section() { + @Composable + override fun Content() { + + } +}+ \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/setup/Requirements.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/setup/Requirements.kt @@ -5,7 +5,6 @@ object Requirements { const val LANGUAGE = 0b00010 const val MAPPINGS = 0b00100 const val SAVE_FOLDER = 0b01000 - const val FFMPEG = 0b10000 fun getName(requirement: Int): String { return when (requirement) { @@ -13,7 +12,6 @@ object Requirements { LANGUAGE -> "LANGUAGE" MAPPINGS -> "MAPPINGS" SAVE_FOLDER -> "SAVE_FOLDER" - FFMPEG -> "FFMPEG" else -> "UNKNOWN" } } diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/setup/SetupActivity.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/setup/SetupActivity.kt @@ -34,7 +34,6 @@ import androidx.navigation.compose.rememberNavController import me.rhunk.snapenhance.SharedContextHolder import me.rhunk.snapenhance.ui.AppMaterialTheme import me.rhunk.snapenhance.ui.setup.screens.SetupScreen -import me.rhunk.snapenhance.ui.setup.screens.impl.FfmpegScreen import me.rhunk.snapenhance.ui.setup.screens.impl.MappingsScreen import me.rhunk.snapenhance.ui.setup.screens.impl.PickLanguageScreen import me.rhunk.snapenhance.ui.setup.screens.impl.SaveFolderScreen @@ -65,9 +64,6 @@ class SetupActivity : ComponentActivity() { if (isFirstRun || hasRequirement(Requirements.MAPPINGS)) { add(MappingsScreen().apply { route = "mappings" }) } - if (isFirstRun || hasRequirement(Requirements.FFMPEG)) { - add(FfmpegScreen().apply { route = "ffmpeg" }) - } } // If there are no required screens, we can just finish the activity diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/setup/screens/impl/FfmpegScreen.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/setup/screens/impl/FfmpegScreen.kt @@ -1,17 +0,0 @@ -package me.rhunk.snapenhance.ui.setup.screens.impl - -import androidx.compose.material3.Button -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import me.rhunk.snapenhance.ui.setup.screens.SetupScreen - -class FfmpegScreen : SetupScreen() { - - @Composable - override fun Content() { - Text(text = "FFmpeg") - Button(onClick = { allowNext(true) }) { - Text(text = "Next") - } - } -}- \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/setup/screens/impl/PickLanguageScreen.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/setup/screens/impl/PickLanguageScreen.kt @@ -40,7 +40,7 @@ class PickLanguageScreen : SetupScreen(){ fun getLocaleDisplayName(locale: String): String { locale.split("_").let { - return java.util.Locale(it[0], it[1]).getDisplayName(java.util.Locale.getDefault()) + return Locale(it[0], it[1]).getDisplayName(Locale.getDefault()) } } diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/SharedContext.kt b/core/src/main/kotlin/me/rhunk/snapenhance/SharedContext.kt @@ -18,51 +18,6 @@ object SharedContext { lateinit var downloadTaskManager: DownloadTaskManager lateinit var translation: LocaleWrapper - private fun askForStoragePermission(context: Context) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - val intent = Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION) - intent.addCategory("android.intent.category.DEFAULT") - intent.data = android.net.Uri.parse("package:${context.packageName}") - if (context !is Activity) { - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - } - context.startActivity(intent) - exitProcess(0) - } - if (context !is Activity) { - Logger.log("Storage permission not granted, exiting") - exitProcess(0) - } - context.requestPermissions(arrayOf(android.Manifest.permission.WRITE_EXTERNAL_STORAGE, android.Manifest.permission.READ_EXTERNAL_STORAGE), 0) - } - - private fun askForPermissions(context: Context) { - - //ask for storage permission - val hasStoragePermission = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - Environment.isExternalStorageManager() - } else { - context.checkSelfPermission(android.Manifest.permission.WRITE_EXTERNAL_STORAGE) == android.content.pm.PackageManager.PERMISSION_GRANTED - } - - if (hasStoragePermission) return - - if (context !is Activity) { - askForStoragePermission(context) - return - } - AlertDialog.Builder(context) - .setTitle("Storage permission") - .setMessage("App needs storage permission to download files and save them to your device. Please allow it in the next screen.") - .setPositiveButton("Grant") { _, _ -> - askForStoragePermission(context) - } - .setNegativeButton("Cancel") { _, _ -> - exitProcess(0) - } - .show() - } - fun ensureInitialized(context: Context) { if (!this::downloadTaskManager.isInitialized) { downloadTaskManager = DownloadTaskManager().apply { diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/config/impl/DownloaderConfig.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/config/impl/DownloaderConfig.kt @@ -16,9 +16,11 @@ class DownloaderConfig : ConfigContainer() { "append_date_time", "append_type", "append_username" - ) + ).apply { set(mutableListOf("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") + val logging = multiple("logging", "started", "success", "progress", "failure").apply { + set(mutableListOf("started", "success")) + } } \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/download/DownloadManagerClient.kt b/core/src/main/kotlin/me/rhunk/snapenhance/download/DownloadManagerClient.kt @@ -9,7 +9,7 @@ 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.enums.DownloadMediaType +import me.rhunk.snapenhance.download.data.DownloadMediaType class DownloadManagerClient ( private val context: ModContext, diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/download/DownloadTaskManager.kt b/core/src/main/kotlin/me/rhunk/snapenhance/download/DownloadTaskManager.kt @@ -5,7 +5,7 @@ import android.content.Context import android.database.sqlite.SQLiteDatabase import me.rhunk.snapenhance.download.data.DownloadMetadata import me.rhunk.snapenhance.download.data.PendingDownload -import me.rhunk.snapenhance.download.enums.DownloadStage +import me.rhunk.snapenhance.download.data.DownloadStage import me.rhunk.snapenhance.ui.download.MediaFilter import me.rhunk.snapenhance.util.SQLiteDatabaseHelper @@ -158,6 +158,7 @@ class DownloadTaskManager { iconUrl = cursor.getString(cursor.getColumnIndex("iconUrl")) ) ).apply { + downloadTaskManager = this@DownloadTaskManager downloadStage = DownloadStage.valueOf(cursor.getString(cursor.getColumnIndex("downloadStage"))) //if downloadStage is not saved, it means the app was killed before the download was finished if (downloadStage != DownloadStage.SAVED) { diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/download/data/DownloadMediaType.kt b/core/src/main/kotlin/me/rhunk/snapenhance/download/data/DownloadMediaType.kt @@ -0,0 +1,22 @@ +package me.rhunk.snapenhance.download.data + +import android.net.Uri + +enum class DownloadMediaType { + PROTO_MEDIA, + DIRECT_MEDIA, + REMOTE_MEDIA, + LOCAL_MEDIA; + + companion object { + fun fromUri(uri: Uri): DownloadMediaType { + return when (uri.scheme) { + "proto" -> PROTO_MEDIA + "direct" -> DIRECT_MEDIA + "http", "https" -> REMOTE_MEDIA + "file" -> LOCAL_MEDIA + else -> throw IllegalArgumentException("Unknown uri scheme: ${uri.scheme}") + } + } + } +}+ \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/download/data/DownloadRequest.kt b/core/src/main/kotlin/me/rhunk/snapenhance/download/data/DownloadRequest.kt @@ -1,7 +1,5 @@ package me.rhunk.snapenhance.download.data -import me.rhunk.snapenhance.download.enums.DownloadMediaType - data class DashOptions(val offsetTime: Long, val duration: Long?) data class InputMedia( diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/download/data/DownloadStage.kt b/core/src/main/kotlin/me/rhunk/snapenhance/download/data/DownloadStage.kt @@ -0,0 +1,14 @@ +package me.rhunk.snapenhance.download.data + +enum class DownloadStage( + val isFinalStage: Boolean = false, +) { + PENDING(false), + DOWNLOADING(false), + MERGING(false), + DOWNLOADED(true), + SAVED(true), + MERGE_FAILED(true), + FAILED(true), + CANCELLED(true) +}+ \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/download/data/PendingDownload.kt b/core/src/main/kotlin/me/rhunk/snapenhance/download/data/PendingDownload.kt @@ -2,15 +2,16 @@ package me.rhunk.snapenhance.download.data import kotlinx.coroutines.Job import me.rhunk.snapenhance.SharedContext -import me.rhunk.snapenhance.download.enums.DownloadStage +import me.rhunk.snapenhance.download.DownloadTaskManager data class PendingDownload( var downloadId: Int = 0, var outputFile: String? = null, - var job: Job? = null, - val metadata : DownloadMetadata ) { + lateinit var downloadTaskManager: DownloadTaskManager + var job: Job? = null + var changeListener = { _: DownloadStage, _: DownloadStage -> } private var _stage: DownloadStage = DownloadStage.PENDING var downloadStage: DownloadStage @@ -20,15 +21,13 @@ data class PendingDownload( set(value) = synchronized(this) { changeListener(_stage, value) _stage = value - SharedContext.downloadTaskManager.updateTask(this) + downloadTaskManager.updateTask(this) } - fun isJobActive(): Boolean { - return job?.isActive ?: false - } + fun isJobActive() = job?.isActive == true fun cancel() { - job?.cancel() downloadStage = DownloadStage.CANCELLED + job?.cancel() } } diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/download/enums/DownloadMediaType.kt b/core/src/main/kotlin/me/rhunk/snapenhance/download/enums/DownloadMediaType.kt @@ -1,22 +0,0 @@ -package me.rhunk.snapenhance.download.enums - -import android.net.Uri - -enum class DownloadMediaType { - PROTO_MEDIA, - DIRECT_MEDIA, - REMOTE_MEDIA, - LOCAL_MEDIA; - - companion object { - fun fromUri(uri: Uri): DownloadMediaType { - return when (uri.scheme) { - "proto" -> PROTO_MEDIA - "direct" -> DIRECT_MEDIA - "http", "https" -> REMOTE_MEDIA - "file" -> LOCAL_MEDIA - else -> throw IllegalArgumentException("Unknown uri scheme: ${uri.scheme}") - } - } - } -}- \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/download/enums/DownloadStage.kt b/core/src/main/kotlin/me/rhunk/snapenhance/download/enums/DownloadStage.kt @@ -1,14 +0,0 @@ -package me.rhunk.snapenhance.download.enums - -enum class DownloadStage( - val isFinalStage: Boolean = false, -) { - PENDING(false), - DOWNLOADING(false), - MERGING(false), - DOWNLOADED(true), - SAVED(true), - MERGE_FAILED(true), - FAILED(true), - CANCELLED(true) -}- \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/downloader/MediaDownloader.kt b/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/downloader/MediaDownloader.kt @@ -5,7 +5,6 @@ import android.graphics.Bitmap import android.graphics.BitmapFactory import android.net.Uri import android.widget.ImageView -import com.arthenica.ffmpegkit.FFmpegKit import kotlinx.coroutines.runBlocking import me.rhunk.snapenhance.Constants.ARROYO_URL_KEY_PROTO_PATH import me.rhunk.snapenhance.Logger @@ -20,11 +19,11 @@ import me.rhunk.snapenhance.data.wrapper.impl.media.opera.Layer import me.rhunk.snapenhance.data.wrapper.impl.media.opera.ParamMap import me.rhunk.snapenhance.database.objects.FriendInfo import me.rhunk.snapenhance.download.DownloadManagerClient +import me.rhunk.snapenhance.download.data.DownloadMediaType import me.rhunk.snapenhance.download.data.DownloadMetadata import me.rhunk.snapenhance.download.data.InputMedia import me.rhunk.snapenhance.download.data.SplitMediaAssetType import me.rhunk.snapenhance.download.data.toKeyPair -import me.rhunk.snapenhance.download.enums.DownloadMediaType import me.rhunk.snapenhance.features.Feature import me.rhunk.snapenhance.features.FeatureLoadParams import me.rhunk.snapenhance.features.impl.Messaging @@ -52,11 +51,8 @@ import kotlin.io.encoding.ExperimentalEncodingApi class MediaDownloader : Feature("MediaDownloader", loadParams = FeatureLoadParams.ACTIVITY_CREATE_ASYNC) { private var lastSeenMediaInfoMap: MutableMap<SplitMediaAssetType, MediaInfo>? = null private var lastSeenMapParams: ParamMap? = null - private val isFFmpegPresent by lazy { - runCatching { FFmpegKit.execute("-version") }.isSuccess - } - private fun provideClientDownloadManager( + private fun provideDownloadManagerClient( pathSuffix: String, mediaIdentifier: String, mediaDisplaySource: String? = null, @@ -115,10 +111,6 @@ class MediaDownloader : Feature("MediaDownloader", loadParams = FeatureLoadParam ) } - private fun canMergeOverlay(): Boolean { - 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 { @@ -246,7 +238,7 @@ class MediaDownloader : Feature("MediaDownloader", loadParams = FeatureLoadParam val author = context.database.getFriendInfo(senderId) ?: return val authorUsername = author.usernameForSorting!! - downloadOperaMedia(provideClientDownloadManager( + downloadOperaMedia(provideDownloadManagerClient( pathSuffix = authorUsername, mediaIdentifier = "$conversationId$senderId${conversationMessage.server_message_id}", mediaDisplaySource = authorUsername, @@ -286,7 +278,7 @@ class MediaDownloader : Feature("MediaDownloader", loadParams = FeatureLoadParam ) ?: throw Exception("Friend not found in database") val authorName = author.usernameForSorting!! - downloadOperaMedia(provideClientDownloadManager( + downloadOperaMedia(provideDownloadManagerClient( pathSuffix = authorName, mediaIdentifier = paramMap["MEDIA_ID"].toString(), mediaDisplaySource = authorName, @@ -305,7 +297,7 @@ class MediaDownloader : Feature("MediaDownloader", loadParams = FeatureLoadParam "[^\\x00-\\x7F]".toRegex(), "") - downloadOperaMedia(provideClientDownloadManager( + downloadOperaMedia(provideDownloadManagerClient( pathSuffix = "Public-Stories/$userDisplayName", mediaIdentifier = paramMap["SNAP_ID"].toString(), mediaDisplayType = userDisplayName, @@ -316,7 +308,7 @@ class MediaDownloader : Feature("MediaDownloader", loadParams = FeatureLoadParam //spotlight if (snapSource == "SINGLE_SNAP_STORY" && (forceDownload || canAutoDownload("spotlight"))) { - downloadOperaMedia(provideClientDownloadManager( + downloadOperaMedia(provideDownloadManagerClient( pathSuffix = "Spotlight", mediaIdentifier = paramMap["SNAP_ID"].toString(), mediaDisplayType = MediaFilter.SPOTLIGHT.mediaDisplayType, @@ -328,11 +320,6 @@ class MediaDownloader : Feature("MediaDownloader", loadParams = FeatureLoadParam //stories with mpeg dash media //TODO: option to download multiple chapters if (paramMap.containsKey("LONGFORM_VIDEO_PLAYLIST_ITEM") && forceDownload) { - if (!isFFmpegPresent) { - context.shortToast("Can't download media. ffmpeg was not found") - return - } - val storyName = paramMap["STORY_NAME"].toString().replace( "[^\\x00-\\x7F]".toRegex(), "") @@ -361,7 +348,7 @@ class MediaDownloader : Feature("MediaDownloader", loadParams = FeatureLoadParam } } - provideClientDownloadManager( + provideDownloadManagerClient( pathSuffix = "Pro-Stories/${storyName}", mediaIdentifier = "${paramMap["STORY_ID"]}-${snapItem.snapId}", mediaDisplaySource = storyName, @@ -396,10 +383,12 @@ class MediaDownloader : Feature("MediaDownloader", loadParams = FeatureLoadParam val mediaInfoMap = mutableMapOf<SplitMediaAssetType, MediaInfo>() val isVideo = mediaParamMap.containsKey("video_media_info_list") + val canMergeOverlay = context.config.downloader.autoDownloadOptions.get().contains("merge_overlay") + mediaInfoMap[SplitMediaAssetType.ORIGINAL] = MediaInfo( (if (isVideo) mediaParamMap["video_media_info_list"] else mediaParamMap["image_media_info"])!! ) - if (canMergeOverlay() && mediaParamMap.containsKey("overlay_image_media_info")) { + if (canMergeOverlay && mediaParamMap.containsKey("overlay_image_media_info")) { mediaInfoMap[SplitMediaAssetType.OVERLAY] = MediaInfo(mediaParamMap["overlay_image_media_info"]!!) } @@ -483,7 +472,7 @@ class MediaDownloader : Feature("MediaDownloader", loadParams = FeatureLoadParam runCatching { if (!isPreview) { val encryptionKeys = EncryptionHelper.getEncryptionKeys(contentType, messageReader, isArroyo = isArroyoMessage) - provideClientDownloadManager( + provideDownloadManagerClient( pathSuffix = authorName, mediaIdentifier = "${message.client_conversation_id}${message.sender_id}${message.server_message_id}", mediaDisplaySource = authorName, diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/ui/download/DownloadListAdapter.kt b/core/src/main/kotlin/me/rhunk/snapenhance/ui/download/DownloadListAdapter.kt @@ -27,7 +27,7 @@ import me.rhunk.snapenhance.SharedContext import me.rhunk.snapenhance.core.R import me.rhunk.snapenhance.data.FileType import me.rhunk.snapenhance.download.data.PendingDownload -import me.rhunk.snapenhance.download.enums.DownloadStage +import me.rhunk.snapenhance.download.data.DownloadStage import me.rhunk.snapenhance.util.snap.PreviewUtils import java.io.File import java.io.FileInputStream