package com.jovan.assistant.ui import android.Manifest import android.content.Intent import android.net.Uri import android.os.Build import android.provider.Settings import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.Logout import androidx.compose.material3.AlertDialog import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedButton import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue 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.platform.LocalContext import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.core.content.ContextCompat import androidx.lifecycle.Lifecycle import androidx.lifecycle.compose.LifecycleEventEffect import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.jovan.assistant.data.AssistantState import com.jovan.assistant.data.FirebaseRepo import com.jovan.assistant.data.LogEntry import com.jovan.assistant.data.Prefs import com.jovan.assistant.data.WakeStatus import com.jovan.assistant.security.SecurityGuard import com.jovan.assistant.service.VoiceService import kotlinx.coroutines.launch import java.text.SimpleDateFormat import java.util.Date import java.util.Locale private val Green = Color(0xFF3DDC97) private val Amber = Color(0xFFFFB84D) private val Red = Color(0xFFFF6B6B) private val Grey = Color(0xFF6B7A99) @OptIn(ExperimentalMaterial3Api::class) @Composable fun DashboardScreen(uid: String, email: String) { val ctx = LocalContext.current val scope = rememberCoroutineScope() val wakeStatus by AssistantState.wakeStatus.collectAsStateWithLifecycle() val lastHeard by AssistantState.lastHeard.collectAsStateWithLifecycle() val a11y by AssistantState.accessibilityConnected.collectAsStateWithLifecycle() val connected by remember { FirebaseRepo.connectedFlow() }.collectAsStateWithLifecycle(false) val logs by remember(uid) { FirebaseRepo.logsFlow(uid) }.collectAsStateWithLifecycle(emptyList()) var overlayOk by remember { mutableStateOf(Settings.canDrawOverlays(ctx)) } var micOk by remember { mutableStateOf( ContextCompat.checkSelfPermission(ctx, Manifest.permission.RECORD_AUDIO) == android.content.pm.PackageManager.PERMISSION_GRANTED ) } LifecycleEventEffect(Lifecycle.Event.ON_RESUME) { overlayOk = Settings.canDrawOverlays(ctx) micOk = ContextCompat.checkSelfPermission(ctx, Manifest.permission.RECORD_AUDIO) == android.content.pm.PackageManager.PERMISSION_GRANTED } var wakeInput by remember { mutableStateOf(Prefs.wakeWord) } var showUnblock by remember { mutableStateOf(false) } val running = wakeStatus != WakeStatus.OFF val permLauncher = rememberLauncherForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { r -> micOk = r[Manifest.permission.RECORD_AUDIO] == true || micOk if (r[Manifest.permission.RECORD_AUDIO] == true) VoiceService.start(ctx) } fun toggleService() { if (running) { VoiceService.stop(ctx); return } if (micOk) { VoiceService.start(ctx); return } val perms = buildList { add(Manifest.permission.RECORD_AUDIO) if (Build.VERSION.SDK_INT >= 33) add(Manifest.permission.POST_NOTIFICATIONS) } permLauncher.launch(perms.toTypedArray()) } Scaffold( containerColor = MaterialTheme.colorScheme.background, topBar = { TopAppBar( title = { Column { Text("Dashboard") Text(email, fontSize = 12.sp, color = MaterialTheme.colorScheme.onSurfaceVariant) } }, actions = { IconButton(onClick = { VoiceService.stop(ctx); FirebaseRepo.logout() }) { Icon(Icons.AutoMirrored.Filled.Logout, "Sign out") } }, colors = TopAppBarDefaults.topAppBarColors(containerColor = MaterialTheme.colorScheme.background) ) } ) { pad -> LazyColumn( Modifier.fillMaxSize().padding(pad).padding(horizontal = 16.dp), verticalArrangement = Arrangement.spacedBy(12.dp) ) { if (wakeStatus == WakeStatus.BLOCKED) item { Card(colors = CardDefaults.cardColors(containerColor = Red.copy(alpha = 0.18f))) { Column(Modifier.padding(16.dp)) { Text("Protective block active", style = MaterialTheme.typography.titleMedium, color = Red) Text( "Suspicious activity was detected. The screen was locked and voice commands are paused.", style = MaterialTheme.typography.bodyMedium ) TextButton(onClick = { showUnblock = true }) { Text("Unblock with password") } } } } item { Card(colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface)) { Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(10.dp)) { StatusRow("Cloud connection", if (connected) "Connected" else "Offline", if (connected) Green else Red) StatusRow("Wake word", wakeStatus.label, statusColor(wakeStatus)) StatusRow("Accessibility control", if (a11y) "Enabled" else "Not enabled", if (a11y) Green else Amber) StatusRow("Microphone", if (micOk) "Granted" else "Not granted", if (micOk) Green else Amber) StatusRow("Overlay permission", if (overlayOk) "Granted" else "Not granted", if (overlayOk) Green else Amber) if (lastHeard.isNotBlank()) { Text("Last heard: \"$lastHeard\"", color = MaterialTheme.colorScheme.onSurfaceVariant, fontSize = 13.sp) } Button(onClick = ::toggleService, modifier = Modifier.fillMaxWidth(), colors = if (running) ButtonDefaults.buttonColors(containerColor = Red) else ButtonDefaults.buttonColors() ) { Text(if (running) "Stop listening" else "Start listening") } } } } item { Card(colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface)) { Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(10.dp)) { Text("Setup", style = MaterialTheme.typography.titleMedium) Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { OutlinedButton(onClick = { ctx.startActivity(Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)) }, Modifier.weight(1f)) { Text("Accessibility") } OutlinedButton(onClick = { ctx.startActivity( Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse("package:${ctx.packageName}")) .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) ) }, Modifier.weight(1f)) { Text("Overlay") } } Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) { OutlinedTextField( wakeInput, { wakeInput = it }, label = { Text("Wake word / AI name") }, singleLine = true, modifier = Modifier.weight(1f) ) Button(onClick = { FirebaseRepo.saveWakeWord(wakeInput); wakeInput = Prefs.wakeWord }) { Text("Save") } } } } } item { Text("Live command log", style = MaterialTheme.typography.titleMedium, modifier = Modifier.padding(top = 4.dp)) } if (logs.isEmpty()) item { Text("No activity yet.", color = MaterialTheme.colorScheme.onSurfaceVariant) } items(logs, key = { it.id.ifBlank { it.ts.toString() } }) { LogRow(it) } item { Spacer(Modifier.size(16.dp)) } } } if (showUnblock) UnblockDialog( onDismiss = { showUnblock = false }, onVerify = { pw, done -> scope.launch { val ok = FirebaseRepo.reauthenticate(pw) if (ok) { SecurityGuard.clearBlock(); AssistantState.wakeStatus.value = WakeStatus.LISTENING_WAKE; showUnblock = false } done(ok) } } ) } private fun statusColor(s: WakeStatus) = when (s) { WakeStatus.OFF -> Grey WakeStatus.LISTENING_WAKE, WakeStatus.AWAITING_COMMAND, WakeStatus.PROCESSING -> Green WakeStatus.BLOCKED, WakeStatus.ERROR -> Red } @Composable private fun StatusRow(label: String, value: String, color: Color) { Row(Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) { Box(Modifier.size(10.dp).clip(CircleShape).background(color)) Spacer(Modifier.width(10.dp)) Text(label, Modifier.weight(1f)) Text(value, color = MaterialTheme.colorScheme.onSurfaceVariant) } } private val timeFmt = SimpleDateFormat("HH:mm:ss", Locale.getDefault()) @Composable private fun LogRow(e: LogEntry) { val c = when (e.level) { "critical", "error" -> Red; "warn" -> Amber; else -> MaterialTheme.colorScheme.onSurfaceVariant } Column( Modifier.fillMaxWidth().clip(RoundedCornerShape(10.dp)) .background(MaterialTheme.colorScheme.surfaceVariant).padding(10.dp) ) { Row { Text(timeFmt.format(Date(e.ts)), fontFamily = FontFamily.Monospace, fontSize = 11.sp, color = MaterialTheme.colorScheme.onSurfaceVariant) Spacer(Modifier.width(8.dp)) Text(e.type, fontSize = 11.sp, color = c) } Text(e.message, fontSize = 13.sp) } } @Composable private fun UnblockDialog(onDismiss: () -> Unit, onVerify: (String, (Boolean) -> Unit) -> Unit) { var pw by remember { mutableStateOf("") } var error by remember { mutableStateOf(false) } var busy by remember { mutableStateOf(false) } AlertDialog( onDismissRequest = onDismiss, title = { Text("Confirm it's you") }, text = { Column { OutlinedTextField( pw, { pw = it; error = false }, label = { Text("Account password") }, singleLine = true, visualTransformation = PasswordVisualTransformation(), isError = error ) if (error) Text("Incorrect password.", color = MaterialTheme.colorScheme.error, fontSize = 12.sp) } }, confirmButton = { TextButton(enabled = !busy && pw.isNotEmpty(), onClick = { busy = true onVerify(pw) { ok -> busy = false; error = !ok } }) { Text("Unblock") } }, dismissButton = { TextButton(onClick = onDismiss) { Text("Cancel") } } ) }