Skip to content

Commit 2b45933

Browse files
authored
fix: [3914] Fix safe area insets on android 34 (#3915)
* fix: [3914] Fix safe area insets on android 34 This fixes both safe area insets and form bottom padding editing on android 34. By trial and error, I found that, with respect to immersive mode, android 34 behaves like android 33 and earlier unless we explicitly enable it by calling WindowCompat.setDecorFitsSystemWindows(window, false), which we don't do. And it is not possible to detect whether this has been called. This patch adjusts immersive heuristics to check for API 35+ instead of 34+ when determining if we are drawing edge to edge on the screen. This impacts the safe area insets implementation as well as form bottom padding editing. As part of this, I have re-enabled the android.useSafeAreaInsets display property by default so taht we use safe area insets by default. This can be opted out. The reason is that on API 35+ it needs this setting to work properly. * fix extra bottom padding when ime is visible
1 parent 3168db3 commit 2b45933

File tree

3 files changed

+101
-50
lines changed

3 files changed

+101
-50
lines changed

Ports/Android/src/com/codename1/impl/android/AndroidImplementation.java

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1292,11 +1292,56 @@ public int getDeviceDensity() {
12921292
return Display.DENSITY_MEDIUM;
12931293
}
12941294

1295+
public static boolean isImmersive() {
1296+
if (getActivity() == null) {
1297+
return false;
1298+
}
1299+
return isImmersive(getActivity().getWindow());
1300+
}
1301+
public static boolean isImmersive(Window window) {
1302+
if (Build.VERSION.SDK_INT >= 35) {
1303+
// Android 15+ is always immersive (overlay mode by default)
1304+
return true;
1305+
}
1306+
// On Android 34 and below, we can't detect decorFitsSystemWindows
1307+
// reliably at runtime. So the app must make the decision explicitly.
1308+
return false;
1309+
}
1310+
public static Rect getSystemBarInsets(final View rootView) {
1311+
final Rect result = new Rect(0, 0, 0, 0);
1312+
try {
1313+
Object insets = View.class
1314+
.getMethod("getRootWindowInsets")
1315+
.invoke(rootView);
1316+
if (insets == null) return result;
1317+
// Get android.view.WindowInsets$Type.systemBars()
1318+
Class typeClass = Class.forName("android.view.WindowInsets$Type");
1319+
int systemBarsMask = ((Integer) typeClass
1320+
.getMethod("systemBars")
1321+
.invoke(null)).intValue();
1322+
// Call insets.getInsets(int)
1323+
Object insetsObject = insets.getClass()
1324+
.getMethod("getInsets", new Class[]{int.class})
1325+
.invoke(insets, new Object[]{systemBarsMask});
1326+
if (insetsObject == null) return result;
1327+
Class insetsClass = insetsObject.getClass();
1328+
int left = ((Integer) insetsClass.getField("left").get(insetsObject)).intValue();
1329+
int top = ((Integer) insetsClass.getField("top").get(insetsObject)).intValue();
1330+
int right = ((Integer) insetsClass.getField("right").get(insetsObject)).intValue();
1331+
int bottom = ((Integer) insetsClass.getField("bottom").get(insetsObject)).intValue();
1332+
result.set(left, top, right, bottom);
1333+
} catch (Throwable t) {
1334+
t.printStackTrace(); // Optional: log this or suppress if expected
1335+
}
1336+
return result;
1337+
}
1338+
1339+
12951340
public Rectangle getDisplaySafeArea(Rectangle rect) {
12961341
if (rect == null) {
12971342
rect = new Rectangle();
12981343
}
1299-
if (getProperty("android.useSafeAreaInsets", "false").equals("false")) {
1344+
if (getProperty("android.useSafeAreaInsets", "true").equals("false")) {
13001345
return super.getDisplaySafeArea(rect);
13011346
}
13021347
if (this.myView != null) {

Ports/Android/src/com/codename1/impl/android/CodenameOneView.java

Lines changed: 54 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,7 @@ public void visibilityChangedTo(boolean visible) {
164164

165165
private void updateSafeArea() {
166166
final Activity activity = CodenameOneView.this.implementation.getActivity();
167+
167168
final Rect rect = this.safeArea;
168169
final View rootView = activity.getWindow().getDecorView();
169170
if (Build.VERSION.SDK_INT >= VERSION_CODE_P) {
@@ -182,10 +183,59 @@ private void updateSafeArea() {
182183
Method getSafeInsetRight = displayCutoutClass.getMethod("getSafeInsetRight");
183184
Method getSafeInsetBottom = displayCutoutClass.getMethod("getSafeInsetBottom");
184185

185-
rect.left = ((Integer) getSafeInsetLeft.invoke(cutout)).intValue();
186-
rect.top = ((Integer) getSafeInsetTop.invoke(cutout)).intValue();
187-
rect.right = ((Integer) getSafeInsetRight.invoke(cutout)).intValue();
188-
rect.bottom = ((Integer) getSafeInsetBottom.invoke(cutout)).intValue();
186+
int left = ((Integer) getSafeInsetLeft.invoke(cutout)).intValue();
187+
int top = ((Integer) getSafeInsetTop.invoke(cutout)).intValue();
188+
int right = ((Integer) getSafeInsetRight.invoke(cutout)).intValue();
189+
int bottom = ((Integer) getSafeInsetBottom.invoke(cutout)).intValue();
190+
boolean imeVisible = false;
191+
try {
192+
Method isVisibleMethod = insets.getClass().getMethod("isVisible", int.class);
193+
Class<?> typeClass = Class.forName("android.view.WindowInsets$Type");
194+
int imeType = ((Integer) typeClass.getMethod("ime").invoke(null)).intValue();
195+
imeVisible = (Boolean) isVisibleMethod.invoke(insets, imeType);
196+
} catch (Throwable t) {
197+
// Fallback or log
198+
}
199+
200+
Rect systemBarInsets = AndroidImplementation.getSystemBarInsets(rootView);
201+
top = Math.max(systemBarInsets.top, top);
202+
if (imeVisible) {
203+
// Avoid double-counting the bottom gesture bar
204+
bottom = Math.max(bottom, 0);
205+
} else {
206+
bottom = Math.max(systemBarInsets.bottom, bottom);
207+
}
208+
left = Math.max(systemBarInsets.left, left);
209+
right = Math.max(systemBarInsets.right, right);
210+
211+
if (!AndroidImplementation.isImmersive()) {
212+
top -= systemBarInsets.top;
213+
if (!imeVisible) {
214+
bottom -= systemBarInsets.bottom;
215+
}
216+
left -= systemBarInsets.left;
217+
right -= systemBarInsets.right;
218+
}
219+
220+
// Only apply if at least one is non-zero
221+
if (left != 0 || top != 0 || right != 0 || bottom != 0) {
222+
boolean isChanged = rect.left != left
223+
|| rect.right != right
224+
|| rect.top != top
225+
|| rect.bottom != bottom;
226+
rect.left = left;
227+
rect.top = top;
228+
rect.right = right;
229+
rect.bottom = bottom;
230+
231+
if (isChanged) {
232+
Display.getInstance().callSerially(new Runnable() {
233+
public void run() {
234+
AndroidImplementation.getInstance().revalidate();
235+
}
236+
});
237+
}
238+
}
189239
}
190240
}
191241
} catch (Exception e) {

Ports/Android/src/com/codename1/impl/android/InPlaceEditView.java

Lines changed: 1 addition & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1398,51 +1398,7 @@ private static void setEditMode(final boolean resize) {
13981398
}
13991399

14001400
private static boolean isImmersive(Window window) {
1401-
View decor = window.getDecorView();
1402-
1403-
// ---------- Modern branch : API 30+ ----------
1404-
if (Build.VERSION.SDK_INT >= 30) {
1405-
try {
1406-
// WindowInsets insets = decor.getRootWindowInsets();
1407-
Object insets = View.class
1408-
.getMethod("getRootWindowInsets")
1409-
.invoke(decor);
1410-
if (insets == null) return false;
1411-
1412-
// int mask = WindowInsets.Type.systemBars();
1413-
Class<?> typeCls =
1414-
Class.forName("android.view.WindowInsets$Type");
1415-
int systemBars = (Integer) typeCls
1416-
.getMethod("systemBars")
1417-
.invoke(null);
1418-
1419-
// boolean barsVisible = insets.isVisible(mask);
1420-
boolean barsVisible = (Boolean) insets.getClass()
1421-
.getMethod("isVisible", int.class)
1422-
.invoke(insets, systemBars);
1423-
1424-
/* --------------------------------------------------
1425-
* Edge-to-edge is guaranteed on API 35+ anyway, and
1426-
* `getDecorFitsSystemWindows()` no longer exists on
1427-
* API 36. We treat everything >=30 as edge-to-edge
1428-
* (decorFits = false) and only look at bar visibility
1429-
* to decide if we’re *immersive*.
1430-
* -------------------------------------------------- */
1431-
return !barsVisible; // immersive ⇢ bars are hidden
1432-
} catch (Throwable ignore) {
1433-
System.out.println("Error: " + ignore);
1434-
}
1435-
}
1436-
1437-
// ---------- Legacy branch : API 19-29 ----------
1438-
int f = decor.getSystemUiVisibility();
1439-
boolean immersiveBits =
1440-
(f & View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY) != 0 ||
1441-
(f & View.SYSTEM_UI_FLAG_IMMERSIVE) != 0;
1442-
boolean barsHidden =
1443-
(f & View.SYSTEM_UI_FLAG_HIDE_NAVIGATION) != 0 &&
1444-
(f & View.SYSTEM_UI_FLAG_FULLSCREEN) != 0;
1445-
return immersiveBits && barsHidden;
1401+
return AndroidImplementation.isImmersive(window);
14461402
}
14471403

14481404
private static void applyImeInsetPaddingReflection(View rootView) {

0 commit comments

Comments
 (0)