mirror of
https://gitlab.futo.org/videostreaming/grayjay.git
synced 2026-05-16 04:52:39 +02:00
Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a792dea4c5 | |||
| a7fc549afb | |||
| b345ba5ca3 | |||
| c65cee86b1 | |||
| cf3fc61f6a | |||
| d03019f0b7 | |||
| f1ce0078fd | |||
| cd90497a59 |
+8
-1
@@ -1,3 +1,6 @@
|
|||||||
|
variables:
|
||||||
|
GIT_SUBMODULE_STRATEGY: recursive
|
||||||
|
|
||||||
buildAndDeployApkUnstable:
|
buildAndDeployApkUnstable:
|
||||||
stage: build
|
stage: build
|
||||||
script:
|
script:
|
||||||
@@ -31,7 +34,7 @@ buildAndDeployPlaystore:
|
|||||||
stage: deploy
|
stage: deploy
|
||||||
script:
|
script:
|
||||||
- sh build-playstore.sh
|
- sh build-playstore.sh
|
||||||
- bash tools/venv_playstore.sh
|
- bash venv-playstore.sh
|
||||||
- . .venv-playstore/bin/activate
|
- . .venv-playstore/bin/activate
|
||||||
- python publish_playstore.py --sa /root/grayjay.json --package com.futo.platformplayer.playstore --aab ./app/build/outputs/bundle/playstoreRelease/app-playstore-release.aab --track production --status completed
|
- python publish_playstore.py --sa /root/grayjay.json --package com.futo.platformplayer.playstore --aab ./app/build/outputs/bundle/playstoreRelease/app-playstore-release.aab --track production --status completed
|
||||||
only:
|
only:
|
||||||
@@ -53,5 +56,9 @@ updateFdroidRepo:
|
|||||||
needs:
|
needs:
|
||||||
- job: buildAndDeployApkStable
|
- job: buildAndDeployApkStable
|
||||||
artifacts: true
|
artifacts: true
|
||||||
|
before_script:
|
||||||
|
- mkdir -p ~/.ssh && chmod 700 ~/.ssh
|
||||||
|
- touch ~/.ssh/known_hosts && chmod 644 ~/.ssh/known_hosts
|
||||||
|
- ssh-keygen -F gitlab.futo.org >/dev/null 2>&1 || ssh-keyscan -t rsa,ecdsa,ed25519 gitlab.futo.org >> ~/.ssh/known_hosts
|
||||||
script:
|
script:
|
||||||
- python3 update_fdroid_index.py
|
- python3 update_fdroid_index.py
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ import com.futo.platformplayer.api.media.models.video.IPlatformVideo
|
|||||||
import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
|
import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
|
||||||
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo
|
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo
|
||||||
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
||||||
|
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSSource
|
||||||
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSDashManifestRawAudioSource
|
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSDashManifestRawAudioSource
|
||||||
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSDashManifestRawSource
|
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSDashManifestRawSource
|
||||||
import com.futo.platformplayer.downloads.VideoLocal
|
import com.futo.platformplayer.downloads.VideoLocal
|
||||||
@@ -382,7 +383,8 @@ class UISlideOverlays {
|
|||||||
val slideUpMenuOverlay = SlideUpMenuOverlay(container.context, container, container.context.getString(R.string.download_video), null, true, items)
|
val slideUpMenuOverlay = SlideUpMenuOverlay(container.context, container, container.context.getString(R.string.download_video), null, true, items)
|
||||||
|
|
||||||
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
|
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
|
||||||
val masterPlaylistResponse = ManagedHttpClient().get(sourceUrl)
|
val modifier = if (source is JSSource && source.hasRequestModifier) source.getRequestModifier() else null
|
||||||
|
val masterPlaylistResponse = ManagedHttpClient().get(sourceUrl, HashMap(), modifier)
|
||||||
check(masterPlaylistResponse.isOk) { "Failed to get master playlist: ${masterPlaylistResponse.code}" }
|
check(masterPlaylistResponse.isOk) { "Failed to get master playlist: ${masterPlaylistResponse.code}" }
|
||||||
|
|
||||||
val masterPlaylistContent = masterPlaylistResponse.body?.string()
|
val masterPlaylistContent = masterPlaylistResponse.body?.string()
|
||||||
@@ -515,7 +517,7 @@ class UISlideOverlays {
|
|||||||
|
|
||||||
slideUpMenuOverlay.onOK.subscribe {
|
slideUpMenuOverlay.onOK.subscribe {
|
||||||
//TODO: Fix SubtitleRawSource issue
|
//TODO: Fix SubtitleRawSource issue
|
||||||
StateDownloads.instance.download(video, selectedVideoVariant, selectedAudioVariant, null);
|
StateDownloads.instance.download(video, selectedVideoVariant, selectedAudioVariant, null, videoModifier = modifier, audioModifier = modifier);
|
||||||
slideUpMenuOverlay.hide()
|
slideUpMenuOverlay.hide()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -526,11 +528,11 @@ class UISlideOverlays {
|
|||||||
if (masterPlaylistContent.lines().any { it.startsWith("#EXTINF:") }) {
|
if (masterPlaylistContent.lines().any { it.startsWith("#EXTINF:") }) {
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
if (source is IHLSManifestSource) {
|
if (source is IHLSManifestSource) {
|
||||||
StateDownloads.instance.download(video, HLSVariantVideoUrlSource("variant", 0, 0, "application/vnd.apple.mpegurl", "", null, 0, false, resolvedPlaylistUrl), null, null)
|
StateDownloads.instance.download(video, HLSVariantVideoUrlSource("variant", 0, 0, "application/vnd.apple.mpegurl", "", null, 0, false, resolvedPlaylistUrl), null, null, videoModifier = modifier)
|
||||||
UIDialogs.toast(container.context, "Variant video HLS playlist download started")
|
UIDialogs.toast(container.context, "Variant video HLS playlist download started")
|
||||||
slideUpMenuOverlay.hide()
|
slideUpMenuOverlay.hide()
|
||||||
} else if (source is IHLSManifestAudioSource) {
|
} else if (source is IHLSManifestAudioSource) {
|
||||||
StateDownloads.instance.download(video, null, HLSVariantAudioUrlSource("variant", 0, "application/vnd.apple.mpegurl", "", "", null, false, false, resolvedPlaylistUrl), null)
|
StateDownloads.instance.download(video, null, HLSVariantAudioUrlSource("variant", 0, "application/vnd.apple.mpegurl", "", "", null, false, false, resolvedPlaylistUrl), null, audioModifier = modifier)
|
||||||
UIDialogs.toast(container.context, "Variant audio HLS playlist download started")
|
UIDialogs.toast(container.context, "Variant audio HLS playlist download started")
|
||||||
slideUpMenuOverlay.hide()
|
slideUpMenuOverlay.hide()
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -5,50 +5,14 @@ import android.content.Context
|
|||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import androidx.core.app.NotificationManagerCompat
|
import androidx.core.app.NotificationManagerCompat
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import com.futo.platformplayer.activities.MainActivity
|
|
||||||
import com.futo.platformplayer.dialogs.AutoUpdateDialog
|
|
||||||
import com.futo.platformplayer.states.StateApp
|
|
||||||
import java.io.File
|
|
||||||
|
|
||||||
class UpdateActionReceiver : BroadcastReceiver() {
|
class UpdateActionReceiver : BroadcastReceiver() {
|
||||||
override fun onReceive(context: Context, intent: Intent) {
|
override fun onReceive(context: Context, intent: Intent) {
|
||||||
when (intent.action) {
|
when (intent.action) {
|
||||||
UpdateNotificationManager.ACTION_UPDATE_YES -> handleUpdateYes(context, intent)
|
|
||||||
UpdateNotificationManager.ACTION_UPDATE_NO -> handleUpdateNo(context)
|
|
||||||
UpdateNotificationManager.ACTION_UPDATE_NEVER -> handleUpdateNever(context)
|
|
||||||
UpdateNotificationManager.ACTION_DOWNLOAD_CANCEL -> handleDownloadCancel(context, intent)
|
UpdateNotificationManager.ACTION_DOWNLOAD_CANCEL -> handleDownloadCancel(context, intent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleUpdateYes(context: Context, intent: Intent) {
|
|
||||||
AutoUpdateDialog.currentDialog?.dismiss()
|
|
||||||
|
|
||||||
val version = intent.getIntExtra(UpdateNotificationManager.EXTRA_VERSION, 0)
|
|
||||||
if (version == 0) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
NotificationManagerCompat.from(context).cancel(UpdateNotificationManager.NOTIF_ID_AVAILABLE)
|
|
||||||
|
|
||||||
val serviceIntent = Intent(context, UpdateDownloadService::class.java).apply {
|
|
||||||
putExtra(UpdateDownloadService.EXTRA_VERSION, version)
|
|
||||||
}
|
|
||||||
ContextCompat.startForegroundService(context, serviceIntent)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun handleUpdateNo(context: Context) {
|
|
||||||
AutoUpdateDialog.currentDialog?.dismiss()
|
|
||||||
NotificationManagerCompat.from(context).cancel(UpdateNotificationManager.NOTIF_ID_AVAILABLE)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun handleUpdateNever(context: Context) {
|
|
||||||
AutoUpdateDialog.currentDialog?.dismiss()
|
|
||||||
Settings.instance.autoUpdate.check = 1
|
|
||||||
Settings.instance.save()
|
|
||||||
|
|
||||||
UpdateNotificationManager.cancelAll(context)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun handleDownloadCancel(context: Context, intent: Intent) {
|
private fun handleDownloadCancel(context: Context, intent: Intent) {
|
||||||
val version = intent.getIntExtra(UpdateNotificationManager.EXTRA_VERSION, 0)
|
val version = intent.getIntExtra(UpdateNotificationManager.EXTRA_VERSION, 0)
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
package com.futo.platformplayer
|
package com.futo.platformplayer
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.work.CoroutineWorker
|
import androidx.work.CoroutineWorker
|
||||||
import androidx.work.WorkerParameters
|
import androidx.work.WorkerParameters
|
||||||
import com.futo.platformplayer.api.http.ManagedHttpClient
|
import com.futo.platformplayer.api.http.ManagedHttpClient
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.states.StateApp
|
|
||||||
import com.futo.platformplayer.states.StateUpdate
|
import com.futo.platformplayer.states.StateUpdate
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
@@ -35,18 +36,16 @@ class UpdateCheckWorker(appContext: Context, workerParams: WorkerParameters) : C
|
|||||||
return@withContext Result.success()
|
return@withContext Result.success()
|
||||||
}
|
}
|
||||||
|
|
||||||
UpdateNotificationManager.showUpdateAvailableNotification(applicationContext, latestVersion)
|
StateUpdate.Companion.instance.setUiAvailable(latestVersion)
|
||||||
|
|
||||||
if (StateApp.instance.isMainActive) {
|
try {
|
||||||
withContext(Dispatchers.Main) {
|
val serviceIntent = Intent(applicationContext, UpdateDownloadService::class.java).apply {
|
||||||
StateApp.withContext { ctx ->
|
putExtra(UpdateDownloadService.EXTRA_VERSION, latestVersion)
|
||||||
try {
|
|
||||||
UIDialogs.showUpdateAvailableDialog(ctx, latestVersion, false)
|
|
||||||
} catch (t: Throwable) {
|
|
||||||
Logger.w(TAG, "Failed to show in-app update dialog from worker", t)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
ContextCompat.startForegroundService(applicationContext, serviceIntent)
|
||||||
|
} catch (t: Throwable) {
|
||||||
|
Logger.w(TAG, "Failed to start UpdateDownloadService", t)
|
||||||
|
StateUpdate.Companion.instance.setUiFailed(latestVersion, t.message)
|
||||||
}
|
}
|
||||||
|
|
||||||
Result.success()
|
Result.success()
|
||||||
|
|||||||
@@ -1,17 +1,15 @@
|
|||||||
package com.futo.platformplayer
|
package com.futo.platformplayer
|
||||||
|
|
||||||
import android.app.Dialog
|
|
||||||
import android.app.Service
|
import android.app.Service
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
import android.os.SystemClock
|
import android.os.SystemClock
|
||||||
import com.futo.platformplayer.UIDialogs.ActionStyle
|
import androidx.core.app.NotificationManagerCompat
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.models.ImageVariable
|
import com.futo.platformplayer.models.ImageVariable
|
||||||
import com.futo.platformplayer.states.AnnouncementType
|
import com.futo.platformplayer.states.AnnouncementType
|
||||||
import com.futo.platformplayer.states.SessionAnnouncement
|
import com.futo.platformplayer.states.SessionAnnouncement
|
||||||
import com.futo.platformplayer.states.StateAnnouncement
|
import com.futo.platformplayer.states.StateAnnouncement
|
||||||
import com.futo.platformplayer.states.StateApp
|
|
||||||
import com.futo.platformplayer.states.StateUpdate
|
import com.futo.platformplayer.states.StateUpdate
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
import java.io.File
|
import java.io.File
|
||||||
@@ -30,8 +28,6 @@ class UpdateDownloadService : Service() {
|
|||||||
private const val INITIAL_BACKOFF_MS = 5_000L
|
private const val INITIAL_BACKOFF_MS = 5_000L
|
||||||
private const val BUFFER_SIZE = 8 * 1024
|
private const val BUFFER_SIZE = 8 * 1024
|
||||||
private const val MIN_PROGRESS_UPDATE_INTERVAL_MS = 500L
|
private const val MIN_PROGRESS_UPDATE_INTERVAL_MS = 500L
|
||||||
|
|
||||||
var updateDownloadedDialog: Dialog? = null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private val job = SupervisorJob()
|
private val job = SupervisorJob()
|
||||||
@@ -56,6 +52,7 @@ class UpdateDownloadService : Service() {
|
|||||||
if (intent.getBooleanExtra(EXTRA_CANCEL, false)) {
|
if (intent.getBooleanExtra(EXTRA_CANCEL, false)) {
|
||||||
cancelRequested = true
|
cancelRequested = true
|
||||||
Logger.i(TAG, "Download cancel requested")
|
Logger.i(TAG, "Download cancel requested")
|
||||||
|
StateUpdate.Companion.instance.clearUi()
|
||||||
stopForeground(Service.STOP_FOREGROUND_REMOVE)
|
stopForeground(Service.STOP_FOREGROUND_REMOVE)
|
||||||
stopSelf()
|
stopSelf()
|
||||||
return START_NOT_STICKY
|
return START_NOT_STICKY
|
||||||
@@ -75,6 +72,10 @@ class UpdateDownloadService : Service() {
|
|||||||
isDownloading = true
|
isDownloading = true
|
||||||
cancelRequested = false
|
cancelRequested = false
|
||||||
|
|
||||||
|
StateUpdate.Companion.instance.setUiDownloading(version, 0, indeterminate = true)
|
||||||
|
|
||||||
|
NotificationManagerCompat.from(this).cancel(UpdateNotificationManager.NOTIF_ID_READY)
|
||||||
|
|
||||||
val notification = UpdateNotificationManager.buildDownloadProgressNotification(this, version, 0, true)
|
val notification = UpdateNotificationManager.buildDownloadProgressNotification(this, version, 0, true)
|
||||||
startForeground(UpdateNotificationManager.NOTIF_ID_DOWNLOADING, notification)
|
startForeground(UpdateNotificationManager.NOTIF_ID_DOWNLOADING, notification)
|
||||||
|
|
||||||
@@ -97,6 +98,7 @@ class UpdateDownloadService : Service() {
|
|||||||
if (force || now - lastProgressUpdateElapsedMs >= MIN_PROGRESS_UPDATE_INTERVAL_MS) {
|
if (force || now - lastProgressUpdateElapsedMs >= MIN_PROGRESS_UPDATE_INTERVAL_MS) {
|
||||||
lastProgressUpdateElapsedMs = now
|
lastProgressUpdateElapsedMs = now
|
||||||
UpdateNotificationManager.updateDownloadProgress(this, version, progress, indeterminate);
|
UpdateNotificationManager.updateDownloadProgress(this, version, progress, indeterminate);
|
||||||
|
StateUpdate.Companion.instance.setUiDownloading(version, progress, indeterminate)
|
||||||
|
|
||||||
if(onProgress != null)
|
if(onProgress != null)
|
||||||
onProgress.invoke(progress);
|
onProgress.invoke(progress);
|
||||||
@@ -132,7 +134,7 @@ class UpdateDownloadService : Service() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
performDownload(StateUpdate.APK_URL, partialFile, version, {
|
performDownload(StateUpdate.getApkUrl(version), partialFile, version, {
|
||||||
try {
|
try {
|
||||||
if (announcement != null)
|
if (announcement != null)
|
||||||
announcement?.setProgress(it);
|
announcement?.setProgress(it);
|
||||||
@@ -159,6 +161,7 @@ class UpdateDownloadService : Service() {
|
|||||||
if (attempt == MAX_RETRIES - 1) {
|
if (attempt == MAX_RETRIES - 1) {
|
||||||
Logger.e(TAG, "Download failed after ${attempt + 1} attempts", t)
|
Logger.e(TAG, "Download failed after ${attempt + 1} attempts", t)
|
||||||
UpdateNotificationManager.showDownloadFailedNotification(this, version, t)
|
UpdateNotificationManager.showDownloadFailedNotification(this, version, t)
|
||||||
|
StateUpdate.Companion.instance.setUiFailed(version, t.message)
|
||||||
break
|
break
|
||||||
} else {
|
} else {
|
||||||
Logger.w(TAG, "Download attempt ${attempt + 1} failed, retrying in ${backoffMs / 1000}s", t)
|
Logger.w(TAG, "Download attempt ${attempt + 1} failed, retrying in ${backoffMs / 1000}s", t)
|
||||||
@@ -264,39 +267,16 @@ class UpdateDownloadService : Service() {
|
|||||||
private fun onDownloadComplete(version: Int, apkFile: File) {
|
private fun onDownloadComplete(version: Int, apkFile: File) {
|
||||||
Logger.i(TAG, "Download complete for version=$version, file=${apkFile.absolutePath}")
|
Logger.i(TAG, "Download complete for version=$version, file=${apkFile.absolutePath}")
|
||||||
UpdateNotificationManager.showDownloadCompleteNotification(this, version, apkFile)
|
UpdateNotificationManager.showDownloadCompleteNotification(this, version, apkFile)
|
||||||
|
StateUpdate.Companion.instance.setUiReady(version, apkFile)
|
||||||
|
|
||||||
if (StateApp.instance.isMainActive) {
|
try {
|
||||||
StateApp.instance.scopeOrNull?.launch(Dispatchers.Main) {
|
val ctx = applicationContext
|
||||||
StateApp.withContext { ctx ->
|
StateAnnouncement.instance.registerAnnouncement("install-update-apk", "Grayjay v${version} is ready!", "You can now install the new Grayjay version.", AnnouncementType.SESSION, OffsetDateTime.now(), "update", "Install") {
|
||||||
try {
|
UpdateNotificationManager.cancelAll(ctx)
|
||||||
updateDownloadedDialog = UIDialogs.showDialog(ctx, R.drawable.foreground,
|
UpdateInstaller.startInstall(ctx, version, apkFile)
|
||||||
"Update downloaded",
|
|
||||||
"Would you like to install it now?", null, 0,
|
|
||||||
UIDialogs.Action("Not now", {
|
|
||||||
updateDownloadedDialog = null
|
|
||||||
}, ActionStyle.NONE, true),
|
|
||||||
UIDialogs.Action("Install", {
|
|
||||||
UpdateNotificationManager.cancelAll(ctx)
|
|
||||||
UpdateInstaller.startInstall(ctx, version, apkFile)
|
|
||||||
}, ActionStyle.PRIMARY, true));
|
|
||||||
|
|
||||||
try {
|
|
||||||
StateAnnouncement.instance.registerAnnouncement("install-update-apk", "Grayjay v${version} is ready!", "You can now install the new Grayjay version.",
|
|
||||||
AnnouncementType.SESSION,
|
|
||||||
OffsetDateTime.now(), "update", "Install", {
|
|
||||||
UpdateNotificationManager.cancelAll(ctx)
|
|
||||||
UpdateInstaller.startInstall(ctx, version, apkFile)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
catch(ex: Throwable) {
|
|
||||||
|
|
||||||
}
|
|
||||||
} catch (t: Throwable) {
|
|
||||||
Logger.w(TAG, "Failed to show in-app update downloaded dialog", t)
|
|
||||||
updateDownloadedDialog = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
} catch (ex: Throwable) {
|
||||||
|
Logger.w(TAG, "Failed to register install announcement", ex)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import java.io.InputStream
|
|||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import com.futo.platformplayer.dialogs.AutoUpdateDialog
|
import com.futo.platformplayer.dialogs.AutoUpdateDialog
|
||||||
import com.futo.platformplayer.states.StateApp
|
import com.futo.platformplayer.states.StateApp
|
||||||
|
import com.futo.platformplayer.states.StateUpdate
|
||||||
|
|
||||||
object UpdateInstaller {
|
object UpdateInstaller {
|
||||||
private const val TAG = "UpdateInstaller"
|
private const val TAG = "UpdateInstaller"
|
||||||
@@ -61,6 +62,17 @@ object UpdateInstaller {
|
|||||||
var inputStream: InputStream? = null
|
var inputStream: InputStream? = null
|
||||||
var session: PackageInstaller.Session? = null
|
var session: PackageInstaller.Session? = null
|
||||||
try {
|
try {
|
||||||
|
val dataLength = apkFile.length()
|
||||||
|
val usable = try { context.filesDir.usableSpace } catch (_: Throwable) { -1L }
|
||||||
|
if (usable in 0 until dataLength) {
|
||||||
|
val msg = "Not enough storage to install update. Need ${dataLength / 1_048_576L}MB, have ${usable / 1_048_576L}MB free."
|
||||||
|
Logger.w(TAG, msg)
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
UIDialogs.toast(context, msg)
|
||||||
|
}
|
||||||
|
UpdateNotificationManager.showInstallFailedNotification(context, version, apkFile, msg)
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
|
||||||
val packageInstaller: PackageInstaller = context.packageManager.packageInstaller
|
val packageInstaller: PackageInstaller = context.packageManager.packageInstaller
|
||||||
val params = PackageInstaller.SessionParams(PackageInstaller.SessionParams.MODE_FULL_INSTALL)
|
val params = PackageInstaller.SessionParams(PackageInstaller.SessionParams.MODE_FULL_INSTALL)
|
||||||
@@ -68,7 +80,6 @@ object UpdateInstaller {
|
|||||||
session = packageInstaller.openSession(sessionId)
|
session = packageInstaller.openSession(sessionId)
|
||||||
|
|
||||||
inputStream = apkFile.inputStream()
|
inputStream = apkFile.inputStream()
|
||||||
val dataLength = apkFile.length()
|
|
||||||
|
|
||||||
session.openWrite("package", 0, dataLength).use { sessionStream ->
|
session.openWrite("package", 0, dataLength).use { sessionStream ->
|
||||||
inputStream.copyToOutputStream(dataLength, sessionStream) { _ -> }
|
inputStream.copyToOutputStream(dataLength, sessionStream) { _ -> }
|
||||||
@@ -91,11 +102,18 @@ object UpdateInstaller {
|
|||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
Logger.w(TAG, "Exception while installing update", e)
|
Logger.w(TAG, "Exception while installing update", e)
|
||||||
session?.abandon()
|
session?.abandon()
|
||||||
|
|
||||||
|
val raw = e.message ?: ""
|
||||||
|
val friendly = if (raw.contains("Failed to allocate") || raw.contains("allocatable") || raw.contains("ENOSPC", ignoreCase = true)) {
|
||||||
|
"Not enough storage to install update. Free up some space and try again."
|
||||||
|
} else {
|
||||||
|
"Failed to install update: $raw"
|
||||||
|
}
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
UIDialogs.toast(context, "Failed to install update: ${e.message}")
|
UIDialogs.toast(context, friendly)
|
||||||
}
|
}
|
||||||
|
|
||||||
UpdateNotificationManager.showInstallFailedNotification(context, version, apkFile, e.message)
|
UpdateNotificationManager.showInstallFailedNotification(context, version, apkFile, friendly)
|
||||||
} finally {
|
} finally {
|
||||||
session?.close()
|
session?.close()
|
||||||
inputStream?.close()
|
inputStream?.close()
|
||||||
@@ -110,10 +128,12 @@ object UpdateInstaller {
|
|||||||
if (result.isNullOrEmpty()) {
|
if (result.isNullOrEmpty()) {
|
||||||
Logger.i(TAG, "Update install finished successfully")
|
Logger.i(TAG, "Update install finished successfully")
|
||||||
UpdateNotificationManager.showInstallSucceededNotification(context, version)
|
UpdateNotificationManager.showInstallSucceededNotification(context, version)
|
||||||
|
StateUpdate.instance.clearUi()
|
||||||
} else {
|
} else {
|
||||||
Logger.w(TAG, "Update install failed: $result")
|
Logger.w(TAG, "Update install failed: $result")
|
||||||
UpdateNotificationManager.showInstallFailedNotification(context, version, apkFile, result)
|
UpdateNotificationManager.showInstallFailedNotification(context, version, apkFile, result)
|
||||||
UIDialogs.showGeneralErrorDialog(context, "Install failed due to:\n$result")
|
UIDialogs.showGeneralErrorDialog(context, "Install failed due to:\n$result")
|
||||||
|
StateUpdate.instance.setUiReady(version, apkFile)
|
||||||
}
|
}
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
Logger.e(TAG, "Failed to handle install result", e)
|
Logger.e(TAG, "Failed to handle install result", e)
|
||||||
|
|||||||
@@ -22,9 +22,6 @@ object UpdateNotificationManager {
|
|||||||
private const val CHANNEL_NAME = "App updates"
|
private const val CHANNEL_NAME = "App updates"
|
||||||
private const val CHANNEL_DESCRIPTION = "Notifications about new app versions"
|
private const val CHANNEL_DESCRIPTION = "Notifications about new app versions"
|
||||||
|
|
||||||
const val ACTION_UPDATE_YES = "com.futo.platformplayer.UPDATE_YES"
|
|
||||||
const val ACTION_UPDATE_NO = "com.futo.platformplayer.UPDATE_NO"
|
|
||||||
const val ACTION_UPDATE_NEVER = "com.futo.platformplayer.UPDATE_NEVER"
|
|
||||||
const val ACTION_DOWNLOAD_CANCEL = "com.futo.platformplayer.UPDATE_CANCEL"
|
const val ACTION_DOWNLOAD_CANCEL = "com.futo.platformplayer.UPDATE_CANCEL"
|
||||||
const val ACTION_INSTALL_NOW = "com.futo.platformplayer.UPDATE_INSTALL"
|
const val ACTION_INSTALL_NOW = "com.futo.platformplayer.UPDATE_INSTALL"
|
||||||
private const val REQUEST_CODE_INSTALL = 1001
|
private const val REQUEST_CODE_INSTALL = 1001
|
||||||
@@ -32,7 +29,6 @@ object UpdateNotificationManager {
|
|||||||
const val EXTRA_VERSION = "version"
|
const val EXTRA_VERSION = "version"
|
||||||
const val EXTRA_APK_PATH = "apk_path"
|
const val EXTRA_APK_PATH = "apk_path"
|
||||||
|
|
||||||
const val NOTIF_ID_AVAILABLE = 2001
|
|
||||||
const val NOTIF_ID_DOWNLOADING = 2002
|
const val NOTIF_ID_DOWNLOADING = 2002
|
||||||
const val NOTIF_ID_READY = 2003
|
const val NOTIF_ID_READY = 2003
|
||||||
const val NOTIF_ID_INSTALL_FAILED = 2004
|
const val NOTIF_ID_INSTALL_FAILED = 2004
|
||||||
@@ -84,43 +80,6 @@ object UpdateNotificationManager {
|
|||||||
NotificationManagerCompat.from(context).notify(NOTIF_ID_INSTALL_SUCCEEDED, builder.build())
|
NotificationManagerCompat.from(context).notify(NOTIF_ID_INSTALL_SUCCEEDED, builder.build())
|
||||||
}
|
}
|
||||||
|
|
||||||
fun showUpdateAvailableNotification(context: Context, version: Int) {
|
|
||||||
if (ContextCompat.checkSelfPermission(context, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ensureChannel(context)
|
|
||||||
|
|
||||||
val yesIntent = Intent(context, UpdateActionReceiver::class.java).apply {
|
|
||||||
action = ACTION_UPDATE_YES
|
|
||||||
putExtra(EXTRA_VERSION, version)
|
|
||||||
}
|
|
||||||
val yesPendingIntent = getBroadcast(context, 0, yesIntent, FLAG_MUTABLE or FLAG_UPDATE_CURRENT)
|
|
||||||
val noIntent = Intent(context, UpdateActionReceiver::class.java).apply {
|
|
||||||
action = ACTION_UPDATE_NO
|
|
||||||
putExtra(EXTRA_VERSION, version)
|
|
||||||
}
|
|
||||||
val noPendingIntent = getBroadcast(context, 1, noIntent, FLAG_MUTABLE or FLAG_UPDATE_CURRENT)
|
|
||||||
val neverIntent = Intent(context, UpdateActionReceiver::class.java).apply {
|
|
||||||
action = ACTION_UPDATE_NEVER
|
|
||||||
putExtra(EXTRA_VERSION, version)
|
|
||||||
}
|
|
||||||
val neverPendingIntent = getBroadcast(context, 2, neverIntent, FLAG_MUTABLE or FLAG_UPDATE_CURRENT)
|
|
||||||
val builder = NotificationCompat.Builder(context, CHANNEL_ID)
|
|
||||||
.setSmallIcon(R.drawable.foreground)
|
|
||||||
.setContentTitle("Update available")
|
|
||||||
.setContentText("A new version ($version) is available.")
|
|
||||||
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
|
|
||||||
.setAutoCancel(true)
|
|
||||||
.setContentIntent(yesPendingIntent)
|
|
||||||
.setSilent(true)
|
|
||||||
.addAction(0, "Never", neverPendingIntent)
|
|
||||||
.addAction(0, "Not now", noPendingIntent)
|
|
||||||
.addAction(0, "Download", yesPendingIntent)
|
|
||||||
|
|
||||||
NotificationManagerCompat.from(context).notify(NOTIF_ID_AVAILABLE, builder.build())
|
|
||||||
}
|
|
||||||
|
|
||||||
fun buildDownloadProgressNotification(context: Context, version: Int, progress: Int, indeterminate: Boolean): Notification {
|
fun buildDownloadProgressNotification(context: Context, version: Int, progress: Int, indeterminate: Boolean): Notification {
|
||||||
ensureChannel(context)
|
ensureChannel(context)
|
||||||
|
|
||||||
@@ -223,11 +182,9 @@ object UpdateNotificationManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun cancelAll(context: Context) {
|
fun cancelAll(context: Context) {
|
||||||
NotificationManagerCompat.from(context).cancel(NOTIF_ID_AVAILABLE)
|
|
||||||
NotificationManagerCompat.from(context).cancel(NOTIF_ID_DOWNLOADING)
|
NotificationManagerCompat.from(context).cancel(NOTIF_ID_DOWNLOADING)
|
||||||
NotificationManagerCompat.from(context).cancel(NOTIF_ID_READY)
|
NotificationManagerCompat.from(context).cancel(NOTIF_ID_READY)
|
||||||
NotificationManagerCompat.from(context).cancel(NOTIF_ID_INSTALL_FAILED)
|
NotificationManagerCompat.from(context).cancel(NOTIF_ID_INSTALL_FAILED)
|
||||||
NotificationManagerCompat.from(context).cancel(NOTIF_ID_INSTALL_SUCCEEDED)
|
NotificationManagerCompat.from(context).cancel(NOTIF_ID_INSTALL_SUCCEEDED)
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import java.time.Duration
|
|||||||
import javax.net.ssl.SSLContext
|
import javax.net.ssl.SSLContext
|
||||||
import javax.net.ssl.TrustManager
|
import javax.net.ssl.TrustManager
|
||||||
import javax.net.ssl.X509TrustManager
|
import javax.net.ssl.X509TrustManager
|
||||||
|
import com.futo.platformplayer.api.media.models.modifier.IRequestModifier
|
||||||
import kotlin.system.measureTimeMillis
|
import kotlin.system.measureTimeMillis
|
||||||
|
|
||||||
open class ManagedHttpClient {
|
open class ManagedHttpClient {
|
||||||
@@ -89,10 +90,16 @@ open class ManagedHttpClient {
|
|||||||
return clonedClient;
|
return clonedClient;
|
||||||
}
|
}
|
||||||
|
|
||||||
fun tryHead(url: String): Map<String, String>? {
|
private fun applyModifier(url: String, headers: MutableMap<String, String>, modifier: IRequestModifier?): Pair<String, MutableMap<String, String>> {
|
||||||
|
if (modifier == null) return Pair(url, headers)
|
||||||
|
val modified = modifier.modifyRequest(url, headers)
|
||||||
|
return Pair(modified.url ?: url, modified.headers.toMutableMap())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun tryHead(url: String, modifier: IRequestModifier? = null): Map<String, String>? {
|
||||||
ensureNotMainThread()
|
ensureNotMainThread()
|
||||||
try {
|
try {
|
||||||
val result = head(url);
|
val result = head(url, HashMap(), modifier);
|
||||||
if(result.isOk)
|
if(result.isOk)
|
||||||
return result.getHeadersFlat();
|
return result.getHeadersFlat();
|
||||||
else
|
else
|
||||||
@@ -141,12 +148,14 @@ open class ManagedHttpClient {
|
|||||||
return Socket(websocket);
|
return Socket(websocket);
|
||||||
}
|
}
|
||||||
|
|
||||||
fun get(url : String, headers : MutableMap<String, String> = HashMap<String, String>()) : Response {
|
fun get(url : String, headers : MutableMap<String, String> = HashMap<String, String>(), modifier: IRequestModifier? = null) : Response {
|
||||||
return execute(Request(url, "GET", null, headers));
|
val (finalUrl, finalHeaders) = applyModifier(url, headers, modifier)
|
||||||
|
return execute(Request(finalUrl, "GET", null, finalHeaders));
|
||||||
}
|
}
|
||||||
|
|
||||||
fun head(url : String, headers : MutableMap<String, String> = HashMap<String, String>()) : Response {
|
fun head(url : String, headers : MutableMap<String, String> = HashMap<String, String>(), modifier: IRequestModifier? = null) : Response {
|
||||||
return execute(Request(url, "HEAD", null, headers));
|
val (finalUrl, finalHeaders) = applyModifier(url, headers, modifier)
|
||||||
|
return execute(Request(finalUrl, "HEAD", null, finalHeaders));
|
||||||
}
|
}
|
||||||
|
|
||||||
fun post(url : String, headers : MutableMap<String, String> = HashMap<String, String>()) : Response {
|
fun post(url : String, headers : MutableMap<String, String> = HashMap<String, String>()) : Response {
|
||||||
|
|||||||
+1
-1
@@ -100,7 +100,7 @@ class SourcePluginDescriptor {
|
|||||||
@FormField(R.string.check_for_updates_setting, FieldForm.TOGGLE, R.string.check_for_updates_setting_description, -1)
|
@FormField(R.string.check_for_updates_setting, FieldForm.TOGGLE, R.string.check_for_updates_setting_description, -1)
|
||||||
var checkForUpdates: Boolean = true;
|
var checkForUpdates: Boolean = true;
|
||||||
@FormField(R.string.automatic_update_setting, FieldForm.TOGGLE, R.string.automatic_update_setting_description, 0)
|
@FormField(R.string.automatic_update_setting, FieldForm.TOGGLE, R.string.automatic_update_setting_description, 0)
|
||||||
var automaticUpdate: Boolean = false;
|
var automaticUpdate: Boolean = true;
|
||||||
|
|
||||||
@FormField(R.string.visibility, "group", R.string.enable_where_this_plugins_content_are_visible, 2)
|
@FormField(R.string.visibility, "group", R.string.enable_where_this_plugins_content_are_visible, 2)
|
||||||
var tabEnabled = TabEnabled();
|
var tabEnabled = TabEnabled();
|
||||||
|
|||||||
@@ -920,6 +920,7 @@ class StateCasting {
|
|||||||
if (videoSource != null) {
|
if (videoSource != null) {
|
||||||
_castServer.addHandlerWithAllowAllOptions(
|
_castServer.addHandlerWithAllowAllOptions(
|
||||||
HttpProxyHandler("GET", videoPath, videoSource.getVideoUrl(), true)
|
HttpProxyHandler("GET", videoPath, videoSource.getVideoUrl(), true)
|
||||||
|
.withIRequestModifier((videoSource as? JSSource)?.getRequestModifier())
|
||||||
.withInjectedHost()
|
.withInjectedHost()
|
||||||
.withHeader("Access-Control-Allow-Origin", "*"), true
|
.withHeader("Access-Control-Allow-Origin", "*"), true
|
||||||
).withTag("cast");
|
).withTag("cast");
|
||||||
@@ -927,6 +928,7 @@ class StateCasting {
|
|||||||
if (audioSource != null) {
|
if (audioSource != null) {
|
||||||
_castServer.addHandlerWithAllowAllOptions(
|
_castServer.addHandlerWithAllowAllOptions(
|
||||||
HttpProxyHandler("GET", audioPath, audioSource.getAudioUrl(), true)
|
HttpProxyHandler("GET", audioPath, audioSource.getAudioUrl(), true)
|
||||||
|
.withIRequestModifier((audioSource as? JSSource)?.getRequestModifier())
|
||||||
.withInjectedHost()
|
.withInjectedHost()
|
||||||
.withHeader("Access-Control-Allow-Origin", "*"), true
|
.withHeader("Access-Control-Allow-Origin", "*"), true
|
||||||
).withTag("cast");
|
).withTag("cast");
|
||||||
@@ -968,8 +970,7 @@ class StateCasting {
|
|||||||
val headers = masterContext.headers.clone()
|
val headers = masterContext.headers.clone()
|
||||||
headers["Content-Type"] = "application/vnd.apple.mpegurl";
|
headers["Content-Type"] = "application/vnd.apple.mpegurl";
|
||||||
|
|
||||||
val req = requestModifier?.modifyRequest(sourceUrl, mapOf())
|
val masterPlaylistResponse = _client.get(sourceUrl, mutableMapOf(), requestModifier)
|
||||||
val masterPlaylistResponse = _client.get(req?.url ?: sourceUrl, (req?.headers ?: mapOf()).toMutableMap())
|
|
||||||
|
|
||||||
check(masterPlaylistResponse.isOk) { "Failed to get master playlist: ${masterPlaylistResponse.code}" }
|
check(masterPlaylistResponse.isOk) { "Failed to get master playlist: ${masterPlaylistResponse.code}" }
|
||||||
|
|
||||||
@@ -1022,7 +1023,7 @@ class StateCasting {
|
|||||||
val vpHeaders = vpContext.headers.clone()
|
val vpHeaders = vpContext.headers.clone()
|
||||||
vpHeaders["Content-Type"] = "application/vnd.apple.mpegurl";
|
vpHeaders["Content-Type"] = "application/vnd.apple.mpegurl";
|
||||||
|
|
||||||
val response = _client.get(variantPlaylistRef.url)
|
val response = _client.get(variantPlaylistRef.url, mutableMapOf(), requestModifier)
|
||||||
check(response.isOk) { "Failed to get variant playlist: ${response.code}" }
|
check(response.isOk) { "Failed to get variant playlist: ${response.code}" }
|
||||||
|
|
||||||
val vpContent = response.body?.string()
|
val vpContent = response.body?.string()
|
||||||
@@ -1059,7 +1060,7 @@ class StateCasting {
|
|||||||
val vpHeaders = vpContext.headers.clone()
|
val vpHeaders = vpContext.headers.clone()
|
||||||
vpHeaders["Content-Type"] = "application/vnd.apple.mpegurl";
|
vpHeaders["Content-Type"] = "application/vnd.apple.mpegurl";
|
||||||
|
|
||||||
val response = _client.get(mediaRendition.uri)
|
val response = _client.get(mediaRendition.uri, mutableMapOf(), requestModifier)
|
||||||
check(response.isOk) { "Failed to get variant playlist: ${response.code}" }
|
check(response.isOk) { "Failed to get variant playlist: ${response.code}" }
|
||||||
|
|
||||||
val vpContent = response.body?.string()
|
val vpContent = response.body?.string()
|
||||||
@@ -1190,6 +1191,7 @@ class StateCasting {
|
|||||||
|
|
||||||
_castServer.addHandlerWithAllowAllOptions(
|
_castServer.addHandlerWithAllowAllOptions(
|
||||||
HttpProxyHandler("GET", audioPath, audioSource.getAudioUrl(), true)
|
HttpProxyHandler("GET", audioPath, audioSource.getAudioUrl(), true)
|
||||||
|
.withIRequestModifier((audioSource as? JSSource)?.getRequestModifier())
|
||||||
.withInjectedHost()
|
.withInjectedHost()
|
||||||
.withHeader("Access-Control-Allow-Origin", "*"), true
|
.withHeader("Access-Control-Allow-Origin", "*"), true
|
||||||
).withTag("castHlsIndirectVariant");
|
).withTag("castHlsIndirectVariant");
|
||||||
@@ -1267,6 +1269,7 @@ class StateCasting {
|
|||||||
|
|
||||||
_castServer.addHandlerWithAllowAllOptions(
|
_castServer.addHandlerWithAllowAllOptions(
|
||||||
HttpProxyHandler("GET", videoPath, videoSource.getVideoUrl(), true)
|
HttpProxyHandler("GET", videoPath, videoSource.getVideoUrl(), true)
|
||||||
|
.withIRequestModifier((videoSource as? JSSource)?.getRequestModifier())
|
||||||
.withInjectedHost()
|
.withInjectedHost()
|
||||||
.withHeader("Access-Control-Allow-Origin", "*"), true
|
.withHeader("Access-Control-Allow-Origin", "*"), true
|
||||||
).withTag("castHlsIndirectVariant");
|
).withTag("castHlsIndirectVariant");
|
||||||
@@ -1350,6 +1353,7 @@ class StateCasting {
|
|||||||
if (videoSource != null) {
|
if (videoSource != null) {
|
||||||
_castServer.addHandlerWithAllowAllOptions(
|
_castServer.addHandlerWithAllowAllOptions(
|
||||||
HttpProxyHandler("GET", videoPath, videoSource.getVideoUrl(), true)
|
HttpProxyHandler("GET", videoPath, videoSource.getVideoUrl(), true)
|
||||||
|
.withIRequestModifier((videoSource as? JSSource)?.getRequestModifier())
|
||||||
.withInjectedHost()
|
.withInjectedHost()
|
||||||
.withHeader("Access-Control-Allow-Origin", "*"), true
|
.withHeader("Access-Control-Allow-Origin", "*"), true
|
||||||
).withTag("cast");
|
).withTag("cast");
|
||||||
@@ -1357,6 +1361,7 @@ class StateCasting {
|
|||||||
if (audioSource != null) {
|
if (audioSource != null) {
|
||||||
_castServer.addHandlerWithAllowAllOptions(
|
_castServer.addHandlerWithAllowAllOptions(
|
||||||
HttpProxyHandler("GET", audioPath, audioSource.getAudioUrl(), true)
|
HttpProxyHandler("GET", audioPath, audioSource.getAudioUrl(), true)
|
||||||
|
.withIRequestModifier((audioSource as? JSSource)?.getRequestModifier())
|
||||||
.withInjectedHost()
|
.withInjectedHost()
|
||||||
.withHeader("Access-Control-Allow-Origin", "*"), true
|
.withHeader("Access-Control-Allow-Origin", "*"), true
|
||||||
).withTag("cast");
|
).withTag("cast");
|
||||||
|
|||||||
@@ -71,16 +71,14 @@ class Event0() : EventBase<(()->Unit), (()->Boolean)>() {
|
|||||||
fun emit() : Boolean {
|
fun emit() : Boolean {
|
||||||
var handled = false;
|
var handled = false;
|
||||||
|
|
||||||
synchronized(_conditionalListeners) {
|
val condSnapshot = synchronized(_conditionalListeners) { _conditionalListeners.toList() };
|
||||||
for (conditional in _conditionalListeners)
|
for (conditional in condSnapshot)
|
||||||
handled = handled || conditional.handler.invoke();
|
handled = handled || conditional.handler.invoke();
|
||||||
}
|
|
||||||
|
|
||||||
synchronized(_listeners) {
|
val snapshot = synchronized(_listeners) { _listeners.toList() };
|
||||||
handled = handled || _listeners.isNotEmpty();
|
handled = handled || snapshot.isNotEmpty();
|
||||||
for (handler in _listeners)
|
for (handler in snapshot)
|
||||||
handler.handler.invoke();
|
handler.handler.invoke();
|
||||||
}
|
|
||||||
|
|
||||||
return handled;
|
return handled;
|
||||||
}
|
}
|
||||||
@@ -88,16 +86,15 @@ class Event0() : EventBase<(()->Unit), (()->Boolean)>() {
|
|||||||
class Event1<T1>() : EventBase<((T1)->Unit), ((T1)->Boolean)>() {
|
class Event1<T1>() : EventBase<((T1)->Unit), ((T1)->Boolean)>() {
|
||||||
fun emit(value : T1): Boolean {
|
fun emit(value : T1): Boolean {
|
||||||
var handled = false;
|
var handled = false;
|
||||||
synchronized(_conditionalListeners) {
|
|
||||||
for (conditional in _conditionalListeners)
|
|
||||||
handled = handled || conditional.handler.invoke(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
synchronized(_listeners) {
|
val condSnapshot = synchronized(_conditionalListeners) { _conditionalListeners.toList() };
|
||||||
handled = handled || _listeners.isNotEmpty();
|
for (conditional in condSnapshot)
|
||||||
for (handler in _listeners)
|
handled = handled || conditional.handler.invoke(value);
|
||||||
handler.handler.invoke(value);
|
|
||||||
}
|
val snapshot = synchronized(_listeners) { _listeners.toList() };
|
||||||
|
handled = handled || snapshot.isNotEmpty();
|
||||||
|
for (handler in snapshot)
|
||||||
|
handler.handler.invoke(value);
|
||||||
|
|
||||||
return handled;
|
return handled;
|
||||||
}
|
}
|
||||||
@@ -106,16 +103,14 @@ class Event2<T1, T2>() : EventBase<((T1, T2)->Unit), ((T1, T2)->Boolean)>() {
|
|||||||
fun emit(value1 : T1, value2 : T2): Boolean {
|
fun emit(value1 : T1, value2 : T2): Boolean {
|
||||||
var handled = false;
|
var handled = false;
|
||||||
|
|
||||||
synchronized(_conditionalListeners) {
|
val condSnapshot = synchronized(_conditionalListeners) { _conditionalListeners.toList() };
|
||||||
for (conditional in _conditionalListeners)
|
for (conditional in condSnapshot)
|
||||||
handled = handled || conditional.handler.invoke(value1, value2);
|
handled = handled || conditional.handler.invoke(value1, value2);
|
||||||
}
|
|
||||||
|
|
||||||
synchronized(_listeners) {
|
val snapshot = synchronized(_listeners) { _listeners.toList() };
|
||||||
handled = handled || _listeners.isNotEmpty();
|
handled = handled || snapshot.isNotEmpty();
|
||||||
for (handler in _listeners)
|
for (handler in snapshot)
|
||||||
handler.handler.invoke(value1, value2);
|
handler.handler.invoke(value1, value2);
|
||||||
}
|
|
||||||
|
|
||||||
return handled;
|
return handled;
|
||||||
}
|
}
|
||||||
@@ -125,16 +120,14 @@ class Event3<T1, T2, T3>() : EventBase<((T1, T2, T3)->Unit), ((T1, T2, T3)->Bool
|
|||||||
fun emit(value1 : T1, value2 : T2, value3 : T3): Boolean {
|
fun emit(value1 : T1, value2 : T2, value3 : T3): Boolean {
|
||||||
var handled = false;
|
var handled = false;
|
||||||
|
|
||||||
synchronized(_conditionalListeners) {
|
val condSnapshot = synchronized(_conditionalListeners) { _conditionalListeners.toList() };
|
||||||
for (conditional in _conditionalListeners)
|
for (conditional in condSnapshot)
|
||||||
handled = handled || conditional.handler.invoke(value1, value2, value3);
|
handled = handled || conditional.handler.invoke(value1, value2, value3);
|
||||||
}
|
|
||||||
|
|
||||||
synchronized(_listeners) {
|
val snapshot = synchronized(_listeners) { _listeners.toList() };
|
||||||
handled = handled || _listeners.isNotEmpty();
|
handled = handled || snapshot.isNotEmpty();
|
||||||
for (handler in _listeners)
|
for (handler in snapshot)
|
||||||
handler.handler.invoke(value1, value2, value3);
|
handler.handler.invoke(value1, value2, value3);
|
||||||
}
|
|
||||||
|
|
||||||
return handled;
|
return handled;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -134,7 +134,7 @@ class AutoUpdateDialog(context: Context?) : AlertDialog(context) {
|
|||||||
var inputStream: InputStream? = null;
|
var inputStream: InputStream? = null;
|
||||||
try {
|
try {
|
||||||
val client = ManagedHttpClient();
|
val client = ManagedHttpClient();
|
||||||
val response = client.get(StateUpdate.APK_URL);
|
val response = client.get(StateUpdate.getApkUrl(_maxVersion));
|
||||||
if (response.isOk && response.body != null) {
|
if (response.isOk && response.body != null) {
|
||||||
inputStream = response.body.byteStream();
|
inputStream = response.body.byteStream();
|
||||||
val dataLength = response.body.contentLength();
|
val dataLength = response.body.contentLength();
|
||||||
|
|||||||
@@ -156,6 +156,18 @@ class VideoDownload {
|
|||||||
var hasVideoRequestModifier: Boolean = false;
|
var hasVideoRequestModifier: Boolean = false;
|
||||||
var hasAudioRequestModifier: Boolean = false;
|
var hasAudioRequestModifier: Boolean = false;
|
||||||
|
|
||||||
|
// Transient: IRequestModifier is a runtime object from the JS plugin engine and cannot be
|
||||||
|
// serialized. After deserialization these are null - DownloadService must re-prepare to
|
||||||
|
// recapture them from the live plugin source (see needsReprepareForAuth).
|
||||||
|
@kotlinx.serialization.Transient
|
||||||
|
private var preparedVideoRequestModifier: IRequestModifier? = null;
|
||||||
|
@kotlinx.serialization.Transient
|
||||||
|
private var preparedAudioRequestModifier: IRequestModifier? = null;
|
||||||
|
|
||||||
|
val needsReprepareForAuth: Boolean get() =
|
||||||
|
(hasVideoRequestModifier && preparedVideoRequestModifier == null && videoSourceLive == null) ||
|
||||||
|
(hasAudioRequestModifier && preparedAudioRequestModifier == null && audioSourceLive == null);
|
||||||
|
|
||||||
var progress: Double = 0.0;
|
var progress: Double = 0.0;
|
||||||
var isCancelled = false;
|
var isCancelled = false;
|
||||||
|
|
||||||
@@ -211,7 +223,7 @@ class VideoDownload {
|
|||||||
this.requireAudioSource = targetBitrate != null; //TODO: May not be a valid check.. can only be determined after live fetch?
|
this.requireAudioSource = targetBitrate != null; //TODO: May not be a valid check.. can only be determined after live fetch?
|
||||||
this.requiredCheck = optionalSources;
|
this.requiredCheck = optionalSources;
|
||||||
}
|
}
|
||||||
constructor(video: IPlatformVideoDetails, videoSource: IVideoSource?, audioSource: IAudioSource?, subtitleSource: SubtitleRawSource?) {
|
constructor(video: IPlatformVideoDetails, videoSource: IVideoSource?, audioSource: IAudioSource?, subtitleSource: SubtitleRawSource?, videoModifier: IRequestModifier? = null, audioModifier: IRequestModifier? = null) {
|
||||||
this.video = SerializedPlatformVideo.fromVideo(video);
|
this.video = SerializedPlatformVideo.fromVideo(video);
|
||||||
this.videoDetails = SerializedPlatformVideoDetails.fromVideo(video, if (subtitleSource != null) listOf(subtitleSource) else listOf());
|
this.videoDetails = SerializedPlatformVideoDetails.fromVideo(video, if (subtitleSource != null) listOf(subtitleSource) else listOf());
|
||||||
this.videoSource = if(videoSource is IVideoUrlSource) VideoUrlSource.fromUrlSource(videoSource) else null;
|
this.videoSource = if(videoSource is IVideoUrlSource) VideoUrlSource.fromUrlSource(videoSource) else null;
|
||||||
@@ -220,12 +232,22 @@ class VideoDownload {
|
|||||||
this.audioSourceLive = if(audioSource is JSSource) audioSource else null;
|
this.audioSourceLive = if(audioSource is JSSource) audioSource else null;
|
||||||
this.subtitleSource = subtitleSource;
|
this.subtitleSource = subtitleSource;
|
||||||
this.prepareTime = OffsetDateTime.now();
|
this.prepareTime = OffsetDateTime.now();
|
||||||
|
this.preparedVideoRequestModifier = videoModifier ?: (if (videoSource is JSSource && videoSource.hasRequestModifier) videoSource.getRequestModifier() else null);
|
||||||
|
this.preparedAudioRequestModifier = audioModifier ?: (if (audioSource is JSSource && audioSource.hasRequestModifier) audioSource.getRequestModifier() else null);
|
||||||
this.hasVideoRequestExecutor = videoSource is JSSource && videoSource.hasRequestExecutor;
|
this.hasVideoRequestExecutor = videoSource is JSSource && videoSource.hasRequestExecutor;
|
||||||
this.hasAudioRequestExecutor = audioSource is JSSource && audioSource.hasRequestExecutor;
|
this.hasAudioRequestExecutor = audioSource is JSSource && audioSource.hasRequestExecutor;
|
||||||
this.hasVideoRequestModifier = videoSource is JSSource && videoSource.hasRequestModifier;
|
// Set modifier flags from either the source or an explicitly provided modifier
|
||||||
this.hasAudioRequestModifier = audioSource is JSSource && audioSource.hasRequestModifier;
|
// (e.g. from the HLS picker, where the source is an HLSVariant, not JSSource).
|
||||||
this.requiresLiveVideoSource = this.hasVideoRequestModifier || this.hasVideoRequestExecutor || (videoSource is JSDashManifestRawSource && videoSource.hasGenerate);
|
// These flags are serialized and used by needsReprepareForAuth after restore.
|
||||||
this.requiresLiveAudioSource = this.hasAudioRequestModifier || this.hasAudioRequestExecutor || (audioSource is JSDashManifestRawAudioSource && audioSource.hasGenerate);
|
this.hasVideoRequestModifier = preparedVideoRequestModifier != null;
|
||||||
|
this.hasAudioRequestModifier = preparedAudioRequestModifier != null;
|
||||||
|
// requiresLiveVideoSource means a live JSSource is needed at download time (for executors
|
||||||
|
// or DASH generation). Modifiers alone don't require a live source - they're already
|
||||||
|
// captured in preparedVideoRequestModifier and recaptured via needsReprepareForAuth.
|
||||||
|
val sourceHasVideoModifier = videoSource is JSSource && videoSource.hasRequestModifier;
|
||||||
|
val sourceHasAudioModifier = audioSource is JSSource && audioSource.hasRequestModifier;
|
||||||
|
this.requiresLiveVideoSource = sourceHasVideoModifier || this.hasVideoRequestExecutor || (videoSource is JSDashManifestRawSource && videoSource.hasGenerate);
|
||||||
|
this.requiresLiveAudioSource = sourceHasAudioModifier || this.hasAudioRequestExecutor || (audioSource is JSDashManifestRawAudioSource && audioSource.hasGenerate);
|
||||||
this.targetVideoName = videoSource?.name;
|
this.targetVideoName = videoSource?.name;
|
||||||
this.targetAudioName = audioSource?.name;
|
this.targetAudioName = audioSource?.name;
|
||||||
this.targetPixelCount = if(videoSource != null) (videoSource.width * videoSource.height).toLong() else null;
|
this.targetPixelCount = if(videoSource != null) (videoSource.width * videoSource.height).toLong() else null;
|
||||||
@@ -321,8 +343,10 @@ class VideoDownload {
|
|||||||
val videoSources = arrayListOf<IVideoSource>()
|
val videoSources = arrayListOf<IVideoSource>()
|
||||||
for (source in original.video.videoSources) {
|
for (source in original.video.videoSources) {
|
||||||
if (source is IHLSManifestSource) {
|
if (source is IHLSManifestSource) {
|
||||||
|
val sourceModifier = if (source is JSSource && source.hasRequestModifier) source.getRequestModifier() else null
|
||||||
|
if (sourceModifier != null) preparedVideoRequestModifier = sourceModifier
|
||||||
try {
|
try {
|
||||||
val playlistResponse = client.get(source.url)
|
val playlistResponse = client.get(source.url, HashMap(), sourceModifier)
|
||||||
if (playlistResponse.isOk) {
|
if (playlistResponse.isOk) {
|
||||||
val resolvedPlaylistUrl = playlistResponse.url
|
val resolvedPlaylistUrl = playlistResponse.url
|
||||||
val playlistContent = playlistResponse.body?.string()
|
val playlistContent = playlistResponse.body?.string()
|
||||||
@@ -349,6 +373,8 @@ class VideoDownload {
|
|||||||
if(vsource is JSSource) {
|
if(vsource is JSSource) {
|
||||||
this.hasVideoRequestExecutor = this.hasVideoRequestExecutor || vsource.hasRequestExecutor;
|
this.hasVideoRequestExecutor = this.hasVideoRequestExecutor || vsource.hasRequestExecutor;
|
||||||
this.requiresLiveVideoSource = this.hasVideoRequestExecutor || (vsource is JSDashManifestRawSource && vsource.hasGenerate);
|
this.requiresLiveVideoSource = this.hasVideoRequestExecutor || (vsource is JSDashManifestRawSource && vsource.hasGenerate);
|
||||||
|
if (vsource.hasRequestModifier && preparedVideoRequestModifier == null)
|
||||||
|
preparedVideoRequestModifier = vsource.getRequestModifier()
|
||||||
}
|
}
|
||||||
|
|
||||||
if(vsource == null) {
|
if(vsource == null) {
|
||||||
@@ -370,8 +396,10 @@ class VideoDownload {
|
|||||||
if (video is VideoUnMuxedSourceDescriptor) {
|
if (video is VideoUnMuxedSourceDescriptor) {
|
||||||
for (source in video.audioSources) {
|
for (source in video.audioSources) {
|
||||||
if (source is IHLSManifestAudioSource) {
|
if (source is IHLSManifestAudioSource) {
|
||||||
|
val sourceModifier = if (source is JSSource && source.hasRequestModifier) source.getRequestModifier() else null
|
||||||
|
if (sourceModifier != null) preparedAudioRequestModifier = sourceModifier
|
||||||
try {
|
try {
|
||||||
val playlistResponse = client.get(source.url)
|
val playlistResponse = client.get(source.url, HashMap(), sourceModifier)
|
||||||
if (playlistResponse.isOk) {
|
if (playlistResponse.isOk) {
|
||||||
val resolvedPlaylistUrl = playlistResponse.url
|
val resolvedPlaylistUrl = playlistResponse.url
|
||||||
val playlistContent = playlistResponse.body?.string()
|
val playlistContent = playlistResponse.body?.string()
|
||||||
@@ -406,6 +434,8 @@ class VideoDownload {
|
|||||||
if(asource is JSSource) {
|
if(asource is JSSource) {
|
||||||
this.hasAudioRequestExecutor = this.hasAudioRequestExecutor || asource.hasRequestExecutor;
|
this.hasAudioRequestExecutor = this.hasAudioRequestExecutor || asource.hasRequestExecutor;
|
||||||
this.requiresLiveAudioSource = this.hasAudioRequestExecutor || (asource is JSDashManifestRawSource && asource.hasGenerate);
|
this.requiresLiveAudioSource = this.hasAudioRequestExecutor || (asource is JSDashManifestRawSource && asource.hasGenerate);
|
||||||
|
if (asource.hasRequestModifier && preparedAudioRequestModifier == null)
|
||||||
|
preparedAudioRequestModifier = asource.getRequestModifier()
|
||||||
}
|
}
|
||||||
|
|
||||||
if(asource == null) {
|
if(asource == null) {
|
||||||
@@ -502,10 +532,16 @@ class VideoDownload {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val videoModifier = preparedVideoRequestModifier
|
||||||
if(actualVideoSource is IVideoUrlSource)
|
if(actualVideoSource is IVideoUrlSource)
|
||||||
videoFileSize = when (videoSource!!.container) {
|
videoFileSize = when (videoSource!!.container) {
|
||||||
"application/vnd.apple.mpegurl" -> downloadHlsSource(context, "Video", client, if (actualVideoSource is JSSource) actualVideoSource else null, videoSource!!.getVideoUrl(), File(downloadDir, videoFileName!!), progressCallback)
|
"application/vnd.apple.mpegurl" -> {
|
||||||
else -> downloadFileSource("Video", client, if (actualVideoSource is JSSource) actualVideoSource else null, videoSource!!.getVideoUrl(), File(downloadDir, videoFileName!!), progressCallback)
|
// HLS segments are concatenated into an MP4 file during download,
|
||||||
|
// so override the container for local playback/casting
|
||||||
|
videoOverrideContainer = "video/mp4";
|
||||||
|
downloadHlsSource(context, "Video", client, videoModifier, videoSource!!.getVideoUrl(), File(downloadDir, videoFileName!!), progressCallback)
|
||||||
|
}
|
||||||
|
else -> downloadFileSource("Video", client, videoModifier, videoSource!!.getVideoUrl(), File(downloadDir, videoFileName!!), progressCallback)
|
||||||
}
|
}
|
||||||
else if(actualVideoSource is JSDashManifestRawSource) {
|
else if(actualVideoSource is JSDashManifestRawSource) {
|
||||||
if(actualAudioSource == null)
|
if(actualAudioSource == null)
|
||||||
@@ -546,10 +582,16 @@ class VideoDownload {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val audioModifier = preparedAudioRequestModifier
|
||||||
if(actualAudioSource is IAudioUrlSource)
|
if(actualAudioSource is IAudioUrlSource)
|
||||||
audioFileSize = when (audioSource!!.container) {
|
audioFileSize = when (audioSource!!.container) {
|
||||||
"application/vnd.apple.mpegurl" -> downloadHlsSource(context, "Audio", client, if (actualAudioSource is JSSource) actualAudioSource else null, audioSource!!.getAudioUrl(), File(downloadDir, audioFileName!!), progressCallback)
|
"application/vnd.apple.mpegurl" -> {
|
||||||
else -> downloadFileSource("Audio", client, if (actualAudioSource is JSSource) actualAudioSource else null, audioSource!!.getAudioUrl(), File(downloadDir, audioFileName!!), progressCallback)
|
// HLS segments are concatenated into an MP4 file during download,
|
||||||
|
// so override the container for local playback/casting
|
||||||
|
audioOverrideContainer = "audio/mp4";
|
||||||
|
downloadHlsSource(context, "Audio", client, audioModifier, audioSource!!.getAudioUrl(), File(downloadDir, audioFileName!!), progressCallback)
|
||||||
|
}
|
||||||
|
else -> downloadFileSource("Audio", client, audioModifier, audioSource!!.getAudioUrl(), File(downloadDir, audioFileName!!), progressCallback)
|
||||||
}
|
}
|
||||||
else if(actualAudioSource is JSDashManifestRawAudioSource) {
|
else if(actualAudioSource is JSDashManifestRawAudioSource) {
|
||||||
audioFileSize = downloadDashFileSource("Audio", client, actualAudioSource, File(downloadDir, audioFileName!!), progressCallback, 2);
|
audioFileSize = downloadDashFileSource("Audio", client, actualAudioSource, File(downloadDir, audioFileName!!), progressCallback, 2);
|
||||||
@@ -663,15 +705,11 @@ class VideoDownload {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun downloadHlsSource(context: Context, name: String, client: ManagedHttpClient, source: JSSource?, hlsUrl: String, targetFile: File, onProgress: (Long, Long, Long) -> Unit): Long {
|
private suspend fun downloadHlsSource(context: Context, name: String, client: ManagedHttpClient, modifier: IRequestModifier?, hlsUrl: String, targetFile: File, onProgress: (Long, Long, Long) -> Unit): Long {
|
||||||
if (targetFile.exists())
|
if (targetFile.exists())
|
||||||
targetFile.delete()
|
targetFile.delete()
|
||||||
|
|
||||||
var downloadedTotalLength = 0L
|
var downloadedTotalLength = 0L
|
||||||
val modifier = if (source is JSSource && source.hasRequestModifier)
|
|
||||||
source.getRequestModifier()
|
|
||||||
else
|
|
||||||
null
|
|
||||||
|
|
||||||
fun downloadBytes(url: String, rangeStart: Long? = null, rangeLength: Long? = null): ByteArray {
|
fun downloadBytes(url: String, rangeStart: Long? = null, rangeLength: Long? = null): ByteArray {
|
||||||
val headers = mutableMapOf<String, String>()
|
val headers = mutableMapOf<String, String>()
|
||||||
@@ -685,17 +723,13 @@ class VideoDownload {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val modified = modifier?.modifyRequest(url, headers)
|
val resp = client.get(url, headers, modifier)
|
||||||
val finalUrl = modified?.url ?: url
|
|
||||||
val finalHeaders = modified?.headers?.toMutableMap() ?: headers
|
|
||||||
|
|
||||||
val resp = client.get(finalUrl, finalHeaders)
|
|
||||||
if (!resp.isOk) {
|
if (!resp.isOk) {
|
||||||
resp.body?.close()
|
resp.body?.close()
|
||||||
throw IllegalStateException("Failed to download HLS resource ($finalUrl): HTTP ${resp.code}")
|
throw IllegalStateException("Failed to download HLS resource ($url): HTTP ${resp.code}")
|
||||||
}
|
}
|
||||||
|
|
||||||
val body = resp.body ?: throw IllegalStateException("Failed to download HLS resource ($finalUrl): Empty body")
|
val body = resp.body ?: throw IllegalStateException("Failed to download HLS resource ($url): Empty body")
|
||||||
val bytes = body.bytes()
|
val bytes = body.bytes()
|
||||||
body.close()
|
body.close()
|
||||||
return bytes
|
return bytes
|
||||||
@@ -710,12 +744,7 @@ class VideoDownload {
|
|||||||
|
|
||||||
val segmentFiles = arrayListOf<File>()
|
val segmentFiles = arrayListOf<File>()
|
||||||
try {
|
try {
|
||||||
val playlistHeaders = mutableMapOf<String, String>()
|
val playlistResp = client.get(hlsUrl, mutableMapOf(), modifier)
|
||||||
val modifiedPlaylistReq = modifier?.modifyRequest(hlsUrl, playlistHeaders)
|
|
||||||
val playlistResp = client.get(
|
|
||||||
modifiedPlaylistReq?.url ?: hlsUrl,
|
|
||||||
modifiedPlaylistReq?.headers?.toMutableMap() ?: playlistHeaders
|
|
||||||
)
|
|
||||||
|
|
||||||
check(playlistResp.isOk) { "Failed to get variant playlist: ${playlistResp.code}" }
|
check(playlistResp.isOk) { "Failed to get variant playlist: ${playlistResp.code}" }
|
||||||
|
|
||||||
@@ -964,16 +993,7 @@ class VideoDownload {
|
|||||||
|
|
||||||
Logger.i(TAG, "Downloading cue ${indexCounter}")
|
Logger.i(TAG, "Downloading cue ${indexCounter}")
|
||||||
val url = foundTemplateUrl.replace("\$Number\$", (indexCounter).toString());
|
val url = foundTemplateUrl.replace("\$Number\$", (indexCounter).toString());
|
||||||
val modified = modifier?.modifyRequest(url, mapOf());
|
val data = executeOrGet(client, executor, modifier, url)
|
||||||
|
|
||||||
val data = if(executor != null)
|
|
||||||
executor.executeRequest("GET", modified?.url ?: url, null, modified?.headers ?: mapOf());
|
|
||||||
else {
|
|
||||||
val resp = client.get(modified?.url ?: url, modified?.headers?.toMutableMap() ?: mutableMapOf());
|
|
||||||
if(!resp.isOk)
|
|
||||||
throw IllegalStateException("Dash request failed for index " + indexCounter.toString() + ", with code: " + resp.code.toString());
|
|
||||||
resp.body!!.bytes()
|
|
||||||
}
|
|
||||||
fileStream.write(data, 0, data.size);
|
fileStream.write(data, 0, data.size);
|
||||||
speedTracker.addWork(data.size.toLong());
|
speedTracker.addWork(data.size.toLong());
|
||||||
written += data.size;
|
written += data.size;
|
||||||
@@ -993,16 +1013,7 @@ class VideoDownload {
|
|||||||
val t2 = cue2.groupValues[1];
|
val t2 = cue2.groupValues[1];
|
||||||
val d2 = cue2.groupValues[2];
|
val d2 = cue2.groupValues[2];
|
||||||
val url2 = foundTemplateUrl2!!.replace("\$Number\$", (index2).toString());
|
val url2 = foundTemplateUrl2!!.replace("\$Number\$", (index2).toString());
|
||||||
val modified2 = modifier?.modifyRequest(url, mapOf());
|
val data = executeOrGet(client, executor, modifier, url2)
|
||||||
|
|
||||||
val data = if(executor != null)
|
|
||||||
executor.executeRequest("GET", modified2?.url ?: url2, null, modified2?.headers ?: mapOf());
|
|
||||||
else {
|
|
||||||
val resp = client.get(modified2?.url ?: url, modified2?.headers?.toMutableMap() ?: mutableMapOf());
|
|
||||||
if(!resp.isOk)
|
|
||||||
throw IllegalStateException("Dash request2 failed for index " + indexCounter.toString() + ", with code: " + resp.code.toString());
|
|
||||||
resp.body!!.bytes()
|
|
||||||
}
|
|
||||||
fileStream2.write(data, 0, data.size);
|
fileStream2.write(data, 0, data.size);
|
||||||
speedTracker.addWork(data.size.toLong());
|
speedTracker.addWork(data.size.toLong());
|
||||||
written2 += data.size;
|
written2 += data.size;
|
||||||
@@ -1071,7 +1082,7 @@ class VideoDownload {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun downloadFileSource(name: String, client: ManagedHttpClient, source: JSSource?, videoUrl: String, targetFile: File, onProgress: (Long, Long, Long) -> Unit): Long {
|
private fun downloadFileSource(name: String, client: ManagedHttpClient, modifier: IRequestModifier?, videoUrl: String, targetFile: File, onProgress: (Long, Long, Long) -> Unit): Long {
|
||||||
if(targetFile.exists())
|
if(targetFile.exists())
|
||||||
targetFile.delete();
|
targetFile.delete();
|
||||||
|
|
||||||
@@ -1080,13 +1091,8 @@ class VideoDownload {
|
|||||||
val sourceLength: Long?;
|
val sourceLength: Long?;
|
||||||
val fileStream = FileOutputStream(targetFile);
|
val fileStream = FileOutputStream(targetFile);
|
||||||
|
|
||||||
val modifier = if (source is JSSource && source.hasRequestModifier)
|
|
||||||
source.getRequestModifier();
|
|
||||||
else
|
|
||||||
null;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
val head = client.tryHead(videoUrl);
|
val head = client.tryHead(videoUrl, modifier);
|
||||||
val relatedPlugin = (video?.url ?: videoDetails?.url)?.let { StatePlatform.instance.getContentClient(it) }?.let { if(it is JSClient) it else null };
|
val relatedPlugin = (video?.url ?: videoDetails?.url)?.let { StatePlatform.instance.getContentClient(it) }?.let { if(it is JSClient) it else null };
|
||||||
if(Settings.instance.downloads.byteRangeDownload && head?.containsKey("accept-ranges") == true && head.containsKey("content-length"))
|
if(Settings.instance.downloads.byteRangeDownload && head?.containsKey("accept-ranges") == true && head.containsKey("content-length"))
|
||||||
{
|
{
|
||||||
@@ -1161,12 +1167,7 @@ class VideoDownload {
|
|||||||
|
|
||||||
var lastSpeed: Long = 0;
|
var lastSpeed: Long = 0;
|
||||||
|
|
||||||
val result = if (modifier != null) {
|
val result = client.get(url, HashMap(), modifier)
|
||||||
val modified = modifier.modifyRequest(url, mapOf())
|
|
||||||
client.get(modified.url!!, modified.headers.toMutableMap())
|
|
||||||
} else {
|
|
||||||
client.get(url)
|
|
||||||
}
|
|
||||||
if (!result.isOk) {
|
if (!result.isOk) {
|
||||||
result.body?.close()
|
result.body?.close()
|
||||||
throw IllegalStateException("Failed to download source. Web[${result.code}] Error");
|
throw IllegalStateException("Failed to download source. Web[${result.code}] Error");
|
||||||
@@ -1379,13 +1380,12 @@ class VideoDownload {
|
|||||||
var lastException: Throwable? = null;
|
var lastException: Throwable? = null;
|
||||||
|
|
||||||
val headers = mutableMapOf(Pair("Range", "bytes=${rangeStart}-${rangeEnd}"));
|
val headers = mutableMapOf(Pair("Range", "bytes=${rangeStart}-${rangeEnd}"));
|
||||||
val modified = modifier?.modifyRequest(url, headers);
|
|
||||||
|
|
||||||
while (retryCount <= 3) {
|
while (retryCount <= 3) {
|
||||||
try {
|
try {
|
||||||
val toRead = rangeEnd - rangeStart;
|
val toRead = rangeEnd - rangeStart;
|
||||||
|
|
||||||
val req = client.get(modified?.url ?: url, modified?.headers?.toMutableMap() ?: headers);
|
val req = client.get(url, headers.toMutableMap(), modifier);
|
||||||
if (!req.isOk) {
|
if (!req.isOk) {
|
||||||
val bodyString = req.body?.string()
|
val bodyString = req.body?.string()
|
||||||
req.body?.close()
|
req.body?.close()
|
||||||
@@ -1519,6 +1519,18 @@ class VideoDownload {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun executeOrGet(client: ManagedHttpClient, executor: JSRequestExecutor?, modifier: IRequestModifier?, url: String, headers: Map<String, String> = mapOf()): ByteArray {
|
||||||
|
if (executor != null) {
|
||||||
|
val modified = modifier?.modifyRequest(url, headers)
|
||||||
|
return executor.executeRequest("GET", modified?.url ?: url, null, modified?.headers ?: headers)
|
||||||
|
} else {
|
||||||
|
val resp = client.get(url, headers.toMutableMap(), modifier)
|
||||||
|
if (!resp.isOk)
|
||||||
|
throw IllegalStateException("Request failed for ($url) with code: ${resp.code}")
|
||||||
|
return resp.body!!.bytes()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val TAG = "VideoDownload";
|
const val TAG = "VideoDownload";
|
||||||
const val GROUP_PLAYLIST = "Playlist";
|
const val GROUP_PLAYLIST = "Playlist";
|
||||||
|
|||||||
@@ -238,6 +238,16 @@ class DownloadService : Service() {
|
|||||||
download.targetBitrate = download.audioSource!!.bitrate.toLong();
|
download.targetBitrate = download.audioSource!!.bitrate.toLong();
|
||||||
download.audioSource = null;
|
download.audioSource = null;
|
||||||
}
|
}
|
||||||
|
// Force re-prepare if auth modifiers are needed but lost (e.g. after deserialization,
|
||||||
|
// since IRequestModifier is transient and cannot survive serialization).
|
||||||
|
// Must also clear sources so prepare() enters the source selection branches where
|
||||||
|
// modifiers are recaptured from the live plugin JSSource.
|
||||||
|
if(download.needsReprepareForAuth) {
|
||||||
|
Logger.w(TAG, "Video Download [${download.name}] needs re-prepare for auth modifiers");
|
||||||
|
download.videoDetails = null;
|
||||||
|
download.videoSource = null;
|
||||||
|
download.audioSource = null;
|
||||||
|
}
|
||||||
if(download.videoDetails == null || (!download.isVideoDownloadReady || !download.isAudioDownloadReady))
|
if(download.videoDetails == null || (!download.isVideoDownloadReady || !download.isAudioDownloadReady))
|
||||||
download.changeState(VideoDownload.State.PREPARING);
|
download.changeState(VideoDownload.State.PREPARING);
|
||||||
notifyDownload(download);
|
notifyDownload(download);
|
||||||
|
|||||||
@@ -668,6 +668,9 @@ class StateApp {
|
|||||||
if (Settings.instance.autoUpdate.isAutoUpdateEnabled()) {
|
if (Settings.instance.autoUpdate.isAutoUpdateEnabled()) {
|
||||||
if (Settings.instance.autoUpdate.shouldBackgroundDownload) {
|
if (Settings.instance.autoUpdate.shouldBackgroundDownload) {
|
||||||
Logger.i(TAG, "MainApp Started: Initialize [AutoUpdate Background]");
|
Logger.i(TAG, "MainApp Started: Initialize [AutoUpdate Background]");
|
||||||
|
scopeOrNull?.launch(Dispatchers.IO) {
|
||||||
|
StateUpdate.instance.seedUiFromDisk(context)
|
||||||
|
}
|
||||||
val constraints = Constraints.Builder()
|
val constraints = Constraints.Builder()
|
||||||
.setRequiredNetworkType(NetworkType.CONNECTED)
|
.setRequiredNetworkType(NetworkType.CONNECTED)
|
||||||
.build();
|
.build();
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import com.futo.platformplayer.UIDialogs
|
|||||||
import com.futo.platformplayer.activities.IWithResultLauncher
|
import com.futo.platformplayer.activities.IWithResultLauncher
|
||||||
import com.futo.platformplayer.api.http.ManagedHttpClient
|
import com.futo.platformplayer.api.http.ManagedHttpClient
|
||||||
import com.futo.platformplayer.api.media.PlatformID
|
import com.futo.platformplayer.api.media.PlatformID
|
||||||
|
import com.futo.platformplayer.api.media.models.modifier.IRequestModifier
|
||||||
import com.futo.platformplayer.api.media.models.streams.sources.IAudioSource
|
import com.futo.platformplayer.api.media.models.streams.sources.IAudioSource
|
||||||
import com.futo.platformplayer.api.media.models.streams.sources.IAudioUrlSource
|
import com.futo.platformplayer.api.media.models.streams.sources.IAudioUrlSource
|
||||||
import com.futo.platformplayer.api.media.models.streams.sources.IVideoSource
|
import com.futo.platformplayer.api.media.models.streams.sources.IVideoSource
|
||||||
@@ -341,8 +342,8 @@ class StateDownloads {
|
|||||||
fun download(video: IPlatformVideo, targetPixelcount: Long?, targetBitrate: Long?) {
|
fun download(video: IPlatformVideo, targetPixelcount: Long?, targetBitrate: Long?) {
|
||||||
download(VideoDownload(video, targetPixelcount, targetBitrate));
|
download(VideoDownload(video, targetPixelcount, targetBitrate));
|
||||||
}
|
}
|
||||||
fun download(video: IPlatformVideoDetails, videoSource: IVideoSource?, audioSource: IAudioSource?, subtitleSource: SubtitleRawSource?) {
|
fun download(video: IPlatformVideoDetails, videoSource: IVideoSource?, audioSource: IAudioSource?, subtitleSource: SubtitleRawSource?, videoModifier: IRequestModifier? = null, audioModifier: IRequestModifier? = null) {
|
||||||
download(VideoDownload(video, videoSource, audioSource, subtitleSource));
|
download(VideoDownload(video, videoSource, audioSource, subtitleSource, videoModifier, audioModifier));
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun download(videoState: VideoDownload, notify: Boolean = true) {
|
private fun download(videoState: VideoDownload, notify: Boolean = true) {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import android.os.Build
|
|||||||
import com.futo.platformplayer.BuildConfig
|
import com.futo.platformplayer.BuildConfig
|
||||||
import com.futo.platformplayer.UIDialogs
|
import com.futo.platformplayer.UIDialogs
|
||||||
import com.futo.platformplayer.api.http.ManagedHttpClient
|
import com.futo.platformplayer.api.http.ManagedHttpClient
|
||||||
|
import com.futo.platformplayer.constructs.Event0
|
||||||
import com.futo.platformplayer.copyToOutputStream
|
import com.futo.platformplayer.copyToOutputStream
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@@ -14,7 +15,97 @@ import java.io.File
|
|||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import java.io.OutputStream
|
import java.io.OutputStream
|
||||||
|
|
||||||
|
enum class UpdateUiState { NONE, AVAILABLE, DOWNLOADING, READY, FAILED }
|
||||||
|
|
||||||
class StateUpdate {
|
class StateUpdate {
|
||||||
|
|
||||||
|
@Volatile var uiState: UpdateUiState = UpdateUiState.NONE
|
||||||
|
private set
|
||||||
|
@Volatile var uiVersion: Int = 0
|
||||||
|
private set
|
||||||
|
@Volatile var uiProgress: Int = 0
|
||||||
|
private set
|
||||||
|
@Volatile var uiIndeterminate: Boolean = true
|
||||||
|
private set
|
||||||
|
@Volatile var uiApkFile: File? = null
|
||||||
|
private set
|
||||||
|
@Volatile var uiError: String? = null
|
||||||
|
private set
|
||||||
|
|
||||||
|
val onUiChanged = Event0()
|
||||||
|
|
||||||
|
fun setUiAvailable(version: Int) {
|
||||||
|
uiState = UpdateUiState.AVAILABLE
|
||||||
|
uiVersion = version
|
||||||
|
uiError = null
|
||||||
|
onUiChanged.emit()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setUiDownloading(version: Int, progress: Int, indeterminate: Boolean) {
|
||||||
|
uiState = UpdateUiState.DOWNLOADING
|
||||||
|
uiVersion = version
|
||||||
|
uiProgress = progress
|
||||||
|
uiIndeterminate = indeterminate
|
||||||
|
uiError = null
|
||||||
|
onUiChanged.emit()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setUiReady(version: Int, apkFile: File) {
|
||||||
|
uiState = UpdateUiState.READY
|
||||||
|
uiVersion = version
|
||||||
|
uiApkFile = apkFile
|
||||||
|
uiError = null
|
||||||
|
onUiChanged.emit()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setUiFailed(version: Int, error: String?) {
|
||||||
|
uiState = UpdateUiState.FAILED
|
||||||
|
uiVersion = version
|
||||||
|
uiError = error
|
||||||
|
onUiChanged.emit()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun clearUi() {
|
||||||
|
uiState = UpdateUiState.NONE
|
||||||
|
uiVersion = 0
|
||||||
|
uiProgress = 0
|
||||||
|
uiIndeterminate = true
|
||||||
|
uiApkFile = null
|
||||||
|
uiError = null
|
||||||
|
onUiChanged.emit()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun seedUiFromDisk(context: Context) {
|
||||||
|
if (uiState != UpdateUiState.NONE) return
|
||||||
|
try {
|
||||||
|
val dir = File(context.filesDir, "updates")
|
||||||
|
if (!dir.exists()) return
|
||||||
|
val abi = try { DESIRED_ABI } catch (t: Throwable) { return }
|
||||||
|
val prefix = "app-$abi-"
|
||||||
|
val suffix = ".apk"
|
||||||
|
val candidates = dir.listFiles { f ->
|
||||||
|
f.isFile && f.name.startsWith(prefix) && f.name.endsWith(suffix)
|
||||||
|
} ?: return
|
||||||
|
var bestVersion = BuildConfig.VERSION_CODE
|
||||||
|
var bestFile: File? = null
|
||||||
|
for (f in candidates) {
|
||||||
|
val versionStr = f.name.removePrefix(prefix).removeSuffix(suffix)
|
||||||
|
val v = versionStr.toIntOrNull() ?: continue
|
||||||
|
if (v > bestVersion && f.length() > 0L) {
|
||||||
|
bestVersion = v
|
||||||
|
bestFile = f
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val ready = bestFile
|
||||||
|
if (ready != null) {
|
||||||
|
Logger.i(TAG, "Seeding UI ready from disk: v=$bestVersion file=${ready.absolutePath}")
|
||||||
|
setUiReady(bestVersion, ready)
|
||||||
|
}
|
||||||
|
} catch (t: Throwable) {
|
||||||
|
Logger.w(TAG, "Failed to seed UI from disk", t)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun checkForUpdates(context: Context, showUpToDateToast: Boolean, hideExceptionButtons: Boolean = false) = withContext(Dispatchers.IO) {
|
suspend fun checkForUpdates(context: Context, showUpToDateToast: Boolean, hideExceptionButtons: Boolean = false) = withContext(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
val client = ManagedHttpClient();
|
val client = ManagedHttpClient();
|
||||||
@@ -97,16 +188,16 @@ class StateUpdate {
|
|||||||
throw Exception("App is not compatible. Supported ABIS: ${Build.SUPPORTED_ABIS.joinToString()}}.");
|
throw Exception("App is not compatible. Supported ABIS: ${Build.SUPPORTED_ABIS.joinToString()}}.");
|
||||||
};
|
};
|
||||||
val VERSION_URL = if (BuildConfig.IS_UNSTABLE_BUILD) {
|
val VERSION_URL = if (BuildConfig.IS_UNSTABLE_BUILD) {
|
||||||
"https://releases.grayjay.app/version-unstable.txt"
|
"https://rel.grayjay.app/version-unstable.txt"
|
||||||
} else {
|
} else {
|
||||||
"https://releases.grayjay.app/version.txt"
|
"https://rel.grayjay.app/version.txt"
|
||||||
}
|
}
|
||||||
val APK_URL = if (BuildConfig.IS_UNSTABLE_BUILD) {
|
fun getApkUrl(version: Int): String = if (BuildConfig.IS_UNSTABLE_BUILD) {
|
||||||
"https://releases.grayjay.app/app-$DESIRED_ABI-release-unstable.apk"
|
"https://rel.grayjay.app/$version/app-$DESIRED_ABI-release-unstable.apk"
|
||||||
} else {
|
} else {
|
||||||
"https://releases.grayjay.app/app-$DESIRED_ABI-release.apk"
|
"https://rel.grayjay.app/$version/app-$DESIRED_ABI-release.apk"
|
||||||
}
|
}
|
||||||
val CHANGELOG_BASE_URL = "https://releases.grayjay.app/changelogs";
|
val CHANGELOG_BASE_URL = "https://rel.grayjay.app/changelogs";
|
||||||
|
|
||||||
fun getApkFile(context: Context, version: Int): File {
|
fun getApkFile(context: Context, version: Int): File {
|
||||||
val dir = File(context.filesDir, "updates");
|
val dir = File(context.filesDir, "updates");
|
||||||
@@ -136,4 +227,4 @@ class StateUpdate {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,158 @@
|
|||||||
|
package com.futo.platformplayer.views.announcements
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.util.AttributeSet
|
||||||
|
import android.view.View
|
||||||
|
import android.widget.FrameLayout
|
||||||
|
import android.widget.ImageView
|
||||||
|
import android.widget.LinearLayout
|
||||||
|
import android.widget.ProgressBar
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.lifecycle.findViewTreeLifecycleOwner
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import com.futo.platformplayer.R
|
||||||
|
import com.futo.platformplayer.Settings
|
||||||
|
import com.futo.platformplayer.UpdateDownloadService
|
||||||
|
import com.futo.platformplayer.UpdateInstaller
|
||||||
|
import com.futo.platformplayer.UpdateNotificationManager
|
||||||
|
import com.futo.platformplayer.logging.Logger
|
||||||
|
import com.futo.platformplayer.states.StateApp
|
||||||
|
import com.futo.platformplayer.states.StateUpdate
|
||||||
|
import com.futo.platformplayer.states.UpdateUiState
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
class UpdateBannerView : LinearLayout {
|
||||||
|
private val _root: FrameLayout
|
||||||
|
private val _iconUpdate: ImageView
|
||||||
|
private val _textTitle: TextView
|
||||||
|
private val _progressBar: ProgressBar
|
||||||
|
private val _buttonAction: FrameLayout
|
||||||
|
private val _textAction: TextView
|
||||||
|
|
||||||
|
private val _scope: CoroutineScope?
|
||||||
|
|
||||||
|
constructor(context: Context, attrs: AttributeSet? = null) : super(context, attrs) {
|
||||||
|
inflate(context, R.layout.view_update_banner, this)
|
||||||
|
|
||||||
|
_scope = findViewTreeLifecycleOwner()?.lifecycleScope ?: StateApp.instance.scopeOrNull
|
||||||
|
|
||||||
|
_root = findViewById(R.id.root)
|
||||||
|
_iconUpdate = findViewById(R.id.icon_update)
|
||||||
|
_textTitle = findViewById(R.id.text_title)
|
||||||
|
_progressBar = findViewById(R.id.update_banner_progress)
|
||||||
|
_buttonAction = findViewById(R.id.button_action)
|
||||||
|
_textAction = findViewById(R.id.text_action)
|
||||||
|
|
||||||
|
_buttonAction.setOnClickListener {
|
||||||
|
onActionClicked()
|
||||||
|
}
|
||||||
|
|
||||||
|
refresh()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAttachedToWindow() {
|
||||||
|
super.onAttachedToWindow()
|
||||||
|
StateUpdate.instance.onUiChanged.subscribe(this) {
|
||||||
|
_scope?.launch(Dispatchers.Main) {
|
||||||
|
refresh()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
refresh()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDetachedFromWindow() {
|
||||||
|
super.onDetachedFromWindow()
|
||||||
|
StateUpdate.instance.onUiChanged.remove(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onActionClicked() {
|
||||||
|
val st = StateUpdate.instance
|
||||||
|
when (st.uiState) {
|
||||||
|
UpdateUiState.READY -> {
|
||||||
|
val apk = st.uiApkFile ?: return
|
||||||
|
UpdateNotificationManager.cancelAll(context)
|
||||||
|
UpdateInstaller.startInstall(context, st.uiVersion, apk)
|
||||||
|
}
|
||||||
|
UpdateUiState.FAILED -> {
|
||||||
|
if (st.uiVersion == 0) return
|
||||||
|
val intent = Intent(context, UpdateDownloadService::class.java).apply {
|
||||||
|
putExtra(UpdateDownloadService.EXTRA_VERSION, st.uiVersion)
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
ContextCompat.startForegroundService(context, intent)
|
||||||
|
} catch (t: Throwable) {
|
||||||
|
Logger.w(TAG, "Retry start service failed", t)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
UpdateUiState.AVAILABLE -> {
|
||||||
|
if (st.uiVersion == 0) return
|
||||||
|
val intent = Intent(context, UpdateDownloadService::class.java).apply {
|
||||||
|
putExtra(UpdateDownloadService.EXTRA_VERSION, st.uiVersion)
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
ContextCompat.startForegroundService(context, intent)
|
||||||
|
} catch (t: Throwable) {
|
||||||
|
Logger.w(TAG, "Download start service failed", t)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
UpdateUiState.DOWNLOADING -> {}
|
||||||
|
UpdateUiState.NONE -> {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun refresh() {
|
||||||
|
val st = StateUpdate.instance
|
||||||
|
val gateOpen = Settings.instance.autoUpdate.shouldBackgroundDownload
|
||||||
|
val visible = gateOpen && st.uiState != UpdateUiState.NONE
|
||||||
|
|
||||||
|
if (!visible) {
|
||||||
|
_root.visibility = View.GONE
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_root.visibility = View.VISIBLE
|
||||||
|
|
||||||
|
when (st.uiState) {
|
||||||
|
UpdateUiState.AVAILABLE -> {
|
||||||
|
_textTitle.text = "Update v${st.uiVersion}"
|
||||||
|
_progressBar.visibility = View.GONE
|
||||||
|
_textAction.text = "Download"
|
||||||
|
_buttonAction.visibility = View.VISIBLE
|
||||||
|
}
|
||||||
|
UpdateUiState.DOWNLOADING -> {
|
||||||
|
if (st.uiIndeterminate) {
|
||||||
|
_textTitle.text = "Downloading v${st.uiVersion}"
|
||||||
|
_progressBar.isIndeterminate = true
|
||||||
|
} else {
|
||||||
|
_textTitle.text = "Downloading v${st.uiVersion} - ${st.uiProgress}%"
|
||||||
|
_progressBar.isIndeterminate = false
|
||||||
|
_progressBar.progress = st.uiProgress
|
||||||
|
}
|
||||||
|
_progressBar.visibility = View.VISIBLE
|
||||||
|
_buttonAction.visibility = View.GONE
|
||||||
|
}
|
||||||
|
UpdateUiState.READY -> {
|
||||||
|
_textTitle.text = "Ready v${st.uiVersion}"
|
||||||
|
_progressBar.visibility = View.GONE
|
||||||
|
_textAction.text = "Install"
|
||||||
|
_buttonAction.visibility = View.VISIBLE
|
||||||
|
}
|
||||||
|
UpdateUiState.FAILED -> {
|
||||||
|
_textTitle.text = "Update failed"
|
||||||
|
_progressBar.visibility = View.GONE
|
||||||
|
_textAction.text = "Retry"
|
||||||
|
_buttonAction.visibility = View.VISIBLE
|
||||||
|
}
|
||||||
|
UpdateUiState.NONE -> {
|
||||||
|
_root.visibility = View.GONE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val TAG = "UpdateBannerView"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -20,11 +20,20 @@
|
|||||||
app:layout_constraintRight_toRightOf="parent"
|
app:layout_constraintRight_toRightOf="parent"
|
||||||
tools:layout="@layout/fragment_overview_top_bar" />
|
tools:layout="@layout/fragment_overview_top_bar" />
|
||||||
|
|
||||||
|
<com.futo.platformplayer.views.announcements.UpdateBannerView
|
||||||
|
android:id="@+id/update_banner"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/fragment_top_bar"
|
||||||
|
app:layout_constraintLeft_toLeftOf="parent"
|
||||||
|
app:layout_constraintRight_toRightOf="parent" />
|
||||||
|
|
||||||
<androidx.fragment.app.FragmentContainerView
|
<androidx.fragment.app.FragmentContainerView
|
||||||
android:id="@+id/fragment_main"
|
android:id="@+id/fragment_main"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="0dp"
|
android:layout_height="0dp"
|
||||||
app:layout_constraintTop_toBottomOf="@id/fragment_top_bar"
|
app:layout_constraintTop_toBottomOf="@id/update_banner"
|
||||||
app:layout_constraintLeft_toLeftOf="parent"
|
app:layout_constraintLeft_toLeftOf="parent"
|
||||||
app:layout_constraintRight_toRightOf="parent"
|
app:layout_constraintRight_toRightOf="parent"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
android:paddingEnd="12dp"
|
android:paddingEnd="12dp"
|
||||||
android:background="@drawable/background_pill"
|
android:background="@drawable/background_pill"
|
||||||
android:layout_marginEnd="6dp"
|
android:layout_marginEnd="6dp"
|
||||||
android:layout_marginTop="17dp"
|
android:layout_marginTop="8dp"
|
||||||
android:layout_marginBottom="8dp"
|
android:layout_marginBottom="8dp"
|
||||||
android:id="@+id/root">
|
android:id="@+id/root">
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
@@ -36,4 +36,4 @@
|
|||||||
tools:text="Tag text" />
|
tools:text="Tag text" />
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
</FrameLayout>
|
</FrameLayout>
|
||||||
|
|||||||
@@ -0,0 +1,71 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:id="@+id/root">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:background="@drawable/background_16_round_4dp"
|
||||||
|
android:layout_marginLeft="10dp"
|
||||||
|
android:layout_marginTop="6dp"
|
||||||
|
android:layout_marginRight="10dp"
|
||||||
|
android:layout_marginBottom="0dp"
|
||||||
|
android:minHeight="40dp"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:paddingLeft="12dp"
|
||||||
|
android:paddingTop="6dp"
|
||||||
|
android:paddingRight="8dp"
|
||||||
|
android:paddingBottom="6dp">
|
||||||
|
|
||||||
|
<ImageView android:id="@+id/icon_update"
|
||||||
|
android:layout_width="18dp"
|
||||||
|
android:layout_height="18dp"
|
||||||
|
android:src="@drawable/ic_update"
|
||||||
|
android:layout_marginRight="10dp"
|
||||||
|
android:alpha="0.9"
|
||||||
|
android:importantForAccessibility="no" />
|
||||||
|
|
||||||
|
<TextView android:id="@+id/text_title"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
tools:text="Downloading v123 - 42%"
|
||||||
|
android:fontFamily="@font/inter_semibold"
|
||||||
|
android:textSize="14sp"
|
||||||
|
android:textColor="@color/white"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:maxLines="1" />
|
||||||
|
|
||||||
|
<ProgressBar android:id="@+id/update_banner_progress"
|
||||||
|
style="?android:attr/progressBarStyleHorizontal"
|
||||||
|
android:layout_width="78dp"
|
||||||
|
android:layout_height="4dp"
|
||||||
|
android:layout_marginLeft="10dp"
|
||||||
|
android:layout_marginRight="4dp"
|
||||||
|
android:max="100"
|
||||||
|
android:visibility="gone"
|
||||||
|
tools:visibility="visible" />
|
||||||
|
|
||||||
|
<FrameLayout android:id="@+id/button_action"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="28dp"
|
||||||
|
android:layout_marginLeft="10dp"
|
||||||
|
android:background="@drawable/background_button_primary_round_4dp">
|
||||||
|
|
||||||
|
<TextView android:id="@+id/text_action"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:gravity="center"
|
||||||
|
tools:text="Install"
|
||||||
|
android:fontFamily="@font/inter_regular"
|
||||||
|
android:textSize="12sp"
|
||||||
|
android:textColor="@color/white"
|
||||||
|
android:paddingLeft="13dp"
|
||||||
|
android:paddingRight="13dp" />
|
||||||
|
</FrameLayout>
|
||||||
|
</LinearLayout>
|
||||||
|
</FrameLayout>
|
||||||
Submodule app/src/stable/assets/sources/apple-podcasts updated: 9c65475be1...8d9dee8a49
Submodule app/src/stable/assets/sources/bilibili updated: 9186672f0f...c63c69beec
Submodule app/src/stable/assets/sources/bitchute updated: b213f91c0b...deed10c077
Submodule app/src/stable/assets/sources/crunchyroll updated: a1714790c5...499ab8b438
Submodule app/src/stable/assets/sources/curiositystream updated: 1ebf5da236...68f85a0d62
Submodule app/src/stable/assets/sources/dailymotion updated: 70f625a3bd...256b8433e0
Submodule app/src/stable/assets/sources/fosdem updated: 2231fbec11...e8fe3b4bb5
Submodule app/src/stable/assets/sources/mixcloud updated: 1b801553b3...c107d15296
Submodule app/src/stable/assets/sources/nebula updated: 090cd76dfa...84e920f378
Submodule app/src/stable/assets/sources/odysee updated: 1c7a8a4974...c6e462db9b
Submodule app/src/stable/assets/sources/patreon updated: 52154f36c2...87b168a7cb
Submodule app/src/stable/assets/sources/peertube updated: 7b52405ad0...c955d8ed56
Submodule app/src/stable/assets/sources/redbull-tv updated: 179b7a6e22...7f4317f5c7
Submodule app/src/stable/assets/sources/soundcloud updated: e785c5d8c9...8ed7c19c45
Submodule app/src/stable/assets/sources/tedtalks updated: 292e459eef...f7f31a4f9a
Submodule app/src/stable/assets/sources/twitch updated: cebdad37a3...3a46d407de
Submodule app/src/stable/assets/sources/youtube updated: fb90a44f83...746b390387
Submodule app/src/unstable/assets/sources/apple-podcasts updated: 9c65475be1...8d9dee8a49
Submodule app/src/unstable/assets/sources/bilibili updated: 9186672f0f...c63c69beec
Submodule app/src/unstable/assets/sources/bitchute updated: b213f91c0b...deed10c077
Submodule app/src/unstable/assets/sources/crunchyroll updated: a1714790c5...499ab8b438
Submodule app/src/unstable/assets/sources/curiositystream updated: 1ebf5da236...68f85a0d62
Submodule app/src/unstable/assets/sources/dailymotion updated: 70f625a3bd...256b8433e0
Submodule app/src/unstable/assets/sources/fosdem updated: 2231fbec11...e8fe3b4bb5
Submodule app/src/unstable/assets/sources/mixcloud updated: 1b801553b3...c107d15296
Submodule app/src/unstable/assets/sources/nebula updated: 090cd76dfa...84e920f378
Submodule app/src/unstable/assets/sources/odysee updated: 1c7a8a4974...c6e462db9b
Submodule app/src/unstable/assets/sources/patreon updated: 52154f36c2...87b168a7cb
Submodule app/src/unstable/assets/sources/peertube updated: 7b52405ad0...c955d8ed56
Submodule app/src/unstable/assets/sources/redbull-tv updated: 179b7a6e22...7f4317f5c7
Submodule app/src/unstable/assets/sources/soundcloud updated: e785c5d8c9...8ed7c19c45
Submodule app/src/unstable/assets/sources/tedtalks updated: 292e459eef...f7f31a4f9a
Submodule app/src/unstable/assets/sources/twitch updated: cebdad37a3...3a46d407de
Submodule app/src/unstable/assets/sources/youtube updated: fb90a44f83...746b390387
Reference in New Issue
Block a user