Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,12 @@
android:name=".ui.ControllerActivity"
android:exported="false" />

<activity
android:name=".utils.UpdateProgressActivity"
android:theme="@style/Theme.AppCompat.Dialog"
android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|keyboard|keyboardHidden"
android:exported="false" />

<service
android:name=".services.PackageInstallerService"
android:foregroundServiceType="dataSync"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ class PackageInstallerService : Service() {
val text = when (state) {
ApkInstaller.InstallProgressStatus.Installing -> 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
}

Expand All @@ -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
Expand All @@ -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
Expand All @@ -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"
Expand All @@ -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)
}
}
}
64 changes: 49 additions & 15 deletions app/src/main/java/com/lagradost/cloudstream3/utils/InAppUpdater.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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 {
Expand All @@ -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
Expand All @@ -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))
}
}
}
Expand All @@ -350,6 +383,7 @@ object InAppUpdater {
getString(R.string.skip_update_key), update.updateNodeId ?: ""
)
}
downloadedApk.delete()
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ class ApkInstaller(private val service: PackageInstallerService) {
Preparing,
Downloading,
Installing,
Finished,
Failed,
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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)
}
}
}
}
29 changes: 29 additions & 0 deletions app/src/main/res/layout/update_progress_dialog.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">

<ProgressBar
android:id="@+id/update_progress_bar"
style="?android:attr/progressBarStyleHorizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp" />

<TextView
android:id="@+id/update_progress_percentage_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:layout_marginBottom="8dp"
android:text="@string/update_progress_percentage" />

<TextView
android:id="@+id/update_status_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/install_update" />

</LinearLayout>
2 changes: 2 additions & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -593,6 +593,8 @@
<string name="update_notification_downloading">Downloading app update…</string>
<string name="update_notification_installing">Installing app update…</string>
<string name="update_notification_failed">Could not install the new version of the app</string>
<string name="update_progress_percentage">0%</string>
<string name="install_update">Installing update…</string>
<string name="apk_installer_legacy">Legacy</string>
<string name="apk_installer_package_installer">PackageInstaller</string>
<string name="delayed_update_notice">App will be updated upon exit</string>
Expand Down