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.
This commit is contained in:
Tobias Brunner 2025-08-04 14:35:11 +02:00
parent 216a9dbb8d
commit 2404b2bee6
25 changed files with 114 additions and 24 deletions

View File

@ -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'

View File

@ -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" >

View File

@ -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);
}

View File

@ -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)
{

View File

@ -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);

View File

@ -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)

View File

@ -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);

View File

@ -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);

View File

@ -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

View File

@ -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);

View File

@ -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);

View File

@ -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);

View File

@ -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());

View File

@ -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 */

View File

@ -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;
});
}
}

View File

@ -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" >
<fragment
class="org.strongswan.android.ui.RemediationInstructionsFragment"

View File

@ -16,7 +16,8 @@
-->
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent" >
android:layout_height="match_parent"
android:id="@+id/layout" >
<fragment
class="org.strongswan.android.ui.LogFragment"

View File

@ -14,23 +14,24 @@
or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
for more details.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical" >
android:paddingBottom="0dp"
android:paddingTop="5dp"
android:paddingStart="5dp"
android:paddingEnd="5dp" >
<ListView
android:id="@+id/log"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_margin="10dp"
android:dividerHeight="0dp"
android:divider="@null"
android:fadeScrollbars="false"
android:scrollbarFadeDuration="0"
android:scrollbarAlwaysDrawVerticalTrack="true"
android:transcriptMode="normal">
android:transcriptMode="normal"
android:clipToPadding="false" />
</ListView>
</LinearLayout>
</FrameLayout>

View File

@ -17,7 +17,8 @@
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical" >
android:orientation="vertical"
android:id="@+id/layout" >
<fragment
class="org.strongswan.android.ui.VpnStateFragment"

View File

@ -20,7 +20,8 @@
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
android:layout_height="match_parent"
android:fitsSystemWindows="true">
<TextView
android:id="@+id/managed_profile"

View File

@ -15,9 +15,10 @@
for more details.
-->
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent" >
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true" >
<LinearLayout
android:layout_width="match_parent"

View File

@ -17,7 +17,7 @@
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="10dp"
android:paddingBottom="0dp"
android:paddingTop="10dp"
android:paddingStart="5dp"
android:paddingEnd="5dp" >
@ -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" />
<TextView android:id="@+id/profile_list_empty"
<TextView android:id="@+id/profile_list_empty"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginStart="15dp"

View File

@ -17,6 +17,7 @@
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
android:id="@+id/fragment_container">
</FrameLayout>
</FrameLayout>

View File

@ -15,10 +15,11 @@
for more details.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
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">
<com.google.android.material.tabs.TabLayout
android:id="@+id/tabs"

View File

@ -18,7 +18,8 @@
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical" >
android:orientation="vertical"
android:id="@+id/layout" >
<fragment
class="org.strongswan.android.ui.VpnProfileListFragment"
@ -28,4 +29,4 @@
android:layout_weight="1"
app:read_only="true" />
</LinearLayout>
</LinearLayout>