在托管应用程序中接收 SMS 消息(转)
http://www.microsoft.com/china/msdn/library/langtool/vsdotnet/ReceivSMSMessages.mspx?mfr=true
在托管应用程序中接收 SMS 消息
Maarten Struys
PTS Software
适用于:
Microsoft® .NET Compact Framework 1.0
Microsoft® Visual Studio® .NET 2003
下载接收 SMS 示例。
摘要:在 本文中,我们说明了一种在托管应用程序中接收 SMS 消息的方法,而 SMS 消息不会出现在收件箱中并且当 SMS 消息到达时不显示弹出通知。示例代码将运行在 Pocket PC 2003 Phone Edition 设备上。只要稍加修改(主要在 MMI 部分中),它应该也可以运行在 Smartphone 2003 中。
本页内容
简介 | |
使用和扩展现有的 MAPI 规则示例 | |
向收件箱注册 MailRuleClient 对象 | |
注销 MailRuleClient 对象 | |
客户端应用程序与 MAPI 规则客户端之间的通信 | |
客户端处理以捕获 SMS 消息 | |
构建示例代码 | |
测试示例代码 | |
缺点 | |
小结 | |
关于作者 |
简介
尽管 .NET Compact Framework 版本 1.0 提供了非常强大的功能,但是它对电话功能的支持却很少。使用 P/Invoke,可以相当容易地访问 Phone API 和 SIM API,如文章“Accessing Phone APIs from the Microsoft .NET Compact Framework” 中所描述的那样。对于 Windows Mobile 2003 软件来说,如果 SMS 消息没有出现在收件箱中,则没有直接的 API 可用来立即处理托管应用程序内接收的 SMS 消息。SDK 文档引用 IMailRuleClient 接口来处理应用程序内的传入消息。MAPI 规则客户端 (MAPI Rule Client) 是可以实现 IMailRuleClient 接口的 COM 对象。注册时,收件箱应用程序将会加载 MAPI 规则客户端。注册后,传入的 SMS 消息会传递给 MAPI 规则客户端,由它决定如何处理该传入消息。
图 1 - 收件箱和 IMailRuleClient 之间的关系
Smartphone 2003 SDK 和 Pocket PC 2003 SDK 中随付有众多代码示例。其中一个示例实现了简单的邮件规则客户端。它位于 Pocket PC 2003 SDK 的子文件夹“\POCKET PC 2003\Samples\Win32\Mapirule”中。对设备进行注册后,该示例 MAPI 规则客户端会接收所有 SMS 消息,并将其传递给收件箱(包含字符串“zzz”的消息除外)。这些消息由 MAPI 规则客户端本身进行处理,以在 MessageBox 中显示消息以及发件人的电话号码,然后删除 SMS 消息。
使用和扩展现有的 MAPI 规则示例
要 创建自己的 MAPI 规则客户端,mapirule 示例是一个非常棒的切入点。如果可以在 mapirule 示例与托管应用程序之间创建一个接口,甚至可以方便地从托管应用程序内访问 SMS 消息。因为 mapirule 示例包含接收 SMS 消息并在处理后删除消息的完整功能,所以唯一需要我们做的事情就是将 SMS 消息从 mapirule 示例传递到托管应用程序中。为此,可以利用几个简单的本机功能来扩展 mapirule 客户端示例,从而将 SMS 消息传递到调用客户端。为了这个目的,本文附带的可下载源代码包含了一个 mapirule 示例的修改版本。在 mapirule 示例代码中,需要修改方法 CMailRuleClient::ProcessMessage。该方法是 MAPI 规则客户端的核心,它接收 SMS 消息并对消息进行处理,或将其传递到收件箱。下面的代码片段显示了在 mapirule.cpp 中原始的 ProcessMessage 方法。
HRESULT CMailRuleClient::ProcessMessage(IMsgStore *pMsgStore,
ULONG cbMsg,
LPENTRYID lpMsg,
ULONG cbDestFolder,
LPENTRYID lpDestFolder,
ULONG *pulEventType,
MRCHANDLED *pHandled)
{
HRESULT hr = S_OK;
SizedSPropTagArray(1, sptaSubject) = { 1, PR_SUBJECT};
SizedSPropTagArray(1, sptaEmail) = { 1, PR_SENDER_EMAIL_ADDRESS};
ULONG cValues = 0;
SPropValue *pspvSubject = NULL;
SPropValue *pspvEmail = NULL;
IMessage *pMsg = NULL;
HRESULT hrRet = S_OK;
// Get the message from the entry ID
hr = pMsgStore->OpenEntry(cbMsg,
lpMsg,
NULL,
0,
NULL,
(LPUNKNOWN *) &pMsg);
if (FAILED(hr)) {
RETAILMSG(TRUE, (TEXT("Unable to get the message!\r\n")));
goto Exit;
}
// For SMS, the subject is also the message body
hr = pMsg->GetProps((SPropTagArray *) &sptaSubject,
MAPI_UNICODE,
&cValues,
&pspvSubject);
if (FAILED(hr)) {
RETAILMSG(TRUE, (TEXT("Unable to get the message body!\r\n")));
goto Exit;
}
// get the sender's address or phone number
hr = pMsg->GetProps((SPropTagArray *) &sptaEmail,
MAPI_UNICODE,
&cValues,
&pspvEmail);
if (FAILED(hr)) {
RETAILMSG(TRUE, (TEXT("Couldn't get the sender's address!\r\n")));
goto Exit;
}
//
// ***** START REPLACE WITH CODE FROM LISTING 2 *****
//
// Here we filter the message on some predetermined string.
// For sample purposes here we use "zzz". What happens when the
// filter condition(s) are met is up to you. You can send
// WM_COPYDATA messages to other app windows for light IPC,
// send an SMS message in response, or whatever you need to do.
// Here, we just play a sound and show the message in a
// standard message box.
if (wcsstr(pspvSubject->Value.lpszW, L"zzz") != NULL)
{
MessageBeep(MB_ICONASTERISK);
MessageBox(NULL,
pspvSubject->Value.lpszW,
pspvEmail->Value.lpszW,
MB_OK);
// Delete the message and mark it as handled
// so it won't show up in Inbox
hr = DeleteMessage(pMsgStore,
pMsg,
cbMsg,
lpMsg,
cbDestFolder,
lpDestFolder, pulEventType, pHandled);
} else {
// a 'normal' message, pass it on
*pHandled = MRC_NOT_HANDLED;
}
//
// ***** END REPLACE CODE *****
//
// Clean up
Exit:
if (pspvEmail) {
MAPIFreeBuffer(pspvEmail);
}
if (pspvSubject) {
MAPIFreeBuffer(pspvSubject);
}
if (pMsg) {
pMsg->Release();
}
return hr;
}
清单 1:原始的 CMailRuleClient::ProcessMessage 示例代码。
解释每 个函数调用以及 ProcessMessage 内部的工作原理已经超出了本文的讨论范围,但需要读者理解这样一个关键点:只要接收到 SMS 消息,收件箱应用程序就会利用消息入口调用 ProcessMessage,该消息入口包含已接收到的 SMS 消息的所有信息。在 ProcessMessage 内调用的所有方法,在 Pocket PC 2003 SDK 附带的 Pocket PC 2003 帮助文件中有详细地解释。利用下面的代码片段替换突出显示的代码行,通过将包含特殊前缀 (zzz) 的所有已接收到的 SMS 消息传递给客户端应用程序,我们能够修改示例。
以下代码片段显示的代码替换了在清单 1 中标记的代码。
// Here we filter the message on some predetermined string. For sample
// purposes here we use "zzz". What happens when the filter condition(s)
// are met is up to you. You can send WM_COPYDATA messages to other
// app windows for light IPC, send an SMS message in response,
// or whatever you need to do.
if (wcsstr(pspvSubject->Value.lpszW, L"zzz") != NULL)
{
if (g_hSmsAvailableEvent != NULL)
{
// We have received an SMS message that needs to be send to our client.
// Since we run in the process space of Inbox, we are using a memory
// mapped file to pass the message and phone number to our client,
// that typically runs in another process space (therefor we can not
// simply copy strings). We protect the memory mapped file with a Mutex
// to make sure that we are not writing new SMS data while the reading
// client is still processing a previous SMS message.
WaitForSingleObject(g_hMutex, INFINITE);
lstrcpy(g_pSmsBuffer->g_szPhoneNr, pspvEmail->Value.lpszW);
lstrcpy(g_pSmsBuffer->g_szMessageBody, pspvSubject->Value.lpszW);
ReleaseMutex(g_hMutex);
SetEvent(g_hSmsAvailableEvent);
}
// Delete the message and mark it as handled so it won't show up in Inbox
hr = DeleteMessage(pMsgStore,
pMsg,
cbMsg,
lpMsg,
cbDestFolder,
lpDestFolder, pulEventType, pHandled);
}
else
{
// a 'normal' message, pass it on
*pHandled = MRC_NOT_HANDLED;
}
清单 2:将所有 SMS 消息传递给客户端进程的替换代码。
向收件箱注册 MailRuleClient 对象
在 我们能够使用 MAPI 规则客户端之前,我们需要将其注册为 COM 对象,同时将它的类标识符添加到下面的注册表项中:HKEY_CLASSES_ROOT\CLSID\。为了使收件箱意识到 MAPI 规则客户端的存在,我们还必须将它的类标识符写入下面的注册表项中: HKEY_LOCAL_MACHINE\Software\Microsoft\Inbox\Svc\SMS\Rules。
为了设置这两个 注册表项,从而完整地注册我们的 COM 对象以便它可以使用,mapirule 示例中包含了函数 DllRegisterServer。可以从要利用 MAPI 规则客户端的任意客户端应用程序调用该函数。因为本文将利用托管的客户端应用程序,所以我们从托管应用程序内使用 P/Invoke 来调用函数 DllRegisterServer。在执行该函数后,所有已接收到的 SMS 消息将会通过 MAPI 规则客户端进行路由,如图 1 所示。在本文提供的示例代码中,我们将包含“zzz”的所有已接收到的 SMS 消息传递给客户端进程,但可以根据任意特定的触发器筛选出特定的 SMS 消息(例如,根据电话号码或特定消息内容进行筛选,或者简单地传递所有消息)。
注销 MailRuleClient 对象
在 客户端应用程序完成捕获 SMS 消息后,注销 MAPI 规则客户端(至少)与注册该对象同样重要。如果忽略该步骤,在我们完成了自己的应用程序后,SMS 消息将不会出现在收件箱中。为了注销 MAPI 规则客户端,我们调用函数 DllUnregisterServer。该函数会删除调用函数 DllRegisterServer 时设置的注册表项。在托管客户端应用程序中,可以使用 P/Invoke 完成这项工作。因为收件箱并不能动态地得到注册表中更改的通知,所以该设备还必须进行软重置。请参阅本文结尾的“缺点”一节。
客户端应用程序与 MAPI 规则客户端之间的通信
建 立客户端应用程序与 MAPI 规则客户端(运行在收件箱的进程空间中)之间的通信有几种方法。除了通信机制外,实现我们这里正在讨论的进程间通信也非常重要。这一点立即就会得到证实。 因为每个 Windows CE 进程都具有它们自己的内存空间,所以不可能简单地将内存指针从一个进程传递到另一个进程。为了能够将 SMS 消息内容传递给客户端应用程序,我们要利用内存映射文件。MAPI 规则客户端在构造函数中创建内存映射文件,并且在析构函数中销毁它。此外,还要创建一个事件来指示 SMS 消息可用于客户端应用程序。
CMailRuleClient::CMailRuleClient()
{
m_cRef = 1;
// Setup a memory mapped file so we can pass SMS messages between this
// server and a listening client.
g_hMMObj = CreateFileMapping((HANDLE)-1,
NULL,
PAGE_READWRITE,
0,
MMBUFSIZE,
TEXT("SmsBuffer"));
assert(g_hMMObj != NULL);
g_pSmsBuffer = (PSMS_BUFFER)MapViewOfFile(g_hMMObj,
FILE_MAP_WRITE, 0, 0, 0);
if (g_pSmsBuffer == NULL) {
CloseHandle(g_hMMObj);
}
assert(g_pSmsBuffer != NULL);
// Create an event to inform the client about a pending SMS message
g_hSmsAvailableEvent = CreateEvent(NULL,
FALSE,
FALSE,
_T("SMSAvailableEvent"));
assert(g_hSmsAvailableEvent != NULL);
}
清单 3:建立与客户端通信的资源。
了解对于单独的进程而言,所有句柄都是本地句柄这一点也非常重要。例 如,无法在 MAPI 规则客户端和客户端应用程序之间共享 SMSAvailableEvent 的句柄,即使客户端应用程序的某些部分是在宿主 MAPI 规则客户端功能的 DLL 内实现也是如此。
客户端处理以捕获 SMS 消息
为了能够在托管应用程序中捕获所有发送的 SMS 消息,我们要在托管应用程序(只需 P/Invokes 一个等待事件被设置的本机函数)内创建辅助线程。这种异步回调到托管代码的机制在标题为“Asynchronous callbacks from native Win32 code”的这篇 MSDN 文章中有详细的说明。
当 CMailRuleClient::ProcessMessage 接收到一条 SMS 消息时,它会将该消息内容复制到内存映射文件中,并且它会设置一个事件以等待线程变为活动线程。我们使用内存映射文件在两个独立的进程之间传输数据。一旦 设置了该事件,消息内容会再次复制到托管数据类型,此后,托管应用程序就可以处理接收到的消息了。托管的辅助线程完成处理后,它会回调给本机函数以等待另 一个 SMS 消息变为可用。因为我们一次只能处理一个 SMS 消息,所以立即对该示例进行简化。为了确保正确地处理同时接收到的 SMS 消息,我们必须实现某种类型的排队机制。为了使示例简单且易于理解,我们决定在提供的代码中忽略对 SMS 消息进行排队的内容。但是,提供的示例是捕获 SMS 消息很棒的切入点,并且简单地对其进行修改就能够处理同时接收到的 SMS 消息。
下列函数包含在 MAPI 规则客户端 DLL 中。在另一个 SMS 消息可用之前,该函数会被阻塞。然后,该函数将 SMS 消息内容传递给可以进一步处理这些消息的托管应用程序。
//////////////////////////////////////////////////////////////////////////////
// SMSMessageAvailable is used to pass data to the caller, whenever the
// worker thread has data available.
//
// SMSMessageAvailable can be called from unmanaged code or via P/Invoke
// from managed code.
//////////////////////////////////////////////////////////////////////////////
EXTERN_C BOOL SMSMessageAvailable (wchar_t *lpDestination, wchar_t *lpPhoneNr)
{
WaitForSingleObject(g_hClientEvent, INFINITE);
if (g_pClientSmsBuffer != NULL) {
WaitForSingleObject(g_hClientMutex, INFINITE);
lstrcpy(lpPhoneNr, g_pClientSmsBuffer->g_szPhoneNr);
lstrcpy(lpDestination, g_pClientSmsBuffer->g_szMessageBody);
ReleaseMutex(g_hClientMutex);
} else {
*lpPhoneNr = '\0';
*lpDestination = '\0';
}
return *lpPhoneNr != '\0';
}
清单 4:等待 MAPI 规则客户端 DLL 内 SMS 消息的客户端应用程序。
托管的客户端应用程 序创建一个辅助线程,可以简单地注册为接收 SMS 消息并输入 while 循环。在 while 循环内,调用的 SMSMessageAvailable 函数存在于本机代码中,包含在 MAPI 规则客户端 DLL 内。在这个简单的示例中,接收到的 SMS 消息在宿主窗体的列表框中显示。下面的代码片段显示 CaptureSMS 客户端应用程序内的托管辅助线程。
/// <summary>
/// This is the worker thread of the SMSListener class. After construction
/// of the object this class runs continuously, until SMSListener is disposed.
/// The worker thread P/Invokes into unmanaged code to SMSMessageAvailable.
/// Inside this function, execution is blocked until new (asynchronous) data
/// (an SMS message) is available. If data is it is simply passed to a
/// listbox, using Control.Invoke.
/// </summary>
private void CheckForData()
{
StringBuilder sbSms = new StringBuilder(255);
StringBuilder sbPhoneNr = new StringBuilder(255);
UnmanagedAPI.SMSMessageAvailable();
while (! bDone)
{
// P/Invoke into a native function that actually blocks until an
// SMS message is available
if (UnmanagedAPI.GetSMSData(sbSms, sbPhoneNr)
{
MessageBox.Show(sbSms.ToString(), sbPhoneNr.ToString());
if (sbSms.Length != 0)
{
parentForm.ReceivedSMSMessage = sbPhoneNr.ToString() +
" - " + sbSms.ToString();
parentForm.Invoke(new EventHandler(parentForm.UpdateListBox));
}
}
}
}
清单 5:接收 SMS 消息的托管辅助线程。
在该辅助线程中,有一个特别的事情需要注意。当接收到 SMS 消息时,辅助线程要在由 parentForm 所拥有的列表框中显示该消息。在拥有特定控件的线程内部更新控件时,可能导致出现意外的问题。因此,辅助线程应该绝对不要直接更新用户界面控件。相反,它 应该使用窗体的 Invoke 方法,传递一个委托以使拥有窗体的线程代表辅助线程更新 UI 控件。
构建示例代码
提供的示例代码由两个独立的项目组成。mapirule 项目包含所有本机代码,包括捕获 SMS 消息的收件箱的 MAPI 规则客户端插件。该项目必须使用嵌入式 Visual C++ 4.0(可以免费下载)构建。因为本机代码是特定于处理器的,所以请确保针对正确的平台编译 mapirule。CaptureSMS 项目只包含托管代码。为了能够构建 CaptureSMS,您需要使用 Visual Studio.NET 2003。因为托管代码与处理器无关,所以在构建 MSClient 时,无需指定特定的处理器类型。但是,为了确保 mapirule 客户端 DLL 与 CaptureSMS 一起部署,我们使用 Visual Studio.NET 2003 内的解决方案资源管理器,将 mapirule.dll 作为内容添加到 CaptureSMS 中。因为现在我们将与处理器相关的代码添加到与处理器无关的解决方案中,所以您必须明确示例代码将要在其上运行的目标处理器。
测试示例代码
在成功地构建了 mapirule 和 CaptureSMS 后,有两种不同的方法可以对应用程序进行实际的测试。第一种方法,将应用程序部署到实际的 SmartPhone 2003 设备或 PocketPC 2003 Phone Edition 设备上,然后使用另外一部移动电话将 SMS 消息发送到目标设备上。在应用程序发布之前,应该实际地执行这种测试。在开发的初始阶段,有一种简单、低开销的方法可以对代码进行测试。只是选择带有虚拟 无线电支持的模拟器作为目标系统即可。为了能够将 SMS 消息发送到模拟器目标,可以使用一个名为 Call Events 的独立应用程序。该应用程序的示例代码可以在标题为“Creating call events in an emulation environment”的这篇文章中找到。在安装完附带的软件后,启动 Call Events 应用程序,您就可以使用该应用程序将 SMS 消息从桌面发送到一个 Windows 移动模拟器中。请确保使用带有虚拟无线电的模拟器作为 SMSClient 客户端应用程序的目标。在启动 CaptureSMS 应用程序后,可以使用 Call Events 将 SMS 消息发送给它。图 2 显示操作中的托管应用程序和 Call Events 应用程序。
图 2 - 将 SMS 消息发送给 PocketPC 2003 Phone Edition 模拟器
缺点
mapirule 示例的文档列出了在卸载 mapirule.dll 后,设备必须被软重置的问题。这可能与特定的注册表项在设备的初始化期间具有只读属性这一事实有关。在安装 mapirule.dll 时也有相同的问题。只要在安装 mapirule.dll 之前没有接收到任何 SMS 消息,本文中说明的示例应用程序就会运行良好。但是,如果在运行该应用程序之前,在设备上已经接收到 SMS 消息,那么在对设备进行软重置前,mapirule 客户端将不会捕获 SMS 消息。
小结
使 用提供的代码示例,您可以相当容易地捕获 SMS 消息,并在客户端应用程序中进行处理(即使客户端应用程序是以托管代码编写的)。当 .NET Compact Framework 版本 2 发布时,在应用程序中处理 SMS 消息很可能会变得更加简单,但是有了这个示例,不必等到 v2 发布您就可以利用 SMS 消息了。在企业应用程序内使用 SMS 消息会相当地有趣。例如,通过下面的方法可以维护库存清单:定义特定的消息协议,使移动设备上的应用程序根据特定的 SMS 消息采取措施。对这样的方案进行说明超出了本文的讨论范围,但是提供的示例代码是将智能设备上托管应用程序内的 SMS 消息与电话功能相结合的良好起点。
关于作者
Maarten Struys 公职于 PTS Software,他负责实时和嵌入式能力中心的工作。Maarten 是一位经验丰富的 Windows (CE) 开发人员,从 Windows CE 出现的那天起,他就一直致力于这方面的工作。自 2000 年以来,Maarten 就在 .NET 环境中使用托管代码。他还是荷兰有关嵌入式系统开发领域的两家权威杂志的自由撰稿人。他最近开设了一个 Web 站点,用来提供有关嵌入式环境中 .NET 的信息,网址为 www.dotnetfordevices.com。Maarten 将托管代码和非托管代码与 Windows CE .NET 4.1 结合在一起使用,专业地评估了 .NET Compact Framework 的实时行为。有关该主题的白皮书,荣获了今年早些时候 Microsoft 颁发的 WinHEC 2003 Whitepaper Award。