refactor: download

- download task manager
- fix installation summary update
This commit is contained in:
rhunk 2023-08-04 12:17:19 +02:00
parent 2ff8a69403
commit 3df11aadb8
22 changed files with 152 additions and 180 deletions

View File

@ -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,18 +18,30 @@ 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 {
runCatching {
config.loadFromContext(androidContext)
translation.userLocale = config.locale
translation.loadFromContext(androidContext)
@ -36,6 +49,10 @@ class RemoteSideContext(
loadFromContext(androidContext)
init()
}
downloadTaskManager.init(androidContext)
}.onFailure {
Logger.error("Failed to initialize RemoteSideContext", it)
}
}
fun getInstallationSummary() = InstallationSummary(

View File

@ -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
}
}

View File

@ -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)
}
}
}

View File

@ -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)
}
}
}

View File

@ -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 {
sections = EnumSection.values().toList().associateWith {
runCatching {
it.section.constructors.first().call()
}.onFailure {
it.printStackTrace()
}.getOrThrow()
}.onEach { (section, instance) ->
with(instance) {
enumSection = section

View File

@ -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() }

View File

@ -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)
}
}
}

View File

@ -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() {
}
}

View File

@ -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"
}
}

View File

@ -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

View File

@ -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")
}
}
}

View File

@ -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())
}
}

View File

@ -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 {

View File

@ -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"))
}
}

View File

@ -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,

View File

@ -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) {

View File

@ -1,4 +1,4 @@
package me.rhunk.snapenhance.download.enums
package me.rhunk.snapenhance.download.data
import android.net.Uri

View File

@ -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(

View File

@ -1,4 +1,4 @@
package me.rhunk.snapenhance.download.enums
package me.rhunk.snapenhance.download.data
enum class DownloadStage(
val isFinalStage: Boolean = false,

View File

@ -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()
}
}

View File

@ -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,

View File

@ -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