Order the book from O'Reilly

Previous PageTable Of ContentsIndexNext Page

In this chapter:

 12.  Uploading and Downloading Data with a Conduit

Now we are going to show you how to move data back and forth from the desktop to the handheld. To do this, we need to discuss quite a few Sync Manager functions. We show you the functionality required in a conduit to support data transfers and some useful additional features as well. After we discuss these topics, we return to our walkthrough of what happens after the HotSync button gets pressed.

Next we discuss portability issues. Knowing that you are breathless with anticipation by this point, we return to the Sales application conduit. We walk through the code that handles uploading and downloading. We show how to upload the sales orders and customers from the handheld, and download the products and customers from the desktop. We also handle deleted records in the customer database.

Conduit Requirements

Top Of Page

At a bare minimum, a conduit that handles data uploading and downloading has to do all of the following:

Where to Store Data

Top Of Page

There is an important demarcation to remember when deciding where to store data on the desktop. Data specific to a particular user should be stored in a private location, whereas data shared among many users should be stored in a group location. For example, in our Sales Application each salesperson has her or his own list of customers but gets the product list from a general location. The first set of data is specific to a particular user; the second type is general. They should be stored in separate locations.

Specific data

Store this data in your conduit folder in the user's folder in the HotSync folder.

General data

Store this data in your application's desktop folder.

Also keep in mind that data doesn't necessary need to be stored locally. While it may be stored on a particular desktop, it is just as likely to be stored on a server or a web site.

Creating, Opening, and Closing Databases

Top Of Page

Database management during synchronization is handled completely by the conduit.

Creating a Database

There is a standard database call used by the Sync Manger to create a database:

 SyncCreateDB(CDbCreateDB& rDbStats)

SyncCreateDB creates a new record or resource database on the handheld and then opens it. You have the same control over database creation from within the conduit that you have on the handheld. The rDbStats parameter is of type CDbCreateDBClass and contains the following important fields:

m_FileHandle

Output field. On a successful return, this contains a handle to the created database with read/write access.

m_Creator

Database creator ID. This should match the creator ID of the application.

m_Flags

The database attributes. Choose one of the following: eRecord for a standard database, eResource for a resource database. Another flag is eBackupDB, which you set for the backup bit.

m_Type

The four-byte database type.

m_CardNo

Memory card where the database is located. Use 0, since no Palm OS device currently has more than one memory card.

m_Name

The database name.

m_Version

The version of the database.

m_dwReserved

Reserved for future use. Must be set to 0.

Opening a Database

The Sync Manager call to open a remote database is:

 SyncOpenDB(char *pname, int nCardNum, Byte& rHandle, Byte openMode)

The values for the four parameters are:

pName

Name of the database.

nCardNum

Memory card where database is located. Use 0, since no Palm OS device currently has more than one memory card.

rHandle

Output parameter. On a successful return, this contains a handle to the open database.

openMode

Use (eDbRead | eDbShowSecret) to read all records, including private ones. Use (eDbRead | eDbWrite | eDbShowSecret) to be able to write and/or delete records.

You need to close any database you open; only one can be open at a time. An error results if you try to open a new database without closing the prior one.

Closing the Database

The Sync Manager call to close a remote database should come as no surprise. It is SyncCloseDB, and it takes only one parameter, the handle you created when you opened or created a database:

 SyncCloseDB(Byte fHandle)

Slightly more sophisticated results can be had from  SyncCloseDBEx. This function also allows you to modify the database's backup and modification date. Both functions close databases that were opened with either SyncCreateDB or SyncOpenDB.

Downloading to the Handheld

Top Of Page

As we discussed earlier, there are several different ways that you might want to move data around during a synchronization. Let's look at what is involved in moving data from the desktop to the handheld. This is commonly done with databases that are exclusively updated on the desktop and are routinely downloaded to handhelds where they aren't modified. Or you may do this in the case that the user chooses "Desktop overwrites handheld" in the HotSync settings dialog (see Figure 11-3 on page 315).

You need to create the database if it doesn't yet exist. You should also delete any existing records before downloading the ones from the desktop. This is necessary because you don't want the old ones; all you want are the newly downloaded ones.

Deleting Existing Records

There are a few different routines to choose from for deleting records:

 SyncDeleteRec

Deletes one specific record

 SyncPurgeAllRecs

Deletes all records

 SyncPurgeAllRecsInCategory

Deletes all records from the specified category

 SyncPurgeDeletedRecs

Deletes all records that have been marked as deleted or archived

In our particular case, SyncPurgeAllRecs is the call we want to use.

Writing Records

Once you have a nice, empty database, you can fill it up with fresh records from the desktop. You do this with the Sync Manager call SyncWriteRec.

 SyncWriteRec (CRawRecordInfo &rInfo)

The parameter rInfo (of class CRawRecordInfo) contains several important fields:

m_FileHandle

Handle to the open database.

m_RecId

Input/output field; the record's unique ID. To add a new record, set this field to 0; on return, the field contains the new record's unique ID. To modify an existing record, set this field to the unique ID of one of the records in the database. An error occurs if this field doesn't match an existing unique record ID. Note that when you add a new record, it's the handheld that assigns the unique record ID.

m_Attribs

The attributes of the record. See "Working with Records" on page 145 for a complete discussion.

m_CatId

The record's category index. Use values from 0 to 14.

m_RecSize

The number of bytes in the record.

m_TotalBytes

The number of bytes of data in the m_pBytes buffer. It should be set to the number of bytes in the record, however, to work around bugs in some versions of the Sync Manager.

NOTE:

Unique record IDs are not perfect. A record maintains its unique ID unless a hard reset happens. Prior to HotSync 3.0, after a hard reset HotSync would generate new unique IDs for the records when it restored the database. Only HotSync 3.0 or later restores the unique record IDs correctly.

On the handheld, when you create a database record you specify the location in the database of that record. When using a conduit, on the other hand, you have no way to specify the record's exact location. Although it could change in the future, SyncWriteRec currently adds new records at the end of the database.

This lack of control over the order of records can be a problem for databases that need to have a specific order. For example, you may have a database sorted by date. The question then becomes, "How can the conduit create the database in sorted order?"

The answer is that unfortunately it can't.

NOTE:

There is a workaround with existing versions of the Sync Manager. If your conduit is writing records to an empty database, it should add them in sorted order. With existing versions of the Sync Manager, the records will then be in the correct order. Be careful, however, as future versions of the Sync Manager may cause the records not to be in sorted order.

NOTE:

In such cases, the sysAppLaunchCmdSyncNotify launch code for Palm OS applications comes to the rescue. After a sync occurs for a database with a specific creator, that database's application is called with the sysAppLaunchCmdSyncNotify launch code. This launch code tells the application that its database has changed, and gives the application a chance to sort it.

Writing the AppInfo Block

You commonly use the AppInfo block of a database to store categories and other information relevant to the database as a whole. The Sync Manager call that you use to write the AppInfo block is:

 SyncWriteDBAppInfoBlock (BYTE fHandle, CDbGenInfo &rInfo)

The parameter rInfo is an object of type CdbGenInfo and contains the following fields:

m_pBytes

A pointer to the data you want copied to the app info block.

m_TotalBytes

The number of bytes of data in the m_pBytes buffer. This should be set to the number of bytes in the record to work around bugs in some versions of the Sync Manager.

m_BytesRead

To work around bugs in some versions of the Sync Manager, set this to m_TotalBytes.

m_dwReserved

Reserved for the future. Set this field to 0.

Uploading to the Desktop

Top Of Page

When you need to send data from the handheld to the desktop you have to read through the records of the remote database and translate them into appropriate structures on the desktop. Here is the process, a step at a time, starting with the choices you have in how you read through the records.

Finding the Number of Records

 SyncGetDBRecordCount finds the number of records in a database:

long SycnGetDBRecordCount(BYTE fHandle, WORD &rNumRecs);

Call it with:

WORD numRecords;
err = SyncGetDBRecordCount(rHandle, numRecords);

Reading Records

You can read records in a remote database using any of the following strategies:

We employ the last strategy for reading the records from our Sales order databases and the first strategy when we fully synchronize our customer list. There are a few points worth mentioning about each strategy.

Iterating through each record stopping only for altered ones

If you want to iterate through the records and stop only on the ones that have been modified, use  SyncReadNextModifiedRec. It retrieves a record from the remote database if the dirty bit in the record has been set.

A variation of this routine is  SyncReadNextModifiedRecInCategory, which also filters based on the record's category. This function takes the category index as an additional parameter.

Looking up exact records via unique record ID

Sometimes you want to read records based on their unique record IDs. In such cases, use  SyncReadRecordByID.

Iterating through the records of a database from beginning to end

Use  SyncReadRecordByIndex to get a record based on the record number. Use this when you want to read through a database from beginning to end. This function takes one parameter, rInfo, which has the record index as one of its fields.

The CRawRecordInfo class

Each of these read routines takes as a parameter an object of the CRawRecordInfo class. The needed fields in the class are:

m_FileHandle

This is a handle to the open database.

m_pBytes

A pointer that you allocate into which the record will be copied.

m_TotalBytes

The size of the m_pBytes pointer. This is the number of bytes that can be copied into m_pBytes without overflowing it.

m_BytesRead

Output field; the number of bytes read. If m_BytesRead is greater than m_TotalBytes, the record is too large. Sync Manager 2.1 or later copies the first m_totalBytes of record data to m_pBytes. Previous versions of the Sync Manager copy nothing.

m_catId

Input field for SyncReadNextRecInCategory and SyncReadNextModifiedRecInCategory. Output field for the other read routines. This contains the category, as a number between 0 and 14.

m_RecIndex

Input field for SyncReadRecordByIndex. Output field for other read routines for Sync Manager 2.1 or later (earlier versions of Sync Manager don't write to this field).

m_Attribs

The attributes of the record.

m_dwReserved

Reserved for the future. Set this field to 0.

NOTE:

Beware of modifying the records in a database while iterating with  SyncReadNextModifiedRec or  SyncReadNextModifiedRecInCatgegory. In pre-2.0 versions of the Palm OS, the iteration routines don't work right. In Palm OS 2.0, a modified record is read again by the iteration routines. In Palm OS 3.0, the modified record isn't reread.

NOTE:

If the record you read is larger than you've allocated space for, the Sync read routines will not return an error. You need to explicitly check for this problem. If, after the read, m_BytesRead is greater than m_TotalBytes, you haven't allocated enough space. For Palm OS 3.0 and earlier, no record can be more than 65,505 bytes.

Reading the AppInfo Block

There are times when you need to read information from the AppInfo block. For example, if the AppInfo block contains category names, you'll need to read it to get them. The Sync Manager call to use is  SyncReadDBAppInfoBlock.

This function takes two parameters, a handle to the open record or database on the handheld, and the object, rInfo, that contains information about the database header.

The parameter rInfo is an object of type CdbGenInfo with the following fields:

m_pBytes

A pointer to memory you've allocated into which you are going to copy the AppInfo block.

m_TotalBytes

The number of bytes allocated for the m_pBytes field.

m_BytesRead

Output field; the number of bytes read. If m_BytesRead is greater than m_TotalBytes, the AppInfo block is too large. Sync Manager 2.1 or later copies the first m_totalBytes of AppInfo block data to m_pBytes. Previous versions of the Sync Manager copy nothing.

m_dwReserved

Reserved for the future. Set this field to 0.

Deleted/Archived Records

For databases that will be two-way synced, the handheld application doesn't completely remove a deleted record; it marks it as deleted, instead. When a sync occurs, those marked records need to be deleted from the desktop database.

There are a couple of ways that you can delete marked records and a few pitfalls to avoid. First, note that you have two different ways in which records might be removed from a database. They can be either completely deleted or just archived. Figure 12-1 shows you the two possible dialog settings that a user can select when given the option to delete a record. Choosing "Save archive copy on PC" means the record is marked as deleted until the next sync, at which point it is saved in an archive file and then deleted from the database. Not choosing "Save archive copy on PC" means the record is marked as deleted until the next sync and then completely deleted from the database. See "Deleting a Record" on page 152 for further details.

-Figure 12- 1. Saving or not saving an archive copy when deleting a record

Archiving records

You should create a separate archive file and append archived records there. This is for situations in which the user doesn't want the records cluttering up the handheld or the normal desktop application, but does want the record available if needed. It's customary to create a separate archive file for each category.

Deleting records

Once any archived records have been archived, and any deleted records have been removed from the corresponding desktop file, those records should be completely deleted from the handheld.  SyncPurgeDeletedRecs is the call you should use:

err = SyncPurgeDeletedRecs(rHandle);

When the HotSync Button Gets Pressed

Top Of Page

We left off in the previous discussion at the point where we are ready to exchange information between the conduit on the desktop and the handheld unit. Let's continue now walking through the chain of events (see Table 12-1).

-Table 12- 1. When the HotSync Button Gets Pressed

Portability Issues

Top Of Page

There are two important portability issues that you need to take into account when moving data back and forth from the handheld to the desktop. They are byte ordering and structure packing.

Byte Ordering

The Palm OS runs on a Motorola platform, which stores bytes differently from Windows running on an Intel platform. This crucial difference can royally mess up data transfers if you are not careful.

On the handheld, the 16-bit number 0x0102 is stored with the high byte, 0x01, first, and the low byte, 0x02, second. In the conduit on Windows, the same number is stored with the low byte, 0x02, first, and the high byte, 0x01, second. As a result, any two-byte values stored in your records or in your AppInfo block must be swapped when transferred between the two systems. (If you fail to swap, a simple request for 3 boxes of toys on the handheld would be processed on the desktop as a request for 768 boxes!) A similar problem occurs with four-byte values; they are also stored in switched forms (see Table 12-2).

-Table 12- 2. Comparison of Byte Orderings for the Four-Byte Value 0x01020304

NOTE:

Strings are not affected by this byte ordering. On both platforms, the string "abc" is stored in the order "a", "b", "c", "\0".

The HotSync Manager provides routines for converting two- and four-byte values from the handheld to host byte ordering:

Word  SyncHHtoHostWord(Word value)
DWord  SyncHHToHostDWord(DWord value)

and for the opposite conversion:

Word SyncHostToHHWord(Word value)
DWord SyncHostToHHDWord(DWord value)

Here are the return values:

Structure Packing

Sometimes the compiler leaves holes in structures between successive fields. This is done in order for fields to begin on specific byte/word/double-word boundaries. As a result, you need to lay out the structures, defining your records and/or AppInfo block in the same way for both the compiler you use for creating your handheld application and the compiler you use to create your conduit.

For Visual C++, we've found that the pack pragma can be used to change the packing rules to match that of CodeWarrior:

#pragma pack(2)

structure declarations for structures that will be read from the handheld

#pragma pack

The Sales Conduit

Top Of Page

We extend the Sales conduit so that our shell from the previous chapter also supports "Desktop overwrites handheld" and "Handheld overwrites desktop." We postponing syncing until Chapter 13, Two-Way Syncing.

For our conduit, we've got to define what it means to do each of these types of overwriting. Here's the logic that we think makes sense for the Sales application:

Desktop overwrites handheld

The products database and the customers database are completely overwritten from the desktop; nothing happens to the orders database.

Handheld overwrites database

The products are ignored (since they can't have changed on the handheld). The customers and orders databases are copied to the desktop. Any archived customers are appended to a separate file; deleted customers are removed from the handheld.

Format Used to Store Data on the Desktop

We store data on the desktop as tab-delimited text files.

The customers will be stored in a file named Customers.txt in the user's directory within the Sales conduit directory. Each line in the file is of the form:

Customer ID<tab>Name<tab>Address<tab>City<tab>Phone

The orders will be stored in a file named Orders.txt in the same directory. Each order is stored as:

ORDER Customer ID
quantity<tab>Product ID
quantity<tab>Product ID
...
qauntity<tab>Product ID

Orders follow one another in the file.

The products are stored in a Products.txt file and start with the categories, followed by the products:

Name of Category 0
Name of Category 1
...
Name of last Category
<empty line>
Product ID<tab>Name<tab>Category Number<tab>Price in dollars and cents
...
Product ID<tab>Name<tab>Category Number<tab>Price in dollars and cents

Modifying OpenConduit

We modify  OpenConduit to handle copying from handheld to desktop (eHHtoPC) and from desktop to handheld (ePCtoHH):

__declspec(dllexport) long OpenConduit(PROGRESSFN progress, 
                                       CSyncProperties &sync)
{
    AFX_MANAGE_STATE(AfxGetStaticModuleState());
    long err = 0;

    if (sync.m_SyncType == eDoNothing) {
        LogAddEntry("Sales - sync configured to Do Nothing", slText, 
            false);
            return 0;
    }

    CONDHANDLE myConduitHandle;
    if ((err = SyncRegisterConduit(myConduitHandle)) != 0) return err;

    LogAddEntry("", slSyncStarted, false);

    if (sync.m_SyncType == eHHtoPC) {
        if ((err = CopyOrdersFromHH(sync)) != 0)
            goto exit;
        if ((err = CopyCustomersFromHH(sync)) != 0)
            goto exit;
    } else if (sync.m_SyncType == ePCtoHH) {
        if ((err = CopyProductsAndCategoriesToHH(sync)) != 0)
            goto exit;
        if ((err = CopyCustomersToHH(sync)) != 0)
            goto exit;
    } else if (sync.m_SyncType == eFast || sync.m_SyncType == eSlow) {
    }

exit:
    LogAddEntry(kConduitName, err ? slSyncAborted : slSyncFinished, 
        false);
    SyncUnRegisterConduit(myConduitHandle);

    return err;
}

General Code

We have some other code to add, as well. We need to define our databases, create a global variable, and add some data structures.

Database defines

We've got defines for the databases (these can be copied directly from the code for the handheld application):

#define salesCreator    'Sles'
#define salesVersion    0
#define customerDBType  'cust'
#define customerDBName  "Customers_Sles"
#define orderDBType     'Ordr'
#define orderDBName     "Orders_Sles"
#define productDBType   'Prod'
#define productDBName   "Products_Sles"

Globals

We read and write records using a global buffer. We size it to be bigger than any possible record (at least on Palm OS 3.0 or earlier):

#define kMaxRecordSize  66000
char    gBigBuffer[kMaxRecordSize];

Data structures

We have structures that need to correspond exactly to structures on the handheld (thus, we use the pack pragma). We then use these structures to read and write data on the handheld:

// on the Palm handheld, the items array in PackedOrder starts at offset 6 
// Natural alignment on Windows would start it at offset 8
#pragma pack(2)

struct Item {
    unsigned long   productID;
    unsigned long   quantity;
};

struct PackedOrder {
    long            customerID;
    unsigned short  numItems;
    Item            items[1];
};

struct PackedCustomer{
    long customerID;
    char name[1];
};

struct PackedProduct {
    unsigned long   productID;
    unsigned long   price;  // in cents
    char    name[1];
};

#define kCategoryNameLength 15
typedef char    CategoryName[kCategoryNameLength + 1];

struct PackedCategories {
    unsigned short  numCategories;
    CategoryName    names[1];
};

#pragma pack()

Next, we've got some structures that we use to store data in memory in the conduit. Since we're using C++, we have constructors and destructors to make our lives easier:

struct Customer {
    Customer() { name = address = city = phone = 0;}
    ~Customer() {delete [] name; delete [] address; delete [] city; 
        delete [] phone; };
    long customerID;
    char *name;
    char *address;
    char *city;
    char *phone;
};

struct Categories {
    Categories(int num) { numCategories = num; 
        names = new CategoryName[num];}
    ~Categories() {delete [] names;};
    unsigned short numCategories;
    CategoryName *names;
};

struct Order {
    Order(unsigned short num) { numItems = num; 
       items = new Item[numItems];};
    ~Order() { delete [] items;};
    long            customerID;
    unsigned short  numItems;
    Item            *items;
};

struct Product {
    Product() {name = 0;};
    ~Product() {delete [] name;};

    unsigned long   productID;
    unsigned long   price;  // in cents
    unsigned char   category:4;
    char    *name;
};

Downloading to the Handheld

To download data to the handheld, we have to take care of a number of things. First, we need to copy the customers to the handheld. If the database doesn't exist, we need to create it. Once the database is open, we need to read through the records. When we finish with customers, we need to do the same things for products.

Downloading customers

We've got to copy the customers to the handheld. We do this in  CopyCustomersToHH:

int CopyCustomersToHH(CSyncProperties &sync)
{
    FILE *fp = NULL;
    BYTE rHandle;
    int err;
    bool dbOpen = false;

    if ((err = SyncOpenDB(customerDBName, 0, rHandle, eDbWrite | eDbRead
      | eDbShowSecret)) != 0) {
        LogAddEntry("SyncOpenDB failed", slWarning, false);
        if (err == SYNCERR_FILE_NOT_FOUND)
        {
            CDbCreateDB dbInfo;
            memset(&dbInfo, 0, sizeof(dbInfo));
            dbInfo.m_Creator  = salesCreator; 
            dbInfo.m_Flags    = eRecord; 
            dbInfo.m_CardNo   = 0; 
            dbInfo.m_Type     = customerDBType; 
            strcpy(dbInfo.m_Name, customerDBName);

            if ((err = SyncCreateDB(dbInfo)) != 0)
            {
                LogAddEntry("SyncCreateDB failed", slWarning, false);
                goto exit;
            }
            rHandle = dbInfo.m_FileHandle; 
        } else
            goto exit;
    }
    dbOpen = true;

    char    buffer[BIG_PATH *2];
    strcpy(buffer, sync.m_PathName);
    strcat(buffer, "Customers.txt");

    if ((fp = fopen(buffer, "r")) == NULL) {
        err = 1;
        LogAddFormattedEntry(slWarning, false, "fopen(%s) failed",
            buffer);
        goto exit;
    }

    if ((err = SyncPurgeAllRecs(rHandle)) != 0) {
        LogAddEntry("SyncPurgeAllRecs failed", slWarning, false);
        goto exit;
    }

    Customer *c;
    while (c = ReadCustomer(fp)) {
        CRawRecordInfo recordInfo;
        recordInfo.m_FileHandle = rHandle;
        recordInfo.m_RecId = 0;
        recordInfo.m_pBytes = (unsigned char *) gBigBuffer;
        recordInfo.m_Attribs = 0;
        recordInfo.m_CatId = 0;
        recordInfo.m_RecSize = CustomerToRawRecord(gBigBuffer,
            sizeof(gBigBuffer), c);
        recordInfo.m_dwReserved = 0;

        if ((err = SyncWriteRec(recordInfo)) !=0) {
            delete c;
            LogAddEntry("SyncWriteRec failed", slWarning, false);
            goto exit;
        }
    
        delete c;
    }

exit:
    if (fp)
        fclose(fp);
    if (dbOpen)
        if ((err = SyncCloseDB(rHandle)) != 0)
            LogAddEntry("SyncDBClose failed", slWarning, false);
    return err;
}

We try to open the customers database on the handheld. If it doesn't exist, we create it. Next, we open Customers.txt, the file with the customers. We delete all the existing records from the customers database on the handheld and then start reading each customer (using ReadCustomer) and writing the customer to the database with SyncWriteRec.

NOTE:

We added a couple of log entries in this code, as well. These were not intended for users, but to help in our debugging. We get notified via the log if the code failed to properly open Customers.txt or if we failed to delete all the existing records.

 ReadCustomer reads a customer from a text file, returning 0 if there are no more customers:

Customer *ReadCustomer(FILE *fp)
{
    const char *separator = "\t";
    if (fgets(gBigBuffer, sizeof(gBigBuffer), fp) == NULL)
        return 0;
    char *customerID = strtok(gBigBuffer, separator);
    char *name = strtok(NULL, separator);
    char *address = strtok(NULL, separator);
    char *city = strtok(NULL, separator);
    char *phone = strtok(NULL, separator);

    if (!address)
        address = "";
    if (!city)
        city = "";
    if (!phone)
        phone = "";
    if (customerID && name) {
        Customer *c = new Customer;
        c->customerID = atol(customerID);
        c->name = new char[strlen(name) + 1];
        strcpy(c->name, name);
        c->address = new char[strlen(address) + 1];
        strcpy(c->address, address);
        c->city = new char[strlen(city) + 1];
        strcpy(c->city, city);
        c->phone = new char[strlen(phone) + 1];
        strcpy(c->phone, phone);
        return c;
    } else
        return 0;
}

 CustomerToRawRecord writes a customer to the passed-in buffer in the format the handheld expects. It returns the number of bytes it has written. Note that it must swap the four-byte customerID to match the byte ordering on the handheld:

int CustomerToRawRecord(void *buf, int bufLength, Customer *c)
{
    PackedCustomer *cp = (PackedCustomer *) buf;
    cp->customerID = SyncHostToHHDWord(c->customerID);
    char *s = cp->name;
    strcpy(s, c->name);
    s += strlen(s) + 1;
    strcpy(s, c->address);
    s += strlen(s) + 1;
    strcpy(s, c->city);
    s += strlen(s) + 1;
    strcpy(s, c->phone);
    s += strlen(s) + 1;
    return s - (char *) buf;
}

Downloading products

The  CopyProductsAndCategoriesToHH function updates the products database on the handheld from the Products.txt file on the PC:

int CopyProductsAndCategoriesToHH(CSyncProperties &sync)
{
    FILE *fp = NULL;
    BYTE rHandle;
    int err;
    bool dbOpen = false;

    char    buffer[BIG_PATH *2];
    strcpy(buffer, sync.m_PathName);
    strcat(buffer, "Products.txt");

    if ((fp = fopen(buffer, "r")) == NULL) {
        err = 1;
        LogAddFormattedEntry(slWarning, false, "fopen(%s) failed",
            buffer);
        goto exit;
    }

    if ((err = SyncOpenDB(productDBName, 0, rHandle, 
        eDbWrite | eDbRead | eDbShowSecret)) != 0) {
        if (err == SYNCERR_FILE_NOT_FOUND)
        {
            CDbCreateDB dbInfo;
            memset(&dbInfo, 0, sizeof(dbInfo));
            dbInfo.m_Creator  = salesCreator; 
            dbInfo.m_Flags    = eRecord; 
            dbInfo.m_CardNo   = 0; 
            dbInfo.m_Type     = productDBType; 
            strcpy(dbInfo.m_Name, productDBName);

            if ((err = SyncCreateDB(dbInfo)) != 0)
            {
                LogAddEntry("SyncCreateDB failed", slWarning, false);
                goto exit;
            }
            rHandle = dbInfo.m_FileHandle; 
        } else
            goto exit;
    }
    dbOpen = true;

    if ((err = SyncPurgeAllRecs(rHandle)) != 0) {
        LogAddEntry("SyncPurgeAllRecs failed", slWarning, false);
        goto exit;
    }

    Categories *c;
    if (c = ReadCategories(fp)) {
        CDbGenInfo  rInfo;

        rInfo.m_pBytes = (unsigned char *) gBigBuffer;
        rInfo.m_TotalBytes = CategoriesToRawRecord(gBigBuffer,
            sizeof(gBigBuffer), c);
        rInfo.m_BytesRead = rInfo.m_TotalBytes; // Because older versions
                  // of the sync manager looked in the wrong field for
                  // the total size, the documented API of
                  // SyncWriteDBAppInfoBLock is that both m_TotalBytes
                  // and m_BytesRead should be filled in with the total
        rInfo.m_dwReserved = 0;
        if ((err = SyncWriteDBAppInfoBlock(rHandle, rInfo)) !=0) {
            delete c;
            LogAddEntry("SyncWriteDBAppInfoBlock failed", slWarning,
              false);
            goto exit;
        }
        delete c;
    }

    Product *p;
    while (p = ReadProduct(fp)) {
        CRawRecordInfo recordInfo;
        recordInfo.m_FileHandle = rHandle;
        recordInfo.m_RecId = 0;
        recordInfo.m_pBytes = (unsigned char *) gBigBuffer;
        recordInfo.m_Attribs = 0;
        recordInfo.m_CatId = p->category;
        recordInfo.m_RecSize = ProductToRawRecord(gBigBuffer,
            sizeof(gBigBuffer), p);
        recordInfo.m_dwReserved = 0;

        if ((err = SyncWriteRec(recordInfo)) !=0) {
            delete p;
            LogAddEntry("SyncWriteRec failed", slWarning, false);
            goto exit;
        }
        delete p;
    }

exit:
    if (fp)
        fclose(fp);

    if (dbOpen)
        if ((err = SyncCloseDB(rHandle)) != 0)
            LogAddEntry("SyncDBClose failed", slWarning, false);
    return err;
}

This routine has almost exactly the same structure as CopyCustomersToHH. The categories are written to the AppInfo block using SyncWriteDBAppInfoBlock instead. It uses  ReadCategories to read the categories from the Products.txt file. The function continues reading categories, one per line, until it reaches an empty line:

#define kMaxCategories  15
Categories *ReadCategories(FILE *fp)
{
    const char *separator = "\n";
    int numCategories = 0;
    Categories *c = new Categories(kMaxCategories);
    for (int i = 0; i < kMaxCategories ; i++) {
        if (fgets(gBigBuffer, sizeof(gBigBuffer), fp) == NULL)
            break;
        // strip newline
        if (gBigBuffer[strlen(gBigBuffer) - 1] == '\n')
            gBigBuffer[strlen(gBigBuffer) - 1] = '\0';
        if (gBigBuffer[0] == '\0')
            break;
        // copy it
        strncpy(c->names[i], gBigBuffer, kCategoryNameLength);
        c->names[i][kCategoryNameLength] = '\0';
    }

    c->numCategories = i;
    return c;
}

 ReadProduct reads the products that follow in the file:

Product *ReadProduct(FILE *fp)
{
    const char *separator = "\t";
    if (fgets(gBigBuffer, sizeof(gBigBuffer), fp) == NULL)
        return 0;
    
    char *productID = strtok(gBigBuffer, separator);
    char *name = strtok(NULL, separator);
    char *categoryNumber = strtok(NULL, separator);
    char *price = strtok(NULL, separator);

    if (productID && name && categoryNumber) {
        Product *p = new Product;
        p->productID = atol(productID);
        p->name = new char[strlen(name) + 1];
        strcpy(p->name, name);
        p->category = (unsigned char) atoi(categoryNumber);
        p->price = (long) (atof(price) * 100);  // convert to cents
        return p;
    } else
        return 0;
}

 CategoriesToRawRecord writes the categories in the format expected by the handheld (therefore, the numCategories two-byte field must be swapped):

int CategoriesToRawRecord(void *buf, int bufLength, Categories *c)
{
    PackedCategories *pc = (PackedCategories *) buf;
    pc->numCategories = SyncHostToHHWord(c->numCategories);
    char *s = (char *) pc->names;
    for (int i = 0; i < c->numCategories; i++) {
        memcpy(s, c->names[i], sizeof(CategoryName));
        s += sizeof(CategoryName);
    }
    return s - (char *) buf;
}

 ProductToRawRecord is similar, but must swap both the productID and the price:

int ProductToRawRecord(void *buf, int bufLength, Product *p)
{
    PackedProduct *pp = (PackedProduct *) buf;
    pp->productID = SyncHostToHHDWord(p->productID);
    pp->price = SyncHostToHHDWord(p->price);
    strcpy(pp->name, p->name);
    return offsetof(PackedProduct, name) + strlen(pp->name) + 1;
}

That completes the conduit code for downloading. Remember, however, that the order in which SyncWriteRec adds new records to the database isn't defined. As a result, the handheld must re-sort the databases (to be sorted by ID). Here's the code in our PilotMain handheld function that does this:

} else if (cmd == sysAppLaunchCmdSyncNotify) {
    DmOpenRef   db;
    
    // code for beaming removed
        
// After a sync, we aren't guaranteed the order of any changed databases.
// We'll just resort the products and customer which could have changed.
// we're going to do an insertion sort because the databases
// should be almost completel sorted (and an insertion sort is
// quicker on an almost-sorted database than a quicksort).
// Since the current implementation of the Sync Manager creates new
// records at the end of the database, our database are probably sorted.
    db= DmOpenDatabaseByTypeCreator(customerDBType, salesCreator, 
        dmModeReadWrite);
    if (db) {
        DmInsertionSort(db, (DmComparF *) CompareIDFunc, 0);
        DmCloseDatabase(db);
    } else 
        error = DmGetLastErr();
    db= DmOpenDatabaseByTypeCreator(productDBType, salesCreator,    
        dmModeReadWrite);
    if (db) {
        DmInsertionSort(db, (DmComparF *) CompareIDFunc, 0);
        DmCloseDatabase(db);
    } else 
        error = DmGetLastErr();
}

Uploading to the Desktop

We need to handle the same sorts of things when we are uploading instead of downloading data. First, we copy orders from the handheld to the desktop by opening the database, reading the records, doing the proper conversion, and sending them along their merry way to the desktop. Then we do the same for customers.

Uploading orders

We've got to copy the orders from the handheld to the desktop:

int  CopyOrdersFromHH(CSyncProperties &sync)
{
    FILE *fp = NULL;
    BYTE rHandle;
    int err;
    bool dbOpen = false;
    int i;

    if ((err = SyncOpenDB(orderDBName, 0, rHandle, 
        eDbRead | eDbShowSecret )) != 0) {
        LogAddEntry("SyncOpenDB failed", slWarning, false);
        goto exit;
    }
    dbOpen = true;

    char    buffer[BIG_PATH *2];
    strcpy(buffer, sync.m_PathName);
    strcat(buffer, "Orders.txt");

    if ((fp = fopen(buffer, "w")) == NULL) {
        LogAddFormattedEntry(slWarning, false, "fopen(%s) failed",
            buffer);
        goto exit;
    }

    WORD recordCount;
    if ((err = SyncGetDBRecordCount(rHandle, recordCount)) !=0) {
        LogAddEntry("SyncGetDBRecordCount failed", slWarning, false);
        goto exit;
    }

    CRawRecordInfo recordInfo;
    recordInfo.m_FileHandle = rHandle;

    for (i = 0; i < recordCount; i++) {
        recordInfo.m_RecIndex = i;
        recordInfo.m_TotalBytes = (unsigned short) sizeof(gBigBuffer);
        recordInfo.m_pBytes = (unsigned char *) gBigBuffer;
        recordInfo.m_dwReserved = 0;
        
        if ((err = SyncReadRecordByIndex(recordInfo)) !=0) {
            LogAddEntry("SyncReadRecordByIndex failed", slWarning, false);
            goto exit;
        }   
    
        Order *o = RawRecordToOrder(recordInfo.m_pBytes);
        if ((err = WriteOrderToFile(fp, o)) != 0) {
            LogAddEntry("WriteOrderToFile failed", slWarning, false);
            delete o;
            goto exit;
        }
        delete o;
    }
exit:
    if (fp)
        fclose(fp);

    if (dbOpen)
        if ((err = SyncCloseDB(rHandle)) != 0)
            LogAddEntry("SyncDBClose failed", slWarning, false);
    return err;
}

The code opens the orders database (read-only, since it won't change the database). Then it creates the Orders.txt file. It finds the number of records in the database with SyncGetDBRecordCount. Then it reads record by record using SyncReadRecordByIndex. RawRecordToOrder reads the raw record and converts it to an in-memory record. Finally, the order is written to the file with WriteOrderToFile.

Here's the code that converts a record to an order (again, byte-swapping is necessary):

Order * RawRecordToOrder(void *p)
{
    PackedOrder *po = (PackedOrder *) p;
    unsigned short numItems = SyncHHToHostWord(po->numItems);
    Order *o = new Order(numItems);
    o->customerID = SyncHHToHostDWord(po->customerID);
    for (int i = 0; i < o->numItems; i++) {
        o->items[i].productID = SyncHHToHostDWord(po->items[i].productID);
        o->items[i].quantity = SyncHHToHostDWord(po->items[i].quantity);
    }
    return o;
}

Last, here's the code that writes the order to the file:

int WriteOrderToFile(FILE *fp, const Order *o)
{
    int result;

    if ((result = fprintf(fp, "ORDER %ld\n", o->customerID)) < 0)
        return result;
    for (int i = 0; i < o->numItems; i++) {
        if ((result = fprintf(fp, "%ld %ld\n", o->items[i].quantity, 
            o->items[i].productID)) < 0)
            return result;
    }
    return 0;
}

Uploading customers

Here's the routine that uploads the customers database:

int  CopyCustomersFromHH(CSyncProperties &sync)
{
    FILE *fp = NULL;
    FILE *archivefp = NULL;
    BYTE rHandle;
    int err;
    bool dbOpen = false;
    int i;

    if ((err = SyncOpenDB(customerDBName, 0, rHandle, 
        eDbWrite | eDbRead | eDbShowSecret)) != 0) {
        LogAddEntry("SyncOpenDB failed", slWarning, false);
        goto exit;
    }
    dbOpen = true;

    char    buffer[BIG_PATH *2];
    strcpy(buffer, sync.m_PathName);
    strcat(buffer, "Customers.txt");

    if ((fp = fopen(buffer, "w")) == NULL) {
        LogAddFormattedEntry(slWarning, false, "fopen(%s) failed",
            buffer);
        goto exit;
    }

    strcpy(buffer, sync.m_PathName);
    strcat(buffer, "CustomersArchive.txt");

    if ((archivefp = fopen(buffer, "a")) == NULL) {
        LogAddFormattedEntry(slWarning, false, "fopen(%s) failed",
            buffer);
        goto exit;
    }

    WORD recordCount;
    if ((err = SyncGetDBRecordCount(rHandle, recordCount)) !=0) {
        LogAddEntry("SyncGetDBRecordCount failed", slWarning, false);
        goto exit;
    }

    CRawRecordInfo recordInfo;
    recordInfo.m_FileHandle = rHandle;

    for (i = 0; i < recordCount; i++) {
        recordInfo.m_RecIndex = i;
        recordInfo.m_TotalBytes = (unsigned short) sizeof(gBigBuffer);
        recordInfo.m_pBytes = (unsigned char *) gBigBuffer;
        recordInfo.m_dwReserved = 0;
        
        if ((err = SyncReadRecordByIndex(recordInfo)) !=0) {
            LogAddEntry("SyncReadRecordByIndex failed", slWarning, false);
            goto exit;
        }   

        FILE *fileToWriteTo;
        if (recordInfo.m_Attribs & eRecAttrArchived)
            fileToWriteTo = archivefp;
        else if (recordInfo.m_Attribs & eRecAttrDeleted)
            continue;   // skip deleted records
        else
            fileToWriteTo = fp;
    
        Customer *c = RawRecordToCustomer(recordInfo.m_pBytes);
        if ((err = WriteCustomerToFile(fileToWriteTo, c)) != 0) {
            delete c;
            LogAddEntry("WriteCustomerToFile failed", slWarning, false);
            goto exit;
        }
        delete c;
    }

    if ((err = SyncPurgeDeletedRecs(rHandle)) != 0)
        LogAddEntry("SyncPurgeDeletedRecs failed", slWarning, false);

exit:
    if (fp)
        fclose(fp);
    
    if (archivefp)
        fclose(archivefp);

    if (dbOpen)
        if ((err = SyncCloseDB(rHandle)) != 0)
            LogAddEntry("SyncDBClose failed", slWarning, false);
    return err;
}

Uploading customers is slightly more complicated than uploading orders, because the handheld supports deleting and archiving customers (see "Editing Customers" on page 168).

After reading each record with SyncReadRecordByIndex, we examine the record attributes (m_Attribs). If the archive bit is set, we write the record to a different file (appending to CustomersArchive.txt). If the delete bit is set, we skip this record.

Once we're done iterating through the records, we remove the deleted and archived records from the handheld (using SyncPurgeDeletedRecs). In order to change the database in this way, we had to open the database with write permission (eDbWrite).

With this code in place, we have a conduit that can upload and download data as needed. Now we are ready to tackle full two-way data syncing.


Palm Programming: The Developer's Guide
Copyright © 1999, O'Rielly and Associates, Inc.
Published on the web by permission of O'Rielly and Associates, Inc. Contents modified for web display.

Previous PageTop Of PageTable Of ContentsIndexNext Page