Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
82acdb79f6 | ||
|
|
478fc60527 | ||
|
|
361adef1a0 | ||
|
|
503beac559 | ||
|
|
9a4f6794d9 | ||
|
|
748f6add54 |
No files matched your search
@@ -1,8 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="org.gingercal"
|
||||
android:versionCode="5"
|
||||
android:versionName="1.4">
|
||||
android:versionCode="7"
|
||||
android:versionName="1.6">
|
||||
|
||||
<uses-sdk android:minSdkVersion="8" android:targetSdkVersion="10" />
|
||||
|
||||
|
||||
@@ -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.
|
||||
(GT-S5570, 240×320, Android 2.3.5) talking to Nextcloud at its normal `https://` address.
|
||||
|
||||
**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,22 +131,24 @@ 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
|
||||
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 (about 1 MB; 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
|
||||
`apktool.jar` (2.9+, which bundles Google's `aapt` and smali). No Gradle.
|
||||
Needs JDK 11+, Python 3, `d8` and `apksigner.jar` (Android SDK build-tools),
|
||||
`apktool.jar` (2.9+, which bundles Google's `aapt` and smali), and the Bouncy Castle sources renamed
|
||||
to `com.minamp.bc` that Plexamp Lite's `tools/setup_toolchain.sh` prepares (`BC_SRC`, default
|
||||
`~/android-toolchain/bcsrc`). No Gradle.
|
||||
|
||||
```sh
|
||||
D8=d8 APKTOOL=~/apktool.jar APKSIGNER=$SDK/build-tools/34.0.0/lib/apksigner.jar ./build.sh
|
||||
@@ -165,7 +167,7 @@ over the existing app.
|
||||
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
|
||||
javac -d out src/org/gingercal/*.java $(ls test/org/gingercal/*.java | grep -v NetTest)
|
||||
java -cp out org.gingercal.CoreTest # discovery, sync, recurrence, create/edit/delete
|
||||
java -cp out org.gingercal.RedirectTest # redirect handling (no server needed)
|
||||
# the bundled TLS transport (needs the android.jar and Bouncy Castle sources to compile):
|
||||
@@ -181,12 +183,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
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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.6 (Android 2.3)";
|
||||
tlsTexts();
|
||||
}
|
||||
|
||||
|
||||
@@ -159,7 +159,7 @@ public class MainActivity extends Activity implements MonthView.Listener {
|
||||
}
|
||||
|
||||
TextView navArrow(String label, final int delta) {
|
||||
TextView b = App.text(this, label, 20, 0xFFD66E2A); // « »: Latin-1, always in the 2.3 font
|
||||
TextView b = App.text(this, label, 20, 0xFF3D8EF0); // « »: Latin-1, always in the 2.3 font
|
||||
b.setGravity(17 /* Gravity.CENTER */);
|
||||
int sel = App.sysDrawable(this, "list_selector_background");
|
||||
if (sel != 0) b.setBackgroundResource(sel); // press highlight
|
||||
|
||||
@@ -28,7 +28,7 @@ public class MonthView extends View {
|
||||
}
|
||||
|
||||
static final int BG = 0xFF101418, GRID = 0xFF2A3036, WEEKDAY = 0xFF90A4AE, NUM = 0xFFECEFF1,
|
||||
OTHER = 0xFF5C666E, TODAY = 0xFFD66E2A, SELECTED = 0xFF2E3B44;
|
||||
OTHER = 0xFF5C666E, TODAY = 0xFF3D8EF0, SELECTED = 0xFF2E3B44;
|
||||
|
||||
final float density;
|
||||
final Paint fill = new Paint(1 /* ANTI_ALIAS_FLAG */), text = new Paint(1), line = new Paint();
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
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 */);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
After Width: | Height: | Size: 973 KiB |
@@ -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)
|
||||
|
||||
@@ -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/<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)
|
||||
|
||||
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" <<EOF
|
||||
version: 2.9.3
|
||||
apkFileName: GingerCal.apk
|
||||
apkFileName: CalendarLite.apk
|
||||
isFrameworkApk: false
|
||||
usesFramework:
|
||||
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.)
|
||||
if [ ! -f "$KS" ]; then
|
||||
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
|
||||
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
|
||||
|
After Width: | Height: | Size: 7.5 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 2.6 KiB |
|
After Width: | Height: | Size: 499 B |
|
After Width: | Height: | Size: 4.0 KiB |
|
After Width: | Height: | Size: 666 B |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
@@ -1,4 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="app_name">GingerCal</string>
|
||||
<string name="app_name">Calendar Lite</string>
|
||||
</resources>
|
||||
@@ -431,7 +431,7 @@ public class ICal {
|
||||
List<String> l = new ArrayList<String>();
|
||||
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();
|
||||
|
||||
@@ -42,7 +42,7 @@ public final class L {
|
||||
static final String[] GL_DAYS_SHORT = {"", "dom", "lun", "mar", "mér", "xov", "ven", "sáb"};
|
||||
|
||||
public static DateFormatSymbols symbols() {
|
||||
if (!gl) return new DateFormatSymbols(Locale.getDefault());
|
||||
if (!gl) return new DateFormatSymbols(Locale.US); // English names, whatever the phone's language
|
||||
DateFormatSymbols s = new DateFormatSymbols(Locale.US);
|
||||
s.setMonths(GL_MONTHS);
|
||||
s.setShortMonths(GL_MONTHS_SHORT);
|
||||
@@ -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");
|
||||
|
||||
@@ -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)
|
||||