Advertisement

Build a GPS Speedometer: Getting Into AIR for Android

by

This Cyber Monday Tuts+ courses will be reduced to just $3 (usually $15). Don't miss out.

Just about every smart phone out there these days has GPS capabilities but apps that take advantage of this don't all have to be about boring old maps. This tutorial will introduce you to Adobe AIR for Android and lead you through the development of an ActionScript 3.0 speedometer app that will run on Android 2.2 devices.

Check out part two of this tutorial Build a GPS Speedometer: User Interface and Polish over on our sister site Activetuts+!

Those who are interested in the platform but don't yet have an Android device can follow this tutorial and test within Flash Professional.


Step 1: Overview

Adobe AIR for Android creates many exciting opportunities for Flash developers wishing to move to the mobile space.

This tutorial will introduce you to the subtle differences when applying your ActionScript skills to mobile. It will lead you through the steps required to write, deploy and test a fully functioning app on your Android handset.

Particular attention will be paid to the geolocation and filesystem classes, which are specific to the AIR SDK.

Don't worry if you don't have an Android 2.2 handset. You'll still be able to build and test the ActionScript within Flash CS5.


Step 2: Installing Adobe AIR for Android

Before you can start developing you'll need to download and install the following components:

  1. Flash Professional CS5 (30-day trial version will do)
  2. The Adobe AIR runtime for Android 2.2
  3. The Adobe Flash Professional CS5 Extension for AIR 2.5
  4. USB Device Drivers (Windows Only)
  5. Adobe AIR 2.5.1 SDK

If you don't already have Flash CS5 then you can download a 30-day trial from Adobe.

If you plan to deploy and test on an actual handset then you'll need to install the free Adobe AIR runtime from the Android Market (just search for Adobe AIR in the Market application).

I used a Google Nexus One for this tutorial but AIR will run on Android devices that meet the following system requirements:

  • Android 2.2 operating system
  • ARMv7-A processor with vector FPU
  • OpenGL ES 2
  • H.264 and AAC hardware decoders
  • 256MB of RAM

If AIR for Android is not supported for your Android handset then you can still follow this tutorial and test in Flash CS5.

You can download the Extension for AIR 2.5 from Adobe Labs. Instructions detailing how to install the Extension can be found here. If you're using Windows then you'll also need to follow the steps detailing how to install USB device drivers that allow your Android device to communicate with the Android SDK.

Finally, download and install the latest version of the Adobe AIR SDK.

Okay, we're read to start coding.


Step 3: FLA Settings

I have provided a FLA containing the artwork required for this tutorial. We will work from this FLA but before we do that, let's first familiarize ourselves with the steps required to create a FLA that targets AIR for Android. After all, you'll need to do this for any future projects of your own.

I'm using Windows 7 for this tutorial, but where necessary I've provided instructions for those using Windows Vista, Windows XP, and Mac OS X.

Launch Flash CS5 and select File | New (Ctrl + N) from the drop-down menu.

From the New Document panel select the Templates tab. From the Category section select: AIR for Android and select: 480x800Android from the Templates section.

Clicking OK will create and set up a FLA that targets AIR for Android. You can confirm this by examining the Stage's Properties panel:

You should clearly see from the PUBLISH section that your FLA is set to use the AIR Android player and that there is a link to open the AIR Android Application & Installer Settings.

Close your FLA and open speedometer-artwork.fla from the source download. We'll work from this FLA from now on. Before continuing, find a suitable location on your hard drive and save it as speedometer.fla.

The FLA has a stage size of 480 x 800 which matches the screen resolution of the Google Nexus One that I used when writing and testing this tutorial. Not all Android devices have the same screen resolution however. For example, the Motorola Droid has a screen resolution of 480 x 854.

If your device's screen resolution differs then change the stage size to match it. To do this click the Edit... button within the Properties panel underneath the PROPERTIES section:

A Document Settings panel will appear. Change the width and height within this panel and click the OK button.

Save the FLA.


Step 4: Testing Geolocation

The speedometer app will rely on the results we continually receive from your phone's GPS sensor. In particular we are interested in your current speed, which is measured in metres per second.

Let's start by familiarizing ourselves with AIR's Geolocation class and outputting your current speed to a temporary text field.


Step 5: Test Interface

Select the Text Tool (T) and draw a 460 x 167 text field onto the stage.

Position the text field at (10, 10). Ensure that the text engine is set to Classic Text and that the field is of Dynamic Text type. Name your text field instance "metresPerSecond."

I used the Regular Droid Sans font with a size of 140pt, however if you don't have that font installed then feel free to use an alternative such as Arial. I also chose to show a border around my text field.

Enter a default value of "0" into the text field and centre-align the text.

Here's a snapshot of the above settings.

Finally click on the Embed... button and embed the font's numeral glyphs. Click OK.

Now add a 375px wide static text field and position it at ( 95, 177 ). Use a font size of 40pt. I didn't bother with a border round this text field.

Enter "metres per second" into the text field and right-align the text.

Here's snapshot of this text field's settings.

Save your FLA.

Time to write some ActionScript to update the dynamic text field with your speed.


Step 6: Using the Geolocation Class

Let's start by creating the document class and adding some code to listen for information coming back from your handset's location sensor.

Create a new ActionScript 3 file by selecting File | New... (Ctrl + N) within Flash Professional. The New Document panel will appear.

Within the New Document panel, click on the General tab and select ActionScript 3.0 Class. Click OK.

Add the following to it:

package
{
	import flash.display.Sprite;
	import flash.events.GeolocationEvent;
	import flash.sensors.Geolocation;
	import flash.text.TextField;
	
	public class Application extends Sprite
	{		
		public  var metresPerSecond :TextField;
		private var geolocation     :Geolocation;
		
		public function Application()
		{				
			geolocation = new Geolocation();
			geolocation.setRequestedUpdateInterval( 1000 );
			geolocation.addEventListener( GeolocationEvent.UPDATE, handleGeolocationUpdate );
		}
		
		private function handleGeolocationUpdate( e :GeolocationEvent ) :void
		{
			metresPerSecond.text = String( Math.round( e.speed ) );
		}
	}
}

Save the class in the same location as your FLA and name it Application.as.

Left-click on the stage (don't click on any of the text fields) and set the Document Class field within the Properties panel to: Application.

Two member variables are declared within the class. Here they are:

public  var metresPerSecond :TextField;
private var geolocation     :Geolocation;

The first, metresPerSecond, is of type TextField and represents the dynamic text field created in step 5. The second, geolocation, will be used to reference an instance of the Geolocation class.

The metresPerSecond member variable has been declared as public since it represents a text field sitting on the stage. Attempting to give a member variable that represents a stage instance a non-public access modifier will result in a run-time error.

The Geolocation class is found within the flash.sensors package. We'll also be making use of the GeolocationEvent class, which has been imported along with Geolocation:

import flash.events.GeolocationEvent;
import flash.sensors.Geolocation;

Using the Geolocation class isn't that difficult. First an instance is created and assigned to your geolocation member variable:

geolocation = new Geolocation();

Then you state how often you'd like to receive update information from your handset. The update interval is measured in milliseconds - we've requested an update every second:

geolocation.setRequestedUpdateInterval( 1000 );

Finally, we hook-up an event handler that will be called every time update information is available. This is done by listening for the GeolocationEvent.UPDATE event from your geolocation instance:

geolocation.addEventListener( GeolocationEvent.UPDATE, handleGeolocationUpdate );

The event handler is where the user's current speed is obtained and output to our text field. Determining the speed is a simple case of extracting the value from the GeolocationEvent's speed property before converting it to a string:

private function handleGeolocationUpdate( e :GeolocationEvent ) :void
{
	metresPerSecond.text = String( Math.round( e.speed ) );
}

That's more-or-less it. You now have the code required to listen for GPS updates and output the speed that the user is traveling at.

One thing to note however is that the setRequestedUpdateInterval() method is only used as a hint to the device. The actual time between location updates may be greater or smaller than your requested interval. For the purposes of this tutorial however, we'll assume that the device will honor the suggested interval.

You can find more detail regarding the Geolocation class on Adobe LiveDocs.


Step 7: Application & Installer Settings

Okay, let's build and deploy your code to your handset.

Select File | AIR Android Settings... to bring-up the Application & Installer Settings panel.

Four tabs are available from this panel, with the General tab being the default.

Fill out the fields available from this tab ensuring that the fields match those shown in the screen below:

Click on the Deployment tab.

Set the Android deployment type to 'Device release' and ensure that both check boxes are selected within the After publishing section.

Next you'll need to create a certificate for your application. You can do this by simply clicking on the Create... button that is adjacent to the Certificate field.

The Create Self-Signed Digital Certificate panel will appear. For the first three fields simply enter your name. Select the appropriate code for your country of residence for the fourth field. For example if you live in the United Kingdom select GB; if you reside in the United States select US. You can find a comprehensive list of country codes here.

Within the 'Password' and 'Confirm password' fields enter a password for your certificate.

Finally, click the Browse button and select a folder on your hard drive where you'd like to store the certificate. The same folder as your FLA should do.

Click OK and a P12 certificate file will be generated and saved. You will be taken back to the Application & Installer Settings panel.

Click the Browse... button next to the Certificate field and select your P12 certificate file. Within the Password field enter the password you associated with your certificate and click the checkbox next to 'Remember password for this session'.

Click on the Permissions (we'll ignore Icons) tab.

When writing Android applications you need to explicitly state what permissions your application will require when in use. At the moment we need the application to access the phone's location. This requires the ACCESS_FINE_LOCATION and ACCESS_COARSE_LOCATION permissions to be set.

The ACCESS_FINE_LOCATION permission allows your app to access GPS data through the Geolocation class. If your phone's location sensor isn't available then the ACCESS_COARSE_LOCATION permission will allow your application to instead attempt to access WIFI and cellular network location data through the Geolocation class. This fallback option however isn't as accurate.

Click OK and save your FLA.


Step 8: Deployment on Device

We're now ready to publish the FLA and deploy it on your handset.

If you haven't already done so, connect your device to your PC using a USB cable.

We've instructed Flash CS5 to launch the application on the connected device immediately after publishing. You'll therefore need to ensure that your device's GPS sensor is enabled beforehand.

The exact settings for this may differ across Android devices but most users will be able to enable it via the homescreen:

Alternatively from the homescreen press the Menu key and then select Settings. Within Settings select Location & Security. From here select Use GPS Satellites to enable GPS.

Now go ahead and publish the FLA by selecting File | Publish (Alt + Shift + F12).

If all goes according to plan, the FLA will publish, an Android APK will be generated and the file will be transferred to your device and launched.

The app should initially read '0 metres per second'. Take your phone outside and start walking or better still start running. You should see the screen update every second with your current speed.

If you don't have a device to test on then don't worry, we'll deal with that in Step 12 - Simulating Geolocation.

For the time being though simply ensure that your code builds by selecting Control | Test Movie | in Flash Professional (Ctrl + Enter). If successful the SWF should run within an AIR window, although for obvious reasons the text field won't update with a speed.

If you're following this tutorial without an Android device then please still add all the code outlined in future steps. The code will still build on your PC even if your desktop doesn't actually provide the same functionality as the Android device.


Step 9: Backlight

You probably noticed while using the app that the backlight still dims according to your device's Screen Timeout settings. This can be extremely inconvenient and even dangerous if, for example, you're constantly having to touch the device to wake the screen when using your GPS app while driving.

Thankfully AIR provides a mechanism for keeping the device's screen awake via the NativeApplication class's systemIdleMode property.

First import the NativeApplication and SystemIdleMode classes into your document class by adding the following lines:

import flash.desktop.NativeApplication;
import flash.desktop.SystemIdleMode;
import flash.display.Sprite;
import flash.events.GeolocationEvent;
import flash.sensors.Geolocation;
import flash.text.TextField;

Now let's add the actual line of code that forces the screen to stay awake while your app is in use:

public function Application()
{
	NativeApplication.nativeApplication.systemIdleMode = SystemIdleMode.KEEP_AWAKE;
	
	geolocation = new Geolocation();
	geolocation.setRequestedUpdateInterval( 1000 );
	geolocation.addEventListener( GeolocationEvent.UPDATE, handleGeolocationUpdate );
}

You will also need to grant your Android handset some additional permissions in order to prevent it from going to sleep.

Select File | AIR Android Settings.... Click on the Permissions tab and check the WAKE_LOCK and DISABLE_KEYGUARD permissions:

Click OK to close the Applications & Installer Settings panel. Save your FLA.

Publish and deploy your app to your handset again (Alt + Shift + F12). The existing version of the app running on your handset will be automatically closed and the new version will be launched.

This time it should run without the backlight going to sleep.

If you've got access to a car then why not take your app for a spin - although please be careful and make sure you adhere to any local laws and regulations.

You can find more information regarding the NativeApplication and SystemIdleMode on Adobe LiveDocs.


Step 10: Application Management

Pressing the Back or Home key on your handset will take you out of your app but it won't actually quit the app. Instead, Android forces apps into the background rather than stopping them completely.

When an AIR app moves to the background it receives an Event.DEACTIVATE event and also has its frame rate reduced to 4fps. However, for the most part, it's up to you to write code to gracefully move your app into a sleep mode that won't unnecessarily drain resources.

This is highlighted by the Geolocation class, which will continue to dispatch GeolocationEvent.UPDATE events after the app has been moved to the background.

It should also be noted that your app will also be moved to the background if your handset receives an incoming call. Again, an Event.DEACTIVATE event will be dispatched, giving you an opportunity to take care of any housekeeping.

The next time you use the app (typically by selecting it from the menu), it will be moved to the foreground again and will behave as if it had been running in the foreground all along. When an AIR app is moved to the foreground it receives an Event.ACTIVATE event and the frame rate is adjusted back to its original value.

Let's verify that your app has been moved to the background when you exit from it.

Publish and deploy the app to your phone (Alt + Shift + F12).

Once the app launches, press the Back key, which should take you to your homescreen. From the homescreen press the Menu key and then select Settings. Within Settings select Applications.

From Applications Settings select Manage Applications and then press the Running tab. You will be shown a list of apps that are currently running on your device. Scroll down until you find the Speedometer app. Selecting the app's icon will take you to the Application Info screen where you can force it to stop completely. This will kill the app, freeing memory and helping to save battery.

Go ahead and force the app to stop.


Step 11: Exiting the App

Depending on the type of app you are writing, you may not have any real need for it to run in the background when the user exits.

You can actually use ActionScript to force your AIR app to close (without it being sent to the background) by making the following call on the NativeApplication object:

NativeApplication.nativeApplication.exit();

Let's actually do this with the speedometer app by intercepting the Back and Home key presses and overriding their default behavior. Let's write some code to handle the Back key.

First add the following two imports to Application.as:

import flash.desktop.NativeApplication;
import flash.desktop.SystemIdleMode;
import flash.display.Sprite;
import flash.events.GeolocationEvent;
import flash.events.KeyboardEvent;
import flash.sensors.Geolocation;
import flash.text.TextField;
import flash.ui.Keyboard;

Now within the constructor add an event listener to capture key presses:

public function Application()
{
	NativeApplication.nativeApplication.systemIdleMode = SystemIdleMode.KEEP_AWAKE;
	NativeApplication.nativeApplication.addEventListener( KeyboardEvent.KEY_DOWN, handleKeyDown );
			
	geolocation = new Geolocation();
	geolocation.setRequestedUpdateInterval( 1000 );
	geolocation.addEventListener( GeolocationEvent.UPDATE, handleGeolocationUpdate );
}

When capturing key presses in the Flash Player we typically add an event listener to the stage object:

stage.addEventListener( KeyboardEvent.KEY_DOWN, handleKeyDown );

However when writing for AIR for Android you should add the event listener to the NativeApplication object:

NativeApplication.nativeApplication.addEventListener( KeyboardEvent.KEY_DOWN, handleKeyDown );

Add the handleKeyDown() method that gets called when KeyboardEvent.KEY_DOWN is captured:

private function handleKeyDown( e :KeyboardEvent ) :void
{
	switch( e.keyCode )
	{
		case Keyboard.BACK:
			NativeApplication.nativeApplication.exit();	
			break;
		
		case Keyboard.SEARCH:
		case Keyboard.MENU:
			e.preventDefault();
			break;				
	}
}

This method determines the key being pressed by examining the keyCode property stored within the KeyboardEvent that was passed to handleKeyDown().

The key codes themselves have been masked by the constants provided by the Keyboard class. The Back key's code is represented by Keyboard.BACK.

You should be able to see from the code that if Back is pressed then a call is made to force the app to quit rather than being moved to the background:

case Keyboard.BACK:
	NativeApplication.nativeApplication.exit();	
	break;

The handleKeyDown() method also looks for the user pressing the Search or Menu keys and prevents their default behaviour by making a call to the event's preventDefault() method:

case Keyboard.SEARCH:
case Keyboard.MENU:
	e.preventDefault();
	break;

This will block the Android virtual keyboard from appearing if the user holds down the Menu key, and will block the Google Voice app when the user holds the Search key. Neither of which we require for the speedometer app.

We haven't addressed the Home key yet. Unlike Back, Search and Menu, you can't actually intercept the Home key via the KeyboardEvent.KEY_DOWN event. Instead when the Home key is pressed an Event.DEACTIVATE event will be dispatched, which you can listen for. This event is dispatched just before an app is moved into the background. Instead you can listen for the event and force the app to quit.

Add code to listen for the Event.DEACTIVATE event:

NativeApplication.nativeApplication.addEventListener( KeyboardEvent.KEY_DOWN, handleKeyDown );
NativeApplication.nativeApplication.addEventListener( Event.DEACTIVATE, handleDeactivate );

You'll also need to import the Event class:

import flash.display.Sprite;
import flash.events.Event;
import flash.events.GeolocationEvent;

Now add the method that gets called when the event is caught:

private function handleDeactivate( e :Event ) :void
{
	NativeApplication.nativeApplication.exit();
}

Save Application.as then save your FLA.

Okay, we should now have an app that quits when the user presses the Back or Home keys rather than switching to the background and eating up battery.

Publish the app (Alt + Shift + F12) and when it launches on your device press the Back or Home key to quit. Now move to the Manage Applications screen (via Settings) on your Android device. Select the Running tab and confirm that Speedometer isn't present in the list.


Step 12: Simulating Geolocation

As you can imagine, testing a GPS-enabled app can become quite exhausting since you need to keep on the move if you want to receive useful data from the Geolocation class. For those running the app within Flash CS5, testing with actual location data has so far been impossible.

Let's try to address both these issues by writing code that simulates the Geolocation class using pre-recorded GPS data. This step will give you an opportunity to use some of the filesystem classes provided by AIR.

You will find a binary file at source/gps.dat that contains a set of speeds measured at one second intervals from my Google Nexus One while driving my car around. We'll write a class that possesses the same public API as the Geolocation class but reads the values from gps.dat rather than reading data from your phone's location sensor.


Step 13: Copying the GPS Data

First let's copy gps.dat to the user folder on your desktop and also to your phone in order to test the app on your device without having to rely on its actual GPS unit.

For desktop, here's where to copy the gps.dat file to:

  • Windows Vista and Windows 7: C:\Users\your_username\
  • Windows XP: C:\Documents and Settings\your_username\
  • Mac OS X: Macintosh HD/Users/your_username/

For your Android phone, copy the gps.dat file to the root of your phone's SD card. To do this connect your device to your desktop via its USB cable. Open the Notification Panel on your device by touching the Status Bar and dragging down.

Inside the Notification Panel you should see an Ongoing section. Tap the USB connected icon within this section to move to the USB Mass Storage screen. From this screen press the 'Turn on USB storage' button.

You can now explore the device's SD card from your desktop. On Windows you will be given the option to view the files on the SD card using Windows Explorer. From the selection panel that appears simply select "Open folder to view files using Windows Explorer".

On Mac OS X, an icon representing your device will appear on the desktop. Simply double-click the icon to open a Finder window. Alternatively open a Finder window from the Dock and select the device from the DEVICES list on the left-hand side of the Finder window.

Now copy the gps.dat file to the root folder on the SD card.

Once the copy is complete, turn off USB storage by pressing the 'Turn off USB storage' button on your device's USB Mass Storage screen.

You're now ready to write some code to read this data.


Step 14: Simulating the Geolocation Class

Create a new ActionScript class and add the following code to it:

package
{
	import flash.events.EventDispatcher;

	public class GeolocationSimulate
		extends EventDispatcher
	{	
		public function GeolocationSimulate()
		{
			/** @todo: Open gps.dat file for reading and set a timer to continually read from it. */
		}
		
		public function setRequestedUpdateInterval( interval :Number ) :void
		{
			/** @todo: Set the interval used to read each speed. */
		}
	}
}

Save the class in the same location as your FLA and name it GeolocationSimulate.as.

The class is very incomplete at the moment. Notice the @todo comments in places. This is to indicate pieces of functionality that we have still to add. Before we do that however, an explanation of what we currently have is needed.

Essentially we are writing a class that adheres to the same public interface as the AIR SDK's Geolocation class. If you look at the Live Docs for Geolocation you'll notice that the API has a comprehensive list of public methods.

For the purposes of this tutorial however, we only actually take advantage of a small subset of Geolocation's public API, therefore we only need to ensure our GeolocationSimulate class provides functionality for the methods from Geolocation that we are actually using.

Here's a list of those methods:

  • setRequestedUpdateInterval()
  • addEventListener()

Looking at our current implementation of GeolocationSimulate you may think that we've only included one of those methods - it may look as if addEventListener() is missing from the class. However if you look a little closer you'll see that GeolocationSimulate extends the Flash API's EventDispatcher class.

The EventDispatcher class provides GeolocationSimulate with an addEventListener() method and also provides functionality to allow our class to eventually dispatch events - we'll need this to allow our GeolocationSimulate class to dispatch the GeolocationEvent.UPDATE event that we listen for from Application.as. Here's the code from Application.as again as a reminder:

geolocation = new Geolocation();
geolocation.setRequestedUpdateInterval( 1000 );
geolocation.addEventListener( GeolocationEvent.UPDATE, handleGeolocationUpdate );

Essentially we want to plug our new GeolocationSimulate class into the code above and have it work as if it were the real Geolocation class. The only difference should be that GeolocationSimulate should dispatch a GeolocationEvent.UPDATE event that contains fake data plucked periodically from the gps.dat file sitting on your phone or desktop.

Although it's far from finished let's hook our new class up to Application.as.

First change the following code to now instantiate GeolocationSimulate rather then Geolocation:

public  var metresPerSecond :TextField;
private var geolocation     :GeolocationSimulate;
		
public function Application()
{
	NativeApplication.nativeApplication.systemIdleMode = SystemIdleMode.KEEP_AWAKE;
	NativeApplication.nativeApplication.addEventListener( KeyboardEvent.KEY_DOWN, handleKeyDown );
	NativeApplication.nativeApplication.addEventListener( Event.DEACTIVATE, handleDeactivate );
			
	geolocation = new GeolocationSimulate();
	geolocation.setRequestedUpdateInterval( 1000 );
	geolocation.addEventListener( GeolocationEvent.UPDATE, handleGeolocationUpdate );
}

Publish and test your code within Flash CS5 for the time being by selecting Control | Test Movie | in Flash Professional (Ctrl + Enter). It won't do anything yet but it should at least publish.


Step 15: Update Interval

Time to start adding some functionality to the stub methods within the GeolocationSimulate class. Let's focus on writing the code for setRequestedUpdateInterval().

Essentially we want the class to periodically read a speed value from gps.dat and dispatch a GeolocationEvent.UPDATE event containing that value to any listeners.

We can do that by adding a Timer instance and setting its delay property within setRequestedUpdateInterval(). Make the following changes to your code:

package
{
	import flash.events.EventDispatcher;
	import flash.events.TimerEvent;
	import flash.utils.Timer;

	public class GeolocationSimulate
		extends EventDispatcher
	{	
		static private const DEFAULT_DELAY :Number = 1000;
		
		private var timer :Timer;
		
		public function GeolocationSimulate()
		{
			/** @todo: Open gps.dat file for reading. */
			timer = new Timer( DEFAULT_DELAY );
			timer.addEventListener( TimerEvent.TIMER, handleTimer );
			timer.start();
		}
		
		public function setRequestedUpdateInterval( interval :Number ) :void
		{
			timer.delay = interval;
		}
		
		private function handleTimer( e :TimerEvent ) :void
		{
			/** @todo: Read the next speed from gps.dat. */
		}
	}
}

Inside the constructor we have created an instance of the Timer class and assigned it to a member variable named timer. When instantiating the Timer object we have instructed it to continuously repeat at one second (1000 milliseconds) intervals. An event listener named handleTimer() has also been added to the timer and will be called on each repeat.

For the time being the handleTimer() method will do nothing. Eventually it will read the next speed value from gps.dat.

Save GeolocationSimualte.as.


Step 16: The Android Debug Bridge

Let's test the current version of GeolocationSimulate by proving that the timer code is working. We can do this by adding a simple trace statement to the handleTimer() method and looking for it in Flash's Output window.

Add the following trace statement:

private function handleTimer( e :TimerEvent ) :void
{
	/** @todo: Read the next speed from gps.dat. */
	trace( "GeolocationSimulate::handleTimer()" );
}

Publish and test your code within Flash CS5 by selecting Control | Test Movie | in Flash Professional (Ctrl + Enter). If all goes according to plan you should see the following being traced to the Output window every second:

GeolocationSimulate::handleTimer()

Let's test the code on your Android device now.

To aid us we'll use the Android Debug Bridge (abd) tool, which can be used to filter and view logging information from various applications on your device, including trace statements coming from your AIR apps.

Finding adb...

Adb is a command line tool and can be found alongside your Flash CS5 installation. Here's where to find adb on either Windows or Mac OS X:

  • Windows Vista and Windows 7: C:\Program Files (x86)\Adobe\Adobe Flash CS5\Android\tools\adb.exe
  • Windows XP: C:\Program Files\Adobe\Adobe Flash CS5\Android\tools\adb.exe
  • Mac OS X: /Applications/Adobe Flash CS5/Android/tools/adb

Move to the sub-section below that's relevant to your OS then move onto the 'Running adb' subsection a little further down.

...on Windows 7 and Windows Vista

If you're running Windows 7 or Vista, open Windows Explorer and navigate to the folder: C:\Program Files (x86)\Adobe\Adobe Flash CS5\Android. Now hold Shift and right-click on the tools folder. From the drop-down menu select 'Open command window here' to open a command window.

...on Windows XP

For those using Windows XP, click the start button and select Run... to launch the Run window:

Within the Run window, type 'cmd':

Click OK to open a command window. Now move to the folder where adb is kept by entering the following:

cd C:\Program Files\Adobe\Adobe Flash CS5\Android\tools

...on Mac OS X

Max OS X users will have to open a terminal window. To do this open the Applications stack and find the Utilities folder:

Click on the Utilities folder and find the Terminal icon:

Click on the Terminal icon to launch a terminal window.

From the terminal window move to the folder where adb is kept by entering the following:

cd '/Applications/Adobe Flash CS5/Android/tools'

Running adb

From the command window we can launch adb and issue the logcat command to it in order to start receiving logging information coming from your Android device. We are only interested in receiving trace information coming from the AIR app so a filter will need to be applied.

Ensure your Android device is connected via the USB port.

If your using Windows enter the following into the command line in order to receive log information from your AIR app:

adb logcat air.speedometer:I *:S

Mac OS X users should enter the following into the terminal window:

./adb logcat air.speedometer:I *:S

Adb is now listening for and outputting any log information coming from your device related to your app. Now we need to actually make a debug build of the app and deploy it on the phone.

Within Flash CS5 select File | AIR Android Settings.... Within the Application & Installer Settings panel, click on the Deployment tab and click on the Debug radio button.

Click OK to close the panel.

Save the FLA.

Now publish the app by selecting File | Publish (Alt + Shift + F12).

The following dialog box may appear on your device when your app is launched:

If it does then simply press Cancel to dismiss the dialog and to resume the execution of your app.

The command window that adb is running from should now be receiving trace information from your GeolocationSimulate class. The output in the command window should look something like this:

I/air.Speedometer( 1821): GeolocationSimulate::handleTimer()

There are many ways to debug AIR for Android apps with adb being one option, and probably the easiest. You can find comprehensive documentation for adb on the official Android Developer Site.

Alternatively you may wish to consider using the Dalvik Debug Monitor or running a remote debug session directly from Flash Pro. Due to the limited scope of this tutorial however, we'll stick with the use of adb via a command window.

Quit the app to prevent any more trace statements appearing in your command window.

Okay we can continue adding functionality to GeolocationSimulate.


Step 17: Reading Data from gps.dat

Now that we are confident the timer code is working, we need to add some functionality that actually reads the fake GPS data from gps.dat. But first let me explain the format of the data stored within gps.dat.

Essentially gps.dat is a binary file that consists of a collection of floating point numbers. Floating point numbers are represented by the Number type in Flash and each of these floating point numbers represents a speed value measured in metres per second.

The Adobe AIR SDK provides a FileStream class that can be used to open, read from, and write to binary files. For our GeolocationSimulate class we will be required to open and read data from the file - writing isn't required.

Start by declaring a private member variable named filestream within the GeolocationSimulate class We'll use this variable to hold a reference to a FileStream object:

private var filestream :FileStream;
private var timer      :Timer;

Let's also define a constant that will hold the name of the binary file that we will be reading from:

static private const DEFAULT_DELAY :Number = 1000;
static private const FILE          :String = "gps.dat";

The next step is to create an instance of the FileStream object within the class's constructor and to open the binary file for reading. Here's the code:

public function GeolocationSimulate()
{
	timer = new Timer( DEFAULT_DELAY );
	timer.addEventListener( TimerEvent.TIMER, handleTimer );
	timer.start();

	var file :File = File.userDirectory;
	file = file.resolvePath( FILE );
	filestream = new FileStream();
	filestream.open( file, FileMode.READ );
}

First a File object is created that points to the location of gps.dat on both your computer and your Android device.

The File class has properties that have meaningful values on different operating systems. For example, Windows, Mac OS X and Android all have different native paths to the user's directory. However, we can easily access the user folder in an operating system independent manner by using File.userDirectory as shown below:

var file :File = File.userDirectory;

Once we have a handle to the user folder, we simply use the File class's resolvePath() method to create the path to the gps.dat file:

file = file.resolvePath( FILE );

The two lines of code outlined above effectively define a path to gps.dat on Windows, Mac and Android. Note the use of the FILE constant, which holds the name of the file.

Now that we have the path to the gps.dat file we can create a FileStream object and open the file. Here are the two lines of code that are responsible for doing that:

filestream = new FileStream();
filestream.open( file, FileMode.READ );

When opening a file you need to explicitly state whether you are opening the file in order to read from it, write to it, or both. In our case we simply want to read from the file, which is done by passing FileMode.READ as an argument of the open() method.

You can see a comprehensive list of constants available from FileMode on Adobe LiveDocs.

All that's left to do now is to actually read the next speed value from the file whenever the timer updates. Add the following code to the handleTimer() method:

private function handleTimer( e :TimerEvent ) :void
{
	if( filestream != null )
	{
		var speed :Number;

		if( filestream.bytesAvailable )
		{
			speed = filestream.readFloat();
		}
		else
		{
			filestream.position = 0;
			speed = filestream.readFloat();
		}
		
		/** @todo: Dispatch GeolocationEvent.EVENT. */
	}
}

The code above reads the next available speed from the file and stores it in a local variable named speed.

Before the speed can be read from the file a check is performed to see if we have reached the end of the file or not. This is done by reading the filestream object's bytesAvailable property. If the end of the file has been reached then bytesAvailable will be 0 indicating that there's no more data to be read. If this is the case then we simply start reading data from the start of the file again by setting the filestream object's position property to 0. Here's the snippet of code responsible for doing that:

if( filestream.bytesAvailable )
{
     speed = filestream.readFloat();
}
else
{
     filestream.position = 0;
     speed = filestream.readFloat();
}

The speed value itself is read from the file by making a call to the readFloat() method. This reads the next available floating point value from the file.

Before testing your latest version of the GeolocationSimulate class you'll need to add some imports to let Flash know where to find the various filestream classes that have been used:

import flash.events.EventDispatcher;
import flash.events.TimerEvent;
import flash.filesystem.File;
import flash.filesystem.FileMode;
import flash.filesystem.FileStream;
import flash.utils.Timer;

Now let's test the latest version of the class by adding a trace statement near the end of the handleTimer() method:

/** @todo: Dispatch GeolocationEvent.EVENT. */
trace( "GeolocationSimulate::handleTimer() speed: " + speed );

This will trace the latest speed value that has been read from the gps.dat file.

Now save the class and test the latest version of the app both within Flash CS5 and on the device. Although the "metres per second" text field won't yet update (we'll get to that in the next step) you should see a trace statement every second with the latest speed value obtained from gps.dat.


Step 18: Dispatching GeolocationEvent.UPDATE

When we were using the Geolocation class in Steps 4-11 you may remember that it dispatched a GeolocationEvent.UPDATE event every time your device's location sensor had new GPS data. From this event we were able to extract the speed and display it within a text field.

We were listening for this event and updating the text field from within the Application class.

As a reminder, here is the code from the Application class's constructor that instantiated the Geolocation class and set up the event listener:

geolocation = new GeolocationSimulate();
geolocation.setRequestedUpdateInterval( 1000 );
geolocation.addEventListener( GeolocationEvent.UPDATE, handleGeolocationUpdate );

And here's the actual method that gets called whenever the Geolocation object dispatches a GeolocationEvent.UPDATE event:

private function handleGeolocationUpdate( e :GeolocationEvent ) :void
{
	metresPerSecond.text = String( Math.round( e.speed ) );
}

For our GeolocationSimulate class to fully simulate Geolocation it too must dispatch a GeolocationEvent.UPDATE event. If it doesn't then our Application class will have no way of knowing that new GPS data is available and therefore won't be able to display the current speed within its text field.

Thankfully dispatching an event from GeolocationSimulate is trivial. If you look back at Step 14 you'll remember that the class extends Flash's EventDispatcher class. This provides GeolocationSimulate with the means to dispatch events via the dispatchEvent() method.

Remove the trace statement you added in the previous step and add the following code near the end of the handleTimer() within GeolocationSimulate.as:

private function handleTimer( e :TimerEvent ) :void
{
	if( filestream != null )
	{
		var speed :Number;

		if( filestream.bytesAvailable )
		{
			speed = filestream.readFloat();
		}
		else
		{
			filestream.position = 0;
			speed = filestream.readFloat();
		}

		dispatchEvent(
			new GeolocationEvent(
				GeolocationEvent.UPDATE,
				false,
				false,
				0,
				0,
				0,
				0,
				0,
				speed,
				0,
				0
			)
		);
	}
}

The dispatchEvent() method expects one argument - an object of type Event. All events within Flash extend from the Event class. The particular event we are interested in is GeolocationEvent, which is instantiated and passed into dispatchEvent() above.

The GeolocationEvent class's constructor expects eleven arguments, two of which we're interested in - the first and ninth parameters.

The first parameter is the type of the event, which in this case is to be of type GeolocationEvent.UPDATE. The ninth parameter is the current speed in metres per second. For this we pass our local variable named speed.

The remaining parameters we fill with the default values detailed in the LiveDocs.

Add an import statement letting Flash know where to find the GeolocationEvent class:

import flash.events.EventDispatcher;
import flash.events.GeolocationEvent;
import flash.events.TimerEvent;

Whenever a new speed value is available, GeolocationSimulate is now able to dispatch an event to any listeners. Save the class file.

Here's the current version of the class for your reference:

package
{
	import flash.events.EventDispatcher;
	import flash.events.GeolocationEvent;
	import flash.events.TimerEvent;
	import flash.filesystem.File;
	import flash.filesystem.FileMode;
	import flash.filesystem.FileStream;
	import flash.utils.Timer;

	public class GeolocationSimulate
		extends EventDispatcher
	{	
		static private const DEFAULT_DELAY :Number = 1000;
		static private const FILE          :String = "gps.dat";
		
		private var filestream :FileStream;
		private var timer      :Timer;
		
		public function GeolocationSimulate()
		{
			timer = new Timer( DEFAULT_DELAY );
			timer.addEventListener( TimerEvent.TIMER, handleTimer );
			timer.start();
			
			var file :File = File.userDirectory;
			file = file.resolvePath( FILE );			
			filestream = new FileStream();
			filestream.open( file, FileMode.READ );
		}
		
		public function setRequestedUpdateInterval( interval :Number ) :void
		{
			timer.delay = interval;
		}
		
		private function handleTimer( e :TimerEvent ) :void
		{
			if( filestream != null )
			{
				var speed :Number;

				if( filestream.bytesAvailable )
				{
					speed = filestream.readFloat();
				}
				else
				{
					filestream.position = 0;
					speed = filestream.readFloat();
				}
			
				dispatchEvent(
					new GeolocationEvent(
						GeolocationEvent.UPDATE,
						false,
						false,
						0,
						0,
						0,
						0,
						0,
						speed,
						0,
						0
					)
				);
			}
		}
	}
}

Now test the latest version of the app both within Flash CS5 and on the device. Within Flash CS5 and on your Android device you will now see the "metres per second" text field update with the latest speeds using the values stored within gps.dat.

This is ideal, as you can now test from within Flash CS5, and on your device without having to actually go anywhere (or even switch on your phone's GPS unit).


Step 19: GPS Errors

When running the latest version of the app you may notice some peculiar speeds being read from gps.dat. Here are the values for the first fifteen seconds of travel:

2, 128, 5, 6, 128, 7, 128, 7, 128, 8, 6, 5, 128, 7, 9

It should be clear that there are odd spikes in the data. For example, the second value listed above would suggest that my car accelerated from 2 metres per second to 128 metres per second. The data is claiming that in the space of a second my car's speed increased by approximately 286 miles per hour. I wasn't driving a rocket car so what happened?

Put simply, GPS receivers within devices aren't always accurate. They need an unobstructed line of sight to four or more GPS satellites in order to provide accurate data. Moving through streets with tall buildings or under tree coverage can impair the accuracy of the readings. This is exactly what has happened when I was recording the original data.

I haven't removed these erroneous values for a very good reason. When writing apps that take advantage of GPS you should never expect the data to be perfect and should instead write code to handle such errors.

We won't worry about the errors in the data just yet, but we will address the issue in part two of this tutorial.


Step 20: Switching between Geolocation classes

So we have a fully functional simulation of Geolocation but how do we swap between the simulation and real version of the class?

I suppose the most obvious way would be to simply swap the class type being used within the Application class.

For example, to change from GeolocationSimulate back to Geolocation you would change this line:

private var geolocation :GeolocationSimulate;

to

private var geolocation :Geolocation;

and this line:

geolocation = new GeolocationSimulate();

to

geolocation = new Geolocation();

As you can no doubt appreciate this could quickly become tedious and be prone to error as you continuously swap between the two during testing.

Another, and possibly more sensible, approach would be to use two member variables within the Application class - one that holds a reference to Geolocation and another that points to GeolocationSimulate. You could also declare a constant that dictates which version you currently want to use. Here's an example:

package
{
	//snip
	
	public class Application extends Sprite
	{		
		static private const REAL_GEOLOCATION :Boolean = true;
		
		public  var metresPerSecond     :TextField;
		private var geolocation         :Geolocation;
		private var geolocationSimulate :GeolocationSimulate;
		
		public function Application()
		{
			NativeApplication.nativeApplication.systemIdleMode = SystemIdleMode.KEEP_AWAKE;
			NativeApplication.nativeApplication.addEventListener( KeyboardEvent.KEY_DOWN, handleKeyDown );
			NativeApplication.nativeApplication.addEventListener( Event.DEACTIVATE, handleDeactivate );
			
			if( REAL_GEOLOCATION )
			{
				geolocation = new Geolocation();
				geolocation.setRequestedUpdateInterval( 1000 );
				geolocation.addEventListener( GeolocationEvent.UPDATE, handleGeolocationUpdate );
			}
			else
			{
				geolocationSimulate = new GeolocationSimulate();
				geolocationSimulate.setRequestedUpdateInterval( 1000 );
				geolocationSimulate.addEventListener( GeolocationEvent.UPDATE, handleGeolocationUpdate );
			}
		}

		//snip
}

By changing the value of the REAL_GEOLOCATION constant from true to false before publishing, you can easily swap between using AIR's Geolocation class and your own GeolocationSimulate class.

To be honest though, it's still not an entirely satisfactory solution.

Considering both Geolocation and GeolocationSimulate provide the same public methods, the need for two separate member variables is far from ideal. It would be nice if we could somehow have a single member variable that could be made to point at either an instance of Geolocation or GeolocationSimulate.

Thankfully this can be achieved through the use of interfaces.

For those unfamiliar with interfaces, think of an interface as a datatype in abstract terms. Whereas a class declares a datatype and provides the implementation for it, an interface does not provide its own implementation. Instead the implementation is provided by any classes that choose to adopt that interface.

If you're struggling with the concept, think of an interface as a contractual obligation that a class agrees to keep. In other words, when a class adopts an interface it must provide an implementation for the public methods outlined within the interface.

Essentially what we are trying to do is identify a common public API that both Geolocation and GeolocationSimulate can agree upon. In other words, we are looking for the public methods across both that are used by the Application class.

Here they are:

  • public function setRequestedUpdateInterval( interval :Number ) :void
  • public function addEventListener( type :String, listener :Function, useCapture :Boolean = false, priority :int = 0, useWeakReference :Boolean = false ):void

Now that we know the public methods that our two classes must provide (and already do) let's create an interface for them.

Create a new ActionScript 3 file and add the following code to it:

package 
{
	import flash.events.IEventDispatcher;
	
	public interface IGeolocation extends IEventDispatcher
	{
		function setRequestedUpdateInterval( interval :Number ) :void;
	}
}

Save the file as IGeolocation.as. It is convention to prefix interface names with an uppercase 'I'.

You may already have noticed that addEventListener() is not explicitly listed within the interface. Don't worry I haven't forgotten about it. Instead I've opted to extend the IEventDispatcher interface, which is provided by the Flash SDK and includes addEventListener(). This will actually force all public methods listed within IEventDispatcher to be part of IGeolocation's interface. This however isn't a problem since both Geolocation and GeolocationSimulate extend the EventDispatcher class and therefore already contain implementations for all these methods.

It's also worth noting that the public keyword was not explicitly used for the declaration of setRequestedUpdatInterval() within the interface. Only public methods can be declared within an interface, therefore the public keyword is not necessary, although you may use it if you so wish.

Now that you have your interface, you'll need to make both Geolocation and GeolocationSimulate adopt it. Let's start with GeolocationSimulate.

Add the following line of code:

public class GeolocationSimulate
	extends EventDispatcher
	implements IGeolocation
{

That's it! Only one line of code is required. We've used the implements keyword to force the class to implement the methods listed within the IGeolocation interface.

If for any reason, implementations for any of these methods are not found within GeolocationSimulate then you'll receive a compile-time error when publishing your app. By agreeing to implement the IGeolocation interface your class is committed to providing implementations for the methods declared within IGeolocation.

Now there's a slight problem with the Geolocation class. It has been provided by the AIR SDK and there is no way to directly alter that class's definition. Therefore we can't simply load the Geolocation class into a text editor and force it to implement IGeolocation.

Instead what we are going to do is create a new class that extends Geolocation and force this new class to implement IGeolocation. This isn't as bad as it sounds and will only require a few lines of code.

Create a new class and add the following to it:

package 
{
	import flash.sensors.Geolocation;
	
	public class GeolocationReal
		extends Geolocation
		implements IGeolocation
	{
	}
}

Save the class as GeolocationReal.as.

This new class has all the functionality of Geolocation but also implements IGeolocation. From now on we'll use GeolocationReal in place of Geolocation within our code.

We're almost finished and things should start to become clear as to why we're using interfaces.

Although an interface does not have its own implementation it can be used as a datatype and this is where the power of interfaces will become apparent. Within the Application class we can create a single member variable that can be used to reference both GeolocationReal and GeolocationSimulate. This member variable will be of type IGeolocation.

First, remove the import statement for flash.sensors.Geolocation from Application.as since we'll now be using GeolocationReal in its place:

import flash.events.KeyboardEvent;
import flash.sensors.Geolocation;
import flash.text.TextField;

Now make the following changes to the class and save it:

package
{
	import flash.desktop.NativeApplication;
	import flash.desktop.SystemIdleMode;
	import flash.display.Sprite;
	import flash.events.Event;
	import flash.events.GeolocationEvent;
	import flash.events.KeyboardEvent;
	import flash.text.TextField;
	import flash.ui.Keyboard;
	
	public class Application extends Sprite
	{
		static private const GEOLOCATION_REAL :Boolean = false;
		
		public  var metresPerSecond :TextField;
		private var geolocation     :IGeolocation;
		
		public function Application()
		{
			NativeApplication.nativeApplication.systemIdleMode = SystemIdleMode.KEEP_AWAKE;
			NativeApplication.nativeApplication.addEventListener( KeyboardEvent.KEY_DOWN, handleKeyDown );
			NativeApplication.nativeApplication.addEventListener( Event.DEACTIVATE, handleDeactivate );
			
			if( GEOLOCATION_REAL )
			{
				geolocation = new GeolocationReal();
				geolocation.setRequestedUpdateInterval( 1000 );
				geolocation.addEventListener( GeolocationEvent.UPDATE, handleGeolocationUpdate );
			}
			else
			{
				geolocation = new GeolocationSimulate();
				geolocation.setRequestedUpdateInterval( 1000 );
				geolocation.addEventListener( GeolocationEvent.UPDATE, handleGeolocationUpdate );
			}
		}
		
		private function handleGeolocationUpdate( e :GeolocationEvent ) :void
		{
			metresPerSecond.text = String( Math.round( e.speed ) );
		}
		
		private function handleKeyDown( e :KeyboardEvent ) :void
		{
			switch( e.keyCode )
			{
				case Keyboard.BACK:
					NativeApplication.nativeApplication.exit();
					break;
				
				case Keyboard.SEARCH:
				case Keyboard.MENU:
					e.preventDefault();
					break;
			}
		}

		private function handleDeactivate( e :Event ) :void
		{
			NativeApplication.nativeApplication.exit();
		}
	}
}

There's one more thing we should do, but first test the current version of the FLA on both your desktop and on your Android phone. Set the GEOLOCATION_REAL constant to true if you'd like to test using your phone's GPS sensor (remember to activate GPS on your device before launching the app) otherwise set it to false to use your simulation class.

Thanks to the use of the IGeolocation interface, the geolocation member variable is able to represent an instance of either the GeolocationReal or GeolocationSimulate class. This will hold true for as long as you only make calls upon the geolocation member variables that belong to IGeolocation. Any attempt to call methods that belong to GeolocationReal or GeolocationSimulate but not IGeolocation will result in a compiler error when publishing.


Step 21: Conditional Compilation

I mentioned in the previous step that there was one last thing I'd like you to do.

Considering that only GeolocationReal or GeolocationSimulate can be used at any one time, it seems somewhat wasteful that both are actually compiled into the final published SWF. It would be ideal if GeolocationSimulate wasn't actually included in the output SWF if we intended to use GeolocationReal and vice versa.

Since the introduction of Flash CS4, conditional compilation has been available to Flash developers. Conditional compilation allows the use of configuration constants to dictate what code is actually included within the published SWF. In other words, conditional compilation allows you to turn blocks of code throughout a project on and off.

Since we will be using conditional compilation we can remove the GEOLOCATION_REAL constant that you've been using to decide whether to instantiate GeolocationReal or GeolocationSimulate. The constants that are used for conditional compilation are set from the Publish Settings panel rather than within ActionScript.

Let's remove the lines of code from the Application class that will no longer be required. You can see them below in bold:

static private const GEOLOCATION_REAL :Boolean = false;

public  var metresPerSecond :TextField;
private var geolocation     :IGeolocation;

public function Application()
{
	NativeApplication.nativeApplication.systemIdleMode = SystemIdleMode.KEEP_AWAKE;
	NativeApplication.nativeApplication.addEventListener( KeyboardEvent.KEY_DOWN, handleKeyDown );
	NativeApplication.nativeApplication.addEventListener( Event.DEACTIVATE, handleDeactivate );
	
	if( GEOLOCATION_REAL )
	{
		geolocation = new GeolocationReal();
		geolocation.setRequestedUpdateInterval( 1000 );
		geolocation.addEventListener( GeolocationEvent.UPDATE, handleGeolocationUpdate );
	}
	else
	{
		geolocation = new GeolocationSimulate();
		geolocation.setRequestedUpdateInterval( 1000 );
		geolocation.addEventListener( GeolocationEvent.UPDATE, handleGeolocationUpdate );
	}
}

Now within Flash CS5 select File | Publish Settings.... The Publish Settings panel will appear.

From the Publish Settings panel select the Flash tab:

Click on the Settings... button to the right of the Script: option. This will open the Advanced ActionScript 3.0 Settings panel:

Click on the Config Constants tab:

From here click on the plus symbol to add a configuration constant. Name the constant CONFIG::GEOLOCATION_REAL and assign it a value of false.

Add a second configuration constant named CONFIG::GEOLOCATION_SIMULATE and assign it a value of true.

The Config Constants pane should now look like this:

Click OK to close the Advanced ActionScript 3.0 Settings panel. Finally click OK to close the Publish Settings panel.

You now have two configuration constants that you can use to specify what code gets published with your SWF. Let's lace those two constants into your Application class. Add the lines of code that are in bold:

public function Application()
{
	NativeApplication.nativeApplication.systemIdleMode = SystemIdleMode.KEEP_AWAKE;
	NativeApplication.nativeApplication.addEventListener( KeyboardEvent.KEY_DOWN, handleKeyDown );
	NativeApplication.nativeApplication.addEventListener( Event.DEACTIVATE, handleDeactivate );
	
	CONFIG::GEOLOCATION_SIMULATE
	{
		geolocation = new GeolocationSimulate();
	}
			
	CONFIG::GEOLOCATION_REAL
	{
		geolocation = new GeolocationReal();
	}
			
	geolocation.setRequestedUpdateInterval( 1000 );
	geolocation.addEventListener( GeolocationEvent.UPDATE, handleGeolocationUpdate );
}

Configuration constants can be set to either true or false. When set to true, any code within a block defined by the constant gets compiled during publication. If the constant is set to false then that code block does not get compiled.

We have initially set CONFIG::GEOLOCATION_SIMULATE to true meaning that the following line of code within this block will get compiled:

CONFIG::GEOLOCATION_SIMULATE
{
	geolocation = new GeolocationSimulate();
}

If we set CONFIG::GEOLOCATION_SIMULATE to false and CONFIG::GEOLOCATION_REAL to true then the line of code within this block will get compiled instead:

CONFIG::GEOLOCATION_REAL
{
	geolocation = new GeolocationReal();
}

Conditional compilation is ideal for turning on and off blocks of code that, for example, implement certain device-specific features or are used for debugging. More information can be found in this Quick Tip.

Save the changes you have made to the Application class.


Step 22: Publish Profiles

Whenever you want to make a build for your Android device and wish to use the device's GPS unit, make sure you change your configuration constants to the following:

CONFIG::GEOLOCATION_REAL     true
CONFIG::GEOLOCATION_SIMULATE false

For testing on your desktop or on mobile without having to rely on its GPS unit, set the configuration constants to:

CONFIG::GEOLOCATION_REAL     false
CONFIG::GEOLOCATION_SIMULATE true

As you can imagine, constantly changing these values within the Advanced ActionScript 3.0 Settings panel could quickly become tiring. It's a good idea to actually create multiple publish profiles, each with its own configuration constant settings.

Within Flash CS5 select File | Publish Settings.... The Publish Settings panel will appear.

Click the Rename Profile icon (fourth icon to the right of the Current Profile drop-down) and name the profile 'Real'.

Click OK to exit from the Profile Properties panel and to commit the profile name. Now click on the Flash tab within the Publish Settings panel then click on the Settings... button to the right of the Script: option. Click on the Config Constants tab and ensure that the configuration constants are as shown below:

Click OK to close the Advanced ActionScript 3.0 Settings panel.

Now from within the Publish Settings panel make a duplicate copy of the current profile by clicking the Duplicate Profile icon (third icon to the right of the Current Profile drop-down). Name the duplicate profile 'Simulate'.

Click OK to exit from the Duplicate Profile panel and to create the duplicate profile.

Now within the Publish Settings panel click the Settings... button to the right of the Script: option. Click on the Config Constants tab and change the configuration constants to the values shown below:

Click OK to close the Advanced ActionScript 3.0 Settings panel. Click OK to close the Publish Settings panel.

Save the FLA.

You now have two profiles - Real and Simulate - that you can easily switch between depending on the type of build you want to create when publishing. This can be done quite easily by selecting File | Publish Settings... within Flash CS5. Now from the Publish Settings panel simply select the desired profile from the Current Profile dropdown:

Go ahead, try publishing the profiles on both desktop and your Android device.


Step 23: Converting Speed to Miles Per Hour

We're just about finished for the first part of this tutorial but let's do one last thing before we wrap things up.

At the moment, the speed that is displayed on-screen is measured in metres per second. The final version of the speedometer that we are working towards actually reports the speed in miles per hour.

So how do we convert metres per second to miles per hour? It's not that difficult to be honest. 1 metre per second is equal to 2.2369362920544 miles per hour.

Before we write any code, add a text field to the stage that will be used to display the speed in miles per hour. Simply copy the existing dynamic and static text fields and position them directly below the existing ones. Position the duplicate dynamic text field at ( 10, 270 ) and position the duplicate static text field at ( 95, 437 ).

Name the duplicate dynamic text field instance "milesPerHour." Change the text within the duplicate static text field to "miles per hour."

The snapshot below shows how your stage should look now:

Add to the Application class a public member variable that represents the new dynamic text field:

public  var metresPerSecond	:TextField;
public  var milesPerHour	:TextField;
private var geolocation		:IGeolocation;

Add a method to your Application class that does the conversion. Here's the code:

private function convertToMilesPerHour( metresPerSecond :Number ) :Number
{
	return metresPerSecond * CONVERSION_BASE;
}

Add the following constant at the top of your class:

static private const CONVERSION_BASE :Number = 2.2369362920544;

Finally add the following line of code to the handleGeolocationUpdate() method:

private function handleGeolocationUpdate( e :GeolocationEvent ) :void
{
	metresPerSecond.text = String( Math.round( e.speed ) );
	milesPerHour.text = String( Math.round( convertToMilesPerHour( e.speed ) ) );
}

You may be wondering why I use Math.round() to round both speed values before displaying them in their text fields. This is actually a workaround for a bug I discovered in the AIR for Android runtime, which was causing my Google Nexus One to crash when rendering floating point values within text fields.

As far as I know, Adobe are aware of this bug and it should be fixed in a future release.

Feel free to remove Math.round() from the handleGeolocationUpdate() method and test it on your device. If it causes the same crash I was experiencing then your device will lock-up for a few minutes before resetting. If you're happy with the values being rounded then I'd suggest you simply leave the code the way it is.

Save the class.

For the avoidance of doubt, here's the current version of the Application class in its entirety:

package
{
	import flash.desktop.NativeApplication;
	import flash.desktop.SystemIdleMode;
	import flash.display.Sprite;
	import flash.events.Event;
	import flash.events.GeolocationEvent;
	import flash.events.KeyboardEvent;
	import flash.text.TextField;
	import flash.ui.Keyboard;
	
	public class Application extends Sprite
	{
		static private const CONVERSION_BASE :Number = 2.2369362920544;
		
		public  var metresPerSecond	:TextField;
		public  var milesPerHour	:TextField;
		private var geolocation		:IGeolocation;
		
		public function Application()
		{
			NativeApplication.nativeApplication.systemIdleMode = SystemIdleMode.KEEP_AWAKE;
			NativeApplication.nativeApplication.addEventListener( KeyboardEvent.KEY_DOWN, handleKeyDown );
			NativeApplication.nativeApplication.addEventListener( Event.DEACTIVATE, handleDeactivate );
			
			CONFIG::GEOLOCATION_SIMULATE
			{
				geolocation = new GeolocationSimulate();
			}
			
			CONFIG::GEOLOCATION_REAL
			{
				geolocation = new GeolocationReal();
			}
			
			geolocation.setRequestedUpdateInterval( 1000 );
			geolocation.addEventListener( GeolocationEvent.UPDATE, handleGeolocationUpdate );
		}
		
		private function handleGeolocationUpdate( e :GeolocationEvent ) :void
		{
			metresPerSecond.text = String( Math.round( e.speed ) );
			milesPerHour.text = String( Math.round( convertToMilesPerHour( e.speed ) ) );
		}
		
		private function handleKeyDown( e :KeyboardEvent ) :void
		{
			switch( e.keyCode )
			{
				case Keyboard.BACK:
					NativeApplication.nativeApplication.exit();	
					break;
				
				case Keyboard.SEARCH:
				case Keyboard.MENU:
					e.preventDefault();
					break;				
			}
		}
		
		private function handleDeactivate( e :Event ) :void
		{
			NativeApplication.nativeApplication.exit();
		}
		
		private function convertToMilesPerHour( metresPerSecond :Number ) :Number
		{
			return metresPerSecond * CONVERSION_BASE;
		}
	}
}

Publish and run your app. You should now see the speed measured in both metres per second and miles per hour. This SWF should show you what to expect.

If you're testing on your desktop then remember to select the Simulate profile. If you're testing on your Android device using the Real profile then remember to switch on your phone's GPS sensor.


Conclusion

The first part of this tutorial has covered quite a lot of ground. You've learned the basics of AIR for Android development and spent time with some AIR specific APIs including GPS support and reading from the filesystem. Additionally we've touched upon interfaces and learned how conditional compilation can be used to include and exclude code from certain builds.

In Part Two you'll learn how to construct a more pleasing user interface, replacing the boring text fields you currently have with an analogue speedometer complete with mileage counter. You'll also learn how to save state within your app, ensuring that it starts where it previously left off the next time it's launched.

Thanks for reading and hopefully I'll see you again for Part Two over on Activetuts+.


Acknowledgements

The artwork used in this tutorial was created using the steps outlined in 'How to Design a Speedometer Icon in Photoshop' - a tutorial by Spanish graphic design guru, Roberto Abril Hidalgo.

A huge thanks to Dave Wagner for pointing me in the right direction with the Mac OS X specifics required for this tutorial.

A special thank you to my sister-in-law, Helen Caleb, who always comes to my rescue with her Photoshop expertise.

Advertisement