From 38fc2c7bf4397e8b5bdf7b4ba50633bba392abeb Mon Sep 17 00:00:00 2001 From: Yu-ga <74749461+yuga-hashimoto@users.noreply.github.com> Date: Mon, 16 Mar 2026 00:57:53 +0900 Subject: [PATCH] chore: improve OSS docs and CI (#317) --- .github/workflows/ci.yml | 28 +++++++++ BUILDING.md | 58 ++++++++++++++++++ CONTRIBUTING.md | 11 +++- README.md | 12 ++++ THIRD_PARTY_LICENSES.md | 59 +++++++++++++++++++ app/google-services.json.example | 35 +++++++++++ .../assistant/node/CalendarHandlerTest.kt | 6 ++ .../assistant/node/ContactsHandlerTest.kt | 6 ++ .../assistant/node/DeviceHandlerTest.kt | 3 + .../node/NotificationsHandlerTest.kt | 37 ++++++------ .../assistant/node/PhotosHandlerTest.kt | 10 +++- 11 files changed, 242 insertions(+), 23 deletions(-) create mode 100644 BUILDING.md create mode 100644 THIRD_PARTY_LICENSES.md create mode 100644 app/google-services.json.example diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 409b335..6b2a2ad 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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. diff --git a/BUILDING.md b/BUILDING.md new file mode 100644 index 0000000..1d3f9bf --- /dev/null +++ b/BUILDING.md @@ -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. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 197d7c3..58e8a1a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -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 diff --git a/README.md b/README.md index 1df23ae..f822366 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/THIRD_PARTY_LICENSES.md b/THIRD_PARTY_LICENSES.md new file mode 100644 index 0000000..24c7de8 --- /dev/null +++ b/THIRD_PARTY_LICENSES.md @@ -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. diff --git a/app/google-services.json.example b/app/google-services.json.example new file mode 100644 index 0000000..8ed74de --- /dev/null +++ b/app/google-services.json.example @@ -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" +} diff --git a/app/src/test/java/com/openclaw/assistant/node/CalendarHandlerTest.kt b/app/src/test/java/com/openclaw/assistant/node/CalendarHandlerTest.kt index 0d61993..5434e02 100644 --- a/app/src/test/java/com/openclaw/assistant/node/CalendarHandlerTest.kt +++ b/app/src/test/java/com/openclaw/assistant/node/CalendarHandlerTest.kt @@ -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() private val contentResolver = mockk() diff --git a/app/src/test/java/com/openclaw/assistant/node/ContactsHandlerTest.kt b/app/src/test/java/com/openclaw/assistant/node/ContactsHandlerTest.kt index de1941c..f74c9b0 100644 --- a/app/src/test/java/com/openclaw/assistant/node/ContactsHandlerTest.kt +++ b/app/src/test/java/com/openclaw/assistant/node/ContactsHandlerTest.kt @@ -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() private val contentResolver = mockk() diff --git a/app/src/test/java/com/openclaw/assistant/node/DeviceHandlerTest.kt b/app/src/test/java/com/openclaw/assistant/node/DeviceHandlerTest.kt index 2ae55d6..8e61c70 100644 --- a/app/src/test/java/com/openclaw/assistant/node/DeviceHandlerTest.kt +++ b/app/src/test/java/com/openclaw/assistant/node/DeviceHandlerTest.kt @@ -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() diff --git a/app/src/test/java/com/openclaw/assistant/node/NotificationsHandlerTest.kt b/app/src/test/java/com/openclaw/assistant/node/NotificationsHandlerTest.kt index cb0f4a5..5654d12 100644 --- a/app/src/test/java/com/openclaw/assistant/node/NotificationsHandlerTest.kt +++ b/app/src/test/java/com/openclaw/assistant/node/NotificationsHandlerTest.kt @@ -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() - private val contentResolver = mockk() + private val context: Context = RuntimeEnvironment.getApplication() private val notificationManager = mockk() 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() + 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) } } diff --git a/app/src/test/java/com/openclaw/assistant/node/PhotosHandlerTest.kt b/app/src/test/java/com/openclaw/assistant/node/PhotosHandlerTest.kt index 511de9f..e990427 100644 --- a/app/src/test/java/com/openclaw/assistant/node/PhotosHandlerTest.kt +++ b/app/src/test/java/com/openclaw/assistant/node/PhotosHandlerTest.kt @@ -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() private val contentResolver = mockk() @@ -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()