Wsparcie Apple Push Notification Service w aplikacji Adobe AIR na iOS

Wielką nieobecną w Adobe AIR dla iOS jest usługa Apple Push Notifications (APNs). Traktując to jako ciekawe wyzwanie postanowiłem spróbować dodać obsługę tej funkcjonalności iOS za pomocą natywnego rozszerzenia (native extension).
Nie przedłużając – udało się, chociaż nie było to tak trywialne jak stworzenie rozszerzenia MailComposer.

Rozszerzenie wraz z przykładową aplikacją można pobrać z GitHuba: https://github.com/pkoscierzynski/NativeAPNService

Aby wykorzysać APNs w swojej aplikacji należy uaktywnić tą usługę w Apple Developer portal i wygenerować certyfikaty developerskie i produkcyjne. Nie będę tego objaśniał – dużo lepiej zrobił to Matthijs Hollemans w tutorialu zamieszczonym na raywenderlich.com – polecam.
Apple Push Notification Services Tutorial: Part 1/2

W powyższym tutorialu zawarty jest kod PHP, który również wykorzystałem do swoich testów, także na pewno działa.

Kompilowanie samej aplikacji AIR 3.1 korzystającej z push notifications wymaga wypełnienia sekcji Entitlements w deskryptorze aplikacji. Należy tam umieścić treść analogiczną do tej znajdującej się w pliku Entitlements.plist znanego z Xcode.

Poniżej kilka screenów z samej testowej aplikacji i krótkie wideo pokazujące jej działanie.

Adobe AIR application descriptor. iPhone Entitlements section.

Apple push notifications in Adobe AIR iOS application from Piotr Koscierzynski on Vimeo.

Natywne rozszerzenia dla Adobe AIR pod iOS. Wysyłanie maila z poziomu aplikacji.


In this tutorial I will show how to create an iOS native extension for Adobe AIR. My extension enables sending an e-mail by invoking MFMailComposeViewController on iOS. This way you can send an e-mail without leaving you app, attachments are supported, too.

Grab the source from github: iOS In-app mail Native Extension for Adobe AIR

How to use iOS mail extension

Create an intance of class pl.randori.air.nativeextensions.ios.MailExtension and call method
sendMail(subject:String, messageBody:String, toRecipients:String, ccRecipients:String = ””, bccRecipients:String = ””, attachmentsData:Array = null):void

To add an attachment from application bundle:

    filename|bundle|file_mimetype|name_of_file_to_be_shown_in_mail

to  add an attachment from application documents directory use

    filename|documents|file_mimetype|name_of_file_to_be_shown_in_mail

sendMail method implementation should make it more clear

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
         * @param subject Mail subject
         * @param messageBody Mail body (can include HTML)
         * @param toRecipients To: recipients in format: "mail@example.com,mail2@example.com"
         * @param ccRecipients Cc: recipients in format: "mail@example.com,mail2@example.com"
         * @param bccRecipients Bcc: recipients in format: "mail@example.com,mail2@example.com"
         * @param attachmentsData Attachments in format: ['filename|bundle|mimetype|name of file to display in attachment']
         * example: ["Default.png|bundle|image/png|Application splash screen.png","Example file.dat|documents|text/xml|A file saved in Adobe AIR iOS app.txt"]
         */

        public function sendMail(subject:String, messageBody:String, toRecipients:String,
                                ccRecipients:String = '', bccRecipients:String = '', attachmentsData:Array = null):void {
           
            extensionContext.addEventListener( StatusEvent.STATUS, onStatusEvent);
            extensionContext.call( "sendMailWithOptions", subject, messageBody, toRecipients,
                                    ccRecipients, bccRecipients, attachmentsData);
        }

MailExtension dispatches MainExtensionEvent.MAIL_COMPOSER_EVENT to let you know what’s going on. You will be notified if the user has sent the mail, saved it, canceled, etc.
Also there will be notifications WILL_SHOW_MAIL_COMPOSER and WILL_HIDE_MAIL_COMPOSER so you could know when the mail composer is shown and dismissed.
This can be used to stop / resume task in AIR while the mail composer is present on the screen

 

 

Creating an native extension for Adobe AIR requires some iOS/Objective-C knowledge so don’t worry if not everything’s clear at the beginning.

I divided the process of development to three steps

1. Creating a native library in Xcode
2. Creating an ActionScript library which will act as a middleware between Adobe AIR and iOS
3. Creating an example Adobe AIR app

My development environtment was Flash Builder 4.5 with Flex SDK 4.5.1 and AIR SDK 3.1 and Xcode 4.2 and iOS SDK 5. Flash Builder 4.5 doesn’t support native extensions development, so I’ve used ANT to compile and package everything.

Before starting be sure to merge Flex SDK with Adobe AIR SDK.

Creating the native library in Xcode for AIR an iOS

A native library can be written in Objective-C, C/C++ or Java depending on the target platform. Currently extensions for iOS, Android, BlackBerry PlayBook and AIR TV are supported. The library exposes an API that the Adobe AIR application can use, which can include functionalities that are not available in the current release of Adobe AIR or make use of performance of native code (math, physics computations; image processing, etc.).

On iOS Adobe AIR cannot access to such APIs as: Game Center, In-App Purchase, Twitter or Bluetooth. With native extensions we can create a fully featured iOS app.

In Xcode create a new static library project and set the project setting as follows:

 

Set „Enable Linking with Shared Libraries” to NO if when packaging your app into an ipa archive you see in console a message that looks like:
ld warning: unexpected srelocation type 9


Add FlashRuntimeExtensions.h and implement required methods
Next, you need to add FlashRuntimeExtensions.h to your project. This file can be found in FLEX_SDK\include. This file will provide necessary data types definitions and functions that will be used for communication between native code and AIR app.

Native extension written using C API (which iOS extension are) requires four methods to be implemented:

  • extension initializer
  • extension finalizer
  • context initializer
  • context finalizer

In MailExtension.m those functions are: ExtInitializer, ExtFinalizer, ContextInitializer, ContextFinalizer. Those are just the names I used, they can actually be named anything you want, but you must provide those functions’ names in configuration xmls.

My main extension file MailExtension.m looks like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
//
//  MailExtension.m
//  MailExtension
//
//  Created by Piotr Kościerzyński on 11-11-29.
//  Copyright (c) 2011 Randori. All rights reserved.
//

#import "MailExtension.h"


@implementation MailExtension

static NSString *attachmentsSeparator = @"----";
static NSString *event_name = @"MAIL_COMPOSER_EVENT";

FREContext g_ctx;
MailComposerHelper *mailComposerHelper;

- (id)init
{
    self = [super init];
    if (self) {
        // Initialization code here.
    }
   
    return self;
}

int canSendMail(void) {

    BOOL result = NO;
   
    //On pre iOS 3.0 devices MFMailComposeViewController does not exists
    Class mailClass = (NSClassFromString(@"MFMailComposeViewController"));
    if (mailClass != nil) {
        // We must always check whether the current device is configured for sending emails
        if ([mailClass canSendMail]) {
            result = YES;
        }
        else {
            result = NO;
        }
    }
    //this will never happen since Adobe AIR requires at least iOS 4.0
    else {
        result = NO;
    }
    return (int)result;
}

//Can we invoke in-app mail ?
FREObject PKIsMailComposerAvailable(FREContext ctx, void* funcData, uint32_t argc, FREObject argv[]) {
   
    BOOL ret = canSendMail();
    FREObject retVal;
   
    FRENewObjectFromBool(ret, &retVal);
    return retVal;    
}

//Send mail
FREObject PKSendMailWithOptions(FREContext ctx, void* funcData, uint32_t argc, FREObject argv[] )
{
    BOOL ret = canSendMail();
   
    if (!ret) {
        FREDispatchStatusEventAsync(ctx, (uint8_t*)[event_name UTF8String], (uint8_t*)[@"MAIL_COMPOSER_NOT_AVAILABLE" UTF8String]);
        return NULL;
    }
   
   
    if(argc<3) {
       
        FREDispatchStatusEventAsync(ctx, (uint8_t*)[event_name UTF8String], (uint8_t*)[@"NOT_ENOUGH_PARAMETERS_PROVIDED" UTF8String]);
       
        return NULL;
    }
   
    //Subject
    uint32_t subjectLength;
    const uint8_t *subjectCString;
    //To Recipients
    uint32_t toRecipientsLength;
    const uint8_t *toRecipientsCString;
    //CC Recipients
    uint32_t ccRecipientsLength;
    const uint8_t *ccRecipientsCString;
    //Bcc Recipients
    uint32_t bccRecipientsLength;
    const uint8_t *bccRecipientsCString;
    //Message Body
    uint32_t messageBodyLength;
    const uint8_t *messageBodyCString;
   
    NSMutableString *attachmentsString = nil;
    NSString *subjectString = nil;
    NSString *toRecipientsString = nil;
    NSString *ccRecipientsString = nil;
    NSString *bccRecipientsString = nil;
    NSString *messageBodyString = nil;
   
    //Create NSStrings from CStrings
    if (FRE_OK == FREGetObjectAsUTF8(argv[0], &subjectLength, &subjectCString)) {
        subjectString = [NSString stringWithUTF8String:(char*)subjectCString];
    }
   
    if (FRE_OK == FREGetObjectAsUTF8(argv[1], &messageBodyLength, &messageBodyCString)) {
        messageBodyString = [NSString stringWithUTF8String:(char*)messageBodyCString];
    }
   
    if (FRE_OK == FREGetObjectAsUTF8(argv[2], &toRecipientsLength, &toRecipientsCString)) {
        toRecipientsString = [NSString stringWithUTF8String:(char*)toRecipientsCString];
    }
   
    if (argc >= 4 && (FRE_OK == FREGetObjectAsUTF8(argv[3], &ccRecipientsLength, &ccRecipientsCString))) {
        ccRecipientsString = [NSString stringWithUTF8String:(char*)ccRecipientsCString];
    }
   
    if (argc >= 5 && (FRE_OK == FREGetObjectAsUTF8(argv[4], &bccRecipientsLength, &bccRecipientsCString))) {
        bccRecipientsString = [NSString stringWithUTF8String:(char*)bccRecipientsCString];
    }
   
     uint32_t attachmentsArrayLength = 0;
   

    //argv[5] is a an array of strings
    if (argc >= 6 && (FRE_OK != FREGetArrayLength(argv[5], &attachmentsArrayLength))) {
        //No valid array of attachments provided.
    }
   
    if (attachmentsArrayLength >= 1) {

        attachmentsString = [[NSMutableString alloc ] init];
        uint32_t attachmentEntryLength;
        const uint8_t *attachmentEntryCString;
   
        for (int i = 0; i < attachmentsArrayLength; i++) {
           
            FREObject arrayElement;
            FREGetArrayElementAt(argv[5], i, &arrayElement);
            FREGetObjectAsUTF8(arrayElement, &attachmentEntryLength, &attachmentEntryCString);
       
            [attachmentsString appendString:[NSString stringWithUTF8String:(char*)attachmentEntryCString]];
       
            if (i<(attachmentsArrayLength-1))
                [attachmentsString appendString:attachmentsSeparator];
        }
    }    
   
    if (mailComposerHelper) {
    }
    else {
        mailComposerHelper = [[MailComposerHelper alloc] init];
    }

    [mailComposerHelper setContext:ctx];
    [mailComposerHelper sendMailWithSubject:subjectString
                               toRecipients:toRecipientsString
                               ccRecipients:ccRecipientsString
                              bccRecipients:bccRecipientsString
                                messageBody:messageBodyString
                            attachmentsData:attachmentsString];
   
    if (attachmentsString != nil)
        [attachmentsString release];
   
   
    return NULL;    
}


//------------------------------------
//
// Required Methods.
//
//------------------------------------

// ContextInitializer()
//
// The context initializer is called when the runtime creates the extension context instance.
void ContextInitializer(void* extData, const uint8_t* ctxType, FREContext ctx,
                        uint32_t* numFunctionsToTest, const FRENamedFunction** functionsToSet)
{
    //we expose two methods to ActionScript
    *numFunctionsToTest = 2;
   
    FRENamedFunction* func = (FRENamedFunction*) malloc(sizeof(FRENamedFunction) * 2);
    func[0].name = (const uint8_t*) "sendMailWithOptions";
    func[0].functionData = NULL;
    func[0].function = &PKSendMailWithOptions;
   
    func[1].name = (const uint8_t*) "isMailComposerAvailable";
    func[1].functionData = NULL;
    func[1].function = &PKIsMailComposerAvailable;
   
    *functionsToSet = func;
   
    g_ctx = ctx;
}

// ContextFinalizer()
//
// The context finalizer is called when the extension's ActionScript code
// calls the ExtensionContext instance's dispose() method.
// If the AIR runtime garbage collector disposes of the ExtensionContext instance, the runtime also calls
// ContextFinalizer().

void ContextFinalizer(FREContext ctx) {

    [mailComposerHelper setContext:NULL];
    [mailComposerHelper release];
    mailComposerHelper = nil;

    return;
}

// ExtInitializer()
//
// The extension initializer is called the first time the ActionScript side of the extension
// calls ExtensionContext.createExtensionContext() for any context.
void ExtInitializer(void** extDataToSet, FREContextInitializer* ctxInitializerToSet,
                    FREContextFinalizer* ctxFinalizerToSet) {
   
    *extDataToSet = NULL;
    *ctxInitializerToSet = &ContextInitializer;
    *ctxFinalizerToSet = &ContextFinalizer;

}

// ExtFinalizer()
//
// The extension finalizer is called when the runtime unloads the extension. However, it is not always called.
void ExtFinalizer(void* extData) {

    return;
}

@end

MailExtension.m is responsible for instantiating the extension and invoking the methods called from ActionScript.

Second part of Objective-C code is MailComposerHelper class which does all the work needed to send the mail.
This class manages MFMailComposeViewController and dispatches events back to ActionScript.

MailComposerHelper implementation looks like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
//
//  MailComposerHelper.m
//  NativeMail iOS extension for Adobe AIR
//
//  Created by Piotr Kościerzyński on 11-11-28.
//  Copyright (c) 2011 Randori. All rights reserved.
//

#import "MailComposerHelper.h"

@implementation MailComposerHelper

static NSString *attachmentPropertySeparator = @"|";
static NSString *attachmentsSeparator = @"----";
//Event name
static  NSString *event_name = @"MAIL_COMPOSER_EVENT";


-(void) sendMailWithSubject:(NSString *)subject
               toRecipients:(NSString *)toRecipients
               ccRecipients:(NSString *)ccRecipients
              bccRecipients:(NSString *)bccRecipients
                messageBody:(NSString *)messageBody
            attachmentsData:(NSString *)attachmentsData
{
   
    FREDispatchStatusEventAsync(context, (uint8_t*)[event_name UTF8String], (uint8_t*)[@"WILL_SHOW_MAIL_COMPOSER" UTF8String]);
   
   
    MFMailComposeViewController *mailComposer = [[MFMailComposeViewController alloc] init];
    mailComposer.mailComposeDelegate = self;
   
    if (subject != nil)
        [mailComposer setSubject: subject];
   
    if (messageBody != nil)
        [mailComposer setMessageBody:messageBody isHTML:YES];
   
    if (toRecipients != nil && [toRecipients rangeOfString:@"@"].location != NSNotFound)
        [mailComposer setToRecipients:[toRecipients componentsSeparatedByString:@","]];
   
    if (ccRecipients != nil && [ccRecipients rangeOfString:@"@"].location != NSNotFound)
        [mailComposer setCcRecipients:[ccRecipients componentsSeparatedByString:@","]];
   
    if (bccRecipients != nil && [bccRecipients rangeOfString:@"@"].location != NSNotFound)
        [mailComposer setBccRecipients:[bccRecipients componentsSeparatedByString:@","]];
   
   
   
    //Add attachments (if any)
    if (attachmentsData) {      
     
        NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
        NSString *documentsDirectory = [paths objectAtIndex:0];
        NSString *filePath;
       
        NSArray *attachmentProperties;
        NSString *fileName;
        NSString *fileExtension;
        NSString *fileSearchSource;
        NSString *fileMimeType;
        NSString *fileAttachName;
       
        NSArray *attachments = [attachmentsData componentsSeparatedByString:attachmentsSeparator];

        for (NSString *attachmentEntry in attachments) {
       
            attachmentProperties = [attachmentEntry componentsSeparatedByString:attachmentPropertySeparator];
            fileName = [[[attachmentProperties objectAtIndex:0] componentsSeparatedByString:@"."] objectAtIndex:0];
            fileExtension = [[[attachmentProperties objectAtIndex:0] componentsSeparatedByString:@"."] objectAtIndex:1];
            fileSearchSource = [(NSString *)[attachmentProperties objectAtIndex:1] lowercaseString];//bundle or documents
            fileMimeType = [attachmentProperties objectAtIndex:2];//mime type of file
            fileAttachName = [attachmentProperties objectAtIndex:3];//how to name the file
           
            //search for file in app bundle
            if ([fileSearchSource isEqualToString:@"bundle"]) {
                filePath = [[NSBundle mainBundle] pathForResource:fileName ofType:fileExtension];                
            }
            else
            //search for file in Documents
            if ([fileSearchSource isEqualToString:@"documents"]) {
                filePath = [documentsDirectory stringByAppendingPathComponent:(NSString *)[attachmentProperties objectAtIndex:0]];            
            }
            else {
                //ERROR - ignoring
                continue;
            }
       
            if ([[NSFileManager defaultManager] fileExistsAtPath:filePath]) {
       
                NSData *fileData = [[NSData alloc] initWithContentsOfFile:filePath];
       
                if (fileData) {
                    [mailComposer addAttachmentData: fileData mimeType:fileMimeType fileName:fileAttachName];            
                }
       
                [fileData release];
       
            }
        }
    }
   
       
    //show mail composer
    [[[[UIApplication sharedApplication] keyWindow] rootViewController] presentModalViewController:mailComposer animated:YES];
   
    [mailComposer release];

}

// Dismisses the email composition interface when users tap Cancel or Send.
-(void) mailComposeController: (MFMailComposeViewController*)controller didFinishWithResult: (MFMailComposeResult)result error:(NSError*)error
{  
    NSString *event_info = @"";
    // Notifies users about errors associated with the interface
    switch (result)
    {
        case MFMailComposeResultCancelled:
            event_info = @"MAIL_CANCELED";
            break;
        case MFMailComposeResultSaved:
            event_info = @"MAIL_SAVED";
            break;
        case MFMailComposeResultSent:
            event_info = @"MAIL_SENT";
            break;
        case MFMailComposeResultFailed:
            event_info = @"MAIL_FAILED";
            break;
        default:
            event_info = @"MAIL_UNKNOWN";
            break;
    }
   
    FREDispatchStatusEventAsync(context, (uint8_t*)[event_name UTF8String], (uint8_t*)[event_info UTF8String]);
    FREDispatchStatusEventAsync(context, (uint8_t*)[event_name UTF8String], (uint8_t*)[@"WILL_HIDE_MAIL_COMPOSER" UTF8String]);
   
    context = nil;

    //hide mail composer
    [[[[UIApplication sharedApplication] keyWindow] rootViewController] dismissModalViewControllerAnimated:YES];
}

-(void)setContext:(FREContext)ctx {
    context = ctx;
}


@end

Native iOS code can dispatch events for ActionScript – it’s done by calling FREDispatchStatusEventAsync. This will be seen as a StatusEvent.STATUS in Adobe AIR. I used it to let my application know whether the mail composer can be shown and to inform about the mail compose result and status.

When you build the static library you will get libMailExtension.a file in ‘build’ directory.
This file will be needed in ‘NativeMail-iOS’ library project to create .swc and .ane files.

 

Creating Adobe Native Extension project

In Flash Builder create a new ActionScript Library project. The project will contain ActionScript classes responsible for calling iOS code.

My project’s structure – you can see the libMailExtension.a file present

 

Since I used command line tools to create the extension there are  lot of configuration files and ANT scripts, if you’re using Flash Builder 4.6 it should be much simpler.

 

iOS library methods are invoked by calling extensionContext.call method.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
package pl.randori.air.nativeextensions.ios
{
    import flash.events.EventDispatcher;
    import flash.events.IEventDispatcher;
    import flash.events.StatusEvent;
    import flash.external.ExtensionContext;

   
    /**
     * An iOS native extension for Adobe AIR 3.1 for sending mail.
     * Implements MFMailComposeViewController in iOS
     *
     * @author Piotr Kościerzyński, piotr@flashsimulations.com
     * www.flashsimulations.com
     * www.randori.pl
     *
     * */

    public class MailExtension extends EventDispatcher
    {
       
        protected var extensionContext:ExtensionContext;
       
        private static const EXTENSION_ID : String = "pl.randori.air.nativeextensions.ios.MailExtension";
       
       
        public function MailExtension(target:IEventDispatcher=null)
        {
            super(target);
            extensionContext = ExtensionContext.createExtensionContext( EXTENSION_ID, null);
        }

       
        /**
         * @param subject Mail subject
         * @param messageBody Mail body (can include HTML)
         * @param toRecipients To: recipients in format: "mail@example.com,mail2@example.com"
         * @param ccRecipients Cc: recipients in format: "mail@example.com,mail2@example.com"
         * @param bccRecipients Bcc: recipients in format: "mail@example.com,mail2@example.com"
         * @param attachmentsData Attachments in format: ['filename|bundle|mimetype|name of file to display in attachment']
         * example: ["Default.png|bundle|image/png|Application splash screen.png","Example file.dat|documents|text/xml|A file saved in Adobe AIR iOS app.txt"]
         */

        public function sendMail(subject:String, messageBody:String, toRecipients:String,
                                ccRecipients:String = '', bccRecipients:String = '', attachmentsData:Array = null):void {
           
            extensionContext.addEventListener( StatusEvent.STATUS, onStatusEvent);
            extensionContext.call( "sendMailWithOptions", subject, messageBody, toRecipients,
                                    ccRecipients, bccRecipients, attachmentsData);
        }
       
        /**
         * @private
         * Handle mail compose result.
         * When the native mail composer finished an result event will be dispatched.
         * Event will contain the result information.
         *
         */
   
        private function onStatusEvent( event : StatusEvent ) : void
        {
            if( event.code == MailExtensionEvent.MAIL_COMPOSER_EVENT)
            {
                dispatchEvent( new MailExtensionEvent(event.code, event.level ));
            }
        }
       
        /**
         * Can the in-app mail composer be invoked?
         */
   
        public function isMailComposerAvailable() : Boolean
        {
            return extensionContext.call( "isMailComposerAvailable") as Boolean;           
        }
       
       
        /**
         * Clean up
         */

        public function dispose():void {
            extensionContext.removeEventListener( StatusEvent.STATUS, onStatusEvent );
            extensionContext.dispose();
        }
       
       
    }
}

ANT script used for creating .ane and .swc files:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
<?xml version="1.0" encoding="UTF-8"?>
<project name="NativeMail iOS for Adobe AIR Extension" default="build-extension" basedir=".">
   
    <property file="local.properties" />
    <property file="build.properties" />
   
    <target name="clean">
        <delete dir="${app.builddir}"/>
        <delete dir="${app.releasedir}"/>
        <mkdir dir="${app.builddir}"/>
        <mkdir dir="${app.releasedir}"/>
        <delete file="${app.rootdir}/library.swf"/>
        <delete file="${app.rootdir}/${app.swcfilename}"/>
    </target>
   
    <target name="build-extension" depends="clean">
        <exec executable="${ACOMPC}">
            <arg line="
                -output ${app.builddir}/${app.swcfilename}
                -load-config+=${app.configfile}
                +configname=airmobile
                -swf-version=14
            "/>

        </exec>
        <copy file="${app.builddir}/${app.swcfilename}" tofile="${app.rootdir}/${app.swcfilename}"/>
        <unzip src="${app.builddir}/${app.swcfilename}" dest="${app.rootdir}"/>
        <delete file="${app.rootdir}/catalog.xml"/>
        <exec executable="${ADT}">
                    <arg line="
                        -package -target ane ${app.releasedir}/iOS_MailExtension.ane ${app.extensionxmlfile}
                        -swc ${app.swcfilename}
                        -platform iPhone-ARM library.swf libMailExtension.a
                        -platformoptions ios-platformoptions.xml
                    "/>

        </exec>
        <delete file="${app.rootdir}/library.swf"/>
        <delete file="${app.rootdir}/${app.swcfilename}"/>
    </target>
</project>

Native extensions ADT parameters

One of the most important parameter in this script is  -platformoptions ios-platformoptions.xml . Why?
My extension uses iOS frameworks that are not linked by ADT packager by default, as a result the linker won’t fine MFMailComposeViewController definition and will fail.
Sadly, the documentation is very poor and I haven’t found any examples except from this one http://blogs.adobe.com/rajorshi/2011/11/16/ios5-support-for-airusing-external-sdks-to-package-apps/

MFMailComposeViewController requires MessageUI.framework so adding it to ADT’s linker solves the problem.

Also, note that -swf-version is set to 14 because we’re targeting for AIR 3.1. See: http://help.adobe.com/en_US/air/extensions/WS99209310cacd98cc2d13931c1300f2c84c7-8000.html

ios-platformoptions.xml looks like this:

1
2
3
4
5
6
7
<platform xmlns="http://ns.adobe.com/air/extension/3.1">
    <sdkVersion>5.0</sdkVersion>
    <linkerOptions>
        <!-- to use the MessageUI framework -->
        <option>-framework MessageUI</option>
    </linkerOptions>
</platform>

Another new parameter is extensions.xml file. It holds information about native libraries we want to include. Here we define the id of extension and the names of extension’s initializer and finalizer methods. See that the names of functions in <initializer> and finalizer match the names of functions in MailExtension.m

Here’s extension.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
<extension xmlns="http://ns.adobe.com/air/extension/3.1">
    <id>pl.randori.air.nativeextensions.ios.MailExtension</id>
    <versionNumber>1.0.0</versionNumber>
    <platforms>
        <platform name="iPhone-ARM">
            <applicationDeployment>
                <nativeLibrary>libMailExtension.a</nativeLibrary>
                <initializer>ExtInitializer</initializer>
                <finalizer>ExtFinalizer</finalizer>
            </applicationDeployment>
        </platform>
    </platforms>
</extension>

After running ANT script we will have two new files: NativeMail_iOS.swc in ‘build’ directory and iOS_MailExtension.ane in ‘release’ directory.
iOS_MailExtension.ane is the AIR native extension we wanted to create.

 

Example – using native extension in Adobe AIR application

 

 

 

 

‘Can I send mail?’ button calls MailExtension.isMailComposerAvailable() method which returns true / false.
This method will return false if iOS mail client hasn’t been properly configured. This check is also done before sending the mail to prevent the app from crashing.

‘Send mail’ button calls MailExtension.sendMail method. In the example all available mail fields are filled and two attachments are added.

 

Invoked mail composer view

 

Example app project structure:

Copy ‘iOS_MailExtension.ane’ file to ‘extensions’ directory.

 
One last thing left to is to add the following information you you application descriptor file. extensionID has to match our extension’s id. Otherwise ADT will fail to package the app.

1
2
3
<extensions>
        <extensionID>pl.randori.air.nativeextensions.ios.MailExtension</extensionID>
</extensions>

Packaging application is done by following ANT script:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
<?xml version="1.0" encoding="UTF-8"?>
<project name="NativeMail iOS extension for Adobe AIR example app" default="publish-ios" basedir=".">
   
    <property file="local.properties" />
    <property file="build.properties" />
   
    <target name="clean">
        <delete dir="${app.builddir}"/>
        <delete dir="${app.releasedir}"/>
        <mkdir dir="${app.builddir}"/>
        <mkdir dir="${app.releasedir}"/>
    </target>
   
    <target name="copy" depends="clean">
        <copy todir="${app.builddir}" preservelastmodified="true" verbose="true">
             <fileset dir="${app.sourcedir}">
                <patternset>
                    <include name="assets/**"/>
                    <include name="*.png"/>
                    <include name="*.xml"/>
                    <include name="*.ane"/>
                </patternset>
             </fileset>
        </copy>
   
    </target>
   
    <target name="compile" depends="copy">
        <exec executable="${MXMLC}">
            <arg line="
                +configname=airmobile
                -output ${app.builddir}/${build.swfname}
                ${app.sourcedir}/NativeMail.mxml
                -source-path+=${app.rootdir}/src
                -load-config+=NativeMail_app.config
                -swf-version=14
            "/>

        </exec>
    </target>
   
    <target name="publish-ios" depends="compile">
        <copy file="${app.builddir}/${build.swfname}" tofile="${app.rootdir}/${build.swfname}"/>
        <copy file="${app.sourcedir}/NativeMail-app.xml" tofile="${app.rootdir}/NativeMail-app.xml"/>
       
        <exec executable="${ADT}">
            <arg line="-package
                        -target ipa-test-interpreter
                        -provisioning-profile ${build.mobileprofile}
                        -storetype ${build.storetype}
                        -keystore ${build.keystore}
                        -storepass YOUR_PASSWORD
                        ${app.releasedir}/${build.name}
                        ${app.descriptor}
                        ${build.swfname}
                        -extdir extensions
                        -C ${app.builddir} Default.png Default@2x.png"/>

        </exec>
    </target>
   
</project>

Notice ‘-swf-version=14′ and ‘-extdir extensions’ parameters