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
18package com.android.im.app;
19
20import java.util.ArrayList;
21import java.util.Date;
22import java.util.Map;
23
24import android.app.Activity;
25import android.app.AlertDialog;
26import android.content.AsyncQueryHandler;
27import android.content.ContentResolver;
28import android.content.ContentUris;
29import android.content.Context;
30import android.content.DialogInterface;
31import android.content.Intent;
32import android.content.res.Resources;
33import android.content.res.Configuration;
34import android.database.ContentObserver;
35import android.database.Cursor;
36import android.database.CursorIndexOutOfBoundsException;
37import android.database.DataSetObserver;
38import android.database.CharArrayBuffer;
39import android.graphics.Typeface;
40import android.net.Uri;
41import android.os.Bundle;
42import android.os.Message;
43import android.os.RemoteException;
44import android.provider.Browser;
45import android.text.Editable;
46import android.text.TextUtils;
47import android.text.TextWatcher;
48import android.text.style.StyleSpan;
49import android.text.style.URLSpan;
50import android.util.AttributeSet;
51import android.util.Log;
52import android.view.KeyEvent;
53import android.view.LayoutInflater;
54import android.view.MotionEvent;
55import android.view.View;
56import android.view.ViewGroup;
57import android.view.inputmethod.InputMethodManager;
58import android.widget.AbsListView;
59import android.widget.AdapterView;
60import android.widget.ArrayAdapter;
61import android.widget.Button;
62import android.widget.CursorAdapter;
63import android.widget.EditText;
64import android.widget.ImageView;
65import android.widget.LinearLayout;
66import android.widget.ListView;
67import android.widget.TextView;
68import android.widget.AbsListView.OnScrollListener;
69import android.widget.AdapterView.OnItemClickListener;
70
71import com.android.im.IChatListener;
72import com.android.im.IChatSession;
73import com.android.im.IChatSessionListener;
74import com.android.im.IChatSessionManager;
75import com.android.im.IContactList;
76import com.android.im.IContactListListener;
77import com.android.im.IContactListManager;
78import com.android.im.IImConnection;
79import com.android.im.R;
80import com.android.im.app.adapter.ChatListenerAdapter;
81import com.android.im.app.adapter.ChatSessionListenerAdapter;
82import com.android.im.engine.Contact;
83import com.android.im.engine.ImConnection;
84import com.android.im.engine.ImErrorInfo;
85import com.android.im.plugin.BrandingResourceIDs;
86import com.android.im.provider.Imps;
87
88public class ChatView extends LinearLayout {
89    // This projection and index are set for the query of active chats
90    static final String[] CHAT_PROJECTION = {
91        Imps.Contacts._ID,
92        Imps.Contacts.ACCOUNT,
93        Imps.Contacts.PROVIDER,
94        Imps.Contacts.USERNAME,
95        Imps.Contacts.NICKNAME,
96        Imps.Contacts.TYPE,
97        Imps.Presence.PRESENCE_STATUS,
98        Imps.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        Imps.Invitation._ID,
111        Imps.Invitation.PROVIDER,
112        Imps.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 == Imps.Contacts.TYPE_GROUP) {
484            final String[] projection = {Imps.GroupMembers.NICKNAME};
485            Uri memberUri = ContentUris.withAppendedId(Imps.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 == Imps.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(Imps.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(Imps.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 = Imps.Messages.getContentUriByThreadId(mChatId);
660
661        if (Log.isLoggable(ImApp.LOG_TAG, Log.DEBUG)){
662            log("queryCursor: uri=" + uri);
663        }
664
665        mQueryHandler.startQuery(QUERY_TOKEN, null,
666                uri,
667                null,
668                null /* selection */,
669                null /* selection args */,
670                null);
671    }
672
673    void scheduleRequery(long interval) {
674        if (mRequeryCallback == null) {
675            mRequeryCallback = new RequeryCallback();
676        } else {
677            mHandler.removeCallbacks(mRequeryCallback);
678        }
679
680        if (Log.isLoggable(ImApp.LOG_TAG, Log.DEBUG)){
681            log("scheduleRequery");
682        }
683        mHandler.postDelayed(mRequeryCallback, interval);
684    }
685
686    void cancelRequery() {
687        if (mRequeryCallback != null) {
688            if (Log.isLoggable(ImApp.LOG_TAG, Log.DEBUG)){
689                log("cancelRequery");
690            }
691            mHandler.removeCallbacks(mRequeryCallback);
692            mRequeryCallback = null;
693        }
694    }
695
696    void requeryCursor() {
697        if (mMessageAdapter.isScrolling()) {
698            mMessageAdapter.setNeedRequeryCursor(true);
699            return;
700        }
701        // TODO: async query?
702        Cursor cursor = getMessageCursor();
703        if (cursor != null) {
704            cursor.requery();
705        }
706    }
707
708    private Cursor getMessageCursor() {
709        return mMessageAdapter == null ? null : mMessageAdapter.getCursor();
710    }
711
712    public void insertSmiley(String smiley) {
713        mEdtInput.append(mMarkup.applyEmoticons(smiley));
714    }
715
716    public void closeChatSession() {
717        if (mChatSession != null) {
718            try {
719                mChatSession.leave();
720            } catch (RemoteException e) {
721                mHandler.showServiceErrorAlert();
722            }
723        } else {
724            // the conversation is already closed, clear data in database
725            ContentResolver cr = mContext.getContentResolver();
726            cr.delete(ContentUris.withAppendedId(Imps.Chats.CONTENT_URI, mChatId),
727                    null, null);
728        }
729        mScreen.finish();
730    }
731
732    public void closeChatSessionIfInactive() {
733        if (mChatSession != null) {
734            try {
735                mChatSession.leaveIfInactive();
736            } catch (RemoteException e) {
737                mHandler.showServiceErrorAlert();
738            }
739        }
740    }
741
742    public void viewProfile() {
743        Uri data = ContentUris.withAppendedId(Imps.Contacts.CONTENT_URI, mChatId);
744        Intent intent = new Intent(Intent.ACTION_VIEW, data);
745        mScreen.startActivity(intent);
746    }
747
748    public void blockContact() {
749        // TODO: unify with codes in ContactListView
750        DialogInterface.OnClickListener confirmListener = new DialogInterface.OnClickListener(){
751            public void onClick(DialogInterface dialog, int whichButton) {
752                try {
753                    IImConnection conn = mApp.getConnection(mProviderId);
754                    IContactListManager manager = conn.getContactListManager();
755                    manager.blockContact(mUserName);
756                    mScreen.finish();
757                } catch (RemoteException e) {
758                    mHandler.showServiceErrorAlert();
759                }
760            }
761        };
762
763        Resources r = getResources();
764
765        // The positive button is deliberately set as no so that
766        // the no is the default value
767        new AlertDialog.Builder(mContext)
768            .setTitle(R.string.confirm)
769            .setMessage(r.getString(R.string.confirm_block_contact, mNickName))
770            .setPositiveButton(R.string.yes, confirmListener) // default button
771            .setNegativeButton(R.string.no, null)
772            .setCancelable(false)
773            .show();
774    }
775
776    public long getProviderId() {
777        return mProviderId;
778    }
779
780    public long getAccountId() {
781        return mAccountId;
782    }
783
784    public String getUserName() {
785        return mUserName;
786    }
787
788    public long getChatId () {
789        try {
790            return mChatSession == null ? -1 : mChatSession.getId();
791        } catch (RemoteException e) {
792            mHandler.showServiceErrorAlert();
793            return -1;
794        }
795    }
796
797    public IChatSession getCurrentChatSession() {
798        return mChatSession;
799    }
800
801    private IChatSessionManager getChatSessionManager(long providerId) {
802        if (mChatSessionMgr == null) {
803            IImConnection conn = mApp.getConnection(providerId);
804            if (conn != null) {
805                try {
806                    mChatSessionMgr = conn.getChatSessionManager();
807                } catch (RemoteException e) {
808                    mHandler.showServiceErrorAlert();
809                }
810            }
811        }
812        return mChatSessionMgr;
813    }
814
815    private IChatSession getChatSession(Cursor cursor) {
816        long providerId = cursor.getLong(PROVIDER_COLUMN);
817        String username = cursor.getString(USERNAME_COLUMN);
818
819        IChatSessionManager sessionMgr = getChatSessionManager(providerId);
820        if (sessionMgr != null) {
821            try {
822                return sessionMgr.getChatSession(username);
823            } catch (RemoteException e) {
824                mHandler.showServiceErrorAlert();
825            }
826        }
827        return null;
828    }
829
830    boolean isGroupChat() {
831        return Imps.Contacts.TYPE_GROUP == mType;
832    }
833
834    void sendMessage() {
835        String msg = mEdtInput.getText().toString();
836
837        if (TextUtils.isEmpty(msg.trim())) {
838            return;
839        }
840
841        if (mChatSession != null) {
842            try {
843                mChatSession.sendMessage(msg);
844                mEdtInput.setText("");
845                mEdtInput.requestFocus();
846                requeryCursor();
847            } catch (RemoteException e) {
848                mHandler.showServiceErrorAlert();
849            }
850        }
851
852        // Close the soft on-screen keyboard if we're in landscape mode so the user can see the
853        // conversation.
854        Configuration config = getResources().getConfiguration();
855        if (config.orientation == config.ORIENTATION_LANDSCAPE) {
856            closeSoftKeyboard();
857        }
858    }
859
860    void registerChatListener() {
861        if (Log.isLoggable(ImApp.LOG_TAG, Log.DEBUG)){
862            log("registerChatListener");
863        }
864        try {
865            if (mChatSession != null) {
866                mChatSession.registerChatListener(mChatListener);
867            }
868            IImConnection conn = mApp.getConnection(mProviderId);
869            if (conn != null) {
870                IContactListManager listMgr = conn.getContactListManager();
871                listMgr.registerContactListListener(mContactListListener);
872            }
873            mApp.dismissChatNotification(mProviderId, mUserName);
874        } catch (RemoteException e) {
875            Log.w(ImApp.LOG_TAG, "<ChatView> registerChatListener fail:" + e.getMessage());
876        }
877    }
878
879    void unregisterChatListener() {
880        if (Log.isLoggable(ImApp.LOG_TAG, Log.DEBUG)){
881            log("unregisterChatListener");
882        }
883        try {
884            if (mChatSession != null) {
885                mChatSession.unregisterChatListener(mChatListener);
886            }
887            IImConnection conn = mApp.getConnection(mProviderId);
888            if (conn != null) {
889                IContactListManager listMgr = conn.getContactListManager();
890                listMgr.unregisterContactListListener(mContactListListener);
891            }
892        } catch (RemoteException e) {
893            Log.w(ImApp.LOG_TAG, "<ChatView> unregisterChatListener fail:" + e.getMessage());
894        }
895    }
896
897    void registerChatSessionListener() {
898        IChatSessionManager sessionMgr = getChatSessionManager(mProviderId);
899        if (sessionMgr != null) {
900            mChatSessionListener = new ChatSessionListener();
901            try {
902                sessionMgr.registerChatSessionListener(mChatSessionListener);
903            } catch (RemoteException e) {
904                mHandler.showServiceErrorAlert();
905            }
906        }
907    }
908
909    void unregisterChatSessionListener() {
910        if (mChatSessionListener != null) {
911            try {
912                IChatSessionManager sessionMgr = getChatSessionManager(mProviderId);
913                sessionMgr.unregisterChatSessionListener(mChatSessionListener);
914                // We unregister the listener when the chat session we are
915                // waiting for has been created or the activity is stopped.
916                // Clear the listener so that we won't unregister the listener
917                // twice.
918                mChatSessionListener = null;
919            } catch (RemoteException e) {
920                mHandler.showServiceErrorAlert();
921            }
922        }
923    }
924
925    void updateWarningView() {
926        int visibility = View.GONE;
927        int iconVisibility = View.GONE;
928        String message = null;
929        boolean isConnected;
930
931        try {
932            IImConnection conn = mApp.getConnection(mProviderId);
933            isConnected = (conn == null) ? false
934                    : conn.getState() != ImConnection.SUSPENDED;
935        } catch (RemoteException e) {
936            // do nothing
937            return;
938        }
939
940        if (isConnected) {
941            if (mType == Imps.Contacts.TYPE_TEMPORARY) {
942                visibility = View.VISIBLE;
943                message = mContext.getString(R.string.contact_not_in_list_warning, mNickName);
944            } else if (mPresenceStatus == Imps.Presence.OFFLINE) {
945                visibility = View.VISIBLE;
946                message = mContext.getString(R.string.contact_offline_warning, mNickName);
947            }
948        } else {
949            visibility = View.VISIBLE;
950            iconVisibility = View.VISIBLE;
951            message = mContext.getString(R.string.disconnected_warning);
952        }
953
954        mStatusWarningView.setVisibility(visibility);
955        if (visibility == View.VISIBLE) {
956            mWarningIcon.setVisibility(iconVisibility);
957            mWarningText.setText(message);
958        }
959    }
960
961    @Override
962    public boolean dispatchKeyEvent(KeyEvent event) {
963        userActionDetected();
964        return super.dispatchKeyEvent(event);
965    }
966
967    @Override
968    public boolean dispatchTouchEvent(MotionEvent ev) {
969        userActionDetected();
970        return super.dispatchTouchEvent(ev);
971    }
972
973    @Override
974    public boolean dispatchTrackballEvent(MotionEvent ev) {
975        userActionDetected();
976        return super.dispatchTrackballEvent(ev);
977    }
978
979    private void userActionDetected() {
980        if (mChatSession != null) {
981            try {
982                mChatSession.markAsRead();
983            } catch (RemoteException e) {
984                mHandler.showServiceErrorAlert();
985            }
986        }
987    }
988
989    private final class ChatViewHandler extends SimpleAlertHandler {
990        public ChatViewHandler() {
991            super(mScreen);
992        }
993
994        @Override
995        public void handleMessage(Message msg) {
996            long providerId = ((long)msg.arg1 << 32) | msg.arg2;
997            if (providerId != mProviderId) {
998                return;
999            }
1000
1001            switch(msg.what) {
1002            case ImApp.EVENT_CONNECTION_LOGGED_IN:
1003                log("Connection resumed");
1004                updateWarningView();
1005                return;
1006            case ImApp.EVENT_CONNECTION_SUSPENDED:
1007                log("Connection suspended");
1008                updateWarningView();
1009                return;
1010            }
1011
1012            super.handleMessage(msg);
1013        }
1014    }
1015
1016    class ChatSessionListener extends ChatSessionListenerAdapter {
1017        @Override
1018        public void onChatSessionCreated(IChatSession session) {
1019            try {
1020                if (session.isGroupChatSession()) {
1021                    final long id = session.getId();
1022                    unregisterChatSessionListener();
1023                    mHandler.post(new Runnable() {
1024                        public void run() {
1025                            bindChat(id);
1026                        }});
1027                }
1028            } catch (RemoteException e) {
1029                mHandler.showServiceErrorAlert();
1030            }
1031        }
1032    }
1033
1034    public static class DeltaCursor implements Cursor {
1035        static final String DELTA_COLUMN_NAME = "delta";
1036
1037        private Cursor mInnerCursor;
1038        private String[] mColumnNames;
1039        private int mDateColumn = -1;
1040        private int mDeltaColumn = -1;
1041
1042        DeltaCursor(Cursor cursor) {
1043            mInnerCursor = cursor;
1044
1045            String[] columnNames = cursor.getColumnNames();
1046            int len = columnNames.length;
1047
1048            mColumnNames = new String[len + 1];
1049
1050            for (int i = 0 ; i < len ; i++) {
1051                mColumnNames[i] = columnNames[i];
1052                if (mColumnNames[i].equals(Imps.Messages.DATE)) {
1053                    mDateColumn = i;
1054                }
1055            }
1056
1057            mDeltaColumn = len;
1058            mColumnNames[mDeltaColumn] = DELTA_COLUMN_NAME;
1059
1060            //if (DBG) log("##### DeltaCursor constructor: mDeltaColumn=" +
1061            //        mDeltaColumn + ", columnName=" + mColumnNames[mDeltaColumn]);
1062        }
1063
1064        public int getCount() {
1065            return mInnerCursor.getCount();
1066        }
1067
1068        public int getPosition() {
1069            return mInnerCursor.getPosition();
1070        }
1071
1072        public boolean move(int offset) {
1073            return mInnerCursor.move(offset);
1074        }
1075
1076        public boolean moveToPosition(int position) {
1077            return mInnerCursor.moveToPosition(position);
1078        }
1079
1080        public boolean moveToFirst() {
1081            return mInnerCursor.moveToFirst();
1082        }
1083
1084        public boolean moveToLast() {
1085            return mInnerCursor.moveToLast();
1086        }
1087
1088        public boolean moveToNext() {
1089            return mInnerCursor.moveToNext();
1090        }
1091
1092        public boolean moveToPrevious() {
1093            return mInnerCursor.moveToPrevious();
1094        }
1095
1096        public boolean isFirst() {
1097            return mInnerCursor.isFirst();
1098        }
1099
1100        public boolean isLast() {
1101            return mInnerCursor.isLast();
1102        }
1103
1104        public boolean isBeforeFirst() {
1105            return mInnerCursor.isBeforeFirst();
1106        }
1107
1108        public boolean isAfterLast() {
1109            return mInnerCursor.isAfterLast();
1110        }
1111
1112        public boolean deleteRow() {
1113            return mInnerCursor.deleteRow();
1114        }
1115
1116        public int getColumnIndex(String columnName) {
1117            if (DELTA_COLUMN_NAME.equals(columnName)) {
1118                return mDeltaColumn;
1119            }
1120
1121            int columnIndex = mInnerCursor.getColumnIndex(columnName);
1122            return columnIndex;
1123        }
1124
1125        public int getColumnIndexOrThrow(String columnName) throws IllegalArgumentException {
1126            if (DELTA_COLUMN_NAME.equals(columnName)) {
1127                return mDeltaColumn;
1128            }
1129
1130            return mInnerCursor.getColumnIndexOrThrow(columnName);
1131        }
1132
1133        public String getColumnName(int columnIndex) {
1134            if (columnIndex == mDeltaColumn) {
1135                return DELTA_COLUMN_NAME;
1136            }
1137
1138            return mInnerCursor.getColumnName(columnIndex);
1139        }
1140
1141        public int getColumnCount() {
1142            return mInnerCursor.getColumnCount() + 1;
1143        }
1144
1145        public boolean supportsUpdates() {
1146            return mInnerCursor.supportsUpdates();
1147        }
1148
1149        public boolean hasUpdates() {
1150            return mInnerCursor.hasUpdates();
1151        }
1152
1153        public boolean updateBlob(int columnIndex, byte[] value) {
1154            if (columnIndex == mDeltaColumn) {
1155                return false;
1156            }
1157
1158            return mInnerCursor.updateBlob(columnIndex, value);
1159        }
1160
1161        public boolean updateString(int columnIndex, String value) {
1162            if (columnIndex == mDeltaColumn) {
1163                return false;
1164            }
1165
1166            return mInnerCursor.updateString(columnIndex, value);
1167        }
1168
1169        public boolean updateShort(int columnIndex, short value) {
1170            if (columnIndex == mDeltaColumn) {
1171                return false;
1172            }
1173
1174            return mInnerCursor.updateShort(columnIndex, value);
1175        }
1176
1177        public boolean updateInt(int columnIndex, int value) {
1178            if (columnIndex == mDeltaColumn) {
1179                return false;
1180            }
1181
1182            return mInnerCursor.updateInt(columnIndex, value);
1183        }
1184
1185        public boolean updateLong(int columnIndex, long value) {
1186            if (columnIndex == mDeltaColumn) {
1187                return false;
1188            }
1189
1190            return mInnerCursor.updateLong(columnIndex, value);
1191        }
1192
1193        public boolean updateFloat(int columnIndex, float value) {
1194            if (columnIndex == mDeltaColumn) {
1195                return false;
1196            }
1197
1198            return mInnerCursor.updateFloat(columnIndex, value);
1199        }
1200
1201        public boolean updateDouble(int columnIndex, double value) {
1202            if (columnIndex == mDeltaColumn) {
1203                return false;
1204            }
1205
1206            return mInnerCursor.updateDouble(columnIndex, value);
1207        }
1208
1209        public boolean updateToNull(int columnIndex) {
1210            if (columnIndex == mDeltaColumn) {
1211                return false;
1212            }
1213
1214            return mInnerCursor.updateToNull(columnIndex);
1215        }
1216
1217        public boolean commitUpdates() {
1218            return mInnerCursor.commitUpdates();
1219        }
1220
1221        public boolean commitUpdates(Map<? extends Long,
1222                ? extends Map<String,Object>> values) {
1223            return mInnerCursor.commitUpdates(values);
1224        }
1225
1226        public void abortUpdates() {
1227            mInnerCursor.abortUpdates();
1228        }
1229
1230        public void deactivate() {
1231            mInnerCursor.deactivate();
1232        }
1233
1234        public boolean requery() {
1235            return mInnerCursor.requery();
1236        }
1237
1238        public void close() {
1239            mInnerCursor.close();
1240        }
1241
1242        public boolean isClosed() {
1243            return mInnerCursor.isClosed();
1244        }
1245
1246        public void registerContentObserver(ContentObserver observer) {
1247            mInnerCursor.registerContentObserver(observer);
1248        }
1249
1250        public void unregisterContentObserver(ContentObserver observer) {
1251            mInnerCursor.unregisterContentObserver(observer);
1252        }
1253
1254        public void registerDataSetObserver(DataSetObserver observer) {
1255            mInnerCursor.registerDataSetObserver(observer);
1256        }
1257
1258        public void unregisterDataSetObserver(DataSetObserver observer) {
1259            mInnerCursor.unregisterDataSetObserver(observer);
1260        }
1261
1262        public void setNotificationUri(ContentResolver cr, Uri uri) {
1263            mInnerCursor.setNotificationUri(cr, uri);
1264        }
1265
1266        public boolean getWantsAllOnMoveCalls() {
1267            return mInnerCursor.getWantsAllOnMoveCalls();
1268        }
1269
1270        public Bundle getExtras() {
1271            return mInnerCursor.getExtras();
1272        }
1273
1274        public Bundle respond(Bundle extras) {
1275            return mInnerCursor.respond(extras);
1276        }
1277
1278        public String[] getColumnNames() {
1279            return mColumnNames;
1280        }
1281
1282        private void checkPosition() {
1283            int pos = mInnerCursor.getPosition();
1284            int count = mInnerCursor.getCount();
1285
1286            if (-1 == pos || count == pos) {
1287                throw new CursorIndexOutOfBoundsException(pos, count);
1288            }
1289        }
1290
1291        public byte[] getBlob(int column) {
1292            checkPosition();
1293
1294            if (column == mDeltaColumn) {
1295                return null;
1296            }
1297
1298            return mInnerCursor.getBlob(column);
1299        }
1300
1301        public String getString(int column) {
1302            checkPosition();
1303
1304            if (column == mDeltaColumn) {
1305                long value = getDeltaValue();
1306                return Long.toString(value);
1307            }
1308
1309            return mInnerCursor.getString(column);
1310        }
1311
1312        public void copyStringToBuffer(int columnIndex, CharArrayBuffer buffer) {
1313            checkPosition();
1314
1315            if (columnIndex == mDeltaColumn) {
1316                long value = getDeltaValue();
1317                String strValue = Long.toString(value);
1318                int len = strValue.length();
1319                char[] data = buffer.data;
1320                if (data == null || data.length < len) {
1321                    buffer.data = strValue.toCharArray();
1322                } else {
1323                    strValue.getChars(0, len, data, 0);
1324                }
1325                buffer.sizeCopied = strValue.length();
1326            } else {
1327                mInnerCursor.copyStringToBuffer(columnIndex, buffer);
1328            }
1329        }
1330
1331        public short getShort(int column) {
1332            checkPosition();
1333
1334            if (column == mDeltaColumn) {
1335                return (short)getDeltaValue();
1336            }
1337
1338            return mInnerCursor.getShort(column);
1339        }
1340
1341        public int getInt(int column) {
1342            checkPosition();
1343
1344            if (column == mDeltaColumn) {
1345                return (int)getDeltaValue();
1346            }
1347
1348            return mInnerCursor.getInt(column);
1349        }
1350
1351        public long getLong(int column) {
1352        //if (DBG) log("DeltaCursor.getLong: column=" + column + ", mDeltaColumn=" + mDeltaColumn);
1353            checkPosition();
1354
1355            if (column == mDeltaColumn) {
1356                return getDeltaValue();
1357            }
1358
1359            return mInnerCursor.getLong(column);
1360        }
1361
1362        public float getFloat(int column) {
1363            checkPosition();
1364
1365            if (column == mDeltaColumn) {
1366                return getDeltaValue();
1367            }
1368
1369            return mInnerCursor.getFloat(column);
1370        }
1371
1372        public double getDouble(int column) {
1373            checkPosition();
1374
1375            if (column == mDeltaColumn) {
1376                return getDeltaValue();
1377            }
1378
1379            return mInnerCursor.getDouble(column);
1380        }
1381
1382        public boolean isNull(int column) {
1383            checkPosition();
1384
1385            if (column == mDeltaColumn) {
1386                return false;
1387            }
1388
1389            return mInnerCursor.isNull(column);
1390        }
1391
1392        private long getDeltaValue() {
1393            int pos = mInnerCursor.getPosition();
1394            //Log.i(LOG_TAG, "getDeltaValue: mPos=" + mPos);
1395
1396            long t2, t1;
1397
1398            if (pos == getCount()-1) {
1399                t1 = mInnerCursor.getLong(mDateColumn);
1400                t2 = System.currentTimeMillis();
1401            } else {
1402                mInnerCursor.moveToPosition(pos + 1);
1403                t2 = mInnerCursor.getLong(mDateColumn);
1404                mInnerCursor.moveToPosition(pos);
1405                t1 = mInnerCursor.getLong(mDateColumn);
1406            }
1407
1408            return t2 - t1;
1409        }
1410    }
1411
1412    private class MessageAdapter extends CursorAdapter implements AbsListView.OnScrollListener {
1413        private int mScrollState;
1414        private boolean mNeedRequeryCursor;
1415
1416        private int mNicknameColumn;
1417        private int mBodyColumn;
1418        private int mDateColumn;
1419        private int mTypeColumn;
1420        private int mErrCodeColumn;
1421        private int mDeltaColumn;
1422        private ChatBackgroundMaker mBgMaker;
1423
1424        private LayoutInflater mInflater;
1425
1426        public MessageAdapter(Activity context, Cursor c) {
1427            super(context, c, false);
1428            mInflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
1429            mBgMaker = new ChatBackgroundMaker(context);
1430            if (c != null) {
1431                resolveColumnIndex(c);
1432            }
1433        }
1434
1435        private void resolveColumnIndex(Cursor c) {
1436            mNicknameColumn = c.getColumnIndexOrThrow(Imps.Messages.NICKNAME);
1437            mBodyColumn = c.getColumnIndexOrThrow(Imps.Messages.BODY);
1438            mDateColumn = c.getColumnIndexOrThrow(Imps.Messages.DATE);
1439            mTypeColumn = c.getColumnIndexOrThrow(Imps.Messages.TYPE);
1440            mErrCodeColumn = c.getColumnIndexOrThrow(Imps.Messages.ERROR_CODE);
1441            mDeltaColumn = c.getColumnIndexOrThrow(DeltaCursor.DELTA_COLUMN_NAME);
1442        }
1443
1444        @Override
1445        public void changeCursor(Cursor cursor) {
1446            super.changeCursor(cursor);
1447            if (cursor != null) {
1448                resolveColumnIndex(cursor);
1449            }
1450        }
1451
1452        @Override
1453        public View newView(Context context, Cursor cursor, ViewGroup parent) {
1454            return mInflater.inflate(R.layout.new_message_item, parent, false);
1455        }
1456
1457        @Override
1458        public void bindView(View view, Context context, Cursor cursor) {
1459            MessageView chatMsgView = (MessageView) view;
1460
1461            int type = cursor.getInt(mTypeColumn);
1462            String contact = isGroupChat() ? cursor.getString(mNicknameColumn) : mNickName;
1463            String body = cursor.getString(mBodyColumn);
1464            long delta = cursor.getLong(mDeltaColumn);
1465            boolean showTimeStamp = (delta > SHOW_TIME_STAMP_INTERVAL);
1466            Date date = showTimeStamp ? new Date(cursor.getLong(mDateColumn)) : null;
1467
1468            switch (type) {
1469                case Imps.MessageType.INCOMING:
1470                    chatMsgView.bindIncomingMessage(contact, body, date, mMarkup, isScrolling());
1471                    break;
1472
1473                case Imps.MessageType.OUTGOING:
1474                case Imps.MessageType.POSTPONED:
1475                    int errCode = cursor.getInt(mErrCodeColumn);
1476                    if (errCode != 0) {
1477                        chatMsgView.bindErrorMessage(errCode);
1478                    } else {
1479                        chatMsgView.bindOutgoingMessage(body, date, mMarkup, isScrolling());
1480                    }
1481                    break;
1482
1483                default:
1484                    chatMsgView.bindPresenceMessage(contact, type, isGroupChat(), isScrolling());
1485            }
1486            if (!isScrolling()) {
1487                mBgMaker.setBackground(chatMsgView, contact, type);
1488            }
1489
1490            // if showTimeStamp is false for the latest message, then set a timer to query the
1491            // cursor again in a minute, so we can update the last message timestamp if no new
1492            // message is received
1493            if (cursor.getPosition() == cursor.getCount()-1) {
1494                if (Log.isLoggable(ImApp.LOG_TAG, Log.DEBUG)){
1495                    log("delta = " + delta + ", showTs=" + showTimeStamp);
1496                }
1497                if (!showTimeStamp) {
1498                    scheduleRequery(SHOW_TIME_STAMP_INTERVAL);
1499                } else {
1500                    cancelRequery();
1501                }
1502            }
1503        }
1504
1505        public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount,
1506                int totalItemCount) {
1507            // do nothing
1508        }
1509
1510        public void onScrollStateChanged(AbsListView view, int scrollState) {
1511            int oldState = mScrollState;
1512            mScrollState = scrollState;
1513
1514            if (mChatSession != null) {
1515                try {
1516                    mChatSession.markAsRead();
1517                } catch (RemoteException e) {
1518                    mHandler.showServiceErrorAlert();
1519                }
1520            }
1521
1522            if (oldState == OnScrollListener.SCROLL_STATE_FLING) {
1523                if (mNeedRequeryCursor) {
1524                    requeryCursor();
1525                } else {
1526                    notifyDataSetChanged();
1527                }
1528            }
1529        }
1530
1531        boolean isScrolling() {
1532            return mScrollState == OnScrollListener.SCROLL_STATE_FLING;
1533        }
1534
1535        void setNeedRequeryCursor(boolean requeryCursor) {
1536            mNeedRequeryCursor = requeryCursor;
1537        }
1538    }
1539}
1540