这两天在看通话记录相关问题,顺便跟踪了Dialer中的通话记录是怎么显示出来的,在这和大家分享下。当有来电或去电时,calllog会被插入到calllog.db的数据中去,具体可以参考《Android 7.1.1 通话记录数据库详解》。在Dialer中通话记录对应的为CallLogFragment这个界面。下面我们先来具体分析这个Fragment的布局,再来看看每个控件的数据是如何获取的。
通话记录对应的界面为CallLogFragment,在onCreateView中加载了call_log_fragment.xml,这个布局中具体有两个控件RecyclerView和EmptyContentView(继承自LinearLayout的自定义控件),代码如下:
call_log_fragment.xml
//packages/apps/Dialer/res/layout/call_log_fragment.xml
//packages/apps/Dialer/src/com/android/dialer/calllog/CallLogFragment.java
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedState) {
View view = inflater.inflate(R.layout.call_log_fragment, container, false);
setupView(view, null);
return view;
}
protected void setupView(
View view, @Nullable VoicemailPlaybackPresenter voicemailPlaybackPresenter) {
mRecyclerView = (RecyclerView) view.findViewById(R.id.recycler_view);
mRecyclerView.setHasFixedSize(true);
mLayoutManager = new LinearLayoutManager(getActivity());
mRecyclerView.setLayoutManager(mLayoutManager);
mEmptyListView = (EmptyContentView) view.findViewById(R.id.empty_list_view);
mEmptyListView.setImage(R.drawable.empty_call_log);
mEmptyListView.setActionClickedListener(this);
int activityType = mIsCallLogActivity ? CallLogAdapter.ACTIVITY_TYPE_CALL_LOG :
CallLogAdapter.ACTIVITY_TYPE_DIALTACTS;
String currentCountryIso = GeoUtil.getCurrentCountryIso(getActivity());
mAdapter = ObjectFactory.newCallLogAdapter(
getActivity(),
this,
new ContactInfoHelper(getActivity(), currentCountryIso),
voicemailPlaybackPresenter,
activityType);
mRecyclerView.setAdapter(mAdapter);
fetchCalls();
}
其中EmptyContentView是当数据库中没有calllog时显示,具体显示mRecyclerView或mEmptyListView是根据CallLogFragment.onCallsFetched中cursor的size决定的。在setupView中主要设置了RecyclerView的adapter和调用fetchCalls查询数据库。我们先来说说mRecyclerView设置的mAdapter,通过ObjectFactory.newCallLogAdapter返回CallLogAdapter的实例,在adapter中根据viewType,创建createVoicemailPromoCardViewHolder或者createCallLogEntryViewHolder,下面具体以创建calllog的entry来讲讲。
在createCallLogEntryViewHolder中加载了call_log_list_item.xml,然后创建了CallLogListItemViewHolder的实例,布局文件咱就不贴代码了,结合着界面来看看具体显示效果
布局文件就不在继续分析了,具体看下CallLogListItemViewHolder中每个item的值都是怎么来的,刚上面讲到在CallLogFragment的setupView中调用了fetchCalls,这个方法就是查询数据库的,具体流程如下
下面主要看看CallLogQueryHandler中fetchCalls查询数据库时的代码,调用startQuery后,数据查询完成后会回调onNotNullableQueryComplete,在这个里面update cursor
//packages/apps/Dialer/src/com/android/dialer/calllog/CallLogQueryHandler.java
private void fetchCalls(int token, int callType, boolean newOnly, long newerThan) {
StringBuilder where = new StringBuilder();
List selectionArgs = Lists.newArrayList();
// Always hide blocked calls.
where.append("(").append(Calls.TYPE).append(" != ?)");
selectionArgs.add(Integer.toString(AppCompatConstants.CALLS_BLOCKED_TYPE));
// Ignore voicemails marked as deleted
if (SdkVersionOverride.getSdkVersion(Build.VERSION_CODES.M)
>= Build.VERSION_CODES.M) {
where.append(" AND (").append(Voicemails.DELETED).append(" = 0)");
}
if (newOnly) {
where.append(" AND (").append(Calls.NEW).append(" = 1)");
}
if (callType > CALL_TYPE_ALL) {
where.append(" AND (").append(Calls.TYPE).append(" = ?)");
selectionArgs.add(Integer.toString(callType));
} else {
where.append(" AND NOT ");
where.append("(" + Calls.TYPE + " = " + AppCompatConstants.CALLS_VOICEMAIL_TYPE + ")");
}
if (newerThan > 0) {
where.append(" AND (").append(Calls.DATE).append(" > ?)");
selectionArgs.add(Long.toString(newerThan));
}
final int limit = (mLogLimit == -1) ? NUM_LOGS_TO_DISPLAY : mLogLimit;
final String selection = where.length() > 0 ? where.toString() : null;
Uri uri = TelecomUtil.getCallLogUri(mContext).buildUpon()
.appendQueryParameter(Calls.LIMIT_PARAM_KEY, Integer.toString(limit))
.build();
startQuery(token, null, uri, CallLogQuery._PROJECTION, selection, selectionArgs.toArray(
new String[selectionArgs.size()]), Calls.DEFAULT_SORT_ORDER);
}
查询完成之后调用onNotNullableQueryComplete,因为CallLogQueryHandler extends NoNullCursorAsyncQueryHandler ,而NoNullCursorAsyncQueryHandler extends AsyncQueryHandler,当调用AsyncQueryHandler的startQuery时会回调AsyncQueryHandler的onQueryComplete(Android原生的,具体实现方式可以自己查看,这里就不啰嗦了)
//packages/apps/Dialer/src/com/android/dialer/calllog/CallLogQueryHandler.java
protected synchronized void onNotNullableQueryComplete(int token, Object cookie,
Cursor cursor) {
if (cursor == null) {
return;
}
try {
if (token == QUERY_CALLLOG_TOKEN || token == QUERY_VOICEMAIL_ARCHIVE) {
if (updateAdapterData(cursor)) {
cursor = null;
}
} else if (token == QUERY_VOICEMAIL_STATUS_TOKEN) {
updateVoicemailStatus(cursor);
} else if (token == QUERY_VOICEMAIL_UNREAD_COUNT_TOKEN) {
updateVoicemailUnreadCount(cursor);
} else if (token == QUERY_MISSED_CALLS_UNREAD_COUNT_TOKEN) {
updateMissedCallsUnreadCount(cursor);
} else {
Log.w(TAG, "Unknown query completed: ignoring: " + token);
}
} finally {
if (cursor != null) {
cursor.close();
}
}
}
在updateAdapterData会调用所有实现了CallLogQueryHandler.Listener接口的onCallsFetched方法
//packages/apps/Dialer/src/com/android/dialer/calllog/CallLogQueryHandler.java
private boolean updateAdapterData(Cursor cursor) {
final Listener listener = mListener.get();
if (listener != null) {
return listener.onCallsFetched(cursor);
}
return false;
}
在CallLogFragment中的onCallsFetched会获取到查询后的cursor,在这调用mAdapter.changeCursor(cursor)后,并根据cursor.getCount决定显示那个view
//packages/apps/Dialer/src/com/android/dialer/calllog/CallLogFragment.java
public boolean onCallsFetched(Cursor cursor) {
...
mAdapter.changeCursor(cursor);
// This will update the state of the "Clear call log" menu item.
getActivity().invalidateOptionsMenu();
boolean showListView = cursor != null && cursor.getCount() > 0;
mRecyclerView.setVisibility(showListView ? View.VISIBLE : View.GONE);
mEmptyListView.setVisibility(!showListView ? View.VISIBLE : View.GONE);
...
}
接下来就要重点说下RecyclerView的adapter了,因为CallLogAdapter继承GroupingListAdapter,当调用mAdapter.changeCursor时,实际调用父类GroupingListAdapter里changeCursor//packages/apps/Dialer/src/com/android/dialer/calllog/GroupingListAdapter.java
public void changeCursor(Cursor cursor, boolean voicemail) {
if (cursor == mCursor) {
return;
}
if (mCursor != null) {
mCursor.unregisterContentObserver(mChangeObserver);
mCursor.unregisterDataSetObserver(mDataSetObserver);
mCursor.close();
}
// Reset whenever the cursor is changed.
reset();
mCursor = cursor;
if (cursor != null) {
if (voicemail) {
addVoicemailGroups(mCursor);
} else {
addGroups(mCursor);
}
// Calculate the item count by subtracting group child counts from the cursor count.
mItemCount = mGroupMetadata.size();
cursor.registerContentObserver(mChangeObserver);
cursor.registerDataSetObserver(mDataSetObserver);
notifyDataSetChanged();
}
}
在这方法里registerContentObserver和registerDataSetObserver,并调用notifyDataSetChanged,当cursor的content发生改变之后,又会调用onContentChanged最后还是会调到CallLogFragment.fetchCalls查询数据库,在这就不围绕这个再说了
protected ContentObserver mChangeObserver = new ContentObserver(new Handler()) {
@Override
public boolean deliverSelfNotifications() {
return true;
}
@Override
public void onChange(boolean selfChange) {
onContentChanged();
}
};
protected DataSetObserver mDataSetObserver = new DataSetObserver() {
@Override
public void onChanged() {
notifyDataSetChanged();
}
};
当调用notifyDataSetChanged后,CallLogAdapter中就会根据查询后的cursor重新rebind and relayout所有可见的view,具体对每个view设置Text,是在CallLogListItemHelper.setPhoneCallDetails里,根据传进去的值,对每个view赋值,最终实现是在PhoneCallDetailsHelper.setPhoneCallDetails这个方法里对各个view设置值,代码太多就不在贴代码了,下面简单总结下整个流程
1、在CallLogFragment的onCreateView中设置RecyclerView的adapter,并调用fetchCalls查询数据。
2、当查询完成后会回调到CallLogFragment的onCallsFetched,并调用changeCursor,registerContentObserver和registerDataSetObserver,数据发生改变的时候,调用RecyclerView.Adapter的notifyDataSetChanged就会根据新的cursor rebind and relayout所有可见的view。