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>
234 lines
8.9 KiB
Java
234 lines
8.9 KiB
Java
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");
|
||
}
|
||
}
|