Calendar Lite 1.6: bundled TLS 1.2, renamed from GingerCal, new icons, English months #1

Merged
xocko merged 8 commits from bundled-tls into main 2026-10-19 17:11:39 +00:00
24 changed files with 37 additions and 71 deletions
Showing only changes of commit 748f6add54 - Show all commits

No files matched your search

+2 -2
View File
@@ -1,8 +1,8 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="org.gingercal" package="org.gingercal"
android:versionCode="5" android:versionCode="6"
android:versionName="1.4"> android:versionName="1.5">
<uses-sdk android:minSdkVersion="8" android:targetSdkVersion="10" /> <uses-sdk android:minSdkVersion="8" android:targetSdkVersion="10" />
+9 -9
View File
@@ -1,9 +1,9 @@
# GingerCal — Nextcloud CalDAV for Android 2.3 (Galaxy Mini) # Calendar Lite — Nextcloud CalDAV for Android 2.3 (Galaxy Mini)
A small native calendar client for Gingerbread (API 9/10). Built for a Samsung 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. (GT-S5570, 240×320, Android 2.3.5) talking to Nextcloud, reached through Pangolin.
**Since 1.4 it brings its own TLS 1.2** (Bouncy Castle, the same client as Minamp, with Mozilla's **Since 1.4 it brings its own TLS 1.2** (Bouncy Castle, the same client as Plexamp Lite, with Mozilla's
root certificates and keep-alive connections), like the iPod version does with BearSSL. Put the root certificates and keep-alive connections), like the iPod version does with BearSSL. Put the
normal `https://` Nextcloud address in Settings, at home or away: no bridge is needed. The bridge normal `https://` Nextcloud address in Settings, at home or away: no bridge is needed. The bridge
sections below are kept for reference; a bridge address (pinned certificate) still works. sections below are kept for reference; a bridge address (pinned certificate) still works.
@@ -131,9 +131,9 @@ you should turn `proxy_ssl_verify` on.
## Install on the phone ## Install on the phone
1. Settings, Applications, tick **Unknown sources**. 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 2. Copy `CalendarLite.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). 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, 3. Open Calendar Lite, Menu, Settings. In Nextcloud create an app password: Settings, Security,
Devices & sessions. Fill in server, user, app password, then **Save and test connection**. Devices & sessions. Fill in server, user, app password, then **Save and test connection**.
Menu key: View (Month/Agenda), New event, Sync, Today, Calendars, Settings. Tap an event for Menu key: View (Month/Agenda), New event, Sync, Today, Calendars, Settings. Tap an event for
@@ -141,11 +141,11 @@ details, Edit and Delete.
If installation fails, check the file arrived whole (53 KB; some browsers save a truncated or If installation fails, check the file arrived whole (53 KB; some browsers save a truncated or
renamed `.apk.zip`) and that there are a few MB free on the internal storage. On a PC with renamed `.apk.zip`) and that there are a few MB free on the internal storage. On a PC with
`adb`, `adb install GingerCal.apk` prints the exact reason (`INSTALL_FAILED_...`). `adb`, `adb install CalendarLite.apk` prints the exact reason (`INSTALL_FAILED_...`).
## Building ## Building
Needs JDK 11+, Python 3 with Pillow, `d8` and `apksigner.jar` (Android SDK build-tools), and Needs JDK 11+, Python 3, `d8` and `apksigner.jar` (Android SDK build-tools), and
`apktool.jar` (2.9+, which bundles Google's `aapt` and smali). No Gradle. `apktool.jar` (2.9+, which bundles Google's `aapt` and smali). No Gradle.
```sh ```sh
@@ -181,12 +181,12 @@ declarations.
- `src/` CalDAV core, platform-neutral: discovery, PROPFIND/REPORT, iCalendar, sync, pinning - `src/` CalDAV core, platform-neutral: discovery, PROPFIND/REPORT, iCalendar, sync, pinning
- `android/` activities, sync service, alarms, SQLite cache, `NetHttp` transport - `android/` activities, sync service, alarms, SQLite cache, `NetHttp` transport
- `android/org/gingercal/net/` the bundled TLS 1.2 HTTP client from Minamp; `assets/roots/` Mozilla roots - `android/org/gingercal/net/` the bundled TLS 1.2 HTTP client from Plexamp Lite; `assets/roots/` Mozilla roots
- `stubs/` compile-only Android API 10 signatures (never packaged) - `stubs/` compile-only Android API 10 signatures (never packaged)
- `bridge/` legacy-TLS endpoint: standalone nginx (config, compose file), `npm/http.conf` for - `bridge/` legacy-TLS endpoint: standalone nginx (config, compose file), `npm/http.conf` for
Nginx Proxy Manager, certificate script Nginx Proxy Manager, certificate script
- `res/` app name; the icon is drawn at build time (mdpi + ldpi for the Galaxy Mini) - `res/` app name, launcher icon (ldpi to xhdpi; master in `art/`) and `stat_notify`, the white status-bar icon for reminders
- `tools/` icon, dex disassembler helper, zipalign, dex/API checkers, d8 workaround - `tools/` dex disassembler helper, zipalign, dex/API checkers, d8 workaround
## Security notes ## Security notes
+1 -1
View File
@@ -29,7 +29,7 @@ public class AlarmReceiver extends BroadcastReceiver {
static synchronized void acquire(Context c) { static synchronized void acquire(Context c) {
if (lock == null) { if (lock == null) {
PowerManager pm = (PowerManager) c.getApplicationContext().getSystemService("power"); PowerManager pm = (PowerManager) c.getApplicationContext().getSystemService("power");
lock = pm.newWakeLock(1 /* PARTIAL_WAKE_LOCK */, "GingerCal sync"); lock = pm.newWakeLock(1 /* PARTIAL_WAKE_LOCK */, "Calendar Lite sync");
lock.setReferenceCounted(false); lock.setReferenceCounted(false);
} }
lock.acquire(5 * 60 * 1000L); lock.acquire(5 * 60 * 1000L);
+1 -1
View File
@@ -9,7 +9,7 @@ public class GingerApp extends Application {
super.onCreate(); super.onCreate();
L.gl = !"en".equals(getSharedPreferences("gingercal", 0).getString("lang", "gl")); L.gl = !"en".equals(getSharedPreferences("gingercal", 0).getString("lang", "gl"));
org.gingercal.net.CertVerifier.setRootStore(new org.gingercal.net.AssetRoots(this)); org.gingercal.net.CertVerifier.setRootStore(new org.gingercal.net.AssetRoots(this));
org.gingercal.net.Http.USER_AGENT = "GingerCal/1.4 (Android 2.3)"; org.gingercal.net.Http.USER_AGENT = "CalendarLite/1.5 (Android 2.3)";
tlsTexts(); tlsTexts();
} }
+1 -1
View File
@@ -10,7 +10,7 @@ import java.util.concurrent.ExecutorService;
import org.gingercal.net.CertVerifier; import org.gingercal.net.CertVerifier;
/** /**
* Android 2.3 transport through the bundled TLS 1.2 client (org.gingercal.net, from Minamp): * Android 2.3 transport through the bundled TLS 1.2 client (org.gingercal.net, from Plexamp Lite):
* the phone's own TLS 1.0 can't reach Nextcloud, so this talks to the normal https:// address * the phone's own TLS 1.0 can't reach Nextcloud, so this talks to the normal https:// address
* directly, any method (PROPFIND/REPORT included), with keep-alive. A pinned certificate (a * directly, any method (PROPFIND/REPORT included), with keep-alive. A pinned certificate (a
* self-signed server or an old bridge) is still honoured. * self-signed server or an old bridge) is still honoured.
+3 -2
View File
@@ -43,8 +43,9 @@ public final class Reminders {
List<Ev> due = Db.get(c).startingBetween(Math.max(notified, now - 30 * ICal.MIN), now + m * ICal.MIN + 30000L); List<Ev> due = Db.get(c).startingBetween(Math.max(notified, now - 30 * ICal.MIN), now + m * ICal.MIN + 30000L);
NotificationManager nm = (NotificationManager) c.getSystemService("notification"); NotificationManager nm = (NotificationManager) c.getSystemService("notification");
long newest = notified; long newest = notified;
int icon = App.sysDrawable(c, "ic_popup_reminder"); // our own white status-bar glyph: the system's colour alarm-clock icon looks out of place up there
if (icon == 0) icon = App.sysDrawable(c, "ic_dialog_info"); int icon = c.getResources().getIdentifier("stat_notify", "drawable", c.getPackageName());
if (icon == 0) icon = App.sysDrawable(c, "ic_popup_reminder");
for (Ev e : due) { for (Ev e : due) {
Intent open = new Intent(c, MainActivity.class); Intent open = new Intent(c, MainActivity.class);
open.setFlags(0x10000000 /* FLAG_ACTIVITY_NEW_TASK */); open.setFlags(0x10000000 /* FLAG_ACTIVITY_NEW_TASK */);
+1 -1
View File
@@ -27,7 +27,7 @@ public class SettingsActivity extends Activity {
@Override @Override
protected void onCreate(Bundle b) { protected void onCreate(Bundle b) {
super.onCreate(b); super.onCreate(b);
setTitle(L.t("GingerCal settings")); setTitle(L.t("Calendar Lite settings"));
LinearLayout f = App.vbox(this); LinearLayout f = App.vbox(this);
int p = App.dp(this, 8); int p = App.dp(this, 8);
f.setPadding(p, p, p, p); f.setPadding(p, p, p, p);
+1 -1
View File
@@ -37,7 +37,7 @@ import com.minamp.bc.crypto.util.PublicKeyFactory;
* Minimal Web PKI path validation for the bundled TLS stack: * Minimal Web PKI path validation for the bundled TLS stack:
* hostname match, validity window, signature chain up to a bundled Mozilla root. * hostname match, validity window, signature chain up to a bundled Mozilla root.
* Optionally one pinned leaf certificate (SHA-256) is trusted as-is, for self-signed servers. * Optionally one pinned leaf certificate (SHA-256) is trusted as-is, for self-signed servers.
* Shared with Minamp (tools/tlskit in the iOS ports' history); keep the copies in sync. * Shared with Plexamp Lite (tools/tlskit in the iOS ports' history); keep the copies in sync.
*/ */
public final class CertVerifier { public final class CertVerifier {
+2 -2
View File
@@ -33,11 +33,11 @@ import com.minamp.bc.tls.crypto.impl.bc.BcTlsCrypto;
/** /**
* Small HTTP/1.1 client with its own TLS 1.2 (Bouncy Castle), keep-alive pooling and * Small HTTP/1.1 client with its own TLS 1.2 (Bouncy Castle), keep-alive pooling and
* session resumption. Android 2.x's built-in SSL only speaks TLS 1.0 and lacks today's roots, * session resumption. Android 2.x's built-in SSL only speaks TLS 1.0 and lacks today's roots,
* so every request in the app goes through here. From Minamp; keep the copies in sync. * so every request in the app goes through here. From Plexamp Lite; keep the copies in sync.
*/ */
public final class Http { public final class Http {
public static String USER_AGENT = "Minamp/1.0"; public static String USER_AGENT = "PlexampLite/1.0";
/** Optional HTTP CONNECT proxy, only used when running the networking code on a desktop JVM. */ /** Optional HTTP CONNECT proxy, only used when running the networking code on a desktop JVM. */
public static String proxyHost; public static String proxyHost;
Binary file not shown.

After

Width:  |  Height:  |  Size: 973 KiB

+3 -3
View File
@@ -1,5 +1,5 @@
TASK FOR CLAUDE CODE (on the Pangolin server, 141.227.129.182) TASK FOR CLAUDE CODE (on the Pangolin server, 141.227.129.182)
Add a raw TCP resource on port 8443 so the GingerCal app (Android 2.3) can reach Nextcloud CalDAV Add a raw TCP resource on port 8443 so the Calendar Lite app (Android 2.3) can reach Nextcloud CalDAV
================================================================================================== ==================================================================================================
CONTEXT CONTEXT
@@ -72,7 +72,7 @@ STEPS
7. Pangolin dashboard (the user does this, or guide them through it): 7. Pangolin dashboard (the user does this, or guide them through it):
- Org of the home server, Resources, Add Resource, type "Raw TCP/UDP resource". - Org of the home server, Resources, Add Resource, type "Raw TCP/UDP resource".
- Name: GingerCal CalDAV. Protocol: TCP. Public port: 8443. - Name: Calendar Lite CalDAV. Protocol: TCP. Public port: 8443.
- Site: the site whose newt runs on the home server 172.30.1.123 (most likely the "casa" site). - Site: the site whose newt runs on the home server 172.30.1.123 (most likely the "casa" site).
- Target: IP 172.30.1.123, port 8443. Leave "Enable Proxy Protocol" OFF (the bridge doesn't - Target: IP 172.30.1.123, port 8443. Leave "Enable Proxy Protocol" OFF (the bridge doesn't
expect it; enabling only one side breaks every connection). expect it; enabling only one side breaks every connection).
@@ -104,7 +104,7 @@ STEPS
PHONE SETUP (after step 8 passes) PHONE SETUP (after step 8 passes)
--------------------------------- ---------------------------------
GingerCal, Menu, Settings: Calendar Lite, Menu, Settings:
Server: https://141.227.129.182:8443 (any *.oxose.org name works too, e.g. Server: https://141.227.129.182:8443 (any *.oxose.org name works too, e.g.
https://cal-old.oxose.org:8443: raw TCP ignores the hostname and the app pins the https://cal-old.oxose.org:8443: raw TCP ignores the hostname and the app pins the
certificate, not the name) certificate, not the name)
+10 -12
View File
@@ -1,7 +1,7 @@
#!/bin/sh #!/bin/sh
# Builds GingerCal.apk for Android 2.3 (API 10) without Gradle. # Builds CalendarLite.apk for Android 2.3 (API 10) without Gradle.
# #
# Needs: JDK 11+ (javac, keytool), python3 with Pillow (icon), and three tools: # Needs: JDK 11+ (javac, keytool), python3, and three tools:
# D8 command that runs d8 (Android SDK build-tools; default "d8" on PATH) # D8 command that runs d8 (Android SDK build-tools; default "d8" on PATH)
# APKTOOL path to apktool.jar (https://apktool.org; bundles aapt and smali) # APKTOOL path to apktool.jar (https://apktool.org; bundles aapt and smali)
# APKSIGNER path to apksigner.jar (Android SDK build-tools/<ver>/lib/apksigner.jar) # APKSIGNER path to apksigner.jar (Android SDK build-tools/<ver>/lib/apksigner.jar)
@@ -20,7 +20,7 @@ VERSION_CODE=$(sed -n 's/.*android:versionCode="\([0-9]*\)".*/\1/p' AndroidManif
VERSION_NAME=$(sed -n 's/.*android:versionName="\([^"]*\)".*/\1/p' AndroidManifest.xml) VERSION_NAME=$(sed -n 's/.*android:versionName="\([^"]*\)".*/\1/p' AndroidManifest.xml)
rm -rf "$OUT" rm -rf "$OUT"
mkdir -p "$OUT/stubs" "$OUT/classes" "$OUT/dex" "$OUT/apk/res/drawable" "$OUT/apk/res/drawable-ldpi" mkdir -p "$OUT/stubs" "$OUT/classes" "$OUT/dex" "$OUT/apk/res"
# 0. every interface string must have its Galician translation # 0. every interface string must have its Galician translation
python3 tools/check_i18n.py . python3 tools/check_i18n.py .
@@ -46,13 +46,11 @@ java -cp "$APKTOOL:$OUT/tools" Disassemble "$OUT/dex/classes.dex" "$OUT/apk/smal
# 3. resources + manifest, compiled by real aapt (v1, the tool of the Gingerbread era) # 3. resources + manifest, compiled by real aapt (v1, the tool of the Gingerbread era)
cp AndroidManifest.xml "$OUT/apk/" cp AndroidManifest.xml "$OUT/apk/"
cp -r res/values "$OUT/apk/res/" cp -r res/* "$OUT/apk/res/" # launcher icon (ldpi..xhdpi) and the status-bar reminder icon
cp -r assets "$OUT/apk/" # Mozilla root certificates for the bundled TLS cp -r assets "$OUT/apk/" # Mozilla root certificates for the bundled TLS
python3 tools/mkicon.py "$OUT/apk/res/drawable/icon.png" 48
python3 tools/mkicon.py "$OUT/apk/res/drawable-ldpi/icon.png" 36
cat > "$OUT/apk/apktool.yml" <<EOF cat > "$OUT/apk/apktool.yml" <<EOF
version: 2.9.3 version: 2.9.3
apkFileName: GingerCal.apk apkFileName: CalendarLite.apk
isFrameworkApk: false isFrameworkApk: false
usesFramework: usesFramework:
ids: ids:
@@ -80,14 +78,14 @@ java -jar "$APKTOOL" -q b --use-aapt1 -api 8 -f -o "$OUT/unsigned.apk" "$OUT/apk
# attributes; apksigner picks exactly that for --min-sdk-version 8. (jarsigner does not.) # attributes; apksigner picks exactly that for --min-sdk-version 8. (jarsigner does not.)
if [ ! -f "$KS" ]; then if [ ! -f "$KS" ]; then
keytool -genkeypair -keystore "$KS" -storetype PKCS12 -storepass android -keypass android \ keytool -genkeypair -keystore "$KS" -storetype PKCS12 -storepass android -keypass android \
-alias gingercal -keyalg RSA -keysize 2048 -sigalg SHA1withRSA -validity 10000 -dname "CN=GingerCal" 2>/dev/null -alias gingercal -keyalg RSA -keysize 2048 -sigalg SHA1withRSA -validity 10000 -dname "CN=Calendar Lite" 2>/dev/null
fi fi
java -jar "$APKSIGNER" sign --ks "$KS" --ks-key-alias gingercal --ks-pass pass:android --key-pass pass:android \ java -jar "$APKSIGNER" sign --ks "$KS" --ks-key-alias gingercal --ks-pass pass:android --key-pass pass:android \
--min-sdk-version 8 --v1-signing-enabled true --v2-signing-enabled false --v3-signing-enabled false \ --min-sdk-version 8 --v1-signing-enabled true --v2-signing-enabled false --v3-signing-enabled false \
--v4-signing-enabled false --out "$OUT/signed.apk" "$OUT/unsigned.apk" --v4-signing-enabled false --out "$OUT/signed.apk" "$OUT/unsigned.apk"
python3 tools/zipalign.py "$OUT/signed.apk" GingerCal.apk python3 tools/zipalign.py "$OUT/signed.apk" CalendarLite.apk
# 5. refuse to ship anything Android 2.3 would reject # 5. refuse to ship anything Android 2.3 would reject
java -jar "$APKSIGNER" verify --min-sdk-version 8 --max-sdk-version 10 GingerCal.apk java -jar "$APKSIGNER" verify --min-sdk-version 8 --max-sdk-version 10 CalendarLite.apk
python3 tools/check_dex.py GingerCal.apk python3 tools/check_dex.py CalendarLite.apk
ls -l GingerCal.apk ls -l CalendarLite.apk
Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 499 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 666 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

+1 -1
View File
@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<string name="app_name">GingerCal</string> <string name="app_name">Calendar Lite</string>
</resources> </resources>
+1 -1
View File
@@ -431,7 +431,7 @@ public class ICal {
List<String> l = new ArrayList<String>(); List<String> l = new ArrayList<String>();
l.add("BEGIN:VCALENDAR"); l.add("BEGIN:VCALENDAR");
l.add("VERSION:2.0"); l.add("VERSION:2.0");
l.add("PRODID:-//GingerCal//Android 2.3//EN"); l.add("PRODID:-//CalendarLite//Android 2.3//EN");
l.add("BEGIN:VEVENT"); l.add("BEGIN:VEVENT");
l.add("UID:" + uid); l.add("UID:" + uid);
long now = System.currentTimeMillis(); long now = System.currentTimeMillis();
+1 -1
View File
@@ -149,7 +149,7 @@ public final class L {
put("Default calendar", "Calendario predeterminado"); put("Default calendar", "Calendario predeterminado");
// settings // settings
put("GingerCal settings", "Configuración de GingerCal"); put("Calendar Lite settings", "Configuración de Calendar Lite");
put("Nextcloud server: your normal https:// address. This app brings its own modern TLS, so no bridge is needed, at home or away. Use an app password, not your real one.", put("Nextcloud server: your normal https:// address. This app brings its own modern TLS, so no bridge is needed, at home or away. Use an app password, not your real one.",
"Servidor Nextcloud: o teu enderezo https:// de sempre. Esta aplicación trae o seu propio TLS moderno, así que non fai falta ningunha ponte, nin na casa nin fóra. Usa un contrasinal de aplicación, non o teu contrasinal real."); "Servidor Nextcloud: o teu enderezo https:// de sempre. Esta aplicación trae o seu propio TLS moderno, así que non fai falta ningunha ponte, nin na casa nin fóra. Usa un contrasinal de aplicación, non o teu contrasinal real.");
put("Server address", "Enderezo do servidor"); put("Server address", "Enderezo do servidor");
-33
View File
@@ -1,33 +0,0 @@
#!/usr/bin/env python3
"""Draws the launcher icon: a little calendar page in ginger tones.
usage: mkicon.py out.png [size] (48 = mdpi, 36 = ldpi like the Galaxy Mini)"""
import sys
from PIL import Image, ImageDraw
S = 4 # supersample then shrink for smooth edges
W = 48 * S
img = Image.new("RGBA", (W, W), (0, 0, 0, 0))
d = ImageDraw.Draw(img)
# shadow
d.rounded_rectangle((5 * S, 7 * S, 45 * S, 46 * S), radius=6 * S, fill=(0, 0, 0, 90))
# page
d.rounded_rectangle((3 * S, 5 * S, 43 * S, 44 * S), radius=6 * S, fill=(250, 246, 238, 255))
# ginger header band
d.rounded_rectangle((3 * S, 5 * S, 43 * S, 17 * S), radius=6 * S, fill=(214, 110, 42, 255))
d.rectangle((3 * S, 11 * S, 43 * S, 17 * S), fill=(214, 110, 42, 255))
# binder rings
for x in (13, 33):
d.rounded_rectangle(((x - 2) * S, 2 * S, (x + 2) * S, 10 * S), radius=2 * S, fill=(90, 60, 40, 255))
# day grid, one day highlighted
for row in range(3):
for col in range(4):
x0 = (8 + col * 8.5) * S
y0 = (21 + row * 7.5) * S
box = (x0, y0, x0 + 5.5 * S, y0 + 5 * S)
if (row, col) == (1, 2):
d.rounded_rectangle(box, radius=1.5 * S, fill=(0, 130, 201, 255)) # Nextcloud blue
else:
d.rounded_rectangle(box, radius=1.5 * S, fill=(200, 188, 172, 255))
size = int(sys.argv[2]) if len(sys.argv) > 2 else 48
img = img.resize((size, size), Image.LANCZOS)
img.save(sys.argv[1] if len(sys.argv) > 1 else "icon.png", optimize=True)