You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Session context for Activity Trace Android project
Project
Name: Activity Trace
License: GPL-3.0-only
Distribution: F-Droid (no Google Play)
Architecture: Single-Activity, Jetpack Compose, Material 3
Database: Room + SQLCipher (AES-256-CBC via Android Keystore/StrongBox)
Search: SQLite FTS5 with porter tokenizer (no AI/ML)
Min SDK: 26 (Android 8.0)
Target SDK: 36
Package: com.activitytrace
Build
./gradlew assembleDebug # debug build
./gradlew assembleRelease # release build (minified, arm64-only)
make build # same as assembleDebug
make test# unit tests
make full-test # builds both + unit tests + boots emulator + instrumented tests
make run # install debug APK on connected device
python3 build_and_test.py # automate full CI workflow
.github/workflows/security.yml: rebuilds release + unit/lint/instrumented/vuln/secret checks on push/PR
scripts/check-release-manifest.sh: fails if the release APK declares android.permission.INTERNET (must NOT be added; app is fully offline)
scripts/check-exported-components.sh: lists exported components from the release APK for human review (NotificationWidget exported=true is required for the app widget)
Gradle task :app:verifyNoInternetPermissionInRelease — same guard, name does not collide with AGP's own checkReleaseManifest task
.github/dependabot.yml: weekly scans for Gradle + GitHub Actions
No INTERNET permission in the manifest; do not add it (no network features)
Development notes
Run lint: ./gradlew lint
Run tests: ./gradlew test
Compose reports at app/build/reports/
PRs and tags used for release; no APK uploads (F-Droid builds from source)
Room schema & migrations
@Database(exportSchema = true); KSP arg room.schemaLocation="$projectDir/schemas" in app/build.gradle.kts
Schema JSONs committed to app/schemas/com.activitytrace.store.ActivityTraceDatabase/{version}.json
Unit tests (Robolectric) read schemas via android.sourceSets["debug"]/["release"].assets.srcDir("$projectDir/schemas") — the JSONs must land in the apk-for-local-test that Robolectric mounts. Robolectric does NOT serve test source-set assets, and test src/test/resources are not on the unit-test classpath (AGP routes Android source-set resources to java_res/<variant>UnitTest/out, which the worker classloader cannot resolve). Consequence: the two small schema JSONs also ship in the debug and release APKs
MigrationTestHelper (from androidx.room:room-testing) requires an Instrumentation; under Robolectric use ShadowInstrumentation.getInstrumentation() — RuntimeEnvironment has no such accessor
Migration tests must exercise the real migration through Room's open path (see ActivityTraceDatabaseMigrationTest), because Room validates the full schema incl. index names after every migration (the dedup-index v6→7 bug was an index-name mismatch)
When bumping the schema version: bump version, add MIGRATION_x_y to the builder's .addMigrations(...), build to export the new JSON, and update/add a MigrationTestHelper test
SQLCipher native libs do NOT load under Robolectric (UnsatisfiedLinkError: no sqlcipher in java.library.path). Unit tests must not touch net.zetetic open/create paths; test the recovery/key logic via a seam (DatabaseOpener, DatabaseKeyStore.WrappingKeyProvider) and push real SQLCipher assertions (plaintext-not-in-file, wrong-key-preserves-bytes, .encoded == null) to instrumented tests (DatabaseEncryptionTest). System.loadLibrary("sqlcipher") is called in ActivityTraceApplication.onCreate; UnsatisfiedLinkError under Robolectric is absorbed by the catch (Throwable) there.
Database-opening failures are never destructive: ActivityTraceDatabase.tryOpen() classifies failures (DatabaseOpenResult / RecoveryReason), persists recovery state, and never deletes the DB or key. No deleteDatabase anywhere
SQLCipher's first creation of a brand-new database can fail its own header self-verify with file is not a database (the file is left fully initialized and a subsequent open succeeds). Both getInstance and DatabaseOpener.open() retry once for this reason; do not remove the retry.
Backup round-trip (BackupRoundTripInstrumentedTest) is deliberately a NON-Compose instrumented class: it runs ATTACH + sqlcipher_export on the live Room connection, which races the SettingsScreen blocked-apps Flow if that composable is mounted simultaneously (previously intermittent file is not a database / SQLiteDiskIOException under the Compose test rule). Keep DB-exercising round-trips out of compose UI tests.
Instrumented (androidTest) JUnit methods must NOT use backtick names containing spaces/parens with a lambda in the body: Kotlin names anonymous lambda classes after the enclosing function (e.g. Foo$concurrent identical inserts produce one row$1), and DEX < 040 (minSdk 26 → DEX 037) rejects spaces in class names, so dexDebugAndroidTest fails with "Space characters in SimpleName ... are not allowed prior to DEX version 040". Use underscore names (concurrent_identical_inserts_produce_one_row) for instrumented tests; Robolectric unit tests are not dexed and can keep spaced backtick names