Android2.1消息应用(Messaging)源码学习笔记之二
在上一篇学习笔记中,我从整理构成应用的主要组件开始,学习了相关的源代码,基本了解了Activity、Service、Rec eiver等组件的主要 职责和任务,接下来我将对重点功能的实现方法进行深入学习,了解它们的实现逻辑、涉及哪些API调用,以及是否与相关协议符合等各个方面的情况。
新消息呼入时系统的 响应过程及方法
Messaging应用对新消息呼入事件的 响应, 有3个配置选项可以由用户自己做出选择:
A.铃声 :用户可以选择已有 铃声/或者静音作为新消息呼入的提醒 ;
该选项的配置是在MessagingPreferenceActivity.RingtonePreference中完成的,它通过ringtoneType属性将系统中的可用声音分为:①铃声(ringtone)、②通知(notification)、③警报(alarm)3种类型,指定类型后最终会通过RingtoneManager. ACTION_RINGTONE_PICKER选取到具体的 声音资源。
用户指定铃声被播放的关键在MessagingNotification工具类中,当有新消息时它的updateNewMessageIndicator()方法会被调用,该方法对 Notification. sound属性做了设置,使得铃音 的 最终得以 播放。当然在该方法中还包括了对状态栏图标、震动效果的处理逻辑。
而铃音被“适时 ”播放的关键是:MM会在TransactionService的update方法(你是否还记得那个观察者模式,该方法是具体业务完成后的回调 )中针对NOTIFICATION_TRANSACTION和RETRIEVE_TRANSACTION两种业务调用 MessagingNotification 工具类 ,而SMS则是在SmsReceiverService的handleSmsReceived()方法中进行调用。
B.震动 :开启后有新消息呼入时会震动提醒。 响应过程 和铃声的 响应 机制完全一样,重点都在 MessagingNotification的 updateNewMessageIndicator()方法中 ;
C.状态栏通知 :用户可以决定是否在状态栏上显示一个代表新消息的小图标, 响应过程 和铃声的 响应 机制基本一样 ,但它的触发不止是在信息呼入时,还会在信息发送失败、设备重新启动等情况下被调用,当然不同情况在状态栏会有不同的icon。
Messaging应用初始化以及简单的Cache系统
Messaging应用定义了自己的Application对象,详见 AndroidManifest.xml中的
public void onCreate() { super.onCreate(); PreferenceManager.setDefaultValues(this, R.xml.preferences, false); MmsConfig.init(this);//读取系统配置参数 ContactInfoCache.init(this);//启动联系人信息缓存系统 Contact.init(this);//初始化联系人工具类 DraftCache.init(this);//初始化草稿缓存系统 Conversation.init(this);//初始化会话对象访问工具 DownloadManager.init(this);//初始化下载管理器 RateController.init(this);//初始化进度控制器 DrmUtils.cleanupStorage(this);//Drm本地数据清理 LayoutManager.init(this);//初始化部件管理器 SmileyParser.init(this);//初始化SMIL解析器 }
其中除了全局工具类MmsConfig、LayoutManager、SmileyParser等对象的初始化之外,其余部分都是Cache系统的启动,本节将初步分析这些Cache系统的功能作用以及实现方法:
- ContactInfoCache联系人信息缓存 :init方法其实是呼叫私有构建器,创建了该类的全局单实例对象。该类的职责是缓存联系人数据表中的查询结果,并提供相关方法访问联系人的常用信息,例如:显示名称等。该类中的getContactInfo()方法是非常有用的方法,你可以以给点的电话号码或E-mail地址作为参数查询联系人的常用信息,并且查询结果是被缓存下来的。与该类配合使用的还有Contact类,它同样内置了缓存系统,还有代表多个联系人的联系人列表对象ContactList,它也提供了很多方便使用的方法,用于组织联系人的基本信息,以及调用RecipientIdCache类的初始化方法,进一步向Cache系统丰富联系人的详细信息。
- DraftCache草稿信息缓存 :init方法将触发构建器的调用,然后调用rebuildCache()方法,这会查询数据库,将其中所有草稿信息的会话ID全部缓存起来,供外部程序使用。
其它执行初始化的全局工具类,我会在后面相关环节谈及 。
会话Conversation的概念及其实现方法
会话Conversation是一种新的信息组织形式,不同于“ 传统 功能 ”以发件箱\收件箱\草稿箱,等 文件夹的 方式来 组织信息,会话会把上下文相关的“往<--->来”信息组织在一起,以方便用户查看管理。所谓上下文相关是指:若某条信息是对另一条信息的‘回复 ’,则认为它们是 上下文相关的。
Messaging应用的首页就是会话列表页面——ConversationList,它列出了用户所有往来 信息,及其未发出的草稿信息,这些内容都来自于ConversationListAdapter适配器。
与会话列表相关的查询操作封装在com.android.mms.data.Conversation类中,它是查询会话信息的接口,并在必要时创建新会话(一个全新信息还未有对应的上下文信息时 )。该类在应用启动的第一时刻就已经用一个后台线程 开始 加载会话列表了(加载到Cache中):
MmsApp.onCreate()方法调用了 Conversation.init()方法,而该初始化方法是实现如下:
public static void init(final Context context) { new Thread(new Runnable() { public void run() { cacheAllThreads(context); } }).start(); }
cacheAllThreads()方法是关键,它将从content://mms-sms/conversations/位置查询数据,并在查询过程中标示了正在loading的状态——mLoadingThreads=true,它将查询出来的数据构建成Conversation对象放入Cache(或更新已存在于Cache中的对象),最后清理掉在Cache中存在,而未存在于查询结果中存在的 Conversation对象 (即清理无效的会话数据)。
然而令我奇怪的是,listView中显示的数据并非来自Cache中,而始于异步查询工具 AsyncQueryHandler的子类 ThreadListQueryHandler 它以异步方式查询并得到会话列表,该查询开始于对 Conversation. startAsyncQuery()方法的调用。查询完成后, 在 mQueryHandler中 回 调了mListAdapter.changeCursor(cursor) ,而此时才真正为 Adapter注入了有效的 Cursor对象 。
草稿信息也会在会话列表中出现,因为在 保存 草稿信息时 , WorkingMessage .saveDraft()会最终呼叫到 Conversation. ensureThreadId方法,它会保证在数据表 (mmssms.db中的threads表) 中创建对应的会话记录,使其正确的显示在会话列表页面。
接收者编辑器RecipientsEditor的实现方法
一条信息可以发送给1到多个 目标 用户——它们被称为“接收者 ” ,以电话号码、E-Mail地址等形式存在。 “接收者 ” 通常是从“联系人应用”中选择或由用户直接输入。
在Messaging应用中,用于输入 接收 者的控件有独立实现——RecipientsEditor,它继承自MultiAutoCompleteTextView, 我们知道 AutoCompleteTextView组件提供了输入时推荐可选项的功能,而 Multi...则意为着可输入多个项目(各项目间以逗号分隔),并分别推荐可选项。
推荐选项的数据源 来自RecipientsAdapter 适配器 ,它从ContactsProvider中读取MOBILE,WORK_MOBILE,MMS 三种类型 的电话号码作为推荐选项,在用户输入时自动匹配并推荐 。
说起接收者我们就不得不讨论Contact(代表接收者)、ContactList(代表接收者列表) 两个重要的类,他们提供了常用的组织联系人基本信息的方法,非常易于使用,同时它们也涉及到 Cache缓存系统 。
SMS <--->MMS 自动转换机制
在Messaging应用中, 编写普通短信SMS和编写彩信MMS的功能是整合在一起的,程序通过特定条件在两种 消息 类型之间 自动 转换,转换机制的入口时ComposeMessageActivity.toastConvertInfo()方法。初始创建的信息是SMS类型,当 出现 以下操作\ 或者条件成立时,会自动将消息从SMS转化为MMS:
- setSubject:为消息设置主题 ,通过为Subject编辑框mSubjectTextEditor设置addTextChangedListener而获得回调 ;
- setAttachment:为消息添加附件 (如:图片\视频) ,通过选项菜单MENU_ADD_ATTACHMENT实现,由showAddAttachmentDialog弹出附件类型列表,并通过addAttachment方法予以响应处理(打开新Activity——SubActivity),然后通过onActivityResult回调方法 最终执行到 WorkingMessage.setAttachment 方法;
- addressContainsEmail :接收地址中包含E-Mail地址 ,接收者输入框(mRecipientsEditor)有TextChangedListener——mRecipientsWatcher,它在接收者文本内容发生改变时调用mWorkingMessage.setHasEmail()方法以触发消息类型的自动转换机制;
- LengthRequires:消息内容的长度超过SMS标准容量 ,用于输入消息文本内容的控件时EditText类型的mTextEditor,它有一个 TextChangedListener——mTextEditorWatcher,当消息文本内容被输入时会调用到updateCounter()方法,该方法计算了消息长度,并在符合条件时调用 mWorkingMessage.setLengthRequiresMms()方法来改变消息类型;
精确捕获具体操作以及条件状态发生变化的关键是——WorkingMessage类,该类代表了正在创建中的消息对象(我们知道 ComposeMessageActivity 类描绘了创建信息的UI,UI之后的数据对象便是WorkingMessage) 它记录着因UI操作而引起的各种状态变化,并将所有变化最终都通过 WorkingMessage.update()方法来予以执行实施。
另外一些状态变化被定义在WorkingMessage.MessageStatusListener接口中 (注意:当前ComposeMessageActivity是该接口的唯一实现者 )包括消息类型发生改变时的回调,以及 WorkingMessage对象生命周期阶段的回调 :
- onProtocolChanged:消息类型发生改变——即在SMS <----> MMS之间发生变化,该方法在ComposeMessageActivity中的实现是调用了toastConvertInfo(),它显示了Toast提示,通知用户消息类型发生了改变;
- onAttachmentChanged:附件发生变化 (反映在 WorkingMessage.setAttachment方法 ) ——即在编辑MMS过程中添加/删除(在mAttachmentEditorHandler处理句柄中) 附件内容时回调;
- onPreMessageSent: 消息 发送前,通过runOnUiThread方法在UI线程中调用了resetMessage(),这似乎是个现场清理逻辑,TODO:留待以后仔细学习吧。
- onMessageSent: 消息 发送前,这并不代表消息发生成功,仅仅是指将消息投递的工作交给了底层网络而已。在 ComposeMessageActivity 中的实现会重新调用startMsgListQuery()方法,获得当前会话下的消息列表;
- onMaxPendingMessagesReached:当发送队列满载时该方法被回调,在现有的实现中会将当前消息保存为草稿,并给用户一个恰当的提示;
以上就是消息类型的自动转换机制,以及消息状态变化监听器,这样的设计使得我们能够以更简单的方式添加针对 WorkingMessage 的新功能。
彩信MMS构成元素的类型以及编码格式
在3GPP的《MMS Media formats and codecs》规范中,详细定义了构成彩信的基本元素类型及其编码格式,其中包括:文本(Text)、语音(Speech)、声音(Audio)、合成声音(Synthetic audio)、静态图片(Still Image)、Bitmap图片(Bitmap graphics)、视频(Video)、矢量图(Vector graphics),
Messaging应用中,对以上各种元素及格式提供了广泛的支持,其中3个大类:图片、视频、音频 ,每种都可以选择现有文件或即时拍摄录制,对文件格式的支持详见下表:
项目 | Messaging | 协议 | 备注 |
文本 | text/plain | text/plain, 无格式文本,可UTF-8,ISO-8859-1 | |
图片 | image/jpeg | 静态图片,jpeg,jfif | |
image/jpg | |||
image/gif | |||
image/png | Bitmap点阵图,GIF87a,GIF89a,PNG | ||
image/vnd.wap.wbmp | |||
视频 | video/3gpp | ||
video/3gpp2 | |||
video/h263 | H.264(AVC),H.263 Profile 3 Level | ||
video/mp4 | MPEG-4 | ||
音频 | audio/aac | ||
audio/amr | Speech:AMR,Enhanced aacPlus,Extended AMR-WB | ||
audio/imelody | |||
audio/mid | Synthetic audio:MIDI | ||
audio/midi | |||
audio/mp3 | |||
audio/mpeg3 | |||
audio/mpeg | |||
audio/mpg | |||
audio/mp4 | |||
audio/x-mid | |||
audio/x-midi | |||
audio/x-mp3 | |||
audio/x-mpeg3 | |||
audio/x-mpeg | |||
audio/x-mpg | |||
audio/3gpp | |||
application/ogg | |||
|
从协议看并没有非常明确的支持要求,而Messaging应用对MMS构成元素的支持却相当广泛。
SMS业务实现机制分析
SMS业务包括:短信呼入、短信发出、SMS投递报告, 以及 本地存储等几个方面,以下我们将对应源代码,全面剖析各个业务的处理过程和实现方法:
- 呼入 :有短信呼入时系统会发出android.provider.Telephony.SMS_RECEIVED广播,这最会调用到PrivilegedSmsReceiver的onReceiveWithPrivilege方法,在该方法内启动了处理呼入业务的SmsReceiverService类,该类持有一个后台工作线程并在ServiceHandler处理句柄中调用handleSmsReceived()方法来处理SMS呼入事件,具体代码如下:
private void handleSmsReceived(Intent intent) { SmsMessage[] msgs = Intents.getMessagesFromIntent(intent); Uri messageUri = insertMessage(this, msgs); //省略...... if (messageUri != null) { MessagingNotification.updateNewMessageIndicator(this, true); } }
可以看出基本流程是:从intent中读取消息数据,调用insertMessage进行本地存储,然后再调用MessagingNotification的updateNewMessageIndicator()方法来通知用户。
仔细探究insertMessage()方法的实现,会发现它并非名副其实,它首先处理class zero类型的短信——直接显示,然后再判断是否 "replace short message" 若是则进行替换更新(update),否则才执行真正的insert将数据存储到数据库中。
- 发出 :发送消息的触发点在“消息创建页面”的mSendButton按钮和选项菜单MENU_SEND上,它们调用了isPreparedForSending, confirmSendMessageIfNeeded两个方法,前者为发送前进行预检查,后者在条件成立的情况下发送消息。首先看预检查的基本逻辑:
private boolean isPreparedForSending() { int recipientCount = recipientCount(); return recipientCount > 0 && recipientCount <= MmsConfig.getRecipientLimit() && (mWorkingMessage.hasAttachment() || mWorkingMessage.hasText()); }
逻辑 很简单,首先要有接收者并且接收者的个数不能超过限制,然后是必须要有文本内容或者必须要有附件内容(针对彩信而言)。
接下来是 confirmSendMessageIfNeeded方法了:它首先检查了“接收者输入框mRecipientsEditor是否可见 ”(当‘回复’信息时不可见,因为已有潜在接收者) ,不可见则直接调用sendMessage方法,否则会调用 mRecipientsEditor的相关方法验证接收者的有效性 :首先会检查是否有无效的接收者地址,如果有再检查是否所有接收者都是无效的,如果全是无效的则告知用户不能发送,否则弹出提示框——允许用户选择忽略无效地址,而向其它有效的接收者地址发送,这些检查都通过后,就来到了重要的 sendMessage方法,sendMessage方法 首先检查当前系统是否是在紧急呼叫模式(emergency callback mode ) ,如果是则显示提示界面并终止发送。否则直接调用WorkingMessage.send方法进行发送。
WorkingMessage.send方法首先调用prepareForSave()进行预处理——确认接收者列表、处理彩信相关的:syncTextToSlideshow、removeSubjectIfEmpty等等问题;然后取得所属会话对象mConversation,取得信息文本内容、再判断是否是SMS类型,若是 则直接在一个新线程运行sendSmsWorker()方法,紧接着就是调用接收者缓存系统的相关更新方法——RecipientIdCache.updateNumbers()。
sendSmsWorker是发送SMS类型短信的核心方法,它在开始和结束分别调用了消息发送前-onPreMessageSent和消息发送后-onMessageSent两个生命周期方法。它直接取得会话id、目标地址,然后将实际发送任务交给业务实体类SmsMessageSender。
SmsMessageSender类是负责发送SMS信息的关键业务类,它首先在构建器中取得消息文本、目标地址、时间戳、服务中心号码等重要信息,然后在sendMessage方法中执行实际的发送操作:核心工具类依然是SmsManager.getDefault(),然后由divideMessage方法将消息文本分割成若干符合规范长度的片段,再取得投递报告的设置状态,然后通过工具类将消息写入Sms.Outbox(短消息发件箱)位置,最后是针对每个目标接收者调用sendMultipartTextMessage方法完成消息发送,而对于消息发送结果进行响应的关键在sentIntent列表中,请看如下的关键代码:
sentIntents.add(PendingIntent.getBroadcast(mContext, 0, new Intent(SmsReceiverService.MESSAGE_SENT_ACTION,uri,mContext,SmsReceiver.class), 0));
消息发送完成后会发出Intent.action=SmsReceiverService.MESSAGE_SENT_ACTION 的广播信息,并强制接收者为SmsReceiver,而该接收者最终会委托给SmsReceiverService类的handleSmsSent方法进行处理。
- 投递报告 :发送消息方法的sendMultipartTextMessage专门有一个系统广播的参数用于 报告 投递情况的状态,关键代码如下:
deliveryIntents.add(PendingIntent.getBroadcast(mContext, 0, new Intent(MessageStatusReceiver.MESSAGE_STATUS_RECEIVED_ACTION, uri, mContext, MessageStatusReceiver.class) ,0));
关键在于Intent.action= MessageStatusReceiver.MESSAGE_STATUS_RECEIVED_ACTION 的广播信息,并且显式指定了处理该广播的目标类MessageStatusReceiver,该类首先取得回应广播(Intent)中的PDU,然后更新了数据库中的消息状态 。
- 本地存储 :将SMS数据 存储到本地数据库中的几个关键在SmsReceiverService类的storeMessage、replaceMessage方法中,下面我们就来详细分析这两个方法的实现: …………待续 …………待续 …… : …………待续 …… 。。。
MMS业务实现机制分析
彩信收发时网络连接的创建过程
在Android系统所在的移动设备上,可以存在多个潜在的网络连接通道,例如:GPRS、Wi-Fi、UMTS等 等 。对于移动数据网络GPRS而言还有APN的概念。APN(Access Point Name) 中文称为 “接入点”,它是手机建立无线数据网络连接的必要参数,事实上它就像我们所熟知的代理服务器 一样 ,作为终端访问网络的 入口 代理而存在 。以中国移动的GPRS为例,常用的APN有两种:CMWAP、CMNET。一般情况下各个APN都可能成功发起网络连接,但根据移动网络的运营情况,不同的数据业务在不同的接入点上可能有不同的计费方式。
Android系统中建立网络连接的方法是调用ConnectivityManager工具类的 startUsingNetworkFeature()方法,方法执行结果——即连接是否成功建立的状态值,由命名为 ConnectivityManager.CONNECTIVITY_ACTION常量的Intent广播消息来通知。
在Messaging应用中,用于建立( 收发彩信的 )网络连接的代码在TransactionService服务类的beginMmsConnectivity()方法中:
// Phone.FEATURE_ENABLE_MMS is String "enableMMS" ConnectivityManager.startUsingNetworkFeature(ConnectivityManager.TYPE_MOBILE, Phone.FEATURE_ENABLE_MMS);
而用于监听网络连接是否建立成功 的 监听器,分别在 TransactionService.onCreate() 和TransactionService.onDestroy()方法中 进行注册与解除 :
//注册监听器 in TransactionService.onCreate() mConnectivityListener = new NetworkConnectivityListener(); mConnectivityListener.registerHandler(mServiceHandler, EVENT_DATA_STATE_CHANGED); mConnectivityListener.startListening(this);
//解除注册监听器 in TransactionService.onDestroy() mConnectivityListener.unregisterHandler(mServiceHandler); mConnectivityListener.stopListening(); mConnectivityListener = null;
在当前的SDK中,NetworkConnectivityListener类被@hide了, 无法直接访问,阅读其源代码可以知道它创建了一个内置的、用于接收特定广播消息的ConnectivityBroadcastReceiver类,并将网 络连接的创建结果封装到NetworkInfo、Reason等属性中,并且它 以向mServiceHandler发送特定Message来达到对业务方法的回调效果 。
原 来在阅读TransactionService类的注释时了解到,MM可以透过mobile data network 和 wi-fi network两种方式来收发。但阅读具体源码后发现并不是这样,收发彩信的网络连接只能是 ConnectivityManager.TYPE_MOBILE类型。
网络连接建立广播最终会转化为mServiceHandler收到的TransactionService.EVENT_DATA_STATE_CHANGED消息, mServiceHandler首先验证连接有效性,再通过sendMessageDelayed()方法建立时间间隔为30秒的计时器,轮询调用 beginMmsConnectivity()方法,以保持连接的持续存在。TransactionService把所有信息收发业务置于“任务队列(mProcessing & mPending)”中,并依次进行处理,因此在所有任务处理完成之前保持连接的持续存在是很重要的。
…………;。。
MMS信息本地存储机制分析
:…………;。。
………;。。
………未完成.....