Chapter 6

Adobe AIR and Native Android Apps

You have already learned how to create interesting Flex-based mobile applications, and in this chapter you will learn about other useful features that are available in Adobe AIR, and also how to merge Android-specific functionality into an Adobe AIR mobile application.

First you will learn how to do two things that are available in Adobe AIR: how to launch a native browser in an AIR application, and how to store application-specific data in a SQLite database. The next part of the chapter delves into Android fundamentals that you need to understand for the code samples that are discussed later in the chapter. This section shows you how to create a simple native Android application, along with a discussion of the main files in Android applications. You will also learn about important Android-specific concepts, such as Activities, Intents, and Services.

The third part of this chapter contains an example of an Adobe AIR mobile application that invokes an external API to provide status information about web sites that users have registered with an external service. Our Adobe AIR application stores the status of each web site in a SQLite database and then displays the status details in a datagrid. This mobile application also enables users to click a button that sends an update to native Android code, which in turn displays the update in the Android notification bar. The final part of this chapter contains the steps that are required to integrate an Adobe AIR mobile application with native Android code.

There are a few points to keep in mind regarding the material in this chapter. First, the Android content is intended to help you understand how to integrate native Android functionality into Adobe AIR applications. Consequently, only a subset of Android topics is covered, which is not enough to become a proficient Android application developer. Second, Adobe AIR is an evolving product, so it's possible that some of the Android features that are currently unavailable from Adobe AIR might become available in a future release. Third, the integration of Adobe AIR applications with native Android functionality is not officially supported by Adobe; as a result, if you experience difficulties with the integration process, there is no formal support mechanism available for you to resolve those difficulties.

Another point to consider pertains to the Android versions that are supported in the target devices for your Adobe AIR mobile application. For example, the number of mobile devices that support Android 2.2 is currently much greater than those that support Android 2.3.x or Android 3.0, both of which are currently limited to only a few tablets (such as the Samsung Galaxy Tab 10.1 and Motorola Xoom) and one smart phone (Samsung Galaxy S II).

On the other hand, if Adobe AIR supports all the functionality and features that you require for creating mobile applications, then you do not need any of the code samples in this chapter that illustrate how to merge an Adobe AIR application with an Android application. If this is the case, you can skip that material without loss of continuity.

Invoking URI Handlers in Adobe AIR

Currently there are five URI-related handlers available in Adobe AIR that enable you to perform the following actions in an Adobe AIR mobile application:

  • tel (make telephone calls)
  • sms (send text messages)
  • mailto (send email)
  • market (perform a market search)
  • http and https (launch a web browser)

The code is very straightforward for each of these handlers, which makes it very simple to embed these handlers in your Adobe AIR mobile applications. One thing to keep in mind is that Adobe AIR does not provide support for the “geo” URI, but you can still navigate to maps.google.com, and users will be prompted about opening the URL in the browser session versions of the Maps application. This “workaround” gives you the ability to support maps-related functionality in Adobe AIR mobile applications.

Create a new Flex mobile project called URIHandlers, using the Mobile Application template, and add the code shown in Listing 6–1.

Listing 6–1. Invoking URI Handlers

<?xml version="1.0" encoding="utf-8"?>
<s:View xmlns:fx="http://ns.adobe.com/mxml/2009"
        xmlns:s="library://ns.adobe.com/flex/spark" title="Home">
  <fx:Script>
    <![CDATA[
      import flash.sensors.Geolocation;

      [Bindable]
      public var tel:String;
      [Bindable]
      public var sms:String;
      [Bindable]
      public var mailto:String;
      [Bindable]
      public var search:String;
      [Bindable]
      public var http:String;
      [Bindable]
      public var geo1:String;

      private var geo:Geolocation;

      private function onTel():void {
         navigateToURL(new URLRequest("tel:"+tel));
      }

      private function onSMS():void {
         navigateToURL(new URLRequest("sms:"+sms));
      }

      private function onMailto():void {
         navigateToURL(new URLRequest("mailto:"+mailto+"?subject=Hello%20AIR"));
      }

      private function onSearch():void {
         navigateToURL(new URLRequest("market://search?q=iReverse"));
      }

      private function onHTTP():void {
         navigateToURL(new URLRequest(http));
      }

      private function onGeo():void {
         this.geo = new Geolocation();
         this.geo.addEventListener(GeolocationEvent.UPDATE, onLocationUpdate);
      }

      private function onLocationUpdate(e:GeolocationEvent):void  {
         this.geo.removeEventListener(GeolocationEvent.UPDATE,onLocationUpdate);
         var long:Number = e.longitude;
         var lat:Number = e.latitude;
         navigateToURL(new URLRequest("http://maps.google.com/"));
      }
    ]]>
  </fx:Script>

  <s:VGroup>
   <s:Form backgroundColor="0xFFFFFF" width="300">
    <s:FormItem>
      <s:HGroup left="0">
      <s:TextInput width="180" height="50" text="{tel}"/>
      <s:Button id="telID" width="250" height="50" label="(Call)" click="onTel();"/>
      </s:HGroup>
    </s:FormItem>

    <s:FormItem>
      <s:HGroup left="0">      <s:TextInput width="180" height="50" text="{sms}"/>
      <s:Button id="smsID" width="250" height="50" label="(Text)" click="onSMS();"/>
      </s:HGroup>
    </s:FormItem>

    <s:FormItem>
      <s:HGroup left="0">
      <s:TextInput width="180" height="50" text="{mailto}"/>
      <s:Button id="mailtoID" width="250" height="50" label="(EMail)"
                click="onMailto();"/>
      </s:HGroup>
    </s:FormItem>

    <s:FormItem>
      <s:HGroup left="0">
      <s:TextInput width="180" height="50" text="{search}"/>
      <s:Button id="searchID" width="250" height="50" label="(Search Market)"
                click="onTel();"/>
      </s:HGroup>
    </s:FormItem>

    <s:FormItem>
      <s:HGroup left="0">
      <s:TextInput width="180" height="50" text="{http}"/>
      <s:Button id="httpID" width="250" height="50" label="(Go)" click="onHTTP();"/>
      </s:HGroup>
    </s:FormItem>

    <s:FormItem>
      <s:HGroup left="0">
      <s:TextInput width="180" height="50" text="{geo1}"/>
      <s:Button id="geoID" width="250" height="50" label="(Geo)" click="onGeo();"/>
      </s:HGroup>
    </s:FormItem>
   </s:Form>
  </s:VGroup>
</s:View>

Listing 6–1 contains a form with various input fields, each of which has an associated event handler that invokes the built-in method navigateToURL() with a different argument. For example, when users enter a URL and then click the associated button, the method onHTTP() launches a URL with the following line of code:

navigateToURL(new URLRequest(http));

Figure 6–1 displays a form with various input fields that illustrate how to use the URI-related functionality in AIR mobile applications.

images

Figure 6–1. Using URI-related functionality

Launching Custom HTML Pages in Adobe AIR

Adobe AIR enables you to launch custom HTML pages (shown ahead) and also to navigate to arbitrary HTML pages (shown in Listing 6–2).

Create a new Flex mobile project called StageWebViewHTML1, using the ActionScript Mobile Application template, and add the code shown in Listing 6–2.

Listing 6–2. Launching a Hard-Coded HTML Page

package {
  import flash.display.Sprite;
  import flash.display.StageAlign;
  import flash.display.StageScaleMode;
  import flash.geom.Rectangle;
  import flash.media.StageWebView;

  public class StageWebViewHTML1 extends Sprite {
    public function StageWebViewHTML1() {
       super();

       // support autoOrients
       stage.align = StageAlign.TOP_LEFT;
       stage.scaleMode = StageScaleMode.NO_SCALE;
       var webView:StageWebView = new StageWebView();
       webView.stage = this.stage;
       webView.viewPort = new Rectangle( 0, 0,
                                         stage.stageWidth,
                                         stage.stageHeight );
       // create an HTML page
       var htmlStr:String = "<!DOCTYPE HTML>" +
         "<html>" +
         "<body>" +
           "<h3>An HTML Page in Adobe AIR</h3>" +
           "<p><strong>Hello from the Author Team:</strong></p>" +
           "<hr/>"+
           "<p>Stephen Chin</p>" +
           "<p>Dean Iverson</p>" +
           "<p>Oswald Campesato</p>" +
           "<p>Paul Trani</p>" +
           "<hr/>"+
           "<br/>"+
           "<p><i>This is the key line of code:</i></p>"+
           "<p>webView.loadString( htmlStr, 'text/html'; );</p>"+
           "<p>'htmlStr' contains the HTML contents</p>";
         "</body>" +
         "</html>";

       // launch the HTML page
       webView.loadString( htmlStr, "text/html" );
    }
  }
}

Listing 6–2 contains several import statements and auto-generated code, and the variable htmlStr is a string with the contents of an HTML page that is launched with one line of code:

       webView.loadString( htmlStr, "text/html" );

If you plan to invoke hard-coded HTML pages in mobile applications, experiment with different HTML5 tags to create the stylistic effects that you need for your HTML page.

Figure 6–2 displays the output from Listing 6–2, which renders an HTML page with the hard-coded contents.

images

Figure 6–2. Launching a hard-coded HTML page

Navigating to HTML Pages in Adobe AIR

In the previous example, you learned how to launch a hard-coded HTML page, and in this section you will learn how to navigate to any HTML page and then launch that HTML page in an Adobe AIR mobile application.

Create a new Flex mobile project called StageWebViewLaunch2, using the ActionScript Mobile Application template, and add the code shown in Listing 6–3.

Listing 6–3. Launching a User-Specified URL

<?xml version="1.0" encoding="utf-8"?>
<s:View xmlns:fx="http://ns.adobe.com/mxml/2009"
        xmlns:s="library://ns.adobe.com/flex/spark"
        xmlns:mx="library://ns.adobe.com/flex/mx"
        title="HomeView" >

  <fx:Script source="StageWebViewLaunch.as"/>

  <s:Label x="10" y="50" width="150" height="50" text="Enter a URL: "/>
  <s:TextInput x="180" y="50" width="290" height="50" text="{url}"/>
  <s:Button x="10" y="120" width="300" height="50"
            label="Launch the URL" click="StageWebViewExample()" />
</s:View>

Listing 6–3 contains an input field that enables users to enter a URL, and also a button for launching the specified URL via the method StageWebViewExample() that is defined in the ActionScript3 file StageWebViewLaunch.as.

Now create the file StageWebViewLaunch.as and insert the code shown in Listing 6–4.

Listing 6–4. The ActionScript3 Code for Launching a URL

import flash.media.StageWebView;
import flash.geom.Rectangle;

import flash.events.ErrorEvent;
import flash.events.Event;
import flash.events.LocationChangeEvent;

[Bindable]
public var url:String = "http://www.google.com";

private var webView:StageWebView = new StageWebView();

public function StageWebViewExample() {
  webView.stage = this.stage;
  webView.viewPort = new Rectangle( 0, 0,
                                    stage.stageWidth,
                                    stage.stageHeight );

  webView.addEventListener(Event.COMPLETE, completeHandler);
  webView.addEventListener(ErrorEvent.ERROR, errorHandler);
  webView.addEventListener(LocationChangeEvent.LOCATION_CHANGING,
                           locationChangingHandler);
  webView.addEventListener(LocationChangeEvent.LOCATION_CHANGE,
                           locationChangeHandler);
  // launch the user-specified URL
  webView.loadURL( url );
}

// Dispatched after the page or web content has been fully loaded
protected function completeHandler(event:Event):void {
  dispatchEvent(event);
}

// Dispatched when the location is about to change
protected function locationChangingHandler(event:Event):void {
  dispatchEvent(event);
}

// Dispatched after the location has changed
protected function locationChangeHandler(event:Event):void {
  dispatchEvent(event);
}

// Dispatched when an error occurs
protected function errorHandler(event:ErrorEvent):void {
  dispatchEvent(event);
}

Listing 6–4 defines the Bindable variable url that references the user-specified URL, followed by the variable webView that contains URL-specific functionality. The method StageWebViewExample() defines various event handlers, all of which invoke the built-in method dispatchEvent(), and then the user-specified URL is launched with this line of code:

  webView.loadURL( url );

Figure 6–3 displays the Google home page, which is the default URL in Listing 6–4.

images

Figure 6–3. Launching a user-specified URL

Accessing SQLite in Adobe AIR

Adobe AIR provides support for accessing data in a SQLite database that is stored on a mobile device. You can also access a SQLite database directly from native Android code, but Adobe AIR provides a higher level of abstraction (and the code is simpler).

Create a new Flex mobile project called SQLite1, using the Mobile Application template, and add the code shown in Listing 6–5.

Listing 6–5. Viewing Data in a SQLite Database

<?xml version="1.0" encoding="utf-8"?>
<s:View xmlns:fx="http://ns.adobe.com/mxml/2009"
        xmlns:s="library://ns.adobe.com/flex/spark"
        title="HomeView" creationComplete="start()">

  <fx:Script source="SQLiteAccess.as"/>

  <s:Label x="10" y="10" width="400" height="50"
           text="Create New Person and Click 'Add'"/>
  <s:Label x="10" y="50" width="150" height="50" text="First name:"/>
  <s:TextInput x="250" y="50" width="200" height="50" id="first_name"/>

  <s:Label x="10" y="100" width="150" height="50" text="Last name:"/>
  <s:TextInput x="250" y="100" width="200" height="50" id="last_name"/>

  <s:Button x="10" y="160" width="200" height="50"
            label="Add" click="addItem()"/>

  <s:Button label="Remove Selected Person" height="50" x="10" y="230"
                  click="remove()" enabled="{dg.selectedIndex != -1}"/>

  <s:DataGrid id="dg" left="10" right="10" top="300" bottom="80"
              dataProvider="{dp}">
     <s:columns>
       <s:ArrayList>
         <s:GridColumn headerText="Index"
                       dataField="id"
                       width="100" />
         <s:GridColumn headerText="First name"
                       dataField="first_name"
                       width="150" />
         <s:GridColumn headerText="Last name"  
                       dataField="last_name"
                       width="150" />
       </s:ArrayList>
    </s:columns>
  </s:DataGrid>
</s:View>

Listing 6–5 contains a XML Script element that references an ActionScript3 file containing methods that access a SQLite database, and its contents will be shown in Listing 6–6. The labels and text input fields enable users to enter a first name and a last name for each new person that will be added to our database.

There is one XML Button element for adding a new person via the addPerson() method, and also one XML Button for deleting an existing person from our database via the removePerson() method. Both methods are defined in SQLiteAccess.as. The variable dp is a Bindable variable that contains the data that is displayed in the datagrid, and since users can add as well as delete rows of data, dp is a Bindable variable.

Since we are accessing data that is stored in a SQLite database, we need to define several methods in ActionScript3 for managing the creating, accessing, and updating of the contents of the database. Create a new ActionScript3 file called SQLiteAccess.as in the same directory as the file SQLite1HomeView.mxml, and add the code shown in Listing 6–6.

Listing 6–6. Defining Database Access Methods in ActionScript3

import flash.data.SQLStatement;
import flash.errors.SQLError;
import flash.events.Event;
import flash.events.SQLErrorEvent;
import flash.events.SQLEvent;
import flash.events.TimerEvent;
import flash.filesystem.File;
import flash.utils.Timer;

import mx.collections.ArrayCollection;
import mx.utils.ObjectUtil;

import org.osmf.events.TimeEvent;

Listing 6–6 contains various SQL-related import statements and also a Timer class, which we will use when we attempt to read the contents of the updated database. Since only one SQL statement can be executing at any given point in time, the Timer class gives us the ability to “try again later” (measured in milliseconds).

// sqlconn holds the database connection
public var sqlconn:SQLConnection = new SQLConnection();

// sqlstmt holds SQL commands
public var sqlstmt:SQLStatement = new SQLStatement();

// a bindable ArrayCollection and the data provider for the datagrid
[Bindable]
public var dp:ArrayCollection = new ArrayCollection();

// invoked after the application has loaded
private function start():void {
  // set 'people.db' as the file for our database (created after it's opened)
  var db:File = File.applicationStorageDirectory.resolvePath("people.db");

  // open the database in asynchronous mode
  sqlconn.openAsync(db);

  // event listeners for handling sql errors and 'result' are
  // invoked whenever data is retrieved from the database
  sqlconn.addEventListener(SQLEvent.OPEN, db_opened);
  sqlconn.addEventListener(SQLErrorEvent.ERROR, error);
  sqlstmt.addEventListener(SQLErrorEvent.ERROR, error);
  sqlstmt.addEventListener(SQLEvent.RESULT, result);
}

The variables sqlconn and sqlstmt enable us to get a connection to our SQLite database and also execute SQL queries. The start() method specifies the database name as people.db, and then opens an asynchronous connection. Note the various event handlers that are used for handling database-related actions as well as handling errors.

private function db_opened(e:SQLEvent):void {
  // specify the connection for the SQL statement
  sqlstmt.sqlConnection = sqlconn;

  // Table "person_table" contains three columns:
  // 1) id  (an autoincrementing integer)
  // 2) first_name (the first name of each person)
  // 3) last_name (the last name of each person)
 sqlstmt.text = "CREATE TABLE IF NOT EXISTS person_table
                            ( id INTEGER PRIMARY KEY AUTOINCREMENT,
                             first_name TEXT, last_name TEXT);";

  // execute the sqlstmt to update the database
  sqlstmt.execute();

  // refresh the datagrid to display all data rows
  refreshDataGrid();
}

// function to append a new row to person_table
// each new row contains first_name and last_name
private function addPerson():void {
  sqlstmt.text = "INSERT INTO person_table (first_name, last_name)
                          VALUES('"+first_name.text+"','"+last_name.text+"'),";
  sqlstmt.execute();

  refreshDataGrid();
}

The method db_opened() specifies the name of the database and the table that contains our person-related data, and then executes the refreshDataGrid() method, which retrieves the latest contents of our database in order to display that data in the datagrid of our mobile application. Notice that after addPerson() has inserted a new person, the refreshDataGrid() method is invoked so that the datagrid will automatically display the newly added person.

// function to refresh the data in datagrid
private function refreshDataGrid(e:TimerEvent = null):void {
  // timer object pauses and then attempts to execute again
  var timer:Timer = new Timer(100,1);
  timer.addEventListener(TimerEvent.TIMER, refreshDataGrid);

  if ( !sqlstmt.executing ) {
    sqlstmt.text = "SELECT * FROM person_table"
    sqlstmt.execute();
  } else {
    timer.start();
  }
}

// invoked when we receive data from a sql command
private function result(e:SQLEvent):void {
  var data:Array = sqlstmt.getResult().data;

  // fill the datagrid
  dp = new ArrayCollection(data);
}

// remove a row from the table
private function removePerson():void {
  sqlstmt.text = "DELETE FROM person_table WHERE id="+dp[dg.selectedIndex].id;
  sqlstmt.execute();
  refreshDataGrid();
}

// error handling method
private function error(e:SQLErrorEvent):void {
//  Alert.show(e.toString());
}

The method refreshDataGrid() first checks whether a SQL statement is currently being executed; if so, then it pauses the specified number of milliseconds (which is 100 in this example) and then retrieves all the rows from the person_table (which will include newly added persons). The method result() populates the variable dp with the refreshed set of data. The removePerson() method deletes the row that users selected by tapping on that row in the datagrid.

Figure 6–4 displays a set of rows that are stored in a SQLite database on a mobile device.

images

Figure 6–4. A set of records in a SQLite database

Learning Basic Concepts in Android

Android is an open source toolkit for developing Android mobile applications, and at the time of writing Android 3.0 (“Honeycomb”) is the latest major release; the most current version of Android is 3.1. Note that Adobe AIR 2.5.1 for mobile applications requires at least Android 2.2, and you can install Adobe AIR applications on mobile devices that support Android 2.2 or higher.

The following sections provide some information about the major features of Android 3.0, where to download Android, and key concepts in Android. When you finish this section, you'll have an understanding of how to create native Android applications.

Major Features of Android 3.0

Earlier versions of Android provided support for UI components and event handlers, audio and video, managing files on the file system, graphics and animation effects, database support, web services, and telephony and messaging (SMS).

Google Android 3.0 (released in early 2011) provides backward-compatible support for the functionality in Android 2.3 (which was released in December 2010). Android 3.0 provides feature improvements over version 2.3 as well as the following new features:

  • A new UI for tablets
  • System bar for status and notifications
  • Action bar for application control
  • Customizable home screen
  • Copy/paste support
  • More connectivity options
  • SVG support
  • Universal remote function
  • Google Docs integration
  • Built-in remote desktop
  • Improved media player
  • Better GPS support
  • Improved multitasking
  • Tracking facility
  • Battery life/power management improvements

The nice feature improvements in Android 3.0 involve longer battery life, faster graphics rendering, and richer media functionality (e.g., time-lapse video, HTTP live streaming, and DRM). Moreover, Android 3.1 supports another set of new features, including APIs for USB accessories and new input events from mice, trackballs, and joysticks. However, in this chapter we are focusing on a small subset of Android functionality that we need to understand in order to merge Adobe AIR applications with native Android applications, so we will not delve into the features of Android 3.x. Navigate to the Android home page to obtain more information about the new and improved suite of features that are supported in |Android 3.x.

Download/Installation of Android

You need to download and install Java, Eclipse, and Android in order to develop Android-based mobile applications in Eclipse. Note that Java is pre-installed on Mac OSX, and Java is also available for download via Linux-based package managers. You can download Java for your platform here: http://java.sun.com/javase/downloads.

If you have a Windows machine, you need to set the environment variable JAVA_HOME to the directory where you uncompressed the Java distribution.

The Android SDK is available for download here: http://developer.android.com/sdk/index.html.

For the Windows platform, the Android distribution is a file with the following type of name (this may be slightly different by the time this book is published): android-sdk_r06-windows.zip.

After you have completed the Java and Eclipse installation, follow the Android installation steps in order to install Android on your machine.

You also need to create an AVD (Android Virtual Device), and the step-by-step instructions for doing so are in Chapter 5.

Key Concepts in Android

Although Android applications are written in Java, the Java code is compiled into a Dalvik executable, which (along with other assets) is part of the .apk application file that is deployed to Android devices.

In addition to support for standard Java language features, Android applications usually involve some combination of the following Android-specific concepts:

  • Activities
  • Intents
  • Services
  • Broadcast receivers

After you have mastered the concepts in the preceding list, you can learn about Intent Filters and Content Providers (a full discussion of these two topics is beyond the scope of this chapter) and how to use them in order to provide more fine-grained Intent-based functionality as well as the ability to share data across Android applications.

The properties of every Android application are specified in the XML document AndroidManifest.xml, which is automatically generated during the creation of every Android application. This manifest file contains information about the Activities, Intents, Services, Broadcast receivers, and permissions that are part of the associated Android application.

An Android application can contain multiple Android Activities, and each Android Activity can contain multiple Intents and Intent Filters. Furthermore, an Android application can contain an Android Service and also an Android Broadcast receiver, both of which are defined as sibling elements of the Android activity elements in AndroidManifest.xml.

Listing 6–7 provides an outline of what you might find in an AndroidManifest.xml project file. In this case, the project file contains the “stubs” for two Android Activities, two Android Services, and two Android Broadcast receivers. The attributes of the XML elements have been omitted so that you can see the overall structure of an Android application, and later in this chapter you will see a complete example of the contents of AndroidManifest.xml.

Listing 6–7. An Outline of an AndroidManifest.xml

<manifest xmlns:android=http://schemas.android.com/apk/res/android>
    <application>
        <activity>
            <intent-filter>
            </intent-filter>
        </activity>
        <activity>
        </activity>
        <service>
        </service>
        <service>
            <intent-filter>
            </intent-filte>
        </service>
        <receiver>
        </receiver>
        <receiver>
            <intent-filter>
            </intent-filter>
        </receiver>
    </application>
</manifest>

Listing 6–7 contains two Android activity elements, two Android service elements, and two Android receiver elements. In each of these three pairs, there is one element that contains an Android intent-filter element, but keep in mind that many variations are possible for the contents of AndroidManifest.xml. The exact contents of the project file depend on the functionality of your Android application.

Android also supports Java Native Interface (JNI), which allows Java code to invoke C/C++ functions. However, you also need to download and install the Android Native Development Kit (NDK), which contains a set of tools for creating libraries that contain functions that can be called from Java code. You can use JNI if you need better performance (especially for graphics) than you can achieve through Java alone, but the details of this topic are beyond the scope of this chapter.

Android Activities

An Android Activity corresponds to a screen or a view of an application, and the main entry point of an Android application is an Android Activity that contains an onCreate() method (which overrides the same method in its superclass) that is invoked whenever you launch an Android application. When you launch your Android application, its Android Activity will be started automatically. As an example, Listing 6–8 displays the contents of HelloWorld.java, which is automatically generated when you create an Eclipse-based “Hello World” Android application later in this chapter.

Listing 6–8. Contents of HelloWorld.java

package com.apress.hello;

import android.app.Activity;
import android.os.Bundle;

public class HelloWorld extends Activity
{
   /** Called when the activity is first created. */
   @Override
   public void onCreate(Bundle savedInstanceState)
   {
      super.onCreate(savedInstanceState);
      setContentView(R.layout.main);
   }
}

During the project creation step for Android applications, you specify the package name and the class name; the rest of the generated code is the same for every Android project.

Notice that HelloWorld extends android.app.Activity, and it also overrides the onCreate() method. As you can probably surmise, an Android Activity is an Android class containing a set of methods (such as onCreate()) that you can override in your Android applications. An Activity contains one or more Views that belong to an Android application.

An Android View is what users see on the screen, which includes the UI widgets of the Android application. The HelloWorld Android application contains an Android class that extends the Android Activity class and overrides the onCreate() method with your custom code. Note that Android applications can also extend other Android classes (such as the Service class), and they can also create threads.

An Android application can contain more than one Android Activity, and as you already know, every Activity must be defined in the XML document AndroidManifest.xml, which is part of every Android application.

The HelloWorld Android project contains the XML document AndroidManifest.xml, in which the Android class HelloWorld is registered in the XML activity element, as shown in Listing 6–9.

Listing 6–9. Contents of AndroidManifest.xml

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
          package="com.apress.hello"
          android:versionCode="1"
          android:versionName="1.0">
    <application android:icon="@drawable/icon"
                        android:label="@string/app_name">
        <activity android:name=".HelloWorld"
                      android:label="@string/app_name">
          <intent-filter>
            <action android:name="android.intent.action.MAIN" />
            <category android:name="android.intent.category.LAUNCHER" />
          </intent-filter>
        </activity>

    </application>
    <uses-sdk android:minSdkVersion="9" />
</manifest>

Notice the period (“.”) that precedes the Android Activity HelloWorld in Listing 6–9. This period is mandatory because the string .HelloWorld is appended to the package name com.apress.hello (also specified in Listing 6–9), so the fully qualified name of HelloWorld.java in this Android project is com.apress.hello.HelloWorld.

Android Intents

The Android UI (user interface) consists of Intents and Views. In abstract terms, an Android Intent represents the details regarding an action (often described by a verb) to perform in an Android application.

An Intent is essentially a notification between Android Activities (or Services). An Intent enables an Android Activity to send data to other Android Activities and also to receive data from other Android Activities.

An Android Intent is similar to an event handler, but Android provides additional functionality for handling multiple Intents and options for using existing Inten ts vs. starting a new Intent. Android Intents can start a new Android Activity, and they can also broadcast messages (which are processed by Android Broadcast receivers). The following snippet illustrates how to start a new Activity via an Intent:

Intent intent = new Intent(action, data);
startActivity(intent);

Android Activities and Intents provide a loosely coupled set of resources that is reminiscent of SOA (service-oriented architecture). The counterpart of an Android Activity would be a web service, and the Intents that the Android Activity can process are comparable to the “operations” or methods that the web service makes available to the world. Other Android applications can explicitly invoke one of those methods, or they can make a “general” request, in which case the “framework” determines which web services will handle that general request.

You can also broadcast Intents in order to send messages between components. The following snippet illustrates how to broadcast an Intent:

Intent intent = new Intent(a-broadcast-receiver-class);
sendBroadcast(intent);

This type of functionality provides a greater flexibility and “openness” for Android applications.

Types of Android Intents

There are several types of Android Intents, each of which provides slightly different functionality. A directed Intent is an Intent with one recipient, whereas a broadcast Intent can be received by any process. An explicit Intent specifies the Java class that needs to be invoked. An implicit Intent is an Intent that does not specify a Java class, which means that the Android system will determine which application will process the implicit Intent. If there are several applications available that can respond to an implicit Intent, the Android system gives users the ability to select one of those applications.

Android also has the notion of Intent Filters, which are used for Intent resolution. An Intent Filter indicates the Intents that an Android Activity (or Android Service) can “consume,” and the details are specified in the XML intent-filter element. Note that if an application does not provide an Intent Filter, then it can be invoked only by an explicit Intent (and not by an implicit Intent).

An Intent Filter is specified in an Android Activity in the file AndroidManifest.xml, as shown in Listing 6–10.

Listing 6–10. An Example of an Intent Filter in Android

<intent-filter>
      <action android:name="android.intent.action.MAIN" />
      <category android:name="android.intent.category.LAUNCHER" />
</intent-filter>

Listing 6–10 displays a fragment from the contents of AndroidManifest.xml, which is shown in Listing 6–9. The XML action element in Listing 6–10 specifies the default value android.intent.action.MAIN, and the XML category element specifies android.intent.category.LAUNCHER (also a default value), which means that the parent Activity will be displayed in the application launcher.

An Intent Filter must contain an XML action element, and optionally contain an XML category element or an XML data element. As you can see, Listing 6–10 contains the mandatory XML action element that specifies the default action, and an optional XML category element, but not the optional XML data element.

An Intent Filter is a set of information that defines a specific action; the XML data element specifies the data to be acted upon, and the XML category element specifies the component that will perform the action.

There are various combinations of these three XML elements that can be specified in an Intent Filter because two of these elements are optional, and Android uses a priority-based algorithm to determine what will be executed for each Intent Filter that you define in AndroidManifest.xml. If you need to learn about Intent Filters in greater detail, consult the Android documentation for additional information.

In case you are interested in finding out about Android Intents that are freely available, you can visit OpenIntents, which is an open source project consisting of various Android Intents that have been donated by other people, and its home page is here: www.openintents.org/en/.

OpenIntents provides Android applications in various categories, such as utilities, business applications, education, and entertainment. The applications are available as .apk files, and sometimes the source code for those applications is also available for free. Moreover, OpenIntents provides links to Android libraries such as game engines, charting packages, and services such as accessing a CouchDB server, Drupal, Facebook, and many others. The OpenIntents libraries are available here: www.openintents.org/en/libraries.

OpenIntents also provides a registry of publicly available Android Intents that can be invoked by Android Activities, along with their class files, and a description of their services. Visit the OpenIntents home page for more information.

Android Services

Android Services are available for handling background tasks and other tasks that do not involve a visual interface. Since Android Services run in the main thread of the main process, Android Services typically start a new thread when they need to perform work without blocking the UI (which is handled in the main thread) of the Android application. Thus, an Android application can “bind” to a Service through a set of APIs that are exposed by that service.

An Android Service is defined via an XML service element in AndroidManifest.xml, as shown here:

<service android:name=".subpackagename.SimpleService"/>

Listing 6–11 displays the contents of ServiceName.java, which provides “skeleton” code for the definition of a custom Android Service class.

Listing 6–11. Contents of SimpleService.java

public class SimpleService extends Service {
   @Override
   public IBinder onBind(Intent intent) {
      return null;
   }

   @Override
   protected void onCreate() {
      super.onCreate();
      startservice(); // defined elsewhere
   }

   @Override
   protected void onCreate() {
      // insert your code here
   }

   @Override
   protected void onStart() {
      // insert your code here
   }
}

For example, if you need to execute something on a regular basis, you can include an instance of a Timer class that schedules and executes a TimerTask as often as required for your application needs.

Android Broadcast Receivers

The purpose of an Android Broadcast receiver is to “listen” to Android Intents. Listing 6–12 displays the definition of an Android Broadcast receiver in the AndroidManifest.xml file for the widget-based Android application that is discussed later in this chapter.

Listing 6–12. Sample Entry for a Receiver in AndroidManifest.xml

<!-- Broadcast Receiver that will process AppWidget updates -->
    <receiver android:name=".MyHelloWidget" android:label="@string/app_name">
      <intent-filter>
        <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
      </intent-filter>
      <meta-data android:name="android.appwidget.provider"
                 android:resource="@xml/hello_widget_provider" />
    </receiver>

Listing 6–12 contains an XML receiver element that specifies MyHelloWidget as the Java class for this widget. This Android receiver contains an XML intent-filter element with an XML action element that causes an action to occur when it is time to update the AppWidget MyHelloWidget, as shown here:

    <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
Android Life Cycle

Earlier in this chapter, you saw the contents of HelloWorld.java, which contains the onCreate() method that overrides the same method in the superclass. In fact, onCreate() is one of the seven Android methods that make up the Android application lifecycle.

A Google Android application contains the following methods, and this is the order in which methods are invoked during the lifecycle of an Android application1:

  • onCreate()
  • onRestart()
  • onStart()
  • onResume()
  • onPause()
  • onStop()
  • onDestroy()

The onCreate() method is invoked when an Activity is created, and its role is similar to init() methods in other languages. The onDestroy() method is invoked when an Activity is removed from memory, and its role is essentially that of a destructor method in C++. The onPause() method is invoked when an Activity must be paused (such as reclaiming resources). The onRestart() method is invoked when an Activity is being restarted. The onResume() method is invoked when an Activity interacts with a user. The onStart() method is invoked when an Activity becomes visible on the screen. Finally, the onStop() method is invoked in order to stop an Activity.

The methods onRestart(), onStart(), and onStop() are in the visible phase; the methods onResume() and onPause() are in the foreground phase. An Android application can pause and resume many times during the execution of that application; the details are specific to the functionality of the application (and possibly the type of user interaction as well).

Creating Android Applications

This section describes how to create an Android application in Eclipse, and a subsequent section shows you the directory structure of an Android application, followed by a discussion of the main files that are created in every Android application.

Launch Eclipse and perform the following sequence of steps in order to create a new Android application called HelloWorld:

  1. Navigate to File images New images Android Project.
  2. Enter “HelloWorld” for the project name.
  3. Select the checkbox on the left of “Android 2.3” for the Build Target.
  4. Enter “HelloWorld” for the Application name.
  5. Enter “com.apress.hello” for the Package name.
  6. Enter “HelloWorld” in the Create Activity input field.
  7. Enter the digit “9” for the Min SDK Version.
  8. Click the Finish button.

Eclipse will generate a new Android project (whose structure is described in the next section). Next, launch this application by right-clicking the project name HelloWorld, and then selecting Run As images Android Application. You must wait until the Android emulator completes its initialization steps, which can require a minute or so (but each subsequent launching of your application will be noticeably faster).

Figure 6–5 displays the output of the HelloWorld application in an Android simulator that is launched from Eclipse.

images

Figure 6–5. The HelloWorld Android application

The Structure of an Android Application

Navigate to the HelloWorld project that you created in the previous section, and right-click the project name in order to display the expanded directory structure. The next several sub-sections discuss the directory structure and the contents of the main files of every Android application.

Listing 6–13 displays the directory structure of the Android project HelloWorld.

Listing 6–13. Structure of an Android Project

+HelloWorld
  src/
    com/
      apress/
        hello/
          HelloWorld.java

  gen/
    com/
      apress/
        hello/
          R.java
  Android 2.3/
    android.jar
  assets/
  res/
    drawable-hdpi/
      icon.png
    drawable-ldpi/
      icon.png
    drawable-mdpi/
      icon.png
    layout/
      main.xml
    values/
      strings.xml
  AndroidManifest.xml
  default.properties
proguard.cfg

This Android application contains two Java files (HelloWorld.java and R.java), a JAR file (android.jar), an image file (icon.png), three XML files (main.xml, strings.xml, and AndroidManifest.xml), and a text file, default.properties.

The Main Files in an Android Application

The files in the HelloWorld Android application that we will discuss in this section are listed here (all files are listed relative to the project root directory):

  • src/com/apress/hello/HelloWorld.java
  • gen/com/apress/hello/R.java
  • AndroidManifest.xml
  • res/layout/main.xml
  • res/values/strings.xml

Listing 6–14 displays the contents of HelloWorld.java, which is where you include all the custom Java code that is required for this Android application.

Listing 6–14. Contents of HelloWorld.java

package com.apress.hello;

import android.app.Activity;
import android.os.Bundle;
public class HelloWorld extends Activity {
    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
    }
}

Earlier in this chapter, you saw the contents of HelloWorld.java. You can think of this code as “boilerplate” code that is automatically generated during project creation, based on the user-supplied values for the package name and the class name.

Your custom code is included immediately after this statement:

        setContentView(R.layout.main);

Instead of setting the View from the file main.xml, it is also common to set the View from another XML file or from a custom class that is defined elsewhere in your Android project.

Now let's look at Listing 6–15, which displays the contents of the resources file R.java, which is automatically generated for you when you create an Android application.

Listing 6–15. Contents of R.java

/* AUTO-GENERATED FILE.  DO NOT MODIFY.
 *
 * This class was automatically generated by the
 * aapt tool from the resource data it found.  It
 * should not be modified by hand.
 */

package com.apress.hello;

public final class R {
    public static final class attr {
    }
    public static final class drawable {
        public static final int icon=0x7f020000;
    }
    public static final class layout {
        public static final int main=0x7f030000;
    }
    public static final class string {
        public static final int app_name=0x7f040001;
        public static final int hello=0x7f040000;
    }
}

The integer values in Listing 6–15 are essentially references that correspond to assets of an Android application. For example, the variable icon is a reference to the icon.png file that is located in a subdirectory of the res directory. The variable main is a reference to the XML file main.xml (shown later in this section) that is in the res/layout subdirectory. The variables app_name and hello are references to the XML app_name element and XML hello element that are in the XML file strings.xml (shown earlier in this section) that is in the res/values subdirectory.

Now that we have explored the contents of the Java-based project files, let's turn our attention to the XML-based files in our Android project. Listing 6–16 displays the entire contents of AndroidManifest.xml.

Listing 6–16. Contents of AndroidManifest.xml

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
          package="com.apress.hello"
          android:versionCode="1"
          android:versionName="1.0">
    <application android:icon="@drawable/icon"
                        android:label="@string/app_name">
        <activity android:name=".HelloWorld"
                      android:label="@string/app_name">
          <intent-filter>
             <action android:name="android.intent.action.MAIN" />
            <category android:name="android.intent.category.LAUNCHER" />
          </intent-filter>
        </activity>

    </application>
    <uses-sdk android:minSdkVersion="9" />
</manifest>

Listing 6–16 starts with an XML declaration, followed by an XML manifest element that contains child XML elements, which provide information about your Android application. Notice that the XML manifest element contains an attribute with the package name of your Android application.

The XML application element in Listing 6–16 contains an android:icon attribute whose value is @drawable/icon, which refers to an image file icon.png that is located in the res subdirectory. Android supports three types of image files: high-density, medium-density, and low-density. The corresponding directories are drawable-hdpi, drawable-mdpi, and drawable-ldpi, all of which are subdirectories of the res directory under the root directory of every Android application.

The XML application element in Listing 6–16 also contains an android:label attribute whose value is @string/app_name, which refers to an XML element in the file strings.xml that is in the res/values subdirectory.

Listing 6–16 contains an XML intent-filter element, which was briefly discussed earlier in this chapter. The final part of Listing 6–10 specifies the minimum Android version number that is required for this application, as shown here:

    <uses-sdk android:minSdkVersion="9" />

In our current example, the minimum version is 9, which is also the number that we specified during the creation step of this Android application.

Now let's take a look at Listing 6–17, which displays the contents of the XML file strings.xml.

Listing 6–17. Contents of strings.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <string name="hello">Hello World, HelloWorld!/string>
    <string name="app_name">HelloWorld</string>
</resources>

Listing 6–17 is straightforward: it contains an XML resources element with two XML child elements that are used to display the string “Hello World, HelloWorld!” when you launch this Android application. Note that the XML application element in the XML document AndroidManifest.xml references also the second XML string element, whose name attribute has the value app_name, as shown here:

<application android:icon="@drawable/icon"
                    android:label="@string/app_name">

Now let's look at Listing 6–18, which displays the contents of the XML document main.xml, which contains View-related information about this Android application.

Listing 6–18. Contents of main.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    >
<TextView
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    android:text="@string/hello"
    />
</LinearLayout>

Listing 6–18 contains an XML LinearLayout element that is the default layout for an Android application. Android supports other layout types, including AbsoluteLayout, FrameLayout, RelativeLayout, and TableLayout (none of which are discussed in this chapter).

The XML LinearLayout element contains a fill_parent attribute that indicates that the current element will be as large as the parent element (minus padding). The attributes layout_width and layout_height specify the basic values for the width and height of the View.

The XML TextView element contains the attributes layout_width and layout_height, whose values are fill_parent and wrap_content, respectively. The wrap_content attribute specifies that the size of the View will be just big enough to enclose its content (plus padding). The attribute text refers to the XML hello element that is specified in the strings.xml file (located in the res/values subdirectory), whose definition is shown here:

<string name="hello">Hello World, HelloWorld!</string>

The string “Hello World, HelloWorld!” is the text that is displayed when you launch the “Hello World” Android application in the Android simulator or in an Android device after having deployed this Android application.

Sending Notifications in Android Applications

As you already know, Adobe does not provide built-in support for notifications, which are available in native Android mobile applications. However, the example in this section will show you how to create an Adobe AIR mobile application and a native Android application that can be merged into a single .apk file, which will support the Notification-related functionality.

James Ward (who wrote the foreword for this book) contributed the socket-based code in this section, and Elad Elrom provided the step-by-step instructions for merging an Adobe AIR mobile application with a native Android application.

This section is lengthy because there is an initial setup sequence (involving six steps), two Adobe AIR source files, and also Android source files for this mobile application. The first part describes the setup sequence; the second part of this section discusses the two source files with Adobe AIR code; and the third part discusses the two source files with native Android code.

  1. Download a package with the required dependencies for extending AIR for Android:
    www.jamesward.com/downloads/extending_air_for_android-flex_4_5-air_2_6–v_1.zip
  2. Create a regular Android project in Eclipse (do not create an Activity yet): specify a “Project name:” of FooAndroid, select Android 2.2 for the “Target name:”, type “FooAndroid” for the “Application name:”, enter “com.proandroidflash” for the “Package name:”, type “8” for the “Min SDK Version:”, and then click the Finish button.
  3. Copy all of the files from the zip file you downloaded in step 1 into the root directory of the newly created Android project. You will need to overwrite the existing files, and Eclipse will prompt you about updating the launch configuration.
  4. Delete the res/layout directory.
  5. Add the airbootstrap.jar file to the project's build path by right-clicking the file, and then select Build Path and Add to Build Path.
  6. Launch the project and confirm that you see “Hello, world” on your Android device. If so, then the AIR application is properly being bootstrapped and the Flex application in assets/app.swf is correctly being run.

Now that we have completed the initial setup steps, let's create a new Flex mobile project called Foo, using the Mobile Application template, and add the code shown in Listing 6–19.

Listing 6–19. Receiving Data and Sending the Data to a Notification on Android

<?xml version="1.0" encoding="utf-8"?>
<s:View xmlns:fx="http://ns.adobe.com/mxml/2009"
        xmlns:s="library://ns.adobe.com/flex/spark"
        title="HomeView" creationComplete="start()">

  <fx:Script source="SQLiteAccess.as"/>

  <!-- get status updates from Montastic -->
  <s:Button x="10" y="10" width="200" height="50"
            label="Get Statuses" click="invokeMontastic()">
  </s:Button>

  <s:Button x="250" y="10" width="200" height="50"
            label="Clear History" click="removeAll()">
  </s:Button>

  <s:DataGrid id="dg" left="10" right="10" top="70" bottom="100"
              dataProvider="{dp}">
    <s:columns>
      <s:ArrayList>
        <s:GridColumn headerText="ID"
                      dataField="id"
                      width="60" />
        <s:GridColumn headerText="Status"
                      dataField="status"
                      width="100" />
        <s:GridColumn headerText="URL"
                      dataField="url"
                      width="300" />
      </s:ArrayList>
    </s:columns>
  </s:DataGrid>

  <s:Button label="Create Notification" x="10" y="650" width="300" height="50">
     <s:click>
        >![CDATA[
           var s:Socket = new Socket();
           s.connect("localhost", 12345);
           s.addEventListener(Event.CONNECT, function(event:Event):void {
             trace('Client successfully connected to server'),
             (event.currentTarget as Socket).writeInt(1);
             (event.currentTarget as Socket).writeUTF(allStatuses);
             (event.currentTarget as Socket).flush();
             (event.currentTarget as Socket).close();
           });
          s.addEventListener(IOErrorEvent.IO_ERROR, function(event:IOErrorEvent):void {
            trace('error sending allStatuses from client: ' + event.errorID);
          });
          s.addEventListener(ProgressEvent.SOCKET_DATA,
function(event:ProgressEvent):void {
            trace('allStatuses sent successfully'),
         });
        ]]>
     </s:click>
  </s:Button>
</s:View>

Listing 6–19 contains an XML Button that invokes the Montastic APIs in order to retrieve the status of the web sites that are registered in Montastic. When users click this button, the statuses of the web sites are stored in a SQLite database and the datagrid is refreshed with the new set of rows.

The second XML Button enables users to delete all the rows in the SQLite table, which is convenient because this table can quickly increase in size. If you want to maintain all the rows of this table, you probably ought to include scrolling capability for the datagrid.

When users click the third XML Button element, this initiates a client-side socket-based connection on port 12345 in order to send the latest statuses of the web sites to a server-side socket that is running in a native Android application. The Android application reads the information sent from the client and then displays the statuses in the Android notification bar.

The ActionScript3 code in Listing 6–20 is similar to Listing 6–8, so you will be able to read its contents quickly, despite the various application-specific changes to the code.

Listing 6–20. Receiving Data and Sending the Data to a Notification on Android

Import flash.data.SQLConnection;
import flash.data.SQLStatement;
import flash.events.Event;
import flash.events.IOErrorEvent;
import flash.errors.SQLErrorEvent;
import flash.events.SQLEvent;
import flash.events.TimerEvent;
import flash.filesystem.File;
import flash.net.URLLoader;
import flash.net.URLRequest;
import flash.net.URLRequestHeader;
import flash.net.URLRequestMethod;
import flash.utils.Timer;

import mx.collections.ArrayCollection;
import mx.utils.Base64Encoder;

//Montastic URL
private static const montasticURL:String =
         "https://www.montastic.com/checkpoints/show";

// sqlconn holds the database connection
public var sqlconn:SQLConnection = new SQLConnection();
// sqlstmt is a SQLStatement that holds SQL commands
public var sqlstmt:SQLStatement = new SQLStatement();

// a bindable ArrayCollection and the data provider for the datagrid
[Bindable]
public var dp:ArrayCollection = new ArrayCollection();
[Bindable]
public var allStatuses:String = "1:UP#2:UP#3:UP";

private var urlList:Array = new Array();
private var statusList:Array = new Array();

Listing 6–20 contains various import statements, followed by variables for opening a database connection and executing SQL statements. The Bindable variables provide access to the contents of the database table, as well as the URL and status of websites that are registered with Montastic.

The variable checkpointsXMLList contains “live” data for the web sites that you have registered with Montastic.

// invoked after the application has loaded
private function start():void {
  // set 'montastic.db' as the file for our database (created after it's opened)
  var db:File = File.applicationStorageDirectory.resolvePath("montastic.db");

  // open the database in asynchronous mode
  sqlconn.openAsync(db);

  // event listeners for handling sql errors and 'result' are
  // invoked whenever data is retrieved from the database
  sqlconn.addEventListener(SQLEvent.OPEN, db_opened);
  sqlconn.addEventListener(SQLErrorEvent.ERROR, error);
  sqlstmt.addEventListener(SQLErrorEvent.ERROR, error);
  sqlstmt.addEventListener(SQLEvent.RESULT, result);
}

private function db_opened(e:SQLEvent):void {
  // specify the connection for the SQL statement
  sqlstmt.sqlConnection = sqlconn;

  // Table "montastic_table" contains three columns:
  // 1) id  (an autoincrementing integer)
  // 2) url (the url of each web site)
  // 3) status (the status of each web site)
  sqlstmt.text = "CREATE TABLE IF NOT EXISTS montastic_table ( id INTEGER PRIMARY KEY
AUTOINCREMENT, url TEXT, status TEXT);";

  // execute the sqlstmt to update the database
  sqlstmt.execute();

  // refresh the datagrid to display all data rows
  refreshDataGrid();
}

The methods start() and db_opened() are similar to the example earlier in this chapter, except that the database name is now montastic.db and the database table montastic_table is updated when users tap the associated Button in the mobile application. Note that the montastic_table contains the columns id, url, and status instead of the columns id, first_name, and last_name.

// function to append new rows to montastic table
// use a begin/commit block to insert multiple rows
private function addWebsiteInfo():void {
  allStatuses = "";
  sqlconn.begin();

  for (var i:uint = 0; i < urlList.length; i++) {
     var stmt:SQLStatement = new SQLStatement();
     stmt.sqlConnection = sqlconn;

     stmt.text = "INSERT INTO montastic_table (url, status) VALUES(:url, :status);";
     stmt.parameters[":url"] = urlList[i];
     stmt.parameters[":status"] = statusList[i];

     stmt.execute();
  }

  // insert the rows into the database table
  sqlconn.commit();

  refreshDataGrid();
}

// refresh the Montastic data in the datagrid
private function refreshDataGrid(e:TimerEvent = null):void {
   // timer object pauses and then attempts to execute again
   var timer:Timer = new Timer(100,1);
   timer.addEventListener(TimerEvent.TIMER, refreshDataGrid);

   if (!sqlstmt.executing) {
       sqlstmt.text = "SELECT * FROM montastic_table"
       sqlstmt.execute();
   } else {
       timer.start();
   }
}

// invoked when we receive data from a sql command
//this method is also called for sql statements to insert items
// and to create our table but in this case sqlstmt.getResult().data
// is null
private function result(e:SQLEvent):void {
   var data:Array = sqlstmt.getResult().data;

   // fill the datagrid with the latest data
   dp = new ArrayCollection(data);
}

// remove all rows from the table
private function removeAll():void {
  sqlstmt.text = "DELETE FROM montastic_table";
  sqlstmt.execute();

  refreshDataGrid();
}

The method addWebsiteInfo() is the “counterpart” to the addPerson() method, and the database insertion is performed inside a begin/end block in order to perform multiple row insertions in one SQL statement. This technique enables us to use the same logic that is used by the two methods refreshDataGrid() and result() for retrieving the latest data from the database without contention errors.

Note that instead of the method remove(), which removes a selected row from the datagrid, we now have a method, removeAll(), that removes all the rows from the database table.

// functions for Montastic
public function invokeMontastic():void {
   var loader:URLLoader = new URLLoader();
   loader.addEventListener(Event.COMPLETE, completeHandler);
   loader.addEventListener(IOErrorEvent.IO_ERROR, ioErrorHandler);

   var request:URLRequest = new URLRequest( montasticURL );
   request.method = URLRequestMethod.GET;

   var encoder:Base64Encoder = new Base64Encoder();
   encoder.encode("[email protected]:insert-your-password-here");
   request.requestHeaders.push(new URLRequestHeader("Authorization",
                                                    "Basic " + encoder.toString()));
   request.requestHeaders.push(new URLRequestHeader("pragma", "no-cache"));
   request.requestHeaders.push(new URLRequestHeader("Accept",
                                                    "application/xml"));
   request.requestHeaders.push(new URLRequestHeader("Content-Type",
                                                    "application/xml"));
   loader.load(request);
}
private function completeHandler(event:Event):void {
   var loader:URLLoader = URLLoader(event.target);
   checkpointsXMLList = new XML(loader.data);

   urlList = new Array();
   statusList = new Array();

   for each (var checkpoint:XML in checkpointsXMLList.checkpoint) {
      statusList.push(checkpoint.status.toString());
      urlList.push(checkpoint.url.toString());

   }

   allStatuses = "1="+statusList[0]+"#2="+statusList[1];

   addWebsiteInfo();
}

When users tap on the associated Button (whose label is “Get Statuses”), the method invokeMontastic() is executed, which in turn invokes the Montastic APIs that return XML containing status-related information regarding the websites that the users have registered with Montastic.

Notice that the method completeHandler() is invoked after the asynchronous request to the Montastic web site has returned the XML-based data.

The allStatuses variable is updated appropriately (we need to send this string to the server socket), and then the method addWebsiteInfo() is executed, which updates the database table montastic_table with the data that we received from Montastic.

private function ioErrorHandler(event:IOErrorEvent):void {
   trace("IO Error" + event);
}
private function sqlError(event:SQLErrorEvent):void {
   trace("SQL Error" + event);
}

The functions ioErrorHandler() and sqlError() are invoked when a related error occurs, and in a production environment, you can add additional error messages that provide helpful debugging information.

As you saw earlier, we are using a hard-coded XML string that contains a sample of the information for websites that you have registered with Montastic. Currently you can retrieve the XML-based status information about your websites by invoking the “curl” program from the command line, as shown here (invoked as a single line):

curl -H 'Accept: application/xml' -H 'Content-type: application/xml' -u
[email protected]:yourpassword https://www.montastic.com/checkpoints/index

Now that we have discussed the AIR-specific code, let's focus on the socket-based native Android code that processes the information from the client. The socket code is part of an Android application that we will name FooAndroid.

Before we discuss the Java code for this application, let's look at Listing 6–21, which contains the file AndroidManifest.xml for our Android application. Note that Listing 6–21 displays the final version of this configuration file, and not the contents that are generated during the creation step of the Android project FooAndroid.

Listing 6–21. AndroidManifest.xml for the Native Android Application

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
          package="com.proandroidflash"
          android:versionCode="1"
          android:versionName="1.0">

      <uses-permission android:name="android.permission.INTERNET" />

     <application android:icon="@drawable/icon"
                  android:label="@string/app_name">
      <activity android:name=".MainApp"
                android:label="@string/app_name"
                android:theme="@android:style/Theme.NoTitleBar"
                android:launchMode="singleTask"
                android:screenOrientation="nosensor"
                android:configChanges="keyboardHidden|orientation"
                android:windowSoftInputMode="stateHidden|adjustResize">
            <uses-permission android:name="android.permission.INTERNET" />
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        <service android:enabled="true" android:name="TestService" />
    </application>
</manifest>

Listing 6–21 specifies MainApp.java as the Android Activity for our Android application. As you will see, the Java class MainApp.java (which contains some custom Java code) extends the Android ActivityAppEntry.java that is a subclass of the Android Activity class. Notice that Listing 6–21 specifies an Android Service class called TestService.java, which contains socket-based custom code that processes information that is received from the Adobe AIR client.

Now create a native Android application in Eclipse called FooAndroid with a Java class MainApp that extends the class AppEntry. The Java class AppEntry.java is a simple, pre-built Java Activity that is an intermediate class between the Android Activity class and our custom Java class, MainApp. Listing 6–22 displays the contents of the Java class MainApp.

Listing 6–22. Main Android Activity Class

package com.proandroidflash;

import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;

public class MainApp extends AppEntry {
    /** Called when the activity is first created. */

    @Override
    public void onCreate(Bundle savedInstanceState)
    {
      super.onCreate(savedInstanceState);

      try {
          Intent srv = new Intent(this, TestService.class);
          startService(srv);
      }
      catch (Exception e)
      {
          // service could not be started
      }
    }
}

The Java class MainApp (which is an indirect subclass of the Android Activity class) is executed when our Android application is launched; the onCreate() method in MainApp launches our custom Java class TestService.java (discussed later) that launches a server-side socket in order to handle data requests from the Adobe AIR client.

As you can see, the onCreate() method invokes the startService() method that is a method in the Android Activity class, in order to launch the TestService Service. This functionality is possible because MainApp is a subclass of the Android Activity class.

Now create the second Java class, TestServiceApp.java, in the com.proandroidflash package, and insert the code in Listing 6–23.

Listing 6–23. An Android Service Class That Processes Data from an AIR Client

package com.proandroidflash;

import java.io.BufferedInputStream;
import java.io.DataInputStream;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;

import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.os.IBinder;
import android.os.Looper;
import android.util.Log;

public class TestService extends Service {
  private boolean stopped=false;
  private Thread serverThread;
  private ServerSocket ss;

  @Override
  public IBinder onBind(Intent intent) {
    return null;
  }
  @Override
  public void onCreate() {
     super.onCreate();

     Log.d(getClass().getSimpleName(), "onCreate");

     serverThread = new Thread(new Runnable() {
          public void run() {
             try {
                Looper.prepare();
                ss = new ServerSocket(12345);
                ss.setReuseAddress(true);
                ss.setPerformancePreferences(100, 100, 1);

                while (!stopped) {
                   Socket accept = ss.accept();
                   accept.setPerformancePreferences(10, 100, 1);
                   accept.setKeepAlive(true);

                   DataInputStream _in = null;

                   try {
                       _in = new DataInputStream(new BufferedInputStream(
                             accept.getInputStream(),1024));
                   }
                   catch (IOException e2) {
                     e2.printStackTrace();
                   }

                   int method = _in.readInt();

                   switch (method) {
                     // send a notification?
                     case 1: doNotification(_in);
                             break;
                   }
                }
             }
             catch (Throwable e) {
                e.printStackTrace();
                Log.e(getClass().getSimpleName(), "** Error in Listener **",e);
             }

               try {
                 ss.close();
               }
               catch (IOException e) {
                  Log.e(getClass().getSimpleName(), "Could not close serversocket");
               }
            }
       },"Server thread");

     serverThread.start();
  }

The initial portion of FooAndroid contains various import statements, private socket-related variables, and the onBind() method. This method can be used for other functionality that is supported by Android Service classes (which is beyond the scope of this example), and for our purposes this method simply returns null.

The next portion of Listing 6–23 contains a lengthy onCreate() method, which starts a server-side socket for handling Adobe AIR client requests.

The onCreate() method starts a Java Thread whose run() method launches a server-side socket on port 12345 (which is the same port as the client-side socket). The onCreate() method contains a while loop that waits for client-side requests and then processes them in the try/catch block.

If the first character in a client-side request is the digit “1”, then we know that the client request is from our AIR application, and the code in the try/catch block invokes the method doNotification(). If necessary, you could enhance onCreate() (i.e., handle the occurrence of other numbers, text strings, and so forth) so that the server-side code can process other client-side requests.

  private void doNotification(DataInputStream in) throws IOException {
    String id = in.readUTF();
    displayNotification(id);
  }
  public void displayNotification(String notificationString)
  {
    int icon = R.drawable.mp_warning_32x32_n;

    CharSequence tickerText = notificationString;
    long when = System.currentTimeMillis();
    Context context = getApplicationContext();
    CharSequence contentTitle = notificationString;
    CharSequence contentText = "Hello World!";

    Intent notificationIntent = new Intent(this, MainApp.class);
    PendingIntent contentIntent = PendingIntent.getActivity(this, 0,
                                                   notificationIntent, 0);
    Notification notification = new Notification(icon, tickerText, when);
    notification.vibrate = new long[] {0,100,200,300};

    notification.setLatestEventInfo(context, contentTitle,
                                    contentText, contentIntent);

    String ns = Context.NOTIFICATION_SERVICE;
    NotificationManager mNotificationManager =
                          (NotificationManager) getSystemService(ns);

    mNotificationManager.notify(1, notification);
  }
  @Override
  public void onDestroy() {
     stopped = true;
     try {
        ss.close();
     }
     catch (IOException e) {}

     serverThread.interrupt();

     try {
        serverThread.join();
     }
     catch (InterruptedException e) {}
  }
}

The doNotification() method simply reads the next character string in the input stream (which was sent from by the client) and then invokes the method displayNotification(). In our case, this character string will be a concatenated string that contains the status (“UP” or “DOWN”) for each registered web site.

As you have probably surmised, the displayNotification() method contains the Android code for displaying a notification in the Android notification bar. The key point to notice is that this method creates an Android Intent with the Java class MainApp.java. The new Intent enables us to create an Android PendingIntent, which in turn allows us to create an Android Notification instance. The final line of code in displayNotification() launches our notification, which displays the status of the registered web sites in the Android notification bar.

The last part of the code is the onDestroy() method, which stops the server-side socket that was launched on the onCreate() method.

Now that we have completed the Java code for this application, we need to take care of the XML-related files for this application. First, make sure that the contents of AndroidManifest.xml in your application are the same as Listing 6–21. Second, include the following strings in the XML file strings.xml:

<string name="button_yes">Yes</string>
<string name="button_no">No</string>
<string name="dialog_title"><b>Adobe AIR</b></string>
<string name="dialog_text">This application requires that you first install Adobe
AIR®. Download it free from Android Market now?</string>

Now that we have completed all the Adobe AIR code and the native Android code, we're ready to merge the files into one mobile application, and the steps for doing so are shown in the next section in this chapter.

You can see an example of invoking the sample application in this section displayed in Figure 6–6, which shows a set of records consisting of URLs and their statuses that are stored in a SQLite database on a mobile device.

images

Figure 6–6. A set of status records for registered web sites

Adobe AIR and Native Android Integration

This section contains a process by which you can integrate native Android functionality (such as the examples in this chapter) into an Adobe AIR mobile application. Please note that the process by which this can be done is not supported by Adobe, and the actual steps could change by the time this book is published. This information was provided courtesy of Elad Elrom.

The commands in Listing 6–24 use the utilities adt, apktool, and adb to merge the contents of an Adobe AIR application, MyAIRApp, with the contents of a native Android application, AndroidNative.apk, in order to create the Adobe AIR mobile application MergedAIRApp.apk.

Listing 6–24 displays the actual commands that you need to invoke in order to create a new .apk file that comprises the code from an Adobe AIR mobile application and a native Android mobile application. Make sure that you update the value of the variable APP_HOME so that it reflects the correct value for your environment.

Listing 6–24. Creating a Merged Application with Adobe AIR and Native Android Code

APP_HOME="/users/ocampesato/AdobeFlashBuilder/MyAIRApp"
cd $APP_HOME/bin-debug
adt -package -target apk -storetype pkcs12 -keystore certificate.p12 -storepass Nyc1982 out.apk MyAIRApp-app.xml MyAIRApp.swf
apktool d -r out.apk air_apk
apktool d -r AndroidNative.apk native_apk
mkdir native_apk/assets
cp -r air_apk/assets/* native_apk/assets
cp air_apk/smali/app/AIRApp/AppEntry*.smali native_apk/smali/app/AIRApp
apktool b native_apk
cd native_apk/dist
jarsigner -verbose -keystore ~/.android/debug.keystore -storepass android out.apk androiddebugkey
zipalign -v 4 out.apk out-new.apk
cd ../../
cp native_apk/dist/out-new.apk MergedAIRApp.apk
rm -r native_apk
rm -r air_apk
rm out.apk
adb uninstall app.AIRApp
adb install -r MergedAIRApp.apk

The commands in Listing 6–24 are straightforward if you are familiar with Linux or Unix. If you prefer to work in a Windows environment, you can convert the commands in Listing 6–24 to a corresponding set of DOS commands by making the following changes:

  • Use “set” when defining the APP_HOME variable.
  • Use DOS-style variables (example: %abc% instead of $abc).
  • Replace cp with copy and replace rm with erase.
  • Replace forward slashes (“/”) with backward slashes (“”).

One important detail to keep in mind: you must obtain the correct self-signed certificate (which is called certificate.p12 in Listing 6–24) for the preceding merging process to work correctly. You can generate a certificate for your Flex-based application as follows:

  • Select your project in FlashBuilder.
  • Click Export Release Build.
  • Specify a location for “Export to folder:” (or click Next).
  • Click the “Create:” button.
  • Provide values for the required fields.
  • Specify a value in the “Save as:” input field.
  • Click the OK button.
  • Click “Remember password for this session” (optional).
  • Click the Finish button.

After generating the self-signed certificate, copy this certificate into the directory where you execute the shell script commands that are shown in Listing 6–24, and if you have done everything correctly, you will generate a merged application that can be deployed to an Android-based mobile device.

This section concludes the multi-step process for creating and then merging Adobe AIR applications with native Android applications. As you can see, this integration process is non-trivial, and arguably non-intuitive as well, so you are bound to stumble during this process (and don't be discouraged when you do). The thing to remember is that the addition of native Android support to an Adobe AIR application could be the sort of thing that can differentiate your application from similar applications that are available in the marketplace.

Summary

In this chapter, you learned how to launch native browsers, access databases, and combine AIR mobile applications with native Android code. More specifically, you learned about the following:

  • Launching a native web browser from a string containing the actual HTML code for a web site
  • Enabling users to specify a URL and then launching that URL in an AIR application
  • Using a SQLite database to create, update, and delete user-specific data, and also how to automatically refresh the display of the updated data
  • Creating native Android applications in Eclipse
  • Integrating the functionality of an external API, a SQLite database, and a native Android notification into one mobile application

The specific sequence of steps for merging an Adobe AIR application with a native Android application.

____________________

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.216.47.169