Files
calendar-lite/android/org/gingercal/SettingsActivity.java
T
XoséandClaude Opus 5.5 f7428287eb GingerCal 1.4: bundled TLS 1.2, no bridge needed
CalDAV now goes through Minamp's own HTTP/TLS client (Bouncy Castle TLS 1.2, Mozilla
roots, keep-alive) instead of Apache HttpClient on Android 2.3's TLS 1.0, as the iPod
version does with BearSSL. The phone reaches the normal https:// Nextcloud address at
home and away; a pinned self-signed server or the old bridge still works.

- NetHttp transport; requests run on big-stack threads (the handshake overflows
  AsyncTask/IntentService stacks on Android 2.x)
- http:// -> https:// redirects on the same host are followed; other hosts still refused
- bridge wording replaced in Galician and English; README updated
- NetTest (TLS + CalDAV discovery through the new transport) replaces BridgeTest

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 19:14:06 +02:00

263 lines
11 KiB
Java

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.content.Intent;
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, langBtn;
int interval, remind;
@Override
protected void onCreate(Bundle b) {
super.onCreate(b);
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, L.t("Nextcloud server: your normal https:// address. This app brings its own modern TLS, so no bridge is needed, at home or away. Use an app password, not your real one."), 12, 0xFFB0BEC5));
url = field(f, L.t("Server address"), "https://cloud.example.com", 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(L.t("Save and test connection"));
f.addView(test, new LinearLayout.LayoutParams(-1, -2));
Button resync = new Button(this);
resync.setText(L.t("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(L.t("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 ? 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];
labels();
}
}).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;
GingerApp.tlsTexts();
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();
}
});
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, L.t("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 ? L.t("Manual only") : h == 1 ? L.t("Every hour") : L.f("Every %d hours", h);
}
void labels() {
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) {
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).putLong("win_from", 0).putLong("win_to", 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, L.t("Server address and user name are required."));
return;
}
if (Dav.normalize(u).startsWith("http://") && !App.isLan(u)) {
new AlertDialog.Builder(this).setTitle(L.t("Unencrypted connection"))
.setMessage(L.t("http:// sends your password in clear text over the internet. Use the https:// address 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(L.t("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, L.t("Connecting\u2026"));
}
@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(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(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(L.t("Sync now"), new DialogInterface.OnClickListener() {
public void onClick(DialogInterface d, int w) {
App.requestSync(SettingsActivity.this, true, false);
finish();
}
}).show();
}
}
}