Posted on by

androidize-featFor 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:

  1. Audio format compatibility
  2. Font usage
  3. Video format compatibility
  4. Full screen usage (no letterboxing)
  5. Key processing
  6. Icons and “Default.png”
  7. 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.

local device = require( "device" )

local sfx = {}
local soundExt = ".m4a"
if ( device.isAndroid ) then
   soundExt = ".ogg"
end
sfx.beep = audio.loadSound( "audio/beep" .. soundExt )
sfx.buzz = audio.loadSound( "audio/buzz" .. soundExt )
sfx.music = audio.loadStream( "audio/music" .. soundExt )

Now your app will load the appropriate sound format for the device.


Font Usage

Fonts shouldn’t be as difficult as they are, but this post isn’t going to rehash all of the various rules for custom fonts. Instead we’ll focus on general font usage.

Creating a text object in Corona is extremely simple:

local text = display.newText( "Hello World", 0, 0, "Helvetica", 24 )

The problem is, Helvetica doesn’t exist on most of the Android devices. Thus, the above call will fall back to native.systemFont which will give the app a slightly different look on different devices.

Using our device information, we can set up a table of fonts to be used under special circumstances.

local font = {}
font.normal = "Helvetica"
font.bold = "Helvetica-Bold"
font.italic = "Helvetica-Oblique"

if ( device.isAndroid ) then
   font.normal = "DroidSans"
   font.bold = "DroidSans-Bold"

   if ( device.isNook ) then
      font.normal = "Arial"
      font.bold = "Arial-Bold"
   elseif ( device.isKindleFire ) then
      font.normal = "arial"
      font.bold = "arial-Bold"
   end
end

local text = display.newText( "Hello World!", 0, 0, font.normal, 24 )

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:

local function videoListener( event )
   storyboard.gotoScene( "menu", "fade", 500 )
end
media.playVideo( "promo.mp4", system.ResourceDirectory, false, videoListener )

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.

application =
{
   content =
   {
      width = 320,
      height = 320 * display.pixelHeight/display.pixelWidth,
      scale = "letterBox",
      imageSuffix =
      {
         ["@2x"] = 1.5,
         ["@4x"] = 3.0,
      },
   },
}

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:

local function onKeyEvent( event )

   local phase = event.phase
   local keyName = event.keyName
   print( event.phase, event.keyName )

   if ( "back" == keyName and phase == "up" ) then
      if ( storyboard.currentScene == "splash" ) then
         native.requestExit()
      else
         if ( storyboard.isOverlay ) then
            storyboard.hideOverlay()
         else
            local lastScene = storyboard.returnTo
            print( "previous scene", lastScene )
            if ( lastScene ) then
               storyboard.gotoScene( lastScene, { effect="crossFade", time=500 } )
            else
               native.requestExit()
            end
         end
      end
   end

   if ( keyName == "volumeUp" and phase == "down" ) then
      local masterVolume = audio.getVolume()
      print( "volume:", masterVolume )
      if ( masterVolume < 1.0 ) then
         masterVolume = masterVolume + 0.1
         audio.setVolume( masterVolume )
      end
      return true
   elseif ( keyName == "volumeDown" and phase == "down" ) then
      local masterVolume = audio.getVolume()
      print( "volume:", masterVolume )
      if ( masterVolume > 0.0 ) then
         masterVolume = masterVolume - 0.1
         audio.setVolume( masterVolume )
      end
      return true
   end
   return false  --SEE NOTE BELOW
end

--add the key callback
Runtime:addEventListener( "key", onKeyEvent )

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-sensitiveDefault.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.

Next, we’ll edit our config.lua file and add this block in the application table:

application =
{
   license =
   {
      google =
      {
         key = "the really long string you copied from Google Play",
         policy = "serverManaged",
      },
   },
}

Next, we’ll edit the build.settings file and include the “com.android.vending.CHECK_LICENSE” to the Android permissions as follows:

android =
{
   usesPermissions =
   {
      "android.permission.INTERNET",
      "com.android.vending.CHECK_LICENSE",
      "com.android.vending.BILLING",
   },
}

Finally, in main.lua, we’ll add this block of code:

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.

As always, please post your questions and comments below.


Posted by . Thanks for reading...

29 Responses to “Tutorial: “Android-izing” a Mobile App”

  1. Tom71

    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… :D

    Reply
  2. Ebizon

    i am facing this problem please suggest what should i do

    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’

    Reply
  3. Marc Bechamp

    It would be awesome if the back key on the simulator skin would actually work in order to test back key functionality on the simulator instead of the actual device. Just a suggestion…

    Reply
  4. Steve Taylor

    I second the need for the ability to test/simulate the back button in Corona. Maybe the capability is out there and I am just slow but I can’t find it.

    Reply
  5. Juanpa

    Hi,

    My KindlFire not work fine with “volumeUp” and “volumeDown” …it go to native.requestExit() ??

    Thanks

    Reply
  6. Stuart Warren

    Does this licensing code work on kindle app sales as well?

    I have been looking for a comprehensive guide to kindle-izing an app and don’t see that yet.

    Since kindle uses android, I guess it could work…

    Thanks!
    stu

    Reply
  7. Rob Miracle

    Licensing is a Google Play only thing. While its used in things like expansion files and such, it’s primary purpose is to replace the DRM used by Google Play. Amazon has it’s own DRM that it uses.

    Beyond that, the rest of this blog post should apply to Amazon.

    Reply
  8. stuart warren

    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.

    Thanks!
    Stu

    Reply
  9. Rob Miracle

    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.

    Reply
  10. Dominic Wood8

    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

    Reply
  11. greg

    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?

    Reply
  12. Wim Coosemans

    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

    Reply
  13. Olivier Romanetti

    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.

    Any help would be great
    Thanks
    Olivier

    Reply
    • Rob Miracle

      Hi Oliver. Can you post this request in the forums? We are probably going to need to see some code and code doesn’t post well in the blog’s comments.

      Rob

      Reply
    • Richie

      Hi Oliver, I have the same problem. When I press the Back button on my smartphone, it exits the app, Have you solve this problem?
      Thanks a lot for your respond!

      Reply
  14. David Powell

    Hi

    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

    Reply
    • Rob Miracle

      Good catch. It looks like that there was intent to say “if not verified then” instead of “If not event.isVerified then”. The If statement works the same either way.

      Rob

      Reply

Leave a Reply

  • (Will Not Be Published)