GingerCal 1.3: month view, Galician interface, Nginx Proxy Manager bridge option

This commit is contained in:
xocko committed 2026-10-15 17:42:46 +02:00
1 parent f44ecf4871
commit 42c3d6da91
39 files changed
+1409 -220

No files matched your search

+28 -28
View File
@@ -32,6 +32,9 @@ import org.gingercal.Model.Ev;
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};
@@ -121,14 +124,14 @@ public final class App {
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.";
return L.f("Can't resolve %s. No connection, or a typo in the server address.", t.getMessage());
}
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;
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) {
@@ -141,22 +144,19 @@ public final class App {
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.";
? 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 ? "Certificate changed!" : "Trust this certificate?")
.setTitle(u.pinMismatch ? L.t("Certificate changed!") : L.t("Trust this certificate?"))
.setMessage(msg)
.setPositiveButton("Trust", new DialogInterface.OnClickListener() {
.setPositiveButton(L.t("Trust"), new DialogInterface.OnClickListener() {
public void onClick(DialogInterface d, int w) {
cb.trusted(u.fingerprint);
}
})
.setNegativeButton("Cancel", null)
.setNegativeButton(L.t("Cancel"), null)
.show();
}
@@ -164,12 +164,12 @@ public final class App {
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));
return L.df(p).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));
return L.df(thisYear ? "EEE d MMM" : "EEE d MMM yyyy").format(new Date(t));
}
static int year(long t) {
@@ -180,16 +180,16 @@ public final class App {
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);
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 "All day";
return "All day, until " + day(lastDay);
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;
@@ -200,11 +200,11 @@ public final class App {
}
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";
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
+6 -6
View File
@@ -24,13 +24,13 @@ public class CalendarsActivity extends Activity {
@Override
protected void onCreate(Bundle b) {
super.onCreate(b);
setTitle("Calendars");
setTitle(L.t("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));
f.addView(App.text(this, L.t("No calendars yet. Set up the account in Settings and sync."), 14, 0xFFFFFFFF));
}
for (Cal c : cals) {
LinearLayout row = new LinearLayout(this);
@@ -41,7 +41,7 @@ public class CalendarsActivity extends Activity {
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.setText(c.name + (c.writable ? "" : " " + L.t("(read-only)")));
cb.setChecked(c.enabled);
row.addView(cb, new LinearLayout.LayoutParams(-1, -2));
boxes.add(cb);
@@ -62,9 +62,9 @@ public class CalendarsActivity extends Activity {
void label() {
String def = App.get(this, "defcal");
String name = "first writable";
String name = L.t("first writable");
for (Cal c : cals) if (c.href.equals(def)) name = c.name;
defBtn.setText("New events go to: " + name);
defBtn.setText(L.f("New events go to: %s", name));
}
void pickDefault() {
@@ -72,7 +72,7 @@ public class CalendarsActivity extends Activity {
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")
new AlertDialog.Builder(this).setTitle(L.t("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();
+68 -27
View File
@@ -10,7 +10,9 @@ import android.app.AlertDialog;
import android.app.DatePickerDialog;
import android.app.ProgressDialog;
import android.app.TimePickerDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.os.AsyncTask;
import android.os.Bundle;
import android.view.View;
@@ -41,39 +43,51 @@ public class EditActivity extends Activity {
@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");
// Events come by value: month view can show events fetched live that aren't in the DB.
String href = getIntent().getStringExtra("href");
if (href != null) {
editing = new Ev();
editing.href = href;
editing.cal = str("cal");
editing.etag = str("etag");
editing.title = str("title");
editing.location = str("location");
editing.descr = str("descr");
editing.start = getIntent().getLongExtra("start", 0);
editing.end = getIntent().getLongExtra("end", 0);
editing.allDay = getIntent().getBooleanExtra("allDay", false);
}
setTitle(editing == null ? L.t("New event") : L.t("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);
title = field(f, L.t("Title"), false);
allDay = new CheckBox(this);
allDay.setText("All day");
allDay.setText(L.t("All day"));
allDay.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
refresh();
}
});
f.addView(allDay);
f.addView(label("Starts"));
f.addView(label(L.t("Starts")));
startDate = new Button(this);
startTime = new Button(this);
f.addView(pair(startDate, startTime));
f.addView(label("Ends"));
f.addView(label(L.t("Ends")));
endDate = new Button(this);
endTime = new Button(this);
f.addView(pair(endDate, endTime));
location = field(f, "Location", false);
notes = field(f, "Notes", true);
location = field(f, L.t("Location"), false);
notes = field(f, L.t("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");
save.setText(L.t("Save"));
f.addView(save, new LinearLayout.LayoutParams(-1, -2));
ScrollView sv = new ScrollView(this);
sv.addView(f);
@@ -117,7 +131,15 @@ public class EditActivity extends Activity {
if (editing == null) {
long now = System.currentTimeMillis();
start.setTimeInMillis(now - now % ICal.HOUR + ICal.HOUR);
long day = getIntent().getLongExtra("day", -1);
if (day >= 0 && day != ICal.startOfDay(now)) {
start.setTimeInMillis(day); // a day picked in the month view: 09:00 there
start.set(Calendar.HOUR_OF_DAY, 9);
} else {
start.setTimeInMillis(now); // next full hour
start.add(Calendar.HOUR_OF_DAY, 1);
}
start.set(Calendar.MINUTE, 0);
start.set(Calendar.SECOND, 0);
start.set(Calendar.MILLISECOND, 0);
end.setTimeInMillis(start.getTimeInMillis() + ICal.HOUR);
@@ -134,6 +156,25 @@ public class EditActivity extends Activity {
}
}
String str(String k) {
String v = getIntent().getStringExtra(k);
return v == null ? "" : v;
}
static Intent editIntent(Context c, Ev e) {
Intent i = new Intent(c, EditActivity.class);
i.putExtra("href", e.href);
i.putExtra("cal", e.cal);
i.putExtra("etag", e.etag);
i.putExtra("title", e.title);
i.putExtra("location", e.location);
i.putExtra("descr", e.descr);
i.putExtra("start", e.start);
i.putExtra("end", e.end);
i.putExtra("allDay", e.allDay);
return i;
}
EditText field(LinearLayout f, String hint, boolean multi) {
EditText e = new EditText(this);
e.setHint(hint);
@@ -181,10 +222,10 @@ public class EditActivity extends Activity {
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));
alarmBtn.setText(L.f("Alarm: %s", App.alarmLabel(alarm)));
String name = "?";
for (Cal c : Db.get(this).calendarList()) if (c.href.equals(calHref)) name = c.name;
calBtn.setText("Calendar: " + name);
calBtn.setText(L.f("Calendar: %s", name));
}
void pickDate(final Calendar c, final boolean isStart) {
@@ -214,7 +255,7 @@ public class EditActivity extends Activity {
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)")
new AlertDialog.Builder(this).setTitle(L.t("Alarm (saved in the event)"))
.setItems(labels, new DialogInterface.OnClickListener() {
public void onClick(DialogInterface d, int w) {
alarm = App.ALARMS[w];
@@ -226,7 +267,7 @@ public class EditActivity extends Activity {
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")
new AlertDialog.Builder(this).setTitle(L.t("Calendar"))
.setItems(names, new DialogInterface.OnClickListener() {
public void onClick(DialogInterface d, int w) {
calHref = cals.get(w).href;
@@ -250,15 +291,15 @@ public class EditActivity extends Activity {
e = end.getTimeInMillis() - end.getTimeInMillis() % 60000L;
}
if (e < s || (ad && e <= s)) {
App.toast(this, "The end is before the start.");
App.toast(this, L.t("The end is before the start."));
return;
}
if (calHref == null) {
App.toast(this, "No writable calendar. Sync first.");
App.toast(this, L.t("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();
new SaveTask(t.length() > 0 ? t : L.t("(no title)"), txt(location), notes.getText().toString().trim(), s, e, ad).execute();
}
// ----------------------------------------------------------- network
@@ -268,7 +309,7 @@ public class EditActivity extends Activity {
@Override
protected void onPreExecute() {
pd = ProgressDialog.show(EditActivity.this, null, "Loading…");
pd = ProgressDialog.show(EditActivity.this, null, L.t("Loading\u2026"));
}
@Override
@@ -291,19 +332,19 @@ public class EditActivity extends Activity {
} catch (RuntimeException ignored) {
}
if (r instanceof Throwable) {
fail("Can't load event", App.describe((Throwable) r));
fail(L.t("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.");
fail(L.t("Repeating event"), L.t("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.");
fail(L.t("Can't edit"), L.t("The server returned no event data."));
return;
}
ICal.VEvent v = vs.get(0);
@@ -316,7 +357,7 @@ public class EditActivity extends Activity {
void fail(String t, String msg) {
new AlertDialog.Builder(this).setTitle(t).setMessage(msg)
.setPositiveButton("OK", new DialogInterface.OnClickListener() {
.setPositiveButton(L.t("OK"), new DialogInterface.OnClickListener() {
public void onClick(DialogInterface d, int w) {
finish();
}
@@ -340,7 +381,7 @@ public class EditActivity extends Activity {
@Override
protected void onPreExecute() {
pd = ProgressDialog.show(EditActivity.this, null, "Saving…");
pd = ProgressDialog.show(EditActivity.this, null, L.t("Saving\u2026"));
}
@Override
@@ -370,13 +411,13 @@ public class EditActivity extends Activity {
} catch (RuntimeException ignored) {
}
if (x != null) {
new AlertDialog.Builder(EditActivity.this).setTitle("Not saved")
.setMessage(App.describe(x)).setPositiveButton("OK", null).show();
new AlertDialog.Builder(EditActivity.this).setTitle(L.t("Not saved"))
.setMessage(App.describe(x)).setPositiveButton(L.t("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");
App.toast(EditActivity.this, L.t("Saved"));
finish();
}
}
+12
View File
@@ -0,0 +1,12 @@
package org.gingercal;
import android.app.Application;
/** Runs before any screen, service or alarm: sets the interface language (Galician by default). */
public class GingerApp extends Application {
@Override
public void onCreate() {
super.onCreate();
L.gl = !"en".equals(getSharedPreferences("gingercal", 0).getString("lang", "gl"));
}
}
+387 -71
View File
@@ -1,7 +1,12 @@
package org.gingercal;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import android.app.Activity;
import android.app.AlertDialog;
@@ -11,6 +16,7 @@ import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.res.Configuration;
import android.os.AsyncTask;
import android.os.Bundle;
import android.view.Menu;
@@ -26,15 +32,23 @@ 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;
/** Main screen: a month grid with the selected day's events, or the upcoming-events agenda. */
public class MainActivity extends Activity implements MonthView.Listener {
static final int M_SYNC = 1, M_NEW = 2, M_CALS = 3, M_SETTINGS = 4, M_VIEW = 5, M_TODAY = 6;
TextView status;
ListView list;
final List<Object> rows = new ArrayList<Object>();
final Agenda adapter = new Agenda();
boolean askedSetup;
LinearLayout root, body;
TextView status, monthTitle;
ListView agendaList, dayList;
MonthView monthView;
final List<Object> agendaRows = new ArrayList<Object>(), dayRows = new ArrayList<Object>();
final Rows agendaAdapter = new Rows(agendaRows), dayAdapter = new Rows(dayRows);
boolean askedSetup, monthMode, uiGl;
long selectedDay;
List<Ev> monthEvents = new ArrayList<Ev>();
/** Months outside the synced window, fetched from the server on demand (key: grid start). */
final Map<Long, List<Ev>> fetched = new HashMap<Long, List<Ev>>();
long fetching = -1;
String fetchError;
final BroadcastReceiver rx = new BroadcastReceiver() {
@Override
@@ -44,7 +58,7 @@ public class MainActivity extends Activity {
showStatus();
return;
}
reload();
reload(true);
String fp = i.getStringExtra("fp");
if (fp != null) {
DavException.UntrustedCert u = new DavException.UntrustedCert(fp, i.getStringExtra("subject"),
@@ -62,29 +76,115 @@ public class MainActivity extends Activity {
@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);
requestWindowFeature(1 /* Window.FEATURE_NO_TITLE: every pixel counts at 240x320 */);
uiGl = L.gl;
monthMode = !"agenda".equals(App.get(this, "view"));
selectedDay = ICal.startOfDay(System.currentTimeMillis());
root = App.vbox(this);
status = App.text(this, "", 11, 0xFFAAAAAA);
int p = App.dp(this, 4);
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() {
agendaList = list(agendaAdapter, agendaRows);
dayList = list(dayAdapter, dayRows);
monthView = new MonthView(this);
monthView.listener = this;
monthView.setMonth(selectedDay);
monthView.setSelected(selectedDay);
monthTitle = App.text(this, "", 15, 0xFFFFFFFF);
monthTitle.setGravity(17 /* Gravity.CENTER */);
monthTitle.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
goToday();
}
});
buildBody();
setContentView(root);
App.scheduleSync(this);
}
ListView list(Rows adapter, final List<Object> rows) {
ListView l = new ListView(this);
l.setAdapter(adapter);
l.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);
return l;
}
static void detach(View v) {
// Views can only have one parent; LinearLayout is the only container we use.
Object parent = v.getParent();
if (parent instanceof LinearLayout) ((LinearLayout) parent).removeView(v);
}
/** (Re)builds the area under the status line for the current mode and orientation. */
void buildBody() {
if (body != null) root.removeView(body);
detach(agendaList);
detach(dayList);
detach(monthView);
detach(monthTitle);
body = App.vbox(this);
if (!monthMode) {
body.addView(agendaList, new LinearLayout.LayoutParams(-1, -1));
} else {
LinearLayout nav = new LinearLayout(this);
nav.setOrientation(LinearLayout.HORIZONTAL);
nav.setBackgroundColor(0xFF1C2329);
nav.addView(navArrow("\u00ab", -1), new LinearLayout.LayoutParams(App.dp(this, 56), App.dp(this, 34)));
nav.addView(monthTitle, new LinearLayout.LayoutParams(0, App.dp(this, 34), 1f));
nav.addView(navArrow("\u00bb", 1), new LinearLayout.LayoutParams(App.dp(this, 56), App.dp(this, 34)));
body.addView(nav, new LinearLayout.LayoutParams(-1, -2));
boolean landscape = getResources().getDisplayMetrics().widthPixels
> getResources().getDisplayMetrics().heightPixels;
if (landscape) {
LinearLayout row = new LinearLayout(this);
row.setOrientation(LinearLayout.HORIZONTAL);
row.addView(monthView, new LinearLayout.LayoutParams(0, -1, 1.25f));
row.addView(dayList, new LinearLayout.LayoutParams(0, -1, 1f));
body.addView(row, new LinearLayout.LayoutParams(-1, -1));
} else {
body.addView(monthView, new LinearLayout.LayoutParams(-1, -2));
body.addView(dayList, new LinearLayout.LayoutParams(-1, -1));
}
}
root.addView(body, new LinearLayout.LayoutParams(-1, -1));
}
TextView navArrow(String label, final int delta) {
TextView b = App.text(this, label, 20, 0xFFD66E2A); // « »: Latin-1, always in the 2.3 font
b.setGravity(17 /* Gravity.CENTER */);
int sel = App.sysDrawable(this, "list_selector_background");
if (sel != 0) b.setBackgroundResource(sel); // press highlight
b.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
monthSwiped(delta);
}
});
return b;
}
@Override
public void onConfigurationChanged(Configuration c) {
super.onConfigurationChanged(c);
buildBody();
}
@Override
protected void onResume() {
super.onResume();
if (uiGl != L.gl) { // language switched in Settings: rebuild every label
finish();
startActivity(new Intent(this, MainActivity.class));
return;
}
registerReceiver(rx, new IntentFilter(SyncService.STATE));
if (!App.configured(this)) {
showStatus();
@@ -94,7 +194,9 @@ public class MainActivity extends Activity {
}
return;
}
reload();
long now = ICal.startOfDay(System.currentTimeMillis());
if (now != monthView.today) monthView.setMonth(monthView.days[15]); // date rolled over
reload(true);
long last = App.prefs(this).getLong("lastsync", 0);
if (!SyncService.running && System.currentTimeMillis() - last > 15 * ICal.MIN) {
App.requestSync(this, true, false);
@@ -109,74 +211,264 @@ public class MainActivity extends Activity {
void showStatus() {
if (!App.configured(this)) {
status.setText("Not set up yet: press Menu > Settings.");
status.setText(L.t("Not set up yet: press Menu > Settings."));
return;
}
if (SyncService.running) {
status.setText("Syncing…");
status.setText(L.t("Syncing\u2026"));
status.setTextColor(0xFFAAAAAA);
return;
}
if (monthMode && fetching >= 0) {
status.setText(L.f("Loading %s from the server\u2026", monthTitle.getText()));
status.setTextColor(0xFFAAAAAA);
return;
}
if (monthMode && fetchError != null) {
status.setText(L.f("Couldn't load this month: %s", fetchError));
status.setTextColor(0xFFFF8A80);
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);
String when = last == 0 ? L.t("never") : App.day(last) + " " + App.time(this, last);
if (err) {
status.setText("Sync failed: " + s + "\nLast good sync: " + when);
status.setText(L.f("Sync failed: %s\nLast good sync: %s", s, when));
status.setTextColor(0xFFFF8A80);
} else {
status.setText("Synced " + when + (s.length() > 0 ? " · " + s : ""));
status.setText(L.f("Synced %s", when) + (s.length() > 0 ? " \u00b7 " + s : ""));
status.setTextColor(0xFFAAAAAA);
}
}
void reload() {
rows.clear();
void reload(boolean allowFetch) {
if (monthMode) loadMonth(allowFetch);
else loadAgenda();
showStatus();
}
// -------------------------------------------------------------- agenda
void loadAgenda() {
agendaRows.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));
agendaRows.add(Long.valueOf(d));
lastDay = d;
}
rows.add(e);
agendaRows.add(e);
}
if (agendaRows.isEmpty() && App.configured(this)) {
agendaRows.add(L.t("No upcoming events. Menu > New event, or Menu > Sync."));
}
agendaAdapter.notifyDataSetChanged();
}
// --------------------------------------------------------------- month
void loadMonth(boolean allowFetch) {
long gs = monthView.gridStart(), ge = monthView.gridEnd();
monthTitle.setText(L.monthTitle(monthView.days[15]));
long wf = App.prefs(this).getLong("win_from", 0), wt = App.prefs(this).getLong("win_to", 0);
if (wf > 0 && gs >= wf && ge <= wt) {
fetchError = null;
showMonthEvents(Db.get(this).agenda(gs, ge));
} else {
List<Ev> cached = fetched.get(Long.valueOf(gs));
showMonthEvents(cached != null ? cached : Db.get(this).agenda(gs, ge));
if (allowFetch && fetching != gs && App.configured(this)) new FetchTask(gs, ge).execute();
}
}
void showMonthEvents(List<Ev> evs) {
monthEvents = evs;
Map<Long, List<Integer>> marks = new HashMap<Long, List<Integer>>();
long gs = monthView.gridStart(), ge = monthView.gridEnd();
for (Ev e : evs) {
long d = Math.max(ICal.startOfDay(e.start), gs);
for (int guard = 0; d < ge && guard < 42; guard++) {
if (!(occupies(e, d))) break;
List<Integer> l = marks.get(Long.valueOf(d));
if (l == null) {
l = new ArrayList<Integer>();
marks.put(Long.valueOf(d), l);
}
l.add(Integer.valueOf(e.color));
d = ICal.addDays(d, 1);
}
}
monthView.setMarks(marks);
showDay();
}
static boolean occupies(Ev e, long d) {
return ICal.onDay(e.start, e.end, d);
}
void showDay() {
dayRows.clear();
int n = 0;
for (Ev e : monthEvents) {
if (occupies(e, selectedDay)) {
dayRows.add(e);
n++;
}
}
if (n == 0) dayRows.add(L.f("Nothing on %s. Long-press a day to add an event.", App.day(selectedDay)));
dayAdapter.notifyDataSetChanged();
}
public void daySelected(long day) {
selectedDay = day;
monthView.setSelected(day);
if (!monthView.inMonth(day)) {
monthView.setMonth(day);
reload(true);
} else {
showDay();
}
}
public void dayLongPressed(long day) {
daySelected(day);
newEvent(day);
}
public void monthSwiped(int delta) {
java.util.Calendar c = java.util.Calendar.getInstance();
c.setTimeInMillis(monthView.days[15]);
c.set(java.util.Calendar.DAY_OF_MONTH, 1);
c.add(java.util.Calendar.MONTH, delta);
long first = c.getTimeInMillis();
monthView.setMonth(first);
long today = ICal.startOfDay(System.currentTimeMillis());
selectedDay = monthView.inMonth(today) ? today : ICal.startOfDay(first);
monthView.setSelected(selectedDay);
fetchError = null;
reload(true);
}
void goToday() {
selectedDay = ICal.startOfDay(System.currentTimeMillis());
monthView.setMonth(selectedDay);
monthView.setSelected(selectedDay);
reload(true);
}
/** Loads a month outside the synced window straight from the server. */
class FetchTask extends AsyncTask<Void, Void, Object> {
final long gs, ge;
FetchTask(long gs, long ge) {
this.gs = gs;
this.ge = ge;
}
@Override
protected void onPreExecute() {
fetching = gs;
fetchError = null;
showStatus();
}
@Override
protected Object doInBackground(Void... v) {
Dav d = null;
try {
d = App.dav(MainActivity.this);
List<Ev> out = new ArrayList<Ev>();
for (Cal c : Db.get(MainActivity.this).calendarList()) {
if (!c.enabled) continue;
for (Ev e : d.events(c, gs, ge)) {
e.color = c.color;
e.writable = c.writable;
out.add(e);
}
}
java.util.Collections.sort(out, new java.util.Comparator<Ev>() {
public int compare(Ev a, Ev b) {
return a.start < b.start ? -1 : a.start > b.start ? 1 : 0;
}
});
return out;
} catch (Throwable t) {
return t;
} finally {
if (d != null) d.close();
}
}
@SuppressWarnings("unchecked")
@Override
protected void onPostExecute(Object r) {
if (fetching == gs) fetching = -1;
if (r instanceof Throwable) {
fetchError = App.describe((Throwable) r);
} else {
fetched.put(Long.valueOf(gs), (List<Ev>) r);
if (monthMode && monthView.gridStart() == gs) showMonthEvents((List<Ev>) r);
}
showStatus();
}
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");
icon(m.add(0, M_VIEW, 0, L.t("View")), "ic_menu_month");
icon(m.add(0, M_NEW, 1, L.t("New event")), "ic_menu_add");
icon(m.add(0, M_SYNC, 2, L.t("Sync")), "ic_menu_refresh");
icon(m.add(0, M_TODAY, 3, L.t("Today")), "ic_menu_today");
icon(m.add(0, M_CALS, 4, L.t("Calendars")), "ic_menu_agenda");
icon(m.add(0, M_SETTINGS, 5, L.t("Settings")), "ic_menu_preferences");
return true;
}
@Override
public boolean onPrepareOptionsMenu(Menu m) {
MenuItem v = m.findItem(M_VIEW);
if (v != null) {
v.setTitle(monthMode ? L.t("Agenda") : L.t("Month"));
icon(v, monthMode ? "ic_menu_agenda" : "ic_menu_month");
}
return true;
}
void icon(MenuItem i, String name) {
int id = App.sysDrawable(this, name);
if (id == 0) id = App.sysDrawable(this, "ic_menu_my_calendar");
if (id != 0) i.setIcon(id);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case M_VIEW:
monthMode = !monthMode;
App.prefs(this).edit().putString("view", monthMode ? "month" : "agenda").commit();
buildBody();
reload(true);
return true;
case M_TODAY:
if (monthMode) goToday();
else agendaList.setSelection(0);
return true;
case M_SYNC:
if (App.configured(this)) App.requestSync(this, true, false);
else startActivity(new Intent(this, SettingsActivity.class));
if (App.configured(this)) {
fetched.clear();
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));
}
newEvent(monthMode ? selectedDay : -1);
return true;
case M_CALS:
startActivity(new Intent(this, CalendarsActivity.class));
@@ -188,6 +480,17 @@ public class MainActivity extends Activity {
return false;
}
void newEvent(long day) {
if (writableCalendars().isEmpty()) {
App.toast(this, L.t("Sync first: no writable calendar yet."));
return;
}
Intent i = new Intent(this, EditActivity.class);
if (day >= 0) i.putExtra("day", day);
fetched.clear();
startActivity(i);
}
List<Cal> writableCalendars() {
List<Cal> out = new ArrayList<Cal>();
for (Cal c : Db.get(this).calendarList()) if (c.enabled && c.writable) out.add(c);
@@ -202,23 +505,22 @@ public class MainActivity extends Activity {
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.");
if (cal != null) m.append("\n\n").append(L.f("Calendar: %s", cal.name));
if (e.recurring) m.append("\n\n").append(L.t("Repeating 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)")
.setTitle(e.title.length() > 0 ? e.title : L.t("(no title)"))
.setMessage(m.toString())
.setNegativeButton("Close", null);
.setNegativeButton(L.t("Close"), null);
if (e.writable && !e.recurring) {
b.setPositiveButton("Edit", new DialogInterface.OnClickListener() {
b.setPositiveButton(L.t("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);
fetched.clear();
startActivity(EditActivity.editIntent(MainActivity.this, e));
}
});
}
if (e.writable) {
b.setNeutralButton("Delete", new DialogInterface.OnClickListener() {
b.setNeutralButton(L.t("Delete"), new DialogInterface.OnClickListener() {
public void onClick(DialogInterface d, int w) {
confirmDelete(e);
}
@@ -229,14 +531,14 @@ public class MainActivity extends Activity {
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() {
.setTitle(L.t("Delete event?"))
.setMessage(e.recurring ? L.f("This deletes EVERY occurrence of \"%s\".", e.title) : L.f("\"%s\" will be deleted on the server.", e.title))
.setPositiveButton(L.t("Delete"), new DialogInterface.OnClickListener() {
public void onClick(DialogInterface d, int w) {
new DeleteTask(e).execute();
}
})
.setNegativeButton("Cancel", null)
.setNegativeButton(L.t("Cancel"), null)
.show();
}
@@ -250,7 +552,7 @@ public class MainActivity extends Activity {
@Override
protected void onPreExecute() {
pd = ProgressDialog.show(MainActivity.this, null, "Deleting…");
pd = ProgressDialog.show(MainActivity.this, null, L.t("Deleting\u2026"));
}
@Override
@@ -274,16 +576,21 @@ public class MainActivity extends Activity {
} catch (RuntimeException ignored) {
}
if (t != null) {
new AlertDialog.Builder(MainActivity.this).setTitle("Delete failed")
.setMessage(App.describe(t)).setPositiveButton("OK", null).show();
new AlertDialog.Builder(MainActivity.this).setTitle(L.t("Delete failed"))
.setMessage(App.describe(t)).setPositiveButton(L.t("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);
// Hide it right away; the sync that follows brings the cache in line.
List<Ev> keep = new ArrayList<Ev>();
for (Ev x : monthEvents) if (!x.href.equals(e.href)) keep.add(x);
fetched.clear();
for (int i = agendaRows.size() - 1; i >= 0; i--) {
Object o = agendaRows.get(i);
if (o instanceof Ev && ((Ev) o).href.equals(e.href)) agendaRows.remove(i);
}
adapter.notifyDataSetChanged();
agendaAdapter.notifyDataSetChanged();
if (monthMode) showMonthEvents(keep);
App.requestSync(MainActivity.this, true, false);
}
}
@@ -294,7 +601,14 @@ public class MainActivity extends Activity {
TextView time, title, where;
}
class Agenda extends BaseAdapter {
/** Day headers (Long), events (Ev) and hint lines (String). */
class Rows extends BaseAdapter {
final List<Object> rows;
Rows(List<Object> rows) {
this.rows = rows;
}
public int getCount() {
return rows.size();
}
@@ -332,10 +646,12 @@ public class MainActivity extends Activity {
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);
int pad = App.dp(c, 5);
h.setPadding(pad * 2, pad, pad, pad);
h.setBackgroundColor(0xFF2E3B44);
h.setText(o instanceof Long ? App.dayHeader(((Long) o).longValue()) : String.valueOf(o));
boolean header = o instanceof Long;
h.setBackgroundColor(header ? 0xFF2E3B44 : 0x00000000);
h.setTextColor(header ? 0xFFFFFFFF : 0xFF90A4AE);
h.setText(header ? App.dayHeader(((Long) o).longValue()) : String.valueOf(o));
return h;
}
Ev e = (Ev) o;
@@ -348,7 +664,7 @@ public class MainActivity extends Activity {
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);
int pad = App.dp(c, 5);
col.setPadding(pad * 2, pad, pad, pad);
hd.time = App.text(c, "", 12, 0xFFB0BEC5);
hd.title = App.text(c, "", 16, 0xFFFFFFFF);
@@ -363,8 +679,8 @@ public class MainActivity extends Activity {
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.time.setText(App.when(c, e) + (e.recurring ? " " + L.t("(repeats)") : ""));
hd.title.setText(e.title.length() > 0 ? e.title : L.t("(no title)"));
hd.where.setText(e.location);
hd.where.setVisibility(e.location.length() > 0 ? View.VISIBLE : View.GONE);
return v;
+215
View File
@@ -0,0 +1,215 @@
package org.gingercal;
import java.util.Calendar;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.View;
/**
* Month grid drawn by hand (Android 2.3 has no calendar widget). Six weeks of days, each with
* up to four coloured marks for its events. Tap selects a day, long-press asks to create an
* event on it, a sideways swipe changes month.
*/
public class MonthView extends View {
public interface Listener {
void daySelected(long day);
void dayLongPressed(long day);
void monthSwiped(int delta);
}
static final int BG = 0xFF101418, GRID = 0xFF2A3036, WEEKDAY = 0xFF90A4AE, NUM = 0xFFECEFF1,
OTHER = 0xFF5C666E, TODAY = 0xFFD66E2A, SELECTED = 0xFF2E3B44;
final float density;
final Paint fill = new Paint(1 /* ANTI_ALIAS_FLAG */), text = new Paint(1), line = new Paint();
final GestureDetector gestures;
final String[] weekdays = new String[7];
final long[] days = new long[42];
Listener listener;
int month = -1; // Calendar.MONTH of the displayed month
long selected = -1, today;
Map<Long, List<Integer>> marks = new HashMap<Long, List<Integer>>();
public MonthView(Context c) {
super(c);
density = c.getResources().getDisplayMetrics().density;
line.setColor(GRID);
String[] shortNames = L.symbols().getShortWeekdays(); // index 1 = Sunday
int first = L.firstDayOfWeek();
for (int i = 0; i < 7; i++) {
String n = shortNames[(first - 1 + i) % 7 + 1];
weekdays[i] = n.length() > 3 ? n.substring(0, 3) : n;
}
gestures = new GestureDetector(c, new GestureDetector.SimpleOnGestureListener() {
@Override
public boolean onDown(MotionEvent e) {
return true;
}
@Override
public boolean onSingleTapUp(MotionEvent e) {
long d = dayAt(e.getX(), e.getY());
if (d >= 0 && listener != null) listener.daySelected(d);
return true;
}
@Override
public void onLongPress(MotionEvent e) {
long d = dayAt(e.getX(), e.getY());
if (d >= 0 && listener != null) {
performHapticFeedback(0 /* HapticFeedbackConstants.LONG_PRESS */);
listener.dayLongPressed(d);
}
}
@Override
public boolean onFling(MotionEvent a, MotionEvent b, float vx, float vy) {
if (listener != null && Math.abs(vx) > Math.abs(vy) * 1.5f && Math.abs(vx) > 250 * density) {
listener.monthSwiped(vx < 0 ? 1 : -1);
return true;
}
return false;
}
});
}
/** Shows the month containing `anyDay`, weeks starting on the locale's first weekday. */
public void setMonth(long anyDay) {
Calendar c = Calendar.getInstance();
c.setTimeInMillis(ICal.startOfDay(anyDay));
c.set(Calendar.DAY_OF_MONTH, 1);
month = c.get(Calendar.MONTH);
int back = (c.get(Calendar.DAY_OF_WEEK) - L.firstDayOfWeek() + 7) % 7;
c.add(Calendar.DAY_OF_MONTH, -back);
for (int i = 0; i < 42; i++) {
days[i] = c.getTimeInMillis();
c.add(Calendar.DAY_OF_MONTH, 1);
}
today = ICal.startOfDay(System.currentTimeMillis());
invalidate();
}
public long gridStart() {
return days[0];
}
public long gridEnd() {
return ICal.addDays(days[41], 1);
}
public boolean inMonth(long day) {
Calendar c = Calendar.getInstance();
c.setTimeInMillis(day);
return c.get(Calendar.MONTH) == month;
}
public void setSelected(long day) {
selected = day;
invalidate();
}
public void setMarks(Map<Long, List<Integer>> m) {
marks = m;
invalidate();
}
// -------------------------------------------------------------- layout
float headerH() {
return 16 * density;
}
@Override
protected void onMeasure(int wSpec, int hSpec) {
int w = MeasureSpec.getSize(wSpec);
int h;
if (MeasureSpec.getMode(hSpec) == MeasureSpec.EXACTLY) {
h = MeasureSpec.getSize(hSpec);
} else {
float cell = Math.max(22 * density, Math.min(w / 7f * 0.66f, 44 * density));
h = (int) (headerH() + 6 * cell);
if (MeasureSpec.getMode(hSpec) == MeasureSpec.AT_MOST) h = Math.min(h, MeasureSpec.getSize(hSpec));
}
setMeasuredDimension(w, h);
}
long dayAt(float x, float y) {
float cw = getWidth() / 7f, ch = (getHeight() - headerH()) / 6f;
if (y < headerH() || cw <= 0 || ch <= 0) return -1;
int col = Math.min(6, Math.max(0, (int) (x / cw)));
int row = Math.min(5, Math.max(0, (int) ((y - headerH()) / ch)));
return days[row * 7 + col];
}
@Override
public boolean onTouchEvent(MotionEvent e) {
return gestures.onTouchEvent(e);
}
// ------------------------------------------------------------- drawing
@Override
protected void onDraw(Canvas canvas) {
if (month < 0) return;
float w = getWidth(), cw = w / 7f, top = headerH(), ch = (getHeight() - top) / 6f;
fill.setStyle(Paint.Style.FILL);
fill.setColor(BG);
canvas.drawRect(0, 0, w, getHeight(), fill);
text.setTextAlign(Paint.Align.CENTER);
text.setFakeBoldText(false);
text.setTextSize(11 * density);
text.setColor(WEEKDAY);
for (int i = 0; i < 7; i++) canvas.drawText(weekdays[i], cw * i + cw / 2, top - 4 * density, text);
float numSize = Math.min(14 * density, ch * 0.45f);
float mark = Math.max(2, 3 * density);
for (int i = 0; i < 42; i++) {
float x0 = cw * (i % 7), y0 = top + ch * (i / 7);
long d = days[i];
if (d == selected) {
fill.setColor(SELECTED);
canvas.drawRect(x0, y0, x0 + cw, y0 + ch, fill);
}
boolean isToday = d == today, here = inMonth(d);
Calendar c = Calendar.getInstance();
c.setTimeInMillis(d);
String num = String.valueOf(c.get(Calendar.DAY_OF_MONTH));
text.setTextSize(numSize);
text.setFakeBoldText(isToday);
text.setColor(isToday ? TODAY : here ? NUM : OTHER);
float ty = y0 + ch * 0.12f - text.ascent();
canvas.drawText(num, x0 + cw / 2, ty, text);
if (isToday) {
fill.setStyle(Paint.Style.STROKE);
fill.setStrokeWidth(Math.max(1, 1.5f * density));
fill.setColor(TODAY);
canvas.drawRect(x0 + 1, y0 + 1, x0 + cw - 1, y0 + ch - 1, fill);
fill.setStyle(Paint.Style.FILL);
}
List<Integer> m = marks.get(Long.valueOf(d));
if (m != null && !m.isEmpty()) {
int n = Math.min(4, m.size());
float gap = 2 * density, mw = Math.min(6 * density, (cw - gap * 5) / 4);
float total = n * mw + (n - 1) * gap, mx = x0 + (cw - total) / 2;
float my = Math.min(y0 + ch - mark - 3 * density, ty + text.descent() + 2 * density);
for (int k = 0; k < n; k++) {
fill.setColor(here ? m.get(k).intValue() : (m.get(k).intValue() & 0x00FFFFFF) | 0x77000000);
canvas.drawRect(mx, my, mx + mw, my + mark, fill);
mx += mw + gap;
}
}
}
for (int r = 0; r <= 6; r++) canvas.drawLine(0, top + ch * r, w, top + ch * r, line);
}
}
+1 -1
View File
@@ -51,7 +51,7 @@ public final class Reminders {
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.setLatestEventInfo(c, e.title.length() > 0 ? e.title : L.t("(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);
+51 -33
View File
@@ -6,6 +6,7 @@ import android.app.Activity;
import android.app.AlertDialog;
import android.app.ProgressDialog;
import android.content.DialogInterface;
import android.content.Intent;
import android.os.AsyncTask;
import android.os.Bundle;
import android.view.View;
@@ -20,33 +21,33 @@ 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;
Button intervalBtn, remindBtn, langBtn;
int interval, remind;
@Override
protected void onCreate(Bundle b) {
super.onCreate(b);
setTitle("GingerCal settings");
setTitle(L.t("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 */);
f.addView(App.text(this, L.t("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, L.t("Server address"), "https://cloud.example.com:8443", 1 | 0x10 /* URI */);
user = field(f, L.t("User name"), "", 1 | 0x80000 /* no suggestions */);
pass = field(f, L.t("App password"), "", 1 | 0x80 /* password */);
pin = field(f, L.t("Pinned certificate SHA-256 (filled in for you)"), L.t("optional"), 1 | 0x80000);
ahead = field(f, L.t("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));
langBtn = new Button(this);
f.addView(langBtn, new LinearLayout.LayoutParams(-1, -2));
Button test = new Button(this);
test.setText("Save and test connection");
test.setText(L.t("Save and test connection"));
f.addView(test, new LinearLayout.LayoutParams(-1, -2));
Button resync = new Button(this);
resync.setText("Re-download everything");
resync.setText(L.t("Re-download everything"));
f.addView(resync, new LinearLayout.LayoutParams(-1, -2));
ScrollView sv = new ScrollView(this);
sv.addView(f);
@@ -65,7 +66,7 @@ public class SettingsActivity extends Activity {
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")
new AlertDialog.Builder(SettingsActivity.this).setTitle(L.t("Background sync"))
.setItems(l, new DialogInterface.OnClickListener() {
public void onClick(DialogInterface d, int w) {
interval = App.INTERVALS[w];
@@ -78,8 +79,8 @@ public class SettingsActivity extends Activity {
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")
for (int i = 0; i < l.length; i++) l[i] = opts[i] < 0 ? L.t("Off") : App.alarmLabel(opts[i]);
new AlertDialog.Builder(SettingsActivity.this).setTitle(L.t("Phone notification"))
.setItems(l, new DialogInterface.OnClickListener() {
public void onClick(DialogInterface d, int w) {
remind = opts[w];
@@ -88,6 +89,22 @@ public class SettingsActivity extends Activity {
}).show();
}
});
langBtn.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
new AlertDialog.Builder(SettingsActivity.this).setTitle("Lingua / Language")
.setItems(new String[] {"Galego", "English"}, new DialogInterface.OnClickListener() {
public void onClick(DialogInterface d, int w) {
boolean toGl = w == 0;
if (toGl == L.gl) return;
save();
App.prefs(SettingsActivity.this).edit().putString("lang", toGl ? "gl" : "en").commit();
L.gl = toGl;
finish(); // reopen so every label is redrawn in the new language
startActivity(new Intent(SettingsActivity.this, SettingsActivity.class));
}
}).show();
}
});
test.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
confirmAndTest();
@@ -98,7 +115,7 @@ public class SettingsActivity extends Activity {
save();
Db.get(SettingsActivity.this).forgetSyncState();
App.requestSync(SettingsActivity.this, true, true);
App.toast(SettingsActivity.this, "Re-downloading in the background");
App.toast(SettingsActivity.this, L.t("Re-downloading in the background"));
finish();
}
});
@@ -117,12 +134,13 @@ public class SettingsActivity extends Activity {
}
static String intervalLabel(int h) {
return h == 0 ? "Manual only" : h == 1 ? "Every hour" : "Every " + h + " hours";
return h == 0 ? L.t("Manual only") : h == 1 ? L.t("Every hour") : L.f("Every %d hours", h);
}
void labels() {
intervalBtn.setText("Background sync: " + intervalLabel(interval));
remindBtn.setText("Notify: " + (remind < 0 ? "off" : App.alarmLabel(remind)));
intervalBtn.setText(L.f("Background sync: %s", intervalLabel(interval)));
remindBtn.setText(L.f("Notification: %s", remind < 0 ? L.t("off") : App.alarmLabel(remind)));
langBtn.setText(L.f("Language: %s", L.gl ? "Galego" : "English"));
}
static String txt(TextView t) {
@@ -155,7 +173,8 @@ public class SettingsActivity extends Activity {
.commit();
if (!oldAccount.equals(u + "|" + txt(user))) {
Db.get(this).clearAll();
App.prefs(this).edit().putLong("lastsync", 0).putString("status", "").commit();
App.prefs(this).edit().putLong("lastsync", 0).putLong("win_from", 0).putLong("win_to", 0)
.putString("status", "").commit();
}
App.scheduleSync(this);
Reminders.schedule(this);
@@ -164,20 +183,19 @@ public class SettingsActivity extends Activity {
void confirmAndTest() {
String u = txt(url);
if (u.length() == 0 || txt(user).length() == 0) {
App.toast(this, "Server address and user name are required.");
App.toast(this, L.t("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() {
new AlertDialog.Builder(this).setTitle(L.t("Unencrypted connection"))
.setMessage(L.t("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(L.t("Use anyway"), new DialogInterface.OnClickListener() {
public void onClick(DialogInterface d, int w) {
save();
new TestTask().execute();
}
})
.setNegativeButton("Cancel", null).show();
.setNegativeButton(L.t("Cancel"), null).show();
return;
}
save();
@@ -189,7 +207,7 @@ public class SettingsActivity extends Activity {
@Override
protected void onPreExecute() {
pd = ProgressDialog.show(SettingsActivity.this, null, "Connecting…");
pd = ProgressDialog.show(SettingsActivity.this, null, L.t("Connecting\u2026"));
}
@Override
@@ -223,16 +241,16 @@ public class SettingsActivity extends Activity {
return;
}
if (r instanceof Throwable) {
new AlertDialog.Builder(SettingsActivity.this).setTitle("Connection failed")
.setMessage(App.describe((Throwable) r)).setPositiveButton("OK", null).show();
new AlertDialog.Builder(SettingsActivity.this).setTitle(L.t("Connection failed"))
.setMessage(App.describe((Throwable) r)).setPositiveButton(L.t("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")
StringBuilder s = new StringBuilder(L.f("Connected. Calendars found: %d\n", cals.size()));
for (Cal c : cals) s.append("\n\u2022 ").append(c.name).append(c.writable ? "" : " " + L.t("(read-only)"));
new AlertDialog.Builder(SettingsActivity.this).setTitle(L.t("It works"))
.setMessage(s.toString())
.setPositiveButton("Sync now", new DialogInterface.OnClickListener() {
.setPositiveButton(L.t("Sync now"), new DialogInterface.OnClickListener() {
public void onClick(DialogInterface d, int w) {
App.requestSync(SettingsActivity.this, true, false);
finish();
+3 -2
View File
@@ -26,8 +26,9 @@ public class SyncService extends IntentService {
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();
Syncer.Result r = Syncer.sync(dav, Db.get(this), now, App.BACK_DAYS, App.ahead(this), force);
p.edit().putLong("lastsync", now).putString("status", r.summary()).putBoolean("error", false)
.putLong("win_from", r.from).putLong("win_to", r.to).commit();
send("done", r.summary(), null);
} catch (DavException.UntrustedCert u) {
p.edit().putString("status", u.getMessage()).putBoolean("error", true).commit();