From c920e8805b7c855387edb7df18f4c0d10100def4 Mon Sep 17 00:00:00 2001 From: wtclaude Date: Sun, 19 Jul 2026 12:52:35 -0500 Subject: [PATCH] chore(scaffold): M0 Gradle + Compose + Hilt skeleton with CI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Stand up the Android-app repo per docs/android/PLAN.md M0: a buildable Kotlin + Jetpack Compose (Material 3) single-activity skeleton wired for Hilt, ready for the M1-M4 functional pass. - Gradle 8.7 wrapper; AGP 8.6.1 / Kotlin 2.0.20, JDK 17, minSdk 29, target 35. - Version catalog (gradle/libs.versions.toml) pins the full planned stack (Compose, Hilt, Retrofit/OkHttp + kotlinx.serialization, DataStore, security-crypto, Coil, Navigation) so later milestones reference by alias. - RunicGatewayApp (@HiltAndroidApp) + MainActivity (Compose) + ui/theme/*. - Strings externalized from day one; adaptive launcher icon; backup rules exclude the token store / DataStore (no session material off-device). - CI: .gitea/workflows/pr-checks.yml gates PRs with lint + test + assembleDebug (JDK 17 + Android SDK on the self-hosted runner; debug builds auto-signed, no secrets). Placeholder JVM unit test so the test gate runs. - .gitattributes forces LF on gradlew so the wrapper runs on the Linux runner. Verified locally: `gradle help`/`projects` configure the :app module and resolve all six plugins cleanly (full assemble needs the Android SDK, done in CI). app id: com.runicgateway.app (PLAN.md §13, pending runicgateway.app domain). Co-Authored-By: Claude --- .gitattributes | 19 ++ .gitea/workflows/pr-checks.yml | 58 ++++ .gitignore | 30 +++ README.md | 59 +++++ app/build.gradle.kts | 107 ++++++++ app/proguard-rules.pro | 3 + app/src/main/AndroidManifest.xml | 32 +++ .../java/com/runicgateway/app/MainActivity.kt | 64 +++++ .../com/runicgateway/app/RunicGatewayApp.kt | 15 ++ .../com/runicgateway/app/ui/theme/Color.kt | 16 ++ .../com/runicgateway/app/ui/theme/Theme.kt | 53 ++++ .../com/runicgateway/app/ui/theme/Type.kt | 21 ++ .../res/drawable/ic_launcher_foreground.xml | 15 ++ .../res/mipmap-anydpi-v26/ic_launcher.xml | 7 + .../mipmap-anydpi-v26/ic_launcher_round.xml | 7 + app/src/main/res/values/colors.xml | 5 + app/src/main/res/values/strings.xml | 12 + app/src/main/res/values/themes.xml | 10 + app/src/main/res/xml/backup_rules.xml | 10 + .../main/res/xml/data_extraction_rules.xml | 16 ++ .../runicgateway/app/ScaffoldSanityTest.kt | 25 ++ build.gradle.kts | 11 + gradle.properties | 11 + gradle/libs.versions.toml | 96 +++++++ gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 43453 bytes gradle/wrapper/gradle-wrapper.properties | 7 + gradlew | 249 ++++++++++++++++++ gradlew.bat | 92 +++++++ settings.gradle.kts | 27 ++ 29 files changed, 1077 insertions(+) create mode 100644 .gitattributes create mode 100644 .gitea/workflows/pr-checks.yml create mode 100644 .gitignore create mode 100644 README.md create mode 100644 app/build.gradle.kts create mode 100644 app/proguard-rules.pro create mode 100644 app/src/main/AndroidManifest.xml create mode 100644 app/src/main/java/com/runicgateway/app/MainActivity.kt create mode 100644 app/src/main/java/com/runicgateway/app/RunicGatewayApp.kt create mode 100644 app/src/main/java/com/runicgateway/app/ui/theme/Color.kt create mode 100644 app/src/main/java/com/runicgateway/app/ui/theme/Theme.kt create mode 100644 app/src/main/java/com/runicgateway/app/ui/theme/Type.kt create mode 100644 app/src/main/res/drawable/ic_launcher_foreground.xml create mode 100644 app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml create mode 100644 app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml create mode 100644 app/src/main/res/values/colors.xml create mode 100644 app/src/main/res/values/strings.xml create mode 100644 app/src/main/res/values/themes.xml create mode 100644 app/src/main/res/xml/backup_rules.xml create mode 100644 app/src/main/res/xml/data_extraction_rules.xml create mode 100644 app/src/test/java/com/runicgateway/app/ScaffoldSanityTest.kt create mode 100644 build.gradle.kts create mode 100644 gradle.properties create mode 100644 gradle/libs.versions.toml create mode 100644 gradle/wrapper/gradle-wrapper.jar create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100644 gradlew create mode 100644 gradlew.bat create mode 100644 settings.gradle.kts diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..638e943 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,19 @@ +# SPDX-License-Identifier: GPL-3.0-or-later +# Deterministic line endings across contributor platforms. + +# Default: normalize to LF in the repo. +* text=auto eol=lf + +# The Gradle wrapper POSIX script MUST stay LF (runs on the Linux CI runner). +gradlew text eol=lf + +# Windows batch files must be CRLF. +*.bat text eol=crlf + +# Binary assets — never touch line endings. +*.jar binary +*.keystore binary +*.jks binary +*.png binary +*.webp binary +*.ico binary diff --git a/.gitea/workflows/pr-checks.yml b/.gitea/workflows/pr-checks.yml new file mode 100644 index 0000000..bdb2815 --- /dev/null +++ b/.gitea/workflows/pr-checks.yml @@ -0,0 +1,58 @@ +# Gate every pull request into `main` on lint + unit tests + a debug build, so a +# broken build can't reach the deployable branch. Debug builds are auto-signed, +# so this gate needs no secrets. The signed *release* APK + Gitea release come +# later (release.yml, M6). See docs/android/PLAN.md §12. +# +# Enforcement (one-time, in the Gitea UI): +# Repository Settings -> Branches -> Branch Protection (rule for `main`) +# * Enable Status Check +# * Status check patterns: PR Checks / * +# +# Runner: the org's self-hosted `ubuntu-latest`, on a bare `ubuntu:latest` +# container that lacks git/curl/unzip (needed by checkout + sdkmanager) -- so the +# first step installs them. (Faster later: switch to a prebuilt Android-SDK +# container image so nothing installs per-run.) + +name: PR Checks + +on: + pull_request: + branches: [main] + +concurrency: + group: pr-checks-${{ github.ref }} + cancel-in-progress: true + +jobs: + android-build: + runs-on: ubuntu-latest + steps: + # Bare ubuntu:latest is missing the tools checkout + the SDK installer need. + - name: Install base tools + run: | + apt-get update + apt-get install -y git curl unzip + + - uses: actions/checkout@v4 + + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: "17" + + - name: Set up Android SDK + uses: android-actions/setup-android@v3 + + - name: Cache Gradle + uses: actions/cache@v4 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: gradle-${{ runner.os }}-${{ hashFiles('**/*.gradle.kts', 'gradle/libs.versions.toml', 'gradle/wrapper/gradle-wrapper.properties') }} + restore-keys: | + gradle-${{ runner.os }}- + + - name: Lint, test, assemble debug + run: ./gradlew --no-daemon lint test assembleDebug diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8cedf8f --- /dev/null +++ b/.gitignore @@ -0,0 +1,30 @@ +# SPDX-License-Identifier: GPL-3.0-or-later +# Android / Gradle / IDE ignores. + +# Built output +/build/ +/app/build/ +*.apk +*.aab +*.ap_ +*.dex + +# Gradle +.gradle/ +local.properties + +# Keystores / signing material — never commit (release keystore is a CI secret, PLAN.md §12) +*.jks +*.keystore +keystore.properties + +# Android Studio / IntelliJ +.idea/ +*.iml +.DS_Store +captures/ +.externalNativeBuild/ +.cxx/ + +# Kotlin +.kotlin/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..76d5d08 --- /dev/null +++ b/README.md @@ -0,0 +1,59 @@ + +# Runic Gateway — Android app + +A native Android client for a Runic Gateway shard's **public site + player self-service**. It is +**purely an API client of the website backend** — it never talks to the `link/` sidecar or the game +shard directly, and it ships none of the shard/sidecar wiring. It surfaces the same content and +player features as the website's browser client, **minus every administrative/management console**. + +The authoritative design contract is [`docs/android/PLAN.md`](https://gitea.whitlocktech.com/RunicGateway/docs) +in the `RunicGateway/docs` repo. The authoritative API reference is the committed OpenAPI spec at +`website/server/swagger/swagger-output.json`. + +## Status + +**M0 — repo scaffold.** Gradle + Compose + Hilt skeleton with CI (lint + unit test + debug build). +The functional Kotlin pass (M1–M4) and the design pass (M5) follow — see the plan's milestones (§9). + +## Stack + +| Concern | Choice | +|---|---| +| Language / UI | Kotlin + Jetpack Compose (Material 3) | +| Navigation | Navigation-Compose, single-activity | +| HTTP | Retrofit + OkHttp, `kotlinx.serialization` | +| Async | Coroutines + Flow | +| DI | Hilt | +| Prefs / base URL | Jetpack DataStore | +| Tokens at rest | EncryptedSharedPreferences | +| Images | Coil | +| Min SDK | Android 10 (API 29) | +| Target / compile SDK | 35 | + +Dependency and plugin versions are pinned in [`gradle/libs.versions.toml`](gradle/libs.versions.toml). + +## Build + +Requires **JDK 17** and the Android SDK (`ANDROID_HOME` / `local.properties`). + +```bash +./gradlew assembleDebug # build a debug APK -> app/build/outputs/apk/debug/ +./gradlew test # JVM unit tests +./gradlew lint # Android lint +./gradlew installDebug # install on a connected device/emulator +``` + +The app self-configures its server URL on first run (PLAN.md §3), so a single build works against +any shard's website — there is no compiled-in API host. + +## CI + +`.gitea/workflows/pr-checks.yml` gates PRs into `main` with `./gradlew lint test assembleDebug` on the +org's self-hosted runner (JDK 17 + Android SDK). Debug builds are auto-signed, so the gate needs no +secrets. A signed **release** APK attached to a Gitea release comes at M6. + +## Contributing + +See [`CONTRIBUTING.md`](CONTRIBUTING.md). **AI-assisted contributions must be disclosed** (org +policy): tick the PR box naming the tool and add a `Co-Authored-By` trailer to AI-authored commits. +Licensed **GPL-3.0-or-later**. diff --git a/app/build.gradle.kts b/app/build.gradle.kts new file mode 100644 index 0000000..65769c5 --- /dev/null +++ b/app/build.gradle.kts @@ -0,0 +1,107 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +plugins { + alias(libs.plugins.android.application) + alias(libs.plugins.kotlin.android) + alias(libs.plugins.kotlin.compose) + alias(libs.plugins.kotlin.serialization) + alias(libs.plugins.ksp) + alias(libs.plugins.hilt) +} + +android { + namespace = "com.runicgateway.app" + compileSdk = 35 + + defaultConfig { + // Target application id per docs/android/PLAN.md §13 (pending runicgateway.app domain). + applicationId = "com.runicgateway.app" + minSdk = 29 + targetSdk = 35 + versionCode = 1 + versionName = "0.1.0" + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + // Signing/minification are wired at M6 (release hardening). Debug is auto-signed. + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro", + ) + } + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + + kotlinOptions { + jvmTarget = "17" + } + + buildFeatures { + compose = true + buildConfig = true + } + + packaging { + resources { + excludes += "/META-INF/{AL2.0,LGPL2.1}" + } + } +} + +dependencies { + // Core / lifecycle / activity + implementation(libs.androidx.core.ktx) + implementation(libs.androidx.lifecycle.runtime.ktx) + implementation(libs.androidx.lifecycle.runtime.compose) + implementation(libs.androidx.lifecycle.viewmodel.compose) + implementation(libs.androidx.activity.compose) + + // Compose (BOM-managed versions) + implementation(platform(libs.androidx.compose.bom)) + implementation(libs.androidx.compose.ui) + implementation(libs.androidx.compose.ui.graphics) + implementation(libs.androidx.compose.ui.tooling.preview) + implementation(libs.androidx.compose.material3) + implementation(libs.androidx.navigation.compose) + debugImplementation(libs.androidx.compose.ui.tooling) + debugImplementation(libs.androidx.compose.ui.test.manifest) + + // DI + implementation(libs.hilt.android) + ksp(libs.hilt.compiler) + implementation(libs.androidx.hilt.navigation.compose) + + // Networking (wired for M1+; declared now so the stack resolves) + implementation(libs.retrofit) + implementation(platform(libs.okhttp.bom)) + implementation(libs.okhttp) + implementation(libs.okhttp.logging.interceptor) + implementation(libs.okhttp.sse) + implementation(libs.kotlinx.serialization.json) + implementation(libs.retrofit.kotlinx.serialization.converter) + + // Storage + implementation(libs.androidx.datastore.preferences) + implementation(libs.androidx.security.crypto) + + // Images + implementation(libs.coil.compose) + + // Unit tests + testImplementation(libs.junit) + testImplementation(libs.kotlinx.coroutines.test) + + // Instrumented tests + androidTestImplementation(libs.androidx.test.ext.junit) + androidTestImplementation(libs.androidx.espresso.core) + androidTestImplementation(platform(libs.androidx.compose.bom)) + androidTestImplementation(libs.androidx.compose.ui.test.junit4) +} diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..178468b --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,3 @@ +# Runic Gateway Android app — ProGuard/R8 rules. +# Minification is disabled until M6 (release hardening); real keep rules for +# kotlinx.serialization DTOs and Retrofit models are added there. diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..209e0ea --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/java/com/runicgateway/app/MainActivity.kt b/app/src/main/java/com/runicgateway/app/MainActivity.kt new file mode 100644 index 0000000..b879ddf --- /dev/null +++ b/app/src/main/java/com/runicgateway/app/MainActivity.kt @@ -0,0 +1,64 @@ +/* + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.runicgateway.app + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import com.runicgateway.app.ui.theme.RunicGatewayTheme +import dagger.hilt.android.AndroidEntryPoint + +/** + * Single-activity host. Navigation-Compose and the first-run base-URL flow (§3) + * land in M1; this M0 skeleton only proves the Compose + Hilt + theme wiring. + */ +@AndroidEntryPoint +class MainActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + enableEdgeToEdge() + setContent { + RunicGatewayTheme { + Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding -> + Placeholder(modifier = Modifier.padding(innerPadding)) + } + } + } + } +} + +@Composable +private fun Placeholder(modifier: Modifier = Modifier) { + Column( + modifier = modifier.fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + ) { + Text( + text = stringResource(id = R.string.app_scaffold_ready), + style = MaterialTheme.typography.titleLarge, + ) + } +} + +@Preview(showBackground = true) +@Composable +private fun PlaceholderPreview() { + RunicGatewayTheme { + Placeholder() + } +} diff --git a/app/src/main/java/com/runicgateway/app/RunicGatewayApp.kt b/app/src/main/java/com/runicgateway/app/RunicGatewayApp.kt new file mode 100644 index 0000000..e3c300d --- /dev/null +++ b/app/src/main/java/com/runicgateway/app/RunicGatewayApp.kt @@ -0,0 +1,15 @@ +/* + * SPDX-License-Identifier: GPL-3.0-or-later + * Runic Gateway — native Android client of a shard's website API. + */ +package com.runicgateway.app + +import android.app.Application +import dagger.hilt.android.HiltAndroidApp + +/** + * Application entry point. Annotated for Hilt so the DI graph is available + * to activities, view models, and (from M1) repositories / API services. + */ +@HiltAndroidApp +class RunicGatewayApp : Application() diff --git a/app/src/main/java/com/runicgateway/app/ui/theme/Color.kt b/app/src/main/java/com/runicgateway/app/ui/theme/Color.kt new file mode 100644 index 0000000..fdb8a88 --- /dev/null +++ b/app/src/main/java/com/runicgateway/app/ui/theme/Color.kt @@ -0,0 +1,16 @@ +/* + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.runicgateway.app.ui.theme + +import androidx.compose.ui.graphics.Color + +// Placeholder palette for the M0 skeleton. The M5 design pass replaces this and +// derives Material 3 colors from each shard's per-install branding (PLAN.md §3, §5). +val Purple80 = Color(0xFFD0BCFF) +val PurpleGrey80 = Color(0xFFCCC2DC) +val Pink80 = Color(0xFFEFB8C8) + +val Purple40 = Color(0xFF6650A4) +val PurpleGrey40 = Color(0xFF625B71) +val Pink40 = Color(0xFF7D5260) diff --git a/app/src/main/java/com/runicgateway/app/ui/theme/Theme.kt b/app/src/main/java/com/runicgateway/app/ui/theme/Theme.kt new file mode 100644 index 0000000..614b724 --- /dev/null +++ b/app/src/main/java/com/runicgateway/app/ui/theme/Theme.kt @@ -0,0 +1,53 @@ +/* + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.runicgateway.app.ui.theme + +import android.os.Build +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.darkColorScheme +import androidx.compose.material3.dynamicDarkColorScheme +import androidx.compose.material3.dynamicLightColorScheme +import androidx.compose.material3.lightColorScheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.platform.LocalContext + +private val DarkColorScheme = darkColorScheme( + primary = Purple80, + secondary = PurpleGrey80, + tertiary = Pink80, +) + +private val LightColorScheme = lightColorScheme( + primary = Purple40, + secondary = PurpleGrey40, + tertiary = Pink40, +) + +/** + * App theme for the M0 skeleton. Dynamic color (Android 12+) is used when + * available; otherwise a static placeholder scheme. The M5 design pass wires + * the color scheme to per-shard branding (PLAN.md §3, §5). + */ +@Composable +fun RunicGatewayTheme( + darkTheme: Boolean = isSystemInDarkTheme(), + dynamicColor: Boolean = true, + content: @Composable () -> Unit, +) { + val colorScheme = when { + dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { + val context = LocalContext.current + if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) + } + darkTheme -> DarkColorScheme + else -> LightColorScheme + } + + MaterialTheme( + colorScheme = colorScheme, + typography = Typography, + content = content, + ) +} diff --git a/app/src/main/java/com/runicgateway/app/ui/theme/Type.kt b/app/src/main/java/com/runicgateway/app/ui/theme/Type.kt new file mode 100644 index 0000000..d980457 --- /dev/null +++ b/app/src/main/java/com/runicgateway/app/ui/theme/Type.kt @@ -0,0 +1,21 @@ +/* + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.runicgateway.app.ui.theme + +import androidx.compose.material3.Typography +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.sp + +// Default Material 3 type scale for the skeleton; refined in the M5 design pass. +val Typography = Typography( + bodyLarge = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 16.sp, + lineHeight = 24.sp, + letterSpacing = 0.5.sp, + ), +) diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 0000000..65c870b --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,15 @@ + + + + + + + diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..cba6ee3 --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 0000000..cba6ee3 --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml new file mode 100644 index 0000000..1047d14 --- /dev/null +++ b/app/src/main/res/values/colors.xml @@ -0,0 +1,5 @@ + + + + #1B1033 + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..afc0ef1 --- /dev/null +++ b/app/src/main/res/values/strings.xml @@ -0,0 +1,12 @@ + + + + + + Runic Gateway + Runic Gateway — scaffold ready + diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml new file mode 100644 index 0000000..ed9031d --- /dev/null +++ b/app/src/main/res/values/themes.xml @@ -0,0 +1,10 @@ + + + + +