mirror of
https://gitlab.futo.org/videostreaming/grayjay.git
synced 2026-05-16 04:52:39 +02:00
Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay into sabr
This commit is contained in:
@@ -261,5 +261,12 @@
|
||||
android:name=".UpdateActionReceiver"
|
||||
android:exported="false" />
|
||||
|
||||
<activity
|
||||
android:name=".activities.InstallUpdateActivity"
|
||||
android:exported="false"
|
||||
android:theme="@style/Theme.App.TransparentNoUi"
|
||||
android:excludeFromRecents="true"
|
||||
android:finishOnTaskLaunch="true" />
|
||||
|
||||
</application>
|
||||
</manifest>
|
||||
|
||||
@@ -875,9 +875,9 @@ class Settings : FragmentedStorageFileJson() {
|
||||
@DropdownFieldOptionsId(R.array.auto_update_when_array)
|
||||
var check: Int = 0;
|
||||
|
||||
@FormField(R.string.background_download, FieldForm.DROPDOWN, R.string.configure_if_background_download_should_be_used, 1)
|
||||
@DropdownFieldOptionsId(R.array.background_download)
|
||||
var backgroundDownload: Int = 0;
|
||||
@FormField(R.string.background_download, FieldForm.TOGGLE, R.string.configure_if_background_download_should_be_used, 1)
|
||||
//@DropdownFieldOptionsId(R.array.background_download)
|
||||
var shouldBackgroundDownload: Boolean = false;
|
||||
|
||||
@FormField(R.string.download_when, FieldForm.DROPDOWN, R.string.configure_when_updates_should_be_downloaded, 2)
|
||||
@DropdownFieldOptionsId(R.array.when_download)
|
||||
@@ -1052,6 +1052,8 @@ class Settings : FragmentedStorageFileJson() {
|
||||
|
||||
@FormField(R.string.polycentric_local_cache, FieldForm.TOGGLE, R.string.polycentric_local_cache_description, 7)
|
||||
var polycentricLocalCache: Boolean = true;
|
||||
|
||||
var showPrivacyModeDialog: Boolean = true;
|
||||
}
|
||||
|
||||
@FormField(R.string.gesture_controls, FieldForm.GROUP, -1, 19)
|
||||
|
||||
@@ -370,17 +370,19 @@ class UIDialogs {
|
||||
}
|
||||
|
||||
|
||||
fun showConfirmationDialog(context: Context, text: String, action: () -> Unit, cancelAction: (() -> Unit)? = null) {
|
||||
fun showConfirmationDialog(context: Context, text: String, action: () -> Unit, cancelAction: (() -> Unit)? = null, dismissAction: (() -> Unit)? = null): AlertDialog {
|
||||
val confirmButtonAction = Action(context.getString(R.string.confirm), action, ActionStyle.PRIMARY)
|
||||
val cancelButtonAction = Action(context.getString(R.string.cancel), cancelAction ?: {}, ActionStyle.ACCENT)
|
||||
showDialog(context, R.drawable.ic_error, text, null, null, 0, cancelButtonAction, confirmButtonAction)
|
||||
return showDialog(context, R.drawable.ic_error, text, null, null, 0, cancelButtonAction, confirmButtonAction).apply {
|
||||
setOnDismissListener { dismissAction?.invoke() }
|
||||
}
|
||||
}
|
||||
|
||||
fun showConfirmationDialog(context: Context, text: String, action: () -> Unit, cancelAction: (() -> Unit)? = null, doNotAskAgainAction: (() -> Unit)? = null) {
|
||||
fun showConfirmationDialog(context: Context, text: String, action: () -> Unit, cancelAction: (() -> Unit)? = null, dismissAction: (() -> Unit)? = null, doNotAskAgainAction: (() -> Unit)? = null): AlertDialog {
|
||||
val confirmButtonAction = Action(context.getString(R.string.confirm), action, ActionStyle.PRIMARY)
|
||||
val cancelButtonAction = Action(context.getString(R.string.cancel), cancelAction ?: {}, ActionStyle.ACCENT)
|
||||
val doNotAskAgain = Action(context.getString(R.string.do_not_ask_again), doNotAskAgainAction ?: {}, ActionStyle.NONE)
|
||||
showDialog(context, R.drawable.ic_error, text, null, null, 0, doNotAskAgain, cancelButtonAction, confirmButtonAction)
|
||||
return showDialog(context, R.drawable.ic_error, text, null, null, 0, doNotAskAgain, cancelButtonAction, confirmButtonAction)
|
||||
}
|
||||
|
||||
fun showUpdateAvailableDialog(context: Context, lastVersion: Int, hideExceptionButtons: Boolean = false) {
|
||||
|
||||
@@ -6,6 +6,7 @@ import android.content.Intent
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
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
|
||||
|
||||
@@ -16,11 +17,12 @@ class UpdateActionReceiver : BroadcastReceiver() {
|
||||
UpdateNotificationManager.ACTION_UPDATE_NO -> handleUpdateNo(context)
|
||||
UpdateNotificationManager.ACTION_UPDATE_NEVER -> handleUpdateNever(context)
|
||||
UpdateNotificationManager.ACTION_DOWNLOAD_CANCEL -> handleDownloadCancel(context, intent)
|
||||
UpdateNotificationManager.ACTION_INSTALL_NOW -> handleInstallNow(context, intent)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleUpdateYes(context: Context, intent: Intent) {
|
||||
AutoUpdateDialog.currentDialog?.dismiss()
|
||||
|
||||
val version = intent.getIntExtra(UpdateNotificationManager.EXTRA_VERSION, 0)
|
||||
if (version == 0) {
|
||||
return
|
||||
@@ -28,31 +30,19 @@ class UpdateActionReceiver : BroadcastReceiver() {
|
||||
|
||||
NotificationManagerCompat.from(context).cancel(UpdateNotificationManager.NOTIF_ID_AVAILABLE)
|
||||
|
||||
if (Settings.instance.autoUpdate.backgroundDownload == 1) {
|
||||
val serviceIntent = Intent(context, UpdateDownloadService::class.java).apply {
|
||||
putExtra(UpdateDownloadService.EXTRA_VERSION, version)
|
||||
}
|
||||
ContextCompat.startForegroundService(context, serviceIntent)
|
||||
} else {
|
||||
if (StateApp.instance.isMainActive) {
|
||||
StateApp.withContext { ctx ->
|
||||
UIDialogs.showUpdateAvailableDialog(ctx, version, false)
|
||||
}
|
||||
} else {
|
||||
val startIntent = Intent(context, MainActivity::class.java).apply {
|
||||
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP)
|
||||
putExtra("SHOW_UPDATE_DIALOG_VERSION", version)
|
||||
}
|
||||
context.startActivity(startIntent)
|
||||
}
|
||||
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()
|
||||
|
||||
@@ -70,21 +60,4 @@ class UpdateActionReceiver : BroadcastReceiver() {
|
||||
|
||||
NotificationManagerCompat.from(context).cancel(UpdateNotificationManager.NOTIF_ID_DOWNLOADING)
|
||||
}
|
||||
|
||||
private fun handleInstallNow(context: Context, intent: Intent) {
|
||||
val version = intent.getIntExtra(UpdateNotificationManager.EXTRA_VERSION, 0)
|
||||
val apkPath = intent.getStringExtra(UpdateNotificationManager.EXTRA_APK_PATH)
|
||||
|
||||
if (version == 0 || apkPath.isNullOrEmpty()) {
|
||||
return
|
||||
}
|
||||
|
||||
val apkFile = File(apkPath)
|
||||
if (!apkFile.exists()) {
|
||||
return
|
||||
}
|
||||
|
||||
UpdateNotificationManager.cancelAll(context)
|
||||
UpdateInstaller.startInstall(context, apkFile)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
package com.futo.platformplayer
|
||||
|
||||
import android.app.Dialog
|
||||
import android.app.Service
|
||||
import android.content.Intent
|
||||
import android.os.IBinder
|
||||
import android.os.SystemClock
|
||||
import com.futo.platformplayer.UIDialogs.ActionStyle
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.states.StateApp
|
||||
import com.futo.platformplayer.states.StateUpdate
|
||||
@@ -21,6 +24,9 @@ class UpdateDownloadService : Service() {
|
||||
private const val MAX_RETRIES = 5
|
||||
private const val INITIAL_BACKOFF_MS = 5_000L
|
||||
private const val BUFFER_SIZE = 8 * 1024
|
||||
private const val MIN_PROGRESS_UPDATE_INTERVAL_MS = 500L
|
||||
|
||||
var updateDownloadedDialog: Dialog? = null
|
||||
}
|
||||
|
||||
private val job = SupervisorJob()
|
||||
@@ -32,6 +38,8 @@ class UpdateDownloadService : Service() {
|
||||
@Volatile
|
||||
private var cancelRequested: Boolean = false
|
||||
|
||||
private var lastProgressUpdateElapsedMs: Long = 0L
|
||||
|
||||
override fun onBind(intent: Intent?): IBinder? = null
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
@@ -77,6 +85,16 @@ class UpdateDownloadService : Service() {
|
||||
job.cancel()
|
||||
}
|
||||
|
||||
private fun throttledUpdateDownloadProgress(version: Int, progress: Int, indeterminate: Boolean) {
|
||||
val now = SystemClock.elapsedRealtime()
|
||||
val force = progress == 100 && !indeterminate
|
||||
|
||||
if (force || now - lastProgressUpdateElapsedMs >= MIN_PROGRESS_UPDATE_INTERVAL_MS) {
|
||||
lastProgressUpdateElapsedMs = now
|
||||
UpdateNotificationManager.updateDownloadProgress(this, version, progress, indeterminate)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun downloadApk(version: Int) {
|
||||
val apkFile = StateUpdate.getApkFile(this, version)
|
||||
val partialFile = StateUpdate.getPartialApkFile(this, version)
|
||||
@@ -186,12 +204,18 @@ class UpdateDownloadService : Service() {
|
||||
progress > 100 -> 100
|
||||
else -> progress
|
||||
}
|
||||
UpdateNotificationManager.updateDownloadProgress(this, version, safeProgress, false)
|
||||
throttledUpdateDownloadProgress(version, safeProgress, indeterminate = false)
|
||||
}
|
||||
} else {
|
||||
UpdateNotificationManager.updateDownloadProgress(this, version, 0, true)
|
||||
throttledUpdateDownloadProgress(version, progress = 0, indeterminate = true)
|
||||
}
|
||||
}
|
||||
|
||||
if (!cancelRequested && totalBytes > 0L) {
|
||||
val finalProgress = 100
|
||||
throttledUpdateDownloadProgress(version, finalProgress, indeterminate = false)
|
||||
}
|
||||
|
||||
output.flush()
|
||||
}
|
||||
}
|
||||
@@ -216,12 +240,19 @@ class UpdateDownloadService : Service() {
|
||||
StateApp.instance.scopeOrNull?.launch(Dispatchers.Main) {
|
||||
StateApp.withContext { ctx ->
|
||||
try {
|
||||
UIDialogs.showConfirmationDialog(ctx, "Update downloaded, press confirm to install", {
|
||||
UpdateNotificationManager.cancelAll(ctx)
|
||||
UpdateInstaller.startInstall(ctx, apkFile)
|
||||
}, {})
|
||||
updateDownloadedDialog = UIDialogs.showDialog(ctx, R.drawable.foreground,
|
||||
"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));
|
||||
} catch (t: Throwable) {
|
||||
Logger.w(TAG, "Failed to show in-app update downloaded dialog", t)
|
||||
updateDownloadedDialog = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,9 @@ import android.app.PendingIntent.getBroadcast
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageInstaller
|
||||
import android.graphics.drawable.Animatable
|
||||
import android.provider.Settings
|
||||
import android.view.View
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.receivers.InstallReceiver
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -17,20 +19,24 @@ import kotlinx.coroutines.withContext
|
||||
import java.io.File
|
||||
import java.io.InputStream
|
||||
import androidx.core.net.toUri
|
||||
import com.futo.platformplayer.dialogs.AutoUpdateDialog
|
||||
import com.futo.platformplayer.states.StateApp
|
||||
|
||||
object UpdateInstaller {
|
||||
private const val TAG = "UpdateInstaller"
|
||||
|
||||
@SuppressLint("RequestInstallPackagesPolicy")
|
||||
fun startInstall(context: Context, apkFile: File) {
|
||||
fun startInstall(context: Context, version: Int, apkFile: File) {
|
||||
if (!apkFile.exists()) {
|
||||
Logger.w(TAG, "APK file does not exist: ${apkFile.absolutePath}")
|
||||
UIDialogs.toast(context, "Update file missing")
|
||||
UpdateNotificationManager.showInstallFailedNotification(context, version, apkFile, "APK file does not exist.")
|
||||
return
|
||||
}
|
||||
|
||||
if (BuildConfig.IS_PLAYSTORE_BUILD) {
|
||||
UIDialogs.toast(context, "Updates are managed by the Play Store")
|
||||
UpdateNotificationManager.showInstallFailedNotification(context, version, apkFile, "Updates are managed by the Play Store.")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -38,6 +44,7 @@ object UpdateInstaller {
|
||||
val pm = context.packageManager
|
||||
if (!pm.canRequestPackageInstalls()) {
|
||||
UIDialogs.toast(context, "Allow this app to install updates, then try again")
|
||||
UpdateNotificationManager.showInstallFailedNotification(context, version, apkFile, "Install update permission was missing.")
|
||||
|
||||
val intent = Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES).apply {
|
||||
data = "package:${context.packageName}".toUri()
|
||||
@@ -53,8 +60,8 @@ object UpdateInstaller {
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
var inputStream: InputStream? = null
|
||||
var session: PackageInstaller.Session? = null
|
||||
|
||||
try {
|
||||
|
||||
val packageInstaller: PackageInstaller = context.packageManager.packageInstaller
|
||||
val params = PackageInstaller.SessionParams(PackageInstaller.SessionParams.MODE_FULL_INSTALL)
|
||||
val sessionId = packageInstaller.createSession(params)
|
||||
@@ -68,10 +75,17 @@ object UpdateInstaller {
|
||||
session.fsync(sessionStream)
|
||||
}
|
||||
|
||||
val intent = Intent(context, InstallReceiver::class.java)
|
||||
val intent = Intent(context, InstallReceiver::class.java).apply {
|
||||
putExtra(UpdateNotificationManager.EXTRA_VERSION, version)
|
||||
putExtra(UpdateNotificationManager.EXTRA_APK_PATH, apkFile.absolutePath)
|
||||
}
|
||||
val pendingIntent = getBroadcast(context, 0, intent, FLAG_MUTABLE or FLAG_UPDATE_CURRENT)
|
||||
val statusReceiver = pendingIntent.intentSender
|
||||
|
||||
InstallReceiver.onReceiveResult.subscribe(this) { message ->
|
||||
InstallReceiver.onReceiveResult.clear();
|
||||
onReceiveResult(context, version, apkFile, message);
|
||||
};
|
||||
Logger.i(TAG, "Committing install session for ${apkFile.absolutePath}")
|
||||
session.commit(statusReceiver)
|
||||
} catch (e: Throwable) {
|
||||
@@ -80,10 +94,29 @@ object UpdateInstaller {
|
||||
withContext(Dispatchers.Main) {
|
||||
UIDialogs.toast(context, "Failed to install update: ${e.message}")
|
||||
}
|
||||
|
||||
UpdateNotificationManager.showInstallFailedNotification(context, version, apkFile, e.message)
|
||||
} finally {
|
||||
session?.close()
|
||||
inputStream?.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun onReceiveResult(context: Context, version: Int, apkFile: File, result: String?) {
|
||||
try {
|
||||
InstallReceiver.onReceiveResult.remove(this)
|
||||
|
||||
if (result.isNullOrEmpty()) {
|
||||
Logger.i(TAG, "Update install finished successfully")
|
||||
UpdateNotificationManager.showInstallSucceededNotification(context, version)
|
||||
} else {
|
||||
Logger.w(TAG, "Update install failed: $result")
|
||||
UpdateNotificationManager.showInstallFailedNotification(context, version, apkFile, result)
|
||||
UIDialogs.showGeneralErrorDialog(context, "Install failed due to:\n$result")
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
Logger.e(TAG, "Failed to handle install result", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import android.Manifest
|
||||
import android.app.Notification
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.app.PendingIntent
|
||||
import android.app.PendingIntent.FLAG_MUTABLE
|
||||
import android.app.PendingIntent.FLAG_UPDATE_CURRENT
|
||||
import android.app.PendingIntent.getBroadcast
|
||||
@@ -13,6 +14,7 @@ import android.content.pm.PackageManager
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.futo.platformplayer.activities.InstallUpdateActivity
|
||||
import java.io.File
|
||||
|
||||
object UpdateNotificationManager {
|
||||
@@ -25,6 +27,7 @@ object UpdateNotificationManager {
|
||||
const val ACTION_UPDATE_NEVER = "com.futo.platformplayer.UPDATE_NEVER"
|
||||
const val ACTION_DOWNLOAD_CANCEL = "com.futo.platformplayer.UPDATE_CANCEL"
|
||||
const val ACTION_INSTALL_NOW = "com.futo.platformplayer.UPDATE_INSTALL"
|
||||
private const val REQUEST_CODE_INSTALL = 1001
|
||||
|
||||
const val EXTRA_VERSION = "version"
|
||||
const val EXTRA_APK_PATH = "apk_path"
|
||||
@@ -32,16 +35,55 @@ object UpdateNotificationManager {
|
||||
const val NOTIF_ID_AVAILABLE = 2001
|
||||
const val NOTIF_ID_DOWNLOADING = 2002
|
||||
const val NOTIF_ID_READY = 2003
|
||||
const val NOTIF_ID_INSTALL_FAILED = 2004
|
||||
const val NOTIF_ID_INSTALL_SUCCEEDED = 2005
|
||||
|
||||
fun ensureChannel(context: Context) {
|
||||
val manager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||
if (manager.getNotificationChannel(CHANNEL_ID) == null) {
|
||||
val channel = NotificationChannel(CHANNEL_ID, CHANNEL_NAME, NotificationManager.IMPORTANCE_DEFAULT)
|
||||
channel.description = CHANNEL_DESCRIPTION
|
||||
val channel = NotificationChannel(CHANNEL_ID, CHANNEL_NAME, NotificationManager.IMPORTANCE_DEFAULT).apply {
|
||||
description = CHANNEL_DESCRIPTION
|
||||
enableVibration(false)
|
||||
enableLights(false)
|
||||
setSound(null, null)
|
||||
}
|
||||
manager.createNotificationChannel(channel)
|
||||
}
|
||||
}
|
||||
|
||||
fun showInstallSucceededNotification(context: Context, version: Int) {
|
||||
if (ContextCompat.checkSelfPermission(context, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {
|
||||
return
|
||||
}
|
||||
|
||||
ensureChannel(context)
|
||||
|
||||
val launchIntent = context.packageManager
|
||||
.getLaunchIntentForPackage(context.packageName)
|
||||
?.apply {
|
||||
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED)
|
||||
}
|
||||
|
||||
val launchPendingIntent = launchIntent?.let {
|
||||
PendingIntent.getActivity(context, REQUEST_CODE_INSTALL, it, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
|
||||
}
|
||||
|
||||
val builder = NotificationCompat.Builder(context, CHANNEL_ID)
|
||||
.setSmallIcon(R.drawable.foreground)
|
||||
.setContentTitle("Update installed")
|
||||
.setContentText("Version $version installed. Tap to open.")
|
||||
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
|
||||
.setAutoCancel(true)
|
||||
.setSilent(true)
|
||||
|
||||
if (launchPendingIntent != null) {
|
||||
builder.setContentIntent(launchPendingIntent)
|
||||
builder.addAction(0, "Open app", launchPendingIntent)
|
||||
}
|
||||
|
||||
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
|
||||
@@ -70,9 +112,11 @@ object UpdateNotificationManager {
|
||||
.setContentText("A new version ($version) is available.")
|
||||
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
|
||||
.setAutoCancel(true)
|
||||
.addAction(0, "Download", yesPendingIntent)
|
||||
.addAction(0, "Not now", noPendingIntent)
|
||||
.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())
|
||||
}
|
||||
@@ -95,8 +139,9 @@ object UpdateNotificationManager {
|
||||
.setSmallIcon(R.drawable.foreground)
|
||||
.setContentTitle("Downloading update")
|
||||
.setContentText("Downloading version $version")
|
||||
.setPriority(NotificationCompat.PRIORITY_LOW)
|
||||
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
|
||||
.setOngoing(true)
|
||||
.setSilent(true)
|
||||
.addAction(0, "Cancel", cancelPendingIntent)
|
||||
|
||||
if (indeterminate) {
|
||||
@@ -123,24 +168,17 @@ object UpdateNotificationManager {
|
||||
}
|
||||
ensureChannel(context)
|
||||
|
||||
val installIntent = Intent(context, UpdateActionReceiver::class.java).apply {
|
||||
action = ACTION_INSTALL_NOW
|
||||
putExtra(EXTRA_VERSION, version)
|
||||
putExtra(EXTRA_APK_PATH, apkFile.absolutePath)
|
||||
}
|
||||
val installPendingIntent = getBroadcast(
|
||||
context,
|
||||
4,
|
||||
installIntent,
|
||||
FLAG_MUTABLE or FLAG_UPDATE_CURRENT
|
||||
)
|
||||
val installIntent = InstallUpdateActivity.createIntent(context, version, apkFile.absolutePath)
|
||||
val installPendingIntent = PendingIntent.getActivity(context, REQUEST_CODE_INSTALL, installIntent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
|
||||
|
||||
val builder = NotificationCompat.Builder(context, CHANNEL_ID)
|
||||
.setSmallIcon(R.drawable.foreground)
|
||||
.setContentTitle("Update downloaded")
|
||||
.setContentText("Tap to install version $version.")
|
||||
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
|
||||
.setContentIntent(installPendingIntent)
|
||||
.setAutoCancel(true)
|
||||
.setSilent(true)
|
||||
.addAction(0, "Install", installPendingIntent)
|
||||
|
||||
NotificationManagerCompat.from(context).notify(NOTIF_ID_READY, builder.build())
|
||||
@@ -159,13 +197,37 @@ object UpdateNotificationManager {
|
||||
.setContentText(error?.message ?: "Unknown error")
|
||||
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
|
||||
.setAutoCancel(true)
|
||||
.setSilent(true)
|
||||
|
||||
NotificationManagerCompat.from(context).notify(NOTIF_ID_READY, builder.build())
|
||||
}
|
||||
|
||||
fun showInstallFailedNotification(context: Context, version: Int, apkFile: File, error: String?) {
|
||||
if (ContextCompat.checkSelfPermission(context, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED)
|
||||
return
|
||||
|
||||
ensureChannel(context)
|
||||
|
||||
val installIntent = InstallUpdateActivity.createIntent(context, version, apkFile.absolutePath)
|
||||
val installPendingIntent = PendingIntent.getActivity(context, REQUEST_CODE_INSTALL, installIntent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
|
||||
val builder = NotificationCompat.Builder(context, CHANNEL_ID)
|
||||
.setSmallIcon(R.drawable.foreground)
|
||||
.setContentTitle("Failed to install update")
|
||||
.setContentText(if (error != null && error.isNotBlank()) "$error Tap to try again." else "Tap to try again.")
|
||||
.setAutoCancel(true)
|
||||
.setSilent(true)
|
||||
.setContentIntent(installPendingIntent)
|
||||
.addAction(0, "Install again", installPendingIntent)
|
||||
|
||||
NotificationManagerCompat.from(context).notify(NOTIF_ID_INSTALL_FAILED, builder.build())
|
||||
}
|
||||
|
||||
fun cancelAll(context: Context) {
|
||||
NotificationManagerCompat.from(context).cancel(NOTIF_ID_AVAILABLE)
|
||||
NotificationManagerCompat.from(context).cancel(NOTIF_ID_DOWNLOADING)
|
||||
NotificationManagerCompat.from(context).cancel(NOTIF_ID_READY)
|
||||
NotificationManagerCompat.from(context).cancel(NOTIF_ID_INSTALL_FAILED)
|
||||
NotificationManagerCompat.from(context).cancel(NOTIF_ID_INSTALL_SUCCEEDED)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -444,15 +444,9 @@ fun addressScore(addr: InetAddress): Int {
|
||||
|
||||
fun <T> Enumeration<T>.toList(): List<T> = Collections.list(this)
|
||||
|
||||
fun <T> RequestBuilder<T>.withMaxSizePx(maxSizePx: Int = 1920, useCenterCrop: Boolean = false): RequestBuilder<T> {
|
||||
var builder = this
|
||||
.downsample(DownsampleStrategy.AT_MOST)
|
||||
.override(maxSizePx, maxSizePx)
|
||||
builder = if (useCenterCrop) {
|
||||
builder.centerCrop()
|
||||
} else {
|
||||
builder.fitCenter()
|
||||
}
|
||||
|
||||
return builder
|
||||
fun <T> RequestBuilder<T>.withMaxSizePx(maxSizePx: Int = 1920): RequestBuilder<T> {
|
||||
return this;
|
||||
//.downsample(DownsampleStrategy.AT_MOST)
|
||||
//.override(maxSizePx, maxSizePx)
|
||||
//.centerInside()
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package com.futo.platformplayer.activities
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import com.futo.platformplayer.UIDialogs
|
||||
import com.futo.platformplayer.UpdateInstaller
|
||||
import com.futo.platformplayer.UpdateNotificationManager
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import java.io.File
|
||||
|
||||
class InstallUpdateActivity : AppCompatActivity() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
UpdateNotificationManager.cancelAll(this)
|
||||
|
||||
val version = intent.getIntExtra(UpdateNotificationManager.EXTRA_VERSION, 0)
|
||||
val apkPath = intent.getStringExtra(UpdateNotificationManager.EXTRA_APK_PATH)
|
||||
|
||||
if (version == 0 || apkPath.isNullOrEmpty()) {
|
||||
Logger.w("InstallUpdateActivity", "Missing version or apkPath")
|
||||
finish()
|
||||
return
|
||||
}
|
||||
|
||||
val apkFile = File(apkPath)
|
||||
if (!apkFile.exists()) {
|
||||
Logger.w("InstallUpdateActivity", "APK file does not exist: $apkPath")
|
||||
UIDialogs.Companion.toast(this, "Update file missing")
|
||||
finish()
|
||||
return
|
||||
}
|
||||
|
||||
UpdateInstaller.startInstall(this, version, apkFile)
|
||||
finish()
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun createIntent(context: Context, version: Int, apkPath: String): Intent =
|
||||
Intent(context, InstallUpdateActivity::class.java).apply {
|
||||
putExtra(UpdateNotificationManager.EXTRA_VERSION, version)
|
||||
putExtra(UpdateNotificationManager.EXTRA_APK_PATH, apkPath)
|
||||
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -618,8 +618,8 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
||||
sharedPreferences.edit().putBoolean("IsFirstBoot", false).apply()
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && Settings.instance.autoUpdate.isAutoUpdateEnabled()) {
|
||||
requestNotificationPermissions("Grayjay uses notifications to inform you when a new app update is available.");
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && Settings.instance.autoUpdate.isAutoUpdateEnabled() && Settings.instance.autoUpdate.shouldBackgroundDownload) {
|
||||
requestNotificationPermissions("You have enabled background updating.\n\nGrayjay uses notifications to inform you when a new app update is available.");
|
||||
}
|
||||
|
||||
val submissionStatus = FragmentedStorage.get<StringStorage>("subscriptionSubmissionStatus")
|
||||
@@ -1299,11 +1299,24 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
||||
navigate(last.first, last.second, false, true);
|
||||
} else {
|
||||
if (_fragVideoDetail.state == VideoDetailFragment.State.CLOSED) {
|
||||
Logger.i(TAG, "Closing activity because _fragVideoDetail.state == closed");
|
||||
finish();
|
||||
} else {
|
||||
//UIDialogs.toast("Grayjay continues in background because of an open video.")
|
||||
if(Settings.instance.playback.isBackgroundPictureInPicture()) {
|
||||
try {
|
||||
_fragVideoDetail._viewDetail?.startPictureInPicture();
|
||||
_fragVideoDetail?.forcePictureInPicture();
|
||||
} catch (ex: Throwable) {
|
||||
} //Fail silently
|
||||
}
|
||||
else
|
||||
moveTaskToBack(false);
|
||||
/*
|
||||
UIDialogs.showConfirmationDialog(this, "There is a video playing, are you sure you want to exit the app?", {
|
||||
finish();
|
||||
})
|
||||
*/
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+318
@@ -0,0 +1,318 @@
|
||||
package com.futo.platformplayer.api.http.server.handlers
|
||||
|
||||
import android.content.ContentResolver
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.provider.MediaStore
|
||||
import android.provider.OpenableColumns
|
||||
import com.futo.platformplayer.api.http.server.HttpContext
|
||||
import com.futo.platformplayer.api.http.server.HttpHeaders
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import java.io.FileNotFoundException
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
|
||||
class HttpContentUriHandler(
|
||||
method: String,
|
||||
path: String,
|
||||
private val contentResolver: ContentResolver,
|
||||
private val uri: Uri,
|
||||
private val explicitContentType: String? = null
|
||||
) : HttpHandler(method, path) {
|
||||
|
||||
override fun handle(httpContext: HttpContext) {
|
||||
val resolver = contentResolver
|
||||
val requestHeaders = httpContext.headers
|
||||
val responseHeaders = this.headers.clone()
|
||||
|
||||
val meta = try {
|
||||
queryMetadata(resolver, uri)
|
||||
} catch (e: Exception) {
|
||||
Logger.e(TAG, "Failed to query metadata for $uri", e)
|
||||
httpContext.respondCode(404, responseHeaders)
|
||||
return
|
||||
}
|
||||
|
||||
val contentType = explicitContentType
|
||||
?: resolver.getType(uri)
|
||||
?: "application/octet-stream"
|
||||
responseHeaders["Content-Type"] = contentType
|
||||
|
||||
meta.lastModifiedMillis?.let { lastModified ->
|
||||
responseHeaders["Last-Modified"] = httpDateFormat.format(Date(lastModified))
|
||||
|
||||
val ifModifiedSinceHeader = requestHeaders["If-Modified-Since"]
|
||||
if (ifModifiedSinceHeader != null) {
|
||||
val ifModifiedSince = try {
|
||||
httpDateFormat.parse(ifModifiedSinceHeader)
|
||||
} catch (_: Exception) {
|
||||
null
|
||||
}
|
||||
|
||||
if (ifModifiedSince != null && lastModified <= ifModifiedSince.time) {
|
||||
httpContext.respondCode(304, responseHeaders)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val safeName = (meta.displayName ?: "content.bin").replace("\"", "\\\"")
|
||||
responseHeaders["Content-Disposition"] = "attachment; filename=\"$safeName\""
|
||||
|
||||
val length = meta.size
|
||||
if (length == null) {
|
||||
Logger.i(TAG, "Streaming $uri with unknown length; Range not supported")
|
||||
responseHeaders.remove("Content-Length")
|
||||
responseHeaders.remove("Content-Range")
|
||||
responseHeaders.remove("Accept-Ranges")
|
||||
|
||||
stream(
|
||||
httpContext = httpContext,
|
||||
resolver = resolver,
|
||||
uri = uri,
|
||||
statusCode = 200,
|
||||
headers = responseHeaders,
|
||||
start = null,
|
||||
length = null
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
responseHeaders["Accept-Ranges"] = "bytes"
|
||||
|
||||
val rangeHeader = requestHeaders["Range"]
|
||||
if (rangeHeader.isNullOrBlank()) {
|
||||
responseHeaders["Content-Length"] = length.toString()
|
||||
Logger.i(TAG, "Sending full content for $uri, length=$length")
|
||||
|
||||
stream(
|
||||
httpContext = httpContext,
|
||||
resolver = resolver,
|
||||
uri = uri,
|
||||
statusCode = 200,
|
||||
headers = responseHeaders,
|
||||
start = 0L,
|
||||
length = length
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
val range = parseRange(rangeHeader, length)
|
||||
if (range == null) {
|
||||
Logger.w(TAG, "Invalid Range '$rangeHeader' for $uri (length=$length)")
|
||||
responseHeaders["Content-Range"] = "bytes */$length"
|
||||
httpContext.respondCode(416, responseHeaders)
|
||||
return
|
||||
}
|
||||
|
||||
val start = range.first
|
||||
val endInclusive = range.last
|
||||
val bytesToSend = endInclusive - start + 1
|
||||
|
||||
responseHeaders["Content-Range"] = "bytes $start-$endInclusive/$length"
|
||||
responseHeaders["Content-Length"] = bytesToSend.toString()
|
||||
Logger.i(TAG, "Sending range $start-$endInclusive (length=$bytesToSend) of $length for $uri")
|
||||
|
||||
stream(
|
||||
httpContext = httpContext,
|
||||
resolver = resolver,
|
||||
uri = uri,
|
||||
statusCode = 206,
|
||||
headers = responseHeaders,
|
||||
start = start,
|
||||
length = bytesToSend
|
||||
)
|
||||
}
|
||||
|
||||
data class ContentMeta(
|
||||
val displayName: String?,
|
||||
val size: Long?,
|
||||
val lastModifiedMillis: Long?
|
||||
)
|
||||
|
||||
private fun queryMetadata(resolver: ContentResolver, uri: Uri): ContentMeta {
|
||||
var displayName: String? = null
|
||||
var size: Long? = null
|
||||
var lastModifiedMillis: Long? = null
|
||||
|
||||
resolver.query(uri, null, null, null, null)?.use { cursor ->
|
||||
if (cursor.moveToFirst()) {
|
||||
val nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
|
||||
if (nameIndex != -1 && !cursor.isNull(nameIndex)) {
|
||||
displayName = cursor.getString(nameIndex)
|
||||
}
|
||||
|
||||
val sizeIndex = cursor.getColumnIndex(OpenableColumns.SIZE)
|
||||
if (sizeIndex != -1 && !cursor.isNull(sizeIndex)) {
|
||||
val s = cursor.getLong(sizeIndex)
|
||||
if (s >= 0) size = s // -1 means unknown
|
||||
}
|
||||
|
||||
val dateModifiedIndex = cursor.getColumnIndex(MediaStore.MediaColumns.DATE_MODIFIED)
|
||||
if (dateModifiedIndex != -1 && !cursor.isNull(dateModifiedIndex)) {
|
||||
val seconds = cursor.getLong(dateModifiedIndex)
|
||||
if (seconds > 0) {
|
||||
lastModifiedMillis = seconds * 1000L
|
||||
}
|
||||
}
|
||||
|
||||
if (lastModifiedMillis == null) {
|
||||
val dateAddedIndex = cursor.getColumnIndex(MediaStore.MediaColumns.DATE_ADDED)
|
||||
if (dateAddedIndex != -1 && !cursor.isNull(dateAddedIndex)) {
|
||||
val seconds = cursor.getLong(dateAddedIndex)
|
||||
if (seconds > 0) {
|
||||
lastModifiedMillis = seconds * 1000L
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (displayName == null) {
|
||||
displayName = uri.lastPathSegment
|
||||
}
|
||||
|
||||
if (size == null) {
|
||||
try {
|
||||
resolver.openAssetFileDescriptor(uri, "r")?.use { afd ->
|
||||
val assetLen = afd.length
|
||||
if (assetLen >= 0) {
|
||||
size = assetLen
|
||||
}
|
||||
}
|
||||
} catch (_: Exception) { }
|
||||
}
|
||||
|
||||
return ContentMeta(
|
||||
displayName = displayName,
|
||||
size = size,
|
||||
lastModifiedMillis = lastModifiedMillis
|
||||
)
|
||||
}
|
||||
|
||||
private fun parseRange(header: String, totalLength: Long): LongRange? {
|
||||
if (totalLength <= 0L) return null
|
||||
|
||||
val prefix = "bytes="
|
||||
if (!header.startsWith(prefix, ignoreCase = true)) return null
|
||||
|
||||
val spec = header.substring(prefix.length).trim()
|
||||
if (spec.isEmpty()) return null
|
||||
|
||||
if (spec.contains(",")) return null
|
||||
|
||||
val dashIndex = spec.indexOf('-')
|
||||
if (dashIndex < 0) return null
|
||||
|
||||
val startPart = spec.substring(0, dashIndex).trim()
|
||||
val endPart = spec.substring(dashIndex + 1).trim()
|
||||
|
||||
return when {
|
||||
startPart.isNotEmpty() -> {
|
||||
val start = startPart.toLongOrNull() ?: return null
|
||||
if (start < 0 || start >= totalLength) return null
|
||||
|
||||
val end = if (endPart.isNotEmpty()) {
|
||||
val rawEnd = endPart.toLongOrNull() ?: return null
|
||||
if (rawEnd < start) return null
|
||||
rawEnd.coerceAtMost(totalLength - 1)
|
||||
} else {
|
||||
totalLength - 1
|
||||
}
|
||||
|
||||
start..end
|
||||
}
|
||||
|
||||
endPart.isNotEmpty() -> {
|
||||
val suffixLen = endPart.toLongOrNull() ?: return null
|
||||
if (suffixLen <= 0L) return null
|
||||
|
||||
if (suffixLen >= totalLength) {
|
||||
0L..(totalLength - 1)
|
||||
} else {
|
||||
val start = totalLength - suffixLen
|
||||
val end = totalLength - 1
|
||||
start..end
|
||||
}
|
||||
}
|
||||
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
private fun stream(httpContext: HttpContext, resolver: ContentResolver, uri: Uri, statusCode: Int, headers: HttpHeaders, start: Long?, length: Long?) {
|
||||
try {
|
||||
val input = resolver.openInputStream(uri)
|
||||
if (input == null) {
|
||||
Logger.w(TAG, "Content not found: $uri")
|
||||
httpContext.respondCode(404, headers)
|
||||
return
|
||||
}
|
||||
|
||||
input.use { inputStream ->
|
||||
httpContext.respond(statusCode, headers) { outputStream ->
|
||||
try {
|
||||
val offset = start ?: 0L
|
||||
if (offset > 0L) {
|
||||
skipFully(inputStream, offset)
|
||||
}
|
||||
copyStream(inputStream, outputStream, length)
|
||||
outputStream.flush()
|
||||
} catch (e: Exception) {
|
||||
Logger.e(TAG, "Error while streaming $uri (start=$start, length=$length)", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: FileNotFoundException) {
|
||||
Logger.w(TAG, "Content not found: $uri", e)
|
||||
httpContext.respondCode(404, headers)
|
||||
} catch (e: Exception) {
|
||||
Logger.e(TAG, "Failed to open stream for $uri", e)
|
||||
httpContext.respondCode(500, headers)
|
||||
}
|
||||
}
|
||||
|
||||
private fun copyStream(input: InputStream, output: OutputStream, limit: Long?) {
|
||||
val buffer = ByteArray(8192)
|
||||
if (limit == null) {
|
||||
while (true) {
|
||||
val read = input.read(buffer)
|
||||
if (read < 0) break
|
||||
output.write(buffer, 0, read)
|
||||
}
|
||||
} else {
|
||||
var remaining = limit
|
||||
while (remaining > 0L) {
|
||||
val toRead = remaining.coerceAtMost(buffer.size.toLong()).toInt()
|
||||
val read = input.read(buffer, 0, toRead)
|
||||
if (read < 0) break
|
||||
output.write(buffer, 0, read)
|
||||
remaining -= read.toLong()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun skipFully(input: InputStream, bytesToSkip: Long) {
|
||||
var remaining = bytesToSkip
|
||||
while (remaining > 0L) {
|
||||
val skipped = input.skip(remaining)
|
||||
if (skipped <= 0L) {
|
||||
val b = input.read()
|
||||
if (b == -1) break
|
||||
remaining -= 1L
|
||||
} else {
|
||||
remaining -= skipped
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "HttpContentUriHandler"
|
||||
|
||||
private val httpDateFormat = SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss 'GMT'", Locale.US).apply {
|
||||
timeZone = TimeZone.getTimeZone("GMT")
|
||||
}
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -73,10 +73,10 @@ open class LocalVideoDetails(
|
||||
override val video: IVideoSourceDescriptor = (if(mimeType?.startsWith("audio/") ?: false)
|
||||
(LocalVideoUnMuxedSourceDescriptor(
|
||||
arrayOf(),
|
||||
arrayOf(LocalAudioContentSource(url, mimeType ?: "", name))
|
||||
arrayOf(LocalAudioContentSource(url, mimeType ?: "", name, duration))
|
||||
))
|
||||
else (LocalVideoMuxedSourceDescriptor(
|
||||
LocalVideoContentSource(url, mimeType ?: "", name)
|
||||
LocalVideoContentSource(url, mimeType ?: "", name, duration)
|
||||
))
|
||||
);
|
||||
override val preview: ISerializedVideoSourceDescriptor? = null;
|
||||
|
||||
+30
-4
@@ -17,11 +17,14 @@ import com.futo.platformplayer.getOrThrow
|
||||
import com.futo.platformplayer.invokeV8
|
||||
import com.futo.platformplayer.invokeV8Void
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.states.StateApp
|
||||
import com.futo.platformplayer.states.StateDeveloper
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.serialization.Serializable
|
||||
import java.util.Base64
|
||||
|
||||
class JSRequestExecutor {
|
||||
class JSRequestExecutor: AutoCloseable {
|
||||
private val _plugin: JSClient;
|
||||
private val _config: IV8PluginConfig;
|
||||
private var _executor: V8ValueObject;
|
||||
@@ -29,6 +32,9 @@ class JSRequestExecutor {
|
||||
|
||||
private val hasCleanup: Boolean;
|
||||
|
||||
private var _cleanLock = Any();
|
||||
private var _cleaned: Boolean = false;
|
||||
|
||||
constructor(plugin: JSClient, executor: V8ValueObject) {
|
||||
this._plugin = plugin;
|
||||
this._executor = executor;
|
||||
@@ -102,8 +108,12 @@ class JSRequestExecutor {
|
||||
|
||||
|
||||
open fun cleanup() {
|
||||
if (!hasCleanup || _executor.isClosed)
|
||||
return;
|
||||
synchronized(_cleanLock) {
|
||||
if (!hasCleanup || _executor.isClosed || _cleaned)
|
||||
return;
|
||||
_cleaned = true;
|
||||
}
|
||||
Logger.i("JSRequestExecutor", "JSRequestExecutor cleanup requested");
|
||||
_plugin.busy {
|
||||
if(_plugin is DevJSClient)
|
||||
StateDeveloper.instance.handleDevCall(_plugin.devID, "requestExecutor.executeRequest()") {
|
||||
@@ -125,9 +135,25 @@ class JSRequestExecutor {
|
||||
}
|
||||
}
|
||||
|
||||
protected fun finalize() {
|
||||
override fun close() {
|
||||
cleanup();
|
||||
}
|
||||
|
||||
fun closeAsync() {
|
||||
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO){
|
||||
try {
|
||||
close();
|
||||
}
|
||||
catch(ex: Throwable) {
|
||||
Logger.e("JSRequestExecutor", "Cleanup failed");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
protected fun finalize() {
|
||||
cleanup();
|
||||
}*/
|
||||
}
|
||||
|
||||
//TODO: are these available..?
|
||||
|
||||
+2
-2
@@ -23,10 +23,10 @@ class LocalAudioContentSource : IAudioSource {
|
||||
|
||||
var contentUrl: String;
|
||||
|
||||
constructor(contentUrl: String, mime: String, name: String? = null) {
|
||||
constructor(contentUrl: String, mime: String, name: String? = null, duration: Long = 0) {
|
||||
this.name = name ?: "File";
|
||||
container = mime;
|
||||
duration = 0;
|
||||
this.duration = duration;
|
||||
|
||||
this.contentUrl = contentUrl;
|
||||
}
|
||||
|
||||
+2
-2
@@ -22,12 +22,12 @@ class LocalVideoContentSource: IVideoSource {
|
||||
|
||||
var contentUrl: String;
|
||||
|
||||
constructor(contentUrl: String, mime: String, name: String? = null) {
|
||||
constructor(contentUrl: String, mime: String, name: String? = null, duration: Long = 0) {
|
||||
this.name = name ?: "File";
|
||||
width = 0;
|
||||
height = 0;
|
||||
container = mime;
|
||||
duration = 0;
|
||||
this.duration = duration;
|
||||
this.contentUrl = contentUrl;
|
||||
}
|
||||
}
|
||||
@@ -239,7 +239,7 @@ class CastingDeviceExp(val device: RsCastingDevice) : CastingDevice() {
|
||||
}
|
||||
|
||||
DeviceConnectionState.Disconnected -> {
|
||||
connectionState = CastConnectionState.CONNECTING
|
||||
connectionState = CastConnectionState.DISCONNECTED
|
||||
onConnectionStateChanged.emit(CastConnectionState.DISCONNECTED)
|
||||
}
|
||||
}
|
||||
@@ -268,4 +268,4 @@ class CastingDeviceExp(val device: RsCastingDevice) : CastingDevice() {
|
||||
companion object {
|
||||
private val TAG = "CastingDeviceExp"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import android.content.Context
|
||||
import android.os.Looper
|
||||
import android.util.Log
|
||||
import androidx.annotation.OptIn
|
||||
import androidx.core.net.toUri
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import com.futo.platformplayer.R
|
||||
import com.futo.platformplayer.Settings
|
||||
@@ -14,6 +15,7 @@ import com.futo.platformplayer.api.http.ManagedHttpClient
|
||||
import com.futo.platformplayer.api.http.server.HttpHeaders
|
||||
import com.futo.platformplayer.api.http.server.ManagedHttpServer
|
||||
import com.futo.platformplayer.api.http.server.handlers.HttpConstantHandler
|
||||
import com.futo.platformplayer.api.http.server.handlers.HttpContentUriHandler
|
||||
import com.futo.platformplayer.api.http.server.handlers.HttpFileHandler
|
||||
import com.futo.platformplayer.api.http.server.handlers.HttpFunctionHandler
|
||||
import com.futo.platformplayer.api.http.server.handlers.HttpProxyHandler
|
||||
@@ -34,6 +36,8 @@ import com.futo.platformplayer.api.media.platforms.js.models.sources.JSDashManif
|
||||
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.JSSource
|
||||
import com.futo.platformplayer.api.media.platforms.local.models.sources.LocalAudioContentSource
|
||||
import com.futo.platformplayer.api.media.platforms.local.models.sources.LocalVideoContentSource
|
||||
import com.futo.platformplayer.awaitCancelConverted
|
||||
import com.futo.platformplayer.builders.DashBuilder
|
||||
import com.futo.platformplayer.models.CastingDeviceInfo
|
||||
@@ -235,9 +239,9 @@ abstract class StateCasting {
|
||||
Logger.i(TAG, "Connect to device ${device.name}")
|
||||
}
|
||||
|
||||
fun metadataFromVideo(video: IPlatformVideoDetails): Metadata {
|
||||
fun metadataFromVideo(video: IPlatformVideoDetails, videoThumbnailOverrideUrl: String? = null): Metadata {
|
||||
return Metadata(
|
||||
title = video.name, thumbnailUrl = video.thumbnails.getHQThumbnail()
|
||||
title = video.name, thumbnailUrl = videoThumbnailOverrideUrl ?: video.thumbnails.getHQThumbnail()
|
||||
)
|
||||
}
|
||||
|
||||
@@ -371,6 +375,12 @@ abstract class StateCasting {
|
||||
} else if (audioSource is LocalAudioSource) {
|
||||
Logger.i(TAG, "Casting as local audio");
|
||||
castLocalAudio(video, audioSource, resumePosition, speed);
|
||||
} else if (videoSource is LocalVideoContentSource) {
|
||||
Logger.i(TAG, "Casting as local video");
|
||||
castLocalVideo(contentResolver, video, videoSource, resumePosition, speed);
|
||||
} else if (audioSource is LocalAudioContentSource) {
|
||||
Logger.i(TAG, "Casting as local audio");
|
||||
castLocalAudio(contentResolver, video, audioSource, resumePosition, speed);
|
||||
} else if (videoSource is JSDashManifestRawSource) {
|
||||
Logger.i(TAG, "Casting as JSDashManifestRawSource video");
|
||||
castDashRaw(contentResolver, video, videoSource as JSDashManifestRawSource?, null, null, resumePosition, speed, castId, onLoadingEstimate, onLoading);
|
||||
@@ -461,6 +471,65 @@ abstract class StateCasting {
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private fun castLocalVideo(contentResolver: ContentResolver, video: IPlatformVideoDetails, videoSource: LocalVideoContentSource, resumePosition: Double, speed: Double?) : List<String> {
|
||||
val ad = activeDevice ?: return listOf();
|
||||
|
||||
val url = getLocalUrl(ad);
|
||||
val id = UUID.randomUUID();
|
||||
val videoPath = "/video-${id}"
|
||||
val videoUrl = url + videoPath;
|
||||
val thumbnailPath = "/thumbnail-${id}"
|
||||
val thumbnailUrl = url + thumbnailPath;
|
||||
val thumbnailContentUrl = video.thumbnails.getHQThumbnail()
|
||||
|
||||
if (thumbnailContentUrl != null) {
|
||||
_castServer.addHandlerWithAllowAllOptions(
|
||||
HttpContentUriHandler("GET", thumbnailPath, contentResolver, thumbnailContentUrl.toUri())
|
||||
.withHeader("Access-Control-Allow-Origin", "*"), true
|
||||
).withTag("cast");
|
||||
}
|
||||
|
||||
_castServer.addHandlerWithAllowAllOptions(
|
||||
HttpContentUriHandler("GET", videoPath, contentResolver, videoSource.contentUrl.toUri())
|
||||
.withHeader("Access-Control-Allow-Origin", "*"), true
|
||||
).withTag("cast");
|
||||
|
||||
Logger.i(TAG, "Casting local video (videoUrl: $videoUrl).");
|
||||
ad.loadVideo("BUFFERED", videoSource.container, videoUrl, resumePosition, video.duration.toDouble(), speed, metadataFromVideo(video, if (thumbnailContentUrl != null) thumbnailUrl else null));
|
||||
|
||||
return listOf(videoUrl);
|
||||
}
|
||||
|
||||
private fun castLocalAudio(contentResolver: ContentResolver, video: IPlatformVideoDetails, audioSource: LocalAudioContentSource, resumePosition: Double, speed: Double?) : List<String> {
|
||||
val ad = activeDevice ?: return listOf();
|
||||
|
||||
val url = getLocalUrl(ad);
|
||||
val id = UUID.randomUUID();
|
||||
val audioPath = "/audio-${id}"
|
||||
val audioUrl = url + audioPath;
|
||||
val thumbnailPath = "/thumbnail-${id}"
|
||||
val thumbnailUrl = url + thumbnailPath;
|
||||
val thumbnailContentUrl = video.thumbnails.getHQThumbnail()
|
||||
|
||||
if (thumbnailContentUrl != null) {
|
||||
_castServer.addHandlerWithAllowAllOptions(
|
||||
HttpContentUriHandler("GET", thumbnailPath, contentResolver, thumbnailContentUrl.toUri())
|
||||
.withHeader("Access-Control-Allow-Origin", "*"), true
|
||||
).withTag("cast");
|
||||
}
|
||||
|
||||
_castServer.addHandlerWithAllowAllOptions(
|
||||
HttpContentUriHandler("GET", audioPath, contentResolver, audioSource.contentUrl.toUri())
|
||||
.withHeader("Access-Control-Allow-Origin", "*"), true
|
||||
).withTag("cast");
|
||||
|
||||
Logger.i(TAG, "Casting local audio (audioUrl: $audioUrl).");
|
||||
ad.loadVideo("BUFFERED", audioSource.container, audioUrl, resumePosition, video.duration.toDouble(), speed, metadataFromVideo(video, if (thumbnailContentUrl != null) thumbnailUrl else null));
|
||||
|
||||
return listOf(audioUrl);
|
||||
}
|
||||
|
||||
private fun castLocalVideo(video: IPlatformVideoDetails, videoSource: LocalVideoSource, resumePosition: Double, speed: Double?) : List<String> {
|
||||
val ad = activeDevice ?: return listOf();
|
||||
|
||||
@@ -1280,10 +1349,14 @@ abstract class StateCasting {
|
||||
}
|
||||
|
||||
if (audioSource != null && audioSource.hasRequestExecutor) {
|
||||
val oldExecutor = _audioExecutor;
|
||||
oldExecutor?.closeAsync();
|
||||
_audioExecutor = audioSource.getRequestExecutor()
|
||||
}
|
||||
|
||||
if (videoSource != null && videoSource.hasRequestExecutor) {
|
||||
val oldExecutor = _videoExecutor;
|
||||
oldExecutor?.closeAsync();
|
||||
_videoExecutor = videoSource.getRequestExecutor()
|
||||
}
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ import com.futo.platformplayer.R
|
||||
import com.futo.platformplayer.Settings
|
||||
import com.futo.platformplayer.UIDialogs
|
||||
import com.futo.platformplayer.UpdateDownloadService
|
||||
import com.futo.platformplayer.UpdateNotificationManager
|
||||
import com.futo.platformplayer.api.http.ManagedHttpClient
|
||||
import com.futo.platformplayer.copyToOutputStream
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
@@ -36,6 +37,8 @@ import java.io.InputStream
|
||||
class AutoUpdateDialog(context: Context?) : AlertDialog(context) {
|
||||
companion object {
|
||||
private val TAG = "AutoUpdateDialog";
|
||||
|
||||
var currentDialog: AutoUpdateDialog? = null
|
||||
}
|
||||
|
||||
private lateinit var _buttonNever: Button;
|
||||
@@ -62,12 +65,14 @@ class AutoUpdateDialog(context: Context?) : AlertDialog(context) {
|
||||
_buttonShowChangelog = findViewById(R.id.button_show_changelog);
|
||||
|
||||
_buttonNever.setOnClickListener {
|
||||
UpdateNotificationManager.cancelAll(context)
|
||||
Settings.instance.autoUpdate.check = 1;
|
||||
Settings.instance.save();
|
||||
dismiss();
|
||||
};
|
||||
|
||||
_buttonClose.setOnClickListener {
|
||||
UpdateNotificationManager.cancelAll(context)
|
||||
dismiss();
|
||||
};
|
||||
|
||||
@@ -77,11 +82,13 @@ class AutoUpdateDialog(context: Context?) : AlertDialog(context) {
|
||||
};
|
||||
|
||||
_buttonUpdate.setOnClickListener {
|
||||
UpdateNotificationManager.cancelAll(context)
|
||||
|
||||
if (_updating) {
|
||||
return@setOnClickListener;
|
||||
}
|
||||
|
||||
if (Settings.instance.autoUpdate.backgroundDownload == 1) {
|
||||
if (Settings.instance.autoUpdate.shouldBackgroundDownload) {
|
||||
val ctx = context.applicationContext;
|
||||
val intent = Intent(ctx, UpdateDownloadService::class.java);
|
||||
intent.putExtra(UpdateDownloadService.EXTRA_VERSION, _maxVersion);
|
||||
@@ -94,11 +101,13 @@ class AutoUpdateDialog(context: Context?) : AlertDialog(context) {
|
||||
}
|
||||
};
|
||||
|
||||
currentDialog = this
|
||||
}
|
||||
|
||||
override fun dismiss() {
|
||||
super.dismiss()
|
||||
InstallReceiver.onReceiveResult.clear();
|
||||
currentDialog = null
|
||||
Logger.i(TAG, "Cleared InstallReceiver.onReceiveResult handler.")
|
||||
}
|
||||
|
||||
|
||||
@@ -865,6 +865,7 @@ class VideoDownload {
|
||||
val sourceLength: Long?;
|
||||
val fileStream = FileOutputStream(targetFile);
|
||||
|
||||
var executor: JSRequestExecutor? = null;
|
||||
try{
|
||||
var manifest = source.manifest;
|
||||
if(source.hasGenerate)
|
||||
@@ -881,7 +882,7 @@ class VideoDownload {
|
||||
if(foundCues.count() <= 0)
|
||||
throw IllegalStateException("No Cues found in manifest (unsupported dash?)");
|
||||
|
||||
val executor = if(source is JSSource && source.hasRequestExecutor)
|
||||
executor = if(source is JSSource && source.hasRequestExecutor)
|
||||
source.getRequestExecutor();
|
||||
else
|
||||
null;
|
||||
@@ -940,6 +941,7 @@ class VideoDownload {
|
||||
}
|
||||
finally {
|
||||
fileStream.close();
|
||||
executor?.closeAsync()
|
||||
}
|
||||
return sourceLength!!;
|
||||
}
|
||||
|
||||
+11
@@ -154,6 +154,17 @@ class MenuBottomBarFragment : MainActivityFragment() {
|
||||
else {
|
||||
StateApp.instance.setPrivacyMode(true);
|
||||
UIDialogs.appToast("Privacy mode enabled");
|
||||
|
||||
if(Settings.instance.other.showPrivacyModeDialog)
|
||||
UIDialogs.showDialog(it.context ?: return@setOnClickListener, R.drawable.incognito, "Privacy Mode",
|
||||
"All requests will be processed anonymously (any logins will be disabled except for the personalized home page), local playback and history tracking will also be disabled.\n\nTap the icon to disable.", null, 0,
|
||||
UIDialogs.Action("Don't show again", {
|
||||
Settings.instance.other.showPrivacyModeDialog = false;
|
||||
Settings.instance.save();
|
||||
}, UIDialogs.ActionStyle.NONE),
|
||||
UIDialogs.Action("Understood", {
|
||||
|
||||
}, UIDialogs.ActionStyle.PRIMARY));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+10
-4
@@ -20,6 +20,7 @@ import androidx.lifecycle.lifecycleScope
|
||||
import androidx.viewpager2.widget.ViewPager2
|
||||
import com.bumptech.glide.Glide
|
||||
import com.futo.platformplayer.R
|
||||
import com.futo.platformplayer.Settings
|
||||
import com.futo.platformplayer.UIDialogs
|
||||
import com.futo.platformplayer.UISlideOverlays
|
||||
import com.futo.platformplayer.api.media.PlatformID
|
||||
@@ -55,6 +56,7 @@ import com.futo.platformplayer.views.adapters.ChannelViewPagerAdapter
|
||||
import com.futo.platformplayer.views.others.CreatorThumbnail
|
||||
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuOverlay
|
||||
import com.futo.platformplayer.views.subscriptions.SubscribeButton
|
||||
import com.futo.platformplayer.withTimestamp
|
||||
import com.futo.polycentric.core.ApiMethods
|
||||
import com.futo.polycentric.core.PolycentricProfile
|
||||
import com.futo.polycentric.core.toURLInfoSystemLinkUrl
|
||||
@@ -198,8 +200,12 @@ class ChannelFragment : MainFragment() {
|
||||
adapter.onContentClicked.subscribe { v, _ ->
|
||||
when (v) {
|
||||
is IPlatformVideo -> {
|
||||
StatePlayer.instance.clearQueue()
|
||||
fragment.navigate<VideoDetailFragment>(v).maximizeVideoDetail()
|
||||
//StatePlayer.instance.clearQueue()
|
||||
if (StatePlayer.instance.hasQueue) {
|
||||
StatePlayer.instance.insertToQueue(v, true);
|
||||
} else {
|
||||
fragment.navigate<VideoDetailFragment>(v).maximizeVideoDetail();
|
||||
}
|
||||
}
|
||||
|
||||
is IPlatformPlaylist -> {
|
||||
@@ -244,7 +250,7 @@ class ChannelFragment : MainFragment() {
|
||||
adapter.onContentUrlClicked.subscribe { url, contentType ->
|
||||
when (contentType) {
|
||||
ContentType.MEDIA -> {
|
||||
StatePlayer.instance.clearQueue()
|
||||
StatePlayer.instance.clearQueue();
|
||||
fragment.navigate<VideoDetailFragment>(url).maximizeVideoDetail()
|
||||
}
|
||||
|
||||
@@ -403,7 +409,7 @@ class ChannelFragment : MainFragment() {
|
||||
_fragment.topBar?.onShown(channel)
|
||||
|
||||
val buttons = arrayListOf(Pair(R.drawable.ic_playlist_add) {
|
||||
UIDialogs.showConfirmationDialog(context,
|
||||
val dialog = UIDialogs.showConfirmationDialog(context,
|
||||
context.getString(R.string.do_you_want_to_convert_channel_channelname_to_a_playlist)
|
||||
.replace("{channelName}", channel.name),
|
||||
{
|
||||
|
||||
+1
-1
@@ -55,7 +55,7 @@ class LoginFragment : MainFragment() {
|
||||
fun showLogin(config: SourcePluginConfig, callback: ((SourceAuth?) -> Unit)? = null) {
|
||||
if(_callback != null) _callback?.invoke(null);
|
||||
_callback = callback;
|
||||
StateApp.instance.activity?.navigate<LoginFragment>(config, false);
|
||||
StateApp.instance.activity?.navigate<LoginFragment>(config, true);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+8
-3
@@ -50,7 +50,7 @@ class VideoDetailFragment() : MainFragment() {
|
||||
|
||||
private var _isActive: Boolean = false;
|
||||
|
||||
private var _viewDetail : VideoDetailView? = null;
|
||||
var _viewDetail : VideoDetailView? = null;
|
||||
private var _view : SingleViewTouchableMotionLayout? = null;
|
||||
|
||||
var isFullscreen : Boolean = false;
|
||||
@@ -372,14 +372,18 @@ class VideoDetailFragment() : MainFragment() {
|
||||
onMinimize.emit();
|
||||
}
|
||||
else if (state != State.MAXIMIZED && progress > 0.9) {
|
||||
state = State.MAXIMIZED;
|
||||
onMaximized.emit();
|
||||
/*
|
||||
if (_isInitialMaximize) {
|
||||
state = State.CLOSED;
|
||||
//state = State.CLOSED; Causes issues? might no longer be needed
|
||||
_isInitialMaximize = false;
|
||||
}
|
||||
else {
|
||||
state = State.MAXIMIZED;
|
||||
onMaximized.emit();
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
if (isTransitioning && (progress > 0.6 || progress < 0.4)) {
|
||||
@@ -450,7 +454,8 @@ class VideoDetailFragment() : MainFragment() {
|
||||
if (viewDetail.shouldEnterPictureInPicture) {
|
||||
_leavingPiP = false
|
||||
}
|
||||
if(Build.VERSION.SDK_INT < Build.VERSION_CODES.S && viewDetail.preventPictureInPicture == false && !StateCasting.instance.isCasting && Settings.instance.playback.isBackgroundPictureInPicture() && !viewDetail.isAudioOnlyUserAction) {
|
||||
val shouldPiP = Settings.instance.playback.isBackgroundPictureInPicture()
|
||||
if(Build.VERSION.SDK_INT < Build.VERSION_CODES.S && viewDetail.preventPictureInPicture == false && !StateCasting.instance.isCasting && shouldPiP && !viewDetail.isAudioOnlyUserAction) {
|
||||
val params = _viewDetail?.getPictureInPictureParams();
|
||||
if(params != null) {
|
||||
Logger.i(TAG, "enterPictureInPictureMode")
|
||||
|
||||
+6
-1
@@ -1,5 +1,6 @@
|
||||
package com.futo.platformplayer.fragment.mainactivity.topbar
|
||||
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
@@ -49,7 +50,11 @@ class GeneralTopBarFragment : TopFragment() {
|
||||
} else if (currentMain is PlaylistsFragment || currentMain is PlaylistFragment) {
|
||||
navigate<SuggestionsFragment>(SuggestionsFragmentData("", SearchType.PLAYLIST));
|
||||
} else if (currentMain is LibraryFragment) {
|
||||
navigate<LibrarySearchFragment>();
|
||||
if(Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
|
||||
UIDialogs.toast("Your Android version is too old for Mediastore search", true);
|
||||
}
|
||||
else
|
||||
navigate<LibrarySearchFragment>();
|
||||
} else {
|
||||
navigate<SuggestionsFragment>(SuggestionsFragmentData("", SearchType.VIDEO));
|
||||
}
|
||||
|
||||
@@ -573,7 +573,7 @@ class StateApp {
|
||||
}
|
||||
|
||||
if (Settings.instance.autoUpdate.isAutoUpdateEnabled()) {
|
||||
if (Settings.instance.autoUpdate.backgroundDownload == 1) {
|
||||
if (Settings.instance.autoUpdate.shouldBackgroundDownload) {
|
||||
Logger.i(TAG, "MainApp Started: Initialize [AutoUpdate Background]");
|
||||
val constraints = Constraints.Builder()
|
||||
.setRequiredNetworkType(NetworkType.CONNECTED)
|
||||
|
||||
@@ -344,7 +344,8 @@ class StateLibrary {
|
||||
MediaStore.Video.Media.DISPLAY_NAME,
|
||||
MediaStore.Video.Media.DATE_ADDED,
|
||||
MediaStore.Video.Media.MIME_TYPE,
|
||||
MediaStore.Video.Media.BUCKET_DISPLAY_NAME
|
||||
MediaStore.Video.Media.BUCKET_DISPLAY_NAME,
|
||||
MediaStore.Video.Media.DURATION
|
||||
);
|
||||
val PROJECTION_MEDIA = arrayOf(
|
||||
MediaStore.Audio.Media._ID, //0
|
||||
@@ -487,9 +488,10 @@ class StateLibrary {
|
||||
"";
|
||||
|
||||
|
||||
val albumContentUrl = if(albumId > 0)
|
||||
ContentUris.withAppendedId(MediaStore.Audio.Albums.EXTERNAL_CONTENT_URI, albumId)?.toString()
|
||||
else null;
|
||||
val albumArtBase = Uri.parse("content://media/external/audio/albumart")
|
||||
val albumContentUrl = if (albumId > 0)
|
||||
ContentUris.withAppendedId(albumArtBase, albumId).toString()
|
||||
else null
|
||||
|
||||
val dateObj = if(date > 0)
|
||||
OffsetDateTime.ofInstant(Instant.ofEpochSecond(date), ZoneOffset.UTC)
|
||||
@@ -515,6 +517,8 @@ class StateLibrary {
|
||||
val date = cursor.getLong(2);
|
||||
val contentType = cursor.getString(3);
|
||||
val category = cursor.getString(4);
|
||||
val durationMs = cursor.getLong(5)
|
||||
val duration = if (durationMs > 0) durationMs / 1000 else -1
|
||||
|
||||
val idLong = id.toLongOrNull();
|
||||
val contentUrl = if(idLong != null )
|
||||
@@ -534,7 +538,7 @@ class StateLibrary {
|
||||
PlatformID("FILE", contentUrl, null, 0, -1),
|
||||
displayName, Thumbnails(arrayOf(
|
||||
Thumbnail(contentUrl, 0)
|
||||
)), authorObj, contentUrl, -1, contentType, dateObj);
|
||||
)), authorObj, contentUrl, duration, contentType, dateObj);
|
||||
}
|
||||
|
||||
private var _instance : StateLibrary? = null;
|
||||
@@ -622,11 +626,12 @@ class Artist {
|
||||
val numTracks = cursor.getInt(2);
|
||||
val numAlbums = cursor.getInt(3);
|
||||
|
||||
val idLong = id.toLongOrNull();
|
||||
val uri = if(idLong != null) ContentUris.withAppendedId(MediaStore.Audio.Albums.EXTERNAL_CONTENT_URI, idLong) else null;
|
||||
val idLong = id.toLongOrNull()
|
||||
val uri = if (idLong != null)
|
||||
ContentUris.withAppendedId(MediaStore.Audio.Artists.EXTERNAL_CONTENT_URI, idLong)
|
||||
else null
|
||||
|
||||
return Artist(artist, numTracks, numAlbums, null, id, uri?.toString());
|
||||
}
|
||||
return Artist(artist, numTracks, numAlbums, null, id, uri?.toString()) }
|
||||
|
||||
fun getArtist(id: Long): Artist? {
|
||||
val resolver = StateApp.instance.contextOrNull?.contentResolver;
|
||||
@@ -730,9 +735,10 @@ class Album {
|
||||
val numTracks = cursor.getInt(2);
|
||||
val artist = cursor.getString(3);
|
||||
|
||||
val idLong = id.toLongOrNull();
|
||||
val uri = if(idLong != null) ContentUris.withAppendedId(MediaStore.Audio.Albums.EXTERNAL_CONTENT_URI, idLong) else null;
|
||||
return Album(album, numTracks, artist, id, uri?.toString());
|
||||
val idLong = id.toLongOrNull()
|
||||
val albumArtBase = Uri.parse("content://media/external/audio/albumart")
|
||||
val uri = if (idLong != null) ContentUris.withAppendedId(albumArtBase, idLong) else null
|
||||
return Album(album, numTracks, artist, id, uri?.toString())
|
||||
}
|
||||
|
||||
fun getAlbumTracks(albumId: Long): List<IPlatformVideo> {
|
||||
|
||||
@@ -169,6 +169,9 @@ class StatePlugins {
|
||||
return false;
|
||||
|
||||
LoginFragment.showLogin(config) {//LoginActivity.showLogin(context, config) {
|
||||
|
||||
if(it == null)
|
||||
return@showLogin;
|
||||
try {
|
||||
StatePlugins.instance.setPluginAuth(config.id, it);
|
||||
} catch (e: Throwable) {
|
||||
|
||||
@@ -77,7 +77,7 @@ class VideoListEditorView : FrameLayout {
|
||||
executeDelete()
|
||||
}, cancelAction = {
|
||||
|
||||
}, doNotAskAgainAction = {
|
||||
}, dismissAction = {}, doNotAskAgainAction = {
|
||||
Settings.instance.other.playlistDeleteConfirmation = false
|
||||
Settings.instance.save()
|
||||
})
|
||||
|
||||
@@ -489,7 +489,7 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
|
||||
|
||||
StatePlayer.instance.onQueueChanged.subscribe(this) {
|
||||
CoroutineScope(Dispatchers.Main).launch(Dispatchers.Main) {
|
||||
setLoopVisible(!StatePlayer.instance.hasQueue)
|
||||
//setLoopVisible(!StatePlayer.instance.hasQueue)
|
||||
updateNextPrevious();
|
||||
}
|
||||
}
|
||||
@@ -886,12 +886,12 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
|
||||
}
|
||||
fun updateLoopVideoUI() {
|
||||
if(StatePlayer.instance.loopVideo) {
|
||||
_control_loop.setImageResource(R.drawable.ic_loop_active);
|
||||
_control_loop_fullscreen.setImageResource(R.drawable.ic_loop_active);
|
||||
_control_loop.setImageResource(R.drawable.ic_repeat_one_active);
|
||||
_control_loop_fullscreen.setImageResource(R.drawable.ic_repeat_one_active);
|
||||
}
|
||||
else {
|
||||
_control_loop.setImageResource(R.drawable.ic_loop);
|
||||
_control_loop_fullscreen.setImageResource(R.drawable.ic_loop);
|
||||
_control_loop.setImageResource(R.drawable.ic_repeat_one);
|
||||
_control_loop_fullscreen.setImageResource(R.drawable.ic_repeat_one);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+12
@@ -111,6 +111,10 @@ public class JSHttpDataSource extends BaseDataSource implements HttpDataSource {
|
||||
* @return This factory.
|
||||
*/
|
||||
public Factory setRequestExecutor(@Nullable JSRequestExecutor requestExecutor) {
|
||||
JSRequestExecutor oldExecutor = this.requestExecutor;
|
||||
if(oldExecutor != null) {
|
||||
oldExecutor.closeAsync();
|
||||
}
|
||||
this.requestExecutor = requestExecutor;
|
||||
return this;
|
||||
}
|
||||
@@ -123,6 +127,10 @@ public class JSHttpDataSource extends BaseDataSource implements HttpDataSource {
|
||||
* @return This factory.
|
||||
*/
|
||||
public Factory setRequestExecutor2(@Nullable JSRequestExecutor requestExecutor) {
|
||||
JSRequestExecutor oldExecutor = this.requestExecutor2;
|
||||
if(oldExecutor != null) {
|
||||
oldExecutor.closeAsync();
|
||||
}
|
||||
this.requestExecutor2 = requestExecutor;
|
||||
return this;
|
||||
}
|
||||
@@ -508,6 +516,10 @@ public class JSHttpDataSource extends BaseDataSource implements HttpDataSource {
|
||||
|
||||
@Override
|
||||
public void close() throws HttpDataSourceException {
|
||||
if(requestExecutor != null)
|
||||
requestExecutor.closeAsync();
|
||||
if(requestExecutor2 != null)
|
||||
requestExecutor2.closeAsync();
|
||||
try {
|
||||
@Nullable InputStream inputStream = this.inputStream;
|
||||
if (inputStream != null) {
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M472.31,587.69L472.31,407.69L424.62,407.69L424.62,372.31L507.69,372.31L507.69,587.69L472.31,587.69ZM292.31,840L160,707.69L292.31,575.38L320.62,604.15L237.08,687.69L692.31,687.69L692.31,527.69L732.31,527.69L732.31,727.69L237.08,727.69L320.62,811.23L292.31,840ZM227.69,432.31L227.69,232.31L722.92,232.31L639.38,148.77L667.69,120L800,252.31L667.69,384.62L639.38,355.85L722.92,272.31L267.69,272.31L267.69,432.31L227.69,432.31Z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960">
|
||||
<path
|
||||
android:fillColor="@color/colorPrimary"
|
||||
android:pathData="M472.31,587.69L472.31,407.69L424.62,407.69L424.62,372.31L507.69,372.31L507.69,587.69L472.31,587.69ZM292.31,840L160,707.69L292.31,575.38L320.62,604.15L237.08,687.69L692.31,687.69L692.31,527.69L732.31,527.69L732.31,727.69L237.08,727.69L320.62,811.23L292.31,840ZM227.69,432.31L227.69,232.31L722.92,232.31L639.38,148.77L667.69,120L800,252.31L667.69,384.62L639.38,355.85L722.92,272.31L267.69,272.31L267.69,432.31L227.69,432.31Z"/>
|
||||
</vector>
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 20 KiB |
@@ -77,12 +77,13 @@
|
||||
android:layout_width="50dp"
|
||||
android:layout_height="50dp"
|
||||
android:contentDescription="@string/cd_incognito_button"
|
||||
android:src="@drawable/ic_disabled_visible_purple"
|
||||
android:src="@drawable/incognito_purple"
|
||||
android:background="@drawable/background_button_round_black"
|
||||
android:scaleType="fitCenter"
|
||||
android:visibility="visible"
|
||||
android:layout_marginLeft="10dp"
|
||||
android:layout_marginBottom="10dp"
|
||||
android:padding="8dp"
|
||||
android:elevation="50dp"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintBottom_toTopOf="@id/toast_view" />
|
||||
|
||||
@@ -75,7 +75,7 @@
|
||||
<ImageView
|
||||
android:layout_width="30dp"
|
||||
android:layout_height="30dp"
|
||||
android:src="@drawable/ic_disabled_visible" />
|
||||
android:src="@drawable/incognito" />
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
@@ -65,7 +65,7 @@
|
||||
android:scaleType="fitCenter"
|
||||
android:clickable="true"
|
||||
android:padding="12dp"
|
||||
app:srcCompat="@drawable/ic_loop" />
|
||||
app:srcCompat="@drawable/ic_repeat_one" />
|
||||
<ImageButton
|
||||
android:id="@+id/button_settings"
|
||||
android:layout_width="50dp"
|
||||
|
||||
@@ -93,7 +93,7 @@
|
||||
android:scaleType="fitCenter"
|
||||
android:clickable="true"
|
||||
android:padding="12dp"
|
||||
app:srcCompat="@drawable/ic_loop" />
|
||||
app:srcCompat="@drawable/ic_repeat_one" />
|
||||
<ImageButton
|
||||
android:id="@+id/button_settings"
|
||||
android:layout_width="50dp"
|
||||
|
||||
@@ -116,4 +116,14 @@
|
||||
<item name="android:fontFamily">@font/inter_regular</item>
|
||||
</style>
|
||||
|
||||
<style name="Theme.App.TransparentNoUi" parent="Theme.MaterialComponents.DayNight.NoActionBar">
|
||||
<item name="android:windowIsTranslucent">true</item>
|
||||
<item name="android:windowBackground">@android:color/transparent</item>
|
||||
<item name="android:windowNoTitle">true</item>
|
||||
<item name="android:windowFullscreen">true</item>
|
||||
<item name="android:colorBackgroundCacheHint">@null</item>
|
||||
<item name="windowNoTitle">true</item>
|
||||
<item name="windowActionBar">false</item>
|
||||
</style>
|
||||
|
||||
</resources>
|
||||
Reference in New Issue
Block a user