In this chapter:
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 |
At a bare minimum, a conduit that handles data uploading and downloading has to do all of the following:
Where to Store Data |
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 |
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 |
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:
Deletes one specific record
Deletes all records
Deletes all records from the specified category
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. |
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 |
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.
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 |
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
Action (by the User or by the System) |
What Is Happening Programmatically |
The HotSync Manager gets the conduit name so that it can display information in the status dialog. |
GetConduitName is called and returns. |
The HotSync Manager prepares to sync by passing the synchronization off to the conduit. |
OpenConduit gets called and the conduit's DLL gets loaded into memory. It is told whether to do a fast sync, a slow sync, a copy from handheld to desktop, a copy from desktop to handheld, or to do nothing. When OpenConduit returns, it will have completed the task. |
The conduit registers with the HotSync Manager. |
SyncRegisterConduit returns a handle. |
The conduit notifies the log that syncing is about to start. |
Conduit calls LogAddEntry("", slSyncStarted, false). |
The conduit opens the remote order database on the handheld. |
Conduit calls SyncOpenDB, which returns a handle to the remote order database. |
The user sees that the Sales orders are being synced. |
All the data is written from the handheld to the desktop. |
The conduit closes the remote database. |
Conduit calls SyncCloseDB to close the Sales order database. |
The user sees that the Sales application product list is being synced. |
Conduit calls SyncOpenDB, which returns a handle to the product database. |
The conduit closes the remote database. |
Conduit calls SyncCloseDB, which destroys the handle opened earlier for that database. |
The user sees that the Customer List is being synced. |
|
Close up the conduit after syncing is finished. |
The application calls SyncUnRegisterConduit to dispose of the handle that was set in SyncRegisterConduit. |
The HotSync Manager backs up other stuff. |
The Backup conduit gets called. |
Portability Issues |
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
Palm Handheld Byte Order |
Wintel Byte Order |
0x01 |
0x04 |
0x02 |
0x03 |
0x03 |
0x02 |
0x04 |
0x01 |
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 |
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.