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