diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 4bf6ef98d..3416c06b7 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -9,8 +9,8 @@
@@ -41,9 +41,9 @@
@@ -66,6 +66,9 @@
+
{
+ if (subject.configuration == Configuration.FLASH) {
+ FlashActivity.flashMagisk(this, file)
+ }
+ }
+ is DownloadSubject.Module -> {
+ if (subject.configuration == Configuration.FLASH) {
+ FlashActivity.flashModule(this, file)
+ }
+ }
+ }
+ }
+
+ // ---
+
+ override fun NotificationCompat.Builder.addActions(file: File, subject: DownloadSubject) =
+ when (subject) {
+ is DownloadSubject.Magisk -> addMagiskActions(file, subject.configuration)
+ is DownloadSubject.Module -> addModuleActions(file, subject.configuration)
+ }
+
+ private fun NotificationCompat.Builder.addMagiskActions(
+ file: File,
+ configuration: Configuration
+ ) = apply {
+ when (configuration) {
+ Configuration.FLASH -> {
+ val inner = FlashActivity.flashMagiskIntent(context, file)
+ val intent = PendingIntent
+ .getActivity(context, nextInt(), inner, PendingIntent.FLAG_ONE_SHOT)
+
+ setContentIntent(intent)
+ }
+ }
+
+ }
+
+ private fun NotificationCompat.Builder.addModuleActions(
+ file: File,
+ configuration: Configuration
+ ) = apply {
+
+ }
+
+ companion object {
+
+ fun download(context: Context, subject: DownloadSubject) =
+ Intent(context, ClassMap[CompoundDownloadService::class.java])
+ .putExtra(ARG_URL, subject)
+ .let { context.startService(it); Unit }
+
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/topjohnwu/magisk/model/download/SubstrateDownloadService.kt b/app/src/main/java/com/topjohnwu/magisk/model/download/SubstrateDownloadService.kt
new file mode 100644
index 000000000..ac6afec4b
--- /dev/null
+++ b/app/src/main/java/com/topjohnwu/magisk/model/download/SubstrateDownloadService.kt
@@ -0,0 +1,135 @@
+package com.topjohnwu.magisk.model.download
+
+import android.app.NotificationManager
+import android.app.Service
+import android.content.Intent
+import android.os.IBinder
+import androidx.core.app.NotificationCompat
+import androidx.core.content.getSystemService
+import com.skoumal.teanity.extensions.subscribeK
+import com.topjohnwu.magisk.Config
+import com.topjohnwu.magisk.Const
+import com.topjohnwu.magisk.R
+import com.topjohnwu.magisk.data.repository.FileRepository
+import com.topjohnwu.magisk.model.entity.internal.DownloadSubject
+import com.topjohnwu.magisk.utils.writeToCachedFile
+import com.topjohnwu.magisk.view.Notifications
+import com.topjohnwu.superuser.ShellUtils
+import io.reactivex.Single
+import okhttp3.ResponseBody
+import org.koin.android.ext.android.inject
+import java.io.File
+import kotlin.random.Random.Default.nextInt
+
+abstract class SubstrateDownloadService : Service() {
+
+ private var _notification: NotificationCompat.Builder? = null
+
+ private val repo by inject()
+
+ private val notification: NotificationCompat.Builder
+ get() = _notification ?: Notifications.progress(this, "")
+ .setContentText(getString(R.string.download_local))
+ .also { _notification = it }
+
+ override fun onBind(p0: Intent?): IBinder? = null
+
+ override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
+ intent?.getParcelableExtra(ARG_URL)?.let {
+ updateNotification { notification -> notification.setContentTitle(it.fileName) }
+ start(it)
+ }
+ return START_REDELIVER_INTENT
+ }
+
+ // ---
+
+ private fun start(subject: DownloadSubject) = search(subject)
+ .onErrorResumeNext(download(subject))
+ .subscribeK {
+ runCatching { onFinished(it, subject) }
+ finish(it, subject)
+ }
+
+ private fun search(subject: DownloadSubject) = Single.fromCallable {
+ if (!Config.isDownloadCacheEnabled) {
+ throw IllegalStateException("The download cache is disabled")
+ }
+
+ val file = runCatching {
+ cacheDir.list().orEmpty()
+ .first { it == subject.fileName } // this throws an exception if not found
+ .let { File(cacheDir, it) }
+ }.getOrElse {
+ Const.EXTERNAL_PATH.list().orEmpty()
+ .first { it == subject.fileName } // this throws an exception if not found
+ .let { File(Const.EXTERNAL_PATH, it) }
+ }
+
+ if (subject is DownloadSubject.Magisk) {
+ if (!ShellUtils.checkSum("MD5", file, subject.magisk.hash)) {
+ throw IllegalStateException("The given file doesn't match the hash")
+ }
+ }
+
+ file
+ }
+
+ private fun download(subject: DownloadSubject) = repo.downloadFile(subject.url)
+ .map { it.toFile(subject.fileName) }
+
+ // ---
+
+ private fun ResponseBody.toFile(name: String): File {
+ val maxRaw = contentLength()
+ val max = maxRaw / 1_000_000f
+
+ return writeToCachedFile(this@SubstrateDownloadService, name) {
+ val progress = it / 1_000_000f
+
+ updateNotification { notification ->
+ notification
+ .setProgress(maxRaw.toInt(), it.toInt(), false)
+ .setContentText(getString(R.string.download_progress, progress, max))
+ }
+ }
+ }
+
+ private fun finish(file: File, subject: DownloadSubject) {
+ stopForeground(false)
+
+ val notification = notification.addActions(file, subject)
+ .setContentText(getString(R.string.download_complete))
+ .setSmallIcon(android.R.drawable.stat_sys_download_done)
+ .setProgress(0, 0, false)
+ .setOngoing(false)
+ .setAutoCancel(true)
+ .build()
+
+ getSystemService()?.notify(nextInt(), notification)
+
+ stopSelf()
+ }
+
+ private inline fun updateNotification(body: (NotificationCompat.Builder) -> Unit = {}) {
+ startForeground(ID, notification.also(body).build())
+ }
+
+ // ---
+
+
+ @Throws(Throwable::class)
+ protected abstract fun onFinished(file: File, subject: DownloadSubject)
+
+ protected abstract fun NotificationCompat.Builder.addActions(
+ file: File,
+ subject: DownloadSubject
+ ): NotificationCompat.Builder
+
+
+ companion object {
+ private const val ID = 300
+ const val ARG_URL = "arg_url"
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/topjohnwu/magisk/model/entity/UpdateInfo.kt b/app/src/main/java/com/topjohnwu/magisk/model/entity/UpdateInfo.kt
index ed80529b0..52b1633bc 100644
--- a/app/src/main/java/com/topjohnwu/magisk/model/entity/UpdateInfo.kt
+++ b/app/src/main/java/com/topjohnwu/magisk/model/entity/UpdateInfo.kt
@@ -1,6 +1,8 @@
package com.topjohnwu.magisk.model.entity
+import android.os.Parcelable
import com.squareup.moshi.Json
+import kotlinx.android.parcel.Parcelize
import se.ansman.kotshi.JsonSerializable
@JsonSerializable
@@ -15,6 +17,7 @@ data class UninstallerJson(
val link: String = ""
)
+@Parcelize
@JsonSerializable
data class MagiskJson(
val version: String = "",
@@ -22,7 +25,7 @@ data class MagiskJson(
val link: String = "",
val note: String = "",
@Json(name = "md5") val hash: String = ""
-)
+) : Parcelable
@JsonSerializable
data class ManagerJson(
diff --git a/app/src/main/java/com/topjohnwu/magisk/model/entity/internal/Configuration.kt b/app/src/main/java/com/topjohnwu/magisk/model/entity/internal/Configuration.kt
new file mode 100644
index 000000000..25bbebcb9
--- /dev/null
+++ b/app/src/main/java/com/topjohnwu/magisk/model/entity/internal/Configuration.kt
@@ -0,0 +1,5 @@
+package com.topjohnwu.magisk.model.entity.internal
+
+enum class Configuration {
+ FLASH, DOWNLOAD
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/topjohnwu/magisk/model/entity/internal/DownloadSubject.kt b/app/src/main/java/com/topjohnwu/magisk/model/entity/internal/DownloadSubject.kt
new file mode 100644
index 000000000..a6ea53fc5
--- /dev/null
+++ b/app/src/main/java/com/topjohnwu/magisk/model/entity/internal/DownloadSubject.kt
@@ -0,0 +1,35 @@
+package com.topjohnwu.magisk.model.entity.internal
+
+import android.os.Parcelable
+import com.topjohnwu.magisk.model.entity.MagiskJson
+import kotlinx.android.parcel.Parcelize
+import com.topjohnwu.magisk.model.entity.Module as MagiskModule
+
+sealed class DownloadSubject : Parcelable {
+
+ abstract val fileName: String
+ abstract val url: String
+
+ @Parcelize
+ data class Module(
+ val module: MagiskModule,
+ val configuration: Configuration
+ ) : DownloadSubject() {
+
+ override val url: String get() = module.path
+ override val fileName: String get() = "${module.name}-v${module.version}(${module.versionCode}).zip"
+
+ }
+
+ @Parcelize
+ data class Magisk(
+ val magisk: MagiskJson,
+ val configuration: Configuration
+ ) : DownloadSubject() {
+
+ override val url: String get() = magisk.link
+ override val fileName get() = "Magisk-v${magisk.version}(${magisk.versionCode}).zip"
+
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index e34c43b60..e8a4f1710 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -71,6 +71,8 @@
Magisk Updates
Progress Notifications
Download complete
+ Looking for local copies…
+ %1$.2f / %2$.2f MB
Error downloading file
Magisk Update Available!
Magisk Manager Update Available!