Welcome to MSDN Blogs Sign in | Join | Help

Crashes happen when the application closes unexpectedly. On Desktops you can leverage on many tools, starting with ADPlus + WinDbg. But this is not the case for us "tiny" programmers… some hints:

1. Start by reading "How the Windows Mobile 5.0 Shell Handles Low Memory Situations": note that the OS doesn't send WM_HIBERNATE nor WM_CLOSE to the foreground application: is your application the one with the focus?

2. How many processes are running at the time of the process' exit? If your process requires so much memory, then you should free the virtual memory associated to the DLLs loaded by other processes (which places the DLL Load Point down in the 32MB), by killing undesired processes and if possible by not starting the drivers you don't need (this last thing is not supported for ISVs however there are some known ways over the web to achieve the result, which may be OEM-specific).

3. If the application is managed: in general, the .NET CF CLR does not close applications without an unhandled exception dialog box. If you are seeing your application disappear in low memory conditions, it is more likely either the OS closing the application because of general low memory or because some native code within the application’s process experienced an unhandled exception.

4. It may be that the exception is raised by the application and not caught by anyone. Global Exception Handling is a concept for example discussed by Daniel Moth here.

5. In order to trap whatever interesting information, you may use BugTrap. This is the device-side executable of the "Windows Mobile Developer Power Toy" named "RAPI Debug". It might be useful in some cases because it can trap exceptions that aren’t caught by the application or are (or should be) smoothly handled by the OS. For any exception type problem, BugTrap will capture the exception details without the need for an ActiveSync\WMDC connection. But with such it gets even better since DEBUGMSG and RETAILMSG info will be available on the attached PC screen (or redirect to a file). You can run the application after starting RAPI_Debug on your development PC (it requires an ActiveSync connection with the device), and you can use it more than once, to see if the caught exception, if any, is always the same. If you have problems on running it, it may be that it's because RAPI Security Policy on the device (chosen by the manufacturer) prevents it to run: in such a case, you may simply start bugtrap.exe on the device BEFORE launching your application."

6. If you enable "Error Reporting", in case of a crash a mini-dump will be automatically generated. A .kdmp file is then available and you can for example look at it through the Windows CE Dump Viewer. Unfortunately, when working with RETAIL devices very rarely this approach leads to something interesting, because a RETAIL image by definition has no Debug Symbols and you may simply end up by knowing that the function at address X failed to access parameter at address Y. So this approach is for OEMs or for In-ROM ISVs\IHVs, not really for ISV Application Developers. Also because without control on the platform, you can’t even control how DrWatson creates the dumps for you.

 

Cheers,

~raffaele

The principle you should follow is the one used by Dr House in his movies: explore all the paths in order to discard a possible conclusion. It’s an attitude more than a process. And after some years you start dealing this way with everything… Tongue out So, when facing a problem with an application you're writing for Windows Mobile, ask yourself the following questions.

1- Make sure the problem is not OEM-specific. Remember that, as I’ve written here, you should contact that OEM's Technical Support in order to troubleshoot the issue, as only the OEM knows in detail what they included in their OS based on the Windows CE\Mobile platform. So, having been able to reproduce it on different devices from different makers, or even better on the Device Emulator, is one of the first steps. If you don’t know precisely how the interaction between Microsoft and the OEMs work, you may for example read my answer #4 on my friend Chris’ blog here.

2- Before wasting huge amount of time on whatever type of troubleshooting, make sure you’re using the state-of-the-art technologies. For example, if you have a weird problem with a managed application running against v2.0, even if SP2, then you can check if v3.5 included a fix precisely to address the issue you’re experiencing. You don’t need to recompile via VS2008, as I’ve discussed about this here. Or, another example is SQL Compact 3.5: SP1 was recently released – if your issue is somehow related to SSCE3.5 then you should make sure if it was addressed in 3.5 SP1. Finally, when talking about state-of-the-art, I’m also referring to possible ROM Upgrades and Updates published by OEMs so that you can verify if the issue was raised by a older version of a OS' or OEM's component.

3- A problem is not something random. It’s random until we discover its pattern, and when we do it, we have a “deterministic problem”. So, a hint is: invest some time on trying to understand if the problem doesn’t happen under certain circumstances. In really very rare cases I had to do with real­ random problems, due to random failures of the CPU… but this was fixed by the CPU-manufacturer some years ago.

4- Remember that the goal of this phase is to exclude possible problem’s causes… so what about 3RD party libraries? I’m talking about the controls and libraries that the application depends on, and which you don’t own the source of. Many times you simply have to make sure that you’re using them correctly (Is this library the right version for this device? Was the control meant to work for NETCF v3.5? …).

5- Once you exclude all the possible external problems, it’s time to dig into it. But… we can’t work on the whole solutions, plenty of projects! You need to separate the “offending” portion. How do you know that? A possible approach, if the problem is reproduced on the Device Emulator, is to instrument your code, for example by adding calls to the OutputDebugString function in critical parts of your code and configure Device Emulator to bring up a serial debug output console. To do so go to the "File", "Configure..." menu item, switch to the "Peripherals" property sheet and check the "Create text console window for serial port 1" option. To make this option permanent, you can use Visual Studio menu Tools\Options\Devices, select the emulator and then you’ll have the same dialog.

6- Last but not least, EQATEC recently came out with a gift to all Mobile Programmers: a new tool called EQATEC Tracer that may be tremendously useful when troubleshooting.

 

Cheers,

~raffaele

Now come with me: follow the line of my thoughts and tell me if I’m correct. This is a blog written by a member of the Microsoft Technical Support (sorry, it’s “Customer Service Support”), right? What’s a Support Engineer good for? (Feel free to add any comment, but I’ll ban out the ones I don’t like… Tongue out) Can you ask a Support Engineer about beta technologies that will be released in 1-2 years time? Well, yes – but purely because he may have spent some spare time on this. This is indeed the job of an Evangelist. Can you ask a Support Engineer to architect a solution based on the “Mobile Accelerator” that suits your exact scenario? Well, yes – but actually he usually works on solutions that have already been architect-ed and it may be that his knowledge-bag misses some guidelines that instead Consultants must surely have.

What’s in my opinion a Support Engineer is really good at is “Troubleshooting”. Developers ask for our intervention when they don’t know how to continue debugging a nasty problem, or if they think that the problem may be related to a bug, or to know if they’re coding the right way, and so on. The blogosphere is so full of really cool and interesting blogs written by “Mobile Programmers” that share their smart code, I can’t compete with them.. Hot In conclusion, what I can probably offer to the Developer Community is some hints and suggestions about how to face a problem when you have it.

This is basically what some of my co-workers already do, see for example Carlo’s blog. “Ahh..”, you may say, “that’s Web Development: for that you have plenty of troubleshooting tools and techniques, this is not the same in our tiny niche.”. Above all, we’re no longer a niche.. see MEDC: it’s now part of the “main-stream” TechEd. And see also how many blogs & web-sites about Mobile Programming recently proliferated. But you’re right about the other aspect: unfortunately we don’t have many tools that desktop programmers take for granted.

So my intention is to share some techniques that might guide you to the solution when facing a problem during mobile development. Hope it'll be useful, and sorry if I'll write something that may appear obvious to many (but not to all).

Cheers,

~raffaele

SDK Sample LAP for Pocket PC is meant to work on Windows Mobile 6 Professional only: here you can see how to modify it so that it can work on a Classic device as well.

 

Have you ever tried to use the SDK Pocket PC's Sample for LAP on a Windows Mobile 6 CLASSIC device or emulator? Well, I've never done that and wasn't aware of a limitation of the sample code... Let me share a very good example of the added value that Microsoft Technical Support can give to ISV Application Developers out there! Wink

Above all, thanks Masahiro for pointing this out! Masahiro is a new friend of mine, an ISV Application Developer from Japan, and pinged me last week with a weird issue: when running the WM6 SDK Sample LAP on a Classic device, he experienced a hung precisely as I described in a previous post of mine... absolutely the same code on a Professional device had no problems. Remember that Classic is a "Pocket PC with NO phone", while Professional is what we were used to call "Pocket PC Phone Edition"...

So, why on the earth would the problem be repro-d on quite the same platform? Above all, I started with the usual initial troubleshooting steps: I've reproduced on an WM6 Classic Emulator to make sure it was not OEM-dependent. It was not. Then I tried with the WM6.1 Classic Emulator, to know if it was a platform issue addressed in 6.1. It was not. I made sure the problem was not with certificates as I mentioned in my other post: however the WM Emulators already come with SDK Certificates on them, therefore the issue must not be there. Then I researched in documentation and Technical Support archives, to make sure I wasn't missing something fundamental... I was not.

So at this point some further deep digging was required: very likely the next step would have been to create a DEBUG image of the WM6 Classic through the "Windows Mobile Adaptation Kit" (you know, the "Platform Builder for Windows Mobile"), or a RETAIL image with some debug-version DLLs... and this is something that only Windows Mobile OEMs can use: the AK is not publicly available to ISV Developers, and many times this is one of the added values given by Microsoft Technical Support.

As it was in this case: my colleagues in Japan (kudos to Ryoichi!) worked at the issue doing exactly what mentioned above, and discovered the following: they used a tool that is not available to ISV Application Developers, and log all "activities" of the boot phase: one of them was lap.dll trying to load phone.dll.

DoImport Failed! Unable to import Library 'PHONE.dll' for 'lap.dll'

So the problem is with the SDK Sample linking "phone.lib" (and doing some other functions available only on Professional\Standard devices -- emergency calls): if the phone.lib is linked to the LAP.DLL, the DLL need to load phone.dll simultaneously. But phone.dll is not available on a WM6 Classic device.

Thus, to make LAP.DLL to load for WM6 classic device, you need to remove "phone.lib" from linker settings from project properties. Also, LAP sample is using following APIs to accomplish emergency calling. To support both Classic and Professional devices, you need to change function calling method to using LoadLibrary("phone.dll") and GetProcAddress().

  • PhoneIsEmergencyNumber
  • PhoneSendDTMFStart
  • PhoneSendDTMFStop
  • PhoneMakeCall

In other words:

1. Add following code to util.h in LAP sample.

#include <phone.h>
typedef BOOL (*PFN_PhoneIsEmergencyNumber)(BOOL,LPCTSTR,LPCTSTR*);
typedef HRESULT (*PFN_PhoneSendDTMFStart)(TCHAR);
typedef void (*PFN_PhoneSendDTMFStop)(void);
typedef LONG (*PFN_PhoneMakeCall)(PHONEMAKECALLINFO *);

extern HMODULE g_hModule;
extern PFN_PhoneIsEmergencyNumber pfnPhoneIsEmergencyNumber;
extern PFN_PhoneSendDTMFStart pfnPhoneSendDTMFStart;
extern PFN_PhoneSendDTMFStop pfnPhoneSendDTMFStop;
extern PFN_PhoneMakeCall pfnPhoneMakeCall;

 

2. Add additional global variables to lap.cpp

HMODULE g_hModule = NULL;
PFN_PhoneIsEmergencyNumber pfnPhoneIsEmergencyNumber = NULL;
PFN_PhoneSendDTMFStart pfnPhoneSendDTMFStart = NULL;
PFN_PhoneSendDTMFStop pfnPhoneSendDTMFStop = NULL;
PFN_PhoneMakeCall pfnPhoneMakeCall = NULL;

 

3. Add following code sample into InitLAP and DeinitLAP in lap.cpp

BOOL InitLAP(InitLap* il) 
{
    ...
    // Check if policy requires us to force enrollment
    g_hPolicyThread = CreateThread(NULL, 0, PolicyThreadProc, NULL, 0, NULL);
    ASSERT(NULL != g_hPolicyThread);

    // ADD FROM HERE
    g_hModule = LoadLibrary(_T("phone.dll"));
    if (g_hModule)
    {
        pfnPhoneIsEmergencyNumber = (PFN_PhoneIsEmergencyNumber)GetProcAddress(g_hModule, _T("PhoneIsEmergencyNumber"));
        pfnPhoneSendDTMFStart = (PFN_PhoneSendDTMFStart)GetProcAddress(g_hModule, _T("PhoneSendDTMFStart"));
        pfnPhoneSendDTMFStop = (PFN_PhoneSendDTMFStop)GetProcAddress(g_hModule, _T("PhoneSendDTMFStop"));
        pfnPhoneMakeCall = (PFN_PhoneMakeCall)GetProcAddress(g_hModule, _T("PhoneMakeCall"));
        if (!pfnPhoneIsEmergencyNumber ||
            !pfnPhoneSendDTMFStart ||
            !pfnPhoneSendDTMFStop ||
            !pfnPhoneMakeCall)
        {
            FreeLibrary(g_hModule);
            pfnPhoneIsEmergencyNumber = NULL;
            pfnPhoneSendDTMFStart = NULL;
            pfnPhoneSendDTMFStop = NULL;
            pfnPhoneMakeCall = NULL;
        }
    }
    // ADD TO HERE
    
    return bInitialized;
}

void DeinitLAP()
{
    ...
    // Free the critical section resources.
    DeleteCriticalSection(&g_csAuthResetSetup);
    
    // ADD FOLLOWING 2 LINES
    if (g_hModule)
    FreeLibrary(g_hModule);
}

 

4. Change some functions name like from PhoneMakeCall to pfnPhoneMakeCall in project. And NULL checking for function pointers before calling pfnXxxx functions.

 

Moral of the story is: when you find yourself in "programming" troubles, get what you paid for and Make sure you realize (or realise...) Your Potential with Microsoft Technical Support!! Open-mouthed

 

Cheers!

~raffaele

Summary: this post contains a sample code showing a way to use MAPI to log and count the mails of a Windows Mobile 6's Outlook Mobile's Inbox folder into a file (sender, recipients, subject, date-time, BODY).

 

Recently I've been involved in another case about MAPI: this time the developers needed to programmatically retrieve the BODY of the mails stored in the ActiveSync account (synchronized with the remote Exchange server) and some other default values such as sender, recipients, date-time. I erroneously imagined that such an apparently simple task already had a sample code available somewhere, however this is not true, probably because InTheHand did an extremely good job on wrapping MAPI for NETCF developers and because, let's admit it, MAPI are not so friendly to use. Smile Even so, in any case for native developers (and for managed ones aiming to wrap few MAPI or to create a native DLL exposing a function, as in this case), to get started you can read Jay Ongg's posts "Getting Started with MAPI" and "Practical Use of MAPI" on the Windows Mobile Dev Team's blog.

Furthermore, I discovered that Windows Mobile 5.0 and 6 behave differently when it comes to mail bodies, and I understood why: basically MAPI Specifications allow to store the information in different ways, and different platforms achieved the same results by using different approaches. And besides of that, on Windows Mobile 5.0 HTML formatting was not natively supported, therefore you don't find PR_BODY_HTML even defined the in the MAPI definitions header files (so you need a compilation-condition #if you want the same code to work on both), while in contrast on Windows Mobile 6 the default formatting is HTML, and accordingly to the documentation (Message Content Properties) the body for incoming messages can be retrieved through PR_BODY_HTML_A.

In conclusion, to make a robust client application you basically need to loop through the possible properties until a match is found: MAPI can be really straightforward in this case, once you know how to do it... Nerd

 

I talked too much, as usual... let's see some code in action:

1- In my case I wanted a native DLL to be invoked by a NETCF client: here it is the prototype of the exported function

//Accesses the "ActiveSync" MAPI Message Store and copies the items from the "Inbox" folder into a file (\mails.txt)
//Returns the numbers of copied messages
INTHEINBOX_API ULONG SaveMessagesIntoFile();

//being: (this is done by VS2008 when applying the template to export symbols -- you can also use a .def file)
#ifdef INTHEINBOX_EXPORTS
#define INTHEINBOX_API __declspec(dllexport)
#else
#define INTHEINBOX_API __declspec(dllimport)
#endif

and here it is how it can be called in a test VB.NET client, for example:

<DllImport("InTheInbox.dll")> _
Private Function SaveMessagesIntoFile() As Integer
End Function

Sub Main()
    'this MsgBox allows to attach the native debugger before the native DLL is used
    MsgBox("Attach the debugger to the process and click Ok to go")
    Dim NumberOfMessagesInInbox As UInteger = SaveMessagesIntoFile()
    MsgBox("Saved " + NumberOfMessagesInInbox.ToString() + " messages.")
End Sub

 

2- Above all the exported function has to get the MAPI Session's "Message Store Table" (IMAPISession::GetMsgStoresTable) to retrieve the Message Store we're interested on, which in this case is the one whose PR_DISPLAY_NAME is "ActiveSync" (if you don't set any other mail-account, Windows Mobile has 2 stores, one for Outlook mails ("ActiveSync") and one for sms ("SMS"): for example Jay Ongg here accessed the "SMS" store). Once the message store is individuated and opened (IMAPISession::OpenMsgStore), the code invokes an internal function that takes the store pointer as argument and saves and counts messages from the Inbox MAPI Folder:

////////////////////////////////////////////////////////////////////////////// 
//SaveMessagesIntoFile
//Accesses the "ActiveSync" MAPI Message Store and copies the items from the "Inbox" folder into a file (\mails.txt)
//Returns the numbers of copied messages
INTHEINBOX_API ULONG SaveMessagesIntoFile()
{
    HRESULT hr = E_FAIL;
    LPENTRYID pEntryId = NULL;
    ULONG cbEntryId = 0;
    LPMAPITABLE ptbl;

    ULONG ulObjType = 0;
    SRowSet *prowset = NULL;
    IMsgStore* pStore = NULL;

    //Initialize MAPI COM Server
    hr = MAPIInitialize(NULL);    
    CHR(hr);

    //Logon to MAPI and get a session pointer
    hr = MAPILogonEx(0, NULL, NULL, 0, (LPMAPISESSION *)&m_pSession);
    CHR(hr);
    
    //variable to be used when setting the columns of the table we're going to retrieve
    static const SizedSPropTagArray(2, spta) = { 2, PR_DISPLAY_NAME, PR_ENTRYID };    
        
    // Get the table of accounts
    hr = m_pSession->GetMsgStoresTable(0, &ptbl);
    CHR(hr);

    // set the columns of the table we will query
    hr = ptbl->SetColumns((SPropTagArray *) &spta, 0);
    CHR(hr);

    while (TRUE)
    {
        // Free the previous row
        FreeProws (prowset);
        prowset = NULL;
 
        hr = ptbl->QueryRows (1, 0, &prowset);
        if ((hr != S_OK) || (prowset == NULL) || (prowset->cRows == 0))
            break;
 
        CBR (prowset->aRow[0].cValues == spta.cValues);
        SPropValue *pval = prowset->aRow[0].lpProps;
 
        CBR (pval[0].ulPropTag == PR_DISPLAY_NAME);
        CBR (pval[1].ulPropTag == PR_ENTRYID);
 
        //Windows Mobile natively has 2 MAPI Message Stores: "ActiveSync" and "SMS" (then also POP\SMTP if user adds them)
        //Now we're interested on ActiveSync
        if (!_tcscmp(pval[0].Value.lpszW, TEXT("ActiveSync")))
        {
            // Get the Message Store pointer
            hr = m_pSession->OpenMsgStore(0, pval[1].Value.bin.cb, (LPENTRYID)pval[1].Value.bin.lpb, 0, 0, &pStore);
            CHR(hr);
 
            //Invoke internal function to save and count messages from store's Inbox
            hr = SaveMessagesFromStore(pStore, g_pszFilename);
            CHR(hr);
        }
    }

    //finishing off...
    hr = m_pSession->Logoff(0, 0, 0);
    
Exit:
    //make sure we don't leak memory
    FreeProws(prowset);
    RELEASE_OBJ(ptbl);
    RELEASE_OBJ(pStore);
    RELEASE_OBJ(m_pSession);   

    MAPIUninitialize();

    return g_ulNumberOfMessages;
}

3- Now, thanks to IMsgStore::GetReceiveFolder, we can grab a pointer to the Inbox folder and invoke another internal function, which works directly on a specific MAPI Folder instead of a MAPI Store (I preferred to maintain them separated so that they can be easily ported to other scenarios -- i.e. for example the Drafts or the Sent Items folder):

////////////////////////////////////////////////////////////////////////////// 
//SaveMessagesFromStore
//Accesses the passed MAPI Message Store and copies the items from the "Inbox" folder into a file (\mails.txt)
HRESULT SaveMessagesFromStore(IMsgStore* pStore, LPCTSTR pszFilename)
{
    HRESULT hr = E_FAIL;
    
    ULONG cbEntryId = 0;
    ULONG ulObjType = 0;
    LPMAPIFOLDER pFolder = NULL;
    LPENTRYID pEntryId = NULL;
    LPMAPITABLE ptbl = NULL;
    SRowSet *prowset = NULL;

    // Get the inbox folder
    hr = pStore->GetReceiveFolder(NULL, MAPI_UNICODE, &cbEntryId, &pEntryId, NULL); 
    CHR(hr);

    // We have the entryid of the inbox folder, let's get the folder and messages in it
    hr = pStore->OpenEntry(cbEntryId, pEntryId, NULL, 0, &ulObjType, (LPUNKNOWN*)&pFolder);
    CHR(hr);

    //check
    CBR(ulObjType == MAPI_FOLDER);

    //invoke helper function to retrieve messages from MAPI Folder
    hr = SaveMessagesFromFolder(pFolder, pszFilename);
    CHR(hr);

    //update global variable so that SaveMessagesIntoFile() can return the number of the messages in Inbox
    hr = CountMessagesInFolder(pFolder, &g_ulNumberOfMessages);
    CHR(hr);

    //success!
    hr = S_OK;

Exit:
    FreeProws (prowset);
    RELEASE_OBJ(pFolder);
    RELEASE_OBJ(ptbl);

    MAPIFreeBuffer(pEntryId);

    return hr;
}

 

4- And this is the function doing the most of the job! A next version of the application may write XML-ish so that a NETCF client can easily use for example XmlTextReader or whatever (at the moment I didn't want wasting time on understanding how to include information containing the characters " and @...).

////////////////////////////////////////////////////////////////////////////// 
//SaveMessagesFromFolder
//Accesses the passed MAPI Message Folder and copies the items into a file
HRESULT SaveMessagesFromFolder(IMAPIFolder* pFolder, LPCTSTR pszFilename)
{
    static const SizedSPropTagArray(4, spta) = {4, PR_ENTRYID, PR_SENDER_EMAIL_ADDRESS, PR_SUBJECT, PR_MESSAGE_DELIVERY_TIME};
    static const SizedSSortOrderSet(1, sortOrderSet) = { 1, 0, 0, { PR_MESSAGE_DELIVERY_TIME, TABLE_SORT_DESCEND } };

    HRESULT hr = E_FAIL;
    
    IMAPITable * pContentsTable  =   NULL;
    SRowSet * psrs = NULL;
    IMessage * pMsg = NULL;

    CBR (pFolder !=  NULL);

    //Strings represeting From, To, Subject, Date\Time  
    //(not Body, as it can be so large that it's better to write it directly to the file)
    LPTSTR pszSender = new TCHAR[MAXBUF];
    ZeroMemory(pszSender, MAXBUF - 1);

    LPTSTR pszTo = new TCHAR[MAXBUF];
    ZeroMemory(pszTo, MAXBUF - 1);
    
    LPTSTR pszSubject = new TCHAR[MAXBUF];
    ZeroMemory(pszSubject, MAXBUF - 1);
    
    LPTSTR pszDate; // = new TCHAR[MAXBUF];
    //ZeroMemory(pszDate, MAXBUF - 1);

    LPTSTR pszTime; // = new TCHAR[MAXBUF];
    //ZeroMemory(pszTime, MAXBUF - 1);

    LPTSTR pszBody;


    //Buffer for writing to the file
    LPTSTR lpBuf = new TCHAR[MAXBUF];
    ZeroMemory(lpBuf, MAXBUF - 1);


    // Open the folder's contents table
    hr = pFolder->GetContentsTable (MAPI_UNICODE, &pContentsTable);
    CHR(hr);
        
    // Sort the table that we obtained by time
    hr = pContentsTable->SortTable((SSortOrderSet *)&sortOrderSet, 0);
    CHR(hr);

    // Set columns we need (PR_ENTRYID, PR_SENDER_EMAIL_ADDRESS, PR_SUBJECT, PR_MESSAGE_DELIVERY_TIME)
    hr = pContentsTable->SetColumns ((SPropTagArray *) &spta, 0);
    CHR(hr);

    //start writing file: \xFEFF is used so that the file can be opened by "special editors"
    hr = LogToFile(TEXT("\xFEFF"), g_pszFilename);  

    // Iterate over each row of the Folder's contents (i.e. messages) table
    while (1)
    {
        // Get a row
        hr = pContentsTable->QueryRows (1, 0, &psrs);
        CHR(hr);
       
        // Did we hit the end of the table?
        if (psrs->cRows != 1)
            break;
                
        // Open this message
        hr = pFolder->OpenEntry (psrs[0].aRow[0].lpProps[0].Value.bin.cb, 
                                 (ENTRYID *) psrs[0].aRow[0].lpProps[0].Value.bin.lpb, 
                                 NULL, 
                                 0, 
                                 NULL, 
                                 (IUnknown **) &pMsg);
        CHR(hr);

        //check
        CBR (psrs[0].aRow[0].lpProps[0].ulPropTag == PR_ENTRYID);
        CBR (psrs[0].aRow[0].lpProps[1].ulPropTag == PR_SENDER_EMAIL_ADDRESS); 
        CBR (psrs[0].aRow[0].lpProps[2].ulPropTag == PR_SUBJECT);
        CBR (psrs[0].aRow[0].lpProps[3].ulPropTag == PR_MESSAGE_DELIVERY_TIME);
        
        //FROM PR_SENDER_EMAIL_ADDRESS:
        //LPCTSTR pszSender = psrs[0].aRow[0].lpProps[1].Value.lpszW;
        hr = StringCchCopy(pszSender, MAXBUF, psrs[0].aRow[0].lpProps[1].Value.lpszW);
        CHR(hr);

        //FROM PR_SUBJECT:
        //LPCTSTR pszSubject = psrs[0].aRow[0].lpProps[2].Value.lpszW;
        hr = StringCchCopy(pszSubject, MAXBUF, psrs[0].aRow[0].lpProps[2].Value.lpszW);
        CHR(hr);

        //FROM PR_MESSAGE_DELIVERY_TIME:
        SYSTEMTIME st = {0};
        FileTimeToSystemTime(&psrs[0].aRow[0].lpProps[3].Value.ft, &st);

        //FROM PR_MESSAGE_DELIVERY_TIME:
        //Date...
        int lenDate = GetDateFormat(LOCALE_USER_DEFAULT, 
                                    DATE_SHORTDATE, 
                                    &st, 
                                    NULL, 
                                    NULL, 
                                    0);
        pszDate = new TCHAR[lenDate]; //The count before includes the terminating null
        if (0 == GetDateFormat(LOCALE_USER_DEFAULT, 
                                    DATE_SHORTDATE, 
                                    &st, 
                                    NULL, 
                                    pszDate, 
                                    lenDate))
        {
            hr = E_FAIL;
            goto Exit;
        }

        //.. and Time:
        int lenTime = GetTimeFormat(LOCALE_USER_DEFAULT, 
                                    TIME_FORCE24HOURFORMAT | TIME_NOTIMEMARKER, 
                                    &st, 
                                    NULL, 
                                    NULL, 
                                    0);
        pszTime = new TCHAR[lenTime]; //The count before includes the terminating null
        if (0 == GetTimeFormat(LOCALE_USER_DEFAULT, 
                                    TIME_FORCE24HOURFORMAT | TIME_NOTIMEMARKER, 
                                    &st, 
                                    NULL, 
                                    pszTime, 
                                    lenTime))
        {
            hr = E_FAIL;
            goto Exit;
        }

        
        //RECIPIENTs are not a simple property: invoke helper function that internally uses GetRecipientTable() and PR_EMAIL_ADDRESS
        hr = WriteMessageRecipientsToString (pMsg, &pszTo);
        CHR(hr);

        //Mail's BODY can be so large that it's better to write BODY's data chunk directly to the file
        //instead of retrieving a LPTSTR representing it and then place it within a LPTSTR without knowing
        //the dimenions a priori
        hr = StringCchPrintf((LPTSTR)lpBuf, 
            LocalSize(lpBuf) / sizeof(TCHAR),
            TEXT("=========================================================\r\n")
            TEXT("FROM:\t\t%s\r\n")            //pszSender
            TEXT("SENT:\t\t%s, %s\r\n")        //pszDate, pszTime
            TEXT("TO:\t\t%s\r\n")            //pszTo
            TEXT("SUBJECT:\t%s\r\n")        //pszSubject
            TEXT("\r\n"),
                pszSender, 
                pszDate, pszTime,
                pszTo,
                pszSubject);
        CHR(hr);

        hr = LogToFile(lpBuf, pszFilename);
        CHR(hr);

        hr = WriteMessageBodyToFile(pMsg, g_pszFilename);
        CHR(hr);

        hr = LogToFile(TEXT("\r\n\r\n"), pszFilename);
        CHR(hr);

        // Clean up before re-using
        FreeProws (psrs);          
        psrs = NULL;       
        RELEASE_OBJ (pMsg);
    }

    //SUCCESS!
    hr = S_OK;
Exit:
    
    FreeProws   (psrs);          
    RELEASE_OBJ (pMsg);
    RELEASE_OBJ (pContentsTable);
    
    DELETE_STR(lpBuf);
    DELETE_STR(pszSender);
    DELETE_STR(pszTo);
    DELETE_STR(pszSubject);
    DELETE_STR(pszDate);
    DELETE_STR(pszTime);

    return hr;
}

 

5- Usually MAPI allow you to invoke a method that retrieves a IMAPITable associated to an object and then sets its columns accordingly to the properties you want to query: for example, you can use IMAPISession::GetMsgStoresTable to get the table of stores; another example is IMAPIFolder::GetContentsTable. To set the columns you can use a SPropTagArray structure. This is fine with properties you know don't require "too much" memory, such as PR_SENDER_EMAIL_ADDRESS, PR_SUBJECT and PR_MESSAGE_DELIVERY_TIME; this is NOT true for properties like PR_BODY (or PR_BODY_HTML) where the object (in this case a string) can even reach hundreds of KB. In such a case, there's a different approach, based on the IMAPIProp::OpenProperty function. The function WriteMessageBodyToFile invoked by the above one shows the second approach (and btw on Windows Mobile only IAttach and IMessage expose such method).

Note the comments before the function's definition as they can explain why a MAPI-based code worked on WM5 and is no longer working on WM6: kudos to my friend David for them! Smile Moreover, those comments explain that in order to have robust code working on different platforms you can query PR_MSG_STATUS to know if the message has a BODY in plain-text, or in HTML, or MIME text (whose parser is NOT included in this sample code).

////////////////////////////////////////////////////////////////////////////// 
//WriteMessageBodyToFile
//Takes a IMessage as input and writes its BODY to a file
/*
NOTES:
- For Windows Mobile 6:
outgoing mail bodies in:
* PR_BODY_W for plain text mail
* PR_BODY_HTML_A (multibyte, for html mail)
incoming mail bodies in:
* ActiveSync account (both Exchange ActiveSync and PC Sync)
 ** PR_BODY_A, or
 ** PR_BODY_HTML_A
* POP3/IMAP messages come in MIME format and we store the full MIME with body included in: PR_CE_MIME_TEXT

Since MAPI specification does allow you to store info in different ways, to make a robust client app you basically 
need to loop through possible location of the properties until a match is found. You can query PR_MSG_STATUS and 
check against flags:
* MSGSTATUS_HAS_PR_BODY
* MSGSTATUS_HAS_PR_BODY_HTML
* MSGSTATUS_HAS_PR_CE_MIME_TEXT 
...to determine which properties are used for a given message.

- For Windows Mobile 5.0:
* incoming bodies are stored in PR_CE_MIME_TEXT (in multibyte)
* outgoing mail bodies are stored in PR_BODY (in Unicode)
* Everything is plain text. No HTML support
*/
HRESULT WriteMessageBodyToFile(IMessage * pMsg, LPCTSTR pszFileName)
{
    HRESULT hr = E_FAIL;

    LPSTREAM pstmBody = NULL;
    BYTE* pszBodyInBytes;
    LPTSTR pszBody; 
    
    IMAPIProp *pProp = NULL; 
    ULONG rgTags[] = {2, PR_MSG_STATUS, PR_MESSAGE_FLAGS};
    ULONG cValues = 0;
    SPropValue *rgMsgProps= NULL;

    //check
    CBR (pMsg !=  NULL);

    // Get an IMAPIProp Interface
    hr = pMsg->QueryInterface(IID_IMAPIProp, (LPVOID *) &pProp);
    CHR(hr);
    CPR(pProp);

    // Get the Message's STATUS and FLAGs properties (for Body we would only need STATUS, not FLAGs)
    hr = pProp->GetProps((LPSPropTagArray)rgTags, MAPI_UNICODE, &cValues, &rgMsgProps);
    CHR(hr);
    CBR(PR_MSG_STATUS == rgMsgProps[0].ulPropTag);            
    CBR(PR_MESSAGE_FLAGS == rgMsgProps[1].ulPropTag);            
    
    //the message can be MIME, HTML or PLAIN-TEXT:
    BOOL bIsMime = (
                        (rgMsgProps[0].ulPropTag == PR_MSG_STATUS) && 
                            (    
                                (rgMsgProps[0].Value.ul & MSGSTATUS_HAS_PR_CE_MIME_TEXT) ||
                                (rgMsgProps[0].Value.ul & MSGSTATUS_HAS_PR_CE_CRYPT_MIME_TEXT) ||
                                (rgMsgProps[0].Value.ul & MSGSTATUS_HAS_PR_CE_SMIME_TEXT)
                            )
                    )
                    && 
                    !(rgMsgProps[0].Value.ul & MSGSTATUS_HAS_PR_BODY) ;
    BOOL bHasBody = (rgMsgProps[0].ulPropTag == PR_MSG_STATUS) && (rgMsgProps[0].Value.ul & MSGSTATUS_HAS_PR_BODY);
    BOOL bHasHtmlBody = (rgMsgProps[0].ulPropTag == PR_MSG_STATUS) && (rgMsgProps[0].Value.ul & MSGSTATUS_HAS_PR_BODY_HTML);

    if (bHasBody) //PR_BODY, PR_BODY_W, PR_BODY_A
    {
        hr = pMsg->OpenProperty (PR_BODY, NULL, STGM_READ, 0, (IUnknown **) &pstmBody);
        if (hr == MAPI_E_NOT_FOUND)
        {
            hr = pMsg->OpenProperty (PR_BODY_W, NULL, STGM_READ, 0, (IUnknown **) &pstmBody);
            if (hr == MAPI_E_NOT_FOUND)
            {
                hr = pMsg->OpenProperty (PR_BODY_A, NULL, STGM_READ, 0, (IUnknown **) &pstmBody);
                //CBR (hr != MAPI_E_NOT_FOUND); --> equivalent to the following
                if (hr == MAPI_E_NOT_FOUND)
                {
                    //in this case no PR_BODY, nor PR_BODY_W, nor PR_BODY_A
                    hr = E_FAIL;
                    goto Exit;
                }
            }
        }
        //pstmBody now contains the body data stream, except for MIME
    }


    
    //PORTABILITY NOTE: Windows Mobile 5.0 doesn't have HTML support, so WM5's mapitags.h doesn't even define PR_BODY_HTML
    //In other words, the compiler complains about PR_BODY_HTML when targeting WM5 if the check about 
    //_WIN32_WCE > 0x501 is not done
#if _WIN32_WCE > 0x501 //we're on WM6, 6.1, ...
    if (bHasHtmlBody) //PR_BODY_HTML, PR_BODY_HTML_W, PR_BODY_HTML_A
    {
        hr = pMsg->OpenProperty (PR_BODY_HTML_A, NULL, STGM_READ, 0, (IUnknown **) &pstmBody);
        if (hr == MAPI_E_NOT_FOUND)
        {
            hr = pMsg->OpenProperty (PR_BODY_HTML_W, NULL, STGM_READ, 0, (IUnknown **) &pstmBody);
            if (hr == MAPI_E_NOT_FOUND)
            {
                hr = pMsg->OpenProperty (PR_BODY_HTML, NULL, STGM_READ, 0, (IUnknown **) &pstmBody);
                //CBR (hr != MAPI_E_NOT_FOUND); --> equivalent to the following
                if (hr == MAPI_E_NOT_FOUND)
                {
                    //in this case no PR_BODY_HTML, nor PR_BODY_HTML_W, nor PR_BODY_HTML_A
                    hr = E_FAIL;
                    goto Exit;
                }
            }
        }
        //pstmBody now contains the body data stream, except for MIME
    }
#endif

    //process pstmBody... (cases PR_BODY(_x) and PR_BODY_HTML(_x)) -- NOT IF MIME
    if (bHasBody || bHasHtmlBody)
    {
        STATSTG statstg;    
        DWORD   cbBody = 0;     
        ULONG   cbRead = 0;     
        
        //Get the size of the body
        hr = pstmBody->Stat(&statstg, 0);
        CHR(hr);

        //Allocate a buffer for the stream
        cbBody = statstg.cbSize.LowPart; 
        pszBodyInBytes = new BYTE[cbBody+1]; 
        ZeroMemory(pszBodyInBytes, cbBody+1);

        //Read stream array (IStream::Read wants a BYTE* as 1st arg)
        hr = pstmBody->Read (pszBodyInBytes, cbBody, &cbRead);
        CHR(hr);

        //check
        CBR(0 != cbRead); 
        CBR(cbBody == (int)cbRead);

        //Add trailing NULL
        pszBodyInBytes[cbBody+1] = '\0';

        //convert byte array to WCHAR array to be consistent with the rest of the strings written to the file
        //Remember that for WM6, as documented in http://msdn.microsoft.com/en-us/library/bb446140.aspx,
        //we're here with PR_BODY_HTML_A in case of both incoming and outgoing mails of the ActiveSync message store
        pszBody = new TCHAR [cbBody+1]; //remember that cbBody==cbRead
        ZeroMemory(pszBody, cbBody+1);

        MultiByteToWideChar(CP_ACP, 0, (char*)pszBodyInBytes, cbRead, pszBody, cbBody+1); 

        //write to file
        hr = LogToFile(pszBody, g_pszFilename);
        CHR(hr);
    }
    
    //MIME
    if (bIsMime)
    {
        //not implemented... but needed if you have to retrieve WM5 received messages
        hr = HRESULT(ERROR_NOT_SUPPORTED);
        goto Exit;
    }

    //Success!
    hr = S_OK;


Exit:
    RELEASE_OBJ (pstmBody);
    RELEASE_OBJ (pProp);

    DELETE_STR (pszBody);
    DELETE_STR (pszBodyInBytes);
    
    return hr;
}

 

6- There was another specific MAPI that was interesting to see in action here: IMessage::GetRecipientTable (to retrieve all the recipients of the mail and fill the file in the <TO> </TO> section):

////////////////////////////////////////////////////////////////////////////// 
//WriteMessageRecipientsToString
//RECIPIENTs are not a simple property: you need MAPI GetRecipientTable() and PR_EMAIL_ADDRESS
HRESULT WriteMessageRecipientsToString (IMessage* pMsg, LPTSTR* pszTo)
{
    HRESULT hr = S_OK;
    IMAPITable * pRecipientTable = NULL;
    SRowSet * psrs = NULL;
    SPropValue * pspv = NULL;
    SPropValue * pspvLast = NULL;

    //this is used simply to add a ", " after the 1st recipient (in case of multiple recipients)
    BOOL fFirstRecipient =   TRUE;
    
    size_t pcchA, pcchB;
 
    //reset passed variable
    size_t len;
    hr = StringCchLength(*pszTo, MAXBUF, &len);
    CHR(hr);
    ZeroMemory(*pszTo, len);

    //check
    CBR (pMsg != NULL);

    //Fill the table of Recipients
    hr = pMsg->GetRecipientTable (MAPI_UNICODE, &pRecipientTable);
    CHR(hr);

    while (1)
    {
        // Copy properties to the ADRLIST
        hr = pRecipientTable->QueryRows (1, 0, &psrs);
        CHR(hr);
        
        // Did we hit the end of the table?
        if (psrs->cRows != 1)
            break;
        
        // Point just past the last property
        pspvLast = psrs->aRow[0].lpProps + psrs->aRow[0].cValues;

        // Loop through all the properties returned for this row
        for (pspv = psrs->aRow[0].lpProps; pspv < pspvLast; ++pspv)
        {
            switch (pspv->ulPropTag)
            {
                //At this point you may also be interested on PR_DISPLAY_NAME
            case PR_EMAIL_ADDRESS:
            case PR_EMAIL_ADDRESS_A:
                if (!fFirstRecipient) {
                    hr = StringCchLength(*pszTo, MAXBUF, &pcchA);
                    CHR(hr);

                    //hr = StringCchLength(TEXT(", "), STRSAFE_MAX_CCH, &pcchB); //This is =2
                    //CHR(hr);

                    //hr = StringCchCat(pszRet, pcchA+2+1, TEXT(", "));
                    hr = StringCchCat(*pszTo, pcchA+2+1, TEXT(", "));
                    CHR(hr);
                }
                else {
                    fFirstRecipient = FALSE;
                }

                hr = StringCchLength(*pszTo, MAXBUF, &pcchA);
                CHR(hr);

                hr = StringCchLength(pspv->Value.lpszW, MAXBUF, &pcchB);
                CHR(hr);

                hr = StringCchCat(*pszTo, pcchA+pcchB+1, pspv->Value.lpszW);
                CHR(hr);

                break;

            default:
                break;
            }
        }

        //Clen rows before re-using
        FreeProws (psrs);
        psrs = NULL;
    }

    //Success!
    hr = S_OK;

Exit:
    FreeProws (psrs);
    RELEASE_OBJ (pRecipientTable);

    return hr;
}

 

7- To be complete, the code needs a function that counts the number of the messages in a given folder:

////////////////////////////////////////////////////////////
// CountMessagesInFolder 
// Takes a folder as argument and fills out the # of messages in it
HRESULT CountMessagesInFolder(LPMAPIFOLDER pFolder, ULONG * ulTotalMessages)
{
    HRESULT hr = E_FAIL;
    ULONG rgTags[] = {2, PR_CONTENT_COUNT, PR_FOLDER_TYPE};
    ULONG cValues = 0;
    IMAPIProp *pProp = NULL; 
    SPropValue *rgFolderProps= NULL;

    // Get an IMAPIProp Interface
    hr = pFolder->QueryInterface(IID_IMAPIProp, (LPVOID *) &pProp);
    CHR(hr);
    CPR(pProp);

    // Get the Folder PR_CONTENT_COUNT property
    hr = pProp->GetProps((LPSPropTagArray)rgTags, MAPI_UNICODE, &cValues, &rgFolderProps);
    CHR(hr);
    CBR(PR_CONTENT_COUNT == rgFolderProps[0].ulPropTag);            
    CBR(PR_FOLDER_TYPE == rgFolderProps[1].ulPropTag);            
    
    //Set #messages
    *ulTotalMessages = rgFolderProps[0].Value.ul;

    hr = S_OK;

Exit:
    RELEASE_OBJ(pProp);

    return hr;            
}

 

and another function that appends a string into a file:

////////////////////////////////////////////////////////////
// LogToFile 
// Writes szLog into the file named pszFilename
HRESULT LogToFile(LPTSTR szLog, LPCTSTR pszFilename)
{
    HRESULT hr = E_FAIL;
    
    //Open the handle to the file (and create it if it doesn't exist
    HANDLE hFile = CreateFile(pszFilename, GENERIC_WRITE, FILE_SHARE_READ, NULL, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
    if (INVALID_HANDLE_VALUE == hFile)
        goto Exit;

    //Set the pointer at the end so that we can append szLog
    DWORD dwFilePointer = SetFilePointer(hFile, 0, NULL, FILE_END);
    if (0xFFFFFFFF == dwFilePointer)
        goto Exit;

    //Write to the file
    DWORD dwBytesWritten = 0;
    BOOL bWriteFileRet = WriteFile(hFile, szLog, wcslen(szLog) * 2, &dwBytesWritten, NULL);
    if (!bWriteFileRet)
        goto Exit;

    //Flush the buffer
    BOOL bFlushFileBuffersRet = FlushFileBuffers(hFile);
    if (!bFlushFileBuffersRet)
        goto Exit;

    //Success
    hr = S_OK;

Exit:
    if (NULL != hFile)
        CloseHandle(hFile);

    return hr;
}

 

For the sake of clarity, the macros I used were:

////////////////////////////////////////////////////////////////////////////// 
//Macros
#define _ExitLabel Exit

#define CHR(hResult) \
    if(FAILED(hResult)) { hr = (hResult); goto _ExitLabel;} 

#define CPR(pPointer) \
    if(NULL == (pPointer)) { hr = (E_OUTOFMEMORY); goto _ExitLabel;} 

#define CBR(fBool) \
    if(!(fBool)) { hr = (E_FAIL); goto _ExitLabel;} 

#define RELEASE_OBJ(s)  \
    if (NULL != s)      \
    {                   \
        s->Release();   \
        s = NULL;       \
    }

#define DELETE_STR(s)    \
    if (NULL != s)        \
        delete [] s;

 

NOTEs:

  • an application invoking MAPI on a client (a device in our case) accesses the local Message Stores of the client. So, if you set a "Download Size Limit" for the mails sync-ed with a backend Exchange server, then be prepared to retrieve PR_BODY and PR_BODY_HTML_A till that limit! In my case I wasn't really understanding why the code above was producing truncated bodies... but thanks to a friend I understood: incredibly simply, the message was not fully downloaded! Confused
  • There's not much error-tracking or tracing in the sample as it's meant to be a mere proof of concept with didactic goals: probably the best approach would be to modify the MACROs so that you can trace\log the function producing the problem, if so.
  • Remember to release the resources to avoid memorey leaks!!

 

And now, enjoy your MAPI-Programming on Windows Mobile 6!! Open-mouthed 

 

Cheers,

~raffaele

Imagine you want to uninstall an application on a Windows Mobile device, and you want to do that through a CAB file. There are other means, yes, (see doc) however in this particular case a CAB was required because IT admins were using SCCM (System Center Configuration Manager) to handle mobile devices remotely and wanted a CAB. Ok, no problems: it's pure XML Provisioning! Nerd It's simply a matter of targeting the UnInstall CSP (Configuration Service Provider) by using for example the following XML and package it into a CAB through makecab.exe accordingly to the documentation (remember that .CAB and .CPF are the same, the only difference being with the outputted result: in the default Messaging account's inbox for .CPF and directly through a dialog for .CAB):

<wap-provisioningdoc> 
   <characteristic type="UnInstall"> 
      <characteristic type="AppName"> 
         <parm name="uninstall" value="1"/>    
      </characteristic> 
   </characteristic> 
</wap-provisioningdoc>

The usual steps when creating a XML Provisioning-based approach involve testing the XML through RapiConfig: we did it and XML was fine (i.e. the application gets uninstalled). Then we packaged a CAB through makecab.exe as the doc dictates and run the CAB on the device: it returned a general installation failure message ("Installation of xx.cab was unsuccessful."). When troubleshooting the problem, we tried by compressing\not compressing the CAB (makecab.exe /D COMPRESS=OFF), then we made sure the problem was not with the long name of the CAB file (this was a problem many years ago, but no more), then with the Security Configuration and even with the formatting (ANSI\UTF-8) of the _setup.xml included in the CAB...

At the end it turned out that the problem is on Windows Mobile Devs' desk for further analysis in order to be addressed in future releases: simply put, the UnInstall CSP doesn't work when invoked through a CAB or CPF and there's simply no workaround apart from choosing a different approach to uninstall the application. If having an executable was acceptable, then we could simply run the following native code:

LPCWSTR g_wszUninstallAppNameXml =
    L"<wap-provisioningdoc> "
    L"   <characteristic type=\"UnInstall\"> "
    L"      <characteristic type=\"AppName\"> "
    L"         <parm name=\"uninstall\" value=\"1\"/> "
    L"      </characteristic> "
    L"   </characteristic>"
    L"</wap-provisioningdoc>";

HRESULT UninstallAppName()
{
    HRESULT hr         = E_FAIL;
    LPWSTR wszOutput   = NULL;

    // Process the XML.
    hr = DMProcessConfigXML(g_wszUninstallAppNameXml, CFGFLAG_PROCESS, &wszOutput);

    // The caller must delete the XML returned from DMProcessConfigXML.
    delete [] wszOutput;

    return hr;
}

 

Or, for a managed approach:

string XmlUninst =  "<wap-provisioningdoc>\r\n" +
                    " <characteristic type=\"UnInstall\">\r\n" +
                    "  <characteristic type=\"AppName\">\r\n" +
                    "   <parm name=\"uninstall\" value=\"1\"/>\r\n" +
                    "  </characteristic>\r\n" +
                    " </characteristic>\r\n" +
                    "</wap-provisioningdoc>";

XmlDocument configDoc = new XmlDocument();
configDoc.LoadXml(XmlUninst);
ConfigurationManager.ProcessConfiguration(configDoc, false);

 

However, as I said, in this case a CAB was required.

We came up with a simple workaround: create a CAB (through a Smart Device CAB project) and include a SETUP.DLL in it, whose Install_Init event invokes the DMProcessConfigFile with the XML Provisioning targeting the UnInstall CSP. The code for such SETUP.DLL was the following -- it's the usual one with simply a customized Install_Init:

HINSTANCE g_hinstModule;

LPCWSTR g_wszUninstallAppNameXml = 
    L"<wap-provisioningdoc> "
    L"   <characteristic type=\"UnInstall\"> "
    L"      <characteristic type=\"AppName\"> "
    L"         <parm name=\"uninstall\" value=\"1\"/> "
    L"      </characteristic> "
    L"   </characteristic>"
    L"</wap-provisioningdoc>";

BOOL APIENTRY DllMain(
    HANDLE hModule, 
    DWORD  ul_reason_for_call, 
    LPVOID lpReserved
    )
{
    switch (ul_reason_for_call)
    {
        case DLL_PROCESS_ATTACH:
        case DLL_THREAD_ATTACH:
        case DLL_THREAD_DETACH:
        case DLL_PROCESS_DETACH:
            g_hinstModule = (HINSTANCE)hModule;
            break;
    }
    return TRUE;
}


// **************************************************************************
// Function Name: Install_Init
//
// Arguments:
//    IN HWND hwndParent  handle to the parent window
//    IN BOOL fFirstCall  indicates that this is the first time this function is being called
//    IN BOOL fPreviouslyInstalled  indicates that the current application is already installed
//    IN LPCTSTR pszInstallDir  name of the user-selected install directory of the application
//
// Return Values:
//    codeINSTALL_INIT
//    returns install status
//
// Description:  
//    The Install_Init function is called before installation begins.
//    User will be prompted to confirm installation.
// **************************************************************************
SETUP_API codeINSTALL_INIT Install_Init(
    HWND        hwndParent,
    BOOL        fFirstCall,
    BOOL        fPreviouslyInstalled,
    LPCTSTR     pszInstallDir
    )
{
    HRESULT hr         = E_FAIL;
    LPWSTR wszOutput   = NULL;

    // Process the XML.
    hr = DMProcessConfigXML(g_wszUninstallAppNameXml, CFGFLAG_PROCESS, &wszOutput);
    CHR(hr);

Error:
    // The caller must delete the XML returned from DMProcessConfigXML.
    if (wszOutput != NULL)
    {
        delete [] wszOutput;
    }
    if (hr == S_OK)
    {
        return codeINSTALL_INIT_CONTINUE;
    } 
    else
    {
        return codeINSTALL_INIT_CANCEL;
    }
}


// **************************************************************************
// Function Name: Install_Exit
// 
// Purpose: processes the push message.
//
// Arguments:
//    IN HWND hwndParent        handle to the parent window
//    IN LPCTSTR pszInstallDir  name of the user-selected install directory of 
//                              the application
//
// Return Values:
//    returns install status: codeINSTALL_EXIT
// **************************************************************************
SETUP_API codeINSTALL_EXIT Install_Exit(
    HWND    hwndParent,
    LPCTSTR pszInstallDir,      
    WORD    cFailedDirs,
    WORD    cFailedFiles,
    WORD    cFailedRegKeys,
    WORD    cFailedRegVals,
    WORD    cFailedShortcuts
    )
{
    return codeINSTALL_EXIT_DONE;
}


// **************************************************************************
// Function Name: Uninstall_Init
//
// Arguments:
//    IN HWND hwndParent  handle to the parent window
//    IN LPCTSTR pszInstallDir  name of the user-selected install directory of the application
//
// Return Values:
//    returns uninstall status: codeUNINSTALL_INIT
//
// **************************************************************************
SETUP_API codeUNINSTALL_INIT Uninstall_Init(
    HWND        hwndParent,
    LPCTSTR     pszInstallDir
    )
{
   return codeUNINSTALL_INIT_CONTINUE;
}


// **************************************************************************
// Function Name: Uninstall_Exit
//
// Arguments:
//    IN HWND hwndParent  handle to the parent window
//
// Return Values:   
//    returns uninstall status: codeUNINSTALL_EXIT
//
// **************************************************************************
SETUP_API codeUNINSTALL_EXIT Uninstall_Exit(
    HWND    hwndParent
    )
{
    return codeUNINSTALL_EXIT_DONE;
}

 

 

Cheers,

~raffaele

What if I have a problem on one device model only? I.e. the same code works like a charm on every Windows Mobile-based device but one... can Microsoft help me engaging the OEM who customized the platform (as they only know what they put in it)? Well... not really, because you should contact that OEM's Technical Support in order to troubleshoot the issue... However at the moment there's a well predictable "procedure" to do this, only for Premier Customers: this is the highest level of Support contract you can have with Microsoft, and TSANet is one of the added values Premier contracts gain. I personally tend to help developers independently on the contract level, however from my experience I can say that only TSANet gives us the assurance that the 3rd party will give us the attention we need.

TSANet Alliance is described here, and here it is the list of the current members: I do hope other companies will consider add their Technical Supports to it.

Cheers,

~raffaele

Sometimes it happens that a developer asks for suggestions about how to design an application from the very beginning so that it'll be power-efficient, and therefore I now have a list of links\suggestions that may be interesting to share...

DWORD PwrFlag, NameLength; 
TCHAR StateName[64] = { 0 }; 
GetSystemPowerState(StateName, NameLength, &PwrFlag); 
if(POWER_STATE_ON != PwrFlag) 
   //perform action only if display is on.
   //for example, in case of Home plugins:
   InvalidateRect(hPlugInWnd, NULL, TRUE);
  • Or the application may listen on Activity Timers through the State&Notification Broker to perform tasks only when necessary.
  • In some cases you may want to turn off only the display when performing some actions - I knew a solution based on ExtEscape API (see old Alex Feinmain's managed sample) and thought that this was supported for OEMs only, however looking at the official doc it's for ISV Application Developers as well (in any case, note that "[...] The device capabilities this function accesses must be implemented by an OEM.", thus meaning that you can see different behaviors on different devices):
HDC gdc = ::GetDC(NULL);
VIDEO_POWER_MANAGEMENT vpm;
vpm.Length = sizeof(VIDEO_POWER_MANAGEMENT);
vpm.DPMSVersion = 0x0001;
vpm.PowerState = VideoPowerOff;
 
// Power off the display
ExtEscape(gdc, SETPOWERMANAGEMENT, vpm.Length, (LPCSTR) &vpm, 0, NULL);
Sleep(5000); //just for demonstration purposes
vpm.PowerState = VideoPowerOn;
 
// Power on the display
ExtEscape(gdc, SETPOWERMANAGEMENT, vpm.Length, (LPCSTR) &vpm, 0, NULL);
::ReleaseDC(NULL, gdc);
  • You may want to have applications continue running when the device is suspended. This is simply not possible, as the processor is in idle state and doesn't offer any CPU cycle to be used by applications. Moreover, an application can't detect entering suspend state: "[...] While an application cannot detect the transition into SUSPEND mode, it is able to detect the transition from SUSPEND mode. It does this by calling the CeRunAppAtEvent function, which takes two parameters: a path and an event code" (from Power Management Features of Windows CE .NET). So if you really want to continue having the application running you can use CeRunAppAtTime() invoking SystemIdleTimerReset() every minute to prevent the device to suspend. You may turn the display off to save battery tough, at least. In such cases, I would encourage revisiting the architecture of the application. In many cases you can simply use CeAppRunAtTime() to perform a task: consider that the API will wake the device up if it's suspended. Note that if the device was suspended and waken up by CeRunAppAtTime it will be in a state between Power On and Suspend: you need to call SetSystemPowerState API to really turn the device on:
SetSystemPowerState(NULL, POWER_STATE_ON, POWER_FORCE)

 

I imagine there are many other techniques out there to save devices to drain battery... feel free to add whatever link or suggestions in a comment! Smile

 

Cheers,

~raffaele

- How to modify MulticabInstall SDK Sample

- Notes about Security

Recently I've been involved in an issue about silently installing mutiple CABs, so I think it may be worth mentioning its results here.

The OS component invoked when installing CABs is WCELOAD.EXE: it's the same if the CAB is created for XML Provisioning (through makecab.exe) or for installing an application (thruogh CabWiz.exe). It's for example responsible to check for certificates when the Security Policies dictate that unsigned CAB are not allowed to run. The version for Windows Mobile (5.0\6) is a bit different from the one for Windows CE 5.0, maily regarding available command-line parameters, check out:

Starting with Windows Mobile 5.0, only one running instance of WCELOAD.EXE is possible, therefore the nested CABs used with Windows Mobile 2003 are no longer possible. A solution to this is the MulticabInstall SDK sample: it basically allows the installation-chaining of CABs packaged into an UberCAB by looking at some registry keys. The sample contains a whitepaper that explains everything needed, I won't double it here.

However, what if I want all those inner CABs to be SILENTLY installed? For a single CAB, the solution is programmatically launch WCELOAD.EXE with /silent option (on Windows Mobile). So I simply looked for the function in the MulticabInstall SDK sample responsible for executing the CAB and found:

BOOL HostExec(LPCTSTR lpszFilePath, HANDLE *phProcess)
{
    BOOL bRet;
    SHELLEXECUTEINFO sei = {0};

    sei.cbSize = sizeof(sei);
    sei.nShow = SW_SHOWNORMAL;
    sei.lpFile = lpszFilePath;
    //sei.lpParameters = TEXT("/silent");  //WRONG APPROACH!!

    bRet = ShellExecuteEx(&sei);

    if (bRet)
    {
        *phProcess = sei.hProcess;
    }

    return bRet;
}

It uses the ShellExecuteEx() API to launch the application on the OS associated to files with .cab extension. I tried to set the parameter lpParameters of the SHELLEXECUTEINFO structure to "/silent", however this didn't work... So I looked at documentation for the SHELLEXECUTEINFO structure and found: "[...] If the lpFile member specifies a document file, this member should be NULL.". At this point I remembered what that sentence means: lpParameters was basically ignored since my lpFile was NOT an .EXE! Embarrassed

In order to continue using ShellExecuteEx (and not CreateProcess, for exampple), the code I came up with was simply:

BOOL HostExec(LPCTSTR lpszFilePath, HANDLE *phProcess)
{
    BOOL bRet;
    SHELLEXECUTEINFO sei = {0};

    sei.cbSize = sizeof(sei);
    sei.nShow = SW_SHOWNORMAL; 
    sei.lpFile = TEXT("\\Windows\\WCELOAD.EXE");

    TCHAR   szLaunchParms[MAX_PATH];
    StringCchPrintf(szLaunchParms,ARRAYSIZE(szLaunchParms), 
            TEXT("/silent \"%s\""), 
            lpszFilePath);
    sei.lpParameters = szLaunchParms;

    bRet = ShellExecuteEx(&sei);

    if (bRet)
    {
        *phProcess = sei.hProcess;
    }

    return bRet;
}

 

As a final note, consider that on WM5 there could be additional prompt regarding CAB installations, if the CAB is not signed with a certificate stored on the SPC store of the device.When using /noui (or /silent) then by default prompts are answered with 'Yes'. However, if the .cab file is unsigned (or signed with a certificate not stored on the device), any security-related prompts will default to 'No' for security reasons, and the installation might SILENTLY fail.

 

Cheers,

~raffaele

Posted