fix: add detailed logging for unlock process to improve debugging and performance tracking

This commit is contained in:
2026-02-05 01:56:18 +05:00
parent e307e8d35d
commit 4dc7c6e9bc
13 changed files with 640 additions and 169 deletions

View File

@@ -0,0 +1,430 @@
package com.rosetta.messenger.ui.settings
import android.content.Context
import android.view.inputmethod.InputMethodManager
import androidx.compose.animation.*
import androidx.compose.animation.core.*
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.airbnb.lottie.compose.*
import com.rosetta.messenger.ui.onboarding.PrimaryBlue
import compose.icons.TablerIcons
import compose.icons.tablericons.*
import kotlinx.coroutines.delay
// Auth colors
private val AuthBackground = Color(0xFF1A1A1A)
private val AuthBackgroundLight = Color(0xFFFFFFFF)
private val AuthSurface = Color(0xFF2C2C2E)
private val AuthSurfaceLight = Color(0xFFF2F2F7)
/**
* Biometric Enable Screen - структура как в auth flow
*/
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun BiometricEnableScreen(
isDarkTheme: Boolean,
onBack: () -> Unit,
onEnable: (password: String, onSuccess: () -> Unit, onError: (String) -> Unit) -> Unit
) {
// Theme animation
val themeAnimSpec = tween<Color>(durationMillis = 500, easing = CubicBezierEasing(0.4f, 0f, 0.2f, 1f))
val backgroundColor by animateColorAsState(
if (isDarkTheme) AuthBackground else AuthBackgroundLight,
animationSpec = themeAnimSpec,
label = "bg"
)
val textColor by animateColorAsState(
if (isDarkTheme) Color.White else Color.Black,
animationSpec = themeAnimSpec,
label = "text"
)
val secondaryTextColor by animateColorAsState(
if (isDarkTheme) Color(0xFF8E8E93) else Color(0xFF666666),
animationSpec = themeAnimSpec,
label = "secondary"
)
val cardColor by animateColorAsState(
if (isDarkTheme) AuthSurface else AuthSurfaceLight,
animationSpec = themeAnimSpec,
label = "card"
)
val errorRed = Color(0xFFFF3B30)
// State
var password by remember { mutableStateOf("") }
var passwordVisible by remember { mutableStateOf(false) }
var passwordError by remember { mutableStateOf<String?>(null) }
var isLoading by remember { mutableStateOf(false) }
var showSuccess by remember { mutableStateOf(false) }
var visible by remember { mutableStateOf(false) }
val focusManager = LocalFocusManager.current
val context = LocalContext.current
val view = LocalView.current
// Function to hide keyboard
fun hideKeyboard() {
focusManager.clearFocus()
val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
imm.hideSoftInputFromWindow(view.windowToken, 0)
}
// Track keyboard visibility
var isKeyboardVisible by remember { mutableStateOf(false) }
DisposableEffect(view) {
val listener = android.view.ViewTreeObserver.OnGlobalLayoutListener {
val rect = android.graphics.Rect()
view.getWindowVisibleDisplayFrame(rect)
val screenHeight = view.rootView.height
val keypadHeight = screenHeight - rect.bottom
isKeyboardVisible = keypadHeight > screenHeight * 0.15
}
view.viewTreeObserver.addOnGlobalLayoutListener(listener)
onDispose { view.viewTreeObserver.removeOnGlobalLayoutListener(listener) }
}
// Lottie animation - одноразовая
val lockComposition by rememberLottieComposition(
LottieCompositionSpec.Asset("lottie/lock.json")
)
val lockProgress by animateLottieCompositionAsState(
composition = lockComposition,
iterations = 1,
speed = 0.8f
)
LaunchedEffect(Unit) { visible = true }
// Handle success - close screen after delay
LaunchedEffect(showSuccess) {
if (showSuccess) {
delay(1500)
onBack()
}
}
// Animated sizes
val lottieSize by animateDpAsState(
targetValue = if (isKeyboardVisible) 80.dp else 160.dp,
animationSpec = tween(300, easing = FastOutSlowInEasing),
label = "lottie"
)
Box(modifier = Modifier.fillMaxSize().background(backgroundColor).navigationBarsPadding()) {
Column(modifier = Modifier.fillMaxSize().statusBarsPadding()) {
// Top Bar - как в auth flow
Row(
modifier = Modifier.fillMaxWidth().padding(horizontal = 8.dp, vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically
) {
IconButton(onClick = onBack, enabled = !isLoading) {
Icon(
imageVector = TablerIcons.ArrowLeft,
contentDescription = "Back",
tint = textColor.copy(alpha = 0.6f)
)
}
Spacer(modifier = Modifier.weight(1f))
Text(
text = "Biometric",
fontSize = 18.sp,
fontWeight = FontWeight.SemiBold,
color = textColor
)
Spacer(modifier = Modifier.weight(1f))
Spacer(modifier = Modifier.width(48.dp))
}
// Content
Column(
modifier = Modifier
.fillMaxSize()
.imePadding()
.padding(horizontal = 24.dp)
.verticalScroll(rememberScrollState()),
horizontalAlignment = Alignment.CenterHorizontally
) {
Spacer(modifier = Modifier.height(if (isKeyboardVisible) 8.dp else 16.dp))
// Lottie Animation
AnimatedVisibility(
visible = visible,
enter = fadeIn(tween(250)) + scaleIn(
initialScale = 0.5f,
animationSpec = tween(250, easing = FastOutSlowInEasing)
)
) {
Box(
modifier = Modifier.size(lottieSize),
contentAlignment = Alignment.Center
) {
if (lockComposition != null) {
LottieAnimation(
composition = lockComposition,
progress = { lockProgress },
modifier = Modifier
.fillMaxSize()
.graphicsLayer {
compositingStrategy = androidx.compose.ui.graphics.CompositingStrategy.Offscreen
},
maintainOriginalImageBounds = true
)
}
}
}
Spacer(modifier = Modifier.height(if (isKeyboardVisible) 12.dp else 24.dp))
// Title
AnimatedVisibility(
visible = visible,
enter = fadeIn(tween(400, delayMillis = 100))
) {
Text(
text = if (showSuccess) "Biometric Enabled!" else "Enable Biometric",
fontSize = if (isKeyboardVisible) 20.sp else 24.sp,
fontWeight = FontWeight.Bold,
color = if (showSuccess) PrimaryBlue else textColor
)
}
Spacer(modifier = Modifier.height(if (isKeyboardVisible) 6.dp else 8.dp))
// Subtitle
AnimatedVisibility(
visible = visible && !showSuccess,
enter = fadeIn(tween(500, delayMillis = 200)),
exit = fadeOut(tween(200))
) {
Text(
text = "Use Face ID or Touch ID to quickly\nunlock your account.",
fontSize = if (isKeyboardVisible) 12.sp else 14.sp,
color = secondaryTextColor,
textAlign = TextAlign.Center,
lineHeight = if (isKeyboardVisible) 16.sp else 20.sp
)
}
// Success message
AnimatedVisibility(
visible = showSuccess,
enter = fadeIn(tween(300)) + expandVertically()
) {
Text(
text = "You can now unlock with biometrics",
fontSize = 14.sp,
color = secondaryTextColor,
textAlign = TextAlign.Center
)
}
Spacer(modifier = Modifier.height(if (isKeyboardVisible) 16.dp else 32.dp))
// Password Field
AnimatedVisibility(
visible = visible && !showSuccess,
enter = fadeIn(tween(400, delayMillis = 300)),
exit = fadeOut(tween(200))
) {
Column(modifier = Modifier.fillMaxWidth()) {
OutlinedTextField(
value = password,
onValueChange = {
password = it
passwordError = null
},
modifier = Modifier.fillMaxWidth(),
placeholder = {
Text(
"Enter your password",
color = secondaryTextColor.copy(alpha = 0.6f)
)
},
leadingIcon = {
Icon(
imageVector = TablerIcons.Lock,
contentDescription = null,
tint = if (passwordError != null) errorRed else secondaryTextColor,
modifier = Modifier.size(22.dp)
)
},
trailingIcon = {
IconButton(onClick = { passwordVisible = !passwordVisible }) {
Icon(
imageVector = if (passwordVisible) TablerIcons.EyeOff else TablerIcons.Eye,
contentDescription = if (passwordVisible) "Hide" else "Show",
tint = secondaryTextColor,
modifier = Modifier.size(22.dp)
)
}
},
visualTransformation = if (passwordVisible) VisualTransformation.None else PasswordVisualTransformation(),
isError = passwordError != null,
singleLine = true,
shape = RoundedCornerShape(14.dp),
colors = OutlinedTextFieldDefaults.colors(
focusedBorderColor = PrimaryBlue,
unfocusedBorderColor = if (isDarkTheme) Color(0xFF48484A) else Color(0xFFD1D1D6),
focusedContainerColor = cardColor,
unfocusedContainerColor = cardColor,
focusedTextColor = textColor,
unfocusedTextColor = textColor,
cursorColor = PrimaryBlue,
errorBorderColor = errorRed,
errorContainerColor = cardColor
),
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Password,
imeAction = ImeAction.Done
)
)
// Error message
AnimatedVisibility(
visible = passwordError != null,
enter = fadeIn() + expandVertically(),
exit = fadeOut() + shrinkVertically()
) {
Row(
modifier = Modifier.padding(top = 8.dp, start = 4.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
imageVector = TablerIcons.AlertCircle,
contentDescription = null,
tint = errorRed,
modifier = Modifier.size(16.dp)
)
Spacer(modifier = Modifier.width(6.dp))
Text(
text = passwordError ?: "",
color = errorRed,
fontSize = 13.sp
)
}
}
}
}
Spacer(modifier = Modifier.height(24.dp))
// Enable button
AnimatedVisibility(
visible = visible && !showSuccess,
enter = fadeIn(tween(400, delayMillis = 400)),
exit = fadeOut(tween(200))
) {
Button(
onClick = {
if (password.isEmpty()) {
passwordError = "Please enter your password"
return@Button
}
hideKeyboard()
isLoading = true
onEnable(
password,
{
isLoading = false
showSuccess = true
},
{ error ->
isLoading = false
passwordError = error
}
)
},
enabled = password.isNotEmpty() && !isLoading,
modifier = Modifier
.fillMaxWidth()
.height(54.dp),
shape = RoundedCornerShape(14.dp),
colors = ButtonDefaults.buttonColors(
containerColor = PrimaryBlue,
contentColor = Color.White,
disabledContainerColor = PrimaryBlue.copy(alpha = 0.4f),
disabledContentColor = Color.White.copy(alpha = 0.6f)
)
) {
if (isLoading) {
CircularProgressIndicator(
modifier = Modifier.size(22.dp),
color = Color.White,
strokeWidth = 2.dp
)
} else {
Icon(
imageVector = TablerIcons.Fingerprint,
contentDescription = null,
modifier = Modifier.size(22.dp)
)
Spacer(modifier = Modifier.width(10.dp))
Text(
text = "Enable Biometric",
fontSize = 16.sp,
fontWeight = FontWeight.SemiBold
)
}
}
}
Spacer(modifier = Modifier.weight(1f))
// Info - hide when keyboard visible
AnimatedVisibility(
visible = visible && !isKeyboardVisible && !showSuccess,
enter = fadeIn(tween(300)),
exit = fadeOut(tween(200))
) {
Row(
modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(12.dp))
.background(cardColor)
.padding(16.dp),
verticalAlignment = Alignment.Top
) {
Icon(
imageVector = TablerIcons.ShieldLock,
contentDescription = null,
tint = PrimaryBlue,
modifier = Modifier.size(20.dp)
)
Spacer(modifier = Modifier.width(12.dp))
Text(
text = "Your password is encrypted and stored securely on this device only.",
fontSize = 13.sp,
color = secondaryTextColor,
lineHeight = 18.sp
)
}
}
Spacer(modifier = Modifier.height(32.dp))
}
}
}
}

View File

@@ -191,6 +191,7 @@ fun ProfileScreen(
onNavigateToSafety: () -> Unit = {},
onNavigateToLogs: () -> Unit = {},
onNavigateToCrashLogs: () -> Unit = {},
onNavigateToBiometric: () -> Unit = {},
viewModel: ProfileViewModel = androidx.lifecycle.viewmodel.compose.viewModel(),
avatarRepository: AvatarRepository? = null,
dialogDao: com.rosetta.messenger.database.DialogDao? = null
@@ -198,22 +199,20 @@ fun ProfileScreen(
val context = LocalContext.current
val activity = context as? FragmentActivity
val biometricManager = remember { BiometricAuthManager(context) }
val biometricPrefs = remember { BiometricPreferences(context) }
val scope = rememberCoroutineScope()
// Biometric state
// Biometric availability
var biometricAvailable by remember {
mutableStateOf<BiometricAvailability>(BiometricAvailability.NotAvailable("Checking..."))
}
var isBiometricEnabled by remember { mutableStateOf(false) }
var showPasswordDialog by remember { mutableStateOf(false) }
var passwordInput by remember { mutableStateOf("") }
var passwordError by remember { mutableStateOf<String?>(null) }
// Biometric enabled state - read directly to always show current state
val biometricPrefs = remember { BiometricPreferences(context) }
val isBiometricEnabled by biometricPrefs.isBiometricEnabled.collectAsState(initial = false)
// Check biometric availability
LaunchedEffect(Unit) {
biometricAvailable = biometricManager.isBiometricAvailable()
isBiometricEnabled = biometricPrefs.isBiometricEnabled.first()
}
// Состояние меню аватара для установки фото профиля
@@ -627,32 +626,17 @@ fun ProfileScreen(
showDivider = biometricAvailable is BiometricAvailability.Available
)
// Biometric toggle (only show if available)
// Biometric settings (only show if available)
if (biometricAvailable is BiometricAvailability.Available && activity != null) {
TelegramBiometricItem(
isEnabled = isBiometricEnabled,
onToggle = {
if (isBiometricEnabled) {
// Disable biometric
scope.launch {
biometricPrefs.disableBiometric()
biometricPrefs.removeEncryptedPassword(accountPublicKey)
isBiometricEnabled = false
android.widget.Toast.makeText(
context,
"Biometric authentication disabled",
android.widget.Toast.LENGTH_SHORT
)
.show()
}
} else {
// Enable biometric - show password dialog first
passwordInput = ""
passwordError = null
showPasswordDialog = true
}
TelegramSettingsItem(
icon = TablerIcons.Fingerprint,
title = "Biometric Authentication",
onClick = {
onNavigateToBiometric()
},
isDarkTheme = isDarkTheme
isDarkTheme = isDarkTheme,
showDivider = false,
subtitle = if (isBiometricEnabled) "Enabled" else "Disabled"
)
}
@@ -713,96 +697,6 @@ fun ProfileScreen(
)
}
// Password dialog for biometric setup
if (showPasswordDialog && activity != null) {
AlertDialog(
onDismissRequest = {
showPasswordDialog = false
passwordInput = ""
passwordError = null
},
title = { Text("Enable Biometric Authentication") },
text = {
Column {
Text("Enter your password to securely save it for biometric unlock:")
Spacer(modifier = Modifier.height(16.dp))
OutlinedTextField(
value = passwordInput,
onValueChange = {
passwordInput = it
passwordError = null
},
label = { Text("Password") },
singleLine = true,
visualTransformation =
androidx.compose.ui.text.input
.PasswordVisualTransformation(),
isError = passwordError != null,
modifier = Modifier.fillMaxWidth()
)
if (passwordError != null) {
Spacer(modifier = Modifier.height(4.dp))
Text(text = passwordError!!, color = Color.Red, fontSize = 12.sp)
}
}
},
confirmButton = {
TextButton(
onClick = {
if (passwordInput.isEmpty()) {
passwordError = "Password cannot be empty"
return@TextButton
}
// Try to encrypt the password with biometric
biometricManager.encryptPassword(
activity = activity,
password = passwordInput,
onSuccess = { encryptedPassword ->
scope.launch {
// Save encrypted password
biometricPrefs.saveEncryptedPassword(
accountPublicKey,
encryptedPassword
)
// Enable biometric
biometricPrefs.enableBiometric()
isBiometricEnabled = true
showPasswordDialog = false
passwordInput = ""
passwordError = null
android.widget.Toast.makeText(
context,
"Biometric authentication enabled successfully",
android.widget.Toast.LENGTH_SHORT
)
.show()
}
},
onError = { error -> passwordError = error },
onCancel = {
showPasswordDialog = false
passwordInput = ""
passwordError = null
}
)
}
) { Text("Enable") }
},
dismissButton = {
TextButton(
onClick = {
showPasswordDialog = false
passwordInput = ""
passwordError = null
}
) { Text("Cancel") }
}
)
}
// 🖼️ Кастомный быстрый Photo Picker
ProfilePhotoPicker(
isVisible = showPhotoPicker,
@@ -1342,9 +1236,11 @@ private fun TelegramSettingsItem(
title: String,
onClick: () -> Unit,
isDarkTheme: Boolean,
showDivider: Boolean = false
showDivider: Boolean = false,
subtitle: String? = null
) {
val textColor = if (isDarkTheme) Color.White else Color.Black
val secondaryTextColor = if (isDarkTheme) Color(0xFF8E8E93) else Color(0xFF666666)
val iconColor = if (isDarkTheme) Color(0xFF8E8E93) else Color(0xFF666666)
val dividerColor = if (isDarkTheme) Color(0xFF3A3A3A) else Color(0xFFE0E0E0)
@@ -1365,7 +1261,13 @@ private fun TelegramSettingsItem(
Spacer(modifier = Modifier.width(20.dp))
Text(text = title, fontSize = 16.sp, color = textColor, modifier = Modifier.weight(1f))
Column(modifier = Modifier.weight(1f)) {
Text(text = title, fontSize = 16.sp, color = textColor)
if (subtitle != null) {
Spacer(modifier = Modifier.height(2.dp))
Text(text = subtitle, fontSize = 13.sp, color = secondaryTextColor)
}
}
}
if (showDivider) {