mirror of
https://github.com/BrendanGreenlee/openclaw-android-heartbeat.git
synced 2026-08-17 16:49:14 +00:00
Import official OpenClaw Android app (apps/android @ 71a59512ba476df3328cf485d84748121cf341f2)
Pristine fork source for PROJ-0088. v1 will turn this into a WebView web shell; the WebSocket node infrastructure stays intact for v2 heartbeat.
This commit is contained in:
commit
7a380c40ed
656 changed files with 209982 additions and 0 deletions
47
benchmark/build.gradle.kts
Normal file
47
benchmark/build.gradle.kts
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
plugins {
|
||||
alias(libs.plugins.android.test)
|
||||
alias(libs.plugins.ktlint)
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "ai.openclaw.app.benchmark"
|
||||
// Match the target app while targetSdk remains an independent behavior opt-in.
|
||||
compileSdk = 37
|
||||
|
||||
defaultConfig {
|
||||
minSdk = 31
|
||||
targetSdk = 36
|
||||
missingDimensionStrategy("store", "play")
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
testInstrumentationRunnerArguments["androidx.benchmark.suppressErrors"] = "DEBUGGABLE,EMULATOR"
|
||||
}
|
||||
|
||||
targetProjectPath = ":app"
|
||||
experimentalProperties["android.experimental.self-instrumenting"] = true
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
}
|
||||
}
|
||||
|
||||
kotlin {
|
||||
compilerOptions {
|
||||
jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17)
|
||||
allWarningsAsErrors.set(true)
|
||||
}
|
||||
}
|
||||
|
||||
ktlint {
|
||||
android.set(true)
|
||||
ignoreFailures.set(false)
|
||||
filter {
|
||||
exclude("**/build/**")
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(libs.androidx.benchmark.macro.junit4)
|
||||
implementation(libs.androidx.test.ext.junit)
|
||||
implementation(libs.androidx.uiautomator)
|
||||
}
|
||||
|
|
@ -0,0 +1,80 @@
|
|||
package ai.openclaw.app.benchmark
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import androidx.test.uiautomator.By
|
||||
import androidx.test.uiautomator.UiDevice
|
||||
import androidx.test.uiautomator.UiObject2
|
||||
import androidx.test.uiautomator.Until
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertNotNull
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class CronJobNavigationTest {
|
||||
private lateinit var device: UiDevice
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
|
||||
device.executeShellCommand("am force-stop $packageName")
|
||||
device.executeShellCommand(
|
||||
"am start -W -n $packageName/.MainActivity " +
|
||||
"--ez openclaw.screenshotMode true --es openclaw.screenshotScene settings",
|
||||
)
|
||||
assertNotNull(device.wait(Until.findObject(By.text("Settings")), waitTimeoutMs))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun opensCronJobFixtureDetail() {
|
||||
findTextAfterScrolling("Automations").click()
|
||||
|
||||
val cronJobLabel = findTextAfterScrolling("Android release digest")
|
||||
val cronJobRow =
|
||||
checkNotNull(
|
||||
generateSequence(cronJobLabel) { it.parent }
|
||||
.firstOrNull { it.isClickable },
|
||||
) { "Cron fixture row must expose a click action" }
|
||||
assertTrue("Cron fixture row must expose a click action", cronJobRow.isClickable)
|
||||
assertFalse(device.hasObject(By.text("Run Now")))
|
||||
cronJobRow.click()
|
||||
|
||||
assertNotNull(findTextAfterScrolling("Run Now"))
|
||||
assertNotNull(findTextAfterScrolling("Recent Runs"))
|
||||
assertNotNull(findTextAfterScrolling("Release checklist ready", exact = false))
|
||||
assertNotNull(findTextAfterScrolling("OK"))
|
||||
assertNotNull(findTextAfterScrolling("Play publish blocked", exact = false))
|
||||
assertNotNull(findTextAfterScrolling("Issue"))
|
||||
}
|
||||
|
||||
private fun findTextAfterScrolling(
|
||||
text: String,
|
||||
exact: Boolean = true,
|
||||
): UiObject2 {
|
||||
val selector = if (exact) By.text(text) else By.textContains(text)
|
||||
repeat(maxScrolls + 1) { attempt ->
|
||||
device.wait(Until.findObject(selector), shortWaitMs)?.let { return it }
|
||||
if (attempt < maxScrolls) {
|
||||
device.swipe(
|
||||
device.displayWidth / 2,
|
||||
(device.displayHeight * 0.8f).toInt(),
|
||||
device.displayWidth / 2,
|
||||
(device.displayHeight * 0.25f).toInt(),
|
||||
24,
|
||||
)
|
||||
device.waitForIdle()
|
||||
}
|
||||
}
|
||||
error("Could not find UI text: $text")
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val packageName = "ai.openclaw.app"
|
||||
const val waitTimeoutMs = 10_000L
|
||||
const val shortWaitMs = 1_000L
|
||||
const val maxScrolls = 6
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
package ai.openclaw.app.benchmark
|
||||
|
||||
import androidx.benchmark.macro.CompilationMode
|
||||
import androidx.benchmark.macro.FrameTimingMetric
|
||||
import androidx.benchmark.macro.StartupMode
|
||||
import androidx.benchmark.macro.StartupTimingMetric
|
||||
import androidx.benchmark.macro.junit4.MacrobenchmarkRule
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import androidx.test.uiautomator.UiDevice
|
||||
import org.junit.Assume.assumeTrue
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class StartupMacrobenchmark {
|
||||
@get:Rule
|
||||
val benchmarkRule = MacrobenchmarkRule()
|
||||
|
||||
private val packageName = "ai.openclaw.app"
|
||||
|
||||
@Test
|
||||
fun coldStartup() {
|
||||
runBenchmarkOrSkip {
|
||||
benchmarkRule.measureRepeated(
|
||||
packageName = packageName,
|
||||
metrics = listOf(StartupTimingMetric()),
|
||||
startupMode = StartupMode.COLD,
|
||||
compilationMode = CompilationMode.None(),
|
||||
iterations = 10,
|
||||
) {
|
||||
pressHome()
|
||||
startActivityAndWait()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun startupAndScrollFrameTiming() {
|
||||
runBenchmarkOrSkip {
|
||||
benchmarkRule.measureRepeated(
|
||||
packageName = packageName,
|
||||
metrics = listOf(FrameTimingMetric()),
|
||||
startupMode = StartupMode.WARM,
|
||||
compilationMode = CompilationMode.None(),
|
||||
iterations = 10,
|
||||
) {
|
||||
startActivityAndWait()
|
||||
val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
|
||||
val x = device.displayWidth / 2
|
||||
val yStart = (device.displayHeight * 0.8f).toInt()
|
||||
val yEnd = (device.displayHeight * 0.25f).toInt()
|
||||
repeat(4) {
|
||||
device.swipe(x, yStart, x, yEnd, 24)
|
||||
device.waitForIdle()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun runBenchmarkOrSkip(run: () -> Unit) {
|
||||
try {
|
||||
run()
|
||||
} catch (err: IllegalStateException) {
|
||||
val message = err.message.orEmpty()
|
||||
val knownDeviceIssue =
|
||||
message.contains("Unable to confirm activity launch completion") ||
|
||||
message.contains("no renderthread slices", ignoreCase = true)
|
||||
if (knownDeviceIssue) {
|
||||
assumeTrue("Skipping benchmark on this device: $message", false)
|
||||
}
|
||||
throw err
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue