Implementing the desktop provider takes more time and effort. This is, in the main, because ActiveSync makes no assumptions about where items are stored and when they are changed. You need to create a full COM component with a class factory, registration, and other features. The component will need to implement the COM component IReplStore, which is used to manage the store, folder, and item manipulation. The desktop provider also needs to implement the IReplObjHandler interface. Representing HREPLITEM and HREPLFLDHREPLITEM and HREPLFLD are pointers used by ActiveSync to point at your data associated with items and folders. In certain circumstances, ActiveSync passes a HREPLOBJ that can point either to a HREPLITEM or HREPLFLD. You therefore need a storage mechanism that can store either a HREPLITEM or HREPLFLD and be able to determine which type is currently being stored. Perhaps the most straightforward technique is to use a structure containing a union (Listing 17.10). Listing 17.10 REPLOBJECT structure// structure and define for HREPLITEM and // HREPLFLD structures #define RT_ITEM 1 #define RT_FOLDER 2 typedef struct tagREPLOBJECT { // uType indicates if a folder (RT_FOLDER) // or item (RT_ITEM) is currently being stored UINT uType; // Create a union so folder and item information can be // stored in the same structure union { // for folder, has the contents been changed BOOL fChanged; // for item, creation time (unique identifier) // and last modify time struct { FILETIME ftCreated, ftModified; }; }; } REPLOBJECT, *LPREPLOBJECT; The member uType can contain either RT_ITEM or RT_FOLDER; they indicate the current use of the structure. Folders use the member fChanged and items use ftCreated or ftModified. ActiveSync uses the data you place in this structure to track changes to folders and items. There is generally a separate structure for each item and folder in store, and ActiveSync stores these structures in repl.dat. You can place any type of data in the structure that is applicable to your application. However, you should attempt to limit the amount of data you store in the structure. There will always be far more items than folders, so you should focus on the amount of data stored in the item. If you use a structure with a union (as shown in Listing 17.10), ensure that the amount of data associated with the folder is less than that used for the item the overall size of the structure is determined by the largest members in the union. Storing Data on the DesktopActiveSync makes no assumptions about where the data being synchronized is being stored you can use flat files, local databases, or server databases. In this example a simple flat file is used, and each item is stored as a NOTE structure (which has a fixed size). The code to access this file is located in ListDB.h, and uses the standard file I/O techniques outlined in Chapter 2. Implementing IReplStoreThe IReplStore COM interface declares functions that ActiveSync uses to obtain information and manipulate the store, folder, and items being synchronized. Table 17.3 shows the functions categorized by function.
Because IReplStore is a COM interface, you must provide an implementation of all these functions; otherwise, your provider will not compile. However, only the functions shown in this chapter are essential in a simple provider. In the example, IReplStore is implemented by the class CActiveSyncEg. The declaration of this class is in Component.h, and the implementation of IReplStore is in the file IReplStore.cpp. IReplStore InitializationActiveSync calls the IReplStore::Initialize function when the provider is first loaded (Listing 17.11). The function passes a pointer to a IReplNotify interface provided by ActiveSync and used by the provider to notify ActiveSync when item changes occur in the store. Since the example doesn't implement continuous synchronization, this pointer is ignored. The uFlags parameter will contain ISF_REMOTE_CONNECTED if synchronization is being carried out over a dialup or other type of remote connection. In this case, you should avoid anything that requires user intervention (such as showing a dialog). Listing 17.11 IReplStore::Initialize implementationSTDMETHODIMP CActiveSyncEg::Initialize( IReplNotify*pNotify,UINT uFlags ) { m_bInitialized = TRUE; return NOERROR; } Some IReplStore functions can be called before Initialize is called, so you should be careful not to rely on this initialization. The functions are GetStoreInfo, GetObjTypeUIDate, GetFolderInfo, ActivateDialog, BytesToObject, ObjectToBytes, and ReportStatus. Store Information and ManipulationActiveSync calls the function GetStoreInfo (Listing 17.12) and passes a pointer to a STOREINFO structure that the provider populates with information about its store. Listing 17.12 IReplStore:: GetStoreInfo implementationSTDMETHODIMP CActiveSyncEg::GetStoreInfo( PSTOREINFO pStoreInfo ) { // Check correct version of StoreInfo structure if(pStoreInfo->cbStruct sizeof(*pStoreInfo)) { MessageBox(NULL, _T("GetStoreInfo Invalid Arg"), NULL, 0); return E_INVALIDARG; } // we only support single-threaded operation pStoreInfo->uFlags = SCF_SINGLE_THREAD; // Set store's progid and description strcpy(pStoreInfo->szProgId, g_szVerIndProgID); strcpy(pStoreInfo->szStoreDesc, g_szFriendlyName); // this is as far as we get if we're not Initialized if(!m_bInitialized) { return NOERROR; } // Create the store's unique identifier // Set the length of the store identifier pStoreInfo->cbStoreId = (strlen(g_szStoreFile) + 1) * sizeof(TCHAR); // ActiveSync calls GetStoreInfo twice. Once to // get the size of the store id (when lpbStoreId is // NULL), and a second time, providing a buffer pointed // to by lpbStoreId where the store id can be placed. if(pStoreInfo->lpbStoreId == NULL) return NOERROR; memcpy(pStoreInfo->lpbStoreId, g_szStoreFile, (strlen(g_szStoreFile) + 1) * sizeof(TCHAR)); return NOERROR; } The function GetStoreInfo is called twice by ActiveSync, the first time to determine the size of the buffer required to hold the store's unique id (cbStoreId), and the second time to copy the store id into a buffer (lpbStoreId). Table 17.4 describes the STOREINFO members used by this implementation of GetStoreInfo.
The function CompareStoreIDs is called by ActiveSync to determine if two store ids are actually the same. Listing 17.13 compares cbID1 and cbID2 to determine whether the number of bytes in the store ids are the same and, if they are, uses memcmp to perform a byte-wise comparison of the two strings. The function returns 0 if the lpbID1 and lpbID2 are ids that refer to the same store. Listing 17.13 IReplStore::CompareStoreIDs implementationSTDMETHODIMP_(int) CActiveSyncEg::CompareStoreIDs (LPBYTE lpbID1, UINT cbID1, LPBYTE lpbID2, UINT cbID2) { if(cbID1 cbID2) // first store is smaller than the second store return -1; if(cbID1 > cbID2) // first store is larger than the second store return 1; // now compare the store ids byte by byte. return memcmp(lpbID1, lpbID2, cbID1); } Folder Information and ManipulationA store can contain one or more folders in which items are placed. ActiveSync calls GetFolderInfo for each object type (for example, "AsyncSample") configured in the registry for the provider. The implementation of GetFolderInfo returns a pointer to the IReplObjHandler interface associated with this folder (the m_DataHandler member is a CDataHandler class object that implements IReplObjHandler) and to a HREPLFLD object (Listing 17.14). Listing 17.14 IReplStore:: GetFolderInfo implementationSTDMETHODIMP CActiveSyncEg::GetFolderInfo(LPSTR lpszObjType, HREPLFLD *phFld, IUnknown ** ppObjHandler) { LPREPLOBJECT pFolder = (LPREPLOBJECT) *phFld; if(pFolder == NULL) // new folder required { pFolder = new REPLOBJECT; } pFolder->uType = RT_FOLDER; pFolder->fChanged = TRUE; *phFld = (HREPLFLD)pFolder; // CDataHandler member m_DataHandler // implements IReplObjHandler *ppObjHandler = &m_DataHandler; return NOERROR; } In the example, HREPLFLD is actually a pointer to a REPLOBJECT. The HREPLFLD parameter can be NULL, in which case a new REPLOBJECT is created, or, if not NULL, the existing REPLOBJECT is used. The function IsFolderChanged is called by ActiveSync to determine whether items in a folder need to be synchronized. With continuous synchronization this function is called frequently, but with manual synchronization it is only called when synchronization starts. In Listing 17.15 the function always sets pfChanged to TRUE, indicating that the folder needs to be synchronized. Listing 17.15 IReplStore:: IsFolderChanged implementationSTDMETHODIMP CActiveSyncEg::IsFolderChanged(HREPLFLD hFld, BOOL *pfChanged ) { *pfChanged = TRUE; return NOERROR; } Iterate Items in a FolderActiveSync requests the provider to iterate through the items in a folder by calling the functions FindFirstItem, FindNextItem, and FindItemClose. The provider reads the first item (FindFirstItem) or the next item (FindNextItem) from the store and creates a HREPLITEM for each item. In the example, the functions GetFirstNote or GetNextNote (located in db. cpp) read the items, and a HREPLITEM is created represented by the REPLOBJECT structure (Listing 17.16). The three REPLOBJECT members are initialized with appropriate values read from the store. The HREPLITEM item is returned through the phItem parameter. Listing 17.16 IReplStore:: FindFirstItem and FindNextItem implementations// Returns an HREPLITEM structure for the first item in // the .DAT file. The data in HREPITEM is the OriginalTime // (the unique identifier) and ModifyTime // (to determine if the item has changed); STDMETHODIMP CActiveSyncEg:: FindFirstItem(HREPLFLD hFld, HREPLITEM *phItem, BOOL *pfExist ) { WCHAR szNote[STRLEN_NOTE]; FILETIME ftCreateTime, ftModifyTime; // attempt to get first record *pfExist = m_ListDB.GetFirstNote(&ftCreateTime, szNote, &ftModifyTime); if(!*pfExist) return NOERROR; // now make up the HREPLFLD LPREPLOBJECT lpRepl = new REPLOBJECT; lpRepl->uType = RT_ITEM; lpRepl->ftCreated = ftCreateTime; lpRepl->ftModified = ftModifyTime; // set our pointer into HREPLITEM *phItem = (HREPLITEM)lpRepl; return NOERROR; } // Find the next item from the .DAT file STDMETHODIMP CActiveSyncEg::FindNextItem(HREPLFLD hFld, HREPLITEM *phItem, BOOL *pfExist ) { WCHAR szNote[STRLEN_NOTE]; FILETIME ftCreateTime, ftModifyTime; // attempt to get first record *pfExist = m_ListDB.GetNextNote(&ftCreateTime, szNote, &ftModifyTime); if(!*pfExist) { return NOERROR; } // now make up the HREPLFLD LPREPLOBJECT lpRepl = new REPLOBJECT; lpRepl->uType = RT_ITEM; lpRepl->ftCreated = ftCreateTime; lpRepl->ftModified = ftModifyTime; // set our pointer into HREPLITEM *phItem = (HREPLITEM)lpRepl; return NOERROR; } FindFirstItem and FindNextItem set the pfExist BOOL parameter to TRUE if a HREPLITEM is returned, or FALSE if no more items exist. FindItemClose is called when pfExist is set to FALSE (Listing 17.17). Listing 17.17 IReplStore:: FindItemClose implementation// Finished going through all records. // Nothing to do in this case. STDMETHODIMP CActiveSyncEg::FindItemClose(HREPLFLD hFld ) { return NOERROR; } Manipulating HREPLITEM and HREPLFLD ObjectsThe provider must implement functions that allow ActiveSync to manipulate HREPLITEM and HREPLFLD objects. Table 17.5 shows the functions that must be implemented. Many of these functions operate on HREPLITEM or HREPLFLD objects, and the functions may need to take different actions depending on which is passed. Remember that the generic type HREPLOBJ is used to refer to both HREPLITEM and HREPLFLD objects.
Using the structure/union REPLOBJECT to store both HREPLITEM and HREPLFLD objects greatly simplifies the coding of these functions. ActiveSync calls ObjectToBytes and BytesToObject when writing and reading objects to and from the file repl.dat. ObjectToBytes (Listing 17.18) will be called twice for each conversion. In the first call, ObjectToBytes simply returns the number of bytes required to write the object. In the second call ActiveSync provides a buffer of the correct length into which the copy is made. Listing 17.18 IReplStore:: ObjectToBytes implementationSTDMETHODIMP_(UINT) CActiveSyncEg::ObjectToBytes (HREPLOBJ hObject, LPBYTE lpb ) { // buffer has been created to requested size if(lpb != NULL) memcpy(lpb, (LPREPLOBJECT)hObject, sizeof(REPLOBJECT)); return sizeof(REPLOBJECT); } Both HREPLITEM and HREPLFLD objects are passed into ObjectToBytes, but because REPLOBJECT is used for both folders and items, the same code can be used for both. BytesToObject (Listing 17.19) creates a new REPLOBJECT, copies from the stream of bytes into this new structure, and returns a pointer cast to a HREPLOBJ. Listing 17.19 IReplStore:: BytesToObject implementationSTDMETHODIMP_(HREPLOBJ) CActiveSyncEg::BytesToObject (LPBYTE lpb, UINT cb ) { if(cb != sizeof(REPLOBJECT)) MessageBox(NULL, _T("Not correct size in Bytes to object"), NULL,0); LPREPLOBJECT lpReplObject = new REPLOBJECT; // perform the copy memcpy(lpReplObject, lpb, cb); return (HREPLOBJ)lpReplObject; } ActiveSync calls FreeObject (Listing 17.20); the implementation should free any memory associated with the HREPLOBJ object. Listing 17.20 IReplStore:: FreeObject implementationSTDMETHODIMP_(void) CActiveSyncEg::FreeObject( HREPLOBJ hObject ) { LPREPLOBJECT pItem = (LPREPLOBJECT)hObject; delete (LPREPLOBJECT) hObject; } From time to time, ActiveSync needs to copy HREPLOBJ objects. The function CopyObject does this by copying a REPLOBJECT from one location to another (Listing 17.21). Listing 17.21 IReplStore:: CopyObject implementationSTDMETHODIMP_(BOOL) CActiveSyncEg::CopyObject( HREPLOBJ hObjSrc, HREPLOBJ hObjDest) { LPREPLOBJECT lpRepObjSrc = (LPREPLOBJECT)hObjSrc; LPREPLOBJECT lpRepObjDest = (LPREPLOBJECT)hObjDest; *lpRepObjDest = *lpRepObjSrc; return TRUE; } Finally, IsValidObject is called when ActiveSync needs to determine whether the HREPOBJ still refers to a valid item or folder. For folders, Listing 17.22 simply checks that the REPLOBJECT uType is RT_FOLDER, returning NOERROR if it is, or RERR_CORRUPT to indicate the object is no longer valid. Listing 17.22 IReplStore:: IsValidObject implementationSTDMETHODIMP CActiveSyncEg::IsValidObject( HREPLFLD hFld, HREPLITEM hItem, UINT uFlags ) { LPREPLOBJECT lpRepObj; if(hFld != NULL) { lpRepObj = (LPREPLOBJECT)hFld; if(lpRepObj->uType == RT_FOLDER) return NOERROR; else return RERR_CORRUPT; } if(hItem!= NULL) { lpRepObj = (LPREPLOBJECT)hItem; if(lpRepObj->uType != RT_ITEM) return RERR_CORRUPT; NOTE aNote; // attempt to find the item if(m_ListDB.FindNote(&lpRepObj->ftCreated, &aNote)) return NOERROR; else return RERR_OBJECT_DELETED; } return NOERROR; } For items, a check needs to be made that uType is RT_ITEM. Additionally, the function needs to check that the item referred to by the HREPLFLD is still present in the store. Calling FindNote (implemented in db.cpp) does this. HREPLITEM SynchronizationHREPLITEM objects are maintained to allow ActiveSync to track changes to objects either on the desktop or the Windows CE device. The provider must implement the functions listed in Table 17.6.
In general, at least two pieces of information are held in a HREPLITEM to enable these functions:
CompareItem is called when ActiveSync needs to know whether two HREPLITEMs refer to the same item in the store. This would be called, for example, when an item from the Windows CE device is updated and ActiveSync needs to find the corresponding item in the store. CompareItem (Listing 17.23) simply passes the ftCreated members to CompareFileTime, which returns 0 if the FILETIMEs are the same, 21 if the first is earlier, or 1 if the first is later. Listing 17.23 IReplStore:: CompareItem implementationSTDMETHODIMP_(int) CActiveSyncEg::CompareItem( HREPLITEM hItem1, HREPLITEM hItem2 ) { LPREPLOBJECT lpRepObj1 = (LPREPLOBJECT)hItem1; LPREPLOBJECT lpRepObj2 = (LPREPLOBJECT)hItem2; int nRet = CompareFileTime(&lpRepObj1->ftCreated, &lpRepObj2->ftCreated); return nRet; } HREPLITEM maintains a modification timestamp that may be different from the item in the database. For example, the database item may have changed since the time the HREPLITEM was created. ActiveSync calls IsItemChanged to determine if this is the case (Listing 17.24). IsItemChanged passes in two HREPLITEM objects. If both are non-NULL, the function compares the two ftModified members in the REPLOBJECT structures pointed to by hItem and hItemComp. If hItemComp is NULL, IsItemChanged compares the ftModified for hItem with the modification time of the item in the database. This is obtained by finding the item in the database using FindNote (db.cpp). Listing 17.24 IReplStore:: IsItemChanged implementationSTDMETHODIMP_(BOOL) CActiveSyncEg::IsItemChanged( HREPLFLD hFld, HREPLITEM hItem, HREPLITEM hItemComp ) { LPREPLOBJECT lpRepObj1 = (LPREPLOBJECT)hItem; if(hItemComp != NULL) { LPREPLOBJECT lpRepObj2 = (LPREPLOBJECT)hItemComp; return CompareFileTime( &lpRepObj1->ftModified, &lpRepObj2->ftModified); } else { // need to compare this object with the // one in the .DAT file NOTE aNote; if(m_ListDB.FindNote( &lpRepObj1->ftCreated, &aNote)) return CompareFileTime( &lpRepObj1->ftModified, &aNote.ftLastUpdate); else { MessageBox(NULL, _T("Could not find record for \ IsItemChanged"), NULL, 0); return FALSE; } } } Generally, all items in the store are synchronized. However, there are times when you may want to filter the items being synchronized for example, you may only want to filter appointments from the last two weeks. The function IsItemReplicated is passed a HREPLITEM, and returns TRUE if the item is to be synchronized (Listing 17.25). Listing 17.25 IReplStore:: IsItemReplicated implementationSTDMETHODIMP_(BOOL) CActiveSyncEg::IsItemReplicated( HREPLFLD hFld, HREPLITEM hItem ) { return TRUE; } Implementing the Desktop IReplObjHandler COM InterfaceThe desktop application must implement the same IReplObjHandler interface functions as the device, but the implementations will typically be different. In the example the class CDataHandler in the file ReplObjHandler.cpp implements the IReplObjHandler interface. IReplObjHandler:: SetupActiveSync calls this function before any item is received or sent. This provides an opportunity to perform initialization. A pointer to a REPLSETUP structure is passed in; this provides information such as the direction of the transfer. The Setup function will normally save a pointer to the REPLSETUP structure for future use. Since ActiveSync is multithreaded, it is possible that a read (outgoing transfer) occurs at the same time as a write (incoming transfer). Therefore, the IReplObjHandler class has two members, m_pReadSetup and m_p- WriteSetup to store separate pointers (Listing 17.26). The pointer will be used later in GetPacket and SetPacket. Listing 17.26 IReplObjHandler:: Setup implementationSTDMETHODIMP CDataHandler::Setup(PREPLSETUP pSetup) { // Can be reading and writing at the same time, // so need two setups if(pSetup->fRead) m_pReadSetup = pSetup; else m_pWriteSetup = pSetup; return NOERROR; } IReplObjHandler:: ResetThe Reset function provides an opportunity to free any resources created during the serialization or deserialization. In this case, there is nothing to do (Listing 17.27). Listing 17.27 IReplObjHandler:: Reset implementationSTDMETHODIMP CDataHandler::Reset(PREPLSETUP pSetup) { return NOERROR; // no resources to be freed } IReplObjHandler::GetPacketActiveSync calls the function IReplObjHandler::GetPacket to request a packet for a particular item being synchronized. Your implementation should produce a byte stream representing the entire item (if it fits into a single packet) or the next packet in sequence. The function passes in the recommended maximum size of the packet in cbRecommend. Listing 17.28 shows the implementation of GetPacket. The function calls FindNote (located in ListDB.cpp) to read the record for the timestamp of when the item was created (lpRepObj->ftCreated, which is the unique identifier for the note). The function serializes the record into a NOTE structure pointed to by pNote, returns a pointer to the structure in lpByte, and returns the size of the NOTE structure in dwLen. The pointer and size are returned to ActiveSync through the parameters lppbData and pcbData. The function returns RWRN_LAST_PACKET, indicating this is the one and only packet for thisitem. Listing 17.28 IReplObjHandler:: GetPacket implementationSTDMETHODIMP CDataHandler::GetPacket(LPBYTE *lppbData, DWORD *pcbData, DWORD cbRecommend) { NOTE * pNote = new NOTE; LPREPLOBJECT lpRepObj = (LPREPLOBJECT)m_pReadSetup->hItem; if(m_pReadSetup->hItem == NULL) return E_UNEXPECTED; // locate the note in the file if(!m_pListDB->FindNote(&lpRepObj->ftCreated, pNote)) { MessageBox(NULL, _T("GetPacket: Could not find record"), NULL, MB_OK); return RERR_BAD_OBJECT; } else { *lppbData = (LPBYTE)pNote; *pcbData = sizeof(NOTE); } return RWRN_LAST_PACKET; } IReplObjHandler::SetPacketSetPacket does the opposite of GetPacket it is passed a pointer to a stream of bytes and writes the data to a new or existing record in the data file. The REPLSETUP structure member dwFlags contains the value RSF_NEW_OBJECT if this item is a new record; otherwise, an existing record is to be updated. In Listing 17.29, SetPacket casts the incoming lpbData pointer to a NOTE pointer. If HREPLITEM is non-NULL, the function FindNote (in ListDB.CPP) is used to locate the item (since it already exists) and then calls UpdateNote to update the new information for the item. For a new object (when HREPLITEM is NULL), a new note is added to the desktop database using the function AddNote. Listing 17.29 IReplObjHandler:: SetPacket implementationSTDMETHODIMP CDataHandler::SetPacket(LPBYTE lpbData, DWORD cbData) { NOTE* lpNote = (NOTE*)lpbData; LPREPLOBJECT lpRepl = new REPLOBJECT; // Have a HREPLITEM must be an existing record if(m_pWriteSetup->hItem != NULL) { if(!m_pListDB->FindNote(&lpNote->ftOriginal)) { MessageBox(NULL, _T("Could not find existing note"), NULL, MB_OK); return E_UNEXPECTED; } // update record m_pListDB->UpdateNote(&lpNote->ftOriginal, lpNote->szNote); } else { // add record m_pListDB->AddNote(&lpNote->ftOriginal, &lpNote->ftLastUpdate, lpNote->szNote); } lpRepl->uType = RT_ITEM; lpRepl->ftModified = lpNote->ftLastUpdate; lpRepl->ftCreated = lpNote->ftOriginal; m_pWriteSetup->hItem = (HREPLITEM)lpRepl; return NOERROR; } IReplObjHandler::DeleteObjDeleteObj is called when an item needs to be deleted from the database. The function is passed a REPLSETUP pointer as a parameter and calls DeleteNote (located in ListDB.CPP) to delete the record (Listing 17.30). Listing 17.30 IReplObjHandler:: DeleteObj implementationSTDMETHODIMP CDataHandler::DeleteObj(PREPLSETUP pSetup) { LPREPLOBJECT lpRepObj = (LPREPLOBJECT)pSetup->hItem; if(!m_pListDB->DeleteNote(&lpRepObj->ftCreated)) MessageBox(NULL, _T("Could not delete record"), NULL, 0); return NOERROR; }
|