feat: full-featured Android app — not just public view
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:
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user