Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
root = true

[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
indent_style = space
indent_size = 4
trim_trailing_whitespace = true

[*.{kt,kts}]
max_line_length = 120

# The default `ktlint_official` style rewrites argument lists and expression bodies far more
# aggressively than the code here is written, and several of its rules cannot be auto-fixed,
# so a format pass leaves the build red. `intellij_idea` matches what Android Studio produces
# on Reformat Code, which is what anyone touching this repo will actually run.
ktlint_code_style = intellij_idea

# Composables are PascalCase by convention; the naming rule assumes camelCase functions.
ktlint_standard_function-naming = disabled

# Rules the intellij_idea style leaves off, all of them auto-fixable. Without these the
# formatter accepts imports in whatever order they were pasted in, which is most of what
# makes a file look untidy at a glance.
ij_kotlin_imports_layout = *
ktlint_standard_import-ordering = enabled
ktlint_standard_no-unused-imports = enabled
ktlint_standard_no-wildcard-imports = enabled
ktlint_standard_no-consecutive-blank-lines = enabled
ktlint_standard_no-trailing-spaces = enabled
ktlint_standard_no-blank-line-before-rbrace = enabled
ktlint_standard_indent = enabled
ktlint_standard_chain-wrapping = enabled
ktlint_standard_colon-spacing = enabled
ktlint_standard_comma-spacing = enabled
ktlint_standard_curly-spacing = enabled
ktlint_standard_keyword-spacing = enabled
ktlint_standard_op-spacing = enabled
ktlint_standard_paren-spacing = enabled

# Trailing commas keep one-argument-per-line diffs to a single changed line.
ij_kotlin_allow_trailing_comma = true
ij_kotlin_allow_trailing_comma_on_call_site = true
ktlint_standard_trailing-comma-on-declaration-site = enabled
ktlint_standard_trailing-comma-on-call-site = enabled

[*.{xml,yml,yaml}]
indent_size = 4

[*.{yml,yaml}]
indent_size = 2

# Generated sources: SQLDelight names classes after their tables (App_settings), which no
# naming rule will accept, and reformatting build output is pointless anyway.
[**/build/**]
ktlint = disabled
86 changes: 86 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
name: CI

on:
push:
branches: [master]
pull_request:
workflow_dispatch:

concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true

jobs:
# One job per gate, so each shows up as its own check: CI / Format, CI / Lint and
# so on, and a red run names the gate in the checks list without opening a log.
#
# A matrix rather than five copies of the same job, since the only thing that
# differs is the Gradle task. These are the tasks `./gradlew ciCheck` depends on;
# adding one there means adding a row here or the local and CI gates drift apart.
#
# The cost is that each job compiles independently. The Gradle cache absorbs most
# of it, and being able to see every failure at once is worth the minutes.
check:
name: ${{ matrix.name }}
runs-on: ubuntu-latest
timeout-minutes: 30
strategy:
fail-fast: false
matrix:
include:
- name: Format
task: ktlintCheck
- name: Static analysis
task: detekt
- name: Lint
task: ':composeApp:lintDebug'
- name: Unit tests
task: ':shared:testAndroid :composeApp:testDebugUnitTest'
- name: Debug build
task: ':composeApp:assembleDebug'

steps:
- uses: actions/checkout@v4

- name: Set up JDK 21
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: '21'

- name: Set up Gradle
uses: gradle/actions/setup-gradle@v4

- name: ${{ matrix.name }}
run: ./gradlew ${{ matrix.task }} --no-daemon --stacktrace

bundle:
name: Release bundle
runs-on: ubuntu-latest
timeout-minutes: 30
needs: check
# Proves the release variant still assembles, which is what actually ships to Play. It
# is unsigned here, the upload keystore never enters CI.
if: github.ref == 'refs/heads/master'

steps:
- uses: actions/checkout@v4

- name: Set up JDK 21
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: '21'

- name: Set up Gradle
uses: gradle/actions/setup-gradle@v4

- name: Bundle release
run: ./gradlew :composeApp:bundleRelease --no-daemon

- name: Upload bundle
uses: actions/upload-artifact@v4
with:
name: aab
path: composeApp/build/outputs/bundle/release/*.aab
retention-days: 14
29 changes: 22 additions & 7 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,16 +1,31 @@
*.iml
.gradle
/local.properties
/.idea/caches
/.idea/libraries
/.idea/modules.xml
/.idea/workspace.xml
/.idea/navEditor.xml
/.idea/assetWizardSettings.xml
/.idea
.DS_Store
/build
**/build
/captures
.externalNativeBuild
.cxx
local.properties
*.aab
*.apk

# The Kotlin plugin's per-build session directory. Appears at the root the first
# time the project is opened or built, and is pure scratch.
.kotlin/

# Heap dumps from the Studio profiler. Large, and easy to commit by accident.
*.hprof

# Signing material must never be committed
keystore.properties
*.jks
*.keystore

# Android Studio's "Inspect Code" HTML export, which lands in the project root.
# Root-scoped on purpose: these are generic names and should not silently swallow
# a real file somewhere in the tree.
/index.html
/script.js
/styles.css
1 change: 0 additions & 1 deletion .idea/.name

This file was deleted.

6 changes: 0 additions & 6 deletions .idea/AndroidProjectSystem.xml

This file was deleted.

6 changes: 0 additions & 6 deletions .idea/compiler.xml

This file was deleted.

10 changes: 0 additions & 10 deletions .idea/deploymentTargetSelector.xml

This file was deleted.

19 changes: 0 additions & 19 deletions .idea/gradle.xml

This file was deleted.

10 changes: 0 additions & 10 deletions .idea/migrations.xml

This file was deleted.

10 changes: 0 additions & 10 deletions .idea/misc.xml

This file was deleted.

17 changes: 0 additions & 17 deletions .idea/runConfigurations.xml

This file was deleted.

6 changes: 0 additions & 6 deletions .idea/vcs.xml

This file was deleted.

73 changes: 73 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# Working in this repo

Notes for anyone, human or otherwise, changing this codebase. Things the code
itself does not say.

## Layout

shared/ domain model, repositories, SQLDelight schema, most of the tests
composeApp/ Android app: UI, widget, notifications, camera, migration
config/ detekt configuration
store-assets/ Play listing assets, with the scripts that generate them

`shared` is multiplatform-shaped but only Android is wired up. Nothing outside
`androidMain` may depend on Android.

## Commands

./gradlew :composeApp:assembleDebug
./gradlew ciCheck # ktlint, detekt, Android Lint, unit tests, debug build
./gradlew ciCheckDevice # the above plus instrumented tests

`ciCheck` and `.github/workflows/ci.yml` must stay in step. CI runs one task per
job step so failures are attributable; adding a task to `ciCheck` means adding a
step there too.

Run the formatter and linter after changing code, and fix what they flag.

## Conventions

- ktlint is wired through the CLI, not the Gradle plugin. AGP 9 supplies its own
Kotlin plugin, so `org.jetbrains.kotlin.android` is never applied and
ktlint-gradle registers no source-set tasks. Style comes from `.editorconfig`.
- American English throughout, including comments and strings. The pre-2.0 code
was British.
- Radii come from `EdrShapes`. No one-off `RoundedCornerShape` values.
- Comments explain why, not what. A rule or a workaround with no stated reason is
one nobody can safely delete later.

## Things that have already gone wrong

- **`LegacyImporter`** is the only path that can lose data users cannot get back.
It reads the 1.5 database and image cache on first launch, once, and leaves the
original files alone. Treat changes here as the highest-risk edits in the repo.
- **R8 strips ML Kit's reflective constructors.** The failure is silent: the app
starts, logs `NoSuchMethodException` at WARN, and scanning never works. Keep
rules and their reasoning are in `composeApp/proguard-rules.pro`. A debug build
cannot show this, so exercise `assembleRelease` before shipping.
- **Widget sizing** lives in `WidgetSizing.kt` as plain Kotlin, deliberately, so
it is testable without Glance. It still leaves dead space and drops its overflow
line at tall sizes.
- **Schema defaults and `AppSettings()` must agree.** They silently disagreed
once; `SettingsDefaultsTest` is the guard.
- **Instrumented test method names cannot contain commas.** Dex will not represent
them. JVM tests can keep backticked sentences.
- **Robolectric boots the real `EdrApplication`** and Koin then refuses to start
twice. Use `@Config(application = Application::class)`.

## Signing

Release builds are signed only when `keystore.properties` exists in the project
root, holding `storeFile`, `storePassword`, `keyAlias` and `keyPassword`. It is
gitignored, along with `*.jks` and `*.keystore`, and must stay that way. Without
it the build produces an unsigned artifact rather than failing configuration, so
CI needs no secrets.

Upload `mapping.txt` with every bundle or Play Console crash reports are
unreadable.

## Known gaps

- The eight translations are machine generated and unreviewed.
- OCR and the GenAI path have only ever run on an emulator.
- Install-over-1.5 has never been tested through a real Play install.
Loading
Loading