ConversationFragment.java

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