Synchronization Service Providers

When a Pocket PC device establishes a connection with ActiveSync, the desktop will use Synchronization Service Providers (SSPs) to handle the replication and synchronization of data between the device and the desktop. ActiveSync hosts the Service Manager, which handles the basic overhead of establishing connections, mapping data, handling conflicts, and transferring objects to the device. In addition, the Service Manager also controls any SSPs that are installed and active for the current device partnership whenever it needs to work with specific application data (see Figure 9.6).

Figure 9.6. ActiveSync and Its Synchronization Service Providers

graphics/09fig06.gif

An individual SSP will handle the details of synchronizing data for a particular application, and is comprised of two COM objects: one that is installed and registered on the desktop, and another for the device. Each provider will be "plugged" into ActiveSync so it can receive instructions on the data that it is working with, and will be able to send information back to ActiveSync regarding status, synchronization settings, or any other user interface updates.

The process for synchronizing information on the device to the desktop is relatively straightforward when a partnership is established, the Service Manager on the desktop communicates with the Service Manager on the device, each initializing its own respective SSPs for the application data that is being synchronized. Each SSP will then compare individual objects of data between the desktop and the device. Once ActiveSync has been notified that a comparison is complete, it updates both the desktop and the device with the most recent data.

When designing a synchronization provider for your application, it is important to spend some time thinking about how you want to handle your data so that ActiveSync can compare it as efficiently as possible. It is recommended that providers be able to divide data into the following components:

  • An individual "blob" of data, such as a contact, should be considered an object.

  • A grouping of similar objects requires a named object type, such as contacts.

  • Each object must have a unique object identifier. An object identifier is a 32-bit value that cannot be changed or used by another object, and must represent a sort order so that you can tell which objects come first.

  • A group of objects require a data store, which is used to hold the objects, and can be a file, database, or some other custom file format. The "contacts database" is an example of a synchronization store for contact objects.

  • Each service provider must provide its own method for comparing objects.

The interfaces that you will use to create both the desktop and device service provider modules are defined in the cesync.h header file.

Writing a Desktop Synchronization Provider Module

Most of the work that goes into building a synchronization provider is done on the desktop side of the connection. Three COM interfaces will interest you when developing a desktop synchronization provider:

  • The IReplNotify interface is implemented by the ActiveSync Manager. The interface provides a synchronization object with the ability to send notification messages to the main ActiveSync window.

  • The IReplStore interface must be implemented by the synchronization provider. It is used by the ActiveSync Manager when enumerating, comparing, and resolving conflicts with objects.

  • The IReplObjHandler interface also must be implemented by the synchronization provider. It provides the methods that are needed to exchange object data between the desktop and the device, as well as delete objects.

Before examining each of the interfaces that you will be working with, let's take a quick look at how a new desktop module needs to be configured in the registry.

NOTE:

Throughout the rest of this section, which covers Service Providers, I will be referencing parts of an example that synchronizes all of the files in a particular directory on the desktop with the device.


Registering a Desktop Provider

In order for ActiveSync to be able to use your synchronization provider, you need to ensure it is registered properly in the desktop system registry.

The first thing you need to set up is the standard COM settings for an InProc COM server module. This means adding an entry to the HKEY_CLASSES_ROOT\CLSID\{New Class ID} key, which contains the values described in Table 9.17.

Table 9.17. Class ID Registry Settings for a Desktop Provider COM Object

Key\Value Name

Description

Default

Description of the desktop provider

InProcServer32\Default

Full path to the desktop module COM object that implements the IReplStore interface

ProgID\Default

Name of the program identifier {ProgID} for the desktop provider module

In addition, COM needs to have the HKEY_CLASSES_ROOT\{ProgID} key, which contains the values described in Table 9.18.

Table 9.18. Program ID Registry Settings for a Desktop Provider COM Object

Key\Value Name

Description

Default

Description of the desktop provider

DisplayName

Name of the service provider to be displayed in ActiveSync

Version

DWORD value indicating the version of the provider

CLSID\Default

The {New Class ID} for the service provider

ActiveSync must have the provider itself registered in order for it to be used in synchronization operations. This can be done by placing a new entry in the following location:

 HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows CE Services\    Services\Synchronization\All\{ProgID} 

This key will require the entries described in Table 9.19.

Table 9.19. Registry Settings Describing a Desktop Synchronization Provider

Key\Value Name

Description

Default

Description of the service provider.

{New Object Type}\Default

Description of the object type.

{New Object Type}\DefaultIcon

Filename or index of the icon for this object type.

{New Object Type}\DisplayName

Name of the object type to be displayed in ActiveSync.

{New Object Type}\Plural Name

Name of two or more objects of this type to be displayed in ActiveSync.

{New Object Type}\Store

The {ProgID} of the COM object that implements the IReplStore and IReplObjHandler interfaces.

{New Object Type}\Disabled

If set to 1, the object type is automatically disabled when ActiveSync creates a new partnership. If set to 0 or removed, ActiveSync will mark it as enabled.

To understand all of the settings that are required for a file filter, let's take a look at an export of the keys that are used for Pocket Inbox:

 // // Registry entries for the Inproc COM Server // [HKEY_CLASSES_ROOT\MS.WinCE.OutLook\CLSID] @="{A585E741-1D36-11d0-8B9B-00A0C90D064A}" [HKEY_CLASSES_ROOT\CLSID\{A585E741-1D36-11d0-8B9B-   00A0C90D064A}] @="Microsoft OutLook Store" [HKEY_CLASSES_ROOT\CLSID\    {A585E741-1D36-11d0-8B9B-00A0C90D064A}\InprocServer32] @="C:\\Program Files\\Microsoft ActiveSync\\outstore.dll" [HKEY_CLASSES_ROOT\CLSID\   {A585E741-1D36-11d0-8B9B-00A0C90D064A}\ProgID] @="MS.WinCE.OutLook" [HKEY_CLASSES_ROOT\MS.WinCE.OutLook] @="Microsoft OutLook Store" "Display Name"="Microsoft Outlook" "Version"=dword:00030000 [HKEY_CLASSES_ROOT\MS.WinCE.OutLook\CLSID] @="{A585E741-1D36-11d0-8B9B-00A0C90D064A}" // // Registry entries for the service provider // [HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows CE   Services\Services\Synchronization\All\MS.WinCE.Outlook] @="Microsoft Windows CE Outlook Synchronization" // // Registry entries for the particular object type in the // service provider // [HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows CE   Services\Services\Synchronization\All\MS.WinCE.Outlook\Inbox] @="Windows Messaging Client" "Store"="MS.WinCE.Outlook" "Display Name"="Inbox" "Plural Name"="E-mail messages" "Disabled"=dword:00000001 "DefaultIcon"="C:\\Program Files\\Microsoft ActiveSync\\outstore.dll,-154" "EmailAttach"=dword:ffffffff "EmailLines"=dword:00000064 "EmailOpt"=dword:00000132 
The IReplStore Interface

The main interface that you need to implement on the desktop side of a service provider is encapsulated by the IReplStore interface. IReplStore provides the methods that enable ActiveSync to examine application data that you are synchronizing. This includes functions that enumerate objects in the data store, check for changes in object data, copy data between objects, and even handle the user interface for setting up synchronization options. Everything that ActiveSync needs in order to get information about your data from your application is handled here.

The IReplStore interface is composed of the methods described in Table 9.20.

Table 9.20. IReplStore Methods

Method

Description

ActivateDialog()

Pops up a provider-specific dialog box

BytesToObject()

Converts an array of bytes to a folder handle or object

CompareItem()

Compares two synchronization objects

CompareStoreIDs()

Compares two store identifiers

CopyObject()

Copies a synchronization object to another synchronization object.

FindFirstItem()

Returns the handle to the first object found in a synchronization folder

FindItemClose()

Closes the handle to the Find operation started with IReplStore::FindFirstItem()

FindNextItem()

Returns the handle to the next object found in a synchronization folder

FreeObject()

Frees a specified synchronization object handle

GetConflictInfo()

Gets details about two objects that are in conflict

GetFolderInfo()

Gets a handle to a folder containing specified object types

GetObjTypeUIData()

Gets user interface data (such as an object icon) that will be used by the ActiveSync Manager

GetStoreInfo()

Gets information about an object data store

Initialize()

Initializes the service provider

IsFolderChanged()

Checks whether any object has changed in the specified folder

IsItemChanged()

Checks whether an object has changed

IsItemReplicated()

Checks whether an object should be replicated

IsValidObject()

Checks whether an object is valid

ObjectToBytes()

Coverts the handle of a folder or object to an array of bytes

RemoveDuplicates()

Finds and removes duplicate objects from the store

ReportStatus()

Used by ActiveSync to get status information about the synchronization process

UpdateItem()

Updates information about a particular synchronization object

Almost every aspect of working with your data objects is handled by the IReplStore interface. Let's take a more detailed look at how ActiveSync uses the methods that you have implemented when synchronizing information between the desktop and the device.

How a Desktop Provider Is Initialized

When a connection occurs between a device and the desktop, ActiveSync must initialize the Service Providers that are enabled for the particular device. Every SSP will be called in the same manner from ActiveSync.

A service provider is initialized by receiving a call into the IReplStore::Initalize() function:

 HRESULT IReplStore::Initialize(IReplNotify *pNotify,   UINT uFlags); 

The first parameter will point to the IReplNotify interface that is implemented by the ActiveSync Manager. As you will see later in this chapter, the IReplNotify interface can be used to send a status message back to the ActiveSync window. This is followed by uFlags, which can be set to ISF_SELECTED_DEVICE if the store has already been already initialized, or ISF_REMOTE_CONNECTED if the store is used via a remote connection. A few words of caution: If a device is connected remotely, you should take special care to not use any blocking API functions, display any type of user interface, or automatically take any default actions, because a user might not be able to respond right away.

The IReplStore::Initialize() function can be used to perform any general initialization routines for your service provider, as shown in the following example:

 STDMETHODIMP CMyReplStore::Initialize(IReplNotify *pNotify,   UINT uFlags) {    // Initialize the service provider. The way our store    // works is as follows:    // A single folder, m_hFolder, is used to store the OPML    // files that we are syncronizing. Each file is an HREPLITEM.    if(!pNotify)       return E_FAIL;    // Store the pointer to ActiveSync    m_pNotify = pNotify;    m_fInitialized = TRUE;    return NOERROR; } 

Once the service provider has been initialized, ActiveSync will perform a check to determine whether any of the data on the desktop store has changed since the last synchronization. The first step to this process is for the Service Manager to call into the IReplStore::GetStoreInfo() function to get the store identifier and then pass it to IReplStore::CompareStoreIDs() to see if it has changed since the last synchronization. By using a store identifier, ActiveSync can tell if a partnership is using the same data store as before, or if a new mapping needs to be established.

The IReplStore::GetStoreInfo() function has the following prototype:

 HRESULT IReplStore::GetStoreInfo(PSTOREINFO pStoreInfo); 

The only parameter that is passed to you is a pointer to a STOREINFO structure, which you need to fill in with information about the data store for the provider. The structure is defined as follows:

 typedef struct tagStoreInfo {    UINT cbStruct;    UINT uFlags;    TCHAR szProgId[256];    TCHAR szStoreDesc[200];    UINT uTimerRes;    UINT cbMaxStoreId;    UINT cbStoreId;    LPBYTE lpbStoreId;  } STOREINFO, *PSTOREINFO; 

The first field, cbStruct, specifies the size of the STOREINFO structure that is being passed in. Next, uFlags can be set to the following options:

  • SCF_SINGLE_THREAD, if the provider does not support multithreaded access to the data in the store. If this is set, the ActiveSync Manager will serialize calls to the service provider.

  • SCF_SIMULATE_RTS, if the provider supports real-time changes to the store data

The szProgId field is used to specify the null-terminated string of the programmatic identifier (such as "MS.WinCE.Outlook" for the Inbox) for the service provider, and is followed by szStoreDesc, which can be used to set a description of the data store.

The uTimerRes field should be set only if the SCF_SIMULATE_RTS flag is set in uFlags. This will determine the frequency with which the data store is checked for changes, and is specified in microseconds. If the flag is not set, you should set this to 0.

The last three parameters involve the store identifier. The maximum value that the Service Manager can accept for a store ID is passed in via the cbMaxStoreId field. You can then use the cbStoreId and lpbStoreId fields to set the identifier and its size, in bytes.

Once ActiveSync has retrieved the store identifier, it will immediately call into the IReplStore::CompareIDs() function. This enables you to determine if the store that the Service Manager is using matches the one with which it was last synchronized.

The function is defined as follows:

 HRESULT IReplStore::CompareStoreIDs(LPBYTE lpbID1, UINT   cbID1, LPBYTE lpbID2, UINT cbID2); 

The first two parameters passed to the function, lpbID1 and cbID1, specify the size, in bytes of the buffer, and a long pointer to the buffer of the first store identifier. lpbID2 and cbID2 pass in the same information for the second ID.

It is the function's responsibility to determine whether the two identifiers that are passed in represent the same store. You need to perform whatever logic is needed in order to make the determination. If the two stores match, you must return a value of 0 for the function. Otherwise, return a value of -1 if the first store is smaller than the second or 1 if the first store is larger than the second one.

The following example shows how you can handle the request to get store information and the comparison of store identifiers:

 STDMETHODIMP CMyReplStore::GetStoreInfo(PSTOREINFO   pStoreInfo) {    // Get the store identifier    if(!pStoreInfo)       return E_POINTER;    if(pStoreInfo->cbStruct != sizeof(STOREINFO))       return E_INVALIDARG;    // Set up the STOREINFO structure with the basic store    // info    pStoreInfo->uFlags = SCF_SINGLE_THREAD;    wsprintf(pStoreInfo->szProgId, TEXT("OPML.Sync"));    wsprintf(pStoreInfo->szStoreDesc, TEXT("OPML File       Sync"));    // If the object has not been initialized, just exist    if(!m_fInitialized)       return NOERROR;    // Set up the unique store ID. For this example, let's just    // hard-code the ID to "OpmlSyncFiles" since we are not worried    // about different stores.    TCHAR tchStoreID[32] = TEXT("\0");    DWORD dwNumBytes = 0;    wsprintf(tchStoreID, TEXT("OpmlSyncFiles"));    dwNumBytes = (lstrlen(tchStoreID)+sizeof(TCHAR))*sizeof(TCHAR);    if(pStoreInfo->cbMaxStoreId < dwNumBytes)       return E_OUTOFMEMORY;    if(!pStoreInfo->lpbStoreId)       return E_POINTER;    // Copy the store ID over    pStoreInfo->cbStoreId = dwNumBytes;    memcpy(pStoreInfo->lpbStoreId, tchStoreID, dwNumBytes);    return NOERROR; } STDMETHODIMP_(INT) CMyReplStore::CompareStoreIDs(LPBYTE   lpbID1, UINT cbID1, LPBYTE lpbID2, UINT cbID2) {    // Make sure that we're using the same store ID as the    // last time    // if(!lpbID1 || !lpbID2)       return 0;    if(cbID1 < cbID2)       return -1;    if(cbID1 > cbID2)       return 1;    return memcmp(lpbID1, lpbID2, cbID1); } 

If the Service Manager receives a response from IReplStore::CompareStoreIDs() indicating that the two stores are different, then ActiveSync will remove any mapping information it already has for the provider, and prompt the end user if they want to combine the data on the device or merge them.

Once ActiveSync has determined that the data store it is using matches the one that was used the last time the device was synchronized, it will attempt to enumerate the desktop objects to see if any data has changed.

ActiveSync Data Object Types

Before we dive into how a service provider works with the data from your application, you should understand the two data types that ActiveSync uses to perform synchronization.

In order to uniquely identify objects, ActiveSync makes use of a generic handle type known as HREPLITEM, which represents a handle to a single data object. This handle is a 32-bit value that a service provider will need in order to get access to a particular piece of data. Whenever ActiveSync needs to work with a particular object, it uses the HREPLITEM handle as the parameter. Note that the IReplStore::FindFirstItem() and IReplStore::FindNextItem() functions are the only two methods that are called to create new HREPLITEM handles.

For example, when the Service Manager requires you to compare data between two objects (which it will quite frequently), it will call the IReplStore::CompareItem() function that you have implemented, passing in two HREPLITEM handles for the objects to compare:

 HRESULT IReplStore::CompareItem(HREPLITEM hItem1, HREPLITEM   hItem2); 

Both parameters are object handles for data that was returned from a previous call into the IReplStore::FindFirstItem() or IReplStore::FindNextItem() function. You should return a value of 0 if they match, 1 if hItem1 is larger than the second, or -1 if hItem1 is smaller than the second.

The other data type that ActiveSync uses is HREPLFLD, which represents a unique 32-bit handle to a folder of objects. As with HREPLITEM, the actual value of an HREPLFLD handle has no particular meaning to the Service Manager; it merely represents a way to access a container object that contains a bunch of HREPLITEMs.

When ActiveSync needs to get the folder handle, it will call into the IReplStore::GetFolderInfo() function, which has the following prototype:

 HRESULT IReplStore::GetFolderInfo(LPSTR lpszObjType,   HREPLFLD *phFld, IUnknown **ppObjHandler); 

The first parameter is a null-terminated string that contains the name of the object for which it is requesting the folder handle. Once you have created an internal data structure with the folder, you can return the handle in the phFld parameter. The last parameter should point to the IReplObjHandler interface that is used to serialize items in the folder.

The following example shows how you can implement the creation of a new folder handle:

 STDMETHODIMP CMyReplStore::GetFolderInfo(LPSTR lpszObjType,   HREPLFLD *phFld, IUnknown **ppunk) {    // Get the folder handle. We will have only one folder,    // the directory for OPML files    SYNCOBJ *pSyncFolder = (SYNCOBJ *)*phFld;    if(!pSyncFolder) {       pSyncFolder = new SYNCOBJ;       memset(pSyncFolder, 0, sizeof(SYNCOBJ));       pSyncFolder->bType = ID_FOLDER;       wsprintf(pSyncFolder->tchName, TEXT("C:\\DesktopSync\\OPML"),           MAX_PATH);       *phFld = (HREPLFLD)pSyncFolder;    }    // Make sure the data handler is hooked up    *ppunk = (IUnknown *)m_pObjHandler;    return NOERROR; } 

During the synchronization process, ActiveSync also needs to occasionally check whether a data object represented by either a HREPLITEM or HREPLFLD handle is still valid. When the check needs to occur, your service provider will receive a call into the IReplStore::IsValidObject() function, which is defined as follows:

 HRESULT IReplStore::IsValidObject(HREPLFLD hFld, HREPLITEM   hObject, UINT uFlags); 

The first two parameters, hFld and hObject, can specify either the handle to the folder or the handle to the object that the service provider should check for validity. The last parameter, uFlags, is not used.

The function should return a value of NOERROR if the object or folder still exists, as shown in the following example:

 STDMETHODIMP CMyReplStore::IsValidObject(HREPLFLD hFld,   HREPLITEM hObject, UINT uFlags) {    SYNCOBJ *pSyncObjectFld = (SYNCOBJ *)hFld;    SYNCOBJ *pSyncObject = (SYNCOBJ *)hObject;    // Check the folder    if(pSyncObjectFld) {       if(pSyncObjectFld->bType != ID_FOLDER)          return RERR_CORRUPT;    }    // Check the object    if(pSyncObject) {       if(pSyncObject->bType != ID_OBJECT)          return RERR_CORRUPT;    }    return NOERROR; } 

When ActiveSync needs to copy either a folder handle or a data object, it will call the appropriately named IReplStore::CopyObject() function:

 HRESULT IReplStore::CopyObject(HREPLOBJ hObjSrc,   HREPLOBJ hObjDest); 

The only parameters you are passed here are the handles to the source and destination objects. If the copy is successful, you should return TRUE.

The following example shows how you can handle a request to copy an object:

 STDMETHODIMP_(BOOL) CMyReplStore::CopyObject(HREPLOBJ   hObjSrc, HREPLOBJ hObjDest) {    // Copy the object    SYNCOBJ *pSyncObjectSrc = (SYNCOBJ *)hObjSrc;    SYNCOBJ *pSyncObjectDst = (SYNCOBJ *)hObjDest;    // If it's the folder, just return    if(pSyncObjectSrc->bType == ID_FOLDER)    return TRUE;    // Copy the object    _tcscpy(pSyncObjectDst->tchName,       pSyncObjectSrc->tchName);    pSyncObjectDst->bType = ID_OBJECT;    memcpy(&pSyncObjectDst->ftModified,       &pSyncObjectSrc->ftModified,       sizeof(FILETIME));    return TRUE; } 

Finally, when ActiveSync requires that the memory that an object has allocated needs to be released, it will call into your IReplStore::FreeObject() function, which has the following prototype:

 HRESULT IReplStore::FreeObject(HREPLOBJ hObject); 
ActiveSync and Object Enumeration

Now that we've taken a quick look at how ActiveSync works with data objects and folders, let's see what happens when the Service Manager needs to enumerate the items in a desktop store in order to perform synchronization. After a device provider has been initialized and the Service Manager has determined that the data store is the same as it was after the last synchronization process, ActiveSync will check all of the object data folders to see if anything has changed.

As the first step of the enumeration process, the Service Manager checks whether a folder has been modified since the last time it was called:

 HRESULT IReplStore::IsFolderChanged(HREPLFLD hFld,   BOOL *pfChanged); 

The first parameter is a handle to a data object folder. It is followed by pfChanged, which you should set to TRUE if the folder has changed since the last time the function was called. If this is the first time the function is called from the service provider (or you're not using real-time synchronization), set it to TRUE so that ActiveSync will proceed to scan the application's data objects. The Service Manager will typically call this function every time it needs to determine whether the store has been changed when your provider object is using real-time synchronization (which is turned on by the IReplStore::GetStoreInfo() function), instead of scanning the entire data store.

The next function that is called in the data enumeration process is IReplStore::FindFirstItem(). This begins the actual process of walking through each item in the data store. You also use this function to assign the handles that you will be using for your data objects.

The function has the following prototype:

 HRESULT IReplStore::FindFirstItem(HREPLFLD hFld,   HREPLITEM *phItem, BOOL *pfExist); 

The first parameter, hFld, will indicate the handle to the folder that contains the objects for enumeration. The phItem pointer should be set to the handle of the first object that you find in the folder. Finally, if there are no objects in the folder, set pfExist to FALSE.

When ActiveSync is ready for the next HREPLITEM handle, it will call into the following:

 HRESULT IReplStore::FindNextItem(HREPLFLD hFld,   HREPLITEM *phItem, BOOL *pfExist); 

The first parameter is the handle to the folder you are enumerating. Next, the pointer that is passed in by the phItem parameter should be set to the next HREPLFLD handle in the enumeration. The last parameter, pfExist, should be set to FALSE if there are no objects left in the folder to enumerate.

When the enumeration process is complete (when either IReplStore::FindFirstItem() or IReplStore::FindNextItem() sets the pfExist flag to FALSE), the ActiveSync Manager will call into the IReplStore::FindItemClose() function. This function should be used to clean up any temporary variables or to free memory that was allocated to perform the enumeration:

 HRESULT IReplStore::FindItemClose(HREPLFLD hFld); 

The only parameter that is passed is the handle to the folder that was being enumerated.

The following example shows how you can implement the functions in a service provider to handle object enumeration:

 STDMETHODIMP CMyReplStore::FindFirstItem(HREPLFLD hFld,   HREPLITEM *phItem, BOOL *pfExist) {    // Start the enumeration of items. This will be a list of    // each OPML file that is in the folder. Each item will get a    // SYNCOBJ created for it.    SYNCOBJ *pSyncFolder = (SYNCOBJ *)hFld;    SYNCOBJ *pSyncObject = NULL;    TCHAR tchOPMLFilePath[MAX_PATH+1] = TEXT("\0");    WIN32_FIND_DATA w32Find;    if(m_hFileEnum != NULL)       return E_UNEXPECTED;    memset(&w32Find, 0, sizeof(WIN32_FIND_DATA));    wsprintf(tchOPMLFilePath, TEXT("%s\\*.opml"),         pSyncFolder->tchName);    m_hFileEnum = FindFirstFile(tchOPMLFilePath, &w32Find);    if(m_hFileEnum == INVALID_HANDLE_VALUE)       return E_OUTOFMEMORY;    pSyncObject = new SYNCOBJ;    memset(pSyncObject, 0, sizeof(SYNCOBJ));    pSyncObject->bType = ID_OBJECT;    pSyncObject->ftModified = w32Find.ftLastWriteTime;    _tcscpy(pSyncObject->tchName, w32Find.cFileName);    *pfExist = TRUE;    *phItem = (HREPLITEM)pSyncObject;    return NOERROR; } STDMETHODIMP CMyReplStore::FindNextItem(HREPLFLD hFld,   HREPLITEM *phItem, BOOL *pfExist) {    SYNCOBJ *pSyncObject = NULL;    WIN32_FIND_DATA w32Find;    if(pfExist)       *pfExist = FALSE;    if(!m_hFileEnum)       return E_FAIL;    // Get the next item, return an HREPLITEM for it    if(FindNextFile(m_hFileEnum, &w32Find) == 0)       return E_FAIL;    // Got an additional file, so get the info    pSyncObject = new SYNCOBJ;    memset(pSyncObject, 0, sizeof(SYNCOBJ));    pSyncObject->bType = ID_OBJECT;    pSyncObject->ftModified = w32Find.ftLastWriteTime;    _tcscpy(pSyncObject->tchName, w32Find.cFileName);    *pfExist = TRUE;    *phItem = (HREPLITEM)pSyncObject;    return NOERROR; } STDMETHODIMP CMyReplStore::FindItemClose(HREPLFLD hFld) {    if(!m_hFileEnum)       return E_FAIL;    // Close up the find function    FindClose(m_hFileEnum);    m_hFileEnum = NULL;    return NOERROR; } 
Object Storage

The ActiveSync Service Manager uses its own persistent file for storing information about an object type that is currently marked for synchronization. This file, which is transparent to the service provider, is used by ActiveSync to map objects to their store IDs, and store information about the last synchronization, as well as some additional overhead information.

When ActiveSync needs to save data to this file, it calls into the IReplStore::ObjectToBytes() function. This function is used to convert the handle for an HREPLITEM or HREPLFLD object into an array of bytes that the Service Manager will save:

The IReplStore::ObjectToBytes() function has the following prototype:

 HRESULT IReplStore::ObjectToBytes(HREPLOBJ hObject,   LPBYTE lpb); 

The first parameter is the handle for either an HREPLITEM or HREPLFLD object. The lpb parameter points to the buffer that should be used to store the array that the handle represents. After you have copied your data to the buffer, you should use the number of bytes that are stored in the buffer as the return value.

The first time that the Service Manager calls into the IReplStore::ObjectToBytes() function, the lpb pointer will be set to NULL. Simply return the number of bytes that you require for storing the object, and ActiveSync will automatically provide a correctly sized buffer on future calls.

The reverse operation, which converts a series of bytes into an object, is done by the following function:

 HRESULT IReplStore::BytesToObject(LPBYTE lpb, UINT cb); 

The first parameter is the array of bytes for the object, and is followed by cb, which indicates the size of the buffer. You should convert the buffer to an appropriate object, and return the new HREPLITEM or HREPLFLD for it.

The following example shows how you can implement the object conversion functions:

 STDMETHODIMP_(UINT) CMyReplStore::ObjectToBytes(HREPLOBJ   hObject, LPBYTE lpb) {    // Convert an SYNCOBJ to an array of bytes. Our object    // array will have the following structure:    // VERSION TYPE BYTES_NAME NAME FTMODIFIED    // Also, here's some constant information:    // VERSION_INFO = 1, ID_FOLDER = 0, ID_OBJECT = 1    DWORD dwBytes = 0;    BOOL fCopyToBytes = TRUE;    SYNCOBJ *pSyncObject = (SYNCOBJ *)hObject;    DWORD dwBufferLength = 0;        // If we receive a NULL buffer, then we just return the    // number of bytes needed    if(!lpb)       fCopyToBytes = FALSE;    // Copy the object to bytes (or just the size)    // Version:    if(fCopyToBytes) {       *lpb = VERSION_INFO;       lpb++;    }    dwBytes++;    // Object type    if(fCopyToBytes) {       *lpb = pSyncObject->bType;       lpb++;    }    dwBytes++;        /////////////////////////////////////    // Copy the URL    dwBufferLength = lstrlen(pSyncObject->tchName)   *sizeof(TCHAR);    if(fCopyToBytes) {       // URL Length       memcpy(lpb, &dwBufferLength, sizeof(DWORD));       lpb += sizeof(DWORD);           // URL       memcpy(lpb, pSyncObject->tchName, dwBufferLength);       lpb += dwBufferLength;    }    dwBytes += sizeof(DWORD) + dwBufferLength;    // Copy the date modified    if(fCopyToBytes) {       memcpy(lpb, &pSyncObject->ftModified,            sizeof(FILETIME));       lpb += sizeof(FILETIME);;    }    dwBytes += sizeof(FILETIME);    // Return the number of bytes    return dwBytes; } STDMETHODIMP_(HREPLOBJ) CMyReplStore::BytesToObject(LPBYTE   lpb, UINT cb) {    // Convert the byte stream to an object.    // VERSION TYPE BYTES_NAME NAME FTMODIFIED    // Also, here's some constant information:    // VERSION_INFO = 1, ID_FOLDER = 0, ID_OBJECT = 1    if(!lpb)       return NULL;    BYTE bVersion = *lpb++;    BYTE bType = *lpb++;    DWORD dwBufferLength = 0;    SYNCOBJ *pSyncObject = new SYNCOBJ;    memset(pSyncObject, 0, sizeof(SYNCOBJ));    // Check to see if we have a folder    if(bType == ID_FOLDER)       pSyncObject->bType = ID_FOLDER;    else       pSyncObject->bType = ID_OBJECT;    // We have a file object, copy the info to it.    // Get the file name    memcpy(&dwBufferLength, lpb, sizeof(DWORD));    lpb += sizeof(DWORD);    memcpy(pSyncObject->tchName, lpb, dwBufferLength);    lpb += dwBufferLength;    // Get the last modified date    memcpy(&pSyncObject->ftModified, lpb, sizeof(FILETIME));    lpb += dwBufferLength;    // Return the handle    return (HREPLOBJ)pSyncObject; } 
The ActiveSync Synchronization Process (Desktop Side)

Now that we've looked at how ActiveSync initializes the desktop provider, stores its data internally, and enumerates desktop data objects, let's examine the actual synchronization process.

After the Service Manager determines that the store identifier returned by the service provider matches the one that was stored in the ActiveSync Manager's persistent data file, it will begin synchronization. ActiveSync accomplishes this by looking at the list of handles with each enumeration of the data store, and then it makes a determination about data objects that have changed or been deleted.

When enumeration begins, ActiveSync marks each handle in its internal table, which was loaded from the persistent data store. After getting the folder information and calling into the desktop provider's IReplStore::FindFirstFile() and IReplStore::FindNextFile() functions, ActiveSync looks for a handle that matches the object that has already been stored. If no match is found, it simply creates the new object. If the handle matches, then ActiveSync removes the mark on the handle and calls into the IReplStore::IsItemChanged() method that your provider has implemented to determine whether anything has been modified.

The function that you need to implement has the following prototype:

 HRESULT IReplStore::IsItemChanged(HREPLFLD hFld, HREPLITEM   hItem, HREPLITEM hItemComp); 

The first parameter will be the handle to the folder that contains the object. Next, hItem will specify the handle to the item. The last parameter, hItemComp, is the handle to the object that should be compared. If hItemComp is NULL, you should check the item by opening the object that hItem specifies to see if anything has changed since the last synchronization. Returning TRUE for this function signifies that the object has changed.

If the item has changed, the Service Manager will call into IReplStore::CopyObject(), updating the internal data store with the new object's information. After that has completed, ActiveSync will call into IReplStore::IsItemReplicated() to determine whether or not the change should be copied to the device:

 HRESULT IReplStore::IsItemReplicated(HREPLFLD hFld,   HREPLITEM hItem); 

The function is passed two handles: the handle to the folder for the object and the handle to the data object itself. If the hItem parameter is a NULL value, then the service provider will have to determine whether or not the folder itself should be replicated. Return a value of TRUE to specify that the object should be copied to the device.

The following example shows how to implement the functions in IReplStore that are used to determine whether an object has changed:

 STDMETHODIMP_(BOOL) CMyReplStore::IsItemChanged(HREPLFLD   hFld, HREPLITEM hItem, HREPLITEM hItemComp) {    SYNCOBJ *pSyncFolder = (SYNCOBJ *)hFld;    SYNCOBJ *pSyncObject = (SYNCOBJ *)hItem;    SYNCOBJ *pSyncObjectComp = (SYNCOBJ *)hItemComp;    SYNCOBJ pTempObj;    BOOL fChanged = FALSE;    // If there's nothing to compare to, then find the object    if(!pSyncObjectComp) {       // Find the object in the node list       if(!FindObject(pSyncFolder->tchName,              pSyncObject->tchName, &pTempObj))          return FALSE;       pSyncObjectComp = &pTempObj;    }    // Check to make sure that the last modified time is    // the same    if(CompareFileTime(&pSyncObject->ftModified,       &pSyncObjectComp->ftModified) != 0)       fChanged = TRUE;    return fChanged; } STDMETHODIMP_(BOOL) CMyReplStore::IsItemReplicated(HREPLFLD   hFld, HREPLITEM hItem) {   // Replicate all objects.   return TRUE; } 

This process continues until all of the items in the data store have been enumerated.

After the first successful synchronization occurs, the ActiveSync Service Manager calls into the IReplStore::RemoveDuplicates() function. It is typical to have duplicate items in the data store, especially after data has been combined.

The function is defined as follows:

 HRESULT IReplStore::RemoveDuplicates(LPSTR lpszObjType,   UINT uFlags); 

The first parameter is the null-terminated string specifying the type of object that you will check for duplicate entries. The uFlags parameter is unused.

Updating ActiveSync

While the synchronization process is in progress, the ActiveSync Service Manager continuously calls the IReplStore::ReportStatus() method to send information on the current status:

 HRESULT IReplStore::ReportStatus(HREPLFLD hFld, HREPLITEM   hItem, UINT uStatus, UINT uParam ); 

The first two parameters, hFld and hItem, specify the handles for the folder and data object that the notification is for (and will be NULL if the status message doesn't apply to a particular folder or object). The last two parameters, uStatus and uParam, contain the message details. A status message can be one of the message types described in Table 9.21.

Table 9.21. Synchronization Status Messages

uStatus Value

Description

RSC_BEGIN_SYNC

Synchronization is about to begin. uParam will be set to BSF_AUTO_SYNC or BSF_REMOTE_SYNC.

RSC_END_SYNC

Synchronization is complete.

RSC_BEGIN_CHECK

ActiveSync is about to call into IReplStore::FindFirstItem() or IReplStore::FindNextItem().

RSC_END_CHECK

Enumeration has completed and is about to call into IReplStore::FindCloseItem().

RSC_DATE_CHANGED

The system date has changed.

RSC_RELEASE

ActiveSync is about to call into IReplStore::Release().

RSC_REMOTE_SYNC

Remote synchronization is about to start if the uParam value is set to TRUE. If uParam is FALSE, then remote synchronization is complete.

RSC_INTERRUPT

ActiveSync is about to interrupt the synchronization process.

RSC_BEGIN_SYNC_OBJ

Synchronization is about to start for a particular object type.

RSC_END_SYNC_OBJ

Synchronization has completed for a particular object type.

RSC_OBJ_TYPE_ENABLED

A specific object type is enabled for synchronization.

RSC_OBJ_TYPE_DISABLED

A specific object type is disabled for synchronization.

RSC_BEGIN_BATCH_WRITE

The IReplObjHandler::SetPackets() function is about to be called.

RSC_END_BATCH_WRITE

The service provider should commit the packet transfer.

RSC_CONNECTION_CHG

The status of the connection has changed. The uParam value will be set to TRUE if it has been established; otherwise, it will be set to FALSE.

RSC_WRITE_OBJ_FAILED

An error occurred while writing information to the device.

RSC_DELETE_OBJ_FAILED

An error occurred while deleting an object on the device.

RSC_WRITE_OBJ_SUCCESS

An object was successfully written to the device.

RSC_DELETE_OBJ_SUCCESS

An object was successfully deleted from the device.

RSC_READ_OBJ_FAILED

An error occurred while reading an object from the device.

RSC_TIME_CHANGED

The system time has changed.

RSC_BEGIN_BACKUP

The backup process is about to begin.

RSC_END_BACKUP

The backup process has completed.

RSC_BEGIN_RESTORE

The restore process is about to begin.

RSC_END_RESTORE

The restore process has completed.

RSC_PREPARE_SYNC_FLD

Synchronization is about to begin on a specific folder.

Whenever the ActiveSync Service Manager needs to write the data for an object to the persistent store, it will call into the IReplStore::UpdateItem() method:

 HRESULT IReplStore::UpdateItem(HREPLFLD hFld, HREPLITEM   hItemDst, HREPLITEM hItemSrc); 

The parameters that the functions receive specify the folder as well as the source and destination object data handles.

ActiveSync and User Interface Elements

ActiveSync will also call into your desktop service provider when the user wants to change any synchronization options. The desktop provider is required to implement its own dialog boxes for configuring the data that will be transferred to a device.

When the user selects the Settings button in ActiveSync, your provider will have the following function called:

 HRESULT IReplStore::ActivateDialog(UINT uidDialog,   HWND hwndParent, HREPLFLD hFld, IEnumReplItem *penumItem); 

The first parameter, uidDialog, specifies the dialog box that ActiveSync is requesting to activate. At this time, only the OPTIONS_DIALOG flag is supported. This is followed by the handle to the parent window, hwndParent. The last two parameters contain information about the object folder and a pointer to an IEnumReplItem interface, which contains an enumeration of HREPLITEM objects for the folder.

After you have shown the dialog box, you should set the return value based on how the user closed the dialog box.

The following is a list of return codes for IReplStore::ActivateDialog():

  • NOERROR should be returned if the user pressed OK to save the changes.

  • RERR_CANCEL should be returned to ignore any changes.

  • RERR_SHUT_DOWN should be returned if the user pressed OK, and ActiveSync needs to be restarted due to the changes.

  • RERR_UNLOAD should be returned if the user pressed OK, and ActiveSync needs to reload the synchronization provider for the change to be enabled.

  • E_NOTIMPL should be returned if you do not support any synchronization options.

The ActiveSync Service Manager will also contact your provider when it needs information about user interface elements for your object types, using the following function:

 HRESULT IReplStore::GetObjTypeUIData(HREPLFLD hFld,   POBJUIDATA pData); 

The hFld parameter is the handle to the folder that contains the objects. The next parameter, pData, points to an OBJUIDATA structure that you will need to fill in. The structure is defined as follows:

 typedef struct tagObjUIData {    UINT cbStruct;    HICON hIconLarge;    HICON hIconSmall;    char szName[MAX_PATH];    char szSyncText[MAX_PATH];    char szTypeText[80];    char szPlTypeText[80]; } OBJUIDATA, *POBJUIDATA; 

The first field, cbStruct, specifies the size of the structure. Next, the handles for the large and small icons for your data object should be set in the hIconLarge and hIconSmall fields. The rest of the fields deal with the null-terminated strings that are displayed: szName is used for the Name column, szSyncText for the Sync Copy In column, szTypeText for the object type, and szPlTypeText for the plural version of szTypeText.

Handling Conflicts

When data on both the device and the desktop has changed since the last time they were synchronized, a conflict occurs. When the Service Manager detects a conflict, it automatically pulls the conflicting data from the device (by using the IReplObjHandler interface, which is discussed later in this chapter), and creates a temporary desktop object. ActiveSync then calls into the IReplStore::GetConflictInfo() function, enabling you to examine both objects. The function requires you to fill in some information in the structure that it is passed, so ActiveSync can display a Conflict Resolution dialog box to the user. The function is defined as follows:

 HRESULT IReplStore::GetConflictInfo(PCONFINFO pConfInfo); 

The function receives a single parameter, which is a pointer to a CONFINFO structure. This structure provides detailed information about the conflicting data:

 typedef struct tagConfInfo {    UINT cbStruct;    HREPLFLD hFolder;    HREPLITEM hLocalItem;    HREPLITEM hRemoteItem;    OBJTYPENAME szLocalName;    TCHAR szLocalDesc[512];    OBJTYPENAME szRemoteName;    TCHAR szRemoteDesc[ 512 ]; } CONFINFO, *PCONFINFO; 

The first field, cbStruct, contains the size, in bytes, of the CONFINFO structure. The hFolder field contains the handle to the folder for the conflicting object, and is followed by handles for both the local object and the temporary object that was created.

The rest of the fields are used by the ActiveSync Service Manager to fill in information in the Conflict Resolution dialog box. The local object name and description should be filled in the szLocalName and szLocalDesc fields, respectively; szRemoteName and szRemoteDesc should be used for the temporary object.

Returning a value of NOERROR forces ActiveSync to prompt the user with a dialog box to handle the conflict. The automatic handling of a data conflict (which you should do for remote users), is determined by the return value:

  • RERR_IGNORE should be returned if you want ActiveSync to just ignore the conflict.

  • RERR_DISCARD should be returned if you want ActiveSync to delete the object on the device.

  • RERR_DISCARD_LOCAL should be returned if you want ActiveSync to delete the object on the desktop.

For example, to handle a conflict, you could do the following:

 STDMETHODIMP CMyReplStore::GetConflictInfo(PCONFINFO   pConfInfo) {    if(pConfInfo->cbStruct != sizeof(CONFINFO))       return E_INVALIDARG;    _tcscpy(pConfInfo->szLocalName, TEXT("OPML File"));    _tcscpy(pConfInfo->szRemoteName, pConfInfo->szLocalName);    // Compare local and remote    SYNCOBJ *pSyncObjectLocal =        (SYNCOBJ *)pConfInfo->hLocalItem;    SYNCOBJ *pSyncObjectRemote =        (SYNCOBJ *)pConfInfo->hRemoteItem;    // Check to see if identical    if(pSyncObjectLocal && pSyncObjectRemote) {       if(!_tcscmp(pSyncObjectLocal->tchName,              pSyncObjectRemote->tchName))          return RERR_IGNORE;    }    // No? Put information in the dialog    SYSTEMTIME sysTime;    if(pSyncObjectLocal) {       FileTimeToSystemTime(&pSyncObjectLocal->ftModified,            &sysTime);       wsprintf(pConfInfo->szLocalDesc, TEXT("Name:          %s\r\nModified: %d-%d-%d"),          pSyncObjectLocal->tchName, sysTime.wDay,          sysTime.wMonth,sysTime.wYear); }    if(pSyncObjectRemote) {       FileTimeToSystemTime(&pSyncObjectRemote->ftModified,            &sysTime);       wsprintf(pConfInfo->szRemoteDesc,          TEXT("Name: %s\r\nModified: %d-%d-%d"),          pSyncObjectRemote->tchName,          sysTime.wDay, sysTime.wMonth, sysTime.wYear);    }    return NOERROR; } 
Implementing IReplObjHandler

You must implement the IReplObjHandler interface inside your desktop service provider, which enables the ActiveSync Service Manager to convert a data object (either a folder or object) into a series of bytes to transfer to the connected device. This process is known as serializing. Converting the data from a series of bytes into an object is called deserializing.

Table 9.22 describes the methods that must be implemented by the IReplObjHandler interface.

Now let's look at how ActiveSync uses these functions to send and receive data.

Table 9.22. IReplObjHandler Methods

Method

Description

DeleteObj()

Deletes an object from the data store.

GetPacket()

Deserializes an object. This function is called when a data object needs to be converted into one or more packets of data.

Reset()

Resets the service provider and frees used resources.

SetPacket()

Serializes a data packet. This function is called when one or more packets of data need to be converted back into an object.

Setup()

Configures the service provider so it can begin serializing and deserializing data.

How ActiveSync Sends and Receives Objects

The first function that ActiveSync will call into when it needs to serialize or deserialize data is the IReplObjHandler::Setup() function, which is called for every object it needs to convert. The function has the following prototype:

 HRESULT IReplObjHandler::Setup(PREPLSETUP pSetup); 

The only parameter that the method is passed is a pointer to a REPLSETUP structure, which contains information about the object:

 typedef struct _tagReplSetup {    UINT cbStruct;    BOOL fRead;    DWORD dwFlags;    HRESULT hr;    OBJTYPENAME szObjType;    IReplNotify *pNotify;    DWORD oid;    DWORD oidNew;    IReplStore *pStore;    HREPLFLD hFolder;    HREPLITEM hItem; } REPLSETUP, *PREPLSETUP; 

The first field, cbStruct, specifies the size of the REPLSETUP structure. If the fRead field is set to TRUE, you are setting up for the serialization of data. If set to FALSE, you are getting ready to deserialize the object. The dwFlags and hr fields are reserved.

The next several fields provide you with information about the object that you are going to either serialize or deserialize. The szObjType field is a null-terminated string that contains the name of the object type you are working with. The oid and oidNew fields provide object identifiers that are used by ActiveSync. You can access the particular object by using the pStore, hFolder, and hItem fields, which will pass you the interface handle to the object's IReplStore interface as well as the folder and object handles.

The pNotify field points to the ActiveSync Service Manager's IReplNotify interface pointer.

The following code shows what is involved when implementing the IReplObjHandler::Setup() function:

 STDMETHODIMP CMyRplObjHandler::Setup(PREPLSETUP pSetup) {    if(pSetup->cbStruct != sizeof(REPLSETUP))       return E_INVALIDARG;    // Since reading/writing can occur at the same time,    // must set up for both    if(pSetup->fRead)       m_pReadSetup = pSetup;    else       m_pWriteSetup = pSetup;    return NOERROR; } 

Whether the object needs to be serialized or deserialized determines the next function into which the ActiveSync Manager calls. If the object requires serialization, then it will call into the IReplObjHandler::GetPacket() function:

 HRESULT IReplObjHandler::GetPacket(LPBYTE *lppbData,   DWORD *pcbData, DWORD cbRecommend); 

The first parameter is a pointer to the data buffer of the outgoing packet, which can be used to copy your data into. The size of the buffer is specified by the pcbData parameter. It is necessary for the desktop provider to allocate and de-allocate the buffer used for the data packet.

The last parameter, cbRecommend, is the maximum recommended size of each packet.

The IReplObjHandler::GetPacket() will be called continuously for the object it is trying to send until you return a value of RWRN_LAST_OBJECT.

The following code serializes an object into a packet:

 STDMETHODIMP CMyRplObjHandler::GetPacket(LPBYTE *lppbData,   DWORD *pcbData, DWORD cbRecommend) {    // Make sure we have the setup information for the read    if(!m_bPacket)       return E_UNEXPECTED;    // We'll need to build a packet based on the object    // (i.e., a file)    // m_pReadSetup will be used.    SYNCOBJ *pSyncObject = (SYNCOBJ *)m_pReadSetup->hItem;    SYNCOBJ *pSyncObjectFld =       (SYNCOBJ *)m_pReadSetup->hFolder;    TCHAR tchFilePath[MAX_PATH+1] = TEXT("\0");    HANDLE hFile = NULL;    DWORD dwBytesRead = 0;    wsprintf(tchFilePath, TEXT("%s\\%s"),       pSyncObjectFld->tchName, pSyncObject->tchName);    hFile = CreateFile(tchFilePath, GENERIC_READ,       FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL);    if(hFile == INVALID_HANDLE_VALUE)       return HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND);    memset(m_bPacket, 0, PACKET_SIZE);    if(!ReadFile(hFile, m_bPacket, PACKET_SIZE, &dwBytesRead,       NULL)) {       CloseHandle(hFile);       return RERR_BAD_OBJECT;    }    CloseHandle(hFile);    *lppbData = (LPBYTE)m_bPacket;    *pcbData = dwBytesRead;    return RWRN_LAST_PACKET; } 

If ActiveSync needs to deserialize the object, the IReplObjHandler::SetPacket() function will be called:

 HRESULT IReplObjHandler::SetPacket(LPBYTE lpbData, DWORD   cbData); 

The function is passed a pointer to the incoming packet of data, as well as its size.

The following code shows how to deserialize an object:

 STDMETHODIMP CMyRplObjHandler::SetPacket(LPBYTE lpbData,   DWORD cbData) {    // Create a SYNCOBJ from the packet    if(!m_pWriteSetup)       return E_UNEXPECTED;    SYNCOBJ *pSyncObject = (SYNCOBJ *)m_pWriteSetup->hItem;    SYNCOBJ *pSyncObjectFld =        (SYNCOBJ *)m_pWriteSetup->hFolder;    TCHAR tchFilePath[MAX_PATH+1] = TEXT("\0");    HANDLE hFile = NULL;    DWORD dwBytesWritten = 0;    wsprintf(tchFilePath, TEXT("%s\\%s"),       pSyncObjectFld->tchName, pSyncObject->tchName);    hFile = CreateFile(tchFilePath,       GENERIC_READ|GENERIC_WRITE, 0, NULL,       CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);    if(hFile == INVALID_HANDLE_VALUE)       return HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND);    // Write to the file    WriteFile(hFile, lpbData, cbData, &dwBytesWritten, NULL);    CloseHandle(hFile);    // Create the new SYNCOBJ    SYNCOBJ *pNewSyncObj = new SYNCOBJ;    SYSTEMTIME sysTime;    FILETIME ft;    memset(pNewSyncObj, 0, sizeof(SYNCOBJ));    pNewSyncObj->bType = ID_OBJECT;    _tcscpy(pNewSyncObj->tchName, pSyncObject->tchName);    GetSystemTime(&sysTime);    SystemTimeToFileTime(&sysTime, &ft);    FileTimeToLocalFileTime(&ft, &pSyncObject->ftModified);    m_pWriteSetup->hItem = (HREPLITEM)pSyncObject;    return NOERROR; } 

Note that the packets you receive from IReplObjHandler::SetPacket() are guaranteed to be in the same number and order as they were when sent from IReplObjHandler::GetPacket().

Once all of the objects have been serialized or deserialized, the ActiveSync Service Manager makes a final call into IReplObjHandler::Reset(). The method has the following prototype:

 HRESULT IReplObjHandler::Reset(PREPLSETUP pSetup); 

The only parameter is a handle to a REPLSETUP structure:

IReplObjHandler::Reset() will be called once per object, and can be used to free any memory that was used when converting your data.

The last function called by the ActiveSync Service Manager is IReplObjHandler::DeleteObj(). This method is used whenever an object needs to be deleted during conflict resolution or synchronization:

 HRESULT IReplObjHandler::DeleteObj(PREPLSETUP pSetup); 

The only parameter that is received is a pointer to a REPLSETUP structure.

Calling ActiveSync from Your Provider (Using IReplNotify)

Both the IReplObjHandler and IReplStore interfaces receive a pointer to an IReplNotify object that can be used to retrieve some basic information from the ActiveSync Service Manager. In addition, you can use this interface to notify ActiveSync when a change has occurred in an object's data so that real-time synchronization can occur. The interface is already implemented by ActiveSync, and supports the methods described in Table 9.23.

Table 9.23. ReplNotify Methods

Method

Description

GetWindow()

Gets the handle to the ActiveSync window. This handle should be used as the parent handle for any dialog box that the service provider creates.

OnItemNotify()

Notifies ActiveSync when a data object has been created, deleted, or modified.

QueryDevice()

Requests information about a device from ActiveSync, such as the name or type of device.

To get the handle to the ActiveSync window to use as a parent, you can call into the following function:

 HRESULT IReplNotify::GetWindow(UINT uFlags); 

The uFlags parameter is not currently used, and should be set to 0.

To get information about the connected device, use the IReplNotify::QueryDevice() function:

 HRESULT IReplNotify::QueryDevice(UINT uCode,   LPVOID *ppvData); 

The first parameter, uCode, should be used to specify what information you want to receive from the Service Manager about a device. The value you set determines the buffer to which ppvData should point.

The uCode parameter can be set to the following:

  • QDC_SEL_DEVICE should be used to get information about the selected device. The ppvData pointer should point to a DEVINFO structure.

  • QDC_CON_DEVICE should be used to get information about the connected device. The ppvData pointer should point to a DEVINFO structure.

  • QDC_SYNC_DATA should be used to get custom device synchronization information. See the SyncData() function on the device service provider for more information. The ppvData pointer should point to an SDREQUEST structure.

  • QDC_SEL_DEVICE_KEY should be used to get the registry key that can be used to store specific information for the selected device. The ppvData pointer should point to an HKEY.

  • QDC_CON_DEVICE_KEY should be used to get the registry key that can be used to store information about the connected device. The ppvData pointer should point to an HKEY.

The DEVINFO structure is defined as follows:

 typedef struct tagDevInfo {    DWORD pid;    char szName[MAX_PATH];    char szType[80];    char szPath[MAX_PATH]; } DEVINFO, *PDEVINFO; 

The structure's fields contain the device ID, and its name, type, and path.

The last function that you will use to send messages back to ActiveSync is the IReplNotify::OnItemNotify() method. This method is extremely useful when performing real-time synchronization, as it lets ActiveSync know that an object has been changed, added, or deleted:

 HRESULT IReplNotify::OnItemNotify(UINT uCode, LPSTR   lpszProgId, LPSTR lpszName, HREPLITEM hItem, ULONG ulFlags); 

The first parameter lets ActiveSync know what has happened to the data object. It can be set to any of the following: RNC_CREATED indicates that a new object has been created, RNC_MODIFIED indicates that an object has been changed, RNC_DELETED indicates that an object has been deleted, and RNC_SHUTDOWN indicates whether the store has been terminated and ActiveSync should unload the synchronization provider.

The next parameter, lpszProgId, is the programmatic identifier of the data store, and is followed by lpszName, which should point to a null-terminated string specifying the object type. The hItem parameter is the handle to the object you are notifying ActiveSync about. The last parameter, ulFlags, is unused.

Writing the Device Provider Module

The other half of a custom synchronization component is the device-side provider module, which will consist of the following items:

  • The device provider must implement the same IReplObjHandler interface as the desktop, to ensure that data packets are serialized and deserialized properly.

  • Three functions must be exported from the device provider DLL: GetObjTypeInfo(), InitObjType(), and ObjectNotify(). In addition, there are three optional functions: FindObjects(), ReportStatus(), and SyncData().

Before taking a more detailed look at the device provider, let's see how a new device provider module needs to be properly configured in the registry.

Registering the Device Provider

Registering the device side of a synchronization provider is much less complicated than its desktop counterpart. To register the synchronization component, you need to place a new key in the following location:

 HKEY_LOCAL_MACHINE\SOFTWARE\Windows CE   Services\Synchronization\Objects\{New Object Name} 

This key requires the values described in Table 9.24.

Table 9.24. Registry Settings for a Device-Side Synchronization Provider

Key\Value Name

Description

Store

Full path to the library that exports the device's provider functions

DisplayName

Name to be used in the device's ActiveSync Service Manager

Let's take a look at the registry keys that are used for the device side of the Pocket Inbox synchronization provider:

 // // Registry entries for the object used by the device service // provider // [HKEY_LOCAL_MACHINE\Windows CE     Services\synchronization\objects\Merlin Mail] "Store"="cemailsync.dll" 
Implementing IReplObjHandler on the Device

The implementation of IReplObjHandler that you need to create for the device side of a synchronization provider should be the same as the desktop implementation. This ensures that packets sent to the desktop are in the correct format, and that packets that are received can be turned into objects properly.

In addition to IReplObjHandler, you need to have your synchronization provider export the functions described in Table 9.25.

Table 9.25. IReplObjHandler Methods

Key\Value Name

Description

GetObjTypeInfo()

Gets the type of object specified.

InitObjType()

Initializes and returns a pointer to the device provider's IReplObjHandler interface.

ObjectNotify()

Notifies the device-side ActiveSync Service Manager when an object is added, changed, or deleted.

ReportStatus()

Optional. Gets the status for synchronization objects.

FindObjects()

Optional. Used to synchronize database volumes.

SyncData()

Optional. Used to provide the desktop provider with a simple way to send and receive data to and from the device.

Let's take a look at how the device's ActiveSync Service Manager uses your provider to perform synchronization tasks.

NOTE:

There is currently no way to debug a device-side ActiveSync provider. I recommend creating some functions that will write output to a log file to help debug your device-side SSP.


Initializing a Device Provider

When the ActiveSync Manager on the device needs to initialize or terminate the device provider, it will call into the InitObjType() function for each object type that it supports. The function has the following prototype:

 BOOL InitObjType(LPWSTR lpszObjType,   IReplObjHandler **ppObjHandler, UINT uPartnerBit); 

The first parameter, lpszObjType, is a null-terminated string that provides you with the object type that ActiveSync is trying to initialize. As part of the initialization process, you need to instantiate the IReplObjHandler interface for the object type it is requesting, and return the pointer to it in the ppObjHandler parameter. The last parameter, uPartnerBit, specifies which partner desktop it is connecting with, either partner 1 or partner 2.

If the lpszObjType value is NULL, then ActiveSync is unloading your service provider, and you should do whatever cleanup is necessary.

The following example initializes the device provider:

 DllExport BOOL InitObjType(LPWSTR lpszObjType,   IReplObjHandler **ppObjHandler, UINT uPartnerBit) {    // Initialize/uninitialize    if(!lpszObjType) {       // Uninit'ing, so cleanup here       WriteToLog(TEXT("InitObjType: Close\r\n"));       if(hLogFile)          CloseHandle(hLogFile);       return TRUE;    }    // Take care of the log    hLogFile = CreateFile(TEXT("\\synclog.txt"),       GENERIC_WRITE|GENERIC_READ,       FILE_SHARE_READ, NULL, CREATE_ALWAYS,       FILE_ATTRIBUTE_NORMAL, NULL);    WriteToLog(TEXT("InitObjType: Open\r\n"));    // Set up the handler    *ppObjHandler = new CMyRplObjHandler();    return TRUE; } 
Device Object Enumeration

The bulk of the work on the client side is done through object enumeration, but it is significantly different from what was implemented in the IReplStore::FindFirstItem() and IReplStore::FindNextItem() functions on the desktop provider.

When a device partnership has been established, ActiveSync enumerates every file system object and call into each service provider's ObjectNotify() function. It is the responsibility of ObjectNotify() to determine whether or not a change to the data should be synchronized.

The function has the following prototype:

 BOOL ObjectNofity(POBJNOTIFY pon); 

The only parameter you are passed is a pointer to an OBJNOTIFY structure, which is defined as follows:

 typedef struct tagObjNotify {    UINT cbStruct;    OBJTYPENAME szObjType;    UINT uFlags;    UINT uPartnerBit;    CEOID oidObject;    CEOIDINFO oidInfo;    UINT cOidChg;    UINT cOidDel;    UINT *poid;    LPBYTE lpbVolumeID;    UINT cbVolumeID; } OBJNOTIFY, *POBJNOTIFY; 

The first field, cbStruct, provides the size of the OBJNOTIFY structure that is being pass in, and is followed by a null-terminated string that holds the object type. The uFlags field specifies the type of object that the structure is for, and can be one or more of the objects described in Table 9.26.

Table 9.26. ObjectNotify() Flags

uFlag Value

Description

ONF_FILE

The object type is a file.

ONF_DIRECTORY

The object type is a folder.

ONF_DATABASE

The object type is a database.

ONF_RECORD

The object type is a database record.

ONF_CHANGED

The object has changed.

ONF_DELETED

The object has been deleted.

ONF_CLEAR_CHANGE

The object is a synchronized object identifier, and should be marked as up-to-date.

ONF_CALL_BACK

Requests ActiveSync to try again in two seconds.

ONF_CALLING_BACK

ActiveSync sets this in response to a ONF_CALL_BACK request.

Next, the uPartnerBit field will specify the desktop partner, either 1 or 2, with which the device is connected.

The remaining fields of the structure are used to provide object identifier (OID) information about the data object(s) that have changed. The oidObject field is the OID for the file system object, database, or database record to which you are being passed information by the structure. If an object has been modified, then you need to set the oidInfo field with a completed OIDINFO structure regarding the updated object. The flags that are set in the uFlag field determine the value of the next two items. When uFlag has the ONF_CHANGED flag set, the cOidChg field should be set to the number of OIDs that have changed and require synchronization; otherwise, set it to 0. When uFlag has the ONF_DELETED flag set, cOidDel should be set to the number of OIDs that have been deleted.

The last field, poid, is a pointer to an array of object identifiers. The number of objects required in the array is specified by the number of changed or deleted items, cOidChg or cOidDel, respectively.

You should return a value of TRUE if you want to specify that the changed object should be synchronized.

The following example shows how you can handle a call into the ObjectNotify() function:

 DllExport BOOL ObjectNotify(POBJNOTIFY pNotify) {    WriteToLog(TEXT("Start ObjectNotify\r\n"));    if(pNotify->cbStruct != sizeof(OBJNOTIFY))    return FALSE;    /* Log File */    TCHAR tchLogBuffer[MAX_PATH] = TEXT("\0");    wsprintf(tchLogBuffer, TEXT("\tObj Name: %s\r\n\tOID:       %u\r\n"), pNotify->szObjType, pNotify->oidObject);    WriteToLog(tchLogBuffer);    CEOIDINFO *pOidInfo = &pNotify->oidInfo;    if(pOidInfo) {       if(pOidInfo->wObjType == OBJTYPE_FILE)          wsprintf(tchLogBuffer, TEXT("\tFile Object: %s,             Length: %d\r\n"), pOidInfo->infFile.szFileName,             pOidInfo->infFile.dwLength);       else          wsprintf(tchLogBuffer, TEXT("\tNo file               object\r\n"));       WriteToLog(tchLogBuffer);    }    /* End Log File */    // Handle the object notify    if(_tcsicmp(pNotify->szObjType, TEXT("OPML Files"))       != 0) {       WriteToLog(TEXT("\tObjectNotify: Invalid Object           Type\r\n"));       return FALSE;    }    // Handle the notifications we're interested in    BOOL fReturn = FALSE;    if(pNotify->uFlags & ONF_FILE) {      if(pNotify->oidInfo.wObjType == OBJTYPE_FILE) {          // Interested in DELETE and CHANGE notifications          if(pNotify->uFlags & ONF_DELETED) {             WriteToLog(TEXT("\tObjectNotify: Deleted\r\n"));             pNotify->cOidDel = 1;             fReturn = TRUE;          }          if(pNotify->uFlags & ONF_CHANGED) {             WriteToLog(TEXT("\tObjectNotify: Changed\r\n"));             pNotify->cOidChg = 1;             fReturn = TRUE;          }       }    }    if(fReturn)       WriteToLog(TEXT("\tObjectNotify: TRUE\r\n"));    else       WriteToLog(TEXT("\tObjectNotify: FALSE\r\n"));    pNotify->poid = (UINT *)&pNotify->oidObject;    return fReturn; } 

Whenever the ActiveSync Service Manager needs more information about an object type, it calls into the GetObjTypeInfo() function:

 BOOL GetObjTypeInfo(POBJTYPEINFO poti); 

The only parameter you are passed is a pointer to an OBJTYPEINFO structure, which you will need to fill in. The structure is defined as follows:

 typedef struct tagObjTypeInfo {    UINT cbStruct;    OBJTYPENAMEW szObjType;    UINT uFlags;    WCHAR szName[80];    UINT cObjects;    UINT cbAllObj;    FILETIME ftLastModified; } OBJTYPEINFO, *POBJTYPEINFO; 

The first field specifies the size of the structure, in bytes; and is followed by szObjType, a null-terminated string that contains the name of the object type. The uFlags field is reserved, and set to 0.

You should set the rest of the OBJTYPEINFO structure's fields with the information about the type. The szName field should be set to a string that contains the name of the file system object that stores them; cObjects should be set to the number of objects; cbAllObj should be set to the total number of bytes; and ftLastModified should be set to the FILETIME that specifies the last time that any of the objects were modified.

You can handle a request to the GetObjTypeInfo() function as follows:

 DllExport BOOL GetObjTypeInfo(POBJTYPEINFO pInfo) {    WriteToLog(TEXT("Start GetObjTypeInfo\r\n"));    if(pInfo->cbStruct < sizeof(OBJTYPEINFO))       return FALSE;    /* Log File */    TCHAR tchLogBuffer[MAX_PATH] = TEXT("\0");    wsprintf(tchLogBuffer, TEXT("\tObj Type Name: %s\r\n"),        pInfo->szObjType);    WriteToLog(tchLogBuffer);    /* End Log File */    // Set up the information about the device object that will    // store the OPML items    wsprintf(pInfo->szName, TEXT("OPML Files"));    // Find out how many items    WIN32_FIND_DATA w32Find;    HANDLE hFileEnum = 0;    memset(&w32Find, 0, sizeof(WIN32_FIND_DATA ));    pInfo->cObjects = 0;    pInfo->cbAllObj = 0;    WriteToLog(TEXT("Starting Enum\r\n"));    hFileEnum = FindFirstFile(TEXT("\\*.opml"), &w32Find);    do {       pInfo->cObjects++;       pInfo->cbAllObj =+ w32Find.nFileSizeLow;       pInfo->ftLastModified = w32Find.ftLastWriteTime;       WriteToLog(TEXT("Found in Enum: "));       WriteToLog(w32Find.cFileName);       WriteToLog(TEXT("\r\n"));       if(FindNextFile(hFileEnum, &w32Find) == FALSE)          break;    } while(hFileEnum != NULL);    if(hFileEnum) {       WriteToLog(TEXT("Closing Enum\r\n"));       FindClose(hFileEnum);    }    return TRUE; } 

If you have exported the FindObjects() function, ActiveSync will also use it to help speed up database synchronization. It is used to enumerate all the objects that are to be synchronized in a database and return the list of OIDs to ActiveSync.

The function is defined as follows:

 HRESULT FindObjects(PFINDOBJINFO pfoi); 

FindObjects() has only a single parameter, which is a pointer to a FINDOBJINFO structure. The structure has the following prototype:

 typedef struct tagFindObjInfo {    UINT uFlags;    OBJTYPENAME szObjType;    UINT *poid;    UINT cUnChg;    UINT cChg;    LPBYTE lpbVolumeID;    UINT cbVolumeID;    LPVOID lpvUser; } FINDOBJINFO, *PFINDOBJINFO; 

The first field, uFlags, indicates whether ActiveSync is starting or finishing an enumeration. The first time ActiveSync calls into FindObjects(), uFlags will be set to 0. This indicates that you should enumerate all of the objects with which the device provider synchronizes. You should set uFlags to FO_MORE_VOLUME if there are more objects to be returned. The uFlags field will be set to FO_DONE_ONE_VOL by the ActiveSync Service Manager if it is finished with the OIDs you have sent to it.

The next field, szObjType, will contain the null-terminated string of the object type that needs enumeration. The poid field should point to an array of OIDs for the objects that are synchronized by the provider. You need to ensure that the first items in the list are those that haven't changed, followed by the OIDs for modified objects. The cUnChg field specifies the number of unchanged objects in poid, and cChg specifies the number of changed objects.

The lpbVolumeID field is the ID of the database volume. If the database is in the local object store, then you can set this to NULL. The size of the volume that you are enumerating should be set in the cbVolumeID field. A few cautionary words: If a synchronized database is stored on more than one volume, you must return one list per volume (and use the FO_MORE_VOLUME flag). The last field, lpvUser, is an empty data pointer that you can use to save any additional information you'd like.

Device-Side Status Reporting

When the ActiveSync Manager on the device has additional information about a synchronization process in progress, it will call into the ReportStatus() function if you have exported it from your device provider. The function is defined as follows:

 BOOL ReportStatus(LPWSTR lpszObjType, UINT uCode, UINT   uParam); 

The first parameter is a null-terminated string that contains the name of the object type. The uCode and uParam parameters are the same as for the desktop IReplStore::ReportStatus() function.

Custom Synchronization Information

ActiveSync also provides a way for your desktop synchronization provider to make a direct request to the device-side provider. This is done by having the desktop call into the IReplNotify::QueryDevice() function using the QDC_SYNC_DATA flag. This request is immediately processed, and the device-side provider receives a call into the optional SyncData() function:

 HRESULT SyncData(PSDREQUEST psd); 

The only parameter that the function receives is a pointer to a SDREQUEST structure, which is defined as follows:

 typedef struct SDREQUEST {    OBJTYPENAME szObjType;    BOOL fSet;    UINT uCode;    LPBYTE lpbData;    UINT cbData; } SDREQUEST, *PSDREQUEST; 

The first field, szObjType, is a null-terminated string that specifies the object type. The fSet field should be set to TRUE if data is being sent to the device from the desktop. This field should be set to FALSE if you are returning data from the device. The uCode field is a user-defined code that is used when getting data from the device. This value must be lower than 8.

The last two fields are used to specify the data buffer and the size. The first time ActiveSync calls into the function, the lpbData field will be set to NULL. At this time, you should set the cbData field with the size of the buffer that you will require. The next time SyncData() is called, the buffer will be properly allocated.



Pocket PC Network Programming
Pocket PC Network Programming
ISBN: 0321133528
EAN: 2147483647
Year: 2005
Pages: 90

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net