Browse DevX
Sign up for e-mail newsletters from DevX


Can You Build One Set of BREW User Interfaces for All Targeted Devices?

Ideally, a single set of user interface (UI) code would work on every BREW device, regardless of screen size or BREW version. Can this ideal be realized by a single, catch-all approach to BREW UI design? If so, what's the best way to do it? Murray Bonner reveals all.




Building the Right Environment to Support AI, Machine Learning and Deep Learning

In this installment, you'll explore the challenges a developer faces when trying to build a single set of user interface code that will work on all targeted platforms. You'll see what goes into creating a multi-control BREW Dialog at run time. Differences between the emulator and actual handsets, in addition to differences between handsets themselves, are highlighted and workarounds are given. The conclusion underscores what is possibly the most important principle of BREW development: as you develop your application, test it on the actual, targeted handsets at frequent intervals.

BREW Dialogs

Figure 1 - A dialog occupies the lower portion of the screen while the drawing activity it controls occurs in the upper portion.

Figure 1 shows a simple UI that controls shape rendering in the upper portion of the screen. The UI itself consists of two list controls and a soft key menu contained by a BREW dialog. The dialog was composed using the BREW Resource Editor (please refer to the BREW Resource Editor Guide for more information). The two list controls determine which shape is drawn in the upper portion of the display and which color is used to do the drawing. The soft key menu choices are Shape, used to move the focus to the list control that handles shape choice, Color, used to transfer control to the second list control which controls drawing color, and Done, which simply exits the application. The left and right keys are used to navigate between the two list controls.

Figure 2 - A dialog containing 2 text controls and a menu.

The following sample application creates and displays the dialog shown running on the emulator in Figure 2. (Be warned: this code requires augmentation before it will work properly on a handset -- more on this later).

For an admittedly contrived example, the displayed result looks reasonably attractive. Now, what happens if we try to port the application to a device with a significantly smaller screen? For starters, in order to shrink the dialog, the soft key menu would have to go! (Arguably, it should not have been there in the first place, since the Clear and End keys can be used to exit the application. For the time being, please bear with me and imagine that the soft key menu serves some useful purpose.) Generally, such a situation would lead to a complete dialog redesign, using the resource editor. Thus, a separate .bar file would be compiled, exclusively for the newly targeted phone. Even if the questionable soft key menu had been excluded from the outset, the dialog's size would still have to change to accommodate the smaller display. Since a dialog's rectangle can't be modified at run time, this means we're stuck with creating, and, worse, maintaining a separate version of the resource file just to get the application working on a less generously proportioned screen. Is there another way?

Building Dialogs at Run Time
As you will see, the code for constructing a dialog at run time is relatively complex and fraught with subtle idiosyncrasies.

#include "AEEModGen.h" // Module interface definitions #include "AEEAppGen.h" // Applet interface definitions #include "AEEShell.h" // Shell interface definitions #include "AEEMenu.h" #include "AEEText.h" #include "AEEStdLib.h" #include "dlghard.bid" /* The use of BREW SDK 1.0 and BREW Emulator 2.0 are assumed. This example demonstrates how to create a dialog at run time. The dialog has 3 controls: 2 ITextCtls, and 1 IMenuCtl. */ #define IDTXT1 (uint16)1 #define IDTXT2 (uint16)2 #define TXTPROPS TP_FRAME #define IDMNU (uint16)3 #define MNUPROPS (uint16)0 #define IDMNULI1 (uint16)1 #define IDMNULI2 (uint16)2 #define IDMNULI3 (uint16)3 #define NUMMNUITEMS 0 #define DLGID (uint16)1 #define NUMCTLS (uint16)3 #define DLGPROPS CP_3D_BORDER #define FOCUSCTL IDTXT1 // dialog layout parameters #define MARGIN 15 #define INSET 1 typedef struct _dlghardapp { AEEApplet a; IDialog *m_pDlg; } dlghardapp; /*------------------------------------------------------------------- Function Prototypes -------------------------------------------------------------------*/ static boolean dlghard_HandleEvent(IApplet * pi, AEEEvent eCode, uint16 wParam, uint32 dwParam); static boolean initData(dlghardapp *papp); int AEEClsCreateInstance(AEECLSID ClsId,IShell * pIShell,IModule * po, void ** ppObj) { *ppObj = NULL; if(ClsId == AEECLSID_DLGHARD) { if(AEEApplet_New(sizeof(dlghardapp), ClsId, pIShell,po,(IApplet**)ppObj, (AEEHANDLER)dlghard_HandleEvent,NULL) == TRUE) { if(initData((dlghardapp*)*ppObj)) return (AEE_SUCCESS); } } return (EFAILED); } static boolean initData(dlghardapp *papp) { papp->m_pDlg = NULL; return TRUE; }

The above code is shown merely for the sake of completeness. The only two things of interest are the #defines and the global definition of the application data structure type, dlghardapp. The type name is intended to capture the fact that this example shows how to create dialogs the hard way.

static boolean dlghard_HandleEvent(IApplet * pi, AEEEvent eCode, uint16 wParam, uint32 dwParam) { dlghardapp *papp = (dlghardapp*)pi; switch (eCode) { case EVT_APP_START: { int bold_h = IDISPLAY_GetFontMetrics(papp->a.m_pIDisplay, AEE_FONT_BOLD, NULL, NULL); int norm_h = IDISPLAY_GetFontMetrics(papp->a.m_pIDisplay, AEE_FONT_NORMAL, NULL, NULL); IMenuCtl *pmnu = NULL; ITextCtl *ptxt = NULL; int r = 0; // return value from ISHELL_CreateDialog() AEEDeviceInfo *pdevinfo = (AEEDeviceInfo*)MALLOC(sizeof(AEEDeviceInfo)); int vert_space = 0; // inter-control spacing /* Allocate memory for a DialogInfo struct with NUMCTLS controls Notes: ====== The DialogInfo struct includes space for 1 DialogItem. Thus, we only need to allocate space for NUMCTLS-1 (2) additional DialogItems. */ DialogInfo *pdi = (DialogInfo*) MALLOC(sizeof(DialogInfo) + (NUMCTLS-1) * sizeof(DialogItem)); DialogItem di; ISHELL_GetDeviceInfo(papp->a.m_pIShell, pdevinfo); // calculate the size of a vertical, inter-control spacer vert_space = (pdevinfo->cyScreen - (3*bold_h+3*norm_h+22*INSET))/4;

The above code snippet shows the beginning of the application event handler. At the top of the EVT_APP_START case, the heights of the device's bold and normal fonts are determined. These measurements are later used to lay out the screen.

After allocating memory for an AEEDeviceInfo struct, memory is allocated for the DialogInfo struct that will be passed to ISHELL_CreateDialog(). Figure 3, below, shows what goes into populating a DialogInfo struct.  For those of you unfamiliar with Unified Modeling Language (UML) notation the diagram shows that a DialogInfo struct has 1, and only 1, DialogInfoHead and 0 to many DialogItems (though one would not, in practice, create a dialog without any DialogItems, it is technically possible to do so).  In turn, a DialogInfoHead has 1 AEERect and each DialogItem has a single DialogItemHead and 0 to many DListItems (the example shown here uses 0 DListItems).  Lastly, a DialogItemHead also includes a single AEERect that determines the DialogItem's position and size.

A control is a DialogItem (e.g. a menu or text control). As shown in Figure 2, the dialog under consideration contains 3 DialogItems—2 text controls and a menu. Given that the DialogInfo's controls array only includes room for 1 DialogItem, space for the 2 additional controls must be included in the call to MALLOC().

A DListItem represents a single menu selection. A text control has no DListItems while a menu may have several. In this sample, no DListItem's are added to the DialogItem struct. Instead, as shown later, the menu items are created by calling IMENUCTL_AddItem() after the dialog has been instantiated.

The subsequent call to ISHELL_GetDeviceInfo() provides access to the screen dimensions. The height of the display (pdevinfo->cyScreen) is used to calculate the amount of vertical, inter-control spacing that will spread the controls evenly over the available space. As indicated in the calculation, each of the 3 controls occupies bold_h + norm_h plus an allowance for a border around the control's contents.

Figure 3 - The structs nested within a DialogInfo struct.

// populate the DialogInfoHead pdi->h.wID = DLGID; // the dialog's 16 bit ID pdi->h.nControls = NUMCTLS; // total number of controls in dialog // use default rect (-1, -1, -1, -1) ==> Full Screen pdi->h.rc.x = -1; pdi->h.rc.y = -1; // dialog rectangle pdi->h.rc.dx = -1; pdi->h.rc.dy = -1; pdi->h.dwProps = DLGPROPS; // dialog properties // dialog title not supported pdi->h.wTitle = 0; pdi->h.wFocusID = FOCUSCTL; // ID of control with initial focus

The initialization of the DialogInfoHead member of the DialogInfo struct is very straightforward.  For more information on the structs used, please see the Data Structures section of the BREW API Reference.

// populate first DialogItem directly pdi->controls[0].h.cls = AEECLSID_MENUCTL; // classID of control pdi->controls[0].h.wID = IDMNU; // control ID pdi->controls[0].h.nItems = NUMMNUITEMS; // # menu items pdi->controls[0].h.dwProps = 0; // menu properties pdi->controls[0].h.wTextID = 0; // ID of text string pdi->controls[0].h.wTitleID = 0; // ID of title string pdi->controls[0].h.rc.x = MARGIN; // rectangle dimensions pdi->controls[0].h.rc.dy = bold_h + norm_h + 10*INSET; pdi->controls[0].h.rc.y = pdevinfo->cyScreen - vert_space - pdi->controls[0].h.rc.dy; pdi->controls[0].h.rc.dx = pdevinfo->cxScreen - 2*MARGIN;

The DialogItems are added to the controls array in reverse focus order. In other words, the control that is last in the focus order (in this case, the menu) should be first in the controls array and the control that is first in the focus order should be last in the controls array. As a direct consequence of this reverse packing scheme, the controls' rectangles are laid out from the bottom of the dialog up. This will become more evident as we add the next two controls.

The wTextID and wTitleID are set to zero since their inclusion here would serve no useful purpose. This is due to the fact that the call to ISHELL_CreateDialog() ignores the resource file name parameter when a DialogInfo pointer is supplied (i.e. when the dialog is not defined in a resource file). This is not a problem, however, since the text and title can be set after the dialog has been created. Specifically, each menu item's text will be set by IMENUCTL_AddItem() and the menu's title will be set by calling IMENUCTL_SetTitle().

// set up next DialogItem. // memory contents will be copied to pdi->controls[1] di.h.cls = AEECLSID_TEXTCTL; // Class ID di.h.wID = IDTXT2; // Control ID di.h.nItems = 0; // a text ctl has no items di.h.dwProps = TXTPROPS; // control properties di.h.wTextID = 0; di.h.wTitleID = 0; di.h.rc.x = MARGIN; // position the control's rectangle di.h.rc.dy = bold_h + norm_h + 6*INSET; di.h.rc.y = pdi->controls[0].h.rc.y - vert_space - di.h.rc.dy; di.h.rc.dx = pdi->controls[0].h.rc.dx; // copy the DialogItem into controls[1] MEMCPY((void*)((long)&pdi->controls[0] + sizeof(DialogItem) - sizeof(DListItem)), &di, sizeof(DialogItem));

Here again, the wTextID and wTitleID are not used. The text control's title will be set after the dialog has been created, using ITEXTCTL_SetTitle() and the user will enter his / her own text.

The call to MEMCPY() requires some explanation. First of all, the address of the controls array's first element must be cast to type long in order for the address to be computed correctly. According to BREW Technical Support, there is no known explanation for this requirement at this time. In determining the address of the next element, the first element must be left untouched. Thus, the address calculation hops over the memory allocated to controls[0]. Recall that no DListItems were added to the first element (i.e. the menu). For this reason, the size of a DListItem must be subtracted from the size of the previously included DialogItem, in calculating the next element's address. (Aside: I owe a big thank you to the BREW Technical Support people for helping me figure this out)

// menu di.h.cls = AEECLSID_TEXTCTL; di.h.wID = IDTXT1; di.h.nItems = 0; di.h.dwProps = TXTPROPS; di.h.wTextID = 0; di.h.wTitleID = 0; di.h.rc.dy = bold_h + norm_h + 6*INSET; di.h.rc.y -= vert_space + di.h.rc.dy; MEMCPY((void*)((long)&pdi->controls[0] + 2*sizeof(DialogItem) - 2*sizeof(DListItem)), &di, sizeof(DialogItem)); // create the dialog r = ISHELL_CreateDialog(papp->a.m_pIShell,NULL , 0, pdi);

Above, the stack-based DialogItem is reused to set up the remaining text control, prior to inclusion in the DialogInfo struct as controls[2]. In this case, the address calculation in the call to MEMCPY() must skip over 2 DialogItems, less the size of 2 DListItems. A second DListItem must be factored into the calculation since the previously added text control also did not include any DListItems.

Finally, the call to ISHELL_CreateDialog() instantiates the dialog using the DialogInfo struct just populated.

if(r == SUCCESS) { papp->m_pDlg = ISHELL_GetActiveDialog(papp->a.m_pIShell); ptxt = (ITextCtl*)IDIALOG_GetControl(papp->m_pDlg, IDTXT1); if(ptxt) { ITEXTCTL_SetTitle(ptxt, NULL, 0, L"Text Control 1"); ITEXTCTL_SetActive(ptxt, TRUE); // redraws control } ptxt = (ITextCtl*)IDIALOG_GetControl(papp->m_pDlg, IDTXT2); if(ptxt) { ITEXTCTL_SetTitle(ptxt, NULL, 0, L"Text Control 2"); ITEXTCTL_Redraw(ptxt); } pmnu = (IMenuCtl*)IDIALOG_GetControl(papp->m_pDlg, IDMNU); if(pmnu) { IMENUCTL_AddItem(pmnu, NULL, 0, IDMNULI1, L"Menu Item 1", 0); IMENUCTL_AddItem(pmnu, NULL, 0, IDMNULI2, L"Menu Item 2", 0); IMENUCTL_AddItem(pmnu, NULL, 0, IDMNULI3, L"Menu Item 3", 0); IMENUCTL_SetTitle(pmnu, NULL, 0, L"Menu"); IMENUCTL_Redraw(pmnu); } FREE(pdi); FREE(pdevinfo); return TRUE; } else { FREE(pdi); FREE(pdevinfo); ISHELL_CloseApplet(papp->a.m_pIShell, TRUE); return FALSE; } }

If the call to ISHELL_CreateDialog() succeeds, each of the controls is set up in turn, otherwise dynamically allocated memory is released and the applet is closed.  Note that setting the focus ID in the DialogInfo header does not activate the control. For this reason, it is necessary to call ITEXTCTL_SetActive() when IDTXT1 is set up. This call ensures that the text control's frame is drawn when the application starts.  All of the API calls shown here are self-explanatory.  If more detail is required, please read the relevant section in the BREW API Reference that comes with the SDK.

For completeness, the remainder of the application's code is shown below.  EVT_DIALOG_INIT, EVT_DIALOG_START, and EVT_DIALOG_END must, at minimum, return TRUE in order for the dialog to work properly.  The AVK_CLR handler returns FALSE, so that the AEE will automatically cause the application to exit when the clear key is pressed whenever the menu has the focus (a text control handles AVK_CLR -- later it will be shown that, in some cases, it will automatically cause the application to exit if  the text control is empty).  Lastly, since the dialog was created in EVT_APP_START, it is released in EVT_APP_STOP, via the call to ISHELL_EndDialog().

case EVT_DIALOG_INIT: case EVT_DIALOG_START: case EVT_DIALOG_END: return TRUE; case EVT_KEY: switch(wParam) { case AVK_CLR: return FALSE; // exit app. } return TRUE; case EVT_APP_STOP: { ISHELL_EndDialog(papp->a.m_pIShell); return TRUE; } default: break; } return FALSE; }

This code runs flawlessly on the BREW Emulator (v2.0). The user can navigate between the 2 text controls, and from a text control to the menu , using the phone's up and down keys (pressing up while on IDTXT1 moves the focus to the menu, as does pressing down on IDTXT2). The focus is moved from the menu to IDTXT2 using the left key, and from the menu to IDTXT1 by pressing the right key.

Due to a known, but undocumented, bug in BREW 1.x, this automatic dialog navigation fails completely when the application is ported to any handset equipped with version 1.x of the AEE (the Motorola T720 (BREW AEE 1.1) and the Sharp Z800 (BREW AEE 1.0) were the only handsets tested). On the phones, pressing direction keys has absolutely no affect on the dialog's focus. This navigation bug is fixed in BREW 2.0 (and subsequent versions).  

There are a couple of other differences between the application's behavior on the handsets versus what is observed when the same code runs on the emulator.  On the Sharp Z800, both text controls appear active on startup (i.e. the frames are displayed and the cursors are visible).  On the T720, the menu shows 2 items at the same time, due to that phone's different allowance for space around the title and menu item text (i.e. the INSET amount required on the emulator and the Z800 is excessive for the smaller text border on the T720).

These observations underscore the importance of testing all code on each targeted handset, throughout the development process.

Making the Same Code Work on Both the Handsets and the Emulator
The following changes are required to ensure that the same set of code will operate on both handsets, in addition to the emulator:

  1. Code must be added to handle inter-control navigation.
  2. IDTXT2 must be set to inactive at startup (for the Z800).
  3. A list control should be used in place of the menu (for the T720), as it is designed to show only one selection at a time.  The list control appears to be unaffected by the apparent difference between the size of the control's text border on each phone.

The necessary code revisions are shown below.  The necessary changes are highlighted with comments.

... /* The use of BREW SDK 1.0 is assumed. This example demonstrates how to create a dialog at run time. The dialog has 3 controls: 2 ITextCtls, and 1 IMenuCtl. This code, with only 1 exception, behaves the same way on the BREW Emulator v2.0, and both tested handsets (the Sharp Z800 and Motorola T720). The exception occurs when the clear key (key code AVK_CLR) is pressed while an empty ITextCtl has the focus. Specifically, the Sharp phone handles AVK_CLR by returning TRUE from ITEXTCTL_HandleEvent(), whether the text control is empty or not. Recall that, in order for AVK_CLR to cause an application to exit, the application's event handler must return FALSE in response to the key event. This difference does not mean there is anything wrong with the Z800, it merely reflects the OEM's implementation decision regarding AVK_CLR handling. Thus different handsets can behave differently in response to the same code. */ ... // The revised applet data structure.. typedef struct _dlghardapp { AEEApplet a; IDialog *m_pDlg; // control pointers added to app. data struct ITextCtl *m_pTxt1, *m_pTxt2; IMenuCtl *m_pMnu; IControl *m_pFocusCtl; } dlghardapp; ... static boolean dlghard_HandleEvent(IApplet * pi, AEEEvent eCode, uint16 wParam, uint32 dwParam) { dlghardapp *papp = (dlghardapp*)pi; switch (eCode) { case EVT_APP_START: { ... /* Set up the first DialogItem for controls[0]. A list menu control is used in place of a standard menu control. */ pdi->controls[0].h.cls = AEECLSID_LISTCTL; // control's class ID ... if(papp->m_pTxt2) { ITEXTCTL_SetTitle(papp->m_pTxt2, NULL, 0, L"Text Control 2"); /* Call to SetActive() is needed for Sharp Z800 since both text controls otherwise appear active when the application starts on this phone. The frame appears around the text control for a split second on startup, then it disappears due to this call. */ ITEXTCTL_SetActive(papp->m_pTxt2, FALSE); // unnecessary on MOT720 ITEXTCTL_Redraw(papp->m_pTxt2); } /* IMENUCTL_SetTitle() is commented out since the title skews the List control to the right by the length, in pixels, of the word "Menu". If a title is really required, IDISPLAY_DrawText() could be used to position it above the list control, thereby maintaining the list control's horizontal size. IMENUCTL_SetTitle(papp->m_pMnu, NULL, 0, L"Menu"); */ ... /* Every key press generates the event sequence EVT_KEY_PRESS, EVT_KEY, EVT_KEY_RELEASE. The dialog first sends EVT_KEY_PRESS to the application's event handler. Then, the dialog gives the currently active control (i.e. the control that has the focus) a chance to handle the EVT_KEY event associated with the same key press. The EVT_KEY event will only be passed to the application if the control with the focus does not handle it. Thus, one can say that a control "consumes" an event that it handles. EVT_KEY_PRESS must NOT be used for navigation as the EVT_KEY associated with the same key press would be processed by the control to which the focus is sent. For example, if EVT_KEY_PRESS was used for navigation and the DOWN key was pressed while IDTXT2 has the focus, the EVT_KEY_PRESS handler would respond by moving the focus to the list control (papp->m_pMnu), where the associated EVT_KEY would be consumed, causing the list selection to change in response to the same DOWN key! */ case EVT_KEY: switch(wParam) { /* On the emulator, MOT720 and Sharp Z800, pressing the clear key in a text control deletes a char to the left of the cursor, until the front of the control is reached. If the text control is empty, pressing the clear key at the front exits the application only on the MOT720 (BREW v1.1) and the emulator (v2.0). On the Sharp Z800 (BREW v1.0), there is no response, due to the aforementioned AVK_CLR handling. If the list ctl is active, pressing the clear key causes the app. to exit regardless of what platform the app. is running on. This is due to the fact that the EVT_KEY associated with the AVK_CLR gets passed to this application event handler, since it is not consumed by the list control (i.e. the list control is not interested in AVK_CLR). In order to make the application's behavior on the Sharp Z800 conform to that observed on the other 2 platforms, a cursor position tracking scheme would have to be implemented. Then, when the insertion point is positioned at the front of the text control, and the length of the control's string is zero, EVT_KEY_PRESS could be used to intercept the AVK_CLR and cause the application to exit, via a call to ISHELL_CloseApplet(). Please see the article "Low Level Management of Multi-Control Screens Under BREW" for an example of a cursor position tracking scheme that could be employed for this purpose. */ case AVK_CLR: return FALSE; // exit app. /* For the phones, due to a bug in the BREW 1.x implementation, the programmer must supply the navigation scheme that came for free with the initial version of this code running on v2.0 of the emulator. Note that this bug has been eliminated in BREW version 2.0. */ case AVK_UP: if(papp->m_pFocusCtl == (IControl*)papp->m_pTxt1) { IDIALOG_SetFocus(papp->m_pDlg, IDMNU); papp->m_pFocusCtl = (IControl*)papp->m_pMnu; } else if(papp->m_pFocusCtl == (IControl*)papp->m_pTxt2) { IDIALOG_SetFocus(papp->m_pDlg, IDTXT1); papp->m_pFocusCtl = (IControl*)papp->m_pTxt1; } break; case AVK_DOWN: if(papp->m_pFocusCtl == (IControl*)papp->m_pTxt1) { IDIALOG_SetFocus(papp->m_pDlg, IDTXT2); papp->m_pFocusCtl = (IControl*)papp->m_pTxt2; } else if(papp->m_pFocusCtl == (IControl*)papp->m_pTxt2) { IDIALOG_SetFocus(papp->m_pDlg, IDMNU); papp->m_pFocusCtl = (IControl*)papp->m_pMnu; } break; case AVK_LEFT: if(papp->m_pFocusCtl == (IControl*)papp->m_pMnu) { IDIALOG_SetFocus(papp->m_pDlg, IDTXT2); papp->m_pFocusCtl = (IControl*)papp->m_pTxt2; } break; case AVK_RIGHT: if(papp->m_pFocusCtl == (IControl*)papp->m_pMnu) { IDIALOG_SetFocus(papp->m_pDlg, IDTXT1); papp->m_pFocusCtl = (IControl*)papp->m_pTxt1; } break; } return TRUE; ... } return FALSE; }

When the amount of code required to initialize the DialogInfo struct is compared to that saved by using a dialog, it becomes clear that a developer seeking a platform-agnostic code base should dispense with the dialog altogether.  Instead, BREW programmers should use the individual controls to lay out each user interface at run time.  Even then, as demonstrated by the menu control on the MOT720, the developer cannot be sure that one handset's ideal UI code will automatically perform as expected when ported to a different handset.

Thus, the dialog-based approach to user interface design covered in this article is not recommended for general use.  Even so, the information presented here is useful because it helps a developer gain a deeper understanding of user interface elements, event handling and BREW in general.

Source code for a non-dialog-based solution, behaviorally identical to the dialog version just presented, is available for download (9K).  As with the dialog-based version above, pressing clear on the Sharp Z800 will only cause the application to exit if the menu has the focus.  On the other two platforms, pressing clear while an empty text control has the focus will also cause the application to exit.  For another UI example that uses only the low-level controls, please see the article Low Level Management of Multi-Control Screens Under BREW.

Perhaps the single, greatest lesson that can be gleaned from this article is that one should regularly test code on the targeted handsets, instead of relying exclusively on the emulator until development nears completion.

Originally published in the BREW Wireless Resource Center

Murray Bonner is President of Golden Creek Software Inc., a BREW software developer and a provider of custom training and software development services. Murray has been working with the BREW SDK for nearly 2 years and writes to share his BREW development experience, gained mostly at the expense of his hairline. He welcomes your
Comment and Contribute






(Maximum characters: 1200). You have 1200 characters left.



Thanks for your registration, follow us on our social networks to keep up-to-date