package com.example.aiapp import android.Manifest import android.content.Intent import android.os.Build import android.os.Bundle import android.widget.Toast import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.statusBarsPadding import androidx.compose.foundation.text.selection.LocalTextSelectionColors import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.draw.drawWithContent import androidx.compose.ui.graphics.luminance import androidx.compose.ui.layout.layout import androidx.core.view.WindowCompat class MainActivity : ComponentActivity() { // Bumped whenever enrollment lands via an aiapp:// intent so the composition below re-reads the // stored settings. private var settingsVersion by mutableIntStateOf(0) // The session a notification tap asked for, or null if nothing has. The serial is what makes a // second tap on the same session's notification a second request: without it the two compare // equal and the composition below has nothing to react to. private var openRequest by mutableStateOf(null) private var opens = 0 // What another app shared into this one, for the same reason and with the same serial. private var shareRequest by mutableStateOf(null) private var shares = 0 // Registered up front since permission launchers must be registered before the activity reaches // STARTED. private val requestLocalNetworkPermission = registerForActivityResult(ActivityResultContracts.RequestPermission()) {} /** * The service starts either way, and posts nothing if this is refused. * * Deliberately not gated on the answer: the permission can be granted later from Android's own * settings, and a service that only ever started at the moment it was granted would stay down * until the app was launched again. */ private val requestNotificationPermission = registerForActivityResult(ActivityResultContracts.RequestPermission()) {} override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // Before anything else that could throw, so the first crash of a launch is caught too. installCrashLog(this) // Transparent status bar on every version; the Surface below paints through underneath it // and content insets itself. Same reasoning as dev-updater's MainActivity. enableEdgeToEdge() // The `bench` build's entire purpose (P0, docs/RUST.md): open straight onto the session // screen against BenchFixture's in-process fake backend, with no enrollment, no network // permission, and no notification prompt -- none of them mean anything with no server and // no real device to notify. See BenchFixture.kt and BenchNetwork.kt for how a screen built // to talk to a real backend is made to talk to this instead. Still needs the same // status/navigation-bar padding the ordinary flow below applies: edge-to-edge is the // platform's own default from Android 15 on this app's targetSdk, with or without the call // above, so skipping the padding here put the header's own buttons under the status bar -- // there to look at, but not there for `ui-trace`'s tap-by-label to land on. if (BuildConfig.FIXTURE_MODE) { installFixtureNetworkOnce() BenchFixture.ensureLoaded(this) setContent { MaterialTheme(colorScheme = AiAppColors) { Surface(modifier = Modifier.fillMaxSize()) { Box(Modifier.fillMaxSize().statusBarsPadding().navigationBarsPadding()) { SessionScreen( settings = BenchFixture.settings, summary = benchSessionSummary(), onBack = { finish() }, onFiles = {}, ) } } } } return } // Dark status-bar icons only over a light background, decided from the scheme rather than // fixed. It was hardcoded to `true`, which was right against the default light surface and // became unreadable the moment the app wore Catppuccin Mocha. WindowCompat.getInsetsController(window, window.decorView).isAppearanceLightStatusBars = AiAppColors.background.luminance() > 0.5f // Android 17+ silently drops local-network traffic without this; requested up front because // a denial is invisible at the socket layer (it just times out). if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.CINNAMON_BUN) { requestLocalNetworkPermission.launch(Manifest.permission.ACCESS_LOCAL_NETWORK) } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { requestNotificationPermission.launch(Manifest.permission.POST_NOTIFICATIONS) } handleIntent(intent) // After enrollment, so a first launch that arrives with a token starts the service with // something to connect to rather than stopping it and waiting for the next launch. NotificationService.sync(this) setContent { // Selection colours with the theme rather than at each place text is drawn: the // transcript is one selection container, and a selection that ran from a reply into the // code block under it would otherwise change colour halfway. MaterialTheme(colorScheme = AiAppColors) { CompositionLocalProvider(LocalTextSelectionColors provides AiAppSelectionColors) { Surface(modifier = Modifier.fillMaxSize()) { Box( modifier = // Timed like the transcript times itself, and for the same reason: // the frame's draw phase is where Compose's measurement lands, and // a report saying "draw is high" cannot otherwise say whether the // cost is the transcript or the chrome around it. The keyboard is // the case that made it matter. Modifier.layout { measurable, constraints -> val started = System.nanoTime() val placeable = measurable.measure(constraints) DebugStats.record( "measure: the app root", System.nanoTime() - started, ) layout(placeable.width, placeable.height) { val placing = System.nanoTime() placeable.place(0, 0) DebugStats.record( "place: the app root", System.nanoTime() - placing, ) } } .drawWithContent { val started = System.nanoTime() drawContent() DebugStats.record( "record: the app root", System.nanoTime() - started, ) } .fillMaxSize() .statusBarsPadding() // The gesture strip at the bottom of most phones. Without it // the send row sits under the swipe area, where a tap is as // likely to navigate away as to press a button. // // No imePadding here, deliberately: applied at the root it // resizes this whole box on every frame of the keyboard // animation, which re-measures, re-places and re-records every // screen's entire tree per frame. Each screen takes the // keyboard itself, so the per-frame cost is scoped to what // actually moves. .navigationBarsPadding() ) { AppRoot(settingsVersion, openRequest, shareRequest) } } } } } } /** The one session the `bench` build ever shows -- BenchFixture's session id, nothing else. */ private fun benchSessionSummary() = SessionSummary( id = BenchFixture.SESSION_ID, setup = "bench", setupName = "bench", provider = "bench", title = "P0 benchmark", model = null, keepsOwnTranscript = false, permissionMode = null, effort = null, takesEffort = false, imported = false, notify = false, autoResume = false, autoResumeMessage = "", resumeAt = null, cwd = null, contextTokens = null, maxImageEdge = null, usageProvider = null, status = "idle", lastActivity = 0.0, subagents = 0, ) // launchMode="singleTop": an enrollment scan, or a notification tapped while the app is open, // lands here rather than in a second activity instance. override fun onNewIntent(intent: Intent) { super.onNewIntent(intent) handleIntent(intent) } /** * The one place an incoming intent is sorted into what it means. * * Three things arrive this way -- a share from another app, and an `aiapp://` URI that is * either an enrollment code or a notification naming a session. The URIs are told apart by host * rather than by two entry points, so a further kind is a branch here. */ private fun handleIntent(intent: Intent?) { intent ?: return sharedContent(intent, shares + 1)?.let { shared -> shares = shared.serial shareRequest = shared return } val uri = intent.data ?: return val sessionId = notifiedSessionId(uri) if (sessionId != null) { opens++ openRequest = SessionOpenRequest(sessionId, opens) return } val settings = parseEnrollmentUri(uri) if (settings == null) { Toast.makeText(this, "Not a valid enrollment code", Toast.LENGTH_LONG).show() return } saveServerSettings(this, settings) settingsVersion++ // Enrolling is the moment there is a backend to watch, and re-enrolling elsewhere is the // moment the old one stops being it. NotificationService.sync(this) Toast.makeText(this, "Enrolled with ${settings.baseUrl}", Toast.LENGTH_LONG).show() } }