首页 > 代码库 > Android4.4 Telephony流程分析——拨号应用(Dialer)的通话记录加载过程

Android4.4 Telephony流程分析——拨号应用(Dialer)的通话记录加载过程

本文代码以MTK平台Android 4.4为分析对象,与Google原生AOSP有些许差异,请读者知悉。


Android系统通话记录存储在联系人数据库contacts2.db中的calls表中,通话记录(calllog)存储到数据库的时机可查看我之前的一篇博客Android4.4 Telephony流程分析——电话挂断step39,系统提供了CallLogProvider这个ContentProvider来供外界访问。我们来看本文将会使用到的CallLogProvider的代码片段:

/**
 * Call log content provider.
 */
public class CallLogProvider extends ContentProvider {

......
    private static final int CALLS_JION_DATA_VIEW = 5;
    private static final int CALLS_JION_DATA_VIEW_ID = 6;
......
    private static final UriMatcher sURIMatcher = new UriMatcher(UriMatcher.NO_MATCH);
    static {
        sURIMatcher.addURI(CallLog.AUTHORITY, "calls", CALLS);
        sURIMatcher.addURI(CallLog.AUTHORITY, "calls/#", CALLS_ID);
        sURIMatcher.addURI(CallLog.AUTHORITY, "calls/filter/*", CALLS_FILTER);
        sURIMatcher.addURI(CallLog.AUTHORITY, "calls/search_filter/*", CALLS_SEARCH_FILTER);
        sURIMatcher.addURI(CallLog.AUTHORITY, "callsjoindataview", CALLS_JION_DATA_VIEW);
        sURIMatcher.addURI(CallLog.AUTHORITY, "callsjoindataview/#", CALLS_JION_DATA_VIEW_ID);
        sURIMatcher.addURI(CallLog.AUTHORITY, SearchManager.SUGGEST_URI_PATH_QUERY, SEARCH_SUGGESTIONS);
        sURIMatcher.addURI(CallLog.AUTHORITY, SearchManager.SUGGEST_URI_PATH_QUERY + "/*", SEARCH_SUGGESTIONS);
        sURIMatcher.addURI(CallLog.AUTHORITY, SearchManager.SUGGEST_URI_PATH_SHORTCUT + "/*", SEARCH_SHORTCUT);
    }
    private static final HashMap<String, String> sCallsProjectionMap;
......
    private static final String mstableCallsJoinData = http://www.mamicode.com/Tables.CALLS + " LEFT JOIN " >
calls表的主要字段及其数据类型可查看下表:



下面是Dialer中通话记录的加载时序图,此图只关注calllog数据的处理:


Dialer模块是Android4.4之后才独立处理的,整个模块大部分的UI显示都是使用Framgment实现。触发通话记录刷新加载的的操作比较多,如Fragment onResume()时、数据库更新时、选择了通话记录过滤等,这些操作都会使用step2的refreshData()方法来查询数据库。

step3~step4,刷新通话记录联系人图片缓存,联系人图片缓存使用的是LruCache技术,异步加载,后面再发博文分析。

step5,读取sim卡过滤设置、通话类型设置,开始查询,

    public void startCallsQuery() {
        mAdapter.setLoading(true);//step6,正在加载联系人,此时联系人列表不显示为 空

        SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this.getActivity());
        int simFilter = prefs.getInt(Constants.SIM_FILTER_PREF, Constants.FILTER_SIM_DEFAULT);//要查看calllog的SIM卡
        int typeFilter = prefs.getInt(Constants.TYPE_FILTER_PREF, Constants.FILTER_TYPE_DEFAULT);//通话类型:来电?去电?未接?全部?
        mCallLogQueryHandler.fetchCallsJionDataView(simFilter, typeFilter);
        /* add wait cursor */
        int count = this.getListView().getCount();
        Log.i(TAG, "***********************count : " + count);
        mIsFinished = false;

        if (0 == count) {//现在列表中记录为空,显示等待加载控件
            Log.i(TAG, "call sendmessage");
            mHandler.sendMessageDelayed(mHandler.obtainMessage(WAIT_CURSOR_START),
                    WAIT_CURSOR_DELAY_TIME);
        }
    }

step7~step11,一步一步将添加查询条件,将查询请求提交给ContentProvider。

step9,设置查询Uri,

        if (QUERY_ALL_CALLS_JOIN_DATA_VIEW_TOKEN == token) {
            queryUri = Uri.parse("content://call_log/callsjoindataview");
            queryProjection = CallLogQueryEx.PROJECTION_CALLS_JOIN_DATAVIEW;
        }
CallLogQueryHandlerEx、NoNullCursorAsyncQueryHandler抽象类、AsyncQueryHandler抽象类是继承关系,继承自Handler,


AsyncQueryHandler是Framework中提供的异步查询类,定义在\frameworks\base\core\java\android\content,step10将查询请求交给它,

    public void startQuery(int token, Object cookie, Uri uri,
            String[] projection, String selection, String[] selectionArgs,
            String orderBy) {
        // Use the token as what so cancelOperations works properly
        Message msg = mWorkerThreadHandler.obtainMessage(token);//mWorkerThreadHandler是WorkerHandler的对象,也是一个Handler,与工作线程通信
        msg.arg1 = EVENT_ARG_QUERY;

        WorkerArgs args = new WorkerArgs();
        args.handler = this;//this即<span style="font-family: Arial; line-height: 23.9999980926514px;">AsyncQueryHandler,用于</span>工作线程返回查询结果Cursor
        args.uri = uri;
        args.projection = projection;
        args.selection = selection;
        args.selectionArgs = selectionArgs;
        args.orderBy = orderBy;
        args.cookie = cookie;
        msg.obj = args;

        mWorkerThreadHandler.sendMessage(msg);//查询将在工作线程中进行
    }

step12~step15,工作线程将查询结果返回给AsyncQueryHandler的handleMessage()处理。

    protected class WorkerHandler extends Handler {
        public WorkerHandler(Looper looper) {
            super(looper);
        }

        @Override
        public void handleMessage(Message msg) {
            final ContentResolver resolver = mResolver.get();
            if (resolver == null) return;

            WorkerArgs args = (WorkerArgs) msg.obj;

            int token = msg.what;
            int event = msg.arg1;

            switch (event) {
                case EVENT_ARG_QUERY:
                    Cursor cursor;
                    try {
                        cursor = resolver.query(args.uri, args.projection,
                                args.selection, args.selectionArgs,
                                args.orderBy);
                        // Calling getCount() causes the cursor window to be filled,
                        // which will make the first access on the main thread a lot faster.
                        if (cursor != null) {
                            cursor.getCount();
                        }
                    } catch (Exception e) {
                        Log.w(TAG, "Exception thrown during handling EVENT_ARG_QUERY", e);
                        cursor = null;
                    }

                    args.result = cursor;//查询结果cursor
                    break;

                    ......
            }

            // passing the original token value back to the caller
            // on top of the event values in arg1.
            Message reply = args.handler.obtainMessage(token); //args.handler就是上文提到的this
            reply.obj = args;
            reply.arg1 = msg.arg1; //EVENT_ARG_QUERY

            reply.sendToTarget();
        }
    }

step16,查询完成,返回cursor,判断cursor是否为空。

    @Override
    protected final void onQueryComplete(int token, Object cookie, Cursor cursor) {
        CookieWithProjection projectionCookie = (CookieWithProjection) cookie;

        super.onQueryComplete(token, projectionCookie.originalCookie, cursor);

        if (cursor == null) {//通话记录为空,创建一个空的cursor返回
            cursor = new EmptyCursor(projectionCookie.projection);
        }
        onNotNullableQueryComplete(token, projectionCookie.originalCookie, cursor);//step17
    }

step18~step19,将结果cursor返回给CallLogFragmentEx。

    @Override
    public void onCallsFetched(Cursor cursor) {
        .......
        mAdapter.setLoading(false);//与step6对应
        mAdapter.changeCursor(cursor);//更改CallLogListAdapter的cursor,刷新ListView
        // when dialpadfrangment is in forgoround, not update dial pad menu item.
        Activity activity = getActivity();
        /// M: for refresh option menu;
        activity.invalidateOptionsMenu();
        if (mScrollToTop) {
            //Modified by Lee 2014-06-30 for flip sms and call start
            final HYListView listView = (HYListView)getListView();
            //Modified by Lee 2014-06-30 for flip sms and call end

            if (listView.getFirstVisiblePosition() > 5) {
                listView.setSelection(5);
            }
           
            listView.setSelection(0);
            mScrollToTop = false;
        }
        mCallLogFetched = true;
        
        /** M: add :Bug Fix for ALPS00115673 @ { */
        Log.i(TAG, "onCallsFetched is call");
        mIsFinished = true;
        mLoadingContainer.startAnimation(AnimationUtils.loadAnimation(getActivity(),
                android.R.anim.fade_out));
        mLoadingContainer.setVisibility(View.GONE);
        mLoadingContact.setVisibility(View.GONE);
        mProgress.setVisibility(View.GONE);
        // hide calldetail view,let no call log warning show on all screen
        if (mCallDetail != null) {
            if (cursor == null || cursor.getCount() == 0) {
                mCallDetail.setVisibility(View.GONE);
            } else {
                mCallDetail.setVisibility(View.VISIBLE);
            }
        }

        mEmptyTitle.setText(R.string.recentCalls_empty);
        /** @ }*/

        destroyEmptyLoaderIfAllDataFetched();
        // send message,the message will execute after the listview inflate
        handle.sendEmptyMessage(SETFIRSTTAG); //设置ListView第一条可显示的数据
    }

step24~step32,主要是处理通话记录的分组显示。

step26中是具体的分组规则、分组过程:

    public void addGroups(Cursor cursor) {
        final int count = cursor.getCount();
        if (count == 0) {
            return;
        }

        int currentGroupSize = 1;
        cursor.moveToFirst();
        // The number of the first entry in the group.
        String firstNumber = cursor.getString(CallLogQueryEx.CALLS_JOIN_DATA_VIEW_NUMBER);
        // This is the type of the first call in the group.
        int firstCallType = cursor.getInt(CallLogQueryEx.CALLS_JOIN_DATA_VIEW_CALL_TYPE);

        //The following lines are provided and maintained by Mediatek Inc.
        int firstSimId = cursor.getInt(CallLogQueryEx.CALLS_JOIN_DATA_VIEW_SIM_ID);
        int firstVtCall = cursor.getInt(CallLogQueryEx.CALLS_JOIN_DATA_VIEW_VTCALL);
        long firstDate = cursor.getLong(CallLogQueryEx.CALLS_JOIN_DATA_VIEW_DATE);
        if (0 != cursor.getCount()) {
            setGroupHeaderPosition(cursor.getPosition());
        }
        /// @}

        while (cursor.moveToNext()) {
            // The number of the current row in the cursor.
            final String currentNumber = cursor.getString(CallLogQueryEx.CALLS_JOIN_DATA_VIEW_NUMBER);
            final int callType = cursor.getInt(CallLogQueryEx.CALLS_JOIN_DATA_VIEW_CALL_TYPE);
            /// @}
            final boolean sameNumber = equalNumbers(firstNumber, currentNumber);
            final boolean shouldGroup;
            /// M: add @{
            final int simId = cursor.getInt(CallLogQueryEx.CALLS_JOIN_DATA_VIEW_SIM_ID);
            final int vtCall = cursor.getInt(CallLogQueryEx.CALLS_JOIN_DATA_VIEW_VTCALL);
            final long date = cursor.getLong(CallLogQueryEx.CALLS_JOIN_DATA_VIEW_DATE);
            final boolean isSameDay = CallLogDateFormatHelper.isSameDay(firstDate, date);
            /// @ }
            /// M: [VVM] voice mail should not be grouped.
            if (firstCallType == Calls.VOICEMAIL_TYPE || !sameNumber || firstCallType != callType
                    || firstSimId != simId || firstVtCall != vtCall || !isSameDay) { //看注释
                // Should only group with calls from the same number, the same
                // callType, the same simId and the same vtCall values.
                shouldGroup = false; //这个条件下,ListView需要显示一条记录
            } else {
                shouldGroup = true; //同一个group ListView只显示一条记录,加上通话记录数目
            }
            /// @}

            if (shouldGroup) {
                // Increment the size of the group to include the current call, but do not create
                // the group until we find a call that does not match.
                currentGroupSize++; //累加
            } else {
                // Create a group for the previous set of calls, excluding the current one, but do
                // not create a group for a single call.
                addGroup(cursor.getPosition() - currentGroupSize, currentGroupSize);
                if (!isSameDay) { //不是同一天的通话记录,需要显示Header(日期)
                    setGroupHeaderPosition(cursor.getPosition());
                }
                /// @}
                // Start a new group; it will include at least the current call.
                currentGroupSize = 1;
                // The current entry is now the first in the group.//上一条记录为参考值,比较
                firstNumber = currentNumber;
                firstCallType = callType;
                /// M: add @{
                firstCallType = callType;
                firstSimId = simId;
                firstVtCall = vtCall;
                firstDate = date;
                /// @}
            }
        }

        addGroup(count - currentGroupSize, currentGroupSize);
        /// @}
    }


step27~step29,记录需要设置Header的位置到mHeaderPositionList这个HashMap中,

    public void setGroupHeaderPosition(int cursorPosition) {
        mHeaderPositionList.put(Integer.valueOf(cursorPosition), Boolean.valueOf(true));
    }

step30~step32,记录一个Group(ListView的一个item)的开始位置和大小(包含的通话记录数目)于mGroupMetadata,

    protected void addGroup(int cursorPosition, int size, boolean expanded) {
        if (mGroupCount >= mGroupMetadata.length) {
            int newSize = idealLongArraySize(
                    mGroupMetadata.length + GROUP_METADATA_ARRAY_INCREMENT);
            long[] array = new long[newSize];
            System.arraycopy(mGroupMetadata, 0, array, 0, mGroupCount);
            mGroupMetadata = http://www.mamicode.com/array;>
mGroupMetadata是long型数组,初始大小为GROUP_METADATA_ARRAY_INITIAL_SIZE,16,当空间不够时,每次以GROUP_METADATA_ARRAY_INCREMENT(128)增大。


通话记录ListView和Adapter的数据绑定是在GroupingListAdapter中的getView()方法中,此类继承自BaseAdapter,来看一下它的继承结构:


通话记录的数据加载先说到这里。


右键复制图片地址,在浏览器中打开即可查看大图。

未完待续,有不对的地方,请指正。



Android4.4 Telephony流程分析——拨号应用(Dialer)的通话记录加载过程