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