org.bombusim.lime.fragments.ChatFragment.java Source code

Java tutorial

Introduction

Here is the source code for org.bombusim.lime.fragments.ChatFragment.java

Source

/*
 * Copyright (c) 2005-2011, Eugene Stahov (evgs@bombus-im.org), 
 * http://bombus-im.org
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this software; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */

package org.bombusim.lime.fragments;

import java.security.InvalidParameterException;

import org.bombusim.lime.Lime;
import org.bombusim.lime.R;
import org.bombusim.lime.activity.ActiveChats;
import org.bombusim.lime.activity.ContactResourceSwitcher;
import org.bombusim.lime.data.Chat;
import org.bombusim.lime.data.ChatHistoryDbAdapter;
import org.bombusim.lime.data.Contact;
import org.bombusim.lime.data.Message;
import org.bombusim.lime.data.Roster;
import org.bombusim.lime.data.SimpleCursorLoader;
import org.bombusim.lime.service.XmppService;
import org.bombusim.lime.service.XmppServiceBinding;
import org.bombusim.lime.widgets.ChatEditText;
import org.bombusim.lime.widgets.ContactBar;
import org.bombusim.xmpp.handlers.ChatStates;
import org.bombusim.xmpp.handlers.MessageDispatcher;
import org.bombusim.xmpp.stanza.XmppPresence;
import org.bombusim.xmpp.stanza.XmppMessage;

import com.actionbarsherlock.app.SherlockFragment;

import android.app.Activity;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.res.Configuration;
import android.content.res.TypedArray;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.graphics.Color;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.support.v4.app.LoaderManager.LoaderCallbacks;
import android.support.v4.content.Loader;
import android.support.v4.widget.CursorAdapter;
import android.text.ClipboardManager;
import android.text.Editable;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.SpannableStringBuilder;
import android.text.TextWatcher;
import android.text.format.Time;
import android.text.method.LinkMovementMethod;
import android.text.style.ForegroundColorSpan;
import android.text.util.Linkify;
import android.view.ContextMenu;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.View.OnCreateContextMenuListener;
import android.view.ViewGroup;
import android.view.ContextMenu.ContextMenuInfo;
import android.view.View.OnClickListener;
import android.view.View.OnKeyListener;
import android.view.ViewGroup.LayoutParams;
import android.view.Window;
import android.view.inputmethod.EditorInfo;
import android.widget.AdapterView.AdapterContextMenuInfo;
import android.widget.BaseAdapter;
import android.widget.EditText;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.ListView;
import android.widget.TextView;
import android.widget.TextView.OnEditorActionListener;
import android.widget.Toast;

public class ChatFragment extends SherlockFragment implements LoaderCallbacks<Cursor> {

    private static final int CHAT_LOADER_ID = 0;

    private String jid;
    private String rJid;

    private ChatEditText mMessageBox;
    private ImageButton mSendButton;
    private ImageButton mSmileButton;

    private ListView chatListView;

    private ContactBar contactBar;

    private View mChatActive;
    private View mChatInactive;

    private XmppServiceBinding serviceBinding;

    Contact visavis;

    private static Chat mChat;

    String sentChatState;

    private CursorAdapter mCursorAdapter;

    protected String visavisNick;
    protected String myNick;

    public interface ChatFragmentListener {
        public void closeChatFragment();

        public boolean isTabMode();
    }

    @Override
    public void onAttach(Activity activity) {
        super.onAttach(activity);

        if (!(activity instanceof ChatFragmentListener))
            throw new ClassCastException(activity.toString() + " must implement ChatFragmentListener");

        serviceBinding = new XmppServiceBinding(activity);
    }

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        //TODO: remove when ActionBar will be implemented
        if (!getChatFragmentListener().isTabMode())
            setHasOptionsMenu(true);
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        View v = inflater.inflate(R.layout.chat, container, false);

        contactBar = (ContactBar) v.findViewById(R.id.contact_head);
        mMessageBox = (ChatEditText) v.findViewById(R.id.messageBox);
        mSendButton = (ImageButton) v.findViewById(R.id.sendButton);
        mSmileButton = (ImageButton) v.findViewById(R.id.smileButton);
        chatListView = (ListView) v.findViewById(R.id.chatListView);

        mChatActive = v.findViewById(R.id.chatActive);
        mChatInactive = v.findViewById(R.id.chatInactive);

        if (!getChatFragmentListener().isTabMode()) {
            contactBar.setVisibility(View.GONE);

            View abCustomView = getSherlockActivity().getSupportActionBar().getCustomView();

            contactBar = (ContactBar) abCustomView.findViewById(R.id.contactHeadActionbar);
            contactBar.removeBackground();
        }

        registerForContextMenu(chatListView);
        enableTrackballTraversing();

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

        mSmileButton.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                mMessageBox.showAddSmileDialog();
            }
        });

        //TODO: optional
        mMessageBox.setOnKeyListener(new OnKeyListener() {
            @Override
            public boolean onKey(View v, int keyCode, KeyEvent event) {

                if (event.getAction() != KeyEvent.ACTION_DOWN)
                    return false; //filtering only KEY_DOWN

                if (keyCode != KeyEvent.KEYCODE_ENTER)
                    return false;
                //if (event.isShiftPressed()) return false; //typing multiline messages with SHIFT+ENTER
                sendMessage();
                return true; //Key was processed
            }
        });

        mMessageBox.addTextChangedListener(new TextWatcher() {

            @Override
            public void onTextChanged(CharSequence s, int start, int before, int count) {
            }

            @Override
            public void beforeTextChanged(CharSequence s, int start, int count, int after) {
            }

            @Override
            public void afterTextChanged(Editable s) {
                if (s.length() > 0)
                    sendChatState(ChatStates.COMPOSING);
            }
        });

        //TODO: optional behavior
        //messageBox.setImeActionLabel("Send", EditorInfo.IME_ACTION_SEND); //Keeps IME opened
        mMessageBox.setImeActionLabel(getString(R.string.sendMessage), EditorInfo.IME_ACTION_DONE); //Closes IME

        mMessageBox.setOnEditorActionListener(new OnEditorActionListener() {

            @Override
            public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
                switch (actionId) {
                case EditorInfo.IME_ACTION_SEND:
                    sendMessage();
                    return true;
                case EditorInfo.IME_ACTION_DONE:
                    sendMessage();
                    return false; //let IME to be closed
                }
                return false;
            }
        });

        contactBar.getContactIconView().setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                new ContactResourceSwitcher().showResources(getActivity(), visavis);
            }
        });

        return v;
    }

    @Override
    public void onActivityCreated(Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        //TODO: set empty view until chat will be attached

        mCursorAdapter = new ChatListAdapter(getActivity(), null);
        chatListView.setAdapter(mCursorAdapter);

        if (jid == null)
            mChat = null;
        getLoaderManager().initLoader(CHAT_LOADER_ID, null, this);
    }

    public void attachToChat(String jid, String rJid) {

        this.jid = jid;
        this.rJid = rJid;

        //TODO: remove workaround after splash screen will be created
        //disable message box until actual chat selected
        mMessageBox.setEnabled(jid != null);

        if (jid == null || rJid == null) {
            showChatActive(false);
            return;
        }
        //throw new InvalidParameterException("No parameters specified for ChatActivity");

        //TODO: move into ChatFactory
        visavis = Lime.getInstance().getRoster().findContact(jid, rJid);

        if (visavis == null) {
            showChatActive(false);
            return;
        }
        showChatActive(true);

        mChat = Lime.getInstance().getChatFactory().getChat(jid, rJid);

        updateContactBar();

        visavisNick = visavis.getScreenName();
        //TODO: get my nick
        myNick = "Me"; //serviceBinding.getXmppStream(visavis.getRosterJid()).jid; 

        refreshVisualContent();

        String s = mChat.getSuspendedText();
        if (s != null) {
            mMessageBox.setText(s);
        }
    }

    private void showChatActive(boolean active) {
        if (active) {
            mChatInactive.setVisibility(View.GONE);
            mChatActive.setVisibility(View.VISIBLE);
        } else {
            mChatActive.setVisibility(View.GONE);
            mChatInactive.setVisibility(View.VISIBLE);
        }
    }

    private void updateContactBar() {

        contactBar.bindContact(visavis, mChat.isComposing());

    }

    private ChatFragmentListener getChatFragmentListener() {
        return (ChatFragmentListener) getActivity();
    }

    @Override
    public void onCreateOptionsMenu(com.actionbarsherlock.view.Menu menu,
            com.actionbarsherlock.view.MenuInflater inflater) {
        inflater.inflate(R.menu.chat_menu, menu);
        super.onCreateOptionsMenu(menu, inflater);
    }

    @Override
    public boolean onOptionsItemSelected(com.actionbarsherlock.view.MenuItem item) {
        switch (item.getItemId()) {
        case R.id.closeChat:

            Lime.getInstance().getChatFactory().resetActiveState(mChat);
            getChatFragmentListener().closeChatFragment(); //finish activity if single mode chat
            break;

        case R.id.addSmile:
            mMessageBox.showAddSmileDialog();
            break;

        case R.id.addMe:
            mMessageBox.addMe();
            break;

        case R.id.cmdChat: {
            ActiveChats chats = new ActiveChats();
            chats.showActiveChats(getActivity(), null);

            break;
        }

        default:
            return true; // on submenu
        }

        return false;
    }

    @Override
    public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {

        super.onCreateContextMenu(menu, v, menuInfo);

        menu.setHeaderTitle(R.string.messageMenuTitle);

        MenuInflater inflater = getActivity().getMenuInflater();
        inflater.inflate(R.menu.message_menu, menu);
    }

    @Override
    public boolean onContextItemSelected(MenuItem item) {
        AdapterContextMenuInfo info = (AdapterContextMenuInfo) item.getMenuInfo();
        switch (item.getItemId()) {
        case R.id.cmdCopy:
            try {
                String s = ((MessageView) (info.targetView)).toString();

                // Gets a handle to the clipboard service.
                ClipboardManager clipboard = (ClipboardManager) getActivity()
                        .getSystemService(Context.CLIPBOARD_SERVICE);

                // Set the clipboard's primary clip.
                clipboard.setText(s);

            } catch (Exception e) {
            }
            return true;

        case R.id.cmdDelete:

            chatListView.setVisibility(View.GONE);
            mChat.removeFromHistory(info.id);
            refreshVisualContent();

            return true;

        default:
            return super.onContextItemSelected(item);
        }
    }

    private void enableTrackballTraversing() {
        //TODO: http://stackoverflow.com/questions/2679948/focusable-edittext-inside-listview
        chatListView.setItemsCanFocus(true);
    }

    private class ChatListAdapter extends CursorAdapter {

        public ChatListAdapter(Context context, Cursor c) {
            super(context, c);
        }

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

            // TODO Auto-generated method stub
            Message m = ChatHistoryDbAdapter.getMessageFromCursor(cursor);

            String sender = (m.type == Message.TYPE_MESSAGE_OUT) ? myNick : visavisNick;
            sv.setText(m.timestamp, sender, m.messageBody, m.type);
            sv.setUnread(m.unread);

        }

        @Override
        public View newView(Context context, Cursor cursor, ViewGroup parent) {
            View v = new MessageView(context);
            bindView(v, context, cursor);
            return v;
        }

    }

    // Time formatter
    private Time tf = new Time(Time.getCurrentTimezone());
    private final static long MS_PER_DAY = 1000 * 60 * 60 * 24;

    private class MessageView extends TextView {
        public MessageView(Context context) {
            super(context);

            //TODO: available in API 11
            //setTextIsSelectable(true);

            //setLayoutParams(new ViewGroup.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT));
        }

        public void setUnread(boolean unread) {
            setBackgroundColor(Message.getBkColor(unread));
        }

        public void setText(long time, String sender, String message, int messageType) {

            //TODO: smart time formatting
            // 1 minute ago,
            // hh:mm (after 1 hour)
            // etc...

            long delay = System.currentTimeMillis() - time;

            String fmt = "%H:%M ";
            if (delay > MS_PER_DAY) {
                fmt = "%d.%m.%Y %H:%M ";
            }

            tf.set(time);
            String tm = tf.format(fmt);

            SpannableStringBuilder ss = new SpannableStringBuilder(tm);

            int addrEnd = 0;

            if (message.startsWith("/me ")) {
                message = "*" + message.replaceAll("(/me)(?:\\s|$)", sender + ' ');
                ;
            } else {
                ss.append('<').append(sender).append("> ");
            }

            addrEnd = ss.length() - 1;

            int color = Message.getColor(messageType);
            ss.setSpan(new ForegroundColorSpan(color), 0, addrEnd, Spannable.SPAN_INCLUSIVE_EXCLUSIVE);

            SpannableString msg = new SpannableString(message);

            Linkify.addLinks(msg, Linkify.EMAIL_ADDRESSES | Linkify.WEB_URLS);
            Lime.getInstance().getSmilify().addSmiles(msg);

            ss.append(msg);

            setText(ss);
            setMovementMethod(LinkMovementMethod.getInstance());

        }

        @Override
        public String toString() {
            return getText().toString();
        }
    }

    protected void sendMessage() {
        String text = mMessageBox.getText().toString();

        //avoid sending of empty messages
        if (text.length() == 0)
            return;

        String to = visavis.getJid();

        Message out = new Message(Message.TYPE_MESSAGE_OUT, to, text);
        mChat.addMessage(out);

        //TODO: resource magic
        XmppMessage msg = new XmppMessage(to, text, null, false);
        msg.setAttribute("id", String.valueOf(out.getId()));

        //TODO: optional delivery confirmation request
        msg.addChildNs("request", MessageDispatcher.URN_XMPP_RECEIPTS);

        //TODO: optional chat state notifications
        msg.addChildNs(ChatStates.ACTIVE, ChatStates.XMLNS_CHATSTATES);
        sentChatState = ChatStates.ACTIVE;

        //TODO: message queue
        if (serviceBinding.isLoggedIn(visavis.getRosterJid())) {

            serviceBinding.postStanza(visavis.getRosterJid(), msg);

            //clear box after success sending
            mMessageBox.setText("");

            if (!visavis.isAvailable()) {
                Toast.makeText(getActivity(), R.string.chatSentOffline, Toast.LENGTH_LONG).show();
            }

        } else {
            Toast.makeText(getActivity(), R.string.shouldBeLoggedIn, Toast.LENGTH_LONG).show();
            //not sent - removing from history
            mChat.removeFromHistory(out.getId());
        }

        refreshVisualContent();
    }

    protected void sendChatState(String state) {

        if (jid == null)
            return;

        //TODO: optional chat state notifications

        if (!mChat.acceptComposingEvents())
            return;

        if (state.equals(sentChatState))
            return; //no duplicates

        //state machine check: composing->paused
        if (state.equals(ChatStates.PAUSED))
            if (!ChatStates.COMPOSING.equals(sentChatState))
                return;

        if (!visavis.isAvailable())
            return;

        String to = visavis.getJid();

        //TODO: resource magic
        XmppMessage msg = new XmppMessage(to);
        //msg.setAttribute("id", "chatState");

        msg.addChildNs(state, ChatStates.XMLNS_CHATSTATES);
        sentChatState = state;

        serviceBinding.postStanza(visavis.getRosterJid(), msg);

    }

    private class ChatBroadcastReceiver extends BroadcastReceiver {

        @Override
        public void onReceive(Context context, Intent intent) {
            refreshVisualContent();
        }

    }

    private class DeliveredReceiver extends BroadcastReceiver {
        @Override
        public void onReceive(Context context, Intent intent) {
            Toast.makeText(ChatFragment.this.getActivity(), R.string.messageDelivered, Toast.LENGTH_SHORT).show();
        }
    }

    private class PresenceReceiver extends BroadcastReceiver {
        @Override
        public void onReceive(Context context, Intent intent) {
            if (jid == null)
                return;
            String jid = intent.getStringExtra("param");
            if (visavis.getJid().equals(jid)) {
                updateContactBar();
            }
        }
    }

    private PresenceReceiver bcPresence;
    private ChatBroadcastReceiver bcUpdateChat;
    private DeliveredReceiver bcDelivered;

    @Override
    public void onResume() {
        super.onResume();

        //TODO: refresh message list, focus to last unread
        serviceBinding.doBindService();

        bcUpdateChat = new ChatBroadcastReceiver();
        //TODO: presence receiver
        getActivity().registerReceiver(bcUpdateChat, new IntentFilter(Chat.UPDATE_CHAT));

        bcDelivered = new DeliveredReceiver();
        getActivity().registerReceiver(bcDelivered, new IntentFilter(Chat.DELIVERED));

        bcPresence = new PresenceReceiver();
        getActivity().registerReceiver(bcPresence, new IntentFilter(Roster.UPDATE_CONTACT));

        mMessageBox.setDialogHostActivity(getActivity());

    }

    public void refreshVisualContent() {

        getLoaderManager().restartLoader(CHAT_LOADER_ID, null, this);

    }

    @Override
    public void onPause() {
        super.onPause();

        //avoid memory leak
        mMessageBox.setDialogHostActivity(null);

        serviceBinding.doUnbindService();
        getActivity().unregisterReceiver(bcUpdateChat);
        getActivity().unregisterReceiver(bcDelivered);
        getActivity().unregisterReceiver(bcPresence);

        suspendChat();
    }

    private void markAllRead() {
        synchronized (visavis) {
            int unreadCount = visavis.getUnread();

            visavis.setUnread(0);

            Cursor cursor = mCursorAdapter.getCursor();
            if (cursor == null)
                return;

            if (cursor.moveToLast())
                do {
                    Message m = ChatHistoryDbAdapter.getMessageFromCursor(cursor);
                    if (m.unread) {
                        mChat.markRead(m.getId());
                        Lime.getInstance().notificationMgr().cancelChatNotification(m.getId());

                        unreadCount--;
                    }
                } while ((unreadCount != 0) && cursor.moveToPrevious());
        }
    }

    public void suspendChat() {
        if (jid == null)
            return;

        mChat.saveSuspendedText(mMessageBox.getText().toString());

        sendChatState(ChatStates.PAUSED);

        markAllRead();

        getLoaderManager().restartLoader(CHAT_LOADER_ID, null, this);
        jid = null;
    }

    private static class ChatCursorLoader extends SimpleCursorLoader {

        public ChatCursorLoader(Context context) {
            super(context);
        }

        @Override
        public Cursor loadInBackground() {
            if (mChat == null)
                return null;

            return mChat.getCursor();
        }

    }

    @Override
    public Loader<Cursor> onCreateLoader(int arg0, Bundle arg1) {
        return new ChatCursorLoader(getActivity());
    }

    @Override
    public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
        // Swap the new cursor in.  (The framework will take care of closing the
        // old cursor once we return.)

        mCursorAdapter.swapCursor(cursor);

        chatListView.setSelection(mCursorAdapter.getCount() - 1);

        //TODO: hide progress
        chatListView.setVisibility(View.VISIBLE);
    }

    @Override
    public void onLoaderReset(Loader<Cursor> loader) {
        mCursorAdapter.swapCursor(null);

        //TODO: show progress
        chatListView.setVisibility(View.GONE);
    }
}