diff --git a/modules/display/src/main/java/com/gluonhq/attach/display/DisplayService.java b/modules/display/src/main/java/com/gluonhq/attach/display/DisplayService.java index dbafa266..6fd77210 100644 --- a/modules/display/src/main/java/com/gluonhq/attach/display/DisplayService.java +++ b/modules/display/src/main/java/com/gluonhq/attach/display/DisplayService.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016, 2019 Gluon + * Copyright (c) 2016, 2025, Gluon * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -30,6 +30,7 @@ import com.gluonhq.attach.util.Services; import javafx.beans.property.ReadOnlyObjectProperty; import javafx.geometry.Dimension2D; +import javafx.geometry.Insets; import java.util.Optional; @@ -162,4 +163,15 @@ static Optional create() { * @since 3.8.0 */ ReadOnlyObjectProperty notchProperty(); + + /** + * Property that contains the insets of the system bars (typically + * the status bar at the top and the navigation bar at the bottom). + * These insets can be used to add the necessary padding so the application + * doesn't get underneath the content shown at the system bars. + * + * @return A read only property with the insets of the system bars + */ + ReadOnlyObjectProperty systemBarsInsetsProperty(); + } diff --git a/modules/display/src/main/java/com/gluonhq/attach/display/impl/AndroidDisplayService.java b/modules/display/src/main/java/com/gluonhq/attach/display/impl/AndroidDisplayService.java index 8288a395..3f4b8ba2 100644 --- a/modules/display/src/main/java/com/gluonhq/attach/display/impl/AndroidDisplayService.java +++ b/modules/display/src/main/java/com/gluonhq/attach/display/impl/AndroidDisplayService.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016, 2020 Gluon + * Copyright (c) 2016, 2025, Gluon * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -29,9 +29,11 @@ import com.gluonhq.attach.display.DisplayService; import com.gluonhq.attach.util.Util; +import javafx.application.Platform; import javafx.beans.property.ReadOnlyObjectProperty; import javafx.beans.property.ReadOnlyObjectWrapper; import javafx.geometry.Dimension2D; +import javafx.geometry.Insets; import javafx.geometry.Rectangle2D; import javafx.stage.Screen; @@ -42,6 +44,8 @@ public class AndroidDisplayService implements DisplayService { private static final Logger LOG = Logger.getLogger(AndroidDisplayService.class.getName()); + private static final ReadOnlyObjectWrapper insetsProperty = new ReadOnlyObjectWrapper<>(); + private static final boolean debug = Util.DEBUG; static { @@ -118,8 +122,18 @@ public ReadOnlyObjectProperty notchProperty() { return new ReadOnlyObjectWrapper<>(Notch.UNKNOWN).getReadOnlyProperty(); } + @Override + public ReadOnlyObjectProperty systemBarsInsetsProperty() { + return insetsProperty.getReadOnlyProperty(); + } + // native private native static boolean isPhoneFactor(); private native static double[] screenSize(); private native static boolean screenRound(); + + // callback + private static void notifyInsets(double top, double right, double bottom, double left) { + Platform.runLater(() -> insetsProperty.set(new Insets(top, right, bottom, left))); + } } \ No newline at end of file diff --git a/modules/display/src/main/java/com/gluonhq/attach/display/impl/DesktopDisplayService.java b/modules/display/src/main/java/com/gluonhq/attach/display/impl/DesktopDisplayService.java index 32648da1..e43a9b1e 100644 --- a/modules/display/src/main/java/com/gluonhq/attach/display/impl/DesktopDisplayService.java +++ b/modules/display/src/main/java/com/gluonhq/attach/display/impl/DesktopDisplayService.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016, 2019 Gluon + * Copyright (c) 2016, 2025, Gluon * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -31,6 +31,7 @@ import javafx.beans.property.ReadOnlyObjectProperty; import javafx.beans.property.ReadOnlyObjectWrapper; import javafx.geometry.Dimension2D; +import javafx.geometry.Insets; import javafx.geometry.Rectangle2D; import javafx.stage.Screen; @@ -41,6 +42,8 @@ public class DesktopDisplayService implements DisplayService { private static final Logger LOG = Logger.getLogger(DesktopDisplayService.class.getName()); + private static final ReadOnlyObjectWrapper insetsProperty = new ReadOnlyObjectWrapper<>(); + private final Dimension2D dimensions; public DesktopDisplayService() { @@ -101,6 +104,11 @@ public ReadOnlyObjectProperty notchProperty() { return new ReadOnlyObjectWrapper<>(Notch.UNKNOWN).getReadOnlyProperty(); } + @Override + public ReadOnlyObjectProperty systemBarsInsetsProperty() { + return insetsProperty.getReadOnlyProperty(); + } + private static void log(String message, Throwable cause) { LOG.log(Level.FINE, message); if (LOG.isLoggable(Level.FINE)) { diff --git a/modules/display/src/main/java/com/gluonhq/attach/display/impl/IOSDisplayService.java b/modules/display/src/main/java/com/gluonhq/attach/display/impl/IOSDisplayService.java index 1a05bf30..99cec6fd 100644 --- a/modules/display/src/main/java/com/gluonhq/attach/display/impl/IOSDisplayService.java +++ b/modules/display/src/main/java/com/gluonhq/attach/display/impl/IOSDisplayService.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016, 2019 Gluon + * Copyright (c) 2016, 2025, Gluon * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -34,6 +34,7 @@ import javafx.beans.property.ReadOnlyObjectProperty; import javafx.beans.property.ReadOnlyObjectWrapper; import javafx.geometry.Dimension2D; +import javafx.geometry.Insets; public class IOSDisplayService implements DisplayService { @@ -44,6 +45,8 @@ public class IOSDisplayService implements DisplayService { private static ReadOnlyObjectWrapper notch; + private static final ReadOnlyObjectWrapper insetsProperty = new ReadOnlyObjectWrapper<>(); + public IOSDisplayService() { notch = new ReadOnlyObjectWrapper<>(Notch.UNKNOWN); LifecycleService.create().ifPresent(l -> { @@ -100,6 +103,11 @@ public ReadOnlyObjectProperty notchProperty() { return notch.getReadOnlyProperty(); } + @Override + public ReadOnlyObjectProperty systemBarsInsetsProperty() { + return insetsProperty.getReadOnlyProperty(); + } + // native private static native void initDisplay(); @@ -119,4 +127,8 @@ private static void notifyDisplay(String o) { Platform.runLater(() -> notch.setValue(d)); } } + + private static void notifyInsets(double top, double right, double bottom, double left) { + Platform.runLater(() -> insetsProperty.set(new Insets(top, right, bottom, left))); + } } \ No newline at end of file diff --git a/modules/display/src/main/native/android/c/display.c b/modules/display/src/main/native/android/c/display.c index e5669dbb..80883e63 100644 --- a/modules/display/src/main/native/android/c/display.c +++ b/modules/display/src/main/native/android/c/display.c @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020, 2021, Gluon + * Copyright (c) 2020, 2025, Gluon * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -34,6 +34,10 @@ static jmethodID jDisplayServiceHeightMethod; static jmethodID jDisplayServiceFactorMethod; static jmethodID jDisplayServiceRoundMethod; +// Graal handles +static jclass jGraalDisplayClass; +static jmethodID jGraalNotifyInsetsMethod; + static void initializeDisplayDalvikHandles() { jDisplayServiceClass = GET_REGISTER_DALVIK_CLASS(jDisplayServiceClass, "com/gluonhq/helloandroid/DalvikDisplayService"); ATTACH_DALVIK(); @@ -49,6 +53,11 @@ static void initializeDisplayDalvikHandles() { DETACH_DALVIK(); } +static void initializeGraalHandles(JNIEnv* env) { + jGraalDisplayClass = (*env)->NewGlobalRef(env, (*env)->FindClass(env, "com/gluonhq/attach/display/impl/AndroidDisplayService")); + jGraalNotifyInsetsMethod = (*env)->GetStaticMethodID(env, jGraalDisplayClass, "notifyInsets", "(DDDD)V"); +} + ////////////////////////// // From Graal to native // ////////////////////////// @@ -65,6 +74,7 @@ JNI_OnLoad_display(JavaVM *vm, void *reserved) return JNI_FALSE; } ATTACH_LOG_FINE("[Display Service] Initializing native Display from OnLoad"); + initializeGraalHandles(graalEnv); initializeDisplayDalvikHandles(); return JNI_VERSION_1_8; #else @@ -109,3 +119,17 @@ JNIEXPORT jboolean JNICALL Java_com_gluonhq_attach_display_impl_AndroidDisplaySe DETACH_DALVIK(); return answer; } + +/////////////////////////// +// From Dalvik to native // +/////////////////////////// + +JNIEXPORT void JNICALL Java_com_gluonhq_helloandroid_DalvikDisplayService_notifyInsets( + JNIEnv *env, jobject service, jdouble top, jdouble right, jdouble bottom, jdouble left) { + if (isDebugAttach()) { + ATTACH_LOG_FINE("Native layer got new inset: %.1f,%.1f,%.1f,%.1f\n", top, right, bottom, left); + } + ATTACH_GRAAL(); + (*graalEnv)->CallStaticVoidMethod(graalEnv, jGraalDisplayClass, jGraalNotifyInsetsMethod, top, right, bottom, left); + DETACH_GRAAL(); +} \ No newline at end of file diff --git a/modules/display/src/main/native/android/dalvik/DalvikDisplayService.java b/modules/display/src/main/native/android/dalvik/DalvikDisplayService.java index f3dd3e64..2bf5c985 100644 --- a/modules/display/src/main/native/android/dalvik/DalvikDisplayService.java +++ b/modules/display/src/main/native/android/dalvik/DalvikDisplayService.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020, Gluon + * Copyright (c) 2020, 2025, Gluon * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -33,8 +33,14 @@ import android.util.DisplayMetrics; import android.util.Log; import android.view.Display; +import android.view.View; +import android.view.Window; import android.view.WindowManager; +import androidx.core.graphics.Insets; +import androidx.core.view.ViewCompat; +import androidx.core.view.WindowInsetsCompat; + public class DalvikDisplayService { private static final String TAG = Util.TAG; @@ -53,6 +59,16 @@ public DalvikDisplayService(Activity activity) { float yInches = metrics.heightPixels / metrics.ydpi; float xInches = metrics.widthPixels / metrics.xdpi; diagonalInches = Math.sqrt(xInches * xInches + yInches * yInches); + + Window window = activity.getWindow(); + View decorView = window.getDecorView(); + ViewCompat.setOnApplyWindowInsetsListener(decorView, (v, insets) -> { + notifyInsets(insets, metrics.density); + return insets; + }); + // Get initial insets + WindowInsetsCompat insets = ViewCompat.getRootWindowInsets(decorView); + notifyInsets(insets, metrics.density); } private boolean isPhoneFactor() { @@ -73,4 +89,15 @@ private boolean isScreenRound() { } return activity.getResources().getConfiguration().isScreenRound(); } + + private void notifyInsets(WindowInsetsCompat insets, double density) { + Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars()); + if (Util.isDebug()) { + Log.v(TAG, "Display got new insets: " + systemBars); + } + notifyInsets(systemBars.top / density, systemBars.right / density, + systemBars.bottom / density, systemBars.left / density); + } + + private native void notifyInsets(double top, double right, double bottom, double left); } \ No newline at end of file diff --git a/modules/display/src/main/native/ios/Display.h b/modules/display/src/main/native/ios/Display.h index eb154eb5..ba275e21 100644 --- a/modules/display/src/main/native/ios/Display.h +++ b/modules/display/src/main/native/ios/Display.h @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2022 Gluon + * Copyright (c) 2018, 2025, Gluon * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -36,6 +36,8 @@ - (void) startObserver; - (void) stopObserver; - (NSString*) getNotch; + - (UIEdgeInsets) getInsets; @end -void sendNotch(); \ No newline at end of file +void sendNotch(); +void sendInsets(UIEdgeInsets insets); \ No newline at end of file diff --git a/modules/display/src/main/native/ios/Display.m b/modules/display/src/main/native/ios/Display.m index 65eae05e..a7195a07 100644 --- a/modules/display/src/main/native/ios/Display.m +++ b/modules/display/src/main/native/ios/Display.m @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016, 2023, Gluon + * Copyright (c) 2016, 2025, Gluon * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -48,6 +48,7 @@ // Display jclass mat_jDisplayServiceClass; jmethodID mat_jDisplayService_notifyDisplay = 0; +jmethodID mat_jDisplayService_notifyInsets = 0; Display *_display; bool iPhoneX; @@ -63,6 +64,7 @@ mat_jDisplayServiceClass = (*env)->NewGlobalRef(env, (*env)->FindClass(env, "com/gluonhq/attach/display/impl/IOSDisplayService")); mat_jDisplayService_notifyDisplay = (*env)->GetStaticMethodID(env, mat_jDisplayServiceClass, "notifyDisplay", "(Ljava/lang/String;)V"); + mat_jDisplayService_notifyInsets = (*env)->GetStaticMethodID(env, mat_jDisplayServiceClass, "notifyInsets", "(DDDD)V"); _display = [[Display alloc] init]; [_display isIPhoneX]; @@ -155,6 +157,13 @@ void sendNotch() { (*env)->DeleteLocalRef(env, arg); } +void sendInsets(UIEdgeInsets insets) { + if (debugAttach) { + AttachLog(@"Insets are %.3f %.3f %.3f %.3f", insets.top, insets.right, insets.bottom, insets.left); + } + (*env)->CallStaticVoidMethod(env, mat_jDisplayServiceClass, mat_jDisplayService_notifyInsets, insets.top, insets.right, insets.bottom, insets.left); +} + @implementation Display NSString * GetDeviceModel(void) @@ -190,7 +199,8 @@ - (void) isIPhoneX @"iPhone14,2", @"iPhone14,3", @"iPhone14,4", @"iPhone14,5", // iPhone 13 Pro, 13 Pro Max, 13 Mini, 13 @"iPhone14,7", @"iPhone14,8", @"iPhone15,2", @"iPhone15,3", // iPhone 14, 14 Plus, 14 Pro, 14 Pro Max @"iPhone15,4", @"iPhone15,5", @"iPhone16,1", @"iPhone16,2", // iPhone 15, 15 Plus, 15 Pro, 15 Pro Max - @"iPhone17,3", @"iPhone17,4", @"iPhone17,1", @"iPhone17,2", // iPhone 16, 16 Plus, 16 Pro, 16 Pro Max + @"iPhone17,3", @"iPhone17,4", @"iPhone17,1", @"iPhone17,2", @"iPhone17,5", // iPhone 16, 16 Plus, 16 Pro, 16 Pro Max, 16e + @"iPhone18,3", @"iPhone18,1", @"iPhone18,2", @"iPhone18,4", // iPhone 17, 17 Pro, 17 Pro Max, 17 Pro Air ]; if ([modelsWithNotch containsObject:GetDeviceModel()]) { @@ -234,19 +244,17 @@ - (void) isIPhoneX - (void) startObserver { + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(OrientationDidChange:) name:UIDeviceOrientationDidChangeNotification object:nil]; + sendInsets([self getInsets]); if (iPhoneX) { - [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(OrientationDidChange:) name:UIDeviceOrientationDidChangeNotification object:nil]; sendNotch(); } } - (void) stopObserver { - if (iPhoneX) - { - [[NSNotificationCenter defaultCenter] removeObserver:self name:UIDeviceOrientationDidChangeNotification object:nil]; - } + [[NSNotificationCenter defaultCenter] removeObserver:self name:UIDeviceOrientationDidChangeNotification object:nil]; } - (NSString*) getNotch @@ -271,9 +279,26 @@ - (NSString*) getNotch return value; } +- (UIEdgeInsets) getInsets +{ + if (@available(iOS 11.0, *)) { + UIWindow *window = UIApplication.sharedApplication.keyWindow; + if (!window) { + AttachLog(@"key window was nil"); + return UIEdgeInsetsMake(0, 0, 0, 0); + } + return window.safeAreaInsets; + } + return UIEdgeInsetsMake(0, 0, 0, 0); +} + -(void)OrientationDidChange:(NSNotification*)notification { - sendNotch(); + sendInsets([self getInsets]); + if (iPhoneX) + { + sendNotch(); + } } @end diff --git a/modules/display/src/main/resources/META-INF/substrate/config/jniconfig-aarch64-android.json b/modules/display/src/main/resources/META-INF/substrate/config/jniconfig-aarch64-android.json new file mode 100644 index 00000000..278575db --- /dev/null +++ b/modules/display/src/main/resources/META-INF/substrate/config/jniconfig-aarch64-android.json @@ -0,0 +1,6 @@ +[ + { + "name" : "com.gluonhq.attach.display.impl.AndroidDisplayService", + "methods":[{"name":"notifyInsets","parameterTypes":["double", "double", "double", "double"] }] + } +] \ No newline at end of file diff --git a/modules/display/src/main/resources/META-INF/substrate/config/jniconfig-arm64-ios.json b/modules/display/src/main/resources/META-INF/substrate/config/jniconfig-arm64-ios.json index b95b833a..c72541ce 100644 --- a/modules/display/src/main/resources/META-INF/substrate/config/jniconfig-arm64-ios.json +++ b/modules/display/src/main/resources/META-INF/substrate/config/jniconfig-arm64-ios.json @@ -1,6 +1,9 @@ [ { "name" : "com.gluonhq.attach.display.impl.IOSDisplayService", - "methods":[{"name":"notifyDisplay","parameterTypes":["java.lang.String"] }] + "methods":[ + {"name":"notifyDisplay","parameterTypes":["java.lang.String"] }, + {"name":"notifyInsets","parameterTypes":["double", "double", "double", "double"] } + ] } ] \ No newline at end of file diff --git a/modules/display/src/main/resources/META-INF/substrate/config/jniconfig-x86_64-ios.json b/modules/display/src/main/resources/META-INF/substrate/config/jniconfig-x86_64-ios.json index b95b833a..c72541ce 100644 --- a/modules/display/src/main/resources/META-INF/substrate/config/jniconfig-x86_64-ios.json +++ b/modules/display/src/main/resources/META-INF/substrate/config/jniconfig-x86_64-ios.json @@ -1,6 +1,9 @@ [ { "name" : "com.gluonhq.attach.display.impl.IOSDisplayService", - "methods":[{"name":"notifyDisplay","parameterTypes":["java.lang.String"] }] + "methods":[ + {"name":"notifyDisplay","parameterTypes":["java.lang.String"] }, + {"name":"notifyInsets","parameterTypes":["double", "double", "double", "double"] } + ] } ] \ No newline at end of file diff --git a/modules/display/src/main/resources/META-INF/substrate/dalvik/android-dependencies.txt b/modules/display/src/main/resources/META-INF/substrate/dalvik/android-dependencies.txt new file mode 100644 index 00000000..b94aca8d --- /dev/null +++ b/modules/display/src/main/resources/META-INF/substrate/dalvik/android-dependencies.txt @@ -0,0 +1 @@ +implementation 'androidx.core:core:1.13.0' diff --git a/modules/display/src/main/resources/META-INF/substrate/dalvik/build.gradle b/modules/display/src/main/resources/META-INF/substrate/dalvik/build.gradle new file mode 100644 index 00000000..527c8361 --- /dev/null +++ b/modules/display/src/main/resources/META-INF/substrate/dalvik/build.gradle @@ -0,0 +1,27 @@ +apply plugin: 'com.android.library' + +android { + + namespace 'com.gluonhq.helloandroid' + + defaultConfig { + minSdkVersion 21 + compileSdk 35 + targetSdkVersion 35 + } + + buildFeatures { + buildConfig = false + resValues = false + } + +} + +repositories { + google() +} + +dependencies { + compileOnly fileTree(dir: '../libs', include: '*.jar') + implementation 'androidx.core:core:1.13.0' +} \ No newline at end of file