Sulla Programmazione

Quattro chiacchere sulla programmazione e sulle bit-tecnologie con Fabrizio Cipriani

Invio delle informazioni di un contatto via SMS con Windows Mobile 5.0

Qualche giorno fa mia moglie mi chiede di inviargli il numero di telefono di una persona via SMS. Benissimo, dico, vado sul mio prode smartphone il quale monta un Windows Mobile 5.0, apro la pagina del contatto e scelgo "Invia via SMS", facil....

Mi fermo interdetto quando noto che nel contatto non esiste una opzione "Invia via SMS". Inspiegabilmente Microsoft ha deciso di non includere questa funzionalità in Windows Mobile 5.0.

Faccio una veloce verifica trovando che la funzionalità è stata aggiunta nel Windows Mobile 5.0 Messaging and Security feature pack, e successivamente in Windows Mobile 6.0.

Ma la Samsung non ha distribuito aggiornamenti o feature pack per il mio SGH I320... Va bene, vediamo quant'è complicato svilupparlsela da soli.

Una estensione dei menu in Windows Mobile deve essere scritta come COM server. Vedremo quindi come scriverne uno in C++ che aggiunge una voce "Invia via SMS" nel menu della pagina del contatto, e che prepara un SMS con le sue informazioni.

Potete scaricare qui: SendContactInfoCab.CAB (27,53 kb) il file di installazione del programma. Per installarlo è sufficiente copiare il CAB sul telefonino, e dal telefonino attivarlo.

I sorgenti del progetto (Microsoft Visual Studio 2008) invece li trovate qui: SendContactInfo.zip (3,65 mb)

1) Registrazione del componente

Come già detto, una estensione dei menu di Pocket Outlook deve essere implementato come componente COM server. Occorre quindi registrare il nostro server nel sistema e far sapere alla shell di Windows Mobile che vogliamo che sia invocato al momento di visualizzare il menu di una particolare sezione di Pocket Outlook.

Le chiavi del registry da creare sono due:

  • la prima dentro HKCR\CLSID, nella quale registriamo il clsid del COM server e la sottochiave "InprocServer32" contenente il percorso della .dll.

  • La seconda deve indicare quale dei menu all'interno di Windows Mobile vogliamo estendere. La chiave per i menu della pagina del contatto è ContextMenus\Contacts\Card_Menu, da aggiungere al percorso HKLM\Software\Microsoft\Shell\Extensions. Al suo interno definiamo il clsid del nostro COM server, in modo che la shell sappia chi chiamare al momento di estendere il menu.

Qui trovate i valori da aggiungere al registry per estendere gli altre menu di Windows Mobile:

http://msdn2.microsoft.com/en-us/library/ms879952.aspx (menu overview)

Inseriamo il codice per la registrazione delle chiavi di registry nella funzione DllRegisterServer, chiamata al momento della registrazione della DLL in fase di installazione:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
#define CLSIDTEXT_MENUEXT TEXT("5DEE67BE-6708-4703-921A-D88F27643A8D")

#define REGKEY_EXTENSIONS_CLSID TEXT("CLSID\\{") 
CLSIDTEXT_TESTEXT _T("}")

STDAPI DllRegisterServer(void)   
{   
  HRESULT hr;   
  HKEY hKeyCLSID = NULL;   
  DWORD dwDisposition = 0;
  hr = RegCreateKeyEx(HKEY_CLASSES_ROOT,   
    REGKEY_EXTENSIONS_CLSID,  NULL,  TEXT(""),   
    REG_OPTION_NON_VOLATILE,  KEY_ALL_ACCESS, NULL,   
    &hKeyCLSID, &dwDisposition);

  CHR(hr);

  // ...Altre chiavi del registry
Error:   
  return SUCCEEDED(hr);

}

2) Creazione ed attivazione dell'estensione

La shell di windows mobile si aspetta che il componente COM server (la nostra DLL) crei ed attivi l'oggetto che fornisce i servizi desiderati. In particolare richiede che gli si ritorni un oggetto che implementa l'interfaccia IClassFactory per poterlo attivare chiamando il metodo CreateInstance:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
STDAPI DllGetClassObject ( REFCLSID rclsid,   
  REFIID riid, void** ppObject )   
{   
  HRESULT hr = S_OK;   
  SendContactInfoClassFactory *pFactory = NULL;
  if (rclsid == CLSID_SENDCONTACTINFO_MENUEXT)   
  {   
    pFactory = new SendContactInfoClassFactory;   
    CPR(pFactory);   
    hr = pFactory->QueryInterface(riid, ppObject);   
  }   
}

HRESULT SendContactInfoClassFactory::QueryInterface (   
  REFIID riid, void** ppObject )   
{   
  HRESULT hr = S_OK;
  if (riid == IID_IUnknown || riid == IID_IClassFactory)   
  {   
    *ppObject= (IClassFactory*)this;   
  }   
  else   
  {   
    CHR(E_NOINTERFACE);   
  }   
  AddRef();

Error:   
  return(hr);   
}

ed infine, chiamata dalla shell:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
HRESULT SendContactInfoClassFactory::CreateInstance (   
  IUnknown *pUnkOuter, REFIID riid, void** ppObject )   
{   
  HRESULT hr = S_OK;   
  IObjectWithSite* pows = NULL;   
  *ppObject = NULL;
  if(pUnkOuter != NULL)   
  {   
    CHR(CLASS_E_NOAGGREGATION);   
  }

  hr = SendContactInfoMenuExtension::Create(&pows);   
  CHR(hr);
  hr = pows->QueryInterface(riid, ppObject);   
  CHR(hr);

Error:   
  RELEASE_OBJ(pows);   
  return hr;   
}

Come vedete, CreateInstance() crea l'estensione del menu SendContactInfoMenuExtension attraverso il metodo statico Create, e lo ritorna alla shell.

3) Implementazione dell'estensione

Bene, abbiamo attivato l'estensione. Diamo un'occhiata più da vicino a SendContactInfoMenuExtension.

Questa classe implementa le interfacce IContextMenu e IObjectWithSite. I tre metodi più importanti della classe sono: SetSite (IObjectWithSite), QueryContextMenu e InvokeCommand (IContextMenu).

Il primo, SetSite(), viene chiamato dalla shell per passare il proprio "site", un puntatore IUnknown che può essere usato per richiedere alla shell dei puntatori agli oggetti usati nel contesto corrente, in questo caso le informazioni sul contatto selezionato.

Il secondo, QueryContextMenu(), viene chiamato quando l'utente apre un menu, e serve per aggiungere le nuovi voci:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
HRESULT STDMETHODCALLTYPE   
  SendContactInfoMenuExtension::QueryContextMenu(  
    HMENU hmenu, UINT indexMenu,   
    UINT idCmdFirst, UINT idCmdLast,   
    UINT uFlags)   
{   
  HRESULT hr = S_OK;   
  BOOL bRet;

  m_idMenu = idCmdFirst;

  bRet = InsertMenu(hmenu, indexMenu,   
    MF_BYPOSITION | MF_STRING,   
    m_idMenu, c_szSendContactInfoMenu);

  CBR(bRet);

Error:   
  return(hr);   
}

Con la chiamata a InsertMenu() inseriamo la nostra nuova voce (il testo della voce è in c_szSendContactInfoMenu).

4) Operazioni da effettuare quando si seleziona la nuova voce di menu

Il secondo metodo, InvokeCommand(), viene chiamato quando l'utente seleziona una delle voci di menu aggiunta dal nostro componente. 

Visto che vogliamo fare qualcosa con le informazioni del contatto correntemente visualizzato, vediamo cosa fare per ottenere l'id del contatto.

Usiamo il puntatore IUnknown passatoci da SetSite(): eseguiamo QueryInterface() richiedendo un puntatore ad un IDataObject, una interfaccia utilizzata per trasferire dati tra client e server in ambito COM, il quale ci permette poi di trasferire i dati relativi al contatto corrente:

hr = m_pSite->QueryInterface(IID_IDataObject, (void**)&pobj);

pobj ora contiene un oggetto che implementa l'interfaccia IDataObject, che nel contesto corrente fornisce informazioni sul contatto visualizzato.

Ottenere informazioni con IDataObject è un pò macchinoso: occorre popolare una struttura FORMATETC specificando che vogliamo informazioni secondo il formato clipboard CFNAME_ITEMREFARRAY (un array di references), in quantità DVASPECT_CONTENT (solo quelle essenziali), e che le informazioni devono essere passate attraverso un handle a della memoria globale (HGLOBAL):

1
2
3
4
5
6
7
STGMEDIUM med = {0};   
FORMATETC form = {0};   
form.cfFormat = RegisterClipboardFormat(CFNAME_ITEMREFARRAY);   
form.lindex = -1;   
form.dwAspect = DVASPECT_CONTENT;   
form.tymed = TYMED_HGLOBAL;   
hr = pobj->GetData(&form, &med);

med ora contiene le informazioni richieste, non bisogna fare altro che estrarle dal formato clipboard specificato:

1
2
3
ItemRefArray *pIra; pIra = (ItemRefArray*)med.hGlobal;   
CEOID oidContact;   
oidContact = (CEOID) pIra->rgRefs[0].pRef;

A questo punto, avendo l'id del contatto, lo possiamo passare alla nostra funzione che prepara l'SMS.

5) Preparazione dell'SMS

Questa è la parte più semplice. Per ottenere informazioni su un contatto del quale abbiamo l'id, occorre prima attivare Pocket Outlook ed effettuare il logon.

Essendo anche Pocket Outlook un COM server, si attiva con una chiamata a CoCreateInstance:

1
2
3
4
5
6
IPOutlookApp2 *polApp = NULL;

hr = CoCreateInstance(CLSID_Application,   
  NULL, CLSCTX_INPROC_SERVER,   
  IID_IPOutlookApp2, (LPVOID *) &polApp);   
hr = polApp->Logon(NULL);

Ci facciamo ritornare un puntatore all'oggetto IItem corrispondente all'id del contatto attraverso il metodo GetItemFromOidEx():

hr = polApp->GetItemFromOidEx(oidContact, 0, &pItem);

E ci facciamo passare i valori di una serie di proprietà delle quali specifichiamo il contact property id:

1
2
3
4
5
6
7
8
CEPROPID rgPropIDs[7] = { PIMPR_DISPLAY_NAME,   
  PIMPR_HOME_TELEPHONE_NUMBER,   
  PIMPR_MOBILE_TELEPHONE_NUMBER,   
  PIMPR_BUSINESS_TELEPHONE_NUMBER };

hr = pItem->GetProps(rgPropIDs,   
  CEDB_ALLOWREALLOC, ARRAYSIZE(rgPropIDs),   
  &pVals, &cbBuffer, heap);

pVals contiene ora i valori delle proprietà. Una volta concatenate le informazioni nella variabile szBodyMessage, è sufficiente una chiamata alla funzione API MailComposeMessage() per aprire una nuova finestra di composizione SMS contenente il testo da noi specificato:

1
2
3
4
5
6
MAILCOMPOSEFIELDS mcf = {0};   
mcf.cbSize = sizeof(mcf);   
mcf.dwFlags = MCF_ACCOUNT_IS_TRANSPORT;   
mcf.pszAccount = TEXT("SMS");   
mcf.pszBody = szBodyMessage;   
MailComposeMessage(&mcf);

Ed ecco fatto. Naturalmente è ancora necessario decidere il destinatario del messaggio, ma questo può essere fatto tranquillamente dalle funzioni standard di windows mobile.

Il risultato è questo, il menu con la voce in più:

..e la finestra di composizione di un nuovo SMS con le informazioni del contatto:

Comments