Shipping an Android App Outside Google Play Is Decided Long Before You Try
Whether an Android app can ship via F-Droid or as a direct APK is settled early: by who holds the signing key, and whether any dependency is closed-source.

Shipping an Android App Outside Google Play Is Decided Long Before You Try
On 24 September F-Droid announced F-Droid 2.0, which it describes as a complete redesign of its official app and the largest app update in ten years. Key components were rewritten in Kotlin with Jetpack Compose, and after 14 test releases it will roll out over the coming weeks.
Two details matter to anyone who ships Android apps. F-Droid says that, largely thanks to pressure from the EU's Digital Markets Act and antitrust actions, Android now offers all app stores a smoother, more automatic install and update path, which its new unified installer uses. And users can now filter by anti-feature; the post's own example is excluding apps that depend on non-free network services.
Above the announcement, the page carries a banner: "F-Droid is under threat. Google is changing the way you install apps on your device. We need your help." The banner says no more than that, and we won't guess at the rest.
It did prompt a concrete question about our two Android apps on Google Play: CoreCall, a dialer that works through the customer's own Twilio or Telnyx account, and CoreTalk, a face-to-face interpreter for one shared phone. Could we offer either one through F-Droid, or as an APK on a download page?
Mostly not cleanly, and neither reason is something a release could fix. Both were settled early, by decisions most teams never revisit.
Decision one: who holds the signing key
Android installs an update over an existing app only when the signing certificates match. Everything about switching channels follows from that.
Both our release builds are signed with an upload key, and Google Play holds the app signing key (Play App Signing). So any APK we sign ourselves for another channel carries a different signature, and Android will not install it as an update over the Play-installed app. The user has to uninstall first, which deletes the app's local data. You can confirm this in a minute:
# The certificate your own APK is signed with...
apksigner verify --print-certs app-release.apk | grep SHA-256
# ...versus the app signing certificate fingerprint in the Play Console.
# Or install it over a copy that came from Play:
adb install -r app-release.apk
# fails with INSTALL_FAILED_UPDATE_INCOMPATIBLEF-Droid does not get around this. It normally builds apps from source and signs them with its own key: a third signature, matching neither Play's nor ours. The exception is reproducible builds: when F-Droid's build from your source matches the APK you signed, it can ship your developer-signed APK. That route requires the developer to hold the key. And for a Play user to move to the F-Droid copy without an uninstall, the key the developer holds would have to be the one Play signs with.
For our apps, moving between channels would always mean a reinstall and lost data. That was fixed when the apps were set up for Play App Signing, not in any release since.
Decision two: what is in the dependency tree
F-Droid builds from source, and its main repository requires the app and its dependencies to be free and open-source software. A closed-source SDK anywhere in the tree keeps the app out, however open your own code is.
Our two apps land on opposite sides of that line.
CoreTalk depends on AndroidX libraries (core, appcompat, lifecycle, activity-compose, security-crypto, and Jetpack Compose with Material 3) and on OkHttp. All of them are open source. On this criterion, nothing in its list stands in the way.
CoreCall uses OkHttp and Telnyx's WebRTC library (3.7.1), but it also depends on Twilio's Voice SDK for Android, version 6.10.4, which Twilio distributes as a compiled binary under its own terms, not as open source. That one dependency is enough to keep CoreCall, as built today, out of F-Droid's main repository. And it is not a line we could simply delete: it is how the app places calls for customers who bring a Twilio account.
Check the resolved classpath, not the list you remember declaring; transitive dependencies count too:
./gradlew :app:dependencies --configuration releaseRuntimeClasspathThe part that is not a blocker: be honest about the network
Both apps depend on network services by design. CoreCall talks to the calling provider account the user brings. CoreTalk requires an access key for its interpretation service, and a store download without a key only gives Demo Mode.
That is not disqualifying in itself. F-Droid labels apps with "Anti-Features", such as depending on a non-free network service, and F-Droid 2.0 lets users filter on exactly that. We think the model is right for every channel: a listing should say plainly what the app needs to be useful and what happens without it. "Works with your own Twilio or Telnyx account" and "Demo Mode until you enter an access key" belong in the description, read before installing, not discovered afterwards.
What we would do differently at the start
This is not an announcement of an F-Droid release. It is what the exercise told us about keeping the option open, which is cheap at the start and expensive later.
Keep a build variant without the proprietary SDK. A product flavor is enough, as long as the SDK's types never leave it:
android {
flavorDimensions += "distribution"
productFlavors {
create("play") { dimension = "distribution" }
create("foss") { dimension = "distribution" }
}
}
dependencies {
implementation(libs.okhttp)
"playImplementation"(libs.twilio.voice) // closed-source binary
}Shared code talks to an interface in src/main; the SDK-backed implementation
lives only in src/play. If a vendor type leaks into shared code, the foss
variant stops compiling, and that failure is the test. For CoreCall, such a
variant would lose Twilio support, and what remains would still have to be
checked dependency by dependency.
Decide key custody deliberately. Play App Signing lets you have Google generate the app signing key or supply your own. If another channel should ever update the same installation, you need a copy of the key the Play build is signed with, and the job of guarding it. Either choice can be right; making it by default is the mistake.
Make release builds reproducible. It is the only route by which F-Droid ships your signature instead of its own. In practice that means pinning every dependency version, committing the Gradle wrapper, keeping timestamps and machine-specific values out of the build, and recording the JDK you release with.
A short checklist
- Which key signs the build your users have installed, and do you hold a copy?
- Does
releaseRuntimeClasspathcontain anything distributed only as a compiled binary under non-open terms? - Would a variant without it compile today?
- Does your listing say which network service the app needs, and what works without it?
- Do two clean builds of the same commit produce the same APK contents, apart from the signature?
Answer these before the first release and the channel question stays open. Answer them later and you may find, as we did, that it was closed some time ago.