From 5885b8c20d555dbde3c3d3c9c605fcb438a906a6 Mon Sep 17 00:00:00 2001 From: topjohnwu Date: Tue, 17 Dec 2024 22:11:01 -0800 Subject: [PATCH] Add new tests for app hiding --- app/core/proguard-rules.pro | 11 +- .../com/topjohnwu/magisk/core/TestImpl.kt | 50 -------- .../magisk/core/model/su/SuPolicy.kt | 13 +- .../magisk/core/su/SuRequestHandler.kt | 2 +- .../magisk/core/tasks/AppMigration.kt | 112 ++++++++++++------ .../com/topjohnwu/magisk/core/utils/AXML.kt | 6 +- .../com/topjohnwu/magisk/test/Environment.kt | 87 ++++++++++++++ .../topjohnwu/magisk/test/MagiskAppTest.kt | 31 +++++ .../magisk/utils/DynamicClassLoader.java | 2 +- app/test/src/main/AndroidManifest.xml | 2 +- .../com/topjohnwu/magisk/test/Environment.kt | 29 ----- .../topjohnwu/magisk/test/MagiskAppTest.kt | 24 ---- .../com/topjohnwu/magisk/test/TestRunner.kt | 18 +++ native/src/core/su/su_daemon.cpp | 2 +- scripts/test_common.sh | 50 ++++++-- 15 files changed, 271 insertions(+), 168 deletions(-) delete mode 100644 app/core/src/main/java/com/topjohnwu/magisk/core/TestImpl.kt create mode 100644 app/core/src/main/java/com/topjohnwu/magisk/test/Environment.kt create mode 100644 app/core/src/main/java/com/topjohnwu/magisk/test/MagiskAppTest.kt delete mode 100644 app/test/src/main/java/com/topjohnwu/magisk/test/Environment.kt delete mode 100644 app/test/src/main/java/com/topjohnwu/magisk/test/MagiskAppTest.kt create mode 100644 app/test/src/main/java/com/topjohnwu/magisk/test/TestRunner.kt diff --git a/app/core/proguard-rules.pro b/app/core/proguard-rules.pro index 4a12a8234..2d6cf76a3 100644 --- a/app/core/proguard-rules.pro +++ b/app/core/proguard-rules.pro @@ -37,13 +37,4 @@ -flattenpackagehierarchy -allowaccessmodification --dontwarn org.junit.Assert --dontwarn org.bouncycastle.jsse.BCSSLParameters --dontwarn org.bouncycastle.jsse.BCSSLSocket --dontwarn org.bouncycastle.jsse.provider.BouncyCastleJsseProvider --dontwarn org.commonmark.ext.gfm.strikethrough.Strikethrough --dontwarn org.conscrypt.Conscrypt* --dontwarn org.conscrypt.ConscryptHostnameVerifier --dontwarn org.openjsse.javax.net.ssl.SSLParameters --dontwarn org.openjsse.javax.net.ssl.SSLSocket --dontwarn org.openjsse.net.ssl.OpenJSSE +-dontwarn org.junit.** diff --git a/app/core/src/main/java/com/topjohnwu/magisk/core/TestImpl.kt b/app/core/src/main/java/com/topjohnwu/magisk/core/TestImpl.kt deleted file mode 100644 index 391d4798e..000000000 --- a/app/core/src/main/java/com/topjohnwu/magisk/core/TestImpl.kt +++ /dev/null @@ -1,50 +0,0 @@ -package com.topjohnwu.magisk.core - -import androidx.annotation.Keep -import com.topjohnwu.magisk.core.di.ServiceLocator -import com.topjohnwu.magisk.core.tasks.MagiskInstaller -import com.topjohnwu.magisk.core.utils.RootUtils -import com.topjohnwu.superuser.CallbackList -import com.topjohnwu.superuser.Shell -import kotlinx.coroutines.runBlocking -import org.junit.Assert.assertTrue -import timber.log.Timber - -/** - * We implement all test logic here and mark it with @Keep so that our instrumentation package - * can properly run tests on fully obfuscated release APKs. - */ -@Keep -object TestImpl { - - fun before() { - assertTrue("Should have root access", Shell.getShell().isRoot) - // Make sure the root service is running - RootUtils.Connection.await() - } - - object LogList : CallbackList(Runnable::run) { - override fun onAddElement(e: String) { - Timber.i(e) - } - } - - fun setupMagisk() { - runBlocking { - MagiskInstaller.Emulator(LogList, LogList).exec() - } - } - - fun setupShellGrantTest() { - // Clear existing grant for ADB shell - runBlocking { - ServiceLocator.policyDB.delete(2000) - Config.suAutoResponse = Config.Value.SU_AUTO_ALLOW - Config.prefs.edit().commit() - } - } - - fun testZygisk() { - assertTrue("Zygisk should be enabled", Info.isZygiskEnabled) - } -} diff --git a/app/core/src/main/java/com/topjohnwu/magisk/core/model/su/SuPolicy.kt b/app/core/src/main/java/com/topjohnwu/magisk/core/model/su/SuPolicy.kt index 61dc8545a..b13d22733 100644 --- a/app/core/src/main/java/com/topjohnwu/magisk/core/model/su/SuPolicy.kt +++ b/app/core/src/main/java/com/topjohnwu/magisk/core/model/su/SuPolicy.kt @@ -1,17 +1,18 @@ package com.topjohnwu.magisk.core.model.su -class SuPolicy(val uid: Int) { +class SuPolicy( + val uid: Int, + var policy: Int = INTERACTIVE, + var until: Long = -1L, + var logging: Boolean = true, + var notification: Boolean = true, +) { companion object { const val INTERACTIVE = 0 const val DENY = 1 const val ALLOW = 2 } - var policy: Int = INTERACTIVE - var until: Long = -1L - var logging: Boolean = true - var notification: Boolean = true - fun toMap(): MutableMap = mutableMapOf( "uid" to uid, "policy" to policy, diff --git a/app/core/src/main/java/com/topjohnwu/magisk/core/su/SuRequestHandler.kt b/app/core/src/main/java/com/topjohnwu/magisk/core/su/SuRequestHandler.kt index d3cd5bd3c..6ae2c183e 100644 --- a/app/core/src/main/java/com/topjohnwu/magisk/core/su/SuRequestHandler.kt +++ b/app/core/src/main/java/com/topjohnwu/magisk/core/su/SuRequestHandler.kt @@ -62,7 +62,7 @@ class SuRequestHandler( return false } output = File(fifo) - policy = SuPolicy(uid) + policy = policyDB.fetch(uid) ?: SuPolicy(uid) try { pkgInfo = pm.getPackageInfo(uid, pid) ?: PackageInfo().apply { val name = pm.getNameForUid(uid) ?: throw PackageManager.NameNotFoundException() diff --git a/app/core/src/main/java/com/topjohnwu/magisk/core/tasks/AppMigration.kt b/app/core/src/main/java/com/topjohnwu/magisk/core/tasks/AppMigration.kt index 0eb32df10..8c78f5b12 100644 --- a/app/core/src/main/java/com/topjohnwu/magisk/core/tasks/AppMigration.kt +++ b/app/core/src/main/java/com/topjohnwu/magisk/core/tasks/AppMigration.kt @@ -4,6 +4,7 @@ import android.app.Activity import android.app.ActivityOptions import android.content.Context import android.content.Intent +import android.content.pm.PackageManager import android.os.Build import android.widget.Toast import com.topjohnwu.magisk.StubApk @@ -25,7 +26,6 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import timber.log.Timber import java.io.File -import java.io.FileOutputStream import java.io.IOException import java.io.OutputStream import java.security.SecureRandom @@ -36,6 +36,7 @@ object AppMigration { private const val ALPHA = "abcdefghijklmnopqrstuvwxyz" private const val ALPHADOTS = "$ALPHA....." private const val ANDROID_MANIFEST = "AndroidManifest.xml" + private const val TEST_PKG_NAME = "$APP_PACKAGE_NAME.test" // Some arbitrary limit const val MAX_LABEL_LENGTH = 32 @@ -131,21 +132,15 @@ object AppMigration { val je = jar.getJarEntry(ANDROID_MANIFEST) val xml = AXML(jar.getRawData(je)) val generator = classNameGenerator() - - if (!xml.patchStrings { - for (i in it.indices) { - val s = it[i] - if (s.contains(APP_PACKAGE_NAME)) { - it[i] = s.replace(APP_PACKAGE_NAME, pkg) - } else if (s.contains(PLACEHOLDER)) { - it[i] = generator.next() - } else if (s == origLabel) { - it[i] = label.toString() - } + val p = xml.patchStrings { + when { + it.contains(APP_PACKAGE_NAME) -> it.replace(APP_PACKAGE_NAME, pkg) + it.contains(PLACEHOLDER) -> generator.next() + it == origLabel -> label.toString() + else -> it } - }) { - return false } + if (!p) return false // Write apk changes jar.getOutputStream(je).use { it.write(xml.bytes) } @@ -159,40 +154,83 @@ object AppMigration { } } - private fun launchApp(activity: Activity, pkg: String) { - val intent = activity.packageManager.getLaunchIntentForPackage(pkg) ?: return + private fun patchTest(apk: File, out: File, pkg: String): Boolean { + try { + JarMap.open(apk, true).use { jar -> + val je = jar.getJarEntry(ANDROID_MANIFEST) + val xml = AXML(jar.getRawData(je)) + val p = xml.patchStrings { + when (it) { + APP_PACKAGE_NAME -> pkg + TEST_PKG_NAME -> "$pkg.test" + else -> it + } + } + if (!p) return false + + // Write apk changes + jar.getOutputStream(je).use { it.write(xml.bytes) } + val keys = Keygen() + out.outputStream().use { SignApk.sign(keys.cert, keys.key, jar, it) } + return true + } + } catch (e: Exception) { + Timber.e(e) + return false + } + } + + private fun launchApp(context: Context, pkg: String) { + val intent = context.packageManager.getLaunchIntentForPackage(pkg) ?: return intent.putExtra(Const.Key.PREV_CONFIG, Config.toBundle()) val options = ActivityOptions.makeBasic() if (Build.VERSION.SDK_INT >= 34) { options.setShareIdentityEnabled(true) } - activity.startActivity(intent, options.toBundle()) - activity.finish() + context.startActivity(intent, options.toBundle()) + if (context is Activity) { + context.finish() + } } - private suspend fun patchAndHide(activity: Activity, label: String): Boolean { - val stub = File(activity.cacheDir, "stub.apk") + suspend fun patchAndHide(context: Context, label: String, pkg: String? = null): Boolean { + val stub = File(context.cacheDir, "stub.apk") try { - activity.assets.open("stub.apk").writeTo(stub) + context.assets.open("stub.apk").writeTo(stub) } catch (e: IOException) { Timber.e(e) return false } - // Generate a new random package name and signature - val repack = File(activity.cacheDir, "patched.apk") - val pkg = genPackageName() + // Generate a new random signature and package name if needed + val pkg = pkg ?: genPackageName() Config.keyStoreRaw = "" - if (!patch(activity, stub, FileOutputStream(repack), pkg, label)) - return false + // Check and patch the test APK + try { + val info = context.packageManager.getApplicationInfo(TEST_PKG_NAME, 0) + val testApk = File(info.sourceDir) + val testRepack = File(context.cacheDir, "test.apk") + if (!patchTest(testApk, testRepack, pkg)) + return false + val cmd = "adb_pm_install $testRepack $pkg.test" + if (!Shell.cmd(cmd).exec().isSuccess) + return false + } catch (e: PackageManager.NameNotFoundException) { + } + + val repack = File(context.cacheDir, "patched.apk") + repack.outputStream().use { + if (!patch(context, stub, it, pkg, label)) + return false + } // Install and auto launch app val cmd = "adb_pm_install $repack $pkg" if (Shell.cmd(cmd).exec().isSuccess) { Config.suManager = pkg Shell.cmd("touch $AppApkPath").exec() - launchApp(activity, pkg) + launchApp(context, pkg) return true } else { return false @@ -216,6 +254,18 @@ object AppMigration { } } + suspend fun restoreApp(context: Context): Boolean { + val apk = StubApk.current(context) + val cmd = "adb_pm_install $apk $APP_PACKAGE_NAME" + if (Shell.cmd(cmd).await().isSuccess) { + Config.suManager = "" + Shell.cmd("touch $AppApkPath").exec() + launchApp(context, APP_PACKAGE_NAME) + return true + } + return false + } + @Suppress("DEPRECATION") suspend fun restore(activity: Activity) { val dialog = android.app.ProgressDialog(activity).apply { @@ -224,13 +274,7 @@ object AppMigration { setCancelable(false) show() } - val apk = StubApk.current(activity) - val cmd = "adb_pm_install $apk $APP_PACKAGE_NAME" - if (Shell.cmd(cmd).await().isSuccess) { - Config.suManager = "" - Shell.cmd("touch $AppApkPath").exec() - launchApp(activity, APP_PACKAGE_NAME) - } else { + if (!restoreApp(activity)) { activity.toast(R.string.failure, Toast.LENGTH_LONG) } dialog.dismiss() diff --git a/app/core/src/main/java/com/topjohnwu/magisk/core/utils/AXML.kt b/app/core/src/main/java/com/topjohnwu/magisk/core/utils/AXML.kt index e86bb1a8a..f45a8f9fc 100644 --- a/app/core/src/main/java/com/topjohnwu/magisk/core/utils/AXML.kt +++ b/app/core/src/main/java/com/topjohnwu/magisk/core/utils/AXML.kt @@ -29,7 +29,7 @@ class AXML(b: ByteArray) { * Followed by an array of uint32_t with size = number of strings * Each entry points to an offset into the string data */ - fun patchStrings(patchFn: (Array) -> Unit): Boolean { + fun patchStrings(mapFn: (String) -> String): Boolean { val buffer = ByteBuffer.wrap(bytes).order(LITTLE_ENDIAN) fun findStringPool(): Int { @@ -65,7 +65,9 @@ class AXML(b: ByteArray) { } val strArr = strList.toTypedArray() - patchFn(strArr) + for (i in strArr.indices) { + strArr[i] = mapFn(strArr[i]) + } // Write everything before string data, will patch values later val baos = RawByteStream() diff --git a/app/core/src/main/java/com/topjohnwu/magisk/test/Environment.kt b/app/core/src/main/java/com/topjohnwu/magisk/test/Environment.kt new file mode 100644 index 000000000..6f304c564 --- /dev/null +++ b/app/core/src/main/java/com/topjohnwu/magisk/test/Environment.kt @@ -0,0 +1,87 @@ +package com.topjohnwu.magisk.test + +import androidx.annotation.Keep +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import com.topjohnwu.magisk.core.BuildConfig.APP_PACKAGE_NAME +import com.topjohnwu.magisk.core.Config +import com.topjohnwu.magisk.core.di.ServiceLocator +import com.topjohnwu.magisk.core.model.su.SuPolicy +import com.topjohnwu.magisk.core.tasks.AppMigration +import com.topjohnwu.magisk.core.tasks.MagiskInstaller +import com.topjohnwu.superuser.CallbackList +import kotlinx.coroutines.runBlocking +import org.junit.Assert.assertTrue +import org.junit.BeforeClass +import org.junit.Test +import org.junit.runner.RunWith +import timber.log.Timber + +@Keep +@RunWith(AndroidJUnit4::class) +class Environment { + + companion object { + @BeforeClass + @JvmStatic + fun before() = MagiskAppTest.before() + } + + @Test + fun setupMagisk() { + val log = object : CallbackList(Runnable::run) { + override fun onAddElement(e: String) { + Timber.i(e) + } + } + runBlocking { + assertTrue( + "Magisk setup failed", + MagiskInstaller.Emulator(log, log).exec() + ) + } + } + + @Test + fun setupShellGrantTest() { + runBlocking { + // Inject an undetermined + mute logging policy for ADB shell + val policy = SuPolicy( + uid = 2000, + logging = false, + notification = false, + until = 0L + ) + ServiceLocator.policyDB.update(policy) + // Bypass the need to actually show a dialog + Config.suAutoResponse = Config.Value.SU_AUTO_ALLOW + Config.prefs.edit().commit() + } + } + + @Test + fun setupAppHide() { + runBlocking { + assertTrue( + "App hiding failed", + AppMigration.patchAndHide( + context = InstrumentationRegistry.getInstrumentation().targetContext, + label = "Settings", + pkg = "repackaged.$APP_PACKAGE_NAME" + ) + ) + } + } + + @Test + fun setupAppRestore() { + runBlocking { + assertTrue( + "App restoration failed", + AppMigration.restoreApp( + context = InstrumentationRegistry.getInstrumentation().targetContext + ) + ) + } + } +} diff --git a/app/core/src/main/java/com/topjohnwu/magisk/test/MagiskAppTest.kt b/app/core/src/main/java/com/topjohnwu/magisk/test/MagiskAppTest.kt new file mode 100644 index 000000000..61d23e1c9 --- /dev/null +++ b/app/core/src/main/java/com/topjohnwu/magisk/test/MagiskAppTest.kt @@ -0,0 +1,31 @@ +package com.topjohnwu.magisk.test + +import androidx.annotation.Keep +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.topjohnwu.magisk.core.Info +import com.topjohnwu.magisk.core.utils.RootUtils +import com.topjohnwu.superuser.Shell +import org.junit.Assert.assertTrue +import org.junit.BeforeClass +import org.junit.Test +import org.junit.runner.RunWith + +@Keep +@RunWith(AndroidJUnit4::class) +class MagiskAppTest { + + companion object { + @BeforeClass + @JvmStatic + fun before() { + assertTrue("Should have root access", Shell.getShell().isRoot) + // Make sure the root service is running + RootUtils.Connection.await() + } + } + + @Test + fun testZygisk() { + assertTrue("Zygisk should be enabled", Info.isZygiskEnabled) + } +} diff --git a/app/shared/src/main/java/com/topjohnwu/magisk/utils/DynamicClassLoader.java b/app/shared/src/main/java/com/topjohnwu/magisk/utils/DynamicClassLoader.java index 1aa5a3938..791fa186e 100644 --- a/app/shared/src/main/java/com/topjohnwu/magisk/utils/DynamicClassLoader.java +++ b/app/shared/src/main/java/com/topjohnwu/magisk/utils/DynamicClassLoader.java @@ -12,7 +12,7 @@ import dalvik.system.BaseDexClassLoader; public class DynamicClassLoader extends BaseDexClassLoader { public DynamicClassLoader(File apk) { - this(apk, getSystemClassLoader()); + this(apk, DynamicClassLoader.class.getClassLoader()); } public DynamicClassLoader(File apk, ClassLoader parent) { diff --git a/app/test/src/main/AndroidManifest.xml b/app/test/src/main/AndroidManifest.xml index 9f8b98755..46e3bba77 100644 --- a/app/test/src/main/AndroidManifest.xml +++ b/app/test/src/main/AndroidManifest.xml @@ -9,7 +9,7 @@ diff --git a/app/test/src/main/java/com/topjohnwu/magisk/test/Environment.kt b/app/test/src/main/java/com/topjohnwu/magisk/test/Environment.kt deleted file mode 100644 index 62159dbe7..000000000 --- a/app/test/src/main/java/com/topjohnwu/magisk/test/Environment.kt +++ /dev/null @@ -1,29 +0,0 @@ -package com.topjohnwu.magisk.test - -import androidx.test.ext.junit.runners.AndroidJUnit4 -import com.topjohnwu.magisk.core.TestImpl -import org.junit.BeforeClass -import org.junit.Test -import org.junit.runner.RunWith - -@RunWith(AndroidJUnit4::class) -class Environment { - - companion object { - @BeforeClass - @JvmStatic - fun before() { - TestImpl.before() - } - } - - @Test - fun setupMagisk() { - TestImpl.setupMagisk() - } - - @Test - fun setupShellGrantTest() { - TestImpl.setupShellGrantTest() - } -} diff --git a/app/test/src/main/java/com/topjohnwu/magisk/test/MagiskAppTest.kt b/app/test/src/main/java/com/topjohnwu/magisk/test/MagiskAppTest.kt deleted file mode 100644 index 109978e51..000000000 --- a/app/test/src/main/java/com/topjohnwu/magisk/test/MagiskAppTest.kt +++ /dev/null @@ -1,24 +0,0 @@ -package com.topjohnwu.magisk.test - -import androidx.test.ext.junit.runners.AndroidJUnit4 -import com.topjohnwu.magisk.core.TestImpl -import org.junit.BeforeClass -import org.junit.Test -import org.junit.runner.RunWith - -@RunWith(AndroidJUnit4::class) -class MagiskAppTest { - - companion object { - @BeforeClass - @JvmStatic - fun before() { - TestImpl.before() - } - } - - @Test - fun testZygisk() { - TestImpl.testZygisk() - } -} diff --git a/app/test/src/main/java/com/topjohnwu/magisk/test/TestRunner.kt b/app/test/src/main/java/com/topjohnwu/magisk/test/TestRunner.kt new file mode 100644 index 000000000..c92080a96 --- /dev/null +++ b/app/test/src/main/java/com/topjohnwu/magisk/test/TestRunner.kt @@ -0,0 +1,18 @@ +package com.topjohnwu.magisk.test + +import android.os.Bundle +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.runner.AndroidJUnitRunner + +class TestRunner : AndroidJUnitRunner() { + override fun onCreate(arguments: Bundle) { + // Force using the target context's classloader to run tests + arguments.putString("classLoader", TestClassLoader::class.java.name) + super.onCreate(arguments) + } +} + +private val targetClassLoader inline get() = + InstrumentationRegistry.getInstrumentation().targetContext.classLoader + +class TestClassLoader : ClassLoader(targetClassLoader) diff --git a/native/src/core/su/su_daemon.cpp b/native/src/core/su/su_daemon.cpp index eabec298e..cc020c9dc 100644 --- a/native/src/core/su/su_daemon.cpp +++ b/native/src/core/su/su_daemon.cpp @@ -80,7 +80,7 @@ void su_info::check_db() { } // We need to check our manager - if (access.log || access.notify) { + if (access.policy == QUERY || access.log || access.notify) { mgr_uid = get_manager(to_user_id(eval_uid), &mgr_pkg, true); } } diff --git a/scripts/test_common.sh b/scripts/test_common.sh index 95a93bdeb..b98377dc3 100644 --- a/scripts/test_common.sh +++ b/scripts/test_common.sh @@ -7,7 +7,6 @@ export PATH="$PATH:$ANDROID_HOME/platform-tools" emu="$ANDROID_HOME/emulator/emulator" sdk="$ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager" avd="$ANDROID_HOME/cmdline-tools/latest/bin/avdmanager" -test_pkg='com.topjohnwu.magisk.test' boot_timeout=600 @@ -24,14 +23,27 @@ print_error() { echo -e "\n\033[41;39m${1}\033[0m\n" } -run_instrument_tests() { - local out=$(adb shell am instrument -w \ - --user 0 \ - -e class "$1" \ - com.topjohnwu.magisk.test/androidx.test.runner.AndroidJUnitRunner) +# $1 = TestClass#method +# $2: boolean = isRepackaged +run_instrument_test() { + local test_pkg + if [ -n "$2" -a $2 ]; then + test_pkg="repackaged.com.topjohnwu.magisk.test" + else + test_pkg=com.topjohnwu.magisk.test + fi + local out=$(adb shell am instrument -w --user 0 \ + -e class "com.topjohnwu.magisk.test.$1" \ + "$test_pkg/com.topjohnwu.magisk.test.TestRunner") grep -q 'OK (' <<< "$out" } +# $1 = pkg +wait_for_pm() { + sleep 5 + adb shell pm uninstall $1 || true +} + test_setup() { local variant=$1 adb shell 'PATH=$PATH:/debug_ramdisk magisk -v' @@ -43,14 +55,34 @@ test_setup() { adb install -r -g out/test-${variant}.apk # Run setup through the test app - run_instrument_tests "$test_pkg.Environment#setupMagisk" + run_instrument_test 'Environment#setupMagisk' } test_app() { # Run app tests - run_instrument_tests "$test_pkg.MagiskAppTest" + run_instrument_test 'MagiskAppTest' # Test shell su request - run_instrument_tests "$test_pkg.Environment#setupShellGrantTest" + run_instrument_test 'Environment#setupShellGrantTest' adb shell /system/xbin/su 2000 su -c id | tee /dev/fd/2 | grep -q 'uid=0' + adb shell am force-stop com.topjohnwu.magisk + + # Test app hiding + run_instrument_test 'Environment#setupAppHide' + wait_for_pm com.topjohnwu.magisk + + # Make sure it still works + run_instrument_test 'MagiskAppTest' true + + # Test shell su request + run_instrument_test 'Environment#setupShellGrantTest' true + adb shell /system/xbin/su 2000 su -c id | tee /dev/fd/2 | grep -q 'uid=0' + adb shell am force-stop repackaged.com.topjohnwu.magisk + + # Test app restore + run_instrument_test 'Environment#setupAppRestore' true + wait_for_pm repackaged.com.topjohnwu.magisk + + # Make sure it still works + run_instrument_test 'MagiskAppTest' }