Case Study – Mobile 1UP porting to BlackBerry 10

Case Studies & Success Stories

As an independent developer, I know the pain points that other independent developers go through when starting to work with a new platform. This was one of my main incentives to take the role as a Senior Technical Evangelist at Research In Motion® (RIM®).

My first task on the job was to do what I hope all independent developers will be doing – writing or porting their applications to the BlackBerry® 10 platform. I figured that if I was going to evangelise the platform, I should experience it firsthand (and port my own applications).

Since early 1999, I have worked with various mobile platforms (10+) for many years with a focus on native (C/C++) application development, and in parallel I have a hobby game development studio (www.mobile1up.com) where I publish a few games for fun.

As you get more and more exposure to platforms, it is natural to seek a mechanism to avoid having to recreate the wheel every time you work with a new platform. This is where developers either utilize third party libraries (SDL, gameplay, cocos2d et al) or they end up creating their own in-house solution for dealing with cross-platform issues.

I was an early starter, so there were not many third party libraries out there, and I wanted to have complete control over what I was creating. A cross-platform abstraction layer started to make shape in-house; the benefit of this is that just implementing the device abstraction layer means the games do not need any form of modification and simply recompile and run on the new target platform.

As a developer; your requirements will be slightly different; but the principles are the same. When it comes to porting across platforms, I typically have the following checklist:

  • application anatomy (mainline, event loop et al)
  • identify a way to debug/handle logging of the application
  • play nice with the launcher/user experience of the platform
  • create a framebuffer object for graphics
  • create a PCM audio callback system for music and sound effects
  • create a handler for input events like touch and keyboard
  • create a resource manager for preferences, game assets
  • create bindings to system resources (memory, time, files, networking)

It really depends on what bits and pieces you need to finish the porting process; for example, you may not need networking or native user interface elements immediately. In this case study I will focus on what was needed to make the game “Cronk” available on the BlackBerry® PlayBook™ tablet.

It is assumed you have prepared your development environment and successfully compiled and deployed the sample applications to the Tablet OS Simulator or BlackBerry PlayBook tablet. To keep things simple, you will just need to replace the main.c file in any existing project to make what I explain here work.

PLATFORM ANATOMY (MAINLINE, EVENT LOOP)

If you have taken the time to look at the sample applications, you will see a very simple pattern in the manner in which things are handled on the BlackBerry 10 platform. The absolute bare minimum of any native application looks like the following:

/**
 ** main.c
 **/

#include <stdlib.h>
#include <stdio.h>

#include <bps/bps.h>
#include <bps/event.h>
#include <bps/navigator.h>

int application_initialize()
{
  int err;

  // initialize the platform services
  err = bps_initialize(); 
  if (err == BPS_FAILURE) return 0;

  // we are interested in navigator events
  navigator_request_events(0);

  return 1;
}

void application_eventloop()
{
  bps_event_t *event_bps;
  int          event_domain;
  int          event_id;
  int          running;
  int          event_timeout;

  // initialize
  event_timeout = 0;  // return immediately

  // start processing events
  running = 1;
  while (running)
  {
    for (;;)
    {
      event_bps = NULL;
      bps_get_event(&event_bps, event_timeout);

      // process the event accordingly
      if (event_bps != NULL)
      {
        event_domain = bps_event_get_domain(event_bps); 

        // did we receive a navigator event?
        if (event_domain == navigator_get_domain())
        {
          event_id = bps_event_get_code(event_bps);

          // what navigator event did we receive?
          switch (event_id)
          {
            case NAVIGATOR_EXIT:

                 // user has requested we exit the application
                 running = 0;
                 break;

            default:
                 break;
          }
        }
      }
      else break;
    }
  }
}

void application_terminate()
{
  // shutdown the platform services
  bps_shutdown();
}

int main(int argc, char **argv)
{
  // lets go! 
  if (application_initialize())
  {
    application_eventloop();
    application_terminate();
  }

  return EXIT_SUCCESS;
}

Compiling and deploying this main.c will on launch show the launcher backdrop (a BlackBerry image) and do nothing until you swipe upwards and click on the close button to shut it down. This is what can be used as a basis to build your own applications.

The main() function is quite simple: first, try to initialize everything you need. If that goes okay, start your event loop, and when it is time to terminate the application do whatever is needed to release any resources that were used.

The application_initialize() starts the BlackBerry Platform Services (BPS) API that manages various platform services (events, et al). It provides a uniform interface for all platform capabilities. Since we want to also respond to navigator events (ie. when the user wants to close the application), we also need to request this.

The application_eventloop() is where all the action happens. With an conditional loop, events are requested and processed accordingly. When we receive a special NAVIGATOR_EXIT event, the loop is terminated and the application shuts down.

The bps_get_event() function in this case blocks until an event is available, at which point the domain (category) and id of the event is identified and processed. Events are categorized with a domain/id pair across different services that exist on the platform, preventing clobbering of event id’s across the various services.

The application_terminate() shuts down the BlackBerry Platform Services (BPS) API as they are no longer required and the user is returned to the launcher. Your application has behaved correctly.

One key element of all games is to be able to control the notion of “framerates” for when the actually game logic is processed. You may want to ensure that 1 second of processing matches 1 second of game play. On BlackBerry 10 you can use timers and custom events for this.

#include <signal.h>
#include <sys/time.h>

// global variables : timer
int g_timer_channel;
int g_timer_domain;
int g_timer_ms;
int g_timer_active;

#define FPS 20 // 20 frames/second

void application_timer_callback(int signal)
{
  bps_event_t *e;

  // post an event for processing (if we are active)
  if (g_timer_active)
  {
    bps_event_create(&e, g_timer_domain, 0, NULL, NULL);
    bps_channel_push_event(g_timer_channel, e);
  }
}

int application_initialize()
{
  struct sigaction action;
  struct itimerval timer;

  ...

  // we need to register a domain for timer event handling
  g_timer_channel = bps_channel_get_active();
  g_timer_domain  = bps_register_domain();

  // whats the desired fps the application wants us to use?
  g_timer_ms = 1000L / FPS;

  // our timer is not active until the window is displayed
  g_timer_active = 0;

  // initialize the interval timer
  memset(&action, 0, sizeof(struct sigaction));
  action.sa_handler = application_timer_callback;
  action.sa_flags   = 0;
  sigemptyset(&action.sa_mask);
  sigaction(SIGALRM, &action, NULL);

  // start the interval timer (repeating)
  memset(&timer, 0, sizeof(struct itimerval));
  timer.it_value.tv_sec     =  g_timer_ms / 1000;
  timer.it_value.tv_usec    = (g_timer_ms % 1000) * 1000;
  timer.it_interval.tv_sec  = timer.it_value.tv_sec;
  timer.it_interval.tv_usec = timer.it_value.tv_usec;
  setitimer(ITIMER_REAL, &timer, NULL);

  ...
}

void application_eventloop()
{
  ...

        // did we receive a timer event?
        if (event_domain == g_timer_domain)
        {
          // do our game logic here
        }

  ...
}

void application_terminate()
{
  struct itimerval timer;

  ...

  // terminate the interval timer
  memset(&timer, 0, sizeof(struct itimerval));
  setitimer(ITIMER_REAL, &timer, NULL);

  ...
}

During application_initialize() a repeating timer is registered to create signals where custom timer events can be added to the event queue for processing within application_eventloop(). It is important, with a custom event to create a new domain so the event can be separated from other services.

We want to wait until our window is presented before we start triggering events – this is the purpose of the g_timer_active global variable, which initially is defined as 0.

IDENTIFY A WAY TO DEBUG/HANDLE LOGGING OF APPLICATIONS

Like most modern development environments – the IDE provides a the ability to do step-by-step debugging (breakpoints, views et al) but in some cases you just want to be able to write information to a log file and view it later.

The BlackBerry 10 platform logs stdout messages inside the logs/log file in the application sandbox.

fprintf(stdout, "[INFO] sample log file entry\n");
fflush(stdout);

To view this log file, simply create an SSH connection with the playbook device. First create a key to be used for SSH, then enable the device to accept SSH connections and finally SSH into the device.

$ ssh-keygen -t rsa -b 4096   // (required once)
$ blackberry-connect xxx.xxx.xxx.xxx -password {password}

...

$ ssh devuser@xxx.xxx.xxx.xxx

You will need two terminal windows: blackberry-connect allows the BlackBerry PlayBook tablet to accept incoming SSH connections, while the second terminal can be used to actually connect to the device itself. It is important to use a debug token when deploying your application, otherwise you will not be able to access the log files for the application.

$ cd /accounts/1000/appdata/com.xxx.yyy/logs
$ cat log

The com.xxx.yyy path will vary based on the package id defined in your application.

PLAY NICE WITH THE LAUNCHER/USER EXPERIENCE OF THE PLATFORM

Depending on the type of application you have in mind, there are a number of events one must respond to to correctly provide a good user experience on the BlackBerry 10 platform — such as dealing with device rotation and orientation changes.

To prevent rotation and re-orientation completely:

navigator_rotation_lock(1);

To play nice; you simply need to handle the following navigator events:

NAVIGATOR_ORIENTATION_CHECK
NAVIGATOR_ORIENTATION
NAVIGATOR_ORIENTATION_DONE
NAVIGATOR_ORIENTATION_RESULT

You can determine exactly when to allow the device to rotate or re-orientate your application during the NAVIGATOR_ORIENTATION_CHECK event. If you wish to rotate, a NAVIGATOR_ORIENTATION event will be posted where it is important to internally re-position or resize any user interface elements in use.

As an example, if you are using windows, you will need to rotate your main window within the NAVIGATOR_ORIENTATION to ensure that it is displayed correctly.

A vital set of events are the NAVIGATOR_WINDOW_xxxx type that can be used to control when animation/timer events should occur. You may want to pause your application/game when the user switches between other applications.

void application_eventloop()
{
   ...

        // did we receive a navigator event?
        if (event_domain == navigator_get_domain())
        {
          event_id = bps_event_get_code(event_bps);

          // what navigator event did we receive?
          switch (event_id)
          {
            ...

            case NAVIGATOR_WINDOW_STATE:

                 switch (navigator_event_get_window_state(event_bps);
                 {
                   case NAVIGATOR_WINDOW_FULLSCREEN:

                        // now we can create timer events
                        g_timer_active = 1; 
                        break;

                   case NAVIGATOR_WINDOW_THUMBNAIL:
                   case NAVIGATOR_WINDOW_INVISIBLE:

                        // we do not want timer events in this state
                        g_timer_active = 0; 
                        break;

                   default:
                        break;
                 }
                 break;

            case NAVIGATOR_WINDOW_ACTIVE:

                 // now we can create timer events
                 g_timer_active = 1; 
                 break;

            case NAVIGATOR_WINDOW_INACTIVE:

                 // we do not want timer events in this state
                 g_timer_active = 0; 
                 break;

            ...
          }
        }

   ...
}

If your application doesn’t need to pause then you would simply
process these events and set g_timer_active to 1 in response to all window states that are received. It is recommended to handle the NAVIGATOR_WINDOW_ACTIVE and NAVIGATOR_WINDOW_INACTIVE events as shown.

CREATE A FRAMEBUFFER OBJECT FOR GRAPHICS

In the Mobile 1UP games, the applications completely render everything to a framebuffer interface (a pixel-by-pixel representation of the display). The BlackBerry 10 platform supports framebuffers in addition to openGL ES for graphics.

To create a framebuffer interface with a 320×240 resolution:

#include <bps/orientation.h>
#include <bps/screen.h>

// global variables
screen_context_t g_screen;
screen_window_t  g_window;
screen_buffer_t  g_framebuffer;

int application_initialize()
{
  int angle, param, pair[2], rect[4];

  ...

  // lets obtain information on the device orientation/angle
  orientation_get(NULL, &angle);

  // lets ensure the launcher does not try to adjust the orientation
  navigator_rotation_lock(1);

  // create the screen context
  screen_create_context(&g_screen, SCREEN_APPLICATION_CONTEXT);

  // create the window
  screen_create_window(&g_window, g_screen);

  // define the screen usage
  param = SCREEN_USAGE_WRITE | SCREEN_USAGE_NATIVE;
  screen_set_window_property_iv(g_window,
                                SCREEN_PROPERTY_USAGE, &param);

  // define the expectations on the framebuffer (16bit, 565)
  param = SCREEN_FORMAT_RGB565;
  screen_set_window_property_iv(g_window,
                                SCREEN_PROPERTY_FORMAT, &param);

  // define the resolution of the window we are expecting
  pair[0] = 320;
  pair[1] = 240;
  screen_set_window_property_iv(g_window,
                                SCREEN_PROPERTY_BUFFER_SIZE, pair);

  // ensure the window is at the correct rotation
  if ((angle % 180) != 0) angle = 0;
  screen_set_window_property_iv(g_window,
                                SCREEN_PROPERTY_ROTATION, &angle); 

  // create a the buffer required for rendering the window
  screen_create_window_buffers(g_window, 1);

  // obtain the framebuffer pointer
  screen_get_window_property_pv(g_window,
                                SCREEN_PROPERTY_RENDER_BUFFERS,
                                (void **)&g_framebuffer);

  // request the window be displayed
  rect[0] = 0;
  rect[1] = 0;
  rect[2] = pair[1];
  rect[3] = pair[2];
  screen_post_window(g_window, g_framebuffer, 1, rect, 0);

  ...
}

void application_terminate()
{
  ...

  // destroy the window
  screen_destroy_window(g_window);

  // destroy the screen context
  screen_destroy_context(g_screen);

  ...
}

The above code is specific to the BlackBerry PlayBook tablet as it is natively a landscape device (hence: angle = 0, 180 is landscape). In application_initialize, a 320×240 framebuffer interface is created that displayed within the bounds of the window (full screen). The window and screen context are released in application_terminate().

Within the BlackBerry 10 platform, a window is scaled automatically to the screen object that it resides in. It is also possible to query the width and height of the screen object before creating the window to have a perfect pixel aspect ratio if required.

To obtain properties of the framebuffer instance (width, height, et al):

{
  int             rowbytes, width, height, pair[2];
  unsigned short *ptr, *p;
  int             i, j, rect[4];

  ...

  // obtain the bounds of the buffer
  screen_get_buffer_property_iv(g_framebuffer,
                                SCREEN_PROPERTY_BUFFER_SIZE, pair);
  width  = pair[0];
  height = pair[1];

  // obtain the rowbytes value (stride) between vertical lines
  screen_get_buffer_property_iv(g_framebuffer,
                                SCREEN_PROPERTY_STRIDE, &rowbytes);

  // obtain the direct framebuffer address of the window buffer
  screen_get_buffer_property_pv(g_framebuffer,
                                SCREEN_PROPERTY_POINTER,
                                (void **)&ptr);

  ...

Using the pointer ptr it is possible to write direct pixel values (16bit 565RGB) values which will be displayed within the window. As an example, some code to create some TV static (random noise):

  ...

  // fill the framebuffer with random pixel values
  p = (unsigned short *)ptr;
  for (j=0; j<height; j++)
  {
    for (i=0; i<width; i++)
    {
      p[i] = (unsigned short)(rand() % 0xffff);
    }
    p += (rowbytes >> 1);
  }

  // flush the framebuffer to the window/display
  rect[0] = 0;
  rect[1] = 0;
  rect[2] = width;
  rect[3] = height;
  screen_post_window(g_window, g_framebuffer, 1, rect, 0);
}

The pointer p is incremented by (rowbytes >> 1) for every row iteration as it is an unsigned short pointer; rowbytes represents the number of bytes on a row – 16bit 565RGB pixels use two bytes. It is important to adjust accordingly based on the rowbytes value not width, as in some framebuffer implementations rowbytes can be more than the width of the buffer for alignment purposes (optimizations).

CREATE A PCM AUDIO CALLBACK SYSTEM FOR MUSIC/SOUND

BlackBerry 10 provides a low level sound library based on the ALSA project libasound, which gives developers byte level access to the audio sub-system on the device. If you are not familiar with PCM audio then a number of third party libraries also exist.

The simplest way to work with low level audio is to initialize the PCM hardware and spawn a new thread to deal with populating the contents of the audio stream:

#include <pthread.h>
#include <sys/asoundlib.h>

// global variables : sound
snd_pcm_t      *g_snd_h;
int             g_snd_running;
pthread_t       g_snd_thread;

int application_initialize()
{
  snd_pcm_channel_info_t   pi;
  snd_pcm_channel_params_t pp;
  int                      card, dev;
  int                      err;

  ..

  // open the preferred PCM handle
  err = snd_pcm_open_preferred(&g_snd_h, &card, &dev, SND_PCM_OPEN_PLAYBACK);
  if (err != 0) return 0;

  // we should disable mmap (as we are not going to use it)
  err = snd_pcm_plugin_set_disable(g_snd_h, PLUGIN_DISABLE_MMAP);
  if (err < 0) return 0;

  // query the capabilities of the channel
  memset(&pi, 0, sizeof(snd_pcm_channel_info_t));
  pi.channel = SND_PCM_CHANNEL_PLAYBACK;
  err = snd_pcm_plugin_info(g_snd_h, &pi);
  if (err < 0) return 0;

  // configure the channel we want to use
  memset(&pp, 0, sizeof(snd_pcm_channel_params_t));
  pp.mode                = SND_PCM_MODE_BLOCK;
  pp.channel             = SND_PCM_CHANNEL_PLAYBACK;
  pp.start_mode          = SND_PCM_START_DATA;
  pp.stop_mode           = SND_PCM_STOP_STOP;
  pp.buf.block.frag_size = pi.max_fragment_size;
  pp.buf.block.frags_max = 1;
  pp.buf.block.frags_min = 1;
  pp.format.interleave   = 1;
  pp.format.voices       = 1;                    // mono
  pp.format.rate         = 11025;                // 11KHz
  pp.format.format       = SND_PCM_SFMT_S16_LE;  // 16-bit
  err = snd_pcm_plugin_params(g_snd_h, &pp);
  if (err < 0) return 0;

  // create the PCM audio thread
  err = pthread_create(&g_snd_thread, NULL,
                       application_pcm_callback, (void *)NULL);
  if (err != 0) return 0; 

  ...
}

void application_terminate()
{
  ...

  // lets bring the PCM audio thread to a close
  g_snd_running = 0;

  // cancel the PCM audio thread
  pthread_cancel(g_snd_thread);
  pthread_join(g_snd_thread, NULL);

  // we must flush the channel
  snd_pcm_plugin_flush(g_snd_h, SND_PCM_CHANNEL_PLAYBACK);

  // close the audio interface device
  snd_pcm_close(g_snd_h);

  ...
}

In application_initialize() we create a 16-bit (little endian), 22KHz mono audio stream for playback. You need to dig around the header files and documentation to find the right configuration values to match your requirements. In application_terminate() we flush the buffer and close the audio interface.

The following code populates the audio buffer and requests playback:

void *application_pcm_callback(void *data)
{
  snd_pcm_channel_status_t  status;
  snd_pcm_channel_setup_t   ps;
  int                       fd;
  fd_set                    fds;
  int                       err;
  int                       buffer_size, frames, total_frames, frags;
  int                       i;
  signed short             *buffer;

  // lets identify how big our "desired" buffer should be
  memset(&ps, 0, sizeof(snd_pcm_channel_setup_t));
  ps.channel = SND_PCM_CHANNEL_PLAYBACK;
  err = snd_pcm_plugin_setup(g_snd_h, &ps);
  if (err < 0) return 0;

  frames = ps.buf.block.frag_size >> 1;   // 16bit, 22Khz, mono
  frags  = 4;
  buffer_size = ps.buf.block.frag_size * frags; ===  

  // allocate the memory we need for the audio buffer
  buffer = (signed short *)malloc(buffer_size);
  if (buffer == NULL) return NULL;

  // obtain the file descriptor from the channel
  fd = snd_pcm_file_descriptor(g_snd_h, SND_PCM_CHANNEL_PLAYBACK);

  // prepare the file descriptors for writing
  FD_ZERO(&fds);

  g_snd_running = 1;
  while (g_snd_running)
  {
    // we only want to fill the buffer while our timer is running
    if (g_timer_active)
    {
      // we must wait until the channel is ready for writing
      fd = snd_pcm_file_descriptor(g_snd_h, SND_PCM_CHANNEL_PLAYBACK);
      FD_SET(fd, &fds);
      select(fd+1, NULL, &fds, NULL, NULL);

      // we ready for writing
      memset(&status, 0, sizeof(status));
      snd_pcm_plugin_status(g_snd_h, &status);

      // do we need to prepare the channel for playback?
      if (status.status == SND_PCM_STATUS_READY    ||
          status.status == SND_PCM_STATUS_UNDERRUN ||
          status.status == SND_PCM_STATUS_OVERRUN)
      {
        err = snd_pcm_plugin_prepare(g_snd_h, SND_PCM_CHANNEL_PLAYBACK);
      }

      // lets fill the buffer with random values (white noise)
      total_frames = frames * frags;
      for (i=0; i<total_frames; i++)
        buffer[i] = (signed short)(rand() % 0xffff);  

      // play buffer
      snd_pcm_plugin_write(g_snd_h, buffer, buffer_size);
    }

    // lets give the operating system a chance to do something
    sched_yield();
  }

  // release memory allocated for the audio buffer
  if (buffer != NULL) free(buffer);

  return NULL;
}

The first step is to query the configuration of the PCM audio playback to determine the fragment size the library is expecting to receive (number of bytes). We will need to take this value and calculate how many frames of audio are required.

I decided the audio buffer should be 4x the fragment size, then allocated a buffer for use. In every iteration, this buffer is filled with random data to simulate white noise (matches with the TV static we created). The resulting data is then provided to library via snd_pcm_plugin_write – passing the buffer and the size in bytes.

<action>play_audio</action>

Accessing audio hardware requires permission which can be done by adding the above entry to the bar-descriptor.xml file in your project.

# Basic libraries required by most native applications
LIBS+= ... asound

When compiling, you will also need to ensure your project links against the asound library – depending on your configuration, this will be editing the common.mk file or working with project settings directly in the IDE.

CREATE A HANDLER FOR INPUT EVENTS LIKE TOUCH AND KEYBOARD

A vital element of all applications is the ability to interact with the user via the touch screen, keyboard and other input devices like the accelerometer (sensor).

BlackBerry 10 offers multiple input types; the main driven within the screen services are pointer, touch and keyboard events. It is possible to connect a Bluetooth® mouse/keyboard to your BlackBerry PlayBook tablet so you may need to handle multiple event cases.

#include <input/screen_helpers.h>
#include <sys/keycodes.h>

int application_initialize();
{
  ...

  // we are interested in screen events (touch, keyboard et al)
  screen_request_events(g_screen);

  ...
}

void application_eventloop()
{
  screen_event_t  event_screen;
  mtouch_event_t  event_touch;
  int             state, key, pos[2];

  ...

        // did we receive a screen event?
        if (event_domain == screen_get_domain())
        {
          event_screen = screen_event_get_event(event_bps);

          // identify the type the screen event
          screen_get_event_property_iv(event_screen,
                                       SCREEN_PROPERTY_TYPE, &event_id);

          switch (event_id)
          {
            case SCREEN_EVENT_POINTER:

                 // identify the position and which buttons are being used
                 screen_get_event_property_iv(event_screen,
                                              SCREEN_PROPERTY_SOURCE_POSITION,
                                              pos);
                 screen_get_event_property_iv(event_screen,
                                              SCREEN_PROPERTY_BUTTONS, &state);

                 // determine if the mouse is down or not
                 if ((state & SCREEN_LEFT_MOUSE_BUTTON) != 0)
                 {
                   // x = pos[0];
                   // y = pos[1];
                 }
                 break;

            case SCREEN_EVENT_MTOUCH_TOUCH:
            case SCREEN_EVENT_MTOUCH_MOVE:

                 // get the information about the touch event
                 screen_get_mtouch_event(event_screen, &event_touch, 0);

                 // for sake of example - focus on first touch
                 if (event_touch.contact_id == 0)
                 {
                   // x = event_touch.x;
                   // y = event_touch.y;
                 }
                 break;

            case SCREEN_EVENT_KEYBOARD:

                 // get the information about the keyboard event
                 screen_get_event_property_iv(event_screen,
                                              SCREEN_PROPERTY_KEY_FLAGS,
                                              &state);

                 // we only care about DOWN/REPEAT cases
                 if ((state & (KEY_DOWN || KEY_REPEAT)) != 0)
                 {
                   screen_get_event_property_iv(event_screen,
                                                SCREEN_PROPERTY_KEY_SYM, &key);

                   // key
                 }
                 break;

            default:
                 break;
          }
        }

  ...
}

In order to start receiving events; they must be requested as shown in application_initialize(). Handling the events is simply a case of detecting the screen event domain and working with the event types received.

CREATE A RESOURCE MANAGER FOR PREFERENCES, GAME ASSETS

The BlackBerry 10 platform provides an application sandbox when an application is executed on a device. This allows the operating system to ensure the application works within its own domain from a security perspective.

${SANDBOX}/
${SANDBOX}/app         - application: .bar contents
${SANDBOX}/data        - application: private data 
${SANDBOX}/temp        - application: temporary data
${SANDBOX}/logs        - application: logs
${SANDBOX}/shared      - shared resources (across all applications)
${SANDBOX}/shared/xxx

During the runtime of the application, the ${SANDBOX} directory can be obtained using the getcwd() API. You can then append the appropriate path and open specific files in the system.

The contents of the .bar file are stored on the device and extracted to the ${SANDBOX}/app directory, so any files packaged within the .bar would be accessible by appending this path with the file name. Application preferences (save state) should be stored in the ${SANDBOX}/data directory and they will survive application upgrades.

In the Mobile 1UP case, all assets/preferences are stored in files so it was simply a matter of defining the appropriate path on the platform. The following code demonstrates how to work with files:

#define MAX_PATH_LENGTH 256

{
  char  sandbox[MAX_PATH_LENGTH];
  char  path[MAX_PATH_LENGTH];

  getcwd(sandbox, MAX_PATH_LENGTH);
  sprintf(path, "%s/data/preferences.dat", sandbox);
}

From here; the path string can be used to open a file using fopen() to read/write values.

The ${SANDBOX}/shared directory requires permission and
allows the application to access common files across applications on the device. It is important to note that it is not possible to restrict sharing to specific applications; if it is in the shared directory, any application can access it.

GENERATE BINDINGS FOR SYSTEM RESOURCES

BlackBerry 10 has an extensive POSIX compliant C-library for the management of threads, memory, strings, date-time, file, networking, timers, math et al – many of which can be used as-is without any problems when an appropriate BlackBerry Platform Services (BPS) API does not exist.

I have put all of the above into a single main.c file, which you can generate on the information above. It is a simple application that simulates a TV screen with no reception, complete with sound effects. You will need to update the appropriate permissions and library dependencies in the project.

/**
 ** main.c
 ** ------
 **
 ** Copyright 2012, Research In Motion
 **
 ** Licensed under the Apache License, Version 2.0 (the "License");
 ** you may not use this file except in compliance with the License.
 ** You may obtain a copy of the License at
 **
 **     http://www.apache.org/licenses/LICENSE-2.0
 **
 ** Unless required by applicable law or agreed to in writing, software
 ** distributed under the License is distributed on an "AS IS" BASIS,
 ** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 ** See the License for the specific language governing permissions and
 ** limitations under the License.
 **/

#include <pthread.h>
#include <stdlib.h>
#include <stdio.h>
#include <signal.h>
#include <input/screen_helpers.h>
#include <sys/asoundlib.h>
#include <sys/keycodes.h>
#include <sys/time.h>

#include <bps/bps.h>
#include <bps/event.h>
#include <bps/navigator.h>
#include <bps/orientation.h>
#include <bps/screen.h>

// global variables : timer
int              g_timer_channel;
int              g_timer_domain;
int              g_timer_ms;
int              g_timer_active;

// global variables : framebuffer
screen_context_t g_screen;
screen_window_t  g_window;
screen_buffer_t  g_framebuffer;

#define FPS 20 // 20 frames/second

// global variables : sound
snd_pcm_t      *g_snd_h;
int             g_snd_running;
pthread_t       g_snd_thread;

void application_timer_callback(int signal)
{
  bps_event_t *e;

  // post an event for processing (if we are active)
  if (g_timer_active)
  {
    bps_event_create(&e, g_timer_domain, 0, NULL, NULL);
    bps_channel_push_event(g_timer_channel, e);
  }
}

void *application_pcm_callback(void *data)
{
  snd_pcm_channel_status_t  status;
  snd_pcm_channel_setup_t   ps;
  int                       fd;
  fd_set                    fds;
  int                       err;
  int                       buffer_size, frames, total_frames, frags;
  int                       i;
  signed short             *buffer;

  // lets identify how big our "desired" buffer should be
  memset(&ps, 0, sizeof(snd_pcm_channel_setup_t));
  ps.channel = SND_PCM_CHANNEL_PLAYBACK;
  err = snd_pcm_plugin_setup(g_snd_h, &ps);
  frames = ps.buf.block.frag_size >> 1;   // 16bit, 22Khz, mono
  frags  = 4;
  buffer_size = ps.buf.block.frag_size * frags;  

  // allocate the memory we need for the audio buffer
  buffer = (signed short *)malloc(buffer_size);
  if (buffer == NULL) return NULL;

  // obtain the file descriptor from the channel
  fd = snd_pcm_file_descriptor(g_snd_h, SND_PCM_CHANNEL_PLAYBACK);

  // prepare the file descriptors for writing
  FD_ZERO(&fds);

  g_snd_running = 1;
  while (g_snd_running)
  {
    // we only want to fill the buffer while our timer is running
    if (g_timer_active)
    {
      // we must wait until the channel is ready for writing
      fd = snd_pcm_file_descriptor(g_snd_h, SND_PCM_CHANNEL_PLAYBACK);
      FD_SET(fd, &fds);
      select(fd+1, NULL, &fds, NULL, NULL);

      // we ready for writing
      memset(&status, 0, sizeof(status));
      snd_pcm_plugin_status(g_snd_h, &status);

      // do we need to prepare the channel for playback?
      if (status.status == SND_PCM_STATUS_READY    ||
          status.status == SND_PCM_STATUS_UNDERRUN ||
          status.status == SND_PCM_STATUS_OVERRUN)
      {
        err = snd_pcm_plugin_prepare(g_snd_h, SND_PCM_CHANNEL_PLAYBACK);
      }

      // lets fill the buffer with random values (white noise)
      total_frames = frames * frags;
      for (i=0; i<total_frames; i++)
        buffer[i] = (signed short)(rand() % 0xffff);  

      // play buffer
      snd_pcm_plugin_write(g_snd_h, buffer, buffer_size);
    }

    // lets give the operating system a chance to do something
    sched_yield();
  }

  // release memory allocated for the audio buffer
  if (buffer != NULL) free(buffer);

  return NULL;
}

int application_initialize()
{
  struct sigaction         action;
  struct itimerval         timer;
  int                      angle, param, pair[2], rect[4];
  snd_pcm_channel_info_t   pi;
  snd_pcm_channel_params_t pp;
  int                      card, dev;
  int                      err;

  // initialize the platform services
  err = bps_initialize(); 
  if (err == BPS_FAILURE) return 0;

  // lets obtain information on the device orientation/angle
  orientation_get(NULL, &angle);

  // lets ensure the launcher does not try to adjust the orientation
  navigator_rotation_lock(1);

  // create the screen context
  screen_create_context(&g_screen, SCREEN_APPLICATION_CONTEXT);

  // create the window
  screen_create_window(&g_window, g_screen);

  // define the screen usage
  param = SCREEN_USAGE_WRITE | SCREEN_USAGE_NATIVE;
  screen_set_window_property_iv(g_window,
                                SCREEN_PROPERTY_USAGE, &param);

  // define the expectations on the framebuffer (16bit, 565)
  param = SCREEN_FORMAT_RGB565;
  screen_set_window_property_iv(g_window,
                                SCREEN_PROPERTY_FORMAT, &param);

  // define the resolution of the window we are expecting
  pair[0] = 320;
  pair[1] = 240;
  screen_set_window_property_iv(g_window,
                                SCREEN_PROPERTY_BUFFER_SIZE, pair);

  // ensure the window is at the correct rotation
  if ((angle % 180) != 0) angle = 0;
  screen_set_window_property_iv(g_window,
                                SCREEN_PROPERTY_ROTATION, &angle); 

  // create a the buffer required for rendering the window
  screen_create_window_buffers(g_window, 1);

  // obtain the framebuffer pointer
  screen_get_window_property_pv(g_window,
                                SCREEN_PROPERTY_RENDER_BUFFERS,
                                (void **)&g_framebuffer);

  // request the window be displayed
  rect[0] = 0;
  rect[1] = 0;
  rect[2] = pair[0]; // width
  rect[3] = pair[1]; // height
  screen_post_window(g_window, g_framebuffer, 1, rect, 0);

  // we are interested in navigator events
  navigator_request_events(0);

  // we are interested in screen events (touch, keyboard et al)
  screen_request_events(g_screen);

  // we need to register a domain for timer event handling
  g_timer_channel = bps_channel_get_active();
  g_timer_domain  = bps_register_domain();

  // whats the desired fps the application wants us to use?
  g_timer_ms = 1000L / FPS;

  // our timer is not active until the window is displayed
  g_timer_active = 0;

  // initialize the interval timer
  memset(&action, 0, sizeof(struct sigaction));
  action.sa_handler = application_timer_callback;
  action.sa_flags   = 0;
  sigemptyset(&action.sa_mask);
  sigaction(SIGALRM, &action, NULL);

  // start the interval timer (repeating)
  memset(&timer, 0, sizeof(struct itimerval));
  timer.it_value.tv_sec     =  g_timer_ms / 1000;
  timer.it_value.tv_usec    = (g_timer_ms % 1000) * 1000;
  timer.it_interval.tv_sec  = timer.it_value.tv_sec;
  timer.it_interval.tv_usec = timer.it_value.tv_usec;
  setitimer(ITIMER_REAL, &timer, NULL);

  // open the preferred PCM handle
  err = snd_pcm_open_preferred(&g_snd_h, &card, &dev, SND_PCM_OPEN_PLAYBACK);
  if (err != 0) return 0;

  // we should disable mmap (as we are not going to use it)
  err = snd_pcm_plugin_set_disable(g_snd_h, PLUGIN_DISABLE_MMAP);
  if (err < 0) return 0;

  // query the capabilities of the channel
  memset(&pi, 0, sizeof(snd_pcm_channel_info_t));
  pi.channel = SND_PCM_CHANNEL_PLAYBACK;
  err = snd_pcm_plugin_info(g_snd_h, &pi);
  if (err < 0) return 0;

  // configure the channel we want to use
  memset(&pp, 0, sizeof(snd_pcm_channel_params_t));
  pp.mode                = SND_PCM_MODE_BLOCK;
  pp.channel             = SND_PCM_CHANNEL_PLAYBACK;
  pp.start_mode          = SND_PCM_START_DATA;
  pp.stop_mode           = SND_PCM_STOP_STOP;
  pp.buf.block.frag_size = pi.max_fragment_size;
  pp.buf.block.frags_max = 1;
  pp.buf.block.frags_min = 1;
  pp.format.interleave   = 1;
  pp.format.voices       = 1;                    // mono
  pp.format.rate         = 11025;                // 11KHz
  pp.format.format       = SND_PCM_SFMT_S16_LE;  // 16-bit
  err = snd_pcm_plugin_params(g_snd_h, &pp);
  if (err < 0) return 0;

  // create the PCM audio thread
  err = pthread_create(&g_snd_thread, NULL,
                       application_pcm_callback, (void *)NULL);
  if (err != 0) return 0; 

  return 1;
}

void application_eventloop()
{
  bps_event_t    *event_bps;
  int             event_domain;
  int             event_id;
  int             running;
  int             event_timeout;
  screen_event_t  event_screen;
  mtouch_event_t  event_touch;
  int             state, key, pos[2];

  // initialize
  event_timeout = 0;

  // start processing events
  running = 1;
  while (running)
  {
    for (;;)
    {
      event_bps = NULL;
      bps_get_event(&event_bps, event_timeout);

      // process the event accordingly
      if (event_bps != NULL)
      {
        event_domain = bps_event_get_domain(event_bps); 

        // did we receive a navigator event?
        if (event_domain == navigator_get_domain())
        {
          event_id = bps_event_get_code(event_bps);

          // what navigator event did we receive?
          switch (event_id)
          {
            case NAVIGATOR_EXIT:

                 // user has requested we exit the application
                 running = 0;
                 break;

            case NAVIGATOR_WINDOW_STATE:

                 switch (navigator_event_get_window_state(event_bps))
                 {
                   case NAVIGATOR_WINDOW_FULLSCREEN:

                        // now we can create timer events
                        g_timer_active = 1;
                        break;

                   case NAVIGATOR_WINDOW_THUMBNAIL:
                   case NAVIGATOR_WINDOW_INVISIBLE:

                        // we do not want timer events in this state
                        g_timer_active = 0;
                        break;

                   default:
                        break;
                 }
                 break;

            case NAVIGATOR_WINDOW_ACTIVE:

                 // now we can create timer events
                 g_timer_active = 1;

                 break;

            case NAVIGATOR_WINDOW_INACTIVE:

                 // we do not want timer events in this state
                 g_timer_active = 0;

                 break;

            default:
                 break;
          }
        }
        else

        // did we receive a screen event?
        if (event_domain == screen_get_domain())
        {
          event_screen  = screen_event_get_event(event_bps);

          // identify the type the screen event
          screen_get_event_property_iv(event_screen ,
                                       SCREEN_PROPERTY_TYPE, &event_id);

          switch (event_id)
          {
            case SCREEN_EVENT_POINTER:

                 // identify the position and which buttons are being used
                 screen_get_event_property_iv(event_screen ,
                                              SCREEN_PROPERTY_SOURCE_POSITION,
                                              pos);
                 screen_get_event_property_iv(event_screen ,
                                              SCREEN_PROPERTY_BUTTONS, &state);

                 // determine if the mouse is down or not
                 if ((state & SCREEN_LEFT_MOUSE_BUTTON) != 0)
                 {
                   // x = pos[0];
                   // y = pos[1];
                 }
                 break;

            case SCREEN_EVENT_MTOUCH_TOUCH:
            case SCREEN_EVENT_MTOUCH_MOVE:

                 // get the information about the touch event
                 screen_get_mtouch_event(event_screen , &event_touch, 0);

                 // for sake of example - focus on first touch
                 if (event_touch.contact_id == 0)
                 {
                   // x = event_touch.x;
                   // y = event_touch.y;
                 }
                 break;

            case SCREEN_EVENT_KEYBOARD:

                 // get the information about the keyboard event
                 screen_get_event_property_iv(event_screen ,
                                              SCREEN_PROPERTY_KEY_FLAGS,
                                              &state);

                 // we only care about DOWN/REPEAT cases
                 if ((state & (KEY_DOWN || KEY_REPEAT)) != 0)
                 {
                   screen_get_event_property_iv(event_screen ,
                                                SCREEN_PROPERTY_KEY_SYM, &key);

                   // key
                 }
                 break;

            default:
                 break;
          }
        }        
        else

        // did we receive a timer event?
        if (event_domain == g_timer_domain)
        {
          unsigned short *p, *ptr;
          int             rowbytes, width, height, pair[2];
          int             i, j, rect[4];

          // obtain the bounds of the buffer
          screen_get_buffer_property_iv(g_framebuffer,
                                        SCREEN_PROPERTY_BUFFER_SIZE, pair);
          width  = pair[0];
          height = pair[1];

          // obtain the rowbytes value (stride) between vertical lines
          screen_get_buffer_property_iv(g_framebuffer,
                                        SCREEN_PROPERTY_STRIDE, &rowbytes);

          // obtain the direct framebuffer address of the window buffer
          screen_get_buffer_property_pv(g_framebuffer,
                                        SCREEN_PROPERTY_POINTER,
                                        (void **)&ptr);

          // fill the framebuffer with random pixel values
          p = (unsigned short *)ptr;
          for (j=0; j<height; j++)
          {
            for (i=0; i<width; i++)
            {
              p[i] = (unsigned short)(rand() % 0xffff);
            }
            p += (rowbytes >> 1);
          }

          // flush the framebuffer to the window/display
          rect[0] = 0;
          rect[1] = 0; 
          rect[2] = width; 
          rect[3] = height;
          screen_post_window(g_window, g_framebuffer, 1, rect, 0);
        }
      }
      else break;
    }
  }
}

void application_terminate()
{
  struct itimerval timer;

  // lets bring the PCM audio thread to a close
  g_snd_running = 0;

  // cancel the PCM audio thread
  pthread_cancel(g_snd_thread);
  pthread_join(g_snd_thread, NULL);

  // we must flush the channel
  snd_pcm_plugin_flush(g_snd_h, SND_PCM_CHANNEL_PLAYBACK);

  // close the audio interface device
  snd_pcm_close(g_snd_h);

  // terminate the interval timer
  memset(&timer, 0, sizeof(struct itimerval));
  setitimer(ITIMER_REAL, &timer, NULL);

  // destroy the window
  screen_destroy_window(g_window);

  // destroy the screen context
  screen_destroy_context(g_screen);

  // shutdown the platform services
  bps_shutdown();
}

int main(int argc, char **argv)
{
  // lets go! 
  if (application_initialize())
  {
    application_eventloop();
    application_terminate();
  }

  return EXIT_SUCCESS;
}

I hope that the information I posted here is beneficial to you and it helps in your initial steps in porting your applications over to the BlackBerry 10 platform. It took Mobile 1UP less than a week from downloading the NDK to having the first title available for sale in the BlackBerry App World™ storefront – which I personally consider a success story.

About Aaron Ardiri

I am a Senior Technical Evangelist for Research In Motion. I have worked with various mobile platforms for many years with a focus on native application development - at the same time, I run a game development studio (www.mobile1up.com) focusing on various platforms when i get some spare CPU cycles.

Join the conversation

Show comments Hide comments
+ -
blog comments powered by Disqus