This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Bible for Android (Alkitab / Quick Bible) — a 100% free, open-source Bible reader app for Android. Published on Google Play as "Alkitab" (Indonesian) and "Quick Bible" (non-Indonesian). The codebase supports multiple product flavors, 100+ downloadable Bible versions, song books, devotions, reading plans, cloud sync, and a daily verse widget.
- Official site: https://alkitab.app
- Developer docs: https://alkitab.app/developer
# Debug APK (open-source build, works out of the box)
./gradlew assemblePlainDebug
# Debug App Bundle
./gradlew bundlePlainDebug
# Unit tests (same as CI)
./gradlew testPlainDebugUnitTest testPlainReleaseUnitTest
# Run a single test class
./gradlew testPlainDebugUnitTest --tests "yuku.alkitab.base.util.QueryTokenizerTest"
# Run a single test method
./gradlew testPlainDebugUnitTest --tests "yuku.alkitab.base.util.QueryTokenizerTest.testQuotedPhrases"Requirements: JDK 17 (Zulu recommended), Android SDK with compile SDK 36, NDK 28.2.13676358.
The plain flavor is the open-source development build and works out of the box with the placeholder Alkitab/google-services.json checked into the repo (Firebase features won't function at runtime, but the app builds and runs). Production flavors (yuku_alkitab, yuku_quick_bible, sabda_alkitab) require:
$ALKITAB_PROPRIETARY_DIR/overlay/<applicationId>/text_raw/— proprietary Bible text$ALKITAB_PROPRIETARY_DIR/google-services.json— real Firebase config (one file with client entries for all production applicationIds)- Signing-key env vars:
SIGN_KEYSTORE,SIGN_ALIAS,SIGN_PASSWORD
With those set, build with a plain ./gradlew assembleYuku_alkitabRelease (or any other production flavor).
The Claude Code VM does not ship with a compatible JDK or the Android SDK. You must provision them yourself before running Gradle — do not rely on GitHub Actions for verification. Prefer plainDebug since it needs no proprietary overlay or signing secrets.
Install paths used below (pick any, but keep them consistent):
- JDK 17:
/home/user/tools/zulu17.64.17-ca-jdk17.0.18-linux_x64 - Android SDK:
/home/user/android-sdk
Disk footprint: expect ~6 GB across all of these combined — NDK r28c alone is ~2 GB unpacked, the rest of the SDK is ~1 GB, and ~/.gradle grows to ~2 GB after the first build. Check free space before starting.
Environment. Every step after the JDK install needs the same env vars. Write them once and source them each time:
cat > /home/user/tools/android-env.sh <<'EOF'
export JAVA_HOME=/home/user/tools/zulu17.64.17-ca-jdk17.0.18-linux_x64
export ANDROID_HOME=/home/user/android-sdk
export PATH="$JAVA_HOME/bin:$ANDROID_HOME/cmdline-tools/latest/bin:$ANDROID_HOME/platform-tools:$PATH"
unset JAVA_TOOL_OPTIONS # strip the sandbox's -Dhttp.proxyHost that would poison Gradle downloads
EOFThen source /home/user/tools/android-env.sh at the start of any shell that runs sdkmanager or Gradle.
-
Install Zulu JDK 17 (the preinstalled JDK is 21, which the Android Gradle Plugin rejects for the
jvmToolchain(17)used across modules):mkdir -p /home/user/tools && cd /home/user/tools curl -fsSL -o zulu17.tar.gz \ https://cdn.azul.com/zulu/bin/zulu17.64.17-ca-jdk17.0.18-linux_x64.tar.gz echo "819e3f09ea628901a21b2104ed8f5256e17ae91a4145b272b2eb2131f832af1d zulu17.tar.gz" | sha256sum -c - tar xzf zulu17.tar.gz && rm zulu17.tar.gz
-
Trust the sandbox egress CA in the JDK truststore. Outbound HTTPS in the Claude Code sandbox goes through an Anthropic TLS-inspection proxy (
sandbox-egress-production TLS Inspection CA).curltrusts it via/etc/ssl/certs, but the JDK keeps its owncacerts, sosdkmanagerand Gradle will fail withPKIX path building faileduntil you import the system CAs:JAVA_HOME=/home/user/tools/zulu17.64.17-ca-jdk17.0.18-linux_x64 for crt in /usr/local/share/ca-certificates/*.crt; do "$JAVA_HOME/bin/keytool" -importcert -noprompt -trustcacerts \ -keystore "$JAVA_HOME/lib/security/cacerts" -storepass changeit \ -alias "$(basename "$crt" .crt)" -file "$crt" done
-
Install the Android SDK command-line tools, then use
sdkmanagerto fetch the exact packages Gradle expects (compileSdk 36,build-tools 36.0.0,ndk 28.2.13676358— the NDK is required because theSnappymodule has JNI C++; note thatsdkmanagerresolves that NDK coordinate toandroid-ndk-r28con disk, which is expected):mkdir -p /home/user/android-sdk/cmdline-tools && cd /home/user/android-sdk/cmdline-tools curl -fsSL -o cmdline-tools.zip \ https://dl.google.com/android/repository/commandlinetools-linux-13114758_latest.zip echo "7ec965280a073311c339e571cd5de778b9975026cfcbe79f2b1cdcb1e15317ee cmdline-tools.zip" | sha256sum -c - unzip -q cmdline-tools.zip && mv cmdline-tools latest && rm cmdline-tools.zip source /home/user/tools/android-env.sh yes | sdkmanager --licenses > /dev/null # silent; prints thousands of "y\n" lines otherwise sdkmanager "platform-tools" "platforms;android-36" \ "build-tools;36.0.0" "ndk;28.2.13676358"
-
Write
local.propertiesso Gradle skips its own SDK discovery:echo "sdk.dir=/home/user/android-sdk" > local.properties
-
Build.
source /home/user/tools/android-env.sh ./gradlew assemblePlainDebugExpect ~3 minutes cold (Gradle downloads the 9.0.0 distribution and dependencies into
~/.gradle). The APK lands atAlkitab/build/outputs/apk/plain/debug/Alkitab-<code>-<version>-<hash>-yuku.alkitab.debug-dev.apk.
Code changes should be verified with a local ./gradlew assemblePlainDebug (and relevant testPlainDebugUnitTest invocations) before committing — do not treat the GitHub Actions run as the first build.
The project is a multi-module Gradle build. The main app module is :Alkitab. All other modules are libraries:
| Module | Purpose |
|---|---|
AlkitabModel |
Core data models (Ari, Version, Book, SingleChapterVerses, MVersion) |
AlkitabIo |
BibleReader interface, UTF-8 decoding, gzip stream handling |
AlkitabYes2 |
YES2 binary Bible format reader/writer with Snappy compression |
AlkitabIntegration |
Inter-app communication API (intent-based verse lookup) |
AlkitabFeedback |
User feedback module |
BiblePlus |
PalmBible+ PDB format reader |
KpriModel |
Song/hymn data model (Song, Verse, Lyric) |
BintexReader / BintexWriter |
Binary serialization format used by YES2 and reading plans |
Afw |
Base Android framework (preferences wrapper, adapter base, app context) |
Snappy |
JNI Snappy compression (native C++ via NDK) |
FlowLayout |
Flow layout widget |
ImportedDesktopVerseUtil |
Desktop verse reference finder/parser |
Bible-version downloads run inside VersionDownloadWorker (a CoroutineWorker) using the shared OkHttp client; the previous PrDownloaderFixed module was deleted in REM-19.
App.java— Application class, extendsyuku.afw.App. Initializes Firebase, FCM registration, preference defaults, extension receivers. Holds the eagerly-initializedApp.services(AppServicescontainer) introduced by REM-24.S.kt— Legacy service locator (~317 lines). Holds lazy references toInternalDb,SongDb, active Bible version state, and calculated UI dimensions. New code should depend on the narrowerStorageProvider/VersionManager/UiDimensionsProviderinterfaces viaApp.services.*; existingS.foocall sites are being migrated incrementally (REM-24).IsiActivity.kt(~2170 lines, atyuku/alkitab/base/IsiActivity.kt) — Main Bible reader activity. Verse display, navigation history, and volume-button navigation still live here; gesture handling, the action-mode callback, and split-view management have been extracted intoReaderGestureHandler(REM-06),VerseActionModeController(REM-07), andSplitViewManager(REM-08) respectively.
S.activeVersion() → MVersion → Version (abstract)
→ Version.loadChapterText() → SingleChapterVerses
→ Version.loadPericope() → section headers
→ VersesDataModel (merges verses + pericopes via itemPointer array)
→ VersesControllerImpl (RecyclerView adapter)
→ VerseRenderer / FormattedTextRenderer (applies formatting codes)
→ VerseItem (custom RelativeLayout with highlight/selection drawing)
The fundamental addressing scheme — a 24-bit integer encoding book, chapter, and verse:
bits 23-16: bookId (0-65)
bits 15-8: chapter (1-based, 0 = whole book)
bits 7-0: verse (1-based, 0 = whole chapter)
Used everywhere: database storage, intent extras, sync protocol, content provider URIs. See Ari.java in AlkitabModel.
The app uses three SQLite files — AlkitabDb and SongDb (hand-rolled SQLiteOpenHelpers, still the active sources of truth for everything Bible-related), plus AlkitabSongRoomDb (Room-backed, the active source of truth for the Songs subsystem after REM-32).
AlkitabDb— hand-rolledSQLiteOpenHelper(InternalDbHelper). Setsuser_versionto the build-timeApp.getVersionCode()and migrates throughonUpgradebased on that. Tables:- Marker — bookmarks, notes, highlights (distinguished by
kindcolumn). Each row has agid(globally unique ID) for sync. - Label — bookmark categories with custom background colors. Columns are Indonesian (
judul,urutan,warnaLatar). - Marker_Label — many-to-many junction between markers and labels.
- Version — metadata for downloaded Bible versions (filename, locale, active flag, ordering).
- Devotion — cached devotional articles keyed by
(name, date, dataFormatVersion). - PerVersion — per-version settings keyed by
versionId. - ProgressMark / ProgressMarkHistory — 5 reading progress pins (addressed by
preset_id) plus the append-only history of every pin update. - ReadingPlan / ReadingPlanProgress — downloaded reading plans (metadata + RPB binary blob) and per-day completion rows.
- SyncShadow / SyncLog — sync state tracking (last-synced entity snapshot per sync set) plus an append-only audit log of every sync event.
- Marker — bookmarks, notes, highlights (distinguished by
SongDb— hand-rolledSQLiteOpenHelper(SongDbHelper). After REM-32, every table here is a rollback safety net only, plus the source of legacy rows forSongDbDataMigrationon first launch with the migrated code.AlkitabSongRoomDb— Room database (yuku.alkitab.base.storage.room.SongRoomDatabase, at@Database(version = 1)). Holds the Songs subsystem tables migrated as part of REM-32. Lives in its own SQLite file because the Songs module is genuinely self-contained — it shares no rows, FKs, or transactions with the Bible-reading tables.- song_info (REM-32) — one row per song, with
(bookName, code)and(bookName, ordering)indexes. ThedataBLOB stores a Parcelable-marshalledyuku.kpri.model.Songsnapshot (REM-21 will swap this to JSON without touching the storage layer). - song_book_info (REM-32) — one row per installed song book, indexed by
name.
- song_info (REM-32) — one row per song, with
The legacy SongInfo / SongBookInfo tables are still created in SongDb as a rollback safety net. SongDbDataMigration (wired from S.songDb's lazy initializer) copies them into Room on first launch with the migrated code; the SongDb facade preserves the public surface so callers don't change. InternalDb plus its per-table facades (MarkerDao, LabelDao, Marker_LabelDao, VersionDao, DevotionDao, PerVersionDao, ProgressMarkDao, ReadingPlanDao, SyncShadowDao) talk directly to AlkitabDb via raw SQL — Room was previously rolled in here (REM-10/11/27-31) but reverted before reaching production; see docs/tech-debt-remediation.md for the rationale.
Verse text uses inline formatting codes (processed by VerseRenderer and FormattedTextRenderer):
@@— marks verse as formatted@0–@4,@^— paragraph indentation levels@6/@5— red letter (Jesus' words) start/end@9/@7— italic start/end@8— line break@<tag@>...@/— special inline elements (cross-references, footnotes)
FormattedVerseText.removeSpecialCodes() strips these for plain-text operations (copy, search).
Delta-based sync over REST (/sync/api/sync). Each entity type has a dedicated class:
Sync_Mabel— markers, labels, marker-label associationsSync_Pins— progress marksSync_Rp— reading plan progressSync_History— reading history
Uses operation deltas (add/mod/del) with base revisions. Firebase Cloud Messaging triggers sync on other devices. Auth is simple token-based (simpleToken).
yuku.alkitab.base.cp.Provider — read-only ContentProvider for external apps to query Bible verses by ARI or LID (sequential verse ID). Supports single verse, range queries, and version listing.
Custom binary format for Bible text files (.yes):
- Header magic bytes, then section index (Bintex-encoded)
- Sections:
versionInfo,booksInfo,text,xrefs,footnotes,pericopies - Text section supports Snappy compression
- Loaded via
YesReaderFactory→Yes2Reader - PDB (PalmBible+) files are converted to YES2 on import
Detailed documentation for each major feature module:
- Songs — Song book browsing, search, audio (MP3/MIDI) playback
- Reading Plans — RPB binary format, daily progress tracking
- Versions — Bible version management, download, YES2 format
- Markers — Bookmarks, notes, highlights system
- Sync — Cloud sync protocol and FCM push
- Devotions — Daily devotional articles
- Audio Playback — ExoPlayer/MIDI controllers for songs
- Search — Full-text verse search engine
- Daily Verse Widget — Home screen widget
- Data Transfer — JSON export/import of user data
- Architecture Deep Dive — Singleton patterns, data flow, module dependencies
- Build System — Flavors, signing, CI/CD, release process
- Text Rendering — Verse formatting pipeline and codes
- Binary Formats — YES2, Bintex, RPB file format specs
- Storage & Database — SQLite schema, preferences, file storage
- Backend Communication — API endpoints, download flows
- Tech Debt & Improvements — Known issues with specific file/line references, potential bugs, deprecated APIs
- Tech Debt Remediation Plan — Prioritized remediation tasks with BRICE evaluation, steps, difficulty, and suggested sprint plan
- Mixed Java/Kotlin codebase (Kotlin preferred for new code, many files still Java)
- JVM toolchain 17 across all modules
- No obfuscation in ProGuard (
-dontobfuscate), only shrinking - EditorConfig disables
import-orderingandno-wildcard-importsfor Kotlin - Preference keys are defined as enum entries in
Prefkey.kt - GIDs (globally unique IDs) are used alongside database
_idfor sync-capable entities Ariencoding is used universally for verse references — never store book/chapter/verse separately- Version IDs follow format
"internal","preset/[name]", or"file/[path]"
- Prefer Kotlin backtick identifiers with descriptive sentences for test function names — they render as readable prose in JUnit output. Use full sentences, not short names.
// Good @Test fun `patchNoConflict treats add on an existing gid as an overwrite (same as mod)`() { ... } // Avoid @Test fun patchNoConflict_addExistingGid_overwrites() { ... }
- Use Robolectric if needed for tests that exercise Android framework code (Context, Intents, Parcelable, DB helpers, etc.). Pure-logic tests should stay plain JUnit. If an Android method is blocking a pure-logic test with
"Method not mocked", first consider whether a tiny test-scope shadow (e.g.src/test/java/android/util/Pair.java) is enough before reaching for Robolectric. - When writing unit tests, assume the production code is correct. If you spot what looks like an obvious bug while writing tests, flag it rather than silently working around it.
- Do not include SHAs or commit hashes in long-lived docs (
docs/tech-debt.md,docs/tech-debt-remediation.md, thisCLAUDE.md, etc.). They rot on rebase/squash-merge and are fragile to maintain. When recording that a step is done, describe what shipped (file paths, test counts, scope) and use an absolute date — never a SHA. (Older entries in these docs may still reference SHAs; leave them alone unless explicitly asked to clean up.)
IsiActivity.ktis ~2170 lines — still large, but no longer monolithic: gesture handling, the action-mode callback, and split-view management were extracted (REM-06/07/08) intoReaderGestureHandler,VerseActionModeController, andSplitViewManagerunderwidget/. Bible reading, navigation history, and volume-button handling are still inline; changes here require careful testing.KpriModel.SongusesParcelableserialization for database storage (acknowledged as a bad design decision in the code). REM-32 migrated the storage engine to Room but preserves the Parcelable payload byte-for-byte; REM-21 (Phase 4) will swap the payload to JSON without touching the storage layer.- The
Snappymodule has native C++ code — NDK must be installed for builds. - A placeholder
Alkitab/google-services.jsonis checked in soplainDebugworks out of the box; Firebase features won't actually function with it. For production flavors, the realgoogle-services.jsonis sourced from$ALKITAB_PROPRIETARY_DIR/google-services.jsonat build time and copied into the gitignoredAlkitab/src/<flavor>/google-services.json(where the GMS plugin's source-set lookup finds it). - The internal Bible version data is split per flavor: the placeholder
ddd_*files live underAlkitab/src/plain/assets/internal/(used by theplainopen-source build only). Production flavors get theirtb_*/kjv_*files copied from the proprietary overlay intobuild/generated/proprietaryAssets/<flavor>/internal/at build time. - Backend host:
BuildConfig.SERVER_HOSTresolves tohttps://api.alkitab.app(defined inAlkitab/build.gradle.kts), not the marketing site atalkitab.app. The two hostnames are different origins behind separate Cloudflare cache zones — when reproducing a backend issue withcurl, hitapi.alkitab.app/...to match what the app actually requests.