mirror of
https://github.com/topjohnwu/Magisk.git
synced 2025-05-20 21:18:20 +00:00
Cleanup DownloadService
This commit is contained in:
parent
e660fabc57
commit
6dbd8baa7e
@ -39,8 +39,6 @@ object Const {
|
|||||||
}
|
}
|
||||||
|
|
||||||
object ID {
|
object ID {
|
||||||
// notifications
|
|
||||||
const val APK_UPDATE_NOTIFICATION_ID = 5
|
|
||||||
const val JOB_SERVICE_ID = 7
|
const val JOB_SERVICE_ID = 7
|
||||||
const val UPDATE_NOTIFICATION_CHANNEL = "update"
|
const val UPDATE_NOTIFICATION_CHANNEL = "update"
|
||||||
const val PROGRESS_NOTIFICATION_CHANNEL = "progress"
|
const val PROGRESS_NOTIFICATION_CHANNEL = "progress"
|
||||||
|
@ -3,66 +3,47 @@ package com.topjohnwu.magisk.core.download
|
|||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.app.Notification
|
import android.app.Notification
|
||||||
import android.app.PendingIntent
|
import android.app.PendingIntent
|
||||||
|
import android.app.PendingIntent.*
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
import androidx.core.net.toFile
|
|
||||||
import androidx.lifecycle.LifecycleOwner
|
import androidx.lifecycle.LifecycleOwner
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import com.topjohnwu.magisk.R
|
import com.topjohnwu.magisk.R
|
||||||
import com.topjohnwu.magisk.arch.BaseUIActivity
|
|
||||||
import com.topjohnwu.magisk.core.ForegroundTracker
|
import com.topjohnwu.magisk.core.ForegroundTracker
|
||||||
import com.topjohnwu.magisk.core.base.BaseService
|
import com.topjohnwu.magisk.core.base.BaseService
|
||||||
import com.topjohnwu.magisk.core.download.Action.Flash
|
|
||||||
import com.topjohnwu.magisk.core.download.Subject.Manager
|
|
||||||
import com.topjohnwu.magisk.core.download.Subject.Module
|
|
||||||
import com.topjohnwu.magisk.core.intent
|
import com.topjohnwu.magisk.core.intent
|
||||||
|
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.outputStream
|
||||||
import com.topjohnwu.magisk.core.utils.ProgressInputStream
|
import com.topjohnwu.magisk.core.utils.ProgressInputStream
|
||||||
import com.topjohnwu.magisk.di.ServiceLocator
|
import com.topjohnwu.magisk.di.ServiceLocator
|
||||||
import com.topjohnwu.magisk.ui.flash.FlashFragment
|
import com.topjohnwu.magisk.ktx.copyAndClose
|
||||||
import com.topjohnwu.magisk.utils.APKInstall
|
import com.topjohnwu.magisk.ktx.synchronized
|
||||||
import com.topjohnwu.magisk.view.Notifications
|
import com.topjohnwu.magisk.view.Notifications
|
||||||
import com.topjohnwu.superuser.internal.UiThreadHandler
|
import com.topjohnwu.magisk.view.Notifications.mgr
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.*
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.cancel
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import okhttp3.ResponseBody
|
import okhttp3.ResponseBody
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import kotlin.collections.HashMap
|
import kotlin.collections.HashMap
|
||||||
import kotlin.random.Random.Default.nextInt
|
|
||||||
|
|
||||||
class DownloadService : BaseService() {
|
class DownloadService : BaseService() {
|
||||||
|
|
||||||
private val context get() = this
|
|
||||||
private val hasNotifications get() = notifications.isNotEmpty()
|
private val hasNotifications get() = notifications.isNotEmpty()
|
||||||
private val notifications = Collections.synchronizedMap(HashMap<Int, Notification.Builder>())
|
private val notifications = HashMap<Int, Notification.Builder>().synchronized()
|
||||||
private val coroutineScope = CoroutineScope(Dispatchers.IO)
|
private val job = Job()
|
||||||
|
|
||||||
val service get() = ServiceLocator.networkService
|
val service get() = ServiceLocator.networkService
|
||||||
private val mgr get() = Notifications.mgr
|
|
||||||
|
|
||||||
// -- Service overrides
|
// -- Service overrides
|
||||||
|
|
||||||
override fun onBind(intent: Intent?): IBinder? = null
|
override fun onBind(intent: Intent?): IBinder? = null
|
||||||
|
|
||||||
override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
|
override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
|
||||||
intent.getParcelableExtra<Subject>(SUBJECT_KEY)?.let { subject ->
|
intent.getParcelableExtra<Subject>(SUBJECT_KEY)?.let { doDownload(it) }
|
||||||
update(subject.notifyID())
|
return START_NOT_STICKY
|
||||||
coroutineScope.launch {
|
|
||||||
try {
|
|
||||||
subject.startDownload()
|
|
||||||
} catch (e: IOException) {
|
|
||||||
Timber.e(e)
|
|
||||||
notifyFail(subject)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return START_REDELIVER_INTENT
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onTaskRemoved(rootIntent: Intent?) {
|
override fun onTaskRemoved(rootIntent: Intent?) {
|
||||||
@ -73,29 +54,40 @@ class DownloadService : BaseService() {
|
|||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
coroutineScope.cancel()
|
job.cancel()
|
||||||
}
|
}
|
||||||
|
|
||||||
// -- Download logic
|
// -- Download logic
|
||||||
|
|
||||||
private suspend fun Subject.startDownload() {
|
private fun doDownload(subject: Subject) {
|
||||||
val stream = service.fetchFile(url).toProgressStream(this)
|
update(subject.notifyId)
|
||||||
when (this) {
|
val coroutineScope = CoroutineScope(job + Dispatchers.IO)
|
||||||
is Module -> // Download and process on-the-fly
|
coroutineScope.launch {
|
||||||
stream.toModule(file, service.fetchInstaller().byteStream())
|
try {
|
||||||
is Manager -> handleAPK(this, stream)
|
val stream = service.fetchFile(subject.url).toProgressStream(subject)
|
||||||
|
when (subject) {
|
||||||
|
is Subject.Manager -> handleAPK(subject, stream)
|
||||||
|
else -> stream.copyAndClose(subject.file.outputStream())
|
||||||
|
}
|
||||||
|
if (ForegroundTracker.hasForeground) {
|
||||||
|
remove(subject.notifyId)
|
||||||
|
subject.pendingIntent(this@DownloadService).send()
|
||||||
|
} else {
|
||||||
|
notifyFinish(subject)
|
||||||
}
|
}
|
||||||
val newId = notifyFinish(this)
|
|
||||||
if (ForegroundTracker.hasForeground)
|
|
||||||
onFinish(this, newId)
|
|
||||||
if (!hasNotifications)
|
if (!hasNotifications)
|
||||||
stopSelf()
|
stopSelf()
|
||||||
|
} catch (e: IOException) {
|
||||||
|
Timber.e(e)
|
||||||
|
notifyFail(subject)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun ResponseBody.toProgressStream(subject: Subject): InputStream {
|
private fun ResponseBody.toProgressStream(subject: Subject): InputStream {
|
||||||
val max = contentLength()
|
val max = contentLength()
|
||||||
val total = max.toFloat() / 1048576
|
val total = max.toFloat() / 1048576
|
||||||
val id = subject.notifyID()
|
val id = subject.notifyId
|
||||||
|
|
||||||
update(id) { it.setContentTitle(subject.title) }
|
update(id) { it.setContentTitle(subject.title) }
|
||||||
|
|
||||||
@ -115,18 +107,18 @@ class DownloadService : BaseService() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Notifications
|
// --- Notification management
|
||||||
|
|
||||||
private fun notifyFail(subject: Subject) = finalNotify(subject.notifyID()) {
|
private fun notifyFail(subject: Subject) = finalNotify(subject.notifyId) {
|
||||||
broadcast(-2f, subject)
|
broadcast(-2f, subject)
|
||||||
it.setContentText(getString(R.string.download_file_error))
|
it.setContentText(getString(R.string.download_file_error))
|
||||||
.setSmallIcon(android.R.drawable.stat_notify_error)
|
.setSmallIcon(android.R.drawable.stat_notify_error)
|
||||||
.setOngoing(false)
|
.setOngoing(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun notifyFinish(subject: Subject) = finalNotify(subject.notifyID()) {
|
private fun notifyFinish(subject: Subject) = finalNotify(subject.notifyId) {
|
||||||
broadcast(1f, subject)
|
broadcast(1f, subject)
|
||||||
it.setIntent(subject)
|
it.setContentIntent(subject.pendingIntent(this))
|
||||||
.setContentTitle(subject.title)
|
.setContentTitle(subject.title)
|
||||||
.setContentText(getString(R.string.download_complete))
|
.setContentText(getString(R.string.download_complete))
|
||||||
.setSmallIcon(android.R.drawable.stat_sys_download_done)
|
.setSmallIcon(android.R.drawable.stat_sys_download_done)
|
||||||
@ -135,17 +127,14 @@ class DownloadService : BaseService() {
|
|||||||
.setAutoCancel(true)
|
.setAutoCancel(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun finalNotify(
|
private fun finalNotify(id: Int, editor: (Notification.Builder) -> Unit): Int {
|
||||||
id: Int,
|
val notification = remove(id)?.also(editor) ?: return -1
|
||||||
editor: (Notification.Builder) -> Notification.Builder
|
val newId = Notifications.nextId()
|
||||||
) : Int {
|
|
||||||
val notification = remove(id)?.run(editor) ?: return -1
|
|
||||||
val newId = nextInt()
|
|
||||||
mgr.notify(newId, notification.build())
|
mgr.notify(newId, notification.build())
|
||||||
return newId
|
return newId
|
||||||
}
|
}
|
||||||
|
|
||||||
fun create() = Notifications.progress(this, "")
|
private fun create() = Notifications.progress(this, "")
|
||||||
|
|
||||||
fun update(id: Int, editor: (Notification.Builder) -> Unit = {}) {
|
fun update(id: Int, editor: (Notification.Builder) -> Unit = {}) {
|
||||||
val wasEmpty = !hasNotifications
|
val wasEmpty = !hasNotifications
|
||||||
@ -171,49 +160,9 @@ class DownloadService : BaseService() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun Notification.Builder.setIntent(subject: Subject) = when (subject) {
|
|
||||||
is Module -> setIntent(subject)
|
|
||||||
is Manager -> setIntent(subject)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun Notification.Builder.setIntent(subject: Module)
|
|
||||||
= when (subject.action) {
|
|
||||||
Flash -> setContentIntent(FlashFragment.installIntent(context, subject.file))
|
|
||||||
else -> setContentIntent(Intent())
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun Notification.Builder.setIntent(subject: Manager) =
|
|
||||||
setContentIntent(APKInstall.installIntent(context, subject.file.toFile()))
|
|
||||||
|
|
||||||
@SuppressLint("InlinedApi")
|
|
||||||
private fun Notification.Builder.setContentIntent(intent: Intent) =
|
|
||||||
setContentIntent(PendingIntent.getActivity(context, nextInt(), intent,
|
|
||||||
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_ONE_SHOT))
|
|
||||||
|
|
||||||
// -- Post download processing
|
|
||||||
|
|
||||||
private fun onFinish(subject: Subject, id: Int) = when (subject) {
|
|
||||||
is Module -> subject.onFinish(id)
|
|
||||||
is Manager -> subject.onFinish(id)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun Module.onFinish(id: Int) = when (action) {
|
|
||||||
Flash -> {
|
|
||||||
UiThreadHandler.run {
|
|
||||||
(ForegroundTracker.foreground as? BaseUIActivity<*, *>)
|
|
||||||
?.navigation?.navigate(FlashFragment.install(file, id))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else -> Unit
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun Manager.onFinish(id: Int) {
|
|
||||||
remove(id)
|
|
||||||
APKInstall.install(context, file.toFile())
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val SUBJECT_KEY = "download_subject"
|
private const val SUBJECT_KEY = "download_subject"
|
||||||
|
private const val REQUEST_CODE = 1
|
||||||
|
|
||||||
private val progressBroadcast = MutableLiveData<Pair<Float, Subject>?>()
|
private val progressBroadcast = MutableLiveData<Pair<Float, Subject>?>()
|
||||||
|
|
||||||
@ -233,13 +182,13 @@ class DownloadService : BaseService() {
|
|||||||
context.intent<DownloadService>().putExtra(SUBJECT_KEY, subject)
|
context.intent<DownloadService>().putExtra(SUBJECT_KEY, subject)
|
||||||
|
|
||||||
@SuppressLint("InlinedApi")
|
@SuppressLint("InlinedApi")
|
||||||
fun pendingIntent(context: Context, subject: Subject): PendingIntent {
|
fun getPendingIntent(context: Context, subject: Subject): PendingIntent {
|
||||||
|
val flag = FLAG_IMMUTABLE or FLAG_UPDATE_CURRENT or FLAG_ONE_SHOT
|
||||||
|
val intent = intent(context, subject)
|
||||||
return if (Build.VERSION.SDK_INT >= 26) {
|
return if (Build.VERSION.SDK_INT >= 26) {
|
||||||
PendingIntent.getForegroundService(context, nextInt(), intent(context, subject),
|
getForegroundService(context, REQUEST_CODE, intent, flag)
|
||||||
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT)
|
|
||||||
} else {
|
} else {
|
||||||
PendingIntent.getService(context, nextInt(), intent(context, subject),
|
getService(context, REQUEST_CODE, intent, flag)
|
||||||
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
package com.topjohnwu.magisk.core.download
|
package com.topjohnwu.magisk.core.download
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import androidx.core.net.toFile
|
import androidx.core.net.toFile
|
||||||
import com.topjohnwu.magisk.DynAPK
|
import com.topjohnwu.magisk.DynAPK
|
||||||
import com.topjohnwu.magisk.R
|
import com.topjohnwu.magisk.R
|
||||||
@ -8,29 +7,14 @@ import com.topjohnwu.magisk.core.Info
|
|||||||
import com.topjohnwu.magisk.core.isRunningAsStub
|
import com.topjohnwu.magisk.core.isRunningAsStub
|
||||||
import com.topjohnwu.magisk.core.tasks.HideAPK
|
import com.topjohnwu.magisk.core.tasks.HideAPK
|
||||||
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.outputStream
|
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.outputStream
|
||||||
|
import com.topjohnwu.magisk.ktx.copyAndClose
|
||||||
import com.topjohnwu.magisk.ktx.relaunchApp
|
import com.topjohnwu.magisk.ktx.relaunchApp
|
||||||
import com.topjohnwu.magisk.ktx.withStreams
|
|
||||||
import com.topjohnwu.magisk.ktx.writeTo
|
import com.topjohnwu.magisk.ktx.writeTo
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import java.io.OutputStream
|
import java.io.OutputStream
|
||||||
|
|
||||||
private fun Context.patch(apk: File) {
|
private class TeeOutputStream(
|
||||||
val patched = File(apk.parent, "patched.apk")
|
|
||||||
HideAPK.patch(this, apk, patched, packageName, applicationInfo.nonLocalizedLabel)
|
|
||||||
apk.delete()
|
|
||||||
patched.renameTo(apk)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun DownloadService.notifyHide(id: Int) {
|
|
||||||
update(id) {
|
|
||||||
it.setProgress(0, 0, true)
|
|
||||||
.setContentTitle(getString(R.string.hide_app_title))
|
|
||||||
.setContentText("")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private class DupOutputStream(
|
|
||||||
private val o1: OutputStream,
|
private val o1: OutputStream,
|
||||||
private val o2: OutputStream
|
private val o2: OutputStream
|
||||||
) : OutputStream() {
|
) : OutputStream() {
|
||||||
@ -50,20 +34,26 @@ private class DupOutputStream(
|
|||||||
|
|
||||||
suspend fun DownloadService.handleAPK(subject: Subject.Manager, stream: InputStream) {
|
suspend fun DownloadService.handleAPK(subject: Subject.Manager, stream: InputStream) {
|
||||||
fun write(output: OutputStream) {
|
fun write(output: OutputStream) {
|
||||||
val ext = subject.externalFile.outputStream()
|
val external = subject.externalFile.outputStream()
|
||||||
val o = DupOutputStream(ext, output)
|
stream.copyAndClose(TeeOutputStream(external, output))
|
||||||
withStreams(stream, o) { src, out -> src.copyTo(out) }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isRunningAsStub) {
|
if (isRunningAsStub) {
|
||||||
val apk = subject.file.toFile()
|
val apk = subject.file.toFile()
|
||||||
val id = subject.notifyID()
|
val id = subject.notifyId
|
||||||
write(DynAPK.update(this).outputStream())
|
write(DynAPK.update(this).outputStream())
|
||||||
if (Info.stub!!.version < subject.stub.versionCode) {
|
if (Info.stub!!.version < subject.stub.versionCode) {
|
||||||
// Also upgrade stub
|
// Also upgrade stub
|
||||||
notifyHide(id)
|
update(id) {
|
||||||
|
it.setProgress(0, 0, true)
|
||||||
|
.setContentTitle(getString(R.string.hide_app_title))
|
||||||
|
.setContentText("")
|
||||||
|
}
|
||||||
service.fetchFile(subject.stub.link).byteStream().writeTo(apk)
|
service.fetchFile(subject.stub.link).byteStream().writeTo(apk)
|
||||||
patch(apk)
|
val patched = File(apk.parent, "patched.apk")
|
||||||
|
HideAPK.patch(this, apk, patched, packageName, applicationInfo.nonLocalizedLabel)
|
||||||
|
apk.delete()
|
||||||
|
patched.renameTo(apk)
|
||||||
} else {
|
} else {
|
||||||
// Simply relaunch the app
|
// Simply relaunch the app
|
||||||
stopSelf()
|
stopSelf()
|
||||||
|
@ -1,43 +0,0 @@
|
|||||||
package com.topjohnwu.magisk.core.download
|
|
||||||
|
|
||||||
import android.net.Uri
|
|
||||||
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.outputStream
|
|
||||||
import com.topjohnwu.magisk.ktx.forEach
|
|
||||||
import com.topjohnwu.magisk.ktx.withStreams
|
|
||||||
import java.io.InputStream
|
|
||||||
import java.util.zip.ZipEntry
|
|
||||||
import java.util.zip.ZipInputStream
|
|
||||||
import java.util.zip.ZipOutputStream
|
|
||||||
|
|
||||||
fun InputStream.toModule(file: Uri, installer: InputStream) {
|
|
||||||
|
|
||||||
val input = ZipInputStream(buffered())
|
|
||||||
val output = ZipOutputStream(file.outputStream().buffered())
|
|
||||||
|
|
||||||
withStreams(input, output) { zin, zout ->
|
|
||||||
zout.putNextEntry(ZipEntry("META-INF/"))
|
|
||||||
zout.putNextEntry(ZipEntry("META-INF/com/"))
|
|
||||||
zout.putNextEntry(ZipEntry("META-INF/com/google/"))
|
|
||||||
zout.putNextEntry(ZipEntry("META-INF/com/google/android/"))
|
|
||||||
zout.putNextEntry(ZipEntry("META-INF/com/google/android/update-binary"))
|
|
||||||
installer.copyTo(zout)
|
|
||||||
|
|
||||||
zout.putNextEntry(ZipEntry("META-INF/com/google/android/updater-script"))
|
|
||||||
zout.write("#MAGISK\n".toByteArray(charset("UTF-8")))
|
|
||||||
|
|
||||||
var off = -1
|
|
||||||
zin.forEach { entry ->
|
|
||||||
if (off < 0) {
|
|
||||||
off = entry.name.indexOf('/') + 1
|
|
||||||
}
|
|
||||||
|
|
||||||
val path = entry.name.substring(off)
|
|
||||||
if (path.isNotEmpty() && !path.startsWith("META-INF")) {
|
|
||||||
zout.putNextEntry(ZipEntry(path))
|
|
||||||
if (!entry.isDirectory) {
|
|
||||||
zin.copyTo(zout)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,7 +1,12 @@
|
|||||||
package com.topjohnwu.magisk.core.download
|
package com.topjohnwu.magisk.core.download
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.app.PendingIntent
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
|
import androidx.core.net.toFile
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import com.topjohnwu.magisk.core.Info
|
import com.topjohnwu.magisk.core.Info
|
||||||
import com.topjohnwu.magisk.core.model.MagiskJson
|
import com.topjohnwu.magisk.core.model.MagiskJson
|
||||||
@ -10,22 +15,33 @@ import com.topjohnwu.magisk.core.model.module.OnlineModule
|
|||||||
import com.topjohnwu.magisk.core.utils.MediaStoreUtils
|
import com.topjohnwu.magisk.core.utils.MediaStoreUtils
|
||||||
import com.topjohnwu.magisk.di.AppContext
|
import com.topjohnwu.magisk.di.AppContext
|
||||||
import com.topjohnwu.magisk.ktx.cachedFile
|
import com.topjohnwu.magisk.ktx.cachedFile
|
||||||
|
import com.topjohnwu.magisk.ui.flash.FlashFragment
|
||||||
|
import com.topjohnwu.magisk.utils.APKInstall
|
||||||
|
import com.topjohnwu.magisk.view.Notifications
|
||||||
import kotlinx.parcelize.IgnoredOnParcel
|
import kotlinx.parcelize.IgnoredOnParcel
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
|
|
||||||
private fun cachedFile(name: String) = AppContext.cachedFile(name).apply { delete() }.toUri()
|
private fun cachedFile(name: String) = AppContext.cachedFile(name).apply { delete() }.toUri()
|
||||||
|
|
||||||
|
enum class Action {
|
||||||
|
Flash,
|
||||||
|
Download
|
||||||
|
}
|
||||||
|
|
||||||
sealed class Subject : Parcelable {
|
sealed class Subject : Parcelable {
|
||||||
|
|
||||||
abstract val url: String
|
abstract val url: String
|
||||||
abstract val file: Uri
|
abstract val file: Uri
|
||||||
abstract val action: Action
|
|
||||||
abstract val title: String
|
abstract val title: String
|
||||||
|
abstract val notifyId: Int
|
||||||
|
|
||||||
|
abstract fun pendingIntent(context: Context): PendingIntent
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
class Module(
|
class Module(
|
||||||
val module: OnlineModule,
|
val module: OnlineModule,
|
||||||
override val action: Action
|
val action: Action,
|
||||||
|
override val notifyId: Int = Notifications.nextId()
|
||||||
) : Subject() {
|
) : Subject() {
|
||||||
override val url: String get() = module.zip_url
|
override val url: String get() = module.zip_url
|
||||||
override val title: String get() = module.downloadFilename
|
override val title: String get() = module.downloadFilename
|
||||||
@ -34,14 +50,19 @@ sealed class Subject : Parcelable {
|
|||||||
override val file by lazy {
|
override val file by lazy {
|
||||||
MediaStoreUtils.getFile(title).uri
|
MediaStoreUtils.getFile(title).uri
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun pendingIntent(context: Context) = when (action) {
|
||||||
|
Action.Flash -> FlashFragment.installIntent(context, file)
|
||||||
|
else -> Intent().toPending(context)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
class Manager(
|
class Manager(
|
||||||
private val json: MagiskJson = Info.remote.magisk,
|
private val json: MagiskJson = Info.remote.magisk,
|
||||||
val stub: StubJson = Info.remote.stub
|
val stub: StubJson = Info.remote.stub,
|
||||||
|
override val notifyId: Int = Notifications.nextId()
|
||||||
) : Subject() {
|
) : Subject() {
|
||||||
override val action get() = Action.Download
|
|
||||||
override val title: String get() = "Magisk-${json.version}(${json.versionCode})"
|
override val title: String get() = "Magisk-${json.version}(${json.versionCode})"
|
||||||
override val url: String get() = json.link
|
override val url: String get() = json.link
|
||||||
|
|
||||||
@ -51,15 +72,14 @@ sealed class Subject : Parcelable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val externalFile get() = MediaStoreUtils.getFile("$title.apk").uri
|
val externalFile get() = MediaStoreUtils.getFile("$title.apk").uri
|
||||||
|
|
||||||
|
override fun pendingIntent(context: Context) =
|
||||||
|
APKInstall.installIntent(context, file.toFile()).toPending(context)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun notifyID() = hashCode()
|
@SuppressLint("InlinedApi")
|
||||||
|
protected fun Intent.toPending(context: Context): PendingIntent {
|
||||||
|
return PendingIntent.getActivity(context, notifyId, this,
|
||||||
|
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_ONE_SHOT)
|
||||||
}
|
}
|
||||||
|
|
||||||
sealed class Action : Parcelable {
|
|
||||||
@Parcelize
|
|
||||||
object Flash : Action()
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
object Download : Action()
|
|
||||||
}
|
}
|
||||||
|
@ -5,7 +5,7 @@ import java.io.InputStream
|
|||||||
|
|
||||||
class ProgressInputStream(
|
class ProgressInputStream(
|
||||||
base: InputStream,
|
base: InputStream,
|
||||||
val progressEmitter: (Long) -> Unit = {}
|
val progressEmitter: (Long) -> Unit
|
||||||
) : FilterInputStream(base) {
|
) : FilterInputStream(base) {
|
||||||
|
|
||||||
private var bytesRead = 0L
|
private var bytesRead = 0L
|
||||||
@ -40,4 +40,9 @@ class ProgressInputStream(
|
|||||||
}
|
}
|
||||||
return sz
|
return sz
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun close() {
|
||||||
|
super.close()
|
||||||
|
progressEmitter(bytesRead)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -14,8 +14,8 @@ class ModuleInstallDialog(private val item: OnlineModule) : DialogEvent() {
|
|||||||
with(dialog) {
|
with(dialog) {
|
||||||
|
|
||||||
fun download(install: Boolean) {
|
fun download(install: Boolean) {
|
||||||
val config = if (install) Action.Flash else Action.Download
|
val action = if (install) Action.Flash else Action.Download
|
||||||
val subject = Subject.Module(item, config)
|
val subject = Subject.Module(item, action)
|
||||||
DownloadService.start(context, subject)
|
DownloadService.start(context, subject)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -19,9 +19,6 @@ inline fun ZipInputStream.forEach(callback: (ZipEntry) -> Unit) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun InputStream.writeTo(file: File) =
|
|
||||||
withStreams(this, file.outputStream()) { reader, writer -> reader.copyTo(writer) }
|
|
||||||
|
|
||||||
inline fun <In : InputStream, Out : OutputStream> withStreams(
|
inline fun <In : InputStream, Out : OutputStream> withStreams(
|
||||||
inStream: In,
|
inStream: In,
|
||||||
outStream: Out,
|
outStream: Out,
|
||||||
@ -34,18 +31,19 @@ inline fun <In : InputStream, Out : OutputStream> withStreams(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun <T> MutableList<T>.update(newList: List<T>) {
|
fun InputStream.copyAndClose(out: OutputStream) = withStreams(this, out) { i, o -> i.copyTo(o) }
|
||||||
clear()
|
|
||||||
addAll(newList)
|
fun InputStream.writeTo(file: File) = copyAndClose(file.outputStream())
|
||||||
}
|
|
||||||
|
|
||||||
operator fun <E> SparseArrayCompat<E>.set(key: Int, value: E) {
|
operator fun <E> SparseArrayCompat<E>.set(key: Int, value: E) {
|
||||||
put(key, value)
|
put(key, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun <T> MutableList<T>.synchronized() = Collections.synchronizedList(this)
|
fun <T> MutableList<T>.synchronized(): MutableList<T> = Collections.synchronizedList(this)
|
||||||
fun <T> MutableSet<T>.synchronized() = Collections.synchronizedSet(this)
|
|
||||||
fun <K, V> MutableMap<K, V>.synchronized() = Collections.synchronizedMap(this)
|
fun <T> MutableSet<T>.synchronized(): MutableSet<T> = Collections.synchronizedSet(this)
|
||||||
|
|
||||||
|
fun <K, V> MutableMap<K, V>.synchronized(): MutableMap<K, V> = Collections.synchronizedMap(this)
|
||||||
|
|
||||||
fun SimpleDateFormat.parseOrNull(date: String) =
|
fun SimpleDateFormat.parseOrNull(date: String) =
|
||||||
runCatching { parse(date) }.onFailure { Timber.e(it) }.getOrNull()
|
runCatching { parse(date) }.onFailure { Timber.e(it) }.getOrNull()
|
||||||
|
@ -111,16 +111,14 @@ class FlashFragment : BaseUIFragment<FlashViewModel, FragmentFlashMd2Binding>()
|
|||||||
|
|
||||||
/* Installing is understood as flashing modules / zips */
|
/* Installing is understood as flashing modules / zips */
|
||||||
|
|
||||||
fun installIntent(context: Context, file: Uri, id: Int = -1) = FlashFragmentArgs(
|
fun installIntent(context: Context, file: Uri) = FlashFragmentArgs(
|
||||||
action = Const.Value.FLASH_ZIP,
|
action = Const.Value.FLASH_ZIP,
|
||||||
additionalData = file,
|
additionalData = file,
|
||||||
dismissId = id
|
|
||||||
).let { createIntent(context, it) }
|
).let { createIntent(context, it) }
|
||||||
|
|
||||||
fun install(file: Uri, id: Int) = MainDirections.actionFlashFragment(
|
fun install(file: Uri) = MainDirections.actionFlashFragment(
|
||||||
action = Const.Value.FLASH_ZIP,
|
action = Const.Value.FLASH_ZIP,
|
||||||
additionalData = file,
|
additionalData = file,
|
||||||
dismissId = id
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -20,7 +20,6 @@ import com.topjohnwu.magisk.databinding.itemBindingOf
|
|||||||
import com.topjohnwu.magisk.databinding.set
|
import com.topjohnwu.magisk.databinding.set
|
||||||
import com.topjohnwu.magisk.events.SnackbarEvent
|
import com.topjohnwu.magisk.events.SnackbarEvent
|
||||||
import com.topjohnwu.magisk.ktx.*
|
import com.topjohnwu.magisk.ktx.*
|
||||||
import com.topjohnwu.magisk.view.Notifications
|
|
||||||
import com.topjohnwu.superuser.CallbackList
|
import com.topjohnwu.superuser.CallbackList
|
||||||
import com.topjohnwu.superuser.Shell
|
import com.topjohnwu.superuser.Shell
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@ -50,9 +49,7 @@ class FlashViewModel : BaseViewModel() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun startFlashing() {
|
fun startFlashing() {
|
||||||
val (action, uri, id) = args
|
val (action, uri) = args
|
||||||
if (id != -1)
|
|
||||||
Notifications.mgr.cancel(id)
|
|
||||||
|
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
val result = when (action) {
|
val result = when (action) {
|
||||||
|
@ -70,4 +70,8 @@ class HomeFragment : BaseUIFragment<HomeViewModel, FragmentHomeMd2Binding>() {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
viewModel.stateManagerProgress = 0
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -8,19 +8,22 @@ import android.os.Build.VERSION.SDK_INT
|
|||||||
import androidx.core.content.getSystemService
|
import androidx.core.content.getSystemService
|
||||||
import androidx.core.graphics.drawable.toIcon
|
import androidx.core.graphics.drawable.toIcon
|
||||||
import com.topjohnwu.magisk.R
|
import com.topjohnwu.magisk.R
|
||||||
import com.topjohnwu.magisk.core.Const
|
|
||||||
import com.topjohnwu.magisk.core.Const.ID.PROGRESS_NOTIFICATION_CHANNEL
|
import com.topjohnwu.magisk.core.Const.ID.PROGRESS_NOTIFICATION_CHANNEL
|
||||||
import com.topjohnwu.magisk.core.Const.ID.UPDATE_NOTIFICATION_CHANNEL
|
import com.topjohnwu.magisk.core.Const.ID.UPDATE_NOTIFICATION_CHANNEL
|
||||||
import com.topjohnwu.magisk.core.download.DownloadService
|
import com.topjohnwu.magisk.core.download.DownloadService
|
||||||
import com.topjohnwu.magisk.core.download.Subject
|
import com.topjohnwu.magisk.core.download.Subject
|
||||||
import com.topjohnwu.magisk.di.AppContext
|
import com.topjohnwu.magisk.di.AppContext
|
||||||
import com.topjohnwu.magisk.ktx.getBitmap
|
import com.topjohnwu.magisk.ktx.getBitmap
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger
|
||||||
|
|
||||||
@Suppress("DEPRECATION")
|
@Suppress("DEPRECATION")
|
||||||
object Notifications {
|
object Notifications {
|
||||||
|
|
||||||
val mgr by lazy { AppContext.getSystemService<NotificationManager>()!! }
|
val mgr by lazy { AppContext.getSystemService<NotificationManager>()!! }
|
||||||
|
|
||||||
|
private const val APK_UPDATE_NOTIFICATION_ID = 5
|
||||||
|
private val nextId = AtomicInteger(APK_UPDATE_NOTIFICATION_ID)
|
||||||
|
|
||||||
fun setup(context: Context) {
|
fun setup(context: Context) {
|
||||||
if (SDK_INT >= 26) {
|
if (SDK_INT >= 26) {
|
||||||
var channel = NotificationChannel(UPDATE_NOTIFICATION_CHANNEL,
|
var channel = NotificationChannel(UPDATE_NOTIFICATION_CHANNEL,
|
||||||
@ -46,7 +49,7 @@ object Notifications {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun managerUpdate(context: Context) {
|
fun managerUpdate(context: Context) {
|
||||||
val intent = DownloadService.pendingIntent(context, Subject.Manager())
|
val intent = DownloadService.getPendingIntent(context, Subject.Manager())
|
||||||
|
|
||||||
val builder = updateBuilder(context)
|
val builder = updateBuilder(context)
|
||||||
.setContentTitle(context.getString(R.string.magisk_update_title))
|
.setContentTitle(context.getString(R.string.magisk_update_title))
|
||||||
@ -54,7 +57,7 @@ object Notifications {
|
|||||||
.setAutoCancel(true)
|
.setAutoCancel(true)
|
||||||
.setContentIntent(intent)
|
.setContentIntent(intent)
|
||||||
|
|
||||||
mgr.notify(Const.ID.APK_UPDATE_NOTIFICATION_ID, builder.build())
|
mgr.notify(APK_UPDATE_NOTIFICATION_ID, builder.build())
|
||||||
}
|
}
|
||||||
|
|
||||||
fun progress(context: Context, title: CharSequence): Notification.Builder {
|
fun progress(context: Context, title: CharSequence): Notification.Builder {
|
||||||
@ -69,4 +72,6 @@ object Notifications {
|
|||||||
.setOngoing(true)
|
.setOngoing(true)
|
||||||
return builder
|
return builder
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun nextId() = nextId.incrementAndGet()
|
||||||
}
|
}
|
||||||
|
@ -51,11 +51,6 @@
|
|||||||
app:argType="android.net.Uri"
|
app:argType="android.net.Uri"
|
||||||
app:nullable="true" />
|
app:nullable="true" />
|
||||||
|
|
||||||
<argument
|
|
||||||
android:name="dismiss_id"
|
|
||||||
android:defaultValue="-1"
|
|
||||||
app:argType="integer" />
|
|
||||||
|
|
||||||
</fragment>
|
</fragment>
|
||||||
|
|
||||||
<fragment
|
<fragment
|
||||||
|
Loading…
x
Reference in New Issue
Block a user