Files
calendar-lite/android/org/gingercal/App.java
T
XoséandClaude Opus 5.5 f7428287eb GingerCal 1.4: bundled TLS 1.2, no bridge needed
CalDAV now goes through Minamp's own HTTP/TLS client (Bouncy Castle TLS 1.2, Mozilla
roots, keep-alive) instead of Apache HttpClient on Android 2.3's TLS 1.0, as the iPod
version does with BearSSL. The phone reaches the normal https:// Nextcloud address at
home and away; a pinned self-signed server or the old bridge still works.

- NetHttp transport; requests run on big-stack threads (the handshake overflows
  AsyncTask/IntentService stacks on Android 2.x)
- http:// -> https:// redirects on the same host are followed; other hosts still refused
- bridge wording replaced in Galician and English; README updated
- NetTest (TLS + CalDAV discovery through the new transport) replaces BridgeTest

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 19:14:06 +02:00

234 lines
8.9 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package org.gingercal;
import java.io.IOException;
import java.net.ConnectException;
import java.net.SocketTimeoutException;
import java.net.URI;
import java.net.UnknownHostException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.Locale;
import javax.net.ssl.SSLException;
import android.app.Activity;
import android.app.AlarmManager;
import android.app.AlertDialog;
import android.app.PendingIntent;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.SharedPreferences;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.widget.LinearLayout;
import android.widget.TextView;
import android.widget.Toast;
import org.gingercal.Model.Ev;
/** Shared helpers: settings, formatting, scheduling, dialogs. */
public final class App {
private App() {}
/** Days of past events kept locally, so this and last month's grid work offline. */
public static final int BACK_DAYS = 35;
public static final int[] INTERVALS = {0, 1, 3, 6, 12, 24};
public static final int[] ALARMS = {-1, 0, 5, 10, 15, 30, 60, 120, 1440};
public static SharedPreferences prefs(Context c) {
return c.getSharedPreferences("gingercal", 0);
}
public static String get(Context c, String k) {
return prefs(c).getString(k, "");
}
public static boolean configured(Context c) {
return get(c, "url").length() > 0 && get(c, "user").length() > 0;
}
public static int interval(Context c) {
return prefs(c).getInt("interval", 3);
}
public static int remind(Context c) {
return prefs(c).getInt("remind", 15);
}
public static int ahead(Context c) {
return prefs(c).getInt("ahead", 60);
}
public static Dav dav(Context c) throws Exception {
return dav(get(c, "url"), get(c, "user"), get(c, "pass"), get(c, "pin"));
}
public static Dav dav(String url, String user, String pass, String pin) throws Exception {
return new Dav(new NetHttp(pin, 30000), url, user, pass);
}
public static boolean online(Context c) {
ConnectivityManager cm = (ConnectivityManager) c.getSystemService("connectivity");
NetworkInfo ni = cm == null ? null : cm.getActiveNetworkInfo();
return ni != null && ni.isConnected();
}
public static void requestSync(Context c, boolean manual, boolean force) {
Intent i = new Intent(c, SyncService.class);
i.putExtra("manual", manual);
i.putExtra("force", force);
c.startService(i);
}
/** (Re)arms the periodic background sync. */
public static void scheduleSync(Context c) {
AlarmManager am = (AlarmManager) c.getSystemService("alarm");
Intent i = new Intent(c, AlarmReceiver.class);
i.setAction(AlarmReceiver.SYNC);
PendingIntent pi = PendingIntent.getBroadcast(c, 1, i, 0);
am.cancel(pi);
int h = interval(c);
if (h > 0 && configured(c)) {
long every = h * ICal.HOUR;
am.setInexactRepeating(1 /* RTC: don't wake the phone just to sync */,
System.currentTimeMillis() + every, every, pi);
}
}
public static boolean isLan(String url) {
try {
String h = URI.create(Dav.normalize(url)).getHost();
if (h == null) return false;
h = h.toLowerCase(Locale.US);
if (h.startsWith("10.") || h.startsWith("192.168.") || h.equals("localhost") || h.endsWith(".local")
|| h.endsWith(".lan") || h.endsWith(".home.arpa")) return true;
if (h.startsWith("172.")) {
String[] p = h.split("\\.");
int b = Integer.parseInt(p[1]);
return b >= 16 && b <= 31;
}
if (h.startsWith("100.")) { // CGNAT / Tailscale-style ranges
int b = Integer.parseInt(h.split("\\.")[1]);
return b >= 64 && b <= 127;
}
} catch (RuntimeException ignored) {
}
return false;
}
// ------------------------------------------------------------ messages
public static String describe(Throwable t) {
if (t instanceof DavException || t instanceof DavException.UntrustedCert) return t.getMessage();
if (t instanceof UnknownHostException) {
return L.f("Can't resolve %s. No connection, or a typo in the server address.", t.getMessage());
}
if (t instanceof ConnectException) return L.f("Can't connect to the server (%s). Wrong address or port, or the server is down?", t.getMessage());
if (t instanceof SocketTimeoutException) return L.t("The server took too long to answer.");
if (t instanceof SSLException || t instanceof com.minamp.bc.tls.TlsException) return L.f("TLS error: %s", t.getMessage());
if (t instanceof IOException) return L.f("Network error: %s", t);
if (t instanceof OutOfMemoryError) return L.t("Out of memory. Lower 'Days ahead' in Settings.");
return L.f("Error: %s", t);
}
public static void toast(Context c, String s) {
Toast.makeText(c, s, 1).show();
}
public interface Trusted {
void trusted(String fingerprint);
}
public static void askTrust(Activity a, final DavException.UntrustedCert u, final Trusted cb) {
String msg = (u.pinMismatch
? L.t("WARNING: the server certificate changed since you trusted it. That happens when a self-signed certificate is regenerated - or when someone is intercepting the connection.\n\n")
: L.t("The server certificate isn't signed by a known authority (normal for a self-signed server or an old TLS 1.0 bridge).\n\n"))
+ L.f("Subject: %s\nIssuer: %s\nExpires: %s\n\nSHA-256:\n%s\n\nOnly trust it if this matches the fingerprint printed on your server.",
u.subject, u.issuer, u.expires, u.fingerprint);
new AlertDialog.Builder(a)
.setTitle(u.pinMismatch ? L.t("Certificate changed!") : L.t("Trust this certificate?"))
.setMessage(msg)
.setPositiveButton(L.t("Trust"), new DialogInterface.OnClickListener() {
public void onClick(DialogInterface d, int w) {
cb.trusted(u.fingerprint);
}
})
.setNegativeButton(L.t("Cancel"), null)
.show();
}
// ------------------------------------------------------------ formatting
public static String time(Context c, long t) {
String p = android.text.format.DateFormat.is24HourFormat(c) ? "HH:mm" : "h:mma";
return L.df(p).format(new Date(t));
}
public static String day(long t) {
boolean thisYear = year(t) == year(System.currentTimeMillis());
return L.df(thisYear ? "EEE d MMM" : "EEE d MMM yyyy").format(new Date(t));
}
static int year(long t) {
Calendar c = Calendar.getInstance();
c.setTimeInMillis(t);
return c.get(Calendar.YEAR);
}
public static String dayHeader(long dayStart) {
long today = ICal.startOfDay(System.currentTimeMillis());
if (dayStart == today) return L.f("Today \u00b7 %s", day(dayStart));
if (dayStart == ICal.addDays(today, 1)) return L.f("Tomorrow \u00b7 %s", day(dayStart));
return day(dayStart);
}
public static String when(Context c, Ev e) {
if (e.allDay) {
long lastDay = ICal.addDays(e.end, -1);
if (lastDay <= e.start) return L.t("All day");
return L.f("All day, until %s", day(lastDay));
}
String s = time(c, e.start);
if (e.end <= e.start) return s;
if (ICal.startOfDay(e.end - 1) != ICal.startOfDay(e.start)) {
return s + " – " + day(e.end) + " " + time(c, e.end);
}
return s + " – " + time(c, e.end);
}
public static String alarmLabel(int m) {
if (m < 0) return L.t("No reminder");
if (m == 0) return L.t("At start time");
if (m < 60) return L.f("%d min before", m);
if (m < 1440) return L.f("%d h before", m / 60);
return L.f("%d day before", m / 1440);
}
// --------------------------------------------------------------- views
public static int dp(Context c, float v) {
return (int) (v * c.getResources().getDisplayMetrics().density + 0.5f);
}
public static TextView text(Context c, String s, float sp, int color) {
TextView t = new TextView(c);
t.setText(s);
t.setTextSize(sp);
t.setTextColor(color);
return t;
}
public static LinearLayout vbox(Context c) {
LinearLayout l = new LinearLayout(c);
l.setOrientation(LinearLayout.VERTICAL);
return l;
}
public static int sysDrawable(Context c, String name) {
return c.getResources().getIdentifier(name, "drawable", "android");
}
}