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-09-30 11:26:40 +02:00
parent 216a9dbb8d
commit 2404b2bee6
25 changed files with 114 additions and 24 deletions
+1
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'
@@ -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" >
@@ -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);
}
@@ -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)
{
@@ -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);
@@ -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)
@@ -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);
@@ -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);
@@ -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
@@ -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);
@@ -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);
@@ -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);
@@ -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());
@@ -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 */
@@ -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;
});
}
}
@@ -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"
@@ -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"
@@ -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>
@@ -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"
@@ -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"
@@ -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"
@@ -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"
@@ -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>
@@ -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"
@@ -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>