diff --git a/AndroidManifest.xml b/AndroidManifest.xml index c9a690a..8a39e48 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -1,8 +1,8 @@ + android:versionCode="6" + android:versionName="1.5"> diff --git a/README.md b/README.md index 500bd86..5035084 100644 --- a/README.md +++ b/README.md @@ -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 (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 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. @@ -131,9 +131,9 @@ 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 +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). -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**. 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 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 -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. ```sh @@ -181,12 +181,12 @@ declarations. - `src/` CalDAV core, platform-neutral: discovery, PROPFIND/REPORT, iCalendar, sync, pinning - `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) - `bridge/` legacy-TLS endpoint: standalone nginx (config, compose file), `npm/http.conf` for Nginx Proxy Manager, certificate script -- `res/` app name; the icon is drawn at build time (mdpi + ldpi for the Galaxy Mini) -- `tools/` icon, dex disassembler helper, zipalign, dex/API checkers, d8 workaround +- `res/` app name, launcher icon (ldpi to xhdpi; master in `art/`) and `stat_notify`, the white status-bar icon for reminders +- `tools/` dex disassembler helper, zipalign, dex/API checkers, d8 workaround ## Security notes diff --git a/android/org/gingercal/AlarmReceiver.java b/android/org/gingercal/AlarmReceiver.java index f3bded0..70ab008 100644 --- a/android/org/gingercal/AlarmReceiver.java +++ b/android/org/gingercal/AlarmReceiver.java @@ -29,7 +29,7 @@ public class AlarmReceiver extends BroadcastReceiver { static synchronized void acquire(Context c) { if (lock == null) { 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.acquire(5 * 60 * 1000L); diff --git a/android/org/gingercal/GingerApp.java b/android/org/gingercal/GingerApp.java index c3d2cd7..2667f22 100644 --- a/android/org/gingercal/GingerApp.java +++ b/android/org/gingercal/GingerApp.java @@ -9,7 +9,7 @@ public class GingerApp extends Application { super.onCreate(); L.gl = !"en".equals(getSharedPreferences("gingercal", 0).getString("lang", "gl")); 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(); } diff --git a/android/org/gingercal/NetHttp.java b/android/org/gingercal/NetHttp.java index c6a5398..07dc9f6 100644 --- a/android/org/gingercal/NetHttp.java +++ b/android/org/gingercal/NetHttp.java @@ -10,7 +10,7 @@ import java.util.concurrent.ExecutorService; 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 * directly, any method (PROPFIND/REPORT included), with keep-alive. A pinned certificate (a * self-signed server or an old bridge) is still honoured. diff --git a/android/org/gingercal/Reminders.java b/android/org/gingercal/Reminders.java index 072f986..3d064f9 100644 --- a/android/org/gingercal/Reminders.java +++ b/android/org/gingercal/Reminders.java @@ -43,8 +43,9 @@ public final class Reminders { List due = Db.get(c).startingBetween(Math.max(notified, now - 30 * ICal.MIN), now + m * ICal.MIN + 30000L); NotificationManager nm = (NotificationManager) c.getSystemService("notification"); long newest = notified; - int icon = App.sysDrawable(c, "ic_popup_reminder"); - if (icon == 0) icon = App.sysDrawable(c, "ic_dialog_info"); + // our own white status-bar glyph: the system's colour alarm-clock icon looks out of place up there + int icon = c.getResources().getIdentifier("stat_notify", "drawable", c.getPackageName()); + if (icon == 0) icon = App.sysDrawable(c, "ic_popup_reminder"); for (Ev e : due) { Intent open = new Intent(c, MainActivity.class); open.setFlags(0x10000000 /* FLAG_ACTIVITY_NEW_TASK */); diff --git a/android/org/gingercal/SettingsActivity.java b/android/org/gingercal/SettingsActivity.java index 786ef7a..82ff437 100644 --- a/android/org/gingercal/SettingsActivity.java +++ b/android/org/gingercal/SettingsActivity.java @@ -27,7 +27,7 @@ public class SettingsActivity extends Activity { @Override protected void onCreate(Bundle b) { super.onCreate(b); - setTitle(L.t("GingerCal settings")); + setTitle(L.t("Calendar Lite settings")); LinearLayout f = App.vbox(this); int p = App.dp(this, 8); f.setPadding(p, p, p, p); diff --git a/android/org/gingercal/net/CertVerifier.java b/android/org/gingercal/net/CertVerifier.java index 28239c6..96108f9 100644 --- a/android/org/gingercal/net/CertVerifier.java +++ b/android/org/gingercal/net/CertVerifier.java @@ -37,7 +37,7 @@ import com.minamp.bc.crypto.util.PublicKeyFactory; * Minimal Web PKI path validation for the bundled TLS stack: * 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. - * 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 { diff --git a/android/org/gingercal/net/Http.java b/android/org/gingercal/net/Http.java index acc2491..b46e512 100644 --- a/android/org/gingercal/net/Http.java +++ b/android/org/gingercal/net/Http.java @@ -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 * 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 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. */ public static String proxyHost; diff --git a/art/icon-master.png b/art/icon-master.png new file mode 100644 index 0000000..059e591 Binary files /dev/null and b/art/icon-master.png differ diff --git a/bridge/pangolin-vps-setup.txt b/bridge/pangolin-vps-setup.txt index b099a6f..ccb1d37 100644 --- a/bridge/pangolin-vps-setup.txt +++ b/bridge/pangolin-vps-setup.txt @@ -1,5 +1,5 @@ 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 @@ -72,7 +72,7 @@ STEPS 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". - - 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). - 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). @@ -104,7 +104,7 @@ STEPS 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. https://cal-old.oxose.org:8443: raw TCP ignores the hostname and the app pins the certificate, not the name) diff --git a/build.sh b/build.sh index 61b4536..17ac89c 100755 --- a/build.sh +++ b/build.sh @@ -1,7 +1,7 @@ #!/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) # APKTOOL path to apktool.jar (https://apktool.org; bundles aapt and smali) # APKSIGNER path to apksigner.jar (Android SDK build-tools//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) 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 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) 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 -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" </dev/null + -alias gingercal -keyalg RSA -keysize 2048 -sigalg SHA1withRSA -validity 10000 -dname "CN=Calendar Lite" 2>/dev/null fi 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 \ --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 -java -jar "$APKSIGNER" verify --min-sdk-version 8 --max-sdk-version 10 GingerCal.apk -python3 tools/check_dex.py GingerCal.apk -ls -l GingerCal.apk +java -jar "$APKSIGNER" verify --min-sdk-version 8 --max-sdk-version 10 CalendarLite.apk +python3 tools/check_dex.py CalendarLite.apk +ls -l CalendarLite.apk diff --git a/res/drawable-hdpi/icon.png b/res/drawable-hdpi/icon.png new file mode 100644 index 0000000..cbd0503 Binary files /dev/null and b/res/drawable-hdpi/icon.png differ diff --git a/res/drawable-hdpi/stat_notify.png b/res/drawable-hdpi/stat_notify.png new file mode 100644 index 0000000..f30c78e Binary files /dev/null and b/res/drawable-hdpi/stat_notify.png differ diff --git a/res/drawable-ldpi/icon.png b/res/drawable-ldpi/icon.png new file mode 100644 index 0000000..c3b77dd Binary files /dev/null and b/res/drawable-ldpi/icon.png differ diff --git a/res/drawable-ldpi/stat_notify.png b/res/drawable-ldpi/stat_notify.png new file mode 100644 index 0000000..74c1e67 Binary files /dev/null and b/res/drawable-ldpi/stat_notify.png differ diff --git a/res/drawable-mdpi/icon.png b/res/drawable-mdpi/icon.png new file mode 100644 index 0000000..d14fd01 Binary files /dev/null and b/res/drawable-mdpi/icon.png differ diff --git a/res/drawable-mdpi/stat_notify.png b/res/drawable-mdpi/stat_notify.png new file mode 100644 index 0000000..d6271da Binary files /dev/null and b/res/drawable-mdpi/stat_notify.png differ diff --git a/res/drawable-xhdpi/icon.png b/res/drawable-xhdpi/icon.png new file mode 100644 index 0000000..4e68a55 Binary files /dev/null and b/res/drawable-xhdpi/icon.png differ diff --git a/res/drawable-xhdpi/stat_notify.png b/res/drawable-xhdpi/stat_notify.png new file mode 100644 index 0000000..4a5d20b Binary files /dev/null and b/res/drawable-xhdpi/stat_notify.png differ diff --git a/res/values/strings.xml b/res/values/strings.xml index a2f45c3..6db879e 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -1,4 +1,4 @@ - GingerCal + Calendar Lite diff --git a/src/org/gingercal/ICal.java b/src/org/gingercal/ICal.java index 7a2c1e1..fb97ed1 100644 --- a/src/org/gingercal/ICal.java +++ b/src/org/gingercal/ICal.java @@ -431,7 +431,7 @@ public class ICal { List l = new ArrayList(); l.add("BEGIN:VCALENDAR"); 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("UID:" + uid); long now = System.currentTimeMillis(); diff --git a/src/org/gingercal/L.java b/src/org/gingercal/L.java index 4baf6b5..0202380 100644 --- a/src/org/gingercal/L.java +++ b/src/org/gingercal/L.java @@ -149,7 +149,7 @@ public final class L { put("Default calendar", "Calendario predeterminado"); // 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.", "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"); diff --git a/tools/mkicon.py b/tools/mkicon.py deleted file mode 100644 index d461462..0000000 --- a/tools/mkicon.py +++ /dev/null @@ -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)