Posts in Category "AIR"

How to Use CameraUI in a Cross-platform Way

If you use, or plan to use, the CameraUI API, this post will explain how to do it in a cross-platform way so your code will work on both iOS and Android.

The AIR mobile profile supports an API called CameraUI which can be used to launch a device’s native camera application for taking both still photos and video. AIR applications can then access the photo or video and do whatever they want with it. I prefer this method to the Camera ActionScript API because it gives the user the native camera experience he or she is accustomed to along with all the camera features and functionality available on the device.

The problem with the API is that there’s an easy way to use it, and a slighter harder way, and the easy way is not cross-platform. I discovered this when writing AIRBench — an AIR application that demonstrates and tests all the mobile profile specific APIs in AIR. I wrote AIRBench before the CameraUI APIs were available on iOS, so I was only able to test them on Android. Naturally, being a programmer who prefers simplicity to complexity, I used the easy CameraUI APIs only to discover, when running AIRBench on iOS, that the CameraUI test didn’t work.

The easy (and non cross-platform) way to use the CameraUI API is to do something like this:

private function onLaunchCameraUI(e:Event):void
{
    var cameraUI:CameraUI = new CameraUI();
    cameraUI.addEventListener(MediaEvent.COMPLETE, onCameraUIComplete);
    cameraUI.launch(MediaType.IMAGE);
}

private function onCameraUIComplete(e:MediaEvent):void
{
    var picture:File = e.data.file;
}

The data property of the MediaEvent is a MediaPromise object which has a file property which, in some cases, points to the photograph that was just taken. In some cases, but not all. On Android, when the CameraUI is used to take a picture, the photo is stored in the camera roll so the file property of the MediaPromise can reference it. On iOS, however, the image isn’t stored in the camera roll (unless you choose to do so yourself) which means that the file reference is null.

The better (more cross-platform) way to write this code is to load the image asynchronously from the MediaPromise itself. Here’s the refactored code from AIRBench:

private function onLaunchCameraUI(e:Event):void
{
    var cameraUI:CameraUI = new CameraUI();
    cameraUI.addEventListener(MediaEvent.COMPLETE, onCameraUIComplete);
    cameraUI.addEventListener(Event.CANCEL, onCameraUICanceled);
    cameraUI.addEventListener(ErrorEvent.ERROR, onCameraError);
    cameraUI.launch(MediaType.IMAGE);
}

private function onCameraUIComplete(e:MediaEvent):void
{
    var cameraUI:CameraUI = e.target as CameraUI;
    cameraUI.removeEventListener(MediaEvent.COMPLETE, onCameraUIComplete);
    cameraUI.removeEventListener(Event.CANCEL, onCameraUICanceled);
    cameraUI.removeEventListener(ErrorEvent.ERROR, onCameraError);

    var mediaPromise:MediaPromise = e.data;
  
    this.mpLoader = new Loader();
    this.mpLoader.contentLoaderInfo.addEventListener(Event.COMPLETE, onMediaPromiseLoaded);
    this.mpLoader.addEventListener(IOErrorEvent.IO_ERROR, onMediaPromiseLoadError);
    this.mpLoader.loadFilePromise(mediaPromise);
}
  
private function onMediaPromiseLoaded(e:Event):void
{
    var mpLoaderInfo:LoaderInfo = e.target as LoaderInfo;
    mpLoaderInfo.removeEventListener(Event.COMPLETE, onMediaPromiseLoaded);
    mpLoaderInfo.loader.removeEventListener(IOErrorEvent.IO_ERROR, onMediaPromiseLoadError);
    this.imageDisplay.source = mpLoaderInfo.loader;
}

It’s a few more lines of code, but it works perfectly on Android and on iOS, and it will also work on any mobile profile platforms that AIR supports in the future.

If you’re using the CameraUI API now on Android, I strongly encourage you to use the technique described above to ensure that your code is as portable and cross-platform as possible.

How to Discover Your AIR Runtime Version

This morning, someone asked me how to figure out what version of the SDK and/or AIR runtime you’re using. Rather than just respond to the email, I figured I would blog it for everyone to see.

Here are two easy ways to see the AIR runtime version you’re using:

  1. Check the "AIR SDK Readme.txt" file in your SDK directory (the version is at the top).
  2. Trace out NativeApplication.nativeApplication.runtimeVersion.

Remember that you don’t want to check the runtime version to see if certain capabilities, features, or APIs exist — rather, you want to use an individual feature’s’ "isSupported" flag. But for debugging purposes, knowing your runtime version programmatically can sometimes be useful.

Zombie Game Preview

Here’s a quick preview of where I am with a zombie game I’m working on. I should have mentioned in the video that I’m using a technique called blitting for rendering which results in extremely good performance, even without hardware acceleration. I may go back to using the display list, but I wanted some blitting experience, so I figured I would go this route first.

I’m not sure where this game is going, but I’m having a blast writing it.

The Importance of Scaling Images on Mobile Devices

When we launched Adobe AIR for Android, I created an application called AIRBench to help us measure the performance of AIR on all the different Android devices out there. Not only did AIRBench tell us which phones run AIR the best, but it also brought to my attention the importance of scaling images before displaying them on mobile devices.

One test we saw consistently fail on Droid devices (only the first Droid — none of the others) was the CameraUI test. The CameraUI test simply lets users take a picture, then places the image on the display list. It’s very simple and straightforward, however we saw numerous failures. After looking into it, we discovered that the issue isn’t actually a bug in AIR, but rather it’s the result of the application running out of memory.

The test uses the Spark Image component to display the image. When the code hands the Image component the file URL of picture that was just taken, two important things happen:

  1. The image is decoded into a bitmap which means it uses much more memory than the compressed JPEG version.
  2. The image is visually scaled down to fit on the screen. I say "visually scaled" because it only looks to be smaller, but the entire uncompressed bitmap is still in memory.

Because the Droid has a pretty high resolution camera (5 megapixels), but a relatively small amount of RAM (256 MB as opposed to the 512 MB of the Droid 2), it’s not hard to get an application to run out of memory by displaying uncompressed bitmaps if there are other applications running at the same time.

Although I’ve never seen this happen on any other device (since most other Android phones that AIR supports have more than 256MB of RAM), it’s still a good idea to really scale your images before displaying them. By "really scale", I mean the following:

  1. Figure out the width and height that gets the image small enough to fit on the screen, but also maintains its aspect ratio.
  2. Draw the data from the Loader to a new BitmapData object and using a Matrix object to actually scale the image down (meaning most of the data is removed).

I did some tests and found that by properly scaling an image before putting it on the display list (in this case, handing it to the Spark Image component), you can easily display images from either the CameraUI or the CameraRoll on the Droid with no issues whatsoever. A little bit of profiling revealed that scaling images properly uses only a few hundred kilobytes as opposed to between 20 and 30 megabytes (depending on the image size).

The sample code below illustrates how to do the following:

  1. Use either the CameraUI (if it’s supported), or a native file browser (for desktop testing) to let the user take a picture or choose an image.
  2. Read the bytes of the image into a ByteArray.
  3. Load the bytes into a Loader.
  4. Figure out the correct scale factor which will allow the image to fit on the screen, but still maintain its aspect ratio.
  5. Create a new BitmapData object with the correct dimensions.
  6. Draw the image into the BitmapData object and scale it down to the correct size.

Ok, enough talk. Here’s the code:

Continue reading…

Playing Games Against Users With Different Devices

One of the things I really love about mobile devices is that they have become very capable gaming platforms — so capable, in fact, that I think they seriously threaten dedicated hand-held gaming platforms like the PSP and Nintendo DS. And since our devices are easily connected, playing games against friends is usually just a matter of jumping on the same local network.

But the problem is fragmentation. Some of my friends have iPhones and some have Android devices which can make it difficult or impossible to play games against each other. But not so with games written with AIR. Last week, I decided to see how hard it would be to add network play to iReverse in order to enable two people to play against either other over a local network no matter what kind of device the two players have (iOS, Android, laptop, etc.). As it turns out, it was less than a day of work. Here’s a demo:


The key to game instances discovering each other on the network and passing data back and forth is RTMFP. The code is extremely simple to write, and no server of any kind is required. I haven’t updated the release versions of iReverse yet (I’ve been on the road a lot lately), but I should have them out before Christmas. In the meantime, you can see how it all works by checking out the code.

The 50×50 Icon Size for the iPad

If you’ve built iOS applications using AIR for iOS (also known as the Packager for iPhone in Flash Professional), you may have come across an inconsistency in icon sizes. According to the iOS Human Interface Guidelines, Apple says you need a 50×50 icon for iPad applications (for use in Spotlight search results), but AIR doesn’t support 50×50 icons. Instead, we support a 48×48 icon both on the desktop, and for the iPad. There are two reasons we stuck with the 48×48 icon size rather than 50×50:

  1. Apple trims a one pixel border from the icon in order to add a shadow, so the icon is really 48×48.
  2. Since we already support 48×48 icons for the desktop, and since Apple turns your 50×50 icon into a 48×48 icon anyway, we figured we should just stick with the 48×48 icon.

Unfortunately, there’s one problem: Apple is currently trimming the one-pixel border off the 48×48 icon, so your 48×48 icons are actually 46×46. The good news is that this is a known issue and we’re fixing it in a very cool way. Rather than supporting 50×50 icons, we’re planning to continue to support 48×48 icons, but we will add a one-pixel border around the icon for you which Apple will then trim off. That means you can design your 48×48 icons without having to worry about the fact that you are going to lose two pixels in both dimensions.

I can’t say when this fix will be out just yet, but I can confirm that it’s in the works.

The Importance of Setting a Background Color When Deploying to Mobile Devices

The video below demonstrates the importance of setting a background color when you deploy to devices which support orientation changes:


The code below shows how to use SWF metadata to set a background color and avoid the ugly white bars:

package
{
    import flash.display.Sprite;
    import flash.display.StageAlign;
    import flash.display.StageScaleMode;
    
    [SWF(frameRate="24", backgroundColor="#666666")]
    public class ReversiIphone extends Sprite
    {
        private var reversi:Reversi;
        public function ReversiIphone()
        { 
            super();
            this.stage.scaleMode = StageScaleMode.NO_SCALE;
            this.stage.align = StageAlign.TOP_LEFT;
            this.reversi = new Reversi(-1);
            this.addChild(this.reversi);
        } 
    }
}

How to Build an AIR Application for Both the iPhone and iPad

If you want to build an AIR application that targets both the iPhone and the iPad, it’s easy. Just follow these steps:

1. Make Sure Your Application Scales

First and foremost, make sure your application will scale properly. Tips and tricks for making your content scale across screens is a topic for another post, but avoid obvious pitfalls like using a background bitmap scaled only for the iPhone’s smaller screen. Assuming your application is designed to scale properly, read on.

2. Specify the UIDeviceFamily

In your application descriptor, add the following as a child of the application tag:

<iPhone>
    <InfoAdditions>
        <![CDATA[
            <key>UIDeviceFamily</key>
            <array>
                <string>1</string>
                <string>2</string>
            </array>
        ]]>
  </InfoAdditions>
</iPhone>

The “1” represents the iPhone and iPod touch, and the “2” represents the iPad. Including both means your IPA targets both classes of devices.

3. Include All Necessary Icons

Make sure you include all the necessary icon sizes:

  • 29×29
  • 48×48 (in place of the 50×50 that Apple requests)
  • 57×57
  • 72×72
  • 512×512 (for iTunes)

4. Include Multiple Default.png Files

In the root of your project, you will need multiple Default.png files to accommodate the two screen sizes, and to account for the fact that iPad applications can start up in landscape mode:

  • Default.png (for the iPhone and iPod touch)
  • Default-Portrait.png (for the iPad)
  • Default-Landscape.png (for the iPad)


That’s all you have to do. Your IPA is now ready for all iOS devices. All we need now is support for the retina display (coming soonish).

(For an example of a project that works on both the iPhone and the iPad, see ReversiIphone.)

Running iReverse on the BlackBerry PlayBook

After watching this excellent MAX session by Julian Dolce, I decided to see what it was like developing AIR applications for the BlackBerry PlayBook using the BlackBerry Tablet OS SDK. As it turns out, the experience is great. It’s very easy to get started, and everything worked as expected. I went from nothing to a fully configured development environment and a working application in just about an hour.


If you’re interested in getting started developing for the PlayBook using the BlackBerry Tablet OS SDK, I recommend the following:

Have fun, and remember that if you submit an application to BlackBerry App World and it gets accepted, RIM will send you a free PlayBook.

URI Handlers in AIR for Android: Phone Calls, Email, Text Messages, Maps, Market, and URLs

The code below demonstrates five URI handlers that AIR for Android currently supports:

  • tel
  • sms
  • mailto
  • market
  • http and https

Invoking applications with URIs is pretty straightforward except in the case of maps. Although AIR does not support the Android "geo" URI intent (it’s not fully supported by Google yet, apparently), the code below demonstrates a very good work-around. Rather than explicitly opening the Maps application with a URI, you can just go to maps.google.com, and Android will ask users if they want to open the URL in the browser or in the Maps application. Simple and effective.

<?xml version="1.0" encoding="utf-8"?>
<s:View xmlns:fx="http://ns.adobe.com/mxml/2009" xmlns:s="library://ns.adobe.com/flex/spark" title="Home">
  <fx:Script>
    <![CDATA[

      import flash.sensors.Geolocation;

      private var geo:Geolocation;

      private function onCallAdobe():void
      {
        navigateToURL(new URLRequest("tel:4085366000"));
      }

      private function onTextAdobe():void
      {
        navigateToURL(new URLRequest("sms:4085366000"));
      }

      private function onEmailChristian():void
      {
        navigateToURL(new URLRequest("mailto:christian.cantrell@adobe.com?subject=AIR%20Rocks"));
      }

      private function onSearchMarket():void
      {
        navigateToURL(new URLRequest("market://search?q=iReverse"));
      }

      private function onChristiansBlog():void
      {
        navigateToURL(new URLRequest("http://blogs.adobe.com/cantrell"));
      }

      private function onGetCurrentLocation():void
      {
        this.geo = new Geolocation();
        this.geo.addEventListener(GeolocationEvent.UPDATE, onLocationUpdate);
      }

      private function onLocationUpdate(e:GeolocationEvent):void
      {
        this.geo.removeEventListener(GeolocationEvent.UPDATE, onLocationUpdate);
        var long:Number = e.longitude;
        var lat:Number = e.latitude;
        navigateToURL(new URLRequest("http://maps.google.com/?q="+String(lat)+","+String(long)));
      }
    ]]>
  </fx:Script>

  <s:VGroup width="100%" height="100%" verticalAlign="middle" horizontalAlign="center" gap="20">
    <s:Button label="Call Adobe" click="onCallAdobe();"/>
    <s:Button label="Text Adobe" click="onTextAdobe();"/>
    <s:Button label="Email Christian" click="onEmailChristian();"/>
    <s:Button label="Search the Market" click="onSearchMarket();"/>
    <s:Button label="Read Christian's Blog" click="onChristiansBlog();"/>
    <s:Button label="Map Your Current Location" click="onGetCurrentLocation();"/>
  </s:VGroup>
</s:View>