Posts in Category "ActionScript"

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>

GPU Rendering in Adobe AIR for Android

I recently started playing around with GPU rendering in AIR for Android, and I decided to do a quick video demonstrating the differences between CPU and GPU rendering modes.


The code for this example is available here on GitHub.

As you can see from the example application, the key to offloading rendering to the GPU is to do three things:

1. Set rendering mode to GPU in your application descriptor like this:

<initialWindow>
    <content>[...]</content>
    <resizable>true</resizable>
    <autoOrients>false</autoOrients>
    <fullScreen>false</fullScreen>
    <visible>true</visible>
    <renderMode>gpu</renderMode>
</initialWindow>

2. Make sure cacheAsBitmap is set to true on your DisplayObject like this:

square.cacheAsBitmap = true;

3. Make sure to assign a Matrix to the cacheAsBitmapMatrix property on your DisplayObject like this:

square.cacheAsBitmapMatrix = new Matrix();

That’s all you have to do to offload your rending to the GPU and to free your CPU up for more important tasks.

Loading Classes Dynamically in ActionScript 3

Loading classes dynamically in ActionScript 3 is easy, but there are a couple of tricks to keep in mind.

First of all, to load a class dynamically, use the flash.utils.getDefinitionByName function. Be sure to pass in the fully qualified class name, like this:

var MyClass:Class = getDefinitionByName("com.mydomain.package.MyClass") as Class;
var myInstance:MyClass = new MyClass();

Easy enough. But there’s something to watch out for. Class definitions only get compiled into your SWF if they’re declared somewhere in your code. In other words, if MyClass was never declared, the class definition wouldn’t be included in your SWF, and you would get a runtime error when trying to load it dynamically. (Note that this is not the case for classes native to the runtime — only your own custom classes.) Even importing the class isn’t enough to get it included — it must actually be declared.

There are two ways to work around this issue:

  1. Declare the class somewhere in the code.
  2. Use a command line argument to force the class definition to be included.

Below is an example of declaring the class before loading it dynamically:

MyClass;
// or
var mc:MyClass;
var MyClass:Class = getDefinitionByName("com.mydomain.package.MyClass") as Class;
var myInstance:MyClass = new MyClass();

This works fine, but in my opinion, it may defeat the purpose of loading a class dynamically. Usually you want to load classes dynamically because you’re not sure until runtime which class you’re going to want. If you knew which classes you wanted before you loaded them dynamically, you could just instantiate them directly.

The other option is to use the mxmlc -includes compiler argument like this:

-includes=com.mydomain.package.MyClass

Now, the class definition will be compiled into your SWF, and you can load it dynamically anytime you want without ever having to declare it.

To specify compiler arguments in Flash Builder, follow these steps:

  1. Right-click on your project.
  2. Choose “Properties”.
  3. Select “Flex Compiler”.
  4. Enter your arguments in the “Additional compiler arguments” section.

As an aside, you can use the flash.utils.getQualifiedClassName and flash.utils.getQualifiedSuperclassName functions to get the fully qualified class name of an object instance.

Using Bookmarklets in Adobe AIR

I use a lot of bookmarklets in my browser: Readability, Delicious, Instapaper, Twitter, etc. So I got to wondering the other day if it was possible to use bookmarklets in AIR. As you can see from the video below, the answer is yes:


There are two different techniques for getting bookmarklets to work in AIR. If your bookmarklet doesn’t need to open a new window, it’s as easy as calling eval() on your bookmarklet JavaScript like this:

this.html.htmlLoader.window.eval("bookmarketJavaScriptHere();");

But if your bookmarklet needs to open a new window, things get a little more complicated. For security reasons, it’s not possible to programmatically call window.open() in AIR unless the call is invoked as a result of user interaction. In other words, the only way to open a new browser window in AIR is for the user to choose to do so. That way, remote content can’t open arbitrary windows and try to trick users into interacting with them (or annoy them with ads). A good way to think about it is that AIR has a built-in popup blocker. (For more information, see HTML Security in Adobe AIR.)

In order to get bookmarklets like Twitter and Delicious to work in AIR, therefore, you have to inject HTML into the host page and give the user something to click on. For a good example (and some pretty slick script-bridging between ActionScript and JavaScript, IMHO), check out the onMore() function in Browser.mxml.

All the code for the BookmarkletDemo project is available here, so check it out if you’re curious how it works.

If you’re getting mysterious IOErrors with URLLoader, URLStream, or Socket, this might be why

MailBrew was the first application I wrote with the new global error handler feature in AIR 2 and Flash Player 10.1. Whenever an unhandled error is thrown, the application opens a utility window with some details and a request to send the information to me so I can get the bug fixed for the next version. I’m a pretty conscientious coder — even in areas where ActionScript 3 doesn’t require you to be — so I wasn’t expecting many reports, but I got several relating to mysterious IOErrors. Unfortunately, errors caught with the global error handler don’t have stack traces, so debugging wasn’t going to be easy. As far as I could tell, I was already catching and registering for IOErrors and IOErrorEvents in all appropriate places, so I initially had no idea what was going on.

Fortunately, Daniel Koestler was finally able to reproduce the error in ADL and get a stack trace. At first, however, it didn’t seem to make any sense. The line throwing the uncaught error was this:

this.urlLoader = new URLLoader();

It took me a minute to figure it out, but now I think I know what’s going on. I believe when the old URLLoader was being destructed (when this.urlLoader already referenced a URLLoader instance), its close() function was implicitly being called. This was usually fine, but in some circumstances, close() can cause a stream error (a type of IOError) which I never had the opportunity to catch. So even though I was registering for IOErrorEvents, and I was calling close() in a try/catch block, it was possible for the close() function to be called in a way that I could not anticipate.

I’m think I’m going to file this as a bug and ask that IOErrors not be thrown in circumstances where close() is called implicitly (in other words, the code that calls close() in the object’s destructor should swallow the exception rather than allow it to propagate), but until it’s fixed, here’s my work-around:

private function start():void
{
    this.dispose();
    this.urlLoader = new URLLoader();
    this.urlLoader.addEventListener(IOErrorEvent.IO_ERROR, onIOError);
    this.urlLoader.addEventListener(Event.COMPLETE, onComplete);
    this.urlLoader.addEventListener(HTTPStatusEvent.HTTP_RESPONSE_STATUS, onResponseStatus);
    // Go on to use the loader...
}

public function dispose():void
{ 
    if (this.urlLoader != null)
    {
        try
        {
            this.urlLoader.close();
        }
        catch (e:IOError)
        {
            // No problem. We're getting rid of it, anyway.
        }
        this.urlLoader.removeEventListener(IOErrorEvent.IO_ERROR, onIOError);
        this.urlLoader.removeEventListener(Event.COMPLETE, onComplete);
        this.urlLoader.removeEventListener(HTTPStatusEvent.HTTP_RESPONSE_STATUS,
                                           onResponseStatus);
        this.urlLoader = null;
    }
}

As you can see, I’m now making sure to call close() explicitly myself before the runtime has the opportunity to call it implicitly whenever I assign a new instance of a URLLoader to a class-level variable. My guess is that there’s a lot of code out there prone to throwing this error, but because the global error handler is so new, most of us just don’t know it yet.

Protect Your AIR Applications From Phantom Monitors

If you use multiple monitors on a regular basis, you have probably been in a situation where a window gets stranded on a nonexistent virtual monitor. Sometimes the fix is to restart the application; sometimes the only way to fix it is to reconnect the monitor, retrieve the window, and drag it on to the main monitor before unplugging it again. Lame.

Don’t write AIR applications that aren’t phantom monitor-proof. If you allow secondary windows to be opened, make sure you think about what will happen when these windows end up on monitors that no longer exist.

AIR and the host operating system usually just do the right thing which is to move all the windows over to the main monitor when non-primary monitors go away. This is always true of normal windows (NativeWindowType.NORMAL). Additionally, if you don’t specify window coordinates at all when opening a new window, the operating system is smart enough to make it appear on an actual monitor rather than one that isn’t there anymore. But if you’re working with lightweight or utility windows, and if you’re controlling where they open, this is something you need to watch out for.

I ran into this issue when working on a new version of MailBrew (which I will probably release this week). I got several requests to make the summary window (a little window that shows you how many unread messages you have for each account) reopen when the application starts, and to position it wherever it was last placed. The problem is that the summary window could very easily be placed on a monitor that no longer exists.

In order to prevent falling victim to a phantom monitor, I wrote this very simple function:

private function verifyPosition():void
{
    var screens:Array = Screen.getScreensForRectangle(this.nativeWindow.bounds);
    if (screens.length == 0)
    {
        var mainScreen:Screen = Screen.mainScreen;
        var newPoint:Point = new Point(mainScreen.visibleBounds.x + 2,
                                       mainScreen.visibleBounds.y + 2);
        this.setLocation(newPoint);
    }
}

All I have to do is call verifyPosition from any code that might cause the summary window to be repositioned. If the summary window is off-screen, I simply reposition it in the top left-hand corner and let the user move it him/herself.

A more interesting solution might be to write some heuristics which tried to place the summary window in a similar location to where it was placed on the nonexistent window, but in order to get the new version of MailBrew out the door this week, I went with a simpler approach. Maybe I’ll have a go at this problem in the next version (and share the code).

Note that this does not address every circumstance when the summary window could end up on a phantom monitor. Specifically, it does not address the situation where the user places the summary window on a different monitor, then changes the monitor configuration without restarting the application or reopening the summary window from the main application widow. The reason I don’t have a more encompassing solution is that AIR currently does not have APIs to detect a change in the user’s monitor configuration, so the only bulletproof way to do this would be set a timer to constantly check to see if the summary window is on a phantom monitor. That seemed like overkill to me, so I wrote my code to do the following instead:

  • When the application is started, if the summary window will not be visible, it defaults to the top left-hand corner of the main monitor.
  • When the summary window is opened from the main application window, if its last location no longer exists (because it’s on a phantom monitor), it defaults to the top left-hand corner of the main monitor.
  • Whenever new email messages are found, if the summary window is open on a phantom monitor, it is repositioned in the top left-hand corner.

In other words, the summary window will never be irrevocably lost, and it will always fix itself eventually. In no cases will ever require the user to reconnect a monitor to retrieve it.

If your applications might be susceptible to phantom monitors, I highly encourage you to use a technique like this. In this age of powerful multi-headed graphics cards, inexpensive LCDs, and extensive multi-tasking (requiring multiple monitors for many of us), it’s only a matter of time before a user runs into this and thinks significantly less of your application.

A Simple Zip Utility for Adobe AIR

I threw together a simple application for zipping and unzipping files in AIR using the FZip project. The application isn’t earth-shattering, but it’s a nice Flex 4 and AIR sample project, and all the source code is available. Screenshot below.

Continue reading…

My Presentation on Multi-screen Development

I did a presentation the other day at the DC Flex User Group about how to write one application that will adapt to any screen size. The application is called iReverse, and you can watch the presentation here. The source code for the app can be found at the links below:

A Third Kind of Orientation: Head-to-Head

Digital board games are going to be big. If you have any doubt, just check out Scrabble on the iPad. But to get the most out of digital board games, I think we’re going to need a third orientation mode in addition to portrait and landscape: head-to-head, or simply flat.

Of course, you don’t have to wait for devices or operating systems to support a third orientation mode. As long as you have access to the accelerometer, you can program head-to-head mode yourself. Below is an example of why a "flat" orientation makes sense (as well as a demo of iReverse running on a Lenovo X201 multi-touch laptop).

Continue reading…

Simple Accelerometer Example

I’m adding some custom accelerometer/orientation behavior to the iReverse game I’m working on, so I threw together a simple test app to output accelerometer data. I figured I’d post the code in case anyone else was working on something similar.

Code and screenshot below:

package
{
    import flash.display.Sprite;
    import flash.display.StageAlign;
    import flash.display.StageScaleMode;
    import flash.events.AccelerometerEvent;
    import flash.events.Event;
    import flash.sensors.Accelerometer;
    import flash.text.TextField;
    import flash.text.TextFormat;

    public class AccelerometerTest extends Sprite
    {
        private var textFormat:TextFormat;
        private var output:TextField;
        private var accelerometer:Accelerometer;

            public function AccelerometerTest()
            {
                super();
                this.stage.scaleMode = StageScaleMode.NO_SCALE;
                this.stage.align = StageAlign.TOP_LEFT;

                this.textFormat = new TextFormat();
                this.textFormat.color = 0x000000;
                this.textFormat.font = "Helvetica";
                this.textFormat.size = 20;

                this.output = new TextField();
                this.output.defaultTextFormat = this.textFormat;

                this.stage.addEventListener(Event.RESIZE, doLayout);

                this.accelerometer = new Accelerometer();
                accelerometer.addEventListener(AccelerometerEvent.UPDATE, onAccelerometerUpdate);
            }

            private function doLayout(e:Event):void
            {
                this.output.width = this.stage.stageWidth;
                this.output.height = this.stage.stageHeight;
                this.output.x = 0;
                this.output.y = 0;
                this.addChild(this.output);
            }

            private function onAccelerometerUpdate(e:AccelerometerEvent):void
            {
                var str:String = "x: " + e.accelerationX;
                str += "\ny: " + e.accelerationY;
                str += "\nz: " + e.accelerationZ;
                this.output.text = str;
            }
    }
}