GPS Programming Tips for Windows Mobile - Part 1
NETCF: Memory leak... now what??
Supporting Kiosk-Applications on Windows Mobile (Technically achievable vs. supported)
Wireless Programming on Windows Mobile: supported or not supported?
Establishing GPRS Connection on Windows CE and Windows Mobile: Sample Codes
Disable WebBrowser's Context-Menu in NETCF applications
MAPI on Windows Mobile 6: Programmatically retrieve mail BODY (sample code)
Microsoft released a HotFix for NETCF v3.5 on Windows Mobile 6.1.4 onwards, to address basic functionalities of WebBrowser control
The right approach to get a Contact’s last communication (IItem’s PIMPR_SMARTPROP)
Remote Desktop Mobile (RDP Client) disconnects after 10 minutes of inactivity
Support Boundaries for Windows Mobile Programming (Developing Drivers, for example... Or even WiFi Programming)
Miei post in italiano sul team-blog del Supporto Tecnico agli Sviluppatori
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);
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);
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!
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!
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.
- MAPI: the wrong approach (yet interesting sample code)
- WebDAV: DELETE command via HTTP
- Customize Pocket Outlook menu items
One of the features that Windows Mobile users frequently asked to the Product Group had always been the native ability to empty the Deleted Items folder on the server which the device is sync-ed to. Finally Windows Mobile 6-based devices have such ability when synchronizing with Exchange 2007 (Menu\Tools\Empty Deleted Items):
The truth is that it's still plenty of Windows Mobile 5.0 devices (and Exchange 2003) in the real world, and many of their users are looking for a way to reach the goal... The only available way seemed to be to access OWA from the device's web browser and here empty the folder: this is not acceptable in many cases for real-world scenarios. In other cases you may have found software to be installed server-side exposing such function to device-based clients. ... I've been working with some colleagues on this and I think it's worth mentioning here what we found...
When started researching about the goal, I came across the MAPI (Messaging API) IMAPIFolder::DeleteMessages. Wow! Precisely the function I need, I said to myself. Unfortunately this wasn't true (BECAUSE I NEEDED A DEVICE-SIDE APPLICATION), but at least I spent some time on MAPI programming, which is really powerful after you understand how it works... (and admittedly it's not really user-friendly...). So I ended up with an application that empties the Deleted Items folder, but only the one ON THE DEVICE, because you can't synchronize back it to the Exchange server. Interestingly, I hadn't to develop the whole code because portions of it were already available on the web (see for example "Practical use of MAPI") or in the WM6 SDK Samples (specifically, windows mobile 6 sdk\Samples\Common\CPP\Win32\InboxMenuExtensibility, which I'm going to talk about in a bit). Note that if you use the same MAPI on a server-side application (e.g. a Web Service exposing a Web Method that devices can invoke), then you would reach the goal: that's basically how the Blackberry Exchange-connector works, for example, when invoked from the device.
The working sample I came up with is the following: it's not thoughtfully tested and I can't say it's error-free (or that it's the best code to achieve the result), but it does what it was meant for -- empty the Deleted Items folder (in its case, ON THE DEVICE). I copied it to the end of the post.
As I said, since the Deleted Items folder is not sync-ed back and forth between server and device, the MAPI-based approach is not the correct one. The one that works, IF WEBDAV IS AN ALLOWED WEB SERVICE EXTENSION OF THE IIS Front-End Exchange SERVER, it's through WebDAV. The idea is very simple: allow a client application to access the mail-folders via HTTPS and send them some commands such as
DELETE /pub2/folder1/folder2 HTTP/1.1 Host: hostname Content-Length: 0
Basically I'm talking about an empty HTTP request (programmatically managed through a HttpWebRequest object, for example, in a .NET application) whose command is DELETE and whose argument is the path of the Deleted%20Items folder. There are many sample codes available at the doc Deleting Items (WebDAV). I'm copying the C# code here to add few comments and to show how simple it is:
using System; using System.Net; namespace ExchangeSDK.Snippets.CSharp { class DeletingItemsWebDAV { [STAThread] static void Main(string[] args) { System.Net.HttpWebRequest Request; System.Net.WebResponse Response; System.Net.CredentialCache MyCredentialCache; string strSourceURI = "http://server/public/TestFolder1/test.txt"; //In this sample user credentials are hard-coded for didactic purposes string strUserName = "UserName"; string strPassword = "!Password"; string strDomain = "Domain"; try { // Create a new CredentialCache object and fill it with the network // credentials required to access the server. // we won't pass the credentials in clear-text... MyCredentialCache = new System.Net.CredentialCache(); MyCredentialCache.Add( new System.Uri(strSourceURI), "NTLM", new System.Net.NetworkCredential(strUserName, strPassword, strDomain) ); // Create the HttpWebRequest object. Request = (System.Net.HttpWebRequest)HttpWebRequest.Create(strSourceURI); // Add the network credentials to the request. Request.Credentials = MyCredentialCache; // Specify the DELETE method. Request.Method = "DELETE"; // Send the DELETE method request. Response = (System.Net.HttpWebResponse)Request.GetResponse(); // Close the HttpWebResponse object. Response.Close(); Console.WriteLine("Item successfully deleted."); } catch(Exception ex) { // Catch any exceptions. Any error codes from the DELETE // method request on the server will be caught here, also. Console.WriteLine(ex.Message); } } } }
The power of WebDAV is that it's independent on the client, so for example you can run all your tests from a DESKTOP console. Moreover, to check if there's any server-side issue, you can use the Microsoft Download Microsoft Exchange Server Public Folder DAV-based Administration Tool.
Exchange servers' administrators may argue to us (mobile developers ) that WebDAV is less secure. Let them read for example the doc at Authentication and Security Using WebDAV: it states that by using SSL, WebDAV is as secure as OWA.
Ok, now that you know how to achieve the goal, you may wonder how to integrate this new functionality on the system's Pocket Outlook (tmail.exe). It may be a bit tricky, but there's a complete NATIVE sample showing that in the SDK (so you'll need to use the native sample code of the Deleting Items (WebDAV) page): windows mobile 6 sdk\Samples\Common\CPP\Win32\InboxMenuExtensibility. This sample needs to be modified in some parts and may require a bit of knowledge on COM programming, but it's fully documented. It's basically a DLL COM Server loaded by tmail.exe (so it may require to be signed with a Priv Certificate), which ultimately invokes InsertMenu API to insert the menu item you want. The sample is more complex than strictly required in our case, as it also shows how to modify the menu items collection depending on the current context (i.e. if a message is selected or viewed).
Cheers!
P.S. The MAPI-based approach:
#include "stdafx.h" #include <windows.h> #include <commctrl.h> #include <initguid.h> #include <pimstore.h> #include <mapiutil.h> HRESULT hr = E_FAIL; LPMAPISESSION m_pSession; ////////////////////////////////////////////////////////////////////////////// //Functions declarations HRESULT CreateEntryList(SRowSet *pRows, ENTRYLIST **ppList); HRESULT DeleteMessages(IMsgStore* pStore); HRESULT GetWastebasketForFolder(LPMAPIFOLDER pFolder, LPMAPIFOLDER* ppfldrWastebasket); ULONG CountMessagesInFolder(LPMAPIFOLDER pFolder); HRESULT CreateEntryList(SRowSet *pRows, ENTRYLIST **ppList); ////////////////////////////////////////////////////////////////////////////// //Macros #define _ErrorLabel Error #define CHR(hResult) \ if(FAILED(hResult)) { hr = (hResult); goto _ErrorLabel;} #define CPR(pPointer) \ if(NULL == (pPointer)) { hr = (E_OUTOFMEMORY); goto _ErrorLabel;} #define CBR(fBool) \ if(!(fBool)) { hr = (E_FAIL); goto _ErrorLabel;} #define ARRAYSIZE(s) (sizeof(s) / sizeof(s[0])) #define RELEASE_OBJ(s) \ if (NULL != s) \ { \ s->Release(); \ s = NULL; \ } //////////////////////////////////////////////////////////////////////////////// //MAIN int _tmain(int argc, _TCHAR* argv[]) { SRowSet *prowset = NULL; IMAPITable* ptbl = NULL; IMsgStore* pStore = NULL; // Warn user that the messages will be permanently deleting int iResult = ::MessageBox(GetActiveWindow(), TEXT("Are you sure you want to permanently delete all messages in Deleted Items ON THE DEVICE?"), TEXT("Permanently Delete Items"), MB_YESNO | MB_ICONWARNING | MB_DEFBUTTON2); // If IDYES - then continue / else exit method now hr = (iResult == IDYES) ? S_OK : E_FAIL; CHR(hr); //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); 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; ASSERT (prowset->aRow[0].cValues == spta.cValues); SPropValue *pval = prowset->aRow[0].lpProps; ASSERT (pval[0].ulPropTag == PR_DISPLAY_NAME); ASSERT (pval[1].ulPropTag == PR_ENTRYID); //Windows Movile has 2 MAPI Message Stores: "ActiveSync" and "SMS" 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); //Empty Deleted Items folder hr = EmptyDeletedMessages(pStore); CHR(hr); } } //finishing off... hr = m_pSession->Logoff(0, 0, 0); Error: //make sure we don't leak memory FreeProws(prowset); RELEASE_OBJ(ptbl); RELEASE_OBJ(pStore); RELEASE_OBJ(m_pSession); MAPIUninitialize(); return hr; } /////////////////////////////////////////////////////////////////////////////// // EmptyDeletedMessages - Takes the Message Store as argument and empties its // Deleted Items folder HRESULT EmptyDeletedMessages(IMsgStore* pStore) { hr = E_FAIL; ULONG cMsgCount = 0; IMAPIFolder* pFolder = NULL; IMAPIFolder* pWasteBasket = NULL; IMAPITable *pTable = NULL; ULONG cbEntryId = 0; ENTRYID *pEntryId = NULL; ULONG ulObjType = 0; SRowSet *pRows = NULL; LPENTRYLIST lpmsgEntryList = NULL; // First retrieve the ENTRYID of the Inbox folder of the message store 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); // Be sure it's a folder ASSERT(ulObjType == MAPI_FOLDER); // Get the Deleted Items Folder (because it may be that current view is on wahtever folder hr = GetWastebasketForFolder(pFolder, &pWasteBasket); CHR(hr); CPR(pWasteBasket); // Get contents of the folder pWasteBasket->GetContentsTable(MAPI_UNICODE, &pTable); // We only care about the EntryID. SizedSPropTagArray (1, spta) = {1, PR_ENTRYID}; pTable->SetColumns((SPropTagArray *)&spta, 0); BOOL done = FALSE; // We need to know how many messages there are cMsgCount = CountMessagesInFolder(pWasteBasket); while(cMsgCount > 0) { // 10 is the max number of rows which QueryRows can return int cCurrentQuery = 0; if (cMsgCount > 10) { cCurrentQuery = 10; } else { cCurrentQuery = cMsgCount; } hr = pTable->QueryRows(cCurrentQuery, 0, &pRows); CHR(hr); // Did we hit the end of the table? if (pRows->cRows == 0) { break; } // Since we are in a loop here and we are re-using lpmsgEntryList // we may need to free the memory from the previous iteration first // In the case of the last iteration or Error - this will be freed at Error: MAPIFreeBuffer(lpmsgEntryList); lpmsgEntryList = NULL; // Otherwise - Get the List of EntryIDs from the RowSet as an EntryList // Note: the resulting will EntryList be dependant on the RowSet so don't free until later hr = CreateEntryList(pRows, &lpmsgEntryList); CHR(hr); // permanently delete the items hr = pWasteBasket->DeleteMessages(lpmsgEntryList, NULL, NULL, NULL); CHR(hr); cMsgCount -= pRows->cRows; } Error: RELEASE_OBJ(pFolder); RELEASE_OBJ(pWasteBasket); RELEASE_OBJ(pTable); FreeProws(pRows); MAPIFreeBuffer(pEntryId); return hr; } //////////////////////////////////////////////////////////// // CountMessagesInFolder - takes a folder as argument and returns // the # of messages in it ULONG CountMessagesInFolder(LPMAPIFOLDER pFolder) { hr = E_FAIL; IMAPIProp *pProp = NULL; ULONG rgTags[] = {2, PR_CONTENT_COUNT, PR_FOLDER_TYPE}; ULONG cValues = 0; 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); Error: // return the COUNT of messages in this folder return rgFolderProps[0].Value.ul; } //////////////////////////////////////////////////////////// // GetWastebasketForFolder // Independently on the currently selected folder, it returns // the related Deleted Items folder HRESULT GetWastebasketForFolder(LPMAPIFOLDER pFolder, LPMAPIFOLDER* ppfldrWastebasket) { hr = E_FAIL; IMsgStore* pms = NULL; ULONG cItems; ULONG rgtagsFldr[] = { 1, PR_OWN_STORE_ENTRYID }; ULONG rgtagsMsgStore[] = { 1, PR_IPM_WASTEBASKET_ENTRYID }; LPSPropValue rgprops = NULL; // This method assumes that the CALLER already logged on to a MAPISession if (!m_pSession) CHR(E_FAIL); // Now request the PR_OWN_STORE_ENTRYID on the folder. This is the // ENTRYID of the message store that owns the folder object. hr = pFolder->GetProps((LPSPropTagArray)rgtagsFldr, MAPI_UNICODE, &cItems, &rgprops); CHR(hr); CBR(PR_OWN_STORE_ENTRYID == rgprops[0].ulPropTag); // Now open the message store object. hr = m_pSession->OpenEntry(rgprops[0].Value.bin.cb, (LPENTRYID)rgprops[0].Value.bin.lpb, NULL, 0, NULL, (LPUNKNOWN*)&pms); CHR(hr); MAPIFreeBuffer(rgprops); rgprops = NULL; // Get the ENTRYID of the wastebasket for the message store hr = pms->GetProps((LPSPropTagArray)rgtagsMsgStore, MAPI_UNICODE, &cItems, &rgprops); CHR(hr); // Now open the correct wastebasket and return it to the caller. CBR(PR_IPM_WASTEBASKET_ENTRYID == rgprops[0].ulPropTag); hr = m_pSession->OpenEntry(rgprops[0].Value.bin.cb, (LPENTRYID)rgprops[0].Value.bin.lpb, NULL, 0, NULL, (LPUNKNOWN*)ppfldrWastebasket); CHR(hr); Error: MAPIFreeBuffer(rgprops); RELEASE_OBJ(pms); return hr; } //////////////////////////////////////////////////////////// // CreateEntryList // Needed because IMAPIFolder::DeleteMessages works on EntryLists HRESULT CreateEntryList(SRowSet *pRows, ENTRYLIST **ppList) { hr = E_FAIL; ENTRYLIST* plist = NULL; // How much space do we need to create this entry list? ULONG cbNeeded = sizeof(SBinaryArray) + (pRows->cRows * (sizeof(SBinary))); // Allocate one buffer to hold all the data for the list. hr = MAPIAllocateBuffer(cbNeeded, (LPVOID*)&plist); CHR(hr); CPR(plist); // Set the number of items in the EntryList plist->cValues = pRows->cRows; // Set plist->lpbin to the place in the buffer where the array items will be // filled in BYTE* pb; pb = (BYTE*)plist; pb += sizeof(SBinaryArray); plist->lpbin = (SBinary*) pb; // Loop through the list setting the contents of the EntryList to the contents // of the incoming SRowSet for (int cItems = 0; cItems < (int)plist->cValues; cItems++) { plist->lpbin[cItems].cb = pRows->aRow[cItems].lpProps[0].Value.bin.cb; plist->lpbin[cItems].lpb = pRows->aRow[cItems].lpProps[0].Value.bin.lpb; // Track our memory usage pb += sizeof(SBinary); } // Make sure that we didn't write off the end of our buffer... CBR(pb <= ((BYTE*)plist + cbNeeded)); Error: if (FAILED(hr)) { MAPIFreeBuffer(plist); plist = NULL; } *ppList = plist; return hr; }