diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index f22adf15..9cafddaa 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -252,5 +252,14 @@
android:name=".activities.QRCodeFullscreenActivity"
android:screenOrientation="sensorPortrait"
android:theme="@style/Theme.FutoVideo.NoActionBar" />
+
+
+
+
diff --git a/app/src/main/java/com/futo/platformplayer/UIDialogs.kt b/app/src/main/java/com/futo/platformplayer/UIDialogs.kt
index 59917e75..6090aa56 100644
--- a/app/src/main/java/com/futo/platformplayer/UIDialogs.kt
+++ b/app/src/main/java/com/futo/platformplayer/UIDialogs.kt
@@ -403,13 +403,6 @@ class UIDialogs {
dialog.setMaxVersion(lastVersion);
}
- fun showInstallDownloadedUpdateDialog(context: Context, apkFile: File) {
- val dialog = AutoUpdateDialog(context);
- registerDialogOpened(dialog);
- dialog.setOnDismissListener { registerDialogClosed(dialog) };
- dialog.showPredownloaded(apkFile);
- }
-
fun showMigrateDialog(context: Context, store: ManagedStore<*>, onConcluded: ()->Unit) {
if(!store.hasMissingReconstructions())
onConcluded();
diff --git a/app/src/main/java/com/futo/platformplayer/UpdateActionReceiver.kt b/app/src/main/java/com/futo/platformplayer/UpdateActionReceiver.kt
new file mode 100644
index 00000000..42f452f0
--- /dev/null
+++ b/app/src/main/java/com/futo/platformplayer/UpdateActionReceiver.kt
@@ -0,0 +1,90 @@
+package com.futo.platformplayer
+
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import androidx.core.app.NotificationManagerCompat
+import androidx.core.content.ContextCompat
+import com.futo.platformplayer.activities.MainActivity
+import com.futo.platformplayer.states.StateApp
+import java.io.File
+
+class UpdateActionReceiver : BroadcastReceiver() {
+ override fun onReceive(context: Context, intent: Intent) {
+ when (intent.action) {
+ UpdateNotificationManager.ACTION_UPDATE_YES -> handleUpdateYes(context, intent)
+ UpdateNotificationManager.ACTION_UPDATE_NO -> handleUpdateNo(context)
+ UpdateNotificationManager.ACTION_UPDATE_NEVER -> handleUpdateNever(context)
+ UpdateNotificationManager.ACTION_DOWNLOAD_CANCEL -> handleDownloadCancel(context, intent)
+ UpdateNotificationManager.ACTION_INSTALL_NOW -> handleInstallNow(context, intent)
+ }
+ }
+
+ private fun handleUpdateYes(context: Context, intent: Intent) {
+ val version = intent.getIntExtra(UpdateNotificationManager.EXTRA_VERSION, 0)
+ if (version == 0) {
+ return
+ }
+
+ 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)
+ }
+ }
+ }
+
+ private fun handleUpdateNo(context: Context) {
+ NotificationManagerCompat.from(context).cancel(UpdateNotificationManager.NOTIF_ID_AVAILABLE)
+ }
+
+ private fun handleUpdateNever(context: Context) {
+ Settings.instance.autoUpdate.check = 1
+ Settings.instance.save()
+
+ UpdateNotificationManager.cancelAll(context)
+ }
+
+ private fun handleDownloadCancel(context: Context, intent: Intent) {
+ val version = intent.getIntExtra(UpdateNotificationManager.EXTRA_VERSION, 0)
+
+ val cancelIntent = Intent(context, UpdateDownloadService::class.java).apply {
+ putExtra(UpdateDownloadService.EXTRA_CANCEL, true)
+ putExtra(UpdateDownloadService.EXTRA_VERSION, version)
+ }
+ ContextCompat.startForegroundService(context, cancelIntent)
+
+ 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/UpdateCheckWorker.kt b/app/src/main/java/com/futo/platformplayer/UpdateCheckWorker.kt
new file mode 100644
index 00000000..549ab71b
--- /dev/null
+++ b/app/src/main/java/com/futo/platformplayer/UpdateCheckWorker.kt
@@ -0,0 +1,64 @@
+package com.futo.platformplayer
+
+import android.content.Context
+import androidx.work.CoroutineWorker
+import androidx.work.WorkerParameters
+import com.futo.platformplayer.api.http.ManagedHttpClient
+import com.futo.platformplayer.logging.Logger
+import com.futo.platformplayer.states.StateApp
+import com.futo.platformplayer.states.StateUpdate
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
+
+class UpdateCheckWorker(appContext: Context, workerParams: WorkerParameters) : CoroutineWorker(appContext, workerParams) {
+
+ override suspend fun doWork(): Result {
+ if (!Settings.instance.autoUpdate.isAutoUpdateEnabled()) {
+ Logger.i(TAG, "Auto-update disabled, skipping worker run")
+ return Result.success()
+ }
+
+ return withContext(Dispatchers.IO) {
+ try {
+ val client = ManagedHttpClient()
+ val latestVersion = StateUpdate.Companion.instance.downloadVersionCode(client)
+
+ if (latestVersion == null) {
+ Logger.w(TAG, "Failed to fetch latest version in worker")
+ return@withContext Result.retry()
+ }
+
+ val currentVersion = BuildConfig.VERSION_CODE
+ Logger.i(TAG, "Worker check: current=$currentVersion, latest=$latestVersion")
+
+ if (latestVersion <= currentVersion) {
+ return@withContext Result.success()
+ }
+
+ UpdateNotificationManager.showUpdateAvailableNotification(applicationContext, latestVersion)
+
+ if (StateApp.instance.isMainActive) {
+ withContext(Dispatchers.Main) {
+ StateApp.withContext { ctx ->
+ try {
+ UIDialogs.showUpdateAvailableDialog(ctx, latestVersion, false)
+ } catch (t: Throwable) {
+ Logger.w(TAG, "Failed to show in-app update dialog from worker", t)
+ }
+ }
+ }
+ }
+
+ Result.success()
+ } catch (t: Throwable) {
+ Logger.w(TAG, "Exception in UpdateCheckWorker", t)
+ Result.retry()
+ }
+ }
+ }
+
+ companion object {
+ private const val TAG = "UpdateCheckWorker"
+ const val UNIQUE_WORK_NAME = "updateCheck"
+ }
+}
diff --git a/app/src/main/java/com/futo/platformplayer/UpdateDownloadService.kt b/app/src/main/java/com/futo/platformplayer/UpdateDownloadService.kt
new file mode 100644
index 00000000..fe01051a
--- /dev/null
+++ b/app/src/main/java/com/futo/platformplayer/UpdateDownloadService.kt
@@ -0,0 +1,230 @@
+package com.futo.platformplayer
+
+import android.app.Service
+import android.content.Intent
+import android.os.IBinder
+import com.futo.platformplayer.logging.Logger
+import com.futo.platformplayer.states.StateApp
+import com.futo.platformplayer.states.StateUpdate
+import kotlinx.coroutines.*
+import java.io.File
+import java.io.FileOutputStream
+import java.net.HttpURLConnection
+import java.net.URL
+
+class UpdateDownloadService : Service() {
+
+ companion object {
+ private const val TAG = "UpdateDownloadService"
+ const val EXTRA_VERSION = "version"
+ const val EXTRA_CANCEL = "cancel"
+ private const val MAX_RETRIES = 5
+ private const val INITIAL_BACKOFF_MS = 5_000L
+ private const val BUFFER_SIZE = 8 * 1024
+ }
+
+ private val job = SupervisorJob()
+ private val scope = CoroutineScope(Dispatchers.IO + job)
+
+ @Volatile
+ private var isDownloading: Boolean = false
+
+ @Volatile
+ private var cancelRequested: Boolean = false
+
+ override fun onBind(intent: Intent?): IBinder? = null
+
+ override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
+ if (intent == null) {
+ stopSelf()
+ return START_NOT_STICKY
+ }
+
+ if (intent.getBooleanExtra(EXTRA_CANCEL, false)) {
+ cancelRequested = true
+ Logger.i(TAG, "Download cancel requested")
+ stopForeground(Service.STOP_FOREGROUND_REMOVE)
+ stopSelf()
+ return START_NOT_STICKY
+ }
+
+ val version = intent.getIntExtra(EXTRA_VERSION, 0)
+ if (version == 0) {
+ stopSelf()
+ return START_NOT_STICKY
+ }
+
+ if (isDownloading) {
+ Logger.i(TAG, "Download already in progress, ignoring new start")
+ return START_STICKY
+ }
+
+ isDownloading = true
+ cancelRequested = false
+
+ val notification = UpdateNotificationManager.buildDownloadProgressNotification(this, version, 0, true)
+ startForeground(UpdateNotificationManager.NOTIF_ID_DOWNLOADING, notification)
+
+ scope.launch {
+ downloadApk(version)
+ }
+
+ return START_STICKY
+ }
+
+ override fun onDestroy() {
+ super.onDestroy()
+ job.cancel()
+ }
+
+ private suspend fun downloadApk(version: Int) {
+ val apkFile = StateUpdate.getApkFile(this, version)
+ val partialFile = StateUpdate.getPartialApkFile(this, version)
+
+ try {
+ if (apkFile.exists() && apkFile.length() > 0L) {
+ Logger.i(TAG, "APK already downloaded at ${apkFile.absolutePath}")
+ onDownloadComplete(version, apkFile)
+ return
+ }
+
+ var backoffMs = INITIAL_BACKOFF_MS
+
+ for (attempt in 0 until MAX_RETRIES) {
+ if (cancelRequested) {
+ Logger.i(TAG, "Download cancelled before attempt ${attempt + 1}")
+ break
+ }
+
+ try {
+ performDownload(StateUpdate.APK_URL, partialFile, version)
+
+ if (!cancelRequested) {
+ if (apkFile.exists()) {
+ apkFile.delete()
+ }
+ if (!partialFile.renameTo(apkFile)) {
+ throw IllegalStateException("Failed to rename partial APK file")
+ }
+ onDownloadComplete(version, apkFile)
+ }
+ break
+ } catch (t: Throwable) {
+ if (cancelRequested) {
+ Logger.i(TAG, "Download cancelled by user", t)
+ break
+ }
+
+ if (attempt == MAX_RETRIES - 1) {
+ Logger.e(TAG, "Download failed after ${attempt + 1} attempts", t)
+ UpdateNotificationManager.showDownloadFailedNotification(this, version, t)
+ break
+ } else {
+ Logger.w(TAG, "Download attempt ${attempt + 1} failed, retrying in ${backoffMs / 1000}s", t)
+ delay(backoffMs)
+ backoffMs *= 2
+ }
+ }
+ }
+ } finally {
+ isDownloading = false
+ cancelRequested = false
+ stopForeground(Service.STOP_FOREGROUND_REMOVE)
+ stopSelf()
+ }
+ }
+
+ private fun performDownload(url: String, partialFile: File, version: Int) {
+ var startOffset = if (partialFile.exists()) partialFile.length() else 0L
+ Logger.i(TAG, "Starting download. url=$url, existingBytes=$startOffset")
+
+ var connection: HttpURLConnection? = null
+ try {
+ connection = (URL(url).openConnection() as HttpURLConnection).apply {
+ connectTimeout = 15_000
+ readTimeout = 30_000
+ if (startOffset > 0L) {
+ setRequestProperty("Range", "bytes=$startOffset-")
+ }
+ }
+
+ connection.connect()
+ val responseCode = connection.responseCode
+
+ if (responseCode == HttpURLConnection.HTTP_OK && startOffset > 0L) {
+ Logger.w(TAG, "Server ignored Range header, restarting download from scratch")
+ partialFile.delete()
+ startOffset = 0L
+ } else if (responseCode != HttpURLConnection.HTTP_OK &&
+ responseCode != HttpURLConnection.HTTP_PARTIAL) {
+ throw IllegalStateException("Unexpected HTTP response code $responseCode")
+ }
+
+ val contentLength = connection.contentLengthLong
+ val totalBytes = if (contentLength > 0L) startOffset + contentLength else -1L
+
+ val buffer = ByteArray(BUFFER_SIZE)
+ var downloaded = 0L
+ var lastProgress = -1
+
+ connection.inputStream.use { input ->
+ FileOutputStream(partialFile, startOffset > 0L).use { output ->
+ while (!cancelRequested) {
+ val read = input.read(buffer)
+ if (read == -1) {
+ break
+ }
+ output.write(buffer, 0, read)
+ downloaded += read
+
+ if (totalBytes > 0L) {
+ val progress = (((startOffset + downloaded) * 100L) / totalBytes).toInt()
+ if (progress != lastProgress) {
+ lastProgress = progress
+ val safeProgress = when {
+ progress < 0 -> 0
+ progress > 100 -> 100
+ else -> progress
+ }
+ UpdateNotificationManager.updateDownloadProgress(this, version, safeProgress, false)
+ }
+ } else {
+ UpdateNotificationManager.updateDownloadProgress(this, version, 0, true)
+ }
+ }
+ output.flush()
+ }
+ }
+
+ if (cancelRequested) {
+ throw CancellationException("Download cancelled")
+ }
+
+ if (totalBytes > 0L && startOffset + downloaded < totalBytes) {
+ throw IllegalStateException("Download incomplete: expected=$totalBytes, got=${startOffset + downloaded}")
+ }
+ } finally {
+ connection?.disconnect()
+ }
+ }
+
+ private fun onDownloadComplete(version: Int, apkFile: File) {
+ Logger.i(TAG, "Download complete for version=$version, file=${apkFile.absolutePath}")
+ UpdateNotificationManager.showDownloadCompleteNotification(this, version, apkFile)
+
+ if (StateApp.instance.isMainActive) {
+ 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)
+ }, {})
+ } catch (t: Throwable) {
+ Logger.w(TAG, "Failed to show in-app update downloaded dialog", t)
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/com/futo/platformplayer/UpdateInstaller.kt b/app/src/main/java/com/futo/platformplayer/UpdateInstaller.kt
new file mode 100644
index 00000000..4f45ed0a
--- /dev/null
+++ b/app/src/main/java/com/futo/platformplayer/UpdateInstaller.kt
@@ -0,0 +1,89 @@
+package com.futo.platformplayer
+
+import android.annotation.SuppressLint
+import android.app.PendingIntent.FLAG_MUTABLE
+import android.app.PendingIntent.FLAG_UPDATE_CURRENT
+import android.app.PendingIntent.getBroadcast
+import android.content.Context
+import android.content.Intent
+import android.content.pm.PackageInstaller
+import android.provider.Settings
+import com.futo.platformplayer.logging.Logger
+import com.futo.platformplayer.receivers.InstallReceiver
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.GlobalScope
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import java.io.File
+import java.io.InputStream
+import androidx.core.net.toUri
+
+object UpdateInstaller {
+ private const val TAG = "UpdateInstaller"
+
+ @SuppressLint("RequestInstallPackagesPolicy")
+ fun startInstall(context: Context, apkFile: File) {
+ if (!apkFile.exists()) {
+ Logger.w(TAG, "APK file does not exist: ${apkFile.absolutePath}")
+ UIDialogs.toast(context, "Update file missing")
+ return
+ }
+
+ if (BuildConfig.IS_PLAYSTORE_BUILD) {
+ UIDialogs.toast(context, "Updates are managed by the Play Store")
+ return
+ }
+
+ try {
+ val pm = context.packageManager
+ if (!pm.canRequestPackageInstalls()) {
+ UIDialogs.toast(context, "Allow this app to install updates, then try again")
+
+ val intent = Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES).apply {
+ data = "package:${context.packageName}".toUri()
+ addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ }
+ context.startActivity(intent)
+ return
+ }
+ } catch (t: Throwable) {
+ Logger.e(TAG, "Failed to check unknown sources permission", t)
+ }
+
+ 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)
+ session = packageInstaller.openSession(sessionId)
+
+ inputStream = apkFile.inputStream()
+ val dataLength = apkFile.length()
+
+ session.openWrite("package", 0, dataLength).use { sessionStream ->
+ inputStream.copyToOutputStream(dataLength, sessionStream) { _ -> }
+ session.fsync(sessionStream)
+ }
+
+ val intent = Intent(context, InstallReceiver::class.java)
+ val pendingIntent = getBroadcast(context, 0, intent, FLAG_MUTABLE or FLAG_UPDATE_CURRENT)
+ val statusReceiver = pendingIntent.intentSender
+
+ Logger.i(TAG, "Committing install session for ${apkFile.absolutePath}")
+ session.commit(statusReceiver)
+ } catch (e: Throwable) {
+ Logger.w(TAG, "Exception while installing update", e)
+ session?.abandon()
+ withContext(Dispatchers.Main) {
+ UIDialogs.toast(context, "Failed to install update: ${e.message}")
+ }
+ } finally {
+ session?.close()
+ inputStream?.close()
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/com/futo/platformplayer/UpdateNotificationManager.kt b/app/src/main/java/com/futo/platformplayer/UpdateNotificationManager.kt
new file mode 100644
index 00000000..aafeeec4
--- /dev/null
+++ b/app/src/main/java/com/futo/platformplayer/UpdateNotificationManager.kt
@@ -0,0 +1,171 @@
+package com.futo.platformplayer
+
+import android.Manifest
+import android.app.Notification
+import android.app.NotificationChannel
+import android.app.NotificationManager
+import android.app.PendingIntent.FLAG_MUTABLE
+import android.app.PendingIntent.FLAG_UPDATE_CURRENT
+import android.app.PendingIntent.getBroadcast
+import android.content.Context
+import android.content.Intent
+import android.content.pm.PackageManager
+import androidx.core.app.NotificationCompat
+import androidx.core.app.NotificationManagerCompat
+import androidx.core.content.ContextCompat
+import java.io.File
+
+object UpdateNotificationManager {
+ private const val CHANNEL_ID = "app_updates"
+ private const val CHANNEL_NAME = "App updates"
+ private const val CHANNEL_DESCRIPTION = "Notifications about new app versions"
+
+ const val ACTION_UPDATE_YES = "com.futo.platformplayer.UPDATE_YES"
+ const val ACTION_UPDATE_NO = "com.futo.platformplayer.UPDATE_NO"
+ const val ACTION_UPDATE_NEVER = "com.futo.platformplayer.UPDATE_NEVER"
+ const val ACTION_DOWNLOAD_CANCEL = "com.futo.platformplayer.UPDATE_CANCEL"
+ const val ACTION_INSTALL_NOW = "com.futo.platformplayer.UPDATE_INSTALL"
+
+ const val EXTRA_VERSION = "version"
+ const val EXTRA_APK_PATH = "apk_path"
+
+ const val NOTIF_ID_AVAILABLE = 2001
+ const val NOTIF_ID_DOWNLOADING = 2002
+ const val NOTIF_ID_READY = 2003
+
+ 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
+ manager.createNotificationChannel(channel)
+ }
+ }
+
+ fun showUpdateAvailableNotification(context: Context, version: Int) {
+ if (ContextCompat.checkSelfPermission(context, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {
+ return
+ }
+
+ ensureChannel(context)
+
+ val yesIntent = Intent(context, UpdateActionReceiver::class.java).apply {
+ action = ACTION_UPDATE_YES
+ putExtra(EXTRA_VERSION, version)
+ }
+ val yesPendingIntent = getBroadcast(context, 0, yesIntent, FLAG_MUTABLE or FLAG_UPDATE_CURRENT)
+ val noIntent = Intent(context, UpdateActionReceiver::class.java).apply {
+ action = ACTION_UPDATE_NO
+ putExtra(EXTRA_VERSION, version)
+ }
+ val noPendingIntent = getBroadcast(context, 1, noIntent, FLAG_MUTABLE or FLAG_UPDATE_CURRENT)
+ val neverIntent = Intent(context, UpdateActionReceiver::class.java).apply {
+ action = ACTION_UPDATE_NEVER
+ putExtra(EXTRA_VERSION, version)
+ }
+ val neverPendingIntent = getBroadcast(context, 2, neverIntent, FLAG_MUTABLE or FLAG_UPDATE_CURRENT)
+ val builder = NotificationCompat.Builder(context, CHANNEL_ID)
+ .setSmallIcon(R.drawable.foreground)
+ .setContentTitle("Update available")
+ .setContentText("A new version ($version) is available.")
+ .setPriority(NotificationCompat.PRIORITY_DEFAULT)
+ .setAutoCancel(true)
+ .addAction(0, "Download", yesPendingIntent)
+ .addAction(0, "Not now", noPendingIntent)
+ .addAction(0, "Never", neverPendingIntent)
+
+ NotificationManagerCompat.from(context).notify(NOTIF_ID_AVAILABLE, builder.build())
+ }
+
+ fun buildDownloadProgressNotification(context: Context, version: Int, progress: Int, indeterminate: Boolean): Notification {
+ ensureChannel(context)
+
+ val cancelIntent = Intent(context, UpdateActionReceiver::class.java).apply {
+ action = ACTION_DOWNLOAD_CANCEL
+ putExtra(EXTRA_VERSION, version)
+ }
+ val cancelPendingIntent = getBroadcast(
+ context,
+ 3,
+ cancelIntent,
+ FLAG_MUTABLE or FLAG_UPDATE_CURRENT
+ )
+
+ val builder = NotificationCompat.Builder(context, CHANNEL_ID)
+ .setSmallIcon(R.drawable.foreground)
+ .setContentTitle("Downloading update")
+ .setContentText("Downloading version $version")
+ .setPriority(NotificationCompat.PRIORITY_LOW)
+ .setOngoing(true)
+ .addAction(0, "Cancel", cancelPendingIntent)
+
+ if (indeterminate) {
+ builder.setProgress(0, 0, true)
+ } else {
+ builder.setProgress(100, progress, false)
+ }
+
+ return builder.build()
+ }
+
+ fun updateDownloadProgress(context: Context, version: Int, progress: Int, indeterminate: Boolean) {
+ if (ContextCompat.checkSelfPermission(context, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {
+ return
+ }
+ val notification = buildDownloadProgressNotification(context, version, progress, indeterminate)
+ NotificationManagerCompat.from(context).notify(NOTIF_ID_DOWNLOADING, notification)
+ }
+
+
+ fun showDownloadCompleteNotification(context: Context, version: Int, apkFile: File) {
+ if (ContextCompat.checkSelfPermission(context, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {
+ return
+ }
+ 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 builder = NotificationCompat.Builder(context, CHANNEL_ID)
+ .setSmallIcon(R.drawable.foreground)
+ .setContentTitle("Update downloaded")
+ .setContentText("Tap to install version $version.")
+ .setPriority(NotificationCompat.PRIORITY_DEFAULT)
+ .setAutoCancel(true)
+ .addAction(0, "Install", installPendingIntent)
+
+ NotificationManagerCompat.from(context).notify(NOTIF_ID_READY, builder.build())
+ }
+
+
+ fun showDownloadFailedNotification(context: Context, version: Int, error: Throwable?) {
+ if (ContextCompat.checkSelfPermission(context, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {
+ return
+ }
+ ensureChannel(context)
+
+ val builder = NotificationCompat.Builder(context, CHANNEL_ID)
+ .setSmallIcon(R.drawable.foreground)
+ .setContentTitle("Failed to download update")
+ .setContentText(error?.message ?: "Unknown error")
+ .setPriority(NotificationCompat.PRIORITY_DEFAULT)
+ .setAutoCancel(true)
+
+ NotificationManagerCompat.from(context).notify(NOTIF_ID_READY, 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)
+ }
+}
diff --git a/app/src/main/java/com/futo/platformplayer/Utility.kt b/app/src/main/java/com/futo/platformplayer/Utility.kt
index 5b00bbb9..b154cb67 100644
--- a/app/src/main/java/com/futo/platformplayer/Utility.kt
+++ b/app/src/main/java/com/futo/platformplayer/Utility.kt
@@ -5,8 +5,6 @@ import android.content.Context
import android.content.Intent
import android.content.res.Resources
import android.graphics.Bitmap
-import android.graphics.BitmapFactory
-import android.icu.util.Output
import android.os.Build
import android.os.Looper
import android.os.OperationCanceledException
@@ -44,6 +42,9 @@ import java.util.*
import java.util.concurrent.ThreadLocalRandom
import java.util.zip.GZIPInputStream
import java.util.zip.GZIPOutputStream
+import androidx.core.graphics.scale
+import com.bumptech.glide.RequestBuilder
+import com.bumptech.glide.load.resource.bitmap.DownsampleStrategy
private val _allowedCharacters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz ";
fun getRandomString(sizeOfRandomString: Int): String {
@@ -114,23 +115,6 @@ fun DocumentFile.writeBytes(context: Context, byteArray: ByteArray) = context.co
it.flush();
};
-fun loadBitmap(url: String): Bitmap {
- try {
- val client = ManagedHttpClient();
- val response = client.get(url);
- if (response.isOk && response.body != null) {
- val bitmapStream = response.body.byteStream();
- val bitmap = BitmapFactory.decodeStream(bitmapStream);
- return bitmap;
- } else {
- throw Exception("Failed to find data at URL.");
- }
- } catch (e: Throwable) {
- Logger.w("Utility", "Exception thrown while downloading bitmap.", e);
- throw e;
- }
-}
-
fun TextView.setPlatformPlayerLinkMovementMethod(context: Context) {
this.movementMethod = PlatformLinkMovementMethod(context);
}
@@ -458,4 +442,17 @@ fun addressScore(addr: InetAddress): Int {
}
}
-fun Enumeration.toList(): List = Collections.list(this)
\ No newline at end of file
+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
+}
\ No newline at end of file
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 00d39ac9..b9ecf3f4 100644
--- a/app/src/main/java/com/futo/platformplayer/activities/MainActivity.kt
+++ b/app/src/main/java/com/futo/platformplayer/activities/MainActivity.kt
@@ -245,19 +245,6 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
}
}
}
- private val _notifPermission = "android.permission.POST_NOTIFICATIONS";
- private val _notificationPermissionLauncher = registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted: Boolean ->
- if (isGranted)
- UIDialogs.toast(this, "Notification permission granted");
- else
- UIDialogs.toast(this, "Notification permission denied");
- };
-
-
-
- fun requestNotificationPermissions() {
- _notificationPermissionLauncher?.launch(_notifPermission);
- }
val mainId = UUID.randomUUID().toString().substring(0, 5)
@@ -631,6 +618,10 @@ 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.");
+ }
+
val submissionStatus = FragmentedStorage.get("subscriptionSubmissionStatus")
val numSubscriptions = StateSubscriptions.instance.getSubscriptionCount()
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 5e2b72de..fbca0f6b 100644
--- a/app/src/main/java/com/futo/platformplayer/dialogs/AutoUpdateDialog.kt
+++ b/app/src/main/java/com/futo/platformplayer/dialogs/AutoUpdateDialog.kt
@@ -16,9 +16,11 @@ import android.widget.Button
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.TextView
+import androidx.core.content.ContextCompat
import com.futo.platformplayer.R
import com.futo.platformplayer.Settings
import com.futo.platformplayer.UIDialogs
+import com.futo.platformplayer.UpdateDownloadService
import com.futo.platformplayer.api.http.ManagedHttpClient
import com.futo.platformplayer.copyToOutputStream
import com.futo.platformplayer.logging.Logger
@@ -46,7 +48,6 @@ class AutoUpdateDialog(context: Context?) : AlertDialog(context) {
private var _maxVersion: Int = 0;
private var _updating: Boolean = false;
- private var _apkFile: File? = null;
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState);
@@ -80,14 +81,19 @@ class AutoUpdateDialog(context: Context?) : AlertDialog(context) {
return@setOnClickListener;
}
- _updating = true;
- update();
+ if (Settings.instance.autoUpdate.backgroundDownload == 1) {
+ val ctx = context.applicationContext;
+ val intent = Intent(ctx, UpdateDownloadService::class.java);
+ intent.putExtra(UpdateDownloadService.EXTRA_VERSION, _maxVersion);
+ ContextCompat.startForegroundService(ctx, intent);
+ UIDialogs.toast(context, "Downloading update in background");
+ dismiss();
+ } else {
+ _updating = true;
+ update();
+ }
};
- }
- fun showPredownloaded(apkFile: File) {
- _apkFile = apkFile;
- super.show()
}
override fun dismiss() {
@@ -118,21 +124,14 @@ class AutoUpdateDialog(context: Context?) : AlertDialog(context) {
GlobalScope.launch(Dispatchers.IO) {
var inputStream: InputStream? = null;
try {
- val apkFile = _apkFile;
- if (apkFile != null) {
- inputStream = apkFile.inputStream();
- val dataLength = apkFile.length();
+ val client = ManagedHttpClient();
+ val response = client.get(StateUpdate.APK_URL);
+ if (response.isOk && response.body != null) {
+ inputStream = response.body.byteStream();
+ val dataLength = response.body.contentLength();
install(inputStream, dataLength);
} else {
- val client = ManagedHttpClient();
- val response = client.get(StateUpdate.APK_URL);
- if (response.isOk && response.body != null) {
- inputStream = response.body.byteStream();
- val dataLength = response.body.contentLength();
- install(inputStream, dataLength);
- } else {
- throw Exception("Failed to download latest version of app.");
- }
+ throw Exception("Failed to download latest version of app.");
}
} catch (e: Throwable) {
Logger.w(TAG, "Exception thrown while downloading and installing latest version of app.", e);
diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/FeedView.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/FeedView.kt
index a85e5951..d49ae1e0 100644
--- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/FeedView.kt
+++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/FeedView.kt
@@ -55,7 +55,7 @@ abstract class FeedView : L
protected val _toolbarContentView: LinearLayout;
protected val _bottomContentView: LinearLayout;
- private var _loading: Boolean = true;
+ private var _loading: Boolean = false;
private val _pagerLock = Object();
private var _cache: ItemCache? = null;
@@ -180,10 +180,9 @@ abstract class FeedView : L
val visibleItemCount = _recyclerResults.childCount;
val firstVisibleItem = recyclerData.layoutManager.findFirstVisibleItemPosition()
- //Logger.i(TAG, "onScrolled loadNextPage visibleItemCount=$visibleItemCount firstVisibleItem=$visibleItemCount")
+ //Logger.i(TAG, "onScrolled loadNextPage(): firstVisibleItem=$firstVisibleItem visibleItemCount=$visibleItemCount visibleThreshold=$visibleThreshold recyclerData.results.size=${recyclerData.results.size}")
- if (!_loading && firstVisibleItem + visibleItemCount + visibleThreshold >= recyclerData.results.size && firstVisibleItem > 0) {
- //Logger.i(TAG, "onScrolled loadNextPage(): firstVisibleItem=$firstVisibleItem visibleItemCount=$visibleItemCount visibleThreshold=$visibleThreshold recyclerData.results.size=${recyclerData.results.size}")
+ if (!_loading && firstVisibleItem + visibleItemCount + visibleThreshold >= recyclerData.results.size) {
loadNextPage();
}
}
@@ -197,57 +196,44 @@ abstract class FeedView : L
}
private fun ensureEnoughContentVisible(filteredResults: List) {
- val canScroll = if (recyclerData.results.isEmpty()) false else {
- val height = resources.displayMetrics.heightPixels;
+ _recyclerResults.post {
+ val canScroll = _recyclerResults.canScrollVertically(1)
+ Logger.i(
+ TAG,
+ "ensureEnoughContentVisible loadNextPage canScroll=$canScroll _automaticNextPageCounter=$_automaticNextPageCounter"
+ )
+ if (!canScroll || filteredResults.isEmpty()) {
+ _automaticNextPageCounter++
+ if (_automaticNextPageCounter < _automaticBackoff.size) {
+ if (_automaticNextPageCounter > 0) {
+ val automaticNextPageCounterSaved = _automaticNextPageCounter;
+ fragment.lifecycleScope.launch(Dispatchers.Default) {
+ val backoff = _automaticBackoff[Math.min(
+ _automaticBackoff.size - 1,
+ _automaticNextPageCounter
+ )];
- val layoutManager = recyclerData.layoutManager
- val firstVisibleItemPosition = layoutManager.findFirstVisibleItemPosition()
- val firstVisibleItemView = if(firstVisibleItemPosition != RecyclerView.NO_POSITION) layoutManager.findViewByPosition(firstVisibleItemPosition) else null;
- val lastVisibleItemPosition = layoutManager.findLastCompletelyVisibleItemPosition();
- val lastVisibleItemView = if(lastVisibleItemPosition != RecyclerView.NO_POSITION) layoutManager.findViewByPosition(lastVisibleItemPosition) else null;
- val rows = if(recyclerData.layoutManager is GridLayoutManager) Math.max(1, recyclerData.results.size / recyclerData.layoutManager.spanCount) else 1;
- val rowsHeight = (firstVisibleItemView?.height ?: 0) * rows;
- if(lastVisibleItemView != null && lastVisibleItemPosition == (recyclerData.results.size - 1)) {
- false;
- }
- else if (firstVisibleItemView != null && height != null && rowsHeight < height) {
- false;
- } else {
- true;
- }
- }
-
- Logger.i(TAG, "ensureEnoughContentVisible loadNextPage canScroll=$canScroll _automaticNextPageCounter=$_automaticNextPageCounter")
- if (!canScroll || filteredResults.isEmpty()) {
- _automaticNextPageCounter++
- if(_automaticNextPageCounter < _automaticBackoff.size) {
- if(_automaticNextPageCounter > 0) {
- val automaticNextPageCounterSaved = _automaticNextPageCounter;
- fragment.lifecycleScope.launch(Dispatchers.Default) {
- val backoff = _automaticBackoff[Math.min(_automaticBackoff.size - 1, _automaticNextPageCounter)];
-
- withContext(Dispatchers.Main) {
- setLoading(true);
- }
- delay(backoff.toLong());
- if(automaticNextPageCounterSaved == _automaticNextPageCounter) {
withContext(Dispatchers.Main) {
- loadNextPage();
+ setLoading(true);
+ }
+ delay(backoff.toLong());
+ if (automaticNextPageCounterSaved == _automaticNextPageCounter) {
+ withContext(Dispatchers.Main) {
+ loadNextPage();
+ }
+ } else {
+ withContext(Dispatchers.Main) {
+ setLoading(false);
+ }
}
}
- else {
- withContext(Dispatchers.Main) {
- setLoading(false);
- }
- }
- }
+ } else
+ loadNextPage();
}
- else
- loadNextPage();
+ } else {
+ Logger.i(TAG, "ensureEnoughContentVisible automaticNextPageCounter reset");
+ _automaticNextPageCounter = 0;
}
- } else {
- Logger.i(TAG, "ensureEnoughContentVisible automaticNextPageCounter reset");
- _automaticNextPageCounter = 0;
}
}
fun resetAutomaticNextPageCounter(){
@@ -484,6 +470,7 @@ abstract class FeedView : L
recyclerData.resultsUnfiltered.addAll(toAdd);
recyclerData.adapter.notifyDataSetChanged();
recyclerData.loadedFeedStyle = feedStyle;
+ setLoading(false)
if(pager.hasMorePages())
ensureEnoughContentVisible(filteredResults)
}
diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/LibraryVideosFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/LibraryVideosFragment.kt
index 43ca3700..0a9f8bf7 100644
--- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/LibraryVideosFragment.kt
+++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/LibraryVideosFragment.kt
@@ -96,7 +96,6 @@ class LibraryVideosFragment : MainFragment() {
fun onShown() {
val initialAlbums = StateLibrary.instance.getAlbums();
Logger.i(TAG, "Initial album count: " + initialAlbums.size);
- val buckets = StateLibrary.instance.getVideoBucketNames();
setPager(StateLibrary.instance.getVideos(fragment._toggleBuckets));
}
diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/RemotePlaylistFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/RemotePlaylistFragment.kt
index 349b4fb0..0b3a0d6b 100644
--- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/RemotePlaylistFragment.kt
+++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/RemotePlaylistFragment.kt
@@ -16,6 +16,7 @@ import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide
+import com.bumptech.glide.load.resource.bitmap.DownsampleStrategy
import com.futo.platformplayer.*
import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylist
import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylistDetails
@@ -363,6 +364,7 @@ class RemotePlaylistFragment : MainFragment() {
_imagePlaylistThumbnail.let {
Glide.with(it)
.load(video.thumbnails.getHQThumbnail())
+ .withMaxSizePx()
.placeholder(R.drawable.placeholder_video_thumbnail)
.crossfade()
.into(it);
diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ShortView.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ShortView.kt
index e3a0a655..753fcb81 100644
--- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ShortView.kt
+++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ShortView.kt
@@ -2,7 +2,9 @@ package com.futo.platformplayer.fragment.mainactivity.main
import android.content.Context
import android.content.Intent
+import android.graphics.Bitmap
import android.graphics.drawable.Animatable
+import android.graphics.drawable.Drawable
import android.util.AttributeSet
import android.view.LayoutInflater
import android.view.WindowManager
@@ -13,10 +15,15 @@ import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.TextView
import androidx.constraintlayout.widget.ConstraintLayout
+import androidx.core.graphics.drawable.toDrawable
import androidx.lifecycle.lifecycleScope
import androidx.media3.common.C
import androidx.media3.common.Format
import androidx.media3.common.util.UnstableApi
+import com.bumptech.glide.Glide
+import com.bumptech.glide.load.resource.bitmap.DownsampleStrategy
+import com.bumptech.glide.request.target.CustomTarget
+import com.bumptech.glide.request.transition.Transition
import com.futo.platformplayer.R
import com.futo.platformplayer.Settings
import com.futo.platformplayer.UIDialogs
@@ -71,6 +78,7 @@ import com.futo.platformplayer.views.video.FutoShortPlayer
import com.futo.platformplayer.views.video.FutoVideoPlayerBase
import com.futo.platformplayer.views.video.FutoVideoPlayerBase.Companion.PREFERED_AUDIO_CONTAINERS
import com.futo.platformplayer.views.video.FutoVideoPlayerBase.Companion.PREFERED_VIDEO_CONTAINERS
+import com.futo.platformplayer.withMaxSizePx
import com.futo.polycentric.core.ApiMethods
import com.futo.polycentric.core.ContentType
import com.futo.polycentric.core.Models
@@ -851,9 +859,8 @@ class ShortView : FrameLayout {
}
val thumbnail = videoDetails.thumbnails.getHQThumbnail()
- /*
if (videoSource == null && !thumbnail.isNullOrBlank()) Glide.with(context).asBitmap()
- .load(thumbnail).into(object : CustomTarget() {
+ .load(thumbnail).withMaxSizePx().into(object : CustomTarget() {
override fun onResourceReady(resource: Bitmap, transition: Transition?) {
player.setArtwork(resource.toDrawable(resources))
}
@@ -863,7 +870,6 @@ class ShortView : FrameLayout {
}
})
else player.setArtwork(null)
- */
fragment.lifecycleScope.launch(Dispatchers.Main) {
try {
diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt
index d3558c07..ca5af1f9 100644
--- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt
+++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt
@@ -42,6 +42,7 @@ import androidx.media3.datasource.HttpDataSource
import androidx.media3.ui.PlayerControlView
import androidx.media3.ui.TimeBar
import com.bumptech.glide.Glide
+import com.bumptech.glide.load.resource.bitmap.DownsampleStrategy
import com.bumptech.glide.request.target.CustomTarget
import com.bumptech.glide.request.transition.Transition
import com.futo.platformplayer.BuildConfig
@@ -161,6 +162,7 @@ import com.futo.platformplayer.views.subscriptions.SubscribeButton
import com.futo.platformplayer.views.video.FutoVideoPlayer
import com.futo.platformplayer.views.video.FutoVideoPlayerBase
import com.futo.platformplayer.views.videometa.UpNextView
+import com.futo.platformplayer.withMaxSizePx
import com.futo.polycentric.core.ApiMethods
import com.futo.polycentric.core.ContentType
import com.futo.polycentric.core.Models
@@ -552,12 +554,12 @@ class VideoDetailView : ConstraintLayout {
_buttonMore = buttonMore;
updateMoreButtons();
- val handleLoaderGameVisibilityChanged = { b: Boolean ->
+ val handleLoaderGameVisibilityChanged: (Boolean) -> Unit = { b: Boolean ->
_loaderGameVisible = b
fragment.lifecycleScope.launch(Dispatchers.Main) {
onShouldEnterPictureInPictureChanged.emit()
+ updateResumeVisibilityFor(lastPositionMilliseconds)
}
- updateResumeVisibilityFor(lastPositionMilliseconds)
}
_player.loaderGameVisibilityChanged.subscribe(handleLoaderGameVisibilityChanged)
_cast.loaderGameVisibilityChanged.subscribe(handleLoaderGameVisibilityChanged)
@@ -2049,7 +2051,7 @@ class VideoDetailView : ConstraintLayout {
} else {
val thumbnail = video.thumbnails.getHQThumbnail();
if ((videoSource == null) && !thumbnail.isNullOrBlank()) // || _player.isAudioMode
- Glide.with(context).asBitmap().load(thumbnail)
+ Glide.with(context).asBitmap().load(thumbnail).withMaxSizePx()
.into(object: CustomTarget() {
override fun onResourceReady(resource: Bitmap, transition: Transition?) {
_player.setArtwork(BitmapDrawable(resources, resource));
diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoListEditorView.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoListEditorView.kt
index ae890d94..830df866 100644
--- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoListEditorView.kt
+++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoListEditorView.kt
@@ -14,6 +14,7 @@ import android.widget.TextView
import androidx.core.view.isVisible
import androidx.core.view.setPadding
import com.bumptech.glide.Glide
+import com.bumptech.glide.load.resource.bitmap.DownsampleStrategy
import com.futo.platformplayer.R
import com.futo.platformplayer.UIDialogs
import com.futo.platformplayer.UISlideOverlays
@@ -28,6 +29,7 @@ import com.futo.platformplayer.toHumanDuration
import com.futo.platformplayer.toHumanTime
import com.futo.platformplayer.views.SearchView
import com.futo.platformplayer.views.lists.VideoListEditorView
+import com.futo.platformplayer.withMaxSizePx
abstract class VideoListEditorView : LinearLayout {
private var _videoListEditorView: VideoListEditorView;
@@ -211,6 +213,7 @@ abstract class VideoListEditorView : LinearLayout {
_imagePlaylistThumbnail.let {
Glide.with(it)
.load(video.thumbnails.getHQThumbnail())
+ .withMaxSizePx()
.placeholder(R.drawable.placeholder_video_thumbnail)
.crossfade()
.into(it);
diff --git a/app/src/main/java/com/futo/platformplayer/images/GlideHelper.kt b/app/src/main/java/com/futo/platformplayer/images/GlideHelper.kt
index 4b98cdeb..842fd5c6 100644
--- a/app/src/main/java/com/futo/platformplayer/images/GlideHelper.kt
+++ b/app/src/main/java/com/futo/platformplayer/images/GlideHelper.kt
@@ -4,8 +4,10 @@ import android.graphics.drawable.Drawable
import android.widget.ImageView
import com.bumptech.glide.Glide
import com.bumptech.glide.RequestBuilder
+import com.bumptech.glide.load.resource.bitmap.DownsampleStrategy
import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions
import com.futo.platformplayer.api.media.models.Thumbnails
+import com.futo.platformplayer.withMaxSizePx
class GlideHelper {
@@ -14,7 +16,7 @@ class GlideHelper {
fun ImageView.loadThumbnails(thumbnails: Thumbnails, isHQ: Boolean = true, continuation: ((RequestBuilder) -> Unit)? = null) {
val url = if(isHQ) thumbnails.getHQThumbnail() ?: thumbnails.getLQThumbnail() else thumbnails.getLQThumbnail();
- val req = Glide.with(this).load(url);
+ val req = Glide.with(this).load(url).withMaxSizePx()
if (thumbnails.hasMultiple() && false) { //TODO: Resolve issue where fallback triggered on second loads?
val fallbackUrl = if (isHQ) thumbnails.getLQThumbnail() else thumbnails.getHQThumbnail();
diff --git a/app/src/main/java/com/futo/platformplayer/receivers/InstallReceiver.kt b/app/src/main/java/com/futo/platformplayer/receivers/InstallReceiver.kt
index 9112b6d5..2105a4a9 100644
--- a/app/src/main/java/com/futo/platformplayer/receivers/InstallReceiver.kt
+++ b/app/src/main/java/com/futo/platformplayer/receivers/InstallReceiver.kt
@@ -1,5 +1,6 @@
package com.futo.platformplayer.receivers
+import android.content.ActivityNotFoundException
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
@@ -26,14 +27,24 @@ class InstallReceiver : BroadcastReceiver() {
val activityIntent: Intent? = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
intent.getParcelableExtra(Intent.EXTRA_INTENT, Intent::class.java)
} else {
+ @Suppress("DEPRECATION")
intent.getParcelableExtra(Intent.EXTRA_INTENT)
}
if (activityIntent == null) {
Logger.w(TAG, "Received STATUS_PENDING_USER_ACTION and activity intent is null.")
+ onReceiveResult.emit(context.getString(R.string.install_failed_device_installer_broken))
return;
}
- context.startActivity(activityIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK))
+
+ activityIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+
+ try {
+ context.startActivity(activityIntent)
+ } catch (e: ActivityNotFoundException) {
+ Logger.e(TAG, "System installer cannot handle CONFIRM_INSTALL intent. ROM is broken; falling back / reporting error.", e)
+ onReceiveResult.emit(context.getString(R.string.install_failed_device_installer_broken))
+ }
}
PackageInstaller.STATUS_SUCCESS -> onReceiveResult.emit(null);
PackageInstaller.STATUS_FAILURE -> onReceiveResult.emit(context.getString(R.string.general_failure));
@@ -45,6 +56,7 @@ class InstallReceiver : BroadcastReceiver() {
PackageInstaller.STATUS_FAILURE_STORAGE -> onReceiveResult.emit(context.getString(R.string.not_enough_storage));
else -> {
val msg = intent.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE);
+ Logger.w(TAG, "Received unknown install status $status, message=$msg")
onReceiveResult.emit(msg)
}
}
diff --git a/app/src/main/java/com/futo/platformplayer/services/MediaPlaybackService.kt b/app/src/main/java/com/futo/platformplayer/services/MediaPlaybackService.kt
index f866975f..0ad801bc 100644
--- a/app/src/main/java/com/futo/platformplayer/services/MediaPlaybackService.kt
+++ b/app/src/main/java/com/futo/platformplayer/services/MediaPlaybackService.kt
@@ -26,6 +26,7 @@ import android.util.Log
import android.view.KeyEvent
import androidx.core.app.NotificationCompat
import com.bumptech.glide.Glide
+import com.bumptech.glide.load.resource.bitmap.DownsampleStrategy
import com.bumptech.glide.request.target.CustomTarget
import com.bumptech.glide.request.transition.Transition
import com.futo.platformplayer.R
@@ -38,6 +39,7 @@ import com.futo.platformplayer.receivers.MediaControlReceiver
import com.futo.platformplayer.states.StatePlatform
import com.futo.platformplayer.states.StatePlayer
import com.futo.platformplayer.stores.FragmentedStorage
+import com.futo.platformplayer.withMaxSizePx
class MediaPlaybackService : Service() {
private val TAG = "MediaPlaybackService";
@@ -172,21 +174,26 @@ class MediaPlaybackService : Service() {
}
fun closeMediaSession() {
- Logger.v(TAG, "closeMediaSession");
- stopForeground(STOP_FOREGROUND_REMOVE);
+ Logger.v(TAG, "closeMediaSession")
+ stopForeground(STOP_FOREGROUND_REMOVE)
abandonAudioFocus()
- val notifManager = _notificationManager;
- Logger.i(TAG, "Cancelling playback notification (notifManager: ${notifManager != null})");
- notifManager?.cancel(MEDIA_NOTIF_ID);
- _notif_last_video = null;
- _notif_last_bitmap = null;
- _mediaSession = null;
+ val notifManager = _notificationManager
+ Logger.i(TAG, "Cancelling playback notification (notifManager: ${notifManager != null})")
+ notifManager?.cancel(MEDIA_NOTIF_ID)
- if(_instance == this)
- _instance = null;
- this.stopSelf();
+ _notif_last_video = null
+ _notif_last_bitmap = null
+
+ _mediaSession?.isActive = false
+ _mediaSession?.release()
+ _mediaSession = null
+
+ if (_instance == this)
+ _instance = null
+
+ stopSelf()
}
fun updateMediaSession(videoUpdated: IPlatformVideo?) {
@@ -206,37 +213,37 @@ class MediaPlaybackService : Service() {
if(_notificationChannel == null || _mediaSession == null)
setupNotificationRequirements();
- _mediaSession?.setMetadata(
- MediaMetadataCompat.Builder()
- .putString(MediaMetadata.METADATA_KEY_ARTIST, video.author.name)
- .putString(MediaMetadata.METADATA_KEY_TITLE, video.name)
- .putLong(MediaMetadata.METADATA_KEY_DURATION, video.duration * 1000)
- .putBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART, lastBitmap)
- .build());
+ updateMediaMetadata(video, lastBitmap)
val thumbnail = video.thumbnails.getHQThumbnail();
_notif_last_video = video;
if(isUpdating)
- notifyMediaSession(video, _notif_last_bitmap);
+ notifyMediaSession(video, _notif_last_bitmap?.takeIf { !it.isRecycled });
else if(thumbnail != null) {
notifyMediaSession(video, null);
val tag = video;
Glide.with(this).asBitmap()
.load(thumbnail)
+ .withMaxSizePx()
.into(object: CustomTarget() {
override fun onResourceReady(resource: Bitmap,transition: Transition?) {
- if(tag == _notif_last_video) {
- notifyMediaSession(video, resource)
- _mediaSession?.setMetadata(
- MediaMetadataCompat.Builder()
- .putString(MediaMetadata.METADATA_KEY_ARTIST, video.author.name)
- .putString(MediaMetadata.METADATA_KEY_TITLE, video.name)
- .putLong(MediaMetadata.METADATA_KEY_DURATION, video.duration * 1000)
- .putBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART, resource)
- .build());
+ if (tag != _notif_last_video) return
+ if (resource.isRecycled) {
+ notifyMediaSession(video, null)
+ return
}
+
+ val albumArt = resource.copy(
+ resource.config ?: Bitmap.Config.ARGB_8888,
+ false
+ )
+
+ _notif_last_bitmap = albumArt
+
+ notifyMediaSession(video, albumArt)
+ updateMediaMetadata(video, albumArt)
}
override fun onLoadCleared(placeholder: Drawable?) {
if(tag == _notif_last_video)
@@ -247,6 +254,19 @@ class MediaPlaybackService : Service() {
else
notifyMediaSession(video, null);
}
+ private fun updateMediaMetadata(video: IPlatformVideo, bitmap: Bitmap?) {
+ val builder = MediaMetadataCompat.Builder()
+ .putString(MediaMetadata.METADATA_KEY_ARTIST, video.author.name)
+ .putString(MediaMetadata.METADATA_KEY_TITLE, video.name)
+ .putLong(MediaMetadata.METADATA_KEY_DURATION, video.duration * 1000)
+
+ val safeBitmap = bitmap?.takeIf { !it.isRecycled }
+ if (safeBitmap != null) {
+ builder.putBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART, safeBitmap)
+ }
+
+ _mediaSession?.setMetadata(builder.build())
+ }
private fun generateMediaAction(icon: Int, title: String, intent: PendingIntent) : NotificationCompat.Action {
return NotificationCompat.Action.Builder(icon, title, intent).build();
}
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 3912f625..7ac860e3 100644
--- a/app/src/main/java/com/futo/platformplayer/states/StateApp.kt
+++ b/app/src/main/java/com/futo/platformplayer/states/StateApp.kt
@@ -572,30 +572,39 @@ class StateApp {
DownloadService.getOrCreateService(context);
}
- Logger.i(TAG, "MainApp Started: Initialize [AutoUpdate]");
- val autoUpdateEnabled = Settings.instance.autoUpdate.isAutoUpdateEnabled();
- val shouldDownload = Settings.instance.autoUpdate.shouldDownload();
- val backgroundDownload = Settings.instance.autoUpdate.backgroundDownload == 1;
- when {
- //Background download
- autoUpdateEnabled && shouldDownload && backgroundDownload -> {
- StateUpdate.instance.setShouldBackgroundUpdate(true);
- }
+ if (Settings.instance.autoUpdate.isAutoUpdateEnabled()) {
+ if (Settings.instance.autoUpdate.backgroundDownload == 1) {
+ Logger.i(TAG, "MainApp Started: Initialize [AutoUpdate Background]");
+ val constraints = Constraints.Builder()
+ .setRequiredNetworkType(NetworkType.CONNECTED)
+ .build();
- autoUpdateEnabled && !shouldDownload && backgroundDownload -> {
- Logger.i(TAG, "Auto update skipped due to wrong network state");
- }
+ val periodicRequest = PeriodicWorkRequest.Builder(
+ UpdateCheckWorker::class.java,
+ 12, TimeUnit.HOURS
+ )
+ .setConstraints(constraints)
+ .build();
- //Foreground download
- autoUpdateEnabled -> {
+ val wm = WorkManager.getInstance(context);
+ wm.enqueueUniquePeriodicWork(
+ UpdateCheckWorker.UNIQUE_WORK_NAME,
+ ExistingPeriodicWorkPolicy.UPDATE,
+ periodicRequest
+ );
+
+ val oneTimeRequest = OneTimeWorkRequest.Builder(UpdateCheckWorker::class.java)
+ .setConstraints(constraints)
+ .build();
+ wm.enqueue(oneTimeRequest);
+ } else {
+ Logger.i(TAG, "MainApp Started: Initialize [AutoUpdate]");
scopeOrNull?.launch(Dispatchers.IO) {
StateUpdate.instance.checkForUpdates(context, false)
}
}
-
- else -> {
- Logger.i(TAG, "Auto update disabled");
- }
+ } else {
+ Logger.i(TAG, "AutoUpdate disabled");
}
Logger.i(TAG, "MainApp Started: Initialize [Noisy]");
@@ -781,24 +790,20 @@ class StateApp {
Logger.i("StateApp", "No AutoBackup configured");
}
-
fun scheduleBackgroundWork(context: Context, active: Boolean = true, intervalMinutes: Int = 60 * 12) {
try {
val wm = WorkManager.getInstance(context);
- if(active) {
- if(BuildConfig.DEBUG)
+ if (active) {
+ if (BuildConfig.DEBUG)
UIDialogs.toast(context, "Scheduling background every ${intervalMinutes} minutes");
val req = PeriodicWorkRequest.Builder(BackgroundWorker::class.java, intervalMinutes.toLong(), TimeUnit.MINUTES, 5, TimeUnit.MINUTES)
- .setConstraints(Constraints.Builder()
- .setRequiredNetworkType(NetworkType.UNMETERED)
- .build())
- .build();
+ .setConstraints(Constraints.Builder().setRequiredNetworkType(NetworkType.UNMETERED).build()).build();
wm.enqueueUniquePeriodicWork("backgroundSubscriptions", ExistingPeriodicWorkPolicy.UPDATE, req);
+ } else {
+ wm.cancelUniqueWork("backgroundSubscriptions");
}
- else
- wm.cancelAllWork();
} catch (e: Throwable) {
Logger.e(TAG, "Failed to schedule background subscription updates.", e)
UIDialogs.toast(context, "Background subscription update failed: " + e.message)
@@ -806,6 +811,7 @@ class StateApp {
}
+
private suspend fun migrateStores(context: Context, managedStores: List>, index: Int) {
if(managedStores.size <= index)
return;
@@ -903,15 +909,6 @@ class StateApp {
try {
if(FragmentedStorage.isInitialized && Settings.instance.downloads.shouldDownload())
StateDownloads.instance.checkForDownloadsTodos();
-
- val autoUpdateEnabled = Settings.instance.autoUpdate.isAutoUpdateEnabled();
- val shouldDownload = Settings.instance.autoUpdate.shouldDownload();
- val backgroundDownload = Settings.instance.autoUpdate.backgroundDownload == 1;
- if (autoUpdateEnabled && shouldDownload && backgroundDownload) {
- StateUpdate.instance.setShouldBackgroundUpdate(true);
- } else {
- StateUpdate.instance.setShouldBackgroundUpdate(false);
- }
} catch(ex: Throwable) {
Logger.w(TAG, "Failed to handle capabilities changed event", ex);
}
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 2a5c4c8e..9176b8ae 100644
--- a/app/src/main/java/com/futo/platformplayer/states/StateLibrary.kt
+++ b/app/src/main/java/com/futo/platformplayer/states/StateLibrary.kt
@@ -1,10 +1,12 @@
package com.futo.platformplayer.states
+import android.content.ContentResolver
import android.content.ContentUris
import android.content.Intent
import android.database.Cursor
import android.net.Uri
import android.os.Build
+import android.os.Bundle
import android.provider.MediaStore
import android.provider.MediaStore.Audio.Artists
import android.webkit.MimeTypeMap
@@ -154,34 +156,101 @@ class StateLibrary {
fun getArtist(id: Long): Artist? {
return Artist.getArtist(id);
}
+ fun getVideos(
+ buckets: List? = null,
+ pageSize: Int = 20
+ ): IPager {
+ val resolver = StateApp.instance.contextOrNull?.contentResolver ?: return EmptyPager()
+ val selection: String?
+ val selectionArgs: Array?
- fun getVideos(buckets: List? = null): IPager {
- var query = if(buckets != null) "${MediaStore.Video.Media.BUCKET_DISPLAY_NAME} IN " + "(" + buckets.map { "'${it}'" }.joinToString(",") + ")" else null;
- val cursor = StateApp.instance.contextOrNull?.contentResolver?.query(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, PROJECTION_VIDEO,
- query,
- null,
- MediaStore.Video.Media.DATE_ADDED + " DESC") ?: return EmptyPager();
+ if (!buckets.isNullOrEmpty()) {
+ val placeholders = buckets.joinToString(",") { "?" }
+ selection = "${MediaStore.Video.Media.BUCKET_DISPLAY_NAME} IN ($placeholders)"
+ selectionArgs = buckets.toTypedArray()
+ } else {
+ selection = null
+ selectionArgs = null
+ }
- //Ongoing usage of cursor..todo disposal
- //return cursor.use {
- cursor.moveToFirst();
- val list = mutableListOf()
- while(!cursor.isAfterLast && list.size < 10) {
- list.add(videoFromCursor(cursor));
- cursor.moveToNext();
+ val collectionUri = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+ MediaStore.Video.Media.getContentUri(MediaStore.VOLUME_EXTERNAL)
+ } else {
+ MediaStore.Video.Media.EXTERNAL_CONTENT_URI
+ }
+
+ var nextPageIndex = 0
+ fun loadPage(pageIndex: Int): List {
+ Logger.i(TAG, "loadPage $pageIndex")
+ val offset = pageIndex * pageSize
+
+ val queryArgs = Bundle().apply {
+ selection?.let {
+ putString(ContentResolver.QUERY_ARG_SQL_SELECTION, it)
+ }
+ selectionArgs?.let {
+ putStringArray(ContentResolver.QUERY_ARG_SQL_SELECTION_ARGS, it)
+ }
+
+ putStringArray(
+ ContentResolver.QUERY_ARG_SORT_COLUMNS,
+ arrayOf(
+ MediaStore.Video.Media.DATE_ADDED,
+ MediaStore.Video.Media._ID
+ )
+ )
+ putInt(
+ ContentResolver.QUERY_ARG_SORT_DIRECTION,
+ ContentResolver.QUERY_SORT_DIRECTION_DESCENDING
+ )
+
+ putInt(ContentResolver.QUERY_ARG_LIMIT, pageSize)
+ putInt(ContentResolver.QUERY_ARG_OFFSET, offset)
}
- return AdhocPager({
- val list = mutableListOf()
- while(!cursor.isAfterLast && list.size < 10) {
- list.add(videoFromCursor(cursor));
- cursor.moveToNext();
+ val cursor = resolver.query(
+ collectionUri,
+ PROJECTION_VIDEO,
+ queryArgs,
+ null
+ )
+
+ if (cursor == null) {
+ Logger.i(TAG, "loadPage $pageIndex null, returning empty list")
+ return emptyList()
+ }
+
+ cursor.use { c ->
+ if (!c.moveToFirst()) {
+ Logger.i(TAG, "loadPage $pageIndex moveToFirst failed, returning empty list")
+ return emptyList()
}
- Logger.i(TAG, "Videos nextPage: ${list.size}")
- return@AdhocPager list;
- }, list);
- //}
+
+ val list = ArrayList(pageSize)
+ do {
+ list.add(videoFromCursor(c))
+ } while (c.moveToNext() && list.size < pageSize)
+
+ Logger.i(TAG, "loadPage $pageIndex found ${list.size} items")
+ return list
+ }
+ }
+
+ val firstPage = loadPage(0)
+ if (firstPage.isEmpty()) {
+ return EmptyPager()
+ }
+ nextPageIndex = 1
+
+ return AdhocPager({
+ val page = loadPage(nextPageIndex)
+ nextPageIndex++
+
+ Logger.i(TAG, "loadPage nextPage: ${page.size}")
+ page
+ }, firstPage)
}
+
fun getRecentVideos(buckets: List? = null, count: Int = 20): List {
val videoPager = getVideos(buckets);
val items = mutableListOf();
@@ -193,48 +262,80 @@ class StateLibrary {
return items;
}
- private var _cacheBucketNames: List? = null;
- fun getVideoBucketNames(): List {
- if(Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU)
- return listOf();
- if(_cacheBucketNames != null)
- return _cacheBucketNames ?: listOf();
- try {
- val cur: Cursor = StateApp.instance.contextOrNull?.contentResolver?.query(
- MediaStore.Video.Media.EXTERNAL_CONTENT_URI, arrayOf(
- MediaStore.Video.Media.BUCKET_ID,
- MediaStore.Video.Media.BUCKET_DISPLAY_NAME,
- ), null, null, null
- ) ?: return listOf();
+ @Volatile
+ private var _cachedVideoBuckets: List? = null
+ private val _bucketCacheLock = Any()
- return cur.use {
- val buckets = mutableListOf();
- val list = HashSet();
- if (cur.moveToFirst()) {
- var id: Long;
- var bucket: String
- do {
- try {
- id = cur.getLong(0);
- bucket = cur.getStringOrNull(1) ?: continue;
- if (!list.contains(id)) {
- list.add(id);
- buckets.add(Bucket(id, bucket));
- }
- } catch (ex: Throwable) {
- Logger.e(TAG, "Failed to parse bucket due to ${ex.message}", ex);
- }
- } while (cur.moveToNext())
+ fun getVideoBucketNames(forceRefresh: Boolean = false): List {
+ if (!forceRefresh) {
+ _cachedVideoBuckets?.let { return it }
+ }
+
+ val resolver = StateApp.instance.contextOrNull?.contentResolver
+ ?: return emptyList()
+
+ val projection = arrayOf(
+ MediaStore.Video.VideoColumns.BUCKET_ID,
+ MediaStore.Video.VideoColumns.BUCKET_DISPLAY_NAME
+ )
+
+ val sortOrder = "${MediaStore.Video.VideoColumns.BUCKET_DISPLAY_NAME} COLLATE NOCASE ASC"
+ val loadedBuckets: List = try {
+ resolver.query(
+ MediaStore.Video.Media.EXTERNAL_CONTENT_URI,
+ projection,
+ null,
+ null,
+ sortOrder
+ )?.use { cursor ->
+ if (!cursor.moveToFirst()) {
+ return@use emptyList()
}
- _cacheBucketNames = buckets.toList()
- return@use _cacheBucketNames ?: listOf();
+
+ val idxId = cursor.getColumnIndexOrThrow(MediaStore.Video.VideoColumns.BUCKET_ID)
+ val idxName = cursor.getColumnIndexOrThrow(MediaStore.Video.VideoColumns.BUCKET_DISPLAY_NAME)
+ val seenIds = HashSet()
+ val buckets = ArrayList()
+
+ do {
+ try {
+ val id = cursor.getLong(idxId)
+ if (!seenIds.add(id)) {
+ continue
+ }
+
+ val name = cursor.getStringOrNull(idxName) ?: continue
+ buckets.add(Bucket(id, name))
+ } catch (e: Exception) {
+ Logger.e(TAG, "Failed to parse video bucket row: ${e.message}", e)
+ }
+ } while (cursor.moveToNext())
+
+ buckets
+ } ?: emptyList()
+ } catch (e: Exception) {
+ Logger.e(TAG, "Buckets loading failed, returning empty: ${e.message}", e)
+ emptyList()
+ }
+
+ if (loadedBuckets.isEmpty()) {
+ if (!forceRefresh) {
+ _cachedVideoBuckets?.let { return it }
}
+ return emptyList()
}
- catch(ex: Throwable) {
- Logger.e(TAG, "Buckets loading failed, returning empty");
- return listOf();
+
+ synchronized(_bucketCacheLock) {
+ if (!forceRefresh) {
+ _cachedVideoBuckets?.let { return it }
+ }
+ _cachedVideoBuckets = loadedBuckets
+ return loadedBuckets
}
}
+ fun invalidateVideoBucketNamesCache() {
+ _cachedVideoBuckets = null
+ }
companion object {
diff --git a/app/src/main/java/com/futo/platformplayer/states/StateNotifications.kt b/app/src/main/java/com/futo/platformplayer/states/StateNotifications.kt
index e5f01a84..11b9719f 100644
--- a/app/src/main/java/com/futo/platformplayer/states/StateNotifications.kt
+++ b/app/src/main/java/com/futo/platformplayer/states/StateNotifications.kt
@@ -10,6 +10,7 @@ import android.graphics.drawable.Drawable
import android.os.Build
import androidx.core.app.NotificationCompat
import com.bumptech.glide.Glide
+import com.bumptech.glide.load.resource.bitmap.DownsampleStrategy
import com.bumptech.glide.request.target.CustomTarget
import com.bumptech.glide.request.transition.Transition
import com.futo.platformplayer.activities.MainActivity
@@ -22,6 +23,7 @@ import com.futo.platformplayer.serializers.PlatformContentSerializer
import com.futo.platformplayer.stores.FragmentedStorage
import com.futo.platformplayer.toHumanNowDiffString
import com.futo.platformplayer.toHumanNowDiffStringMinDay
+import com.futo.platformplayer.withMaxSizePx
import java.time.OffsetDateTime
class StateNotifications {
@@ -96,6 +98,7 @@ class StateNotifications {
if(thumbnail != null)
Glide.with(context).asBitmap()
.load(thumbnail)
+ .withMaxSizePx()
.into(object: CustomTarget() {
override fun onResourceReady(resource: Bitmap, transition: Transition?) {
notifyNewContent(context, manager, notificationChannel, id, content, resource);
diff --git a/app/src/main/java/com/futo/platformplayer/states/StatePlayer.kt b/app/src/main/java/com/futo/platformplayer/states/StatePlayer.kt
index fa2fb58b..2e4845fb 100644
--- a/app/src/main/java/com/futo/platformplayer/states/StatePlayer.kt
+++ b/app/src/main/java/com/futo/platformplayer/states/StatePlayer.kt
@@ -247,17 +247,29 @@ class StatePlayer {
}
private fun createShuffledQueue() {
- val currentItem = getCurrentQueueItem();
- if (_queuePosition == -1 || currentItem == null) {
- _queueShuffled = _queue.shuffled().toMutableList()
- return;
+ if (_queue.isEmpty()) {
+ _queueShuffled = mutableListOf()
+ return
}
- val nextItems = _queue.subList(Math.min(_queuePosition + 1, _queue.size - 1), _queue.size).shuffled();
- val previousItems = _queue.subList(0, _queuePosition).shuffled();
- _queueShuffled = (previousItems + currentItem + nextItems).toMutableList();
+ val currentItem = getCurrentQueueItem()
+ if (currentItem == null || _queuePosition !in _queue.indices) {
+ _queueShuffled = _queue.shuffled().toMutableList()
+ return
+ }
+
+ val previousItems = _queue
+ .take(_queuePosition)
+ .shuffled()
+
+ val nextItems = _queue
+ .drop(_queuePosition + 1)
+ .shuffled()
+
+ _queueShuffled = (previousItems + currentItem + nextItems).toMutableList()
}
+
private fun addToShuffledQueue(video: IPlatformVideo) {
val isLastVideo = _queuePosition + 1 >= _queue.size;
if (isLastVideo) {
diff --git a/app/src/main/java/com/futo/platformplayer/states/StateUpdate.kt b/app/src/main/java/com/futo/platformplayer/states/StateUpdate.kt
index 7ef370c3..2a39bb1b 100644
--- a/app/src/main/java/com/futo/platformplayer/states/StateUpdate.kt
+++ b/app/src/main/java/com/futo/platformplayer/states/StateUpdate.kt
@@ -15,146 +15,6 @@ import java.io.InputStream
import java.io.OutputStream
class StateUpdate {
- private var _backgroundUpdateFinished = false;
- private var _gettingOrDownloadingLastApk = false;
- private var _shouldBackgroundUpdate = false;
- private val _lockObject = Object();
-
- private fun getOrDownloadLastApkFile(filesDir: File): File? {
- try {
- Logger.i(TAG, "Started getting or downloading latest APK file.");
-
- if (!_shouldBackgroundUpdate) {
- Logger.i(TAG, "Update download cancelled 1.");
- return null;
- }
-
- Logger.i(TAG, "Started background update download.");
- val client = ManagedHttpClient();
- val latestVersion = downloadVersionCode(client);
- if (!_shouldBackgroundUpdate) {
- Logger.i(TAG, "Update download cancelled 2.");
- return null;
- }
-
- if (latestVersion != null) {
- val currentVersion = BuildConfig.VERSION_CODE;
- Logger.i(TAG, "Current version ${currentVersion} latest version ${latestVersion}.");
-
- if (latestVersion <= currentVersion) {
- Logger.i(TAG, "Already up to date.");
- _backgroundUpdateFinished = true;
- return null;
- }
-
- val outputDirectory = File(filesDir, "autoupdate");
- if (!outputDirectory.exists()) {
- outputDirectory.mkdirs();
- }
-
- if (!_shouldBackgroundUpdate) {
- Logger.i(TAG, "Update download cancelled 3.");
- return null;
- }
-
- val apkOutputFile = File(outputDirectory, "last_version.apk");
- val versionOutputFile = File(outputDirectory, "last_version.txt");
-
- var cachedVersionInvalid = false;
- if (!versionOutputFile.exists() || !apkOutputFile.exists()) {
- Logger.i(TAG, "No downloaded version exists.");
- cachedVersionInvalid = true;
- } else {
- try {
- val downloadedVersion = versionOutputFile.readText().toInt();
- Logger.i(TAG, "Downloaded version is $downloadedVersion.");
- if (downloadedVersion != latestVersion) {
- Logger.i(TAG, "Downloaded version is not newest version.");
- cachedVersionInvalid = true;
- }
- }
- catch(ex: Throwable) {
- Logger.w(TAG, "Deleted version file as it was inaccessible");
- versionOutputFile.delete();
- cachedVersionInvalid = true;
- }
- }
-
- if (!_shouldBackgroundUpdate) {
- Logger.i(TAG, "Update download cancelled 4.");
- return null;
- }
-
- if (cachedVersionInvalid) {
- Logger.i(TAG, "Downloading new APK to '${apkOutputFile.path}'...");
- downloadApkToFile(client, apkOutputFile) { !_shouldBackgroundUpdate };
- versionOutputFile.writeText(latestVersion.toString());
-
- Logger.i(TAG, "Downloaded APK to '${apkOutputFile.path}'.");
- } else {
- Logger.i(TAG, "Latest APK is already downloaded in '${apkOutputFile.path}'...");
- }
-
- if (!_shouldBackgroundUpdate) {
- Logger.i(TAG, "Update download cancelled 5.");
- return null;
- }
-
- return apkOutputFile;
- } else {
- Logger.w(TAG, "Failed to retrieve version from version URL.");
- return null;
- }
- } catch (e: Throwable) {
- Logger.e(TAG, "Failed to download APK.", e);
- return null;
- } finally {
- _gettingOrDownloadingLastApk = false;
- }
- }
-
- fun setShouldBackgroundUpdate(shouldBackgroundUpdate: Boolean) {
- synchronized (_lockObject) {
- if (_backgroundUpdateFinished) {
- _shouldBackgroundUpdate = false;
- return;
- }
-
- _shouldBackgroundUpdate = shouldBackgroundUpdate;
- if (shouldBackgroundUpdate && !_gettingOrDownloadingLastApk) {
- Logger.i(TAG, "Auto Updating in Background");
-
- _gettingOrDownloadingLastApk = true;
- StateApp.withContext { context ->
- StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
- try {
- val file = getOrDownloadLastApkFile(context.filesDir);
- if (file == null) {
- Logger.i(TAG, "Failed to get or download update.");
- return@launch;
- }
-
- withContext(Dispatchers.Main) {
- try {
- context.let { c ->
- _backgroundUpdateFinished = true;
- UIDialogs.showInstallDownloadedUpdateDialog(c, file);
- };
- Logger.i(TAG, "Showing install dialog for '${file.path}'.");
- } catch (e: Throwable) {
- context.let { c -> UIDialogs.toast(c, "Failed to show update dialog"); };
- Logger.w(TAG, "Error occurred in update dialog.", e);
- }
- }
- } catch (e: Throwable) {
- Logger.e(TAG, "Failed to get last downloaded APK file.", e)
- }
- }
- }
- }
- }
- }
-
suspend fun checkForUpdates(context: Context, showUpToDateToast: Boolean, hideExceptionButtons: Boolean = false) = withContext(Dispatchers.IO) {
try {
val client = ManagedHttpClient();
@@ -196,25 +56,6 @@ class StateUpdate {
}
}
- private fun downloadApkToFile(client: ManagedHttpClient, destinationFile: File, isCancelled: (() -> Boolean)? = null) {
- var apkStream: InputStream? = null;
- var outputStream: OutputStream? = null;
-
- try {
- val response = client.get(APK_URL);
- if (response.isOk && response.body != null) {
- apkStream = response.body.byteStream();
- outputStream = destinationFile.outputStream();
- apkStream.copyToOutputStream(outputStream, isCancelled);
- apkStream.close();
- outputStream.close();
- }
- } finally {
- apkStream?.close();
- outputStream?.close();
- }
- }
-
fun downloadVersionCode(client: ManagedHttpClient): Int? {
val response = client.get(VERSION_URL);
if (!response.isOk || response.body == null) {
@@ -267,6 +108,22 @@ class StateUpdate {
}
val CHANGELOG_BASE_URL = "https://releases.grayjay.app/changelogs";
+ fun getApkFile(context: Context, version: Int): File {
+ val dir = File(context.filesDir, "updates");
+ if (!dir.exists()) {
+ dir.mkdirs();
+ }
+ return File(dir, "app-${DESIRED_ABI}-${version}.apk");
+ }
+
+ fun getPartialApkFile(context: Context, version: Int): File {
+ val dir = File(context.filesDir, "updates");
+ if (!dir.exists()) {
+ dir.mkdirs();
+ }
+ return File(dir, "app-${DESIRED_ABI}-${version}.apk.part");
+ }
+
fun finish() {
_instance?.let {
_instance = null;
diff --git a/app/src/main/java/com/futo/platformplayer/views/adapters/PlaylistsViewHolder.kt b/app/src/main/java/com/futo/platformplayer/views/adapters/PlaylistsViewHolder.kt
index e7fc852a..35d01f13 100644
--- a/app/src/main/java/com/futo/platformplayer/views/adapters/PlaylistsViewHolder.kt
+++ b/app/src/main/java/com/futo/platformplayer/views/adapters/PlaylistsViewHolder.kt
@@ -7,10 +7,12 @@ import android.widget.TextView
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.recyclerview.widget.RecyclerView.ViewHolder
import com.bumptech.glide.Glide
+import com.bumptech.glide.load.resource.bitmap.DownsampleStrategy
import com.futo.platformplayer.R
import com.futo.platformplayer.constructs.Event0
import com.futo.platformplayer.images.GlideHelper.Companion.crossfade
import com.futo.platformplayer.models.Playlist
+import com.futo.platformplayer.withMaxSizePx
class PlaylistsViewHolder : ViewHolder {
private val _root: ConstraintLayout;
@@ -44,6 +46,7 @@ class PlaylistsViewHolder : ViewHolder {
if (p.videos.isNotEmpty()) {
Glide.with(_imageThumbnail)
.load(p.videos[0].thumbnails.getMinimumThumbnail(380))
+ .withMaxSizePx()
.placeholder(R.drawable.placeholder_video_thumbnail)
.crossfade()
.into(_imageThumbnail);
diff --git a/app/src/main/java/com/futo/platformplayer/views/adapters/VideoListEditorViewHolder.kt b/app/src/main/java/com/futo/platformplayer/views/adapters/VideoListEditorViewHolder.kt
index 469ce702..61e840d6 100644
--- a/app/src/main/java/com/futo/platformplayer/views/adapters/VideoListEditorViewHolder.kt
+++ b/app/src/main/java/com/futo/platformplayer/views/adapters/VideoListEditorViewHolder.kt
@@ -12,6 +12,7 @@ import androidx.constraintlayout.widget.ConstraintLayout
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.RecyclerView.ViewHolder
import com.bumptech.glide.Glide
+import com.bumptech.glide.load.resource.bitmap.DownsampleStrategy
import com.futo.platformplayer.R
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
import com.futo.platformplayer.constructs.Event1
@@ -23,6 +24,7 @@ import com.futo.platformplayer.toHumanNumber
import com.futo.platformplayer.toHumanTime
import com.futo.platformplayer.views.others.ProgressBar
import com.futo.platformplayer.views.platform.PlatformIndicator
+import com.futo.platformplayer.withMaxSizePx
class VideoListEditorViewHolder : ViewHolder {
private val _root: ConstraintLayout;
@@ -89,6 +91,7 @@ class VideoListEditorViewHolder : ViewHolder {
fun bind(v: IPlatformVideo, canEdit: Boolean) {
Glide.with(_imageThumbnail)
.load(v.thumbnails.getHQThumbnail())
+ .withMaxSizePx()
.placeholder(R.drawable.placeholder_video_thumbnail)
.crossfade()
.into(_imageThumbnail);
diff --git a/app/src/main/java/com/futo/platformplayer/views/adapters/viewholders/LocalVideoTileViewHolder.kt b/app/src/main/java/com/futo/platformplayer/views/adapters/viewholders/LocalVideoTileViewHolder.kt
index 1adee422..609220f8 100644
--- a/app/src/main/java/com/futo/platformplayer/views/adapters/viewholders/LocalVideoTileViewHolder.kt
+++ b/app/src/main/java/com/futo/platformplayer/views/adapters/viewholders/LocalVideoTileViewHolder.kt
@@ -7,6 +7,7 @@ import android.widget.ImageView
import android.widget.TextView
import androidx.core.view.isVisible
import com.bumptech.glide.Glide
+import com.bumptech.glide.load.resource.bitmap.DownsampleStrategy
import com.futo.platformplayer.R
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
@@ -16,6 +17,7 @@ import com.futo.platformplayer.states.Artist
import com.futo.platformplayer.toHumanNowDiffString
import com.futo.platformplayer.toHumanTime
import com.futo.platformplayer.views.adapters.AnyAdapter
+import com.futo.platformplayer.withMaxSizePx
import com.google.android.material.imageview.ShapeableImageView
@@ -49,6 +51,7 @@ class LocalVideoTileViewHolder(val _viewGroup: ViewGroup) : AnyAdapter.AnyViewHo
Glide.with(it)
.load(content.thumbnails.getHQThumbnail())
.placeholder(R.drawable.unknown_music)
+ .withMaxSizePx()
.into(it)
else
Glide.with(it).load(R.drawable.unknown_music).into(it);
diff --git a/app/src/main/java/com/futo/platformplayer/views/buttons/BigButton.kt b/app/src/main/java/com/futo/platformplayer/views/buttons/BigButton.kt
index f3bbc7b8..26123083 100644
--- a/app/src/main/java/com/futo/platformplayer/views/buttons/BigButton.kt
+++ b/app/src/main/java/com/futo/platformplayer/views/buttons/BigButton.kt
@@ -2,6 +2,7 @@ package com.futo.platformplayer.views.buttons
import android.content.Context
import android.graphics.Bitmap
+import android.os.Looper
import android.util.AttributeSet
import android.util.TypedValue
import android.view.View
@@ -98,46 +99,58 @@ open class BigButton : LinearLayout {
return this;
}
- fun withIcon(resourceId: Int, rounded: Boolean = false): BigButton {
+ private fun applyIcon(resourceId: Int, rounded: Boolean) {
if (resourceId != -1) {
- _icon.visibility = View.VISIBLE;
- _icon.setImageResource(resourceId);
- } else
- _icon.visibility = View.GONE;
-
- if (rounded) {
- val shapeAppearanceModel = ShapeAppearanceModel().toBuilder()
- .setAllCornerSizes(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 16.0f, context.resources.displayMetrics))
- .build();
-
- _icon.scaleType = ImageView.ScaleType.FIT_CENTER;
- _icon.shapeAppearanceModel = shapeAppearanceModel;
+ _icon.visibility = View.VISIBLE
+ _icon.setImageResource(resourceId)
} else {
- _icon.scaleType = ImageView.ScaleType.CENTER_CROP;
- _icon.shapeAppearanceModel = ShapeAppearanceModel();
+ _icon.visibility = View.GONE
}
-
- return this;
+ applyRounded(rounded)
}
+ fun withIcon(resourceId: Int, rounded: Boolean = false): BigButton {
+ if (Looper.myLooper() == Looper.getMainLooper()) {
+ applyIcon(resourceId, rounded)
+ } else {
+ post { applyIcon(resourceId, rounded) }
+ }
+ return this
+ }
fun withIcon(bitmap: Bitmap, rounded: Boolean = false): BigButton {
- _icon.visibility = View.VISIBLE;
- _icon.setImageBitmap(bitmap);
-
- if (rounded) {
- val shapeAppearanceModel = ShapeAppearanceModel().toBuilder()
- .setAllCornerSizes(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 16.0f, context.resources.displayMetrics))
- .build();
-
- _icon.scaleType = ImageView.ScaleType.FIT_CENTER;
- _icon.shapeAppearanceModel = shapeAppearanceModel;
+ if (Looper.myLooper() == Looper.getMainLooper()) {
+ applyIcon(bitmap, rounded)
} else {
- _icon.scaleType = ImageView.ScaleType.CENTER_CROP;
- _icon.shapeAppearanceModel = ShapeAppearanceModel();
+ post { applyIcon(bitmap, rounded) }
}
+ return this
+ }
- return this;
+ private fun applyRounded(rounded: Boolean) {
+ if (rounded) {
+ val radiusPx = TypedValue.applyDimension(
+ TypedValue.COMPLEX_UNIT_DIP,
+ 16.0f,
+ context.resources.displayMetrics
+ )
+ val shapeAppearanceModel = ShapeAppearanceModel()
+ .toBuilder()
+ .setAllCornerSizes(radiusPx)
+ .build()
+
+ _icon.scaleType = ImageView.ScaleType.FIT_CENTER
+ _icon.shapeAppearanceModel = shapeAppearanceModel
+ } else {
+ _icon.scaleType = ImageView.ScaleType.CENTER_CROP
+ _icon.shapeAppearanceModel = ShapeAppearanceModel()
+ }
+ }
+
+ private fun applyIcon(bitmap: Bitmap, rounded: Boolean) {
+ _icon.visibility = View.VISIBLE
+ _icon.setImageBitmap(bitmap)
+ applyRounded(rounded)
}
fun withBackground(resourceId: Int): BigButton {
diff --git a/app/src/main/java/com/futo/platformplayer/views/casting/CastView.kt b/app/src/main/java/com/futo/platformplayer/views/casting/CastView.kt
index fe55714e..7095eca2 100644
--- a/app/src/main/java/com/futo/platformplayer/views/casting/CastView.kt
+++ b/app/src/main/java/com/futo/platformplayer/views/casting/CastView.kt
@@ -18,6 +18,7 @@ import androidx.media3.common.util.UnstableApi
import androidx.media3.ui.DefaultTimeBar
import androidx.media3.ui.TimeBar
import com.bumptech.glide.Glide
+import com.bumptech.glide.load.resource.bitmap.DownsampleStrategy
import com.futo.platformplayer.R
import com.futo.platformplayer.Settings
import com.futo.platformplayer.api.media.models.chapters.IChapter
@@ -32,6 +33,7 @@ import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.states.StatePlayer
import com.futo.platformplayer.views.TargetTapLoaderView
import com.futo.platformplayer.views.behavior.GestureControlView
+import com.futo.platformplayer.withMaxSizePx
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
@@ -306,6 +308,7 @@ class CastView : ConstraintLayout {
Glide.with(_thumbnail)
.load(video.thumbnails.getHQThumbnail())
.placeholder(R.drawable.placeholder_video_thumbnail)
+ .withMaxSizePx()
.into(_thumbnail);
_textPosition.text = (position * 1000).formatDuration();
_textDuration.text = (video.duration * 1000).formatDuration();
diff --git a/app/src/main/java/com/futo/platformplayer/views/items/ActiveDownloadItem.kt b/app/src/main/java/com/futo/platformplayer/views/items/ActiveDownloadItem.kt
index fa393dd9..a45bfdeb 100644
--- a/app/src/main/java/com/futo/platformplayer/views/items/ActiveDownloadItem.kt
+++ b/app/src/main/java/com/futo/platformplayer/views/items/ActiveDownloadItem.kt
@@ -6,6 +6,7 @@ import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.TextView
import com.bumptech.glide.Glide
+import com.bumptech.glide.load.resource.bitmap.DownsampleStrategy
import com.futo.platformplayer.*
import com.futo.platformplayer.downloads.VideoDownload
import com.futo.platformplayer.images.GlideHelper.Companion.crossfade
@@ -61,6 +62,7 @@ class ActiveDownloadItem: LinearLayout {
Glide.with(_videoImage)
.load(download.thumbnail)
+ .withMaxSizePx()
.crossfade()
.into(_videoImage);
diff --git a/app/src/main/java/com/futo/platformplayer/views/items/PlaylistDownloadItem.kt b/app/src/main/java/com/futo/platformplayer/views/items/PlaylistDownloadItem.kt
index 8a5a43f2..a9d2d830 100644
--- a/app/src/main/java/com/futo/platformplayer/views/items/PlaylistDownloadItem.kt
+++ b/app/src/main/java/com/futo/platformplayer/views/items/PlaylistDownloadItem.kt
@@ -5,9 +5,11 @@ import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.TextView
import com.bumptech.glide.Glide
+import com.bumptech.glide.load.resource.bitmap.DownsampleStrategy
import com.futo.platformplayer.R
import com.futo.platformplayer.images.GlideHelper.Companion.crossfade
import com.futo.platformplayer.models.PlaylistDownloaded
+import com.futo.platformplayer.withMaxSizePx
class PlaylistDownloadItem(context: Context, playlistName: String, playlistThumbnail: String?, val obj: Any): LinearLayout(context) {
init { inflate(context, R.layout.list_downloaded_playlist, this) }
@@ -19,6 +21,7 @@ class PlaylistDownloadItem(context: Context, playlistName: String, playlistThumb
imageText.text = playlistName;
Glide.with(imageView)
.load(playlistThumbnail)
+ .withMaxSizePx()
.crossfade()
.into(imageView);
}
diff --git a/app/src/main/java/com/futo/platformplayer/views/video/FutoThumbnailPlayer.kt b/app/src/main/java/com/futo/platformplayer/views/video/FutoThumbnailPlayer.kt
index 80175ab8..3a6457fd 100644
--- a/app/src/main/java/com/futo/platformplayer/views/video/FutoThumbnailPlayer.kt
+++ b/app/src/main/java/com/futo/platformplayer/views/video/FutoThumbnailPlayer.kt
@@ -15,6 +15,7 @@ import androidx.media3.common.util.UnstableApi
import androidx.media3.ui.PlayerControlView
import androidx.media3.ui.PlayerView
import com.bumptech.glide.Glide
+import com.bumptech.glide.load.resource.bitmap.DownsampleStrategy
import com.bumptech.glide.request.target.CustomTarget
import com.bumptech.glide.request.transition.Transition
import com.futo.platformplayer.R
@@ -25,6 +26,7 @@ import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
import com.futo.platformplayer.helpers.VideoHelper
import com.futo.platformplayer.toHumanTime
import com.futo.platformplayer.video.PlayerManager
+import com.futo.platformplayer.withMaxSizePx
class FutoThumbnailPlayer : FutoVideoPlayerBase {
@@ -135,7 +137,7 @@ class FutoThumbnailPlayer : FutoVideoPlayerBase {
if (videoSource == null && audioSource != null) {
val thumbnail = video.thumbnails.getHQThumbnail();
if (!thumbnail.isNullOrBlank()) {
- Glide.with(videoView).asBitmap().load(thumbnail).into(_loadArtwork);
+ Glide.with(videoView).asBitmap().load(thumbnail).withMaxSizePx().into(_loadArtwork);
} else {
Glide.with(videoView).clear(_loadArtwork);
setArtwork(null);
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 52fa0c63..af666ca9 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
@@ -54,6 +54,7 @@ import com.futo.platformplayer.states.StatePlayer
import com.futo.platformplayer.views.TargetTapLoaderView
import com.futo.platformplayer.views.behavior.GestureControlView
import com.futo.platformplayer.views.others.ProgressBar
+import com.futo.platformplayer.withMaxSizePx
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
@@ -928,11 +929,9 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
override fun switchToAudioMode(video: IPlatformVideoDetails?) {
super.switchToAudioMode(video)
- //This causes issues, and is in general confusing, needs improvements
- /*
val thumbnail = video?.thumbnails?.getHQThumbnail()
if (!thumbnail.isNullOrBlank()) {
- Glide.with(context).asBitmap().load(thumbnail)
+ Glide.with(context).asBitmap().load(thumbnail).withMaxSizePx()
.into(object : CustomTarget() {
override fun onResourceReady(
resource: Bitmap,
@@ -946,6 +945,5 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
}
})
}
- */
}
}
\ No newline at end of file
diff --git a/app/src/main/java/com/futo/platformplayer/views/videometa/UpNextView.kt b/app/src/main/java/com/futo/platformplayer/views/videometa/UpNextView.kt
index 26a908c6..0b1ac688 100644
--- a/app/src/main/java/com/futo/platformplayer/views/videometa/UpNextView.kt
+++ b/app/src/main/java/com/futo/platformplayer/views/videometa/UpNextView.kt
@@ -11,11 +11,13 @@ import android.widget.TextView
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.content.ContextCompat
import com.bumptech.glide.Glide
+import com.bumptech.glide.load.resource.bitmap.DownsampleStrategy
import com.futo.platformplayer.states.StatePlayer
import com.futo.platformplayer.R
import com.futo.platformplayer.constructs.Event0
import com.futo.platformplayer.toHumanNowDiffString
import com.futo.platformplayer.toHumanNumber
+import com.futo.platformplayer.withMaxSizePx
class UpNextView : LinearLayout {
private val _layoutContainer: LinearLayout;
@@ -160,6 +162,7 @@ class UpNextView : LinearLayout {
_textChannelName.text = nextItem.author.name;
Glide.with(_imageThumbnail)
.load(nextItem.thumbnails.getHQThumbnail())
+ .withMaxSizePx()
.placeholder(R.drawable.placeholder_video_thumbnail)
.into(_imageThumbnail);
Glide.with(_imageChannelThumbnail)
diff --git a/app/src/main/res/layout/dialog_update.xml b/app/src/main/res/layout/dialog_update.xml
index 9dceccd4..fd42e3f8 100644
--- a/app/src/main/res/layout/dialog_update.xml
+++ b/app/src/main/res/layout/dialog_update.xml
@@ -106,7 +106,7 @@
Subscriptions
Loading
Retry
+ Failed to start system installer. Your device’s ROM is not compatible with automatic updates.
Cancel
Failed to retrieve data, are you connected?
Settings