diff --git a/app/src/main/java/com/topjohnwu/magisk/core/download/DownloadService.kt b/app/src/main/java/com/topjohnwu/magisk/core/download/DownloadService.kt index d2b76ceaf..d343045e9 100644 --- a/app/src/main/java/com/topjohnwu/magisk/core/download/DownloadService.kt +++ b/app/src/main/java/com/topjohnwu/magisk/core/download/DownloadService.kt @@ -9,15 +9,18 @@ import android.os.Build import android.webkit.MimeTypeMap import com.topjohnwu.magisk.R import com.topjohnwu.magisk.core.intent +import com.topjohnwu.magisk.core.tasks.EnvFixTask import com.topjohnwu.magisk.extensions.chooser import com.topjohnwu.magisk.extensions.exists import com.topjohnwu.magisk.extensions.provide +import com.topjohnwu.magisk.extensions.subscribeK import com.topjohnwu.magisk.legacy.flash.FlashActivity 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.DownloadSubject import com.topjohnwu.magisk.model.entity.internal.DownloadSubject.* import com.topjohnwu.magisk.utils.APKInstall +import io.reactivex.Completable import org.koin.core.get import java.io.File import kotlin.random.Random.Default.nextInt @@ -43,6 +46,7 @@ open class DownloadService : RemoteFileService() { id: Int ) = when (val conf = subject.configuration) { 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 Flash -> FlashActivity.flash(this, subject.file, conf is Secondary, id) else -> Unit @@ -60,10 +64,14 @@ open class DownloadService : RemoteFileService() { subject: Manager, id: Int ) { - remove(id) - when (subject.configuration) { - is APK.Upgrade -> APKInstall.install(this, subject.file) - is APK.Restore -> Unit + Completable.fromAction { + handleAPK(subject) + }.subscribeK { + remove(id) + when (subject.configuration) { + is APK.Upgrade -> APKInstall.install(this, subject.file) + is APK.Restore -> Unit + } } } diff --git a/app/src/main/java/com/topjohnwu/magisk/core/download/ManagerUpgrade.kt b/app/src/main/java/com/topjohnwu/magisk/core/download/ManagerUpgrade.kt index b91057638..f3c2ecff9 100644 --- a/app/src/main/java/com/topjohnwu/magisk/core/download/ManagerUpgrade.kt +++ b/app/src/main/java/com/topjohnwu/magisk/core/download/ManagerUpgrade.kt @@ -20,8 +20,8 @@ private fun RemoteFileService.patch(apk: File, id: Int) { if (packageName == BuildConfig.APPLICATION_ID) return - update(id) { notification -> - notification.setProgress(0, 0, true) + update(id) { + it.setProgress(0, 0, true) .setProgress(0, 0, true) .setContentTitle(getString(R.string.hide_manager_title)) .setContentText("") @@ -53,11 +53,11 @@ private fun RemoteFileService.upgrade(apk: File, id: Int) { } private fun RemoteFileService.restore(apk: File, id: Int) { - update(id) { notification -> - notification.setProgress(0, 0, true) - .setProgress(0, 0, true) - .setContentTitle(getString(R.string.restore_img_msg)) - .setContentText("") + update(id) { + it.setProgress(0, 0, true) + .setProgress(0, 0, true) + .setContentTitle(getString(R.string.restore_img_msg)) + .setContentText("") } Config.export() // 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() } -fun RemoteFileService.handleAPK(subject: DownloadSubject.Manager) - = when (subject.configuration) { +fun RemoteFileService.handleAPK(subject: DownloadSubject.Manager) = + when (subject.configuration) { is Upgrade -> upgrade(subject.file, subject.hashCode()) is Restore -> restore(subject.file, subject.hashCode()) } diff --git a/app/src/main/java/com/topjohnwu/magisk/core/download/NotificationService.kt b/app/src/main/java/com/topjohnwu/magisk/core/download/NotificationService.kt index b0b7a14d5..c6fb94432 100644 --- a/app/src/main/java/com/topjohnwu/magisk/core/download/NotificationService.kt +++ b/app/src/main/java/com/topjohnwu/magisk/core/download/NotificationService.kt @@ -7,16 +7,14 @@ import com.topjohnwu.magisk.core.base.BaseService import com.topjohnwu.magisk.core.view.Notifications import org.koin.core.KoinComponent import java.util.* +import kotlin.collections.HashMap import kotlin.random.Random.Default.nextInt abstract class NotificationService : BaseService(), KoinComponent { - abstract val defaultNotification: Notification.Builder - private val hasNotifications get() = notifications.isNotEmpty() - private val notifications = - Collections.synchronizedMap(mutableMapOf()) + private val notifications = Collections.synchronizedMap(HashMap()) override fun onTaskRemoved(rootIntent: Intent?) { super.onTaskRemoved(rootIntent) @@ -24,22 +22,23 @@ abstract class NotificationService : BaseService(), KoinComponent { notifications.clear() } + abstract fun createNotification(): Notification.Builder + // -- fun update( id: Int, body: (Notification.Builder) -> Unit = {} ) { - val notification = notifications.getOrPut(id) { defaultNotification } - - notify(id, notification.also(body).build()) - - if (notifications.size == 1) { + val wasEmpty = notifications.isEmpty() + val notification = notifications.getOrPut(id, ::createNotification).also(body) + if (wasEmpty) updateForeground() - } + else + notify(id, notification.build()) } - protected fun finishNotify( + protected fun lastNotify( id: Int, editBody: (Notification.Builder) -> Notification.Builder? = { null } ) : Int { @@ -57,6 +56,11 @@ abstract class NotificationService : BaseService(), KoinComponent { return newId } + protected fun remove(id: Int) = notifications.remove(id).also { + cancel(id) + updateForeground() + } + // --- private fun notify(id: Int, notification: Notification) { @@ -67,16 +71,13 @@ abstract class NotificationService : BaseService(), KoinComponent { Notifications.mgr.cancel(id) } - protected fun remove(id: Int) = notifications.remove(id).also { - cancel(id) - updateForeground() - } - private fun updateForeground() { - if (hasNotifications) - startForeground(notifications.keys.first(), notifications.values.first().build()) - else + if (hasNotifications) { + val first = notifications.entries.first() + startForeground(first.key, first.value.build()) + } else { stopForeground(true) + } } // -- diff --git a/app/src/main/java/com/topjohnwu/magisk/core/download/RemoteFileService.kt b/app/src/main/java/com/topjohnwu/magisk/core/download/RemoteFileService.kt index 38a9dd411..18a1a475b 100644 --- a/app/src/main/java/com/topjohnwu/magisk/core/download/RemoteFileService.kt +++ b/app/src/main/java/com/topjohnwu/magisk/core/download/RemoteFileService.kt @@ -14,7 +14,8 @@ import com.topjohnwu.magisk.extensions.get import com.topjohnwu.magisk.extensions.subscribeK 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.Magisk +import com.topjohnwu.magisk.model.entity.internal.DownloadSubject.Module import com.topjohnwu.superuser.ShellUtils import io.reactivex.Completable import okhttp3.ResponseBody @@ -27,19 +28,17 @@ abstract class RemoteFileService : NotificationService() { val service: GithubRawServices by inject() - override val defaultNotification - get() = Notifications.progress(this, "") - override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { intent?.getParcelableExtra(ARG_URL)?.let { start(it) } return START_REDELIVER_INTENT } + override fun createNotification() = Notifications.progress(this, "") + // --- private fun start(subject: DownloadSubject) = checkExisting(subject) .onErrorResumeNext { download(subject) } - .doOnSubscribe { update(subject.hashCode()) { it.setContentTitle(subject.title) } } .subscribeK(onError = { Timber.e(it) failNotify(subject) @@ -52,16 +51,14 @@ abstract class RemoteFileService : NotificationService() { private fun checkExisting(subject: DownloadSubject) = Completable.fromAction { check(subject is Magisk) { "Download cache is disabled" } - - subject.file.also { - check(it.exists() && ShellUtils.checkSum("MD5", it, subject.magisk.md5)) { - "The given file does not match checksum" - } + check(subject.file.exists() && + ShellUtils.checkSum("MD5", subject.file, subject.magisk.md5)) { + "The given file does not match checksum" } } private fun download(subject: DownloadSubject) = service.fetchFile(subject.url) - .map { it.toStream(subject.hashCode(), subject) } + .map { it.toProgressStream(subject) } .flatMapCompletable { stream -> when (subject) { is Module -> service.fetchInstaller() @@ -69,14 +66,14 @@ abstract class RemoteFileService : NotificationService() { .ignoreElement() 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 max = maxRaw / 1_000_000f + val id = subject.hashCode() + + update(id) { it.setContentTitle(subject.title) } return ProgressInputStream(byteStream()) { 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) it.setContentText(getString(R.string.download_file_error)) .setSmallIcon(android.R.drawable.stat_notify_error) .setOngoing(false) } - private fun finishNotify(subject: DownloadSubject) = finishNotify(subject.hashCode()) { + private fun finishNotify(subject: DownloadSubject) = lastNotify(subject.hashCode()) { send(1f, subject) it.addActions(subject) .setContentText(getString(R.string.download_complete)) diff --git a/app/src/main/java/com/topjohnwu/magisk/core/tasks/FlashResultListener.kt b/app/src/main/java/com/topjohnwu/magisk/core/tasks/FlashResultListener.kt new file mode 100644 index 000000000..df7f9fca9 --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/core/tasks/FlashResultListener.kt @@ -0,0 +1,10 @@ +package com.topjohnwu.magisk.core.tasks + +import androidx.annotation.MainThread + +interface FlashResultListener { + + @MainThread + fun onResult(success: Boolean) + +} diff --git a/app/src/main/java/com/topjohnwu/magisk/core/tasks/FlashZip.kt b/app/src/main/java/com/topjohnwu/magisk/core/tasks/FlashZip.kt index fefbf60ba..c46fef2d4 100644 --- a/app/src/main/java/com/topjohnwu/magisk/core/tasks/FlashZip.kt +++ b/app/src/main/java/com/topjohnwu/magisk/core/tasks/FlashZip.kt @@ -18,7 +18,7 @@ abstract class FlashZip( private val mUri: Uri, private val console: MutableList, private val logs: MutableList -) { +) : FlashResultListener { private val context: Context by inject() private val installFolder = File(context.cacheDir, "flash").apply { @@ -94,6 +94,4 @@ abstract class FlashZip( .subscribeK(onError = { onResult(false) }) { onResult(it) } .let { Unit } // ignores result disposable - - protected abstract fun onResult(success: Boolean) } diff --git a/app/src/main/java/com/topjohnwu/magisk/model/flash/Flashing.kt b/app/src/main/java/com/topjohnwu/magisk/core/tasks/Flashing.kt similarity index 80% rename from app/src/main/java/com/topjohnwu/magisk/model/flash/Flashing.kt rename to app/src/main/java/com/topjohnwu/magisk/core/tasks/Flashing.kt index 21e4672d6..99a9fe1e1 100644 --- a/app/src/main/java/com/topjohnwu/magisk/model/flash/Flashing.kt +++ b/app/src/main/java/com/topjohnwu/magisk/core/tasks/Flashing.kt @@ -1,9 +1,8 @@ -package com.topjohnwu.magisk.model.flash +package com.topjohnwu.magisk.core.tasks import android.content.Context import android.net.Uri import androidx.core.os.postDelayed -import com.topjohnwu.magisk.core.tasks.FlashZip import com.topjohnwu.magisk.extensions.inject import com.topjohnwu.superuser.Shell import com.topjohnwu.superuser.internal.UiThreadHandler @@ -28,16 +27,7 @@ sealed class Flashing( console: MutableList, log: MutableList, resultListener: FlashResultListener - ) : Flashing(uri, console, log, resultListener) { - - override fun onResult(success: Boolean) { - if (success) { - //Utils.loadModules() - } - super.onResult(success) - } - - } + ) : Flashing(uri, console, log, resultListener) class Uninstall( uri: Uri, diff --git a/app/src/main/java/com/topjohnwu/magisk/core/tasks/MagiskInstallImpl.kt b/app/src/main/java/com/topjohnwu/magisk/core/tasks/MagiskInstallImpl.kt new file mode 100644 index 000000000..7a4b1a02f --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/core/tasks/MagiskInstallImpl.kt @@ -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 + private val logs: MutableList + 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, err: MutableList) { + console = out + logs = err + zipUri = zip + installDir = File(get(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.sh() = Shell.sh(*this).to(console, logs).exec() + private fun String.fsh() = ShellUtils.fastCmd(this) + private fun Array.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) } + } + +} diff --git a/app/src/main/java/com/topjohnwu/magisk/core/tasks/MagiskInstaller.kt b/app/src/main/java/com/topjohnwu/magisk/core/tasks/MagiskInstaller.kt index 395e0cb02..c695d465b 100644 --- a/app/src/main/java/com/topjohnwu/magisk/core/tasks/MagiskInstaller.kt +++ b/app/src/main/java/com/topjohnwu/magisk/core/tasks/MagiskInstaller.kt @@ -1,382 +1,77 @@ package com.topjohnwu.magisk.core.tasks -import android.content.Context +import android.content.Intent import android.net.Uri -import android.os.Build -import android.text.TextUtils -import androidx.annotation.MainThread -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.magisk.net.Networking -import com.topjohnwu.signing.SignBoot +import android.widget.Toast +import androidx.core.os.postDelayed +import androidx.localbroadcastmanager.content.LocalBroadcastManager +import com.topjohnwu.magisk.R +import com.topjohnwu.magisk.core.utils.Utils +import com.topjohnwu.magisk.extensions.reboot +import com.topjohnwu.magisk.model.events.dialog.EnvFixDialog 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 com.topjohnwu.superuser.internal.UiThreadHandler 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, + logs: MutableList, + private val resultListener: FlashResultListener +) : MagiskInstallImpl(file, console, logs) { - protected lateinit var installDir: File - private lateinit var srcBoot: String - private lateinit var destFile: File - private lateinit var zipUri: Uri - - private val console: MutableList - private val logs: MutableList - 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, err: MutableList) { - console = out - logs = err - zipUri = zip - installDir = File(get(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" + override fun onResult(success: Boolean) { + if (success) { + console.add("- All done!") } else { - if (TextUtils.equals(Build.CPU_ABI, "x86")) "x86" else "arm" + Shell.sh("rm -rf $installDir").submit() + console.add("! Installation failed") } - - 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 + resultListener.onResult(success) } - private fun newEntry(name: String, size: Long): TarEntry { - console.add("-- Writing: $name") - return TarEntry(TarHeader.createHeader(name, size, 0, false, 420 /* 0644 */)) + class Patch( + file: Uri, + private val uri: Uri, + console: MutableList, + logs: MutableList, + resultListener: FlashResultListener + ) : MagiskInstaller(file, console, logs, resultListener) { + override fun operations() = doPatchFile(uri) } - @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 - } - } + class SecondSlot( + file: Uri, + console: MutableList, + logs: MutableList, + resultListener: FlashResultListener + ) : MagiskInstaller(file, console, logs, resultListener) { + override fun operations() = secondSlot() } - 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.sh() = Shell.sh(*this).to(console, logs).exec() - private fun String.fsh() = ShellUtils.fastCmd(this) - private fun Array.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() - 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 Direct( + file: Uri, + console: MutableList, + logs: MutableList, + resultListener: FlashResultListener + ) : MagiskInstaller(file, console, logs, resultListener) { + override fun operations() = direct() } } + +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() } + } +} diff --git a/app/src/main/java/com/topjohnwu/magisk/legacy/flash/FlashViewModel.kt b/app/src/main/java/com/topjohnwu/magisk/legacy/flash/FlashViewModel.kt index 662284916..67bff9ed4 100644 --- a/app/src/main/java/com/topjohnwu/magisk/legacy/flash/FlashViewModel.kt +++ b/app/src/main/java/com/topjohnwu/magisk/legacy/flash/FlashViewModel.kt @@ -12,13 +12,13 @@ import androidx.databinding.ObservableArrayList import com.topjohnwu.magisk.R import com.topjohnwu.magisk.core.Config 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.model.binding.BindingAdapter import com.topjohnwu.magisk.model.entity.recycler.ConsoleItem 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.diffListOf import com.topjohnwu.magisk.ui.base.itemBindingOf @@ -60,26 +60,26 @@ class FlashViewModel( Const.Value.UNINSTALL -> Flashing .Uninstall(installer, outItems, logItems, this) .exec() - Const.Value.FLASH_MAGISK -> Patching + Const.Value.FLASH_MAGISK -> MagiskInstaller .Direct(installer, outItems, logItems, this) .exec() - Const.Value.FLASH_INACTIVE_SLOT -> Patching + Const.Value.FLASH_INACTIVE_SLOT -> MagiskInstaller .SecondSlot(installer, outItems, logItems, this) .exec() - Const.Value.PATCH_FILE -> Patching - .File(installer, uri ?: return, outItems, logItems, this) + Const.Value.PATCH_FILE -> MagiskInstaller + .Patch(installer, uri ?: return, outItems, logItems, this) .exec() } } - override fun onResult(isSuccess: Boolean) { - state = if (isSuccess) State.LOADED else State.LOADING_FAILED + override fun onResult(success: Boolean) { + state = if (success) State.LOADED else State.LOADING_FAILED behaviorText.value = when { - isSuccess -> resources.getString(R.string.done) + success -> resources.getString(R.string.done) else -> resources.getString(R.string.failure) } - if (isSuccess) { + if (success) { Handler().postDelayed(500) { showRestartTitle.value = true } diff --git a/app/src/main/java/com/topjohnwu/magisk/model/entity/internal/Configuration.kt b/app/src/main/java/com/topjohnwu/magisk/model/entity/internal/Configuration.kt index da6027944..1ea2c0df6 100644 --- a/app/src/main/java/com/topjohnwu/magisk/model/entity/internal/Configuration.kt +++ b/app/src/main/java/com/topjohnwu/magisk/model/entity/internal/Configuration.kt @@ -31,7 +31,10 @@ sealed class Configuration : Parcelable { @Parcelize object Uninstall : Configuration() + @Parcelize + object EnvFix : Configuration() + @Parcelize data class Patch(val fileUri: Uri) : Configuration() -} \ No newline at end of file +} diff --git a/app/src/main/java/com/topjohnwu/magisk/model/entity/internal/DownloadSubject.kt b/app/src/main/java/com/topjohnwu/magisk/model/entity/internal/DownloadSubject.kt index 01eb7d274..c7b3a2484 100644 --- a/app/src/main/java/com/topjohnwu/magisk/model/entity/internal/DownloadSubject.kt +++ b/app/src/main/java/com/topjohnwu/magisk/model/entity/internal/DownloadSubject.kt @@ -20,7 +20,7 @@ sealed class DownloadSubject : Parcelable { open val title: String get() = file.name @Parcelize - data class Module( + class Module( val module: Repo, val configuration: Configuration ) : DownloadSubject() { @@ -33,7 +33,7 @@ sealed class DownloadSubject : Parcelable { } @Parcelize - data class Manager( + class Manager( val configuration: Configuration.APK ) : DownloadSubject() { @@ -53,13 +53,13 @@ sealed class DownloadSubject : Parcelable { } - sealed class Magisk : DownloadSubject() { + abstract class Magisk : DownloadSubject() { abstract val configuration: Configuration val magisk: MagiskJson = Info.remote.magisk @Parcelize - data class Flash( + private class DownloadInternal( override val configuration: Configuration ) : Magisk() { override val url: String get() = magisk.link @@ -72,8 +72,8 @@ sealed class DownloadSubject : Parcelable { } @Parcelize - class Uninstall : Magisk() { - override val configuration: Configuration get() = Configuration.Uninstall + private class Uninstall : Magisk() { + override val configuration get() = Configuration.Uninstall override val url: String get() = Info.remote.uninstaller.link @IgnoredOnParcel @@ -83,8 +83,8 @@ sealed class DownloadSubject : Parcelable { } @Parcelize - class Download : Magisk() { - override val configuration: Configuration get() = Configuration.Download + private class Download : Magisk() { + override val configuration get() = Configuration.Download override val url: String get() = magisk.link @IgnoredOnParcel @@ -97,7 +97,8 @@ sealed class DownloadSubject : Parcelable { operator fun invoke(configuration: Configuration) = when (configuration) { Configuration.Download -> Download() Configuration.Uninstall -> Uninstall() - else -> Flash(configuration) + Configuration.EnvFix, is Configuration.Flash -> DownloadInternal(configuration) + else -> throw IllegalArgumentException() } } diff --git a/app/src/main/java/com/topjohnwu/magisk/model/events/dialog/EnvFixDialog.kt b/app/src/main/java/com/topjohnwu/magisk/model/events/dialog/EnvFixDialog.kt index 9d8f4a9ba..b9a134d33 100644 --- a/app/src/main/java/com/topjohnwu/magisk/model/events/dialog/EnvFixDialog.kt +++ b/app/src/main/java/com/topjohnwu/magisk/model/events/dialog/EnvFixDialog.kt @@ -1,14 +1,15 @@ package com.topjohnwu.magisk.model.events.dialog -import android.content.DialogInterface -import android.widget.Toast +import android.content.BroadcastReceiver +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.core.tasks.MagiskInstaller -import com.topjohnwu.magisk.core.utils.Utils -import com.topjohnwu.magisk.extensions.reboot +import com.topjohnwu.magisk.core.download.DownloadService +import com.topjohnwu.magisk.model.entity.internal.Configuration.EnvFix +import com.topjohnwu.magisk.model.entity.internal.DownloadSubject.Magisk import com.topjohnwu.magisk.view.MagiskDialog -import com.topjohnwu.superuser.internal.UiThreadHandler -import org.koin.core.KoinComponent class EnvFixDialog : DialogEvent() { @@ -21,14 +22,16 @@ class EnvFixDialog : DialogEvent() { onClick { dialog.applyTitle(R.string.setup_title) .applyMessage(R.string.setup_msg) - .applyButton(MagiskDialog.ButtonType.POSITIVE) { - title = "" - } - .applyButton(MagiskDialog.ButtonType.NEGATIVE) { - title = "" - } + .resetButtons() .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) { @@ -36,20 +39,7 @@ class EnvFixDialog : DialogEvent() { } .let { Unit } - private fun fixEnv(dialog: DialogInterface) { - object : MagiskInstaller(), KoinComponent { - 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() + companion object { + const val DISMISS = "com.topjohnwu.magisk.ENV_DONE" } - } diff --git a/app/src/main/java/com/topjohnwu/magisk/model/flash/FlashResultListener.kt b/app/src/main/java/com/topjohnwu/magisk/model/flash/FlashResultListener.kt deleted file mode 100644 index f89d87f12..000000000 --- a/app/src/main/java/com/topjohnwu/magisk/model/flash/FlashResultListener.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.topjohnwu.magisk.model.flash - -interface FlashResultListener { - - fun onResult(isSuccess: Boolean) - -} \ No newline at end of file diff --git a/app/src/main/java/com/topjohnwu/magisk/model/flash/Patching.kt b/app/src/main/java/com/topjohnwu/magisk/model/flash/Patching.kt deleted file mode 100644 index b3693706b..000000000 --- a/app/src/main/java/com/topjohnwu/magisk/model/flash/Patching.kt +++ /dev/null @@ -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, - logs: MutableList, - 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, - logs: MutableList, - resultListener: FlashResultListener - ) : Patching(file, console, logs, resultListener) { - override fun operations() = doPatchFile(uri) - } - - class SecondSlot( - file: Uri, - console: MutableList, - logs: MutableList, - resultListener: FlashResultListener - ) : Patching(file, console, logs, resultListener) { - override fun operations() = secondSlot() - } - - class Direct( - file: Uri, - console: MutableList, - logs: MutableList, - resultListener: FlashResultListener - ) : Patching(file, console, logs, resultListener) { - override fun operations() = direct() - } - -}