Skip to content

Commit d5f7c26

Browse files
F-Droid build without ML Kit (#29)
The fdroid flavor reads text with the open-source Tesseract engine instead of Google ML Kit, adds F-Droid/IzzyOnDroid fastlane metadata and docs/store/FDROID.md, and CI checks the F-Droid APK for proprietary Google classes.
1 parent 1ec67d1 commit d5f7c26

19 files changed

Lines changed: 643 additions & 84 deletions

File tree

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
#!/usr/bin/env python3
2+
"""Fail if an APK contains classes from proprietary Google libraries.
3+
4+
Used on the F-Droid flavor, which must not ship Google ML Kit, Google Play Services or
5+
Firebase. Scans the type descriptors in every classes*.dex file of each APK given.
6+
"""
7+
import re
8+
import sys
9+
import zipfile
10+
11+
FORBIDDEN = (
12+
b"Lcom/google/mlkit/",
13+
b"Lcom/google/android/gms/",
14+
b"Lcom/google/firebase/",
15+
)
16+
DEX_NAME = re.compile(r"^classes\d*\.dex$")
17+
18+
19+
def scan(apk_path):
20+
hits = {}
21+
with zipfile.ZipFile(apk_path) as apk:
22+
dex_names = [name for name in apk.namelist() if DEX_NAME.match(name)]
23+
if not dex_names:
24+
raise SystemExit(f"{apk_path}: no classes*.dex found")
25+
for name in dex_names:
26+
data = apk.read(name)
27+
for prefix in FORBIDDEN:
28+
for match in re.finditer(re.escape(prefix) + rb"[A-Za-z0-9_/$]*;", data):
29+
hits.setdefault(prefix.decode(), set()).add(f"{name}: {match.group().decode(errors='replace')}")
30+
return hits
31+
32+
33+
def main(paths):
34+
if not paths:
35+
raise SystemExit("usage: check_no_proprietary_classes.py APK...")
36+
failed = False
37+
for path in paths:
38+
hits = scan(path)
39+
if hits:
40+
failed = True
41+
print(f"FAIL {path}")
42+
for prefix, entries in sorted(hits.items()):
43+
print(f" {prefix} ({len(entries)} references)")
44+
for entry in sorted(entries)[:20]:
45+
print(f" {entry}")
46+
else:
47+
print(f"OK {path}: no {', '.join(p.decode() for p in FORBIDDEN)} classes")
48+
return 1 if failed else 0
49+
50+
51+
if __name__ == "__main__":
52+
sys.exit(main(sys.argv[1:]))

‎.github/workflows/android-ci.yml‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,9 @@ jobs:
4646
- name: Build debug APKs (GitHub, F-Droid, Play)
4747
run: bash ./gradlew assembleGithubDebug assembleFdroidDebug assemblePlayDebug --stacktrace
4848

49+
- name: Check F-Droid APK has no ML Kit, Play Services or Firebase classes
50+
run: python3 ../.github/scripts/check_no_proprietary_classes.py app/build/outputs/apk/fdroid/debug/*.apk
51+
4952
- name: Check 16 KB page support of native libraries
5053
continue-on-error: true
5154
run: python3 ../.github/scripts/check_16kb_pages.py app/build/outputs/apk/play/debug/*.apk

‎docs/store/FDROID.md‎

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
# F-Droid and IzzyOnDroid release checklist
2+
3+
The `fdroid` build flavor is the one meant for F-Droid and IzzyOnDroid. It contains no Google ML Kit, Google Play Services or Firebase code. CI checks this on every build (`.github/scripts/check_no_proprietary_classes.py` scans the dex files of the F-Droid debug APK).
4+
5+
## Build
6+
7+
```bash
8+
cd pocket_llm_src
9+
bash ./gradlew assembleFdroidRelease
10+
```
11+
12+
- Output: `app/build/outputs/apk/fdroid/release/app-fdroid-release-unsigned.apk`. Sign it with your own release key (`apksigner sign --ks <keystore> ...`), or configure a `signingConfig` locally. Never commit the keystore.
13+
- Always sign F-Droid/IzzyOnDroid APKs with the **same key**. Users cannot update across keys without uninstalling.
14+
- Release builds fail until `src/main/res/values/legal_contact.xml` is filled in (see [docs/legal](../legal/README.md)).
15+
- Debug builds install side by side with the other flavors as `io.github.dineshsoudagar.pocketllm.debug.fdroid`.
16+
17+
## What differs in the fdroid flavor
18+
19+
| | github / play | fdroid |
20+
|---|---|---|
21+
| OCR engine | Google ML Kit text recognition (proprietary, model bundled, sends diagnostics to Google) | Tesseract 5 via Tesseract4Android (Apache-2.0) |
22+
| OCR language data | inside the ML Kit library | `eng.traineddata` from [tesseract-ocr/tessdata_fast](https://github.com/tesseract-ocr/tessdata_fast) tag `4.1.0`, about 4 MB, downloaded on first OCR use into `filesDir/ocr/tessdata`, size and SHA-256 pinned |
23+
| Code | `src/mlkit/java` (added to the github and play source sets) | `src/fdroid/java` |
24+
25+
Both implement the `OcrEngine` interface in `src/main`. Callers (`OcrInput` for camera/gallery images, `AttachmentImporter` for scanned PDF pages) are the same in every flavor.
26+
27+
If OCR data is missing and the phone is offline, OCR fails with "Text recognition needs its English language data (about 4 MB) once. Connect to the internet and try again." In the chat screen, download progress is shown as a short status message. The PDF importer runs in a background worker and shows the same error on the attachment if the download fails.
28+
29+
Tesseract is noticeably slower than ML Kit and somewhat weaker on photos of curved or angled text. It is English-only for now; more languages can be added by pinning more `*.traineddata` files in `TesseractLanguageData.kt`.
30+
31+
### Where Tesseract4Android is published
32+
33+
Tesseract4Android (`cz.adaptech.tesseract4android:tesseract4android:4.9.0`, Tesseract 5.5.1 + Leptonica, Apache-2.0) is **only published on JitPack**, not on Maven Central. The build adds `https://jitpack.io` in `settings.gradle.kts`, restricted to the group `cz.adaptech.tesseract4android`.
34+
35+
Trade-off:
36+
- IzzyOnDroid does not build from source, so a JitPack dependency is fine there; it only scans the APK.
37+
- F-Droid builds from source and dislikes binaries prebuilt by JitPack. For the main F-Droid repository, Tesseract4Android should be built from source as a `srclib` (it is a plain Gradle + CMake/NDK project, so this is feasible) and the JitPack repository removed in the recipe's `prebuild`.
38+
- The older `com.rmtheis:tess-two` is on Maven Central but unmaintained (Tesseract 3/4 era), so it was not chosen.
39+
40+
## IzzyOnDroid (fastest route)
41+
42+
IzzyOnDroid takes APKs straight from GitHub Releases and reads the store listing from `fastlane/metadata/android/` in this repository.
43+
44+
1. Build and sign `assembleFdroidRelease`. Attach the APK to the GitHub release with a clear name, for example `pocket_llm_v1.6.0_fdroid.apk`. Keep the GitHub (ML Kit) APK as a separate asset; tell Izzy which file name pattern to pick.
45+
2. Keep `fastlane/metadata/android/en-US/` up to date: `title.txt`, `short_description.txt` (max 80 characters), `full_description.txt`, `images/icon.png`, and `changelogs/<versionCode>.txt` for every release (current versionCode: 15). Screenshots go into `images/phoneScreenshots/` (none added yet).
46+
3. Open a request at https://codeberg.org/IzzyOnDroid/repodata/issues with the repository URL, license, the APK asset name and a short description. Mention that the app downloads models from Hugging Face at runtime and that the F-Droid flavor uses Tesseract instead of ML Kit.
47+
4. **APK size:** IzzyOnDroid's general rule is about 30 MB per APK; larger apps need an exception. This APK is far larger (see blockers), so ask for an exception in the request, or ship a per-ABI `arm64-v8a` APK.
48+
5. Izzy's scanner checks for proprietary libraries and trackers. The CI check above covers ML Kit, Play Services and Firebase.
49+
50+
## Main F-Droid repository
51+
52+
1. Fork https://gitlab.com/fdroid/fdroiddata and add `metadata/io.github.dineshsoudagar.pocketllm.yml` with a build recipe: `subdir: pocket_llm_src/app`, `gradle: [fdroid]`, the release commit or tag, `AutoUpdateMode`/`UpdateCheckMode: Tags`, and `srclibs`/`prebuild` steps for every native dependency that must be built from source (see blockers).
53+
2. Test locally with `fdroid build -v -l io.github.dineshsoudagar.pocketllm` (fdroidserver) and `fdroid lint`.
54+
3. Open a merge request. Reviewers will run the scanner and ask about every prebuilt binary and non-standard Maven repository.
55+
56+
F-Droid can also offer reproducible builds signed with the developer's key, if the build output matches byte for byte.
57+
58+
## Open blockers for the main F-Droid repository
59+
60+
These are not fixed in this branch; the fdroid flavor is free of ML Kit/Play Services but still ships prebuilt native code.
61+
62+
| Dependency | Source | Problem for F-Droid |
63+
|---|---|---|
64+
| LiteRT-LM `com.google.ai.edge.litertlm:litertlm-android:0.17.1` | Google Maven | Prebuilt native AAR. Source is Apache-2.0 (google-ai-edge/LiteRT-LM) but built with Bazel; F-Droid would need to build it from source. Its transitive dependencies should also be checked for Google-only artifacts (the CI dex check would catch Play Services classes). This is the core inference engine, so it is the largest blocker. |
65+
| ONNX Runtime `com.microsoft.onnxruntime:onnxruntime-android:1.27.0` | Maven Central | MIT, but prebuilt `libonnxruntime.so` for 4 ABIs. Building from source is possible but heavy. |
66+
| sherpa-onnx `sherpa-onnx-static-link-onnxruntime-1.13.4.aar` | GitHub Releases via an Ivy repository in `settings.gradle.kts`, then stripped by `verifyOnnxRuntimeNativeCompatibility` in `app/build.gradle.kts` | Apache-2.0, but a prebuilt binary AAR from a non-Maven repository. The scanner flags both. Needs a from-source `srclib` build. |
67+
| Tesseract4Android 4.9.0 | JitPack | Apache-2.0, prebuilt by JitPack; build from source as a srclib (see above). |
68+
| APK size | | The universal APK is about 300 MB because of native libraries for 4 ABIs. Consider ABI splits for the fdroid flavor. |
69+
70+
Other points reviewers may raise:
71+
- Models are not in the APK; they are downloaded at runtime from Hugging Face. Some built-in models (Gemma) use Google's Gemma terms rather than an OSI license, so reviewers may discuss an anti-feature label for downloaded non-free assets.
72+
- No other proprietary libraries were found: AndroidX, CameraX, Material Components, Markwon, PdfBox-Android, WorkManager and org.json (public domain since 2022; version 20240303 is used) are free software.
73+
74+
## Test on a phone before submitting
75+
76+
Install the fdroid debug APK (`app/build/outputs/apk/fdroid/debug/`) and check:
77+
78+
1. **Gallery OCR** with mobile data/Wi-Fi on: the first use shows "Downloading OCR language data..." and then the recognized text.
79+
2. **Gallery OCR offline** after clearing app data: a clear error asking to connect once, no crash.
80+
3. **Camera OCR**: photo of a printed page, including a portrait photo (EXIF rotation).
81+
4. **Scanned PDF** attachment (pages without embedded text): text is extracted, and an offline first run fails with the same clear error.
82+
5. Repeat 1, 3 and 4 on the github or play debug build to confirm ML Kit behaviour did not change.
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
* Use your phone as a password-protected LLM server on your local network, with a browser chat UI and an OpenAI-compatible API
2+
* Import your own LiteRT-LM model (beta)
3+
* Text, PDF and audio attachments, including OCR for scanned PDF pages
4+
* Offline multilingual Whisper speech-to-text
5+
* Per-model context length setting
6+
* LiteRT-LM updated to 0.17.1
7+
* F-Droid build: open-source Tesseract OCR instead of Google ML Kit
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
Pocket LLM runs large language models directly on your Android phone. Chats, prompts, attachments and model answers stay on the device. There are no accounts, no ads, no analytics and no server run by the developer.
2+
3+
<b>How it works</b>
4+
The app itself does not contain a model. After installing, you choose a built-in model (Qwen2.5, Qwen3, DeepSeek R1 Distill Qwen or Gemma 4) and the app downloads it from Hugging Face (huggingface.co). Models are several hundred MB to a few GB, so use Wi-Fi. You can also import your own LiteRT-LM model file (beta). Once a model is installed, chatting works offline.
5+
6+
<b>Features</b>
7+
* Streaming chat with multi-turn history, Markdown rendering and optional thinking mode on supported models
8+
* Attach text files and PDFs; scanned PDF pages are read with on-device OCR
9+
* Read text from photos and images with on-device OCR, or send images directly to models that support image input
10+
* Offline speech-to-text with multilingual Whisper (downloaded once from Hugging Face, about 100 MB)
11+
* Audio attachments for models with native audio support
12+
* Editable instructions, prompt presets and per-model context length
13+
14+
<b>Local network server</b>
15+
Optionally turn the phone into a password-protected LLM server for your home network: open the built-in web chat in a computer browser, or connect OpenAI-compatible clients and scripts. Requests stay inside your local network. The server only runs while you keep it switched on.
16+
17+
<b>Good to know</b>
18+
* Running LLMs needs a recent phone with plenty of RAM. Large models or long contexts can fail to load or crash on some devices.
19+
* This version uses the open-source Tesseract engine for text recognition. The first time you use OCR it downloads the English language data (about 4 MB) from GitHub.
20+
* Network access is used only for model and data downloads you start, and for the optional local network server.
6.1 KB
Loading
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Private, offline AI chat on your phone, with a local network LLM server
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Pocket LLM

‎pocket_llm_src/app/build.gradle.kts‎

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,18 @@ android {
5050
}
5151
}
5252

53+
// OCR: github and play share the Google ML Kit implementation in src/mlkit;
54+
// fdroid uses the open-source Tesseract implementation in src/fdroid.
55+
// AGP's built-in Kotlin compiles the kotlin source directories, so register the shared folder there.
56+
sourceSets {
57+
listOf("github", "play").forEach { flavor ->
58+
getByName(flavor) {
59+
java.srcDir("src/mlkit/java")
60+
kotlin.srcDir("src/mlkit/java")
61+
}
62+
}
63+
}
64+
5365
buildTypes {
5466
release {
5567
isMinifyEnabled = false
@@ -123,7 +135,11 @@ dependencies {
123135
implementation("androidx.camera:camera-camera2:1.4.2")
124136
implementation("androidx.camera:camera-lifecycle:1.4.2")
125137
implementation("androidx.camera:camera-view:1.4.2")
126-
implementation("com.google.mlkit:text-recognition:16.0.1")
138+
// Proprietary ML Kit stays out of the fdroid flavor.
139+
"githubImplementation"("com.google.mlkit:text-recognition:16.0.1")
140+
"playImplementation"("com.google.mlkit:text-recognition:16.0.1")
141+
// Tesseract OCR (Apache-2.0), JitPack-only; see docs/store/FDROID.md.
142+
"fdroidImplementation"("cz.adaptech.tesseract4android:tesseract4android:4.9.0")
127143
implementation("com.google.ai.edge.litertlm:litertlm-android:$liteRtLmVersion")
128144
implementation("com.tom-roush:pdfbox-android:2.0.27.0")
129145
implementation("androidx.work:work-runtime-ktx:2.11.2")

0 commit comments

Comments
 (0)