From 2e4e0c51848f45fcd5e09b636f358cf04552071a Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 3 Apr 2026 14:45:28 +0000 Subject: [PATCH 1/6] Complete Material 3 migration with unified edge-to-edge MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the manual edge-to-edge workaround in ApplicationEx (which only handled Android 15+ with custom insets/padding/background hacks) with a single EdgeToEdge.enable() call that works across all API levels. This uses SystemBarStyle.dark(colorPrimaryDark) for the status bar and transparent auto-scrim for the navigation bar. - Remove AppTheme.EdgeToEdge style and DetailsActivity manual setup - Migrate all AlertDialog.Builder → MaterialAlertDialogBuilder (7 files) - Migrate all TextAppearance.AppCompat → Material3 equivalents in XML - Migrate all Widget.AppCompat.Button → Material3 equivalents in XML - Migrate Theme.AppCompat/MaterialComponents → Material3 in layouts - Update ActionBarTitleText and text utility styles to M3 parents - Add explicit androidx.activity dependency for EdgeToEdge API https://claude.ai/code/session_01AKoCWnAeeHrRa4KjngEiTM --- TODO.md | 24 ++----- app/build.gradle | 1 + app/src/main/AndroidManifest.xml | 2 +- .../faircode/netguard/ActivityForwarding.java | 4 +- .../eu/faircode/netguard/ActivityLog.java | 4 +- .../eu/faircode/netguard/ActivityMain.java | 12 ++-- .../faircode/netguard/ActivitySettings.java | 4 +- .../eu/faircode/netguard/ApplicationEx.java | 64 +++---------------- .../main/java/eu/faircode/netguard/Util.java | 4 +- .../missioncontrol/ActivityBlocklists.java | 6 +- .../missioncontrol/DetailsActivity.java | 19 +----- .../details/ActionsFragment.java | 4 +- .../main/res/layout/activity_onboarding.xml | 2 +- app/src/main/res/layout/item_onboarding.xml | 4 +- .../layout/item_onboarding_blocking_mode.xml | 12 ++-- .../res/layout/list_item_trackers_header.xml | 4 +- app/src/main/res/layout/traffic.xml | 18 +++--- app/src/main/res/layout/troubleshooting.xml | 6 +- app/src/main/res/values/styles.xml | 18 ++---- 19 files changed, 70 insertions(+), 142 deletions(-) diff --git a/TODO.md b/TODO.md index 146e152d4..18deac1a1 100644 --- a/TODO.md +++ b/TODO.md @@ -106,24 +106,12 @@ If revisited later, start by deciding whether the desired goal is: - just to reduce cross-app cache bleed, or - to redesign tracker attribution so "per-app blocking" is backed by per-app evidence. -## Remaining Material 3 migration items +## Material 3 migration — completed -The core M3 migration is complete (themes, switches, settings with MaterialToolbar, FABs, tabs). The following are cosmetic items that still reference AppCompat/MaterialComponents but work correctly: +The M3 migration is complete. All themes, components, text appearances, button styles, dialog builders, and layout-level theme references use Material 3 equivalents. -### TextAppearance references (~16 occurrences) -- `traffic.xml` — 8 uses of `Base.TextAppearance.AppCompat.Small` → should be `TextAppearance.Material3.BodySmall` -- `item_onboarding.xml` / `item_onboarding_blocking_mode.xml` — uses of `TextAppearance.AppCompat.Large`, `.Body1`, `.Caption` → should be `TextAppearance.Material3.HeadlineSmall`, `.BodyLarge`, `.LabelSmall` +### Edge-to-edge +Edge-to-edge is handled globally via `EdgeToEdge.enable()` in `ApplicationEx`, applied to all activities via lifecycle callbacks. Uses `SystemBarStyle.dark(colorPrimaryDark)` for the status bar (red/dark red scrim with white icons) and `SystemBarStyle.auto(transparent, transparent)` for the navigation bar. The old `AppTheme.EdgeToEdge` style and per-activity manual insets handling in `DetailsActivity` have been removed. -### Button styles (4 buttons) -- `activity_onboarding.xml:28` — `Widget.AppCompat.Button.Borderless` → `Widget.Material3.Button.TextButton` -- `troubleshooting.xml:56,90,156` — `Widget.AppCompat.Button.Borderless.Colored` → `Widget.Material3.Button.TextButton` - -### Layout-level theme references -- `traffic.xml:5` — `Theme.AppCompat.Light.DarkActionBar` on a RelativeLayout -- `list_item_trackers_header.xml:158-159` — `Theme.MaterialComponents.DayNight` and `Widget.MaterialComponents.Button.OutlinedButton` → `Theme.Material3.DayNight` and `Widget.Material3.Button.OutlinedButton` - -### AlertDialog → MaterialAlertDialogBuilder (12 occurrences) -Uses of `androidx.appcompat.app.AlertDialog.Builder` across ActivityMain, ActivityLog, ActivitySettings, ActivityForwarding, ActivityBlocklists, ActionsFragment, and Util. Migrating to `com.google.android.material.dialog.MaterialAlertDialogBuilder` would give M3-styled dialogs (rounded corners, M3 color tokens). - -### Action bar styles in styles.xml -`ActionBar.Red`, `ActionBarTheme.Red`, `Toolbar.Red`, `ActionBarTitleText` still reference AppCompat parents (`Widget.AppCompat.ActionBar.Solid`, `TextAppearance.AppCompat.Widget.ActionBar.Title`). These are used by the main screen's legacy action bar and work correctly but aren't pure M3. +### Remaining AppCompat reference +`ActionBar.Red` in `styles.xml` still uses `Widget.AppCompat.ActionBar.Solid` as parent — there is no Material 3 equivalent for the framework ActionBar widget style. This is cosmetic; the action bar renders correctly with M3 theming applied via `ActionBarTheme.Red` (which uses `ThemeOverlay.Material3.Dark.ActionBar`). diff --git a/app/build.gradle b/app/build.gradle index e88829c6e..9a285d3bd 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -137,6 +137,7 @@ dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) // https://developer.android.com/jetpack/androidx/releases/ + implementation 'androidx.activity:activity:1.9.3' implementation 'androidx.appcompat:appcompat:1.7.1' implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.2.0' implementation 'androidx.recyclerview:recyclerview:1.4.0' diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index b312699b8..40eaa6934 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -172,7 +172,7 @@ android:exported="true" android:label="@string/title_activity_detail" android:parentActivityName="eu.faircode.netguard.ActivityMain" - android:theme="@style/AppTheme.EdgeToEdge"> + android:theme="@style/AppTheme.NoActionBar"> diff --git a/app/src/main/java/eu/faircode/netguard/ActivityForwarding.java b/app/src/main/java/eu/faircode/netguard/ActivityForwarding.java index 0ebfee88f..37bf8da40 100644 --- a/app/src/main/java/eu/faircode/netguard/ActivityForwarding.java +++ b/app/src/main/java/eu/faircode/netguard/ActivityForwarding.java @@ -39,6 +39,8 @@ import android.widget.Toast; import androidx.appcompat.app.AlertDialog; + +import com.google.android.material.dialog.MaterialAlertDialogBuilder; import androidx.appcompat.app.AppCompatActivity; import net.kollnig.missioncontrol.R; @@ -183,7 +185,7 @@ protected void onPostExecute(List rules) { }; task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); - dialog = new AlertDialog.Builder(this) + dialog = new MaterialAlertDialogBuilder(this) .setView(view) .setCancelable(true) .setPositiveButton(android.R.string.yes, new DialogInterface.OnClickListener() { diff --git a/app/src/main/java/eu/faircode/netguard/ActivityLog.java b/app/src/main/java/eu/faircode/netguard/ActivityLog.java index dfa7d9c5c..7607b16b0 100644 --- a/app/src/main/java/eu/faircode/netguard/ActivityLog.java +++ b/app/src/main/java/eu/faircode/netguard/ActivityLog.java @@ -41,6 +41,8 @@ import android.widget.Toast; import androidx.appcompat.app.AlertDialog; + +import com.google.android.material.dialog.MaterialAlertDialogBuilder; import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.widget.SearchView; import com.google.android.material.materialswitch.MaterialSwitch; @@ -405,7 +407,7 @@ public boolean onOptionsItemSelected(MenuItem item) { } // warn that enabling will send data to ipinfo.io - AlertDialog.Builder builder = new AlertDialog.Builder(this) + AlertDialog.Builder builder = new MaterialAlertDialogBuilder(this) .setTitle(R.string.confirm_ipinfo_title) .setMessage(R.string.confirm_ipinfo) .setPositiveButton(R.string.yes, (dialog, id2) -> { diff --git a/app/src/main/java/eu/faircode/netguard/ActivityMain.java b/app/src/main/java/eu/faircode/netguard/ActivityMain.java index a04704a0f..b4e10a7fb 100644 --- a/app/src/main/java/eu/faircode/netguard/ActivityMain.java +++ b/app/src/main/java/eu/faircode/netguard/ActivityMain.java @@ -60,6 +60,8 @@ import androidx.annotation.NonNull; import androidx.appcompat.app.AlertDialog; + +import com.google.android.material.dialog.MaterialAlertDialogBuilder; import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.widget.SearchView; import com.google.android.material.materialswitch.MaterialSwitch; @@ -182,7 +184,7 @@ protected void onCreate(Bundle savedInstanceState) { // Check for filtering if (!Util.canFilter(this)) { - AlertDialog.Builder builder = new AlertDialog.Builder(this) + AlertDialog.Builder builder = new MaterialAlertDialogBuilder(this) .setTitle(R.string.device_not_supported_title) .setMessage(R.string.device_not_supported_msg) .setPositiveButton(R.string.ok, (dialog, id) -> { @@ -302,7 +304,7 @@ public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { LayoutInflater inflater = LayoutInflater.from(ActivityMain.this); final View view = inflater.inflate(R.layout.vpn, null, false); - dialogVpn = new AlertDialog.Builder(ActivityMain.this) + dialogVpn = new MaterialAlertDialogBuilder(ActivityMain.this) .setView(view) .setCancelable(false) .setPositiveButton(android.R.string.yes, new DialogInterface.OnClickListener() { @@ -1197,7 +1199,7 @@ public void onClick(View v) { } }); - dialogTroubleshooting = new AlertDialog.Builder(this) + dialogTroubleshooting = new MaterialAlertDialogBuilder(this) .setView(view) .setCancelable(true) .setPositiveButton(android.R.string.ok, null) @@ -1247,7 +1249,7 @@ private void menu_legend() { } // Show dialog - dialogLegend = new AlertDialog.Builder(this) + dialogLegend = new MaterialAlertDialogBuilder(this) .setView(view) .setCancelable(true) .setOnDismissListener(new DialogInterface.OnDismissListener() { @@ -1289,7 +1291,7 @@ public void onClick(View view) { }); // Show dialog - dialogAbout = new AlertDialog.Builder(this) + dialogAbout = new MaterialAlertDialogBuilder(this) .setView(view) .setCancelable(true) .setOnDismissListener(new DialogInterface.OnDismissListener() { diff --git a/app/src/main/java/eu/faircode/netguard/ActivitySettings.java b/app/src/main/java/eu/faircode/netguard/ActivitySettings.java index f22968db9..31e99c5a6 100644 --- a/app/src/main/java/eu/faircode/netguard/ActivitySettings.java +++ b/app/src/main/java/eu/faircode/netguard/ActivitySettings.java @@ -52,6 +52,8 @@ import androidx.annotation.NonNull; import androidx.appcompat.app.AlertDialog; + +import com.google.android.material.dialog.MaterialAlertDialogBuilder; import androidx.appcompat.app.AppCompatActivity; import androidx.core.app.NavUtils; import androidx.core.util.PatternsCompat; @@ -624,7 +626,7 @@ else if ("filter".equals(name)) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && prefs.getBoolean(name, true)) { LayoutInflater inflater = LayoutInflater.from(ActivitySettings.this); View view = inflater.inflate(R.layout.filter, null, false); - dialogFilter = new AlertDialog.Builder(ActivitySettings.this) + dialogFilter = new MaterialAlertDialogBuilder(ActivitySettings.this) .setView(view) .setCancelable(false) .setPositiveButton(android.R.string.yes, new DialogInterface.OnClickListener() { diff --git a/app/src/main/java/eu/faircode/netguard/ApplicationEx.java b/app/src/main/java/eu/faircode/netguard/ApplicationEx.java index 4134d18c3..ea44207d5 100644 --- a/app/src/main/java/eu/faircode/netguard/ApplicationEx.java +++ b/app/src/main/java/eu/faircode/netguard/ApplicationEx.java @@ -31,25 +31,19 @@ import android.content.Context; import android.content.SharedPreferences; import android.graphics.Color; -import android.graphics.drawable.ColorDrawable; import android.os.Build; import android.os.Bundle; import android.os.StrictMode; import android.util.Log; -import android.view.View; +import androidx.activity.ComponentActivity; +import androidx.activity.EdgeToEdge; +import androidx.activity.SystemBarStyle; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.content.ContextCompat; -import androidx.core.graphics.Insets; -import androidx.core.view.OnApplyWindowInsetsListener; -import androidx.core.view.ViewCompat; -import androidx.core.view.WindowCompat; -import androidx.core.view.WindowInsetsCompat; import net.kollnig.missioncontrol.BuildConfig; -import net.kollnig.missioncontrol.Common; -import net.kollnig.missioncontrol.DetailsActivity; import net.kollnig.missioncontrol.R; import net.kollnig.missioncontrol.data.BlockingMode; @@ -129,79 +123,37 @@ public void onCreate() { registerActivityLifecycleCallbacks(new ActivityLifecycleCallbacks() { @Override public void onActivityCreated(@NonNull Activity activity, @Nullable Bundle savedInstanceState) { - if (activity.getClass() == DetailsActivity.class) - return; - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) { - android.widget.FrameLayout content = activity.findViewById(android.R.id.content); - - // On Android 15+, setStatusBarColor is ignored - // Set window background to primary color - this shows behind the status bar + if (activity instanceof ComponentActivity) { int statusBarColor = ContextCompat.getColor(activity, R.color.colorPrimaryDark); - activity.getWindow().setBackgroundDrawable(new ColorDrawable(statusBarColor)); - - boolean isNight = Common.isNight(activity); - - // Set status bar icons to light (white) since our background is dark - View decor = activity.getWindow().getDecorView(); - WindowCompat.getInsetsController(activity.getWindow(), decor).setAppearanceLightStatusBars(false); - WindowCompat.getInsetsController(activity.getWindow(), decor) - .setAppearanceLightNavigationBars(!isNight); - - ViewCompat.setOnApplyWindowInsetsListener(content, new OnApplyWindowInsetsListener() { - @NonNull - @Override - public WindowInsetsCompat onApplyWindowInsets(@NonNull View v, - @NonNull WindowInsetsCompat insets) { - Insets bars = insets.getInsets(WindowInsetsCompat.Type.systemBars() - | WindowInsetsCompat.Type.displayCutout() | WindowInsetsCompat.Type.ime()); - - // Apply padding to android.R.id.content for system bars - // We do NOT apply bottom padding here, so the content background (White/Black) - // extends to the bottom - v.setPadding(bars.left, bars.top, bars.right, 0); - - // Set background on the actual layout (first child), not on the content frame - // This way the padding area shows the window background (primary color) - if (content.getChildCount() > 0) { - View child = content.getChildAt(0); - child.setBackgroundColor(Common.isNight(activity) ? Color.BLACK : Color.WHITE); - } - - return insets; - } - }); + EdgeToEdge.enable( + (ComponentActivity) activity, + SystemBarStyle.dark(statusBarColor), + SystemBarStyle.auto(Color.TRANSPARENT, Color.TRANSPARENT)); } } @Override public void onActivityStarted(@NonNull Activity activity) { - } @Override public void onActivityResumed(@NonNull Activity activity) { - } @Override public void onActivityPaused(@NonNull Activity activity) { - } @Override public void onActivityStopped(@NonNull Activity activity) { - } @Override public void onActivitySaveInstanceState(@NonNull Activity activity, @NonNull Bundle outState) { - } @Override public void onActivityDestroyed(@NonNull Activity activity) { - } }); } diff --git a/app/src/main/java/eu/faircode/netguard/Util.java b/app/src/main/java/eu/faircode/netguard/Util.java index 4a89443ff..52690fe59 100644 --- a/app/src/main/java/eu/faircode/netguard/Util.java +++ b/app/src/main/java/eu/faircode/netguard/Util.java @@ -50,7 +50,7 @@ import android.view.View; import android.widget.TextView; -import androidx.appcompat.app.AlertDialog; +import com.google.android.material.dialog.MaterialAlertDialogBuilder; import androidx.appcompat.app.AppCompatDelegate; import androidx.core.app.ActivityCompat; import androidx.core.net.ConnectivityManagerCompat; @@ -560,7 +560,7 @@ public static void areYouSure(Context context, int explanation, final DoubtListe View view = inflater.inflate(R.layout.sure, null, false); TextView tvExplanation = view.findViewById(R.id.tvExplanation); tvExplanation.setText(explanation); - new AlertDialog.Builder(context) + new MaterialAlertDialogBuilder(context) .setView(view) .setCancelable(true) .setPositiveButton(android.R.string.yes, new DialogInterface.OnClickListener() { diff --git a/app/src/main/java/net/kollnig/missioncontrol/ActivityBlocklists.java b/app/src/main/java/net/kollnig/missioncontrol/ActivityBlocklists.java index 078277988..6d2697b66 100644 --- a/app/src/main/java/net/kollnig/missioncontrol/ActivityBlocklists.java +++ b/app/src/main/java/net/kollnig/missioncontrol/ActivityBlocklists.java @@ -14,6 +14,8 @@ import androidx.annotation.NonNull; import androidx.appcompat.app.AlertDialog; + +import com.google.android.material.dialog.MaterialAlertDialogBuilder; import androidx.appcompat.app.AppCompatActivity; import com.google.android.material.materialswitch.MaterialSwitch; import androidx.recyclerview.widget.LinearLayoutManager; @@ -68,7 +70,7 @@ public boolean onOptionsItemSelected(MenuItem item) { } private void showAddDialog(Blocklist item) { - AlertDialog.Builder builder = new AlertDialog.Builder(this); + AlertDialog.Builder builder = new MaterialAlertDialogBuilder(this); builder.setTitle(item == null ? R.string.title_add_blocklist : R.string.title_blocklists); final EditText input = new EditText(this); @@ -162,7 +164,7 @@ public void onBindViewHolder(@NonNull ViewHolder holder, int position) { holder.itemView.setOnClickListener(v -> showAddDialog(item)); holder.btnDelete.setOnClickListener(v -> { - new AlertDialog.Builder(context) + new MaterialAlertDialogBuilder(context) .setTitle(R.string.title_delete_blocklist) .setMessage(R.string.msg_delete_blocklist_confirm) .setPositiveButton(android.R.string.yes, (dialog, which) -> { diff --git a/app/src/main/java/net/kollnig/missioncontrol/DetailsActivity.java b/app/src/main/java/net/kollnig/missioncontrol/DetailsActivity.java index 06e242f78..39ec95cca 100644 --- a/app/src/main/java/net/kollnig/missioncontrol/DetailsActivity.java +++ b/app/src/main/java/net/kollnig/missioncontrol/DetailsActivity.java @@ -26,7 +26,6 @@ import android.content.Intent; import android.content.SharedPreferences; import android.database.Cursor; -import android.graphics.drawable.ColorDrawable; import android.net.Uri; import android.os.AsyncTask; import android.os.Bundle; @@ -40,12 +39,9 @@ import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.widget.Toolbar; import androidx.core.app.NavUtils; -import androidx.core.content.ContextCompat; import androidx.core.graphics.Insets; import androidx.core.view.ViewCompat; -import androidx.core.view.WindowCompat; import androidx.core.view.WindowInsetsCompat; -import androidx.core.view.WindowInsetsControllerCompat; import androidx.viewpager2.widget.ViewPager2; import com.google.android.material.appbar.AppBarLayout; @@ -122,22 +118,9 @@ protected void onActivityResult(int requestCode, int resultCode, final Intent da @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - // Enable edge-to-edge content - WindowCompat.setDecorFitsSystemWindows(getWindow(), false); + // Edge-to-edge is enabled globally by ApplicationEx via EdgeToEdge.enable() setContentView(R.layout.activity_details); - // Status bar appearance - WindowInsetsControllerCompat insetsController = new WindowInsetsControllerCompat(getWindow(), - getWindow().getDecorView()); - insetsController.setAppearanceLightStatusBars(false); - if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.VANILLA_ICE_CREAM) { - getWindow().setStatusBarColor(ContextCompat.getColor(this, R.color.colorPrimaryDark)); - } - - // Set window background to primary dark color to show behind the transparent - // status bar - getWindow().setBackgroundDrawable(new ColorDrawable(ContextCompat.getColor(this, R.color.colorPrimaryDark))); - running = true; // Receive about details diff --git a/app/src/main/java/net/kollnig/missioncontrol/details/ActionsFragment.java b/app/src/main/java/net/kollnig/missioncontrol/details/ActionsFragment.java index bd11eca6d..949cad3fa 100644 --- a/app/src/main/java/net/kollnig/missioncontrol/details/ActionsFragment.java +++ b/app/src/main/java/net/kollnig/missioncontrol/details/ActionsFragment.java @@ -30,6 +30,8 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.AlertDialog; + +import com.google.android.material.dialog.MaterialAlertDialogBuilder; import androidx.fragment.app.Fragment; import com.google.android.material.snackbar.Snackbar; @@ -136,7 +138,7 @@ public void onClick(View v) { Context c = getContext(); if (c == null) return; - AlertDialog.Builder builder = new AlertDialog.Builder(c) + AlertDialog.Builder builder = new MaterialAlertDialogBuilder(c) .setTitle(R.string.external_servers) .setMessage(R.string.confirm_google_info) .setPositiveButton(R.string.yes, (dialog, id2) -> { diff --git a/app/src/main/res/layout/activity_onboarding.xml b/app/src/main/res/layout/activity_onboarding.xml index 95556582f..dc4a4312d 100644 --- a/app/src/main/res/layout/activity_onboarding.xml +++ b/app/src/main/res/layout/activity_onboarding.xml @@ -25,7 +25,7 @@