mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-09 15:59:30 +00:00
test(android): cover main activity lifecycle gates (#102764)
This commit is contained in:
parent
d6cb18736a
commit
66db3024bc
3 changed files with 164 additions and 21 deletions
|
|
@ -131,7 +131,7 @@
|
|||
},
|
||||
{
|
||||
"kind": "ui-named-argument",
|
||||
"line": 199,
|
||||
"line": 247,
|
||||
"path": "apps/android/app/src/main/java/ai/openclaw/app/MainActivity.kt",
|
||||
"source": "OPENCLAW",
|
||||
"surface": "android",
|
||||
|
|
|
|||
|
|
@ -42,16 +42,15 @@ class MainActivity : ComponentActivity() {
|
|||
private val viewModel: MainViewModel by viewModels()
|
||||
private lateinit var permissionRequester: PermissionRequester
|
||||
private var initializedViewModel: MainViewModel? = null
|
||||
private var didAttachRuntimeUi = false
|
||||
private var didStartNodeService = false
|
||||
private var didStartViewModelCollectors = false
|
||||
private var foreground = false
|
||||
private var pendingIntent: Intent? = null
|
||||
private val pendingIntentRouter = MainActivityPendingIntentRouter()
|
||||
private val runtimeUiStarter = MainActivityRuntimeUiStarter()
|
||||
private var screenshotScene: AndroidScreenshotScene? = null
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
pendingIntent = intent
|
||||
pendingIntentRouter.setInitialIntent(intent)
|
||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||
permissionRequester = PermissionRequester(this)
|
||||
if (BuildConfig.DEBUG) {
|
||||
|
|
@ -111,8 +110,9 @@ class MainActivity : ComponentActivity() {
|
|||
override fun onNewIntent(intent: android.content.Intent) {
|
||||
super.onNewIntent(intent)
|
||||
setIntent(intent)
|
||||
pendingIntent = intent
|
||||
initializedViewModel?.let { handleAssistantIntent(viewModel = it, intent = intent) }
|
||||
pendingIntentRouter.onNewIntent(intent) { routedIntent ->
|
||||
initializedViewModel?.let { handleAssistantIntent(viewModel = it, intent = routedIntent) }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -123,9 +123,8 @@ class MainActivity : ComponentActivity() {
|
|||
initializedViewModel = readyViewModel
|
||||
readyViewModel.setForeground(foreground)
|
||||
startViewModelCollectors(readyViewModel)
|
||||
pendingIntent?.let { initialIntent ->
|
||||
pendingIntentRouter.activate { initialIntent ->
|
||||
handleAssistantIntent(viewModel = readyViewModel, intent = initialIntent)
|
||||
pendingIntent = null
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -151,18 +150,17 @@ class MainActivity : ComponentActivity() {
|
|||
lifecycleScope.launch {
|
||||
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||
readyViewModel.runtimeInitialized.collect { ready ->
|
||||
if (!ready || didAttachRuntimeUi) return@collect
|
||||
if (screenshotScene != null) {
|
||||
didAttachRuntimeUi = true
|
||||
return@collect
|
||||
}
|
||||
// Runtime UI helpers need an Activity owner, so attach once after NodeRuntime is ready.
|
||||
readyViewModel.attachRuntimeUi(owner = this@MainActivity, permissionRequester = permissionRequester)
|
||||
didAttachRuntimeUi = true
|
||||
if (!didStartNodeService) {
|
||||
NodeForegroundService.start(this@MainActivity)
|
||||
didStartNodeService = true
|
||||
}
|
||||
runtimeUiStarter.onRuntimeInitialized(
|
||||
ready = ready,
|
||||
startRuntimeUi = screenshotScene == null,
|
||||
attachRuntimeUi = {
|
||||
// Runtime UI helpers need an Activity owner, so attach once after NodeRuntime is ready.
|
||||
readyViewModel.attachRuntimeUi(owner = this@MainActivity, permissionRequester = permissionRequester)
|
||||
},
|
||||
startNodeService = {
|
||||
NodeForegroundService.start(this@MainActivity)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -184,6 +182,56 @@ class MainActivity : ComponentActivity() {
|
|||
}
|
||||
}
|
||||
|
||||
/** Holds launch intents until ViewModel activation, then routes every later intent immediately. */
|
||||
internal class MainActivityPendingIntentRouter {
|
||||
private var activated = false
|
||||
private var pendingIntent: Intent? = null
|
||||
|
||||
fun setInitialIntent(intent: Intent?) {
|
||||
if (!activated) pendingIntent = intent
|
||||
}
|
||||
|
||||
fun onNewIntent(
|
||||
intent: Intent,
|
||||
routeIntent: (Intent) -> Unit,
|
||||
) {
|
||||
if (activated) {
|
||||
routeIntent(intent)
|
||||
return
|
||||
}
|
||||
pendingIntent = intent
|
||||
}
|
||||
|
||||
fun activate(routeIntent: (Intent) -> Unit): Boolean {
|
||||
if (activated) return false
|
||||
activated = true
|
||||
pendingIntent?.let(routeIntent)
|
||||
pendingIntent = null
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
/** Preserves one-shot runtime UI startup while allowing screenshot fixtures to skip side effects. */
|
||||
internal class MainActivityRuntimeUiStarter {
|
||||
private var completed = false
|
||||
|
||||
fun onRuntimeInitialized(
|
||||
ready: Boolean,
|
||||
startRuntimeUi: Boolean,
|
||||
attachRuntimeUi: () -> Unit,
|
||||
startNodeService: () -> Unit,
|
||||
) {
|
||||
if (!ready || completed) return
|
||||
if (!startRuntimeUi) {
|
||||
completed = true
|
||||
return
|
||||
}
|
||||
attachRuntimeUi()
|
||||
completed = true
|
||||
startNodeService()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun StartupSurface() {
|
||||
Surface(
|
||||
|
|
|
|||
|
|
@ -0,0 +1,95 @@
|
|||
package ai.openclaw.app
|
||||
|
||||
import android.content.Intent
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.robolectric.RobolectricTestRunner
|
||||
import org.robolectric.annotation.Config
|
||||
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
@Config(sdk = [34])
|
||||
class MainActivityLifecycleTest {
|
||||
@Test
|
||||
fun pendingIntentRouterUsesLatestIntentBeforeActivation() {
|
||||
val router = MainActivityPendingIntentRouter()
|
||||
val initial = Intent("initial")
|
||||
val replacement = Intent("replacement")
|
||||
val routed = mutableListOf<Intent>()
|
||||
|
||||
router.setInitialIntent(initial)
|
||||
router.onNewIntent(replacement, routed::add)
|
||||
|
||||
assertTrue(router.activate(routed::add))
|
||||
assertEquals(listOf(replacement), routed)
|
||||
assertFalse(router.activate(routed::add))
|
||||
assertEquals(listOf(replacement), routed)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun pendingIntentRouterRoutesImmediatelyAfterActivation() {
|
||||
val router = MainActivityPendingIntentRouter()
|
||||
val routed = mutableListOf<Intent>()
|
||||
val next = Intent("next")
|
||||
|
||||
assertTrue(router.activate(routed::add))
|
||||
router.onNewIntent(next, routed::add)
|
||||
router.setInitialIntent(Intent("ignored"))
|
||||
|
||||
assertEquals(listOf(next), routed)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun runtimeUiStarterWaitsForReadinessAndStartsOnce() {
|
||||
val starter = MainActivityRuntimeUiStarter()
|
||||
var attachCount = 0
|
||||
var serviceCount = 0
|
||||
|
||||
starter.onRuntimeInitialized(
|
||||
ready = false,
|
||||
startRuntimeUi = true,
|
||||
attachRuntimeUi = { attachCount += 1 },
|
||||
startNodeService = { serviceCount += 1 },
|
||||
)
|
||||
starter.onRuntimeInitialized(
|
||||
ready = true,
|
||||
startRuntimeUi = true,
|
||||
attachRuntimeUi = { attachCount += 1 },
|
||||
startNodeService = { serviceCount += 1 },
|
||||
)
|
||||
starter.onRuntimeInitialized(
|
||||
ready = true,
|
||||
startRuntimeUi = true,
|
||||
attachRuntimeUi = { attachCount += 1 },
|
||||
startNodeService = { serviceCount += 1 },
|
||||
)
|
||||
|
||||
assertEquals(1, attachCount)
|
||||
assertEquals(1, serviceCount)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun runtimeUiStarterCompletesWithoutSideEffectsForScreenshotFixture() {
|
||||
val starter = MainActivityRuntimeUiStarter()
|
||||
var attachCount = 0
|
||||
var serviceCount = 0
|
||||
|
||||
starter.onRuntimeInitialized(
|
||||
ready = true,
|
||||
startRuntimeUi = false,
|
||||
attachRuntimeUi = { attachCount += 1 },
|
||||
startNodeService = { serviceCount += 1 },
|
||||
)
|
||||
starter.onRuntimeInitialized(
|
||||
ready = true,
|
||||
startRuntimeUi = true,
|
||||
attachRuntimeUi = { attachCount += 1 },
|
||||
startNodeService = { serviceCount += 1 },
|
||||
)
|
||||
|
||||
assertEquals(0, attachCount)
|
||||
assertEquals(0, serviceCount)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue