Compare commits

...

7 Commits

Author SHA1 Message Date
Kavish Devar
044aff731f android: keep only xposed flavor
also changed Build.ID check to startsWith("CP1A")
2026-05-07 21:12:10 +05:30
Kavish Devar
216c97f9ca android: add CP1A.260505.005 to comptible build ids on Pixel 2026-05-06 17:29:23 +05:30
Kavish Devar
fd3774b513 android: bump version 2026-05-05 13:18:08 +05:30
Kavish Devar
b7336940e6 android: add convo detect broadcast 2026-05-05 13:17:31 +05:30
Kavish Devar
b2ba830a80 android: hide reconnect when app hasn't connected once 2026-05-05 13:11:50 +05:30
Kavish Devar
f08769e62f android: add optmized charge limit config 2026-05-05 13:05:54 +05:30
thisisAcidic
d1933c3b67 android: add popup toggles (#561)
* android: add toggles to disable bottom sheet and dynamic island popups

* android: translations for popup customization (de, es, fr, pt)
2026-05-05 12:48:22 +05:30
39 changed files with 305 additions and 313 deletions

View File

@@ -1,6 +1,6 @@
import java.util.Properties import java.util.Properties
val appVersionName = "0.2.8" val appVersionName = "0.2.9"
plugins { plugins {
alias(libs.plugins.android.application) alias(libs.plugins.android.application)
@@ -28,9 +28,8 @@ android {
defaultConfig { defaultConfig {
applicationId = "me.kavishdevar.librepods" applicationId = "me.kavishdevar.librepods"
minSdk = 33
targetSdk = 37 targetSdk = 37
versionCode = 49 versionCode = 52
versionName = appVersionName versionName = appVersionName
} }
buildTypes { buildTypes {
@@ -47,21 +46,29 @@ android {
} }
buildConfigField("Boolean", "PLAY_BUILD", "false") buildConfigField("Boolean", "PLAY_BUILD", "false")
signingConfig = signingConfigs.getByName("release") signingConfig = signingConfigs.getByName("release")
defaultConfig {
minSdk = 33
}
} }
debug { debug {
buildConfigField("Boolean", "PLAY_BUILD", "false") buildConfigField("Boolean", "PLAY_BUILD", "false")
signingConfig = signingConfigs.getByName("release") signingConfig = signingConfigs.getByName("release")
versionNameSuffix = "-debug" 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") buildConfigField("Boolean", "PLAY_BUILD", "true")
versionNameSuffix = "-play" versionNameSuffix = "-play"
} minSdk = 36
create("playDebug") {
initWith(getByName("debug"))
buildConfigField("Boolean", "PLAY_BUILD", "true")
versionNameSuffix = "-youshouldnothavethis"
} }
} }
compileOptions { compileOptions {
@@ -91,25 +98,6 @@ android {
ndkVersion = "30.0.14904198" ndkVersion = "30.0.14904198"
flavorDimensions += "env" flavorDimensions += "env"
productFlavors {
create("normal") {
dimension = "env"
externalNativeBuild {
cmake {
arguments += "-DIS_XPOSED=OFF"
}
}
}
create("xposed") {
dimension = "env"
externalNativeBuild {
cmake {
arguments += "-DIS_XPOSED=ON"
}
}
}
}
} }
dependencies { dependencies {
@@ -139,9 +127,10 @@ dependencies {
implementation(libs.backdrop) implementation(libs.backdrop)
// implementation(libs.hilt) // implementation(libs.hilt)
// implementation(libs.hilt.compiler) // implementation(libs.hilt.compiler)
add("xposedCompileOnly", libs.libxposed.api) compileOnly(libs.libxposed.api)
add("xposedImplementation", libs.libxposed.service) implementation(libs.libxposed.service)
add("playReleaseImplementation", libs.billing) implementation(libs.play.review)
implementation(libs.play.review.ktx)
} }
aboutLibraries { aboutLibraries {
@@ -184,14 +173,14 @@ fun registerRootModuleZipTask(
} }
val zipRelease = registerRootModuleZipTask( val zipRelease = registerRootModuleZipTask(
"zipXposedReleaseModule", "zipReleaseModule",
"xposed", "foss",
"release" "release"
) )
val zipDebug = registerRootModuleZipTask( val zipDebug = registerRootModuleZipTask(
"zipXposedDebugModule", "zipDebugModule",
"xposed", "foss",
"debug" "debug"
) )
@@ -200,22 +189,22 @@ val collect = tasks.register<Copy>("collectReleaseArtifacts") {
dependsOn( dependsOn(
zipRelease, zipRelease,
zipDebug, zipDebug,
"bundleXposedPlayRelease" "bundlePlayRelease"
) )
into(releaseDir) into(releaseDir)
from(layout.buildDirectory.dir("outputs/apk/xposed/release")) { from(layout.buildDirectory.dir("outputs/apk/foss/release")) {
include("*.apk") include("*.apk")
rename(".*", "LibrePods-FOSS-v$appVersionName-release.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") include("*.apk")
rename(".*", "LibrePods-FOSS-v$appVersionName-debug.apk") rename(".*", "LibrePods-FOSS-v$appVersionName-debug.apk")
} }
from(layout.buildDirectory.dir("outputs/bundle/xposedPlayRelease")) { from(layout.buildDirectory.dir("outputs/bundle/playRelease")) {
include("*.aab") include("*.aab")
} }

View File

@@ -3,8 +3,6 @@ cmake_minimum_required(VERSION 3.22.1)
project("l2c_fcr_hook") project("l2c_fcr_hook")
set(CMAKE_CXX_STANDARD 23) set(CMAKE_CXX_STANDARD 23)
option(IS_XPOSED "Build Xposed components" OFF)
add_library(bluetooth_socket SHARED add_library(bluetooth_socket SHARED
bluetooth_socket.cpp bluetooth_socket.cpp
) )
@@ -24,40 +22,36 @@ target_link_libraries(bluetooth_socket
log 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 add_library(l2c_fcr_hook SHARED
${XPOSED_SRC_DIR}/l2c_fcr_hook.cpp l2c_fcr_hook.cpp
${XPOSED_SRC_DIR}/xz/xz_crc32.c xz/xz_crc32.c
${XPOSED_SRC_DIR}/xz/xz_crc64.c xz/xz_crc64.c
${XPOSED_SRC_DIR}/xz/xz_sha256.c xz/xz_sha256.c
${XPOSED_SRC_DIR}/xz/xz_dec_stream.c xz/xz_dec_stream.c
${XPOSED_SRC_DIR}/xz/xz_dec_lzma2.c xz/xz_dec_lzma2.c
${XPOSED_SRC_DIR}/xz/xz_dec_bcj.c xz/xz_dec_bcj.c
) )
target_include_directories(l2c_fcr_hook PRIVATE target_include_directories(l2c_fcr_hook PRIVATE
${XPOSED_SRC_DIR} xz
${XPOSED_SRC_DIR}/xz )
)
target_compile_definitions(l2c_fcr_hook PRIVATE target_compile_definitions(l2c_fcr_hook PRIVATE
XZ_DEC_X86 XZ_DEC_X86
XZ_DEC_ARM XZ_DEC_ARM
XZ_DEC_ARMTHUMB XZ_DEC_ARMTHUMB
XZ_DEC_ARM64 XZ_DEC_ARM64
XZ_DEC_ANY_CHECK XZ_DEC_ANY_CHECK
XZ_USE_CRC64 XZ_USE_CRC64
XZ_USE_SHA256 XZ_USE_SHA256
XZ_DEC_CONCATENATED XZ_DEC_CONCATENATED
) )
target_link_libraries(l2c_fcr_hook target_link_libraries(l2c_fcr_hook
android android
log log
) )
endif()

View File

@@ -12,6 +12,7 @@ import me.kavishdevar.librepods.utils.XposedServiceHolder
import me.kavishdevar.librepods.utils.XposedState import me.kavishdevar.librepods.utils.XposedState
class LibrePodsApplication: Application(), XposedServiceHelper.OnServiceListener, DefaultLifecycleObserver { class LibrePodsApplication: Application(), XposedServiceHelper.OnServiceListener, DefaultLifecycleObserver {
override fun onCreate() { override fun onCreate() {
XposedServiceHelper.registerListener(this) XposedServiceHelper.registerListener(this)
BillingManager.provider = BillingProviderFactory.create(this) BillingManager.provider = BillingProviderFactory.create(this)

View File

@@ -24,6 +24,7 @@ package me.kavishdevar.librepods
// import me.kavishdevar.librepods.utils.RadareOffsetFinder // import me.kavishdevar.librepods.utils.RadareOffsetFinder
//import dagger.hilt.android.AndroidEntryPoint //import dagger.hilt.android.AndroidEntryPoint
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.Activity
import android.content.BroadcastReceiver import android.content.BroadcastReceiver
import android.content.ComponentName import android.content.ComponentName
import android.content.Context import android.content.Context
@@ -65,7 +66,6 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.input.rememberTextFieldState
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Notifications 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.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.scale import androidx.compose.ui.draw.scale
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.drawscope.rotate import androidx.compose.ui.graphics.drawscope.rotate
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.platform.LocalWindowInfo import androidx.compose.ui.platform.LocalWindowInfo
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.vectorResource 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.MultiplePermissionsState
import com.google.accompanist.permissions.isGranted import com.google.accompanist.permissions.isGranted
import com.google.accompanist.permissions.rememberMultiplePermissionsState 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.layerBackdrop
import com.kyant.backdrop.backdrops.rememberLayerBackdrop import com.kyant.backdrop.backdrops.rememberLayerBackdrop
import dev.chrisbanes.haze.hazeSource 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.AirPodsNotifications
import me.kavishdevar.librepods.data.ControlCommandRepository import me.kavishdevar.librepods.data.ControlCommandRepository
import me.kavishdevar.librepods.presentation.components.AppInfoCard 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.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.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.AccessibilitySettingsScreen
import me.kavishdevar.librepods.presentation.screens.AdaptiveStrengthScreen import me.kavishdevar.librepods.presentation.screens.AdaptiveStrengthScreen
import me.kavishdevar.librepods.presentation.screens.AirPodsSettingsScreen import me.kavishdevar.librepods.presentation.screens.AirPodsSettingsScreen
@@ -159,6 +152,7 @@ import kotlin.io.encoding.ExperimentalEncodingApi
lateinit var serviceConnection: ServiceConnection lateinit var serviceConnection: ServiceConnection
lateinit var connectionStatusReceiver: BroadcastReceiver lateinit var connectionStatusReceiver: BroadcastReceiver
lateinit var testReviewReceiver: BroadcastReceiver
//@AndroidEntryPoint //@AndroidEntryPoint
@ExperimentalMaterial3Api @ExperimentalMaterial3Api
@@ -225,8 +219,6 @@ fun Main() {
val context = LocalContext.current val context = LocalContext.current
val sharedPreferences = context.getSharedPreferences("settings", MODE_PRIVATE) val sharedPreferences = context.getSharedPreferences("settings", MODE_PRIVATE)
if (!isSupported(sharedPreferences) && !XposedState.bluetoothScopeEnabled) { if (!isSupported(sharedPreferences) && !XposedState.bluetoothScopeEnabled) {
val showDialog = remember { mutableStateOf(false) }
val showPlayBypassVisible = remember { mutableStateOf(false) }
val hazeState = rememberHazeState() val hazeState = rememberHazeState()
val backdrop = rememberLayerBackdrop() val backdrop = rememberLayerBackdrop()
val isDarkTheme = isSystemInDarkTheme() val isDarkTheme = isSystemInDarkTheme()
@@ -243,7 +235,7 @@ fun Main() {
.background(if (isDarkTheme) Color.Black else Color(0xFFF2F2F7)), .background(if (isDarkTheme) Color.Black else Color(0xFFF2F2F7)),
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
Column ( Column(
modifier = Modifier modifier = Modifier
.padding(horizontal = 16.dp) .padding(horizontal = 16.dp)
.verticalScroll(scrollState), .verticalScroll(scrollState),
@@ -288,173 +280,25 @@ fun Main() {
.padding(horizontal = 12.dp, vertical = 16.dp) .padding(horizontal = 12.dp, vertical = 16.dp)
) )
} }
StyledButton( Spacer(modifier = Modifier.height(4.dp))
onClick = { showDialog.value = true }, Text(
backdrop = rememberLayerBackdrop(), 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 modifier = Modifier
.fillMaxWidth(), .fillMaxWidth()
isInteractive = false, .padding(horizontal = 16.dp, vertical = 16.dp)
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))
DeviceInfoCard() DeviceInfoCard()
AppInfoCard() AppInfoCard()
} }
Spacer(modifier = Modifier.height(48.dp)) 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 return
} }
@@ -515,6 +359,31 @@ fun Main() {
val navController = rememberNavController() 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( Box(
modifier = Modifier.fillMaxSize() modifier = Modifier.fillMaxSize()
) { ) {
@@ -652,6 +521,12 @@ fun Main() {
override fun onServiceConnected(name: ComponentName?, service: IBinder?) { override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
val binder = service as AirPodsService.LocalBinder val binder = service as AirPodsService.LocalBinder
airPodsService.value = binder.getService() 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?) { 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) @OptIn(ExperimentalPermissionsApi::class, ExperimentalMaterial3Api::class)
@Composable @Composable
fun PermissionsScreen( fun PermissionsScreen(

View File

@@ -109,7 +109,8 @@ class AACPManager {
EAR_DETECTION_CONFIG(0x0A), AUTOMATIC_CONNECTION_CONFIG(0x20), OWNS_CONNECTION(0x06), PPE_TOGGLE_CONFIG( EAR_DETECTION_CONFIG(0x0A), AUTOMATIC_CONNECTION_CONFIG(0x20), OWNS_CONNECTION(0x06), PPE_TOGGLE_CONFIG(
0x37 0x37
), ),
PPE_CAP_LEVEL_CONFIG(0x38); PPE_CAP_LEVEL_CONFIG(0x38),
DYNAMIC_END_OF_CHARGE(0x3B);
companion object { companion object {
fun fromByte(byte: Byte): ControlCommandIdentifiers? = fun fromByte(byte: Byte): ControlCommandIdentifiers? =

View File

@@ -131,7 +131,6 @@ fun AirPodsSettingsScreen(viewModel: AirPodsViewModel, navController: NavControl
viewModel.refreshInitialData() viewModel.refreshInitialData()
} }
isSystemInDarkTheme()
val hazeStateS = remember { mutableStateOf(HazeState()) } val hazeStateS = remember { mutableStateOf(HazeState()) }
StyledScaffold( StyledScaffold(
@@ -398,6 +397,16 @@ fun AirPodsSettingsScreen(viewModel: AirPodsViewModel, navController: NavControl
} }
} }
item(key = "spacer_dynamic_end_of_charge") { Spacer(modifier = Modifier.height(16.dp)) }
item(key = "dynamic_end_of_charge") {
StyledToggle(
label = stringResource(R.string.optimized_charging),
description = stringResource(R.string.optimized_charging_description),
checked = state.dynamicEndOfCharge,
onCheckedChange = viewModel::setDynamicEndOfCharge
)
}
item(key = "spacer_accessibility") { Spacer(modifier = Modifier.height(16.dp)) } item(key = "spacer_accessibility") { Spacer(modifier = Modifier.height(16.dp)) }
item(key = "accessibility") { item(key = "accessibility") {
NavigationButton( NavigationButton(
@@ -542,19 +551,22 @@ fun AirPodsSettingsScreen(viewModel: AirPodsViewModel, navController: NavControl
} }
Spacer(Modifier.height(16.dp)) Spacer(Modifier.height(16.dp))
} }
StyledButton( if (state.connectionSuccessful) {
onClick = { StyledButton(
viewModel.reconnectFromSavedMac() onClick = {
}, backdrop = backdrop, modifier = Modifier.fillMaxWidth(0.9f) viewModel.reconnectFromSavedMac()
) { }, backdrop = backdrop, modifier = Modifier.fillMaxWidth(0.9f)
Text( ) {
text = stringResource(R.string.reconnect_to_last_device), style = TextStyle( Text(
fontSize = 16.sp, text = stringResource(R.string.reconnect_to_last_device),
fontWeight = FontWeight.Medium, style = TextStyle(
fontFamily = FontFamily(Font(R.font.sf_pro)), fontSize = 16.sp,
color = if (isSystemInDarkTheme()) Color.White else Color.Black fontWeight = FontWeight.Medium,
fontFamily = FontFamily(Font(R.font.sf_pro)),
color = if (isSystemInDarkTheme()) Color.White else Color.Black
)
) )
) }
} }
} }
} }

View File

@@ -157,6 +157,48 @@ fun AppSettingsScreen(
enabled = state.isPremium enabled = state.isPremium
) )
Text(
text = stringResource(R.string.popup_animations), style = TextStyle(
fontSize = 14.sp,
fontWeight = FontWeight.Bold,
color = textColor.copy(alpha = 0.6f),
fontFamily = FontFamily(Font(R.font.sf_pro))
), modifier = Modifier.padding(16.dp, bottom = 2.dp, top = 24.dp)
)
Spacer(modifier = Modifier.height(2.dp))
Column(
modifier = Modifier
.fillMaxWidth()
.background(
backgroundColor, RoundedCornerShape(28.dp)
)
.padding(vertical = 4.dp)
) {
StyledToggle(
label = stringResource(R.string.show_bottom_sheet_popup),
description = stringResource(R.string.show_bottom_sheet_popup_description),
checked = state.showBottomSheetPopup,
onCheckedChange = viewModel::setShowBottomSheetPopup,
independent = false
)
HorizontalDivider(
thickness = 1.dp,
color = Color(0x40888888),
modifier = Modifier.padding(horizontal = 12.dp)
)
StyledToggle(
label = stringResource(R.string.show_island_popup),
description = stringResource(R.string.show_island_popup_description),
checked = state.showIslandPopup,
onCheckedChange = viewModel::setShowIslandPopup,
independent = false
)
}
Text( Text(
text = stringResource(R.string.conversational_awareness), style = TextStyle( text = stringResource(R.string.conversational_awareness), style = TextStyle(
fontSize = 14.sp, fontSize = 14.sp,

View File

@@ -89,7 +89,11 @@ data class AirPodsUiState(
val hearingAidData: ByteArray = byteArrayOf(), val hearingAidData: ByteArray = byteArrayOf(),
val isPremium: Boolean = false, val isPremium: Boolean = false,
val vendorIdHook: Boolean = false val vendorIdHook: Boolean = false,
val dynamicEndOfCharge: Boolean = false,
val connectionSuccessful: Boolean = false
) )
class AirPodsViewModel( class AirPodsViewModel(
@@ -268,9 +272,16 @@ class AirPodsViewModel(
val current = state.controlStates[identifier] val current = state.controlStates[identifier]
if (current?.contentEquals(value) == true) return@update state if (current?.contentEquals(value) == true) return@update state
state.copy( if (identifier == ControlCommandIdentifiers.DYNAMIC_END_OF_CHARGE) {
controlStates = state.controlStates + (identifier to value) state.copy(
) dynamicEndOfCharge = value[0] == 0x01.toByte(),
controlStates = state.controlStates + (identifier to value)
)
} else {
state.copy(
controlStates = state.controlStates + (identifier to value)
)
}
} }
} }
@@ -305,6 +316,7 @@ class AirPodsViewModel(
ControlCommandIdentifiers.AUTOMATIC_CONNECTION_CONFIG, ControlCommandIdentifiers.AUTOMATIC_CONNECTION_CONFIG,
ControlCommandIdentifiers.OWNS_CONNECTION, ControlCommandIdentifiers.OWNS_CONNECTION,
ControlCommandIdentifiers.PPE_TOGGLE_CONFIG, ControlCommandIdentifiers.PPE_TOGGLE_CONFIG,
ControlCommandIdentifiers.DYNAMIC_END_OF_CHARGE
) )
for (identifier in identifiersList) { for (identifier in identifiersList) {
observeControl(identifier) observeControl(identifier)
@@ -342,6 +354,9 @@ class AirPodsViewModel(
) ?: "CYCLE_NOISE_CONTROL_MODES" ) ?: "CYCLE_NOISE_CONTROL_MODES"
) )
val vendorIdHook = xposedRemotePref.getBoolean("vendor_id_hook", false) val vendorIdHook = xposedRemotePref.getBoolean("vendor_id_hook", false)
val dynamicEndOfCharge = sharedPreferences.getBoolean("dynamic_end_of_charge", false)
val connectionSuccessful = sharedPreferences.getBoolean("connection_successful", false)
_uiState.update { _uiState.update {
it.copy( it.copy(
@@ -351,7 +366,9 @@ class AirPodsViewModel(
headGesturesEnabled = headGesturesEnabled, headGesturesEnabled = headGesturesEnabled,
leftAction = leftAction, leftAction = leftAction,
rightAction = rightAction, rightAction = rightAction,
vendorIdHook = vendorIdHook vendorIdHook = vendorIdHook,
dynamicEndOfCharge = dynamicEndOfCharge,
connectionSuccessful = connectionSuccessful
) )
} }
} }
@@ -371,6 +388,14 @@ class AirPodsViewModel(
} }
} }
fun setDynamicEndOfCharge(enabled: Boolean) {
service.aacpManager.sendControlCommand(ControlCommandIdentifiers.DYNAMIC_END_OF_CHARGE.value, enabled)
sharedPreferences.edit { putBoolean("dynamic_end_of_charge", enabled) }
_uiState.update {
it.copy(dynamicEndOfCharge = enabled)
}
}
private fun loadControlList() { private fun loadControlList() {
_uiState.update { _uiState.update {
it.copy( it.copy(

View File

@@ -32,7 +32,9 @@ data class AppSettingsUiState(
val cameraPackageError: String? = null, val cameraPackageError: String? = null,
val vendorIdHook: Boolean = false, val vendorIdHook: Boolean = false,
val isPremium: Boolean = false, val isPremium: Boolean = false,
val connectionSuccessful: Boolean = false val connectionSuccessful: Boolean = false,
val showBottomSheetPopup: Boolean = true,
val showIslandPopup: Boolean = true
) )
class AppSettingsViewModel(application: Application) : AndroidViewModel(application) { class AppSettingsViewModel(application: Application) : AndroidViewModel(application) {
@@ -86,7 +88,9 @@ class AppSettingsViewModel(application: Application) : AndroidViewModel(applicat
conversationalAwarenessVolume = sharedPreferences.getInt("conversational_awareness_volume", 43).toFloat(), conversationalAwarenessVolume = sharedPreferences.getInt("conversational_awareness_volume", 43).toFloat(),
cameraPackageValue = sharedPreferences.getString("custom_camera_package", "") ?: "", cameraPackageValue = sharedPreferences.getString("custom_camera_package", "") ?: "",
vendorIdHook = xposedRemotePref.getBoolean("vendor_id_hook", false), vendorIdHook = xposedRemotePref.getBoolean("vendor_id_hook", false),
connectionSuccessful = sharedPreferences.getBoolean("connection_successful", false) connectionSuccessful = sharedPreferences.getBoolean("connection_successful", false),
showBottomSheetPopup = sharedPreferences.getBoolean("show_bottom_sheet_popup", true),
showIslandPopup = sharedPreferences.getBoolean("show_island_popup", true)
) )
} }
} }
@@ -176,4 +180,14 @@ class AppSettingsViewModel(application: Application) : AndroidViewModel(applicat
xposedRemotePref.putBoolean("vendor_id_hook", enabled) xposedRemotePref.putBoolean("vendor_id_hook", enabled)
_uiState.update { it.copy(vendorIdHook = enabled) } _uiState.update { it.copy(vendorIdHook = enabled) }
} }
fun setShowBottomSheetPopup(enabled: Boolean) {
sharedPreferences.edit { putBoolean("show_bottom_sheet_popup", enabled) }
_uiState.update { it.copy(showBottomSheetPopup = enabled) }
}
fun setShowIslandPopup(enabled: Boolean) {
sharedPreferences.edit { putBoolean("show_island_popup", enabled) }
_uiState.update { it.copy(showIslandPopup = enabled) }
}
} }

View File

@@ -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 me.kavishdevar.librepods.utils.SystemApisUtils.METADATA_UNTETHERED_RIGHT_LOW_BATTERY_THRESHOLD
import java.nio.ByteBuffer import java.nio.ByteBuffer
import java.nio.ByteOrder import java.nio.ByteOrder
import java.time.LocalDateTime
import kotlin.io.encoding.Base64 import kotlin.io.encoding.Base64
import kotlin.io.encoding.ExperimentalEncodingApi import kotlin.io.encoding.ExperimentalEncodingApi
@@ -526,7 +527,7 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
initializeConfig() initializeConfig()
ancModeReceiver = object : BroadcastReceiver() { externalBroadcastReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) { override fun onReceive(context: Context?, intent: Intent?) {
if (intent?.action == "me.kavishdevar.librepods.SET_ANC_MODE") { if (intent?.action == "me.kavishdevar.librepods.SET_ANC_MODE") {
if (intent.hasExtra("mode")) { if (intent.hasExtra("mode")) {
@@ -555,15 +556,23 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
"Cycling ANC mode from $currentMode to $nextMode" "Cycling ANC mode from $currentMode to $nextMode"
) )
} }
} else if (intent?.action == "me.kavishdevar.librepods.CONVO_DETECT") {
if (intent.hasExtra("enabled")) {
val enabled = intent.getBooleanExtra("enabled", false)
aacpManager.sendControlCommand(
AACPManager.Companion.ControlCommandIdentifiers.CONVERSATION_DETECT_CONFIG.value,
enabled
)
}
} }
} }
} }
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
registerReceiver(ancModeReceiver, ancModeFilter, RECEIVER_EXPORTED) registerReceiver(externalBroadcastReceiver, externalBroadcastFilter, RECEIVER_EXPORTED)
} else { } else {
@Suppress("UnspecifiedRegisterReceiverFlag") registerReceiver( @Suppress("UnspecifiedRegisterReceiverFlag") registerReceiver(
ancModeReceiver, ancModeFilter externalBroadcastReceiver, externalBroadcastFilter
) )
} }
val audioManager = this@AirPodsService.getSystemService(AUDIO_SERVICE) as AudioManager val audioManager = this@AirPodsService.getSystemService(AUDIO_SERVICE) as AudioManager
@@ -1636,6 +1645,9 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
var popupShown = false var popupShown = false
fun showPopup(service: Service, name: String) { fun showPopup(service: Service, name: String) {
if (!sharedPreferences.getBoolean("show_bottom_sheet_popup", true)) {
return
}
if (!Settings.canDrawOverlays(service)) { if (!Settings.canDrawOverlays(service)) {
Log.d(TAG, "No permission for SYSTEM_ALERT_WINDOW") Log.d(TAG, "No permission for SYSTEM_ALERT_WINDOW")
return return
@@ -1660,6 +1672,9 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
otherDeviceName: String? = null otherDeviceName: String? = null
) { ) {
Log.d(TAG, "Showing island window") Log.d(TAG, "Showing island window")
if (!sharedPreferences.getBoolean("show_island_popup", true)) {
return
}
if (!Settings.canDrawOverlays(service)) { if (!Settings.canDrawOverlays(service)) {
Log.d(TAG, "No permission for SYSTEM_ALERT_WINDOW") Log.d(TAG, "No permission for SYSTEM_ALERT_WINDOW")
return return
@@ -2391,8 +2406,11 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
} }
} }
val ancModeFilter = IntentFilter("me.kavishdevar.librepods.SET_ANC_MODE") val externalBroadcastFilter = IntentFilter().apply {
var ancModeReceiver: BroadcastReceiver? = null addAction("me.kavishdevar.librepods.SET_ANC_MODE")
addAction("me.kavishdevar.librepods.CONVO_DETECT")
}
var externalBroadcastReceiver: BroadcastReceiver? = null
@SuppressLint("InlinedApi", "MissingPermission", "UnspecifiedRegisterReceiverFlag") @SuppressLint("InlinedApi", "MissingPermission", "UnspecifiedRegisterReceiverFlag")
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
@@ -2695,6 +2713,14 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
) )
Log.d(TAG, "<LogCollector:Complete:Success> Socket connected") Log.d(TAG, "<LogCollector:Complete:Success> Socket connected")
sharedPreferences.edit { putBoolean("connection_successful", true) } 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)) sendBroadcast(Intent(AirPodsNotifications.AIRPODS_L2CAP_CONNECTED))
} catch (e: Exception) { } catch (e: Exception) {
// sharedPreferences.edit { putBoolean("connection_successful", false) } // sharedPreferences.edit { putBoolean("connection_successful", false) }
@@ -3098,7 +3124,7 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
e.printStackTrace() e.printStackTrace()
} }
try { try {
unregisterReceiver(ancModeReceiver) unregisterReceiver(externalBroadcastReceiver)
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
} }

View File

@@ -23,7 +23,7 @@ import android.os.Build
fun isSupported(sharedPreferences: SharedPreferences): Boolean { fun isSupported(sharedPreferences: SharedPreferences): Boolean {
val isPixel = Build.MANUFACTURER.lowercase() == "google" 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) val isBypassFlagActive = sharedPreferences.getBoolean("bypass_device_check.v2", false)
if (isBypassFlagActive) return true if (isBypassFlagActive) return true
@@ -31,14 +31,14 @@ fun isSupported(sharedPreferences: SharedPreferences): Boolean {
if (isPixel) { if (isPixel) {
when (Build.VERSION.SDK_INT) { when (Build.VERSION.SDK_INT) {
36 -> { 36 -> {
return Build.ID == "CP1A.260305.018" || Build.ID == "CP1A.260405.005" return Build.ID.startsWith("CP1A")
} }
37 -> { 37 -> {
return true return true
} }
} }
} else if (isOppoOrOnePlus) { } else if (isOppoFamily) {
return Build.VERSION.SDK_INT >= 36 return Build.VERSION.SDK_INT >= 36
} }
return false return false

View File

@@ -0,0 +1,7 @@
<resources xmlns:tools="http://schemas.android.com/tools" tools:ignore="MissingTranslation">
<string name="popup_animations">Popup-Animationen</string>
<string name="show_bottom_sheet_popup">Popup unten</string>
<string name="show_bottom_sheet_popup_description">Zeigt das Popup im iOS-Stil unten an, wenn AirPods sich verbinden.</string>
<string name="show_island_popup">Dynamic Island Popup</string>
<string name="show_island_popup_description">Zeigt das Popup im Dynamic-Island-Stil oben für Verbindungs- und Übergabe-Ereignisse.</string>
</resources>

View File

@@ -210,4 +210,9 @@
<string name="listening_mode_transparency_description">Deja entrar los sonidos externos</string> <string name="listening_mode_transparency_description">Deja entrar los sonidos externos</string>
<string name="listening_mode_adaptive_description">Ajuste dinámico del ruido externo</string> <string name="listening_mode_adaptive_description">Ajuste dinámico del ruido externo</string>
<string name="listening_mode_noise_cancellation_description">Bloquea los sonidos externos</string> <string name="listening_mode_noise_cancellation_description">Bloquea los sonidos externos</string>
<string name="popup_animations">Animaciones emergentes</string>
<string name="show_bottom_sheet_popup">Ventana emergente inferior</string>
<string name="show_bottom_sheet_popup_description">Muestra la ventana emergente estilo iOS en la parte inferior cuando los AirPods se conectan.</string>
<string name="show_island_popup">Ventana emergente Dynamic Island</string>
<string name="show_island_popup_description">Muestra la ventana emergente estilo Dynamic Island en la parte superior para eventos de conexión y traspaso.</string>
</resources> </resources>

View File

@@ -210,4 +210,9 @@
<string name="listening_mode_transparency_description">Laisser entrer les sons extérieurs</string> <string name="listening_mode_transparency_description">Laisser entrer les sons extérieurs</string>
<string name="listening_mode_adaptive_description">Ajuster dynamiquement les sons extérieurs</string> <string name="listening_mode_adaptive_description">Ajuster dynamiquement les sons extérieurs</string>
<string name="listening_mode_noise_cancellation_description">Bloquer les sons extérieurs</string> <string name="listening_mode_noise_cancellation_description">Bloquer les sons extérieurs</string>
<string name="popup_animations">Animations contextuelles</string>
<string name="show_bottom_sheet_popup">Fenêtre contextuelle en bas</string>
<string name="show_bottom_sheet_popup_description">Afficher la fenêtre contextuelle de style iOS en bas de l\'écran lors de la connexion des AirPods.</string>
<string name="show_island_popup">Fenêtre Dynamic Island</string>
<string name="show_island_popup_description">Afficher la fenêtre de style Dynamic Island en haut de l\'écran pour les événements de connexion et de transfert.</string>
</resources> </resources>

View File

@@ -210,4 +210,9 @@
<string name="listening_mode_transparency_description">Permite sons externos</string> <string name="listening_mode_transparency_description">Permite sons externos</string>
<string name="listening_mode_adaptive_description">Ajusta dinamicamente o ruído externo</string> <string name="listening_mode_adaptive_description">Ajusta dinamicamente o ruído externo</string>
<string name="listening_mode_noise_cancellation_description">Bloqueia sons externos</string> <string name="listening_mode_noise_cancellation_description">Bloqueia sons externos</string>
<string name="popup_animations">Animações de pop-up</string>
<string name="show_bottom_sheet_popup">Pop-up inferior</string>
<string name="show_bottom_sheet_popup_description">Exibe o pop-up estilo iOS na parte inferior quando os AirPods se conectam.</string>
<string name="show_island_popup">Pop-up Dynamic Island</string>
<string name="show_island_popup_description">Exibe o pop-up estilo Dynamic Island no topo da tela em eventos de conexão e transferência.</string>
</resources> </resources>

View File

@@ -140,6 +140,11 @@
<string name="widget">Widget</string> <string name="widget">Widget</string>
<string name="show_phone_battery_in_widget">Show phone battery in widget</string> <string name="show_phone_battery_in_widget">Show phone battery in widget</string>
<string name="show_phone_battery_in_widget_description">Display your phone\'s battery level in the widget alongside AirPods battery</string> <string name="show_phone_battery_in_widget_description">Display your phone\'s battery level in the widget alongside AirPods battery</string>
<string name="popup_animations">Popup Animations</string>
<string name="show_bottom_sheet_popup">Bottom sheet popup</string>
<string name="show_bottom_sheet_popup_description">Show the iOS-style modal popup at the bottom when AirPods connect.</string>
<string name="show_island_popup">Dynamic Island popup</string>
<string name="show_island_popup_description">Show the Dynamic Island-style popup at the top for connection and takeover events.</string>
<string name="conversational_awareness_volume">Conversational Awareness Volume</string> <string name="conversational_awareness_volume">Conversational Awareness Volume</string>
<string name="quick_settings_tile">Quick Settings Tile</string> <string name="quick_settings_tile">Quick Settings Tile</string>
<string name="open_dialog_for_controlling">Open dialog for controlling</string> <string name="open_dialog_for_controlling">Open dialog for controlling</string>
@@ -247,7 +252,8 @@
\n• Google Pixel® running 17 Beta 3 and above \n• Google Pixel® running 17 Beta 3 and above
\n• OnePlus devices running OxygenOS 16 or later \n• OnePlus devices running OxygenOS 16 or later
\n• Oppo devices running ColorOS 16 or later \n• Oppo devices running ColorOS 16 or later
\n\nFor details, see the project documentation.</string> \n\nFor details, see the project documentation.
</string>
<string name="name_your_own_price">(Name your own price)</string> <string name="name_your_own_price">(Name your own price)</string>
<string name="compatibility_play_dialog_confirmation"> <string name="compatibility_play_dialog_confirmation">
This device may not be supported due to platform limitations and requires an Xposed framework. Tick the checkbox below and type OK to continue. This device may not be supported due to platform limitations and requires an Xposed framework. Tick the checkbox below and type OK to continue.
@@ -267,4 +273,7 @@
<string name="app_enabled_in_xposed">App enabled in Xposed</string> <string name="app_enabled_in_xposed">App enabled in Xposed</string>
<string name="subject">Subject</string> <string name="subject">Subject</string>
<string name="describe_your_issue">Describe your issue</string> <string name="describe_your_issue">Describe your issue</string>
<string name="optimized_charging">Optimized Charge Limit</string>
<string name="optimized_charging_description">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.</string>
<string name="enable_app_in_xposed_or_update_device">Enable LibrePods in Xposed or update your device to proceed.</string>
</resources> </resources>

View File

@@ -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<Application>.onCreate()
}
override fun onResume(owner: LifecycleOwner) {
BillingManager.provider.queryPurchases()
}
}

View File

@@ -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) { }
}

View File

@@ -1,24 +1,25 @@
[versions] [versions]
accompanistPermissions = "0.37.3" accompanistPermissions = "0.37.3"
agp = "9.1.0" agp = "9.1.1"
kotlin = "2.3.20" kotlin = "2.3.21"
coreKtx = "1.18.0" coreKtx = "1.18.0"
lifecycleRuntimeKtx = "2.10.0" lifecycleRuntimeKtx = "2.10.0"
activityCompose = "1.13.0" activityCompose = "1.13.0"
composeBom = "2026.03.01" composeBom = "2026.05.00"
annotations = "26.1.0" annotations = "26.1.0"
navigationCompose = "2.9.7" navigationCompose = "2.9.8"
constraintlayout = "2.2.1" constraintlayout = "2.2.1"
haze = "1.7.2" haze = "1.7.2"
hazeMaterials = "1.7.2" hazeMaterials = "1.7.2"
dynamicanimation = "1.1.0" dynamicanimation = "1.1.0"
aboutLibraries = "14.0.1" aboutLibraries = "14.2.0"
materialIconsCore = "1.7.8" materialIconsCore = "1.7.8"
backdrop = "2.0.0-alpha03" backdrop = "2.0.0-alpha03"
billing = "8.3.0" billing = "8.3.0"
hilt = "2.59.2" hilt = "2.59.2"
xposed = "101.0.0" xposed = "101.0.0"
lifecycleProcess = "2.10.0" lifecycleProcess = "2.10.0"
play = "2.0.2"
[libraries] [libraries]
accompanist-permissions = { module = "com.google.accompanist:accompanist-permissions", version.ref = "accompanistPermissions" } 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-api = { group = "io.github.libxposed", name = "api", version.ref = "xposed" }
libxposed-service = { group = "io.github.libxposed", name = "service", 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" } 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] [plugins]
android-application = { id = "com.android.application", version.ref = "agp" } android-application = { id = "com.android.application", version.ref = "agp" }