Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

End-to-End Real World BlackBerry Application, Part 5

0.00/5 (No votes)
3 Sep 2008 4  
Real world BlackBerry application walkthrough, Part 5.

Introduction

This is the fifth part of my end-to-end BlackBerry application walk-through. In this article, I will go over the following subjects:

  • Using resource files for localizing text strings without coding changes
  • Setting an application icon on the Ribbon
  • Home Screen’s command list icons
  • Trimming the cached articles list
  • Encoding HTTP request data
  • Application “About” dialog
  • Alerting the user when a long-running operation is taking place

Using Resource Files for Localizing Text Strings

Although an in-depth discussion about resource files is beyond the scope of this article (a good place to see more information on this subject is the BlackBerry Java Development Environment Fundamentals Guide, which you can obtain from the BlackBerry developers site), I will tell you that each application needs a resource header file, a resource contents file for the root (global) locale, and a resource content file for each specific locale it will support.

The resource header file defines descriptive keys for each localized string. Resource content files map descriptive keys to values for the global and specific locales.

Resources for a given locale end up being stored in a ResourceBundle object. A ResourceBundleFamily object groups the resources for an application as a collection of ResourceBundle objects. This is the mechanism an application uses to switch languages depending on the user's locale.

For our application, I created the resource header file (KnowledgeBase.rrh) and a resource contents file for the root locale (KnowledgeBase.rrc). Based on these files, the BlackBerry IDE will create, at compile time, a resource interface, KnowledgeBaseResource, that we can use to gain access to the localized strings. The resource interface has the same name as the resource header file, with the word Resource appended.

To access our localized resources through the application’s resource bundle, we first need to obtain a reference to our resource bundle:

private ResourceBundle resources = 
         ResourceBundle.getBundle(KnowledgeBaseResource.BUNDLE_ID,
         KnowledgeBaseResource.BUNDLE_NAME);

Now, we’re ready to retrieve a localized string, like so:

String screenTitle = resources.getString(KnowledgeBaseResource.TTL_HOME_SCRN);

Like I said at the beginning of this section, there’s much more to resources than what I’ve covered here. You should plan on learning more, and I would suggest you start by checking out the BlackBerry Java Development Environment Fundamentals Guide, available on the BlackBerry developers site.

Setting an Application Icon

It’s time now to replace the default application icon with something nicer. To set the application icon through the BlackBerry JDE, add the image that will serve as the application icon to the Project and bring up its properties:

Setting-App-Icon.gif

In the File Properties applet, select “Use as application icon”:

Setting-App-Icon2.gif

And, when we run the application, we see the new application icon:

App-Icon-Ribbon.jpg

Note that although this procedure allows you to set the application icon, it does not allow you to change the image when the application is selected or unselected within the Ribbon. I will describe how to achieve this behavior in a future article.

Home Screen’s Command List Icons

Originally, the three commands on the Home Screen were preceded by the same icon, a green star:

Home-Screen1.gif

I’ve made some changes to the Home Screen so that now, each command is preceded by an icon that reflects the command’s function:

Home-Screen-Icons.jpg

I achieved this by modifying the code in the MyObjectListField class within the Home Screen. In a nutshell, MyObjectListField now has three private bitmaps, one per command, and when it’s time to draw the list row, the appropriate bitmap is drawn before the command. This is how the code looks:

private class MyObjectListField extends ObjectListField {
        
    private Bitmap searchIcon = 
      Bitmap.getBitmapResource(resources.getString(
      KnowledgeBaseResource.IMG_SEARCH));  
    private Bitmap tagsIcon = 
      Bitmap.getBitmapResource(resources.getString(
      KnowledgeBaseResource.IMG_TAGS_LIST)); 
    private Bitmap recentIcon = 
      Bitmap.getBitmapResource(resources.getString(
      KnowledgeBaseResource.IMG_VIEW_RECENT)); 
    private Bitmap icon;

    // We are going to take care of drawing the item.
    public void drawListRow(ListField listField, Graphics graphics, 
            int index, int y, int width) {
                
        if ( mainMenuItems[index] == mnuSearchTitles) {
            icon = searchIcon;
        }   
        if ( mainMenuItems[index] == mnuBrowseTags) {
            icon = tagsIcon;
        }  
        if ( mainMenuItems[index] == mnuViewRecent) {
            icon = recentIcon;
        }

        if (null != icon) {
            int offsetY = (this.getRowHeight() - icon.getHeight())/2;
            graphics.drawBitmap(1,y + offsetY, icon.getWidth(),
                    icon.getHeight(),icon,0,0);
            graphics.drawText(mainMenuItems[index], 
                icon.getWidth() + 2, y, DrawStyle.ELLIPSIS, 
                width - icon.getWidth() + 2);
        } else {
            graphics.drawText("- " + mainMenuItems[index], 0,
                y, DrawStyle.ELLIPSIS, width - 
                graphics.getFont().getAdvance("- "));
        }
    }
}

Trimming the Cached Articles List

Based on the requirements I set, our application should maintain an in-memory cache with the most recently viewed articles.

The application’s DataStore class is already capable of handling the storage and retrieval of the cached articles via the setCachedArticles() and getCachedArticles() methods. We also have a way for the user to set the maximum number of cached articles there can be.

What we’re missing is the mechanism to add such an article to the cache (once a user opens an article) and prune the cache so the number of articles does not exceed the maximum the user defined. A good place to implement such a mechanism is the Article Screen. Let’s add some code that will take care of these possible scenarios:

  1. The cache is empty
  2. The cache has some articles, but it still has room
  3. The cache is full
  4. The article we’re viewing is already cached

When the cache is empty, we only need to add the currently viewed article:

// Handle the case when there's nothing cached.
if (null == cachedItems || cachedItems.length == 0) {
            
    cachedItems = new Article[]{article};
    DataStore.setCachedArticles(cachedItems);
    return;
            
}

When the cache is not empty but still has room, we only need to insert the current article at the top of the list:

// Handle the case when we need to add one more article to the list.
Article[] newList = new Article[cachedItems.length + 1];
                
// Insert the old items in the new list.
for (int i = 1; i <= cachedItems.length; i++) {
                     
 // We're shifting the articles too, to leave room at the top of the list
 // for the article we're currently viewing.
 newList[i] = cachedItems[i - 1];
                     
}
                
// Now, move the article we're viewing to the top of the list.
newList[0] = article;
                
DataStore.setCachedArticles(newList);

When the cache is full, we have to make room for the current article:

// Handle the case when the cached need to be trimmed.
Article[] newList = new Article[maxAllowed];
// Insert all the items, except the oldest, in the new list.
for (int i = 1; i < maxAllowed; i++) {
                    
 // We're shifting the articles too, to leave room at the top of the list
 // for the article we're currently viewing.
 newList[i] = cachedItems[i - 1];
                    
} 
                
// Now, move the article we're viewing to the top of the list.
newList[0] = article;
                
DataStore.setCachedArticles(newList);

When the article we’re viewing is already cached, we just bail out:

// Bail if the article we're viewing is already cached.
for (int i = 0; i < cachedItems.length; i++) {                
 if (article.id.equalsIgnoreCase(cachedItems[i].id)) {
  return;
 }
}

Looks like we took care of all the scenarios, doesn’t it? Well, not quite yet. What happens if the cache has N number of articles and the user, through the Options Screen, changes the maximum number of articles the cache can store to a number less than N? To handle this situation, we need to add some code to the Options Screen:

private void trimCachedArticlesList() {
        
    Article[] cached = DataStore.getCachedArticles();
    
    if (null == cached || cached.length == 0) {
        return;
    }
    
    int maxAllowed = DataStore.getMaxCachedArticles();
    
    if (cached.length &lt;= maxAllowed) {
        return;
    }
    
    Article[] newList = new Article[maxAllowed];
    
    for (int i = 0; i < maxAllowed; i++) {
        newList[i] = cached[i];
    }
    
    DataStore.setCachedArticles(newList);

}

And now, I think we’re in good shape. If I forgot some detail, I’m sure you will let me know. :)

Encoding HTTP Request Data

On the subject of forgetting details, back when I added the HTTP handling code, the routine that took care of creating the HTTP request data looked like this:

public static String createRequestString(final String[] keys, final String[] values) {
        
    StringBuffer requestContents = new StringBuffer(&quot;&quot;);
    if (keys != null) {               
          for (int i = 0; i < keys.length; i++) {
             requestContents.append(keys[i] + "=" +  values[i] + &quot;&amp;&quot;);
          }
    }
    // Terminate the request with a valid sequence.
    requestContents.append("0=0\r\n";);    
       
    return requestContents.toString();
}

Everything seems fine with this code until you start wondering what would happen if any actual values (any of the values[i]) in the request data contained reserved characters like ";", "/", "?", ":", "=", or "&". And, it can become as serious as it became for me last week when a production application started “randomly” failing because of this encoding issue.

What we need here is to make sure that our request data is correctly encoded so our server-side application can make sense of the request.

Working to fix my last week’s emergency, I stumbled upon net.rim.blackberry.api.browser.URLEncodedPostData. URLEncodedPostData is precisely a class that encodes form data using URL-encoding. Let’s use it to take care of our encoding issues:

public static byte[] createPostData(final String[] keys, final String[] values) {

    URLEncodedPostData postData = 
      new URLEncodedPostData(URLEncodedPostData.DEFAULT_CHARSET, true);
    
    if (keys != null) {
         for (int i = 0; i < keys.length; i++) {
             postData.append( keys[i], values[i]);
         }
     }

     return postData.getBytes();
}

Application “About” Dialog

I thought of the “About” dialog as a nice touch for our application. It is triggered by the “About” menu that I just added to the Home Screen.

About-Dialog.jpg

Of interest here is the use of ApplicationDescriptor to obtain the application name and the version number:

private MenuItem aboutMenu = 
     new MenuItem(resources.getString(KnowledgeBaseResource.MNU_ABOUT),150,10) {
   public void run() {
       String crNotice = resources.getString(KnowledgeBaseResource.LBL_ABOUT);
       ApplicationDescriptor descriptor = 
              ApplicationDescriptor.currentApplicationDescriptor();
       String appVersion = descriptor.getName() + " " + descriptor.getVersion();
       Dialog.alert(appVersion + "\n" + crNotice);
   } 
};

Alerting Users When Long-Running Operations Take Place

Some of the features of our application, like searching for articles, can potentially involve long execution times. It’s important that we don’t frustrate our users by starting a long-running operation without giving them some visual clue indicating that our program is actually working and they might need to wait some time for an operation to complete.

Some developers prefer to use screens as status indicators, but a simple dialog does just fine.

Downloading-Dialog.jpg

This is how it’s done:

Bitmap icon = Bitmap.getBitmapResource(
              resources.getString(KnowledgeBaseResource.IMG_DOWNLOAD)); 
statusDlg = new Dialog(resources.getString(
                KnowledgeBaseResource.LBL_DOWNLOADING),null,null,0,icon); 
private void sendHttpRequest(String[] keys, String[] values) {
        
   String url = DataStore.getAppServerUrl();
   // Make sure we can create an http request.
   // If the app. server URL has not been set, we cannot make any http requests.
    if (null == url || url.length() == 0) {
        Dialog.alert(resources.getString(
               KnowledgeBaseResource.ERR_INVALID_APP_SERVER_URL));
        return;
    }
    
    statusDlg.show();
 .
 .
 .
}
public void processResponse(final int HTTPResponseCode, final String data) {
    UiApplication.getUiApplication().invokeLater(new Runnable() {
        public void run()
        {
            statusDlg.close();
 .
 .
 .
}

What's Next

Having covered the above important details, we still need to write the server-side code to handle incoming HTTP requests, retrieve information from the database, and send responses back to the application on the BlackBerry device.

History

Here are the links to the previous articles of this series:

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here