diff --git a/Ports/iOSPort/nativeSources/CodenameOne_GLAppDelegate.m b/Ports/iOSPort/nativeSources/CodenameOne_GLAppDelegate.m index 0c15173d1f..02e4b3c3c7 100644 --- a/Ports/iOSPort/nativeSources/CodenameOne_GLAppDelegate.m +++ b/Ports/iOSPort/nativeSources/CodenameOne_GLAppDelegate.m @@ -181,9 +181,11 @@ - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:( if (isIOS10()) { UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter]; center.delegate = self; +#if !TARGET_OS_SIMULATOR [center requestAuthorizationWithOptions:(UNAuthorizationOptionSound | UNAuthorizationOptionAlert | UNAuthorizationOptionBadge) completionHandler:^(BOOL granted, NSError * _Nullable error){ if( !error ) {} }]; +#endif } } #endif diff --git a/Ports/iOSPort/nativeSources/IOSNative.m b/Ports/iOSPort/nativeSources/IOSNative.m index 1800226307..adc39439f1 100644 --- a/Ports/iOSPort/nativeSources/IOSNative.m +++ b/Ports/iOSPort/nativeSources/IOSNative.m @@ -9729,6 +9729,22 @@ JAVA_INT com_codename1_impl_ios_IOSNative_readNSFile___long(CN1_THREAD_STATE_MUL #endif +static void cn1CancelScheduledLocalNotificationById(NSString *nsId) { + if (nsId == nil) { + return; + } + UIApplication *app = [UIApplication sharedApplication]; + NSArray *eventArray = [app scheduledLocalNotifications]; + for (int i = 0; i < [eventArray count]; i++) { + UILocalNotification *n = [eventArray objectAtIndex:i]; + NSDictionary *userInfo = n.userInfo; + NSString *uid = [NSString stringWithFormat:@"%@", [userInfo valueForKey:@"__ios_id__"]]; + if ([nsId isEqualToString:uid]) { + [app cancelLocalNotification:n]; + } + } +} + JAVA_VOID com_codename1_impl_ios_IOSNative_sendLocalNotification___java_lang_String_java_lang_String_java_lang_String_java_lang_String_int_long_int_boolean( CN1_THREAD_STATE_MULTI_ARG 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 ) { @@ -9759,7 +9775,8 @@ 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__"]; + NSString *notificationIdString = toNSString(CN1_THREAD_STATE_PASS_ARG notificationId); + [dict setObject: notificationIdString forKey: @"__ios_id__"]; if (foreground) { [dict setObject: @"true" forKey: @"foreground"]; } @@ -9861,6 +9878,7 @@ JAVA_VOID com_codename1_impl_ios_IOSNative_sendLocalNotification___java_lang_Str dispatch_sync(dispatch_get_main_queue(), ^{ + cn1CancelScheduledLocalNotificationById(notificationIdString); #ifdef __IPHONE_8_0 if ([UIApplication instancesRespondToSelector:@selector(registerUserNotificationSettings:)]){ [[UIApplication sharedApplication] registerUserNotificationSettings:[UIUserNotificationSettings settingsForTypes:UIUserNotificationTypeAlert|UIUserNotificationTypeBadge|UIUserNotificationTypeSound categories:nil]]; @@ -9878,16 +9896,7 @@ JAVA_VOID com_codename1_impl_ios_IOSNative_cancelLocalNotification___java_lang_S } NSString *nsId = toNSString(CN1_THREAD_STATE_PASS_ARG notificationId); dispatch_sync(dispatch_get_main_queue(), ^{ - UIApplication *app = [UIApplication sharedApplication]; - NSArray *eventArray = [app scheduledLocalNotifications]; - for (int i=0; i<[eventArray count]; i++) { - UILocalNotification *n = [eventArray objectAtIndex:i]; - NSDictionary *userInfo = n.userInfo; - NSString *uid = [NSString stringWithFormat:@"%@", [userInfo valueForKey: @"__ios_id__"]]; - if ([nsId isEqualToString:uid]) { - [app cancelLocalNotification:n]; - } - } + cn1CancelScheduledLocalNotificationById(nsId); }); } diff --git a/docs/developer-guide/Working-With-iOS.asciidoc b/docs/developer-guide/Working-With-iOS.asciidoc index 6f6959558f..5b1ee8b232 100644 --- a/docs/developer-guide/Working-With-iOS.asciidoc +++ b/docs/developer-guide/Working-With-iOS.asciidoc @@ -105,6 +105,8 @@ Display.getInstance().scheduleLocalNotification( ); ----- +NOTE: On iOS, scheduling a local notification with an `id` that already exists now replaces the previously scheduled notification with that same `id` instead of keeping duplicates. + The resulting notification will look like .Resulting notification in iOS diff --git a/scripts/hellocodenameone/android/src/main/java/com/codenameone/examples/hellocodenameone/InPlaceEditViewNativeImpl.java b/scripts/hellocodenameone/android/src/main/java/com/codenameone/examples/hellocodenameone/InPlaceEditViewNativeImpl.java index 0091c4fcac..7a3733b6d5 100644 --- a/scripts/hellocodenameone/android/src/main/java/com/codenameone/examples/hellocodenameone/InPlaceEditViewNativeImpl.java +++ b/scripts/hellocodenameone/android/src/main/java/com/codenameone/examples/hellocodenameone/InPlaceEditViewNativeImpl.java @@ -7,9 +7,10 @@ import com.codename1.ui.layouts.BoxLayout; import com.codename1.impl.android.InPlaceEditView; import com.codename1.impl.android.AndroidImplementation; +import com.codenameone.examples.hellocodenameone.tests.InPlaceEditViewTest; public class InPlaceEditViewNativeImpl { - public void runReproductionTest(final ReproductionCallback callback) { + public void runReproductionTest() { Display.getInstance().callSerially(() -> { try { java.lang.reflect.Method getImplMethod = Display.class.getDeclaredMethod("getImplementation"); @@ -17,8 +18,8 @@ public void runReproductionTest(final ReproductionCallback callback) { final Object impl = getImplMethod.invoke(Display.getInstance()); if (!(impl instanceof AndroidImplementation)) { - Display.getInstance().callSerially(() -> callback.onResult(false, "Implementation is not AndroidImplementation: " + impl.getClass().getName())); - return; + Display.getInstance().callSerially(() -> InPlaceEditViewTest.onError("Implementation is not AndroidImplementation: " + impl.getClass().getName())); + return; } final AndroidImplementation androidImpl = (AndroidImplementation) impl; @@ -57,16 +58,16 @@ public void runReproductionTest(final ReproductionCallback callback) { } }); } - Display.getInstance().callSerially(() -> callback.onResult(true, null)); + Display.getInstance().callSerially(() -> InPlaceEditViewTest.onSuccess()); } catch (Throwable t) { t.printStackTrace(); - Display.getInstance().callSerially(() -> callback.onResult(false, t.toString())); + Display.getInstance().callSerially(() -> InPlaceEditViewTest.onError(t.toString())); } }).start(); } catch (Throwable t) { t.printStackTrace(); - Display.getInstance().callSerially(() -> callback.onResult(false, t.toString())); + Display.getInstance().callSerially(() -> InPlaceEditViewTest.onError(t.toString())); } }); } @@ -74,4 +75,5 @@ public void runReproductionTest(final ReproductionCallback callback) { public boolean isSupported() { return true; } + } 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..c9f92c64b9 --- /dev/null +++ b/scripts/hellocodenameone/android/src/main/java/com/codenameone/examples/hellocodenameone/LocalNotificationNativeImpl.java @@ -0,0 +1,15 @@ +package com.codenameone.examples.hellocodenameone; + +public class LocalNotificationNativeImpl { + public void clearScheduledLocalNotifications(String param) { + } + + public int getScheduledLocalNotificationCount(String param) { + return 0; + } + + public boolean isSupported() { + return false; + } + +} diff --git a/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/InPlaceEditViewNative.java b/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/InPlaceEditViewNative.java index c936ed9873..36f274191a 100644 --- a/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/InPlaceEditViewNative.java +++ b/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/InPlaceEditViewNative.java @@ -3,5 +3,5 @@ import com.codename1.system.NativeInterface; public interface InPlaceEditViewNative extends NativeInterface { - void runReproductionTest(ReproductionCallback callback); + void runReproductionTest(); } 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..ee00c4bdd2 --- /dev/null +++ b/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/LocalNotificationNative.java @@ -0,0 +1,8 @@ +package com.codenameone.examples.hellocodenameone; + +import com.codename1.system.NativeInterface; + +public interface LocalNotificationNative extends NativeInterface { + void clearScheduledLocalNotifications(String notificationId); + int getScheduledLocalNotificationCount(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..60835b8fc4 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 @@ -75,6 +75,7 @@ public final class Cn1ssDeviceRunner extends DeviceRunner { new BackgroundThreadUiAccessTest(), new VPNDetectionAPITest(), new CallDetectionAPITest(), + new LocalNotificationOverrideTest(), new AccessibilityTest())); public static void addTest(BaseTest test) { diff --git a/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/InPlaceEditViewTest.java b/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/InPlaceEditViewTest.java index 599be9b275..041d4849a0 100644 --- a/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/InPlaceEditViewTest.java +++ b/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/InPlaceEditViewTest.java @@ -5,6 +5,8 @@ import com.codename1.ui.Display; public class InPlaceEditViewTest extends BaseTest { + private static InPlaceEditViewTest instance; + @Override public boolean shouldTakeScreenshot() { return false; @@ -12,18 +14,21 @@ public boolean shouldTakeScreenshot() { @Override public boolean runTest() throws Exception { + instance = this; InPlaceEditViewNative nativeInterface = NativeLookup.create(InPlaceEditViewNative.class); if (nativeInterface != null && nativeInterface.isSupported()) { - nativeInterface.runReproductionTest((success, error) -> { - if (!success) { - fail("Reproduction test failed: " + error); - } else { - done(); - } - }); + nativeInterface.runReproductionTest(); } else { done(); } return true; } + + public static void onSuccess() { + instance.done(); + } + + public static void onError(String error) { + instance.fail("Reproduction test failed: " + error); + } } diff --git a/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/LocalNotificationOverrideTest.java b/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/LocalNotificationOverrideTest.java new file mode 100644 index 0000000000..b4ab24cf35 --- /dev/null +++ b/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/LocalNotificationOverrideTest.java @@ -0,0 +1,56 @@ +package com.codenameone.examples.hellocodenameone.tests; + +import com.codename1.notifications.LocalNotification; +import com.codename1.system.NativeLookup; +import com.codename1.ui.Display; +import com.codenameone.examples.hellocodenameone.LocalNotificationNative; + +public class LocalNotificationOverrideTest extends BaseTest { + private static final long FIRE_OFFSET_MS = 5 * 60 * 1000L; + + @Override + public boolean runTest() { + LocalNotificationNative nativeInterface = NativeLookup.create(LocalNotificationNative.class); + if (nativeInterface == null || !nativeInterface.isSupported()) { + done(); + return true; + } + + try { + String notificationId = "cn1-local-notification-override-" + System.currentTimeMillis(); + nativeInterface.clearScheduledLocalNotifications(notificationId); + + schedule(notificationId, "first"); + schedule(notificationId, "second"); + + int count = nativeInterface.getScheduledLocalNotificationCount(notificationId); + nativeInterface.clearScheduledLocalNotifications(notificationId); + + if (count != 1) { + fail("Expected one scheduled notification for duplicate id, but found " + count); + return true; + } + done(); + } catch (Throwable t) { + fail("Local notification override test failed: " + t); + } + return true; + } + + private void schedule(String notificationId, String body) { + LocalNotification notification = new LocalNotification(); + notification.setId(notificationId); + notification.setAlertTitle("Local Notification Override Test"); + notification.setAlertBody(body); + Display.getInstance().scheduleLocalNotification( + notification, + System.currentTimeMillis() + FIRE_OFFSET_MS, + LocalNotification.REPEAT_NONE + ); + } + + @Override + public boolean shouldTakeScreenshot() { + return false; + } +} diff --git a/scripts/hellocodenameone/ios/src/main/java/com/codenameone/examples/hellocodenameone/InPlaceEditViewNativeImpl.java b/scripts/hellocodenameone/ios/src/main/java/com/codenameone/examples/hellocodenameone/InPlaceEditViewNativeImpl.java deleted file mode 100644 index 8a6263700b..0000000000 --- a/scripts/hellocodenameone/ios/src/main/java/com/codenameone/examples/hellocodenameone/InPlaceEditViewNativeImpl.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.codenameone.examples.hellocodenameone; - -public class InPlaceEditViewNativeImpl { - public void runReproductionTest(ReproductionCallback callback) { - } - - public boolean isSupported() { - return false; - } -} diff --git a/scripts/hellocodenameone/ios/src/main/objectivec/com_codenameone_examples_hellocodenameone_InPlaceEditViewNativeImpl.h b/scripts/hellocodenameone/ios/src/main/objectivec/com_codenameone_examples_hellocodenameone_InPlaceEditViewNativeImpl.h index fa7ba872c8..6c20a29175 100644 --- a/scripts/hellocodenameone/ios/src/main/objectivec/com_codenameone_examples_hellocodenameone_InPlaceEditViewNativeImpl.h +++ b/scripts/hellocodenameone/ios/src/main/objectivec/com_codenameone_examples_hellocodenameone_InPlaceEditViewNativeImpl.h @@ -3,7 +3,6 @@ @interface com_codenameone_examples_hellocodenameone_InPlaceEditViewNativeImpl : NSObject { } --(void)runReproductionTest:(id)param; +-(void)runReproductionTest; -(BOOL)isSupported; - @end diff --git a/scripts/hellocodenameone/ios/src/main/objectivec/com_codenameone_examples_hellocodenameone_InPlaceEditViewNativeImpl.m b/scripts/hellocodenameone/ios/src/main/objectivec/com_codenameone_examples_hellocodenameone_InPlaceEditViewNativeImpl.m index 65387fd891..d83d25e941 100644 --- a/scripts/hellocodenameone/ios/src/main/objectivec/com_codenameone_examples_hellocodenameone_InPlaceEditViewNativeImpl.m +++ b/scripts/hellocodenameone/ios/src/main/objectivec/com_codenameone_examples_hellocodenameone_InPlaceEditViewNativeImpl.m @@ -2,7 +2,7 @@ @implementation com_codenameone_examples_hellocodenameone_InPlaceEditViewNativeImpl --(void)runReproductionTest:(id)param{ +-(void)runReproductionTest{ } -(BOOL)isSupported{ 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..bb2edfa8a4 --- /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 { +} + +-(void)clearScheduledLocalNotifications:(NSString*)param; +-(int)getScheduledLocalNotificationCount:(NSString*)param; +-(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..a0b075c8f7 --- /dev/null +++ b/scripts/hellocodenameone/ios/src/main/objectivec/com_codenameone_examples_hellocodenameone_LocalNotificationNativeImpl.m @@ -0,0 +1,46 @@ +#import "com_codenameone_examples_hellocodenameone_LocalNotificationNativeImpl.h" +#import + +@implementation com_codenameone_examples_hellocodenameone_LocalNotificationNativeImpl + +-(void)clearScheduledLocalNotifications:(NSString*)param{ + if (param == nil) { + return; + } + dispatch_sync(dispatch_get_main_queue(), ^{ + UIApplication *app = [UIApplication sharedApplication]; + NSArray *scheduled = [app scheduledLocalNotifications]; + for (UILocalNotification *notification in scheduled) { + NSDictionary *userInfo = notification.userInfo; + NSString *uid = [NSString stringWithFormat:@"%@", [userInfo valueForKey:@"__ios_id__"]]; + if ([param isEqualToString:uid]) { + [app cancelLocalNotification:notification]; + } + } + }); +} + +-(int)getScheduledLocalNotificationCount:(NSString*)param{ + if (param == nil) { + return 0; + } + __block int count = 0; + dispatch_sync(dispatch_get_main_queue(), ^{ + UIApplication *app = [UIApplication sharedApplication]; + NSArray *scheduled = [app scheduledLocalNotifications]; + for (UILocalNotification *notification in scheduled) { + NSDictionary *userInfo = notification.userInfo; + NSString *uid = [NSString stringWithFormat:@"%@", [userInfo valueForKey:@"__ios_id__"]]; + if ([param isEqualToString:uid]) { + count++; + } + } + }); + return count; +} + +-(BOOL)isSupported{ + return YES; +} + +@end diff --git a/scripts/hellocodenameone/javascript/src/main/javascript/com_codenameone_examples_hellocodenameone_InPlaceEditViewNative.js b/scripts/hellocodenameone/javascript/src/main/javascript/com_codenameone_examples_hellocodenameone_InPlaceEditViewNative.js new file mode 100644 index 0000000000..e95a812409 --- /dev/null +++ b/scripts/hellocodenameone/javascript/src/main/javascript/com_codenameone_examples_hellocodenameone_InPlaceEditViewNative.js @@ -0,0 +1,15 @@ +(function(exports){ + +var o = {}; + + o.runReproductionTest_ = function(callback) { + callback.error(new Error("Not implemented yet")); + }; + + o.isSupported_ = function(callback) { + callback.complete(false); + }; + +exports.com_codenameone_examples_hellocodenameone_InPlaceEditViewNative= o; + +})(cn1_get_native_interfaces()); diff --git a/scripts/hellocodenameone/javascript/src/main/javascript/com_codenameone_examples_hellocodenameone_LocalNotificationNative.js b/scripts/hellocodenameone/javascript/src/main/javascript/com_codenameone_examples_hellocodenameone_LocalNotificationNative.js new file mode 100644 index 0000000000..7528bfc5da --- /dev/null +++ b/scripts/hellocodenameone/javascript/src/main/javascript/com_codenameone_examples_hellocodenameone_LocalNotificationNative.js @@ -0,0 +1,19 @@ +(function(exports){ + +var o = {}; + + o.clearScheduledLocalNotifications__java_lang_String = function(param1, callback) { + callback.error(new Error("Not implemented yet")); + }; + + o.getScheduledLocalNotificationCount__java_lang_String = function(param1, callback) { + callback.error(new Error("Not implemented yet")); + }; + + o.isSupported_ = function(callback) { + callback.complete(false); + }; + +exports.com_codenameone_examples_hellocodenameone_LocalNotificationNative= o; + +})(cn1_get_native_interfaces()); diff --git a/scripts/hellocodenameone/javase/src/main/java/com/codenameone/examples/hellocodenameone/InPlaceEditViewNativeImpl.java b/scripts/hellocodenameone/javase/src/main/java/com/codenameone/examples/hellocodenameone/InPlaceEditViewNativeImpl.java new file mode 100644 index 0000000000..edd7a45311 --- /dev/null +++ b/scripts/hellocodenameone/javase/src/main/java/com/codenameone/examples/hellocodenameone/InPlaceEditViewNativeImpl.java @@ -0,0 +1,11 @@ +package com.codenameone.examples.hellocodenameone; + +public class InPlaceEditViewNativeImpl implements com.codenameone.examples.hellocodenameone.InPlaceEditViewNative{ + public void runReproductionTest() { + } + + public boolean isSupported() { + return false; + } + +} diff --git a/scripts/hellocodenameone/javase/src/main/java/com/codenameone/examples/hellocodenameone/LocalNotificationNativeImpl.java b/scripts/hellocodenameone/javase/src/main/java/com/codenameone/examples/hellocodenameone/LocalNotificationNativeImpl.java new file mode 100644 index 0000000000..f12c1dd122 --- /dev/null +++ b/scripts/hellocodenameone/javase/src/main/java/com/codenameone/examples/hellocodenameone/LocalNotificationNativeImpl.java @@ -0,0 +1,15 @@ +package com.codenameone.examples.hellocodenameone; + +public class LocalNotificationNativeImpl implements com.codenameone.examples.hellocodenameone.LocalNotificationNative{ + public void clearScheduledLocalNotifications(String param) { + } + + public int getScheduledLocalNotificationCount(String param) { + return 0; + } + + public boolean isSupported() { + return false; + } + +} diff --git a/scripts/hellocodenameone/win/src/main/csharp/com/codenameone/examples/hellocodenameone/InPlaceEditViewNativeImpl.cs b/scripts/hellocodenameone/win/src/main/csharp/com/codenameone/examples/hellocodenameone/InPlaceEditViewNativeImpl.cs new file mode 100644 index 0000000000..05661041f4 --- /dev/null +++ b/scripts/hellocodenameone/win/src/main/csharp/com/codenameone/examples/hellocodenameone/InPlaceEditViewNativeImpl.cs @@ -0,0 +1,13 @@ +namespace com.codenameone.examples.hellocodenameone{ + + +public class InPlaceEditViewNativeImpl : IInPlaceEditViewNativeImpl { + public void runReproductionTest() { + } + + public bool isSupported() { + return false; + } + +} +} diff --git a/scripts/hellocodenameone/win/src/main/csharp/com/codenameone/examples/hellocodenameone/LocalNotificationNativeImpl.cs b/scripts/hellocodenameone/win/src/main/csharp/com/codenameone/examples/hellocodenameone/LocalNotificationNativeImpl.cs new file mode 100644 index 0000000000..940dd06c6a --- /dev/null +++ b/scripts/hellocodenameone/win/src/main/csharp/com/codenameone/examples/hellocodenameone/LocalNotificationNativeImpl.cs @@ -0,0 +1,17 @@ +namespace com.codenameone.examples.hellocodenameone{ + + +public class LocalNotificationNativeImpl : ILocalNotificationNativeImpl { + public void clearScheduledLocalNotifications(String param) { + } + + public int getScheduledLocalNotificationCount(String param) { + return 0; + } + + public bool isSupported() { + return false; + } + +} +} diff --git a/skills/ios-cn1ss-xcode-debug/SKILL.md b/skills/ios-cn1ss-xcode-debug/SKILL.md new file mode 100644 index 0000000000..493c899a38 --- /dev/null +++ b/skills/ios-cn1ss-xcode-debug/SKILL.md @@ -0,0 +1,133 @@ +--- +name: ios-cn1ss-xcode-debug +description: Reproduce and debug HelloCodenameOne iOS CN1SS build/test failures on a Mac using local ios-source generation plus xcodebuild logs. +--- + +# iOS CN1SS Xcode Debug + +Use this skill when CN1SS iOS runs fail, produce blank screenshots, or crash during build/test. +This workflow is validated to compile and run locally against repository snapshots. + +## Preconditions + +- macOS with Xcode CLI tools installed (`xcodebuild` available). +- Java 8 available locally. +- Repository root checked out. + +## Fast Workflow + +1. Set Java 8 for Maven runs. +2. Install CN1 iOS artifacts from this repo into `~/.m2`. +3. Force-regenerate iOS local source for `scripts/hellocodenameone` so local snapshots are actually copied. +4. Run `xcodebuild` on the generated workspace. +5. Run the full CN1SS runner and inspect screenshot/log artifacts. +6. Extract first compile/runtime/test failure from logs. + +## Commands + +Run from repo root unless noted. + +```bash +export JAVA_HOME=/Users/shai/Library/Java/JavaVirtualMachines/azul-1.8.0_372/Contents/Home +export PATH="$JAVA_HOME/bin:$PATH" +``` + +```bash +cd maven +mvn -q -pl ios -am -DskipTests -Dmaven.javadoc.skip=true install +``` + +```bash +cd ../scripts/hellocodenameone +rm -rf ios/target/hellocodenameone-ios-1.0-SNAPSHOT-ios-source +./mvnw -q package -DskipTests -Dcodename1.platform=ios -Dcodename1.buildTarget=ios-source -o -e +``` + +```bash +cd ios/target/hellocodenameone-ios-1.0-SNAPSHOT-ios-source +xcodebuild -list -workspace HelloCodenameOne.xcworkspace +xcodebuild -workspace HelloCodenameOne.xcworkspace -scheme HelloCodenameOne -configuration Debug -sdk iphonesimulator -destination 'generic/platform=iOS Simulator' build +``` + +For full logs plus focused errors: + +```bash +xcodebuild -workspace HelloCodenameOne.xcworkspace -scheme HelloCodenameOne -configuration Debug -sdk iphonesimulator -destination 'generic/platform=iOS Simulator' build > /tmp/hellocn1-xcodebuild.log 2>&1 +rg -n "error:|fatal error:|undeclared function|BUILD FAILED|test host" /tmp/hellocn1-xcodebuild.log +``` + +Run the full UI test harness: + +```bash +XCODE_APP=/Applications/Xcode.app \ +scripts/run-ios-ui-tests.sh \ +scripts/hellocodenameone/ios/target/hellocodenameone-ios-1.0-SNAPSHOT-ios-source/HelloCodenameOne.xcworkspace \ +HelloCodenameOne +``` + +Post-run quick checks: + +```bash +ls -1 artifacts/*.png | wc -l +shasum artifacts/*.png | sort | uniq -c | sort -nr | head +rg -n "CN1SS:ERR|CN1SS:SUITE:FINISHED" artifacts/device-runner.log +``` + +## Common Failure Signatures + +- `Could not find test host for HelloCodenameOneTests` + - The generated unit-test scheme host path is invalid. Prefer building/running `HelloCodenameOne` scheme directly for CN1SS troubleshooting. + +- `call to undeclared function 'virtual_com_codename1_...'` + - Common cause: static invocation of an instance API in app/test code. + - Verify generated symbols in `HelloCodenameOne-src/com_codename1_*.m` and match call style. + - Reinstall local CN1 maven artifacts from current repo (`maven -pl ios -am`) and regenerate ios-source. + - Re-generate `ios-source` after reinstall. + +- Build succeeds but screenshots are blank or repetitive + - Check CN1SS log markers in app/test output: + - `CN1SS:INFO:test=... png_bytes=...` + - `CN1SS:ERR:...` + - If PNG bytes are tiny or absent, investigate app startup and current form readiness (`Cn1ssDeviceRunnerHelper.emitCurrentFormScreenshot` path). + - If many screenshots share the same hash, the runner is capturing a stable frame repeatedly. Check test navigation/state transitions before capture. + - If tests appear hung and simulator shows system permission alerts, this is usually not a screenshot pipeline bug; unblock/disable the modal first. + +- `Scheme file not found for env injection ... xcschemes/*.xcscheme` + - The runner can continue, but scheme-based env injection is skipped. + - Inspect `artifacts/device-runner.log` and decoded screenshots directly. + +## Artifacts To Inspect + +- Generated iOS project: + - `scripts/hellocodenameone/ios/target/hellocodenameone-ios-1.0-SNAPSHOT-ios-source` +- Build log: + - `/tmp/hellocn1-xcodebuild.log` +- CN1SS device log: + - `artifacts/device-runner.log` +- Decoded screenshots: + - `artifacts/*.png` +- Xcode derived data for this project: + - `~/Library/Developer/Xcode/DerivedData/HelloCodenameOne-*` + +## Notes + +- `scripts/hellocodenameone/ios/pom.xml` includes Objective-C files from `src/main/objectivec` as resources; headers should only include imports that are available in generated project context. +- If a native interface exists in common, platform impl classes should exist for every built platform variant used by generated stubs (at least android/ios/javase in this project). + +## Lessons Learned (Local Notifications + White Screenshots Incident) + +- Keep only stable fixes in core code: + - Temporary diagnostics (`NSLog`, extra CN1SS/CN1SHOT channels, timeout tweaks) are useful for triage but should be removed once root cause is proven. +- Propagation is a common trap: + - Rebuilding `maven/ios` is not enough if generated `ios-source` is stale. + - Always delete `scripts/hellocodenameone/ios/target/...-ios-source` before regeneration when validating port/native changes. +- Simulator behavior differs from device behavior: + - Startup notification authorization can block CN1SS runs with an iOS modal. + - A correct simulator-safe fix is to avoid notification authorization prompts on simulator startup paths. +- Stale SpringBoard state can mask fixes: + - After changing notification authorization behavior, erase the simulator before re-testing: + - `xcrun simctl shutdown ` + - `xcrun simctl erase ` +- Validate assumptions with direct evidence: + - Capture live simulator screenshots while tests run. + - Query logs directly with `simctl log show` for app process markers to confirm which code path actually executed. diff --git a/skills/ios-cn1ss-xcode-debug/agents/openai.yaml b/skills/ios-cn1ss-xcode-debug/agents/openai.yaml new file mode 100644 index 0000000000..b2e7c7c4eb --- /dev/null +++ b/skills/ios-cn1ss-xcode-debug/agents/openai.yaml @@ -0,0 +1,4 @@ +version: 1 +display_name: iOS CN1SS Xcode Debug +short_description: Reproduce and triage CN1SS iOS build/test failures with local Xcode logs. +default_prompt: Build local CN1 iOS snapshots, regenerate HelloCodenameOne ios-source offline, run xcodebuild and the CN1SS iOS runner, then summarize the first concrete failure and likely cause.