| package com.blaze.agent |
|
|
| import android.Manifest |
| import android.app.Activity |
| import android.content.Intent |
| import android.content.pm.PackageManager |
| import android.media.projection.MediaProjectionManager |
| import android.os.Bundle |
| import android.provider.Settings |
| import android.view.accessibility.AccessibilityManager |
| import androidx.activity.ComponentActivity |
| import androidx.activity.compose.setContent |
| import androidx.activity.result.contract.ActivityResultContracts |
| import androidx.compose.animation.AnimatedVisibility |
| import androidx.compose.animation.core.animateFloatAsState |
| import androidx.compose.foundation.background |
| import androidx.compose.foundation.layout.* |
| import androidx.compose.foundation.shape.CircleShape |
| import androidx.compose.foundation.shape.RoundedCornerShape |
| 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.text.font.FontWeight |
| import androidx.compose.ui.text.style.TextAlign |
| import androidx.compose.ui.unit.dp |
| import androidx.compose.ui.unit.sp |
| import androidx.core.content.ContextCompat |
| import androidx.lifecycle.lifecycleScope |
| import com.blaze.agent.ai.BonsaiDownloader |
| import com.blaze.agent.accessibility.BlazeAccessibilityService |
| import com.blaze.agent.state.AppState |
| import com.blaze.agent.state.AppStateManager |
| import kotlinx.coroutines.launch |
|
|
| class MainActivity : ComponentActivity() { |
|
|
| private lateinit var bonsaiDownloader: BonsaiDownloader |
| private val projectionManager by lazy { getSystemService(MEDIA_PROJECTION_SERVICE) as MediaProjectionManager } |
|
|
| private val micPermissionLauncher = registerForActivityResult(ActivityResultContracts.RequestPermission()) {} |
|
|
| private val projectionLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> |
| if (result.resultCode == Activity.RESULT_OK && result.data != null) { |
| val projection = projectionManager.getMediaProjection(result.resultCode, result.data!!) |
| BlazeAccessibilityService.instance?.initScreenCapture(projection) |
| } |
| } |
|
|
| override fun onCreate(savedInstanceState: Bundle?) { |
| super.onCreate(savedInstanceState) |
| bonsaiDownloader = BonsaiDownloader(this) |
| if (ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) |
| micPermissionLauncher.launch(Manifest.permission.RECORD_AUDIO) |
| setContent { |
| BlazeTheme { |
| BlazeHomeScreen( |
| bonsaiDownloader = bonsaiDownloader, |
| isAccessibilityEnabled = ::isAccessibilityEnabled, |
| openAccessibilitySettings = { startActivity(Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS)) }, |
| requestScreenCapture = { projectionLauncher.launch(projectionManager.createScreenCaptureIntent()) }, |
| onStartListening = { AppStateManager.transitionTo(AppState.LISTENING) } |
| ) |
| } |
| } |
| if (!bonsaiDownloader.isModelReady()) { |
| lifecycleScope.launch { bonsaiDownloader.download() } |
| } |
| } |
|
|
| private fun isAccessibilityEnabled(): Boolean { |
| val enabled = Settings.Secure.getString(contentResolver, Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES) ?: return false |
| return enabled.contains(packageName) |
| } |
| } |
|
|
| @Composable |
| fun BlazeHomeScreen( |
| bonsaiDownloader: BonsaiDownloader, |
| isAccessibilityEnabled: () -> Boolean, |
| openAccessibilitySettings: () -> Unit, |
| requestScreenCapture: () -> Unit, |
| onStartListening: () -> Unit |
| ) { |
| val appState by AppStateManager.state.collectAsState() |
| val downloadState by bonsaiDownloader.state.collectAsState() |
| val accessibilityEnabled = remember { mutableStateOf(isAccessibilityEnabled()) } |
|
|
| Surface(modifier = Modifier.fillMaxSize(), color = Color(0xFF0A0A0F)) { |
| Column( |
| modifier = Modifier.fillMaxSize().padding(24.dp), |
| horizontalAlignment = Alignment.CenterHorizontally, |
| verticalArrangement = Arrangement.spacedBy(20.dp) |
| ) { |
| Spacer(modifier = Modifier.height(48.dp)) |
| Text("BLAZE", fontSize = 36.sp, fontWeight = FontWeight.Bold, color = Color(0xFFFF6B00), letterSpacing = 4.sp) |
| Text("Your AI Phone Agent", fontSize = 14.sp, color = Color(0xFF888888)) |
| Spacer(modifier = Modifier.height(16.dp)) |
| BlazeStateOrb(appState) |
| ModeChip(bonsaiDownloader.isModelReady()) |
| Spacer(modifier = Modifier.height(8.dp)) |
| AnimatedVisibility(visible = downloadState is BonsaiDownloader.DownloadState.Downloading) { |
| BonsaiDownloadCard(downloadState) |
| } |
| if (!accessibilityEnabled.value) { |
| SetupCard("Enable Accessibility", "Required for Blaze to control your phone", "Open Settings", openAccessibilitySettings) |
| } |
| Spacer(modifier = Modifier.weight(1f)) |
| Button( |
| onClick = onStartListening, |
| enabled = accessibilityEnabled.value && appState == AppState.SLEEP, |
| modifier = Modifier.fillMaxWidth().height(60.dp), |
| shape = RoundedCornerShape(16.dp), |
| colors = ButtonDefaults.buttonColors(containerColor = Color(0xFFFF6B00), disabledContainerColor = Color(0xFF333333)) |
| ) { |
| Text( |
| text = when (appState) { |
| AppState.SLEEP -> "Tap to Talk" |
| AppState.LISTENING -> "Listening..." |
| AppState.ACTIVE -> "Working..." |
| AppState.MONITORING -> "Monitoring Active" |
| }, fontSize = 18.sp, fontWeight = FontWeight.SemiBold |
| ) |
| } |
| Spacer(modifier = Modifier.height(16.dp)) |
| } |
| } |
| } |
|
|
| @Composable |
| fun BlazeStateOrb(state: AppState) { |
| val color = when (state) { |
| AppState.SLEEP -> Color(0xFF444444); AppState.LISTENING -> Color(0xFF2196F3) |
| AppState.ACTIVE -> Color(0xFFFF6B00); AppState.MONITORING -> Color(0xFF4CAF50) |
| } |
| Column(horizontalAlignment = Alignment.CenterHorizontally) { |
| Box(modifier = Modifier.size(100.dp).clip(CircleShape).background(color.copy(alpha = 0.2f)), contentAlignment = Alignment.Center) { |
| Box(modifier = Modifier.size(60.dp).clip(CircleShape).background(color)) |
| } |
| Spacer(modifier = Modifier.height(8.dp)) |
| Text(text = state.name, color = color, fontWeight = FontWeight.SemiBold, fontSize = 16.sp) |
| } |
| } |
|
|
| @Composable |
| fun ModeChip(bonsaiReady: Boolean) { |
| Surface(shape = RoundedCornerShape(50), color = Color(0xFF1A1A2E)) { |
| Text( |
| text = if (bonsaiReady) "Offline Ready (Bonsai)" else "Online Mode (Gemini)", |
| modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp), |
| color = Color(0xFFAAAAAA), fontSize = 13.sp |
| ) |
| } |
| } |
|
|
| @Composable |
| fun BonsaiDownloadCard(state: BonsaiDownloader.DownloadState) { |
| val downloading = state as? BonsaiDownloader.DownloadState.Downloading ?: return |
| val progress by animateFloatAsState(targetValue = downloading.progressPercent / 100f, label = "dl") |
| Card(modifier = Modifier.fillMaxWidth(), shape = RoundedCornerShape(16.dp), colors = CardDefaults.cardColors(containerColor = Color(0xFF1A1A2E))) { |
| Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) { |
| Text("Downloading Bonsai AI Model", color = Color.White, fontWeight = FontWeight.SemiBold) |
| Text("${downloading.downloadedMb.toInt()}MB / ${downloading.totalMb.toInt()}MB at ${downloading.speedKbps.toInt()} KB/s", color = Color(0xFF888888), fontSize = 12.sp) |
| LinearProgressIndicator(progress = { progress }, modifier = Modifier.fillMaxWidth().clip(RoundedCornerShape(50)), color = Color(0xFFFF6B00), trackColor = Color(0xFF333333)) |
| } |
| } |
| } |
|
|
| @Composable |
| fun SetupCard(title: String, subtitle: String, buttonText: String, onClick: () -> Unit) { |
| Card(modifier = Modifier.fillMaxWidth(), shape = RoundedCornerShape(16.dp), colors = CardDefaults.cardColors(containerColor = Color(0xFF1A1A2E))) { |
| Row(modifier = Modifier.padding(16.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(12.dp)) { |
| Column(modifier = Modifier.weight(1f)) { |
| Text(title, color = Color.White, fontWeight = FontWeight.SemiBold) |
| Text(subtitle, color = Color(0xFF888888), fontSize = 12.sp) |
| } |
| Button(onClick = onClick, colors = ButtonDefaults.buttonColors(containerColor = Color(0xFFFF6B00)), shape = RoundedCornerShape(10.dp)) { Text(buttonText, fontSize = 12.sp) } |
| } |
| } |
| } |
|
|
| @Composable |
| fun BlazeTheme(content: @Composable () -> Unit) { |
| MaterialTheme(colorScheme = darkColorScheme(primary = Color(0xFFFF6B00), background = Color(0xFF0A0A0F), surface = Color(0xFF1A1A2E)), content = content) |
| } |
|
|