OSDN Git Service

auto import from //branches/cupcake/...@132276
[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.ContentValues;
30 import android.content.Context;
31 import android.content.DialogInterface;
32 import android.content.Intent;
33 import android.content.res.Resources;
34 import android.content.res.Configuration;
35 import android.database.ContentObserver;
36 import android.database.Cursor;
37 import android.database.CursorIndexOutOfBoundsException;
38 import android.database.DataSetObserver;
39 import android.database.CharArrayBuffer;
40 import android.graphics.Typeface;
41 import android.net.Uri;
42 import android.os.Bundle;
43 import android.os.Message;
44 import android.os.RemoteException;
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.addCategory(Intent.CATEGORY_BROWSABLE);
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                             } else {
352                                 handleEnterKey();
353                             }
354                             return true;
355                     }
356                 }
357                 return false;
358             }
359         });
360         // TODO: this is a hack to implement BUG #1611278, when dispatchKeyEvent() works with
361         // the soft keyboard, we should remove this hack.
362         mEdtInput.addTextChangedListener(new TextWatcher() {
363             public void beforeTextChanged(CharSequence s, int start, int before, int after) {
364             }
365
366             public void onTextChanged(CharSequence s, int start, int before, int after) {
367                 //log("TextWatcher: " + s);
368                 userActionDetected();
369             }
370
371             public void afterTextChanged(Editable s) {
372             }
373         });
374
375         mSendButton.setOnClickListener(new OnClickListener() {
376             public void onClick(View v) {
377                 sendMessage();
378             }
379         });
380     }
381
382     public void onResume(){
383         if (mViewType == VIEW_TYPE_CHAT) {
384             Cursor cursor = getMessageCursor();
385             if (cursor == null) {
386                 startQuery();
387             } else {
388                 requeryCursor();
389             }
390             updateWarningView();
391         }
392         registerChatListener();
393         registerForConnEvents();
394     }
395
396     public void onPause(){
397         Cursor cursor = getMessageCursor();
398         if (cursor != null) {
399             cursor.deactivate();
400         }
401         cancelRequery();
402         if (mViewType == VIEW_TYPE_CHAT && mChatSession != null) {
403             try {
404                 mChatSession.markAsRead();
405             } catch (RemoteException e) {
406                 mHandler.showServiceErrorAlert();
407             }
408         }
409         unregisterChatListener();
410         unregisterForConnEvents();
411         unregisterChatSessionListener();
412     }
413
414     private void handleEnterKey() {
415         Configuration config = getResources().getConfiguration();
416         if (config.orientation == config.ORIENTATION_LANDSCAPE) {
417             // in the landscape mode, we'll send the message if the user is using a physical
418             // keyboard. However, on the soft keyboard, we'll close the keyboard and put the
419             // focus on the Send button, in order to prevent accidental sending the message.
420             if (config.hardKeyboardHidden == config.HARDKEYBOARDHIDDEN_NO) {
421                 sendMessage();
422             } else {
423                 closeSoftKeyboard();
424                 mSendButton.requestFocus();
425             }
426         } else {
427             // in the portrait mode, the user would always be using the soft keyboard, so pressing
428             // the Enter key would close the keyboard and puts the focus on the Send button.
429             closeSoftKeyboard();
430             mSendButton.requestFocus();
431         }
432     }
433
434     private void closeSoftKeyboard() {
435         InputMethodManager inputMethodManager =
436             (InputMethodManager)mApp.getSystemService(Context.INPUT_METHOD_SERVICE);
437
438         inputMethodManager.hideSoftInputFromWindow(mEdtInput.getWindowToken(), 0);
439     }
440
441     void updateChat() {
442         setViewType(VIEW_TYPE_CHAT);
443
444         long oldChatId = mChatId;
445
446         updateContactInfo();
447
448         setStatusIcon();
449         setTitle();
450
451         IImConnection conn = mApp.getConnection(mProviderId);
452         if (conn == null) {
453             if (Log.isLoggable(ImApp.LOG_TAG, Log.DEBUG)) log("Connection has been signed out");
454             mScreen.finish();
455             return;
456         }
457
458         BrandingResources brandingRes = mApp.getBrandingResource(mProviderId);
459         mHistory.setBackgroundDrawable(
460                 brandingRes.getDrawable(BrandingResourceIDs.DRAWABLE_CHAT_WATERMARK));
461
462         if (mMarkup == null) {
463             mMarkup = new Markup(brandingRes);
464         }
465
466         if (mMessageAdapter == null) {
467             mMessageAdapter = new MessageAdapter(mScreen, null);
468             mHistory.setAdapter(mMessageAdapter);
469         }
470
471         // only change the message adapter when we switch to another chat
472         if (mChatId != oldChatId) {
473             startQuery();
474             mEdtInput.setText("");
475         }
476
477         updateWarningView();
478     }
479
480     private void updateContactInfo() {
481         mChatId = mCursor.getLong(CONTACT_ID_COLUMN);
482         mProviderId = mCursor.getLong(PROVIDER_COLUMN);
483         mAccountId = mCursor.getLong(ACCOUNT_COLUMN);
484         mPresenceStatus = mCursor.getInt(PRESENCE_STATUS_COLUMN);
485         mType = mCursor.getInt(TYPE_COLUMN);
486         mUserName = mCursor.getString(USERNAME_COLUMN);
487         mNickName = mCursor.getString(NICKNAME_COLUMN);
488     }
489
490     private void setTitle() {
491         if (mType == Im.Contacts.TYPE_GROUP) {
492             final String[] projection = {Im.GroupMembers.NICKNAME};
493             Uri memberUri = ContentUris.withAppendedId(Im.GroupMembers.CONTENT_URI, mChatId);
494             ContentResolver cr = mScreen.getContentResolver();
495             Cursor c = cr.query(memberUri, projection, null, null, null);
496             StringBuilder buf = new StringBuilder();
497             if(c != null) {
498                 while(c.moveToNext()) {
499                     buf.append(c.getString(0));
500                     if(!c.isLast()) {
501                         buf.append(',');
502                     }
503                 }
504                 c.close();
505             }
506             mTitle.setText(mContext.getString(R.string.chat_with, buf.toString()));
507         } else {
508             mTitle.setText(mContext.getString(R.string.chat_with, mNickName));
509         }
510     }
511
512     private void setStatusIcon() {
513         if (mType == Im.Contacts.TYPE_GROUP) {
514             // hide the status icon for group chat.
515             mStatusIcon.setVisibility(GONE);
516         } else {
517             mStatusIcon.setVisibility(VISIBLE);
518             BrandingResources brandingRes = mApp.getBrandingResource(mProviderId);
519             int presenceResId = PresenceUtils.getStatusIconId(mPresenceStatus);
520             mStatusIcon.setImageDrawable(brandingRes.getDrawable(presenceResId));
521         }
522     }
523
524     public void bindChat(long chatId) {
525         if (mCursor != null) {
526             mCursor.deactivate();
527         }
528         Uri contactUri = ContentUris.withAppendedId(Im.Contacts.CONTENT_URI, chatId);
529         mCursor = mScreen.managedQuery(contactUri, CHAT_PROJECTION, null, null);
530         if (mCursor == null || !mCursor.moveToFirst()) {
531             if (Log.isLoggable(ImApp.LOG_TAG, Log.DEBUG)){
532                 log("Failed to query chat: " + chatId);
533             }
534             mScreen.finish();
535             return;
536         } else {
537             mChatSession = getChatSession(mCursor);
538             updateChat();
539             registerChatListener();
540         }
541     }
542
543     public void bindInvitation(long invitationId) {
544         Uri uri = ContentUris.withAppendedId(Im.Invitation.CONTENT_URI, invitationId);
545         ContentResolver cr = mScreen.getContentResolver();
546         Cursor cursor = cr.query(uri, INVITATION_PROJECT, null, null, null);
547         if (cursor == null || !cursor.moveToFirst()) {
548             if (Log.isLoggable(ImApp.LOG_TAG, Log.DEBUG)){
549                 log("Failed to query invitation: " + invitationId);
550             }
551             mScreen.finish();
552         } else {
553             setViewType(VIEW_TYPE_INVITATION);
554
555             mInvitationId = cursor.getLong(INVITATION_ID_COLUMN);
556             mProviderId = cursor.getLong(INVITATION_PROVIDER_COLUMN);
557             String sender = cursor.getString(INVITATION_SENDER_COLUMN);
558
559             TextView mInvitationText = (TextView)findViewById(R.id.txtInvitation);
560             mInvitationText.setText(mContext.getString(R.string.invitation_prompt, sender));
561             mTitle.setText(mContext.getString(R.string.chat_with, sender));
562         }
563
564         if (cursor != null) {
565             cursor.close();
566         }
567     }
568
569     public void bindSubscription(long providerId, String from) {
570         mProviderId = providerId;
571         mUserName = from;
572
573         setViewType(VIEW_TYPE_SUBSCRIPTION);
574
575         TextView text =  (TextView)findViewById(R.id.txtSubscription);
576         String displayableAddr = ImpsAddressUtils.getDisplayableAddress(from);
577         text.setText(mContext.getString(R.string.subscription_prompt, displayableAddr));
578         mTitle.setText(mContext.getString(R.string.chat_with, displayableAddr));
579
580         mApp.dismissChatNotification(providerId, from);
581     }
582
583     void acceptInvitation() {
584         try {
585
586             IImConnection conn = mApp.getConnection(mProviderId);
587             if (conn != null) {
588                 // register a chat session listener and wait for a group chat
589                 // session to be created after we accept the invitation.
590                 registerChatSessionListener();
591                 conn.acceptInvitation(mInvitationId);
592             }
593         } catch (RemoteException e) {
594             mHandler.showServiceErrorAlert();
595         }
596     }
597
598     void declineInvitation() {
599         try {
600             IImConnection conn = mApp.getConnection(mProviderId);
601             if (conn != null) {
602                 conn.rejectInvitation(mInvitationId);
603             }
604             mScreen.finish();
605         } catch (RemoteException e) {
606             mHandler.showServiceErrorAlert();
607         }
608     }
609
610     void approveSubscription() {
611         IImConnection conn = mApp.getConnection(mProviderId);
612         try {
613             IContactListManager manager = conn.getContactListManager();
614             manager.approveSubscription(mUserName);
615         } catch (RemoteException ex) {
616             mHandler.showServiceErrorAlert();
617         }
618         mScreen.finish();
619     }
620
621     void declineSubscription() {
622         IImConnection conn = mApp.getConnection(mProviderId);
623         try {
624             IContactListManager manager = conn.getContactListManager();
625             manager.declineSubscription(mUserName);
626         } catch (RemoteException ex) {
627             mHandler.showServiceErrorAlert();
628         }
629         mScreen.finish();
630     }
631
632     private void setViewType(int type) {
633         mViewType = type;
634         if (type == VIEW_TYPE_CHAT) {
635             findViewById(R.id.invitationPanel).setVisibility(GONE);
636             findViewById(R.id.subscription).setVisibility(GONE);
637             setChatViewEnabled(true);
638         }  else if (type == VIEW_TYPE_INVITATION) {
639             setChatViewEnabled(false);
640             findViewById(R.id.invitationPanel).setVisibility(VISIBLE);
641             findViewById(R.id.btnAccept).requestFocus();
642         } else if (type == VIEW_TYPE_SUBSCRIPTION) {
643             setChatViewEnabled(false);
644             findViewById(R.id.subscription).setVisibility(VISIBLE);
645             findViewById(R.id.btnApproveSubscription).requestFocus();
646         }
647     }
648
649     private void setChatViewEnabled(boolean enabled) {
650         mEdtInput.setEnabled(enabled);
651         mSendButton.setEnabled(enabled);
652         if (enabled) {
653             mEdtInput.requestFocus();
654         } else {
655             mHistory.setAdapter(null);
656         }
657     }
658
659     private void startQuery() {
660         if (mQueryHandler == null) {
661             mQueryHandler = new QueryHandler(mContext);
662         } else {
663             // Cancel any pending queries
664             mQueryHandler.cancelOperation(QUERY_TOKEN);
665         }
666
667         Uri uri;
668         if (Im.Contacts.TYPE_GROUP == mType) {
669             uri = ContentUris.withAppendedId(Im.GroupMessages.CONTENT_URI_GROUP_MESSAGES_BY, mChatId);
670         } else {
671             uri = Im.Messages.getContentUriByContact(mProviderId, mAccountId, mUserName);
672         }
673
674         if (Log.isLoggable(ImApp.LOG_TAG, Log.DEBUG)){
675             log("queryCursor: uri=" + uri);
676         }
677
678         mQueryHandler.startQuery(QUERY_TOKEN, null,
679                 uri,
680                 null,
681                 null /* selection */,
682                 null /* selection args */,
683                 null);
684     }
685
686     void scheduleRequery(long interval) {
687         if (mRequeryCallback == null) {
688             mRequeryCallback = new RequeryCallback();
689         } else {
690             mHandler.removeCallbacks(mRequeryCallback);
691         }
692
693         if (Log.isLoggable(ImApp.LOG_TAG, Log.DEBUG)){
694             log("scheduleRequery");
695         }
696         mHandler.postDelayed(mRequeryCallback, interval);
697     }
698
699     void cancelRequery() {
700         if (mRequeryCallback != null) {
701             if (Log.isLoggable(ImApp.LOG_TAG, Log.DEBUG)){
702                 log("cancelRequery");
703             }
704             mHandler.removeCallbacks(mRequeryCallback);
705             mRequeryCallback = null;
706         }
707     }
708
709     void requeryCursor() {
710         if (mMessageAdapter.isScrolling()) {
711             mMessageAdapter.setNeedRequeryCursor(true);
712             return;
713         }
714         // TODO: async query?
715         Cursor cursor = getMessageCursor();
716         if (cursor != null) {
717             cursor.requery();
718         }
719     }
720
721     private Cursor getMessageCursor() {
722         return mMessageAdapter == null ? null : mMessageAdapter.getCursor();
723     }
724
725     public void insertSmiley(String smiley) {
726         mEdtInput.append(mMarkup.applyEmoticons(smiley));
727     }
728
729     public void closeChatSession() {
730         if (mChatSession != null) {
731             try {
732                 mChatSession.leave();
733             } catch (RemoteException e) {
734                 mHandler.showServiceErrorAlert();
735             }
736         } else {
737             // the conversation is already closed, clear data in database
738             ContentResolver cr = mContext.getContentResolver();
739             cr.delete(ContentUris.withAppendedId(Im.Chats.CONTENT_URI, mChatId),
740                     null, null);
741         }
742         mScreen.finish();
743     }
744
745     public void closeChatSessionIfInactive() {
746         if (mChatSession != null) {
747             try {
748                 mChatSession.leaveIfInactive();
749             } catch (RemoteException e) {
750                 mHandler.showServiceErrorAlert();
751             }
752         }
753     }
754
755     public void viewProfile() {
756         Uri data = ContentUris.withAppendedId(Im.Contacts.CONTENT_URI, mChatId);
757         Intent intent = new Intent(Intent.ACTION_VIEW, data);
758         mScreen.startActivity(intent);
759     }
760
761     public void blockContact() {
762         // TODO: unify with codes in ContactListView
763         DialogInterface.OnClickListener confirmListener = new DialogInterface.OnClickListener(){
764             public void onClick(DialogInterface dialog, int whichButton) {
765                 try {
766                     IImConnection conn = mApp.getConnection(mProviderId);
767                     IContactListManager manager = conn.getContactListManager();
768                     manager.blockContact(mUserName);
769                     mScreen.finish();
770                 } catch (RemoteException e) {
771                     mHandler.showServiceErrorAlert();
772                 }
773             }
774         };
775
776         Resources r = getResources();
777
778         // The positive button is deliberately set as no so that
779         // the no is the default value
780         new AlertDialog.Builder(mContext)
781             .setTitle(R.string.confirm)
782             .setMessage(r.getString(R.string.confirm_block_contact, mNickName))
783             .setPositiveButton(R.string.no, null) // default button
784             .setNegativeButton(R.string.yes, confirmListener)
785             .setCancelable(false)
786             .show();
787     }
788
789     public long getProviderId() {
790         return mProviderId;
791     }
792
793     public long getAccountId() {
794         return mAccountId;
795     }
796
797     public String getUserName() {
798         return mUserName;
799     }
800
801     public long getChatId () {
802         try {
803             return mChatSession == null ? -1 : mChatSession.getId();
804         } catch (RemoteException e) {
805             mHandler.showServiceErrorAlert();
806             return -1;
807         }
808     }
809
810     public IChatSession getCurrentChatSession() {
811         return mChatSession;
812     }
813
814     private IChatSessionManager getChatSessionManager(long providerId) {
815         if (mChatSessionMgr == null) {
816             IImConnection conn = mApp.getConnection(providerId);
817             if (conn != null) {
818                 try {
819                     mChatSessionMgr = conn.getChatSessionManager();
820                 } catch (RemoteException e) {
821                     mHandler.showServiceErrorAlert();
822                 }
823             }
824         }
825         return mChatSessionMgr;
826     }
827
828     private IChatSession getChatSession(Cursor cursor) {
829         long providerId = cursor.getLong(PROVIDER_COLUMN);
830         String username = cursor.getString(USERNAME_COLUMN);
831
832         IChatSessionManager sessionMgr = getChatSessionManager(providerId);
833         if (sessionMgr != null) {
834             try {
835                 return sessionMgr.getChatSession(username);
836             } catch (RemoteException e) {
837                 mHandler.showServiceErrorAlert();
838             }
839         }
840         return null;
841     }
842
843     boolean isGroupChat() {
844         return Im.Contacts.TYPE_GROUP == mType;
845     }
846
847     void sendMessage() {
848         String msg = mEdtInput.getText().toString();
849
850         if (TextUtils.isEmpty(msg.trim())) {
851             return;
852         }
853
854         if (mChatSession != null) {
855             try {
856                 mChatSession.sendMessage(msg);
857                 mEdtInput.setText("");
858                 mEdtInput.requestFocus();
859                 requeryCursor();
860             } catch (RemoteException e) {
861                 mHandler.showServiceErrorAlert();
862             }
863         }
864
865         // Close the soft on-screen keyboard if we're in landscape mode so the user can see the
866         // conversation.
867         Configuration config = getResources().getConfiguration();
868         if (config.orientation == config.ORIENTATION_LANDSCAPE) {
869             closeSoftKeyboard();
870         }
871     }
872
873     void registerChatListener() {
874         if (Log.isLoggable(ImApp.LOG_TAG, Log.DEBUG)){
875             log("registerChatListener");
876         }
877         try {
878             if (mChatSession != null) {
879                 mChatSession.registerChatListener(mChatListener);
880             }
881             IImConnection conn = mApp.getConnection(mProviderId);
882             if (conn != null) {
883                 IContactListManager listMgr = conn.getContactListManager();
884                 listMgr.registerContactListListener(mContactListListener);
885             }
886             mApp.dismissChatNotification(mProviderId, mUserName);
887         } catch (RemoteException e) {
888             Log.w(ImApp.LOG_TAG, "<ChatView> registerChatListener fail:" + e.getMessage());
889         }
890     }
891
892     void unregisterChatListener() {
893         if (Log.isLoggable(ImApp.LOG_TAG, Log.DEBUG)){
894             log("unregisterChatListener");
895         }
896         try {
897             if (mChatSession != null) {
898                 mChatSession.unregisterChatListener(mChatListener);
899             }
900             IImConnection conn = mApp.getConnection(mProviderId);
901             if (conn != null) {
902                 IContactListManager listMgr = conn.getContactListManager();
903                 listMgr.unregisterContactListListener(mContactListListener);
904             }
905         } catch (RemoteException e) {
906             Log.w(ImApp.LOG_TAG, "<ChatView> unregisterChatListener fail:" + e.getMessage());
907         }
908     }
909
910     void registerChatSessionListener() {
911         IChatSessionManager sessionMgr = getChatSessionManager(mProviderId);
912         if (sessionMgr != null) {
913             mChatSessionListener = new ChatSessionListener();
914             try {
915                 sessionMgr.registerChatSessionListener(mChatSessionListener);
916             } catch (RemoteException e) {
917                 mHandler.showServiceErrorAlert();
918             }
919         }
920     }
921
922     void unregisterChatSessionListener() {
923         if (mChatSessionListener != null) {
924             try {
925                 IChatSessionManager sessionMgr = getChatSessionManager(mProviderId);
926                 sessionMgr.unregisterChatSessionListener(mChatSessionListener);
927                 // We unregister the listener when the chat session we are
928                 // waiting for has been created or the activity is stopped.
929                 // Clear the listener so that we won't unregister the listener
930                 // twice.
931                 mChatSessionListener = null;
932             } catch (RemoteException e) {
933                 mHandler.showServiceErrorAlert();
934             }
935         }
936     }
937
938     void updateWarningView() {
939         int visibility = View.GONE;
940         int iconVisibility = View.GONE;
941         String message = null;
942         boolean isConnected;
943
944         try {
945             IImConnection conn = mApp.getConnection(mProviderId);
946             isConnected = (conn == null) ? false
947                     : conn.getState() != ImConnection.SUSPENDED;
948         } catch (RemoteException e) {
949             // do nothing
950             return;
951         }
952
953         if (isConnected) {
954             if (mType == Im.Contacts.TYPE_TEMPORARY) {
955                 visibility = View.VISIBLE;
956                 message = mContext.getString(R.string.contact_not_in_list_warning, mNickName);
957             } else if (mPresenceStatus == Im.Presence.OFFLINE) {
958                 visibility = View.VISIBLE;
959                 message = mContext.getString(R.string.contact_offline_warning, mNickName);
960             }
961         } else {
962             visibility = View.VISIBLE;
963             iconVisibility = View.VISIBLE;
964             message = mContext.getString(R.string.disconnected_warning);
965         }
966
967         mStatusWarningView.setVisibility(visibility);
968         if (visibility == View.VISIBLE) {
969             mWarningIcon.setVisibility(iconVisibility);
970             mWarningText.setText(message);
971         }
972     }
973
974     @Override
975     public boolean dispatchKeyEvent(KeyEvent event) {
976         userActionDetected();
977         return super.dispatchKeyEvent(event);
978     }
979
980     @Override
981     public boolean dispatchTouchEvent(MotionEvent ev) {
982         userActionDetected();
983         return super.dispatchTouchEvent(ev);
984     }
985
986     @Override
987     public boolean dispatchTrackballEvent(MotionEvent ev) {
988         userActionDetected();
989         return super.dispatchTrackballEvent(ev);
990     }
991
992     private void userActionDetected() {
993         if (mChatSession != null) {
994             try {
995                 mChatSession.markAsRead();
996             } catch (RemoteException e) {
997                 mHandler.showServiceErrorAlert();
998             }
999         }
1000     }
1001
1002     private final class ChatViewHandler extends SimpleAlertHandler {
1003         public ChatViewHandler() {
1004             super(mScreen);
1005         }
1006
1007         @Override
1008         public void handleMessage(Message msg) {
1009             long providerId = ((long)msg.arg1 << 32) | msg.arg2;
1010             if (providerId != mProviderId) {
1011                 return;
1012             }
1013
1014             switch(msg.what) {
1015             case ImApp.EVENT_CONNECTION_LOGGED_IN:
1016                 log("Connection resumed");
1017                 updateWarningView();
1018                 return;
1019             case ImApp.EVENT_CONNECTION_SUSPENDED:
1020                 log("Connection suspended");
1021                 updateWarningView();
1022                 return;
1023             }
1024
1025             super.handleMessage(msg);
1026         }
1027     }
1028
1029     class ChatSessionListener extends ChatSessionListenerAdapter {
1030         @Override
1031         public void onChatSessionCreated(IChatSession session) {
1032             try {
1033                 if (session.isGroupChatSession()) {
1034                     final long id = session.getId();
1035                     unregisterChatSessionListener();
1036                     mHandler.post(new Runnable() {
1037                         public void run() {
1038                             bindChat(id);
1039                         }});
1040                 }
1041             } catch (RemoteException e) {
1042                 mHandler.showServiceErrorAlert();
1043             }
1044         }
1045     }
1046
1047     public static class DeltaCursor implements Cursor {
1048         static final String DELTA_COLUMN_NAME = "delta";
1049
1050         private Cursor mInnerCursor;
1051         private String[] mColumnNames;
1052         private int mDateColumn = -1;
1053         private int mDeltaColumn = -1;
1054
1055         DeltaCursor(Cursor cursor) {
1056             mInnerCursor = cursor;
1057
1058             String[] columnNames = cursor.getColumnNames();
1059             int len = columnNames.length;
1060
1061             mColumnNames = new String[len + 1];
1062
1063             for (int i = 0 ; i < len ; i++) {
1064                 mColumnNames[i] = columnNames[i];
1065                 if (mColumnNames[i].equals(Im.BaseMessageColumns.DATE)) {
1066                     mDateColumn = i;
1067                 }
1068             }
1069
1070             mDeltaColumn = len;
1071             mColumnNames[mDeltaColumn] = DELTA_COLUMN_NAME;
1072
1073             //if (DBG) log("##### DeltaCursor constructor: mDeltaColumn=" +
1074             //        mDeltaColumn + ", columnName=" + mColumnNames[mDeltaColumn]);
1075         }
1076
1077         public int getCount() {
1078             return mInnerCursor.getCount();
1079         }
1080
1081         public int getPosition() {
1082             return mInnerCursor.getPosition();
1083         }
1084
1085         public boolean move(int offset) {
1086             return mInnerCursor.move(offset);
1087         }
1088
1089         public boolean moveToPosition(int position) {
1090             return mInnerCursor.moveToPosition(position);
1091         }
1092
1093         public boolean moveToFirst() {
1094             return mInnerCursor.moveToFirst();
1095         }
1096
1097         public boolean moveToLast() {
1098             return mInnerCursor.moveToLast();
1099         }
1100
1101         public boolean moveToNext() {
1102             return mInnerCursor.moveToNext();
1103         }
1104
1105         public boolean moveToPrevious() {
1106             return mInnerCursor.moveToPrevious();
1107         }
1108
1109         public boolean isFirst() {
1110             return mInnerCursor.isFirst();
1111         }
1112
1113         public boolean isLast() {
1114             return mInnerCursor.isLast();
1115         }
1116
1117         public boolean isBeforeFirst() {
1118             return mInnerCursor.isBeforeFirst();
1119         }
1120
1121         public boolean isAfterLast() {
1122             return mInnerCursor.isAfterLast();
1123         }
1124
1125         public boolean deleteRow() {
1126             return mInnerCursor.deleteRow();
1127         }
1128
1129         public int getColumnIndex(String columnName) {
1130             if (DELTA_COLUMN_NAME.equals(columnName)) {
1131                 return mDeltaColumn;
1132             }
1133
1134             int columnIndex = mInnerCursor.getColumnIndex(columnName);
1135             return columnIndex;
1136         }
1137
1138         public int getColumnIndexOrThrow(String columnName) throws IllegalArgumentException {
1139             if (DELTA_COLUMN_NAME.equals(columnName)) {
1140                 return mDeltaColumn;
1141             }
1142
1143             return mInnerCursor.getColumnIndexOrThrow(columnName);
1144         }
1145
1146         public String getColumnName(int columnIndex) {
1147             if (columnIndex == mDeltaColumn) {
1148                 return DELTA_COLUMN_NAME;
1149             }
1150
1151             return mInnerCursor.getColumnName(columnIndex);
1152         }
1153
1154         public int getColumnCount() {
1155             return mInnerCursor.getColumnCount() + 1;
1156         }
1157
1158         public boolean supportsUpdates() {
1159             return mInnerCursor.supportsUpdates();
1160         }
1161
1162         public boolean hasUpdates() {
1163             return mInnerCursor.hasUpdates();
1164         }
1165
1166         public boolean updateBlob(int columnIndex, byte[] value) {
1167             if (columnIndex == mDeltaColumn) {
1168                 return false;
1169             }
1170
1171             return mInnerCursor.updateBlob(columnIndex, value);
1172         }
1173
1174         public boolean updateString(int columnIndex, String value) {
1175             if (columnIndex == mDeltaColumn) {
1176                 return false;
1177             }
1178
1179             return mInnerCursor.updateString(columnIndex, value);
1180         }
1181
1182         public boolean updateShort(int columnIndex, short value) {
1183             if (columnIndex == mDeltaColumn) {
1184                 return false;
1185             }
1186
1187             return mInnerCursor.updateShort(columnIndex, value);
1188         }
1189
1190         public boolean updateInt(int columnIndex, int value) {
1191             if (columnIndex == mDeltaColumn) {
1192                 return false;
1193             }
1194
1195             return mInnerCursor.updateInt(columnIndex, value);
1196         }
1197
1198         public boolean updateLong(int columnIndex, long value) {
1199             if (columnIndex == mDeltaColumn) {
1200                 return false;
1201             }
1202
1203             return mInnerCursor.updateLong(columnIndex, value);
1204         }
1205
1206         public boolean updateFloat(int columnIndex, float value) {
1207             if (columnIndex == mDeltaColumn) {
1208                 return false;
1209             }
1210
1211             return mInnerCursor.updateFloat(columnIndex, value);
1212         }
1213
1214         public boolean updateDouble(int columnIndex, double value) {
1215             if (columnIndex == mDeltaColumn) {
1216                 return false;
1217             }
1218
1219             return mInnerCursor.updateDouble(columnIndex, value);
1220         }
1221
1222         public boolean updateToNull(int columnIndex) {
1223             if (columnIndex == mDeltaColumn) {
1224                 return false;
1225             }
1226
1227             return mInnerCursor.updateToNull(columnIndex);
1228         }
1229
1230         public boolean commitUpdates() {
1231             return mInnerCursor.commitUpdates();
1232         }
1233
1234         public boolean commitUpdates(Map<? extends Long,
1235                 ? extends Map<String,Object>> values) {
1236             return mInnerCursor.commitUpdates(values);
1237         }
1238
1239         public void abortUpdates() {
1240             mInnerCursor.abortUpdates();
1241         }
1242
1243         public void deactivate() {
1244             mInnerCursor.deactivate();
1245         }
1246
1247         public boolean requery() {
1248             return mInnerCursor.requery();
1249         }
1250
1251         public void close() {
1252             mInnerCursor.close();
1253         }
1254
1255         public boolean isClosed() {
1256             return mInnerCursor.isClosed();
1257         }
1258
1259         public void registerContentObserver(ContentObserver observer) {
1260             mInnerCursor.registerContentObserver(observer);
1261         }
1262
1263         public void unregisterContentObserver(ContentObserver observer) {
1264             mInnerCursor.unregisterContentObserver(observer);
1265         }
1266
1267         public void registerDataSetObserver(DataSetObserver observer) {
1268             mInnerCursor.registerDataSetObserver(observer);
1269         }
1270
1271         public void unregisterDataSetObserver(DataSetObserver observer) {
1272             mInnerCursor.unregisterDataSetObserver(observer);
1273         }
1274
1275         public void setNotificationUri(ContentResolver cr, Uri uri) {
1276             mInnerCursor.setNotificationUri(cr, uri);
1277         }
1278
1279         public boolean getWantsAllOnMoveCalls() {
1280             return mInnerCursor.getWantsAllOnMoveCalls();
1281         }
1282
1283         public Bundle getExtras() {
1284             return mInnerCursor.getExtras();
1285         }
1286
1287         public Bundle respond(Bundle extras) {
1288             return mInnerCursor.respond(extras);
1289         }
1290
1291         public String[] getColumnNames() {
1292             return mColumnNames;
1293         }
1294
1295         private void checkPosition() {
1296             int pos = mInnerCursor.getPosition();
1297             int count = mInnerCursor.getCount();
1298
1299             if (-1 == pos || count == pos) {
1300                 throw new CursorIndexOutOfBoundsException(pos, count);
1301             }
1302         }
1303
1304         public byte[] getBlob(int column) {
1305             checkPosition();
1306
1307             if (column == mDeltaColumn) {
1308                 return null;
1309             }
1310
1311             return mInnerCursor.getBlob(column);
1312         }
1313
1314         public String getString(int column) {
1315             checkPosition();
1316
1317             if (column == mDeltaColumn) {
1318                 long value = getDeltaValue();
1319                 return Long.toString(value);
1320             }
1321
1322             return mInnerCursor.getString(column);
1323         }
1324
1325         public void copyStringToBuffer(int columnIndex, CharArrayBuffer buffer) {
1326             checkPosition();
1327
1328             if (columnIndex == mDeltaColumn) {
1329                 long value = getDeltaValue();
1330                 String strValue = Long.toString(value);
1331                 int len = strValue.length();
1332                 char[] data = buffer.data;
1333                 if (data == null || data.length < len) {
1334                     buffer.data = strValue.toCharArray();
1335                 } else {
1336                     strValue.getChars(0, len, data, 0);
1337                 }
1338                 buffer.sizeCopied = strValue.length();
1339             } else {
1340                 mInnerCursor.copyStringToBuffer(columnIndex, buffer);
1341             }
1342         }
1343
1344         public short getShort(int column) {
1345             checkPosition();
1346
1347             if (column == mDeltaColumn) {
1348                 return (short)getDeltaValue();
1349             }
1350
1351             return mInnerCursor.getShort(column);
1352         }
1353
1354         public int getInt(int column) {
1355             checkPosition();
1356
1357             if (column == mDeltaColumn) {
1358                 return (int)getDeltaValue();
1359             }
1360
1361             return mInnerCursor.getInt(column);
1362         }
1363
1364         public long getLong(int column) {
1365         //if (DBG) log("DeltaCursor.getLong: column=" + column + ", mDeltaColumn=" + mDeltaColumn);
1366             checkPosition();
1367
1368             if (column == mDeltaColumn) {
1369                 return getDeltaValue();
1370             }
1371
1372             return mInnerCursor.getLong(column);
1373         }
1374
1375         public float getFloat(int column) {
1376             checkPosition();
1377
1378             if (column == mDeltaColumn) {
1379                 return getDeltaValue();
1380             }
1381
1382             return mInnerCursor.getFloat(column);
1383         }
1384
1385         public double getDouble(int column) {
1386             checkPosition();
1387
1388             if (column == mDeltaColumn) {
1389                 return getDeltaValue();
1390             }
1391
1392             return mInnerCursor.getDouble(column);
1393         }
1394
1395         public boolean isNull(int column) {
1396             checkPosition();
1397
1398             if (column == mDeltaColumn) {
1399                 return false;
1400             }
1401
1402             return mInnerCursor.isNull(column);
1403         }
1404
1405         private long getDeltaValue() {
1406             int pos = mInnerCursor.getPosition();
1407             //Log.i(LOG_TAG, "getDeltaValue: mPos=" + mPos);
1408
1409             long t2, t1;
1410
1411             if (pos == getCount()-1) {
1412                 t1 = mInnerCursor.getLong(mDateColumn);
1413                 t2 = System.currentTimeMillis();
1414             } else {
1415                 mInnerCursor.moveToPosition(pos + 1);
1416                 t2 = mInnerCursor.getLong(mDateColumn);
1417                 mInnerCursor.moveToPosition(pos);
1418                 t1 = mInnerCursor.getLong(mDateColumn);
1419             }
1420
1421             return t2 - t1;
1422         }
1423     }
1424
1425     private class MessageAdapter extends CursorAdapter implements AbsListView.OnScrollListener {
1426         private int mScrollState;
1427         private boolean mNeedRequeryCursor;
1428
1429         private int mContactColumn;
1430         private int mBodyColumn;
1431         private int mDateColumn;
1432         private int mTypeColumn;
1433         private int mErrCodeColumn;
1434         private int mDeltaColumn;
1435         private ChatBackgroundMaker mBgMaker;
1436
1437         private LayoutInflater mInflater;
1438
1439         public MessageAdapter(Activity context, Cursor c) {
1440             super(context, c, false);
1441             mInflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
1442             mBgMaker = new ChatBackgroundMaker(context);
1443             if (c != null) {
1444                 resolveColumnIndex(c);
1445             }
1446         }
1447
1448         private void resolveColumnIndex(Cursor c) {
1449             mContactColumn = c.getColumnIndexOrThrow(Im.BaseMessageColumns.CONTACT);
1450             mBodyColumn = c.getColumnIndexOrThrow(Im.BaseMessageColumns.BODY);
1451             mDateColumn = c.getColumnIndexOrThrow(Im.BaseMessageColumns.DATE);
1452             mTypeColumn = c.getColumnIndexOrThrow(Im.BaseMessageColumns.TYPE);
1453             mErrCodeColumn = c.getColumnIndexOrThrow(Im.BaseMessageColumns.ERROR_CODE);
1454             mDeltaColumn = c.getColumnIndexOrThrow(DeltaCursor.DELTA_COLUMN_NAME);
1455         }
1456
1457         @Override
1458         public void changeCursor(Cursor cursor) {
1459             super.changeCursor(cursor);
1460             if (cursor != null) {
1461                 resolveColumnIndex(cursor);
1462             }
1463         }
1464
1465         @Override
1466         public View newView(Context context, Cursor cursor, ViewGroup parent) {
1467             return mInflater.inflate(R.layout.new_message_item, parent, false);
1468         }
1469
1470         @Override
1471         public void bindView(View view, Context context, Cursor cursor) {
1472             MessageView chatMsgView = (MessageView) view;
1473
1474             int type = cursor.getInt(mTypeColumn);
1475             String contact = isGroupChat() ? cursor.getString(mContactColumn) : mNickName;
1476             String body = cursor.getString(mBodyColumn);
1477             long delta = cursor.getLong(mDeltaColumn);
1478             boolean showTimeStamp = (delta > SHOW_TIME_STAMP_INTERVAL);
1479             Date date = showTimeStamp ? new Date(cursor.getLong(mDateColumn)) : null;
1480
1481             switch (type) {
1482                 case Im.MessageType.INCOMING:
1483                     chatMsgView.bindIncomingMessage(contact, body, date, mMarkup, isScrolling());
1484                     break;
1485
1486                 case Im.MessageType.OUTGOING:
1487                 case Im.MessageType.POSTPONED:
1488                     int errCode = cursor.getInt(mErrCodeColumn);
1489                     if (errCode != 0) {
1490                         chatMsgView.bindErrorMessage(errCode);
1491                     } else {
1492                         chatMsgView.bindOutgoingMessage(body, date, mMarkup, isScrolling());
1493                     }
1494                     break;
1495
1496                 default:
1497                     chatMsgView.bindPresenceMessage(contact, type, isGroupChat(), isScrolling());
1498             }
1499             if (!isScrolling()) {
1500                 mBgMaker.setBackground(chatMsgView, contact, type);
1501             }
1502
1503             // if showTimeStamp is false for the latest message, then set a timer to query the
1504             // cursor again in a minute, so we can update the last message timestamp if no new
1505             // message is received
1506             if (cursor.getPosition() == cursor.getCount()-1) {
1507                 if (Log.isLoggable(ImApp.LOG_TAG, Log.DEBUG)){
1508                     log("delta = " + delta + ", showTs=" + showTimeStamp);
1509                 }
1510                 if (!showTimeStamp) {
1511                     scheduleRequery(SHOW_TIME_STAMP_INTERVAL);
1512                 } else {
1513                     cancelRequery();
1514                 }
1515             }
1516         }
1517
1518         public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount,
1519                 int totalItemCount) {
1520             // do nothing
1521         }
1522
1523         public void onScrollStateChanged(AbsListView view, int scrollState) {
1524             int oldState = mScrollState;
1525             mScrollState = scrollState;
1526
1527             if (mChatSession != null) {
1528                 try {
1529                     mChatSession.markAsRead();
1530                 } catch (RemoteException e) {
1531                     mHandler.showServiceErrorAlert();
1532                 }
1533             }
1534
1535             if (oldState == OnScrollListener.SCROLL_STATE_FLING) {
1536                 if (mNeedRequeryCursor) {
1537                     requeryCursor();
1538                 } else {
1539                     notifyDataSetChanged();
1540                 }
1541             }
1542         }
1543
1544         boolean isScrolling() {
1545             return mScrollState == OnScrollListener.SCROLL_STATE_FLING;
1546         }
1547
1548         void setNeedRequeryCursor(boolean requeryCursor) {
1549             mNeedRequeryCursor = requeryCursor;
1550         }
1551     }
1552 }