Commit 967bdeae authored by Viktor De Pasquale's avatar Viktor De Pasquale Committed by John Wu

Updated service architecture and extracted useful tools to separate class

parent 452db516
package a
import com.topjohnwu.magisk.model.download.CompoundDownloadService
import com.topjohnwu.magisk.model.download.DownloadService
class k : CompoundDownloadService()
\ No newline at end of file
class k : DownloadService()
\ No newline at end of file
package com.topjohnwu.magisk
import com.topjohnwu.magisk.model.download.CompoundDownloadService
import com.topjohnwu.magisk.model.download.DownloadModuleService
import com.topjohnwu.magisk.model.download.DownloadService
import com.topjohnwu.magisk.model.receiver.GeneralReceiver
import com.topjohnwu.magisk.model.update.UpdateCheckService
import com.topjohnwu.magisk.ui.MainActivity
......@@ -18,7 +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,
DownloadService::class.java to a.k::class.java,
SuRequestActivity::class.java to a.m::class.java
)
......
......@@ -6,22 +6,39 @@ import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.os.Build
import android.webkit.MimeTypeMap
import android.widget.Toast
import androidx.annotation.RequiresPermission
import androidx.core.app.NotificationCompat
import com.topjohnwu.magisk.ClassMap
import com.topjohnwu.magisk.Const
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.model.entity.internal.Configuration.*
import com.topjohnwu.magisk.model.entity.internal.Configuration.Flash.Secondary
import com.topjohnwu.magisk.model.entity.internal.DownloadSubject
import com.topjohnwu.magisk.model.entity.internal.DownloadSubject.Magisk
import com.topjohnwu.magisk.model.entity.internal.DownloadSubject.Module
import com.topjohnwu.magisk.ui.flash.FlashActivity
import com.topjohnwu.magisk.utils.Utils
import com.topjohnwu.magisk.utils.provide
import java.io.File
import kotlin.random.Random.Default.nextInt
/* More of a facade for [RemoteFileService], but whatever... */
@SuppressLint("Registered")
open class CompoundDownloadService : SubstrateDownloadService() {
open class DownloadService : RemoteFileService() {
private val context get() = this
private val String.downloadsFile get() = File(Const.EXTERNAL_PATH, this)
private val File.type
get() = MimeTypeMap.getSingleton()
.getMimeTypeFromExtension(extension)
.orEmpty()
override fun map(subject: DownloadSubject, file: File): File = when (subject) {
is Module -> file // todo tbd
else -> file
}
override fun onFinished(file: File, subject: DownloadSubject) = when (subject) {
is Magisk -> onFinishedInternal(file, subject)
......@@ -83,18 +100,43 @@ open class CompoundDownloadService : SubstrateDownloadService() {
PendingIntent.getActivity(context, nextInt(), intent, PendingIntent.FLAG_ONE_SHOT)
.let { setContentIntent(it) }
// ---
private fun moveToDownloads(file: File) {
val destination = file.name.downloadsFile
if (file != destination) {
destination.deleteRecursively()
file.copyTo(destination)
}
Utils.toast(
getString(R.string.internal_storage, "/Download/${file.name}"),
Toast.LENGTH_LONG
)
}
private fun fileIntent(fileName: String): Intent {
val file = fileName.downloadsFile
return Intent(Intent.ACTION_VIEW)
.setDataAndType(file.provide(this), file.type)
.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
}
class Builder {
lateinit var subject: DownloadSubject
}
companion object {
@RequiresPermission(allOf = [Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE])
fun download(context: Context, subject: DownloadSubject) {
Intent(context, ClassMap[CompoundDownloadService::class.java])
.putExtra(ARG_URL, subject)
.let {
inline operator fun invoke(context: Context, argBuilder: Builder.() -> Unit) {
val builder = Builder().apply(argBuilder)
val intent = Intent(context, ClassMap[DownloadService::class.java])
.putExtra(ARG_URL, builder.subject)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
context.startForegroundService(it)
context.startForegroundService(intent)
} else {
context.startService(it)
}
context.startService(intent)
}
}
......
package com.topjohnwu.magisk.model.download
import android.app.Notification
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 java.util.*
import kotlin.random.Random.Default.nextInt
abstract class NotificationService : Service() {
abstract val defaultNotification: NotificationCompat.Builder
private val manager get() = getSystemService<NotificationManager>()
private val hasNotifications get() = notifications.isEmpty()
private val notifications =
Collections.synchronizedMap(mutableMapOf<Int, NotificationCompat.Builder>())
override fun onTaskRemoved(rootIntent: Intent?) {
super.onTaskRemoved(rootIntent)
notifications.values.forEach { cancel(it.hashCode()) }
notifications.clear()
}
// --
protected fun update(
id: Int,
body: (NotificationCompat.Builder) -> Unit = {}
) {
val notification = notifications.getOrPut(id) { defaultNotification }
notify(id, notification.also(body).build())
if (notifications.size == 1) {
updateForeground()
}
}
protected fun finishWork(
id: Int,
editBody: (NotificationCompat.Builder) -> NotificationCompat.Builder? = { null }
) {
val currentNotification = remove(id)?.run(editBody) ?: let {
cancel(id)
return
}
updateForeground()
cancel(id)
notify(nextInt(), currentNotification.build())
if (!hasNotifications) {
stopForeground(true)
stopSelf()
}
}
// ---
private fun notify(id: Int, notification: Notification) {
manager?.notify(id, notification)
}
private fun cancel(id: Int) {
manager?.cancel(id)
}
private fun remove(id: Int) = notifications.remove(id)
.also { updateForeground() }
private fun updateForeground() {
runCatching { notifications.keys.first() to notifications.values.first() }
.getOrNull()
?.let { startForeground(it.first, it.second.build()) }
}
// --
override fun onBind(p0: Intent?): IBinder? = null
}
\ No newline at end of file
package com.topjohnwu.magisk.model.download
import android.app.NotificationManager
import android.app.Service
import android.content.Intent
import android.os.IBinder
import android.webkit.MimeTypeMap
import android.widget.Toast
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.Utils
import com.topjohnwu.magisk.utils.provide
import com.topjohnwu.magisk.utils.writeToCachedFile
import com.topjohnwu.magisk.view.Notifications
import com.topjohnwu.superuser.ShellUtils
......@@ -24,35 +16,26 @@ import okhttp3.ResponseBody
import org.koin.android.ext.android.inject
import timber.log.Timber
import java.io.File
import java.util.*
import kotlin.random.Random.Default.nextInt
abstract class SubstrateDownloadService : Service() {
abstract class RemoteFileService : NotificationService() {
private val repo by inject<FileRepository>()
private val manager get() = getSystemService<NotificationManager>()
private val notifications =
Collections.synchronizedMap(mutableMapOf<Int, NotificationCompat.Builder>())
override fun onBind(p0: Intent?): IBinder? = null
override val defaultNotification: NotificationCompat.Builder
get() = Notifications
.progress(this, "")
.setContentText(getString(R.string.download_local))
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
intent?.getParcelableExtra<DownloadSubject>(ARG_URL)?.let { start(it) }
return START_REDELIVER_INTENT
}
override fun onTaskRemoved(rootIntent: Intent?) {
super.onTaskRemoved(rootIntent)
notifications.values.forEach { manager?.cancel(it.hashCode()) }
notifications.clear()
}
// ---
private fun start(subject: DownloadSubject) = search(subject)
.onErrorResumeNext(download(subject))
.doOnSubscribe { updateNotification(subject.hashCode()) { it.setContentTitle(subject.fileName) } }
.doOnSubscribe { update(subject.hashCode()) { it.setContentTitle(subject.fileName) } }
.subscribeK {
runCatching { onFinished(it, subject) }.onFailure { Timber.e(it) }
finish(it, subject)
......@@ -84,45 +67,18 @@ abstract class SubstrateDownloadService : Service() {
private fun download(subject: DownloadSubject) = repo.downloadFile(subject.url)
.map { it.toFile(subject.hashCode(), subject.fileName) }
.map { map(subject, it) }
// ---
protected fun fileIntent(fileName: String): Intent {
val file = downloadsFile(fileName)
return Intent(Intent.ACTION_VIEW)
.setDataAndType(file.provide(this), file.type)
.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
}
protected fun moveToDownloads(file: File) {
val destination = downloadsFile(file.name)
if (file != destination) {
destination.deleteRecursively()
file.copyTo(destination)
}
Utils.toast(
getString(R.string.internal_storage, "/Download/${file.name}"),
Toast.LENGTH_LONG
)
}
// ---
private val File.type
get() = MimeTypeMap.getSingleton()
.getMimeTypeFromExtension(extension)
.orEmpty()
private fun downloadsFile(name: String) = File(Const.EXTERNAL_PATH, name)
private fun ResponseBody.toFile(id: Int, name: String): File {
val maxRaw = contentLength()
val max = maxRaw / 1_000_000f
return writeToCachedFile(this@SubstrateDownloadService, name) {
return writeToCachedFile(this@RemoteFileService, name) {
val progress = it / 1_000_000f
updateNotification(id) { notification ->
update(id) { notification ->
notification
.setProgress(maxRaw.toInt(), it.toInt(), false)
.setContentText(getString(R.string.download_progress, progress, max))
......@@ -130,51 +86,13 @@ abstract class SubstrateDownloadService : Service() {
}
}
private fun finish(file: File, subject: DownloadSubject) {
val currentNotification = notifications.remove(subject.hashCode()) ?: let {
manager?.cancel(subject.hashCode())
return
}
val notification = currentNotification.addActions(file, subject)
private fun finish(file: File, subject: DownloadSubject) = finishWork(subject.hashCode()) {
it.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()
updateForeground()
manager?.cancel(subject.hashCode())
manager?.notify(nextInt(), notification)
if (notifications.isEmpty()) {
stopForeground(true)
stopSelf()
}
}
private inline fun updateNotification(
id: Int,
body: (NotificationCompat.Builder) -> Unit = {}
) {
val notification = notifications.getOrPut(id) {
Notifications
.progress(this, "")
.setContentText(getString(R.string.download_local))
}
manager?.notify(id, notification.also(body).build())
if (notifications.size == 1) {
updateForeground()
}
}
private fun updateForeground() {
runCatching { notifications.keys.first() to notifications.values.first() }
.getOrNull()
?.let { startForeground(it.first, it.second.build()) }
}
// ---
......@@ -188,6 +106,7 @@ abstract class SubstrateDownloadService : Service() {
subject: DownloadSubject
): NotificationCompat.Builder
protected abstract fun map(subject: DownloadSubject, file: File): File
companion object {
const val ARG_URL = "arg_url"
......
......@@ -10,7 +10,7 @@ import com.skoumal.teanity.viewevents.ViewEvent
import com.topjohnwu.magisk.Config
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.databinding.FragmentReposBinding
import com.topjohnwu.magisk.model.download.CompoundDownloadService
import com.topjohnwu.magisk.model.download.DownloadService
import com.topjohnwu.magisk.model.entity.Repo
import com.topjohnwu.magisk.model.entity.internal.Configuration
import com.topjohnwu.magisk.model.entity.internal.DownloadSubject
......@@ -96,12 +96,11 @@ class ReposFragment : MagiskFragment<ModuleViewModel, FragmentReposBinding>(),
private fun installModule(item: Repo) {
val context = magiskActivity
fun download(install: Boolean) {
context.withExternalRW {
fun download(install: Boolean) = context.withExternalRW {
onSuccess {
DownloadService(context) {
val config = if (install) Configuration.Flash() else Configuration.Download
val subject = DownloadSubject.Module(item, config)
CompoundDownloadService.download(context, subject)
subject = DownloadSubject.Module(item, config)
}
}
}
......
......@@ -8,7 +8,7 @@ import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import com.topjohnwu.magisk.Const
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.model.download.CompoundDownloadService
import com.topjohnwu.magisk.model.download.DownloadService
import com.topjohnwu.magisk.model.entity.internal.Configuration
import com.topjohnwu.magisk.model.entity.internal.DownloadSubject
import com.topjohnwu.magisk.ui.base.MagiskActivity
......@@ -32,8 +32,9 @@ internal class InstallMethodDialog(activity: MagiskActivity<*, *>, options: List
@SuppressLint("MissingPermission")
private fun flash(activity: MagiskActivity<*, *>) = activity.withExternalRW {
onSuccess {
val subject = DownloadSubject.Magisk(Configuration.Flash.Primary)
CompoundDownloadService.download(context, subject)
DownloadService(context) {
subject = DownloadSubject.Magisk(Configuration.Flash.Primary)
}
}
}
......@@ -46,9 +47,10 @@ internal class InstallMethodDialog(activity: MagiskActivity<*, *>, options: List
.addCategory(Intent.CATEGORY_OPENABLE)
activity.startActivityForResult(intent, Const.ID.SELECT_BOOT) { resultCode, data ->
if (resultCode == Activity.RESULT_OK && data != null) {
DownloadService(activity) {
val safeData = data.data ?: Uri.EMPTY
val subject = DownloadSubject.Magisk(Configuration.Patch(safeData))
CompoundDownloadService.download(activity, subject)
subject = DownloadSubject.Magisk(Configuration.Patch(safeData))
}
}
}
}
......@@ -57,8 +59,9 @@ internal class InstallMethodDialog(activity: MagiskActivity<*, *>, options: List
@SuppressLint("MissingPermission")
private fun downloadOnly(activity: MagiskActivity<*, *>) = activity.withExternalRW {
onSuccess {
val subject = DownloadSubject.Magisk(Configuration.Download)
CompoundDownloadService.download(activity, subject)
DownloadService(activity) {
subject = DownloadSubject.Magisk(Configuration.Download)
}
}
}
......@@ -69,8 +72,9 @@ internal class InstallMethodDialog(activity: MagiskActivity<*, *>, options: List
.setMessage(R.string.install_inactive_slot_msg)
.setCancelable(true)
.setPositiveButton(R.string.yes) { _, _ ->
val subject = DownloadSubject.Magisk(Configuration.Flash.Secondary)
CompoundDownloadService.download(activity, subject)
DownloadService(activity) {
subject = DownloadSubject.Magisk(Configuration.Flash.Secondary)
}
}
.setNegativeButton(R.string.no_thanks, null)
.show()
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment