diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 9cafddaa..5c5fb27d 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -261,5 +261,12 @@
android:name=".UpdateActionReceiver"
android:exported="false" />
+
+
diff --git a/app/src/main/java/com/futo/platformplayer/Settings.kt b/app/src/main/java/com/futo/platformplayer/Settings.kt
index c5456bc4..b01e6a94 100644
--- a/app/src/main/java/com/futo/platformplayer/Settings.kt
+++ b/app/src/main/java/com/futo/platformplayer/Settings.kt
@@ -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)
diff --git a/app/src/main/java/com/futo/platformplayer/UIDialogs.kt b/app/src/main/java/com/futo/platformplayer/UIDialogs.kt
index 6090aa56..6c1e134d 100644
--- a/app/src/main/java/com/futo/platformplayer/UIDialogs.kt
+++ b/app/src/main/java/com/futo/platformplayer/UIDialogs.kt
@@ -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) {
diff --git a/app/src/main/java/com/futo/platformplayer/UpdateActionReceiver.kt b/app/src/main/java/com/futo/platformplayer/UpdateActionReceiver.kt
index 42f452f0..3050a154 100644
--- a/app/src/main/java/com/futo/platformplayer/UpdateActionReceiver.kt
+++ b/app/src/main/java/com/futo/platformplayer/UpdateActionReceiver.kt
@@ -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)
- }
}
diff --git a/app/src/main/java/com/futo/platformplayer/UpdateDownloadService.kt b/app/src/main/java/com/futo/platformplayer/UpdateDownloadService.kt
index fe01051a..3147ce62 100644
--- a/app/src/main/java/com/futo/platformplayer/UpdateDownloadService.kt
+++ b/app/src/main/java/com/futo/platformplayer/UpdateDownloadService.kt
@@ -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
}
}
}
diff --git a/app/src/main/java/com/futo/platformplayer/UpdateInstaller.kt b/app/src/main/java/com/futo/platformplayer/UpdateInstaller.kt
index 4f45ed0a..b81d5096 100644
--- a/app/src/main/java/com/futo/platformplayer/UpdateInstaller.kt
+++ b/app/src/main/java/com/futo/platformplayer/UpdateInstaller.kt
@@ -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)
+ }
+ }
}
diff --git a/app/src/main/java/com/futo/platformplayer/UpdateNotificationManager.kt b/app/src/main/java/com/futo/platformplayer/UpdateNotificationManager.kt
index aafeeec4..b424dcd9 100644
--- a/app/src/main/java/com/futo/platformplayer/UpdateNotificationManager.kt
+++ b/app/src/main/java/com/futo/platformplayer/UpdateNotificationManager.kt
@@ -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)
+
}
}
diff --git a/app/src/main/java/com/futo/platformplayer/Utility.kt b/app/src/main/java/com/futo/platformplayer/Utility.kt
index b154cb67..c41be6d0 100644
--- a/app/src/main/java/com/futo/platformplayer/Utility.kt
+++ b/app/src/main/java/com/futo/platformplayer/Utility.kt
@@ -444,15 +444,9 @@ fun addressScore(addr: InetAddress): Int {
fun Enumeration.toList(): List = Collections.list(this)
-fun RequestBuilder.withMaxSizePx(maxSizePx: Int = 1920, useCenterCrop: Boolean = false): RequestBuilder {
- var builder = this
- .downsample(DownsampleStrategy.AT_MOST)
- .override(maxSizePx, maxSizePx)
- builder = if (useCenterCrop) {
- builder.centerCrop()
- } else {
- builder.fitCenter()
- }
-
- return builder
+fun RequestBuilder.withMaxSizePx(maxSizePx: Int = 1920): RequestBuilder {
+ return this;
+ //.downsample(DownsampleStrategy.AT_MOST)
+ //.override(maxSizePx, maxSizePx)
+ //.centerInside()
}
\ No newline at end of file
diff --git a/app/src/main/java/com/futo/platformplayer/activities/InstallUpdateActivity.kt b/app/src/main/java/com/futo/platformplayer/activities/InstallUpdateActivity.kt
new file mode 100644
index 00000000..48d600e5
--- /dev/null
+++ b/app/src/main/java/com/futo/platformplayer/activities/InstallUpdateActivity.kt
@@ -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)
+ }
+ }
+}
diff --git a/app/src/main/java/com/futo/platformplayer/activities/MainActivity.kt b/app/src/main/java/com/futo/platformplayer/activities/MainActivity.kt
index b9ecf3f4..85e4c47d 100644
--- a/app/src/main/java/com/futo/platformplayer/activities/MainActivity.kt
+++ b/app/src/main/java/com/futo/platformplayer/activities/MainActivity.kt
@@ -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("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();
})
+ */
}
}
}
diff --git a/app/src/main/java/com/futo/platformplayer/api/http/server/handlers/HttpContentUriHandler.kt b/app/src/main/java/com/futo/platformplayer/api/http/server/handlers/HttpContentUriHandler.kt
new file mode 100644
index 00000000..63745991
--- /dev/null
+++ b/app/src/main/java/com/futo/platformplayer/api/http/server/handlers/HttpContentUriHandler.kt
@@ -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")
+ }
+ }
+}
diff --git a/app/src/main/java/com/futo/platformplayer/api/media/models/video/LocalPlatformVideoDetails.kt b/app/src/main/java/com/futo/platformplayer/api/media/models/video/LocalPlatformVideoDetails.kt
index 52659b46..98dc3524 100644
--- a/app/src/main/java/com/futo/platformplayer/api/media/models/video/LocalPlatformVideoDetails.kt
+++ b/app/src/main/java/com/futo/platformplayer/api/media/models/video/LocalPlatformVideoDetails.kt
@@ -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;
diff --git a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSRequestExecutor.kt b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSRequestExecutor.kt
index ed428790..5a41d0a2 100644
--- a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSRequestExecutor.kt
+++ b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSRequestExecutor.kt
@@ -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..?
diff --git a/app/src/main/java/com/futo/platformplayer/api/media/platforms/local/models/sources/LocalAudioContentSource.kt b/app/src/main/java/com/futo/platformplayer/api/media/platforms/local/models/sources/LocalAudioContentSource.kt
index 06f1c50c..23f268f2 100644
--- a/app/src/main/java/com/futo/platformplayer/api/media/platforms/local/models/sources/LocalAudioContentSource.kt
+++ b/app/src/main/java/com/futo/platformplayer/api/media/platforms/local/models/sources/LocalAudioContentSource.kt
@@ -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;
}
diff --git a/app/src/main/java/com/futo/platformplayer/api/media/platforms/local/models/sources/LocalVideoContentSource.kt b/app/src/main/java/com/futo/platformplayer/api/media/platforms/local/models/sources/LocalVideoContentSource.kt
index d8507fab..e8b37364 100644
--- a/app/src/main/java/com/futo/platformplayer/api/media/platforms/local/models/sources/LocalVideoContentSource.kt
+++ b/app/src/main/java/com/futo/platformplayer/api/media/platforms/local/models/sources/LocalVideoContentSource.kt
@@ -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;
}
}
\ No newline at end of file
diff --git a/app/src/main/java/com/futo/platformplayer/casting/CastingDeviceExp.kt b/app/src/main/java/com/futo/platformplayer/casting/CastingDeviceExp.kt
index 1560eff1..84d96e02 100644
--- a/app/src/main/java/com/futo/platformplayer/casting/CastingDeviceExp.kt
+++ b/app/src/main/java/com/futo/platformplayer/casting/CastingDeviceExp.kt
@@ -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"
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/com/futo/platformplayer/casting/StateCasting.kt b/app/src/main/java/com/futo/platformplayer/casting/StateCasting.kt
index 269a73b8..5c38f12f 100644
--- a/app/src/main/java/com/futo/platformplayer/casting/StateCasting.kt
+++ b/app/src/main/java/com/futo/platformplayer/casting/StateCasting.kt
@@ -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 {
+ 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 {
+ 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 {
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()
}
diff --git a/app/src/main/java/com/futo/platformplayer/dialogs/AutoUpdateDialog.kt b/app/src/main/java/com/futo/platformplayer/dialogs/AutoUpdateDialog.kt
index fbca0f6b..ccee6082 100644
--- a/app/src/main/java/com/futo/platformplayer/dialogs/AutoUpdateDialog.kt
+++ b/app/src/main/java/com/futo/platformplayer/dialogs/AutoUpdateDialog.kt
@@ -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.")
}
diff --git a/app/src/main/java/com/futo/platformplayer/downloads/VideoDownload.kt b/app/src/main/java/com/futo/platformplayer/downloads/VideoDownload.kt
index 0881bb6e..3b727d21 100644
--- a/app/src/main/java/com/futo/platformplayer/downloads/VideoDownload.kt
+++ b/app/src/main/java/com/futo/platformplayer/downloads/VideoDownload.kt
@@ -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!!;
}
diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/bottombar/MenuBottomBarFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/bottombar/MenuBottomBarFragment.kt
index a51e7620..3e176ece 100644
--- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/bottombar/MenuBottomBarFragment.kt
+++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/bottombar/MenuBottomBarFragment.kt
@@ -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));
}
}
diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ChannelFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ChannelFragment.kt
index 91e6aaa3..c95238aa 100644
--- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ChannelFragment.kt
+++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ChannelFragment.kt
@@ -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(v).maximizeVideoDetail()
+ //StatePlayer.instance.clearQueue()
+ if (StatePlayer.instance.hasQueue) {
+ StatePlayer.instance.insertToQueue(v, true);
+ } else {
+ fragment.navigate(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(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),
{
diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/LoginFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/LoginFragment.kt
index 86cb4cc4..9b05816c 100644
--- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/LoginFragment.kt
+++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/LoginFragment.kt
@@ -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(config, false);
+ StateApp.instance.activity?.navigate(config, true);
}
}
diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailFragment.kt
index 26a2577b..4c693230 100644
--- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailFragment.kt
+++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailFragment.kt
@@ -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")
diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/topbar/GeneralTopBarFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/topbar/GeneralTopBarFragment.kt
index 43f9dc26..a496c668 100644
--- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/topbar/GeneralTopBarFragment.kt
+++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/topbar/GeneralTopBarFragment.kt
@@ -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(SuggestionsFragmentData("", SearchType.PLAYLIST));
} else if (currentMain is LibraryFragment) {
- navigate();
+ if(Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
+ UIDialogs.toast("Your Android version is too old for Mediastore search", true);
+ }
+ else
+ navigate();
} else {
navigate(SuggestionsFragmentData("", SearchType.VIDEO));
}
diff --git a/app/src/main/java/com/futo/platformplayer/states/StateApp.kt b/app/src/main/java/com/futo/platformplayer/states/StateApp.kt
index 7ac860e3..ad58ffbb 100644
--- a/app/src/main/java/com/futo/platformplayer/states/StateApp.kt
+++ b/app/src/main/java/com/futo/platformplayer/states/StateApp.kt
@@ -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)
diff --git a/app/src/main/java/com/futo/platformplayer/states/StateLibrary.kt b/app/src/main/java/com/futo/platformplayer/states/StateLibrary.kt
index 9176b8ae..b2d4149b 100644
--- a/app/src/main/java/com/futo/platformplayer/states/StateLibrary.kt
+++ b/app/src/main/java/com/futo/platformplayer/states/StateLibrary.kt
@@ -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 {
diff --git a/app/src/main/java/com/futo/platformplayer/states/StatePlugins.kt b/app/src/main/java/com/futo/platformplayer/states/StatePlugins.kt
index cb853f07..38473023 100644
--- a/app/src/main/java/com/futo/platformplayer/states/StatePlugins.kt
+++ b/app/src/main/java/com/futo/platformplayer/states/StatePlugins.kt
@@ -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) {
diff --git a/app/src/main/java/com/futo/platformplayer/views/lists/VideoListEditorView.kt b/app/src/main/java/com/futo/platformplayer/views/lists/VideoListEditorView.kt
index 67be4058..7bc7dffe 100644
--- a/app/src/main/java/com/futo/platformplayer/views/lists/VideoListEditorView.kt
+++ b/app/src/main/java/com/futo/platformplayer/views/lists/VideoListEditorView.kt
@@ -77,7 +77,7 @@ class VideoListEditorView : FrameLayout {
executeDelete()
}, cancelAction = {
- }, doNotAskAgainAction = {
+ }, dismissAction = {}, doNotAskAgainAction = {
Settings.instance.other.playlistDeleteConfirmation = false
Settings.instance.save()
})
diff --git a/app/src/main/java/com/futo/platformplayer/views/video/FutoVideoPlayer.kt b/app/src/main/java/com/futo/platformplayer/views/video/FutoVideoPlayer.kt
index af666ca9..b62c8f50 100644
--- a/app/src/main/java/com/futo/platformplayer/views/video/FutoVideoPlayer.kt
+++ b/app/src/main/java/com/futo/platformplayer/views/video/FutoVideoPlayer.kt
@@ -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);
}
}
diff --git a/app/src/main/java/com/futo/platformplayer/views/video/datasources/JSHttpDataSource.java b/app/src/main/java/com/futo/platformplayer/views/video/datasources/JSHttpDataSource.java
index 859c1b2f..9c106a9a 100644
--- a/app/src/main/java/com/futo/platformplayer/views/video/datasources/JSHttpDataSource.java
+++ b/app/src/main/java/com/futo/platformplayer/views/video/datasources/JSHttpDataSource.java
@@ -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) {
diff --git a/app/src/main/res/drawable/ic_repeat_one.xml b/app/src/main/res/drawable/ic_repeat_one.xml
new file mode 100644
index 00000000..48a96fbe
--- /dev/null
+++ b/app/src/main/res/drawable/ic_repeat_one.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_repeat_one_active.xml b/app/src/main/res/drawable/ic_repeat_one_active.xml
new file mode 100644
index 00000000..4556487e
--- /dev/null
+++ b/app/src/main/res/drawable/ic_repeat_one_active.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/incognito.png b/app/src/main/res/drawable/incognito.png
new file mode 100644
index 00000000..7321cdc5
Binary files /dev/null and b/app/src/main/res/drawable/incognito.png differ
diff --git a/app/src/main/res/drawable/incognito_purple.png b/app/src/main/res/drawable/incognito_purple.png
new file mode 100644
index 00000000..b74d9215
Binary files /dev/null and b/app/src/main/res/drawable/incognito_purple.png differ
diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml
index df3abc69..9adf1cd4 100644
--- a/app/src/main/res/layout/activity_main.xml
+++ b/app/src/main/res/layout/activity_main.xml
@@ -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" />
diff --git a/app/src/main/res/layout/fragment_overview_bottom_bar.xml b/app/src/main/res/layout/fragment_overview_bottom_bar.xml
index 4fde34a4..070130bf 100644
--- a/app/src/main/res/layout/fragment_overview_bottom_bar.xml
+++ b/app/src/main/res/layout/fragment_overview_bottom_bar.xml
@@ -75,7 +75,7 @@
+ android:src="@drawable/incognito" />
diff --git a/app/src/main/res/layout/video_player_ui.xml b/app/src/main/res/layout/video_player_ui.xml
index 486936a6..8e31747c 100644
--- a/app/src/main/res/layout/video_player_ui.xml
+++ b/app/src/main/res/layout/video_player_ui.xml
@@ -65,7 +65,7 @@
android:scaleType="fitCenter"
android:clickable="true"
android:padding="12dp"
- app:srcCompat="@drawable/ic_loop" />
+ app:srcCompat="@drawable/ic_repeat_one" />
+ app:srcCompat="@drawable/ic_repeat_one" />
@font/inter_regular
+
+
\ No newline at end of file