diff --git a/android/app/src/main/java/com/mealtracker/app/MainActivity.java b/android/app/src/main/java/com/mealtracker/app/MainActivity.java
index 6dbfba0..1486fac 100644
--- a/android/app/src/main/java/com/mealtracker/app/MainActivity.java
+++ b/android/app/src/main/java/com/mealtracker/app/MainActivity.java
@@ -6,13 +6,16 @@ import android.content.SharedPreferences;
import android.graphics.Bitmap;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
+import android.net.http.SslError;
import android.os.Bundle;
import android.view.KeyEvent;
import android.view.View;
import android.webkit.CookieManager;
+import android.webkit.SslErrorHandler;
import android.webkit.WebChromeClient;
import android.webkit.WebResourceError;
import android.webkit.WebResourceRequest;
+import android.webkit.WebResourceResponse;
import android.webkit.WebSettings;
import android.webkit.WebView;
import android.webkit.WebViewClient;
@@ -30,6 +33,7 @@ public class MainActivity extends AppCompatActivity {
private static final String PREFS_NAME = "mealtracker_prefs";
private static final String KEY_SERVER_URL = "server_url";
+ private static final String KEY_PUBLIC_URL = "public_url"; // migrate old key
private WebView webView;
private SwipeRefreshLayout swipeRefresh;
@@ -38,6 +42,7 @@ public class MainActivity extends AppCompatActivity {
private TextView toolbarTitle;
private String serverUrl;
private String currentUrl;
+ private boolean firstLoad = true;
@SuppressLint("SetJavaScriptEnabled")
@Override
@@ -61,11 +66,22 @@ public class MainActivity extends AppCompatActivity {
SharedPreferences prefs = getSharedPreferences(PREFS_NAME, MODE_PRIVATE);
serverUrl = prefs.getString(KEY_SERVER_URL, "");
+
+ // Migrate from old public_url key if present
+ if (serverUrl.isEmpty()) {
+ String oldUrl = prefs.getString(KEY_PUBLIC_URL, "");
+ if (!oldUrl.isEmpty()) {
+ prefs.edit().putString(KEY_SERVER_URL, oldUrl).remove(KEY_PUBLIC_URL).apply();
+ serverUrl = oldUrl;
+ }
+ }
+
if (!serverUrl.isEmpty()) {
loadUrl(serverUrl);
- toolbarTitle.setText(getString(R.string.app_name));
} else {
- toolbarTitle.setText(R.string.app_name);
+ errorView.setText(R.string.server_not_configured);
+ errorView.setVisibility(View.VISIBLE);
+ webView.setVisibility(View.GONE);
showSettingsDialog();
}
}
@@ -86,7 +102,9 @@ public class MainActivity extends AppCompatActivity {
settings.setAllowFileAccess(false);
settings.setSaveFormData(true);
- // Accept all cookies so login sessions persist
+ // Avoid blank flash — match the site background
+ webView.setBackgroundColor(0xFFF5F5F5);
+
CookieManager.getInstance().setAcceptCookie(true);
CookieManager.getInstance().setAcceptThirdPartyCookies(webView, true);
@@ -96,6 +114,7 @@ public class MainActivity extends AppCompatActivity {
webView.setWebViewClient(new WebViewClient() {
@Override
public void onPageStarted(WebView view, String url, Bitmap favicon) {
+ firstLoad = false;
progressBar.setVisibility(View.VISIBLE);
errorView.setVisibility(View.GONE);
webView.setVisibility(View.VISIBLE);
@@ -112,23 +131,39 @@ public class MainActivity extends AppCompatActivity {
public void onReceivedError(WebView view, WebResourceRequest request,
WebResourceError error) {
if (request.isForMainFrame()) {
- progressBar.setVisibility(View.GONE);
- swipeRefresh.setRefreshing(false);
- if (!isNetworkAvailable()) {
- errorView.setText(R.string.error_no_network);
- } else {
- errorView.setText(R.string.error_load_failed);
- }
- errorView.setVisibility(View.VISIBLE);
- webView.setVisibility(View.GONE);
+ showError(isNetworkAvailable()
+ ? R.string.error_load_failed : R.string.error_no_network);
}
}
+ @Override
+ public void onReceivedHttpError(WebView view, WebResourceRequest request,
+ WebResourceResponse errorResponse) {
+ // Only show error for the main page, not subresources
+ if (request.isForMainFrame() && firstLoad) {
+ int code = errorResponse.getStatusCode();
+ errorView.setText(getString(R.string.error_http, code));
+ showError(0); // 0 means keep the text we just set
+ }
+ }
+
+ @Override
+ public void onReceivedSslError(WebView view, SslErrorHandler handler,
+ SslError error) {
+ // Let the user decide — some self-hosted instances use self-signed certs
+ new AlertDialog.Builder(MainActivity.this)
+ .setTitle(R.string.ssl_error_title)
+ .setMessage(getString(R.string.ssl_error_msg,
+ error.getUrl(), error.getPrimaryError()))
+ .setPositiveButton(R.string.ssl_proceed, (d, w) -> handler.proceed())
+ .setNegativeButton(R.string.ssl_cancel, (d, w) -> handler.cancel())
+ .setCancelable(false)
+ .show();
+ }
+
@Override
public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) {
- String url = request.getUrl().toString();
- // Keep all navigation inside the WebView
- view.loadUrl(url);
+ view.loadUrl(request.getUrl().toString());
return true;
}
});
@@ -136,6 +171,7 @@ public class MainActivity extends AppCompatActivity {
webView.setWebChromeClient(new WebChromeClient() {
@Override
public void onProgressChanged(WebView view, int newProgress) {
+ progressBar.setProgress(newProgress);
if (newProgress == 100) {
progressBar.setVisibility(View.GONE);
swipeRefresh.setRefreshing(false);
@@ -151,17 +187,32 @@ public class MainActivity extends AppCompatActivity {
});
errorView.setOnClickListener(v -> {
- if (serverUrl != null) {
+ if (serverUrl != null && !serverUrl.isEmpty()) {
+ firstLoad = true;
loadUrl(serverUrl);
+ } else {
+ showSettingsDialog();
}
});
}
+ private void showError(int resId) {
+ progressBar.setVisibility(View.GONE);
+ swipeRefresh.setRefreshing(false);
+ if (resId != 0) {
+ errorView.setText(resId);
+ }
+ errorView.setVisibility(View.VISIBLE);
+ webView.setVisibility(View.GONE);
+ }
+
private void loadUrl(String url) {
serverUrl = url;
currentUrl = url;
+ firstLoad = true;
errorView.setVisibility(View.GONE);
webView.setVisibility(View.VISIBLE);
+ progressBar.setVisibility(View.VISIBLE);
webView.loadUrl(url);
}
@@ -182,13 +233,14 @@ public class MainActivity extends AppCompatActivity {
builder.setPositiveButton(R.string.save, (dialog, which) -> {
String url = urlInput.getText().toString().trim();
if (!url.isEmpty()) {
- // Normalize: strip trailing slashes, auto-add https://
url = url.replaceAll("/+$", "");
if (!url.startsWith("http://") && !url.startsWith("https://")) {
url = "https://" + url;
}
prefs.edit().putString(KEY_SERVER_URL, url).apply();
+ firstLoad = true;
loadUrl(url);
+ toolbarTitle.setText(R.string.app_name);
Toast.makeText(this, R.string.url_saved, Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(this, R.string.url_empty, Toast.LENGTH_SHORT).show();
@@ -220,9 +272,7 @@ public class MainActivity extends AppCompatActivity {
webView.clearFormData();
CookieManager.getInstance().removeAllCookies(null);
CookieManager.getInstance().flush();
- // Clear WebView storage
webView.clearSslPreferences();
- // Nuke DOM/local storage
webView.evaluateJavascript("localStorage.clear(); sessionStorage.clear();", null);
}
diff --git a/android/app/src/main/res/layout/activity_main.xml b/android/app/src/main/res/layout/activity_main.xml
index 70225ea..211b6df 100644
--- a/android/app/src/main/res/layout/activity_main.xml
+++ b/android/app/src/main/res/layout/activity_main.xml
@@ -60,9 +60,10 @@
style="?android:attr/progressBarStyleHorizontal"
android:layout_width="match_parent"
android:layout_height="3dp"
- android:indeterminate="true"
+ android:max="100"
android:visibility="gone"
- android:indeterminateTint="@color/colorAccent" />
+ android:indeterminateTint="@color/colorAccent"
+ android:progressTint="@color/colorAccent" />
diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml
index 5c3a5ec..34246c9 100644
--- a/android/app/src/main/res/values/strings.xml
+++ b/android/app/src/main/res/values/strings.xml
@@ -5,14 +5,19 @@
Settings
Server Configuration
Enter your Meal Tracker server URL:
- This is the URL where your household\'s meal tracker is hosted.\nExample: https://meals.example.com
+ The address where your meal tracker is hosted.\nExample: https://meals.example.com
Save
Cancel
Clear Data
Connected to server
Please enter a server URL
No network connection.\nTap to retry.
- Could not reach the server.\nCheck the URL and tap to retry.
- No server configured.\nTap the gear icon to set up.
+ Could not reach the server.\nCheck the URL and tap to retry.\n\nTip: make sure the URL opens in a browser first.
+ Server returned HTTP %d.\nCheck your server URL and tap to retry.
+ No server configured.\nTap the gear icon ⚙ to enter your server URL.
Local data cleared
+ ⚠ Certificate Warning
+ The server at %1$s uses an untrusted certificate (error %2$d).\n\nThis is common for self-hosted servers. Proceed only if you trust this server.
+ Proceed
+ Cancel