Added new CompoundDownloadService which will encapsulate all downloads and should manage post-download events as well

As of now it's still in a development stage and isn't connected to anything
This commit is contained in:
Viktor De Pasquale 2019-07-09 20:08:00 +02:00 committed by John Wu
parent 8af832a496
commit 9542ca773f
12 changed files with 283 additions and 3 deletions

View File

@ -9,8 +9,8 @@
<uses-permission android:name="android.permission.USE_FINGERPRINT" />
<application
android:allowBackup="true"
android:name="a.e"
android:allowBackup="true"
android:theme="@style/MagiskTheme"
android:usesCleartextTraffic="true"
tools:ignore="UnusedAttribute,GoogleAppIndexingWarning">
@ -41,9 +41,9 @@
<activity
android:name="a.m"
android:exported="false"
android:directBootAware="true"
android:excludeFromRecents="true"
android:exported="false"
android:theme="@style/MagiskTheme.SU" />
<!-- Receiver -->
@ -66,6 +66,9 @@
<!-- Service -->
<service android:name="a.j" />
<service
android:name="a.k"
android:exported="false" />
<!-- Hardcode GMS version -->
<meta-data

5
app/src/main/java/a/k.kt Normal file
View File

@ -0,0 +1,5 @@
package a
import com.topjohnwu.magisk.model.download.CompoundDownloadService
class k : CompoundDownloadService()

View File

@ -1,5 +1,6 @@
package com.topjohnwu.magisk
import com.topjohnwu.magisk.model.download.CompoundDownloadService
import com.topjohnwu.magisk.model.download.DownloadModuleService
import com.topjohnwu.magisk.model.receiver.GeneralReceiver
import com.topjohnwu.magisk.model.update.UpdateCheckService
@ -17,6 +18,7 @@ object ClassMap {
UpdateCheckService::class.java to a.g::class.java,
GeneralReceiver::class.java to a.h::class.java,
DownloadModuleService::class.java to a.j::class.java,
CompoundDownloadService::class.java to a.k::class.java,
SuRequestActivity::class.java to a.m::class.java
)

View File

@ -42,6 +42,7 @@ object Config : PreferenceModel, DBConfig {
const val ETAG_KEY = "ETag"
const val REPO_ORDER = "repo_order"
const val SHOW_SYSTEM_APP = "show_system"
const val DOWNLOAD_CACHE = "download_cache"
// system state
const val MAGISKHIDE = "magiskhide"
@ -94,6 +95,7 @@ object Config : PreferenceModel, DBConfig {
if (Utils.isCanary) Value.CANARY_DEBUG_CHANNEL
else Value.DEFAULT_CHANNEL
var isDownloadCacheEnabled by preference(Key.DOWNLOAD_CACHE, true)
var repoOrder by preference(Key.REPO_ORDER, Value.ORDER_DATE)
var suDefaultTimeout by preferenceStrInt(Key.SU_REQUEST_TIMEOUT, 10)

View File

@ -0,0 +1,11 @@
package com.topjohnwu.magisk.data.repository
import com.topjohnwu.magisk.data.network.GithubRawApiServices
class FileRepository(
private val api: GithubRawApiServices
) {
fun downloadFile(url: String) = api.fetchFile(url)
}

View File

@ -1,6 +1,7 @@
package com.topjohnwu.magisk.di
import com.topjohnwu.magisk.data.repository.AppRepository
import com.topjohnwu.magisk.data.repository.FileRepository
import com.topjohnwu.magisk.data.repository.LogRepository
import com.topjohnwu.magisk.data.repository.MagiskRepository
import org.koin.dsl.module
@ -10,4 +11,5 @@ val repositoryModule = module {
single { MagiskRepository(get(), get(), get()) }
single { LogRepository(get()) }
single { AppRepository(get()) }
single { FileRepository(get()) }
}

View File

@ -0,0 +1,75 @@
package com.topjohnwu.magisk.model.download
import android.annotation.SuppressLint
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import androidx.core.app.NotificationCompat
import com.topjohnwu.magisk.ClassMap
import com.topjohnwu.magisk.model.entity.internal.Configuration
import com.topjohnwu.magisk.model.entity.internal.DownloadSubject
import com.topjohnwu.magisk.ui.flash.FlashActivity
import java.io.File
import kotlin.random.Random.Default.nextInt
@SuppressLint("Registered")
open class CompoundDownloadService : SubstrateDownloadService() {
private val context get() = this
override fun onFinished(file: File, subject: DownloadSubject) {
when (subject) {
is DownloadSubject.Magisk -> {
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 }
}
}

View File

@ -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<FileRepository>()
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<DownloadSubject>(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<NotificationManager>()?.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"
}
}

View File

@ -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(

View File

@ -0,0 +1,5 @@
package com.topjohnwu.magisk.model.entity.internal
enum class Configuration {
FLASH, DOWNLOAD
}

View File

@ -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"
}
}

View File

@ -71,6 +71,8 @@
<string name="update_channel">Magisk Updates</string>
<string name="progress_channel">Progress Notifications</string>
<string name="download_complete">Download complete</string>
<string name="download_local">Looking for local copies…</string>
<string name="download_progress">%1$.2f / %2$.2f MB</string>
<string name="download_file_error">Error downloading file</string>
<string name="magisk_update_title">Magisk Update Available!</string>
<string name="manager_update_title">Magisk Manager Update Available!</string>