Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,13 @@

<uses-sdk tools:overrideLibrary="androidx.core.splashscreen" />

<queries>
<package android:name="com.topjohnwu.magisk" />
<package android:name="com.topjohnwu.magisk.debug" />
<package android:name="com.topjohnwu.magisk.canary" />
<package android:name="com.topjohnwu.magisk.alpha" />
</queries>

<application
android:name=".BasicRootCheckerApplication"
android:allowBackup="false"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,17 @@ object Analytics {
)
}

fun trackRootProvider(provider: String, version: String?) {
if (!enabled) return
TelemetryDeck.signal(
"rootProviderDetected",
mapOf(
"provider" to provider,
"version" to (version ?: ""),
),
)
}

fun trackUpdateAvailable() {
if (!enabled) return
TelemetryDeck.signal("updateAvailable")
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,93 @@
package com.iboalali.basicrootchecker.data

import android.content.Context
import android.content.pm.PackageManager
import com.topjohnwu.superuser.Shell
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.withContext
import java.io.File

sealed interface RootResult {
data object NotRooted : RootResult
data object Unknown : RootResult
data class Rooted(val provider: RootProvider, val version: String?) : RootResult
}

enum class RootProvider { MAGISK, OTHER, UNKNOWN }

object RootChecker {

suspend fun checkRoot(): Boolean? = withContext(Dispatchers.IO) {
val result = Shell.isAppGrantedRoot()
private val MAGISK_PACKAGES = listOf(
"com.topjohnwu.magisk",
"com.topjohnwu.magisk.debug",
"com.topjohnwu.magisk.canary",
"com.topjohnwu.magisk.alpha",
)

suspend fun check(context: Context): RootResult = withContext(Dispatchers.IO) {
val granted = Shell.isAppGrantedRoot()
val packageHit = probeMagiskPackage(context)
val mountHit = probeMagiskMounts()

val result = when (granted) {
true -> {
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
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
)
}
}
}

Expand Down Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 = "",
Expand Down Expand Up @@ -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)
}
}
}

Expand Down
4 changes: 4 additions & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@
<string name="rootAvailable">Your Device has Root access</string>
<string name="rootUnknown">Couldn\'t get Root status of your device</string>
<string name="rootNotAvailable">Your Device doesn\'t have Root access</string>
<string name="root_provider_magisk" translatable="false">Magisk</string>
<string name="root_provider_other">Other</string>
<string name="root_provider_via">via %1$s</string>
<string name="root_provider_via_with_version">via %1$s v%2$s</string>
<string name="action_licence">Licenses</string>
<string name="action_about">About</string>
<string name="action_settings">Settings</string>
Expand Down