Tutorial: “Android-izing” a Mobile App

For some multi-platform developers, building for Android is planned from the beginning, but it’s not fully implemented until after the launch of the iOS version. However, there are several things that should be done while building a cross-platform app to make sure that Android’s unique functionality is handled in advance of submission to the marketplace.

At the very least, you should take the following into consideration:

Audio format compatibility

Font usage

Video format compatibility

Full screen usage (no letterboxing)

Key processing

Icons and “Default.png”

Google’s new licensing process

Audio Format Compatibility

Audio formats vary greatly, as does their support. MP3, while having potential patent royalty issues, is a format that works on both iOS and Android. WAV files are also cross-compatible but they are huge in size.

If we don’t want to worry about the issues that surround MP3, we can use a similar compressed format like .m4a for iOS and another compressed format like .ogg file (Ogg Vorbis) side by side.

To do this, we need to know what type of device you are on. Several weeks ago, there was a blog post called Device Detection on Steroids which includes a file called device.lua that you can use in your app. This file makes it easy to determine what platform the app is running on. For purposes of this tutorial, please download it and include it in the main project folder.

With this system, we can use whatever font you want for a given device (custom or not), yet in most cases retain one set of code.

Video Format Compatibility

When using media.playVideo(), Android knows how to play video as MP4s but not QuickTime .mov files. We can use a similar technique as with audio to play a device specific video if necessary.

The one gotcha with Android and media.playVideo() is that it actually suspends the app, plays the video in a separate process, and resumes the app when the video completes. To play a video at the beginning of the app — i.e. an animated studio logo — we can do this in the main.lua file:

When the video finishes, it calls the listener function and proceeds with running the app.

Full Screen Usage (No Letterboxing)

Both Amazon and Barnes & Noble prefer apps to utilize the entire screen, and they might reject letterboxed apps, i.e. those that don’t use the full screen and instead fill the remaining space with black bars.

Most of this was covered in the blog post on the Ultimate config.lua file. Several people didn’t like the varied positioning required by that file. Luckily, most Android screens don’t have the diversity that iOS devices do, in regards to screen ratio. If we want to use fixed coordinates, zoomStretch isn’t that bad on Android. But if you want to use letterBox to keep your display correct, changing the last else clause will perfectly match the screen’s coordinates to the device’s resolution. Of course, we’ll need to position things relative to the center or to the edges.

Key Processing

Android devices have a property in which they provide access to both “soft” and physical keys that we can take advantage of (in some stores it’s required to handle this). These include the back key and the volume up and down keys.

Most marketplaces now require handling of the back key. On NOOK, this can be both the physical “n” key or a soft back key on a button bar. We also get access to the volume keys. Look at this code:

The keyboard handler will return the key name ( “back”, “volumeUp”, or “volumeDown” ) and we’ll get two events per keypress that are the “up” and “down” phases. Generally, we only need to respond to one of them. The example above uses the “up” phase.

NOTE: You shouldn’t always return true in the onKeyEvent() function illustrated above. You should only return true for keys that your app is “overriding.” This is to help future-proof the app as we add support for more keys.

“Back” is a tricky state, and in this example using Storyboard, we are not provided with enough information to manage a history to go back to. It’s tempting to use storyboard.getPrevious() to go back to the last scene, but this will start an infinite loop. Consider this:

main.lua → splash.lua → menu.lua → level1.lua

If we call storyboard.getPrevious() while in level1.lua, “menu” is returned. Perfect! — that takes you back. But next, while in menu.lua, calling storyboard.getPrevious() will return “level1″, not“splash” as desired. level1 was, in fact, the previous scene. Calling getPrevious() again returns to the menu, then back to level1, etc.

The solution is to maintain a custom “history.” Just add a member variable to the storyboard table called returnTo. It’s best to set this .returnTo property in each scene’s enterScene() event function.

storyboard.returnTo = "menu"

Then, set the string for returnTo to where you want the back button to go when pressed from that scene. When it goes back as far as it can logically go, set it to nil.

storyboard.returnTo = nil

In the example above, splash.lua would have its storyboard.returnTo set to nil. level1 would go to menu, and finally menu to splash.

When the back button is pressed, the code detects if the user is in the splash.lua module, and if so, it requests an exit using native.requestExit() (this is the proper way to gracefully force-exit an Android app). If the scene is an overlay, the code detects this and hides it. Finally, it checks the value of the storyboard.returnTo attribute and goes to that scene. If it’s not set, then it’s in the “terminal scene” and it exits.

The second part of this function handles the volume keys. It will raise or lower the volume by 10% on each button press, assuming that we’re not already at maximum or minimum volume. On the Google Nexus 7, if the volume is at 50%, these keys will let you change the app’s volume from 0 to 50%. We cannot make it louder than the volume at launch time.

Icons and “Default.png”

By default, Android does not support a Default.png file as a “launch image.” Corona, however, will look for this file regardless and briefly show it on app startup. Unfortunately, there is no real standard on the pixel dimensions of this file, but this guide provides some further details. Don’t forget that this file is case-sensitive: Default.png.

In regards to app icons, Android uses various icon sizes with names that seem peculiar at first, but once we study the pattern it makes sense: Icon- + [size] + dpi.png (dots per inch).

The sizes are low, medium, high and xtra-high — or in practical terms:

Icon-ldpi.png : 36 × 36

Icon-mdpi.png : 48 × 48

Icon-hdpi.png : 72 × 72

Icon-xhdpi.png : 96 × 96

Just create and include these icon files in the core project directory and Corona will handle the rest.

Google’s New Licensing Process

Google has recently changed how they protect apps from piracy. Instead of adding a slew of DRM encryption to each app, they now use a licensing service where the app “contacts” Google Play for verification. There are two modes of verification: once and cache the results if a network connection isn’t available or a check isn’t required every time the app runs.

When creating a new app on Google Play, there’s a link that redirects to a screen where we’re presented with a License Key. This will be a very long string. We’ll copy it to the clipboard.

local licensing = require( "licensing" )
licensing.init( "google" )
local function licensingListener( event )
local verified = event.isVerified
if not event.isVerified then
--failed verify app from the play store, we print a message
print( "Pirates: Walk the Plank!!!" )
native.requestExit() --assuming this is how we handle pirates
end
end
licensing.verify( licensingListener )

And that’s all we need to cover the new Google Play licensing requirements. How you handle pirates is up to you!

In Summary…

Hopefully this tutorial has presented some useful, practical tips on how to prepare your cross-platform Corona app for Android. Remember, it’s beneficial to work these tactics in sooner than later, so you’re not “scrambling to convert” while your app is swimming along nicely in the iOS App Store.

29 comments on “Tutorial: “Android-izing” a Mobile App”

Great post! But you should also mention in the key processing example that ‘storyboard.isOverlay’ is a custom variable like you do with ‘storyboard.returnTo’. I just spent several hours on the edge of despair because of this…

Corona Simulator[428:903] Runtime error
module ‘licensing’ not found:resource (licensing.lu) does not exist in archive
no field package.preload[‘licensing’]
no file ‘/Users/macoo8/Desktop/MyApp/licensing.lua’
no file ‘/Applications/CoronaSDK/Corona Simulator.app/Contents/Resources/licensing.lua’
no file ‘./licensing.dylib’
no file ‘/Applications/CoronaSDK/Corona Simulator.app/Contents/Resources/licensing.dylib’

Does google licensing require an internet connection to authenticate at startup or does it download an auth key to allow it to be run offline? I want my app to run on ipads and many don’t have wifi where they use them. if it is a one shot auth, and following attempts retrieve a code key from disk, that’s ok.

Rob Miraclesays:

Well, I’m not the Android expert around here, but with regards to DRM, I would assume that’s local. It would be silly to require Internet for your app to run. With regards to expansion packs and any Google Play services, well you have to be online to use them, if you’re using them, your online already.

Regarding CHECK_LICENSE validation. I understand this is a requirement for Google Play, will this also allow us to distribute Apps directly to our clients via email & website downloads. We have a B2B App so were planning to use both methods of distribution. Will we need to have 2 seperate builds one for Goggle Play and one without for direct distrubution? Thanks

Re the lines “licensing.init( “google” )” etc can I ask:
* I assume the normal practice is to have one build (re code) only and use this directly to build for both say IOS and Android?
* This being the case, I assume you have to put “am I on Android” type checks before hitting the code “licensing.init( “google” )” etc, else it would through an error?

Good article
It would be good to note that for Android apps on the Samsung store you are required to support the ZOOM keys for volume control, if you want to support devices like the Galaxy Camera
Otherwise the app gets rejected

Hi Rob
Thank you for this tutorial.
I currently work on “Android-izing” my iOS App
– I used onKeyEvent listener
– I add the key callback Runtime:addEventListener( “key”, onKeyEvent )
– I add a member variable to the storyboard table called returnTo
– I set this .returnTo property in each scene’s enterScene() event function.

Unfortunately, When I press the back button (on a Nexus 7) It doesn’t work.
wherever I am, app exits.

In the following code block, why do you need the local variable “verified”? It doesn’t seem to be doing anything.

local function licensingListener( event )

local verified = event.isVerified
if not event.isVerified then
–failed verify app from the play store, we print a message
print( “Pirates: Walk the Plank!!!” )
native.requestExit() –assuming this is how we handle pirates
end
end