package org.yaxim.androidclient.chat; import java.text.SimpleDateFormat; import java.util.Date; import org.yaxim.androidclient.MainWindow; import org.yaxim.androidclient.R; import org.yaxim.androidclient.YaximApplication; import org.yaxim.androidclient.data.ChatProvider; import org.yaxim.androidclient.data.ChatProvider.ChatConstants; import org.yaxim.androidclient.data.RosterProvider; import org.yaxim.androidclient.service.IXMPPChatService; import org.yaxim.androidclient.service.XMPPService; import org.yaxim.androidclient.util.StatusMode; import com.actionbarsherlock.app.ActionBar; import com.actionbarsherlock.app.SherlockListActivity; import com.actionbarsherlock.view.Window; import android.content.ComponentName; import android.content.ContentValues; import android.content.Context; import android.content.Intent; import android.content.ServiceConnection; import android.database.ContentObserver; import android.database.Cursor; import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.TransitionDrawable; import android.net.Uri; import android.os.Bundle; import android.os.IBinder; import android.os.Handler; import android.text.ClipboardManager; import android.text.Editable; import android.text.TextWatcher; import android.util.Log; import android.util.TypedValue; import android.view.ContextMenu; import android.view.KeyEvent; import android.view.LayoutInflater; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.view.View.OnKeyListener; import android.widget.AdapterView.AdapterContextMenuInfo; import android.widget.Button; import android.widget.EditText; import android.widget.ImageView; import android.widget.ListAdapter; import android.widget.SimpleCursorAdapter; import android.widget.TextView; import android.widget.Toast; @SuppressWarnings("deprecation") /* recent ClipboardManager only available since API 11 */ public class ChatWindow extends SherlockListActivity implements OnKeyListener, TextWatcher { public static final String INTENT_EXTRA_USERNAME = ChatWindow.class.getName() + ".username"; public static final String INTENT_EXTRA_MESSAGE = ChatWindow.class.getName() + ".message"; private static final String TAG = "yaxim.ChatWindow"; private static final String[] PROJECTION_FROM = new String[] { ChatProvider.ChatConstants._ID, ChatProvider.ChatConstants.DATE, ChatProvider.ChatConstants.DIRECTION, ChatProvider.ChatConstants.JID, ChatProvider.ChatConstants.MESSAGE, ChatProvider.ChatConstants.DELIVERY_STATUS }; private static final int[] PROJECTION_TO = new int[] { R.id.chat_date, R.id.chat_from, R.id.chat_message }; private static final int DELAY_NEWMSG = 2000; private ContentObserver mContactObserver = new ContactObserver(); private ImageView mStatusMode; private TextView mTitle; private TextView mSubTitle; private Button mSendButton = null; private EditText mChatInput = null; private String mWithJabberID = null; private String mUserScreenName = null; private Intent mServiceIntent; private ServiceConnection mServiceConnection; private XMPPChatServiceAdapter mServiceAdapter; private int mChatFontSize; @Override public void onCreate(Bundle savedInstanceState) { setTheme(YaximApplication.getConfig(this).getTheme()); super.onCreate(savedInstanceState); mChatFontSize = Integer.valueOf(YaximApplication.getConfig(this).chatFontSize); requestWindowFeature(Window.FEATURE_ACTION_BAR); setContentView(R.layout.mainchat); getContentResolver().registerContentObserver(RosterProvider.CONTENT_URI, true, mContactObserver); ActionBar actionBar = getSupportActionBar(); actionBar.setHomeButtonEnabled(true); actionBar.setDisplayHomeAsUpEnabled(true); registerForContextMenu(getListView()); setContactFromUri(); registerXMPPService(); setSendButton(); setUserInput(); String titleUserid; if (mUserScreenName != null) { titleUserid = mUserScreenName; } else { titleUserid = mWithJabberID; } setCustomTitle(titleUserid); setChatWindowAdapter(); } private void setCustomTitle(String title) { LayoutInflater inflater = (LayoutInflater) getSystemService(LAYOUT_INFLATER_SERVICE); View layout = inflater.inflate(R.layout.chat_action_title, null); mStatusMode = (ImageView)layout.findViewById(R.id.action_bar_status); mTitle = (TextView)layout.findViewById(R.id.action_bar_title); mSubTitle = (TextView)layout.findViewById(R.id.action_bar_subtitle); mTitle.setText(title); setTitle(null); getSupportActionBar().setCustomView(layout); getSupportActionBar().setDisplayShowCustomEnabled(true); } private void setChatWindowAdapter() { String selection = ChatConstants.JID + "='" + mWithJabberID + "'"; Cursor cursor = managedQuery(ChatProvider.CONTENT_URI, PROJECTION_FROM, selection, null, null); ListAdapter adapter = new ChatWindowAdapter(cursor, PROJECTION_FROM, PROJECTION_TO, mWithJabberID, mUserScreenName); setListAdapter(adapter); } @Override protected void onResume() { super.onResume(); updateContactStatus(); } @Override public void onWindowFocusChanged(boolean hasFocus) { super.onWindowFocusChanged(hasFocus); if (hasFocus) bindXMPPService(); else unbindXMPPService(); } @Override public void onDestroy() { super.onDestroy(); if (hasWindowFocus()) unbindXMPPService(); getContentResolver().unregisterContentObserver(mContactObserver); } private void registerXMPPService() { Log.i(TAG, "called startXMPPService()"); mServiceIntent = new Intent(this, XMPPService.class); Uri chatURI = Uri.parse(mWithJabberID); mServiceIntent.setData(chatURI); mServiceIntent.setAction("org.yaxim.androidclient.XMPPSERVICE"); mServiceConnection = new ServiceConnection() { public void onServiceConnected(ComponentName name, IBinder service) { Log.i(TAG, "called onServiceConnected()"); mServiceAdapter = new XMPPChatServiceAdapter( IXMPPChatService.Stub.asInterface(service), mWithJabberID); mServiceAdapter.clearNotifications(mWithJabberID); updateContactStatus(); } public void onServiceDisconnected(ComponentName name) { Log.i(TAG, "called onServiceDisconnected()"); } }; } private void unbindXMPPService() { try { unbindService(mServiceConnection); } catch (IllegalArgumentException e) { Log.e(TAG, "Service wasn't bound!"); } } private void bindXMPPService() { bindService(mServiceIntent, mServiceConnection, BIND_AUTO_CREATE); } private void setSendButton() { mSendButton = (Button) findViewById(R.id.Chat_SendButton); View.OnClickListener onSend = getOnSetListener(); mSendButton.setOnClickListener(onSend); mSendButton.setEnabled(false); } private void setUserInput() { Intent i = getIntent(); mChatInput = (EditText) findViewById(R.id.Chat_UserInput); mChatInput.addTextChangedListener(this); if (i.hasExtra(INTENT_EXTRA_MESSAGE)) { mChatInput.setText(i.getExtras().getString(INTENT_EXTRA_MESSAGE)); } } private void setContactFromUri() { Intent i = getIntent(); mWithJabberID = i.getDataString().toLowerCase(); if (i.hasExtra(INTENT_EXTRA_USERNAME)) { mUserScreenName = i.getExtras().getString(INTENT_EXTRA_USERNAME); } else { mUserScreenName = mWithJabberID; } } @Override public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) { super.onCreateContextMenu(menu, v, menuInfo); View target = ((AdapterContextMenuInfo)menuInfo).targetView; TextView from = (TextView)target.findViewById(R.id.chat_from); getMenuInflater().inflate(R.menu.chat_contextmenu, menu); if (!from.getText().equals(getString(R.string.chat_from_me))) { menu.findItem(R.id.chat_contextmenu_resend).setEnabled(false); } } private CharSequence getMessageFromContextMenu(MenuItem item) { View target = ((AdapterContextMenuInfo)item.getMenuInfo()).targetView; TextView message = (TextView)target.findViewById(R.id.chat_message); return message.getText(); } public boolean onContextItemSelected(MenuItem item) { switch (item.getItemId()) { case R.id.chat_contextmenu_copy_text: ClipboardManager cm = (ClipboardManager)getSystemService(Context.CLIPBOARD_SERVICE); cm.setText(getMessageFromContextMenu(item)); return true; case R.id.chat_contextmenu_resend: sendMessage(getMessageFromContextMenu(item).toString()); Log.d(TAG, "resend!"); return true; default: return super.onContextItemSelected((android.view.MenuItem) item); } } private View.OnClickListener getOnSetListener() { return new View.OnClickListener() { public void onClick(View v) { sendMessageIfNotNull(); } }; } private void sendMessageIfNotNull() { if (mChatInput.getText().length() >= 1) { sendMessage(mChatInput.getText().toString()); } } private void sendMessage(String message) { mChatInput.setText(null); mSendButton.setEnabled(false); mServiceAdapter.sendMessage(mWithJabberID, message); if (!mServiceAdapter.isServiceAuthenticated()) showToastNotification(R.string.toast_stored_offline); } private void markAsReadDelayed(final int id, final int delay) { new Thread() { @Override public void run() { try { Thread.sleep(delay); } catch (Exception e) {} markAsRead(id); } }.start(); } private void markAsRead(int id) { Uri rowuri = Uri.parse("content://" + ChatProvider.AUTHORITY + "/" + ChatProvider.TABLE_NAME + "/" + id); Log.d(TAG, "markAsRead: " + rowuri); ContentValues values = new ContentValues(); values.put(ChatConstants.DELIVERY_STATUS, ChatConstants.DS_SENT_OR_READ); getContentResolver().update(rowuri, values, null, null); } class ChatWindowAdapter extends SimpleCursorAdapter { String mScreenName, mJID; ChatWindowAdapter(Cursor cursor, String[] from, int[] to, String JID, String screenName) { super(ChatWindow.this, android.R.layout.simple_list_item_1, cursor, from, to); mScreenName = screenName; mJID = JID; } @Override public View getView(int position, View convertView, ViewGroup parent) { View row = convertView; ChatItemWrapper wrapper = null; Cursor cursor = this.getCursor(); cursor.moveToPosition(position); long dateMilliseconds = cursor.getLong(cursor .getColumnIndex(ChatProvider.ChatConstants.DATE)); int _id = cursor.getInt(cursor .getColumnIndex(ChatProvider.ChatConstants._ID)); String date = getDateString(dateMilliseconds); String message = cursor.getString(cursor .getColumnIndex(ChatProvider.ChatConstants.MESSAGE)); boolean from_me = (cursor.getInt(cursor .getColumnIndex(ChatProvider.ChatConstants.DIRECTION)) == ChatConstants.OUTGOING); String jid = cursor.getString(cursor .getColumnIndex(ChatProvider.ChatConstants.JID)); int delivery_status = cursor.getInt(cursor .getColumnIndex(ChatProvider.ChatConstants.DELIVERY_STATUS)); if (row == null) { LayoutInflater inflater = getLayoutInflater(); row = inflater.inflate(R.layout.chatrow, null); wrapper = new ChatItemWrapper(row, ChatWindow.this); row.setTag(wrapper); } else { wrapper = (ChatItemWrapper) row.getTag(); } if (!from_me && delivery_status == ChatConstants.DS_NEW) { markAsReadDelayed(_id, DELAY_NEWMSG); } String from = jid; if (jid.equals(mJID)) from = mScreenName; wrapper.populateFrom(date, from_me, from, message, delivery_status); return row; } } private String getDateString(long milliSeconds) { SimpleDateFormat dateFormater = new SimpleDateFormat("yy-MM-dd HH:mm:ss"); Date date = new Date(milliSeconds); return dateFormater.format(date); } public class ChatItemWrapper { private TextView mDateView = null; private TextView mFromView = null; private TextView mMessageView = null; private ImageView mIconView = null; private final View mRowView; private ChatWindow chatWindow; ChatItemWrapper(View row, ChatWindow chatWindow) { this.mRowView = row; this.chatWindow = chatWindow; } void populateFrom(String date, boolean from_me, String from, String message, int delivery_status) { // Log.i(TAG, "populateFrom(" + from_me + ", " + from + ", " + message + ")"); getDateView().setText(date); TypedValue tv = new TypedValue(); if (from_me) { getTheme().resolveAttribute(R.attr.ChatMsgHeaderMeColor, tv, true); getDateView().setTextColor(tv.data); getFromView().setText(getString(R.string.chat_from_me)); getFromView().setTextColor(tv.data); } else { getTheme().resolveAttribute(R.attr.ChatMsgHeaderYouColor, tv, true); getDateView().setTextColor(tv.data); getFromView().setText(from + ":"); getFromView().setTextColor(tv.data); } switch (delivery_status) { case ChatConstants.DS_NEW: ColorDrawable layers[] = new ColorDrawable[2]; getTheme().resolveAttribute(R.attr.ChatNewMessageColor, tv, true); layers[0] = new ColorDrawable(tv.data); if (from_me) { // message stored for later transmission getTheme().resolveAttribute(R.attr.ChatStoredMessageColor, tv, true); layers[1] = new ColorDrawable(tv.data); } else { layers[1] = new ColorDrawable(0x00000000); } TransitionDrawable backgroundColorAnimation = new TransitionDrawable(layers); int l = mRowView.getPaddingLeft(); int t = mRowView.getPaddingTop(); int r = mRowView.getPaddingRight(); int b = mRowView.getPaddingBottom(); mRowView.setBackgroundDrawable(backgroundColorAnimation); mRowView.setPadding(l, t, r, b); backgroundColorAnimation.setCrossFadeEnabled(true); backgroundColorAnimation.startTransition(DELAY_NEWMSG); getIconView().setImageResource(R.drawable.ic_chat_msg_status_queued); break; case ChatConstants.DS_SENT_OR_READ: getIconView().setImageResource(R.drawable.ic_chat_msg_status_unread); mRowView.setBackgroundColor(0x00000000); // default is transparent break; case ChatConstants.DS_ACKED: getIconView().setImageResource(R.drawable.ic_chat_msg_status_ok); mRowView.setBackgroundColor(0x00000000); // default is transparent break; case ChatConstants.DS_FAILED: getIconView().setImageResource(R.drawable.ic_chat_msg_status_failed); mRowView.setBackgroundColor(0x30ff0000); // default is transparent break; } getMessageView().setText(message); getMessageView().setTextSize(TypedValue.COMPLEX_UNIT_SP, chatWindow.mChatFontSize); getDateView().setTextSize(TypedValue.COMPLEX_UNIT_SP, chatWindow.mChatFontSize*2/3); getFromView().setTextSize(TypedValue.COMPLEX_UNIT_SP, chatWindow.mChatFontSize*2/3); } TextView getDateView() { if (mDateView == null) { mDateView = (TextView) mRowView.findViewById(R.id.chat_date); } return mDateView; } TextView getFromView() { if (mFromView == null) { mFromView = (TextView) mRowView.findViewById(R.id.chat_from); } return mFromView; } TextView getMessageView() { if (mMessageView == null) { mMessageView = (TextView) mRowView .findViewById(R.id.chat_message); } return mMessageView; } ImageView getIconView() { if (mIconView == null) { mIconView = (ImageView) mRowView .findViewById(R.id.iconView); } return mIconView; } } public boolean onKey(View v, int keyCode, KeyEvent event) { if (event.getAction() == KeyEvent.ACTION_DOWN && keyCode == KeyEvent.KEYCODE_ENTER) { sendMessageIfNotNull(); return true; } return false; } public void afterTextChanged(Editable s) { if (mChatInput.getText().length() >= 1) { mChatInput.setOnKeyListener(this); mSendButton.setEnabled(true); } } public void beforeTextChanged(CharSequence s, int start, int count, int after) { // TODO Auto-generated method stub } public void onTextChanged(CharSequence s, int start, int before, int count) { } private void showToastNotification(int message) { Toast toastNotification = Toast.makeText(this, message, Toast.LENGTH_SHORT); toastNotification.show(); } @Override public boolean onOptionsItemSelected(com.actionbarsherlock.view.MenuItem item) { switch (item.getItemId()) { case android.R.id.home: Intent intent = new Intent(this, MainWindow.class); intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); startActivity(intent); return true; default: return super.onOptionsItemSelected(item); } } private static final String[] STATUS_QUERY = new String[] { RosterProvider.RosterConstants.STATUS_MODE, RosterProvider.RosterConstants.STATUS_MESSAGE, }; private void updateContactStatus() { Cursor cursor = getContentResolver().query(RosterProvider.CONTENT_URI, STATUS_QUERY, RosterProvider.RosterConstants.JID + " = ?", new String[] { mWithJabberID }, null); int MODE_IDX = cursor.getColumnIndex(RosterProvider.RosterConstants.STATUS_MODE); int MSG_IDX = cursor.getColumnIndex(RosterProvider.RosterConstants.STATUS_MESSAGE); if (cursor.getCount() == 1) { cursor.moveToFirst(); int status_mode = cursor.getInt(MODE_IDX); String status_message = cursor.getString(MSG_IDX); Log.d(TAG, "contact status changed: " + status_mode + " " + status_message); mSubTitle.setVisibility((status_message != null && status_message.length() != 0)? View.VISIBLE : View.GONE); mSubTitle.setText(status_message); if (mServiceAdapter == null || !mServiceAdapter.isServiceAuthenticated()) status_mode = 0; // override icon if we are offline mStatusMode.setImageResource(StatusMode.values()[status_mode].getDrawableId()); } cursor.close(); } private class ContactObserver extends ContentObserver { public ContactObserver() { super(new Handler()); } public void onChange(boolean selfChange) { Log.d(TAG, "ContactObserver.onChange: " + selfChange); updateContactStatus(); } } }