From bc550ae8f512350407708a307fd461154f53b7fc Mon Sep 17 00:00:00 2001 From: Koen Date: Wed, 26 Jun 2024 16:01:08 +0200 Subject: [PATCH] Removed exporting service. --- app/src/main/AndroidManifest.xml | 3 - .../platformplayer/downloads/VideoExport.kt | 43 ++-- .../mainactivity/main/DownloadsFragment.kt | 16 +- .../services/ExportingService.kt | 236 ------------------ .../futo/platformplayer/states/StateApp.kt | 3 - .../platformplayer/states/StateDownloads.kt | 106 +++----- .../platformplayer/views/MonetizationView.kt | 2 +- .../viewholders/VideoDownloadViewHolder.kt | 10 +- 8 files changed, 62 insertions(+), 357 deletions(-) delete mode 100644 app/src/main/java/com/futo/platformplayer/services/ExportingService.kt diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 49076988..be7cd437 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -41,9 +41,6 @@ - diff --git a/app/src/main/java/com/futo/platformplayer/downloads/VideoExport.kt b/app/src/main/java/com/futo/platformplayer/downloads/VideoExport.kt index 81f0fb09..7c1c4e09 100644 --- a/app/src/main/java/com/futo/platformplayer/downloads/VideoExport.kt +++ b/app/src/main/java/com/futo/platformplayer/downloads/VideoExport.kt @@ -1,47 +1,37 @@ package com.futo.platformplayer.downloads import android.content.Context -import android.net.Uri -import android.os.Environment import androidx.documentfile.provider.DocumentFile -import com.arthenica.ffmpegkit.* -import com.futo.platformplayer.api.media.models.streams.sources.* -import com.futo.platformplayer.constructs.Event1 +import com.arthenica.ffmpegkit.FFmpegKit +import com.arthenica.ffmpegkit.LogCallback +import com.arthenica.ffmpegkit.ReturnCode +import com.arthenica.ffmpegkit.StatisticsCallback +import com.futo.platformplayer.api.media.models.streams.sources.LocalAudioSource +import com.futo.platformplayer.api.media.models.streams.sources.LocalSubtitleSource +import com.futo.platformplayer.api.media.models.streams.sources.LocalVideoSource import com.futo.platformplayer.helpers.FileHelper.Companion.sanitizeFileName import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.states.StateApp import com.futo.platformplayer.toHumanBitrate -import kotlinx.coroutines.* -import java.io.* +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.suspendCancellableCoroutine +import kotlinx.coroutines.withContext +import java.io.File +import java.io.FileInputStream +import java.io.IOException +import java.io.OutputStream import java.util.UUID -import java.util.concurrent.CancellationException import java.util.concurrent.Executors import kotlin.coroutines.resumeWithException @kotlinx.serialization.Serializable class VideoExport { - var state: State = State.QUEUED; - var videoLocal: VideoLocal; var videoSource: LocalVideoSource?; var audioSource: LocalAudioSource?; var subtitleSource: LocalSubtitleSource?; - var progress: Double = 0.0; - var isCancelled = false; - - var error: String? = null; - - @kotlinx.serialization.Transient - val onStateChanged = Event1(); - @kotlinx.serialization.Transient - val onProgressChanged = Event1(); - - fun changeState(newState: State) { - state = newState; - onStateChanged.emit(newState); - } - constructor(videoLocal: VideoLocal, videoSource: LocalVideoSource?, audioSource: LocalAudioSource?, subtitleSource: LocalSubtitleSource?) { this.videoLocal = videoLocal; this.videoSource = videoSource; @@ -50,8 +40,6 @@ class VideoExport { } suspend fun export(context: Context, onProgress: ((Double) -> Unit)? = null): DocumentFile = coroutineScope { - if(isCancelled) throw CancellationException("Export got cancelled"); - val v = videoSource; val a = audioSource; val s = subtitleSource; @@ -107,7 +95,6 @@ class VideoExport { throw Exception("Cannot export when no audio or video source is set."); } - onProgressChanged.emit(100.0); return@coroutineScope outputFile; } diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/DownloadsFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/DownloadsFragment.kt index ae584d40..93b5e7e9 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/DownloadsFragment.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/DownloadsFragment.kt @@ -8,7 +8,7 @@ import android.widget.LinearLayout import android.widget.TextView import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.RecyclerView -import com.futo.platformplayer.* +import com.futo.platformplayer.R import com.futo.platformplayer.downloads.VideoDownload import com.futo.platformplayer.downloads.VideoLocal import com.futo.platformplayer.logging.Logger @@ -16,12 +16,13 @@ import com.futo.platformplayer.models.Playlist import com.futo.platformplayer.states.StateDownloads import com.futo.platformplayer.states.StatePlayer import com.futo.platformplayer.states.StatePlaylists +import com.futo.platformplayer.toHumanBytesSize import com.futo.platformplayer.views.AnyInsertedAdapterView import com.futo.platformplayer.views.AnyInsertedAdapterView.Companion.asAnyWithTop -import com.futo.platformplayer.views.others.ProgressBar import com.futo.platformplayer.views.adapters.viewholders.VideoDownloadViewHolder import com.futo.platformplayer.views.items.ActiveDownloadItem import com.futo.platformplayer.views.items.PlaylistDownloadItem +import com.futo.platformplayer.views.others.ProgressBar import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -64,16 +65,6 @@ class DownloadsFragment : MainFragment() { } } }; - StateDownloads.instance.onExportsChanged.subscribe(this) { - lifecycleScope.launch(Dispatchers.Main) { - try { - Logger.i(TAG, "Reloading UI for exports"); - _view?.reloadUI() - } catch (e: Throwable) { - Logger.e(TAG, "Failed to reload UI for exports", e) - } - } - }; } override fun onPause() { @@ -81,7 +72,6 @@ class DownloadsFragment : MainFragment() { StateDownloads.instance.onDownloadsChanged.remove(this); StateDownloads.instance.onDownloadedChanged.remove(this); - StateDownloads.instance.onExportsChanged.remove(this); } private class DownloadsView : LinearLayout { diff --git a/app/src/main/java/com/futo/platformplayer/services/ExportingService.kt b/app/src/main/java/com/futo/platformplayer/services/ExportingService.kt deleted file mode 100644 index a447f70c..00000000 --- a/app/src/main/java/com/futo/platformplayer/services/ExportingService.kt +++ /dev/null @@ -1,236 +0,0 @@ -package com.futo.platformplayer.services - -import android.app.NotificationChannel -import android.app.NotificationManager -import android.app.PendingIntent -import android.app.Service -import android.content.Context -import android.content.Intent -import android.content.pm.ServiceInfo -import android.os.Build -import android.os.IBinder -import androidx.core.app.NotificationCompat -import com.futo.platformplayer.R -import com.futo.platformplayer.activities.MainActivity -import com.futo.platformplayer.api.http.ManagedHttpClient -import com.futo.platformplayer.downloads.VideoExport -import com.futo.platformplayer.logging.Logger -import com.futo.platformplayer.share -import com.futo.platformplayer.states.Announcement -import com.futo.platformplayer.states.AnnouncementType -import com.futo.platformplayer.states.StateAnnouncement -import com.futo.platformplayer.states.StateDownloads -import com.futo.platformplayer.stores.FragmentedStorage -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.cancel -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import java.time.OffsetDateTime -import java.util.UUID - - -class ExportingService : Service() { - private val TAG = "ExportingService"; - - private val EXPORT_NOTIF_ID = 4; - private val EXPORT_NOTIF_TAG = "export"; - private val EXPORT_NOTIF_CHANNEL_ID = "exportChannel"; - private val EXPORT_NOTIF_CHANNEL_NAME = "Export"; - - //Context - private val _scope: CoroutineScope = CoroutineScope(Dispatchers.Default); - private var _notificationManager: NotificationManager? = null; - private var _notificationChannel: NotificationChannel? = null; - - private val _client = ManagedHttpClient(); - - private var _started = false; - - override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { - Logger.i(TAG, "onStartCommand"); - - synchronized(this) { - if(_started) - return START_STICKY; - - if(!FragmentedStorage.isInitialized) { - closeExportSession(); - return START_NOT_STICKY; - } - - _started = true; - } - setupNotificationRequirements(); - - _callOnStarted?.invoke(this); - _instance = this; - - _scope.launch { - try { - doExporting(); - } - catch(ex: Throwable) { - try { - StateAnnouncement.instance.registerAnnouncementSession( - Announcement( - "rootExportException", - "An root export service exception happened", - ex.message ?: "", - AnnouncementType.SESSION, - OffsetDateTime.now() - ) - ); - } catch(_: Throwable){} - } - }; - - return START_STICKY; - } - fun setupNotificationRequirements() { - _notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager; - _notificationChannel = NotificationChannel(EXPORT_NOTIF_CHANNEL_ID, EXPORT_NOTIF_CHANNEL_NAME, NotificationManager.IMPORTANCE_DEFAULT).apply { - this.enableVibration(false); - this.setSound(null, null); - }; - _notificationManager!!.createNotificationChannel(_notificationChannel!!); - } - - override fun onCreate() { - Logger.i(TAG, "onCreate"); - super.onCreate() - } - - override fun onBind(p0: Intent?): IBinder? { - return null; - } - - private suspend fun doExporting() { - Logger.i(TAG, "doExporting - Starting Exports"); - val ignore = mutableListOf(); - var currentExport: VideoExport? = StateDownloads.instance.getExporting().firstOrNull(); - while (currentExport != null) - { - try{ - notifyExport(currentExport); - doExport(applicationContext, currentExport); - } - catch(ex: Throwable) { - Logger.e(TAG, "Failed export [${currentExport.videoLocal.name}]: ${ex.message}", ex); - currentExport.error = ex.message; - currentExport.changeState(VideoExport.State.ERROR); - ignore.add(currentExport); - - //Give it a sec - Thread.sleep(500); - } - - currentExport = StateDownloads.instance.getExporting().filter { !ignore.contains(it) }.firstOrNull(); - } - Logger.i(TAG, "doExporting - Ending Exports"); - stopService(this); - } - - private suspend fun doExport(context: Context, export: VideoExport) { - Logger.i(TAG, "Exporting [${export.videoLocal.name}] started"); - - export.changeState(VideoExport.State.EXPORTING); - - var lastNotifyTime: Long = 0L; - val file = export.export(context) { progress -> - export.progress = progress; - - val currentTime = System.currentTimeMillis(); - if (currentTime - lastNotifyTime > 500) { - notifyExport(export); - lastNotifyTime = currentTime; - } - } - export.changeState(VideoExport.State.COMPLETED); - Logger.i(TAG, "Export [${export.videoLocal.name}] finished"); - StateDownloads.instance.removeExport(export); - notifyExport(export); - - withContext(Dispatchers.Main) { - StateAnnouncement.instance.registerAnnouncement(UUID.randomUUID().toString(), "File exported", "Exported [${file.uri}]", AnnouncementType.SESSION, time = null, category = "download", actionButton = "Open") { - file.share(this@ExportingService); - }; - } - } - - private fun notifyExport(export: VideoExport) { - val channel = _notificationChannel ?: return; - - val bringUpIntent = Intent(this, MainActivity::class.java); - bringUpIntent.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT); - bringUpIntent.action = "TAB"; - bringUpIntent.putExtra("TAB", "Exports"); - - var builder = NotificationCompat.Builder(this, EXPORT_NOTIF_TAG) - .setSmallIcon(R.drawable.ic_export) - .setOngoing(true) - .setSilent(true) - .setContentIntent(PendingIntent.getActivity(this, 5, bringUpIntent, PendingIntent.FLAG_IMMUTABLE)) - .setContentTitle("${export.state}: ${export.videoLocal.name}") - .setContentText(export.getExportInfo()) - .setProgress(100, (export.progress * 100).toInt(), export.progress == 0.0) - .setChannelId(channel.id) - - val notif = builder.build(); - notif.flags = notif.flags or NotificationCompat.FLAG_ONGOING_EVENT or NotificationCompat.FLAG_NO_CLEAR; - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - startForeground(EXPORT_NOTIF_ID, notif, ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC); - } else { - startForeground(EXPORT_NOTIF_ID, notif); - } - } - - fun closeExportSession() { - Logger.i(TAG, "closeExportSession"); - stopForeground(STOP_FOREGROUND_REMOVE); - _notificationManager?.cancel(EXPORT_NOTIF_ID); - stopService(); - _started = false; - super.stopSelf(); - } - override fun onDestroy() { - Logger.i(TAG, "onDestroy"); - _instance = null; - _scope.cancel("onDestroy"); - super.onDestroy(); - } - - companion object { - private var _instance: ExportingService? = null; - private var _callOnStarted: ((ExportingService)->Unit)? = null; - - @Synchronized - fun getOrCreateService(context: Context, handle: ((ExportingService)->Unit)? = null) { - if(!FragmentedStorage.isInitialized) - return; - if(_instance == null) { - _callOnStarted = handle; - val intent = Intent(context, ExportingService::class.java); - context.startForegroundService(intent); - } - else _instance?.let { - if(handle != null) - handle(it); - } - } - @Synchronized - fun getService() : ExportingService? { - return _instance; - } - - @Synchronized - fun stopService(service: ExportingService? = null) { - (service ?: _instance)?.let { - if(_instance == it) - _instance = null; - it.closeExportSession(); - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/futo/platformplayer/states/StateApp.kt b/app/src/main/java/com/futo/platformplayer/states/StateApp.kt index 7fd26d63..ae0f24a3 100644 --- a/app/src/main/java/com/futo/platformplayer/states/StateApp.kt +++ b/app/src/main/java/com/futo/platformplayer/states/StateApp.kt @@ -445,9 +445,6 @@ class StateApp { DownloadService.getOrCreateService(context); } - Logger.i(TAG, "MainApp Started: Check [Exports]"); - StateDownloads.instance.checkForExportTodos(); - Logger.i(TAG, "MainApp Started: Initialize [AutoUpdate]"); val autoUpdateEnabled = Settings.instance.autoUpdate.isAutoUpdateEnabled(); val shouldDownload = Settings.instance.autoUpdate.shouldDownload(); diff --git a/app/src/main/java/com/futo/platformplayer/states/StateDownloads.kt b/app/src/main/java/com/futo/platformplayer/states/StateDownloads.kt index edeb1859..c07f74b9 100644 --- a/app/src/main/java/com/futo/platformplayer/states/StateDownloads.kt +++ b/app/src/main/java/com/futo/platformplayer/states/StateDownloads.kt @@ -1,13 +1,13 @@ package com.futo.platformplayer.states import android.content.ContentResolver +import android.content.Context import android.os.StatFs import com.futo.platformplayer.R import com.futo.platformplayer.Settings import com.futo.platformplayer.UIDialogs import com.futo.platformplayer.api.http.ManagedHttpClient import com.futo.platformplayer.api.media.PlatformID -import com.futo.platformplayer.api.media.exceptions.AlreadyQueuedException import com.futo.platformplayer.api.media.models.streams.sources.IAudioUrlSource import com.futo.platformplayer.api.media.models.streams.sources.IVideoUrlSource import com.futo.platformplayer.api.media.models.streams.sources.LocalAudioSource @@ -27,10 +27,14 @@ import com.futo.platformplayer.models.DiskUsage import com.futo.platformplayer.models.Playlist import com.futo.platformplayer.models.PlaylistDownloaded import com.futo.platformplayer.services.DownloadService -import com.futo.platformplayer.services.ExportingService +import com.futo.platformplayer.share import com.futo.platformplayer.stores.FragmentedStorage import com.futo.platformplayer.stores.v2.ManagedStore +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import java.io.File +import java.util.UUID /*** * Used to maintain downloads @@ -50,12 +54,8 @@ class StateDownloads { private val _downloadPlaylists = FragmentedStorage.storeJson("playlistDownloads") .load(); - private val _exporting = FragmentedStorage.storeJson("exporting") - .load(); - private lateinit var _downloadedSet: HashSet; - val onExportsChanged = Event0(); val onDownloadsChanged = Event0(); val onDownloadedChanged = Event0(); @@ -457,17 +457,6 @@ class StateDownloads { } } - try { - val currentDownloads = _downloaded.getItems().map { it.url }.toHashSet(); - val exporting = _exporting.findItems { !currentDownloads.contains(it.videoLocal.url) }; - for (export in exporting) - _exporting.delete(export); - } - catch(ex: Throwable) { - Logger.e(TAG, "Failed to delete dangling export:", ex); - UIDialogs.toast("Failed to delete dangling export:\n" + ex); - } - return Pair(totalDeletedCount, totalDeleted); } @@ -475,66 +464,41 @@ class StateDownloads { return _downloadsDirectory; } + fun export(context: Context, videoLocal: VideoLocal, videoSource: LocalVideoSource?, audioSource: LocalAudioSource?, subtitleSource: LocalSubtitleSource?) { + var lastNotifyTime = -1L; + UIDialogs.showDialogProgress(context) { + it.setText("Exporting content.."); + it.setProgress(0f); + StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) { + val export = VideoExport(videoLocal, videoSource, audioSource, subtitleSource); + try { + Logger.i(TAG, "Exporting [${export.videoLocal.name}] started"); - //Export - fun getExporting(): List { - return _exporting.getItems(); - } - fun checkForExportTodos() { - if(_exporting.hasItems()) { - StateApp.withContext { - ExportingService.getOrCreateService(it); + val file = export.export(context) { progress -> + val now = System.currentTimeMillis(); + if (lastNotifyTime == -1L || now - lastNotifyTime > 100) { + it.setProgress(progress); + lastNotifyTime = now; + } + } + + withContext(Dispatchers.Main) { + it.setProgress(100.0f) + it.dismiss() + + StateAnnouncement.instance.registerAnnouncement(UUID.randomUUID().toString(), "File exported", "Exported [${file.uri}]", AnnouncementType.SESSION, time = null, category = "download", actionButton = "Open") { + file.share(context); + }; + } + } catch(ex: Throwable) { + Logger.e(TAG, "Failed export [${export.videoLocal.name}]: ${ex.message}", ex); + + } } } } - fun validateExport(export: VideoExport) { - if(_exporting.hasItem { it.videoLocal.url == export.videoLocal.url }) - throw AlreadyQueuedException("Video [${export.videoLocal.name}] is already queued for export"); - } - fun export(videoLocal: VideoLocal, videoSource: LocalVideoSource?, audioSource: LocalAudioSource?, subtitleSource: LocalSubtitleSource?, notify: Boolean = true) { - val shortName = if(videoLocal.name.length > 23) - videoLocal.name.substring(0, 20) + "..."; - else - videoLocal.name; - - val videoExport = VideoExport(videoLocal, videoSource, audioSource, subtitleSource); - - try { - validateExport(videoExport); - _exporting.save(videoExport); - - if(notify) { - UIDialogs.toast("Exporting [${shortName}]"); - StateApp.withContext { ExportingService.getOrCreateService(it) }; - onExportsChanged.emit(); - } - } - catch (ex: AlreadyQueuedException) { - Logger.e(TAG, "File is already queued for export.", ex); - StateApp.withContext { ExportingService.getOrCreateService(it) }; - } - catch(ex: Throwable) { - StateApp.withContext { - UIDialogs.showDialog( - it, - R.drawable.ic_error, - "Failed to start export due to:\n${ex.message}", null, null, - 0, - UIDialogs.Action("Ok", {}, UIDialogs.ActionStyle.PRIMARY) - ); - } - } - } - - - fun removeExport(export: VideoExport) { - _exporting.delete(export); - export.isCancelled = true; - onExportsChanged.emit(); - } - companion object { const val TAG = "StateDownloads"; diff --git a/app/src/main/java/com/futo/platformplayer/views/MonetizationView.kt b/app/src/main/java/com/futo/platformplayer/views/MonetizationView.kt index 7dc464a0..62da748b 100644 --- a/app/src/main/java/com/futo/platformplayer/views/MonetizationView.kt +++ b/app/src/main/java/com/futo/platformplayer/views/MonetizationView.kt @@ -1,7 +1,6 @@ package com.futo.platformplayer.views import android.content.Context -import android.content.Intent import android.net.Uri import android.util.AttributeSet import android.view.View @@ -49,6 +48,7 @@ class MonetizationView : LinearLayout { private val _taskLoadMerchandise = TaskHandler>(StateApp.instance.scopeGetter, { url -> val client = ManagedHttpClient(); + Logger.i(TAG, "Loading https://storecache.grayjay.app/StoreData?url=$url") val result = client.get("https://storecache.grayjay.app/StoreData?url=$url") if (!result.isOk) { throw Exception("Failed to retrieve store data."); diff --git a/app/src/main/java/com/futo/platformplayer/views/adapters/viewholders/VideoDownloadViewHolder.kt b/app/src/main/java/com/futo/platformplayer/views/adapters/viewholders/VideoDownloadViewHolder.kt index ae4e6ec9..5be5a4b0 100644 --- a/app/src/main/java/com/futo/platformplayer/views/adapters/viewholders/VideoDownloadViewHolder.kt +++ b/app/src/main/java/com/futo/platformplayer/views/adapters/viewholders/VideoDownloadViewHolder.kt @@ -16,6 +16,8 @@ import com.futo.platformplayer.states.StateApp import com.futo.platformplayer.states.StateDownloads import com.futo.platformplayer.states.StatePlayer import com.futo.platformplayer.views.adapters.AnyAdapter +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch class VideoDownloadViewHolder(_viewGroup: ViewGroup) : AnyAdapter.AnyViewHolder( @@ -57,10 +59,14 @@ class VideoDownloadViewHolder(_viewGroup: ViewGroup) : AnyAdapter.AnyViewHolder< return@changeExternalDownloadDirectory; } - StateDownloads.instance.export(v, v.videoSource.firstOrNull(), v.audioSource.firstOrNull(), v.subtitlesSources.firstOrNull()); + StateApp.instance.scopeOrNull?.launch(Dispatchers.Main) { + StateDownloads.instance.export(_viewGroup.context, v, v.videoSource.firstOrNull(), v.audioSource.firstOrNull(), v.subtitlesSources.firstOrNull()); + } }; } else { - StateDownloads.instance.export(v, v.videoSource.firstOrNull(), v.audioSource.firstOrNull(), v.subtitlesSources.firstOrNull()); + StateApp.instance.scopeOrNull?.launch(Dispatchers.Main) { + StateDownloads.instance.export(_viewGroup.context, v, v.videoSource.firstOrNull(), v.audioSource.firstOrNull(), v.subtitlesSources.firstOrNull()); + } } } }