🛠️ Molly Stuck Loading: Expired Build from a Stale F-Droid Repo¶
🔴 Symptom¶
Molly (a hardened Signal fork) sits on a permanent loading spinner and never completes device linking. The failure is completely silent:
- No error dialog, no crash, no
FATAL EXCEPTION, no ANR - Zero bytes transferred during the "sync"
- The app is alive and allocating memory — it simply never talks to the network
Nothing in logcat names the problem.
🎯 Root Cause¶
F-Droid was pointing at a stale repository, serving an old Molly build long after upstream had moved on.
Molly deliberately caps build lifespan to avoid Signal server incompatibility (mollyim#677, changelog v7.72.2-1). Signal's servers reject clients that fall too far behind, so Molly expires its own builds rather than fail in confusing ways. When the build ages out, the client refuses to complete provisioning.
Real-world instance (2026-07-20):
| Installed | v8.7.3-2, released 29 Apr — 82 days old |
| Current upstream | v8.19.2-2 (Signal 8.19.2) |
Fix: point F-Droid at Molly's official repo (https://molly.im/fdroid/repo), update, and re-link. Or install the current APK from GitHub releases.
Check the version first
For any app that self-expires, this is a ten-second check that short-circuits hours of debugging:
Compare against the upstream releases page before touching logcat.
⚠️ The Screen Is Unobservable¶
Molly sets FLAG_SECURE, and on GrapheneOS the usual escapes are closed too:
adb exec-out screencap -preturns a solid black imageadb shell uiautomator dumpis refused
There was almost certainly an on-screen "update required" banner the entire time, and it was invisible to tooling. Ask the user what the screen says — do not assume you can capture it.
🟢 Diagnostic Procedure¶
With the UI unreadable, the diagnosis has to come from system services.
1. Confirm connection and package¶
adb devices -l # Pixel 6a = model:Pixel_6a, codename bluejay
adb shell pm list packages | grep -i molly # im.molly.app
2. Version vs upstream — do this first¶
3. Is it actually moving data?¶
Per-interface totals, which is what you want when an app may be using a non-default network:
Sample twice ~20 s apart and diff. A few KB device-wide means nothing is happening.
dumpsys netstats buckets are per-network-ident
A byte counter scoped to the Wi-Fi ident will not show traffic crossing any other interface. Use /proc/net/dev for ground truth.
/proc/net/tcp is root-only on modern Android
An empty socket dump is inconclusive — not proof there is no connection.
4. Rule out power management¶
adb shell am get-standby-bucket im.molly.app # 5=EXEMPTED 10=ACTIVE 20=WORKING_SET ...
adb shell dumpsys deviceidle whitelist | grep -i molly
5. Rule out DNS/network path¶
for d in chat.signal.org cdn.signal.org cdsi.signal.org storage.signal.org; do
printf '%-24s ' "$d"; adb shell ping -c1 -W2 $d 2>&1 | head -1
done
Real IPs = DNS is fine. Relevant on a network running Pi-hole or NextDNS, where a blocklist can eat legitimate endpoints.
6. Registration/connection state¶
Key line, and it is self-explaining:
D/IncomingMessageObserver: [Does Not Need Connection] Network: true,
PushAvailable: false, WS Open or Keep-alives: false,
Registered: false, Force websocket: true
Does Not Need Connection + Registered: false means the app is deliberately not opening its websocket because there is no account yet — that is the normal pre-link state, not a fault.
📋 Things That Look Like Bugs But Aren't¶
| Observation | Reality |
|---|---|
PushAvailable: false, Force websocket: true |
Normal. Since v7.68.5 Molly is a single FOSS build — Play Services SDK removed, FCM reimplemented open-source. There is no longer an "FCM flavor" vs "FOSS flavor". |
FcmReceiveService running |
Normal for that open-source FCM implementation. |
| Not in the doze whitelist | Only matters after linking — see below. |
NotificationVibratorHelper: at least one timing must be non-zero |
Cosmetic Android system notification bug, unrelated. |
| Black screenshot | FLAG_SECURE, not a broken UI. |
🔋 Post-Link: Set Battery to Unrestricted¶
Because Force websocket: true means Molly holds its own connection instead of relying on FCM push, doze will stall delivery.
Settings → Apps → Molly → Battery → Unrestricted, then verify:
adb shell dumpsys deviceidle whitelist | grep -i molly # user,im.molly.app,10009
adb shell am get-standby-bucket im.molly.app # 5 (EXEMPTED)
The bucket moving from 10 (ACTIVE) to 5 (EXEMPTED) confirms it took effect.
🔗 Linking an Android Phone as a Secondary Device¶
Signal supports this — "You can link a Desktop, iPad or Android device (tablet or phone) to your primary Signal Device." Molly runs as a non-primary device too (changelog #647, SSRE2 linked-device fixes).
Direction matters: the Android device displays the QR via "Link your account"; the primary scans it (Settings → Linked devices → Link a new device).
\"Don't Transfer\" is a one-shot decision
Declining message-history transfer during linking cannot be undone later. Per Signal's docs, changing your mind means reinstalling on the linked device and re-linking with the transfer option.
🧭 Debugging Lesson¶
Four plausible hypotheses were investigated and disproven before the real cause surfaced: Pi-hole DNS blocking, doze/battery restriction, Wi-Fi Direct p2p failure, and a flapping 2G cellular network with a captive portal.
All four were infrastructure explanations. The actual cause was a stale package repository. When a homelab's history is full of DNS and network problems, that's where suspicion goes first — worth consciously checking the boring supply-chain explanation (is this software simply out of date?) early.