Android短信列表源码分析

 

 

 

 

目 录

1.      概要

2.     ConversationList

2.1        ConversationList

2.1.1         Layout分析

2.1.2         代码分析

2.1.2.1    ConversationListAdapter

2.2             功能分析

2.2.1         列表项click

2.2.2         Long Press click to Delete

2.3             数据操作

2.3.1         会话ID创建

2.3.1.1    CB message

2.3.1.2    新建短信

2.3.1.3    通过联系人发送短信

3.     ConversationListItem

3.1             ConversationListItem

3.1.1         Layout分析

3.1.2         代码分析

3.1.2.1    ConversationListItem实例创建

3.1.2.2    CheckBox

3.1.2.3    Avatar

3.2             绘制分析

3.2.1         Message列表创建

3.3             功能分析

3.3.1         Message列表更新

3.3.2         Message列表定位

 


1.       概要

本文主要分析短信列表界面的主要控件的界面显示和处理逻辑。

2.    ConversationList

2.1         ConversationList

2.1.1         Layout分析

ConversationList对应的layout是conversation_list_screen,它主要分3部分,第一部分是做全选的控制,第二部分是list列表,第三部分是列表为空的布局。具体的表现形式如下,布局文件就不再展开了。其中list列表将在另外的章节分析。

《Android短信列表源码分析》

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删除会话流程如下:

《Android短信列表源码分析》

长按出现选择菜单,

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是如何创建的,

 

2.3.1.1        CB message

应用在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来查询,

《Android短信列表源码分析》

所以在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。

 

 

2.3.1.2        新建短信

在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,具体过程和下节相同。

 

 

2.3.1.3        通过联系人发送短信

在联系人的详情界面,点击发送短信,因为有号码存在,所以在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布局的,在本例中,其直观显示是如下形式的一条短信记录,

 《Android短信列表源码分析》

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编号,便于理解。

 《Android短信列表源码分析》

分析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”

《Android短信列表源码分析》

<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”

《Android短信列表源码分析》

<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);

    }

 

 

 

3.1.2.2                                  CheckBox

 

 

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在代码里的处理过程。

 

 

3.1.2.3                                  Avatar

 

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的方法更新。

 

 

 

 

目 录

1.      概要

2.     ConversationList

2.1        ConversationList

2.1.1         Layout分析

2.1.2         代码分析

2.1.2.1    ConversationListAdapter

2.2             功能分析

2.2.1         列表项click

2.2.2         Long Press click to Delete

2.3             数据操作

2.3.1         会话ID创建

2.3.1.1    CB message

2.3.1.2    新建短信

2.3.1.3    通过联系人发送短信

3.     ConversationListItem

3.1             ConversationListItem

3.1.1         Layout分析

3.1.2         代码分析

3.1.2.1    ConversationListItem实例创建

3.1.2.2    CheckBox

3.1.2.3    Avatar

3.2             绘制分析

3.2.1         Message列表创建

3.3             功能分析

3.3.1         Message列表更新

3.3.2         Message列表定位

 


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是如何创建的,

 

2.3.1.1        CB message

应用在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。

 

 

2.3.1.2        新建短信

在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,具体过程和下节相同。

 

 

2.3.1.3        通过联系人发送短信

在联系人的详情界面,点击发送短信,因为有号码存在,所以在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);

    }

 

 

 

3.1.2.2                                  CheckBox

 

 

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在代码里的处理过程。

 

 

3.1.2.3                                  Avatar

 

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_详细分析

 

目 录

1.      概要

2.     ConversationList

2.1        ConversationList

2.1.1         Layout分析

2.1.2         代码分析

2.1.2.1    ConversationListAdapter

2.2             功能分析

2.2.1         列表项click

2.2.2         Long Press click to Delete

2.3             数据操作

2.3.1         会话ID创建

2.3.1.1    CB message

2.3.1.2    新建短信

2.3.1.3    通过联系人发送短信

3.     ConversationListItem

3.1             ConversationListItem

3.1.1         Layout分析

3.1.2         代码分析

3.1.2.1    ConversationListItem实例创建

3.1.2.2    CheckBox

3.1.2.3    Avatar

3.2             绘制分析

3.2.1         Message列表创建

3.3             功能分析

3.3.1         Message列表更新

3.3.2         Message列表定位

 


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是如何创建的,

 

2.3.1.1        CB message

应用在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。

 

 

2.3.1.2        新建短信

在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,具体过程和下节相同。

 

 

2.3.1.3        通过联系人发送短信

在联系人的详情界面,点击发送短信,因为有号码存在,所以在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);

    }

 

 

 

3.1.2.2                                  CheckBox

 

 

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在代码里的处理过程。

 

 

3.1.2.3                                  Avatar

 

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的方法更新。

 

 

 

 

目 录

1.      概要

2.     ConversationList

2.1        ConversationList

2.1.1         Layout分析

2.1.2         代码分析

2.1.2.1    ConversationListAdapter

2.2             功能分析

2.2.1         列表项click

2.2.2         Long Press click to Delete

2.3             数据操作

2.3.1         会话ID创建

2.3.1.1    CB message

2.3.1.2    新建短信

2.3.1.3    通过联系人发送短信

3.     ConversationListItem

3.1             ConversationListItem

3.1.1         Layout分析

3.1.2         代码分析

3.1.2.1    ConversationListItem实例创建

3.1.2.2    CheckBox

3.1.2.3    Avatar

3.2             绘制分析

3.2.1         Message列表创建

3.3             功能分析

3.3.1         Message列表更新

3.3.2         Message列表定位

 


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是如何创建的,

 

2.3.1.1        CB message

应用在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。

 

 

2.3.1.2        新建短信

在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,具体过程和下节相同。

 

 

2.3.1.3        通过联系人发送短信

在联系人的详情界面,点击发送短信,因为有号码存在,所以在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);

    }

 

 

 

3.1.2.2                                  CheckBox

 

 

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在代码里的处理过程。

 

 

3.1.2.3                                  Avatar

 

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_详细分析

 

 

目 录

1.      概要

2.     ConversationList

2.1        ConversationList

2.1.1         Layout分析

2.1.2         代码分析

2.1.2.1    ConversationListAdapter

2.2             功能分析

2.2.1         列表项click

2.2.2         Long Press click to Delete

2.3             数据操作

2.3.1         会话ID创建

2.3.1.1    CB message

2.3.1.2    新建短信

2.3.1.3    通过联系人发送短信

3.     ConversationListItem

3.1             ConversationListItem

3.1.1         Layout分析

3.1.2         代码分析

3.1.2.1    ConversationListItem实例创建

3.1.2.2    CheckBox

3.1.2.3    Avatar

3.2             绘制分析

3.2.1         Message列表创建

3.3             功能分析

3.3.1         Message列表更新

3.3.2         Message列表定位

 


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是如何创建的,

 

2.3.1.1        CB message

应用在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。

 

 

2.3.1.2        新建短信

在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,具体过程和下节相同。

 

 

2.3.1.3        通过联系人发送短信

在联系人的详情界面,点击发送短信,因为有号码存在,所以在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);

    }

 

 

 

3.1.2.2                                  CheckBox

 

 

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在代码里的处理过程。

 

 

3.1.2.3                                  Avatar

 

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的方法更新。

 

 

 

 

目 录

1.      概要

2.     ConversationList

2.1        ConversationList

2.1.1         Layout分析

2.1.2         代码分析

2.1.2.1    ConversationListAdapter

2.2             功能分析

2.2.1         列表项click

2.2.2         Long Press click to Delete

2.3             数据操作

2.3.1         会话ID创建

2.3.1.1    CB message

2.3.1.2    新建短信

2.3.1.3    通过联系人发送短信

3.     ConversationListItem

3.1             ConversationListItem

3.1.1         Layout分析

3.1.2         代码分析

3.1.2.1    ConversationListItem实例创建

3.1.2.2    CheckBox

3.1.2.3    Avatar

3.2             绘制分析

3.2.1         Message列表创建

3.3             功能分析

3.3.1         Message列表更新

3.3.2         Message列表定位

 


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是如何创建的,

 

2.3.1.1        CB message

应用在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。

 

 

2.3.1.2        新建短信

在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,具体过程和下节相同。

 

 

2.3.1.3        通过联系人发送短信

在联系人的详情界面,点击发送短信,因为有号码存在,所以在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);

    }

 

 

 

3.1.2.2                                  CheckBox

 

 

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在代码里的处理过程。

 

 

3.1.2.3                                  Avatar

 

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_详细分析

 

目 录

1.      概要

2.     ConversationList

2.1        ConversationList

2.1.1         Layout分析

2.1.2         代码分析

2.1.2.1    ConversationListAdapter

2.2             功能分析

2.2.1         列表项click

2.2.2         Long Press click to Delete

2.3             数据操作

2.3.1         会话ID创建

2.3.1.1    CB message

2.3.1.2    新建短信

2.3.1.3    通过联系人发送短信

3.     ConversationListItem

3.1             ConversationListItem

3.1.1         Layout分析

3.1.2         代码分析

3.1.2.1    ConversationListItem实例创建

3.1.2.2    CheckBox

3.1.2.3    Avatar

3.2             绘制分析

3.2.1         Message列表创建

3.3             功能分析

3.3.1         Message列表更新

3.3.2         Message列表定位

 


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是如何创建的,

 

2.3.1.1        CB message

应用在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。

 

 

2.3.1.2        新建短信

在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,具体过程和下节相同。

 

 

2.3.1.3        通过联系人发送短信

在联系人的详情界面,点击发送短信,因为有号码存在,所以在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);

    }

 

 

 

3.1.2.2                                  CheckBox

 

 

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在代码里的处理过程。

 

 

3.1.2.3                                  Avatar

 

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的方法更新。

 

 

 

 

目 录

1.      概要

2.     ConversationList

2.1        ConversationList

2.1.1         Layout分析

2.1.2         代码分析

2.1.2.1    ConversationListAdapter

2.2             功能分析

2.2.1         列表项click

2.2.2         Long Press click to Delete

2.3             数据操作

2.3.1         会话ID创建

2.3.1.1    CB message

2.3.1.2    新建短信

2.3.1.3    通过联系人发送短信

3.     ConversationListItem

3.1             ConversationListItem

3.1.1         Layout分析

3.1.2         代码分析

3.1.2.1    ConversationListItem实例创建

3.1.2.2    CheckBox

3.1.2.3    Avatar

3.2             绘制分析

3.2.1         Message列表创建

3.3             功能分析

3.3.1         Message列表更新

3.3.2         Message列表定位

 


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是如何创建的,

 

2.3.1.1        CB message

应用在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。

 

 

2.3.1.2        新建短信

在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,具体过程和下节相同。

 

 

2.3.1.3        通过联系人发送短信

在联系人的详情界面,点击发送短信,因为有号码存在,所以在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);

    }

 

 

 

3.1.2.2                                  CheckBox

 

 

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在代码里的处理过程。

 

 

3.1.2.3                                  Avatar

 

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_详细分析

 

 

目 录

1.      概要

2.     ConversationList

2.1        ConversationList

2.1.1         Layout分析

2.1.2         代码分析

2.1.2.1    ConversationListAdapter

2.2             功能分析

2.2.1         列表项click

2.2.2         Long Press click to Delete

2.3             数据操作

2.3.1         会话ID创建

2.3.1.1    CB message

2.3.1.2    新建短信

2.3.1.3    通过联系人发送短信

3.     ConversationListItem

3.1             ConversationListItem

3.1.1         Layout分析

3.1.2         代码分析

3.1.2.1    ConversationListItem实例创建

3.1.2.2    CheckBox

3.1.2.3    Avatar

3.2             绘制分析

3.2.1         Message列表创建

3.3             功能分析

3.3.1         Message列表更新

3.3.2         Message列表定位

 


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是如何创建的,

 

2.3.1.1        CB message

应用在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。

 

 

2.3.1.2        新建短信

在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,具体过程和下节相同。

 

 

2.3.1.3        通过联系人发送短信

在联系人的详情界面,点击发送短信,因为有号码存在,所以在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);

    }

 

 

 

3.1.2.2                                  CheckBox

 

 

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在代码里的处理过程。

 

 

3.1.2.3                                  Avatar

 

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的方法更新。

 

 

 

 

目 录

1.      概要

2.     ConversationList

2.1        ConversationList

2.1.1         Layout分析

2.1.2         代码分析

2.1.2.1    ConversationListAdapter

2.2             功能分析

2.2.1         列表项click

2.2.2         Long Press click to Delete

2.3             数据操作

2.3.1         会话ID创建

2.3.1.1    CB message

2.3.1.2    新建短信

2.3.1.3    通过联系人发送短信

3.     ConversationListItem

3.1             ConversationListItem

3.1.1         Layout分析

3.1.2         代码分析

3.1.2.1    ConversationListItem实例创建

3.1.2.2    CheckBox

3.1.2.3    Avatar

3.2             绘制分析

3.2.1         Message列表创建

3.3             功能分析

3.3.1         Message列表更新

3.3.2         Message列表定位

 


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是如何创建的,

 

2.3.1.1        CB message

应用在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。

 

 

2.3.1.2        新建短信

在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,具体过程和下节相同。

 

 

2.3.1.3        通过联系人发送短信

在联系人的详情界面,点击发送短信,因为有号码存在,所以在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);

    }

 

 

 

3.1.2.2                                  CheckBox

 

 

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在代码里的处理过程。

 

 

3.1.2.3                                  Avatar

 

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_详细分析

 

 

 

目 录

1.      概要

2.     ConversationList

2.1        ConversationList

2.1.1         Layout分析

2.1.2         代码分析

2.1.2.1    ConversationListAdapter

2.2             功能分析

2.2.1         列表项click

2.2.2         Long Press click to Delete

2.3             数据操作

2.3.1         会话ID创建

2.3.1.1    CB message

2.3.1.2    新建短信

2.3.1.3    通过联系人发送短信

3.     ConversationListItem

3.1             ConversationListItem

3.1.1         Layout分析

3.1.2         代码分析

3.1.2.1    ConversationListItem实例创建

3.1.2.2    CheckBox

3.1.2.3    Avatar

3.2             绘制分析

3.2.1         Message列表创建

3.3             功能分析

3.3.1         Message列表更新

3.3.2         Message列表定位

 


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是如何创建的,

 

2.3.1.1        CB message

应用在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。

 

 

2.3.1.2        新建短信

在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,具体过程和下节相同。

 

 

2.3.1.3        通过联系人发送短信

在联系人的详情界面,点击发送短信,因为有号码存在,所以在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);

    }

 

 

 

3.1.2.2                                  CheckBox

 

 

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在代码里的处理过程。

 

 

3.1.2.3                                  Avatar

 

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的方法更新。

 

 

 

 

目 录

1.      概要

2.     ConversationList

2.1        ConversationList

2.1.1         Layout分析

2.1.2         代码分析

2.1.2.1    ConversationListAdapter

2.2             功能分析

2.2.1         列表项click

2.2.2         Long Press click to Delete

2.3             数据操作

2.3.1         会话ID创建

2.3.1.1    CB message

2.3.1.2    新建短信

2.3.1.3    通过联系人发送短信

3.     ConversationListItem

3.1             ConversationListItem

3.1.1         Layout分析

3.1.2         代码分析

3.1.2.1    ConversationListItem实例创建

3.1.2.2    CheckBox

3.1.2.3    Avatar

3.2             绘制分析

3.2.1         Message列表创建

3.3             功能分析

3.3.1         Message列表更新

3.3.2         Message列表定位

 


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是如何创建的,

 

2.3.1.1        CB message

应用在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。

 

 

2.3.1.2        新建短信

在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,具体过程和下节相同。

 

 

2.3.1.3        通过联系人发送短信

在联系人的详情界面,点击发送短信,因为有号码存在,所以在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);

    }

 

 

 

3.1.2.2                                  CheckBox

 

 

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在代码里的处理过程。

 

 

3.1.2.3                                  Avatar

 

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的方法更新。

 

 

 

 

目 录

1.      概要

2.     ConversationList

2.1        ConversationList

2.1.1         Layout分析

2.1.2         代码分析

2.1.2.1    ConversationListAdapter

2.2             功能分析

2.2.1         列表项click

2.2.2         Long Press click to Delete

2.3             数据操作

2.3.1         会话ID创建

2.3.1.1    CB message

2.3.1.2    新建短信

2.3.1.3    通过联系人发送短信

3.     ConversationListItem

3.1             ConversationListItem

3.1.1         Layout分析

3.1.2         代码分析

3.1.2.1    ConversationListItem实例创建

3.1.2.2    CheckBox

3.1.2.3    Avatar

3.2             绘制分析

3.2.1         Message列表创建

3.3             功能分析

3.3.1         Message列表更新

3.3.2         Message列表定位

 


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是如何创建的,

 

2.3.1.1        CB message

应用在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。

 

 

2.3.1.2        新建短信

在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,具体过程和下节相同。

 

 

2.3.1.3        通过联系人发送短信

在联系人的详情界面,点击发送短信,因为有号码存在,所以在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);

    }

 

 

 

3.1.2.2                                  CheckBox

 

 

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在代码里的处理过程。

 

 

3.1.2.3                                  Avatar

 

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的方法更新。

 

 

 

 

 

1.      概要

2.     ConversationList

2.1        ConversationList

2.1.1         Layout分析

2.1.2         代码分析

2.1.2.1    ConversationListAdapter

2.2             功能分析

2.2.1         列表项click

2.2.2         Long Press click to Delete

2.3             数据操作

2.3.1         会话ID创建

2.3.1.1    CB message

2.3.1.2    新建短信

2.3.1.3    通过联系人发送短信

3.     ConversationListItem

3.1             ConversationListItem

3.1.1         Layout分析

3.1.2         代码分析

3.1.2.1    ConversationListItem实例创建

3.1.2.2    CheckBox

3.1.2.3    Avatar

3.2             绘制分析

3.2.1         Message列表创建

3.3             功能分析

3.3.1         Message列表更新

3.3.2         Message列表定位

 



1.       概要

本文主要分析短信列表界面的主要控件的界面显示和处理逻辑。

2.    ConversationList

2.1         ConversationList

2.1.1         Layout分析

ConversationList对应的layoutconversation_list_screen,它主要分3部分,第一部分是做全选的控制,第二部分是list列表,第三部分是列表为空的布局。具体的表现形式如下,布局文件就不再展开了。其中list列表将在另外的章节分析。

《Android短信列表源码分析》

2.1.2         代码分析

 

2.1.2.1                                  ConversationListAdapter

 

ConversationListAdapterConversationListItem的数据适配器,在ConversationList.onCreate里创建,它通过bindView将会话信息绑定到itemlayout上。

其创建过程如下,

     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继承自ListActivitysetListAdapterAdapter实例设置给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删除会话流程如下:

《Android短信列表源码分析》

长按出现选择菜单,

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; ,在本例中,这个handlerThreadListQueryHandler

 

数据库查询完成后执行onQueryCompleteHAVE_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是如何创建的,

 

2.3.1.1        CB message

应用在CBMessageReceiverService.java收到小区广播后,使用handleCBMessageReceived方法进行处理,其中insertMessage是用来存储CB的。

 

insertMessage会调用CbProviderinsert方法

      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 MessageThreadId是怎样获取的,在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和参数recipientcellbroadcast,并使用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会调用到ContentResolverContentResolver则最终调用到MmsSmsProvider

 

MmsSmsProvider.query里面,也是通过URI match找到操作码URI_THREAD_ID,根据参数cellbroadcast,使用getCBThreadIdCB查找或创建其ThreadId

 

Threads表内容如下所示,里面的关键字段有_id recipient_ids,要想找到合适的ThreadId,只有通过recipient_ids来查询,

《Android短信列表源码分析》

所以在getCBThreadId方法里面,通过getAddressIdscanonical_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

 

 

2.3.1.2        新建短信

Message列表界面通过按钮新建短信时,执行createNewMessage(),通过createIntent传递一个threadId=0ID过去,

 

    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;

    }

 

因为threadId0,在会话创建中会获取或创建一个threadId,具体过程和下节相同。

 

 

2.3.1.3        通过联系人发送短信

在联系人的详情界面,点击发送短信,因为有号码存在,所以在ComposeMessageActivity.Oncreate方法里面会去查询或创建一个threadId

 

onCreateinitActivityState里,有这么一段代码是获取会话实例的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查找或创建其ThreadIdgetAddressIds获取到匹配的recipientIds,再取Threads表里查询,如果查询到,返回游标,如果没有记录,则用insertThreadThreads里插入记录,再查询到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布局的,在本例中,其直观显示是如下形式的一条短信记录,

《Android短信列表源码分析》

 

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编号,便于理解。

《Android短信列表源码分析》

 

分析xml文件,并对照图例可以看出,

ConversationListItem是整个item布局类,表现形式见下图左边,它指定了高度

    android:layout_height=”?android:attr/listPreferredItemHeight”

 

这个布局由6个同级view组成,见下图中间及右上,其中

checkbox供选择使用,见右下数字0alignParentStart表示它在开头位置,

        android:layout_alignParentStart=”true”

MmsQuickContactBadge做头像显示,见右下数字1

ImageView做未读标识等用途,见右下数字2,通过下面的属性将它放置在最右上的位置

        android:layout_alignParentEnd=”true”

        android:layout_alignParentTop=”true”

 

 

LinearLayout是个组合视图,见右下数字3iconlist小图,它有四个子ImageView表示短信的信息状态,具体名字见iconlist小图。注意四个子ImageView表示同时最多只能显示四个图标,不是表示短信只有这四种状态。通过下面属性,将它放置在MmsQuickContactBadge后面。

        android:orientation=”horizontal”

《Android短信列表源码分析》

<iconlist>

 

RelativeLayout是个组合视图,见右下数字4Text Title小图,包含3TextView,用来显示Title和消息数目、未读消息数目。在RelativeLayout,通过下面的属性设置,将RelativeLayoutindex4view)和上面的LinearLayoutindex3view)并行放置在同一行。

        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”

《Android短信列表源码分析》

<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);

    }

 

 

 

3.1.2.2                                  CheckBox

 

 

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在代码里的处理过程。

 

 

3.1.2.3                                  Avatar

 

R.id.avatar用来显示发件人头像,定义如下,MmsQuickContactBadge继承自QuickContactBadge,再往上是ImageView,一个通用的视图组件。

private MmsQuickContactBadge mAvatarView;

 

其实例获取在ConversationListItemonFinishInflate里面,

      mAvatarView = (MmsQuickContactBadge) findViewById(R.id.avatar);

 

如果希望AvatarView能够被点击,相应事件,则可以看到bind有如下代码,表示在删除模式下不能被点击,在列表模式下可以被点击。

         if (mSelectMode) {

            mAvatarView.setClickable(false);

        } else {

            mAvatarView.setClickable(true);

        }

 

AvatarView被点击之后,执行的调用栈如下,可见是由View直接调用了MmsQuickContactBadge类的onClick,并不是ConversationListConversationListItem里做了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则是通过ConversationListItemupdateAvatarView调用的(传入的number就赋值给mContactPhone),

                   mAvatarView.assignContactFromPhone(number, true);

 

updateAvatarView可以被updateFromViewbind调用,这两个方法我们在前面有过描述。所以从整个过程来看,我们可以看出avatar被点击和其需要的数据来源。

 

 

3.2         绘制分析

3.2.1         Message列表创建

ConversationListlayout描述,Messag列表主要是由list呈现,而listitem项是由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   

 

从上往下分析该栈:

1ConversationListItem.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

 

2ConversationListAdapter

ConversationListAdapter ConversationListlayout里的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创建时创建。

 

3ListView

ListView部分,我们从layoutChildren向上分析,向下部分设计到太多ListView的实现机制问题,暂不讨论。

 

layoutChildren实现ListView子窗口布局,实际上就是listlayout所占部分。

本例从launcher进入Message列表,执行代码路径如下,mSyncPosition是当前item索引值,mSpecificTop是当前layout相对Top的位置。

layoutChildren

            case LAYOUT_SYNC:

                sel = fillSpecific(mSyncPosition, mSpecificTop);

 

fillSpecific在知道位置绘制指定项的item数据,然后上下绘制其他的item。它先通过makeAndAddView完成当前item的创建并添加到layoutfillDownfillUp则根据layout大小,上下绘制item完成整个list的绘制,使用的方法也是makeAndAddViewadjustViewsUpOrDown调整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负责创建viewsetupChild负责将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将数据填充到viewmAdapter.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 

 

从下往上分析,

1ContentObserver

我们已经很熟悉handleCallback处理的是Post的消息,消息一般是runable,所以可以看到ContentObserver. dispatchChangepost这一消息的过程(dispatchChange的触发过程比较复杂,我们将在其他章节介绍,本例中当数据库内容变化时会触发这个流程)。

     public final void dispatchChange(boolean selfChange, Uri uri) {

        if (mHandler == null) {

            onChange(selfChange, uri);

        } else {

            mHandler.post(new NotificationRunnable(selfChange, uri));

        }

    }

 

然后这个消息会运行其内部类NotificationRunnablerun方法,

         public void run() {

            ContentObserver.this.onChange(mSelfChange, mUri);

        }

 

This指向的是ContentObserver的实例ChangeObserver,它是CursorAdapter的内部类,也是ContentObserver子类,由于ConversationListAdapter通过CursorWrapperContentObservable注册了ContentObserver,所以当dispatchChange事件时,通过轮询,最终将事件传递给ChangeObserver

而后,ChangeObserver调用封装类CursorAdapteronContentChanged,将事件上传。

     private class ChangeObserver extends ContentObserver {

        public void onChange(boolean selfChange) {

            onContentChanged();

        }

    }

 

 

2onContentChanged

onContentChangedCursorAdapter的一个方法,主要功能是当数据库内容更新后,重新使用游标查询数据。但这个方法被ConversationListAdapter重载,它给自己的客户提供一个借口类OnContentChangedListener,让客户去实现这个借口类,自己则调用接口类的onContentChanged方法执行客户定义的功能。

     protected void onContentChanged() {

        if (mCursor != null && !mCursor.isClosed()) {

            if (mOnContentChangedListener != null) {

                mOnContentChangedListener.onContentChanged(this);

            }

        }

    }

 

 

3ConversationList

ConversationList就是ConversationListAdapter的客户,它实现了接口类,如下,主要是调用startAsyncQuery进行数据的查询。

     private final ConversationListAdapter.OnContentChangedListener mContentChangedListener =

        new ConversationListAdapter.OnContentChangedListener() {

 

        @Override

        public void onContentChanged(ConversationListAdapter adapter) {

            if (mIsInActivity) {

                mNeedQuery = true;

                startAsyncQuery();

            }

        }

    };

 

并且ConversationList在其onStartConversationListAdapter注册了这个接口类监听器:

            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列表定位

因为ConversationListListActivity所以可以使用getListView获取ListView再使用ListViewsetSelectionFromTop将列表显示开始位置指定到特定位置。

 

需要注意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的方法更新。

 

 

 

 

 

1.      概要

2.     ConversationList

2.1        ConversationList

2.1.1         Layout分析

2.1.2         代码分析

2.1.2.1    ConversationListAdapter

2.2             功能分析

2.2.1         列表项click

2.2.2         Long Press click to Delete

2.3             数据操作

2.3.1         会话ID创建

2.3.1.1    CB message

2.3.1.2    新建短信

2.3.1.3    通过联系人发送短信

3.     ConversationListItem

3.1             ConversationListItem

3.1.1         Layout分析

3.1.2         代码分析

3.1.2.1    ConversationListItem实例创建

3.1.2.2    CheckBox

3.1.2.3    Avatar

3.2             绘制分析

3.2.1         Message列表创建

3.3             功能分析

3.3.1         Message列表更新

3.3.2         Message列表定位

 



1.       概要

本文主要分析短信列表界面的主要控件的界面显示和处理逻辑。

2.    ConversationList

2.1         ConversationList

2.1.1         Layout分析

ConversationList对应的layoutconversation_list_screen,它主要分3部分,第一部分是做全选的控制,第二部分是list列表,第三部分是列表为空的布局。具体的表现形式如下,布局文件就不再展开了。其中list列表将在另外的章节分析。

《Android短信列表源码分析》

2.1.2         代码分析

 

2.1.2.1                                  ConversationListAdapter

 

ConversationListAdapterConversationListItem的数据适配器,在ConversationList.onCreate里创建,它通过bindView将会话信息绑定到itemlayout上。

其创建过程如下,

     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继承自ListActivitysetListAdapterAdapter实例设置给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删除会话流程如下:

《Android短信列表源码分析》

长按出现选择菜单,

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; ,在本例中,这个handlerThreadListQueryHandler

 

数据库查询完成后执行onQueryCompleteHAVE_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是如何创建的,

 

2.3.1.1        CB message

应用在CBMessageReceiverService.java收到小区广播后,使用handleCBMessageReceived方法进行处理,其中insertMessage是用来存储CB的。

 

insertMessage会调用CbProviderinsert方法

      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 MessageThreadId是怎样获取的,在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和参数recipientcellbroadcast,并使用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会调用到ContentResolverContentResolver则最终调用到MmsSmsProvider

 

MmsSmsProvider.query里面,也是通过URI match找到操作码URI_THREAD_ID,根据参数cellbroadcast,使用getCBThreadIdCB查找或创建其ThreadId

 

Threads表内容如下所示,里面的关键字段有_id recipient_ids,要想找到合适的ThreadId,只有通过recipient_ids来查询,

《Android短信列表源码分析》

所以在getCBThreadId方法里面,通过getAddressIdscanonical_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

 

 

2.3.1.2        新建短信

Message列表界面通过按钮新建短信时,执行createNewMessage(),通过createIntent传递一个threadId=0ID过去,

 

    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;

    }

 

因为threadId0,在会话创建中会获取或创建一个threadId,具体过程和下节相同。

 

 

2.3.1.3        通过联系人发送短信

在联系人的详情界面,点击发送短信,因为有号码存在,所以在ComposeMessageActivity.Oncreate方法里面会去查询或创建一个threadId

 

onCreateinitActivityState里,有这么一段代码是获取会话实例的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查找或创建其ThreadIdgetAddressIds获取到匹配的recipientIds,再取Threads表里查询,如果查询到,返回游标,如果没有记录,则用insertThreadThreads里插入记录,再查询到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布局的,在本例中,其直观显示是如下形式的一条短信记录,

《Android短信列表源码分析》

 

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编号,便于理解。

《Android短信列表源码分析》

 

分析xml文件,并对照图例可以看出,

ConversationListItem是整个item布局类,表现形式见下图左边,它指定了高度

    android:layout_height=”?android:attr/listPreferredItemHeight”

 

这个布局由6个同级view组成,见下图中间及右上,其中

checkbox供选择使用,见右下数字0alignParentStart表示它在开头位置,

        android:layout_alignParentStart=”true”

MmsQuickContactBadge做头像显示,见右下数字1

ImageView做未读标识等用途,见右下数字2,通过下面的属性将它放置在最右上的位置

        android:layout_alignParentEnd=”true”

        android:layout_alignParentTop=”true”

 

 

LinearLayout是个组合视图,见右下数字3iconlist小图,它有四个子ImageView表示短信的信息状态,具体名字见iconlist小图。注意四个子ImageView表示同时最多只能显示四个图标,不是表示短信只有这四种状态。通过下面属性,将它放置在MmsQuickContactBadge后面。

        android:orientation=”horizontal”

《Android短信列表源码分析》

<iconlist>

 

RelativeLayout是个组合视图,见右下数字4Text Title小图,包含3TextView,用来显示Title和消息数目、未读消息数目。在RelativeLayout,通过下面的属性设置,将RelativeLayoutindex4view)和上面的LinearLayoutindex3view)并行放置在同一行。

        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”

《Android短信列表源码分析》

<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);

    }

 

 

 

3.1.2.2                                  CheckBox

 

 

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在代码里的处理过程。

 

 

3.1.2.3                                  Avatar

 

R.id.avatar用来显示发件人头像,定义如下,MmsQuickContactBadge继承自QuickContactBadge,再往上是ImageView,一个通用的视图组件。

private MmsQuickContactBadge mAvatarView;

 

其实例获取在ConversationListItemonFinishInflate里面,

      mAvatarView = (MmsQuickContactBadge) findViewById(R.id.avatar);

 

如果希望AvatarView能够被点击,相应事件,则可以看到bind有如下代码,表示在删除模式下不能被点击,在列表模式下可以被点击。

         if (mSelectMode) {

            mAvatarView.setClickable(false);

        } else {

            mAvatarView.setClickable(true);

        }

 

AvatarView被点击之后,执行的调用栈如下,可见是由View直接调用了MmsQuickContactBadge类的onClick,并不是ConversationListConversationListItem里做了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则是通过ConversationListItemupdateAvatarView调用的(传入的number就赋值给mContactPhone),

                   mAvatarView.assignContactFromPhone(number, true);

 

updateAvatarView可以被updateFromViewbind调用,这两个方法我们在前面有过描述。所以从整个过程来看,我们可以看出avatar被点击和其需要的数据来源。

 

 

3.2         绘制分析

3.2.1         Message列表创建

ConversationListlayout描述,Messag列表主要是由list呈现,而listitem项是由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   

 

从上往下分析该栈:

1ConversationListItem.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

 

2ConversationListAdapter

ConversationListAdapter ConversationListlayout里的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创建时创建。

 

3ListView

ListView部分,我们从layoutChildren向上分析,向下部分设计到太多ListView的实现机制问题,暂不讨论。

 

layoutChildren实现ListView子窗口布局,实际上就是listlayout所占部分。

本例从launcher进入Message列表,执行代码路径如下,mSyncPosition是当前item索引值,mSpecificTop是当前layout相对Top的位置。

layoutChildren

            case LAYOUT_SYNC:

                sel = fillSpecific(mSyncPosition, mSpecificTop);

 

fillSpecific在知道位置绘制指定项的item数据,然后上下绘制其他的item。它先通过makeAndAddView完成当前item的创建并添加到layoutfillDownfillUp则根据layout大小,上下绘制item完成整个list的绘制,使用的方法也是makeAndAddViewadjustViewsUpOrDown调整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负责创建viewsetupChild负责将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将数据填充到viewmAdapter.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 

 

从下往上分析,

1ContentObserver

我们已经很熟悉handleCallback处理的是Post的消息,消息一般是runable,所以可以看到ContentObserver. dispatchChangepost这一消息的过程(dispatchChange的触发过程比较复杂,我们将在其他章节介绍,本例中当数据库内容变化时会触发这个流程)。

     public final void dispatchChange(boolean selfChange, Uri uri) {

        if (mHandler == null) {

            onChange(selfChange, uri);

        } else {

            mHandler.post(new NotificationRunnable(selfChange, uri));

        }

    }

 

然后这个消息会运行其内部类NotificationRunnablerun方法,

         public void run() {

            ContentObserver.this.onChange(mSelfChange, mUri);

        }

 

This指向的是ContentObserver的实例ChangeObserver,它是CursorAdapter的内部类,也是ContentObserver子类,由于ConversationListAdapter通过CursorWrapperContentObservable注册了ContentObserver,所以当dispatchChange事件时,通过轮询,最终将事件传递给ChangeObserver

而后,ChangeObserver调用封装类CursorAdapteronContentChanged,将事件上传。

     private class ChangeObserver extends ContentObserver {

        public void onChange(boolean selfChange) {

            onContentChanged();

        }

    }

 

 

2onContentChanged

onContentChangedCursorAdapter的一个方法,主要功能是当数据库内容更新后,重新使用游标查询数据。但这个方法被ConversationListAdapter重载,它给自己的客户提供一个借口类OnContentChangedListener,让客户去实现这个借口类,自己则调用接口类的onContentChanged方法执行客户定义的功能。

     protected void onContentChanged() {

        if (mCursor != null && !mCursor.isClosed()) {

            if (mOnContentChangedListener != null) {

                mOnContentChangedListener.onContentChanged(this);

            }

        }

    }

 

 

3ConversationList

ConversationList就是ConversationListAdapter的客户,它实现了接口类,如下,主要是调用startAsyncQuery进行数据的查询。

     private final ConversationListAdapter.OnContentChangedListener mContentChangedListener =

        new ConversationListAdapter.OnContentChangedListener() {

 

        @Override

        public void onContentChanged(ConversationListAdapter adapter) {

            if (mIsInActivity) {

                mNeedQuery = true;

                startAsyncQuery();

            }

        }

    };

 

并且ConversationList在其onStartConversationListAdapter注册了这个接口类监听器:

            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列表定位

因为ConversationListListActivity所以可以使用getListView获取ListView再使用ListViewsetSelectionFromTop将列表显示开始位置指定到特定位置。

 

需要注意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的方法更新。

 

    原文作者:Android源码分析
    原文地址: https://blog.csdn.net/xiashaohua/article/details/40555627
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞