OSDN Git Service

665c7a5040c4348a478a9f59daced44a5a37b1d7
[android-x86/packages-apps-IM.git] / src / com / android / im / app / ChatView.java
1 /*
2  * Copyright (C) 2008 Esmertec AG.
3  * Copyright (C) 2008 The Android Open Source Project
4  *
5  * Licensed under the Apache License, Version 2.0 (the "License");
6  * you may not use this file except in compliance with the License.
7  * You may obtain a copy of the License at
8  *
9  *      http://www.apache.org/licenses/LICENSE-2.0
10  *
11  * Unless required by applicable law or agreed to in writing, software
12  * distributed under the License is distributed on an "AS IS" BASIS,
13  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14  * See the License for the specific language governing permissions and
15  * limitations under the License.
16  */
17
18 package com.android.im.app;
19
20 import java.util.ArrayList;
21 import java.util.Date;
22 import java.util.Map;
23
24 import android.app.Activity;
25 import android.app.AlertDialog;
26 import android.content.AsyncQueryHandler;
27 import android.content.ContentResolver;
28 import android.content.ContentUris;
29 import android.content.Context;
30 import android.content.DialogInterface;
31 import android.content.Intent;
32 import android.content.res.Resources;
33 import android.content.res.Configuration;
34 import android.database.ContentObserver;
35 import android.database.Cursor;
36 import android.database.CursorIndexOutOfBoundsException;
37 import android.database.DataSetObserver;
38 import android.database.CharArrayBuffer;
39 import android.graphics.Typeface;
40 import android.net.Uri;
41 import android.os.Bundle;
42 import android.os.Message;
43 import android.os.RemoteException;
44 import android.provider.Browser;
45 import android.provider.Im;
46 import android.text.Editable;
47 import android.text.TextUtils;
48 import android.text.TextWatcher;
49 import android.text.style.StyleSpan;
50 import android.text.style.URLSpan;
51 import android.util.AttributeSet;
52 import android.util.Log;
53 import android.view.KeyEvent;
54 import android.view.LayoutInflater;
55 import android.view.MotionEvent;
56 import android.view.View;
57 import android.view.ViewGroup;
58 import android.view.inputmethod.InputMethodManager;
59 import android.widget.AbsListView;
60 import android.widget.AdapterView;
61 import android.widget.ArrayAdapter;
62 import android.widget.Button;
63 import android.widget.CursorAdapter;
64 import android.widget.EditText;
65 import android.widget.ImageView;
66 import android.widget.LinearLayout;
67 import android.widget.ListView;
68 import android.widget.TextView;
69 import android.widget.AbsListView.OnScrollListener;
70 import android.widget.AdapterView.OnItemClickListener;
71
72 import com.android.im.IChatListener;
73 import com.android.im.IChatSession;
74 import com.android.im.IChatSessionListener;
75 import com.android.im.IChatSessionManager;
76 import com.android.im.IContactList;
77 import com.android.im.IContactListListener;
78 import com.android.im.IContactListManager;
79 import com.android.im.IImConnection;
80 import com.android.im.R;
81 import com.android.im.app.adapter.ChatListenerAdapter;
82 import com.android.im.app.adapter.ChatSessionListenerAdapter;
83 import com.android.im.engine.Contact;
84 import com.android.im.engine.ImConnection;
85 import com.android.im.engine.ImErrorInfo;
86 import com.android.im.plugin.BrandingResourceIDs;
87
88 public class ChatView extends LinearLayout {
89     // This projection and index are set for the query of active chats
90     static final String[] CHAT_PROJECTION = {
91         Im.Contacts._ID,
92         Im.Contacts.ACCOUNT,
93         Im.Contacts.PROVIDER,
94         Im.Contacts.USERNAME,
95         Im.Contacts.NICKNAME,
96         Im.Contacts.TYPE,
97         Im.Presence.PRESENCE_STATUS,
98         Im.Chats.LAST_UNREAD_MESSAGE,
99     };
100     static final int CONTACT_ID_COLUMN             = 0;
101     static final int ACCOUNT_COLUMN                = 1;
102     static final int PROVIDER_COLUMN               = 2;
103     static final int USERNAME_COLUMN               = 3;
104     static final int NICKNAME_COLUMN               = 4;
105     static final int TYPE_COLUMN                   = 5;
106     static final int PRESENCE_STATUS_COLUMN        = 6;
107     static final int LAST_UNREAD_MESSAGE_COLUMN    = 7;
108
109     static final String[] INVITATION_PROJECT = {
110         Im.Invitation._ID,
111         Im.Invitation.PROVIDER,
112         Im.Invitation.SENDER,
113     };
114     static final int INVITATION_ID_COLUMN = 0;
115     static final int INVITATION_PROVIDER_COLUMN = 1;
116     static final int INVITATION_SENDER_COLUMN = 2;
117
118     static final StyleSpan STYLE_BOLD = new StyleSpan(Typeface.BOLD);
119
120     Markup mMarkup;
121
122     Activity mScreen;
123     ImApp mApp;
124     SimpleAlertHandler mHandler;
125     Cursor mCursor;
126
127     private ImageView   mStatusIcon;
128     private TextView    mTitle;
129     /*package*/ListView    mHistory;
130     EditText    mEdtInput;
131     private Button      mSendButton;
132     private View mStatusWarningView;
133     private ImageView mWarningIcon;
134     private TextView mWarningText;
135
136     private MessageAdapter mMessageAdapter;
137     private IChatSessionManager mChatSessionMgr;
138     private IChatSessionListener mChatSessionListener;
139
140     private IChatSession mChatSession;
141     private long mChatId;
142     int mType;
143     String mNickName;
144     String mUserName;
145     long mProviderId;
146     long mAccountId;
147     long mInvitationId;
148     private int mPresenceStatus;
149
150     private int mViewType;
151
152     private static final int VIEW_TYPE_CHAT = 1;
153     private static final int VIEW_TYPE_INVITATION = 2;
154     private static final int VIEW_TYPE_SUBSCRIPTION = 3;
155
156     private static final long SHOW_TIME_STAMP_INTERVAL = 60 * 1000;     // 1 minute
157     private static final int QUERY_TOKEN = 10;
158
159     // Async QueryHandler
160     private final class QueryHandler extends AsyncQueryHandler {
161         public QueryHandler(Context context) {
162             super(context.getContentResolver());
163         }
164
165         @Override
166         protected void onQueryComplete(int token, Object cookie, Cursor c) {
167             Cursor cursor = new DeltaCursor(c);
168
169             if (Log.isLoggable(ImApp.LOG_TAG, Log.DEBUG)){
170                 log("onQueryComplete: cursor.count=" + cursor.getCount());
171             }
172
173             mMessageAdapter.changeCursor(cursor);
174         }
175     }
176     private QueryHandler mQueryHandler;
177
178     private class RequeryCallback implements Runnable {
179         public void run() {
180             if (Log.isLoggable(ImApp.LOG_TAG, Log.DEBUG)){
181                 log("RequeryCallback");
182             }
183             requeryCursor();
184         }
185     }
186     private RequeryCallback mRequeryCallback = null;
187
188     private OnItemClickListener mOnItemClickListener = new OnItemClickListener() {
189         public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
190             if (!(view instanceof MessageView)) {
191                 return;
192             }
193             URLSpan[] links = ((MessageView)view).getMessageLinks();
194             if (links.length == 0){
195                 return;
196             }
197
198             final ArrayList<String> linkUrls = new ArrayList<String>(links.length);
199             for (URLSpan u : links) {
200                 linkUrls.add(u.getURL());
201             }
202             ArrayAdapter<String> a = new ArrayAdapter<String>(mScreen,
203                     android.R.layout.select_dialog_item, linkUrls);
204             AlertDialog.Builder b = new AlertDialog.Builder(mScreen);
205             b.setTitle(R.string.select_link_title);
206             b.setCancelable(true);
207             b.setAdapter(a, new DialogInterface.OnClickListener() {
208                 public void onClick(DialogInterface dialog, int which) {
209                     Uri uri = Uri.parse(linkUrls.get(which));
210                     Intent intent = new Intent(Intent.ACTION_VIEW, uri);
211                     intent.putExtra(Browser.EXTRA_APPLICATION_ID, mScreen.getPackageName());
212                     mScreen.startActivity(intent);
213                 }
214             });
215             b.setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() {
216                 public void onClick(DialogInterface dialog, int which) {
217                     dialog.dismiss();
218                 }
219             });
220             b.show();
221         }
222     };
223
224     private IChatListener mChatListener = new ChatListenerAdapter() {
225         @Override
226         public void onIncomingMessage(IChatSession ses,
227                 com.android.im.engine.Message msg) {
228             scheduleRequery(0);
229         }
230
231         @Override
232         public void onContactJoined(IChatSession ses, Contact contact) {
233             scheduleRequery(0);
234         }
235
236         @Override
237         public void onContactLeft(IChatSession ses, Contact contact) {
238             scheduleRequery(0);
239         }
240
241         @Override
242         public void onSendMessageError(IChatSession ses,
243                 com.android.im.engine.Message msg, ImErrorInfo error) {
244             scheduleRequery(0);
245         }
246     };
247
248     private Runnable mUpdateChatCallback = new Runnable() {
249         public void run() {
250             if (mCursor.requery() && mCursor.moveToFirst()) {
251                 updateChat();
252             }
253         }
254     };
255     private IContactListListener mContactListListener = new IContactListListener.Stub () {
256         public void onAllContactListsLoaded() {
257         }
258
259         public void onContactChange(int type, IContactList list, Contact contact){
260         }
261
262         public void onContactError(int errorType, ImErrorInfo error,
263                 String listName, Contact contact) {
264         }
265
266         public void onContactsPresenceUpdate(Contact[] contacts) {
267             if (Log.isLoggable(ImApp.LOG_TAG, Log.DEBUG)) {
268                 log("onContactsPresenceUpdate()");
269             }
270             for (Contact c : contacts) {
271                 if (c.getAddress().getFullName().equals(mUserName)) {
272                     mHandler.post(mUpdateChatCallback);
273                     scheduleRequery(0);
274                     break;
275                 }
276             }
277         }
278     };
279
280     static final void log(String msg) {
281         Log.d(ImApp.LOG_TAG, "<ChatView> " +msg);
282     }
283
284     public ChatView(Context context, AttributeSet attrs) {
285         super(context, attrs);
286         mScreen = (Activity) context;
287         mApp = ImApp.getApplication(mScreen);
288         mHandler = new ChatViewHandler();
289     }
290
291     void registerForConnEvents() {
292         mApp.registerForConnEvents(mHandler);
293     }
294
295     void unregisterForConnEvents() {
296         mApp.unregisterForConnEvents(mHandler);
297     }
298
299     @Override
300     protected void onFinishInflate() {
301         mStatusIcon     = (ImageView) findViewById(R.id.statusIcon);
302         mTitle          = (TextView) findViewById(R.id.title);
303         mHistory        = (ListView) findViewById(R.id.history);
304         mEdtInput       = (EditText) findViewById(R.id.edtInput);
305         mSendButton     = (Button)findViewById(R.id.btnSend);
306         mHistory.setOnItemClickListener(mOnItemClickListener);
307
308         mStatusWarningView = findViewById(R.id.warning);
309         mWarningIcon = (ImageView)findViewById(R.id.warningIcon);
310         mWarningText = (TextView)findViewById(R.id.warningText);
311
312         Button acceptInvitation = (Button)findViewById(R.id.btnAccept);
313         Button declineInvitation= (Button)findViewById(R.id.btnDecline);
314
315         Button approveSubscription = (Button)findViewById(R.id.btnApproveSubscription);
316         Button declineSubscription = (Button)findViewById(R.id.btnDeclineSubscription);
317
318         acceptInvitation.setOnClickListener(new OnClickListener() {
319             public void onClick(View v) {
320                 acceptInvitation();
321             }
322         });
323         declineInvitation.setOnClickListener(new OnClickListener() {
324             public void onClick(View v) {
325                 declineInvitation();
326             }
327         });
328
329         approveSubscription.setOnClickListener(new OnClickListener(){
330             public void onClick(View v) {
331                 approveSubscription();
332             }
333         });
334         declineSubscription.setOnClickListener(new OnClickListener(){
335             public void onClick(View v) {
336                 declineSubscription();
337             }
338         });
339
340         mEdtInput.setOnKeyListener(new OnKeyListener(){
341             public boolean onKey(View v, int keyCode, KeyEvent event) {
342                 if (event.getAction() == KeyEvent.ACTION_DOWN) {
343                     switch (keyCode) {
344                         case KeyEvent.KEYCODE_DPAD_CENTER:
345                             sendMessage();
346                             return true;
347
348                         case KeyEvent.KEYCODE_ENTER:
349                             if (event.isAltPressed()) {
350                                 mEdtInput.append("\n");
351                                 return true;
352                             }
353                     }
354                 }
355                 return false;
356             }
357         });
358
359         mEdtInput.setOnEditorActionListener(new TextView.OnEditorActionListener() {
360             public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
361                 if (event != null) {
362                     if (event.isAltPressed()) {
363                         return false;
364                     }
365                 }
366
367                 sendMessage();
368                 return true;
369             }
370         });
371
372         // TODO: this is a hack to implement BUG #1611278, when dispatchKeyEvent() works with
373         // the soft keyboard, we should remove this hack.
374         mEdtInput.addTextChangedListener(new TextWatcher() {
375             public void beforeTextChanged(CharSequence s, int start, int before, int after) {
376             }
377
378             public void onTextChanged(CharSequence s, int start, int before, int after) {
379                 //log("TextWatcher: " + s);
380                 userActionDetected();
381             }
382
383             public void afterTextChanged(Editable s) {
384             }
385         });
386
387         mSendButton.setOnClickListener(new OnClickListener() {
388             public void onClick(View v) {
389                 sendMessage();
390             }
391         });
392     }
393
394     public void onResume(){
395         if (mViewType == VIEW_TYPE_CHAT) {
396             Cursor cursor = getMessageCursor();
397             if (cursor == null) {
398                 startQuery();
399             } else {
400                 requeryCursor();
401             }
402             updateWarningView();
403         }
404         registerChatListener();
405         registerForConnEvents();
406     }
407
408     public void onPause(){
409         Cursor cursor = getMessageCursor();
410         if (cursor != null) {
411             cursor.deactivate();
412         }
413         cancelRequery();
414         if (mViewType == VIEW_TYPE_CHAT && mChatSession != null) {
415             try {
416                 mChatSession.markAsRead();
417             } catch (RemoteException e) {
418                 mHandler.showServiceErrorAlert();
419             }
420         }
421         unregisterChatListener();
422         unregisterForConnEvents();
423         unregisterChatSessionListener();
424     }
425
426     private void closeSoftKeyboard() {
427         InputMethodManager inputMethodManager =
428             (InputMethodManager)mApp.getSystemService(Context.INPUT_METHOD_SERVICE);
429
430         inputMethodManager.hideSoftInputFromWindow(mEdtInput.getWindowToken(), 0);
431     }
432
433     void updateChat() {
434         setViewType(VIEW_TYPE_CHAT);
435
436         long oldChatId = mChatId;
437
438         updateContactInfo();
439
440         setStatusIcon();
441         setTitle();
442
443         IImConnection conn = mApp.getConnection(mProviderId);
444         if (conn == null) {
445             if (Log.isLoggable(ImApp.LOG_TAG, Log.DEBUG)) log("Connection has been signed out");
446             mScreen.finish();
447             return;
448         }
449
450         BrandingResources brandingRes = mApp.getBrandingResource(mProviderId);
451         mHistory.setBackgroundDrawable(
452                 brandingRes.getDrawable(BrandingResourceIDs.DRAWABLE_CHAT_WATERMARK));
453
454         if (mMarkup == null) {
455             mMarkup = new Markup(brandingRes);
456         }
457
458         if (mMessageAdapter == null) {
459             mMessageAdapter = new MessageAdapter(mScreen, null);
460             mHistory.setAdapter(mMessageAdapter);
461         }
462
463         // only change the message adapter when we switch to another chat
464         if (mChatId != oldChatId) {
465             startQuery();
466             mEdtInput.setText("");
467         }
468
469         updateWarningView();
470     }
471
472     private void updateContactInfo() {
473         mChatId = mCursor.getLong(CONTACT_ID_COLUMN);
474         mProviderId = mCursor.getLong(PROVIDER_COLUMN);
475         mAccountId = mCursor.getLong(ACCOUNT_COLUMN);
476         mPresenceStatus = mCursor.getInt(PRESENCE_STATUS_COLUMN);
477         mType = mCursor.getInt(TYPE_COLUMN);
478         mUserName = mCursor.getString(USERNAME_COLUMN);
479         mNickName = mCursor.getString(NICKNAME_COLUMN);
480     }
481
482     private void setTitle() {
483         if (mType == Im.Contacts.TYPE_GROUP) {
484             final String[] projection = {Im.GroupMembers.NICKNAME};
485             Uri memberUri = ContentUris.withAppendedId(Im.GroupMembers.CONTENT_URI, mChatId);
486             ContentResolver cr = mScreen.getContentResolver();
487             Cursor c = cr.query(memberUri, projection, null, null, null);
488             StringBuilder buf = new StringBuilder();
489             if(c != null) {
490                 while(c.moveToNext()) {
491                     buf.append(c.getString(0));
492                     if(!c.isLast()) {
493                         buf.append(',');
494                     }
495                 }
496                 c.close();
497             }
498             mTitle.setText(mContext.getString(R.string.chat_with, buf.toString()));
499         } else {
500             mTitle.setText(mContext.getString(R.string.chat_with, mNickName));
501         }
502     }
503
504     private void setStatusIcon() {
505         if (mType == Im.Contacts.TYPE_GROUP) {
506             // hide the status icon for group chat.
507             mStatusIcon.setVisibility(GONE);
508         } else {
509             mStatusIcon.setVisibility(VISIBLE);
510             BrandingResources brandingRes = mApp.getBrandingResource(mProviderId);
511             int presenceResId = PresenceUtils.getStatusIconId(mPresenceStatus);
512             mStatusIcon.setImageDrawable(brandingRes.getDrawable(presenceResId));
513         }
514     }
515
516     public void bindChat(long chatId) {
517         if (mCursor != null) {
518             mCursor.deactivate();
519         }
520         Uri contactUri = ContentUris.withAppendedId(Im.Contacts.CONTENT_URI, chatId);
521         mCursor = mScreen.managedQuery(contactUri, CHAT_PROJECTION, null, null);
522         if (mCursor == null || !mCursor.moveToFirst()) {
523             if (Log.isLoggable(ImApp.LOG_TAG, Log.DEBUG)){
524                 log("Failed to query chat: " + chatId);
525             }
526             mScreen.finish();
527             return;
528         } else {
529             mChatSession = getChatSession(mCursor);
530             updateChat();
531             registerChatListener();
532         }
533     }
534
535     public void bindInvitation(long invitationId) {
536         Uri uri = ContentUris.withAppendedId(Im.Invitation.CONTENT_URI, invitationId);
537         ContentResolver cr = mScreen.getContentResolver();
538         Cursor cursor = cr.query(uri, INVITATION_PROJECT, null, null, null);
539         if (cursor == null || !cursor.moveToFirst()) {
540             if (Log.isLoggable(ImApp.LOG_TAG, Log.DEBUG)){
541                 log("Failed to query invitation: " + invitationId);
542             }
543             mScreen.finish();
544         } else {
545             setViewType(VIEW_TYPE_INVITATION);
546
547             mInvitationId = cursor.getLong(INVITATION_ID_COLUMN);
548             mProviderId = cursor.getLong(INVITATION_PROVIDER_COLUMN);
549             String sender = cursor.getString(INVITATION_SENDER_COLUMN);
550
551             TextView mInvitationText = (TextView)findViewById(R.id.txtInvitation);
552             mInvitationText.setText(mContext.getString(R.string.invitation_prompt, sender));
553             mTitle.setText(mContext.getString(R.string.chat_with, sender));
554         }
555
556         if (cursor != null) {
557             cursor.close();
558         }
559     }
560
561     public void bindSubscription(long providerId, String from) {
562         mProviderId = providerId;
563         mUserName = from;
564
565         setViewType(VIEW_TYPE_SUBSCRIPTION);
566
567         TextView text =  (TextView)findViewById(R.id.txtSubscription);
568         String displayableAddr = ImpsAddressUtils.getDisplayableAddress(from);
569         text.setText(mContext.getString(R.string.subscription_prompt, displayableAddr));
570         mTitle.setText(mContext.getString(R.string.chat_with, displayableAddr));
571
572         mApp.dismissChatNotification(providerId, from);
573     }
574
575     void acceptInvitation() {
576         try {
577
578             IImConnection conn = mApp.getConnection(mProviderId);
579             if (conn != null) {
580                 // register a chat session listener and wait for a group chat
581                 // session to be created after we accept the invitation.
582                 registerChatSessionListener();
583                 conn.acceptInvitation(mInvitationId);
584             }
585         } catch (RemoteException e) {
586             mHandler.showServiceErrorAlert();
587         }
588     }
589
590     void declineInvitation() {
591         try {
592             IImConnection conn = mApp.getConnection(mProviderId);
593             if (conn != null) {
594                 conn.rejectInvitation(mInvitationId);
595             }
596             mScreen.finish();
597         } catch (RemoteException e) {
598             mHandler.showServiceErrorAlert();
599         }
600     }
601
602     void approveSubscription() {
603         IImConnection conn = mApp.getConnection(mProviderId);
604         try {
605             IContactListManager manager = conn.getContactListManager();
606             manager.approveSubscription(mUserName);
607         } catch (RemoteException ex) {
608             mHandler.showServiceErrorAlert();
609         }
610         mScreen.finish();
611     }
612
613     void declineSubscription() {
614         IImConnection conn = mApp.getConnection(mProviderId);
615         try {
616             IContactListManager manager = conn.getContactListManager();
617             manager.declineSubscription(mUserName);
618         } catch (RemoteException ex) {
619             mHandler.showServiceErrorAlert();
620         }
621         mScreen.finish();
622     }
623
624     private void setViewType(int type) {
625         mViewType = type;
626         if (type == VIEW_TYPE_CHAT) {
627             findViewById(R.id.invitationPanel).setVisibility(GONE);
628             findViewById(R.id.subscription).setVisibility(GONE);
629             setChatViewEnabled(true);
630         }  else if (type == VIEW_TYPE_INVITATION) {
631             setChatViewEnabled(false);
632             findViewById(R.id.invitationPanel).setVisibility(VISIBLE);
633             findViewById(R.id.btnAccept).requestFocus();
634         } else if (type == VIEW_TYPE_SUBSCRIPTION) {
635             setChatViewEnabled(false);
636             findViewById(R.id.subscription).setVisibility(VISIBLE);
637             findViewById(R.id.btnApproveSubscription).requestFocus();
638         }
639     }
640
641     private void setChatViewEnabled(boolean enabled) {
642         mEdtInput.setEnabled(enabled);
643         mSendButton.setEnabled(enabled);
644         if (enabled) {
645             mEdtInput.requestFocus();
646         } else {
647             mHistory.setAdapter(null);
648         }
649     }
650
651     private void startQuery() {
652         if (mQueryHandler == null) {
653             mQueryHandler = new QueryHandler(mContext);
654         } else {
655             // Cancel any pending queries
656             mQueryHandler.cancelOperation(QUERY_TOKEN);
657         }
658
659         Uri uri;
660         if (Im.Contacts.TYPE_GROUP == mType) {
661             uri = ContentUris.withAppendedId(Im.GroupMessages.CONTENT_URI_GROUP_MESSAGES_BY, mChatId);
662         } else {
663             uri = Im.Messages.getContentUriByContact(mProviderId, mAccountId, mUserName);
664         }
665
666         if (Log.isLoggable(ImApp.LOG_TAG, Log.DEBUG)){
667             log("queryCursor: uri=" + uri);
668         }
669
670         mQueryHandler.startQuery(QUERY_TOKEN, null,
671                 uri,
672                 null,
673                 null /* selection */,
674                 null /* selection args */,
675                 null);
676     }
677
678     void scheduleRequery(long interval) {
679         if (mRequeryCallback == null) {
680             mRequeryCallback = new RequeryCallback();
681         } else {
682             mHandler.removeCallbacks(mRequeryCallback);
683         }
684
685         if (Log.isLoggable(ImApp.LOG_TAG, Log.DEBUG)){
686             log("scheduleRequery");
687         }
688         mHandler.postDelayed(mRequeryCallback, interval);
689     }
690
691     void cancelRequery() {
692         if (mRequeryCallback != null) {
693             if (Log.isLoggable(ImApp.LOG_TAG, Log.DEBUG)){
694                 log("cancelRequery");
695             }
696             mHandler.removeCallbacks(mRequeryCallback);
697             mRequeryCallback = null;
698         }
699     }
700
701     void requeryCursor() {
702         if (mMessageAdapter.isScrolling()) {
703             mMessageAdapter.setNeedRequeryCursor(true);
704             return;
705         }
706         // TODO: async query?
707         Cursor cursor = getMessageCursor();
708         if (cursor != null) {
709             cursor.requery();
710         }
711     }
712
713     private Cursor getMessageCursor() {
714         return mMessageAdapter == null ? null : mMessageAdapter.getCursor();
715     }
716
717     public void insertSmiley(String smiley) {
718         mEdtInput.append(mMarkup.applyEmoticons(smiley));
719     }
720
721     public void closeChatSession() {
722         if (mChatSession != null) {
723             try {
724                 mChatSession.leave();
725             } catch (RemoteException e) {
726                 mHandler.showServiceErrorAlert();
727             }
728         } else {
729             // the conversation is already closed, clear data in database
730             ContentResolver cr = mContext.getContentResolver();
731             cr.delete(ContentUris.withAppendedId(Im.Chats.CONTENT_URI, mChatId),
732                     null, null);
733         }
734         mScreen.finish();
735     }
736
737     public void closeChatSessionIfInactive() {
738         if (mChatSession != null) {
739             try {
740                 mChatSession.leaveIfInactive();
741             } catch (RemoteException e) {
742                 mHandler.showServiceErrorAlert();
743             }
744         }
745     }
746
747     public void viewProfile() {
748         Uri data = ContentUris.withAppendedId(Im.Contacts.CONTENT_URI, mChatId);
749         Intent intent = new Intent(Intent.ACTION_VIEW, data);
750         mScreen.startActivity(intent);
751     }
752
753     public void blockContact() {
754         // TODO: unify with codes in ContactListView
755         DialogInterface.OnClickListener confirmListener = new DialogInterface.OnClickListener(){
756             public void onClick(DialogInterface dialog, int whichButton) {
757                 try {
758                     IImConnection conn = mApp.getConnection(mProviderId);
759                     IContactListManager manager = conn.getContactListManager();
760                     manager.blockContact(mUserName);
761                     mScreen.finish();
762                 } catch (RemoteException e) {
763                     mHandler.showServiceErrorAlert();
764                 }
765             }
766         };
767
768         Resources r = getResources();
769
770         // The positive button is deliberately set as no so that
771         // the no is the default value
772         new AlertDialog.Builder(mContext)
773             .setTitle(R.string.confirm)
774             .setMessage(r.getString(R.string.confirm_block_contact, mNickName))
775             .setPositiveButton(R.string.yes, confirmListener) // default button
776             .setNegativeButton(R.string.no, null)
777             .setCancelable(false)
778             .show();
779     }
780
781     public long getProviderId() {
782         return mProviderId;
783     }
784
785     public long getAccountId() {
786         return mAccountId;
787     }
788
789     public String getUserName() {
790         return mUserName;
791     }
792
793     public long getChatId () {
794         try {
795             return mChatSession == null ? -1 : mChatSession.getId();
796         } catch (RemoteException e) {
797             mHandler.showServiceErrorAlert();
798             return -1;
799         }
800     }
801
802     public IChatSession getCurrentChatSession() {
803         return mChatSession;
804     }
805
806     private IChatSessionManager getChatSessionManager(long providerId) {
807         if (mChatSessionMgr == null) {
808             IImConnection conn = mApp.getConnection(providerId);
809             if (conn != null) {
810                 try {
811                     mChatSessionMgr = conn.getChatSessionManager();
812                 } catch (RemoteException e) {
813                     mHandler.showServiceErrorAlert();
814                 }
815             }
816         }
817         return mChatSessionMgr;
818     }
819
820     private IChatSession getChatSession(Cursor cursor) {
821         long providerId = cursor.getLong(PROVIDER_COLUMN);
822         String username = cursor.getString(USERNAME_COLUMN);
823
824         IChatSessionManager sessionMgr = getChatSessionManager(providerId);
825         if (sessionMgr != null) {
826             try {
827                 return sessionMgr.getChatSession(username);
828             } catch (RemoteException e) {
829                 mHandler.showServiceErrorAlert();
830             }
831         }
832         return null;
833     }
834
835     boolean isGroupChat() {
836         return Im.Contacts.TYPE_GROUP == mType;
837     }
838
839     void sendMessage() {
840         String msg = mEdtInput.getText().toString();
841
842         if (TextUtils.isEmpty(msg.trim())) {
843             return;
844         }
845
846         if (mChatSession != null) {
847             try {
848                 mChatSession.sendMessage(msg);
849                 mEdtInput.setText("");
850                 mEdtInput.requestFocus();
851                 requeryCursor();
852             } catch (RemoteException e) {
853                 mHandler.showServiceErrorAlert();
854             }
855         }
856
857         // Close the soft on-screen keyboard if we're in landscape mode so the user can see the
858         // conversation.
859         Configuration config = getResources().getConfiguration();
860         if (config.orientation == config.ORIENTATION_LANDSCAPE) {
861             closeSoftKeyboard();
862         }
863     }
864
865     void registerChatListener() {
866         if (Log.isLoggable(ImApp.LOG_TAG, Log.DEBUG)){
867             log("registerChatListener");
868         }
869         try {
870             if (mChatSession != null) {
871                 mChatSession.registerChatListener(mChatListener);
872             }
873             IImConnection conn = mApp.getConnection(mProviderId);
874             if (conn != null) {
875                 IContactListManager listMgr = conn.getContactListManager();
876                 listMgr.registerContactListListener(mContactListListener);
877             }
878             mApp.dismissChatNotification(mProviderId, mUserName);
879         } catch (RemoteException e) {
880             Log.w(ImApp.LOG_TAG, "<ChatView> registerChatListener fail:" + e.getMessage());
881         }
882     }
883
884     void unregisterChatListener() {
885         if (Log.isLoggable(ImApp.LOG_TAG, Log.DEBUG)){
886             log("unregisterChatListener");
887         }
888         try {
889             if (mChatSession != null) {
890                 mChatSession.unregisterChatListener(mChatListener);
891             }
892             IImConnection conn = mApp.getConnection(mProviderId);
893             if (conn != null) {
894                 IContactListManager listMgr = conn.getContactListManager();
895                 listMgr.unregisterContactListListener(mContactListListener);
896             }
897         } catch (RemoteException e) {
898             Log.w(ImApp.LOG_TAG, "<ChatView> unregisterChatListener fail:" + e.getMessage());
899         }
900     }
901
902     void registerChatSessionListener() {
903         IChatSessionManager sessionMgr = getChatSessionManager(mProviderId);
904         if (sessionMgr != null) {
905             mChatSessionListener = new ChatSessionListener();
906             try {
907                 sessionMgr.registerChatSessionListener(mChatSessionListener);
908             } catch (RemoteException e) {
909                 mHandler.showServiceErrorAlert();
910             }
911         }
912     }
913
914     void unregisterChatSessionListener() {
915         if (mChatSessionListener != null) {
916             try {
917                 IChatSessionManager sessionMgr = getChatSessionManager(mProviderId);
918                 sessionMgr.unregisterChatSessionListener(mChatSessionListener);
919                 // We unregister the listener when the chat session we are
920                 // waiting for has been created or the activity is stopped.
921                 // Clear the listener so that we won't unregister the listener
922                 // twice.
923                 mChatSessionListener = null;
924             } catch (RemoteException e) {
925                 mHandler.showServiceErrorAlert();
926             }
927         }
928     }
929
930     void updateWarningView() {
931         int visibility = View.GONE;
932         int iconVisibility = View.GONE;
933         String message = null;
934         boolean isConnected;
935
936         try {
937             IImConnection conn = mApp.getConnection(mProviderId);
938             isConnected = (conn == null) ? false
939                     : conn.getState() != ImConnection.SUSPENDED;
940         } catch (RemoteException e) {
941             // do nothing
942             return;
943         }
944
945         if (isConnected) {
946             if (mType == Im.Contacts.TYPE_TEMPORARY) {
947                 visibility = View.VISIBLE;
948                 message = mContext.getString(R.string.contact_not_in_list_warning, mNickName);
949             } else if (mPresenceStatus == Im.Presence.OFFLINE) {
950                 visibility = View.VISIBLE;
951                 message = mContext.getString(R.string.contact_offline_warning, mNickName);
952             }
953         } else {
954             visibility = View.VISIBLE;
955             iconVisibility = View.VISIBLE;
956             message = mContext.getString(R.string.disconnected_warning);
957         }
958
959         mStatusWarningView.setVisibility(visibility);
960         if (visibility == View.VISIBLE) {
961             mWarningIcon.setVisibility(iconVisibility);
962             mWarningText.setText(message);
963         }
964     }
965
966     @Override
967     public boolean dispatchKeyEvent(KeyEvent event) {
968         userActionDetected();
969         return super.dispatchKeyEvent(event);
970     }
971
972     @Override
973     public boolean dispatchTouchEvent(MotionEvent ev) {
974         userActionDetected();
975         return super.dispatchTouchEvent(ev);
976     }
977
978     @Override
979     public boolean dispatchTrackballEvent(MotionEvent ev) {
980         userActionDetected();
981         return super.dispatchTrackballEvent(ev);
982     }
983
984     private void userActionDetected() {
985         if (mChatSession != null) {
986             try {
987                 mChatSession.markAsRead();
988             } catch (RemoteException e) {
989                 mHandler.showServiceErrorAlert();
990             }
991         }
992     }
993
994     private final class ChatViewHandler extends SimpleAlertHandler {
995         public ChatViewHandler() {
996             super(mScreen);
997         }
998
999         @Override
1000         public void handleMessage(Message msg) {
1001             long providerId = ((long)msg.arg1 << 32) | msg.arg2;
1002             if (providerId != mProviderId) {
1003                 return;
1004             }
1005
1006             switch(msg.what) {
1007             case ImApp.EVENT_CONNECTION_LOGGED_IN:
1008                 log("Connection resumed");
1009                 updateWarningView();
1010                 return;
1011             case ImApp.EVENT_CONNECTION_SUSPENDED:
1012                 log("Connection suspended");
1013                 updateWarningView();
1014                 return;
1015             }
1016
1017             super.handleMessage(msg);
1018         }
1019     }
1020
1021     class ChatSessionListener extends ChatSessionListenerAdapter {
1022         @Override
1023         public void onChatSessionCreated(IChatSession session) {
1024             try {
1025                 if (session.isGroupChatSession()) {
1026                     final long id = session.getId();
1027                     unregisterChatSessionListener();
1028                     mHandler.post(new Runnable() {
1029                         public void run() {
1030                             bindChat(id);
1031                         }});
1032                 }
1033             } catch (RemoteException e) {
1034                 mHandler.showServiceErrorAlert();
1035             }
1036         }
1037     }
1038
1039     public static class DeltaCursor implements Cursor {
1040         static final String DELTA_COLUMN_NAME = "delta";
1041
1042         private Cursor mInnerCursor;
1043         private String[] mColumnNames;
1044         private int mDateColumn = -1;
1045         private int mDeltaColumn = -1;
1046
1047         DeltaCursor(Cursor cursor) {
1048             mInnerCursor = cursor;
1049
1050             String[] columnNames = cursor.getColumnNames();
1051             int len = columnNames.length;
1052
1053             mColumnNames = new String[len + 1];
1054
1055             for (int i = 0 ; i < len ; i++) {
1056                 mColumnNames[i] = columnNames[i];
1057                 if (mColumnNames[i].equals(Im.BaseMessageColumns.DATE)) {
1058                     mDateColumn = i;
1059                 }
1060             }
1061
1062             mDeltaColumn = len;
1063             mColumnNames[mDeltaColumn] = DELTA_COLUMN_NAME;
1064
1065             //if (DBG) log("##### DeltaCursor constructor: mDeltaColumn=" +
1066             //        mDeltaColumn + ", columnName=" + mColumnNames[mDeltaColumn]);
1067         }
1068
1069         public int getCount() {
1070             return mInnerCursor.getCount();
1071         }
1072
1073         public int getPosition() {
1074             return mInnerCursor.getPosition();
1075         }
1076
1077         public boolean move(int offset) {
1078             return mInnerCursor.move(offset);
1079         }
1080
1081         public boolean moveToPosition(int position) {
1082             return mInnerCursor.moveToPosition(position);
1083         }
1084
1085         public boolean moveToFirst() {
1086             return mInnerCursor.moveToFirst();
1087         }
1088
1089         public boolean moveToLast() {
1090             return mInnerCursor.moveToLast();
1091         }
1092
1093         public boolean moveToNext() {
1094             return mInnerCursor.moveToNext();
1095         }
1096
1097         public boolean moveToPrevious() {
1098             return mInnerCursor.moveToPrevious();
1099         }
1100
1101         public boolean isFirst() {
1102             return mInnerCursor.isFirst();
1103         }
1104
1105         public boolean isLast() {
1106             return mInnerCursor.isLast();
1107         }
1108
1109         public boolean isBeforeFirst() {
1110             return mInnerCursor.isBeforeFirst();
1111         }
1112
1113         public boolean isAfterLast() {
1114             return mInnerCursor.isAfterLast();
1115         }
1116
1117         public boolean deleteRow() {
1118             return mInnerCursor.deleteRow();
1119         }
1120
1121         public int getColumnIndex(String columnName) {
1122             if (DELTA_COLUMN_NAME.equals(columnName)) {
1123                 return mDeltaColumn;
1124             }
1125
1126             int columnIndex = mInnerCursor.getColumnIndex(columnName);
1127             return columnIndex;
1128         }
1129
1130         public int getColumnIndexOrThrow(String columnName) throws IllegalArgumentException {
1131             if (DELTA_COLUMN_NAME.equals(columnName)) {
1132                 return mDeltaColumn;
1133             }
1134
1135             return mInnerCursor.getColumnIndexOrThrow(columnName);
1136         }
1137
1138         public String getColumnName(int columnIndex) {
1139             if (columnIndex == mDeltaColumn) {
1140                 return DELTA_COLUMN_NAME;
1141             }
1142
1143             return mInnerCursor.getColumnName(columnIndex);
1144         }
1145
1146         public int getColumnCount() {
1147             return mInnerCursor.getColumnCount() + 1;
1148         }
1149
1150         public boolean supportsUpdates() {
1151             return mInnerCursor.supportsUpdates();
1152         }
1153
1154         public boolean hasUpdates() {
1155             return mInnerCursor.hasUpdates();
1156         }
1157
1158         public boolean updateBlob(int columnIndex, byte[] value) {
1159             if (columnIndex == mDeltaColumn) {
1160                 return false;
1161             }
1162
1163             return mInnerCursor.updateBlob(columnIndex, value);
1164         }
1165
1166         public boolean updateString(int columnIndex, String value) {
1167             if (columnIndex == mDeltaColumn) {
1168                 return false;
1169             }
1170
1171             return mInnerCursor.updateString(columnIndex, value);
1172         }
1173
1174         public boolean updateShort(int columnIndex, short value) {
1175             if (columnIndex == mDeltaColumn) {
1176                 return false;
1177             }
1178
1179             return mInnerCursor.updateShort(columnIndex, value);
1180         }
1181
1182         public boolean updateInt(int columnIndex, int value) {
1183             if (columnIndex == mDeltaColumn) {
1184                 return false;
1185             }
1186
1187             return mInnerCursor.updateInt(columnIndex, value);
1188         }
1189
1190         public boolean updateLong(int columnIndex, long value) {
1191             if (columnIndex == mDeltaColumn) {
1192                 return false;
1193             }
1194
1195             return mInnerCursor.updateLong(columnIndex, value);
1196         }
1197
1198         public boolean updateFloat(int columnIndex, float value) {
1199             if (columnIndex == mDeltaColumn) {
1200                 return false;
1201             }
1202
1203             return mInnerCursor.updateFloat(columnIndex, value);
1204         }
1205
1206         public boolean updateDouble(int columnIndex, double value) {
1207             if (columnIndex == mDeltaColumn) {
1208                 return false;
1209             }
1210
1211             return mInnerCursor.updateDouble(columnIndex, value);
1212         }
1213
1214         public boolean updateToNull(int columnIndex) {
1215             if (columnIndex == mDeltaColumn) {
1216                 return false;
1217             }
1218
1219             return mInnerCursor.updateToNull(columnIndex);
1220         }
1221
1222         public boolean commitUpdates() {
1223             return mInnerCursor.commitUpdates();
1224         }
1225
1226         public boolean commitUpdates(Map<? extends Long,
1227                 ? extends Map<String,Object>> values) {
1228             return mInnerCursor.commitUpdates(values);
1229         }
1230
1231         public void abortUpdates() {
1232             mInnerCursor.abortUpdates();
1233         }
1234
1235         public void deactivate() {
1236             mInnerCursor.deactivate();
1237         }
1238
1239         public boolean requery() {
1240             return mInnerCursor.requery();
1241         }
1242
1243         public void close() {
1244             mInnerCursor.close();
1245         }
1246
1247         public boolean isClosed() {
1248             return mInnerCursor.isClosed();
1249         }
1250
1251         public void registerContentObserver(ContentObserver observer) {
1252             mInnerCursor.registerContentObserver(observer);
1253         }
1254
1255         public void unregisterContentObserver(ContentObserver observer) {
1256             mInnerCursor.unregisterContentObserver(observer);
1257         }
1258
1259         public void registerDataSetObserver(DataSetObserver observer) {
1260             mInnerCursor.registerDataSetObserver(observer);
1261         }
1262
1263         public void unregisterDataSetObserver(DataSetObserver observer) {
1264             mInnerCursor.unregisterDataSetObserver(observer);
1265         }
1266
1267         public void setNotificationUri(ContentResolver cr, Uri uri) {
1268             mInnerCursor.setNotificationUri(cr, uri);
1269         }
1270
1271         public boolean getWantsAllOnMoveCalls() {
1272             return mInnerCursor.getWantsAllOnMoveCalls();
1273         }
1274
1275         public Bundle getExtras() {
1276             return mInnerCursor.getExtras();
1277         }
1278
1279         public Bundle respond(Bundle extras) {
1280             return mInnerCursor.respond(extras);
1281         }
1282
1283         public String[] getColumnNames() {
1284             return mColumnNames;
1285         }
1286
1287         private void checkPosition() {
1288             int pos = mInnerCursor.getPosition();
1289             int count = mInnerCursor.getCount();
1290
1291             if (-1 == pos || count == pos) {
1292                 throw new CursorIndexOutOfBoundsException(pos, count);
1293             }
1294         }
1295
1296         public byte[] getBlob(int column) {
1297             checkPosition();
1298
1299             if (column == mDeltaColumn) {
1300                 return null;
1301             }
1302
1303             return mInnerCursor.getBlob(column);
1304         }
1305
1306         public String getString(int column) {
1307             checkPosition();
1308
1309             if (column == mDeltaColumn) {
1310                 long value = getDeltaValue();
1311                 return Long.toString(value);
1312             }
1313
1314             return mInnerCursor.getString(column);
1315         }
1316
1317         public void copyStringToBuffer(int columnIndex, CharArrayBuffer buffer) {
1318             checkPosition();
1319
1320             if (columnIndex == mDeltaColumn) {
1321                 long value = getDeltaValue();
1322                 String strValue = Long.toString(value);
1323                 int len = strValue.length();
1324                 char[] data = buffer.data;
1325                 if (data == null || data.length < len) {
1326                     buffer.data = strValue.toCharArray();
1327                 } else {
1328                     strValue.getChars(0, len, data, 0);
1329                 }
1330                 buffer.sizeCopied = strValue.length();
1331             } else {
1332                 mInnerCursor.copyStringToBuffer(columnIndex, buffer);
1333             }
1334         }
1335
1336         public short getShort(int column) {
1337             checkPosition();
1338
1339             if (column == mDeltaColumn) {
1340                 return (short)getDeltaValue();
1341             }
1342
1343             return mInnerCursor.getShort(column);
1344         }
1345
1346         public int getInt(int column) {
1347             checkPosition();
1348
1349             if (column == mDeltaColumn) {
1350                 return (int)getDeltaValue();
1351             }
1352
1353             return mInnerCursor.getInt(column);
1354         }
1355
1356         public long getLong(int column) {
1357         //if (DBG) log("DeltaCursor.getLong: column=" + column + ", mDeltaColumn=" + mDeltaColumn);
1358             checkPosition();
1359
1360             if (column == mDeltaColumn) {
1361                 return getDeltaValue();
1362             }
1363
1364             return mInnerCursor.getLong(column);
1365         }
1366
1367         public float getFloat(int column) {
1368             checkPosition();
1369
1370             if (column == mDeltaColumn) {
1371                 return getDeltaValue();
1372             }
1373
1374             return mInnerCursor.getFloat(column);
1375         }
1376
1377         public double getDouble(int column) {
1378             checkPosition();
1379
1380             if (column == mDeltaColumn) {
1381                 return getDeltaValue();
1382             }
1383
1384             return mInnerCursor.getDouble(column);
1385         }
1386
1387         public boolean isNull(int column) {
1388             checkPosition();
1389
1390             if (column == mDeltaColumn) {
1391                 return false;
1392             }
1393
1394             return mInnerCursor.isNull(column);
1395         }
1396
1397         private long getDeltaValue() {
1398             int pos = mInnerCursor.getPosition();
1399             //Log.i(LOG_TAG, "getDeltaValue: mPos=" + mPos);
1400
1401             long t2, t1;
1402
1403             if (pos == getCount()-1) {
1404                 t1 = mInnerCursor.getLong(mDateColumn);
1405                 t2 = System.currentTimeMillis();
1406             } else {
1407                 mInnerCursor.moveToPosition(pos + 1);
1408                 t2 = mInnerCursor.getLong(mDateColumn);
1409                 mInnerCursor.moveToPosition(pos);
1410                 t1 = mInnerCursor.getLong(mDateColumn);
1411             }
1412
1413             return t2 - t1;
1414         }
1415     }
1416
1417     private class MessageAdapter extends CursorAdapter implements AbsListView.OnScrollListener {
1418         private int mScrollState;
1419         private boolean mNeedRequeryCursor;
1420
1421         private int mContactColumn;
1422         private int mBodyColumn;
1423         private int mDateColumn;
1424         private int mTypeColumn;
1425         private int mErrCodeColumn;
1426         private int mDeltaColumn;
1427         private ChatBackgroundMaker mBgMaker;
1428
1429         private LayoutInflater mInflater;
1430
1431         public MessageAdapter(Activity context, Cursor c) {
1432             super(context, c, false);
1433             mInflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
1434             mBgMaker = new ChatBackgroundMaker(context);
1435             if (c != null) {
1436                 resolveColumnIndex(c);
1437             }
1438         }
1439
1440         private void resolveColumnIndex(Cursor c) {
1441             mContactColumn = c.getColumnIndexOrThrow(Im.BaseMessageColumns.CONTACT);
1442             mBodyColumn = c.getColumnIndexOrThrow(Im.BaseMessageColumns.BODY);
1443             mDateColumn = c.getColumnIndexOrThrow(Im.BaseMessageColumns.DATE);
1444             mTypeColumn = c.getColumnIndexOrThrow(Im.BaseMessageColumns.TYPE);
1445             mErrCodeColumn = c.getColumnIndexOrThrow(Im.BaseMessageColumns.ERROR_CODE);
1446             mDeltaColumn = c.getColumnIndexOrThrow(DeltaCursor.DELTA_COLUMN_NAME);
1447         }
1448
1449         @Override
1450         public void changeCursor(Cursor cursor) {
1451             super.changeCursor(cursor);
1452             if (cursor != null) {
1453                 resolveColumnIndex(cursor);
1454             }
1455         }
1456
1457         @Override
1458         public View newView(Context context, Cursor cursor, ViewGroup parent) {
1459             return mInflater.inflate(R.layout.new_message_item, parent, false);
1460         }
1461
1462         @Override
1463         public void bindView(View view, Context context, Cursor cursor) {
1464             MessageView chatMsgView = (MessageView) view;
1465
1466             int type = cursor.getInt(mTypeColumn);
1467             String contact = isGroupChat() ? cursor.getString(mContactColumn) : mNickName;
1468             String body = cursor.getString(mBodyColumn);
1469             long delta = cursor.getLong(mDeltaColumn);
1470             boolean showTimeStamp = (delta > SHOW_TIME_STAMP_INTERVAL);
1471             Date date = showTimeStamp ? new Date(cursor.getLong(mDateColumn)) : null;
1472
1473             switch (type) {
1474                 case Im.MessageType.INCOMING:
1475                     chatMsgView.bindIncomingMessage(contact, body, date, mMarkup, isScrolling());
1476                     break;
1477
1478                 case Im.MessageType.OUTGOING:
1479                 case Im.MessageType.POSTPONED:
1480                     int errCode = cursor.getInt(mErrCodeColumn);
1481                     if (errCode != 0) {
1482                         chatMsgView.bindErrorMessage(errCode);
1483                     } else {
1484                         chatMsgView.bindOutgoingMessage(body, date, mMarkup, isScrolling());
1485                     }
1486                     break;
1487
1488                 default:
1489                     chatMsgView.bindPresenceMessage(contact, type, isGroupChat(), isScrolling());
1490             }
1491             if (!isScrolling()) {
1492                 mBgMaker.setBackground(chatMsgView, contact, type);
1493             }
1494
1495             // if showTimeStamp is false for the latest message, then set a timer to query the
1496             // cursor again in a minute, so we can update the last message timestamp if no new
1497             // message is received
1498             if (cursor.getPosition() == cursor.getCount()-1) {
1499                 if (Log.isLoggable(ImApp.LOG_TAG, Log.DEBUG)){
1500                     log("delta = " + delta + ", showTs=" + showTimeStamp);
1501                 }
1502                 if (!showTimeStamp) {
1503                     scheduleRequery(SHOW_TIME_STAMP_INTERVAL);
1504                 } else {
1505                     cancelRequery();
1506                 }
1507             }
1508         }
1509
1510         public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount,
1511                 int totalItemCount) {
1512             // do nothing
1513         }
1514
1515         public void onScrollStateChanged(AbsListView view, int scrollState) {
1516             int oldState = mScrollState;
1517             mScrollState = scrollState;
1518
1519             if (mChatSession != null) {
1520                 try {
1521                     mChatSession.markAsRead();
1522                 } catch (RemoteException e) {
1523                     mHandler.showServiceErrorAlert();
1524                 }
1525             }
1526
1527             if (oldState == OnScrollListener.SCROLL_STATE_FLING) {
1528                 if (mNeedRequeryCursor) {
1529                     requeryCursor();
1530                 } else {
1531                     notifyDataSetChanged();
1532                 }
1533             }
1534         }
1535
1536         boolean isScrolling() {
1537             return mScrollState == OnScrollListener.SCROLL_STATE_FLING;
1538         }
1539
1540         void setNeedRequeryCursor(boolean requeryCursor) {
1541             mNeedRequeryCursor = requeryCursor;
1542         }
1543     }
1544 }