diff --git a/README.ja.md b/README.ja.md
index ff0943ad..43d05250 100644
--- a/README.ja.md
+++ b/README.ja.md
@@ -243,6 +243,8 @@ app/build/outputs/apk/github/release/app-github-release-unsigned.apk
adb install -r app/build/outputs/apk/github/debug/app-github-debug.apk
```
+debug ビルド(リリースに添付される `-debug.apk` を含む)は別アプリ `com.doublepi123.andcode.debug`(「DoubleCode Debug」、アイコンに虫のバッジ)としてインストールされ、データとローカル OpenCode のポート(4099)も別です。release 版を上書きせず、データも共有しません。両方のアプリで同じ OAuth プロバイダ(Codex、Antigravity)に同時にサインインすると、固定の localhost コールバックポートが衝突することがあります。
+
## ドキュメント
- [オンデバイスランタイム設計](docs/LOCAL_RUNTIME.md)
diff --git a/README.md b/README.md
index 48de0707..b4972fc8 100644
--- a/README.md
+++ b/README.md
@@ -243,6 +243,8 @@ Install to device:
adb install -r app/build/outputs/apk/github/debug/app-github-debug.apk
```
+Debug builds (including the `-debug.apk` attached to releases) install as a separate app, `com.doublepi123.andcode.debug` ("DoubleCode Debug", bug badge on the icon), with its own data and local OpenCode port 4099. They do not upgrade or share data with the release app. Signing in to the same OAuth provider (Codex, Antigravity) from both apps at the same time can collide on its fixed localhost callback port.
+
## Documentation
- [On-Device Runtime Design](docs/LOCAL_RUNTIME.md)
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index e3c9d4d6..2024ce42 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -128,6 +128,8 @@ android {
versionCode = 66
versionName = "1.2.27-dp7"
buildConfigField("String", "GITHUB_CLIENT_ID", "\"$githubClientId\"")
+ // 0 keeps the port from the bundled runtime manifest; the debug build type sets its own.
+ buildConfigField("int", "LOCAL_RUNTIME_PORT_OVERRIDE", "0")
// The on-device runtime (see ANDROID_ABIS in scripts/prepare_android_runtime_native_libs.py)
// only exists for these two ABIs. Without the filter JNA and Vosk drag in libraries for
@@ -180,6 +182,13 @@ android {
}
buildTypes {
+ debug {
+ // Installs beside the release build: its own package (data, runtime, password), its
+ // own local OpenCode port, and its own launcher name and badged icon (src/debug/res).
+ applicationIdSuffix = ".debug"
+ versionNameSuffix = "-debug"
+ buildConfigField("int", "LOCAL_RUNTIME_PORT_OVERRIDE", "4099")
+ }
release {
isMinifyEnabled = true
isShrinkResources = true
diff --git a/app/src/debug/res/drawable/ic_launcher_debug_badge.xml b/app/src/debug/res/drawable/ic_launcher_debug_badge.xml
new file mode 100644
index 00000000..d290d5d8
--- /dev/null
+++ b/app/src/debug/res/drawable/ic_launcher_debug_badge.xml
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
diff --git a/app/src/debug/res/drawable/ic_launcher_debug_badge_monochrome.xml b/app/src/debug/res/drawable/ic_launcher_debug_badge_monochrome.xml
new file mode 100644
index 00000000..732de79d
--- /dev/null
+++ b/app/src/debug/res/drawable/ic_launcher_debug_badge_monochrome.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
diff --git a/app/src/debug/res/drawable/ic_launcher_debug_foreground.xml b/app/src/debug/res/drawable/ic_launcher_debug_foreground.xml
new file mode 100644
index 00000000..b9737ca3
--- /dev/null
+++ b/app/src/debug/res/drawable/ic_launcher_debug_foreground.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/app/src/debug/res/drawable/ic_launcher_debug_monochrome.xml b/app/src/debug/res/drawable/ic_launcher_debug_monochrome.xml
new file mode 100644
index 00000000..0c88f728
--- /dev/null
+++ b/app/src/debug/res/drawable/ic_launcher_debug_monochrome.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/app/src/debug/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/debug/res/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 00000000..3ff50414
--- /dev/null
+++ b/app/src/debug/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/app/src/debug/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/debug/res/mipmap-anydpi-v26/ic_launcher_round.xml
new file mode 100644
index 00000000..3ff50414
--- /dev/null
+++ b/app/src/debug/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/app/src/debug/res/mipmap-anydpi-v33/ic_launcher.xml b/app/src/debug/res/mipmap-anydpi-v33/ic_launcher.xml
new file mode 100644
index 00000000..33053c42
--- /dev/null
+++ b/app/src/debug/res/mipmap-anydpi-v33/ic_launcher.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
diff --git a/app/src/debug/res/mipmap-anydpi-v33/ic_launcher_round.xml b/app/src/debug/res/mipmap-anydpi-v33/ic_launcher_round.xml
new file mode 100644
index 00000000..33053c42
--- /dev/null
+++ b/app/src/debug/res/mipmap-anydpi-v33/ic_launcher_round.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
diff --git a/app/src/debug/res/values-ar/strings.xml b/app/src/debug/res/values-ar/strings.xml
new file mode 100644
index 00000000..968c143c
--- /dev/null
+++ b/app/src/debug/res/values-ar/strings.xml
@@ -0,0 +1,4 @@
+
+
+ DoubleCode Debug
+
diff --git a/app/src/debug/res/values-es/strings.xml b/app/src/debug/res/values-es/strings.xml
new file mode 100644
index 00000000..968c143c
--- /dev/null
+++ b/app/src/debug/res/values-es/strings.xml
@@ -0,0 +1,4 @@
+
+
+ DoubleCode Debug
+
diff --git a/app/src/debug/res/values-fr/strings.xml b/app/src/debug/res/values-fr/strings.xml
new file mode 100644
index 00000000..968c143c
--- /dev/null
+++ b/app/src/debug/res/values-fr/strings.xml
@@ -0,0 +1,4 @@
+
+
+ DoubleCode Debug
+
diff --git a/app/src/debug/res/values-ja/strings.xml b/app/src/debug/res/values-ja/strings.xml
new file mode 100644
index 00000000..968c143c
--- /dev/null
+++ b/app/src/debug/res/values-ja/strings.xml
@@ -0,0 +1,4 @@
+
+
+ DoubleCode Debug
+
diff --git a/app/src/debug/res/values-pt-rBR/strings.xml b/app/src/debug/res/values-pt-rBR/strings.xml
new file mode 100644
index 00000000..968c143c
--- /dev/null
+++ b/app/src/debug/res/values-pt-rBR/strings.xml
@@ -0,0 +1,4 @@
+
+
+ DoubleCode Debug
+
diff --git a/app/src/debug/res/values-ru/strings.xml b/app/src/debug/res/values-ru/strings.xml
new file mode 100644
index 00000000..968c143c
--- /dev/null
+++ b/app/src/debug/res/values-ru/strings.xml
@@ -0,0 +1,4 @@
+
+
+ DoubleCode Debug
+
diff --git a/app/src/debug/res/values-zh-rCN/strings.xml b/app/src/debug/res/values-zh-rCN/strings.xml
new file mode 100644
index 00000000..968c143c
--- /dev/null
+++ b/app/src/debug/res/values-zh-rCN/strings.xml
@@ -0,0 +1,4 @@
+
+
+ DoubleCode Debug
+
diff --git a/app/src/debug/res/values/strings.xml b/app/src/debug/res/values/strings.xml
new file mode 100644
index 00000000..853acaed
--- /dev/null
+++ b/app/src/debug/res/values/strings.xml
@@ -0,0 +1,7 @@
+
+
+
+ DoubleCode Debug
+
diff --git a/app/src/main/java/com/yugahashimoto/andcode/runtime/local/LocalRuntimeManifest.kt b/app/src/main/java/com/yugahashimoto/andcode/runtime/local/LocalRuntimeManifest.kt
index f1d08d65..20ad267b 100644
--- a/app/src/main/java/com/yugahashimoto/andcode/runtime/local/LocalRuntimeManifest.kt
+++ b/app/src/main/java/com/yugahashimoto/andcode/runtime/local/LocalRuntimeManifest.kt
@@ -1,6 +1,7 @@
package com.yugahashimoto.andcode.runtime.local
import android.content.Context
+import com.yugahashimoto.andcode.BuildConfig
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
@@ -17,6 +18,9 @@ data class LocalRuntimeManifest(
fun architecture(abi: String): LocalRuntimeArchitecture =
requireNotNull(architectures[abi]) { "Local runtime does not support ABI $abi" }
+ /** This manifest with [port] replaced by [override], or unchanged when [override] is 0. */
+ fun withPortOverride(override: Int): LocalRuntimeManifest = if (override == 0) this else copy(port = override)
+
fun validate() {
require(schemaVersion == 1) { "Unsupported local runtime manifest schema: $schemaVersion" }
require(runtimeVersion.isNotBlank()) { "Runtime version is missing" }
@@ -53,13 +57,15 @@ class LocalRuntimeManifestReader(
ignoreUnknownKeys = true
isLenient = true
},
+ /** Replaces the manifest's port when non-zero, so a debug build can run beside a release one. */
+ private val portOverride: Int = BuildConfig.LOCAL_RUNTIME_PORT_OVERRIDE,
) {
/** The local server port this build uses; see [LocalRuntimeManifest.port]. */
fun port(): Int = read().port
fun read(): LocalRuntimeManifest {
val payload = context.assets.open(ASSET_NAME).bufferedReader().use { it.readText() }
- return json.decodeFromString(payload).also { it.validate() }
+ return json.decodeFromString(payload).withPortOverride(portOverride).also { it.validate() }
}
companion object {
diff --git a/app/src/test/java/com/yugahashimoto/andcode/ReleaseMetadataTest.kt b/app/src/test/java/com/yugahashimoto/andcode/ReleaseMetadataTest.kt
index 91c8c960..0746752b 100644
--- a/app/src/test/java/com/yugahashimoto/andcode/ReleaseMetadataTest.kt
+++ b/app/src/test/java/com/yugahashimoto/andcode/ReleaseMetadataTest.kt
@@ -9,7 +9,9 @@ class ReleaseMetadataTest {
fun `android package metadata matches the release tag`() {
val releaseVersion = repositoryRoot().resolve(".release-version").readText().trim().removePrefix("v")
- assertEquals(releaseVersion, BuildConfig.VERSION_NAME)
+ // Debug builds carry a "-debug" suffix (versionNameSuffix) so they are told apart on device.
+ val expected = if (BuildConfig.DEBUG) "$releaseVersion-debug" else releaseVersion
+ assertEquals(expected, BuildConfig.VERSION_NAME)
assertEquals(66, BuildConfig.VERSION_CODE)
}
diff --git a/app/src/test/java/com/yugahashimoto/andcode/runtime/local/LocalRuntimeManifestTest.kt b/app/src/test/java/com/yugahashimoto/andcode/runtime/local/LocalRuntimeManifestTest.kt
index c7e39664..695fa413 100644
--- a/app/src/test/java/com/yugahashimoto/andcode/runtime/local/LocalRuntimeManifestTest.kt
+++ b/app/src/test/java/com/yugahashimoto/andcode/runtime/local/LocalRuntimeManifestTest.kt
@@ -30,6 +30,22 @@ class LocalRuntimeManifestTest {
assertEquals(architecture, manifest.architecture("arm64-v8a"))
}
+ @Test
+ fun `port override replaces the manifest port only when set`() {
+ val manifest =
+ LocalRuntimeManifest(
+ schemaVersion = 1,
+ runtimeVersion = "2026.07.18.1",
+ openCodeVersion = "1.18.3",
+ alpineVersion = "3.24.1",
+ port = 4098,
+ architectures = mapOf("arm64-v8a" to architecture),
+ )
+
+ assertEquals(4098, manifest.withPortOverride(0).port)
+ assertEquals(4099, manifest.withPortOverride(4099).port)
+ }
+
@Test
fun `manifest rejects insecure download URL`() {
val invalid = architecture.copy(alpineUrl = "http://example.com/alpine.tar.gz")