mirror of
https://github.com/rhunk/SnapEnhance.git
synced 2025-05-03 16:04:30 +02:00
refactor: download
- download task manager - fix installation summary update
This commit is contained in:
parent
2ff8a69403
commit
3df11aadb8
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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() }
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@ -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() {
|
||||
|
||||
}
|
||||
}
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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"))
|
||||
}
|
||||
}
|
@ -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,
|
||||
|
@ -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) {
|
||||
|
@ -1,4 +1,4 @@
|
||||
package me.rhunk.snapenhance.download.enums
|
||||
package me.rhunk.snapenhance.download.data
|
||||
|
||||
import android.net.Uri
|
||||
|
@ -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(
|
||||
|
@ -1,4 +1,4 @@
|
||||
package me.rhunk.snapenhance.download.enums
|
||||
package me.rhunk.snapenhance.download.data
|
||||
|
||||
enum class DownloadStage(
|
||||
val isFinalStage: Boolean = false,
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user