Skip to content

Commit 65eadf7

Browse files
committed
feat: splash screen with heart logo; fix: CA bundle for Zig TLS on Android (AllProvidersFailed)
1 parent e87c7ff commit 65eadf7

3 files changed

Lines changed: 86 additions & 8 deletions

File tree

app/src/main/java/com/thinkoff/clawwatch/ClawRunner.kt

Lines changed: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,18 +31,41 @@ class ClawRunner(private val context: Context) {
3131
private val configFile get() = File(filesDir, CONFIG_NAME)
3232
private val nullclawConfigFile get() = File(homeDir, ".nullclaw/config.json")
3333

34+
private val caBundleFile get() = File(filesDir, "ca-certificates.crt")
35+
3436
/** Write NullClaw home config with API key and model, copy app config from assets. */
3537
suspend fun ensureInstalled() = withContext(Dispatchers.IO) {
3638
Log.i(TAG, "NullClaw binary at ${binaryFile.absolutePath} (exists: ${binaryFile.exists()})")
37-
// Only copy app config if it doesn't exist
3839
if (!configFile.exists()) {
3940
context.assets.open(CONFIG_NAME).use { it.copyTo(configFile.outputStream()) }
4041
}
41-
// Always write ~/.nullclaw/config.json with API key + model so NullClaw is configured
4242
writeNullclawHomeConfig()
43+
buildCaBundle()
4344
Log.i(TAG, "NullClaw ready, home=${homeDir.absolutePath}")
4445
}
4546

47+
/** Bundle Android system CA certs into a single PEM file for Zig's TLS. */
48+
private fun buildCaBundle() {
49+
if (caBundleFile.exists()) return
50+
val certDirs = listOf(
51+
"/apex/com.android.conscrypt/cacerts",
52+
"/system/etc/security/cacerts"
53+
)
54+
val bundle = StringBuilder()
55+
for (dir in certDirs) {
56+
val d = File(dir)
57+
if (!d.exists()) continue
58+
d.listFiles()?.forEach { cert ->
59+
try { bundle.append(cert.readText()).append("\n") } catch (_: Exception) {}
60+
}
61+
if (bundle.isNotEmpty()) break
62+
}
63+
if (bundle.isNotEmpty()) {
64+
caBundleFile.writeText(bundle.toString())
65+
Log.i(TAG, "CA bundle written: ${caBundleFile.absolutePath}")
66+
}
67+
}
68+
4669
/** Write ~/.nullclaw/config.json so NullClaw knows the model + provider. */
4770
private fun writeNullclawHomeConfig() {
4871
val apiKey = getApiKey() ?: return
@@ -52,7 +75,7 @@ class ClawRunner(private val context: Context) {
5275
"agents": {
5376
"defaults": {
5477
"model": {
55-
"primary": "claude-opus-4-6"
78+
"primary": "anthropic/claude-opus-4-6"
5679
}
5780
}
5881
},
@@ -100,11 +123,19 @@ class ClawRunner(private val context: Context) {
100123
put("ANTHROPIC_API_KEY", apiKey)
101124
put("HOME", homeDir.absolutePath)
102125
put("PATH", "/system/bin:/system/xbin")
126+
// CA bundle for Zig's TLS (musl has no system store on Android)
127+
if (caBundleFile.exists()) {
128+
put("SSL_CERT_FILE", caBundleFile.absolutePath)
129+
put("CURL_CA_BUNDLE", caBundleFile.absolutePath)
130+
put("CAINFO", caBundleFile.absolutePath)
131+
}
103132
}
104133
}
105134
.start()
106135

107136
// Fix #7: read stdout and stderr concurrently
137+
Log.i(TAG, "NullClaw running: prompt='${prompt.take(50)}' binary=${binaryFile.absolutePath} home=${homeDir.absolutePath} config=${nullclawConfigFile.exists()}")
138+
108139
var output = ""
109140
var error = ""
110141
val stdoutThread = Thread { output = process.inputStream.bufferedReader().readText() }
@@ -115,9 +146,14 @@ class ClawRunner(private val context: Context) {
115146
stdoutThread.join()
116147
stderrThread.join()
117148

149+
Log.i(TAG, "NullClaw exit=$exit output='${output.take(100)}' stderr='${error.take(200)}'")
150+
118151
if (exit != 0) {
119-
Log.e(TAG, "NullClaw exit $exit: $error")
152+
Log.e(TAG, "NullClaw failed exit $exit: $error")
120153
Result.failure(RuntimeException(error.take(120)))
154+
} else if (output.isBlank()) {
155+
Log.e(TAG, "NullClaw returned empty output. stderr: $error")
156+
Result.failure(RuntimeException(if (error.isNotBlank()) error.take(120) else "Empty response"))
121157
} else {
122158
Result.success(output.trim())
123159
}

app/src/main/java/com/thinkoff/clawwatch/MainActivity.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,7 @@ class MainActivity : AppCompatActivity() {
155155
private fun setState(s: State) {
156156
state = s
157157
runOnUiThread {
158+
binding.splashPanel.visibility = View.GONE
158159
binding.setupPanel.visibility = if (s == State.SETUP) View.VISIBLE else View.GONE
159160
binding.mainPanel.visibility = if (s != State.SETUP) View.VISIBLE else View.GONE
160161
binding.thinkingIndicator.visibility =

app/src/main/res/layout/activity_main.xml

Lines changed: 45 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,38 @@
66
android:layout_height="match_parent"
77
android:background="#000000">
88

9+
<!-- SPLASH: shown on startup while initialising -->
10+
<LinearLayout
11+
android:id="@+id/splashPanel"
12+
android:layout_width="match_parent"
13+
android:layout_height="match_parent"
14+
android:orientation="vertical"
15+
android:gravity="center"
16+
android:visibility="visible">
17+
18+
<ImageView
19+
android:layout_width="72dp"
20+
android:layout_height="72dp"
21+
android:src="@mipmap/ic_launcher"
22+
android:contentDescription="ClawWatch"
23+
android:scaleType="fitCenter" />
24+
25+
<TextView
26+
android:layout_width="wrap_content"
27+
android:layout_height="wrap_content"
28+
android:text="ClawWatch"
29+
android:textColor="#D4A5E9"
30+
android:textSize="16sp"
31+
android:textStyle="bold"
32+
android:layout_marginTop="8dp" />
33+
34+
<ProgressBar
35+
android:layout_width="24dp"
36+
android:layout_height="24dp"
37+
android:layout_marginTop="12dp" />
38+
39+
</LinearLayout>
40+
941
<!-- SETUP PANEL: shown when no API key -->
1042
<LinearLayout
1143
android:id="@+id/setupPanel"
@@ -16,12 +48,19 @@
1648
android:padding="16dp"
1749
android:visibility="gone">
1850

51+
<ImageView
52+
android:layout_width="40dp"
53+
android:layout_height="40dp"
54+
android:src="@mipmap/ic_launcher"
55+
android:scaleType="fitCenter"
56+
android:layout_marginBottom="8dp" />
57+
1958
<TextView
2059
android:layout_width="match_parent"
2160
android:layout_height="wrap_content"
22-
android:text="Anthropic API Key"
61+
android:text="Run set_key.sh\nfrom your Mac"
2362
android:textColor="#D4A5E9"
24-
android:textSize="13sp"
63+
android:textSize="12sp"
2564
android:gravity="center" />
2665

2766
<EditText
@@ -33,15 +72,17 @@
3372
android:textColorHint="#666666"
3473
android:textSize="11sp"
3574
android:inputType="textPassword"
36-
android:layout_marginTop="8dp" />
75+
android:layout_marginTop="8dp"
76+
android:visibility="gone" />
3777

3878
<Button
3979
android:id="@+id/saveKeyBtn"
4080
android:layout_width="wrap_content"
4181
android:layout_height="wrap_content"
4282
android:text="Save"
4383
android:layout_marginTop="8dp"
44-
android:backgroundTint="#D4A5E9" />
84+
android:backgroundTint="#D4A5E9"
85+
android:visibility="gone" />
4586

4687
</LinearLayout>
4788

0 commit comments

Comments
 (0)