1package soy.iko.opencode
4import android.content.Intent
5import android.content.pm.PackageManager
7import android.os.Bundle
8import androidx.activity.ComponentActivity
9import androidx.activity.compose.setContent
10import androidx.activity.enableEdgeToEdge
11import androidx.activity.result.contract.ActivityResultContracts
12import androidx.compose.foundation.isSystemInDarkTheme
13import androidx.compose.runtime.getValue
14import androidx.core.content.ContextCompat
15import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
16import androidx.lifecycle.compose.collectAsStateWithLifecycle
17import soy.iko.opencode.data.repo.ThemeMode
18import soy.iko.opencode.ui.OpencodeApp as OpencodeAppUi
19import soy.iko.opencode.ui.theme.OpencodeTheme
21class MainActivity : ComponentActivity() {
23 private val requestNotificationPermission = registerForActivityResult(
24 ActivityResultContracts.RequestPermission(),
25 ) { /* Result is best-effort; notifications are silently skipped if denied. */ }
27 override fun onCreate(savedInstanceState: Bundle?) {
29 super.onCreate(savedInstanceState)
31 val container = (application as OpencodeApp).container
32 if (savedInstanceState == null) handleIntent(intent)
33 maybeRequestNotificationPermission()
35 val themeMode by container.settingsStore.themeMode
36 .collectAsStateWithLifecycle(initialValue = ThemeMode.SYSTEM)
37 val dynamicColor by container.settingsStore.dynamicColor
38 .collectAsStateWithLifecycle(initialValue = true)
39 val dark = when (themeMode) {
40 ThemeMode.SYSTEM -> isSystemInDarkTheme()
41 ThemeMode.LIGHT -> false
42 ThemeMode.DARK -> true
44 OpencodeTheme(darkTheme = dark, dynamicColor = dynamicColor) {
45 OpencodeAppUi(container = container)
50 override fun onNewIntent(intent: Intent) {
51 super.onNewIntent(intent)
56 /** Capture text shared from another app so it can be prefilled into a session draft,
57 * and session ids from notification taps / deep links so we can open them. */
58 private fun handleIntent(intent: Intent?) {
59 val container = (application as OpencodeApp).container
60 if (intent?.action == Intent.ACTION_SEND) {
61 val text = intent.getStringExtra(Intent.EXTRA_TEXT)?.takeIf { it.isNotBlank() }
62 if (text != null) container.setPendingShare(text)
64 // Deep link: opencode://session/{sessionId} (or the EXTRA_SESSION_ID extra).
65 val deepLinkId = intent?.takeIf { it.action == Intent.ACTION_VIEW }
67 ?.takeIf { it.host == "session" }
69 val id = deepLinkId ?: intent?.getStringExtra(EXTRA_SESSION_ID)?.takeIf { it.isNotBlank() }
70 // Validate: session ids are opaque server-generated identifiers. Reject anything
71 // with path separators or other traversal/control characters so a malicious deep
72 // link can't inject path components into the REST URL path.
73 id?.takeIf { it.isNotBlank() && it.matches(VALID_SESSION_ID) }?.let { container.requestOpenSession(it) }
77 /** Intent extra carrying a session id to open (notifications / deep links). */
78 const val EXTRA_SESSION_ID = "soy.iko.opencode.extra.SESSION_ID"
80 /** Session ids are alphanumeric with dashes/underscores. Reject path traversal. */
81 private val VALID_SESSION_ID = Regex("[A-Za-z0-9_-]+")
84 /** Ask for POST_NOTIFICATIONS once on Android 13+ so run/completion notifications show. */
85 private fun maybeRequestNotificationPermission() {
86 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) return
87 val granted = ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) ==
88 PackageManager.PERMISSION_GRANTED
89 if (!granted) requestNotificationPermission.launch(Manifest.permission.POST_NOTIFICATIONS)
keysesc to close
- j k
- move down, up
- enter
- open
- esc
- back, or up a level
- 1 2 3 4
- readme, files, commits, refs
- /
- filter
- y
- copy the clone command
- c
- compare the chosen commit with another (or shift-click both)
- ?
- show or hide this list