Archive for August, 2010

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.

MailBrew Beta Test

I’m hoping to update MailBrew this week, but I’d love to get a few beta testers first. Below is a list of the changes I made:

  • Added opt-in analytics (using Google Analytics) to help me better understand how people are using the application.
  • Fixed two runtime errors that people were reporting: IOError and a TypeError.
  • Made the summary window selection and its location persistent across sessions (meaning it will reopen and position itself next time you open the app).
  • Added "Check All" to the Dock and System Tray menus (by special request).
  • Fixed one or two other small bugs that made it through my QA process.

If you want to help beta test 0.91, download the AIR file and give it a try. If you find an issue, please either post details here, or email me at my first name dot my last name at adobe dot com.

Thanks!

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.