diff --git a/.claude/rules/android.md b/.claude/rules/android.md index bf28edc69c..7422848a80 100644 --- a/.claude/rules/android.md +++ b/.claude/rules/android.md @@ -15,8 +15,9 @@ Gradle マルチモジュール構成: ## ビルドフレーバー -- `embedded`: Go バックエンドを `libclawdroid.so` として jniLibs に含む -- `termux`: バックエンドなし(Termux 上で別途 Go バイナリを実行) +- `variant`: `embedded`(Go バックエンドを `libclawdroid.so` として jniLibs に含む) / `termux`(バックエンドなし。Termux 上で別途 Go バイナリを実行) +- `distribution`: `direct` / `googleplay` +- `termuxGoogleplay` は `androidComponents.beforeVariants` で除外する ## 技術スタック @@ -70,6 +71,6 @@ Gradle マルチモジュール構成: コード修正後は以下を実行して確認すること: 1. `cd android && ./gradlew lint` — Lint チェック 2. `cd android && ./gradlew test` — ユニットテスト実行 -3. `cd android && ./gradlew assembleEmbeddedDebug` — ビルド確認 +3. `cd android && ./gradlew assembleEmbeddedGoogleplayDebug` — Google Play 向け Embedded ビルド確認 コードを修正したら、関連するテストも合わせて修正・追加すること。 diff --git a/.github/workflows/android-aab.yml b/.github/workflows/android-aab.yml index da2dbb637d..bbbe4c5ac1 100644 --- a/.github/workflows/android-aab.yml +++ b/.github/workflows/android-aab.yml @@ -5,7 +5,7 @@ on: jobs: build-aab: - name: Build Embedded AAB + name: Build Embedded Google Play AAB runs-on: ubuntu-latest steps: - name: Checkout @@ -46,10 +46,10 @@ jobs: - name: Build release AAB working-directory: android - run: ./gradlew bundleEmbeddedRelease + run: ./gradlew bundleEmbeddedGoogleplayRelease - name: Upload AAB artifact uses: actions/upload-artifact@v4 with: - name: clawdroid-embedded-release.aab - path: android/app/build/outputs/bundle/embeddedRelease/app-embedded-release.aab + name: clawdroid-embedded-googleplay-release.aab + path: android/app/build/outputs/bundle/embeddedGoogleplayRelease/*.aab diff --git a/.github/workflows/android-build.yml b/.github/workflows/android-build.yml index 0eac873761..565443ebf6 100644 --- a/.github/workflows/android-build.yml +++ b/.github/workflows/android-build.yml @@ -7,7 +7,7 @@ on: workflow_dispatch: jobs: - termux: + termux-direct: runs-on: ubuntu-latest defaults: run: @@ -41,15 +41,15 @@ jobs: "$STORE_PASSWORD" "$KEY_ALIAS" "$KEY_PASSWORD" > keystore.properties - name: Build release APK - run: ./gradlew assembleTermuxRelease + run: ./gradlew assembleTermuxDirectRelease - name: Upload APK uses: actions/upload-artifact@v4 with: - name: clawdroid-termux-release-apk - path: android/app/build/outputs/apk/termux/release/*.apk + name: clawdroid-termux-direct-release-apk + path: android/app/build/outputs/apk/termuxDirect/release/*.apk - embedded: + embedded-direct: runs-on: ubuntu-latest steps: - name: Checkout @@ -91,24 +91,75 @@ jobs: - name: Build release APKs working-directory: android - run: ./gradlew assembleEmbeddedRelease -PenableAbiSplit + run: ./gradlew assembleEmbeddedDirectRelease -PenableAbiSplit - name: Upload APKs uses: actions/upload-artifact@v4 with: - name: clawdroid-embedded-release-apks - path: android/app/build/outputs/apk/embedded/release/*.apk + name: clawdroid-embedded-direct-release-apks + path: android/app/build/outputs/apk/embeddedDirect/release/*.apk + + embedded-googleplay: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version-file: go.mod + + - name: Build Go binaries for Android + run: make build-android + + - name: Set up JDK + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: 17 + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v4 + + - name: Decode keystore + env: + KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }} + working-directory: android + run: | + echo "$KEYSTORE_BASE64" | base64 -d > release.keystore + + - name: Create keystore.properties + env: + STORE_PASSWORD: ${{ secrets.ANDROID_STORE_PASSWORD }} + KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }} + KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }} + working-directory: android + run: | + printf 'storeFile=release.keystore\nstorePassword=%s\nkeyAlias=%s\nkeyPassword=%s\n' \ + "$STORE_PASSWORD" "$KEY_ALIAS" "$KEY_PASSWORD" > keystore.properties + + - name: Build release APKs + working-directory: android + run: ./gradlew assembleEmbeddedGoogleplayRelease -PenableAbiSplit + + - name: Upload APKs + uses: actions/upload-artifact@v4 + with: + name: clawdroid-embedded-googleplay-release-apks + path: android/app/build/outputs/apk/embeddedGoogleplay/release/*.apk gate: if: always() - needs: [termux, embedded] + needs: [termux-direct, embedded-direct, embedded-googleplay] runs-on: ubuntu-latest steps: - name: Check results run: | for result in \ - "${{ needs.termux.result }}" \ - "${{ needs.embedded.result }}"; do + "${{ needs.termux-direct.result }}" \ + "${{ needs.embedded-direct.result }}" \ + "${{ needs.embedded-googleplay.result }}"; do if [ "$result" = "failure" ] || [ "$result" = "cancelled" ]; then exit 1 fi diff --git a/.github/workflows/android-pr.yml b/.github/workflows/android-pr.yml index 936795125a..cbb4bc9481 100644 --- a/.github/workflows/android-pr.yml +++ b/.github/workflows/android-pr.yml @@ -25,7 +25,7 @@ jobs: - name: Run ktlint run: ./gradlew ktlintCheck - lint-termux: + lint-termux-direct: runs-on: ubuntu-latest defaults: run: @@ -43,10 +43,10 @@ jobs: - name: Setup Gradle uses: gradle/actions/setup-gradle@v4 - - name: Run Android Lint (Termux) - run: ./gradlew :app:lintTermuxDebug + - name: Run Android Lint (Termux Direct) + run: ./gradlew :app:lintTermuxDirectDebug - lint-embedded: + lint-embedded-direct: runs-on: ubuntu-latest defaults: run: @@ -64,10 +64,10 @@ jobs: - name: Setup Gradle uses: gradle/actions/setup-gradle@v4 - - name: Run Android Lint (Embedded) - run: ./gradlew :app:lintEmbeddedDebug + - name: Run Android Lint (Embedded Direct) + run: ./gradlew :app:lintEmbeddedDirectDebug - test-termux: + lint-embedded-googleplay: runs-on: ubuntu-latest defaults: run: @@ -85,10 +85,10 @@ jobs: - name: Setup Gradle uses: gradle/actions/setup-gradle@v4 - - name: Run unit tests (Termux) - run: ./gradlew testTermuxDebugUnitTest + - name: Run Android Lint (Embedded Google Play) + run: ./gradlew :app:lintEmbeddedGoogleplayDebug - test-embedded: + test-termux-direct: runs-on: ubuntu-latest defaults: run: @@ -106,22 +106,66 @@ jobs: - name: Setup Gradle uses: gradle/actions/setup-gradle@v4 - - name: Run unit tests (Embedded) - run: ./gradlew testEmbeddedDebugUnitTest + - name: Run unit tests (Termux Direct) + run: ./gradlew testTermuxDirectDebugUnitTest + + test-embedded-direct: + runs-on: ubuntu-latest + defaults: + run: + working-directory: android + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up JDK + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: 17 + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v4 + + - name: Run unit tests (Embedded Direct) + run: ./gradlew testEmbeddedDirectDebugUnitTest + + test-embedded-googleplay: + runs-on: ubuntu-latest + defaults: + run: + working-directory: android + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up JDK + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: 17 + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v4 + + - name: Run unit tests (Embedded Google Play) + run: ./gradlew testEmbeddedGoogleplayDebugUnitTest gate: if: always() - needs: [ktlint, lint-termux, lint-embedded, test-termux, test-embedded] + needs: [ktlint, lint-termux-direct, lint-embedded-direct, lint-embedded-googleplay, test-termux-direct, test-embedded-direct, test-embedded-googleplay] runs-on: ubuntu-latest steps: - name: Check results run: | for result in \ "${{ needs.ktlint.result }}" \ - "${{ needs.lint-termux.result }}" \ - "${{ needs.lint-embedded.result }}" \ - "${{ needs.test-termux.result }}" \ - "${{ needs.test-embedded.result }}"; do + "${{ needs.lint-termux-direct.result }}" \ + "${{ needs.lint-embedded-direct.result }}" \ + "${{ needs.lint-embedded-googleplay.result }}" \ + "${{ needs.test-termux-direct.result }}" \ + "${{ needs.test-embedded-direct.result }}" \ + "${{ needs.test-embedded-googleplay.result }}"; do if [ "$result" = "failure" ] || [ "$result" = "cancelled" ]; then exit 1 fi diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ec44c06a04..a6edd0a06e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -73,20 +73,20 @@ jobs: "$STORE_PASSWORD" "$KEY_ALIAS" "$KEY_PASSWORD" > keystore.properties - name: Build release APK - run: ./gradlew assembleTermuxRelease + run: ./gradlew assembleTermuxDirectRelease - name: Upload APK to release env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} RELEASE_TAG: ${{ github.event.release.tag_name }} run: | - mv app/build/outputs/apk/termux/release/app-termux-release.apk \ - app/build/outputs/apk/termux/release/clawdroid-noembedded-"$RELEASE_TAG".apk + mv app/build/outputs/apk/termuxDirect/release/app-termux-direct-release.apk \ + app/build/outputs/apk/termuxDirect/release/clawdroid-noembedded-"$RELEASE_TAG".apk gh release upload "$RELEASE_TAG" \ - app/build/outputs/apk/termux/release/clawdroid-noembedded-"$RELEASE_TAG".apk + app/build/outputs/apk/termuxDirect/release/clawdroid-noembedded-"$RELEASE_TAG".apk - build-android-embedded: - name: Android Embedded APKs + build-android-embedded-direct: + name: Android Embedded Direct APKs runs-on: ubuntu-latest permissions: contents: write @@ -131,33 +131,77 @@ jobs: - name: Build release APKs working-directory: android - run: ./gradlew assembleEmbeddedRelease -PenableAbiSplit - - - name: Build release AAB - working-directory: android - run: ./gradlew bundleEmbeddedRelease + run: ./gradlew assembleEmbeddedDirectRelease -PenableAbiSplit - name: Upload APKs to release env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} RELEASE_TAG: ${{ github.event.release.tag_name }} - working-directory: android/app/build/outputs/apk/embedded/release + working-directory: android/app/build/outputs/apk/embeddedDirect/release run: | - mv app-embedded-arm64-v8a-release.apk clawdroid-"$RELEASE_TAG"-arm64-v8a.apk - mv app-embedded-armeabi-v7a-release.apk clawdroid-"$RELEASE_TAG"-armeabi-v7a.apk - mv app-embedded-x86_64-release.apk clawdroid-"$RELEASE_TAG"-x86_64.apk - mv app-embedded-universal-release.apk clawdroid-"$RELEASE_TAG"-universal.apk + mv app-embedded-direct-arm64-v8a-release.apk clawdroid-"$RELEASE_TAG"-arm64-v8a.apk + mv app-embedded-direct-armeabi-v7a-release.apk clawdroid-"$RELEASE_TAG"-armeabi-v7a.apk + mv app-embedded-direct-x86_64-release.apk clawdroid-"$RELEASE_TAG"-x86_64.apk + mv app-embedded-direct-universal-release.apk clawdroid-"$RELEASE_TAG"-universal.apk gh release upload "$RELEASE_TAG" \ clawdroid-"$RELEASE_TAG"-arm64-v8a.apk \ clawdroid-"$RELEASE_TAG"-armeabi-v7a.apk \ clawdroid-"$RELEASE_TAG"-x86_64.apk \ clawdroid-"$RELEASE_TAG"-universal.apk + build-android-embedded-googleplay: + name: Android Embedded Google Play AAB + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - name: Checkout tag + uses: actions/checkout@v4 + with: + ref: ${{ github.event.release.tag_name }} + + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version-file: go.mod + + - name: Build Go binaries for Android + run: make build-android + + - name: Set up JDK + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: 17 + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v4 + + - name: Decode keystore + env: + KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }} + working-directory: android + run: echo "$KEYSTORE_BASE64" | base64 -d > release.keystore + + - name: Create keystore.properties + env: + STORE_PASSWORD: ${{ secrets.ANDROID_STORE_PASSWORD }} + KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }} + KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }} + working-directory: android + run: | + printf 'storeFile=release.keystore\nstorePassword=%s\nkeyAlias=%s\nkeyPassword=%s\n' \ + "$STORE_PASSWORD" "$KEY_ALIAS" "$KEY_PASSWORD" > keystore.properties + + - name: Build release AAB + working-directory: android + run: ./gradlew bundleEmbeddedGoogleplayRelease + - name: Upload AAB to release env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} RELEASE_TAG: ${{ github.event.release.tag_name }} - working-directory: android/app/build/outputs/bundle/embeddedRelease + working-directory: android/app/build/outputs/bundle/embeddedGoogleplayRelease run: | - mv app-embedded-release.aab clawdroid-"$RELEASE_TAG".aab + mv app-embedded-googleplay-release.aab clawdroid-"$RELEASE_TAG".aab gh release upload "$RELEASE_TAG" clawdroid-"$RELEASE_TAG".aab diff --git a/CLAUDE.md b/CLAUDE.md index 9a8a092563..8d686fc284 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -31,10 +31,13 @@ make run # ビルド後に実行 ```bash make build-android # Go バックエンドを jniLibs としてビルド(全アーキテクチャ) make build-android-arm64 # arm64-v8a のみ -cd android && ./gradlew assembleEmbeddedDebug # Embedded フレーバー(Go バックエンド込み) -cd android && ./gradlew assembleTermuxDebug # Termux フレーバー(バックエンドなし) +cd android && ./gradlew assembleEmbeddedDirectDebug # Embedded + Direct +cd android && ./gradlew assembleEmbeddedGoogleplayDebug # Embedded + Google Play +cd android && ./gradlew assembleTermuxDirectDebug # Termux + Direct ``` +Android フレーバーは `variant`(`embedded` / `termux`)と `distribution`(`direct` / `googleplay`)の 2 軸。`termuxGoogleplay` は生成しない。 + ## 通信フロー Android アプリ ↔ Go バックエンド: WebSocket (`ws://127.0.0.1:18793`) diff --git a/README.ja.md b/README.ja.md index 2567c1f70f..aee470d81e 100644 --- a/README.ja.md +++ b/README.ja.md @@ -312,9 +312,9 @@ vi ~/.clawdroid/config.json LLM モデルと API キーを設定します。 -**5. Termux 版 APK のインストール** +**5. 非組み込み版 APK のインストール** -Termux フレーバーの APK(`clawdroid-noembedded-*.apk`)を同じデバイスにインストールします。 +`clawdroid-noembedded-*.apk` を同じデバイスにインストールします。 **6. バックエンドの起動** @@ -566,12 +566,20 @@ Android Studio で `android/` を開くか、Gradle でビルド: ```bash cd android -# 組み込み版(Go バックエンドを APK に内蔵) +# フレーバーマトリクス: +# - variant: embedded / termux +# - distribution: direct / googleplay +# - termuxGoogleplay は除外 +# +# 組み込み版 Direct(Go バックエンドを APK に内蔵) make build-android # Go バックエンドを jniLibs としてビルド -./gradlew assembleEmbeddedDebug +./gradlew assembleEmbeddedDirectDebug -# Termux 版(バックエンドなし) -./gradlew assembleTermuxDebug +# 組み込み版 Google Play +./gradlew assembleEmbeddedGoogleplayDebug + +# Termux 版 Direct(バックエンドなし) +./gradlew assembleTermuxDirectDebug ``` パッケージ名: `io.clawdroid` diff --git a/README.md b/README.md index f42f07ca92..2473670902 100644 --- a/README.md +++ b/README.md @@ -314,9 +314,9 @@ vi ~/.clawdroid/config.json Add your LLM model and API key. -**5. Install the Termux APK** +**5. Install the non-embedded APK** -Install the Termux-flavor APK (`clawdroid-noembedded-*.apk`) on the same device. +Install `clawdroid-noembedded-*.apk` on the same device. **6. Start the backend** @@ -568,12 +568,20 @@ Open `android/` in Android Studio or build with Gradle: ```bash cd android -# Embedded version (includes Go backend in APK) +# Flavor matrix: +# - variant: embedded / termux +# - distribution: direct / googleplay +# - termuxGoogleplay is excluded +# +# Embedded Direct (includes Go backend in APK) make build-android # Build Go backend as jniLibs -./gradlew assembleEmbeddedDebug +./gradlew assembleEmbeddedDirectDebug -# Termux version (no backend in APK) -./gradlew assembleTermuxDebug +# Embedded Google Play +./gradlew assembleEmbeddedGoogleplayDebug + +# Termux Direct (no backend in APK) +./gradlew assembleTermuxDirectDebug ``` Package name: `io.clawdroid` diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index 3a933b1568..e89f66b263 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -55,10 +55,12 @@ android { } } - flavorDimensions += "variant" + flavorDimensions += listOf("variant", "distribution") productFlavors { create("termux") { dimension = "variant" } create("embedded") { dimension = "variant" } + create("direct") { dimension = "distribution" } + create("googleplay") { dimension = "distribution" } } buildFeatures { @@ -80,8 +82,10 @@ android { } sourceSets { - getByName("termux") { java.srcDirs("src/termux/java") } - getByName("embedded") { java.srcDirs("src/embedded/java") } + getByName("termux") { java.setSrcDirs(listOf("src/termux/java")) } + getByName("embedded") { java.setSrcDirs(listOf("src/embedded/java")) } + getByName("direct") { java.setSrcDirs(listOf("src/direct/java")) } + getByName("googleplay") { java.setSrcDirs(listOf("src/googleplay/java")) } } lint { @@ -100,6 +104,16 @@ android { } } +androidComponents { + beforeVariants( + selector() + .withFlavor("variant" to "termux") + .withFlavor("distribution" to "googleplay"), + ) { variantBuilder -> + variantBuilder.enable = false + } +} + dependencies { implementation(project(":feature:chat")) implementation(project(":core:domain")) diff --git a/android/app/src/direct/java/io/clawdroid/di/DistributionModule.kt b/android/app/src/direct/java/io/clawdroid/di/DistributionModule.kt new file mode 100644 index 0000000000..33bbb1cfa3 --- /dev/null +++ b/android/app/src/direct/java/io/clawdroid/di/DistributionModule.kt @@ -0,0 +1,8 @@ +package io.clawdroid.di + +import org.koin.core.qualifier.named +import org.koin.dsl.module + +val distributionModule = module { + single>(named("distributionEnv")) { emptyMap() } +} diff --git a/android/app/src/embedded/java/io/clawdroid/di/FlavorModule.kt b/android/app/src/embedded/java/io/clawdroid/di/FlavorModule.kt index 18d6a020f3..a4bbef44a0 100644 --- a/android/app/src/embedded/java/io/clawdroid/di/FlavorModule.kt +++ b/android/app/src/embedded/java/io/clawdroid/di/FlavorModule.kt @@ -4,9 +4,10 @@ import io.clawdroid.backend.api.BackendLifecycle import io.clawdroid.backend.loader.EmbeddedBackendLifecycle import io.clawdroid.backend.loader.GatewayProcessManager import org.koin.android.ext.koin.androidContext +import org.koin.core.qualifier.named import org.koin.dsl.module val flavorModule = module { - single { GatewayProcessManager(androidContext(), get()) } + single { GatewayProcessManager(androidContext(), get(), get(named("distributionEnv"))) } single { EmbeddedBackendLifecycle(androidContext(), get()) } } diff --git a/android/app/src/googleplay/java/io/clawdroid/di/DistributionModule.kt b/android/app/src/googleplay/java/io/clawdroid/di/DistributionModule.kt new file mode 100644 index 0000000000..33bbb1cfa3 --- /dev/null +++ b/android/app/src/googleplay/java/io/clawdroid/di/DistributionModule.kt @@ -0,0 +1,8 @@ +package io.clawdroid.di + +import org.koin.core.qualifier.named +import org.koin.dsl.module + +val distributionModule = module { + single>(named("distributionEnv")) { emptyMap() } +} diff --git a/android/app/src/main/java/io/clawdroid/ClawDroidApp.kt b/android/app/src/main/java/io/clawdroid/ClawDroidApp.kt index c0886a5578..1768d9a777 100644 --- a/android/app/src/main/java/io/clawdroid/ClawDroidApp.kt +++ b/android/app/src/main/java/io/clawdroid/ClawDroidApp.kt @@ -9,6 +9,7 @@ import io.clawdroid.backend.config.ConfigApiClient import io.clawdroid.backend.config.configModule import io.clawdroid.core.data.remote.WebSocketClient import io.clawdroid.di.appModule +import io.clawdroid.di.distributionModule import io.clawdroid.di.flavorModule import io.clawdroid.receiver.NotificationHelper import kotlinx.coroutines.CoroutineScope @@ -28,7 +29,7 @@ class ClawDroidApp : Application() { super.onCreate() val koinApp = startKoin { androidContext(this@ClawDroidApp) - modules(appModule, flavorModule, configModule) + modules(appModule, flavorModule, distributionModule, configModule) } NotificationHelper.createNotificationChannel(this) diff --git a/android/backend/loader/src/main/java/io/clawdroid/backend/loader/GatewayProcessManager.kt b/android/backend/loader/src/main/java/io/clawdroid/backend/loader/GatewayProcessManager.kt index d24b68579c..6186e8afcc 100644 --- a/android/backend/loader/src/main/java/io/clawdroid/backend/loader/GatewayProcessManager.kt +++ b/android/backend/loader/src/main/java/io/clawdroid/backend/loader/GatewayProcessManager.kt @@ -27,6 +27,7 @@ import java.util.UUID class GatewayProcessManager( private val context: Context, private val settingsStore: GatewaySettingsStore, + private val extraEnv: Map = emptyMap(), ) { private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO) private val _state = MutableStateFlow(BackendState.STOPPED) @@ -75,7 +76,7 @@ class GatewayProcessManager( _state.value = BackendState.STARTING val settings = settingsStore.settings.value - val env = mapOf( + val env = extraEnv + mapOf( "HOME" to context.filesDir.absolutePath, "CLAWDROID_GATEWAY_API_KEY" to settings.apiKey, "TZ" to java.util.TimeZone.getDefault().id,