本文代码以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 = Tables.CALLS + " LEFT JOIN " + " (SELECT * FROM " + Views.DATA + " WHERE " + Data._ID + " IN " + "(SELECT " + Calls.DATA_ID + " FROM " + Tables.CALLS + ")) AS " + Views.DATA + " ON(" + Tables.CALLS + "." + Calls.DATA_ID + " = " + Views.DATA + "." + Data._ID + ")"; ...... private static final HashMap<String, String> sCallsJoinDataViewProjectionMap; ...... @Override public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { final SQLiteQueryBuilder qb = new SQLiteQueryBuilder(); ....... switch (match) { ...... case CALLS_JION_DATA_VIEW: { qb.setTables(mstableCallsJoinData); qb.setProjectionMap(sCallsJoinDataViewProjectionMap); qb.setStrict(true); break; } case CALLS_JION_DATA_VIEW_ID: { qb.setTables(mstableCallsJoinData);//将查询这个数据集合,<span style="line-height: 23.9999980926514px; font-family: Arial;">mstableCallsJoinData</span>前面已定义 qb.setProjectionMap(sCallsJoinDataViewProjectionMap); qb.setStrict(true); selectionBuilder.addClause(getEqualityClause(Tables.CALLS + "." + Calls._ID, parseCallIdFromUri(uri))); break; } ...... } ...... } ...... }
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 = array; } long metadata = ((long)size << 32) | cursorPosition; if (expanded) { metadata |= EXPANDED_GROUP_MASK; } mGroupMetadata[mGroupCount++] = metadata; }
mGroupMetadata是long型数组,初始大小为GROUP_METADATA_ARRAY_INITIAL_SIZE,16,当空间不够时,每次以GROUP_METADATA_ARRAY_INCREMENT(128)增大。
通话记录ListView和Adapter的数据绑定是在GroupingListAdapter中的getView()方法中,此类继承自BaseAdapter,来看一下它的继承结构:
通话记录的数据加载先说到这里。
右键复制图片地址,在浏览器中打开即可查看大图。
未完待续,有不对的地方,请指正。