diff --git a/app/src/main/java/com/topjohnwu/magisk/App.kt b/app/src/main/java/com/topjohnwu/magisk/App.kt index 40bab9705..1c6e14a5b 100644 --- a/app/src/main/java/com/topjohnwu/magisk/App.kt +++ b/app/src/main/java/com/topjohnwu/magisk/App.kt @@ -41,12 +41,10 @@ open class App : Application() { protectedContext = baseContext self = this - deContext = base if (Build.VERSION.SDK_INT >= 24) { protectedContext = base.createDeviceProtectedStorageContext() - deContext = protectedContext - deContext.moveSharedPreferencesFrom(base, base.defaultPrefsName) + protectedContext.moveSharedPreferencesFrom(base, base.defaultPrefsName) } registerActivityLifecycleCallbacks(get()) @@ -69,11 +67,6 @@ open class App : Application() { @JvmStatic lateinit var self: App - @SuppressLint("StaticFieldLeak") - @Deprecated("Use dependency injection; replace with protectedContext") - @JvmStatic - lateinit var deContext: Context - @Deprecated("Use Rx or similar") @JvmField var THREAD_POOL: ThreadPoolExecutor diff --git a/app/src/main/java/com/topjohnwu/magisk/tasks/MagiskInstaller.java b/app/src/main/java/com/topjohnwu/magisk/tasks/MagiskInstaller.java deleted file mode 100644 index 630c1feb5..000000000 --- a/app/src/main/java/com/topjohnwu/magisk/tasks/MagiskInstaller.java +++ /dev/null @@ -1,351 +0,0 @@ -package com.topjohnwu.magisk.tasks; - -import android.net.Uri; -import android.os.Build; -import android.text.TextUtils; - -import androidx.annotation.MainThread; -import androidx.annotation.WorkerThread; - -import com.topjohnwu.magisk.App; -import com.topjohnwu.magisk.Const; -import com.topjohnwu.magisk.Info; -import com.topjohnwu.magisk.utils.Utils; -import com.topjohnwu.net.Networking; -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.internal.UiThreadHandler; -import com.topjohnwu.superuser.io.SuFile; -import com.topjohnwu.superuser.io.SuFileInputStream; -import com.topjohnwu.superuser.io.SuFileOutputStream; - -import org.kamranzafar.jtar.TarEntry; -import org.kamranzafar.jtar.TarHeader; -import org.kamranzafar.jtar.TarInputStream; -import org.kamranzafar.jtar.TarOutputStream; - -import java.io.BufferedInputStream; -import java.io.BufferedOutputStream; -import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.nio.ByteBuffer; -import java.util.Arrays; -import java.util.List; -import java.util.zip.ZipEntry; -import java.util.zip.ZipInputStream; - -public abstract class MagiskInstaller { - - protected String srcBoot; - protected File destFile; - protected File installDir; - protected Uri zipUri; - - private final List console; - private final List logs; - private boolean isTar = false; - - protected MagiskInstaller() { - console = NOPList.getInstance(); - logs = NOPList.getInstance(); - } - - public MagiskInstaller(Uri zip, List out, List err) { - console = out; - logs = err; - zipUri = zip; - installDir = new File(App.deContext.getFilesDir().getParent(), "install"); - Shell.sh("rm -rf " + installDir).exec(); - installDir.mkdirs(); - } - - protected boolean findImage() { - srcBoot = ShellUtils.fastCmd("find_boot_image", "echo \"$BOOTIMAGE\""); - if (srcBoot.isEmpty()) { - console.add("! Unable to detect target image"); - return false; - } - console.add("- Target image: " + srcBoot); - return true; - } - - protected boolean findSecondaryImage() { - String slot = ShellUtils.fastCmd("echo $SLOT"); - String target = TextUtils.equals(slot, "_a") ? "_b" : "_a"; - console.add("- Target slot: " + target); - srcBoot = ShellUtils.fastCmd( - "SLOT=" + target, - "find_boot_image", - "SLOT=" + slot, - "echo \"$BOOTIMAGE\"" - ); - if (srcBoot.isEmpty()) { - console.add("! Unable to detect target image"); - return false; - } - console.add("- Target image: " + srcBoot); - return true; - } - - protected boolean extractZip() { - String arch; - if (Build.VERSION.SDK_INT >= 21) { - List abis = Arrays.asList(Build.SUPPORTED_ABIS); - arch = abis.contains("x86") ? "x86" : "arm"; - } else { - arch = TextUtils.equals(Build.CPU_ABI, "x86") ? "x86" : "arm"; - } - - console.add("- Device platform: " + Build.CPU_ABI); - - try { - ZipInputStream zi = new ZipInputStream(new BufferedInputStream( - App.self.getContentResolver().openInputStream(zipUri))); - ZipEntry ze; - while ((ze = zi.getNextEntry()) != null) { - if (ze.isDirectory()) - continue; - String name = null; - String[] names = { arch + "/", "common/", "META-INF/com/google/android/update-binary" }; - for (String n : names) { - if (ze.getName().startsWith(n)) { - name = ze.getName().substring(ze.getName().lastIndexOf('/') + 1); - break; - } - } - if (name == null && ze.getName().startsWith("chromeos/")) - name = ze.getName(); - if (name == null) - continue; - File dest = installDir instanceof SuFile ? - new SuFile(installDir, name) : - new File(installDir, name); - dest.getParentFile().mkdirs(); - try (OutputStream out = new SuFileOutputStream(dest)) { - ShellUtils.pump(zi, out); - } - } - } catch (IOException e) { - console.add("! Cannot unzip zip"); - return false; - } - - File init64 = SuFile.open(installDir, "magiskinit64"); - if (Build.VERSION.SDK_INT >= 21 && Build.SUPPORTED_64_BIT_ABIS.length != 0) { - init64.renameTo(SuFile.open(installDir, "magiskinit")); - } else { - init64.delete(); - } - Shell.sh("cd " + installDir, "chmod 755 *").exec(); - return true; - } - - private TarEntry newEntry(String name, long size) { - console.add("-- Writing: " + name); - return new TarEntry(TarHeader.createHeader(name, size, 0, false, 0644)); - } - - private void handleTar(InputStream in) throws IOException { - console.add("- Processing tar file"); - boolean vbmeta = false; - try (TarInputStream tarIn = new TarInputStream(in); - TarOutputStream tarOut = new TarOutputStream(destFile)) { - TarEntry entry; - while ((entry = tarIn.getNextEntry()) != null) { - if (entry.getName().contains("boot.img") - || entry.getName().contains("recovery.img")) { - String name = entry.getName(); - console.add("-- Extracting: " + name); - File extract = new File(installDir, name); - try (FileOutputStream fout = new FileOutputStream(extract)) { - ShellUtils.pump(tarIn, fout); - } - if (name.contains(".lz4")) { - console.add("-- Decompressing: " + name); - Shell.sh("./magiskboot --decompress " + extract).to(console).exec(); - } - } else if (entry.getName().contains("vbmeta.img")) { - vbmeta = true; - ByteBuffer buf = ByteBuffer.allocate(256); - buf.put("AVB0".getBytes()); // magic - buf.putInt(1); // required_libavb_version_major - buf.putInt(120, 2); // flags - buf.position(128); // release_string - buf.put("avbtool 1.1.0".getBytes()); - tarOut.putNextEntry(newEntry("vbmeta.img", 256)); - tarOut.write(buf.array()); - } else { - console.add("-- Writing: " + entry.getName()); - tarOut.putNextEntry(entry); - ShellUtils.pump(tarIn, tarOut); - } - } - File boot = SuFile.open(installDir, "boot.img"); - File recovery = SuFile.open(installDir, "recovery.img"); - if (vbmeta && recovery.exists() && boot.exists()) { - // Install Magisk to recovery - srcBoot = recovery.getPath(); - // Repack boot image to prevent restore - Shell.sh( - "./magiskboot --unpack boot.img", - "./magiskboot --repack boot.img", - "./magiskboot --cleanup", - "mv new-boot.img boot.img").exec(); - try (InputStream sin = new SuFileInputStream(boot)) { - tarOut.putNextEntry(newEntry("boot.img", boot.length())); - ShellUtils.pump(sin, tarOut); - } - boot.delete(); - } else { - if (!boot.exists()) { - console.add("! No boot image found"); - throw new IOException(); - } - srcBoot = boot.getPath(); - } - } - } - - protected boolean handleFile(Uri uri) { - try (InputStream in = new BufferedInputStream(App.self.getContentResolver().openInputStream(uri))) { - in.mark(500); - byte[] magic = new byte[5]; - if (in.skip(257) != 257 || in.read(magic) != magic.length) { - console.add("! Invalid file"); - return false; - } - in.reset(); - if (Arrays.equals(magic, "ustar".getBytes())) { - isTar = true; - destFile = new File(Const.EXTERNAL_PATH, "magisk_patched.tar"); - handleTar(in); - } else { - // Raw image - srcBoot = new File(installDir, "boot.img").getPath(); - destFile = new File(Const.EXTERNAL_PATH, "magisk_patched.img"); - console.add("- Copying image to cache"); - try (OutputStream out = new FileOutputStream(srcBoot)) { - ShellUtils.pump(in, out); - } - } - } catch (IOException e) { - console.add("! Process error"); - e.printStackTrace(); - return false; - } - return true; - } - - protected boolean patchBoot() { - boolean isSigned; - try (InputStream in = new SuFileInputStream(srcBoot)) { - isSigned = SignBoot.verifySignature(in, null); - if (isSigned) { - console.add("- Boot image is signed with AVB 1.0"); - } - } catch (IOException e) { - console.add("! Unable to check signature"); - return false; - } - - if (!Shell.sh(Utils.INSTANCE.fmt( - "KEEPFORCEENCRYPT=%b KEEPVERITY=%b RECOVERYMODE=%b " + - "sh update-binary sh boot_patch.sh %s", - Info.keepEnc, Info.keepVerity, Info.recovery, srcBoot)) - .to(console, logs).exec().isSuccess()) - return false; - - Shell.Job job = Shell.sh("./magiskboot --cleanup", - "mv bin/busybox busybox", - "rm -rf magisk.apk bin boot.img update-binary", - "cd /"); - - File patched = new File(installDir, "new-boot.img"); - if (isSigned) { - console.add("- Signing boot image with test keys"); - File signed = new File(installDir, "signed.img"); - try (InputStream in = new SuFileInputStream(patched); - OutputStream out = new BufferedOutputStream(new FileOutputStream(signed))) { - SignBoot.doSignature("/boot", in, out, null, null); - } catch (IOException e) { - return false; - } - job.add("mv -f " + signed + " " + patched); - } - job.exec(); - return true; - } - - protected boolean flashBoot() { - if (!Shell.su(Utils.INSTANCE.fmt("direct_install %s %s", installDir, srcBoot)) - .to(console, logs).exec().isSuccess()) - return false; - if (!Info.keepVerity) - Shell.su("patch_dtbo_image").to(console, logs).exec(); - return true; - } - - protected boolean storeBoot() { - File patched = SuFile.open(installDir, "new-boot.img"); - try { - OutputStream os; - if (isTar) { - os = new TarOutputStream(destFile, true); - ((TarOutputStream) os).putNextEntry(newEntry( - srcBoot.contains("recovery") ? "recovery.img" : "boot.img", - patched.length())); - } else { - os = new BufferedOutputStream(new FileOutputStream(destFile)); - } - try (InputStream in = new SuFileInputStream(patched); - OutputStream out = os) { - ShellUtils.pump(in, out); - } - } catch (IOException e) { - console.add("! Failed to output to " + destFile); - e.printStackTrace(); - } - patched.delete(); - console.add(""); - console.add("****************************"); - console.add(" Output file is placed in "); - console.add(" " + destFile + " "); - console.add("****************************"); - return true; - } - - protected boolean postOTA() { - SuFile bootctl = new SuFile("/data/adb/bootctl"); - try (InputStream in = Networking.get(Const.Url.BOOTCTL_URL).execForInputStream().getResult(); - OutputStream out = new SuFileOutputStream(bootctl)) { - ShellUtils.pump(in, out); - } catch (IOException e) { - e.printStackTrace(); - return false; - } - Shell.su("post_ota " + bootctl.getParent()).exec(); - console.add("***************************************"); - console.add(" Next reboot will boot to second slot!"); - console.add("***************************************"); - return true; - } - - @WorkerThread - protected abstract boolean operations(); - - @MainThread - protected abstract void onResult(boolean success); - - public void exec() { - App.THREAD_POOL.execute(() -> { - boolean b = operations(); - UiThreadHandler.run(() -> onResult(b)); - }); - } - -} diff --git a/app/src/main/java/com/topjohnwu/magisk/tasks/MagiskInstaller.kt b/app/src/main/java/com/topjohnwu/magisk/tasks/MagiskInstaller.kt new file mode 100644 index 000000000..9ef1c8d72 --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/tasks/MagiskInstaller.kt @@ -0,0 +1,356 @@ +package com.topjohnwu.magisk.tasks + +import android.content.Context +import android.net.Uri +import android.os.Build +import android.text.TextUtils +import androidx.annotation.MainThread +import androidx.annotation.WorkerThread +import com.skoumal.teanity.extensions.subscribeK +import com.topjohnwu.magisk.Config +import com.topjohnwu.magisk.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.* +import java.nio.ByteBuffer +import java.util.* +import java.util.zip.ZipEntry +import java.util.zip.ZipInputStream + +abstract class MagiskInstaller { + + protected lateinit var srcBoot: String + protected lateinit var destFile: File + protected lateinit var installDir: File + protected lateinit var zipUri: Uri + + private val console: MutableList + private val logs: MutableList + private var isTar = false + + 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() + } + + protected 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 + } + + protected 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 + } + + protected 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 + withStreams(TarInputStream(input), TarOutputStream(destFile)) { tarIn, tarOut -> + 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 + } + } + } + + protected 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 (Arrays.equals(magic, "ustar".toByteArray())) { + isTar = true + 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 + } + + protected 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 test 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 + } + + protected fun flashBoot(): Boolean { + if (!"direct_install $installDir $srcBoot".sh().isSuccess) + return false + if (!Info.keepVerity) + "patch_dtbo_image".sh() + return true + } + + protected fun storeBoot(): Boolean { + val patched = SuFile.open(installDir, "new-boot.img") + try { + val os: OutputStream + if (isTar) { + os = TarOutputStream(destFile, true) + os.putNextEntry(newEntry( + if (srcBoot.contains("recovery")) "recovery.img" else "boot.img", + patched.length())) + } else { + os = 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 + } + + protected 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) + + @WorkerThread + protected abstract fun operations(): Boolean + + @MainThread + protected abstract fun onResult(success: Boolean) + + fun exec() { + Single.fromCallable { operations() }.subscribeK { onResult(it) } + } + +}