Pages

Saturday 15 December 2012

Modular Approach to Implementing Android Preferences

This article details the solution I engineered to implement a modular approach for handling Android preferences. The objective is to display common preferences in an Android library on the same screen as custom preferences that are specific to the game (app) that uses that library.

I came up with this solution because I am building an augmented reality game engine library containing common code that I plan to use with multiple game projects. On the options screen I want to display both preferences common to all games (and thus the code was in the library itself) and also preferences specific to each game with an efficient way of implementing it in the game project with a minimal amount of coding.

The end result is illustrated in the following image.


The preference category titles and preference widgets (the checkboxes) that are in the red squares are settings common to all games, and thus are implemented in the game library. The green squares are preferences/titles that are specific to that particular game, and thus are implemented in the game project code.

I will explain below the important files in the game and game library projects. Download links for both projects are available at the bottom.

The Game Project


First we have the the Java class implementation that extends the AbstractOptionsScreenActivity abstract class in the game library which is where most of the Java code is. By just calling the super.onCreate method all the hard work is done for us as soon as the activity is created.

Optionally, you may implement some code in the abstract addExtraCustomPreferenceResources like I have here. This example will add a new CustomPreferenceScreen object to the array list that is referenced when the list of preferences is created. The object contains the file name of an XML resource file that defines a preference screen and the title of the preference category under which all the preferences inside the resource file will be added to.
public class OptionsScreenActivityImpl extends AbstractOptionsScreenActivity {
 @Override
 public void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
 }

 @Override
 protected void addExtraCustomPreferenceResources() {
  mCustomPreferenceScreensList.add(new CustomPreferenceScreen("test_preferences", "My New Category"));
 }
}

The above code tells the library that the game project has a test_preferences.xml file in the project's res/xml folder and to add all its children preferences under a new preference category titled "My New Category". The current implementation of the game library adds 4 default preference categories - Game Settings, Graphics, Audio and Reporting Problems - to the list. Any new preference categories defined in addExtraCustomPreferenceResources will be rendered at the bottom of the list. The order that the CustomPreferenceScreen objects are added to the list will reflect the order they are appended to the screen.

<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android" >

    <CheckBoxPreference
        android:defaultValue="false"
        android:key="test key"
        android:summary="test summary"
        android:title="test title" >
    </CheckBoxPreference>

</PreferenceScreen>

The 2 preference categories, Game Settings and Graphics, are created by the library but it is possible to define preference widgets in the XML resource files in the game project as this example does. There are files with the same name in the game library project but the game project's versions take precedence when the game runs. The game library will add the preference widgets defined in the game project's implementation of these files.

<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android" >

    <CheckBoxPreference
        android:defaultValue="false"
        android:key="options_screen_prefs_description_game_setting"
        android:summary="@string/options_screen_prefs_description_game_setting"
        android:title="@string/options_screen_prefs_name_game_setting" >
    </CheckBoxPreference>

</PreferenceScreen>

<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android" >

    <!-- My Game specific graphic setting 1 -->

    <CheckBoxPreference
        android:defaultValue="false"
        android:key="options_screen_prefs_description_game_graphics1"
        android:summary="@string/options_screen_prefs_description_game_graphics1"
        android:title="@string/options_screen_prefs_name_game_graphics_setting1" >
    </CheckBoxPreference>

    <!-- My Game specific graphic setting 2 -->

    <CheckBoxPreference
        android:defaultValue="false"
        android:key="options_screen_prefs_description_game_graphics2"
        android:summary="@string/options_screen_prefs_description_game_graphics2"
        android:title="@string/options_screen_prefs_name_game_graphics_setting2" >
    </CheckBoxPreference>

</PreferenceScreen>


The Game Library Project


The root_preferences.xml defines the top level preference screen on the options screen. Here we define the 4 default preference categories (Game Settings, Graphics, Audio and Reporting Problems). Note that the Game Settings doesn't have any common preference widgets defined in the library (but it would be okay to add some). The Graphics has the single "enable Augmented Reality" checkbox preference widget. Both categories have other widgets added that are defined in the game project. The other categories, Audio and Reporting Problems, are common to all games and are not modified by the game project at all.
<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android" >

    <!-- Game settings -->

    <PreferenceCategory
        android:key="pref_key_game_settings"
        android:title="@string/options_screen_prefs_category_title_game_settings" >
    </PreferenceCategory>

    <!-- Graphics settings -->

    <PreferenceCategory
        android:key="pref_key_graphics_settings"
        android:title="@string/options_screen_prefs_category_title_graphics" >

        <!-- Enable augmented reality display -->

        <CheckBoxPreference
            android:defaultValue="false"
            android:key="pref_key_graphics_settings_ar_enable"
            android:summary="@string/options_screen_prefs_description_ar"
            android:title="@string/options_screen_prefs_name_ar" >
        </CheckBoxPreference>
    </PreferenceCategory>

    <!-- Audio settings -->

    <PreferenceCategory
        android:key="pref_key_audio_settings"
        android:title="@string/options_screen_prefs_category_title_audio" >

        <!-- Enable sound effects -->

        <CheckBoxPreference
            android:defaultValue="false"
            android:key="pref_key_audio_settings_sound_effects_enable"
            android:summary="@string/options_screen_prefs_description_sound_effects"
            android:title="@string/options_screen_prefs_name_sound_effects" >
        </CheckBoxPreference>

        <!-- Enable background music -->

        <CheckBoxPreference
            android:defaultValue="false"
            android:key="pref_key_audio_settings_background_music_enable"
            android:summary="@string/options_screen_prefs_description_background_music"
            android:title="@string/options_screen_prefs_name_background_music" >
        </CheckBoxPreference>
    </PreferenceCategory>

    <!-- Reporting problems settings -->

    <PreferenceCategory
        android:key="pref_key_reporting_problems_settings"
        android:title="@string/options_screen_prefs_category_title_reporting_problems" >

        <!-- Enable automatic error reporting -->

        <CheckBoxPreference
            android:defaultValue="false"
            android:key="pref_key_reporting_problems_settings_automatic_error_reporting_enable"
            android:summary="@string/options_screen_prefs_description_automatic_error_reporting"
            android:title="@string/options_screen_prefs_name_automatic_error_reporting" >
        </CheckBoxPreference>

        <!-- Enable trace logging -->

        <CheckBoxPreference
            android:defaultValue="false"
            android:key="pref_key_reporting_problems_settings_trace_logging_enable"
            android:summary="@string/options_screen_prefs_description_trace_logging"
            android:title="@string/options_screen_prefs_name_trace_logging" >
        </CheckBoxPreference>
    </PreferenceCategory>

</PreferenceScreen>
The abstract class below is where the bulk of the Java code is. The in-code comments in conjunction with running the actual projects in debug mode will be the best way to understand the finer details of how it all works but in brief:

  • The game runs its activity which calls its abstract parent's constructor
  • That constructor sets a custom layout with a ListView which will be the root view in the preferences hierarchy.
  • An array list is initialised containing the custom preferences that are to be added to the options screen. The 2 XML resource file names for Game Settings and Graphics and their associated preference categories keys are defined here.
  • The way preferences should be handled on Android depends on what version of Android is being used; either use a PreferenceActivity for older versions or PreferenceFragment since Honeycomb (Android 3.0). This solution handles both old and new methods automatically.
  • The root preference screen is added.
  • The list of custom preference screens is iterated.
  • The defined XML resource file containing the preference screen is inflated and instantiated as a PreferenceScreen object. Because the Android API hides the method for inflating preference XML files, Java reflection is used.
  • If the custom preference screen has a corresponding category name defined, then the root preference screen is scanned for an already existing preference category with a key that matches that name. If a match is found, then all the children preference widgets in the XML resource file are appended to the existing preference category.
  • If no match is found, then a new preference category is created and appended to the end of the root preference screen list. If a corresponding category name is defined, then the title of the new preference category will be set to that.
  • The result is all preference screens/categories/widgets are merged together and rendered as one logical screen.
 
public abstract class AbstractOptionsScreenActivity extends PreferenceActivity {
 private final static String TAG = "GameLib";
 
 /**
  * Encapsulation class for a single custom preference XML resource and the optional {@link PreferenceCategory}
  * that its inner {@link Preference} widgets are located in.
  */
 protected class CustomPreferenceScreen {
  /** Name of the XML resource file that contains the custom preference screen */
  public String xmlResFilename;
  
  /**
   * If the name matches the key of an existing preference category in the root preference screen,
   * then the custom preference screen's contents are appended here. Otherwise, a new preference
   * category with the title set to the value of this name will be created and the contents added
   * there.
   */
  public String preferenceCategoryName;

  /**
   * Encapsulating class for an XML resource file that contains custom preferences implemented in the
   * game project that is using this library.
   * 
   * @param xmlResFilename The XML resource filename in the res/xml directory.
   * @param categoryName The key name of the existing preference category to add to, or the title value of
   * the new preference category to create. Can be null (and will append a new preference category with no title).
   */
  public CustomPreferenceScreen(String xmlResFilename, String categoryName) {
   this.xmlResFilename = xmlResFilename;
   this.preferenceCategoryName = categoryName;
  }
 }

 /**
  * Array list of {@link CustomPreferenceScreen}s for containing the list of custom preference screen to show on
  * the options screen.
  */
 protected ArrayList<CustomPreferenceScreen> mCustomPreferenceScreensList;

 private RootPreferencesFragment mRootPreferencesFragment;

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

  setContentView(R.layout.preferences_screen_layout);

  loadPreferences();
 }

 @Override
 public void onDestroy() {
  super.onDestroy();

  if (mRootPreferencesFragment != null) {
   // Avoid memory leaks with stale context references
   mRootPreferencesFragment.mContext = null;
  }
 }

 /**
  * Implement this method to add extra preference screen resources to the list that is referenced when
  * rendering the options screen.<br>
  * <br>
  * List is {@link #mCustomPreferenceScreensList} and element objects to insert are {@link CustomPreferenceScreen}.
  */
 protected abstract void addExtraCustomPreferenceResources();

 protected final void loadPreferences() {
  // Initialise the list of custom preference screen resources.
  // At a minimum there are game_preferences.xml and graphics_preferences.xml whose contents should
  // be implemented (i.e. the game specific preferences defined) in the game project using the library.
  mCustomPreferenceScreensList = new ArrayList<CustomPreferenceScreen>();

  // Map that the preferences defined in games_preferences.xml belong in the preference category with
  // the key pref_key_game_settings
  mCustomPreferenceScreensList.add(new CustomPreferenceScreen("game_preferences", "pref_key_game_settings"));

  // Map that the preferences defined in graphics_preferences.xml belong in the preference category with
  // the key pref_key_graphics_settings
  mCustomPreferenceScreensList.add(new CustomPreferenceScreen("graphics_preferences", "pref_key_graphics_settings"));

  // Optionally, any subclass may implement the following method to add further add entries to the list so
  // we invoke that method here to ensure they're loaded in.
  addExtraCustomPreferenceResources();

  // The way to handle preferences changed since API 11 (Honeycomb). From API 11 onwards, it is recommended to use
  // PreferenceFragment inside of a normal Activity instead of PreferenceActivity. Here we will handle both for the
  // older Android APIs and the new ones.
  // Source: http://developer.android.com/guide/topics/ui/settings.html
  if (isFragmentSupported()) {
   loadPreferencesWithPreferenceFragment();
  } else {
   loadPreferencesWithPreferenceActivity();
  }
 }

 @SuppressWarnings("deprecation")
 private void loadPreferencesWithPreferenceActivity() {

  Log.d(TAG, "Loading preferences with the old preference activity method");

  // Load the root preferences using PreferenceActivity (which this class subclasses).
  // These preferences are common to all games that use the library.
  addPreferencesFromResource(R.xml.root_preferences);

  // Render the custom preference screens and their widgets
  addCustomPreferenceScreens(this, getPreferenceScreen(), mCustomPreferenceScreensList);
 }

 @TargetApi(11)
 private void loadPreferencesWithPreferenceFragment() {
  Log.d(TAG, "Loading preferences with the new preference fragment method");

  // Instantiate the root preferences fragment
  mRootPreferencesFragment = new RootPreferencesFragment();
  mRootPreferencesFragment.mContext = this;
  mRootPreferencesFragment.mCustomPreferenceScreenResourcesList = this.mCustomPreferenceScreensList;
  getFragmentManager().beginTransaction().replace(android.R.id.content, mRootPreferencesFragment).commit();
 }

 private static void addCustomPreferenceScreens(
   Context context,
   PreferenceScreen rootPreferenceScreen,
   ArrayList<CustomPreferenceScreen> customPreferenceScreensList) {
  if (context == null || rootPreferenceScreen == null || customPreferenceScreensList == null) {
   return;
  }

  // Iterate over the list of custom preference screens
  for (CustomPreferenceScreen customPrefScreen : customPreferenceScreensList) {
   if (customPrefScreen != null && customPrefScreen.xmlResFilename != null) {
    // Reference to the preference category to place the preference widgets in
    PreferenceCategory currentPreferenceCategory = null;

    // Search for a category matching the name of the specified key
    for (int i = 0; i < rootPreferenceScreen.getPreferenceCount(); i  ) {
     // Get the current preference object in the root preference screen's hierarchy
     Preference currentRootPreferenceScreenPreference = rootPreferenceScreen.getPreference(i);

     // Check if current root preference screen preference is a preference category and if its key matches the
     // key we were specified
     if (currentRootPreferenceScreenPreference != null && currentRootPreferenceScreenPreference instanceof PreferenceCategory) {
      if (((PreferenceCategory) currentRootPreferenceScreenPreference).getKey() != null
        && ((PreferenceCategory) currentRootPreferenceScreenPreference).getKey().equals(customPrefScreen.preferenceCategoryName)) {
       // A match - this is the preference category where to add our custom preferences
       currentPreferenceCategory = (PreferenceCategory) currentRootPreferenceScreenPreference;
       break;
      }
     }
    }

    // Otherwise create a new preference category object
    if (currentPreferenceCategory == null) {
     // Instantiate a new preference category to hold the custom preference screen
     currentPreferenceCategory = new PreferenceCategory(context);
     currentPreferenceCategory.setTitle(customPrefScreen.preferenceCategoryName);
     
     // Add the new preference category to the end of the root preference screen
     rootPreferenceScreen.addPreference(currentPreferenceCategory);
    }

    // Add the contents of of the custom preference screen to the preference category
    int resId = context.getResources().getIdentifier(customPrefScreen.xmlResFilename, "xml", context.getPackageName());
    PreferenceScreen resCustomPreferenceScreen = inflatePreferenceScreenFromResource(context, resId);
    int order = currentPreferenceCategory.getPreferenceCount() - 1; // offset by the number of preferences already in the group so custom preferences are appended
    if (resCustomPreferenceScreen != null) {
     for (int j = 0 ; j < resCustomPreferenceScreen.getPreferenceCount(); j  ) {
      Preference preferenceWidget = resCustomPreferenceScreen.getPreference(j);
      preferenceWidget.setOrder(  order);
      currentPreferenceCategory.addPreference(preferenceWidget);
     }
    }
   }
  }
 }

 /**
  * Subclassed {@link PreferenceFragment} to load preferences for Android devices running 3.0 (Honeycomb API 11) and up.
  *
  */
 @TargetApi(11)
 public static class RootPreferencesFragment extends PreferenceFragment {
  protected Context mContext;
  protected ArrayList<CustomPreferenceScreen> mCustomPreferenceScreenResourcesList;

  public RootPreferencesFragment() {
  }

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

   // Load the root preferences using PreferenceFragment.
   // These preferences are common to all games that use the library.
   addPreferencesFromResource(R.xml.root_preferences);

   // Render the custom preference screens and their widgets
   addCustomPreferenceScreens(mContext, getPreferenceScreen(), mCustomPreferenceScreenResourcesList);
  }
 }

 /**
  * Inflates a {@link android.preference.PreferenceScreen PreferenceScreen} from the specified
  * resource.<br>
  * <br>
  * The resource should come from {@code R.xml}.
  * 
  * @param context The context.
  * @param resId The ID of the XML file.
  * @return The preference screen or null on failure.
  */
 protected static PreferenceScreen inflatePreferenceScreenFromResource(Context context, int resId) {
  try {
   // The Android API doesn't provide a publicly available method to inflate preference
   // screens from an XML resource into a PreferenceScreen object so we use reflection here
   // to get access to PreferenceManager's private inflateFromResource method.
   Constructor<PreferenceManager> preferenceManagerCtor = PreferenceManager.class.getDeclaredConstructor(Context.class);
   preferenceManagerCtor.setAccessible(true);
   PreferenceManager preferenceManager = preferenceManagerCtor.newInstance(context);
   Method inflateFromResourceMethod = PreferenceManager.class.getDeclaredMethod(
     "inflateFromResource", Context.class, int.class, PreferenceScreen.class);
   return (PreferenceScreen) inflateFromResourceMethod.invoke(preferenceManager, context, resId, null);
  } catch (Exception e) {
   Log.w(TAG, "Could not inflate preference screen from XML resource ID "   resId, e);
  }

  return null;
 }
 
 /**
  * Convenience method to check whether the device's Android version >= API 11 (3.0 )
  * as the {@link android.support.v4.app.Fragment} API is only available since API 11.
  * 
  * @return true if Fragment and associated classes are supported, false otherwise
  */
 public static boolean isFragmentSupported() {
  if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.HONEYCOMB) {
   return true;
  }
  return false;
 }
}
Download Eclipse Game Project and Game Library Project.

Saturday 10 November 2012

Sharing a Single Eclipse Workspace on 2 Computers via Dropbox

I use 2 computers for development; my main office desktop running Windows 7 and my MacBook Air running OS X. When in my office I prefer the desktop's big keyboard and display for working on, but then I like to be able just to grab my Air and take it with me and be able to continue development on that somewhere else like the nearby beach cafe, the lounge for coding while watching TV etc without having to manually copy files and such each time I switch between computers.

I came up with a little hack to allow me to relatively easily share my Eclipse workspace using Dropbox. For those who don't know, Dropbox is a serivce that allows to you to automatically sync files like documents and photos across all your devices; computers, smartphones etc.

The major caveat of sharing a single Eclipse workspace across two different computers with two different operating systems is many of the workspace's settings are path dependent relative to the host operating system (think Windows \ versus OS X/Linux /). All these configurations are saved in the .metadata folder within the top of your workspace. I got around these issues by having two copies of this folder, one for the Windows based workspace and one for the OS X based workspace. A wrapper script copies the relevant folder before starting Eclipse and backs up a copy to save any new changes after Eclipse exits. This approach means you cannot have both Eclipse's running simultaneously on both machines otherwise the workspace's configuration may get corrupted.

Generally any changes made to a project on one machine will then be reflected on the other machine as well. Try to avoid using hardcoded paths when configuring the build paths for a project. Instead use Eclipse's User Libraries as they will be saved unique to each operating system's workspace settings.

Some people may say why bother with such a setup like this and instead just use a version control system. I am intending to also use Git for my projects but for tasks that aren't ready to be committed I think this Dropbox approach is fine to allow semi-seamless transition between two development machines.

Steps to Share an Eclipse Workspace On Windows and OS X Machines

These steps should be applicable to a Windows/Linux combination as well, substituting Linux in place of OS X.
  1. Install the same version of Eclipse separately on each machine.
  2. Sign up to Dropbox and install the client software on both computers.
  3. Make a folder in your dropbox for your workspace (e.g. C:\User\soliax\Dropbox\eclipse-workspace or /Users/soliax/Dropbox/eclipse-workspace). If Dropbox is setup properly, you'll only have to create it on one computer and then it will automatically be sync'd (created) on the other computer.
  4. Launch Eclipse on one of your computers, lets say Windows, and specify the location of your workspace folder. Optionally, you may setup workspace configurations at this time like installing any plugins, setting classpaths etc. Exit Eclipse when you're finished.
  5. With Eclipse closed, rename the .metadata folder to .metadata-windows.
  6. Repeat steps 4. and 5. but on your OS X computer and rename the folder to -osx, not -windows.
  7. Create a batch script on your Windows computer and save it as something like eclipse-dropbox.bat in the Eclipse installation folder. This is the wrapper script to start Eclipse. Copy/paste the following contents in. Configure the WORKSPACE variable to point to your actual workspace folder in your dropbox.
    @echo off
    REM Eclipse startup script for workspaces shared amongst two computers on Dropbox.
    REM Set path of your Eclipse workspace
    set WORKSPACE=%USERPROFILE%\Dropbox\Dev\workspaces\aroha
    echo Restoring Windows metadata for Eclipse...
    @call xcopy /HIEQRY "%WORKSPACE%\.metadata-windows" "%WORKSPACE%\.metadata"
    call eclipse.exe
    echo Backing up Windows metadata for Eclipse...
    @call xcopy /HIEQRY "%WORKSPACE%\.metadata" "%WORKSPACE%\.metadata-windows"
    
  8. If you want to pin this script to your Windows taskbar, try the following steps:
    1. Rename the *.bat script to *.exe. Then drop the icon on to your taskbar. For some silly reason Windows doesn't allow bat/cmd files to easily be pinned.
    2. Rename the script back to *.bat.
    3. Right-click the pinned icon, then right click the filename at the top of the menu (e.g. eclipse-dropbox) and choose Properties from the next menu.
    4. Rename the .exe extension to .bat.
    5. Change the icon as well if you wish.
  9. Create a shell script on your OS X computer and save it in the anywhere (I used /usr/bin) with the name of something like eclipse-dropbox. This is the wrapper script to start Eclipse. Make sure you use the command chmod +x in a Terminal window or something similar to make the script executable. Copy/paste the following contents in.This assumes Eclipse is installed in your Applications folder. Configure the WORKSPACE variable to point to your actual workspace folder in your dropbox.
    #!/bin/sh
    # Eclipse startup script for workspaces shared amongst two computers on Dropbox.
    
    # Set path of your Eclipse workspace
    WORKSPACE=$HOME/Dropbox/Dev/workspaces/aroha
    
    # Copy the backup .metadata folder used for the OS X version of Eclipse to the actual working copy
    echo Restoring OS X metadata for Eclipse...
    rm -rf "${WORKSPACE}/.metadata"
    cp -Rfp "${WORKSPACE}/.metadata-osx" "${WORKSPACE}/.metadata"
    
    # Launch Eclipse
    open -W /Applications/Eclipse
    
    # Backup the working copy of .metadata for the OS X version of Eclipse when Eclipse closes
    echo Backing up OS X metadata for Eclipse...
    rm -rf "${WORKSPACE}/.metadata-osx"
    cp -Rfp "${WORKSPACE}/.metadata" "${WORKSPACE}/.metadata-osx"
    
  10. If you want to add the OS X shell script to your dock, use Automator to create an application that has a single job to execute a shell script and point it to the script you created in step 9. Then you can drop that application onto your dock.
Now use these wrappers to start Eclipse. Make sure Eclipse is closed on the other computer before starting it. As the workspaces are technically different, any changes you make to one you will need to also do to the other. If you open a project on Windows, it will not automatically mean that project will be open next time you launch the OS X workspace. Changes you make to source code files and project specific settings should automatically be reflected (assuming to allow enough time to transfer to/from Dropbox's servers).

Wednesday 17 October 2012

OS X Development Environment for Vuforia Android Apps + Streaming Video Playback Sample

Qualcomm has instructions on setting up an environment to develop Vuforia based apps for Android (for those of you unaware, Qualcomm's mobile augmented reality SDK was renamed from QCAR to Vuforia). It focuses on Windows platforms with some tips/hints for Linux and OS X. This blog article will focus on setting up the same kind of development environment but on OS X. The plan is to document as I give it ago and give a bit more in-depth detail. At the end I'll cover running the Video Playback sample app and modifying it slightly to stream video off of the internet. I'll also cover debugging native code from within Eclipse.

Components to Install

  • JDK (1.6.0_35)
  • Eclipse IDE for Java (Juno)
  • Android SDK (r20)
  • Android ADT (Eclipse plugin for r20)
  • Android NDK (r8b - native development kit )
  • Android CDT (C/C++ development tools)
  • Vuforia SDK (1.5.9 - Qualcomm's augmented reality SDK)
  • Xcode command line/developer tools

Java, Eclipse and Android SDK

The Qualcomm setup docs suggest to use Java 1.7 but I'm going to stick with the stock standard JDK that Apple bundled with my Mountain Lion (Java 1.6.0_u35-b10 on OS X 10.8.2). They also suggest to use the the 32-bit version of Eclipse for Mac but I've already got the 64-bit of Juno so am going to stick with that too.

There's already a plethora of info on setting up a standard Android development environment with the Eclipse IDE and Android SDK so I suggest either searching Google or going straight to the official Android docs site for help with this first part. At least, you will require the Android SDK, Eclipse for Java and ADT (the Eclipse Android Development Tool) plugin installed.

Note: install the NDK Plugins at the same time as you install the ADT plugin in Eclipse.


Android NDK Installation

The following instructions assume you already have a basic Android development environment setup with Eclipse.

At the native level, Android runs its programs that were originally written in C/C++ (and no doubt some ASM). The majority of Android apps are written purely in Java, running on on the Android-optimized JVM called Davlik. Using the NDK (native development kit), developers can also themselves write part of their apps in C/C++. Such apps would be a blend of Java and C/C++ glued together with JNI - the Java Native Interface which is the bridge between code written in the two languages.

Vuforia itself is written in C++. The SDK library was no doubt written in native code because of the increased performance for its critical features like the computer vision algorithms and OpenGL code.

In order to compile NDK code on OS X you'll need Xcode and its Command Line Tools installed for programs like the GNU compiler gcc and the build tool make.

  1. Download Xcode from here and once you've installed it goto Xcode -> Preferences -> Downloads and choose to install Command Line Tools (hopefully it's in your list). I am using Xcode 4.5.1. Alternatively, although I haven't tested it, you can try downloading and installing Xcode bundled with the Developer Tools package from here. In either case, you'll probably need to create a free developer account with Apple.
  2. Download the Android NDK for OS X from here and unzip the archive in the same folder as you installed the Android SDK. I installed version r8b under /Users/wocky/Documents/android/android-ndk-macosx.
  3. The installation path needs to be added to the environment variable called PATH. Do so by adding it to the end of the PATH configuration in ~/.bash_profile. For example: PATH=$PATH:/Users/wocky/Documents/android/android-sdk-macosx/platform-tools/:/Users/wocky/Documents/android/android-ndk-macosx; export PATH
  4. Change to the samples/san-angeles directory (under the NDK installation directory) and run the command ndk-build from a terminal window to ensure the path setting in step 3 is correct. You should see some output saying the San Angeles project's C files have been compiled and the samples/san-angeles/libs/armeabi/libsanangeles.so library created.
  5. Next we will install the CDT plugin for Eclipse. This is the C/C++ Development Tool plugin. Goto Help -> Install Software and choose the URL of the version that matches your Eclipse (http://download.eclipse.org/releases/juno for Juno). Under the Programming Languages category check C/C++ Development Tools and proceed to install it. Eclipse will need to be restarted after the installation (it should prompt you anyway).
  6. Install the NDK Plugins for Eclipse that can be found on the same site as the ADT plugin. Following the same steps as above, the URL will be something like https://dl-ssl.google.com/android/eclipse.
  7. Lastly, tell Eclipse where the NDK is installed by specifying the installation path in Eclipse -> Preferences -> Android -> NDK.
I won't go over the specifics of creating a new Android NDK project but here are some more articles to reference. They cover technical details of NDK/JNI and and setting up the environment and debugging. I haven't ran through these articles myself so can't attest for their validity.


Vuforia Augmented Reality SDK Setup

  1. Download the SDK for OS X from here. You will probably need to register a free developers account with Qualcomm. I am using SDK version 1.5.9.
  2. Once the package was downloaded, I unarchived it and ran its GUI installation program. I chose the same folder as the Android SDK and NDK to house Vuforia as well. It's /Users/wocky/Documents/android/vuforia-sdk-android-1-5-9 in my case.
  3. We need to make Eclipse aware of where the Vuforia SDK is. Open Eclipse ->Preferences -> Java -> Build Path -> Classpath Variables and create a new variable called QCAR_SDK_ROOT and point it to your installation folder.


Running Sample VideoPlayback App and Debugging with Eclipse

The Android NDK section above gives some links at the bottom to other blogs as an excellent source for learning more about the basics of Android NDK projects' structure and inner workings. I'm going to end this particular blog article documenting a couple of areas of interest; first the video playback feature that was made available in Vuforia 1.5 and debugging NDK applications from within Eclipse - something I had a lot of trouble with trying to setup on my Windows box a few months back.

Qualcomm announced a sample video playback app for Android and iOS earlier this year here. I downloaded the sample app for Android and placed the VideoPlayback directory in the zip file under the samples folder in the Vuforia SDK installation path.

Installing the App

  1. Download the Eclipse project from the announcement link above (download).
  2. In Eclipse, open File -> Import -> Android -> Existing Android Code Into Workspace and search for the folder where you copied the sample VideoPlayback app to.
  3. Rename the Eclipse project to something simply like VideoPlayback if you prefer, instead of the default Java package name that Eclipse tends to use for imported projects.
  4. Open a terminal and goto the VideoPlayback directory and run the ndk-build command to build the native library code. The sources should compile with no errors.
  5. Right-click the VideoPlayback project in Eclipse and choose Refresh to make Eclipse aware of the new files created by step 4.
  6. Right-click again and select Run As -> Android application to run it on your device.
  7. You'll need either a printout (or display the image on your screen) of the stones and/or chips targets that you can find in samples/VideoPlayback/media under the Vuforia installation directory.
  8. Once the VideoPlayback app is running on your Android and you see the video input from the camera, point the camera at the target and you should then see a play button to play a video like:
  9. As an optional step you can modify the VideoPlayback sample to stream a video from the internet rather than play the mpeg4 videos packaged with the app. 
Streaming Video Media from the Internet

To stream a video from the internet instead of playing the default mpeg4 movies included in the sample app, open VideoPlaybackHelper.java and modify the following the load method to set the MediaPlayer object's data source to a file on the internet and comment out the lines that load the movie files from the asset resources.

try
{
    mMediaPlayer = new MediaPlayer();

    // This example shows how to load the movie from the assets folder of the app
    // However, if you would like to load the movie from the sdcard or from a network location
    // simply comment the three lines below
    //AssetFileDescriptor afd = mParentActivity.getAssets().openFd(filename);
    //mMediaPlayer.setDataSource(afd.getFileDescriptor(),afd.getStartOffset(),afd.getLength());
    //afd.close();

    // and uncomment this one
    mMediaPlayer.setDataSource("http://people.sc.fsu.edu/~jburkardt/data/mp4/cavity_flow_movie.mp4");
    ...
    }

I used this video as a sample. The file needs to be either over HTTP or RTSP and capable of progressive downloads (otherwise streaming wouldn't work). Here's a screenshot of it with the chips marker being display on my Mac air's screen.


I was even able to play a flash video so potentially YouTube videos could be used if you know the direct link to the FLV file (but apparently that breaches their TOCs). This option may only available on later versions of Android. I used ICS (4.0.4) on my Galaxy S3 but it didn't work on my 2.3.4 Nexus S.

Debugging Native Code in Eclipse

About a year ago when I first started playing with Vuforia and Android's NDK I remember it was a real nightmare to get debugging working. At that time I was using Eclipse on Windows 7. It seems the ADT plugin has come a wee way since then and the process has been simplified. I don't actually recall a direct "Run As -> Android native application" option. If memory serves me right, you needed to put a breakpoint in the Java code right before the C/C++ native code was called and then doing a normal Java debug session, it would jump into the native code space. Here's a summary of what I needed to do get ADT r20 running on my new setup:
  1. Ensure you have set the path to the NDK installation in Eclipse's Android preferences and the CDT plugin is installed.
  2. Right-click the VideoPlayback project and select Android Tools -> Add native support.
  3. Eclipse will most likely automatically switch from the Java to C/C++ perspective (think of an Eclipse perspective is like an "editing mode" for that particular language/feature).
  4. If you are using version r20 of the Android ADT Eclipse plugin there's a bug that will give you a bunch of warnings and errors with the cpp files in the jni folder. The actual building of these source files is handled externally by the ndk-build script but if you use Eclipse as an editor it's clutter on the screen you don't need. Following the advice here, I disabled all the items under project properties -> C/C++ General -> Code Analysis as a temporary work around.
  5. Add a breakpoint somewhere in one of the cpp files. I chose the beginning of the 
    Java_com_qualcomm_QCARSamples_VideoPlayback_VideoPlayback_initApplicationNative function in VideoPlayback.cpp.
  6. Lastly, select the project in Eclipse's package explorer, right-click and choose Debug As -> Android Native Application.
    That's all folks.

    Monday 15 October 2012

    Catchup

    Albeit a year has almost past since I wrote Android AR 101. Since then I've become a father, worked in the UK and found myself back in Fukuoka doing AR related development again. I've also picked up iOS programming and released my first iPhone app (FlagIt! - a travel app to flag countries visited or on your wishlist of places to travel). I've moved my daily development over to my recently acquired MacBook Air. Never having owned my own Mac before, I purchased it mainly for my iOS work but have fallen in love with it. It's compactness, lightness, touch pad and long last battery have really turned me into a can-program-anywhere kinda person. Whether that be the beach or local pub with beer in hand :)

    Wednesday 25 July 2012

    Get country name from coordinates or coordinates from search term using Objective-C and Google API

    One project I have been working on recently saw me messing around with Objective-C as I created an app using the MKMapView for interacting with Google Maps. I'm only hoping that when I release the app Apple aren't gonna be stupid and reject my app saying I might as well rewrite it using their new APIs as they're doing away with Google maps in iOS6. Oh well, we'll see...

    Anyway, this time around I am posting a couple of methods that outline how I retrieve a country name by doing a JSON query passing in the latitude and longitude of a place, and secondly how I get the coordinates to a search term (a string).

    As usual I'll let the code speak for itself...

    To get the string name of a country from latitude and longitude, also with the ability to specify which language you get the country name back in...
    // Do reverse geocode lookup on latitude/longitude to get country name based on specified locale
    - (NSString*)getCountryNameFromCoordinates:(CLLocationCoordinate2D)coordinates locale:(NSString*)language;
    {
        NSLog(@"Querying Google location API for %@ country name for latitude %f and longitude %f...", language, coordinates.latitude, coordinates.longitude);
        
        // Get JSON contents from Google API
        NSString *url = [[NSString stringWithFormat:@"http://maps.googleapis.com/maps/api/geocode/json?latlng=%f,%f&sensor=false&language=%@", coordinates.latitude, coordinates.longitude, language] stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
        NSLog(@"Query URL is: %@", url);
        NSError *error = nil;
        NSString *jsonString = [NSString stringWithContentsOfURL:[NSURL URLWithString:url] encoding:NSUTF8StringEncoding error:&error];
        if (error != nil)
        {
            NSLog(@"Error doing reverse geocode lookup on %f, %f, lang=%@: %@", coordinates.latitude, coordinates.longitude, language, error.localizedDescription);
            return nil;
        }
    
        // Extract country by parsing JSON data
        SBJsonParser *jsonParser = [[SBJsonParser alloc] init];
        error = nil;
        NSDictionary *jsonObjects = [jsonParser objectWithString:jsonString error:&error];
        if (error != nil)
        {
            NSLog(@"Error parsing JSON response to reverse geocode lookup: %@", jsonParser.error);
            
            [jsonParser release];
            [jsonObjects release];
    
            return nil;
        }
        NSDictionary *results = [jsonObjects objectForKey:@"results"];
        NSString *country = nil;
        for (NSDictionary *item in results)
        {
            if ([[item objectForKey:@"types"] containsObject:@"country"])
            {
                country = [[[item objectForKey:@"address_components"] objectAtIndex:0] objectForKey:@"long_name"];
                NSLog(@"Found matching country name: %@", country);
                [item release];
                break;
            }
            
            [item release];
        }
        if (country == nil) {
            NSLog(@"Couldn't find a matching country name");
            
            [jsonParser release];
            [jsonObjects release];
            [results release];
            [country release];
            
            return nil;
        }
        
        [jsonParser release];
        [jsonObjects release];
        [results release];
        
        return country;
    }
    

    And to center your map (or whatever behaviour you so desire) on the coordinates obtained from a search on a specified location string...

    
    - (void)searchCoordinatesForAddress:(NSString *)inAddress
    {
        NSLog(@"Querying Google location API for latitude/longitude coordinates for search term %@", inAddress);
        
        // Get JSON contents from Google API
        NSString *url = [[NSString stringWithFormat:@"http://maps.googleapis.com/maps/api/geocode/json?address=%@&sensor=false", inAddress] stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
        NSLog(@"Query URL is: %@", url);
        NSError *error = nil;
        NSString *jsonString = [NSString stringWithContentsOfURL:[NSURL URLWithString:url] encoding:NSUTF8StringEncoding error:&error];
        if (error != nil)
        {
            NSLog(@"Error doing coordinate lookup on %@: %@", inAddress, error.localizedDescription);
            return;
        }
        
        // Extract coordinates by parsing JSON data
        SBJsonParser *jsonParser = [[SBJsonParser alloc] init];
        error = nil;
        NSDictionary *jsonObjects = [jsonParser objectWithString:jsonString error:&error];
        if (error != nil)
        {
            NSLog(@"Error parsing JSON response to extract coordinates: %@", jsonParser.error);
    
            [jsonParser release];
            [jsonObjects release];
            
            return;
        }
        NSArray *results;
        NSDictionary *location;
        @try {
            results = [jsonObjects objectForKey:@"results"];
            location = [[[results objectAtIndex:0] objectForKey:@"geometry"] objectForKey:@"location"];
        }
        @catch (NSException *exception) {
            NSLog(@"Could not find searched location's coordinates");
            [jsonParser release];
            [jsonObjects release];
            return;
        }
    
        // Get latitude and longitude as double values
        double longitude = 0.0;
        double latitude = 0.0;   
        NSNumber *lat = [location objectForKey:@"lat"];
        NSNumber *lng = [location objectForKey:@"lng"];
        latitude = [lat doubleValue];
        longitude = [lng doubleValue];
        [lat release];
        [lng release];
        
        [jsonParser release];
        [jsonObjects release];
        [results release];
        
        if (longitude == 0 && latitude == 0) {
            NSLog(@"Could not find searched location's coordinates");
            return;
        }
        NSLog(@"Coordinates found for searched location: %f %f", latitude, longitude);
        
        // I zoom my map to the area in question.
        [self zoomMapAndCenterAtLatitude:latitude andLongitude:longitude];
    }
    

    News

    This blog hasn't had any activity for the past few months. After my wife and I embarked on our round the world trip back in February, stopping off home in New Zealand for our second wedding, we found out she was pregnant :)

    We are delighted to say we will be expecting our first baby boy in October. We didn't end up completing most of the trip we had planned and instead I've been working for the past few months out of the UK. I will be moving back to Japan later this year and hope to refocus my efforts on augmented reality work then. Not that I am expecting much free time with fatherdom looming, but I will try and post the odd helpful article when I can!

    Monday 23 January 2012

    Creating a custom Android Intent Chooser

    I recently added a feature to my Zedusa image resizer application that required me to write a custom intent chooser. The reason for this was so I could add a checkbox on the list so the user could choose to make the application they selected the default one going forward.


    I thought I'd share a snippet of the code on my blog as it could be useful for some. Feel free to ask any questions about the inner workings of it, otherwise I'll leave it up to you to read and understand. Please be aware I stripped some code out so I haven't actually ran the exact code below on my phone.

    public void startDefaultAppOrPromptUserForSelection() {
     String action = Intent.ACTION_SEND;
    
     // Get list of handler apps that can send
     Intent intent = new Intent(action);
     intent.setType("image/jpeg");
     PackageManager pm = getPackageManager();
     List<ResolveInfo> resInfos = pm.queryIntentActivities(intent, 0);
    
     boolean useDefaultSendApplication = sPrefs.getBoolean("useDefaultSendApplication", false);
     if (!useDefaultSendApplication) {
      // Referenced http://stackoverflow.com/questions/3920640/how-to-add-icon-in-alert-dialog-before-each-item
    
      // Class for a singular activity item on the list of apps to send to
      class ListItem {
       public final String name;
       public final Drawable icon;
       public final String context;
       public final String packageClassName;
       public ListItem(String text, Drawable icon, String context, String packageClassName) {
        this.name = text;
        this.icon = icon;
        this.context = context;
        this.packageClassName = packageClassName;
       }
       @Override
       public String toString() {
        return name;
       }
      }
    
      // Form those activities into an array for the list adapter
      final ListItem[] items = new ListItem[resInfos.size()];
      int i = 0;
      for (ResolveInfo resInfo : resInfos) {
       String context = resInfo.activityInfo.packageName;
       String packageClassName = resInfo.activityInfo.name;
       CharSequence label = resInfo.loadLabel(pm);
       Drawable icon = resInfo.loadIcon(pm);
       items[i] = new ListItem(label.toString(), icon, context, packageClassName);
       i  ;
      }
      ListAdapter adapter = new ArrayAdapter<ListItem>(
        this,
        android.R.layout.select_dialog_item,
        android.R.id.text1,
        items){
    
       public View getView(int position, View convertView, ViewGroup parent) {
        // User super class to create the View
        View v = super.getView(position, convertView, parent);
        TextView tv = (TextView)v.findViewById(android.R.id.text1);
    
        // Put the icon drawable on the TextView (support various screen densities)
        int dpS = (int) (32 * getResources().getDisplayMetrics().density   0.5f);
        items[position].icon.setBounds(0, 0, dpS, dpS);
        tv.setCompoundDrawables(items[position].icon, null, null, null);
    
        // Add margin between image and name (support various screen densities)
        int dp5 = (int) (5 * getResources().getDisplayMetrics().density   0.5f);
        tv.setCompoundDrawablePadding(dp5);
    
        return v;
       }
      };
    
      // Build the list of send applications
      AlertDialog.Builder builder = new AlertDialog.Builder(this);
      builder.setTitle("Choose your app:");
      builder.setIcon(R.drawable.dialog_icon);
      CheckBox checkbox = new CheckBox(getApplicationContext());
      checkbox.setText(getString(R.string.enable_default_send_application));
      checkbox.setOnCheckedChangeListener(new OnCheckedChangeListener() {
    
       // Save user preference of whether to use default send application
       @Override
       public void onCheckedChanged(CompoundButton paramCompoundButton,
         boolean paramBoolean) {
        SharedPreferences.Editor editor = sPrefs.edit();
        editor.putBoolean("useDefaultSendApplication", paramBoolean);
        editor.commit();
       }
      });
      builder.setView(checkbox);
      builder.setOnCancelListener(new OnCancelListener() {
    
       @Override
       public void onCancel(DialogInterface paramDialogInterface) {
        // do something
       }
      });
    
      // Set the adapter of items in the list
      builder.setAdapter(adapter, new DialogInterface.OnClickListener() {
       @Override
       public void onClick(DialogInterface dialog, int which) {
        SharedPreferences.Editor editor = sPrefs.edit();
        editor.putString("defaultSendApplicationName", items[which].name);
        editor.putString("defaultSendApplicationPackageContext", items[which].context);
        editor.putString("defaultSendApplicationPackageClassName", items[which].packageClassName);
        editor.commit();
    
        dialog.dismiss();
    
        // Start the selected activity sending it the URLs of the resized images
        Intent intent;
        intent = new Intent(Intent.ACTION_SEND);
        intent.setType("image/jpeg");
        intent.setClassName(items[which].context, items[which].packageClassName);
        startActivity(intent);
        finish();
       }
      });
    
      AlertDialog dialog = builder.create();
      dialog.show();
    
    
     } else { // Start the default send application
    
      // Get default app name saved in preferences
      String defaultSendApplicationName = sPrefs.getString("defaultSendApplicationName", "<null>");
      String defaultSendApplicationPackageContext = sPrefs.getString("defaultSendApplicationPackageContext", "<null>");
      String defaultSendApplicationPackageClassName = sPrefs.getString("defaultSendApplicationPackageClassName", "<null>");
      if (defaultSendApplicationPackageContext == "<null>" || defaultSendApplicationPackageClassName == "<null>") {
       Toast.makeText(getApplicationContext(), "Can't find app: "  defaultSendApplicationName  
         " ("   defaultSendApplicationPackageClassName   ")", Toast.LENGTH_LONG).show();
    
       // don't have default application details in prefs file so set use default app to null and rerun this method
       SharedPreferences.Editor editor = sPrefs.edit();
       editor.putBoolean("useDefaultSendApplication", false);
       editor.commit();
       startDefaultAppOrPromptUserForSelection();
       return;
      }
    
      // Check app is still installed
      try {
       ApplicationInfo info = getPackageManager().getApplicationInfo(defaultSendApplicationPackageContext, 0);
      } catch (PackageManager.NameNotFoundException e){
       Toast.makeText(getApplicationContext(),  "Can't find app: "   defaultSendApplicationName  
         " ("   defaultSendApplicationPackageClassName   ")", Toast.LENGTH_LONG).show();
    
       // don't have default application installed so set use default app to null and rerun this method
       SharedPreferences.Editor editor = sPrefs.edit();
       editor.putBoolean("useDefaultSendApplication", false);
       editor.commit();
       startDefaultAppOrPromptUserForSelection();
       return;
      }
    
      // Start the selected activity
      intent = new Intent(Intent.ACTION_SEND);
      intent.setType("image/jpeg");
      intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
      intent.setClassName(defaultSendApplicationPackageContext, defaultSendApplicationPackageClassName);
      startActivity(intent);
      finish();
      return;
     }
    }
    

    Friday 20 January 2012

    ARWedding - My first AR app


    I had a bit of a hiatus on AR stuff while I got caught up doing some development for other Android apps. I'm getting married in a week so I thought I'd kick off by making an adaption of Qualcomm's QCAR (since renamed Vuforia) sample app ImageTargets into a novelty wedding gift. It simply shows a photo of the two of us for the splash screen and a rose as the 3D model. I'll be releasing both Android and iPhone versions which you can find from the corresponding links on the top-right of my blog. The links to the projects' sources are below.

    I won't go into detail of how I did what as that's just a little bit too time consuming with lots of wedding preparations to do;-) If you do have any specific questions feel free to drop a comment and I'll try and reply when I can.

    The customizations I did to the Android/iPhone verions are:

    • Custom app icon
    • Custom 3D model of a rose to replace the teapot
    • Inserted rose coloured textures (ideally I'd want to have multiple textures; one for the green of the stalk and another for the red of the flower but that turned out to be a more advanced topic that I decided to put aside for a rainy day)
    • Changed the trackables (markers) to a QR code (see below) and the faces on 1000, 5000 and 10,000 yen Japanese notes
    • Played with the kObjectScale to get a better sized rose and rotated the projection matrix to make the rose appear as if it was standing upright

    Here are the links to the projects' source code. I was using the QCAR SDK 1.5.4 beta1, the latest at the time of coding.

    • Android project for Eclipse. I was using Eclipse Helios testing on my Nexus S 2.3.4 phone and my Asus Transformer EEE TF101 tablet running Android 3.2. You may need to edit the NDK settings so it can find the QCAR SDK properly.
    • iPhone project for Xcode. I was using Xcode 3.2.3 and tested on my jailbroken iPhone 3GS running iOS 4.1 and on my iPhone4 running iOS 5. The project was in the same folder as the QCAR SDK sample projects, I'm not sure whether this would involve some settings changes if you use a different folder location.

    Besides the Japanese notes you can also use the following QR code as a marker. The colour of the rose associated with the QR code is red whereas the money trackables correspond with a red-green coloured texture.

    Friday 6 January 2012

    Error 9015 on the Oneworld.com Round the World site

    I thought it might be time for my first non-programming article. My wife and I are going to be doing a round the world trip for our honeymoon next month. I've been spending literally hours at One World's online RTW interactive planner trying to work out the cheapest and best route. It's quite a fun tool just to play with as there are so many destinations and heaps of permutations. The route we eventually settled is the map below start in Seoul:

     You can see the little message at the bottom which basically means everything's validated - ie. all the flights/stopovers and paths don't break any of One World's RTW ticket rules and regulations. And trust me there's heaps. From a programmer's point of view I appreciate how complex such a system is and trying to code in the conditions for all that business logic must be a nightmare designing and coding. With a system this big it's inevitable that there will be some bugs in the system.

    Anyway, onto the title of this post. After I validated my journey, confirmed the price etc. I went ahead with entering our personal info and then my credit card info. On the very last step of purchasing it sat there for about 3-5 minutes processing and then came back with error "9015" telling me to contact my Travel Assistance Desk for help.

    The problem is this site doesn't seem to have a single point of contact for help. Eventually I rang American Airlines and got through to their RTW hotline (phone number +1800 247 3247) and the lady I spoke with was at least familiar with the system somewhat and had a list of common error codes on hand. Unfortunately 9015 wasn't in that list. She tried manually booking my ticket for me but as the ticket wasn't starting in the US, it was starting in South Korea, they needed to get a quote from their office there. The base tariffs and taxes etc. are based on how many continents you stop in and differ greatly depending on the country you start in (Seoul was a very cheap place to start for people starting in Asia - I just catch the boat to Korea and hop on the RTW from there:).

    To cut a long story short, the crux of the story is I wasn't able to purchase my RTW ticket in Korea using the online planner with my credit card because I needed either a) a Korean credit card (the billing address must be there; mine was Japanese) or b) turn up in person with my credit card to the Seoul office of American Airlines which was a non-option because I won't be in Seoul until 2 days before my world trip begins.

    I now am guessing that the 9015 error was due to my credit card's billing address not being in the same country as the starting point of my journey. In the end I booked on the phone through a friend's workplace in London for a sweet deal. The guys @ www.roundtheworldexperts.co.uk are great!