Commit 084e0a73 authored by topjohnwu's avatar topjohnwu

Cleanup DownloadService

parent 10f991b8
package com.topjohnwu.magisk.core.download package com.topjohnwu.magisk.core.download
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.Notification
import android.app.PendingIntent 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.net.Uri
import android.os.Build import android.os.Build
import android.os.IBinder import androidx.core.net.toFile
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.MutableLiveData import com.topjohnwu.magisk.PhoenixActivity
import com.topjohnwu.magisk.R import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.StubApk
import com.topjohnwu.magisk.core.ActivityTracker import com.topjohnwu.magisk.core.ActivityTracker
import com.topjohnwu.magisk.core.base.BaseService import com.topjohnwu.magisk.core.Info
import com.topjohnwu.magisk.core.intent import com.topjohnwu.magisk.core.intent
import com.topjohnwu.magisk.core.utils.ProgressInputStream import com.topjohnwu.magisk.core.isRunningAsStub
import com.topjohnwu.magisk.di.ServiceLocator import com.topjohnwu.magisk.core.tasks.HideAPK
import com.topjohnwu.magisk.ktx.synchronized import com.topjohnwu.magisk.core.utils.MediaStoreUtils.outputStream
import com.topjohnwu.magisk.view.Notifications import com.topjohnwu.magisk.ktx.copyAndClose
import com.topjohnwu.magisk.view.Notifications.mgr import com.topjohnwu.magisk.ktx.forEach
import com.topjohnwu.magisk.ktx.withStreams
import com.topjohnwu.magisk.ktx.writeTo
import com.topjohnwu.magisk.utils.APKInstall
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import okhttp3.ResponseBody
import timber.log.Timber import timber.log.Timber
import java.io.File
import java.io.InputStream import java.io.InputStream
import java.io.OutputStream
import java.util.zip.ZipEntry
import java.util.zip.ZipInputStream
import java.util.zip.ZipOutputStream
class DownloadService : BaseService() { class DownloadService : NotificationService() {
private val hasNotifications get() = notifications.isNotEmpty()
private val notifications = HashMap<Int, Notification.Builder>().synchronized()
private val job = Job() private val job = Job()
val service get() = ServiceLocator.networkService
// -- Service overrides
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 { doDownload(it) } intent.getParcelableExtra<Subject>(SUBJECT_KEY)?.let { download(it) }
return START_NOT_STICKY return START_NOT_STICKY
} }
override fun onTaskRemoved(rootIntent: Intent?) {
super.onTaskRemoved(rootIntent)
notifications.forEach { mgr.cancel(it.key) }
notifications.clear()
}
override fun onDestroy() { override fun onDestroy() {
super.onDestroy() super.onDestroy()
job.cancel() job.cancel()
} }
// -- Download logic private fun download(subject: Subject) {
private fun doDownload(subject: Subject) {
update(subject.notifyId) update(subject.notifyId)
val coroutineScope = CoroutineScope(job + Dispatchers.IO) val coroutineScope = CoroutineScope(job + Dispatchers.IO)
coroutineScope.launch { coroutineScope.launch {
try { try {
val stream = service.fetchFile(subject.url).toProgressStream(subject) val stream = service.fetchFile(subject.url).toProgressStream(subject)
when (subject) { when (subject) {
is Subject.Manager -> handleAPK(subject, stream) is Subject.App -> handleApp(stream, subject)
is Subject.Module -> stream.toModule(subject.file, assets.open("module_installer.sh")) is Subject.Module -> handleModule(stream, subject.file)
} }
val activity = ActivityTracker.foreground val activity = ActivityTracker.foreground
if (activity != null && subject.autoStart) { if (activity != null && subject.autoStart) {
...@@ -83,88 +75,88 @@ class DownloadService : BaseService() { ...@@ -83,88 +75,88 @@ class DownloadService : BaseService() {
} }
} }
private fun ResponseBody.toProgressStream(subject: Subject): InputStream { private suspend fun handleApp(stream: InputStream, subject: Subject.App) {
val max = contentLength() fun write(output: OutputStream) {
val total = max.toFloat() / 1048576 val external = subject.externalFile.outputStream()
val id = subject.notifyId stream.copyAndClose(TeeOutputStream(external, output))
}
update(id) { it.setContentTitle(subject.title) }
if (isRunningAsStub) {
return ProgressInputStream(byteStream()) { val apk = subject.file.toFile()
val progress = it.toFloat() / 1048576 val id = subject.notifyId
update(id) { notification -> write(StubApk.update(this).outputStream())
if (max > 0) { if (Info.stub!!.version < subject.stub.versionCode) {
broadcast(progress / total, subject) // Also upgrade stub
notification update(id) {
.setProgress(max.toInt(), it.toInt(), false) it.setProgress(0, 0, true)
.setContentText("%.2f / %.2f MB".format(progress, total)) .setContentTitle(getString(R.string.hide_app_title))
} else { .setContentText("")
broadcast(-1f, subject)
notification.setContentText("%.2f MB / ??".format(progress))
} }
service.fetchFile(subject.stub.link).byteStream().writeTo(apk)
val patched = File(apk.parent, "patched.apk")
HideAPK.patch(this, apk, patched, packageName, applicationInfo.nonLocalizedLabel)
apk.delete()
patched.renameTo(apk)
} else {
val clz = Info.stub!!.classToComponent["PHOENIX"]!!
PhoenixActivity.rebirth(this, clz)
return
} }
} }
val receiver = APKInstall.register(this, null, null)
write(APKInstall.openStream(this, false))
subject.intent = receiver.waitIntent()
} }
// --- Notification management private fun handleModule(src: InputStream, file: Uri) {
val input = ZipInputStream(src.buffered())
private fun notifyFail(subject: Subject) = finalNotify(subject.notifyId) { val output = ZipOutputStream(file.outputStream().buffered())
broadcast(-2f, subject)
it.setContentText(getString(R.string.download_file_error)) withStreams(input, output) { zin, zout ->
.setSmallIcon(android.R.drawable.stat_notify_error) zout.putNextEntry(ZipEntry("META-INF/"))
.setOngoing(false) zout.putNextEntry(ZipEntry("META-INF/com/"))
} zout.putNextEntry(ZipEntry("META-INF/com/google/"))
zout.putNextEntry(ZipEntry("META-INF/com/google/android/"))
private fun notifyFinish(subject: Subject) = finalNotify(subject.notifyId) { zout.putNextEntry(ZipEntry("META-INF/com/google/android/update-binary"))
broadcast(1f, subject) assets.open("module_installer.sh").copyTo(zout)
it.setContentTitle(subject.title)
.setContentText(getString(R.string.download_complete)) zout.putNextEntry(ZipEntry("META-INF/com/google/android/updater-script"))
.setSmallIcon(android.R.drawable.stat_sys_download_done) zout.write("#MAGISK\n".toByteArray())
.setProgress(0, 0, false)
.setOngoing(false) zin.forEach { entry ->
.setAutoCancel(true) val path = entry.name
subject.pendingIntent(this)?.let { intent -> it.setContentIntent(intent) } if (path.isNotEmpty() && !path.startsWith("META-INF")) {
} zout.putNextEntry(ZipEntry(path))
if (!entry.isDirectory) {
private fun finalNotify(id: Int, editor: (Notification.Builder) -> Unit): Int { zin.copyTo(zout)
val notification = remove(id)?.also(editor) ?: return -1 }
val newId = Notifications.nextId() }
mgr.notify(newId, notification.build()) }
return newId }
}
private fun create() = Notifications.progress(this, "")
fun update(id: Int, editor: (Notification.Builder) -> Unit = {}) {
val wasEmpty = !hasNotifications
val notification = notifications.getOrPut(id, ::create).also(editor)
if (wasEmpty)
updateForeground()
else
mgr.notify(id, notification.build())
}
private fun remove(id: Int): Notification.Builder? {
val n = notifications.remove(id)?.also { updateForeground() }
mgr.cancel(id)
return n
} }
private fun updateForeground() { private class TeeOutputStream(
if (hasNotifications) { private val o1: OutputStream,
val (id, notification) = notifications.entries.first() private val o2: OutputStream
startForeground(id, notification.build()) ) : OutputStream() {
} else { override fun write(b: Int) {
stopForeground(false) o1.write(b)
o2.write(b)
}
override fun write(b: ByteArray?, off: Int, len: Int) {
o1.write(b, off, len)
o2.write(b, off, len)
}
override fun close() {
o1.close()
o2.close()
} }
} }
companion object { companion object {
private const val SUBJECT_KEY = "download_subject" private const val SUBJECT_KEY = "subject"
private const val REQUEST_CODE = 1 private const val REQUEST_CODE = 1
private val progressBroadcast = MutableLiveData<Pair<Float, Subject>?>()
fun observeProgress(owner: LifecycleOwner, callback: (Float, Subject) -> Unit) { fun observeProgress(owner: LifecycleOwner, callback: (Float, Subject) -> Unit) {
progressBroadcast.value = null progressBroadcast.value = null
progressBroadcast.observe(owner) { progressBroadcast.observe(owner) {
...@@ -173,10 +165,6 @@ class DownloadService : BaseService() { ...@@ -173,10 +165,6 @@ class DownloadService : BaseService() {
} }
} }
private fun broadcast(progress: Float, subject: Subject) {
progressBroadcast.postValue(progress to subject)
}
private fun intent(context: Context, subject: Subject) = private fun intent(context: Context, subject: Subject) =
context.intent<DownloadService>().putExtra(SUBJECT_KEY, subject) context.intent<DownloadService>().putExtra(SUBJECT_KEY, subject)
...@@ -200,5 +188,4 @@ class DownloadService : BaseService() { ...@@ -200,5 +188,4 @@ class DownloadService : BaseService() {
} }
} }
} }
} }
package com.topjohnwu.magisk.core.download
import androidx.core.net.toFile
import com.topjohnwu.magisk.PhoenixActivity
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.StubApk
import com.topjohnwu.magisk.core.Info
import com.topjohnwu.magisk.core.isRunningAsStub
import com.topjohnwu.magisk.core.tasks.HideAPK
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.outputStream
import com.topjohnwu.magisk.ktx.copyAndClose
import com.topjohnwu.magisk.ktx.writeTo
import com.topjohnwu.magisk.utils.APKInstall
import java.io.File
import java.io.InputStream
import java.io.OutputStream
private class TeeOutputStream(
private val o1: OutputStream,
private val o2: OutputStream
) : OutputStream() {
override fun write(b: Int) {
o1.write(b)
o2.write(b)
}
override fun write(b: ByteArray?, off: Int, len: Int) {
o1.write(b, off, len)
o2.write(b, off, len)
}
override fun close() {
o1.close()
o2.close()
}
}
suspend fun DownloadService.handleAPK(subject: Subject.Manager, stream: InputStream) {
fun write(output: OutputStream) {
val external = subject.externalFile.outputStream()
stream.copyAndClose(TeeOutputStream(external, output))
}
if (isRunningAsStub) {
val apk = subject.file.toFile()
val id = subject.notifyId
write(StubApk.update(this).outputStream())
if (Info.stub!!.version < subject.stub.versionCode) {
// Also upgrade stub
update(id) {
it.setProgress(0, 0, true)
.setContentTitle(getString(R.string.hide_app_title))
.setContentText("")
}
service.fetchFile(subject.stub.link).byteStream().writeTo(apk)
val patched = File(apk.parent, "patched.apk")
HideAPK.patch(this, apk, patched, packageName, applicationInfo.nonLocalizedLabel)
apk.delete()
patched.renameTo(apk)
} else {
val clz = Info.stub!!.classToComponent["PHOENIX"]!!
PhoenixActivity.rebirth(this, clz)
return
}
}
val receiver = APKInstall.register(this, null, null)
write(APKInstall.openStream(this, false))
subject.intent = receiver.waitIntent()
}
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")))
zin.forEach { entry ->
val path = entry.name
if (path.isNotEmpty() && !path.startsWith("META-INF")) {
zout.putNextEntry(ZipEntry(path))
if (!entry.isDirectory) {
zin.copyTo(zout)
}
}
}
}
}
package com.topjohnwu.magisk.core.download
import android.app.Notification
import android.content.Intent
import android.os.IBinder
import androidx.lifecycle.MutableLiveData
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.core.base.BaseService
import com.topjohnwu.magisk.core.utils.ProgressInputStream
import com.topjohnwu.magisk.di.ServiceLocator
import com.topjohnwu.magisk.ktx.synchronized
import com.topjohnwu.magisk.view.Notifications
import okhttp3.ResponseBody
import java.io.InputStream
open class NotificationService : BaseService() {
private val notifications = HashMap<Int, Notification.Builder>().synchronized()
protected val hasNotifications get() = notifications.isNotEmpty()
protected val service get() = ServiceLocator.networkService
override fun onBind(intent: Intent?): IBinder? = null
override fun onTaskRemoved(rootIntent: Intent?) {
super.onTaskRemoved(rootIntent)
notifications.forEach { Notifications.mgr.cancel(it.key) }
notifications.clear()
}
protected fun ResponseBody.toProgressStream(subject: Subject): InputStream {
val max = contentLength()
val total = max.toFloat() / 1048576
val id = subject.notifyId
update(id) { it.setContentTitle(subject.title) }
return ProgressInputStream(byteStream()) {
val progress = it.toFloat() / 1048576
update(id) { notification ->
if (max > 0) {
broadcast(progress / total, subject)
notification
.setProgress(max.toInt(), it.toInt(), false)
.setContentText("%.2f / %.2f MB".format(progress, total))
} else {
broadcast(-1f, subject)
notification.setContentText("%.2f MB / ??".format(progress))
}
}
}
}
private fun finalNotify(id: Int, editor: (Notification.Builder) -> Unit): Int {
val notification = remove(id)?.also(editor) ?: return -1
val newId = Notifications.nextId()
Notifications.mgr.notify(newId, notification.build())
return newId
}
protected fun notifyFail(subject: Subject) = finalNotify(subject.notifyId) {
broadcast(-2f, subject)
it.setContentText(getString(R.string.download_file_error))
.setSmallIcon(android.R.drawable.stat_notify_error)
.setOngoing(false)
}
protected fun notifyFinish(subject: Subject) = finalNotify(subject.notifyId) {
broadcast(1f, subject)
it.setContentTitle(subject.title)
.setContentText(getString(R.string.download_complete))
.setSmallIcon(android.R.drawable.stat_sys_download_done)
.setProgress(0, 0, false)
.setOngoing(false)
.setAutoCancel(true)
subject.pendingIntent(this)?.let { intent -> it.setContentIntent(intent) }
}
private fun create() = Notifications.progress(this, "")
private fun updateForeground() {
if (hasNotifications) {
val (id, notification) = notifications.entries.first()
startForeground(id, notification.build())
} else {
stopForeground(false)
}
}
protected fun update(id: Int, editor: (Notification.Builder) -> Unit = {}) {
val wasEmpty = !hasNotifications
val notification = notifications.getOrPut(id, ::create).also(editor)
if (wasEmpty)
updateForeground()
else
Notifications.mgr.notify(id, notification.build())
}
protected fun remove(id: Int): Notification.Builder? {
val n = notifications.remove(id)?.also { updateForeground() }
Notifications.mgr.cancel(id)
return n
}
companion object {
@JvmStatic
protected val progressBroadcast = MutableLiveData<Pair<Float, Subject>?>()
private fun broadcast(progress: Float, subject: Subject) {
progressBroadcast.postValue(progress to subject)
}
}
}
...@@ -56,7 +56,7 @@ sealed class Subject : Parcelable { ...@@ -56,7 +56,7 @@ sealed class Subject : Parcelable {
} }
@Parcelize @Parcelize
class Manager( class App(
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() override val notifyId: Int = Notifications.nextId()
......
...@@ -29,7 +29,7 @@ class ManagerInstallDialog : MarkDownDialog() { ...@@ -29,7 +29,7 @@ class ManagerInstallDialog : MarkDownDialog() {
setCancelable(true) setCancelable(true)
setButton(MagiskDialog.ButtonType.POSITIVE) { setButton(MagiskDialog.ButtonType.POSITIVE) {
text = R.string.install text = R.string.install
onClick { DownloadService.start(context, Subject.Manager()) } onClick { DownloadService.start(context, Subject.App()) }
} }
setButton(MagiskDialog.ButtonType.NEGATIVE) { setButton(MagiskDialog.ButtonType.NEGATIVE) {
text = android.R.string.cancel text = android.R.string.cancel
......
...@@ -10,7 +10,7 @@ import com.topjohnwu.magisk.arch.* ...@@ -10,7 +10,7 @@ import com.topjohnwu.magisk.arch.*
import com.topjohnwu.magisk.core.Config import com.topjohnwu.magisk.core.Config
import com.topjohnwu.magisk.core.Info import com.topjohnwu.magisk.core.Info
import com.topjohnwu.magisk.core.download.Subject import com.topjohnwu.magisk.core.download.Subject
import com.topjohnwu.magisk.core.download.Subject.Manager import com.topjohnwu.magisk.core.download.Subject.App
import com.topjohnwu.magisk.data.repository.NetworkService import com.topjohnwu.magisk.data.repository.NetworkService
import com.topjohnwu.magisk.databinding.itemBindingOf import com.topjohnwu.magisk.databinding.itemBindingOf
import com.topjohnwu.magisk.databinding.set import com.topjohnwu.magisk.databinding.set
...@@ -112,7 +112,7 @@ class HomeViewModel( ...@@ -112,7 +112,7 @@ class HomeViewModel(
}.publish() }.publish()
fun onProgressUpdate(progress: Float, subject: Subject) { fun onProgressUpdate(progress: Float, subject: Subject) {
if (subject is Manager) if (subject is App)
stateManagerProgress = progress.times(100f).roundToInt() stateManagerProgress = progress.times(100f).roundToInt()
} }
......
...@@ -48,7 +48,7 @@ object Notifications { ...@@ -48,7 +48,7 @@ object Notifications {
} }
fun managerUpdate(context: Context) { fun managerUpdate(context: Context) {
val intent = DownloadService.getPendingIntent(context, Subject.Manager()) val intent = DownloadService.getPendingIntent(context, Subject.App())
val builder = updateBuilder(context) val builder = updateBuilder(context)
.setContentTitle(context.getString(R.string.magisk_update_title)) .setContentTitle(context.getString(R.string.magisk_update_title))
......
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