Palm Developer Tips & Tricks

Donate with PayPal! Donate with PayPal!

After a few years of Palm software development, I have learned a few tips and tricks that may be useful for other developers. As a matter of fact, I know some of them are helpful, since I sometimes receive direct requests about some feature in my softs. This is how I came with the idea for this page.

I intend to use this space to present the most interesting tips I have collected, in the form of short information, sample sources or anything in between.

This will start with a single tip, one for which I receive quite a number of queries, but I expect the page to grow with time. Let me know if you have specific requests ;-)

Since I write only C code (with good old prc-tools and pilrc), the occasional sample source will be in this language. However, most of the tips I will discuss here may be used with any language and development tool.

 
Quick Connect, "Map" option
 

The Palm Tungsten T (and later devices, except the Treo series) comes with an enhanced version of the address book which offers a feature named "Quick Connect". This feature allows to open another application with the context of the contact on which it was launched. The standard configuration lets you easily place a call, send an SMS or a mail to the contact you have selected.

This feature is based on simple helper notifications, well documented in the PalmOS developer guides (for example, check this chapter in the PalmOS Companion guide). I will not enter in the details of this API and I assume you have understood how it works.

However you may have noticed that there is an option in Quick Connect, named "Map", which is not documented at all. I will give you the missing information, which I used to build "A2Map", a small utility that "Quick Connects" to a ViaMichelin navigation software and centers the selected contact address location on the map.

The first piece of information you need to implement a helper for the "Map" service is its "service class": 'mapH' (as for all service classes, this is an unsigned long integer).

The second piece of information required (and much more difficult to find out) is the data your helper receives when the user tries to "Quick Connect". Here is the C structure type of the "detailsP" parameter coming with the notification:

typedef struct
{
  UInt16 version;
  UInt32 recid;
  Char * address;
  Char * city;
  Char * state;
  Char * zipcode;
  Char * country;
} HelperServiceMapDetailsType;

and a few explanations:

- version
Structure version number (should be 1).
- recid
Record ID of the selected contact in the Contacts database (useful if you need information about the contact other than his address).
- address, city, state, zipcode, country
Pointers to the different address fields.
 
Writing a DA (Desk Accessory)
 

Let's assume you know what a DA is. If not, check this page first. Now, if you came here, it's because you want to create your own DA. There used to be a lot of resources about the subject but, since the Japanese have more or less given up their Palms, the web sites are closing down one after the other (yes, the DA concept originated in Japan and most of the apps as well).

Although what I will discuss here is applicable to any language, the sample code and the "recipes" are designed for C developers (and preferably those using prc-tools). For users of PP Compiler, there is another source of information with a sample written in their favorite language. As a matter of fact, my sample is directly derived from this one (Thanks, Palmipod ;-).

Before reading the explanations, you may download the source for the sample. Before you start building your DA from this sample, remember you need a CreatorID as for any PalmOS software. You may set your own ID in the makefile

Now, let's begin with the core of the matter. A DA is a piece of executable code that differs from a regular program on the following points:

  • The resource type must be 'DAcc' (not 'appl').
  • The code resource must have an ID of 1000 (not 1).
  • A DA is invoked via a direct call to the "main" function of this code resource, which must be either at the top of the code resource or designated as the "entry point" (there is no startup code and there is no notion of a "launch code").
  • As for any type of stand-alone code (it is also true for static libraries and for hacks), you cannot use static and global variables (that's a good exercise for those who abuse of them ;-)
  • Generally speaking, a DA must run on very limited resources. An arbitrary rule is that a DA should never use more than 2kb of stack.
  • You may define and use any graphical resource you want but it is a good policy to have only one form (without a menu) and this form should not cover the entire screen. It appears that this form should have a 'SAVEBEHIND' attribute.
  • Give the user a mean of quitting your DA. It is generally achieved by a tap outside the DA's form but it may also take the form of an 'OK' button.

You're still with me? OK, let's see how this translates in code.

1) First the resource type: setting the type is easily done with a '-t DAcc' parameter in the 'build-prc' command.

2) While we are in the makefile, let's take care of the absence of start-up code and the specific code resource ID: a '-nostartfiles' parameter in the link command will take care of the first and a '--no-check-resources' in the 'build-prc' commmand ensures the tool will not complain about the missing standard resource.

3) The last change we have to implement in the makefile is about defining the 'entry point'. In our sample, it will be a function named 'DAMain'. We specify this to the linker with a '-e DAMain' parameter.

Let's summarize this with a complete pair of link and build commands:

$(CC) -nostartfiles -e DAMain $(CFLAGS) -o $@ $(OBJS)
$(BUILDPRC) --no-check-resources -o $@ -n $(ICONTEXT) -c $(APPID) -t DAcc SampleDA SampleDA.ro

The rest of the makefile is pretty standard.

4) Next step: just a quick look at the form resource (in SampleDA.rcp):

FORM ID ID_FORM_MAIN AT (30 30 100 80)
MODAL
SAVEBEHIND
USABLE
...

5) Now let's see the C code itself (in SampleDA.c). We will start with changing the code resource ID to 1000. For this we will use the 'STANDALONE_CODE_RESOURCE_ID' macro, defined in 'standalone.h' (this is a standard prc-tools header file). At the top of your source, you then insert:

#include <standalone.h>
STANDALONE_CODE_RESOURCE_ID(1000);

And now, we have to write the code for the entry point, which is a simple function without parameter and returning nothing. To make things clearer, I have split the whole process in separate functions:

void DAMain(void)
{
  FormPtr form = NULL;
  // Start application
  StartApplication(&form);
  // Test if OK
  if (form != NULL)
  {
    // Run
    EventLoop();
    // Stop
    StopApplication(form);
  }
}

The function 'StartApplication()' will initialize and display the main form:

static void StartApplication(FormPtr * form)
{
  // Load the form resource.
  *form = FrmInitForm(ID_FORM_MAIN);
  // Check if OK
  if (*form)
  {
    // Set as active form
    FrmSetActiveForm(*form);
    // Display form
    FrmDrawForm(*form);
  }
}

The function 'EventLoop()' manages the actual event loop:

static void EventLoop(void)
{
  EventType event;
  Boolean   done = false;
  do
  {
    // Wait next event
    EvtGetEvent(&event, evtWaitForever);
    // System event processing & system form events handler
    if (!SysHandleEvent(&event) && !FrmHandleEvent(FrmGetActiveForm(), &event))
    {
      // Local form handler
      done = MainFormHandleEvent(&event);
    }
  }
  while (!done);
}

(Note that in the sample, there is no menu and the event loop has no provision to handle menu events)

And the final function 'StopApplication()' takes care of cleaning up after the user quits the DA:

static void StopApplication(FormPtr form)
{
  // Remove form from screen
  FrmEraseForm(form);
  // Erase form data from memory
  FrmDeleteForm(form);
}

As you may have noticed, the form handler (MainFormHandleEvent in the example) is called directly from the event loop and it must return 'true' when the user quits. The sample source includes the code that let the user quit the DA with a stylus tap outside the main window (check the 'penDownEvent' event processing).

The original "Desk Accessory Specification" is located here

 
Playing with the network services
 

OK, this is not something you are likely to do every day, but when you start looking for information on such things as listing the network services, querying or changing the default services, you will realize this is a challenge... I am not talking about creating or modifying a service configuration: there are quite a few sources of information for this out there.

I stumbled upon the problem for a few days, while planning for NetSelDA, when I finally found what I was looking for in an old post by Gavin Peacock. And everything suddenly becomes simple! You just have to query the network panel for the services need...

And indeed, you can find these lines in the standard SystemMgr.h header:

// Standard Service Panel launch codes (used by network panel, dialer panel, etc.)
#define sysSvcLaunchCmdSetServiceID    20
#define sysSvcLaunchCmdGetServiceID    21
#define sysSvcLaunchCmdGetServiceList  22
#define sysSvcLaunchCmdGetServiceInfo  23

What was missing though, is a description of the data we should exchange with the network panel... Gavin gives us the answers.

The 'sysSvcLaunchCmdSetServiceID' & 'sysSvcLaunchCmdGetServiceID' launch codes both expect a pointer to a 'ServiceIDType' as a parameter:

typedef UInt32 ServiceIDType;

The 'sysSvcLaunchCmdGetServiceInfo' code (which returns a service name given its ID) expects a pointer to a 'serviceInfoType' structure:

#define maxServiceNameLen       32              // Max length of a service name
typedef struct
{
  Err           error;                          // Error code
  ServiceIDType serviceID;                      // ID of service to query (in)
  UInt32        reserved;                       // Reserved - set to 0
  Char          serviceName[maxServiceNameLen]; // Name of service (out)
} serviceInfoType, * serviceInfoPtr;

And the 'sysSvcLaunchCmdGetServiceList' code, expects a pointer to a 'serviceListType' structure:

typedef struct
{
  Err           error;                          // Error code
  UInt16        numServices;                    // Number of services available
  MemHandle     IDListH;                        // MemHandle to an array of serviceIDs
  MemHandle     nameListH;                      // MemHandle to an array of service names
} serviceListType, * serviceListPtr;

On return, the 'IDListH' and 'nameListH' handles contain a list of service IDs and names, respectively. The handles are allocated by the network panel but it is the caller's responsibility to dispose of them when done.

Now, let's use this in an example: we will query the list of available services and display them in a simple UI list (with an ID of 'ID_LIST_NETC'). Note that the code is provided as an example, you should assemble the parts yourself (including error checking, memory release, ...).

1) We need to launch the network panel and to do so, we have to find its 'card number' and 'db ID':

static Boolean FindNetworkPanel(UInt16 * card, LocalID * dbid)
{
  DmSearchStateType searchState;
  // Find card number and db id for network panel
  if (DmGetNextDatabaseByTypeCreator(true, &searchState, sysFileTPanel,
           sysFileCNetworkPanel, true, card, dbid) == errNone)
  {
    return true;
  }
  // Error
  return false;
}

2) Then we can send a query to the network panel. This is the interesting part. You would do about the same for querying or setting the current default service or to get information about a service.

static Boolean GetServiceList(UInt16 card, LocalID dbid, serviceListType * srvList)
{
  UInt32 result;
  // Request a list of services
  if (SysAppLaunch(card, dbid, 0, sysSvcLaunchCmdGetServiceList,
                   (MemPtr) &srvList, &result) == errNone)
  {
    // Any service returned
    if (pNetSelData -> srvList.numServices > 0)
    {
      // OK
      return true;
    }
  }
  // Error
  return false;
}

3) And finally, to display the list (this part is rather straightforward...):

static void FillUIServiceList(serviceListType * srvList, ListPtr list)
{
  // Lock the name list handle
  Char * srvNameList = (Char *) MemHandleLock(srvList -> nameListH);
  // Create an array for the UI list
  MemHandle srvNameArray = SysFormPointerArrayToStrings(srvNameList, srvList -> numServices);
  // Lock handle
  Char ** srvNames = (Char **) MemHandleLock(srvNameArray);
  // Fill UI list
  LstSetListChoices(list, srvNames, srvList -> numServices);
}
 
A simple HTTP library
 

Network applications are becoming quite common these days, particularly over HTTP, and recent devices from Palm come with a high-level HTTP library. However, if you want to write an HTTP-aware program supporting older PalmOS versions, you have to write everything from scratch.

I encountered this problem in 2002 and came accross a library by Laurent Demailly that could help. Since it was released as open source and I needed some enhancements, I have made some changes to the original code, mainly to make a regular PalmOS shared library and to support HTTP 1.1.

You may download the complete source and executable package here. Just as the original work from Laurent, this is released under the Artistic License. Please check the 'readme.txt' file before doing anything.

Here, I won't enter the details of the sources of the library itself but I am going to show you how you may use the library from your own program. For this, we will proceed through the steps required to read a page from a web server, through the 'GET' method.

First, you must include the library header in your source:

#include "httplib.h"

You can then open the library:

UInt16 HTTPLibRef;
Err err;
// Find the library if it's already loaded
err = SysLibFind(HTTPLibName, &HTTPLibRef);
// Not found
if (err != errnone)
{
  // Load the library
  err = SysLibLoad(sysFileTLibrary, HTTPLibCreator, &HTTPLibRef);
}
// We have a reference to the library
if (err == errNone)
{
  // Open it
  err = HTTPLibOpen(HTTPLibRef);
}
// The library is open
if (err == errNone)
{
  // Open the Net Library
  // At this stage, the device will try to open the default network service
  err = HTTPLibNetOpen(HTTPLibRef);
}
// Something went wrong
if (err)
{
  // Process errors...
  FrmAlert(LoadLibraryAlert);
  return err;
}

And when it's time to send your request to the server, just run the 'HTTPLibGet' function:

err = HTTPLibGet(HTTPLibRef, &ret, theURL, &data, &len, type);

Where:

  • err is the general return code (errNone is no error)
  • HTTPLibRef is the library reference number (obtained on opening the library)
  • ret is the HTTP return code. It also holds network errors when they occur. The complete list of possible codes is described in the library header file (httplib.h)
  • theURL is the URL you are querying
  • data is a pointer to the string of characters that will receive the page you requested.
  • len is an integer holding the length of the content returned by the server (as found in the HTTP response headers)
  • type is a string holding the content type returned by the server (as found in the HTTP response headers)

Notes:

  • If you pass a null pointer for the resulting page, the library will issue a 'HEAD' request and will return the length and type of content of the resource you are accessing.
  • The library expects to find a content length header in the HTTP response. Otherwise it will return an error.
  • You must allocate the string that will hold the content type returned by the library. It should be of sufficient length and there is no control about this (there is potential for serious problems, here)
  • The library allocates the buffer that holds the returned data. Upon return, you must check if the buffer has been actually allocated. It is your responsibility to free it when you are done with the data

When you are done with the library, there is some clean-up to do:

Err err;
Int16 usecount;
// Free memory allocated by the library
if (data)
{
  MemPtrFree(data);
}
// Close the net library
HTTPLibNetClose(HTTPLibRef);
// Close HTTP library
err = HTTPLibClose(HTTPLibRef, &usecount);
// If closed and not used anymore
if (err == errNone && usecount == 0)
{
  // Remove from memory
  SysLibRemove(HTTPLibRef);
}
 
Creating a preference panel
 

Implementing a program in the "Preferences" of the PalmOS is not very complex but to do it cleanly requires some care. Since there is no point reinventing the wheel, I have packaged most of what is needed in a pair of source files (one header file utilpanl.h and the actual C source utilpanl.c), that you may download freely. And the rest of this article will explain how to use this material.

Let's start with a bit of theory about the PalmOS preferences and a "preferences panel":

  • To be included in the preferences, a program must have a 'panl' type (instead of 'appl').
  • Because there are 2 different implementations of the Preferences application on present and past PalmOS devices (the "original" version and the more recent one developped by Palm), the user interface for a panel must adapt to the context.
  • In the "original" Preferences application, your panel must provide the means for the user to switch to another panel. In this version, the current panel should also be stored so that it opens the next time the user opens the Preferences application.
  • In the "Palm" version, you should have a "Close" button that allows the user to return to the Preferences application.

Now let's put this in practice. First, you must include the utilpanl.c code in your build (compilation & link).

Setting the resource type for the program is easily done with a '-t panl' parameter in the 'build-prc' command:

$(BUILDPRC) -o $@ -n $(ICONTEXT) -c $(APPID) -t panl MyPref MyPref.ro

The various mandatory UI elements required in all versions of a preferences panel are included in this resource template:

FORM ID ID_FORM_MAIN AT (0 0 160 160)
NOFRAME
NOSAVEBEHIND
USABLE
DEFAULTBTNID ID_BUTN_CLOS
BEGIN
  TITLE        "APPNM"
  POPUPTRIGGER ""      ID ID_TRIG_PANL AT (RIGHT@159 1 AUTO AUTO)    USABLE RIGHTANCHOR
  LIST         ""      ID ID_LIST_PANL AT (PREVLEFT PREVTOP 55 AUTO) NONUSABLE VISIBLEITEMS 1
  POPUPLIST            ID ID_TRIG_PANL ID_LIST_PANL
  LABEL        "APPNM" ID ID_LABL_TITL AT (RIGHT@159 1)              NONUSABLE

  /* Insert your own controls here */

  BUTTON       "BTEND" ID ID_BUTN_CLOS AT (2 144 AUTO AUTO)          NONUSABLE FRAME
END

where:

  • APPNM is your panel "title" and BTEND is the label for for the "close" button. You may replace this strings with your own text or use the TRANSLATION feature in PilRC to localize these strings.
  • The various ID_ identifier must be defined somewhere. If you want to minimize your work, you should define them in a resources.rh file (it is the file name used in utilpanl.h)
  • You may notice that some controls have a NONUSABLE attribute: the form is designed to be compatible with the "original" version of the Preferences application by default and the source will change the UI only when invoked from a new version.

The code for your application is not very different from a standard one. In the PilotMain() function, you have to process some specific launch codes (used with the "original" Preferences application):

UInt32 PilotMain(UInt16 cmd, void * cmdPBP, UInt16 launchFlags)
{
  // Test launch code
  switch (cmd)
  {
    // Process normal launch & preferences launch
    case sysAppLaunchCmdNormalLaunch:
    case sysAppLaunchCmdPanelCalledFromApp:
    case sysAppLaunchCmdReturnFromPanel:
    // Store call mode and save current panel
    PanelSetCallMode(cmd == sysAppLaunchCmdPanelCalledFromApp, myCreatorID);

    // Proceed with your normal event loop
    /* !!! Your code here !!! */
    break;
  }
}

The call to PanelSetCallMode is used to store the launch mode for future use (through a global variable, which is not very nice but it's the only way to make this work with an autonomous utility package). It also saves your panel as the current one in the Palm preferences, so that the next time the preferences are opened, it will be on that same panel. For this to work, you must replace the myCreatorID with your own creator ID (as an UIn32 of course).

The actual application processing is done after and should include the opening of the main form:

FrmGotoForm(ID_FORM_MAIN);

as well as a regular event loop.

All that is left to do is some specific "preferences" tasks handling in your main form handler. Here is a skeleton for the handler:

static Boolean MainFormHandleEvent(EventType * event)
{
  Boolean handled = false;
  switch (event->eType)
  {
    case frmOpenEvent:
    // Initialize pref panel
    PanelFormInit(FrmGetActiveForm());
    // Draw form content
    FrmDrawForm(form);
    handled = true;
    break;

    case ctlSelectEvent:
    // Close button
    handled = PanelSelectClose(event);
    // Other controls
    if (!handled)
    {
    }
    break;

    case popSelectEvent:
    handled = PanelSelectPanel(event);
    break;

    case frmCloseEvent:
    PanelFormClose();
    break;
  }
  return handled;
}

Of course, for each of these events, you may also add your own processing...

If you are interested, here are a few explanations about this code:

  • PanelFormInit hides & shows the right controls in the form, depending on the context (old or new Preferences application);
  • PanelSelectClose processes the close button on recent versions;
  • PanelSelectPanel switches the preferences panel after a new one has been selected in the pick list (for old versions);
  • PanelFormClose cleans up memory before exit.