From: Tobias Brunner Date: Mon, 4 Aug 2025 12:35:11 +0000 (+0200) Subject: android: Apply UI changes for edge-to-edge views in Android 15+ X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=2f358d7edd061af33c63677a990e75e7f47b6e75;p=thirdparty%2Fstrongswan.git 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. --- diff --git a/src/frontends/android/app/build.gradle b/src/frontends/android/app/build.gradle index c9023aa9f8..f660312e0f 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-rc01' implementation 'androidx.lifecycle:lifecycle-process:2.9.2' implementation 'androidx.preference:preference:1.2.1' implementation 'com.google.android.material:material:1.12.0' diff --git a/src/frontends/android/app/src/main/AndroidManifest.xml b/src/frontends/android/app/src/main/AndroidManifest.xml index 51c6ff89e3..c0a8ed4840 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 40738fe636..c7d052ecdd 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 ca873a7fd8..d83e1ec4a0 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 603dedef94..a1e647265c 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 d1de552145..3e8bd03b12 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 d1b66e4eea..b3c23ffc86 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 d1b5190e42..eec782fb71 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 322e21e2c0..a794e93bae 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 0a0b26a8a4..c9e2428810 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 cf1789d4b9..13e38273e6 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 ee56c1e530..f10ce0e14b 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 1679ef5bd3..e845cc6598 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 dd64d0c759..56aefbede6 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 5142ee1b59..ac1a315c44 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 1c6494f6e1..a2db35116f 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 2aaf181b2a..06932866f2 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 1eee0590df..ee7ec19013 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 +