Update dialogs

This commit is contained in:
Kelvin
2026-01-05 23:56:53 +01:00
parent 71262da3c2
commit 8536861e09
5 changed files with 120 additions and 16 deletions
@@ -7,6 +7,10 @@ import android.os.IBinder
import android.os.SystemClock import android.os.SystemClock
import com.futo.platformplayer.UIDialogs.ActionStyle import com.futo.platformplayer.UIDialogs.ActionStyle
import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.models.ImageVariable
import com.futo.platformplayer.states.AnnouncementType
import com.futo.platformplayer.states.SessionAnnouncement
import com.futo.platformplayer.states.StateAnnouncement
import com.futo.platformplayer.states.StateApp import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StateUpdate import com.futo.platformplayer.states.StateUpdate
import kotlinx.coroutines.* import kotlinx.coroutines.*
@@ -14,6 +18,7 @@ import java.io.File
import java.io.FileOutputStream import java.io.FileOutputStream
import java.net.HttpURLConnection import java.net.HttpURLConnection
import java.net.URL import java.net.URL
import java.time.OffsetDateTime
class UpdateDownloadService : Service() { class UpdateDownloadService : Service() {
@@ -85,13 +90,16 @@ class UpdateDownloadService : Service() {
job.cancel() job.cancel()
} }
private fun throttledUpdateDownloadProgress(version: Int, progress: Int, indeterminate: Boolean) { private fun throttledUpdateDownloadProgress(version: Int, progress: Int, indeterminate: Boolean, onProgress: ((Int) -> Unit)? = null) {
val now = SystemClock.elapsedRealtime() val now = SystemClock.elapsedRealtime()
val force = progress == 100 && !indeterminate val force = progress == 100 && !indeterminate
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);
if(onProgress != null)
onProgress.invoke(progress);
} }
} }
@@ -99,6 +107,7 @@ class UpdateDownloadService : Service() {
val apkFile = StateUpdate.getApkFile(this, version) val apkFile = StateUpdate.getApkFile(this, version)
val partialFile = StateUpdate.getPartialApkFile(this, version) val partialFile = StateUpdate.getPartialApkFile(this, version)
var announcement: SessionAnnouncement? = null;
try { try {
if (apkFile.exists() && apkFile.length() > 0L) { if (apkFile.exists() && apkFile.length() > 0L) {
Logger.i(TAG, "APK already downloaded at ${apkFile.absolutePath}") Logger.i(TAG, "APK already downloaded at ${apkFile.absolutePath}")
@@ -106,6 +115,14 @@ class UpdateDownloadService : Service() {
return return
} }
try {
announcement = StateAnnouncement.instance.registerLoading("Downloading new version [${version}]", "New version is being downloaded..",
ImageVariable.fromResource(R.drawable.foreground));
}
catch(ex: Exception){
Logger.e(TAG, "Failed to set progress announcement", ex);
}
var backoffMs = INITIAL_BACKOFF_MS var backoffMs = INITIAL_BACKOFF_MS
for (attempt in 0 until MAX_RETRIES) { for (attempt in 0 until MAX_RETRIES) {
@@ -115,7 +132,13 @@ class UpdateDownloadService : Service() {
} }
try { try {
performDownload(StateUpdate.APK_URL, partialFile, version) performDownload(StateUpdate.APK_URL, partialFile, version, {
try {
if (announcement != null)
announcement?.setProgress(it);
}
catch(ex: Throwable) {}
})
if (!cancelRequested) { if (!cancelRequested) {
if (apkFile.exists()) { if (apkFile.exists()) {
@@ -145,6 +168,12 @@ class UpdateDownloadService : Service() {
} }
} }
} finally { } finally {
try {
if (announcement != null) {
StateAnnouncement.instance.closeAnnouncement(announcement.id);
}
}
catch(ex: Throwable){}
isDownloading = false isDownloading = false
cancelRequested = false cancelRequested = false
stopForeground(Service.STOP_FOREGROUND_REMOVE) stopForeground(Service.STOP_FOREGROUND_REMOVE)
@@ -152,7 +181,7 @@ class UpdateDownloadService : Service() {
} }
} }
private fun performDownload(url: String, partialFile: File, version: Int) { private fun performDownload(url: String, partialFile: File, version: Int, onProgress: ((Int)->Unit)? = null) {
var startOffset = if (partialFile.exists()) partialFile.length() else 0L var startOffset = if (partialFile.exists()) partialFile.length() else 0L
Logger.i(TAG, "Starting download. url=$url, existingBytes=$startOffset") Logger.i(TAG, "Starting download. url=$url, existingBytes=$startOffset")
@@ -204,7 +233,7 @@ class UpdateDownloadService : Service() {
progress > 100 -> 100 progress > 100 -> 100
else -> progress else -> progress
} }
throttledUpdateDownloadProgress(version, safeProgress, indeterminate = false) throttledUpdateDownloadProgress(version, safeProgress, indeterminate = false, onProgress)
} }
} else { } else {
throttledUpdateDownloadProgress(version, progress = 0, indeterminate = true) throttledUpdateDownloadProgress(version, progress = 0, indeterminate = true)
@@ -250,6 +279,18 @@ class UpdateDownloadService : Service() {
UpdateNotificationManager.cancelAll(ctx) UpdateNotificationManager.cancelAll(ctx)
UpdateInstaller.startInstall(ctx, version, apkFile) UpdateInstaller.startInstall(ctx, version, apkFile)
}, ActionStyle.PRIMARY, true)); }, 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) { } catch (t: Throwable) {
Logger.w(TAG, "Failed to show in-app update downloaded dialog", t) Logger.w(TAG, "Failed to show in-app update downloaded dialog", t)
updateDownloadedDialog = null updateDownloadedDialog = null
@@ -7,6 +7,8 @@ import com.futo.platformplayer.UIDialogs
import com.futo.platformplayer.api.http.ManagedHttpClient import com.futo.platformplayer.api.http.ManagedHttpClient
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
import com.futo.platformplayer.constructs.Event0 import com.futo.platformplayer.constructs.Event0
import com.futo.platformplayer.constructs.Event1
import com.futo.platformplayer.constructs.Event2
import com.futo.platformplayer.dialogs.PluginUpdateDialog import com.futo.platformplayer.dialogs.PluginUpdateDialog
import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.models.ImageVariable import com.futo.platformplayer.models.ImageVariable
@@ -118,8 +120,8 @@ class StateAnnouncement {
} }
//Special Announcements //Special Announcements
fun registerPluginUpdate(oldConfig: SourcePluginConfig, newConfig: SourcePluginConfig) { fun registerPluginUpdate(oldConfig: SourcePluginConfig, newConfig: SourcePluginConfig): SessionAnnouncement {
registerAnnouncementSession(SessionAnnouncement( val announcement = SessionAnnouncement(
"update-plugin-" + UUID.randomUUID().toString(), "update-plugin-" + UUID.randomUUID().toString(),
"${newConfig.name} update v${newConfig.version} available!", "${newConfig.name} update v${newConfig.version} available!",
"An update is available to upgrade from ${oldConfig.version} to ${newConfig.version}.", "An update is available to upgrade from ${oldConfig.version} to ${newConfig.version}.",
@@ -127,7 +129,9 @@ class StateAnnouncement {
null, "updates", "Update", StateAnnouncement.ACTION_UPDATE_PLUGIN, null, "updates", "Update", StateAnnouncement.ACTION_UPDATE_PLUGIN,
null, null,oldConfig.id, null, null,oldConfig.id,
newConfig?.absoluteIconUrl?.let { ImageVariable.fromUrl(it) } newConfig?.absoluteIconUrl?.let { ImageVariable.fromUrl(it) }
).withExtraAction("Changelog", StateAnnouncement.ACTION_CHANGELOG, oldConfig.id)); ).withExtraAction("Changelog", StateAnnouncement.ACTION_CHANGELOG, oldConfig.id);
registerAnnouncementSession(announcement);
return announcement;
} }
fun registerPluginUpdated(newConfig: SourcePluginConfig) { fun registerPluginUpdated(newConfig: SourcePluginConfig) {
registerAnnouncementSession(SessionAnnouncement( registerAnnouncementSession(SessionAnnouncement(
@@ -141,17 +145,18 @@ class StateAnnouncement {
).withExtraAction("Changelog", StateAnnouncement.ACTION_CHANGELOG, newConfig.id)); ).withExtraAction("Changelog", StateAnnouncement.ACTION_CHANGELOG, newConfig.id));
} }
fun registerLoading(title: String, description: String, icon: ImageVariable? = null): String { fun registerLoading(title: String, description: String, icon: ImageVariable? = null, customId: String? = null): SessionAnnouncement {
val id = "loading-" + UUID.randomUUID().toString(); val id = "loading-" + UUID.randomUUID().toString();
registerAnnouncementSession(SessionAnnouncement( val announcement = SessionAnnouncement(
id, customId ?: id,
title, title,
description, description,
AnnouncementType.ONGOING, AnnouncementType.ONGOING,
null, "loading", null, null, null, "loading", null, null,
null, null,null, icon null, null,null, icon
)); );
return id; registerAnnouncementSession(announcement);
return announcement;
} }
@@ -338,9 +343,10 @@ class StateAnnouncement {
return return
closeAnnouncement(notifId); closeAnnouncement(notifId);
val loadingId = registerLoading("Updating ${plugin.config.name}..", "An update is in progress for ${plugin.config.name}.", val loadingAnnouncement = registerLoading("Updating ${plugin.config.name}..", "An update is in progress for ${plugin.config.name}.",
if(plugin.config.absoluteIconUrl != null) ImageVariable.fromUrl(plugin.config.absoluteIconUrl!!) else null); if(plugin.config.absoluteIconUrl != null) ImageVariable.fromUrl(plugin.config.absoluteIconUrl!!) else null);
val loadingId = loadingAnnouncement.id;
StateApp.instance.contextOrNull?.let { context -> StateApp.instance.contextOrNull?.let { context ->
@@ -462,12 +468,26 @@ class SessionAnnouncement(
var extraActionId: String? = null; var extraActionId: String? = null;
var extraActionData: String? = null; var extraActionData: String? = null;
var extraObj: Any? = null;
var progress: Double? = null;
val onProgressChanged = Event1<SessionAnnouncement>();
fun withExtraAction(name: String, id: String, data: String? = null): SessionAnnouncement { fun withExtraAction(name: String, id: String, data: String? = null): SessionAnnouncement {
extraActionName = name; extraActionName = name;
extraActionId = id; extraActionId = id;
extraActionData = data; extraActionData = data;
return this; return this;
} }
fun setProgress(progress: Double) {
this.progress = progress;
onProgressChanged?.emit(this);
}
fun setProgress(progress: Int) {
this.progress = progress.toDouble().div(100);
onProgressChanged?.emit(this);
}
} }
enum class AnnouncementType(val value : Int) { enum class AnnouncementType(val value : Int) {
@@ -113,7 +113,10 @@ class StateUpdate {
if (!dir.exists()) { if (!dir.exists()) {
dir.mkdirs(); dir.mkdirs();
} }
return File(dir, "app-${DESIRED_ABI}-${version}.apk"); val result = File(dir, "app-${DESIRED_ABI}-${version}.apk");
//if(result.exists())
// result.delete();
return result;
} }
fun getPartialApkFile(context: Context, version: Int): File { fun getPartialApkFile(context: Context, version: Int): File {
@@ -121,7 +124,10 @@ class StateUpdate {
if (!dir.exists()) { if (!dir.exists()) {
dir.mkdirs(); dir.mkdirs();
} }
return File(dir, "app-${DESIRED_ABI}-${version}.apk.part"); val result = File(dir, "app-${DESIRED_ABI}-${version}.apk.part");
//if(result.exists())
// result.delete();
return result;
} }
fun finish() { fun finish() {
@@ -7,8 +7,11 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.ImageView import android.widget.ImageView
import android.widget.LinearLayout import android.widget.LinearLayout
import android.widget.ProgressBar
import android.widget.TextView import android.widget.TextView
import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.view.isVisible
import androidx.lifecycle.findViewTreeLifecycleOwner
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.futo.platformplayer.R import com.futo.platformplayer.R
import com.futo.platformplayer.fragment.mainactivity.main.MainFragment import com.futo.platformplayer.fragment.mainactivity.main.MainFragment
@@ -78,6 +81,7 @@ class NotificationOverlayView: ConstraintLayout {
protected val _buttonExtra: LinearLayout protected val _buttonExtra: LinearLayout
protected val _buttonExtraText: TextView protected val _buttonExtraText: TextView
protected val _loader: LoaderView; protected val _loader: LoaderView;
protected val _progress: ProgressBar;
init { init {
_textName = _view.findViewById(R.id.text_name); _textName = _view.findViewById(R.id.text_name);
@@ -90,6 +94,7 @@ class NotificationOverlayView: ConstraintLayout {
_buttonExtraText = _view.findViewById(R.id.button_extra_text); _buttonExtraText = _view.findViewById(R.id.button_extra_text);
_icon = _view.findViewById(R.id.icon); _icon = _view.findViewById(R.id.icon);
_loader = _view.findViewById(R.id.loader); _loader = _view.findViewById(R.id.loader);
_progress = _view.findViewById(R.id.progress);
_buttonIgnore.setOnClickListener { _buttonIgnore.setOnClickListener {
_announcement.let { _announcement.let {
@@ -116,8 +121,12 @@ class NotificationOverlayView: ConstraintLayout {
override fun bind(value: Announcement) { override fun bind(value: Announcement) {
val oldAnnouncement = _announcement;
_announcement = value; _announcement = value;
if(oldAnnouncement is SessionAnnouncement)
oldAnnouncement.onProgressChanged.clear();
_textName.text = value.title; _textName.text = value.title;
_textMetadata.text = value.msg; _textMetadata.text = value.msg;
@@ -141,6 +150,23 @@ class NotificationOverlayView: ConstraintLayout {
else { else {
_buttonIgnore.visibility = View.VISIBLE; _buttonIgnore.visibility = View.VISIBLE;
} }
if(value.progress != null && value.announceType == AnnouncementType.ONGOING) {
_progress.isVisible = true;
_progress.min = 0;
_progress.max = 100;
value.onProgressChanged.subscribe {
val prog = it.progress;
if(prog == 0.toDouble() || prog == 100.toDouble()) {
_progress.isIndeterminate = true;
}
else {
_progress.isIndeterminate = false;
_progress.setProgress(it.progress?.times(100)?.toInt() ?: 0, false);
}
}
}
else
_progress.isVisible = false;
} }
else { else {
_buttonExtra.visibility = View.GONE; _buttonExtra.visibility = View.GONE;
@@ -143,6 +143,17 @@
</LinearLayout> </LinearLayout>
</LinearLayout> </LinearLayout>
<ProgressBar
android:id="@+id/progress"
style="@style/Widget.AppCompat.ProgressBar.Horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
android:layout_marginBottom="1dp"
android:progressTint="@color/primary"
/>
<View <View
android:id="@+id/separator" android:id="@+id/separator"