-
Notifications
You must be signed in to change notification settings - Fork 1.1k
feat(updater): silent background pre-download and instant install for Compose updater #3220
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -65,9 +65,101 @@ object ApkUpdater : AppUpdater { | |||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| fun clearOldFiles(activity: Activity) { | ||||||||||||||||||||||||||||||
| fun getCachedUpdateFile(context: Context, versionTag: String): File { | ||||||||||||||||||||||||||||||
| return File(context.cacheDir, "${APP_UPDATE_NAME}_$versionTag.$APP_UPDATE_SUFFIX") | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| suspend fun downloadSilently( | ||||||||||||||||||||||||||||||
| context: Context, | ||||||||||||||||||||||||||||||
| url: String, | ||||||||||||||||||||||||||||||
| versionTag: String, | ||||||||||||||||||||||||||||||
| digest: DigestPair? | ||||||||||||||||||||||||||||||
| ): File? = withContext(Dispatchers.IO) { | ||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||
| val targetFile = getCachedUpdateFile(context, versionTag) | ||||||||||||||||||||||||||||||
| if (targetFile.exists() && targetFile.length() > 0) { | ||||||||||||||||||||||||||||||
| return@withContext targetFile | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| clearOldFiles(context) | ||||||||||||||||||||||||||||||
| val request = app.get(url) | ||||||||||||||||||||||||||||||
| val length = request.size | ||||||||||||||||||||||||||||||
| val body = request.body | ||||||||||||||||||||||||||||||
| val tempFile = File.createTempFile(APP_UPDATE_NAME, ".$APP_UPDATE_SUFFIX", context.cacheDir) | ||||||||||||||||||||||||||||||
| body.use { body -> | ||||||||||||||||||||||||||||||
| val readStream = body.byteStream() | ||||||||||||||||||||||||||||||
| tempFile.outputStream().use { writeStream -> | ||||||||||||||||||||||||||||||
| transfer(writeStream, readStream, length ?: body.contentLength(), { _, _ -> }, digest) | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| if (tempFile.renameTo(targetFile)) { | ||||||||||||||||||||||||||||||
| targetFile | ||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||
| tempFile | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| } catch (_: Throwable) { | ||||||||||||||||||||||||||||||
| null | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| @Throws | ||||||||||||||||||||||||||||||
| suspend fun installFromFile( | ||||||||||||||||||||||||||||||
| activity: Activity, | ||||||||||||||||||||||||||||||
| file: File, | ||||||||||||||||||||||||||||||
| settings: AppSettings, | ||||||||||||||||||||||||||||||
| installProgress: (Long, Long?) -> Unit | ||||||||||||||||||||||||||||||
| ) = withContext(Dispatchers.IO) { | ||||||||||||||||||||||||||||||
| when (settings.updates.apkInstaller.get()) { | ||||||||||||||||||||||||||||||
| 0 -> { | ||||||||||||||||||||||||||||||
| val length = file.length() | ||||||||||||||||||||||||||||||
| file.inputStream().use { inputStream -> | ||||||||||||||||||||||||||||||
| var sessionId: Int? = null | ||||||||||||||||||||||||||||||
| val packageInstaller = activity.packageManager.packageInstaller | ||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||
| val installParams = | ||||||||||||||||||||||||||||||
| PackageInstaller.SessionParams(PackageInstaller.SessionParams.MODE_FULL_INSTALL) | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { | ||||||||||||||||||||||||||||||
| installParams.setRequireUserAction(PackageInstaller.SessionParams.USER_ACTION_NOT_REQUIRED) | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| installParams.setSize(length) | ||||||||||||||||||||||||||||||
|
Comment on lines
+120
to
+126
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Please properly group code. |
||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| sessionId = packageInstaller.createSession(installParams) | ||||||||||||||||||||||||||||||
| val session = packageInstaller.openSession(sessionId) | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| session.openWrite(activity.packageName, 0, length) | ||||||||||||||||||||||||||||||
| .use { writeStream -> | ||||||||||||||||||||||||||||||
| transfer(writeStream, inputStream, length, installProgress, null) | ||||||||||||||||||||||||||||||
| session.fsync(writeStream) | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| val receiverIntent = Intent(activity, PackageInstallerStatusReceiver::class.java) | ||||||||||||||||||||||||||||||
| val flags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { | ||||||||||||||||||||||||||||||
| PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT | ||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||
| PendingIntent.FLAG_UPDATE_CURRENT | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| val receiverPendingIntent = PendingIntent.getBroadcast(activity, 0, receiverIntent, flags) | ||||||||||||||||||||||||||||||
|
Comment on lines
+136
to
+143
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe it's possible to use |
||||||||||||||||||||||||||||||
| session.commit(receiverPendingIntent.intentSender) | ||||||||||||||||||||||||||||||
| session.close() | ||||||||||||||||||||||||||||||
| } catch (t: Throwable) { | ||||||||||||||||||||||||||||||
| sessionId?.let { id -> | ||||||||||||||||||||||||||||||
| packageInstaller.abandonSession(id) | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| throw t | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
Comment on lines
+117
to
+151
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is all just copy and paste from |
||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| else -> { | ||||||||||||||||||||||||||||||
| openApk(activity, file) | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| fun clearOldFiles(context: Context) { | ||||||||||||||||||||||||||||||
| // Delete old files | ||||||||||||||||||||||||||||||
| activity.cacheDir.listFiles()?.filter { | ||||||||||||||||||||||||||||||
| context.cacheDir.listFiles()?.filter { | ||||||||||||||||||||||||||||||
| it.name.startsWith(APP_UPDATE_NAME) && it.extension == APP_UPDATE_SUFFIX | ||||||||||||||||||||||||||||||
| }?.forEach { | ||||||||||||||||||||||||||||||
| deleteFileOnExit(it) | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -24,6 +24,7 @@ data class GithubState( | |||||
| sealed class GithubUpdateDialogState { | ||||||
| data class Error(val error: Throwable) : GithubUpdateDialogState() | ||||||
| data class DownloadProgress(val progress: Long, val total: Long?) : GithubUpdateDialogState() | ||||||
| data class InstallProgress(val progress: Long, val total: Long?) : GithubUpdateDialogState() | ||||||
| object Loading : GithubUpdateDialogState() | ||||||
| object NoUpdateFound : GithubUpdateDialogState() | ||||||
| data class UpdateFound( | ||||||
|
|
@@ -153,6 +154,7 @@ class GithubViewModel( | |||||
|
|
||||||
| is GithubAction.SkipThisUpdate -> { | ||||||
| settings.updates.skipUpdate.set(action.file.nodeId) | ||||||
| deleteCachedApk(action.file.tagName) | ||||||
| } | ||||||
|
|
||||||
| GithubAction.AutoSearchForUpdate -> { | ||||||
|
|
@@ -165,11 +167,12 @@ class GithubViewModel( | |||||
|
|
||||||
| is GithubAction.SkipUpdate -> { | ||||||
| settings.updates.skipUpdate.set(action.file.nodeId) | ||||||
| deleteCachedApk(action.file.tagName) | ||||||
| } | ||||||
|
|
||||||
| is GithubAction.Update -> { | ||||||
| ioSafe { | ||||||
| installUpdate(action.file.downloadUrl, action.file.digest) | ||||||
| installUpdate(action.file) | ||||||
| } | ||||||
| } | ||||||
| } | ||||||
|
|
@@ -193,22 +196,52 @@ class GithubViewModel( | |||||
| } | ||||||
| } | ||||||
|
|
||||||
| private suspend fun installUpdate(url: String, digestPair: String?) = dispatchUpdate { | ||||||
| updater.update( | ||||||
| settings = settings, | ||||||
| url = url, | ||||||
| digest = DigestPair.parse(digestPair), | ||||||
| ) { progress, total -> | ||||||
| private suspend fun installUpdate(file: GithubReleases.GithubFile) = dispatchUpdate { | ||||||
| val activity = com.lagradost.cloudstream3.CommonActivity.activity | ||||||
| val cachedFile = activity?.let { | ||||||
| ApkUpdater.getCachedUpdateFile(it, file.tagName) | ||||||
| } | ||||||
|
|
||||||
| if (activity != null && cachedFile != null && cachedFile.exists() && cachedFile.length() > 0) { | ||||||
| updateState { | ||||||
| copy( | ||||||
| dialog = dialog?.copy( | ||||||
| state = GithubUpdateDialogState.DownloadProgress( | ||||||
| progress = progress, | ||||||
| total = total | ||||||
| state = GithubUpdateDialogState.InstallProgress( | ||||||
| progress = 0, | ||||||
| total = cachedFile.length() | ||||||
| ) | ||||||
| ) | ||||||
| ) | ||||||
| } | ||||||
| ApkUpdater.installFromFile(activity, cachedFile, settings) { progress, total -> | ||||||
| updateState { | ||||||
| copy( | ||||||
| dialog = dialog?.copy( | ||||||
| state = GithubUpdateDialogState.InstallProgress( | ||||||
| progress = progress, | ||||||
| total = total | ||||||
| ) | ||||||
| ) | ||||||
| ) | ||||||
| } | ||||||
| } | ||||||
| } else { | ||||||
| updater.update( | ||||||
| settings = settings, | ||||||
| url = file.downloadUrl, | ||||||
| digest = DigestPair.parse(file.digest), | ||||||
| ) { progress, total -> | ||||||
| updateState { | ||||||
| copy( | ||||||
| dialog = dialog?.copy( | ||||||
| state = GithubUpdateDialogState.DownloadProgress( | ||||||
| progress = progress, | ||||||
| total = total | ||||||
| ) | ||||||
| ) | ||||||
| ) | ||||||
| } | ||||||
| } | ||||||
| } | ||||||
| updateState { | ||||||
| copy( | ||||||
|
|
@@ -266,6 +299,19 @@ class GithubViewModel( | |||||
| return@dispatchUpdate | ||||||
| } | ||||||
|
|
||||||
| // If automated background search, download the update APK silently in advance | ||||||
| if (!fromUser) { | ||||||
| val activity = com.lagradost.cloudstream3.CommonActivity.activity | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
| if (activity != null) { | ||||||
| ApkUpdater.downloadSilently( | ||||||
| activity, | ||||||
| release.downloadUrl, | ||||||
| release.tagName, | ||||||
| DigestPair.parse(release.digest) | ||||||
| ) | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| updateState { | ||||||
| copy( | ||||||
| dialog = baseDialog.copy( | ||||||
|
|
@@ -296,4 +342,14 @@ class GithubViewModel( | |||||
| userName = remoteUserName, | ||||||
| repository = remoteRepository, | ||||||
| ) | ||||||
|
|
||||||
| private fun deleteCachedApk(tagName: String) { | ||||||
| val activity = com.lagradost.cloudstream3.CommonActivity.activity | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
| if (activity != null) { | ||||||
| val file = ApkUpdater.getCachedUpdateFile(activity, tagName) | ||||||
| if (file.exists()) { | ||||||
| file.delete() | ||||||
| } | ||||||
| } | ||||||
| } | ||||||
| } | ||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why should this rename fail? In case it's because
targetFilecould still exist, we should deletetargetFilefirst.