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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions .github/workflows/scripts-ios.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ on:
- 'scripts/build-ios-port.sh'
- 'scripts/build-ios-app.sh'
- 'scripts/run-ios-ui-tests.sh'
- 'scripts/run-ios-native-tests.sh'
- 'scripts/ios/notification-tests/native-tests/**'
- 'scripts/ios/notification-tests/install-native-notification-tests.sh'
- 'scripts/ios/notification-tests/**'
- 'scripts/hellocodenameone/**'
- 'scripts/ios/tests/**'
- 'scripts/ios/screenshots/**'
Expand All @@ -32,6 +36,10 @@ on:
- 'scripts/build-ios-port.sh'
- 'scripts/build-ios-app.sh'
- 'scripts/run-ios-ui-tests.sh'
- 'scripts/run-ios-native-tests.sh'
- 'scripts/ios/notification-tests/native-tests/**'
- 'scripts/ios/notification-tests/install-native-notification-tests.sh'
- 'scripts/ios/notification-tests/**'
- 'scripts/hellocodenameone/**'
- 'scripts/ios/tests/**'
- 'scripts/ios/screenshots/**'
Expand Down Expand Up @@ -145,6 +153,17 @@ jobs:
"${{ steps.build-ios-app.outputs.scheme }}"
timeout-minutes: 30

- name: Run native iOS notification tests (XCTest)
env:
ARTIFACTS_DIR: ${{ github.workspace }}/artifacts
run: |
set -euo pipefail
mkdir -p "${ARTIFACTS_DIR}"
./scripts/run-ios-native-tests.sh \
"${{ steps.build-ios-app.outputs.workspace }}" \
"${{ steps.build-ios-app.outputs.scheme }}"
timeout-minutes: 20

- name: Upload iOS artifacts
if: always()
uses: actions/upload-artifact@v4
Expand Down
18 changes: 1 addition & 17 deletions Ports/iOSPort/nativeSources/CodenameOne_GLAppDelegate.m
Original file line number Diff line number Diff line change
Expand Up @@ -160,13 +160,6 @@ - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(
}
com_codename1_impl_ios_IOSImplementation_callback__(CN1_THREAD_GET_STATE_PASS_SINGLE_ARG);

if (launchOptions[UIApplicationLaunchOptionsLocalNotificationKey]) {
CN1Log(@"Background notification received");
UILocalNotification *notification = launchOptions[UIApplicationLaunchOptionsLocalNotificationKey];
com_codename1_impl_ios_IOSImplementation_localNotificationReceived___java_lang_String(CN1_THREAD_GET_STATE_PASS_ARG fromNSString(CN1_THREAD_GET_STATE_PASS_ARG [notification.userInfo valueForKey:@"__ios_id__"]));
application.applicationIconBadgeNumber = 0;
}

id locationValue = [launchOptions objectForKey:UIApplicationLaunchOptionsLocationKey];
if (locationValue) {
com_codename1_impl_ios_IOSImplementation_appDidLaunchWithLocation__(CN1_THREAD_GET_STATE_PASS_SINGLE_ARG);
Expand Down Expand Up @@ -440,7 +433,7 @@ - (void)userNotificationCenter:(UNUserNotificationCenter *)center willPresentNot
}


- (void)userNotificationCenter:(UNUserNotificationCenter *)center didReceiveNotificationResponse:(UNNotificationResponse *)response withCompletionHandler:(void (^)())completionHandler {
- (void)userNotificationCenter:(UNUserNotificationCenter *)center didReceiveNotificationResponse:(UNNotificationResponse *)response withCompletionHandler:(void (^)(void))completionHandler {
if (@available(iOS 10, *)) {
if( [response.notification.request.content.userInfo valueForKey:@"__ios_id__"] != NULL)
{
Expand Down Expand Up @@ -618,15 +611,6 @@ -(void)application:(UIApplication*)application didChangeStatusBarFrame:(CGRect)o
repaintUI();
}

- (void)application:(UIApplication*)application didReceiveLocalNotification:(UILocalNotification*)notification {
CN1Log(@"Received local notification while running: %@", notification);
if( [notification.userInfo valueForKey:@"__ios_id__"] != NULL)
{
NSString* alertValue = [notification.userInfo valueForKey:@"__ios_id__"];
com_codename1_impl_ios_IOSImplementation_localNotificationReceived___java_lang_String(CN1_THREAD_GET_STATE_PASS_ARG fromNSString(CN1_THREAD_GET_STATE_PASS_ARG alertValue));
}
}

#ifndef CN1_USE_ARC
- (void)dealloc
{
Expand Down
185 changes: 75 additions & 110 deletions Ports/iOSPort/nativeSources/IOSNative.m
Original file line number Diff line number Diff line change
Expand Up @@ -9733,18 +9733,63 @@ 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];
#ifdef CN1_INCLUDE_NOTIFICATIONS2
if (@available(iOS 10, *)) {
UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
dispatch_semaphore_t sem = dispatch_semaphore_create(0);
__block NSMutableArray<NSString *> *matches = [NSMutableArray array];
[center getPendingNotificationRequestsWithCompletionHandler:^(NSArray<UNNotificationRequest *> * _Nonnull requests) {
for (UNNotificationRequest *request in requests) {
NSString *uid = [NSString stringWithFormat:@"%@", [request.content.userInfo valueForKey:@"__ios_id__"]];
if ([nsId isEqualToString:uid] || [nsId isEqualToString:request.identifier]) {
[matches addObject:request.identifier];
}
}
dispatch_semaphore_signal(sem);
}];
dispatch_semaphore_wait(sem, dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5 * NSEC_PER_SEC)));
if ([matches count] > 0) {
[center removePendingNotificationRequestsWithIdentifiers:matches];
[center removeDeliveredNotificationsWithIdentifiers:matches];
}
}
#endif
}

#ifdef CN1_INCLUDE_NOTIFICATIONS2
static UNNotificationTrigger* cn1CreateNotificationTrigger(JAVA_LONG fireDate, JAVA_INT repeatType) API_AVAILABLE(ios(10.0));
static UNNotificationTrigger* cn1CreateNotificationTrigger(JAVA_LONG fireDate, JAVA_INT repeatType) {
NSTimeInterval targetTime = fireDate / 1000.0 + 1;
NSDate *targetDate = [NSDate dateWithTimeIntervalSince1970:targetTime];
NSTimeInterval delta = targetTime - [[NSDate date] timeIntervalSince1970];
if (delta < 1) {
delta = 1;
}

NSCalendar *calendar = [NSCalendar currentCalendar];
NSDateComponents *components;
switch (repeatType) {
case 0:
return [UNTimeIntervalNotificationTrigger triggerWithTimeInterval:delta repeats:NO];
case 1:
components = [calendar components:(NSCalendarUnitSecond) fromDate:targetDate];
return [UNCalendarNotificationTrigger triggerWithDateMatchingComponents:components repeats:YES];
case 3:
components = [calendar components:(NSCalendarUnitMinute | NSCalendarUnitSecond) fromDate:targetDate];
return [UNCalendarNotificationTrigger triggerWithDateMatchingComponents:components repeats:YES];
case 4:
components = [calendar components:(NSCalendarUnitHour | NSCalendarUnitMinute | NSCalendarUnitSecond) fromDate:targetDate];
return [UNCalendarNotificationTrigger triggerWithDateMatchingComponents:components repeats:YES];
case 5:
components = [calendar components:(NSCalendarUnitWeekday | NSCalendarUnitHour | NSCalendarUnitMinute | NSCalendarUnitSecond) fromDate:targetDate];
return [UNCalendarNotificationTrigger triggerWithDateMatchingComponents:components repeats:YES];
default:
CN1Log(@"Unknown repeat interval type %d. Ignoring repeat interval", repeatType);
return [UNTimeIntervalNotificationTrigger triggerWithTimeInterval:delta repeats:NO];
}
}
#endif

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
) {
Expand Down Expand Up @@ -9780,112 +9825,32 @@ JAVA_VOID com_codename1_impl_ios_IOSNative_sendLocalNotification___java_lang_Str
if (foreground) {
[dict setObject: @"true" forKey: @"foreground"];
}

if (NO && @available(iOS 10, *)) {
// November 23, 2020 - Steve
// Disabling this block, which uses the new UNUserNotifications API for sending local notifications,
// and opting to continue to use the old UILocalNotifications API for now. This is because
// the new API doesn't have an option to use a different repeat interval than the firstFire
// interval, and the UNUserNotifications API can still be used to receive the notification
// in the application delegate class fine.
// Eventually we'll probably want to switch to the new API, but for now, it is just too much work
// to try to replicate the functionality lost by the new API.
dispatch_sync(dispatch_get_main_queue(), ^{

UNMutableNotificationContent* content = [[UNMutableNotificationContent alloc] init];

content.title = [NSString localizedUserNotificationStringForKey:title arguments:nil];
content.body = [NSString localizedUserNotificationStringForKey:body
arguments:nil];
if (alertSound) {
content.sound = [UNNotificationSound soundNamed:toNSString(CN1_THREAD_STATE_PASS_ARG alertSound)];

}
if (badgeNumber >= 0) {

content.badge = [NSNumber numberWithInt:badgeNumber];
if (@available(iOS 10, *)) {
UNMutableNotificationContent* content = [[UNMutableNotificationContent alloc] init];
content.title = [NSString localizedUserNotificationStringForKey:title arguments:nil];
content.body = [NSString localizedUserNotificationStringForKey:body arguments:nil];
if (alertSound != NULL) {
NSString *soundName = toNSString(CN1_THREAD_STATE_PASS_ARG alertSound);
if (soundName != nil && [soundName length] > 0) {
content.sound = [UNNotificationSound soundNamed:soundName];
}
content.userInfo = dict;




UNTimeIntervalNotificationTrigger *trigger = [UNTimeIntervalNotificationTrigger triggerWithTimeInterval:fireDate/1000 - [[NSDate date] timeIntervalSince1970] + 1 repeats:NO];

// Create the request object.
UNNotificationRequest* request = [UNNotificationRequest
requestWithIdentifier:toNSString(CN1_THREAD_STATE_PASS_ARG notificationId) content:content trigger:trigger];



UNUserNotificationCenter* center = [UNUserNotificationCenter currentNotificationCenter];
UNAuthorizationOptions authOptions;
if (@available(iOS 12.0, *)) {
authOptions = UNAuthorizationOptionProvisional | UNAuthorizationOptionAlert | UNAuthorizationOptionSound | UNAuthorizationOptionBadge;

} else {
authOptions = UNAuthorizationOptionAlert | UNAuthorizationOptionSound | UNAuthorizationOptionBadge;
}
if (badgeNumber >= 0) {
content.badge = [NSNumber numberWithInt:badgeNumber];
}
content.userInfo = dict;

UNNotificationTrigger *trigger = cn1CreateNotificationTrigger(fireDate, repeatType);
UNNotificationRequest *request = [UNNotificationRequest requestWithIdentifier:notificationIdString content:content trigger:trigger];
UNUserNotificationCenter* center = [UNUserNotificationCenter currentNotificationCenter];
cn1CancelScheduledLocalNotificationById(notificationIdString);
[center addNotificationRequest:request withCompletionHandler:^(NSError * _Nullable error) {
if (error != nil) {
CN1Log(@"Failed to schedule local notification: %@", error.localizedDescription);
}
[center requestAuthorizationWithOptions:authOptions
completionHandler:^(BOOL granted, NSError * _Nullable error) {
[center addNotificationRequest:request withCompletionHandler:^(NSError * _Nullable error) {
if (error != nil) {
NSLog(@"%@", error.localizedDescription);
}
}];
}];

});


return;
}];
} else {
UILocalNotification *notification = [[UILocalNotification alloc] init];
notification.alertTitle = title;
notification.alertBody = body;

notification.soundName= toNSString(CN1_THREAD_STATE_PASS_ARG alertSound);
notification.fireDate = [NSDate dateWithTimeIntervalSince1970: fireDate/1000 + 1];
notification.timeZone = [NSTimeZone defaultTimeZone];
if (badgeNumber >= 0) {
notification.applicationIconBadgeNumber = badgeNumber;
}
switch (repeatType) {
case 0:
notification.repeatInterval = nil;
break;
case 1:
notification.repeatInterval = NSMinuteCalendarUnit;
break;
case 3:
notification.repeatInterval = NSHourCalendarUnit;
break;
case 4:
notification.repeatInterval = NSDayCalendarUnit;
break;
case 5:
notification.repeatInterval = NSWeekCalendarUnit;
break;
default:
CN1Log(@"Unknown repeat interval type %d. Ignoring repeat interval", repeatType);
notification.repeatInterval = nil;
}



notification.userInfo = dict;


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]];
}
#endif
[[UIApplication sharedApplication] scheduleLocalNotification: notification];
});
CN1Log(@"Ignoring local notification request on iOS versions below 10");
}
#endif
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1633,6 +1633,9 @@ public void usesClassMethod(String cls, String method) {
"ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;\n"
+ " ENABLE_BITCODE = NO;\n");
}
if (xcodeVersion >= 9) {
replaceAllInFile(pbx, "ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = LaunchImage;", "");
}
}

if (useMetal) {
Expand All @@ -1642,6 +1645,12 @@ public void usesClassMethod(String cls, String method) {
} catch (Exception ex) {
throw new BuildException("Failed to update infoplist file", ex);
}

try {
normalizeAssetCatalogs(request);
} catch (Exception ex) {
throw new BuildException("Failed to normalize iOS asset catalogs", ex);
}
stopwatch.split("Post-VM Setup");
if (runPods) {
try {
Expand Down Expand Up @@ -2687,9 +2696,11 @@ private boolean generateIcons(BuildRequest request) throws Exception {
File resDir = getResDir();

BufferedImage iconImage = ImageIO.read(new ByteArrayInputStream(request.getIcon()));
icon512 = new File(iconDirectory, "iTunesArtwork");
// Legacy iOS icon files are still copied into the root resources, but should not
// be placed inside AppIcon.appiconset as they are not referenced by Contents.json.
icon512 = new File(resDir, "iTunesArtwork");
createFile(icon512, request.getIcon());
icon57 = new File(iconDirectory, "Icon.png");
icon57 = new File(resDir, "Icon.png");
createIconFile(icon57, iconImage, 57, 57);
createIconFile(new File(iconDirectory, "iPhoneNotification@2x.png"), iconImage, 40, 40);
createIconFile(new File(iconDirectory, "iPhoneNotification@3x.png"), iconImage, 60, 60);
Expand Down Expand Up @@ -2717,9 +2728,6 @@ private boolean generateIcons(BuildRequest request) throws Exception {
createIconFile(new File(iconDirectory, "iPadPro@2x.png"), iconImage, 167, 167);
createIconFile(new File(iconDirectory, "AppStore.png"), iconImage, 1024, 1024);


copy(icon512, new File(resDir, icon512.getName()));
copy(icon57, new File(resDir, icon57.getName()));
copyIcons(iconDirectory, resDir,
"iPhoneNotification@2x.png",
"iPhoneNotification@3x.png",
Expand Down Expand Up @@ -2770,10 +2778,34 @@ private boolean generateLaunchScreen(BuildRequest request) throws Exception {
copy(defaultLaunchStoryBoard, launchStoryBoard);
}
defaultLaunchStoryBoard.delete();

// Xcode 9+ uses LaunchScreen.storyboard. Keeping the legacy launch image set
// causes asset-catalog warnings for many missing legacy image files.
File legacyLaunchImages = new File(tmpFile, "dist/" + request.getMainClass() + "-src/Images.xcassets/LaunchImage.launchimage");
if (legacyLaunchImages.exists()) {
delTree(legacyLaunchImages);
}
}
return true;
}

private void normalizeAssetCatalogs(BuildRequest request) throws IOException {
File appSrcDir = new File(tmpFile, "dist/" + request.getMainClass() + "-src");
File appIconContents = new File(appSrcDir, "Images.xcassets/AppIcon.appiconset/Contents.json");
if (appIconContents.exists()) {
replaceInFile(appIconContents,
",\n {\n \"size\" : \"120x120\",\n \"idiom\" : \"iphone\",\n \"filename\" : \"Icon7@2x.png\",\n \"scale\" : \"1x\"\n },\n {\n \"size\" : \"167x167\",\n \"idiom\" : \"ipad\",\n \"filename\" : \"Icon-167.png\",\n \"scale\" : \"3x\"\n }",
"");
}

if (xcodeVersion >= 9) {
File legacyLaunchImages = new File(appSrcDir, "Images.xcassets/LaunchImage.launchimage");
if (legacyLaunchImages.exists()) {
delTree(legacyLaunchImages);
}
}
}


private static String createReverseGoogleClientId(String clientId) {
String[] parts = clientId.split("\\.");
Expand Down
Loading
Loading