100% functional manager self upgrade

Fix #2929
This commit is contained in:
topjohnwu 2020-08-28 04:46:05 -07:00
parent 7e133b0cf4
commit 01efe7a4ea
17 changed files with 102 additions and 123 deletions

View File

@ -64,14 +64,6 @@ object Const {
const val ETAG_KEY = "ETag"
// intents
const val OPEN_SECTION = "section"
const val OPEN_SETTINGS = "settings"
const val INTENT_SET_APP = "app_json"
const val FLASH_INSTALLER = "installer"
const val FLASH_ACTION = "action"
const val FLASH_DATA = "additional_data"
const val DISMISS_ID = "dismiss_id"
const val BROADCAST_MANAGER_UPDATE = "manager_update"
const val BROADCAST_REBOOT = "reboot"
}
object Value {

View File

@ -3,13 +3,8 @@ package com.topjohnwu.magisk.core
import android.content.ContextWrapper
import android.content.Intent
import com.topjohnwu.magisk.core.base.BaseReceiver
import com.topjohnwu.magisk.core.download.Action
import com.topjohnwu.magisk.core.download.DownloadService
import com.topjohnwu.magisk.core.download.Subject
import com.topjohnwu.magisk.core.magiskdb.PolicyDao
import com.topjohnwu.magisk.core.model.ManagerJson
import com.topjohnwu.magisk.core.su.SuCallbackHandler
import com.topjohnwu.magisk.ktx.reboot
import com.topjohnwu.magisk.view.Shortcuts
import com.topjohnwu.superuser.Shell
import kotlinx.coroutines.GlobalScope
@ -46,15 +41,6 @@ open class GeneralReceiver : BaseReceiver() {
Shell.su("magiskhide --rm $pkg").submit()
}
Intent.ACTION_LOCALE_CHANGED -> Shortcuts.setupDynamic(context)
Const.Key.BROADCAST_MANAGER_UPDATE -> {
intent.getParcelableExtra<ManagerJson>(Const.Key.INTENT_SET_APP)?.let {
Info.remote = Info.remote.copy(app = it)
}
DownloadService(context) {
subject = Subject.Manager(Action.APK.Upgrade)
}
}
Const.Key.BROADCAST_REBOOT -> reboot()
}
}
}

View File

@ -13,7 +13,6 @@ import com.topjohnwu.magisk.core.utils.MediaStoreUtils.outputStream
import com.topjohnwu.magisk.core.utils.ProgressInputStream
import com.topjohnwu.magisk.data.network.GithubRawServices
import com.topjohnwu.magisk.ktx.withStreams
import com.topjohnwu.magisk.ktx.writeTo
import com.topjohnwu.magisk.view.Notifications
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@ -29,7 +28,7 @@ import java.util.*
import kotlin.collections.HashMap
import kotlin.random.Random.Default.nextInt
abstract class BaseDownloadService : BaseService(), KoinComponent {
abstract class BaseDownloader : BaseService(), KoinComponent {
private val hasNotifications get() = notifications.isNotEmpty()
private val notifications = Collections.synchronizedMap(HashMap<Int, Notification.Builder>())
@ -41,8 +40,8 @@ abstract class BaseDownloadService : BaseService(), KoinComponent {
override fun onBind(intent: Intent?): IBinder? = null
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
intent?.getParcelableExtra<Subject>(ARG_URL)?.let { subject ->
override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
intent.getParcelableExtra<Subject>(ACTION_KEY)?.let { subject ->
update(subject.notifyID())
coroutineScope.launch {
try {
@ -76,8 +75,11 @@ abstract class BaseDownloadService : BaseService(), KoinComponent {
when (this) {
is Subject.Module -> // Download and process on-the-fly
stream.toModule(file, service.fetchInstaller().byteStream())
else ->
else -> {
withStreams(stream, file.outputStream()) { it, out -> it.copyTo(out) }
if (this is Subject.Manager)
handleAPK(this)
}
}
}
val newId = notifyFinish(this)
@ -124,6 +126,7 @@ abstract class BaseDownloadService : BaseService(), KoinComponent {
private fun notifyFinish(subject: Subject) = lastNotify(subject.notifyID()) {
broadcast(1f, subject)
it.setIntent(subject)
.setContentTitle(subject.title)
.setContentText(getString(R.string.download_complete))
.setSmallIcon(android.R.drawable.stat_sys_download_done)
.setProgress(0, 0, false)
@ -152,16 +155,15 @@ abstract class BaseDownloadService : BaseService(), KoinComponent {
return newId
}
private fun remove(id: Int) = notifications.remove(id)?.also {
updateForeground()
cancel(id)
}
protected fun remove(id: Int) = notifications.remove(id)
?.also { updateForeground(); cancel(id) }
?: { cancel(id); null }()
private fun notify(id: Int, notification: Notification) {
Notifications.mgr.notify(id, notification)
}
protected fun cancel(id: Int) {
private fun cancel(id: Int) {
Notifications.mgr.cancel(id)
}
@ -178,13 +180,12 @@ abstract class BaseDownloadService : BaseService(), KoinComponent {
protected abstract suspend fun onFinish(subject: Subject, id: Int)
protected abstract fun Notification.Builder.setIntent(subject: Subject)
: Notification.Builder
protected abstract fun Notification.Builder.setIntent(subject: Subject): Notification.Builder
// ---
companion object : KoinComponent {
const val ARG_URL = "arg_url"
const val ACTION_KEY = "download_action"
private val progressBroadcast = MutableLiveData<Pair<Float, Subject>>()

View File

@ -17,7 +17,7 @@ import com.topjohnwu.magisk.utils.APKInstall
import kotlin.random.Random.Default.nextInt
@SuppressLint("Registered")
open class DownloadService : BaseDownloadService() {
open class DownloadService : BaseDownloader() {
private val context get() = this
@ -30,7 +30,7 @@ open class DownloadService : BaseDownloadService() {
private suspend fun Magisk.onFinish(id: Int) = when (val action = action) {
Uninstall -> FlashFragment.uninstall(file, id)
EnvFix -> {
cancel(id)
remove(id)
EnvFixTask(file).exec()
Unit
}
@ -44,9 +44,9 @@ open class DownloadService : BaseDownloadService() {
else -> Unit
}
private suspend fun Manager.onFinish(id: Int) {
handleAPK(this)
cancel(id)
private fun Manager.onFinish(id: Int) {
remove(id)
APKInstall.install(context, file.toFile())
}
// --- Customize finish notification
@ -85,24 +85,29 @@ open class DownloadService : BaseDownloadService() {
// ---
class Builder {
lateinit var subject: Subject
}
companion object {
inline operator fun invoke(context: Context, argBuilder: Builder.() -> Unit) {
val app = context.applicationContext
val builder = Builder().apply(argBuilder)
val intent = app.intent<DownloadService>().putExtra(ARG_URL, builder.subject)
private fun intent(context: Context, subject: Subject) =
context.intent<DownloadService>().putExtra(ACTION_KEY, subject)
if (Build.VERSION.SDK_INT >= 26) {
app.startForegroundService(intent)
fun pendingIntent(context: Context, subject: Subject): PendingIntent {
return if (Build.VERSION.SDK_INT >= 26) {
PendingIntent.getForegroundService(context, nextInt(),
intent(context, subject), PendingIntent.FLAG_UPDATE_CURRENT)
} else {
app.startService(intent)
PendingIntent.getService(context, nextInt(),
intent(context, subject), PendingIntent.FLAG_UPDATE_CURRENT)
}
}
operator fun invoke(context: Context, subject: Subject) {
val app = context.applicationContext
if (Build.VERSION.SDK_INT >= 26) {
app.startForegroundService(intent(app, subject))
} else {
app.startService(intent(app, subject))
}
}
}
}

View File

@ -1,5 +1,6 @@
package com.topjohnwu.magisk.core.download
import android.content.Context
import androidx.core.net.toFile
import com.topjohnwu.magisk.BuildConfig
import com.topjohnwu.magisk.DynAPK
@ -12,49 +13,48 @@ import com.topjohnwu.magisk.core.isRunningAsStub
import com.topjohnwu.magisk.core.utils.PatchAPK
import com.topjohnwu.magisk.ktx.relaunchApp
import com.topjohnwu.magisk.ktx.writeTo
import com.topjohnwu.magisk.utils.APKInstall
import com.topjohnwu.superuser.Shell
import java.io.File
private fun DownloadService.patch(apk: File, id: Int) {
if (packageName == BuildConfig.APPLICATION_ID)
return
update(id) {
it.setProgress(0, 0, true)
.setProgress(0, 0, true)
.setContentTitle(getString(R.string.hide_manager_title))
.setContentText("")
}
private fun Context.patch(apk: File) {
val patched = File(apk.parent, "patched.apk")
PatchAPK.patch(apk.path, patched.path, packageName, applicationInfo.nonLocalizedLabel)
apk.delete()
patched.renameTo(apk)
}
private suspend fun DownloadService.upgrade(apk: File, id: Int) {
private fun BaseDownloader.notifyHide(id: Int) {
update(id) {
it.setProgress(0, 0, true)
.setContentTitle(getString(R.string.hide_manager_title))
.setContentText("")
}
}
private suspend fun BaseDownloader.upgrade(subject: Subject.Manager) {
val apk = subject.file.toFile()
val id = subject.notifyID()
if (isRunningAsStub) {
// Move to upgrade location
apk.copyTo(DynAPK.update(this), overwrite = true)
apk.delete()
if (Info.stubChk.version < Info.remote.stub.versionCode) {
// We also want to upgrade stub
service.fetchFile(Info.remote.stub.link).byteStream().use {
it.writeTo(apk)
}
patch(apk, id)
if (Info.stubChk.version < subject.stub.versionCode) {
notifyHide(id)
// Also upgrade stub
service.fetchFile(subject.stub.link).byteStream().use { it.writeTo(apk) }
patch(apk)
} else {
// Simply relaunch the app
stopSelf()
relaunchApp(this)
}
} else {
patch(apk, id)
} else if (packageName != BuildConfig.APPLICATION_ID) {
notifyHide(id)
patch(apk)
}
APKInstall.install(this, apk)
}
private fun DownloadService.restore(apk: File, id: Int) {
private fun BaseDownloader.restore(apk: File, id: Int) {
update(id) {
it.setProgress(0, 0, true)
.setProgress(0, 0, true)
@ -65,8 +65,8 @@ private fun DownloadService.restore(apk: File, id: Int) {
Shell.su("pm install $apk && pm uninstall $packageName").exec()
}
suspend fun DownloadService.handleAPK(subject: Subject.Manager) =
suspend fun BaseDownloader.handleAPK(subject: Subject.Manager) =
when (subject.action) {
is Upgrade -> upgrade(subject.file.toFile(), subject.notifyID())
is Upgrade -> upgrade(subject)
is Restore -> restore(subject.file.toFile(), subject.notifyID())
}

View File

@ -7,13 +7,16 @@ import androidx.core.net.toUri
import com.topjohnwu.magisk.core.Info
import com.topjohnwu.magisk.core.model.MagiskJson
import com.topjohnwu.magisk.core.model.ManagerJson
import com.topjohnwu.magisk.core.model.StubJson
import com.topjohnwu.magisk.core.model.module.Repo
import com.topjohnwu.magisk.core.utils.MediaStoreUtils
import com.topjohnwu.magisk.ktx.cachedFile
import com.topjohnwu.magisk.ktx.get
import com.topjohnwu.magisk.core.utils.MediaStoreUtils
import kotlinx.android.parcel.IgnoredOnParcel
import kotlinx.android.parcel.Parcelize
private fun cachedFile(name: String) = get<Context>().cachedFile(name).apply { delete() }.toUri()
sealed class Subject : Parcelable {
abstract val url: String
@ -37,21 +40,20 @@ sealed class Subject : Parcelable {
@Parcelize
class Manager(
override val action: Action.APK
override val action: Action.APK,
private val app: ManagerJson = Info.remote.app,
val stub: StubJson = Info.remote.stub
) : Subject() {
@IgnoredOnParcel
val manager: ManagerJson = Info.remote.app
override val title: String
get() = "MagiskManager-${manager.version}(${manager.versionCode})"
get() = "MagiskManager-${app.version}(${app.versionCode})"
override val url: String
get() = manager.link
get() = app.link
@IgnoredOnParcel
override val file by lazy {
get<Context>().cachedFile("manager.apk").toUri()
cachedFile("manager.apk")
}
}
@ -69,7 +71,7 @@ sealed class Subject : Parcelable {
@IgnoredOnParcel
override val file by lazy {
get<Context>().cachedFile("magisk.zip").toUri()
cachedFile("magisk.zip")
}
}
@ -81,7 +83,7 @@ sealed class Subject : Parcelable {
@IgnoredOnParcel
override val file by lazy {
get<Context>().cachedFile(title).toUri()
cachedFile(title)
}
}

View File

@ -35,8 +35,9 @@ data class ManagerJson(
val note: String = ""
) : Parcelable
@Parcelize
@JsonClass(generateAdapter = true)
data class StubJson(
val versionCode: Int = -1,
val link: String = ""
)
) : Parcelable

View File

@ -31,7 +31,7 @@ class EnvFixDialog : DialogEvent() {
lbm.unregisterReceiver(this)
}
}, IntentFilter(DISMISS))
DownloadService(dialog.context) { subject = Magisk(EnvFix) }
DownloadService(dialog.context, Magisk(EnvFix))
}
}
.applyButton(MagiskDialog.ButtonType.NEGATIVE) {

View File

@ -25,7 +25,7 @@ class ManagerInstallDialog : DialogEvent() {
applyButton(MagiskDialog.ButtonType.POSITIVE) {
titleRes = R.string.install
onClick { DownloadService(context) { this.subject = subject } }
onClick { DownloadService(context, subject) }
}
if (Info.remote.app.note.isEmpty()) return

View File

@ -12,9 +12,10 @@ class ModuleInstallDialog(private val item: Repo) : DialogEvent() {
override fun build(dialog: MagiskDialog) {
with(dialog) {
fun download(install: Boolean) = DownloadService(context) {
fun download(install: Boolean) {
val config = if (install) Action.Flash.Primary else Action.Download
subject = Subject.Module(item, config)
val subject = Subject.Module(item, config)
DownloadService(context, subject)
}
applyTitle(context.getString(R.string.repo_install_title, item.name))

View File

@ -47,9 +47,7 @@ class UninstallDialog : DialogEvent() {
}
private fun completeUninstall() {
DownloadService(dialog.context) {
subject = Subject.Magisk(Action.Uninstall)
}
DownloadService(dialog.context, Subject.Magisk(Action.Uninstall))
}
}

View File

@ -4,7 +4,7 @@ import android.os.Bundle
import android.view.*
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.arch.BaseUIFragment
import com.topjohnwu.magisk.core.download.BaseDownloadService
import com.topjohnwu.magisk.core.download.BaseDownloader
import com.topjohnwu.magisk.databinding.FragmentHomeMd2Binding
import com.topjohnwu.magisk.events.RebootEvent
import com.topjohnwu.superuser.Shell
@ -19,7 +19,7 @@ class HomeFragment : BaseUIFragment<HomeViewModel, FragmentHomeMd2Binding>() {
super.onStart()
activity.title = resources.getString(R.string.section_home)
setHasOptionsMenu(true)
BaseDownloadService.observeProgress(this, viewModel::onProgressUpdate)
BaseDownloader.observeProgress(this, viewModel::onProgressUpdate)
}
override fun onCreateView(

View File

@ -4,7 +4,7 @@ import android.content.Intent
import androidx.lifecycle.viewModelScope
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.arch.BaseUIFragment
import com.topjohnwu.magisk.core.download.BaseDownloadService
import com.topjohnwu.magisk.core.download.BaseDownloader
import com.topjohnwu.magisk.databinding.FragmentInstallMd2Binding
import com.topjohnwu.magisk.events.RequestFileEvent
import com.topjohnwu.magisk.ktx.coroutineScope
@ -26,7 +26,7 @@ class InstallFragment : BaseUIFragment<InstallViewModel, FragmentInstallMd2Bindi
// Allow markwon to run in viewmodel scope
binding.releaseNotes.coroutineScope = viewModel.viewModelScope
BaseDownloadService.observeProgress(this, viewModel::onProgressUpdate)
BaseDownloader.observeProgress(this, viewModel::onProgressUpdate)
}
}

View File

@ -78,17 +78,16 @@ class InstallViewModel(
step = nextStep
}
fun install() = DownloadService(get()) {
subject = Subject.Magisk(resolveConfiguration())
}.also { state = State.LOADING }
fun install() =
DownloadService(get(), Subject.Magisk(resolveAction())).also { state = State.LOADING }
// ---
private fun resolveConfiguration() = when (method) {
private fun resolveAction() = when (method) {
R.id.method_download -> Action.Download
R.id.method_patch -> Action.Patch(data!!)
R.id.method_direct -> Action.Flash.Primary
R.id.method_inactive_slot -> Action.Flash.Secondary
else -> throw IllegalArgumentException("Unknown value")
else -> error("Unknown value")
}
}

View File

@ -12,7 +12,7 @@ import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.arch.BaseUIFragment
import com.topjohnwu.magisk.arch.ReselectionTarget
import com.topjohnwu.magisk.arch.ViewEvent
import com.topjohnwu.magisk.core.download.BaseDownloadService
import com.topjohnwu.magisk.core.download.BaseDownloader
import com.topjohnwu.magisk.databinding.FragmentModuleMd2Binding
import com.topjohnwu.magisk.events.InstallExternalModuleEvent
import com.topjohnwu.magisk.ktx.hideKeyboard
@ -51,7 +51,7 @@ class ModuleFragment : BaseUIFragment<ModuleViewModel, FragmentModuleMd2Binding>
super.onStart()
setHasOptionsMenu(true)
activity.title = resources.getString(R.string.modules)
BaseDownloadService.observeProgress(this, viewModel::onProgressUpdate)
BaseDownloader.observeProgress(this, viewModel::onProgressUpdate)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {

View File

@ -145,9 +145,7 @@ class SettingsViewModel(
}
private fun restoreManager() {
DownloadService(get()) {
subject = Subject.Manager(Action.APK.Restore)
}
DownloadService(get(), Subject.Manager(Action.APK.Restore))
}
}

View File

@ -10,9 +10,15 @@ import androidx.core.app.TaskStackBuilder
import androidx.core.content.getSystemService
import androidx.core.graphics.drawable.toIcon
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.core.*
import com.topjohnwu.magisk.core.Const
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.SplashActivity
import com.topjohnwu.magisk.core.cmp
import com.topjohnwu.magisk.core.download.Action
import com.topjohnwu.magisk.core.download.DownloadService
import com.topjohnwu.magisk.core.download.Subject
import com.topjohnwu.magisk.core.intent
import com.topjohnwu.magisk.ktx.get
import com.topjohnwu.magisk.ktx.getBitmap
@ -52,12 +58,9 @@ object Notifications {
stackBuilder.addParentStack(SplashActivity::class.java.cmp(context.packageName))
stackBuilder.addNextIntent(intent)
val pendingIntent = stackBuilder.getPendingIntent(
Const.ID.MAGISK_UPDATE_NOTIFICATION_ID,
PendingIntent.FLAG_UPDATE_CURRENT)
Const.ID.MAGISK_UPDATE_NOTIFICATION_ID, PendingIntent.FLAG_UPDATE_CURRENT)
val builder = updateBuilder(
context
)
val builder = updateBuilder(context)
.setContentTitle(context.getString(R.string.magisk_update_title))
.setContentText(context.getString(R.string.manager_download_install))
.setAutoCancel(true)
@ -67,20 +70,13 @@ object Notifications {
}
fun managerUpdate(context: Context) {
val intent = context.intent<GeneralReceiver>()
.setAction(Const.Key.BROADCAST_MANAGER_UPDATE)
.putExtra(Const.Key.INTENT_SET_APP, Info.remote.app)
val intent = DownloadService.pendingIntent(context, Subject.Manager(Action.APK.Upgrade))
val pendingIntent = PendingIntent.getBroadcast(context,
Const.ID.APK_UPDATE_NOTIFICATION_ID, intent, PendingIntent.FLAG_UPDATE_CURRENT)
val builder = updateBuilder(
context
)
val builder = updateBuilder(context)
.setContentTitle(context.getString(R.string.manager_update_title))
.setContentText(context.getString(R.string.manager_download_install))
.setAutoCancel(true)
.setContentIntent(pendingIntent)
.setContentIntent(intent)
mgr.notify(Const.ID.APK_UPDATE_NOTIFICATION_ID, builder.build())
}