diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index ee4c978f2be..d88c78dbb97 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -275,6 +275,12 @@
android:name=".ui.ControllerActivity"
android:exported="false" />
+
+
R.string.update_notification_installing
ApkInstaller.InstallProgressStatus.Preparing, ApkInstaller.InstallProgressStatus.Downloading -> R.string.update_notification_downloading
+ ApkInstaller.InstallProgressStatus.Finished -> R.string.download_done
ApkInstaller.InstallProgressStatus.Failed -> R.string.update_notification_failed
}
@@ -141,12 +142,37 @@ class PackageInstallerService : Service() {
val id =
if (state == ApkInstaller.InstallProgressStatus.Failed) UPDATE_NOTIFICATION_ID + 1 else UPDATE_NOTIFICATION_ID
notificationManager.notify(id, newNotification)
+
+ // Broadcast to UpdateProgressActivity if active
+ try {
+ sendBroadcast(
+ Intent(PROGRESS_UPDATE_ACTION).apply {
+ putExtra("progress", percentage)
+ setPackage(packageName)
+ }
+ )
+ sendBroadcast(
+ Intent(STATUS_UPDATE_ACTION).apply {
+ putExtra("status", state.name)
+ setPackage(packageName)
+ }
+ )
+ } catch (e: Exception) {
+ logError(e)
+ }
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
- val url = intent?.getStringExtra(EXTRA_URL) ?: return START_NOT_STICKY
+ val url = intent?.getStringExtra(EXTRA_URL)
+ val filePath = intent?.getStringExtra(EXTRA_FILE_PATH)
+ if (url == null && filePath == null) return START_NOT_STICKY
+
ioSafe {
- downloadUpdate(url)
+ if (filePath != null) {
+ installFromFile(filePath)
+ } else if (url != null) {
+ downloadUpdate(url)
+ }
// Close the service after the update is done
// If no sleep then the install prompt may not appear and the notification
// will disappear instantly
@@ -156,6 +182,47 @@ class PackageInstallerService : Service() {
return START_NOT_STICKY
}
+ private suspend fun installFromFile(filePath: String): Boolean {
+ try {
+ Log.d("PackageInstallerService", "Installing update from file: $filePath")
+ val file = java.io.File(filePath)
+ if (!file.exists()) {
+ updateNotificationProgress(0f, ApkInstaller.InstallProgressStatus.Failed)
+ return false
+ }
+
+ updateLock.withLock {
+ updateNotificationProgress(
+ 0f,
+ ApkInstaller.InstallProgressStatus.Preparing
+ )
+
+ val inputStream = file.inputStream()
+ installer = ApkInstaller(this)
+ val totalSize = file.length()
+ var currentSize = 0L
+
+ installer?.installApk(this, inputStream, totalSize, { bytesRead ->
+ currentSize += bytesRead
+ if (totalSize == 0L) return@installApk
+
+ val percentage = (currentSize / totalSize.toFloat()).coerceIn(0f, 1f)
+ updateNotificationProgress(
+ percentage,
+ ApkInstaller.InstallProgressStatus.Installing
+ )
+ }) { status ->
+ updateNotificationProgress(1f, status)
+ }
+ }
+ return true
+ } catch (e: Exception) {
+ logError(e)
+ updateNotificationProgress(0f, ApkInstaller.InstallProgressStatus.Failed)
+ return false
+ }
+ }
+
override fun onDestroy() {
installer?.unregisterInstallActionReceiver()
installer = null
@@ -172,6 +239,10 @@ class PackageInstallerService : Service() {
companion object {
private const val EXTRA_URL = "EXTRA_URL"
+ private const val EXTRA_FILE_PATH = "EXTRA_FILE_PATH"
+
+ const val PROGRESS_UPDATE_ACTION = "com.lagradost.cloudstream3.PROGRESS_UPDATE"
+ const val STATUS_UPDATE_ACTION = "com.lagradost.cloudstream3.STATUS_UPDATE"
const val UPDATE_CHANNEL_ID = "cloudstream3.updates"
const val UPDATE_CHANNEL_NAME = "App Updates"
@@ -185,5 +256,13 @@ class PackageInstallerService : Service() {
return Intent(context, PackageInstallerService::class.java)
.putExtra(EXTRA_URL, url)
}
+
+ fun getInstallFromFileIntent(
+ context: Context,
+ filePath: String,
+ ): Intent {
+ return Intent(context, PackageInstallerService::class.java)
+ .putExtra(EXTRA_FILE_PATH, filePath)
+ }
}
}
\ No newline at end of file
diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/InAppUpdater.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/InAppUpdater.kt
index f4d6b4271fa..d6b989d5035 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/utils/InAppUpdater.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/utils/InAppUpdater.kt
@@ -186,6 +186,39 @@ object InAppUpdater {
private val updateLock = Mutex()
+ private suspend fun Context.downloadUpdateSilently(url: String, version: String): File? {
+ return try {
+ val appUpdateName = "CloudStream-$version"
+ val appUpdateSuffix = "apk"
+ val targetFile = File(this.cacheDir, "$appUpdateName.$appUpdateSuffix")
+
+ if (targetFile.exists() && targetFile.length() > 0) {
+ return targetFile
+ }
+
+ // Delete old downloaded apk files
+ this.cacheDir.listFiles()?.filter {
+ it.name.startsWith("CloudStream") && it.extension == appUpdateSuffix
+ }?.forEach { deleteFileOnExit(it) }
+
+ val tempFile = File.createTempFile("CloudStream_tmp", ".$appUpdateSuffix", this.cacheDir)
+ updateLock.withLock {
+ val sink: BufferedSink = tempFile.sink().buffer()
+ sink.writeAll(app.get(url).body.source())
+ sink.close()
+ }
+
+ if (tempFile.renameTo(targetFile)) {
+ targetFile
+ } else {
+ tempFile
+ }
+ } catch (e: Exception) {
+ logError(e)
+ null
+ }
+ }
+
private suspend fun Activity.downloadUpdate(url: String): Boolean {
try {
Log.d(LOG_TAG, "Downloading update: $url")
@@ -273,6 +306,13 @@ object InAppUpdater {
return false
}
+ // Silent Background Download first (no notifications, no toast, no UI)
+ val targetVersion = update.updateVersion ?: "latest"
+ val downloadedApk = downloadUpdateSilently(update.updateURL, targetVersion)
+ if (downloadedApk == null || !downloadedApk.exists()) {
+ return false
+ }
+
runOnUiThread {
safe {
val currentVersion = packageName?.let {
@@ -297,8 +337,6 @@ object InAppUpdater {
// Forcefully start any delayed installations
if (ApkInstaller.delayedInstaller?.startInstallation() == true) return@setPositiveButton
- showToast(R.string.download_started, Toast.LENGTH_LONG)
-
// Check if the setting hasn't been changed
if (settingsManager.getInt(
getString(R.string.apk_installer_key), -1
@@ -317,26 +355,21 @@ object InAppUpdater {
)
when (currentInstaller) {
- // New method
+ // New method (PackageInstaller with progress dialog)
0 -> {
- val intent = PackageInstallerService.Companion.getIntent(
- this@runAutoUpdate, update.updateURL
+ // Start progress dialog activity
+ startActivity(UpdateProgressActivity.intent(this@runAutoUpdate))
+
+ val intent = PackageInstallerService.getInstallFromFileIntent(
+ this@runAutoUpdate, downloadedApk.absolutePath
)
ContextCompat.startForegroundService(
this@runAutoUpdate, intent
)
}
- // Legacy
+ // Legacy (System Package Installer)
1 -> {
- ioSafe {
- if (!downloadUpdate(update.updateURL)) {
- runOnUiThread {
- showToast(
- R.string.download_failed, Toast.LENGTH_LONG
- )
- }
- }
- }
+ openApk(this@runAutoUpdate, Uri.fromFile(downloadedApk))
}
}
}
@@ -350,6 +383,7 @@ object InAppUpdater {
getString(R.string.skip_update_key), update.updateNodeId ?: ""
)
}
+ downloadedApk.delete()
}
}
}
diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/PackageInstaller.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/PackageInstaller.kt
index 67851f629cc..cfef81e5c86 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/utils/PackageInstaller.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/utils/PackageInstaller.kt
@@ -52,6 +52,7 @@ class ApkInstaller(private val service: PackageInstallerService) {
Preparing,
Downloading,
Installing,
+ Finished,
Failed,
}
diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/UpdateProgressActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/UpdateProgressActivity.kt
new file mode 100644
index 00000000000..1487d36e89b
--- /dev/null
+++ b/app/src/main/java/com/lagradost/cloudstream3/utils/UpdateProgressActivity.kt
@@ -0,0 +1,103 @@
+package com.lagradost.cloudstream3.utils
+
+import android.app.Activity
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import android.os.Bundle
+import android.widget.ProgressBar
+import android.widget.TextView
+import androidx.core.content.ContextCompat
+import com.lagradost.cloudstream3.R
+import com.lagradost.cloudstream3.services.PackageInstallerService
+
+class UpdateProgressActivity : Activity() {
+ private lateinit var progressBar: ProgressBar
+ private lateinit var progressPercentageText: TextView
+ private lateinit var statusText: TextView
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.update_progress_dialog)
+ progressBar = findViewById(R.id.update_progress_bar)
+ progressPercentageText = findViewById(R.id.update_progress_percentage_text)
+ statusText = findViewById(R.id.update_status_text)
+ updateProgress(0)
+ }
+
+ private fun updateProgress(progress: Int) {
+ progressBar.progress = progress
+ progressPercentageText.text = "$progress%"
+ }
+
+ fun updateStatus(status: String) {
+ statusText.text = status
+ }
+
+ private val progressReceiver = object : BroadcastReceiver() {
+ override fun onReceive(context: Context, intent: Intent) {
+ val progress = intent.getFloatExtra("progress", 0f)
+ updateProgress((progress * 100).toInt())
+ }
+ }
+
+ private val statusReceiver = object : BroadcastReceiver() {
+ override fun onReceive(context: Context, intent: Intent) {
+ val statusString = intent.getStringExtra("status") ?: return
+ val status = try {
+ ApkInstaller.InstallProgressStatus.valueOf(statusString)
+ } catch (_: Exception) {
+ return
+ }
+ updateStatus(getString(getStatusStringResource(status)))
+ if (status == ApkInstaller.InstallProgressStatus.Finished || status == ApkInstaller.InstallProgressStatus.Failed) {
+ finish()
+ }
+ }
+ }
+
+ private fun getStatusStringResource(status: ApkInstaller.InstallProgressStatus): Int {
+ return when (status) {
+ ApkInstaller.InstallProgressStatus.Downloading -> R.string.update_notification_downloading
+ ApkInstaller.InstallProgressStatus.Installing -> R.string.install_update
+ ApkInstaller.InstallProgressStatus.Preparing -> R.string.install_update
+ ApkInstaller.InstallProgressStatus.Finished -> R.string.download_done
+ ApkInstaller.InstallProgressStatus.Failed -> R.string.update_notification_failed
+ }
+ }
+
+ override fun onStart() {
+ super.onStart()
+ val flags = ContextCompat.RECEIVER_NOT_EXPORTED
+ ContextCompat.registerReceiver(
+ this,
+ progressReceiver,
+ IntentFilter(PackageInstallerService.PROGRESS_UPDATE_ACTION),
+ flags
+ )
+ ContextCompat.registerReceiver(
+ this,
+ statusReceiver,
+ IntentFilter(PackageInstallerService.STATUS_UPDATE_ACTION),
+ flags
+ )
+ }
+
+ override fun onStop() {
+ super.onStop()
+ try {
+ unregisterReceiver(progressReceiver)
+ unregisterReceiver(statusReceiver)
+ } catch (_: Exception) {
+ }
+ }
+
+ companion object {
+ fun intent(context: Context): Intent {
+ return Intent(context, UpdateProgressActivity::class.java).apply {
+ addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ }
+ }
+ }
+}
diff --git a/app/src/main/res/layout/update_progress_dialog.xml b/app/src/main/res/layout/update_progress_dialog.xml
new file mode 100644
index 00000000000..e0f77738f53
--- /dev/null
+++ b/app/src/main/res/layout/update_progress_dialog.xml
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 66eb3533c4c..eb8e9815ced 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -593,6 +593,8 @@
Downloading app update…
Installing app update…
Could not install the new version of the app
+ 0%
+ Installing update…
Legacy
PackageInstaller
App will be updated upon exit