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.imePadding import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.statusBarsPadding import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.luminance 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) // 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 then stay * down until the app was launched again -- which is the case notifications exist to avoid. */ private val requestNotificationPermission = registerForActivityResult(ActivityResultContracts.RequestPermission()) {} override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // 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() // Dark status-bar icons only over a light background, decided from the scheme rather // than fixed. It was hardcoded to `true` -- dark icons -- which was right against the // default light surface and became unreadable the moment the app wore Catppuccin Mocha. // Asking the colour means a future palette change cannot reintroduce that: whatever // `background` becomes, the icons follow it. 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) } handleEnrollment(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 { MaterialTheme(colorScheme = AiAppColors) { Surface(modifier = Modifier.fillMaxSize()) { Box( modifier = Modifier.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. .navigationBarsPadding() .imePadding() ) { AppRoot(settingsVersion) } } } } } // launchMode="singleTop": an enrollment scan while the app is open // lands here rather than in a second activity instance. override fun onNewIntent(intent: Intent) { super.onNewIntent(intent) handleEnrollment(intent) } private fun handleEnrollment(intent: Intent?) { val uri = intent?.data ?: 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() } }