Import GingerCal 1.0 source
This commit is contained in:
commit
01f49852ab
116 files changed
+5079
No files matched your search
@@ -0,0 +1,41 @@
|
||||
package org.gingercal;
|
||||
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.PowerManager;
|
||||
|
||||
/** Boot, periodic-sync and reminder alarms all land here. */
|
||||
public class AlarmReceiver extends BroadcastReceiver {
|
||||
public static final String SYNC = "org.gingercal.SYNC";
|
||||
public static final String REMIND = "org.gingercal.REMIND";
|
||||
private static PowerManager.WakeLock lock;
|
||||
|
||||
@Override
|
||||
public void onReceive(Context c, Intent i) {
|
||||
String a = i.getAction();
|
||||
if (REMIND.equals(a)) {
|
||||
Reminders.fire(c);
|
||||
} else if (SYNC.equals(a)) {
|
||||
acquire(c);
|
||||
App.requestSync(c, false, false);
|
||||
} else {
|
||||
// BOOT_COMPLETED / package replaced: alarms don't survive either
|
||||
App.scheduleSync(c);
|
||||
Reminders.schedule(c);
|
||||
}
|
||||
}
|
||||
|
||||
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.setReferenceCounted(false);
|
||||
}
|
||||
lock.acquire(5 * 60 * 1000L);
|
||||
}
|
||||
|
||||
static synchronized void release() {
|
||||
if (lock != null && lock.isHeld()) lock.release();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
package org.gingercal;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.lang.reflect.Method;
|
||||
import java.net.InetAddress;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.Socket;
|
||||
import java.net.URI;
|
||||
import java.security.GeneralSecurityException;
|
||||
|
||||
import javax.net.ssl.HttpsURLConnection;
|
||||
import javax.net.ssl.SSLContext;
|
||||
import javax.net.ssl.SSLException;
|
||||
import javax.net.ssl.SSLSocket;
|
||||
import javax.net.ssl.SSLSocketFactory;
|
||||
import javax.net.ssl.TrustManager;
|
||||
|
||||
import org.apache.http.Header;
|
||||
import org.apache.http.HttpEntity;
|
||||
import org.apache.http.HttpResponse;
|
||||
import org.apache.http.client.methods.HttpEntityEnclosingRequestBase;
|
||||
import org.apache.http.conn.scheme.LayeredSocketFactory;
|
||||
import org.apache.http.conn.scheme.PlainSocketFactory;
|
||||
import org.apache.http.conn.scheme.Scheme;
|
||||
import org.apache.http.conn.scheme.SchemeRegistry;
|
||||
import org.apache.http.entity.StringEntity;
|
||||
import org.apache.http.impl.client.DefaultHttpClient;
|
||||
import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager;
|
||||
import org.apache.http.params.BasicHttpParams;
|
||||
import org.apache.http.params.HttpConnectionParams;
|
||||
import org.apache.http.params.HttpParams;
|
||||
import org.apache.http.params.HttpProtocolParams;
|
||||
|
||||
/**
|
||||
* Android 2.3 transport. HttpURLConnection refuses PROPFIND/REPORT there, so this uses the
|
||||
* bundled Apache HttpClient, plus a TLS layer that supports certificate pinning (needed for a
|
||||
* self-signed legacy-TLS bridge) and SNI where the platform offers it.
|
||||
*/
|
||||
public class ApacheHttp implements Http {
|
||||
final DefaultHttpClient client;
|
||||
final Tls.PinTrust trust;
|
||||
|
||||
public ApacheHttp(String pin, int timeoutMs) throws GeneralSecurityException {
|
||||
trust = new Tls.PinTrust(pin);
|
||||
HttpParams p = new BasicHttpParams();
|
||||
HttpConnectionParams.setConnectionTimeout(p, timeoutMs);
|
||||
HttpConnectionParams.setSoTimeout(p, timeoutMs);
|
||||
HttpConnectionParams.setStaleCheckingEnabled(p, true);
|
||||
HttpProtocolParams.setUseExpectContinue(p, false);
|
||||
HttpProtocolParams.setUserAgent(p, "GingerCal/1.0 (Android 2.3)");
|
||||
p.setBooleanParameter("http.protocol.handle-redirects", false);
|
||||
SchemeRegistry reg = new SchemeRegistry();
|
||||
reg.register(new Scheme("http", PlainSocketFactory.getSocketFactory(), 80));
|
||||
reg.register(new Scheme("https", new TlsFactory(trust), 443));
|
||||
client = new DefaultHttpClient(new ThreadSafeClientConnManager(p, reg), p);
|
||||
}
|
||||
|
||||
static class Req extends HttpEntityEnclosingRequestBase {
|
||||
final String m;
|
||||
|
||||
Req(String m, URI u) {
|
||||
this.m = m;
|
||||
setURI(u);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getMethod() {
|
||||
return m;
|
||||
}
|
||||
}
|
||||
|
||||
public Resp exec(String method, String url, String[] headers, String body, String contentType)
|
||||
throws IOException {
|
||||
Req r = new Req(method, URI.create(url));
|
||||
for (int i = 0; i + 1 < headers.length; i += 2) r.addHeader(headers[i], headers[i + 1]);
|
||||
if (body != null) {
|
||||
StringEntity e = new StringEntity(body, "UTF-8");
|
||||
e.setContentType(contentType);
|
||||
r.setEntity(e);
|
||||
}
|
||||
trust.reset();
|
||||
HttpResponse res;
|
||||
try {
|
||||
res = client.execute(r);
|
||||
} catch (IOException e) {
|
||||
throw trust.translate(e);
|
||||
}
|
||||
Resp out = new Resp();
|
||||
out.code = res.getStatusLine().getStatusCode();
|
||||
Header h = res.getFirstHeader("ETag");
|
||||
if (h != null) out.etag = h.getValue();
|
||||
h = res.getFirstHeader("Location");
|
||||
if (h != null) out.location = h.getValue();
|
||||
HttpEntity en = res.getEntity();
|
||||
if (en != null) {
|
||||
InputStream in = en.getContent();
|
||||
try {
|
||||
ByteArrayOutputStream bo = new ByteArrayOutputStream();
|
||||
byte[] buf = new byte[8192];
|
||||
int n;
|
||||
while ((n = in.read(buf)) > 0) bo.write(buf, 0, n);
|
||||
out.body = new String(bo.toByteArray(), "UTF-8");
|
||||
} finally {
|
||||
en.consumeContent();
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
public void close() {
|
||||
client.getConnectionManager().shutdown();
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ TLS
|
||||
|
||||
static class TlsFactory implements LayeredSocketFactory {
|
||||
final SSLSocketFactory f;
|
||||
final Tls.PinTrust trust;
|
||||
|
||||
TlsFactory(Tls.PinTrust trust) throws GeneralSecurityException {
|
||||
this.trust = trust;
|
||||
SSLContext c = SSLContext.getInstance("TLS");
|
||||
c.init(null, new TrustManager[] {trust}, null);
|
||||
f = c.getSocketFactory();
|
||||
}
|
||||
|
||||
public Socket createSocket() throws IOException {
|
||||
return f.createSocket();
|
||||
}
|
||||
|
||||
public Socket connectSocket(Socket sock, String host, int port, InetAddress local, int localPort,
|
||||
HttpParams params) throws IOException {
|
||||
SSLSocket s = (SSLSocket) (sock != null ? sock : createSocket());
|
||||
if (local != null || localPort > 0) {
|
||||
s.bind(new InetSocketAddress(local, localPort < 0 ? 0 : localPort));
|
||||
}
|
||||
sni(s, host);
|
||||
s.connect(new InetSocketAddress(host, port), HttpConnectionParams.getConnectionTimeout(params));
|
||||
s.setSoTimeout(HttpConnectionParams.getSoTimeout(params));
|
||||
s.startHandshake();
|
||||
verifyHost(s, host);
|
||||
return s;
|
||||
}
|
||||
|
||||
public Socket createSocket(Socket socket, String host, int port, boolean autoClose) throws IOException {
|
||||
SSLSocket s = (SSLSocket) f.createSocket(socket, host, port, autoClose);
|
||||
sni(s, host);
|
||||
s.startHandshake();
|
||||
verifyHost(s, host);
|
||||
return s;
|
||||
}
|
||||
|
||||
public boolean isSecure(Socket sock) {
|
||||
return true;
|
||||
}
|
||||
|
||||
void verifyHost(SSLSocket s, String host) throws IOException {
|
||||
if (trust.pinned()) return; // the pin identifies the exact certificate
|
||||
if (!HttpsURLConnection.getDefaultHostnameVerifier().verify(host, s.getSession())) {
|
||||
s.close();
|
||||
throw new SSLException("Certificate is not valid for " + host);
|
||||
}
|
||||
}
|
||||
|
||||
/** Gingerbread's OpenSSLSocketImpl has a hidden setHostname() for SNI. */
|
||||
static void sni(SSLSocket s, String host) {
|
||||
if (host.matches("[0-9.]+") || host.indexOf(':') >= 0) return;
|
||||
try {
|
||||
Method m = s.getClass().getMethod("setHostname", String.class);
|
||||
m.invoke(s, host);
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,233 @@
|
||||
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");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
package org.gingercal;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.AlertDialog;
|
||||
import android.content.DialogInterface;
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
import android.widget.Button;
|
||||
import android.widget.CheckBox;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.ScrollView;
|
||||
|
||||
import org.gingercal.Model.Cal;
|
||||
|
||||
/** Pick which calendars to show and which one new events go to. */
|
||||
public class CalendarsActivity extends Activity {
|
||||
final List<Cal> cals = new ArrayList<Cal>();
|
||||
final List<CheckBox> boxes = new ArrayList<CheckBox>();
|
||||
Button defBtn;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle b) {
|
||||
super.onCreate(b);
|
||||
setTitle("Calendars");
|
||||
cals.addAll(Db.get(this).calendarList());
|
||||
LinearLayout f = App.vbox(this);
|
||||
int p = App.dp(this, 8);
|
||||
f.setPadding(p, p, p, p);
|
||||
if (cals.isEmpty()) {
|
||||
f.addView(App.text(this, "No calendars yet. Set up the account in Settings and sync.", 14, 0xFFFFFFFF));
|
||||
}
|
||||
for (Cal c : cals) {
|
||||
LinearLayout row = new LinearLayout(this);
|
||||
row.setOrientation(LinearLayout.HORIZONTAL);
|
||||
View bar = new View(this);
|
||||
bar.setBackgroundColor(c.color);
|
||||
LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(App.dp(this, 6), -1);
|
||||
lp.setMargins(0, App.dp(this, 6), App.dp(this, 4), App.dp(this, 6));
|
||||
row.addView(bar, lp);
|
||||
CheckBox cb = new CheckBox(this);
|
||||
cb.setText(c.name + (c.writable ? "" : " (read-only)"));
|
||||
cb.setChecked(c.enabled);
|
||||
row.addView(cb, new LinearLayout.LayoutParams(-1, -2));
|
||||
boxes.add(cb);
|
||||
f.addView(row, new LinearLayout.LayoutParams(-1, -2));
|
||||
}
|
||||
defBtn = new Button(this);
|
||||
defBtn.setOnClickListener(new View.OnClickListener() {
|
||||
public void onClick(View v) {
|
||||
pickDefault();
|
||||
}
|
||||
});
|
||||
if (!cals.isEmpty()) f.addView(defBtn, new LinearLayout.LayoutParams(-1, -2));
|
||||
ScrollView sv = new ScrollView(this);
|
||||
sv.addView(f);
|
||||
setContentView(sv);
|
||||
label();
|
||||
}
|
||||
|
||||
void label() {
|
||||
String def = App.get(this, "defcal");
|
||||
String name = "first writable";
|
||||
for (Cal c : cals) if (c.href.equals(def)) name = c.name;
|
||||
defBtn.setText("New events go to: " + name);
|
||||
}
|
||||
|
||||
void pickDefault() {
|
||||
final List<Cal> w = new ArrayList<Cal>();
|
||||
for (Cal c : cals) if (c.writable) w.add(c);
|
||||
String[] names = new String[w.size()];
|
||||
for (int i = 0; i < names.length; i++) names[i] = w.get(i).name;
|
||||
new AlertDialog.Builder(this).setTitle("Default calendar")
|
||||
.setItems(names, new DialogInterface.OnClickListener() {
|
||||
public void onClick(DialogInterface d, int which) {
|
||||
App.prefs(CalendarsActivity.this).edit().putString("defcal", w.get(which).href).commit();
|
||||
label();
|
||||
}
|
||||
}).show();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPause() {
|
||||
super.onPause();
|
||||
boolean changed = false;
|
||||
for (int i = 0; i < cals.size(); i++) {
|
||||
boolean on = boxes.get(i).isChecked();
|
||||
if (on != cals.get(i).enabled) {
|
||||
Db.get(this).setEnabled(cals.get(i).href, on);
|
||||
cals.get(i).enabled = on;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
if (changed) App.requestSync(this, true, false);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,193 @@
|
||||
package org.gingercal;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
import android.database.sqlite.SQLiteOpenHelper;
|
||||
|
||||
import org.gingercal.Model.Cal;
|
||||
import org.gingercal.Model.Ev;
|
||||
|
||||
/** Local cache so the agenda works offline and the phone doesn't refetch on every launch. */
|
||||
public class Db extends SQLiteOpenHelper implements Model.Store {
|
||||
private static Db instance;
|
||||
|
||||
public static synchronized Db get(Context c) {
|
||||
if (instance == null) instance = new Db(c.getApplicationContext());
|
||||
return instance;
|
||||
}
|
||||
|
||||
private Db(Context c) {
|
||||
super(c, "gingercal.db", null, 1);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(SQLiteDatabase db) {
|
||||
db.execSQL("CREATE TABLE cals (href TEXT PRIMARY KEY, name TEXT, color INTEGER, enabled INTEGER,"
|
||||
+ " writable INTEGER, ctag TEXT, synced INTEGER)");
|
||||
db.execSQL("CREATE TABLE events (_id INTEGER PRIMARY KEY AUTOINCREMENT, cal TEXT, href TEXT, etag TEXT,"
|
||||
+ " uid TEXT, title TEXT, location TEXT, descr TEXT, dtstart INTEGER, dtend INTEGER,"
|
||||
+ " allday INTEGER, recurring INTEGER)");
|
||||
db.execSQL("CREATE INDEX ev_start ON events(dtstart)");
|
||||
db.execSQL("CREATE INDEX ev_cal ON events(cal)");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUpgrade(SQLiteDatabase db, int from, int to) {
|
||||
db.execSQL("DROP TABLE IF EXISTS events");
|
||||
db.execSQL("DROP TABLE IF EXISTS cals");
|
||||
onCreate(db);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------ calendars
|
||||
|
||||
public synchronized Map<String, Cal> calendars() {
|
||||
Map<String, Cal> m = new LinkedHashMap<String, Cal>();
|
||||
Cursor c = getReadableDatabase().rawQuery(
|
||||
"SELECT href, name, color, enabled, writable, ctag, synced FROM cals ORDER BY name", null);
|
||||
try {
|
||||
while (c.moveToNext()) {
|
||||
Cal x = new Cal();
|
||||
x.href = c.getString(0);
|
||||
x.name = c.getString(1);
|
||||
x.color = c.getInt(2);
|
||||
x.enabled = c.getInt(3) != 0;
|
||||
x.writable = c.getInt(4) != 0;
|
||||
x.ctag = c.getString(5);
|
||||
x.synced = c.getLong(6);
|
||||
m.put(x.href, x);
|
||||
}
|
||||
} finally {
|
||||
c.close();
|
||||
}
|
||||
return m;
|
||||
}
|
||||
|
||||
public List<Cal> calendarList() {
|
||||
return new ArrayList<Cal>(calendars().values());
|
||||
}
|
||||
|
||||
public synchronized void saveCalendar(Cal c) {
|
||||
getWritableDatabase().execSQL("INSERT OR REPLACE INTO cals (href, name, color, enabled, writable, ctag, synced)"
|
||||
+ " VALUES (?,?,?,?,?,?,?)", new Object[] {c.href, c.name, Integer.valueOf(c.color),
|
||||
Integer.valueOf(c.enabled ? 1 : 0), Integer.valueOf(c.writable ? 1 : 0), c.ctag, Long.valueOf(c.synced)});
|
||||
}
|
||||
|
||||
public synchronized void setEnabled(String href, boolean on) {
|
||||
// reset ctag/synced so the next sync refetches it
|
||||
getWritableDatabase().execSQL("UPDATE cals SET enabled=?, ctag='', synced=0 WHERE href=?",
|
||||
new Object[] {Integer.valueOf(on ? 1 : 0), href});
|
||||
if (!on) getWritableDatabase().execSQL("DELETE FROM events WHERE cal=?", new Object[] {href});
|
||||
}
|
||||
|
||||
public synchronized void deleteCalendar(String href) {
|
||||
SQLiteDatabase db = getWritableDatabase();
|
||||
db.execSQL("DELETE FROM events WHERE cal=?", new Object[] {href});
|
||||
db.execSQL("DELETE FROM cals WHERE href=?", new Object[] {href});
|
||||
}
|
||||
|
||||
public synchronized void forgetSyncState() {
|
||||
getWritableDatabase().execSQL("UPDATE cals SET ctag='', synced=0");
|
||||
}
|
||||
|
||||
public synchronized void clearAll() {
|
||||
SQLiteDatabase db = getWritableDatabase();
|
||||
db.execSQL("DELETE FROM events");
|
||||
db.execSQL("DELETE FROM cals");
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------- events
|
||||
|
||||
public synchronized void replaceEvents(String cal, List<Ev> evs) {
|
||||
SQLiteDatabase db = getWritableDatabase();
|
||||
db.beginTransaction();
|
||||
try {
|
||||
db.execSQL("DELETE FROM events WHERE cal=?", new Object[] {cal});
|
||||
for (Ev e : evs) {
|
||||
db.execSQL("INSERT INTO events (cal, href, etag, uid, title, location, descr, dtstart, dtend, allday,"
|
||||
+ " recurring) VALUES (?,?,?,?,?,?,?,?,?,?,?)", new Object[] {cal, e.href, e.etag, e.uid,
|
||||
e.title, e.location, e.descr, Long.valueOf(e.start), Long.valueOf(e.end),
|
||||
Integer.valueOf(e.allDay ? 1 : 0), Integer.valueOf(e.recurring ? 1 : 0)});
|
||||
}
|
||||
db.setTransactionSuccessful();
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
}
|
||||
}
|
||||
|
||||
static final String EV_COLS = "e._id, e.cal, e.href, e.etag, e.uid, e.title, e.location, e.descr, e.dtstart,"
|
||||
+ " e.dtend, e.allday, e.recurring, c.color, c.writable";
|
||||
|
||||
static Ev read(Cursor c) {
|
||||
Ev e = new Ev();
|
||||
e.id = c.getLong(0);
|
||||
e.cal = c.getString(1);
|
||||
e.href = c.getString(2);
|
||||
e.etag = c.getString(3);
|
||||
e.uid = c.getString(4);
|
||||
e.title = c.getString(5);
|
||||
e.location = c.getString(6);
|
||||
e.descr = c.getString(7);
|
||||
e.start = c.getLong(8);
|
||||
e.end = c.getLong(9);
|
||||
e.allDay = c.getInt(10) != 0;
|
||||
e.recurring = c.getInt(11) != 0;
|
||||
e.color = c.getInt(12);
|
||||
e.writable = c.getInt(13) != 0;
|
||||
return e;
|
||||
}
|
||||
|
||||
List<Ev> query(String where, String[] args) {
|
||||
List<Ev> out = new ArrayList<Ev>();
|
||||
Cursor c = getReadableDatabase().rawQuery("SELECT " + EV_COLS + " FROM events e JOIN cals c ON c.href = e.cal"
|
||||
+ " WHERE c.enabled = 1 AND (" + where + ") ORDER BY e.dtstart, e.allday DESC, e.title", args);
|
||||
try {
|
||||
while (c.moveToNext()) out.add(read(c));
|
||||
} finally {
|
||||
c.close();
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/** Everything still running or starting after `from`. */
|
||||
public synchronized List<Ev> agenda(long from, long to) {
|
||||
return query("e.dtend > ? AND e.dtstart < ? OR (e.dtstart = e.dtend AND e.dtstart >= ? AND e.dtstart < ?)",
|
||||
new String[] {String.valueOf(from), String.valueOf(to), String.valueOf(from), String.valueOf(to)});
|
||||
}
|
||||
|
||||
public synchronized Ev event(long id) {
|
||||
List<Ev> l = query("e._id = ?", new String[] {String.valueOf(id)});
|
||||
return l.isEmpty() ? null : l.get(0);
|
||||
}
|
||||
|
||||
/** Timed events starting in (after, until]. */
|
||||
public synchronized List<Ev> startingBetween(long after, long until) {
|
||||
return query("e.allday = 0 AND e.dtstart > ? AND e.dtstart <= ?",
|
||||
new String[] {String.valueOf(after), String.valueOf(until)});
|
||||
}
|
||||
|
||||
public synchronized long nextStartAfter(long after) {
|
||||
Cursor c = getReadableDatabase().rawQuery("SELECT MIN(e.dtstart) FROM events e JOIN cals c ON c.href = e.cal"
|
||||
+ " WHERE c.enabled = 1 AND e.allday = 0 AND e.dtstart > ?", new String[] {String.valueOf(after)});
|
||||
try {
|
||||
if (c.moveToNext() && !c.isNull(0)) return c.getLong(0);
|
||||
return -1;
|
||||
} finally {
|
||||
c.close();
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized int count() {
|
||||
Cursor c = getReadableDatabase().rawQuery("SELECT COUNT(*) FROM events", null);
|
||||
try {
|
||||
return c.moveToNext() ? c.getInt(0) : 0;
|
||||
} finally {
|
||||
c.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,383 @@
|
||||
package org.gingercal;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Calendar;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.AlertDialog;
|
||||
import android.app.DatePickerDialog;
|
||||
import android.app.ProgressDialog;
|
||||
import android.app.TimePickerDialog;
|
||||
import android.content.DialogInterface;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
import android.widget.Button;
|
||||
import android.widget.CheckBox;
|
||||
import android.widget.DatePicker;
|
||||
import android.widget.EditText;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.ScrollView;
|
||||
import android.widget.TextView;
|
||||
import android.widget.TimePicker;
|
||||
|
||||
import org.gingercal.Model.Cal;
|
||||
import org.gingercal.Model.Ev;
|
||||
|
||||
/** Create or edit a single (non-repeating) event. */
|
||||
public class EditActivity extends Activity {
|
||||
EditText title, location, notes;
|
||||
CheckBox allDay;
|
||||
Button startDate, startTime, endDate, endTime, alarmBtn, calBtn, save;
|
||||
final Calendar start = Calendar.getInstance(), end = Calendar.getInstance();
|
||||
int alarm = 15, originalAlarm = -3;
|
||||
String calHref;
|
||||
Ev editing;
|
||||
String ics, etag;
|
||||
List<Cal> cals = new ArrayList<Cal>();
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle b) {
|
||||
super.onCreate(b);
|
||||
long id = getIntent().getLongExtra("id", -1);
|
||||
if (id >= 0) editing = Db.get(this).event(id);
|
||||
setTitle(editing == null ? "New event" : "Edit event");
|
||||
for (Cal c : Db.get(this).calendarList()) if (c.enabled && c.writable) cals.add(c);
|
||||
|
||||
LinearLayout f = App.vbox(this);
|
||||
int p = App.dp(this, 8);
|
||||
f.setPadding(p, p, p, p);
|
||||
title = field(f, "Title", false);
|
||||
allDay = new CheckBox(this);
|
||||
allDay.setText("All day");
|
||||
allDay.setOnClickListener(new View.OnClickListener() {
|
||||
public void onClick(View v) {
|
||||
refresh();
|
||||
}
|
||||
});
|
||||
f.addView(allDay);
|
||||
f.addView(label("Starts"));
|
||||
startDate = new Button(this);
|
||||
startTime = new Button(this);
|
||||
f.addView(pair(startDate, startTime));
|
||||
f.addView(label("Ends"));
|
||||
endDate = new Button(this);
|
||||
endTime = new Button(this);
|
||||
f.addView(pair(endDate, endTime));
|
||||
location = field(f, "Location", false);
|
||||
notes = field(f, "Notes", true);
|
||||
alarmBtn = new Button(this);
|
||||
f.addView(alarmBtn, new LinearLayout.LayoutParams(-1, -2));
|
||||
calBtn = new Button(this);
|
||||
f.addView(calBtn, new LinearLayout.LayoutParams(-1, -2));
|
||||
save = new Button(this);
|
||||
save.setText("Save");
|
||||
f.addView(save, new LinearLayout.LayoutParams(-1, -2));
|
||||
ScrollView sv = new ScrollView(this);
|
||||
sv.addView(f);
|
||||
setContentView(sv);
|
||||
|
||||
startDate.setOnClickListener(new View.OnClickListener() {
|
||||
public void onClick(View v) {
|
||||
pickDate(start, true);
|
||||
}
|
||||
});
|
||||
endDate.setOnClickListener(new View.OnClickListener() {
|
||||
public void onClick(View v) {
|
||||
pickDate(end, false);
|
||||
}
|
||||
});
|
||||
startTime.setOnClickListener(new View.OnClickListener() {
|
||||
public void onClick(View v) {
|
||||
pickTime(start, true);
|
||||
}
|
||||
});
|
||||
endTime.setOnClickListener(new View.OnClickListener() {
|
||||
public void onClick(View v) {
|
||||
pickTime(end, false);
|
||||
}
|
||||
});
|
||||
alarmBtn.setOnClickListener(new View.OnClickListener() {
|
||||
public void onClick(View v) {
|
||||
pickAlarm();
|
||||
}
|
||||
});
|
||||
calBtn.setOnClickListener(new View.OnClickListener() {
|
||||
public void onClick(View v) {
|
||||
pickCalendar();
|
||||
}
|
||||
});
|
||||
save.setOnClickListener(new View.OnClickListener() {
|
||||
public void onClick(View v) {
|
||||
save();
|
||||
}
|
||||
});
|
||||
|
||||
if (editing == null) {
|
||||
long now = System.currentTimeMillis();
|
||||
start.setTimeInMillis(now - now % ICal.HOUR + ICal.HOUR);
|
||||
start.set(Calendar.SECOND, 0);
|
||||
start.set(Calendar.MILLISECOND, 0);
|
||||
end.setTimeInMillis(start.getTimeInMillis() + ICal.HOUR);
|
||||
String def = App.get(this, "defcal");
|
||||
calHref = cals.isEmpty() ? null : cals.get(0).href;
|
||||
for (Cal c : cals) if (c.href.equals(def)) calHref = def;
|
||||
refresh();
|
||||
} else {
|
||||
calHref = editing.cal;
|
||||
calBtn.setEnabled(false);
|
||||
fill(editing.title, editing.location, editing.descr, editing.start, editing.end, editing.allDay);
|
||||
save.setEnabled(false);
|
||||
new LoadTask().execute();
|
||||
}
|
||||
}
|
||||
|
||||
EditText field(LinearLayout f, String hint, boolean multi) {
|
||||
EditText e = new EditText(this);
|
||||
e.setHint(hint);
|
||||
if (multi) {
|
||||
e.setInputType(1 | 0x4000 | 0x20000); // text | cap sentences | multi-line
|
||||
e.setMinLines(3);
|
||||
} else {
|
||||
e.setSingleLine(true);
|
||||
e.setInputType(1 | 0x4000);
|
||||
}
|
||||
f.addView(e, new LinearLayout.LayoutParams(-1, -2));
|
||||
return e;
|
||||
}
|
||||
|
||||
TextView label(String s) {
|
||||
TextView t = App.text(this, s, 13, 0xFFB0BEC5);
|
||||
t.setPadding(App.dp(this, 4), App.dp(this, 6), 0, 0);
|
||||
return t;
|
||||
}
|
||||
|
||||
LinearLayout pair(Button a, Button b) {
|
||||
LinearLayout l = new LinearLayout(this);
|
||||
l.setOrientation(LinearLayout.HORIZONTAL);
|
||||
l.addView(a, new LinearLayout.LayoutParams(0, -2, 3f));
|
||||
l.addView(b, new LinearLayout.LayoutParams(0, -2, 2f));
|
||||
return l;
|
||||
}
|
||||
|
||||
void fill(String t, String loc, String desc, long s, long e, boolean ad) {
|
||||
title.setText(t);
|
||||
location.setText(loc);
|
||||
notes.setText(desc);
|
||||
allDay.setChecked(ad);
|
||||
start.setTimeInMillis(s);
|
||||
// all-day DTEND is exclusive; show the last day instead
|
||||
end.setTimeInMillis(ad ? Math.max(s, ICal.addDays(e, -1)) : e);
|
||||
refresh();
|
||||
}
|
||||
|
||||
void refresh() {
|
||||
boolean ad = allDay.isChecked();
|
||||
startDate.setText(App.day(start.getTimeInMillis()));
|
||||
endDate.setText(App.day(end.getTimeInMillis()));
|
||||
startTime.setText(App.time(this, start.getTimeInMillis()));
|
||||
endTime.setText(App.time(this, end.getTimeInMillis()));
|
||||
startTime.setVisibility(ad ? View.GONE : View.VISIBLE);
|
||||
endTime.setVisibility(ad ? View.GONE : View.VISIBLE);
|
||||
alarmBtn.setText("Alarm: " + App.alarmLabel(alarm));
|
||||
String name = "?";
|
||||
for (Cal c : Db.get(this).calendarList()) if (c.href.equals(calHref)) name = c.name;
|
||||
calBtn.setText("Calendar: " + name);
|
||||
}
|
||||
|
||||
void pickDate(final Calendar c, final boolean isStart) {
|
||||
new DatePickerDialog(this, new DatePickerDialog.OnDateSetListener() {
|
||||
public void onDateSet(DatePicker v, int y, int m, int d) {
|
||||
long dur = end.getTimeInMillis() - start.getTimeInMillis();
|
||||
c.set(y, m, d);
|
||||
if (isStart) end.setTimeInMillis(start.getTimeInMillis() + Math.max(0, dur));
|
||||
refresh();
|
||||
}
|
||||
}, c.get(Calendar.YEAR), c.get(Calendar.MONTH), c.get(Calendar.DAY_OF_MONTH)).show();
|
||||
}
|
||||
|
||||
void pickTime(final Calendar c, final boolean isStart) {
|
||||
new TimePickerDialog(this, new TimePickerDialog.OnTimeSetListener() {
|
||||
public void onTimeSet(TimePicker v, int h, int m) {
|
||||
long dur = end.getTimeInMillis() - start.getTimeInMillis();
|
||||
c.set(Calendar.HOUR_OF_DAY, h);
|
||||
c.set(Calendar.MINUTE, m);
|
||||
if (isStart) end.setTimeInMillis(start.getTimeInMillis() + Math.max(0, dur));
|
||||
refresh();
|
||||
}
|
||||
}, c.get(Calendar.HOUR_OF_DAY), c.get(Calendar.MINUTE),
|
||||
android.text.format.DateFormat.is24HourFormat(this)).show();
|
||||
}
|
||||
|
||||
void pickAlarm() {
|
||||
String[] labels = new String[App.ALARMS.length];
|
||||
for (int i = 0; i < labels.length; i++) labels[i] = App.alarmLabel(App.ALARMS[i]);
|
||||
new AlertDialog.Builder(this).setTitle("Alarm (saved in the event)")
|
||||
.setItems(labels, new DialogInterface.OnClickListener() {
|
||||
public void onClick(DialogInterface d, int w) {
|
||||
alarm = App.ALARMS[w];
|
||||
refresh();
|
||||
}
|
||||
}).show();
|
||||
}
|
||||
|
||||
void pickCalendar() {
|
||||
String[] names = new String[cals.size()];
|
||||
for (int i = 0; i < names.length; i++) names[i] = cals.get(i).name;
|
||||
new AlertDialog.Builder(this).setTitle("Calendar")
|
||||
.setItems(names, new DialogInterface.OnClickListener() {
|
||||
public void onClick(DialogInterface d, int w) {
|
||||
calHref = cals.get(w).href;
|
||||
refresh();
|
||||
}
|
||||
}).show();
|
||||
}
|
||||
|
||||
static String txt(TextView t) {
|
||||
return t.getText().toString().trim();
|
||||
}
|
||||
|
||||
void save() {
|
||||
boolean ad = allDay.isChecked();
|
||||
long s, e;
|
||||
if (ad) {
|
||||
s = ICal.startOfDay(start.getTimeInMillis());
|
||||
e = ICal.addDays(ICal.startOfDay(end.getTimeInMillis()), 1);
|
||||
} else {
|
||||
s = start.getTimeInMillis() - start.getTimeInMillis() % 60000L;
|
||||
e = end.getTimeInMillis() - end.getTimeInMillis() % 60000L;
|
||||
}
|
||||
if (e < s || (ad && e <= s)) {
|
||||
App.toast(this, "The end is before the start.");
|
||||
return;
|
||||
}
|
||||
if (calHref == null) {
|
||||
App.toast(this, "No writable calendar. Sync first.");
|
||||
return;
|
||||
}
|
||||
String t = txt(title);
|
||||
new SaveTask(t.length() > 0 ? t : "(no title)", txt(location), notes.getText().toString().trim(), s, e, ad).execute();
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------- network
|
||||
|
||||
class LoadTask extends AsyncTask<Void, Void, Object> {
|
||||
ProgressDialog pd;
|
||||
|
||||
@Override
|
||||
protected void onPreExecute() {
|
||||
pd = ProgressDialog.show(EditActivity.this, null, "Loading…");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Object doInBackground(Void... v) {
|
||||
Dav d = null;
|
||||
try {
|
||||
d = App.dav(EditActivity.this);
|
||||
return d.get(editing.href);
|
||||
} catch (Throwable t) {
|
||||
return t;
|
||||
} finally {
|
||||
if (d != null) d.close();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(Object r) {
|
||||
try {
|
||||
pd.dismiss();
|
||||
} catch (RuntimeException ignored) {
|
||||
}
|
||||
if (r instanceof Throwable) {
|
||||
fail("Can't load event", App.describe((Throwable) r));
|
||||
return;
|
||||
}
|
||||
String[] got = (String[]) r;
|
||||
ics = got[0];
|
||||
etag = got[1] != null ? got[1] : editing.etag;
|
||||
if (ICal.isRecurring(ics)) {
|
||||
fail("Repeating event", "Repeating events can't be edited on the phone yet. Edit the series in Nextcloud.");
|
||||
return;
|
||||
}
|
||||
List<ICal.VEvent> vs = ICal.parse(ics);
|
||||
if (vs.isEmpty()) {
|
||||
fail("Can't edit", "The server returned no event data.");
|
||||
return;
|
||||
}
|
||||
ICal.VEvent v = vs.get(0);
|
||||
alarm = v.alarmMin;
|
||||
originalAlarm = v.alarmMin;
|
||||
fill(v.summary, v.location, v.description, v.start, v.end, v.allDay);
|
||||
save.setEnabled(true);
|
||||
}
|
||||
}
|
||||
|
||||
void fail(String t, String msg) {
|
||||
new AlertDialog.Builder(this).setTitle(t).setMessage(msg)
|
||||
.setPositiveButton("OK", new DialogInterface.OnClickListener() {
|
||||
public void onClick(DialogInterface d, int w) {
|
||||
finish();
|
||||
}
|
||||
}).show();
|
||||
}
|
||||
|
||||
class SaveTask extends AsyncTask<Void, Void, Throwable> {
|
||||
final String t, loc, desc;
|
||||
final long s, e;
|
||||
final boolean ad;
|
||||
ProgressDialog pd;
|
||||
|
||||
SaveTask(String t, String loc, String desc, long s, long e, boolean ad) {
|
||||
this.t = t;
|
||||
this.loc = loc;
|
||||
this.desc = desc;
|
||||
this.s = s;
|
||||
this.e = e;
|
||||
this.ad = ad;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPreExecute() {
|
||||
pd = ProgressDialog.show(EditActivity.this, null, "Saving…");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Throwable doInBackground(Void... v) {
|
||||
Dav d = null;
|
||||
try {
|
||||
d = App.dav(EditActivity.this);
|
||||
if (editing == null) {
|
||||
String uid = UUID.randomUUID().toString().toUpperCase();
|
||||
d.create(calHref, uid, ICal.newEvent(uid, t, loc, desc, s, e, ad, alarm));
|
||||
} else {
|
||||
int a = alarm == originalAlarm ? -2 : alarm;
|
||||
d.update(editing.href, etag, ICal.modify(ics, t, loc, desc, s, e, ad, a));
|
||||
}
|
||||
return null;
|
||||
} catch (Throwable x) {
|
||||
return x;
|
||||
} finally {
|
||||
if (d != null) d.close();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(Throwable x) {
|
||||
try {
|
||||
pd.dismiss();
|
||||
} catch (RuntimeException ignored) {
|
||||
}
|
||||
if (x != null) {
|
||||
new AlertDialog.Builder(EditActivity.this).setTitle("Not saved")
|
||||
.setMessage(App.describe(x)).setPositiveButton("OK", null).show();
|
||||
return;
|
||||
}
|
||||
if (editing == null) App.prefs(EditActivity.this).edit().putString("defcal", calHref).commit();
|
||||
App.requestSync(EditActivity.this, true, false);
|
||||
App.toast(EditActivity.this, "Saved");
|
||||
finish();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,373 @@
|
||||
package org.gingercal;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.AlertDialog;
|
||||
import android.app.ProgressDialog;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Bundle;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.AdapterView;
|
||||
import android.widget.BaseAdapter;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.ListView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.gingercal.Model.Cal;
|
||||
import org.gingercal.Model.Ev;
|
||||
|
||||
/** Agenda: upcoming events grouped by day. */
|
||||
public class MainActivity extends Activity {
|
||||
static final int M_SYNC = 1, M_NEW = 2, M_CALS = 3, M_SETTINGS = 4;
|
||||
|
||||
TextView status;
|
||||
ListView list;
|
||||
final List<Object> rows = new ArrayList<Object>();
|
||||
final Agenda adapter = new Agenda();
|
||||
boolean askedSetup;
|
||||
|
||||
final BroadcastReceiver rx = new BroadcastReceiver() {
|
||||
@Override
|
||||
public void onReceive(Context c, Intent i) {
|
||||
String state = i.getStringExtra("state");
|
||||
if ("start".equals(state)) {
|
||||
showStatus();
|
||||
return;
|
||||
}
|
||||
reload();
|
||||
String fp = i.getStringExtra("fp");
|
||||
if (fp != null) {
|
||||
DavException.UntrustedCert u = new DavException.UntrustedCert(fp, i.getStringExtra("subject"),
|
||||
i.getStringExtra("issuer"), i.getStringExtra("expires"), i.getBooleanExtra("mismatch", false));
|
||||
App.askTrust(MainActivity.this, u, new App.Trusted() {
|
||||
public void trusted(String f) {
|
||||
App.prefs(MainActivity.this).edit().putString("pin", f).commit();
|
||||
App.requestSync(MainActivity.this, true, false);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle b) {
|
||||
super.onCreate(b);
|
||||
setTitle("GingerCal");
|
||||
LinearLayout root = App.vbox(this);
|
||||
status = App.text(this, "", 12, 0xFFAAAAAA);
|
||||
int p = App.dp(this, 6);
|
||||
status.setPadding(p * 2, p, p * 2, p);
|
||||
status.setBackgroundColor(0xFF1C1C1C);
|
||||
root.addView(status, new LinearLayout.LayoutParams(-1, -2));
|
||||
list = new ListView(this);
|
||||
list.setAdapter(adapter);
|
||||
list.setOnItemClickListener(new AdapterView.OnItemClickListener() {
|
||||
public void onItemClick(AdapterView<?> parent, View v, int pos, long id) {
|
||||
Object o = rows.get(pos);
|
||||
if (o instanceof Ev) showEvent((Ev) o);
|
||||
}
|
||||
});
|
||||
root.addView(list, new LinearLayout.LayoutParams(-1, -1));
|
||||
setContentView(root);
|
||||
App.scheduleSync(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
registerReceiver(rx, new IntentFilter(SyncService.STATE));
|
||||
if (!App.configured(this)) {
|
||||
showStatus();
|
||||
if (!askedSetup) {
|
||||
askedSetup = true;
|
||||
startActivity(new Intent(this, SettingsActivity.class));
|
||||
}
|
||||
return;
|
||||
}
|
||||
reload();
|
||||
long last = App.prefs(this).getLong("lastsync", 0);
|
||||
if (!SyncService.running && System.currentTimeMillis() - last > 15 * ICal.MIN) {
|
||||
App.requestSync(this, true, false);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPause() {
|
||||
super.onPause();
|
||||
unregisterReceiver(rx);
|
||||
}
|
||||
|
||||
void showStatus() {
|
||||
if (!App.configured(this)) {
|
||||
status.setText("Not set up yet: press Menu > Settings.");
|
||||
return;
|
||||
}
|
||||
if (SyncService.running) {
|
||||
status.setText("Syncing…");
|
||||
status.setTextColor(0xFFAAAAAA);
|
||||
return;
|
||||
}
|
||||
long last = App.prefs(this).getLong("lastsync", 0);
|
||||
boolean err = App.prefs(this).getBoolean("error", false);
|
||||
String s = App.get(this, "status");
|
||||
String when = last == 0 ? "never" : App.day(last) + " " + App.time(this, last);
|
||||
if (err) {
|
||||
status.setText("Sync failed: " + s + "\nLast good sync: " + when);
|
||||
status.setTextColor(0xFFFF8A80);
|
||||
} else {
|
||||
status.setText("Synced " + when + (s.length() > 0 ? " · " + s : ""));
|
||||
status.setTextColor(0xFFAAAAAA);
|
||||
}
|
||||
}
|
||||
|
||||
void reload() {
|
||||
rows.clear();
|
||||
long today = ICal.startOfDay(System.currentTimeMillis());
|
||||
List<Ev> evs = Db.get(this).agenda(today, ICal.addDays(today, App.ahead(this) + 1));
|
||||
long lastDay = -1;
|
||||
for (Ev e : evs) {
|
||||
long d = Math.max(ICal.startOfDay(e.start), today);
|
||||
if (d != lastDay) {
|
||||
rows.add(Long.valueOf(d));
|
||||
lastDay = d;
|
||||
}
|
||||
rows.add(e);
|
||||
}
|
||||
if (rows.isEmpty() && App.configured(this)) rows.add("No upcoming events. Menu > New event, or Menu > Sync.");
|
||||
adapter.notifyDataSetChanged();
|
||||
showStatus();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- menu
|
||||
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(Menu m) {
|
||||
icon(m.add(0, M_SYNC, 0, "Sync"), "ic_menu_refresh");
|
||||
icon(m.add(0, M_NEW, 1, "New event"), "ic_menu_add");
|
||||
icon(m.add(0, M_CALS, 2, "Calendars"), "ic_menu_agenda");
|
||||
icon(m.add(0, M_SETTINGS, 3, "Settings"), "ic_menu_preferences");
|
||||
return true;
|
||||
}
|
||||
|
||||
void icon(MenuItem i, String name) {
|
||||
int id = App.sysDrawable(this, name);
|
||||
if (id != 0) i.setIcon(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case M_SYNC:
|
||||
if (App.configured(this)) App.requestSync(this, true, false);
|
||||
else startActivity(new Intent(this, SettingsActivity.class));
|
||||
return true;
|
||||
case M_NEW:
|
||||
if (writableCalendars().isEmpty()) {
|
||||
App.toast(this, "Sync first: no writable calendar yet.");
|
||||
} else {
|
||||
startActivity(new Intent(this, EditActivity.class));
|
||||
}
|
||||
return true;
|
||||
case M_CALS:
|
||||
startActivity(new Intent(this, CalendarsActivity.class));
|
||||
return true;
|
||||
case M_SETTINGS:
|
||||
startActivity(new Intent(this, SettingsActivity.class));
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
List<Cal> writableCalendars() {
|
||||
List<Cal> out = new ArrayList<Cal>();
|
||||
for (Cal c : Db.get(this).calendarList()) if (c.enabled && c.writable) out.add(c);
|
||||
return out;
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------- details
|
||||
|
||||
void showEvent(final Ev e) {
|
||||
StringBuilder m = new StringBuilder();
|
||||
m.append(App.day(e.start)).append("\n").append(App.when(this, e));
|
||||
if (e.location.length() > 0) m.append("\n\n").append(e.location);
|
||||
if (e.descr.length() > 0) m.append("\n\n").append(e.descr);
|
||||
Cal cal = Db.get(this).calendars().get(e.cal);
|
||||
if (cal != null) m.append("\n\nCalendar: ").append(cal.name);
|
||||
if (e.recurring) m.append("\n\nRepeating event: edit the series in Nextcloud. Delete removes the whole series.");
|
||||
AlertDialog.Builder b = new AlertDialog.Builder(this)
|
||||
.setTitle(e.title.length() > 0 ? e.title : "(no title)")
|
||||
.setMessage(m.toString())
|
||||
.setNegativeButton("Close", null);
|
||||
if (e.writable && !e.recurring) {
|
||||
b.setPositiveButton("Edit", new DialogInterface.OnClickListener() {
|
||||
public void onClick(DialogInterface d, int w) {
|
||||
Intent i = new Intent(MainActivity.this, EditActivity.class);
|
||||
i.putExtra("id", e.id);
|
||||
startActivity(i);
|
||||
}
|
||||
});
|
||||
}
|
||||
if (e.writable) {
|
||||
b.setNeutralButton("Delete", new DialogInterface.OnClickListener() {
|
||||
public void onClick(DialogInterface d, int w) {
|
||||
confirmDelete(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
b.show();
|
||||
}
|
||||
|
||||
void confirmDelete(final Ev e) {
|
||||
new AlertDialog.Builder(this)
|
||||
.setTitle("Delete event?")
|
||||
.setMessage(e.recurring ? "This deletes EVERY occurrence of \"" + e.title + "\"." : "\"" + e.title + "\" will be deleted on the server.")
|
||||
.setPositiveButton("Delete", new DialogInterface.OnClickListener() {
|
||||
public void onClick(DialogInterface d, int w) {
|
||||
new DeleteTask(e).execute();
|
||||
}
|
||||
})
|
||||
.setNegativeButton("Cancel", null)
|
||||
.show();
|
||||
}
|
||||
|
||||
class DeleteTask extends AsyncTask<Void, Void, Throwable> {
|
||||
final Ev e;
|
||||
ProgressDialog pd;
|
||||
|
||||
DeleteTask(Ev e) {
|
||||
this.e = e;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPreExecute() {
|
||||
pd = ProgressDialog.show(MainActivity.this, null, "Deleting…");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Throwable doInBackground(Void... v) {
|
||||
Dav d = null;
|
||||
try {
|
||||
d = App.dav(MainActivity.this);
|
||||
d.delete(e.href, e.etag);
|
||||
return null;
|
||||
} catch (Throwable t) {
|
||||
return t;
|
||||
} finally {
|
||||
if (d != null) d.close();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(Throwable t) {
|
||||
try {
|
||||
pd.dismiss();
|
||||
} catch (RuntimeException ignored) {
|
||||
}
|
||||
if (t != null) {
|
||||
new AlertDialog.Builder(MainActivity.this).setTitle("Delete failed")
|
||||
.setMessage(App.describe(t)).setPositiveButton("OK", null).show();
|
||||
return;
|
||||
}
|
||||
App.requestSync(MainActivity.this, true, false);
|
||||
for (int i = rows.size() - 1; i >= 0; i--) {
|
||||
Object o = rows.get(i);
|
||||
if (o instanceof Ev && ((Ev) o).href.equals(e.href)) rows.remove(i);
|
||||
}
|
||||
adapter.notifyDataSetChanged();
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------- adapter
|
||||
|
||||
static class Holder {
|
||||
View bar;
|
||||
TextView time, title, where;
|
||||
}
|
||||
|
||||
class Agenda extends BaseAdapter {
|
||||
public int getCount() {
|
||||
return rows.size();
|
||||
}
|
||||
|
||||
public Object getItem(int p) {
|
||||
return rows.get(p);
|
||||
}
|
||||
|
||||
public long getItemId(int p) {
|
||||
return p;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getViewTypeCount() {
|
||||
return 2;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemViewType(int p) {
|
||||
return rows.get(p) instanceof Ev ? 1 : 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean areAllItemsEnabled() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEnabled(int p) {
|
||||
return rows.get(p) instanceof Ev;
|
||||
}
|
||||
|
||||
public View getView(int p, View convert, ViewGroup parent) {
|
||||
Object o = rows.get(p);
|
||||
Context c = MainActivity.this;
|
||||
if (!(o instanceof Ev)) {
|
||||
TextView h = convert instanceof TextView ? (TextView) convert : App.text(c, "", 13, 0xFFFFFFFF);
|
||||
int pad = App.dp(c, 6);
|
||||
h.setPadding(pad * 2, pad, pad, pad);
|
||||
h.setBackgroundColor(0xFF2E3B44);
|
||||
h.setText(o instanceof Long ? App.dayHeader(((Long) o).longValue()) : String.valueOf(o));
|
||||
return h;
|
||||
}
|
||||
Ev e = (Ev) o;
|
||||
Holder hd;
|
||||
View v = convert;
|
||||
if (v == null || !(v.getTag() instanceof Holder)) {
|
||||
hd = new Holder();
|
||||
LinearLayout row = new LinearLayout(c);
|
||||
row.setOrientation(LinearLayout.HORIZONTAL);
|
||||
hd.bar = new View(c);
|
||||
row.addView(hd.bar, new LinearLayout.LayoutParams(App.dp(c, 5), -1));
|
||||
LinearLayout col = App.vbox(c);
|
||||
int pad = App.dp(c, 6);
|
||||
col.setPadding(pad * 2, pad, pad, pad);
|
||||
hd.time = App.text(c, "", 12, 0xFFB0BEC5);
|
||||
hd.title = App.text(c, "", 16, 0xFFFFFFFF);
|
||||
hd.where = App.text(c, "", 12, 0xFF90A4AE);
|
||||
col.addView(hd.time);
|
||||
col.addView(hd.title);
|
||||
col.addView(hd.where);
|
||||
row.addView(col, new LinearLayout.LayoutParams(-1, -2));
|
||||
row.setTag(hd);
|
||||
v = row;
|
||||
} else {
|
||||
hd = (Holder) v.getTag();
|
||||
}
|
||||
hd.bar.setBackgroundColor(e.color);
|
||||
hd.time.setText(App.when(c, e) + (e.recurring ? " (repeats)" : ""));
|
||||
hd.title.setText(e.title.length() > 0 ? e.title : "(no title)");
|
||||
hd.where.setText(e.location);
|
||||
hd.where.setVisibility(e.location.length() > 0 ? View.VISIBLE : View.GONE);
|
||||
return v;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package org.gingercal;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import android.app.AlarmManager;
|
||||
import android.app.Notification;
|
||||
import android.app.NotificationManager;
|
||||
import android.app.PendingIntent;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
|
||||
import org.gingercal.Model.Ev;
|
||||
|
||||
/** Local notifications before timed events, driven from the cached agenda. */
|
||||
public final class Reminders {
|
||||
private Reminders() {}
|
||||
|
||||
static PendingIntent alarmIntent(Context c) {
|
||||
Intent i = new Intent(c, AlarmReceiver.class);
|
||||
i.setAction(AlarmReceiver.REMIND);
|
||||
return PendingIntent.getBroadcast(c, 2, i, 0x08000000 /* FLAG_UPDATE_CURRENT */);
|
||||
}
|
||||
|
||||
public static void schedule(Context c) {
|
||||
AlarmManager am = (AlarmManager) c.getSystemService("alarm");
|
||||
PendingIntent pi = alarmIntent(c);
|
||||
am.cancel(pi);
|
||||
int m = App.remind(c);
|
||||
if (m < 0) return;
|
||||
long now = System.currentTimeMillis();
|
||||
long notified = App.prefs(c).getLong("notified", 0);
|
||||
long next = Db.get(c).nextStartAfter(Math.max(notified, now - 60000L));
|
||||
if (next < 0) return;
|
||||
am.set(0 /* RTC_WAKEUP */, Math.max(now + 1000, next - m * ICal.MIN), pi);
|
||||
}
|
||||
|
||||
public static void fire(Context c) {
|
||||
int m = App.remind(c);
|
||||
if (m < 0) return;
|
||||
long now = System.currentTimeMillis();
|
||||
long notified = App.prefs(c).getLong("notified", 0);
|
||||
// never nag about things that started more than 30 minutes ago
|
||||
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");
|
||||
for (Ev e : due) {
|
||||
Intent open = new Intent(c, MainActivity.class);
|
||||
open.setFlags(0x10000000 /* FLAG_ACTIVITY_NEW_TASK */);
|
||||
PendingIntent pi = PendingIntent.getActivity(c, 0, open, 0);
|
||||
Notification n = new Notification(icon, e.title, e.start);
|
||||
String body = App.when(c, e) + (e.location.length() > 0 ? " · " + e.location : "");
|
||||
n.setLatestEventInfo(c, e.title.length() > 0 ? e.title : "(no title)", body, pi);
|
||||
n.flags |= 16; // FLAG_AUTO_CANCEL
|
||||
n.defaults = -1; // DEFAULT_ALL: sound, vibrate, lights
|
||||
nm.notify((e.href + e.start).hashCode(), n);
|
||||
if (e.start > newest) newest = e.start;
|
||||
}
|
||||
if (newest != notified) App.prefs(c).edit().putLong("notified", newest).commit();
|
||||
schedule(c);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,243 @@
|
||||
package org.gingercal;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.AlertDialog;
|
||||
import android.app.ProgressDialog;
|
||||
import android.content.DialogInterface;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
import android.widget.Button;
|
||||
import android.widget.EditText;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.ScrollView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.gingercal.Model.Cal;
|
||||
|
||||
/** Account, TLS pin, sync interval and reminder settings. */
|
||||
public class SettingsActivity extends Activity {
|
||||
EditText url, user, pass, pin, ahead;
|
||||
Button intervalBtn, remindBtn;
|
||||
int interval, remind;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle b) {
|
||||
super.onCreate(b);
|
||||
setTitle("GingerCal settings");
|
||||
LinearLayout f = App.vbox(this);
|
||||
int p = App.dp(this, 8);
|
||||
f.setPadding(p, p, p, p);
|
||||
f.addView(App.text(this, "Nextcloud server. Android 2.3 can't speak TLS 1.2, so use either your "
|
||||
+ "legacy-TLS bridge (https://cloud.example.com:8443) or http:// to the server on your home "
|
||||
+ "Wi-Fi. Use an app password, not your real one.", 12, 0xFFB0BEC5));
|
||||
url = field(f, "Server address", "https://cloud.example.com:8443", 1 | 0x10 /* URI */);
|
||||
user = field(f, "User name", "", 1 | 0x80000 /* no suggestions */);
|
||||
pass = field(f, "App password", "", 1 | 0x80 /* password */);
|
||||
pin = field(f, "Pinned certificate SHA-256 (filled in for you)", "optional", 1 | 0x80000);
|
||||
ahead = field(f, "Days ahead to sync", "60", 2 /* number */);
|
||||
intervalBtn = new Button(this);
|
||||
f.addView(intervalBtn, new LinearLayout.LayoutParams(-1, -2));
|
||||
remindBtn = new Button(this);
|
||||
f.addView(remindBtn, new LinearLayout.LayoutParams(-1, -2));
|
||||
Button test = new Button(this);
|
||||
test.setText("Save and test connection");
|
||||
f.addView(test, new LinearLayout.LayoutParams(-1, -2));
|
||||
Button resync = new Button(this);
|
||||
resync.setText("Re-download everything");
|
||||
f.addView(resync, new LinearLayout.LayoutParams(-1, -2));
|
||||
ScrollView sv = new ScrollView(this);
|
||||
sv.addView(f);
|
||||
setContentView(sv);
|
||||
|
||||
url.setText(App.get(this, "url"));
|
||||
user.setText(App.get(this, "user"));
|
||||
pass.setText(App.get(this, "pass"));
|
||||
pin.setText(App.get(this, "pin"));
|
||||
ahead.setText(String.valueOf(App.ahead(this)));
|
||||
interval = App.interval(this);
|
||||
remind = App.remind(this);
|
||||
labels();
|
||||
|
||||
intervalBtn.setOnClickListener(new View.OnClickListener() {
|
||||
public void onClick(View v) {
|
||||
final String[] l = new String[App.INTERVALS.length];
|
||||
for (int i = 0; i < l.length; i++) l[i] = intervalLabel(App.INTERVALS[i]);
|
||||
new AlertDialog.Builder(SettingsActivity.this).setTitle("Background sync")
|
||||
.setItems(l, new DialogInterface.OnClickListener() {
|
||||
public void onClick(DialogInterface d, int w) {
|
||||
interval = App.INTERVALS[w];
|
||||
labels();
|
||||
}
|
||||
}).show();
|
||||
}
|
||||
});
|
||||
remindBtn.setOnClickListener(new View.OnClickListener() {
|
||||
public void onClick(View v) {
|
||||
final int[] opts = {-1, 0, 5, 10, 15, 30, 60};
|
||||
String[] l = new String[opts.length];
|
||||
for (int i = 0; i < l.length; i++) l[i] = opts[i] < 0 ? "Off" : App.alarmLabel(opts[i]);
|
||||
new AlertDialog.Builder(SettingsActivity.this).setTitle("Phone notification")
|
||||
.setItems(l, new DialogInterface.OnClickListener() {
|
||||
public void onClick(DialogInterface d, int w) {
|
||||
remind = opts[w];
|
||||
labels();
|
||||
}
|
||||
}).show();
|
||||
}
|
||||
});
|
||||
test.setOnClickListener(new View.OnClickListener() {
|
||||
public void onClick(View v) {
|
||||
confirmAndTest();
|
||||
}
|
||||
});
|
||||
resync.setOnClickListener(new View.OnClickListener() {
|
||||
public void onClick(View v) {
|
||||
save();
|
||||
Db.get(SettingsActivity.this).forgetSyncState();
|
||||
App.requestSync(SettingsActivity.this, true, true);
|
||||
App.toast(SettingsActivity.this, "Re-downloading in the background");
|
||||
finish();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
EditText field(LinearLayout f, String label, String hint, int type) {
|
||||
TextView l = App.text(this, label, 13, 0xFFFFFFFF);
|
||||
l.setPadding(App.dp(this, 4), App.dp(this, 8), 0, 0);
|
||||
f.addView(l);
|
||||
EditText e = new EditText(this);
|
||||
e.setHint(hint);
|
||||
e.setSingleLine(true);
|
||||
e.setInputType(type);
|
||||
f.addView(e, new LinearLayout.LayoutParams(-1, -2));
|
||||
return e;
|
||||
}
|
||||
|
||||
static String intervalLabel(int h) {
|
||||
return h == 0 ? "Manual only" : h == 1 ? "Every hour" : "Every " + h + " hours";
|
||||
}
|
||||
|
||||
void labels() {
|
||||
intervalBtn.setText("Background sync: " + intervalLabel(interval));
|
||||
remindBtn.setText("Notify: " + (remind < 0 ? "off" : App.alarmLabel(remind)));
|
||||
}
|
||||
|
||||
static String txt(TextView t) {
|
||||
return t.getText().toString().trim();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPause() {
|
||||
super.onPause();
|
||||
if (txt(url).length() > 0) save();
|
||||
}
|
||||
|
||||
void save() {
|
||||
String oldAccount = App.get(this, "url") + "|" + App.get(this, "user");
|
||||
int days = 60;
|
||||
try {
|
||||
days = Math.max(7, Math.min(365, Integer.parseInt(txt(ahead))));
|
||||
} catch (NumberFormatException ignored) {
|
||||
}
|
||||
String u = txt(url);
|
||||
if (u.length() > 0) u = Dav.normalize(u);
|
||||
App.prefs(this).edit()
|
||||
.putString("url", u)
|
||||
.putString("user", txt(user))
|
||||
.putString("pass", pass.getText().toString())
|
||||
.putString("pin", Tls.normPin(txt(pin)).length() == 64 ? txt(pin) : "")
|
||||
.putInt("ahead", days)
|
||||
.putInt("interval", interval)
|
||||
.putInt("remind", remind)
|
||||
.commit();
|
||||
if (!oldAccount.equals(u + "|" + txt(user))) {
|
||||
Db.get(this).clearAll();
|
||||
App.prefs(this).edit().putLong("lastsync", 0).putString("status", "").commit();
|
||||
}
|
||||
App.scheduleSync(this);
|
||||
Reminders.schedule(this);
|
||||
}
|
||||
|
||||
void confirmAndTest() {
|
||||
String u = txt(url);
|
||||
if (u.length() == 0 || txt(user).length() == 0) {
|
||||
App.toast(this, "Server address and user name are required.");
|
||||
return;
|
||||
}
|
||||
if (Dav.normalize(u).startsWith("http://") && !App.isLan(u)) {
|
||||
new AlertDialog.Builder(this).setTitle("Unencrypted connection")
|
||||
.setMessage("http:// sends your password in clear text over the internet. Use the legacy-TLS "
|
||||
+ "bridge instead, or only use http:// to a LAN address.")
|
||||
.setPositiveButton("Use anyway", new DialogInterface.OnClickListener() {
|
||||
public void onClick(DialogInterface d, int w) {
|
||||
save();
|
||||
new TestTask().execute();
|
||||
}
|
||||
})
|
||||
.setNegativeButton("Cancel", null).show();
|
||||
return;
|
||||
}
|
||||
save();
|
||||
new TestTask().execute();
|
||||
}
|
||||
|
||||
class TestTask extends AsyncTask<Void, Void, Object> {
|
||||
ProgressDialog pd;
|
||||
|
||||
@Override
|
||||
protected void onPreExecute() {
|
||||
pd = ProgressDialog.show(SettingsActivity.this, null, "Connecting…");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Object doInBackground(Void... v) {
|
||||
Dav d = null;
|
||||
try {
|
||||
d = App.dav(SettingsActivity.this);
|
||||
return d.calendars(d.calendarHome());
|
||||
} catch (Throwable t) {
|
||||
return t;
|
||||
} finally {
|
||||
if (d != null) d.close();
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
protected void onPostExecute(Object r) {
|
||||
try {
|
||||
pd.dismiss();
|
||||
} catch (RuntimeException ignored) {
|
||||
}
|
||||
if (r instanceof DavException.UntrustedCert) {
|
||||
App.askTrust(SettingsActivity.this, (DavException.UntrustedCert) r, new App.Trusted() {
|
||||
public void trusted(String fp) {
|
||||
pin.setText(fp);
|
||||
save();
|
||||
new TestTask().execute();
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (r instanceof Throwable) {
|
||||
new AlertDialog.Builder(SettingsActivity.this).setTitle("Connection failed")
|
||||
.setMessage(App.describe((Throwable) r)).setPositiveButton("OK", null).show();
|
||||
return;
|
||||
}
|
||||
List<Cal> cals = (List<Cal>) r;
|
||||
StringBuilder s = new StringBuilder("Connected. Found " + cals.size() + " calendar(s):\n");
|
||||
for (Cal c : cals) s.append("\n• ").append(c.name).append(c.writable ? "" : " (read-only)");
|
||||
new AlertDialog.Builder(SettingsActivity.this).setTitle("It works")
|
||||
.setMessage(s.toString())
|
||||
.setPositiveButton("Sync now", new DialogInterface.OnClickListener() {
|
||||
public void onClick(DialogInterface d, int w) {
|
||||
App.requestSync(SettingsActivity.this, true, false);
|
||||
finish();
|
||||
}
|
||||
}).show();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package org.gingercal;
|
||||
|
||||
import android.app.IntentService;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
|
||||
/** Runs a sync on a worker thread and broadcasts the outcome to whatever screen is open. */
|
||||
public class SyncService extends IntentService {
|
||||
public static final String STATE = "org.gingercal.SYNC_STATE";
|
||||
public static volatile boolean running;
|
||||
|
||||
public SyncService() {
|
||||
super("GingerCalSync");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onHandleIntent(Intent intent) {
|
||||
boolean manual = intent != null && intent.getBooleanExtra("manual", false);
|
||||
boolean force = intent != null && intent.getBooleanExtra("force", false);
|
||||
SharedPreferences p = App.prefs(this);
|
||||
Dav dav = null;
|
||||
try {
|
||||
if (!App.configured(this)) return;
|
||||
if (!manual && !App.online(this)) return;
|
||||
running = true;
|
||||
send("start", null, null);
|
||||
dav = App.dav(this);
|
||||
long now = System.currentTimeMillis();
|
||||
Syncer.Result r = Syncer.sync(dav, Db.get(this), now, 1, App.ahead(this), force);
|
||||
p.edit().putLong("lastsync", now).putString("status", r.summary()).putBoolean("error", false).commit();
|
||||
send("done", r.summary(), null);
|
||||
} catch (DavException.UntrustedCert u) {
|
||||
p.edit().putString("status", u.getMessage()).putBoolean("error", true).commit();
|
||||
send("error", u.getMessage(), u);
|
||||
} catch (Throwable t) {
|
||||
String msg = App.describe(t);
|
||||
p.edit().putString("status", msg).putBoolean("error", true).commit();
|
||||
send("error", msg, null);
|
||||
} finally {
|
||||
running = false;
|
||||
if (dav != null) dav.close();
|
||||
try {
|
||||
Reminders.schedule(this);
|
||||
} catch (RuntimeException ignored) {
|
||||
}
|
||||
AlarmReceiver.release();
|
||||
}
|
||||
}
|
||||
|
||||
void send(String state, String msg, DavException.UntrustedCert u) {
|
||||
Intent b = new Intent(STATE);
|
||||
b.setPackage(getPackageName());
|
||||
b.putExtra("state", state);
|
||||
if (msg != null) b.putExtra("msg", msg);
|
||||
if (u != null) {
|
||||
b.putExtra("fp", u.fingerprint);
|
||||
b.putExtra("subject", u.subject);
|
||||
b.putExtra("issuer", u.issuer);
|
||||
b.putExtra("expires", u.expires);
|
||||
b.putExtra("mismatch", u.pinMismatch);
|
||||
}
|
||||
sendBroadcast(b);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user