How CloudBridge works — the full technical tour
This page documents the complete CloudBridge system: the app architecture, how rclone is embedded, the build pipeline, automatic releases, the update mechanism, and the Google Play publishing flow. It is meant for contributors and for anyone who wants to understand what is running on their device.
1. Project architecture
The repository contains three Gradle modules:
| Module | Language | Purpose |
|---|---|---|
app |
Java + Kotlin | The Android application: UI, services, background tasks. Older code lives in the ca.pkay.rcloneexplorer namespace (inherited from rcloneExplorer), newer code in de.schuelken.cloudbridge. |
rclone |
Go | Cross-compiles rclone into librclone.so for every Android ABI. No rclone binary is executed; the library is loaded via JNI. |
safdav |
Java | Bridges Android's Storage Access Framework (SAF) to WebDAV, so rclone can read and write SD cards, USB sticks and other SAF-provided storage. |
2. How rclone is embedded
rclone is a Go program. The rclone module builds it with the official
Go NDK toolchain as a shared library (librclone.so) that exposes the
rclone remote-control API over a C interface.
The app calls into this library through JNI and exchanges JSON requests and
responses — for example to list a directory, start a sync, or create a remote.
- The exact rclone source is pinned in
gradle.properties(de.schuelken.cloudbridge.rCloneRepoUrlandrCloneRef). Builds fetch that exact commit intorclone/cache/. - CloudBridge uses a project fork (
github.com/thies2005/rclone) that adds an Internxt auto-token-renewal fix and other Android-specific patches. - Upgrading rclone normally means changing one line (
rCloneRef) and rebuilding — the whole point of this setup is upgradeability. - Your rclone configuration (remotes, OAuth tokens, crypt passwords) is a normal rclone config file in the app's private storage. You can export or import it from within the app.
3. Build system
Prerequisites: JDK 17, Go 1.25+, Android SDK with the NDK version pinned in gradle.properties.
./gradlew assembleOssDebug # debug APK
./gradlew assembleOssRelease # signed release APKs
./gradlew bundleOssRelease -PnoAbiSplits # AAB for Google Play
- Flavors: the
editiondimension hasoss(the standard build) andrs. - ABI splits: release builds produce one APK per ABI (arm64-v8a, armeabi-v7a, x86, x86_64) plus a universal APK. APK splitting must be disabled for AAB builds — that is what the
-PnoAbiSplitsswitch does. - Signing: local builds read
keystore.properties(git-ignored); CI readsCB_STORE_FILE,CB_STORE_PASSWORD,CB_KEY_ALIAS,CB_KEY_PASSWORDenvironment variables. If neither exists, the debug key is used so fresh checkouts still build. - First build downloads the pinned rclone source and cross-compiles it for all four ABIs — expect roughly 20–30 minutes. Later builds are incremental.
- Debug builds append
.debugto the application id, so debug and release can be installed side by side.
4. Version numbers
versionNamefollows major.minor.patch (currently 1.0.1) and is set inapp/build.gradle.versionCodemust end in 0. The last digit is reserved for ABI offsets added to split APKs (arm64 gets +7, x86_64 +9, and so on), so every APK of one build gets a unique, increasing code.- CI betas compute the code automatically:
versionCode = commitCount × 10andversionName = <base>-beta.<shortsha>. Because the commit count only grows, every build is a valid upgrade of the previous one — on GitHub and on Google Play alike.
5. CI/CD — how a push becomes a release
The workflow .github/workflows/android.yml runs on every push to master:
- Check out the full history (needed for the commit-count version code).
- Install the pinned JDK, Go and NDK versions from
gradle.properties. - Decode the release keystore from the encrypted repository secret
CB_KEYSTORE_BASE64into a temporary file. Passwords arrive as masked secrets. The keystore is never committed. - Build all signed release APKs (
assembleOssRelease) and the signed AAB (bundleOssRelease -PnoAbiSplits). - Create a GitHub pre-release tagged
v1.0.1-beta.<shortsha>with the five APKs and the.aabattached. Previous beta releases are kept.
Other workflows: lint.yml runs unit tests and lint on pull requests,
dependencies.yml rebuilds when build.gradle changes, and
translations.yml checks translated resources for profanity.
6. The update checker
If enabled, a WorkManager job runs about every 14 days and asks the GitHub API whether a newer release tag exists. When it finds one, it shows a notification only: "A new version might be available." Tapping it opens the release page in your browser, where you can read the changelog and download the APK yourself. There is also an "Ignore Update" action for this version.
On installations that came from an app store (Play Store, F-Droid and similar), the update check disables itself automatically — the store keeps you up to date.
7. Storage Access Framework (SAF)
Android does not let normal apps write to SD cards or USB sticks directly.
CloudBridge solves this with the safdav module: it exposes any
SAF document tree (as granted by you via the system picker) through a local
WebDAV server, and rclone can then use that WebDAV endpoint like any other
remote. This is how sync tasks can include external storage.
8. Google Play pipeline
Releasing on Play works alongside GitHub releases, not instead of them:
- Artifact: Play requires an Android App Bundle (
.aab), produced by CI on every push and attached to the beta release. - Signing: the AAB is signed with the project's upload key. On first upload, Google enrolls the app in Play App Signing: Google holds the final signing key and re-signs everything it distributes. Keep the upload keystore and its passwords safe — every future update must be signed with it.
- Upload: the AAB from the GitHub release is uploaded in Play Console to a testing track first (internal testing reviews in minutes), then promoted to production.
- versionCode: every upload must have a higher code than the last. The CI scheme (commit count × 10) guarantees this automatically.
- Policies that matter for CloudBridge: the
MANAGE_EXTERNAL_STORAGEpermission requires a declaration form (allowed for file managers, since broad file access is the core feature), the Data safety form can state that no data is collected (see the privacy policy), and in-app self-updating is not allowed — which is why the update checker is notification-only.
9. Local development
git clone https://github.com/thies2005/CloudBridge
cd CloudBridge
./gradlew assembleOssDebug
# APK: app/build/outputs/apk/oss/debug/
local.propertiesmust point to your Android SDK (sdk.dir).- Useful checks before opening a PR:
./gradlew testOssDebugUnitTest lint -x :rclone:buildAll. - Windows builds need the rclone module's NDK handling (see
WINDOWS_BUILD_GUIDE.md).
10. Troubleshooting
| Symptom | Cause / fix |
|---|---|
| First build takes ~30 minutes | rclone is being downloaded and cross-compiled for four ABIs. Normal; subsequent builds are incremental. |
AAB build fails: Multiple shrunk-resources files | APK splits were enabled during a bundle build. Use bundleOssRelease -PnoAbiSplits. |
| NDK / Go version mismatch | Both are pinned in gradle.properties (ndkVersion, goVersion) — install exactly those. |
| Rclone behaves differently than upstream | The project uses its own fork with Android-specific fixes (for example Internxt token renewal). Compare against rCloneRepoUrl/rCloneRef in gradle.properties. |
| Release APK only signed with debug key | No keystore.properties and no CB_* environment variables found — see section 3. |