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
1 change: 1 addition & 0 deletions android/app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,7 @@ dependencies {
implementation(libs.androidx.navigation3.runtime)
implementation(libs.androidx.lifecycle.viewmodel.navigation3)
implementation(libs.androidx.navigationevent)
testImplementation(libs.junit)
}

aboutLibraries {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,23 +22,64 @@ import android.content.SharedPreferences
import android.os.Build
import androidx.core.content.edit

internal const val ANDROID_16_SDK = 36
internal const val ANDROID_17_SDK = 37

private const val BYPASS_DEVICE_CHECK_KEY = "bypass_device_check.v2"
internal const val GOOGLE_MANUFACTURER = "google"
internal const val ONEPLUS_MANUFACTURER = "oneplus"
internal const val OPPO_MANUFACTURER = "oppo"
internal const val REALME_MANUFACTURER = "realme"
internal const val XIAOMI_MANUFACTURER = "xiaomi"
internal const val POCO_MANUFACTURER = "poco"
internal const val REDMI_MANUFACTURER = "redmi"
private const val PIXEL_ANDROID_16_SUPPORTED_BUILD_PREFIX = "CP1A"

private val OPPO_FAMILY_MANUFACTURERS = setOf(
ONEPLUS_MANUFACTURER,
OPPO_MANUFACTURER,
REALME_MANUFACTURER,
)
private val XIAOMI_FAMILY_MANUFACTURERS = setOf(
XIAOMI_MANUFACTURER,
POCO_MANUFACTURER,
REDMI_MANUFACTURER,
)

fun isSupported(sharedPreferences: SharedPreferences): Boolean {
if (Build.VERSION.SDK_INT >= 37) return true
val isBypassFlagActive = sharedPreferences.getBoolean(BYPASS_DEVICE_CHECK_KEY, false)

val isBypassFlagActive = sharedPreferences.getBoolean("bypass_device_check.v2", false)
return isRootlessSupported(
sdkInt = Build.VERSION.SDK_INT,
manufacturer = Build.MANUFACTURER,
buildId = Build.ID,
isBypassFlagActive = isBypassFlagActive,
)
}

fun isRootlessSupported(
sdkInt: Int,
manufacturer: String,
buildId: String,
isBypassFlagActive: Boolean,
): Boolean {
if (sdkInt >= ANDROID_17_SDK) return true
if (isBypassFlagActive) return true

val isPixel = Build.MANUFACTURER.lowercase() == "google"
val isOppoFamily = Build.MANUFACTURER.lowercase() in listOf("oneplus", "oppo", "realme")
val normalizedManufacturer = manufacturer.lowercase()
val isPixel = normalizedManufacturer == GOOGLE_MANUFACTURER
val isOppoFamily = normalizedManufacturer in OPPO_FAMILY_MANUFACTURERS
val isXiaomiFamily = normalizedManufacturer in XIAOMI_FAMILY_MANUFACTURERS

if (isPixel && Build.VERSION.SDK_INT == 36) {
return Build.ID.startsWith("CP1A")
} else if (isOppoFamily) {
return Build.VERSION.SDK_INT >= 36
if (isPixel && sdkInt == ANDROID_16_SDK) {
return buildId.startsWith(PIXEL_ANDROID_16_SUPPORTED_BUILD_PREFIX)
} else if (isOppoFamily || isXiaomiFamily) {
return sdkInt >= ANDROID_16_SDK
}

return false
}

fun bypassDeviceCheck(sharedPreferences: SharedPreferences) {
sharedPreferences.edit{ putBoolean("bypass_device_check.v2", true) }
sharedPreferences.edit{ putBoolean(BYPASS_DEVICE_CHECK_KEY, true) }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/*
LibrePods - AirPods liberated from Apple's ecosystem
Copyright (C) 2025 LibrePods contributors

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/

package me.kavishdevar.librepods.utils

import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test

class RootlessSupportTest {
@Test
fun android16XiaomiFamilyDevicesAreSupported() {
assertTrue(isRootlessSupported(ANDROID_16_SDK, XIAOMI_MANUFACTURER, GENERIC_ANDROID_16_BUILD_ID, false))
assertTrue(isRootlessSupported(ANDROID_16_SDK, POCO_MANUFACTURER, GENERIC_ANDROID_16_BUILD_ID, false))
assertTrue(isRootlessSupported(ANDROID_16_SDK, REDMI_MANUFACTURER, GENERIC_ANDROID_16_BUILD_ID, false))
}

@Test
fun android16OppoFamilyDevicesRemainSupported() {
assertTrue(isRootlessSupported(ANDROID_16_SDK, ONEPLUS_MANUFACTURER, GENERIC_ANDROID_16_BUILD_ID, false))
assertTrue(isRootlessSupported(ANDROID_16_SDK, OPPO_MANUFACTURER, GENERIC_ANDROID_16_BUILD_ID, false))
assertTrue(isRootlessSupported(ANDROID_16_SDK, REALME_MANUFACTURER, GENERIC_ANDROID_16_BUILD_ID, false))
}

@Test
fun pixelAndroid16RequiresMarchUpdateBuildPrefix() {
assertTrue(isRootlessSupported(ANDROID_16_SDK, GOOGLE_MANUFACTURER, SUPPORTED_PIXEL_ANDROID_16_BUILD_ID, false))
assertFalse(isRootlessSupported(ANDROID_16_SDK, GOOGLE_MANUFACTURER, GENERIC_ANDROID_16_BUILD_ID, false))
}

@Test
fun bypassFlagSupportsOtherwiseUnsupportedDevices() {
assertTrue(isRootlessSupported(ANDROID_16_SDK, UNSUPPORTED_MANUFACTURER, GENERIC_ANDROID_16_BUILD_ID, true))
}

@Test
fun android17AndLaterAreSupported() {
assertTrue(isRootlessSupported(ANDROID_17_SDK, UNSUPPORTED_MANUFACTURER, GENERIC_ANDROID_16_BUILD_ID, false))
}

private companion object {
private const val GENERIC_ANDROID_16_BUILD_ID = "BP2A.250605.031"
private const val SUPPORTED_PIXEL_ANDROID_16_BUILD_ID = "CP1A.250305.019"
private const val UNSUPPORTED_MANUFACTURER = "samsung"
}
}
2 changes: 2 additions & 0 deletions android/gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ hilt = "2.59.2"
xposed = "101.0.0"
lifecycleProcess = "2.10.0"
play = "2.0.2"
junit = "4.13.2"
nav3Core = "1.1.2"
lifecycleViewmodelNav3 = "2.11.0-rc01"
navevent = "1.1.1"
Expand Down Expand Up @@ -58,6 +59,7 @@ libxposed-service = { group = "io.github.libxposed", name = "service", version.r
androidx-lifecycle-process = { group = "androidx.lifecycle", name = "lifecycle-process", version.ref = "lifecycleProcess" }
play-review = { group = "com.google.android.play", name="review", version.ref = "play" }
play-review-ktx = { group = "com.google.android.play", name="review-ktx", version.ref = "play" }
junit = { group = "junit", name = "junit", version.ref = "junit" }
androidx-navigation3-runtime = { module = "androidx.navigation3:navigation3-runtime", version.ref = "nav3Core" }
androidx-navigation3-ui = { module = "androidx.navigation3:navigation3-ui", version.ref = "nav3Core" }
androidx-lifecycle-viewmodel-navigation3 = { module = "androidx.lifecycle:lifecycle-viewmodel-navigation3", version.ref = "lifecycleViewmodelNav3" }
Expand Down