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.rememberScrollState
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardOptions
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.automirrored.filled.KeyboardArrowLeft 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.ExperimentalMaterial3Api
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.OutlinedTextFieldDefaults
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.Slider import androidx.compose.material3.Slider
import androidx.compose.material3.SliderDefaults import androidx.compose.material3.SliderDefaults
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
import androidx.compose.material3.TextField
import androidx.compose.material3.TextFieldDefaults
import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect 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.Font
import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight 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.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
@@ -88,10 +95,13 @@ import dev.chrisbanes.haze.materials.CupertinoMaterials
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
import me.kavishdevar.librepods.R import me.kavishdevar.librepods.R
import me.kavishdevar.librepods.composables.StyledSwitch import me.kavishdevar.librepods.composables.StyledSwitch
import me.kavishdevar.librepods.utils.AACPManager
import me.kavishdevar.librepods.utils.RadareOffsetFinder import me.kavishdevar.librepods.utils.RadareOffsetFinder
import kotlin.io.encoding.Base64
import kotlin.io.encoding.ExperimentalEncodingApi
import kotlin.math.roundToInt import kotlin.math.roundToInt
@OptIn(ExperimentalMaterial3Api::class, ExperimentalHazeMaterialsApi::class) @OptIn(ExperimentalMaterial3Api::class, ExperimentalHazeMaterialsApi::class, ExperimentalEncodingApi::class)
@Composable @Composable
fun AppSettingsScreen(navController: NavController) { fun AppSettingsScreen(navController: NavController) {
val sharedPreferences = LocalContext.current.getSharedPreferences("settings", Context.MODE_PRIVATE) val sharedPreferences = LocalContext.current.getSharedPreferences("settings", Context.MODE_PRIVATE)
@@ -103,6 +113,35 @@ fun AppSettingsScreen(navController: NavController) {
val hazeState = remember { HazeState() } val hazeState = remember { HazeState() }
var showResetDialog by remember { mutableStateOf(false) } 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 { var showPhoneBatteryInWidget by remember {
mutableStateOf(sharedPreferences.getBoolean("show_phone_battery_in_widget", true)) mutableStateOf(sharedPreferences.getBoolean("show_phone_battery_in_widget", true))
@@ -142,6 +181,11 @@ fun AppSettingsScreen(navController: NavController) {
var mDensity by remember { mutableFloatStateOf(0f) } var mDensity by remember { mutableFloatStateOf(0f) }
fun validateHexInput(input: String): Boolean {
val hexPattern = Regex("^[0-9a-fA-F]{32}$")
return hexPattern.matches(input)
}
Scaffold( Scaffold(
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
topBar = { topBar = {
@@ -659,7 +703,6 @@ fun AppSettingsScreen(navController: NavController) {
modifier = Modifier.padding(top = 12.dp, bottom = 4.dp) modifier = Modifier.padding(top = 12.dp, bottom = 4.dp)
) )
// Disconnected
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
@@ -701,7 +744,6 @@ fun AppSettingsScreen(navController: NavController) {
) )
} }
// Idle
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
@@ -743,7 +785,6 @@ fun AppSettingsScreen(navController: NavController) {
) )
} }
// Music
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
@@ -785,7 +826,6 @@ fun AppSettingsScreen(navController: NavController) {
) )
} }
// Call
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
@@ -837,7 +877,6 @@ fun AppSettingsScreen(navController: NavController) {
modifier = Modifier.padding(top = 8.dp, bottom = 4.dp) modifier = Modifier.padding(top = 8.dp, bottom = 4.dp)
) )
// Ringing Call
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
@@ -879,7 +918,6 @@ fun AppSettingsScreen(navController: NavController) {
) )
} }
// Media Start
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
@@ -944,6 +982,64 @@ fun AppSettingsScreen(navController: NavController) {
) )
.padding(horizontal = 16.dp, vertical = 4.dp) .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( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .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) { if (abs(deltaY) > 5 || isBeingDragged) {
isBeingDragged = true isBeingDragged = true
// Cancel auto close timer when dragging starts
autoCloseHandler?.removeCallbacks(autoCloseRunnable ?: return@setOnTouchListener false)
val dampedDeltaY = if (deltaY > 0) { val dampedDeltaY = if (deltaY > 0) {
initialY + (deltaY * 0.6f) initialY + (deltaY * 0.6f)
} else { } else {
@@ -417,6 +420,7 @@ class IslandWindow(private val context: Context) {
} }
private fun resetAutoCloseTimer() { private fun resetAutoCloseTimer() {
autoCloseHandler?.removeCallbacks(autoCloseRunnable ?: return)
autoCloseHandler = Handler(Looper.getMainLooper()) autoCloseHandler = Handler(Looper.getMainLooper())
autoCloseRunnable = Runnable { close() } autoCloseRunnable = Runnable { close() }
autoCloseHandler?.postDelayed(autoCloseRunnable!!, 4500) autoCloseHandler?.postDelayed(autoCloseRunnable!!, 4500)
@@ -501,7 +505,7 @@ class IslandWindow(private val context: Context) {
} }
flingAnimator.addListener(object : AnimatorListenerAdapter() { flingAnimator.addListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) { override fun onAnimationEnd(animation: Animator) {
close() forceClose()
} }
}) })
@@ -556,7 +560,7 @@ class IslandWindow(private val context: Context) {
normalizeAnimator.addListener(object : AnimatorListenerAdapter() { normalizeAnimator.addListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) { override fun onAnimationEnd(animation: Animator) {
ServiceManager.getService()?.startMainActivity() ServiceManager.getService()?.startMainActivity()
close() forceClose()
} }
}) })
@@ -611,7 +615,12 @@ class IslandWindow(private val context: Context) {
resetStretchEffects(0f) resetStretchEffects(0f)
val videoView = islandView.findViewById<VideoView>(R.id.island_video_view) 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 scaleX = PropertyValuesHolder.ofFloat(View.SCALE_X, containerView.scaleX, 0.5f)
val scaleY = PropertyValuesHolder.ofFloat(View.SCALE_Y, containerView.scaleY, 0.5f) val scaleY = PropertyValuesHolder.ofFloat(View.SCALE_Y, containerView.scaleY, 0.5f)
val translationY = PropertyValuesHolder.ofFloat(View.TRANSLATION_Y, containerView.translationY, -200f) val translationY = PropertyValuesHolder.ofFloat(View.TRANSLATION_Y, containerView.translationY, -200f)
@@ -620,19 +629,56 @@ class IslandWindow(private val context: Context) {
interpolator = AnticipateOvershootInterpolator() interpolator = AnticipateOvershootInterpolator()
addListener(object : AnimatorListenerAdapter() { addListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) { override fun onAnimationEnd(animation: Animator) {
containerView.visibility = View.GONE cleanupAndRemoveView()
try {
windowManager.removeView(containerView)
} catch (e: Exception) {
e("IslandWindow", "Error removing view: $e")
}
isClosing = false
} }
}) })
start() start()
} }
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() 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
} }
} }
} }