Merge Magisk install zip into Magisk Manager

Distribute Magisk directly with Magisk Manager APK. The APK will
contain all required binaries and scripts for installation and
uninstallation. App versions will now align with Magisk releases.

Extra effort is spent to make the APK itself also a flashable zip that
can be used in custom recoveries, so those still prefer to install
Magisk with recoveries will not be affected with this change.

As a bonus, this makes the whole installation and uninstallation
process 100% offline. The existing Magisk Manager was not really
functional without an Internet connection, as the installation process
was highly tied to zips hosted on the server.

An additional bonus: since all binaries are now shipped as "native
libraries" of the APK, we can finally bump the target SDK version
higher than 28. The target SDK version was stuck at 28 for a long time
because newer SELinux restricts running executables from internal
storage. More details can be found here: https://github.com/termux/termux-app/issues/1072
The target SDK bump will be addressed in a future commit.

Co-authored with @vvb2060
This commit is contained in:
topjohnwu 2021-01-22 02:28:53 -08:00
parent 61d52991f1
commit ec8fffe61c
37 changed files with 784 additions and 1025 deletions

5
app/.gitignore vendored
View File

@ -6,6 +6,7 @@
app/release
*.hprof
.externalNativeBuild/
public.certificate.x509.pem
private.key.pk8
*.apk
src/main/assets
src/main/jniLibs
src/main/resources

View File

@ -1,3 +1,4 @@
import org.apache.tools.ant.filters.FixCrLfFilter
import java.io.PrintStream
plugins {
@ -22,8 +23,8 @@ android {
applicationId = "com.topjohnwu.magisk"
vectorDrawables.useSupportLibrary = true
multiDexEnabled = true
versionName = Config.appVersion
versionCode = Config.appVersionCode
versionName = Config.version
versionCode = Config.versionCode
javaCompileOptions.annotationProcessorOptions.arguments(
mapOf("room.incremental" to "true")
@ -51,13 +52,14 @@ android {
}
packagingOptions {
exclude("/META-INF/**")
exclude("/META-INF/*")
exclude("/org/bouncycastle/**")
exclude("/kotlin/**")
exclude("/kotlinx/**")
exclude("/okhttp3/**")
exclude("/*.txt")
exclude("/*.bin")
doNotStrip("**/*.so")
}
kotlinOptions {
@ -65,10 +67,82 @@ android {
}
}
tasks["preBuild"]?.dependsOn(tasks.register("copyUtils", Copy::class) {
from(rootProject.file("scripts/util_functions.sh"))
into("src/main/res/raw")
})
val syncLibs by tasks.registering(Sync::class) {
into("src/main/jniLibs")
into("armeabi-v7a") {
from(rootProject.file("native/out/armeabi-v7a")) {
include("busybox", "magiskboot", "magiskinit", "magisk")
rename { if (it == "magisk") "libmagisk32.so" else "lib$it.so" }
}
from(rootProject.file("native/out/arm64-v8a")) {
include("magisk")
rename { if (it == "magisk") "libmagisk64.so" else "lib$it.so" }
}
}
into("x86") {
from(rootProject.file("native/out/x86")) {
include("busybox", "magiskboot", "magiskinit", "magisk")
rename { if (it == "magisk") "libmagisk32.so" else "lib$it.so" }
}
from(rootProject.file("native/out/x86_64")) {
include("magisk")
rename { if (it == "magisk") "libmagisk64.so" else "lib$it.so" }
}
}
doFirst {
if (inputs.sourceFiles.files.size != 10)
throw StopExecutionException("Build binary files first")
}
}
val createStubLibs by tasks.registering {
dependsOn(syncLibs)
doLast {
val arm64 = project.file("src/main/jniLibs/arm64-v8a/libstub.so")
arm64.parentFile.mkdirs()
arm64.createNewFile()
val x64 = project.file("src/main/jniLibs/x86_64/libstub.so")
x64.parentFile.mkdirs()
x64.createNewFile()
}
}
val syncAssets by tasks.registering(Sync::class) {
dependsOn(createStubLibs)
inputs.property("version", Config.version)
inputs.property("versionCode", Config.versionCode)
into("src/main/assets")
from(rootProject.file("scripts")) {
include("util_functions.sh", "boot_patch.sh", "magisk_uninstaller.sh", "addon.d.sh")
}
into("chromeos") {
from(rootProject.file("tools/futility"))
from(rootProject.file("tools/keys")) {
include("kernel_data_key.vbprivk", "kernel.keyblock")
}
}
filesMatching("**/util_functions.sh") {
filter {
it.replace("#MAGISK_VERSION_STUB",
"MAGISK_VER='${Config.version}'\n" +
"MAGISK_VER_CODE=${Config.versionCode}")
}
filter<FixCrLfFilter>("eol" to FixCrLfFilter.CrLf.newInstance("lf"))
}
}
val syncResources by tasks.registering(Sync::class) {
dependsOn(syncAssets)
into("src/main/resources/META-INF/com/google/android")
from(rootProject.file("scripts/update_binary.sh")) {
rename { "update-binary" }
}
from(rootProject.file("scripts/flash_script.sh")) {
rename { "updater-script" }
}
}
tasks["preBuild"]?.dependsOn(syncResources)
android.applicationVariants.all {
val keysDir = rootProject.file("tools/keys")
@ -178,6 +252,5 @@ dependencies {
implementation("androidx.transition:transition:1.3.1")
implementation("androidx.multidex:multidex:2.0.1")
implementation("androidx.core:core-ktx:1.3.2")
implementation("androidx.localbroadcastmanager:localbroadcastmanager:1.0.0")
implementation("com.google.android.material:material:1.2.1")
}

View File

@ -13,6 +13,8 @@
android:icon="@drawable/ic_launcher"
android:name="a.e"
android:allowBackup="false"
android:multiArch="true"
android:extractNativeLibs="true"
tools:ignore="UnusedAttribute,GoogleAppIndexingWarning">
<!-- Splash -->

View File

@ -19,6 +19,7 @@ import com.topjohnwu.superuser.Shell
import org.koin.android.ext.koin.androidContext
import org.koin.core.context.startKoin
import timber.log.Timber
import java.io.File
import kotlin.system.exitProcess
open class App() : Application() {
@ -61,6 +62,12 @@ open class App() : Application() {
val wrapped = impl.wrap()
super.attachBaseContext(wrapped)
val info = base.applicationInfo
val libDir = runCatching {
info.javaClass.getDeclaredField("secondaryNativeLibraryDir").get(info) as String?
}.getOrNull() ?: info.nativeLibraryDir
Const.NATIVE_LIB_DIR = File(libDir)
// Normal startup
startKoin {
androidContext(wrapped)

View File

@ -1,13 +1,30 @@
package com.topjohnwu.magisk.core
import android.os.Build
import android.os.Process
import java.io.File
@Suppress("DEPRECATION")
object Const {
val CPU_ABI: String
val CPU_ABI_32: String
init {
if (Build.VERSION.SDK_INT >= 21) {
CPU_ABI = Build.SUPPORTED_ABIS[0]
CPU_ABI_32 = Build.SUPPORTED_32_BIT_ABIS[0]
} else {
CPU_ABI = Build.CPU_ABI
CPU_ABI_32 = CPU_ABI
}
}
// Paths
lateinit var MAGISKTMP: String
lateinit var NATIVE_LIB_DIR: File
val MAGISK_PATH get() = "$MAGISKTMP/modules"
const val TMP_FOLDER_PATH = "/dev/tmp"
const val TMPDIR = "/dev/tmp"
const val MAGISK_LOG = "/cache/magisk.log"
// Versions
@ -19,11 +36,9 @@ object Const {
val USER_ID = Process.myUid() / 100000
object Version {
const val MIN_VERSION = "v19.0"
const val MIN_VERCODE = 19000
const val MIN_VERSION = "v20.4"
const val MIN_VERCODE = 20400
fun atLeast_20_2() = Info.env.magiskVersionCode >= 20200 || isCanary()
fun atLeast_20_4() = Info.env.magiskVersionCode >= 20400 || isCanary()
fun atLeast_21_0() = Info.env.magiskVersionCode >= 21000 || isCanary()
fun atLeast_21_2() = Info.env.magiskVersionCode >= 21200 || isCanary()
fun isCanary() = Info.env.magiskVersionCode % 100 != 0

View File

@ -1,31 +0,0 @@
package com.topjohnwu.magisk.core.download
import android.net.Uri
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
sealed class Action : Parcelable {
sealed class Flash : Action() {
@Parcelize
object Primary : Flash()
@Parcelize
object Secondary : Flash()
}
@Parcelize
object Download : Action()
@Parcelize
object Uninstall : Action()
@Parcelize
object EnvFix : Action()
@Parcelize
data class Patch(val fileUri: Uri) : Action()
}

View File

@ -8,7 +8,6 @@ import androidx.lifecycle.MutableLiveData
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.core.ForegroundTracker
import com.topjohnwu.magisk.core.base.BaseService
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.checkSum
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.outputStream
import com.topjohnwu.magisk.core.utils.ProgressInputStream
import com.topjohnwu.magisk.data.repository.NetworkService
@ -69,17 +68,14 @@ abstract class BaseDownloader : BaseService(), KoinComponent {
// -- Download logic
private suspend fun Subject.startDownload() {
val skip = this is Subject.Magisk && file.checkSum("MD5", magisk.md5)
if (!skip) {
val stream = service.fetchFile(url).toProgressStream(this)
when (this) {
is Subject.Module -> // Download and process on-the-fly
stream.toModule(file, service.fetchInstaller().byteStream())
else -> {
withStreams(stream, file.outputStream()) { it, out -> it.copyTo(out) }
if (this is Subject.Manager)
handleAPK(this)
}
val stream = service.fetchFile(url).toProgressStream(this)
when (this) {
is Subject.Module -> // Download and process on-the-fly
stream.toModule(file, service.fetchInstaller().byteStream())
else -> {
withStreams(stream, file.outputStream()) { it, out -> it.copyTo(out) }
if (this is Subject.Manager)
handleAPK(this)
}
}
val newId = notifyFinish(this)

View File

@ -7,11 +7,10 @@ import android.content.Context
import android.content.Intent
import android.os.Build
import androidx.core.net.toFile
import com.topjohnwu.magisk.core.download.Action.*
import com.topjohnwu.magisk.core.download.Action.Flash.Secondary
import com.topjohnwu.magisk.core.download.Subject.*
import com.topjohnwu.magisk.core.download.Action.Flash
import com.topjohnwu.magisk.core.download.Subject.Manager
import com.topjohnwu.magisk.core.download.Subject.Module
import com.topjohnwu.magisk.core.intent
import com.topjohnwu.magisk.core.tasks.EnvFixTask
import com.topjohnwu.magisk.ui.flash.FlashFragment
import com.topjohnwu.magisk.utils.APKInstall
import kotlin.random.Random.Default.nextInt
@ -22,25 +21,12 @@ open class DownloadService : BaseDownloader() {
private val context get() = this
override suspend fun onFinish(subject: Subject, id: Int) = when (subject) {
is Magisk -> subject.onFinish(id)
is Module -> subject.onFinish(id)
is Manager -> subject.onFinish(id)
}
private suspend fun Magisk.onFinish(id: Int) = when (val action = action) {
Uninstall -> FlashFragment.uninstall(file, id)
EnvFix -> {
remove(id)
EnvFixTask(file).exec()
Unit
}
is Patch -> FlashFragment.patch(file, action.fileUri, id)
is Flash -> FlashFragment.flash(file, action is Secondary, id)
else -> Unit
}
private fun Module.onFinish(id: Int) = when (action) {
is Flash -> FlashFragment.install(file, id)
Flash -> FlashFragment.install(file, id)
else -> Unit
}
@ -53,22 +39,13 @@ open class DownloadService : BaseDownloader() {
override fun Notification.Builder.setIntent(subject: Subject)
= when (subject) {
is Magisk -> setIntent(subject)
is Module -> setIntent(subject)
is Manager -> setIntent(subject)
}
private fun Notification.Builder.setIntent(subject: Magisk)
= when (val action = subject.action) {
Uninstall -> setContentIntent(FlashFragment.uninstallIntent(context, subject.file))
is Flash -> setContentIntent(FlashFragment.flashIntent(context, subject.file, action is Secondary))
is Patch -> setContentIntent(FlashFragment.patchIntent(context, subject.file, action.fileUri))
else -> setContentIntent(Intent())
}
private fun Notification.Builder.setIntent(subject: Module)
= when (subject.action) {
is Flash -> setContentIntent(FlashFragment.installIntent(context, subject.file))
Flash -> setContentIntent(FlashFragment.installIntent(context, subject.file))
else -> setContentIntent(Intent())
}

View File

@ -5,7 +5,6 @@ import android.net.Uri
import android.os.Parcelable
import androidx.core.net.toUri
import com.topjohnwu.magisk.core.Info
import com.topjohnwu.magisk.core.model.MagiskJson
import com.topjohnwu.magisk.core.model.ManagerJson
import com.topjohnwu.magisk.core.model.StubJson
import com.topjohnwu.magisk.core.model.module.OnlineModule
@ -53,57 +52,12 @@ sealed class Subject : Parcelable {
}
}
abstract class Magisk : Subject() {
val magisk: MagiskJson = Info.remote.magisk
@Parcelize
private class Internal(
override val action: Action
) : Magisk() {
override val url: String get() = magisk.link
override val title: String get() = "Magisk-${magisk.version}(${magisk.versionCode})"
@IgnoredOnParcel
override val file by lazy {
cachedFile("magisk.zip")
}
}
@Parcelize
private class Uninstall : Magisk() {
override val action get() = Action.Uninstall
override val url: String get() = Info.remote.uninstaller.link
override val title: String get() = "uninstall.zip"
@IgnoredOnParcel
override val file by lazy {
cachedFile(title)
}
}
@Parcelize
private class Download : Magisk() {
override val action get() = Action.Download
override val url: String get() = magisk.link
override val title: String get() = "Magisk-${magisk.version}(${magisk.versionCode}).zip"
@IgnoredOnParcel
override val file by lazy {
MediaStoreUtils.getFile(title).uri
}
}
companion object {
operator fun invoke(config: Action) = when (config) {
Action.Download -> Download()
Action.Uninstall -> Uninstall()
Action.EnvFix, is Action.Flash, is Action.Patch -> Internal(config)
}
}
}
}
sealed class Action : Parcelable {
@Parcelize
object Flash : Action()
@Parcelize
object Download : Action()
}

View File

@ -2,14 +2,13 @@ package com.topjohnwu.magisk.core.tasks
import android.content.Context
import android.net.Uri
import androidx.core.os.postDelayed
import androidx.core.net.toFile
import com.topjohnwu.magisk.core.Const
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.displayName
import com.topjohnwu.magisk.core.utils.unzip
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.inputStream
import com.topjohnwu.magisk.core.utils.unzip
import com.topjohnwu.magisk.ktx.writeTo
import com.topjohnwu.superuser.Shell
import com.topjohnwu.superuser.internal.UiThreadHandler
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.koin.core.KoinComponent
@ -25,61 +24,50 @@ open class FlashZip(
private val logs: MutableList<String>
): KoinComponent {
val context: Context by inject()
private val installFolder = File(context.cacheDir, "flash").apply {
if (!exists()) mkdirs()
}
private val tmpFile: File = File(installFolder, "install.zip")
@Throws(IOException::class)
private fun unzipAndCheck(): Boolean {
val parentFile = tmpFile.parentFile ?: return false
tmpFile.unzip(parentFile, "META-INF/com/google/android", true)
val updaterScript = File(parentFile, "updater-script")
return Shell
.su("grep -q '#MAGISK' $updaterScript")
.exec()
.isSuccess
}
private val context: Context by inject()
private val installDir = File(context.cacheDir, "flash")
private lateinit var zipFile: File
@Throws(IOException::class)
private fun flash(): Boolean {
console.add("- Copying zip to temp directory")
installDir.deleteRecursively()
installDir.mkdirs()
runCatching {
mUri.inputStream().writeTo(tmpFile)
}.getOrElse {
when (it) {
is FileNotFoundException -> console.add("! Invalid Uri")
is IOException -> console.add("! Cannot copy to cache")
zipFile = if (mUri.scheme == "file") {
mUri.toFile()
} else {
File(installDir, "install.zip").also {
console.add("- Copying zip to temp directory")
try {
mUri.inputStream().writeTo(it)
} catch (e: IOException) {
when (e) {
is FileNotFoundException -> console.add("! Invalid Uri")
else -> console.add("! Cannot copy to cache")
}
throw e
}
}
throw it
}
val isMagiskModule = runCatching {
unzipAndCheck()
val isValid = runCatching {
zipFile.unzip(installDir, "META-INF/com/google/android", true)
val script = File(installDir, "updater-script")
script.readText().contains("#MAGISK")
}.getOrElse {
console.add("! Unzip error")
throw it
}
if (!isMagiskModule) {
console.add("! This zip is not a Magisk Module!")
if (!isValid) {
console.add("! This zip is not a Magisk module!")
return false
}
console.add("- Installing ${mUri.displayName}")
val parentFile = tmpFile.parent ?: return false
return Shell
.su(
"cd $parentFile",
"BOOTMODE=true sh update-binary dummy 1 $tmpFile"
)
.to(console, logs)
.exec().isSuccess
return Shell.su("sh $installDir/update-binary dummy 1 $zipFile")
.to(console, logs).exec().isSuccess
}
open suspend fun exec() = withContext(Dispatchers.IO) {
@ -94,25 +82,7 @@ open class FlashZip(
Timber.e(e)
false
} finally {
Shell.su("cd /", "rm -rf ${tmpFile.parent} ${Const.TMP_FOLDER_PATH}").submit()
Shell.su("cd /", "rm -rf $installDir ${Const.TMPDIR}").submit()
}
}
class Uninstall(
uri: Uri,
console: MutableList<String>,
log: MutableList<String>
) : FlashZip(uri, console, log) {
override suspend fun exec(): Boolean {
val success = super.exec()
if (success) {
UiThreadHandler.handler.postDelayed(3000) {
Shell.su("pm uninstall " + context.packageName).exec()
}
}
return success
}
}
}

View File

@ -91,8 +91,7 @@ object HideAPK {
}
private suspend fun patchAndHide(context: Context, label: String): Boolean {
val dlStub = !isRunningAsStub && SDK_INT >= 28 && Const.Version.atLeast_20_2()
val src = if (dlStub) {
val src = if (!isRunningAsStub && SDK_INT >= 28) {
val stub = File(context.cacheDir, "stub.apk")
try {
svc.fetchFile(Info.remote.stub.link).byteStream().use {

View File

@ -1,25 +1,25 @@
package com.topjohnwu.magisk.core.tasks
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.widget.Toast
import androidx.annotation.WorkerThread
import androidx.core.os.postDelayed
import androidx.localbroadcastmanager.content.LocalBroadcastManager
import com.topjohnwu.magisk.BuildConfig
import com.topjohnwu.magisk.DynAPK
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.core.Config
import com.topjohnwu.magisk.core.Info
import com.topjohnwu.magisk.core.Const
import com.topjohnwu.magisk.core.isRunningAsStub
import com.topjohnwu.magisk.core.utils.MediaStoreUtils
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.inputStream
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.outputStream
import com.topjohnwu.magisk.data.repository.NetworkService
import com.topjohnwu.magisk.di.Protected
import com.topjohnwu.magisk.events.dialog.EnvFixDialog
import com.topjohnwu.magisk.ktx.reboot
import com.topjohnwu.magisk.ktx.symlink
import com.topjohnwu.magisk.ktx.withStreams
import com.topjohnwu.magisk.ktx.writeTo
import com.topjohnwu.magisk.utils.Utils
import com.topjohnwu.signing.SignBoot
import com.topjohnwu.superuser.Shell
@ -37,7 +37,6 @@ import org.kamranzafar.jtar.TarHeader
import org.kamranzafar.jtar.TarInputStream
import org.kamranzafar.jtar.TarOutputStream
import org.koin.core.KoinComponent
import org.koin.core.get
import org.koin.core.inject
import timber.log.Timber
import java.io.*
@ -46,10 +45,8 @@ import java.security.SecureRandom
import java.util.*
import java.util.zip.ZipEntry
import java.util.zip.ZipInputStream
import kotlin.collections.set
abstract class MagiskInstallImpl protected constructor(
private var zipUri: Uri,
protected val console: MutableList<String> = NOPList.getInstance(),
private val logs: MutableList<String> = NOPList.getInstance()
) : KoinComponent {
@ -59,17 +56,7 @@ abstract class MagiskInstallImpl protected constructor(
private var tarOut: TarOutputStream? = null
private val service: NetworkService by inject()
protected val context: Context by inject()
companion object {
private val ABI_MAP = TreeMap<String, String>()
init {
ABI_MAP["armeabi-v7a"] = "arm"
ABI_MAP["arm64-v8a"] = "arm64"
ABI_MAP["x86"] = "x86"
ABI_MAP["x86_64"] = "x64"
}
}
protected val context: Context by inject(Protected)
private fun findImage(): Boolean {
srcBoot = "find_boot_image; echo \"\$BOOTIMAGE\"".fsh()
@ -98,69 +85,85 @@ abstract class MagiskInstallImpl protected constructor(
return true
}
@Suppress("DEPRECATION")
private fun extractZip(): Boolean {
val arch: String
val arch32: String
if (Build.VERSION.SDK_INT >= 21) {
arch = ABI_MAP[Build.SUPPORTED_ABIS[0]]!!
arch32 = ABI_MAP[Build.SUPPORTED_32_BIT_ABIS[0]]!!
private fun installDirFile(name: String): File {
return if (installDir is SuFile)
SuFile(installDir, name)
else
File(installDir, name)
}
private fun extractFiles(): Boolean {
console.add("- Device platform: ${Const.CPU_ABI}")
console.add("- Installing: ${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})")
val binDir = File(context.filesDir.parent, "install")
binDir.deleteRecursively()
binDir.mkdirs()
installDir = if (Shell.rootAccess()) {
SuFile("${Const.TMPDIR}/install")
} else {
arch = ABI_MAP[Build.CPU_ABI]!!
arch32 = arch
}
console.add("- Device platform: $arch")
console.add("- Magisk Manager: ${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})")
console.add("- Install target: ${Info.remote.magisk.version} (${Info.remote.magisk.versionCode})")
fun newFile(name: String): File {
return if (installDir is SuFile)
SuFile(installDir, name)
else
File(installDir, name)
binDir
}
try {
ZipInputStream(zipUri.inputStream().buffered()).use { zi ->
lateinit var ze: ZipEntry
while (zi.nextEntry?.let { ze = it } != null) {
if (ze.isDirectory)
continue
// Extract binaries
if (isRunningAsStub) {
ZipInputStream(DynAPK.current(context).inputStream().buffered()).use { zi ->
lateinit var ze: ZipEntry
while (zi.nextEntry?.let { ze = it } != null) {
if (ze.isDirectory)
continue
var name: String? = null
if (ze.name.startsWith("chromeos/")) {
name = ze.name
} else {
for (n in listOf("$arch32/", "common/", "META-INF/com/google/android/update-binary")) {
if (ze.name.startsWith(n)) {
name = ze.name.substring(ze.name.lastIndexOf('/') + 1)
break
}
val name = if (ze.name.startsWith("lib/${Const.CPU_ABI_32}/")) {
val n = ze.name.substring(ze.name.lastIndexOf('/') + 1)
n.substring(3, n.length - 3)
} else {
continue
}
val dest = File(binDir, name)
dest.outputStream().use { zi.copyTo(it) }
}
name ?: continue
val dest = newFile(name)
dest.parentFile!!.mkdirs()
SuFileOutputStream(dest).use { s -> zi.copyTo(s) }
}
} else {
val libs = Const.NATIVE_LIB_DIR.listFiles { _, name ->
name.startsWith("lib") && name.endsWith(".so")
} ?: emptyArray()
for (lib in libs) {
val name = lib.name.substring(3, lib.name.length - 3)
val bin = File(binDir, name)
symlink(lib.path, bin.path)
}
}
} catch (e: IOException) {
console.add("! Cannot unzip zip")
// Extract scripts
for (script in listOf("util_functions.sh", "boot_patch.sh", "addon.d.sh")) {
val dest = File(binDir, script)
context.assets.open(script).use { it.writeTo(dest) }
}
// Extract chromeos tools
File(binDir, "chromeos").mkdir()
for (file in listOf("futility", "kernel_data_key.vbprivk", "kernel.keyblock")) {
val name = "chromeos/$file"
val dest = File(binDir, name)
context.assets.open(name).use { it.writeTo(dest) }
}
} catch (e: Exception) {
console.add("! Unable to extract files")
Timber.e(e)
return false
}
val init64 = newFile("magiskinit64")
if (init64.exists() && arch != arch32) {
init64.renameTo(newFile("magiskinit"))
} else {
init64.delete()
if (installDir !== binDir) {
arrayOf(
"rm -rf $installDir",
"mkdir -p $installDir",
"cp_readlink $binDir $installDir",
"rm -rf $binDir"
).sh()
}
"cd $installDir; chmod 755 *".sh()
return true
}
@ -177,7 +180,7 @@ abstract class MagiskInstallImpl protected constructor(
lateinit var entry: TarEntry
fun decompressedStream() =
if (entry.name.contains(".lz4")) LZ4FrameInputStream(tarIn) else tarIn
if (entry.name.endsWith(".lz4")) LZ4FrameInputStream(tarIn) else tarIn
while (tarIn.nextEntry?.let { entry = it } != null) {
if (entry.name.contains("boot.img") ||
@ -186,12 +189,9 @@ abstract class MagiskInstallImpl protected constructor(
console.add("-- Extracting: $name")
val extract = File(installDir, name)
FileOutputStream(extract).use { decompressedStream().copyTo(it) }
decompressedStream().writeTo(extract)
} else if (entry.name.contains("vbmeta.img")) {
val rawData = ByteArrayOutputStream().let {
decompressedStream().copyTo(it)
it.toByteArray()
}
val rawData = decompressedStream().readBytes()
// Valid vbmeta.img should be at least 256 bytes
if (rawData.size < 256)
continue
@ -208,8 +208,8 @@ abstract class MagiskInstallImpl protected constructor(
tarIn.copyTo(tarOut, bufferSize = 1024 * 1024)
}
}
val boot = SuFile.open(installDir, "boot.img")
val recovery = SuFile.open(installDir, "recovery.img")
val boot = installDirFile("boot.img")
val recovery = installDirFile("recovery.img")
if (Config.recovery && recovery.exists() && boot.exists()) {
// Install Magisk to recovery
srcBoot = recovery.path
@ -306,10 +306,19 @@ abstract class MagiskInstallImpl protected constructor(
return false
}
// Fix up binaries
if (installDir is SuFile) {
"fix_env $installDir".sh()
} else {
"cp_readlink $installDir".sh()
}
return true
}
private fun patchBoot(): Boolean {
"cd $installDir".sh()
var srcNand = ""
if ("[ -c $srcBoot ] && nanddump -f boot.img $srcBoot".sh().isSuccess) {
srcNand = srcBoot
@ -334,26 +343,20 @@ abstract class MagiskInstallImpl protected constructor(
"KEEPVERITY=${Config.keepVerity} " +
"RECOVERYMODE=${Config.recovery}"
if (!("$FLAGS sh update-binary sh boot_patch.sh $srcBoot").sh().isSuccess) {
if (!"$FLAGS sh boot_patch.sh $srcBoot".sh().isSuccess)
return false
}
if (srcNand.isNotEmpty()) {
if (srcNand.isNotEmpty())
srcBoot = srcNand
}
val job = Shell.sh(
"./magiskboot cleanup",
"mv bin/busybox busybox",
"rm -rf magisk.apk bin boot.img update-binary",
"cd /")
val job = Shell.sh("./magiskboot cleanup", "cd /")
val patched = File(installDir, "new-boot.img")
val patched = installDirFile("new-boot.img")
if (isSigned) {
console.add("- Signing boot image with verity keys")
val signed = File(installDir, "signed.img")
val signed = installDirFile("signed.img")
try {
withStreams(SuFileInputStream(patched), signed.outputStream().buffered()) {
withStreams(SuFileInputStream(patched), SuFileOutputStream(signed)) {
input, out -> SignBoot.doSignature(null, null, input, out, "/boot")
}
} catch (e: IOException) {
@ -368,18 +371,10 @@ abstract class MagiskInstallImpl protected constructor(
return true
}
private fun copySepolicyRules(): Boolean {
if (Info.remote.magisk.versionCode >= 21100) {
// Copy existing rules for migration
"copy_sepolicy_rules".sh()
}
return true
}
private fun flashBoot(): Boolean {
if (!"direct_install $installDir $srcBoot".sh().isSuccess)
return false
"run_migrations".sh()
arrayOf("run_migrations", "copy_sepolicy_rules").sh()
return true
}
@ -403,24 +398,28 @@ abstract class MagiskInstallImpl protected constructor(
return true
}
protected fun String.sh() = Shell.sh(this).to(console, logs).exec()
protected fun uninstall(): Boolean {
val apk = if (isRunningAsStub) {
DynAPK.current(context).path
} else {
context.packageCodePath
}
return "run_uninstaller $apk".sh().isSuccess
}
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)
protected fun doPatchFile(patchFile: Uri) = extractFiles() && handleFile(patchFile)
protected fun direct() = findImage() && extractZip() && patchBoot() &&
copySepolicyRules() && flashBoot()
protected fun direct() = findImage() && extractFiles() && patchBoot() && flashBoot()
protected suspend fun secondSlot() = findSecondaryImage() && extractZip() &&
patchBoot() && copySepolicyRules() && flashBoot() && postOTA()
protected suspend fun secondSlot() =
findSecondaryImage() && extractFiles() && patchBoot() && flashBoot() && postOTA()
protected fun fixEnv(): Boolean {
installDir = SuFile("/data/adb/magisk")
Shell.su("rm -rf /data/adb/magisk/*").exec()
return extractZip() && Shell.su("fix_env").exec().isSuccess
}
protected fun fixEnv() = extractFiles() && "fix_env $installDir".sh().isSuccess
@WorkerThread
protected abstract suspend fun operations(): Boolean
@ -429,85 +428,84 @@ abstract class MagiskInstallImpl protected constructor(
}
abstract class MagiskInstaller(
zip: Uri,
console: MutableList<String>,
logs: MutableList<String>
) : MagiskInstallImpl(zip, console, logs) {
init {
installDir = File(get<Context>(Protected).filesDir.parent, "install")
"rm -rf $installDir".sh()
installDir.mkdirs()
}
) : MagiskInstallImpl(console, logs) {
override suspend fun exec(): Boolean {
val success = super.exec()
if (success) {
console.add("- All done!")
} else {
Shell.sh("rm -rf $installDir").submit()
if (installDir is SuFile) {
Shell.sh("rm -rf ${Const.TMPDIR}").submit()
} else {
Shell.sh("rm -rf $installDir").submit()
}
console.add("! Installation failed")
}
return success
}
class Patch(
zip: Uri,
private val uri: Uri,
console: MutableList<String>,
logs: MutableList<String>
) : MagiskInstaller(zip, console, logs) {
) : MagiskInstaller(console, logs) {
override suspend fun operations() = doPatchFile(uri)
}
class SecondSlot(
zip: Uri,
console: MutableList<String>,
logs: MutableList<String>
) : MagiskInstaller(zip, console, logs) {
) : MagiskInstaller(console, logs) {
override suspend fun operations() = secondSlot()
}
class Direct(
zip: Uri,
console: MutableList<String>,
logs: MutableList<String>
) : MagiskInstaller(zip, console, logs) {
) : MagiskInstaller(console, logs) {
override suspend fun operations() = direct()
}
class Emulator(
zip: Uri,
console: MutableList<String>,
logs: MutableList<String>
) : MagiskInstallImpl(zip, console, logs) {
) : MagiskInstaller(console, logs) {
override suspend fun operations() = fixEnv()
}
class Uninstall(
console: MutableList<String>,
logs: MutableList<String>
) : MagiskInstallImpl(console, logs) {
override suspend fun operations() = uninstall()
override suspend fun exec(): Boolean {
val success = super.exec()
if (success) {
console.add("- All done!")
} else {
console.add("! Installation failed")
UiThreadHandler.handler.postDelayed(3000) {
Shell.su("pm uninstall ${context.packageName}").exec()
}
}
return success
}
}
}
class EnvFixTask(zip: Uri) : MagiskInstallImpl(zip) {
class FixEnv(private val callback: () -> Unit) : MagiskInstallImpl() {
override suspend fun operations() = fixEnv()
override suspend fun operations() = fixEnv()
override suspend fun exec(): Boolean {
val success = super.exec()
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() }
return success
override suspend fun exec(): Boolean {
val success = super.exec()
callback()
Utils.toast(
if (success) R.string.reboot_delay_toast else R.string.setup_fail,
Toast.LENGTH_LONG
)
if (success)
UiThreadHandler.handler.postDelayed(5000) { reboot() }
return success
}
}
}

View File

@ -2,14 +2,17 @@ package com.topjohnwu.magisk.core.utils
import android.content.Context
import android.os.Build
import com.topjohnwu.magisk.DynAPK
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.core.Config
import com.topjohnwu.magisk.core.Const
import com.topjohnwu.magisk.core.Info
import com.topjohnwu.magisk.core.wrap
import com.topjohnwu.magisk.core.*
import com.topjohnwu.magisk.ktx.cachedFile
import com.topjohnwu.magisk.ktx.deviceProtectedContext
import com.topjohnwu.magisk.ktx.rawResource
import com.topjohnwu.magisk.ktx.writeTo
import com.topjohnwu.superuser.Shell
import com.topjohnwu.superuser.ShellUtils
import java.io.File
import java.util.jar.JarFile
class RootInit : Shell.Initializer() {
@ -17,31 +20,50 @@ class RootInit : Shell.Initializer() {
return init(context.wrap(), shell)
}
fun init(context: Context, shell: Shell): Boolean {
shell.newJob().apply {
add("export SDK_INT=${Build.VERSION.SDK_INT}")
if (Const.Version.atLeast_20_4()) {
add("export MAGISKTMP=\$(magisk --path)/.magisk")
} else {
add("export MAGISKTMP=/sbin/.magisk")
}
if (Const.Version.atLeast_21_0()) {
add("export ASH_STANDALONE=1")
add("[ -x /data/adb/magisk/busybox ] && exec /data/adb/magisk/busybox sh")
} else {
add("export PATH=\"\$MAGISKTMP/busybox:\$PATH\"")
}
add(context.rawResource(R.raw.manager))
if (shell.isRoot) {
add(context.rawResource(R.raw.util_functions))
}
add("mm_init")
}.exec()
private fun init(context: Context, shell: Shell): Boolean {
fun fastCmd(cmd: String) = ShellUtils.fastCmd(shell, cmd)
fun getVar(name: String) = fastCmd("echo \$$name")
fun getBool(name: String) = getVar(name).toBoolean()
shell.newJob().apply {
add("export SDK_INT=${Build.VERSION.SDK_INT}")
add("export ASH_STANDALONE=1")
val localBB: File
if (isRunningAsStub) {
val jar = JarFile(DynAPK.current(context))
val bb = jar.getJarEntry("lib/${Const.CPU_ABI_32}/libbusybox.so")
localBB = context.deviceProtectedContext.cachedFile("busybox")
jar.getInputStream(bb).writeTo(localBB)
localBB.setExecutable(true)
} else {
localBB = File(Const.NATIVE_LIB_DIR, "libbusybox.so")
}
if (!shell.isRoot) {
// Directly execute the file
add("exec $localBB sh")
} else {
// Copy it out of /data to workaround Samsung BS
add(
"export MAGISKTMP=\$(magisk --path)/.magisk",
"if [ -x \$MAGISKTMP/busybox/busybox ]; then",
" cp -af $localBB \$MAGISKTMP/busybox/busybox",
" exec \$MAGISKTMP/busybox/busybox sh",
"else",
" exec $localBB sh",
"fi"
)
}
add(context.rawResource(R.raw.manager))
if (shell.isRoot) {
add(context.assets.open("util_functions.sh"))
}
add("mm_init")
}.exec()
Const.MAGISKTMP = getVar("MAGISKTMP")
Info.isSAR = getBool("SYSTEM_ROOT")
Info.ramdisk = getBool("RAMDISKEXIST")

View File

@ -110,7 +110,7 @@ class SelectModuleEvent : ViewEvent(), FragmentExecutor {
activity.startActivityForResult(intent, Const.ID.FETCH_ZIP) { code, intent ->
if (code == Activity.RESULT_OK && intent != null) {
intent.data?.also {
MainDirections.actionFlashFragment(it, Const.Value.FLASH_ZIP).navigate()
MainDirections.actionFlashFragment(Const.Value.FLASH_ZIP, it).navigate()
}
}
}

View File

@ -10,7 +10,9 @@ abstract class DialogEvent : ViewEvent(), ActivityExecutor {
protected lateinit var dialog: MagiskDialog
override fun invoke(activity: BaseUIActivity<*, *>) {
dialog = MagiskDialog(activity).apply(this::build).reveal()
dialog = MagiskDialog(activity)
.apply { setOwnerActivity(activity) }
.apply(this::build).reveal()
}
abstract fun build(dialog: MagiskDialog)

View File

@ -1,15 +1,11 @@
package com.topjohnwu.magisk.events.dialog
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import androidx.localbroadcastmanager.content.LocalBroadcastManager
import androidx.lifecycle.lifecycleScope
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.core.download.Action.EnvFix
import com.topjohnwu.magisk.core.download.DownloadService
import com.topjohnwu.magisk.core.download.Subject.Magisk
import com.topjohnwu.magisk.core.base.BaseActivity
import com.topjohnwu.magisk.core.tasks.MagiskInstaller
import com.topjohnwu.magisk.view.MagiskDialog
import kotlinx.coroutines.launch
class EnvFixDialog : DialogEvent() {
@ -24,20 +20,17 @@ class EnvFixDialog : DialogEvent() {
.applyMessage(R.string.setup_msg)
.resetButtons()
.cancellable(false)
val lbm = LocalBroadcastManager.getInstance(dialog.context)
lbm.registerReceiver(object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent?) {
(dialog.ownerActivity as BaseActivity).lifecycleScope.launch {
MagiskInstaller.FixEnv {
dialog.dismiss()
lbm.unregisterReceiver(this)
}
}, IntentFilter(DISMISS))
DownloadService.start(dialog.context, Magisk(EnvFix))
}.exec()
}
}
}
.applyButton(MagiskDialog.ButtonType.NEGATIVE) {
titleRes = android.R.string.cancel
}
.let { Unit }
.let { }
companion object {
const val DISMISS = "com.topjohnwu.magisk.ENV_DONE"

View File

@ -14,7 +14,7 @@ class ModuleInstallDialog(private val item: OnlineModule) : DialogEvent() {
with(dialog) {
fun download(install: Boolean) {
val config = if (install) Action.Flash.Primary else Action.Download
val config = if (install) Action.Flash else Action.Download
val subject = Subject.Module(item, config)
DownloadService.start(context, subject)
}

View File

@ -4,9 +4,7 @@ import android.app.ProgressDialog
import android.widget.Toast
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.core.Info
import com.topjohnwu.magisk.core.download.Action
import com.topjohnwu.magisk.core.download.DownloadService
import com.topjohnwu.magisk.core.download.Subject
import com.topjohnwu.magisk.ui.flash.FlashFragment
import com.topjohnwu.magisk.utils.Utils
import com.topjohnwu.magisk.view.MagiskDialog
import com.topjohnwu.superuser.Shell
@ -46,7 +44,7 @@ class UninstallDialog : DialogEvent() {
}
private fun completeUninstall() {
DownloadService.start(dialog.context, Subject.Magisk(Action.Uninstall))
FlashFragment.uninstall()
}
}

View File

@ -23,6 +23,7 @@ import android.graphics.drawable.LayerDrawable
import android.net.Uri
import android.os.Build
import android.os.Build.VERSION.SDK_INT
import android.system.Os
import android.text.PrecomputedText
import android.view.View
import android.view.ViewGroup
@ -59,6 +60,20 @@ import java.lang.reflect.Array as JArray
val packageName: String get() = get<Context>().packageName
fun symlink(oldPath: String, newPath: String) {
if (SDK_INT >= 21) {
Os.symlink(oldPath, newPath)
} else {
// Just copy the files pre 5.0
val old = File(oldPath)
val newFile = File(newPath)
old.copyTo(newFile)
if (old.canExecute())
newFile.setExecutable(true)
}
}
val ServiceInfo.isIsolated get() = (flags and FLAG_ISOLATED_PROCESS) != 0
@get:SuppressLint("InlinedApi")
@ -83,6 +98,11 @@ fun Context.getBitmap(id: Int): Bitmap {
return bitmap
}
val Context.deviceProtectedContext: Context get() =
if (SDK_INT >= 24) {
createDeviceProtectedStorageContext()
} else { this }
fun Intent.startActivity(context: Context) = context.startActivity(this)
fun Intent.startActivityWithRoot() {

View File

@ -17,13 +17,12 @@ import com.topjohnwu.magisk.ui.MainActivity
import org.koin.androidx.viewmodel.ext.android.viewModel
import org.koin.core.parameter.parametersOf
import com.topjohnwu.magisk.MainDirections.Companion.actionFlashFragment as toFlash
import com.topjohnwu.magisk.ui.flash.FlashFragmentArgs as args
class FlashFragment : BaseUIFragment<FlashViewModel, FragmentFlashMd2Binding>() {
override val layoutRes = R.layout.fragment_flash_md2
override val viewModel by viewModel<FlashViewModel> {
parametersOf(args.fromBundle(requireArguments()))
parametersOf(FlashFragmentArgs.fromBundle(requireArguments()))
}
private var defaultOrientation = -1
@ -78,7 +77,7 @@ class FlashFragment : BaseUIFragment<FlashViewModel, FragmentFlashMd2Binding>()
companion object {
private fun createIntent(context: Context, args: args) =
private fun createIntent(context: Context, args: FlashFragmentArgs) =
NavDeepLinkBuilder(context)
.setGraph(R.navigation.main)
.setComponentName(MainActivity::class.java.cmp(context.packageName))
@ -91,59 +90,34 @@ class FlashFragment : BaseUIFragment<FlashViewModel, FragmentFlashMd2Binding>()
/* Flashing is understood as installing / flashing magisk itself */
fun flashIntent(context: Context, file: Uri, isSecondSlot: Boolean, id: Int = -1) = args(
installer = file,
action = flashType(isSecondSlot),
dismissId = id
).let { createIntent(context, it) }
fun flash(file: Uri, isSecondSlot: Boolean, id: Int) = toFlash(
installer = file,
action = flashType(isSecondSlot),
dismissId = id
fun flash(isSecondSlot: Boolean) = toFlash(
action = flashType(isSecondSlot)
).let { BaseUIActivity.postDirections(it) }
/* Patching is understood as injecting img files with magisk */
fun patchIntent(context: Context, file: Uri, uri: Uri, id: Int = -1) = args(
installer = file,
fun patch(uri: Uri) = toFlash(
action = Const.Value.PATCH_FILE,
additionalData = uri,
dismissId = id
).let { createIntent(context, it) }
fun patch(file: Uri, uri: Uri, id: Int) = toFlash(
installer = file,
action = Const.Value.PATCH_FILE,
additionalData = uri,
dismissId = id
additionalData = uri
).let { BaseUIActivity.postDirections(it) }
/* Uninstalling is understood as removing magisk entirely */
fun uninstallIntent(context: Context, file: Uri, id: Int = -1) = args(
installer = file,
action = Const.Value.UNINSTALL,
dismissId = id
).let { createIntent(context, it) }
fun uninstall(file: Uri, id: Int) = toFlash(
installer = file,
action = Const.Value.UNINSTALL,
dismissId = id
fun uninstall() = toFlash(
action = Const.Value.UNINSTALL
).let { BaseUIActivity.postDirections(it) }
/* Installing is understood as flashing modules / zips */
fun installIntent(context: Context, file: Uri, id: Int = -1) = args(
installer = file,
fun installIntent(context: Context, file: Uri, id: Int = -1) = FlashFragmentArgs(
action = Const.Value.FLASH_ZIP,
additionalData = file,
dismissId = id
).let { createIntent(context, it) }
fun install(file: Uri, id: Int) = toFlash(
installer = file,
action = Const.Value.FLASH_ZIP,
additionalData = file,
dismissId = id
).let { BaseUIActivity.postDirections(it) }
}

View File

@ -27,9 +27,7 @@ import com.topjohnwu.superuser.Shell
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
class FlashViewModel(
args: FlashFragmentArgs
) : BaseViewModel() {
class FlashViewModel(args: FlashFragmentArgs) : BaseViewModel() {
@get:Bindable
var showReboot = Shell.rootAccess()
@ -52,36 +50,35 @@ class FlashViewModel(
}
init {
args.dismissId.takeIf { it != -1 }?.also {
Notifications.mgr.cancel(it)
}
val (installer, action, uri) = args
startFlashing(installer, uri, action)
val (action, uri, id) = args
if (id != -1)
Notifications.mgr.cancel(id)
startFlashing(action, uri)
}
private fun startFlashing(installer: Uri, uri: Uri?, action: String) {
private fun startFlashing(action: String, uri: Uri?) {
viewModelScope.launch {
val result = when (action) {
Const.Value.FLASH_ZIP -> {
FlashZip(installer, outItems, logItems).exec()
FlashZip(uri!!, outItems, logItems).exec()
}
Const.Value.UNINSTALL -> {
showReboot = false
FlashZip.Uninstall(installer, outItems, logItems).exec()
MagiskInstaller.Uninstall(outItems, logItems).exec()
}
Const.Value.FLASH_MAGISK -> {
if (Info.isEmulator)
MagiskInstaller.Emulator(installer, outItems, logItems).exec()
MagiskInstaller.Emulator(outItems, logItems).exec()
else
MagiskInstaller.Direct(installer, outItems, logItems).exec()
MagiskInstaller.Direct(outItems, logItems).exec()
}
Const.Value.FLASH_INACTIVE_SLOT -> {
MagiskInstaller.SecondSlot(installer, outItems, logItems).exec()
MagiskInstaller.SecondSlot(outItems, logItems).exec()
}
Const.Value.PATCH_FILE -> {
uri ?: return@launch
showReboot = false
MagiskInstaller.Patch(installer, uri, outItems, logItems).exec()
MagiskInstaller.Patch(uri, outItems, logItems).exec()
}
else -> {
back()

View File

@ -7,7 +7,6 @@ import android.view.ViewGroup
import androidx.lifecycle.viewModelScope
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.arch.BaseUIFragment
import com.topjohnwu.magisk.core.download.BaseDownloader
import com.topjohnwu.magisk.databinding.FragmentInstallMd2Binding
import com.topjohnwu.magisk.ktx.coroutineScope
import org.koin.androidx.viewmodel.ext.android.viewModel
@ -23,7 +22,6 @@ class InstallFragment : BaseUIFragment<InstallViewModel, FragmentInstallMd2Bindi
// Allow markwon to run in viewmodel scope
binding.releaseNotes.coroutineScope = viewModel.viewModelScope
BaseDownloader.observeProgress(this, viewModel::onProgressUpdate)
}
override fun onCreateView(

View File

@ -8,23 +8,19 @@ import com.topjohnwu.magisk.BR
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.arch.BaseViewModel
import com.topjohnwu.magisk.core.Info
import com.topjohnwu.magisk.core.download.Action
import com.topjohnwu.magisk.core.download.DownloadService
import com.topjohnwu.magisk.core.download.Subject
import com.topjohnwu.magisk.data.repository.NetworkService
import com.topjohnwu.magisk.events.MagiskInstallFileEvent
import com.topjohnwu.magisk.events.dialog.SecondSlotWarningDialog
import com.topjohnwu.magisk.ui.flash.FlashFragment
import com.topjohnwu.magisk.utils.set
import com.topjohnwu.superuser.Shell
import kotlinx.coroutines.launch
import org.koin.core.get
import timber.log.Timber
import java.io.IOException
import kotlin.math.roundToInt
class InstallViewModel(
svc: NetworkService
) : BaseViewModel(State.LOADED) {
) : BaseViewModel() {
val isRooted = Shell.rootAccess()
val skipOptions = Info.isEmulator || (Info.ramdisk && !Info.isFDE && Info.isSAR)
@ -53,10 +49,6 @@ class InstallViewModel(
}
}
@get:Bindable
var progress = 0
set(value) = set(value, field, { field = it }, BR.progress)
@get:Bindable
var data: Uri? = null
set(value) = set(value, field, { field = it }, BR.data)
@ -75,34 +67,17 @@ class InstallViewModel(
}
}
fun onProgressUpdate(progress: Float, subject: Subject) {
if (subject !is Subject.Magisk) {
return
}
this.progress = progress.times(100).roundToInt()
if (this.progress >= 100) {
state = State.LOADED
} else if (this.progress < -150) {
state = State.LOADING_FAILED
}
}
fun step(nextStep: Int) {
step = nextStep
}
fun install() {
DownloadService.start(get(), Subject.Magisk(resolveAction()))
when (method) {
R.id.method_patch -> FlashFragment.patch(data!!)
R.id.method_direct -> FlashFragment.flash(false)
R.id.method_inactive_slot -> FlashFragment.flash(true)
else -> error("Unknown value")
}
state = State.LOADING
}
// ---
private fun resolveAction() = when (method) {
R.id.method_download -> Action.Download
R.id.method_patch -> Action.Patch(data!!)
R.id.method_direct -> Action.Flash.Primary
R.id.method_inactive_slot -> Action.Flash.Secondary
else -> error("Unknown value")
}
}

View File

@ -25,306 +25,217 @@
app:fitsSystemWindowsInsets="top|bottom"
tools:paddingTop="24dp">
<FrameLayout
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
android:layout_height="wrap_content"
android:clipToPadding="false"
android:orientation="vertical"
android:paddingTop="@dimen/l_50">
<LinearLayout
goneUnless="@{viewModel.loaded &amp;&amp; viewModel.progress &lt;= 0}"
<com.google.android.material.card.MaterialCardView
style="@style/WidgetFoundation.Card"
gone="@{viewModel.skipOptions}"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:orientation="vertical"
android:paddingTop="@dimen/l_50">
<com.google.android.material.card.MaterialCardView
style="@style/WidgetFoundation.Card"
gone="@{viewModel.skipOptions}"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/l1"
android:layout_marginEnd="@dimen/l1"
android:focusable="false">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<ImageView
style="@style/WidgetFoundation.Icon"
isSelected="@{viewModel.step > 0}"
android:layout_marginStart="@dimen/l_25"
app:srcCompat="@drawable/ic_check_circle_md2" />
<TextView
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_marginStart="@dimen/l1"
android:layout_weight="1"
android:gravity="center_vertical"
android:text="@string/install_options_title"
android:textAppearance="@style/AppearanceFoundation.Body"
android:textStyle="bold" />
<Button
style="@style/WidgetFoundation.Button.Text"
gone="@{viewModel.step != 0}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="@{() -> viewModel.step(1)}"
android:text="@string/install_next" />
</LinearLayout>
<LinearLayout
gone="@{viewModel.step != 0}"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/l1"
android:layout_marginTop="@dimen/l_50"
android:layout_marginEnd="@dimen/l1"
android:layout_marginBottom="@dimen/l_50"
android:orientation="vertical"
android:paddingStart="3dp"
android:paddingEnd="3dp"
tools:layout_gravity="center">
<CheckBox
style="@style/WidgetFoundation.Checkbox"
gone="@{Info.isSAR}"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:checked="@={Config.keepVerity}"
android:text="@string/keep_dm_verity"
tools:checked="true" />
<CheckBox
style="@style/WidgetFoundation.Checkbox"
goneUnless="@{Info.isFDE}"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:checked="@={Config.keepEnc}"
android:text="@string/keep_force_encryption"
app:tint="?colorPrimary" />
<CheckBox
style="@style/WidgetFoundation.Checkbox"
gone="@{Info.ramdisk}"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:checked="@={Config.recovery}"
android:text="@string/recovery_mode"
app:tint="?colorPrimary" />
</LinearLayout>
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
<com.google.android.material.card.MaterialCardView
style="@style/WidgetFoundation.Card"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/l1"
android:layout_marginTop="@dimen/l1"
android:layout_marginEnd="@dimen/l1"
android:focusable="false">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<ImageView
style="@style/WidgetFoundation.Icon"
isSelected="@{viewModel.step > 1}"
android:layout_marginStart="@dimen/l_25"
app:srcCompat="@drawable/ic_check_circle_md2" />
<TextView
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_marginStart="@dimen/l1"
android:layout_weight="1"
android:gravity="center_vertical"
android:text="@string/install_method_title"
android:textAppearance="@style/AppearanceFoundation.Body"
android:textStyle="bold" />
<Button
style="@style/WidgetFoundation.Button.Text"
gone="@{viewModel.step != 1}"
isEnabled="@{viewModel.method == @id/method_patch ? viewModel.data != null : viewModel.method != -1}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="@{() -> viewModel.install()}"
android:text="@string/install_start"
app:icon="@drawable/ic_forth_md2"
app:iconGravity="textEnd" />
</LinearLayout>
<RadioGroup
gone="@{viewModel.step != 1}"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/l1"
android:layout_marginTop="@dimen/l_50"
android:layout_marginEnd="@dimen/l1"
android:layout_marginBottom="@dimen/l_50"
android:checkedButton="@={viewModel.method}"
android:paddingStart="3dp"
android:paddingEnd="3dp">
<RadioButton
android:id="@+id/method_download"
style="@style/WidgetFoundation.RadioButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/download_zip_only"
tools:checked="true" />
<RadioButton
android:id="@+id/method_patch"
style="@style/WidgetFoundation.RadioButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/select_patch_file" />
<RadioButton
android:id="@+id/method_direct"
style="@style/WidgetFoundation.RadioButton"
gone="@{!viewModel.rooted}"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/direct_install" />
<RadioButton
android:id="@+id/method_inactive_slot"
style="@style/WidgetFoundation.RadioButton"
gone="@{viewModel.noSecondSlot}"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/install_inactive_slot" />
</RadioGroup>
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
<com.google.android.material.card.MaterialCardView
style="@style/WidgetFoundation.Card"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/l1"
android:layout_marginStart="@dimen/l1"
android:layout_marginEnd="@dimen/l1"
android:focusable="false">
<TextView
android:id="@+id/release_notes"
markdownText="@{viewModel.notes}"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="15dp"
android:textAppearance="@style/AppearanceFoundation.Caption"
android:visibility="gone"
tools:maxLines="5"
tools:text="@tools:sample/lorem/random"
tools:visibility="visible" />
</com.google.android.material.card.MaterialCardView>
</LinearLayout>
<LinearLayout
goneUnless="@{viewModel.loading}"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/l1"
android:layout_marginEnd="@dimen/l1"
android:gravity="center"
android:orientation="vertical">
android:focusable="false">
<TextView
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:text="@string/loading"
android:textAppearance="@style/AppearanceFoundation.Title" />
android:orientation="vertical">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/l1">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<ProgressBar
style="@style/WidgetFoundation.ProgressBar"
invisible="@{viewModel.progress &lt;= 0}"
android:layout_width="100dp"
android:layout_gravity="center"
android:progress="@{viewModel.progress}" />
<ImageView
style="@style/WidgetFoundation.Icon"
isSelected="@{viewModel.step > 0}"
android:layout_marginStart="@dimen/l_25"
app:srcCompat="@drawable/ic_check_circle_md2" />
<ProgressBar
style="@style/WidgetFoundation.ProgressBar.Indeterminate"
invisibleUnless="@{viewModel.progress &lt;= 0}"
android:layout_width="100dp"
android:layout_gravity="center" />
<TextView
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_marginStart="@dimen/l1"
android:layout_weight="1"
android:gravity="center_vertical"
android:text="@string/install_options_title"
android:textAppearance="@style/AppearanceFoundation.Body"
android:textStyle="bold" />
</FrameLayout>
<Button
style="@style/WidgetFoundation.Button.Text"
gone="@{viewModel.step != 0}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="@{() -> viewModel.step(1)}"
android:text="@string/install_next" />
</LinearLayout>
</LinearLayout>
<LinearLayout
gone="@{viewModel.step != 0}"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/l1"
android:layout_marginTop="@dimen/l_50"
android:layout_marginEnd="@dimen/l1"
android:layout_marginBottom="@dimen/l_50"
android:orientation="vertical"
android:paddingStart="3dp"
android:paddingEnd="3dp"
tools:layout_gravity="center">
<LinearLayout
goneUnless="@{viewModel.loaded &amp;&amp; viewModel.progress >= 100}"
<CheckBox
style="@style/WidgetFoundation.Checkbox"
gone="@{Info.isSAR}"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:checked="@={Config.keepVerity}"
android:text="@string/keep_dm_verity"
tools:checked="true" />
<CheckBox
style="@style/WidgetFoundation.Checkbox"
goneUnless="@{Info.isFDE}"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:checked="@={Config.keepEnc}"
android:text="@string/keep_force_encryption"
app:tint="?colorPrimary" />
<CheckBox
style="@style/WidgetFoundation.Checkbox"
gone="@{Info.ramdisk}"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:checked="@={Config.recovery}"
android:text="@string/recovery_mode"
app:tint="?colorPrimary" />
</LinearLayout>
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
<com.google.android.material.card.MaterialCardView
style="@style/WidgetFoundation.Card"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:gravity="center"
android:orientation="vertical">
android:layout_marginStart="@dimen/l1"
android:layout_marginTop="@dimen/l1"
android:layout_marginEnd="@dimen/l1"
android:focusable="false">
<TextView
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:text="@string/download_complete"
android:textAppearance="@style/AppearanceFoundation.Title" />
android:orientation="vertical">
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<LinearLayout
goneUnless="@{viewModel.loadFailed}"
<ImageView
style="@style/WidgetFoundation.Icon"
isSelected="@{viewModel.step > 1}"
android:layout_marginStart="@dimen/l_25"
app:srcCompat="@drawable/ic_check_circle_md2" />
<TextView
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_marginStart="@dimen/l1"
android:layout_weight="1"
android:gravity="center_vertical"
android:text="@string/install_method_title"
android:textAppearance="@style/AppearanceFoundation.Body"
android:textStyle="bold" />
<Button
style="@style/WidgetFoundation.Button.Text"
gone="@{viewModel.step != 1}"
isEnabled="@{viewModel.method == @id/method_patch ? viewModel.data != null : viewModel.method != -1}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="@{() -> viewModel.install()}"
android:text="@string/install_start"
app:icon="@drawable/ic_forth_md2"
app:iconGravity="textEnd" />
</LinearLayout>
<RadioGroup
gone="@{viewModel.step != 1}"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/l1"
android:layout_marginTop="@dimen/l_50"
android:layout_marginEnd="@dimen/l1"
android:layout_marginBottom="@dimen/l_50"
android:checkedButton="@={viewModel.method}"
android:paddingStart="3dp"
android:paddingEnd="3dp">
<RadioButton
android:id="@+id/method_patch"
style="@style/WidgetFoundation.RadioButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/select_patch_file" />
<RadioButton
android:id="@+id/method_direct"
style="@style/WidgetFoundation.RadioButton"
gone="@{!viewModel.rooted}"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/direct_install" />
<RadioButton
android:id="@+id/method_inactive_slot"
style="@style/WidgetFoundation.RadioButton"
gone="@{viewModel.noSecondSlot}"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/install_inactive_slot" />
</RadioGroup>
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
<com.google.android.material.card.MaterialCardView
style="@style/WidgetFoundation.Card"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:gravity="center"
android:orientation="vertical">
android:layout_marginTop="@dimen/l1"
android:layout_marginStart="@dimen/l1"
android:layout_marginEnd="@dimen/l1"
android:focusable="false">
<TextView
android:id="@+id/release_notes"
markdownText="@{viewModel.notes}"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:text="@string/download_file_error"
android:textAppearance="@style/AppearanceFoundation.Title" />
android:layout_margin="15dp"
android:textAppearance="@style/AppearanceFoundation.Caption"
android:visibility="gone"
tools:maxLines="5"
tools:text="@tools:sample/lorem/random"
tools:visibility="visible" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
</FrameLayout>
</LinearLayout>
</androidx.core.widget.NestedScrollView>

View File

@ -49,10 +49,6 @@
android:label="FlashFragment"
tools:layout="@layout/fragment_flash_md2">
<argument
android:name="installer"
app:argType="android.net.Uri" />
<argument
android:name="action"
app:argType="string" />

View File

@ -1 +0,0 @@
/util_functions.sh

View File

@ -13,21 +13,35 @@ env_check() {
return 0
}
fix_env() {
cd $MAGISKBIN
PATH=/system/bin /system/bin/sh update-binary -x
./busybox rm -f update-binary magisk.apk
./busybox chmod -R 755 .
./magiskinit -x magisk magisk
cp_readlink() {
if [ -z $2 ]; then
cd $1
else
cp -af $1/. $2
cd $2
fi
for file in *; do
if [ -L $file ]; then
local full=$(readlink -f $file)
rm $file
cp -af $full $file
fi
done
chmod -R 755 .
cd /
}
direct_install() {
rm -rf $MAGISKBIN/* 2>/dev/null
fix_env() {
# Cleanup and make dirs
rm -rf $MAGISKBIN/*
mkdir -p $MAGISKBIN 2>/dev/null
chmod 700 $NVBASE
cp -af $1/. $MAGISKBIN
rm -f $MAGISKBIN/new-boot.img
rm -rf $1
chown -R 0:0 $MAGISKBIN
}
direct_install() {
echo "- Flashing new boot image"
flash_image $1/new-boot.img $2
case $? in
@ -40,10 +54,20 @@ direct_install() {
return 2
;;
esac
rm -rf $1
rm -f $1/new-boot.img
fix_env $1
return 0
}
run_uninstaller() {
rm -rf /dev/tmp
mkdir -p /dev/tmp/install
unzip -o "$1" "assets/*" "lib/*" -d /dev/tmp/install
INSTALLER=/dev/tmp/install sh /dev/tmp/install/assets/magisk_uninstaller.sh dummy 1 "$1"
}
restore_imgs() {
[ -z $SHA1 ] && return 1
local BACKUPDIR=/data/magisk_backup_$SHA1

View File

@ -18,7 +18,7 @@ buildscript {
extra["vNav"] = vNav
dependencies {
classpath("com.android.tools.build:gradle:4.1.1")
classpath("com.android.tools.build:gradle:4.1.2")
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.4.21")
classpath("androidx.navigation:navigation-safe-args-gradle-plugin:${vNav}")

144
build.py
View File

@ -222,7 +222,7 @@ def sign_zip(unsigned):
msg = '* Signing APK'
apksigner = op.join(find_build_tools(), 'apksigner')
execArgs = [apksigner, 'sign',
exec_args = [apksigner, 'sign',
'--ks', config['keyStore'],
'--ks-pass', f'pass:{config["keyStorePass"]}',
'--ks-key-alias', config['keyAlias'],
@ -232,14 +232,14 @@ def sign_zip(unsigned):
if unsigned.endswith('.zip'):
msg = '* Signing zip'
execArgs.extend(['--min-sdk-version', '17',
exec_args.extend(['--min-sdk-version', '17',
'--v2-signing-enabled', 'false',
'--v3-signing-enabled', 'false'])
execArgs.append(unsigned)
exec_args.append(unsigned)
header(msg)
proc = execv(execArgs)
proc = execv(exec_args)
if proc.returncode != 0:
error('Signing failed!')
@ -254,29 +254,6 @@ def binary_dump(src, out, var_name):
out.flush()
def gen_update_binary():
bs = 1024
update_bin = bytearray(bs)
file = op.join('native', 'out', 'x86', 'busybox')
with open(file, 'rb') as f:
x86_bb = f.read()
file = op.join('native', 'out', 'armeabi-v7a', 'busybox')
with open(file, 'rb') as f:
arm_bb = f.read()
file = op.join('scripts', 'update_binary.sh')
with open(file, 'rb') as f:
script = f.read()
# Align x86 busybox to bs
blk_cnt = (len(x86_bb) - 1) // bs + 1
script = script.replace(b'__X86_CNT__', b'%d' % blk_cnt)
update_bin[:len(script)] = script
update_bin.extend(x86_bb)
# Padding for alignment
update_bin.extend(b'\0' * (blk_cnt * bs - len(x86_bb)))
update_bin.extend(arm_bb)
return update_bin
def run_ndk_build(flags):
os.chdir('native')
proc = system(f'{ndk_build} {base_flags} {flags} -j{cpu_count}')
@ -407,96 +384,20 @@ def build_snet(args):
header('Output: ' + target)
def zip_main(args):
header('* Packing Flashable Zip')
if config['prettyName']:
name = f'Magisk-v{config["version"]}.zip'
elif args.release:
name = 'magisk-release.zip'
else:
name = 'magisk-debug.zip'
output = op.join(config['outdir'], name)
with zipfile.ZipFile(output, 'w', compression=zipfile.ZIP_DEFLATED, allowZip64=False) as zipf:
# update-binary
target = op.join('META-INF', 'com', 'google',
'android', 'update-binary')
vprint('zip: ' + target)
zipf.writestr(target, gen_update_binary())
# updater-script
source = op.join('scripts', 'flash_script.sh')
target = op.join('META-INF', 'com', 'google',
'android', 'updater-script')
zip_with_msg(zipf, source, target)
# Binaries
for lib_dir, zip_dir in zip(archs, ('arm', 'x86')):
for binary in ['magiskinit', 'magiskboot', 'magisk']:
source = op.join('native', 'out', lib_dir, binary)
target = op.join(zip_dir, 'magisk32' if binary == 'magisk' else binary)
zip_with_msg(zipf, source, target)
for lib_dir, zip_dir in zip(arch64, ('arm', 'x86')):
source = op.join('native', 'out', lib_dir, 'magisk')
target = op.join(zip_dir, 'magisk64')
zip_with_msg(zipf, source, target)
# APK
source = op.join(
config['outdir'], 'app-release.apk' if args.release else 'app-debug.apk')
target = op.join('common', 'magisk.apk')
zip_with_msg(zipf, source, target)
# boot_patch.sh
source = op.join('scripts', 'boot_patch.sh')
target = op.join('common', 'boot_patch.sh')
zip_with_msg(zipf, source, target)
# util_functions.sh
source = op.join('scripts', 'util_functions.sh')
with open(source, 'r') as script:
# Add version info util_functions.sh
util_func = script.read().replace(
'#MAGISK_VERSION_STUB',
f'MAGISK_VER="{config["version"]}"\nMAGISK_VER_CODE={config["versionCode"]}')
target = op.join('common', 'util_functions.sh')
vprint(f'zip: {source} -> {target}')
zipf.writestr(target, util_func)
# addon.d.sh
source = op.join('scripts', 'addon.d.sh')
target = op.join('common', 'addon.d.sh')
zip_with_msg(zipf, source, target)
# chromeos
for tool in ['futility', 'kernel_data_key.vbprivk', 'kernel.keyblock']:
if tool == 'futility':
source = op.join('tools', tool)
else:
source = op.join('tools', 'keys', tool)
target = op.join('chromeos', tool)
zip_with_msg(zipf, source, target)
# End of zipping
sign_zip(output)
header('Output: ' + output)
def zip_uninstaller(args):
header('* Packing Uninstaller Zip')
datestr = datetime.datetime.now().strftime("%Y%m%d")
datestr = f'{datetime.datetime.now():%Y%m%d}'
name = f'Magisk-uninstaller-{datestr}.zip' if config['prettyName'] else 'magisk-uninstaller.zip'
output = op.join(config['outdir'], name)
with zipfile.ZipFile(output, 'w', compression=zipfile.ZIP_DEFLATED, allowZip64=False) as zipf:
# update-binary
source = op.join('scripts', 'update_binary.sh')
target = op.join('META-INF', 'com', 'google',
'android', 'update-binary')
vprint('zip: ' + target)
zipf.writestr(target, gen_update_binary())
zip_with_msg(zipf, source, target)
# updater-script
source = op.join('scripts', 'magisk_uninstaller.sh')
target = op.join('META-INF', 'com', 'google',
@ -504,17 +405,23 @@ def zip_uninstaller(args):
zip_with_msg(zipf, source, target)
# Binaries
for lib_dir, zip_dir in [('armeabi-v7a', 'arm'), ('x86', 'x86')]:
source = op.join('native', 'out', lib_dir, 'magiskboot')
target = op.join(zip_dir, 'magiskboot')
zip_with_msg(zipf, source, target)
for exe in ('busybox', 'magiskboot'):
for arch in archs:
source = op.join('native', 'out', arch, exe)
target = op.join('lib', arch, f'lib{exe}.so')
zip_with_msg(zipf, source, target)
# util_functions.sh
source = op.join('scripts', 'util_functions.sh')
with open(source, 'r') as script:
target = op.join('util_functions.sh')
# Add version info util_functions.sh
util_func = script.read().replace(
'#MAGISK_VERSION_STUB',
f'MAGISK_VER="{config["version"]}"\n'
f'MAGISK_VER_CODE={config["versionCode"]}')
target = op.join('assets', 'util_functions.sh')
vprint(f'zip: {source} -> {target}')
zipf.writestr(target, script.read())
zipf.writestr(target, util_func)
# chromeos
for tool in ['futility', 'kernel_data_key.vbprivk', 'kernel.keyblock']:
@ -522,7 +429,7 @@ def zip_uninstaller(args):
source = op.join('tools', tool)
else:
source = op.join('tools', 'keys', tool)
target = op.join('chromeos', tool)
target = op.join('assets', 'chromeos', tool)
zip_with_msg(zipf, source, target)
# End of zipping
@ -597,9 +504,8 @@ def setup_ndk(args):
def build_all(args):
vars(args)['target'] = []
build_stub(args)
build_app(args)
build_binary(args)
zip_main(args)
build_app(args)
zip_uninstaller(args)
@ -614,7 +520,7 @@ parser.add_argument('-c', '--config', default='config.prop',
subparsers = parser.add_subparsers(title='actions')
all_parser = subparsers.add_parser(
'all', help='build binaries, apks, zips')
'all', help='build everything')
all_parser.set_defaults(func=build_all)
binary_parser = subparsers.add_parser('binary', help='build binaries')
@ -636,10 +542,6 @@ snet_parser = subparsers.add_parser(
'snet', help='build snet extension')
snet_parser.set_defaults(func=build_snet)
zip_parser = subparsers.add_parser(
'zip', help='zip Magisk into a flashable zip')
zip_parser.set_defaults(func=zip_main)
un_parser = subparsers.add_parser(
'uninstaller', help='create flashable uninstaller')
un_parser.set_defaults(func=zip_uninstaller)

View File

@ -18,12 +18,13 @@ object Config {
}
fun contains(key: String) = get(key) != null
val appVersion: String get() = get("appVersion") ?: commitHash
val appVersionCode: Int get() = commitCount
val version: String = get("version") ?: commitHash
val versionCode: Int get() = get("magisk.versionCode")!!.toInt()
}
class MagiskPlugin : Plugin<Project> {
override fun apply(project: Project) {
project.rootProject.file("gradle.properties").inputStream().use { props.load(it) }
val configPath: String? by project
val config = configPath?.let { File(it) } ?: project.rootProject.file("config.prop")
if (config.exists())

View File

@ -6,9 +6,6 @@
# The version name of Magisk. Default: git HEAD short SHA1
version=string
# The version name of Magisk Manager. Default: git HEAD short SHA1
appVersion=string
# Output path. Default: out
outdir=string

View File

@ -1,8 +1,7 @@
#!/system/bin/sh
###########################################################################################
#
#######################################################################################
# Magisk Boot Image Patcher
# by topjohnwu
#######################################################################################
#
# Usage: boot_patch.sh <bootimage>
#
@ -14,16 +13,17 @@
# File name Type Description
#
# boot_patch.sh script A script to patch boot image for Magisk.
# (this file) The script will use binaries and files in its same directory
# to complete the patching process
# util_functions.sh script A script which hosts all functions required for this script
# to work properly
# magiskinit binary The binary to replace /init; magisk binary embedded
# (this file) The script will use files in its same
# directory to complete the patching process
# util_functions.sh script A script which hosts all functions required
# for this script to work properly
# magiskinit binary The binary to replace /init
# magisk(32/64) binary The magisk binaries
# magiskboot binary A tool to manipulate boot images
# chromeos folder This folder includes all the utilities and keys to sign
# (optional) chromeos boot images. Currently only used for Pixel C
# chromeos folder This folder includes the utility and keys to sign
# (optional) chromeos boot images. Only used for Pixel C.
#
###########################################################################################
#######################################################################################
############
# Functions
@ -50,7 +50,7 @@ getdir() {
if [ -z $SOURCEDMODE ]; then
# Switch to the location of the script file
cd "`getdir "${BASH_SOURCE:-$0}"`"
cd "$(getdir "${BASH_SOURCE:-$0}")"
# Load utility functions
. ./util_functions.sh
# Check if 64-bit
@ -106,14 +106,14 @@ fi
case $((STATUS & 3)) in
0 ) # Stock boot
ui_print "- Stock boot image detected"
SHA1=`./magiskboot sha1 "$BOOTIMAGE" 2>/dev/null`
SHA1=$(./magiskboot sha1 "$BOOTIMAGE" 2>/dev/null)
cat $BOOTIMAGE > stock_boot.img
cp -af ramdisk.cpio ramdisk.cpio.orig 2>/dev/null
;;
1 ) # Magisk patched
ui_print "- Magisk patched boot image detected"
# Find SHA1 of stock boot image
[ -z $SHA1 ] && SHA1=`./magiskboot cpio ramdisk.cpio sha1 2>/dev/null`
[ -z $SHA1 ] && SHA1=$(./magiskboot cpio ramdisk.cpio sha1 2>/dev/null)
./magiskboot cpio ramdisk.cpio restore
cp -af ramdisk.cpio ramdisk.cpio.orig
;;

View File

@ -1,31 +1,26 @@
#MAGISK
############################################
#
# Magisk Flash Script (updater-script)
# by topjohnwu
#
############################################
##############
# Preparation
##############
COMMONDIR=$INSTALLER/common
APK=$COMMONDIR/magisk.apk
CHROMEDIR=$INSTALLER/chromeos
# Default permissions
umask 022
OUTFD=$2
ZIP=$3
APK="$3"
COMMONDIR=$INSTALLER/assets
CHROMEDIR=$INSTALLER/assets/chromeos
if [ ! -f $COMMONDIR/util_functions.sh ]; then
echo "! Unable to extract zip file!"
exit 1
fi
# Load utility fuctions
# Load utility functions
. $COMMONDIR/util_functions.sh
setup_flashable
@ -57,7 +52,11 @@ api_level_arch_detect
ui_print "- Device platform: $ARCH"
BINDIR=$INSTALLER/$ARCH32
BINDIR=$INSTALLER/lib/$ARCH32
[ ! -d "$BINDIR" ] && BINDIR=$INSTALLER/lib/armeabi-v7a
cd $BINDIR
for file in lib*.so; do mv "$file" "${file:3:${#file}-6}"; done
cd /
chmod -R 755 $CHROMEDIR $BINDIR
# Check if system root is installed and remove
@ -72,24 +71,22 @@ ui_print "- Constructing environment"
# Copy required files
rm -rf $MAGISKBIN/* 2>/dev/null
mkdir -p $MAGISKBIN 2>/dev/null
cp -af $BINDIR/. $COMMONDIR/. $CHROMEDIR $BBBIN $MAGISKBIN
cp -af $BINDIR/. $COMMONDIR/. $BBBIN $MAGISKBIN
chmod -R 755 $MAGISKBIN
# addon.d
if [ -d /system/addon.d ]; then
ui_print "- Adding addon.d survival script"
blockdev --setrw /dev/block/mapper/system$SLOT 2>/dev/null
mount -o rw,remount /system
mount -o rw,remount /system || mount -o rw,remount /
ADDOND=/system/addon.d/99-magisk.sh
cp -af $COMMONDIR/addon.d.sh $ADDOND
chmod 755 $ADDOND
fi
$BOOTMODE || recovery_actions
#####################
# Boot/DTBO Patching
#####################
##################
# Image Patching
##################
install_magisk

View File

@ -1,72 +1,75 @@
#MAGISK
############################################
#
# Magisk Uninstaller
# by topjohnwu
#
# Magisk Uninstaller (updater-script)
############################################
##############
# Preparation
##############
# This path should work in any cases
TMPDIR=/dev/tmp
INSTALLER=$TMPDIR/install
CHROMEDIR=$INSTALLER/chromeos
# Default permissions
umask 022
OUTFD=$2
ZIP=$3
APK="$3"
COMMONDIR=$INSTALLER/assets
CHROMEDIR=$INSTALLER/assets/chromeos
if [ ! -f $INSTALLER/util_functions.sh ]; then
if [ ! -f $COMMONDIR/util_functions.sh ]; then
echo "! Unable to extract zip file!"
exit 1
fi
# Load utility functions
. $INSTALLER/util_functions.sh
. $COMMONDIR/util_functions.sh
setup_flashable
print_title "Magisk Uninstaller"
############
# Detection
############
if echo $MAGISK_VER | grep -q '\.'; then
PRETTY_VER=$MAGISK_VER
else
PRETTY_VER="$MAGISK_VER($MAGISK_VER_CODE)"
fi
print_title "Magisk $PRETTY_VER Uninstaller"
is_mounted /data || mount /data || abort "! Unable to mount /data, please uninstall with Magisk Manager"
mount_partitions
check_data
$DATA_DE || abort "! Cannot access /data, please uninstall with Magisk Manager"
if ! $BOOTMODE; then
# Mounting stuffs in recovery (best effort)
mount_name metadata /metadata
mount_name "cache cac" /cache
mount_name persist /persist
fi
mount_partitions
get_flags
find_boot_image
[ -z $BOOTIMAGE ] && abort "! Unable to detect target image"
ui_print "- Target image: $BOOTIMAGE"
# Detect version and architecture
api_level_arch_detect
ui_print "- Device platform: $ARCH"
MAGISKBIN=$INSTALLER/$ARCH32
mv $CHROMEDIR $MAGISKBIN
chmod -R 755 $MAGISKBIN
check_data
$DATA_DE || abort "! Cannot access /data, please uninstall with Magisk Manager"
$BOOTMODE || recovery_actions
run_migrations
BINDIR=$INSTALLER/lib/$ARCH32
[ ! -d "$BINDIR" ] && BINDIR=$INSTALLER/lib/armeabi-v7a
cd $BINDIR
for file in lib*.so; do mv "$file" "${file:3:${#file}-6}"; done
cd /
chmod -R 755 $CHROMEDIR $BINDIR
cp -af $CHROMEDIR/. $BINDIR/chromeos
############
# Uninstall
############
get_flags
find_boot_image
[ -e $BOOTIMAGE ] || abort "! Unable to detect boot image"
ui_print "- Found target image: $BOOTIMAGE"
[ -z $DTBOIMAGE ] || ui_print "- Found dtbo image: $DTBOIMAGE"
cd $MAGISKBIN
cd $BINDIR
CHROMEOS=false
@ -108,14 +111,14 @@ case $((STATUS & 3)) in
1 ) # Magisk patched
ui_print "- Magisk patched image detected"
# Find SHA1 of stock boot image
SHA1=`./magiskboot cpio ramdisk.cpio sha1 2>/dev/null`
SHA1=$(./magiskboot cpio ramdisk.cpio sha1 2>/dev/null)
BACKUPDIR=/data/magisk_backup_$SHA1
if [ -d $BACKUPDIR ]; then
ui_print "- Restoring stock boot image"
flash_image $BACKUPDIR/boot.img.gz $BOOTIMAGE
for name in dtb dtbo dtbs; do
[ -f $BACKUPDIR/${name}.img.gz ] || continue
IMAGE=`find_block $name$SLOT`
IMAGE=$(find_block $name$SLOT)
[ -z $IMAGE ] && continue
ui_print "- Restoring stock $name image"
flash_image $BACKUPDIR/${name}.img.gz $IMAGE
@ -124,7 +127,7 @@ case $((STATUS & 3)) in
ui_print "! Boot image backup unavailable"
ui_print "- Restoring ramdisk with internal backup"
./magiskboot cpio ramdisk.cpio restore
if ! ./magiskboot cpio ramdisk.cpio "exists init.rc"; then
if ! ./magiskboot cpio ramdisk.cpio "exists init"; then
# A only system-as-root
rm -f ramdisk.cpio
fi
@ -148,10 +151,11 @@ rm -rf \
/data/adb/post-fs-data.d /data/adb/service.d /data/adb/modules* \
/data/unencrypted/magisk /metadata/magisk /persist/magisk /mnt/vendor/persist/magisk
if [ -f /system/addon.d/99-magisk.sh ]; then
ADDOND=/system/addon.d/99-magisk.sh
if [ -f $ADDOND ]; then
blockdev --setrw /dev/block/mapper/system$SLOT 2>/dev/null
mount -o rw,remount /system || mount -o rw,remount /
rm -f /system/addon.d/99-magisk.sh
rm -f $ADDOND
fi
cd /
@ -163,7 +167,10 @@ if $BOOTMODE; then
ui_print "********************************************"
(sleep 8; /system/bin/reboot)&
else
rm -rf /data/data/*magisk* /data/user*/*/*magisk* /data/app/*magisk* /data/app/*/*magisk*
ui_print "********************************************"
ui_print " Magisk Manager will not be uninstalled"
ui_print " Please uninstall it manually after reboot"
ui_print "********************************************"
recovery_cleanup
ui_print "- Done"
fi

View File

@ -1,37 +1,18 @@
#!/sbin/sh
X86_CNT=__X86_CNT__
extract_bb() {
touch "$BBBIN"
chmod 755 "$BBBIN"
dd if="$0" of="$BBBIN" bs=1024 skip=1 count=$X86_CNT
"$BBBIN" >/dev/null 2>&1 || dd if="$0" of="$BBBIN" bs=1024 skip=$(($X86_CNT + 1))
}
setup_bb() {
mkdir -p $TMPDIR 2>/dev/null
BBBIN=$TMPDIR/busybox
extract_bb
}
export BBBIN
case "$1" in
"extract"|"-x")
BBBIN=./busybox
[ -z "$2" ] || BBBIN="$2"
extract_bb
;;
"sh")
TMPDIR=.
setup_bb
shift
exec ./busybox sh -o standalone "$@"
;;
*)
TMPDIR=/dev/tmp
rm -rf $TMPDIR 2>/dev/null
setup_bb
export INSTALLER=$TMPDIR/install
$BBBIN mkdir -p $INSTALLER
$BBBIN unzip -o "$3" -d $INSTALLER >&2
exec $BBBIN sh -o standalone $INSTALLER/META-INF/com/google/android/updater-script "$@"
;;
esac
exit
TMPDIR=/dev/tmp
rm -rf $TMPDIR
mkdir -p $TMPDIR 2>/dev/null
export BBBIN=$TMPDIR/busybox
unzip -o "$3" lib/x86/libbusybox.so lib/armeabi-v7a/libbusybox.so -d $TMPDIR >&2
chmod -R 755 $TMPDIR/lib
mv -f $TMPDIR/lib/x86/libbusybox.so $BBBIN
$BBBIN >/dev/null 2>&1 || mv -f $TMPDIR/lib/armeabi-v7a/libbusybox.so $BBBIN
$BBBIN rm -rf $TMPDIR/lib
export INSTALLER=$TMPDIR/install
$BBBIN mkdir -p $INSTALLER
$BBBIN unzip -o "$3" "assets/*" "lib/*" "META-INF/com/google/*" -x "lib/*/libbusybox.so" -d $INSTALLER >&2
export ASH_STANDALONE=1
exec $BBBIN sh "$INSTALLER/META-INF/com/google/android/updater-script" "$@"

View File

@ -1,8 +1,5 @@
############################################
#
# Magisk General Utility Functions
# by topjohnwu
#
############################################
#MAGISK_VERSION_STUB
@ -33,7 +30,7 @@ grep_prop() {
shift
local FILES=$@
[ -z "$FILES" ] && FILES='/system/build.prop'
cat $FILES | dos2unix | sed -n "$REGEX" 2>/dev/null | head -n 1
cat $FILES 2>/dev/null | dos2unix | sed -n "$REGEX" | head -n 1
}
getvar() {
@ -42,11 +39,11 @@ getvar() {
local PROPPATH='/data/.magisk /cache/.magisk'
[ -n $MAGISKTMP ] && PROPPATH="$MAGISKTMP/config $PROPPATH"
VALUE=$(grep_prop $VARNAME $PROPPATH)
[ ! -z $VALUE ] && eval $VARNAME=\$VALUE
[ -n $VALUE ] && eval $VARNAME=\$VALUE
}
is_mounted() {
grep -q " `readlink -f $1` " /proc/mounts 2>/dev/null
grep -q " $(readlink -f $1) " /proc/mounts 2>/dev/null
return $?
}
@ -96,6 +93,7 @@ setup_flashable() {
fi
done
fi
recovery_actions
}
ensure_bb() {
@ -309,51 +307,57 @@ mount_apex() {
$BOOTMODE || [ ! -d /system/apex ] && return
local APEX DEST
setup_mntpoint /apex
mount -t tmpfs tmpfs /apex -o mode=755
local PATTERN='s/.*"name":[^"]*"\([^"]*\).*/\1/p'
for APEX in /system/apex/*; do
DEST=/apex/$(basename $APEX .apex)
[ "$DEST" == /apex/com.android.runtime.release ] && DEST=/apex/com.android.runtime
mkdir -p $DEST 2>/dev/null
if [ -f $APEX ]; then
# APEX APKs, extract and loop mount
unzip -qo $APEX apex_payload.img -d /apex
loop_setup apex_payload.img
DEST=/apex/$(unzip -qp $APEX apex_manifest.pb | strings | head -n 1)
[ -z $DEST ] && DEST=/apex/$(unzip -qp $APEX apex_manifest.json | sed -n $PATTERN)
[ -z $DEST ] && continue
mkdir -p $DEST
loop_setup /apex/apex_payload.img
if [ ! -z $LOOPDEV ]; then
ui_print "- Mounting $DEST"
mount -t ext4 -o ro,noatime $LOOPDEV $DEST
fi
rm -f apex_payload.img
rm -f /apex/apex_payload.img
elif [ -d $APEX ]; then
# APEX folders, bind mount directory
if [ -f $APEX/apex_manifest.json ]; then
DEST=/apex/$(sed -n $PATTERN $APEX/apex_manifest.json)
elif [ -f $APEX/apex_manifest.pb ]; then
DEST=/apex/$(strings apex_manifest.pb | head -n 1)
else
continue
fi
mkdir -p $DEST
ui_print "- Mounting $DEST"
mount -o bind $APEX $DEST
fi
done
export ANDROID_RUNTIME_ROOT=/apex/com.android.runtime
export ANDROID_TZDATA_ROOT=/apex/com.android.tzdata
local APEXRJPATH=/apex/com.android.runtime/javalib
local SYSFRAME=/system/framework
export BOOTCLASSPATH=\
$APEXRJPATH/core-oj.jar:$APEXRJPATH/core-libart.jar:$APEXRJPATH/okhttp.jar:\
$APEXRJPATH/bouncycastle.jar:$APEXRJPATH/apache-xml.jar:$SYSFRAME/framework.jar:\
$SYSFRAME/ext.jar:$SYSFRAME/telephony-common.jar:$SYSFRAME/voip-common.jar:\
$SYSFRAME/ims-common.jar:$SYSFRAME/android.test.base.jar:$SYSFRAME/telephony-ext.jar:\
/apex/com.android.conscrypt/javalib/conscrypt.jar:\
/apex/com.android.media/javalib/updatable-media.jar
export ANDROID_ART_ROOT=/apex/com.android.art
export ANDROID_I18N_ROOT=/apex/com.android.i18n
local APEXJARS=$(find /apex -name '*.jar' | sort | tr '\n' ':')
local FWK=/system/framework
export BOOTCLASSPATH=${APEXJARS}\
$FWK/framework.jar:$FWK/ext.jar:$FWK/telephony-common.jar:\
$FWK/voip-common.jar:$FWK/ims-common.jar:$FWK/telephony-ext.jar
}
umount_apex() {
[ -d /apex ] || return
local DEST SRC
for DEST in /apex/*; do
[ "$DEST" = '/apex/*' ] && break
SRC=$(grep $DEST /proc/mounts | awk '{ print $1 }')
umount -l $DEST
# Detach loop device just in case
losetup -d $SRC 2>/dev/null
umount -l /apex
for loop in /dev/block/loop*; do
losetup -d $loop 2>/dev/null
done
rm -rf /apex
unset ANDROID_RUNTIME_ROOT
unset ANDROID_TZDATA_ROOT
unset ANDROID_ART_ROOT
unset ANDROID_I18N_ROOT
unset BOOTCLASSPATH
}
@ -389,7 +393,7 @@ find_boot_image() {
BOOTIMAGE=
if $RECOVERYMODE; then
BOOTIMAGE=`find_block recovery_ramdisk$SLOT recovery$SLOT sos`
elif [ ! -z $SLOT ]; then
elif [ -n $SLOT ]; then
BOOTIMAGE=`find_block ramdisk$SLOT recovery_ramdisk$SLOT boot$SLOT`
else
BOOTIMAGE=`find_block ramdisk recovery_ramdisk kern-a android_boot kernel bootimg boot lnx boot_a`
@ -402,7 +406,7 @@ find_boot_image() {
flash_image() {
case "$1" in
*.gz) CMD1="$MAGISKBIN/magiskboot decompress '$1' - 2>/dev/null";;
*.gz) CMD1="gzip -d < '$1' 2>/dev/null";;
*) CMD1="cat '$1'";;
esac
if $BOOTSIGNED; then

View File

@ -3,12 +3,12 @@ plugins {
}
android {
val canary = !Config.appVersion.contains(".")
val canary = !Config.version.contains(".")
defaultConfig {
applicationId = "com.topjohnwu.magisk"
versionCode = 1
versionName = Config.appVersion
versionName = Config.version
buildConfigField("int", "STUB_VERSION", "15")
buildConfigField("String", "DEV_CHANNEL", Config["DEV_CHANNEL"] ?: "null")
buildConfigField("boolean", "CANARY", if (canary) "true" else "false")