編輯:關於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 Listresponses = 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(Listresponses) { // 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(); HashMapmessageMap = 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函數:
ListexecuteSimpleCommand(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函數獲取返回結果:
ListgetCommandResponses() 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 { ............ //得到目的端網絡地址 ListsocketAddresses = 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的下載流程。
整個思想比較簡單,但涉及較多的封裝和回調,帶來了一定的閱讀困難。
整體來講,整個邏輯大概可以縮略為下圖:
較為詳細的函數調用過程為:
大圖地址
MainActivity.java 0) { startThreads(threads, iterations); } } }); }
自從項目中使用RxJava以來,可以很方便的切換線程。至於是怎麼實現的,一直沒有深入的研究過!本篇文章就是分析RxJava的線程模型。 RxJava基本使用 先上一個
本文實例為大家分享了Android實現蒙板效果的相關代碼,供大家參考,具體內容如下1、不保留標題欄蒙板的實現效果:原理:1、彈窗時,設置背景窗體的透明度2、取消彈窗時,恢
在網上查了好多資料,大致都雷同,大家都是互相抄襲的,看著很費勁,不好理解,自己總結一下,留著需要看的話來查找。代碼中的例子如下 <ImageView