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:
- Flash Professional CS5 (30-day trial version will do)
- The Adobe AIR runtime for Android 2.2
- The Adobe Flash Professional CS5 Extension for AIR 2.5
- USB Device Drivers (Windows Only)
- 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.
Comments