diff --git a/android/app/src/main/java/me/kavishdevar/librepods/screens/AppSettingsScreen.kt b/android/app/src/main/java/me/kavishdevar/librepods/screens/AppSettingsScreen.kt index d49021e..bbe332f 100644 --- a/android/app/src/main/java/me/kavishdevar/librepods/screens/AppSettingsScreen.kt +++ b/android/app/src/main/java/me/kavishdevar/librepods/screens/AppSettingsScreen.kt @@ -20,6 +20,7 @@ package me.kavishdevar.librepods.screens import android.content.Context import android.widget.Toast +import androidx.activity.compose.BackHandler import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource @@ -99,6 +100,9 @@ import me.kavishdevar.librepods.utils.RadareOffsetFinder import kotlin.io.encoding.Base64 import kotlin.io.encoding.ExperimentalEncodingApi import kotlin.math.roundToInt +import androidx.compose.runtime.rememberCoroutineScope +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch @OptIn(ExperimentalMaterial3Api::class, ExperimentalHazeMaterialsApi::class, ExperimentalEncodingApi::class) @Composable @@ -107,6 +111,7 @@ fun AppSettingsScreen(navController: NavController) { val name = remember { mutableStateOf(sharedPreferences.getString("name", "") ?: "") } val isDarkTheme = isSystemInDarkTheme() val context = LocalContext.current + val coroutineScope = rememberCoroutineScope() val scrollState = rememberScrollState() val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior() val hazeState = remember { HazeState() } @@ -200,6 +205,11 @@ fun AppSettingsScreen(navController: NavController) { return hexPattern.matches(input) } + var isProcessingSdp by remember { mutableStateOf(false) } + var actAsAppleDevice by remember { mutableStateOf(false) } + + BackHandler(enabled = isProcessingSdp) {} + Scaffold( modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), topBar = { @@ -233,8 +243,11 @@ fun AppSettingsScreen(navController: NavController) { navigationIcon = { TextButton( onClick = { - navController.popBackStack() + if (!isProcessingSdp) { + navController.popBackStack() + } }, + enabled = !isProcessingSdp, shape = RoundedCornerShape(8.dp), modifier = Modifier.width(180.dp) ) { @@ -1189,6 +1202,91 @@ fun AppSettingsScreen(navController: NavController) { ) } } + + LaunchedEffect(Unit) { + actAsAppleDevice = RadareOffsetFinder.isSdpOffsetAvailable() + } + + Row( + modifier = Modifier + .fillMaxWidth() + .clickable( + enabled = !isProcessingSdp, + indication = null, + interactionSource = remember { MutableInteractionSource() } + ) { + if (!isProcessingSdp) { + val newValue = !actAsAppleDevice + actAsAppleDevice = newValue + isProcessingSdp = true + coroutineScope.launch { + if (newValue) { + val radareOffsetFinder = RadareOffsetFinder(context) + val success = radareOffsetFinder.findSdpOffset() ?: false + if (success) { + Toast.makeText(context, "Found offset please restart the Bluetooth process", Toast.LENGTH_LONG).show() + } + } else { + RadareOffsetFinder.clearSdpOffset() + } + isProcessingSdp = false + } + } + }, + verticalAlignment = Alignment.CenterVertically + ) { + Column( + modifier = Modifier + .weight(1f) + .padding(vertical = 8.dp) + .padding(end = 4.dp) + ) { + Text( + text = "Act as an Apple device", + fontSize = 16.sp, + color = textColor + ) + Spacer(modifier = Modifier.height(4.dp)) + Text( + text = "Enables multi-device connectivity and Accessibility features like customizing transparency mode (amplification, tone, ambient noise reduction, conversation boost, and EQ)", + fontSize = 14.sp, + color = textColor.copy(0.6f), + lineHeight = 16.sp, + ) + if (actAsAppleDevice) { + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = "Might be unstable!! A maximum of two devices can be connected to your AirPods. If you are using with an Apple device like an iPad or Mac, then please connect that device first and then your Android.", + fontSize = 12.sp, + color = MaterialTheme.colorScheme.error, + lineHeight = 14.sp, + ) + } + } + + StyledSwitch( + checked = actAsAppleDevice, + onCheckedChange = { + if (!isProcessingSdp) { + actAsAppleDevice = it + isProcessingSdp = true + coroutineScope.launch { + if (it) { + val radareOffsetFinder = RadareOffsetFinder(context) + val success = radareOffsetFinder.findSdpOffset() ?: false + if (success) { + Toast.makeText(context, "Found offset please restart the Bluetooth process", Toast.LENGTH_LONG).show() + } + } else { + RadareOffsetFinder.clearSdpOffset() + } + isProcessingSdp = false + } + } + }, + enabled = !isProcessingSdp + ) + } } Spacer(modifier = Modifier.height(16.dp)) diff --git a/android/app/src/main/java/me/kavishdevar/librepods/utils/RadareOffsetFinder.kt b/android/app/src/main/java/me/kavishdevar/librepods/utils/RadareOffsetFinder.kt index e9a8c0f..6cc06b5 100644 --- a/android/app/src/main/java/me/kavishdevar/librepods/utils/RadareOffsetFinder.kt +++ b/android/app/src/main/java/me/kavishdevar/librepods/utils/RadareOffsetFinder.kt @@ -78,7 +78,7 @@ class RadareOffsetFinder(context: Context) { "setprop $HOOK_OFFSET_PROP '' && " + "setprop $CFG_REQ_OFFSET_PROP '' && " + "setprop $CSM_CONFIG_OFFSET_PROP '' && " + - "setprop $PEER_INFO_REQ_OFFSET_PROP ''" + + "setprop $PEER_INFO_REQ_OFFSET_PROP '' &&" + "setprop $SDP_OFFSET_PROP ''" )) val exitCode = process.waitFor() @@ -94,6 +94,44 @@ class RadareOffsetFinder(context: Context) { } return false } + + fun clearSdpOffset(): Boolean { + try { + val process = Runtime.getRuntime().exec(arrayOf( + "su", "-c", "setprop $SDP_OFFSET_PROP ''" + )) + val exitCode = process.waitFor() + + if (exitCode == 0) { + Log.d(TAG, "Successfully cleared SDP offset property") + return true + } else { + Log.e(TAG, "Failed to clear SDP offset property, exit code: $exitCode") + } + } catch (e: Exception) { + Log.e(TAG, "Error clearing SDP offset property", e) + } + return false + } + + fun isSdpOffsetAvailable(): Boolean { + try { + val process = Runtime.getRuntime().exec(arrayOf("getprop", SDP_OFFSET_PROP)) + val reader = BufferedReader(InputStreamReader(process.inputStream)) + val propValue = reader.readLine() + process.waitFor() + + if (propValue != null && propValue.isNotEmpty()) { + Log.d(TAG, "SDP offset property exists: $propValue") + return true + } + } catch (e: Exception) { + Log.e(TAG, "Error checking if SDP offset property exists", e) + } + + Log.d(TAG, "No SDP offset available") + return false + } } private val radare2TarballFile = File(context.cacheDir, "radare2.tar.gz") @@ -661,4 +699,57 @@ class RadareOffsetFinder(context: Context) { Log.e(TAG, "Failed to cleanup extracted files", e) } } + + suspend fun findSdpOffset(): Boolean = withContext(Dispatchers.IO) { + try { + _progressState.value = ProgressState.Downloading + if (!downloadRadare2TarballIfNeeded()) { + _progressState.value = ProgressState.Error("Failed to download radare2 tarball") + Log.e(TAG, "Failed to download radare2 tarball") + return@withContext false + } + + _progressState.value = ProgressState.Extracting + if (!extractRadare2Tarball()) { + _progressState.value = ProgressState.Error("Failed to extract radare2 tarball") + Log.e(TAG, "Failed to extract radare2 tarball") + return@withContext false + } + + _progressState.value = ProgressState.MakingExecutable + if (!makeExecutable()) { + _progressState.value = ProgressState.Error("Failed to make binaries executable") + Log.e(TAG, "Failed to make binaries executable") + return@withContext false + } + + _progressState.value = ProgressState.FindingOffset + val libraryPath = findBluetoothLibraryPath() + if (libraryPath == null) { + _progressState.value = ProgressState.Error("Failed to find Bluetooth library") + Log.e(TAG, "Failed to find Bluetooth library") + return@withContext false + } + + @Suppress("LocalVariableName") val currentLD_LIBRARY_PATH = ProcessBuilder().command("su", "-c", "printenv LD_LIBRARY_PATH").start().inputStream.bufferedReader().readText().trim() + val currentPATH = ProcessBuilder().command("su", "-c", "printenv PATH").start().inputStream.bufferedReader().readText().trim() + val envSetup = """ + export LD_LIBRARY_PATH="$RADARE2_LIB_PATH:$currentLD_LIBRARY_PATH" + export PATH="$BUSYBOX_PATH:$RADARE2_BIN_PATH:$currentPATH" + """.trimIndent() + + findAndSaveSdpOffset(libraryPath, envSetup) + + _progressState.value = ProgressState.Cleaning + cleanupExtractedFiles() + + _progressState.value = ProgressState.Success(0L) + return@withContext true + + } catch (e: Exception) { + _progressState.value = ProgressState.Error("Error: ${e.message}") + Log.e(TAG, "Error in findSdpOffset", e) + return@withContext false + } + } }