The app stores data in three separate SQLite files. The two legacy SQLiteOpenHelper-managed files (AlkitabDb, SongDb) remain the active sources of truth for everything Bible-related; the Songs subsystem moved to Room (AlkitabSongRoomDb) under REM-32.
AlkitabDb— hand-rolledSQLiteOpenHelper(InternalDbHelper). Setsuser_versionto the build-timeApp.getVersionCode()and migrates throughonUpgradebased on that. Holds every Bible-related table (markers, labels, versions, devotions, per-version settings, progress marks, reading plans, sync shadows, sync logs).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 (SongRoomDatabaseat@Database(version = 1)). Owns thesong_infoandsong_book_infotables. Lives in a separate file fromAlkitabDbbecause the Songs subsystem is module-isolated (see Songs) — it shares no rows, foreign keys, or transactions with the Bible-reading tables.
InternalDb plus per-table facade DAOs (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 / REM-11 / REM-27 / REM-28 / REM-29 / REM-30 / REM-31) but reverted before reaching production; see docs/tech-debt-remediation.md for the rationale. The SongDb facade routes through SongRoomDatabase.
Schema is created in InternalDbHelper.onCreate and migrated in InternalDbHelper.onUpgrade. Tables:
Marker — bookmarks, notes, highlights
_id INTEGER PRIMARY KEY AUTOINCREMENT
gid TEXT -- globally unique ID for sync
ari INTEGER NOT NULL -- verse reference (ARI encoding)
kind INTEGER NOT NULL -- 0=bookmark, 1=note, 2=highlight
caption TEXT -- title/content/highlight JSON
verseCount INTEGER NOT NULL
createTime INTEGER NOT NULL -- epoch seconds
modifyTime INTEGER NOT NULL -- epoch seconds
-- Indices: ari, (kind, ari), (kind, modifyTime), (kind, createTime), gid,
-- (kind, caption COLLATE NOCASE)Label — bookmark categories (Indonesian column names from the legacy schema)
_id INTEGER PRIMARY KEY AUTOINCREMENT
gid TEXT
judul TEXT -- title
urutan INTEGER -- ordering
warnaLatar TEXT -- backgroundColorMarker_Label — junction table
_id INTEGER PRIMARY KEY AUTOINCREMENT
gid TEXT
marker_gid TEXT
label_gid TEXTVersion — downloaded Bible versions
_id INTEGER PRIMARY KEY AUTOINCREMENT
locale TEXT
shortName TEXT
longName TEXT
description TEXT
filename TEXT
preset_name TEXT
modifyTime INTEGER
active INTEGER -- 0 or 1
ordering INTEGERProgressMark — 5 reading progress pins
preset_id INTEGER PRIMARY KEY -- 0-4
caption TEXT
ari INTEGER
modifyTime TEXTProgressMarkHistory — append-only history of every pin update
_id INTEGER PRIMARY KEY AUTOINCREMENT
progress_mark_preset_id INTEGER
progress_mark_caption TEXT
ari INTEGER
createTime INTEGERReadingPlan — reading plan data
_id INTEGER PRIMARY KEY AUTOINCREMENT
version INTEGER
name TEXT
title TEXT
description TEXT
duration INTEGER -- total days
startTime INTEGER
data BLOB -- RPB binary dataReadingPlanProgress — completion tracking
_id INTEGER PRIMARY KEY AUTOINCREMENT
reading_plan_progress_gid TEXT NOT NULL
reading_code INTEGER NOT NULL -- (day << 8) | sequence
checkTime INTEGER
-- Unique index: (reading_plan_progress_gid, reading_code)Devotion — cached devotional articles
_id INTEGER PRIMARY KEY AUTOINCREMENT
name TEXT
date TEXT -- yyyymmdd
readyToUse INTEGER
body TEXT
touchTime INTEGER
dataFormatVersion INTEGERSyncShadow — last-synced entity state
_id INTEGER PRIMARY KEY AUTOINCREMENT
syncSetName TEXT NOT NULL
revno INTEGER NOT NULL
data BLOB
-- Index: syncSetName (non-unique)A SyncShadow row's data column can exceed the Android CursorWindow 2 MB cap (full Mabel snapshots routinely cross that mark); SyncShadowDao.getBySyncSetName reads the blob in 1 MB chunks via SQLite's substr().
SyncLog — sync audit log
_id INTEGER PRIMARY KEY AUTOINCREMENT
createTime INTEGER NOT NULL
kind INTEGER NOT NULL -- SyncRecorder.EventKind.code
syncSetName TEXT
params TEXT -- JSON of the kvpairs passed to SyncRecorder.log
-- Index: createTime (non-unique)PerVersion — per-version settings
_id INTEGER PRIMARY KEY AUTOINCREMENT
versionId TEXT NOT NULL
settings TEXT -- JSON settings blob
-- Unique index: versionIdyuku.alkitab.base.storage.room.SongRoomDatabase at @Database(version = 1). Lives in its own SQLite file so the Songs subsystem stays module-isolated. Schema JSON is exported under Alkitab/schemas/yuku.alkitab.base.storage.room.SongRoomDatabase/.
song_info — every song in every installed song book
_id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL
bookName TEXT
code TEXT
title TEXT
title_original TEXT
ordering INTEGER NOT NULL -- caller-supplied display order
dataFormatVersion INTEGER NOT NULL -- currently 3
data BLOB -- Parcelable-marshalled yuku.kpri.model.Song
updateTime INTEGER NOT NULL -- Sqlitil.nowDateTime()
-- Indices: (bookName, code) non-unique, (bookName, ordering) non-uniqueThe data BLOB stores the song's lyrics, verses, refrains, scripture references, etc. as a Parcelable-marshalled Song. This was acknowledged as a bad design choice in the code; REM-21 (Phase 4) will swap the payload to JSON without touching the storage layer.
song_book_info — installed song book metadata
_id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL
name TEXT -- the lookup key, e.g. "NKB" / "PKJ" / "KJ"
title TEXT
copyright TEXT
-- Index: name (non-unique, matching legacy `SongBookInfo_001_index`)The legacy SongDb SQLite file (managed by SongDbHelper) is left intact as a rollback safety net; SongDbDataMigration performs a one-time idempotent copy of both tables into Room on first launch with the migrated code, gated by Prefkey.song_db_data_migration_v1_done (flag-based gate to avoid the #195 resurrect-on-clear footgun).
Accessed via Afw.Preferences wrapper with caching and batch commits. Keys defined in Prefkey.kt enum (100+ entries).
- UI:
fontSize,typeface,lineSpacing,backgroundColor,textColor,verseNumberColor,nightMode - Navigation:
lastBookId,lastChapter,lastVerse,lastSplitVersion,lastSplitOrientation - Sync:
sync_simpleToken,fcm_registrationId,installation_id - Reading:
activeReadingPlanId,currentReadingPlanProgress - Search:
searchHistory - Devotion:
devotion_lastDate,devotion_kind - Widget: per-widget settings keyed by widget ID
files/version_config.json— cached version catalog update from serverfiles/*.yes— downloaded Bible version filesfiles/pdb-*.yes— converted PDB Bible versionsfiles/songs/— cached MIDI files for song playback
assets/internal/— placeholder internal Bible version data (73 files)assets/version_config.json— bundled version catalog (fallback)assets/help/— help documentation HTMLassets/templates/— template files
The app previously supported reading .yes files from external storage (READ_EXTERNAL_STORAGE / WRITE_EXTERNAL_STORAGE). These permissions are capped at max SDK 32, reflecting the move to scoped storage.
All database access goes through InternalDb (accessed via App.services.storage.db, or the legacy S.db). Common patterns:
- Markers:
insertOrUpdateMarker(),deleteMarkerById(),listMarkersForAriKind()— route throughMarkerDao→ raw SQL onAlkitabDb - Highlights:
updateOrInsertHighlights()— manages per-verse highlight data - Attributes:
putAttributes()— loads bookmark/note/highlight counts for verse display - Versions:
listAllVersions()— retrieves all versions sorted by ordering, viaVersionDao→ raw SQL onAlkitabDb - Devotions:
storeArticleToDevotions()— caches downloaded articles - Songs:
SongDb.storeSongs()/SongDb.getSong()— route throughSongRoomDao→ Room
Database operations are generally synchronous on the calling thread. The Songs Room database enables allowMainThreadQueries() to keep the existing synchronous facade contract.