voice mail 下载流程

今天接到一个任务,需要解决同事在美国测试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的下载流程。
整个思想比较简单,但涉及较多的封装和回调,带来了一定的阅读困难。
整体来讲,整个逻辑大概可以缩略为下图:

较为详细的函数调用过程为:

大图地址

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值