今天接到一个任务,需要解决同事在美国测试Voicemail功能时,出现的下载失败问题。
目前,国内的运营商似乎没有支持Voicemail功能,因此资料相对较少。自己以前对这块流程也不太熟悉,没有解决过相应的bug。不得已,只好根据同事提供的截图,从界面开始一步一步分析整个Voicemail的下载流程。
一、整体结构
问题机使用的是厂商和Qualcomm修改过的软件版本,处于保密要求,不能拿来分析。
不过看了一下Android N的源码和修改过的版本,发现整体的设计架构基本一致。
因此,我们就以Google Voicemail下载相关的架构来进行分析。,
Voicemail涉及的主要文件,定义于packages/apps/dialer/src/com/android/dialer/voicemail文件夹下。
下图是Voicemail下载流程涉及的主要类。
大图链接
界面部分的主要类是:VoicemailPlaybackLayout和VoicemailPlaybackPresenter。
从代码来看,VoicemailPlaybackLayout是Android原生的一个示例界面,主要是用来测试Voicemail的基本功能。
负责与底层交互的类是VoicemailPlaybackPresenter,它定义了接口用于启动实际的功能。
对于下载流程而言,VoicemailPlaybackPresenter将以广播的方式通知FetchVoicemailReceiver。后者接收到广播后,将利用ImapHelper类来进行下载操作。
ImapHelper与相关的一系列类,例如ImapStore、ImapConnection等,完成实际的下载工作后,将通过ImapResponseParser解析下载的结果,并以回调的方式通知ImapHelper中定义的MessagebodyFetchedListener。
后者进一步通知VoicemailFetchedCallback中的接口。
VoicemailFetchedCallback负责将信息写入到数据库,以触发VoicemailPlaybackPresenter中的内部类FetchResultHandler。
FetchResultHandler将根据结果,更新界面并进行下载完成的后续操作。
二、主要流程分析
对整体架构有了一个基本的了解后,我们就可以看看源码是如何实现的了。
注意到整个Voicemail相关的功能很多,例如下载完后可以开始播放、还提供了收藏和分享功能,
我们目前仅关注于下载这个部分相关的流程。
1、VoicemailPlaybackLayout
我们首先看一下VoicemailPlaybackLayout类。
虽然这个类可能并没有在真实场景下使用,但作为例子还是值得借鉴的。
以下代码是VoicemailPlaybackLayout中下载相关,比较主要的代码:
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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
|
//注意到VoicemailPlaybackLayout实现了VoicemailPlaybackPresenter.PlaybackView接口
public
class
VoicemailPlaybackLayout
extends
LinearLayout
implements
VoicemailPlaybackPresenter.PlaybackView,
CallLogAsyncTaskUtil.CallLogAsyncTaskListener {
...........
/**
* Click listener to play or pause voicemail playback.
*/
//定义播放按键对应的OnClickListener
private
final
View.OnClickListener mStartStopButtonListener =
new
View.OnClickListener() {
@Override
public
void
onClick(View view) {
if
(mPresenter ==
null
) {
return
;
}
if
(mIsPlaying) {
//对应暂停功能
mPresenter.pausePlayback();
}
else
{
//第一次点击播放时,mIsPlaying为false,进入这个分支
mPresenter.resumePlayback();
}
}
};
..............
//mPresenter的类型为VoicemailPlaybackPresenter
private
VoicemailPlaybackPresenter mPresenter;
.............
//提供了接口,设定VoicemailPlaybackPresenter和voicemailUri
//voicemailUri对应于Voicemail的下载地址
public
void
setPresenter(VoicemailPlaybackPresenter presenter, Uri voicemailUri) {
mPresenter = presenter;
mVoicemailUri = voicemailUri;
//收藏按键
if
(ObjectFactory.isVoicemailArchiveEnabled(mContext)) {
updateArchiveUI(mVoicemailUri);
updateArchiveButton(mVoicemailUri);
}
//分享按键
if
(ObjectFactory.isVoicemailShareEnabled(mContext)) {
// Show share button and space before it
mShareSpace.setVisibility(View.VISIBLE);
mShareButton.setVisibility(View.VISIBLE);
}
}
protected
void
onFinishInflate() {
.........
//加载界面时,设定OnClickListener
mStartStopButton.setOnClickListener(mStartStopButtonListener);
.........
}
...........
//以下两个是VoicemailPlaybackPresenter.PlaybackView中定义接口的实现
public
void
setIsFetchingContent() {
disableUiElements();
//这里是在界面显示,类似“正在抓取语音邮件”
mStateText.setText(getString(R.string.voicemail_fetching_content));
}
@Override
public
void
setFetchContentTimeout() {
mStartStopButton.setEnabled(
true
);
//这里是在界面显示,类似“无法抓取语音邮件”
mStateText.setText(getString(R.string.voicemail_fetching_timout));
}
...........
}
|
在上面的代码中,目前我们只需要记住:
1、点击播放开关时,mPresenter.resumePlayback将发起下载流程;
2、setIsFetchingContent、setFetchContentTimeout等oicemailPlaybackPresenter.PlaybackView定义的接口,将会被回调,用于更新界面。
2、VoicemailPlaybackPresenter
2.1 resumePlayback
假设我们点击了播放按键,进入到了VoicemailPlaybackPresenter的下载流程。
正如上文介绍的,将调用VoicemailPlaybackPresenter的resumePlayback函数:
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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
|
public
void
resumePlayback() {
if
(mView ==
null
) {
return
;
}
//消息没准备好,进入下载流程(我们主要关注这一部分)
if
(!mIsPrepared) {
//checkForContent将根据mVoicemailUri
//判断之前是否已经开始下载对应的Voicemail,目的是避免重复下载
//检查完毕后,回调OnContentCheckedListener的接口onContentChecked
checkForContent(
new
OnContentCheckedListener() {
@Override
public
void
onContentChecked(
boolean
hasContent) {
if
(!hasContent) {
// No local content, download from server. Queue playing if the request was
// issued,
//调用requestContent开始下载
mIsPlaying = requestContent(PLAYBACK_REQUEST);
}
else
{
// Queue playing once the media play loaded the content.
mIsPlaying =
true
;
prepareContent();
}
}
});
return
;
}
//以下是判断消息已经下载过的流程(我们不关注)
//消息已经下载好了,对应从暂停到播放的场景
mIsPlaying =
true
;
if
(mMediaPlayer !=
null
&& !mMediaPlayer.isPlaying()) {
// Clamp the start position between 0 and the duration.
//找到继续播放的位置
mPosition = Math.max(
0
, Math.min(mPosition, mDuration.get()));
mMediaPlayer.seekTo(mPosition);
try
{
// Grab audio focus.
// Can throw RejectedExecutionException.
mVoicemailAudioManager.requestAudioFocus();
//开始播放
mMediaPlayer.start();
setSpeakerphoneOn(mIsSpeakerphoneOn);
}
catch
(RejectedExecutionException e) {
handleError(e);
}
}
................
//调用VoicemailPlaybackLayout实现的VoicemailPlaybackPresenter.PlaybackView接口
//更新界面
mView.onPlaybackStarted(mDuration.get(), getScheduledExecutorServiceInstance());
}
|
从上面的代码,我们知道当用户点击播放按键时:
当Voicemail已经下载完毕或者之前已经播放过,那么将执行播放相关的准备工作或继续播放;
当Voicemail没有下载过,VoicemailPlaybackPresenter将调用requestContent开始下载Voicemail。
2.2 requestContent
我们主要关注下载流程,因此跟进一下requestContent函数:
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
31
32
|
protected
boolean
requestContent(
int
code) {
if
(mContext ==
null
|| mVoicemailUri ==
null
) {
return
false
;
}
//1、注意这里创建了一个FetchResultHandler
FetchResultHandler tempFetchResultHandler =
new
FetchResultHandler(
new
Handler(), mVoicemailUri, code);
switch
(code) {
case
ARCHIVE_REQUEST:
//收藏相关,不关注
mArchiveResultHandlers.add(tempFetchResultHandler);
break
;
default
:
//消除旧有的FetchResultHandler
if
(mFetchResultHandler !=
null
) {
mFetchResultHandler.destroy();
}
//调用界面继承的回调接口,更新界面
//此时界面就会显示类似“抓取语音邮件ing”的字段
mView.setIsFetchingContent();
mFetchResultHandler = tempFetchResultHandler;
break
;
}
// Send voicemail fetch request.
//通过广播来驱动实际的下载过程
Intent intent =
new
Intent(VoicemailContract.ACTION_FETCH_VOICEMAIL, mVoicemailUri);
mContext.sendBroadcast(intent);
return
true
;
}
|
对于下载流程而言,上面的代码主要做了两件事:
1、创建了一个FetchResultHandler;2、发送了ACTION_FETCH_VOICEMAIL广播。
2.2.1 FetchResultHandler
我们先看看FetchResultHandler:
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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
|
//注意FetchResultHandler继承了ContentObserver
@ThreadSafe
private
class
FetchResultHandler
extends
ContentObserver
implements
Runnable {
//表明是否在等待结果,初始值为true
private
AtomicBoolean mIsWaitingForResult =
new
AtomicBoolean(
true
);
..........
public
FetchResultHandler(Handler handler, Uri uri,
int
code) {
super
(handler);
mFetchResultHandler = handler;
mRequestCode = code;
mVoicemailUri = uri;
if
(mContext !=
null
) {
//监听mVoicemailUri对应的字段;
//当Voicemail下载完毕时,将更新这个字段
mContext.getContentResolver().registerContentObserver(
mVoicemailUri,
false
,
this
);
//延迟发送一个Runnable对象,其实就是自己
//延迟时间默认为20s
mFetchResultHandler.postDelayed(
this
, FETCH_CONTENT_TIMEOUT_MS);
}
}
@Override
public
void
run() {
//若延迟20s执行后,发现仍然在等待结果
if
(mIsWaitingForResult.getAndSet(
false
) && mContext !=
null
) {
mContext.getContentResolver().unregisterContentObserver(
this
);
if
(mView !=
null
) {
//调用界面实现的回调接口,此时界面就会更新为“无法抓取语音邮件”或“抓取超时”之类的
mView.setFetchContentTimeout();
}
}
}
//销毁过程,较为简单
public
void
destroy() {
if
(mIsWaitingForResult.getAndSet(
false
) && mContext !=
null
) {
mContext.getContentResolver().unregisterContentObserver(
this
);
mFetchResultHandler.removeCallbacks(
this
);
}
}
//监控的字段发生变化
@Override
public
void
onChange(
boolean
selfChange) {
mAsyncTaskExecutor.submit(Tasks.CHECK_CONTENT_AFTER_CHANGE,
new
AsyncTask<
void
,
boolean
=
""
>() {
@Override
public
Boolean doInBackground(Void... params) {
//查询数据库,判断下载的信息是否写入数据库
return
queryHasContent(mVoicemailUri);
}
@Override
public
void
onPostExecute(Boolean hasContent) {
//下载成功,将mIsWaitingForResult置为false
//于是20s超时到期时,run函数也不会更新界面
if
(hasContent && mContext !=
null
&& mIsWaitingForResult.getAndSet(
false
)) {
mContext.getContentResolver().unregisterContentObserver(
FetchResultHandler.
this
);
//做好播放的准备工作
prepareContent();
//收藏相关的工作
if
(mRequestCode == ARCHIVE_REQUEST) {
startArchiveVoicemailTask(mVoicemailUri,
true
/* archivedByUser */
);
}
else
if
(mRequestCode == SHARE_REQUEST) {
//分享相关的工作
startArchiveVoicemailTask(mVoicemailUri,
false
/* archivedByUser */
);
}
}
}
});
}
}</
void
,>
|
从上面的代码,我们知道了FetchResultHandler主要用于监控Voicemail是否在规定时间内下载完毕。
在FetchResultHandler创建时,发送了一个延迟消息;当延迟消息被执行时,若发现消息仍未下载完,就会在界面显示出错信息。
在延迟消息执行之前,若FetchResultHandler监控到数据变化,并判断出Voicemail下载成功,就可以为播放做相应的准备工作了。
了解了FetchResultHandler的功能后,我们将目光投向下载相关的广播消息。
3、FetchVoicemailReceiver
3.1 onReceive
在源码中,FetchVoicemailReceiver负责接收VoicemailContract.ACTION_FETCH_VOICEMAIL:
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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
|
public
void
onReceive(
final
Context context, Intent intent) {
if
(VoicemailContract.ACTION_FETCH_VOICEMAIL.equals(intent.getAction())) {
mContext = context;
mContentResolver = context.getContentResolver();
//取出要下载的Uri
mUri = intent.getData();
//检查数据有效性
...........
Cursor cursor = mContentResolver.query(mUri, PROJECTION,
null
,
null
,
null
);
try
{
if
(cursor.moveToFirst()) {
//取出Voicemail的账户信息
mUid = cursor.getString(SOURCE_DATA);
String accountId = cursor.getString(PHONE_ACCOUNT_ID);
if
(TextUtils.isEmpty(accountId)) {
TelephonyManager telephonyManager = (TelephonyManager)
context.getSystemService(Context.TELEPHONY_SERVICE);
accountId = telephonyManager.getSimSerialNumber();
........
}
//构造出账户
mPhoneAccount = PhoneUtils.makePstnPhoneAccountHandle(accountId);
//判断账户是否注册
if
(!OmtpVvmSourceManager.getInstance(context)
.isVvmSourceRegistered(mPhoneAccount)) {
Log.w(TAG,
"Account not registered - cannot retrieve message."
);
return
;
}
//其实就是利用mPhoneAccount中的IccId得到对应的phone,然后取出subId
int
subId = PhoneUtils.getSubIdForPhoneAccountHandle(mPhoneAccount);
//得到运营商配置信息
OmtpVvmCarrierConfigHelper carrierConfigHelper =
new
OmtpVvmCarrierConfigHelper(context, subId);
//fetchVoicemailNetworkRequestCallback为内部类
mNetworkCallback =
new
fetchVoicemailNetworkRequestCallback(context,
mPhoneAccount);
//申请网络
mNetworkCallback.requestNetwork();
}
}
finally
{
cursor.close();
}
}
}
|
在onReceive中,主要工作分为3部:1、获取账户信息;2、获取运营商的配置信息;3、申请网络。
3.2 fetchVoicemailNetworkRequestCallback
我们不深究获取账户信息和运营商配置信息的流程,仅关注申请网络的执行步骤。
为此,我们看一下FetchVoicemailReceiver的内部类fetchVoicemailNetworkRequestCallback:
1
2
3
4
5
6
7
8
9
10
11
12
|
private
class
fetchVoicemailNetworkRequestCallback
extends
VvmNetworkRequestCallback {
public
fetchVoicemailNetworkRequestCallback(Context context,
PhoneAccountHandle phoneAccount) {
super
(context, phoneAccount);
}
@Override
public
void
onAvailable(
final
Network network) {
super
.onAvailable(network);
fetchVoicemail(network);
}
}
|
从上面的代码,可以看出fetchVoicemailNetworkRequestCallback继承VvmNetworkRequestCallback。
requestNetwork的工作将由VvmNetworkRequestCallback来执行。
我们知道当网络建立成功后,ConnectivityService将会回调观察者的onAvailable接口。
于是,当网络建立成功后,fetchVoicemailNetworkRequestCallback就会调用fetchVoicemail函数。
3.2.1 VvmNetworkRequestCallback
在分析fetchVoicemail函数前,我们先看一下VvmNetworkRequestCallback类:
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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
|
public
abstract
class
VvmNetworkRequestCallback
extends
ConnectivityManager.NetworkCallback {
..........
public
VvmNetworkRequestCallback(Context context, PhoneAccountHandle phoneAccount) {
mContext = context;
mPhoneAccount = phoneAccount;
mSubId = PhoneUtils.getSubIdForPhoneAccountHandle(phoneAccount);
mCarrierConfigHelper =
new
OmtpVvmCarrierConfigHelper(context, mSubId);
//构造函数中,就创建了NetworkRequest
mNetworkRequest = createNetworkRequest();
}
private
NetworkRequest createNetworkRequest() {
NetworkRequest.Builder builder =
new
NetworkRequest.Builder()
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET);
//运营商配置信息若指定必须使用数据网络
if
(mCarrierConfigHelper.isCellularDataRequired()) {
Log.d(TAG,
"Transport type: CELLULAR"
);
//那么就指定NetworkRequest的TransportType
builder.addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR)
.setNetworkSpecifier(Integer.toString(mSubId));
}
else
{
Log.d(TAG,
"Transport type: ANY"
);
}
return
builder.build();
}
...............
public
void
requestNetwork() {
//每次申请网络,都要重新构造一次VvmNetworkRequestCallback
if
(mRequestSent ==
true
) {
Log.e(TAG,
"requestNetwork() called twice"
);
return
;
}
mRequestSent =
true
;
//getNetworkRequest取出createNetworkRequest创造的结果
//ConnectivityManager的requestNetwork进入建立短连接的流程
getConnectivityManager().requestNetwork(getNetworkRequest(),
this
);
Handler handler =
new
Handler(Looper.getMainLooper());
//发送一个超时消息,默认超时时间为60s
handler.postDelayed(
new
Runnable() {
@Override
public
void
run() {
//当建立网络成功时,ConnectivityService回调onAvailable接口时,会将mResultReceived置为true
if
(mResultReceived ==
false
) {
//若建立网络失败,则调用onFailed函数
onFailed(NETWORK_REQUEST_FAILED_TIMEOUT);
}
}
}, NETWORK_REQUEST_TIMEOUT_MILLIS);
}
...........
//建立网络失败,就更改状态,同时释放建立网络的请求
public
void
onFailed(String reason) {
Log.d(TAG,
"onFailed: "
+ reason);
if
(mCarrierConfigHelper.isCellularDataRequired()) {
VoicemailUtils.setDataChannelState(
mContext, mPhoneAccount,
Status.DATA_CHANNEL_STATE_NO_CONNECTION_CELLULAR_REQUIRED);
}
else
{
VoicemailUtils.setDataChannelState(
mContext, mPhoneAccount, Status.DATA_CHANNEL_STATE_NO_CONNECTION);
}
releaseNetwork();
}
}
|
VvmNetworkRequestCallback的工作比较清晰,就是构造NetworkRequest,然后通过ConnectivityManager来建立短连接。
一但短连接建立成功后,其子类的onAvailable函数就会被调用。
3.3 fetchVoicemail
现在假设网络已经建立成功,下载流程开始执行FetchVoicemailReceiver的fetchVoicemail函数:
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
31
32
|
private
void
fetchVoicemail(
final
Network network) {
//用户可能会下载很多次,避免每次都创建线程
//于是使用了Executors.newCachedThreadPool
Executor executor = Executors.newCachedThreadPool();
executor.execute(
new
Runnable() {
public
void
run() {
try
{
while
(mRetryCount >
0
) {
//创建ImapHelper
ImapHelper imapHelper =
new
ImapHelper(mContext, mPhoneAccount, network);
//判断ImapHelper是否创建成功
if
(!imapHelper.isSuccessfullyInitialized()) {
Log.w(TAG,
"Can't retrieve Imap credentials."
);
return
;
}
//注意这里创建了VoicemailFetchedCallback
//当下载完成后会回调VoicemailFetchedCallback的setVoicemailContent接口,执行更新数据库的操作
//通知VoicemailPlaybackPresenter中的FetchResultHandler
boolean
success = imapHelper.fetchVoicemailPayload(
new
VoicemailFetchedCallback(mContext, mUri), mUid);
}
}
finally
{
//下载结束释放网络
if
(mNetworkCallback !=
null
) {
mNetworkCallback.releaseNetwork();
}
}
}
});
}
|
从上面的代码可以看出,fetchVoicemail主要是通过ImapHelper来进行实际的下载工作,同时创建VoicemailFetchedCallback来监听下载的结果。
3.3.1 VoicemailFetchedCallback
在分析ImapHelper前,我们先看看VoicemailFetchedCallback:
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
31
32
33
34
|
public
class
VoicemailFetchedCallback {
...........
public
VoicemailFetchedCallback(Context context, Uri uri) {
mContentResolver = context.getContentResolver();
mUri = uri;
}
//信息下载完成的回调接口
public
void
setVoicemailContent(VoicemailPayload voicemailPayload) {
...............
OutputStream outputStream =
null
;
try
{
//自己见识还是少,这个用法第一次见
outputStream = mContentResolver.openOutputStream(mUri);
byte
[] inputBytes = voicemailPayload.getBytes();
//将Voicemail的payload信息写入到数据库中
if
(inputBytes !=
null
) {
outputStream.write(inputBytes);
}
}
catch
(IOException e) {
Log.w(TAG, String.format(
"File not found for %s"
, mUri));
return
;
}
finally
{
IoUtils.closeQuietly(outputStream);
}
//更新一下,通知FetchResultHandler
ContentValues values =
new
ContentValues();
values.put(Voicemails.MIME_TYPE, voicemailPayload.getMimeType());
values.put(Voicemails.HAS_CONTENT,
true
);
int
updatedCount = mContentResolver.update(mUri, values,
null
,
null
);
..........
}
}
|
从上面的代码可以看出,VoicemailFetchedCallback的工作就是在回调后,写入和更新数据库。
FetchResultHandler收到数据库更新的通知后,就会取出数据,执行播放的准备工作。
4、ImapHelper
前面的代码中涉及到了ImapHelper的构造函数和fetchVoicemailPayload。
现在,我们看看这两个函数的实现。
4.1 构造函数
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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
|
public
class
ImapHelper {
..........
public
ImapHelper(Context context, PhoneAccountHandle phoneAccount, Network network) {
mContext = context;
mPhoneAccount = phoneAccount;
mNetwork = network;
try
{
..........
//获取账户对应的username、password、servername和port等信息
//实际上这些信息都是从SharedPreference中获取的
String username = VisualVoicemailSettingsUtil.getVisualVoicemailCredentials(context,
OmtpConstants.IMAP_USER_NAME, phoneAccount);
String password = VisualVoicemailSettingsUtil.getVisualVoicemailCredentials(context,
OmtpConstants.IMAP_PASSWORD, phoneAccount);
String serverName = VisualVoicemailSettingsUtil.getVisualVoicemailCredentials(context,
OmtpConstants.SERVER_ADDRESS, phoneAccount);
int
port = Integer.parseInt(
VisualVoicemailSettingsUtil.getVisualVoicemailCredentials(context,
OmtpConstants.IMAP_PORT, phoneAccount));
//默认未定义认证类型
int
auth = ImapStore.FLAG_NONE;
//与前面FetchVoicemailReceiver一样,获取运营商配置信息
OmtpVvmCarrierConfigHelper carrierConfigHelper =
new
OmtpVvmCarrierConfigHelper(context,
PhoneUtils.getSubIdForPhoneAccountHandle(phoneAccount));
//特殊的Vvm type有对应的端口和认证类型
if
(TelephonyManager.VVM_TYPE_CVVM.equals(carrierConfigHelper.getVvmType())) {
port =
993
;
auth = ImapStore.FLAG_SSL;
}
//创建了ImapStore
mImapStore =
new
ImapStore(
context,
this
, username, password, port, serverName, auth, network);
}
catch
(NumberFormatException e) {
//异常,则更改状态
VoicemailUtils.setDataChannelState(
mContext, mPhoneAccount, Status.DATA_CHANNEL_STATE_BAD_CONFIGURATION);
LogUtils.w(TAG,
"Could not parse port number"
);
}
...............
}
...........
//ImapHelper是否创建成功依赖于ImapStore的创建
public
boolean
isSuccessfullyInitialized() {
return
mImapStore !=
null
;
}
..........
}
|
从上面的代码可以看出,ImapHelper的构造函数主要是:
1、从账户信息中得到网络访问必须的信息;
2、创建出ImapStore对象。
4.1.1 ImapStore的构造函数
我们跟进一下ImapStore的构造函数:
1
2
3
4
5
6
7
8
9
10
|
public
ImapStore(Context context, ImapHelper helper, String username, String password,
int
port,
String serverName,
int
flags, Network network) {
mContext = context;
mHelper = helper;
mUsername = username;
mPassword = password;
//注意这里创建了MailTransport,最后实际发送将依赖该对象
mTransport =
new
MailTransport(context,
this
.getImapHelper(),
network, serverName, port, flags);
}
|
在ImapStore的构造函数中,创建出了关键的MailTransport对象。
MailTransport是直接与网络打交道,进行数据收发的类。我们后文再介绍这个类。
4.2 fetchVoicemailPayload
现在我们可以开始分析fetchVoicemailPayload函数了,在这个函数中将进行数据下载:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
public
boolean
fetchVoicemailPayload(VoicemailFetchedCallback callback,
final
String uid) {
try
{
//1、创建并打开ImapFolder
mFolder = openImapFolder(ImapFolder.MODE_READ_WRITE);
............
//利用ImapFolder获取message
Message message = mFolder.getMessage(uid);
..........
//2、利用message构造VoicemailPayload
VoicemailPayload voicemailPayload = fetchVoicemailPayload(message);
..........
//调用VoicemailFetchedCallback的setVoicemailContent接口
callback.setVoicemailContent(voicemailPayload);
return
true
;
}
catch
(MessagingException e) {
}
finally
{
closeImapFolder();
}
return
false
;
}
|
从上面的代码可以看出,fetchVoicemailPayload中创建出了ImapFolder对象。实际的下载工作似乎都与ImapFolder有关。
我们先不深入分析ImapFolder,姑且认为它的功能是下载。
优先看看fetchVoicemailPayload中,调用的一些关键函数的内容。
4.2.1 openImapFolder
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
private
ImapFolder openImapFolder(String modeReadWrite) {
try
{
if
(mImapStore ==
null
) {
return
null
;
}
//创建ImapFolder
ImapFolder folder =
new
ImapFolder(mImapStore, ImapConstants.INBOX);
//调用open
folder.open(modeReadWrite);
return
folder;
}
catch
(MessagingException e) {
LogUtils.e(TAG, e,
"Messaging Exception"
);
}
return
null
;
}
|
openImapFolder的功能比较简单,就是创建ImapFolder,然后调用其open接口。
4.2.2 fetchVoicemailPayload(message)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
//此时已经用ImapFolder得到了Message
private
VoicemailPayload fetchVoicemailPayload(Message message)
throws
MessagingException {
...........
//创建MessageBodyFetchedListener,用于回调
MessageBodyFetchedListener listener =
new
MessageBodyFetchedListener();
//Voicemail完整的数据结构包含了许多部分
//创建FetchProfile,用于指定需下载的部分
FetchProfile fetchProfile =
new
FetchProfile();
//此处进需要下载Item.BODY
fetchProfile.add(FetchProfile.Item.BODY);
//调用ImapFolder的fetch函数(有阻塞的能力)
mFolder.fetch(
new
Message[] {message}, fetchProfile, listener);
return
listener.getVoicemailPayload();
}
|
ImapHelper在调用 fetchVoicemailPayload(message)函数前,已经利用ImapFolder得到了Voicemail对应的Message信息。
个人觉得Message可以认为是Voicemail对应的一种缩略信息。
从上面的代码可以看出,在fetchVoicemailPayload(message)函数中,仍需要调用ImapFolder的fetch函数获取FetchProfile指定部分的内容。
注意到ImapFolder的fetch函数是具有阻塞能力的,因此上面的函数创建了MessageBodyFetchedListener。
当下载完成后,MessageBodyFetchedListener的接口会被回调,以完成VoicemailPayload的创建。
当回调函数执行完毕后,ImapFolder的fetch函数才真正返回。
于是,fetchVoicemailPayload(message)函数的最后,才能调用MessageBodyFetchedListener.getVoicemailPayload。
4.2.2.1 MessageBodyFetchedListener
我们一起看一下MessageBodyFetchedListener的相关定义:
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
31
32
33
34
35
36
37
38
39
40
41
42
43
|
private
final
class
MessageBodyFetchedListener
implements
ImapFolder.MessageRetrievalListener {
private
VoicemailPayload mVoicemailPayload;
public
VoicemailPayload getVoicemailPayload() {
return
mVoicemailPayload;
}
@Override
//ImapFolder fetch message成功后的回调接口
public
void
messageRetrieved(Message message) {
LogUtils.d(TAG,
"Fetched message body for "
+ message.getUid());
LogUtils.d(TAG,
"Message retrieved: "
+ message);
try
{
//利用Message构造出VoicemailPayload
mVoicemailPayload = getVoicemailPayloadFromMessage(message);
}
catch
(MessagingException e) {
LogUtils.e(TAG,
"Messaging Exception:"
, e);
}
catch
(IOException e) {
LogUtils.e(TAG,
"IO Exception:"
, e);
}
}
private
VoicemailPayload getVoicemailPayloadFromMessage(Message message)
throws
MessagingException, IOException {
//解析message中内容
Multipart multipart = (Multipart) message.getBody();
for
(
int
i =
0
; i < multipart.getCount(); ++i) {
BodyPart bodyPart = multipart.getBodyPart(i);
String bodyPartMimeType = bodyPart.getMimeType().toLowerCase();
LogUtils.d(TAG,
"bodyPart mime type: "
+ bodyPartMimeType);
if
(bodyPartMimeType.startsWith(
"audio/"
)) {
//音频部分
byte
[] bytes = getDataFromBody(bodyPart.getBody());
LogUtils.d(TAG, String.format(
"Fetched %s bytes of data"
, bytes.length));
//仅利用音频内容构成VoicemailPayload
return
new
VoicemailPayload(bodyPartMimeType, bytes);
}
}
LogUtils.e(TAG,
"No audio attachment found on this voicemail"
);
return
null
;
}
}
|
从上面的代码可以看出,当ImapFolder的fetch函数下载了Voicemail的指定内容后,MessageBodyFetchedListener的回调接口被调用。
MessageBodyFetchedListener将负责将原始数据中的音频部分解析出来,构造成Voicemail的payload。
5、ImapFolder
现在我们开始分析ImapFolder相关的内容。
前面的流程中遗留了ImapFolder的构造函数、open、getMessage和fetch函数。
我们依次进行分析。
1
2
3
4
|
public
ImapFolder(ImapStore store, String name) {
mStore = store;
mName = name;
}
|
ImapFolder的构造函数比较简单,主要是保存ImapStore对象。
5.1 open
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
31
|
public
void
open(String mode)
throws
MessagingException {
try
{
//第一次打开时,isOpen返回false
if
(isOpen()) {
..........
}
synchronized
(
this
) {
//从ImapStore取出ImapConnection
//第一次时,将创建一个ImapConnection
mConnection = mStore.getConnection();
}
try
{
doSelect();
}
catch
(IOException ioe) {
throw
ioExceptionHandler(mConnection, ioe);
}
finally
{
destroyResponses();
}
}
catch
(AuthenticationFailedException e) {
// Don't cache this connection, so we're forced to try connecting/login again
mConnection =
null
;
close(
false
);
throw
e;
}
catch
(MessagingException e) {
mExists =
false
;
close(
false
);
throw
e;
}
}
|
上面的代码中提到了一个新的概念ImapConnection。
敏感的朋友一看这个名字,就知道下载的任务一定会移交到ImapConnection来执行。
我们将ImapConnection的内容放到后面,先看看open函数中的另一个重点doSelect。
5.1.1 doSelect
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
31
32
33
34
35
36
37
38
|
/**
* Selects the folder for use. Before performing any operations on this folder, it
* must be selected.
*/
private
void
doSelect()
throws
IOException, MessagingException {
//调用ImapConnection的executeSimpleCommand函数,执行SELECT命令(SELECT mName)
//这里已经开始与网络侧交互了
final
List<imapresponse> responses = mConnection.executeSimpleCommand(
String.format(Locale.US, ImapConstants.SELECT +
" \"%s\""
, mName));
// Assume the folder is opened read-write; unless we are notified otherwise
mMode = MODE_READ_WRITE;
int
messageCount = -
1
;
//处理命令的返回结果
for
(ImapResponse response : responses) {
//网络侧的结果:EXISTS字段表示message的数量
if
(response.isDataResponse(
1
, ImapConstants.EXISTS)) {
messageCount = response.getStringOrEmpty(
0
).getNumberOrZero();
}
else
if
(response.isOk()) {
//读写模式
final
ImapString responseCode = response.getResponseCodeOrEmpty();
if
(responseCode.is(ImapConstants.READ_ONLY)) {
mMode = MODE_READ_ONLY;
}
else
if
(responseCode.is(ImapConstants.READ_WRITE)) {
mMode = MODE_READ_WRITE;
}
}
else
if
(response.isTagged()) {
// Not OK
mStore.getImapHelper().setDataChannelState(Status.DATA_CHANNEL_STATE_SERVER_ERROR);
throw
new
MessagingException(
"Can't open mailbox: "
+ response.getStatusResponseTextOrEmpty());
}
}
if
(messageCount == -
1
) {
throw
new
MessagingException(
"Did not find message count during select"
);
}
mMessageCount = messageCount;
mExists =
true
;
}</imapresponse>
|
从上面的代码可以看出,doSelect主要是选中Voicemail用户对应的文件夹,同时得到其中的信息数量及读写模式。
这些工作需要与网络进行交互才能完成,将被委托给ImapConnection进行处理。
ImapConnection的工作,将于后文介绍。
5.2 getMessage
接下来,我们看看ImapFolder的getMessage函数。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
public
Message getMessage(String uid)
throws
MessagingException {
//判断ImapConnection是否依然存在
checkOpen();
//获取服务器上的UID数组
final
String[] uids = searchForUids(ImapConstants.UID +
" "
+ uid);
for
(
int
i =
0
; i < uids.length; i++) {
if
(uids[i].equals(uid)) {
//找到了匹配项,就构造并返回ImapMessage
//可以看到此时的ImapMessage并没有实质的内容
return
new
ImapMessage(uid,
this
);
}
}
LogUtils.e(TAG,
"UID "
+ uid +
" not found on server"
);
return
null
;
}
|
我们跟进一下searchForUids:
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
31
32
33
34
35
36
37
38
39
40
41
42
|
String[] searchForUids(String searchCriteria)
throws
MessagingException {
checkOpen();
try
{
try
{
final
String command = ImapConstants.UID_SEARCH +
" "
+ searchCriteria;
//依然是调用ImapConnection的executeSimpleCommand函数,只是命令不同
//然后利用getSearchUids处理返回的ImapResponse
final
String[] result = getSearchUids(mConnection.executeSimpleCommand(command));
LogUtils.d(TAG,
"searchForUids '"
+ searchCriteria +
"' results: "
+
result.length);
return
result;
}
catch
(ImapException me) {
LogUtils.d(TAG,
"ImapException in search: "
+ searchCriteria, me);
return
Utility.EMPTY_STRINGS;
// Not found
}
catch
(IOException ioe) {
LogUtils.d(TAG,
"IOException in search: "
+ searchCriteria, ioe);
throw
ioExceptionHandler(mConnection, ioe);
}
}
finally
{
destroyResponses();
}
}
//负责从ImapResponse中解析出UID数组
String[] getSearchUids(List<imapresponse> responses) {
// S: * SEARCH 2 3 6
final
ArrayList<string> uids =
new
ArrayList<string>();
for
(ImapResponse response : responses) {
//仅处理包含SEARCH字段的结果
if
(!response.isDataResponse(
0
, ImapConstants.SEARCH)) {
continue
;
}
// Found SEARCH response data
for
(
int
i =
1
; i < response.size(); i++) {
ImapString s = response.getStringOrEmpty(i);
if
(s.isString()) {
uids.add(s.getString());
}
}
}
return
uids.toArray(Utility.EMPTY_STRINGS);
}</string></string></imapresponse>
|
从上面的代码,我们知道ImapFolder的getMessage函数,依然需要利用ImapConnection与网络交互,
最终返回的结果仅用于定义所有需要下载的消息。
5.3 fetch
ImapFolder的fetch函数才是实际下载消息的接口。
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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
|
public
void
fetch(Message[] messages, FetchProfile fp,
MessageRetrievalListener listener)
throws
MessagingException {
try
{
fetchInternal(messages, fp, listener);
}
catch
(RuntimeException e) {
// Probably a parser error.
LogUtils.w(TAG,
"Exception detected: "
+ e.getMessage());
throw
e;
}
}
public
void
fetchInternal(Message[] messages, FetchProfile fp,
MessageRetrievalListener listener)
throws
MessagingException {
if
(messages.length ==
0
) {
return
;
}
checkOpen();
HashMap<string, message=
""
> messageMap =
new
HashMap<string, message=
""
>();
//这里是为同时下载多条消息做的设计
for
(Message m : messages) {
messageMap.put(m.getUid(), m);
}
/*
* Figure out what command we are going to run:
* FLAGS - UID FETCH (FLAGS)
* ENVELOPE - UID FETCH (INTERNALDATE UID RFC822.SIZE FLAGS BODY.PEEK[
* HEADER.FIELDS (date subject from content-type to cc)])
* STRUCTURE - UID FETCH (BODYSTRUCTURE)
* BODY_SANE - UID FETCH (BODY.PEEK[]<0.N>) where N = max bytes returned
* BODY - UID FETCH (BODY.PEEK[])
* Part - UID FETCH (BODY.PEEK[ID]) where ID = mime part ID
*/
//以上是一个消息对应的各种字段
final
LinkedHashSet<string> fetchFields =
new
LinkedHashSet<string>();
//根据FetchProfile指定的内容,填充命令
//下载Voicemail时,指定的字段是FetchProfile.Item.BODY
fetchFields.add(ImapConstants.UID);
if
(fp.contains(FetchProfile.Item.FLAGS)) {
...............
}
if
(fp.contains(FetchProfile.Item.ENVELOPE)) {
..............
}
if
(fp.contains(FetchProfile.Item.STRUCTURE)) {
............
}
if
(fp.contains(FetchProfile.Item.BODY_SANE)) {
..........
}
if
(fp.contains(FetchProfile.Item.BODY)) {
fetchFields.add(ImapConstants.FETCH_FIELD_BODY_PEEK);
}
//对第一个字段特殊处理,为了满足编码或协议要求吧
final
Part fetchPart = fp.getFirstPart();
if
(fetchPart !=
null
) {
final
String[] partIds =
fetchPart.getHeader(MimeHeader.HEADER_ANDROID_ATTACHMENT_STORE_DATA);
if
(partIds !=
null
) {
fetchFields.add(ImapConstants.FETCH_FIELD_BODY_PEEK_BARE
+
"["
+ partIds[
0
] +
"]"
);
}
}
try
{
//依然利用ImapConnection进行网络交互
mConnection.sendCommand(String.format(Locale.US,
ImapConstants.UID_FETCH +
" %s (%s)"
, ImapStore.joinMessageUids(messages),
Utility.combine(fetchFields.toArray(
new
String[fetchFields.size()]),
' '
)
),
false
);
mapResponse response;
do
{
response =
null
;
try
{
//读取返回结果,有阻塞能力
response = mConnection.readResponse();
//仅处理FETCH对应的Response
if
(!response.isDataResponse(
1
, ImapConstants.FETCH)) {
continue
;
// Ignore
}
final
ImapList fetchList = response.getListOrEmpty(
2
);
//根据FetchProfile的定义,进行解码操作
...............
if
(fp.contains(FetchProfile.Item.BODY)
|| fp.contains(FetchProfile.Item.BODY_SANE)) {
// Body is keyed by "BODY[]...".
// Previously used "BODY[..." but this can be confused with "BODY[HEADER..."
// TODO Should we accept "RFC822" as well??
ImapString body = fetchList.getKeyedStringOrEmpty(
"BODY[]"
,
true
);
InputStream bodyStream = body.getAsStream();
//解码操作
message.parse(bodyStream);
}
............
if
(listener !=
null
) {
//解析完毕,调用ImapHelper中内部类的回调接口,才能够返回
listener.messageRetrieved(message);
}
}
finally
{
destroyResponses();
}
}
while
(!response.isTagged());
}
catch
(IOException ioe) {
throw
ioExceptionHandler(mConnection, ioe);
}
}</string></string></string,></string,>
|
不出所料,fetch函数与网络的交互工作,依然需要拜托给ImapConnection,下载的实际内容由FetchProfile定义。
当下载完成后,fetch函数进行相应的解码工作,然后调用ImapHelper中定义的回调接口。
6、ImapConnection
前面的流程网络交互相关的内容,全部由ImapConnection来完成。
主要涉及到了ImapConnection的构造函数、executeSimpleCommand、sendCommand和readResponse接口。
现在我们来看看这部分接口对应的流程。
6.1 构造函数
1
2
3
4
5
6
7
8
|
ImapConnection(ImapStore store) {
setStore(store);
}
void
setStore(ImapStore store) {
mImapStore = store;
mLoginPhrase =
null
;
}
|
ImapConnection的构造函数比较简单,主要是保存ImapStore和LoginPhrase。
LoginPhrase是String对象,即访问服务器的口令。
6.2 executeSimpleCommand
我们看看向网络侧发送命令用到的executeSimpleCommand函数:
1
2
3
4
5
6
7
8
9
10
11
12
|
List<imapresponse> executeSimpleCommand(String command)
throws
IOException, MessagingException{
return
executeSimpleCommand(command,
false
);
}
List<imapresponse> executeSimpleCommand(String command,
boolean
sensitive)
throws
IOException, MessagingException {
//executeSimpleCommand是通过sendCommand发送命令的
sendCommand(command, sensitive);
//getCommandResponses获取执行结果
return
getCommandResponses();
}</imapresponse></imapresponse>
|
从代码可以看出,executeSimpleCommand打包了发送和接收过程。
6.2.1 sendCommand
我们先看看发送过程对应的sendCommand函数:
1
2
3
4
5
6
7
8
9
10
11
|
String sendCommand(String command,
boolean
sensitive)
throws
IOException, MessagingException {
//完成一些必要的初始化工作
open();
.........
String tag = Integer.toString(mNextCommandTag.incrementAndGet());
String commandToSend = tag +
" "
+ command;
//利用MailTransport进行写操作
mTransport.writeLine(commandToSend, (sensitive ? IMAP_REDACTED_LOG : command));
return
tag;
}
|
上面代码中有两个重要的地方,一是open函数完成的初始化工作;二是MailTransport的writeLine函数。
6.2.1.1 open
MailTransport的内容,放在后面说。先看看ImapConnection的open函数:
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
31
32
33
34
|
void
open()
throws
IOException, MessagingException {
//避免重复打开
if
(mTransport !=
null
&& mTransport.isOpen()) {
return
;
}
try
{
if
(mTransport ==
null
) {
//利用ImapStore创建MailTransport
//实际上ImapStore初始化时已经创建了MailTransport,此处调用MailTransport的clone方法
mTransport = mImapStore.cloneTransport();
//调用MailTransport的open接口,连接服务器
//重点部分后文分析
mTransport.open();
//创建出ImapResponseParser,内含PeekableInputStream封装MailTransport的输入流
createParser();
doLogin();
}
}
catch
(SSLException e) {
LogUtils.d(TAG,
"SSLException "
, e);
mImapStore.getImapHelper().setDataChannelState(Status.DATA_CHANNEL_STATE_SERVER_ERROR);
throw
new
CertificateValidationException(e.getMessage(), e);
}
catch
(IOException ioe) {
LogUtils.d(TAG,
"IOException"
, ioe);
mImapStore.getImapHelper()
.setDataChannelState(Status.DATA_CHANNEL_STATE_COMMUNICATION_ERROR);
throw
ioe;
}
finally
{
destroyResponses();
}
}
|
ImapConnection的open函数内容很丰富,主要包括3部分:
1、调用MailTransport的open接口,这里将会和网络交互得到输入输出流;
2、创建出ImapResponseParser,该对象将分装输入流,将字节流解析成ImapResponse;
3、调用doLogin函数,完成登陆工作。
MailTransport相关的工作留在后文分析,此处仅跟进一下doLogin函数:
1
2
3
4
5
6
7
8
9
10
|
private
void
doLogin()
throws
IOException, MessagingException, AuthenticationFailedException {
try
{
//再次调用executeSimpleCommand
//此时不在需要open MailTransport,直接往服务端写信息即可
executeSimpleCommand(getLoginPhrase(),
true
);
}
catch
(ImapException ie) {
//分析异常原因,作纪录后抛出异常
.........
}
}
|
我们看看getLoginPhrase函数:
1
2
3
4
5
6
7
8
9
10
11
|
String getLoginPhrase() {
if
(mLoginPhrase ==
null
) {
if
(mImapStore.getUsername() !=
null
&& mImapStore.getPassword() !=
null
) {
// build the LOGIN string once (instead of over-and-over again.)
// apply the quoting here around the built-up password
mLoginPhrase = ImapConstants.LOGIN +
" "
+ mImapStore.getUsername() +
" "
+ ImapUtility.imapQuoted(mImapStore.getPassword());
}
}
return
mLoginPhrase;
}
|
从上面的代码可以看出mLoginPhrase就是用户名和密码组成的登陆字符串。
6.2.2 getCommandResponses
当向服务器发送命令成功后,我们利用getCommandResponses函数获取返回结果:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
List<imapresponse> getCommandResponses()
throws
IOException, MessagingException {
final
List<imapresponse> responses =
new
ArrayList<imapresponse>();
ImapResponse response;
do
{
//利用ImapResponserParser读取结果,此处会阻塞
//ImapConnection的readResponse函数,就是利用这行代码读取response
response = mParser.readResponse();
responses.add(response);
}
while
(!response.isTagged());
if
(!response.isOk()) {
//错误处理,记录,抛异常等
.........
}
return
responses;
}</imapresponse></imapresponse></imapresponse>
|
上面这段代码中,利用ImapResponserParser读取ImapResponse。
ImapResponserParser中封装了与网络交互的InputStream,将调用InputStream.read函数得到字节流,然后进行解码工作。
这里知道原理即可,解码的细节不作关注。
7、MailTransport
最后我们看看MailTransport相关的流程。
从上文来看,我们知道MailTransport是实际与网络打交道的类,它负责建立起网络连接,负责命令的发送。
这里我们主要分析前面流程里提到的MailTransport.open函数和MailTransport.writeLine函数。
7.1 MailTransport.open
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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
|
public
void
open()
throws
MessagingException {
............
//得到目的端网络地址
List<inetsocketaddress> socketAddresses =
new
ArrayList<inetsocketaddress>();
if
(mNetwork ==
null
) {
//无网络的情况下,利用host和port来构建
socketAddresses.add(
new
InetSocketAddress(mHost, mPort));
}
else
{
try
{
//有网络时,利用网络解析目的端对应的Ip地址
InetAddress[] inetAddresses = mNetwork.getAllByName(mHost);
............
for
(
int
i =
0
; i < inetAddresses.length; i++) {
socketAddresses.add(
new
InetSocketAddress(inetAddresses[i], mPort));
}
}
catch
(IOException ioe) {
...........
}
}
boolean
success =
false
;
while
(socketAddresses.size() >
0
) {
//利用Network的SocketFactory创建socket
mSocket = createSocket();
try
{
InetSocketAddress address = socketAddresses.remove(
0
);
//连接服务器
mSocket.connect(address, SOCKET_CONNECT_TIMEOUT);
//若支持加密传输
if
(canTrySslSecurity()) {
LogUtils.d(TAG,
"open: converting to SSL socket"
);
//将普通socket转换为SSL socket
mSocket = HttpsURLConnection.getDefaultSSLSocketFactory()
.createSocket(mSocket, address.getHostName(), address.getPort(),
true
);
if
(!canTrustAllCertificates()) {
//如果需要,进行验证
verifyHostname(mSocket, mHost);
}
}
//得到输入流和输出流
mIn =
new
BufferedInputStream(mSocket.getInputStream(),
1024
);
mOut =
new
BufferedOutputStream(mSocket.getOutputStream(),
512
);
//超时时间为1min
mSocket.setSoTimeout(SOCKET_READ_TIMEOUT);
success =
true
;
return
;
}
catch
(IOException ioe) {
..........
}
finally
{
if
(!success) {
try
{
mSocket.close();
mSocket =
null
;
}
catch
(IOException ioe) {
..........
}
}
}
}
}</inetsocketaddress></inetsocketaddress>
|
1
2
3
4
5
6
7
8
9
10
11
12
|
private
void
verifyHostname(Socket socket, String hostname)
throws
IOException {
SSLSocket ssl = (SSLSocket) socket;
ssl.startHandshake();
..........
SSLSession session = ssl.getSession();
.........
//HOSTNAME_VERIFIER由HttpsURLConnection.getDefaultHostnameVerifier得到
if
(!HOSTNAME_VERIFIER.verify(hostname, session)) {
//抛异常
...........
}
}
|
MailTransport的open函数很长,但意思很清晰:就是创建出与服务器通信的socket,得到交互的输入输出流。
如果需要SSL加密的话,则创建的是SSLSocket,同时利用HostnameVerifier对HostName进行验证。
7.2 MailTransport.writeLine
1
2
3
4
5
6
7
8
9
|
public
void
writeLine(String s, String sensitiveReplacement)
throws
IOException {
.............
OutputStream out = getOutputStream();
out.write(s.getBytes());
out.write(
'\r'
);
out.write(
'\n'
);
out.flush();
}
|
了解MailTransport.open函数后,writeLine函数就比较简单了,就是利用输出流将命令以字节流的方式发送给服务器。
三、总结
以上是Android 7.0原生代码中,Voicemail的下载流程。
整个思想比较简单,但涉及较多的封装和回调,带来了一定的阅读困难。
整体来讲,整个逻辑大概可以缩略为下图:
较为详细的函数调用过程为:
大图地址