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

今天接到一个任务,需要解决同事在美国测试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);
   
  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值