Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions controller/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -73,12 +73,13 @@
the IME. singleTop lets the keep-alive notification's SINGLE_TOP intent reuse the running
instance. Not exported: launched only in-app (redesign Settings, the terminal's own
notification PendingIntent). -->
<!-- K2GO-439: no adjustResize; the global edge-to-edge handler pads the IME inset (padAllAndIme),
so a single mechanism keeps the terminal input above the keyboard. -->
<activity
android:name=".TerminalActivity"
android:exported="false"
android:launchMode="singleTop"
android:configChanges="orientation|screenSize|screenLayout|keyboardHidden"
android:windowSoftInputMode="adjustResize" />
android:configChanges="orientation|screenSize|screenLayout|keyboardHidden" />

<activity
android:name=".PortalActivity"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,9 @@ public void onCreate() {
AnalyticsClient.with(this).applyConsent();
AnalyticsClient.with(this).logFirstRunIfNeeded();
registerActivityLifecycleCallbacks(new ForegroundTracker());
// K2GO-439: one owner pads every activity's content by the system-bar insets (targetSdk 35
// edge-to-edge); screens that manage their own insets opt out via EdgeToEdge.SelfManaged.
registerActivityLifecycleCallbacks(new org.appdevforall.k2go.ui.EdgeToEdgeCallbacks());
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@
import androidx.core.content.ContextCompat;


public class SplashActivity extends AppCompatActivity {
public class SplashActivity extends AppCompatActivity
implements org.appdevforall.k2go.ui.EdgeToEdge.SelfManaged {

private static final long EXIT_AT_MS = 3100L;
private static final long EXIT_FADE_MS = 400L;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,8 @@
* add one and for the rootfs-served docs. In-manual links stay in the WebView; external links open
* in the system browser.
*/
public class HelpViewerActivity extends AppCompatActivity {
public class HelpViewerActivity extends AppCompatActivity
implements org.appdevforall.k2go.ui.EdgeToEdge.SelfManaged {

private static final String TAG = "K2Go-Help";
private static final String APPASSETS_HOST = "appassets.androidplatform.net";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@
* (Lottie) that flips to OPEN once the server is reachable.
* Phase 2 = runtime gate. Content cards, wizard and Step-2 land in later phases.
*/
public class LibraryActivity extends AppCompatActivity implements ServerController.Host {
public class LibraryActivity extends AppCompatActivity
implements ServerController.Host, org.appdevforall.k2go.ui.EdgeToEdge.SelfManaged {

private static final String TAG = "K2Go-Library";
private static final long GATE_SAFETY_MS = 25000L;
Expand Down Expand Up @@ -131,6 +132,14 @@ protected void onCreate(Bundle savedInstanceState) {

bottomNav = findViewById(R.id.k2go_bottom_nav);
railNav = findViewById(R.id.k2go_nav_rail);
// K2GO-439: targetSdk 35 forces edge-to-edge. Pad only the TOP of the fragment content for the
// status bar; the Material BottomNavigationView and NavigationRailView inset THEMSELVES for the
// navigation bar by default, so padding the container's bottom too would double the inset (a gap
// under the nav). The boot-gate overlay is a sibling and stays full-screen.
org.appdevforall.k2go.ui.EdgeToEdge.padTop(findViewById(R.id.k2go_nav_host));
// K2GO-439: the install overlay is a sibling of the content container (not inside it), so inset
// its bottom too, else its controls draw under the navigation bar during install.
org.appdevforall.k2go.ui.EdgeToEdge.padBottom(findViewById(R.id.k2go_install_progress));
NavigationBarView.OnItemSelectedListener navListener = item -> {
if (!navSyncing) {
currentTab = item.getItemId();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
/*
* ============================================================================
* Name : EdgeToEdge.java
* Author : AppDevForAll
* Copyright : Copyright (c) 2026 AppDevForAll
* Description : Shared system-bar inset handling (K2GO-439). targetSdk 35 forces
* edge-to-edge on Android 15+, so app content draws behind the
* status bar and the navigation bar. Each screen pads the right
* views by the system-bar (and display-cutout) insets so content
* keeps clear of the bars. Single source for the rule; the call
* site is per-screen because the activities share no base class.
* See https://developer.android.com/develop/ui/views/layout/edge-to-edge
* ============================================================================
*/
package org.appdevforall.k2go.ui;

import android.view.View;

import androidx.core.graphics.Insets;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowInsetsCompat;

/** Stateless helper that pads a view by the system-bar insets, preserving its original padding. */
public final class EdgeToEdge {

private EdgeToEdge() {}

/**
* Activities that manage their own system-bar insets, that want none (a full-screen splash),
* or whose layout already consumes insets (android:fitsSystemWindows, or an inset listener of
* their own) implement this to opt out of the global {@link EdgeToEdgeCallbacks} handler, so the
* insets are never applied twice.
*/
public interface SelfManaged {}

/** Pad the view's top by the status-bar (and top cutout) inset. */
public static void padTop(View v) { apply(v, true, false, false, false, false); }

/** Pad the view's bottom by the navigation-bar inset. */
public static void padBottom(View v) { apply(v, false, false, false, true, false); }

/** Pad the view on every system-bar edge (top, bottom, and the sides for cutouts/landscape). */
public static void padAll(View v) { apply(v, true, true, true, true, false); }

/**
* Like {@link #padAll} but the bottom edge also clears the soft keyboard (IME), so a focused
* field is not hidden. Use for a screen's own content, not for a bottom bar that should stay put.
*/
public static void padAllAndIme(View v) { apply(v, true, true, true, true, true); }

private static void apply(final View v, final boolean top, final boolean left,
final boolean right, final boolean bottom, final boolean ime) {
if (v == null) return;
// Capture the view's own padding once so repeated inset passes (e.g. rotation) never accumulate.
final int pl = v.getPaddingLeft();
final int pt = v.getPaddingTop();
final int pr = v.getPaddingRight();
final int pb = v.getPaddingBottom();
ViewCompat.setOnApplyWindowInsetsListener(v, (view, insets) -> {
int mask = WindowInsetsCompat.Type.systemBars() | WindowInsetsCompat.Type.displayCutout();
if (ime) mask |= WindowInsetsCompat.Type.ime();
Insets bars = insets.getInsets(mask);
int[] p = resolvePadding(pl, pt, pr, pb, bars.left, bars.top, bars.right, bars.bottom,
top, left, right, bottom);
view.setPadding(p[0], p[1], p[2], p[3]);
return insets;
});
// Insets may have already been delivered before the listener was attached; request a fresh pass.
ViewCompat.requestApplyInsets(v);
}

/**
* Pure rule: the new [left, top, right, bottom] padding = the original padding plus the inset on
* each selected edge. Adds to the captured original (never to the already-padded value), so an
* inset pass is idempotent. Package-private for unit testing.
*/
static int[] resolvePadding(int pl, int pt, int pr, int pb,
int il, int it, int ir, int ib,
boolean top, boolean left, boolean right, boolean bottom) {
return new int[]{
left ? pl + il : pl,
top ? pt + it : pt,
right ? pr + ir : pr,
bottom ? pb + ib : pb,
};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/*
* ============================================================================
* Name : EdgeToEdgeCallbacks.java
* Author : AppDevForAll
* Copyright : Copyright (c) 2026 AppDevForAll
* Description : One owner for edge-to-edge insets (K2GO-439). Registered from
* IIABApplication, it pads every activity's content by the system
* bars (and the IME) so nothing draws under the status / navigation
* bars or the keyboard on targetSdk 35 (Android 15+). Screens that
* manage their own insets (LibraryActivity), whose layout already
* consumes them (HelpViewerActivity via fitsSystemWindows), or want
* none (SplashActivity) opt out via EdgeToEdge.SelfManaged.
* PortalActivity stays in: its immersive mode zeroes the insets when
* it hides the bars, so the one handler covers both of its states.
* ============================================================================
*/
package org.appdevforall.k2go.ui;

import android.app.Activity;
import android.app.Application;
import android.os.Bundle;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

/** Applies system-bar and IME insets to each activity's content view, except opted-out screens. */
public final class EdgeToEdgeCallbacks implements Application.ActivityLifecycleCallbacks {

@Override
public void onActivityCreated(@NonNull Activity a, @Nullable Bundle savedInstanceState) {
if (a instanceof EdgeToEdge.SelfManaged) return;
// android.R.id.content is the frame that holds setContentView's view; padding it insets the screen.
// These screens have no bottom bar, so clearing the keyboard (IME) on the bottom is safe.
EdgeToEdge.padAllAndIme(a.findViewById(android.R.id.content));
}

@Override public void onActivityStarted(@NonNull Activity a) {}
@Override public void onActivityResumed(@NonNull Activity a) {}
@Override public void onActivityPaused(@NonNull Activity a) {}
@Override public void onActivityStopped(@NonNull Activity a) {}
@Override public void onActivitySaveInstanceState(@NonNull Activity a, @NonNull Bundle outState) {}
@Override public void onActivityDestroyed(@NonNull Activity a) {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package org.appdevforall.k2go.ui;

import static org.junit.Assert.assertArrayEquals;

import org.junit.Test;

/**
* Unit tests for {@link EdgeToEdge#resolvePadding} (K2GO-439): the pure rule that adds the selected
* inset edges to the view's original padding. Padding is [left, top, right, bottom].
*/
public class EdgeToEdgeTest {

@Test
public void padAllAddsEveryEdgeToTheOriginal() {
assertArrayEquals(new int[]{15, 25, 35, 45},
EdgeToEdge.resolvePadding(5, 5, 5, 5, 10, 20, 30, 40,
true, true, true, true));
}

@Test
public void padTopAddsOnlyTheTopEdge() {
assertArrayEquals(new int[]{5, 25, 5, 5},
EdgeToEdge.resolvePadding(5, 5, 5, 5, 10, 20, 30, 40,
true, false, false, false));
}

@Test
public void padBottomAddsOnlyTheBottomEdge() {
assertArrayEquals(new int[]{5, 5, 5, 45},
EdgeToEdge.resolvePadding(5, 5, 5, 5, 10, 20, 30, 40,
false, false, false, true));
}

@Test
public void noEdgesKeepsTheOriginalPadding() {
assertArrayEquals(new int[]{5, 5, 5, 5},
EdgeToEdge.resolvePadding(5, 5, 5, 5, 10, 20, 30, 40,
false, false, false, false));
}

@Test
public void addsToTheOriginalNotToZero() {
// The original padding is preserved and the inset is added on top (not replaced).
assertArrayEquals(new int[]{100, 107, 100, 100},
EdgeToEdge.resolvePadding(100, 100, 100, 100, 1, 7, 3, 9,
true, false, false, false));
}
}
Loading