Skip to content

Commit 88a7db5

Browse files
authored
Merge branch 'main' into sutrah/self-knowledge-docs-prompt
2 parents b47c456 + 4ff0aa9 commit 88a7db5

142 files changed

Lines changed: 6914 additions & 1985 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

apps/android/app/src/main/java/ai/openclaw/app/MainActivity.kt

Lines changed: 108 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -2,37 +2,117 @@ package ai.openclaw.app
22

33
import ai.openclaw.app.ui.OpenClawTheme
44
import ai.openclaw.app.ui.RootScreen
5+
import android.content.Intent
56
import android.os.Bundle
67
import android.view.WindowManager
78
import androidx.activity.ComponentActivity
89
import androidx.activity.compose.setContent
910
import androidx.activity.viewModels
11+
import androidx.compose.foundation.layout.Box
12+
import androidx.compose.foundation.layout.fillMaxSize
1013
import androidx.compose.material3.Surface
14+
import androidx.compose.material3.Text
15+
import androidx.compose.runtime.Composable
16+
import androidx.compose.runtime.LaunchedEffect
17+
import androidx.compose.runtime.getValue
18+
import androidx.compose.runtime.mutableStateOf
19+
import androidx.compose.runtime.remember
20+
import androidx.compose.runtime.setValue
21+
import androidx.compose.runtime.withFrameNanos
22+
import androidx.compose.ui.Alignment
1123
import androidx.compose.ui.Modifier
24+
import androidx.compose.ui.graphics.Color
25+
import androidx.compose.ui.text.font.FontWeight
26+
import androidx.compose.ui.unit.sp
1227
import androidx.core.view.WindowCompat
1328
import androidx.lifecycle.Lifecycle
1429
import androidx.lifecycle.lifecycleScope
1530
import androidx.lifecycle.repeatOnLifecycle
31+
import kotlinx.coroutines.Dispatchers
1632
import kotlinx.coroutines.launch
33+
import kotlinx.coroutines.withContext
1734

1835
/**
1936
* Main Android activity that owns Compose UI attachment and runtime UI wiring.
2037
*/
2138
class MainActivity : ComponentActivity() {
2239
private val viewModel: MainViewModel by viewModels()
2340
private lateinit var permissionRequester: PermissionRequester
41+
private var initializedViewModel: MainViewModel? = null
2442
private var didAttachRuntimeUi = false
2543
private var didStartNodeService = false
44+
private var didStartViewModelCollectors = false
45+
private var foreground = false
46+
private var pendingIntent: Intent? = null
2647

2748
override fun onCreate(savedInstanceState: Bundle?) {
2849
super.onCreate(savedInstanceState)
29-
handleAssistantIntent(intent)
50+
pendingIntent = intent
3051
WindowCompat.setDecorFitsSystemWindows(window, false)
3152
permissionRequester = PermissionRequester(this)
3253

54+
setContent {
55+
var activeViewModel by remember { mutableStateOf<MainViewModel?>(null) }
56+
57+
LaunchedEffect(Unit) {
58+
withFrameNanos { }
59+
withContext(Dispatchers.Default) {
60+
(application as NodeApp).prefs
61+
}
62+
val readyViewModel = viewModel
63+
activateViewModel(readyViewModel)
64+
activeViewModel = readyViewModel
65+
}
66+
67+
OpenClawTheme {
68+
activeViewModel?.let { RootScreen(viewModel = it) } ?: StartupSurface()
69+
}
70+
}
71+
}
72+
73+
override fun onStart() {
74+
super.onStart()
75+
foreground = true
76+
initializedViewModel?.setForeground(true)
77+
}
78+
79+
override fun onStop() {
80+
foreground = false
81+
initializedViewModel?.setForeground(false)
82+
super.onStop()
83+
}
84+
85+
override fun onNewIntent(intent: android.content.Intent) {
86+
super.onNewIntent(intent)
87+
setIntent(intent)
88+
pendingIntent = intent
89+
initializedViewModel?.let { handleAssistantIntent(viewModel = it, intent = intent) }
90+
}
91+
92+
/**
93+
* Wires MainViewModel only after Activity first draw and background prefs warm-up.
94+
*/
95+
private fun activateViewModel(readyViewModel: MainViewModel) {
96+
if (initializedViewModel != null) return
97+
initializedViewModel = readyViewModel
98+
readyViewModel.setForeground(foreground)
99+
startViewModelCollectors(readyViewModel)
100+
pendingIntent?.let { initialIntent ->
101+
handleAssistantIntent(viewModel = readyViewModel, intent = initialIntent)
102+
pendingIntent = null
103+
}
104+
}
105+
106+
/**
107+
* Starts lifecycle collectors after ViewModel construction so they cannot force early startup.
108+
*/
109+
private fun startViewModelCollectors(readyViewModel: MainViewModel) {
110+
if (didStartViewModelCollectors) return
111+
didStartViewModelCollectors = true
112+
33113
lifecycleScope.launch {
34114
repeatOnLifecycle(Lifecycle.State.STARTED) {
35-
viewModel.preventSleep.collect { enabled ->
115+
readyViewModel.preventSleep.collect { enabled ->
36116
if (enabled) {
37117
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
38118
} else {
@@ -44,10 +124,10 @@ class MainActivity : ComponentActivity() {
44124

45125
lifecycleScope.launch {
46126
repeatOnLifecycle(Lifecycle.State.STARTED) {
47-
viewModel.runtimeInitialized.collect { ready ->
127+
readyViewModel.runtimeInitialized.collect { ready ->
48128
if (!ready || didAttachRuntimeUi) return@collect
49129
// Runtime UI helpers need an Activity owner, so attach once after NodeRuntime is ready.
50-
viewModel.attachRuntimeUi(owner = this@MainActivity, permissionRequester = permissionRequester)
130+
readyViewModel.attachRuntimeUi(owner = this@MainActivity, permissionRequester = permissionRequester)
51131
didAttachRuntimeUi = true
52132
if (!didStartNodeService) {
53133
NodeForegroundService.start(this@MainActivity)
@@ -56,36 +136,15 @@ class MainActivity : ComponentActivity() {
56136
}
57137
}
58138
}
59-
60-
setContent {
61-
OpenClawTheme {
62-
Surface(modifier = Modifier) {
63-
RootScreen(viewModel = viewModel)
64-
}
65-
}
66-
}
67-
}
68-
69-
override fun onStart() {
70-
super.onStart()
71-
viewModel.setForeground(true)
72-
}
73-
74-
override fun onStop() {
75-
viewModel.setForeground(false)
76-
super.onStop()
77-
}
78-
79-
override fun onNewIntent(intent: android.content.Intent) {
80-
super.onNewIntent(intent)
81-
setIntent(intent)
82-
handleAssistantIntent(intent)
83139
}
84140

85141
/**
86142
* Routes assistant/app-action intents into ViewModel state without recreating the activity.
87143
*/
88-
private fun handleAssistantIntent(intent: android.content.Intent?) {
144+
private fun handleAssistantIntent(
145+
viewModel: MainViewModel,
146+
intent: Intent?,
147+
) {
89148
parseHomeDestinationIntent(intent)?.let { destination ->
90149
viewModel.requestHomeDestination(destination)
91150
return
@@ -94,3 +153,23 @@ class MainActivity : ComponentActivity() {
94153
viewModel.handleAssistantLaunch(request)
95154
}
96155
}
156+
157+
@Composable
158+
private fun StartupSurface() {
159+
Surface(
160+
modifier = Modifier.fillMaxSize(),
161+
color = Color.Black,
162+
contentColor = Color.White,
163+
) {
164+
Box(
165+
modifier = Modifier.fillMaxSize(),
166+
contentAlignment = Alignment.Center,
167+
) {
168+
Text(
169+
text = "OPENCLAW",
170+
fontSize = 22.sp,
171+
fontWeight = FontWeight.Medium,
172+
)
173+
}
174+
}
175+
}

apps/android/app/src/main/java/ai/openclaw/app/MainViewModel.kt

Lines changed: 91 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import ai.openclaw.app.chat.ChatMessage
44
import ai.openclaw.app.chat.ChatPendingToolCall
55
import ai.openclaw.app.chat.ChatSessionEntry
66
import ai.openclaw.app.chat.OutgoingAttachment
7+
import ai.openclaw.app.gateway.DeviceAuthStore
8+
import ai.openclaw.app.gateway.DeviceIdentityStore
79
import ai.openclaw.app.gateway.GatewayEndpoint
810
import ai.openclaw.app.gateway.GatewayUpdateAvailableSummary
911
import ai.openclaw.app.node.CameraCaptureManager
@@ -14,13 +16,15 @@ import android.app.Application
1416
import androidx.lifecycle.AndroidViewModel
1517
import androidx.lifecycle.LifecycleOwner
1618
import androidx.lifecycle.viewModelScope
19+
import kotlinx.coroutines.Dispatchers
1720
import kotlinx.coroutines.ExperimentalCoroutinesApi
1821
import kotlinx.coroutines.flow.MutableStateFlow
1922
import kotlinx.coroutines.flow.SharingStarted
2023
import kotlinx.coroutines.flow.StateFlow
2124
import kotlinx.coroutines.flow.flatMapLatest
2225
import kotlinx.coroutines.flow.flowOf
2326
import kotlinx.coroutines.flow.stateIn
27+
import kotlinx.coroutines.launch
2428

2529
/**
2630
* UI-facing bridge that exposes NodeRuntime and preference state as Compose-friendly StateFlows.
@@ -32,7 +36,11 @@ class MainViewModel(
3236
private val nodeApp = app as NodeApp
3337
private val prefs = nodeApp.prefs
3438
private val runtimeRef = MutableStateFlow<NodeRuntime?>(null)
35-
private var foreground = true
39+
40+
@Volatile private var foreground = false
41+
42+
@Volatile private var runtimeStartupQueued = false
43+
3644
private val _requestedHomeDestination = MutableStateFlow<HomeDestination?>(null)
3745
val requestedHomeDestination: StateFlow<HomeDestination?> = _requestedHomeDestination
3846
private val _startOnboardingAtGatewaySetup = MutableStateFlow(false)
@@ -53,6 +61,19 @@ class MainViewModel(
5361
return runtime
5462
}
5563

64+
/**
65+
* Starts the node runtime off the main thread so fresh installs can render
66+
* the shell before encrypted prefs, device identity, and gateway setup warm up.
67+
*/
68+
private fun queueRuntimeStartup() {
69+
if (runtimeRef.value != null || runtimeStartupQueued) return
70+
runtimeStartupQueued = true
71+
viewModelScope.launch(Dispatchers.Default) {
72+
runCatching { ensureRuntime() }
73+
runtimeStartupQueued = false
74+
}
75+
}
76+
5677
/**
5778
* Adapts a runtime StateFlow to a stable ViewModel StateFlow before runtime startup.
5879
*/
@@ -91,6 +112,7 @@ class MainViewModel(
91112
val isConnected: StateFlow<Boolean> = runtimeState(initial = false) { it.isConnected }
92113
val isNodeConnected: StateFlow<Boolean> = runtimeState(initial = false) { it.nodeConnected }
93114
val statusText: StateFlow<String> = runtimeState(initial = "Offline") { it.statusText }
115+
val gatewayConnectionProblem: StateFlow<GatewayConnectionProblem?> = runtimeState(initial = null) { it.gatewayConnectionProblem }
94116
val serverName: StateFlow<String?> = runtimeState(initial = null) { it.serverName }
95117
val remoteAddress: StateFlow<String?> = runtimeState(initial = null) { it.remoteAddress }
96118
val gatewayVersion: StateFlow<String?> = runtimeState(initial = null) { it.gatewayVersion }
@@ -180,12 +202,6 @@ class MainViewModel(
180202
val chatSessions: StateFlow<List<ChatSessionEntry>> = runtimeState(initial = emptyList()) { it.chatSessions }
181203
val pendingRunCount: StateFlow<Int> = runtimeState(initial = 0) { it.pendingRunCount }
182204

183-
init {
184-
if (prefs.onboardingCompleted.value) {
185-
ensureRuntime()
186-
}
187-
}
188-
189205
val canvas: CanvasController
190206
get() = ensureRuntime().canvas
191207

@@ -213,13 +229,10 @@ class MainViewModel(
213229
*/
214230
fun setForeground(value: Boolean) {
215231
foreground = value
216-
val runtime =
217-
if (value && prefs.onboardingCompleted.value) {
218-
ensureRuntime()
219-
} else {
220-
runtimeRef.value
221-
}
222-
runtime?.setForeground(value)
232+
if (value && prefs.onboardingCompleted.value) {
233+
queueRuntimeStartup()
234+
}
235+
runtimeRef.value?.setForeground(value)
223236
}
224237

225238
fun setDisplayName(value: String) {
@@ -270,9 +283,51 @@ class MainViewModel(
270283
prefs.setGatewayPassword(value)
271284
}
272285

273-
/** Clears setup credentials through the runtime so active gateway sessions drop stale auth state. */
274-
fun resetGatewaySetupAuth() {
275-
ensureRuntime().resetGatewaySetupAuth()
286+
/** Clears setup credentials without starting the runtime just to discard first-run pairing auth. */
287+
private fun resetGatewaySetupAuth() {
288+
runtimeRef.value?.resetGatewaySetupAuth() ?: resetGatewaySetupAuthWithoutRuntime()
289+
}
290+
291+
private fun resetGatewaySetupAuthWithoutRuntime() {
292+
prefs.clearGatewaySetupAuth()
293+
val deviceId = DeviceIdentityStore(nodeApp).loadOrCreate().deviceId
294+
val deviceAuthStore = DeviceAuthStore(prefs)
295+
deviceAuthStore.clearToken(deviceId, "node")
296+
deviceAuthStore.clearToken(deviceId, "operator")
297+
}
298+
299+
fun saveGatewayConfigAndConnect(
300+
host: String,
301+
port: Int,
302+
tls: Boolean,
303+
token: String,
304+
bootstrapToken: String,
305+
password: String,
306+
resetSetupAuth: Boolean,
307+
) {
308+
// Gateway pairing touches encrypted prefs, identity files, and sockets; keep
309+
// the whole sequence off the Compose thread so retries cannot trigger ANRs.
310+
viewModelScope.launch(Dispatchers.Default) {
311+
if (resetSetupAuth) {
312+
resetGatewaySetupAuth()
313+
}
314+
prefs.setManualEnabled(true)
315+
prefs.setManualHost(host)
316+
prefs.setManualPort(port)
317+
prefs.setManualTls(tls)
318+
prefs.setGatewayBootstrapToken(bootstrapToken)
319+
prefs.setGatewayToken(token)
320+
prefs.setGatewayPassword(password)
321+
ensureRuntime()
322+
.connect(
323+
GatewayEndpoint.manual(host = host, port = port),
324+
NodeRuntime.GatewayConnectAuth(
325+
token = token.ifEmpty { null },
326+
bootstrapToken = bootstrapToken.ifEmpty { null },
327+
password = password.ifEmpty { null },
328+
),
329+
)
330+
}
276331
}
277332

278333
/** Marks onboarding complete and starts the runtime before UI observes connected-state flows. */
@@ -285,10 +340,12 @@ class MainViewModel(
285340

286341
/** Re-enters gateway setup after disconnecting and clearing one-time setup credentials. */
287342
fun pairNewGateway() {
288-
runtimeRef.value?.disconnect()
289-
resetGatewaySetupAuth()
290-
_startOnboardingAtGatewaySetup.value = true
291-
prefs.setOnboardingCompleted(false)
343+
viewModelScope.launch(Dispatchers.Default) {
344+
runtimeRef.value?.disconnect()
345+
resetGatewaySetupAuth()
346+
prefs.setOnboardingCompleted(false)
347+
_startOnboardingAtGatewaySetup.value = true
348+
}
292349
}
293350

294351
/** Acknowledges the one-shot request that opens onboarding at the gateway setup step. */
@@ -384,13 +441,25 @@ class MainViewModel(
384441
}
385442

386443
fun refreshGatewayConnection() {
387-
ensureRuntime().refreshGatewayConnection()
444+
viewModelScope.launch(Dispatchers.Default) {
445+
ensureRuntime().refreshGatewayConnection()
446+
}
447+
}
448+
449+
fun startGatewayDiscovery() {
450+
queueRuntimeStartup()
388451
}
389452

390453
fun connect(endpoint: GatewayEndpoint) {
391454
ensureRuntime().connect(endpoint)
392455
}
393456

457+
fun connectInBackground(endpoint: GatewayEndpoint) {
458+
viewModelScope.launch(Dispatchers.Default) {
459+
ensureRuntime().connect(endpoint)
460+
}
461+
}
462+
394463
fun connect(
395464
endpoint: GatewayEndpoint,
396465
token: String?,

0 commit comments

Comments
 (0)