目 录
2. ConversationList
2.1.2.1 ConversationListAdapter
2.2.1 列表项click
2.2.2 Long Press click to Delete
2.3.1 会话ID创建
3. ConversationListItem
3.1.2.1 ConversationListItem实例创建
1. 概要
本文主要分析短信列表界面的主要控件的界面显示和处理逻辑。
2. ConversationList
2.1 ConversationList
2.1.1 Layout分析
ConversationList对应的layout是conversation_list_screen,它主要分3部分,第一部分是做全选的控制,第二部分是list列表,第三部分是列表为空的布局。具体的表现形式如下,布局文件就不再展开了。其中list列表将在另外的章节分析。
2.1.2 代码分析
2.1.2.1 ConversationListAdapter
ConversationListAdapter是ConversationListItem的数据适配器,在ConversationList.onCreate里创建,它通过bindView将会话信息绑定到item的layout上。
其创建过程如下,
private void initListAdapter() {
mListAdapter = new ConversationListAdapter(this, null);
/** M: now this code is useless and will lead to a JE, comment it.
* listener is set in onStart
*/
//mListAdapter.setOnContentChangedListener(mContentChangedListener);
setListAdapter(mListAdapter);
getListView().setRecyclerListener(mListAdapter);
}
因为ConversationList继承自ListActivity,setListAdapter将Adapter实例设置给ListActivity,这里会调用ensureList,它会加载一个布局,确保有view能用来对应Adapter。
public void setListAdapter(ListAdapter adapter) {
synchronized (this) {
ensureList();
mAdapter = adapter;
mList.setAdapter(adapter);
}
}
private void ensureList() {
if (mList != null) {
return;
}
setContentView(com.android.internal.R.layout.list_content_simple);
}
而实际上,mList已经在 onContentChanged被初始化为Layout里对应的ListView,
mList = (ListView)findViewById(com.android.internal.R.id.list);
这个ListView就是Message列表的容器了。
2.2 功能分析
2.2.1 列表项click
单击列表项的调用栈如下:
ConversationList.onListItemClick(ListView, View, int, long) line: 1086
ListActivity$2.onItemClick(AdapterView, View, int, long) line: 319
ListView(AdapterView).performItemClick(View, int, long) line: 298
ListView(AbsListView).performItemClick(View, int, long) line: 1128
AbsListView$PerformClick.run() line: 2815
AbsListView$1.run() line: 3574
Handler.handleCallback(Message) line: 800
ViewRootImpl$ViewRootHandler(Handler).dispatchMessage(Message) line: 100
2.2.2 Long Press click to Delete
长按item删除会话流程如下:
长按出现选择菜单,
mConvListOnCreateContextMenuListener
onCreateContextMenu
点击删除
ConversationList.confirmDeleteThreads(Collection, AsyncQueryHandler) line:
ConversationList.confirmDeleteThread(long, AsyncQueryHandler) line: 1320
ConversationList.onContextItemSelected(MenuItem) line: 1247
ConversationList(Activity).onMenuItemSelected(int, MenuItem) line: 2584
PhoneWindow$DialogMenuCallback.onMenuItemSelected(MenuBuilder, MenuItem) line:
开始删除之前,要做一个后台查询,看是否有lock住的消息,Conversation.startQueryHaveLockedMessages,
之后是后台查询handler.startQuery:
AsyncQueryHandler
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);
msg.arg1 = EVENT_ARG_QUERY;
WorkerArgs args = new WorkerArgs();
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);
}
这个查询过程将消息发给mWorkerThreadHandler,这是异步查询AsyncQueryHandler自己的handler,它封装完消息EVENT_ARG_QUERY后,再发送出去,
Message reply = args.handler.obtainMessage(token);
reply.obj = args;
reply.arg1 = msg.arg1;
reply.sendToTarget();
这个消息的接收对象的设置见args.handler = this; ,在本例中,这个handler是ThreadListQueryHandler。
数据库查询完成后执行onQueryComplete(HAVE_LOCKED_MESSAGES_TOKEN),
ConversationList$ThreadListQueryHandler.onQueryComplete(int, Object, Cursor) line: 1610
ConversationList$ThreadListQueryHandler(AsyncQueryHandler).handleMessage(Message) line: 344
ConversationList$ThreadListQueryHandler(Handler).dispatchMessage(Message) line: 107
Looper.loop() line: 194
之后会给出一个确认删除对话框confirmDeleteThreadDialog
在弹出的确认对话框中,选择删除,执行listener函数。
ConversationList$DeleteThreadListener.onClick(DialogInterface, int) line: 1484
AlertController$ButtonHandler.handleMessage(Message) line: 176
之后开始删除Conversation.startDelete
最后onDeleteComplete
响应函数会触发一系列其他的数据库操作,如查询list,查询unread等,
AsyncQueryHandler$WorkerHandler.handleMessage(Message) line: 120
AsyncQueryHandler$WorkerHandler(Handler).dispatchMessage(Message) line: 107
【Tips】
它的threadIds是在点击删除功能时,onContextItemSelected设置的,和长按的时候在onCreateContextMenu里选择的有时间差,不一定是同一个,在会话列表更新的时候,会有影响,所以会误删。
所以在onCreateContextMenu里要记录thread id。
2.3 数据操作
2.3.1 会话ID创建
Message列表是以会话为item进行显示的,会话是以为ID记录的,我们分析一下各种会话ID是如何创建的,
应用在CBMessageReceiverService.java收到小区广播后,使用handleCBMessageReceived方法进行处理,其中insertMessage是用来存储CB的。
insertMessage会调用CbProvider的insert方法
public Uri insert(Uri url, ContentValues initialValues) {
Uri result = null;
// TODO Check Permission
// checkPermission();
SQLiteDatabase db = mOpenHelper.getWritableDatabase();
SQLiteDatabase dbmmssms = mMmsSmsOpenHelper.getWritableDatabase();
int match = URI_MATCHER.match(url);
ContentValues values;
long rowID;
String table = null;
Log.d(TAG, " insert match = "+match);
switch (match) {
case URL_MESSAGES:
//table = CbDatabaseHelper.CBMESSAGE_TABLE;
table = MmsSmsDatabaseHelper.TABLE_CELLBROADCAST;
values = internalInsertMessages(initialValues);
rowID = dbmmssms.insert(table, null, values);
Log.d(TAG, "insert to cellbroadcast " + values);
if (rowID > 0) {
result = Uri.parse("content://messages/" + rowID);
notifyChange();
return result;
}
break;
…
}
在这里,我们重点关注一下CB Message的ThreadId是怎样获取的,在internalInsertMessages里面,显然threadId不是我们主动填充进去的,所以是通过getOrCreateThreadId获得的。
private ContentValues internalInsertMessages(ContentValues initialValues) {
ContentValues values;
if (initialValues != null) {
values = new ContentValues(initialValues);
} else {
values = new ContentValues();
}
Long threadId = values.getAsLong(EncapsulatedTelephony.SmsCb.THREAD_ID);
String address = values.getAsString(EncapsulatedTelephony.SmsCb.CHANNEL_ID);
if (((threadId == null) || (threadId == 0)) && (address != null)) {
values.put(EncapsulatedTelephony.SmsCb.THREAD_ID,
getOrCreateThreadId(address));
}
…
}
getOrCreateThreadId代码如下,它封装一个URI地址,包含了threadID和参数recipient、cellbroadcast,并使用SqliteWrapper.query进行查询,
private String getOrCreateThreadId(String address) {
//Uri.Builder uriBuilder = THREAD_ID_URI.buildUpon();
Uri.Builder uriBuilder = (Uri.parse("content://mms-sms/threadID")).buildUpon();
uriBuilder.appendQueryParameter("recipient", address);
uriBuilder.appendQueryParameter("cellbroadcast", address);
Uri uri = uriBuilder.build();
// TODO need replace with helper interface.
Cursor cursor = null;
long token = Binder.clearCallingIdentity();
try {
cursor = SqliteWrapper.query(this.getContext(), this
.getContext().getContentResolver(), uri, ID_PROJECTION, null,
null, null);
…
if (cursor.moveToFirst()) {
return String.valueOf(cursor.getLong(0));
…
}
SqliteWrapper会调用到ContentResolver,ContentResolver则最终调用到MmsSmsProvider,
在MmsSmsProvider.query里面,也是通过URI match找到操作码URI_THREAD_ID,根据参数cellbroadcast,使用getCBThreadId给CB查找或创建其ThreadId。
Threads表内容如下所示,里面的关键字段有_id ,recipient_ids,要想找到合适的ThreadId,只有通过recipient_ids来查询,
所以在getCBThreadId方法里面,通过getAddressIds从canonical_addresses表里面找到和号码匹配的recipient_ids列表,再到Threads表里面找到recipient_ids相同和type是小区广播的_id,即是我们需要的ThreadId,使用的查询语句如下。
String queryString = "SELECT _id FROM threads " + "WHERE type=" + EncapsulatedTelephony.Threads.CELL_BROADCAST_THREAD + " AND recipient_ids=?";
如果没有满足条件的ThreadId,调用insertThread()创建一个新的thread。
在Message列表界面通过按钮新建短信时,执行createNewMessage(),通过createIntent传递一个threadId=0的ID过去,
private void createNewMessage() {
startActivity(ComposeMessageActivity.createIntent(this, 0));
}
public static Intent createIntent(Context context, long threadId) {
Intent intent = new Intent(context, ComposeMessageActivity.class);
if (threadId > 0) {
intent.setData(Conversation.getUri(threadId));
}
return intent;
}
因为threadId为0,在会话创建中会获取或创建一个threadId,具体过程和下节相同。
在联系人的详情界面,点击发送短信,因为有号码存在,所以在ComposeMessageActivity.Oncreate方法里面会去查询或创建一个threadId。
onCreate的initActivityState里,有这么一段代码是获取会话实例的,mConversation.get通过号码获取到会话实例,
Uri intentData = intent.getData();
/// M: Code analyze 034, If intent is SEND,just create a new empty thread,
/// otherwise Conversation.get() will throw exception.
String action = intent.getAction();
if (intentData != null && (TextUtils.isEmpty(action) ||
!action.equals(Intent.ACTION_SEND))) {
/// M: group-contact send message
// try to get a conversation based on the data URI passed to our intent.
if (intentData.getPathSegments().size() < 2) {
mConversation = mConversation.get(getApplicationContext(),ContactList.getByNumbers(
getStringForMultipleRecipients(Conversation.getRecipients(intentData)),
false /* don't block */, true /* replace number */),false);
} else {
mConversation = Conversation.get(getApplicationContext(), intentData, false);
}
/// @}
mWorkingMessage.setText(getBody(intentData));
Conversation get代码如下,如果号码为空则新建会话,如果缓冲里存储了对应号码的会话,则使用缓冲里的数据,如果条件都不满足,则需要去数据库查询到ID,并新建会话,之后将新会话存放到缓冲。
public static Conversation get(Context context, ContactList recipients, boolean allowQuery) {
if (recipients.size() < 1) {
return createNew(context);
}
Conversation conv = Cache.get(recipients);
if (conv != null) {
return conv;
}
long threadId = getOrCreateThreadId(context, recipients);
conv = new Conversation(context, threadId, allowQuery);
。。。
Cache.put(conv);
}
getOrCreateThreadId用来获取ThreadId,由Threads.getOrCreateThreadId实现,这个方法在Telephony.java里面。
getOrCreateThreadId类似前面提到的小区广播的实现过程,先封装一个"content://mms-sms/threadID"的URI,但只使用一个参数recipient,
在MmsSmsProvider.query里面,也是通过URI match找到操作码URI_THREAD_ID,,使用getThreadId查找或创建其ThreadId,getAddressIds获取到匹配的recipientIds,再取Threads表里查询,如果查询到,返回游标,如果没有记录,则用insertThread向Threads里插入记录,再查询到ThreadId。
private synchronized Cursor getThreadId(List<String> recipients) {
Set<Long> addressIds = getAddressIds(recipients);
。。。
recipientIds = Long.toString(addressId);
。。。
String[] selectionArgs = new String[] { recipientIds };
SQLiteDatabase db = mOpenHelper.getReadableDatabase();
db.beginTransaction();
Cursor cursor = null;
try {
// Find the thread with the given recipients
cursor = db.rawQuery(THREAD_QUERY, selectionArgs);
if (cursor.getCount() == 0) {
// No thread with those recipients exists, so create the thread.
cursor.close();
Log.d(LOG_TAG, "getThreadId: create new thread_id for recipients " + recipients);
insertThread(recipientIds, recipients);
// The thread was just created, now find it and return it.
cursor = db.rawQuery(THREAD_QUERY, selectionArgs);
。。。
}
3. ConversationListItem
3.1 ConversationListItem
3.1.1 Layout分析
如前所述,短信记录的每条信息显示是使用conversation_list_item.xml布局的,在本例中,其直观显示是如下形式的一条短信记录,
conversation_list_item.xml文件如下,它是一个ConversationListItem类型的布局,
(需要注意不同屏幕使用这个文件的位置,本例中在packages\apps\Mms\res\layout-mdpi下面,不是packages\apps\Mms\res\layout)
conversation_list_item.xml
<?xml version="1.0" encoding="utf-8"?>
<com.android.mms.ui.ConversationListItem xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="?android:attr/listPreferredItemHeight"
android:background="?android:attr/activatedBackgroundIndicator" >
<CheckBox
android:id="@+id/select_box"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_centerVertical="true"
android:clickable="false"
android:focusable="false"
android:focusableInTouchMode="false" />
<com.android.mms.ui.MmsQuickContactBadge
android:id="@+id/avatar"
android:visibility="gone"
android:layout_centerVertical="true"
android:layout_toEndOf="@id/select_box"
style="?android:attr/quickContactBadgeStyleWindowLarge" />
<ImageView
android:id="@+id/presence"
android:visibility="invisible"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_alignParentTop="true"
android:src="@drawable/ic_unread_label"/>
<LinearLayout
android:id="@+id/iconlist"
android:layout_alignParentTop="true"
android:layout_alignParentEnd="true"
android:layout_marginTop="7dip"
android:layout_marginEnd="10dip"
android:orientation="horizontal"
android:layout_height="wrap_content"
android:layout_width="wrap_content" >
<ImageView android:id="@+id/draft"
android:visibility="gone"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:src="@drawable/ic_draft" />
<ImageView android:id="@+id/error"
android:paddingStart="6dp"
android:visibility="gone"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:src="@drawable/ic_list_alert_sms_failed" />
<ImageView android:id="@+id/attachment"
android:paddingStart="6dp"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:visibility="gone"
android:src="@drawable/ic_attachment_universal_small" />
<ImageView android:id="@+id/mute"
android:paddingStart="6dp"
android:visibility="gone"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:src="@drawable/ipmsg_silent" />
</LinearLayout>
<RelativeLayout
android:layout_alignParentTop="true"
android:layout_marginTop="4dip"
android:layout_marginStart="12dip"
android:layout_toEndOf="@id/avatar"
android:layout_toStartOf="@id/iconlist"
android:gravity="center_vertical"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView android:id="@+id/unread"
android:background="@drawable/ipmsg_message_box"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:textSize="12sp"
android:textColor="@color/text_color_unread"
android:singleLine="true"
android:layout_alignParentEnd="true" />
<RelativeLayout android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:layout_toStartOf="@id/unread">
<TextView android:id="@+id/draft_and_smgcount"
android:textAppearance="?android:attr/textAppearanceMedium"
android:layout_alignParentEnd="true"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="6dip"
android:singleLine="true" />
<com.android.mms.util.AlwaysMarqueeTextView android:id="@+id/from"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceMedium"
android:singleLine="true"
android:layout_alignParentStart="true"
android:layout_toStartOf="@id/draft_and_smgcount"
android:layout_marginEnd="6dip"
android:ellipsize="marquee" />
</RelativeLayout>
</RelativeLayout>
<TextView android:id="@+id/date"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:textSize= "12sp"
android:textColor="@color/dlg_text_counter_color"
android:layout_alignParentBottom="true"
android:layout_alignParentEnd="true"
android:layout_marginEnd="10dip"
android:layout_marginBottom="4dip"
android:gravity="bottom" />
<TextView android:id="@+id/subject"
android:layout_height="wrap_content"
android:layout_width="fill_parent"
android:textSize= "12sp"
android:layout_alignParentBottom="true"
android:layout_toEndOf="@id/avatar"
android:layout_toStartOf="@id/date"
android:layout_marginStart="12dip"
android:layout_marginBottom="4dip"
android:gravity="bottom"
android:textColor="@color/dlg_text_counter_color"
android:lines="2"
android:ellipsize="end" />
</com.android.mms.ui.ConversationListItem>
其对应的布局图形如下,图中左边是layout组成,右上是layout布局表现,右下是对layout里面的子view编号,便于理解。
分析xml文件,并对照图例可以看出,
ConversationListItem是整个item布局类,表现形式见下图左边,它指定了高度
android:layout_height="?android:attr/listPreferredItemHeight"
这个布局由6个同级view组成,见下图中间及右上,其中
checkbox供选择使用,见右下数字0,alignParentStart表示它在开头位置,
android:layout_alignParentStart="true"
MmsQuickContactBadge做头像显示,见右下数字1,
ImageView做未读标识等用途,见右下数字2,通过下面的属性将它放置在最右上的位置
android:layout_alignParentEnd="true"
android:layout_alignParentTop="true"
LinearLayout是个组合视图,见右下数字3和iconlist小图,它有四个子ImageView表示短信的信息状态,具体名字见iconlist小图。注意四个子ImageView表示同时最多只能显示四个图标,不是表示短信只有这四种状态。通过下面属性,将它放置在MmsQuickContactBadge后面。
android:orientation="horizontal"
<iconlist>
RelativeLayout是个组合视图,见右下数字4和Text Title小图,包含3个TextView,用来显示Title和消息数目、未读消息数目。在RelativeLayout,通过下面的属性设置,将RelativeLayout(index4的view)和上面的LinearLayout(index3的view)并行放置在同一行。
android:layout_toEndOf="@id/avatar"
android:layout_toStartOf="@id/iconlist"
TextView显示日期,见右下数字5,用下面2个属性表示它的位置在右下,
android:layout_alignParentBottom="true"
android:layout_alignParentEnd="true"
TextView 显示短线截取的部分内容,见右下数字6,用下面3个属性表明了它的位置。
android:layout_alignParentBottom="true"
android:layout_toEndOf="@id/avatar"
android:layout_toStartOf="@id/date"
<Text Content>
3.1.2 代码分析
3.1.2.1 ConversationListItem实例创建
ConversationListItem实例创建栈如下,因为有缓存使用,不一定每次进入列表都会创建这个类实例,所以不一定能调试到,要在此设置断点调试,有一个小技巧,就是将mms进程终止掉,让其自动重启(可通过发送短信或cb的方法加快重启过程),这样就可以进行调试。
ConversationListItem.<init>(Context, AttributeSet) line: 130
Constructor.constructNative(Object[], Class, Class[], int, boolean) line: not available [native method]
Constructor.newInstance(Object...) line: 423
PhoneLayoutInflater(LayoutInflater).createView(String, String, AttributeSet) line: 594
PhoneLayoutInflater(LayoutInflater).createViewFromTag(View, String, AttributeSet) line: 696
PhoneLayoutInflater(LayoutInflater).inflate(XmlPullParser, ViewGroup, boolean) line: 469
PhoneLayoutInflater(LayoutInflater).inflate(int, ViewGroup, boolean) line: 397
ConversationListAdapter.newView(Context, Cursor, ViewGroup) line: 103
ConversationListAdapter(CursorAdapter).getView(int, View, ViewGroup) line: 250
ListView(AbsListView).obtainView(int, boolean[]) line: 2338
ListView.makeAndAddView(int, int, boolean, int, boolean) line: 1812
ListView.fillDown(int, int) line: 698
ListView.fillFromTop(int) line: 759
ListView.layoutChildren() line: 1631
ListView(AbsListView).onLayout(boolean, int, int, int, int) line: 2149
ConversationListItem实例创建的主要过程分析如下,在getView来获取view的时候,因为缓冲没有数据,会使用newView新建一个view,
public View getView(int position, View convertView, ViewGroup parent) {
View v;
if (convertView == null) {
v = newView(mContext, mCursor, parent);
} else {
v = convertView;
}
bindView(v, mContext, mCursor);
return v;
}
newView实际的过程是inflate一个布局文件,这个布局文件就是我们分析的layout,它是ConversationListItem形式的,我们知道,在inflate创建布局文件时,会根据节点创建view实例,所以会使用类的构造类Constructor来创建ConversationListItem实例。
public View newView(Context context, Cursor cursor, ViewGroup parent) {
if (LOCAL_LOGV) Log.v(TAG, "inflating new view");
return mFactory.inflate(R.layout.conversation_list_item, parent, false);
}
Checkbox的主要作用是用来勾选短信列表的某一个item,并进行删除操作的。
对于checkbox,在ConversationListItem里面定义了一个引用mSelectBox,
private CheckBox mSelectBox;
并在onFinishInflate里面初始化它,关联到Layout里面的select_box,
mSelectBox = (CheckBox) findViewById(R.id.select_box);
当列表项被点击时,执行ConversationList.onListItemClick(ListView, View, int, long)这个方法,在里面添加下面代码,当处于删除模式时,如下,将Conversation设置为check状态,然后更新界面,
if (mListAdapter != null && mDeleteMode) {
if (conv.isChecked()) {
conv.setIsChecked(false);
} else {
conv.setIsChecked(true);
}
mListAdapter.notifyDataSetChanged();
updateSelectView();
return;
}
更新界面时,执行ConversationListItem.bind(Context, Conversation) ,每加载一个item项都会执行一次bind,在这里,会根据mSelectMode 决定checkbox是否显示,根据Conversation的状态决定是否选中。
所以实际上,不是我们主观上看到的那样由checkbox决定了当前项目是否选中,而是当前项选中后决定checkbox的显示状态。
if (mSelectMode) {
mSelectBox.setVisibility(View.VISIBLE);
mAvatarView.setClickable(false);
} else {
mSelectBox.setVisibility(View.GONE);
mAvatarView.setClickable(true);
}
if (mConversation.isChecked()) {
mSelectBox.setChecked(true);
} else {
mSelectBox.setChecked(false);
}
上面就是checkbox在代码里的处理过程。
R.id.avatar用来显示发件人头像,定义如下,MmsQuickContactBadge继承自QuickContactBadge,再往上是ImageView,一个通用的视图组件。
private MmsQuickContactBadge mAvatarView;
其实例获取在ConversationListItem的onFinishInflate里面,
mAvatarView = (MmsQuickContactBadge) findViewById(R.id.avatar);
如果希望AvatarView能够被点击,相应事件,则可以看到在bind有如下代码,表示在删除模式下不能被点击,在列表模式下可以被点击。
if (mSelectMode) {
mAvatarView.setClickable(false);
} else {
mAvatarView.setClickable(true);
}
AvatarView被点击之后,执行的调用栈如下,可见是由View直接调用了MmsQuickContactBadge类的onClick,并不是ConversationList或ConversationListItem里做了onClick的相关处理。
MmsQuickContactBadge(QuickContactBadge).onClick(View) line: 217
MmsQuickContactBadge.onClick(View) line: 75
MmsQuickContactBadge(View).performClick() line: 4212
View$PerformClick.run() line: 17476
Handler.handleCallback(Message) line: 800
ViewRootImpl$ViewRootHandler(Handler).dispatchMessage(Message) line: 100
QuickContactBadge类的onClick代码如下,
@Override
public void onClick(View v) {
if (mContactUri != null) {
QuickContact.showQuickContact(getContext(), QuickContactBadge.this, mContactUri,
QuickContact.MODE_LARGE, mExcludeMimes);
} else if (mContactEmail != null) {
mQueryHandler.startQuery(TOKEN_EMAIL_LOOKUP_AND_TRIGGER, mContactEmail,
Uri.withAppendedPath(Email.CONTENT_LOOKUP_URI, Uri.encode(mContactEmail)),
EMAIL_LOOKUP_PROJECTION, null, null, null);
} else if (mContactPhone != null) {
mQueryHandler.startQuery(TOKEN_PHONE_LOOKUP_AND_TRIGGER, mContactPhone,
Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI, mContactPhone),
PHONE_LOOKUP_PROJECTION, null, null, null);
} else {
// If a contact hasn't been assigned, don't react to click.
return;
}
}
我们当前的代码走mContactPhone路径,mContactPhone是在assignContactFromPhone里面赋值的,assignContactFromPhone则是通过ConversationListItem的updateAvatarView调用的(传入的number就赋值给mContactPhone),
mAvatarView.assignContactFromPhone(number, true);
updateAvatarView可以被updateFromView和bind调用,这两个方法我们在前面有过描述。所以从整个过程来看,我们可以看出avatar被点击和其需要的数据来源。
3.2 绘制分析
3.2.1 Message列表创建
如ConversationList的layout描述,Messag列表主要是由list呈现,而list的item项是由ConversationListItem布局的,我们这里讨论每个item项的创建以及整个list的创建。
在使用Cache时,ConversationListItem的创建过程如下,
ConversationListItem.bind(Context, Conversation) line: 480
ConversationListAdapter.bindView(View, Context, Cursor) line: 77
ConversationListAdapter(CursorAdapter).getView(int, View, ViewGroup) line: 250
ListView(AbsListView).obtainView(int, boolean[]) line: 2186
ListView.makeAndAddView(int, int, boolean, int, boolean) line: 1877
ListView.fillSpecific(int, int) line: 1356
ListView.layoutChildren() line: 1671
ListView(AbsListView).onLayout(boolean, int, int, int, int) line: 2037
ListView(View).layout(int, int, int, int) line: 14118
ListView(ViewGroup).layout(int, int, int, int) line: 4467
RelativeLayout.onLayout(boolean, int, int, int, int) line: 1021
从上往下分析该栈:
1)ConversationListItem.bind
ConversationListItem.bind的代码比较多,仔细分析,实际上逻辑比较简单,主要是设置每个view的内容和状态,比如说设置日期的代码如下,
mDateView.setVisibility(VISIBLE);
if(ConversationList.sConversationListOption ==
ConversationList.OPTION_CONVERSATION_LIST_IMPORTANT &&
!conversation.hasUnreadMessages()) {
mDateView.setText(MessageUtils.formatTimeStampStringExtend(context,
conversation.getImpDate()));
}
其数据来源于conversation,我们要进一步分析conversation是怎么传递进来的,以及为什么要调用bind。
2)ConversationListAdapter
ConversationListAdapter 是ConversationList的layout里的list对应的Adapter,它继承自MessageCursorAdapter-CursorAdapter-baseAdapter, 其方法bindView在本例中执行的主要代码如下,其中headerview是一个ConversationListItem引用,是调用方法传递过来的,conversation是根据游标新建的,最后调用bind,所以该方法的作用就是获取到conversation,并用准备将其中的数据赋值到view。
public void bindView(View view, Context context, Cursor cursor) {
ConversationListItem headerView = (ConversationListItem) view;
Conversation conv;
if (!mIsScrolling) {
Conversation.setNeedCacheConv(false);
conv = Conversation.from(context, cursor);
Conversation.setNeedCacheConv(true);
if (mSubjectSingleLine) {
headerView.setSubjectSingleLineMode(true);
}
if (conv != null) {
conv.setIsChecked(sSelectedTheadsId.contains(conv.getThreadId()));
}
headerView.bind(context, conv);
}
}
(CursorAdapter).getView则负责获取到view实例和游标实例。
view的获取有两种方式,一种是调用者传递过来以前缓存的,一种是新创建一个,在ConversationListItem实例创建中有讲到如何创建。
游标实例则在Adapter创建时创建。
3)ListView
在ListView部分,我们从layoutChildren向上分析,向下部分设计到太多ListView的实现机制问题,暂不讨论。
layoutChildren实现ListView子窗口布局,实际上就是list在layout所占部分。
本例从launcher进入Message列表,执行代码路径如下,mSyncPosition是当前item索引值,mSpecificTop是当前layout相对Top的位置。
layoutChildren:
case LAYOUT_SYNC:
sel = fillSpecific(mSyncPosition, mSpecificTop);
fillSpecific在知道位置绘制指定项的item数据,然后上下绘制其他的item。它先通过makeAndAddView完成当前item的创建并添加到layout,fillDown、fillUp则根据layout大小,上下绘制item完成整个list的绘制,使用的方法也是makeAndAddView,adjustViewsUpOrDown调整item位置。
/**
* Put a specific item at a specific location on the screen and then build
* up and down from there.
*
* @param position The reference view to use as the starting point
* @param top Pixel offset from the top of this view to the top of the
* reference view.
*
* @return The selected view, or null if the selected view is outside the
* visible area.
*/
private View fillSpecific(int position, int top) {
boolean tempIsSelected = position == mSelectedPosition;
View temp = makeAndAddView(position, top, true, mListPadding.left, tempIsSelected);
…
below = fillDown(position + 1, temp.getBottom() + dividerHeight);
// This will correct for the bottom of the last view not touching the bottom of the list
adjustViewsUpOrDown();
above = fillUp(position - 1, temp.getTop() - dividerHeight);
int childCount = getChildCount();
if (childCount > 0) {
correctTooLow(childCount);
}
}
顾名思义,makeAndAddView就是新建一个view,并将它添加到list里面。obtainView负责创建view,setupChild负责将view添加到layout,并设置好view的位置和相关属性。
private View makeAndAddView(int position, int y, boolean flow, int childrenLeft,
boolean selected) {
View child;
child = obtainView(position, mIsScrap);
// This needs to be positioned and measured
setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]);
return child;
}
obtainView将对应位置的view和数据关联起来,并返回view。它通过mRecycler获取到缓存的view,再通过mAdapter.getView将数据填充到view,mAdapter.getView即是我们前面分析到的ConversationListAdapter(CursorAdapter).getView。
至此,Message列表的创建过程就明了了。
3.3 功能分析
3.3.1 Message列表更新
当数据库内容发生变化(包括新短信、删除、发送状态改变等),就会触发onContentChanged,其调用栈如下:
ConversationList$2.onContentChanged(ConversationListAdapter) line: 474
ConversationListAdapter.onContentChanged() line: 118
CursorAdapter$ChangeObserver.onChange(boolean) line: 463
CursorAdapter$ChangeObserver(ContentObserver).onChange(boolean, Uri) line: 129
ContentObserver$NotificationRunnable.run() line: 180
Handler.handleCallback(Message) line: 808
Handler.dispatchMessage(Message) line: 103
Looper.loop() line: 193
从下往上分析,
1)ContentObserver
我们已经很熟悉handleCallback处理的是Post的消息,消息一般是runable,所以可以看到ContentObserver. dispatchChange有post这一消息的过程(dispatchChange的触发过程比较复杂,我们将在其他章节介绍,本例中当数据库内容变化时会触发这个流程)。
public final void dispatchChange(boolean selfChange, Uri uri) {
if (mHandler == null) {
onChange(selfChange, uri);
} else {
mHandler.post(new NotificationRunnable(selfChange, uri));
}
}
然后这个消息会运行其内部类NotificationRunnable的run方法,
public void run() {
ContentObserver.this.onChange(mSelfChange, mUri);
}
This指向的是ContentObserver的实例ChangeObserver,它是CursorAdapter的内部类,也是ContentObserver的子类,由于ConversationListAdapter通过CursorWrapper向ContentObservable注册了ContentObserver,所以当dispatchChange事件时,通过轮询,最终将事件传递给ChangeObserver。
而后,ChangeObserver调用封装类CursorAdapter的onContentChanged,将事件上传。
private class ChangeObserver extends ContentObserver {
public void onChange(boolean selfChange) {
onContentChanged();
}
}
2)onContentChanged
onContentChanged是CursorAdapter的一个方法,主要功能是当数据库内容更新后,重新使用游标查询数据。但这个方法被ConversationListAdapter重载,它给自己的客户提供一个借口类OnContentChangedListener,让客户去实现这个借口类,自己则调用接口类的onContentChanged方法执行客户定义的功能。
protected void onContentChanged() {
if (mCursor != null && !mCursor.isClosed()) {
if (mOnContentChangedListener != null) {
mOnContentChangedListener.onContentChanged(this);
}
}
}
3)ConversationList
ConversationList就是ConversationListAdapter的客户,它实现了接口类,如下,主要是调用startAsyncQuery进行数据的查询。
private final ConversationListAdapter.OnContentChangedListener mContentChangedListener =
new ConversationListAdapter.OnContentChangedListener() {
@Override
public void onContentChanged(ConversationListAdapter adapter) {
if (mIsInActivity) {
mNeedQuery = true;
startAsyncQuery();
}
}
};
并且ConversationList在其onStart向ConversationListAdapter注册了这个接口类监听器:
mListAdapter.setOnContentChangedListener(mContentChangedListener);
所以,当数据库数据改变时,根据上面相关调用流程,完成了Message列表的刷新功能。
BTW,附上游标改变后注册ContentObserver的调用栈:
ContentResolver$CursorWrapperInner(CursorWrapper).registerContentObserver(ContentObserver) line: 178
ConversationListAdapter(CursorAdapter).swapCursor(Cursor) line: 340
ConversationListAdapter(CursorAdapter).changeCursor(Cursor) line: 313
ConversationList$ThreadListQueryHandler.onQueryComplete(int, Object, Cursor) line: 1896
ConversationList$ThreadListQueryHandler(AsyncQueryHandler).handleMessage(Message) line: 344
ConversationList$ThreadListQueryHandler(Handler).dispatchMessage(Message) line: 110
Looper.loop() line: 193
3.3.2 Message列表定位
因为ConversationList是ListActivity,所以可以使用getListView获取ListView,再使用ListView的setSelectionFromTop,将列表显示开始位置指定到特定位置。
需要注意2个问题,
1)
在ConversationList会记录上次浏览列表的起始位置,所以不是随意就能设置好列表的显示位置,如onQueryComplete通过下面代码控制了什么时候可以设置这个起始位置,我们需要根据mSavedFirstVisiblePosition的值和相关逻辑去处理。
if (mSavedFirstVisiblePosition != AdapterView.INVALID_POSITION) {
// Restore the list to its previous position.
getListView().setSelectionFromTop(mSavedFirstVisiblePosition,
mSavedFirstItemOffset);
mSavedFirstVisiblePosition = AdapterView.INVALID_POSITION;
}
结合列表的创建过程,我们知道,当同步绘制列表时,是从mSyncPosition开始的,
fillSpecific(mSyncPosition, mSpecificTop);
而mSyncPosition正是setSelectionFromTop里传递进去的,所以列表起始位置只能通过setSelectionFromTop来设置。
public void setSelectionFromTop(int position, int y) {
…
mSyncPosition = position;
…
}
2)
游标的更新,当数据库数据变化后,需要更新游标,才能获取到正确的值。下面是onQueryComplete完成后,用changeCursor替换掉旧游标的过程,防止游标数据过期。
ConversationListAdapter(CursorAdapter).swapCursor(Cursor) line: 340
ConversationListAdapter(CursorAdapter).changeCursor(Cursor) line: 313
ConversationList$ThreadListQueryHandler.onQueryComplete(int, Object, Cursor) line: 1896
ConversationList$ThreadListQueryHandler(AsyncQueryHandler).handleMessage(Message) line: 344
ConversationList$ThreadListQueryHandler(Handler).dispatchMessage(Message) line: 110
Looper.loop() line: 193
在列表创建时,CursorAdapter. getView的游标最初是在CursorAdapte创建时传入的,之后可以通过swapCursor的方法更新。
目 录
2. ConversationList
2.1.2.1 ConversationListAdapter
2.2.1 列表项click
2.2.2 Long Press click to Delete
2.3.1 会话ID创建
3. ConversationListItem
3.1.2.1 ConversationListItem实例创建
1. 概要
本文主要分析短信列表界面的主要控件的界面显示和处理逻辑。
2. ConversationList
2.1 ConversationList
2.1.1 Layout分析
ConversationList对应的layout是conversation_list_screen,它主要分3部分,第一部分是做全选的控制,第二部分是list列表,第三部分是列表为空的布局。具体的表现形式如下,布局文件就不再展开了。其中list列表将在另外的章节分析。
2.1.2 代码分析
2.1.2.1 ConversationListAdapter
ConversationListAdapter是ConversationListItem的数据适配器,在ConversationList.onCreate里创建,它通过bindView将会话信息绑定到item的layout上。
其创建过程如下,
private void initListAdapter() {
mListAdapter = new ConversationListAdapter(this, null);
/** M: now this code is useless and will lead to a JE, comment it.
* listener is set in onStart
*/
//mListAdapter.setOnContentChangedListener(mContentChangedListener);
setListAdapter(mListAdapter);
getListView().setRecyclerListener(mListAdapter);
}
因为ConversationList继承自ListActivity,setListAdapter将Adapter实例设置给ListActivity,这里会调用ensureList,它会加载一个布局,确保有view能用来对应Adapter。
public void setListAdapter(ListAdapter adapter) {
synchronized (this) {
ensureList();
mAdapter = adapter;
mList.setAdapter(adapter);
}
}
private void ensureList() {
if (mList != null) {
return;
}
setContentView(com.android.internal.R.layout.list_content_simple);
}
而实际上,mList已经在 onContentChanged被初始化为Layout里对应的ListView,
mList = (ListView)findViewById(com.android.internal.R.id.list);
这个ListView就是Message列表的容器了。
2.2 功能分析
2.2.1 列表项click
单击列表项的调用栈如下:
ConversationList.onListItemClick(ListView, View, int, long) line: 1086
ListActivity$2.onItemClick(AdapterView, View, int, long) line: 319
ListView(AdapterView).performItemClick(View, int, long) line: 298
ListView(AbsListView).performItemClick(View, int, long) line: 1128
AbsListView$PerformClick.run() line: 2815
AbsListView$1.run() line: 3574
Handler.handleCallback(Message) line: 800
ViewRootImpl$ViewRootHandler(Handler).dispatchMessage(Message) line: 100
2.2.2 Long Press click to Delete
长按item删除会话流程如下:
长按出现选择菜单,
mConvListOnCreateContextMenuListener
onCreateContextMenu
点击删除
ConversationList.confirmDeleteThreads(Collection, AsyncQueryHandler) line:
ConversationList.confirmDeleteThread(long, AsyncQueryHandler) line: 1320
ConversationList.onContextItemSelected(MenuItem) line: 1247
ConversationList(Activity).onMenuItemSelected(int, MenuItem) line: 2584
PhoneWindow$DialogMenuCallback.onMenuItemSelected(MenuBuilder, MenuItem) line:
开始删除之前,要做一个后台查询,看是否有lock住的消息,Conversation.startQueryHaveLockedMessages,
之后是后台查询handler.startQuery:
AsyncQueryHandler
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);
msg.arg1 = EVENT_ARG_QUERY;
WorkerArgs args = new WorkerArgs();
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);
}
这个查询过程将消息发给mWorkerThreadHandler,这是异步查询AsyncQueryHandler自己的handler,它封装完消息EVENT_ARG_QUERY后,再发送出去,
Message reply = args.handler.obtainMessage(token);
reply.obj = args;
reply.arg1 = msg.arg1;
reply.sendToTarget();
这个消息的接收对象的设置见args.handler = this; ,在本例中,这个handler是ThreadListQueryHandler。
数据库查询完成后执行onQueryComplete(HAVE_LOCKED_MESSAGES_TOKEN),
ConversationList$ThreadListQueryHandler.onQueryComplete(int, Object, Cursor) line: 1610
ConversationList$ThreadListQueryHandler(AsyncQueryHandler).handleMessage(Message) line: 344
ConversationList$ThreadListQueryHandler(Handler).dispatchMessage(Message) line: 107
Looper.loop() line: 194
之后会给出一个确认删除对话框confirmDeleteThreadDialog
在弹出的确认对话框中,选择删除,执行listener函数。
ConversationList$DeleteThreadListener.onClick(DialogInterface, int) line: 1484
AlertController$ButtonHandler.handleMessage(Message) line: 176
之后开始删除Conversation.startDelete
最后onDeleteComplete
响应函数会触发一系列其他的数据库操作,如查询list,查询unread等,
AsyncQueryHandler$WorkerHandler.handleMessage(Message) line: 120
AsyncQueryHandler$WorkerHandler(Handler).dispatchMessage(Message) line: 107
【Tips】
它的threadIds是在点击删除功能时,onContextItemSelected设置的,和长按的时候在onCreateContextMenu里选择的有时间差,不一定是同一个,在会话列表更新的时候,会有影响,所以会误删。
所以在onCreateContextMenu里要记录thread id。
2.3 数据操作
2.3.1 会话ID创建
Message列表是以会话为item进行显示的,会话是以为ID记录的,我们分析一下各种会话ID是如何创建的,
应用在CBMessageReceiverService.java收到小区广播后,使用handleCBMessageReceived方法进行处理,其中insertMessage是用来存储CB的。
insertMessage会调用CbProvider的insert方法
public Uri insert(Uri url, ContentValues initialValues) {
Uri result = null;
// TODO Check Permission
// checkPermission();
SQLiteDatabase db = mOpenHelper.getWritableDatabase();
SQLiteDatabase dbmmssms = mMmsSmsOpenHelper.getWritableDatabase();
int match = URI_MATCHER.match(url);
ContentValues values;
long rowID;
String table = null;
Log.d(TAG, " insert match = "+match);
switch (match) {
case URL_MESSAGES:
//table = CbDatabaseHelper.CBMESSAGE_TABLE;
table = MmsSmsDatabaseHelper.TABLE_CELLBROADCAST;
values = internalInsertMessages(initialValues);
rowID = dbmmssms.insert(table, null, values);
Log.d(TAG, "insert to cellbroadcast " + values);
if (rowID > 0) {
result = Uri.parse("content://messages/" + rowID);
notifyChange();
return result;
}
break;
…
}
在这里,我们重点关注一下CB Message的ThreadId是怎样获取的,在internalInsertMessages里面,显然threadId不是我们主动填充进去的,所以是通过getOrCreateThreadId获得的。
private ContentValues internalInsertMessages(ContentValues initialValues) {
ContentValues values;
if (initialValues != null) {
values = new ContentValues(initialValues);
} else {
values = new ContentValues();
}
Long threadId = values.getAsLong(EncapsulatedTelephony.SmsCb.THREAD_ID);
String address = values.getAsString(EncapsulatedTelephony.SmsCb.CHANNEL_ID);
if (((threadId == null) || (threadId == 0)) && (address != null)) {
values.put(EncapsulatedTelephony.SmsCb.THREAD_ID,
getOrCreateThreadId(address));
}
…
}
getOrCreateThreadId代码如下,它封装一个URI地址,包含了threadID和参数recipient、cellbroadcast,并使用SqliteWrapper.query进行查询,
private String getOrCreateThreadId(String address) {
//Uri.Builder uriBuilder = THREAD_ID_URI.buildUpon();
Uri.Builder uriBuilder = (Uri.parse("content://mms-sms/threadID")).buildUpon();
uriBuilder.appendQueryParameter("recipient", address);
uriBuilder.appendQueryParameter("cellbroadcast", address);
Uri uri = uriBuilder.build();
// TODO need replace with helper interface.
Cursor cursor = null;
long token = Binder.clearCallingIdentity();
try {
cursor = SqliteWrapper.query(this.getContext(), this
.getContext().getContentResolver(), uri, ID_PROJECTION, null,
null, null);
…
if (cursor.moveToFirst()) {
return String.valueOf(cursor.getLong(0));
…
}
SqliteWrapper会调用到ContentResolver,ContentResolver则最终调用到MmsSmsProvider,
在MmsSmsProvider.query里面,也是通过URI match找到操作码URI_THREAD_ID,根据参数cellbroadcast,使用getCBThreadId给CB查找或创建其ThreadId。
Threads表内容如下所示,里面的关键字段有_id ,recipient_ids,要想找到合适的ThreadId,只有通过recipient_ids来查询,
所以在getCBThreadId方法里面,通过getAddressIds从canonical_addresses表里面找到和号码匹配的recipient_ids列表,再到Threads表里面找到recipient_ids相同和type是小区广播的_id,即是我们需要的ThreadId,使用的查询语句如下。
String queryString = "SELECT _id FROM threads " + "WHERE type=" + EncapsulatedTelephony.Threads.CELL_BROADCAST_THREAD + " AND recipient_ids=?";
如果没有满足条件的ThreadId,调用insertThread()创建一个新的thread。
在Message列表界面通过按钮新建短信时,执行createNewMessage(),通过createIntent传递一个threadId=0的ID过去,
private void createNewMessage() {
startActivity(ComposeMessageActivity.createIntent(this, 0));
}
public static Intent createIntent(Context context, long threadId) {
Intent intent = new Intent(context, ComposeMessageActivity.class);
if (threadId > 0) {
intent.setData(Conversation.getUri(threadId));
}
return intent;
}
因为threadId为0,在会话创建中会获取或创建一个threadId,具体过程和下节相同。
在联系人的详情界面,点击发送短信,因为有号码存在,所以在ComposeMessageActivity.Oncreate方法里面会去查询或创建一个threadId。
onCreate的initActivityState里,有这么一段代码是获取会话实例的,mConversation.get通过号码获取到会话实例,
Uri intentData = intent.getData();
/// M: Code analyze 034, If intent is SEND,just create a new empty thread,
/// otherwise Conversation.get() will throw exception.
String action = intent.getAction();
if (intentData != null && (TextUtils.isEmpty(action) ||
!action.equals(Intent.ACTION_SEND))) {
/// M: group-contact send message
// try to get a conversation based on the data URI passed to our intent.
if (intentData.getPathSegments().size() < 2) {
mConversation = mConversation.get(getApplicationContext(),ContactList.getByNumbers(
getStringForMultipleRecipients(Conversation.getRecipients(intentData)),
false /* don't block */, true /* replace number */),false);
} else {
mConversation = Conversation.get(getApplicationContext(), intentData, false);
}
/// @}
mWorkingMessage.setText(getBody(intentData));
Conversation get代码如下,如果号码为空则新建会话,如果缓冲里存储了对应号码的会话,则使用缓冲里的数据,如果条件都不满足,则需要去数据库查询到ID,并新建会话,之后将新会话存放到缓冲。
public static Conversation get(Context context, ContactList recipients, boolean allowQuery) {
if (recipients.size() < 1) {
return createNew(context);
}
Conversation conv = Cache.get(recipients);
if (conv != null) {
return conv;
}
long threadId = getOrCreateThreadId(context, recipients);
conv = new Conversation(context, threadId, allowQuery);
。。。
Cache.put(conv);
}
getOrCreateThreadId用来获取ThreadId,由Threads.getOrCreateThreadId实现,这个方法在Telephony.java里面。
getOrCreateThreadId类似前面提到的小区广播的实现过程,先封装一个"content://mms-sms/threadID"的URI,但只使用一个参数recipient,
在MmsSmsProvider.query里面,也是通过URI match找到操作码URI_THREAD_ID,,使用getThreadId查找或创建其ThreadId,getAddressIds获取到匹配的recipientIds,再取Threads表里查询,如果查询到,返回游标,如果没有记录,则用insertThread向Threads里插入记录,再查询到ThreadId。
private synchronized Cursor getThreadId(List<String> recipients) {
Set<Long> addressIds = getAddressIds(recipients);
。。。
recipientIds = Long.toString(addressId);
。。。
String[] selectionArgs = new String[] { recipientIds };
SQLiteDatabase db = mOpenHelper.getReadableDatabase();
db.beginTransaction();
Cursor cursor = null;
try {
// Find the thread with the given recipients
cursor = db.rawQuery(THREAD_QUERY, selectionArgs);
if (cursor.getCount() == 0) {
// No thread with those recipients exists, so create the thread.
cursor.close();
Log.d(LOG_TAG, "getThreadId: create new thread_id for recipients " + recipients);
insertThread(recipientIds, recipients);
// The thread was just created, now find it and return it.
cursor = db.rawQuery(THREAD_QUERY, selectionArgs);
。。。
}
3. ConversationListItem
3.1 ConversationListItem
3.1.1 Layout分析
如前所述,短信记录的每条信息显示是使用conversation_list_item.xml布局的,在本例中,其直观显示是如下形式的一条短信记录,
conversation_list_item.xml文件如下,它是一个ConversationListItem类型的布局,
(需要注意不同屏幕使用这个文件的位置,本例中在packages\apps\Mms\res\layout-mdpi下面,不是packages\apps\Mms\res\layout)
conversation_list_item.xml
<?xml version="1.0" encoding="utf-8"?>
<com.android.mms.ui.ConversationListItem xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="?android:attr/listPreferredItemHeight"
android:background="?android:attr/activatedBackgroundIndicator" >
<CheckBox
android:id="@+id/select_box"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_centerVertical="true"
android:clickable="false"
android:focusable="false"
android:focusableInTouchMode="false" />
<com.android.mms.ui.MmsQuickContactBadge
android:id="@+id/avatar"
android:visibility="gone"
android:layout_centerVertical="true"
android:layout_toEndOf="@id/select_box"
style="?android:attr/quickContactBadgeStyleWindowLarge" />
<ImageView
android:id="@+id/presence"
android:visibility="invisible"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_alignParentTop="true"
android:src="@drawable/ic_unread_label"/>
<LinearLayout
android:id="@+id/iconlist"
android:layout_alignParentTop="true"
android:layout_alignParentEnd="true"
android:layout_marginTop="7dip"
android:layout_marginEnd="10dip"
android:orientation="horizontal"
android:layout_height="wrap_content"
android:layout_width="wrap_content" >
<ImageView android:id="@+id/draft"
android:visibility="gone"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:src="@drawable/ic_draft" />
<ImageView android:id="@+id/error"
android:paddingStart="6dp"
android:visibility="gone"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:src="@drawable/ic_list_alert_sms_failed" />
<ImageView android:id="@+id/attachment"
android:paddingStart="6dp"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:visibility="gone"
android:src="@drawable/ic_attachment_universal_small" />
<ImageView android:id="@+id/mute"
android:paddingStart="6dp"
android:visibility="gone"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:src="@drawable/ipmsg_silent" />
</LinearLayout>
<RelativeLayout
android:layout_alignParentTop="true"
android:layout_marginTop="4dip"
android:layout_marginStart="12dip"
android:layout_toEndOf="@id/avatar"
android:layout_toStartOf="@id/iconlist"
android:gravity="center_vertical"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView android:id="@+id/unread"
android:background="@drawable/ipmsg_message_box"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:textSize="12sp"
android:textColor="@color/text_color_unread"
android:singleLine="true"
android:layout_alignParentEnd="true" />
<RelativeLayout android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:layout_toStartOf="@id/unread">
<TextView android:id="@+id/draft_and_smgcount"
android:textAppearance="?android:attr/textAppearanceMedium"
android:layout_alignParentEnd="true"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="6dip"
android:singleLine="true" />
<com.android.mms.util.AlwaysMarqueeTextView android:id="@+id/from"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceMedium"
android:singleLine="true"
android:layout_alignParentStart="true"
android:layout_toStartOf="@id/draft_and_smgcount"
android:layout_marginEnd="6dip"
android:ellipsize="marquee" />
</RelativeLayout>
</RelativeLayout>
<TextView android:id="@+id/date"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:textSize= "12sp"
android:textColor="@color/dlg_text_counter_color"
android:layout_alignParentBottom="true"
android:layout_alignParentEnd="true"
android:layout_marginEnd="10dip"
android:layout_marginBottom="4dip"
android:gravity="bottom" />
<TextView android:id="@+id/subject"
android:layout_height="wrap_content"
android:layout_width="fill_parent"
android:textSize= "12sp"
android:layout_alignParentBottom="true"
android:layout_toEndOf="@id/avatar"
android:layout_toStartOf="@id/date"
android:layout_marginStart="12dip"
android:layout_marginBottom="4dip"
android:gravity="bottom"
android:textColor="@color/dlg_text_counter_color"
android:lines="2"
android:ellipsize="end" />
</com.android.mms.ui.ConversationListItem>
其对应的布局图形如下,图中左边是layout组成,右上是layout布局表现,右下是对layout里面的子view编号,便于理解。
分析xml文件,并对照图例可以看出,
ConversationListItem是整个item布局类,表现形式见下图左边,它指定了高度
android:layout_height="?android:attr/listPreferredItemHeight"
这个布局由6个同级view组成,见下图中间及右上,其中
checkbox供选择使用,见右下数字0,alignParentStart表示它在开头位置,
android:layout_alignParentStart="true"
MmsQuickContactBadge做头像显示,见右下数字1,
ImageView做未读标识等用途,见右下数字2,通过下面的属性将它放置在最右上的位置
android:layout_alignParentEnd="true"
android:layout_alignParentTop="true"
LinearLayout是个组合视图,见右下数字3和iconlist小图,它有四个子ImageView表示短信的信息状态,具体名字见iconlist小图。注意四个子ImageView表示同时最多只能显示四个图标,不是表示短信只有这四种状态。通过下面属性,将它放置在MmsQuickContactBadge后面。
android:orientation="horizontal"
<iconlist>
RelativeLayout是个组合视图,见右下数字4和Text Title小图,包含3个TextView,用来显示Title和消息数目、未读消息数目。在RelativeLayout,通过下面的属性设置,将RelativeLayout(index4的view)和上面的LinearLayout(index3的view)并行放置在同一行。
android:layout_toEndOf="@id/avatar"
android:layout_toStartOf="@id/iconlist"
TextView显示日期,见右下数字5,用下面2个属性表示它的位置在右下,
android:layout_alignParentBottom="true"
android:layout_alignParentEnd="true"
TextView 显示短线截取的部分内容,见右下数字6,用下面3个属性表明了它的位置。
android:layout_alignParentBottom="true"
android:layout_toEndOf="@id/avatar"
android:layout_toStartOf="@id/date"
<Text Content>
3.1.2 代码分析
3.1.2.1 ConversationListItem实例创建
ConversationListItem实例创建栈如下,因为有缓存使用,不一定每次进入列表都会创建这个类实例,所以不一定能调试到,要在此设置断点调试,有一个小技巧,就是将mms进程终止掉,让其自动重启(可通过发送短信或cb的方法加快重启过程),这样就可以进行调试。
ConversationListItem.<init>(Context, AttributeSet) line: 130
Constructor.constructNative(Object[], Class, Class[], int, boolean) line: not available [native method]
Constructor.newInstance(Object...) line: 423
PhoneLayoutInflater(LayoutInflater).createView(String, String, AttributeSet) line: 594
PhoneLayoutInflater(LayoutInflater).createViewFromTag(View, String, AttributeSet) line: 696
PhoneLayoutInflater(LayoutInflater).inflate(XmlPullParser, ViewGroup, boolean) line: 469
PhoneLayoutInflater(LayoutInflater).inflate(int, ViewGroup, boolean) line: 397
ConversationListAdapter.newView(Context, Cursor, ViewGroup) line: 103
ConversationListAdapter(CursorAdapter).getView(int, View, ViewGroup) line: 250
ListView(AbsListView).obtainView(int, boolean[]) line: 2338
ListView.makeAndAddView(int, int, boolean, int, boolean) line: 1812
ListView.fillDown(int, int) line: 698
ListView.fillFromTop(int) line: 759
ListView.layoutChildren() line: 1631
ListView(AbsListView).onLayout(boolean, int, int, int, int) line: 2149
ConversationListItem实例创建的主要过程分析如下,在getView来获取view的时候,因为缓冲没有数据,会使用newView新建一个view,
public View getView(int position, View convertView, ViewGroup parent) {
View v;
if (convertView == null) {
v = newView(mContext, mCursor, parent);
} else {
v = convertView;
}
bindView(v, mContext, mCursor);
return v;
}
newView实际的过程是inflate一个布局文件,这个布局文件就是我们分析的layout,它是ConversationListItem形式的,我们知道,在inflate创建布局文件时,会根据节点创建view实例,所以会使用类的构造类Constructor来创建ConversationListItem实例。
public View newView(Context context, Cursor cursor, ViewGroup parent) {
if (LOCAL_LOGV) Log.v(TAG, "inflating new view");
return mFactory.inflate(R.layout.conversation_list_item, parent, false);
}
Checkbox的主要作用是用来勾选短信列表的某一个item,并进行删除操作的。
对于checkbox,在ConversationListItem里面定义了一个引用mSelectBox,
private CheckBox mSelectBox;
并在onFinishInflate里面初始化它,关联到Layout里面的select_box,
mSelectBox = (CheckBox) findViewById(R.id.select_box);
当列表项被点击时,执行ConversationList.onListItemClick(ListView, View, int, long)这个方法,在里面添加下面代码,当处于删除模式时,如下,将Conversation设置为check状态,然后更新界面,
if (mListAdapter != null && mDeleteMode) {
if (conv.isChecked()) {
conv.setIsChecked(false);
} else {
conv.setIsChecked(true);
}
mListAdapter.notifyDataSetChanged();
updateSelectView();
return;
}
更新界面时,执行ConversationListItem.bind(Context, Conversation) ,每加载一个item项都会执行一次bind,在这里,会根据mSelectMode 决定checkbox是否显示,根据Conversation的状态决定是否选中。
所以实际上,不是我们主观上看到的那样由checkbox决定了当前项目是否选中,而是当前项选中后决定checkbox的显示状态。
if (mSelectMode) {
mSelectBox.setVisibility(View.VISIBLE);
mAvatarView.setClickable(false);
} else {
mSelectBox.setVisibility(View.GONE);
mAvatarView.setClickable(true);
}
if (mConversation.isChecked()) {
mSelectBox.setChecked(true);
} else {
mSelectBox.setChecked(false);
}
上面就是checkbox在代码里的处理过程。
R.id.avatar用来显示发件人头像,定义如下,MmsQuickContactBadge继承自QuickContactBadge,再往上是ImageView,一个通用的视图组件。
private MmsQuickContactBadge mAvatarView;
其实例获取在ConversationListItem的onFinishInflate里面,
mAvatarView = (MmsQuickContactBadge) findViewById(R.id.avatar);
如果希望AvatarView能够被点击,相应事件,则可以看到在bind有如下代码,表示在删除模式下不能被点击,在列表模式下可以被点击。
if (mSelectMode) {
mAvatarView.setClickable(false);
} else {
mAvatarView.setClickable(true);
}
AvatarView被点击之后,执行的调用栈如下,可见是由View直接调用了MmsQuickContactBadge类的onClick,并不是ConversationList或ConversationListItem里做了onClick的相关处理。
MmsQuickContactBadge(QuickContactBadge).onClick(View) line: 217
MmsQuickContactBadge.onClick(View) line: 75
MmsQuickContactBadge(View).performClick() line: 4212
View$PerformClick.run() line: 17476
Handler.handleCallback(Message) line: 800
ViewRootImpl$ViewRootHandler(Handler).dispatchMessage(Message) line: 100
QuickContactBadge类的onClick代码如下,
@Override
public void onClick(View v) {
if (mContactUri != null) {
QuickContact.showQuickContact(getContext(), QuickContactBadge.this, mContactUri,
QuickContact.MODE_LARGE, mExcludeMimes);
} else if (mContactEmail != null) {
mQueryHandler.startQuery(TOKEN_EMAIL_LOOKUP_AND_TRIGGER, mContactEmail,
Uri.withAppendedPath(Email.CONTENT_LOOKUP_URI, Uri.encode(mContactEmail)),
EMAIL_LOOKUP_PROJECTION, null, null, null);
} else if (mContactPhone != null) {
mQueryHandler.startQuery(TOKEN_PHONE_LOOKUP_AND_TRIGGER, mContactPhone,
Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI, mContactPhone),
PHONE_LOOKUP_PROJECTION, null, null, null);
} else {
// If a contact hasn't been assigned, don't react to click.
return;
}
}
我们当前的代码走mContactPhone路径,mContactPhone是在assignContactFromPhone里面赋值的,assignContactFromPhone则是通过ConversationListItem的updateAvatarView调用的(传入的number就赋值给mContactPhone),
mAvatarView.assignContactFromPhone(number, true);
updateAvatarView可以被updateFromView和bind调用,这两个方法我们在前面有过描述。所以从整个过程来看,我们可以看出avatar被点击和其需要的数据来源。
3.2 绘制分析
3.2.1 Message列表创建
如ConversationList的layout描述,Messag列表主要是由list呈现,而list的item项是由ConversationListItem布局的,我们这里讨论每个item项的创建以及整个list的创建。
在使用Cache时,ConversationListItem的创建过程如下,
ConversationListItem.bind(Context, Conversation) line: 480
ConversationListAdapter.bindView(View, Context, Cursor) line: 77
ConversationListAdapter(CursorAdapter).getView(int, View, ViewGroup) line: 250
ListView(AbsListView).obtainView(int, boolean[]) line: 2186
ListView.makeAndAddView(int, int, boolean, int, boolean) line: 1877
ListView.fillSpecific(int, int) line: 1356
ListView.layoutChildren() line: 1671
ListView(AbsListView).onLayout(boolean, int, int, int, int) line: 2037
ListView(View).layout(int, int, int, int) line: 14118
ListView(ViewGroup).layout(int, int, int, int) line: 4467
RelativeLayout.onLayout(boolean, int, int, int, int) line: 1021
从上往下分析该栈:
1)ConversationListItem.bind
ConversationListItem.bind的代码比较多,仔细分析,实际上逻辑比较简单,主要是设置每个view的内容和状态,比如说设置日期的代码如下,
mDateView.setVisibility(VISIBLE);
if(ConversationList.sConversationListOption ==
ConversationList.OPTION_CONVERSATION_LIST_IMPORTANT &&
!conversation.hasUnreadMessages()) {
mDateView.setText(MessageUtils.formatTimeStampStringExtend(context,
conversation.getImpDate()));
}
其数据来源于conversation,我们要进一步分析conversation是怎么传递进来的,以及为什么要调用bind。
2)ConversationListAdapter
ConversationListAdapter 是ConversationList的layout里的list对应的Adapter,它继承自MessageCursorAdapter-CursorAdapter-baseAdapter, 其方法bindView在本例中执行的主要代码如下,其中headerview是一个ConversationListItem引用,是调用方法传递过来的,conversation是根据游标新建的,最后调用bind,所以该方法的作用就是获取到conversation,并用准备将其中的数据赋值到view。
public void bindView(View view, Context context, Cursor cursor) {
ConversationListItem headerView = (ConversationListItem) view;
Conversation conv;
if (!mIsScrolling) {
Conversation.setNeedCacheConv(false);
conv = Conversation.from(context, cursor);
Conversation.setNeedCacheConv(true);
if (mSubjectSingleLine) {
headerView.setSubjectSingleLineMode(true);
}
if (conv != null) {
conv.setIsChecked(sSelectedTheadsId.contains(conv.getThreadId()));
}
headerView.bind(context, conv);
}
}
(CursorAdapter).getView则负责获取到view实例和游标实例。
view的获取有两种方式,一种是调用者传递过来以前缓存的,一种是新创建一个,在ConversationListItem实例创建中有讲到如何创建。
游标实例则在Adapter创建时创建。
3)ListView
在ListView部分,我们从layoutChildren向上分析,向下部分设计到太多ListView的实现机制问题,暂不讨论。
layoutChildren实现ListView子窗口布局,实际上就是list在layout所占部分。
本例从launcher进入Message列表,执行代码路径如下,mSyncPosition是当前item索引值,mSpecificTop是当前layout相对Top的位置。
layoutChildren:
case LAYOUT_SYNC:
sel = fillSpecific(mSyncPosition, mSpecificTop);
fillSpecific在知道位置绘制指定项的item数据,然后上下绘制其他的item。它先通过makeAndAddView完成当前item的创建并添加到layout,fillDown、fillUp则根据layout大小,上下绘制item完成整个list的绘制,使用的方法也是makeAndAddView,adjustViewsUpOrDown调整item位置。
/**
* Put a specific item at a specific location on the screen and then build
* up and down from there.
*
* @param position The reference view to use as the starting point
* @param top Pixel offset from the top of this view to the top of the
* reference view.
*
* @return The selected view, or null if the selected view is outside the
* visible area.
*/
private View fillSpecific(int position, int top) {
boolean tempIsSelected = position == mSelectedPosition;
View temp = makeAndAddView(position, top, true, mListPadding.left, tempIsSelected);
…
below = fillDown(position + 1, temp.getBottom() + dividerHeight);
// This will correct for the bottom of the last view not touching the bottom of the list
adjustViewsUpOrDown();
above = fillUp(position - 1, temp.getTop() - dividerHeight);
int childCount = getChildCount();
if (childCount > 0) {
correctTooLow(childCount);
}
}
顾名思义,makeAndAddView就是新建一个view,并将它添加到list里面。obtainView负责创建view,setupChild负责将view添加到layout,并设置好view的位置和相关属性。
private View makeAndAddView(int position, int y, boolean flow, int childrenLeft,
boolean selected) {
View child;
child = obtainView(position, mIsScrap);
// This needs to be positioned and measured
setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]);
return child;
}
obtainView将对应位置的view和数据关联起来,并返回view。它通过mRecycler获取到缓存的view,再通过mAdapter.getView将数据填充到view,mAdapter.getView即是我们前面分析到的ConversationListAdapter(CursorAdapter).getView。
至此,Message列表的创建过程就明了了。
3.3 功能分析
3.3.1 Message列表更新
当数据库内容发生变化(包括新短信、删除、发送状态改变等),就会触发onContentChanged,其调用栈如下:
ConversationList$2.onContentChanged(ConversationListAdapter) line: 474
ConversationListAdapter.onContentChanged() line: 118
CursorAdapter$ChangeObserver.onChange(boolean) line: 463
CursorAdapter$ChangeObserver(ContentObserver).onChange(boolean, Uri) line: 129
ContentObserver$NotificationRunnable.run() line: 180
Handler.handleCallback(Message) line: 808
Handler.dispatchMessage(Message) line: 103
Looper.loop() line: 193
从下往上分析,
1)ContentObserver
我们已经很熟悉handleCallback处理的是Post的消息,消息一般是runable,所以可以看到ContentObserver. dispatchChange有post这一消息的过程(dispatchChange的触发过程比较复杂,我们将在其他章节介绍,本例中当数据库内容变化时会触发这个流程)。
public final void dispatchChange(boolean selfChange, Uri uri) {
if (mHandler == null) {
onChange(selfChange, uri);
} else {
mHandler.post(new NotificationRunnable(selfChange, uri));
}
}
然后这个消息会运行其内部类NotificationRunnable的run方法,
public void run() {
ContentObserver.this.onChange(mSelfChange, mUri);
}
This指向的是ContentObserver的实例ChangeObserver,它是CursorAdapter的内部类,也是ContentObserver的子类,由于ConversationListAdapter通过CursorWrapper向ContentObservable注册了ContentObserver,所以当dispatchChange事件时,通过轮询,最终将事件传递给ChangeObserver。
而后,ChangeObserver调用封装类CursorAdapter的onContentChanged,将事件上传。
private class ChangeObserver extends ContentObserver {
public void onChange(boolean selfChange) {
onContentChanged();
}
}
2)onContentChanged
onContentChanged是CursorAdapter的一个方法,主要功能是当数据库内容更新后,重新使用游标查询数据。但这个方法被ConversationListAdapter重载,它给自己的客户提供一个借口类OnContentChangedListener,让客户去实现这个借口类,自己则调用接口类的onContentChanged方法执行客户定义的功能。
protected void onContentChanged() {
if (mCursor != null && !mCursor.isClosed()) {
if (mOnContentChangedListener != null) {
mOnContentChangedListener.onContentChanged(this);
}
}
}
3)ConversationList
ConversationList就是ConversationListAdapter的客户,它实现了接口类,如下,主要是调用startAsyncQuery进行数据的查询。
private final ConversationListAdapter.OnContentChangedListener mContentChangedListener =
new ConversationListAdapter.OnContentChangedListener() {
@Override
public void onContentChanged(ConversationListAdapter adapter) {
if (mIsInActivity) {
mNeedQuery = true;
startAsyncQuery();
}
}
};
并且ConversationList在其onStart向ConversationListAdapter注册了这个接口类监听器:
mListAdapter.setOnContentChangedListener(mContentChangedListener);
所以,当数据库数据改变时,根据上面相关调用流程,完成了Message列表的刷新功能。
BTW,附上游标改变后注册ContentObserver的调用栈:
ContentResolver$CursorWrapperInner(CursorWrapper).registerContentObserver(ContentObserver) line: 178
ConversationListAdapter(CursorAdapter).swapCursor(Cursor) line: 340
ConversationListAdapter(CursorAdapter).changeCursor(Cursor) line: 313
ConversationList$ThreadListQueryHandler.onQueryComplete(int, Object, Cursor) line: 1896
ConversationList$ThreadListQueryHandler(AsyncQueryHandler).handleMessage(Message) line: 344
ConversationList$ThreadListQueryHandler(Handler).dispatchMessage(Message) line: 110
Looper.loop() line: 193
3.3.2 Message列表定位
因为ConversationList是ListActivity,所以可以使用getListView获取ListView,再使用ListView的setSelectionFromTop,将列表显示开始位置指定到特定位置。
需要注意2个问题,
1)
在ConversationList会记录上次浏览列表的起始位置,所以不是随意就能设置好列表的显示位置,如onQueryComplete通过下面代码控制了什么时候可以设置这个起始位置,我们需要根据mSavedFirstVisiblePosition的值和相关逻辑去处理。
if (mSavedFirstVisiblePosition != AdapterView.INVALID_POSITION) {
// Restore the list to its previous position.
getListView().setSelectionFromTop(mSavedFirstVisiblePosition,
mSavedFirstItemOffset);
mSavedFirstVisiblePosition = AdapterView.INVALID_POSITION;
}
结合列表的创建过程,我们知道,当同步绘制列表时,是从mSyncPosition开始的,
fillSpecific(mSyncPosition, mSpecificTop);
而mSyncPosition正是setSelectionFromTop里传递进去的,所以列表起始位置只能通过setSelectionFromTop来设置。
public void setSelectionFromTop(int position, int y) {
…
mSyncPosition = position;
…
}
2)
游标的更新,当数据库数据变化后,需要更新游标,才能获取到正确的值。下面是onQueryComplete完成后,用changeCursor替换掉旧游标的过程,防止游标数据过期。
ConversationListAdapter(CursorAdapter).swapCursor(Cursor) line: 340
ConversationListAdapter(CursorAdapter).changeCursor(Cursor) line: 313
ConversationList$ThreadListQueryHandler.onQueryComplete(int, Object, Cursor) line: 1896
ConversationList$ThreadListQueryHandler(AsyncQueryHandler).handleMessage(Message) line: 344
ConversationList$ThreadListQueryHandler(Handler).dispatchMessage(Message) line: 110
Looper.loop() line: 193
在列表创建时,CursorAdapter. getView的游标最初是在CursorAdapte创建时传入的,之后可以通过swapCursor的方法更新。
6572_message_conversationList_详细分析
目 录
2. ConversationList
2.1.2.1 ConversationListAdapter
2.2.1 列表项click
2.2.2 Long Press click to Delete
2.3.1 会话ID创建
3. ConversationListItem
3.1.2.1 ConversationListItem实例创建
1. 概要
本文主要分析短信列表界面的主要控件的界面显示和处理逻辑。
2. ConversationList
2.1 ConversationList
2.1.1 Layout分析
ConversationList对应的layout是conversation_list_screen,它主要分3部分,第一部分是做全选的控制,第二部分是list列表,第三部分是列表为空的布局。具体的表现形式如下,布局文件就不再展开了。其中list列表将在另外的章节分析。
2.1.2 代码分析
2.1.2.1 ConversationListAdapter
ConversationListAdapter是ConversationListItem的数据适配器,在ConversationList.onCreate里创建,它通过bindView将会话信息绑定到item的layout上。
其创建过程如下,
private void initListAdapter() {
mListAdapter = new ConversationListAdapter(this, null);
/** M: now this code is useless and will lead to a JE, comment it.
* listener is set in onStart
*/
//mListAdapter.setOnContentChangedListener(mContentChangedListener);
setListAdapter(mListAdapter);
getListView().setRecyclerListener(mListAdapter);
}
因为ConversationList继承自ListActivity,setListAdapter将Adapter实例设置给ListActivity,这里会调用ensureList,它会加载一个布局,确保有view能用来对应Adapter。
public void setListAdapter(ListAdapter adapter) {
synchronized (this) {
ensureList();
mAdapter = adapter;
mList.setAdapter(adapter);
}
}
private void ensureList() {
if (mList != null) {
return;
}
setContentView(com.android.internal.R.layout.list_content_simple);
}
而实际上,mList已经在 onContentChanged被初始化为Layout里对应的ListView,
mList = (ListView)findViewById(com.android.internal.R.id.list);
这个ListView就是Message列表的容器了。
2.2 功能分析
2.2.1 列表项click
单击列表项的调用栈如下:
ConversationList.onListItemClick(ListView, View, int, long) line: 1086
ListActivity$2.onItemClick(AdapterView, View, int, long) line: 319
ListView(AdapterView).performItemClick(View, int, long) line: 298
ListView(AbsListView).performItemClick(View, int, long) line: 1128
AbsListView$PerformClick.run() line: 2815
AbsListView$1.run() line: 3574
Handler.handleCallback(Message) line: 800
ViewRootImpl$ViewRootHandler(Handler).dispatchMessage(Message) line: 100
2.2.2 Long Press click to Delete
长按item删除会话流程如下:
长按出现选择菜单,
mConvListOnCreateContextMenuListener
onCreateContextMenu
点击删除
ConversationList.confirmDeleteThreads(Collection, AsyncQueryHandler) line:
ConversationList.confirmDeleteThread(long, AsyncQueryHandler) line: 1320
ConversationList.onContextItemSelected(MenuItem) line: 1247
ConversationList(Activity).onMenuItemSelected(int, MenuItem) line: 2584
PhoneWindow$DialogMenuCallback.onMenuItemSelected(MenuBuilder, MenuItem) line:
开始删除之前,要做一个后台查询,看是否有lock住的消息,Conversation.startQueryHaveLockedMessages,
之后是后台查询handler.startQuery:
AsyncQueryHandler
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);
msg.arg1 = EVENT_ARG_QUERY;
WorkerArgs args = new WorkerArgs();
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);
}
这个查询过程将消息发给mWorkerThreadHandler,这是异步查询AsyncQueryHandler自己的handler,它封装完消息EVENT_ARG_QUERY后,再发送出去,
Message reply = args.handler.obtainMessage(token);
reply.obj = args;
reply.arg1 = msg.arg1;
reply.sendToTarget();
这个消息的接收对象的设置见args.handler = this; ,在本例中,这个handler是ThreadListQueryHandler。
数据库查询完成后执行onQueryComplete(HAVE_LOCKED_MESSAGES_TOKEN),
ConversationList$ThreadListQueryHandler.onQueryComplete(int, Object, Cursor) line: 1610
ConversationList$ThreadListQueryHandler(AsyncQueryHandler).handleMessage(Message) line: 344
ConversationList$ThreadListQueryHandler(Handler).dispatchMessage(Message) line: 107
Looper.loop() line: 194
之后会给出一个确认删除对话框confirmDeleteThreadDialog
在弹出的确认对话框中,选择删除,执行listener函数。
ConversationList$DeleteThreadListener.onClick(DialogInterface, int) line: 1484
AlertController$ButtonHandler.handleMessage(Message) line: 176
之后开始删除Conversation.startDelete
最后onDeleteComplete
响应函数会触发一系列其他的数据库操作,如查询list,查询unread等,
AsyncQueryHandler$WorkerHandler.handleMessage(Message) line: 120
AsyncQueryHandler$WorkerHandler(Handler).dispatchMessage(Message) line: 107
【Tips】
它的threadIds是在点击删除功能时,onContextItemSelected设置的,和长按的时候在onCreateContextMenu里选择的有时间差,不一定是同一个,在会话列表更新的时候,会有影响,所以会误删。
所以在onCreateContextMenu里要记录thread id。
2.3 数据操作
2.3.1 会话ID创建
Message列表是以会话为item进行显示的,会话是以为ID记录的,我们分析一下各种会话ID是如何创建的,
应用在CBMessageReceiverService.java收到小区广播后,使用handleCBMessageReceived方法进行处理,其中insertMessage是用来存储CB的。
insertMessage会调用CbProvider的insert方法
public Uri insert(Uri url, ContentValues initialValues) {
Uri result = null;
// TODO Check Permission
// checkPermission();
SQLiteDatabase db = mOpenHelper.getWritableDatabase();
SQLiteDatabase dbmmssms = mMmsSmsOpenHelper.getWritableDatabase();
int match = URI_MATCHER.match(url);
ContentValues values;
long rowID;
String table = null;
Log.d(TAG, " insert match = "+match);
switch (match) {
case URL_MESSAGES:
//table = CbDatabaseHelper.CBMESSAGE_TABLE;
table = MmsSmsDatabaseHelper.TABLE_CELLBROADCAST;
values = internalInsertMessages(initialValues);
rowID = dbmmssms.insert(table, null, values);
Log.d(TAG, "insert to cellbroadcast " + values);
if (rowID > 0) {
result = Uri.parse("content://messages/" + rowID);
notifyChange();
return result;
}
break;
…
}
在这里,我们重点关注一下CB Message的ThreadId是怎样获取的,在internalInsertMessages里面,显然threadId不是我们主动填充进去的,所以是通过getOrCreateThreadId获得的。
private ContentValues internalInsertMessages(ContentValues initialValues) {
ContentValues values;
if (initialValues != null) {
values = new ContentValues(initialValues);
} else {
values = new ContentValues();
}
Long threadId = values.getAsLong(EncapsulatedTelephony.SmsCb.THREAD_ID);
String address = values.getAsString(EncapsulatedTelephony.SmsCb.CHANNEL_ID);
if (((threadId == null) || (threadId == 0)) && (address != null)) {
values.put(EncapsulatedTelephony.SmsCb.THREAD_ID,
getOrCreateThreadId(address));
}
…
}
getOrCreateThreadId代码如下,它封装一个URI地址,包含了threadID和参数recipient、cellbroadcast,并使用SqliteWrapper.query进行查询,
private String getOrCreateThreadId(String address) {
//Uri.Builder uriBuilder = THREAD_ID_URI.buildUpon();
Uri.Builder uriBuilder = (Uri.parse("content://mms-sms/threadID")).buildUpon();
uriBuilder.appendQueryParameter("recipient", address);
uriBuilder.appendQueryParameter("cellbroadcast", address);
Uri uri = uriBuilder.build();
// TODO need replace with helper interface.
Cursor cursor = null;
long token = Binder.clearCallingIdentity();
try {
cursor = SqliteWrapper.query(this.getContext(), this
.getContext().getContentResolver(), uri, ID_PROJECTION, null,
null, null);
…
if (cursor.moveToFirst()) {
return String.valueOf(cursor.getLong(0));
…
}
SqliteWrapper会调用到ContentResolver,ContentResolver则最终调用到MmsSmsProvider,
在MmsSmsProvider.query里面,也是通过URI match找到操作码URI_THREAD_ID,根据参数cellbroadcast,使用getCBThreadId给CB查找或创建其ThreadId。
Threads表内容如下所示,里面的关键字段有_id ,recipient_ids,要想找到合适的ThreadId,只有通过recipient_ids来查询,
所以在getCBThreadId方法里面,通过getAddressIds从canonical_addresses表里面找到和号码匹配的recipient_ids列表,再到Threads表里面找到recipient_ids相同和type是小区广播的_id,即是我们需要的ThreadId,使用的查询语句如下。
String queryString = "SELECT _id FROM threads " + "WHERE type=" + EncapsulatedTelephony.Threads.CELL_BROADCAST_THREAD + " AND recipient_ids=?";
如果没有满足条件的ThreadId,调用insertThread()创建一个新的thread。
在Message列表界面通过按钮新建短信时,执行createNewMessage(),通过createIntent传递一个threadId=0的ID过去,
private void createNewMessage() {
startActivity(ComposeMessageActivity.createIntent(this, 0));
}
public static Intent createIntent(Context context, long threadId) {
Intent intent = new Intent(context, ComposeMessageActivity.class);
if (threadId > 0) {
intent.setData(Conversation.getUri(threadId));
}
return intent;
}
因为threadId为0,在会话创建中会获取或创建一个threadId,具体过程和下节相同。
在联系人的详情界面,点击发送短信,因为有号码存在,所以在ComposeMessageActivity.Oncreate方法里面会去查询或创建一个threadId。
onCreate的initActivityState里,有这么一段代码是获取会话实例的,mConversation.get通过号码获取到会话实例,
Uri intentData = intent.getData();
/// M: Code analyze 034, If intent is SEND,just create a new empty thread,
/// otherwise Conversation.get() will throw exception.
String action = intent.getAction();
if (intentData != null && (TextUtils.isEmpty(action) ||
!action.equals(Intent.ACTION_SEND))) {
/// M: group-contact send message
// try to get a conversation based on the data URI passed to our intent.
if (intentData.getPathSegments().size() < 2) {
mConversation = mConversation.get(getApplicationContext(),ContactList.getByNumbers(
getStringForMultipleRecipients(Conversation.getRecipients(intentData)),
false /* don't block */, true /* replace number */),false);
} else {
mConversation = Conversation.get(getApplicationContext(), intentData, false);
}
/// @}
mWorkingMessage.setText(getBody(intentData));
Conversation get代码如下,如果号码为空则新建会话,如果缓冲里存储了对应号码的会话,则使用缓冲里的数据,如果条件都不满足,则需要去数据库查询到ID,并新建会话,之后将新会话存放到缓冲。
public static Conversation get(Context context, ContactList recipients, boolean allowQuery) {
if (recipients.size() < 1) {
return createNew(context);
}
Conversation conv = Cache.get(recipients);
if (conv != null) {
return conv;
}
long threadId = getOrCreateThreadId(context, recipients);
conv = new Conversation(context, threadId, allowQuery);
。。。
Cache.put(conv);
}
getOrCreateThreadId用来获取ThreadId,由Threads.getOrCreateThreadId实现,这个方法在Telephony.java里面。
getOrCreateThreadId类似前面提到的小区广播的实现过程,先封装一个"content://mms-sms/threadID"的URI,但只使用一个参数recipient,
在MmsSmsProvider.query里面,也是通过URI match找到操作码URI_THREAD_ID,,使用getThreadId查找或创建其ThreadId,getAddressIds获取到匹配的recipientIds,再取Threads表里查询,如果查询到,返回游标,如果没有记录,则用insertThread向Threads里插入记录,再查询到ThreadId。
private synchronized Cursor getThreadId(List<String> recipients) {
Set<Long> addressIds = getAddressIds(recipients);
。。。
recipientIds = Long.toString(addressId);
。。。
String[] selectionArgs = new String[] { recipientIds };
SQLiteDatabase db = mOpenHelper.getReadableDatabase();
db.beginTransaction();
Cursor cursor = null;
try {
// Find the thread with the given recipients
cursor = db.rawQuery(THREAD_QUERY, selectionArgs);
if (cursor.getCount() == 0) {
// No thread with those recipients exists, so create the thread.
cursor.close();
Log.d(LOG_TAG, "getThreadId: create new thread_id for recipients " + recipients);
insertThread(recipientIds, recipients);
// The thread was just created, now find it and return it.
cursor = db.rawQuery(THREAD_QUERY, selectionArgs);
。。。
}
3. ConversationListItem
3.1 ConversationListItem
3.1.1 Layout分析
如前所述,短信记录的每条信息显示是使用conversation_list_item.xml布局的,在本例中,其直观显示是如下形式的一条短信记录,
conversation_list_item.xml文件如下,它是一个ConversationListItem类型的布局,
(需要注意不同屏幕使用这个文件的位置,本例中在packages\apps\Mms\res\layout-mdpi下面,不是packages\apps\Mms\res\layout)
conversation_list_item.xml
<?xml version="1.0" encoding="utf-8"?>
<com.android.mms.ui.ConversationListItem xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="?android:attr/listPreferredItemHeight"
android:background="?android:attr/activatedBackgroundIndicator" >
<CheckBox
android:id="@+id/select_box"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_centerVertical="true"
android:clickable="false"
android:focusable="false"
android:focusableInTouchMode="false" />
<com.android.mms.ui.MmsQuickContactBadge
android:id="@+id/avatar"
android:visibility="gone"
android:layout_centerVertical="true"
android:layout_toEndOf="@id/select_box"
style="?android:attr/quickContactBadgeStyleWindowLarge" />
<ImageView
android:id="@+id/presence"
android:visibility="invisible"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_alignParentTop="true"
android:src="@drawable/ic_unread_label"/>
<LinearLayout
android:id="@+id/iconlist"
android:layout_alignParentTop="true"
android:layout_alignParentEnd="true"
android:layout_marginTop="7dip"
android:layout_marginEnd="10dip"
android:orientation="horizontal"
android:layout_height="wrap_content"
android:layout_width="wrap_content" >
<ImageView android:id="@+id/draft"
android:visibility="gone"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:src="@drawable/ic_draft" />
<ImageView android:id="@+id/error"
android:paddingStart="6dp"
android:visibility="gone"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:src="@drawable/ic_list_alert_sms_failed" />
<ImageView android:id="@+id/attachment"
android:paddingStart="6dp"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:visibility="gone"
android:src="@drawable/ic_attachment_universal_small" />
<ImageView android:id="@+id/mute"
android:paddingStart="6dp"
android:visibility="gone"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:src="@drawable/ipmsg_silent" />
</LinearLayout>
<RelativeLayout
android:layout_alignParentTop="true"
android:layout_marginTop="4dip"
android:layout_marginStart="12dip"
android:layout_toEndOf="@id/avatar"
android:layout_toStartOf="@id/iconlist"
android:gravity="center_vertical"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView android:id="@+id/unread"
android:background="@drawable/ipmsg_message_box"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:textSize="12sp"
android:textColor="@color/text_color_unread"
android:singleLine="true"
android:layout_alignParentEnd="true" />
<RelativeLayout android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:layout_toStartOf="@id/unread">
<TextView android:id="@+id/draft_and_smgcount"
android:textAppearance="?android:attr/textAppearanceMedium"
android:layout_alignParentEnd="true"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="6dip"
android:singleLine="true" />
<com.android.mms.util.AlwaysMarqueeTextView android:id="@+id/from"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceMedium"
android:singleLine="true"
android:layout_alignParentStart="true"
android:layout_toStartOf="@id/draft_and_smgcount"
android:layout_marginEnd="6dip"
android:ellipsize="marquee" />
</RelativeLayout>
</RelativeLayout>
<TextView android:id="@+id/date"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:textSize= "12sp"
android:textColor="@color/dlg_text_counter_color"
android:layout_alignParentBottom="true"
android:layout_alignParentEnd="true"
android:layout_marginEnd="10dip"
android:layout_marginBottom="4dip"
android:gravity="bottom" />
<TextView android:id="@+id/subject"
android:layout_height="wrap_content"
android:layout_width="fill_parent"
android:textSize= "12sp"
android:layout_alignParentBottom="true"
android:layout_toEndOf="@id/avatar"
android:layout_toStartOf="@id/date"
android:layout_marginStart="12dip"
android:layout_marginBottom="4dip"
android:gravity="bottom"
android:textColor="@color/dlg_text_counter_color"
android:lines="2"
android:ellipsize="end" />
</com.android.mms.ui.ConversationListItem>
其对应的布局图形如下,图中左边是layout组成,右上是layout布局表现,右下是对layout里面的子view编号,便于理解。
分析xml文件,并对照图例可以看出,
ConversationListItem是整个item布局类,表现形式见下图左边,它指定了高度
android:layout_height="?android:attr/listPreferredItemHeight"
这个布局由6个同级view组成,见下图中间及右上,其中
checkbox供选择使用,见右下数字0,alignParentStart表示它在开头位置,
android:layout_alignParentStart="true"
MmsQuickContactBadge做头像显示,见右下数字1,
ImageView做未读标识等用途,见右下数字2,通过下面的属性将它放置在最右上的位置
android:layout_alignParentEnd="true"
android:layout_alignParentTop="true"
LinearLayout是个组合视图,见右下数字3和iconlist小图,它有四个子ImageView表示短信的信息状态,具体名字见iconlist小图。注意四个子ImageView表示同时最多只能显示四个图标,不是表示短信只有这四种状态。通过下面属性,将它放置在MmsQuickContactBadge后面。
android:orientation="horizontal"
<iconlist>
RelativeLayout是个组合视图,见右下数字4和Text Title小图,包含3个TextView,用来显示Title和消息数目、未读消息数目。在RelativeLayout,通过下面的属性设置,将RelativeLayout(index4的view)和上面的LinearLayout(index3的view)并行放置在同一行。
android:layout_toEndOf="@id/avatar"
android:layout_toStartOf="@id/iconlist"
TextView显示日期,见右下数字5,用下面2个属性表示它的位置在右下,
android:layout_alignParentBottom="true"
android:layout_alignParentEnd="true"
TextView 显示短线截取的部分内容,见右下数字6,用下面3个属性表明了它的位置。
android:layout_alignParentBottom="true"
android:layout_toEndOf="@id/avatar"
android:layout_toStartOf="@id/date"
<Text Content>
3.1.2 代码分析
3.1.2.1 ConversationListItem实例创建
ConversationListItem实例创建栈如下,因为有缓存使用,不一定每次进入列表都会创建这个类实例,所以不一定能调试到,要在此设置断点调试,有一个小技巧,就是将mms进程终止掉,让其自动重启(可通过发送短信或cb的方法加快重启过程),这样就可以进行调试。
ConversationListItem.<init>(Context, AttributeSet) line: 130
Constructor.constructNative(Object[], Class, Class[], int, boolean) line: not available [native method]
Constructor.newInstance(Object...) line: 423
PhoneLayoutInflater(LayoutInflater).createView(String, String, AttributeSet) line: 594
PhoneLayoutInflater(LayoutInflater).createViewFromTag(View, String, AttributeSet) line: 696
PhoneLayoutInflater(LayoutInflater).inflate(XmlPullParser, ViewGroup, boolean) line: 469
PhoneLayoutInflater(LayoutInflater).inflate(int, ViewGroup, boolean) line: 397
ConversationListAdapter.newView(Context, Cursor, ViewGroup) line: 103
ConversationListAdapter(CursorAdapter).getView(int, View, ViewGroup) line: 250
ListView(AbsListView).obtainView(int, boolean[]) line: 2338
ListView.makeAndAddView(int, int, boolean, int, boolean) line: 1812
ListView.fillDown(int, int) line: 698
ListView.fillFromTop(int) line: 759
ListView.layoutChildren() line: 1631
ListView(AbsListView).onLayout(boolean, int, int, int, int) line: 2149
ConversationListItem实例创建的主要过程分析如下,在getView来获取view的时候,因为缓冲没有数据,会使用newView新建一个view,
public View getView(int position, View convertView, ViewGroup parent) {
View v;
if (convertView == null) {
v = newView(mContext, mCursor, parent);
} else {
v = convertView;
}
bindView(v, mContext, mCursor);
return v;
}
newView实际的过程是inflate一个布局文件,这个布局文件就是我们分析的layout,它是ConversationListItem形式的,我们知道,在inflate创建布局文件时,会根据节点创建view实例,所以会使用类的构造类Constructor来创建ConversationListItem实例。
public View newView(Context context, Cursor cursor, ViewGroup parent) {
if (LOCAL_LOGV) Log.v(TAG, "inflating new view");
return mFactory.inflate(R.layout.conversation_list_item, parent, false);
}
Checkbox的主要作用是用来勾选短信列表的某一个item,并进行删除操作的。
对于checkbox,在ConversationListItem里面定义了一个引用mSelectBox,
private CheckBox mSelectBox;
并在onFinishInflate里面初始化它,关联到Layout里面的select_box,
mSelectBox = (CheckBox) findViewById(R.id.select_box);
当列表项被点击时,执行ConversationList.onListItemClick(ListView, View, int, long)这个方法,在里面添加下面代码,当处于删除模式时,如下,将Conversation设置为check状态,然后更新界面,
if (mListAdapter != null && mDeleteMode) {
if (conv.isChecked()) {
conv.setIsChecked(false);
} else {
conv.setIsChecked(true);
}
mListAdapter.notifyDataSetChanged();
updateSelectView();
return;
}
更新界面时,执行ConversationListItem.bind(Context, Conversation) ,每加载一个item项都会执行一次bind,在这里,会根据mSelectMode 决定checkbox是否显示,根据Conversation的状态决定是否选中。
所以实际上,不是我们主观上看到的那样由checkbox决定了当前项目是否选中,而是当前项选中后决定checkbox的显示状态。
if (mSelectMode) {
mSelectBox.setVisibility(View.VISIBLE);
mAvatarView.setClickable(false);
} else {
mSelectBox.setVisibility(View.GONE);
mAvatarView.setClickable(true);
}
if (mConversation.isChecked()) {
mSelectBox.setChecked(true);
} else {
mSelectBox.setChecked(false);
}
上面就是checkbox在代码里的处理过程。
R.id.avatar用来显示发件人头像,定义如下,MmsQuickContactBadge继承自QuickContactBadge,再往上是ImageView,一个通用的视图组件。
private MmsQuickContactBadge mAvatarView;
其实例获取在ConversationListItem的onFinishInflate里面,
mAvatarView = (MmsQuickContactBadge) findViewById(R.id.avatar);
如果希望AvatarView能够被点击,相应事件,则可以看到在bind有如下代码,表示在删除模式下不能被点击,在列表模式下可以被点击。
if (mSelectMode) {
mAvatarView.setClickable(false);
} else {
mAvatarView.setClickable(true);
}
AvatarView被点击之后,执行的调用栈如下,可见是由View直接调用了MmsQuickContactBadge类的onClick,并不是ConversationList或ConversationListItem里做了onClick的相关处理。
MmsQuickContactBadge(QuickContactBadge).onClick(View) line: 217
MmsQuickContactBadge.onClick(View) line: 75
MmsQuickContactBadge(View).performClick() line: 4212
View$PerformClick.run() line: 17476
Handler.handleCallback(Message) line: 800
ViewRootImpl$ViewRootHandler(Handler).dispatchMessage(Message) line: 100
QuickContactBadge类的onClick代码如下,
@Override
public void onClick(View v) {
if (mContactUri != null) {
QuickContact.showQuickContact(getContext(), QuickContactBadge.this, mContactUri,
QuickContact.MODE_LARGE, mExcludeMimes);
} else if (mContactEmail != null) {
mQueryHandler.startQuery(TOKEN_EMAIL_LOOKUP_AND_TRIGGER, mContactEmail,
Uri.withAppendedPath(Email.CONTENT_LOOKUP_URI, Uri.encode(mContactEmail)),
EMAIL_LOOKUP_PROJECTION, null, null, null);
} else if (mContactPhone != null) {
mQueryHandler.startQuery(TOKEN_PHONE_LOOKUP_AND_TRIGGER, mContactPhone,
Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI, mContactPhone),
PHONE_LOOKUP_PROJECTION, null, null, null);
} else {
// If a contact hasn't been assigned, don't react to click.
return;
}
}
我们当前的代码走mContactPhone路径,mContactPhone是在assignContactFromPhone里面赋值的,assignContactFromPhone则是通过ConversationListItem的updateAvatarView调用的(传入的number就赋值给mContactPhone),
mAvatarView.assignContactFromPhone(number, true);
updateAvatarView可以被updateFromView和bind调用,这两个方法我们在前面有过描述。所以从整个过程来看,我们可以看出avatar被点击和其需要的数据来源。
3.2 绘制分析
3.2.1 Message列表创建
如ConversationList的layout描述,Messag列表主要是由list呈现,而list的item项是由ConversationListItem布局的,我们这里讨论每个item项的创建以及整个list的创建。
在使用Cache时,ConversationListItem的创建过程如下,
ConversationListItem.bind(Context, Conversation) line: 480
ConversationListAdapter.bindView(View, Context, Cursor) line: 77
ConversationListAdapter(CursorAdapter).getView(int, View, ViewGroup) line: 250
ListView(AbsListView).obtainView(int, boolean[]) line: 2186
ListView.makeAndAddView(int, int, boolean, int, boolean) line: 1877
ListView.fillSpecific(int, int) line: 1356
ListView.layoutChildren() line: 1671
ListView(AbsListView).onLayout(boolean, int, int, int, int) line: 2037
ListView(View).layout(int, int, int, int) line: 14118
ListView(ViewGroup).layout(int, int, int, int) line: 4467
RelativeLayout.onLayout(boolean, int, int, int, int) line: 1021
从上往下分析该栈:
1)ConversationListItem.bind
ConversationListItem.bind的代码比较多,仔细分析,实际上逻辑比较简单,主要是设置每个view的内容和状态,比如说设置日期的代码如下,
mDateView.setVisibility(VISIBLE);
if(ConversationList.sConversationListOption ==
ConversationList.OPTION_CONVERSATION_LIST_IMPORTANT &&
!conversation.hasUnreadMessages()) {
mDateView.setText(MessageUtils.formatTimeStampStringExtend(context,
conversation.getImpDate()));
}
其数据来源于conversation,我们要进一步分析conversation是怎么传递进来的,以及为什么要调用bind。
2)ConversationListAdapter
ConversationListAdapter 是ConversationList的layout里的list对应的Adapter,它继承自MessageCursorAdapter-CursorAdapter-baseAdapter, 其方法bindView在本例中执行的主要代码如下,其中headerview是一个ConversationListItem引用,是调用方法传递过来的,conversation是根据游标新建的,最后调用bind,所以该方法的作用就是获取到conversation,并用准备将其中的数据赋值到view。
public void bindView(View view, Context context, Cursor cursor) {
ConversationListItem headerView = (ConversationListItem) view;
Conversation conv;
if (!mIsScrolling) {
Conversation.setNeedCacheConv(false);
conv = Conversation.from(context, cursor);
Conversation.setNeedCacheConv(true);
if (mSubjectSingleLine) {
headerView.setSubjectSingleLineMode(true);
}
if (conv != null) {
conv.setIsChecked(sSelectedTheadsId.contains(conv.getThreadId()));
}
headerView.bind(context, conv);
}
}
(CursorAdapter).getView则负责获取到view实例和游标实例。
view的获取有两种方式,一种是调用者传递过来以前缓存的,一种是新创建一个,在ConversationListItem实例创建中有讲到如何创建。
游标实例则在Adapter创建时创建。
3)ListView
在ListView部分,我们从layoutChildren向上分析,向下部分设计到太多ListView的实现机制问题,暂不讨论。
layoutChildren实现ListView子窗口布局,实际上就是list在layout所占部分。
本例从launcher进入Message列表,执行代码路径如下,mSyncPosition是当前item索引值,mSpecificTop是当前layout相对Top的位置。
layoutChildren:
case LAYOUT_SYNC:
sel = fillSpecific(mSyncPosition, mSpecificTop);
fillSpecific在知道位置绘制指定项的item数据,然后上下绘制其他的item。它先通过makeAndAddView完成当前item的创建并添加到layout,fillDown、fillUp则根据layout大小,上下绘制item完成整个list的绘制,使用的方法也是makeAndAddView,adjustViewsUpOrDown调整item位置。
/**
* Put a specific item at a specific location on the screen and then build
* up and down from there.
*
* @param position The reference view to use as the starting point
* @param top Pixel offset from the top of this view to the top of the
* reference view.
*
* @return The selected view, or null if the selected view is outside the
* visible area.
*/
private View fillSpecific(int position, int top) {
boolean tempIsSelected = position == mSelectedPosition;
View temp = makeAndAddView(position, top, true, mListPadding.left, tempIsSelected);
…
below = fillDown(position + 1, temp.getBottom() + dividerHeight);
// This will correct for the bottom of the last view not touching the bottom of the list
adjustViewsUpOrDown();
above = fillUp(position - 1, temp.getTop() - dividerHeight);
int childCount = getChildCount();
if (childCount > 0) {
correctTooLow(childCount);
}
}
顾名思义,makeAndAddView就是新建一个view,并将它添加到list里面。obtainView负责创建view,setupChild负责将view添加到layout,并设置好view的位置和相关属性。
private View makeAndAddView(int position, int y, boolean flow, int childrenLeft,
boolean selected) {
View child;
child = obtainView(position, mIsScrap);
// This needs to be positioned and measured
setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]);
return child;
}
obtainView将对应位置的view和数据关联起来,并返回view。它通过mRecycler获取到缓存的view,再通过mAdapter.getView将数据填充到view,mAdapter.getView即是我们前面分析到的ConversationListAdapter(CursorAdapter).getView。
至此,Message列表的创建过程就明了了。
3.3 功能分析
3.3.1 Message列表更新
当数据库内容发生变化(包括新短信、删除、发送状态改变等),就会触发onContentChanged,其调用栈如下:
ConversationList$2.onContentChanged(ConversationListAdapter) line: 474
ConversationListAdapter.onContentChanged() line: 118
CursorAdapter$ChangeObserver.onChange(boolean) line: 463
CursorAdapter$ChangeObserver(ContentObserver).onChange(boolean, Uri) line: 129
ContentObserver$NotificationRunnable.run() line: 180
Handler.handleCallback(Message) line: 808
Handler.dispatchMessage(Message) line: 103
Looper.loop() line: 193
从下往上分析,
1)ContentObserver
我们已经很熟悉handleCallback处理的是Post的消息,消息一般是runable,所以可以看到ContentObserver. dispatchChange有post这一消息的过程(dispatchChange的触发过程比较复杂,我们将在其他章节介绍,本例中当数据库内容变化时会触发这个流程)。
public final void dispatchChange(boolean selfChange, Uri uri) {
if (mHandler == null) {
onChange(selfChange, uri);
} else {
mHandler.post(new NotificationRunnable(selfChange, uri));
}
}
然后这个消息会运行其内部类NotificationRunnable的run方法,
public void run() {
ContentObserver.this.onChange(mSelfChange, mUri);
}
This指向的是ContentObserver的实例ChangeObserver,它是CursorAdapter的内部类,也是ContentObserver的子类,由于ConversationListAdapter通过CursorWrapper向ContentObservable注册了ContentObserver,所以当dispatchChange事件时,通过轮询,最终将事件传递给ChangeObserver。
而后,ChangeObserver调用封装类CursorAdapter的onContentChanged,将事件上传。
private class ChangeObserver extends ContentObserver {
public void onChange(boolean selfChange) {
onContentChanged();
}
}
2)onContentChanged
onContentChanged是CursorAdapter的一个方法,主要功能是当数据库内容更新后,重新使用游标查询数据。但这个方法被ConversationListAdapter重载,它给自己的客户提供一个借口类OnContentChangedListener,让客户去实现这个借口类,自己则调用接口类的onContentChanged方法执行客户定义的功能。
protected void onContentChanged() {
if (mCursor != null && !mCursor.isClosed()) {
if (mOnContentChangedListener != null) {
mOnContentChangedListener.onContentChanged(this);
}
}
}
3)ConversationList
ConversationList就是ConversationListAdapter的客户,它实现了接口类,如下,主要是调用startAsyncQuery进行数据的查询。
private final ConversationListAdapter.OnContentChangedListener mContentChangedListener =
new ConversationListAdapter.OnContentChangedListener() {
@Override
public void onContentChanged(ConversationListAdapter adapter) {
if (mIsInActivity) {
mNeedQuery = true;
startAsyncQuery();
}
}
};
并且ConversationList在其onStart向ConversationListAdapter注册了这个接口类监听器:
mListAdapter.setOnContentChangedListener(mContentChangedListener);
所以,当数据库数据改变时,根据上面相关调用流程,完成了Message列表的刷新功能。
BTW,附上游标改变后注册ContentObserver的调用栈:
ContentResolver$CursorWrapperInner(CursorWrapper).registerContentObserver(ContentObserver) line: 178
ConversationListAdapter(CursorAdapter).swapCursor(Cursor) line: 340
ConversationListAdapter(CursorAdapter).changeCursor(Cursor) line: 313
ConversationList$ThreadListQueryHandler.onQueryComplete(int, Object, Cursor) line: 1896
ConversationList$ThreadListQueryHandler(AsyncQueryHandler).handleMessage(Message) line: 344
ConversationList$ThreadListQueryHandler(Handler).dispatchMessage(Message) line: 110
Looper.loop() line: 193
3.3.2 Message列表定位
因为ConversationList是ListActivity,所以可以使用getListView获取ListView,再使用ListView的setSelectionFromTop,将列表显示开始位置指定到特定位置。
需要注意2个问题,
1)
在ConversationList会记录上次浏览列表的起始位置,所以不是随意就能设置好列表的显示位置,如onQueryComplete通过下面代码控制了什么时候可以设置这个起始位置,我们需要根据mSavedFirstVisiblePosition的值和相关逻辑去处理。
if (mSavedFirstVisiblePosition != AdapterView.INVALID_POSITION) {
// Restore the list to its previous position.
getListView().setSelectionFromTop(mSavedFirstVisiblePosition,
mSavedFirstItemOffset);
mSavedFirstVisiblePosition = AdapterView.INVALID_POSITION;
}
结合列表的创建过程,我们知道,当同步绘制列表时,是从mSyncPosition开始的,
fillSpecific(mSyncPosition, mSpecificTop);
而mSyncPosition正是setSelectionFromTop里传递进去的,所以列表起始位置只能通过setSelectionFromTop来设置。
public void setSelectionFromTop(int position, int y) {
…
mSyncPosition = position;
…
}
2)
游标的更新,当数据库数据变化后,需要更新游标,才能获取到正确的值。下面是onQueryComplete完成后,用changeCursor替换掉旧游标的过程,防止游标数据过期。
ConversationListAdapter(CursorAdapter).swapCursor(Cursor) line: 340
ConversationListAdapter(CursorAdapter).changeCursor(Cursor) line: 313
ConversationList$ThreadListQueryHandler.onQueryComplete(int, Object, Cursor) line: 1896
ConversationList$ThreadListQueryHandler(AsyncQueryHandler).handleMessage(Message) line: 344
ConversationList$ThreadListQueryHandler(Handler).dispatchMessage(Message) line: 110
Looper.loop() line: 193
在列表创建时,CursorAdapter. getView的游标最初是在CursorAdapte创建时传入的,之后可以通过swapCursor的方法更新。
目 录
2. ConversationList
2.1.2.1 ConversationListAdapter
2.2.1 列表项click
2.2.2 Long Press click to Delete
2.3.1 会话ID创建
3. ConversationListItem
3.1.2.1 ConversationListItem实例创建
1. 概要
本文主要分析短信列表界面的主要控件的界面显示和处理逻辑。
2. ConversationList
2.1 ConversationList
2.1.1 Layout分析
ConversationList对应的layout是conversation_list_screen,它主要分3部分,第一部分是做全选的控制,第二部分是list列表,第三部分是列表为空的布局。具体的表现形式如下,布局文件就不再展开了。其中list列表将在另外的章节分析。
2.1.2 代码分析
2.1.2.1 ConversationListAdapter
ConversationListAdapter是ConversationListItem的数据适配器,在ConversationList.onCreate里创建,它通过bindView将会话信息绑定到item的layout上。
其创建过程如下,
private void initListAdapter() {
mListAdapter = new ConversationListAdapter(this, null);
/** M: now this code is useless and will lead to a JE, comment it.
* listener is set in onStart
*/
//mListAdapter.setOnContentChangedListener(mContentChangedListener);
setListAdapter(mListAdapter);
getListView().setRecyclerListener(mListAdapter);
}
因为ConversationList继承自ListActivity,setListAdapter将Adapter实例设置给ListActivity,这里会调用ensureList,它会加载一个布局,确保有view能用来对应Adapter。
public void setListAdapter(ListAdapter adapter) {
synchronized (this) {
ensureList();
mAdapter = adapter;
mList.setAdapter(adapter);
}
}
private void ensureList() {
if (mList != null) {
return;
}
setContentView(com.android.internal.R.layout.list_content_simple);
}
而实际上,mList已经在 onContentChanged被初始化为Layout里对应的ListView,
mList = (ListView)findViewById(com.android.internal.R.id.list);
这个ListView就是Message列表的容器了。
2.2 功能分析
2.2.1 列表项click
单击列表项的调用栈如下:
ConversationList.onListItemClick(ListView, View, int, long) line: 1086
ListActivity$2.onItemClick(AdapterView, View, int, long) line: 319
ListView(AdapterView).performItemClick(View, int, long) line: 298
ListView(AbsListView).performItemClick(View, int, long) line: 1128
AbsListView$PerformClick.run() line: 2815
AbsListView$1.run() line: 3574
Handler.handleCallback(Message) line: 800
ViewRootImpl$ViewRootHandler(Handler).dispatchMessage(Message) line: 100
2.2.2 Long Press click to Delete
长按item删除会话流程如下:
长按出现选择菜单,
mConvListOnCreateContextMenuListener
onCreateContextMenu
点击删除
ConversationList.confirmDeleteThreads(Collection, AsyncQueryHandler) line:
ConversationList.confirmDeleteThread(long, AsyncQueryHandler) line: 1320
ConversationList.onContextItemSelected(MenuItem) line: 1247
ConversationList(Activity).onMenuItemSelected(int, MenuItem) line: 2584
PhoneWindow$DialogMenuCallback.onMenuItemSelected(MenuBuilder, MenuItem) line:
开始删除之前,要做一个后台查询,看是否有lock住的消息,Conversation.startQueryHaveLockedMessages,
之后是后台查询handler.startQuery:
AsyncQueryHandler
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);
msg.arg1 = EVENT_ARG_QUERY;
WorkerArgs args = new WorkerArgs();
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);
}
这个查询过程将消息发给mWorkerThreadHandler,这是异步查询AsyncQueryHandler自己的handler,它封装完消息EVENT_ARG_QUERY后,再发送出去,
Message reply = args.handler.obtainMessage(token);
reply.obj = args;
reply.arg1 = msg.arg1;
reply.sendToTarget();
这个消息的接收对象的设置见args.handler = this; ,在本例中,这个handler是ThreadListQueryHandler。
数据库查询完成后执行onQueryComplete(HAVE_LOCKED_MESSAGES_TOKEN),
ConversationList$ThreadListQueryHandler.onQueryComplete(int, Object, Cursor) line: 1610
ConversationList$ThreadListQueryHandler(AsyncQueryHandler).handleMessage(Message) line: 344
ConversationList$ThreadListQueryHandler(Handler).dispatchMessage(Message) line: 107
Looper.loop() line: 194
之后会给出一个确认删除对话框confirmDeleteThreadDialog
在弹出的确认对话框中,选择删除,执行listener函数。
ConversationList$DeleteThreadListener.onClick(DialogInterface, int) line: 1484
AlertController$ButtonHandler.handleMessage(Message) line: 176
之后开始删除Conversation.startDelete
最后onDeleteComplete
响应函数会触发一系列其他的数据库操作,如查询list,查询unread等,
AsyncQueryHandler$WorkerHandler.handleMessage(Message) line: 120
AsyncQueryHandler$WorkerHandler(Handler).dispatchMessage(Message) line: 107
【Tips】
它的threadIds是在点击删除功能时,onContextItemSelected设置的,和长按的时候在onCreateContextMenu里选择的有时间差,不一定是同一个,在会话列表更新的时候,会有影响,所以会误删。
所以在onCreateContextMenu里要记录thread id。
2.3 数据操作
2.3.1 会话ID创建
Message列表是以会话为item进行显示的,会话是以为ID记录的,我们分析一下各种会话ID是如何创建的,
应用在CBMessageReceiverService.java收到小区广播后,使用handleCBMessageReceived方法进行处理,其中insertMessage是用来存储CB的。
insertMessage会调用CbProvider的insert方法
public Uri insert(Uri url, ContentValues initialValues) {
Uri result = null;
// TODO Check Permission
// checkPermission();
SQLiteDatabase db = mOpenHelper.getWritableDatabase();
SQLiteDatabase dbmmssms = mMmsSmsOpenHelper.getWritableDatabase();
int match = URI_MATCHER.match(url);
ContentValues values;
long rowID;
String table = null;
Log.d(TAG, " insert match = "+match);
switch (match) {
case URL_MESSAGES:
//table = CbDatabaseHelper.CBMESSAGE_TABLE;
table = MmsSmsDatabaseHelper.TABLE_CELLBROADCAST;
values = internalInsertMessages(initialValues);
rowID = dbmmssms.insert(table, null, values);
Log.d(TAG, "insert to cellbroadcast " + values);
if (rowID > 0) {
result = Uri.parse("content://messages/" + rowID);
notifyChange();
return result;
}
break;
…
}
在这里,我们重点关注一下CB Message的ThreadId是怎样获取的,在internalInsertMessages里面,显然threadId不是我们主动填充进去的,所以是通过getOrCreateThreadId获得的。
private ContentValues internalInsertMessages(ContentValues initialValues) {
ContentValues values;
if (initialValues != null) {
values = new ContentValues(initialValues);
} else {
values = new ContentValues();
}
Long threadId = values.getAsLong(EncapsulatedTelephony.SmsCb.THREAD_ID);
String address = values.getAsString(EncapsulatedTelephony.SmsCb.CHANNEL_ID);
if (((threadId == null) || (threadId == 0)) && (address != null)) {
values.put(EncapsulatedTelephony.SmsCb.THREAD_ID,
getOrCreateThreadId(address));
}
…
}
getOrCreateThreadId代码如下,它封装一个URI地址,包含了threadID和参数recipient、cellbroadcast,并使用SqliteWrapper.query进行查询,
private String getOrCreateThreadId(String address) {
//Uri.Builder uriBuilder = THREAD_ID_URI.buildUpon();
Uri.Builder uriBuilder = (Uri.parse("content://mms-sms/threadID")).buildUpon();
uriBuilder.appendQueryParameter("recipient", address);
uriBuilder.appendQueryParameter("cellbroadcast", address);
Uri uri = uriBuilder.build();
// TODO need replace with helper interface.
Cursor cursor = null;
long token = Binder.clearCallingIdentity();
try {
cursor = SqliteWrapper.query(this.getContext(), this
.getContext().getContentResolver(), uri, ID_PROJECTION, null,
null, null);
…
if (cursor.moveToFirst()) {
return String.valueOf(cursor.getLong(0));
…
}
SqliteWrapper会调用到ContentResolver,ContentResolver则最终调用到MmsSmsProvider,
在MmsSmsProvider.query里面,也是通过URI match找到操作码URI_THREAD_ID,根据参数cellbroadcast,使用getCBThreadId给CB查找或创建其ThreadId。
Threads表内容如下所示,里面的关键字段有_id ,recipient_ids,要想找到合适的ThreadId,只有通过recipient_ids来查询,
所以在getCBThreadId方法里面,通过getAddressIds从canonical_addresses表里面找到和号码匹配的recipient_ids列表,再到Threads表里面找到recipient_ids相同和type是小区广播的_id,即是我们需要的ThreadId,使用的查询语句如下。
String queryString = "SELECT _id FROM threads " + "WHERE type=" + EncapsulatedTelephony.Threads.CELL_BROADCAST_THREAD + " AND recipient_ids=?";
如果没有满足条件的ThreadId,调用insertThread()创建一个新的thread。
在Message列表界面通过按钮新建短信时,执行createNewMessage(),通过createIntent传递一个threadId=0的ID过去,
private void createNewMessage() {
startActivity(ComposeMessageActivity.createIntent(this, 0));
}
public static Intent createIntent(Context context, long threadId) {
Intent intent = new Intent(context, ComposeMessageActivity.class);
if (threadId > 0) {
intent.setData(Conversation.getUri(threadId));
}
return intent;
}
因为threadId为0,在会话创建中会获取或创建一个threadId,具体过程和下节相同。
在联系人的详情界面,点击发送短信,因为有号码存在,所以在ComposeMessageActivity.Oncreate方法里面会去查询或创建一个threadId。
onCreate的initActivityState里,有这么一段代码是获取会话实例的,mConversation.get通过号码获取到会话实例,
Uri intentData = intent.getData();
/// M: Code analyze 034, If intent is SEND,just create a new empty thread,
/// otherwise Conversation.get() will throw exception.
String action = intent.getAction();
if (intentData != null && (TextUtils.isEmpty(action) ||
!action.equals(Intent.ACTION_SEND))) {
/// M: group-contact send message
// try to get a conversation based on the data URI passed to our intent.
if (intentData.getPathSegments().size() < 2) {
mConversation = mConversation.get(getApplicationContext(),ContactList.getByNumbers(
getStringForMultipleRecipients(Conversation.getRecipients(intentData)),
false /* don't block */, true /* replace number */),false);
} else {
mConversation = Conversation.get(getApplicationContext(), intentData, false);
}
/// @}
mWorkingMessage.setText(getBody(intentData));
Conversation get代码如下,如果号码为空则新建会话,如果缓冲里存储了对应号码的会话,则使用缓冲里的数据,如果条件都不满足,则需要去数据库查询到ID,并新建会话,之后将新会话存放到缓冲。
public static Conversation get(Context context, ContactList recipients, boolean allowQuery) {
if (recipients.size() < 1) {
return createNew(context);
}
Conversation conv = Cache.get(recipients);
if (conv != null) {
return conv;
}
long threadId = getOrCreateThreadId(context, recipients);
conv = new Conversation(context, threadId, allowQuery);
。。。
Cache.put(conv);
}
getOrCreateThreadId用来获取ThreadId,由Threads.getOrCreateThreadId实现,这个方法在Telephony.java里面。
getOrCreateThreadId类似前面提到的小区广播的实现过程,先封装一个"content://mms-sms/threadID"的URI,但只使用一个参数recipient,
在MmsSmsProvider.query里面,也是通过URI match找到操作码URI_THREAD_ID,,使用getThreadId查找或创建其ThreadId,getAddressIds获取到匹配的recipientIds,再取Threads表里查询,如果查询到,返回游标,如果没有记录,则用insertThread向Threads里插入记录,再查询到ThreadId。
private synchronized Cursor getThreadId(List<String> recipients) {
Set<Long> addressIds = getAddressIds(recipients);
。。。
recipientIds = Long.toString(addressId);
。。。
String[] selectionArgs = new String[] { recipientIds };
SQLiteDatabase db = mOpenHelper.getReadableDatabase();
db.beginTransaction();
Cursor cursor = null;
try {
// Find the thread with the given recipients
cursor = db.rawQuery(THREAD_QUERY, selectionArgs);
if (cursor.getCount() == 0) {
// No thread with those recipients exists, so create the thread.
cursor.close();
Log.d(LOG_TAG, "getThreadId: create new thread_id for recipients " + recipients);
insertThread(recipientIds, recipients);
// The thread was just created, now find it and return it.
cursor = db.rawQuery(THREAD_QUERY, selectionArgs);
。。。
}
3. ConversationListItem
3.1 ConversationListItem
3.1.1 Layout分析
如前所述,短信记录的每条信息显示是使用conversation_list_item.xml布局的,在本例中,其直观显示是如下形式的一条短信记录,
conversation_list_item.xml文件如下,它是一个ConversationListItem类型的布局,
(需要注意不同屏幕使用这个文件的位置,本例中在packages\apps\Mms\res\layout-mdpi下面,不是packages\apps\Mms\res\layout)
conversation_list_item.xml
<?xml version="1.0" encoding="utf-8"?>
<com.android.mms.ui.ConversationListItem xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="?android:attr/listPreferredItemHeight"
android:background="?android:attr/activatedBackgroundIndicator" >
<CheckBox
android:id="@+id/select_box"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_centerVertical="true"
android:clickable="false"
android:focusable="false"
android:focusableInTouchMode="false" />
<com.android.mms.ui.MmsQuickContactBadge
android:id="@+id/avatar"
android:visibility="gone"
android:layout_centerVertical="true"
android:layout_toEndOf="@id/select_box"
style="?android:attr/quickContactBadgeStyleWindowLarge" />
<ImageView
android:id="@+id/presence"
android:visibility="invisible"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_alignParentTop="true"
android:src="@drawable/ic_unread_label"/>
<LinearLayout
android:id="@+id/iconlist"
android:layout_alignParentTop="true"
android:layout_alignParentEnd="true"
android:layout_marginTop="7dip"
android:layout_marginEnd="10dip"
android:orientation="horizontal"
android:layout_height="wrap_content"
android:layout_width="wrap_content" >
<ImageView android:id="@+id/draft"
android:visibility="gone"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:src="@drawable/ic_draft" />
<ImageView android:id="@+id/error"
android:paddingStart="6dp"
android:visibility="gone"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:src="@drawable/ic_list_alert_sms_failed" />
<ImageView android:id="@+id/attachment"
android:paddingStart="6dp"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:visibility="gone"
android:src="@drawable/ic_attachment_universal_small" />
<ImageView android:id="@+id/mute"
android:paddingStart="6dp"
android:visibility="gone"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:src="@drawable/ipmsg_silent" />
</LinearLayout>
<RelativeLayout
android:layout_alignParentTop="true"
android:layout_marginTop="4dip"
android:layout_marginStart="12dip"
android:layout_toEndOf="@id/avatar"
android:layout_toStartOf="@id/iconlist"
android:gravity="center_vertical"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView android:id="@+id/unread"
android:background="@drawable/ipmsg_message_box"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:textSize="12sp"
android:textColor="@color/text_color_unread"
android:singleLine="true"
android:layout_alignParentEnd="true" />
<RelativeLayout android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:layout_toStartOf="@id/unread">
<TextView android:id="@+id/draft_and_smgcount"
android:textAppearance="?android:attr/textAppearanceMedium"
android:layout_alignParentEnd="true"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="6dip"
android:singleLine="true" />
<com.android.mms.util.AlwaysMarqueeTextView android:id="@+id/from"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceMedium"
android:singleLine="true"
android:layout_alignParentStart="true"
android:layout_toStartOf="@id/draft_and_smgcount"
android:layout_marginEnd="6dip"
android:ellipsize="marquee" />
</RelativeLayout>
</RelativeLayout>
<TextView android:id="@+id/date"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:textSize= "12sp"
android:textColor="@color/dlg_text_counter_color"
android:layout_alignParentBottom="true"
android:layout_alignParentEnd="true"
android:layout_marginEnd="10dip"
android:layout_marginBottom="4dip"
android:gravity="bottom" />
<TextView android:id="@+id/subject"
android:layout_height="wrap_content"
android:layout_width="fill_parent"
android:textSize= "12sp"
android:layout_alignParentBottom="true"
android:layout_toEndOf="@id/avatar"
android:layout_toStartOf="@id/date"
android:layout_marginStart="12dip"
android:layout_marginBottom="4dip"
android:gravity="bottom"
android:textColor="@color/dlg_text_counter_color"
android:lines="2"
android:ellipsize="end" />
</com.android.mms.ui.ConversationListItem>
其对应的布局图形如下,图中左边是layout组成,右上是layout布局表现,右下是对layout里面的子view编号,便于理解。
分析xml文件,并对照图例可以看出,
ConversationListItem是整个item布局类,表现形式见下图左边,它指定了高度
android:layout_height="?android:attr/listPreferredItemHeight"
这个布局由6个同级view组成,见下图中间及右上,其中
checkbox供选择使用,见右下数字0,alignParentStart表示它在开头位置,
android:layout_alignParentStart="true"
MmsQuickContactBadge做头像显示,见右下数字1,
ImageView做未读标识等用途,见右下数字2,通过下面的属性将它放置在最右上的位置
android:layout_alignParentEnd="true"
android:layout_alignParentTop="true"
LinearLayout是个组合视图,见右下数字3和iconlist小图,它有四个子ImageView表示短信的信息状态,具体名字见iconlist小图。注意四个子ImageView表示同时最多只能显示四个图标,不是表示短信只有这四种状态。通过下面属性,将它放置在MmsQuickContactBadge后面。
android:orientation="horizontal"
<iconlist>
RelativeLayout是个组合视图,见右下数字4和Text Title小图,包含3个TextView,用来显示Title和消息数目、未读消息数目。在RelativeLayout,通过下面的属性设置,将RelativeLayout(index4的view)和上面的LinearLayout(index3的view)并行放置在同一行。
android:layout_toEndOf="@id/avatar"
android:layout_toStartOf="@id/iconlist"
TextView显示日期,见右下数字5,用下面2个属性表示它的位置在右下,
android:layout_alignParentBottom="true"
android:layout_alignParentEnd="true"
TextView 显示短线截取的部分内容,见右下数字6,用下面3个属性表明了它的位置。
android:layout_alignParentBottom="true"
android:layout_toEndOf="@id/avatar"
android:layout_toStartOf="@id/date"
<Text Content>
3.1.2 代码分析
3.1.2.1 ConversationListItem实例创建
ConversationListItem实例创建栈如下,因为有缓存使用,不一定每次进入列表都会创建这个类实例,所以不一定能调试到,要在此设置断点调试,有一个小技巧,就是将mms进程终止掉,让其自动重启(可通过发送短信或cb的方法加快重启过程),这样就可以进行调试。
ConversationListItem.<init>(Context, AttributeSet) line: 130
Constructor.constructNative(Object[], Class, Class[], int, boolean) line: not available [native method]
Constructor.newInstance(Object...) line: 423
PhoneLayoutInflater(LayoutInflater).createView(String, String, AttributeSet) line: 594
PhoneLayoutInflater(LayoutInflater).createViewFromTag(View, String, AttributeSet) line: 696
PhoneLayoutInflater(LayoutInflater).inflate(XmlPullParser, ViewGroup, boolean) line: 469
PhoneLayoutInflater(LayoutInflater).inflate(int, ViewGroup, boolean) line: 397
ConversationListAdapter.newView(Context, Cursor, ViewGroup) line: 103
ConversationListAdapter(CursorAdapter).getView(int, View, ViewGroup) line: 250
ListView(AbsListView).obtainView(int, boolean[]) line: 2338
ListView.makeAndAddView(int, int, boolean, int, boolean) line: 1812
ListView.fillDown(int, int) line: 698
ListView.fillFromTop(int) line: 759
ListView.layoutChildren() line: 1631
ListView(AbsListView).onLayout(boolean, int, int, int, int) line: 2149
ConversationListItem实例创建的主要过程分析如下,在getView来获取view的时候,因为缓冲没有数据,会使用newView新建一个view,
public View getView(int position, View convertView, ViewGroup parent) {
View v;
if (convertView == null) {
v = newView(mContext, mCursor, parent);
} else {
v = convertView;
}
bindView(v, mContext, mCursor);
return v;
}
newView实际的过程是inflate一个布局文件,这个布局文件就是我们分析的layout,它是ConversationListItem形式的,我们知道,在inflate创建布局文件时,会根据节点创建view实例,所以会使用类的构造类Constructor来创建ConversationListItem实例。
public View newView(Context context, Cursor cursor, ViewGroup parent) {
if (LOCAL_LOGV) Log.v(TAG, "inflating new view");
return mFactory.inflate(R.layout.conversation_list_item, parent, false);
}
Checkbox的主要作用是用来勾选短信列表的某一个item,并进行删除操作的。
对于checkbox,在ConversationListItem里面定义了一个引用mSelectBox,
private CheckBox mSelectBox;
并在onFinishInflate里面初始化它,关联到Layout里面的select_box,
mSelectBox = (CheckBox) findViewById(R.id.select_box);
当列表项被点击时,执行ConversationList.onListItemClick(ListView, View, int, long)这个方法,在里面添加下面代码,当处于删除模式时,如下,将Conversation设置为check状态,然后更新界面,
if (mListAdapter != null && mDeleteMode) {
if (conv.isChecked()) {
conv.setIsChecked(false);
} else {
conv.setIsChecked(true);
}
mListAdapter.notifyDataSetChanged();
updateSelectView();
return;
}
更新界面时,执行ConversationListItem.bind(Context, Conversation) ,每加载一个item项都会执行一次bind,在这里,会根据mSelectMode 决定checkbox是否显示,根据Conversation的状态决定是否选中。
所以实际上,不是我们主观上看到的那样由checkbox决定了当前项目是否选中,而是当前项选中后决定checkbox的显示状态。
if (mSelectMode) {
mSelectBox.setVisibility(View.VISIBLE);
mAvatarView.setClickable(false);
} else {
mSelectBox.setVisibility(View.GONE);
mAvatarView.setClickable(true);
}
if (mConversation.isChecked()) {
mSelectBox.setChecked(true);
} else {
mSelectBox.setChecked(false);
}
上面就是checkbox在代码里的处理过程。
R.id.avatar用来显示发件人头像,定义如下,MmsQuickContactBadge继承自QuickContactBadge,再往上是ImageView,一个通用的视图组件。
private MmsQuickContactBadge mAvatarView;
其实例获取在ConversationListItem的onFinishInflate里面,
mAvatarView = (MmsQuickContactBadge) findViewById(R.id.avatar);
如果希望AvatarView能够被点击,相应事件,则可以看到在bind有如下代码,表示在删除模式下不能被点击,在列表模式下可以被点击。
if (mSelectMode) {
mAvatarView.setClickable(false);
} else {
mAvatarView.setClickable(true);
}
AvatarView被点击之后,执行的调用栈如下,可见是由View直接调用了MmsQuickContactBadge类的onClick,并不是ConversationList或ConversationListItem里做了onClick的相关处理。
MmsQuickContactBadge(QuickContactBadge).onClick(View) line: 217
MmsQuickContactBadge.onClick(View) line: 75
MmsQuickContactBadge(View).performClick() line: 4212
View$PerformClick.run() line: 17476
Handler.handleCallback(Message) line: 800
ViewRootImpl$ViewRootHandler(Handler).dispatchMessage(Message) line: 100
QuickContactBadge类的onClick代码如下,
@Override
public void onClick(View v) {
if (mContactUri != null) {
QuickContact.showQuickContact(getContext(), QuickContactBadge.this, mContactUri,
QuickContact.MODE_LARGE, mExcludeMimes);
} else if (mContactEmail != null) {
mQueryHandler.startQuery(TOKEN_EMAIL_LOOKUP_AND_TRIGGER, mContactEmail,
Uri.withAppendedPath(Email.CONTENT_LOOKUP_URI, Uri.encode(mContactEmail)),
EMAIL_LOOKUP_PROJECTION, null, null, null);
} else if (mContactPhone != null) {
mQueryHandler.startQuery(TOKEN_PHONE_LOOKUP_AND_TRIGGER, mContactPhone,
Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI, mContactPhone),
PHONE_LOOKUP_PROJECTION, null, null, null);
} else {
// If a contact hasn't been assigned, don't react to click.
return;
}
}
我们当前的代码走mContactPhone路径,mContactPhone是在assignContactFromPhone里面赋值的,assignContactFromPhone则是通过ConversationListItem的updateAvatarView调用的(传入的number就赋值给mContactPhone),
mAvatarView.assignContactFromPhone(number, true);
updateAvatarView可以被updateFromView和bind调用,这两个方法我们在前面有过描述。所以从整个过程来看,我们可以看出avatar被点击和其需要的数据来源。
3.2 绘制分析
3.2.1 Message列表创建
如ConversationList的layout描述,Messag列表主要是由list呈现,而list的item项是由ConversationListItem布局的,我们这里讨论每个item项的创建以及整个list的创建。
在使用Cache时,ConversationListItem的创建过程如下,
ConversationListItem.bind(Context, Conversation) line: 480
ConversationListAdapter.bindView(View, Context, Cursor) line: 77
ConversationListAdapter(CursorAdapter).getView(int, View, ViewGroup) line: 250
ListView(AbsListView).obtainView(int, boolean[]) line: 2186
ListView.makeAndAddView(int, int, boolean, int, boolean) line: 1877
ListView.fillSpecific(int, int) line: 1356
ListView.layoutChildren() line: 1671
ListView(AbsListView).onLayout(boolean, int, int, int, int) line: 2037
ListView(View).layout(int, int, int, int) line: 14118
ListView(ViewGroup).layout(int, int, int, int) line: 4467
RelativeLayout.onLayout(boolean, int, int, int, int) line: 1021
从上往下分析该栈:
1)ConversationListItem.bind
ConversationListItem.bind的代码比较多,仔细分析,实际上逻辑比较简单,主要是设置每个view的内容和状态,比如说设置日期的代码如下,
mDateView.setVisibility(VISIBLE);
if(ConversationList.sConversationListOption ==
ConversationList.OPTION_CONVERSATION_LIST_IMPORTANT &&
!conversation.hasUnreadMessages()) {
mDateView.setText(MessageUtils.formatTimeStampStringExtend(context,
conversation.getImpDate()));
}
其数据来源于conversation,我们要进一步分析conversation是怎么传递进来的,以及为什么要调用bind。
2)ConversationListAdapter
ConversationListAdapter 是ConversationList的layout里的list对应的Adapter,它继承自MessageCursorAdapter-CursorAdapter-baseAdapter, 其方法bindView在本例中执行的主要代码如下,其中headerview是一个ConversationListItem引用,是调用方法传递过来的,conversation是根据游标新建的,最后调用bind,所以该方法的作用就是获取到conversation,并用准备将其中的数据赋值到view。
public void bindView(View view, Context context, Cursor cursor) {
ConversationListItem headerView = (ConversationListItem) view;
Conversation conv;
if (!mIsScrolling) {
Conversation.setNeedCacheConv(false);
conv = Conversation.from(context, cursor);
Conversation.setNeedCacheConv(true);
if (mSubjectSingleLine) {
headerView.setSubjectSingleLineMode(true);
}
if (conv != null) {
conv.setIsChecked(sSelectedTheadsId.contains(conv.getThreadId()));
}
headerView.bind(context, conv);
}
}
(CursorAdapter).getView则负责获取到view实例和游标实例。
view的获取有两种方式,一种是调用者传递过来以前缓存的,一种是新创建一个,在ConversationListItem实例创建中有讲到如何创建。
游标实例则在Adapter创建时创建。
3)ListView
在ListView部分,我们从layoutChildren向上分析,向下部分设计到太多ListView的实现机制问题,暂不讨论。
layoutChildren实现ListView子窗口布局,实际上就是list在layout所占部分。
本例从launcher进入Message列表,执行代码路径如下,mSyncPosition是当前item索引值,mSpecificTop是当前layout相对Top的位置。
layoutChildren:
case LAYOUT_SYNC:
sel = fillSpecific(mSyncPosition, mSpecificTop);
fillSpecific在知道位置绘制指定项的item数据,然后上下绘制其他的item。它先通过makeAndAddView完成当前item的创建并添加到layout,fillDown、fillUp则根据layout大小,上下绘制item完成整个list的绘制,使用的方法也是makeAndAddView,adjustViewsUpOrDown调整item位置。
/**
* Put a specific item at a specific location on the screen and then build
* up and down from there.
*
* @param position The reference view to use as the starting point
* @param top Pixel offset from the top of this view to the top of the
* reference view.
*
* @return The selected view, or null if the selected view is outside the
* visible area.
*/
private View fillSpecific(int position, int top) {
boolean tempIsSelected = position == mSelectedPosition;
View temp = makeAndAddView(position, top, true, mListPadding.left, tempIsSelected);
…
below = fillDown(position + 1, temp.getBottom() + dividerHeight);
// This will correct for the bottom of the last view not touching the bottom of the list
adjustViewsUpOrDown();
above = fillUp(position - 1, temp.getTop() - dividerHeight);
int childCount = getChildCount();
if (childCount > 0) {
correctTooLow(childCount);
}
}
顾名思义,makeAndAddView就是新建一个view,并将它添加到list里面。obtainView负责创建view,setupChild负责将view添加到layout,并设置好view的位置和相关属性。
private View makeAndAddView(int position, int y, boolean flow, int childrenLeft,
boolean selected) {
View child;
child = obtainView(position, mIsScrap);
// This needs to be positioned and measured
setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]);
return child;
}
obtainView将对应位置的view和数据关联起来,并返回view。它通过mRecycler获取到缓存的view,再通过mAdapter.getView将数据填充到view,mAdapter.getView即是我们前面分析到的ConversationListAdapter(CursorAdapter).getView。
至此,Message列表的创建过程就明了了。
3.3 功能分析
3.3.1 Message列表更新
当数据库内容发生变化(包括新短信、删除、发送状态改变等),就会触发onContentChanged,其调用栈如下:
ConversationList$2.onContentChanged(ConversationListAdapter) line: 474
ConversationListAdapter.onContentChanged() line: 118
CursorAdapter$ChangeObserver.onChange(boolean) line: 463
CursorAdapter$ChangeObserver(ContentObserver).onChange(boolean, Uri) line: 129
ContentObserver$NotificationRunnable.run() line: 180
Handler.handleCallback(Message) line: 808
Handler.dispatchMessage(Message) line: 103
Looper.loop() line: 193
从下往上分析,
1)ContentObserver
我们已经很熟悉handleCallback处理的是Post的消息,消息一般是runable,所以可以看到ContentObserver. dispatchChange有post这一消息的过程(dispatchChange的触发过程比较复杂,我们将在其他章节介绍,本例中当数据库内容变化时会触发这个流程)。
public final void dispatchChange(boolean selfChange, Uri uri) {
if (mHandler == null) {
onChange(selfChange, uri);
} else {
mHandler.post(new NotificationRunnable(selfChange, uri));
}
}
然后这个消息会运行其内部类NotificationRunnable的run方法,
public void run() {
ContentObserver.this.onChange(mSelfChange, mUri);
}
This指向的是ContentObserver的实例ChangeObserver,它是CursorAdapter的内部类,也是ContentObserver的子类,由于ConversationListAdapter通过CursorWrapper向ContentObservable注册了ContentObserver,所以当dispatchChange事件时,通过轮询,最终将事件传递给ChangeObserver。
而后,ChangeObserver调用封装类CursorAdapter的onContentChanged,将事件上传。
private class ChangeObserver extends ContentObserver {
public void onChange(boolean selfChange) {
onContentChanged();
}
}
2)onContentChanged
onContentChanged是CursorAdapter的一个方法,主要功能是当数据库内容更新后,重新使用游标查询数据。但这个方法被ConversationListAdapter重载,它给自己的客户提供一个借口类OnContentChangedListener,让客户去实现这个借口类,自己则调用接口类的onContentChanged方法执行客户定义的功能。
protected void onContentChanged() {
if (mCursor != null && !mCursor.isClosed()) {
if (mOnContentChangedListener != null) {
mOnContentChangedListener.onContentChanged(this);
}
}
}
3)ConversationList
ConversationList就是ConversationListAdapter的客户,它实现了接口类,如下,主要是调用startAsyncQuery进行数据的查询。
private final ConversationListAdapter.OnContentChangedListener mContentChangedListener =
new ConversationListAdapter.OnContentChangedListener() {
@Override
public void onContentChanged(ConversationListAdapter adapter) {
if (mIsInActivity) {
mNeedQuery = true;
startAsyncQuery();
}
}
};
并且ConversationList在其onStart向ConversationListAdapter注册了这个接口类监听器:
mListAdapter.setOnContentChangedListener(mContentChangedListener);
所以,当数据库数据改变时,根据上面相关调用流程,完成了Message列表的刷新功能。
BTW,附上游标改变后注册ContentObserver的调用栈:
ContentResolver$CursorWrapperInner(CursorWrapper).registerContentObserver(ContentObserver) line: 178
ConversationListAdapter(CursorAdapter).swapCursor(Cursor) line: 340
ConversationListAdapter(CursorAdapter).changeCursor(Cursor) line: 313
ConversationList$ThreadListQueryHandler.onQueryComplete(int, Object, Cursor) line: 1896
ConversationList$ThreadListQueryHandler(AsyncQueryHandler).handleMessage(Message) line: 344
ConversationList$ThreadListQueryHandler(Handler).dispatchMessage(Message) line: 110
Looper.loop() line: 193
3.3.2 Message列表定位
因为ConversationList是ListActivity,所以可以使用getListView获取ListView,再使用ListView的setSelectionFromTop,将列表显示开始位置指定到特定位置。
需要注意2个问题,
1)
在ConversationList会记录上次浏览列表的起始位置,所以不是随意就能设置好列表的显示位置,如onQueryComplete通过下面代码控制了什么时候可以设置这个起始位置,我们需要根据mSavedFirstVisiblePosition的值和相关逻辑去处理。
if (mSavedFirstVisiblePosition != AdapterView.INVALID_POSITION) {
// Restore the list to its previous position.
getListView().setSelectionFromTop(mSavedFirstVisiblePosition,
mSavedFirstItemOffset);
mSavedFirstVisiblePosition = AdapterView.INVALID_POSITION;
}
结合列表的创建过程,我们知道,当同步绘制列表时,是从mSyncPosition开始的,
fillSpecific(mSyncPosition, mSpecificTop);
而mSyncPosition正是setSelectionFromTop里传递进去的,所以列表起始位置只能通过setSelectionFromTop来设置。
public void setSelectionFromTop(int position, int y) {
…
mSyncPosition = position;
…
}
2)
游标的更新,当数据库数据变化后,需要更新游标,才能获取到正确的值。下面是onQueryComplete完成后,用changeCursor替换掉旧游标的过程,防止游标数据过期。
ConversationListAdapter(CursorAdapter).swapCursor(Cursor) line: 340
ConversationListAdapter(CursorAdapter).changeCursor(Cursor) line: 313
ConversationList$ThreadListQueryHandler.onQueryComplete(int, Object, Cursor) line: 1896
ConversationList$ThreadListQueryHandler(AsyncQueryHandler).handleMessage(Message) line: 344
ConversationList$ThreadListQueryHandler(Handler).dispatchMessage(Message) line: 110
Looper.loop() line: 193
在列表创建时,CursorAdapter. getView的游标最初是在CursorAdapte创建时传入的,之后可以通过swapCursor的方法更新。
6572_message_conversationList_详细分析
目 录
2. ConversationList
2.1.2.1 ConversationListAdapter
2.2.1 列表项click
2.2.2 Long Press click to Delete
2.3.1 会话ID创建
3. ConversationListItem
3.1.2.1 ConversationListItem实例创建
1. 概要
本文主要分析短信列表界面的主要控件的界面显示和处理逻辑。
2. ConversationList
2.1 ConversationList
2.1.1 Layout分析
ConversationList对应的layout是conversation_list_screen,它主要分3部分,第一部分是做全选的控制,第二部分是list列表,第三部分是列表为空的布局。具体的表现形式如下,布局文件就不再展开了。其中list列表将在另外的章节分析。
2.1.2 代码分析
2.1.2.1 ConversationListAdapter
ConversationListAdapter是ConversationListItem的数据适配器,在ConversationList.onCreate里创建,它通过bindView将会话信息绑定到item的layout上。
其创建过程如下,
private void initListAdapter() {
mListAdapter = new ConversationListAdapter(this, null);
/** M: now this code is useless and will lead to a JE, comment it.
* listener is set in onStart
*/
//mListAdapter.setOnContentChangedListener(mContentChangedListener);
setListAdapter(mListAdapter);
getListView().setRecyclerListener(mListAdapter);
}
因为ConversationList继承自ListActivity,setListAdapter将Adapter实例设置给ListActivity,这里会调用ensureList,它会加载一个布局,确保有view能用来对应Adapter。
public void setListAdapter(ListAdapter adapter) {
synchronized (this) {
ensureList();
mAdapter = adapter;
mList.setAdapter(adapter);
}
}
private void ensureList() {
if (mList != null) {
return;
}
setContentView(com.android.internal.R.layout.list_content_simple);
}
而实际上,mList已经在 onContentChanged被初始化为Layout里对应的ListView,
mList = (ListView)findViewById(com.android.internal.R.id.list);
这个ListView就是Message列表的容器了。
2.2 功能分析
2.2.1 列表项click
单击列表项的调用栈如下:
ConversationList.onListItemClick(ListView, View, int, long) line: 1086
ListActivity$2.onItemClick(AdapterView, View, int, long) line: 319
ListView(AdapterView).performItemClick(View, int, long) line: 298
ListView(AbsListView).performItemClick(View, int, long) line: 1128
AbsListView$PerformClick.run() line: 2815
AbsListView$1.run() line: 3574
Handler.handleCallback(Message) line: 800
ViewRootImpl$ViewRootHandler(Handler).dispatchMessage(Message) line: 100
2.2.2 Long Press click to Delete
长按item删除会话流程如下:
长按出现选择菜单,
mConvListOnCreateContextMenuListener
onCreateContextMenu
点击删除
ConversationList.confirmDeleteThreads(Collection, AsyncQueryHandler) line:
ConversationList.confirmDeleteThread(long, AsyncQueryHandler) line: 1320
ConversationList.onContextItemSelected(MenuItem) line: 1247
ConversationList(Activity).onMenuItemSelected(int, MenuItem) line: 2584
PhoneWindow$DialogMenuCallback.onMenuItemSelected(MenuBuilder, MenuItem) line:
开始删除之前,要做一个后台查询,看是否有lock住的消息,Conversation.startQueryHaveLockedMessages,
之后是后台查询handler.startQuery:
AsyncQueryHandler
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);
msg.arg1 = EVENT_ARG_QUERY;
WorkerArgs args = new WorkerArgs();
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);
}
这个查询过程将消息发给mWorkerThreadHandler,这是异步查询AsyncQueryHandler自己的handler,它封装完消息EVENT_ARG_QUERY后,再发送出去,
Message reply = args.handler.obtainMessage(token);
reply.obj = args;
reply.arg1 = msg.arg1;
reply.sendToTarget();
这个消息的接收对象的设置见args.handler = this; ,在本例中,这个handler是ThreadListQueryHandler。
数据库查询完成后执行onQueryComplete(HAVE_LOCKED_MESSAGES_TOKEN),
ConversationList$ThreadListQueryHandler.onQueryComplete(int, Object, Cursor) line: 1610
ConversationList$ThreadListQueryHandler(AsyncQueryHandler).handleMessage(Message) line: 344
ConversationList$ThreadListQueryHandler(Handler).dispatchMessage(Message) line: 107
Looper.loop() line: 194
之后会给出一个确认删除对话框confirmDeleteThreadDialog
在弹出的确认对话框中,选择删除,执行listener函数。
ConversationList$DeleteThreadListener.onClick(DialogInterface, int) line: 1484
AlertController$ButtonHandler.handleMessage(Message) line: 176
之后开始删除Conversation.startDelete
最后onDeleteComplete
响应函数会触发一系列其他的数据库操作,如查询list,查询unread等,
AsyncQueryHandler$WorkerHandler.handleMessage(Message) line: 120
AsyncQueryHandler$WorkerHandler(Handler).dispatchMessage(Message) line: 107
【Tips】
它的threadIds是在点击删除功能时,onContextItemSelected设置的,和长按的时候在onCreateContextMenu里选择的有时间差,不一定是同一个,在会话列表更新的时候,会有影响,所以会误删。
所以在onCreateContextMenu里要记录thread id。
2.3 数据操作
2.3.1 会话ID创建
Message列表是以会话为item进行显示的,会话是以为ID记录的,我们分析一下各种会话ID是如何创建的,
应用在CBMessageReceiverService.java收到小区广播后,使用handleCBMessageReceived方法进行处理,其中insertMessage是用来存储CB的。
insertMessage会调用CbProvider的insert方法
public Uri insert(Uri url, ContentValues initialValues) {
Uri result = null;
// TODO Check Permission
// checkPermission();
SQLiteDatabase db = mOpenHelper.getWritableDatabase();
SQLiteDatabase dbmmssms = mMmsSmsOpenHelper.getWritableDatabase();
int match = URI_MATCHER.match(url);
ContentValues values;
long rowID;
String table = null;
Log.d(TAG, " insert match = "+match);
switch (match) {
case URL_MESSAGES:
//table = CbDatabaseHelper.CBMESSAGE_TABLE;
table = MmsSmsDatabaseHelper.TABLE_CELLBROADCAST;
values = internalInsertMessages(initialValues);
rowID = dbmmssms.insert(table, null, values);
Log.d(TAG, "insert to cellbroadcast " + values);
if (rowID > 0) {
result = Uri.parse("content://messages/" + rowID);
notifyChange();
return result;
}
break;
…
}
在这里,我们重点关注一下CB Message的ThreadId是怎样获取的,在internalInsertMessages里面,显然threadId不是我们主动填充进去的,所以是通过getOrCreateThreadId获得的。
private ContentValues internalInsertMessages(ContentValues initialValues) {
ContentValues values;
if (initialValues != null) {
values = new ContentValues(initialValues);
} else {
values = new ContentValues();
}
Long threadId = values.getAsLong(EncapsulatedTelephony.SmsCb.THREAD_ID);
String address = values.getAsString(EncapsulatedTelephony.SmsCb.CHANNEL_ID);
if (((threadId == null) || (threadId == 0)) && (address != null)) {
values.put(EncapsulatedTelephony.SmsCb.THREAD_ID,
getOrCreateThreadId(address));
}
…
}
getOrCreateThreadId代码如下,它封装一个URI地址,包含了threadID和参数recipient、cellbroadcast,并使用SqliteWrapper.query进行查询,
private String getOrCreateThreadId(String address) {
//Uri.Builder uriBuilder = THREAD_ID_URI.buildUpon();
Uri.Builder uriBuilder = (Uri.parse("content://mms-sms/threadID")).buildUpon();
uriBuilder.appendQueryParameter("recipient", address);
uriBuilder.appendQueryParameter("cellbroadcast", address);
Uri uri = uriBuilder.build();
// TODO need replace with helper interface.
Cursor cursor = null;
long token = Binder.clearCallingIdentity();
try {
cursor = SqliteWrapper.query(this.getContext(), this
.getContext().getContentResolver(), uri, ID_PROJECTION, null,
null, null);
…
if (cursor.moveToFirst()) {
return String.valueOf(cursor.getLong(0));
…
}
SqliteWrapper会调用到ContentResolver,ContentResolver则最终调用到MmsSmsProvider,
在MmsSmsProvider.query里面,也是通过URI match找到操作码URI_THREAD_ID,根据参数cellbroadcast,使用getCBThreadId给CB查找或创建其ThreadId。
Threads表内容如下所示,里面的关键字段有_id ,recipient_ids,要想找到合适的ThreadId,只有通过recipient_ids来查询,
所以在getCBThreadId方法里面,通过getAddressIds从canonical_addresses表里面找到和号码匹配的recipient_ids列表,再到Threads表里面找到recipient_ids相同和type是小区广播的_id,即是我们需要的ThreadId,使用的查询语句如下。
String queryString = "SELECT _id FROM threads " + "WHERE type=" + EncapsulatedTelephony.Threads.CELL_BROADCAST_THREAD + " AND recipient_ids=?";
如果没有满足条件的ThreadId,调用insertThread()创建一个新的thread。
在Message列表界面通过按钮新建短信时,执行createNewMessage(),通过createIntent传递一个threadId=0的ID过去,
private void createNewMessage() {
startActivity(ComposeMessageActivity.createIntent(this, 0));
}
public static Intent createIntent(Context context, long threadId) {
Intent intent = new Intent(context, ComposeMessageActivity.class);
if (threadId > 0) {
intent.setData(Conversation.getUri(threadId));
}
return intent;
}
因为threadId为0,在会话创建中会获取或创建一个threadId,具体过程和下节相同。
在联系人的详情界面,点击发送短信,因为有号码存在,所以在ComposeMessageActivity.Oncreate方法里面会去查询或创建一个threadId。
onCreate的initActivityState里,有这么一段代码是获取会话实例的,mConversation.get通过号码获取到会话实例,
Uri intentData = intent.getData();
/// M: Code analyze 034, If intent is SEND,just create a new empty thread,
/// otherwise Conversation.get() will throw exception.
String action = intent.getAction();
if (intentData != null && (TextUtils.isEmpty(action) ||
!action.equals(Intent.ACTION_SEND))) {
/// M: group-contact send message
// try to get a conversation based on the data URI passed to our intent.
if (intentData.getPathSegments().size() < 2) {
mConversation = mConversation.get(getApplicationContext(),ContactList.getByNumbers(
getStringForMultipleRecipients(Conversation.getRecipients(intentData)),
false /* don't block */, true /* replace number */),false);
} else {
mConversation = Conversation.get(getApplicationContext(), intentData, false);
}
/// @}
mWorkingMessage.setText(getBody(intentData));
Conversation get代码如下,如果号码为空则新建会话,如果缓冲里存储了对应号码的会话,则使用缓冲里的数据,如果条件都不满足,则需要去数据库查询到ID,并新建会话,之后将新会话存放到缓冲。
public static Conversation get(Context context, ContactList recipients, boolean allowQuery) {
if (recipients.size() < 1) {
return createNew(context);
}
Conversation conv = Cache.get(recipients);
if (conv != null) {
return conv;
}
long threadId = getOrCreateThreadId(context, recipients);
conv = new Conversation(context, threadId, allowQuery);
。。。
Cache.put(conv);
}
getOrCreateThreadId用来获取ThreadId,由Threads.getOrCreateThreadId实现,这个方法在Telephony.java里面。
getOrCreateThreadId类似前面提到的小区广播的实现过程,先封装一个"content://mms-sms/threadID"的URI,但只使用一个参数recipient,
在MmsSmsProvider.query里面,也是通过URI match找到操作码URI_THREAD_ID,,使用getThreadId查找或创建其ThreadId,getAddressIds获取到匹配的recipientIds,再取Threads表里查询,如果查询到,返回游标,如果没有记录,则用insertThread向Threads里插入记录,再查询到ThreadId。
private synchronized Cursor getThreadId(List<String> recipients) {
Set<Long> addressIds = getAddressIds(recipients);
。。。
recipientIds = Long.toString(addressId);
。。。
String[] selectionArgs = new String[] { recipientIds };
SQLiteDatabase db = mOpenHelper.getReadableDatabase();
db.beginTransaction();
Cursor cursor = null;
try {
// Find the thread with the given recipients
cursor = db.rawQuery(THREAD_QUERY, selectionArgs);
if (cursor.getCount() == 0) {
// No thread with those recipients exists, so create the thread.
cursor.close();
Log.d(LOG_TAG, "getThreadId: create new thread_id for recipients " + recipients);
insertThread(recipientIds, recipients);
// The thread was just created, now find it and return it.
cursor = db.rawQuery(THREAD_QUERY, selectionArgs);
。。。
}
3. ConversationListItem
3.1 ConversationListItem
3.1.1 Layout分析
如前所述,短信记录的每条信息显示是使用conversation_list_item.xml布局的,在本例中,其直观显示是如下形式的一条短信记录,
conversation_list_item.xml文件如下,它是一个ConversationListItem类型的布局,
(需要注意不同屏幕使用这个文件的位置,本例中在packages\apps\Mms\res\layout-mdpi下面,不是packages\apps\Mms\res\layout)
conversation_list_item.xml
<?xml version="1.0" encoding="utf-8"?>
<com.android.mms.ui.ConversationListItem xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="?android:attr/listPreferredItemHeight"
android:background="?android:attr/activatedBackgroundIndicator" >
<CheckBox
android:id="@+id/select_box"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_centerVertical="true"
android:clickable="false"
android:focusable="false"
android:focusableInTouchMode="false" />
<com.android.mms.ui.MmsQuickContactBadge
android:id="@+id/avatar"
android:visibility="gone"
android:layout_centerVertical="true"
android:layout_toEndOf="@id/select_box"
style="?android:attr/quickContactBadgeStyleWindowLarge" />
<ImageView
android:id="@+id/presence"
android:visibility="invisible"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_alignParentTop="true"
android:src="@drawable/ic_unread_label"/>
<LinearLayout
android:id="@+id/iconlist"
android:layout_alignParentTop="true"
android:layout_alignParentEnd="true"
android:layout_marginTop="7dip"
android:layout_marginEnd="10dip"
android:orientation="horizontal"
android:layout_height="wrap_content"
android:layout_width="wrap_content" >
<ImageView android:id="@+id/draft"
android:visibility="gone"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:src="@drawable/ic_draft" />
<ImageView android:id="@+id/error"
android:paddingStart="6dp"
android:visibility="gone"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:src="@drawable/ic_list_alert_sms_failed" />
<ImageView android:id="@+id/attachment"
android:paddingStart="6dp"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:visibility="gone"
android:src="@drawable/ic_attachment_universal_small" />
<ImageView android:id="@+id/mute"
android:paddingStart="6dp"
android:visibility="gone"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:src="@drawable/ipmsg_silent" />
</LinearLayout>
<RelativeLayout
android:layout_alignParentTop="true"
android:layout_marginTop="4dip"
android:layout_marginStart="12dip"
android:layout_toEndOf="@id/avatar"
android:layout_toStartOf="@id/iconlist"
android:gravity="center_vertical"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView android:id="@+id/unread"
android:background="@drawable/ipmsg_message_box"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:textSize="12sp"
android:textColor="@color/text_color_unread"
android:singleLine="true"
android:layout_alignParentEnd="true" />
<RelativeLayout android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:layout_toStartOf="@id/unread">
<TextView android:id="@+id/draft_and_smgcount"
android:textAppearance="?android:attr/textAppearanceMedium"
android:layout_alignParentEnd="true"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="6dip"
android:singleLine="true" />
<com.android.mms.util.AlwaysMarqueeTextView android:id="@+id/from"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceMedium"
android:singleLine="true"
android:layout_alignParentStart="true"
android:layout_toStartOf="@id/draft_and_smgcount"
android:layout_marginEnd="6dip"
android:ellipsize="marquee" />
</RelativeLayout>
</RelativeLayout>
<TextView android:id="@+id/date"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:textSize= "12sp"
android:textColor="@color/dlg_text_counter_color"
android:layout_alignParentBottom="true"
android:layout_alignParentEnd="true"
android:layout_marginEnd="10dip"
android:layout_marginBottom="4dip"
android:gravity="bottom" />
<TextView android:id="@+id/subject"
android:layout_height="wrap_content"
android:layout_width="fill_parent"
android:textSize= "12sp"
android:layout_alignParentBottom="true"
android:layout_toEndOf="@id/avatar"
android:layout_toStartOf="@id/date"
android:layout_marginStart="12dip"
android:layout_marginBottom="4dip"
android:gravity="bottom"
android:textColor="@color/dlg_text_counter_color"
android:lines="2"
android:ellipsize="end" />
</com.android.mms.ui.ConversationListItem>
其对应的布局图形如下,图中左边是layout组成,右上是layout布局表现,右下是对layout里面的子view编号,便于理解。
分析xml文件,并对照图例可以看出,
ConversationListItem是整个item布局类,表现形式见下图左边,它指定了高度
android:layout_height="?android:attr/listPreferredItemHeight"
这个布局由6个同级view组成,见下图中间及右上,其中
checkbox供选择使用,见右下数字0,alignParentStart表示它在开头位置,
android:layout_alignParentStart="true"
MmsQuickContactBadge做头像显示,见右下数字1,
ImageView做未读标识等用途,见右下数字2,通过下面的属性将它放置在最右上的位置
android:layout_alignParentEnd="true"
android:layout_alignParentTop="true"
LinearLayout是个组合视图,见右下数字3和iconlist小图,它有四个子ImageView表示短信的信息状态,具体名字见iconlist小图。注意四个子ImageView表示同时最多只能显示四个图标,不是表示短信只有这四种状态。通过下面属性,将它放置在MmsQuickContactBadge后面。
android:orientation="horizontal"
<iconlist>
RelativeLayout是个组合视图,见右下数字4和Text Title小图,包含3个TextView,用来显示Title和消息数目、未读消息数目。在RelativeLayout,通过下面的属性设置,将RelativeLayout(index4的view)和上面的LinearLayout(index3的view)并行放置在同一行。
android:layout_toEndOf="@id/avatar"
android:layout_toStartOf="@id/iconlist"
TextView显示日期,见右下数字5,用下面2个属性表示它的位置在右下,
android:layout_alignParentBottom="true"
android:layout_alignParentEnd="true"
TextView 显示短线截取的部分内容,见右下数字6,用下面3个属性表明了它的位置。
android:layout_alignParentBottom="true"
android:layout_toEndOf="@id/avatar"
android:layout_toStartOf="@id/date"
<Text Content>
3.1.2 代码分析
3.1.2.1 ConversationListItem实例创建
ConversationListItem实例创建栈如下,因为有缓存使用,不一定每次进入列表都会创建这个类实例,所以不一定能调试到,要在此设置断点调试,有一个小技巧,就是将mms进程终止掉,让其自动重启(可通过发送短信或cb的方法加快重启过程),这样就可以进行调试。
ConversationListItem.<init>(Context, AttributeSet) line: 130
Constructor.constructNative(Object[], Class, Class[], int, boolean) line: not available [native method]
Constructor.newInstance(Object...) line: 423
PhoneLayoutInflater(LayoutInflater).createView(String, String, AttributeSet) line: 594
PhoneLayoutInflater(LayoutInflater).createViewFromTag(View, String, AttributeSet) line: 696
PhoneLayoutInflater(LayoutInflater).inflate(XmlPullParser, ViewGroup, boolean) line: 469
PhoneLayoutInflater(LayoutInflater).inflate(int, ViewGroup, boolean) line: 397
ConversationListAdapter.newView(Context, Cursor, ViewGroup) line: 103
ConversationListAdapter(CursorAdapter).getView(int, View, ViewGroup) line: 250
ListView(AbsListView).obtainView(int, boolean[]) line: 2338
ListView.makeAndAddView(int, int, boolean, int, boolean) line: 1812
ListView.fillDown(int, int) line: 698
ListView.fillFromTop(int) line: 759
ListView.layoutChildren() line: 1631
ListView(AbsListView).onLayout(boolean, int, int, int, int) line: 2149
ConversationListItem实例创建的主要过程分析如下,在getView来获取view的时候,因为缓冲没有数据,会使用newView新建一个view,
public View getView(int position, View convertView, ViewGroup parent) {
View v;
if (convertView == null) {
v = newView(mContext, mCursor, parent);
} else {
v = convertView;
}
bindView(v, mContext, mCursor);
return v;
}
newView实际的过程是inflate一个布局文件,这个布局文件就是我们分析的layout,它是ConversationListItem形式的,我们知道,在inflate创建布局文件时,会根据节点创建view实例,所以会使用类的构造类Constructor来创建ConversationListItem实例。
public View newView(Context context, Cursor cursor, ViewGroup parent) {
if (LOCAL_LOGV) Log.v(TAG, "inflating new view");
return mFactory.inflate(R.layout.conversation_list_item, parent, false);
}
Checkbox的主要作用是用来勾选短信列表的某一个item,并进行删除操作的。
对于checkbox,在ConversationListItem里面定义了一个引用mSelectBox,
private CheckBox mSelectBox;
并在onFinishInflate里面初始化它,关联到Layout里面的select_box,
mSelectBox = (CheckBox) findViewById(R.id.select_box);
当列表项被点击时,执行ConversationList.onListItemClick(ListView, View, int, long)这个方法,在里面添加下面代码,当处于删除模式时,如下,将Conversation设置为check状态,然后更新界面,
if (mListAdapter != null && mDeleteMode) {
if (conv.isChecked()) {
conv.setIsChecked(false);
} else {
conv.setIsChecked(true);
}
mListAdapter.notifyDataSetChanged();
updateSelectView();
return;
}
更新界面时,执行ConversationListItem.bind(Context, Conversation) ,每加载一个item项都会执行一次bind,在这里,会根据mSelectMode 决定checkbox是否显示,根据Conversation的状态决定是否选中。
所以实际上,不是我们主观上看到的那样由checkbox决定了当前项目是否选中,而是当前项选中后决定checkbox的显示状态。
if (mSelectMode) {
mSelectBox.setVisibility(View.VISIBLE);
mAvatarView.setClickable(false);
} else {
mSelectBox.setVisibility(View.GONE);
mAvatarView.setClickable(true);
}
if (mConversation.isChecked()) {
mSelectBox.setChecked(true);
} else {
mSelectBox.setChecked(false);
}
上面就是checkbox在代码里的处理过程。
R.id.avatar用来显示发件人头像,定义如下,MmsQuickContactBadge继承自QuickContactBadge,再往上是ImageView,一个通用的视图组件。
private MmsQuickContactBadge mAvatarView;
其实例获取在ConversationListItem的onFinishInflate里面,
mAvatarView = (MmsQuickContactBadge) findViewById(R.id.avatar);
如果希望AvatarView能够被点击,相应事件,则可以看到在bind有如下代码,表示在删除模式下不能被点击,在列表模式下可以被点击。
if (mSelectMode) {
mAvatarView.setClickable(false);
} else {
mAvatarView.setClickable(true);
}
AvatarView被点击之后,执行的调用栈如下,可见是由View直接调用了MmsQuickContactBadge类的onClick,并不是ConversationList或ConversationListItem里做了onClick的相关处理。
MmsQuickContactBadge(QuickContactBadge).onClick(View) line: 217
MmsQuickContactBadge.onClick(View) line: 75
MmsQuickContactBadge(View).performClick() line: 4212
View$PerformClick.run() line: 17476
Handler.handleCallback(Message) line: 800
ViewRootImpl$ViewRootHandler(Handler).dispatchMessage(Message) line: 100
QuickContactBadge类的onClick代码如下,
@Override
public void onClick(View v) {
if (mContactUri != null) {
QuickContact.showQuickContact(getContext(), QuickContactBadge.this, mContactUri,
QuickContact.MODE_LARGE, mExcludeMimes);
} else if (mContactEmail != null) {
mQueryHandler.startQuery(TOKEN_EMAIL_LOOKUP_AND_TRIGGER, mContactEmail,
Uri.withAppendedPath(Email.CONTENT_LOOKUP_URI, Uri.encode(mContactEmail)),
EMAIL_LOOKUP_PROJECTION, null, null, null);
} else if (mContactPhone != null) {
mQueryHandler.startQuery(TOKEN_PHONE_LOOKUP_AND_TRIGGER, mContactPhone,
Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI, mContactPhone),
PHONE_LOOKUP_PROJECTION, null, null, null);
} else {
// If a contact hasn't been assigned, don't react to click.
return;
}
}
我们当前的代码走mContactPhone路径,mContactPhone是在assignContactFromPhone里面赋值的,assignContactFromPhone则是通过ConversationListItem的updateAvatarView调用的(传入的number就赋值给mContactPhone),
mAvatarView.assignContactFromPhone(number, true);
updateAvatarView可以被updateFromView和bind调用,这两个方法我们在前面有过描述。所以从整个过程来看,我们可以看出avatar被点击和其需要的数据来源。
3.2 绘制分析
3.2.1 Message列表创建
如ConversationList的layout描述,Messag列表主要是由list呈现,而list的item项是由ConversationListItem布局的,我们这里讨论每个item项的创建以及整个list的创建。
在使用Cache时,ConversationListItem的创建过程如下,
ConversationListItem.bind(Context, Conversation) line: 480
ConversationListAdapter.bindView(View, Context, Cursor) line: 77
ConversationListAdapter(CursorAdapter).getView(int, View, ViewGroup) line: 250
ListView(AbsListView).obtainView(int, boolean[]) line: 2186
ListView.makeAndAddView(int, int, boolean, int, boolean) line: 1877
ListView.fillSpecific(int, int) line: 1356
ListView.layoutChildren() line: 1671
ListView(AbsListView).onLayout(boolean, int, int, int, int) line: 2037
ListView(View).layout(int, int, int, int) line: 14118
ListView(ViewGroup).layout(int, int, int, int) line: 4467
RelativeLayout.onLayout(boolean, int, int, int, int) line: 1021
从上往下分析该栈:
1)ConversationListItem.bind
ConversationListItem.bind的代码比较多,仔细分析,实际上逻辑比较简单,主要是设置每个view的内容和状态,比如说设置日期的代码如下,
mDateView.setVisibility(VISIBLE);
if(ConversationList.sConversationListOption ==
ConversationList.OPTION_CONVERSATION_LIST_IMPORTANT &&
!conversation.hasUnreadMessages()) {
mDateView.setText(MessageUtils.formatTimeStampStringExtend(context,
conversation.getImpDate()));
}
其数据来源于conversation,我们要进一步分析conversation是怎么传递进来的,以及为什么要调用bind。
2)ConversationListAdapter
ConversationListAdapter 是ConversationList的layout里的list对应的Adapter,它继承自MessageCursorAdapter-CursorAdapter-baseAdapter, 其方法bindView在本例中执行的主要代码如下,其中headerview是一个ConversationListItem引用,是调用方法传递过来的,conversation是根据游标新建的,最后调用bind,所以该方法的作用就是获取到conversation,并用准备将其中的数据赋值到view。
public void bindView(View view, Context context, Cursor cursor) {
ConversationListItem headerView = (ConversationListItem) view;
Conversation conv;
if (!mIsScrolling) {
Conversation.setNeedCacheConv(false);
conv = Conversation.from(context, cursor);
Conversation.setNeedCacheConv(true);
if (mSubjectSingleLine) {
headerView.setSubjectSingleLineMode(true);
}
if (conv != null) {
conv.setIsChecked(sSelectedTheadsId.contains(conv.getThreadId()));
}
headerView.bind(context, conv);
}
}
(CursorAdapter).getView则负责获取到view实例和游标实例。
view的获取有两种方式,一种是调用者传递过来以前缓存的,一种是新创建一个,在ConversationListItem实例创建中有讲到如何创建。
游标实例则在Adapter创建时创建。
3)ListView
在ListView部分,我们从layoutChildren向上分析,向下部分设计到太多ListView的实现机制问题,暂不讨论。
layoutChildren实现ListView子窗口布局,实际上就是list在layout所占部分。
本例从launcher进入Message列表,执行代码路径如下,mSyncPosition是当前item索引值,mSpecificTop是当前layout相对Top的位置。
layoutChildren:
case LAYOUT_SYNC:
sel = fillSpecific(mSyncPosition, mSpecificTop);
fillSpecific在知道位置绘制指定项的item数据,然后上下绘制其他的item。它先通过makeAndAddView完成当前item的创建并添加到layout,fillDown、fillUp则根据layout大小,上下绘制item完成整个list的绘制,使用的方法也是makeAndAddView,adjustViewsUpOrDown调整item位置。
/**
* Put a specific item at a specific location on the screen and then build
* up and down from there.
*
* @param position The reference view to use as the starting point
* @param top Pixel offset from the top of this view to the top of the
* reference view.
*
* @return The selected view, or null if the selected view is outside the
* visible area.
*/
private View fillSpecific(int position, int top) {
boolean tempIsSelected = position == mSelectedPosition;
View temp = makeAndAddView(position, top, true, mListPadding.left, tempIsSelected);
…
below = fillDown(position + 1, temp.getBottom() + dividerHeight);
// This will correct for the bottom of the last view not touching the bottom of the list
adjustViewsUpOrDown();
above = fillUp(position - 1, temp.getTop() - dividerHeight);
int childCount = getChildCount();
if (childCount > 0) {
correctTooLow(childCount);
}
}
顾名思义,makeAndAddView就是新建一个view,并将它添加到list里面。obtainView负责创建view,setupChild负责将view添加到layout,并设置好view的位置和相关属性。
private View makeAndAddView(int position, int y, boolean flow, int childrenLeft,
boolean selected) {
View child;
child = obtainView(position, mIsScrap);
// This needs to be positioned and measured
setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]);
return child;
}
obtainView将对应位置的view和数据关联起来,并返回view。它通过mRecycler获取到缓存的view,再通过mAdapter.getView将数据填充到view,mAdapter.getView即是我们前面分析到的ConversationListAdapter(CursorAdapter).getView。
至此,Message列表的创建过程就明了了。
3.3 功能分析
3.3.1 Message列表更新
当数据库内容发生变化(包括新短信、删除、发送状态改变等),就会触发onContentChanged,其调用栈如下:
ConversationList$2.onContentChanged(ConversationListAdapter) line: 474
ConversationListAdapter.onContentChanged() line: 118
CursorAdapter$ChangeObserver.onChange(boolean) line: 463
CursorAdapter$ChangeObserver(ContentObserver).onChange(boolean, Uri) line: 129
ContentObserver$NotificationRunnable.run() line: 180
Handler.handleCallback(Message) line: 808
Handler.dispatchMessage(Message) line: 103
Looper.loop() line: 193
从下往上分析,
1)ContentObserver
我们已经很熟悉handleCallback处理的是Post的消息,消息一般是runable,所以可以看到ContentObserver. dispatchChange有post这一消息的过程(dispatchChange的触发过程比较复杂,我们将在其他章节介绍,本例中当数据库内容变化时会触发这个流程)。
public final void dispatchChange(boolean selfChange, Uri uri) {
if (mHandler == null) {
onChange(selfChange, uri);
} else {
mHandler.post(new NotificationRunnable(selfChange, uri));
}
}
然后这个消息会运行其内部类NotificationRunnable的run方法,
public void run() {
ContentObserver.this.onChange(mSelfChange, mUri);
}
This指向的是ContentObserver的实例ChangeObserver,它是CursorAdapter的内部类,也是ContentObserver的子类,由于ConversationListAdapter通过CursorWrapper向ContentObservable注册了ContentObserver,所以当dispatchChange事件时,通过轮询,最终将事件传递给ChangeObserver。
而后,ChangeObserver调用封装类CursorAdapter的onContentChanged,将事件上传。
private class ChangeObserver extends ContentObserver {
public void onChange(boolean selfChange) {
onContentChanged();
}
}
2)onContentChanged
onContentChanged是CursorAdapter的一个方法,主要功能是当数据库内容更新后,重新使用游标查询数据。但这个方法被ConversationListAdapter重载,它给自己的客户提供一个借口类OnContentChangedListener,让客户去实现这个借口类,自己则调用接口类的onContentChanged方法执行客户定义的功能。
protected void onContentChanged() {
if (mCursor != null && !mCursor.isClosed()) {
if (mOnContentChangedListener != null) {
mOnContentChangedListener.onContentChanged(this);
}
}
}
3)ConversationList
ConversationList就是ConversationListAdapter的客户,它实现了接口类,如下,主要是调用startAsyncQuery进行数据的查询。
private final ConversationListAdapter.OnContentChangedListener mContentChangedListener =
new ConversationListAdapter.OnContentChangedListener() {
@Override
public void onContentChanged(ConversationListAdapter adapter) {
if (mIsInActivity) {
mNeedQuery = true;
startAsyncQuery();
}
}
};
并且ConversationList在其onStart向ConversationListAdapter注册了这个接口类监听器:
mListAdapter.setOnContentChangedListener(mContentChangedListener);
所以,当数据库数据改变时,根据上面相关调用流程,完成了Message列表的刷新功能。
BTW,附上游标改变后注册ContentObserver的调用栈:
ContentResolver$CursorWrapperInner(CursorWrapper).registerContentObserver(ContentObserver) line: 178
ConversationListAdapter(CursorAdapter).swapCursor(Cursor) line: 340
ConversationListAdapter(CursorAdapter).changeCursor(Cursor) line: 313
ConversationList$ThreadListQueryHandler.onQueryComplete(int, Object, Cursor) line: 1896
ConversationList$ThreadListQueryHandler(AsyncQueryHandler).handleMessage(Message) line: 344
ConversationList$ThreadListQueryHandler(Handler).dispatchMessage(Message) line: 110
Looper.loop() line: 193
3.3.2 Message列表定位
因为ConversationList是ListActivity,所以可以使用getListView获取ListView,再使用ListView的setSelectionFromTop,将列表显示开始位置指定到特定位置。
需要注意2个问题,
1)
在ConversationList会记录上次浏览列表的起始位置,所以不是随意就能设置好列表的显示位置,如onQueryComplete通过下面代码控制了什么时候可以设置这个起始位置,我们需要根据mSavedFirstVisiblePosition的值和相关逻辑去处理。
if (mSavedFirstVisiblePosition != AdapterView.INVALID_POSITION) {
// Restore the list to its previous position.
getListView().setSelectionFromTop(mSavedFirstVisiblePosition,
mSavedFirstItemOffset);
mSavedFirstVisiblePosition = AdapterView.INVALID_POSITION;
}
结合列表的创建过程,我们知道,当同步绘制列表时,是从mSyncPosition开始的,
fillSpecific(mSyncPosition, mSpecificTop);
而mSyncPosition正是setSelectionFromTop里传递进去的,所以列表起始位置只能通过setSelectionFromTop来设置。
public void setSelectionFromTop(int position, int y) {
…
mSyncPosition = position;
…
}
2)
游标的更新,当数据库数据变化后,需要更新游标,才能获取到正确的值。下面是onQueryComplete完成后,用changeCursor替换掉旧游标的过程,防止游标数据过期。
ConversationListAdapter(CursorAdapter).swapCursor(Cursor) line: 340
ConversationListAdapter(CursorAdapter).changeCursor(Cursor) line: 313
ConversationList$ThreadListQueryHandler.onQueryComplete(int, Object, Cursor) line: 1896
ConversationList$ThreadListQueryHandler(AsyncQueryHandler).handleMessage(Message) line: 344
ConversationList$ThreadListQueryHandler(Handler).dispatchMessage(Message) line: 110
Looper.loop() line: 193
在列表创建时,CursorAdapter. getView的游标最初是在CursorAdapte创建时传入的,之后可以通过swapCursor的方法更新。
目 录
2. ConversationList
2.1.2.1 ConversationListAdapter
2.2.1 列表项click
2.2.2 Long Press click to Delete
2.3.1 会话ID创建
3. ConversationListItem
3.1.2.1 ConversationListItem实例创建
1. 概要
本文主要分析短信列表界面的主要控件的界面显示和处理逻辑。
2. ConversationList
2.1 ConversationList
2.1.1 Layout分析
ConversationList对应的layout是conversation_list_screen,它主要分3部分,第一部分是做全选的控制,第二部分是list列表,第三部分是列表为空的布局。具体的表现形式如下,布局文件就不再展开了。其中list列表将在另外的章节分析。
2.1.2 代码分析
2.1.2.1 ConversationListAdapter
ConversationListAdapter是ConversationListItem的数据适配器,在ConversationList.onCreate里创建,它通过bindView将会话信息绑定到item的layout上。
其创建过程如下,
private void initListAdapter() {
mListAdapter = new ConversationListAdapter(this, null);
/** M: now this code is useless and will lead to a JE, comment it.
* listener is set in onStart
*/
//mListAdapter.setOnContentChangedListener(mContentChangedListener);
setListAdapter(mListAdapter);
getListView().setRecyclerListener(mListAdapter);
}
因为ConversationList继承自ListActivity,setListAdapter将Adapter实例设置给ListActivity,这里会调用ensureList,它会加载一个布局,确保有view能用来对应Adapter。
public void setListAdapter(ListAdapter adapter) {
synchronized (this) {
ensureList();
mAdapter = adapter;
mList.setAdapter(adapter);
}
}
private void ensureList() {
if (mList != null) {
return;
}
setContentView(com.android.internal.R.layout.list_content_simple);
}
而实际上,mList已经在 onContentChanged被初始化为Layout里对应的ListView,
mList = (ListView)findViewById(com.android.internal.R.id.list);
这个ListView就是Message列表的容器了。
2.2 功能分析
2.2.1 列表项click
单击列表项的调用栈如下:
ConversationList.onListItemClick(ListView, View, int, long) line: 1086
ListActivity$2.onItemClick(AdapterView, View, int, long) line: 319
ListView(AdapterView).performItemClick(View, int, long) line: 298
ListView(AbsListView).performItemClick(View, int, long) line: 1128
AbsListView$PerformClick.run() line: 2815
AbsListView$1.run() line: 3574
Handler.handleCallback(Message) line: 800
ViewRootImpl$ViewRootHandler(Handler).dispatchMessage(Message) line: 100
2.2.2 Long Press click to Delete
长按item删除会话流程如下:
长按出现选择菜单,
mConvListOnCreateContextMenuListener
onCreateContextMenu
点击删除
ConversationList.confirmDeleteThreads(Collection, AsyncQueryHandler) line:
ConversationList.confirmDeleteThread(long, AsyncQueryHandler) line: 1320
ConversationList.onContextItemSelected(MenuItem) line: 1247
ConversationList(Activity).onMenuItemSelected(int, MenuItem) line: 2584
PhoneWindow$DialogMenuCallback.onMenuItemSelected(MenuBuilder, MenuItem) line:
开始删除之前,要做一个后台查询,看是否有lock住的消息,Conversation.startQueryHaveLockedMessages,
之后是后台查询handler.startQuery:
AsyncQueryHandler
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);
msg.arg1 = EVENT_ARG_QUERY;
WorkerArgs args = new WorkerArgs();
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);
}
这个查询过程将消息发给mWorkerThreadHandler,这是异步查询AsyncQueryHandler自己的handler,它封装完消息EVENT_ARG_QUERY后,再发送出去,
Message reply = args.handler.obtainMessage(token);
reply.obj = args;
reply.arg1 = msg.arg1;
reply.sendToTarget();
这个消息的接收对象的设置见args.handler = this; ,在本例中,这个handler是ThreadListQueryHandler。
数据库查询完成后执行onQueryComplete(HAVE_LOCKED_MESSAGES_TOKEN),
ConversationList$ThreadListQueryHandler.onQueryComplete(int, Object, Cursor) line: 1610
ConversationList$ThreadListQueryHandler(AsyncQueryHandler).handleMessage(Message) line: 344
ConversationList$ThreadListQueryHandler(Handler).dispatchMessage(Message) line: 107
Looper.loop() line: 194
之后会给出一个确认删除对话框confirmDeleteThreadDialog
在弹出的确认对话框中,选择删除,执行listener函数。
ConversationList$DeleteThreadListener.onClick(DialogInterface, int) line: 1484
AlertController$ButtonHandler.handleMessage(Message) line: 176
之后开始删除Conversation.startDelete
最后onDeleteComplete
响应函数会触发一系列其他的数据库操作,如查询list,查询unread等,
AsyncQueryHandler$WorkerHandler.handleMessage(Message) line: 120
AsyncQueryHandler$WorkerHandler(Handler).dispatchMessage(Message) line: 107
【Tips】
它的threadIds是在点击删除功能时,onContextItemSelected设置的,和长按的时候在onCreateContextMenu里选择的有时间差,不一定是同一个,在会话列表更新的时候,会有影响,所以会误删。
所以在onCreateContextMenu里要记录thread id。
2.3 数据操作
2.3.1 会话ID创建
Message列表是以会话为item进行显示的,会话是以为ID记录的,我们分析一下各种会话ID是如何创建的,
应用在CBMessageReceiverService.java收到小区广播后,使用handleCBMessageReceived方法进行处理,其中insertMessage是用来存储CB的。
insertMessage会调用CbProvider的insert方法
public Uri insert(Uri url, ContentValues initialValues) {
Uri result = null;
// TODO Check Permission
// checkPermission();
SQLiteDatabase db = mOpenHelper.getWritableDatabase();
SQLiteDatabase dbmmssms = mMmsSmsOpenHelper.getWritableDatabase();
int match = URI_MATCHER.match(url);
ContentValues values;
long rowID;
String table = null;
Log.d(TAG, " insert match = "+match);
switch (match) {
case URL_MESSAGES:
//table = CbDatabaseHelper.CBMESSAGE_TABLE;
table = MmsSmsDatabaseHelper.TABLE_CELLBROADCAST;
values = internalInsertMessages(initialValues);
rowID = dbmmssms.insert(table, null, values);
Log.d(TAG, "insert to cellbroadcast " + values);
if (rowID > 0) {
result = Uri.parse("content://messages/" + rowID);
notifyChange();
return result;
}
break;
…
}
在这里,我们重点关注一下CB Message的ThreadId是怎样获取的,在internalInsertMessages里面,显然threadId不是我们主动填充进去的,所以是通过getOrCreateThreadId获得的。
private ContentValues internalInsertMessages(ContentValues initialValues) {
ContentValues values;
if (initialValues != null) {
values = new ContentValues(initialValues);
} else {
values = new ContentValues();
}
Long threadId = values.getAsLong(EncapsulatedTelephony.SmsCb.THREAD_ID);
String address = values.getAsString(EncapsulatedTelephony.SmsCb.CHANNEL_ID);
if (((threadId == null) || (threadId == 0)) && (address != null)) {
values.put(EncapsulatedTelephony.SmsCb.THREAD_ID,
getOrCreateThreadId(address));
}
…
}
getOrCreateThreadId代码如下,它封装一个URI地址,包含了threadID和参数recipient、cellbroadcast,并使用SqliteWrapper.query进行查询,
private String getOrCreateThreadId(String address) {
//Uri.Builder uriBuilder = THREAD_ID_URI.buildUpon();
Uri.Builder uriBuilder = (Uri.parse("content://mms-sms/threadID")).buildUpon();
uriBuilder.appendQueryParameter("recipient", address);
uriBuilder.appendQueryParameter("cellbroadcast", address);
Uri uri = uriBuilder.build();
// TODO need replace with helper interface.
Cursor cursor = null;
long token = Binder.clearCallingIdentity();
try {
cursor = SqliteWrapper.query(this.getContext(), this
.getContext().getContentResolver(), uri, ID_PROJECTION, null,
null, null);
…
if (cursor.moveToFirst()) {
return String.valueOf(cursor.getLong(0));
…
}
SqliteWrapper会调用到ContentResolver,ContentResolver则最终调用到MmsSmsProvider,
在MmsSmsProvider.query里面,也是通过URI match找到操作码URI_THREAD_ID,根据参数cellbroadcast,使用getCBThreadId给CB查找或创建其ThreadId。
Threads表内容如下所示,里面的关键字段有_id ,recipient_ids,要想找到合适的ThreadId,只有通过recipient_ids来查询,
所以在getCBThreadId方法里面,通过getAddressIds从canonical_addresses表里面找到和号码匹配的recipient_ids列表,再到Threads表里面找到recipient_ids相同和type是小区广播的_id,即是我们需要的ThreadId,使用的查询语句如下。
String queryString = "SELECT _id FROM threads " + "WHERE type=" + EncapsulatedTelephony.Threads.CELL_BROADCAST_THREAD + " AND recipient_ids=?";
如果没有满足条件的ThreadId,调用insertThread()创建一个新的thread。
在Message列表界面通过按钮新建短信时,执行createNewMessage(),通过createIntent传递一个threadId=0的ID过去,
private void createNewMessage() {
startActivity(ComposeMessageActivity.createIntent(this, 0));
}
public static Intent createIntent(Context context, long threadId) {
Intent intent = new Intent(context, ComposeMessageActivity.class);
if (threadId > 0) {
intent.setData(Conversation.getUri(threadId));
}
return intent;
}
因为threadId为0,在会话创建中会获取或创建一个threadId,具体过程和下节相同。
在联系人的详情界面,点击发送短信,因为有号码存在,所以在ComposeMessageActivity.Oncreate方法里面会去查询或创建一个threadId。
onCreate的initActivityState里,有这么一段代码是获取会话实例的,mConversation.get通过号码获取到会话实例,
Uri intentData = intent.getData();
/// M: Code analyze 034, If intent is SEND,just create a new empty thread,
/// otherwise Conversation.get() will throw exception.
String action = intent.getAction();
if (intentData != null && (TextUtils.isEmpty(action) ||
!action.equals(Intent.ACTION_SEND))) {
/// M: group-contact send message
// try to get a conversation based on the data URI passed to our intent.
if (intentData.getPathSegments().size() < 2) {
mConversation = mConversation.get(getApplicationContext(),ContactList.getByNumbers(
getStringForMultipleRecipients(Conversation.getRecipients(intentData)),
false /* don't block */, true /* replace number */),false);
} else {
mConversation = Conversation.get(getApplicationContext(), intentData, false);
}
/// @}
mWorkingMessage.setText(getBody(intentData));
Conversation get代码如下,如果号码为空则新建会话,如果缓冲里存储了对应号码的会话,则使用缓冲里的数据,如果条件都不满足,则需要去数据库查询到ID,并新建会话,之后将新会话存放到缓冲。
public static Conversation get(Context context, ContactList recipients, boolean allowQuery) {
if (recipients.size() < 1) {
return createNew(context);
}
Conversation conv = Cache.get(recipients);
if (conv != null) {
return conv;
}
long threadId = getOrCreateThreadId(context, recipients);
conv = new Conversation(context, threadId, allowQuery);
。。。
Cache.put(conv);
}
getOrCreateThreadId用来获取ThreadId,由Threads.getOrCreateThreadId实现,这个方法在Telephony.java里面。
getOrCreateThreadId类似前面提到的小区广播的实现过程,先封装一个"content://mms-sms/threadID"的URI,但只使用一个参数recipient,
在MmsSmsProvider.query里面,也是通过URI match找到操作码URI_THREAD_ID,,使用getThreadId查找或创建其ThreadId,getAddressIds获取到匹配的recipientIds,再取Threads表里查询,如果查询到,返回游标,如果没有记录,则用insertThread向Threads里插入记录,再查询到ThreadId。
private synchronized Cursor getThreadId(List<String> recipients) {
Set<Long> addressIds = getAddressIds(recipients);
。。。
recipientIds = Long.toString(addressId);
。。。
String[] selectionArgs = new String[] { recipientIds };
SQLiteDatabase db = mOpenHelper.getReadableDatabase();
db.beginTransaction();
Cursor cursor = null;
try {
// Find the thread with the given recipients
cursor = db.rawQuery(THREAD_QUERY, selectionArgs);
if (cursor.getCount() == 0) {
// No thread with those recipients exists, so create the thread.
cursor.close();
Log.d(LOG_TAG, "getThreadId: create new thread_id for recipients " + recipients);
insertThread(recipientIds, recipients);
// The thread was just created, now find it and return it.
cursor = db.rawQuery(THREAD_QUERY, selectionArgs);
。。。
}
3. ConversationListItem
3.1 ConversationListItem
3.1.1 Layout分析
如前所述,短信记录的每条信息显示是使用conversation_list_item.xml布局的,在本例中,其直观显示是如下形式的一条短信记录,
conversation_list_item.xml文件如下,它是一个ConversationListItem类型的布局,
(需要注意不同屏幕使用这个文件的位置,本例中在packages\apps\Mms\res\layout-mdpi下面,不是packages\apps\Mms\res\layout)
conversation_list_item.xml
<?xml version="1.0" encoding="utf-8"?>
<com.android.mms.ui.ConversationListItem xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="?android:attr/listPreferredItemHeight"
android:background="?android:attr/activatedBackgroundIndicator" >
<CheckBox
android:id="@+id/select_box"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_centerVertical="true"
android:clickable="false"
android:focusable="false"
android:focusableInTouchMode="false" />
<com.android.mms.ui.MmsQuickContactBadge
android:id="@+id/avatar"
android:visibility="gone"
android:layout_centerVertical="true"
android:layout_toEndOf="@id/select_box"
style="?android:attr/quickContactBadgeStyleWindowLarge" />
<ImageView
android:id="@+id/presence"
android:visibility="invisible"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_alignParentTop="true"
android:src="@drawable/ic_unread_label"/>
<LinearLayout
android:id="@+id/iconlist"
android:layout_alignParentTop="true"
android:layout_alignParentEnd="true"
android:layout_marginTop="7dip"
android:layout_marginEnd="10dip"
android:orientation="horizontal"
android:layout_height="wrap_content"
android:layout_width="wrap_content" >
<ImageView android:id="@+id/draft"
android:visibility="gone"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:src="@drawable/ic_draft" />
<ImageView android:id="@+id/error"
android:paddingStart="6dp"
android:visibility="gone"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:src="@drawable/ic_list_alert_sms_failed" />
<ImageView android:id="@+id/attachment"
android:paddingStart="6dp"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:visibility="gone"
android:src="@drawable/ic_attachment_universal_small" />
<ImageView android:id="@+id/mute"
android:paddingStart="6dp"
android:visibility="gone"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:src="@drawable/ipmsg_silent" />
</LinearLayout>
<RelativeLayout
android:layout_alignParentTop="true"
android:layout_marginTop="4dip"
android:layout_marginStart="12dip"
android:layout_toEndOf="@id/avatar"
android:layout_toStartOf="@id/iconlist"
android:gravity="center_vertical"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView android:id="@+id/unread"
android:background="@drawable/ipmsg_message_box"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:textSize="12sp"
android:textColor="@color/text_color_unread"
android:singleLine="true"
android:layout_alignParentEnd="true" />
<RelativeLayout android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:layout_toStartOf="@id/unread">
<TextView android:id="@+id/draft_and_smgcount"
android:textAppearance="?android:attr/textAppearanceMedium"
android:layout_alignParentEnd="true"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="6dip"
android:singleLine="true" />
<com.android.mms.util.AlwaysMarqueeTextView android:id="@+id/from"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceMedium"
android:singleLine="true"
android:layout_alignParentStart="true"
android:layout_toStartOf="@id/draft_and_smgcount"
android:layout_marginEnd="6dip"
android:ellipsize="marquee" />
</RelativeLayout>
</RelativeLayout>
<TextView android:id="@+id/date"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:textSize= "12sp"
android:textColor="@color/dlg_text_counter_color"
android:layout_alignParentBottom="true"
android:layout_alignParentEnd="true"
android:layout_marginEnd="10dip"
android:layout_marginBottom="4dip"
android:gravity="bottom" />
<TextView android:id="@+id/subject"
android:layout_height="wrap_content"
android:layout_width="fill_parent"
android:textSize= "12sp"
android:layout_alignParentBottom="true"
android:layout_toEndOf="@id/avatar"
android:layout_toStartOf="@id/date"
android:layout_marginStart="12dip"
android:layout_marginBottom="4dip"
android:gravity="bottom"
android:textColor="@color/dlg_text_counter_color"
android:lines="2"
android:ellipsize="end" />
</com.android.mms.ui.ConversationListItem>
其对应的布局图形如下,图中左边是layout组成,右上是layout布局表现,右下是对layout里面的子view编号,便于理解。
分析xml文件,并对照图例可以看出,
ConversationListItem是整个item布局类,表现形式见下图左边,它指定了高度
android:layout_height="?android:attr/listPreferredItemHeight"
这个布局由6个同级view组成,见下图中间及右上,其中
checkbox供选择使用,见右下数字0,alignParentStart表示它在开头位置,
android:layout_alignParentStart="true"
MmsQuickContactBadge做头像显示,见右下数字1,
ImageView做未读标识等用途,见右下数字2,通过下面的属性将它放置在最右上的位置
android:layout_alignParentEnd="true"
android:layout_alignParentTop="true"
LinearLayout是个组合视图,见右下数字3和iconlist小图,它有四个子ImageView表示短信的信息状态,具体名字见iconlist小图。注意四个子ImageView表示同时最多只能显示四个图标,不是表示短信只有这四种状态。通过下面属性,将它放置在MmsQuickContactBadge后面。
android:orientation="horizontal"
<iconlist>
RelativeLayout是个组合视图,见右下数字4和Text Title小图,包含3个TextView,用来显示Title和消息数目、未读消息数目。在RelativeLayout,通过下面的属性设置,将RelativeLayout(index4的view)和上面的LinearLayout(index3的view)并行放置在同一行。
android:layout_toEndOf="@id/avatar"
android:layout_toStartOf="@id/iconlist"
TextView显示日期,见右下数字5,用下面2个属性表示它的位置在右下,
android:layout_alignParentBottom="true"
android:layout_alignParentEnd="true"
TextView 显示短线截取的部分内容,见右下数字6,用下面3个属性表明了它的位置。
android:layout_alignParentBottom="true"
android:layout_toEndOf="@id/avatar"
android:layout_toStartOf="@id/date"
<Text Content>
3.1.2 代码分析
3.1.2.1 ConversationListItem实例创建
ConversationListItem实例创建栈如下,因为有缓存使用,不一定每次进入列表都会创建这个类实例,所以不一定能调试到,要在此设置断点调试,有一个小技巧,就是将mms进程终止掉,让其自动重启(可通过发送短信或cb的方法加快重启过程),这样就可以进行调试。
ConversationListItem.<init>(Context, AttributeSet) line: 130
Constructor.constructNative(Object[], Class, Class[], int, boolean) line: not available [native method]
Constructor.newInstance(Object...) line: 423
PhoneLayoutInflater(LayoutInflater).createView(String, String, AttributeSet) line: 594
PhoneLayoutInflater(LayoutInflater).createViewFromTag(View, String, AttributeSet) line: 696
PhoneLayoutInflater(LayoutInflater).inflate(XmlPullParser, ViewGroup, boolean) line: 469
PhoneLayoutInflater(LayoutInflater).inflate(int, ViewGroup, boolean) line: 397
ConversationListAdapter.newView(Context, Cursor, ViewGroup) line: 103
ConversationListAdapter(CursorAdapter).getView(int, View, ViewGroup) line: 250
ListView(AbsListView).obtainView(int, boolean[]) line: 2338
ListView.makeAndAddView(int, int, boolean, int, boolean) line: 1812
ListView.fillDown(int, int) line: 698
ListView.fillFromTop(int) line: 759
ListView.layoutChildren() line: 1631
ListView(AbsListView).onLayout(boolean, int, int, int, int) line: 2149
ConversationListItem实例创建的主要过程分析如下,在getView来获取view的时候,因为缓冲没有数据,会使用newView新建一个view,
public View getView(int position, View convertView, ViewGroup parent) {
View v;
if (convertView == null) {
v = newView(mContext, mCursor, parent);
} else {
v = convertView;
}
bindView(v, mContext, mCursor);
return v;
}
newView实际的过程是inflate一个布局文件,这个布局文件就是我们分析的layout,它是ConversationListItem形式的,我们知道,在inflate创建布局文件时,会根据节点创建view实例,所以会使用类的构造类Constructor来创建ConversationListItem实例。
public View newView(Context context, Cursor cursor, ViewGroup parent) {
if (LOCAL_LOGV) Log.v(TAG, "inflating new view");
return mFactory.inflate(R.layout.conversation_list_item, parent, false);
}
Checkbox的主要作用是用来勾选短信列表的某一个item,并进行删除操作的。
对于checkbox,在ConversationListItem里面定义了一个引用mSelectBox,
private CheckBox mSelectBox;
并在onFinishInflate里面初始化它,关联到Layout里面的select_box,
mSelectBox = (CheckBox) findViewById(R.id.select_box);
当列表项被点击时,执行ConversationList.onListItemClick(ListView, View, int, long)这个方法,在里面添加下面代码,当处于删除模式时,如下,将Conversation设置为check状态,然后更新界面,
if (mListAdapter != null && mDeleteMode) {
if (conv.isChecked()) {
conv.setIsChecked(false);
} else {
conv.setIsChecked(true);
}
mListAdapter.notifyDataSetChanged();
updateSelectView();
return;
}
更新界面时,执行ConversationListItem.bind(Context, Conversation) ,每加载一个item项都会执行一次bind,在这里,会根据mSelectMode 决定checkbox是否显示,根据Conversation的状态决定是否选中。
所以实际上,不是我们主观上看到的那样由checkbox决定了当前项目是否选中,而是当前项选中后决定checkbox的显示状态。
if (mSelectMode) {
mSelectBox.setVisibility(View.VISIBLE);
mAvatarView.setClickable(false);
} else {
mSelectBox.setVisibility(View.GONE);
mAvatarView.setClickable(true);
}
if (mConversation.isChecked()) {
mSelectBox.setChecked(true);
} else {
mSelectBox.setChecked(false);
}
上面就是checkbox在代码里的处理过程。
R.id.avatar用来显示发件人头像,定义如下,MmsQuickContactBadge继承自QuickContactBadge,再往上是ImageView,一个通用的视图组件。
private MmsQuickContactBadge mAvatarView;
其实例获取在ConversationListItem的onFinishInflate里面,
mAvatarView = (MmsQuickContactBadge) findViewById(R.id.avatar);
如果希望AvatarView能够被点击,相应事件,则可以看到在bind有如下代码,表示在删除模式下不能被点击,在列表模式下可以被点击。
if (mSelectMode) {
mAvatarView.setClickable(false);
} else {
mAvatarView.setClickable(true);
}
AvatarView被点击之后,执行的调用栈如下,可见是由View直接调用了MmsQuickContactBadge类的onClick,并不是ConversationList或ConversationListItem里做了onClick的相关处理。
MmsQuickContactBadge(QuickContactBadge).onClick(View) line: 217
MmsQuickContactBadge.onClick(View) line: 75
MmsQuickContactBadge(View).performClick() line: 4212
View$PerformClick.run() line: 17476
Handler.handleCallback(Message) line: 800
ViewRootImpl$ViewRootHandler(Handler).dispatchMessage(Message) line: 100
QuickContactBadge类的onClick代码如下,
@Override
public void onClick(View v) {
if (mContactUri != null) {
QuickContact.showQuickContact(getContext(), QuickContactBadge.this, mContactUri,
QuickContact.MODE_LARGE, mExcludeMimes);
} else if (mContactEmail != null) {
mQueryHandler.startQuery(TOKEN_EMAIL_LOOKUP_AND_TRIGGER, mContactEmail,
Uri.withAppendedPath(Email.CONTENT_LOOKUP_URI, Uri.encode(mContactEmail)),
EMAIL_LOOKUP_PROJECTION, null, null, null);
} else if (mContactPhone != null) {
mQueryHandler.startQuery(TOKEN_PHONE_LOOKUP_AND_TRIGGER, mContactPhone,
Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI, mContactPhone),
PHONE_LOOKUP_PROJECTION, null, null, null);
} else {
// If a contact hasn't been assigned, don't react to click.
return;
}
}
我们当前的代码走mContactPhone路径,mContactPhone是在assignContactFromPhone里面赋值的,assignContactFromPhone则是通过ConversationListItem的updateAvatarView调用的(传入的number就赋值给mContactPhone),
mAvatarView.assignContactFromPhone(number, true);
updateAvatarView可以被updateFromView和bind调用,这两个方法我们在前面有过描述。所以从整个过程来看,我们可以看出avatar被点击和其需要的数据来源。
3.2 绘制分析
3.2.1 Message列表创建
如ConversationList的layout描述,Messag列表主要是由list呈现,而list的item项是由ConversationListItem布局的,我们这里讨论每个item项的创建以及整个list的创建。
在使用Cache时,ConversationListItem的创建过程如下,
ConversationListItem.bind(Context, Conversation) line: 480
ConversationListAdapter.bindView(View, Context, Cursor) line: 77
ConversationListAdapter(CursorAdapter).getView(int, View, ViewGroup) line: 250
ListView(AbsListView).obtainView(int, boolean[]) line: 2186
ListView.makeAndAddView(int, int, boolean, int, boolean) line: 1877
ListView.fillSpecific(int, int) line: 1356
ListView.layoutChildren() line: 1671
ListView(AbsListView).onLayout(boolean, int, int, int, int) line: 2037
ListView(View).layout(int, int, int, int) line: 14118
ListView(ViewGroup).layout(int, int, int, int) line: 4467
RelativeLayout.onLayout(boolean, int, int, int, int) line: 1021
从上往下分析该栈:
1)ConversationListItem.bind
ConversationListItem.bind的代码比较多,仔细分析,实际上逻辑比较简单,主要是设置每个view的内容和状态,比如说设置日期的代码如下,
mDateView.setVisibility(VISIBLE);
if(ConversationList.sConversationListOption ==
ConversationList.OPTION_CONVERSATION_LIST_IMPORTANT &&
!conversation.hasUnreadMessages()) {
mDateView.setText(MessageUtils.formatTimeStampStringExtend(context,
conversation.getImpDate()));
}
其数据来源于conversation,我们要进一步分析conversation是怎么传递进来的,以及为什么要调用bind。
2)ConversationListAdapter
ConversationListAdapter 是ConversationList的layout里的list对应的Adapter,它继承自MessageCursorAdapter-CursorAdapter-baseAdapter, 其方法bindView在本例中执行的主要代码如下,其中headerview是一个ConversationListItem引用,是调用方法传递过来的,conversation是根据游标新建的,最后调用bind,所以该方法的作用就是获取到conversation,并用准备将其中的数据赋值到view。
public void bindView(View view, Context context, Cursor cursor) {
ConversationListItem headerView = (ConversationListItem) view;
Conversation conv;
if (!mIsScrolling) {
Conversation.setNeedCacheConv(false);
conv = Conversation.from(context, cursor);
Conversation.setNeedCacheConv(true);
if (mSubjectSingleLine) {
headerView.setSubjectSingleLineMode(true);
}
if (conv != null) {
conv.setIsChecked(sSelectedTheadsId.contains(conv.getThreadId()));
}
headerView.bind(context, conv);
}
}
(CursorAdapter).getView则负责获取到view实例和游标实例。
view的获取有两种方式,一种是调用者传递过来以前缓存的,一种是新创建一个,在ConversationListItem实例创建中有讲到如何创建。
游标实例则在Adapter创建时创建。
3)ListView
在ListView部分,我们从layoutChildren向上分析,向下部分设计到太多ListView的实现机制问题,暂不讨论。
layoutChildren实现ListView子窗口布局,实际上就是list在layout所占部分。
本例从launcher进入Message列表,执行代码路径如下,mSyncPosition是当前item索引值,mSpecificTop是当前layout相对Top的位置。
layoutChildren:
case LAYOUT_SYNC:
sel = fillSpecific(mSyncPosition, mSpecificTop);
fillSpecific在知道位置绘制指定项的item数据,然后上下绘制其他的item。它先通过makeAndAddView完成当前item的创建并添加到layout,fillDown、fillUp则根据layout大小,上下绘制item完成整个list的绘制,使用的方法也是makeAndAddView,adjustViewsUpOrDown调整item位置。
/**
* Put a specific item at a specific location on the screen and then build
* up and down from there.
*
* @param position The reference view to use as the starting point
* @param top Pixel offset from the top of this view to the top of the
* reference view.
*
* @return The selected view, or null if the selected view is outside the
* visible area.
*/
private View fillSpecific(int position, int top) {
boolean tempIsSelected = position == mSelectedPosition;
View temp = makeAndAddView(position, top, true, mListPadding.left, tempIsSelected);
…
below = fillDown(position + 1, temp.getBottom() + dividerHeight);
// This will correct for the bottom of the last view not touching the bottom of the list
adjustViewsUpOrDown();
above = fillUp(position - 1, temp.getTop() - dividerHeight);
int childCount = getChildCount();
if (childCount > 0) {
correctTooLow(childCount);
}
}
顾名思义,makeAndAddView就是新建一个view,并将它添加到list里面。obtainView负责创建view,setupChild负责将view添加到layout,并设置好view的位置和相关属性。
private View makeAndAddView(int position, int y, boolean flow, int childrenLeft,
boolean selected) {
View child;
child = obtainView(position, mIsScrap);
// This needs to be positioned and measured
setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]);
return child;
}
obtainView将对应位置的view和数据关联起来,并返回view。它通过mRecycler获取到缓存的view,再通过mAdapter.getView将数据填充到view,mAdapter.getView即是我们前面分析到的ConversationListAdapter(CursorAdapter).getView。
至此,Message列表的创建过程就明了了。
3.3 功能分析
3.3.1 Message列表更新
当数据库内容发生变化(包括新短信、删除、发送状态改变等),就会触发onContentChanged,其调用栈如下:
ConversationList$2.onContentChanged(ConversationListAdapter) line: 474
ConversationListAdapter.onContentChanged() line: 118
CursorAdapter$ChangeObserver.onChange(boolean) line: 463
CursorAdapter$ChangeObserver(ContentObserver).onChange(boolean, Uri) line: 129
ContentObserver$NotificationRunnable.run() line: 180
Handler.handleCallback(Message) line: 808
Handler.dispatchMessage(Message) line: 103
Looper.loop() line: 193
从下往上分析,
1)ContentObserver
我们已经很熟悉handleCallback处理的是Post的消息,消息一般是runable,所以可以看到ContentObserver. dispatchChange有post这一消息的过程(dispatchChange的触发过程比较复杂,我们将在其他章节介绍,本例中当数据库内容变化时会触发这个流程)。
public final void dispatchChange(boolean selfChange, Uri uri) {
if (mHandler == null) {
onChange(selfChange, uri);
} else {
mHandler.post(new NotificationRunnable(selfChange, uri));
}
}
然后这个消息会运行其内部类NotificationRunnable的run方法,
public void run() {
ContentObserver.this.onChange(mSelfChange, mUri);
}
This指向的是ContentObserver的实例ChangeObserver,它是CursorAdapter的内部类,也是ContentObserver的子类,由于ConversationListAdapter通过CursorWrapper向ContentObservable注册了ContentObserver,所以当dispatchChange事件时,通过轮询,最终将事件传递给ChangeObserver。
而后,ChangeObserver调用封装类CursorAdapter的onContentChanged,将事件上传。
private class ChangeObserver extends ContentObserver {
public void onChange(boolean selfChange) {
onContentChanged();
}
}
2)onContentChanged
onContentChanged是CursorAdapter的一个方法,主要功能是当数据库内容更新后,重新使用游标查询数据。但这个方法被ConversationListAdapter重载,它给自己的客户提供一个借口类OnContentChangedListener,让客户去实现这个借口类,自己则调用接口类的onContentChanged方法执行客户定义的功能。
protected void onContentChanged() {
if (mCursor != null && !mCursor.isClosed()) {
if (mOnContentChangedListener != null) {
mOnContentChangedListener.onContentChanged(this);
}
}
}
3)ConversationList
ConversationList就是ConversationListAdapter的客户,它实现了接口类,如下,主要是调用startAsyncQuery进行数据的查询。
private final ConversationListAdapter.OnContentChangedListener mContentChangedListener =
new ConversationListAdapter.OnContentChangedListener() {
@Override
public void onContentChanged(ConversationListAdapter adapter) {
if (mIsInActivity) {
mNeedQuery = true;
startAsyncQuery();
}
}
};
并且ConversationList在其onStart向ConversationListAdapter注册了这个接口类监听器:
mListAdapter.setOnContentChangedListener(mContentChangedListener);
所以,当数据库数据改变时,根据上面相关调用流程,完成了Message列表的刷新功能。
BTW,附上游标改变后注册ContentObserver的调用栈:
ContentResolver$CursorWrapperInner(CursorWrapper).registerContentObserver(ContentObserver) line: 178
ConversationListAdapter(CursorAdapter).swapCursor(Cursor) line: 340
ConversationListAdapter(CursorAdapter).changeCursor(Cursor) line: 313
ConversationList$ThreadListQueryHandler.onQueryComplete(int, Object, Cursor) line: 1896
ConversationList$ThreadListQueryHandler(AsyncQueryHandler).handleMessage(Message) line: 344
ConversationList$ThreadListQueryHandler(Handler).dispatchMessage(Message) line: 110
Looper.loop() line: 193
3.3.2 Message列表定位
因为ConversationList是ListActivity,所以可以使用getListView获取ListView,再使用ListView的setSelectionFromTop,将列表显示开始位置指定到特定位置。
需要注意2个问题,
1)
在ConversationList会记录上次浏览列表的起始位置,所以不是随意就能设置好列表的显示位置,如onQueryComplete通过下面代码控制了什么时候可以设置这个起始位置,我们需要根据mSavedFirstVisiblePosition的值和相关逻辑去处理。
if (mSavedFirstVisiblePosition != AdapterView.INVALID_POSITION) {
// Restore the list to its previous position.
getListView().setSelectionFromTop(mSavedFirstVisiblePosition,
mSavedFirstItemOffset);
mSavedFirstVisiblePosition = AdapterView.INVALID_POSITION;
}
结合列表的创建过程,我们知道,当同步绘制列表时,是从mSyncPosition开始的,
fillSpecific(mSyncPosition, mSpecificTop);
而mSyncPosition正是setSelectionFromTop里传递进去的,所以列表起始位置只能通过setSelectionFromTop来设置。
public void setSelectionFromTop(int position, int y) {
…
mSyncPosition = position;
…
}
2)
游标的更新,当数据库数据变化后,需要更新游标,才能获取到正确的值。下面是onQueryComplete完成后,用changeCursor替换掉旧游标的过程,防止游标数据过期。
ConversationListAdapter(CursorAdapter).swapCursor(Cursor) line: 340
ConversationListAdapter(CursorAdapter).changeCursor(Cursor) line: 313
ConversationList$ThreadListQueryHandler.onQueryComplete(int, Object, Cursor) line: 1896
ConversationList$ThreadListQueryHandler(AsyncQueryHandler).handleMessage(Message) line: 344
ConversationList$ThreadListQueryHandler(Handler).dispatchMessage(Message) line: 110
Looper.loop() line: 193
在列表创建时,CursorAdapter. getView的游标最初是在CursorAdapte创建时传入的,之后可以通过swapCursor的方法更新。
6572_message_conversationList_详细分析
目 录
2. ConversationList
2.1.2.1 ConversationListAdapter
2.2.1 列表项click
2.2.2 Long Press click to Delete
2.3.1 会话ID创建
3. ConversationListItem
3.1.2.1 ConversationListItem实例创建
1. 概要
本文主要分析短信列表界面的主要控件的界面显示和处理逻辑。
2. ConversationList
2.1 ConversationList
2.1.1 Layout分析
ConversationList对应的layout是conversation_list_screen,它主要分3部分,第一部分是做全选的控制,第二部分是list列表,第三部分是列表为空的布局。具体的表现形式如下,布局文件就不再展开了。其中list列表将在另外的章节分析。
2.1.2 代码分析
2.1.2.1 ConversationListAdapter
ConversationListAdapter是ConversationListItem的数据适配器,在ConversationList.onCreate里创建,它通过bindView将会话信息绑定到item的layout上。
其创建过程如下,
private void initListAdapter() {
mListAdapter = new ConversationListAdapter(this, null);
/** M: now this code is useless and will lead to a JE, comment it.
* listener is set in onStart
*/
//mListAdapter.setOnContentChangedListener(mContentChangedListener);
setListAdapter(mListAdapter);
getListView().setRecyclerListener(mListAdapter);
}
因为ConversationList继承自ListActivity,setListAdapter将Adapter实例设置给ListActivity,这里会调用ensureList,它会加载一个布局,确保有view能用来对应Adapter。
public void setListAdapter(ListAdapter adapter) {
synchronized (this) {
ensureList();
mAdapter = adapter;
mList.setAdapter(adapter);
}
}
private void ensureList() {
if (mList != null) {
return;
}
setContentView(com.android.internal.R.layout.list_content_simple);
}
而实际上,mList已经在 onContentChanged被初始化为Layout里对应的ListView,
mList = (ListView)findViewById(com.android.internal.R.id.list);
这个ListView就是Message列表的容器了。
2.2 功能分析
2.2.1 列表项click
单击列表项的调用栈如下:
ConversationList.onListItemClick(ListView, View, int, long) line: 1086
ListActivity$2.onItemClick(AdapterView, View, int, long) line: 319
ListView(AdapterView).performItemClick(View, int, long) line: 298
ListView(AbsListView).performItemClick(View, int, long) line: 1128
AbsListView$PerformClick.run() line: 2815
AbsListView$1.run() line: 3574
Handler.handleCallback(Message) line: 800
ViewRootImpl$ViewRootHandler(Handler).dispatchMessage(Message) line: 100
2.2.2 Long Press click to Delete
长按item删除会话流程如下:
长按出现选择菜单,
mConvListOnCreateContextMenuListener
onCreateContextMenu
点击删除
ConversationList.confirmDeleteThreads(Collection, AsyncQueryHandler) line:
ConversationList.confirmDeleteThread(long, AsyncQueryHandler) line: 1320
ConversationList.onContextItemSelected(MenuItem) line: 1247
ConversationList(Activity).onMenuItemSelected(int, MenuItem) line: 2584
PhoneWindow$DialogMenuCallback.onMenuItemSelected(MenuBuilder, MenuItem) line:
开始删除之前,要做一个后台查询,看是否有lock住的消息,Conversation.startQueryHaveLockedMessages,
之后是后台查询handler.startQuery:
AsyncQueryHandler
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);
msg.arg1 = EVENT_ARG_QUERY;
WorkerArgs args = new WorkerArgs();
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);
}
这个查询过程将消息发给mWorkerThreadHandler,这是异步查询AsyncQueryHandler自己的handler,它封装完消息EVENT_ARG_QUERY后,再发送出去,
Message reply = args.handler.obtainMessage(token);
reply.obj = args;
reply.arg1 = msg.arg1;
reply.sendToTarget();
这个消息的接收对象的设置见args.handler = this; ,在本例中,这个handler是ThreadListQueryHandler。
数据库查询完成后执行onQueryComplete(HAVE_LOCKED_MESSAGES_TOKEN),
ConversationList$ThreadListQueryHandler.onQueryComplete(int, Object, Cursor) line: 1610
ConversationList$ThreadListQueryHandler(AsyncQueryHandler).handleMessage(Message) line: 344
ConversationList$ThreadListQueryHandler(Handler).dispatchMessage(Message) line: 107
Looper.loop() line: 194
之后会给出一个确认删除对话框confirmDeleteThreadDialog
在弹出的确认对话框中,选择删除,执行listener函数。
ConversationList$DeleteThreadListener.onClick(DialogInterface, int) line: 1484
AlertController$ButtonHandler.handleMessage(Message) line: 176
之后开始删除Conversation.startDelete
最后onDeleteComplete
响应函数会触发一系列其他的数据库操作,如查询list,查询unread等,
AsyncQueryHandler$WorkerHandler.handleMessage(Message) line: 120
AsyncQueryHandler$WorkerHandler(Handler).dispatchMessage(Message) line: 107
【Tips】
它的threadIds是在点击删除功能时,onContextItemSelected设置的,和长按的时候在onCreateContextMenu里选择的有时间差,不一定是同一个,在会话列表更新的时候,会有影响,所以会误删。
所以在onCreateContextMenu里要记录thread id。
2.3 数据操作
2.3.1 会话ID创建
Message列表是以会话为item进行显示的,会话是以为ID记录的,我们分析一下各种会话ID是如何创建的,
应用在CBMessageReceiverService.java收到小区广播后,使用handleCBMessageReceived方法进行处理,其中insertMessage是用来存储CB的。
insertMessage会调用CbProvider的insert方法
public Uri insert(Uri url, ContentValues initialValues) {
Uri result = null;
// TODO Check Permission
// checkPermission();
SQLiteDatabase db = mOpenHelper.getWritableDatabase();
SQLiteDatabase dbmmssms = mMmsSmsOpenHelper.getWritableDatabase();
int match = URI_MATCHER.match(url);
ContentValues values;
long rowID;
String table = null;
Log.d(TAG, " insert match = "+match);
switch (match) {
case URL_MESSAGES:
//table = CbDatabaseHelper.CBMESSAGE_TABLE;
table = MmsSmsDatabaseHelper.TABLE_CELLBROADCAST;
values = internalInsertMessages(initialValues);
rowID = dbmmssms.insert(table, null, values);
Log.d(TAG, "insert to cellbroadcast " + values);
if (rowID > 0) {
result = Uri.parse("content://messages/" + rowID);
notifyChange();
return result;
}
break;
…
}
在这里,我们重点关注一下CB Message的ThreadId是怎样获取的,在internalInsertMessages里面,显然threadId不是我们主动填充进去的,所以是通过getOrCreateThreadId获得的。
private ContentValues internalInsertMessages(ContentValues initialValues) {
ContentValues values;
if (initialValues != null) {
values = new ContentValues(initialValues);
} else {
values = new ContentValues();
}
Long threadId = values.getAsLong(EncapsulatedTelephony.SmsCb.THREAD_ID);
String address = values.getAsString(EncapsulatedTelephony.SmsCb.CHANNEL_ID);
if (((threadId == null) || (threadId == 0)) && (address != null)) {
values.put(EncapsulatedTelephony.SmsCb.THREAD_ID,
getOrCreateThreadId(address));
}
…
}
getOrCreateThreadId代码如下,它封装一个URI地址,包含了threadID和参数recipient、cellbroadcast,并使用SqliteWrapper.query进行查询,
private String getOrCreateThreadId(String address) {
//Uri.Builder uriBuilder = THREAD_ID_URI.buildUpon();
Uri.Builder uriBuilder = (Uri.parse("content://mms-sms/threadID")).buildUpon();
uriBuilder.appendQueryParameter("recipient", address);
uriBuilder.appendQueryParameter("cellbroadcast", address);
Uri uri = uriBuilder.build();
// TODO need replace with helper interface.
Cursor cursor = null;
long token = Binder.clearCallingIdentity();
try {
cursor = SqliteWrapper.query(this.getContext(), this
.getContext().getContentResolver(), uri, ID_PROJECTION, null,
null, null);
…
if (cursor.moveToFirst()) {
return String.valueOf(cursor.getLong(0));
…
}
SqliteWrapper会调用到ContentResolver,ContentResolver则最终调用到MmsSmsProvider,
在MmsSmsProvider.query里面,也是通过URI match找到操作码URI_THREAD_ID,根据参数cellbroadcast,使用getCBThreadId给CB查找或创建其ThreadId。
Threads表内容如下所示,里面的关键字段有_id ,recipient_ids,要想找到合适的ThreadId,只有通过recipient_ids来查询,
所以在getCBThreadId方法里面,通过getAddressIds从canonical_addresses表里面找到和号码匹配的recipient_ids列表,再到Threads表里面找到recipient_ids相同和type是小区广播的_id,即是我们需要的ThreadId,使用的查询语句如下。
String queryString = "SELECT _id FROM threads " + "WHERE type=" + EncapsulatedTelephony.Threads.CELL_BROADCAST_THREAD + " AND recipient_ids=?";
如果没有满足条件的ThreadId,调用insertThread()创建一个新的thread。
在Message列表界面通过按钮新建短信时,执行createNewMessage(),通过createIntent传递一个threadId=0的ID过去,
private void createNewMessage() {
startActivity(ComposeMessageActivity.createIntent(this, 0));
}
public static Intent createIntent(Context context, long threadId) {
Intent intent = new Intent(context, ComposeMessageActivity.class);
if (threadId > 0) {
intent.setData(Conversation.getUri(threadId));
}
return intent;
}
因为threadId为0,在会话创建中会获取或创建一个threadId,具体过程和下节相同。
在联系人的详情界面,点击发送短信,因为有号码存在,所以在ComposeMessageActivity.Oncreate方法里面会去查询或创建一个threadId。
onCreate的initActivityState里,有这么一段代码是获取会话实例的,mConversation.get通过号码获取到会话实例,
Uri intentData = intent.getData();
/// M: Code analyze 034, If intent is SEND,just create a new empty thread,
/// otherwise Conversation.get() will throw exception.
String action = intent.getAction();
if (intentData != null && (TextUtils.isEmpty(action) ||
!action.equals(Intent.ACTION_SEND))) {
/// M: group-contact send message
// try to get a conversation based on the data URI passed to our intent.
if (intentData.getPathSegments().size() < 2) {
mConversation = mConversation.get(getApplicationContext(),ContactList.getByNumbers(
getStringForMultipleRecipients(Conversation.getRecipients(intentData)),
false /* don't block */, true /* replace number */),false);
} else {
mConversation = Conversation.get(getApplicationContext(), intentData, false);
}
/// @}
mWorkingMessage.setText(getBody(intentData));
Conversation get代码如下,如果号码为空则新建会话,如果缓冲里存储了对应号码的会话,则使用缓冲里的数据,如果条件都不满足,则需要去数据库查询到ID,并新建会话,之后将新会话存放到缓冲。
public static Conversation get(Context context, ContactList recipients, boolean allowQuery) {
if (recipients.size() < 1) {
return createNew(context);
}
Conversation conv = Cache.get(recipients);
if (conv != null) {
return conv;
}
long threadId = getOrCreateThreadId(context, recipients);
conv = new Conversation(context, threadId, allowQuery);
。。。
Cache.put(conv);
}
getOrCreateThreadId用来获取ThreadId,由Threads.getOrCreateThreadId实现,这个方法在Telephony.java里面。
getOrCreateThreadId类似前面提到的小区广播的实现过程,先封装一个"content://mms-sms/threadID"的URI,但只使用一个参数recipient,
在MmsSmsProvider.query里面,也是通过URI match找到操作码URI_THREAD_ID,,使用getThreadId查找或创建其ThreadId,getAddressIds获取到匹配的recipientIds,再取Threads表里查询,如果查询到,返回游标,如果没有记录,则用insertThread向Threads里插入记录,再查询到ThreadId。
private synchronized Cursor getThreadId(List<String> recipients) {
Set<Long> addressIds = getAddressIds(recipients);
。。。
recipientIds = Long.toString(addressId);
。。。
String[] selectionArgs = new String[] { recipientIds };
SQLiteDatabase db = mOpenHelper.getReadableDatabase();
db.beginTransaction();
Cursor cursor = null;
try {
// Find the thread with the given recipients
cursor = db.rawQuery(THREAD_QUERY, selectionArgs);
if (cursor.getCount() == 0) {
// No thread with those recipients exists, so create the thread.
cursor.close();
Log.d(LOG_TAG, "getThreadId: create new thread_id for recipients " + recipients);
insertThread(recipientIds, recipients);
// The thread was just created, now find it and return it.
cursor = db.rawQuery(THREAD_QUERY, selectionArgs);
。。。
}
3. ConversationListItem
3.1 ConversationListItem
3.1.1 Layout分析
如前所述,短信记录的每条信息显示是使用conversation_list_item.xml布局的,在本例中,其直观显示是如下形式的一条短信记录,
conversation_list_item.xml文件如下,它是一个ConversationListItem类型的布局,
(需要注意不同屏幕使用这个文件的位置,本例中在packages\apps\Mms\res\layout-mdpi下面,不是packages\apps\Mms\res\layout)
conversation_list_item.xml
<?xml version="1.0" encoding="utf-8"?>
<com.android.mms.ui.ConversationListItem xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="?android:attr/listPreferredItemHeight"
android:background="?android:attr/activatedBackgroundIndicator" >
<CheckBox
android:id="@+id/select_box"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_centerVertical="true"
android:clickable="false"
android:focusable="false"
android:focusableInTouchMode="false" />
<com.android.mms.ui.MmsQuickContactBadge
android:id="@+id/avatar"
android:visibility="gone"
android:layout_centerVertical="true"
android:layout_toEndOf="@id/select_box"
style="?android:attr/quickContactBadgeStyleWindowLarge" />
<ImageView
android:id="@+id/presence"
android:visibility="invisible"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_alignParentTop="true"
android:src="@drawable/ic_unread_label"/>
<LinearLayout
android:id="@+id/iconlist"
android:layout_alignParentTop="true"
android:layout_alignParentEnd="true"
android:layout_marginTop="7dip"
android:layout_marginEnd="10dip"
android:orientation="horizontal"
android:layout_height="wrap_content"
android:layout_width="wrap_content" >
<ImageView android:id="@+id/draft"
android:visibility="gone"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:src="@drawable/ic_draft" />
<ImageView android:id="@+id/error"
android:paddingStart="6dp"
android:visibility="gone"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:src="@drawable/ic_list_alert_sms_failed" />
<ImageView android:id="@+id/attachment"
android:paddingStart="6dp"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:visibility="gone"
android:src="@drawable/ic_attachment_universal_small" />
<ImageView android:id="@+id/mute"
android:paddingStart="6dp"
android:visibility="gone"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:src="@drawable/ipmsg_silent" />
</LinearLayout>
<RelativeLayout
android:layout_alignParentTop="true"
android:layout_marginTop="4dip"
android:layout_marginStart="12dip"
android:layout_toEndOf="@id/avatar"
android:layout_toStartOf="@id/iconlist"
android:gravity="center_vertical"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView android:id="@+id/unread"
android:background="@drawable/ipmsg_message_box"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:textSize="12sp"
android:textColor="@color/text_color_unread"
android:singleLine="true"
android:layout_alignParentEnd="true" />
<RelativeLayout android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:layout_toStartOf="@id/unread">
<TextView android:id="@+id/draft_and_smgcount"
android:textAppearance="?android:attr/textAppearanceMedium"
android:layout_alignParentEnd="true"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="6dip"
android:singleLine="true" />
<com.android.mms.util.AlwaysMarqueeTextView android:id="@+id/from"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceMedium"
android:singleLine="true"
android:layout_alignParentStart="true"
android:layout_toStartOf="@id/draft_and_smgcount"
android:layout_marginEnd="6dip"
android:ellipsize="marquee" />
</RelativeLayout>
</RelativeLayout>
<TextView android:id="@+id/date"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:textSize= "12sp"
android:textColor="@color/dlg_text_counter_color"
android:layout_alignParentBottom="true"
android:layout_alignParentEnd="true"
android:layout_marginEnd="10dip"
android:layout_marginBottom="4dip"
android:gravity="bottom" />
<TextView android:id="@+id/subject"
android:layout_height="wrap_content"
android:layout_width="fill_parent"
android:textSize= "12sp"
android:layout_alignParentBottom="true"
android:layout_toEndOf="@id/avatar"
android:layout_toStartOf="@id/date"
android:layout_marginStart="12dip"
android:layout_marginBottom="4dip"
android:gravity="bottom"
android:textColor="@color/dlg_text_counter_color"
android:lines="2"
android:ellipsize="end" />
</com.android.mms.ui.ConversationListItem>
其对应的布局图形如下,图中左边是layout组成,右上是layout布局表现,右下是对layout里面的子view编号,便于理解。
分析xml文件,并对照图例可以看出,
ConversationListItem是整个item布局类,表现形式见下图左边,它指定了高度
android:layout_height="?android:attr/listPreferredItemHeight"
这个布局由6个同级view组成,见下图中间及右上,其中
checkbox供选择使用,见右下数字0,alignParentStart表示它在开头位置,
android:layout_alignParentStart="true"
MmsQuickContactBadge做头像显示,见右下数字1,
ImageView做未读标识等用途,见右下数字2,通过下面的属性将它放置在最右上的位置
android:layout_alignParentEnd="true"
android:layout_alignParentTop="true"
LinearLayout是个组合视图,见右下数字3和iconlist小图,它有四个子ImageView表示短信的信息状态,具体名字见iconlist小图。注意四个子ImageView表示同时最多只能显示四个图标,不是表示短信只有这四种状态。通过下面属性,将它放置在MmsQuickContactBadge后面。
android:orientation="horizontal"
<iconlist>
RelativeLayout是个组合视图,见右下数字4和Text Title小图,包含3个TextView,用来显示Title和消息数目、未读消息数目。在RelativeLayout,通过下面的属性设置,将RelativeLayout(index4的view)和上面的LinearLayout(index3的view)并行放置在同一行。
android:layout_toEndOf="@id/avatar"
android:layout_toStartOf="@id/iconlist"
TextView显示日期,见右下数字5,用下面2个属性表示它的位置在右下,
android:layout_alignParentBottom="true"
android:layout_alignParentEnd="true"
TextView 显示短线截取的部分内容,见右下数字6,用下面3个属性表明了它的位置。
android:layout_alignParentBottom="true"
android:layout_toEndOf="@id/avatar"
android:layout_toStartOf="@id/date"
<Text Content>
3.1.2 代码分析
3.1.2.1 ConversationListItem实例创建
ConversationListItem实例创建栈如下,因为有缓存使用,不一定每次进入列表都会创建这个类实例,所以不一定能调试到,要在此设置断点调试,有一个小技巧,就是将mms进程终止掉,让其自动重启(可通过发送短信或cb的方法加快重启过程),这样就可以进行调试。
ConversationListItem.<init>(Context, AttributeSet) line: 130
Constructor.constructNative(Object[], Class, Class[], int, boolean) line: not available [native method]
Constructor.newInstance(Object...) line: 423
PhoneLayoutInflater(LayoutInflater).createView(String, String, AttributeSet) line: 594
PhoneLayoutInflater(LayoutInflater).createViewFromTag(View, String, AttributeSet) line: 696
PhoneLayoutInflater(LayoutInflater).inflate(XmlPullParser, ViewGroup, boolean) line: 469
PhoneLayoutInflater(LayoutInflater).inflate(int, ViewGroup, boolean) line: 397
ConversationListAdapter.newView(Context, Cursor, ViewGroup) line: 103
ConversationListAdapter(CursorAdapter).getView(int, View, ViewGroup) line: 250
ListView(AbsListView).obtainView(int, boolean[]) line: 2338
ListView.makeAndAddView(int, int, boolean, int, boolean) line: 1812
ListView.fillDown(int, int) line: 698
ListView.fillFromTop(int) line: 759
ListView.layoutChildren() line: 1631
ListView(AbsListView).onLayout(boolean, int, int, int, int) line: 2149
ConversationListItem实例创建的主要过程分析如下,在getView来获取view的时候,因为缓冲没有数据,会使用newView新建一个view,
public View getView(int position, View convertView, ViewGroup parent) {
View v;
if (convertView == null) {
v = newView(mContext, mCursor, parent);
} else {
v = convertView;
}
bindView(v, mContext, mCursor);
return v;
}
newView实际的过程是inflate一个布局文件,这个布局文件就是我们分析的layout,它是ConversationListItem形式的,我们知道,在inflate创建布局文件时,会根据节点创建view实例,所以会使用类的构造类Constructor来创建ConversationListItem实例。
public View newView(Context context, Cursor cursor, ViewGroup parent) {
if (LOCAL_LOGV) Log.v(TAG, "inflating new view");
return mFactory.inflate(R.layout.conversation_list_item, parent, false);
}
Checkbox的主要作用是用来勾选短信列表的某一个item,并进行删除操作的。
对于checkbox,在ConversationListItem里面定义了一个引用mSelectBox,
private CheckBox mSelectBox;
并在onFinishInflate里面初始化它,关联到Layout里面的select_box,
mSelectBox = (CheckBox) findViewById(R.id.select_box);
当列表项被点击时,执行ConversationList.onListItemClick(ListView, View, int, long)这个方法,在里面添加下面代码,当处于删除模式时,如下,将Conversation设置为check状态,然后更新界面,
if (mListAdapter != null && mDeleteMode) {
if (conv.isChecked()) {
conv.setIsChecked(false);
} else {
conv.setIsChecked(true);
}
mListAdapter.notifyDataSetChanged();
updateSelectView();
return;
}
更新界面时,执行ConversationListItem.bind(Context, Conversation) ,每加载一个item项都会执行一次bind,在这里,会根据mSelectMode 决定checkbox是否显示,根据Conversation的状态决定是否选中。
所以实际上,不是我们主观上看到的那样由checkbox决定了当前项目是否选中,而是当前项选中后决定checkbox的显示状态。
if (mSelectMode) {
mSelectBox.setVisibility(View.VISIBLE);
mAvatarView.setClickable(false);
} else {
mSelectBox.setVisibility(View.GONE);
mAvatarView.setClickable(true);
}
if (mConversation.isChecked()) {
mSelectBox.setChecked(true);
} else {
mSelectBox.setChecked(false);
}
上面就是checkbox在代码里的处理过程。
R.id.avatar用来显示发件人头像,定义如下,MmsQuickContactBadge继承自QuickContactBadge,再往上是ImageView,一个通用的视图组件。
private MmsQuickContactBadge mAvatarView;
其实例获取在ConversationListItem的onFinishInflate里面,
mAvatarView = (MmsQuickContactBadge) findViewById(R.id.avatar);
如果希望AvatarView能够被点击,相应事件,则可以看到在bind有如下代码,表示在删除模式下不能被点击,在列表模式下可以被点击。
if (mSelectMode) {
mAvatarView.setClickable(false);
} else {
mAvatarView.setClickable(true);
}
AvatarView被点击之后,执行的调用栈如下,可见是由View直接调用了MmsQuickContactBadge类的onClick,并不是ConversationList或ConversationListItem里做了onClick的相关处理。
MmsQuickContactBadge(QuickContactBadge).onClick(View) line: 217
MmsQuickContactBadge.onClick(View) line: 75
MmsQuickContactBadge(View).performClick() line: 4212
View$PerformClick.run() line: 17476
Handler.handleCallback(Message) line: 800
ViewRootImpl$ViewRootHandler(Handler).dispatchMessage(Message) line: 100
QuickContactBadge类的onClick代码如下,
@Override
public void onClick(View v) {
if (mContactUri != null) {
QuickContact.showQuickContact(getContext(), QuickContactBadge.this, mContactUri,
QuickContact.MODE_LARGE, mExcludeMimes);
} else if (mContactEmail != null) {
mQueryHandler.startQuery(TOKEN_EMAIL_LOOKUP_AND_TRIGGER, mContactEmail,
Uri.withAppendedPath(Email.CONTENT_LOOKUP_URI, Uri.encode(mContactEmail)),
EMAIL_LOOKUP_PROJECTION, null, null, null);
} else if (mContactPhone != null) {
mQueryHandler.startQuery(TOKEN_PHONE_LOOKUP_AND_TRIGGER, mContactPhone,
Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI, mContactPhone),
PHONE_LOOKUP_PROJECTION, null, null, null);
} else {
// If a contact hasn't been assigned, don't react to click.
return;
}
}
我们当前的代码走mContactPhone路径,mContactPhone是在assignContactFromPhone里面赋值的,assignContactFromPhone则是通过ConversationListItem的updateAvatarView调用的(传入的number就赋值给mContactPhone),
mAvatarView.assignContactFromPhone(number, true);
updateAvatarView可以被updateFromView和bind调用,这两个方法我们在前面有过描述。所以从整个过程来看,我们可以看出avatar被点击和其需要的数据来源。
3.2 绘制分析
3.2.1 Message列表创建
如ConversationList的layout描述,Messag列表主要是由list呈现,而list的item项是由ConversationListItem布局的,我们这里讨论每个item项的创建以及整个list的创建。
在使用Cache时,ConversationListItem的创建过程如下,
ConversationListItem.bind(Context, Conversation) line: 480
ConversationListAdapter.bindView(View, Context, Cursor) line: 77
ConversationListAdapter(CursorAdapter).getView(int, View, ViewGroup) line: 250
ListView(AbsListView).obtainView(int, boolean[]) line: 2186
ListView.makeAndAddView(int, int, boolean, int, boolean) line: 1877
ListView.fillSpecific(int, int) line: 1356
ListView.layoutChildren() line: 1671
ListView(AbsListView).onLayout(boolean, int, int, int, int) line: 2037
ListView(View).layout(int, int, int, int) line: 14118
ListView(ViewGroup).layout(int, int, int, int) line: 4467
RelativeLayout.onLayout(boolean, int, int, int, int) line: 1021
从上往下分析该栈:
1)ConversationListItem.bind
ConversationListItem.bind的代码比较多,仔细分析,实际上逻辑比较简单,主要是设置每个view的内容和状态,比如说设置日期的代码如下,
mDateView.setVisibility(VISIBLE);
if(ConversationList.sConversationListOption ==
ConversationList.OPTION_CONVERSATION_LIST_IMPORTANT &&
!conversation.hasUnreadMessages()) {
mDateView.setText(MessageUtils.formatTimeStampStringExtend(context,
conversation.getImpDate()));
}
其数据来源于conversation,我们要进一步分析conversation是怎么传递进来的,以及为什么要调用bind。
2)ConversationListAdapter
ConversationListAdapter 是ConversationList的layout里的list对应的Adapter,它继承自MessageCursorAdapter-CursorAdapter-baseAdapter, 其方法bindView在本例中执行的主要代码如下,其中headerview是一个ConversationListItem引用,是调用方法传递过来的,conversation是根据游标新建的,最后调用bind,所以该方法的作用就是获取到conversation,并用准备将其中的数据赋值到view。
public void bindView(View view, Context context, Cursor cursor) {
ConversationListItem headerView = (ConversationListItem) view;
Conversation conv;
if (!mIsScrolling) {
Conversation.setNeedCacheConv(false);
conv = Conversation.from(context, cursor);
Conversation.setNeedCacheConv(true);
if (mSubjectSingleLine) {
headerView.setSubjectSingleLineMode(true);
}
if (conv != null) {
conv.setIsChecked(sSelectedTheadsId.contains(conv.getThreadId()));
}
headerView.bind(context, conv);
}
}
(CursorAdapter).getView则负责获取到view实例和游标实例。
view的获取有两种方式,一种是调用者传递过来以前缓存的,一种是新创建一个,在ConversationListItem实例创建中有讲到如何创建。
游标实例则在Adapter创建时创建。
3)ListView
在ListView部分,我们从layoutChildren向上分析,向下部分设计到太多ListView的实现机制问题,暂不讨论。
layoutChildren实现ListView子窗口布局,实际上就是list在layout所占部分。
本例从launcher进入Message列表,执行代码路径如下,mSyncPosition是当前item索引值,mSpecificTop是当前layout相对Top的位置。
layoutChildren:
case LAYOUT_SYNC:
sel = fillSpecific(mSyncPosition, mSpecificTop);
fillSpecific在知道位置绘制指定项的item数据,然后上下绘制其他的item。它先通过makeAndAddView完成当前item的创建并添加到layout,fillDown、fillUp则根据layout大小,上下绘制item完成整个list的绘制,使用的方法也是makeAndAddView,adjustViewsUpOrDown调整item位置。
/**
* Put a specific item at a specific location on the screen and then build
* up and down from there.
*
* @param position The reference view to use as the starting point
* @param top Pixel offset from the top of this view to the top of the
* reference view.
*
* @return The selected view, or null if the selected view is outside the
* visible area.
*/
private View fillSpecific(int position, int top) {
boolean tempIsSelected = position == mSelectedPosition;
View temp = makeAndAddView(position, top, true, mListPadding.left, tempIsSelected);
…
below = fillDown(position + 1, temp.getBottom() + dividerHeight);
// This will correct for the bottom of the last view not touching the bottom of the list
adjustViewsUpOrDown();
above = fillUp(position - 1, temp.getTop() - dividerHeight);
int childCount = getChildCount();
if (childCount > 0) {
correctTooLow(childCount);
}
}
顾名思义,makeAndAddView就是新建一个view,并将它添加到list里面。obtainView负责创建view,setupChild负责将view添加到layout,并设置好view的位置和相关属性。
private View makeAndAddView(int position, int y, boolean flow, int childrenLeft,
boolean selected) {
View child;
child = obtainView(position, mIsScrap);
// This needs to be positioned and measured
setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]);
return child;
}
obtainView将对应位置的view和数据关联起来,并返回view。它通过mRecycler获取到缓存的view,再通过mAdapter.getView将数据填充到view,mAdapter.getView即是我们前面分析到的ConversationListAdapter(CursorAdapter).getView。
至此,Message列表的创建过程就明了了。
3.3 功能分析
3.3.1 Message列表更新
当数据库内容发生变化(包括新短信、删除、发送状态改变等),就会触发onContentChanged,其调用栈如下:
ConversationList$2.onContentChanged(ConversationListAdapter) line: 474
ConversationListAdapter.onContentChanged() line: 118
CursorAdapter$ChangeObserver.onChange(boolean) line: 463
CursorAdapter$ChangeObserver(ContentObserver).onChange(boolean, Uri) line: 129
ContentObserver$NotificationRunnable.run() line: 180
Handler.handleCallback(Message) line: 808
Handler.dispatchMessage(Message) line: 103
Looper.loop() line: 193
从下往上分析,
1)ContentObserver
我们已经很熟悉handleCallback处理的是Post的消息,消息一般是runable,所以可以看到ContentObserver. dispatchChange有post这一消息的过程(dispatchChange的触发过程比较复杂,我们将在其他章节介绍,本例中当数据库内容变化时会触发这个流程)。
public final void dispatchChange(boolean selfChange, Uri uri) {
if (mHandler == null) {
onChange(selfChange, uri);
} else {
mHandler.post(new NotificationRunnable(selfChange, uri));
}
}
然后这个消息会运行其内部类NotificationRunnable的run方法,
public void run() {
ContentObserver.this.onChange(mSelfChange, mUri);
}
This指向的是ContentObserver的实例ChangeObserver,它是CursorAdapter的内部类,也是ContentObserver的子类,由于ConversationListAdapter通过CursorWrapper向ContentObservable注册了ContentObserver,所以当dispatchChange事件时,通过轮询,最终将事件传递给ChangeObserver。
而后,ChangeObserver调用封装类CursorAdapter的onContentChanged,将事件上传。
private class ChangeObserver extends ContentObserver {
public void onChange(boolean selfChange) {
onContentChanged();
}
}
2)onContentChanged
onContentChanged是CursorAdapter的一个方法,主要功能是当数据库内容更新后,重新使用游标查询数据。但这个方法被ConversationListAdapter重载,它给自己的客户提供一个借口类OnContentChangedListener,让客户去实现这个借口类,自己则调用接口类的onContentChanged方法执行客户定义的功能。
protected void onContentChanged() {
if (mCursor != null && !mCursor.isClosed()) {
if (mOnContentChangedListener != null) {
mOnContentChangedListener.onContentChanged(this);
}
}
}
3)ConversationList
ConversationList就是ConversationListAdapter的客户,它实现了接口类,如下,主要是调用startAsyncQuery进行数据的查询。
private final ConversationListAdapter.OnContentChangedListener mContentChangedListener =
new ConversationListAdapter.OnContentChangedListener() {
@Override
public void onContentChanged(ConversationListAdapter adapter) {
if (mIsInActivity) {
mNeedQuery = true;
startAsyncQuery();
}
}
};
并且ConversationList在其onStart向ConversationListAdapter注册了这个接口类监听器:
mListAdapter.setOnContentChangedListener(mContentChangedListener);
所以,当数据库数据改变时,根据上面相关调用流程,完成了Message列表的刷新功能。
BTW,附上游标改变后注册ContentObserver的调用栈:
ContentResolver$CursorWrapperInner(CursorWrapper).registerContentObserver(ContentObserver) line: 178
ConversationListAdapter(CursorAdapter).swapCursor(Cursor) line: 340
ConversationListAdapter(CursorAdapter).changeCursor(Cursor) line: 313
ConversationList$ThreadListQueryHandler.onQueryComplete(int, Object, Cursor) line: 1896
ConversationList$ThreadListQueryHandler(AsyncQueryHandler).handleMessage(Message) line: 344
ConversationList$ThreadListQueryHandler(Handler).dispatchMessage(Message) line: 110
Looper.loop() line: 193
3.3.2 Message列表定位
因为ConversationList是ListActivity,所以可以使用getListView获取ListView,再使用ListView的setSelectionFromTop,将列表显示开始位置指定到特定位置。
需要注意2个问题,
1)
在ConversationList会记录上次浏览列表的起始位置,所以不是随意就能设置好列表的显示位置,如onQueryComplete通过下面代码控制了什么时候可以设置这个起始位置,我们需要根据mSavedFirstVisiblePosition的值和相关逻辑去处理。
if (mSavedFirstVisiblePosition != AdapterView.INVALID_POSITION) {
// Restore the list to its previous position.
getListView().setSelectionFromTop(mSavedFirstVisiblePosition,
mSavedFirstItemOffset);
mSavedFirstVisiblePosition = AdapterView.INVALID_POSITION;
}
结合列表的创建过程,我们知道,当同步绘制列表时,是从mSyncPosition开始的,
fillSpecific(mSyncPosition, mSpecificTop);
而mSyncPosition正是setSelectionFromTop里传递进去的,所以列表起始位置只能通过setSelectionFromTop来设置。
public void setSelectionFromTop(int position, int y) {
…
mSyncPosition = position;
…
}
2)
游标的更新,当数据库数据变化后,需要更新游标,才能获取到正确的值。下面是onQueryComplete完成后,用changeCursor替换掉旧游标的过程,防止游标数据过期。
ConversationListAdapter(CursorAdapter).swapCursor(Cursor) line: 340
ConversationListAdapter(CursorAdapter).changeCursor(Cursor) line: 313
ConversationList$ThreadListQueryHandler.onQueryComplete(int, Object, Cursor) line: 1896
ConversationList$ThreadListQueryHandler(AsyncQueryHandler).handleMessage(Message) line: 344
ConversationList$ThreadListQueryHandler(Handler).dispatchMessage(Message) line: 110
Looper.loop() line: 193
在列表创建时,CursorAdapter. getView的游标最初是在CursorAdapte创建时传入的,之后可以通过swapCursor的方法更新。
目 录
2. ConversationList
2.1.2.1 ConversationListAdapter
2.2.1 列表项click
2.2.2 Long Press click to Delete
2.3.1 会话ID创建
3. ConversationListItem
3.1.2.1 ConversationListItem实例创建
1. 概要
本文主要分析短信列表界面的主要控件的界面显示和处理逻辑。
2. ConversationList
2.1 ConversationList
2.1.1 Layout分析
ConversationList对应的layout是conversation_list_screen,它主要分3部分,第一部分是做全选的控制,第二部分是list列表,第三部分是列表为空的布局。具体的表现形式如下,布局文件就不再展开了。其中list列表将在另外的章节分析。
2.1.2 代码分析
2.1.2.1 ConversationListAdapter
ConversationListAdapter是ConversationListItem的数据适配器,在ConversationList.onCreate里创建,它通过bindView将会话信息绑定到item的layout上。
其创建过程如下,
private void initListAdapter() {
mListAdapter = new ConversationListAdapter(this, null);
/** M: now this code is useless and will lead to a JE, comment it.
* listener is set in onStart
*/
//mListAdapter.setOnContentChangedListener(mContentChangedListener);
setListAdapter(mListAdapter);
getListView().setRecyclerListener(mListAdapter);
}
因为ConversationList继承自ListActivity,setListAdapter将Adapter实例设置给ListActivity,这里会调用ensureList,它会加载一个布局,确保有view能用来对应Adapter。
public void setListAdapter(ListAdapter adapter) {
synchronized (this) {
ensureList();
mAdapter = adapter;
mList.setAdapter(adapter);
}
}
private void ensureList() {
if (mList != null) {
return;
}
setContentView(com.android.internal.R.layout.list_content_simple);
}
而实际上,mList已经在 onContentChanged被初始化为Layout里对应的ListView,
mList = (ListView)findViewById(com.android.internal.R.id.list);
这个ListView就是Message列表的容器了。
2.2 功能分析
2.2.1 列表项click
单击列表项的调用栈如下:
ConversationList.onListItemClick(ListView, View, int, long) line: 1086
ListActivity$2.onItemClick(AdapterView, View, int, long) line: 319
ListView(AdapterView).performItemClick(View, int, long) line: 298
ListView(AbsListView).performItemClick(View, int, long) line: 1128
AbsListView$PerformClick.run() line: 2815
AbsListView$1.run() line: 3574
Handler.handleCallback(Message) line: 800
ViewRootImpl$ViewRootHandler(Handler).dispatchMessage(Message) line: 100
2.2.2 Long Press click to Delete
长按item删除会话流程如下:
长按出现选择菜单,
mConvListOnCreateContextMenuListener
onCreateContextMenu
点击删除
ConversationList.confirmDeleteThreads(Collection, AsyncQueryHandler) line:
ConversationList.confirmDeleteThread(long, AsyncQueryHandler) line: 1320
ConversationList.onContextItemSelected(MenuItem) line: 1247
ConversationList(Activity).onMenuItemSelected(int, MenuItem) line: 2584
PhoneWindow$DialogMenuCallback.onMenuItemSelected(MenuBuilder, MenuItem) line:
开始删除之前,要做一个后台查询,看是否有lock住的消息,Conversation.startQueryHaveLockedMessages,
之后是后台查询handler.startQuery:
AsyncQueryHandler
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);
msg.arg1 = EVENT_ARG_QUERY;
WorkerArgs args = new WorkerArgs();
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);
}
这个查询过程将消息发给mWorkerThreadHandler,这是异步查询AsyncQueryHandler自己的handler,它封装完消息EVENT_ARG_QUERY后,再发送出去,
Message reply = args.handler.obtainMessage(token);
reply.obj = args;
reply.arg1 = msg.arg1;
reply.sendToTarget();
这个消息的接收对象的设置见args.handler = this; ,在本例中,这个handler是ThreadListQueryHandler。
数据库查询完成后执行onQueryComplete(HAVE_LOCKED_MESSAGES_TOKEN),
ConversationList$ThreadListQueryHandler.onQueryComplete(int, Object, Cursor) line: 1610
ConversationList$ThreadListQueryHandler(AsyncQueryHandler).handleMessage(Message) line: 344
ConversationList$ThreadListQueryHandler(Handler).dispatchMessage(Message) line: 107
Looper.loop() line: 194
之后会给出一个确认删除对话框confirmDeleteThreadDialog
在弹出的确认对话框中,选择删除,执行listener函数。
ConversationList$DeleteThreadListener.onClick(DialogInterface, int) line: 1484
AlertController$ButtonHandler.handleMessage(Message) line: 176
之后开始删除Conversation.startDelete
最后onDeleteComplete
响应函数会触发一系列其他的数据库操作,如查询list,查询unread等,
AsyncQueryHandler$WorkerHandler.handleMessage(Message) line: 120
AsyncQueryHandler$WorkerHandler(Handler).dispatchMessage(Message) line: 107
【Tips】
它的threadIds是在点击删除功能时,onContextItemSelected设置的,和长按的时候在onCreateContextMenu里选择的有时间差,不一定是同一个,在会话列表更新的时候,会有影响,所以会误删。
所以在onCreateContextMenu里要记录thread id。
2.3 数据操作
2.3.1 会话ID创建
Message列表是以会话为item进行显示的,会话是以为ID记录的,我们分析一下各种会话ID是如何创建的,
应用在CBMessageReceiverService.java收到小区广播后,使用handleCBMessageReceived方法进行处理,其中insertMessage是用来存储CB的。
insertMessage会调用CbProvider的insert方法
public Uri insert(Uri url, ContentValues initialValues) {
Uri result = null;
// TODO Check Permission
// checkPermission();
SQLiteDatabase db = mOpenHelper.getWritableDatabase();
SQLiteDatabase dbmmssms = mMmsSmsOpenHelper.getWritableDatabase();
int match = URI_MATCHER.match(url);
ContentValues values;
long rowID;
String table = null;
Log.d(TAG, " insert match = "+match);
switch (match) {
case URL_MESSAGES:
//table = CbDatabaseHelper.CBMESSAGE_TABLE;
table = MmsSmsDatabaseHelper.TABLE_CELLBROADCAST;
values = internalInsertMessages(initialValues);
rowID = dbmmssms.insert(table, null, values);
Log.d(TAG, "insert to cellbroadcast " + values);
if (rowID > 0) {
result = Uri.parse("content://messages/" + rowID);
notifyChange();
return result;
}
break;
…
}
在这里,我们重点关注一下CB Message的ThreadId是怎样获取的,在internalInsertMessages里面,显然threadId不是我们主动填充进去的,所以是通过getOrCreateThreadId获得的。
private ContentValues internalInsertMessages(ContentValues initialValues) {
ContentValues values;
if (initialValues != null) {
values = new ContentValues(initialValues);
} else {
values = new ContentValues();
}
Long threadId = values.getAsLong(EncapsulatedTelephony.SmsCb.THREAD_ID);
String address = values.getAsString(EncapsulatedTelephony.SmsCb.CHANNEL_ID);
if (((threadId == null) || (threadId == 0)) && (address != null)) {
values.put(EncapsulatedTelephony.SmsCb.THREAD_ID,
getOrCreateThreadId(address));
}
…
}
getOrCreateThreadId代码如下,它封装一个URI地址,包含了threadID和参数recipient、cellbroadcast,并使用SqliteWrapper.query进行查询,
private String getOrCreateThreadId(String address) {
//Uri.Builder uriBuilder = THREAD_ID_URI.buildUpon();
Uri.Builder uriBuilder = (Uri.parse("content://mms-sms/threadID")).buildUpon();
uriBuilder.appendQueryParameter("recipient", address);
uriBuilder.appendQueryParameter("cellbroadcast", address);
Uri uri = uriBuilder.build();
// TODO need replace with helper interface.
Cursor cursor = null;
long token = Binder.clearCallingIdentity();
try {
cursor = SqliteWrapper.query(this.getContext(), this
.getContext().getContentResolver(), uri, ID_PROJECTION, null,
null, null);
…
if (cursor.moveToFirst()) {
return String.valueOf(cursor.getLong(0));
…
}
SqliteWrapper会调用到ContentResolver,ContentResolver则最终调用到MmsSmsProvider,
在MmsSmsProvider.query里面,也是通过URI match找到操作码URI_THREAD_ID,根据参数cellbroadcast,使用getCBThreadId给CB查找或创建其ThreadId。
Threads表内容如下所示,里面的关键字段有_id ,recipient_ids,要想找到合适的ThreadId,只有通过recipient_ids来查询,
所以在getCBThreadId方法里面,通过getAddressIds从canonical_addresses表里面找到和号码匹配的recipient_ids列表,再到Threads表里面找到recipient_ids相同和type是小区广播的_id,即是我们需要的ThreadId,使用的查询语句如下。
String queryString = "SELECT _id FROM threads " + "WHERE type=" + EncapsulatedTelephony.Threads.CELL_BROADCAST_THREAD + " AND recipient_ids=?";
如果没有满足条件的ThreadId,调用insertThread()创建一个新的thread。
在Message列表界面通过按钮新建短信时,执行createNewMessage(),通过createIntent传递一个threadId=0的ID过去,
private void createNewMessage() {
startActivity(ComposeMessageActivity.createIntent(this, 0));
}
public static Intent createIntent(Context context, long threadId) {
Intent intent = new Intent(context, ComposeMessageActivity.class);
if (threadId > 0) {
intent.setData(Conversation.getUri(threadId));
}
return intent;
}
因为threadId为0,在会话创建中会获取或创建一个threadId,具体过程和下节相同。
在联系人的详情界面,点击发送短信,因为有号码存在,所以在ComposeMessageActivity.Oncreate方法里面会去查询或创建一个threadId。
onCreate的initActivityState里,有这么一段代码是获取会话实例的,mConversation.get通过号码获取到会话实例,
Uri intentData = intent.getData();
/// M: Code analyze 034, If intent is SEND,just create a new empty thread,
/// otherwise Conversation.get() will throw exception.
String action = intent.getAction();
if (intentData != null && (TextUtils.isEmpty(action) ||
!action.equals(Intent.ACTION_SEND))) {
/// M: group-contact send message
// try to get a conversation based on the data URI passed to our intent.
if (intentData.getPathSegments().size() < 2) {
mConversation = mConversation.get(getApplicationContext(),ContactList.getByNumbers(
getStringForMultipleRecipients(Conversation.getRecipients(intentData)),
false /* don't block */, true /* replace number */),false);
} else {
mConversation = Conversation.get(getApplicationContext(), intentData, false);
}
/// @}
mWorkingMessage.setText(getBody(intentData));
Conversation get代码如下,如果号码为空则新建会话,如果缓冲里存储了对应号码的会话,则使用缓冲里的数据,如果条件都不满足,则需要去数据库查询到ID,并新建会话,之后将新会话存放到缓冲。
public static Conversation get(Context context, ContactList recipients, boolean allowQuery) {
if (recipients.size() < 1) {
return createNew(context);
}
Conversation conv = Cache.get(recipients);
if (conv != null) {
return conv;
}
long threadId = getOrCreateThreadId(context, recipients);
conv = new Conversation(context, threadId, allowQuery);
。。。
Cache.put(conv);
}
getOrCreateThreadId用来获取ThreadId,由Threads.getOrCreateThreadId实现,这个方法在Telephony.java里面。
getOrCreateThreadId类似前面提到的小区广播的实现过程,先封装一个"content://mms-sms/threadID"的URI,但只使用一个参数recipient,
在MmsSmsProvider.query里面,也是通过URI match找到操作码URI_THREAD_ID,,使用getThreadId查找或创建其ThreadId,getAddressIds获取到匹配的recipientIds,再取Threads表里查询,如果查询到,返回游标,如果没有记录,则用insertThread向Threads里插入记录,再查询到ThreadId。
private synchronized Cursor getThreadId(List<String> recipients) {
Set<Long> addressIds = getAddressIds(recipients);
。。。
recipientIds = Long.toString(addressId);
。。。
String[] selectionArgs = new String[] { recipientIds };
SQLiteDatabase db = mOpenHelper.getReadableDatabase();
db.beginTransaction();
Cursor cursor = null;
try {
// Find the thread with the given recipients
cursor = db.rawQuery(THREAD_QUERY, selectionArgs);
if (cursor.getCount() == 0) {
// No thread with those recipients exists, so create the thread.
cursor.close();
Log.d(LOG_TAG, "getThreadId: create new thread_id for recipients " + recipients);
insertThread(recipientIds, recipients);
// The thread was just created, now find it and return it.
cursor = db.rawQuery(THREAD_QUERY, selectionArgs);
。。。
}
3. ConversationListItem
3.1 ConversationListItem
3.1.1 Layout分析
如前所述,短信记录的每条信息显示是使用conversation_list_item.xml布局的,在本例中,其直观显示是如下形式的一条短信记录,
conversation_list_item.xml文件如下,它是一个ConversationListItem类型的布局,
(需要注意不同屏幕使用这个文件的位置,本例中在packages\apps\Mms\res\layout-mdpi下面,不是packages\apps\Mms\res\layout)
conversation_list_item.xml
<?xml version="1.0" encoding="utf-8"?>
<com.android.mms.ui.ConversationListItem xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="?android:attr/listPreferredItemHeight"
android:background="?android:attr/activatedBackgroundIndicator" >
<CheckBox
android:id="@+id/select_box"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_centerVertical="true"
android:clickable="false"
android:focusable="false"
android:focusableInTouchMode="false" />
<com.android.mms.ui.MmsQuickContactBadge
android:id="@+id/avatar"
android:visibility="gone"
android:layout_centerVertical="true"
android:layout_toEndOf="@id/select_box"
style="?android:attr/quickContactBadgeStyleWindowLarge" />
<ImageView
android:id="@+id/presence"
android:visibility="invisible"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_alignParentTop="true"
android:src="@drawable/ic_unread_label"/>
<LinearLayout
android:id="@+id/iconlist"
android:layout_alignParentTop="true"
android:layout_alignParentEnd="true"
android:layout_marginTop="7dip"
android:layout_marginEnd="10dip"
android:orientation="horizontal"
android:layout_height="wrap_content"
android:layout_width="wrap_content" >
<ImageView android:id="@+id/draft"
android:visibility="gone"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:src="@drawable/ic_draft" />
<ImageView android:id="@+id/error"
android:paddingStart="6dp"
android:visibility="gone"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:src="@drawable/ic_list_alert_sms_failed" />
<ImageView android:id="@+id/attachment"
android:paddingStart="6dp"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:visibility="gone"
android:src="@drawable/ic_attachment_universal_small" />
<ImageView android:id="@+id/mute"
android:paddingStart="6dp"
android:visibility="gone"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:src="@drawable/ipmsg_silent" />
</LinearLayout>
<RelativeLayout
android:layout_alignParentTop="true"
android:layout_marginTop="4dip"
android:layout_marginStart="12dip"
android:layout_toEndOf="@id/avatar"
android:layout_toStartOf="@id/iconlist"
android:gravity="center_vertical"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView android:id="@+id/unread"
android:background="@drawable/ipmsg_message_box"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:textSize="12sp"
android:textColor="@color/text_color_unread"
android:singleLine="true"
android:layout_alignParentEnd="true" />
<RelativeLayout android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:layout_toStartOf="@id/unread">
<TextView android:id="@+id/draft_and_smgcount"
android:textAppearance="?android:attr/textAppearanceMedium"
android:layout_alignParentEnd="true"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="6dip"
android:singleLine="true" />
<com.android.mms.util.AlwaysMarqueeTextView android:id="@+id/from"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceMedium"
android:singleLine="true"
android:layout_alignParentStart="true"
android:layout_toStartOf="@id/draft_and_smgcount"
android:layout_marginEnd="6dip"
android:ellipsize="marquee" />
</RelativeLayout>
</RelativeLayout>
<TextView android:id="@+id/date"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:textSize= "12sp"
android:textColor="@color/dlg_text_counter_color"
android:layout_alignParentBottom="true"
android:layout_alignParentEnd="true"
android:layout_marginEnd="10dip"
android:layout_marginBottom="4dip"
android:gravity="bottom" />
<TextView android:id="@+id/subject"
android:layout_height="wrap_content"
android:layout_width="fill_parent"
android:textSize= "12sp"
android:layout_alignParentBottom="true"
android:layout_toEndOf="@id/avatar"
android:layout_toStartOf="@id/date"
android:layout_marginStart="12dip"
android:layout_marginBottom="4dip"
android:gravity="bottom"
android:textColor="@color/dlg_text_counter_color"
android:lines="2"
android:ellipsize="end" />
</com.android.mms.ui.ConversationListItem>
其对应的布局图形如下,图中左边是layout组成,右上是layout布局表现,右下是对layout里面的子view编号,便于理解。
分析xml文件,并对照图例可以看出,
ConversationListItem是整个item布局类,表现形式见下图左边,它指定了高度
android:layout_height="?android:attr/listPreferredItemHeight"
这个布局由6个同级view组成,见下图中间及右上,其中
checkbox供选择使用,见右下数字0,alignParentStart表示它在开头位置,
android:layout_alignParentStart="true"
MmsQuickContactBadge做头像显示,见右下数字1,
ImageView做未读标识等用途,见右下数字2,通过下面的属性将它放置在最右上的位置
android:layout_alignParentEnd="true"
android:layout_alignParentTop="true"
LinearLayout是个组合视图,见右下数字3和iconlist小图,它有四个子ImageView表示短信的信息状态,具体名字见iconlist小图。注意四个子ImageView表示同时最多只能显示四个图标,不是表示短信只有这四种状态。通过下面属性,将它放置在MmsQuickContactBadge后面。
android:orientation="horizontal"
<iconlist>
RelativeLayout是个组合视图,见右下数字4和Text Title小图,包含3个TextView,用来显示Title和消息数目、未读消息数目。在RelativeLayout,通过下面的属性设置,将RelativeLayout(index4的view)和上面的LinearLayout(index3的view)并行放置在同一行。
android:layout_toEndOf="@id/avatar"
android:layout_toStartOf="@id/iconlist"
TextView显示日期,见右下数字5,用下面2个属性表示它的位置在右下,
android:layout_alignParentBottom="true"
android:layout_alignParentEnd="true"
TextView 显示短线截取的部分内容,见右下数字6,用下面3个属性表明了它的位置。
android:layout_alignParentBottom="true"
android:layout_toEndOf="@id/avatar"
android:layout_toStartOf="@id/date"
<Text Content>
3.1.2 代码分析
3.1.2.1 ConversationListItem实例创建
ConversationListItem实例创建栈如下,因为有缓存使用,不一定每次进入列表都会创建这个类实例,所以不一定能调试到,要在此设置断点调试,有一个小技巧,就是将mms进程终止掉,让其自动重启(可通过发送短信或cb的方法加快重启过程),这样就可以进行调试。
ConversationListItem.<init>(Context, AttributeSet) line: 130
Constructor.constructNative(Object[], Class, Class[], int, boolean) line: not available [native method]
Constructor.newInstance(Object...) line: 423
PhoneLayoutInflater(LayoutInflater).createView(String, String, AttributeSet) line: 594
PhoneLayoutInflater(LayoutInflater).createViewFromTag(View, String, AttributeSet) line: 696
PhoneLayoutInflater(LayoutInflater).inflate(XmlPullParser, ViewGroup, boolean) line: 469
PhoneLayoutInflater(LayoutInflater).inflate(int, ViewGroup, boolean) line: 397
ConversationListAdapter.newView(Context, Cursor, ViewGroup) line: 103
ConversationListAdapter(CursorAdapter).getView(int, View, ViewGroup) line: 250
ListView(AbsListView).obtainView(int, boolean[]) line: 2338
ListView.makeAndAddView(int, int, boolean, int, boolean) line: 1812
ListView.fillDown(int, int) line: 698
ListView.fillFromTop(int) line: 759
ListView.layoutChildren() line: 1631
ListView(AbsListView).onLayout(boolean, int, int, int, int) line: 2149
ConversationListItem实例创建的主要过程分析如下,在getView来获取view的时候,因为缓冲没有数据,会使用newView新建一个view,
public View getView(int position, View convertView, ViewGroup parent) {
View v;
if (convertView == null) {
v = newView(mContext, mCursor, parent);
} else {
v = convertView;
}
bindView(v, mContext, mCursor);
return v;
}
newView实际的过程是inflate一个布局文件,这个布局文件就是我们分析的layout,它是ConversationListItem形式的,我们知道,在inflate创建布局文件时,会根据节点创建view实例,所以会使用类的构造类Constructor来创建ConversationListItem实例。
public View newView(Context context, Cursor cursor, ViewGroup parent) {
if (LOCAL_LOGV) Log.v(TAG, "inflating new view");
return mFactory.inflate(R.layout.conversation_list_item, parent, false);
}
Checkbox的主要作用是用来勾选短信列表的某一个item,并进行删除操作的。
对于checkbox,在ConversationListItem里面定义了一个引用mSelectBox,
private CheckBox mSelectBox;
并在onFinishInflate里面初始化它,关联到Layout里面的select_box,
mSelectBox = (CheckBox) findViewById(R.id.select_box);
当列表项被点击时,执行ConversationList.onListItemClick(ListView, View, int, long)这个方法,在里面添加下面代码,当处于删除模式时,如下,将Conversation设置为check状态,然后更新界面,
if (mListAdapter != null && mDeleteMode) {
if (conv.isChecked()) {
conv.setIsChecked(false);
} else {
conv.setIsChecked(true);
}
mListAdapter.notifyDataSetChanged();
updateSelectView();
return;
}
更新界面时,执行ConversationListItem.bind(Context, Conversation) ,每加载一个item项都会执行一次bind,在这里,会根据mSelectMode 决定checkbox是否显示,根据Conversation的状态决定是否选中。
所以实际上,不是我们主观上看到的那样由checkbox决定了当前项目是否选中,而是当前项选中后决定checkbox的显示状态。
if (mSelectMode) {
mSelectBox.setVisibility(View.VISIBLE);
mAvatarView.setClickable(false);
} else {
mSelectBox.setVisibility(View.GONE);
mAvatarView.setClickable(true);
}
if (mConversation.isChecked()) {
mSelectBox.setChecked(true);
} else {
mSelectBox.setChecked(false);
}
上面就是checkbox在代码里的处理过程。
R.id.avatar用来显示发件人头像,定义如下,MmsQuickContactBadge继承自QuickContactBadge,再往上是ImageView,一个通用的视图组件。
private MmsQuickContactBadge mAvatarView;
其实例获取在ConversationListItem的onFinishInflate里面,
mAvatarView = (MmsQuickContactBadge) findViewById(R.id.avatar);
如果希望AvatarView能够被点击,相应事件,则可以看到在bind有如下代码,表示在删除模式下不能被点击,在列表模式下可以被点击。
if (mSelectMode) {
mAvatarView.setClickable(false);
} else {
mAvatarView.setClickable(true);
}
AvatarView被点击之后,执行的调用栈如下,可见是由View直接调用了MmsQuickContactBadge类的onClick,并不是ConversationList或ConversationListItem里做了onClick的相关处理。
MmsQuickContactBadge(QuickContactBadge).onClick(View) line: 217
MmsQuickContactBadge.onClick(View) line: 75
MmsQuickContactBadge(View).performClick() line: 4212
View$PerformClick.run() line: 17476
Handler.handleCallback(Message) line: 800
ViewRootImpl$ViewRootHandler(Handler).dispatchMessage(Message) line: 100
QuickContactBadge类的onClick代码如下,
@Override
public void onClick(View v) {
if (mContactUri != null) {
QuickContact.showQuickContact(getContext(), QuickContactBadge.this, mContactUri,
QuickContact.MODE_LARGE, mExcludeMimes);
} else if (mContactEmail != null) {
mQueryHandler.startQuery(TOKEN_EMAIL_LOOKUP_AND_TRIGGER, mContactEmail,
Uri.withAppendedPath(Email.CONTENT_LOOKUP_URI, Uri.encode(mContactEmail)),
EMAIL_LOOKUP_PROJECTION, null, null, null);
} else if (mContactPhone != null) {
mQueryHandler.startQuery(TOKEN_PHONE_LOOKUP_AND_TRIGGER, mContactPhone,
Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI, mContactPhone),
PHONE_LOOKUP_PROJECTION, null, null, null);
} else {
// If a contact hasn't been assigned, don't react to click.
return;
}
}
我们当前的代码走mContactPhone路径,mContactPhone是在assignContactFromPhone里面赋值的,assignContactFromPhone则是通过ConversationListItem的updateAvatarView调用的(传入的number就赋值给mContactPhone),
mAvatarView.assignContactFromPhone(number, true);
updateAvatarView可以被updateFromView和bind调用,这两个方法我们在前面有过描述。所以从整个过程来看,我们可以看出avatar被点击和其需要的数据来源。
3.2 绘制分析
3.2.1 Message列表创建
如ConversationList的layout描述,Messag列表主要是由list呈现,而list的item项是由ConversationListItem布局的,我们这里讨论每个item项的创建以及整个list的创建。
在使用Cache时,ConversationListItem的创建过程如下,
ConversationListItem.bind(Context, Conversation) line: 480
ConversationListAdapter.bindView(View, Context, Cursor) line: 77
ConversationListAdapter(CursorAdapter).getView(int, View, ViewGroup) line: 250
ListView(AbsListView).obtainView(int, boolean[]) line: 2186
ListView.makeAndAddView(int, int, boolean, int, boolean) line: 1877
ListView.fillSpecific(int, int) line: 1356
ListView.layoutChildren() line: 1671
ListView(AbsListView).onLayout(boolean, int, int, int, int) line: 2037
ListView(View).layout(int, int, int, int) line: 14118
ListView(ViewGroup).layout(int, int, int, int) line: 4467
RelativeLayout.onLayout(boolean, int, int, int, int) line: 1021
从上往下分析该栈:
1)ConversationListItem.bind
ConversationListItem.bind的代码比较多,仔细分析,实际上逻辑比较简单,主要是设置每个view的内容和状态,比如说设置日期的代码如下,
mDateView.setVisibility(VISIBLE);
if(ConversationList.sConversationListOption ==
ConversationList.OPTION_CONVERSATION_LIST_IMPORTANT &&
!conversation.hasUnreadMessages()) {
mDateView.setText(MessageUtils.formatTimeStampStringExtend(context,
conversation.getImpDate()));
}
其数据来源于conversation,我们要进一步分析conversation是怎么传递进来的,以及为什么要调用bind。
2)ConversationListAdapter
ConversationListAdapter 是ConversationList的layout里的list对应的Adapter,它继承自MessageCursorAdapter-CursorAdapter-baseAdapter, 其方法bindView在本例中执行的主要代码如下,其中headerview是一个ConversationListItem引用,是调用方法传递过来的,conversation是根据游标新建的,最后调用bind,所以该方法的作用就是获取到conversation,并用准备将其中的数据赋值到view。
public void bindView(View view, Context context, Cursor cursor) {
ConversationListItem headerView = (ConversationListItem) view;
Conversation conv;
if (!mIsScrolling) {
Conversation.setNeedCacheConv(false);
conv = Conversation.from(context, cursor);
Conversation.setNeedCacheConv(true);
if (mSubjectSingleLine) {
headerView.setSubjectSingleLineMode(true);
}
if (conv != null) {
conv.setIsChecked(sSelectedTheadsId.contains(conv.getThreadId()));
}
headerView.bind(context, conv);
}
}
(CursorAdapter).getView则负责获取到view实例和游标实例。
view的获取有两种方式,一种是调用者传递过来以前缓存的,一种是新创建一个,在ConversationListItem实例创建中有讲到如何创建。
游标实例则在Adapter创建时创建。
3)ListView
在ListView部分,我们从layoutChildren向上分析,向下部分设计到太多ListView的实现机制问题,暂不讨论。
layoutChildren实现ListView子窗口布局,实际上就是list在layout所占部分。
本例从launcher进入Message列表,执行代码路径如下,mSyncPosition是当前item索引值,mSpecificTop是当前layout相对Top的位置。
layoutChildren:
case LAYOUT_SYNC:
sel = fillSpecific(mSyncPosition, mSpecificTop);
fillSpecific在知道位置绘制指定项的item数据,然后上下绘制其他的item。它先通过makeAndAddView完成当前item的创建并添加到layout,fillDown、fillUp则根据layout大小,上下绘制item完成整个list的绘制,使用的方法也是makeAndAddView,adjustViewsUpOrDown调整item位置。
/**
* Put a specific item at a specific location on the screen and then build
* up and down from there.
*
* @param position The reference view to use as the starting point
* @param top Pixel offset from the top of this view to the top of the
* reference view.
*
* @return The selected view, or null if the selected view is outside the
* visible area.
*/
private View fillSpecific(int position, int top) {
boolean tempIsSelected = position == mSelectedPosition;
View temp = makeAndAddView(position, top, true, mListPadding.left, tempIsSelected);
…
below = fillDown(position + 1, temp.getBottom() + dividerHeight);
// This will correct for the bottom of the last view not touching the bottom of the list
adjustViewsUpOrDown();
above = fillUp(position - 1, temp.getTop() - dividerHeight);
int childCount = getChildCount();
if (childCount > 0) {
correctTooLow(childCount);
}
}
顾名思义,makeAndAddView就是新建一个view,并将它添加到list里面。obtainView负责创建view,setupChild负责将view添加到layout,并设置好view的位置和相关属性。
private View makeAndAddView(int position, int y, boolean flow, int childrenLeft,
boolean selected) {
View child;
child = obtainView(position, mIsScrap);
// This needs to be positioned and measured
setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]);
return child;
}
obtainView将对应位置的view和数据关联起来,并返回view。它通过mRecycler获取到缓存的view,再通过mAdapter.getView将数据填充到view,mAdapter.getView即是我们前面分析到的ConversationListAdapter(CursorAdapter).getView。
至此,Message列表的创建过程就明了了。
3.3 功能分析
3.3.1 Message列表更新
当数据库内容发生变化(包括新短信、删除、发送状态改变等),就会触发onContentChanged,其调用栈如下:
ConversationList$2.onContentChanged(ConversationListAdapter) line: 474
ConversationListAdapter.onContentChanged() line: 118
CursorAdapter$ChangeObserver.onChange(boolean) line: 463
CursorAdapter$ChangeObserver(ContentObserver).onChange(boolean, Uri) line: 129
ContentObserver$NotificationRunnable.run() line: 180
Handler.handleCallback(Message) line: 808
Handler.dispatchMessage(Message) line: 103
Looper.loop() line: 193
从下往上分析,
1)ContentObserver
我们已经很熟悉handleCallback处理的是Post的消息,消息一般是runable,所以可以看到ContentObserver. dispatchChange有post这一消息的过程(dispatchChange的触发过程比较复杂,我们将在其他章节介绍,本例中当数据库内容变化时会触发这个流程)。
public final void dispatchChange(boolean selfChange, Uri uri) {
if (mHandler == null) {
onChange(selfChange, uri);
} else {
mHandler.post(new NotificationRunnable(selfChange, uri));
}
}
然后这个消息会运行其内部类NotificationRunnable的run方法,
public void run() {
ContentObserver.this.onChange(mSelfChange, mUri);
}
This指向的是ContentObserver的实例ChangeObserver,它是CursorAdapter的内部类,也是ContentObserver的子类,由于ConversationListAdapter通过CursorWrapper向ContentObservable注册了ContentObserver,所以当dispatchChange事件时,通过轮询,最终将事件传递给ChangeObserver。
而后,ChangeObserver调用封装类CursorAdapter的onContentChanged,将事件上传。
private class ChangeObserver extends ContentObserver {
public void onChange(boolean selfChange) {
onContentChanged();
}
}
2)onContentChanged
onContentChanged是CursorAdapter的一个方法,主要功能是当数据库内容更新后,重新使用游标查询数据。但这个方法被ConversationListAdapter重载,它给自己的客户提供一个借口类OnContentChangedListener,让客户去实现这个借口类,自己则调用接口类的onContentChanged方法执行客户定义的功能。
protected void onContentChanged() {
if (mCursor != null && !mCursor.isClosed()) {
if (mOnContentChangedListener != null) {
mOnContentChangedListener.onContentChanged(this);
}
}
}
3)ConversationList
ConversationList就是ConversationListAdapter的客户,它实现了接口类,如下,主要是调用startAsyncQuery进行数据的查询。
private final ConversationListAdapter.OnContentChangedListener mContentChangedListener =
new ConversationListAdapter.OnContentChangedListener() {
@Override
public void onContentChanged(ConversationListAdapter adapter) {
if (mIsInActivity) {
mNeedQuery = true;
startAsyncQuery();
}
}
};
并且ConversationList在其onStart向ConversationListAdapter注册了这个接口类监听器:
mListAdapter.setOnContentChangedListener(mContentChangedListener);
所以,当数据库数据改变时,根据上面相关调用流程,完成了Message列表的刷新功能。
BTW,附上游标改变后注册ContentObserver的调用栈:
ContentResolver$CursorWrapperInner(CursorWrapper).registerContentObserver(ContentObserver) line: 178
ConversationListAdapter(CursorAdapter).swapCursor(Cursor) line: 340
ConversationListAdapter(CursorAdapter).changeCursor(Cursor) line: 313
ConversationList$ThreadListQueryHandler.onQueryComplete(int, Object, Cursor) line: 1896
ConversationList$ThreadListQueryHandler(AsyncQueryHandler).handleMessage(Message) line: 344
ConversationList$ThreadListQueryHandler(Handler).dispatchMessage(Message) line: 110
Looper.loop() line: 193
3.3.2 Message列表定位
因为ConversationList是ListActivity,所以可以使用getListView获取ListView,再使用ListView的setSelectionFromTop,将列表显示开始位置指定到特定位置。
需要注意2个问题,
1)
在ConversationList会记录上次浏览列表的起始位置,所以不是随意就能设置好列表的显示位置,如onQueryComplete通过下面代码控制了什么时候可以设置这个起始位置,我们需要根据mSavedFirstVisiblePosition的值和相关逻辑去处理。
if (mSavedFirstVisiblePosition != AdapterView.INVALID_POSITION) {
// Restore the list to its previous position.
getListView().setSelectionFromTop(mSavedFirstVisiblePosition,
mSavedFirstItemOffset);
mSavedFirstVisiblePosition = AdapterView.INVALID_POSITION;
}
结合列表的创建过程,我们知道,当同步绘制列表时,是从mSyncPosition开始的,
fillSpecific(mSyncPosition, mSpecificTop);
而mSyncPosition正是setSelectionFromTop里传递进去的,所以列表起始位置只能通过setSelectionFromTop来设置。
public void setSelectionFromTop(int position, int y) {
…
mSyncPosition = position;
…
}
2)
游标的更新,当数据库数据变化后,需要更新游标,才能获取到正确的值。下面是onQueryComplete完成后,用changeCursor替换掉旧游标的过程,防止游标数据过期。
ConversationListAdapter(CursorAdapter).swapCursor(Cursor) line: 340
ConversationListAdapter(CursorAdapter).changeCursor(Cursor) line: 313
ConversationList$ThreadListQueryHandler.onQueryComplete(int, Object, Cursor) line: 1896
ConversationList$ThreadListQueryHandler(AsyncQueryHandler).handleMessage(Message) line: 344
ConversationList$ThreadListQueryHandler(Handler).dispatchMessage(Message) line: 110
Looper.loop() line: 193
在列表创建时,CursorAdapter. getView的游标最初是在CursorAdapte创建时传入的,之后可以通过swapCursor的方法更新。
6572_message_conversationList_详细分析
目 录
2. ConversationList
2.1.2.1 ConversationListAdapter
2.2.1 列表项click
2.2.2 Long Press click to Delete
2.3.1 会话ID创建
3. ConversationListItem
3.1.2.1 ConversationListItem实例创建
1. 概要
本文主要分析短信列表界面的主要控件的界面显示和处理逻辑。
2. ConversationList
2.1 ConversationList
2.1.1 Layout分析
ConversationList对应的layout是conversation_list_screen,它主要分3部分,第一部分是做全选的控制,第二部分是list列表,第三部分是列表为空的布局。具体的表现形式如下,布局文件就不再展开了。其中list列表将在另外的章节分析。
2.1.2 代码分析
2.1.2.1 ConversationListAdapter
ConversationListAdapter是ConversationListItem的数据适配器,在ConversationList.onCreate里创建,它通过bindView将会话信息绑定到item的layout上。
其创建过程如下,
private void initListAdapter() {
mListAdapter = new ConversationListAdapter(this, null);
/** M: now this code is useless and will lead to a JE, comment it.
* listener is set in onStart
*/
//mListAdapter.setOnContentChangedListener(mContentChangedListener);
setListAdapter(mListAdapter);
getListView().setRecyclerListener(mListAdapter);
}
因为ConversationList继承自ListActivity,setListAdapter将Adapter实例设置给ListActivity,这里会调用ensureList,它会加载一个布局,确保有view能用来对应Adapter。
public void setListAdapter(ListAdapter adapter) {
synchronized (this) {
ensureList();
mAdapter = adapter;
mList.setAdapter(adapter);
}
}
private void ensureList() {
if (mList != null) {
return;
}
setContentView(com.android.internal.R.layout.list_content_simple);
}
而实际上,mList已经在 onContentChanged被初始化为Layout里对应的ListView,
mList = (ListView)findViewById(com.android.internal.R.id.list);
这个ListView就是Message列表的容器了。
2.2 功能分析
2.2.1 列表项click
单击列表项的调用栈如下:
ConversationList.onListItemClick(ListView, View, int, long) line: 1086
ListActivity$2.onItemClick(AdapterView, View, int, long) line: 319
ListView(AdapterView).performItemClick(View, int, long) line: 298
ListView(AbsListView).performItemClick(View, int, long) line: 1128
AbsListView$PerformClick.run() line: 2815
AbsListView$1.run() line: 3574
Handler.handleCallback(Message) line: 800
ViewRootImpl$ViewRootHandler(Handler).dispatchMessage(Message) line: 100
2.2.2 Long Press click to Delete
长按item删除会话流程如下:
长按出现选择菜单,
mConvListOnCreateContextMenuListener
onCreateContextMenu
点击删除
ConversationList.confirmDeleteThreads(Collection, AsyncQueryHandler) line:
ConversationList.confirmDeleteThread(long, AsyncQueryHandler) line: 1320
ConversationList.onContextItemSelected(MenuItem) line: 1247
ConversationList(Activity).onMenuItemSelected(int, MenuItem) line: 2584
PhoneWindow$DialogMenuCallback.onMenuItemSelected(MenuBuilder, MenuItem) line:
开始删除之前,要做一个后台查询,看是否有lock住的消息,Conversation.startQueryHaveLockedMessages,
之后是后台查询handler.startQuery:
AsyncQueryHandler
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);
msg.arg1 = EVENT_ARG_QUERY;
WorkerArgs args = new WorkerArgs();
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);
}
这个查询过程将消息发给mWorkerThreadHandler,这是异步查询AsyncQueryHandler自己的handler,它封装完消息EVENT_ARG_QUERY后,再发送出去,
Message reply = args.handler.obtainMessage(token);
reply.obj = args;
reply.arg1 = msg.arg1;
reply.sendToTarget();
这个消息的接收对象的设置见args.handler = this; ,在本例中,这个handler是ThreadListQueryHandler。
数据库查询完成后执行onQueryComplete(HAVE_LOCKED_MESSAGES_TOKEN),
ConversationList$ThreadListQueryHandler.onQueryComplete(int, Object, Cursor) line: 1610
ConversationList$ThreadListQueryHandler(AsyncQueryHandler).handleMessage(Message) line: 344
ConversationList$ThreadListQueryHandler(Handler).dispatchMessage(Message) line: 107
Looper.loop() line: 194
之后会给出一个确认删除对话框confirmDeleteThreadDialog
在弹出的确认对话框中,选择删除,执行listener函数。
ConversationList$DeleteThreadListener.onClick(DialogInterface, int) line: 1484
AlertController$ButtonHandler.handleMessage(Message) line: 176
之后开始删除Conversation.startDelete
最后onDeleteComplete
响应函数会触发一系列其他的数据库操作,如查询list,查询unread等,
AsyncQueryHandler$WorkerHandler.handleMessage(Message) line: 120
AsyncQueryHandler$WorkerHandler(Handler).dispatchMessage(Message) line: 107
【Tips】
它的threadIds是在点击删除功能时,onContextItemSelected设置的,和长按的时候在onCreateContextMenu里选择的有时间差,不一定是同一个,在会话列表更新的时候,会有影响,所以会误删。
所以在onCreateContextMenu里要记录thread id。
2.3 数据操作
2.3.1 会话ID创建
Message列表是以会话为item进行显示的,会话是以为ID记录的,我们分析一下各种会话ID是如何创建的,
应用在CBMessageReceiverService.java收到小区广播后,使用handleCBMessageReceived方法进行处理,其中insertMessage是用来存储CB的。
insertMessage会调用CbProvider的insert方法
public Uri insert(Uri url, ContentValues initialValues) {
Uri result = null;
// TODO Check Permission
// checkPermission();
SQLiteDatabase db = mOpenHelper.getWritableDatabase();
SQLiteDatabase dbmmssms = mMmsSmsOpenHelper.getWritableDatabase();
int match = URI_MATCHER.match(url);
ContentValues values;
long rowID;
String table = null;
Log.d(TAG, " insert match = "+match);
switch (match) {
case URL_MESSAGES:
//table = CbDatabaseHelper.CBMESSAGE_TABLE;
table = MmsSmsDatabaseHelper.TABLE_CELLBROADCAST;
values = internalInsertMessages(initialValues);
rowID = dbmmssms.insert(table, null, values);
Log.d(TAG, "insert to cellbroadcast " + values);
if (rowID > 0) {
result = Uri.parse("content://messages/" + rowID);
notifyChange();
return result;
}
break;
…
}
在这里,我们重点关注一下CB Message的ThreadId是怎样获取的,在internalInsertMessages里面,显然threadId不是我们主动填充进去的,所以是通过getOrCreateThreadId获得的。
private ContentValues internalInsertMessages(ContentValues initialValues) {
ContentValues values;
if (initialValues != null) {
values = new ContentValues(initialValues);
} else {
values = new ContentValues();
}
Long threadId = values.getAsLong(EncapsulatedTelephony.SmsCb.THREAD_ID);
String address = values.getAsString(EncapsulatedTelephony.SmsCb.CHANNEL_ID);
if (((threadId == null) || (threadId == 0)) && (address != null)) {
values.put(EncapsulatedTelephony.SmsCb.THREAD_ID,
getOrCreateThreadId(address));
}
…
}
getOrCreateThreadId代码如下,它封装一个URI地址,包含了threadID和参数recipient、cellbroadcast,并使用SqliteWrapper.query进行查询,
private String getOrCreateThreadId(String address) {
//Uri.Builder uriBuilder = THREAD_ID_URI.buildUpon();
Uri.Builder uriBuilder = (Uri.parse("content://mms-sms/threadID")).buildUpon();
uriBuilder.appendQueryParameter("recipient", address);
uriBuilder.appendQueryParameter("cellbroadcast", address);
Uri uri = uriBuilder.build();
// TODO need replace with helper interface.
Cursor cursor = null;
long token = Binder.clearCallingIdentity();
try {
cursor = SqliteWrapper.query(this.getContext(), this
.getContext().getContentResolver(), uri, ID_PROJECTION, null,
null, null);
…
if (cursor.moveToFirst()) {
return String.valueOf(cursor.getLong(0));
…
}
SqliteWrapper会调用到ContentResolver,ContentResolver则最终调用到MmsSmsProvider,
在MmsSmsProvider.query里面,也是通过URI match找到操作码URI_THREAD_ID,根据参数cellbroadcast,使用getCBThreadId给CB查找或创建其ThreadId。
Threads表内容如下所示,里面的关键字段有_id ,recipient_ids,要想找到合适的ThreadId,只有通过recipient_ids来查询,
所以在getCBThreadId方法里面,通过getAddressIds从canonical_addresses表里面找到和号码匹配的recipient_ids列表,再到Threads表里面找到recipient_ids相同和type是小区广播的_id,即是我们需要的ThreadId,使用的查询语句如下。
String queryString = "SELECT _id FROM threads " + "WHERE type=" + EncapsulatedTelephony.Threads.CELL_BROADCAST_THREAD + " AND recipient_ids=?";
如果没有满足条件的ThreadId,调用insertThread()创建一个新的thread。
在Message列表界面通过按钮新建短信时,执行createNewMessage(),通过createIntent传递一个threadId=0的ID过去,
private void createNewMessage() {
startActivity(ComposeMessageActivity.createIntent(this, 0));
}
public static Intent createIntent(Context context, long threadId) {
Intent intent = new Intent(context, ComposeMessageActivity.class);
if (threadId > 0) {
intent.setData(Conversation.getUri(threadId));
}
return intent;
}
因为threadId为0,在会话创建中会获取或创建一个threadId,具体过程和下节相同。
在联系人的详情界面,点击发送短信,因为有号码存在,所以在ComposeMessageActivity.Oncreate方法里面会去查询或创建一个threadId。
onCreate的initActivityState里,有这么一段代码是获取会话实例的,mConversation.get通过号码获取到会话实例,
Uri intentData = intent.getData();
/// M: Code analyze 034, If intent is SEND,just create a new empty thread,
/// otherwise Conversation.get() will throw exception.
String action = intent.getAction();
if (intentData != null && (TextUtils.isEmpty(action) ||
!action.equals(Intent.ACTION_SEND))) {
/// M: group-contact send message
// try to get a conversation based on the data URI passed to our intent.
if (intentData.getPathSegments().size() < 2) {
mConversation = mConversation.get(getApplicationContext(),ContactList.getByNumbers(
getStringForMultipleRecipients(Conversation.getRecipients(intentData)),
false /* don't block */, true /* replace number */),false);
} else {
mConversation = Conversation.get(getApplicationContext(), intentData, false);
}
/// @}
mWorkingMessage.setText(getBody(intentData));
Conversation get代码如下,如果号码为空则新建会话,如果缓冲里存储了对应号码的会话,则使用缓冲里的数据,如果条件都不满足,则需要去数据库查询到ID,并新建会话,之后将新会话存放到缓冲。
public static Conversation get(Context context, ContactList recipients, boolean allowQuery) {
if (recipients.size() < 1) {
return createNew(context);
}
Conversation conv = Cache.get(recipients);
if (conv != null) {
return conv;
}
long threadId = getOrCreateThreadId(context, recipients);
conv = new Conversation(context, threadId, allowQuery);
。。。
Cache.put(conv);
}
getOrCreateThreadId用来获取ThreadId,由Threads.getOrCreateThreadId实现,这个方法在Telephony.java里面。
getOrCreateThreadId类似前面提到的小区广播的实现过程,先封装一个"content://mms-sms/threadID"的URI,但只使用一个参数recipient,
在MmsSmsProvider.query里面,也是通过URI match找到操作码URI_THREAD_ID,,使用getThreadId查找或创建其ThreadId,getAddressIds获取到匹配的recipientIds,再取Threads表里查询,如果查询到,返回游标,如果没有记录,则用insertThread向Threads里插入记录,再查询到ThreadId。
private synchronized Cursor getThreadId(List<String> recipients) {
Set<Long> addressIds = getAddressIds(recipients);
。。。
recipientIds = Long.toString(addressId);
。。。
String[] selectionArgs = new String[] { recipientIds };
SQLiteDatabase db = mOpenHelper.getReadableDatabase();
db.beginTransaction();
Cursor cursor = null;
try {
// Find the thread with the given recipients
cursor = db.rawQuery(THREAD_QUERY, selectionArgs);
if (cursor.getCount() == 0) {
// No thread with those recipients exists, so create the thread.
cursor.close();
Log.d(LOG_TAG, "getThreadId: create new thread_id for recipients " + recipients);
insertThread(recipientIds, recipients);
// The thread was just created, now find it and return it.
cursor = db.rawQuery(THREAD_QUERY, selectionArgs);
。。。
}
3. ConversationListItem
3.1 ConversationListItem
3.1.1 Layout分析
如前所述,短信记录的每条信息显示是使用conversation_list_item.xml布局的,在本例中,其直观显示是如下形式的一条短信记录,
conversation_list_item.xml文件如下,它是一个ConversationListItem类型的布局,
(需要注意不同屏幕使用这个文件的位置,本例中在packages\apps\Mms\res\layout-mdpi下面,不是packages\apps\Mms\res\layout)
conversation_list_item.xml
<?xml version="1.0" encoding="utf-8"?>
<com.android.mms.ui.ConversationListItem xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="?android:attr/listPreferredItemHeight"
android:background="?android:attr/activatedBackgroundIndicator" >
<CheckBox
android:id="@+id/select_box"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_centerVertical="true"
android:clickable="false"
android:focusable="false"
android:focusableInTouchMode="false" />
<com.android.mms.ui.MmsQuickContactBadge
android:id="@+id/avatar"
android:visibility="gone"
android:layout_centerVertical="true"
android:layout_toEndOf="@id/select_box"
style="?android:attr/quickContactBadgeStyleWindowLarge" />
<ImageView
android:id="@+id/presence"
android:visibility="invisible"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_alignParentTop="true"
android:src="@drawable/ic_unread_label"/>
<LinearLayout
android:id="@+id/iconlist"
android:layout_alignParentTop="true"
android:layout_alignParentEnd="true"
android:layout_marginTop="7dip"
android:layout_marginEnd="10dip"
android:orientation="horizontal"
android:layout_height="wrap_content"
android:layout_width="wrap_content" >
<ImageView android:id="@+id/draft"
android:visibility="gone"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:src="@drawable/ic_draft" />
<ImageView android:id="@+id/error"
android:paddingStart="6dp"
android:visibility="gone"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:src="@drawable/ic_list_alert_sms_failed" />
<ImageView android:id="@+id/attachment"
android:paddingStart="6dp"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:visibility="gone"
android:src="@drawable/ic_attachment_universal_small" />
<ImageView android:id="@+id/mute"
android:paddingStart="6dp"
android:visibility="gone"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:src="@drawable/ipmsg_silent" />
</LinearLayout>
<RelativeLayout
android:layout_alignParentTop="true"
android:layout_marginTop="4dip"
android:layout_marginStart="12dip"
android:layout_toEndOf="@id/avatar"
android:layout_toStartOf="@id/iconlist"
android:gravity="center_vertical"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView android:id="@+id/unread"
android:background="@drawable/ipmsg_message_box"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:textSize="12sp"
android:textColor="@color/text_color_unread"
android:singleLine="true"
android:layout_alignParentEnd="true" />
<RelativeLayout android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:layout_toStartOf="@id/unread">
<TextView android:id="@+id/draft_and_smgcount"
android:textAppearance="?android:attr/textAppearanceMedium"
android:layout_alignParentEnd="true"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="6dip"
android:singleLine="true" />
<com.android.mms.util.AlwaysMarqueeTextView android:id="@+id/from"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceMedium"
android:singleLine="true"
android:layout_alignParentStart="true"
android:layout_toStartOf="@id/draft_and_smgcount"
android:layout_marginEnd="6dip"
android:ellipsize="marquee" />
</RelativeLayout>
</RelativeLayout>
<TextView android:id="@+id/date"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:textSize= "12sp"
android:textColor="@color/dlg_text_counter_color"
android:layout_alignParentBottom="true"
android:layout_alignParentEnd="true"
android:layout_marginEnd="10dip"
android:layout_marginBottom="4dip"
android:gravity="bottom" />
<TextView android:id="@+id/subject"
android:layout_height="wrap_content"
android:layout_width="fill_parent"
android:textSize= "12sp"
android:layout_alignParentBottom="true"
android:layout_toEndOf="@id/avatar"
android:layout_toStartOf="@id/date"
android:layout_marginStart="12dip"
android:layout_marginBottom="4dip"
android:gravity="bottom"
android:textColor="@color/dlg_text_counter_color"
android:lines="2"
android:ellipsize="end" />
</com.android.mms.ui.ConversationListItem>
其对应的布局图形如下,图中左边是layout组成,右上是layout布局表现,右下是对layout里面的子view编号,便于理解。
分析xml文件,并对照图例可以看出,
ConversationListItem是整个item布局类,表现形式见下图左边,它指定了高度
android:layout_height="?android:attr/listPreferredItemHeight"
这个布局由6个同级view组成,见下图中间及右上,其中
checkbox供选择使用,见右下数字0,alignParentStart表示它在开头位置,
android:layout_alignParentStart="true"
MmsQuickContactBadge做头像显示,见右下数字1,
ImageView做未读标识等用途,见右下数字2,通过下面的属性将它放置在最右上的位置
android:layout_alignParentEnd="true"
android:layout_alignParentTop="true"
LinearLayout是个组合视图,见右下数字3和iconlist小图,它有四个子ImageView表示短信的信息状态,具体名字见iconlist小图。注意四个子ImageView表示同时最多只能显示四个图标,不是表示短信只有这四种状态。通过下面属性,将它放置在MmsQuickContactBadge后面。
android:orientation="horizontal"
<iconlist>
RelativeLayout是个组合视图,见右下数字4和Text Title小图,包含3个TextView,用来显示Title和消息数目、未读消息数目。在RelativeLayout,通过下面的属性设置,将RelativeLayout(index4的view)和上面的LinearLayout(index3的view)并行放置在同一行。
android:layout_toEndOf="@id/avatar"
android:layout_toStartOf="@id/iconlist"
TextView显示日期,见右下数字5,用下面2个属性表示它的位置在右下,
android:layout_alignParentBottom="true"
android:layout_alignParentEnd="true"
TextView 显示短线截取的部分内容,见右下数字6,用下面3个属性表明了它的位置。
android:layout_alignParentBottom="true"
android:layout_toEndOf="@id/avatar"
android:layout_toStartOf="@id/date"
<Text Content>
3.1.2 代码分析
3.1.2.1 ConversationListItem实例创建
ConversationListItem实例创建栈如下,因为有缓存使用,不一定每次进入列表都会创建这个类实例,所以不一定能调试到,要在此设置断点调试,有一个小技巧,就是将mms进程终止掉,让其自动重启(可通过发送短信或cb的方法加快重启过程),这样就可以进行调试。
ConversationListItem.<init>(Context, AttributeSet) line: 130
Constructor.constructNative(Object[], Class, Class[], int, boolean) line: not available [native method]
Constructor.newInstance(Object...) line: 423
PhoneLayoutInflater(LayoutInflater).createView(String, String, AttributeSet) line: 594
PhoneLayoutInflater(LayoutInflater).createViewFromTag(View, String, AttributeSet) line: 696
PhoneLayoutInflater(LayoutInflater).inflate(XmlPullParser, ViewGroup, boolean) line: 469
PhoneLayoutInflater(LayoutInflater).inflate(int, ViewGroup, boolean) line: 397
ConversationListAdapter.newView(Context, Cursor, ViewGroup) line: 103
ConversationListAdapter(CursorAdapter).getView(int, View, ViewGroup) line: 250
ListView(AbsListView).obtainView(int, boolean[]) line: 2338
ListView.makeAndAddView(int, int, boolean, int, boolean) line: 1812
ListView.fillDown(int, int) line: 698
ListView.fillFromTop(int) line: 759
ListView.layoutChildren() line: 1631
ListView(AbsListView).onLayout(boolean, int, int, int, int) line: 2149
ConversationListItem实例创建的主要过程分析如下,在getView来获取view的时候,因为缓冲没有数据,会使用newView新建一个view,
public View getView(int position, View convertView, ViewGroup parent) {
View v;
if (convertView == null) {
v = newView(mContext, mCursor, parent);
} else {
v = convertView;
}
bindView(v, mContext, mCursor);
return v;
}
newView实际的过程是inflate一个布局文件,这个布局文件就是我们分析的layout,它是ConversationListItem形式的,我们知道,在inflate创建布局文件时,会根据节点创建view实例,所以会使用类的构造类Constructor来创建ConversationListItem实例。
public View newView(Context context, Cursor cursor, ViewGroup parent) {
if (LOCAL_LOGV) Log.v(TAG, "inflating new view");
return mFactory.inflate(R.layout.conversation_list_item, parent, false);
}
Checkbox的主要作用是用来勾选短信列表的某一个item,并进行删除操作的。
对于checkbox,在ConversationListItem里面定义了一个引用mSelectBox,
private CheckBox mSelectBox;
并在onFinishInflate里面初始化它,关联到Layout里面的select_box,
mSelectBox = (CheckBox) findViewById(R.id.select_box);
当列表项被点击时,执行ConversationList.onListItemClick(ListView, View, int, long)这个方法,在里面添加下面代码,当处于删除模式时,如下,将Conversation设置为check状态,然后更新界面,
if (mListAdapter != null && mDeleteMode) {
if (conv.isChecked()) {
conv.setIsChecked(false);
} else {
conv.setIsChecked(true);
}
mListAdapter.notifyDataSetChanged();
updateSelectView();
return;
}
更新界面时,执行ConversationListItem.bind(Context, Conversation) ,每加载一个item项都会执行一次bind,在这里,会根据mSelectMode 决定checkbox是否显示,根据Conversation的状态决定是否选中。
所以实际上,不是我们主观上看到的那样由checkbox决定了当前项目是否选中,而是当前项选中后决定checkbox的显示状态。
if (mSelectMode) {
mSelectBox.setVisibility(View.VISIBLE);
mAvatarView.setClickable(false);
} else {
mSelectBox.setVisibility(View.GONE);
mAvatarView.setClickable(true);
}
if (mConversation.isChecked()) {
mSelectBox.setChecked(true);
} else {
mSelectBox.setChecked(false);
}
上面就是checkbox在代码里的处理过程。
R.id.avatar用来显示发件人头像,定义如下,MmsQuickContactBadge继承自QuickContactBadge,再往上是ImageView,一个通用的视图组件。
private MmsQuickContactBadge mAvatarView;
其实例获取在ConversationListItem的onFinishInflate里面,
mAvatarView = (MmsQuickContactBadge) findViewById(R.id.avatar);
如果希望AvatarView能够被点击,相应事件,则可以看到在bind有如下代码,表示在删除模式下不能被点击,在列表模式下可以被点击。
if (mSelectMode) {
mAvatarView.setClickable(false);
} else {
mAvatarView.setClickable(true);
}
AvatarView被点击之后,执行的调用栈如下,可见是由View直接调用了MmsQuickContactBadge类的onClick,并不是ConversationList或ConversationListItem里做了onClick的相关处理。
MmsQuickContactBadge(QuickContactBadge).onClick(View) line: 217
MmsQuickContactBadge.onClick(View) line: 75
MmsQuickContactBadge(View).performClick() line: 4212
View$PerformClick.run() line: 17476
Handler.handleCallback(Message) line: 800
ViewRootImpl$ViewRootHandler(Handler).dispatchMessage(Message) line: 100
QuickContactBadge类的onClick代码如下,
@Override
public void onClick(View v) {
if (mContactUri != null) {
QuickContact.showQuickContact(getContext(), QuickContactBadge.this, mContactUri,
QuickContact.MODE_LARGE, mExcludeMimes);
} else if (mContactEmail != null) {
mQueryHandler.startQuery(TOKEN_EMAIL_LOOKUP_AND_TRIGGER, mContactEmail,
Uri.withAppendedPath(Email.CONTENT_LOOKUP_URI, Uri.encode(mContactEmail)),
EMAIL_LOOKUP_PROJECTION, null, null, null);
} else if (mContactPhone != null) {
mQueryHandler.startQuery(TOKEN_PHONE_LOOKUP_AND_TRIGGER, mContactPhone,
Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI, mContactPhone),
PHONE_LOOKUP_PROJECTION, null, null, null);
} else {
// If a contact hasn't been assigned, don't react to click.
return;
}
}
我们当前的代码走mContactPhone路径,mContactPhone是在assignContactFromPhone里面赋值的,assignContactFromPhone则是通过ConversationListItem的updateAvatarView调用的(传入的number就赋值给mContactPhone),
mAvatarView.assignContactFromPhone(number, true);
updateAvatarView可以被updateFromView和bind调用,这两个方法我们在前面有过描述。所以从整个过程来看,我们可以看出avatar被点击和其需要的数据来源。
3.2 绘制分析
3.2.1 Message列表创建
如ConversationList的layout描述,Messag列表主要是由list呈现,而list的item项是由ConversationListItem布局的,我们这里讨论每个item项的创建以及整个list的创建。
在使用Cache时,ConversationListItem的创建过程如下,
ConversationListItem.bind(Context, Conversation) line: 480
ConversationListAdapter.bindView(View, Context, Cursor) line: 77
ConversationListAdapter(CursorAdapter).getView(int, View, ViewGroup) line: 250
ListView(AbsListView).obtainView(int, boolean[]) line: 2186
ListView.makeAndAddView(int, int, boolean, int, boolean) line: 1877
ListView.fillSpecific(int, int) line: 1356
ListView.layoutChildren() line: 1671
ListView(AbsListView).onLayout(boolean, int, int, int, int) line: 2037
ListView(View).layout(int, int, int, int) line: 14118
ListView(ViewGroup).layout(int, int, int, int) line: 4467
RelativeLayout.onLayout(boolean, int, int, int, int) line: 1021
从上往下分析该栈:
1)ConversationListItem.bind
ConversationListItem.bind的代码比较多,仔细分析,实际上逻辑比较简单,主要是设置每个view的内容和状态,比如说设置日期的代码如下,
mDateView.setVisibility(VISIBLE);
if(ConversationList.sConversationListOption ==
ConversationList.OPTION_CONVERSATION_LIST_IMPORTANT &&
!conversation.hasUnreadMessages()) {
mDateView.setText(MessageUtils.formatTimeStampStringExtend(context,
conversation.getImpDate()));
}
其数据来源于conversation,我们要进一步分析conversation是怎么传递进来的,以及为什么要调用bind。
2)ConversationListAdapter
ConversationListAdapter 是ConversationList的layout里的list对应的Adapter,它继承自MessageCursorAdapter-CursorAdapter-baseAdapter, 其方法bindView在本例中执行的主要代码如下,其中headerview是一个ConversationListItem引用,是调用方法传递过来的,conversation是根据游标新建的,最后调用bind,所以该方法的作用就是获取到conversation,并用准备将其中的数据赋值到view。
public void bindView(View view, Context context, Cursor cursor) {
ConversationListItem headerView = (ConversationListItem) view;
Conversation conv;
if (!mIsScrolling) {
Conversation.setNeedCacheConv(false);
conv = Conversation.from(context, cursor);
Conversation.setNeedCacheConv(true);
if (mSubjectSingleLine) {
headerView.setSubjectSingleLineMode(true);
}
if (conv != null) {
conv.setIsChecked(sSelectedTheadsId.contains(conv.getThreadId()));
}
headerView.bind(context, conv);
}
}
(CursorAdapter).getView则负责获取到view实例和游标实例。
view的获取有两种方式,一种是调用者传递过来以前缓存的,一种是新创建一个,在ConversationListItem实例创建中有讲到如何创建。
游标实例则在Adapter创建时创建。
3)ListView
在ListView部分,我们从layoutChildren向上分析,向下部分设计到太多ListView的实现机制问题,暂不讨论。
layoutChildren实现ListView子窗口布局,实际上就是list在layout所占部分。
本例从launcher进入Message列表,执行代码路径如下,mSyncPosition是当前item索引值,mSpecificTop是当前layout相对Top的位置。
layoutChildren:
case LAYOUT_SYNC:
sel = fillSpecific(mSyncPosition, mSpecificTop);
fillSpecific在知道位置绘制指定项的item数据,然后上下绘制其他的item。它先通过makeAndAddView完成当前item的创建并添加到layout,fillDown、fillUp则根据layout大小,上下绘制item完成整个list的绘制,使用的方法也是makeAndAddView,adjustViewsUpOrDown调整item位置。
/**
* Put a specific item at a specific location on the screen and then build
* up and down from there.
*
* @param position The reference view to use as the starting point
* @param top Pixel offset from the top of this view to the top of the
* reference view.
*
* @return The selected view, or null if the selected view is outside the
* visible area.
*/
private View fillSpecific(int position, int top) {
boolean tempIsSelected = position == mSelectedPosition;
View temp = makeAndAddView(position, top, true, mListPadding.left, tempIsSelected);
…
below = fillDown(position + 1, temp.getBottom() + dividerHeight);
// This will correct for the bottom of the last view not touching the bottom of the list
adjustViewsUpOrDown();
above = fillUp(position - 1, temp.getTop() - dividerHeight);
int childCount = getChildCount();
if (childCount > 0) {
correctTooLow(childCount);
}
}
顾名思义,makeAndAddView就是新建一个view,并将它添加到list里面。obtainView负责创建view,setupChild负责将view添加到layout,并设置好view的位置和相关属性。
private View makeAndAddView(int position, int y, boolean flow, int childrenLeft,
boolean selected) {
View child;
child = obtainView(position, mIsScrap);
// This needs to be positioned and measured
setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]);
return child;
}
obtainView将对应位置的view和数据关联起来,并返回view。它通过mRecycler获取到缓存的view,再通过mAdapter.getView将数据填充到view,mAdapter.getView即是我们前面分析到的ConversationListAdapter(CursorAdapter).getView。
至此,Message列表的创建过程就明了了。
3.3 功能分析
3.3.1 Message列表更新
当数据库内容发生变化(包括新短信、删除、发送状态改变等),就会触发onContentChanged,其调用栈如下:
ConversationList$2.onContentChanged(ConversationListAdapter) line: 474
ConversationListAdapter.onContentChanged() line: 118
CursorAdapter$ChangeObserver.onChange(boolean) line: 463
CursorAdapter$ChangeObserver(ContentObserver).onChange(boolean, Uri) line: 129
ContentObserver$NotificationRunnable.run() line: 180
Handler.handleCallback(Message) line: 808
Handler.dispatchMessage(Message) line: 103
Looper.loop() line: 193
从下往上分析,
1)ContentObserver
我们已经很熟悉handleCallback处理的是Post的消息,消息一般是runable,所以可以看到ContentObserver. dispatchChange有post这一消息的过程(dispatchChange的触发过程比较复杂,我们将在其他章节介绍,本例中当数据库内容变化时会触发这个流程)。
public final void dispatchChange(boolean selfChange, Uri uri) {
if (mHandler == null) {
onChange(selfChange, uri);
} else {
mHandler.post(new NotificationRunnable(selfChange, uri));
}
}
然后这个消息会运行其内部类NotificationRunnable的run方法,
public void run() {
ContentObserver.this.onChange(mSelfChange, mUri);
}
This指向的是ContentObserver的实例ChangeObserver,它是CursorAdapter的内部类,也是ContentObserver的子类,由于ConversationListAdapter通过CursorWrapper向ContentObservable注册了ContentObserver,所以当dispatchChange事件时,通过轮询,最终将事件传递给ChangeObserver。
而后,ChangeObserver调用封装类CursorAdapter的onContentChanged,将事件上传。
private class ChangeObserver extends ContentObserver {
public void onChange(boolean selfChange) {
onContentChanged();
}
}
2)onContentChanged
onContentChanged是CursorAdapter的一个方法,主要功能是当数据库内容更新后,重新使用游标查询数据。但这个方法被ConversationListAdapter重载,它给自己的客户提供一个借口类OnContentChangedListener,让客户去实现这个借口类,自己则调用接口类的onContentChanged方法执行客户定义的功能。
protected void onContentChanged() {
if (mCursor != null && !mCursor.isClosed()) {
if (mOnContentChangedListener != null) {
mOnContentChangedListener.onContentChanged(this);
}
}
}
3)ConversationList
ConversationList就是ConversationListAdapter的客户,它实现了接口类,如下,主要是调用startAsyncQuery进行数据的查询。
private final ConversationListAdapter.OnContentChangedListener mContentChangedListener =
new ConversationListAdapter.OnContentChangedListener() {
@Override
public void onContentChanged(ConversationListAdapter adapter) {
if (mIsInActivity) {
mNeedQuery = true;
startAsyncQuery();
}
}
};
并且ConversationList在其onStart向ConversationListAdapter注册了这个接口类监听器:
mListAdapter.setOnContentChangedListener(mContentChangedListener);
所以,当数据库数据改变时,根据上面相关调用流程,完成了Message列表的刷新功能。
BTW,附上游标改变后注册ContentObserver的调用栈:
ContentResolver$CursorWrapperInner(CursorWrapper).registerContentObserver(ContentObserver) line: 178
ConversationListAdapter(CursorAdapter).swapCursor(Cursor) line: 340
ConversationListAdapter(CursorAdapter).changeCursor(Cursor) line: 313
ConversationList$ThreadListQueryHandler.onQueryComplete(int, Object, Cursor) line: 1896
ConversationList$ThreadListQueryHandler(AsyncQueryHandler).handleMessage(Message) line: 344
ConversationList$ThreadListQueryHandler(Handler).dispatchMessage(Message) line: 110
Looper.loop() line: 193
3.3.2 Message列表定位
因为ConversationList是ListActivity,所以可以使用getListView获取ListView,再使用ListView的setSelectionFromTop,将列表显示开始位置指定到特定位置。
需要注意2个问题,
1)
在ConversationList会记录上次浏览列表的起始位置,所以不是随意就能设置好列表的显示位置,如onQueryComplete通过下面代码控制了什么时候可以设置这个起始位置,我们需要根据mSavedFirstVisiblePosition的值和相关逻辑去处理。
if (mSavedFirstVisiblePosition != AdapterView.INVALID_POSITION) {
// Restore the list to its previous position.
getListView().setSelectionFromTop(mSavedFirstVisiblePosition,
mSavedFirstItemOffset);
mSavedFirstVisiblePosition = AdapterView.INVALID_POSITION;
}
结合列表的创建过程,我们知道,当同步绘制列表时,是从mSyncPosition开始的,
fillSpecific(mSyncPosition, mSpecificTop);
而mSyncPosition正是setSelectionFromTop里传递进去的,所以列表起始位置只能通过setSelectionFromTop来设置。
public void setSelectionFromTop(int position, int y) {
…
mSyncPosition = position;
…
}
2)
游标的更新,当数据库数据变化后,需要更新游标,才能获取到正确的值。下面是onQueryComplete完成后,用changeCursor替换掉旧游标的过程,防止游标数据过期。
ConversationListAdapter(CursorAdapter).swapCursor(Cursor) line: 340
ConversationListAdapter(CursorAdapter).changeCursor(Cursor) line: 313
ConversationList$ThreadListQueryHandler.onQueryComplete(int, Object, Cursor) line: 1896
ConversationList$ThreadListQueryHandler(AsyncQueryHandler).handleMessage(Message) line: 344
ConversationList$ThreadListQueryHandler(Handler).dispatchMessage(Message) line: 110
Looper.loop() line: 193
在列表创建时,CursorAdapter. getView的游标最初是在CursorAdapte创建时传入的,之后可以通过swapCursor的方法更新。
目 录
2. ConversationList
2.1.2.1 ConversationListAdapter
2.2.1 列表项click
2.2.2 Long Press click to Delete
2.3.1 会话ID创建
3. ConversationListItem
3.1.2.1 ConversationListItem实例创建
1. 概要
本文主要分析短信列表界面的主要控件的界面显示和处理逻辑。
2. ConversationList
2.1 ConversationList
2.1.1 Layout分析
ConversationList对应的layout是conversation_list_screen,它主要分3部分,第一部分是做全选的控制,第二部分是list列表,第三部分是列表为空的布局。具体的表现形式如下,布局文件就不再展开了。其中list列表将在另外的章节分析。
2.1.2 代码分析
2.1.2.1 ConversationListAdapter
ConversationListAdapter是ConversationListItem的数据适配器,在ConversationList.onCreate里创建,它通过bindView将会话信息绑定到item的layout上。
其创建过程如下,
private void initListAdapter() {
mListAdapter = new ConversationListAdapter(this, null);
/** M: now this code is useless and will lead to a JE, comment it.
* listener is set in onStart
*/
//mListAdapter.setOnContentChangedListener(mContentChangedListener);
setListAdapter(mListAdapter);
getListView().setRecyclerListener(mListAdapter);
}
因为ConversationList继承自ListActivity,setListAdapter将Adapter实例设置给ListActivity,这里会调用ensureList,它会加载一个布局,确保有view能用来对应Adapter。
public void setListAdapter(ListAdapter adapter) {
synchronized (this) {
ensureList();
mAdapter = adapter;
mList.setAdapter(adapter);
}
}
private void ensureList() {
if (mList != null) {
return;
}
setContentView(com.android.internal.R.layout.list_content_simple);
}
而实际上,mList已经在 onContentChanged被初始化为Layout里对应的ListView,
mList = (ListView)findViewById(com.android.internal.R.id.list);
这个ListView就是Message列表的容器了。
2.2 功能分析
2.2.1 列表项click
单击列表项的调用栈如下:
ConversationList.onListItemClick(ListView, View, int, long) line: 1086
ListActivity$2.onItemClick(AdapterView, View, int, long) line: 319
ListView(AdapterView).performItemClick(View, int, long) line: 298
ListView(AbsListView).performItemClick(View, int, long) line: 1128
AbsListView$PerformClick.run() line: 2815
AbsListView$1.run() line: 3574
Handler.handleCallback(Message) line: 800
ViewRootImpl$ViewRootHandler(Handler).dispatchMessage(Message) line: 100
2.2.2 Long Press click to Delete
长按item删除会话流程如下:
长按出现选择菜单,
mConvListOnCreateContextMenuListener
onCreateContextMenu
点击删除
ConversationList.confirmDeleteThreads(Collection, AsyncQueryHandler) line:
ConversationList.confirmDeleteThread(long, AsyncQueryHandler) line: 1320
ConversationList.onContextItemSelected(MenuItem) line: 1247
ConversationList(Activity).onMenuItemSelected(int, MenuItem) line: 2584
PhoneWindow$DialogMenuCallback.onMenuItemSelected(MenuBuilder, MenuItem) line:
开始删除之前,要做一个后台查询,看是否有lock住的消息,Conversation.startQueryHaveLockedMessages,
之后是后台查询handler.startQuery:
AsyncQueryHandler
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);
msg.arg1 = EVENT_ARG_QUERY;
WorkerArgs args = new WorkerArgs();
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);
}
这个查询过程将消息发给mWorkerThreadHandler,这是异步查询AsyncQueryHandler自己的handler,它封装完消息EVENT_ARG_QUERY后,再发送出去,
Message reply = args.handler.obtainMessage(token);
reply.obj = args;
reply.arg1 = msg.arg1;
reply.sendToTarget();
这个消息的接收对象的设置见args.handler = this; ,在本例中,这个handler是ThreadListQueryHandler。
数据库查询完成后执行onQueryComplete(HAVE_LOCKED_MESSAGES_TOKEN),
ConversationList$ThreadListQueryHandler.onQueryComplete(int, Object, Cursor) line: 1610
ConversationList$ThreadListQueryHandler(AsyncQueryHandler).handleMessage(Message) line: 344
ConversationList$ThreadListQueryHandler(Handler).dispatchMessage(Message) line: 107
Looper.loop() line: 194
之后会给出一个确认删除对话框confirmDeleteThreadDialog
在弹出的确认对话框中,选择删除,执行listener函数。
ConversationList$DeleteThreadListener.onClick(DialogInterface, int) line: 1484
AlertController$ButtonHandler.handleMessage(Message) line: 176
之后开始删除Conversation.startDelete
最后onDeleteComplete
响应函数会触发一系列其他的数据库操作,如查询list,查询unread等,
AsyncQueryHandler$WorkerHandler.handleMessage(Message) line: 120
AsyncQueryHandler$WorkerHandler(Handler).dispatchMessage(Message) line: 107
【Tips】
它的threadIds是在点击删除功能时,onContextItemSelected设置的,和长按的时候在onCreateContextMenu里选择的有时间差,不一定是同一个,在会话列表更新的时候,会有影响,所以会误删。
所以在onCreateContextMenu里要记录thread id。
2.3 数据操作
2.3.1 会话ID创建
Message列表是以会话为item进行显示的,会话是以为ID记录的,我们分析一下各种会话ID是如何创建的,
应用在CBMessageReceiverService.java收到小区广播后,使用handleCBMessageReceived方法进行处理,其中insertMessage是用来存储CB的。
insertMessage会调用CbProvider的insert方法
public Uri insert(Uri url, ContentValues initialValues) {
Uri result = null;
// TODO Check Permission
// checkPermission();
SQLiteDatabase db = mOpenHelper.getWritableDatabase();
SQLiteDatabase dbmmssms = mMmsSmsOpenHelper.getWritableDatabase();
int match = URI_MATCHER.match(url);
ContentValues values;
long rowID;
String table = null;
Log.d(TAG, " insert match = "+match);
switch (match) {
case URL_MESSAGES:
//table = CbDatabaseHelper.CBMESSAGE_TABLE;
table = MmsSmsDatabaseHelper.TABLE_CELLBROADCAST;
values = internalInsertMessages(initialValues);
rowID = dbmmssms.insert(table, null, values);
Log.d(TAG, "insert to cellbroadcast " + values);
if (rowID > 0) {
result = Uri.parse("content://messages/" + rowID);
notifyChange();
return result;
}
break;
…
}
在这里,我们重点关注一下CB Message的ThreadId是怎样获取的,在internalInsertMessages里面,显然threadId不是我们主动填充进去的,所以是通过getOrCreateThreadId获得的。
private ContentValues internalInsertMessages(ContentValues initialValues) {
ContentValues values;
if (initialValues != null) {
values = new ContentValues(initialValues);
} else {
values = new ContentValues();
}
Long threadId = values.getAsLong(EncapsulatedTelephony.SmsCb.THREAD_ID);
String address = values.getAsString(EncapsulatedTelephony.SmsCb.CHANNEL_ID);
if (((threadId == null) || (threadId == 0)) && (address != null)) {
values.put(EncapsulatedTelephony.SmsCb.THREAD_ID,
getOrCreateThreadId(address));
}
…
}
getOrCreateThreadId代码如下,它封装一个URI地址,包含了threadID和参数recipient、cellbroadcast,并使用SqliteWrapper.query进行查询,
private String getOrCreateThreadId(String address) {
//Uri.Builder uriBuilder = THREAD_ID_URI.buildUpon();
Uri.Builder uriBuilder = (Uri.parse("content://mms-sms/threadID")).buildUpon();
uriBuilder.appendQueryParameter("recipient", address);
uriBuilder.appendQueryParameter("cellbroadcast", address);
Uri uri = uriBuilder.build();
// TODO need replace with helper interface.
Cursor cursor = null;
long token = Binder.clearCallingIdentity();
try {
cursor = SqliteWrapper.query(this.getContext(), this
.getContext().getContentResolver(), uri, ID_PROJECTION, null,
null, null);
…
if (cursor.moveToFirst()) {
return String.valueOf(cursor.getLong(0));
…
}
SqliteWrapper会调用到ContentResolver,ContentResolver则最终调用到MmsSmsProvider,
在MmsSmsProvider.query里面,也是通过URI match找到操作码URI_THREAD_ID,根据参数cellbroadcast,使用getCBThreadId给CB查找或创建其ThreadId。
Threads表内容如下所示,里面的关键字段有_id ,recipient_ids,要想找到合适的ThreadId,只有通过recipient_ids来查询,
所以在getCBThreadId方法里面,通过getAddressIds从canonical_addresses表里面找到和号码匹配的recipient_ids列表,再到Threads表里面找到recipient_ids相同和type是小区广播的_id,即是我们需要的ThreadId,使用的查询语句如下。
String queryString = "SELECT _id FROM threads " + "WHERE type=" + EncapsulatedTelephony.Threads.CELL_BROADCAST_THREAD + " AND recipient_ids=?";
如果没有满足条件的ThreadId,调用insertThread()创建一个新的thread。
在Message列表界面通过按钮新建短信时,执行createNewMessage(),通过createIntent传递一个threadId=0的ID过去,
private void createNewMessage() {
startActivity(ComposeMessageActivity.createIntent(this, 0));
}
public static Intent createIntent(Context context, long threadId) {
Intent intent = new Intent(context, ComposeMessageActivity.class);
if (threadId > 0) {
intent.setData(Conversation.getUri(threadId));
}
return intent;
}
因为threadId为0,在会话创建中会获取或创建一个threadId,具体过程和下节相同。
在联系人的详情界面,点击发送短信,因为有号码存在,所以在ComposeMessageActivity.Oncreate方法里面会去查询或创建一个threadId。
onCreate的initActivityState里,有这么一段代码是获取会话实例的,mConversation.get通过号码获取到会话实例,
Uri intentData = intent.getData();
/// M: Code analyze 034, If intent is SEND,just create a new empty thread,
/// otherwise Conversation.get() will throw exception.
String action = intent.getAction();
if (intentData != null && (TextUtils.isEmpty(action) ||
!action.equals(Intent.ACTION_SEND))) {
/// M: group-contact send message
// try to get a conversation based on the data URI passed to our intent.
if (intentData.getPathSegments().size() < 2) {
mConversation = mConversation.get(getApplicationContext(),ContactList.getByNumbers(
getStringForMultipleRecipients(Conversation.getRecipients(intentData)),
false /* don't block */, true /* replace number */),false);
} else {
mConversation = Conversation.get(getApplicationContext(), intentData, false);
}
/// @}
mWorkingMessage.setText(getBody(intentData));
Conversation get代码如下,如果号码为空则新建会话,如果缓冲里存储了对应号码的会话,则使用缓冲里的数据,如果条件都不满足,则需要去数据库查询到ID,并新建会话,之后将新会话存放到缓冲。
public static Conversation get(Context context, ContactList recipients, boolean allowQuery) {
if (recipients.size() < 1) {
return createNew(context);
}
Conversation conv = Cache.get(recipients);
if (conv != null) {
return conv;
}
long threadId = getOrCreateThreadId(context, recipients);
conv = new Conversation(context, threadId, allowQuery);
。。。
Cache.put(conv);
}
getOrCreateThreadId用来获取ThreadId,由Threads.getOrCreateThreadId实现,这个方法在Telephony.java里面。
getOrCreateThreadId类似前面提到的小区广播的实现过程,先封装一个"content://mms-sms/threadID"的URI,但只使用一个参数recipient,
在MmsSmsProvider.query里面,也是通过URI match找到操作码URI_THREAD_ID,,使用getThreadId查找或创建其ThreadId,getAddressIds获取到匹配的recipientIds,再取Threads表里查询,如果查询到,返回游标,如果没有记录,则用insertThread向Threads里插入记录,再查询到ThreadId。
private synchronized Cursor getThreadId(List<String> recipients) {
Set<Long> addressIds = getAddressIds(recipients);
。。。
recipientIds = Long.toString(addressId);
。。。
String[] selectionArgs = new String[] { recipientIds };
SQLiteDatabase db = mOpenHelper.getReadableDatabase();
db.beginTransaction();
Cursor cursor = null;
try {
// Find the thread with the given recipients
cursor = db.rawQuery(THREAD_QUERY, selectionArgs);
if (cursor.getCount() == 0) {
// No thread with those recipients exists, so create the thread.
cursor.close();
Log.d(LOG_TAG, "getThreadId: create new thread_id for recipients " + recipients);
insertThread(recipientIds, recipients);
// The thread was just created, now find it and return it.
cursor = db.rawQuery(THREAD_QUERY, selectionArgs);
。。。
}
3. ConversationListItem
3.1 ConversationListItem
3.1.1 Layout分析
如前所述,短信记录的每条信息显示是使用conversation_list_item.xml布局的,在本例中,其直观显示是如下形式的一条短信记录,
conversation_list_item.xml文件如下,它是一个ConversationListItem类型的布局,
(需要注意不同屏幕使用这个文件的位置,本例中在packages\apps\Mms\res\layout-mdpi下面,不是packages\apps\Mms\res\layout)
conversation_list_item.xml
<?xml version="1.0" encoding="utf-8"?>
<com.android.mms.ui.ConversationListItem xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="?android:attr/listPreferredItemHeight"
android:background="?android:attr/activatedBackgroundIndicator" >
<CheckBox
android:id="@+id/select_box"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_centerVertical="true"
android:clickable="false"
android:focusable="false"
android:focusableInTouchMode="false" />
<com.android.mms.ui.MmsQuickContactBadge
android:id="@+id/avatar"
android:visibility="gone"
android:layout_centerVertical="true"
android:layout_toEndOf="@id/select_box"
style="?android:attr/quickContactBadgeStyleWindowLarge" />
<ImageView
android:id="@+id/presence"
android:visibility="invisible"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_alignParentTop="true"
android:src="@drawable/ic_unread_label"/>
<LinearLayout
android:id="@+id/iconlist"
android:layout_alignParentTop="true"
android:layout_alignParentEnd="true"
android:layout_marginTop="7dip"
android:layout_marginEnd="10dip"
android:orientation="horizontal"
android:layout_height="wrap_content"
android:layout_width="wrap_content" >
<ImageView android:id="@+id/draft"
android:visibility="gone"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:src="@drawable/ic_draft" />
<ImageView android:id="@+id/error"
android:paddingStart="6dp"
android:visibility="gone"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:src="@drawable/ic_list_alert_sms_failed" />
<ImageView android:id="@+id/attachment"
android:paddingStart="6dp"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:visibility="gone"
android:src="@drawable/ic_attachment_universal_small" />
<ImageView android:id="@+id/mute"
android:paddingStart="6dp"
android:visibility="gone"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:src="@drawable/ipmsg_silent" />
</LinearLayout>
<RelativeLayout
android:layout_alignParentTop="true"
android:layout_marginTop="4dip"
android:layout_marginStart="12dip"
android:layout_toEndOf="@id/avatar"
android:layout_toStartOf="@id/iconlist"
android:gravity="center_vertical"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView android:id="@+id/unread"
android:background="@drawable/ipmsg_message_box"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:textSize="12sp"
android:textColor="@color/text_color_unread"
android:singleLine="true"
android:layout_alignParentEnd="true" />
<RelativeLayout android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:layout_toStartOf="@id/unread">
<TextView android:id="@+id/draft_and_smgcount"
android:textAppearance="?android:attr/textAppearanceMedium"
android:layout_alignParentEnd="true"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="6dip"
android:singleLine="true" />
<com.android.mms.util.AlwaysMarqueeTextView android:id="@+id/from"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceMedium"
android:singleLine="true"
android:layout_alignParentStart="true"
android:layout_toStartOf="@id/draft_and_smgcount"
android:layout_marginEnd="6dip"
android:ellipsize="marquee" />
</RelativeLayout>
</RelativeLayout>
<TextView android:id="@+id/date"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:textSize= "12sp"
android:textColor="@color/dlg_text_counter_color"
android:layout_alignParentBottom="true"
android:layout_alignParentEnd="true"
android:layout_marginEnd="10dip"
android:layout_marginBottom="4dip"
android:gravity="bottom" />
<TextView android:id="@+id/subject"
android:layout_height="wrap_content"
android:layout_width="fill_parent"
android:textSize= "12sp"
android:layout_alignParentBottom="true"
android:layout_toEndOf="@id/avatar"
android:layout_toStartOf="@id/date"
android:layout_marginStart="12dip"
android:layout_marginBottom="4dip"
android:gravity="bottom"
android:textColor="@color/dlg_text_counter_color"
android:lines="2"
android:ellipsize="end" />
</com.android.mms.ui.ConversationListItem>
其对应的布局图形如下,图中左边是layout组成,右上是layout布局表现,右下是对layout里面的子view编号,便于理解。
分析xml文件,并对照图例可以看出,
ConversationListItem是整个item布局类,表现形式见下图左边,它指定了高度
android:layout_height="?android:attr/listPreferredItemHeight"
这个布局由6个同级view组成,见下图中间及右上,其中
checkbox供选择使用,见右下数字0,alignParentStart表示它在开头位置,
android:layout_alignParentStart="true"
MmsQuickContactBadge做头像显示,见右下数字1,
ImageView做未读标识等用途,见右下数字2,通过下面的属性将它放置在最右上的位置
android:layout_alignParentEnd="true"
android:layout_alignParentTop="true"
LinearLayout是个组合视图,见右下数字3和iconlist小图,它有四个子ImageView表示短信的信息状态,具体名字见iconlist小图。注意四个子ImageView表示同时最多只能显示四个图标,不是表示短信只有这四种状态。通过下面属性,将它放置在MmsQuickContactBadge后面。
android:orientation="horizontal"
<iconlist>
RelativeLayout是个组合视图,见右下数字4和Text Title小图,包含3个TextView,用来显示Title和消息数目、未读消息数目。在RelativeLayout,通过下面的属性设置,将RelativeLayout(index4的view)和上面的LinearLayout(index3的view)并行放置在同一行。
android:layout_toEndOf="@id/avatar"
android:layout_toStartOf="@id/iconlist"
TextView显示日期,见右下数字5,用下面2个属性表示它的位置在右下,
android:layout_alignParentBottom="true"
android:layout_alignParentEnd="true"
TextView 显示短线截取的部分内容,见右下数字6,用下面3个属性表明了它的位置。
android:layout_alignParentBottom="true"
android:layout_toEndOf="@id/avatar"
android:layout_toStartOf="@id/date"
<Text Content>
3.1.2 代码分析
3.1.2.1 ConversationListItem实例创建
ConversationListItem实例创建栈如下,因为有缓存使用,不一定每次进入列表都会创建这个类实例,所以不一定能调试到,要在此设置断点调试,有一个小技巧,就是将mms进程终止掉,让其自动重启(可通过发送短信或cb的方法加快重启过程),这样就可以进行调试。
ConversationListItem.<init>(Context, AttributeSet) line: 130
Constructor.constructNative(Object[], Class, Class[], int, boolean) line: not available [native method]
Constructor.newInstance(Object...) line: 423
PhoneLayoutInflater(LayoutInflater).createView(String, String, AttributeSet) line: 594
PhoneLayoutInflater(LayoutInflater).createViewFromTag(View, String, AttributeSet) line: 696
PhoneLayoutInflater(LayoutInflater).inflate(XmlPullParser, ViewGroup, boolean) line: 469
PhoneLayoutInflater(LayoutInflater).inflate(int, ViewGroup, boolean) line: 397
ConversationListAdapter.newView(Context, Cursor, ViewGroup) line: 103
ConversationListAdapter(CursorAdapter).getView(int, View, ViewGroup) line: 250
ListView(AbsListView).obtainView(int, boolean[]) line: 2338
ListView.makeAndAddView(int, int, boolean, int, boolean) line: 1812
ListView.fillDown(int, int) line: 698
ListView.fillFromTop(int) line: 759
ListView.layoutChildren() line: 1631
ListView(AbsListView).onLayout(boolean, int, int, int, int) line: 2149
ConversationListItem实例创建的主要过程分析如下,在getView来获取view的时候,因为缓冲没有数据,会使用newView新建一个view,
public View getView(int position, View convertView, ViewGroup parent) {
View v;
if (convertView == null) {
v = newView(mContext, mCursor, parent);
} else {
v = convertView;
}
bindView(v, mContext, mCursor);
return v;
}
newView实际的过程是inflate一个布局文件,这个布局文件就是我们分析的layout,它是ConversationListItem形式的,我们知道,在inflate创建布局文件时,会根据节点创建view实例,所以会使用类的构造类Constructor来创建ConversationListItem实例。
public View newView(Context context, Cursor cursor, ViewGroup parent) {
if (LOCAL_LOGV) Log.v(TAG, "inflating new view");
return mFactory.inflate(R.layout.conversation_list_item, parent, false);
}
Checkbox的主要作用是用来勾选短信列表的某一个item,并进行删除操作的。
对于checkbox,在ConversationListItem里面定义了一个引用mSelectBox,
private CheckBox mSelectBox;
并在onFinishInflate里面初始化它,关联到Layout里面的select_box,
mSelectBox = (CheckBox) findViewById(R.id.select_box);
当列表项被点击时,执行ConversationList.onListItemClick(ListView, View, int, long)这个方法,在里面添加下面代码,当处于删除模式时,如下,将Conversation设置为check状态,然后更新界面,
if (mListAdapter != null && mDeleteMode) {
if (conv.isChecked()) {
conv.setIsChecked(false);
} else {
conv.setIsChecked(true);
}
mListAdapter.notifyDataSetChanged();
updateSelectView();
return;
}
更新界面时,执行ConversationListItem.bind(Context, Conversation) ,每加载一个item项都会执行一次bind,在这里,会根据mSelectMode 决定checkbox是否显示,根据Conversation的状态决定是否选中。
所以实际上,不是我们主观上看到的那样由checkbox决定了当前项目是否选中,而是当前项选中后决定checkbox的显示状态。
if (mSelectMode) {
mSelectBox.setVisibility(View.VISIBLE);
mAvatarView.setClickable(false);
} else {
mSelectBox.setVisibility(View.GONE);
mAvatarView.setClickable(true);
}
if (mConversation.isChecked()) {
mSelectBox.setChecked(true);
} else {
mSelectBox.setChecked(false);
}
上面就是checkbox在代码里的处理过程。
R.id.avatar用来显示发件人头像,定义如下,MmsQuickContactBadge继承自QuickContactBadge,再往上是ImageView,一个通用的视图组件。
private MmsQuickContactBadge mAvatarView;
其实例获取在ConversationListItem的onFinishInflate里面,
mAvatarView = (MmsQuickContactBadge) findViewById(R.id.avatar);
如果希望AvatarView能够被点击,相应事件,则可以看到在bind有如下代码,表示在删除模式下不能被点击,在列表模式下可以被点击。
if (mSelectMode) {
mAvatarView.setClickable(false);
} else {
mAvatarView.setClickable(true);
}
AvatarView被点击之后,执行的调用栈如下,可见是由View直接调用了MmsQuickContactBadge类的onClick,并不是ConversationList或ConversationListItem里做了onClick的相关处理。
MmsQuickContactBadge(QuickContactBadge).onClick(View) line: 217
MmsQuickContactBadge.onClick(View) line: 75
MmsQuickContactBadge(View).performClick() line: 4212
View$PerformClick.run() line: 17476
Handler.handleCallback(Message) line: 800
ViewRootImpl$ViewRootHandler(Handler).dispatchMessage(Message) line: 100
QuickContactBadge类的onClick代码如下,
@Override
public void onClick(View v) {
if (mContactUri != null) {
QuickContact.showQuickContact(getContext(), QuickContactBadge.this, mContactUri,
QuickContact.MODE_LARGE, mExcludeMimes);
} else if (mContactEmail != null) {
mQueryHandler.startQuery(TOKEN_EMAIL_LOOKUP_AND_TRIGGER, mContactEmail,
Uri.withAppendedPath(Email.CONTENT_LOOKUP_URI, Uri.encode(mContactEmail)),
EMAIL_LOOKUP_PROJECTION, null, null, null);
} else if (mContactPhone != null) {
mQueryHandler.startQuery(TOKEN_PHONE_LOOKUP_AND_TRIGGER, mContactPhone,
Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI, mContactPhone),
PHONE_LOOKUP_PROJECTION, null, null, null);
} else {
// If a contact hasn't been assigned, don't react to click.
return;
}
}
我们当前的代码走mContactPhone路径,mContactPhone是在assignContactFromPhone里面赋值的,assignContactFromPhone则是通过ConversationListItem的updateAvatarView调用的(传入的number就赋值给mContactPhone),
mAvatarView.assignContactFromPhone(number, true);
updateAvatarView可以被updateFromView和bind调用,这两个方法我们在前面有过描述。所以从整个过程来看,我们可以看出avatar被点击和其需要的数据来源。
3.2 绘制分析
3.2.1 Message列表创建
如ConversationList的layout描述,Messag列表主要是由list呈现,而list的item项是由ConversationListItem布局的,我们这里讨论每个item项的创建以及整个list的创建。
在使用Cache时,ConversationListItem的创建过程如下,
ConversationListItem.bind(Context, Conversation) line: 480
ConversationListAdapter.bindView(View, Context, Cursor) line: 77
ConversationListAdapter(CursorAdapter).getView(int, View, ViewGroup) line: 250
ListView(AbsListView).obtainView(int, boolean[]) line: 2186
ListView.makeAndAddView(int, int, boolean, int, boolean) line: 1877
ListView.fillSpecific(int, int) line: 1356
ListView.layoutChildren() line: 1671
ListView(AbsListView).onLayout(boolean, int, int, int, int) line: 2037
ListView(View).layout(int, int, int, int) line: 14118
ListView(ViewGroup).layout(int, int, int, int) line: 4467
RelativeLayout.onLayout(boolean, int, int, int, int) line: 1021
从上往下分析该栈:
1)ConversationListItem.bind
ConversationListItem.bind的代码比较多,仔细分析,实际上逻辑比较简单,主要是设置每个view的内容和状态,比如说设置日期的代码如下,
mDateView.setVisibility(VISIBLE);
if(ConversationList.sConversationListOption ==
ConversationList.OPTION_CONVERSATION_LIST_IMPORTANT &&
!conversation.hasUnreadMessages()) {
mDateView.setText(MessageUtils.formatTimeStampStringExtend(context,
conversation.getImpDate()));
}
其数据来源于conversation,我们要进一步分析conversation是怎么传递进来的,以及为什么要调用bind。
2)ConversationListAdapter
ConversationListAdapter 是ConversationList的layout里的list对应的Adapter,它继承自MessageCursorAdapter-CursorAdapter-baseAdapter, 其方法bindView在本例中执行的主要代码如下,其中headerview是一个ConversationListItem引用,是调用方法传递过来的,conversation是根据游标新建的,最后调用bind,所以该方法的作用就是获取到conversation,并用准备将其中的数据赋值到view。
public void bindView(View view, Context context, Cursor cursor) {
ConversationListItem headerView = (ConversationListItem) view;
Conversation conv;
if (!mIsScrolling) {
Conversation.setNeedCacheConv(false);
conv = Conversation.from(context, cursor);
Conversation.setNeedCacheConv(true);
if (mSubjectSingleLine) {
headerView.setSubjectSingleLineMode(true);
}
if (conv != null) {
conv.setIsChecked(sSelectedTheadsId.contains(conv.getThreadId()));
}
headerView.bind(context, conv);
}
}
(CursorAdapter).getView则负责获取到view实例和游标实例。
view的获取有两种方式,一种是调用者传递过来以前缓存的,一种是新创建一个,在ConversationListItem实例创建中有讲到如何创建。
游标实例则在Adapter创建时创建。
3)ListView
在ListView部分,我们从layoutChildren向上分析,向下部分设计到太多ListView的实现机制问题,暂不讨论。
layoutChildren实现ListView子窗口布局,实际上就是list在layout所占部分。
本例从launcher进入Message列表,执行代码路径如下,mSyncPosition是当前item索引值,mSpecificTop是当前layout相对Top的位置。
layoutChildren:
case LAYOUT_SYNC:
sel = fillSpecific(mSyncPosition, mSpecificTop);
fillSpecific在知道位置绘制指定项的item数据,然后上下绘制其他的item。它先通过makeAndAddView完成当前item的创建并添加到layout,fillDown、fillUp则根据layout大小,上下绘制item完成整个list的绘制,使用的方法也是makeAndAddView,adjustViewsUpOrDown调整item位置。
/**
* Put a specific item at a specific location on the screen and then build
* up and down from there.
*
* @param position The reference view to use as the starting point
* @param top Pixel offset from the top of this view to the top of the
* reference view.
*
* @return The selected view, or null if the selected view is outside the
* visible area.
*/
private View fillSpecific(int position, int top) {
boolean tempIsSelected = position == mSelectedPosition;
View temp = makeAndAddView(position, top, true, mListPadding.left, tempIsSelected);
…
below = fillDown(position + 1, temp.getBottom() + dividerHeight);
// This will correct for the bottom of the last view not touching the bottom of the list
adjustViewsUpOrDown();
above = fillUp(position - 1, temp.getTop() - dividerHeight);
int childCount = getChildCount();
if (childCount > 0) {
correctTooLow(childCount);
}
}
顾名思义,makeAndAddView就是新建一个view,并将它添加到list里面。obtainView负责创建view,setupChild负责将view添加到layout,并设置好view的位置和相关属性。
private View makeAndAddView(int position, int y, boolean flow, int childrenLeft,
boolean selected) {
View child;
child = obtainView(position, mIsScrap);
// This needs to be positioned and measured
setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]);
return child;
}
obtainView将对应位置的view和数据关联起来,并返回view。它通过mRecycler获取到缓存的view,再通过mAdapter.getView将数据填充到view,mAdapter.getView即是我们前面分析到的ConversationListAdapter(CursorAdapter).getView。
至此,Message列表的创建过程就明了了。
3.3 功能分析
3.3.1 Message列表更新
当数据库内容发生变化(包括新短信、删除、发送状态改变等),就会触发onContentChanged,其调用栈如下:
ConversationList$2.onContentChanged(ConversationListAdapter) line: 474
ConversationListAdapter.onContentChanged() line: 118
CursorAdapter$ChangeObserver.onChange(boolean) line: 463
CursorAdapter$ChangeObserver(ContentObserver).onChange(boolean, Uri) line: 129
ContentObserver$NotificationRunnable.run() line: 180
Handler.handleCallback(Message) line: 808
Handler.dispatchMessage(Message) line: 103
Looper.loop() line: 193
从下往上分析,
1)ContentObserver
我们已经很熟悉handleCallback处理的是Post的消息,消息一般是runable,所以可以看到ContentObserver. dispatchChange有post这一消息的过程(dispatchChange的触发过程比较复杂,我们将在其他章节介绍,本例中当数据库内容变化时会触发这个流程)。
public final void dispatchChange(boolean selfChange, Uri uri) {
if (mHandler == null) {
onChange(selfChange, uri);
} else {
mHandler.post(new NotificationRunnable(selfChange, uri));
}
}
然后这个消息会运行其内部类NotificationRunnable的run方法,
public void run() {
ContentObserver.this.onChange(mSelfChange, mUri);
}
This指向的是ContentObserver的实例ChangeObserver,它是CursorAdapter的内部类,也是ContentObserver的子类,由于ConversationListAdapter通过CursorWrapper向ContentObservable注册了ContentObserver,所以当dispatchChange事件时,通过轮询,最终将事件传递给ChangeObserver。
而后,ChangeObserver调用封装类CursorAdapter的onContentChanged,将事件上传。
private class ChangeObserver extends ContentObserver {
public void onChange(boolean selfChange) {
onContentChanged();
}
}
2)onContentChanged
onContentChanged是CursorAdapter的一个方法,主要功能是当数据库内容更新后,重新使用游标查询数据。但这个方法被ConversationListAdapter重载,它给自己的客户提供一个借口类OnContentChangedListener,让客户去实现这个借口类,自己则调用接口类的onContentChanged方法执行客户定义的功能。
protected void onContentChanged() {
if (mCursor != null && !mCursor.isClosed()) {
if (mOnContentChangedListener != null) {
mOnContentChangedListener.onContentChanged(this);
}
}
}
3)ConversationList
ConversationList就是ConversationListAdapter的客户,它实现了接口类,如下,主要是调用startAsyncQuery进行数据的查询。
private final ConversationListAdapter.OnContentChangedListener mContentChangedListener =
new ConversationListAdapter.OnContentChangedListener() {
@Override
public void onContentChanged(ConversationListAdapter adapter) {
if (mIsInActivity) {
mNeedQuery = true;
startAsyncQuery();
}
}
};
并且ConversationList在其onStart向ConversationListAdapter注册了这个接口类监听器:
mListAdapter.setOnContentChangedListener(mContentChangedListener);
所以,当数据库数据改变时,根据上面相关调用流程,完成了Message列表的刷新功能。
BTW,附上游标改变后注册ContentObserver的调用栈:
ContentResolver$CursorWrapperInner(CursorWrapper).registerContentObserver(ContentObserver) line: 178
ConversationListAdapter(CursorAdapter).swapCursor(Cursor) line: 340
ConversationListAdapter(CursorAdapter).changeCursor(Cursor) line: 313
ConversationList$ThreadListQueryHandler.onQueryComplete(int, Object, Cursor) line: 1896
ConversationList$ThreadListQueryHandler(AsyncQueryHandler).handleMessage(Message) line: 344
ConversationList$ThreadListQueryHandler(Handler).dispatchMessage(Message) line: 110
Looper.loop() line: 193
3.3.2 Message列表定位
因为ConversationList是ListActivity,所以可以使用getListView获取ListView,再使用ListView的setSelectionFromTop,将列表显示开始位置指定到特定位置。
需要注意2个问题,
1)
在ConversationList会记录上次浏览列表的起始位置,所以不是随意就能设置好列表的显示位置,如onQueryComplete通过下面代码控制了什么时候可以设置这个起始位置,我们需要根据mSavedFirstVisiblePosition的值和相关逻辑去处理。
if (mSavedFirstVisiblePosition != AdapterView.INVALID_POSITION) {
// Restore the list to its previous position.
getListView().setSelectionFromTop(mSavedFirstVisiblePosition,
mSavedFirstItemOffset);
mSavedFirstVisiblePosition = AdapterView.INVALID_POSITION;
}
结合列表的创建过程,我们知道,当同步绘制列表时,是从mSyncPosition开始的,
fillSpecific(mSyncPosition, mSpecificTop);
而mSyncPosition正是setSelectionFromTop里传递进去的,所以列表起始位置只能通过setSelectionFromTop来设置。
public void setSelectionFromTop(int position, int y) {
…
mSyncPosition = position;
…
}
2)
游标的更新,当数据库数据变化后,需要更新游标,才能获取到正确的值。下面是onQueryComplete完成后,用changeCursor替换掉旧游标的过程,防止游标数据过期。
ConversationListAdapter(CursorAdapter).swapCursor(Cursor) line: 340
ConversationListAdapter(CursorAdapter).changeCursor(Cursor) line: 313
ConversationList$ThreadListQueryHandler.onQueryComplete(int, Object, Cursor) line: 1896
ConversationList$ThreadListQueryHandler(AsyncQueryHandler).handleMessage(Message) line: 344
ConversationList$ThreadListQueryHandler(Handler).dispatchMessage(Message) line: 110
Looper.loop() line: 193
在列表创建时,CursorAdapter. getView的游标最初是在CursorAdapte创建时传入的,之后可以通过swapCursor的方法更新。
6572_message_conversationList_详细分析
目 录
2. ConversationList
2.1.2.1 ConversationListAdapter
2.2.1 列表项click
2.2.2 Long Press click to Delete
2.3.1 会话ID创建
3. ConversationListItem
3.1.2.1 ConversationListItem实例创建
1. 概要
本文主要分析短信列表界面的主要控件的界面显示和处理逻辑。
2. ConversationList
2.1 ConversationList
2.1.1 Layout分析
ConversationList对应的layout是conversation_list_screen,它主要分3部分,第一部分是做全选的控制,第二部分是list列表,第三部分是列表为空的布局。具体的表现形式如下,布局文件就不再展开了。其中list列表将在另外的章节分析。
2.1.2 代码分析
2.1.2.1 ConversationListAdapter
ConversationListAdapter是ConversationListItem的数据适配器,在ConversationList.onCreate里创建,它通过bindView将会话信息绑定到item的layout上。
其创建过程如下,
private void initListAdapter() {
mListAdapter = new ConversationListAdapter(this, null);
/** M: now this code is useless and will lead to a JE, comment it.
* listener is set in onStart
*/
//mListAdapter.setOnContentChangedListener(mContentChangedListener);
setListAdapter(mListAdapter);
getListView().setRecyclerListener(mListAdapter);
}
因为ConversationList继承自ListActivity,setListAdapter将Adapter实例设置给ListActivity,这里会调用ensureList,它会加载一个布局,确保有view能用来对应Adapter。
public void setListAdapter(ListAdapter adapter) {
synchronized (this) {
ensureList();
mAdapter = adapter;
mList.setAdapter(adapter);
}
}
private void ensureList() {
if (mList != null) {
return;
}
setContentView(com.android.internal.R.layout.list_content_simple);
}
而实际上,mList已经在 onContentChanged被初始化为Layout里对应的ListView,
mList = (ListView)findViewById(com.android.internal.R.id.list);
这个ListView就是Message列表的容器了。
2.2 功能分析
2.2.1 列表项click
单击列表项的调用栈如下:
ConversationList.onListItemClick(ListView, View, int, long) line: 1086
ListActivity$2.onItemClick(AdapterView, View, int, long) line: 319
ListView(AdapterView).performItemClick(View, int, long) line: 298
ListView(AbsListView).performItemClick(View, int, long) line: 1128
AbsListView$PerformClick.run() line: 2815
AbsListView$1.run() line: 3574
Handler.handleCallback(Message) line: 800
ViewRootImpl$ViewRootHandler(Handler).dispatchMessage(Message) line: 100
2.2.2 Long Press click to Delete
长按item删除会话流程如下:
长按出现选择菜单,
mConvListOnCreateContextMenuListener
onCreateContextMenu
点击删除
ConversationList.confirmDeleteThreads(Collection, AsyncQueryHandler) line:
ConversationList.confirmDeleteThread(long, AsyncQueryHandler) line: 1320
ConversationList.onContextItemSelected(MenuItem) line: 1247
ConversationList(Activity).onMenuItemSelected(int, MenuItem) line: 2584
PhoneWindow$DialogMenuCallback.onMenuItemSelected(MenuBuilder, MenuItem) line:
开始删除之前,要做一个后台查询,看是否有lock住的消息,Conversation.startQueryHaveLockedMessages,
之后是后台查询handler.startQuery:
AsyncQueryHandler
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);
msg.arg1 = EVENT_ARG_QUERY;
WorkerArgs args = new WorkerArgs();
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);
}
这个查询过程将消息发给mWorkerThreadHandler,这是异步查询AsyncQueryHandler自己的handler,它封装完消息EVENT_ARG_QUERY后,再发送出去,
Message reply = args.handler.obtainMessage(token);
reply.obj = args;
reply.arg1 = msg.arg1;
reply.sendToTarget();
这个消息的接收对象的设置见args.handler = this; ,在本例中,这个handler是ThreadListQueryHandler。
数据库查询完成后执行onQueryComplete(HAVE_LOCKED_MESSAGES_TOKEN),
ConversationList$ThreadListQueryHandler.onQueryComplete(int, Object, Cursor) line: 1610
ConversationList$ThreadListQueryHandler(AsyncQueryHandler).handleMessage(Message) line: 344
ConversationList$ThreadListQueryHandler(Handler).dispatchMessage(Message) line: 107
Looper.loop() line: 194
之后会给出一个确认删除对话框confirmDeleteThreadDialog
在弹出的确认对话框中,选择删除,执行listener函数。
ConversationList$DeleteThreadListener.onClick(DialogInterface, int) line: 1484
AlertController$ButtonHandler.handleMessage(Message) line: 176
之后开始删除Conversation.startDelete
最后onDeleteComplete
响应函数会触发一系列其他的数据库操作,如查询list,查询unread等,
AsyncQueryHandler$WorkerHandler.handleMessage(Message) line: 120
AsyncQueryHandler$WorkerHandler(Handler).dispatchMessage(Message) line: 107
【Tips】
它的threadIds是在点击删除功能时,onContextItemSelected设置的,和长按的时候在onCreateContextMenu里选择的有时间差,不一定是同一个,在会话列表更新的时候,会有影响,所以会误删。
所以在onCreateContextMenu里要记录thread id。
2.3 数据操作
2.3.1 会话ID创建
Message列表是以会话为item进行显示的,会话是以为ID记录的,我们分析一下各种会话ID是如何创建的,
应用在CBMessageReceiverService.java收到小区广播后,使用handleCBMessageReceived方法进行处理,其中insertMessage是用来存储CB的。
insertMessage会调用CbProvider的insert方法
public Uri insert(Uri url, ContentValues initialValues) {
Uri result = null;
// TODO Check Permission
// checkPermission();
SQLiteDatabase db = mOpenHelper.getWritableDatabase();
SQLiteDatabase dbmmssms = mMmsSmsOpenHelper.getWritableDatabase();
int match = URI_MATCHER.match(url);
ContentValues values;
long rowID;
String table = null;
Log.d(TAG, " insert match = "+match);
switch (match) {
case URL_MESSAGES:
//table = CbDatabaseHelper.CBMESSAGE_TABLE;
table = MmsSmsDatabaseHelper.TABLE_CELLBROADCAST;
values = internalInsertMessages(initialValues);
rowID = dbmmssms.insert(table, null, values);
Log.d(TAG, "insert to cellbroadcast " + values);
if (rowID > 0) {
result = Uri.parse("content://messages/" + rowID);
notifyChange();
return result;
}
break;
…
}
在这里,我们重点关注一下CB Message的ThreadId是怎样获取的,在internalInsertMessages里面,显然threadId不是我们主动填充进去的,所以是通过getOrCreateThreadId获得的。
private ContentValues internalInsertMessages(ContentValues initialValues) {
ContentValues values;
if (initialValues != null) {
values = new ContentValues(initialValues);
} else {
values = new ContentValues();
}
Long threadId = values.getAsLong(EncapsulatedTelephony.SmsCb.THREAD_ID);
String address = values.getAsString(EncapsulatedTelephony.SmsCb.CHANNEL_ID);
if (((threadId == null) || (threadId == 0)) && (address != null)) {
values.put(EncapsulatedTelephony.SmsCb.THREAD_ID,
getOrCreateThreadId(address));
}
…
}
getOrCreateThreadId代码如下,它封装一个URI地址,包含了threadID和参数recipient、cellbroadcast,并使用SqliteWrapper.query进行查询,
private String getOrCreateThreadId(String address) {
//Uri.Builder uriBuilder = THREAD_ID_URI.buildUpon();
Uri.Builder uriBuilder = (Uri.parse("content://mms-sms/threadID")).buildUpon();
uriBuilder.appendQueryParameter("recipient", address);
uriBuilder.appendQueryParameter("cellbroadcast", address);
Uri uri = uriBuilder.build();
// TODO need replace with helper interface.
Cursor cursor = null;
long token = Binder.clearCallingIdentity();
try {
cursor = SqliteWrapper.query(this.getContext(), this
.getContext().getContentResolver(), uri, ID_PROJECTION, null,
null, null);
…
if (cursor.moveToFirst()) {
return String.valueOf(cursor.getLong(0));
…
}
SqliteWrapper会调用到ContentResolver,ContentResolver则最终调用到MmsSmsProvider,
在MmsSmsProvider.query里面,也是通过URI match找到操作码URI_THREAD_ID,根据参数cellbroadcast,使用getCBThreadId给CB查找或创建其ThreadId。
Threads表内容如下所示,里面的关键字段有_id ,recipient_ids,要想找到合适的ThreadId,只有通过recipient_ids来查询,
所以在getCBThreadId方法里面,通过getAddressIds从canonical_addresses表里面找到和号码匹配的recipient_ids列表,再到Threads表里面找到recipient_ids相同和type是小区广播的_id,即是我们需要的ThreadId,使用的查询语句如下。
String queryString = "SELECT _id FROM threads " + "WHERE type=" + EncapsulatedTelephony.Threads.CELL_BROADCAST_THREAD + " AND recipient_ids=?";
如果没有满足条件的ThreadId,调用insertThread()创建一个新的thread。
在Message列表界面通过按钮新建短信时,执行createNewMessage(),通过createIntent传递一个threadId=0的ID过去,
private void createNewMessage() {
startActivity(ComposeMessageActivity.createIntent(this, 0));
}
public static Intent createIntent(Context context, long threadId) {
Intent intent = new Intent(context, ComposeMessageActivity.class);
if (threadId > 0) {
intent.setData(Conversation.getUri(threadId));
}
return intent;
}
因为threadId为0,在会话创建中会获取或创建一个threadId,具体过程和下节相同。
在联系人的详情界面,点击发送短信,因为有号码存在,所以在ComposeMessageActivity.Oncreate方法里面会去查询或创建一个threadId。
onCreate的initActivityState里,有这么一段代码是获取会话实例的,mConversation.get通过号码获取到会话实例,
Uri intentData = intent.getData();
/// M: Code analyze 034, If intent is SEND,just create a new empty thread,
/// otherwise Conversation.get() will throw exception.
String action = intent.getAction();
if (intentData != null && (TextUtils.isEmpty(action) ||
!action.equals(Intent.ACTION_SEND))) {
/// M: group-contact send message
// try to get a conversation based on the data URI passed to our intent.
if (intentData.getPathSegments().size() < 2) {
mConversation = mConversation.get(getApplicationContext(),ContactList.getByNumbers(
getStringForMultipleRecipients(Conversation.getRecipients(intentData)),
false /* don't block */, true /* replace number */),false);
} else {
mConversation = Conversation.get(getApplicationContext(), intentData, false);
}
/// @}
mWorkingMessage.setText(getBody(intentData));
Conversation get代码如下,如果号码为空则新建会话,如果缓冲里存储了对应号码的会话,则使用缓冲里的数据,如果条件都不满足,则需要去数据库查询到ID,并新建会话,之后将新会话存放到缓冲。
public static Conversation get(Context context, ContactList recipients, boolean allowQuery) {
if (recipients.size() < 1) {
return createNew(context);
}
Conversation conv = Cache.get(recipients);
if (conv != null) {
return conv;
}
long threadId = getOrCreateThreadId(context, recipients);
conv = new Conversation(context, threadId, allowQuery);
。。。
Cache.put(conv);
}
getOrCreateThreadId用来获取ThreadId,由Threads.getOrCreateThreadId实现,这个方法在Telephony.java里面。
getOrCreateThreadId类似前面提到的小区广播的实现过程,先封装一个"content://mms-sms/threadID"的URI,但只使用一个参数recipient,
在MmsSmsProvider.query里面,也是通过URI match找到操作码URI_THREAD_ID,,使用getThreadId查找或创建其ThreadId,getAddressIds获取到匹配的recipientIds,再取Threads表里查询,如果查询到,返回游标,如果没有记录,则用insertThread向Threads里插入记录,再查询到ThreadId。
private synchronized Cursor getThreadId(List<String> recipients) {
Set<Long> addressIds = getAddressIds(recipients);
。。。
recipientIds = Long.toString(addressId);
。。。
String[] selectionArgs = new String[] { recipientIds };
SQLiteDatabase db = mOpenHelper.getReadableDatabase();
db.beginTransaction();
Cursor cursor = null;
try {
// Find the thread with the given recipients
cursor = db.rawQuery(THREAD_QUERY, selectionArgs);
if (cursor.getCount() == 0) {
// No thread with those recipients exists, so create the thread.
cursor.close();
Log.d(LOG_TAG, "getThreadId: create new thread_id for recipients " + recipients);
insertThread(recipientIds, recipients);
// The thread was just created, now find it and return it.
cursor = db.rawQuery(THREAD_QUERY, selectionArgs);
。。。
}
3. ConversationListItem
3.1 ConversationListItem
3.1.1 Layout分析
如前所述,短信记录的每条信息显示是使用conversation_list_item.xml布局的,在本例中,其直观显示是如下形式的一条短信记录,
conversation_list_item.xml文件如下,它是一个ConversationListItem类型的布局,
(需要注意不同屏幕使用这个文件的位置,本例中在packages\apps\Mms\res\layout-mdpi下面,不是packages\apps\Mms\res\layout)
conversation_list_item.xml
<?xml version="1.0" encoding="utf-8"?>
<com.android.mms.ui.ConversationListItem xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="?android:attr/listPreferredItemHeight"
android:background="?android:attr/activatedBackgroundIndicator" >
<CheckBox
android:id="@+id/select_box"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_centerVertical="true"
android:clickable="false"
android:focusable="false"
android:focusableInTouchMode="false" />
<com.android.mms.ui.MmsQuickContactBadge
android:id="@+id/avatar"
android:visibility="gone"
android:layout_centerVertical="true"
android:layout_toEndOf="@id/select_box"
style="?android:attr/quickContactBadgeStyleWindowLarge" />
<ImageView
android:id="@+id/presence"
android:visibility="invisible"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_alignParentTop="true"
android:src="@drawable/ic_unread_label"/>
<LinearLayout
android:id="@+id/iconlist"
android:layout_alignParentTop="true"
android:layout_alignParentEnd="true"
android:layout_marginTop="7dip"
android:layout_marginEnd="10dip"
android:orientation="horizontal"
android:layout_height="wrap_content"
android:layout_width="wrap_content" >
<ImageView android:id="@+id/draft"
android:visibility="gone"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:src="@drawable/ic_draft" />
<ImageView android:id="@+id/error"
android:paddingStart="6dp"
android:visibility="gone"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:src="@drawable/ic_list_alert_sms_failed" />
<ImageView android:id="@+id/attachment"
android:paddingStart="6dp"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:visibility="gone"
android:src="@drawable/ic_attachment_universal_small" />
<ImageView android:id="@+id/mute"
android:paddingStart="6dp"
android:visibility="gone"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:src="@drawable/ipmsg_silent" />
</LinearLayout>
<RelativeLayout
android:layout_alignParentTop="true"
android:layout_marginTop="4dip"
android:layout_marginStart="12dip"
android:layout_toEndOf="@id/avatar"
android:layout_toStartOf="@id/iconlist"
android:gravity="center_vertical"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView android:id="@+id/unread"
android:background="@drawable/ipmsg_message_box"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:textSize="12sp"
android:textColor="@color/text_color_unread"
android:singleLine="true"
android:layout_alignParentEnd="true" />
<RelativeLayout android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:layout_toStartOf="@id/unread">
<TextView android:id="@+id/draft_and_smgcount"
android:textAppearance="?android:attr/textAppearanceMedium"
android:layout_alignParentEnd="true"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="6dip"
android:singleLine="true" />
<com.android.mms.util.AlwaysMarqueeTextView android:id="@+id/from"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceMedium"
android:singleLine="true"
android:layout_alignParentStart="true"
android:layout_toStartOf="@id/draft_and_smgcount"
android:layout_marginEnd="6dip"
android:ellipsize="marquee" />
</RelativeLayout>
</RelativeLayout>
<TextView android:id="@+id/date"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:textSize= "12sp"
android:textColor="@color/dlg_text_counter_color"
android:layout_alignParentBottom="true"
android:layout_alignParentEnd="true"
android:layout_marginEnd="10dip"
android:layout_marginBottom="4dip"
android:gravity="bottom" />
<TextView android:id="@+id/subject"
android:layout_height="wrap_content"
android:layout_width="fill_parent"
android:textSize= "12sp"
android:layout_alignParentBottom="true"
android:layout_toEndOf="@id/avatar"
android:layout_toStartOf="@id/date"
android:layout_marginStart="12dip"
android:layout_marginBottom="4dip"
android:gravity="bottom"
android:textColor="@color/dlg_text_counter_color"
android:lines="2"
android:ellipsize="end" />
</com.android.mms.ui.ConversationListItem>
其对应的布局图形如下,图中左边是layout组成,右上是layout布局表现,右下是对layout里面的子view编号,便于理解。
分析xml文件,并对照图例可以看出,
ConversationListItem是整个item布局类,表现形式见下图左边,它指定了高度
android:layout_height="?android:attr/listPreferredItemHeight"
这个布局由6个同级view组成,见下图中间及右上,其中
checkbox供选择使用,见右下数字0,alignParentStart表示它在开头位置,
android:layout_alignParentStart="true"
MmsQuickContactBadge做头像显示,见右下数字1,
ImageView做未读标识等用途,见右下数字2,通过下面的属性将它放置在最右上的位置
android:layout_alignParentEnd="true"
android:layout_alignParentTop="true"
LinearLayout是个组合视图,见右下数字3和iconlist小图,它有四个子ImageView表示短信的信息状态,具体名字见iconlist小图。注意四个子ImageView表示同时最多只能显示四个图标,不是表示短信只有这四种状态。通过下面属性,将它放置在MmsQuickContactBadge后面。
android:orientation="horizontal"
<iconlist>
RelativeLayout是个组合视图,见右下数字4和Text Title小图,包含3个TextView,用来显示Title和消息数目、未读消息数目。在RelativeLayout,通过下面的属性设置,将RelativeLayout(index4的view)和上面的LinearLayout(index3的view)并行放置在同一行。
android:layout_toEndOf="@id/avatar"
android:layout_toStartOf="@id/iconlist"
TextView显示日期,见右下数字5,用下面2个属性表示它的位置在右下,
android:layout_alignParentBottom="true"
android:layout_alignParentEnd="true"
TextView 显示短线截取的部分内容,见右下数字6,用下面3个属性表明了它的位置。
android:layout_alignParentBottom="true"
android:layout_toEndOf="@id/avatar"
android:layout_toStartOf="@id/date"
<Text Content>
3.1.2 代码分析
3.1.2.1 ConversationListItem实例创建
ConversationListItem实例创建栈如下,因为有缓存使用,不一定每次进入列表都会创建这个类实例,所以不一定能调试到,要在此设置断点调试,有一个小技巧,就是将mms进程终止掉,让其自动重启(可通过发送短信或cb的方法加快重启过程),这样就可以进行调试。
ConversationListItem.<init>(Context, AttributeSet) line: 130
Constructor.constructNative(Object[], Class, Class[], int, boolean) line: not available [native method]
Constructor.newInstance(Object...) line: 423
PhoneLayoutInflater(LayoutInflater).createView(String, String, AttributeSet) line: 594
PhoneLayoutInflater(LayoutInflater).createViewFromTag(View, String, AttributeSet) line: 696
PhoneLayoutInflater(LayoutInflater).inflate(XmlPullParser, ViewGroup, boolean) line: 469
PhoneLayoutInflater(LayoutInflater).inflate(int, ViewGroup, boolean) line: 397
ConversationListAdapter.newView(Context, Cursor, ViewGroup) line: 103
ConversationListAdapter(CursorAdapter).getView(int, View, ViewGroup) line: 250
ListView(AbsListView).obtainView(int, boolean[]) line: 2338
ListView.makeAndAddView(int, int, boolean, int, boolean) line: 1812
ListView.fillDown(int, int) line: 698
ListView.fillFromTop(int) line: 759
ListView.layoutChildren() line: 1631
ListView(AbsListView).onLayout(boolean, int, int, int, int) line: 2149
ConversationListItem实例创建的主要过程分析如下,在getView来获取view的时候,因为缓冲没有数据,会使用newView新建一个view,
public View getView(int position, View convertView, ViewGroup parent) {
View v;
if (convertView == null) {
v = newView(mContext, mCursor, parent);
} else {
v = convertView;
}
bindView(v, mContext, mCursor);
return v;
}
newView实际的过程是inflate一个布局文件,这个布局文件就是我们分析的layout,它是ConversationListItem形式的,我们知道,在inflate创建布局文件时,会根据节点创建view实例,所以会使用类的构造类Constructor来创建ConversationListItem实例。
public View newView(Context context, Cursor cursor, ViewGroup parent) {
if (LOCAL_LOGV) Log.v(TAG, "inflating new view");
return mFactory.inflate(R.layout.conversation_list_item, parent, false);
}
Checkbox的主要作用是用来勾选短信列表的某一个item,并进行删除操作的。
对于checkbox,在ConversationListItem里面定义了一个引用mSelectBox,
private CheckBox mSelectBox;
并在onFinishInflate里面初始化它,关联到Layout里面的select_box,
mSelectBox = (CheckBox) findViewById(R.id.select_box);
当列表项被点击时,执行ConversationList.onListItemClick(ListView, View, int, long)这个方法,在里面添加下面代码,当处于删除模式时,如下,将Conversation设置为check状态,然后更新界面,
if (mListAdapter != null && mDeleteMode) {
if (conv.isChecked()) {
conv.setIsChecked(false);
} else {
conv.setIsChecked(true);
}
mListAdapter.notifyDataSetChanged();
updateSelectView();
return;
}
更新界面时,执行ConversationListItem.bind(Context, Conversation) ,每加载一个item项都会执行一次bind,在这里,会根据mSelectMode 决定checkbox是否显示,根据Conversation的状态决定是否选中。
所以实际上,不是我们主观上看到的那样由checkbox决定了当前项目是否选中,而是当前项选中后决定checkbox的显示状态。
if (mSelectMode) {
mSelectBox.setVisibility(View.VISIBLE);
mAvatarView.setClickable(false);
} else {
mSelectBox.setVisibility(View.GONE);
mAvatarView.setClickable(true);
}
if (mConversation.isChecked()) {
mSelectBox.setChecked(true);
} else {
mSelectBox.setChecked(false);
}
上面就是checkbox在代码里的处理过程。
R.id.avatar用来显示发件人头像,定义如下,MmsQuickContactBadge继承自QuickContactBadge,再往上是ImageView,一个通用的视图组件。
private MmsQuickContactBadge mAvatarView;
其实例获取在ConversationListItem的onFinishInflate里面,
mAvatarView = (MmsQuickContactBadge) findViewById(R.id.avatar);
如果希望AvatarView能够被点击,相应事件,则可以看到在bind有如下代码,表示在删除模式下不能被点击,在列表模式下可以被点击。
if (mSelectMode) {
mAvatarView.setClickable(false);
} else {
mAvatarView.setClickable(true);
}
AvatarView被点击之后,执行的调用栈如下,可见是由View直接调用了MmsQuickContactBadge类的onClick,并不是ConversationList或ConversationListItem里做了onClick的相关处理。
MmsQuickContactBadge(QuickContactBadge).onClick(View) line: 217
MmsQuickContactBadge.onClick(View) line: 75
MmsQuickContactBadge(View).performClick() line: 4212
View$PerformClick.run() line: 17476
Handler.handleCallback(Message) line: 800
ViewRootImpl$ViewRootHandler(Handler).dispatchMessage(Message) line: 100
QuickContactBadge类的onClick代码如下,
@Override
public void onClick(View v) {
if (mContactUri != null) {
QuickContact.showQuickContact(getContext(), QuickContactBadge.this, mContactUri,
QuickContact.MODE_LARGE, mExcludeMimes);
} else if (mContactEmail != null) {
mQueryHandler.startQuery(TOKEN_EMAIL_LOOKUP_AND_TRIGGER, mContactEmail,
Uri.withAppendedPath(Email.CONTENT_LOOKUP_URI, Uri.encode(mContactEmail)),
EMAIL_LOOKUP_PROJECTION, null, null, null);
} else if (mContactPhone != null) {
mQueryHandler.startQuery(TOKEN_PHONE_LOOKUP_AND_TRIGGER, mContactPhone,
Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI, mContactPhone),
PHONE_LOOKUP_PROJECTION, null, null, null);
} else {
// If a contact hasn't been assigned, don't react to click.
return;
}
}
我们当前的代码走mContactPhone路径,mContactPhone是在assignContactFromPhone里面赋值的,assignContactFromPhone则是通过ConversationListItem的updateAvatarView调用的(传入的number就赋值给mContactPhone),
mAvatarView.assignContactFromPhone(number, true);
updateAvatarView可以被updateFromView和bind调用,这两个方法我们在前面有过描述。所以从整个过程来看,我们可以看出avatar被点击和其需要的数据来源。
3.2 绘制分析
3.2.1 Message列表创建
如ConversationList的layout描述,Messag列表主要是由list呈现,而list的item项是由ConversationListItem布局的,我们这里讨论每个item项的创建以及整个list的创建。
在使用Cache时,ConversationListItem的创建过程如下,
ConversationListItem.bind(Context, Conversation) line: 480
ConversationListAdapter.bindView(View, Context, Cursor) line: 77
ConversationListAdapter(CursorAdapter).getView(int, View, ViewGroup) line: 250
ListView(AbsListView).obtainView(int, boolean[]) line: 2186
ListView.makeAndAddView(int, int, boolean, int, boolean) line: 1877
ListView.fillSpecific(int, int) line: 1356
ListView.layoutChildren() line: 1671
ListView(AbsListView).onLayout(boolean, int, int, int, int) line: 2037
ListView(View).layout(int, int, int, int) line: 14118
ListView(ViewGroup).layout(int, int, int, int) line: 4467
RelativeLayout.onLayout(boolean, int, int, int, int) line: 1021
从上往下分析该栈:
1)ConversationListItem.bind
ConversationListItem.bind的代码比较多,仔细分析,实际上逻辑比较简单,主要是设置每个view的内容和状态,比如说设置日期的代码如下,
mDateView.setVisibility(VISIBLE);
if(ConversationList.sConversationListOption ==
ConversationList.OPTION_CONVERSATION_LIST_IMPORTANT &&
!conversation.hasUnreadMessages()) {
mDateView.setText(MessageUtils.formatTimeStampStringExtend(context,
conversation.getImpDate()));
}
其数据来源于conversation,我们要进一步分析conversation是怎么传递进来的,以及为什么要调用bind。
2)ConversationListAdapter
ConversationListAdapter 是ConversationList的layout里的list对应的Adapter,它继承自MessageCursorAdapter-CursorAdapter-baseAdapter, 其方法bindView在本例中执行的主要代码如下,其中headerview是一个ConversationListItem引用,是调用方法传递过来的,conversation是根据游标新建的,最后调用bind,所以该方法的作用就是获取到conversation,并用准备将其中的数据赋值到view。
public void bindView(View view, Context context, Cursor cursor) {
ConversationListItem headerView = (ConversationListItem) view;
Conversation conv;
if (!mIsScrolling) {
Conversation.setNeedCacheConv(false);
conv = Conversation.from(context, cursor);
Conversation.setNeedCacheConv(true);
if (mSubjectSingleLine) {
headerView.setSubjectSingleLineMode(true);
}
if (conv != null) {
conv.setIsChecked(sSelectedTheadsId.contains(conv.getThreadId()));
}
headerView.bind(context, conv);
}
}
(CursorAdapter).getView则负责获取到view实例和游标实例。
view的获取有两种方式,一种是调用者传递过来以前缓存的,一种是新创建一个,在ConversationListItem实例创建中有讲到如何创建。
游标实例则在Adapter创建时创建。
3)ListView
在ListView部分,我们从layoutChildren向上分析,向下部分设计到太多ListView的实现机制问题,暂不讨论。
layoutChildren实现ListView子窗口布局,实际上就是list在layout所占部分。
本例从launcher进入Message列表,执行代码路径如下,mSyncPosition是当前item索引值,mSpecificTop是当前layout相对Top的位置。
layoutChildren:
case LAYOUT_SYNC:
sel = fillSpecific(mSyncPosition, mSpecificTop);
fillSpecific在知道位置绘制指定项的item数据,然后上下绘制其他的item。它先通过makeAndAddView完成当前item的创建并添加到layout,fillDown、fillUp则根据layout大小,上下绘制item完成整个list的绘制,使用的方法也是makeAndAddView,adjustViewsUpOrDown调整item位置。
/**
* Put a specific item at a specific location on the screen and then build
* up and down from there.
*
* @param position The reference view to use as the starting point
* @param top Pixel offset from the top of this view to the top of the
* reference view.
*
* @return The selected view, or null if the selected view is outside the
* visible area.
*/
private View fillSpecific(int position, int top) {
boolean tempIsSelected = position == mSelectedPosition;
View temp = makeAndAddView(position, top, true, mListPadding.left, tempIsSelected);
…
below = fillDown(position + 1, temp.getBottom() + dividerHeight);
// This will correct for the bottom of the last view not touching the bottom of the list
adjustViewsUpOrDown();
above = fillUp(position - 1, temp.getTop() - dividerHeight);
int childCount = getChildCount();
if (childCount > 0) {
correctTooLow(childCount);
}
}
顾名思义,makeAndAddView就是新建一个view,并将它添加到list里面。obtainView负责创建view,setupChild负责将view添加到layout,并设置好view的位置和相关属性。
private View makeAndAddView(int position, int y, boolean flow, int childrenLeft,
boolean selected) {
View child;
child = obtainView(position, mIsScrap);
// This needs to be positioned and measured
setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]);
return child;
}
obtainView将对应位置的view和数据关联起来,并返回view。它通过mRecycler获取到缓存的view,再通过mAdapter.getView将数据填充到view,mAdapter.getView即是我们前面分析到的ConversationListAdapter(CursorAdapter).getView。
至此,Message列表的创建过程就明了了。
3.3 功能分析
3.3.1 Message列表更新
当数据库内容发生变化(包括新短信、删除、发送状态改变等),就会触发onContentChanged,其调用栈如下:
ConversationList$2.onContentChanged(ConversationListAdapter) line: 474
ConversationListAdapter.onContentChanged() line: 118
CursorAdapter$ChangeObserver.onChange(boolean) line: 463
CursorAdapter$ChangeObserver(ContentObserver).onChange(boolean, Uri) line: 129
ContentObserver$NotificationRunnable.run() line: 180
Handler.handleCallback(Message) line: 808
Handler.dispatchMessage(Message) line: 103
Looper.loop() line: 193
从下往上分析,
1)ContentObserver
我们已经很熟悉handleCallback处理的是Post的消息,消息一般是runable,所以可以看到ContentObserver. dispatchChange有post这一消息的过程(dispatchChange的触发过程比较复杂,我们将在其他章节介绍,本例中当数据库内容变化时会触发这个流程)。
public final void dispatchChange(boolean selfChange, Uri uri) {
if (mHandler == null) {
onChange(selfChange, uri);
} else {
mHandler.post(new NotificationRunnable(selfChange, uri));
}
}
然后这个消息会运行其内部类NotificationRunnable的run方法,
public void run() {
ContentObserver.this.onChange(mSelfChange, mUri);
}
This指向的是ContentObserver的实例ChangeObserver,它是CursorAdapter的内部类,也是ContentObserver的子类,由于ConversationListAdapter通过CursorWrapper向ContentObservable注册了ContentObserver,所以当dispatchChange事件时,通过轮询,最终将事件传递给ChangeObserver。
而后,ChangeObserver调用封装类CursorAdapter的onContentChanged,将事件上传。
private class ChangeObserver extends ContentObserver {
public void onChange(boolean selfChange) {
onContentChanged();
}
}
2)onContentChanged
onContentChanged是CursorAdapter的一个方法,主要功能是当数据库内容更新后,重新使用游标查询数据。但这个方法被ConversationListAdapter重载,它给自己的客户提供一个借口类OnContentChangedListener,让客户去实现这个借口类,自己则调用接口类的onContentChanged方法执行客户定义的功能。
protected void onContentChanged() {
if (mCursor != null && !mCursor.isClosed()) {
if (mOnContentChangedListener != null) {
mOnContentChangedListener.onContentChanged(this);
}
}
}
3)ConversationList
ConversationList就是ConversationListAdapter的客户,它实现了接口类,如下,主要是调用startAsyncQuery进行数据的查询。
private final ConversationListAdapter.OnContentChangedListener mContentChangedListener =
new ConversationListAdapter.OnContentChangedListener() {
@Override
public void onContentChanged(ConversationListAdapter adapter) {
if (mIsInActivity) {
mNeedQuery = true;
startAsyncQuery();
}
}
};
并且ConversationList在其onStart向ConversationListAdapter注册了这个接口类监听器:
mListAdapter.setOnContentChangedListener(mContentChangedListener);
所以,当数据库数据改变时,根据上面相关调用流程,完成了Message列表的刷新功能。
BTW,附上游标改变后注册ContentObserver的调用栈:
ContentResolver$CursorWrapperInner(CursorWrapper).registerContentObserver(ContentObserver) line: 178
ConversationListAdapter(CursorAdapter).swapCursor(Cursor) line: 340
ConversationListAdapter(CursorAdapter).changeCursor(Cursor) line: 313
ConversationList$ThreadListQueryHandler.onQueryComplete(int, Object, Cursor) line: 1896
ConversationList$ThreadListQueryHandler(AsyncQueryHandler).handleMessage(Message) line: 344
ConversationList$ThreadListQueryHandler(Handler).dispatchMessage(Message) line: 110
Looper.loop() line: 193
3.3.2 Message列表定位
因为ConversationList是ListActivity,所以可以使用getListView获取ListView,再使用ListView的setSelectionFromTop,将列表显示开始位置指定到特定位置。
需要注意2个问题,
1)
在ConversationList会记录上次浏览列表的起始位置,所以不是随意就能设置好列表的显示位置,如onQueryComplete通过下面代码控制了什么时候可以设置这个起始位置,我们需要根据mSavedFirstVisiblePosition的值和相关逻辑去处理。
if (mSavedFirstVisiblePosition != AdapterView.INVALID_POSITION) {
// Restore the list to its previous position.
getListView().setSelectionFromTop(mSavedFirstVisiblePosition,
mSavedFirstItemOffset);
mSavedFirstVisiblePosition = AdapterView.INVALID_POSITION;
}
结合列表的创建过程,我们知道,当同步绘制列表时,是从mSyncPosition开始的,
fillSpecific(mSyncPosition, mSpecificTop);
而mSyncPosition正是setSelectionFromTop里传递进去的,所以列表起始位置只能通过setSelectionFromTop来设置。
public void setSelectionFromTop(int position, int y) {
…
mSyncPosition = position;
…
}
2)
游标的更新,当数据库数据变化后,需要更新游标,才能获取到正确的值。下面是onQueryComplete完成后,用changeCursor替换掉旧游标的过程,防止游标数据过期。
ConversationListAdapter(CursorAdapter).swapCursor(Cursor) line: 340
ConversationListAdapter(CursorAdapter).changeCursor(Cursor) line: 313
ConversationList$ThreadListQueryHandler.onQueryComplete(int, Object, Cursor) line: 1896
ConversationList$ThreadListQueryHandler(AsyncQueryHandler).handleMessage(Message) line: 344
ConversationList$ThreadListQueryHandler(Handler).dispatchMessage(Message) line: 110
Looper.loop() line: 193
在列表创建时,CursorAdapter. getView的游标最初是在CursorAdapte创建时传入的,之后可以通过swapCursor的方法更新。
目 录
2. ConversationList
2.1.2.1 ConversationListAdapter
2.2.1 列表项click
2.2.2 Long Press click to Delete
2.3.1 会话ID创建
3. ConversationListItem
3.1.2.1 ConversationListItem实例创建
1. 概要
本文主要分析短信列表界面的主要控件的界面显示和处理逻辑。
2. ConversationList
2.1 ConversationList
2.1.1 Layout分析
ConversationList对应的layout是conversation_list_screen,它主要分3部分,第一部分是做全选的控制,第二部分是list列表,第三部分是列表为空的布局。具体的表现形式如下,布局文件就不再展开了。其中list列表将在另外的章节分析。
2.1.2 代码分析
2.1.2.1 ConversationListAdapter
ConversationListAdapter是ConversationListItem的数据适配器,在ConversationList.onCreate里创建,它通过bindView将会话信息绑定到item的layout上。
其创建过程如下,
private void initListAdapter() {
mListAdapter = new ConversationListAdapter(this, null);
/** M: now this code is useless and will lead to a JE, comment it.
* listener is set in onStart
*/
//mListAdapter.setOnContentChangedListener(mContentChangedListener);
setListAdapter(mListAdapter);
getListView().setRecyclerListener(mListAdapter);
}
因为ConversationList继承自ListActivity,setListAdapter将Adapter实例设置给ListActivity,这里会调用ensureList,它会加载一个布局,确保有view能用来对应Adapter。
public void setListAdapter(ListAdapter adapter) {
synchronized (this) {
ensureList();
mAdapter = adapter;
mList.setAdapter(adapter);
}
}
private void ensureList() {
if (mList != null) {
return;
}
setContentView(com.android.internal.R.layout.list_content_simple);
}
而实际上,mList已经在 onContentChanged被初始化为Layout里对应的ListView,
mList = (ListView)findViewById(com.android.internal.R.id.list);
这个ListView就是Message列表的容器了。
2.2 功能分析
2.2.1 列表项click
单击列表项的调用栈如下:
ConversationList.onListItemClick(ListView, View, int, long) line: 1086
ListActivity$2.onItemClick(AdapterView, View, int, long) line: 319
ListView(AdapterView).performItemClick(View, int, long) line: 298
ListView(AbsListView).performItemClick(View, int, long) line: 1128
AbsListView$PerformClick.run() line: 2815
AbsListView$1.run() line: 3574
Handler.handleCallback(Message) line: 800
ViewRootImpl$ViewRootHandler(Handler).dispatchMessage(Message) line: 100
2.2.2 Long Press click to Delete
长按item删除会话流程如下:
长按出现选择菜单,
mConvListOnCreateContextMenuListener
onCreateContextMenu
点击删除
ConversationList.confirmDeleteThreads(Collection, AsyncQueryHandler) line:
ConversationList.confirmDeleteThread(long, AsyncQueryHandler) line: 1320
ConversationList.onContextItemSelected(MenuItem) line: 1247
ConversationList(Activity).onMenuItemSelected(int, MenuItem) line: 2584
PhoneWindow$DialogMenuCallback.onMenuItemSelected(MenuBuilder, MenuItem) line:
开始删除之前,要做一个后台查询,看是否有lock住的消息,Conversation.startQueryHaveLockedMessages,
之后是后台查询handler.startQuery:
AsyncQueryHandler
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);
msg.arg1 = EVENT_ARG_QUERY;
WorkerArgs args = new WorkerArgs();
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);
}
这个查询过程将消息发给mWorkerThreadHandler,这是异步查询AsyncQueryHandler自己的handler,它封装完消息EVENT_ARG_QUERY后,再发送出去,
Message reply = args.handler.obtainMessage(token);
reply.obj = args;
reply.arg1 = msg.arg1;
reply.sendToTarget();
这个消息的接收对象的设置见args.handler = this; ,在本例中,这个handler是ThreadListQueryHandler。
数据库查询完成后执行onQueryComplete(HAVE_LOCKED_MESSAGES_TOKEN),
ConversationList$ThreadListQueryHandler.onQueryComplete(int, Object, Cursor) line: 1610
ConversationList$ThreadListQueryHandler(AsyncQueryHandler).handleMessage(Message) line: 344
ConversationList$ThreadListQueryHandler(Handler).dispatchMessage(Message) line: 107
Looper.loop() line: 194
之后会给出一个确认删除对话框confirmDeleteThreadDialog
在弹出的确认对话框中,选择删除,执行listener函数。
ConversationList$DeleteThreadListener.onClick(DialogInterface, int) line: 1484
AlertController$ButtonHandler.handleMessage(Message) line: 176
之后开始删除Conversation.startDelete
最后onDeleteComplete
响应函数会触发一系列其他的数据库操作,如查询list,查询unread等,
AsyncQueryHandler$WorkerHandler.handleMessage(Message) line: 120
AsyncQueryHandler$WorkerHandler(Handler).dispatchMessage(Message) line: 107
【Tips】
它的threadIds是在点击删除功能时,onContextItemSelected设置的,和长按的时候在onCreateContextMenu里选择的有时间差,不一定是同一个,在会话列表更新的时候,会有影响,所以会误删。
所以在onCreateContextMenu里要记录thread id。
2.3 数据操作
2.3.1 会话ID创建
Message列表是以会话为item进行显示的,会话是以为ID记录的,我们分析一下各种会话ID是如何创建的,
应用在CBMessageReceiverService.java收到小区广播后,使用handleCBMessageReceived方法进行处理,其中insertMessage是用来存储CB的。
insertMessage会调用CbProvider的insert方法
public Uri insert(Uri url, ContentValues initialValues) {
Uri result = null;
// TODO Check Permission
// checkPermission();
SQLiteDatabase db = mOpenHelper.getWritableDatabase();
SQLiteDatabase dbmmssms = mMmsSmsOpenHelper.getWritableDatabase();
int match = URI_MATCHER.match(url);
ContentValues values;
long rowID;
String table = null;
Log.d(TAG, " insert match = "+match);
switch (match) {
case URL_MESSAGES:
//table = CbDatabaseHelper.CBMESSAGE_TABLE;
table = MmsSmsDatabaseHelper.TABLE_CELLBROADCAST;
values = internalInsertMessages(initialValues);
rowID = dbmmssms.insert(table, null, values);
Log.d(TAG, "insert to cellbroadcast " + values);
if (rowID > 0) {
result = Uri.parse("content://messages/" + rowID);
notifyChange();
return result;
}
break;
…
}
在这里,我们重点关注一下CB Message的ThreadId是怎样获取的,在internalInsertMessages里面,显然threadId不是我们主动填充进去的,所以是通过getOrCreateThreadId获得的。
private ContentValues internalInsertMessages(ContentValues initialValues) {
ContentValues values;
if (initialValues != null) {
values = new ContentValues(initialValues);
} else {
values = new ContentValues();
}
Long threadId = values.getAsLong(EncapsulatedTelephony.SmsCb.THREAD_ID);
String address = values.getAsString(EncapsulatedTelephony.SmsCb.CHANNEL_ID);
if (((threadId == null) || (threadId == 0)) && (address != null)) {
values.put(EncapsulatedTelephony.SmsCb.THREAD_ID,
getOrCreateThreadId(address));
}
…
}
getOrCreateThreadId代码如下,它封装一个URI地址,包含了threadID和参数recipient、cellbroadcast,并使用SqliteWrapper.query进行查询,
private String getOrCreateThreadId(String address) {
//Uri.Builder uriBuilder = THREAD_ID_URI.buildUpon();
Uri.Builder uriBuilder = (Uri.parse("content://mms-sms/threadID")).buildUpon();
uriBuilder.appendQueryParameter("recipient", address);
uriBuilder.appendQueryParameter("cellbroadcast", address);
Uri uri = uriBuilder.build();
// TODO need replace with helper interface.
Cursor cursor = null;
long token = Binder.clearCallingIdentity();
try {
cursor = SqliteWrapper.query(this.getContext(), this
.getContext().getContentResolver(), uri, ID_PROJECTION, null,
null, null);
…
if (cursor.moveToFirst()) {
return String.valueOf(cursor.getLong(0));
…
}
SqliteWrapper会调用到ContentResolver,ContentResolver则最终调用到MmsSmsProvider,
在MmsSmsProvider.query里面,也是通过URI match找到操作码URI_THREAD_ID,根据参数cellbroadcast,使用getCBThreadId给CB查找或创建其ThreadId。
Threads表内容如下所示,里面的关键字段有_id ,recipient_ids,要想找到合适的ThreadId,只有通过recipient_ids来查询,
所以在getCBThreadId方法里面,通过getAddressIds从canonical_addresses表里面找到和号码匹配的recipient_ids列表,再到Threads表里面找到recipient_ids相同和type是小区广播的_id,即是我们需要的ThreadId,使用的查询语句如下。
String queryString = "SELECT _id FROM threads " + "WHERE type=" + EncapsulatedTelephony.Threads.CELL_BROADCAST_THREAD + " AND recipient_ids=?";
如果没有满足条件的ThreadId,调用insertThread()创建一个新的thread。
在Message列表界面通过按钮新建短信时,执行createNewMessage(),通过createIntent传递一个threadId=0的ID过去,
private void createNewMessage() {
startActivity(ComposeMessageActivity.createIntent(this, 0));
}
public static Intent createIntent(Context context, long threadId) {
Intent intent = new Intent(context, ComposeMessageActivity.class);
if (threadId > 0) {
intent.setData(Conversation.getUri(threadId));
}
return intent;
}
因为threadId为0,在会话创建中会获取或创建一个threadId,具体过程和下节相同。
在联系人的详情界面,点击发送短信,因为有号码存在,所以在ComposeMessageActivity.Oncreate方法里面会去查询或创建一个threadId。
onCreate的initActivityState里,有这么一段代码是获取会话实例的,mConversation.get通过号码获取到会话实例,
Uri intentData = intent.getData();
/// M: Code analyze 034, If intent is SEND,just create a new empty thread,
/// otherwise Conversation.get() will throw exception.
String action = intent.getAction();
if (intentData != null && (TextUtils.isEmpty(action) ||
!action.equals(Intent.ACTION_SEND))) {
/// M: group-contact send message
// try to get a conversation based on the data URI passed to our intent.
if (intentData.getPathSegments().size() < 2) {
mConversation = mConversation.get(getApplicationContext(),ContactList.getByNumbers(
getStringForMultipleRecipients(Conversation.getRecipients(intentData)),
false /* don't block */, true /* replace number */),false);
} else {
mConversation = Conversation.get(getApplicationContext(), intentData, false);
}
/// @}
mWorkingMessage.setText(getBody(intentData));
Conversation get代码如下,如果号码为空则新建会话,如果缓冲里存储了对应号码的会话,则使用缓冲里的数据,如果条件都不满足,则需要去数据库查询到ID,并新建会话,之后将新会话存放到缓冲。
public static Conversation get(Context context, ContactList recipients, boolean allowQuery) {
if (recipients.size() < 1) {
return createNew(context);
}
Conversation conv = Cache.get(recipients);
if (conv != null) {
return conv;
}
long threadId = getOrCreateThreadId(context, recipients);
conv = new Conversation(context, threadId, allowQuery);
。。。
Cache.put(conv);
}
getOrCreateThreadId用来获取ThreadId,由Threads.getOrCreateThreadId实现,这个方法在Telephony.java里面。
getOrCreateThreadId类似前面提到的小区广播的实现过程,先封装一个"content://mms-sms/threadID"的URI,但只使用一个参数recipient,
在MmsSmsProvider.query里面,也是通过URI match找到操作码URI_THREAD_ID,,使用getThreadId查找或创建其ThreadId,getAddressIds获取到匹配的recipientIds,再取Threads表里查询,如果查询到,返回游标,如果没有记录,则用insertThread向Threads里插入记录,再查询到ThreadId。
private synchronized Cursor getThreadId(List<String> recipients) {
Set<Long> addressIds = getAddressIds(recipients);
。。。
recipientIds = Long.toString(addressId);
。。。
String[] selectionArgs = new String[] { recipientIds };
SQLiteDatabase db = mOpenHelper.getReadableDatabase();
db.beginTransaction();
Cursor cursor = null;
try {
// Find the thread with the given recipients
cursor = db.rawQuery(THREAD_QUERY, selectionArgs);
if (cursor.getCount() == 0) {
// No thread with those recipients exists, so create the thread.
cursor.close();
Log.d(LOG_TAG, "getThreadId: create new thread_id for recipients " + recipients);
insertThread(recipientIds, recipients);
// The thread was just created, now find it and return it.
cursor = db.rawQuery(THREAD_QUERY, selectionArgs);
。。。
}
3. ConversationListItem
3.1 ConversationListItem
3.1.1 Layout分析
如前所述,短信记录的每条信息显示是使用conversation_list_item.xml布局的,在本例中,其直观显示是如下形式的一条短信记录,
conversation_list_item.xml文件如下,它是一个ConversationListItem类型的布局,
(需要注意不同屏幕使用这个文件的位置,本例中在packages\apps\Mms\res\layout-mdpi下面,不是packages\apps\Mms\res\layout)
conversation_list_item.xml
<?xml version="1.0" encoding="utf-8"?>
<com.android.mms.ui.ConversationListItem xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="?android:attr/listPreferredItemHeight"
android:background="?android:attr/activatedBackgroundIndicator" >
<CheckBox
android:id="@+id/select_box"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_centerVertical="true"
android:clickable="false"
android:focusable="false"
android:focusableInTouchMode="false" />
<com.android.mms.ui.MmsQuickContactBadge
android:id="@+id/avatar"
android:visibility="gone"
android:layout_centerVertical="true"
android:layout_toEndOf="@id/select_box"
style="?android:attr/quickContactBadgeStyleWindowLarge" />
<ImageView
android:id="@+id/presence"
android:visibility="invisible"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_alignParentTop="true"
android:src="@drawable/ic_unread_label"/>
<LinearLayout
android:id="@+id/iconlist"
android:layout_alignParentTop="true"
android:layout_alignParentEnd="true"
android:layout_marginTop="7dip"
android:layout_marginEnd="10dip"
android:orientation="horizontal"
android:layout_height="wrap_content"
android:layout_width="wrap_content" >
<ImageView android:id="@+id/draft"
android:visibility="gone"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:src="@drawable/ic_draft" />
<ImageView android:id="@+id/error"
android:paddingStart="6dp"
android:visibility="gone"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:src="@drawable/ic_list_alert_sms_failed" />
<ImageView android:id="@+id/attachment"
android:paddingStart="6dp"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:visibility="gone"
android:src="@drawable/ic_attachment_universal_small" />
<ImageView android:id="@+id/mute"
android:paddingStart="6dp"
android:visibility="gone"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:src="@drawable/ipmsg_silent" />
</LinearLayout>
<RelativeLayout
android:layout_alignParentTop="true"
android:layout_marginTop="4dip"
android:layout_marginStart="12dip"
android:layout_toEndOf="@id/avatar"
android:layout_toStartOf="@id/iconlist"
android:gravity="center_vertical"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView android:id="@+id/unread"
android:background="@drawable/ipmsg_message_box"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:textSize="12sp"
android:textColor="@color/text_color_unread"
android:singleLine="true"
android:layout_alignParentEnd="true" />
<RelativeLayout android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:layout_toStartOf="@id/unread">
<TextView android:id="@+id/draft_and_smgcount"
android:textAppearance="?android:attr/textAppearanceMedium"
android:layout_alignParentEnd="true"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="6dip"
android:singleLine="true" />
<com.android.mms.util.AlwaysMarqueeTextView android:id="@+id/from"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceMedium"
android:singleLine="true"
android:layout_alignParentStart="true"
android:layout_toStartOf="@id/draft_and_smgcount"
android:layout_marginEnd="6dip"
android:ellipsize="marquee" />
</RelativeLayout>
</RelativeLayout>
<TextView android:id="@+id/date"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:textSize= "12sp"
android:textColor="@color/dlg_text_counter_color"
android:layout_alignParentBottom="true"
android:layout_alignParentEnd="true"
android:layout_marginEnd="10dip"
android:layout_marginBottom="4dip"
android:gravity="bottom" />
<TextView android:id="@+id/subject"
android:layout_height="wrap_content"
android:layout_width="fill_parent"
android:textSize= "12sp"
android:layout_alignParentBottom="true"
android:layout_toEndOf="@id/avatar"
android:layout_toStartOf="@id/date"
android:layout_marginStart="12dip"
android:layout_marginBottom="4dip"
android:gravity="bottom"
android:textColor="@color/dlg_text_counter_color"
android:lines="2"
android:ellipsize="end" />
</com.android.mms.ui.ConversationListItem>
其对应的布局图形如下,图中左边是layout组成,右上是layout布局表现,右下是对layout里面的子view编号,便于理解。
分析xml文件,并对照图例可以看出,
ConversationListItem是整个item布局类,表现形式见下图左边,它指定了高度
android:layout_height="?android:attr/listPreferredItemHeight"
这个布局由6个同级view组成,见下图中间及右上,其中
checkbox供选择使用,见右下数字0,alignParentStart表示它在开头位置,
android:layout_alignParentStart="true"
MmsQuickContactBadge做头像显示,见右下数字1,
ImageView做未读标识等用途,见右下数字2,通过下面的属性将它放置在最右上的位置
android:layout_alignParentEnd="true"
android:layout_alignParentTop="true"
LinearLayout是个组合视图,见右下数字3和iconlist小图,它有四个子ImageView表示短信的信息状态,具体名字见iconlist小图。注意四个子ImageView表示同时最多只能显示四个图标,不是表示短信只有这四种状态。通过下面属性,将它放置在MmsQuickContactBadge后面。
android:orientation="horizontal"
<iconlist>
RelativeLayout是个组合视图,见右下数字4和Text Title小图,包含3个TextView,用来显示Title和消息数目、未读消息数目。在RelativeLayout,通过下面的属性设置,将RelativeLayout(index4的view)和上面的LinearLayout(index3的view)并行放置在同一行。
android:layout_toEndOf="@id/avatar"
android:layout_toStartOf="@id/iconlist"
TextView显示日期,见右下数字5,用下面2个属性表示它的位置在右下,
android:layout_alignParentBottom="true"
android:layout_alignParentEnd="true"
TextView 显示短线截取的部分内容,见右下数字6,用下面3个属性表明了它的位置。
android:layout_alignParentBottom="true"
android:layout_toEndOf="@id/avatar"
android:layout_toStartOf="@id/date"
<Text Content>
3.1.2 代码分析
3.1.2.1 ConversationListItem实例创建
ConversationListItem实例创建栈如下,因为有缓存使用,不一定每次进入列表都会创建这个类实例,所以不一定能调试到,要在此设置断点调试,有一个小技巧,就是将mms进程终止掉,让其自动重启(可通过发送短信或cb的方法加快重启过程),这样就可以进行调试。
ConversationListItem.<init>(Context, AttributeSet) line: 130
Constructor.constructNative(Object[], Class, Class[], int, boolean) line: not available [native method]
Constructor.newInstance(Object...) line: 423
PhoneLayoutInflater(LayoutInflater).createView(String, String, AttributeSet) line: 594
PhoneLayoutInflater(LayoutInflater).createViewFromTag(View, String, AttributeSet) line: 696
PhoneLayoutInflater(LayoutInflater).inflate(XmlPullParser, ViewGroup, boolean) line: 469
PhoneLayoutInflater(LayoutInflater).inflate(int, ViewGroup, boolean) line: 397
ConversationListAdapter.newView(Context, Cursor, ViewGroup) line: 103
ConversationListAdapter(CursorAdapter).getView(int, View, ViewGroup) line: 250
ListView(AbsListView).obtainView(int, boolean[]) line: 2338
ListView.makeAndAddView(int, int, boolean, int, boolean) line: 1812
ListView.fillDown(int, int) line: 698
ListView.fillFromTop(int) line: 759
ListView.layoutChildren() line: 1631
ListView(AbsListView).onLayout(boolean, int, int, int, int) line: 2149
ConversationListItem实例创建的主要过程分析如下,在getView来获取view的时候,因为缓冲没有数据,会使用newView新建一个view,
public View getView(int position, View convertView, ViewGroup parent) {
View v;
if (convertView == null) {
v = newView(mContext, mCursor, parent);
} else {
v = convertView;
}
bindView(v, mContext, mCursor);
return v;
}
newView实际的过程是inflate一个布局文件,这个布局文件就是我们分析的layout,它是ConversationListItem形式的,我们知道,在inflate创建布局文件时,会根据节点创建view实例,所以会使用类的构造类Constructor来创建ConversationListItem实例。
public View newView(Context context, Cursor cursor, ViewGroup parent) {
if (LOCAL_LOGV) Log.v(TAG, "inflating new view");
return mFactory.inflate(R.layout.conversation_list_item, parent, false);
}
Checkbox的主要作用是用来勾选短信列表的某一个item,并进行删除操作的。
对于checkbox,在ConversationListItem里面定义了一个引用mSelectBox,
private CheckBox mSelectBox;
并在onFinishInflate里面初始化它,关联到Layout里面的select_box,
mSelectBox = (CheckBox) findViewById(R.id.select_box);
当列表项被点击时,执行ConversationList.onListItemClick(ListView, View, int, long)这个方法,在里面添加下面代码,当处于删除模式时,如下,将Conversation设置为check状态,然后更新界面,
if (mListAdapter != null && mDeleteMode) {
if (conv.isChecked()) {
conv.setIsChecked(false);
} else {
conv.setIsChecked(true);
}
mListAdapter.notifyDataSetChanged();
updateSelectView();
return;
}
更新界面时,执行ConversationListItem.bind(Context, Conversation) ,每加载一个item项都会执行一次bind,在这里,会根据mSelectMode 决定checkbox是否显示,根据Conversation的状态决定是否选中。
所以实际上,不是我们主观上看到的那样由checkbox决定了当前项目是否选中,而是当前项选中后决定checkbox的显示状态。
if (mSelectMode) {
mSelectBox.setVisibility(View.VISIBLE);
mAvatarView.setClickable(false);
} else {
mSelectBox.setVisibility(View.GONE);
mAvatarView.setClickable(true);
}
if (mConversation.isChecked()) {
mSelectBox.setChecked(true);
} else {
mSelectBox.setChecked(false);
}
上面就是checkbox在代码里的处理过程。
R.id.avatar用来显示发件人头像,定义如下,MmsQuickContactBadge继承自QuickContactBadge,再往上是ImageView,一个通用的视图组件。
private MmsQuickContactBadge mAvatarView;
其实例获取在ConversationListItem的onFinishInflate里面,
mAvatarView = (MmsQuickContactBadge) findViewById(R.id.avatar);
如果希望AvatarView能够被点击,相应事件,则可以看到在bind有如下代码,表示在删除模式下不能被点击,在列表模式下可以被点击。
if (mSelectMode) {
mAvatarView.setClickable(false);
} else {
mAvatarView.setClickable(true);
}
AvatarView被点击之后,执行的调用栈如下,可见是由View直接调用了MmsQuickContactBadge类的onClick,并不是ConversationList或ConversationListItem里做了onClick的相关处理。
MmsQuickContactBadge(QuickContactBadge).onClick(View) line: 217
MmsQuickContactBadge.onClick(View) line: 75
MmsQuickContactBadge(View).performClick() line: 4212
View$PerformClick.run() line: 17476
Handler.handleCallback(Message) line: 800
ViewRootImpl$ViewRootHandler(Handler).dispatchMessage(Message) line: 100
QuickContactBadge类的onClick代码如下,
@Override
public void onClick(View v) {
if (mContactUri != null) {
QuickContact.showQuickContact(getContext(), QuickContactBadge.this, mContactUri,
QuickContact.MODE_LARGE, mExcludeMimes);
} else if (mContactEmail != null) {
mQueryHandler.startQuery(TOKEN_EMAIL_LOOKUP_AND_TRIGGER, mContactEmail,
Uri.withAppendedPath(Email.CONTENT_LOOKUP_URI, Uri.encode(mContactEmail)),
EMAIL_LOOKUP_PROJECTION, null, null, null);
} else if (mContactPhone != null) {
mQueryHandler.startQuery(TOKEN_PHONE_LOOKUP_AND_TRIGGER, mContactPhone,
Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI, mContactPhone),
PHONE_LOOKUP_PROJECTION, null, null, null);
} else {
// If a contact hasn't been assigned, don't react to click.
return;
}
}
我们当前的代码走mContactPhone路径,mContactPhone是在assignContactFromPhone里面赋值的,assignContactFromPhone则是通过ConversationListItem的updateAvatarView调用的(传入的number就赋值给mContactPhone),
mAvatarView.assignContactFromPhone(number, true);
updateAvatarView可以被updateFromView和bind调用,这两个方法我们在前面有过描述。所以从整个过程来看,我们可以看出avatar被点击和其需要的数据来源。
3.2 绘制分析
3.2.1 Message列表创建
如ConversationList的layout描述,Messag列表主要是由list呈现,而list的item项是由ConversationListItem布局的,我们这里讨论每个item项的创建以及整个list的创建。
在使用Cache时,ConversationListItem的创建过程如下,
ConversationListItem.bind(Context, Conversation) line: 480
ConversationListAdapter.bindView(View, Context, Cursor) line: 77
ConversationListAdapter(CursorAdapter).getView(int, View, ViewGroup) line: 250
ListView(AbsListView).obtainView(int, boolean[]) line: 2186
ListView.makeAndAddView(int, int, boolean, int, boolean) line: 1877
ListView.fillSpecific(int, int) line: 1356
ListView.layoutChildren() line: 1671
ListView(AbsListView).onLayout(boolean, int, int, int, int) line: 2037
ListView(View).layout(int, int, int, int) line: 14118
ListView(ViewGroup).layout(int, int, int, int) line: 4467
RelativeLayout.onLayout(boolean, int, int, int, int) line: 1021
从上往下分析该栈:
1)ConversationListItem.bind
ConversationListItem.bind的代码比较多,仔细分析,实际上逻辑比较简单,主要是设置每个view的内容和状态,比如说设置日期的代码如下,
mDateView.setVisibility(VISIBLE);
if(ConversationList.sConversationListOption ==
ConversationList.OPTION_CONVERSATION_LIST_IMPORTANT &&
!conversation.hasUnreadMessages()) {
mDateView.setText(MessageUtils.formatTimeStampStringExtend(context,
conversation.getImpDate()));
}
其数据来源于conversation,我们要进一步分析conversation是怎么传递进来的,以及为什么要调用bind。
2)ConversationListAdapter
ConversationListAdapter 是ConversationList的layout里的list对应的Adapter,它继承自MessageCursorAdapter-CursorAdapter-baseAdapter, 其方法bindView在本例中执行的主要代码如下,其中headerview是一个ConversationListItem引用,是调用方法传递过来的,conversation是根据游标新建的,最后调用bind,所以该方法的作用就是获取到conversation,并用准备将其中的数据赋值到view。
public void bindView(View view, Context context, Cursor cursor) {
ConversationListItem headerView = (ConversationListItem) view;
Conversation conv;
if (!mIsScrolling) {
Conversation.setNeedCacheConv(false);
conv = Conversation.from(context, cursor);
Conversation.setNeedCacheConv(true);
if (mSubjectSingleLine) {
headerView.setSubjectSingleLineMode(true);
}
if (conv != null) {
conv.setIsChecked(sSelectedTheadsId.contains(conv.getThreadId()));
}
headerView.bind(context, conv);
}
}
(CursorAdapter).getView则负责获取到view实例和游标实例。
view的获取有两种方式,一种是调用者传递过来以前缓存的,一种是新创建一个,在ConversationListItem实例创建中有讲到如何创建。
游标实例则在Adapter创建时创建。
3)ListView
在ListView部分,我们从layoutChildren向上分析,向下部分设计到太多ListView的实现机制问题,暂不讨论。
layoutChildren实现ListView子窗口布局,实际上就是list在layout所占部分。
本例从launcher进入Message列表,执行代码路径如下,mSyncPosition是当前item索引值,mSpecificTop是当前layout相对Top的位置。
layoutChildren:
case LAYOUT_SYNC:
sel = fillSpecific(mSyncPosition, mSpecificTop);
fillSpecific在知道位置绘制指定项的item数据,然后上下绘制其他的item。它先通过makeAndAddView完成当前item的创建并添加到layout,fillDown、fillUp则根据layout大小,上下绘制item完成整个list的绘制,使用的方法也是makeAndAddView,adjustViewsUpOrDown调整item位置。
/**
* Put a specific item at a specific location on the screen and then build
* up and down from there.
*
* @param position The reference view to use as the starting point
* @param top Pixel offset from the top of this view to the top of the
* reference view.
*
* @return The selected view, or null if the selected view is outside the
* visible area.
*/
private View fillSpecific(int position, int top) {
boolean tempIsSelected = position == mSelectedPosition;
View temp = makeAndAddView(position, top, true, mListPadding.left, tempIsSelected);
…
below = fillDown(position + 1, temp.getBottom() + dividerHeight);
// This will correct for the bottom of the last view not touching the bottom of the list
adjustViewsUpOrDown();
above = fillUp(position - 1, temp.getTop() - dividerHeight);
int childCount = getChildCount();
if (childCount > 0) {
correctTooLow(childCount);
}
}
顾名思义,makeAndAddView就是新建一个view,并将它添加到list里面。obtainView负责创建view,setupChild负责将view添加到layout,并设置好view的位置和相关属性。
private View makeAndAddView(int position, int y, boolean flow, int childrenLeft,
boolean selected) {
View child;
child = obtainView(position, mIsScrap);
// This needs to be positioned and measured
setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]);
return child;
}
obtainView将对应位置的view和数据关联起来,并返回view。它通过mRecycler获取到缓存的view,再通过mAdapter.getView将数据填充到view,mAdapter.getView即是我们前面分析到的ConversationListAdapter(CursorAdapter).getView。
至此,Message列表的创建过程就明了了。
3.3 功能分析
3.3.1 Message列表更新
当数据库内容发生变化(包括新短信、删除、发送状态改变等),就会触发onContentChanged,其调用栈如下:
ConversationList$2.onContentChanged(ConversationListAdapter) line: 474
ConversationListAdapter.onContentChanged() line: 118
CursorAdapter$ChangeObserver.onChange(boolean) line: 463
CursorAdapter$ChangeObserver(ContentObserver).onChange(boolean, Uri) line: 129
ContentObserver$NotificationRunnable.run() line: 180
Handler.handleCallback(Message) line: 808
Handler.dispatchMessage(Message) line: 103
Looper.loop() line: 193
从下往上分析,
1)ContentObserver
我们已经很熟悉handleCallback处理的是Post的消息,消息一般是runable,所以可以看到ContentObserver. dispatchChange有post这一消息的过程(dispatchChange的触发过程比较复杂,我们将在其他章节介绍,本例中当数据库内容变化时会触发这个流程)。
public final void dispatchChange(boolean selfChange, Uri uri) {
if (mHandler == null) {
onChange(selfChange, uri);
} else {
mHandler.post(new NotificationRunnable(selfChange, uri));
}
}
然后这个消息会运行其内部类NotificationRunnable的run方法,
public void run() {
ContentObserver.this.onChange(mSelfChange, mUri);
}
This指向的是ContentObserver的实例ChangeObserver,它是CursorAdapter的内部类,也是ContentObserver的子类,由于ConversationListAdapter通过CursorWrapper向ContentObservable注册了ContentObserver,所以当dispatchChange事件时,通过轮询,最终将事件传递给ChangeObserver。
而后,ChangeObserver调用封装类CursorAdapter的onContentChanged,将事件上传。
private class ChangeObserver extends ContentObserver {
public void onChange(boolean selfChange) {
onContentChanged();
}
}
2)onContentChanged
onContentChanged是CursorAdapter的一个方法,主要功能是当数据库内容更新后,重新使用游标查询数据。但这个方法被ConversationListAdapter重载,它给自己的客户提供一个借口类OnContentChangedListener,让客户去实现这个借口类,自己则调用接口类的onContentChanged方法执行客户定义的功能。
protected void onContentChanged() {
if (mCursor != null && !mCursor.isClosed()) {
if (mOnContentChangedListener != null) {
mOnContentChangedListener.onContentChanged(this);
}
}
}
3)ConversationList
ConversationList就是ConversationListAdapter的客户,它实现了接口类,如下,主要是调用startAsyncQuery进行数据的查询。
private final ConversationListAdapter.OnContentChangedListener mContentChangedListener =
new ConversationListAdapter.OnContentChangedListener() {
@Override
public void onContentChanged(ConversationListAdapter adapter) {
if (mIsInActivity) {
mNeedQuery = true;
startAsyncQuery();
}
}
};
并且ConversationList在其onStart向ConversationListAdapter注册了这个接口类监听器:
mListAdapter.setOnContentChangedListener(mContentChangedListener);
所以,当数据库数据改变时,根据上面相关调用流程,完成了Message列表的刷新功能。
BTW,附上游标改变后注册ContentObserver的调用栈:
ContentResolver$CursorWrapperInner(CursorWrapper).registerContentObserver(ContentObserver) line: 178
ConversationListAdapter(CursorAdapter).swapCursor(Cursor) line: 340
ConversationListAdapter(CursorAdapter).changeCursor(Cursor) line: 313
ConversationList$ThreadListQueryHandler.onQueryComplete(int, Object, Cursor) line: 1896
ConversationList$ThreadListQueryHandler(AsyncQueryHandler).handleMessage(Message) line: 344
ConversationList$ThreadListQueryHandler(Handler).dispatchMessage(Message) line: 110
Looper.loop() line: 193
3.3.2 Message列表定位
因为ConversationList是ListActivity,所以可以使用getListView获取ListView,再使用ListView的setSelectionFromTop,将列表显示开始位置指定到特定位置。
需要注意2个问题,
1)
在ConversationList会记录上次浏览列表的起始位置,所以不是随意就能设置好列表的显示位置,如onQueryComplete通过下面代码控制了什么时候可以设置这个起始位置,我们需要根据mSavedFirstVisiblePosition的值和相关逻辑去处理。
if (mSavedFirstVisiblePosition != AdapterView.INVALID_POSITION) {
// Restore the list to its previous position.
getListView().setSelectionFromTop(mSavedFirstVisiblePosition,
mSavedFirstItemOffset);
mSavedFirstVisiblePosition = AdapterView.INVALID_POSITION;
}
结合列表的创建过程,我们知道,当同步绘制列表时,是从mSyncPosition开始的,
fillSpecific(mSyncPosition, mSpecificTop);
而mSyncPosition正是setSelectionFromTop里传递进去的,所以列表起始位置只能通过setSelectionFromTop来设置。
public void setSelectionFromTop(int position, int y) {
…
mSyncPosition = position;
…
}
2)
游标的更新,当数据库数据变化后,需要更新游标,才能获取到正确的值。下面是onQueryComplete完成后,用changeCursor替换掉旧游标的过程,防止游标数据过期。
ConversationListAdapter(CursorAdapter).swapCursor(Cursor) line: 340
ConversationListAdapter(CursorAdapter).changeCursor(Cursor) line: 313
ConversationList$ThreadListQueryHandler.onQueryComplete(int, Object, Cursor) line: 1896
ConversationList$ThreadListQueryHandler(AsyncQueryHandler).handleMessage(Message) line: 344
ConversationList$ThreadListQueryHandler(Handler).dispatchMessage(Message) line: 110
Looper.loop() line: 193
在列表创建时,CursorAdapter. getView的游标最初是在CursorAdapte创建时传入的,之后可以通过swapCursor的方法更新。
目 录
2. ConversationList
2.1.2.1 ConversationListAdapter
2.2.1 列表项click
2.2.2 Long Press click to Delete
2.3.1 会话ID创建
3. ConversationListItem
3.1.2.1 ConversationListItem实例创建
1. 概要
本文主要分析短信列表界面的主要控件的界面显示和处理逻辑。
2. ConversationList
2.1 ConversationList
2.1.1 Layout分析
ConversationList对应的layout是conversation_list_screen,它主要分3部分,第一部分是做全选的控制,第二部分是list列表,第三部分是列表为空的布局。具体的表现形式如下,布局文件就不再展开了。其中list列表将在另外的章节分析。
2.1.2 代码分析
2.1.2.1 ConversationListAdapter
ConversationListAdapter是ConversationListItem的数据适配器,在ConversationList.onCreate里创建,它通过bindView将会话信息绑定到item的layout上。
其创建过程如下,
private void initListAdapter() {
mListAdapter = new ConversationListAdapter(this, null);
/** M: now this code is useless and will lead to a JE, comment it.
* listener is set in onStart
*/
//mListAdapter.setOnContentChangedListener(mContentChangedListener);
setListAdapter(mListAdapter);
getListView().setRecyclerListener(mListAdapter);
}
因为ConversationList继承自ListActivity,setListAdapter将Adapter实例设置给ListActivity,这里会调用ensureList,它会加载一个布局,确保有view能用来对应Adapter。
public void setListAdapter(ListAdapter adapter) {
synchronized (this) {
ensureList();
mAdapter = adapter;
mList.setAdapter(adapter);
}
}
private void ensureList() {
if (mList != null) {
return;
}
setContentView(com.android.internal.R.layout.list_content_simple);
}
而实际上,mList已经在 onContentChanged被初始化为Layout里对应的ListView,
mList = (ListView)findViewById(com.android.internal.R.id.list);
这个ListView就是Message列表的容器了。
2.2 功能分析
2.2.1 列表项click
单击列表项的调用栈如下:
ConversationList.onListItemClick(ListView, View, int, long) line: 1086
ListActivity$2.onItemClick(AdapterView, View, int, long) line: 319
ListView(AdapterView).performItemClick(View, int, long) line: 298
ListView(AbsListView).performItemClick(View, int, long) line: 1128
AbsListView$PerformClick.run() line: 2815
AbsListView$1.run() line: 3574
Handler.handleCallback(Message) line: 800
ViewRootImpl$ViewRootHandler(Handler).dispatchMessage(Message) line: 100
2.2.2 Long Press click to Delete
长按item删除会话流程如下:
长按出现选择菜单,
mConvListOnCreateContextMenuListener
onCreateContextMenu
点击删除
ConversationList.confirmDeleteThreads(Collection, AsyncQueryHandler) line:
ConversationList.confirmDeleteThread(long, AsyncQueryHandler) line: 1320
ConversationList.onContextItemSelected(MenuItem) line: 1247
ConversationList(Activity).onMenuItemSelected(int, MenuItem) line: 2584
PhoneWindow$DialogMenuCallback.onMenuItemSelected(MenuBuilder, MenuItem) line:
开始删除之前,要做一个后台查询,看是否有lock住的消息,Conversation.startQueryHaveLockedMessages,
之后是后台查询handler.startQuery:
AsyncQueryHandler
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);
msg.arg1 = EVENT_ARG_QUERY;
WorkerArgs args = new WorkerArgs();
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);
}
这个查询过程将消息发给mWorkerThreadHandler,这是异步查询AsyncQueryHandler自己的handler,它封装完消息EVENT_ARG_QUERY后,再发送出去,
Message reply = args.handler.obtainMessage(token);
reply.obj = args;
reply.arg1 = msg.arg1;
reply.sendToTarget();
这个消息的接收对象的设置见args.handler = this; ,在本例中,这个handler是ThreadListQueryHandler。
数据库查询完成后执行onQueryComplete(HAVE_LOCKED_MESSAGES_TOKEN),
ConversationList$ThreadListQueryHandler.onQueryComplete(int, Object, Cursor) line: 1610
ConversationList$ThreadListQueryHandler(AsyncQueryHandler).handleMessage(Message) line: 344
ConversationList$ThreadListQueryHandler(Handler).dispatchMessage(Message) line: 107
Looper.loop() line: 194
之后会给出一个确认删除对话框confirmDeleteThreadDialog
在弹出的确认对话框中,选择删除,执行listener函数。
ConversationList$DeleteThreadListener.onClick(DialogInterface, int) line: 1484
AlertController$ButtonHandler.handleMessage(Message) line: 176
之后开始删除Conversation.startDelete
最后onDeleteComplete
响应函数会触发一系列其他的数据库操作,如查询list,查询unread等,
AsyncQueryHandler$WorkerHandler.handleMessage(Message) line: 120
AsyncQueryHandler$WorkerHandler(Handler).dispatchMessage(Message) line: 107
【Tips】
它的threadIds是在点击删除功能时,onContextItemSelected设置的,和长按的时候在onCreateContextMenu里选择的有时间差,不一定是同一个,在会话列表更新的时候,会有影响,所以会误删。
所以在onCreateContextMenu里要记录thread id。
2.3 数据操作
2.3.1 会话ID创建
Message列表是以会话为item进行显示的,会话是以为ID记录的,我们分析一下各种会话ID是如何创建的,
应用在CBMessageReceiverService.java收到小区广播后,使用handleCBMessageReceived方法进行处理,其中insertMessage是用来存储CB的。
insertMessage会调用CbProvider的insert方法
public Uri insert(Uri url, ContentValues initialValues) {
Uri result = null;
// TODO Check Permission
// checkPermission();
SQLiteDatabase db = mOpenHelper.getWritableDatabase();
SQLiteDatabase dbmmssms = mMmsSmsOpenHelper.getWritableDatabase();
int match = URI_MATCHER.match(url);
ContentValues values;
long rowID;
String table = null;
Log.d(TAG, " insert match = "+match);
switch (match) {
case URL_MESSAGES:
//table = CbDatabaseHelper.CBMESSAGE_TABLE;
table = MmsSmsDatabaseHelper.TABLE_CELLBROADCAST;
values = internalInsertMessages(initialValues);
rowID = dbmmssms.insert(table, null, values);
Log.d(TAG, "insert to cellbroadcast " + values);
if (rowID > 0) {
result = Uri.parse("content://messages/" + rowID);
notifyChange();
return result;
}
break;
…
}
在这里,我们重点关注一下CB Message的ThreadId是怎样获取的,在internalInsertMessages里面,显然threadId不是我们主动填充进去的,所以是通过getOrCreateThreadId获得的。
private ContentValues internalInsertMessages(ContentValues initialValues) {
ContentValues values;
if (initialValues != null) {
values = new ContentValues(initialValues);
} else {
values = new ContentValues();
}
Long threadId = values.getAsLong(EncapsulatedTelephony.SmsCb.THREAD_ID);
String address = values.getAsString(EncapsulatedTelephony.SmsCb.CHANNEL_ID);
if (((threadId == null) || (threadId == 0)) && (address != null)) {
values.put(EncapsulatedTelephony.SmsCb.THREAD_ID,
getOrCreateThreadId(address));
}
…
}
getOrCreateThreadId代码如下,它封装一个URI地址,包含了threadID和参数recipient、cellbroadcast,并使用SqliteWrapper.query进行查询,
private String getOrCreateThreadId(String address) {
//Uri.Builder uriBuilder = THREAD_ID_URI.buildUpon();
Uri.Builder uriBuilder = (Uri.parse("content://mms-sms/threadID")).buildUpon();
uriBuilder.appendQueryParameter("recipient", address);
uriBuilder.appendQueryParameter("cellbroadcast", address);
Uri uri = uriBuilder.build();
// TODO need replace with helper interface.
Cursor cursor = null;
long token = Binder.clearCallingIdentity();
try {
cursor = SqliteWrapper.query(this.getContext(), this
.getContext().getContentResolver(), uri, ID_PROJECTION, null,
null, null);
…
if (cursor.moveToFirst()) {
return String.valueOf(cursor.getLong(0));
…
}
SqliteWrapper会调用到ContentResolver,ContentResolver则最终调用到MmsSmsProvider,
在MmsSmsProvider.query里面,也是通过URI match找到操作码URI_THREAD_ID,根据参数cellbroadcast,使用getCBThreadId给CB查找或创建其ThreadId。
Threads表内容如下所示,里面的关键字段有_id ,recipient_ids,要想找到合适的ThreadId,只有通过recipient_ids来查询,
所以在getCBThreadId方法里面,通过getAddressIds从canonical_addresses表里面找到和号码匹配的recipient_ids列表,再到Threads表里面找到recipient_ids相同和type是小区广播的_id,即是我们需要的ThreadId,使用的查询语句如下。
String queryString = "SELECT _id FROM threads " + "WHERE type=" + EncapsulatedTelephony.Threads.CELL_BROADCAST_THREAD + " AND recipient_ids=?";
如果没有满足条件的ThreadId,调用insertThread()创建一个新的thread。
在Message列表界面通过按钮新建短信时,执行createNewMessage(),通过createIntent传递一个threadId=0的ID过去,
private void createNewMessage() {
startActivity(ComposeMessageActivity.createIntent(this, 0));
}
public static Intent createIntent(Context context, long threadId) {
Intent intent = new Intent(context, ComposeMessageActivity.class);
if (threadId > 0) {
intent.setData(Conversation.getUri(threadId));
}
return intent;
}
因为threadId为0,在会话创建中会获取或创建一个threadId,具体过程和下节相同。
在联系人的详情界面,点击发送短信,因为有号码存在,所以在ComposeMessageActivity.Oncreate方法里面会去查询或创建一个threadId。
onCreate的initActivityState里,有这么一段代码是获取会话实例的,mConversation.get通过号码获取到会话实例,
Uri intentData = intent.getData();
/// M: Code analyze 034, If intent is SEND,just create a new empty thread,
/// otherwise Conversation.get() will throw exception.
String action = intent.getAction();
if (intentData != null && (TextUtils.isEmpty(action) ||
!action.equals(Intent.ACTION_SEND))) {
/// M: group-contact send message
// try to get a conversation based on the data URI passed to our intent.
if (intentData.getPathSegments().size() < 2) {
mConversation = mConversation.get(getApplicationContext(),ContactList.getByNumbers(
getStringForMultipleRecipients(Conversation.getRecipients(intentData)),
false /* don't block */, true /* replace number */),false);
} else {
mConversation = Conversation.get(getApplicationContext(), intentData, false);
}
/// @}
mWorkingMessage.setText(getBody(intentData));
Conversation get代码如下,如果号码为空则新建会话,如果缓冲里存储了对应号码的会话,则使用缓冲里的数据,如果条件都不满足,则需要去数据库查询到ID,并新建会话,之后将新会话存放到缓冲。
public static Conversation get(Context context, ContactList recipients, boolean allowQuery) {
if (recipients.size() < 1) {
return createNew(context);
}
Conversation conv = Cache.get(recipients);
if (conv != null) {
return conv;
}
long threadId = getOrCreateThreadId(context, recipients);
conv = new Conversation(context, threadId, allowQuery);
。。。
Cache.put(conv);
}
getOrCreateThreadId用来获取ThreadId,由Threads.getOrCreateThreadId实现,这个方法在Telephony.java里面。
getOrCreateThreadId类似前面提到的小区广播的实现过程,先封装一个"content://mms-sms/threadID"的URI,但只使用一个参数recipient,
在MmsSmsProvider.query里面,也是通过URI match找到操作码URI_THREAD_ID,,使用getThreadId查找或创建其ThreadId,getAddressIds获取到匹配的recipientIds,再取Threads表里查询,如果查询到,返回游标,如果没有记录,则用insertThread向Threads里插入记录,再查询到ThreadId。
private synchronized Cursor getThreadId(List<String> recipients) {
Set<Long> addressIds = getAddressIds(recipients);
。。。
recipientIds = Long.toString(addressId);
。。。
String[] selectionArgs = new String[] { recipientIds };
SQLiteDatabase db = mOpenHelper.getReadableDatabase();
db.beginTransaction();
Cursor cursor = null;
try {
// Find the thread with the given recipients
cursor = db.rawQuery(THREAD_QUERY, selectionArgs);
if (cursor.getCount() == 0) {
// No thread with those recipients exists, so create the thread.
cursor.close();
Log.d(LOG_TAG, "getThreadId: create new thread_id for recipients " + recipients);
insertThread(recipientIds, recipients);
// The thread was just created, now find it and return it.
cursor = db.rawQuery(THREAD_QUERY, selectionArgs);
。。。
}
3. ConversationListItem
3.1 ConversationListItem
3.1.1 Layout分析
如前所述,短信记录的每条信息显示是使用conversation_list_item.xml布局的,在本例中,其直观显示是如下形式的一条短信记录,
conversation_list_item.xml文件如下,它是一个ConversationListItem类型的布局,
(需要注意不同屏幕使用这个文件的位置,本例中在packages\apps\Mms\res\layout-mdpi下面,不是packages\apps\Mms\res\layout)
conversation_list_item.xml
<?xml version="1.0" encoding="utf-8"?>
<com.android.mms.ui.ConversationListItem xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="?android:attr/listPreferredItemHeight"
android:background="?android:attr/activatedBackgroundIndicator" >
<CheckBox
android:id="@+id/select_box"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_centerVertical="true"
android:clickable="false"
android:focusable="false"
android:focusableInTouchMode="false" />
<com.android.mms.ui.MmsQuickContactBadge
android:id="@+id/avatar"
android:visibility="gone"
android:layout_centerVertical="true"
android:layout_toEndOf="@id/select_box"
style="?android:attr/quickContactBadgeStyleWindowLarge" />
<ImageView
android:id="@+id/presence"
android:visibility="invisible"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_alignParentTop="true"
android:src="@drawable/ic_unread_label"/>
<LinearLayout
android:id="@+id/iconlist"
android:layout_alignParentTop="true"
android:layout_alignParentEnd="true"
android:layout_marginTop="7dip"
android:layout_marginEnd="10dip"
android:orientation="horizontal"
android:layout_height="wrap_content"
android:layout_width="wrap_content" >
<ImageView android:id="@+id/draft"
android:visibility="gone"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:src="@drawable/ic_draft" />
<ImageView android:id="@+id/error"
android:paddingStart="6dp"
android:visibility="gone"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:src="@drawable/ic_list_alert_sms_failed" />
<ImageView android:id="@+id/attachment"
android:paddingStart="6dp"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:visibility="gone"
android:src="@drawable/ic_attachment_universal_small" />
<ImageView android:id="@+id/mute"
android:paddingStart="6dp"
android:visibility="gone"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:src="@drawable/ipmsg_silent" />
</LinearLayout>
<RelativeLayout
android:layout_alignParentTop="true"
android:layout_marginTop="4dip"
android:layout_marginStart="12dip"
android:layout_toEndOf="@id/avatar"
android:layout_toStartOf="@id/iconlist"
android:gravity="center_vertical"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView android:id="@+id/unread"
android:background="@drawable/ipmsg_message_box"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:textSize="12sp"
android:textColor="@color/text_color_unread"
android:singleLine="true"
android:layout_alignParentEnd="true" />
<RelativeLayout android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:layout_toStartOf="@id/unread">
<TextView android:id="@+id/draft_and_smgcount"
android:textAppearance="?android:attr/textAppearanceMedium"
android:layout_alignParentEnd="true"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="6dip"
android:singleLine="true" />
<com.android.mms.util.AlwaysMarqueeTextView android:id="@+id/from"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceMedium"
android:singleLine="true"
android:layout_alignParentStart="true"
android:layout_toStartOf="@id/draft_and_smgcount"
android:layout_marginEnd="6dip"
android:ellipsize="marquee" />
</RelativeLayout>
</RelativeLayout>
<TextView android:id="@+id/date"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:textSize= "12sp"
android:textColor="@color/dlg_text_counter_color"
android:layout_alignParentBottom="true"
android:layout_alignParentEnd="true"
android:layout_marginEnd="10dip"
android:layout_marginBottom="4dip"
android:gravity="bottom" />
<TextView android:id="@+id/subject"
android:layout_height="wrap_content"
android:layout_width="fill_parent"
android:textSize= "12sp"
android:layout_alignParentBottom="true"
android:layout_toEndOf="@id/avatar"
android:layout_toStartOf="@id/date"
android:layout_marginStart="12dip"
android:layout_marginBottom="4dip"
android:gravity="bottom"
android:textColor="@color/dlg_text_counter_color"
android:lines="2"
android:ellipsize="end" />
</com.android.mms.ui.ConversationListItem>
其对应的布局图形如下,图中左边是layout组成,右上是layout布局表现,右下是对layout里面的子view编号,便于理解。
分析xml文件,并对照图例可以看出,
ConversationListItem是整个item布局类,表现形式见下图左边,它指定了高度
android:layout_height="?android:attr/listPreferredItemHeight"
这个布局由6个同级view组成,见下图中间及右上,其中
checkbox供选择使用,见右下数字0,alignParentStart表示它在开头位置,
android:layout_alignParentStart="true"
MmsQuickContactBadge做头像显示,见右下数字1,
ImageView做未读标识等用途,见右下数字2,通过下面的属性将它放置在最右上的位置
android:layout_alignParentEnd="true"
android:layout_alignParentTop="true"
LinearLayout是个组合视图,见右下数字3和iconlist小图,它有四个子ImageView表示短信的信息状态,具体名字见iconlist小图。注意四个子ImageView表示同时最多只能显示四个图标,不是表示短信只有这四种状态。通过下面属性,将它放置在MmsQuickContactBadge后面。
android:orientation="horizontal"
<iconlist>
RelativeLayout是个组合视图,见右下数字4和Text Title小图,包含3个TextView,用来显示Title和消息数目、未读消息数目。在RelativeLayout,通过下面的属性设置,将RelativeLayout(index4的view)和上面的LinearLayout(index3的view)并行放置在同一行。
android:layout_toEndOf="@id/avatar"
android:layout_toStartOf="@id/iconlist"
TextView显示日期,见右下数字5,用下面2个属性表示它的位置在右下,
android:layout_alignParentBottom="true"
android:layout_alignParentEnd="true"
TextView 显示短线截取的部分内容,见右下数字6,用下面3个属性表明了它的位置。
android:layout_alignParentBottom="true"
android:layout_toEndOf="@id/avatar"
android:layout_toStartOf="@id/date"
<Text Content>
3.1.2 代码分析
3.1.2.1 ConversationListItem实例创建
ConversationListItem实例创建栈如下,因为有缓存使用,不一定每次进入列表都会创建这个类实例,所以不一定能调试到,要在此设置断点调试,有一个小技巧,就是将mms进程终止掉,让其自动重启(可通过发送短信或cb的方法加快重启过程),这样就可以进行调试。
ConversationListItem.<init>(Context, AttributeSet) line: 130
Constructor.constructNative(Object[], Class, Class[], int, boolean) line: not available [native method]
Constructor.newInstance(Object...) line: 423
PhoneLayoutInflater(LayoutInflater).createView(String, String, AttributeSet) line: 594
PhoneLayoutInflater(LayoutInflater).createViewFromTag(View, String, AttributeSet) line: 696
PhoneLayoutInflater(LayoutInflater).inflate(XmlPullParser, ViewGroup, boolean) line: 469
PhoneLayoutInflater(LayoutInflater).inflate(int, ViewGroup, boolean) line: 397
ConversationListAdapter.newView(Context, Cursor, ViewGroup) line: 103
ConversationListAdapter(CursorAdapter).getView(int, View, ViewGroup) line: 250
ListView(AbsListView).obtainView(int, boolean[]) line: 2338
ListView.makeAndAddView(int, int, boolean, int, boolean) line: 1812
ListView.fillDown(int, int) line: 698
ListView.fillFromTop(int) line: 759
ListView.layoutChildren() line: 1631
ListView(AbsListView).onLayout(boolean, int, int, int, int) line: 2149
ConversationListItem实例创建的主要过程分析如下,在getView来获取view的时候,因为缓冲没有数据,会使用newView新建一个view,
public View getView(int position, View convertView, ViewGroup parent) {
View v;
if (convertView == null) {
v = newView(mContext, mCursor, parent);
} else {
v = convertView;
}
bindView(v, mContext, mCursor);
return v;
}
newView实际的过程是inflate一个布局文件,这个布局文件就是我们分析的layout,它是ConversationListItem形式的,我们知道,在inflate创建布局文件时,会根据节点创建view实例,所以会使用类的构造类Constructor来创建ConversationListItem实例。
public View newView(Context context, Cursor cursor, ViewGroup parent) {
if (LOCAL_LOGV) Log.v(TAG, "inflating new view");
return mFactory.inflate(R.layout.conversation_list_item, parent, false);
}
Checkbox的主要作用是用来勾选短信列表的某一个item,并进行删除操作的。
对于checkbox,在ConversationListItem里面定义了一个引用mSelectBox,
private CheckBox mSelectBox;
并在onFinishInflate里面初始化它,关联到Layout里面的select_box,
mSelectBox = (CheckBox) findViewById(R.id.select_box);
当列表项被点击时,执行ConversationList.onListItemClick(ListView, View, int, long)这个方法,在里面添加下面代码,当处于删除模式时,如下,将Conversation设置为check状态,然后更新界面,
if (mListAdapter != null && mDeleteMode) {
if (conv.isChecked()) {
conv.setIsChecked(false);
} else {
conv.setIsChecked(true);
}
mListAdapter.notifyDataSetChanged();
updateSelectView();
return;
}
更新界面时,执行ConversationListItem.bind(Context, Conversation) ,每加载一个item项都会执行一次bind,在这里,会根据mSelectMode 决定checkbox是否显示,根据Conversation的状态决定是否选中。
所以实际上,不是我们主观上看到的那样由checkbox决定了当前项目是否选中,而是当前项选中后决定checkbox的显示状态。
if (mSelectMode) {
mSelectBox.setVisibility(View.VISIBLE);
mAvatarView.setClickable(false);
} else {
mSelectBox.setVisibility(View.GONE);
mAvatarView.setClickable(true);
}
if (mConversation.isChecked()) {
mSelectBox.setChecked(true);
} else {
mSelectBox.setChecked(false);
}
上面就是checkbox在代码里的处理过程。
R.id.avatar用来显示发件人头像,定义如下,MmsQuickContactBadge继承自QuickContactBadge,再往上是ImageView,一个通用的视图组件。
private MmsQuickContactBadge mAvatarView;
其实例获取在ConversationListItem的onFinishInflate里面,
mAvatarView = (MmsQuickContactBadge) findViewById(R.id.avatar);
如果希望AvatarView能够被点击,相应事件,则可以看到在bind有如下代码,表示在删除模式下不能被点击,在列表模式下可以被点击。
if (mSelectMode) {
mAvatarView.setClickable(false);
} else {
mAvatarView.setClickable(true);
}
AvatarView被点击之后,执行的调用栈如下,可见是由View直接调用了MmsQuickContactBadge类的onClick,并不是ConversationList或ConversationListItem里做了onClick的相关处理。
MmsQuickContactBadge(QuickContactBadge).onClick(View) line: 217
MmsQuickContactBadge.onClick(View) line: 75
MmsQuickContactBadge(View).performClick() line: 4212
View$PerformClick.run() line: 17476
Handler.handleCallback(Message) line: 800
ViewRootImpl$ViewRootHandler(Handler).dispatchMessage(Message) line: 100
QuickContactBadge类的onClick代码如下,
@Override
public void onClick(View v) {
if (mContactUri != null) {
QuickContact.showQuickContact(getContext(), QuickContactBadge.this, mContactUri,
QuickContact.MODE_LARGE, mExcludeMimes);
} else if (mContactEmail != null) {
mQueryHandler.startQuery(TOKEN_EMAIL_LOOKUP_AND_TRIGGER, mContactEmail,
Uri.withAppendedPath(Email.CONTENT_LOOKUP_URI, Uri.encode(mContactEmail)),
EMAIL_LOOKUP_PROJECTION, null, null, null);
} else if (mContactPhone != null) {
mQueryHandler.startQuery(TOKEN_PHONE_LOOKUP_AND_TRIGGER, mContactPhone,
Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI, mContactPhone),
PHONE_LOOKUP_PROJECTION, null, null, null);
} else {
// If a contact hasn't been assigned, don't react to click.
return;
}
}
我们当前的代码走mContactPhone路径,mContactPhone是在assignContactFromPhone里面赋值的,assignContactFromPhone则是通过ConversationListItem的updateAvatarView调用的(传入的number就赋值给mContactPhone),
mAvatarView.assignContactFromPhone(number, true);
updateAvatarView可以被updateFromView和bind调用,这两个方法我们在前面有过描述。所以从整个过程来看,我们可以看出avatar被点击和其需要的数据来源。
3.2 绘制分析
3.2.1 Message列表创建
如ConversationList的layout描述,Messag列表主要是由list呈现,而list的item项是由ConversationListItem布局的,我们这里讨论每个item项的创建以及整个list的创建。
在使用Cache时,ConversationListItem的创建过程如下,
ConversationListItem.bind(Context, Conversation) line: 480
ConversationListAdapter.bindView(View, Context, Cursor) line: 77
ConversationListAdapter(CursorAdapter).getView(int, View, ViewGroup) line: 250
ListView(AbsListView).obtainView(int, boolean[]) line: 2186
ListView.makeAndAddView(int, int, boolean, int, boolean) line: 1877
ListView.fillSpecific(int, int) line: 1356
ListView.layoutChildren() line: 1671
ListView(AbsListView).onLayout(boolean, int, int, int, int) line: 2037
ListView(View).layout(int, int, int, int) line: 14118
ListView(ViewGroup).layout(int, int, int, int) line: 4467
RelativeLayout.onLayout(boolean, int, int, int, int) line: 1021
从上往下分析该栈:
1)ConversationListItem.bind
ConversationListItem.bind的代码比较多,仔细分析,实际上逻辑比较简单,主要是设置每个view的内容和状态,比如说设置日期的代码如下,
mDateView.setVisibility(VISIBLE);
if(ConversationList.sConversationListOption ==
ConversationList.OPTION_CONVERSATION_LIST_IMPORTANT &&
!conversation.hasUnreadMessages()) {
mDateView.setText(MessageUtils.formatTimeStampStringExtend(context,
conversation.getImpDate()));
}
其数据来源于conversation,我们要进一步分析conversation是怎么传递进来的,以及为什么要调用bind。
2)ConversationListAdapter
ConversationListAdapter 是ConversationList的layout里的list对应的Adapter,它继承自MessageCursorAdapter-CursorAdapter-baseAdapter, 其方法bindView在本例中执行的主要代码如下,其中headerview是一个ConversationListItem引用,是调用方法传递过来的,conversation是根据游标新建的,最后调用bind,所以该方法的作用就是获取到conversation,并用准备将其中的数据赋值到view。
public void bindView(View view, Context context, Cursor cursor) {
ConversationListItem headerView = (ConversationListItem) view;
Conversation conv;
if (!mIsScrolling) {
Conversation.setNeedCacheConv(false);
conv = Conversation.from(context, cursor);
Conversation.setNeedCacheConv(true);
if (mSubjectSingleLine) {
headerView.setSubjectSingleLineMode(true);
}
if (conv != null) {
conv.setIsChecked(sSelectedTheadsId.contains(conv.getThreadId()));
}
headerView.bind(context, conv);
}
}
(CursorAdapter).getView则负责获取到view实例和游标实例。
view的获取有两种方式,一种是调用者传递过来以前缓存的,一种是新创建一个,在ConversationListItem实例创建中有讲到如何创建。
游标实例则在Adapter创建时创建。
3)ListView
在ListView部分,我们从layoutChildren向上分析,向下部分设计到太多ListView的实现机制问题,暂不讨论。
layoutChildren实现ListView子窗口布局,实际上就是list在layout所占部分。
本例从launcher进入Message列表,执行代码路径如下,mSyncPosition是当前item索引值,mSpecificTop是当前layout相对Top的位置。
layoutChildren:
case LAYOUT_SYNC:
sel = fillSpecific(mSyncPosition, mSpecificTop);
fillSpecific在知道位置绘制指定项的item数据,然后上下绘制其他的item。它先通过makeAndAddView完成当前item的创建并添加到layout,fillDown、fillUp则根据layout大小,上下绘制item完成整个list的绘制,使用的方法也是makeAndAddView,adjustViewsUpOrDown调整item位置。
/**
* Put a specific item at a specific location on the screen and then build
* up and down from there.
*
* @param position The reference view to use as the starting point
* @param top Pixel offset from the top of this view to the top of the
* reference view.
*
* @return The selected view, or null if the selected view is outside the
* visible area.
*/
private View fillSpecific(int position, int top) {
boolean tempIsSelected = position == mSelectedPosition;
View temp = makeAndAddView(position, top, true, mListPadding.left, tempIsSelected);
…
below = fillDown(position + 1, temp.getBottom() + dividerHeight);
// This will correct for the bottom of the last view not touching the bottom of the list
adjustViewsUpOrDown();
above = fillUp(position - 1, temp.getTop() - dividerHeight);
int childCount = getChildCount();
if (childCount > 0) {
correctTooLow(childCount);
}
}
顾名思义,makeAndAddView就是新建一个view,并将它添加到list里面。obtainView负责创建view,setupChild负责将view添加到layout,并设置好view的位置和相关属性。
private View makeAndAddView(int position, int y, boolean flow, int childrenLeft,
boolean selected) {
View child;
child = obtainView(position, mIsScrap);
// This needs to be positioned and measured
setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]);
return child;
}
obtainView将对应位置的view和数据关联起来,并返回view。它通过mRecycler获取到缓存的view,再通过mAdapter.getView将数据填充到view,mAdapter.getView即是我们前面分析到的ConversationListAdapter(CursorAdapter).getView。
至此,Message列表的创建过程就明了了。
3.3 功能分析
3.3.1 Message列表更新
当数据库内容发生变化(包括新短信、删除、发送状态改变等),就会触发onContentChanged,其调用栈如下:
ConversationList$2.onContentChanged(ConversationListAdapter) line: 474
ConversationListAdapter.onContentChanged() line: 118
CursorAdapter$ChangeObserver.onChange(boolean) line: 463
CursorAdapter$ChangeObserver(ContentObserver).onChange(boolean, Uri) line: 129
ContentObserver$NotificationRunnable.run() line: 180
Handler.handleCallback(Message) line: 808
Handler.dispatchMessage(Message) line: 103
Looper.loop() line: 193
从下往上分析,
1)ContentObserver
我们已经很熟悉handleCallback处理的是Post的消息,消息一般是runable,所以可以看到ContentObserver. dispatchChange有post这一消息的过程(dispatchChange的触发过程比较复杂,我们将在其他章节介绍,本例中当数据库内容变化时会触发这个流程)。
public final void dispatchChange(boolean selfChange, Uri uri) {
if (mHandler == null) {
onChange(selfChange, uri);
} else {
mHandler.post(new NotificationRunnable(selfChange, uri));
}
}
然后这个消息会运行其内部类NotificationRunnable的run方法,
public void run() {
ContentObserver.this.onChange(mSelfChange, mUri);
}
This指向的是ContentObserver的实例ChangeObserver,它是CursorAdapter的内部类,也是ContentObserver的子类,由于ConversationListAdapter通过CursorWrapper向ContentObservable注册了ContentObserver,所以当dispatchChange事件时,通过轮询,最终将事件传递给ChangeObserver。
而后,ChangeObserver调用封装类CursorAdapter的onContentChanged,将事件上传。
private class ChangeObserver extends ContentObserver {
public void onChange(boolean selfChange) {
onContentChanged();
}
}
2)onContentChanged
onContentChanged是CursorAdapter的一个方法,主要功能是当数据库内容更新后,重新使用游标查询数据。但这个方法被ConversationListAdapter重载,它给自己的客户提供一个借口类OnContentChangedListener,让客户去实现这个借口类,自己则调用接口类的onContentChanged方法执行客户定义的功能。
protected void onContentChanged() {
if (mCursor != null && !mCursor.isClosed()) {
if (mOnContentChangedListener != null) {
mOnContentChangedListener.onContentChanged(this);
}
}
}
3)ConversationList
ConversationList就是ConversationListAdapter的客户,它实现了接口类,如下,主要是调用startAsyncQuery进行数据的查询。
private final ConversationListAdapter.OnContentChangedListener mContentChangedListener =
new ConversationListAdapter.OnContentChangedListener() {
@Override
public void onContentChanged(ConversationListAdapter adapter) {
if (mIsInActivity) {
mNeedQuery = true;
startAsyncQuery();
}
}
};
并且ConversationList在其onStart向ConversationListAdapter注册了这个接口类监听器:
mListAdapter.setOnContentChangedListener(mContentChangedListener);
所以,当数据库数据改变时,根据上面相关调用流程,完成了Message列表的刷新功能。
BTW,附上游标改变后注册ContentObserver的调用栈:
ContentResolver$CursorWrapperInner(CursorWrapper).registerContentObserver(ContentObserver) line: 178
ConversationListAdapter(CursorAdapter).swapCursor(Cursor) line: 340
ConversationListAdapter(CursorAdapter).changeCursor(Cursor) line: 313
ConversationList$ThreadListQueryHandler.onQueryComplete(int, Object, Cursor) line: 1896
ConversationList$ThreadListQueryHandler(AsyncQueryHandler).handleMessage(Message) line: 344
ConversationList$ThreadListQueryHandler(Handler).dispatchMessage(Message) line: 110
Looper.loop() line: 193
3.3.2 Message列表定位
因为ConversationList是ListActivity,所以可以使用getListView获取ListView,再使用ListView的setSelectionFromTop,将列表显示开始位置指定到特定位置。
需要注意2个问题,
1)
在ConversationList会记录上次浏览列表的起始位置,所以不是随意就能设置好列表的显示位置,如onQueryComplete通过下面代码控制了什么时候可以设置这个起始位置,我们需要根据mSavedFirstVisiblePosition的值和相关逻辑去处理。
if (mSavedFirstVisiblePosition != AdapterView.INVALID_POSITION) {
// Restore the list to its previous position.
getListView().setSelectionFromTop(mSavedFirstVisiblePosition,
mSavedFirstItemOffset);
mSavedFirstVisiblePosition = AdapterView.INVALID_POSITION;
}
结合列表的创建过程,我们知道,当同步绘制列表时,是从mSyncPosition开始的,
fillSpecific(mSyncPosition, mSpecificTop);
而mSyncPosition正是setSelectionFromTop里传递进去的,所以列表起始位置只能通过setSelectionFromTop来设置。
public void setSelectionFromTop(int position, int y) {
…
mSyncPosition = position;
…
}
2)
游标的更新,当数据库数据变化后,需要更新游标,才能获取到正确的值。下面是onQueryComplete完成后,用changeCursor替换掉旧游标的过程,防止游标数据过期。
ConversationListAdapter(CursorAdapter).swapCursor(Cursor) line: 340
ConversationListAdapter(CursorAdapter).changeCursor(Cursor) line: 313
ConversationList$ThreadListQueryHandler.onQueryComplete(int, Object, Cursor) line: 1896
ConversationList$ThreadListQueryHandler(AsyncQueryHandler).handleMessage(Message) line: 344
ConversationList$ThreadListQueryHandler(Handler).dispatchMessage(Message) line: 110
Looper.loop() line: 193
在列表创建时,CursorAdapter. getView的游标最初是在CursorAdapte创建时传入的,之后可以通过swapCursor的方法更新。
目 录
2.1.2.1 ConversationListAdapter
2.2.2 Long Press click to Delete
3.1.2.1 ConversationListItem实例创建
1. 概要
本文主要分析短信列表界面的主要控件的界面显示和处理逻辑。
2. ConversationList
2.1 ConversationList
2.1.1 Layout分析
ConversationList对应的layout是conversation_list_screen,它主要分3部分,第一部分是做全选的控制,第二部分是list列表,第三部分是列表为空的布局。具体的表现形式如下,布局文件就不再展开了。其中list列表将在另外的章节分析。
2.1.2 代码分析
2.1.2.1 ConversationListAdapter
ConversationListAdapter是ConversationListItem的数据适配器,在ConversationList.onCreate里创建,它通过bindView将会话信息绑定到item的layout上。
其创建过程如下,
private void initListAdapter() {
mListAdapter = new ConversationListAdapter(this, null);
/** M: now this code is useless and will lead to a JE, comment it.
* listener is set in onStart
*/
//mListAdapter.setOnContentChangedListener(mContentChangedListener);
setListAdapter(mListAdapter);
getListView().setRecyclerListener(mListAdapter);
}
因为ConversationList继承自ListActivity,setListAdapter将Adapter实例设置给ListActivity,这里会调用ensureList,它会加载一个布局,确保有view能用来对应Adapter。
public void setListAdapter(ListAdapter adapter) {
synchronized (this) {
ensureList();
mAdapter = adapter;
mList.setAdapter(adapter);
}
}
private void ensureList() {
if (mList != null) {
return;
}
setContentView(com.android.internal.R.layout.list_content_simple);
}
而实际上,mList已经在 onContentChanged被初始化为Layout里对应的ListView,
mList = (ListView)findViewById(com.android.internal.R.id.list);
这个ListView就是Message列表的容器了。
2.2 功能分析
2.2.1 列表项click
单击列表项的调用栈如下:
ConversationList.onListItemClick(ListView, View, int, long) line: 1086
ListActivity$2.onItemClick(AdapterView, View, int, long) line: 319
ListView(AdapterView).performItemClick(View, int, long) line: 298
ListView(AbsListView).performItemClick(View, int, long) line: 1128
AbsListView$PerformClick.run() line: 2815
AbsListView$1.run() line: 3574
Handler.handleCallback(Message) line: 800
ViewRootImpl$ViewRootHandler(Handler).dispatchMessage(Message) line: 100
2.2.2 Long Press click to Delete
长按item删除会话流程如下:
长按出现选择菜单,
mConvListOnCreateContextMenuListener
onCreateContextMenu
点击删除
ConversationList.confirmDeleteThreads(Collection, AsyncQueryHandler) line:
ConversationList.confirmDeleteThread(long, AsyncQueryHandler) line: 1320
ConversationList.onContextItemSelected(MenuItem) line: 1247
ConversationList(Activity).onMenuItemSelected(int, MenuItem) line: 2584
PhoneWindow$DialogMenuCallback.onMenuItemSelected(MenuBuilder, MenuItem) line:
开始删除之前,要做一个后台查询,看是否有lock住的消息,Conversation.startQueryHaveLockedMessages,
之后是后台查询handler.startQuery:
AsyncQueryHandler
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);
msg.arg1 = EVENT_ARG_QUERY;
WorkerArgs args = new WorkerArgs();
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);
}
这个查询过程将消息发给mWorkerThreadHandler,这是异步查询AsyncQueryHandler自己的handler,它封装完消息EVENT_ARG_QUERY后,再发送出去,
Message reply = args.handler.obtainMessage(token);
reply.obj = args;
reply.arg1 = msg.arg1;
reply.sendToTarget();
这个消息的接收对象的设置见args.handler = this; ,在本例中,这个handler是ThreadListQueryHandler。
数据库查询完成后执行onQueryComplete(HAVE_LOCKED_MESSAGES_TOKEN),
ConversationList$ThreadListQueryHandler.onQueryComplete(int, Object, Cursor) line: 1610
ConversationList$ThreadListQueryHandler(AsyncQueryHandler).handleMessage(Message) line: 344
ConversationList$ThreadListQueryHandler(Handler).dispatchMessage(Message) line: 107
Looper.loop() line: 194
之后会给出一个确认删除对话框confirmDeleteThreadDialog
在弹出的确认对话框中,选择删除,执行listener函数。
ConversationList$DeleteThreadListener.onClick(DialogInterface, int) line: 1484
AlertController$ButtonHandler.handleMessage(Message) line: 176
之后开始删除Conversation.startDelete
最后onDeleteComplete
响应函数会触发一系列其他的数据库操作,如查询list,查询unread等,
AsyncQueryHandler$WorkerHandler.handleMessage(Message) line: 120
AsyncQueryHandler$WorkerHandler(Handler).dispatchMessage(Message) line: 107
【Tips】
它的threadIds是在点击删除功能时,onContextItemSelected设置的,和长按的时候在onCreateContextMenu里选择的有时间差,不一定是同一个,在会话列表更新的时候,会有影响,所以会误删。
所以在onCreateContextMenu里要记录thread id。
2.3 数据操作
2.3.1 会话ID创建
Message列表是以会话为item进行显示的,会话是以为ID记录的,我们分析一下各种会话ID是如何创建的,
应用在CBMessageReceiverService.java收到小区广播后,使用handleCBMessageReceived方法进行处理,其中insertMessage是用来存储CB的。
insertMessage会调用CbProvider的insert方法
public Uri insert(Uri url, ContentValues initialValues) {
Uri result = null;
// TODO Check Permission
// checkPermission();
SQLiteDatabase db = mOpenHelper.getWritableDatabase();
SQLiteDatabase dbmmssms = mMmsSmsOpenHelper.getWritableDatabase();
int match = URI_MATCHER.match(url);
ContentValues values;
long rowID;
String table = null;
Log.d(TAG, " insert match = "+match);
switch (match) {
case URL_MESSAGES:
//table = CbDatabaseHelper.CBMESSAGE_TABLE;
table = MmsSmsDatabaseHelper.TABLE_CELLBROADCAST;
values = internalInsertMessages(initialValues);
rowID = dbmmssms.insert(table, null, values);
Log.d(TAG, "insert to cellbroadcast " + values);
if (rowID > 0) {
result = Uri.parse("content://messages/" + rowID);
notifyChange();
return result;
}
break;
…
}
在这里,我们重点关注一下CB Message的ThreadId是怎样获取的,在internalInsertMessages里面,显然threadId不是我们主动填充进去的,所以是通过getOrCreateThreadId获得的。
private ContentValues internalInsertMessages(ContentValues initialValues) {
ContentValues values;
if (initialValues != null) {
values = new ContentValues(initialValues);
} else {
values = new ContentValues();
}
Long threadId = values.getAsLong(EncapsulatedTelephony.SmsCb.THREAD_ID);
String address = values.getAsString(EncapsulatedTelephony.SmsCb.CHANNEL_ID);
if (((threadId == null) || (threadId == 0)) && (address != null)) {
values.put(EncapsulatedTelephony.SmsCb.THREAD_ID,
getOrCreateThreadId(address));
}
…
}
getOrCreateThreadId代码如下,它封装一个URI地址,包含了threadID和参数recipient、cellbroadcast,并使用SqliteWrapper.query进行查询,
private String getOrCreateThreadId(String address) {
//Uri.Builder uriBuilder = THREAD_ID_URI.buildUpon();
Uri.Builder uriBuilder = (Uri.parse("content://mms-sms/threadID")).buildUpon();
uriBuilder.appendQueryParameter("recipient", address);
uriBuilder.appendQueryParameter("cellbroadcast", address);
Uri uri = uriBuilder.build();
// TODO need replace with helper interface.
Cursor cursor = null;
long token = Binder.clearCallingIdentity();
try {
cursor = SqliteWrapper.query(this.getContext(), this
.getContext().getContentResolver(), uri, ID_PROJECTION, null,
null, null);
…
if (cursor.moveToFirst()) {
return String.valueOf(cursor.getLong(0));
…
}
SqliteWrapper会调用到ContentResolver,ContentResolver则最终调用到MmsSmsProvider,
在MmsSmsProvider.query里面,也是通过URI match找到操作码URI_THREAD_ID,根据参数cellbroadcast,使用getCBThreadId给CB查找或创建其ThreadId。
Threads表内容如下所示,里面的关键字段有_id ,recipient_ids,要想找到合适的ThreadId,只有通过recipient_ids来查询,
所以在getCBThreadId方法里面,通过getAddressIds从canonical_addresses表里面找到和号码匹配的recipient_ids列表,再到Threads表里面找到recipient_ids相同和type是小区广播的_id,即是我们需要的ThreadId,使用的查询语句如下。
String queryString = "SELECT _id FROM threads " + "WHERE type=" + EncapsulatedTelephony.Threads.CELL_BROADCAST_THREAD + " AND recipient_ids=?";
如果没有满足条件的ThreadId,调用insertThread()创建一个新的thread。
在Message列表界面通过按钮新建短信时,执行createNewMessage(),通过createIntent传递一个threadId=0的ID过去,
private void createNewMessage() {
startActivity(ComposeMessageActivity.createIntent(this, 0));
}
public static Intent createIntent(Context context, long threadId) {
Intent intent = new Intent(context, ComposeMessageActivity.class);
if (threadId > 0) {
intent.setData(Conversation.getUri(threadId));
}
return intent;
}
因为threadId为0,在会话创建中会获取或创建一个threadId,具体过程和下节相同。
在联系人的详情界面,点击发送短信,因为有号码存在,所以在ComposeMessageActivity.Oncreate方法里面会去查询或创建一个threadId。
onCreate的initActivityState里,有这么一段代码是获取会话实例的,mConversation.get通过号码获取到会话实例,
Uri intentData = intent.getData();
/// M: Code analyze 034, If intent is SEND,just create a new empty thread,
/// otherwise Conversation.get() will throw exception.
String action = intent.getAction();
if (intentData != null && (TextUtils.isEmpty(action) ||
!action.equals(Intent.ACTION_SEND))) {
/// M: group-contact send message
// try to get a conversation based on the data URI passed to our intent.
if (intentData.getPathSegments().size() < 2) {
mConversation = mConversation.get(getApplicationContext(),ContactList.getByNumbers(
getStringForMultipleRecipients(Conversation.getRecipients(intentData)),
false /* don't block */, true /* replace number */),false);
} else {
mConversation = Conversation.get(getApplicationContext(), intentData, false);
}
/// @}
mWorkingMessage.setText(getBody(intentData));
Conversation get代码如下,如果号码为空则新建会话,如果缓冲里存储了对应号码的会话,则使用缓冲里的数据,如果条件都不满足,则需要去数据库查询到ID,并新建会话,之后将新会话存放到缓冲。
public static Conversation get(Context context, ContactList recipients, boolean allowQuery) {
if (recipients.size() < 1) {
return createNew(context);
}
Conversation conv = Cache.get(recipients);
if (conv != null) {
return conv;
}
long threadId = getOrCreateThreadId(context, recipients);
conv = new Conversation(context, threadId, allowQuery);
。。。
Cache.put(conv);
}
getOrCreateThreadId用来获取ThreadId,由Threads.getOrCreateThreadId实现,这个方法在Telephony.java里面。
getOrCreateThreadId类似前面提到的小区广播的实现过程,先封装一个"content://mms-sms/threadID"的URI,但只使用一个参数recipient,
在MmsSmsProvider.query里面,也是通过URI match找到操作码URI_THREAD_ID,,使用getThreadId查找或创建其ThreadId,getAddressIds获取到匹配的recipientIds,再取Threads表里查询,如果查询到,返回游标,如果没有记录,则用insertThread向Threads里插入记录,再查询到ThreadId。
private synchronized Cursor getThreadId(List<String> recipients) {
Set<Long> addressIds = getAddressIds(recipients);
。。。
recipientIds = Long.toString(addressId);
。。。
String[] selectionArgs = new String[] { recipientIds };
SQLiteDatabase db = mOpenHelper.getReadableDatabase();
db.beginTransaction();
Cursor cursor = null;
try {
// Find the thread with the given recipients
cursor = db.rawQuery(THREAD_QUERY, selectionArgs);
if (cursor.getCount() == 0) {
// No thread with those recipients exists, so create the thread.
cursor.close();
Log.d(LOG_TAG, "getThreadId: create new thread_id for recipients " + recipients);
insertThread(recipientIds, recipients);
// The thread was just created, now find it and return it.
cursor = db.rawQuery(THREAD_QUERY, selectionArgs);
。。。
}
3. ConversationListItem
3.1 ConversationListItem
3.1.1 Layout分析
如前所述,短信记录的每条信息显示是使用conversation_list_item.xml布局的,在本例中,其直观显示是如下形式的一条短信记录,
conversation_list_item.xml文件如下,它是一个ConversationListItem类型的布局,
(需要注意不同屏幕使用这个文件的位置,本例中在packages\apps\Mms\res\layout-mdpi下面,不是packages\apps\Mms\res\layout)
conversation_list_item.xml
<?xml version="1.0" encoding="utf-8"?>
<com.android.mms.ui.ConversationListItem xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="?android:attr/listPreferredItemHeight"
android:background="?android:attr/activatedBackgroundIndicator" >
<CheckBox
android:id="@+id/select_box"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_centerVertical="true"
android:clickable="false"
android:focusable="false"
android:focusableInTouchMode="false" />
<com.android.mms.ui.MmsQuickContactBadge
android:id="@+id/avatar"
android:visibility="gone"
android:layout_centerVertical="true"
android:layout_toEndOf="@id/select_box"
style="?android:attr/quickContactBadgeStyleWindowLarge" />
<ImageView
android:id="@+id/presence"
android:visibility="invisible"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_alignParentTop="true"
android:src="@drawable/ic_unread_label"/>
<LinearLayout
android:id="@+id/iconlist"
android:layout_alignParentTop="true"
android:layout_alignParentEnd="true"
android:layout_marginTop="7dip"
android:layout_marginEnd="10dip"
android:orientation="horizontal"
android:layout_height="wrap_content"
android:layout_width="wrap_content" >
<ImageView android:id="@+id/draft"
android:visibility="gone"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:src="@drawable/ic_draft" />
<ImageView android:id="@+id/error"
android:paddingStart="6dp"
android:visibility="gone"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:src="@drawable/ic_list_alert_sms_failed" />
<ImageView android:id="@+id/attachment"
android:paddingStart="6dp"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:visibility="gone"
android:src="@drawable/ic_attachment_universal_small" />
<ImageView android:id="@+id/mute"
android:paddingStart="6dp"
android:visibility="gone"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:src="@drawable/ipmsg_silent" />
</LinearLayout>
<RelativeLayout
android:layout_alignParentTop="true"
android:layout_marginTop="4dip"
android:layout_marginStart="12dip"
android:layout_toEndOf="@id/avatar"
android:layout_toStartOf="@id/iconlist"
android:gravity="center_vertical"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView android:id="@+id/unread"
android:background="@drawable/ipmsg_message_box"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:textSize="12sp"
android:textColor="@color/text_color_unread"
android:singleLine="true"
android:layout_alignParentEnd="true" />
<RelativeLayout android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:layout_toStartOf="@id/unread">
<TextView android:id="@+id/draft_and_smgcount"
android:textAppearance="?android:attr/textAppearanceMedium"
android:layout_alignParentEnd="true"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="6dip"
android:singleLine="true" />
<com.android.mms.util.AlwaysMarqueeTextView android:id="@+id/from"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceMedium"
android:singleLine="true"
android:layout_alignParentStart="true"
android:layout_toStartOf="@id/draft_and_smgcount"
android:layout_marginEnd="6dip"
android:ellipsize="marquee" />
</RelativeLayout>
</RelativeLayout>
<TextView android:id="@+id/date"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:textSize= "12sp"
android:textColor="@color/dlg_text_counter_color"
android:layout_alignParentBottom="true"
android:layout_alignParentEnd="true"
android:layout_marginEnd="10dip"
android:layout_marginBottom="4dip"
android:gravity="bottom" />
<TextView android:id="@+id/subject"
android:layout_height="wrap_content"
android:layout_width="fill_parent"
android:textSize= "12sp"
android:layout_alignParentBottom="true"
android:layout_toEndOf="@id/avatar"
android:layout_toStartOf="@id/date"
android:layout_marginStart="12dip"
android:layout_marginBottom="4dip"
android:gravity="bottom"
android:textColor="@color/dlg_text_counter_color"
android:lines="2"
android:ellipsize="end" />
</com.android.mms.ui.ConversationListItem>
其对应的布局图形如下,图中左边是layout组成,右上是layout布局表现,右下是对layout里面的子view编号,便于理解。
分析xml文件,并对照图例可以看出,
ConversationListItem是整个item布局类,表现形式见下图左边,它指定了高度
android:layout_height="?android:attr/listPreferredItemHeight"
这个布局由6个同级view组成,见下图中间及右上,其中
checkbox供选择使用,见右下数字0,alignParentStart表示它在开头位置,
android:layout_alignParentStart="true"
MmsQuickContactBadge做头像显示,见右下数字1,
ImageView做未读标识等用途,见右下数字2,通过下面的属性将它放置在最右上的位置
android:layout_alignParentEnd="true"
android:layout_alignParentTop="true"
LinearLayout是个组合视图,见右下数字3和iconlist小图,它有四个子ImageView表示短信的信息状态,具体名字见iconlist小图。注意四个子ImageView表示同时最多只能显示四个图标,不是表示短信只有这四种状态。通过下面属性,将它放置在MmsQuickContactBadge后面。
android:orientation="horizontal"
<iconlist>
RelativeLayout是个组合视图,见右下数字4和Text Title小图,包含3个TextView,用来显示Title和消息数目、未读消息数目。在RelativeLayout,通过下面的属性设置,将RelativeLayout(index4的view)和上面的LinearLayout(index3的view)并行放置在同一行。
android:layout_toEndOf="@id/avatar"
android:layout_toStartOf="@id/iconlist"
TextView显示日期,见右下数字5,用下面2个属性表示它的位置在右下,
android:layout_alignParentBottom="true"
android:layout_alignParentEnd="true"
TextView 显示短线截取的部分内容,见右下数字6,用下面3个属性表明了它的位置。
android:layout_alignParentBottom="true"
android:layout_toEndOf="@id/avatar"
android:layout_toStartOf="@id/date"
<Text Content>
3.1.2 代码分析
3.1.2.1 ConversationListItem实例创建
ConversationListItem实例创建栈如下,因为有缓存使用,不一定每次进入列表都会创建这个类实例,所以不一定能调试到,要在此设置断点调试,有一个小技巧,就是将mms进程终止掉,让其自动重启(可通过发送短信或cb的方法加快重启过程),这样就可以进行调试。
ConversationListItem.<init>(Context, AttributeSet) line: 130
Constructor.constructNative(Object[], Class, Class[], int, boolean) line: not available [native method]
Constructor.newInstance(Object...) line: 423
PhoneLayoutInflater(LayoutInflater).createView(String, String, AttributeSet) line: 594
PhoneLayoutInflater(LayoutInflater).createViewFromTag(View, String, AttributeSet) line: 696
PhoneLayoutInflater(LayoutInflater).inflate(XmlPullParser, ViewGroup, boolean) line: 469
PhoneLayoutInflater(LayoutInflater).inflate(int, ViewGroup, boolean) line: 397
ConversationListAdapter.newView(Context, Cursor, ViewGroup) line: 103
ConversationListAdapter(CursorAdapter).getView(int, View, ViewGroup) line: 250
ListView(AbsListView).obtainView(int, boolean[]) line: 2338
ListView.makeAndAddView(int, int, boolean, int, boolean) line: 1812
ListView.fillDown(int, int) line: 698
ListView.fillFromTop(int) line: 759
ListView.layoutChildren() line: 1631
ListView(AbsListView).onLayout(boolean, int, int, int, int) line: 2149
ConversationListItem实例创建的主要过程分析如下,在getView来获取view的时候,因为缓冲没有数据,会使用newView新建一个view,
public View getView(int position, View convertView, ViewGroup parent) {
View v;
if (convertView == null) {
v = newView(mContext, mCursor, parent);
} else {
v = convertView;
}
bindView(v, mContext, mCursor);
return v;
}
newView实际的过程是inflate一个布局文件,这个布局文件就是我们分析的layout,它是ConversationListItem形式的,我们知道,在inflate创建布局文件时,会根据节点创建view实例,所以会使用类的构造类Constructor来创建ConversationListItem实例。
public View newView(Context context, Cursor cursor, ViewGroup parent) {
if (LOCAL_LOGV) Log.v(TAG, "inflating new view");
return mFactory.inflate(R.layout.conversation_list_item, parent, false);
}
Checkbox的主要作用是用来勾选短信列表的某一个item,并进行删除操作的。
对于checkbox,在ConversationListItem里面定义了一个引用mSelectBox,
private CheckBox mSelectBox;
并在onFinishInflate里面初始化它,关联到Layout里面的select_box,
mSelectBox = (CheckBox) findViewById(R.id.select_box);
当列表项被点击时,执行ConversationList.onListItemClick(ListView, View, int, long)这个方法,在里面添加下面代码,当处于删除模式时,如下,将Conversation设置为check状态,然后更新界面,
if (mListAdapter != null && mDeleteMode) {
if (conv.isChecked()) {
conv.setIsChecked(false);
} else {
conv.setIsChecked(true);
}
mListAdapter.notifyDataSetChanged();
updateSelectView();
return;
}
更新界面时,执行ConversationListItem.bind(Context, Conversation) ,每加载一个item项都会执行一次bind,在这里,会根据mSelectMode 决定checkbox是否显示,根据Conversation的状态决定是否选中。
所以实际上,不是我们主观上看到的那样由checkbox决定了当前项目是否选中,而是当前项选中后决定checkbox的显示状态。
if (mSelectMode) {
mSelectBox.setVisibility(View.VISIBLE);
mAvatarView.setClickable(false);
} else {
mSelectBox.setVisibility(View.GONE);
mAvatarView.setClickable(true);
}
if (mConversation.isChecked()) {
mSelectBox.setChecked(true);
} else {
mSelectBox.setChecked(false);
}
上面就是checkbox在代码里的处理过程。
R.id.avatar用来显示发件人头像,定义如下,MmsQuickContactBadge继承自QuickContactBadge,再往上是ImageView,一个通用的视图组件。
private MmsQuickContactBadge mAvatarView;
其实例获取在ConversationListItem的onFinishInflate里面,
mAvatarView = (MmsQuickContactBadge) findViewById(R.id.avatar);
如果希望AvatarView能够被点击,相应事件,则可以看到在bind有如下代码,表示在删除模式下不能被点击,在列表模式下可以被点击。
if (mSelectMode) {
mAvatarView.setClickable(false);
} else {
mAvatarView.setClickable(true);
}
AvatarView被点击之后,执行的调用栈如下,可见是由View直接调用了MmsQuickContactBadge类的onClick,并不是ConversationList或ConversationListItem里做了onClick的相关处理。
MmsQuickContactBadge(QuickContactBadge).onClick(View) line: 217
MmsQuickContactBadge.onClick(View) line: 75
MmsQuickContactBadge(View).performClick() line: 4212
View$PerformClick.run() line: 17476
Handler.handleCallback(Message) line: 800
ViewRootImpl$ViewRootHandler(Handler).dispatchMessage(Message) line: 100
QuickContactBadge类的onClick代码如下,
@Override
public void onClick(View v) {
if (mContactUri != null) {
QuickContact.showQuickContact(getContext(), QuickContactBadge.this, mContactUri,
QuickContact.MODE_LARGE, mExcludeMimes);
} else if (mContactEmail != null) {
mQueryHandler.startQuery(TOKEN_EMAIL_LOOKUP_AND_TRIGGER, mContactEmail,
Uri.withAppendedPath(Email.CONTENT_LOOKUP_URI, Uri.encode(mContactEmail)),
EMAIL_LOOKUP_PROJECTION, null, null, null);
} else if (mContactPhone != null) {
mQueryHandler.startQuery(TOKEN_PHONE_LOOKUP_AND_TRIGGER, mContactPhone,
Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI, mContactPhone),
PHONE_LOOKUP_PROJECTION, null, null, null);
} else {
// If a contact hasn't been assigned, don't react to click.
return;
}
}
我们当前的代码走mContactPhone路径,mContactPhone是在assignContactFromPhone里面赋值的,assignContactFromPhone则是通过ConversationListItem的updateAvatarView调用的(传入的number就赋值给mContactPhone),
mAvatarView.assignContactFromPhone(number, true);
updateAvatarView可以被updateFromView和bind调用,这两个方法我们在前面有过描述。所以从整个过程来看,我们可以看出avatar被点击和其需要的数据来源。
3.2 绘制分析
3.2.1 Message列表创建
如ConversationList的layout描述,Messag列表主要是由list呈现,而list的item项是由ConversationListItem布局的,我们这里讨论每个item项的创建以及整个list的创建。
在使用Cache时,ConversationListItem的创建过程如下,
ConversationListItem.bind(Context, Conversation) line: 480
ConversationListAdapter.bindView(View, Context, Cursor) line: 77
ConversationListAdapter(CursorAdapter).getView(int, View, ViewGroup) line: 250
ListView(AbsListView).obtainView(int, boolean[]) line: 2186
ListView.makeAndAddView(int, int, boolean, int, boolean) line: 1877
ListView.fillSpecific(int, int) line: 1356
ListView.layoutChildren() line: 1671
ListView(AbsListView).onLayout(boolean, int, int, int, int) line: 2037
ListView(View).layout(int, int, int, int) line: 14118
ListView(ViewGroup).layout(int, int, int, int) line: 4467
RelativeLayout.onLayout(boolean, int, int, int, int) line: 1021
从上往下分析该栈:
1)ConversationListItem.bind
ConversationListItem.bind的代码比较多,仔细分析,实际上逻辑比较简单,主要是设置每个view的内容和状态,比如说设置日期的代码如下,
mDateView.setVisibility(VISIBLE);
if(ConversationList.sConversationListOption ==
ConversationList.OPTION_CONVERSATION_LIST_IMPORTANT &&
!conversation.hasUnreadMessages()) {
mDateView.setText(MessageUtils.formatTimeStampStringExtend(context,
conversation.getImpDate()));
}
其数据来源于conversation,我们要进一步分析conversation是怎么传递进来的,以及为什么要调用bind。
2)ConversationListAdapter
ConversationListAdapter 是ConversationList的layout里的list对应的Adapter,它继承自MessageCursorAdapter-CursorAdapter-baseAdapter, 其方法bindView在本例中执行的主要代码如下,其中headerview是一个ConversationListItem引用,是调用方法传递过来的,conversation是根据游标新建的,最后调用bind,所以该方法的作用就是获取到conversation,并用准备将其中的数据赋值到view。
public void bindView(View view, Context context, Cursor cursor) {
ConversationListItem headerView = (ConversationListItem) view;
Conversation conv;
if (!mIsScrolling) {
Conversation.setNeedCacheConv(false);
conv = Conversation.from(context, cursor);
Conversation.setNeedCacheConv(true);
if (mSubjectSingleLine) {
headerView.setSubjectSingleLineMode(true);
}
if (conv != null) {
conv.setIsChecked(sSelectedTheadsId.contains(conv.getThreadId()));
}
headerView.bind(context, conv);
}
}
(CursorAdapter).getView则负责获取到view实例和游标实例。
view的获取有两种方式,一种是调用者传递过来以前缓存的,一种是新创建一个,在ConversationListItem实例创建中有讲到如何创建。
游标实例则在Adapter创建时创建。
3)ListView
在ListView部分,我们从layoutChildren向上分析,向下部分设计到太多ListView的实现机制问题,暂不讨论。
layoutChildren实现ListView子窗口布局,实际上就是list在layout所占部分。
本例从launcher进入Message列表,执行代码路径如下,mSyncPosition是当前item索引值,mSpecificTop是当前layout相对Top的位置。
layoutChildren:
case LAYOUT_SYNC:
sel = fillSpecific(mSyncPosition, mSpecificTop);
fillSpecific在知道位置绘制指定项的item数据,然后上下绘制其他的item。它先通过makeAndAddView完成当前item的创建并添加到layout,fillDown、fillUp则根据layout大小,上下绘制item完成整个list的绘制,使用的方法也是makeAndAddView,adjustViewsUpOrDown调整item位置。
/**
* Put a specific item at a specific location on the screen and then build
* up and down from there.
*
* @param position The reference view to use as the starting point
* @param top Pixel offset from the top of this view to the top of the
* reference view.
*
* @return The selected view, or null if the selected view is outside the
* visible area.
*/
private View fillSpecific(int position, int top) {
boolean tempIsSelected = position == mSelectedPosition;
View temp = makeAndAddView(position, top, true, mListPadding.left, tempIsSelected);
…
below = fillDown(position + 1, temp.getBottom() + dividerHeight);
// This will correct for the bottom of the last view not touching the bottom of the list
adjustViewsUpOrDown();
above = fillUp(position - 1, temp.getTop() - dividerHeight);
int childCount = getChildCount();
if (childCount > 0) {
correctTooLow(childCount);
}
}
顾名思义,makeAndAddView就是新建一个view,并将它添加到list里面。obtainView负责创建view,setupChild负责将view添加到layout,并设置好view的位置和相关属性。
private View makeAndAddView(int position, int y, boolean flow, int childrenLeft,
boolean selected) {
View child;
child = obtainView(position, mIsScrap);
// This needs to be positioned and measured
setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]);
return child;
}
obtainView将对应位置的view和数据关联起来,并返回view。它通过mRecycler获取到缓存的view,再通过mAdapter.getView将数据填充到view,mAdapter.getView即是我们前面分析到的ConversationListAdapter(CursorAdapter).getView。
至此,Message列表的创建过程就明了了。
3.3 功能分析
3.3.1 Message列表更新
当数据库内容发生变化(包括新短信、删除、发送状态改变等),就会触发onContentChanged,其调用栈如下:
ConversationList$2.onContentChanged(ConversationListAdapter) line: 474
ConversationListAdapter.onContentChanged() line: 118
CursorAdapter$ChangeObserver.onChange(boolean) line: 463
CursorAdapter$ChangeObserver(ContentObserver).onChange(boolean, Uri) line: 129
ContentObserver$NotificationRunnable.run() line: 180
Handler.handleCallback(Message) line: 808
Handler.dispatchMessage(Message) line: 103
Looper.loop() line: 193
从下往上分析,
1)ContentObserver
我们已经很熟悉handleCallback处理的是Post的消息,消息一般是runable,所以可以看到ContentObserver. dispatchChange有post这一消息的过程(dispatchChange的触发过程比较复杂,我们将在其他章节介绍,本例中当数据库内容变化时会触发这个流程)。
public final void dispatchChange(boolean selfChange, Uri uri) {
if (mHandler == null) {
onChange(selfChange, uri);
} else {
mHandler.post(new NotificationRunnable(selfChange, uri));
}
}
然后这个消息会运行其内部类NotificationRunnable的run方法,
public void run() {
ContentObserver.this.onChange(mSelfChange, mUri);
}
This指向的是ContentObserver的实例ChangeObserver,它是CursorAdapter的内部类,也是ContentObserver的子类,由于ConversationListAdapter通过CursorWrapper向ContentObservable注册了ContentObserver,所以当dispatchChange事件时,通过轮询,最终将事件传递给ChangeObserver。
而后,ChangeObserver调用封装类CursorAdapter的onContentChanged,将事件上传。
private class ChangeObserver extends ContentObserver {
public void onChange(boolean selfChange) {
onContentChanged();
}
}
2)onContentChanged
onContentChanged是CursorAdapter的一个方法,主要功能是当数据库内容更新后,重新使用游标查询数据。但这个方法被ConversationListAdapter重载,它给自己的客户提供一个借口类OnContentChangedListener,让客户去实现这个借口类,自己则调用接口类的onContentChanged方法执行客户定义的功能。
protected void onContentChanged() {
if (mCursor != null && !mCursor.isClosed()) {
if (mOnContentChangedListener != null) {
mOnContentChangedListener.onContentChanged(this);
}
}
}
3)ConversationList
ConversationList就是ConversationListAdapter的客户,它实现了接口类,如下,主要是调用startAsyncQuery进行数据的查询。
private final ConversationListAdapter.OnContentChangedListener mContentChangedListener =
new ConversationListAdapter.OnContentChangedListener() {
@Override
public void onContentChanged(ConversationListAdapter adapter) {
if (mIsInActivity) {
mNeedQuery = true;
startAsyncQuery();
}
}
};
并且ConversationList在其onStart向ConversationListAdapter注册了这个接口类监听器:
mListAdapter.setOnContentChangedListener(mContentChangedListener);
所以,当数据库数据改变时,根据上面相关调用流程,完成了Message列表的刷新功能。
BTW,附上游标改变后注册ContentObserver的调用栈:
ContentResolver$CursorWrapperInner(CursorWrapper).registerContentObserver(ContentObserver) line: 178
ConversationListAdapter(CursorAdapter).swapCursor(Cursor) line: 340
ConversationListAdapter(CursorAdapter).changeCursor(Cursor) line: 313
ConversationList$ThreadListQueryHandler.onQueryComplete(int, Object, Cursor) line: 1896
ConversationList$ThreadListQueryHandler(AsyncQueryHandler).handleMessage(Message) line: 344
ConversationList$ThreadListQueryHandler(Handler).dispatchMessage(Message) line: 110
Looper.loop() line: 193
3.3.2 Message列表定位
因为ConversationList是ListActivity,所以可以使用getListView获取ListView,再使用ListView的setSelectionFromTop,将列表显示开始位置指定到特定位置。
需要注意2个问题,
1)
在ConversationList会记录上次浏览列表的起始位置,所以不是随意就能设置好列表的显示位置,如onQueryComplete通过下面代码控制了什么时候可以设置这个起始位置,我们需要根据mSavedFirstVisiblePosition的值和相关逻辑去处理。
if (mSavedFirstVisiblePosition != AdapterView.INVALID_POSITION) {
// Restore the list to its previous position.
getListView().setSelectionFromTop(mSavedFirstVisiblePosition,
mSavedFirstItemOffset);
mSavedFirstVisiblePosition = AdapterView.INVALID_POSITION;
}
结合列表的创建过程,我们知道,当同步绘制列表时,是从mSyncPosition开始的,
fillSpecific(mSyncPosition, mSpecificTop);
而mSyncPosition正是setSelectionFromTop里传递进去的,所以列表起始位置只能通过setSelectionFromTop来设置。
public void setSelectionFromTop(int position, int y) {
…
mSyncPosition = position;
…
}
2)
游标的更新,当数据库数据变化后,需要更新游标,才能获取到正确的值。下面是onQueryComplete完成后,用changeCursor替换掉旧游标的过程,防止游标数据过期。
ConversationListAdapter(CursorAdapter).swapCursor(Cursor) line: 340
ConversationListAdapter(CursorAdapter).changeCursor(Cursor) line: 313
ConversationList$ThreadListQueryHandler.onQueryComplete(int, Object, Cursor) line: 1896
ConversationList$ThreadListQueryHandler(AsyncQueryHandler).handleMessage(Message) line: 344
ConversationList$ThreadListQueryHandler(Handler).dispatchMessage(Message) line: 110
Looper.loop() line: 193
在列表创建时,CursorAdapter. getView的游标最初是在CursorAdapte创建时传入的,之后可以通过swapCursor的方法更新。
目 录
2.1.2.1 ConversationListAdapter
2.2.2 Long Press click to Delete
3.1.2.1 ConversationListItem实例创建
1. 概要
本文主要分析短信列表界面的主要控件的界面显示和处理逻辑。
2. ConversationList
2.1 ConversationList
2.1.1 Layout分析
ConversationList对应的layout是conversation_list_screen,它主要分3部分,第一部分是做全选的控制,第二部分是list列表,第三部分是列表为空的布局。具体的表现形式如下,布局文件就不再展开了。其中list列表将在另外的章节分析。
2.1.2 代码分析
2.1.2.1 ConversationListAdapter
ConversationListAdapter是ConversationListItem的数据适配器,在ConversationList.onCreate里创建,它通过bindView将会话信息绑定到item的layout上。
其创建过程如下,
private void initListAdapter() {
mListAdapter = new ConversationListAdapter(this, null);
/** M: now this code is useless and will lead to a JE, comment it.
* listener is set in onStart
*/
//mListAdapter.setOnContentChangedListener(mContentChangedListener);
setListAdapter(mListAdapter);
getListView().setRecyclerListener(mListAdapter);
}
因为ConversationList继承自ListActivity,setListAdapter将Adapter实例设置给ListActivity,这里会调用ensureList,它会加载一个布局,确保有view能用来对应Adapter。
public void setListAdapter(ListAdapter adapter) {
synchronized (this) {
ensureList();
mAdapter = adapter;
mList.setAdapter(adapter);
}
}
private void ensureList() {
if (mList != null) {
return;
}
setContentView(com.android.internal.R.layout.list_content_simple);
}
而实际上,mList已经在 onContentChanged被初始化为Layout里对应的ListView,
mList = (ListView)findViewById(com.android.internal.R.id.list);
这个ListView就是Message列表的容器了。
2.2 功能分析
2.2.1 列表项click
单击列表项的调用栈如下:
ConversationList.onListItemClick(ListView, View, int, long) line: 1086
ListActivity$2.onItemClick(AdapterView, View, int, long) line: 319
ListView(AdapterView).performItemClick(View, int, long) line: 298
ListView(AbsListView).performItemClick(View, int, long) line: 1128
AbsListView$PerformClick.run() line: 2815
AbsListView$1.run() line: 3574
Handler.handleCallback(Message) line: 800
ViewRootImpl$ViewRootHandler(Handler).dispatchMessage(Message) line: 100
2.2.2 Long Press click to Delete
长按item删除会话流程如下:
长按出现选择菜单,
mConvListOnCreateContextMenuListener
onCreateContextMenu
点击删除
ConversationList.confirmDeleteThreads(Collection, AsyncQueryHandler) line:
ConversationList.confirmDeleteThread(long, AsyncQueryHandler) line: 1320
ConversationList.onContextItemSelected(MenuItem) line: 1247
ConversationList(Activity).onMenuItemSelected(int, MenuItem) line: 2584
PhoneWindow$DialogMenuCallback.onMenuItemSelected(MenuBuilder, MenuItem) line:
开始删除之前,要做一个后台查询,看是否有lock住的消息,Conversation.startQueryHaveLockedMessages,
之后是后台查询handler.startQuery:
AsyncQueryHandler
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);
msg.arg1 = EVENT_ARG_QUERY;
WorkerArgs args = new WorkerArgs();
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);
}
这个查询过程将消息发给mWorkerThreadHandler,这是异步查询AsyncQueryHandler自己的handler,它封装完消息EVENT_ARG_QUERY后,再发送出去,
Message reply = args.handler.obtainMessage(token);
reply.obj = args;
reply.arg1 = msg.arg1;
reply.sendToTarget();
这个消息的接收对象的设置见args.handler = this; ,在本例中,这个handler是ThreadListQueryHandler。
数据库查询完成后执行onQueryComplete(HAVE_LOCKED_MESSAGES_TOKEN),
ConversationList$ThreadListQueryHandler.onQueryComplete(int, Object, Cursor) line: 1610
ConversationList$ThreadListQueryHandler(AsyncQueryHandler).handleMessage(Message) line: 344
ConversationList$ThreadListQueryHandler(Handler).dispatchMessage(Message) line: 107
Looper.loop() line: 194
之后会给出一个确认删除对话框confirmDeleteThreadDialog
在弹出的确认对话框中,选择删除,执行listener函数。
ConversationList$DeleteThreadListener.onClick(DialogInterface, int) line: 1484
AlertController$ButtonHandler.handleMessage(Message) line: 176
之后开始删除Conversation.startDelete
最后onDeleteComplete
响应函数会触发一系列其他的数据库操作,如查询list,查询unread等,
AsyncQueryHandler$WorkerHandler.handleMessage(Message) line: 120
AsyncQueryHandler$WorkerHandler(Handler).dispatchMessage(Message) line: 107
【Tips】
它的threadIds是在点击删除功能时,onContextItemSelected设置的,和长按的时候在onCreateContextMenu里选择的有时间差,不一定是同一个,在会话列表更新的时候,会有影响,所以会误删。
所以在onCreateContextMenu里要记录thread id。
2.3 数据操作
2.3.1 会话ID创建
Message列表是以会话为item进行显示的,会话是以为ID记录的,我们分析一下各种会话ID是如何创建的,
应用在CBMessageReceiverService.java收到小区广播后,使用handleCBMessageReceived方法进行处理,其中insertMessage是用来存储CB的。
insertMessage会调用CbProvider的insert方法
public Uri insert(Uri url, ContentValues initialValues) {
Uri result = null;
// TODO Check Permission
// checkPermission();
SQLiteDatabase db = mOpenHelper.getWritableDatabase();
SQLiteDatabase dbmmssms = mMmsSmsOpenHelper.getWritableDatabase();
int match = URI_MATCHER.match(url);
ContentValues values;
long rowID;
String table = null;
Log.d(TAG, " insert match = "+match);
switch (match) {
case URL_MESSAGES:
//table = CbDatabaseHelper.CBMESSAGE_TABLE;
table = MmsSmsDatabaseHelper.TABLE_CELLBROADCAST;
values = internalInsertMessages(initialValues);
rowID = dbmmssms.insert(table, null, values);
Log.d(TAG, "insert to cellbroadcast " + values);
if (rowID > 0) {
result = Uri.parse("content://messages/" + rowID);
notifyChange();
return result;
}
break;
…
}
在这里,我们重点关注一下CB Message的ThreadId是怎样获取的,在internalInsertMessages里面,显然threadId不是我们主动填充进去的,所以是通过getOrCreateThreadId获得的。
private ContentValues internalInsertMessages(ContentValues initialValues) {
ContentValues values;
if (initialValues != null) {
values = new ContentValues(initialValues);
} else {
values = new ContentValues();
}
Long threadId = values.getAsLong(EncapsulatedTelephony.SmsCb.THREAD_ID);
String address = values.getAsString(EncapsulatedTelephony.SmsCb.CHANNEL_ID);
if (((threadId == null) || (threadId == 0)) && (address != null)) {
values.put(EncapsulatedTelephony.SmsCb.THREAD_ID,
getOrCreateThreadId(address));
}
…
}
getOrCreateThreadId代码如下,它封装一个URI地址,包含了threadID和参数recipient、cellbroadcast,并使用SqliteWrapper.query进行查询,
private String getOrCreateThreadId(String address) {
//Uri.Builder uriBuilder = THREAD_ID_URI.buildUpon();
Uri.Builder uriBuilder = (Uri.parse("content://mms-sms/threadID")).buildUpon();
uriBuilder.appendQueryParameter("recipient", address);
uriBuilder.appendQueryParameter("cellbroadcast", address);
Uri uri = uriBuilder.build();
// TODO need replace with helper interface.
Cursor cursor = null;
long token = Binder.clearCallingIdentity();
try {
cursor = SqliteWrapper.query(this.getContext(), this
.getContext().getContentResolver(), uri, ID_PROJECTION, null,
null, null);
…
if (cursor.moveToFirst()) {
return String.valueOf(cursor.getLong(0));
…
}
SqliteWrapper会调用到ContentResolver,ContentResolver则最终调用到MmsSmsProvider,
在MmsSmsProvider.query里面,也是通过URI match找到操作码URI_THREAD_ID,根据参数cellbroadcast,使用getCBThreadId给CB查找或创建其ThreadId。
Threads表内容如下所示,里面的关键字段有_id ,recipient_ids,要想找到合适的ThreadId,只有通过recipient_ids来查询,
所以在getCBThreadId方法里面,通过getAddressIds从canonical_addresses表里面找到和号码匹配的recipient_ids列表,再到Threads表里面找到recipient_ids相同和type是小区广播的_id,即是我们需要的ThreadId,使用的查询语句如下。
String queryString = "SELECT _id FROM threads " + "WHERE type=" + EncapsulatedTelephony.Threads.CELL_BROADCAST_THREAD + " AND recipient_ids=?";
如果没有满足条件的ThreadId,调用insertThread()创建一个新的thread。
在Message列表界面通过按钮新建短信时,执行createNewMessage(),通过createIntent传递一个threadId=0的ID过去,
private void createNewMessage() {
startActivity(ComposeMessageActivity.createIntent(this, 0));
}
public static Intent createIntent(Context context, long threadId) {
Intent intent = new Intent(context, ComposeMessageActivity.class);
if (threadId > 0) {
intent.setData(Conversation.getUri(threadId));
}
return intent;
}
因为threadId为0,在会话创建中会获取或创建一个threadId,具体过程和下节相同。
在联系人的详情界面,点击发送短信,因为有号码存在,所以在ComposeMessageActivity.Oncreate方法里面会去查询或创建一个threadId。
onCreate的initActivityState里,有这么一段代码是获取会话实例的,mConversation.get通过号码获取到会话实例,
Uri intentData = intent.getData();
/// M: Code analyze 034, If intent is SEND,just create a new empty thread,
/// otherwise Conversation.get() will throw exception.
String action = intent.getAction();
if (intentData != null && (TextUtils.isEmpty(action) ||
!action.equals(Intent.ACTION_SEND))) {
/// M: group-contact send message
// try to get a conversation based on the data URI passed to our intent.
if (intentData.getPathSegments().size() < 2) {
mConversation = mConversation.get(getApplicationContext(),ContactList.getByNumbers(
getStringForMultipleRecipients(Conversation.getRecipients(intentData)),
false /* don't block */, true /* replace number */),false);
} else {
mConversation = Conversation.get(getApplicationContext(), intentData, false);
}
/// @}
mWorkingMessage.setText(getBody(intentData));
Conversation get代码如下,如果号码为空则新建会话,如果缓冲里存储了对应号码的会话,则使用缓冲里的数据,如果条件都不满足,则需要去数据库查询到ID,并新建会话,之后将新会话存放到缓冲。
public static Conversation get(Context context, ContactList recipients, boolean allowQuery) {
if (recipients.size() < 1) {
return createNew(context);
}
Conversation conv = Cache.get(recipients);
if (conv != null) {
return conv;
}
long threadId = getOrCreateThreadId(context, recipients);
conv = new Conversation(context, threadId, allowQuery);
。。。
Cache.put(conv);
}
getOrCreateThreadId用来获取ThreadId,由Threads.getOrCreateThreadId实现,这个方法在Telephony.java里面。
getOrCreateThreadId类似前面提到的小区广播的实现过程,先封装一个"content://mms-sms/threadID"的URI,但只使用一个参数recipient,
在MmsSmsProvider.query里面,也是通过URI match找到操作码URI_THREAD_ID,,使用getThreadId查找或创建其ThreadId,getAddressIds获取到匹配的recipientIds,再取Threads表里查询,如果查询到,返回游标,如果没有记录,则用insertThread向Threads里插入记录,再查询到ThreadId。
private synchronized Cursor getThreadId(List<String> recipients) {
Set<Long> addressIds = getAddressIds(recipients);
。。。
recipientIds = Long.toString(addressId);
。。。
String[] selectionArgs = new String[] { recipientIds };
SQLiteDatabase db = mOpenHelper.getReadableDatabase();
db.beginTransaction();
Cursor cursor = null;
try {
// Find the thread with the given recipients
cursor = db.rawQuery(THREAD_QUERY, selectionArgs);
if (cursor.getCount() == 0) {
// No thread with those recipients exists, so create the thread.
cursor.close();
Log.d(LOG_TAG, "getThreadId: create new thread_id for recipients " + recipients);
insertThread(recipientIds, recipients);
// The thread was just created, now find it and return it.
cursor = db.rawQuery(THREAD_QUERY, selectionArgs);
。。。
}
3. ConversationListItem
3.1 ConversationListItem
3.1.1 Layout分析
如前所述,短信记录的每条信息显示是使用conversation_list_item.xml布局的,在本例中,其直观显示是如下形式的一条短信记录,
conversation_list_item.xml文件如下,它是一个ConversationListItem类型的布局,
(需要注意不同屏幕使用这个文件的位置,本例中在packages\apps\Mms\res\layout-mdpi下面,不是packages\apps\Mms\res\layout)
conversation_list_item.xml
<?xml version="1.0" encoding="utf-8"?>
<com.android.mms.ui.ConversationListItem xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="?android:attr/listPreferredItemHeight"
android:background="?android:attr/activatedBackgroundIndicator" >
<CheckBox
android:id="@+id/select_box"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_centerVertical="true"
android:clickable="false"
android:focusable="false"
android:focusableInTouchMode="false" />
<com.android.mms.ui.MmsQuickContactBadge
android:id="@+id/avatar"
android:visibility="gone"
android:layout_centerVertical="true"
android:layout_toEndOf="@id/select_box"
style="?android:attr/quickContactBadgeStyleWindowLarge" />
<ImageView
android:id="@+id/presence"
android:visibility="invisible"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_alignParentTop="true"
android:src="@drawable/ic_unread_label"/>
<LinearLayout
android:id="@+id/iconlist"
android:layout_alignParentTop="true"
android:layout_alignParentEnd="true"
android:layout_marginTop="7dip"
android:layout_marginEnd="10dip"
android:orientation="horizontal"
android:layout_height="wrap_content"
android:layout_width="wrap_content" >
<ImageView android:id="@+id/draft"
android:visibility="gone"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:src="@drawable/ic_draft" />
<ImageView android:id="@+id/error"
android:paddingStart="6dp"
android:visibility="gone"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:src="@drawable/ic_list_alert_sms_failed" />
<ImageView android:id="@+id/attachment"
android:paddingStart="6dp"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:visibility="gone"
android:src="@drawable/ic_attachment_universal_small" />
<ImageView android:id="@+id/mute"
android:paddingStart="6dp"
android:visibility="gone"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:src="@drawable/ipmsg_silent" />
</LinearLayout>
<RelativeLayout
android:layout_alignParentTop="true"
android:layout_marginTop="4dip"
android:layout_marginStart="12dip"
android:layout_toEndOf="@id/avatar"
android:layout_toStartOf="@id/iconlist"
android:gravity="center_vertical"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView android:id="@+id/unread"
android:background="@drawable/ipmsg_message_box"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:textSize="12sp"
android:textColor="@color/text_color_unread"
android:singleLine="true"
android:layout_alignParentEnd="true" />
<RelativeLayout android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:layout_toStartOf="@id/unread">
<TextView android:id="@+id/draft_and_smgcount"
android:textAppearance="?android:attr/textAppearanceMedium"
android:layout_alignParentEnd="true"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="6dip"
android:singleLine="true" />
<com.android.mms.util.AlwaysMarqueeTextView android:id="@+id/from"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceMedium"
android:singleLine="true"
android:layout_alignParentStart="true"
android:layout_toStartOf="@id/draft_and_smgcount"
android:layout_marginEnd="6dip"
android:ellipsize="marquee" />
</RelativeLayout>
</RelativeLayout>
<TextView android:id="@+id/date"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:textSize= "12sp"
android:textColor="@color/dlg_text_counter_color"
android:layout_alignParentBottom="true"
android:layout_alignParentEnd="true"
android:layout_marginEnd="10dip"
android:layout_marginBottom="4dip"
android:gravity="bottom" />
<TextView android:id="@+id/subject"
android:layout_height="wrap_content"
android:layout_width="fill_parent"
android:textSize= "12sp"
android:layout_alignParentBottom="true"
android:layout_toEndOf="@id/avatar"
android:layout_toStartOf="@id/date"
android:layout_marginStart="12dip"
android:layout_marginBottom="4dip"
android:gravity="bottom"
android:textColor="@color/dlg_text_counter_color"
android:lines="2"
android:ellipsize="end" />
</com.android.mms.ui.ConversationListItem>
其对应的布局图形如下,图中左边是layout组成,右上是layout布局表现,右下是对layout里面的子view编号,便于理解。
分析xml文件,并对照图例可以看出,
ConversationListItem是整个item布局类,表现形式见下图左边,它指定了高度
android:layout_height="?android:attr/listPreferredItemHeight"
这个布局由6个同级view组成,见下图中间及右上,其中
checkbox供选择使用,见右下数字0,alignParentStart表示它在开头位置,
android:layout_alignParentStart="true"
MmsQuickContactBadge做头像显示,见右下数字1,
ImageView做未读标识等用途,见右下数字2,通过下面的属性将它放置在最右上的位置
android:layout_alignParentEnd="true"
android:layout_alignParentTop="true"
LinearLayout是个组合视图,见右下数字3和iconlist小图,它有四个子ImageView表示短信的信息状态,具体名字见iconlist小图。注意四个子ImageView表示同时最多只能显示四个图标,不是表示短信只有这四种状态。通过下面属性,将它放置在MmsQuickContactBadge后面。
android:orientation="horizontal"
<iconlist>
RelativeLayout是个组合视图,见右下数字4和Text Title小图,包含3个TextView,用来显示Title和消息数目、未读消息数目。在RelativeLayout,通过下面的属性设置,将RelativeLayout(index4的view)和上面的LinearLayout(index3的view)并行放置在同一行。
android:layout_toEndOf="@id/avatar"
android:layout_toStartOf="@id/iconlist"
TextView显示日期,见右下数字5,用下面2个属性表示它的位置在右下,
android:layout_alignParentBottom="true"
android:layout_alignParentEnd="true"
TextView 显示短线截取的部分内容,见右下数字6,用下面3个属性表明了它的位置。
android:layout_alignParentBottom="true"
android:layout_toEndOf="@id/avatar"
android:layout_toStartOf="@id/date"
<Text Content>
3.1.2 代码分析
3.1.2.1 ConversationListItem实例创建
ConversationListItem实例创建栈如下,因为有缓存使用,不一定每次进入列表都会创建这个类实例,所以不一定能调试到,要在此设置断点调试,有一个小技巧,就是将mms进程终止掉,让其自动重启(可通过发送短信或cb的方法加快重启过程),这样就可以进行调试。
ConversationListItem.<init>(Context, AttributeSet) line: 130
Constructor.constructNative(Object[], Class, Class[], int, boolean) line: not available [native method]
Constructor.newInstance(Object...) line: 423
PhoneLayoutInflater(LayoutInflater).createView(String, String, AttributeSet) line: 594
PhoneLayoutInflater(LayoutInflater).createViewFromTag(View, String, AttributeSet) line: 696
PhoneLayoutInflater(LayoutInflater).inflate(XmlPullParser, ViewGroup, boolean) line: 469
PhoneLayoutInflater(LayoutInflater).inflate(int, ViewGroup, boolean) line: 397
ConversationListAdapter.newView(Context, Cursor, ViewGroup) line: 103
ConversationListAdapter(CursorAdapter).getView(int, View, ViewGroup) line: 250
ListView(AbsListView).obtainView(int, boolean[]) line: 2338
ListView.makeAndAddView(int, int, boolean, int, boolean) line: 1812
ListView.fillDown(int, int) line: 698
ListView.fillFromTop(int) line: 759
ListView.layoutChildren() line: 1631
ListView(AbsListView).onLayout(boolean, int, int, int, int) line: 2149
ConversationListItem实例创建的主要过程分析如下,在getView来获取view的时候,因为缓冲没有数据,会使用newView新建一个view,
public View getView(int position, View convertView, ViewGroup parent) {
View v;
if (convertView == null) {
v = newView(mContext, mCursor, parent);
} else {
v = convertView;
}
bindView(v, mContext, mCursor);
return v;
}
newView实际的过程是inflate一个布局文件,这个布局文件就是我们分析的layout,它是ConversationListItem形式的,我们知道,在inflate创建布局文件时,会根据节点创建view实例,所以会使用类的构造类Constructor来创建ConversationListItem实例。
public View newView(Context context, Cursor cursor, ViewGroup parent) {
if (LOCAL_LOGV) Log.v(TAG, "inflating new view");
return mFactory.inflate(R.layout.conversation_list_item, parent, false);
}
Checkbox的主要作用是用来勾选短信列表的某一个item,并进行删除操作的。
对于checkbox,在ConversationListItem里面定义了一个引用mSelectBox,
private CheckBox mSelectBox;
并在onFinishInflate里面初始化它,关联到Layout里面的select_box,
mSelectBox = (CheckBox) findViewById(R.id.select_box);
当列表项被点击时,执行ConversationList.onListItemClick(ListView, View, int, long)这个方法,在里面添加下面代码,当处于删除模式时,如下,将Conversation设置为check状态,然后更新界面,
if (mListAdapter != null && mDeleteMode) {
if (conv.isChecked()) {
conv.setIsChecked(false);
} else {
conv.setIsChecked(true);
}
mListAdapter.notifyDataSetChanged();
updateSelectView();
return;
}
更新界面时,执行ConversationListItem.bind(Context, Conversation) ,每加载一个item项都会执行一次bind,在这里,会根据mSelectMode 决定checkbox是否显示,根据Conversation的状态决定是否选中。
所以实际上,不是我们主观上看到的那样由checkbox决定了当前项目是否选中,而是当前项选中后决定checkbox的显示状态。
if (mSelectMode) {
mSelectBox.setVisibility(View.VISIBLE);
mAvatarView.setClickable(false);
} else {
mSelectBox.setVisibility(View.GONE);
mAvatarView.setClickable(true);
}
if (mConversation.isChecked()) {
mSelectBox.setChecked(true);
} else {
mSelectBox.setChecked(false);
}
上面就是checkbox在代码里的处理过程。
R.id.avatar用来显示发件人头像,定义如下,MmsQuickContactBadge继承自QuickContactBadge,再往上是ImageView,一个通用的视图组件。
private MmsQuickContactBadge mAvatarView;
其实例获取在ConversationListItem的onFinishInflate里面,
mAvatarView = (MmsQuickContactBadge) findViewById(R.id.avatar);
如果希望AvatarView能够被点击,相应事件,则可以看到在bind有如下代码,表示在删除模式下不能被点击,在列表模式下可以被点击。
if (mSelectMode) {
mAvatarView.setClickable(false);
} else {
mAvatarView.setClickable(true);
}
AvatarView被点击之后,执行的调用栈如下,可见是由View直接调用了MmsQuickContactBadge类的onClick,并不是ConversationList或ConversationListItem里做了onClick的相关处理。
MmsQuickContactBadge(QuickContactBadge).onClick(View) line: 217
MmsQuickContactBadge.onClick(View) line: 75
MmsQuickContactBadge(View).performClick() line: 4212
View$PerformClick.run() line: 17476
Handler.handleCallback(Message) line: 800
ViewRootImpl$ViewRootHandler(Handler).dispatchMessage(Message) line: 100
QuickContactBadge类的onClick代码如下,
@Override
public void onClick(View v) {
if (mContactUri != null) {
QuickContact.showQuickContact(getContext(), QuickContactBadge.this, mContactUri,
QuickContact.MODE_LARGE, mExcludeMimes);
} else if (mContactEmail != null) {
mQueryHandler.startQuery(TOKEN_EMAIL_LOOKUP_AND_TRIGGER, mContactEmail,
Uri.withAppendedPath(Email.CONTENT_LOOKUP_URI, Uri.encode(mContactEmail)),
EMAIL_LOOKUP_PROJECTION, null, null, null);
} else if (mContactPhone != null) {
mQueryHandler.startQuery(TOKEN_PHONE_LOOKUP_AND_TRIGGER, mContactPhone,
Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI, mContactPhone),
PHONE_LOOKUP_PROJECTION, null, null, null);
} else {
// If a contact hasn't been assigned, don't react to click.
return;
}
}
我们当前的代码走mContactPhone路径,mContactPhone是在assignContactFromPhone里面赋值的,assignContactFromPhone则是通过ConversationListItem的updateAvatarView调用的(传入的number就赋值给mContactPhone),
mAvatarView.assignContactFromPhone(number, true);
updateAvatarView可以被updateFromView和bind调用,这两个方法我们在前面有过描述。所以从整个过程来看,我们可以看出avatar被点击和其需要的数据来源。
3.2 绘制分析
3.2.1 Message列表创建
如ConversationList的layout描述,Messag列表主要是由list呈现,而list的item项是由ConversationListItem布局的,我们这里讨论每个item项的创建以及整个list的创建。
在使用Cache时,ConversationListItem的创建过程如下,
ConversationListItem.bind(Context, Conversation) line: 480
ConversationListAdapter.bindView(View, Context, Cursor) line: 77
ConversationListAdapter(CursorAdapter).getView(int, View, ViewGroup) line: 250
ListView(AbsListView).obtainView(int, boolean[]) line: 2186
ListView.makeAndAddView(int, int, boolean, int, boolean) line: 1877
ListView.fillSpecific(int, int) line: 1356
ListView.layoutChildren() line: 1671
ListView(AbsListView).onLayout(boolean, int, int, int, int) line: 2037
ListView(View).layout(int, int, int, int) line: 14118
ListView(ViewGroup).layout(int, int, int, int) line: 4467
RelativeLayout.onLayout(boolean, int, int, int, int) line: 1021
从上往下分析该栈:
1)ConversationListItem.bind
ConversationListItem.bind的代码比较多,仔细分析,实际上逻辑比较简单,主要是设置每个view的内容和状态,比如说设置日期的代码如下,
mDateView.setVisibility(VISIBLE);
if(ConversationList.sConversationListOption ==
ConversationList.OPTION_CONVERSATION_LIST_IMPORTANT &&
!conversation.hasUnreadMessages()) {
mDateView.setText(MessageUtils.formatTimeStampStringExtend(context,
conversation.getImpDate()));
}
其数据来源于conversation,我们要进一步分析conversation是怎么传递进来的,以及为什么要调用bind。
2)ConversationListAdapter
ConversationListAdapter 是ConversationList的layout里的list对应的Adapter,它继承自MessageCursorAdapter-CursorAdapter-baseAdapter, 其方法bindView在本例中执行的主要代码如下,其中headerview是一个ConversationListItem引用,是调用方法传递过来的,conversation是根据游标新建的,最后调用bind,所以该方法的作用就是获取到conversation,并用准备将其中的数据赋值到view。
public void bindView(View view, Context context, Cursor cursor) {
ConversationListItem headerView = (ConversationListItem) view;
Conversation conv;
if (!mIsScrolling) {
Conversation.setNeedCacheConv(false);
conv = Conversation.from(context, cursor);
Conversation.setNeedCacheConv(true);
if (mSubjectSingleLine) {
headerView.setSubjectSingleLineMode(true);
}
if (conv != null) {
conv.setIsChecked(sSelectedTheadsId.contains(conv.getThreadId()));
}
headerView.bind(context, conv);
}
}
(CursorAdapter).getView则负责获取到view实例和游标实例。
view的获取有两种方式,一种是调用者传递过来以前缓存的,一种是新创建一个,在ConversationListItem实例创建中有讲到如何创建。
游标实例则在Adapter创建时创建。
3)ListView
在ListView部分,我们从layoutChildren向上分析,向下部分设计到太多ListView的实现机制问题,暂不讨论。
layoutChildren实现ListView子窗口布局,实际上就是list在layout所占部分。
本例从launcher进入Message列表,执行代码路径如下,mSyncPosition是当前item索引值,mSpecificTop是当前layout相对Top的位置。
layoutChildren:
case LAYOUT_SYNC:
sel = fillSpecific(mSyncPosition, mSpecificTop);
fillSpecific在知道位置绘制指定项的item数据,然后上下绘制其他的item。它先通过makeAndAddView完成当前item的创建并添加到layout,fillDown、fillUp则根据layout大小,上下绘制item完成整个list的绘制,使用的方法也是makeAndAddView,adjustViewsUpOrDown调整item位置。
/**
* Put a specific item at a specific location on the screen and then build
* up and down from there.
*
* @param position The reference view to use as the starting point
* @param top Pixel offset from the top of this view to the top of the
* reference view.
*
* @return The selected view, or null if the selected view is outside the
* visible area.
*/
private View fillSpecific(int position, int top) {
boolean tempIsSelected = position == mSelectedPosition;
View temp = makeAndAddView(position, top, true, mListPadding.left, tempIsSelected);
…
below = fillDown(position + 1, temp.getBottom() + dividerHeight);
// This will correct for the bottom of the last view not touching the bottom of the list
adjustViewsUpOrDown();
above = fillUp(position - 1, temp.getTop() - dividerHeight);
int childCount = getChildCount();
if (childCount > 0) {
correctTooLow(childCount);
}
}
顾名思义,makeAndAddView就是新建一个view,并将它添加到list里面。obtainView负责创建view,setupChild负责将view添加到layout,并设置好view的位置和相关属性。
private View makeAndAddView(int position, int y, boolean flow, int childrenLeft,
boolean selected) {
View child;
child = obtainView(position, mIsScrap);
// This needs to be positioned and measured
setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]);
return child;
}
obtainView将对应位置的view和数据关联起来,并返回view。它通过mRecycler获取到缓存的view,再通过mAdapter.getView将数据填充到view,mAdapter.getView即是我们前面分析到的ConversationListAdapter(CursorAdapter).getView。
至此,Message列表的创建过程就明了了。
3.3 功能分析
3.3.1 Message列表更新
当数据库内容发生变化(包括新短信、删除、发送状态改变等),就会触发onContentChanged,其调用栈如下:
ConversationList$2.onContentChanged(ConversationListAdapter) line: 474
ConversationListAdapter.onContentChanged() line: 118
CursorAdapter$ChangeObserver.onChange(boolean) line: 463
CursorAdapter$ChangeObserver(ContentObserver).onChange(boolean, Uri) line: 129
ContentObserver$NotificationRunnable.run() line: 180
Handler.handleCallback(Message) line: 808
Handler.dispatchMessage(Message) line: 103
Looper.loop() line: 193
从下往上分析,
1)ContentObserver
我们已经很熟悉handleCallback处理的是Post的消息,消息一般是runable,所以可以看到ContentObserver. dispatchChange有post这一消息的过程(dispatchChange的触发过程比较复杂,我们将在其他章节介绍,本例中当数据库内容变化时会触发这个流程)。
public final void dispatchChange(boolean selfChange, Uri uri) {
if (mHandler == null) {
onChange(selfChange, uri);
} else {
mHandler.post(new NotificationRunnable(selfChange, uri));
}
}
然后这个消息会运行其内部类NotificationRunnable的run方法,
public void run() {
ContentObserver.this.onChange(mSelfChange, mUri);
}
This指向的是ContentObserver的实例ChangeObserver,它是CursorAdapter的内部类,也是ContentObserver的子类,由于ConversationListAdapter通过CursorWrapper向ContentObservable注册了ContentObserver,所以当dispatchChange事件时,通过轮询,最终将事件传递给ChangeObserver。
而后,ChangeObserver调用封装类CursorAdapter的onContentChanged,将事件上传。
private class ChangeObserver extends ContentObserver {
public void onChange(boolean selfChange) {
onContentChanged();
}
}
2)onContentChanged
onContentChanged是CursorAdapter的一个方法,主要功能是当数据库内容更新后,重新使用游标查询数据。但这个方法被ConversationListAdapter重载,它给自己的客户提供一个借口类OnContentChangedListener,让客户去实现这个借口类,自己则调用接口类的onContentChanged方法执行客户定义的功能。
protected void onContentChanged() {
if (mCursor != null && !mCursor.isClosed()) {
if (mOnContentChangedListener != null) {
mOnContentChangedListener.onContentChanged(this);
}
}
}
3)ConversationList
ConversationList就是ConversationListAdapter的客户,它实现了接口类,如下,主要是调用startAsyncQuery进行数据的查询。
private final ConversationListAdapter.OnContentChangedListener mContentChangedListener =
new ConversationListAdapter.OnContentChangedListener() {
@Override
public void onContentChanged(ConversationListAdapter adapter) {
if (mIsInActivity) {
mNeedQuery = true;
startAsyncQuery();
}
}
};
并且ConversationList在其onStart向ConversationListAdapter注册了这个接口类监听器:
mListAdapter.setOnContentChangedListener(mContentChangedListener);
所以,当数据库数据改变时,根据上面相关调用流程,完成了Message列表的刷新功能。
BTW,附上游标改变后注册ContentObserver的调用栈:
ContentResolver$CursorWrapperInner(CursorWrapper).registerContentObserver(ContentObserver) line: 178
ConversationListAdapter(CursorAdapter).swapCursor(Cursor) line: 340
ConversationListAdapter(CursorAdapter).changeCursor(Cursor) line: 313
ConversationList$ThreadListQueryHandler.onQueryComplete(int, Object, Cursor) line: 1896
ConversationList$ThreadListQueryHandler(AsyncQueryHandler).handleMessage(Message) line: 344
ConversationList$ThreadListQueryHandler(Handler).dispatchMessage(Message) line: 110
Looper.loop() line: 193
3.3.2 Message列表定位
因为ConversationList是ListActivity,所以可以使用getListView获取ListView,再使用ListView的setSelectionFromTop,将列表显示开始位置指定到特定位置。
需要注意2个问题,
1)
在ConversationList会记录上次浏览列表的起始位置,所以不是随意就能设置好列表的显示位置,如onQueryComplete通过下面代码控制了什么时候可以设置这个起始位置,我们需要根据mSavedFirstVisiblePosition的值和相关逻辑去处理。
if (mSavedFirstVisiblePosition != AdapterView.INVALID_POSITION) {
// Restore the list to its previous position.
getListView().setSelectionFromTop(mSavedFirstVisiblePosition,
mSavedFirstItemOffset);
mSavedFirstVisiblePosition = AdapterView.INVALID_POSITION;
}
结合列表的创建过程,我们知道,当同步绘制列表时,是从mSyncPosition开始的,
fillSpecific(mSyncPosition, mSpecificTop);
而mSyncPosition正是setSelectionFromTop里传递进去的,所以列表起始位置只能通过setSelectionFromTop来设置。
public void setSelectionFromTop(int position, int y) {
…
mSyncPosition = position;
…
}
2)
游标的更新,当数据库数据变化后,需要更新游标,才能获取到正确的值。下面是onQueryComplete完成后,用changeCursor替换掉旧游标的过程,防止游标数据过期。
ConversationListAdapter(CursorAdapter).swapCursor(Cursor) line: 340
ConversationListAdapter(CursorAdapter).changeCursor(Cursor) line: 313
ConversationList$ThreadListQueryHandler.onQueryComplete(int, Object, Cursor) line: 1896
ConversationList$ThreadListQueryHandler(AsyncQueryHandler).handleMessage(Message) line: 344
ConversationList$ThreadListQueryHandler(Handler).dispatchMessage(Message) line: 110
Looper.loop() line: 193
在列表创建时,CursorAdapter. getView的游标最初是在CursorAdapte创建时传入的,之后可以通过swapCursor的方法更新。