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() {} 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 ApacheHttp(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 "Can't resolve " + t.getMessage() + ". No connection, or a typo in the server address."; } if (t instanceof ConnectException) return "Can't connect to the server (" + t.getMessage() + "). Wrong port, or the bridge is down?"; if (t instanceof SocketTimeoutException) return "The server took too long to answer."; if (t instanceof SSLException) return "TLS error: " + t.getMessage(); if (t instanceof IOException) return "Network error: " + t; if (t instanceof OutOfMemoryError) return "Out of memory. Lower 'Days ahead' in Settings."; return "Error: " + 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 ? "WARNING: the server certificate changed since you trusted it. That happens when you " + "regenerate the bridge certificate - or when someone is intercepting the connection.\n\n" : "This phone doesn't trust the server certificate (normal for the self-signed legacy bridge, " + "or for Let's Encrypt on Android 2.3).\n\n") + "Subject: " + u.subject + "\nIssuer: " + u.issuer + "\nExpires: " + u.expires + "\n\nSHA-256:\n" + u.fingerprint + "\n\nOnly trust it if this matches the fingerprint printed on your server."; new AlertDialog.Builder(a) .setTitle(u.pinMismatch ? "Certificate changed!" : "Trust this certificate?") .setMessage(msg) .setPositiveButton("Trust", new DialogInterface.OnClickListener() { public void onClick(DialogInterface d, int w) { cb.trusted(u.fingerprint); } }) .setNegativeButton("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 new SimpleDateFormat(p, Locale.getDefault()).format(new Date(t)); } public static String day(long t) { boolean thisYear = year(t) == year(System.currentTimeMillis()); return new SimpleDateFormat(thisYear ? "EEE d MMM" : "EEE d MMM yyyy", Locale.getDefault()).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 "Today · " + day(dayStart); if (dayStart == ICal.addDays(today, 1)) return "Tomorrow · " + 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 "All day"; return "All day, until " + 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 "No reminder"; if (m == 0) return "At start time"; if (m < 60) return m + " min before"; if (m < 1440) return (m / 60) + " h before"; return (m / 1440) + " day before"; } // --------------------------------------------------------------- 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"); } }