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 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 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 port, or the bridge is down?", t.getMessage()); if (t instanceof SocketTimeoutException) return L.t("The server took too long to answer."); if (t instanceof SSLException) 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 you regenerate the bridge certificate - or when someone is intercepting the connection.\n\n") : L.t("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")) + 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"); } }