mirror of
https://github.com/kavishdevar/librepods.git
synced 2026-01-28 22:01:50 +00:00
android: fix island not closing
This commit is contained in:
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user