ConversationFragment.java

   1package eu.siacs.conversations.ui;
   2
   3import android.app.Activity;
   4import android.app.AlertDialog;
   5import android.app.Fragment;
   6import android.app.PendingIntent;
   7import android.content.ActivityNotFoundException;
   8import android.content.Context;
   9import android.content.DialogInterface;
  10import android.content.Intent;
  11import android.content.IntentSender.SendIntentException;
  12import android.os.Bundle;
  13import android.os.Handler;
  14import android.support.v13.view.inputmethod.InputConnectionCompat;
  15import android.support.v13.view.inputmethod.InputContentInfoCompat;
  16import android.text.Editable;
  17import android.text.InputType;
  18import android.text.TextWatcher;
  19import android.text.style.StyleSpan;
  20import android.util.Log;
  21import android.util.Pair;
  22import android.view.ContextMenu;
  23import android.view.ContextMenu.ContextMenuInfo;
  24import android.view.Gravity;
  25import android.view.KeyEvent;
  26import android.view.LayoutInflater;
  27import android.view.MenuItem;
  28import android.view.View;
  29import android.view.View.OnClickListener;
  30import android.view.ViewGroup;
  31import android.view.inputmethod.EditorInfo;
  32import android.view.inputmethod.InputMethodManager;
  33import android.widget.AbsListView;
  34import android.widget.AbsListView.OnScrollListener;
  35import android.widget.AdapterView;
  36import android.widget.AdapterView.AdapterContextMenuInfo;
  37import android.widget.ImageButton;
  38import android.widget.ListView;
  39import android.widget.PopupMenu;
  40import android.widget.RelativeLayout;
  41import android.widget.TextView;
  42import android.widget.TextView.OnEditorActionListener;
  43import android.widget.Toast;
  44
  45import net.java.otr4j.session.SessionStatus;
  46
  47import java.util.ArrayList;
  48import java.util.Arrays;
  49import java.util.Collections;
  50import java.util.List;
  51import java.util.UUID;
  52import java.util.concurrent.atomic.AtomicBoolean;
  53
  54import eu.siacs.conversations.Config;
  55import eu.siacs.conversations.R;
  56import eu.siacs.conversations.entities.Account;
  57import eu.siacs.conversations.entities.Blockable;
  58import eu.siacs.conversations.entities.Contact;
  59import eu.siacs.conversations.entities.Conversation;
  60import eu.siacs.conversations.entities.DownloadableFile;
  61import eu.siacs.conversations.entities.Message;
  62import eu.siacs.conversations.entities.MucOptions;
  63import eu.siacs.conversations.entities.Presence;
  64import eu.siacs.conversations.entities.Transferable;
  65import eu.siacs.conversations.entities.TransferablePlaceholder;
  66import eu.siacs.conversations.http.HttpDownloadConnection;
  67import eu.siacs.conversations.persistance.FileBackend;
  68import eu.siacs.conversations.services.MessageArchiveService;
  69import eu.siacs.conversations.services.XmppConnectionService;
  70import eu.siacs.conversations.ui.XmppActivity.OnPresenceSelected;
  71import eu.siacs.conversations.ui.XmppActivity.OnValueEdited;
  72import eu.siacs.conversations.ui.adapter.MessageAdapter;
  73import eu.siacs.conversations.ui.adapter.MessageAdapter.OnContactPictureClicked;
  74import eu.siacs.conversations.ui.adapter.MessageAdapter.OnContactPictureLongClicked;
  75import eu.siacs.conversations.ui.widget.EditMessage;
  76import eu.siacs.conversations.ui.widget.ListSelectionManager;
  77import eu.siacs.conversations.utils.NickValidityChecker;
  78import eu.siacs.conversations.utils.StylingHelper;
  79import eu.siacs.conversations.utils.UIHelper;
  80import eu.siacs.conversations.xmpp.XmppConnection;
  81import eu.siacs.conversations.xmpp.chatstate.ChatState;
  82import eu.siacs.conversations.xmpp.jid.Jid;
  83
  84public class ConversationFragment extends Fragment implements EditMessage.KeyboardListener {
  85
  86	protected Conversation conversation;
  87	private OnClickListener leaveMuc = new OnClickListener() {
  88
  89		@Override
  90		public void onClick(View v) {
  91			activity.endConversation(conversation);
  92		}
  93	};
  94	private OnClickListener joinMuc = new OnClickListener() {
  95
  96		@Override
  97		public void onClick(View v) {
  98			activity.xmppConnectionService.joinMuc(conversation);
  99		}
 100	};
 101	private OnClickListener enterPassword = new OnClickListener() {
 102
 103		@Override
 104		public void onClick(View v) {
 105			MucOptions muc = conversation.getMucOptions();
 106			String password = muc.getPassword();
 107			if (password == null) {
 108				password = "";
 109			}
 110			activity.quickPasswordEdit(password, new OnValueEdited() {
 111
 112				@Override
 113				public void onValueEdited(String value) {
 114					activity.xmppConnectionService.providePasswordForMuc(
 115							conversation, value);
 116				}
 117			});
 118		}
 119	};
 120	protected ListView messagesView;
 121	final protected List<Message> messageList = new ArrayList<>();
 122	protected MessageAdapter messageListAdapter;
 123	private EditMessage mEditMessage;
 124	private ImageButton mSendButton;
 125	private RelativeLayout snackbar;
 126	private TextView snackbarMessage;
 127	private TextView snackbarAction;
 128	private Toast messageLoaderToast;
 129
 130	private OnScrollListener mOnScrollListener = new OnScrollListener() {
 131
 132		@Override
 133		public void onScrollStateChanged(AbsListView view, int scrollState) {
 134			// TODO Auto-generated method stub
 135
 136		}
 137
 138		@Override
 139		public void onScroll(AbsListView view, int firstVisibleItem,
 140							 int visibleItemCount, int totalItemCount) {
 141			synchronized (ConversationFragment.this.messageList) {
 142				if (firstVisibleItem < 5 && conversation != null && conversation.messagesLoaded.compareAndSet(true,false) && messageList.size() > 0) {
 143					long timestamp;
 144					if (messageList.get(0).getType() == Message.TYPE_STATUS && messageList.size() >= 2) {
 145						timestamp = messageList.get(1).getTimeSent();
 146					} else {
 147						timestamp = messageList.get(0).getTimeSent();
 148					}
 149					activity.xmppConnectionService.loadMoreMessages(conversation, timestamp, new XmppConnectionService.OnMoreMessagesLoaded() {
 150						@Override
 151						public void onMoreMessagesLoaded(final int c, final Conversation conversation) {
 152							if (ConversationFragment.this.conversation != conversation) {
 153								conversation.messagesLoaded.set(true);
 154								return;
 155							}
 156							activity.runOnUiThread(new Runnable() {
 157								@Override
 158								public void run() {
 159									final int oldPosition = messagesView.getFirstVisiblePosition();
 160									Message message = null;
 161									int childPos;
 162									for(childPos = 0; childPos + oldPosition < messageList.size(); ++childPos) {
 163										message =  messageList.get(oldPosition + childPos);
 164										if (message.getType() != Message.TYPE_STATUS) {
 165											break;
 166										}
 167									}
 168									final String uuid = message != null ? message.getUuid() : null;
 169									View v = messagesView.getChildAt(childPos);
 170									final int pxOffset = (v == null) ? 0 : v.getTop();
 171									ConversationFragment.this.conversation.populateWithMessages(ConversationFragment.this.messageList);
 172									try {
 173										updateStatusMessages();
 174									} catch (IllegalStateException e) {
 175										Log.d(Config.LOGTAG,"caught illegal state exception while updating status messages");
 176									}
 177									messageListAdapter.notifyDataSetChanged();
 178									int pos = Math.max(getIndexOf(uuid,messageList),0);
 179									messagesView.setSelectionFromTop(pos, pxOffset);
 180									if (messageLoaderToast != null) {
 181										messageLoaderToast.cancel();
 182									}
 183									conversation.messagesLoaded.set(true);
 184								}
 185							});
 186						}
 187
 188						@Override
 189						public void informUser(final int resId) {
 190
 191							activity.runOnUiThread(new Runnable() {
 192								@Override
 193								public void run() {
 194									if (messageLoaderToast != null) {
 195										messageLoaderToast.cancel();
 196									}
 197									if (ConversationFragment.this.conversation != conversation) {
 198										return;
 199									}
 200									messageLoaderToast = Toast.makeText(activity, resId, Toast.LENGTH_LONG);
 201									messageLoaderToast.show();
 202								}
 203							});
 204
 205						}
 206					});
 207
 208				}
 209			}
 210		}
 211	};
 212
 213	private int getIndexOf(String uuid, List<Message> messages) {
 214		if (uuid == null) {
 215			return messages.size() - 1;
 216		}
 217		for(int i = 0; i < messages.size(); ++i) {
 218			if (uuid.equals(messages.get(i).getUuid())) {
 219				return i;
 220			} else {
 221				Message next = messages.get(i);
 222				while(next != null && next.wasMergedIntoPrevious()) {
 223					if (uuid.equals(next.getUuid())) {
 224						return i;
 225					}
 226					next = next.next();
 227				}
 228
 229			}
 230		}
 231		return -1;
 232	}
 233
 234	public Pair<Integer,Integer> getScrollPosition() {
 235		if (this.messagesView.getCount() == 0 ||
 236				this.messagesView.getLastVisiblePosition() == this.messagesView.getCount() - 1) {
 237			return null;
 238		} else {
 239			final int pos = messagesView.getFirstVisiblePosition();
 240			final View view = messagesView.getChildAt(0);
 241			if (view == null) {
 242				return null;
 243			} else {
 244				return new Pair<>(pos, view.getTop());
 245			}
 246		}
 247	}
 248
 249	public void setScrollPosition(Pair<Integer,Integer> scrollPosition) {
 250		if (scrollPosition != null) {
 251			this.messagesView.setSelectionFromTop(scrollPosition.first, scrollPosition.second);
 252		}
 253	}
 254
 255	protected OnClickListener clickToDecryptListener = new OnClickListener() {
 256
 257		@Override
 258		public void onClick(View v) {
 259			PendingIntent pendingIntent = conversation.getAccount().getPgpDecryptionService().getPendingIntent();
 260			if (pendingIntent != null) {
 261				try {
 262					activity.startIntentSenderForResult(pendingIntent.getIntentSender(),
 263                            ConversationActivity.REQUEST_DECRYPT_PGP,
 264                            null,
 265                            0,
 266                            0,
 267                            0);
 268				} catch (SendIntentException e) {
 269					Toast.makeText(activity,R.string.unable_to_connect_to_keychain, Toast.LENGTH_SHORT).show();
 270					conversation.getAccount().getPgpDecryptionService().continueDecryption(true);
 271				}
 272			}
 273			updateSnackBar(conversation);
 274		}
 275	};
 276	protected OnClickListener clickToVerify = new OnClickListener() {
 277
 278		@Override
 279		public void onClick(View v) {
 280			activity.verifyOtrSessionDialog(conversation, v);
 281		}
 282	};
 283	private OnEditorActionListener mEditorActionListener = new OnEditorActionListener() {
 284
 285		@Override
 286		public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
 287			if (actionId == EditorInfo.IME_ACTION_SEND) {
 288				InputMethodManager imm = (InputMethodManager) v.getContext()
 289						.getSystemService(Context.INPUT_METHOD_SERVICE);
 290				if (imm.isFullscreenMode()) {
 291					imm.hideSoftInputFromWindow(v.getWindowToken(), 0);
 292				}
 293				sendMessage();
 294				return true;
 295			} else {
 296				return false;
 297			}
 298		}
 299	};
 300	private EditMessage.OnCommitContentListener mEditorContentListener = new EditMessage.OnCommitContentListener() {
 301		@Override
 302		public boolean onCommitContent(InputContentInfoCompat inputContentInfo, int flags, Bundle opts, String[] contentMimeTypes) {
 303			// try to get permission to read the image, if applicable
 304			if ((flags & InputConnectionCompat.INPUT_CONTENT_GRANT_READ_URI_PERMISSION) != 0) {
 305				try {
 306					inputContentInfo.requestPermission();
 307				} catch (Exception e) {
 308					Log.e(Config.LOGTAG, "InputContentInfoCompat#requestPermission() failed.", e);
 309					Toast.makeText(
 310							activity,
 311							activity.getString(R.string.no_permission_to_access_x, inputContentInfo.getDescription()),
 312							Toast.LENGTH_LONG
 313					).show();
 314					return false;
 315				}
 316			}
 317			if (activity.hasStoragePermission(ConversationActivity.REQUEST_ADD_EDITOR_CONTENT)) {
 318				activity.attachImageToConversation(inputContentInfo.getContentUri());
 319			} else {
 320				activity.mPendingEditorContent = inputContentInfo.getContentUri();
 321			}
 322			return true;
 323		}
 324	};
 325	private OnClickListener mSendButtonListener = new OnClickListener() {
 326
 327		@Override
 328		public void onClick(View v) {
 329			Object tag = v.getTag();
 330			if (tag instanceof SendButtonAction) {
 331				SendButtonAction action = (SendButtonAction) tag;
 332				switch (action) {
 333					case TAKE_PHOTO:
 334						activity.attachFile(ConversationActivity.ATTACHMENT_CHOICE_TAKE_PHOTO);
 335						break;
 336					case RECORD_VIDEO:
 337						activity.attachFile(ConversationActivity.ATTACHMENT_CHOICE_RECORD_VIDEO);
 338						break;
 339					case SEND_LOCATION:
 340						activity.attachFile(ConversationActivity.ATTACHMENT_CHOICE_LOCATION);
 341						break;
 342					case RECORD_VOICE:
 343						activity.attachFile(ConversationActivity.ATTACHMENT_CHOICE_RECORD_VOICE);
 344						break;
 345					case CHOOSE_PICTURE:
 346						activity.attachFile(ConversationActivity.ATTACHMENT_CHOICE_CHOOSE_IMAGE);
 347						break;
 348					case CANCEL:
 349						if (conversation != null) {
 350							if(conversation.setCorrectingMessage(null)) {
 351								mEditMessage.setText("");
 352								mEditMessage.append(conversation.getDraftMessage());
 353								conversation.setDraftMessage(null);
 354							} else if (conversation.getMode() == Conversation.MODE_MULTI) {
 355								conversation.setNextCounterpart(null);
 356							}
 357							updateChatMsgHint();
 358							updateSendButton();
 359							updateEditablity();
 360						}
 361						break;
 362					default:
 363						sendMessage();
 364				}
 365			} else {
 366				sendMessage();
 367			}
 368		}
 369	};
 370	private OnClickListener clickToMuc = new OnClickListener() {
 371
 372		@Override
 373		public void onClick(View v) {
 374			Intent intent = new Intent(getActivity(), ConferenceDetailsActivity.class);
 375			intent.setAction(ConferenceDetailsActivity.ACTION_VIEW_MUC);
 376			intent.putExtra("uuid", conversation.getUuid());
 377			startActivity(intent);
 378		}
 379	};
 380	private ConversationActivity activity;
 381	private Message selectedMessage;
 382
 383	private void sendMessage() {
 384		final String body = mEditMessage.getText().toString();
 385		final Conversation conversation = this.conversation;
 386		if (body.length() == 0 || conversation == null) {
 387			return;
 388		}
 389		final Message message;
 390		if (conversation.getCorrectingMessage() == null) {
 391			message = new Message(conversation, body, conversation.getNextEncryption());
 392			if (conversation.getMode() == Conversation.MODE_MULTI) {
 393				if (conversation.getNextCounterpart() != null) {
 394					message.setCounterpart(conversation.getNextCounterpart());
 395					message.setType(Message.TYPE_PRIVATE);
 396				}
 397			}
 398		} else {
 399			message = conversation.getCorrectingMessage();
 400			message.setBody(body);
 401			message.setEdited(message.getUuid());
 402			message.setUuid(UUID.randomUUID().toString());
 403		}
 404		switch (message.getConversation().getNextEncryption()) {
 405			case Message.ENCRYPTION_OTR:
 406				sendOtrMessage(message);
 407				break;
 408			case Message.ENCRYPTION_PGP:
 409				sendPgpMessage(message);
 410				break;
 411			case Message.ENCRYPTION_AXOLOTL:
 412				if(!activity.trustKeysIfNeeded(ConversationActivity.REQUEST_TRUST_KEYS_TEXT)) {
 413					sendAxolotlMessage(message);
 414				}
 415				break;
 416			default:
 417				sendPlainTextMessage(message);
 418		}
 419	}
 420
 421	public void updateChatMsgHint() {
 422		final boolean multi = conversation.getMode() == Conversation.MODE_MULTI;
 423		if (conversation.getCorrectingMessage() != null) {
 424			this.mEditMessage.setHint(R.string.send_corrected_message);
 425		} else if (multi && conversation.getNextCounterpart() != null) {
 426			this.mEditMessage.setHint(getString(
 427					R.string.send_private_message_to,
 428					conversation.getNextCounterpart().getResourcepart()));
 429		} else if (multi && !conversation.getMucOptions().participating()) {
 430			this.mEditMessage.setHint(R.string.you_are_not_participating);
 431		} else {
 432			this.mEditMessage.setHint(UIHelper.getMessageHint(activity,conversation));
 433			getActivity().invalidateOptionsMenu();
 434		}
 435	}
 436
 437	public void setupIme() {
 438		if (activity != null) {
 439			if (activity.usingEnterKey() && activity.enterIsSend()) {
 440				mEditMessage.setInputType(mEditMessage.getInputType() & (~InputType.TYPE_TEXT_FLAG_MULTI_LINE));
 441				mEditMessage.setInputType(mEditMessage.getInputType() & (~InputType.TYPE_TEXT_VARIATION_SHORT_MESSAGE));
 442			} else if (activity.usingEnterKey()) {
 443				mEditMessage.setInputType(mEditMessage.getInputType() | InputType.TYPE_TEXT_FLAG_MULTI_LINE);
 444				mEditMessage.setInputType(mEditMessage.getInputType() & (~InputType.TYPE_TEXT_VARIATION_SHORT_MESSAGE));
 445			} else {
 446				mEditMessage.setInputType(mEditMessage.getInputType() | InputType.TYPE_TEXT_FLAG_MULTI_LINE);
 447				mEditMessage.setInputType(mEditMessage.getInputType() | InputType.TYPE_TEXT_VARIATION_SHORT_MESSAGE);
 448			}
 449		}
 450	}
 451
 452	@Override
 453	public View onCreateView(final LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
 454		final View view = inflater.inflate(R.layout.fragment_conversation, container, false);
 455		view.setOnClickListener(null);
 456
 457		mEditMessage = (EditMessage) view.findViewById(R.id.textinput);
 458		mEditMessage.setOnClickListener(new OnClickListener() {
 459
 460			@Override
 461			public void onClick(View v) {
 462				if (activity != null) {
 463					activity.hideConversationsOverview();
 464				}
 465			}
 466		});
 467
 468		mEditMessage.addTextChangedListener(new StylingHelper.MessageEditorStyler(mEditMessage));
 469
 470		mEditMessage.setOnEditorActionListener(mEditorActionListener);
 471		mEditMessage.setRichContentListener(new String[]{"image/*"}, mEditorContentListener);
 472
 473		mSendButton = (ImageButton) view.findViewById(R.id.textSendButton);
 474		mSendButton.setOnClickListener(this.mSendButtonListener);
 475
 476		snackbar = (RelativeLayout) view.findViewById(R.id.snackbar);
 477		snackbarMessage = (TextView) view.findViewById(R.id.snackbar_message);
 478		snackbarAction = (TextView) view.findViewById(R.id.snackbar_action);
 479
 480		messagesView = (ListView) view.findViewById(R.id.messages_view);
 481		messagesView.setOnScrollListener(mOnScrollListener);
 482		messagesView.setTranscriptMode(ListView.TRANSCRIPT_MODE_NORMAL);
 483		messageListAdapter = new MessageAdapter((ConversationActivity) getActivity(), this.messageList);
 484		messageListAdapter.setOnContactPictureClicked(new OnContactPictureClicked() {
 485
 486			@Override
 487			public void onContactPictureClicked(Message message) {
 488				if (message.getStatus() <= Message.STATUS_RECEIVED) {
 489					if (message.getConversation().getMode() == Conversation.MODE_MULTI) {
 490						Jid user = message.getCounterpart();
 491						if (user != null && !user.isBareJid()) {
 492							if (!message.getConversation().getMucOptions().isUserInRoom(user)) {
 493								Toast.makeText(activity,activity.getString(R.string.user_has_left_conference,user.getResourcepart()),Toast.LENGTH_SHORT).show();
 494							}
 495							highlightInConference(user.getResourcepart());
 496						}
 497					} else {
 498						if (!message.getContact().isSelf()) {
 499							String fingerprint;
 500							if (message.getEncryption() == Message.ENCRYPTION_PGP
 501									|| message.getEncryption() == Message.ENCRYPTION_DECRYPTED) {
 502								fingerprint = "pgp";
 503							} else {
 504								fingerprint = message.getFingerprint();
 505							}
 506							activity.switchToContactDetails(message.getContact(), fingerprint);
 507						}
 508					}
 509				} else {
 510					Account account = message.getConversation().getAccount();
 511					Intent intent;
 512					if (activity.manuallyChangePresence()) {
 513						intent = new Intent(activity, SetPresenceActivity.class);
 514						intent.putExtra(SetPresenceActivity.EXTRA_ACCOUNT, account.getJid().toBareJid().toString());
 515					} else {
 516						intent = new Intent(activity, EditAccountActivity.class);
 517						intent.putExtra("jid", account.getJid().toBareJid().toString());
 518						String fingerprint;
 519						if (message.getEncryption() == Message.ENCRYPTION_PGP
 520								|| message.getEncryption() == Message.ENCRYPTION_DECRYPTED) {
 521							fingerprint = "pgp";
 522						} else if (message.getEncryption() == Message.ENCRYPTION_OTR) {
 523							fingerprint = "otr";
 524						} else {
 525							fingerprint = message.getFingerprint();
 526						}
 527						intent.putExtra("fingerprint", fingerprint);
 528					}
 529					startActivity(intent);
 530				}
 531			}
 532		});
 533		messageListAdapter
 534				.setOnContactPictureLongClicked(new OnContactPictureLongClicked() {
 535
 536					@Override
 537					public void onContactPictureLongClicked(Message message) {
 538						if (message.getStatus() <= Message.STATUS_RECEIVED) {
 539							if (message.getConversation().getMode() == Conversation.MODE_MULTI) {
 540								Jid user = message.getCounterpart();
 541								if (user != null && !user.isBareJid()) {
 542									if (message.getConversation().getMucOptions().isUserInRoom(user)) {
 543										privateMessageWith(user);
 544									} else {
 545										Toast.makeText(activity, activity.getString(R.string.user_has_left_conference, user.getResourcepart()), Toast.LENGTH_SHORT).show();
 546									}
 547								}
 548							}
 549						} else {
 550							activity.showQrCode();
 551						}
 552					}
 553				});
 554		messageListAdapter.setOnQuoteListener(new MessageAdapter.OnQuoteListener() {
 555
 556			@Override
 557			public void onQuote(String text) {
 558				if (mEditMessage.isEnabled()) {
 559					text = text.replaceAll("(\n *){2,}", "\n").replaceAll("(^|\n)", "$1> ").replaceAll("\n$", "");
 560					Editable editable = mEditMessage.getEditableText();
 561					int position = mEditMessage.getSelectionEnd();
 562					if (position == -1) position = editable.length();
 563					if (position > 0 && editable.charAt(position - 1) != '\n') {
 564						editable.insert(position++, "\n");
 565					}
 566					editable.insert(position, text);
 567					position += text.length();
 568					editable.insert(position++, "\n");
 569					if (position < editable.length() && editable.charAt(position) != '\n') {
 570						editable.insert(position, "\n");
 571					}
 572					mEditMessage.setSelection(position);
 573					mEditMessage.requestFocus();
 574					InputMethodManager inputMethodManager = (InputMethodManager) getActivity()
 575							.getSystemService(Context.INPUT_METHOD_SERVICE);
 576					if (inputMethodManager != null) {
 577						inputMethodManager.showSoftInput(mEditMessage, InputMethodManager.SHOW_IMPLICIT);
 578					}
 579				}
 580			}
 581		});
 582		messagesView.setAdapter(messageListAdapter);
 583
 584		registerForContextMenu(messagesView);
 585
 586		return view;
 587	}
 588
 589	@Override
 590	public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
 591		synchronized (this.messageList) {
 592			super.onCreateContextMenu(menu, v, menuInfo);
 593			AdapterView.AdapterContextMenuInfo acmi = (AdapterContextMenuInfo) menuInfo;
 594			this.selectedMessage = this.messageList.get(acmi.position);
 595			populateContextMenu(menu);
 596		}
 597	}
 598
 599	private void populateContextMenu(ContextMenu menu) {
 600		final Message m = this.selectedMessage;
 601		final Transferable t = m.getTransferable();
 602		Message relevantForCorrection = m;
 603		while(relevantForCorrection.mergeable(relevantForCorrection.next())) {
 604			relevantForCorrection = relevantForCorrection.next();
 605		}
 606		if (m.getType() != Message.TYPE_STATUS) {
 607			final boolean treatAsFile = m.getType() != Message.TYPE_TEXT
 608					&& m.getType() != Message.TYPE_PRIVATE
 609					&& t == null;
 610			activity.getMenuInflater().inflate(R.menu.message_context, menu);
 611			menu.setHeaderTitle(R.string.message_options);
 612			MenuItem selectText = menu.findItem(R.id.select_text);
 613			MenuItem retryDecryption = menu.findItem(R.id.retry_decryption);
 614			MenuItem correctMessage = menu.findItem(R.id.correct_message);
 615			MenuItem shareWith = menu.findItem(R.id.share_with);
 616			MenuItem sendAgain = menu.findItem(R.id.send_again);
 617			MenuItem copyUrl = menu.findItem(R.id.copy_url);
 618			MenuItem downloadFile = menu.findItem(R.id.download_file);
 619			MenuItem cancelTransmission = menu.findItem(R.id.cancel_transmission);
 620			MenuItem deleteFile = menu.findItem(R.id.delete_file);
 621			MenuItem showErrorMessage = menu.findItem(R.id.show_error_message);
 622			if (!treatAsFile && !m.isGeoUri() && !m.treatAsDownloadable()) {
 623				selectText.setVisible(ListSelectionManager.isSupported());
 624			}
 625			if (m.getEncryption() == Message.ENCRYPTION_DECRYPTION_FAILED) {
 626				retryDecryption.setVisible(true);
 627			}
 628			if (relevantForCorrection.getType() == Message.TYPE_TEXT
 629					&& relevantForCorrection.isLastCorrectableMessage()
 630					&& (m.getConversation().getMucOptions().nonanonymous() || m.getConversation().getMode() == Conversation.MODE_SINGLE)) {
 631				correctMessage.setVisible(true);
 632			}
 633			if (treatAsFile || (m.getType() == Message.TYPE_TEXT && !m.treatAsDownloadable())) {
 634				shareWith.setVisible(true);
 635			}
 636			if (m.getStatus() == Message.STATUS_SEND_FAILED) {
 637				sendAgain.setVisible(true);
 638			}
 639			if (m.hasFileOnRemoteHost()
 640					|| m.isGeoUri()
 641					|| m.treatAsDownloadable()
 642					|| (t != null && t instanceof HttpDownloadConnection)) {
 643				copyUrl.setVisible(true);
 644			}
 645			if ((m.isFileOrImage() && t instanceof TransferablePlaceholder && m.hasFileOnRemoteHost())) {
 646				downloadFile.setVisible(true);
 647				downloadFile.setTitle(activity.getString(R.string.download_x_file,UIHelper.getFileDescriptionString(activity, m)));
 648			}
 649			boolean waitingOfferedSending = m.getStatus() == Message.STATUS_WAITING
 650					|| m.getStatus() == Message.STATUS_UNSEND
 651					|| m.getStatus() == Message.STATUS_OFFERED;
 652			if ((t != null && !(t instanceof TransferablePlaceholder)) || waitingOfferedSending && m.needsUploading()) {
 653				cancelTransmission.setVisible(true);
 654			}
 655			if (treatAsFile) {
 656				String path = m.getRelativeFilePath();
 657				if (path == null || !path.startsWith("/")) {
 658					deleteFile.setVisible(true);
 659					deleteFile.setTitle(activity.getString(R.string.delete_x_file, UIHelper.getFileDescriptionString(activity, m)));
 660				}
 661			}
 662			if (m.getStatus() == Message.STATUS_SEND_FAILED && m.getErrorMessage() != null) {
 663				showErrorMessage.setVisible(true);
 664			}
 665		}
 666	}
 667
 668	@Override
 669	public boolean onContextItemSelected(MenuItem item) {
 670		switch (item.getItemId()) {
 671			case R.id.share_with:
 672				shareWith(selectedMessage);
 673				return true;
 674			case R.id.select_text:
 675				selectText(selectedMessage);
 676				return true;
 677			case R.id.correct_message:
 678				correctMessage(selectedMessage);
 679				return true;
 680			case R.id.send_again:
 681				resendMessage(selectedMessage);
 682				return true;
 683			case R.id.copy_url:
 684				copyUrl(selectedMessage);
 685				return true;
 686			case R.id.download_file:
 687				downloadFile(selectedMessage);
 688				return true;
 689			case R.id.cancel_transmission:
 690				cancelTransmission(selectedMessage);
 691				return true;
 692			case R.id.retry_decryption:
 693				retryDecryption(selectedMessage);
 694				return true;
 695			case R.id.delete_file:
 696				deleteFile(selectedMessage);
 697				return true;
 698			case R.id.show_error_message:
 699				showErrorMessage(selectedMessage);
 700				return true;
 701			default:
 702				return super.onContextItemSelected(item);
 703		}
 704	}
 705
 706	private void showErrorMessage(final Message message) {
 707		AlertDialog.Builder builder = new AlertDialog.Builder(activity);
 708		builder.setTitle(R.string.error_message);
 709		builder.setMessage(message.getErrorMessage());
 710		builder.setPositiveButton(R.string.confirm,null);
 711		builder.create().show();
 712	}
 713
 714	private void shareWith(Message message) {
 715		Intent shareIntent = new Intent();
 716		shareIntent.setAction(Intent.ACTION_SEND);
 717		if (message.isGeoUri()) {
 718			shareIntent.putExtra(Intent.EXTRA_TEXT, message.getBody());
 719			shareIntent.setType("text/plain");
 720		} else if (!message.isFileOrImage()) {
 721			shareIntent.putExtra(Intent.EXTRA_TEXT, message.getMergedBody().toString());
 722			shareIntent.setType("text/plain");
 723		} else {
 724			final DownloadableFile file = activity.xmppConnectionService.getFileBackend().getFile(message);
 725			try {
 726				shareIntent.putExtra(Intent.EXTRA_STREAM, FileBackend.getUriForFile(activity, file));
 727			} catch (SecurityException e) {
 728				Toast.makeText(activity, activity.getString(R.string.no_permission_to_access_x, file.getAbsolutePath()), Toast.LENGTH_SHORT).show();
 729				return;
 730			}
 731			shareIntent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
 732			String mime = message.getMimeType();
 733			if (mime == null) {
 734				mime = "*/*";
 735			}
 736			shareIntent.setType(mime);
 737		}
 738		try {
 739			activity.startActivity(Intent.createChooser(shareIntent, getText(R.string.share_with)));
 740		} catch (ActivityNotFoundException e) {
 741			//This should happen only on faulty androids because normally chooser is always available
 742			Toast.makeText(activity,R.string.no_application_found_to_open_file,Toast.LENGTH_SHORT).show();
 743		}
 744	}
 745
 746	private void selectText(Message message) {
 747		final int index;
 748		synchronized (this.messageList) {
 749			index = this.messageList.indexOf(message);
 750		}
 751		if (index >= 0) {
 752			final int first = this.messagesView.getFirstVisiblePosition();
 753			final int last = first + this.messagesView.getChildCount();
 754			if (index >= first && index < last)	{
 755				final View view = this.messagesView.getChildAt(index - first);
 756				final TextView messageBody = this.messageListAdapter.getMessageBody(view);
 757				if (messageBody != null) {
 758					ListSelectionManager.startSelection(messageBody);
 759				}
 760			}
 761		}
 762	}
 763
 764	private void deleteFile(Message message) {
 765		if (activity.xmppConnectionService.getFileBackend().deleteFile(message)) {
 766			message.setTransferable(new TransferablePlaceholder(Transferable.STATUS_DELETED));
 767			activity.updateConversationList();
 768			updateMessages();
 769		}
 770	}
 771
 772	private void resendMessage(final Message message) {
 773		if (message.isFileOrImage()) {
 774			DownloadableFile file = activity.xmppConnectionService.getFileBackend().getFile(message);
 775			if (file.exists()) {
 776				final Conversation conversation = message.getConversation();
 777				final XmppConnection xmppConnection = conversation.getAccount().getXmppConnection();
 778				if (!message.hasFileOnRemoteHost()
 779						&& xmppConnection != null
 780						&& !xmppConnection.getFeatures().httpUpload(message.getFileParams().size)) {
 781					activity.selectPresence(conversation, new OnPresenceSelected() {
 782						@Override
 783						public void onPresenceSelected() {
 784							message.setCounterpart(conversation.getNextCounterpart());
 785							activity.xmppConnectionService.resendFailedMessages(message);
 786						}
 787					});
 788					return;
 789				}
 790			} else {
 791				Toast.makeText(activity, R.string.file_deleted, Toast.LENGTH_SHORT).show();
 792				message.setTransferable(new TransferablePlaceholder(Transferable.STATUS_DELETED));
 793				activity.updateConversationList();
 794				updateMessages();
 795				return;
 796			}
 797		}
 798		activity.xmppConnectionService.resendFailedMessages(message);
 799	}
 800
 801	private void copyUrl(Message message) {
 802		final String url;
 803		final int resId;
 804		if (message.isGeoUri()) {
 805			resId = R.string.location;
 806			url = message.getBody();
 807		} else if (message.hasFileOnRemoteHost()) {
 808			resId = R.string.file_url;
 809			url = message.getFileParams().url.toString();
 810		} else {
 811			url = message.getBody().trim();
 812			resId = R.string.file_url;
 813		}
 814		if (activity.copyTextToClipboard(url, resId)) {
 815			Toast.makeText(activity, R.string.url_copied_to_clipboard,
 816					Toast.LENGTH_SHORT).show();
 817		}
 818	}
 819
 820	private void downloadFile(Message message) {
 821		activity.xmppConnectionService.getHttpConnectionManager().createNewDownloadConnection(message,true);
 822	}
 823
 824	private void cancelTransmission(Message message) {
 825		Transferable transferable = message.getTransferable();
 826		if (transferable != null) {
 827			transferable.cancel();
 828		} else if (message.getStatus() != Message.STATUS_RECEIVED) {
 829			activity.xmppConnectionService.markMessage(message,Message.STATUS_SEND_FAILED);
 830		}
 831	}
 832
 833	private void retryDecryption(Message message) {
 834		message.setEncryption(Message.ENCRYPTION_PGP);
 835		activity.updateConversationList();
 836		updateMessages();
 837		conversation.getAccount().getPgpDecryptionService().decrypt(message, false);
 838	}
 839
 840	protected void privateMessageWith(final Jid counterpart) {
 841		if (conversation.setOutgoingChatState(Config.DEFAULT_CHATSTATE)) {
 842			activity.xmppConnectionService.sendChatState(conversation);
 843		}
 844		this.mEditMessage.setText("");
 845		this.conversation.setNextCounterpart(counterpart);
 846		updateChatMsgHint();
 847		updateSendButton();
 848		updateEditablity();
 849	}
 850
 851	private void correctMessage(Message message) {
 852		while(message.mergeable(message.next())) {
 853			message = message.next();
 854		}
 855		this.conversation.setCorrectingMessage(message);
 856		final Editable editable = mEditMessage.getText();
 857		this.conversation.setDraftMessage(editable.toString());
 858		this.mEditMessage.setText("");
 859		this.mEditMessage.append(message.getBody());
 860
 861	}
 862
 863	protected void highlightInConference(String nick) {
 864		final Editable editable = mEditMessage.getText();
 865		String oldString = editable.toString().trim();
 866		final int pos = mEditMessage.getSelectionStart();
 867		if (oldString.isEmpty() || pos == 0) {
 868			editable.insert(0, nick + ": ");
 869		} else {
 870			final char before = editable.charAt(pos - 1);
 871			final char after = editable.length() > pos ? editable.charAt(pos) : '\0';
 872			if (before == '\n') {
 873				editable.insert(pos, nick + ": ");
 874			} else {
 875				if (pos > 2 && editable.subSequence(pos-2,pos).toString().equals(": ")) {
 876					if (NickValidityChecker.check(conversation,Arrays.asList(editable.subSequence(0,pos-2).toString().split(", ")))) {
 877						editable.insert(pos - 2, ", " + nick);
 878						return;
 879					}
 880				}
 881				editable.insert(pos, (Character.isWhitespace(before) ? "" : " ") + nick + (Character.isWhitespace(after) ? "" : " "));
 882				if (Character.isWhitespace(after)) {
 883					mEditMessage.setSelection(mEditMessage.getSelectionStart() + 1);
 884				}
 885			}
 886		}
 887	}
 888
 889	@Override
 890	public void onStop() {
 891		super.onStop();
 892		if (activity == null || !activity.isChangingConfigurations()) {
 893			messageListAdapter.stopAudioPlayer();
 894		}
 895		if (this.conversation != null) {
 896			final String msg = mEditMessage.getText().toString();
 897			if (this.conversation.setNextMessage(msg)) {
 898				activity.xmppConnectionService.updateConversation(this.conversation);
 899			}
 900			updateChatState(this.conversation, msg);
 901		}
 902	}
 903
 904	private void updateChatState(final Conversation conversation, final String msg) {
 905		ChatState state = msg.length() == 0 ? Config.DEFAULT_CHATSTATE : ChatState.PAUSED;
 906		Account.State status = conversation.getAccount().getStatus();
 907		if (status == Account.State.ONLINE && conversation.setOutgoingChatState(state)) {
 908			activity.xmppConnectionService.sendChatState(conversation);
 909		}
 910	}
 911
 912	public boolean reInit(Conversation conversation) {
 913		if (conversation == null) {
 914			return false;
 915		}
 916		this.activity = (ConversationActivity) getActivity();
 917		setupIme();
 918		if (this.conversation != null) {
 919			final String msg = mEditMessage.getText().toString();
 920			if (this.conversation.setNextMessage(msg)) {
 921				activity.xmppConnectionService.updateConversation(conversation);
 922			}
 923			if (this.conversation != conversation) {
 924				updateChatState(this.conversation, msg);
 925				messageListAdapter.stopAudioPlayer();
 926			}
 927			this.conversation.trim();
 928
 929		}
 930
 931		if (activity != null) {
 932			this.mSendButton.setContentDescription(activity.getString(R.string.send_message_to_x,conversation.getName()));
 933		}
 934
 935		this.conversation = conversation;
 936		this.mEditMessage.setKeyboardListener(null);
 937		this.mEditMessage.setText("");
 938		this.mEditMessage.append(this.conversation.getNextMessage());
 939		this.mEditMessage.setKeyboardListener(this);
 940		messageListAdapter.updatePreferences();
 941		this.messagesView.setAdapter(messageListAdapter);
 942		updateMessages();
 943		this.conversation.messagesLoaded.set(true);
 944		synchronized (this.messageList) {
 945			final Message first = conversation.getFirstUnreadMessage();
 946			final int bottom = Math.max(0, this.messageList.size() - 1);
 947			final int pos;
 948			if (first == null) {
 949				pos = bottom;
 950			} else {
 951				int i = getIndexOf(first.getUuid(), this.messageList);
 952				pos = i < 0 ? bottom : i;
 953			}
 954			messagesView.setSelection(pos);
 955			return pos == bottom;
 956		}
 957	}
 958
 959	private OnClickListener mEnableAccountListener = new OnClickListener() {
 960		@Override
 961		public void onClick(View v) {
 962			final Account account = conversation == null ? null : conversation.getAccount();
 963			if (account != null) {
 964				account.setOption(Account.OPTION_DISABLED, false);
 965				activity.xmppConnectionService.updateAccount(account);
 966			}
 967		}
 968	};
 969
 970	private OnClickListener mUnblockClickListener = new OnClickListener() {
 971		@Override
 972		public void onClick(final View v) {
 973			v.post(new Runnable() {
 974				@Override
 975				public void run() {
 976					v.setVisibility(View.INVISIBLE);
 977				}
 978			});
 979			if (conversation.isDomainBlocked()) {
 980				BlockContactDialog.show(activity, conversation);
 981			} else {
 982				activity.unblockConversation(conversation);
 983			}
 984		}
 985	};
 986
 987	private void showBlockSubmenu(View view) {
 988		final Jid jid = conversation.getJid();
 989			if (jid.isDomainJid()) {
 990				BlockContactDialog.show(activity, conversation);
 991			} else {
 992				PopupMenu popupMenu = new PopupMenu(activity, view);
 993				popupMenu.inflate(R.menu.block);
 994				popupMenu.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() {
 995					@Override
 996					public boolean onMenuItemClick(MenuItem menuItem) {
 997						Blockable blockable;
 998						switch (menuItem.getItemId()) {
 999							case R.id.block_domain:
1000								blockable = conversation.getAccount().getRoster().getContact(jid.toDomainJid());
1001								break;
1002							default:
1003								blockable = conversation;
1004						}
1005						BlockContactDialog.show(activity, blockable);
1006						return true;
1007					}
1008				});
1009				popupMenu.show();
1010			}
1011	}
1012
1013	private OnClickListener mBlockClickListener = new OnClickListener() {
1014		@Override
1015		public void onClick(final View view) {
1016			showBlockSubmenu(view);
1017		}
1018	};
1019
1020	private OnClickListener mAddBackClickListener = new OnClickListener() {
1021
1022		@Override
1023		public void onClick(View v) {
1024			final Contact contact = conversation == null ? null : conversation.getContact();
1025			if (contact != null) {
1026				activity.xmppConnectionService.createContact(contact);
1027				activity.switchToContactDetails(contact);
1028			}
1029		}
1030	};
1031
1032	private View.OnLongClickListener mLongPressBlockListener = new View.OnLongClickListener() {
1033		@Override
1034		public boolean onLongClick(View v) {
1035			showBlockSubmenu(v);
1036			return true;
1037		}
1038	};
1039
1040	private OnClickListener mAllowPresenceSubscription = new OnClickListener() {
1041		@Override
1042		public void onClick(View v) {
1043			final Contact contact = conversation == null ? null : conversation.getContact();
1044			if (contact != null) {
1045				activity.xmppConnectionService.sendPresencePacket(contact.getAccount(),
1046						activity.xmppConnectionService.getPresenceGenerator()
1047								.sendPresenceUpdatesTo(contact));
1048				hideSnackbar();
1049			}
1050		}
1051	};
1052
1053	private OnClickListener mAnswerSmpClickListener = new OnClickListener() {
1054		@Override
1055		public void onClick(View view) {
1056			Intent intent = new Intent(activity, VerifyOTRActivity.class);
1057			intent.setAction(VerifyOTRActivity.ACTION_VERIFY_CONTACT);
1058			intent.putExtra("contact", conversation.getContact().getJid().toBareJid().toString());
1059			intent.putExtra(VerifyOTRActivity.EXTRA_ACCOUNT, conversation.getAccount().getJid().toBareJid().toString());
1060			intent.putExtra("mode", VerifyOTRActivity.MODE_ANSWER_QUESTION);
1061			startActivity(intent);
1062		}
1063	};
1064
1065	private void updateSnackBar(final Conversation conversation) {
1066		final Account account = conversation.getAccount();
1067		final XmppConnection connection = account.getXmppConnection();
1068		final int mode = conversation.getMode();
1069		final Contact contact = mode == Conversation.MODE_SINGLE ? conversation.getContact() : null;
1070		if (account.getStatus() == Account.State.DISABLED) {
1071			showSnackbar(R.string.this_account_is_disabled, R.string.enable, this.mEnableAccountListener);
1072		} else if (conversation.isBlocked()) {
1073			showSnackbar(R.string.contact_blocked, R.string.unblock, this.mUnblockClickListener);
1074		} else if (contact != null && !contact.showInRoster() && contact.getOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST)) {
1075			showSnackbar(R.string.contact_added_you, R.string.add_back, this.mAddBackClickListener, this.mLongPressBlockListener);
1076		} else if (contact != null && contact.getOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST)) {
1077			showSnackbar(R.string.contact_asks_for_presence_subscription, R.string.allow, this.mAllowPresenceSubscription, this.mLongPressBlockListener);
1078		} else if (mode == Conversation.MODE_MULTI
1079				&& !conversation.getMucOptions().online()
1080				&& account.getStatus() == Account.State.ONLINE) {
1081			switch (conversation.getMucOptions().getError()) {
1082				case NICK_IN_USE:
1083					showSnackbar(R.string.nick_in_use, R.string.edit, clickToMuc);
1084					break;
1085				case NO_RESPONSE:
1086					showSnackbar(R.string.joining_conference, 0, null);
1087					break;
1088				case SERVER_NOT_FOUND:
1089					if (conversation.receivedMessagesCount() > 0) {
1090						showSnackbar(R.string.remote_server_not_found,R.string.try_again, joinMuc);
1091					} else {
1092						showSnackbar(R.string.remote_server_not_found, R.string.leave, leaveMuc);
1093					}
1094					break;
1095				case PASSWORD_REQUIRED:
1096					showSnackbar(R.string.conference_requires_password, R.string.enter_password, enterPassword);
1097					break;
1098				case BANNED:
1099					showSnackbar(R.string.conference_banned, R.string.leave, leaveMuc);
1100					break;
1101				case MEMBERS_ONLY:
1102					showSnackbar(R.string.conference_members_only, R.string.leave, leaveMuc);
1103					break;
1104				case KICKED:
1105					showSnackbar(R.string.conference_kicked, R.string.join, joinMuc);
1106					break;
1107				case UNKNOWN:
1108					showSnackbar(R.string.conference_unknown_error, R.string.join, joinMuc);
1109					break;
1110				case SHUTDOWN:
1111					showSnackbar(R.string.conference_shutdown, R.string.join, joinMuc);
1112					break;
1113				default:
1114					hideSnackbar();
1115					break;
1116			}
1117		} else if (account.hasPendingPgpIntent(conversation)) {
1118			showSnackbar(R.string.openpgp_messages_found, R.string.decrypt, clickToDecryptListener);
1119		} else if (mode == Conversation.MODE_SINGLE
1120				&& conversation.smpRequested()) {
1121			showSnackbar(R.string.smp_requested, R.string.verify, this.mAnswerSmpClickListener);
1122		} else if (mode == Conversation.MODE_SINGLE
1123				&& conversation.hasValidOtrSession()
1124				&& (conversation.getOtrSession().getSessionStatus() == SessionStatus.ENCRYPTED)
1125				&& (!conversation.isOtrFingerprintVerified())) {
1126			showSnackbar(R.string.unknown_otr_fingerprint, R.string.verify, clickToVerify);
1127		} else if (connection != null
1128				&& connection.getFeatures().blocking()
1129				&& conversation.countMessages() != 0
1130				&& !conversation.isBlocked()
1131				&& conversation.isWithStranger()) {
1132			showSnackbar(R.string.received_message_from_stranger,R.string.block, mBlockClickListener);
1133		} else {
1134			hideSnackbar();
1135		}
1136	}
1137
1138	public void updateMessages() {
1139		synchronized (this.messageList) {
1140			if (getView() == null) {
1141				return;
1142			}
1143			final ConversationActivity activity = (ConversationActivity) getActivity();
1144			if (this.conversation != null) {
1145				conversation.populateWithMessages(ConversationFragment.this.messageList);
1146				updateSnackBar(conversation);
1147				updateStatusMessages();
1148				this.messageListAdapter.notifyDataSetChanged();
1149				updateChatMsgHint();
1150				if (!activity.isConversationsOverviewVisable() || !activity.isConversationsOverviewHideable()) {
1151					activity.sendReadMarkerIfNecessary(conversation);
1152				}
1153				updateSendButton();
1154				updateEditablity();
1155			}
1156		}
1157	}
1158
1159	protected void messageSent() {
1160		mSendingPgpMessage.set(false);
1161		mEditMessage.setText("");
1162		if (conversation.setCorrectingMessage(null)) {
1163			mEditMessage.append(conversation.getDraftMessage());
1164			conversation.setDraftMessage(null);
1165		}
1166		if (conversation.setNextMessage(mEditMessage.getText().toString())) {
1167			activity.xmppConnectionService.updateConversation(conversation);
1168		}
1169		updateChatMsgHint();
1170		new Handler().post(new Runnable() {
1171			@Override
1172			public void run() {
1173				int size = messageList.size();
1174				messagesView.setSelection(size - 1);
1175			}
1176		});
1177	}
1178
1179	public void setFocusOnInputField() {
1180		mEditMessage.requestFocus();
1181	}
1182
1183	public void doneSendingPgpMessage() {
1184		mSendingPgpMessage.set(false);
1185	}
1186
1187	enum SendButtonAction {TEXT, TAKE_PHOTO, SEND_LOCATION, RECORD_VOICE, CANCEL, CHOOSE_PICTURE, RECORD_VIDEO;
1188
1189		public static SendButtonAction valueOfOrDefault(String setting, SendButtonAction text) {
1190			try {
1191				return valueOf(setting);
1192			} catch (IllegalArgumentException e) {
1193				return TEXT;
1194			}
1195		}
1196	}
1197
1198	private int getSendButtonImageResource(SendButtonAction action, Presence.Status status) {
1199		switch (action) {
1200			case TEXT:
1201				switch (status) {
1202					case CHAT:
1203					case ONLINE:
1204						return R.drawable.ic_send_text_online;
1205					case AWAY:
1206						return R.drawable.ic_send_text_away;
1207					case XA:
1208					case DND:
1209						return R.drawable.ic_send_text_dnd;
1210					default:
1211						return activity.getThemeResource(R.attr.ic_send_text_offline, R.drawable.ic_send_text_offline);
1212				}
1213			case RECORD_VIDEO:
1214				switch (status) {
1215					case CHAT:
1216					case ONLINE:
1217						return R.drawable.ic_send_videocam_online;
1218					case AWAY:
1219						return R.drawable.ic_send_videocam_away;
1220					case XA:
1221					case DND:
1222						return R.drawable.ic_send_videocam_dnd;
1223					default:
1224						return activity.getThemeResource(R.attr.ic_send_videocam_offline, R.drawable.ic_send_videocam_offline);
1225				}
1226			case TAKE_PHOTO:
1227				switch (status) {
1228					case CHAT:
1229					case ONLINE:
1230						return R.drawable.ic_send_photo_online;
1231					case AWAY:
1232						return R.drawable.ic_send_photo_away;
1233					case XA:
1234					case DND:
1235						return R.drawable.ic_send_photo_dnd;
1236					default:
1237						return activity.getThemeResource(R.attr.ic_send_photo_offline, R.drawable.ic_send_photo_offline);
1238				}
1239			case RECORD_VOICE:
1240				switch (status) {
1241					case CHAT:
1242					case ONLINE:
1243						return R.drawable.ic_send_voice_online;
1244					case AWAY:
1245						return R.drawable.ic_send_voice_away;
1246					case XA:
1247					case DND:
1248						return R.drawable.ic_send_voice_dnd;
1249					default:
1250						return activity.getThemeResource(R.attr.ic_send_voice_offline, R.drawable.ic_send_voice_offline);
1251				}
1252			case SEND_LOCATION:
1253				switch (status) {
1254					case CHAT:
1255					case ONLINE:
1256						return R.drawable.ic_send_location_online;
1257					case AWAY:
1258						return R.drawable.ic_send_location_away;
1259					case XA:
1260					case DND:
1261						return R.drawable.ic_send_location_dnd;
1262					default:
1263						return activity.getThemeResource(R.attr.ic_send_location_offline, R.drawable.ic_send_location_offline);
1264				}
1265			case CANCEL:
1266				switch (status) {
1267					case CHAT:
1268					case ONLINE:
1269						return R.drawable.ic_send_cancel_online;
1270					case AWAY:
1271						return R.drawable.ic_send_cancel_away;
1272					case XA:
1273					case DND:
1274						return R.drawable.ic_send_cancel_dnd;
1275					default:
1276						return activity.getThemeResource(R.attr.ic_send_cancel_offline, R.drawable.ic_send_cancel_offline);
1277				}
1278			case CHOOSE_PICTURE:
1279				switch (status) {
1280					case CHAT:
1281					case ONLINE:
1282						return R.drawable.ic_send_picture_online;
1283					case AWAY:
1284						return R.drawable.ic_send_picture_away;
1285					case XA:
1286					case DND:
1287						return R.drawable.ic_send_picture_dnd;
1288					default:
1289						return activity.getThemeResource(R.attr.ic_send_picture_offline, R.drawable.ic_send_picture_offline);
1290				}
1291		}
1292		return activity.getThemeResource(R.attr.ic_send_text_offline, R.drawable.ic_send_text_offline);
1293	}
1294
1295	private void updateEditablity() {
1296		boolean canWrite = this.conversation.getMode() == Conversation.MODE_SINGLE || this.conversation.getMucOptions().participating() || this.conversation.getNextCounterpart() != null;
1297		this.mEditMessage.setFocusable(canWrite);
1298		this.mEditMessage.setFocusableInTouchMode(canWrite);
1299		this.mSendButton.setEnabled(canWrite);
1300		this.mEditMessage.setCursorVisible(canWrite);
1301	}
1302
1303	public void updateSendButton() {
1304		final Conversation c = this.conversation;
1305		final SendButtonAction action;
1306		final Presence.Status status;
1307		final String text = this.mEditMessage == null ? "" : this.mEditMessage.getText().toString();
1308		final boolean empty = text.length() == 0;
1309		final boolean conference = c.getMode() == Conversation.MODE_MULTI;
1310		if (c.getCorrectingMessage() != null && (empty || text.equals(c.getCorrectingMessage().getBody()))) {
1311			action = SendButtonAction.CANCEL;
1312		} else if (conference && !c.getAccount().httpUploadAvailable()) {
1313			if (empty && c.getNextCounterpart() != null) {
1314				action = SendButtonAction.CANCEL;
1315			} else {
1316				action = SendButtonAction.TEXT;
1317			}
1318		} else {
1319			if (empty) {
1320				if (conference && c.getNextCounterpart() != null) {
1321					action = SendButtonAction.CANCEL;
1322				} else {
1323					String setting = activity.getPreferences().getString("quick_action", activity.getResources().getString(R.string.quick_action));
1324					if (!setting.equals("none") && UIHelper.receivedLocationQuestion(conversation.getLatestMessage())) {
1325						action = SendButtonAction.SEND_LOCATION;
1326					} else {
1327						if (setting.equals("recent")) {
1328							setting = activity.getPreferences().getString(ConversationActivity.RECENTLY_USED_QUICK_ACTION, SendButtonAction.TEXT.toString());
1329							action = SendButtonAction.valueOfOrDefault(setting,SendButtonAction.TEXT);
1330						} else {
1331							action = SendButtonAction.valueOfOrDefault(setting,SendButtonAction.TEXT);
1332						}
1333					}
1334				}
1335			} else {
1336				action = SendButtonAction.TEXT;
1337			}
1338		}
1339		if (activity.useSendButtonToIndicateStatus() && c.getAccount().getStatus() == Account.State.ONLINE) {
1340			if (activity.xmppConnectionService != null && activity.xmppConnectionService.getMessageArchiveService().isCatchingUp(c)) {
1341				status = Presence.Status.OFFLINE;
1342			} else if (c.getMode() == Conversation.MODE_SINGLE) {
1343				status = c.getContact().getShownStatus();
1344			} else {
1345				status = c.getMucOptions().online() ? Presence.Status.ONLINE : Presence.Status.OFFLINE;
1346			}
1347		} else {
1348			status = Presence.Status.OFFLINE;
1349		}
1350		this.mSendButton.setTag(action);
1351		this.mSendButton.setImageResource(getSendButtonImageResource(action, status));
1352	}
1353
1354	protected void updateDateSeparators() {
1355		synchronized (this.messageList) {
1356			for(int i = 0; i < this.messageList.size(); ++i) {
1357				final Message current = this.messageList.get(i);
1358				if (i == 0 || !UIHelper.sameDay(this.messageList.get(i-1).getTimeSent(),current.getTimeSent())) {
1359					this.messageList.add(i,Message.createDateSeparator(current));
1360					i++;
1361				}
1362			}
1363		}
1364	}
1365
1366	protected void updateStatusMessages() {
1367		updateDateSeparators();
1368		synchronized (this.messageList) {
1369			if (showLoadMoreMessages(conversation)) {
1370				this.messageList.add(0, Message.createLoadMoreMessage(conversation));
1371			}
1372			if (conversation.getMode() == Conversation.MODE_SINGLE) {
1373				ChatState state = conversation.getIncomingChatState();
1374				if (state == ChatState.COMPOSING) {
1375					this.messageList.add(Message.createStatusMessage(conversation, getString(R.string.contact_is_typing, conversation.getName())));
1376				} else if (state == ChatState.PAUSED) {
1377					this.messageList.add(Message.createStatusMessage(conversation, getString(R.string.contact_has_stopped_typing, conversation.getName())));
1378				} else {
1379					for (int i = this.messageList.size() - 1; i >= 0; --i) {
1380						if (this.messageList.get(i).getStatus() == Message.STATUS_RECEIVED) {
1381							return;
1382						} else {
1383							if (this.messageList.get(i).getStatus() == Message.STATUS_SEND_DISPLAYED) {
1384								this.messageList.add(i + 1,
1385										Message.createStatusMessage(conversation, getString(R.string.contact_has_read_up_to_this_point, conversation.getName())));
1386								return;
1387							}
1388						}
1389					}
1390				}
1391			} else {
1392				ChatState state = ChatState.COMPOSING;
1393				List<MucOptions.User> users = conversation.getMucOptions().getUsersWithChatState(state,5);
1394				if (users.size() == 0) {
1395					state = ChatState.PAUSED;
1396					users = conversation.getMucOptions().getUsersWithChatState(state, 5);
1397
1398				}
1399				if (users.size() > 0) {
1400					Message statusMessage;
1401					if (users.size() == 1) {
1402						MucOptions.User user = users.get(0);
1403						int id = state == ChatState.COMPOSING ? R.string.contact_is_typing : R.string.contact_has_stopped_typing;
1404						statusMessage = Message.createStatusMessage(conversation, getString(id, UIHelper.getDisplayName(user)));
1405						statusMessage.setTrueCounterpart(user.getRealJid());
1406						statusMessage.setCounterpart(user.getFullJid());
1407					} else {
1408						StringBuilder builder = new StringBuilder();
1409						for(MucOptions.User user : users) {
1410							if (builder.length() != 0) {
1411								builder.append(", ");
1412							}
1413							builder.append(UIHelper.getDisplayName(user));
1414						}
1415						int id = state == ChatState.COMPOSING ? R.string.contacts_are_typing : R.string.contacts_have_stopped_typing;
1416						statusMessage = Message.createStatusMessage(conversation, getString(id, builder.toString()));
1417					}
1418					this.messageList.add(statusMessage);
1419				}
1420
1421			}
1422		}
1423	}
1424
1425	private boolean showLoadMoreMessages(final Conversation c) {
1426		final boolean mam = hasMamSupport(c);
1427		final MessageArchiveService service = activity.xmppConnectionService.getMessageArchiveService();
1428		return mam && (c.getLastClearHistory().getTimestamp() != 0  || (c.countMessages() == 0 && c.messagesLoaded.get() && c.hasMessagesLeftOnServer()  && !service.queryInProgress(c)));
1429	}
1430
1431	private boolean hasMamSupport(final Conversation c) {
1432		if (c.getMode() == Conversation.MODE_SINGLE) {
1433			final XmppConnection connection = c.getAccount().getXmppConnection();
1434			return connection != null && connection.getFeatures().mam();
1435		} else {
1436			return c.getMucOptions().mamSupport();
1437		}
1438	}
1439
1440	protected void showSnackbar(final int message, final int action, final OnClickListener clickListener) {
1441		showSnackbar(message,action,clickListener,null);
1442	}
1443
1444	protected void showSnackbar(final int message, final int action, final OnClickListener clickListener, final View.OnLongClickListener longClickListener) {
1445		snackbar.setVisibility(View.VISIBLE);
1446		snackbar.setOnClickListener(null);
1447		snackbarMessage.setText(message);
1448		snackbarMessage.setOnClickListener(null);
1449		snackbarAction.setVisibility(clickListener == null ? View.GONE : View.VISIBLE);
1450		if (action != 0) {
1451			snackbarAction.setText(action);
1452		}
1453		snackbarAction.setOnClickListener(clickListener);
1454		snackbarAction.setOnLongClickListener(longClickListener);
1455	}
1456
1457	protected void hideSnackbar() {
1458		snackbar.setVisibility(View.GONE);
1459	}
1460
1461	protected void sendPlainTextMessage(Message message) {
1462		ConversationActivity activity = (ConversationActivity) getActivity();
1463		activity.xmppConnectionService.sendMessage(message);
1464		messageSent();
1465	}
1466
1467	private AtomicBoolean mSendingPgpMessage = new AtomicBoolean(false);
1468
1469	protected void sendPgpMessage(final Message message) {
1470		final ConversationActivity activity = (ConversationActivity) getActivity();
1471		final XmppConnectionService xmppService = activity.xmppConnectionService;
1472		final Contact contact = message.getConversation().getContact();
1473		if (!activity.hasPgp()) {
1474			activity.showInstallPgpDialog();
1475			return;
1476		}
1477		if (conversation.getAccount().getPgpSignature() == null) {
1478			activity.announcePgp(conversation.getAccount(), conversation, activity.onOpenPGPKeyPublished);
1479			return;
1480		}
1481		if (!mSendingPgpMessage.compareAndSet(false,true)) {
1482			Log.d(Config.LOGTAG,"sending pgp message already in progress");
1483		}
1484		if (conversation.getMode() == Conversation.MODE_SINGLE) {
1485			if (contact.getPgpKeyId() != 0) {
1486				xmppService.getPgpEngine().hasKey(contact,
1487						new UiCallback<Contact>() {
1488
1489							@Override
1490							public void userInputRequried(PendingIntent pi,
1491														  Contact contact) {
1492								activity.runIntent(
1493										pi,
1494										ConversationActivity.REQUEST_ENCRYPT_MESSAGE);
1495							}
1496
1497							@Override
1498							public void success(Contact contact) {
1499								activity.encryptTextMessage(message);
1500							}
1501
1502							@Override
1503							public void error(int error, Contact contact) {
1504								activity.runOnUiThread(new Runnable() {
1505									@Override
1506									public void run() {
1507										Toast.makeText(activity,
1508												R.string.unable_to_connect_to_keychain,
1509												Toast.LENGTH_SHORT
1510										).show();
1511									}
1512								});
1513								mSendingPgpMessage.set(false);
1514							}
1515						});
1516
1517			} else {
1518				showNoPGPKeyDialog(false,
1519						new DialogInterface.OnClickListener() {
1520
1521							@Override
1522							public void onClick(DialogInterface dialog,
1523												int which) {
1524								conversation
1525										.setNextEncryption(Message.ENCRYPTION_NONE);
1526								xmppService.updateConversation(conversation);
1527								message.setEncryption(Message.ENCRYPTION_NONE);
1528								xmppService.sendMessage(message);
1529								messageSent();
1530							}
1531						});
1532			}
1533		} else {
1534			if (conversation.getMucOptions().pgpKeysInUse()) {
1535				if (!conversation.getMucOptions().everybodyHasKeys()) {
1536					Toast warning = Toast
1537							.makeText(getActivity(),
1538									R.string.missing_public_keys,
1539									Toast.LENGTH_LONG);
1540					warning.setGravity(Gravity.CENTER_VERTICAL, 0, 0);
1541					warning.show();
1542				}
1543				activity.encryptTextMessage(message);
1544			} else {
1545				showNoPGPKeyDialog(true,
1546						new DialogInterface.OnClickListener() {
1547
1548							@Override
1549							public void onClick(DialogInterface dialog,
1550												int which) {
1551								conversation
1552										.setNextEncryption(Message.ENCRYPTION_NONE);
1553								message.setEncryption(Message.ENCRYPTION_NONE);
1554								xmppService.updateConversation(conversation);
1555								xmppService.sendMessage(message);
1556								messageSent();
1557							}
1558						});
1559			}
1560		}
1561	}
1562
1563	public void showNoPGPKeyDialog(boolean plural,
1564								   DialogInterface.OnClickListener listener) {
1565		AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
1566		builder.setIconAttribute(android.R.attr.alertDialogIcon);
1567		if (plural) {
1568			builder.setTitle(getString(R.string.no_pgp_keys));
1569			builder.setMessage(getText(R.string.contacts_have_no_pgp_keys));
1570		} else {
1571			builder.setTitle(getString(R.string.no_pgp_key));
1572			builder.setMessage(getText(R.string.contact_has_no_pgp_key));
1573		}
1574		builder.setNegativeButton(getString(R.string.cancel), null);
1575		builder.setPositiveButton(getString(R.string.send_unencrypted),
1576				listener);
1577		builder.create().show();
1578	}
1579
1580	protected void sendAxolotlMessage(final Message message) {
1581		final ConversationActivity activity = (ConversationActivity) getActivity();
1582		final XmppConnectionService xmppService = activity.xmppConnectionService;
1583		xmppService.sendMessage(message);
1584		messageSent();
1585	}
1586
1587	protected void sendOtrMessage(final Message message) {
1588		final ConversationActivity activity = (ConversationActivity) getActivity();
1589		final XmppConnectionService xmppService = activity.xmppConnectionService;
1590		activity.selectPresence(message.getConversation(),
1591				new OnPresenceSelected() {
1592
1593					@Override
1594					public void onPresenceSelected() {
1595						message.setCounterpart(conversation.getNextCounterpart());
1596						xmppService.sendMessage(message);
1597						messageSent();
1598					}
1599				});
1600	}
1601
1602	public void appendText(String text) {
1603		if (text == null) {
1604			return;
1605		}
1606		String previous = this.mEditMessage.getText().toString();
1607		if (previous.length() != 0 && !previous.endsWith(" ")) {
1608			text = " " + text;
1609		}
1610		this.mEditMessage.append(text);
1611	}
1612
1613	@Override
1614	public boolean onEnterPressed() {
1615		if (activity.enterIsSend()) {
1616			sendMessage();
1617			return true;
1618		} else {
1619			return false;
1620		}
1621	}
1622
1623	@Override
1624	public void onTypingStarted() {
1625		Account.State status = conversation.getAccount().getStatus();
1626		if (status == Account.State.ONLINE && conversation.setOutgoingChatState(ChatState.COMPOSING)) {
1627			activity.xmppConnectionService.sendChatState(conversation);
1628		}
1629		activity.hideConversationsOverview();
1630		updateSendButton();
1631	}
1632
1633	@Override
1634	public void onTypingStopped() {
1635		Account.State status = conversation.getAccount().getStatus();
1636		if (status == Account.State.ONLINE && conversation.setOutgoingChatState(ChatState.PAUSED)) {
1637			activity.xmppConnectionService.sendChatState(conversation);
1638		}
1639	}
1640
1641	@Override
1642	public void onTextDeleted() {
1643		Account.State status = conversation.getAccount().getStatus();
1644		if (status == Account.State.ONLINE && conversation.setOutgoingChatState(Config.DEFAULT_CHATSTATE)) {
1645			activity.xmppConnectionService.sendChatState(conversation);
1646		}
1647		updateSendButton();
1648	}
1649
1650	@Override
1651	public void onTextChanged() {
1652		if (conversation != null && conversation.getCorrectingMessage() != null) {
1653			updateSendButton();
1654		}
1655	}
1656
1657	private int completionIndex = 0;
1658	private int lastCompletionLength = 0;
1659	private String incomplete;
1660	private int lastCompletionCursor;
1661	private boolean firstWord = false;
1662
1663	@Override
1664	public boolean onTabPressed(boolean repeated) {
1665		if (conversation == null || conversation.getMode() == Conversation.MODE_SINGLE) {
1666			return false;
1667		}
1668		if (repeated) {
1669			completionIndex++;
1670		} else {
1671			lastCompletionLength = 0;
1672			completionIndex = 0;
1673			final String content = mEditMessage.getText().toString();
1674			lastCompletionCursor = mEditMessage.getSelectionEnd();
1675			int start = lastCompletionCursor > 0 ? content.lastIndexOf(" ",lastCompletionCursor-1) + 1 : 0;
1676			firstWord = start == 0;
1677			incomplete = content.substring(start,lastCompletionCursor);
1678		}
1679		List<String> completions = new ArrayList<>();
1680		for(MucOptions.User user : conversation.getMucOptions().getUsers()) {
1681			String name = user.getName();
1682			if (name != null && name.startsWith(incomplete)) {
1683				completions.add(name+(firstWord ? ": " : " "));
1684			}
1685		}
1686		Collections.sort(completions);
1687		if (completions.size() > completionIndex) {
1688			String completion = completions.get(completionIndex).substring(incomplete.length());
1689			mEditMessage.getEditableText().delete(lastCompletionCursor,lastCompletionCursor + lastCompletionLength);
1690			mEditMessage.getEditableText().insert(lastCompletionCursor, completion);
1691			lastCompletionLength = completion.length();
1692		} else {
1693			completionIndex = -1;
1694			mEditMessage.getEditableText().delete(lastCompletionCursor,lastCompletionCursor + lastCompletionLength);
1695			lastCompletionLength = 0;
1696		}
1697		return true;
1698	}
1699
1700	@Override
1701	public void onActivityResult(int requestCode, int resultCode,
1702	                                final Intent data) {
1703		if (resultCode == Activity.RESULT_OK) {
1704			if (requestCode == ConversationActivity.REQUEST_DECRYPT_PGP) {
1705				activity.getSelectedConversation().getAccount().getPgpDecryptionService().continueDecryption(true);
1706			} else if (requestCode == ConversationActivity.REQUEST_TRUST_KEYS_TEXT) {
1707				final String body = mEditMessage.getText().toString();
1708				Message message = new Message(conversation, body, conversation.getNextEncryption());
1709				sendAxolotlMessage(message);
1710			} else if (requestCode == ConversationActivity.REQUEST_TRUST_KEYS_MENU) {
1711				int choice = data.getIntExtra("choice", ConversationActivity.ATTACHMENT_CHOICE_INVALID);
1712				activity.selectPresenceToAttachFile(choice, conversation.getNextEncryption());
1713			}
1714		} else if (resultCode == Activity.RESULT_CANCELED) {
1715			if (requestCode == ConversationActivity.REQUEST_DECRYPT_PGP) {
1716				// discard the message to prevent decryption being blocked
1717				conversation.getAccount().getPgpDecryptionService().giveUpCurrentDecryption();
1718			}
1719		}
1720	}
1721
1722}