这段时间在搞一个隐私联系人功能,没办法又重新梳理了一下通话记录的生成和展现过程,顺便贴出来跟大家分享一下。
一、通话记录的保存分为七步:
1、CallsManager.java 的构造函数中
// PhoneStateListener 监听电话是否断开连接
mListeners.add(mCallLogManager);
2、CallLogManager.java
// onCallStateChanged用于监听通话状态,设置状态 OUTGOING_TYPE、MISSED_TYPE和INCOMING_TYPE
public void onCallStateChanged(Call call, int oldState, int newState) {
logCall(call, type);
}
3、CallLogManager.java —> logCall()
// 插入一个通话记录
void logCall(Call call, int callLogType) {
final long creationTime = call.getCreationTimeMillis();
final long age = call.getAgeMillis();
final String logNumber = getLogNumber(call);
Log.d(TAG, "logNumber set to: %s", Log.pii(logNumber));
final int presentation = getPresentation(call);
final PhoneAccountHandle accountHandle = call.getTargetPhoneAccount();
// TODO(vt): Once data usage is available, wire it up here.
int callFeatures = getCallFeatures(call.getVideoStateHistory());
// 插入一个通话记录
logCall(call.getCallerInfo(), logNumber, presentation, callLogType, callFeatures,
accountHandle, creationTime, age, null);
}
4、AddCallArgs —> AddCallArgs()
// CallLogAsync.AddCallArgs这个类即为管理增加通话记录的类;
// CallLogManager.java
private void logCall(
CallerInfo callerInfo,
String number,
int presentation,
int callType,
int features,
PhoneAccountHandle accountHandle,
long start,
long duration,
Long dataUsage) {
boolean isEmergencyNumber = PhoneNumberUtils.isLocalEmergencyNumber(mContext, number);
// On some devices, to avoid accidental redialing of emergency numbers, we *never* log
// emergency calls to the Call Log. (This behavior is set on a per-product basis, based
// on carrier requirements.)
final boolean okToLogEmergencyNumber =
mContext.getResources().getBoolean(R.bool.allow_emergency_numbers_in_call_log);
// Don't log emergency numbers if the device doesn't allow it.
final boolean isOkToLogThisCall = !isEmergencyNumber || okToLogEmergencyNumber;
sendAddCallBroadcast(callType, duration);
if (isOkToLogThisCall) {
Log.d(TAG, "Logging Calllog entry: " + callerInfo + ", "
+ Log.pii(number) + "," + presentation + ", " + callType
+ ", " + start + ", " + duration);
// CallLogAsync.AddCallArgs这个类即为管理增加通话记录的类;
AddCallArgs args = new AddCallArgs(mContext, callerInfo, number, presentation,
callType, features, accountHandle, start, duration, dataUsage);
logCallAsync(args);
} else {
Log.d(TAG, "Not adding emergency call to call log.");
}
}
// AddCallArgs.java
public AsyncTask<AddCallArgs, Void, Uri[]> logCallAsync(AddCallArgs args) {
return new LogCallAsyncTask().execute(args);
}
/**
* Helper AsyncTask to access the call logs database asynchronously since database operations
* can take a long time depending on the system's load. Since it extends AsyncTask, it uses
* its own thread pool.
*/
private class LogCallAsyncTask extends AsyncTask<AddCallArgs, Void, Uri[]> {
@Override
protected Uri[] doInBackground(AddCallArgs... callList) {
int count = callList.length;
Uri[] result = new Uri[count];
for (int i = 0; i < count; i++) {
AddCallArgs c = callList[i];
try {
// May block.
// 这个方法即为初始化的证据。插入的证据在这: addCall(),即为插入DB的证据;
result[i] = Calls.addCall(c.callerInfo, c.context, c.number, c.presentation,
c.callType, c.features, c.accountHandle, c.timestamp, c.durationInSec,
c.dataUsage, true /* addForAllUsers */);
} catch (Exception e) {
// This is very rare but may happen in legitimate cases.
// E.g. If the phone is encrypted and thus write request fails, it may cause
// some kind of Exception (right now it is IllegalArgumentException, but this
// might change).
//
// We don't want to crash the whole process just because of that, so just log
// it instead.
Log.e(TAG, e, "Exception raised during adding CallLog entry.");
result[i] = null;
}
}
return result;
}
/**
* Performs a simple sanity check to make sure the call was written in the database.
* Typically there is only one result per call so it is easy to identify which one failed.
*/
@Override
protected void onPostExecute(Uri[] result) {
for (Uri uri : result) {
if (uri == null) {
Log.w(TAG, "Failed to write call to the log.");
}
}
}
}
5、CallLog —> addCall()
// 这个方法即为初始化的证据。插入的证据在这: addCall(),即为插入DB的证据;
frameworks/base/core/java/android/provider/CallLog.java.
// CallLog.java.
public static Uri addCall(CallerInfo ci, Context context, String number,
int presentation, int callType, int features, PhoneAccountHandle accountHandle,
long start, int duration, Long dataUsage, boolean addForAllUsers) {
final ContentResolver resolver = context.getContentResolver();
int numberPresentation = PRESENTATION_ALLOWED;
// Remap network specified number presentation types
// PhoneConstants.PRESENTATION_xxx to calllog number presentation types
// Calls.PRESENTATION_xxx, in order to insulate the persistent calllog
// from any future radio changes.
// If the number field is empty set the presentation type to Unknown.
if (presentation == PhoneConstants.PRESENTATION_RESTRICTED) {
numberPresentation = PRESENTATION_RESTRICTED;
} else if (presentation == PhoneConstants.PRESENTATION_PAYPHONE) {
numberPresentation = PRESENTATION_PAYPHONE;
} else if (TextUtils.isEmpty(number)
|| presentation == PhoneConstants.PRESENTATION_UNKNOWN) {
numberPresentation = PRESENTATION_UNKNOWN;
}
if (numberPresentation != PRESENTATION_ALLOWED) {
number = "";
if (ci != null) {
ci.name = "";
}
}
// accountHandle information
String accountComponentString = null;
String accountId = null;
if (accountHandle != null) {
accountComponentString = accountHandle.getComponentName().flattenToString();
accountId = accountHandle.getId();
}
ContentValues values = new ContentValues(6);
values.put(NUMBER, number);
values.put(NUMBER_PRESENTATION, Integer.valueOf(numberPresentation));
values.put(TYPE, Integer.valueOf(callType));
values.put(FEATURES, features);
values.put(DATE, Long.valueOf(start));
values.put(DURATION, Long.valueOf(duration));
if (dataUsage != null) {
values.put(DATA_USAGE, dataUsage);
}
values.put(PHONE_ACCOUNT_COMPONENT_NAME, accountComponentString);
values.put(PHONE_ACCOUNT_ID, accountId);
values.put(NEW, Integer.valueOf(1));
if (callType == MISSED_TYPE) {
values.put(IS_READ, Integer.valueOf(0));
}
if (ci != null) {
values.put(CACHED_NAME, ci.name);
values.put(CACHED_NUMBER_TYPE, ci.numberType);
values.put(CACHED_NUMBER_LABEL, ci.numberLabel);
}
if ((ci != null) && (ci.contactIdOrZero > 0)) {
// Update usage information for the number associated with the contact ID.
// We need to use both the number and the ID for obtaining a data ID since other
// contacts may have the same number.
final Cursor cursor;
// We should prefer normalized one (probably coming from
// Phone.NORMALIZED_NUMBER column) first. If it isn't available try others.
if (ci.normalizedNumber != null) {
final String normalizedPhoneNumber = ci.normalizedNumber;
cursor = resolver.query(Phone.CONTENT_URI,
new String[] { Phone._ID },
Phone.CONTACT_ID + " =? AND " + Phone.NORMALIZED_NUMBER + " =?",
new String[] { String.valueOf(ci.contactIdOrZero),
normalizedPhoneNumber},
null);
} else {
final String phoneNumber = ci.phoneNumber != null ? ci.phoneNumber : number;
cursor = resolver.query(
Uri.withAppendedPath(Callable.CONTENT_FILTER_URI,
Uri.encode(phoneNumber)),
new String[] { Phone._ID },
Phone.CONTACT_ID + " =?",
new String[] { String.valueOf(ci.contactIdOrZero) },
null);
}
// 更新通话记录操作
if (cursor != null) {
try {
if (cursor.getCount() > 0 && cursor.moveToFirst()) {
final String dataId = cursor.getString(0);
updateDataUsageStatForData(resolver, dataId);
if (duration >= MIN_DURATION_FOR_NORMALIZED_NUMBER_UPDATE_MS
&& callType == Calls.OUTGOING_TYPE
&& TextUtils.isEmpty(ci.normalizedNumber)) {
updateNormalizedNumber(context, resolver, dataId, number);
}
}
} finally {
cursor.close();
}
}
}
/// M: new feature:IP dial enhancement start @{
String ipPrefix = null;
ipPrefix = Settings.System.getString(resolver, "ipprefix" + accountId);
if (null != ipPrefix && null != number && number.startsWith(ipPrefix)
&& !number.equals(ipPrefix) && callType == Calls.OUTGOING_TYPE) {
values.put(IP_PREFIX, ipPrefix);
String tmpNumber = number.substring(ipPrefix.length(), number.length());
values.put(NUMBER, tmpNumber);
}
/// @}
Uri result = null;
if (addForAllUsers) {
// Insert the entry for all currently running users, in order to trigger any
// ContentObservers currently set on the call log.
final UserManager userManager = (UserManager) context.getSystemService(
Context.USER_SERVICE);
List<UserInfo> users = userManager.getUsers(true);
final int currentUserId = userManager.getUserHandle();
final int count = users.size();
for (int i = 0; i < count; i++) {
final UserInfo user = users.get(i);
final UserHandle userHandle = user.getUserHandle();
if (userManager.isUserRunning(userHandle)
&& !userManager.hasUserRestriction(UserManager.DISALLOW_OUTGOING_CALLS,
userHandle)
&& !user.isManagedProfile()) {
// 插入通话记录操作
Uri uri = addEntryAndRemoveExpiredEntries(context,
ContentProvider.maybeAddUserId(CONTENT_URI, user.id), values);
if (user.id == currentUserId) {
result = uri;
}
}
}
} else {
result = addEntryAndRemoveExpiredEntries(context, CONTENT_URI, values);
}
return result;
}
6、CallLog —> addEntryAndRemoveExpiredEntries()
// 插入通话记录操作
// CallLog.java
private static Uri addEntryAndRemoveExpiredEntries(Context context, Uri uri,
ContentValues values) {
final ContentResolver resolver = context.getContentResolver();
Uri result = resolver.insert(uri, values);
resolver.delete(uri, "_id IN " +
"(SELECT _id FROM calls ORDER BY " + DEFAULT_SORT_ORDER
+ " LIMIT -1 OFFSET 500)", null);
return result;
}
7、CallLog —> resolver.insert(uri, values)
// 通话记录插入操作
二、通话记录的显示总共25步
1、CallLogFragment —>onResume()
// 每次切换导航栏、切换语言、点击Home键都会重新刷新通话记录
// CallLogFragment.java
@Override
public void onResume() {
super.onResume();
// 用于查询数据库
refreshData();
}
2、CallLogFragment —>refreshData()
// 申请更新数据用于显示
// CallLogFragment.java
// 申请更新数据用于显示
private void refreshData() {
// 是否需要刷新数据
if (mRefreshDataRequired) {
/* 在接触信息缓存中的所有条目都会被记录出来,这样他们就会被人看到
再次显示一次。*/
mAdapter.invalidateCache();
startCallsQuery();
startVoicemailStatusQuery();
updateOnEntry();
mRefreshDataRequired = false;
/// M: for ALPS01772987 @{
// need to update data without re-query
} else {
mAdapter.notifyDataSetChanged();
}
/// @}
if (mNeedAccountFilter) {
updateNotice();
}
}
3、CallLogAdapter—>invalidateCache()
/* 在接触信息缓存中的所有条目都会被记录出来,这样他们就会被人看到
再次显示一次。*/
// CallLogAdapter.java
public void invalidateCache() {
mContactInfoCache.expireAll();
// Restart the request-processing thread after the next draw.
stopRequestProcessing();
unregisterPreDrawListener();
}
4、CallLogFragment —>startCallsQuery()
// 读取sim卡过滤设置、通话类型设置(来电?去电?未接?全部? ),开始查询
// CallLogFragment.java
// 读取sim卡过滤设置、通话类型设置,开始查询
public void startCallsQuery() {
// 正在加载联系人,此时联系人列表不显示为 空
mAdapter.setLoading(true);
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getActivity());
// 通话类型:来电?去电?未接?全部?
mCallTypeFilter = prefs.getInt(Constants.TYPE_FILTER_PREF, CallLogQueryHandler.CALL_TYPE_ALL);
mCallLogQueryHandler.fetchCalls(mCallTypeFilter, mDateLimit, getAccountFilterId());
}
5、CallLogAdapter —>setLoading(true)
// 正在加载联系人,此时联系人列表不显示为 空
// CallLogAdapter.java
public void setLoading(boolean loading) {
mLoading = loading;
}
6、CallLogQueryHandler —>fetchCalls()
// 获取列表中的通话记录
// CallLogQueryHandler.java
public void fetchCalls(int callType, long newerThan, String accountId) {
cancelFetch();
fetchCalls(QUERY_CALLLOG_TOKEN, callType, false /* newOnly */, newerThan, accountId);
}
注:一步一步将添加查询条件,将查询请求提交给ContentProvider。
7、NoNullCursorAsyncQueryHandler—>startQuery
// 设置查询Uri,
// CallLogQueryHandler.java
/** Fetches the list of calls in the call log. */
private void fetchCalls(int token, int callType, boolean newOnly,
long newerThan, String accountId) {
// We need to check for NULL explicitly otherwise entries with where READ is NULL
// may not match either the query or its negation.
// We consider the calls that are not yet consumed (i.e. IS_READ = 0) as "new".
StringBuilder where = new StringBuilder();
List<String> selectionArgs = Lists.newArrayList();
if (newOnly) {
where.append(Calls.NEW);
where.append(" = 1");
}
if (callType > CALL_TYPE_ALL) {
if (where.length() > 0) {
where.append(" AND ");
}
// Add a clause to fetch only items of type voicemail.
where.append(String.format("(%s = ?)", Calls.TYPE));
// Add a clause to fetch only items newer than the requested date
selectionArgs.add(Integer.toString(callType));
}
if (newerThan > 0) {
if (where.length() > 0) {
where.append(" AND ");
}
where.append(String.format("(%s > ?)", Calls.DATE));
selectionArgs.add(Long.toString(newerThan));
}
if (!PhoneAccountInfoHelper.FILTER_ALL_ACCOUNT_ID.equals(accountId)) {
if (where.length() > 0) {
where.append(" AND ");
}
// query the Call Log by account id
where.append(String.format("(%s = ?)", Calls.PHONE_ACCOUNT_ID));
selectionArgs.add(accountId);
}
/// M: for Plug-in @{
ExtensionManager.getInstance().getCallLogExtension().appendQuerySelection(callType, where, selectionArgs);
/// @}
final int limit = (mLogLimit == -1) ? NUM_LOGS_TO_DISPLAY : mLogLimit;
final String selection = where.length() > 0 ? where.toString() : null;
Uri uri = Calls.CONTENT_URI_WITH_VOICEMAIL.buildUpon()
.appendQueryParameter(Calls.LIMIT_PARAM_KEY, Integer.toString(limit))
.build();
/// M: [Union Query] MTK CallLog Query @{
if (DialerFeatureOptions.CALL_LOG_UNION_QUERY) {
// change CallLog query data source to calls join data view
uri = Uri.parse("content://call_log/callsjoindataview").buildUpon()
.appendQueryParameter(Calls.ALLOW_VOICEMAILS_PARAM_KEY, "true")
.appendQueryParameter(Calls.LIMIT_PARAM_KEY, Integer.toString(limit))
.build();
}
LogUtils.d(TAG, "fetchCalls(), queryUri = " + uri.toString() + ", selection = " + selection
+ ", selectionArgs = " + selectionArgs);
/// @}
// 设置查询Uri,
startQuery(token, null, uri,
CallLogQuery._PROJECTION, selection, selectionArgs.toArray(EMPTY_STRING_ARRAY),
Calls.DEFAULT_SORT_ORDER);
}
// NoNullCursorAsyncQueryHandler.java
@Override
public void startQuery(int token, Object cookie, Uri uri, String[] projection, String selection,
String[] selectionArgs, String orderBy) {
final CookieWithProjection projectionCookie = new CookieWithProjection(cookie, projection);
super.startQuery(token, projectionCookie, uri, projection, selection, selectionArgs,
orderBy);
}
注:CallLogQueryHandlerEx、NoNullCursorAsyncQueryHandler抽象类、AsyncQueryHandler抽象类是继承关系,继承自Handler
9、AsyncQueryHandler—>super.startQuery
//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
//mWorkerThreadHandler是WorkerHandler的对象,也是一个Handler,与工作线程通信
Message msg = mWorkerThreadHandler.obtainMessage(token);
msg.arg1 = EVENT_ARG_QUERY;
WorkerArgs args = new WorkerArgs();
//this即AsyncQueryHandler,用于工作线程返回查询结果Cursor
args.handler = this;
args.uri = uri;
args.projection = projection;
args.selection = selection;
args.selectionArgs = selectionArgs;
args.orderBy = orderBy;
args.cookie = cookie;
msg.obj = args;
//查询将在工作线程中进行
mWorkerThreadHandler.sendMessage(msg);
}
10、WorkerHandler—>mWorkerThreadHandler.sendMessage(msg)
//mWorkerThreadHandler是WorkerHandler的对象,也是一个Handler,与工作线程通信查询将在工作线程中进行
注:11~14步 工作线程将查询结果返回给AsyncQueryHandler的handleMessage()处理。
11、WorkerHandler—>handleMessage()
// 工作线程将查询结果返回给AsyncQueryHandler的handleMessage()处理。
12、WorkerHandler—>resolver.query()
// 使用query查询获得cursor,
13、WorkerHandler—>reply.sendToTarget()
1)将查询的cursor 赋值 args.result = cursor;
2)//args.handler就是上文提到的this ,this即AsyncQueryHandler,用于工作线程返回查询结果Cursor
Message reply = args.handler.obtainMessage(token);
// 发送message
reply.sendToTarget();
// AsyncQueryHandler.java
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 {
/*
selection表示查找的列,
selectionArgs为where条件
*/
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;
}
//查询结果cursor
args.result = cursor;
break;
}
// passing the original token value back to the caller
// on top of the event values in arg1.
//args.handler就是上文提到的this
Message reply = args.handler.obtainMessage(token);
reply.obj = args;
reply.arg1 = msg.arg1;
if (localLOGV) {
Log.d(TAG, "WorkerHandler.handleMsg: msg.arg1=" + msg.arg1
+ ", reply.what=" + reply.what);
}
/*
你好,Message.sendToTarget() ---> Message.target.sendMessage(), 这里target是个Handler,所以你说的消息是发送到这个Message内部持有的Handler对象,加入他的MessageQueue。所以你要找到赋给该Message Handler的地方,在它的handleMessage里就可以看到处理了。至于在什么地方赋的值,可以看在什么地方调用了Message.obtain(Hanlder)或者Handler.obtainMessage()这类的函数
*/
reply.sendToTarget();
}
}
14、NoNullCursorAsyncQueryHandler—>onQueryComplete()
//通话记录为空,创建一个空的cursor返回
查询完成,返回cursor,判断cursor是否为空。
// NoNullCursorAsyncQueryHandler.java
@Override
protected final void onQueryComplete(int token, Object cookie, Cursor cursor) {
CookieWithProjection projectionCookie = (CookieWithProjection) cookie;
super.onQueryComplete(token, projectionCookie.originalCookie, cursor);
//通话记录为空,创建一个空的cursor返回
if (cursor == null) {
cursor = new EmptyCursor(projectionCookie.projection);
}
onNotNullableQueryComplete(token, projectionCookie.originalCookie, cursor);
}
15、CallLogQueryHandler—>onNotNullableQueryComplete()
1)、QUERY_CALLLOG_TOKEN 查询的标记,以从调用通话记录中获取旧的条目
2)、QUERY_SEARCH_TOKEN 用于通话记录全局搜索
// CallLogQueryHandler.java
@Override
protected synchronized void onNotNullableQueryComplete(int token, Object cookie, Cursor cursor) {
if (cursor == null) {
return;
}
try {
/// M: [CallLog Search] For call log global search. @{
/*
1)、QUERY_CALLLOG_TOKEN 查询的标记,以从调用通话记录中获取旧的条目
2)、QUERY_SEARCH_TOKEN 用于通话记录全局搜索
*/
if (token == QUERY_CALLLOG_TOKEN || token == QUERY_SEARCH_TOKEN) {
/// @}
if (updateAdapterData(cursor)) {
cursor = null;
}
} else if (token == QUERY_VOICEMAIL_STATUS_TOKEN) {
updateVoicemailStatus(cursor);
} else {
Log.w(TAG, "Unknown query completed: ignoring: " + token);
}
} finally {
if (cursor != null) {
cursor.close();
}
}
}
16、CallLogQueryHandler—>updateAdapterData()
// 更新adapter 在calllogfragment中用于显示更新后的数据
// 更新adapter 在calllogfragment中用于显示更新后的数据
private boolean updateAdapterData(Cursor cursor) {
final Listener listener = mListener.get();
if (listener != null) {
return listener.onCallsFetched(cursor);
}
return false;
}
17、CallLogFragment—>listener.onCallsFetched()
// 当通话记录列表被获取或者更新时被CallLogQueryHandler 调用,将结果cursor返回给CallLogFragment。
// CallLogFragment.java
// 被CallLogQueryHandler 调用当通话记录列表被获取或者更新时
@Override
public boolean onCallsFetched(Cursor cursor) {
// 联系人加载完毕,此时通话记录显示更新后的联系人
mAdapter.setLoading(false);
//更改CallLogListAdapter的cursor,刷新ListView
mAdapter.changeCursor(cursor);
// This will update the state of the "Clear call log" menu item.
getActivity().invalidateOptionsMenu();
if (mScrollToTop) {
final ListView listView = getListView();
if (listView.getFirstVisiblePosition() > 5) {
listView.setSelection(5);
}
// Workaround for framework issue: the smooth-scroll doesn't
// occur if setSelection() is called immediately before.
mHandler.post(new Runnable() {
@Override
public void run() {
if (getActivity() == null || getActivity().isFinishing()) {
return;
}
listView.smoothScrollToPosition(0);
}
});
mScrollToTop = false;
}
mCallLogFetched = true;
destroyEmptyLoaderIfAllDataFetched();
return true;
}
注:step18~step19,将结果cursor返回给CallLogFragment。
18、CallLogFragment—>mAdapter.setLoading(false)
// 联系人加载完毕,此时通话记录显示更新后的联系人
19、CallLogAdapter—>mAdapter.changeCursor()
//更改CallLogListAdapter的cursor,刷新ListView
20~24、主要是处理通话记录的分组显示。
20、GroupingListAdapter—>super.changeCursor
// GroupingListAdapter.java
public void changeCursor(Cursor cursor) {
if (cursor == mCursor) {
return;
}
if (mCursor != null) {
mCursor.unregisterContentObserver(mChangeObserver);
mCursor.unregisterDataSetObserver(mDataSetObserver);
mCursor.close();
}
mCursor = cursor;
resetCache();
findGroups();
if (cursor != null) {
cursor.registerContentObserver(mChangeObserver);
cursor.registerDataSetObserver(mDataSetObserver);
mRowIdColumnIndex = cursor.getColumnIndexOrThrow("_id");
notifyDataSetChanged();
} else {
// notify the observers about the lack of a data set
notifyDataSetInvalidated();
}
}
21、CallLogAdapter—>addGroups()
// CallLogAdapter.java
@Override
protected void addGroups(Cursor cursor) {
mCallLogGroupBuilder.addGroups(cursor);
}
22、GroupingListAdapter—>findGroups()
设置mGroupMetadata初始化大小16,用于记录一个Group(ListView的一个item)的开始位置和大小(包含的通话记录数目)
// GroupingListAdapter.java
private void findGroups() {
mGroupCount = 0;
mGroupMetadata = new long[GROUP_METADATA_ARRAY_INITIAL_SIZE];
if (mCursor == null) {
return;
}
addGroups(mCursor);
}
23、CallLogGroupBuilder—>mCallLogGroupBuilder.addGroups(cursor)()
具体的分组规则和分组过程
// CallLogGroupBuilder.java
public void addGroups(Cursor cursor) {
final int count = cursor.getCount();
if (count == 0) {
return;
}
// Clear any previous day grouping information.
mGroupCreator.clearDayGroups();
// Get current system time, used for calculating which day group calls belong to.
long currentTime = System.currentTimeMillis();
int currentGroupSize = 1;
cursor.moveToFirst();
// The number of the first entry in the group.
String firstNumber = cursor.getString(CallLogQuery.NUMBER);
// This is the type of the first call in the group.
int firstCallType = cursor.getInt(CallLogQuery.CALL_TYPE);
// The account information of the first entry in the group.
String firstAccountComponentName = cursor.getString(CallLogQuery.ACCOUNT_COMPONENT_NAME);
String firstAccountId = cursor.getString(CallLogQuery.ACCOUNT_ID);
// Determine the day group for the first call in the cursor.
final long firstDate = cursor.getLong(CallLogQuery.DATE);
final long firstRowId = cursor.getLong(CallLogQuery.ID);
int currentGroupDayGroup = getDayGroup(firstDate, currentTime);
mGroupCreator.setDayGroup(firstRowId, currentGroupDayGroup);
while (cursor.moveToNext()) {
// The number of the current row in the cursor.
final String currentNumber = cursor.getString(CallLogQuery.NUMBER);
final int callType = cursor.getInt(CallLogQuery.CALL_TYPE);
final String currentAccountComponentName = cursor.getString(
CallLogQuery.ACCOUNT_COMPONENT_NAME);
final String currentAccountId = cursor.getString(CallLogQuery.ACCOUNT_ID);
final boolean sameNumber = equalNumbers(firstNumber, currentNumber);
final boolean sameAccountComponentName = Objects.equals(
firstAccountComponentName,
currentAccountComponentName);
final boolean sameAccountId = Objects.equals(
firstAccountId,
currentAccountId);
final boolean sameAccount = sameAccountComponentName && sameAccountId;
final boolean shouldGroup;
final long currentCallId = cursor.getLong(CallLogQuery.ID);
final long date = cursor.getLong(CallLogQuery.DATE);
if (!sameNumber || !sameAccount) {
// Should only group with calls from the same number.
shouldGroup = false;
} else if (firstCallType == Calls.VOICEMAIL_TYPE) {
// never group voicemail.
shouldGroup = false;
} else {
// Incoming, outgoing, and missed calls group together.
shouldGroup = callType != Calls.VOICEMAIL_TYPE;
}
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 {
// The call group has changed, so determine the day group for the new call group.
// This ensures all calls grouped together in the call log are assigned the same
// day group.
currentGroupDayGroup = getDayGroup(date, currentTime);
// Create a group for the previous set of calls, excluding the current one, but do
// not create a group for a single call.
if (currentGroupSize > 1) {
addGroup(cursor.getPosition() - currentGroupSize, currentGroupSize);
}
// 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;
firstAccountComponentName = currentAccountComponentName;
firstAccountId = currentAccountId;
}
// Save the day group associated with the current call.
mGroupCreator.setDayGroup(currentCallId, currentGroupDayGroup);
}
// If the last set of calls at the end of the call log was itself a group, create it now.
if (currentGroupSize > 1) {
addGroup(count - currentGroupSize, currentGroupSize);
}
}
24、CallLogGroupBuilder—>mGroupCreator.setDayGroup()
用于跟踪每一个呼叫所属的天组,同一天组作为第一次呼叫组,分为今天,昨天,上周,和其他几种分组,在setDayGroup()设置getDayGroup()中获取
25、CallLogAdapter —> bindView()
通话记录ListView和Adapter的数据绑定是在GroupingListAdapter中的getView()方法中,具体实现放在CallLogAdapter的bindView()实现,最后,将通话记录加载到列表中