mirror of
https://github.com/topjohnwu/Magisk.git
synced 2025-02-17 15:28:28 +00:00
Isolate download logic from service lifecycle
This commit is contained in:
parent
154121f3dd
commit
c5d34670c4
@ -61,7 +61,7 @@
|
|||||||
</receiver>
|
</receiver>
|
||||||
|
|
||||||
<service
|
<service
|
||||||
android:name=".core.download.DownloadService"
|
android:name=".core.Service"
|
||||||
android:exported="false"
|
android:exported="false"
|
||||||
android:foregroundServiceType="dataSync" />
|
android:foregroundServiceType="dataSync" />
|
||||||
|
|
||||||
|
40
app/src/main/java/com/topjohnwu/magisk/core/Service.kt
Normal file
40
app/src/main/java/com/topjohnwu/magisk/core/Service.kt
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
package com.topjohnwu.magisk.core
|
||||||
|
|
||||||
|
import android.app.Notification
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Build
|
||||||
|
import androidx.core.app.ServiceCompat
|
||||||
|
import androidx.core.content.IntentCompat
|
||||||
|
import com.topjohnwu.magisk.core.base.BaseService
|
||||||
|
import com.topjohnwu.magisk.core.download.DownloadManager
|
||||||
|
import com.topjohnwu.magisk.core.download.Subject
|
||||||
|
|
||||||
|
class Service : BaseService(), DownloadManager.Session {
|
||||||
|
|
||||||
|
private lateinit var dm: DownloadManager
|
||||||
|
override val context get() = this
|
||||||
|
|
||||||
|
override fun onCreate() {
|
||||||
|
super.onCreate()
|
||||||
|
dm = DownloadManager(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
|
||||||
|
if (intent.action == DownloadManager.ACTION) {
|
||||||
|
IntentCompat
|
||||||
|
.getParcelableExtra(intent, DownloadManager.SUBJECT_KEY, Subject::class.java)
|
||||||
|
?.let { dm.download(it) }
|
||||||
|
}
|
||||||
|
return START_NOT_STICKY
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun attach(id: Int, builder: Notification.Builder) {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S)
|
||||||
|
builder.setForegroundServiceBehavior(Notification.FOREGROUND_SERVICE_IMMEDIATE)
|
||||||
|
startForeground(id, builder.build())
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun stop() {
|
||||||
|
ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE)
|
||||||
|
}
|
||||||
|
}
|
@ -2,30 +2,41 @@ package com.topjohnwu.magisk.core.download
|
|||||||
|
|
||||||
import android.Manifest
|
import android.Manifest
|
||||||
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.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
|
import androidx.collection.SparseArrayCompat
|
||||||
|
import androidx.collection.isNotEmpty
|
||||||
import androidx.core.net.toFile
|
import androidx.core.net.toFile
|
||||||
import androidx.lifecycle.LifecycleOwner
|
import androidx.lifecycle.LifecycleOwner
|
||||||
|
import androidx.lifecycle.MutableLiveData
|
||||||
import com.topjohnwu.magisk.R
|
import com.topjohnwu.magisk.R
|
||||||
import com.topjohnwu.magisk.StubApk
|
import com.topjohnwu.magisk.StubApk
|
||||||
import com.topjohnwu.magisk.core.ActivityTracker
|
import com.topjohnwu.magisk.core.ActivityTracker
|
||||||
import com.topjohnwu.magisk.core.Info
|
import com.topjohnwu.magisk.core.Info
|
||||||
import com.topjohnwu.magisk.core.base.BaseActivity
|
import com.topjohnwu.magisk.core.base.BaseActivity
|
||||||
|
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.isRunningAsStub
|
||||||
import com.topjohnwu.magisk.core.ktx.*
|
import com.topjohnwu.magisk.core.ktx.copyAndClose
|
||||||
|
import com.topjohnwu.magisk.core.ktx.forEach
|
||||||
|
import com.topjohnwu.magisk.core.ktx.selfLaunchIntent
|
||||||
|
import com.topjohnwu.magisk.core.ktx.set
|
||||||
|
import com.topjohnwu.magisk.core.ktx.withStreams
|
||||||
|
import com.topjohnwu.magisk.core.ktx.writeTo
|
||||||
import com.topjohnwu.magisk.core.tasks.HideAPK
|
import com.topjohnwu.magisk.core.tasks.HideAPK
|
||||||
import com.topjohnwu.magisk.core.utils.MediaStoreUtils
|
import com.topjohnwu.magisk.core.utils.MediaStoreUtils
|
||||||
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.outputStream
|
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.outputStream
|
||||||
|
import com.topjohnwu.magisk.core.utils.ProgressInputStream
|
||||||
import com.topjohnwu.magisk.utils.APKInstall
|
import com.topjohnwu.magisk.utils.APKInstall
|
||||||
|
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 timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.io.ByteArrayInputStream
|
import java.io.ByteArrayInputStream
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
@ -37,27 +48,77 @@ import java.util.zip.ZipFile
|
|||||||
import java.util.zip.ZipInputStream
|
import java.util.zip.ZipInputStream
|
||||||
import java.util.zip.ZipOutputStream
|
import java.util.zip.ZipOutputStream
|
||||||
|
|
||||||
class DownloadService : NotificationService() {
|
class DownloadManager(
|
||||||
|
private val session: Session
|
||||||
|
) {
|
||||||
|
|
||||||
private val job = Job()
|
interface Session {
|
||||||
|
val context: Context
|
||||||
|
|
||||||
override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
|
fun attach(id: Int, notification: Notification.Builder)
|
||||||
intent.getParcelableExtra<Subject>(SUBJECT_KEY)?.let { download(it) }
|
fun stop()
|
||||||
return START_NOT_STICKY
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroy() {
|
companion object {
|
||||||
job.cancel()
|
const val ACTION = "com.topjohnwu.magisk.DOWNLOAD"
|
||||||
|
const val SUBJECT_KEY = "subject"
|
||||||
|
private const val REQUEST_CODE = 1
|
||||||
|
|
||||||
|
private val progressBroadcast = MutableLiveData<Pair<Float, Subject>?>()
|
||||||
|
|
||||||
|
private fun broadcast(progress: Float, subject: Subject) {
|
||||||
|
progressBroadcast.postValue(progress to subject)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun observeProgress(owner: LifecycleOwner, callback: (Float, Subject) -> Unit) {
|
||||||
|
progressBroadcast.value = null
|
||||||
|
progressBroadcast.observe(owner) {
|
||||||
|
val (progress, subject) = it ?: return@observe
|
||||||
|
callback(progress, subject)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createIntent(context: Context, subject: Subject) =
|
||||||
|
context.intent<com.topjohnwu.magisk.core.Service>()
|
||||||
|
.setAction(ACTION)
|
||||||
|
.putExtra(SUBJECT_KEY, subject)
|
||||||
|
|
||||||
|
@SuppressLint("InlinedApi")
|
||||||
|
fun getPendingIntent(context: Context, subject: Subject): PendingIntent {
|
||||||
|
val flag = PendingIntent.FLAG_IMMUTABLE or
|
||||||
|
PendingIntent.FLAG_UPDATE_CURRENT or
|
||||||
|
PendingIntent.FLAG_ONE_SHOT
|
||||||
|
val intent = createIntent(context, subject)
|
||||||
|
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
PendingIntent.getForegroundService(context, REQUEST_CODE, intent, flag)
|
||||||
|
} else {
|
||||||
|
PendingIntent.getService(context, REQUEST_CODE, intent, flag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("InlinedApi")
|
||||||
|
fun start(activity: BaseActivity, subject: Subject) {
|
||||||
|
activity.withPermission(Manifest.permission.POST_NOTIFICATIONS) {
|
||||||
|
// Always download regardless of notification permission status
|
||||||
|
val app = activity.applicationContext
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
app.startForegroundService(createIntent(app, subject))
|
||||||
|
} else {
|
||||||
|
app.startService(createIntent(app, subject))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private 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 = service.fetchFile(subject.url).toProgressStream(subject)
|
val stream = network.fetchFile(subject.url).toProgressStream(subject)
|
||||||
when (subject) {
|
when (subject) {
|
||||||
is Subject.App -> handleApp(stream, subject)
|
is Subject.App -> handleApp(stream, subject)
|
||||||
is Subject.Module -> handleModule(stream, subject.file)
|
is Subject.Module -> handleModule(stream, subject.file)
|
||||||
|
is Subject.Test -> stream.copyAndClose(subject.file.outputStream())
|
||||||
}
|
}
|
||||||
val activity = ActivityTracker.foreground
|
val activity = ActivityTracker.foreground
|
||||||
if (activity != null && subject.autoLaunch) {
|
if (activity != null && subject.autoLaunch) {
|
||||||
@ -67,15 +128,99 @@ class DownloadService : NotificationService() {
|
|||||||
notifyFinish(subject)
|
notifyFinish(subject)
|
||||||
}
|
}
|
||||||
subject.postDownload?.invoke()
|
subject.postDownload?.invoke()
|
||||||
if (!hasNotifications)
|
|
||||||
stopSelf()
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Timber.e(e)
|
Timber.e(e)
|
||||||
notifyFail(subject)
|
notifyFail(subject)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
synchronized(this@DownloadManager) {
|
||||||
|
if (notifications.isEmpty)
|
||||||
|
session.stop()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val notifications = SparseArrayCompat<Notification.Builder>()
|
||||||
|
private var attachedId = -1
|
||||||
|
|
||||||
|
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 {
|
||||||
|
val notification = notifyRemove(id)?.also(editor) ?: return -1
|
||||||
|
val newId = Notifications.nextId()
|
||||||
|
Notifications.mgr.notify(newId, notification.build())
|
||||||
|
return newId
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun notifyFail(subject: Subject) = finalNotify(subject.notifyId) {
|
||||||
|
broadcast(-2f, subject)
|
||||||
|
it.setContentText(context.getString(R.string.download_file_error))
|
||||||
|
.setSmallIcon(android.R.drawable.stat_notify_error)
|
||||||
|
.setOngoing(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun notifyFinish(subject: Subject) = finalNotify(subject.notifyId) {
|
||||||
|
broadcast(1f, subject)
|
||||||
|
it.setContentTitle(subject.title)
|
||||||
|
.setContentText(context.getString(R.string.download_complete))
|
||||||
|
.setSmallIcon(android.R.drawable.stat_sys_download_done)
|
||||||
|
.setProgress(0, 0, false)
|
||||||
|
.setOngoing(false)
|
||||||
|
.setAutoCancel(true)
|
||||||
|
subject.pendingIntent(context)?.let { intent -> it.setContentIntent(intent) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun attachNotification(id: Int, notification: Notification.Builder) {
|
||||||
|
attachedId = id
|
||||||
|
session.attach(id, notification)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
private fun notifyUpdate(id: Int, editor: (Notification.Builder) -> Unit = {}) {
|
||||||
|
val notification = (notifications[id] ?: Notifications.startProgress("").also {
|
||||||
|
notifications[id] = it
|
||||||
|
}).apply(editor)
|
||||||
|
|
||||||
|
if (attachedId < 0)
|
||||||
|
attachNotification(id, notification)
|
||||||
|
else
|
||||||
|
Notifications.mgr.notify(id, notification.build())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
private fun notifyRemove(id: Int): Notification.Builder? {
|
||||||
|
val idx = notifications.indexOfKey(id)
|
||||||
|
var n: Notification.Builder? = null
|
||||||
|
|
||||||
|
if (idx >= 0) {
|
||||||
|
n = notifications.valueAt(idx)
|
||||||
|
notifications.removeAt(idx)
|
||||||
|
|
||||||
|
// The cancelled notification is the one attached to the session, need special handling
|
||||||
|
if (attachedId == id) {
|
||||||
|
if (notifications.isNotEmpty()) {
|
||||||
|
// There are still remaining notifications, pick one and attach to the session
|
||||||
|
val anotherId = notifications.keyAt(0)
|
||||||
|
val notification = notifications.valueAt(0)
|
||||||
|
// Attaching a new notification will automatically remove the current one
|
||||||
|
attachNotification(anotherId, notification)
|
||||||
|
} else {
|
||||||
|
// No more notifications left, terminate the session
|
||||||
|
attachedId = -1
|
||||||
|
session.stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Notifications.mgr.cancel(id)
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
|
||||||
private fun handleApp(stream: InputStream, subject: Subject.App) {
|
private fun handleApp(stream: InputStream, subject: Subject.App) {
|
||||||
fun writeTee(output: OutputStream) {
|
fun writeTee(output: OutputStream) {
|
||||||
val uri = MediaStoreUtils.getFile("${subject.title}.apk").uri
|
val uri = MediaStoreUtils.getFile("${subject.title}.apk").uri
|
||||||
@ -84,7 +229,7 @@ class DownloadService : NotificationService() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (isRunningAsStub) {
|
if (isRunningAsStub) {
|
||||||
val updateApk = StubApk.update(this)
|
val updateApk = StubApk.update(context)
|
||||||
try {
|
try {
|
||||||
// Download full APK to stub update path
|
// Download full APK to stub update path
|
||||||
writeTee(updateApk.outputStream())
|
writeTee(updateApk.outputStream())
|
||||||
@ -97,7 +242,7 @@ class DownloadService : NotificationService() {
|
|||||||
// Also upgrade stub
|
// Also upgrade stub
|
||||||
notifyUpdate(subject.notifyId) {
|
notifyUpdate(subject.notifyId) {
|
||||||
it.setProgress(0, 0, true)
|
it.setProgress(0, 0, true)
|
||||||
.setContentTitle(getString(R.string.hide_app_title))
|
.setContentTitle(context.getString(R.string.hide_app_title))
|
||||||
.setContentText("")
|
.setContentText("")
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -107,7 +252,7 @@ class DownloadService : NotificationService() {
|
|||||||
zf.close()
|
zf.close()
|
||||||
|
|
||||||
// Patch and install
|
// Patch and install
|
||||||
subject.intent = HideAPK.upgrade(this, apk)
|
subject.intent = HideAPK.upgrade(context, apk)
|
||||||
?: throw IOException("HideAPK patch error")
|
?: throw IOException("HideAPK patch error")
|
||||||
apk.delete()
|
apk.delete()
|
||||||
} else {
|
} else {
|
||||||
@ -116,7 +261,7 @@ class DownloadService : NotificationService() {
|
|||||||
StubApk.restartProcess(it)
|
StubApk.restartProcess(it)
|
||||||
} ?: run {
|
} ?: run {
|
||||||
// Or else kill the current process after posting notification
|
// Or else kill the current process after posting notification
|
||||||
subject.intent = selfLaunchIntent()
|
subject.intent = context.selfLaunchIntent()
|
||||||
subject.postDownload = { Runtime.getRuntime().exit(0) }
|
subject.postDownload = { Runtime.getRuntime().exit(0) }
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
@ -127,8 +272,8 @@ class DownloadService : NotificationService() {
|
|||||||
throw e
|
throw e
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
val session = APKInstall.startSession(this)
|
val session = APKInstall.startSession(context)
|
||||||
writeTee(session.openStream(this))
|
writeTee(session.openStream(context))
|
||||||
subject.intent = session.waitIntent()
|
subject.intent = session.waitIntent()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -143,7 +288,7 @@ class DownloadService : NotificationService() {
|
|||||||
zout.putNextEntry(ZipEntry("META-INF/com/google/"))
|
zout.putNextEntry(ZipEntry("META-INF/com/google/"))
|
||||||
zout.putNextEntry(ZipEntry("META-INF/com/google/android/"))
|
zout.putNextEntry(ZipEntry("META-INF/com/google/android/"))
|
||||||
zout.putNextEntry(ZipEntry("META-INF/com/google/android/update-binary"))
|
zout.putNextEntry(ZipEntry("META-INF/com/google/android/update-binary"))
|
||||||
assets.open("module_installer.sh").copyTo(zout)
|
context.assets.open("module_installer.sh").copyTo(zout)
|
||||||
|
|
||||||
zout.putNextEntry(ZipEntry("META-INF/com/google/android/updater-script"))
|
zout.putNextEntry(ZipEntry("META-INF/com/google/android/updater-script"))
|
||||||
zout.write("#MAGISK\n".toByteArray())
|
zout.write("#MAGISK\n".toByteArray())
|
||||||
@ -178,41 +323,24 @@ class DownloadService : NotificationService() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
private fun ResponseBody.toProgressStream(subject: Subject): InputStream {
|
||||||
private const val SUBJECT_KEY = "subject"
|
val max = contentLength()
|
||||||
private const val REQUEST_CODE = 1
|
val total = max.toFloat() / 1048576
|
||||||
|
val id = subject.notifyId
|
||||||
|
|
||||||
fun observeProgress(owner: LifecycleOwner, callback: (Float, Subject) -> Unit) {
|
notifyUpdate(id) { it.setContentTitle(subject.title) }
|
||||||
progressBroadcast.value = null
|
|
||||||
progressBroadcast.observe(owner) {
|
|
||||||
val (progress, subject) = it ?: return@observe
|
|
||||||
callback(progress, subject)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun intent(context: Context, subject: Subject) =
|
return ProgressInputStream(byteStream()) {
|
||||||
context.intent<DownloadService>().putExtra(SUBJECT_KEY, subject)
|
val progress = it.toFloat() / 1048576
|
||||||
|
notifyUpdate(id) { notification ->
|
||||||
@SuppressLint("InlinedApi")
|
if (max > 0) {
|
||||||
fun getPendingIntent(context: Context, subject: Subject): PendingIntent {
|
broadcast(progress / total, subject)
|
||||||
val flag = FLAG_IMMUTABLE or FLAG_UPDATE_CURRENT or FLAG_ONE_SHOT
|
notification
|
||||||
val intent = intent(context, subject)
|
.setProgress(max.toInt(), it.toInt(), false)
|
||||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
.setContentText("%.2f / %.2f MB".format(progress, total))
|
||||||
getForegroundService(context, REQUEST_CODE, intent, flag)
|
|
||||||
} else {
|
|
||||||
getService(context, REQUEST_CODE, intent, flag)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressLint("InlinedApi")
|
|
||||||
fun start(activity: BaseActivity, subject: Subject) {
|
|
||||||
activity.withPermission(Manifest.permission.POST_NOTIFICATIONS) {
|
|
||||||
// Always download regardless of notification permission status
|
|
||||||
val app = activity.applicationContext
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
||||||
app.startForegroundService(intent(app, subject))
|
|
||||||
} else {
|
} else {
|
||||||
app.startService(intent(app, subject))
|
broadcast(-1f, subject)
|
||||||
|
notification.setContentText("%.2f MB / ??".format(progress))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,128 +0,0 @@
|
|||||||
package com.topjohnwu.magisk.core.download
|
|
||||||
|
|
||||||
import android.app.Notification
|
|
||||||
import android.content.Intent
|
|
||||||
import android.os.Build
|
|
||||||
import androidx.lifecycle.MutableLiveData
|
|
||||||
import com.topjohnwu.magisk.R
|
|
||||||
import com.topjohnwu.magisk.core.base.BaseService
|
|
||||||
import com.topjohnwu.magisk.core.di.ServiceLocator
|
|
||||||
import com.topjohnwu.magisk.core.ktx.synchronized
|
|
||||||
import com.topjohnwu.magisk.core.utils.ProgressInputStream
|
|
||||||
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
|
|
||||||
|
|
||||||
private var attachedNotificationId = 0
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
notifyUpdate(id) { it.setContentTitle(subject.title) }
|
|
||||||
|
|
||||||
return ProgressInputStream(byteStream()) {
|
|
||||||
val progress = it.toFloat() / 1048576
|
|
||||||
notifyUpdate(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 = notifyRemove(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 attachNotification(id: Int, notification: Notification) {
|
|
||||||
attachedNotificationId = id
|
|
||||||
startForeground(id, notification)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun maybeDetachNotification(id: Int) : Boolean {
|
|
||||||
if (attachedNotificationId != id) return false
|
|
||||||
if (hasNotifications) {
|
|
||||||
val (anotherId, notification) = notifications.entries.first()
|
|
||||||
// Attaching a new notification will remove the current showing one
|
|
||||||
attachNotification(anotherId, notification.build())
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
|
||||||
stopForeground(STOP_FOREGROUND_REMOVE)
|
|
||||||
} else {
|
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
stopForeground(true)
|
|
||||||
}
|
|
||||||
attachedNotificationId = 0
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
protected fun notifyUpdate(id: Int, editor: (Notification.Builder) -> Unit = {}) {
|
|
||||||
fun create() = Notifications.startProgress("")
|
|
||||||
|
|
||||||
val wasEmpty = !hasNotifications
|
|
||||||
val notification = notifications.getOrPut(id, ::create).also(editor).build()
|
|
||||||
if (wasEmpty)
|
|
||||||
attachNotification(id, notification)
|
|
||||||
else
|
|
||||||
Notifications.mgr.notify(id, notification)
|
|
||||||
}
|
|
||||||
|
|
||||||
protected fun notifyRemove(id: Int): Notification.Builder? {
|
|
||||||
val n = notifications.remove(id)
|
|
||||||
if (n == null || !maybeDetachNotification(id))
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -17,8 +17,8 @@ import com.topjohnwu.magisk.ui.flash.FlashFragment
|
|||||||
import com.topjohnwu.magisk.view.Notifications
|
import com.topjohnwu.magisk.view.Notifications
|
||||||
import kotlinx.parcelize.IgnoredOnParcel
|
import kotlinx.parcelize.IgnoredOnParcel
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
|
import java.io.File
|
||||||
private fun cachedFile(name: String) = AppContext.cachedFile(name).apply { delete() }.toUri()
|
import java.util.UUID
|
||||||
|
|
||||||
enum class Action {
|
enum class Action {
|
||||||
Flash,
|
Flash,
|
||||||
@ -34,7 +34,7 @@ sealed class Subject : Parcelable {
|
|||||||
open val autoLaunch: Boolean get() = true
|
open val autoLaunch: Boolean get() = true
|
||||||
open val postDownload: (() -> Unit)? get() = null
|
open val postDownload: (() -> Unit)? get() = null
|
||||||
|
|
||||||
abstract fun pendingIntent(context: Context): PendingIntent?
|
open fun pendingIntent(context: Context): PendingIntent? = null
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
class Module(
|
class Module(
|
||||||
@ -65,7 +65,7 @@ sealed class Subject : Parcelable {
|
|||||||
|
|
||||||
@IgnoredOnParcel
|
@IgnoredOnParcel
|
||||||
override val file by lazy {
|
override val file by lazy {
|
||||||
cachedFile("manager.apk")
|
AppContext.cachedFile("manager.apk").apply { delete() }.toUri()
|
||||||
}
|
}
|
||||||
|
|
||||||
@IgnoredOnParcel
|
@IgnoredOnParcel
|
||||||
@ -76,6 +76,16 @@ sealed class Subject : Parcelable {
|
|||||||
override fun pendingIntent(context: Context) = intent?.toPending(context)
|
override fun pendingIntent(context: Context) = intent?.toPending(context)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
class Test(
|
||||||
|
override val notifyId: Int = Notifications.nextId(),
|
||||||
|
override val title: String = UUID.randomUUID().toString().substring(0, 6)
|
||||||
|
) : Subject() {
|
||||||
|
override val url get() = "http://link.testfile.org/150MB"
|
||||||
|
override val file get() = File("/dev/null").toUri()
|
||||||
|
override val autoLaunch get() = false
|
||||||
|
}
|
||||||
|
|
||||||
@SuppressLint("InlinedApi")
|
@SuppressLint("InlinedApi")
|
||||||
protected fun Intent.toPending(context: Context): PendingIntent {
|
protected fun Intent.toPending(context: Context): PendingIntent {
|
||||||
return PendingIntent.getActivity(context, notifyId, this,
|
return PendingIntent.getActivity(context, notifyId, this,
|
||||||
|
@ -4,7 +4,7 @@ import com.topjohnwu.magisk.R
|
|||||||
import com.topjohnwu.magisk.core.Info
|
import com.topjohnwu.magisk.core.Info
|
||||||
import com.topjohnwu.magisk.core.di.AppContext
|
import com.topjohnwu.magisk.core.di.AppContext
|
||||||
import com.topjohnwu.magisk.core.di.ServiceLocator
|
import com.topjohnwu.magisk.core.di.ServiceLocator
|
||||||
import com.topjohnwu.magisk.core.download.DownloadService
|
import com.topjohnwu.magisk.core.download.DownloadManager
|
||||||
import com.topjohnwu.magisk.core.download.Subject
|
import com.topjohnwu.magisk.core.download.Subject
|
||||||
import com.topjohnwu.magisk.view.MagiskDialog
|
import com.topjohnwu.magisk.view.MagiskDialog
|
||||||
import java.io.File
|
import java.io.File
|
||||||
@ -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(activity, Subject.App()) }
|
onClick { DownloadManager.start(activity, Subject.App()) }
|
||||||
}
|
}
|
||||||
setButton(MagiskDialog.ButtonType.NEGATIVE) {
|
setButton(MagiskDialog.ButtonType.NEGATIVE) {
|
||||||
text = android.R.string.cancel
|
text = android.R.string.cancel
|
||||||
|
@ -3,7 +3,7 @@ package com.topjohnwu.magisk.dialog
|
|||||||
import com.topjohnwu.magisk.R
|
import com.topjohnwu.magisk.R
|
||||||
import com.topjohnwu.magisk.core.di.ServiceLocator
|
import com.topjohnwu.magisk.core.di.ServiceLocator
|
||||||
import com.topjohnwu.magisk.core.download.Action
|
import com.topjohnwu.magisk.core.download.Action
|
||||||
import com.topjohnwu.magisk.core.download.DownloadService
|
import com.topjohnwu.magisk.core.download.DownloadManager
|
||||||
import com.topjohnwu.magisk.core.download.Subject
|
import com.topjohnwu.magisk.core.download.Subject
|
||||||
import com.topjohnwu.magisk.core.model.module.OnlineModule
|
import com.topjohnwu.magisk.core.model.module.OnlineModule
|
||||||
import com.topjohnwu.magisk.view.MagiskDialog
|
import com.topjohnwu.magisk.view.MagiskDialog
|
||||||
@ -24,7 +24,7 @@ class OnlineModuleInstallDialog(private val item: OnlineModule) : MarkDownDialog
|
|||||||
fun download(install: Boolean) {
|
fun download(install: Boolean) {
|
||||||
val action = if (install) Action.Flash else Action.Download
|
val action = if (install) Action.Flash else Action.Download
|
||||||
val subject = Subject.Module(item, action)
|
val subject = Subject.Module(item, action)
|
||||||
DownloadService.start(activity, subject)
|
DownloadManager.start(activity, subject)
|
||||||
}
|
}
|
||||||
|
|
||||||
val title = context.getString(R.string.repo_install_title,
|
val title = context.getString(R.string.repo_install_title,
|
||||||
|
@ -14,7 +14,7 @@ import com.topjohnwu.magisk.R
|
|||||||
import com.topjohnwu.magisk.arch.BaseFragment
|
import com.topjohnwu.magisk.arch.BaseFragment
|
||||||
import com.topjohnwu.magisk.arch.viewModel
|
import com.topjohnwu.magisk.arch.viewModel
|
||||||
import com.topjohnwu.magisk.core.Info
|
import com.topjohnwu.magisk.core.Info
|
||||||
import com.topjohnwu.magisk.core.download.DownloadService
|
import com.topjohnwu.magisk.core.download.DownloadManager
|
||||||
import com.topjohnwu.magisk.databinding.FragmentHomeMd2Binding
|
import com.topjohnwu.magisk.databinding.FragmentHomeMd2Binding
|
||||||
|
|
||||||
class HomeFragment : BaseFragment<FragmentHomeMd2Binding>(), MenuProvider {
|
class HomeFragment : BaseFragment<FragmentHomeMd2Binding>(), MenuProvider {
|
||||||
@ -25,7 +25,7 @@ class HomeFragment : BaseFragment<FragmentHomeMd2Binding>(), MenuProvider {
|
|||||||
override fun onStart() {
|
override fun onStart() {
|
||||||
super.onStart()
|
super.onStart()
|
||||||
activity?.setTitle(R.string.section_home)
|
activity?.setTitle(R.string.section_home)
|
||||||
DownloadService.observeProgress(this, viewModel::onProgressUpdate)
|
DownloadManager.observeProgress(this, viewModel::onProgressUpdate)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun checkTitle(text: TextView, icon: ImageView) {
|
private fun checkTitle(text: TextView, icon: ImageView) {
|
||||||
|
@ -11,7 +11,7 @@ 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.di.AppContext
|
import com.topjohnwu.magisk.core.di.AppContext
|
||||||
import com.topjohnwu.magisk.core.download.DownloadService
|
import com.topjohnwu.magisk.core.download.DownloadManager
|
||||||
import com.topjohnwu.magisk.core.download.Subject
|
import com.topjohnwu.magisk.core.download.Subject
|
||||||
import com.topjohnwu.magisk.core.ktx.getBitmap
|
import com.topjohnwu.magisk.core.ktx.getBitmap
|
||||||
import com.topjohnwu.magisk.core.ktx.selfLaunchIntent
|
import com.topjohnwu.magisk.core.ktx.selfLaunchIntent
|
||||||
@ -67,7 +67,7 @@ object Notifications {
|
|||||||
|
|
||||||
fun updateAvailable() {
|
fun updateAvailable() {
|
||||||
AppContext.apply {
|
AppContext.apply {
|
||||||
val intent = DownloadService.getPendingIntent(this, Subject.App())
|
val intent = DownloadManager.getPendingIntent(this, Subject.App())
|
||||||
val bitmap = getBitmap(R.drawable.ic_magisk_outline)
|
val bitmap = getBitmap(R.drawable.ic_magisk_outline)
|
||||||
val builder = if (SDK_INT >= Build.VERSION_CODES.O) {
|
val builder = if (SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
Notification.Builder(this, UPDATE_CHANNEL)
|
Notification.Builder(this, UPDATE_CHANNEL)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user