From 044aff731fe984e4b1bd74149fd1f91284cb79de Mon Sep 17 00:00:00 2001 From: Kavish Devar Date: Thu, 7 May 2026 20:32:28 +0530 Subject: [PATCH] android: keep only xposed flavor also changed Build.ID check to startsWith("CP1A") --- android/app/build.gradle.kts | 67 +++-- android/app/src/main/cpp/CMakeLists.txt | 60 ++--- .../src/{xposed => main}/cpp/l2c_fcr_hook.cpp | 0 .../src/{xposed => main}/cpp/l2c_fcr_hook.h | 0 android/app/src/{xposed => main}/cpp/xz/xz.h | 0 .../src/{xposed => main}/cpp/xz/xz_config.h | 0 .../src/{xposed => main}/cpp/xz/xz_crc32.c | 0 .../src/{xposed => main}/cpp/xz/xz_crc64.c | 0 .../src/{xposed => main}/cpp/xz/xz_dec_bcj.c | 0 .../{xposed => main}/cpp/xz/xz_dec_lzma2.c | 0 .../{xposed => main}/cpp/xz/xz_dec_stream.c | 0 .../src/{xposed => main}/cpp/xz/xz_lzma2.h | 0 .../src/{xposed => main}/cpp/xz/xz_private.h | 0 .../src/{xposed => main}/cpp/xz/xz_sha256.c | 0 .../src/{xposed => main}/cpp/xz/xz_stream.h | 0 .../librepods/LibrePodsApplication.kt | 1 + .../me/kavishdevar/librepods/MainActivity.kt | 230 +++++------------- .../librepods/data/XposedRemotePrefImpl.kt | 0 .../screens/AirPodsSettingsScreen.kt | 1 - .../librepods/services/AirPodsService.kt | 9 + .../librepods/utils/KotlinModule.kt | 0 .../librepods/utils/RootlessSupport.kt | 6 +- .../librepods/utils/XposedServiceHolder.kt | 0 android/app/src/main/res/values/strings.xml | 6 +- .../resources/META-INF/xposed/java_init.list | 0 .../resources/META-INF/xposed/module.prop | 0 .../META-INF/xposed/native_init.list | 0 .../resources/META-INF/xposed/scope.list | 0 .../librepods/LibrePodsApplication.kt | 21 -- .../librepods/data/XposedRemotePrefImpl.kt | 11 - android/gradle/libs.versions.toml | 13 +- 31 files changed, 138 insertions(+), 287 deletions(-) rename android/app/src/{xposed => main}/cpp/l2c_fcr_hook.cpp (100%) rename android/app/src/{xposed => main}/cpp/l2c_fcr_hook.h (100%) rename android/app/src/{xposed => main}/cpp/xz/xz.h (100%) rename android/app/src/{xposed => main}/cpp/xz/xz_config.h (100%) rename android/app/src/{xposed => main}/cpp/xz/xz_crc32.c (100%) rename android/app/src/{xposed => main}/cpp/xz/xz_crc64.c (100%) rename android/app/src/{xposed => main}/cpp/xz/xz_dec_bcj.c (100%) rename android/app/src/{xposed => main}/cpp/xz/xz_dec_lzma2.c (100%) rename android/app/src/{xposed => main}/cpp/xz/xz_dec_stream.c (100%) rename android/app/src/{xposed => main}/cpp/xz/xz_lzma2.h (100%) rename android/app/src/{xposed => main}/cpp/xz/xz_private.h (100%) rename android/app/src/{xposed => main}/cpp/xz/xz_sha256.c (100%) rename android/app/src/{xposed => main}/cpp/xz/xz_stream.h (100%) rename android/app/src/{xposed => main}/java/me/kavishdevar/librepods/LibrePodsApplication.kt (99%) rename android/app/src/{xposed => main}/java/me/kavishdevar/librepods/data/XposedRemotePrefImpl.kt (100%) rename android/app/src/{xposed => main}/java/me/kavishdevar/librepods/utils/KotlinModule.kt (100%) rename android/app/src/{xposed => main}/java/me/kavishdevar/librepods/utils/XposedServiceHolder.kt (100%) rename android/app/src/{xposed => main}/resources/META-INF/xposed/java_init.list (100%) rename android/app/src/{xposed => main}/resources/META-INF/xposed/module.prop (100%) rename android/app/src/{xposed => main}/resources/META-INF/xposed/native_init.list (100%) rename android/app/src/{xposed => main}/resources/META-INF/xposed/scope.list (100%) delete mode 100644 android/app/src/normal/java/me/kavishdevar/librepods/LibrePodsApplication.kt delete mode 100644 android/app/src/normal/java/me/kavishdevar/librepods/data/XposedRemotePrefImpl.kt diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index e1afd0f..05169f8 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -28,9 +28,8 @@ android { defaultConfig { applicationId = "me.kavishdevar.librepods" - minSdk = 33 targetSdk = 37 - versionCode = 50 + versionCode = 52 versionName = appVersionName } buildTypes { @@ -47,21 +46,29 @@ android { } buildConfigField("Boolean", "PLAY_BUILD", "false") signingConfig = signingConfigs.getByName("release") + defaultConfig { + minSdk = 33 + } } debug { buildConfigField("Boolean", "PLAY_BUILD", "false") signingConfig = signingConfigs.getByName("release") versionNameSuffix = "-debug" + defaultConfig { + minSdk = 33 + } } - create("playRelease") { - initWith(getByName("release")) + } + productFlavors { + create("foss") { + dimension = "env" + buildConfigField("Boolean", "PLAY_BUILD", "false") + } + create("play") { + dimension = "env" buildConfigField("Boolean", "PLAY_BUILD", "true") versionNameSuffix = "-play" - } - create("playDebug") { - initWith(getByName("debug")) - buildConfigField("Boolean", "PLAY_BUILD", "true") - versionNameSuffix = "-youshouldnothavethis" + minSdk = 36 } } compileOptions { @@ -91,25 +98,6 @@ android { ndkVersion = "30.0.14904198" flavorDimensions += "env" - - productFlavors { - create("normal") { - dimension = "env" - externalNativeBuild { - cmake { - arguments += "-DIS_XPOSED=OFF" - } - } - } - create("xposed") { - dimension = "env" - externalNativeBuild { - cmake { - arguments += "-DIS_XPOSED=ON" - } - } - } - } } dependencies { @@ -139,9 +127,10 @@ dependencies { implementation(libs.backdrop) // implementation(libs.hilt) // implementation(libs.hilt.compiler) - add("xposedCompileOnly", libs.libxposed.api) - add("xposedImplementation", libs.libxposed.service) - add("playReleaseImplementation", libs.billing) + compileOnly(libs.libxposed.api) + implementation(libs.libxposed.service) + implementation(libs.play.review) + implementation(libs.play.review.ktx) } aboutLibraries { @@ -184,14 +173,14 @@ fun registerRootModuleZipTask( } val zipRelease = registerRootModuleZipTask( - "zipXposedReleaseModule", - "xposed", + "zipReleaseModule", + "foss", "release" ) val zipDebug = registerRootModuleZipTask( - "zipXposedDebugModule", - "xposed", + "zipDebugModule", + "foss", "debug" ) @@ -200,22 +189,22 @@ val collect = tasks.register("collectReleaseArtifacts") { dependsOn( zipRelease, zipDebug, - "bundleXposedPlayRelease" + "bundlePlayRelease" ) into(releaseDir) - from(layout.buildDirectory.dir("outputs/apk/xposed/release")) { + from(layout.buildDirectory.dir("outputs/apk/foss/release")) { include("*.apk") rename(".*", "LibrePods-FOSS-v$appVersionName-release.apk") } - from(layout.buildDirectory.dir("outputs/apk/xposed/debug")) { + from(layout.buildDirectory.dir("outputs/apk/foss/debug")) { include("*.apk") rename(".*", "LibrePods-FOSS-v$appVersionName-debug.apk") } - from(layout.buildDirectory.dir("outputs/bundle/xposedPlayRelease")) { + from(layout.buildDirectory.dir("outputs/bundle/playRelease")) { include("*.aab") } diff --git a/android/app/src/main/cpp/CMakeLists.txt b/android/app/src/main/cpp/CMakeLists.txt index 104773a..02de88f 100644 --- a/android/app/src/main/cpp/CMakeLists.txt +++ b/android/app/src/main/cpp/CMakeLists.txt @@ -3,8 +3,6 @@ cmake_minimum_required(VERSION 3.22.1) project("l2c_fcr_hook") set(CMAKE_CXX_STANDARD 23) -option(IS_XPOSED "Build Xposed components" OFF) - add_library(bluetooth_socket SHARED bluetooth_socket.cpp ) @@ -24,40 +22,36 @@ target_link_libraries(bluetooth_socket log ) -if(IS_XPOSED) - set(XPOSED_SRC_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../../xposed/cpp) +set(XPOSED_SRC_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../../xposed/cpp) - add_library(l2c_fcr_hook SHARED - ${XPOSED_SRC_DIR}/l2c_fcr_hook.cpp +add_library(l2c_fcr_hook SHARED + l2c_fcr_hook.cpp - ${XPOSED_SRC_DIR}/xz/xz_crc32.c - ${XPOSED_SRC_DIR}/xz/xz_crc64.c - ${XPOSED_SRC_DIR}/xz/xz_sha256.c - ${XPOSED_SRC_DIR}/xz/xz_dec_stream.c - ${XPOSED_SRC_DIR}/xz/xz_dec_lzma2.c - ${XPOSED_SRC_DIR}/xz/xz_dec_bcj.c - ) + xz/xz_crc32.c + xz/xz_crc64.c + xz/xz_sha256.c + xz/xz_dec_stream.c + xz/xz_dec_lzma2.c + xz/xz_dec_bcj.c +) - target_include_directories(l2c_fcr_hook PRIVATE - ${XPOSED_SRC_DIR} - ${XPOSED_SRC_DIR}/xz - ) +target_include_directories(l2c_fcr_hook PRIVATE + xz +) - target_compile_definitions(l2c_fcr_hook PRIVATE - XZ_DEC_X86 - XZ_DEC_ARM - XZ_DEC_ARMTHUMB - XZ_DEC_ARM64 - XZ_DEC_ANY_CHECK - XZ_USE_CRC64 - XZ_USE_SHA256 - XZ_DEC_CONCATENATED - ) +target_compile_definitions(l2c_fcr_hook PRIVATE + XZ_DEC_X86 + XZ_DEC_ARM + XZ_DEC_ARMTHUMB + XZ_DEC_ARM64 + XZ_DEC_ANY_CHECK + XZ_USE_CRC64 + XZ_USE_SHA256 + XZ_DEC_CONCATENATED +) - target_link_libraries(l2c_fcr_hook - android - log - ) - -endif() +target_link_libraries(l2c_fcr_hook + android + log +) diff --git a/android/app/src/xposed/cpp/l2c_fcr_hook.cpp b/android/app/src/main/cpp/l2c_fcr_hook.cpp similarity index 100% rename from android/app/src/xposed/cpp/l2c_fcr_hook.cpp rename to android/app/src/main/cpp/l2c_fcr_hook.cpp diff --git a/android/app/src/xposed/cpp/l2c_fcr_hook.h b/android/app/src/main/cpp/l2c_fcr_hook.h similarity index 100% rename from android/app/src/xposed/cpp/l2c_fcr_hook.h rename to android/app/src/main/cpp/l2c_fcr_hook.h diff --git a/android/app/src/xposed/cpp/xz/xz.h b/android/app/src/main/cpp/xz/xz.h similarity index 100% rename from android/app/src/xposed/cpp/xz/xz.h rename to android/app/src/main/cpp/xz/xz.h diff --git a/android/app/src/xposed/cpp/xz/xz_config.h b/android/app/src/main/cpp/xz/xz_config.h similarity index 100% rename from android/app/src/xposed/cpp/xz/xz_config.h rename to android/app/src/main/cpp/xz/xz_config.h diff --git a/android/app/src/xposed/cpp/xz/xz_crc32.c b/android/app/src/main/cpp/xz/xz_crc32.c similarity index 100% rename from android/app/src/xposed/cpp/xz/xz_crc32.c rename to android/app/src/main/cpp/xz/xz_crc32.c diff --git a/android/app/src/xposed/cpp/xz/xz_crc64.c b/android/app/src/main/cpp/xz/xz_crc64.c similarity index 100% rename from android/app/src/xposed/cpp/xz/xz_crc64.c rename to android/app/src/main/cpp/xz/xz_crc64.c diff --git a/android/app/src/xposed/cpp/xz/xz_dec_bcj.c b/android/app/src/main/cpp/xz/xz_dec_bcj.c similarity index 100% rename from android/app/src/xposed/cpp/xz/xz_dec_bcj.c rename to android/app/src/main/cpp/xz/xz_dec_bcj.c diff --git a/android/app/src/xposed/cpp/xz/xz_dec_lzma2.c b/android/app/src/main/cpp/xz/xz_dec_lzma2.c similarity index 100% rename from android/app/src/xposed/cpp/xz/xz_dec_lzma2.c rename to android/app/src/main/cpp/xz/xz_dec_lzma2.c diff --git a/android/app/src/xposed/cpp/xz/xz_dec_stream.c b/android/app/src/main/cpp/xz/xz_dec_stream.c similarity index 100% rename from android/app/src/xposed/cpp/xz/xz_dec_stream.c rename to android/app/src/main/cpp/xz/xz_dec_stream.c diff --git a/android/app/src/xposed/cpp/xz/xz_lzma2.h b/android/app/src/main/cpp/xz/xz_lzma2.h similarity index 100% rename from android/app/src/xposed/cpp/xz/xz_lzma2.h rename to android/app/src/main/cpp/xz/xz_lzma2.h diff --git a/android/app/src/xposed/cpp/xz/xz_private.h b/android/app/src/main/cpp/xz/xz_private.h similarity index 100% rename from android/app/src/xposed/cpp/xz/xz_private.h rename to android/app/src/main/cpp/xz/xz_private.h diff --git a/android/app/src/xposed/cpp/xz/xz_sha256.c b/android/app/src/main/cpp/xz/xz_sha256.c similarity index 100% rename from android/app/src/xposed/cpp/xz/xz_sha256.c rename to android/app/src/main/cpp/xz/xz_sha256.c diff --git a/android/app/src/xposed/cpp/xz/xz_stream.h b/android/app/src/main/cpp/xz/xz_stream.h similarity index 100% rename from android/app/src/xposed/cpp/xz/xz_stream.h rename to android/app/src/main/cpp/xz/xz_stream.h diff --git a/android/app/src/xposed/java/me/kavishdevar/librepods/LibrePodsApplication.kt b/android/app/src/main/java/me/kavishdevar/librepods/LibrePodsApplication.kt similarity index 99% rename from android/app/src/xposed/java/me/kavishdevar/librepods/LibrePodsApplication.kt rename to android/app/src/main/java/me/kavishdevar/librepods/LibrePodsApplication.kt index 236718f..d696803 100644 --- a/android/app/src/xposed/java/me/kavishdevar/librepods/LibrePodsApplication.kt +++ b/android/app/src/main/java/me/kavishdevar/librepods/LibrePodsApplication.kt @@ -12,6 +12,7 @@ import me.kavishdevar.librepods.utils.XposedServiceHolder import me.kavishdevar.librepods.utils.XposedState class LibrePodsApplication: Application(), XposedServiceHelper.OnServiceListener, DefaultLifecycleObserver { + override fun onCreate() { XposedServiceHelper.registerListener(this) BillingManager.provider = BillingProviderFactory.create(this) diff --git a/android/app/src/main/java/me/kavishdevar/librepods/MainActivity.kt b/android/app/src/main/java/me/kavishdevar/librepods/MainActivity.kt index b0a2aaf..e46563b 100644 --- a/android/app/src/main/java/me/kavishdevar/librepods/MainActivity.kt +++ b/android/app/src/main/java/me/kavishdevar/librepods/MainActivity.kt @@ -24,6 +24,7 @@ package me.kavishdevar.librepods // import me.kavishdevar.librepods.utils.RadareOffsetFinder //import dagger.hilt.android.AndroidEntryPoint import android.annotation.SuppressLint +import android.app.Activity import android.content.BroadcastReceiver import android.content.ComponentName import android.content.Context @@ -65,7 +66,6 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.foundation.text.input.rememberTextFieldState import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Notifications @@ -87,13 +87,11 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.scale -import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.drawscope.rotate import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.platform.LocalWindowInfo import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.vectorResource @@ -114,6 +112,7 @@ import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.google.accompanist.permissions.MultiplePermissionsState import com.google.accompanist.permissions.isGranted import com.google.accompanist.permissions.rememberMultiplePermissionsState +import com.google.android.play.core.review.ReviewManagerFactory import com.kyant.backdrop.backdrops.layerBackdrop import com.kyant.backdrop.backdrops.rememberLayerBackdrop import dev.chrisbanes.haze.hazeSource @@ -122,14 +121,8 @@ import dev.chrisbanes.haze.rememberHazeState import me.kavishdevar.librepods.data.AirPodsNotifications import me.kavishdevar.librepods.data.ControlCommandRepository import me.kavishdevar.librepods.presentation.components.AppInfoCard -import me.kavishdevar.librepods.presentation.components.ConfirmationDialog import me.kavishdevar.librepods.presentation.components.DeviceInfoCard -import me.kavishdevar.librepods.presentation.components.SelectItem -import me.kavishdevar.librepods.presentation.components.StyledBottomSheet -import me.kavishdevar.librepods.presentation.components.StyledButton import me.kavishdevar.librepods.presentation.components.StyledIconButton -import me.kavishdevar.librepods.presentation.components.StyledInputField -import me.kavishdevar.librepods.presentation.components.StyledSelectList import me.kavishdevar.librepods.presentation.screens.AccessibilitySettingsScreen import me.kavishdevar.librepods.presentation.screens.AdaptiveStrengthScreen import me.kavishdevar.librepods.presentation.screens.AirPodsSettingsScreen @@ -159,6 +152,7 @@ import kotlin.io.encoding.ExperimentalEncodingApi lateinit var serviceConnection: ServiceConnection lateinit var connectionStatusReceiver: BroadcastReceiver +lateinit var testReviewReceiver: BroadcastReceiver //@AndroidEntryPoint @ExperimentalMaterial3Api @@ -225,8 +219,6 @@ fun Main() { val context = LocalContext.current val sharedPreferences = context.getSharedPreferences("settings", MODE_PRIVATE) if (!isSupported(sharedPreferences) && !XposedState.bluetoothScopeEnabled) { - val showDialog = remember { mutableStateOf(false) } - val showPlayBypassVisible = remember { mutableStateOf(false) } val hazeState = rememberHazeState() val backdrop = rememberLayerBackdrop() val isDarkTheme = isSystemInDarkTheme() @@ -243,7 +235,7 @@ fun Main() { .background(if (isDarkTheme) Color.Black else Color(0xFFF2F2F7)), contentAlignment = Alignment.Center ) { - Column ( + Column( modifier = Modifier .padding(horizontal = 16.dp) .verticalScroll(scrollState), @@ -288,173 +280,25 @@ fun Main() { .padding(horizontal = 12.dp, vertical = 16.dp) ) } - StyledButton( - onClick = { showDialog.value = true }, - backdrop = rememberLayerBackdrop(), + Spacer(modifier = Modifier.height(4.dp)) + Text( + text = stringResource(R.string.enable_app_in_xposed_or_update_device), + style = TextStyle( + fontFamily = FontFamily(Font(R.font.sf_pro)), + fontWeight = FontWeight.Light, + color = if (isDarkTheme) Color.White else Color.Black, + fontSize = 14.sp + ), modifier = Modifier - .fillMaxWidth(), - isInteractive = false, - surfaceColor = if (isDarkTheme) Color(0xFF862424) else Color(0xFFC94646) - ) { - Text( - text = stringResource(R.string.bypass_compatibility_check), - style = TextStyle( - fontFamily = FontFamily(Font(R.font.sf_pro)), - fontWeight = FontWeight.Medium, - color = Color.White, - fontSize = 16.sp - ), - ) - } - Spacer(modifier = Modifier.height(24.dp)) + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 16.dp) + ) DeviceInfoCard() AppInfoCard() } Spacer(modifier = Modifier.height(48.dp)) } } - - ConfirmationDialog( - showDialog = showDialog, - title = stringResource(R.string.bypass_compatibility_check), - message = stringResource(R.string.bypass_compatiblity_check_confirmation), - confirmText = stringResource(R.string.yes), - dismissText = stringResource(R.string.no), - onConfirm = { - showDialog.value = false - if (BuildConfig.PLAY_BUILD) { - showPlayBypassVisible.value = true - } else { - sharedPreferences.edit { - putBoolean("bypass_device_check.v2", true) - } - val intent = Intent(context, MainActivity::class.java) - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK) - context.startActivity(intent) - } - }, - onDismiss = { - showDialog.value = false - }, - backdrop = backdrop -// hazeState = hazeState - ) - - if (BuildConfig.PLAY_BUILD) { - StyledBottomSheet( - visible = showPlayBypassVisible.value, - onDismiss = { - showPlayBypassVisible.value = false - showDialog.value = true - }, - backdrop = backdrop - ) { innerBackdrop, _ -> - val contentColor = if (isDarkTheme) Color.White else Color.Black - - var acknowledged by remember { mutableStateOf(false) } - val inputState = rememberTextFieldState("") - - val isValid = acknowledged && inputState.text.trim() == "OK" - - val sfPro = FontFamily(Font(R.font.sf_pro)) - - Column(verticalArrangement = Arrangement.spacedBy(12.dp)) { - Text( - text = stringResource(R.string.bypass_compatibility_check), - style = TextStyle( - fontFamily = sfPro, - fontWeight = FontWeight.SemiBold, - fontSize = 18.sp, - color = contentColor - ), - modifier = Modifier.padding(horizontal = 12.dp) - ) - - Text( - text = stringResource(R.string.compatibility_play_dialog_confirmation), - style = TextStyle( - fontFamily = sfPro, - fontWeight = FontWeight.Medium, - fontSize = 14.sp, - color = contentColor - ), - modifier = Modifier.padding(horizontal = 12.dp) - ) - - StyledSelectList( - items = listOf( - SelectItem( - name = stringResource(R.string.read_compatibility_requirements), - selected = acknowledged, - onClick = { acknowledged = !acknowledged } - ) - ) - ) - - val focusRequester = remember { FocusRequester() } - val keyboardController = LocalSoftwareKeyboardController.current - - LaunchedEffect(Unit) { - focusRequester.requestFocus() - keyboardController?.show() - } - - StyledInputField( - inputState = inputState, - focusRequester = focusRequester, - placeholder = stringResource(R.string.type_ok_to_continue, "OK") - ) - - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(24.dp) - ) { - StyledButton( - onClick = { showPlayBypassVisible.value = false }, - backdrop = innerBackdrop, - modifier = Modifier.weight(1f), - ) { - Text( - text = stringResource(R.string.no), - style = TextStyle( - fontFamily = sfPro, - fontWeight = FontWeight.Medium, - fontSize = 14.sp, - color = contentColor - ) - ) - } - StyledButton( - onClick = { - showPlayBypassVisible.value = false - sharedPreferences.edit { - putBoolean("bypass_device_check.v2", true) - val intent = Intent(context, MainActivity::class.java) - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK) - context.startActivity(intent) - } - }, - backdrop = innerBackdrop, - isInteractive = isValid, - modifier = Modifier.weight(1f), - enabled = isValid, - surfaceColor = if (isDarkTheme) Color(0xFF0091FF) else Color(0xFF0088FF) - ) { - Text( - text = stringResource(R.string.proceed), - style = TextStyle( - fontFamily = sfPro, - fontWeight = FontWeight.Medium, - fontSize = 14.sp, - color = if (isValid) contentColor else contentColor.copy(alpha = 0.4f) - ) - ) - } - } - } - } - } - return } @@ -515,6 +359,31 @@ fun Main() { val navController = rememberNavController() + LaunchedEffect(Unit) { + if (BuildConfig.PLAY_BUILD) { + val now = System.currentTimeMillis() + val firstConn = + sharedPreferences.getLong("first_connection_successful_time", 0L) + + val alreadyPrompted = + sharedPreferences.getBoolean("review_prompted", false) + + val oneDay = 24 * 60 * 60 * 1000L + + if ( + firstConn != 0L && + !alreadyPrompted && + (now - firstConn) > oneDay + ) { + triggerReviewFlow(context as? Activity ?: return@LaunchedEffect) + + sharedPreferences.edit { + putBoolean("review_prompted", true) + } + } + } + } + Box( modifier = Modifier.fillMaxSize() ) { @@ -652,6 +521,12 @@ fun Main() { override fun onServiceConnected(name: ComponentName?, service: IBinder?) { val binder = service as AirPodsService.LocalBinder airPodsService.value = binder.getService() + + if (!sharedPreferences.contains("first_connection_successful_time")) { + sharedPreferences.edit { + putLong("first_connection_successful_time", System.currentTimeMillis()) + } + } } override fun onServiceDisconnected(name: ComponentName?) { @@ -677,6 +552,17 @@ fun Main() { } } +private fun triggerReviewFlow(activity: Activity) { + val manager = ReviewManagerFactory.create(activity) + val request = manager.requestReviewFlow() + request.addOnCompleteListener { task -> + if (task.isSuccessful) { + val reviewInfo = task.result + manager.launchReviewFlow(activity, reviewInfo) + } + } +} + @OptIn(ExperimentalPermissionsApi::class, ExperimentalMaterial3Api::class) @Composable fun PermissionsScreen( diff --git a/android/app/src/xposed/java/me/kavishdevar/librepods/data/XposedRemotePrefImpl.kt b/android/app/src/main/java/me/kavishdevar/librepods/data/XposedRemotePrefImpl.kt similarity index 100% rename from android/app/src/xposed/java/me/kavishdevar/librepods/data/XposedRemotePrefImpl.kt rename to android/app/src/main/java/me/kavishdevar/librepods/data/XposedRemotePrefImpl.kt diff --git a/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/AirPodsSettingsScreen.kt b/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/AirPodsSettingsScreen.kt index 339c39d..c96c9f7 100644 --- a/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/AirPodsSettingsScreen.kt +++ b/android/app/src/main/java/me/kavishdevar/librepods/presentation/screens/AirPodsSettingsScreen.kt @@ -131,7 +131,6 @@ fun AirPodsSettingsScreen(viewModel: AirPodsViewModel, navController: NavControl viewModel.refreshInitialData() } - isSystemInDarkTheme() val hazeStateS = remember { mutableStateOf(HazeState()) } StyledScaffold( diff --git a/android/app/src/main/java/me/kavishdevar/librepods/services/AirPodsService.kt b/android/app/src/main/java/me/kavishdevar/librepods/services/AirPodsService.kt index 9c54d90..ecc4b55 100644 --- a/android/app/src/main/java/me/kavishdevar/librepods/services/AirPodsService.kt +++ b/android/app/src/main/java/me/kavishdevar/librepods/services/AirPodsService.kt @@ -126,6 +126,7 @@ import me.kavishdevar.librepods.utils.SystemApisUtils.METADATA_UNTETHERED_RIGHT_ import me.kavishdevar.librepods.utils.SystemApisUtils.METADATA_UNTETHERED_RIGHT_LOW_BATTERY_THRESHOLD import java.nio.ByteBuffer import java.nio.ByteOrder +import java.time.LocalDateTime import kotlin.io.encoding.Base64 import kotlin.io.encoding.ExperimentalEncodingApi @@ -2712,6 +2713,14 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList ) Log.d(TAG, " Socket connected") sharedPreferences.edit { putBoolean("connection_successful", true) } + if (!sharedPreferences.contains("first_connection_successful_time")) { + sharedPreferences.edit { + putLong( + "first_connection_successful_time", + System.currentTimeMillis() + ) + } + } sendBroadcast(Intent(AirPodsNotifications.AIRPODS_L2CAP_CONNECTED)) } catch (e: Exception) { // sharedPreferences.edit { putBoolean("connection_successful", false) } diff --git a/android/app/src/xposed/java/me/kavishdevar/librepods/utils/KotlinModule.kt b/android/app/src/main/java/me/kavishdevar/librepods/utils/KotlinModule.kt similarity index 100% rename from android/app/src/xposed/java/me/kavishdevar/librepods/utils/KotlinModule.kt rename to android/app/src/main/java/me/kavishdevar/librepods/utils/KotlinModule.kt 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 6b2fbd4..55cd5f2 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 @@ -23,7 +23,7 @@ import android.os.Build fun isSupported(sharedPreferences: SharedPreferences): Boolean { val isPixel = Build.MANUFACTURER.lowercase() == "google" - val isOppoOrOnePlus = Build.MANUFACTURER.lowercase() in listOf("oneplus", "oppo") + val isOppoFamily = Build.MANUFACTURER.lowercase() in listOf("oneplus", "oppo", "realme") val isBypassFlagActive = sharedPreferences.getBoolean("bypass_device_check.v2", false) if (isBypassFlagActive) return true @@ -31,14 +31,14 @@ fun isSupported(sharedPreferences: SharedPreferences): Boolean { if (isPixel) { when (Build.VERSION.SDK_INT) { 36 -> { - return Build.ID == "CP1A.260305.018" || Build.ID == "CP1A.260405.005" || Build.ID == "CP1A.260505.005" + return Build.ID.startsWith("CP1A") } 37 -> { return true } } - } else if (isOppoOrOnePlus) { + } else if (isOppoFamily) { return Build.VERSION.SDK_INT >= 36 } return false diff --git a/android/app/src/xposed/java/me/kavishdevar/librepods/utils/XposedServiceHolder.kt b/android/app/src/main/java/me/kavishdevar/librepods/utils/XposedServiceHolder.kt similarity index 100% rename from android/app/src/xposed/java/me/kavishdevar/librepods/utils/XposedServiceHolder.kt rename to android/app/src/main/java/me/kavishdevar/librepods/utils/XposedServiceHolder.kt diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml index f769983..0130fc9 100644 --- a/android/app/src/main/res/values/strings.xml +++ b/android/app/src/main/res/values/strings.xml @@ -252,7 +252,8 @@ \n• Google Pixel® running 17 Beta 3 and above \n• OnePlus devices running OxygenOS 16 or later \n• Oppo devices running ColorOS 16 or later - \n\nFor details, see the project documentation. + \n\nFor details, see the project documentation. + (Name your own price) This device may not be supported due to platform limitations and requires an Xposed framework. Tick the checkbox below and type OK to continue. @@ -273,5 +274,6 @@ Subject Describe your issue Optimized Charge Limit - AirPods can learn from your daily usage and determine when to charge to an optmized limit and when to allow or full charge. This limit adapts to your daily usage and preserves your battery lifespan over time.\\nThis setting may not affect unsupported AirPods, or AirPods on an older firmware version. + AirPods can learn from your daily usage and determine when to charge to an optmized limit and when to allow or full charge. This limit adapts to your daily usage and preserves your battery lifespan over time.\nThis setting may not affect unsupported AirPods, or AirPods on an older firmware version. + Enable LibrePods in Xposed or update your device to proceed. diff --git a/android/app/src/xposed/resources/META-INF/xposed/java_init.list b/android/app/src/main/resources/META-INF/xposed/java_init.list similarity index 100% rename from android/app/src/xposed/resources/META-INF/xposed/java_init.list rename to android/app/src/main/resources/META-INF/xposed/java_init.list diff --git a/android/app/src/xposed/resources/META-INF/xposed/module.prop b/android/app/src/main/resources/META-INF/xposed/module.prop similarity index 100% rename from android/app/src/xposed/resources/META-INF/xposed/module.prop rename to android/app/src/main/resources/META-INF/xposed/module.prop diff --git a/android/app/src/xposed/resources/META-INF/xposed/native_init.list b/android/app/src/main/resources/META-INF/xposed/native_init.list similarity index 100% rename from android/app/src/xposed/resources/META-INF/xposed/native_init.list rename to android/app/src/main/resources/META-INF/xposed/native_init.list diff --git a/android/app/src/xposed/resources/META-INF/xposed/scope.list b/android/app/src/main/resources/META-INF/xposed/scope.list similarity index 100% rename from android/app/src/xposed/resources/META-INF/xposed/scope.list rename to android/app/src/main/resources/META-INF/xposed/scope.list diff --git a/android/app/src/normal/java/me/kavishdevar/librepods/LibrePodsApplication.kt b/android/app/src/normal/java/me/kavishdevar/librepods/LibrePodsApplication.kt deleted file mode 100644 index 95774ab..0000000 --- a/android/app/src/normal/java/me/kavishdevar/librepods/LibrePodsApplication.kt +++ /dev/null @@ -1,21 +0,0 @@ -package me.kavishdevar.librepods - -import android.app.Application -import androidx.lifecycle.DefaultLifecycleObserver -import androidx.lifecycle.LifecycleOwner -import androidx.lifecycle.ProcessLifecycleOwner -import me.kavishdevar.librepods.billing.BillingManager -import me.kavishdevar.librepods.billing.BillingProviderFactory - -class LibrePodsApplication: Application(), DefaultLifecycleObserver { - override fun onCreate() { - BillingManager.provider = BillingProviderFactory.create(this) - ProcessLifecycleOwner.get().lifecycle.addObserver(this) - - super.onCreate() - } - - override fun onResume(owner: LifecycleOwner) { - BillingManager.provider.queryPurchases() - } -} diff --git a/android/app/src/normal/java/me/kavishdevar/librepods/data/XposedRemotePrefImpl.kt b/android/app/src/normal/java/me/kavishdevar/librepods/data/XposedRemotePrefImpl.kt deleted file mode 100644 index 072d9c3..0000000 --- a/android/app/src/normal/java/me/kavishdevar/librepods/data/XposedRemotePrefImpl.kt +++ /dev/null @@ -1,11 +0,0 @@ -package me.kavishdevar.librepods.data - -class XposedRemotePrefImpl: XposedRemotePref { - override fun isAvailable(): Boolean { return false } - - override fun getBoolean(key: String, def: Boolean): Boolean { - return false - } - - override fun putBoolean(key: String, value: Boolean) { } -} diff --git a/android/gradle/libs.versions.toml b/android/gradle/libs.versions.toml index 3c8add6..b81f1bd 100644 --- a/android/gradle/libs.versions.toml +++ b/android/gradle/libs.versions.toml @@ -1,24 +1,25 @@ [versions] accompanistPermissions = "0.37.3" -agp = "9.1.0" -kotlin = "2.3.20" +agp = "9.1.1" +kotlin = "2.3.21" coreKtx = "1.18.0" lifecycleRuntimeKtx = "2.10.0" activityCompose = "1.13.0" -composeBom = "2026.03.01" +composeBom = "2026.05.00" annotations = "26.1.0" -navigationCompose = "2.9.7" +navigationCompose = "2.9.8" constraintlayout = "2.2.1" haze = "1.7.2" hazeMaterials = "1.7.2" dynamicanimation = "1.1.0" -aboutLibraries = "14.0.1" +aboutLibraries = "14.2.0" materialIconsCore = "1.7.8" backdrop = "2.0.0-alpha03" billing = "8.3.0" hilt = "2.59.2" xposed = "101.0.0" lifecycleProcess = "2.10.0" +play = "2.0.2" [libraries] accompanist-permissions = { module = "com.google.accompanist:accompanist-permissions", version.ref = "accompanistPermissions" } @@ -49,6 +50,8 @@ hilt-compiler = { group = "com.google.dagger", name = "hilt-compiler", version.r libxposed-api = { group = "io.github.libxposed", name = "api", version.ref = "xposed" } libxposed-service = { group = "io.github.libxposed", name = "service", version.ref = "xposed" } 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" } [plugins] android-application = { id = "com.android.application", version.ref = "agp" }