今天接到一个任务,需要解决同事在美国测试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<Void, Void, Boolean>() {
@Override
public Boolean doInBackground(Void... params) {
//查询数据库,判断下载的信息是否写入数据库
return queryHasContent(mVoicemailUri);