In the world of server and desktop software development, the product development cycle is often measured in months or years. Not so in mobile development, where carrier certification requirements force you to release specific builds of your application for each target on which it will run. Even if you’re planning on a long development cycle between features, to remain competitive you must allocate resources to provide ports of your application on as many handsets as possible. Sometimes, this isn’t just common sense but a requirement of carriers, who want to see as many applications as possible available on the broadest range of handsets as possible.
Spending a bit of time up-front to consider the effort required to keep your application running on the latest hardware can go a long way to streamline the application porting process once your application is on the market. Developers have praised the Qualcomm BREW platform for ease of porting-in most cases ports between handsets can take only a few days-but with planning you can make this success story more likely to happen for your product.
Key to speedy handset ports is reducing the changes you must make to your application binaries between releases: it’s obvious that the less code you change, the less likely you are to introduce other problems within your application. The three tricks I outline here all have that goal in mind, letting one application run seamlessly across many different BREW-enabled handsets.
Know Your Device Capabilities
First and foremost, it’s crucial you know the capabilities of your target handsets, both now and in the future. Surveying the features-screen size, media support, heap resources, and so forth-of handsets on the market today is crucial when designing your application; keeping current and learning what a specific target supports is required before trying to bring up your application on new hardware. The Qualcomm Web site provides data sheets for both current and preproduction hardware; these data sheets are required reading at my office for anyone involved in handset porting.
Becoming familiar with device capabilities helps you create a lowest-common-denominator approach to many aspects of your application, such as user interface and media formats. For example, a glance at the data sheets immediately suggests that while there are a wide variety of screen sizes, today’s BREW-enabled handsets typically sport screens of one of the following types:
- Small. Small devices, such as the Audiovox CDM-180, have screens small enough to be the secondary displays on larger handsets. Often barely over a hundred pixels on a side, screens on these devices may odd aspect ratios as well. Not quite in this category, but not as large as medium screens, are the first color handsets that often had screens that were around 128×176 pixels or so.
- Medium. Many devices now use a standard size of 176×220 pixels, regardless of manufacturer or carrier. Moreover, a number of handsets use screens roughly that size, usually a few pixels within that size along one or both dimensions.
- Large. Many high-end handsets are moving towards providing a QVGA screen in either portrait or landscape layout.
Binning handset screens into these categories-small, medium, and large-makes designing user interfaces much easier. A good GUI designer can work to provide one set of screen layouts for an application that works well for all three screen sizes, and then build appropriate images and text for each of those three profiles. Application developers should use dynamic layout whenever possible when building the code that lays out each screen (using BREW uiOne, constraint and proportional containers, or brute-force arithmetic with older or home-grown controls) to ensure the same look and feel across all handsets.
This approach is effective whenever you’re setting out to design your application. Other areas you should consider binning and looking for commonalities include media support (supported image types or audio types) and supported interfaces. For back-of-the-envelope estimation purposes, it’s safe to assume that a specific series of handsets from a single manufacturer running a specific version of BREW probably (but not always) will support the same media types. But before you plan your product launch, it’s wise to check the data sheets!
Discover Handset Features at Run Time
If different handsets require different code paths-say your application uses BCI images on handsets made by LG and PNG images on handsets made by Samsung – you have two choices. You can either make the decision at compile time and create two different binaries, or you can make the decision at run time. To do this, however, you need to know what kind of handset is executing your application. You can do this using the ISHELL_DeviceInfo function, consulting your handy device sheets for the platform id’s for the handsets you’re supporting:
AEEDeviceInfo di = {0}; di.wStructSize = sizeof( AEEDeviceInfo ); ISHELL_GetDeviceInfo( pMe->a.m_pIShell, &di ); switch( di.dwPlatformID ) { case xxx: pMe->dwImageClsID = AEECLSID_BCI; break; case yyy: pMe->dwImageClsID = AEECLSID_PNG; break; }
This works, and for the handsets you’re supporting now, lets you use a single binary. But what about when LG or Samsung ships a new handset? You will need to go back and extend the terms of your case statement again to include the new class id’s. Granted that this isn’t a terribly risky change, anything that opens up the binary to change injects risk.
There’s a better way. If you’re interested in providing these abstractions at run time, you can simply embed them in a configuration file you read at run-time. Reading and parsing a small name-value list at runtime is a fast operation; even parsing a configuration file in XML takes no noticeable time to the user. Once this is done, you can create a configuration file for each of LG and Samsung handsets; the LG handset might read:
Image_classid: 16793606
And then you can read the configuration file using the IFileMgr, IFile, and IGetLine interfaces. Using IGetLine, you can obtain a single line using IGETLINE_GetLine, break it into its name and value parts, and then extract the information you need. In fact, if you’re running on BREW 2.x and greater handsets, you can just load the data into an IRamCache and query the IRamCache when you need the data:
#define NEW_INTERFACE( cls, p ) ISHELL_CreateInstance( pMe->a.m_pIShell, cls, (void **)&p ) #define ANOTHER_INTERFACE( previous_result, cls, p ) ( ( result ) == SUCCESS ) ? ISHELL_CreateInstance( pMe->a.m_pIShell, cls, (void **)&p ) : ( result ); #define RELEASEIF( p ) if (p) { IBASE_Release((IBase *)p); p = 0; } #define MAX_LINE_LEN 128 int App_LoadConfig( CApp *pMe ) { GetLine sLine = {0}; IFileMgr *pifm = NULL; IFile *pif = NULL; ISourceUtil *pisu = NULL; IGetLine *pigl = NULL; ISource *pis = NULL; int result = EFAILED; int r; result = NEW_INTERFACE( AEECLSID_FILEMGR, pifm ); result = ANOTHER_INTERFACE( result, AEECLSID_SOURCEUTIL, pisu ); if ( SUCCESS == result ) { pif = IFILEMGR_OpenFile( pifm, "config.txt", _OFM_READ ); if ( pif ) { result = ISOURCEUTIL_SourceFromAStream( pisu, (IAStream *)pif, &pis ); if ( SUCCESS == result ) ISOURCEUTIL_GetLineFromSource( pisu, pis, MAX_LINE_LEN, &pigl ); if ( SUCCESS == result ) { // While there are lines in the file, get a // line, fracture it, and stash the values in the cache. do { r = IGETLINE_GetLine( pigl, &sLine, IGETLINE_CR ); if ( sLine.nLen > 0 ) { char *pSep = STRCHR( sLine.psz, '=' ); IRAMCACHE_Add( pMe->pIConfigCache, sLine.psz, pSep - sLine.psz, pSep+1, STRLEN( pSep ) ); } } while ( IGETLINE_END !=r && IGETLINE_ERROR != r); } } else { result = IFILEMGR_GetLastError( pifm ); } } RELEASEIF( pifm ); RELEASEIF( pif ); RELEASEIF( pisu ); RELEASEIF( pis ); RELEASEIF( pigl ); return result; }
This is all pretty basic stuff: create the necessary IFileMgr and ISourceUtil interfaces, open the file, and get an GetLine interface for the file using ISourceUtil. With that, the code reads one line at a time, separating the name and value pairs using the STRCHR call, and inserts each name/value pair into the cache.
This trick lets you abstract not just simple things like class id’s of needed interfaces that way, but whole bits of program behavior. Instead of using conditional-compile flags, you can embed the appropriate code for different handsets (say, a work-around to a platform issue) in a function, and test against the value of the flag in the configuration file before invoking the appropriate function.
Rely on the Network
Most BREW applications use the network at some point. If your application needs to access the network anyway, why not have it fetch its configuration file over the network when it first runs? It can save the resulting data on the file system as a file or entries for the application preferences to avoid making the transaction in the future, or simply make the request each time (letting you update application configuration options after an application has shipped).
How you do this is somewhat application- and carrier-dependent, of course. Some carriers may place restrictions preventing your application from making a network transaction during the splash screen, because it may slow a user’s access to the application. If you can, it’s best to piggyback a request for configuration atop other network activity to keep your application as interactive as possible. Another option is to deliver whatever configuration information might be needed with the results of a specific transaction. Of course, sometimes this is implicit: if your server is serving images to applications and it knows what platform the application is running on, all it needs to do is deliver images of that type (say, BCI instead of PNG).
Unlike the previous example, where I suggest that using the platform ID is a bad choice for identifying the handset for the purposes of selecting code paths, using the platform ID is the ideal choice here, because you can pass the platform ID as an HTTP header and the server can use the platform ID as an index into a database of configuration information. Simply get the platform ID and print it into a template such as X-BREW-Platform-ID: %d, and then set a custom header using IWeb like this:
WebOpt aWebOpts[2]; AEEDeviceInfo di = {0}; di.wStructSize = sizeof( AEEDeviceInfo ); ISHELL_GetDeviceInfo( pMe->a.m_pIShell, &di ); SPRINTF( pMe->szPlatformHeader, "X-BREW-PlatformID: %d", di.dwPlatformID ); aWebOpts[0].nId = WEBOPT_HEADER; aWebOpts[0].pVal = (void *)pMe->szPlatformHeader; aWebOpts[1].nId = WEBOPT_END; if (IWEB_AddOpt(pMe->piWeb, aWebOpts) != SUCCESS) { DBGPRINTF("SetHeaders webopt failed"); }
Your server can then look up the device’s capabilities in its database and present results appropriate for the destination device. (This is, of course, analogous to how many Web applications use the HTTP User-Agent string to determine your browser type and tune the HTML content for your specific browser.)
Conclusion
In porting to new handsets, the goal is to do so with as few changes to your application as possible. By coding your application so that one binary can run on the broadest number of handsets, you reduce the both the likelihood of error (which can slow the porting process tremendously) and eliminate the costly edit-build-test cycle, reducing it to a reconfigure-test cycle. Try these tricks in your next application for Qualcomm BREW; you won’t be disappointed.
Related Resources
QUALCOMM BREW: http://www.qualcomm.com/brew/
About the Author
Ray Rischpater is the chief architect at Rocket Mobile, Inc., specializing in the design and development of messaging and information access applications for today’s wireless devices. Ray Rischpater is the author of several books on software Development including eBay Application Development and Software Development for the QUALCOMM BREW Platform, both available from Apress, and is an active Amateur Radio operator. Contact Ray at kf6gpe@lothlorien.com.