From 2404b2bee632d852a03f710bf4e32ee4bddd9339 Mon Sep 17 00:00:00 2001 From: Tobias Brunner Date: Mon, 4 Aug 2025 14:35:11 +0200 Subject: [PATCH] android: Apply UI changes for edge-to-edge views in Android 15+ When targeting Android 15, edge-to-edge is the default and when targeting Android 16, apps can't opt-out from this anymore. So we update our views and enable edge-to-edge also for older versions (avoids the black bar behind the system UI at the bottom). For most views we just use automatic margins via android:fitsSystemWindows (or programmatically via setDecorFitsSystemWindows). However, for the profile lists and log views, we take some extra measures that allow the lists to go behind the bottom system UI. Appropriate padding is applied at the bottom of the lists so the last item(s) can be scrolled into full view. --- src/frontends/android/app/build.gradle | 1 + .../android/app/src/main/AndroidManifest.xml | 1 + .../strongswan/android/ui/LogActivity.java | 4 ++ .../strongswan/android/ui/LogFragment.java | 3 ++ .../strongswan/android/ui/MainActivity.java | 4 ++ .../ui/RemediationInstructionsActivity.java | 3 ++ .../ui/SelectedApplicationsActivity.java | 3 ++ .../android/ui/SettingsActivity.java | 3 ++ .../ui/TrustedCertificateImportActivity.java | 1 + .../ui/TrustedCertificatesActivity.java | 3 ++ .../android/ui/VpnProfileDetailActivity.java | 2 + .../android/ui/VpnProfileImportActivity.java | 2 + .../android/ui/VpnProfileListFragment.java | 3 ++ .../android/ui/VpnProfileSelectActivity.java | 4 ++ .../org/strongswan/android/utils/Utils.java | 42 +++++++++++++++++++ .../layout-large/remediation_instructions.xml | 3 +- .../app/src/main/res/layout/log_activity.xml | 3 +- .../app/src/main/res/layout/log_fragment.xml | 15 +++---- .../android/app/src/main/res/layout/main.xml | 3 +- .../main/res/layout/profile_detail_view.xml | 3 +- .../main/res/layout/profile_import_view.xml | 7 ++-- .../main/res/layout/profile_list_fragment.xml | 8 ++-- .../res/layout/remediation_instructions.xml | 3 +- .../layout/trusted_certificates_activity.xml | 9 ++-- .../main/res/layout/vpn_profile_select.xml | 5 ++- 25 files changed, 114 insertions(+), 24 deletions(-) diff --git a/src/frontends/android/app/build.gradle b/src/frontends/android/app/build.gradle index 6db309f15..8991567a9 100644 --- a/src/frontends/android/app/build.gradle +++ b/src/frontends/android/app/build.gradle @@ -46,6 +46,7 @@ android { dependencies { implementation 'androidx.appcompat:appcompat:1.7.1' + implementation 'androidx.core:core:1.17.0' implementation 'androidx.lifecycle:lifecycle-process:2.9.4' implementation 'androidx.preference:preference:1.2.1' implementation 'com.google.android.material:material:1.13.0' diff --git a/src/frontends/android/app/src/main/AndroidManifest.xml b/src/frontends/android/app/src/main/AndroidManifest.xml index 51c6ff89e..c0a8ed484 100644 --- a/src/frontends/android/app/src/main/AndroidManifest.xml +++ b/src/frontends/android/app/src/main/AndroidManifest.xml @@ -36,6 +36,7 @@ android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:theme="@style/ApplicationTheme" + android:windowSoftInputMode="adjustResize" android:networkSecurityConfig="@xml/network_security_config" android:enableOnBackInvokedCallback="true" android:allowBackup="false" > diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/ui/LogActivity.java b/src/frontends/android/app/src/main/java/org/strongswan/android/ui/LogActivity.java index 40738fe63..c7d052ecd 100644 --- a/src/frontends/android/app/src/main/java/org/strongswan/android/ui/LogActivity.java +++ b/src/frontends/android/app/src/main/java/org/strongswan/android/ui/LogActivity.java @@ -26,10 +26,12 @@ import android.widget.Toast; import org.strongswan.android.R; import org.strongswan.android.data.LogContentProvider; import org.strongswan.android.logic.CharonVpnService; +import org.strongswan.android.utils.Utils; import java.io.File; import androidx.appcompat.app.AppCompatActivity; +import androidx.core.view.WindowCompat; public class LogActivity extends AppCompatActivity { @@ -38,6 +40,8 @@ public class LogActivity extends AppCompatActivity { super.onCreate(savedInstanceState); setContentView(R.layout.log_activity); + WindowCompat.enableEdgeToEdge(getWindow()); + Utils.applyWindowInsetsAsMarginsForLists(findViewById(R.id.layout)); getSupportActionBar().setDisplayHomeAsUpEnabled(true); } diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/ui/LogFragment.java b/src/frontends/android/app/src/main/java/org/strongswan/android/ui/LogFragment.java index ca873a7fd..d83e1ec4a 100644 --- a/src/frontends/android/app/src/main/java/org/strongswan/android/ui/LogFragment.java +++ b/src/frontends/android/app/src/main/java/org/strongswan/android/ui/LogFragment.java @@ -30,6 +30,7 @@ import android.widget.ListView; import org.strongswan.android.R; import org.strongswan.android.logic.CharonVpnService; +import org.strongswan.android.utils.Utils; import java.io.BufferedReader; import java.io.File; @@ -81,6 +82,8 @@ public class LogFragment extends Fragment mLog = view.findViewById(R.id.log); mLog.setAdapter(mLogAdapter); + Utils.applyWindowInsetsAsPaddingForLists(mLog); + mScrollPosition = -1; if (savedInstanceState != null) { diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/ui/MainActivity.java b/src/frontends/android/app/src/main/java/org/strongswan/android/ui/MainActivity.java index 603dedef9..a1e647265 100644 --- a/src/frontends/android/app/src/main/java/org/strongswan/android/ui/MainActivity.java +++ b/src/frontends/android/app/src/main/java/org/strongswan/android/ui/MainActivity.java @@ -34,6 +34,7 @@ import org.strongswan.android.data.VpnProfile; import org.strongswan.android.logic.StrongSwanApplication; import org.strongswan.android.logic.TrustedCertificateManager; import org.strongswan.android.ui.VpnProfileListFragment.OnVpnProfileSelectedListener; +import org.strongswan.android.utils.Utils; import java.io.File; import java.util.ArrayList; @@ -43,6 +44,7 @@ import androidx.appcompat.app.ActionBar; import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.app.AppCompatDialogFragment; +import androidx.core.view.WindowCompat; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentManager; import androidx.fragment.app.FragmentTransaction; @@ -66,6 +68,8 @@ public class MainActivity extends AppCompatActivity implements OnVpnProfileSelec { super.onCreate(savedInstanceState); setContentView(R.layout.main); + WindowCompat.enableEdgeToEdge(getWindow()); + Utils.applyWindowInsetsAsMarginsForLists(findViewById(R.id.layout)); ActionBar bar = getSupportActionBar(); bar.setDisplayShowHomeEnabled(true); diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/ui/RemediationInstructionsActivity.java b/src/frontends/android/app/src/main/java/org/strongswan/android/ui/RemediationInstructionsActivity.java index d1de55214..3e8bd03b1 100644 --- a/src/frontends/android/app/src/main/java/org/strongswan/android/ui/RemediationInstructionsActivity.java +++ b/src/frontends/android/app/src/main/java/org/strongswan/android/ui/RemediationInstructionsActivity.java @@ -18,6 +18,8 @@ package org.strongswan.android.ui; import android.os.Bundle; import androidx.appcompat.app.AppCompatActivity; +import androidx.core.view.WindowCompat; + import android.view.MenuItem; import org.strongswan.android.R; @@ -33,6 +35,7 @@ public class RemediationInstructionsActivity extends AppCompatActivity implement { super.onCreate(savedInstanceState); setContentView(R.layout.remediation_instructions); + WindowCompat.enableEdgeToEdge(getWindow()); getSupportActionBar().setDisplayHomeAsUpEnabled(true); if (savedInstanceState != null) diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/ui/SelectedApplicationsActivity.java b/src/frontends/android/app/src/main/java/org/strongswan/android/ui/SelectedApplicationsActivity.java index d1b66e4ee..b3c23ffc8 100644 --- a/src/frontends/android/app/src/main/java/org/strongswan/android/ui/SelectedApplicationsActivity.java +++ b/src/frontends/android/app/src/main/java/org/strongswan/android/ui/SelectedApplicationsActivity.java @@ -26,6 +26,7 @@ import androidx.activity.OnBackPressedCallback; import androidx.annotation.Nullable; import androidx.appcompat.app.ActionBar; import androidx.appcompat.app.AppCompatActivity; +import androidx.core.view.WindowCompat; import androidx.fragment.app.FragmentManager; public class SelectedApplicationsActivity extends AppCompatActivity @@ -37,6 +38,8 @@ public class SelectedApplicationsActivity extends AppCompatActivity protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); + WindowCompat.enableEdgeToEdge(getWindow()); + WindowCompat.setDecorFitsSystemWindows(getWindow(), true); ActionBar actionBar = getSupportActionBar(); actionBar.setDisplayHomeAsUpEnabled(true); diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/ui/SettingsActivity.java b/src/frontends/android/app/src/main/java/org/strongswan/android/ui/SettingsActivity.java index d1b5190e4..eec782fb7 100644 --- a/src/frontends/android/app/src/main/java/org/strongswan/android/ui/SettingsActivity.java +++ b/src/frontends/android/app/src/main/java/org/strongswan/android/ui/SettingsActivity.java @@ -20,6 +20,7 @@ import android.os.Bundle; import android.view.MenuItem; import androidx.appcompat.app.AppCompatActivity; +import androidx.core.view.WindowCompat; public class SettingsActivity extends AppCompatActivity { @@ -28,6 +29,8 @@ public class SettingsActivity extends AppCompatActivity protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); + WindowCompat.enableEdgeToEdge(getWindow()); + WindowCompat.setDecorFitsSystemWindows(getWindow(), true); getSupportActionBar().setDisplayHomeAsUpEnabled(true); diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/ui/TrustedCertificateImportActivity.java b/src/frontends/android/app/src/main/java/org/strongswan/android/ui/TrustedCertificateImportActivity.java index 322e21e2c..a794e93ba 100644 --- a/src/frontends/android/app/src/main/java/org/strongswan/android/ui/TrustedCertificateImportActivity.java +++ b/src/frontends/android/app/src/main/java/org/strongswan/android/ui/TrustedCertificateImportActivity.java @@ -41,6 +41,7 @@ import androidx.activity.result.contract.ActivityResultContracts; import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.app.AppCompatDialogFragment; +import androidx.core.view.WindowCompat; import androidx.fragment.app.FragmentTransaction; public class TrustedCertificateImportActivity extends AppCompatActivity diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/ui/TrustedCertificatesActivity.java b/src/frontends/android/app/src/main/java/org/strongswan/android/ui/TrustedCertificatesActivity.java index 0a0b26a8a..c9e242881 100644 --- a/src/frontends/android/app/src/main/java/org/strongswan/android/ui/TrustedCertificatesActivity.java +++ b/src/frontends/android/app/src/main/java/org/strongswan/android/ui/TrustedCertificatesActivity.java @@ -33,6 +33,7 @@ import org.strongswan.android.logic.TrustedCertificateManager; import org.strongswan.android.logic.TrustedCertificateManager.TrustedCertificateSource; import org.strongswan.android.security.TrustedCertificateEntry; import org.strongswan.android.ui.CertificateDeleteConfirmationDialog.OnCertificateDeleteListener; +import org.strongswan.android.utils.Utils; import java.security.KeyStore; @@ -41,6 +42,7 @@ import androidx.activity.result.contract.ActivityResultContracts; import androidx.annotation.NonNull; import androidx.appcompat.app.ActionBar; import androidx.appcompat.app.AppCompatActivity; +import androidx.core.view.WindowCompat; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentActivity; import androidx.viewpager2.adapter.FragmentStateAdapter; @@ -71,6 +73,7 @@ public class TrustedCertificatesActivity extends AppCompatActivity implements Tr { super.onCreate(savedInstanceState); setContentView(R.layout.trusted_certificates_activity); + WindowCompat.enableEdgeToEdge(getWindow()); ActionBar actionBar = getSupportActionBar(); actionBar.setDisplayHomeAsUpEnabled(true); diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/ui/VpnProfileDetailActivity.java b/src/frontends/android/app/src/main/java/org/strongswan/android/ui/VpnProfileDetailActivity.java index cf1789d4b..13e38273e 100644 --- a/src/frontends/android/app/src/main/java/org/strongswan/android/ui/VpnProfileDetailActivity.java +++ b/src/frontends/android/app/src/main/java/org/strongswan/android/ui/VpnProfileDetailActivity.java @@ -84,6 +84,7 @@ import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.app.AppCompatDialogFragment; import androidx.appcompat.widget.SwitchCompat; import androidx.core.text.HtmlCompat; +import androidx.core.view.WindowCompat; import androidx.localbroadcastmanager.content.LocalBroadcastManager; public class VpnProfileDetailActivity extends AppCompatActivity @@ -199,6 +200,7 @@ public class VpnProfileDetailActivity extends AppCompatActivity mDataSource.open(); setContentView(R.layout.profile_detail_view); + WindowCompat.enableEdgeToEdge(getWindow()); mManagedProfile = findViewById(R.id.managed_profile); diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/ui/VpnProfileImportActivity.java b/src/frontends/android/app/src/main/java/org/strongswan/android/ui/VpnProfileImportActivity.java index ee56c1e53..f10ce0e14 100644 --- a/src/frontends/android/app/src/main/java/org/strongswan/android/ui/VpnProfileImportActivity.java +++ b/src/frontends/android/app/src/main/java/org/strongswan/android/ui/VpnProfileImportActivity.java @@ -78,6 +78,7 @@ import javax.net.ssl.SSLHandshakeException; import androidx.activity.result.ActivityResultLauncher; import androidx.activity.result.contract.ActivityResultContracts; import androidx.appcompat.app.AppCompatActivity; +import androidx.core.view.WindowCompat; import androidx.loader.app.LoaderManager; import androidx.loader.content.AsyncTaskLoader; import androidx.loader.content.Loader; @@ -204,6 +205,7 @@ public class VpnProfileImportActivity extends AppCompatActivity mDataSource.open(); setContentView(R.layout.profile_import_view); + WindowCompat.enableEdgeToEdge(getWindow()); mProgressBar = findViewById(R.id.progress_bar); mExistsWarning = findViewById(R.id.exists_warning); diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/ui/VpnProfileListFragment.java b/src/frontends/android/app/src/main/java/org/strongswan/android/ui/VpnProfileListFragment.java index 1679ef5bd..e845cc659 100644 --- a/src/frontends/android/app/src/main/java/org/strongswan/android/ui/VpnProfileListFragment.java +++ b/src/frontends/android/app/src/main/java/org/strongswan/android/ui/VpnProfileListFragment.java @@ -48,6 +48,7 @@ import org.strongswan.android.data.VpnProfileSource; import org.strongswan.android.logic.StrongSwanApplication; import org.strongswan.android.ui.adapter.VpnProfileAdapter; import org.strongswan.android.utils.Constants; +import org.strongswan.android.utils.Utils; import java.util.ArrayList; import java.util.HashSet; @@ -148,6 +149,8 @@ public class VpnProfileListFragment extends Fragment implements MenuProvider mListView.setEmptyView(view.findViewById(R.id.profile_list_empty)); mListView.setOnItemClickListener(mVpnProfileClicked); + Utils.applyWindowInsetsAsPaddingForLists(mListView); + if (!mReadOnly) { requireActivity().addMenuProvider(this, getViewLifecycleOwner()); diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/ui/VpnProfileSelectActivity.java b/src/frontends/android/app/src/main/java/org/strongswan/android/ui/VpnProfileSelectActivity.java index dd64d0c75..56aefbede 100644 --- a/src/frontends/android/app/src/main/java/org/strongswan/android/ui/VpnProfileSelectActivity.java +++ b/src/frontends/android/app/src/main/java/org/strongswan/android/ui/VpnProfileSelectActivity.java @@ -22,11 +22,13 @@ import android.os.Bundle; import org.strongswan.android.R; import org.strongswan.android.data.VpnProfile; import org.strongswan.android.ui.VpnProfileListFragment.OnVpnProfileSelectedListener; +import org.strongswan.android.utils.Utils; import androidx.appcompat.app.AppCompatActivity; import androidx.core.content.pm.ShortcutInfoCompat; import androidx.core.content.pm.ShortcutManagerCompat; import androidx.core.graphics.drawable.IconCompat; +import androidx.core.view.WindowCompat; public class VpnProfileSelectActivity extends AppCompatActivity implements OnVpnProfileSelectedListener { @@ -35,6 +37,8 @@ public class VpnProfileSelectActivity extends AppCompatActivity implements OnVpn { super.onCreate(savedInstanceState); setContentView(R.layout.vpn_profile_select); + WindowCompat.enableEdgeToEdge(getWindow()); + Utils.applyWindowInsetsAsMarginsForLists(findViewById(R.id.layout)); /* we should probably return a result also if the user clicks the back * button before selecting a profile */ diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/utils/Utils.java b/src/frontends/android/app/src/main/java/org/strongswan/android/utils/Utils.java index 5142ee1b5..ac1a315c4 100644 --- a/src/frontends/android/app/src/main/java/org/strongswan/android/utils/Utils.java +++ b/src/frontends/android/app/src/main/java/org/strongswan/android/utils/Utils.java @@ -17,9 +17,16 @@ package org.strongswan.android.utils; +import android.view.View; +import android.view.ViewGroup; + import java.net.InetAddress; import java.net.UnknownHostException; +import androidx.core.graphics.Insets; +import androidx.core.view.ViewCompat; +import androidx.core.view.WindowInsetsCompat; + public class Utils { static final char[] HEXDIGITS = "0123456789abcdef".toCharArray(); @@ -75,4 +82,39 @@ public class Utils } return InetAddress.getByAddress(bytes); } + + /** + * Apply window insets for the system UI as margins except for the bottom, + * which is useful if the view ends with a list. WindowInsetsCompat.CONSUMED + * is not returned so padding can be applied to the list. + * + * @param view view to apply margins to + */ + public static void applyWindowInsetsAsMarginsForLists(View view) + { + ViewCompat.setOnApplyWindowInsetsListener(view, (v, windowInsets) -> { + Insets insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()); + ViewGroup.MarginLayoutParams mlp = (ViewGroup.MarginLayoutParams)v.getLayoutParams(); + mlp.topMargin = insets.top; + mlp.leftMargin = insets.left; + mlp.rightMargin = insets.right; + v.setLayoutParams(mlp); + return windowInsets; + }); + } + + /** + * Apply bottom inset for the system UI as padding on the given (list) view + * so the last item can be scrolled fully into view. + * + * @param view view to apply padding to + */ + public static void applyWindowInsetsAsPaddingForLists(View view) + { + ViewCompat.setOnApplyWindowInsetsListener(view, (v, windowInsets) -> { + Insets insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()); + v.setPaddingRelative(0, 0, 0, insets.bottom); + return WindowInsetsCompat.CONSUMED; + }); + } } diff --git a/src/frontends/android/app/src/main/res/layout-large/remediation_instructions.xml b/src/frontends/android/app/src/main/res/layout-large/remediation_instructions.xml index 1c6494f6e..a2db35116 100644 --- a/src/frontends/android/app/src/main/res/layout-large/remediation_instructions.xml +++ b/src/frontends/android/app/src/main/res/layout-large/remediation_instructions.xml @@ -18,7 +18,8 @@ android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="horizontal" - android:baselineAligned="false" > + android:baselineAligned="false" + android:fitsSystemWindows="true" > + android:layout_height="match_parent" + android:id="@+id/layout" > - + android:paddingBottom="0dp" + android:paddingTop="5dp" + android:paddingStart="5dp" + android:paddingEnd="5dp" > + android:transcriptMode="normal" + android:clipToPadding="false" /> - - - + diff --git a/src/frontends/android/app/src/main/res/layout/main.xml b/src/frontends/android/app/src/main/res/layout/main.xml index 2aaf181b2..06932866f 100644 --- a/src/frontends/android/app/src/main/res/layout/main.xml +++ b/src/frontends/android/app/src/main/res/layout/main.xml @@ -17,7 +17,8 @@ + android:orientation="vertical" + android:id="@+id/layout" > + android:layout_height="match_parent" + android:fitsSystemWindows="true"> + xmlns:app="http://schemas.android.com/apk/res-auto" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:fitsSystemWindows="true" > @@ -28,9 +28,11 @@ android:layout_height="match_parent" android:dividerHeight="1dp" android:divider="?android:attr/listDivider" - android:scrollbarAlwaysDrawVerticalTrack="true" /> + android:overScrollFooter="@android:color/transparent" + android:scrollbarAlwaysDrawVerticalTrack="true" + android:clipToPadding="false" /> - - \ No newline at end of file + diff --git a/src/frontends/android/app/src/main/res/layout/trusted_certificates_activity.xml b/src/frontends/android/app/src/main/res/layout/trusted_certificates_activity.xml index 1eee0590d..ee7ec1901 100644 --- a/src/frontends/android/app/src/main/res/layout/trusted_certificates_activity.xml +++ b/src/frontends/android/app/src/main/res/layout/trusted_certificates_activity.xml @@ -15,10 +15,11 @@ for more details. --> + xmlns:app="http://schemas.android.com/apk/res-auto" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="vertical" + android:fitsSystemWindows="true"> + android:orientation="vertical" + android:id="@+id/layout" > - \ No newline at end of file +