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