chore: improve OSS docs and CI (#317)

This commit is contained in:
Yu-ga
2026-03-15 08:57:53 -07:00
committed by GitHub
parent be179b36f6
commit 38fc2c7bf4
11 changed files with 242 additions and 23 deletions
+28
View File
@@ -65,6 +65,34 @@ jobs:
- name: Run Lint (standard/debug only)
run: ./gradlew lintStandardDebug
# ── Unit Tests ────────────────────────────────────────────────────────────
# Runs the standard/debug unit tests with the checked-in Firebase stub so
# contributors can reproduce the exact command locally.
unit-test:
runs-on: ubuntu-latest
env:
FIREBASE_ENABLED: false
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'
- name: Setup google-services.json
run: cp app/google-services.json.example app/google-services.json
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v4
- name: Run Unit Tests (standard/debug)
run: ./gradlew clean testStandardDebugUnitTest
# ── Build APKs ────────────────────────────────────────────────────────────
# Builds Standard and Full debug APKs. Using --parallel lets Gradle compile
# the two flavors concurrently within the same job.
+58
View File
@@ -0,0 +1,58 @@
# Building OpenClaw Assistant
This repository can be built locally without access to the release signing key or production Firebase configuration.
## Prerequisites
- JDK 17
- Android SDK Platform 35 and build tools installed
- `local.properties` pointing to your Android SDK if you are not using Android Studio
## Debug builds
1. Copy the checked-in Firebase stub:
```bash
cp app/google-services.json.example app/google-services.json
```
2. Disable Firebase at build time when using the stub config:
```bash
FIREBASE_ENABLED=false ./gradlew assembleStandardDebug
```
3. Optional variants:
```bash
FIREBASE_ENABLED=false ./gradlew assembleFullDebug
FIREBASE_ENABLED=false ./gradlew lintStandardDebug
FIREBASE_ENABLED=false ./gradlew clean testStandardDebugUnitTest
```
Notes:
- Debug builds use the normal Android debug keystore. No manual keystore setup is required.
- `standard` excludes VOICEVOX. `full` bundles VOICEVOX runtime assets and is larger.
- The checked-in `google-services.json.example` is for local debug builds only. Do not use it for releases.
## Release builds
Release builds require your own Firebase config, signing key, and a populated `local.properties`.
Required `local.properties` keys:
```properties
storeFile=release.keystore
storePassword=...
keyAlias=...
keyPassword=...
```
You also need a real `app/google-services.json` before running:
```bash
./gradlew assembleStandardRelease assembleFullRelease bundleStandardRelease bundleFullRelease
```
The GitHub release workflow uses repository secrets for these values; contributors do not need them for normal OSS development.
+8 -3
View File
@@ -38,9 +38,14 @@ Please follow these steps to have your contribution considered by the maintainer
1. **Fork the repository** and clone it locally.
2. Create a **new branch** for your feature or fix: `git checkout -b feature/amazing-feature` or `git checkout -b fix/annoying-bug`.
3. **Commit your changes** with a clear commit message.
4. **Push to the branch**: `git push origin feature/amazing-feature`.
5. **Submit a pull request** through GitHub.
3. Follow the local setup and build instructions in [BUILDING.md](BUILDING.md).
4. Run the baseline checks before opening a PR:
- `cp app/google-services.json.example app/google-services.json`
- `FIREBASE_ENABLED=false ./gradlew lintStandardDebug`
- `FIREBASE_ENABLED=false ./gradlew clean testStandardDebugUnitTest`
5. **Commit your changes** with a clear commit message.
6. **Push to the branch**: `git push origin feature/amazing-feature`.
7. **Submit a pull request** through GitHub.
## Styleguides
+12
View File
@@ -111,6 +111,8 @@
Download APK from [Releases](https://github.com/yuga-hashimoto/OpenClawAssistant/releases), or build from source.
For local source builds, see [BUILDING.md](BUILDING.md). It covers the checked-in debug Firebase stub, test commands, and release signing requirements.
#### 2. Gateway Connection (Recommended)
The app connects to your OpenClaw server via the Gateway protocol.
@@ -248,10 +250,14 @@ ngrok http 18789
Pull Requests welcome! Feel free to report issues.
Please follow [BUILDING.md](BUILDING.md) for local setup and run the documented lint/unit test commands before opening a PR.
### 📄 License
MIT License - See [LICENSE](LICENSE) for details.
Bundled third-party binaries and assets are documented in [THIRD_PARTY_LICENSES.md](THIRD_PARTY_LICENSES.md).
---
## 日本語
@@ -331,6 +337,8 @@ MIT License - See [LICENSE](LICENSE) for details.
[Releases](https://github.com/yuga-hashimoto/OpenClawAssistant/releases) からAPKをダウンロード、またはソースからビルド。
ローカルでのソースビルド手順は [BUILDING.md](BUILDING.md) を参照してください。debug 用 Firebase スタブ、テストコマンド、release 署名の前提をまとめています。
#### 2. Gateway接続(推奨)
アプリはGatewayプロトコルを通じてOpenClawサーバーと接続します。
@@ -467,10 +475,14 @@ Chat Completions APIが有効であることを `moltbot.json` で確認:
Pull Requests歓迎!Issues報告もお気軽に。
PR 前のローカルセットアップと lint / unit test 手順は [BUILDING.md](BUILDING.md) を参照してください。
### 📄 ライセンス
MIT License - 詳細は [LICENSE](LICENSE) を参照。
同梱している第三者バイナリ・アセットは [THIRD_PARTY_LICENSES.md](THIRD_PARTY_LICENSES.md) にまとめています。
---
## Star History
+59
View File
@@ -0,0 +1,59 @@
# Third-Party Licenses
This file tracks third-party binaries and assets that are checked into this repository or shipped in release artifacts.
It is not a complete list of all transitive Gradle dependencies. For those, refer to the dependency metadata resolved during the build.
## Bundled components
### Vosk Android bindings
- Usage: offline wake word and speech recognition support
- Source in this repo: `com.alphacephei:vosk-android:0.3.75`
- Upstream project: https://github.com/alphacep/vosk-api
- Upstream license: Apache-2.0
- Notes in this repo:
- app code dependency declaration: `app/build.gradle.kts`
- bundled model notice: `app/src/main/assets/model/README`
### Vosk English mobile model
- Usage: default offline English recognition model
- Source in this repo: `app/src/main/assets/model/`
- Bundled notice file: `app/src/main/assets/model/README`
- Notes:
- The checked-in model directory includes the upstream README notice.
- If you replace or redistribute a different Vosk model, verify that model's own license terms from the upstream distribution.
### VOICEVOX CORE Android AAR
- Usage: VOICEVOX support in the `full` flavor
- Source in this repo: `app/libs/voicevoxcore-android-0.16.4.aar`
- Provenance metadata: `app/libs/java_packages.zip`
- Upstream project: https://github.com/VOICEVOX/voicevox_core
- Upstream license for version `0.16.4`: MIT
- Notes:
- VOICEVOX states that versions earlier than `0.16` used a different license. Keep the version pinned unless you re-review the license.
### ONNX Runtime Android library
- Usage: ONNX runtime support for VOICEVOX-related binaries
- Source in this repo: `app/libs/onnxruntime-android.aar`
- Upstream project: https://github.com/microsoft/onnxruntime
- Upstream license: MIT
### Custom VOICEVOX ONNX Runtime binary
- Usage: runtime required by the bundled VOICEVOX full flavor
- Source in this repo: `app/src/full/jniLibs/arm64-v8a/libvoicevox_onnxruntime.so`
- Notes:
- This binary is distributed together with the VOICEVOX full flavor assets.
- Review upstream ONNX Runtime and VOICEVOX release materials again before upgrading or replacing it.
### Open JTalk dictionary
- Usage: Japanese text processing assets for VOICEVOX
- Source in this repo: `app/src/full/assets/open_jtalk_dic_utf_8-1.11/`
- Bundled license file: `app/src/full/assets/open_jtalk_dic_utf_8-1.11/COPYING`
- Notes:
- Keep `COPYING` with the dictionary assets when redistributing.
+35
View File
@@ -0,0 +1,35 @@
{
"project_info": {
"project_number": "000000000000",
"project_id": "mock-project-id",
"storage_bucket": "mock-project-id.appspot.com"
},
"client": [
{
"client_info": {
"mobilesdk_app_id": "1:000000000000:android:0000000000000001",
"android_client_info": {
"package_name": "com.openclaw.assistant.debug"
}
},
"api_key": [
{
"current_key": "mock-api-key"
}
],
"services": {
"analytics_service": {
"status": 1
},
"appinvite_service": {
"status": 1,
"other_platform_oauth_client": []
},
"ads_service": {
"status": 2
}
}
}
],
"configuration_version": "1"
}
@@ -1,5 +1,6 @@
package com.openclaw.assistant.node
import android.app.Application
import android.content.Context
import com.openclaw.assistant.gateway.GatewaySession
import io.mockk.every
@@ -8,6 +9,7 @@ import io.mockk.mockkStatic
import io.mockk.unmockkStatic
import org.junit.Assert.assertEquals
import org.junit.Test
import org.junit.runner.RunWith
import androidx.core.content.ContextCompat
import android.content.pm.PackageManager
import android.Manifest
@@ -15,7 +17,11 @@ import android.provider.CalendarContract
import android.database.MatrixCursor
import android.content.ContentResolver
import kotlinx.coroutines.runBlocking
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config
@RunWith(RobolectricTestRunner::class)
@Config(application = Application::class)
class CalendarHandlerTest {
private val context = mockk<Context>()
private val contentResolver = mockk<ContentResolver>()
@@ -1,5 +1,6 @@
package com.openclaw.assistant.node
import android.app.Application
import android.content.Context
import com.openclaw.assistant.gateway.GatewaySession
import io.mockk.every
@@ -8,6 +9,7 @@ import io.mockk.mockkStatic
import io.mockk.unmockkStatic
import org.junit.Assert.assertEquals
import org.junit.Test
import org.junit.runner.RunWith
import androidx.core.content.ContextCompat
import android.content.pm.PackageManager
import android.Manifest
@@ -15,7 +17,11 @@ import android.provider.ContactsContract
import android.database.MatrixCursor
import android.content.ContentResolver
import kotlinx.coroutines.runBlocking
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config
@RunWith(RobolectricTestRunner::class)
@Config(application = Application::class)
class ContactsHandlerTest {
private val context = mockk<Context>()
private val contentResolver = mockk<ContentResolver>()
@@ -1,5 +1,6 @@
package com.openclaw.assistant.node
import android.app.Application
import android.content.Context
import com.openclaw.assistant.LocationMode
import com.openclaw.assistant.SecurePrefs
@@ -13,8 +14,10 @@ import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RuntimeEnvironment
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config
@RunWith(RobolectricTestRunner::class)
@Config(application = Application::class)
class DeviceHandlerTest {
private val context: Context = RuntimeEnvironment.getApplication()
@@ -1,51 +1,55 @@
package com.openclaw.assistant.node
import android.app.Application
import android.app.Notification
import android.content.Context
import com.openclaw.assistant.gateway.GatewaySession
import io.mockk.every
import io.mockk.mockk
import io.mockk.mockkStatic
import org.junit.Assert.assertEquals
import org.junit.Test
import org.junit.runner.RunWith
import android.service.notification.StatusBarNotification
import io.mockk.unmockkStatic
import android.provider.Settings
import android.content.ContentResolver
import kotlinx.coroutines.runBlocking
import org.robolectric.RobolectricTestRunner
import org.robolectric.RuntimeEnvironment
import org.robolectric.annotation.Config
@RunWith(RobolectricTestRunner::class)
@Config(application = Application::class)
class NotificationsHandlerTest {
private val context = mockk<Context>()
private val contentResolver = mockk<ContentResolver>()
private val context: Context = RuntimeEnvironment.getApplication()
private val notificationManager = mockk<NotificationManager>()
private val handler = NotificationsHandler(context, notificationManager)
@Test
fun `handleList returns error when service disabled`() = runBlocking {
every { context.contentResolver } returns contentResolver
every { context.packageName } returns "com.openclaw.assistant"
mockkStatic(Settings.Secure::class)
every { Settings.Secure.getString(contentResolver, "enabled_notification_listeners") } returns ""
Settings.Secure.putString(context.contentResolver, "enabled_notification_listeners", "")
val result = handler.handleList()
assertEquals(false, result.ok)
assertEquals("NOTIFICATIONS_PERMISSION_REQUIRED", result.error?.code)
unmockkStatic(Settings.Secure::class)
}
@Test
fun `handleList returns notifications when service enabled`() = runBlocking {
every { context.contentResolver } returns contentResolver
every { context.packageName } returns "com.openclaw.assistant"
mockkStatic(Settings.Secure::class)
every { Settings.Secure.getString(contentResolver, "enabled_notification_listeners") } returns "com.openclaw.assistant"
Settings.Secure.putString(
context.contentResolver,
"enabled_notification_listeners",
context.packageName
)
val sbn = mockk<StatusBarNotification>()
val notification = Notification.Builder(context, "test-channel")
.setContentTitle("Title")
.setContentText("Text")
.build()
every { sbn.key } returns "test_key"
every { sbn.packageName } returns "com.test"
every { sbn.postTime } returns 12345L
every { sbn.notification.extras.getCharSequence("android.title") } returns "Title"
every { sbn.notification.extras.getCharSequence("android.text") } returns "Text"
every { sbn.notification } returns notification
every { notificationManager.getActiveNotifications() } returns listOf(sbn)
@@ -54,6 +58,5 @@ class NotificationsHandlerTest {
assertEquals(true, result.ok)
val json = result.payloadJson ?: ""
assertEquals(true, json.contains("test_key"))
unmockkStatic(Settings.Secure::class)
}
}
@@ -1,5 +1,6 @@
package com.openclaw.assistant.node
import android.app.Application
import android.content.Context
import com.openclaw.assistant.gateway.GatewaySession
import io.mockk.every
@@ -8,15 +9,19 @@ import io.mockk.mockkStatic
import io.mockk.unmockkStatic
import org.junit.Assert.assertEquals
import org.junit.Test
import org.junit.runner.RunWith
import androidx.core.content.ContextCompat
import android.content.pm.PackageManager
import android.Manifest
import android.os.Build
import android.provider.MediaStore
import android.database.MatrixCursor
import android.content.ContentResolver
import kotlinx.coroutines.runBlocking
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config
@RunWith(RobolectricTestRunner::class)
@Config(application = Application::class)
class PhotosHandlerTest {
private val context = mockk<Context>()
private val contentResolver = mockk<ContentResolver>()
@@ -25,8 +30,7 @@ class PhotosHandlerTest {
@Test
fun `handleLatest returns error when permission missing`() = runBlocking {
mockkStatic(ContextCompat::class)
// Assume API < 33
every { ContextCompat.checkSelfPermission(context, Manifest.permission.READ_EXTERNAL_STORAGE) } returns PackageManager.PERMISSION_DENIED
every { ContextCompat.checkSelfPermission(context, any()) } returns PackageManager.PERMISSION_DENIED
val result = handler.handleLatest()