# GingerCal — Nextcloud CalDAV for Android 2.3 (Galaxy Mini) A small native calendar client for Gingerbread (API 9/10). Built for a Samsung Galaxy Mini (GT-S5570, 240×320, Android 2.3.5) talking to Nextcloud, reached through Pangolin. **What it does** - Agenda of upcoming events grouped by day, colour-coded per calendar, cached offline (SQLite) - Create, edit and delete events (title, all-day, start/end, location, notes, alarm) - Repeating events are shown correctly (server-side expansion, with a client-side fallback); they can be deleted on the phone but edited only in Nextcloud - Pick which calendars to show and which one new events go to - Background sync every 1–24 h, plus phone notifications N minutes before timed events - Certificate pinning, so a self-signed "legacy" endpoint is still safe against interception It does not feed the stock Samsung calendar: 2.3 has no public calendar-provider API for that. ## Why HTTPS breaks on 2.3, and the fix Three separate problems, all fatal for a normal Pangolin/Traefik URL: | Problem | Gingerbread | Pangolin (Traefik) | |---|---|---| | TLS version | TLS 1.0 max | TLS 1.2 minimum | | Root CA | no ISRG Root X1 (Let's Encrypt) | Let's Encrypt certificates | | SNI | unreliable in Apache HttpClient | routes HTTPS by SNI | Lowering Traefik's TLS settings would weaken every site you host and still leave the CA problem. Instead, put a tiny **legacy-TLS bridge** next to Nextcloud and expose it with a Pangolin **raw TCP resource** (port-based, so no SNI needed). The bridge speaks TLS 1.0 with a self-signed certificate that the app pins by fingerprint, and forwards **only** `/remote.php/dav/` to Nextcloud. ``` Galaxy Mini --TLS 1.0, pinned cert--> Pangolin VPS :8443 (raw TCP, WireGuard tunnel) --> newt --> caldav-bridge (nginx) --HTTP--> Nextcloud (CalDAV only) ``` ### Option A: bridge through Pangolin (works anywhere) 1. **Bridge** (on the Nextcloud host, in `bridge/`): - Edit `nginx.conf`: the `server nextcloud:80;` upstream and `set $nc_host` (a name in Nextcloud's `trusted_domains`). If Nextcloud forces HTTPS, also switch to `proxy_pass https://` (see "Forced HTTPS / HSTS" below). - Edit `docker-compose.yml`: the Docker network your Nextcloud container is on. - `./make-cert.sh` creates the certificate and prints its SHA-256 fingerprint. Note it. - `docker compose up -d` 2. **Pangolin server** (VPS): - `config/config.yml`: `flags: allow_raw_resources: true` - `docker-compose.yml`, `gerbil` service: add port `8443:8443` - `config/traefik/traefik_config.yml`, `entryPoints`: add `tcp-8443: { address: ":8443/tcp" }` (the `tcp-` name is required) - Open TCP 8443 in the VPS firewall, then `docker compose down && docker compose up -d` 3. **Pangolin dashboard**: Resources, Add Resource, **Raw TCP/UDP**, TCP, public port **8443**, target = the bridge as seen from your newt site (`caldav-bridge:8443` if newt shares its Docker network, otherwise `127.0.0.1:8443` with the port published). Leave Proxy Protocol off. 4. **Phone**: server address `https://:8443`. On the first test the app shows the certificate fingerprint; tap **Trust** only if it matches step 1. Pangolin's SSO does not apply to raw TCP resources; protection here is Nextcloud's login (use an app password) plus the pinned certificate. The web UI isn't reachable through the bridge. Optional, to let Nextcloud's brute-force protection see real client IPs: tick **Enable Proxy Protocol** on the resource, add the `pp-transport-v1/v2` block to Pangolin's `dynamic_config.yml` (see the Pangolin raw-resources docs), and in `nginx.conf` use `listen 8443 ssl proxy_protocol;`, `set_real_ip_from ;`, `real_ip_header proxy_protocol;`. Enable both sides together or every connection fails. ### Option B: home Wi-Fi only (no Pangolin) Use `http://` as the server address. Nothing to set up, but it syncs only at home and the password crosses your LAN unencrypted. The app warns if `http://` points at a non-LAN address. Avoid a plain-HTTP Pangolin resource: it would send your password in clear over the internet. ### Forced HTTPS / HSTS **HSTS itself doesn't matter to the app.** `Strict-Transport-Security` is enforced by browsers; the app's HTTP client ignores it, and a Pangolin raw TCP resource passes bytes through without adding headers. The bridge strips the header anyway, so a modern browser that you point at the bridge (e.g. while debugging) doesn't lock itself to the self-signed certificate. **What does matter is a Nextcloud that redirects plain HTTP to its public `https://` URL** (web server rule, `overwriteprotocol`, `overwrite.cli.url`). Symptoms in the app: | App says | Cause | Fix | |---|---|---| | "Redirect loop … switch the bridge to proxy_pass https://" | bridge talks HTTP to a Nextcloud that redirects HTTP→HTTPS | in `nginx.conf`: `server nextcloud:443;` and `proxy_pass https://nextcloud;` | | "The server redirected http:// to https://… (forces HTTPS / HSTS)" | Option B: phone talks `http://` to that Nextcloud on the LAN | use the bridge address, or let the LAN vhost serve `/remote.php/dav/` without the redirect | | "The server redirected to https://cloud…, not the address set in the app" | an absolute redirect wasn't rewritten | keep the `proxy_redirect` and `absolute_redirect off` lines in `nginx.conf` | With `proxy_pass https://` only the bridge-to-Nextcloud hop uses HTTPS (modern TLS, any certificate, since it's your own server). The phone side stays TLS 1.0 with the pinned certificate. If the bridge can't reach Nextcloud directly, `proxy_pass` can also point at the public Pangolin URL. In that case Pangolin authentication must let `/remote.php/dav/` through, and you should turn `proxy_ssl_verify` on. ## Install on the phone 1. Settings, Applications, tick **Unknown sources**. 2. Copy `GingerCal.apk` to the SD card (USB) and open it with the file manager, or download it over plain `http://` (the 2.3 browser can't open most HTTPS sites either). 3. Open GingerCal, Menu, Settings. In Nextcloud create an app password: Settings, Security, Devices & sessions. Fill in server, user, app password, then **Save and test connection**. Menu key: Sync, New event, Calendars, Settings. Tap an event for details, Edit and Delete. ## Building Needs JDK 11+, Python 3 with Pillow, and `d8` (Android SDK build-tools). No Gradle, no aapt: `tools/mkapk.py` writes the binary manifest and resource table itself. ```sh ./build.sh # uses ./stubs to compile ANDROID_JAR=$SDK/platforms/android-10/android.jar ./build.sh # or the real platform jar ``` `gingercal.keystore` (password `android`) is the key the supplied APK was signed with; keep building with it so updates install over the existing app. A different key means uninstalling first. Signing uses SHA-1 because Android before 4.3 rejects anything else. Tests (desktop JVM, need a CalDAV server such as Radicale on :5232 with user `xose`/`secret`): ```sh javac -d out src/org/gingercal/*.java test/org/gingercal/*.java java -cp out org.gingercal.CoreTest # discovery, sync, recurrence, create/edit/delete java -cp out org.gingercal.RedirectTest # forced-HTTPS / redirect-loop handling (no server needed) ``` `tools/check_api.py` verifies every framework call in the dex against published Android API declarations. ## Layout - `src/` CalDAV core, platform-neutral: discovery, PROPFIND/REPORT, iCalendar, sync, pinning - `android/` activities, sync service, alarms, SQLite cache, Apache HttpClient transport - `stubs/` compile-only Android API 10 signatures (never packaged) - `bridge/` nginx legacy-TLS bridge: config, compose file, certificate script - `tools/` APK packer, icon, zipalign, d8 workaround, API checker ## Security notes TLS 1.0 with AES-CBC is old but, combined with a pinned certificate, still keeps the connection private and authenticated against anyone without the bridge's private key. The bridge exposes only the DAV endpoint. Use an app password so you can revoke the phone from Nextcloud. If the phone ever reports that the certificate changed and you didn't regenerate it, don't trust it.