diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index 9b20a00ba..598a5df9d 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -158,6 +158,7 @@ dependencies { implementation(libs.androidx.navigation3.runtime) implementation(libs.androidx.lifecycle.viewmodel.navigation3) implementation(libs.androidx.navigationevent) + testImplementation(libs.junit) } aboutLibraries { diff --git a/android/app/src/main/java/me/kavishdevar/librepods/utils/RootlessSupport.kt b/android/app/src/main/java/me/kavishdevar/librepods/utils/RootlessSupport.kt index 57741a81c..f7cbc7350 100644 --- a/android/app/src/main/java/me/kavishdevar/librepods/utils/RootlessSupport.kt +++ b/android/app/src/main/java/me/kavishdevar/librepods/utils/RootlessSupport.kt @@ -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) } } diff --git a/android/app/src/test/java/me/kavishdevar/librepods/utils/RootlessSupportTest.kt b/android/app/src/test/java/me/kavishdevar/librepods/utils/RootlessSupportTest.kt new file mode 100644 index 000000000..3a0b30d30 --- /dev/null +++ b/android/app/src/test/java/me/kavishdevar/librepods/utils/RootlessSupportTest.kt @@ -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 . +*/ + +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" + } +} diff --git a/android/gradle/libs.versions.toml b/android/gradle/libs.versions.toml index 0999d3957..f6a177176 100644 --- a/android/gradle/libs.versions.toml +++ b/android/gradle/libs.versions.toml @@ -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" @@ -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" }