Removed exporting service.

This commit is contained in:
Koen 2024-06-26 16:01:08 +02:00
parent c76ef7f19b
commit bc550ae8f5
8 changed files with 62 additions and 357 deletions

View file

@ -41,9 +41,6 @@
<service android:name=".services.DownloadService"
android:enabled="true"
android:foregroundServiceType="dataSync" />
<service android:name=".services.ExportingService"
android:enabled="true"
android:foregroundServiceType="dataSync" />
<receiver android:name=".receivers.MediaControlReceiver" />
<receiver android:name=".receivers.AudioNoisyReceiver" />

View file

@ -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<State>();
@kotlinx.serialization.Transient
val onProgressChanged = Event1<Double>();
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;
}

View file

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

View file

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

View file

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

View file

@ -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<PlaylistDownloadDescriptor>("playlistDownloads")
.load();
private val _exporting = FragmentedStorage.storeJson<VideoExport>("exporting")
.load();
private lateinit var _downloadedSet: HashSet<PlatformID>;
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<VideoExport> {
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";

View file

@ -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<String, List<StoreItem>>(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.");

View file

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