本文代码以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);
/// @}
}
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,来看一下它的继承结构:
通话记录的数据加载先说到这里。
右键复制图片地址,在浏览器中打开即可查看大图。
未完待续,有不对的地方,请指正。
版权声明:本文为博主原创文章,未经博主允许不得转载。