FileDocCategorySizeDatePackage
ChatView.javaAPI DocAndroid 1.5 API50960Wed May 06 22:42:46 BST 2009com.android.im.app

ChatView.java

/*
 * Copyright (C) 2008 Esmertec AG.
 * Copyright (C) 2008 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.im.app;

import java.util.ArrayList;
import java.util.Date;
import java.util.Map;

import android.app.Activity;
import android.app.AlertDialog;
import android.content.AsyncQueryHandler;
import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.res.Resources;
import android.content.res.Configuration;
import android.database.ContentObserver;
import android.database.Cursor;
import android.database.CursorIndexOutOfBoundsException;
import android.database.DataSetObserver;
import android.database.CharArrayBuffer;
import android.graphics.Typeface;
import android.net.Uri;
import android.os.Bundle;
import android.os.Message;
import android.os.RemoteException;
import android.provider.Browser;
import android.provider.Im;
import android.text.Editable;
import android.text.TextUtils;
import android.text.TextWatcher;
import android.text.style.StyleSpan;
import android.text.style.URLSpan;
import android.util.AttributeSet;
import android.util.Log;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.inputmethod.InputMethodManager;
import android.widget.AbsListView;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.CursorAdapter;
import android.widget.EditText;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.ListView;
import android.widget.TextView;
import android.widget.AbsListView.OnScrollListener;
import android.widget.AdapterView.OnItemClickListener;

import com.android.im.IChatListener;
import com.android.im.IChatSession;
import com.android.im.IChatSessionListener;
import com.android.im.IChatSessionManager;
import com.android.im.IContactList;
import com.android.im.IContactListListener;
import com.android.im.IContactListManager;
import com.android.im.IImConnection;
import com.android.im.R;
import com.android.im.app.adapter.ChatListenerAdapter;
import com.android.im.app.adapter.ChatSessionListenerAdapter;
import com.android.im.engine.Contact;
import com.android.im.engine.ImConnection;
import com.android.im.engine.ImErrorInfo;
import com.android.im.plugin.BrandingResourceIDs;

public class ChatView extends LinearLayout {
    // This projection and index are set for the query of active chats
    static final String[] CHAT_PROJECTION = {
        Im.Contacts._ID,
        Im.Contacts.ACCOUNT,
        Im.Contacts.PROVIDER,
        Im.Contacts.USERNAME,
        Im.Contacts.NICKNAME,
        Im.Contacts.TYPE,
        Im.Presence.PRESENCE_STATUS,
        Im.Chats.LAST_UNREAD_MESSAGE,
    };
    static final int CONTACT_ID_COLUMN             = 0;
    static final int ACCOUNT_COLUMN                = 1;
    static final int PROVIDER_COLUMN               = 2;
    static final int USERNAME_COLUMN               = 3;
    static final int NICKNAME_COLUMN               = 4;
    static final int TYPE_COLUMN                   = 5;
    static final int PRESENCE_STATUS_COLUMN        = 6;
    static final int LAST_UNREAD_MESSAGE_COLUMN    = 7;

    static final String[] INVITATION_PROJECT = {
        Im.Invitation._ID,
        Im.Invitation.PROVIDER,
        Im.Invitation.SENDER,
    };
    static final int INVITATION_ID_COLUMN = 0;
    static final int INVITATION_PROVIDER_COLUMN = 1;
    static final int INVITATION_SENDER_COLUMN = 2;

    static final StyleSpan STYLE_BOLD = new StyleSpan(Typeface.BOLD);

    Markup mMarkup;

    Activity mScreen;
    ImApp mApp;
    SimpleAlertHandler mHandler;
    Cursor mCursor;

    private ImageView   mStatusIcon;
    private TextView    mTitle;
    /*package*/ListView    mHistory;
    EditText    mEdtInput;
    private Button      mSendButton;
    private View mStatusWarningView;
    private ImageView mWarningIcon;
    private TextView mWarningText;

    private MessageAdapter mMessageAdapter;
    private IChatSessionManager mChatSessionMgr;
    private IChatSessionListener mChatSessionListener;

    private IChatSession mChatSession;
    private long mChatId;
    int mType;
    String mNickName;
    String mUserName;
    long mProviderId;
    long mAccountId;
    long mInvitationId;
    private int mPresenceStatus;

    private int mViewType;

    private static final int VIEW_TYPE_CHAT = 1;
    private static final int VIEW_TYPE_INVITATION = 2;
    private static final int VIEW_TYPE_SUBSCRIPTION = 3;

    private static final long SHOW_TIME_STAMP_INTERVAL = 60 * 1000;     // 1 minute
    private static final int QUERY_TOKEN = 10;

    // Async QueryHandler
    private final class QueryHandler extends AsyncQueryHandler {
        public QueryHandler(Context context) {
            super(context.getContentResolver());
        }

        @Override
        protected void onQueryComplete(int token, Object cookie, Cursor c) {
            Cursor cursor = new DeltaCursor(c);

            if (Log.isLoggable(ImApp.LOG_TAG, Log.DEBUG)){
                log("onQueryComplete: cursor.count=" + cursor.getCount());
            }

            mMessageAdapter.changeCursor(cursor);
        }
    }
    private QueryHandler mQueryHandler;

    private class RequeryCallback implements Runnable {
        public void run() {
            if (Log.isLoggable(ImApp.LOG_TAG, Log.DEBUG)){
                log("RequeryCallback");
            }
            requeryCursor();
        }
    }
    private RequeryCallback mRequeryCallback = null;

    private OnItemClickListener mOnItemClickListener = new OnItemClickListener() {
        public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
            if (!(view instanceof MessageView)) {
                return;
            }
            URLSpan[] links = ((MessageView)view).getMessageLinks();
            if (links.length == 0){
                return;
            }

            final ArrayList<String> linkUrls = new ArrayList<String>(links.length);
            for (URLSpan u : links) {
                linkUrls.add(u.getURL());
            }
            ArrayAdapter<String> a = new ArrayAdapter<String>(mScreen,
                    android.R.layout.select_dialog_item, linkUrls);
            AlertDialog.Builder b = new AlertDialog.Builder(mScreen);
            b.setTitle(R.string.select_link_title);
            b.setCancelable(true);
            b.setAdapter(a, new DialogInterface.OnClickListener() {
                public void onClick(DialogInterface dialog, int which) {
                    Uri uri = Uri.parse(linkUrls.get(which));
                    Intent intent = new Intent(Intent.ACTION_VIEW, uri);
                    intent.putExtra(Browser.EXTRA_APPLICATION_ID, mScreen.getPackageName());
                    mScreen.startActivity(intent);
                }
            });
            b.setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() {
                public void onClick(DialogInterface dialog, int which) {
                    dialog.dismiss();
                }
            });
            b.show();
        }
    };

    private IChatListener mChatListener = new ChatListenerAdapter() {
        @Override
        public void onIncomingMessage(IChatSession ses,
                com.android.im.engine.Message msg) {
            scheduleRequery(0);
        }

        @Override
        public void onContactJoined(IChatSession ses, Contact contact) {
            scheduleRequery(0);
        }

        @Override
        public void onContactLeft(IChatSession ses, Contact contact) {
            scheduleRequery(0);
        }

        @Override
        public void onSendMessageError(IChatSession ses,
                com.android.im.engine.Message msg, ImErrorInfo error) {
            scheduleRequery(0);
        }
    };

    private Runnable mUpdateChatCallback = new Runnable() {
        public void run() {
            if (mCursor.requery() && mCursor.moveToFirst()) {
                updateChat();
            }
        }
    };
    private IContactListListener mContactListListener = new IContactListListener.Stub () {
        public void onAllContactListsLoaded() {
        }

        public void onContactChange(int type, IContactList list, Contact contact){
        }

        public void onContactError(int errorType, ImErrorInfo error,
                String listName, Contact contact) {
        }

        public void onContactsPresenceUpdate(Contact[] contacts) {
            if (Log.isLoggable(ImApp.LOG_TAG, Log.DEBUG)) {
                log("onContactsPresenceUpdate()");
            }
            for (Contact c : contacts) {
                if (c.getAddress().getFullName().equals(mUserName)) {
                    mHandler.post(mUpdateChatCallback);
                    scheduleRequery(0);
                    break;
                }
            }
        }
    };

    static final void log(String msg) {
        Log.d(ImApp.LOG_TAG, "<ChatView> " +msg);
    }

    public ChatView(Context context, AttributeSet attrs) {
        super(context, attrs);
        mScreen = (Activity) context;
        mApp = ImApp.getApplication(mScreen);
        mHandler = new ChatViewHandler();
    }

    void registerForConnEvents() {
        mApp.registerForConnEvents(mHandler);
    }

    void unregisterForConnEvents() {
        mApp.unregisterForConnEvents(mHandler);
    }

    @Override
    protected void onFinishInflate() {
        mStatusIcon     = (ImageView) findViewById(R.id.statusIcon);
        mTitle          = (TextView) findViewById(R.id.title);
        mHistory        = (ListView) findViewById(R.id.history);
        mEdtInput       = (EditText) findViewById(R.id.edtInput);
        mSendButton     = (Button)findViewById(R.id.btnSend);
        mHistory.setOnItemClickListener(mOnItemClickListener);

        mStatusWarningView = findViewById(R.id.warning);
        mWarningIcon = (ImageView)findViewById(R.id.warningIcon);
        mWarningText = (TextView)findViewById(R.id.warningText);

        Button acceptInvitation = (Button)findViewById(R.id.btnAccept);
        Button declineInvitation= (Button)findViewById(R.id.btnDecline);

        Button approveSubscription = (Button)findViewById(R.id.btnApproveSubscription);
        Button declineSubscription = (Button)findViewById(R.id.btnDeclineSubscription);

        acceptInvitation.setOnClickListener(new OnClickListener() {
            public void onClick(View v) {
                acceptInvitation();
            }
        });
        declineInvitation.setOnClickListener(new OnClickListener() {
            public void onClick(View v) {
                declineInvitation();
            }
        });

        approveSubscription.setOnClickListener(new OnClickListener(){
            public void onClick(View v) {
                approveSubscription();
            }
        });
        declineSubscription.setOnClickListener(new OnClickListener(){
            public void onClick(View v) {
                declineSubscription();
            }
        });

        mEdtInput.setOnKeyListener(new OnKeyListener(){
            public boolean onKey(View v, int keyCode, KeyEvent event) {
                if (event.getAction() == KeyEvent.ACTION_DOWN) {
                    switch (keyCode) {
                        case KeyEvent.KEYCODE_DPAD_CENTER:
                            sendMessage();
                            return true;

                        case KeyEvent.KEYCODE_ENTER:
                            if (event.isAltPressed()) {
                                mEdtInput.append("\n");
                                return true;
                            }
                    }
                }
                return false;
            }
        });

        mEdtInput.setOnEditorActionListener(new TextView.OnEditorActionListener() {
            public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
                if (event != null) {
                    if (event.isAltPressed()) {
                        return false;
                    }
                }

                sendMessage();
                return true;
            }
        });

        // TODO: this is a hack to implement BUG #1611278, when dispatchKeyEvent() works with
        // the soft keyboard, we should remove this hack.
        mEdtInput.addTextChangedListener(new TextWatcher() {
            public void beforeTextChanged(CharSequence s, int start, int before, int after) {
            }

            public void onTextChanged(CharSequence s, int start, int before, int after) {
                //log("TextWatcher: " + s);
                userActionDetected();
            }

            public void afterTextChanged(Editable s) {
            }
        });

        mSendButton.setOnClickListener(new OnClickListener() {
            public void onClick(View v) {
                sendMessage();
            }
        });
    }

    public void onResume(){
        if (mViewType == VIEW_TYPE_CHAT) {
            Cursor cursor = getMessageCursor();
            if (cursor == null) {
                startQuery();
            } else {
                requeryCursor();
            }
            updateWarningView();
        }
        registerChatListener();
        registerForConnEvents();
    }

    public void onPause(){
        Cursor cursor = getMessageCursor();
        if (cursor != null) {
            cursor.deactivate();
        }
        cancelRequery();
        if (mViewType == VIEW_TYPE_CHAT && mChatSession != null) {
            try {
                mChatSession.markAsRead();
            } catch (RemoteException e) {
                mHandler.showServiceErrorAlert();
            }
        }
        unregisterChatListener();
        unregisterForConnEvents();
        unregisterChatSessionListener();
    }

    private void closeSoftKeyboard() {
        InputMethodManager inputMethodManager =
            (InputMethodManager)mApp.getSystemService(Context.INPUT_METHOD_SERVICE);

        inputMethodManager.hideSoftInputFromWindow(mEdtInput.getWindowToken(), 0);
    }

    void updateChat() {
        setViewType(VIEW_TYPE_CHAT);

        long oldChatId = mChatId;

        updateContactInfo();

        setStatusIcon();
        setTitle();

        IImConnection conn = mApp.getConnection(mProviderId);
        if (conn == null) {
            if (Log.isLoggable(ImApp.LOG_TAG, Log.DEBUG)) log("Connection has been signed out");
            mScreen.finish();
            return;
        }

        BrandingResources brandingRes = mApp.getBrandingResource(mProviderId);
        mHistory.setBackgroundDrawable(
                brandingRes.getDrawable(BrandingResourceIDs.DRAWABLE_CHAT_WATERMARK));

        if (mMarkup == null) {
            mMarkup = new Markup(brandingRes);
        }

        if (mMessageAdapter == null) {
            mMessageAdapter = new MessageAdapter(mScreen, null);
            mHistory.setAdapter(mMessageAdapter);
        }

        // only change the message adapter when we switch to another chat
        if (mChatId != oldChatId) {
            startQuery();
            mEdtInput.setText("");
        }

        updateWarningView();
    }

    private void updateContactInfo() {
        mChatId = mCursor.getLong(CONTACT_ID_COLUMN);
        mProviderId = mCursor.getLong(PROVIDER_COLUMN);
        mAccountId = mCursor.getLong(ACCOUNT_COLUMN);
        mPresenceStatus = mCursor.getInt(PRESENCE_STATUS_COLUMN);
        mType = mCursor.getInt(TYPE_COLUMN);
        mUserName = mCursor.getString(USERNAME_COLUMN);
        mNickName = mCursor.getString(NICKNAME_COLUMN);
    }

    private void setTitle() {
        if (mType == Im.Contacts.TYPE_GROUP) {
            final String[] projection = {Im.GroupMembers.NICKNAME};
            Uri memberUri = ContentUris.withAppendedId(Im.GroupMembers.CONTENT_URI, mChatId);
            ContentResolver cr = mScreen.getContentResolver();
            Cursor c = cr.query(memberUri, projection, null, null, null);
            StringBuilder buf = new StringBuilder();
            if(c != null) {
                while(c.moveToNext()) {
                    buf.append(c.getString(0));
                    if(!c.isLast()) {
                        buf.append(',');
                    }
                }
                c.close();
            }
            mTitle.setText(mContext.getString(R.string.chat_with, buf.toString()));
        } else {
            mTitle.setText(mContext.getString(R.string.chat_with, mNickName));
        }
    }

    private void setStatusIcon() {
        if (mType == Im.Contacts.TYPE_GROUP) {
            // hide the status icon for group chat.
            mStatusIcon.setVisibility(GONE);
        } else {
            mStatusIcon.setVisibility(VISIBLE);
            BrandingResources brandingRes = mApp.getBrandingResource(mProviderId);
            int presenceResId = PresenceUtils.getStatusIconId(mPresenceStatus);
            mStatusIcon.setImageDrawable(brandingRes.getDrawable(presenceResId));
        }
    }

    public void bindChat(long chatId) {
        if (mCursor != null) {
            mCursor.deactivate();
        }
        Uri contactUri = ContentUris.withAppendedId(Im.Contacts.CONTENT_URI, chatId);
        mCursor = mScreen.managedQuery(contactUri, CHAT_PROJECTION, null, null);
        if (mCursor == null || !mCursor.moveToFirst()) {
            if (Log.isLoggable(ImApp.LOG_TAG, Log.DEBUG)){
                log("Failed to query chat: " + chatId);
            }
            mScreen.finish();
            return;
        } else {
            mChatSession = getChatSession(mCursor);
            updateChat();
            registerChatListener();
        }
    }

    public void bindInvitation(long invitationId) {
        Uri uri = ContentUris.withAppendedId(Im.Invitation.CONTENT_URI, invitationId);
        ContentResolver cr = mScreen.getContentResolver();
        Cursor cursor = cr.query(uri, INVITATION_PROJECT, null, null, null);
        if (cursor == null || !cursor.moveToFirst()) {
            if (Log.isLoggable(ImApp.LOG_TAG, Log.DEBUG)){
                log("Failed to query invitation: " + invitationId);
            }
            mScreen.finish();
        } else {
            setViewType(VIEW_TYPE_INVITATION);

            mInvitationId = cursor.getLong(INVITATION_ID_COLUMN);
            mProviderId = cursor.getLong(INVITATION_PROVIDER_COLUMN);
            String sender = cursor.getString(INVITATION_SENDER_COLUMN);

            TextView mInvitationText = (TextView)findViewById(R.id.txtInvitation);
            mInvitationText.setText(mContext.getString(R.string.invitation_prompt, sender));
            mTitle.setText(mContext.getString(R.string.chat_with, sender));
        }

        if (cursor != null) {
            cursor.close();
        }
    }

    public void bindSubscription(long providerId, String from) {
        mProviderId = providerId;
        mUserName = from;

        setViewType(VIEW_TYPE_SUBSCRIPTION);

        TextView text =  (TextView)findViewById(R.id.txtSubscription);
        String displayableAddr = ImpsAddressUtils.getDisplayableAddress(from);
        text.setText(mContext.getString(R.string.subscription_prompt, displayableAddr));
        mTitle.setText(mContext.getString(R.string.chat_with, displayableAddr));

        mApp.dismissChatNotification(providerId, from);
    }

    void acceptInvitation() {
        try {

            IImConnection conn = mApp.getConnection(mProviderId);
            if (conn != null) {
                // register a chat session listener and wait for a group chat
                // session to be created after we accept the invitation.
                registerChatSessionListener();
                conn.acceptInvitation(mInvitationId);
            }
        } catch (RemoteException e) {
            mHandler.showServiceErrorAlert();
        }
    }

    void declineInvitation() {
        try {
            IImConnection conn = mApp.getConnection(mProviderId);
            if (conn != null) {
                conn.rejectInvitation(mInvitationId);
            }
            mScreen.finish();
        } catch (RemoteException e) {
            mHandler.showServiceErrorAlert();
        }
    }

    void approveSubscription() {
        IImConnection conn = mApp.getConnection(mProviderId);
        try {
            IContactListManager manager = conn.getContactListManager();
            manager.approveSubscription(mUserName);
        } catch (RemoteException ex) {
            mHandler.showServiceErrorAlert();
        }
        mScreen.finish();
    }

    void declineSubscription() {
        IImConnection conn = mApp.getConnection(mProviderId);
        try {
            IContactListManager manager = conn.getContactListManager();
            manager.declineSubscription(mUserName);
        } catch (RemoteException ex) {
            mHandler.showServiceErrorAlert();
        }
        mScreen.finish();
    }

    private void setViewType(int type) {
        mViewType = type;
        if (type == VIEW_TYPE_CHAT) {
            findViewById(R.id.invitationPanel).setVisibility(GONE);
            findViewById(R.id.subscription).setVisibility(GONE);
            setChatViewEnabled(true);
        }  else if (type == VIEW_TYPE_INVITATION) {
            setChatViewEnabled(false);
            findViewById(R.id.invitationPanel).setVisibility(VISIBLE);
            findViewById(R.id.btnAccept).requestFocus();
        } else if (type == VIEW_TYPE_SUBSCRIPTION) {
            setChatViewEnabled(false);
            findViewById(R.id.subscription).setVisibility(VISIBLE);
            findViewById(R.id.btnApproveSubscription).requestFocus();
        }
    }

    private void setChatViewEnabled(boolean enabled) {
        mEdtInput.setEnabled(enabled);
        mSendButton.setEnabled(enabled);
        if (enabled) {
            mEdtInput.requestFocus();
        } else {
            mHistory.setAdapter(null);
        }
    }

    private void startQuery() {
        if (mQueryHandler == null) {
            mQueryHandler = new QueryHandler(mContext);
        } else {
            // Cancel any pending queries
            mQueryHandler.cancelOperation(QUERY_TOKEN);
        }

        Uri uri;
        if (Im.Contacts.TYPE_GROUP == mType) {
            uri = ContentUris.withAppendedId(Im.GroupMessages.CONTENT_URI_GROUP_MESSAGES_BY, mChatId);
        } else {
            uri = Im.Messages.getContentUriByContact(mProviderId, mAccountId, mUserName);
        }

        if (Log.isLoggable(ImApp.LOG_TAG, Log.DEBUG)){
            log("queryCursor: uri=" + uri);
        }

        mQueryHandler.startQuery(QUERY_TOKEN, null,
                uri,
                null,
                null /* selection */,
                null /* selection args */,
                null);
    }

    void scheduleRequery(long interval) {
        if (mRequeryCallback == null) {
            mRequeryCallback = new RequeryCallback();
        } else {
            mHandler.removeCallbacks(mRequeryCallback);
        }

        if (Log.isLoggable(ImApp.LOG_TAG, Log.DEBUG)){
            log("scheduleRequery");
        }
        mHandler.postDelayed(mRequeryCallback, interval);
    }

    void cancelRequery() {
        if (mRequeryCallback != null) {
            if (Log.isLoggable(ImApp.LOG_TAG, Log.DEBUG)){
                log("cancelRequery");
            }
            mHandler.removeCallbacks(mRequeryCallback);
            mRequeryCallback = null;
        }
    }

    void requeryCursor() {
        if (mMessageAdapter.isScrolling()) {
            mMessageAdapter.setNeedRequeryCursor(true);
            return;
        }
        // TODO: async query?
        Cursor cursor = getMessageCursor();
        if (cursor != null) {
            cursor.requery();
        }
    }

    private Cursor getMessageCursor() {
        return mMessageAdapter == null ? null : mMessageAdapter.getCursor();
    }

    public void insertSmiley(String smiley) {
        mEdtInput.append(mMarkup.applyEmoticons(smiley));
    }

    public void closeChatSession() {
        if (mChatSession != null) {
            try {
                mChatSession.leave();
            } catch (RemoteException e) {
                mHandler.showServiceErrorAlert();
            }
        } else {
            // the conversation is already closed, clear data in database
            ContentResolver cr = mContext.getContentResolver();
            cr.delete(ContentUris.withAppendedId(Im.Chats.CONTENT_URI, mChatId),
                    null, null);
        }
        mScreen.finish();
    }

    public void closeChatSessionIfInactive() {
        if (mChatSession != null) {
            try {
                mChatSession.leaveIfInactive();
            } catch (RemoteException e) {
                mHandler.showServiceErrorAlert();
            }
        }
    }

    public void viewProfile() {
        Uri data = ContentUris.withAppendedId(Im.Contacts.CONTENT_URI, mChatId);
        Intent intent = new Intent(Intent.ACTION_VIEW, data);
        mScreen.startActivity(intent);
    }

    public void blockContact() {
        // TODO: unify with codes in ContactListView
        DialogInterface.OnClickListener confirmListener = new DialogInterface.OnClickListener(){
            public void onClick(DialogInterface dialog, int whichButton) {
                try {
                    IImConnection conn = mApp.getConnection(mProviderId);
                    IContactListManager manager = conn.getContactListManager();
                    manager.blockContact(mUserName);
                    mScreen.finish();
                } catch (RemoteException e) {
                    mHandler.showServiceErrorAlert();
                }
            }
        };

        Resources r = getResources();

        // The positive button is deliberately set as no so that
        // the no is the default value
        new AlertDialog.Builder(mContext)
            .setTitle(R.string.confirm)
            .setMessage(r.getString(R.string.confirm_block_contact, mNickName))
            .setPositiveButton(R.string.yes, confirmListener) // default button
            .setNegativeButton(R.string.no, null)
            .setCancelable(false)
            .show();
    }

    public long getProviderId() {
        return mProviderId;
    }

    public long getAccountId() {
        return mAccountId;
    }

    public String getUserName() {
        return mUserName;
    }

    public long getChatId () {
        try {
            return mChatSession == null ? -1 : mChatSession.getId();
        } catch (RemoteException e) {
            mHandler.showServiceErrorAlert();
            return -1;
        }
    }

    public IChatSession getCurrentChatSession() {
        return mChatSession;
    }

    private IChatSessionManager getChatSessionManager(long providerId) {
        if (mChatSessionMgr == null) {
            IImConnection conn = mApp.getConnection(providerId);
            if (conn != null) {
                try {
                    mChatSessionMgr = conn.getChatSessionManager();
                } catch (RemoteException e) {
                    mHandler.showServiceErrorAlert();
                }
            }
        }
        return mChatSessionMgr;
    }

    private IChatSession getChatSession(Cursor cursor) {
        long providerId = cursor.getLong(PROVIDER_COLUMN);
        String username = cursor.getString(USERNAME_COLUMN);

        IChatSessionManager sessionMgr = getChatSessionManager(providerId);
        if (sessionMgr != null) {
            try {
                return sessionMgr.getChatSession(username);
            } catch (RemoteException e) {
                mHandler.showServiceErrorAlert();
            }
        }
        return null;
    }

    boolean isGroupChat() {
        return Im.Contacts.TYPE_GROUP == mType;
    }

    void sendMessage() {
        String msg = mEdtInput.getText().toString();

        if (TextUtils.isEmpty(msg.trim())) {
            return;
        }

        if (mChatSession != null) {
            try {
                mChatSession.sendMessage(msg);
                mEdtInput.setText("");
                mEdtInput.requestFocus();
                requeryCursor();
            } catch (RemoteException e) {
                mHandler.showServiceErrorAlert();
            }
        }

        // Close the soft on-screen keyboard if we're in landscape mode so the user can see the
        // conversation.
        Configuration config = getResources().getConfiguration();
        if (config.orientation == config.ORIENTATION_LANDSCAPE) {
            closeSoftKeyboard();
        }
    }

    void registerChatListener() {
        if (Log.isLoggable(ImApp.LOG_TAG, Log.DEBUG)){
            log("registerChatListener");
        }
        try {
            if (mChatSession != null) {
                mChatSession.registerChatListener(mChatListener);
            }
            IImConnection conn = mApp.getConnection(mProviderId);
            if (conn != null) {
                IContactListManager listMgr = conn.getContactListManager();
                listMgr.registerContactListListener(mContactListListener);
            }
            mApp.dismissChatNotification(mProviderId, mUserName);
        } catch (RemoteException e) {
            Log.w(ImApp.LOG_TAG, "<ChatView> registerChatListener fail:" + e.getMessage());
        }
    }

    void unregisterChatListener() {
        if (Log.isLoggable(ImApp.LOG_TAG, Log.DEBUG)){
            log("unregisterChatListener");
        }
        try {
            if (mChatSession != null) {
                mChatSession.unregisterChatListener(mChatListener);
            }
            IImConnection conn = mApp.getConnection(mProviderId);
            if (conn != null) {
                IContactListManager listMgr = conn.getContactListManager();
                listMgr.unregisterContactListListener(mContactListListener);
            }
        } catch (RemoteException e) {
            Log.w(ImApp.LOG_TAG, "<ChatView> unregisterChatListener fail:" + e.getMessage());
        }
    }

    void registerChatSessionListener() {
        IChatSessionManager sessionMgr = getChatSessionManager(mProviderId);
        if (sessionMgr != null) {
            mChatSessionListener = new ChatSessionListener();
            try {
                sessionMgr.registerChatSessionListener(mChatSessionListener);
            } catch (RemoteException e) {
                mHandler.showServiceErrorAlert();
            }
        }
    }

    void unregisterChatSessionListener() {
        if (mChatSessionListener != null) {
            try {
                IChatSessionManager sessionMgr = getChatSessionManager(mProviderId);
                sessionMgr.unregisterChatSessionListener(mChatSessionListener);
                // We unregister the listener when the chat session we are
                // waiting for has been created or the activity is stopped.
                // Clear the listener so that we won't unregister the listener
                // twice.
                mChatSessionListener = null;
            } catch (RemoteException e) {
                mHandler.showServiceErrorAlert();
            }
        }
    }

    void updateWarningView() {
        int visibility = View.GONE;
        int iconVisibility = View.GONE;
        String message = null;
        boolean isConnected;

        try {
            IImConnection conn = mApp.getConnection(mProviderId);
            isConnected = (conn == null) ? false
                    : conn.getState() != ImConnection.SUSPENDED;
        } catch (RemoteException e) {
            // do nothing
            return;
        }

        if (isConnected) {
            if (mType == Im.Contacts.TYPE_TEMPORARY) {
                visibility = View.VISIBLE;
                message = mContext.getString(R.string.contact_not_in_list_warning, mNickName);
            } else if (mPresenceStatus == Im.Presence.OFFLINE) {
                visibility = View.VISIBLE;
                message = mContext.getString(R.string.contact_offline_warning, mNickName);
            }
        } else {
            visibility = View.VISIBLE;
            iconVisibility = View.VISIBLE;
            message = mContext.getString(R.string.disconnected_warning);
        }

        mStatusWarningView.setVisibility(visibility);
        if (visibility == View.VISIBLE) {
            mWarningIcon.setVisibility(iconVisibility);
            mWarningText.setText(message);
        }
    }

    @Override
    public boolean dispatchKeyEvent(KeyEvent event) {
        userActionDetected();
        return super.dispatchKeyEvent(event);
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        userActionDetected();
        return super.dispatchTouchEvent(ev);
    }

    @Override
    public boolean dispatchTrackballEvent(MotionEvent ev) {
        userActionDetected();
        return super.dispatchTrackballEvent(ev);
    }

    private void userActionDetected() {
        if (mChatSession != null) {
            try {
                mChatSession.markAsRead();
            } catch (RemoteException e) {
                mHandler.showServiceErrorAlert();
            }
        }
    }

    private final class ChatViewHandler extends SimpleAlertHandler {
        public ChatViewHandler() {
            super(mScreen);
        }

        @Override
        public void handleMessage(Message msg) {
            long providerId = ((long)msg.arg1 << 32) | msg.arg2;
            if (providerId != mProviderId) {
                return;
            }

            switch(msg.what) {
            case ImApp.EVENT_CONNECTION_LOGGED_IN:
                log("Connection resumed");
                updateWarningView();
                return;
            case ImApp.EVENT_CONNECTION_SUSPENDED:
                log("Connection suspended");
                updateWarningView();
                return;
            }

            super.handleMessage(msg);
        }
    }

    class ChatSessionListener extends ChatSessionListenerAdapter {
        @Override
        public void onChatSessionCreated(IChatSession session) {
            try {
                if (session.isGroupChatSession()) {
                    final long id = session.getId();
                    unregisterChatSessionListener();
                    mHandler.post(new Runnable() {
                        public void run() {
                            bindChat(id);
                        }});
                }
            } catch (RemoteException e) {
                mHandler.showServiceErrorAlert();
            }
        }
    }

    public static class DeltaCursor implements Cursor {
        static final String DELTA_COLUMN_NAME = "delta";

        private Cursor mInnerCursor;
        private String[] mColumnNames;
        private int mDateColumn = -1;
        private int mDeltaColumn = -1;

        DeltaCursor(Cursor cursor) {
            mInnerCursor = cursor;

            String[] columnNames = cursor.getColumnNames();
            int len = columnNames.length;

            mColumnNames = new String[len + 1];

            for (int i = 0 ; i < len ; i++) {
                mColumnNames[i] = columnNames[i];
                if (mColumnNames[i].equals(Im.BaseMessageColumns.DATE)) {
                    mDateColumn = i;
                }
            }

            mDeltaColumn = len;
            mColumnNames[mDeltaColumn] = DELTA_COLUMN_NAME;

            //if (DBG) log("##### DeltaCursor constructor: mDeltaColumn=" +
            //        mDeltaColumn + ", columnName=" + mColumnNames[mDeltaColumn]);
        }

        public int getCount() {
            return mInnerCursor.getCount();
        }

        public int getPosition() {
            return mInnerCursor.getPosition();
        }

        public boolean move(int offset) {
            return mInnerCursor.move(offset);
        }

        public boolean moveToPosition(int position) {
            return mInnerCursor.moveToPosition(position);
        }

        public boolean moveToFirst() {
            return mInnerCursor.moveToFirst();
        }

        public boolean moveToLast() {
            return mInnerCursor.moveToLast();
        }

        public boolean moveToNext() {
            return mInnerCursor.moveToNext();
        }

        public boolean moveToPrevious() {
            return mInnerCursor.moveToPrevious();
        }

        public boolean isFirst() {
            return mInnerCursor.isFirst();
        }

        public boolean isLast() {
            return mInnerCursor.isLast();
        }

        public boolean isBeforeFirst() {
            return mInnerCursor.isBeforeFirst();
        }

        public boolean isAfterLast() {
            return mInnerCursor.isAfterLast();
        }

        public boolean deleteRow() {
            return mInnerCursor.deleteRow();
        }

        public int getColumnIndex(String columnName) {
            if (DELTA_COLUMN_NAME.equals(columnName)) {
                return mDeltaColumn;
            }

            int columnIndex = mInnerCursor.getColumnIndex(columnName);
            return columnIndex;
        }

        public int getColumnIndexOrThrow(String columnName) throws IllegalArgumentException {
            if (DELTA_COLUMN_NAME.equals(columnName)) {
                return mDeltaColumn;
            }

            return mInnerCursor.getColumnIndexOrThrow(columnName);
        }

        public String getColumnName(int columnIndex) {
            if (columnIndex == mDeltaColumn) {
                return DELTA_COLUMN_NAME;
            }

            return mInnerCursor.getColumnName(columnIndex);
        }

        public int getColumnCount() {
            return mInnerCursor.getColumnCount() + 1;
        }

        public boolean supportsUpdates() {
            return mInnerCursor.supportsUpdates();
        }

        public boolean hasUpdates() {
            return mInnerCursor.hasUpdates();
        }

        public boolean updateBlob(int columnIndex, byte[] value) {
            if (columnIndex == mDeltaColumn) {
                return false;
            }

            return mInnerCursor.updateBlob(columnIndex, value);
        }

        public boolean updateString(int columnIndex, String value) {
            if (columnIndex == mDeltaColumn) {
                return false;
            }

            return mInnerCursor.updateString(columnIndex, value);
        }

        public boolean updateShort(int columnIndex, short value) {
            if (columnIndex == mDeltaColumn) {
                return false;
            }

            return mInnerCursor.updateShort(columnIndex, value);
        }

        public boolean updateInt(int columnIndex, int value) {
            if (columnIndex == mDeltaColumn) {
                return false;
            }

            return mInnerCursor.updateInt(columnIndex, value);
        }

        public boolean updateLong(int columnIndex, long value) {
            if (columnIndex == mDeltaColumn) {
                return false;
            }

            return mInnerCursor.updateLong(columnIndex, value);
        }

        public boolean updateFloat(int columnIndex, float value) {
            if (columnIndex == mDeltaColumn) {
                return false;
            }

            return mInnerCursor.updateFloat(columnIndex, value);
        }

        public boolean updateDouble(int columnIndex, double value) {
            if (columnIndex == mDeltaColumn) {
                return false;
            }

            return mInnerCursor.updateDouble(columnIndex, value);
        }

        public boolean updateToNull(int columnIndex) {
            if (columnIndex == mDeltaColumn) {
                return false;
            }

            return mInnerCursor.updateToNull(columnIndex);
        }

        public boolean commitUpdates() {
            return mInnerCursor.commitUpdates();
        }

        public boolean commitUpdates(Map<? extends Long,
                ? extends Map<String,Object>> values) {
            return mInnerCursor.commitUpdates(values);
        }

        public void abortUpdates() {
            mInnerCursor.abortUpdates();
        }

        public void deactivate() {
            mInnerCursor.deactivate();
        }

        public boolean requery() {
            return mInnerCursor.requery();
        }

        public void close() {
            mInnerCursor.close();
        }

        public boolean isClosed() {
            return mInnerCursor.isClosed();
        }

        public void registerContentObserver(ContentObserver observer) {
            mInnerCursor.registerContentObserver(observer);
        }

        public void unregisterContentObserver(ContentObserver observer) {
            mInnerCursor.unregisterContentObserver(observer);
        }

        public void registerDataSetObserver(DataSetObserver observer) {
            mInnerCursor.registerDataSetObserver(observer);
        }

        public void unregisterDataSetObserver(DataSetObserver observer) {
            mInnerCursor.unregisterDataSetObserver(observer);
        }

        public void setNotificationUri(ContentResolver cr, Uri uri) {
            mInnerCursor.setNotificationUri(cr, uri);
        }

        public boolean getWantsAllOnMoveCalls() {
            return mInnerCursor.getWantsAllOnMoveCalls();
        }

        public Bundle getExtras() {
            return mInnerCursor.getExtras();
        }

        public Bundle respond(Bundle extras) {
            return mInnerCursor.respond(extras);
        }

        public String[] getColumnNames() {
            return mColumnNames;
        }

        private void checkPosition() {
            int pos = mInnerCursor.getPosition();
            int count = mInnerCursor.getCount();

            if (-1 == pos || count == pos) {
                throw new CursorIndexOutOfBoundsException(pos, count);
            }
        }

        public byte[] getBlob(int column) {
            checkPosition();

            if (column == mDeltaColumn) {
                return null;
            }

            return mInnerCursor.getBlob(column);
        }

        public String getString(int column) {
            checkPosition();

            if (column == mDeltaColumn) {
                long value = getDeltaValue();
                return Long.toString(value);
            }

            return mInnerCursor.getString(column);
        }

        public void copyStringToBuffer(int columnIndex, CharArrayBuffer buffer) {
            checkPosition();

            if (columnIndex == mDeltaColumn) {
                long value = getDeltaValue();
                String strValue = Long.toString(value);
                int len = strValue.length();
                char[] data = buffer.data;
                if (data == null || data.length < len) {
                    buffer.data = strValue.toCharArray();
                } else {
                    strValue.getChars(0, len, data, 0);
                }
                buffer.sizeCopied = strValue.length();
            } else {
                mInnerCursor.copyStringToBuffer(columnIndex, buffer);
            }
        }

        public short getShort(int column) {
            checkPosition();

            if (column == mDeltaColumn) {
                return (short)getDeltaValue();
            }

            return mInnerCursor.getShort(column);
        }

        public int getInt(int column) {
            checkPosition();

            if (column == mDeltaColumn) {
                return (int)getDeltaValue();
            }

            return mInnerCursor.getInt(column);
        }

        public long getLong(int column) {
        //if (DBG) log("DeltaCursor.getLong: column=" + column + ", mDeltaColumn=" + mDeltaColumn);
            checkPosition();

            if (column == mDeltaColumn) {
                return getDeltaValue();
            }

            return mInnerCursor.getLong(column);
        }

        public float getFloat(int column) {
            checkPosition();

            if (column == mDeltaColumn) {
                return getDeltaValue();
            }

            return mInnerCursor.getFloat(column);
        }

        public double getDouble(int column) {
            checkPosition();

            if (column == mDeltaColumn) {
                return getDeltaValue();
            }

            return mInnerCursor.getDouble(column);
        }

        public boolean isNull(int column) {
            checkPosition();

            if (column == mDeltaColumn) {
                return false;
            }

            return mInnerCursor.isNull(column);
        }

        private long getDeltaValue() {
            int pos = mInnerCursor.getPosition();
            //Log.i(LOG_TAG, "getDeltaValue: mPos=" + mPos);

            long t2, t1;

            if (pos == getCount()-1) {
                t1 = mInnerCursor.getLong(mDateColumn);
                t2 = System.currentTimeMillis();
            } else {
                mInnerCursor.moveToPosition(pos + 1);
                t2 = mInnerCursor.getLong(mDateColumn);
                mInnerCursor.moveToPosition(pos);
                t1 = mInnerCursor.getLong(mDateColumn);
            }

            return t2 - t1;
        }
    }

    private class MessageAdapter extends CursorAdapter implements AbsListView.OnScrollListener {
        private int mScrollState;
        private boolean mNeedRequeryCursor;

        private int mContactColumn;
        private int mBodyColumn;
        private int mDateColumn;
        private int mTypeColumn;
        private int mErrCodeColumn;
        private int mDeltaColumn;
        private ChatBackgroundMaker mBgMaker;

        private LayoutInflater mInflater;

        public MessageAdapter(Activity context, Cursor c) {
            super(context, c, false);
            mInflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
            mBgMaker = new ChatBackgroundMaker(context);
            if (c != null) {
                resolveColumnIndex(c);
            }
        }

        private void resolveColumnIndex(Cursor c) {
            mContactColumn = c.getColumnIndexOrThrow(Im.BaseMessageColumns.CONTACT);
            mBodyColumn = c.getColumnIndexOrThrow(Im.BaseMessageColumns.BODY);
            mDateColumn = c.getColumnIndexOrThrow(Im.BaseMessageColumns.DATE);
            mTypeColumn = c.getColumnIndexOrThrow(Im.BaseMessageColumns.TYPE);
            mErrCodeColumn = c.getColumnIndexOrThrow(Im.BaseMessageColumns.ERROR_CODE);
            mDeltaColumn = c.getColumnIndexOrThrow(DeltaCursor.DELTA_COLUMN_NAME);
        }

        @Override
        public void changeCursor(Cursor cursor) {
            super.changeCursor(cursor);
            if (cursor != null) {
                resolveColumnIndex(cursor);
            }
        }

        @Override
        public View newView(Context context, Cursor cursor, ViewGroup parent) {
            return mInflater.inflate(R.layout.new_message_item, parent, false);
        }

        @Override
        public void bindView(View view, Context context, Cursor cursor) {
            MessageView chatMsgView = (MessageView) view;

            int type = cursor.getInt(mTypeColumn);
            String contact = isGroupChat() ? cursor.getString(mContactColumn) : mNickName;
            String body = cursor.getString(mBodyColumn);
            long delta = cursor.getLong(mDeltaColumn);
            boolean showTimeStamp = (delta > SHOW_TIME_STAMP_INTERVAL);
            Date date = showTimeStamp ? new Date(cursor.getLong(mDateColumn)) : null;

            switch (type) {
                case Im.MessageType.INCOMING:
                    chatMsgView.bindIncomingMessage(contact, body, date, mMarkup, isScrolling());
                    break;

                case Im.MessageType.OUTGOING:
                case Im.MessageType.POSTPONED:
                    int errCode = cursor.getInt(mErrCodeColumn);
                    if (errCode != 0) {
                        chatMsgView.bindErrorMessage(errCode);
                    } else {
                        chatMsgView.bindOutgoingMessage(body, date, mMarkup, isScrolling());
                    }
                    break;

                default:
                    chatMsgView.bindPresenceMessage(contact, type, isGroupChat(), isScrolling());
            }
            if (!isScrolling()) {
                mBgMaker.setBackground(chatMsgView, contact, type);
            }

            // if showTimeStamp is false for the latest message, then set a timer to query the
            // cursor again in a minute, so we can update the last message timestamp if no new
            // message is received
            if (cursor.getPosition() == cursor.getCount()-1) {
                if (Log.isLoggable(ImApp.LOG_TAG, Log.DEBUG)){
                    log("delta = " + delta + ", showTs=" + showTimeStamp);
                }
                if (!showTimeStamp) {
                    scheduleRequery(SHOW_TIME_STAMP_INTERVAL);
                } else {
                    cancelRequery();
                }
            }
        }

        public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount,
                int totalItemCount) {
            // do nothing
        }

        public void onScrollStateChanged(AbsListView view, int scrollState) {
            int oldState = mScrollState;
            mScrollState = scrollState;

            if (mChatSession != null) {
                try {
                    mChatSession.markAsRead();
                } catch (RemoteException e) {
                    mHandler.showServiceErrorAlert();
                }
            }

            if (oldState == OnScrollListener.SCROLL_STATE_FLING) {
                if (mNeedRequeryCursor) {
                    requeryCursor();
                } else {
                    notifyDataSetChanged();
                }
            }
        }

        boolean isScrolling() {
            return mScrollState == OnScrollListener.SCROLL_STATE_FLING;
        }

        void setNeedRequeryCursor(boolean requeryCursor) {
            mNeedRequeryCursor = requeryCursor;
        }
    }
}