Localytics Adobe AIR native extension

I just released a Localytics native extension for AIR. It’s available on github https://github.com/randori/ANE-Localytics . This version supports iOS only, but Android support should be added in a few days.
Localytics is one of the biggest analytics services available for mobile devices, I like it because the collected data is available almost in real-time and for it’s simplicity.

Update 22 June 2012
Localytics ANE now supports iOS and Android AIR 

How to use ANE-Localytics

Sign up for a free / premium Localytics account and create an application key https://dashboard.localytics.com/localytics_applications
Download .ane and .swc from https://github.com/randori/ANE-Localytics/downloads and add the ANE to you project

import pl.randori.ane.Localytics;

Start a Localytics session when the app finishes loading:

//Start Localytics session
Localytics.startSession(‘APP-KEY’);

When the session has been initialized you can log events and screens. Events can have attributes (optional).

//Example 1: Location based sports app – finished run / walk
Localytics.tagEvent(“run_finished”, {‘distance’:’1-2km’, ‘time’:’6-10min’, ‘gps_accuracy’:’10-20 meters’});
Localytics.tagEvent(“run_finished”, {‘distance’:'>10km’, ‘time’:’30-45min’, ‘gps_accuracy’:'>100 meters’});

//Example 2: button was pressed
Localytics.tagEvent(“button_pressed”);
Localytics.tagEvent(“button_pressed”, {‘button_id’:'login’});
Localytics.tagEvent(“button_pressed”, {‘button_id’:'share’, ‘share_type’:'facebook’});

//Example 3, Screen flow: E-commerce app
Localytics.tagScreen(‘home’);
Localytics.tagScreen(‘categories’);

Check out the example projects (Flash CS6, Flash Builder 4.6) as a starting point.

TIP
There are two versions of the .ane file – release and debug. What’s the differece between ANE_Localytics-debug and ANE_Localytics-release? Release builds are optimized builds and don’t print logs to iOS console.

Requirements
ANE Localytics supports iOS 4.0 and newer. 
—- 

Example app events

Localytics application dashboard

‘run_finished’ event – ‘distance’ attribute

 Localytics event attributes sample

Apple Push Notification Service native extension for Adobe AIR

Here’s another AIR native extension for iOS I’m currently working on – Apple Push Notification Service (APNs) support.
It’s almost complete – the most important things are there. You can register to APNs, get your device token and receive remote notification in ActionScript – everything that you need to build a great app.

Grab the source from github: https://github.com/pkoscierzynski/NativeAPNService

To run the example you need to go to Apple Developer Portal and create a new application id, generate push notification certificates and a new mobile provisioning profile.
I’d recomend this tutorial by Matthijs Hollemans, it’s very detailed and self explanatory (like all the tutorials on Ray’s site).
Apple Push Notification Services Tutorial: Part 1/2

I also used php sample to publish my message to APNs sandbox servers. Follow the tutorial step by step and you should get everything working.

When you’ll have your app id and be sure to put it in the apropriate xml tags of AIR application descriptior.
In AIR 3.1, application descriptor has a new section for iOS apps – Entitlements. In Entitlements section you need to provide the id of you application, otherwise the APN won’t work (this section is the content of Entitlements.plist file known from native Xcode / Objective-C).

Adobe AIR application descriptor. iPhone Entitlements section.

Apple Push Notifications in Adobe AIR iOS application from Piotr Koscierzynski on Vimeo.

iOS native extension for Adobe AIR. In-app mail composer


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 Adobe Blogs: iOS5 support for AIR/Using 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: Adobe Docs: Building the ActionScript library of a native extension

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

That’s it.

Worth reading:
Adobe Blogs: iOS5 support for AIR/Using external SDKs to package apps
MFMailComposeViewController Class Reference
Adobe Docs: Building the ActionScript library of a native extension
Native Alert iOS native extension tutorial
AIR Native Extension Example: iBattery for iOS
as3c2dm – AIR native extension to push notifications with C2DM

SupAIR Pong – Adobe AIR 2.6 for iOS in action

SupAIR Pong's icon

SupAIR Pong splash screen

SupAIR Pong splash screen

I can't play pong

When newest version of Adobe AIR came out I needed to give it a try, of course.
It has been a while since I’ve done some coding in AIR, beacause for the last few months I focused on developing native iOS applications (Objective-C, C++, Xcode) and wrote only a small AIR app for BlackBerry PlayBook to check out the tools and workflow (sadly, it’s not developer-friendly).

Adobe AIR 2.6 doesn’t user PFI (Packager for iPhone) anymone, now it’s using the same tool for generating mobile iOS and Android applications. Also, iPhone 2G and 3G are no longer supported, due to “armv7″ and “opengles-2″ requirements for the target runtime (UIRequiredDeviceCapabilities).

The packaging process is faster than with PFI / AIR 2.5 (AIR 2.6: 45-50 seconds, AIR 2.5: 2-4 minutes). My pong clone runs smoothly on iPhone 3Gs 30fps and it’s a highly addictive game. The AIR 2.6 apps support iOS 4 features like multitasking, so you can save and resume your applications’ state. The game has a super-feature: you can change the colors (see the video).

SupAIR Pong will be available  on the App Store soon (if it gets accepted :) .

AIR 2.6 mobile workflow is way better than AIR for BlackBerry PlayBook (BBP), because you can test your app in the simulator and with BBP Tablet it’s not that easy

  • you need VM Ware to run the BBP simulator (30day trial then you need to buy it) / standard adt simulator doesn’t have required APIs (QNX) will crash with most apps
  • the BBP simulator doesn’t support orientation change / you need to use your imagination to test portrait mode
  • the BBP simulator doesn’t support cameras /  you need to use your imagination and then pray for the code to work
  • you need to use command line quite often – it’s not possible to remove an application from the simulator in any other way
  • QNX components are really ugly – check out the green preloader icon. Skinning them is a slow and tedious task. QNX components use Tweener internally (strange choice, since TweenMax/TweenLite is a lot faster)

Few things to remember:

  1. You cannot use <img> tag inside TextField’s htmlText property due to Adobe AIR’s security policy
  2. Use cacheAsBitmap on static textfields (boosts performance)
  3. doubleClickEnabled property doesn’t work, user flash.utils.getTimer instead (the same thing happens on BlackBerry PlayBook)
  4. When embedding fonts set embedAsCFF=”false” (when using font with AS3 TextFields)
  5. stage.stageWidth and stage.stageHeight properties contain wrong screen dimensions. On my iPhone 3Gs (480×320 fullscreen) and in the simulator the size was 500×375. (a bug ? ). Wasted some time wondering why graphics aren’t positioned correctly.
  6. Clicks on html links inside TextFields are working really bad on the device (3Gs), it was very hard to get a TextEvent.LINK event to fire. In the simulator everything’s fine. Had to change this approach to oridinary Sprites to act as buttons.

Preview versions of mobile Flex "Hero" and Flash Builder "Burrito" released

Today Adobe released preview version of Flex ‘Hero’ optimized for mobile devices and a preview version of next version of Flash Builder – codenamed ‘Burrito’.

‘Burrito’ comes with mobile Flex SDK and Air 2.5 SDK – the IDE has many interesting features that will make development for multiple platforms and different screen sizes much easier.
It’s the first release of Air 2.5, so anyone who didn’t apply to Air Prelease can now start creating mobile applications for Android.

Spark family of components have been optimized for mobile devices (i.e. – all of the skins have been coded in ActionScript, the components can now respond to touch screens events ).
The list of changes and new features is pretty impressive, so check them out Adobe Flash Builder “Burrito”

How to create iOS applications with Adobe AIR

On 9th September Apple revoked the restrictions for technologies that are allowed to be used for creating iPhoneOs (iOS) applications. This means that you can use Flash, Unity3D, Titanium Developer or any other tools to make an mobile application for Apple devices.
As a result of this decision Adobe will resume work on Packager for iPhone which has been on hold since April this year (as a result of sections 3 of iOS Developer Program license).

Latest version of the packager has been released on 11th October, so let’s give it a try.
This approach for developing iPhone / iPad / iPod applications is a great way to reuse you ActionScript code but there are some Flash features that are not available or are known not to work properly – see ‘Known Issues’ section and ‘Packager for iPhone developer guide’ on Adobe Labs to be aware of those limitations.

There are two ways of creating native iOS applications with Adobe Flash platform:

  • using Adobe Flash CS5
  • using Packager for iPhone from the command line

Create iPhone / iPod / iPad application – step by step

1. Download ‘Packager for iPhone’ (PFI). Flex SDK is also required.

2. PFI requires 32-bit Java runtime environment, be sure to have it installed.

3. Create and Adobe Air project using ActionScript, not MXML.

In FlashDevelop create  ‘AIR AS3 Projector’ project.
In Flash Builder create ‘Flex Project’, but set the main application extension to ‘.as’.

4. Modify the Adobe Air application descriptor xml.

It should look something 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
<?xml version="1.0" encoding="utf-8" ?>
<application xmlns="http://ns.adobe.com/air/application/2.0">
 
	<id>com.flashsimulations.mobile.ios.Hello</id>
	<version>1.0</version>
	<filename>HelloIOS</filename>
	<name>Hello iOS</name>
	<description>Just a test</description>
	<copyright>Piotr Koscierzynski</copyright>
 
	<initialWindow>
		<title>Hello iOS</title>
		<content>HelloIOS.swf</content>
		<systemChrome>standard</systemChrome>
<transparent>false</transparent>
		<visible>true</visible>
		<minimizable>true</minimizable>
		<autoOrients>true</autoOrients>
		<resizable>true</resizable>
		<renderMode>gpu</renderMode>
		<fullScreen>true</fullScreen>
		<aspectRatio>portrait</aspectRatio>
	</initialWindow>
 
	<supportedProfiles>mobileDevice desktop</supportedProfiles>
 
	<iPhone>
		<infoAdditions>
		<![CDATA[
			<key>UIStatusBarStyle</key>
			<string>UIStatusBarStyleBlackOpaque</string>
			<key>UIRequiresPersistentWiFi</key>
			<string>NO</string>
			<key>UIDeviceFamily</key>
			<array>
			<string>1</string>
			<string>2</string>
			</array>
		]]>
		</infoAdditions>
	</iPhone>
 
</application>

The descriptor is almost the same as the one for Android apps, but there are some differences

  • iOS apps must use Air 2.0 (line 1), Android uses Air 2.5
  • iOS apps use <version> not <versionNumber> for specifying version of application. But value should be in the same format – numbers only XX.XX.XX or XX.XX
  • the information for operating system is defined between <iPhone> tag. This may look new, but those parameters are the same that can be found in Info.plist file when developing iOS apps in XCode. For full list of available options visit Information Property List Key Reference for UIKit e.g. UIDeviceFamily defines on which devices the application can run – 1 is for iPhone / iPod, 2 is for iPad.

5. Register in iPhone Developer Program. Get a certificate and provisioning profile from Apple ($99-$300 per year). You also need to convert the
certificate into a P12 certificate. See the “Obtaining developer files from Apple” section, page 4 of ‘Packager for iPhone developer guide’. This process could take few days or even a week (Apple needs to verify information you provided) and can be complicated.

There is a second – ‘non-Apple’ – way. Well, you don’t really need to buy anything, but you won’t be able to submit your application to AppStore and you won’t be able to test on the ‘non-jailbroken’ iOS device. All you have to do is to find a ‘special’ certificate ‘.p12′ and ‘.mobileprovision’ files – Google will help you. Use those files to pack your application.

6. Configure and run the packaging batch ‘Package_iOS_Application.bat’ (if on Windows) or run the packager from the command line using this syntax:
pfi -package -target [ipa-test ipa-debug ipa-app-store ipa-ad-hoc] -provisioning-profile PROFILE_PATH SIGNING_OPTIONS TARGET_IPA_FILE APP_DESCRIPTOR SOURCE_FILES

7. Install you application on the device / submit to AppStore.

Adobe Air into native iOS – How does it work

The packager creates an native iOS application – it’s not Adobe Air application, because Apple forbids Just-In-Time compilation on their devices – that means no ActionScript Virtual Machine available – you won’t be able to run ActionScript code from external .swf files. Packager uses the LLVM compiler to translate ActionScript into Objective-C bytecode, it’s the same technology that’s behind Adobe Alchemy.

Example project

This project is exactly the same as my ‘Hello World’ example for Android. The project’s file structure looks like this

Hello iOS project structure

Packaging batch file

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
@echo off
 
:: iPhone application packaging script - based on FlashDevelop's AIR AS3 template.
:: Modified by Piotr Koscierzynski, flashsimulations.com
 
:: Flex SDK path
set PATH=%PATH%;"C:\Program Files (x86)\FlashDevelop\Tools\flexsdk\bin"
 
:: Directory containing 'Packager for iPhone', version released on 11th October 2010
set PFI_DIR="G:\piotr\SDK\packagerforiphone_v2_win_101110"
 
:: Packager for iPhone requires 32-bit Java runtime. The run parameters taken from PFI .bat file.
set JAVA_32BIT_RUNTIME_PATH="C:\Program Files (x86)\Java\jre6\bin\java" -Xms256m -Xmx1024m
 
:: You should use a valid certificate from Apple Developer Program.
:: You can also generate it by yourself - (see 'CreateCertificate.bat').
:: Certificate file path
set CERTIFICATE=pkoscierzynski.p12
 
:: Provisioning Profile from Apple Developer Program, required.
set MOBILE_PROVISION_FILE=pkoscierzynski.mobileprovision
 
:: Certificate and provisioning profile part
set SIGNING_OPTIONS=-provisioning-profile %MOBILE_PROVISION_FILE% -storetype pkcs12 -keystore %CERTIFICATE%
 
if not exist %CERTIFICATE% goto certificate
 
:: ---- Output ------------
if not exist ipa md ipa
 
:: iPhone IPA file
set IPA_FILE="ipa/HelloIOS.ipa"
:: IPA target type
set IPA_TARGET_TYPE=ipa-test
 
:: --- Input -----------
:: Application descriptor
set APP_XML="Hello-iOS-app.xml"
 
:: Application main swf
set FILE_OR_DIR=-C bin HelloIOS.swf
 
echo Packaging iPhone application using certificate %CERTIFICATE%
echo This process will take a few minutes. Please wait...
 
:: Package command
call %JAVA_32BIT_RUNTIME_PATH% -jar %PFI_DIR%\lib\pfi.jar -package -target %IPA_TARGET_TYPE% %SIGNING_OPTIONS% %IPA_FILE% %APP_XML% %FILE_OR_DIR%
 
if errorlevel 1 goto failed
 
echo.
echo SUCCESS! iPhone IPA installer created: %IPA_FILE%
echo.
goto end
 
:certificate
echo Certificate not found: %CERTIFICATE%
echo.
echo Troubleshotting:
echo A certificate is required, generate one using 'CreateCertificate.bat'
echo.
goto end
 
:failed
echo iPhone packaging FAILED.
echo.
echo Troubleshotting:
echo Please check Flex SDK and PFI path in this Batch file.
echo.
 
:end
pause

Creating first app for Android using Adobe Air

To illustrate the workflow of developing Android apps in Adobe Air I created a very simple application that can react to screen orientation changes, gestures and touches – it’s written in ActionScript.

The user interface consists of checkbox, label and a button.
If the user clicks the button or make a swipe gesture the background color will change to a random color.
Checkbox enables the screen orientation – this is an interesting feature since we can decide whether or not to let the screen rotate.

I used a set of Android-like components created by Kevin Hoyt, Adobe’s Platform Evengelist. They are very easy to use and mimic the Android look & feel pretty good.

Hello Android Air screen

The process is made of following steps

  • create Flex Project
  • generate the certificate
  • package the Air application into an apk file
  • install the apk to device / emulator

I assume that you have all the necessary dependencies already installed (Flex SDK with AIR 2.5 , Android SDK and an emulator /device), if not then check my previous post.

Creating Flex Builder project

1. Create a new Flex Project. Be sure to use Flex SDK merged with AIR 2.5 SDK.

Adobe Air app wizard - Flash Builder step 1

In the second step change the main application file extention from ‘.mxml’ to “.as”, since we won’t be using Flex framework, only ActionScript 3.

Using Flex for mobile applications is possible but the performance would suffer. Until mobile version of Flex 4.5 isn’t released (should be available later this year) it’s better to stick to ActionScript only.

Adobe Air app wizard - Flash Builder step 2

Modify application’s descriptor xml

Flash Builder has generated all the necessary files, there are only few thing to modify in the xml descriptor  - set the <visible> tag to “true” and specify Android manifest – if your application is going to use Android APIs (camera, wifi, file access, etc.). If you don’t specify the functionality you need, the application won’t work properly.

The application main class 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
package
{
	import com.greensock.TweenMax;
	import com.greensock.easing.Cubic;
 
	import flash.display.Sprite;
	import flash.display.StageAlign;
	import flash.display.StageScaleMode;
 
	import flash.events.Event;
	import flash.events.MouseEvent;
	import flash.events.StageOrientationEvent;
	import flash.events.TransformGestureEvent;
 
	import flash.geom.Point;
 
	import flash.net.URLRequest;
	import flash.net.navigateToURL;
 
 
 
	/**
	 * Adobe AIR Android application example
	 *
	 * @author Piotr Koscierzynski, flashsimulations.com
	 *
	 * */
	[SWF(backgroundColor='0xf8f8f8',frameRate='31')]
	public class HelloAndroid extends Sprite
	{
		/* Android style components by Kevin Hoyt */
		private var superButton:Button;
		private var autoOrientCheckbox:CheckBox;
		private var footerLabel:Label;
 
		//
		private var uiCreated:Boolean = false;
 
		private var labels:Array = ['Click me!', "Welcome to Android world!", 'Life is like a box of chocolates.', 'Have a nice day.'];
		private var counter:uint = 0;
 
		private static const TWEEN_TIME:Number = 0.6;
		private var tweenFunction:Function = com.greensock.easing.Cubic.easeInOut;
 
		private var backgroundSprite:Sprite;
 
 
		public function HelloAndroid()
		{
			addEventListener(Event.ADDED_TO_STAGE, onAdded, false, 0, true);
		}
 
 
		private final function onAdded(e:Event):void {
 
			removeEventListener(Event.ADDED_TO_STAGE, onAdded);
 
			init();
		}
 
		/**
		 * Stage resize handler.
		 * */
		private final function onResize(e:Event = null):void {
 
			//no stage instance available
			if (!stage) return;
 
			//ui not instantiated
			if (!uiCreated) return;
 
			var stageWidth:uint = stage.stageWidth;
			var stageHeight:uint = stage.stageHeight;
 
			//background
			backgroundSprite.width = stageWidth;
			backgroundSprite.height = stageHeight;
			backgroundSprite.x = 0;
			backgroundSprite.y = 0;
 
 
 
			//checkbox position
			var autoOrientCheckbox_x:int = autoOrientCheckbox.width - 30;
			var autoOrientCheckbox_y:int =  10;
 
			TweenMax.killTweensOf(autoOrientCheckbox);
			TweenMax.to(autoOrientCheckbox, TWEEN_TIME, {x:autoOrientCheckbox_x, y: autoOrientCheckbox_y, ease:tweenFunction});
 
			//center the button
			superButton.setWidth(stageWidth - 20);
			var superButton_x:int = (stageWidth - superButton.width) >> 1;
			var superButton_y:int = (stageHeight - superButton.height) >> 1;
 
			TweenMax.killTweensOf(superButton);
			TweenMax.to(superButton, TWEEN_TIME, {x:superButton_x, y: superButton_y, ease:tweenFunction});
 
			var footerLabel_x:int = (stageWidth - footerLabel.width) >> 1;
			var footerLabel_y:int = (stageHeight - footerLabel.height - 10);
 
			TweenMax.killTweensOf(footerLabel);
			TweenMax.to(footerLabel, TWEEN_TIME, {x:footerLabel_x, y: footerLabel_y, ease:tweenFunction});
 
		}
 
		/**
		 * If the <autoOrients>true</autoOrients> tag is set to true then the application will receive this event.
		 * We can block the screen rotation by calling e.preventDefault().
		 *
		 * */
		private final function onOrientationChanging(e:StageOrientationEvent):void {
 
			//prevent the screen from rotating
			if (autoOrientCheckbox.selected == false)
			{
				e.preventDefault();
			}
		}
 
		/**
		 * Init stage align and listeners.
		 *
		 * */
		private final function init():void {
 
			buildUI();
 
			if (stage) {
 
				stage.align = StageAlign.TOP_LEFT;
				stage.scaleMode = StageScaleMode.NO_SCALE;
				stage.addEventListener(Event.RESIZE, onResize, false, 0, true);
				stage.addEventListener(StageOrientationEvent.ORIENTATION_CHANGING, onOrientationChanging, false, 0, true);
				stage.addEventListener(TransformGestureEvent.GESTURE_SWIPE, onSwipe, false, 0, true);
 
			}
 
 
			//set UI elements positions
			onResize();
 
		}
 
 
		/**
		 * Handle screen swipe - change the background of the screen.
		 *
		 * */
		private final function onSwipe(e:TransformGestureEvent):void {
 
 
				changeBgColor();
 
		}
 
		/**
		 * Build the user interface
		 * */
		private final function buildUI():void {
 
			//background
			backgroundSprite = new Sprite();
			backgroundSprite.graphics.beginFill(0xFFFFFF, 1);
			backgroundSprite.graphics.drawRect(0,0, 1, 1);
			backgroundSprite.graphics.endFill();
			backgroundSprite.mouseChildren = false;
			backgroundSprite.mouseEnabled = false;
			addChildAt(backgroundSprite, 0);
 
			//button
			superButton = new Button(labels[0], 300);
			superButton.addEventListener(MouseEvent.CLICK, handleButtonClick, false, 0, true);
			addChild(superButton);
 
 
			//checkbox - auto orientation enabled
			autoOrientCheckbox = new CheckBox('Auto orient screen');
			autoOrientCheckbox.selected = false;
			addChild(autoOrientCheckbox);
 
			//label for footer
			footerLabel = new Label('flashsimulations.com');
			footerLabel.addEventListener(MouseEvent.CLICK, handleFooterClick, false, 0, true);
			footerLabel.buttonMode = true;
			footerLabel.mouseChildren = false;
			addChild(footerLabel);
 
			uiCreated = true;
		}
 
		private function changeBgColor():void {
 
 
			TweenMax.to(backgroundSprite, 0.4, {tint:Math.random()*0xFFFFFF});
		}
 
		private final function handleButtonClick(e:Event):void {
			superButton.label = labels[(++counter)%labels.length];
			changeBgColor();
 
		}
 
		private final function handleFooterClick(e:Event):void {
			navigateToURL(new URLRequest('http://flashsimulations.com'));
		}
 
 
	}
}
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
<?xml version="1.0" encoding="utf-8" standalone="no"?>
<application xmlns="http://ns.adobe.com/air/application/2.5">
	<id>com.flashsimulations.air.HelloAndroid</id>
	<filename>HelloAndroid</filename>
	<name>Hello AndroAir</name>
	<versionNumber>1.0.0</versionNumber>
	<description>This application demonstrates the new Flash API for mobile devices (gestures, screen orientation). Created using Adobe AIR 2.5.</description>
	<copyright>Piotr Koscierzynski, flashsimulations.com</copyright>
	<supportedProfiles>mobileDevice</supportedProfiles>
 
	<!-- Settings for the application's initial window. Required. -->
	<initialWindow>
		<content>[This value will be overwritten by Flash Builder in the output app.xml]</content>
		<visible>true</visible>
		<autoOrients>true</autoOrients>
	</initialWindow>
 
	<android>
			<manifestAdditions>
			<![CDATA[ <manifest>
								<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
								<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
								<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
								<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
								<uses-permission android:name="android.permission.CAMERA" />
								<uses-permission android:name="android.permission.DISABLE_KEYGUARD" />
								<uses-permission android:name="android.permission.INTERNET" />
								<uses-permission android:name="android.permission.READ_PHONE_STATE" />
								<uses-permission android:name="android.permission.RECORD_AUDIO" />
								<uses-permission android:name="android.permission.WAKE_LOCK" />
								<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
			</manifest>
			]]>
			</manifestAdditions>
	</android>
</application>

2. Create an apk file and deploy it to the device / emulator.

Packaging Adobe Air for Android

Copy ‘HelloAndroid.swf’ and ‘HelloAndroid-app.xml’ into FLEX_SDK\bin directory, then run these commands

cd /Users/piotr/SDK/flex_sdk_4.1_air_2.5/bin
./adt -certificate -cn flashsimulations -ou piotrkoscierzynski -c PL -validityPeriod 25 2048-RSA android_cert.p12 SecretPassword
./adt -package -target apk-emulator -storetype pkcs12 -keystore android_cert.p12  -storepass SecretPassword HelloAndroid-emu.apk HelloAndroid-app.xml HelloAndroid.swf
cp HelloAndroid-emu.apk /Users/piotr/SDK/Android_SDK/tools
cd /Users/piotr/SDK/Android_SDK/tools
./adb devices
./adb -e install -r HelloAndroid-emu.apk

The application looks like this

Sources: Hello Android Air Flash Builder project

Developing Android applications with Adobe AIR – setting up the environment

Since July 2010 I’ve been playing with Adobe AIR for Android. I got the prerelease version of the runtime and started investigating the new APIs for mobile devices. Adobe AIR hit the Android Market on 9th October 2010, so it’s time for the AIR developers to get to work. There are few things that if found difficult at the begining, especially the configuration and building the Android APK archive.

Setup your development environment

Here’s how I setup my Adobe AIR for Android environment (both MacOS X and Windows)

Setup – Adobe AIR / Flex part

FLEX_DIR (MacOs) = /Users/piotr/SDK/flex_sdk_4.1_air_2.5
FLEX_DIR (Windows) = C:\SDK\flex_sdk_4.1_air_2.5

1. Download Flex SDK (version 3.5 and newer – I used 4.1) and put it in directory FLEX_DIR

2. Get the Adobe AIR 2.5 SDK – it’s a zip file like Flex SDK 3. Extract AIR 2.5 SDK and copy all the files to FLEX_DIR – overwrite all the files in FLEX_DIR that might overlap 4. In FLEX_DIR you now have the Flex SDK combined with AIR 2.5

Setup – Android part

ANDROID_DIR (MacOs) = /Users/piotr/SDK/android
ANDROID_DIR (Windows) = C:\SDK\android

3. Download Android SDK and put it in ANDROID_DIR

4. Get Android 2.2 libraries and emulator using “Android SDK and AVD manager”
- goto ANDROID_DIR\tools
- run android (Windows) or ./android (MacOs X)

5. Create an virtual device (for emulator) with Android 2.2 system (if you don’t have an Android 2.2 device)

You could add the FLEX_SDK\bin and ANDROID_SDK\tools directories to your system’s PATH variables to make the development process faster, but I’m not going to cover this topic here.
I will keep it simple to illustrate the whole process.

6. To be able to test you application you need to install Adobe AIR runtime for Android.
The AIR prerelease comes with two versions of the runtime – one for the device and one for the emulator.

The runtime for the device is Runtime_Device_Froyo_20100930.apk

The runtime for the emulator is Runtime_Emulator_Froyo_20100930.apk

Creating Android 2.2 virtual device for emulator

I created an Android 2.2 emulator (virtual device) with name “Android_22″ and the install process looks like this:

I’ll use the ANDROID_EMULATOR_NAME for “Android_22″ – my virtual device name. If you created a virtual device with a different name, then it would be the device’s name.

First, you need to make sure that the emulator is up and running.
Goto ANDROID_SDK\tools by typing: cd ANDROID_SDK\tools
Run the emulator:

(MacOS) ./emulator -avd ANDROID_EMULATOR_NAME or ./emulator @ANDROID_EMULATOR_NAME

(Windows) emulator -avd ANDROID_EMULATOR_NAME or emulator @ANDROID_EMULATOR_NAME

Android - starting emulator with virtual device

Install Adobe AIR for Android runtime

(Important) Copy both Runtime_Device_Froyo_20100930.apk and Runtime_Device_Froyo_20100930.apk into ANDROID_SDK\tools.

To install the AIR runtime on the emulator:
(MacOS X): ./adb -e install -r Runtime_Emulator_Froyo_20100930.apk
(Windows) adb -e install -r Runtime_Emulator_Froyo_20100930.apk

To install the AIR runtime on the device:

(MacOS X) ./adb  install -r Runtime_Device_Froyo_20100930.apk
(Windows): adb install -r Runtime_Device_Froyo_20100930.apk

Installing Adobe AIR on Android

Potential problems

If you get the “device not found” message you might need to restart the adb service.
I had that problem but it worked after running these commands:

adb kill-server
adb start-server

After the adb has restarted give the runtime install another try, it should work.

You rock

If you see “Adobe AIR” under Settings -> Applications -> Manage Applications than congratulations – you are ready to develop AIR apps for Android.

Adobe AIR runtime on Android apps list

Add Flex + AIR 2.5 SDK to your IDE
One last thing is to add the FLEX_DIR into your IDE (Flash Builder or other like FlashDevelop)

Flash_Builder_SDKs_Android_AIR

In the next part of this series I’ll show how to create and deploy and Adobe AIR project and Android.

Other posts that you might find useful

http://blog.digitalbackcountry.com/2010/10/publishing-air-apps-to-the-android-market/ http://www.adobe.com/newsletters/edge/august2010/articles/article1/ http://unitedmindset.com/jonbcampos/2010/07/20/creating-a-flash-builder-android-project/

Handling image loading in TLF TextFlow and TextField

Recently, I was building a cms driven (Drupal 6 to be precise) website and faced the problem of detecting whether the content of TextField or TFL TextFlow has completed loading.
This example is based on TLF Editor from Tour de Flex, which I used as a base for the created editor/display component.

1. TLF TextFlow solution

The solution I used is actually very simple.

First, you need to analyze the TextFlow markup to get the number of images in text.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//total images counter
private var totalImages:uint = 0;
//number of loaded images
private var imagesLoaded:uint = 0;

private function initTextFlowImagesCounter(textFlowMarkup:String):void {
//<img  source="http://image_url.jpg" />
            var source_AttributeRegExp:RegExp = /source="[^"]+"/gi;
            var result:Object = source_AttributeRegExp.exec(textFlowMarkup);

            while (result != null) {
                var image_source:String = String(result);

                totalImages++;
                result = source_AttributeRegExp.exec(textFlowMarkup);
            }

}

Then, create a TextFlow instance and setup the necessary listeners.

1
2
3
initTextFlowImagesCounter(markup);
var activeFlow:TextFlow = TextConverter.importToFlow(markup, TextConverter.TEXT_LAYOUT_FORMAT);
activeFlow.addEventListener(StatusChangeEvent.INLINE_GRAPHIC_STATUS_CHANGE,recomposeOnLoadComplete,false,0,true);

We handle all StatusChangeEvent.INLINE_GRAPHIC_STATUS_CHANGE events in recomposeOnLoadComplete function.
Basicaly, this function updates the ‘imagesLoaded’ counter, but there are few things worth mentioning.
The whole image loading process in TLF is quite different than flash TextField approach, we need to manually handle the states of image elements.
When the status is InlineGraphicElementStatus.SIZE_PENDING then the image has finished loaded, but the TLF didn’t actually render it, because the controller didn’t know the dimensions of the image – so we need to call activeFlow.flowComposer.updateAllControllers(). This situation will happen only when an tag doesn’t have declared ‘width’ and ‘height’ explicitly to a number.

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
private function recomposeOnLoadComplete(e:StatusChangeEvent):void
        {
        //image load error
            if (e.status == InlineGraphicElementStatus.ERROR)
            {
                imagesLoaded--;
        }

            if (e.element.getTextFlow() == activeFlow) {
                //image has loaded - we need to recalculate the size of the TLF container
                if (e.status == InlineGraphicElementStatus.SIZE_PENDING)
                {
                    activeFlow.flowComposer.updateAllControllers();
                }
                else
                //image has been loaded and updated
                if (e.status == InlineGraphicElementStatus.READY)
                {
                    imagesLoaded++;
                }

                checkIfLoaded();
            }

        }

private function checkIfLoaded():void {

//all images loaded
            if (totalImages == imagesLoaded)
            {
                updateSizeInfo();//just an option
                                dispatchEvent(new Event(Event.COMPLETE));
            }

}

The content displayed on the website has a lot of images, so need to know the height of the TextFlow object to be able to scroll it.
This is done by this handy function.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
private function updateSizeInfo():int {

if (activeFlow) {
                var cont:ContainerController = activeFlow.flowComposer.getControllerAt(0);
                if (cont.container)
                {
//height of the TextFlow instance
                    var textHeight:int = Math.ceil(cont.getContentBounds().height);
                    return textHeight;
                }
            }

            return 0;
}

2. TextField solution

The whole idea is the same, but we need to setup a Loader instance for each image to handle errors and load events. We can get the height of TextField content from the textHeight attribute.

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
private function initTextFieldImagesLoading(htmlText:String):void {
//search for the 'id' attribute of 'img' tag
    var idAttributeRegExp:RegExp = /id="[^"]+"/gi;
    var result:Object = idAttributeRegExp.exec(htmlText);

    while (result != null) {
        var imageId:String = String(result);
        imageId = imageId.replace('id="', '');
        imageId = imageId.replace('"', '');
        //create an Loader instance for each image - txtDescription is our TextField with htmlText
        var ldr:Loader = txtDescription.getImageReference(imageId) as Loader;

        if (ldr != null) {
            totalImages++;
            ldr.contentLoaderInfo.addEventListener(Event.INIT, onTextFieldImageLoaded);
            ldr.contentLoaderInfo.addEventListener(IOErrorEvent.IO_ERROR, onTextFieldIOError);
        }

        result = idAttributeRegExp.exec(htmlText);
    }

private function onTextFieldImageLoaded(e:Event):void
        {
            e.target.removeEventListener(Event.INIT, onTextFieldImageLoaded);
            e.target.removeEventListener(IOErrorEvent.IO_ERROR, onTextFieldIOError);
            imagesLoaded++;

            checkIfLoaded();
        }

        private function onTextFieldIOError(e:IOErrorEvent):void
        {
            e.target.removeEventListener(Event.INIT, onTextFieldImageLoaded);
            e.target.removeEventListener(IOErrorEvent.IO_ERROR, onTextFieldIOError);

            totalImages--;
            checkIfLoaded();
        }
}

3d physics in Flash with Jiglibflash and Away3DLite

Newest experiment using Jiglibflash and Away3DLite, requires Flash Player 10. The speed boost in comparison to PV3D and FP9 version of Jiglib is unbelieveable (over 3500 triangles and realtime collision detection). It’s a work in progress, but you can take a look here: Letters and car 3d physics demo.

Using jiglib car physics in Away3DLite is pretty hard, I don’t know why it differs so much from the PV3D version. If the car starts to behave ‘crazy’ or gets stuck, press the ‘r’ key to reset it. You can also throw a snowball by pressing ‘b’ key.
Use arrows to steer the car and space bar for handbrake.