Project: BombusLime
/*
 * Copyright (c) 2005-2011, Eugene Stahov ([email protected]),  
 * 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, nullthis); 
 
  
    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
        } 
   
  defaultreturn 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() == 0return
   
  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, nullfalse); 
  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==nullreturn
         
  //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 == nullreturn
   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, nullthis); 
   
 
  
  
 @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 == nullreturn
    
   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==nullreturn
 
        mChat.saveSuspendedText(mMessageBox.getText().toString()); 
         
        sendChatState(ChatStates.PAUSED); 
 
        markAllRead(); 
 
        getLoaderManager().restartLoader(CHAT_LOADER_ID, nullthis);         
        jid = null
    } 
     
    private static class ChatCursorLoader extends SimpleCursorLoader { 
 
        public ChatCursorLoader(Context context) { 
            super(context); 
        } 
         
        @Override 
        public Cursor loadInBackground() { 
            if (mChat == nullreturn 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); 
    } 
}