mirror of
https://github.com/topjohnwu/Magisk.git
synced 2024-12-21 23:47:39 +00:00
Split file processing into its own class
This commit is contained in:
parent
c512496847
commit
4eb4097b9b
@ -11,6 +11,7 @@ import androidx.core.content.getSystemService
|
|||||||
import com.topjohnwu.magisk.core.base.BaseJobService
|
import com.topjohnwu.magisk.core.base.BaseJobService
|
||||||
import com.topjohnwu.magisk.core.di.ServiceLocator
|
import com.topjohnwu.magisk.core.di.ServiceLocator
|
||||||
import com.topjohnwu.magisk.core.download.DownloadEngine
|
import com.topjohnwu.magisk.core.download.DownloadEngine
|
||||||
|
import com.topjohnwu.magisk.core.download.DownloadSession
|
||||||
import com.topjohnwu.magisk.core.download.Subject
|
import com.topjohnwu.magisk.core.download.Subject
|
||||||
import com.topjohnwu.magisk.view.Notifications
|
import com.topjohnwu.magisk.view.Notifications
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@ -25,7 +26,7 @@ class JobService : BaseJobService() {
|
|||||||
@TargetApi(value = 34)
|
@TargetApi(value = 34)
|
||||||
inner class Session(
|
inner class Session(
|
||||||
private var params: JobParameters
|
private var params: JobParameters
|
||||||
) : DownloadEngine.Session {
|
) : DownloadSession {
|
||||||
|
|
||||||
override val context get() = this@JobService
|
override val context get() = this@JobService
|
||||||
val engine = DownloadEngine(this)
|
val engine = DownloadEngine(this)
|
||||||
|
@ -7,9 +7,10 @@ import androidx.core.app.ServiceCompat
|
|||||||
import androidx.core.content.IntentCompat
|
import androidx.core.content.IntentCompat
|
||||||
import com.topjohnwu.magisk.core.base.BaseService
|
import com.topjohnwu.magisk.core.base.BaseService
|
||||||
import com.topjohnwu.magisk.core.download.DownloadEngine
|
import com.topjohnwu.magisk.core.download.DownloadEngine
|
||||||
|
import com.topjohnwu.magisk.core.download.DownloadSession
|
||||||
import com.topjohnwu.magisk.core.download.Subject
|
import com.topjohnwu.magisk.core.download.Subject
|
||||||
|
|
||||||
class Service : BaseService(), DownloadEngine.Session {
|
class Service : BaseService(), DownloadSession {
|
||||||
|
|
||||||
private var mEngine: DownloadEngine? = null
|
private var mEngine: DownloadEngine? = null
|
||||||
override val context get() = this
|
override val context get() = this
|
||||||
|
@ -7,7 +7,6 @@ import android.app.PendingIntent
|
|||||||
import android.app.job.JobInfo
|
import android.app.job.JobInfo
|
||||||
import android.app.job.JobScheduler
|
import android.app.job.JobScheduler
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.Uri
|
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
@ -16,7 +15,6 @@ import androidx.collection.isNotEmpty
|
|||||||
import androidx.core.content.getSystemService
|
import androidx.core.content.getSystemService
|
||||||
import androidx.lifecycle.LifecycleOwner
|
import androidx.lifecycle.LifecycleOwner
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import com.topjohnwu.magisk.StubApk
|
|
||||||
import com.topjohnwu.magisk.core.AppContext
|
import com.topjohnwu.magisk.core.AppContext
|
||||||
import com.topjohnwu.magisk.core.Const
|
import com.topjohnwu.magisk.core.Const
|
||||||
import com.topjohnwu.magisk.core.JobService
|
import com.topjohnwu.magisk.core.JobService
|
||||||
@ -25,30 +23,16 @@ import com.topjohnwu.magisk.core.base.IActivityExtension
|
|||||||
import com.topjohnwu.magisk.core.cmp
|
import com.topjohnwu.magisk.core.cmp
|
||||||
import com.topjohnwu.magisk.core.di.ServiceLocator
|
import com.topjohnwu.magisk.core.di.ServiceLocator
|
||||||
import com.topjohnwu.magisk.core.intent
|
import com.topjohnwu.magisk.core.intent
|
||||||
import com.topjohnwu.magisk.core.isRunningAsStub
|
|
||||||
import com.topjohnwu.magisk.core.ktx.cachedFile
|
|
||||||
import com.topjohnwu.magisk.core.ktx.copyAll
|
|
||||||
import com.topjohnwu.magisk.core.ktx.copyAndClose
|
|
||||||
import com.topjohnwu.magisk.core.ktx.set
|
import com.topjohnwu.magisk.core.ktx.set
|
||||||
import com.topjohnwu.magisk.core.ktx.withInOut
|
|
||||||
import com.topjohnwu.magisk.core.ktx.writeTo
|
|
||||||
import com.topjohnwu.magisk.core.tasks.AppMigration
|
|
||||||
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.utils.APKInstall
|
|
||||||
import com.topjohnwu.magisk.view.Notifications
|
import com.topjohnwu.magisk.view.Notifications
|
||||||
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 okhttp3.ResponseBody
|
||||||
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry
|
|
||||||
import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream
|
|
||||||
import org.apache.commons.compress.archivers.zip.ZipFile
|
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.io.IOException
|
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import java.io.OutputStream
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This class drives the execution of file downloads and notification management.
|
* This class drives the execution of file downloads and notification management.
|
||||||
@ -67,16 +51,7 @@ import java.io.OutputStream
|
|||||||
* For API 23 - 33, we use a foreground service as a session.
|
* For API 23 - 33, we use a foreground service as a session.
|
||||||
* For API 34 and higher, we use user-initiated job services as a session.
|
* For API 34 and higher, we use user-initiated job services as a session.
|
||||||
*/
|
*/
|
||||||
class DownloadEngine(
|
class DownloadEngine(session: DownloadSession) : DownloadSession by session, DownloadNotifier {
|
||||||
private val session: Session
|
|
||||||
) {
|
|
||||||
|
|
||||||
interface Session {
|
|
||||||
val context: Context
|
|
||||||
|
|
||||||
fun attachNotification(id: Int, builder: Notification.Builder)
|
|
||||||
fun onDownloadComplete()
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val ACTION = "com.topjohnwu.magisk.DOWNLOAD"
|
const val ACTION = "com.topjohnwu.magisk.DOWNLOAD"
|
||||||
@ -140,6 +115,7 @@ class DownloadEngine(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressLint("MissingPermission")
|
||||||
fun start(context: Context, subject: Subject) {
|
fun start(context: Context, subject: Subject) {
|
||||||
if (Build.VERSION.SDK_INT >= 34) {
|
if (Build.VERSION.SDK_INT >= 34) {
|
||||||
val scheduler = context.getSystemService<JobScheduler>()!!
|
val scheduler = context.getSystemService<JobScheduler>()!!
|
||||||
@ -163,16 +139,18 @@ class DownloadEngine(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val notifications = SparseArrayCompat<Notification.Builder>()
|
||||||
|
private var attachedId = -1
|
||||||
|
private val job = Job()
|
||||||
|
private val processor = DownloadProcessor(this)
|
||||||
|
private val network get() = ServiceLocator.networkService
|
||||||
|
|
||||||
fun download(subject: Subject) {
|
fun download(subject: Subject) {
|
||||||
notifyUpdate(subject.notifyId)
|
notifyUpdate(subject.notifyId)
|
||||||
CoroutineScope(job + Dispatchers.IO).launch {
|
CoroutineScope(job + Dispatchers.IO).launch {
|
||||||
try {
|
try {
|
||||||
val stream = network.fetchFile(subject.url).toProgressStream(subject)
|
val stream = network.fetchFile(subject.url).toProgressStream(subject)
|
||||||
when (subject) {
|
processor.handle(stream, subject)
|
||||||
is Subject.App -> handleApp(stream, subject)
|
|
||||||
is Subject.Module -> handleModule(stream, subject.file)
|
|
||||||
else -> stream.copyAndClose(subject.file.outputStream())
|
|
||||||
}
|
|
||||||
val activity = AppContext.foregroundActivity
|
val activity = AppContext.foregroundActivity
|
||||||
if (activity != null && subject.autoLaunch) {
|
if (activity != null && subject.autoLaunch) {
|
||||||
notifyRemove(subject.notifyId)
|
notifyRemove(subject.notifyId)
|
||||||
@ -190,16 +168,13 @@ class DownloadEngine(
|
|||||||
@Synchronized
|
@Synchronized
|
||||||
fun reattach() {
|
fun reattach() {
|
||||||
val builder = notifications[attachedId] ?: return
|
val builder = notifications[attachedId] ?: return
|
||||||
session.attachNotification(attachedId, builder)
|
attachNotification(attachedId, builder)
|
||||||
}
|
}
|
||||||
|
|
||||||
private val notifications = SparseArrayCompat<Notification.Builder>()
|
private fun attach(id: Int, notification: Notification.Builder) {
|
||||||
private var attachedId = -1
|
attachedId = id
|
||||||
|
attachNotification(id, notification)
|
||||||
private val job = Job()
|
}
|
||||||
|
|
||||||
private val context get() = session.context
|
|
||||||
private val network get() = ServiceLocator.networkService
|
|
||||||
|
|
||||||
private fun finalNotify(id: Int, editor: (Notification.Builder) -> Unit): Int {
|
private fun finalNotify(id: Int, editor: (Notification.Builder) -> Unit): Int {
|
||||||
val notification = notifyRemove(id)?.also(editor) ?: return -1
|
val notification = notifyRemove(id)?.also(editor) ?: return -1
|
||||||
@ -226,19 +201,14 @@ class DownloadEngine(
|
|||||||
subject.pendingIntent(context)?.let { intent -> it.setContentIntent(intent) }
|
subject.pendingIntent(context)?.let { intent -> it.setContentIntent(intent) }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun attachNotification(id: Int, notification: Notification.Builder) {
|
|
||||||
attachedId = id
|
|
||||||
session.attachNotification(id, notification)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Synchronized
|
@Synchronized
|
||||||
private fun notifyUpdate(id: Int, editor: (Notification.Builder) -> Unit = {}) {
|
override fun notifyUpdate(id: Int, editor: (Notification.Builder) -> Unit) {
|
||||||
val notification = (notifications[id] ?: Notifications.startProgress("").also {
|
val notification = (notifications[id] ?: Notifications.startProgress("").also {
|
||||||
notifications[id] = it
|
notifications[id] = it
|
||||||
}).apply(editor)
|
}).apply(editor)
|
||||||
|
|
||||||
if (attachedId < 0)
|
if (attachedId < 0)
|
||||||
attachNotification(id, notification)
|
attach(id, notification)
|
||||||
else
|
else
|
||||||
Notifications.mgr.notify(id, notification.build())
|
Notifications.mgr.notify(id, notification.build())
|
||||||
}
|
}
|
||||||
@ -258,11 +228,11 @@ class DownloadEngine(
|
|||||||
// There are still remaining notifications, pick one and attach to the session
|
// There are still remaining notifications, pick one and attach to the session
|
||||||
val anotherId = notifications.keyAt(0)
|
val anotherId = notifications.keyAt(0)
|
||||||
val notification = notifications.valueAt(0)
|
val notification = notifications.valueAt(0)
|
||||||
attachNotification(anotherId, notification)
|
attach(anotherId, notification)
|
||||||
} else {
|
} else {
|
||||||
// No more notifications left, terminate the session
|
// No more notifications left, terminate the session
|
||||||
attachedId = -1
|
attachedId = -1
|
||||||
session.onDownloadComplete()
|
onDownloadComplete()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -271,97 +241,6 @@ class DownloadEngine(
|
|||||||
return n
|
return n
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun handleApp(stream: InputStream, subject: Subject.App) {
|
|
||||||
val external = subject.file.outputStream()
|
|
||||||
|
|
||||||
if (isRunningAsStub) {
|
|
||||||
val updateApk = StubApk.update(context)
|
|
||||||
try {
|
|
||||||
// Download full APK to stub update path
|
|
||||||
stream.copyAndClose(TeeOutputStream(external, updateApk.outputStream()))
|
|
||||||
|
|
||||||
// Also upgrade stub
|
|
||||||
notifyUpdate(subject.notifyId) {
|
|
||||||
it.setProgress(0, 0, true)
|
|
||||||
.setContentTitle(context.getString(R.string.hide_app_title))
|
|
||||||
.setContentText("")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extract stub
|
|
||||||
val apk = context.cachedFile("stub.apk")
|
|
||||||
ZipFile.Builder().setFile(updateApk).get().use { zf ->
|
|
||||||
apk.delete()
|
|
||||||
zf.getInputStream(zf.getEntry("assets/stub.apk")).writeTo(apk)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Patch and install
|
|
||||||
subject.intent = AppMigration.upgradeStub(context, apk)
|
|
||||||
?: throw IOException("HideAPK patch error")
|
|
||||||
apk.delete()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
// If any error occurred, do not let stub load the new APK
|
|
||||||
updateApk.delete()
|
|
||||||
throw e
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
val session = APKInstall.startSession(context)
|
|
||||||
stream.copyAndClose(TeeOutputStream(external, session.openStream(context)))
|
|
||||||
subject.intent = session.waitIntent()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun handleModule(src: InputStream, file: Uri) {
|
|
||||||
val tmp = context.cachedFile("module.zip")
|
|
||||||
try {
|
|
||||||
// First download the entire zip into cache so we can process it
|
|
||||||
src.writeTo(tmp)
|
|
||||||
|
|
||||||
val input = ZipFile.Builder().setFile(tmp).get()
|
|
||||||
val output = ZipArchiveOutputStream(file.outputStream())
|
|
||||||
withInOut(input, output) { zin, zout ->
|
|
||||||
zout.putArchiveEntry(ZipArchiveEntry("META-INF/"))
|
|
||||||
zout.closeArchiveEntry()
|
|
||||||
zout.putArchiveEntry(ZipArchiveEntry("META-INF/com/"))
|
|
||||||
zout.closeArchiveEntry()
|
|
||||||
zout.putArchiveEntry(ZipArchiveEntry("META-INF/com/google/"))
|
|
||||||
zout.closeArchiveEntry()
|
|
||||||
zout.putArchiveEntry(ZipArchiveEntry("META-INF/com/google/android/"))
|
|
||||||
zout.closeArchiveEntry()
|
|
||||||
|
|
||||||
zout.putArchiveEntry(ZipArchiveEntry("META-INF/com/google/android/update-binary"))
|
|
||||||
context.assets.open("module_installer.sh").use { it.copyAll(zout) }
|
|
||||||
zout.closeArchiveEntry()
|
|
||||||
|
|
||||||
zout.putArchiveEntry(ZipArchiveEntry("META-INF/com/google/android/updater-script"))
|
|
||||||
zout.write("#MAGISK\n".toByteArray())
|
|
||||||
zout.closeArchiveEntry()
|
|
||||||
|
|
||||||
// Then simply copy all entries to output
|
|
||||||
zin.copyRawEntries(zout) { entry -> !entry.name.startsWith("META-INF") }
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
tmp.delete()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
||||||
|
@ -0,0 +1,122 @@
|
|||||||
|
package com.topjohnwu.magisk.core.download
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
|
import com.topjohnwu.magisk.StubApk
|
||||||
|
import com.topjohnwu.magisk.core.R
|
||||||
|
import com.topjohnwu.magisk.core.isRunningAsStub
|
||||||
|
import com.topjohnwu.magisk.core.ktx.cachedFile
|
||||||
|
import com.topjohnwu.magisk.core.ktx.copyAll
|
||||||
|
import com.topjohnwu.magisk.core.ktx.copyAndClose
|
||||||
|
import com.topjohnwu.magisk.core.ktx.withInOut
|
||||||
|
import com.topjohnwu.magisk.core.ktx.writeTo
|
||||||
|
import com.topjohnwu.magisk.core.tasks.AppMigration
|
||||||
|
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.outputStream
|
||||||
|
import com.topjohnwu.magisk.utils.APKInstall
|
||||||
|
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry
|
||||||
|
import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream
|
||||||
|
import org.apache.commons.compress.archivers.zip.ZipFile
|
||||||
|
import java.io.IOException
|
||||||
|
import java.io.InputStream
|
||||||
|
import java.io.OutputStream
|
||||||
|
|
||||||
|
class DownloadProcessor(notifier: DownloadNotifier) : DownloadNotifier by notifier {
|
||||||
|
|
||||||
|
suspend fun handle(stream: InputStream, subject: Subject) {
|
||||||
|
when (subject) {
|
||||||
|
is Subject.App -> handleApp(stream, subject)
|
||||||
|
is Subject.Module -> handleModule(stream, subject.file)
|
||||||
|
else -> stream.copyAndClose(subject.file.outputStream())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun handleApp(stream: InputStream, subject: Subject.App) {
|
||||||
|
val external = subject.file.outputStream()
|
||||||
|
|
||||||
|
if (isRunningAsStub) {
|
||||||
|
val updateApk = StubApk.update(context)
|
||||||
|
try {
|
||||||
|
// Download full APK to stub update path
|
||||||
|
stream.copyAndClose(TeeOutputStream(external, updateApk.outputStream()))
|
||||||
|
|
||||||
|
// Also upgrade stub
|
||||||
|
notifyUpdate(subject.notifyId) {
|
||||||
|
it.setProgress(0, 0, true)
|
||||||
|
.setContentTitle(context.getString(R.string.hide_app_title))
|
||||||
|
.setContentText("")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract stub
|
||||||
|
val apk = context.cachedFile("stub.apk")
|
||||||
|
ZipFile.Builder().setFile(updateApk).get().use { zf ->
|
||||||
|
apk.delete()
|
||||||
|
zf.getInputStream(zf.getEntry("assets/stub.apk")).writeTo(apk)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Patch and install
|
||||||
|
subject.intent = AppMigration.upgradeStub(context, apk)
|
||||||
|
?: throw IOException("HideAPK patch error")
|
||||||
|
apk.delete()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
// If any error occurred, do not let stub load the new APK
|
||||||
|
updateApk.delete()
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
val session = APKInstall.startSession(context)
|
||||||
|
stream.copyAndClose(TeeOutputStream(external, session.openStream(context)))
|
||||||
|
subject.intent = session.waitIntent()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun handleModule(src: InputStream, file: Uri) {
|
||||||
|
val tmp = context.cachedFile("module.zip")
|
||||||
|
try {
|
||||||
|
// First download the entire zip into cache so we can process it
|
||||||
|
src.writeTo(tmp)
|
||||||
|
|
||||||
|
val input = ZipFile.Builder().setFile(tmp).get()
|
||||||
|
val output = ZipArchiveOutputStream(file.outputStream())
|
||||||
|
withInOut(input, output) { zin, zout ->
|
||||||
|
zout.putArchiveEntry(ZipArchiveEntry("META-INF/"))
|
||||||
|
zout.closeArchiveEntry()
|
||||||
|
zout.putArchiveEntry(ZipArchiveEntry("META-INF/com/"))
|
||||||
|
zout.closeArchiveEntry()
|
||||||
|
zout.putArchiveEntry(ZipArchiveEntry("META-INF/com/google/"))
|
||||||
|
zout.closeArchiveEntry()
|
||||||
|
zout.putArchiveEntry(ZipArchiveEntry("META-INF/com/google/android/"))
|
||||||
|
zout.closeArchiveEntry()
|
||||||
|
|
||||||
|
zout.putArchiveEntry(ZipArchiveEntry("META-INF/com/google/android/update-binary"))
|
||||||
|
context.assets.open("module_installer.sh").use { it.copyAll(zout) }
|
||||||
|
zout.closeArchiveEntry()
|
||||||
|
|
||||||
|
zout.putArchiveEntry(ZipArchiveEntry("META-INF/com/google/android/updater-script"))
|
||||||
|
zout.write("#MAGISK\n".toByteArray())
|
||||||
|
zout.closeArchiveEntry()
|
||||||
|
|
||||||
|
// Then simply copy all entries to output
|
||||||
|
zin.copyRawEntries(zout) { entry -> !entry.name.startsWith("META-INF") }
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
tmp.delete()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,15 @@
|
|||||||
|
package com.topjohnwu.magisk.core.download
|
||||||
|
|
||||||
|
import android.app.Notification
|
||||||
|
import android.content.Context
|
||||||
|
|
||||||
|
interface DownloadSession {
|
||||||
|
val context: Context
|
||||||
|
fun attachNotification(id: Int, builder: Notification.Builder)
|
||||||
|
fun onDownloadComplete()
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DownloadNotifier {
|
||||||
|
val context: Context
|
||||||
|
fun notifyUpdate(id: Int, editor: (Notification.Builder) -> Unit = {})
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user