ActiveSync on a Windows CE device is based around store objects such as files, directories, databases, and database records. As described in Chapter 4, each object has a unique object identifier or CEOID. Any object that has a CEOID can be an item synchronized by ActiveSync. Thus, you can synchronize files, directories, databases, or database records. You can use these objects to hold more than one item (for example a file might contain many records, each of which is an item), but you will need to manage lists of these items, and this gets more complex. The simplest approach is to represent an ActiveSync item by a single database record. Windows CE 2.1 and later versions allow databases to be created in volumes that can be located on, for example, storage cards. Additional functions (FindObjects and SyncData) are provided to synchronize databases in these volumes. Note that some storage cards and other media do not use the CEOID object identifiers for the file system, so they are more difficult to synchronize. Remember, the Windows CE ActiveSync DLL is not a COM component. However, the DLL does implement the interface IReplObjHandler. The DLL itself is responsible for creating an instance of this interface (usually through implementing the interface using a C++ class), and not any external application. Therefore, all the usual COM elements, such as class factories, DLLGetObject, and other exported functions, are not required. First, let's look at the functions the ActiveSync provider must export. InitObjType Exported FunctionInitObjType, located in ASDevice.cpp, is called by ActiveSync when the provider is started and terminated. This occurs when the Windows CE device connects or disconnects. This function carries out any initialization/termination required by the provider and returns a pointer to the IReplObjHandler interface. Listing 17.1 Implementation of InitObjTypeextern "C" BOOL _declspec(dllexport) InitObjType( LPWSTR lpszObjType, IReplObjHandler **ppObjHandler, UINT uPartnerBit) { if ( lpszObjType == NULL ) { // Terminates the device provider module and // frees all allocated resources. return TRUE; } // Allocate a new IReplObjHandler. *ppObjHandler = new CDataHandler; // Save the uPartnerBit so that you can use it later on g_uPartnerBit = uPartnerBit; // Find Object Identifier of our database g_oidDataBase = ASGetDBOID(DB_NAME); return TRUE; } In Listing 17.1 the C++ class CDataHandler implements the IReplObjHandler interface, so an instance of the class is created and a pointer returned through the parameter ppObjHandler. A Windows CE device can maintain synchronization with up to two desktop PCs. The uPartnerBit has a value 1 when synchronizing with the first partnership and 2 with the second. Maintaining two partnerships is more complex; you can choose to support only one partnership, as is the case with the EMail ActiveSync option. In this code the partnership bit is saved in the global variable g_uPartnerBit. Lastly, the CEOID of our database is stored in the global variable g_oidDatabase. The function ASGetDBOID is located in DB.CPP, together with the other database access code for the provider. ObjectNotify Exported FunctionWindows CE constantly monitors changes in the object store. When a change occurs (for example, a database record is updated), all loaded ActiveSync providers are notified of the change through a call to ObjectNotify. This function determines the nature of the change (whether it was a file, directory, database, or record change) and returns TRUE if it is an item this provider can synchronize. The provider should ensure that the record is in its own database. There is no point synchronizing someone else's database! This is done through the function ASRecInDB to be found in DB.CPP, which determines the parent CEOID for the database record by calling CEOidGetInfo, and checks that this is the same as the CEOID for our database. Listing 17.2 Implementation of ObjectNotifyextern "C" BOOL _declspec(dllexport) ObjectNotify( POBJNOTIFY pNotify) { // Check to see if the structure size // is the smaller (version control). if ( pNotify->cbStruct sizeof( OBJNOTIFY ) ) { MessageBox(NULL, _T("ObjectNotify incorrect version"), NULL, MB_OK); return FALSE; } // We're only interested in database record // changes or clear change notifications if(!(pNotify->uFlags & (ONF_RECORD | ONF_CLEAR_CHANGE))) return FALSE; // For non-deleted records, check that the // record is in our database if(!(pNotify->uFlags & ONF_DELETED)) { if(!(pNotify->uFlags & ONF_RECORD)) // it's not actually a record, so ignore return FALSE; if(!ASRecInDB(g_oidDataBase, pNotify->oidObject)) // not in our database return FALSE; } // sets the oid of the object to be replicated pNotify->poid = (UINT*) &pNotify->oidObject; // if object is to be deleted, set the // number of objects to be deleted if(pNotify->uFlags & ONF_DELETED) pNotify->cOidDel = 1; else pNotify->cOidChg = 1; return TRUE; } ObjectNotify is passed a pointer to an OBJNOTIFY structure. The members of this structure used in this sample are shown in Table 17.1.
GetObjTypeInfo Exported FunctionActiveSync on the Windows CE device calls this exported function when it needs information about the database being synchronized. The function fills in members of the OBJTYPEINFO structure, such as the following:
Listing 17.3 shows the implementation of GetObjTypeInfo from the file ASDevice.CPP. Listing 17.3 Implementation of GetObjTypeInfoextern "C" BOOL _declspec(dllexport) GetObjTypeInfo (POBJTYPEINFO pInfo) { CEOIDINFO oidInfo; // Check versioning of the structure if ( pInfo->cbStruct < sizeof( OBJTYPEINFO ) ) { MessageBox(NULL, _T("GetObjTypeInfo called wrong version"), NULL, MB_OK); return FALSE; } // Clear the structure. memset( &(oidInfo), 0, sizeof(oidInfo)); // Retrieves information about the object // in the object store. CeOidGetInfo( g_oidDataBase, &oidInfo ); // Store the database information into // the OBJTYPEINFO structure. wcscpy( pInfo->szName, oidInfo.infDatabase.szDbaseName ); pInfo->cObjects = oidInfo.infDatabase.wNumRecords; pInfo->cbAllObj = oidInfo.infDatabase.dwSize; pInfo->ftLastModified = oidInfo.infDatabase.ftLastModified; return TRUE; } Note that the version of the OBJTYPEINFO structure is checked. Information about the database is obtained through a call to the Windows CE function CeOidGetInfo, which fills in a CEOIDINFO structure. Implementing the Device IReplObjHandler COM InterfaceIReplObjHandler functions are responsible for converting your items (such as database records) into a stream of bytes (serialization) or converting a stream of bytes into an item (deserialization). Serialization and deserialization are required so that items can be transferred between Windows CE devices and desktop PCs. The desktop PC ActiveSync provider also needs to implement the IReplObjHandler and should serialize and deserialize items using the same data format. However, the implementations are typically different (since the item stores are not the same), so it is usually best to keep to separate code implementations. In the example, IReplObjHandler is implemented by the C++ class CDataHandler that is declared in ReplObjHandler.h and implemented in ReplObjHandler.cpp. Since IReplObjHandler is declared as a COM interface, IUKnown must be implemented. However, since we are only implementing a COM interface and not an entire COM component, these implementations are very straightforward. AddRef and Release simply increment and decrement a reference count. QueryInterfce always returns E_NOINTERFACE this function will never actually be called. An overview of the essential IReplObjHandler interface functions is provided in Table 17.2.
Serialization FormatYou will need to determine the format to be used for serialization and deserialization. GetPacket and SetPacket provide LPBYTE pointers, but this can be cast to any pointer you like. In the example, a typedef for a structure called NOTE is used, and this contains the creation FILETIME (the unique identifier), the last modify timestamp as a FILETIME, and a Unicode string (Listing 17.4). It is declared in db.h. Listing 17.4 Structure NOTEtypedef struct tagNOTE { FILETIME ftOriginal; // time when note was created FILETIME ftLastUpdate; // time last updated WCHAR szNote[STRLEN_NOTE]; } NOTE; Note that the last modify timestamp member is not used on the device, and so is not strictly required in this structure. However, this same structure is used on the desktop PC for storing data, so it is convenient to leave it here. It is essential that exactly the same format is used by the ActiveSync provider on the Windows CE device and the desktop PC. Note how the Unicode string has been declared as WCHAR and not TCHAR. This ensures that it is defined as a Unicode string even if the code is compiled for ANSI (which is most often the case on the desktop PC). IReplObjHandler::SetupThis function is called by ActiveSync before any item is received or sent. This provides an opportunity to perform any initialization. A pointer to a REPLSETUP structure is passed in, and 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.5). The pointer will be used later in GetPacket and SetPacket. Listing 17.5 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; } Most REPLSETUP members are only used on a desktop implementation of IReplObjHandler. Those listed below may be used on the Windows CE device.
IReplObjHandler::ResetThe Reset function provides an opportunity to free any resources created during serialization or deserialization. In this case, there is nothing to do (Listing 17.6). Listing 17.6 IReplObjHandler::Reset implementationSTDMETHODIMP CDataHandler::Reset(PREPLSETUP pSetup) { return NOERROR; // no resources to be freed } IReplObjHandler::GetPacketActiveSync calls this function 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.7 shows the implementation of GetPacket. The function calls ASSerializeRecord (located in db.cpp) to read the record for the given CEOID (m_pReadSetup->oid). The function serializes the record into a NOTE structure, 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. Listing 17.7 IReplObjHandler:: GetPacket implementationSTDMETHODIMP CDataHandler::GetPacket(LPBYTE *lppbData, DWORD *pcbData, DWORD cbRecommend) { HRESULT hr = RWRN_LAST_PACKET; LPBYTE lpByte; DWORD dwLen; if(!ASSerialiseRecord(m_pReadSetup->oid, &lpByte, &dwLen)) hr = RERR_BAD_OBJECT; else { *lppbData = lpByte; *pcbData = dwLen; } return hr; } GetPacket returns RWRN_LAST_PACKET if the serialization was successful and this is the last or only packet. RERR_BAD_OBJECT is returned if the object could not be serialized. 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 database. 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.8, SetPacket casts the incoming lpbData pointer to a NOTE pointer and calls ASDeserializeRecord (located in db.cpp) to perform the update. The REPLSETUP structure member oid contains the CEOID of the record to be updated, or 0 if this is a new record. Listing 17.8 IReplObjHandler:: SetPacket implementationSTDMETHODIMP CDataHandler::SetPacket(LPBYTE lpbData, DWORD cbData) { NOTE* aNote; CEOID oidNewRec; BOOL bNewRec; aNote = (NOTE*)lpbData; bNewRec = m_pWriteSetup->dwFlags & RSF_NEW_OBJECT; if((oidNewRec = ASDeserializeRecord(&aNote->ftOriginal, &aNote->ftLastUpdate, aNote->szNote, wcslen(aNote->szNote), bNewRec, m_pWriteSetup->oid)) == 0) return RERR_SKIP_ALL; else { m_pWriteSetup->oidNew = oidNewRec; return NOERROR; } } SetPacket returns RERR_SKIP_ALL if the update fails. This will cause all subsequent packets to be discarded. If successful, the CEOID of the new record is assigned to the oidNew member of REPLSETUP, and the function returns 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 ASDeleteRecord (located in DB.CPP) to delete the record (Listing 17.9). Listing 17.9 IReplObjHandler:: DeleteObj implementationSTDMETHODIMP CDataHandler::DeleteObj(PREPLSETUP pSetup) { if(ASDeleteRecord(pSetup->oid)) return NOERROR; else return E_UNEXPECTED; }
|