android: add toggle for DID hook

This commit is contained in:
Kavish Devar
2025-09-15 19:59:43 +05:30
parent 9e6d97198b
commit 5bef8c384e
2 changed files with 191 additions and 2 deletions

View File

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

View File

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