mirror of
https://github.com/topjohnwu/Magisk.git
synced 2024-12-22 16:07:39 +00:00
Update env fix handling logic
This commit is contained in:
parent
5c0e86383c
commit
fc05f377fb
@ -9,15 +9,18 @@ import android.os.Build
|
|||||||
import android.webkit.MimeTypeMap
|
import android.webkit.MimeTypeMap
|
||||||
import com.topjohnwu.magisk.R
|
import com.topjohnwu.magisk.R
|
||||||
import com.topjohnwu.magisk.core.intent
|
import com.topjohnwu.magisk.core.intent
|
||||||
|
import com.topjohnwu.magisk.core.tasks.EnvFixTask
|
||||||
import com.topjohnwu.magisk.extensions.chooser
|
import com.topjohnwu.magisk.extensions.chooser
|
||||||
import com.topjohnwu.magisk.extensions.exists
|
import com.topjohnwu.magisk.extensions.exists
|
||||||
import com.topjohnwu.magisk.extensions.provide
|
import com.topjohnwu.magisk.extensions.provide
|
||||||
|
import com.topjohnwu.magisk.extensions.subscribeK
|
||||||
import com.topjohnwu.magisk.legacy.flash.FlashActivity
|
import com.topjohnwu.magisk.legacy.flash.FlashActivity
|
||||||
import com.topjohnwu.magisk.model.entity.internal.Configuration.*
|
import com.topjohnwu.magisk.model.entity.internal.Configuration.*
|
||||||
import com.topjohnwu.magisk.model.entity.internal.Configuration.Flash.Secondary
|
import com.topjohnwu.magisk.model.entity.internal.Configuration.Flash.Secondary
|
||||||
import com.topjohnwu.magisk.model.entity.internal.DownloadSubject
|
import com.topjohnwu.magisk.model.entity.internal.DownloadSubject
|
||||||
import com.topjohnwu.magisk.model.entity.internal.DownloadSubject.*
|
import com.topjohnwu.magisk.model.entity.internal.DownloadSubject.*
|
||||||
import com.topjohnwu.magisk.utils.APKInstall
|
import com.topjohnwu.magisk.utils.APKInstall
|
||||||
|
import io.reactivex.Completable
|
||||||
import org.koin.core.get
|
import org.koin.core.get
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import kotlin.random.Random.Default.nextInt
|
import kotlin.random.Random.Default.nextInt
|
||||||
@ -43,6 +46,7 @@ open class DownloadService : RemoteFileService() {
|
|||||||
id: Int
|
id: Int
|
||||||
) = when (val conf = subject.configuration) {
|
) = when (val conf = subject.configuration) {
|
||||||
Uninstall -> FlashActivity.uninstall(this, subject.file, id)
|
Uninstall -> FlashActivity.uninstall(this, subject.file, id)
|
||||||
|
EnvFix -> { remove(id); EnvFixTask(subject.file).exec() }
|
||||||
is Patch -> FlashActivity.patch(this, subject.file, conf.fileUri, id)
|
is Patch -> FlashActivity.patch(this, subject.file, conf.fileUri, id)
|
||||||
is Flash -> FlashActivity.flash(this, subject.file, conf is Secondary, id)
|
is Flash -> FlashActivity.flash(this, subject.file, conf is Secondary, id)
|
||||||
else -> Unit
|
else -> Unit
|
||||||
@ -60,10 +64,14 @@ open class DownloadService : RemoteFileService() {
|
|||||||
subject: Manager,
|
subject: Manager,
|
||||||
id: Int
|
id: Int
|
||||||
) {
|
) {
|
||||||
remove(id)
|
Completable.fromAction {
|
||||||
when (subject.configuration) {
|
handleAPK(subject)
|
||||||
is APK.Upgrade -> APKInstall.install(this, subject.file)
|
}.subscribeK {
|
||||||
is APK.Restore -> Unit
|
remove(id)
|
||||||
|
when (subject.configuration) {
|
||||||
|
is APK.Upgrade -> APKInstall.install(this, subject.file)
|
||||||
|
is APK.Restore -> Unit
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -20,8 +20,8 @@ private fun RemoteFileService.patch(apk: File, id: Int) {
|
|||||||
if (packageName == BuildConfig.APPLICATION_ID)
|
if (packageName == BuildConfig.APPLICATION_ID)
|
||||||
return
|
return
|
||||||
|
|
||||||
update(id) { notification ->
|
update(id) {
|
||||||
notification.setProgress(0, 0, true)
|
it.setProgress(0, 0, true)
|
||||||
.setProgress(0, 0, true)
|
.setProgress(0, 0, true)
|
||||||
.setContentTitle(getString(R.string.hide_manager_title))
|
.setContentTitle(getString(R.string.hide_manager_title))
|
||||||
.setContentText("")
|
.setContentText("")
|
||||||
@ -53,11 +53,11 @@ private fun RemoteFileService.upgrade(apk: File, id: Int) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun RemoteFileService.restore(apk: File, id: Int) {
|
private fun RemoteFileService.restore(apk: File, id: Int) {
|
||||||
update(id) { notification ->
|
update(id) {
|
||||||
notification.setProgress(0, 0, true)
|
it.setProgress(0, 0, true)
|
||||||
.setProgress(0, 0, true)
|
.setProgress(0, 0, true)
|
||||||
.setContentTitle(getString(R.string.restore_img_msg))
|
.setContentTitle(getString(R.string.restore_img_msg))
|
||||||
.setContentText("")
|
.setContentText("")
|
||||||
}
|
}
|
||||||
Config.export()
|
Config.export()
|
||||||
// Make it world readable
|
// Make it world readable
|
||||||
@ -65,8 +65,8 @@ private fun RemoteFileService.restore(apk: File, id: Int) {
|
|||||||
Shell.su("pm install $apk && pm uninstall $packageName").exec()
|
Shell.su("pm install $apk && pm uninstall $packageName").exec()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun RemoteFileService.handleAPK(subject: DownloadSubject.Manager)
|
fun RemoteFileService.handleAPK(subject: DownloadSubject.Manager) =
|
||||||
= when (subject.configuration) {
|
when (subject.configuration) {
|
||||||
is Upgrade -> upgrade(subject.file, subject.hashCode())
|
is Upgrade -> upgrade(subject.file, subject.hashCode())
|
||||||
is Restore -> restore(subject.file, subject.hashCode())
|
is Restore -> restore(subject.file, subject.hashCode())
|
||||||
}
|
}
|
||||||
|
@ -7,16 +7,14 @@ import com.topjohnwu.magisk.core.base.BaseService
|
|||||||
import com.topjohnwu.magisk.core.view.Notifications
|
import com.topjohnwu.magisk.core.view.Notifications
|
||||||
import org.koin.core.KoinComponent
|
import org.koin.core.KoinComponent
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
import kotlin.collections.HashMap
|
||||||
import kotlin.random.Random.Default.nextInt
|
import kotlin.random.Random.Default.nextInt
|
||||||
|
|
||||||
abstract class NotificationService : BaseService(), KoinComponent {
|
abstract class NotificationService : BaseService(), KoinComponent {
|
||||||
|
|
||||||
abstract val defaultNotification: Notification.Builder
|
|
||||||
|
|
||||||
private val hasNotifications get() = notifications.isNotEmpty()
|
private val hasNotifications get() = notifications.isNotEmpty()
|
||||||
|
|
||||||
private val notifications =
|
private val notifications = Collections.synchronizedMap(HashMap<Int, Notification.Builder>())
|
||||||
Collections.synchronizedMap(mutableMapOf<Int, Notification.Builder>())
|
|
||||||
|
|
||||||
override fun onTaskRemoved(rootIntent: Intent?) {
|
override fun onTaskRemoved(rootIntent: Intent?) {
|
||||||
super.onTaskRemoved(rootIntent)
|
super.onTaskRemoved(rootIntent)
|
||||||
@ -24,22 +22,23 @@ abstract class NotificationService : BaseService(), KoinComponent {
|
|||||||
notifications.clear()
|
notifications.clear()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
abstract fun createNotification(): Notification.Builder
|
||||||
|
|
||||||
// --
|
// --
|
||||||
|
|
||||||
fun update(
|
fun update(
|
||||||
id: Int,
|
id: Int,
|
||||||
body: (Notification.Builder) -> Unit = {}
|
body: (Notification.Builder) -> Unit = {}
|
||||||
) {
|
) {
|
||||||
val notification = notifications.getOrPut(id) { defaultNotification }
|
val wasEmpty = notifications.isEmpty()
|
||||||
|
val notification = notifications.getOrPut(id, ::createNotification).also(body)
|
||||||
notify(id, notification.also(body).build())
|
if (wasEmpty)
|
||||||
|
|
||||||
if (notifications.size == 1) {
|
|
||||||
updateForeground()
|
updateForeground()
|
||||||
}
|
else
|
||||||
|
notify(id, notification.build())
|
||||||
}
|
}
|
||||||
|
|
||||||
protected fun finishNotify(
|
protected fun lastNotify(
|
||||||
id: Int,
|
id: Int,
|
||||||
editBody: (Notification.Builder) -> Notification.Builder? = { null }
|
editBody: (Notification.Builder) -> Notification.Builder? = { null }
|
||||||
) : Int {
|
) : Int {
|
||||||
@ -57,6 +56,11 @@ abstract class NotificationService : BaseService(), KoinComponent {
|
|||||||
return newId
|
return newId
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected fun remove(id: Int) = notifications.remove(id).also {
|
||||||
|
cancel(id)
|
||||||
|
updateForeground()
|
||||||
|
}
|
||||||
|
|
||||||
// ---
|
// ---
|
||||||
|
|
||||||
private fun notify(id: Int, notification: Notification) {
|
private fun notify(id: Int, notification: Notification) {
|
||||||
@ -67,16 +71,13 @@ abstract class NotificationService : BaseService(), KoinComponent {
|
|||||||
Notifications.mgr.cancel(id)
|
Notifications.mgr.cancel(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
protected fun remove(id: Int) = notifications.remove(id).also {
|
|
||||||
cancel(id)
|
|
||||||
updateForeground()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun updateForeground() {
|
private fun updateForeground() {
|
||||||
if (hasNotifications)
|
if (hasNotifications) {
|
||||||
startForeground(notifications.keys.first(), notifications.values.first().build())
|
val first = notifications.entries.first()
|
||||||
else
|
startForeground(first.key, first.value.build())
|
||||||
|
} else {
|
||||||
stopForeground(true)
|
stopForeground(true)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --
|
// --
|
||||||
|
@ -14,7 +14,8 @@ import com.topjohnwu.magisk.extensions.get
|
|||||||
import com.topjohnwu.magisk.extensions.subscribeK
|
import com.topjohnwu.magisk.extensions.subscribeK
|
||||||
import com.topjohnwu.magisk.extensions.writeTo
|
import com.topjohnwu.magisk.extensions.writeTo
|
||||||
import com.topjohnwu.magisk.model.entity.internal.DownloadSubject
|
import com.topjohnwu.magisk.model.entity.internal.DownloadSubject
|
||||||
import com.topjohnwu.magisk.model.entity.internal.DownloadSubject.*
|
import com.topjohnwu.magisk.model.entity.internal.DownloadSubject.Magisk
|
||||||
|
import com.topjohnwu.magisk.model.entity.internal.DownloadSubject.Module
|
||||||
import com.topjohnwu.superuser.ShellUtils
|
import com.topjohnwu.superuser.ShellUtils
|
||||||
import io.reactivex.Completable
|
import io.reactivex.Completable
|
||||||
import okhttp3.ResponseBody
|
import okhttp3.ResponseBody
|
||||||
@ -27,19 +28,17 @@ abstract class RemoteFileService : NotificationService() {
|
|||||||
|
|
||||||
val service: GithubRawServices by inject()
|
val service: GithubRawServices by inject()
|
||||||
|
|
||||||
override val defaultNotification
|
|
||||||
get() = Notifications.progress(this, "")
|
|
||||||
|
|
||||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||||
intent?.getParcelableExtra<DownloadSubject>(ARG_URL)?.let { start(it) }
|
intent?.getParcelableExtra<DownloadSubject>(ARG_URL)?.let { start(it) }
|
||||||
return START_REDELIVER_INTENT
|
return START_REDELIVER_INTENT
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun createNotification() = Notifications.progress(this, "")
|
||||||
|
|
||||||
// ---
|
// ---
|
||||||
|
|
||||||
private fun start(subject: DownloadSubject) = checkExisting(subject)
|
private fun start(subject: DownloadSubject) = checkExisting(subject)
|
||||||
.onErrorResumeNext { download(subject) }
|
.onErrorResumeNext { download(subject) }
|
||||||
.doOnSubscribe { update(subject.hashCode()) { it.setContentTitle(subject.title) } }
|
|
||||||
.subscribeK(onError = {
|
.subscribeK(onError = {
|
||||||
Timber.e(it)
|
Timber.e(it)
|
||||||
failNotify(subject)
|
failNotify(subject)
|
||||||
@ -52,16 +51,14 @@ abstract class RemoteFileService : NotificationService() {
|
|||||||
|
|
||||||
private fun checkExisting(subject: DownloadSubject) = Completable.fromAction {
|
private fun checkExisting(subject: DownloadSubject) = Completable.fromAction {
|
||||||
check(subject is Magisk) { "Download cache is disabled" }
|
check(subject is Magisk) { "Download cache is disabled" }
|
||||||
|
check(subject.file.exists() &&
|
||||||
subject.file.also {
|
ShellUtils.checkSum("MD5", subject.file, subject.magisk.md5)) {
|
||||||
check(it.exists() && ShellUtils.checkSum("MD5", it, subject.magisk.md5)) {
|
"The given file does not match checksum"
|
||||||
"The given file does not match checksum"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun download(subject: DownloadSubject) = service.fetchFile(subject.url)
|
private fun download(subject: DownloadSubject) = service.fetchFile(subject.url)
|
||||||
.map { it.toStream(subject.hashCode(), subject) }
|
.map { it.toProgressStream(subject) }
|
||||||
.flatMapCompletable { stream ->
|
.flatMapCompletable { stream ->
|
||||||
when (subject) {
|
when (subject) {
|
||||||
is Module -> service.fetchInstaller()
|
is Module -> service.fetchInstaller()
|
||||||
@ -69,14 +66,14 @@ abstract class RemoteFileService : NotificationService() {
|
|||||||
.ignoreElement()
|
.ignoreElement()
|
||||||
else -> Completable.fromAction { stream.writeTo(subject.file) }
|
else -> Completable.fromAction { stream.writeTo(subject.file) }
|
||||||
}
|
}
|
||||||
}.doOnComplete {
|
|
||||||
if (subject is Manager)
|
|
||||||
handleAPK(subject)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun ResponseBody.toStream(id: Int, subject: DownloadSubject): InputStream {
|
private fun ResponseBody.toProgressStream(subject: DownloadSubject): InputStream {
|
||||||
val maxRaw = contentLength()
|
val maxRaw = contentLength()
|
||||||
val max = maxRaw / 1_000_000f
|
val max = maxRaw / 1_000_000f
|
||||||
|
val id = subject.hashCode()
|
||||||
|
|
||||||
|
update(id) { it.setContentTitle(subject.title) }
|
||||||
|
|
||||||
return ProgressInputStream(byteStream()) {
|
return ProgressInputStream(byteStream()) {
|
||||||
val progress = it / 1_000_000f
|
val progress = it / 1_000_000f
|
||||||
@ -94,14 +91,14 @@ abstract class RemoteFileService : NotificationService() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun failNotify(subject: DownloadSubject) = finishNotify(subject.hashCode()) {
|
private fun failNotify(subject: DownloadSubject) = lastNotify(subject.hashCode()) {
|
||||||
send(0f, subject)
|
send(0f, 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 finishNotify(subject: DownloadSubject) = finishNotify(subject.hashCode()) {
|
private fun finishNotify(subject: DownloadSubject) = lastNotify(subject.hashCode()) {
|
||||||
send(1f, subject)
|
send(1f, subject)
|
||||||
it.addActions(subject)
|
it.addActions(subject)
|
||||||
.setContentText(getString(R.string.download_complete))
|
.setContentText(getString(R.string.download_complete))
|
||||||
|
@ -0,0 +1,10 @@
|
|||||||
|
package com.topjohnwu.magisk.core.tasks
|
||||||
|
|
||||||
|
import androidx.annotation.MainThread
|
||||||
|
|
||||||
|
interface FlashResultListener {
|
||||||
|
|
||||||
|
@MainThread
|
||||||
|
fun onResult(success: Boolean)
|
||||||
|
|
||||||
|
}
|
@ -18,7 +18,7 @@ abstract class FlashZip(
|
|||||||
private val mUri: Uri,
|
private val mUri: Uri,
|
||||||
private val console: MutableList<String>,
|
private val console: MutableList<String>,
|
||||||
private val logs: MutableList<String>
|
private val logs: MutableList<String>
|
||||||
) {
|
) : FlashResultListener {
|
||||||
|
|
||||||
private val context: Context by inject()
|
private val context: Context by inject()
|
||||||
private val installFolder = File(context.cacheDir, "flash").apply {
|
private val installFolder = File(context.cacheDir, "flash").apply {
|
||||||
@ -94,6 +94,4 @@ abstract class FlashZip(
|
|||||||
.subscribeK(onError = { onResult(false) }) { onResult(it) }
|
.subscribeK(onError = { onResult(false) }) { onResult(it) }
|
||||||
.let { Unit } // ignores result disposable
|
.let { Unit } // ignores result disposable
|
||||||
|
|
||||||
|
|
||||||
protected abstract fun onResult(success: Boolean)
|
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,8 @@
|
|||||||
package com.topjohnwu.magisk.model.flash
|
package com.topjohnwu.magisk.core.tasks
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import androidx.core.os.postDelayed
|
import androidx.core.os.postDelayed
|
||||||
import com.topjohnwu.magisk.core.tasks.FlashZip
|
|
||||||
import com.topjohnwu.magisk.extensions.inject
|
import com.topjohnwu.magisk.extensions.inject
|
||||||
import com.topjohnwu.superuser.Shell
|
import com.topjohnwu.superuser.Shell
|
||||||
import com.topjohnwu.superuser.internal.UiThreadHandler
|
import com.topjohnwu.superuser.internal.UiThreadHandler
|
||||||
@ -28,16 +27,7 @@ sealed class Flashing(
|
|||||||
console: MutableList<String>,
|
console: MutableList<String>,
|
||||||
log: MutableList<String>,
|
log: MutableList<String>,
|
||||||
resultListener: FlashResultListener
|
resultListener: FlashResultListener
|
||||||
) : Flashing(uri, console, log, resultListener) {
|
) : Flashing(uri, console, log, resultListener)
|
||||||
|
|
||||||
override fun onResult(success: Boolean) {
|
|
||||||
if (success) {
|
|
||||||
//Utils.loadModules()
|
|
||||||
}
|
|
||||||
super.onResult(success)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
class Uninstall(
|
class Uninstall(
|
||||||
uri: Uri,
|
uri: Uri,
|
@ -0,0 +1,370 @@
|
|||||||
|
package com.topjohnwu.magisk.core.tasks
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Build
|
||||||
|
import android.text.TextUtils
|
||||||
|
import androidx.annotation.WorkerThread
|
||||||
|
import androidx.core.net.toUri
|
||||||
|
import com.topjohnwu.magisk.core.Config
|
||||||
|
import com.topjohnwu.magisk.core.Info
|
||||||
|
import com.topjohnwu.magisk.data.network.GithubRawServices
|
||||||
|
import com.topjohnwu.magisk.di.Protected
|
||||||
|
import com.topjohnwu.magisk.extensions.*
|
||||||
|
import com.topjohnwu.signing.SignBoot
|
||||||
|
import com.topjohnwu.superuser.Shell
|
||||||
|
import com.topjohnwu.superuser.ShellUtils
|
||||||
|
import com.topjohnwu.superuser.internal.NOPList
|
||||||
|
import com.topjohnwu.superuser.io.SuFile
|
||||||
|
import com.topjohnwu.superuser.io.SuFileInputStream
|
||||||
|
import com.topjohnwu.superuser.io.SuFileOutputStream
|
||||||
|
import io.reactivex.Single
|
||||||
|
import org.kamranzafar.jtar.TarEntry
|
||||||
|
import org.kamranzafar.jtar.TarHeader
|
||||||
|
import org.kamranzafar.jtar.TarInputStream
|
||||||
|
import org.kamranzafar.jtar.TarOutputStream
|
||||||
|
import timber.log.Timber
|
||||||
|
import java.io.File
|
||||||
|
import java.io.FileOutputStream
|
||||||
|
import java.io.IOException
|
||||||
|
import java.io.InputStream
|
||||||
|
import java.nio.ByteBuffer
|
||||||
|
import java.util.zip.ZipEntry
|
||||||
|
import java.util.zip.ZipInputStream
|
||||||
|
|
||||||
|
abstract class MagiskInstallImpl : FlashResultListener {
|
||||||
|
|
||||||
|
protected lateinit var installDir: File
|
||||||
|
private lateinit var srcBoot: String
|
||||||
|
private lateinit var destFile: File
|
||||||
|
private lateinit var zipUri: Uri
|
||||||
|
|
||||||
|
private val console: MutableList<String>
|
||||||
|
private val logs: MutableList<String>
|
||||||
|
private var tarOut: TarOutputStream? = null
|
||||||
|
|
||||||
|
private val service: GithubRawServices by inject()
|
||||||
|
protected val context: Context by inject()
|
||||||
|
|
||||||
|
protected constructor() {
|
||||||
|
console = NOPList.getInstance()
|
||||||
|
logs = NOPList.getInstance()
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(zip: Uri, out: MutableList<String>, err: MutableList<String>) {
|
||||||
|
console = out
|
||||||
|
logs = err
|
||||||
|
zipUri = zip
|
||||||
|
installDir = File(get<Context>(Protected).filesDir.parent, "install")
|
||||||
|
"rm -rf $installDir".sh()
|
||||||
|
installDir.mkdirs()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun findImage(): Boolean {
|
||||||
|
srcBoot = "find_boot_image; echo \"\$BOOTIMAGE\"".fsh()
|
||||||
|
if (srcBoot.isEmpty()) {
|
||||||
|
console.add("! Unable to detect target image")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
console.add("- Target image: $srcBoot")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun findSecondaryImage(): Boolean {
|
||||||
|
val slot = "echo \$SLOT".fsh()
|
||||||
|
val target = if (slot == "_a") "_b" else "_a"
|
||||||
|
console.add("- Target slot: $target")
|
||||||
|
srcBoot = arrayOf(
|
||||||
|
"SLOT=$target",
|
||||||
|
"find_boot_image",
|
||||||
|
"SLOT=$slot",
|
||||||
|
"echo \"\$BOOTIMAGE\"").fsh()
|
||||||
|
if (srcBoot.isEmpty()) {
|
||||||
|
console.add("! Unable to detect target image")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
console.add("- Target image: $srcBoot")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun extractZip(): Boolean {
|
||||||
|
val arch: String
|
||||||
|
arch = if (Build.VERSION.SDK_INT >= 21) {
|
||||||
|
val abis = listOf(*Build.SUPPORTED_ABIS)
|
||||||
|
if (abis.contains("x86")) "x86" else "arm"
|
||||||
|
} else {
|
||||||
|
if (TextUtils.equals(Build.CPU_ABI, "x86")) "x86" else "arm"
|
||||||
|
}
|
||||||
|
|
||||||
|
console.add("- Device platform: " + Build.CPU_ABI)
|
||||||
|
|
||||||
|
try {
|
||||||
|
ZipInputStream(context.readUri(zipUri).buffered()).use { zi ->
|
||||||
|
lateinit var ze: ZipEntry
|
||||||
|
while (zi.nextEntry?.let { ze = it } != null) {
|
||||||
|
if (ze.isDirectory)
|
||||||
|
continue
|
||||||
|
var name: String? = null
|
||||||
|
val names = arrayOf("$arch/", "common/", "META-INF/com/google/android/update-binary")
|
||||||
|
for (n in names) {
|
||||||
|
ze.name.run {
|
||||||
|
if (startsWith(n)) {
|
||||||
|
name = substring(lastIndexOf('/') + 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
name ?: continue
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if (name == null && ze.name.startsWith("chromeos/"))
|
||||||
|
name = ze.name
|
||||||
|
if (name == null)
|
||||||
|
continue
|
||||||
|
val dest = if (installDir is SuFile)
|
||||||
|
SuFile(installDir, name)
|
||||||
|
else
|
||||||
|
File(installDir, name)
|
||||||
|
dest.parentFile!!.mkdirs()
|
||||||
|
SuFileOutputStream(dest).use { zi.copyTo(it) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: IOException) {
|
||||||
|
console.add("! Cannot unzip zip")
|
||||||
|
Timber.e(e)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
val init64 = SuFile.open(installDir, "magiskinit64")
|
||||||
|
if (Build.VERSION.SDK_INT >= 21 && Build.SUPPORTED_64_BIT_ABIS.isNotEmpty()) {
|
||||||
|
init64.renameTo(SuFile.open(installDir, "magiskinit"))
|
||||||
|
} else {
|
||||||
|
init64.delete()
|
||||||
|
}
|
||||||
|
"cd $installDir; chmod 755 *".sh()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun newEntry(name: String, size: Long): TarEntry {
|
||||||
|
console.add("-- Writing: $name")
|
||||||
|
return TarEntry(TarHeader.createHeader(name, size, 0, false, 420 /* 0644 */))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(IOException::class)
|
||||||
|
private fun handleTar(input: InputStream) {
|
||||||
|
console.add("- Processing tar file")
|
||||||
|
var vbmeta = false
|
||||||
|
val tarOut = TarOutputStream(destFile)
|
||||||
|
this.tarOut = tarOut
|
||||||
|
TarInputStream(input).use { tarIn ->
|
||||||
|
lateinit var entry: TarEntry
|
||||||
|
while (tarIn.nextEntry?.let { entry = it } != null) {
|
||||||
|
if (entry.name.contains("boot.img") || entry.name.contains("recovery.img")) {
|
||||||
|
val name = entry.name
|
||||||
|
console.add("-- Extracting: $name")
|
||||||
|
val extract = File(installDir, name)
|
||||||
|
FileOutputStream(extract).use { tarIn.copyTo(it) }
|
||||||
|
if (name.contains(".lz4")) {
|
||||||
|
console.add("-- Decompressing: $name")
|
||||||
|
"./magiskboot --decompress $extract".sh()
|
||||||
|
}
|
||||||
|
} else if (entry.name.contains("vbmeta.img")) {
|
||||||
|
vbmeta = true
|
||||||
|
val buf = ByteBuffer.allocate(256)
|
||||||
|
buf.put("AVB0".toByteArray()) // magic
|
||||||
|
buf.putInt(1) // required_libavb_version_major
|
||||||
|
buf.putInt(120, 2) // flags
|
||||||
|
buf.position(128) // release_string
|
||||||
|
buf.put("avbtool 1.1.0".toByteArray())
|
||||||
|
tarOut.putNextEntry(newEntry("vbmeta.img", 256))
|
||||||
|
tarOut.write(buf.array())
|
||||||
|
} else {
|
||||||
|
console.add("-- Writing: " + entry.name)
|
||||||
|
tarOut.putNextEntry(entry)
|
||||||
|
tarIn.copyTo(tarOut)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val boot = SuFile.open(installDir, "boot.img")
|
||||||
|
val recovery = SuFile.open(installDir, "recovery.img")
|
||||||
|
if (vbmeta && recovery.exists() && boot.exists()) {
|
||||||
|
// Install Magisk to recovery
|
||||||
|
srcBoot = recovery.path
|
||||||
|
// Repack boot image to prevent restore
|
||||||
|
arrayOf(
|
||||||
|
"./magiskboot --unpack boot.img",
|
||||||
|
"./magiskboot --repack boot.img",
|
||||||
|
"./magiskboot --cleanup",
|
||||||
|
"mv new-boot.img boot.img").sh()
|
||||||
|
SuFileInputStream(boot).use {
|
||||||
|
tarOut.putNextEntry(newEntry("boot.img", boot.length()))
|
||||||
|
it.copyTo(tarOut)
|
||||||
|
}
|
||||||
|
boot.delete()
|
||||||
|
} else {
|
||||||
|
if (!boot.exists()) {
|
||||||
|
console.add("! No boot image found")
|
||||||
|
throw IOException()
|
||||||
|
}
|
||||||
|
srcBoot = boot.path
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleFile(uri: Uri): Boolean {
|
||||||
|
try {
|
||||||
|
context.readUri(uri).buffered().use {
|
||||||
|
it.mark(500)
|
||||||
|
val magic = ByteArray(5)
|
||||||
|
if (it.skip(257) != 257L || it.read(magic) != magic.size) {
|
||||||
|
console.add("! Invalid file")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
it.reset()
|
||||||
|
if (magic.contentEquals("ustar".toByteArray())) {
|
||||||
|
destFile = File(Config.downloadDirectory, "magisk_patched.tar")
|
||||||
|
handleTar(it)
|
||||||
|
} else {
|
||||||
|
// Raw image
|
||||||
|
srcBoot = File(installDir, "boot.img").path
|
||||||
|
destFile = File(Config.downloadDirectory, "magisk_patched.img")
|
||||||
|
console.add("- Copying image to cache")
|
||||||
|
FileOutputStream(srcBoot).use { out -> it.copyTo(out) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: IOException) {
|
||||||
|
console.add("! Process error")
|
||||||
|
Timber.e(e)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun patchBoot(): Boolean {
|
||||||
|
var isSigned = false
|
||||||
|
try {
|
||||||
|
SuFileInputStream(srcBoot).use {
|
||||||
|
isSigned = SignBoot.verifySignature(it, null)
|
||||||
|
if (isSigned) {
|
||||||
|
console.add("- Boot image is signed with AVB 1.0")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: IOException) {
|
||||||
|
console.add("! Unable to check signature")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!("KEEPFORCEENCRYPT=${Info.keepEnc} KEEPVERITY=${Info.keepVerity} " +
|
||||||
|
"RECOVERYMODE=${Info.recovery} sh update-binary " +
|
||||||
|
"sh boot_patch.sh $srcBoot").sh().isSuccess) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
val job = Shell.sh(
|
||||||
|
"./magiskboot --cleanup",
|
||||||
|
"mv bin/busybox busybox",
|
||||||
|
"rm -rf magisk.apk bin boot.img update-binary",
|
||||||
|
"cd /")
|
||||||
|
|
||||||
|
val patched = File(installDir, "new-boot.img")
|
||||||
|
if (isSigned) {
|
||||||
|
console.add("- Signing boot image with verity keys")
|
||||||
|
val signed = File(installDir, "signed.img")
|
||||||
|
try {
|
||||||
|
withStreams(SuFileInputStream(patched), signed.outputStream().buffered()) {
|
||||||
|
input, out -> SignBoot.doSignature("/boot", input, out, null, null)
|
||||||
|
}
|
||||||
|
} catch (e: IOException) {
|
||||||
|
console.add("! Unable to sign image")
|
||||||
|
Timber.e(e)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
job.add("mv -f $signed $patched")
|
||||||
|
}
|
||||||
|
job.exec()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun flashBoot(): Boolean {
|
||||||
|
if (!"direct_install $installDir $srcBoot".sh().isSuccess)
|
||||||
|
return false
|
||||||
|
arrayOf(
|
||||||
|
"(KEEPVERITY=${Info.keepVerity} patch_dtb_partitions)",
|
||||||
|
"run_migrations"
|
||||||
|
).sh()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun storeBoot(): Boolean {
|
||||||
|
val patched = SuFile.open(installDir, "new-boot.img")
|
||||||
|
try {
|
||||||
|
val os = tarOut?.let {
|
||||||
|
it.putNextEntry(newEntry(
|
||||||
|
if (srcBoot.contains("recovery")) "recovery.img" else "boot.img",
|
||||||
|
patched.length()))
|
||||||
|
tarOut = null
|
||||||
|
it
|
||||||
|
} ?: destFile.outputStream()
|
||||||
|
patched.suInputStream().use { it.copyTo(os); os.close() }
|
||||||
|
} catch (e: IOException) {
|
||||||
|
console.add("! Failed to output to $destFile")
|
||||||
|
Timber.e(e)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
patched.delete()
|
||||||
|
console.add("")
|
||||||
|
console.add("****************************")
|
||||||
|
console.add(" Output file is placed in ")
|
||||||
|
console.add(" $destFile ")
|
||||||
|
console.add("****************************")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun postOTA(): Boolean {
|
||||||
|
val bootctl = SuFile("/data/adb/bootctl")
|
||||||
|
try {
|
||||||
|
withStreams(service.fetchBootctl().blockingGet().byteStream(), bootctl.suOutputStream()) {
|
||||||
|
input, out -> input.copyTo(out)
|
||||||
|
}
|
||||||
|
} catch (e: IOException) {
|
||||||
|
console.add("! Unable to download bootctl")
|
||||||
|
Timber.e(e)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
"post_ota ${bootctl.parent}".sh()
|
||||||
|
|
||||||
|
console.add("***************************************")
|
||||||
|
console.add(" Next reboot will boot to second slot!")
|
||||||
|
console.add("***************************************")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun String.sh() = Shell.sh(this).to(console, logs).exec()
|
||||||
|
private fun Array<String>.sh() = Shell.sh(*this).to(console, logs).exec()
|
||||||
|
private fun String.fsh() = ShellUtils.fastCmd(this)
|
||||||
|
private fun Array<String>.fsh() = ShellUtils.fastCmd(*this)
|
||||||
|
|
||||||
|
protected fun doPatchFile(patchFile: Uri) =
|
||||||
|
extractZip() && handleFile(patchFile) && patchBoot() && storeBoot()
|
||||||
|
|
||||||
|
protected fun direct() = findImage() && extractZip() && patchBoot() && flashBoot()
|
||||||
|
|
||||||
|
protected fun secondSlot() =
|
||||||
|
findSecondaryImage() && extractZip() && patchBoot() && flashBoot() && postOTA()
|
||||||
|
|
||||||
|
protected fun fixEnv(zip: File): Boolean {
|
||||||
|
installDir = SuFile("/data/adb/magisk")
|
||||||
|
Shell.su("rm -rf /data/adb/magisk/*").exec()
|
||||||
|
zipUri = zip.toUri()
|
||||||
|
return extractZip() && Shell.su("fix_env").exec().isSuccess
|
||||||
|
}
|
||||||
|
|
||||||
|
@WorkerThread
|
||||||
|
protected abstract fun operations(): Boolean
|
||||||
|
|
||||||
|
fun exec() {
|
||||||
|
Single.fromCallable { operations() }.subscribeK { onResult(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -1,382 +1,77 @@
|
|||||||
package com.topjohnwu.magisk.core.tasks
|
package com.topjohnwu.magisk.core.tasks
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Intent
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.widget.Toast
|
||||||
import android.text.TextUtils
|
import androidx.core.os.postDelayed
|
||||||
import androidx.annotation.MainThread
|
import androidx.localbroadcastmanager.content.LocalBroadcastManager
|
||||||
import androidx.annotation.WorkerThread
|
import com.topjohnwu.magisk.R
|
||||||
import androidx.core.net.toUri
|
import com.topjohnwu.magisk.core.utils.Utils
|
||||||
import com.topjohnwu.magisk.core.Config
|
import com.topjohnwu.magisk.extensions.reboot
|
||||||
import com.topjohnwu.magisk.core.Info
|
import com.topjohnwu.magisk.model.events.dialog.EnvFixDialog
|
||||||
import com.topjohnwu.magisk.data.network.GithubRawServices
|
|
||||||
import com.topjohnwu.magisk.di.Protected
|
|
||||||
import com.topjohnwu.magisk.extensions.*
|
|
||||||
import com.topjohnwu.magisk.net.Networking
|
|
||||||
import com.topjohnwu.signing.SignBoot
|
|
||||||
import com.topjohnwu.superuser.Shell
|
import com.topjohnwu.superuser.Shell
|
||||||
import com.topjohnwu.superuser.ShellUtils
|
import com.topjohnwu.superuser.internal.UiThreadHandler
|
||||||
import com.topjohnwu.superuser.internal.NOPList
|
|
||||||
import com.topjohnwu.superuser.io.SuFile
|
|
||||||
import com.topjohnwu.superuser.io.SuFileInputStream
|
|
||||||
import com.topjohnwu.superuser.io.SuFileOutputStream
|
|
||||||
import io.reactivex.Single
|
|
||||||
import org.kamranzafar.jtar.TarEntry
|
|
||||||
import org.kamranzafar.jtar.TarHeader
|
|
||||||
import org.kamranzafar.jtar.TarInputStream
|
|
||||||
import org.kamranzafar.jtar.TarOutputStream
|
|
||||||
import timber.log.Timber
|
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.FileOutputStream
|
|
||||||
import java.io.IOException
|
|
||||||
import java.io.InputStream
|
|
||||||
import java.nio.ByteBuffer
|
|
||||||
import java.util.zip.ZipEntry
|
|
||||||
import java.util.zip.ZipInputStream
|
|
||||||
|
|
||||||
abstract class MagiskInstaller {
|
sealed class MagiskInstaller(
|
||||||
|
file: Uri,
|
||||||
|
private val console: MutableList<String>,
|
||||||
|
logs: MutableList<String>,
|
||||||
|
private val resultListener: FlashResultListener
|
||||||
|
) : MagiskInstallImpl(file, console, logs) {
|
||||||
|
|
||||||
protected lateinit var installDir: File
|
override fun onResult(success: Boolean) {
|
||||||
private lateinit var srcBoot: String
|
if (success) {
|
||||||
private lateinit var destFile: File
|
console.add("- All done!")
|
||||||
private lateinit var zipUri: Uri
|
|
||||||
|
|
||||||
private val console: MutableList<String>
|
|
||||||
private val logs: MutableList<String>
|
|
||||||
private var tarOut: TarOutputStream? = null
|
|
||||||
|
|
||||||
private val service: GithubRawServices by inject()
|
|
||||||
private val context: Context by inject()
|
|
||||||
|
|
||||||
protected constructor() {
|
|
||||||
console = NOPList.getInstance()
|
|
||||||
logs = NOPList.getInstance()
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(zip: Uri, out: MutableList<String>, err: MutableList<String>) {
|
|
||||||
console = out
|
|
||||||
logs = err
|
|
||||||
zipUri = zip
|
|
||||||
installDir = File(get<Context>(Protected).filesDir.parent, "install")
|
|
||||||
"rm -rf $installDir".sh()
|
|
||||||
installDir.mkdirs()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun findImage(): Boolean {
|
|
||||||
srcBoot = "find_boot_image; echo \"\$BOOTIMAGE\"".fsh()
|
|
||||||
if (srcBoot.isEmpty()) {
|
|
||||||
console.add("! Unable to detect target image")
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
console.add("- Target image: $srcBoot")
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun findSecondaryImage(): Boolean {
|
|
||||||
val slot = "echo \$SLOT".fsh()
|
|
||||||
val target = if (slot == "_a") "_b" else "_a"
|
|
||||||
console.add("- Target slot: $target")
|
|
||||||
srcBoot = arrayOf(
|
|
||||||
"SLOT=$target",
|
|
||||||
"find_boot_image",
|
|
||||||
"SLOT=$slot",
|
|
||||||
"echo \"\$BOOTIMAGE\"").fsh()
|
|
||||||
if (srcBoot.isEmpty()) {
|
|
||||||
console.add("! Unable to detect target image")
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
console.add("- Target image: $srcBoot")
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun extractZip(): Boolean {
|
|
||||||
val arch: String
|
|
||||||
arch = if (Build.VERSION.SDK_INT >= 21) {
|
|
||||||
val abis = listOf(*Build.SUPPORTED_ABIS)
|
|
||||||
if (abis.contains("x86")) "x86" else "arm"
|
|
||||||
} else {
|
} else {
|
||||||
if (TextUtils.equals(Build.CPU_ABI, "x86")) "x86" else "arm"
|
Shell.sh("rm -rf $installDir").submit()
|
||||||
|
console.add("! Installation failed")
|
||||||
}
|
}
|
||||||
|
resultListener.onResult(success)
|
||||||
console.add("- Device platform: " + Build.CPU_ABI)
|
|
||||||
|
|
||||||
try {
|
|
||||||
ZipInputStream(context.readUri(zipUri).buffered()).use { zi ->
|
|
||||||
lateinit var ze: ZipEntry
|
|
||||||
while (zi.nextEntry?.let { ze = it } != null) {
|
|
||||||
if (ze.isDirectory)
|
|
||||||
continue
|
|
||||||
var name: String? = null
|
|
||||||
val names = arrayOf("$arch/", "common/", "META-INF/com/google/android/update-binary")
|
|
||||||
for (n in names) {
|
|
||||||
ze.name.run {
|
|
||||||
if (startsWith(n)) {
|
|
||||||
name = substring(lastIndexOf('/') + 1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
name ?: continue
|
|
||||||
break
|
|
||||||
}
|
|
||||||
if (name == null && ze.name.startsWith("chromeos/"))
|
|
||||||
name = ze.name
|
|
||||||
if (name == null)
|
|
||||||
continue
|
|
||||||
val dest = if (installDir is SuFile)
|
|
||||||
SuFile(installDir, name)
|
|
||||||
else
|
|
||||||
File(installDir, name)
|
|
||||||
dest.parentFile!!.mkdirs()
|
|
||||||
SuFileOutputStream(dest).use { zi.copyTo(it) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e: IOException) {
|
|
||||||
console.add("! Cannot unzip zip")
|
|
||||||
Timber.e(e)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
val init64 = SuFile.open(installDir, "magiskinit64")
|
|
||||||
if (Build.VERSION.SDK_INT >= 21 && Build.SUPPORTED_64_BIT_ABIS.isNotEmpty()) {
|
|
||||||
init64.renameTo(SuFile.open(installDir, "magiskinit"))
|
|
||||||
} else {
|
|
||||||
init64.delete()
|
|
||||||
}
|
|
||||||
"cd $installDir; chmod 755 *".sh()
|
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun newEntry(name: String, size: Long): TarEntry {
|
class Patch(
|
||||||
console.add("-- Writing: $name")
|
file: Uri,
|
||||||
return TarEntry(TarHeader.createHeader(name, size, 0, false, 420 /* 0644 */))
|
private val uri: Uri,
|
||||||
|
console: MutableList<String>,
|
||||||
|
logs: MutableList<String>,
|
||||||
|
resultListener: FlashResultListener
|
||||||
|
) : MagiskInstaller(file, console, logs, resultListener) {
|
||||||
|
override fun operations() = doPatchFile(uri)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(IOException::class)
|
class SecondSlot(
|
||||||
private fun handleTar(input: InputStream) {
|
file: Uri,
|
||||||
console.add("- Processing tar file")
|
console: MutableList<String>,
|
||||||
var vbmeta = false
|
logs: MutableList<String>,
|
||||||
val tarOut = TarOutputStream(destFile)
|
resultListener: FlashResultListener
|
||||||
this.tarOut = tarOut
|
) : MagiskInstaller(file, console, logs, resultListener) {
|
||||||
TarInputStream(input).use { tarIn ->
|
override fun operations() = secondSlot()
|
||||||
lateinit var entry: TarEntry
|
|
||||||
while (tarIn.nextEntry?.let { entry = it } != null) {
|
|
||||||
if (entry.name.contains("boot.img") || entry.name.contains("recovery.img")) {
|
|
||||||
val name = entry.name
|
|
||||||
console.add("-- Extracting: $name")
|
|
||||||
val extract = File(installDir, name)
|
|
||||||
FileOutputStream(extract).use { tarIn.copyTo(it) }
|
|
||||||
if (name.contains(".lz4")) {
|
|
||||||
console.add("-- Decompressing: $name")
|
|
||||||
"./magiskboot --decompress $extract".sh()
|
|
||||||
}
|
|
||||||
} else if (entry.name.contains("vbmeta.img")) {
|
|
||||||
vbmeta = true
|
|
||||||
val buf = ByteBuffer.allocate(256)
|
|
||||||
buf.put("AVB0".toByteArray()) // magic
|
|
||||||
buf.putInt(1) // required_libavb_version_major
|
|
||||||
buf.putInt(120, 2) // flags
|
|
||||||
buf.position(128) // release_string
|
|
||||||
buf.put("avbtool 1.1.0".toByteArray())
|
|
||||||
tarOut.putNextEntry(newEntry("vbmeta.img", 256))
|
|
||||||
tarOut.write(buf.array())
|
|
||||||
} else {
|
|
||||||
console.add("-- Writing: " + entry.name)
|
|
||||||
tarOut.putNextEntry(entry)
|
|
||||||
tarIn.copyTo(tarOut)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val boot = SuFile.open(installDir, "boot.img")
|
|
||||||
val recovery = SuFile.open(installDir, "recovery.img")
|
|
||||||
if (vbmeta && recovery.exists() && boot.exists()) {
|
|
||||||
// Install Magisk to recovery
|
|
||||||
srcBoot = recovery.path
|
|
||||||
// Repack boot image to prevent restore
|
|
||||||
arrayOf(
|
|
||||||
"./magiskboot --unpack boot.img",
|
|
||||||
"./magiskboot --repack boot.img",
|
|
||||||
"./magiskboot --cleanup",
|
|
||||||
"mv new-boot.img boot.img").sh()
|
|
||||||
SuFileInputStream(boot).use {
|
|
||||||
tarOut.putNextEntry(newEntry("boot.img", boot.length()))
|
|
||||||
it.copyTo(tarOut)
|
|
||||||
}
|
|
||||||
boot.delete()
|
|
||||||
} else {
|
|
||||||
if (!boot.exists()) {
|
|
||||||
console.add("! No boot image found")
|
|
||||||
throw IOException()
|
|
||||||
}
|
|
||||||
srcBoot = boot.path
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleFile(uri: Uri): Boolean {
|
class Direct(
|
||||||
try {
|
file: Uri,
|
||||||
context.readUri(uri).buffered().use {
|
console: MutableList<String>,
|
||||||
it.mark(500)
|
logs: MutableList<String>,
|
||||||
val magic = ByteArray(5)
|
resultListener: FlashResultListener
|
||||||
if (it.skip(257) != 257L || it.read(magic) != magic.size) {
|
) : MagiskInstaller(file, console, logs, resultListener) {
|
||||||
console.add("! Invalid file")
|
override fun operations() = direct()
|
||||||
return false
|
|
||||||
}
|
|
||||||
it.reset()
|
|
||||||
if (magic.contentEquals("ustar".toByteArray())) {
|
|
||||||
destFile = File(Config.downloadDirectory, "magisk_patched.tar")
|
|
||||||
handleTar(it)
|
|
||||||
} else {
|
|
||||||
// Raw image
|
|
||||||
srcBoot = File(installDir, "boot.img").path
|
|
||||||
destFile = File(Config.downloadDirectory, "magisk_patched.img")
|
|
||||||
console.add("- Copying image to cache")
|
|
||||||
FileOutputStream(srcBoot).use { out -> it.copyTo(out) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e: IOException) {
|
|
||||||
console.add("! Process error")
|
|
||||||
Timber.e(e)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun patchBoot(): Boolean {
|
|
||||||
var isSigned = false
|
|
||||||
try {
|
|
||||||
SuFileInputStream(srcBoot).use {
|
|
||||||
isSigned = SignBoot.verifySignature(it, null)
|
|
||||||
if (isSigned) {
|
|
||||||
console.add("- Boot image is signed with AVB 1.0")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e: IOException) {
|
|
||||||
console.add("! Unable to check signature")
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!("KEEPFORCEENCRYPT=${Info.keepEnc} KEEPVERITY=${Info.keepVerity} " +
|
|
||||||
"RECOVERYMODE=${Info.recovery} sh update-binary " +
|
|
||||||
"sh boot_patch.sh $srcBoot").sh().isSuccess) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
val job = Shell.sh(
|
|
||||||
"./magiskboot --cleanup",
|
|
||||||
"mv bin/busybox busybox",
|
|
||||||
"rm -rf magisk.apk bin boot.img update-binary",
|
|
||||||
"cd /")
|
|
||||||
|
|
||||||
val patched = File(installDir, "new-boot.img")
|
|
||||||
if (isSigned) {
|
|
||||||
console.add("- Signing boot image with verity keys")
|
|
||||||
val signed = File(installDir, "signed.img")
|
|
||||||
try {
|
|
||||||
withStreams(SuFileInputStream(patched), signed.outputStream().buffered()) {
|
|
||||||
input, out -> SignBoot.doSignature("/boot", input, out, null, null)
|
|
||||||
}
|
|
||||||
} catch (e: IOException) {
|
|
||||||
console.add("! Unable to sign image")
|
|
||||||
Timber.e(e)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
job.add("mv -f $signed $patched")
|
|
||||||
}
|
|
||||||
job.exec()
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun flashBoot(): Boolean {
|
|
||||||
if (!"direct_install $installDir $srcBoot".sh().isSuccess)
|
|
||||||
return false
|
|
||||||
arrayOf(
|
|
||||||
"(KEEPVERITY=${Info.keepVerity} patch_dtb_partitions)",
|
|
||||||
"run_migrations"
|
|
||||||
).sh()
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun storeBoot(): Boolean {
|
|
||||||
val patched = SuFile.open(installDir, "new-boot.img")
|
|
||||||
try {
|
|
||||||
val os = tarOut?.let {
|
|
||||||
it.putNextEntry(newEntry(
|
|
||||||
if (srcBoot.contains("recovery")) "recovery.img" else "boot.img",
|
|
||||||
patched.length()))
|
|
||||||
tarOut = null
|
|
||||||
it
|
|
||||||
} ?: destFile.outputStream()
|
|
||||||
patched.suInputStream().use { it.copyTo(os); os.close() }
|
|
||||||
} catch (e: IOException) {
|
|
||||||
console.add("! Failed to output to $destFile")
|
|
||||||
Timber.e(e)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
patched.delete()
|
|
||||||
console.add("")
|
|
||||||
console.add("****************************")
|
|
||||||
console.add(" Output file is placed in ")
|
|
||||||
console.add(" $destFile ")
|
|
||||||
console.add("****************************")
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun postOTA(): Boolean {
|
|
||||||
val bootctl = SuFile("/data/adb/bootctl")
|
|
||||||
try {
|
|
||||||
withStreams(service.fetchBootctl().blockingGet().byteStream(), bootctl.suOutputStream()) {
|
|
||||||
input, out -> input.copyTo(out)
|
|
||||||
}
|
|
||||||
} catch (e: IOException) {
|
|
||||||
console.add("! Unable to download bootctl")
|
|
||||||
Timber.e(e)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
"post_ota ${bootctl.parent}".sh()
|
|
||||||
|
|
||||||
console.add("***************************************")
|
|
||||||
console.add(" Next reboot will boot to second slot!")
|
|
||||||
console.add("***************************************")
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun String.sh() = Shell.sh(this).to(console, logs).exec()
|
|
||||||
private fun Array<String>.sh() = Shell.sh(*this).to(console, logs).exec()
|
|
||||||
private fun String.fsh() = ShellUtils.fastCmd(this)
|
|
||||||
private fun Array<String>.fsh() = ShellUtils.fastCmd(*this)
|
|
||||||
|
|
||||||
protected fun doPatchFile(patchFile: Uri) =
|
|
||||||
extractZip() && handleFile(patchFile) && patchBoot() && storeBoot()
|
|
||||||
|
|
||||||
protected fun direct() = findImage() && extractZip() && patchBoot() && flashBoot()
|
|
||||||
|
|
||||||
protected fun secondSlot() =
|
|
||||||
findSecondaryImage() && extractZip() && patchBoot() && flashBoot() && postOTA()
|
|
||||||
|
|
||||||
protected fun fixEnv(): Boolean {
|
|
||||||
val context = get<Context>()
|
|
||||||
val zip: File = context.cachedFile("magisk.zip")
|
|
||||||
|
|
||||||
installDir = SuFile("/data/adb/magisk")
|
|
||||||
Shell.su("rm -rf /data/adb/magisk/*").exec()
|
|
||||||
|
|
||||||
if (!ShellUtils.checkSum("MD5", zip, Info.remote.magisk.md5))
|
|
||||||
Networking.get(Info.remote.magisk.link).execForFile(zip)
|
|
||||||
|
|
||||||
zipUri = zip.toUri()
|
|
||||||
return extractZip() && Shell.su("fix_env").exec().isSuccess
|
|
||||||
}
|
|
||||||
|
|
||||||
@WorkerThread
|
|
||||||
protected abstract fun operations(): Boolean
|
|
||||||
|
|
||||||
@MainThread
|
|
||||||
protected abstract fun onResult(success: Boolean)
|
|
||||||
|
|
||||||
fun exec() {
|
|
||||||
Single.fromCallable { operations() }.subscribeK { onResult(it) }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class EnvFixTask(
|
||||||
|
private val zip: File
|
||||||
|
) : MagiskInstallImpl() {
|
||||||
|
override fun operations() = fixEnv(zip)
|
||||||
|
|
||||||
|
override fun onResult(success: Boolean) {
|
||||||
|
LocalBroadcastManager.getInstance(context).sendBroadcast(Intent(EnvFixDialog.DISMISS))
|
||||||
|
Utils.toast(
|
||||||
|
if (success) R.string.reboot_delay_toast else R.string.setup_fail,
|
||||||
|
Toast.LENGTH_LONG
|
||||||
|
)
|
||||||
|
if (success)
|
||||||
|
UiThreadHandler.handler.postDelayed(5000) { reboot() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -12,13 +12,13 @@ import androidx.databinding.ObservableArrayList
|
|||||||
import com.topjohnwu.magisk.R
|
import com.topjohnwu.magisk.R
|
||||||
import com.topjohnwu.magisk.core.Config
|
import com.topjohnwu.magisk.core.Config
|
||||||
import com.topjohnwu.magisk.core.Const
|
import com.topjohnwu.magisk.core.Const
|
||||||
|
import com.topjohnwu.magisk.core.tasks.FlashResultListener
|
||||||
|
import com.topjohnwu.magisk.core.tasks.Flashing
|
||||||
|
import com.topjohnwu.magisk.core.tasks.MagiskInstaller
|
||||||
import com.topjohnwu.magisk.extensions.*
|
import com.topjohnwu.magisk.extensions.*
|
||||||
import com.topjohnwu.magisk.model.binding.BindingAdapter
|
import com.topjohnwu.magisk.model.binding.BindingAdapter
|
||||||
import com.topjohnwu.magisk.model.entity.recycler.ConsoleItem
|
import com.topjohnwu.magisk.model.entity.recycler.ConsoleItem
|
||||||
import com.topjohnwu.magisk.model.events.SnackbarEvent
|
import com.topjohnwu.magisk.model.events.SnackbarEvent
|
||||||
import com.topjohnwu.magisk.model.flash.FlashResultListener
|
|
||||||
import com.topjohnwu.magisk.model.flash.Flashing
|
|
||||||
import com.topjohnwu.magisk.model.flash.Patching
|
|
||||||
import com.topjohnwu.magisk.ui.base.BaseViewModel
|
import com.topjohnwu.magisk.ui.base.BaseViewModel
|
||||||
import com.topjohnwu.magisk.ui.base.diffListOf
|
import com.topjohnwu.magisk.ui.base.diffListOf
|
||||||
import com.topjohnwu.magisk.ui.base.itemBindingOf
|
import com.topjohnwu.magisk.ui.base.itemBindingOf
|
||||||
@ -60,26 +60,26 @@ class FlashViewModel(
|
|||||||
Const.Value.UNINSTALL -> Flashing
|
Const.Value.UNINSTALL -> Flashing
|
||||||
.Uninstall(installer, outItems, logItems, this)
|
.Uninstall(installer, outItems, logItems, this)
|
||||||
.exec()
|
.exec()
|
||||||
Const.Value.FLASH_MAGISK -> Patching
|
Const.Value.FLASH_MAGISK -> MagiskInstaller
|
||||||
.Direct(installer, outItems, logItems, this)
|
.Direct(installer, outItems, logItems, this)
|
||||||
.exec()
|
.exec()
|
||||||
Const.Value.FLASH_INACTIVE_SLOT -> Patching
|
Const.Value.FLASH_INACTIVE_SLOT -> MagiskInstaller
|
||||||
.SecondSlot(installer, outItems, logItems, this)
|
.SecondSlot(installer, outItems, logItems, this)
|
||||||
.exec()
|
.exec()
|
||||||
Const.Value.PATCH_FILE -> Patching
|
Const.Value.PATCH_FILE -> MagiskInstaller
|
||||||
.File(installer, uri ?: return, outItems, logItems, this)
|
.Patch(installer, uri ?: return, outItems, logItems, this)
|
||||||
.exec()
|
.exec()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onResult(isSuccess: Boolean) {
|
override fun onResult(success: Boolean) {
|
||||||
state = if (isSuccess) State.LOADED else State.LOADING_FAILED
|
state = if (success) State.LOADED else State.LOADING_FAILED
|
||||||
behaviorText.value = when {
|
behaviorText.value = when {
|
||||||
isSuccess -> resources.getString(R.string.done)
|
success -> resources.getString(R.string.done)
|
||||||
else -> resources.getString(R.string.failure)
|
else -> resources.getString(R.string.failure)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isSuccess) {
|
if (success) {
|
||||||
Handler().postDelayed(500) {
|
Handler().postDelayed(500) {
|
||||||
showRestartTitle.value = true
|
showRestartTitle.value = true
|
||||||
}
|
}
|
||||||
|
@ -31,7 +31,10 @@ sealed class Configuration : Parcelable {
|
|||||||
@Parcelize
|
@Parcelize
|
||||||
object Uninstall : Configuration()
|
object Uninstall : Configuration()
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
object EnvFix : Configuration()
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data class Patch(val fileUri: Uri) : Configuration()
|
data class Patch(val fileUri: Uri) : Configuration()
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -20,7 +20,7 @@ sealed class DownloadSubject : Parcelable {
|
|||||||
open val title: String get() = file.name
|
open val title: String get() = file.name
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data class Module(
|
class Module(
|
||||||
val module: Repo,
|
val module: Repo,
|
||||||
val configuration: Configuration
|
val configuration: Configuration
|
||||||
) : DownloadSubject() {
|
) : DownloadSubject() {
|
||||||
@ -33,7 +33,7 @@ sealed class DownloadSubject : Parcelable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data class Manager(
|
class Manager(
|
||||||
val configuration: Configuration.APK
|
val configuration: Configuration.APK
|
||||||
) : DownloadSubject() {
|
) : DownloadSubject() {
|
||||||
|
|
||||||
@ -53,13 +53,13 @@ sealed class DownloadSubject : Parcelable {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
sealed class Magisk : DownloadSubject() {
|
abstract class Magisk : DownloadSubject() {
|
||||||
|
|
||||||
abstract val configuration: Configuration
|
abstract val configuration: Configuration
|
||||||
val magisk: MagiskJson = Info.remote.magisk
|
val magisk: MagiskJson = Info.remote.magisk
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data class Flash(
|
private class DownloadInternal(
|
||||||
override val configuration: Configuration
|
override val configuration: Configuration
|
||||||
) : Magisk() {
|
) : Magisk() {
|
||||||
override val url: String get() = magisk.link
|
override val url: String get() = magisk.link
|
||||||
@ -72,8 +72,8 @@ sealed class DownloadSubject : Parcelable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
class Uninstall : Magisk() {
|
private class Uninstall : Magisk() {
|
||||||
override val configuration: Configuration get() = Configuration.Uninstall
|
override val configuration get() = Configuration.Uninstall
|
||||||
override val url: String get() = Info.remote.uninstaller.link
|
override val url: String get() = Info.remote.uninstaller.link
|
||||||
|
|
||||||
@IgnoredOnParcel
|
@IgnoredOnParcel
|
||||||
@ -83,8 +83,8 @@ sealed class DownloadSubject : Parcelable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
class Download : Magisk() {
|
private class Download : Magisk() {
|
||||||
override val configuration: Configuration get() = Configuration.Download
|
override val configuration get() = Configuration.Download
|
||||||
override val url: String get() = magisk.link
|
override val url: String get() = magisk.link
|
||||||
|
|
||||||
@IgnoredOnParcel
|
@IgnoredOnParcel
|
||||||
@ -97,7 +97,8 @@ sealed class DownloadSubject : Parcelable {
|
|||||||
operator fun invoke(configuration: Configuration) = when (configuration) {
|
operator fun invoke(configuration: Configuration) = when (configuration) {
|
||||||
Configuration.Download -> Download()
|
Configuration.Download -> Download()
|
||||||
Configuration.Uninstall -> Uninstall()
|
Configuration.Uninstall -> Uninstall()
|
||||||
else -> Flash(configuration)
|
Configuration.EnvFix, is Configuration.Flash -> DownloadInternal(configuration)
|
||||||
|
else -> throw IllegalArgumentException()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,14 +1,15 @@
|
|||||||
package com.topjohnwu.magisk.model.events.dialog
|
package com.topjohnwu.magisk.model.events.dialog
|
||||||
|
|
||||||
import android.content.DialogInterface
|
import android.content.BroadcastReceiver
|
||||||
import android.widget.Toast
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.IntentFilter
|
||||||
|
import androidx.localbroadcastmanager.content.LocalBroadcastManager
|
||||||
import com.topjohnwu.magisk.R
|
import com.topjohnwu.magisk.R
|
||||||
import com.topjohnwu.magisk.core.tasks.MagiskInstaller
|
import com.topjohnwu.magisk.core.download.DownloadService
|
||||||
import com.topjohnwu.magisk.core.utils.Utils
|
import com.topjohnwu.magisk.model.entity.internal.Configuration.EnvFix
|
||||||
import com.topjohnwu.magisk.extensions.reboot
|
import com.topjohnwu.magisk.model.entity.internal.DownloadSubject.Magisk
|
||||||
import com.topjohnwu.magisk.view.MagiskDialog
|
import com.topjohnwu.magisk.view.MagiskDialog
|
||||||
import com.topjohnwu.superuser.internal.UiThreadHandler
|
|
||||||
import org.koin.core.KoinComponent
|
|
||||||
|
|
||||||
class EnvFixDialog : DialogEvent() {
|
class EnvFixDialog : DialogEvent() {
|
||||||
|
|
||||||
@ -21,14 +22,16 @@ class EnvFixDialog : DialogEvent() {
|
|||||||
onClick {
|
onClick {
|
||||||
dialog.applyTitle(R.string.setup_title)
|
dialog.applyTitle(R.string.setup_title)
|
||||||
.applyMessage(R.string.setup_msg)
|
.applyMessage(R.string.setup_msg)
|
||||||
.applyButton(MagiskDialog.ButtonType.POSITIVE) {
|
.resetButtons()
|
||||||
title = ""
|
|
||||||
}
|
|
||||||
.applyButton(MagiskDialog.ButtonType.NEGATIVE) {
|
|
||||||
title = ""
|
|
||||||
}
|
|
||||||
.cancellable(false)
|
.cancellable(false)
|
||||||
fixEnv(it)
|
val lbm = LocalBroadcastManager.getInstance(dialog.context)
|
||||||
|
lbm.registerReceiver(object : BroadcastReceiver() {
|
||||||
|
override fun onReceive(context: Context, intent: Intent?) {
|
||||||
|
dialog.dismiss()
|
||||||
|
lbm.unregisterReceiver(this)
|
||||||
|
}
|
||||||
|
}, IntentFilter(DISMISS))
|
||||||
|
DownloadService(dialog.context) { subject = Magisk(EnvFix) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.applyButton(MagiskDialog.ButtonType.NEGATIVE) {
|
.applyButton(MagiskDialog.ButtonType.NEGATIVE) {
|
||||||
@ -36,20 +39,7 @@ class EnvFixDialog : DialogEvent() {
|
|||||||
}
|
}
|
||||||
.let { Unit }
|
.let { Unit }
|
||||||
|
|
||||||
private fun fixEnv(dialog: DialogInterface) {
|
companion object {
|
||||||
object : MagiskInstaller(), KoinComponent {
|
const val DISMISS = "com.topjohnwu.magisk.ENV_DONE"
|
||||||
override fun operations() = fixEnv()
|
|
||||||
|
|
||||||
override fun onResult(success: Boolean) {
|
|
||||||
dialog.dismiss()
|
|
||||||
Utils.toast(
|
|
||||||
if (success) R.string.reboot_delay_toast else R.string.setup_fail,
|
|
||||||
Toast.LENGTH_LONG
|
|
||||||
)
|
|
||||||
if (success)
|
|
||||||
UiThreadHandler.handler.postDelayed({ reboot() }, 5000)
|
|
||||||
}
|
|
||||||
}.exec()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,7 +0,0 @@
|
|||||||
package com.topjohnwu.magisk.model.flash
|
|
||||||
|
|
||||||
interface FlashResultListener {
|
|
||||||
|
|
||||||
fun onResult(isSuccess: Boolean)
|
|
||||||
|
|
||||||
}
|
|
@ -1,52 +0,0 @@
|
|||||||
package com.topjohnwu.magisk.model.flash
|
|
||||||
|
|
||||||
import android.net.Uri
|
|
||||||
import com.topjohnwu.magisk.core.tasks.MagiskInstaller
|
|
||||||
import com.topjohnwu.superuser.Shell
|
|
||||||
|
|
||||||
sealed class Patching(
|
|
||||||
file: Uri,
|
|
||||||
private val console: MutableList<String>,
|
|
||||||
logs: MutableList<String>,
|
|
||||||
private val resultListener: FlashResultListener
|
|
||||||
) : MagiskInstaller(file, console, logs) {
|
|
||||||
|
|
||||||
override fun onResult(success: Boolean) {
|
|
||||||
if (success) {
|
|
||||||
console.add("- All done!")
|
|
||||||
} else {
|
|
||||||
Shell.sh("rm -rf $installDir").submit()
|
|
||||||
console.add("! Installation failed")
|
|
||||||
}
|
|
||||||
resultListener.onResult(success)
|
|
||||||
}
|
|
||||||
|
|
||||||
class File(
|
|
||||||
file: Uri,
|
|
||||||
private val uri: Uri,
|
|
||||||
console: MutableList<String>,
|
|
||||||
logs: MutableList<String>,
|
|
||||||
resultListener: FlashResultListener
|
|
||||||
) : Patching(file, console, logs, resultListener) {
|
|
||||||
override fun operations() = doPatchFile(uri)
|
|
||||||
}
|
|
||||||
|
|
||||||
class SecondSlot(
|
|
||||||
file: Uri,
|
|
||||||
console: MutableList<String>,
|
|
||||||
logs: MutableList<String>,
|
|
||||||
resultListener: FlashResultListener
|
|
||||||
) : Patching(file, console, logs, resultListener) {
|
|
||||||
override fun operations() = secondSlot()
|
|
||||||
}
|
|
||||||
|
|
||||||
class Direct(
|
|
||||||
file: Uri,
|
|
||||||
console: MutableList<String>,
|
|
||||||
logs: MutableList<String>,
|
|
||||||
resultListener: FlashResultListener
|
|
||||||
) : Patching(file, console, logs, resultListener) {
|
|
||||||
override fun operations() = direct()
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
Loading…
x
Reference in New Issue
Block a user