diff --git a/app/apk/src/main/java/com/topjohnwu/magisk/ui/settings/SettingsViewModel.kt b/app/apk/src/main/java/com/topjohnwu/magisk/ui/settings/SettingsViewModel.kt index b4768c75418c..10180cb3ae88 100644 --- a/app/apk/src/main/java/com/topjohnwu/magisk/ui/settings/SettingsViewModel.kt +++ b/app/apk/src/main/java/com/topjohnwu/magisk/ui/settings/SettingsViewModel.kt @@ -92,7 +92,6 @@ class SettingsViewModel : BaseViewModel(), BaseSettingsItem.Handler { DownloadPath -> withExternalRW(doAction) UpdateChecker -> withPostNotificationPermission(doAction) Authentication -> AuthEvent(doAction).publish() - Hide, Restore -> withInstallPermission(doAction) else -> doAction() } } diff --git a/app/core/proguard-rules.pro b/app/core/proguard-rules.pro index 4a12a8234a90..2d6cf76a3283 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 391d4798e38a..000000000000 --- 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/data/magiskdb/PolicyDao.kt b/app/core/src/main/java/com/topjohnwu/magisk/core/data/magiskdb/PolicyDao.kt index a424c5d6c3f0..1ed417fab7b1 100644 --- a/app/core/src/main/java/com/topjohnwu/magisk/core/data/magiskdb/PolicyDao.kt +++ b/app/core/src/main/java/com/topjohnwu/magisk/core/data/magiskdb/PolicyDao.kt @@ -20,7 +20,7 @@ class PolicyDao : MagiskDB() { } suspend fun fetch(uid: Int): SuPolicy? { - val query = "SELECT * FROM ${Table.POLICY} WHERE uid == $uid LIMIT = 1" + val query = "SELECT * FROM ${Table.POLICY} WHERE uid == $uid LIMIT 1" return exec(query, ::toPolicy).firstOrNull() } 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 61dc8545aea5..b13d2273328a 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 d3cd5bd3c718..6ae2c183e5d6 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 28e53e222a23..8c78f5b12a5d 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 @@ -13,7 +14,6 @@ import com.topjohnwu.magisk.core.Config import com.topjohnwu.magisk.core.Const import com.topjohnwu.magisk.core.R import com.topjohnwu.magisk.core.ktx.await -import com.topjohnwu.magisk.core.ktx.copyAndClose import com.topjohnwu.magisk.core.ktx.toast import com.topjohnwu.magisk.core.ktx.writeTo import com.topjohnwu.magisk.core.signing.JarMap @@ -23,11 +23,9 @@ import com.topjohnwu.magisk.core.utils.Keygen import com.topjohnwu.magisk.utils.APKInstall import com.topjohnwu.superuser.Shell import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Runnable 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 @@ -38,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 @@ -133,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) } @@ -161,52 +154,87 @@ 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, onFailure: Runnable): 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) { + } - // Install and auto launch app - val session = APKInstall.startSession(activity, pkg, onFailure) { - Config.suManager = pkg - Shell.cmd("touch $AppApkPath").exec() - launchApp(activity, pkg) + 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) return true - - try { - repack.inputStream().copyAndClose(session.openStream(activity)) - } catch (e: IOException) { - Timber.e(e) + if (Shell.cmd(cmd).exec().isSuccess) { + Config.suManager = pkg + Shell.cmd("touch $AppApkPath").exec() + launchApp(context, pkg) + return true + } else { return false } - session.waitIntent()?.let { activity.startActivity(it) } ?: return false - return true } @Suppress("DEPRECATION") @@ -217,14 +245,25 @@ object AppMigration { setCancelable(false) show() } - val onFailure = Runnable { + val success = withContext(Dispatchers.IO) { + patchAndHide(activity, label) + } + if (!success) { dialog.dismiss() activity.toast(R.string.failure, Toast.LENGTH_LONG) } - val success = withContext(Dispatchers.IO) { - patchAndHide(activity, label, onFailure) + } + + 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 } - if (!success) onFailure.run() + return false } @Suppress("DEPRECATION") @@ -235,30 +274,10 @@ object AppMigration { setCancelable(false) show() } - val onFailure = Runnable { - dialog.dismiss() + if (!restoreApp(activity)) { activity.toast(R.string.failure, Toast.LENGTH_LONG) } - val apk = StubApk.current(activity) - val session = APKInstall.startSession(activity, APP_PACKAGE_NAME, onFailure) { - Config.suManager = "" - Shell.cmd("touch $AppApkPath").exec() - launchApp(activity, APP_PACKAGE_NAME) - dialog.dismiss() - } - val cmd = "adb_pm_install $apk $APP_PACKAGE_NAME" - if (Shell.cmd(cmd).await().isSuccess) return - val success = withContext(Dispatchers.IO) { - try { - apk.inputStream().copyAndClose(session.openStream(activity)) - } catch (e: IOException) { - Timber.e(e) - return@withContext false - } - session.waitIntent()?.let { activity.startActivity(it) } ?: return@withContext false - return@withContext true - } - if (!success) onFailure.run() + dialog.dismiss() } suspend fun upgradeStub(context: Context, apk: File): Intent? { 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 e86bb1a8a4ce..f45a8f9fc13e 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 000000000000..6f304c564159 --- /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 000000000000..61d23e1c9801 --- /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 1aa5a3938c59..791fa186eb0b 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 @@ 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 9f8b98755aed..46e3bba77d91 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 62159dbe769f..000000000000 --- 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 109978e51447..000000000000 --- 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 000000000000..c92080a96c14 --- /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 eabec298e302..cc020c9dcc46 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 95a93bdeb0f0..b98377dc3905 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' }