From 4c484489ab6d3f444b5e4978eecd096540d32c11 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 29 Apr 2026 21:10:40 +0000 Subject: [PATCH] detect Magisk and surface provider on root status Replaces RootChecker's Boolean? return with a RootResult sealed type that carries the root provider (Magisk / Other / Unknown) and version when available. Magisk is detected via a combination of: - PackageManager probe for com.topjohnwu.magisk(.debug/.canary/.alpha) (gated by a new block in AndroidManifest) - /proc/self/mounts inspection (root-free) - magisk -V via libsu when root is granted - /data/adb/magisk and friends as a fallback When root is granted by Magisk, MainScreen now shows a secondary "via Magisk vX.Y" line under the existing status text. Other root providers fall back to the generic ROOTED display. Closes the "Magisk root check" TODO from the README. https://claude.ai/code/session_01NxY1tfQZxw6qwi41XkCWKa --- app/src/main/AndroidManifest.xml | 7 ++ .../basicrootchecker/analytics/Analytics.kt | 11 +++ .../basicrootchecker/data/RootChecker.kt | 82 ++++++++++++++++++- .../basicrootchecker/ui/main/MainScreen.kt | 26 ++++++ .../basicrootchecker/ui/main/MainViewModel.kt | 33 ++++++-- app/src/main/res/values/strings.xml | 4 + 6 files changed, 154 insertions(+), 9 deletions(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 5327a1df..6982f8f6 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -6,6 +6,13 @@ + + + + + + + { + val version = queryMagiskVersion() + val isMagisk = version != null || packageHit || mountHit || probeMagiskFiles() + RootResult.Rooted( + provider = if (isMagisk) RootProvider.MAGISK else RootProvider.OTHER, + version = version, + ) + } + + false -> RootResult.NotRooted + null -> RootResult.Unknown + } delay(1000) result } + + private fun probeMagiskPackage(context: Context): Boolean { + val pm = context.packageManager + return MAGISK_PACKAGES.any { name -> + try { + pm.getPackageInfo(name, 0) + true + } catch (_: PackageManager.NameNotFoundException) { + false + } + } + } + + private fun probeMagiskMounts(): Boolean = try { + File("/proc/self/mounts").readText().contains("magisk", ignoreCase = true) + } catch (_: Exception) { + false + } + + private fun probeMagiskFiles(): Boolean { + val result = Shell.cmd( + "test -d /data/adb/magisk || test -d /sbin/.magisk || test -d /data/adb/modules" + ).exec() + return result.isSuccess + } + + private fun queryMagiskVersion(): String? { + val nameResult = Shell.cmd("magisk -V").exec() + if (nameResult.isSuccess) { + val name = nameResult.out.firstOrNull()?.trim().orEmpty() + if (name.isNotEmpty()) return name + } + val codeResult = Shell.cmd("magisk -vV").exec() + if (codeResult.isSuccess) { + val raw = codeResult.out.firstOrNull()?.trim().orEmpty() + val code = raw.substringBefore(':').toLongOrNull() + if (code != null) { + val major = code / 1000 + val minor = (code % 1000) / 100 + return "$major.$minor" + } + } + return null + } } diff --git a/app/src/main/java/com/iboalali/basicrootchecker/ui/main/MainScreen.kt b/app/src/main/java/com/iboalali/basicrootchecker/ui/main/MainScreen.kt index 648a4ac8..a49099a2 100644 --- a/app/src/main/java/com/iboalali/basicrootchecker/ui/main/MainScreen.kt +++ b/app/src/main/java/com/iboalali/basicrootchecker/ui/main/MainScreen.kt @@ -74,6 +74,7 @@ import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel import com.iboalali.basicrootchecker.R +import com.iboalali.basicrootchecker.data.RootProvider import com.iboalali.basicrootchecker.ui.theme.BasicRootCheckerTheme import com.iboalali.basicrootchecker.update.AppUpdateEvent import com.iboalali.basicrootchecker.util.PreviewLocales @@ -319,6 +320,29 @@ fun MainScreenContent( textAlign = TextAlign.Center, ) } + + if (uiState.rootStatus == RootStatus.ROOTED && + uiState.rootProvider != RootProvider.UNKNOWN + ) { + val providerName = when (uiState.rootProvider) { + RootProvider.MAGISK -> stringResource(R.string.root_provider_magisk) + RootProvider.OTHER -> stringResource(R.string.root_provider_other) + RootProvider.UNKNOWN -> "" + } + val version = uiState.rootProviderVersion + val providerText = if (version != null) { + stringResource(R.string.root_provider_via_with_version, providerName, version) + } else { + stringResource(R.string.root_provider_via, providerName) + } + Spacer(Modifier.height(4.dp)) + Text( + text = providerText, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + textAlign = TextAlign.Center, + ) + } } } @@ -490,6 +514,8 @@ private fun MainScreenRootedPreview() { MainScreenContent( uiState = MainUiState( rootStatus = RootStatus.ROOTED, + rootProvider = RootProvider.MAGISK, + rootProviderVersion = "27.0", deviceMarketingName = "Pixel 8 Pro", deviceModelName = "husky", androidVersion = "Android 16", diff --git a/app/src/main/java/com/iboalali/basicrootchecker/ui/main/MainViewModel.kt b/app/src/main/java/com/iboalali/basicrootchecker/ui/main/MainViewModel.kt index 4b0f2672..948e5645 100644 --- a/app/src/main/java/com/iboalali/basicrootchecker/ui/main/MainViewModel.kt +++ b/app/src/main/java/com/iboalali/basicrootchecker/ui/main/MainViewModel.kt @@ -9,6 +9,8 @@ import com.iboalali.basicrootchecker.BuildConfig import com.iboalali.basicrootchecker.R import com.iboalali.basicrootchecker.analytics.Analytics import com.iboalali.basicrootchecker.data.RootChecker +import com.iboalali.basicrootchecker.data.RootProvider +import com.iboalali.basicrootchecker.data.RootResult import com.iboalali.basicrootchecker.data.UserPreferences import com.iboalali.basicrootchecker.update.AppUpdateEvent import com.iboalali.basicrootchecker.util.DeviceInfo @@ -30,6 +32,8 @@ enum class RootStatus { data class MainUiState( val rootStatus: RootStatus = RootStatus.NOT_CHECKED, + val rootProvider: RootProvider = RootProvider.UNKNOWN, + val rootProviderVersion: String? = null, val deviceMarketingName: String = "", val deviceModelName: String = "", val androidVersion: String = "", @@ -87,16 +91,31 @@ class MainViewModel(application: Application) : AndroidViewModel(application) { fun checkRoot() { viewModelScope.launch { - _uiState.update { it.copy(rootStatus = RootStatus.CHECKING) } + _uiState.update { + it.copy( + rootStatus = RootStatus.CHECKING, + rootProvider = RootProvider.UNKNOWN, + rootProviderVersion = null, + ) + } Analytics.trackRootCheckStarted() - val result = RootChecker.checkRoot() - val status = when (result) { - true -> RootStatus.ROOTED - false -> RootStatus.NOT_ROOTED - null -> RootStatus.UNKNOWN + val result = RootChecker.check(getApplication()) + val (status, provider, version) = when (result) { + is RootResult.Rooted -> Triple(RootStatus.ROOTED, result.provider, result.version) + RootResult.NotRooted -> Triple(RootStatus.NOT_ROOTED, RootProvider.UNKNOWN, null) + RootResult.Unknown -> Triple(RootStatus.UNKNOWN, RootProvider.UNKNOWN, null) + } + _uiState.update { + it.copy( + rootStatus = status, + rootProvider = provider, + rootProviderVersion = version, + ) } - _uiState.update { it.copy(rootStatus = status) } Analytics.trackRootCheckResult(status.name) + if (status == RootStatus.ROOTED) { + Analytics.trackRootProvider(provider.name, version) + } } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 09dfeb57..453e602d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -7,6 +7,10 @@ Your Device has Root access Couldn\'t get Root status of your device Your Device doesn\'t have Root access + Magisk + Other + via %1$s + via %1$s v%2$s Licenses About Settings