This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
./gradlew assembleProDebug- Build the Pro debug variant./gradlew assembleProDebugAndroidTest- Build the Pro debug test APK./gradlew assembleLiteDebug- Build the Lite debug variant./gradlew bundleProRelease- Build Pro release bundle for Play Store./gradlew bundleLiteRelease- Build Lite release bundle for Play Store./build-test.sh- Convenience script to build Pro debug and test APKs
./gradlew testProDebugUnitTest- Run JVM unit tests (no device needed)./gradlew connectedAndroidTest- Run instrumented tests on connected devicefastlane android tests- Alternative way to run connected tests
./gradlew lintProDebug- Run Android lint checks (errors fail the build)./gradlew spotlessApply- Apply formatting (google-java-format AOSP for java, ktfmt kotlinlang style for kotlin)./gradlew spotlessCheck- Verify formatting; CI runs this first
fastlane android deployPro- Deploy Pro version to Google Play internal trackfastlane android deployLite- Deploy Lite version to Google Play internal track
./gradlew clean- Clean build artifacts (includes custom .cxx directory cleanup)
Document Processing Pipeline:
CoreLoader- Primary document processor using the native C++ ODR core libraryRawLoader- Plain text and other raw file processorOnlineLoader- Remote document fetcherMetadataLoader- Document metadata extractor
Service Architecture:
LoaderService- Background service managing all document loading operationsLoaderServiceQueue- Queue management for multiple document loading requests- Document loaders implement
FileLoaderListenerinterface for async communication
UI Architecture:
MainActivity- Main activity with service binding and menu managementDocumentFragment- Primary document display fragment using WebViewPageView- Custom WebView for document rendering- Action mode callbacks for edit, find, and TTS functionality
Multi-flavor Android App:
- Lite flavor: Free version with ads and tracking enabled
- Pro flavor: Paid version with ads disabled and tracking disabled
Native Dependencies:
- Uses Conan package manager for C++ dependencies
- The app compiles no native code itself; conan builds odrcore (including its JNI
bindings) and the deployer drops the resulting
.sofiles intojniLibs - NDK version 28.2.13676358 required (for conan's cross build, and for the
libc++_shared.sothat gets shipped alongsidelibodr_jni.so) - C++20 standard
conanis taken from PATH; override with-Podr.conanExecutable=...orODR_CONAN. Note the conan gradle plugin does not trackapp/conanprofile.txtas a task input, so after editing itconanInstall-*stays UP-TO-DATE and the native libs keep their old settings - run the conan install by hand to pick the change up locally.
Core Library Integration:
- The JNI interface comes from odrcore itself, both halves out of the one conan package
built with the recipe's
with_jnioption: java classes underapp.opendocument.corefromshare/java/odr-core-java.jar, and the matchinglibodr_jni.so.CoreLoaderis the only thing wrapping it. - Taking both from the same package is deliberate and should stay that way - handles
cross as raw longs and enums as ordinals with no version negotiation, so a separately
versioned java artifact could drift from the
.so. It also keeps the build free of credentials, which f-droid and other clean source builders need.conandeployer.pyputs the jar inbuild/conan/<arch>/libsandapp/build.gradledepends on the armv8 copy as a file, not through a repository. - Anything the bindings use must exist on android API 26, which is far below what
their
--release 17compiler accepts. That is a runtime-only failure, on device (java.lang.ref.CleanerandList.ofboth had to be fixed upstream for this reason) - Supports multiple architectures: armv8, armv7, x86, x86_64
- Assets deployed to
assets/coreand native libraries tojniLibs/<abi>via the custom Conan deployer (app/conandeployer.py)
app/src/main/java/app/opendocument/droid/background/- Document processing servicesapp/src/main/java/app/opendocument/droid/ui/- UI components and activitiesapp/src/main/java/app/opendocument/droid/nonfree/- Analytics, billing, and adsapp/src/main/assets/- HTML templates and fonts for document rendering
Core Android:
- AndroidX libraries (AppCompat, Core, Material, WebKit)
- Google Play Services (Ads, Review, User Messaging Platform)
Document Processing:
- Custom ODR core library via Conan
Testing:
- Espresso for UI testing
- JUnit for unit testing
- Test APKs require connected device/emulator
- Minimum SDK: 26, Target SDK: 36, Compile SDK: 37 (ahead of target on purpose)
- AGP 9 / Gradle 9, versions live in
gradle/libs.versions.toml - R8/ProGuard enabled for release builds with resource shrinking
- Configuration cache enabled for parallel Conan installs
- Release signing credentials come from gradle properties or environment variables (see README); without them release variants build unsigned rather than failing
namespace (app.opendocument.droid) and applicationId (at.tomtasche.reader, plus
the .pro suffix) differ on purpose - do not "fix" the mismatch:
namespaceis only the java/kotlin package plusR/BuildConfig, and is free to rename. Nothing native is bound to it anymore - the JNI symbols live in odrcore's ownapp.opendocument.corepackage, which the app does not rename - so the keeps inproguard-rules.txtare about that package, not this one.applicationIdis the identity on Play and F-Droid and can never change: Play has no rename path, so a new one is a new listing that existing installs never update to. Store-facing renaming goes through the listing title infastlane/metadata/.- The component names in
AndroidManifest.xml(MainActivityand theCATCH_ALL/STRICT_CATCHaliases) also still readat.tomtasche.reader.*on purpose. The OS persists those strings for pinned launcher icons and for "always open .odt with this app", so they survive asactivity-aliasentries pointing at the relocated activity. TheComponentNamestrings inMainActivitymust keep matching them. - Anything reading
getPackageName()at runtime (the FileProvider authority inAndroidFileCache, the SharedPreferences name inMainActivity) followsapplicationIdand must stay that way, or existing users lose their saved prefs.
Kotlin; support is built into AGP 9, no kotlin plugin is applied. The only java left is
com/commonsware/android/print, which is vendored third party code with its own copyright
header and stays java so it can still be diffed against upstream. It calls nothing of ours,
so there is no java-to-kotlin call anywhere in the project.
That means @JvmStatic, @JvmField, @JvmOverloads and @Throws are not needed for
interop and should not be added back for it. What is left of them is there for a runtime
that reflects over the bytecode, and each one says so:
@JvmFieldon theCREATORs inFileLoader, which the parcelable contract requires to be a static field.@JvmOverloadsonProgressDialogFragment's constructor, so the no-arg constructor the fragment framework re-creates it with exists.@JvmStaticon the@BeforeClass/@AfterClassmethods in the instrumented tests, which JUnit requires to be static.