franksunny的个人技术空间
获得人生中的成功需要的专注与坚持不懈多过天才与机会。 ——C.W. Wendte

Symbian OS中的消息存储与常用操作

说明:本文前面消息的基本知识主要参考《Series60应用程序开发》中的有关内容,后面是前段做MTM开发中用到的代码。

一、消息存储基本知识

Symbian OS提供的消息传送架构基于Client/Server机制,服务器负责管理手机上的各种消息,在进行消息相关操作之前我们需要了解Symbian OS是如何组织和存储消息的。

手机中的各种消息都是以数据项(Entry)形式供程序操作,数据项有4种类型,SymbianOS为每种数据项提供了相应的常量标识UID,这些UID保存在msvuids.h文件中:

Ø         文件夹类型,,对应常量UIDKUidMsvFolderEntry,和PC上的文件系统一样,每个文件夹可以包含其它数据项也可能是其它数据项的子数据项。

Ø         消息类型,对应常量UIDKUidMsvMessageEntry,它表示该数据项是一条消息。

Ø         附件类型,对应常量UIDKUidMsvAttachmentEntry,它表示该数据项是某条信息的附件。

Ø         服务类型,对应常量UIDKUidMsvServiceEntry,服务数据项包含某个消息服务的配置信息,在一般情况还拥有通过该服务收发的消息数据项。  

除了上面提到的四种类型UID还有常用到的UIDKUidMsvRootEntrymsvids.h),它指的是根数据项,根数据项包含了4个标准文件夹数据项,分别是收件箱(KMsvGlobalInBoxIndexEntryId)、发件箱(KMsvGlobalOutBoxIndexEntryId)、草稿箱(KMsvDraftEntryId)和已发送项(KMsvSentEntryId),另外根数据项下面还包含有各种消息服务的服务项,Symbian OS中消息存储如下图所示:

Symbian OS中的消息服务器负责保存各种类型的数据项,这里有两个基本概念需要了解:消息存储和消息索引。消息存储保存了数据项的数据,保存的数据格式取决于消息服务,服务数据项使用消息存储保存服务配置信息,文件夹数据项不使用消息存储(即存储为空),Symbian 提供了CMsvStore类来访问数据项的消息存储;为了节省内存和快速检索消息,消息服务器把数据项的一些概要信息(标题,日期,类型,ID等)写到消息索引中,当消息服务器启动时将索引装载到RAM中直到消息服务器关闭,Symbian提供了TMsvEntry类表示数据项的索引。

操作消息常用的类和数据类型:

CMsvSession

该类代表客户端(客户端MTM、用户接口MTM或者客户端消息应用程序)与消息服务器端的通讯通道即C/S架构中客户端与消息服务器的会话。每一个客户端线程对应一个该类的实例,使用它获得下面将要提到的CMsvEntry上下文对象。一个消息客户端应用必须在正常使用任何MTMCMsvEntry对象前,使用OpenSyncL()或者OpenASyncL()来新建一个session对象。

CClientMtmRegistry

Registry掌握了客户端所有目前可用的MTM有关的细节,消息客户端可以使用该类获得从CBaseMtm继承的对象。

CBaseMtm

这个类主要用来操作消息条目,比如可以新建、修改、发送消息条目。

TMsvId

它只是一个TInt32typedef,消息服务器为每个数据项分配一个惟一的数值做为标识,除了上面提到的几个固定的标识,其它的标识都是动态分配的。想要对某个消息进行操作必须先得到它的IDSymbian中消息相关的大部分函数都会用到TMsvId

CMsvEntrySelection

CMsvEntrySelection是一个可以存储TMsvId的数组,在使用CMsvEntryCMsvServerEntry)的许多操作中都会做为参数或者返回对象。

TMsvEntry

上面已经提到过了它表示数据项的索引,只包含消息的一些概要信息,主要会用到Id()成员函数得到数据项的标识ID和公有数据成员iDetailsiDescriptioniDate,前面两个成员可以用来获取和设置索引的概要信息,iDate成员可以获取和设置数据项的日期及时间。

CMsvEntryCMsvServerEntry

CMsvEntryCMsvServerEntry可以理解为数据项的上下文(Context),这两个类非常类似,只不过CMsvEntry用于客户端,CMsvServerEntry用于实现消息的服务器端。它包含两个部分的功能:一是可以允许访问与这个entry关联的,不同类型的数据(比如可以根据指定ID定位数据项、获得消息存储和消息索引);二是运行访问它的子entry和父entry(当然对新得条目又可以进行一功能)。

CMsvStore

上面已经提到过它表示数据项的存储,可以通过CMsvEntryCMsvServerEntry)的 EditStoreL()ReadStoreL()函数取得可编辑存储或只读存储。 

 

二、数据项常用操作

因为消息处理建立C/S架构上,所以在消息操作之前,先要进行一些预处理,大致步骤如下:

1、通过消息会话类CMsvSession连接到消息服务器,建立会话。因为通常连接都采用异步方式,所以为了获取连接的事件通知,实例化CMsvSession对象的类需要继承自MMsvSessionObserver

2、构造客户端MTM注册表对象(通过CClientMtmRegistry::NewL(CMsvSession &aMsvSession)来实现)

3、构造具体的客户端MTM对象(通过CClientMtmRegistry::NewMtmL(TUid aMtmTypeUid)这里的MtmTypeUIDTMsvEntry内的消息类型ID是一致的)

4、接下来的操作就是根据具体的需求创建、编辑、验证、发送消息条目(如果只是创建和编辑消息条目,则不用如上这么复杂,可省略MTM对象构造)。

下面的消息操作使用了一个CMsvEntry CMsvServerEntry的指针对象,这两个类提供的功能基本一样,但有一部分函数名会不一样,可以查一下SDK

1 获得当前数据项索引和ID

TMsvEntry oldEntry = iServerEntry->Entry(); //这里iServerEntry应该是CMsvServerEntry

TMsvId oldContext = oldEntry.Id(); //如果使用CMsvEntry可以直接使用Id()

 

2 定位到指定数据项

在更换当前数据项之前通常先保存当前数据项索引ID,更换数据项并完成相关操作后再更换回原来的数据项,这可以避免影响其它函数,是一个很好的习惯。

TMsvId oldContext = iServerEntry->Entry().Id();

//使用SetEntry()更换当前数据项到root

iServerEntry->SetEntry(KMsvRootIndexEntryId);

//具体操作后更换回原来数据项

iServerEntry->SetEntry(oldContext);

 

3 查找数据项

下面的三个CMsvEntry成员函数都能完成在当前数据项下进行查找的功能:

CMsvEntrySelection* ChildrenWithMtmL(TUid aMtm) const;

根据消息服务(MTM)进行查找,查找消息索引对象(TMsvEntry)的成员iMtm等于aMtm的数据项ID

CMsvEntrySelection* ChildrenWithServiceL(TMsvId aId) const;

根据消息服务ID进行查找,查找消息索引对象(TMsvEntry)的成员iServiceId等于aId的数据项ID

CMsvEntrySelection* ChildrenWithTypeL(TUid aEntryType) const;

根据数据项类型进行查找,查找消息索引对角的(TMsvEntry)的成员iType等于aEntryType的数据项ID

CMsvServerEntry与之相对应的三个函数为GetChildrenWithMtm(), GetChildrenWithService(), GetChildrenWithType(),注意的一点是CMsvEntry的三个函数都返回一个CMsvEntrySelection对象的指针,使用完之后我们要负责释放,使用CMsvServerEntry的三个函数需要事先构造一个CMsvEntrySelection对象,用完之后也需要释放。

找出POP3邮箱个数的代码

iMsvEntry->SetEntryL( KUidMsgTypePop3 );

CMsvEntrySelection* sel = NULL;

sel = entry->ChildrenWithMtmL( KPkiSmtpTechnologyTypeUid );

TInt cnt = sel->Count();  //获得集合中数据项的个数

delete sel;

 

4 更改消息索引

TMsvEntry entry = iMsvEntry->Entry();

entry.iDetails.Set( _L( “New details” ) );

iMsvEntry->ChangeL( entry );  //把更改后的数据项索引写回消息索引中去(相当于commit提交)

  这里最后的ChangeL很重要,如果不进行该步调用,那么未将修改的消息索引写回到消息存储中。

5 数据项的读写

在进行数据项的读写之前需要使用EditStoreL()ReadStoreL()函数得到相应的存储CMsvStore通过它提供的接口进行操作。

void CMessageView::ViewMessageL(TMsvId aId)

{     

       // Construct the CMsvEntry

       CMsvEntry* entry = iSession->GetEntryL(aId);

       CleanupStack::PushL(entry);

       // Get the messaging store

       CMsvStore* store = entry->ReadStoreL();

       CleanupStack::PushL(store);

       // Construct the CRichText and restore the body text

       CParaFormatLayer* paraLayer = CParaFormatLayer::NewL();

       CleanupStack::PushL(paraLayer);

       CCharFormatLayer* charLayer = CCharFormatLayer::NewL();

       CleanupStack::PushL(charLayer);

       CRichText* body = CRichText::NewL(paraLayer, charLayer);

       CleanupStack::PushL(body);

 

       store->RestoreBodyTextL(*body);

       // Extract body text from CRichText

       TInt len = body->DocumentLength();  //get length

       HBufC *buf = HBufC::NewL( len );

       TPtr ptrBuf = buf->Des();

       body->Extract( ptrBuf, 0, len );  //get data

//因为不同的消息的存储格式不同,还可能需要对ptrBuf进行相应的解码才能正常

//显示

       delete buf;

       buf = NULL;

       CleanupStack::PopAndDestroy(5, entry);

}

 

三、关于SMS的一些特殊操作

1.读取消息条目

       //读取发件箱消息条目索引

       TMsvSelectionOrdering sort;

       //全部内容排序,包括隐藏

       sort.SetShowInvisibleEntries(ETrue);

       //设置入口为outbox,也就是发信箱

       CMsvEntry* entry = CMsvEntry::NewL(*iSession,KMsvGlobalOutBoxIndexEntryId,sort);

       CleanupStack::PushL(entry);

       //选择全部内容

       CMsvEntrySelection* entries = entry->ChildrenL();

       CleanupStack::PushL(entries);

       //读取消息索引信息,At(0)代表首信息,取其他的可以给出相应的index

       TMsvId aEntryId = entries->At(0);

       //得到消息索引的时间

       TTime time;

       time = entry->ChildDataL(aEntryId).iDate;

       iSmsMtm->SwitchCurrentEntryL(aEntryId);//iSmsMtmCSmsClientMtm类型的指针变量,它已经初始化

       iSmsMtm->LoadMessageL(); // load the message

       CRichText& body = iSmsMtm->Body(); //sms的内容存到CRichText控件对象中

       TPtrC msg(body.Read(0));

       //弹出对话框,提示短信内容

       CAknInformationNote* informationNote = new (ELeave) CAknInformationNote;

       informationNote->ExecuteLD(msg);

       CleanupStack::PopAndDestroy();

 

2.删除消息条目

       //删除草稿箱中的消息条目

       TMsvSelectionOrdering sort;

       sort.SetShowInvisibleEntries(ETrue);

       CMsvEntry* entry = CMsvEntry::NewL(*iSession,KMsvDraftEntryId,sort);

       CleanupStack::PushL(entry);

       CMsvEntrySelection* entries = entry->ChildrenL();

       CleanupStack::PushL(entries);

       TInt i = entries->Count();

       for(TInt ncount=0;ncount<i;ncount++)

       {

              entry->DeleteL(entries->At(ncount));

       }

       // information to the user

       iEikonEnv->InfoMsg(_L("DeleteAll Done!"));

       CleanupStack::PopAndDestroy(2);

 

3.创建消息条目

创建消息条目部分在收件箱和草稿箱两个地方是不一样的,具体代码如下:

       //在草稿箱中创建短信

       const TInt LEN = 12;

       //转入收件箱

       iSmsMtm->SwitchCurrentEntryL(KMsvDraftEntryId);

       //构建消息索引

       TMsvEntry newIndexEntry;

       newIndexEntry.iDate.HomeTime();

       newIndexEntry.iMtm = KUidMsgTypeSMS;

       newIndexEntry.iType = KUidMsvMessageEntry;

       //in 3rd edition crashes here if capabilities are wrong

       newIndexEntry.iServiceId = iSmsMtm->ServiceId();

       newIndexEntry.iDetails.Set(aName);//aName为收件人名称

       newIndexEntry.iDescription.Set(aContent.Left(LEN));

       newIndexEntry.SetUnread(ETrue);

       iSmsMtm->Entry().CreateL(newIndexEntry);

       TMsvId smsId = newIndexEntry.Id();

       iSmsMtm->SwitchCurrentEntryL(smsId);

       //填充消息存储区

       iSmsMtm->AddAddresseeL(aAddr);//aAddr为收件人号码,信息位于消息头中

       CRichText& body = iSmsMtm->Body();//消息正文

       body.Reset();

       body.InsertL(0, *iSmsContext);

       //提交保存

       iSmsMtm->SaveMessageL();

注:当然如果要发送的短信,还需要进行具体的设定,这部分将在发送消息里面详述,这里只是做为普通的创建消息,与收件箱中消息进行比较,具体见下面代码示例。

 

       //在收件箱中创建短信

       const TInt LEN = 12;

       //转入收件箱

       iSmsMtm->SwitchCurrentEntryL(KMsvGlobalInBoxIndexEntryId);

       //构建消息索引

       TMsvEntry newIndexEntry;

       //newIndexEntry.iDate.HomeTime();//收件箱短信索引头在CSmsHeader中设置

       newIndexEntry.iMtm = KUidMsgTypeSMS;

       newIndexEntry.iType = KUidMsvMessageEntry;

       //in 3rd edition crashes here if capabilities are wrong

       newIndexEntry.iServiceId = iSmsMtm->ServiceId();

       newIndexEntry.iDetails.Set(aAddr);

       newIndexEntry.iDescription.Set(aContent.Left(LEN));

       newIndexEntry.SetUnread(ETrue);

       iSmsMtm->Entry().CreateL(newIndexEntry);

       TMsvId smsId = newIndexEntry.Id();

       iSmsMtm->SwitchCurrentEntryL(smsId);

       //填充消息存储区

       //CSmsHeader& header = iSmsMtm->SmsHeader();

       //header.SetFromAddressL(aAddr);//这里我采用了两种方法,均不能正确设置发件人号码

       //iSmsMtm->AddAddresseeL(aAddr);//后来才知道这里为收件人号码,所以必须在以后修改

       CRichText& body = iSmsMtm->Body();

       body.Reset();

       body.InsertL(0, *iSmsContext);

       //提交保存

       iSmsMtm->SaveMessageL();

       //完善消息头,设置发件人号码和发送时间

       CMsvStore* messageStore = iSmsMtm->Entry().EditStoreL();

       CleanupStack::PushL( messageStore );

       CSmsHeader* hdr = CSmsHeader::NewL( CSmsPDU::ESmsDeliver, body );

       CleanupStack::PushL( hdr );

       hdr->SetFromAddressL(iNumber);

       TTime nowTime;

       nowTime.HomeTime();

       hdr->Deliver().SetServiceCenterTimeStamp(nowTime);

       hdr->StoreL(*messageStore);      

       messageStore->CommitL();

       CleanupStack::PopAndDestroy(hdr);    

       CleanupStack::PopAndDestroy(messageStore);

    // 修改当前消息索引为只读,这样收件箱列表处浏览会有回复选项

       //但是如果在之前就设置ReadOnly就会导致SaveMessageL出错 

       newIndexEntry.SetReadOnly(ETrue);

       //消息索引提交更改

       iSmsMtm->Entry().ChangeL(newIndexEntry);

4.发送消息条目

其实发送消息可以使用客户端MTM方法,但是一般都是在活动对象中实现,或者实现Send-As APICSendAppUi类来实现。以下代码简单给出客户端Mtm的方法,具体可以下载诺基亚论坛liuxg大大编写的SmsSendTest例子,在这里给出其下载连接http://www.cppblog.com/Files/franksunny/SmsSendTest.rar

       //发送消息

       //iMtm是在新建sms阶段设定

       TMsvEntry msvEntry = iMtm->Entry().Entry();

       //重新设定TMsvEntry

       msvEntry.iDetails.Set(iRecipient->Des());  // set recipient info in details

       msvEntry.SetInPreparation(EFalse);         // set inPreparation to false

       msvEntry.SetSendingState(KMsvSendStateWaiting);   // set the sending state (immediately)

       msvEntry.iDate.HomeTime();                        // set time to Home Time

       // 得到sms内容

       CRichText& mtmBody = iMtm->Body();

       mtmBody.Reset();

       mtmBody.InsertL(0, KGDSMSTag);   //插入我们的短信内容

       // 使用CSmsClientMtm类处理sms

       CSmsClientMtm* smsMtm = STATIC_CAST(CSmsClientMtm*, iMtm);

       smsMtm->RestoreServiceAndSettingsL();

       //CSmsHeader封装sms消息的参数,像服务中心号码和发送设定

       CSmsHeader& header = smsMtm->SmsHeader();

       //CSmsSettings类用来详细设定sms Header

       CSmsSettings* sendOptions = CSmsSettings::NewL();

       CleanupStack::PushL(sendOptions);

       sendOptions->CopyL(smsMtm->ServiceSettings());

       sendOptions->SetDelivery(ESmsDeliveryImmediately);//设定立即发送

       header.SetSmsSettingsL(*sendOptions);

       //检查服务中心号码有效性

       if(header.Message().ServiceCenterAddress().Length() == 0)

       {

              // 如果没有设定,则查找默认中心号码

              CSmsSettings* serviceSettings = &(smsMtm->ServiceSettings());

              //中心号码列表为空

              if(!serviceSettings->NumSCAddresses())

              {

                   // 错误消息

                     iEikonEnv->InfoWinL(_L("No service center number"),_L("cannot send this one."));

              }

             else

              {

                     // 设定为默认服务中心号码

                     CSmsNumber* sc = 0;

                     sc = &(serviceSettings->SCAddress(serviceSettings->DefaultSC()));

                     header.Message().SetServiceCenterAddressL(sc->Address());

              }

       }

       CleanupStack::PopAndDestroy();

      ... ...

       CMsvEntrySelection* selection = new (ELeave) CMsvEntrySelection;

       CleanupStack::PushL(selection);

       selection->AppendL(movedId); // 添加我们要发送的smsmovedId在省略部分有定义,是TMsvId型变量

       // 调用的这个函数是用于发送的,具体的代码后面介绍

       SetScheduledSendingStateL(selection);  

       CleanupStack::PopAndDestroy(); // selection

       return ETrue; // 到这里sms已被发送

---------------------------------------------------------------------------------------------------

---------------------------------------------------------------------------------------------------

       void SetScheduledSendingStateL(CMsvEntrySelection* aSelection)

       {

              CBaseMtm* smsMtm = iMtm;

              // 添加entry到任务列表

              TBuf8<1> dummyParams;

              CCommandAbsorbingControl::NewLC();

              CMsvOperationWait* waiter = CMsvOperationWait::NewLC();

              waiter->Start();

              // 这个函数是关键

              CMsvOperation* op= smsMtm->InvokeAsyncFunctionL(

                            ESmsMtmCommandScheduleCopy,

                            *aSelection,

                            dummyParams,

                            waiter->iStatus);

              CleanupStack::PushL(op);

              CActiveScheduler::Start(); //开始时间表中任务

              CleanupStack::PopAndDestroy(3); // waiter, op, CCommandAbsorbingControl

       }

 

SMS操作补充

在项目开发过程中会时不时爆发些问题,为此一旦遇到问题后得以解决都会在这里进行知识点与实践的积累

关于CSmsClientMtm::LoadMessageL

在调用CSmsClientMtm::LoadMessageL之前,必须调用SwitchCurrentEntryL(TMsvId aId)SetCurrentEntryL(CMsvEntry* aEntry)将客户端Mtm转到当前项,然后才能顺利调用,由于有时短信在Sim卡上,故调用时可能会产生KErrAccessDenied-21)的错误,这种情况下我们通常采用如下代码实现

TInt error = KErrNone;

for (TInt i; i<KNumRetries; ++i)

{

    TRAP(error, iSmsMtm->LoadMessageL());

    if (error == KErrAccessDenied) // something is still busy with this entry

    {

        User::After(KDelayPeriod);

    }

    else

    {

        break;

    }

}

User::LeaveIfError(error);

后来还发现一种情况,就是如果当前的消息是彩信,则在正确使用如上代码的时候,在调用CSmsClientMtm::LoadMessageL会遇到KErrNotFound-1)的错误,这个问题还是需要值得注意一下的。

关于短信时间问题

在短信中用到的时间,都是UTC时间,也即总所周知的GMT时间,系统的短信程序在显示时间是会将这个UTC时间转化为手机当前所设时区的本地时间,在发送短信的时候,因为存在草稿箱和发件箱中的短信只有一个时间,所以问题不大,我们只要在创建短信时调用TMsvEntry::iDate.UniversalTime()函数即可。

在收件箱中的短信则有两个时间,一个跟上面一样,简单表示接受时的本地时间,而另外一个时间则在信息详情中才能看到,这是服务中心的时间,也即短信具体何时经服务中心发出到手机的时间。我们在测试过程中发现,中国移动在099月份,所有139号段发过来的短信在这个服务中心时间上存在错误,其GMT时间变成了北京时间,其时区变成了格林尼治的零时区。当时因为这个问题还花了一天时间来进行测试验证。

另外画蛇添足下,有一个UTC时间转为本地时间的简单函数如下:

#include <tz.h>

#include <tzconverter.h> //tzclient.lib

void ConvertTimeToLocal(TTime& aTime)

{

       RTz tzServer;

       User::LeaveIfError(tzServer.Connect());

       CleanupClosePushL(tzServer);

       CTzConverter* tzConverter = CTzConverter::NewL(tzServer);

       CleanupStack::PushL(tzConverter);

       tzConverter->ConvertToLocalTime(aTime);

       CleanupStack::PopAndDestroy(2);

}

关于客户端MTM对象读取和发送注意事项

在工程代码中发现当只实例化一个CSmsClientMtm对象,而要用该对象进行读取短信和发送短信操作时,未调用读取操作时,发送正常,但是一旦调用读取短信操作,就会在语句CSmsClientMtm::AddAddresseeL设置收件人号码时报-5KErrNotSupported)功能不支持的错误提示。

具体原因是为什么暂不知道,只是看到newlc论坛有该问题的解决方案,采用创建两个客户端MTM对象的方法来实现,具体代码是

iSmsMtmR = static_cast(iMtmReg->NewMtmL(KUidMsgTypeSMS)); // used for receive

iSmsMtmS = static_cast(iMtmReg->NewMtmL(KUidMsgTypeSMS)); // used for send

验证的确可以,如果要看原帖具体地址则在http://nioulk.newlc.com/topic-10736

 

 

致谢:本文其实本人并非原创,只是将Beover1984的原文《Symbian OS中的消息存储与常用操作》和网上流传的《SMS故事》做了学习后,结合工作中的实际而整理得到,非常感谢以上两位作者的指点。

 

 





本文有一个图片没有显示出来,但是word文档中是有的,为此给出word文档的下载地址:http://www.cppblog.com/Files/franksunny/SymbianOSMessage.rar
posted on 2008-07-30 21:04 frank.sunny 阅读(3214) 评论(2)  编辑 收藏 引用 所属分类: symbian 开发

FeedBack:
# re: [整理]Symbian OS中的消息存储与常用操作
2009-06-20 11:18 | svyee
非常感谢~~  回复  更多评论
  
# re: [整理]Symbian OS中的消息存储与常用操作
2011-03-28 17:26 | lovelycheep
非常感谢楼主讲解的这么详细,但是我还是有个问题想向LZ请教一下 关于CSmsClientMtm::LoadMessageL 还是有点问题的

我在TRAP(error, iSmsMtm->LoadMessageL());之后error还是0,没出现问题,但是我就是取不到信息内容,一下是我的代码
TInt error = KErrNone;
TRAP(error, iSmsMtm->LoadMessageL());

CRichText& richText= iSmsMtm->Body();
inboxStore->RestoreBodyTextL(richText);
const TInt length = richText.DocumentLength();

length返回值总是0,研究了两天了,还没搞懂,请LZ赐教……  回复  更多评论
  

只有注册用户登录后才能发表评论。
网站导航: 博客园   IT新闻   BlogJava   知识库   博问   管理



常用链接

留言簿(13)

随笔分类

个人其它博客

基础知识链接

最新评论

阅读排行榜

评论排行榜