Archive for the 'flex' Category

Interacting with an AIR app from a browser based app

Interacting with an AIR app from a browser based app

We’ve all seen the AIR installation badges that let you install an AIR application from a website. But the API exposed to do that lets you do more than just a simple badge. I’ve been working on a web-based service for AgileAgenda. One of the components of that is to manage the list of files you’ve saved to the service and be able to open those in the desktop AIR application. So right from within the online Flex based app it sure would be nice to detect if the application is installed, give the user the option to install, and then launch it for them and automatically open the desired file. Something like this…

To implement that we need to:
  1. Detect whether or not the application is installed.
  2. Display the version number if it is. (Disable the “Open schedule in…” button if it’s not)
  3. When clicking on the “Open” link, launch the application with a few parameters so it know what to open.
  4. When clicking on the “Install” link, install the application and pass a few parameters so it know what to open when it launches directly after the install.
Doing things like that falls outside of the normal AS3 web development API. But Adobe provides a swf that you can load at runtime and them make calls to. You can read all about that over here: http://livedocs.adobe.com/air/1/devappshtml/help.html?content=distributing_apps_3.html.
But there’s an annoying thing with that. You need to load up that remote swf and then access it like a dynamic object. No compiler-time type checking. Not the standard event mechanism. And no code-completion from within Flex Builder.
Luckily for you, I went through and made a wrapper class that you can download from here:
This will handle the loading of the remote swf, dispatching events when it loads (or fails to load) and then wraps the API for the entire process. There’s nothing revolutionary in there, it’s mostly takendirectly from that livedocs page above.
So now that you have that handy-dandy wrapper class, lets look at how to actually get something done.
Setting up your AIR application to work with your website (Important)
First thing, go into your AIR application’s -app.xml file and make sure allowBrowserInvocation is set to true. By default it’s set to false.

<allowBrowserInvocation>true</allowBrowserInvocation>

If you don’t do this, you won’t even be able to query version information on your application. But, be careful. By doing this you’re letting any website launch your AIR application from a web page. You need to be careful in how much your app trusts command line arguments passed to it. For instance, you should never pass a file to delete on the command line.

Now that you’ve set allowBrowserInvocation to true, create a new .air file and post that to your website somewhere.

Using the wrapper class to interact with your application

Either open an existing Flex or Actionscript project in FlexBuilder and put the source to the AIRBrowserRuntime.as somewhere that the compiler will find it. Somewhere in your main application, create an AIRBrowserRuntime object, set some event listeners, and call the load() method to load the air.swf file from Adobe’s servers.

var api:AIRBrowserRuntime;

api = new AIRBrowserRuntime();

api.addEventListener(AIRBrowserRuntimeEvent.READY, onReady );

// Optional: api.addEventListener(IOErrorEvent.IO_ERROR, onAirFail );

api.load();

Once the READY event is dispatched, you can start calling methods to query application versions, install apps, or launch applications.

Checking if AIR is installed

To see if the AIR runtime is installed, call the getStatus() method. It will return one of three values.

available - The AIR runtime can be installed on this computer but currently it is not installed

unavailable - The AIR runtime cannot be installed on this computer.

installed - The AIR runtime is installed on this computer.

Example:

switch( api.getStatus() )

{

case “available”: trace(”AIR is available, but not installed.”); break;

case “unavailable”: trace(”AIR is not available for this computer.”); break;

case “installed”: trace(”AIR is already installed on this computer.”); break;

}

Checking the version of an installed application

The getApplicationVersion method will check to see if a given application is installed and give you the version if it is. This method operates asynchronously so you have to create an event listener before you call it. If you look at the method signature…

public function getApplicationVersion(applicationID:String, publisherID:String) : void

You’ll see that it takes an applicationID and a publisherID. The applicationID is just value of the <id> tag of your application descriptor (the -app.xml file). It’ll probably be something like this, but you need to make sure to make each application unique:

<id>com.agileagenda.AgileTracker</id>

The publisherID is a little trickier to find. That doesn’t get assigned until you actually sign your .AIR file with your code-signing key. I know of 2 ways of finding it, but there’s probably an easier way (please leave a comment if you know how).

Warning about Publisher ID:  If you change the key your sign your app with, the publisher ID will change.

Option 1: Get it at runtime.

In your application’s main MXML throw up an Alert message with the publisher ID. Then build a .air, install the .air and run it. Copy & Paste the result.  Example:

<?xml version=”1.0″ encoding=”utf-8″?>

<mx:WindowedApplication xmlns:mx=”http://www.adobe.com/2006/mxml

    creationComplete=”init()” >

  <mx:Script>

  <![CDATA[

    protected function init() : void 

    {

      Alert.show( nativeApplication.publisherID);

    }

  ]]>

  </mx:Script>

</mx:WindowedApplication>

Which results in something resembling:

It’s hard to see in that picture, but the publisher ID is: F49A4D8DF78A1FEE7A3BE440DC11BAB18D922274.1 as it wraps to two lines.
Option 2: Get it after install
This is probably a bit easier, but I’m not 100% sure where this directory goes on a windows box.
On OSX (maybe on Windows, I’m not sure exactly where) the application storage directory that gets created for your application has the publisher ID appended to it. So if you look in your user directory -> Library -> Preferences, you should see a directory that starts with your application ID and ends with your publisher ID.
As you can see there, we get the same publisher ID value as Option #1.
On to checking installed version
Now that you have your application ID and your publisher ID you can make a call from your web application to the API to request the installed version of your AIR application.  This operates asynchronously so you’ll need an event listener for the result.
Example:

api.addEventListener(AIRBrowserRuntimeEvent.APP_VERSION_RESULT, onTrackerVersionResult );

api.getApplicationVersion( “com.agileagenda.AgileTracker”, “F49A4D8DF78A1FEE7A3BE440DC11BAB18D922274.1″ );

protected function onTrackerVersionResult(event:AIRBrowserRuntimeEvent) : void

{

trace(“Tracker version installed: “ + event.detectedVersion );

}

The event.detectedVersion that comes back will be the value in your application descriptor version tag.  Mine looks like this:

<version>v1</version>

So the trace output looks like this:

Tracker version installed: v1

Launching an installed AIR application

Assuming you followed along in the previous section, you have an Application ID and a Publisher ID ready. To launch an AIR app from a web app you just call

api.launchApplication( “com.agileagenda.AgileTracker”, “F49A4D8DF78A1FEE7A3BE440DC11BAB18D922274.1″ );

launchApplication has a third, optional, parameter called arguments and is typed as an array. Anything you pass into that will be passed along to the application in an INVOKE event. This way you can pass information from the web application to the AIR application. Example:
api.launchApplication( “com.agileagenda.AgileTracker”, “F49A4D8DF78A1FEE7A3BE440DC11BAB18D922274.1″, ["Argument1","Argument2" ] );
Be careful, the arguments are very restrictive in what characters can be passed. Things like dashes, percent signs, underscores, all cause a runtime exception on the web application. You’re probably safest only using alphanumeric characters. I haven’t found a comprehensive list of what characters are or are not valid. If someone has that, please leave a comment below.  This was done for security reasons.
Installing an AIR application
To install an AIR application, all you need to do is call the installApplication with the absolute URL to the .air file you want to install. Example:
api.installApplication(”http://www.agileagenda.com/download/AgileTracker.air”);

This will take care of installing the AIR runtime, installing the application, and launching it for the first time. installApplication has two more optional parameters. The AIR runtime that the application requires, and arguments that can be passed to the application for it’s first run. Example:

api.installApplication(”http://www.agileagenda.com/download/AgileTracker.air”,”1.0″,["Arg1","Arg2"]);
That’s all there is to it.
So that’s it, pretty simple, huh? You might also want to look into using a LocalConnection to do realtime communication between your web-app and your air-app once the air-app has been launched. Recap of the links:
Wrapper Class:
Adobe’s Documentation:
My Blog (where any updates to the wrapper class will be posted)
The actual air.swf that does all the heavy lifting (This URL is wrong in some of the docs!)

http://airdownload.adobe.com/air/browserapi/air.swf

ObjectHandles Demo

Here’s a short demo of some of the stuff that ObjectHandles (my Flex library for moving & resizing stuff) can do with a very minimal amount of code.  

The custom things I did:
  • Has a MOVING / RESIZING event handler to show a custom tooltip (hides the tooltip on MOVED / RESIZED)
  • Has custom resize handle images that look like grey horizontal bars
  • Only allows vertical resizing (allowHResize = false).
  • On a MOVED event, the objects have an animation that snaps them to a column.
It’s a little hard to see in that video, but the duration & start time in the tooltip update as you move or resize the boxes around.

Custom Cursors + Multiple Windows

Quick tip.  Don’t use CursorManager.setCursor anymore.  It works fine for single-window flex apps, but breaks once you have an AIR app with multiple windows.

UIComponent now has a cursorManager property that references the cursor manager for the current window.  If you use that it’ll work all the time, not just in web Flex apps.
I understand why they left CursorManager in there for backwards compatibility, but it’s a shame we can’t mark it as @deprecated like in the Java world.

AgileAgenda - New Version

Posted a new version of AgileAgenda over the weekend.  Fixed a few minor bugs, redesigned the opening screens, and put in our new logo.  The entire initial user experience should be better now.   Here’s a quick glimpse of what it looks like now:

ObjectHandles 1.0.9 available for download

New version is up. It fixes the selectNone() bug, has the correct
metadata for events, and no longer creates event listeners on the
parent object for every single OH you instantiate.
I also added an updateAfterEvent call when things are getting moved,
it really made the motion a lot smoother, never thought it would
matter much.

http://code.google.com/p/flex-object-handles/downloads/list

ASDocs now posted as well, it’s pretty sparse so it looks like I need to add
in a bunch of comments :)

http://rogue-development.com/objecthandles/asdocs/

-Marc

Spreadsheet Demo #2 - ComboBox

UPDATE: http://www.rogue-development.com/blog/2008/03/first-public-release-of-my-flex.html
Last night I wanted to see how my component would work with custom editors that weren’t just text boxes.

Here’s a quick demo of my spreadsheet component with a custom renderer descended from a combo box.  Notice a few things:

  • 1 click on a cell to open, not one click to activate the cell and another to open it.
  • You can keyboard navigate to it, hit the down-arrow, open the list of choices, use the arrows to select one, and then hit enter to select it, just like you should be able to.

New ObjectHandles Build

I posted a new ObjectHandles build yesterday.  It fixes a couple bugs and integrates some patches people have sent me including:

1) Graphical handle support
2) Fixed aspect ratio support
3) Ability to detect transparency & clicks 
There will likely be another build in the next week to integrate another patch and fix a few a performance issue.

Modal dialog blur

I usually want to turn off the modal dialog blur in applications but can never remember how. This blog post is here so I can find it quickly next time.

Set this style:

Application {
modalTransparencyBlur: 0;
modalTransparencyDuration: 0;
modalTransparency: 0;
}

DataGrid can go DIAF

(That’s “Die in a fire” for those of you not hip to texting lingo and don’t have a little sister to help make you hip)

Making the DataGrid do things it’s not meant to do is amazingly hard. Look at this gem that’s found in the itemEditorItemEditEndHandler method:

if (event.reason == DataGridEventReason.OTHER || !event.isDefaultPrevented())
{
destroyItemEditor();
}

If the event is canceled it goes ahead and destroys your item renderer.

So if you want to say, NOT end editing and you cancel the event, it goes ahead and kills your editor anyways.

No problem… we could just override the method and fix it. Except someone on the 2.0 SDK team got private method happy with that one.

Are there any good DataGrid alternatives out there? Especially for mimicking spreadsheet functionality where you’re adding and removing rows dynamically through user input?

My first Red5 experience

I’ve worked with both Chris Allen and John Grden on some projects for my employer. During that time, I can’t count the number of times I heard about Red5, but I never got a chance to try it out.

This past week I caught Chris’ “Building Red5 Applications” session at FlashForward. I finally got up the motivation to download and try it out.

Here’s my experience. It’s my first time playing with Red5 (but I have plenty of java & eclipse experience) so it’s almost guaranteed I’m not doing everything the “right way”. But this way worked for me, maybe it’ll help someone else. If it does help you, please leave a comment. Writing this up takes a while and I’m not going to bother in the future for other things if nobody gets value out of it.


Attempt #1

I wanted to try out the .tar.gz package so I would know how to deal with it on Linux as well as Windows. That way I’d be able to deploy anything I built on my webhost fairly easily.

I unpacked it, ran ant, and let it all build. So far so good.

Then I ran the red5.bat file. Lots of exceptions. Like millions, maybe billions, I lost track. Seems my sample applications weren’t found. I poked around a bit, but no good. Screw this.

Attempt #2
I went for the windows installation package. That unpacked everything and the server ran fine. I could open up a web browser to http://localhost:5080/ and view the demos, admin panel, etc.

Note: The default admin username/password is admin/admin. You can change that in conf/tomcat-users.xml I’d suggest changing it.

Eclipse

So now I want to get it rocking with eclipse. I launch eclipse (I’m lazy and like things easy so I use easy eclipse.)

I select File->Import->Existing project, then pick my Red5 project. Bamn, I got an eclipse project set up.

But there’s compile errors.

Severity and Description Path Resource Location Creation Time Id
Project red5_server is missing required source folder: ‘test’ red5_server Build path 1190574481375 173990
The project cannot be built until build path errors are resolved red5_server Unknown 1190574530687 173992

So I closed the red5 eclipse project, fired up a text editor, and looked in my .classpath file inside the red5 folder. It had the offending entry.

<classpathentry kind=”src” path=”test”/>

Note… I delete this line later on:
<classpathentry kind=”con” path=”org.jayasoft.ivyde.eclipse.cpcontainer.IVYDE_CONTAINER/ivy.xml/test,”/>
You might as well do it now since you’re in there.

So I removed it, and repopened eclipse.

A quick Project->Clean and…
3656 Errors

Ok, seems it doesn’t see some libraries. Right click on my project, select properties. Go into “Java Build Path”, select the “Libraries” tab, click the “Add Jars…” button, and add all of the .jar files in the lib directory.

A quick Project->Clean and…
0 Errors

WOOT

Running it from Eclipse

Now I go into src/org/red5/server, find the StandAlone.java, right click it, and select “Run as…”->”Java Application”.

I check my eclipse console panel and get a bunch of lines the last one being:

[INFO] 2007-09-23 15:14:20,875 main:( org.red5.server.Standalone.main ) Startup done in: 5875 ms

Nice.

I point a web browser at: http://localhost:5080/ and see that the red5 server is indeed running. I launch the ball demo and push the connect button. Then I make a new browser window and launch the demo again. When I move the red5 logo in one window, it moves in the other. That means it’s all working.

Eclipse also helpfully added an entry to my run button (the green circle with the play button) in my toolbar so I don’t have to find the StandAlone.java file again.

I want a quick launch button for debugging too, so I kill the server (click the red square on the console panel) and right click the StandAlone.java and select “Debug as java application”. The server starts in debug mode, and eclipse adds it to my debug button on the toolbar. (the green beetle button)

Making something

Now, I want to make something. But first, I want to get rid of the samples. So I kill the server, close the eclipse project, and edit the .classpath file again. I know there’s a way to do this through eclipse, but I find editing the file quicker.

I get rid of these entries:
<classpathentry excluding=”.svn/*” kind=”src” path=”webapps/echo/WEB-INF/src”/>
<classpathentry excluding=”.svn/*” kind=”src” path=”webapps/tutorial/WEB-INF/src”/>
<classpathentry excluding=”.svn/*” kind=”src” path=”webapps/fitcDemo/WEB-INF/src”/>
<classpathentry excluding=”.svn/*” kind=”src” path=”webapps/oflaDemo/WEB-INF/src”/>
<classpathentry excluding=”.svn/*” kind=”src” path=”conf”/>
<classpathentry excluding=”.svn/*” kind=”src” path=”webapps/test/WEB-INF/src”/>
<classpathentry excluding=”.svn/*” kind=”src” path=”webapps/midiDemo/WEB-INF/src”/>
<classpathentry excluding=”.svn/*” kind=”src” path=”webapps/agile/WEB-INF/src”/>

And I add one for my project, I’m going to work on something for AgileAgenda, so I’ll call it agile.

<classpathentry excluding=”.svn/*” kind=”src” path=”webapps/agile/WEB-INF/src”/>

Then I go rename the “echo” example to “agile”, and delete all the other samples (I deleted admin too, not sure if that was a good idea or not)

Open up the project in Eclipse again.

And I don’t see my agile webapp. WTF? Oh yeah… Eclipse is retarded. Right Click->Refresh and it shows up.

I want to rename the Application class in my webapps folder (see the picture).

Luckily, Eclipse rocks Java more than it rocks Flex, so I can just right click, select refactor, and pick move. Then I click “Create Package” enter in com.agileagenda.services and click ok a bunch of times.

I delete the rest of the webapp echo source tree.

A project->Clean and…

I think I broke something. Oh well.

Project->Properties->Builders and remove the invalid entry.

Project->Clean and build successful.

I hit the debug button, and search the console output. No errors reported and:

[INFO] 2007-09-23 15:44:45,375 main:( /agile.invoke0 ) Initializing Spring root WebApplicationContext

So I guess it found my new app. Yay!

Kill the server.

So I go back to my Application class and make a new method.

public function login(username:String, password:String) : Boolean
{
return true;
}

Then I remember that this is Java and not Actionscript so I rewrite it…

public boolean login(String username, String password)
{
return true;
}

I go to my project menu and turn on “Build Automatically” and get…

Severity and Description Path Resource Location Creation Time Id
Unbound classpath container: ‘org.jayasoft.ivyde.eclipse.cpcontainer.IVYDE_CONTAINER/ivy.xml/test,’ in project red5_server red5_server Build path 1190576857156 182811

WTF?

Close the project, open my .classpath file, remove the offending line:
<classpathentry kind=”con” path=”org.jayasoft.ivyde.eclipse.cpcontainer.IVYDE_CONTAINER/ivy.xml/test,”/>

Open the project, Project->Clean, build success.

I have no idea why that didn’t bite me earlier.

I press the debug button. The red5 server launches.
[INFO] 2007-09-23 15:51:26,062 main:( org.red5.server.Standalone.main ) Startup done in: 4937 ms

Yay.

Writing some actionscript

Now that I have a (dumb) login method, I want to try it out in Flex.

I have a “Sandbox” AIR application set up in Eclipse that I use when I want to quickly play with something. I highly suggest everyone do that. So I open my sandbox, open the MXML and create a quick login form.

<mx:Label x="26" y="43" text="Username:"/>
<mx:TextInput x="101" y="41" id="txtUsername"/>
<mx:TextInput x=”101″ y=”71″ id=”txtPassword” displayAsPassword=”true”/>
<mx:Label x=”26″ y=”73″ text=”Password”/>
<mx:Button x=”196″ y=”101″ label=”Login” click=”onLoginClick()”/>
<mx:Label x=”58″ y=”148″ text=”Label” id=”lblResult” width=”281″/>

And then add in some code (remembering that this is Actionscript!)…

private var netConn:NetConnection;

protected function onLoginClick() : void
{
NetConnection.defaultObjectEncoding = flash.net.ObjectEncoding.AMF0;
SharedObject.defaultObjectEncoding = flash.net.ObjectEncoding.AMF0;

netConn = new NetConnection();
netConn.addEventListener(NetStatusEvent.NET_STATUS, onNetStatus );
netConn.addEventListener( SecurityErrorEvent.SECURITY_ERROR, errorHandler );
netConn.addEventListener( AsyncErrorEvent.ASYNC_ERROR, errorHandler );
netConn.addEventListener( IOErrorEvent.IO_ERROR, errorHandler );

netConn.connect(”rtmp://localhost/agile”);

}

protected function sendLogin() : void
{
var responder:Responder = new Responder( onLoginResult );
netConn.call(”login”,responder,txtUsername.text, txtPassword.text);
}

protected function onLoginResult( result:Boolean ) : void
{
lblResult.text = result ? “Login Success!” : “Login Fail!?!”;
}

protected function onNetStatus(event:NetStatusEvent) : void
{
switch( event.info.code )
{
case “NetConnection.Connect.Success”:
lblResult.text = “Connected, sending login”;
sendLogin();
break;

default:
lblResult.text = “ERROR!! ” + event.toString();
break;
}

}

protected function errorHandler(event:Event) : void
{
lblResult.text = “ERROR! ” + event.toString();
}

I start the server, run my sandbox app, click the login button, and get a connection error. Something went wrong somewhere.

I bet it’s a configuration problem. I open webapps/agile/WEB-INF and notice 3 config files.

in web.xml I make this change:

<context-param>
<param-name>webAppRootKey</param-name>
<param-value>/agile</param-value>
</context-param>

In red5-web.xml I make this change:

<bean id=”web.handler”
class=”com.agileagenda.services.Application
singleton=”true” />

In red5-web.properties, I make this change:

webapp.contextPath=/agile

Kill the server, restart it (debug mode), try my app again. “Login Sucess” YES!

Now I head into the java class and put a breakpoint on the “return true;” line. I hit the button on my AIR app and Eclipse correctly breaks on the line. Yay!

I inspect the username and password parameters, and the values I had filled into the AIR app are present. Yay!

I’m now to the point where I can start actually writing my application.