android: fix island not closing

This commit is contained in:
Kavish Devar
2025-05-20 22:31:53 +05:30
parent e852182b48
commit 5472e09293
2 changed files with 337 additions and 17 deletions

View File

@@ -38,6 +38,7 @@ import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.KeyboardArrowLeft
@@ -49,11 +50,15 @@ import androidx.compose.material3.CenterAlignedTopAppBar
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.OutlinedTextFieldDefaults
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Slider
import androidx.compose.material3.SliderDefaults
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TextField
import androidx.compose.material3.TextFieldDefaults
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
@@ -76,6 +81,8 @@ import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.Font
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
@@ -88,10 +95,13 @@ import dev.chrisbanes.haze.materials.CupertinoMaterials
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
import me.kavishdevar.librepods.R
import me.kavishdevar.librepods.composables.StyledSwitch
import me.kavishdevar.librepods.utils.AACPManager
import me.kavishdevar.librepods.utils.RadareOffsetFinder
import kotlin.io.encoding.Base64
import kotlin.io.encoding.ExperimentalEncodingApi
import kotlin.math.roundToInt
@OptIn(ExperimentalMaterial3Api::class, ExperimentalHazeMaterialsApi::class)
@OptIn(ExperimentalMaterial3Api::class, ExperimentalHazeMaterialsApi::class, ExperimentalEncodingApi::class)
@Composable
fun AppSettingsScreen(navController: NavController) {
val sharedPreferences = LocalContext.current.getSharedPreferences("settings", Context.MODE_PRIVATE)
@@ -103,6 +113,35 @@ fun AppSettingsScreen(navController: NavController) {
val hazeState = remember { HazeState() }
var showResetDialog by remember { mutableStateOf(false) }
var showIrkDialog by remember { mutableStateOf(false) }
var showEncKeyDialog by remember { mutableStateOf(false) }
var irkValue by remember { mutableStateOf("") }
var encKeyValue by remember { mutableStateOf("") }
var irkError by remember { mutableStateOf<String?>(null) }
var encKeyError by remember { mutableStateOf<String?>(null) }
LaunchedEffect(Unit) {
val savedIrk = sharedPreferences.getString(AACPManager.Companion.ProximityKeyType.IRK.name, null)
val savedEncKey = sharedPreferences.getString(AACPManager.Companion.ProximityKeyType.ENC_KEY.name, null)
if (savedIrk != null) {
try {
val decoded = Base64.decode(savedIrk)
irkValue = decoded.joinToString("") { "%02x".format(it) }
} catch (e: Exception) {
irkValue = ""
}
}
if (savedEncKey != null) {
try {
val decoded = Base64.decode(savedEncKey)
encKeyValue = decoded.joinToString("") { "%02x".format(it) }
} catch (e: Exception) {
encKeyValue = ""
}
}
}
var showPhoneBatteryInWidget by remember {
mutableStateOf(sharedPreferences.getBoolean("show_phone_battery_in_widget", true))
@@ -142,6 +181,11 @@ fun AppSettingsScreen(navController: NavController) {
var mDensity by remember { mutableFloatStateOf(0f) }
fun validateHexInput(input: String): Boolean {
val hexPattern = Regex("^[0-9a-fA-F]{32}$")
return hexPattern.matches(input)
}
Scaffold(
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
topBar = {
@@ -659,7 +703,6 @@ fun AppSettingsScreen(navController: NavController) {
modifier = Modifier.padding(top = 12.dp, bottom = 4.dp)
)
// Disconnected
Row(
modifier = Modifier
.fillMaxWidth()
@@ -701,7 +744,6 @@ fun AppSettingsScreen(navController: NavController) {
)
}
// Idle
Row(
modifier = Modifier
.fillMaxWidth()
@@ -743,7 +785,6 @@ fun AppSettingsScreen(navController: NavController) {
)
}
// Music
Row(
modifier = Modifier
.fillMaxWidth()
@@ -785,7 +826,6 @@ fun AppSettingsScreen(navController: NavController) {
)
}
// Call
Row(
modifier = Modifier
.fillMaxWidth()
@@ -837,7 +877,6 @@ fun AppSettingsScreen(navController: NavController) {
modifier = Modifier.padding(top = 8.dp, bottom = 4.dp)
)
// Ringing Call
Row(
modifier = Modifier
.fillMaxWidth()
@@ -879,7 +918,6 @@ fun AppSettingsScreen(navController: NavController) {
)
}
// Media Start
Row(
modifier = Modifier
.fillMaxWidth()
@@ -944,6 +982,64 @@ fun AppSettingsScreen(navController: NavController) {
)
.padding(horizontal = 16.dp, vertical = 4.dp)
) {
Row(
modifier = Modifier
.fillMaxWidth()
.clickable {
showIrkDialog = true
},
verticalAlignment = Alignment.CenterVertically
) {
Column(
modifier = Modifier
.weight(1f)
.padding(vertical = 8.dp)
.padding(end = 4.dp)
) {
Text(
text = "Set Identity Resolving Key (IRK)",
fontSize = 16.sp,
color = textColor
)
Spacer(modifier = Modifier.height(4.dp))
Text(
text = "Manually set the IRK value used for resolving BLE random addresses",
fontSize = 14.sp,
color = textColor.copy(0.6f),
lineHeight = 16.sp,
)
}
}
Row(
modifier = Modifier
.fillMaxWidth()
.clickable {
showEncKeyDialog = true
},
verticalAlignment = Alignment.CenterVertically
) {
Column(
modifier = Modifier
.weight(1f)
.padding(vertical = 8.dp)
.padding(end = 4.dp)
) {
Text(
text = "Set Encryption Key",
fontSize = 16.sp,
color = textColor
)
Spacer(modifier = Modifier.height(4.dp))
Text(
text = "Manually set the ENC_KEY value used for decrypting BLE advertisements",
fontSize = 14.sp,
color = textColor.copy(0.6f),
lineHeight = 16.sp,
)
}
}
Row(
modifier = Modifier
.fillMaxWidth()
@@ -1073,6 +1169,184 @@ fun AppSettingsScreen(navController: NavController) {
}
)
}
if (showIrkDialog) {
AlertDialog(
onDismissRequest = { showIrkDialog = false },
title = {
Text(
"Set Identity Resolving Key (IRK)",
fontFamily = FontFamily(Font(R.font.sf_pro)),
fontWeight = FontWeight.Medium
)
},
text = {
Column {
Text(
"Enter 16-byte IRK as hex string (32 characters):",
fontFamily = FontFamily(Font(R.font.sf_pro)),
modifier = Modifier.padding(bottom = 8.dp)
)
OutlinedTextField(
value = irkValue,
onValueChange = {
irkValue = it.lowercase().filter { char -> char.isDigit() || char in 'a'..'f' }
irkError = null
},
modifier = Modifier.fillMaxWidth(),
isError = irkError != null,
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Ascii,
capitalization = KeyboardCapitalization.None
),
colors = OutlinedTextFieldDefaults.colors(
focusedBorderColor = if (isDarkTheme) Color(0xFF007AFF) else Color(0xFF3C6DF5),
unfocusedBorderColor = if (isDarkTheme) Color.Gray else Color.LightGray
),
supportingText = {
if (irkError != null) {
Text(irkError!!, color = MaterialTheme.colorScheme.error)
}
},
label = { Text("IRK Hex Value") }
)
}
},
confirmButton = {
TextButton(
onClick = {
if (!validateHexInput(irkValue)) {
irkError = "Must be exactly 32 hex characters"
return@TextButton
}
try {
val hexBytes = ByteArray(16)
for (i in 0 until 16) {
val hexByte = irkValue.substring(i * 2, i * 2 + 2)
hexBytes[i] = hexByte.toInt(16).toByte()
}
val base64Value = Base64.encode(hexBytes)
sharedPreferences.edit().putString(AACPManager.Companion.ProximityKeyType.IRK.name, base64Value).apply()
Toast.makeText(context, "IRK has been set successfully", Toast.LENGTH_SHORT).show()
showIrkDialog = false
} catch (e: Exception) {
irkError = "Error converting hex: ${e.message}"
}
}
) {
Text(
"Save",
fontFamily = FontFamily(Font(R.font.sf_pro)),
fontWeight = FontWeight.Medium
)
}
},
dismissButton = {
TextButton(
onClick = { showIrkDialog = false }
) {
Text(
"Cancel",
fontFamily = FontFamily(Font(R.font.sf_pro)),
fontWeight = FontWeight.Medium
)
}
}
)
}
if (showEncKeyDialog) {
AlertDialog(
onDismissRequest = { showEncKeyDialog = false },
title = {
Text(
"Set Encryption Key",
fontFamily = FontFamily(Font(R.font.sf_pro)),
fontWeight = FontWeight.Medium
)
},
text = {
Column {
Text(
"Enter 16-byte ENC_KEY as hex string (32 characters):",
fontFamily = FontFamily(Font(R.font.sf_pro)),
modifier = Modifier.padding(bottom = 8.dp)
)
OutlinedTextField(
value = encKeyValue,
onValueChange = {
encKeyValue = it.lowercase().filter { char -> char.isDigit() || char in 'a'..'f' }
encKeyError = null
},
modifier = Modifier.fillMaxWidth(),
isError = encKeyError != null,
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Ascii,
capitalization = KeyboardCapitalization.None
),
colors = OutlinedTextFieldDefaults.colors(
focusedBorderColor = if (isDarkTheme) Color(0xFF007AFF) else Color(0xFF3C6DF5),
unfocusedBorderColor = if (isDarkTheme) Color.Gray else Color.LightGray
),
supportingText = {
if (encKeyError != null) {
Text(encKeyError!!, color = MaterialTheme.colorScheme.error)
}
},
label = { Text("ENC_KEY Hex Value") }
)
}
},
confirmButton = {
TextButton(
onClick = {
if (!validateHexInput(encKeyValue)) {
encKeyError = "Must be exactly 32 hex characters"
return@TextButton
}
try {
val hexBytes = ByteArray(16)
for (i in 0 until 16) {
val hexByte = encKeyValue.substring(i * 2, i * 2 + 2)
hexBytes[i] = hexByte.toInt(16).toByte()
}
val base64Value = Base64.encode(hexBytes)
sharedPreferences.edit().putString(AACPManager.Companion.ProximityKeyType.ENC_KEY.name, base64Value).apply()
Toast.makeText(context, "Encryption key has been set successfully", Toast.LENGTH_SHORT).show()
showEncKeyDialog = false
} catch (e: Exception) {
encKeyError = "Error converting hex: ${e.message}"
}
}
) {
Text(
"Save",
fontFamily = FontFamily(Font(R.font.sf_pro)),
fontWeight = FontWeight.Medium
)
}
},
dismissButton = {
TextButton(
onClick = { showEncKeyDialog = false }
) {
Text(
"Cancel",
fontFamily = FontFamily(Font(R.font.sf_pro)),
fontWeight = FontWeight.Medium
)
}
}
)
}
}
}
}

View File

@@ -263,6 +263,9 @@ class IslandWindow(private val context: Context) {
if (abs(deltaY) > 5 || isBeingDragged) {
isBeingDragged = true
// Cancel auto close timer when dragging starts
autoCloseHandler?.removeCallbacks(autoCloseRunnable ?: return@setOnTouchListener false)
val dampedDeltaY = if (deltaY > 0) {
initialY + (deltaY * 0.6f)
} else {
@@ -417,6 +420,7 @@ class IslandWindow(private val context: Context) {
}
private fun resetAutoCloseTimer() {
autoCloseHandler?.removeCallbacks(autoCloseRunnable ?: return)
autoCloseHandler = Handler(Looper.getMainLooper())
autoCloseRunnable = Runnable { close() }
autoCloseHandler?.postDelayed(autoCloseRunnable!!, 4500)
@@ -501,7 +505,7 @@ class IslandWindow(private val context: Context) {
}
flingAnimator.addListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) {
close()
forceClose()
}
})
@@ -556,7 +560,7 @@ class IslandWindow(private val context: Context) {
normalizeAnimator.addListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) {
ServiceManager.getService()?.startMainActivity()
close()
forceClose()
}
})
@@ -611,7 +615,12 @@ class IslandWindow(private val context: Context) {
resetStretchEffects(0f)
val videoView = islandView.findViewById<VideoView>(R.id.island_video_view)
videoView.stopPlayback()
try {
videoView.stopPlayback()
} catch (e: Exception) {
e.printStackTrace()
}
val scaleX = PropertyValuesHolder.ofFloat(View.SCALE_X, containerView.scaleX, 0.5f)
val scaleY = PropertyValuesHolder.ofFloat(View.SCALE_Y, containerView.scaleY, 0.5f)
val translationY = PropertyValuesHolder.ofFloat(View.TRANSLATION_Y, containerView.translationY, -200f)
@@ -620,19 +629,56 @@ class IslandWindow(private val context: Context) {
interpolator = AnticipateOvershootInterpolator()
addListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) {
containerView.visibility = View.GONE
try {
windowManager.removeView(containerView)
} catch (e: Exception) {
e("IslandWindow", "Error removing view: $e")
}
isClosing = false
cleanupAndRemoveView()
}
})
start()
}
} catch (e: Exception) {
e.printStackTrace()
// Even if animation fails, ensure we cleanup
cleanupAndRemoveView()
}
}
private fun cleanupAndRemoveView() {
containerView.visibility = View.GONE
try {
if (containerView.parent != null) {
windowManager.removeView(containerView)
}
} catch (e: Exception) {
e("IslandWindow", "Error removing view: $e")
}
isClosing = false
// Make sure all animations are canceled
springAnimation.cancel()
flingAnimator.cancel()
}
fun forceClose() {
try {
if (isClosing) return
isClosing = true
try {
context.unregisterReceiver(batteryReceiver)
} catch (e: Exception) {
// Silent catch - receiver might already be unregistered
}
ServiceManager.getService()?.islandOpen = false
autoCloseHandler?.removeCallbacks(autoCloseRunnable ?: return)
// Cancel all ongoing animations
springAnimation.cancel()
flingAnimator.cancel()
// Immediately remove the view without animations
cleanupAndRemoveView()
} catch (e: Exception) {
e.printStackTrace()
isClosing = false
}
}
}