From e09dc03f631daa1f3a76a237122314f0c22dc52d Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Thu, 12 Feb 2026 04:42:12 +0200 Subject: [PATCH 1/9] Fix iOS local notification replacement and add Android native stub --- .../notifications/LocalNotification.java | 8 +++ Ports/iOSPort/nativeSources/IOSNative.m | 19 ++++++- .../LocalNotificationNativeImpl.java | 11 ++++ .../LocalNotificationNative.java | 7 +++ .../tests/Cn1ssDeviceRunner.java | 1 + .../LocalNotificationIdOverrideTest.java | 50 +++++++++++++++++++ ...ocodenameone_LocalNotificationNativeImpl.h | 9 ++++ ...ocodenameone_LocalNotificationNativeImpl.m | 29 +++++++++++ 8 files changed, 132 insertions(+), 2 deletions(-) create mode 100644 scripts/hellocodenameone/android/src/main/java/com/codenameone/examples/hellocodenameone/LocalNotificationNativeImpl.java create mode 100644 scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/LocalNotificationNative.java create mode 100644 scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/LocalNotificationIdOverrideTest.java create mode 100644 scripts/hellocodenameone/ios/src/main/objectivec/com_codenameone_examples_hellocodenameone_LocalNotificationNativeImpl.h create mode 100644 scripts/hellocodenameone/ios/src/main/objectivec/com_codenameone_examples_hellocodenameone_LocalNotificationNativeImpl.m diff --git a/CodenameOne/src/com/codename1/notifications/LocalNotification.java b/CodenameOne/src/com/codename1/notifications/LocalNotification.java index 3621694637..4d5f4c6120 100644 --- a/CodenameOne/src/com/codename1/notifications/LocalNotification.java +++ b/CodenameOne/src/com/codename1/notifications/LocalNotification.java @@ -202,6 +202,10 @@ public void setAlertSound(String alertSound) { /// /// The ID can also be used to cancel the notification later using `com.codename1.ui.Display#cancelLocalNotification(java.lang.String)` /// + /// **Platform note (iOS):** Notification IDs map to `UNNotificationRequest` identifiers. + /// Scheduling another local notification with the same ID replaces the existing pending notification + /// rather than adding a second one. + /// /// #### Returns /// /// the id @@ -216,6 +220,10 @@ public String getId() { /// /// The ID can also be used to cancel the notification later using `com.codename1.ui.Display#cancelLocalNotification(java.lang.String)` /// + /// **Platform note (iOS):** Notification IDs map to `UNNotificationRequest` identifiers. + /// Scheduling another local notification with the same ID replaces the existing pending notification + /// rather than adding a second one. + /// /// #### Parameters /// /// - `id`: the id to set diff --git a/Ports/iOSPort/nativeSources/IOSNative.m b/Ports/iOSPort/nativeSources/IOSNative.m index 1800226307..ca3601209b 100644 --- a/Ports/iOSPort/nativeSources/IOSNative.m +++ b/Ports/iOSPort/nativeSources/IOSNative.m @@ -9733,6 +9733,7 @@ JAVA_VOID com_codename1_impl_ios_IOSNative_sendLocalNotification___java_lang_Str JAVA_OBJECT me, JAVA_OBJECT notificationId, JAVA_OBJECT alertTitle, JAVA_OBJECT alertBody, JAVA_OBJECT alertSound, JAVA_INT badgeNumber, JAVA_LONG fireDate, JAVA_INT repeatType, JAVA_BOOLEAN foreground ) { #ifdef CN1_INCLUDE_NOTIFICATIONS2 + NSString *nsNotificationId = notificationId != JAVA_NULL ? toNSString(CN1_THREAD_STATE_PASS_ARG notificationId) : nil; NSString * title = [NSString string]; NSString * body = [NSString string]; NSString *tmpStr; @@ -9759,7 +9760,9 @@ JAVA_VOID com_codename1_impl_ios_IOSNative_sendLocalNotification___java_lang_Str body = tmpStr; NSMutableDictionary *dict = [[NSMutableDictionary alloc] init]; - [dict setObject: toNSString(CN1_THREAD_STATE_PASS_ARG notificationId) forKey: @"__ios_id__"]; + if (nsNotificationId != nil) { + [dict setObject: nsNotificationId forKey: @"__ios_id__"]; + } if (foreground) { [dict setObject: @"true" forKey: @"foreground"]; } @@ -9796,8 +9799,9 @@ JAVA_VOID com_codename1_impl_ios_IOSNative_sendLocalNotification___java_lang_Str UNTimeIntervalNotificationTrigger *trigger = [UNTimeIntervalNotificationTrigger triggerWithTimeInterval:fireDate/1000 - [[NSDate date] timeIntervalSince1970] + 1 repeats:NO]; // Create the request object. + NSString *requestIdentifier = nsNotificationId != nil ? nsNotificationId : [[NSUUID UUID] UUIDString]; UNNotificationRequest* request = [UNNotificationRequest - requestWithIdentifier:toNSString(CN1_THREAD_STATE_PASS_ARG notificationId) content:content trigger:trigger]; + requestWithIdentifier:requestIdentifier content:content trigger:trigger]; @@ -9866,6 +9870,17 @@ JAVA_VOID com_codename1_impl_ios_IOSNative_sendLocalNotification___java_lang_Str [[UIApplication sharedApplication] registerUserNotificationSettings:[UIUserNotificationSettings settingsForTypes:UIUserNotificationTypeAlert|UIUserNotificationTypeBadge|UIUserNotificationTypeSound categories:nil]]; } #endif + if (nsNotificationId != nil) { + UIApplication *app = [UIApplication sharedApplication]; + NSArray *scheduledNotifications = [app scheduledLocalNotifications]; + for (UILocalNotification *scheduled in scheduledNotifications) { + NSDictionary *userInfo = scheduled.userInfo; + NSString *scheduledId = [NSString stringWithFormat:@"%@", [userInfo valueForKey:@"__ios_id__"]]; + if ([nsNotificationId isEqualToString:scheduledId]) { + [app cancelLocalNotification:scheduled]; + } + } + } [[UIApplication sharedApplication] scheduleLocalNotification: notification]; }); } diff --git a/scripts/hellocodenameone/android/src/main/java/com/codenameone/examples/hellocodenameone/LocalNotificationNativeImpl.java b/scripts/hellocodenameone/android/src/main/java/com/codenameone/examples/hellocodenameone/LocalNotificationNativeImpl.java new file mode 100644 index 0000000000..1e61e06d04 --- /dev/null +++ b/scripts/hellocodenameone/android/src/main/java/com/codenameone/examples/hellocodenameone/LocalNotificationNativeImpl.java @@ -0,0 +1,11 @@ +package com.codenameone.examples.hellocodenameone; + +public class LocalNotificationNativeImpl { + public int countPendingNotificationsWithId(String notificationId) { + return -1; + } + + public boolean isSupported() { + return false; + } +} diff --git a/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/LocalNotificationNative.java b/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/LocalNotificationNative.java new file mode 100644 index 0000000000..40d9d096af --- /dev/null +++ b/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/LocalNotificationNative.java @@ -0,0 +1,7 @@ +package com.codenameone.examples.hellocodenameone; + +import com.codename1.system.NativeInterface; + +public interface LocalNotificationNative extends NativeInterface { + int countPendingNotificationsWithId(String notificationId); +} diff --git a/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/Cn1ssDeviceRunner.java b/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/Cn1ssDeviceRunner.java index 6157688bb8..ec6bcd9e09 100644 --- a/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/Cn1ssDeviceRunner.java +++ b/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/Cn1ssDeviceRunner.java @@ -71,6 +71,7 @@ public final class Cn1ssDeviceRunner extends DeviceRunner { new OrientationLockScreenshotTest(), new SheetScreenshotTest(), new InPlaceEditViewTest(), + new LocalNotificationIdOverrideTest(), new BytecodeTranslatorRegressionTest(), new BackgroundThreadUiAccessTest(), new VPNDetectionAPITest(), diff --git a/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/LocalNotificationIdOverrideTest.java b/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/LocalNotificationIdOverrideTest.java new file mode 100644 index 0000000000..fdc6fc07cb --- /dev/null +++ b/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/LocalNotificationIdOverrideTest.java @@ -0,0 +1,50 @@ +package com.codenameone.examples.hellocodenameone.tests; + +import com.codename1.notifications.LocalNotification; +import com.codename1.system.NativeLookup; +import com.codename1.ui.Display; +import com.codename1.ui.util.UITimer; +import com.codenameone.examples.hellocodenameone.LocalNotificationNative; + +public class LocalNotificationIdOverrideTest extends BaseTest { + @Override + public boolean shouldTakeScreenshot() { + return false; + } + + @Override + public boolean runTest() { + LocalNotificationNative nativeInterface = NativeLookup.create(LocalNotificationNative.class); + if (nativeInterface == null || !nativeInterface.isSupported()) { + done(); + return true; + } + + String notificationId = "cn1ss-local-notification-id-override"; + Display.getInstance().cancelLocalNotification(notificationId); + + LocalNotification first = new LocalNotification(); + first.setId(notificationId); + first.setAlertTitle("First"); + first.setAlertBody("First body"); + Display.getInstance().scheduleLocalNotification(first, System.currentTimeMillis() + 60000, LocalNotification.REPEAT_NONE); + + LocalNotification second = new LocalNotification(); + second.setId(notificationId); + second.setAlertTitle("Second"); + second.setAlertBody("Second body"); + Display.getInstance().scheduleLocalNotification(second, System.currentTimeMillis() + 120000, LocalNotification.REPEAT_NONE); + + UITimer.timer(1000, false, Display.getInstance().getCurrent(), () -> { + int pendingWithSameId = nativeInterface.countPendingNotificationsWithId(notificationId); + Display.getInstance().cancelLocalNotification(notificationId); + if (pendingWithSameId != 1) { + fail("Expected a single pending notification for id '" + notificationId + "' but found " + pendingWithSameId); + } else { + done(); + } + }); + + return true; + } +} diff --git a/scripts/hellocodenameone/ios/src/main/objectivec/com_codenameone_examples_hellocodenameone_LocalNotificationNativeImpl.h b/scripts/hellocodenameone/ios/src/main/objectivec/com_codenameone_examples_hellocodenameone_LocalNotificationNativeImpl.h new file mode 100644 index 0000000000..a24945b79e --- /dev/null +++ b/scripts/hellocodenameone/ios/src/main/objectivec/com_codenameone_examples_hellocodenameone_LocalNotificationNativeImpl.h @@ -0,0 +1,9 @@ +#import + +@interface com_codenameone_examples_hellocodenameone_LocalNotificationNativeImpl : NSObject { +} + +-(int)countPendingNotificationsWithId:(NSString*)notificationId; +-(BOOL)isSupported; + +@end diff --git a/scripts/hellocodenameone/ios/src/main/objectivec/com_codenameone_examples_hellocodenameone_LocalNotificationNativeImpl.m b/scripts/hellocodenameone/ios/src/main/objectivec/com_codenameone_examples_hellocodenameone_LocalNotificationNativeImpl.m new file mode 100644 index 0000000000..8f3b647263 --- /dev/null +++ b/scripts/hellocodenameone/ios/src/main/objectivec/com_codenameone_examples_hellocodenameone_LocalNotificationNativeImpl.m @@ -0,0 +1,29 @@ +#import "com_codenameone_examples_hellocodenameone_LocalNotificationNativeImpl.h" +#import + +@implementation com_codenameone_examples_hellocodenameone_LocalNotificationNativeImpl + +-(int)countPendingNotificationsWithId:(NSString*)notificationId { + if (@available(iOS 10.0, *)) { + __block NSInteger matches = 0; + dispatch_semaphore_t semaphore = dispatch_semaphore_create(0); + [[UNUserNotificationCenter currentNotificationCenter] getPendingNotificationRequestsWithCompletionHandler:^(NSArray * _Nonnull requests) { + for (UNNotificationRequest* request in requests) { + if ([request.identifier isEqualToString:notificationId]) { + matches++; + } + } + dispatch_semaphore_signal(semaphore); + }]; + dispatch_time_t timeout = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)); + dispatch_semaphore_wait(semaphore, timeout); + return (int)matches; + } + return -1; +} + +-(BOOL)isSupported{ + return YES; +} + +@end From 69e8f8ccfd895593791f994ca8b9cefac07d3fbb Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Thu, 12 Feb 2026 05:56:08 +0200 Subject: [PATCH 2/9] Stabilize iOS notification native test counting and logging --- .../LocalNotificationIdOverrideTest.java | 22 ++++++++++---- ...ocodenameone_LocalNotificationNativeImpl.m | 30 +++++++++---------- 2 files changed, 30 insertions(+), 22 deletions(-) diff --git a/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/LocalNotificationIdOverrideTest.java b/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/LocalNotificationIdOverrideTest.java index fdc6fc07cb..0fd07793c4 100644 --- a/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/LocalNotificationIdOverrideTest.java +++ b/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/LocalNotificationIdOverrideTest.java @@ -1,5 +1,6 @@ package com.codenameone.examples.hellocodenameone.tests; +import com.codename1.io.Log; import com.codename1.notifications.LocalNotification; import com.codename1.system.NativeLookup; import com.codename1.ui.Display; @@ -36,12 +37,21 @@ public boolean runTest() { Display.getInstance().scheduleLocalNotification(second, System.currentTimeMillis() + 120000, LocalNotification.REPEAT_NONE); UITimer.timer(1000, false, Display.getInstance().getCurrent(), () -> { - int pendingWithSameId = nativeInterface.countPendingNotificationsWithId(notificationId); - Display.getInstance().cancelLocalNotification(notificationId); - if (pendingWithSameId != 1) { - fail("Expected a single pending notification for id '" + notificationId + "' but found " + pendingWithSameId); - } else { - done(); + try { + int pendingWithSameId = nativeInterface.countPendingNotificationsWithId(notificationId); + Display.getInstance().cancelLocalNotification(notificationId); + if (pendingWithSameId < 0) { + fail("Native notification count returned an error for id '" + notificationId + "': " + pendingWithSameId); + return; + } + if (pendingWithSameId != 1) { + fail("Expected a single pending notification for id '" + notificationId + "' but found " + pendingWithSameId); + } else { + done(); + } + } catch (Throwable t) { + Log.e(t); + fail("Native notification count failed with exception: " + t.getMessage()); } }); diff --git a/scripts/hellocodenameone/ios/src/main/objectivec/com_codenameone_examples_hellocodenameone_LocalNotificationNativeImpl.m b/scripts/hellocodenameone/ios/src/main/objectivec/com_codenameone_examples_hellocodenameone_LocalNotificationNativeImpl.m index 8f3b647263..c25bab3e2e 100644 --- a/scripts/hellocodenameone/ios/src/main/objectivec/com_codenameone_examples_hellocodenameone_LocalNotificationNativeImpl.m +++ b/scripts/hellocodenameone/ios/src/main/objectivec/com_codenameone_examples_hellocodenameone_LocalNotificationNativeImpl.m @@ -1,25 +1,23 @@ #import "com_codenameone_examples_hellocodenameone_LocalNotificationNativeImpl.h" -#import +#import @implementation com_codenameone_examples_hellocodenameone_LocalNotificationNativeImpl -(int)countPendingNotificationsWithId:(NSString*)notificationId { - if (@available(iOS 10.0, *)) { - __block NSInteger matches = 0; - dispatch_semaphore_t semaphore = dispatch_semaphore_create(0); - [[UNUserNotificationCenter currentNotificationCenter] getPendingNotificationRequestsWithCompletionHandler:^(NSArray * _Nonnull requests) { - for (UNNotificationRequest* request in requests) { - if ([request.identifier isEqualToString:notificationId]) { - matches++; - } - } - dispatch_semaphore_signal(semaphore); - }]; - dispatch_time_t timeout = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)); - dispatch_semaphore_wait(semaphore, timeout); - return (int)matches; + if (notificationId == nil) { + return 0; } - return -1; + UIApplication *app = [UIApplication sharedApplication]; + NSArray *scheduledNotifications = [app scheduledLocalNotifications]; + NSInteger matches = 0; + for (UILocalNotification *notification in scheduledNotifications) { + NSDictionary *userInfo = notification.userInfo; + NSString *scheduledId = [NSString stringWithFormat:@"%@", [userInfo valueForKey:@"__ios_id__"]]; + if ([notificationId isEqualToString:scheduledId]) { + matches++; + } + } + return (int)matches; } -(BOOL)isSupported{ From c146cc83e6fba38cdfafefd8cd82aea194e62333 Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Thu, 12 Feb 2026 06:41:16 +0200 Subject: [PATCH 3/9] Harden iOS notification override path and screenshot diagnostics --- Ports/iOSPort/nativeSources/IOSNative.m | 19 +---- .../codename1/impl/ios/IOSImplementation.java | 8 +- .../tests/Cn1ssDeviceRunnerHelper.java | 73 +++++++++++++++---- ...ocodenameone_LocalNotificationNativeImpl.m | 20 ++--- 4 files changed, 77 insertions(+), 43 deletions(-) diff --git a/Ports/iOSPort/nativeSources/IOSNative.m b/Ports/iOSPort/nativeSources/IOSNative.m index ca3601209b..1800226307 100644 --- a/Ports/iOSPort/nativeSources/IOSNative.m +++ b/Ports/iOSPort/nativeSources/IOSNative.m @@ -9733,7 +9733,6 @@ JAVA_VOID com_codename1_impl_ios_IOSNative_sendLocalNotification___java_lang_Str JAVA_OBJECT me, JAVA_OBJECT notificationId, JAVA_OBJECT alertTitle, JAVA_OBJECT alertBody, JAVA_OBJECT alertSound, JAVA_INT badgeNumber, JAVA_LONG fireDate, JAVA_INT repeatType, JAVA_BOOLEAN foreground ) { #ifdef CN1_INCLUDE_NOTIFICATIONS2 - NSString *nsNotificationId = notificationId != JAVA_NULL ? toNSString(CN1_THREAD_STATE_PASS_ARG notificationId) : nil; NSString * title = [NSString string]; NSString * body = [NSString string]; NSString *tmpStr; @@ -9760,9 +9759,7 @@ JAVA_VOID com_codename1_impl_ios_IOSNative_sendLocalNotification___java_lang_Str body = tmpStr; NSMutableDictionary *dict = [[NSMutableDictionary alloc] init]; - if (nsNotificationId != nil) { - [dict setObject: nsNotificationId forKey: @"__ios_id__"]; - } + [dict setObject: toNSString(CN1_THREAD_STATE_PASS_ARG notificationId) forKey: @"__ios_id__"]; if (foreground) { [dict setObject: @"true" forKey: @"foreground"]; } @@ -9799,9 +9796,8 @@ JAVA_VOID com_codename1_impl_ios_IOSNative_sendLocalNotification___java_lang_Str UNTimeIntervalNotificationTrigger *trigger = [UNTimeIntervalNotificationTrigger triggerWithTimeInterval:fireDate/1000 - [[NSDate date] timeIntervalSince1970] + 1 repeats:NO]; // Create the request object. - NSString *requestIdentifier = nsNotificationId != nil ? nsNotificationId : [[NSUUID UUID] UUIDString]; UNNotificationRequest* request = [UNNotificationRequest - requestWithIdentifier:requestIdentifier content:content trigger:trigger]; + requestWithIdentifier:toNSString(CN1_THREAD_STATE_PASS_ARG notificationId) content:content trigger:trigger]; @@ -9870,17 +9866,6 @@ JAVA_VOID com_codename1_impl_ios_IOSNative_sendLocalNotification___java_lang_Str [[UIApplication sharedApplication] registerUserNotificationSettings:[UIUserNotificationSettings settingsForTypes:UIUserNotificationTypeAlert|UIUserNotificationTypeBadge|UIUserNotificationTypeSound categories:nil]]; } #endif - if (nsNotificationId != nil) { - UIApplication *app = [UIApplication sharedApplication]; - NSArray *scheduledNotifications = [app scheduledLocalNotifications]; - for (UILocalNotification *scheduled in scheduledNotifications) { - NSDictionary *userInfo = scheduled.userInfo; - NSString *scheduledId = [NSString stringWithFormat:@"%@", [userInfo valueForKey:@"__ios_id__"]]; - if ([nsNotificationId isEqualToString:scheduledId]) { - [app cancelLocalNotification:scheduled]; - } - } - } [[UIApplication sharedApplication] scheduleLocalNotification: notification]; }); } diff --git a/Ports/iOSPort/src/com/codename1/impl/ios/IOSImplementation.java b/Ports/iOSPort/src/com/codename1/impl/ios/IOSImplementation.java index 337710b512..b31dd9dc72 100644 --- a/Ports/iOSPort/src/com/codename1/impl/ios/IOSImplementation.java +++ b/Ports/iOSPort/src/com/codename1/impl/ios/IOSImplementation.java @@ -9204,9 +9204,13 @@ public void splitString(String source, char separator, ArrayList out) { } public void scheduleLocalNotification(LocalNotification n, long firstTime, int repeat) { - + String id = n.getId(); + if (id != null) { + nativeInstance.cancelLocalNotification(id); + } + nativeInstance.sendLocalNotification( - n.getId(), + id, n.getAlertTitle(), n.getAlertBody(), n.getAlertSound(), diff --git a/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/Cn1ssDeviceRunnerHelper.java b/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/Cn1ssDeviceRunnerHelper.java index 58a2187a1e..a91af63b23 100644 --- a/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/Cn1ssDeviceRunnerHelper.java +++ b/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/Cn1ssDeviceRunnerHelper.java @@ -35,31 +35,22 @@ static void emitCurrentFormScreenshot(String testName) { if (current == null) { println("CN1SS:ERR:test=" + safeName + " message=Current form is null"); println("CN1SS:END:" + safeName); + return; } int width = Math.max(1, current.getWidth()); int height = Math.max(1, current.getHeight()); - Image[] img = new Image[1]; - Display.getInstance().screenshot(screen -> img[0] = screen); - long time = System.currentTimeMillis(); - Display.getInstance().invokeAndBlock(() -> { - while(img[0] == null) { - Util.sleep(50); - // timeout - if (System.currentTimeMillis() - time > 2000) { - return; - } - } - }); - if (img[0] == null) { - println("CN1SS:ERR:test=" + safeName + " message=Screenshot process timed out"); + Image screenshot = captureScreenshotWithRetries(safeName, current, width, height); + if (screenshot == null) { + println("CN1SS:ERR:test=" + safeName + " message=Unable to capture screenshot"); println("CN1SS:END:" + safeName); + return; } - Image screenshot = img[0]; try { ImageIO io = ImageIO.getImageIO(); if (io == null || !io.isFormatSupported(ImageIO.FORMAT_PNG)) { println("CN1SS:ERR:test=" + safeName + " message=PNG encoding unavailable"); println("CN1SS:END:" + safeName); + return; } if(Display.getInstance().isSimulator()) { io.save(screenshot, Storage.getInstance().createOutputStream(safeName + ".png"), ImageIO.FORMAT_PNG, 1); @@ -85,6 +76,58 @@ static void emitCurrentFormScreenshot(String testName) { } } + static Image captureScreenshotWithRetries(String safeName, Form current, int width, int height) { + final int maxAttempts = 3; + Image screenshot = null; + for (int attempt = 1; attempt <= maxAttempts; attempt++) { + current.revalidate(); + Image[] img = new Image[1]; + Display.getInstance().screenshot(screen -> img[0] = screen); + long time = System.currentTimeMillis(); + Display.getInstance().invokeAndBlock(() -> { + while(img[0] == null) { + Util.sleep(50); + if (System.currentTimeMillis() - time > 2000) { + return; + } + } + }); + screenshot = img[0]; + if (screenshot == null) { + println("CN1SS:WARN:test=" + safeName + " message=Screenshot process timed out attempt=" + attempt); + continue; + } + + int[] imageData = screenshot.getRGBCached(); + if (!isLikelyBlankImage(imageData)) { + return screenshot; + } + + int sample = imageData.length > 0 ? imageData[0] : 0; + println("CN1SS:WARN:test=" + safeName + " message=Blank screenshot detected width=" + width + " height=" + height + " rgb0=" + sample + " attempt=" + attempt + " form=" + current.getClass().getName()); + if (attempt < maxAttempts) { + screenshot.dispose(); + screenshot = null; + Util.sleep(250); + } + } + return screenshot; + } + + static boolean isLikelyBlankImage(int[] imageData) { + if (imageData == null || imageData.length == 0) { + return true; + } + int first = imageData[0]; + int maxSamples = Math.min(imageData.length, 4096); + for (int i = 1; i < maxSamples; i++) { + if (imageData[i] != first) { + return false; + } + } + return true; + } + static byte[] encodePreview(ImageIO io, Image screenshot, String safeName) throws IOException { byte[] chosenPreview = null; int chosenQuality = 0; diff --git a/scripts/hellocodenameone/ios/src/main/objectivec/com_codenameone_examples_hellocodenameone_LocalNotificationNativeImpl.m b/scripts/hellocodenameone/ios/src/main/objectivec/com_codenameone_examples_hellocodenameone_LocalNotificationNativeImpl.m index c25bab3e2e..0f0fa374ee 100644 --- a/scripts/hellocodenameone/ios/src/main/objectivec/com_codenameone_examples_hellocodenameone_LocalNotificationNativeImpl.m +++ b/scripts/hellocodenameone/ios/src/main/objectivec/com_codenameone_examples_hellocodenameone_LocalNotificationNativeImpl.m @@ -7,16 +7,18 @@ -(int)countPendingNotificationsWithId:(NSString*)notificationId { if (notificationId == nil) { return 0; } - UIApplication *app = [UIApplication sharedApplication]; - NSArray *scheduledNotifications = [app scheduledLocalNotifications]; - NSInteger matches = 0; - for (UILocalNotification *notification in scheduledNotifications) { - NSDictionary *userInfo = notification.userInfo; - NSString *scheduledId = [NSString stringWithFormat:@"%@", [userInfo valueForKey:@"__ios_id__"]]; - if ([notificationId isEqualToString:scheduledId]) { - matches++; + __block NSInteger matches = 0; + dispatch_sync(dispatch_get_main_queue(), ^{ + UIApplication *app = [UIApplication sharedApplication]; + NSArray *scheduledNotifications = [app scheduledLocalNotifications]; + for (UILocalNotification *notification in scheduledNotifications) { + NSDictionary *userInfo = notification.userInfo; + NSString *scheduledId = [NSString stringWithFormat:@"%@", [userInfo valueForKey:@"__ios_id__"]]; + if ([notificationId isEqualToString:scheduledId]) { + matches++; + } } - } + }); return (int)matches; } From 99c5337453021b13e2625bfb61c455b8c5d2af1e Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Thu, 12 Feb 2026 16:32:45 +0200 Subject: [PATCH 4/9] Fail CN1 screenshot suite on capture errors and timeouts --- .../examples/hellocodenameone/tests/BaseTest.java | 9 ++++++--- .../hellocodenameone/tests/Cn1ssDeviceRunner.java | 10 ++++++++++ .../tests/Cn1ssDeviceRunnerHelper.java | 10 ++++++---- 3 files changed, 22 insertions(+), 7 deletions(-) diff --git a/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/BaseTest.java b/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/BaseTest.java index 1036f4f205..970a0d8978 100644 --- a/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/BaseTest.java +++ b/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/BaseTest.java @@ -34,8 +34,11 @@ protected Form createForm(String title, Layout layout, final String imageName) { @Override protected void onShowCompleted() { registerReadyCallback(this, () -> { - Cn1ssDeviceRunnerHelper.emitCurrentFormScreenshot(imageName); - done(); + if (Cn1ssDeviceRunnerHelper.emitCurrentFormScreenshot(imageName)) { + done(); + } else { + fail("Screenshot capture failed for " + imageName); + } }); } }; @@ -53,4 +56,4 @@ protected synchronized void done() { public synchronized boolean isDone() { return done; } -} \ No newline at end of file +} diff --git a/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/Cn1ssDeviceRunner.java b/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/Cn1ssDeviceRunner.java index ec6bcd9e09..9a03bff501 100644 --- a/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/Cn1ssDeviceRunner.java +++ b/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/Cn1ssDeviceRunner.java @@ -83,6 +83,7 @@ public static void addTest(BaseTest test) { } public void runSuite() { + boolean suiteFailed = false; CN.callSerially(() -> { Display.getInstance().addEdtErrorHandler(e -> { log("CN1SS:ERR:exception caught in EDT " + e.getSource()); @@ -105,6 +106,7 @@ public void runSuite() { } catch (Throwable t) { log("CN1SS:ERR:suite test=" + testClass + " failed=" + t); t.printStackTrace(); + testClass.fail("Unhandled exception: " + t.getMessage()); } }); int timeout = 30000; @@ -115,8 +117,10 @@ public void runSuite() { testClass.cleanup(); if(timeout == 0) { log("CN1SS:ERR:suite test=" + testClass + " failed due to timeout waiting for DONE"); + suiteFailed = true; } else if (testClass.isFailed()) { log("CN1SS:ERR:suite test=" + testClass + " failed: " + testClass.getFailMessage()); + suiteFailed = true; } else { if (!testClass.shouldTakeScreenshot()) { log("CN1SS:INFO:test=" + testClass + " screenshot=none"); @@ -125,7 +129,13 @@ public void runSuite() { log("CN1SS:INFO:suite finished test=" + testClass); } log("CN1SS:SUITE:FINISHED"); + if (suiteFailed) { + log("CN1SS:ERR:suite failed due to one or more test errors"); + } TestReporting.getInstance().testExecutionFinished(getClass().getName()); + if (suiteFailed) { + throw new RuntimeException("CN1SS suite failed"); + } if (CN.isSimulator()) { Display.getInstance().exitApplication(); } diff --git a/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/Cn1ssDeviceRunnerHelper.java b/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/Cn1ssDeviceRunnerHelper.java index a91af63b23..0b4581115d 100644 --- a/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/Cn1ssDeviceRunnerHelper.java +++ b/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/Cn1ssDeviceRunnerHelper.java @@ -29,13 +29,13 @@ static void runOnEdtSync(Runnable runnable) { } } - static void emitCurrentFormScreenshot(String testName) { + static boolean emitCurrentFormScreenshot(String testName) { String safeName = sanitizeTestName(testName); Form current = Display.getInstance().getCurrent(); if (current == null) { println("CN1SS:ERR:test=" + safeName + " message=Current form is null"); println("CN1SS:END:" + safeName); - return; + return false; } int width = Math.max(1, current.getWidth()); int height = Math.max(1, current.getHeight()); @@ -43,14 +43,14 @@ static void emitCurrentFormScreenshot(String testName) { if (screenshot == null) { println("CN1SS:ERR:test=" + safeName + " message=Unable to capture screenshot"); println("CN1SS:END:" + safeName); - return; + return false; } try { ImageIO io = ImageIO.getImageIO(); if (io == null || !io.isFormatSupported(ImageIO.FORMAT_PNG)) { println("CN1SS:ERR:test=" + safeName + " message=PNG encoding unavailable"); println("CN1SS:END:" + safeName); - return; + return false; } if(Display.getInstance().isSimulator()) { io.save(screenshot, Storage.getInstance().createOutputStream(safeName + ".png"), ImageIO.FORMAT_PNG, 1); @@ -67,10 +67,12 @@ static void emitCurrentFormScreenshot(String testName) { } else { println("CN1SS:INFO:test=" + safeName + " preview_jpeg_bytes=0 preview_quality=0"); } + return true; } catch (IOException ex) { println("CN1SS:ERR:test=" + safeName + " message=" + ex); Log.e(ex); println("CN1SS:END:" + safeName); + return false; } finally { screenshot.dispose(); } From 5c2e06b0a2ed8537cf7ae4c1357ab53d3e350e80 Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Thu, 12 Feb 2026 16:32:51 +0200 Subject: [PATCH 5/9] Add Java/native thread dumps on CN1 screenshot test failures --- .../TestDiagnosticsNativeImpl.java | 10 ++++++ .../TestDiagnosticsNative.java | 7 +++++ .../tests/Cn1ssDeviceRunner.java | 31 +++++++++++++++++++ ...llocodenameone_TestDiagnosticsNativeImpl.h | 9 ++++++ ...llocodenameone_TestDiagnosticsNativeImpl.m | 24 ++++++++++++++ 5 files changed, 81 insertions(+) create mode 100644 scripts/hellocodenameone/android/src/main/java/com/codenameone/examples/hellocodenameone/TestDiagnosticsNativeImpl.java create mode 100644 scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/TestDiagnosticsNative.java create mode 100644 scripts/hellocodenameone/ios/src/main/objectivec/com_codenameone_examples_hellocodenameone_TestDiagnosticsNativeImpl.h create mode 100644 scripts/hellocodenameone/ios/src/main/objectivec/com_codenameone_examples_hellocodenameone_TestDiagnosticsNativeImpl.m diff --git a/scripts/hellocodenameone/android/src/main/java/com/codenameone/examples/hellocodenameone/TestDiagnosticsNativeImpl.java b/scripts/hellocodenameone/android/src/main/java/com/codenameone/examples/hellocodenameone/TestDiagnosticsNativeImpl.java new file mode 100644 index 0000000000..8929488946 --- /dev/null +++ b/scripts/hellocodenameone/android/src/main/java/com/codenameone/examples/hellocodenameone/TestDiagnosticsNativeImpl.java @@ -0,0 +1,10 @@ +package com.codenameone.examples.hellocodenameone; + +public class TestDiagnosticsNativeImpl { + public void dumpNativeThreads(String reason) { + } + + public boolean isSupported() { + return false; + } +} diff --git a/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/TestDiagnosticsNative.java b/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/TestDiagnosticsNative.java new file mode 100644 index 0000000000..6e7ea41003 --- /dev/null +++ b/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/TestDiagnosticsNative.java @@ -0,0 +1,7 @@ +package com.codenameone.examples.hellocodenameone; + +import com.codename1.system.NativeInterface; + +public interface TestDiagnosticsNative extends NativeInterface { + void dumpNativeThreads(String reason); +} diff --git a/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/Cn1ssDeviceRunner.java b/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/Cn1ssDeviceRunner.java index 9a03bff501..a931bc86ee 100644 --- a/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/Cn1ssDeviceRunner.java +++ b/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/Cn1ssDeviceRunner.java @@ -3,10 +3,12 @@ import com.codename1.io.Util; import com.codename1.testing.DeviceRunner; import com.codename1.testing.TestReporting; +import com.codename1.system.NativeLookup; import com.codename1.ui.CN; import com.codename1.ui.Display; import com.codename1.ui.Form; import com.codename1.util.StringUtil; +import com.codenameone.examples.hellocodenameone.TestDiagnosticsNative; import com.codenameone.examples.hellocodenameone.tests.graphics.AffineScale; import com.codenameone.examples.hellocodenameone.tests.graphics.Clip; import com.codenameone.examples.hellocodenameone.tests.graphics.DrawArc; @@ -117,9 +119,11 @@ public void runSuite() { testClass.cleanup(); if(timeout == 0) { log("CN1SS:ERR:suite test=" + testClass + " failed due to timeout waiting for DONE"); + dumpDiagnostics("timeout test=" + testClass); suiteFailed = true; } else if (testClass.isFailed()) { log("CN1SS:ERR:suite test=" + testClass + " failed: " + testClass.getFailMessage()); + dumpDiagnostics("failure test=" + testClass + " message=" + testClass.getFailMessage()); suiteFailed = true; } else { if (!testClass.shouldTakeScreenshot()) { @@ -145,6 +149,33 @@ private static void log(String msg) { System.out.println(msg); } + private static void dumpDiagnostics(String reason) { + try { + log("CN1SS:ERR:capturing java thread dump reason=" + reason); + Thread current = Thread.currentThread(); + for (java.util.Map.Entry e : Thread.getAllStackTraces().entrySet()) { + Thread t = e.getKey(); + log("CN1SS:THREAD:name=" + t.getName() + " id=" + t.getId() + " state=" + t.getState() + " daemon=" + t.isDaemon() + " current=" + (t == current)); + for (StackTraceElement ste : e.getValue()) { + log("CN1SS:STACK:" + ste); + } + } + } catch (Throwable t) { + log("CN1SS:ERR:failed to capture java thread dump=" + t); + } + + try { + TestDiagnosticsNative nativeDiagnostics = NativeLookup.create(TestDiagnosticsNative.class); + if (nativeDiagnostics != null && nativeDiagnostics.isSupported()) { + nativeDiagnostics.dumpNativeThreads(reason); + } else { + log("CN1SS:INFO:native thread dump unsupported"); + } + } catch (Throwable t) { + log("CN1SS:ERR:failed to capture native thread dump=" + t); + } + } + @Override protected void startApplicationInstance() { Cn1ssDeviceRunnerHelper.runOnEdtSync(() -> { diff --git a/scripts/hellocodenameone/ios/src/main/objectivec/com_codenameone_examples_hellocodenameone_TestDiagnosticsNativeImpl.h b/scripts/hellocodenameone/ios/src/main/objectivec/com_codenameone_examples_hellocodenameone_TestDiagnosticsNativeImpl.h new file mode 100644 index 0000000000..00901e2263 --- /dev/null +++ b/scripts/hellocodenameone/ios/src/main/objectivec/com_codenameone_examples_hellocodenameone_TestDiagnosticsNativeImpl.h @@ -0,0 +1,9 @@ +#import + +@interface com_codenameone_examples_hellocodenameone_TestDiagnosticsNativeImpl : NSObject { +} + +-(void)dumpNativeThreads:(NSString*)reason; +-(BOOL)isSupported; + +@end diff --git a/scripts/hellocodenameone/ios/src/main/objectivec/com_codenameone_examples_hellocodenameone_TestDiagnosticsNativeImpl.m b/scripts/hellocodenameone/ios/src/main/objectivec/com_codenameone_examples_hellocodenameone_TestDiagnosticsNativeImpl.m new file mode 100644 index 0000000000..af5573c215 --- /dev/null +++ b/scripts/hellocodenameone/ios/src/main/objectivec/com_codenameone_examples_hellocodenameone_TestDiagnosticsNativeImpl.m @@ -0,0 +1,24 @@ +#import "com_codenameone_examples_hellocodenameone_TestDiagnosticsNativeImpl.h" + +@implementation com_codenameone_examples_hellocodenameone_TestDiagnosticsNativeImpl + +-(void)dumpNativeThreads:(NSString*)reason { + @try { + NSString *label = reason != nil ? reason : @"unspecified"; + NSLog(@"CN1SS:NATIVE:THREAD_DUMP:BEGIN reason=%@", label); + NSLog(@"CN1SS:NATIVE:THREAD_DUMP:current=%@ isMain=%@", [NSThread currentThread], [NSThread isMainThread] ? @"true" : @"false"); + NSArray *symbols = [NSThread callStackSymbols]; + for (NSString *line in symbols) { + NSLog(@"CN1SS:NATIVE:STACK:%@", line); + } + NSLog(@"CN1SS:NATIVE:THREAD_DUMP:END reason=%@", label); + } @catch (NSException *ex) { + NSLog(@"CN1SS:NATIVE:THREAD_DUMP:ERROR reason=%@ exception=%@", reason, ex); + } +} + +-(BOOL)isSupported{ + return YES; +} + +@end From 610f2e23086839678d9e9216afa49cde59b26730 Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Thu, 12 Feb 2026 16:54:43 +0200 Subject: [PATCH 6/9] Remove unsupported Thread.isDaemon call from diagnostics --- .../examples/hellocodenameone/tests/Cn1ssDeviceRunner.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/Cn1ssDeviceRunner.java b/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/Cn1ssDeviceRunner.java index a931bc86ee..9253f664ef 100644 --- a/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/Cn1ssDeviceRunner.java +++ b/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/Cn1ssDeviceRunner.java @@ -155,7 +155,7 @@ private static void dumpDiagnostics(String reason) { Thread current = Thread.currentThread(); for (java.util.Map.Entry e : Thread.getAllStackTraces().entrySet()) { Thread t = e.getKey(); - log("CN1SS:THREAD:name=" + t.getName() + " id=" + t.getId() + " state=" + t.getState() + " daemon=" + t.isDaemon() + " current=" + (t == current)); + log("CN1SS:THREAD:name=" + t.getName() + " id=" + t.getId() + " state=" + t.getState() + " current=" + (t == current)); for (StackTraceElement ste : e.getValue()) { log("CN1SS:STACK:" + ste); } From 7d2c9e305d5e66c724429f03f968ee228b0523bb Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Thu, 12 Feb 2026 17:04:41 +0200 Subject: [PATCH 7/9] Use CN1-compatible stack diagnostics in device runner --- .../hellocodenameone/tests/Cn1ssDeviceRunner.java | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/Cn1ssDeviceRunner.java b/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/Cn1ssDeviceRunner.java index 9253f664ef..3e7b8503c2 100644 --- a/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/Cn1ssDeviceRunner.java +++ b/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/Cn1ssDeviceRunner.java @@ -151,17 +151,17 @@ private static void log(String msg) { private static void dumpDiagnostics(String reason) { try { - log("CN1SS:ERR:capturing java thread dump reason=" + reason); Thread current = Thread.currentThread(); - for (java.util.Map.Entry e : Thread.getAllStackTraces().entrySet()) { - Thread t = e.getKey(); - log("CN1SS:THREAD:name=" + t.getName() + " id=" + t.getId() + " state=" + t.getState() + " current=" + (t == current)); - for (StackTraceElement ste : e.getValue()) { - log("CN1SS:STACK:" + ste); + log("CN1SS:ERR:capturing java stack reason=" + reason + " thread=" + current.getName()); + String stack = Display.getInstance().getStackTrace(current, new RuntimeException("CN1SS diagnostics: " + reason)); + for (String s : StringUtil.tokenize(stack, '\n')) { + if (s.length() > 400) { + s = s.substring(0, 400); } + log("CN1SS:STACK:" + s); } } catch (Throwable t) { - log("CN1SS:ERR:failed to capture java thread dump=" + t); + log("CN1SS:ERR:failed to capture java stack=" + t); } try { From 47460c3cc0b009bdbb71c68f60f2aa879b4c1465 Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Thu, 12 Feb 2026 18:38:48 +0200 Subject: [PATCH 8/9] Fail fast on blank screenshots and add native crash dump hook --- .../hellocodenameone/TestDiagnosticsNativeImpl.java | 3 +++ .../hellocodenameone/TestDiagnosticsNative.java | 1 + .../hellocodenameone/tests/Cn1ssDeviceRunner.java | 12 ++++++++++++ .../tests/Cn1ssDeviceRunnerHelper.java | 3 +++ ...ples_hellocodenameone_TestDiagnosticsNativeImpl.h | 1 + ...ples_hellocodenameone_TestDiagnosticsNativeImpl.m | 8 ++++++++ 6 files changed, 28 insertions(+) diff --git a/scripts/hellocodenameone/android/src/main/java/com/codenameone/examples/hellocodenameone/TestDiagnosticsNativeImpl.java b/scripts/hellocodenameone/android/src/main/java/com/codenameone/examples/hellocodenameone/TestDiagnosticsNativeImpl.java index 8929488946..4282d1adbe 100644 --- a/scripts/hellocodenameone/android/src/main/java/com/codenameone/examples/hellocodenameone/TestDiagnosticsNativeImpl.java +++ b/scripts/hellocodenameone/android/src/main/java/com/codenameone/examples/hellocodenameone/TestDiagnosticsNativeImpl.java @@ -4,6 +4,9 @@ public class TestDiagnosticsNativeImpl { public void dumpNativeThreads(String reason) { } + public void failFastWithNativeThreadDump(String reason) { + } + public boolean isSupported() { return false; } diff --git a/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/TestDiagnosticsNative.java b/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/TestDiagnosticsNative.java index 6e7ea41003..e3c73ba3fb 100644 --- a/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/TestDiagnosticsNative.java +++ b/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/TestDiagnosticsNative.java @@ -4,4 +4,5 @@ public interface TestDiagnosticsNative extends NativeInterface { void dumpNativeThreads(String reason); + void failFastWithNativeThreadDump(String reason); } diff --git a/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/Cn1ssDeviceRunner.java b/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/Cn1ssDeviceRunner.java index 3e7b8503c2..4a68087a36 100644 --- a/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/Cn1ssDeviceRunner.java +++ b/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/Cn1ssDeviceRunner.java @@ -138,6 +138,7 @@ public void runSuite() { } TestReporting.getInstance().testExecutionFinished(getClass().getName()); if (suiteFailed) { + failFastWithNativeCrash("CN1SS suite failed"); throw new RuntimeException("CN1SS suite failed"); } if (CN.isSimulator()) { @@ -176,6 +177,17 @@ private static void dumpDiagnostics(String reason) { } } + private static void failFastWithNativeCrash(String reason) { + try { + TestDiagnosticsNative nativeDiagnostics = NativeLookup.create(TestDiagnosticsNative.class); + if (nativeDiagnostics != null && nativeDiagnostics.isSupported()) { + nativeDiagnostics.failFastWithNativeThreadDump(reason); + } + } catch (Throwable t) { + log("CN1SS:ERR:failed to invoke native fail-fast dump=" + t); + } + } + @Override protected void startApplicationInstance() { Cn1ssDeviceRunnerHelper.runOnEdtSync(() -> { diff --git a/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/Cn1ssDeviceRunnerHelper.java b/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/Cn1ssDeviceRunnerHelper.java index 0b4581115d..68da1ea77b 100644 --- a/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/Cn1ssDeviceRunnerHelper.java +++ b/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/Cn1ssDeviceRunnerHelper.java @@ -111,6 +111,9 @@ static Image captureScreenshotWithRetries(String safeName, Form current, int wid screenshot.dispose(); screenshot = null; Util.sleep(250); + } else { + screenshot.dispose(); + screenshot = null; } } return screenshot; diff --git a/scripts/hellocodenameone/ios/src/main/objectivec/com_codenameone_examples_hellocodenameone_TestDiagnosticsNativeImpl.h b/scripts/hellocodenameone/ios/src/main/objectivec/com_codenameone_examples_hellocodenameone_TestDiagnosticsNativeImpl.h index 00901e2263..51a8fa7dd9 100644 --- a/scripts/hellocodenameone/ios/src/main/objectivec/com_codenameone_examples_hellocodenameone_TestDiagnosticsNativeImpl.h +++ b/scripts/hellocodenameone/ios/src/main/objectivec/com_codenameone_examples_hellocodenameone_TestDiagnosticsNativeImpl.h @@ -4,6 +4,7 @@ } -(void)dumpNativeThreads:(NSString*)reason; +-(void)failFastWithNativeThreadDump:(NSString*)reason; -(BOOL)isSupported; @end diff --git a/scripts/hellocodenameone/ios/src/main/objectivec/com_codenameone_examples_hellocodenameone_TestDiagnosticsNativeImpl.m b/scripts/hellocodenameone/ios/src/main/objectivec/com_codenameone_examples_hellocodenameone_TestDiagnosticsNativeImpl.m index af5573c215..be73e29d0f 100644 --- a/scripts/hellocodenameone/ios/src/main/objectivec/com_codenameone_examples_hellocodenameone_TestDiagnosticsNativeImpl.m +++ b/scripts/hellocodenameone/ios/src/main/objectivec/com_codenameone_examples_hellocodenameone_TestDiagnosticsNativeImpl.m @@ -17,6 +17,14 @@ -(void)dumpNativeThreads:(NSString*)reason { } } +-(void)failFastWithNativeThreadDump:(NSString*)reason { + NSString *label = reason != nil ? reason : @"unspecified"; + NSLog(@"CN1SS:NATIVE:FAIL_FAST:BEGIN reason=%@", label); + [self dumpNativeThreads:label]; + NSLog(@"CN1SS:NATIVE:FAIL_FAST:ABORT reason=%@", label); + abort(); +} + -(BOOL)isSupported{ return YES; } From 0d5936068b395082c9036bd6d8d0df7235bf7aea Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Thu, 12 Feb 2026 18:39:40 +0200 Subject: [PATCH 9/9] Install iOS native signal handlers for crash diagnostics --- .../TestDiagnosticsNativeImpl.java | 3 ++ .../TestDiagnosticsNative.java | 1 + .../tests/Cn1ssDeviceRunner.java | 12 +++++ ...llocodenameone_TestDiagnosticsNativeImpl.h | 1 + ...llocodenameone_TestDiagnosticsNativeImpl.m | 49 +++++++++++++++++++ 5 files changed, 66 insertions(+) diff --git a/scripts/hellocodenameone/android/src/main/java/com/codenameone/examples/hellocodenameone/TestDiagnosticsNativeImpl.java b/scripts/hellocodenameone/android/src/main/java/com/codenameone/examples/hellocodenameone/TestDiagnosticsNativeImpl.java index 4282d1adbe..b063376a07 100644 --- a/scripts/hellocodenameone/android/src/main/java/com/codenameone/examples/hellocodenameone/TestDiagnosticsNativeImpl.java +++ b/scripts/hellocodenameone/android/src/main/java/com/codenameone/examples/hellocodenameone/TestDiagnosticsNativeImpl.java @@ -1,6 +1,9 @@ package com.codenameone.examples.hellocodenameone; public class TestDiagnosticsNativeImpl { + public void enableNativeCrashSignalLogging(String reason) { + } + public void dumpNativeThreads(String reason) { } diff --git a/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/TestDiagnosticsNative.java b/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/TestDiagnosticsNative.java index e3c73ba3fb..a4f2452bdc 100644 --- a/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/TestDiagnosticsNative.java +++ b/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/TestDiagnosticsNative.java @@ -3,6 +3,7 @@ import com.codename1.system.NativeInterface; public interface TestDiagnosticsNative extends NativeInterface { + void enableNativeCrashSignalLogging(String reason); void dumpNativeThreads(String reason); void failFastWithNativeThreadDump(String reason); } diff --git a/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/Cn1ssDeviceRunner.java b/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/Cn1ssDeviceRunner.java index 4a68087a36..5c0f8d1795 100644 --- a/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/Cn1ssDeviceRunner.java +++ b/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/Cn1ssDeviceRunner.java @@ -86,6 +86,7 @@ public static void addTest(BaseTest test) { public void runSuite() { boolean suiteFailed = false; + enableNativeCrashSignalLogging(); CN.callSerially(() -> { Display.getInstance().addEdtErrorHandler(e -> { log("CN1SS:ERR:exception caught in EDT " + e.getSource()); @@ -188,6 +189,17 @@ private static void failFastWithNativeCrash(String reason) { } } + private static void enableNativeCrashSignalLogging() { + try { + TestDiagnosticsNative nativeDiagnostics = NativeLookup.create(TestDiagnosticsNative.class); + if (nativeDiagnostics != null && nativeDiagnostics.isSupported()) { + nativeDiagnostics.enableNativeCrashSignalLogging("CN1SS runSuite start"); + } + } catch (Throwable t) { + log("CN1SS:ERR:failed to install native crash signal handlers=" + t); + } + } + @Override protected void startApplicationInstance() { Cn1ssDeviceRunnerHelper.runOnEdtSync(() -> { diff --git a/scripts/hellocodenameone/ios/src/main/objectivec/com_codenameone_examples_hellocodenameone_TestDiagnosticsNativeImpl.h b/scripts/hellocodenameone/ios/src/main/objectivec/com_codenameone_examples_hellocodenameone_TestDiagnosticsNativeImpl.h index 51a8fa7dd9..37ab2508b4 100644 --- a/scripts/hellocodenameone/ios/src/main/objectivec/com_codenameone_examples_hellocodenameone_TestDiagnosticsNativeImpl.h +++ b/scripts/hellocodenameone/ios/src/main/objectivec/com_codenameone_examples_hellocodenameone_TestDiagnosticsNativeImpl.h @@ -5,6 +5,7 @@ -(void)dumpNativeThreads:(NSString*)reason; -(void)failFastWithNativeThreadDump:(NSString*)reason; +-(void)enableNativeCrashSignalLogging:(NSString*)reason; -(BOOL)isSupported; @end diff --git a/scripts/hellocodenameone/ios/src/main/objectivec/com_codenameone_examples_hellocodenameone_TestDiagnosticsNativeImpl.m b/scripts/hellocodenameone/ios/src/main/objectivec/com_codenameone_examples_hellocodenameone_TestDiagnosticsNativeImpl.m index be73e29d0f..8cb032d52a 100644 --- a/scripts/hellocodenameone/ios/src/main/objectivec/com_codenameone_examples_hellocodenameone_TestDiagnosticsNativeImpl.m +++ b/scripts/hellocodenameone/ios/src/main/objectivec/com_codenameone_examples_hellocodenameone_TestDiagnosticsNativeImpl.m @@ -1,7 +1,56 @@ #import "com_codenameone_examples_hellocodenameone_TestDiagnosticsNativeImpl.h" +#include +#include +#include +#include + +static volatile sig_atomic_t cn1ssSignalHandlersInstalled = 0; + +static void cn1ss_writeLine(const char *line) { + if (line == NULL) { + return; + } + write(STDERR_FILENO, line, strlen(line)); + write(STDERR_FILENO, "\n", 1); +} + +static void cn1ss_signalHandler(int signo) { + cn1ss_writeLine("CN1SS:NATIVE:SIGNAL:BEGIN"); + switch (signo) { + case SIGABRT: cn1ss_writeLine("CN1SS:NATIVE:SIGNAL:type=SIGABRT"); break; + case SIGSEGV: cn1ss_writeLine("CN1SS:NATIVE:SIGNAL:type=SIGSEGV"); break; + case SIGBUS: cn1ss_writeLine("CN1SS:NATIVE:SIGNAL:type=SIGBUS"); break; + case SIGILL: cn1ss_writeLine("CN1SS:NATIVE:SIGNAL:type=SIGILL"); break; + case SIGFPE: cn1ss_writeLine("CN1SS:NATIVE:SIGNAL:type=SIGFPE"); break; + case SIGTRAP: cn1ss_writeLine("CN1SS:NATIVE:SIGNAL:type=SIGTRAP"); break; + default: cn1ss_writeLine("CN1SS:NATIVE:SIGNAL:type=OTHER"); break; + } + + void *stack[64]; + int frames = backtrace(stack, 64); + backtrace_symbols_fd(stack, frames, STDERR_FILENO); + cn1ss_writeLine("CN1SS:NATIVE:SIGNAL:END"); + + signal(signo, SIG_DFL); + raise(signo); +} @implementation com_codenameone_examples_hellocodenameone_TestDiagnosticsNativeImpl +-(void)enableNativeCrashSignalLogging:(NSString*)reason { + if (cn1ssSignalHandlersInstalled) { + return; + } + cn1ssSignalHandlersInstalled = 1; + NSLog(@"CN1SS:NATIVE:SIGNAL:install handlers reason=%@", reason != nil ? reason : @"unspecified"); + signal(SIGABRT, cn1ss_signalHandler); + signal(SIGSEGV, cn1ss_signalHandler); + signal(SIGBUS, cn1ss_signalHandler); + signal(SIGILL, cn1ss_signalHandler); + signal(SIGFPE, cn1ss_signalHandler); + signal(SIGTRAP, cn1ss_signalHandler); +} + -(void)dumpNativeThreads:(NSString*)reason { @try { NSString *label = reason != nil ? reason : @"unspecified";