feat: full-featured Android app — not just public view
CI / test-and-package (push) Successful in 40s
CI / build-android (push) Successful in 3m10s

Rewrote the Android WebView app to load the entire meal-tracker
web application, not just the public sharing page:

- User configures their server base URL (e.g. https://meals.example.com)
- Full login/register, dashboard, meal responses, history, settings
- Cookie/session support — login persists across app restarts
- Dynamic toolbar title from page
- 'Clear Data' button in settings to wipe cookies/cache/sessions
- Server URL auto-normalized (trailing slash stripped, https:// added)
- Back button navigates within WebView history
- Swipe-to-refresh, loading bar, network error handling
This commit is contained in:
agentbox
2026-08-01 11:35:05 +00:00
parent d37efa1dcc
commit f93e75e6f0
4 changed files with 105 additions and 28 deletions
@@ -9,7 +9,7 @@ import android.net.NetworkInfo;
import android.os.Bundle;
import android.view.KeyEvent;
import android.view.View;
import android.view.inputmethod.EditorInfo;
import android.webkit.CookieManager;
import android.webkit.WebChromeClient;
import android.webkit.WebResourceError;
import android.webkit.WebResourceRequest;
@@ -29,12 +29,14 @@ import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
public class MainActivity extends AppCompatActivity {
private static final String PREFS_NAME = "mealtracker_prefs";
private static final String KEY_PUBLIC_URL = "public_url";
private static final String KEY_SERVER_URL = "server_url";
private WebView webView;
private SwipeRefreshLayout swipeRefresh;
private ProgressBar progressBar;
private TextView errorView;
private TextView toolbarTitle;
private String serverUrl;
private String currentUrl;
@SuppressLint("SetJavaScriptEnabled")
@@ -47,6 +49,7 @@ public class MainActivity extends AppCompatActivity {
swipeRefresh = findViewById(R.id.swipe_refresh);
progressBar = findViewById(R.id.progress_bar);
errorView = findViewById(R.id.error_view);
toolbarTitle = findViewById(R.id.toolbar_title);
ImageButton btnSettings = findViewById(R.id.btn_settings);
ImageButton btnRefresh = findViewById(R.id.btn_refresh);
@@ -56,12 +59,13 @@ public class MainActivity extends AppCompatActivity {
btnRefresh.setOnClickListener(v -> webView.reload());
btnSettings.setOnClickListener(v -> showSettingsDialog());
// Load saved URL or show setup
SharedPreferences prefs = getSharedPreferences(PREFS_NAME, MODE_PRIVATE);
String savedUrl = prefs.getString(KEY_PUBLIC_URL, "");
if (!savedUrl.isEmpty()) {
loadUrl(savedUrl);
serverUrl = prefs.getString(KEY_SERVER_URL, "");
if (!serverUrl.isEmpty()) {
loadUrl(serverUrl);
toolbarTitle.setText(getString(R.string.app_name));
} else {
toolbarTitle.setText(R.string.app_name);
showSettingsDialog();
}
}
@@ -71,6 +75,7 @@ public class MainActivity extends AppCompatActivity {
WebSettings settings = webView.getSettings();
settings.setJavaScriptEnabled(true);
settings.setDomStorageEnabled(true);
settings.setDatabaseEnabled(true);
settings.setUseWideViewPort(true);
settings.setLoadWithOverviewMode(true);
settings.setSupportZoom(true);
@@ -78,12 +83,22 @@ public class MainActivity extends AppCompatActivity {
settings.setDisplayZoomControls(false);
settings.setCacheMode(WebSettings.LOAD_DEFAULT);
settings.setMixedContentMode(WebSettings.MIXED_CONTENT_ALWAYS_ALLOW);
settings.setAllowFileAccess(false);
settings.setSaveFormData(true);
// Accept all cookies so login sessions persist
CookieManager.getInstance().setAcceptCookie(true);
CookieManager.getInstance().setAcceptThirdPartyCookies(webView, true);
String ua = settings.getUserAgentString();
settings.setUserAgentString(ua + " MealTrackerApp/1.0");
webView.setWebViewClient(new WebViewClient() {
@Override
public void onPageStarted(WebView view, String url, Bitmap favicon) {
progressBar.setVisibility(View.VISIBLE);
errorView.setVisibility(View.GONE);
webView.setVisibility(View.VISIBLE);
}
@Override
@@ -111,8 +126,9 @@ public class MainActivity extends AppCompatActivity {
@Override
public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) {
String url = request.getUrl().toString();
// Keep all navigation inside the WebView
view.loadUrl(request.getUrl().toString());
view.loadUrl(url);
return true;
}
});
@@ -125,17 +141,24 @@ public class MainActivity extends AppCompatActivity {
swipeRefresh.setRefreshing(false);
}
}
@Override
public void onReceivedTitle(WebView view, String title) {
if (title != null && !title.isEmpty() && !title.startsWith("http")) {
toolbarTitle.setText(title);
}
}
});
// Retry on error tap
errorView.setOnClickListener(v -> {
if (currentUrl != null) {
loadUrl(currentUrl);
if (serverUrl != null) {
loadUrl(serverUrl);
}
});
}
private void loadUrl(String url) {
serverUrl = url;
currentUrl = url;
errorView.setVisibility(View.GONE);
webView.setVisibility(View.VISIBLE);
@@ -151,32 +174,61 @@ public class MainActivity extends AppCompatActivity {
TextView hintView = dialogView.findViewById(R.id.url_hint);
SharedPreferences prefs = getSharedPreferences(PREFS_NAME, MODE_PRIVATE);
String savedUrl = prefs.getString(KEY_PUBLIC_URL, "");
String savedUrl = prefs.getString(KEY_SERVER_URL, "");
urlInput.setText(savedUrl);
hintView.setText(R.string.url_hint_text);
builder.setView(dialogView);
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_PUBLIC_URL, url).apply();
prefs.edit().putString(KEY_SERVER_URL, url).apply();
loadUrl(url);
Toast.makeText(this, R.string.url_saved, Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(this, R.string.url_empty, Toast.LENGTH_SHORT).show();
}
});
builder.setNegativeButton(R.string.cancel, null);
builder.setNegativeButton(R.string.cancel, (dialog, which) -> {
if (serverUrl == null || serverUrl.isEmpty()) {
finish();
}
});
builder.setNeutralButton(R.string.clear_data, (dialog, which) -> {
clearAllData();
prefs.edit().remove(KEY_SERVER_URL).apply();
serverUrl = null;
webView.loadUrl("about:blank");
webView.setVisibility(View.GONE);
errorView.setText(R.string.server_not_configured);
errorView.setVisibility(View.VISIBLE);
toolbarTitle.setText(R.string.app_name);
Toast.makeText(this, R.string.data_cleared, Toast.LENGTH_SHORT).show();
});
builder.setCancelable(false);
builder.show();
}
private void clearAllData() {
webView.clearCache(true);
webView.clearHistory();
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);
}
private boolean isNetworkAvailable() {
ConnectivityManager cm = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
if (cm == null) return false;
NetworkInfo info = cm.getActiveNetworkInfo();
return info != null && info.isConnected();
}
@@ -189,4 +241,16 @@ public class MainActivity extends AppCompatActivity {
}
return super.onKeyDown(keyCode, event);
}
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
webView.saveState(outState);
}
@Override
protected void onRestoreInstanceState(Bundle savedInstanceState) {
super.onRestoreInstanceState(savedInstanceState);
webView.restoreState(savedInstanceState);
}
}
@@ -20,16 +20,26 @@
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:title="@string/app_name"
app:titleTextColor="@android:color/white"
android:background="@color/colorPrimary">
android:background="@color/colorPrimary"
app:contentInsetStart="12dp">
<TextView
android:id="@+id/toolbar_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/app_name"
android:textColor="@android:color/white"
android:textSize="18sp"
android:maxLines="1"
android:ellipsize="end"
android:gravity="center_vertical" />
<ImageButton
android:id="@+id/btn_refresh"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_gravity="end"
android:layout_marginEnd="4dp"
android:src="@drawable/ic_refresh"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/refresh" />
@@ -39,7 +49,6 @@
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_gravity="end"
android:layout_marginEnd="4dp"
android:src="@drawable/ic_settings"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/settings" />
@@ -12,18 +12,19 @@
android:text="@string/url_label"
android:textSize="14sp"
android:textColor="@android:color/darker_gray"
android:paddingBottom="4dp" />
android:paddingBottom="6dp" />
<EditText
android:id="@+id/url_input"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textUri"
android:hint="https://meals.example.com/public/..."
android:hint="https://meals.example.com"
android:padding="12dp"
android:background="@android:drawable/edit_text"
android:textSize="14sp"
android:importantForAutofill="no" />
android:importantForAutofill="no"
android:autofillHints="no" />
<TextView
android:id="@+id/url_hint"
+9 -6
View File
@@ -3,13 +3,16 @@
<string name="app_name">Meal Tracker</string>
<string name="refresh">Refresh</string>
<string name="settings">Settings</string>
<string name="settings_title">Public URL</string>
<string name="url_label">Enter the public sharing URL for your household:</string>
<string name="url_hint_text">Paste the link your household admin shared with you. It looks like:\nhttps://.../public/abc123...</string>
<string name="settings_title">Server Configuration</string>
<string name="url_label">Enter your Meal Tracker server URL:</string>
<string name="url_hint_text">This is the URL where your household\'s meal tracker is hosted.\nExample: https://meals.example.com</string>
<string name="save">Save</string>
<string name="cancel">Cancel</string>
<string name="url_saved">URL saved</string>
<string name="url_empty">Please enter a URL</string>
<string name="clear_data">Clear Data</string>
<string name="url_saved">Connected to server</string>
<string name="url_empty">Please enter a server URL</string>
<string name="error_no_network">No network connection.\nTap to retry.</string>
<string name="error_load_failed">Could not load the page.\nTap to retry.</string>
<string name="error_load_failed">Could not reach the server.\nCheck the URL and tap to retry.</string>
<string name="server_not_configured">No server configured.\nTap the gear icon to set up.</string>
<string name="data_cleared">Local data cleared</string>
</resources>