Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> Android7.0 Voicemail (1) Voicemail的下載流程

Android7.0 Voicemail (1) Voicemail的下載流程

編輯:關於Android編程

今天接到一個任務,需要解決同事在美國測試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中下載相關,比較主要的代碼:

//注意到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函數:

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函數:

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:

//注意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() {

            @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 */);
                    }
                }
            }
        });
    }
}

從上面的代碼,我們知道了FetchResultHandler主要用於監控Voicemail是否在規定時間內下載完畢。
在FetchResultHandler創建時,發送了一個延遲消息;當延遲消息被執行時,若發現消息仍未下載完,就會在界面顯示出錯信息。
在延遲消息執行之前,若FetchResultHandler監控到數據變化,並判斷出Voicemail下載成功,就可以為播放做相應的准備工作了。

了解了FetchResultHandler的功能後,我們將目光投向下載相關的廣播消息。

3、FetchVoicemailReceiver
3.1 onReceive
在源碼中,FetchVoicemailReceiver負責接收VoicemailContract.ACTION_FETCH_VOICEMAIL:

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:

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類:

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函數:

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:

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 構造函數

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的構造函數:

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函數了,在這個函數中將進行數據下載:

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

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)

//此時已經用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的相關定義:

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函數。
我們依次進行分析。

public ImapFolder(ImapStore store, String name) {
    mStore = store;
    mName = name;
}

ImapFolder的構造函數比較簡單,主要是保存ImapStore對象。

5.1 open

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

/**
* 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 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;
}

從上面的代碼可以看出,doSelect主要是選中Voicemail用戶對應的文件夾,同時得到其中的信息數量及讀寫模式。
這些工作需要與網絡進行交互才能完成,將被委托給ImapConnection進行處理。
ImapConnection的工作,將於後文介紹。

5.2 getMessage
接下來,我們看看ImapFolder的getMessage函數。

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:

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 responses) {
    // S: * SEARCH 2 3 6
    final ArrayList uids = new ArrayList();
    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);
}

從上面的代碼,我們知道ImapFolder的getMessage函數,依然需要利用ImapConnection與網絡交互,
最終返回的結果僅用於定義所有需要下載的消息。

5.3 fetch
ImapFolder的fetch函數才是實際下載消息的接口。

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 messageMap = new HashMap();
    //這裡是為同時下載多條消息做的設計
    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 fetchFields = new LinkedHashSet();

    //根據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);
    }
}

不出所料,fetch函數與網絡的交互工作,依然需要拜托給ImapConnection,下載的實際內容由FetchProfile定義。
當下載完成後,fetch函數進行相應的解碼工作,然後調用ImapHelper中定義的回調接口。

6、ImapConnection
前面的流程網絡交互相關的內容,全部由ImapConnection來完成。
主要涉及到了ImapConnection的構造函數、executeSimpleCommand、sendCommand和readResponse接口。
現在我們來看看這部分接口對應的流程。

6.1 構造函數

ImapConnection(ImapStore store) {
    setStore(store);
}

void setStore(ImapStore store) {
    mImapStore = store;
    mLoginPhrase = null;
}

ImapConnection的構造函數比較簡單,主要是保存ImapStore和LoginPhrase。
LoginPhrase是String對象,即訪問服務器的口令。

6.2 executeSimpleCommand

我們看看向網絡側發送命令用到的executeSimpleCommand函數:

List executeSimpleCommand(String command)
        throws IOException, MessagingException{
    return executeSimpleCommand(command, false);
}

List executeSimpleCommand(String command, boolean sensitive)
        throws IOException, MessagingException {
    //executeSimpleCommand是通過sendCommand發送命令的
    sendCommand(command, sensitive);
    //getCommandResponses獲取執行結果
    return getCommandResponses();
}

從代碼可以看出,executeSimpleCommand打包了發送和接收過程。

6.2.1 sendCommand
我們先看看發送過程對應的sendCommand函數:

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函數:

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函數:

private void doLogin() throws IOException, MessagingException, AuthenticationFailedException {
    try {
        //再次調用executeSimpleCommand
        //此時不在需要open MailTransport,直接往服務端寫信息即可
        executeSimpleCommand(getLoginPhrase(), true);
    } catch (ImapException ie) {
        //分析異常原因,作紀錄後拋出異常
        .........
    }
}

我們看看getLoginPhrase函數:

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函數獲取返回結果:

List getCommandResponses() throws IOException, MessagingException {
    final List responses = new ArrayList();
    ImapResponse response;
    do {
        //利用ImapResponserParser讀取結果,此處會阻塞
        //ImapConnection的readResponse函數,就是利用這行代碼讀取response
        response = mParser.readResponse();
        responses.add(response);
    } while (!response.isTagged());

    if (!response.isOk()) {
        //錯誤處理,記錄,拋異常等
        .........
    }
    return responses;
}

上面這段代碼中,利用ImapResponserParser讀取ImapResponse。
ImapResponserParser中封裝了與網絡交互的InputStream,將調用InputStream.read函數得到字節流,然後進行解碼工作。
這裡知道原理即可,解碼的細節不作關注。

7、MailTransport
最後我們看看MailTransport相關的流程。
從上文來看,我們知道MailTransport是實際與網絡打交道的類,它負責建立起網絡連接,負責命令的發送。

這裡我們主要分析前面流程裡提到的MailTransport.open函數和MailTransport.writeLine函數。

7.1 MailTransport.open

public void open() throws MessagingException {
    ............
    //得到目的端網絡地址
    List socketAddresses = new ArrayList();
    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) {
                    ..........
                }
            }
        }
    }
}
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

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的下載流程。
整個思想比較簡單,但涉及較多的封裝和回調,帶來了一定的閱讀困難。
整體來講,整個邏輯大概可以縮略為下圖:

較為詳細的函數調用過程為:

大圖地址

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved