Understanding Mobile Data Synchronization: Creating Custom File Filters

Custom File Filters in a Nutshell

In the previous article, "Understanding Mobile Data Synchronization: Utilizing MS ActiveSync Capabilities at a High Level," you learned how to utilize simple but useful MS ActiveSync things in your desktop applications that want to interact with your PDA. Now, you will investigate one more area still related to the PC side—Custom File Filters.

Let me briefly explain what I will talk about. When you want to copy files from the desktop to a handheld device and vice versa, you may be required to perform some data conversion; for example, from desktop Word's format to Pocket Word's format. The same is true for all other types of Office documents such as Access databases (.mdb/.cdb files) or Excel files (.xls/.pxl), fonts (.fon), and so forth. You may want to handle your data in similar way. Besides, you even might have a desire/requirement to make such a conversion on the desktop and then send your data to you PDA. File Filters will serve to your needs here!

So what is a File Filter? It's just a standard desktop COM Server. The detailed answer is located in the replfilt.h header file. If you take a look, you find quite a few interfaces there to make the job easier for you:

  • ICeFileFilterSite
  • ICeFileFilter
  • ICeFileFilterOptions

You have to implement at least the ICeFileFilter interface as a normal COM object with all its related stuff. Once your filter is properly registered, MS ActiveSync then call will it upon all copy/move operations on your data files.

Note: File Filter won't be called if you transfer the data any way other than via the Mobile Device folder.

As you might guess already, ActiveSync has no other way to know about your file filter than the Registry. Your COM server, which implements the ICeFileFilter interface, must be registered as a regular COM object with some additional stuff. This stuff includes a couple of keys under HKEY_CLASSES_ROO\CLSID\YourFilterCLSID, file types registration, and appropriate Windows CE Services Registry entries. As a bottom line, to use your newly created file filter, you will have to perform a few magical passes over the desktop Registry using either CeUtil functions or the standard registry API. CeUtil functions will help you there because you may register file filter for already known devices as well as newly connected ones.

Implementing a Custom File Filter

Now, on to the real coding. Because File Filter is a regular COM Server, it has to have all the usual COM decorations. You can implement it your favorite way, with ATL or manually. You may even use C# if you want to. Just take care about the appropriate C# interface wrappers and method calling convention (stdcall), that's all. For simplicity, I will use C++ for all samples.

So, skipping the COM stuff, the ICeFileFilter implementation is a piece of cake. It has only one important method: NextConvertFile. This method is called by ActiveSync when it needs to perform file conversion. It is declared as follows:

HRESULT __stdcall NextConvertFile (int nConversion,
                  PFF_CONVERTINFO *pci,
                  PFF_SOURCEFILE *psf,
                  PFF_DESTINATIONFILE *pdf,
                  volatile BOOL *pbCancel, PF_ERROR *perr);

As you see, parameters contain an information about source and destination files, PFF_CONVERTINFO struct that has members describing how to perform the conversion; for example, pointer to ICeFileFilterSite, and so forth. ICeFileFilterSite is used to manage read/write operations over source and destination files and so on. For more details, please refer to the SDK documentation. Your sample implementation will simply translate ASCII data to UNICODE. The actual declaration and code are presented below:

class CDat2LogFileFilter : public ICeFileFilter,
                           public ICeFileFilterOptions
{
private:
   long m_lRef;
   UINT m_nCodePage;
   BOOL m_bShowOptions;

public:
   CDat2LogFileFilter();
   ~CDat2LogFileFilter();

   // IUnknown methods
   HRESULT __stdcall QueryInterface(REFIID riid, LPVOID *ppvObj);
   ULONG __stdcall AddRef();
   ULONG __stdcall Release();

   // ICeFileFilter methods
   HRESULT __stdcall NextConvertFile (int nConversion,
                     PFF_CONVERTINFO *pci, PFF_SOURCEFILE *psf,
                     PFF_DESTINATIONFILE *pdf,
                     volatile BOOL *pbCancel, PF_ERROR *perr);
   HRESULT __stdcall FilterOptions (THIS_ HWND hwndParent);
   HRESULT __stdcall FormatMessage (THIS_ DWORD dwFlags,
                                    DWORD dwMessageId,
                     DWORD dwLanguageId, LPTSTR lpBuffer,
                     DWORD dwSize, va_list *args, DWORD *pcb);

   // ICeFileFilterOptions methods
   HRESULT __stdcall SetFilterOptions(CFF_CONVERTOPTIONS* pco);
};
...
HRESULT __stdcall CDat2LogFileFilter::NextConvertFile (
                  int nConversion,
                  CFF_CONVERTINFO *pci,
                  CFF_SOURCEFILE *psf,
                  CFF_DESTINATIONFILE *pdf,
                  volatile BOOL *pbCancel, CF_ERROR *perr)
{
   IStream *pstreamSrc = NULL;
   IStream  *pstreamDest = NULL;
   ICeFileFilterSite *pffs = NULL;
   DWORD cBytesRemaining, cBytesRead;
   HRESULT hr = 0;
   int nToRead = 0;
   ULONG ulTotalMoved = 0;
   BYTE pBuff[BUFFSIZE];
   WCHAR pWBuff[BUFFSIZE*2+2];
   WORD wUnicodeSig = 0xFEFF;
   BOOL bSrcIsUnicode = FALSE;

   // return if we're called not the very first time
   if (nConversion > 0)
      return HRESULT_FROM_WIN32(ERROR_NO_MORE_ITEMS);

   if ( m_bShowOptions )
      FilterOptions(pci->hwndParent);

   ZeroMemory(pBuff,sizeof(pBuff));
   ZeroMemory(pWBuff,sizeof(pWBuff));

   // Get pointer to FileFilterSite interface.
   pffs = pci->pffs;

   // Open source file.
   hr = pffs->OpenSourceFile(PF_OPENFLAT, (PVOID *)&pstreamSrc);
   if (!SUCCEEDED (hr))
   {
      *perr = HRESULT_TO_PFERROR (hr, ERROR_ACCESS_DENIED);
      return E_FAIL;
   }

   // Open destination file.
   hr = pffs->OpenDestinationFile(PF_OPENFLAT, pdf->szFullpath,
                                  (PVOID *)&pstreamDest);
   if (!SUCCEEDED (hr))
   {
      pffs->CloseSourceFile (pstreamSrc);
      *perr = HRESULT_TO_PFERROR (hr, ERROR_ACCESS_DENIED);
      return E_FAIL;
   }

   cBytesRemaining = psf->cbSize;
   if ( pci->bImport )
   {
      hr = pstreamDest->Write ((PBYTE)&wUnicodeSig, 2, NULL);
      if (!SUCCEEDED (hr))
      {
         pffs->CloseSourceFile (pstreamSrc);
         pffs->CloseDestinationFile (TRUE, pstreamDest);
         *perr = HRESULT_TO_PFERROR (hr, ERROR_ACCESS_DENIED);
         return E_FAIL;
      }
   }


   // Convert & Copy data.
   for (; cBytesRemaining > 0; )
   {
      nToRead = min (BUFFSIZE, cBytesRemaining);


      hr = pstreamSrc->Read (pBuff, nToRead, &cBytesRead);
      if (cBytesRead == 0)
         break;

      if (*pbCancel)
      {
         hr = ERROR_CANCELLED;
         break;
      }

      int nLen = MultiByteToWideChar(m_nCodePage,0,(char*)pBuff,
                                     cBytesRead,0,0);
      MultiByteToWideChar(m_nCodePage,0,(char*)pBuff,cBytesRead,
                          pWBuff,nLen);

      hr = pstreamDest->Write ((PBYTE)pWBuff, nLen*2, NULL);
      if (!SUCCEEDED (hr))
         break;

      ulTotalMoved += cBytesRead;
      cBytesRemaining -= cBytesRead;
      pffs->ReportProgress (ulTotalMoved/psf->cbSize * 100);
   }

   // Perform some cleanup
   pffs->CloseSourceFile (pstreamSrc);
   pffs->CloseDestinationFile (TRUE, pstreamDest);

   if (hr == ERROR_CANCELLED)
      return HRESULT_FROM_WIN32 (ERROR_CANCELLED);

   if (!SUCCEEDED (hr))
   {
      *perr = hr;
      return E_FAIL;
   }

   return HRESULT_FROM_WIN32(ERROR_NO_MORE_ITEMS);
}

Understanding Mobile Data Synchronization: Creating Custom File Filters

This code sample follows the standard procedure:

  • Create source and destination streams via an ICeFileFilterSite instance passed in the pci->pffs parameter.
  • Subsequently read and write data.
  • Perform the desired conversion.
  • Report about operation's progress.
  • Close all streams.
  • Return ERROR_NO_MORE_ITEMS to indicate that conversion is completed.

Your filter may support one other interface, ICeFileFilterOptions. In this case, MS ActiveSync will be able to inform your filter whether or not to show the options dialog during file conversion. All this will cost you one additional simple method, SetFilterOptions, and HasOptions in the Registry:

HRESULT __stdcall CDat2LogFileFilter::SetFilterOptions
                  (CFF_CONVERTOPTIONS* pco)
{
   m_bShowOptions = pco->bNoModalUI;
   return S_OK;
}

After doing all this, your filter will be able to behave properly. I leave all additional details behind the scene, so you can take a closer look at the accompanying sample for all other details. For those of you who are mad about C#, let me drop a schematic C# class wrapping its C++ buddy (don't curse me for possible errors); I just like to illustrate the possibility) here:

using System;
using System.Runtime.InteropServices;
using System.Text;
using System.ComponentModel;

namespace CSHFilter
{
   [ComImport(), InterfaceType(ComInterfaceType.InterfaceIsIUnknown),
                 GuidAttribute("6C5C05E1-97A2-11cf-8011-00A0C90A8F78")]
   public interface ICeFileFilterSite
   {
      uint OpenSourceFile(int nHowToOpenFile, ref UCOMIStream ppObj);
      uint OpenDestinationFile(int nHowToOpenFile, byte[] pszFullpath,
                               ref UCOMIStream ppObj);
      uint CloseSourceFile(ref UCOMIStream pObj);
      uint CloseDestinationFile(bool bKeepFile,ref UCOMIStream pObj);
      uint ReportProgress(uint nPercent);
      uint ReportLoss(uint dw, StringBuilder psz, System.ArgIterator args);
   };

   [StructLayout(LayoutKind.Sequential)]
   public struct csCFF_CONVERTINFO
   {
      public bool bImport;
      public uint hwndParent;
      public uint bYesToAll;
      public ICeFileFilterSite pffs;
   };

   [StructLayout(LayoutKind.Sequential)]
   public struct csCFF_DESTINATIONFILE
   {
      public byte[] szFullpath;
      public byte[] szPath;
      public byte[] szFilename;
      public byte[] szExtension;
   };

   [StructLayout(LayoutKind.Sequential)]
   public struct csCFF_SOURCEFILE
   {
      public byte[] szFullpath;
      public byte[] szPath;
      public byte[] szFilename;
      public byte[] szExtension;
      public uint cbSize;
      public double ftCreated;
      public double ftModified;
   };

   /// <summary>
   /// Summary description for Class1.
   /// </summary>
   [ComImport(), InterfaceType(ComInterfaceType.InterfaceIsIUnknown),
                 GuidAttribute("418C5FF2-DD1B-45d1-A160-A1A954832713")]
   public interface ICeFilter
   {
      [PreserveSig()]
      uint NextConvertFile(int nConversion,
                           ref csCFF_CONVERTINFO pci,
                           ref csCFF_SOURCEFILE psf,
                           ref csCFF_DESTINATIONFILE pdf,
                           ref int pbCancel,
                           ref long perr);
      [PreserveSig()]
      uint FilterOptions(uint hwndParent);
      [PreserveSig()]
      uint FormatMessage(uint  dwFlags,
                         uint  dwMessageId,
                         uint  dwLanguageId,
                         [MarshalAs(UnmanagedType.LPStr)]
                         String  lpBuffer,
                         uint  nSize,
                         System.ArgIterator Arguments,
                         ref uint  pcb);
   }

   // Class ComClass1
   [Guid("418C5FF2-DD1B-45d1-A160-A1A954832713"),
   ClassInterface(ClassInterfaceType.None),
   ComVisible(true)]
   public unsafe class CCeFilter : ICeFilter
   {
      public const int ERROR_NO_MORE_ITEMS = 259;
      public const int PF_OPENFLAT = 0;
      public const int PF_OPENCOMPOUND = 1;
      public const uint E_FAIL = 2417500037;
      public const uint ERROR_ACCESS_DENIED = 5;
      public const uint ERROR_CANCELLED = 1223;

      public uint HRESULT_FROM_WIN32(uint x)
      {
         if ( x <= 0 )
         {
            return x;
         }
         else
         {
            uint y = (x & 0x0000FFFF) | (7 << 16) | 0x80000000;
            return y;
         }
      }
      uint HRESULT_TO_CFERROR(uint hr, uint def)
      {
         if ( hr == 0 )
            return 0;
         else
         {
            if ( (((hr) >> 16) & 0x1fff) == 7 )
               return ((hr) & 0xFFFF);
            else
               return def;
         }
      }

      [DllImport("kernel32.dll", EntryPoint = "FormatMessageA",
                 CharSet=CharSet.Ansi,SetLastError=true)]
      static extern uint FormatMessageA(
            uint dwFlags, uint lpSource,
            uint dwMessageId, uint dwLanguageId,
            [MarshalAs(UnmanagedType.LPStr)] String lpBuffer,
            uint nSize,
            System.ArgIterator Arguments);

      public uint NextConvertFile(int nConversion,
         ref csCFF_CONVERTINFO pci,
         ref csCFF_SOURCEFILE psf,
         ref csCFF_DESTINATIONFILE pdf,
         ref int pbCancel,
         ref long perr)
      {
         UCOMIStream pstreamSrc = null;
         UCOMIStream  pstreamDest = null;
         ICeFileFilterSite pffs;
         uint cBytesRemaining = 0, cBytesRead = 0, cBytesWritten = 0;
         uint hr = 0;
         int nToRead = 0;
         uint ulTotalMoved = 0;
         byte [] pBuff = new byte[4096];
         ushort usUnicodeSig = 0xFEFF;

         // return if we're called not the very first time
         if (nConversion > 0)
            return HRESULT_FROM_WIN32(ERROR_NO_MORE_ITEMS);

         //if ( m_bShowOptions )
         FilterOptions(pci.hwndParent);

         pBuff.Initialize();
         // Get pointer to FileFilterSite interface.
         pffs = pci.pffs;

         // Open source file.
         hr = pffs.OpenSourceFile(PF_OPENFLAT, ref pstreamSrc);
         if ( 0 != hr )
         {
            perr = HRESULT_TO_CFERROR (hr, ERROR_ACCESS_DENIED);
            return E_FAIL;
         }

         // Open destination file.
         hr = pffs.OpenDestinationFile(PF_OPENFLAT, pdf.szFullpath,
                                       ref pstreamDest);
         if ( 0 != hr )
         {
            pffs.CloseSourceFile (ref pstreamSrc);
            perr = HRESULT_TO_CFERROR (hr, ERROR_ACCESS_DENIED);
            return E_FAIL;
         }

         cBytesRemaining = psf.cbSize;
         if ( pci.bImport )
         {
            pstreamDest.Write(BitConverter.GetBytes(usUnicodeSig),
                              2, (IntPtr)cBytesWritten);
            if ( cBytesWritten != 2 )
            {
               pffs.CloseSourceFile (ref pstreamSrc);
               pffs.CloseDestinationFile (true,  ref pstreamDest);
               perr = HRESULT_TO_CFERROR (hr, ERROR_ACCESS_DENIED);
               return E_FAIL;
            }
         }


         // Convert & Copy data.
         for (; cBytesRemaining > 0; )
         {
            if ( cBytesRemaining > 4096 )
               nToRead = 4096;
            else
               nToRead = (int)cBytesRemaining;


            pstreamSrc.Read (pBuff, nToRead, (IntPtr)(cBytesRead));
            if (cBytesRead == 0)
               break;

            if (pbCancel == 1)
            {
               hr = ERROR_CANCELLED;
               break;
            }

            // Create two different encodings.
            Encoding ascii = Encoding.ASCII;
            Encoding unicode = Encoding.Unicode;

            // Convert the string into a byte[].
            char[] chars = new char[cBytesRead];
            pBuff.CopyTo(chars,0);
            byte[] asciiBytes = ascii.GetBytes(chars,0,(int)cBytesRead);

            // Perform the conversion from one encoding to the other.
            byte[] unicodeBytes = Encoding.Convert(ascii, unicode,
                                                   asciiBytes);

            pstreamDest.Write (unicodeBytes, unicodeBytes.Length,
                               (IntPtr)cBytesWritten);
            if ( cBytesWritten == 0 )
               break;

            ulTotalMoved += cBytesRead;
            cBytesRemaining -= cBytesRead;
            pffs.ReportProgress (ulTotalMoved/psf.cbSize * 100);
         }

         // Perform some cleanup
         pffs.CloseSourceFile (ref pstreamSrc);
         pffs.CloseDestinationFile (true, ref pstreamDest);

         if (hr == ERROR_CANCELLED)
            return HRESULT_FROM_WIN32 (ERROR_CANCELLED);

         if ( 0 != hr)
         {
            perr = hr;
            return E_FAIL;
         }
         return 0;
      }
      public uint FilterOptions(uint hwndParent)
      {
         return 0;
      }
      public uint FormatMessage(uint  dwFlags,
                                uint  dwMessageId,
                                uint  dwLanguageId,
                                [MarshalAs(UnmanagedType.LPStr)]
                                String  lpBuffer,
                                uint  nSize,
                                System.ArgIterator Arguments,
                                ref uint  pcb)
      {
         pcb = FormatMessageA (
               dwFlags,
               0, dwMessageId, dwLanguageId,
               lpBuffer, nSize, Arguments);
               if (pcb == 0)
            return 0x80004005;

         return 0;
      }
   }
}

Registering the File Filter

Well, you have successfully developed your great file filter and you can be proud of it. The only thing left is to kindly inform MS ActiveSync that the new filter exists and how to use it. You have several options about to do it:

  • Create a separate Install/Uninstall application.
  • Implement and export DllRegisterServer/DllUnregisterServer functions inside your file filter.
  • Create a REG file with all required information.

The first two options give you more power because you will be able to enumerate all existing device profiles and set up your filter there as well as for future connections. The REG file will do only for future connections. Let me place it here in case you're interested:

REGEDIT4

; Register COM Object itself
[HKEY_CLASSES_ROOT\CLSID\{FDF0CFF3-48B4-458e-BD51-ED0C4CCBA4F1}]
@="Developer.com Sample Filter"

[HKEY_CLASSES_ROOT\CLSID\{FDF0CFF3-48B4-458e-BD51-ED0C4CCBA4F1}
                  \DefaultIcon]
@="Dat2Log.dll,-100"

[HKEY_CLASSES_ROOT\CLSID\{FDF0CFF3-48B4-458e-BD51-ED0C4CCBA4F1}
                  \InProcServer32]
@="Dat2Log.dll"
"ThreadingModel"="Apartment"

[HKEY_CLASSES_ROOT\CLSID\{FDF0CFF3-48B4-458e-BD51-ED0C4CCBA4F1}
                  \PegasusFilter]
"Import"=""
"Description"="Copy DAT file with A2W conversion"
"NewExtension"="log"
"HasOptions"=""

; DAT file stuff
[HKEY_CLASSES_ROOT\.dat]
@="datfile"

[HKEY_CLASSES_ROOT\datfile]
@="PC DAT File"

[HKEY_CLASSES_ROOT\datfile\DefaultIcon]
@="Dat2Log.dll,-100"

; LOG file stuff
[HKEY_CLASSES_ROOT\.log]
@="logfile"

[HKEY_CLASSES_ROOT\logfile]
@="PDA LOG File"

[HKEY_CLASSES_ROOT\logfile\DefaultIcon]
@="Dat2Log.dll,-101"

; Register Our Filter under WinCE Services root
[HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows CE Services\Filters\.dat]
"DefaultImport"="{FDF0CFF3-48B4-458e-BD51-ED0C4CCBA4F1}"
"DefaultExport"="Binary Copy"

[HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows CE Services\Filters
                   \.dat\InstalledFilters]
"{FDF0CFF3-48B4-458e-BD51-ED0C4CCBA4F1}"=""

As you see, this is just regular registration stuff. Thus, you may see the results of your effort in the following figure:

Now, if you try to copy a DAT file from the desktop to the PDA, it will be converted to the selected code page.

Using the File Filter Manually

To use any file filter from your application directly, you have to recreate ActiveSync flow; for example:

  1. Implement an ICeFileFilterSite interface.
  2. Create an instance of the file filter.
  3. Fill in all structs for the source and destination files.
  4. Call ICeFileFilter::NextConvertFile.
  5. Release all stuff.

You do not need to bother implementing a full COM object for ICeFileFilterSite. Simply inherit a class from this interface and use it later on. You will find such a sample object in the accompanying zip. As a final comment, I'll give you this tiny code snippet that performs manual file conversion:

int _tmain(int argc, TCHAR* argv[], TCHAR* envp[])
{
   int nRetCode = 0;

   // initialize MFC and print and error on failure
   if (!AfxWinInit(::GetModuleHandle(NULL), NULL,
       ::GetCommandLine(), 0))
   {
      // TODO: change error code to suit your needs
      cerr << _T("Fatal Error: MFC initialization failed") << endl;
      nRetCode = 1;
      return nRetCode;
   }

   CoInitialize(0);
   ICeFileFilter *pFilter = NULL;
   HRESULT hr = 0;
   hr = ::CoCreateInstance(CLSID_Dat2LogFileFilter,0,CLSCTX_SERVER,
                           IID_IUnknown,(void**)&pFilter);

   CCeFilterSite site;
   site.m_sSrcFile = "d:\\Sample.dat";

   CFF_CONVERTINFO convertInfo;

   convertInfo.bImport = TRUE;
   convertInfo.hwndParent = NULL;
   convertInfo.bYesToAll = FALSE;
   convertInfo.pffs = &site;

   CFF_SOURCEFILE sourceFile;
   _tcscpy(sourceFile.szFullpath, _T(""));

   CFF_DESTINATIONFILE destFile;
   _tcscpy(destFile.szFullpath, _T("d:\\Sample.log"));

   CF_ERROR error;
   BOOL bCancel = FALSE;
   BOOL bOK = FALSE;

   hr = pFilter->NextConvertFile(0, &convertInfo, &sourceFile,
                                 &destFile, &bCancel, &error);

   pFilter->Release();

   CoUninitialize();
   return nRetCode;
}

Conclusion

In this article, you have played around with one more MS ActiveSync supported feature. Along with the connection notifications discussed previously, it already gives you a wide elbowroom for powerful implementations. If all goes smoothly, the next articles will show you how to develop more complicated stuff, ActiveSync Service Provider components.

Download

Download the accompanying code's zip file here

About the Author

Alex Gusev started to play with mainframes at the end of the 1980s, using Pascal and REXX, but soon switched to C/C++ and Java on different platforms. When mobile PDAs seriously rose their heads in the IT market, Alex did it too. Now, he works at an international retail software company as a team leader of the Mobile R department, making programmers' lives in the mobile jungles a little bit simpler.



Downloads

Comments

  • There are no comments yet. Be the first to comment!

Leave a Comment
  • Your email address will not be published. All fields are required.

Top White Papers and Webcasts

  • Live Event Date: October 29, 2014 @ 11:00 a.m. ET / 8:00 a.m. PT Are you interested in building a cognitive application using the power of IBM Watson? Need a platform that provides speed and ease for rapidly deploying this application? Join Chris Madison, Watson Solution Architect, as he walks through the process of building a Watson powered application on IBM Bluemix. Chris will talk about the new Watson Services just released on IBM bluemix, but more importantly he will do a step by step cognitive …

  • Packaged application development teams frequently operate with limited testing environments due to time and labor constraints. By virtualizing the entire application stack, packaged application development teams can deliver business results faster, at higher quality, and with lower risk.

Most Popular Programming Stories

More for Developers

Latest Developer Headlines

RSS Feeds