mirror of
https://gitlab.futo.org/videostreaming/grayjay.git
synced 2026-05-16 04:52:39 +02:00
Implemented feedback for update flow.
This commit is contained in:
@@ -5,50 +5,14 @@ import android.content.Context
|
|||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import androidx.core.app.NotificationManagerCompat
|
import androidx.core.app.NotificationManagerCompat
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import com.futo.platformplayer.activities.MainActivity
|
|
||||||
import com.futo.platformplayer.dialogs.AutoUpdateDialog
|
|
||||||
import com.futo.platformplayer.states.StateApp
|
|
||||||
import java.io.File
|
|
||||||
|
|
||||||
class UpdateActionReceiver : BroadcastReceiver() {
|
class UpdateActionReceiver : BroadcastReceiver() {
|
||||||
override fun onReceive(context: Context, intent: Intent) {
|
override fun onReceive(context: Context, intent: Intent) {
|
||||||
when (intent.action) {
|
when (intent.action) {
|
||||||
UpdateNotificationManager.ACTION_UPDATE_YES -> handleUpdateYes(context, intent)
|
|
||||||
UpdateNotificationManager.ACTION_UPDATE_NO -> handleUpdateNo(context)
|
|
||||||
UpdateNotificationManager.ACTION_UPDATE_NEVER -> handleUpdateNever(context)
|
|
||||||
UpdateNotificationManager.ACTION_DOWNLOAD_CANCEL -> handleDownloadCancel(context, intent)
|
UpdateNotificationManager.ACTION_DOWNLOAD_CANCEL -> handleDownloadCancel(context, intent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleUpdateYes(context: Context, intent: Intent) {
|
|
||||||
AutoUpdateDialog.currentDialog?.dismiss()
|
|
||||||
|
|
||||||
val version = intent.getIntExtra(UpdateNotificationManager.EXTRA_VERSION, 0)
|
|
||||||
if (version == 0) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
NotificationManagerCompat.from(context).cancel(UpdateNotificationManager.NOTIF_ID_AVAILABLE)
|
|
||||||
|
|
||||||
val serviceIntent = Intent(context, UpdateDownloadService::class.java).apply {
|
|
||||||
putExtra(UpdateDownloadService.EXTRA_VERSION, version)
|
|
||||||
}
|
|
||||||
ContextCompat.startForegroundService(context, serviceIntent)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun handleUpdateNo(context: Context) {
|
|
||||||
AutoUpdateDialog.currentDialog?.dismiss()
|
|
||||||
NotificationManagerCompat.from(context).cancel(UpdateNotificationManager.NOTIF_ID_AVAILABLE)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun handleUpdateNever(context: Context) {
|
|
||||||
AutoUpdateDialog.currentDialog?.dismiss()
|
|
||||||
Settings.instance.autoUpdate.check = 1
|
|
||||||
Settings.instance.save()
|
|
||||||
|
|
||||||
UpdateNotificationManager.cancelAll(context)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun handleDownloadCancel(context: Context, intent: Intent) {
|
private fun handleDownloadCancel(context: Context, intent: Intent) {
|
||||||
val version = intent.getIntExtra(UpdateNotificationManager.EXTRA_VERSION, 0)
|
val version = intent.getIntExtra(UpdateNotificationManager.EXTRA_VERSION, 0)
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
package com.futo.platformplayer
|
package com.futo.platformplayer
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.work.CoroutineWorker
|
import androidx.work.CoroutineWorker
|
||||||
import androidx.work.WorkerParameters
|
import androidx.work.WorkerParameters
|
||||||
import com.futo.platformplayer.api.http.ManagedHttpClient
|
import com.futo.platformplayer.api.http.ManagedHttpClient
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.states.StateApp
|
|
||||||
import com.futo.platformplayer.states.StateUpdate
|
import com.futo.platformplayer.states.StateUpdate
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
@@ -35,18 +36,16 @@ class UpdateCheckWorker(appContext: Context, workerParams: WorkerParameters) : C
|
|||||||
return@withContext Result.success()
|
return@withContext Result.success()
|
||||||
}
|
}
|
||||||
|
|
||||||
UpdateNotificationManager.showUpdateAvailableNotification(applicationContext, latestVersion)
|
StateUpdate.Companion.instance.setUiAvailable(latestVersion)
|
||||||
|
|
||||||
if (StateApp.instance.isMainActive) {
|
try {
|
||||||
withContext(Dispatchers.Main) {
|
val serviceIntent = Intent(applicationContext, UpdateDownloadService::class.java).apply {
|
||||||
StateApp.withContext { ctx ->
|
putExtra(UpdateDownloadService.EXTRA_VERSION, latestVersion)
|
||||||
try {
|
|
||||||
UIDialogs.showUpdateAvailableDialog(ctx, latestVersion, false)
|
|
||||||
} catch (t: Throwable) {
|
|
||||||
Logger.w(TAG, "Failed to show in-app update dialog from worker", t)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
ContextCompat.startForegroundService(applicationContext, serviceIntent)
|
||||||
|
} catch (t: Throwable) {
|
||||||
|
Logger.w(TAG, "Failed to start UpdateDownloadService", t)
|
||||||
|
StateUpdate.Companion.instance.setUiFailed(latestVersion, t.message)
|
||||||
}
|
}
|
||||||
|
|
||||||
Result.success()
|
Result.success()
|
||||||
|
|||||||
@@ -1,17 +1,15 @@
|
|||||||
package com.futo.platformplayer
|
package com.futo.platformplayer
|
||||||
|
|
||||||
import android.app.Dialog
|
|
||||||
import android.app.Service
|
import android.app.Service
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
import android.os.SystemClock
|
import android.os.SystemClock
|
||||||
import com.futo.platformplayer.UIDialogs.ActionStyle
|
import androidx.core.app.NotificationManagerCompat
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.models.ImageVariable
|
import com.futo.platformplayer.models.ImageVariable
|
||||||
import com.futo.platformplayer.states.AnnouncementType
|
import com.futo.platformplayer.states.AnnouncementType
|
||||||
import com.futo.platformplayer.states.SessionAnnouncement
|
import com.futo.platformplayer.states.SessionAnnouncement
|
||||||
import com.futo.platformplayer.states.StateAnnouncement
|
import com.futo.platformplayer.states.StateAnnouncement
|
||||||
import com.futo.platformplayer.states.StateApp
|
|
||||||
import com.futo.platformplayer.states.StateUpdate
|
import com.futo.platformplayer.states.StateUpdate
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
import java.io.File
|
import java.io.File
|
||||||
@@ -30,8 +28,6 @@ class UpdateDownloadService : Service() {
|
|||||||
private const val INITIAL_BACKOFF_MS = 5_000L
|
private const val INITIAL_BACKOFF_MS = 5_000L
|
||||||
private const val BUFFER_SIZE = 8 * 1024
|
private const val BUFFER_SIZE = 8 * 1024
|
||||||
private const val MIN_PROGRESS_UPDATE_INTERVAL_MS = 500L
|
private const val MIN_PROGRESS_UPDATE_INTERVAL_MS = 500L
|
||||||
|
|
||||||
var updateDownloadedDialog: Dialog? = null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private val job = SupervisorJob()
|
private val job = SupervisorJob()
|
||||||
@@ -56,6 +52,7 @@ class UpdateDownloadService : Service() {
|
|||||||
if (intent.getBooleanExtra(EXTRA_CANCEL, false)) {
|
if (intent.getBooleanExtra(EXTRA_CANCEL, false)) {
|
||||||
cancelRequested = true
|
cancelRequested = true
|
||||||
Logger.i(TAG, "Download cancel requested")
|
Logger.i(TAG, "Download cancel requested")
|
||||||
|
StateUpdate.Companion.instance.clearUi()
|
||||||
stopForeground(Service.STOP_FOREGROUND_REMOVE)
|
stopForeground(Service.STOP_FOREGROUND_REMOVE)
|
||||||
stopSelf()
|
stopSelf()
|
||||||
return START_NOT_STICKY
|
return START_NOT_STICKY
|
||||||
@@ -75,6 +72,10 @@ class UpdateDownloadService : Service() {
|
|||||||
isDownloading = true
|
isDownloading = true
|
||||||
cancelRequested = false
|
cancelRequested = false
|
||||||
|
|
||||||
|
StateUpdate.Companion.instance.setUiDownloading(version, 0, indeterminate = true)
|
||||||
|
|
||||||
|
NotificationManagerCompat.from(this).cancel(UpdateNotificationManager.NOTIF_ID_READY)
|
||||||
|
|
||||||
val notification = UpdateNotificationManager.buildDownloadProgressNotification(this, version, 0, true)
|
val notification = UpdateNotificationManager.buildDownloadProgressNotification(this, version, 0, true)
|
||||||
startForeground(UpdateNotificationManager.NOTIF_ID_DOWNLOADING, notification)
|
startForeground(UpdateNotificationManager.NOTIF_ID_DOWNLOADING, notification)
|
||||||
|
|
||||||
@@ -97,6 +98,7 @@ class UpdateDownloadService : Service() {
|
|||||||
if (force || now - lastProgressUpdateElapsedMs >= MIN_PROGRESS_UPDATE_INTERVAL_MS) {
|
if (force || now - lastProgressUpdateElapsedMs >= MIN_PROGRESS_UPDATE_INTERVAL_MS) {
|
||||||
lastProgressUpdateElapsedMs = now
|
lastProgressUpdateElapsedMs = now
|
||||||
UpdateNotificationManager.updateDownloadProgress(this, version, progress, indeterminate);
|
UpdateNotificationManager.updateDownloadProgress(this, version, progress, indeterminate);
|
||||||
|
StateUpdate.Companion.instance.setUiDownloading(version, progress, indeterminate)
|
||||||
|
|
||||||
if(onProgress != null)
|
if(onProgress != null)
|
||||||
onProgress.invoke(progress);
|
onProgress.invoke(progress);
|
||||||
@@ -159,6 +161,7 @@ class UpdateDownloadService : Service() {
|
|||||||
if (attempt == MAX_RETRIES - 1) {
|
if (attempt == MAX_RETRIES - 1) {
|
||||||
Logger.e(TAG, "Download failed after ${attempt + 1} attempts", t)
|
Logger.e(TAG, "Download failed after ${attempt + 1} attempts", t)
|
||||||
UpdateNotificationManager.showDownloadFailedNotification(this, version, t)
|
UpdateNotificationManager.showDownloadFailedNotification(this, version, t)
|
||||||
|
StateUpdate.Companion.instance.setUiFailed(version, t.message)
|
||||||
break
|
break
|
||||||
} else {
|
} else {
|
||||||
Logger.w(TAG, "Download attempt ${attempt + 1} failed, retrying in ${backoffMs / 1000}s", t)
|
Logger.w(TAG, "Download attempt ${attempt + 1} failed, retrying in ${backoffMs / 1000}s", t)
|
||||||
@@ -264,39 +267,16 @@ class UpdateDownloadService : Service() {
|
|||||||
private fun onDownloadComplete(version: Int, apkFile: File) {
|
private fun onDownloadComplete(version: Int, apkFile: File) {
|
||||||
Logger.i(TAG, "Download complete for version=$version, file=${apkFile.absolutePath}")
|
Logger.i(TAG, "Download complete for version=$version, file=${apkFile.absolutePath}")
|
||||||
UpdateNotificationManager.showDownloadCompleteNotification(this, version, apkFile)
|
UpdateNotificationManager.showDownloadCompleteNotification(this, version, apkFile)
|
||||||
|
StateUpdate.Companion.instance.setUiReady(version, apkFile)
|
||||||
|
|
||||||
if (StateApp.instance.isMainActive) {
|
try {
|
||||||
StateApp.instance.scopeOrNull?.launch(Dispatchers.Main) {
|
val ctx = applicationContext
|
||||||
StateApp.withContext { ctx ->
|
StateAnnouncement.instance.registerAnnouncement("install-update-apk", "Grayjay v${version} is ready!", "You can now install the new Grayjay version.", AnnouncementType.SESSION, OffsetDateTime.now(), "update", "Install") {
|
||||||
try {
|
UpdateNotificationManager.cancelAll(ctx)
|
||||||
updateDownloadedDialog = UIDialogs.showDialog(ctx, R.drawable.foreground,
|
UpdateInstaller.startInstall(ctx, version, apkFile)
|
||||||
"Update downloaded",
|
|
||||||
"Would you like to install it now?", null, 0,
|
|
||||||
UIDialogs.Action("Not now", {
|
|
||||||
updateDownloadedDialog = null
|
|
||||||
}, ActionStyle.NONE, true),
|
|
||||||
UIDialogs.Action("Install", {
|
|
||||||
UpdateNotificationManager.cancelAll(ctx)
|
|
||||||
UpdateInstaller.startInstall(ctx, version, apkFile)
|
|
||||||
}, ActionStyle.PRIMARY, true));
|
|
||||||
|
|
||||||
try {
|
|
||||||
StateAnnouncement.instance.registerAnnouncement("install-update-apk", "Grayjay v${version} is ready!", "You can now install the new Grayjay version.",
|
|
||||||
AnnouncementType.SESSION,
|
|
||||||
OffsetDateTime.now(), "update", "Install", {
|
|
||||||
UpdateNotificationManager.cancelAll(ctx)
|
|
||||||
UpdateInstaller.startInstall(ctx, version, apkFile)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
catch(ex: Throwable) {
|
|
||||||
|
|
||||||
}
|
|
||||||
} catch (t: Throwable) {
|
|
||||||
Logger.w(TAG, "Failed to show in-app update downloaded dialog", t)
|
|
||||||
updateDownloadedDialog = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
} catch (ex: Throwable) {
|
||||||
|
Logger.w(TAG, "Failed to register install announcement", ex)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import java.io.InputStream
|
|||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import com.futo.platformplayer.dialogs.AutoUpdateDialog
|
import com.futo.platformplayer.dialogs.AutoUpdateDialog
|
||||||
import com.futo.platformplayer.states.StateApp
|
import com.futo.platformplayer.states.StateApp
|
||||||
|
import com.futo.platformplayer.states.StateUpdate
|
||||||
|
|
||||||
object UpdateInstaller {
|
object UpdateInstaller {
|
||||||
private const val TAG = "UpdateInstaller"
|
private const val TAG = "UpdateInstaller"
|
||||||
@@ -61,6 +62,17 @@ object UpdateInstaller {
|
|||||||
var inputStream: InputStream? = null
|
var inputStream: InputStream? = null
|
||||||
var session: PackageInstaller.Session? = null
|
var session: PackageInstaller.Session? = null
|
||||||
try {
|
try {
|
||||||
|
val dataLength = apkFile.length()
|
||||||
|
val usable = try { context.filesDir.usableSpace } catch (_: Throwable) { -1L }
|
||||||
|
if (usable in 0 until dataLength) {
|
||||||
|
val msg = "Not enough storage to install update. Need ${dataLength / 1_048_576L}MB, have ${usable / 1_048_576L}MB free."
|
||||||
|
Logger.w(TAG, msg)
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
UIDialogs.toast(context, msg)
|
||||||
|
}
|
||||||
|
UpdateNotificationManager.showInstallFailedNotification(context, version, apkFile, msg)
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
|
||||||
val packageInstaller: PackageInstaller = context.packageManager.packageInstaller
|
val packageInstaller: PackageInstaller = context.packageManager.packageInstaller
|
||||||
val params = PackageInstaller.SessionParams(PackageInstaller.SessionParams.MODE_FULL_INSTALL)
|
val params = PackageInstaller.SessionParams(PackageInstaller.SessionParams.MODE_FULL_INSTALL)
|
||||||
@@ -68,7 +80,6 @@ object UpdateInstaller {
|
|||||||
session = packageInstaller.openSession(sessionId)
|
session = packageInstaller.openSession(sessionId)
|
||||||
|
|
||||||
inputStream = apkFile.inputStream()
|
inputStream = apkFile.inputStream()
|
||||||
val dataLength = apkFile.length()
|
|
||||||
|
|
||||||
session.openWrite("package", 0, dataLength).use { sessionStream ->
|
session.openWrite("package", 0, dataLength).use { sessionStream ->
|
||||||
inputStream.copyToOutputStream(dataLength, sessionStream) { _ -> }
|
inputStream.copyToOutputStream(dataLength, sessionStream) { _ -> }
|
||||||
@@ -91,11 +102,18 @@ object UpdateInstaller {
|
|||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
Logger.w(TAG, "Exception while installing update", e)
|
Logger.w(TAG, "Exception while installing update", e)
|
||||||
session?.abandon()
|
session?.abandon()
|
||||||
|
|
||||||
|
val raw = e.message ?: ""
|
||||||
|
val friendly = if (raw.contains("Failed to allocate") || raw.contains("allocatable") || raw.contains("ENOSPC", ignoreCase = true)) {
|
||||||
|
"Not enough storage to install update. Free up some space and try again."
|
||||||
|
} else {
|
||||||
|
"Failed to install update: $raw"
|
||||||
|
}
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
UIDialogs.toast(context, "Failed to install update: ${e.message}")
|
UIDialogs.toast(context, friendly)
|
||||||
}
|
}
|
||||||
|
|
||||||
UpdateNotificationManager.showInstallFailedNotification(context, version, apkFile, e.message)
|
UpdateNotificationManager.showInstallFailedNotification(context, version, apkFile, friendly)
|
||||||
} finally {
|
} finally {
|
||||||
session?.close()
|
session?.close()
|
||||||
inputStream?.close()
|
inputStream?.close()
|
||||||
@@ -110,10 +128,12 @@ object UpdateInstaller {
|
|||||||
if (result.isNullOrEmpty()) {
|
if (result.isNullOrEmpty()) {
|
||||||
Logger.i(TAG, "Update install finished successfully")
|
Logger.i(TAG, "Update install finished successfully")
|
||||||
UpdateNotificationManager.showInstallSucceededNotification(context, version)
|
UpdateNotificationManager.showInstallSucceededNotification(context, version)
|
||||||
|
StateUpdate.instance.clearUi()
|
||||||
} else {
|
} else {
|
||||||
Logger.w(TAG, "Update install failed: $result")
|
Logger.w(TAG, "Update install failed: $result")
|
||||||
UpdateNotificationManager.showInstallFailedNotification(context, version, apkFile, result)
|
UpdateNotificationManager.showInstallFailedNotification(context, version, apkFile, result)
|
||||||
UIDialogs.showGeneralErrorDialog(context, "Install failed due to:\n$result")
|
UIDialogs.showGeneralErrorDialog(context, "Install failed due to:\n$result")
|
||||||
|
StateUpdate.instance.setUiReady(version, apkFile)
|
||||||
}
|
}
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
Logger.e(TAG, "Failed to handle install result", e)
|
Logger.e(TAG, "Failed to handle install result", e)
|
||||||
|
|||||||
@@ -22,9 +22,6 @@ object UpdateNotificationManager {
|
|||||||
private const val CHANNEL_NAME = "App updates"
|
private const val CHANNEL_NAME = "App updates"
|
||||||
private const val CHANNEL_DESCRIPTION = "Notifications about new app versions"
|
private const val CHANNEL_DESCRIPTION = "Notifications about new app versions"
|
||||||
|
|
||||||
const val ACTION_UPDATE_YES = "com.futo.platformplayer.UPDATE_YES"
|
|
||||||
const val ACTION_UPDATE_NO = "com.futo.platformplayer.UPDATE_NO"
|
|
||||||
const val ACTION_UPDATE_NEVER = "com.futo.platformplayer.UPDATE_NEVER"
|
|
||||||
const val ACTION_DOWNLOAD_CANCEL = "com.futo.platformplayer.UPDATE_CANCEL"
|
const val ACTION_DOWNLOAD_CANCEL = "com.futo.platformplayer.UPDATE_CANCEL"
|
||||||
const val ACTION_INSTALL_NOW = "com.futo.platformplayer.UPDATE_INSTALL"
|
const val ACTION_INSTALL_NOW = "com.futo.platformplayer.UPDATE_INSTALL"
|
||||||
private const val REQUEST_CODE_INSTALL = 1001
|
private const val REQUEST_CODE_INSTALL = 1001
|
||||||
@@ -32,7 +29,6 @@ object UpdateNotificationManager {
|
|||||||
const val EXTRA_VERSION = "version"
|
const val EXTRA_VERSION = "version"
|
||||||
const val EXTRA_APK_PATH = "apk_path"
|
const val EXTRA_APK_PATH = "apk_path"
|
||||||
|
|
||||||
const val NOTIF_ID_AVAILABLE = 2001
|
|
||||||
const val NOTIF_ID_DOWNLOADING = 2002
|
const val NOTIF_ID_DOWNLOADING = 2002
|
||||||
const val NOTIF_ID_READY = 2003
|
const val NOTIF_ID_READY = 2003
|
||||||
const val NOTIF_ID_INSTALL_FAILED = 2004
|
const val NOTIF_ID_INSTALL_FAILED = 2004
|
||||||
@@ -84,43 +80,6 @@ object UpdateNotificationManager {
|
|||||||
NotificationManagerCompat.from(context).notify(NOTIF_ID_INSTALL_SUCCEEDED, builder.build())
|
NotificationManagerCompat.from(context).notify(NOTIF_ID_INSTALL_SUCCEEDED, builder.build())
|
||||||
}
|
}
|
||||||
|
|
||||||
fun showUpdateAvailableNotification(context: Context, version: Int) {
|
|
||||||
if (ContextCompat.checkSelfPermission(context, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ensureChannel(context)
|
|
||||||
|
|
||||||
val yesIntent = Intent(context, UpdateActionReceiver::class.java).apply {
|
|
||||||
action = ACTION_UPDATE_YES
|
|
||||||
putExtra(EXTRA_VERSION, version)
|
|
||||||
}
|
|
||||||
val yesPendingIntent = getBroadcast(context, 0, yesIntent, FLAG_MUTABLE or FLAG_UPDATE_CURRENT)
|
|
||||||
val noIntent = Intent(context, UpdateActionReceiver::class.java).apply {
|
|
||||||
action = ACTION_UPDATE_NO
|
|
||||||
putExtra(EXTRA_VERSION, version)
|
|
||||||
}
|
|
||||||
val noPendingIntent = getBroadcast(context, 1, noIntent, FLAG_MUTABLE or FLAG_UPDATE_CURRENT)
|
|
||||||
val neverIntent = Intent(context, UpdateActionReceiver::class.java).apply {
|
|
||||||
action = ACTION_UPDATE_NEVER
|
|
||||||
putExtra(EXTRA_VERSION, version)
|
|
||||||
}
|
|
||||||
val neverPendingIntent = getBroadcast(context, 2, neverIntent, FLAG_MUTABLE or FLAG_UPDATE_CURRENT)
|
|
||||||
val builder = NotificationCompat.Builder(context, CHANNEL_ID)
|
|
||||||
.setSmallIcon(R.drawable.foreground)
|
|
||||||
.setContentTitle("Update available")
|
|
||||||
.setContentText("A new version ($version) is available.")
|
|
||||||
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
|
|
||||||
.setAutoCancel(true)
|
|
||||||
.setContentIntent(yesPendingIntent)
|
|
||||||
.setSilent(true)
|
|
||||||
.addAction(0, "Never", neverPendingIntent)
|
|
||||||
.addAction(0, "Not now", noPendingIntent)
|
|
||||||
.addAction(0, "Download", yesPendingIntent)
|
|
||||||
|
|
||||||
NotificationManagerCompat.from(context).notify(NOTIF_ID_AVAILABLE, builder.build())
|
|
||||||
}
|
|
||||||
|
|
||||||
fun buildDownloadProgressNotification(context: Context, version: Int, progress: Int, indeterminate: Boolean): Notification {
|
fun buildDownloadProgressNotification(context: Context, version: Int, progress: Int, indeterminate: Boolean): Notification {
|
||||||
ensureChannel(context)
|
ensureChannel(context)
|
||||||
|
|
||||||
@@ -223,11 +182,9 @@ object UpdateNotificationManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun cancelAll(context: Context) {
|
fun cancelAll(context: Context) {
|
||||||
NotificationManagerCompat.from(context).cancel(NOTIF_ID_AVAILABLE)
|
|
||||||
NotificationManagerCompat.from(context).cancel(NOTIF_ID_DOWNLOADING)
|
NotificationManagerCompat.from(context).cancel(NOTIF_ID_DOWNLOADING)
|
||||||
NotificationManagerCompat.from(context).cancel(NOTIF_ID_READY)
|
NotificationManagerCompat.from(context).cancel(NOTIF_ID_READY)
|
||||||
NotificationManagerCompat.from(context).cancel(NOTIF_ID_INSTALL_FAILED)
|
NotificationManagerCompat.from(context).cancel(NOTIF_ID_INSTALL_FAILED)
|
||||||
NotificationManagerCompat.from(context).cancel(NOTIF_ID_INSTALL_SUCCEEDED)
|
NotificationManagerCompat.from(context).cancel(NOTIF_ID_INSTALL_SUCCEEDED)
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -71,16 +71,14 @@ class Event0() : EventBase<(()->Unit), (()->Boolean)>() {
|
|||||||
fun emit() : Boolean {
|
fun emit() : Boolean {
|
||||||
var handled = false;
|
var handled = false;
|
||||||
|
|
||||||
synchronized(_conditionalListeners) {
|
val condSnapshot = synchronized(_conditionalListeners) { _conditionalListeners.toList() };
|
||||||
for (conditional in _conditionalListeners)
|
for (conditional in condSnapshot)
|
||||||
handled = handled || conditional.handler.invoke();
|
handled = handled || conditional.handler.invoke();
|
||||||
}
|
|
||||||
|
|
||||||
synchronized(_listeners) {
|
val snapshot = synchronized(_listeners) { _listeners.toList() };
|
||||||
handled = handled || _listeners.isNotEmpty();
|
handled = handled || snapshot.isNotEmpty();
|
||||||
for (handler in _listeners)
|
for (handler in snapshot)
|
||||||
handler.handler.invoke();
|
handler.handler.invoke();
|
||||||
}
|
|
||||||
|
|
||||||
return handled;
|
return handled;
|
||||||
}
|
}
|
||||||
@@ -88,16 +86,15 @@ class Event0() : EventBase<(()->Unit), (()->Boolean)>() {
|
|||||||
class Event1<T1>() : EventBase<((T1)->Unit), ((T1)->Boolean)>() {
|
class Event1<T1>() : EventBase<((T1)->Unit), ((T1)->Boolean)>() {
|
||||||
fun emit(value : T1): Boolean {
|
fun emit(value : T1): Boolean {
|
||||||
var handled = false;
|
var handled = false;
|
||||||
synchronized(_conditionalListeners) {
|
|
||||||
for (conditional in _conditionalListeners)
|
|
||||||
handled = handled || conditional.handler.invoke(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
synchronized(_listeners) {
|
val condSnapshot = synchronized(_conditionalListeners) { _conditionalListeners.toList() };
|
||||||
handled = handled || _listeners.isNotEmpty();
|
for (conditional in condSnapshot)
|
||||||
for (handler in _listeners)
|
handled = handled || conditional.handler.invoke(value);
|
||||||
handler.handler.invoke(value);
|
|
||||||
}
|
val snapshot = synchronized(_listeners) { _listeners.toList() };
|
||||||
|
handled = handled || snapshot.isNotEmpty();
|
||||||
|
for (handler in snapshot)
|
||||||
|
handler.handler.invoke(value);
|
||||||
|
|
||||||
return handled;
|
return handled;
|
||||||
}
|
}
|
||||||
@@ -106,16 +103,14 @@ class Event2<T1, T2>() : EventBase<((T1, T2)->Unit), ((T1, T2)->Boolean)>() {
|
|||||||
fun emit(value1 : T1, value2 : T2): Boolean {
|
fun emit(value1 : T1, value2 : T2): Boolean {
|
||||||
var handled = false;
|
var handled = false;
|
||||||
|
|
||||||
synchronized(_conditionalListeners) {
|
val condSnapshot = synchronized(_conditionalListeners) { _conditionalListeners.toList() };
|
||||||
for (conditional in _conditionalListeners)
|
for (conditional in condSnapshot)
|
||||||
handled = handled || conditional.handler.invoke(value1, value2);
|
handled = handled || conditional.handler.invoke(value1, value2);
|
||||||
}
|
|
||||||
|
|
||||||
synchronized(_listeners) {
|
val snapshot = synchronized(_listeners) { _listeners.toList() };
|
||||||
handled = handled || _listeners.isNotEmpty();
|
handled = handled || snapshot.isNotEmpty();
|
||||||
for (handler in _listeners)
|
for (handler in snapshot)
|
||||||
handler.handler.invoke(value1, value2);
|
handler.handler.invoke(value1, value2);
|
||||||
}
|
|
||||||
|
|
||||||
return handled;
|
return handled;
|
||||||
}
|
}
|
||||||
@@ -125,16 +120,14 @@ class Event3<T1, T2, T3>() : EventBase<((T1, T2, T3)->Unit), ((T1, T2, T3)->Bool
|
|||||||
fun emit(value1 : T1, value2 : T2, value3 : T3): Boolean {
|
fun emit(value1 : T1, value2 : T2, value3 : T3): Boolean {
|
||||||
var handled = false;
|
var handled = false;
|
||||||
|
|
||||||
synchronized(_conditionalListeners) {
|
val condSnapshot = synchronized(_conditionalListeners) { _conditionalListeners.toList() };
|
||||||
for (conditional in _conditionalListeners)
|
for (conditional in condSnapshot)
|
||||||
handled = handled || conditional.handler.invoke(value1, value2, value3);
|
handled = handled || conditional.handler.invoke(value1, value2, value3);
|
||||||
}
|
|
||||||
|
|
||||||
synchronized(_listeners) {
|
val snapshot = synchronized(_listeners) { _listeners.toList() };
|
||||||
handled = handled || _listeners.isNotEmpty();
|
handled = handled || snapshot.isNotEmpty();
|
||||||
for (handler in _listeners)
|
for (handler in snapshot)
|
||||||
handler.handler.invoke(value1, value2, value3);
|
handler.handler.invoke(value1, value2, value3);
|
||||||
}
|
|
||||||
|
|
||||||
return handled;
|
return handled;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -668,6 +668,9 @@ class StateApp {
|
|||||||
if (Settings.instance.autoUpdate.isAutoUpdateEnabled()) {
|
if (Settings.instance.autoUpdate.isAutoUpdateEnabled()) {
|
||||||
if (Settings.instance.autoUpdate.shouldBackgroundDownload) {
|
if (Settings.instance.autoUpdate.shouldBackgroundDownload) {
|
||||||
Logger.i(TAG, "MainApp Started: Initialize [AutoUpdate Background]");
|
Logger.i(TAG, "MainApp Started: Initialize [AutoUpdate Background]");
|
||||||
|
scopeOrNull?.launch(Dispatchers.IO) {
|
||||||
|
StateUpdate.instance.seedUiFromDisk(context)
|
||||||
|
}
|
||||||
val constraints = Constraints.Builder()
|
val constraints = Constraints.Builder()
|
||||||
.setRequiredNetworkType(NetworkType.CONNECTED)
|
.setRequiredNetworkType(NetworkType.CONNECTED)
|
||||||
.build();
|
.build();
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import android.os.Build
|
|||||||
import com.futo.platformplayer.BuildConfig
|
import com.futo.platformplayer.BuildConfig
|
||||||
import com.futo.platformplayer.UIDialogs
|
import com.futo.platformplayer.UIDialogs
|
||||||
import com.futo.platformplayer.api.http.ManagedHttpClient
|
import com.futo.platformplayer.api.http.ManagedHttpClient
|
||||||
|
import com.futo.platformplayer.constructs.Event0
|
||||||
import com.futo.platformplayer.copyToOutputStream
|
import com.futo.platformplayer.copyToOutputStream
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@@ -14,7 +15,113 @@ import java.io.File
|
|||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import java.io.OutputStream
|
import java.io.OutputStream
|
||||||
|
|
||||||
|
enum class UpdateUiState { NONE, AVAILABLE, DOWNLOADING, READY, FAILED }
|
||||||
|
|
||||||
class StateUpdate {
|
class StateUpdate {
|
||||||
|
|
||||||
|
@Volatile var uiState: UpdateUiState = UpdateUiState.NONE
|
||||||
|
private set
|
||||||
|
@Volatile var uiVersion: Int = 0
|
||||||
|
private set
|
||||||
|
@Volatile var uiProgress: Int = 0
|
||||||
|
private set
|
||||||
|
@Volatile var uiIndeterminate: Boolean = true
|
||||||
|
private set
|
||||||
|
@Volatile var uiApkFile: File? = null
|
||||||
|
private set
|
||||||
|
@Volatile var uiError: String? = null
|
||||||
|
private set
|
||||||
|
@Volatile var uiDismissed: Boolean = false
|
||||||
|
private set
|
||||||
|
|
||||||
|
val onUiChanged = Event0()
|
||||||
|
|
||||||
|
fun setUiAvailable(version: Int) {
|
||||||
|
val transitioned = uiState != UpdateUiState.AVAILABLE
|
||||||
|
uiState = UpdateUiState.AVAILABLE
|
||||||
|
uiVersion = version
|
||||||
|
uiError = null
|
||||||
|
if (transitioned) uiDismissed = false
|
||||||
|
onUiChanged.emit()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setUiDownloading(version: Int, progress: Int, indeterminate: Boolean) {
|
||||||
|
val transitioned = uiState != UpdateUiState.DOWNLOADING
|
||||||
|
uiState = UpdateUiState.DOWNLOADING
|
||||||
|
uiVersion = version
|
||||||
|
uiProgress = progress
|
||||||
|
uiIndeterminate = indeterminate
|
||||||
|
uiError = null
|
||||||
|
if (transitioned) uiDismissed = false
|
||||||
|
onUiChanged.emit()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setUiReady(version: Int, apkFile: File) {
|
||||||
|
val transitioned = uiState != UpdateUiState.READY
|
||||||
|
uiState = UpdateUiState.READY
|
||||||
|
uiVersion = version
|
||||||
|
uiApkFile = apkFile
|
||||||
|
uiError = null
|
||||||
|
if (transitioned) uiDismissed = false
|
||||||
|
onUiChanged.emit()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setUiFailed(version: Int, error: String?) {
|
||||||
|
val transitioned = uiState != UpdateUiState.FAILED
|
||||||
|
uiState = UpdateUiState.FAILED
|
||||||
|
uiVersion = version
|
||||||
|
uiError = error
|
||||||
|
if (transitioned) uiDismissed = false
|
||||||
|
onUiChanged.emit()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun clearUi() {
|
||||||
|
uiState = UpdateUiState.NONE
|
||||||
|
uiVersion = 0
|
||||||
|
uiProgress = 0
|
||||||
|
uiIndeterminate = true
|
||||||
|
uiApkFile = null
|
||||||
|
uiError = null
|
||||||
|
uiDismissed = false
|
||||||
|
onUiChanged.emit()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun dismissUi() {
|
||||||
|
uiDismissed = true
|
||||||
|
onUiChanged.emit()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun seedUiFromDisk(context: Context) {
|
||||||
|
if (uiState != UpdateUiState.NONE) return
|
||||||
|
try {
|
||||||
|
val dir = File(context.filesDir, "updates")
|
||||||
|
if (!dir.exists()) return
|
||||||
|
val abi = try { DESIRED_ABI } catch (t: Throwable) { return }
|
||||||
|
val prefix = "app-$abi-"
|
||||||
|
val suffix = ".apk"
|
||||||
|
val candidates = dir.listFiles { f ->
|
||||||
|
f.isFile && f.name.startsWith(prefix) && f.name.endsWith(suffix)
|
||||||
|
} ?: return
|
||||||
|
var bestVersion = BuildConfig.VERSION_CODE
|
||||||
|
var bestFile: File? = null
|
||||||
|
for (f in candidates) {
|
||||||
|
val versionStr = f.name.removePrefix(prefix).removeSuffix(suffix)
|
||||||
|
val v = versionStr.toIntOrNull() ?: continue
|
||||||
|
if (v > bestVersion && f.length() > 0L) {
|
||||||
|
bestVersion = v
|
||||||
|
bestFile = f
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val ready = bestFile
|
||||||
|
if (ready != null) {
|
||||||
|
Logger.i(TAG, "Seeding UI ready from disk: v=$bestVersion file=${ready.absolutePath}")
|
||||||
|
setUiReady(bestVersion, ready)
|
||||||
|
}
|
||||||
|
} catch (t: Throwable) {
|
||||||
|
Logger.w(TAG, "Failed to seed UI from disk", t)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun checkForUpdates(context: Context, showUpToDateToast: Boolean, hideExceptionButtons: Boolean = false) = withContext(Dispatchers.IO) {
|
suspend fun checkForUpdates(context: Context, showUpToDateToast: Boolean, hideExceptionButtons: Boolean = false) = withContext(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
val client = ManagedHttpClient();
|
val client = ManagedHttpClient();
|
||||||
|
|||||||
@@ -0,0 +1,186 @@
|
|||||||
|
package com.futo.platformplayer.views.announcements
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.util.AttributeSet
|
||||||
|
import android.view.View
|
||||||
|
import android.widget.FrameLayout
|
||||||
|
import android.widget.ImageView
|
||||||
|
import android.widget.LinearLayout
|
||||||
|
import android.widget.ProgressBar
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.lifecycle.findViewTreeLifecycleOwner
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import com.futo.platformplayer.R
|
||||||
|
import com.futo.platformplayer.Settings
|
||||||
|
import com.futo.platformplayer.UpdateDownloadService
|
||||||
|
import com.futo.platformplayer.UpdateInstaller
|
||||||
|
import com.futo.platformplayer.UpdateNotificationManager
|
||||||
|
import com.futo.platformplayer.logging.Logger
|
||||||
|
import com.futo.platformplayer.states.StateApp
|
||||||
|
import com.futo.platformplayer.states.StateUpdate
|
||||||
|
import com.futo.platformplayer.states.UpdateUiState
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
class UpdateBannerView : LinearLayout {
|
||||||
|
private val _root: FrameLayout
|
||||||
|
private val _iconUpdate: ImageView
|
||||||
|
private val _textTitle: TextView
|
||||||
|
private val _textBody: TextView
|
||||||
|
private val _progressBar: ProgressBar
|
||||||
|
private val _buttonAction: FrameLayout
|
||||||
|
private val _textAction: TextView
|
||||||
|
private val _buttonClose: ImageView
|
||||||
|
|
||||||
|
private val _scope: CoroutineScope?
|
||||||
|
|
||||||
|
constructor(context: Context, attrs: AttributeSet? = null) : super(context, attrs) {
|
||||||
|
inflate(context, R.layout.view_update_banner, this)
|
||||||
|
|
||||||
|
_scope = findViewTreeLifecycleOwner()?.lifecycleScope ?: StateApp.instance.scopeOrNull
|
||||||
|
|
||||||
|
_root = findViewById(R.id.root)
|
||||||
|
_iconUpdate = findViewById(R.id.icon_update)
|
||||||
|
_textTitle = findViewById(R.id.text_title)
|
||||||
|
_textBody = findViewById(R.id.text_body)
|
||||||
|
_progressBar = findViewById(R.id.update_banner_progress)
|
||||||
|
_buttonAction = findViewById(R.id.button_action)
|
||||||
|
_textAction = findViewById(R.id.text_action)
|
||||||
|
_buttonClose = findViewById(R.id.button_close)
|
||||||
|
|
||||||
|
_buttonClose.setOnClickListener {
|
||||||
|
StateUpdate.instance.dismissUi()
|
||||||
|
}
|
||||||
|
|
||||||
|
_buttonAction.setOnClickListener {
|
||||||
|
onActionClicked()
|
||||||
|
}
|
||||||
|
|
||||||
|
refresh()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAttachedToWindow() {
|
||||||
|
super.onAttachedToWindow()
|
||||||
|
StateUpdate.instance.onUiChanged.subscribe(this) {
|
||||||
|
_scope?.launch(Dispatchers.Main) {
|
||||||
|
refresh()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
refresh()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDetachedFromWindow() {
|
||||||
|
super.onDetachedFromWindow()
|
||||||
|
StateUpdate.instance.onUiChanged.remove(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onActionClicked() {
|
||||||
|
val st = StateUpdate.instance
|
||||||
|
when (st.uiState) {
|
||||||
|
UpdateUiState.READY -> {
|
||||||
|
val apk = st.uiApkFile ?: return
|
||||||
|
UpdateNotificationManager.cancelAll(context)
|
||||||
|
UpdateInstaller.startInstall(context, st.uiVersion, apk)
|
||||||
|
}
|
||||||
|
UpdateUiState.FAILED -> {
|
||||||
|
if (st.uiVersion == 0) return
|
||||||
|
val intent = Intent(context, UpdateDownloadService::class.java).apply {
|
||||||
|
putExtra(UpdateDownloadService.EXTRA_VERSION, st.uiVersion)
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
ContextCompat.startForegroundService(context, intent)
|
||||||
|
} catch (t: Throwable) {
|
||||||
|
Logger.w(TAG, "Retry start service failed", t)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
UpdateUiState.DOWNLOADING -> {
|
||||||
|
val intent = Intent(context, UpdateDownloadService::class.java).apply {
|
||||||
|
putExtra(UpdateDownloadService.EXTRA_VERSION, st.uiVersion)
|
||||||
|
putExtra(UpdateDownloadService.EXTRA_CANCEL, true)
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
ContextCompat.startForegroundService(context, intent)
|
||||||
|
} catch (t: Throwable) {
|
||||||
|
Logger.w(TAG, "Cancel start service failed", t)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
UpdateUiState.AVAILABLE -> {
|
||||||
|
if (st.uiVersion == 0) return
|
||||||
|
val intent = Intent(context, UpdateDownloadService::class.java).apply {
|
||||||
|
putExtra(UpdateDownloadService.EXTRA_VERSION, st.uiVersion)
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
ContextCompat.startForegroundService(context, intent)
|
||||||
|
} catch (t: Throwable) {
|
||||||
|
Logger.w(TAG, "Download start service failed", t)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
UpdateUiState.NONE -> {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun refresh() {
|
||||||
|
val st = StateUpdate.instance
|
||||||
|
val gateOpen = Settings.instance.autoUpdate.shouldBackgroundDownload
|
||||||
|
val visible = gateOpen && !st.uiDismissed && st.uiState != UpdateUiState.NONE
|
||||||
|
|
||||||
|
if (!visible) {
|
||||||
|
_root.visibility = View.GONE
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_root.visibility = View.VISIBLE
|
||||||
|
|
||||||
|
when (st.uiState) {
|
||||||
|
UpdateUiState.AVAILABLE -> {
|
||||||
|
_textTitle.text = "Update available (v${st.uiVersion})"
|
||||||
|
_textBody.text = "A new Grayjay version is available."
|
||||||
|
_textBody.visibility = View.VISIBLE
|
||||||
|
_progressBar.visibility = View.GONE
|
||||||
|
_textAction.text = "Download"
|
||||||
|
_buttonAction.visibility = View.VISIBLE
|
||||||
|
}
|
||||||
|
UpdateUiState.DOWNLOADING -> {
|
||||||
|
_textTitle.text = "Downloading update (v${st.uiVersion})"
|
||||||
|
if (st.uiIndeterminate) {
|
||||||
|
_textBody.text = "Starting download…"
|
||||||
|
_progressBar.isIndeterminate = true
|
||||||
|
} else {
|
||||||
|
_textBody.text = "${st.uiProgress}% downloaded"
|
||||||
|
_progressBar.isIndeterminate = false
|
||||||
|
_progressBar.progress = st.uiProgress
|
||||||
|
}
|
||||||
|
_textBody.visibility = View.VISIBLE
|
||||||
|
_progressBar.visibility = View.VISIBLE
|
||||||
|
_textAction.text = "Cancel"
|
||||||
|
_buttonAction.visibility = View.VISIBLE
|
||||||
|
}
|
||||||
|
UpdateUiState.READY -> {
|
||||||
|
_textTitle.text = "Update v${st.uiVersion} ready"
|
||||||
|
_textBody.text = "Tap install to apply the update."
|
||||||
|
_textBody.visibility = View.VISIBLE
|
||||||
|
_progressBar.visibility = View.GONE
|
||||||
|
_textAction.text = "Install"
|
||||||
|
_buttonAction.visibility = View.VISIBLE
|
||||||
|
}
|
||||||
|
UpdateUiState.FAILED -> {
|
||||||
|
_textTitle.text = "Update failed"
|
||||||
|
val err = st.uiError
|
||||||
|
_textBody.text = if (err.isNullOrBlank()) "Could not download v${st.uiVersion}." else err
|
||||||
|
_textBody.visibility = View.VISIBLE
|
||||||
|
_progressBar.visibility = View.GONE
|
||||||
|
_textAction.text = "Retry"
|
||||||
|
_buttonAction.visibility = View.VISIBLE
|
||||||
|
}
|
||||||
|
UpdateUiState.NONE -> {
|
||||||
|
_root.visibility = View.GONE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val TAG = "UpdateBannerView"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -20,11 +20,20 @@
|
|||||||
app:layout_constraintRight_toRightOf="parent"
|
app:layout_constraintRight_toRightOf="parent"
|
||||||
tools:layout="@layout/fragment_overview_top_bar" />
|
tools:layout="@layout/fragment_overview_top_bar" />
|
||||||
|
|
||||||
|
<com.futo.platformplayer.views.announcements.UpdateBannerView
|
||||||
|
android:id="@+id/update_banner"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/fragment_top_bar"
|
||||||
|
app:layout_constraintLeft_toLeftOf="parent"
|
||||||
|
app:layout_constraintRight_toRightOf="parent" />
|
||||||
|
|
||||||
<androidx.fragment.app.FragmentContainerView
|
<androidx.fragment.app.FragmentContainerView
|
||||||
android:id="@+id/fragment_main"
|
android:id="@+id/fragment_main"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="0dp"
|
android:layout_height="0dp"
|
||||||
app:layout_constraintTop_toBottomOf="@id/fragment_top_bar"
|
app:layout_constraintTop_toBottomOf="@id/update_banner"
|
||||||
app:layout_constraintLeft_toLeftOf="parent"
|
app:layout_constraintLeft_toLeftOf="parent"
|
||||||
app:layout_constraintRight_toRightOf="parent"
|
app:layout_constraintRight_toRightOf="parent"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
|||||||
@@ -0,0 +1,94 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:id="@+id/root">
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:background="@drawable/background_16_round_4dp"
|
||||||
|
android:paddingLeft="10dp"
|
||||||
|
android:paddingTop="10dp"
|
||||||
|
android:paddingRight="10dp"
|
||||||
|
android:paddingBottom="10dp"
|
||||||
|
android:layout_margin="10dp">
|
||||||
|
|
||||||
|
<ImageView android:id="@+id/icon_update"
|
||||||
|
android:layout_width="20dp"
|
||||||
|
android:layout_height="20dp"
|
||||||
|
android:src="@drawable/ic_update"
|
||||||
|
android:layout_marginEnd="8dp"
|
||||||
|
app:layout_constraintLeft_toLeftOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="@id/text_title"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@id/text_title" />
|
||||||
|
|
||||||
|
<TextView android:id="@+id/text_title"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
tools:text="Downloading update v123"
|
||||||
|
android:fontFamily="@font/inter_semibold"
|
||||||
|
android:textSize="15sp"
|
||||||
|
android:textColor="@color/white"
|
||||||
|
android:layout_marginStart="8dp"
|
||||||
|
app:layout_constraintLeft_toRightOf="@id/icon_update"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintRight_toLeftOf="@id/button_close" />
|
||||||
|
|
||||||
|
<ImageView android:id="@+id/button_close"
|
||||||
|
android:layout_width="32dp"
|
||||||
|
android:layout_height="32dp"
|
||||||
|
android:padding="6dp"
|
||||||
|
android:src="@drawable/ic_close"
|
||||||
|
android:contentDescription="@string/dismiss"
|
||||||
|
android:background="?android:attr/selectableItemBackgroundBorderless"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintRight_toRightOf="parent" />
|
||||||
|
|
||||||
|
<TextView android:id="@+id/text_body"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
tools:text="42% downloaded"
|
||||||
|
android:fontFamily="@font/inter_light"
|
||||||
|
android:textSize="14sp"
|
||||||
|
android:textColor="#9D9D9D"
|
||||||
|
android:layout_marginTop="2dp"
|
||||||
|
app:layout_constraintLeft_toLeftOf="parent"
|
||||||
|
app:layout_constraintRight_toLeftOf="@id/button_close"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/text_title" />
|
||||||
|
|
||||||
|
<ProgressBar android:id="@+id/update_banner_progress"
|
||||||
|
style="?android:attr/progressBarStyleHorizontal"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:max="100"
|
||||||
|
android:visibility="gone"
|
||||||
|
app:layout_constraintLeft_toLeftOf="parent"
|
||||||
|
app:layout_constraintRight_toRightOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/text_body" />
|
||||||
|
|
||||||
|
<FrameLayout android:id="@+id/button_action"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:background="@drawable/background_button_primary_round_4dp"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/update_banner_progress"
|
||||||
|
app:layout_constraintRight_toRightOf="parent">
|
||||||
|
|
||||||
|
<TextView android:id="@+id/text_action"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
tools:text="Install"
|
||||||
|
android:fontFamily="@font/inter_regular"
|
||||||
|
android:textSize="14sp"
|
||||||
|
android:textColor="@color/white"
|
||||||
|
android:paddingTop="10dp"
|
||||||
|
android:paddingBottom="10dp"
|
||||||
|
android:paddingLeft="20dp"
|
||||||
|
android:paddingRight="20dp" />
|
||||||
|
</FrameLayout>
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
</FrameLayout>
|
||||||
Reference in New Issue
Block a user