ConversationFragment.java

   1package eu.siacs.conversations.ui;
   2
   3import android.annotation.SuppressLint;
   4import android.app.Activity;
   5import android.content.pm.PackageManager;
   6import android.databinding.DataBindingUtil;
   7import android.net.Uri;
   8import android.os.Build;
   9import android.provider.MediaStore;
  10import android.support.v7.app.AlertDialog;
  11import android.app.Fragment;
  12import android.app.PendingIntent;
  13import android.content.ActivityNotFoundException;
  14import android.content.Context;
  15import android.content.DialogInterface;
  16import android.content.Intent;
  17import android.content.IntentSender.SendIntentException;
  18import android.os.Bundle;
  19import android.os.Handler;
  20import android.os.SystemClock;
  21import android.support.v13.view.inputmethod.InputConnectionCompat;
  22import android.support.v13.view.inputmethod.InputContentInfoCompat;
  23import android.text.Editable;
  24import android.text.InputType;
  25import android.util.Log;
  26import android.util.Pair;
  27import android.view.ContextMenu;
  28import android.view.ContextMenu.ContextMenuInfo;
  29import android.view.Gravity;
  30import android.view.LayoutInflater;
  31import android.view.Menu;
  32import android.view.MenuInflater;
  33import android.view.MenuItem;
  34import android.view.MotionEvent;
  35import android.view.View;
  36import android.view.View.OnClickListener;
  37import android.view.ViewGroup;
  38import android.view.inputmethod.EditorInfo;
  39import android.view.inputmethod.InputMethodManager;
  40import android.widget.AbsListView;
  41import android.widget.AbsListView.OnScrollListener;
  42import android.widget.AdapterView;
  43import android.widget.AdapterView.AdapterContextMenuInfo;
  44import android.widget.CheckBox;
  45import android.widget.ImageButton;
  46import android.widget.ListView;
  47import android.widget.PopupMenu;
  48import android.widget.RelativeLayout;
  49import android.widget.TextView;
  50import android.widget.TextView.OnEditorActionListener;
  51import android.widget.Toast;
  52
  53import org.openintents.openpgp.util.OpenPgpApi;
  54
  55import java.util.ArrayList;
  56import java.util.Arrays;
  57import java.util.Collections;
  58import java.util.HashSet;
  59import java.util.Iterator;
  60import java.util.List;
  61import java.util.Map;
  62import java.util.Set;
  63import java.util.UUID;
  64import java.util.concurrent.atomic.AtomicBoolean;
  65import java.util.concurrent.atomic.AtomicInteger;
  66
  67import eu.siacs.conversations.Config;
  68import eu.siacs.conversations.R;
  69import eu.siacs.conversations.crypto.axolotl.AxolotlService;
  70import eu.siacs.conversations.crypto.axolotl.FingerprintStatus;
  71import eu.siacs.conversations.databinding.FragmentConversationBinding;
  72import eu.siacs.conversations.entities.Account;
  73import eu.siacs.conversations.entities.Blockable;
  74import eu.siacs.conversations.entities.Contact;
  75import eu.siacs.conversations.entities.Conversation;
  76import eu.siacs.conversations.entities.DownloadableFile;
  77import eu.siacs.conversations.entities.Message;
  78import eu.siacs.conversations.entities.MucOptions;
  79import eu.siacs.conversations.entities.Presence;
  80import eu.siacs.conversations.entities.Presences;
  81import eu.siacs.conversations.entities.ReadByMarker;
  82import eu.siacs.conversations.entities.Transferable;
  83import eu.siacs.conversations.entities.TransferablePlaceholder;
  84import eu.siacs.conversations.http.HttpDownloadConnection;
  85import eu.siacs.conversations.persistance.FileBackend;
  86import eu.siacs.conversations.services.MessageArchiveService;
  87import eu.siacs.conversations.services.XmppConnectionService;
  88import eu.siacs.conversations.ui.adapter.MessageAdapter;
  89import eu.siacs.conversations.ui.util.ActivityResult;
  90import eu.siacs.conversations.ui.util.AttachmentTool;
  91import eu.siacs.conversations.ui.util.ConversationMenuConfigurator;
  92import eu.siacs.conversations.ui.util.PresenceSelector;
  93import eu.siacs.conversations.ui.util.SendButtonAction;
  94import eu.siacs.conversations.ui.util.SendButtonTool;
  95import eu.siacs.conversations.ui.widget.EditMessage;
  96import eu.siacs.conversations.utils.MessageUtils;
  97import eu.siacs.conversations.utils.NickValidityChecker;
  98import eu.siacs.conversations.utils.StylingHelper;
  99import eu.siacs.conversations.utils.UIHelper;
 100import eu.siacs.conversations.xmpp.XmppConnection;
 101import eu.siacs.conversations.xmpp.chatstate.ChatState;
 102import eu.siacs.conversations.xmpp.jid.InvalidJidException;
 103import eu.siacs.conversations.xmpp.jid.Jid;
 104
 105import static eu.siacs.conversations.ui.XmppActivity.EXTRA_ACCOUNT;
 106import static eu.siacs.conversations.ui.XmppActivity.REQUEST_ANNOUNCE_PGP;
 107import static eu.siacs.conversations.ui.XmppActivity.REQUEST_CHOOSE_PGP_ID;
 108
 109
 110public class ConversationFragment extends Fragment implements EditMessage.KeyboardListener {
 111
 112
 113	public static final int REQUEST_SEND_MESSAGE = 0x0201;
 114	public static final int REQUEST_DECRYPT_PGP = 0x0202;
 115	public static final int REQUEST_ENCRYPT_MESSAGE = 0x0207;
 116	public static final int REQUEST_TRUST_KEYS_TEXT = 0x0208;
 117	public static final int REQUEST_TRUST_KEYS_MENU = 0x0209;
 118	public static final int REQUEST_START_DOWNLOAD = 0x0210;
 119	public static final int REQUEST_ADD_EDITOR_CONTENT = 0x0211;
 120	public static final int ATTACHMENT_CHOICE_CHOOSE_IMAGE = 0x0301;
 121	public static final int ATTACHMENT_CHOICE_TAKE_PHOTO = 0x0302;
 122	public static final int ATTACHMENT_CHOICE_CHOOSE_FILE = 0x0303;
 123	public static final int ATTACHMENT_CHOICE_RECORD_VOICE = 0x0304;
 124	public static final int ATTACHMENT_CHOICE_LOCATION = 0x0305;
 125	public static final int ATTACHMENT_CHOICE_INVALID = 0x0306;
 126	public static final int ATTACHMENT_CHOICE_RECORD_VIDEO = 0x0307;
 127
 128	public static final String RECENTLY_USED_QUICK_ACTION = "recently_used_quick_action";
 129
 130
 131	final protected List<Message> messageList = new ArrayList<>();
 132	protected Conversation conversation;
 133
 134	private FragmentConversationBinding binding;
 135
 136	protected MessageAdapter messageListAdapter;
 137	private Toast messageLoaderToast;
 138
 139	private ActivityResult postponedActivityResult = null;
 140	public Uri mPendingEditorContent = null;
 141
 142	private ConversationActivity activity;
 143
 144	private OnClickListener clickToMuc = new OnClickListener() {
 145
 146		@Override
 147		public void onClick(View v) {
 148			Intent intent = new Intent(getActivity(), ConferenceDetailsActivity.class);
 149			intent.setAction(ConferenceDetailsActivity.ACTION_VIEW_MUC);
 150			intent.putExtra("uuid", conversation.getUuid());
 151			startActivity(intent);
 152		}
 153	};
 154	private OnClickListener leaveMuc = new OnClickListener() {
 155
 156		@Override
 157		public void onClick(View v) {
 158			activity.endConversation(conversation);
 159		}
 160	};
 161	private OnClickListener joinMuc = new OnClickListener() {
 162
 163		@Override
 164		public void onClick(View v) {
 165			activity.xmppConnectionService.joinMuc(conversation);
 166		}
 167	};
 168	private OnClickListener enterPassword = new OnClickListener() {
 169
 170		@Override
 171		public void onClick(View v) {
 172			MucOptions muc = conversation.getMucOptions();
 173			String password = muc.getPassword();
 174			if (password == null) {
 175				password = "";
 176			}
 177			activity.quickPasswordEdit(password, value -> {
 178				activity.xmppConnectionService.providePasswordForMuc(conversation, value);
 179				return null;
 180			});
 181		}
 182	};
 183	private OnScrollListener mOnScrollListener = new OnScrollListener() {
 184
 185		@Override
 186		public void onScrollStateChanged(AbsListView view, int scrollState) {
 187			// TODO Auto-generated method stub
 188
 189		}
 190
 191		@Override
 192		public void onScroll(final AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
 193			synchronized (ConversationFragment.this.messageList) {
 194				if (firstVisibleItem < 5 && conversation != null && conversation.messagesLoaded.compareAndSet(true, false) && messageList.size() > 0) {
 195					long timestamp;
 196					if (messageList.get(0).getType() == Message.TYPE_STATUS && messageList.size() >= 2) {
 197						timestamp = messageList.get(1).getTimeSent();
 198					} else {
 199						timestamp = messageList.get(0).getTimeSent();
 200					}
 201					activity.xmppConnectionService.loadMoreMessages(conversation, timestamp, new XmppConnectionService.OnMoreMessagesLoaded() {
 202						@Override
 203						public void onMoreMessagesLoaded(final int c, final Conversation conversation) {
 204							if (ConversationFragment.this.conversation != conversation) {
 205								conversation.messagesLoaded.set(true);
 206								return;
 207							}
 208							getActivity().runOnUiThread(() -> {
 209								final int oldPosition = binding.messagesView.getFirstVisiblePosition();
 210								Message message = null;
 211								int childPos;
 212								for (childPos = 0; childPos + oldPosition < messageList.size(); ++childPos) {
 213									message = messageList.get(oldPosition + childPos);
 214									if (message.getType() != Message.TYPE_STATUS) {
 215										break;
 216									}
 217								}
 218								final String uuid = message != null ? message.getUuid() : null;
 219								View v = binding.messagesView.getChildAt(childPos);
 220								final int pxOffset = (v == null) ? 0 : v.getTop();
 221								ConversationFragment.this.conversation.populateWithMessages(ConversationFragment.this.messageList);
 222								try {
 223									updateStatusMessages();
 224								} catch (IllegalStateException e) {
 225									Log.d(Config.LOGTAG, "caught illegal state exception while updating status messages");
 226								}
 227								messageListAdapter.notifyDataSetChanged();
 228								int pos = Math.max(getIndexOf(uuid, messageList), 0);
 229								binding.messagesView.setSelectionFromTop(pos, pxOffset);
 230								if (messageLoaderToast != null) {
 231									messageLoaderToast.cancel();
 232								}
 233								conversation.messagesLoaded.set(true);
 234							});
 235						}
 236
 237						@Override
 238						public void informUser(final int resId) {
 239
 240							getActivity().runOnUiThread(() -> {
 241								if (messageLoaderToast != null) {
 242									messageLoaderToast.cancel();
 243								}
 244								if (ConversationFragment.this.conversation != conversation) {
 245									return;
 246								}
 247								messageLoaderToast = Toast.makeText(view.getContext(), resId, Toast.LENGTH_LONG);
 248								messageLoaderToast.show();
 249							});
 250
 251						}
 252					});
 253
 254				}
 255			}
 256		}
 257	};
 258
 259	private EditMessage.OnCommitContentListener mEditorContentListener = new EditMessage.OnCommitContentListener() {
 260		@Override
 261		public boolean onCommitContent(InputContentInfoCompat inputContentInfo, int flags, Bundle opts, String[] contentMimeTypes) {
 262			// try to get permission to read the image, if applicable
 263			if ((flags & InputConnectionCompat.INPUT_CONTENT_GRANT_READ_URI_PERMISSION) != 0) {
 264				try {
 265					inputContentInfo.requestPermission();
 266				} catch (Exception e) {
 267					Log.e(Config.LOGTAG, "InputContentInfoCompat#requestPermission() failed.", e);
 268					Toast.makeText(getActivity(),activity.getString(R.string.no_permission_to_access_x, inputContentInfo.getDescription()), Toast.LENGTH_LONG
 269					).show();
 270					return false;
 271				}
 272			}
 273			if (activity.hasStoragePermission(REQUEST_ADD_EDITOR_CONTENT)) {
 274				attachImageToConversation(inputContentInfo.getContentUri());
 275			} else {
 276				mPendingEditorContent = inputContentInfo.getContentUri();
 277			}
 278			return true;
 279		}
 280	};
 281	private Message selectedMessage;
 282	private OnClickListener mEnableAccountListener = new OnClickListener() {
 283		@Override
 284		public void onClick(View v) {
 285			final Account account = conversation == null ? null : conversation.getAccount();
 286			if (account != null) {
 287				account.setOption(Account.OPTION_DISABLED, false);
 288				activity.xmppConnectionService.updateAccount(account);
 289			}
 290		}
 291	};
 292	private OnClickListener mUnblockClickListener = new OnClickListener() {
 293		@Override
 294		public void onClick(final View v) {
 295			v.post(() -> v.setVisibility(View.INVISIBLE));
 296			if (conversation.isDomainBlocked()) {
 297				BlockContactDialog.show(activity, conversation);
 298			} else {
 299				unblockConversation(conversation);
 300			}
 301		}
 302	};
 303	private OnClickListener mBlockClickListener = this::showBlockSubmenu;
 304	private OnClickListener mAddBackClickListener = new OnClickListener() {
 305
 306		@Override
 307		public void onClick(View v) {
 308			final Contact contact = conversation == null ? null : conversation.getContact();
 309			if (contact != null) {
 310				activity.xmppConnectionService.createContact(contact);
 311				activity.switchToContactDetails(contact);
 312			}
 313		}
 314	};
 315	private View.OnLongClickListener mLongPressBlockListener = this::showBlockSubmenu;
 316	private OnClickListener mAllowPresenceSubscription = new OnClickListener() {
 317		@Override
 318		public void onClick(View v) {
 319			final Contact contact = conversation == null ? null : conversation.getContact();
 320			if (contact != null) {
 321				activity.xmppConnectionService.sendPresencePacket(contact.getAccount(),
 322						activity.xmppConnectionService.getPresenceGenerator()
 323								.sendPresenceUpdatesTo(contact));
 324				hideSnackbar();
 325			}
 326		}
 327	};
 328
 329	protected OnClickListener clickToDecryptListener = new OnClickListener() {
 330
 331		@Override
 332		public void onClick(View v) {
 333			PendingIntent pendingIntent = conversation.getAccount().getPgpDecryptionService().getPendingIntent();
 334			if (pendingIntent != null) {
 335				try {
 336					getActivity().startIntentSenderForResult(pendingIntent.getIntentSender(),
 337							REQUEST_DECRYPT_PGP,
 338							null,
 339							0,
 340							0,
 341							0);
 342				} catch (SendIntentException e) {
 343					Toast.makeText(getActivity(), R.string.unable_to_connect_to_keychain, Toast.LENGTH_SHORT).show();
 344					conversation.getAccount().getPgpDecryptionService().continueDecryption(true);
 345				}
 346			}
 347			updateSnackBar(conversation);
 348		}
 349	};
 350	private AtomicBoolean mSendingPgpMessage = new AtomicBoolean(false);
 351	private OnEditorActionListener mEditorActionListener = (v, actionId, event) -> {
 352		if (actionId == EditorInfo.IME_ACTION_SEND) {
 353			InputMethodManager imm = (InputMethodManager) v.getContext()
 354					.getSystemService(Context.INPUT_METHOD_SERVICE);
 355			if (imm.isFullscreenMode()) {
 356				imm.hideSoftInputFromWindow(v.getWindowToken(), 0);
 357			}
 358			sendMessage();
 359			return true;
 360		} else {
 361			return false;
 362		}
 363	};
 364	private OnClickListener mSendButtonListener = new OnClickListener() {
 365
 366		@Override
 367		public void onClick(View v) {
 368			Object tag = v.getTag();
 369			if (tag instanceof SendButtonAction) {
 370				SendButtonAction action = (SendButtonAction) tag;
 371				switch (action) {
 372					case TAKE_PHOTO:
 373					case RECORD_VIDEO:
 374					case SEND_LOCATION:
 375					case RECORD_VOICE:
 376					case CHOOSE_PICTURE:
 377						attachFile(action.toChoice());
 378						break;
 379					case CANCEL:
 380						if (conversation != null) {
 381							if (conversation.setCorrectingMessage(null)) {
 382								binding.textinput.setText("");
 383								binding.textinput.append(conversation.getDraftMessage());
 384								conversation.setDraftMessage(null);
 385							} else if (conversation.getMode() == Conversation.MODE_MULTI) {
 386								conversation.setNextCounterpart(null);
 387							}
 388							updateChatMsgHint();
 389							updateSendButton();
 390							updateEditablity();
 391						}
 392						break;
 393					default:
 394						sendMessage();
 395				}
 396			} else {
 397				sendMessage();
 398			}
 399		}
 400	};
 401	private int completionIndex = 0;
 402	private int lastCompletionLength = 0;
 403	private String incomplete;
 404	private int lastCompletionCursor;
 405	private boolean firstWord = false;
 406	private Message mPendingDownloadableMessage;
 407
 408	private int getIndexOf(String uuid, List<Message> messages) {
 409		if (uuid == null) {
 410			return messages.size() - 1;
 411		}
 412		for (int i = 0; i < messages.size(); ++i) {
 413			if (uuid.equals(messages.get(i).getUuid())) {
 414				return i;
 415			} else {
 416				Message next = messages.get(i);
 417				while (next != null && next.wasMergedIntoPrevious()) {
 418					if (uuid.equals(next.getUuid())) {
 419						return i;
 420					}
 421					next = next.next();
 422				}
 423
 424			}
 425		}
 426		return -1;
 427	}
 428
 429	public Pair<Integer, Integer> getScrollPosition() {
 430		if (this.binding.messagesView.getCount() == 0 ||
 431				this.binding.messagesView.getLastVisiblePosition() == this.binding.messagesView.getCount() - 1) {
 432			return null;
 433		} else {
 434			final int pos = this.binding.messagesView.getFirstVisiblePosition();
 435			final View view = this.binding.messagesView.getChildAt(0);
 436			if (view == null) {
 437				return null;
 438			} else {
 439				return new Pair<>(pos, view.getTop());
 440			}
 441		}
 442	}
 443
 444	public void setScrollPosition(Pair<Integer, Integer> scrollPosition) {
 445		if (scrollPosition != null) {
 446			this.binding.messagesView.setSelectionFromTop(scrollPosition.first, scrollPosition.second);
 447		}
 448	}
 449
 450
 451	private void attachLocationToConversation(Conversation conversation, Uri uri) {
 452		if (conversation == null) {
 453			return;
 454		}
 455		activity.xmppConnectionService.attachLocationToConversation(conversation, uri, new UiCallback<Message>() {
 456
 457			@Override
 458			public void success(Message message) {
 459				activity.xmppConnectionService.sendMessage(message);
 460			}
 461
 462			@Override
 463			public void error(int errorCode, Message object) {
 464
 465			}
 466
 467			@Override
 468			public void userInputRequried(PendingIntent pi, Message object) {
 469
 470			}
 471		});
 472	}
 473
 474	private void attachFileToConversation(Conversation conversation, Uri uri) {
 475		if (conversation == null) {
 476			return;
 477		}
 478		final Toast prepareFileToast = Toast.makeText(getActivity(), getText(R.string.preparing_file), Toast.LENGTH_LONG);
 479		prepareFileToast.show();
 480		activity.delegateUriPermissionsToService(uri);
 481		activity.xmppConnectionService.attachFileToConversation(conversation, uri, new UiInformableCallback<Message>() {
 482			@Override
 483			public void inform(final String text) {
 484				hidePrepareFileToast(prepareFileToast);
 485				getActivity().runOnUiThread(() -> activity.replaceToast(text));
 486			}
 487
 488			@Override
 489			public void success(Message message) {
 490				getActivity().runOnUiThread(() -> activity.hideToast());
 491				hidePrepareFileToast(prepareFileToast);
 492				activity.xmppConnectionService.sendMessage(message);
 493			}
 494
 495			@Override
 496			public void error(final int errorCode, Message message) {
 497				hidePrepareFileToast(prepareFileToast);
 498				getActivity().runOnUiThread(() -> activity.replaceToast(getString(errorCode)));
 499
 500			}
 501
 502			@Override
 503			public void userInputRequried(PendingIntent pi, Message message) {
 504				hidePrepareFileToast(prepareFileToast);
 505			}
 506		});
 507	}
 508
 509	public void attachImageToConversation(Uri uri) {
 510		this.attachImageToConversation(conversation, uri);
 511	}
 512
 513	private void attachImageToConversation(Conversation conversation, Uri uri) {
 514		if (conversation == null) {
 515			return;
 516		}
 517		final Toast prepareFileToast = Toast.makeText(getActivity(), getText(R.string.preparing_image), Toast.LENGTH_LONG);
 518		prepareFileToast.show();
 519		activity.delegateUriPermissionsToService(uri);
 520		activity.xmppConnectionService.attachImageToConversation(conversation, uri,
 521				new UiCallback<Message>() {
 522
 523					@Override
 524					public void userInputRequried(PendingIntent pi, Message object) {
 525						hidePrepareFileToast(prepareFileToast);
 526					}
 527
 528					@Override
 529					public void success(Message message) {
 530						hidePrepareFileToast(prepareFileToast);
 531						activity.xmppConnectionService.sendMessage(message);
 532					}
 533
 534					@Override
 535					public void error(final int error, Message message) {
 536						hidePrepareFileToast(prepareFileToast);
 537						activity.runOnUiThread(() -> activity.replaceToast(getString(error)));
 538					}
 539				});
 540	}
 541
 542	private void hidePrepareFileToast(final Toast prepareFileToast) {
 543		if (prepareFileToast != null) {
 544			activity.runOnUiThread(prepareFileToast::cancel);
 545		}
 546	}
 547
 548	private void sendMessage() {
 549		final String body = this.binding.textinput.getText().toString();
 550		final Conversation conversation = this.conversation;
 551		if (body.length() == 0 || conversation == null) {
 552			return;
 553		}
 554		final Message message;
 555		if (conversation.getCorrectingMessage() == null) {
 556			message = new Message(conversation, body, conversation.getNextEncryption());
 557			if (conversation.getMode() == Conversation.MODE_MULTI) {
 558				final Jid nextCounterpart = conversation.getNextCounterpart();
 559				if (nextCounterpart != null) {
 560					message.setCounterpart(nextCounterpart);
 561					message.setTrueCounterpart(conversation.getMucOptions().getTrueCounterpart(nextCounterpart));
 562					message.setType(Message.TYPE_PRIVATE);
 563				}
 564			}
 565		} else {
 566			message = conversation.getCorrectingMessage();
 567			message.setBody(body);
 568			message.setEdited(message.getUuid());
 569			message.setUuid(UUID.randomUUID().toString());
 570		}
 571		switch (message.getConversation().getNextEncryption()) {
 572			case Message.ENCRYPTION_PGP:
 573				sendPgpMessage(message);
 574				break;
 575			case Message.ENCRYPTION_AXOLOTL:
 576				if (!trustKeysIfNeeded(REQUEST_TRUST_KEYS_TEXT)) {
 577					sendAxolotlMessage(message);
 578				}
 579				break;
 580			default:
 581				sendPlainTextMessage(message);
 582		}
 583	}
 584
 585	protected boolean trustKeysIfNeeded(int requestCode) {
 586		return trustKeysIfNeeded(requestCode, ATTACHMENT_CHOICE_INVALID);
 587	}
 588
 589	protected boolean trustKeysIfNeeded(int requestCode, int attachmentChoice) {
 590		AxolotlService axolotlService = conversation.getAccount().getAxolotlService();
 591		final List<Jid> targets = axolotlService.getCryptoTargets(conversation);
 592		boolean hasUnaccepted = !conversation.getAcceptedCryptoTargets().containsAll(targets);
 593		boolean hasUndecidedOwn = !axolotlService.getKeysWithTrust(FingerprintStatus.createActiveUndecided()).isEmpty();
 594		boolean hasUndecidedContacts = !axolotlService.getKeysWithTrust(FingerprintStatus.createActiveUndecided(), targets).isEmpty();
 595		boolean hasPendingKeys = !axolotlService.findDevicesWithoutSession(conversation).isEmpty();
 596		boolean hasNoTrustedKeys = axolotlService.anyTargetHasNoTrustedKeys(targets);
 597		if (hasUndecidedOwn || hasUndecidedContacts || hasPendingKeys || hasNoTrustedKeys || hasUnaccepted) {
 598			axolotlService.createSessionsIfNeeded(conversation);
 599			Intent intent = new Intent(getActivity(), TrustKeysActivity.class);
 600			String[] contacts = new String[targets.size()];
 601			for (int i = 0; i < contacts.length; ++i) {
 602				contacts[i] = targets.get(i).toString();
 603			}
 604			intent.putExtra("contacts", contacts);
 605			intent.putExtra(EXTRA_ACCOUNT, conversation.getAccount().getJid().toBareJid().toString());
 606			intent.putExtra("choice", attachmentChoice);
 607			intent.putExtra("conversation", conversation.getUuid());
 608			startActivityForResult(intent, requestCode);
 609			return true;
 610		} else {
 611			return false;
 612		}
 613	}
 614
 615	public void updateChatMsgHint() {
 616		final boolean multi = conversation.getMode() == Conversation.MODE_MULTI;
 617		if (conversation.getCorrectingMessage() != null) {
 618			this.binding.textinput.setHint(R.string.send_corrected_message);
 619		} else if (multi && conversation.getNextCounterpart() != null) {
 620			this.binding.textinput.setHint(getString(
 621					R.string.send_private_message_to,
 622					conversation.getNextCounterpart().getResourcepart()));
 623		} else if (multi && !conversation.getMucOptions().participating()) {
 624			this.binding.textinput.setHint(R.string.you_are_not_participating);
 625		} else {
 626			this.binding.textinput.setHint(UIHelper.getMessageHint(getActivity(), conversation));
 627			getActivity().invalidateOptionsMenu();
 628		}
 629	}
 630
 631	public void setupIme() {
 632		this.binding.textinput.refreshIme();
 633	}
 634
 635	private void handleActivityResult(ActivityResult activityResult) {
 636		if (activityResult.resultCode == Activity.RESULT_OK) {
 637			handlePositiveActivityResult(activityResult.requestCode, activityResult.data);
 638		} else {
 639			handleNegativeActivityResult(activityResult.requestCode);
 640		}
 641	}
 642
 643	private void handlePositiveActivityResult(int requestCode, final Intent data) {
 644		switch (requestCode) {
 645			case REQUEST_DECRYPT_PGP:
 646				conversation.getAccount().getPgpDecryptionService().continueDecryption(data);
 647				break;
 648			case REQUEST_TRUST_KEYS_TEXT:
 649				final String body = this.binding.textinput.getText().toString();
 650				Message message = new Message(conversation, body, conversation.getNextEncryption());
 651				sendAxolotlMessage(message);
 652				break;
 653			case REQUEST_TRUST_KEYS_MENU:
 654				int choice = data.getIntExtra("choice", ATTACHMENT_CHOICE_INVALID);
 655				selectPresenceToAttachFile(choice);
 656				break;
 657			case REQUEST_CHOOSE_PGP_ID:
 658				long id = data.getLongExtra(OpenPgpApi.EXTRA_SIGN_KEY_ID,0);
 659				if (id != 0) {
 660					conversation.getAccount().setPgpSignId(id);
 661					activity.announcePgp(conversation.getAccount(),null,null,activity.onOpenPGPKeyPublished);
 662				} else {
 663					activity.choosePgpSignId(conversation.getAccount());
 664				}
 665				break;
 666			case REQUEST_ANNOUNCE_PGP:
 667				activity.announcePgp(conversation.getAccount(), conversation, data, activity.onOpenPGPKeyPublished);
 668				break;
 669			case ATTACHMENT_CHOICE_CHOOSE_IMAGE:
 670				List<Uri> imageUris = AttachmentTool.extractUriFromIntent(data);
 671				for (Iterator<Uri> i = imageUris.iterator(); i.hasNext(); i.remove()) {
 672					Log.d(Config.LOGTAG, "ConversationsActivity.onActivityResult() - attaching image to conversations. CHOOSE_IMAGE");
 673					attachImageToConversation(conversation, i.next());
 674				}
 675				break;
 676			case ATTACHMENT_CHOICE_CHOOSE_FILE:
 677			case ATTACHMENT_CHOICE_RECORD_VIDEO:
 678			case ATTACHMENT_CHOICE_RECORD_VOICE:
 679				final List<Uri> fileUris = AttachmentTool.extractUriFromIntent(data);
 680				final PresenceSelector.OnPresenceSelected callback = () -> {
 681					for (Iterator<Uri> i = fileUris.iterator(); i.hasNext(); i.remove()) {
 682						Log.d(Config.LOGTAG, "ConversationsActivity.onActivityResult() - attaching file to conversations. CHOOSE_FILE/RECORD_VOICE/RECORD_VIDEO");
 683						attachFileToConversation(conversation, i.next());
 684					}
 685				};
 686				if (conversation == null || conversation.getMode() == Conversation.MODE_MULTI || FileBackend.allFilesUnderSize(getActivity(), fileUris, activity.getMaxHttpUploadSize(conversation))) {
 687					callback.onPresenceSelected();
 688				} else {
 689					activity.selectPresence(conversation, callback);
 690				}
 691				break;
 692			case ATTACHMENT_CHOICE_LOCATION:
 693				double latitude = data.getDoubleExtra("latitude", 0);
 694				double longitude = data.getDoubleExtra("longitude", 0);
 695				Uri geo = Uri.parse("geo:" + String.valueOf(latitude) + "," + String.valueOf(longitude));
 696				attachLocationToConversation(conversation, geo);
 697				break;
 698		}
 699	}
 700
 701	private void handleNegativeActivityResult(int requestCode) {
 702		switch (requestCode) {
 703			case REQUEST_DECRYPT_PGP:
 704				// discard the message to prevent decryption being blocked
 705				conversation.getAccount().getPgpDecryptionService().giveUpCurrentDecryption();
 706				break;
 707		}
 708	}
 709
 710	@Override
 711	public void onActivityResult(int requestCode, int resultCode, final Intent data) {
 712		super.onActivityResult(requestCode, resultCode, data);
 713		ActivityResult activityResult = ActivityResult.of(requestCode,resultCode,data);
 714		if (activity != null && activity.xmppConnectionService != null) {
 715			handleActivityResult(activityResult);
 716		} else {
 717			this.postponedActivityResult = activityResult;
 718		}
 719	}
 720
 721	public void unblockConversation(final Blockable conversation) {
 722		activity.xmppConnectionService.sendUnblockRequest(conversation);
 723	}
 724
 725	@Override
 726	public void onAttach(Context context) {
 727		if (context instanceof ConversationActivity) {
 728			this.activity = (ConversationActivity) context;
 729		} else {
 730			throw new IllegalStateException("Trying to attach fragment to activity that is not the ConversationActivity");
 731		}
 732		super.onAttach(context);
 733	}
 734
 735	@Override
 736	public void onCreate(Bundle savedInstanceState) {
 737		super.onCreate(savedInstanceState);
 738		setHasOptionsMenu(true);
 739	}
 740
 741
 742	@Override
 743	public void onCreateOptionsMenu(Menu menu, MenuInflater menuInflater) {
 744		menuInflater.inflate(R.menu.fragment_conversation, menu);
 745		final MenuItem menuMucDetails = menu.findItem(R.id.action_muc_details);
 746		final MenuItem menuContactDetails = menu.findItem(R.id.action_contact_details);
 747		final MenuItem menuInviteContact = menu.findItem(R.id.action_invite);
 748		final MenuItem menuMute = menu.findItem(R.id.action_mute);
 749		final MenuItem menuUnmute = menu.findItem(R.id.action_unmute);
 750
 751
 752		if (conversation != null) {
 753			if (conversation.getMode() == Conversation.MODE_MULTI) {
 754				menuContactDetails.setVisible(false);
 755				menuInviteContact.setVisible(conversation.getMucOptions().canInvite());
 756			} else {
 757				menuContactDetails.setVisible(!this.conversation.withSelf());
 758				menuMucDetails.setVisible(false);
 759				final XmppConnectionService service = activity.xmppConnectionService;
 760				menuInviteContact.setVisible(service != null && service.findConferenceServer(conversation.getAccount()) != null);
 761			}
 762			if (conversation.isMuted()) {
 763				menuMute.setVisible(false);
 764			} else {
 765				menuUnmute.setVisible(false);
 766			}
 767			ConversationMenuConfigurator.configureAttachmentMenu(conversation, menu);
 768			ConversationMenuConfigurator.configureEncryptionMenu(conversation, menu);
 769		}
 770		super.onCreateOptionsMenu(menu, menuInflater);
 771	}
 772
 773	@Override
 774	public View onCreateView(final LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
 775		this.binding = DataBindingUtil.inflate(inflater,R.layout.fragment_conversation,container,false);
 776		binding.getRoot().setOnClickListener(null); //TODO why the fuck did we do this?
 777
 778		binding.textinput.addTextChangedListener(new StylingHelper.MessageEditorStyler(binding.textinput));
 779
 780		binding.textinput.setOnEditorActionListener(mEditorActionListener);
 781		binding.textinput.setRichContentListener(new String[]{"image/*"}, mEditorContentListener);
 782
 783		binding.textSendButton.setOnClickListener(this.mSendButtonListener);
 784
 785		binding.messagesView.setOnScrollListener(mOnScrollListener);
 786		binding.messagesView.setTranscriptMode(ListView.TRANSCRIPT_MODE_NORMAL);
 787		messageListAdapter = new MessageAdapter((ConversationActivity) getActivity(), this.messageList);
 788		messageListAdapter.setOnContactPictureClicked(message -> {
 789			final boolean received = message.getStatus() <= Message.STATUS_RECEIVED;
 790			if (received) {
 791				if (message.getConversation().getMode() == Conversation.MODE_MULTI) {
 792					Jid user = message.getCounterpart();
 793					if (user != null && !user.isBareJid()) {
 794						if (!message.getConversation().getMucOptions().isUserInRoom(user)) {
 795							Toast.makeText(getActivity(), activity.getString(R.string.user_has_left_conference, user.getResourcepart()), Toast.LENGTH_SHORT).show();
 796						}
 797						highlightInConference(user.getResourcepart());
 798					}
 799					return;
 800				} else {
 801					if (!message.getContact().isSelf()) {
 802						String fingerprint;
 803						if (message.getEncryption() == Message.ENCRYPTION_PGP
 804								|| message.getEncryption() == Message.ENCRYPTION_DECRYPTED) {
 805							fingerprint = "pgp";
 806						} else {
 807							fingerprint = message.getFingerprint();
 808						}
 809						activity.switchToContactDetails(message.getContact(), fingerprint);
 810						return;
 811					}
 812				}
 813			}
 814			Account account = message.getConversation().getAccount();
 815			Intent intent;
 816			if (activity.manuallyChangePresence() && !received) {
 817				intent = new Intent(activity, SetPresenceActivity.class);
 818				intent.putExtra(EXTRA_ACCOUNT, account.getJid().toBareJid().toString());
 819			} else {
 820				intent = new Intent(activity, EditAccountActivity.class);
 821				intent.putExtra("jid", account.getJid().toBareJid().toString());
 822				String fingerprint;
 823				if (message.getEncryption() == Message.ENCRYPTION_PGP
 824						|| message.getEncryption() == Message.ENCRYPTION_DECRYPTED) {
 825					fingerprint = "pgp";
 826				} else {
 827					fingerprint = message.getFingerprint();
 828				}
 829				intent.putExtra("fingerprint", fingerprint);
 830			}
 831			startActivity(intent);
 832		});
 833		messageListAdapter.setOnContactPictureLongClicked(message -> {
 834			if (message.getStatus() <= Message.STATUS_RECEIVED) {
 835				if (message.getConversation().getMode() == Conversation.MODE_MULTI) {
 836					final MucOptions mucOptions = conversation.getMucOptions();
 837					if (!mucOptions.allowPm()) {
 838						Toast.makeText(getActivity(), R.string.private_messages_are_disabled, Toast.LENGTH_SHORT).show();
 839						return;
 840					}
 841					Jid user = message.getCounterpart();
 842					if (user != null && !user.isBareJid()) {
 843						if (mucOptions.isUserInRoom(user)) {
 844							privateMessageWith(user);
 845						} else {
 846							Toast.makeText(getActivity(), activity.getString(R.string.user_has_left_conference, user.getResourcepart()), Toast.LENGTH_SHORT).show();
 847						}
 848					}
 849				}
 850			} else {
 851				activity.showQrCode();
 852			}
 853		});
 854		messageListAdapter.setOnQuoteListener(this::quoteText);
 855		binding.messagesView.setAdapter(messageListAdapter);
 856
 857		registerForContextMenu(binding.messagesView);
 858
 859		return binding.getRoot();
 860	}
 861
 862	private void quoteText(String text) {
 863		if (binding.textinput.isEnabled()) {
 864			text = text.replaceAll("(\n *){2,}", "\n").replaceAll("(^|\n)", "$1> ").replaceAll("\n$", "");
 865			Editable editable = binding.textinput.getEditableText();
 866			int position = binding.textinput.getSelectionEnd();
 867			if (position == -1) position = editable.length();
 868			if (position > 0 && editable.charAt(position - 1) != '\n') {
 869				editable.insert(position++, "\n");
 870			}
 871			editable.insert(position, text);
 872			position += text.length();
 873			editable.insert(position++, "\n");
 874			if (position < editable.length() && editable.charAt(position) != '\n') {
 875				editable.insert(position, "\n");
 876			}
 877			binding.textinput.setSelection(position);
 878			binding.textinput.requestFocus();
 879			InputMethodManager inputMethodManager = (InputMethodManager) getActivity().getSystemService(Context.INPUT_METHOD_SERVICE);
 880			if (inputMethodManager != null) {
 881				inputMethodManager.showSoftInput(binding.textinput, InputMethodManager.SHOW_IMPLICIT);
 882			}
 883		}
 884	}
 885
 886	private void quoteMessage(Message message) {
 887		quoteText(MessageUtils.prepareQuote(message));
 888	}
 889
 890	@Override
 891	public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
 892		synchronized (this.messageList) {
 893			super.onCreateContextMenu(menu, v, menuInfo);
 894			AdapterView.AdapterContextMenuInfo acmi = (AdapterContextMenuInfo) menuInfo;
 895			this.selectedMessage = this.messageList.get(acmi.position);
 896			populateContextMenu(menu);
 897		}
 898	}
 899
 900	private void populateContextMenu(ContextMenu menu) {
 901		final Message m = this.selectedMessage;
 902		final Transferable t = m.getTransferable();
 903		Message relevantForCorrection = m;
 904		while (relevantForCorrection.mergeable(relevantForCorrection.next())) {
 905			relevantForCorrection = relevantForCorrection.next();
 906		}
 907		if (m.getType() != Message.TYPE_STATUS) {
 908			final boolean treatAsFile = m.getType() != Message.TYPE_TEXT
 909					&& m.getType() != Message.TYPE_PRIVATE
 910					&& t == null;
 911			final boolean encrypted = m.getEncryption() == Message.ENCRYPTION_DECRYPTION_FAILED
 912					|| m.getEncryption() == Message.ENCRYPTION_PGP;
 913			activity.getMenuInflater().inflate(R.menu.message_context, menu);
 914			menu.setHeaderTitle(R.string.message_options);
 915			MenuItem copyMessage = menu.findItem(R.id.copy_message);
 916			MenuItem quoteMessage = menu.findItem(R.id.quote_message);
 917			MenuItem retryDecryption = menu.findItem(R.id.retry_decryption);
 918			MenuItem correctMessage = menu.findItem(R.id.correct_message);
 919			MenuItem shareWith = menu.findItem(R.id.share_with);
 920			MenuItem sendAgain = menu.findItem(R.id.send_again);
 921			MenuItem copyUrl = menu.findItem(R.id.copy_url);
 922			MenuItem downloadFile = menu.findItem(R.id.download_file);
 923			MenuItem cancelTransmission = menu.findItem(R.id.cancel_transmission);
 924			MenuItem deleteFile = menu.findItem(R.id.delete_file);
 925			MenuItem showErrorMessage = menu.findItem(R.id.show_error_message);
 926			if (!treatAsFile && !encrypted && !m.isGeoUri() && !m.treatAsDownloadable()) {
 927				copyMessage.setVisible(true);
 928				quoteMessage.setVisible(MessageUtils.prepareQuote(m).length() > 0);
 929			}
 930			if (m.getEncryption() == Message.ENCRYPTION_DECRYPTION_FAILED) {
 931				retryDecryption.setVisible(true);
 932			}
 933			if (relevantForCorrection.getType() == Message.TYPE_TEXT
 934					&& relevantForCorrection.isLastCorrectableMessage()
 935					&& (m.getConversation().getMucOptions().nonanonymous() || m.getConversation().getMode() == Conversation.MODE_SINGLE)) {
 936				correctMessage.setVisible(true);
 937			}
 938			if (treatAsFile || (m.getType() == Message.TYPE_TEXT && !m.treatAsDownloadable())) {
 939				shareWith.setVisible(true);
 940			}
 941			if (m.getStatus() == Message.STATUS_SEND_FAILED) {
 942				sendAgain.setVisible(true);
 943			}
 944			if (m.hasFileOnRemoteHost()
 945					|| m.isGeoUri()
 946					|| m.treatAsDownloadable()
 947					|| (t != null && t instanceof HttpDownloadConnection)) {
 948				copyUrl.setVisible(true);
 949			}
 950			if ((m.isFileOrImage() && t instanceof TransferablePlaceholder && m.hasFileOnRemoteHost())) {
 951				downloadFile.setVisible(true);
 952				downloadFile.setTitle(activity.getString(R.string.download_x_file, UIHelper.getFileDescriptionString(activity, m)));
 953			}
 954			boolean waitingOfferedSending = m.getStatus() == Message.STATUS_WAITING
 955					|| m.getStatus() == Message.STATUS_UNSEND
 956					|| m.getStatus() == Message.STATUS_OFFERED;
 957			if ((t != null && !(t instanceof TransferablePlaceholder)) || waitingOfferedSending && m.needsUploading()) {
 958				cancelTransmission.setVisible(true);
 959			}
 960			if (treatAsFile) {
 961				String path = m.getRelativeFilePath();
 962				if (path == null || !path.startsWith("/")) {
 963					deleteFile.setVisible(true);
 964					deleteFile.setTitle(activity.getString(R.string.delete_x_file, UIHelper.getFileDescriptionString(activity, m)));
 965				}
 966			}
 967			if (m.getStatus() == Message.STATUS_SEND_FAILED && m.getErrorMessage() != null) {
 968				showErrorMessage.setVisible(true);
 969			}
 970		}
 971	}
 972
 973	@Override
 974	public boolean onContextItemSelected(MenuItem item) {
 975		switch (item.getItemId()) {
 976			case R.id.share_with:
 977				shareWith(selectedMessage);
 978				return true;
 979			case R.id.correct_message:
 980				correctMessage(selectedMessage);
 981				return true;
 982			case R.id.copy_message:
 983				copyMessage(selectedMessage);
 984				return true;
 985			case R.id.quote_message:
 986				quoteMessage(selectedMessage);
 987				return true;
 988			case R.id.send_again:
 989				resendMessage(selectedMessage);
 990				return true;
 991			case R.id.copy_url:
 992				copyUrl(selectedMessage);
 993				return true;
 994			case R.id.download_file:
 995				downloadFile(selectedMessage);
 996				return true;
 997			case R.id.cancel_transmission:
 998				cancelTransmission(selectedMessage);
 999				return true;
1000			case R.id.retry_decryption:
1001				retryDecryption(selectedMessage);
1002				return true;
1003			case R.id.delete_file:
1004				deleteFile(selectedMessage);
1005				return true;
1006			case R.id.show_error_message:
1007				showErrorMessage(selectedMessage);
1008				return true;
1009			default:
1010				return super.onContextItemSelected(item);
1011		}
1012	}
1013
1014	@Override
1015	public boolean onOptionsItemSelected(final MenuItem item) {
1016		if (conversation == null) {
1017			return super.onOptionsItemSelected(item);
1018		}
1019		switch (item.getItemId()) {
1020			case R.id.encryption_choice_axolotl:
1021			case R.id.encryption_choice_pgp:
1022			case R.id.encryption_choice_none:
1023				handleEncryptionSelection(item);
1024				break;
1025			case R.id.attach_choose_picture:
1026			case R.id.attach_take_picture:
1027			case R.id.attach_record_video:
1028			case R.id.attach_choose_file:
1029			case R.id.attach_record_voice:
1030			case R.id.attach_location:
1031				handleAttachmentSelection(item);
1032				break;
1033			case R.id.action_archive:
1034				activity.endConversation(conversation);
1035				break;
1036			case R.id.action_contact_details:
1037				activity.switchToContactDetails(conversation.getContact());
1038				break;
1039			case R.id.action_muc_details:
1040				Intent intent = new Intent(getActivity(), ConferenceDetailsActivity.class);
1041				intent.setAction(ConferenceDetailsActivity.ACTION_VIEW_MUC);
1042				intent.putExtra("uuid", conversation.getUuid());
1043				startActivity(intent);
1044				break;
1045			case R.id.action_invite:
1046				activity.inviteToConversation(conversation);
1047				break;
1048			case R.id.action_clear_history:
1049				clearHistoryDialog(conversation);
1050				break;
1051			case R.id.action_mute:
1052				muteConversationDialog(conversation);
1053				break;
1054			case R.id.action_unmute:
1055				unmuteConversation(conversation);
1056				break;
1057			case R.id.action_block:
1058			case R.id.action_unblock:
1059				final Activity activity = getActivity();
1060				if (activity instanceof XmppActivity) {
1061					BlockContactDialog.show((XmppActivity) activity, conversation);
1062				}
1063				break;
1064			default:
1065				break;
1066		}
1067		return super.onOptionsItemSelected(item);
1068	}
1069
1070	private void handleAttachmentSelection(MenuItem item) {
1071		switch (item.getItemId()) {
1072			case R.id.attach_choose_picture:
1073				attachFile(ATTACHMENT_CHOICE_CHOOSE_IMAGE);
1074				break;
1075			case R.id.attach_take_picture:
1076				attachFile(ATTACHMENT_CHOICE_TAKE_PHOTO);
1077				break;
1078			case R.id.attach_record_video:
1079				attachFile(ATTACHMENT_CHOICE_RECORD_VIDEO);
1080				break;
1081			case R.id.attach_choose_file:
1082				attachFile(ATTACHMENT_CHOICE_CHOOSE_FILE);
1083				break;
1084			case R.id.attach_record_voice:
1085				attachFile(ATTACHMENT_CHOICE_RECORD_VOICE);
1086				break;
1087			case R.id.attach_location:
1088				attachFile(ATTACHMENT_CHOICE_LOCATION);
1089				break;
1090		}
1091	}
1092
1093	private void handleEncryptionSelection(MenuItem item) {
1094		if (conversation == null) {
1095			return;
1096		}
1097		final ConversationFragment fragment = (ConversationFragment) getFragmentManager().findFragmentByTag("conversation");
1098		switch (item.getItemId()) {
1099			case R.id.encryption_choice_none:
1100				conversation.setNextEncryption(Message.ENCRYPTION_NONE);
1101				item.setChecked(true);
1102				break;
1103			case R.id.encryption_choice_pgp:
1104				if (activity.hasPgp()) {
1105					if (conversation.getAccount().getPgpSignature() != null) {
1106						conversation.setNextEncryption(Message.ENCRYPTION_PGP);
1107						item.setChecked(true);
1108					} else {
1109						activity.announcePgp(conversation.getAccount(), conversation, null, activity.onOpenPGPKeyPublished);
1110					}
1111				} else {
1112					activity.showInstallPgpDialog();
1113				}
1114				break;
1115			case R.id.encryption_choice_axolotl:
1116				Log.d(Config.LOGTAG, AxolotlService.getLogprefix(conversation.getAccount())
1117						+ "Enabled axolotl for Contact " + conversation.getContact().getJid());
1118				conversation.setNextEncryption(Message.ENCRYPTION_AXOLOTL);
1119				item.setChecked(true);
1120				break;
1121			default:
1122				conversation.setNextEncryption(Message.ENCRYPTION_NONE);
1123				break;
1124		}
1125		activity.xmppConnectionService.updateConversation(conversation);
1126		fragment.updateChatMsgHint();
1127		getActivity().invalidateOptionsMenu();
1128		activity.refreshUi();
1129	}
1130
1131	public void attachFile(final int attachmentChoice) {
1132		if (attachmentChoice != ATTACHMENT_CHOICE_LOCATION) {
1133			if (!Config.ONLY_INTERNAL_STORAGE && !activity.hasStoragePermission(attachmentChoice)) {
1134				return;
1135			}
1136		}
1137		try {
1138			activity.getPreferences().edit()
1139					.putString(RECENTLY_USED_QUICK_ACTION, SendButtonAction.of(attachmentChoice).toString())
1140					.apply();
1141		} catch (IllegalArgumentException e) {
1142			//just do not save
1143		}
1144		final int encryption = conversation.getNextEncryption();
1145		final int mode = conversation.getMode();
1146		if (encryption == Message.ENCRYPTION_PGP) {
1147			if (activity.hasPgp()) {
1148				if (mode == Conversation.MODE_SINGLE && conversation.getContact().getPgpKeyId() != 0) {
1149					activity.xmppConnectionService.getPgpEngine().hasKey(
1150							conversation.getContact(),
1151							new UiCallback<Contact>() {
1152
1153								@Override
1154								public void userInputRequried(PendingIntent pi, Contact contact) {
1155									activity.runIntent(pi, attachmentChoice);
1156								}
1157
1158								@Override
1159								public void success(Contact contact) {
1160									selectPresenceToAttachFile(attachmentChoice);
1161								}
1162
1163								@Override
1164								public void error(int error, Contact contact) {
1165									activity.replaceToast(getString(error));
1166								}
1167							});
1168				} else if (mode == Conversation.MODE_MULTI && conversation.getMucOptions().pgpKeysInUse()) {
1169					if (!conversation.getMucOptions().everybodyHasKeys()) {
1170						Toast warning = Toast.makeText(getActivity(), R.string.missing_public_keys, Toast.LENGTH_LONG);
1171						warning.setGravity(Gravity.CENTER_VERTICAL, 0, 0);
1172						warning.show();
1173					}
1174					selectPresenceToAttachFile(attachmentChoice);
1175				} else {
1176					final ConversationFragment fragment = (ConversationFragment) getFragmentManager()
1177							.findFragmentByTag("conversation");
1178					if (fragment != null) {
1179						fragment.showNoPGPKeyDialog(false, (dialog, which) -> {
1180									conversation.setNextEncryption(Message.ENCRYPTION_NONE);
1181									activity.xmppConnectionService.updateConversation(conversation);
1182									selectPresenceToAttachFile(attachmentChoice);
1183								});
1184					}
1185				}
1186			} else {
1187				activity.showInstallPgpDialog();
1188			}
1189		} else {
1190			if (encryption != Message.ENCRYPTION_AXOLOTL || !trustKeysIfNeeded(REQUEST_TRUST_KEYS_MENU, attachmentChoice)) {
1191				selectPresenceToAttachFile(attachmentChoice);
1192			}
1193		}
1194	}
1195
1196	@Override
1197	public void onRequestPermissionsResult(int requestCode, String permissions[], int[] grantResults) {
1198		if (grantResults.length > 0)
1199			if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
1200				if (requestCode == REQUEST_START_DOWNLOAD) {
1201					if (this.mPendingDownloadableMessage != null) {
1202						startDownloadable(this.mPendingDownloadableMessage);
1203					}
1204				} else if (requestCode == REQUEST_ADD_EDITOR_CONTENT) {
1205					if (this.mPendingEditorContent != null) {
1206						attachImageToConversation(this.mPendingEditorContent);
1207					}
1208				} else {
1209					attachFile(requestCode);
1210				}
1211			} else {
1212				Toast.makeText(getActivity(), R.string.no_storage_permission, Toast.LENGTH_SHORT).show();
1213			}
1214	}
1215
1216	public void startDownloadable(Message message) {
1217		if (!Config.ONLY_INTERNAL_STORAGE && !activity.hasStoragePermission(REQUEST_START_DOWNLOAD)) {
1218			this.mPendingDownloadableMessage = message;
1219			return;
1220		}
1221		Transferable transferable = message.getTransferable();
1222		if (transferable != null) {
1223			if (!transferable.start()) {
1224				Toast.makeText(getActivity(), R.string.not_connected_try_again, Toast.LENGTH_SHORT).show();
1225			}
1226		} else if (message.treatAsDownloadable()) {
1227			activity.xmppConnectionService.getHttpConnectionManager().createNewDownloadConnection(message, true);
1228		}
1229	}
1230
1231	@SuppressLint("InflateParams")
1232	protected void clearHistoryDialog(final Conversation conversation) {
1233		AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
1234		builder.setTitle(getString(R.string.clear_conversation_history));
1235		final View dialogView = getActivity().getLayoutInflater().inflate(R.layout.dialog_clear_history, null);
1236		final CheckBox endConversationCheckBox = dialogView.findViewById(R.id.end_conversation_checkbox);
1237		builder.setView(dialogView);
1238		builder.setNegativeButton(getString(R.string.cancel), null);
1239		builder.setPositiveButton(getString(R.string.delete_messages), (dialog, which) -> {
1240			this.activity.xmppConnectionService.clearConversationHistory(conversation);
1241			if (endConversationCheckBox.isChecked()) {
1242				this.activity.endConversation(conversation);
1243			} else {
1244				activity.updateConversationList();
1245				updateMessages();
1246			}
1247		});
1248		builder.create().show();
1249	}
1250
1251	protected void muteConversationDialog(final Conversation conversation) {
1252		AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
1253		builder.setTitle(R.string.disable_notifications);
1254		final int[] durations = getResources().getIntArray(R.array.mute_options_durations);
1255		builder.setItems(R.array.mute_options_descriptions, (dialog, which) -> {
1256			final long till;
1257			if (durations[which] == -1) {
1258				till = Long.MAX_VALUE;
1259			} else {
1260				till = System.currentTimeMillis() + (durations[which] * 1000);
1261			}
1262			conversation.setMutedTill(till);
1263			activity.xmppConnectionService.updateConversation(conversation);
1264			activity.updateConversationList();
1265			updateMessages();
1266			getActivity().invalidateOptionsMenu();
1267		});
1268		builder.create().show();
1269	}
1270
1271	public void unmuteConversation(final Conversation conversation) {
1272		conversation.setMutedTill(0);
1273		this.activity.xmppConnectionService.updateConversation(conversation);
1274		this.activity.updateConversationList();
1275		updateMessages();
1276		getActivity().invalidateOptionsMenu();
1277	}
1278
1279	protected void selectPresenceToAttachFile(final int attachmentChoice) {
1280		final Account account = conversation.getAccount();
1281		final PresenceSelector.OnPresenceSelected callback = () -> {
1282			Intent intent = new Intent();
1283			boolean chooser = false;
1284			String fallbackPackageId = null;
1285			switch (attachmentChoice) {
1286				case ATTACHMENT_CHOICE_CHOOSE_IMAGE:
1287					intent.setAction(Intent.ACTION_GET_CONTENT);
1288					if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
1289						intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true);
1290					}
1291					intent.setType("image/*");
1292					chooser = true;
1293					break;
1294				case ATTACHMENT_CHOICE_RECORD_VIDEO:
1295					intent.setAction(MediaStore.ACTION_VIDEO_CAPTURE);
1296					break;
1297				case ATTACHMENT_CHOICE_TAKE_PHOTO:
1298					Uri uri = activity.xmppConnectionService.getFileBackend().getTakePhotoUri();
1299					//mPendingImageUris.clear();
1300					//mPendingImageUris.add(uri);
1301					intent.putExtra(MediaStore.EXTRA_OUTPUT, uri);
1302					intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
1303					intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
1304					intent.setAction(MediaStore.ACTION_IMAGE_CAPTURE);
1305					break;
1306				case ATTACHMENT_CHOICE_CHOOSE_FILE:
1307					chooser = true;
1308					intent.setType("*/*");
1309					intent.addCategory(Intent.CATEGORY_OPENABLE);
1310					intent.setAction(Intent.ACTION_GET_CONTENT);
1311					break;
1312				case ATTACHMENT_CHOICE_RECORD_VOICE:
1313					intent.setAction(MediaStore.Audio.Media.RECORD_SOUND_ACTION);
1314					fallbackPackageId = "eu.siacs.conversations.voicerecorder";
1315					break;
1316				case ATTACHMENT_CHOICE_LOCATION:
1317					intent.setAction("eu.siacs.conversations.location.request");
1318					fallbackPackageId = "eu.siacs.conversations.sharelocation";
1319					break;
1320			}
1321			if (intent.resolveActivity(getActivity().getPackageManager()) != null) {
1322				if (chooser) {
1323					startActivityForResult(
1324							Intent.createChooser(intent, getString(R.string.perform_action_with)),
1325							attachmentChoice);
1326				} else {
1327					startActivityForResult(intent, attachmentChoice);
1328				}
1329			} else if (fallbackPackageId != null) {
1330				startActivity(getInstallApkIntent(fallbackPackageId));
1331			}
1332		};
1333		if (account.httpUploadAvailable() || attachmentChoice == ATTACHMENT_CHOICE_LOCATION) {
1334			conversation.setNextCounterpart(null);
1335			callback.onPresenceSelected();
1336		} else {
1337			activity.selectPresence(conversation, callback);
1338		}
1339	}
1340
1341	private Intent getInstallApkIntent(final String packageId) {
1342		Intent intent = new Intent(Intent.ACTION_VIEW);
1343		intent.setData(Uri.parse("market://details?id=" + packageId));
1344		if (intent.resolveActivity(getActivity().getPackageManager()) != null) {
1345			return intent;
1346		} else {
1347			intent.setData(Uri.parse("http://play.google.com/store/apps/details?id=" + packageId));
1348			return intent;
1349		}
1350	}
1351
1352	@Override
1353	public void onResume() {
1354		new Handler().post(() -> {
1355			final PackageManager packageManager = getActivity().getPackageManager();
1356			ConversationMenuConfigurator.updateAttachmentAvailability(packageManager);
1357			getActivity().invalidateOptionsMenu();
1358		});
1359		super.onResume();
1360	}
1361
1362	private void showErrorMessage(final Message message) {
1363		AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
1364		builder.setTitle(R.string.error_message);
1365		builder.setMessage(message.getErrorMessage());
1366		builder.setPositiveButton(R.string.confirm, null);
1367		builder.create().show();
1368	}
1369
1370	private void shareWith(Message message) {
1371		Intent shareIntent = new Intent();
1372		shareIntent.setAction(Intent.ACTION_SEND);
1373		if (message.isGeoUri()) {
1374			shareIntent.putExtra(Intent.EXTRA_TEXT, message.getBody());
1375			shareIntent.setType("text/plain");
1376		} else if (!message.isFileOrImage()) {
1377			shareIntent.putExtra(Intent.EXTRA_TEXT, message.getMergedBody().toString());
1378			shareIntent.setType("text/plain");
1379		} else {
1380			final DownloadableFile file = activity.xmppConnectionService.getFileBackend().getFile(message);
1381			try {
1382				shareIntent.putExtra(Intent.EXTRA_STREAM, FileBackend.getUriForFile(getActivity(), file));
1383			} catch (SecurityException e) {
1384				Toast.makeText(getActivity(), activity.getString(R.string.no_permission_to_access_x, file.getAbsolutePath()), Toast.LENGTH_SHORT).show();
1385				return;
1386			}
1387			shareIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
1388			String mime = message.getMimeType();
1389			if (mime == null) {
1390				mime = "*/*";
1391			}
1392			shareIntent.setType(mime);
1393		}
1394		try {
1395			startActivity(Intent.createChooser(shareIntent, getText(R.string.share_with)));
1396		} catch (ActivityNotFoundException e) {
1397			//This should happen only on faulty androids because normally chooser is always available
1398			Toast.makeText(getActivity(), R.string.no_application_found_to_open_file, Toast.LENGTH_SHORT).show();
1399		}
1400	}
1401
1402	private void copyMessage(Message message) {
1403		if (activity.copyTextToClipboard(message.getMergedBody().toString(), R.string.message)) {
1404			Toast.makeText(getActivity(), R.string.message_copied_to_clipboard, Toast.LENGTH_SHORT).show();
1405		}
1406	}
1407
1408	private void deleteFile(Message message) {
1409		if (activity.xmppConnectionService.getFileBackend().deleteFile(message)) {
1410			message.setTransferable(new TransferablePlaceholder(Transferable.STATUS_DELETED));
1411			activity.updateConversationList();
1412			updateMessages();
1413		}
1414	}
1415
1416	private void resendMessage(final Message message) {
1417		if (message.isFileOrImage()) {
1418			DownloadableFile file = activity.xmppConnectionService.getFileBackend().getFile(message);
1419			if (file.exists()) {
1420				final Conversation conversation = message.getConversation();
1421				final XmppConnection xmppConnection = conversation.getAccount().getXmppConnection();
1422				if (!message.hasFileOnRemoteHost()
1423						&& xmppConnection != null
1424						&& !xmppConnection.getFeatures().httpUpload(message.getFileParams().size)) {
1425					activity.selectPresence(conversation, () -> {
1426						message.setCounterpart(conversation.getNextCounterpart());
1427						activity.xmppConnectionService.resendFailedMessages(message);
1428					});
1429					return;
1430				}
1431			} else {
1432				Toast.makeText(activity, R.string.file_deleted, Toast.LENGTH_SHORT).show();
1433				message.setTransferable(new TransferablePlaceholder(Transferable.STATUS_DELETED));
1434				activity.updateConversationList();
1435				updateMessages();
1436				return;
1437			}
1438		}
1439		activity.xmppConnectionService.resendFailedMessages(message);
1440	}
1441
1442	private void copyUrl(Message message) {
1443		final String url;
1444		final int resId;
1445		if (message.isGeoUri()) {
1446			resId = R.string.location;
1447			url = message.getBody();
1448		} else if (message.hasFileOnRemoteHost()) {
1449			resId = R.string.file_url;
1450			url = message.getFileParams().url.toString();
1451		} else {
1452			url = message.getBody().trim();
1453			resId = R.string.file_url;
1454		}
1455		if (activity.copyTextToClipboard(url, resId)) {
1456			Toast.makeText(getActivity(), R.string.url_copied_to_clipboard, Toast.LENGTH_SHORT).show();
1457		}
1458	}
1459
1460	private void downloadFile(Message message) {
1461		activity.xmppConnectionService.getHttpConnectionManager().createNewDownloadConnection(message, true);
1462	}
1463
1464	private void cancelTransmission(Message message) {
1465		Transferable transferable = message.getTransferable();
1466		if (transferable != null) {
1467			transferable.cancel();
1468		} else if (message.getStatus() != Message.STATUS_RECEIVED) {
1469			activity.xmppConnectionService.markMessage(message, Message.STATUS_SEND_FAILED);
1470		}
1471	}
1472
1473	private void retryDecryption(Message message) {
1474		message.setEncryption(Message.ENCRYPTION_PGP);
1475		activity.updateConversationList();
1476		updateMessages();
1477		conversation.getAccount().getPgpDecryptionService().decrypt(message, false);
1478	}
1479
1480	protected void privateMessageWith(final Jid counterpart) {
1481		if (conversation.setOutgoingChatState(Config.DEFAULT_CHATSTATE)) {
1482			activity.xmppConnectionService.sendChatState(conversation);
1483		}
1484		this.binding.textinput.setText("");
1485		this.conversation.setNextCounterpart(counterpart);
1486		updateChatMsgHint();
1487		updateSendButton();
1488		updateEditablity();
1489	}
1490
1491	private void correctMessage(Message message) {
1492		while (message.mergeable(message.next())) {
1493			message = message.next();
1494		}
1495		this.conversation.setCorrectingMessage(message);
1496		final Editable editable = binding.textinput.getText();
1497		this.conversation.setDraftMessage(editable.toString());
1498		this.binding.textinput.setText("");
1499		this.binding.textinput.append(message.getBody());
1500
1501	}
1502
1503	protected void highlightInConference(String nick) {
1504		final Editable editable = this.binding.textinput.getText();
1505		String oldString = editable.toString().trim();
1506		final int pos = this.binding.textinput.getSelectionStart();
1507		if (oldString.isEmpty() || pos == 0) {
1508			editable.insert(0, nick + ": ");
1509		} else {
1510			final char before = editable.charAt(pos - 1);
1511			final char after = editable.length() > pos ? editable.charAt(pos) : '\0';
1512			if (before == '\n') {
1513				editable.insert(pos, nick + ": ");
1514			} else {
1515				if (pos > 2 && editable.subSequence(pos - 2, pos).toString().equals(": ")) {
1516					if (NickValidityChecker.check(conversation, Arrays.asList(editable.subSequence(0, pos - 2).toString().split(", ")))) {
1517						editable.insert(pos - 2, ", " + nick);
1518						return;
1519					}
1520				}
1521				editable.insert(pos, (Character.isWhitespace(before) ? "" : " ") + nick + (Character.isWhitespace(after) ? "" : " "));
1522				if (Character.isWhitespace(after)) {
1523					this.binding.textinput.setSelection(this.binding.textinput.getSelectionStart() + 1);
1524				}
1525			}
1526		}
1527	}
1528
1529	@Override
1530	public void onStop() {
1531		super.onStop();
1532		final Activity activity = getActivity();
1533		if (activity == null || !activity.isChangingConfigurations()) {
1534			messageListAdapter.stopAudioPlayer();
1535		}
1536		if (this.conversation != null) {
1537			final String msg = this.binding.textinput.getText().toString();
1538			if (this.conversation.setNextMessage(msg)) {
1539				this.activity.xmppConnectionService.updateConversation(this.conversation);
1540			}
1541			updateChatState(this.conversation, msg);
1542		}
1543	}
1544
1545	private void updateChatState(final Conversation conversation, final String msg) {
1546		ChatState state = msg.length() == 0 ? Config.DEFAULT_CHATSTATE : ChatState.PAUSED;
1547		Account.State status = conversation.getAccount().getStatus();
1548		if (status == Account.State.ONLINE && conversation.setOutgoingChatState(state)) {
1549			activity.xmppConnectionService.sendChatState(conversation);
1550		}
1551	}
1552
1553	public boolean reInit(Conversation conversation) {
1554		if (conversation == null) {
1555			return false;
1556		}
1557		setupIme();
1558		if (this.conversation != null) {
1559			final String msg = this.binding.textinput.getText().toString();
1560			if (this.conversation.setNextMessage(msg)) {
1561				activity.xmppConnectionService.updateConversation(conversation);
1562			}
1563			if (this.conversation != conversation) {
1564				updateChatState(this.conversation, msg);
1565				messageListAdapter.stopAudioPlayer();
1566			}
1567			this.conversation.trim();
1568
1569		}
1570
1571		if (activity != null) {
1572			this.binding.textSendButton.setContentDescription(activity.getString(R.string.send_message_to_x, conversation.getName()));
1573		}
1574
1575		this.conversation = conversation;
1576		this.binding.textinput.setKeyboardListener(null);
1577		this.binding.textinput.setText("");
1578		this.binding.textinput.append(this.conversation.getNextMessage());
1579		this.binding.textinput.setKeyboardListener(this);
1580		messageListAdapter.updatePreferences();
1581		this.binding.messagesView.setAdapter(messageListAdapter);
1582		updateMessages();
1583		this.conversation.messagesLoaded.set(true);
1584		synchronized (this.messageList) {
1585			final Message first = conversation.getFirstUnreadMessage();
1586			final int bottom = Math.max(0, this.messageList.size() - 1);
1587			final int pos;
1588			if (first == null) {
1589				pos = bottom;
1590			} else {
1591				int i = getIndexOf(first.getUuid(), this.messageList);
1592				pos = i < 0 ? bottom : i;
1593			}
1594			this.binding.messagesView.setSelection(pos);
1595			return pos == bottom;
1596		}
1597	}
1598
1599	private boolean showBlockSubmenu(View view) {
1600		final Jid jid = conversation.getJid();
1601		if (jid.isDomainJid()) {
1602			BlockContactDialog.show(activity, conversation);
1603		} else {
1604			PopupMenu popupMenu = new PopupMenu(getActivity(), view);
1605			popupMenu.inflate(R.menu.block);
1606			popupMenu.setOnMenuItemClickListener(menuItem -> {
1607				Blockable blockable;
1608				switch (menuItem.getItemId()) {
1609					case R.id.block_domain:
1610						blockable = conversation.getAccount().getRoster().getContact(jid.toDomainJid());
1611						break;
1612					default:
1613						blockable = conversation;
1614				}
1615				BlockContactDialog.show(activity, blockable);
1616				return true;
1617			});
1618			popupMenu.show();
1619		}
1620		return true;
1621	}
1622
1623	private void updateSnackBar(final Conversation conversation) {
1624		final Account account = conversation.getAccount();
1625		final XmppConnection connection = account.getXmppConnection();
1626		final int mode = conversation.getMode();
1627		final Contact contact = mode == Conversation.MODE_SINGLE ? conversation.getContact() : null;
1628		if (account.getStatus() == Account.State.DISABLED) {
1629			showSnackbar(R.string.this_account_is_disabled, R.string.enable, this.mEnableAccountListener);
1630		} else if (conversation.isBlocked()) {
1631			showSnackbar(R.string.contact_blocked, R.string.unblock, this.mUnblockClickListener);
1632		} else if (contact != null && !contact.showInRoster() && contact.getOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST)) {
1633			showSnackbar(R.string.contact_added_you, R.string.add_back, this.mAddBackClickListener, this.mLongPressBlockListener);
1634		} else if (contact != null && contact.getOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST)) {
1635			showSnackbar(R.string.contact_asks_for_presence_subscription, R.string.allow, this.mAllowPresenceSubscription, this.mLongPressBlockListener);
1636		} else if (mode == Conversation.MODE_MULTI
1637				&& !conversation.getMucOptions().online()
1638				&& account.getStatus() == Account.State.ONLINE) {
1639			switch (conversation.getMucOptions().getError()) {
1640				case NICK_IN_USE:
1641					showSnackbar(R.string.nick_in_use, R.string.edit, clickToMuc);
1642					break;
1643				case NO_RESPONSE:
1644					showSnackbar(R.string.joining_conference, 0, null);
1645					break;
1646				case SERVER_NOT_FOUND:
1647					if (conversation.receivedMessagesCount() > 0) {
1648						showSnackbar(R.string.remote_server_not_found, R.string.try_again, joinMuc);
1649					} else {
1650						showSnackbar(R.string.remote_server_not_found, R.string.leave, leaveMuc);
1651					}
1652					break;
1653				case PASSWORD_REQUIRED:
1654					showSnackbar(R.string.conference_requires_password, R.string.enter_password, enterPassword);
1655					break;
1656				case BANNED:
1657					showSnackbar(R.string.conference_banned, R.string.leave, leaveMuc);
1658					break;
1659				case MEMBERS_ONLY:
1660					showSnackbar(R.string.conference_members_only, R.string.leave, leaveMuc);
1661					break;
1662				case KICKED:
1663					showSnackbar(R.string.conference_kicked, R.string.join, joinMuc);
1664					break;
1665				case UNKNOWN:
1666					showSnackbar(R.string.conference_unknown_error, R.string.try_again, joinMuc);
1667					break;
1668				case INVALID_NICK:
1669					showSnackbar(R.string.invalid_muc_nick, R.string.edit, clickToMuc);
1670				case SHUTDOWN:
1671					showSnackbar(R.string.conference_shutdown, R.string.try_again, joinMuc);
1672					break;
1673				default:
1674					hideSnackbar();
1675					break;
1676			}
1677		} else if (account.hasPendingPgpIntent(conversation)) {
1678			showSnackbar(R.string.openpgp_messages_found, R.string.decrypt, clickToDecryptListener);
1679		} else if (connection != null
1680				&& connection.getFeatures().blocking()
1681				&& conversation.countMessages() != 0
1682				&& !conversation.isBlocked()
1683				&& conversation.isWithStranger()) {
1684			showSnackbar(R.string.received_message_from_stranger, R.string.block, mBlockClickListener);
1685		} else {
1686			hideSnackbar();
1687		}
1688	}
1689
1690	public void updateMessages() {
1691		synchronized (this.messageList) {
1692			if (getView() == null) {
1693				return;
1694			}
1695			final ConversationActivity activity = (ConversationActivity) getActivity();
1696			if (this.conversation != null) {
1697				conversation.populateWithMessages(ConversationFragment.this.messageList);
1698				updateSnackBar(conversation);
1699				updateStatusMessages();
1700				this.messageListAdapter.notifyDataSetChanged();
1701				updateChatMsgHint();
1702				if (!activity.isConversationsOverviewVisable() || !activity.isConversationsOverviewHideable()) {
1703					activity.sendReadMarkerIfNecessary(conversation);
1704				}
1705				updateSendButton();
1706				updateEditablity();
1707			}
1708		}
1709	}
1710
1711	protected void messageSent() {
1712		mSendingPgpMessage.set(false);
1713		this.binding.textinput.setText("");
1714		if (conversation.setCorrectingMessage(null)) {
1715			this.binding.textinput.append(conversation.getDraftMessage());
1716			conversation.setDraftMessage(null);
1717		}
1718		if (conversation.setNextMessage(this.binding.textinput.getText().toString())) {
1719			activity.xmppConnectionService.updateConversation(conversation);
1720		}
1721		updateChatMsgHint();
1722		new Handler().post(() -> {
1723			int size = messageList.size();
1724			this.binding.messagesView.setSelection(size - 1);
1725		});
1726	}
1727
1728	public void setFocusOnInputField() {
1729		this.binding.textinput.requestFocus();
1730	}
1731
1732	public void doneSendingPgpMessage() {
1733		mSendingPgpMessage.set(false);
1734	}
1735
1736
1737	private void updateEditablity() {
1738		boolean canWrite = this.conversation.getMode() == Conversation.MODE_SINGLE || this.conversation.getMucOptions().participating() || this.conversation.getNextCounterpart() != null;
1739		this.binding.textinput.setFocusable(canWrite);
1740		this.binding.textinput.setFocusableInTouchMode(canWrite);
1741		this.binding.textSendButton.setEnabled(canWrite);
1742		this.binding.textinput.setCursorVisible(canWrite);
1743	}
1744
1745	public void updateSendButton() {
1746		final Conversation c = this.conversation;
1747		final Presence.Status status;
1748		final String text = this.binding.textinput == null ? "" : this.binding.textinput.getText().toString();
1749		final SendButtonAction action = SendButtonTool.getAction(getActivity(),c,text);
1750		if (activity.useSendButtonToIndicateStatus() && c.getAccount().getStatus() == Account.State.ONLINE) {
1751			if (activity.xmppConnectionService != null && activity.xmppConnectionService.getMessageArchiveService().isCatchingUp(c)) {
1752				status = Presence.Status.OFFLINE;
1753			} else if (c.getMode() == Conversation.MODE_SINGLE) {
1754				status = c.getContact().getShownStatus();
1755			} else {
1756				status = c.getMucOptions().online() ? Presence.Status.ONLINE : Presence.Status.OFFLINE;
1757			}
1758		} else {
1759			status = Presence.Status.OFFLINE;
1760		}
1761		this.binding.textSendButton.setTag(action);
1762		this.binding.textSendButton.setImageResource(SendButtonTool.getSendButtonImageResource(getActivity(), action, status));
1763	}
1764
1765	protected void updateDateSeparators() {
1766		synchronized (this.messageList) {
1767			for (int i = 0; i < this.messageList.size(); ++i) {
1768				final Message current = this.messageList.get(i);
1769				if (i == 0 || !UIHelper.sameDay(this.messageList.get(i - 1).getTimeSent(), current.getTimeSent())) {
1770					this.messageList.add(i, Message.createDateSeparator(current));
1771					i++;
1772				}
1773			}
1774		}
1775	}
1776
1777	protected void updateStatusMessages() {
1778		updateDateSeparators();
1779		synchronized (this.messageList) {
1780			if (showLoadMoreMessages(conversation)) {
1781				this.messageList.add(0, Message.createLoadMoreMessage(conversation));
1782			}
1783			if (conversation.getMode() == Conversation.MODE_SINGLE) {
1784				ChatState state = conversation.getIncomingChatState();
1785				if (state == ChatState.COMPOSING) {
1786					this.messageList.add(Message.createStatusMessage(conversation, getString(R.string.contact_is_typing, conversation.getName())));
1787				} else if (state == ChatState.PAUSED) {
1788					this.messageList.add(Message.createStatusMessage(conversation, getString(R.string.contact_has_stopped_typing, conversation.getName())));
1789				} else {
1790					for (int i = this.messageList.size() - 1; i >= 0; --i) {
1791						if (this.messageList.get(i).getStatus() == Message.STATUS_RECEIVED) {
1792							return;
1793						} else {
1794							if (this.messageList.get(i).getStatus() == Message.STATUS_SEND_DISPLAYED) {
1795								this.messageList.add(i + 1,
1796										Message.createStatusMessage(conversation, getString(R.string.contact_has_read_up_to_this_point, conversation.getName())));
1797								return;
1798							}
1799						}
1800					}
1801				}
1802			} else {
1803				final MucOptions mucOptions = conversation.getMucOptions();
1804				final List<MucOptions.User> allUsers = mucOptions.getUsers();
1805				final Set<ReadByMarker> addedMarkers = new HashSet<>();
1806				ChatState state = ChatState.COMPOSING;
1807				List<MucOptions.User> users = conversation.getMucOptions().getUsersWithChatState(state, 5);
1808				if (users.size() == 0) {
1809					state = ChatState.PAUSED;
1810					users = conversation.getMucOptions().getUsersWithChatState(state, 5);
1811				}
1812				if (mucOptions.isPrivateAndNonAnonymous()) {
1813					for (int i = this.messageList.size() - 1; i >= 0; --i) {
1814						final Set<ReadByMarker> markersForMessage = messageList.get(i).getReadByMarkers();
1815						final List<MucOptions.User> shownMarkers = new ArrayList<>();
1816						for (ReadByMarker marker : markersForMessage) {
1817							if (!ReadByMarker.contains(marker, addedMarkers)) {
1818								addedMarkers.add(marker); //may be put outside this condition. set should do dedup anyway
1819								MucOptions.User user = mucOptions.findUser(marker);
1820								if (user != null && !users.contains(user)) {
1821									shownMarkers.add(user);
1822								}
1823							}
1824						}
1825						final ReadByMarker markerForSender = ReadByMarker.from(messageList.get(i));
1826						final Message statusMessage;
1827						final int size = shownMarkers.size();
1828						if (size > 1) {
1829							final String body;
1830							if (size <= 4) {
1831								body = getString(R.string.contacts_have_read_up_to_this_point, UIHelper.concatNames(shownMarkers));
1832							} else {
1833								body = getString(R.string.contacts_and_n_more_have_read_up_to_this_point, UIHelper.concatNames(shownMarkers, 3), size - 3);
1834							}
1835							statusMessage = Message.createStatusMessage(conversation, body);
1836							statusMessage.setCounterparts(shownMarkers);
1837						} else if (size == 1) {
1838							statusMessage = Message.createStatusMessage(conversation, getString(R.string.contact_has_read_up_to_this_point, UIHelper.getDisplayName(shownMarkers.get(0))));
1839							statusMessage.setCounterpart(shownMarkers.get(0).getFullJid());
1840							statusMessage.setTrueCounterpart(shownMarkers.get(0).getRealJid());
1841						} else {
1842							statusMessage = null;
1843						}
1844						if (statusMessage != null) {
1845							this.messageList.add(i + 1, statusMessage);
1846						}
1847						addedMarkers.add(markerForSender);
1848						if (ReadByMarker.allUsersRepresented(allUsers, addedMarkers)) {
1849							break;
1850						}
1851					}
1852				}
1853				if (users.size() > 0) {
1854					Message statusMessage;
1855					if (users.size() == 1) {
1856						MucOptions.User user = users.get(0);
1857						int id = state == ChatState.COMPOSING ? R.string.contact_is_typing : R.string.contact_has_stopped_typing;
1858						statusMessage = Message.createStatusMessage(conversation, getString(id, UIHelper.getDisplayName(user)));
1859						statusMessage.setTrueCounterpart(user.getRealJid());
1860						statusMessage.setCounterpart(user.getFullJid());
1861					} else {
1862						int id = state == ChatState.COMPOSING ? R.string.contacts_are_typing : R.string.contacts_have_stopped_typing;
1863						statusMessage = Message.createStatusMessage(conversation, getString(id, UIHelper.concatNames(users)));
1864						statusMessage.setCounterparts(users);
1865					}
1866					this.messageList.add(statusMessage);
1867				}
1868
1869			}
1870		}
1871	}
1872
1873	public void stopScrolling() {
1874		long now = SystemClock.uptimeMillis();
1875		MotionEvent cancel = MotionEvent.obtain(now, now, MotionEvent.ACTION_CANCEL, 0, 0, 0);
1876		binding.messagesView.dispatchTouchEvent(cancel);
1877	}
1878
1879	private boolean showLoadMoreMessages(final Conversation c) {
1880		final boolean mam = hasMamSupport(c) && !c.getContact().isBlocked();
1881		final MessageArchiveService service = activity.xmppConnectionService.getMessageArchiveService();
1882		return mam && (c.getLastClearHistory().getTimestamp() != 0 || (c.countMessages() == 0 && c.messagesLoaded.get() && c.hasMessagesLeftOnServer() && !service.queryInProgress(c)));
1883	}
1884
1885	private boolean hasMamSupport(final Conversation c) {
1886		if (c.getMode() == Conversation.MODE_SINGLE) {
1887			final XmppConnection connection = c.getAccount().getXmppConnection();
1888			return connection != null && connection.getFeatures().mam();
1889		} else {
1890			return c.getMucOptions().mamSupport();
1891		}
1892	}
1893
1894	protected void showSnackbar(final int message, final int action, final OnClickListener clickListener) {
1895		showSnackbar(message, action, clickListener, null);
1896	}
1897
1898	protected void showSnackbar(final int message, final int action, final OnClickListener clickListener, final View.OnLongClickListener longClickListener) {
1899		this.binding.snackbar.setVisibility(View.VISIBLE);
1900		this.binding.snackbar.setOnClickListener(null);
1901		this.binding.snackbarMessage.setText(message);
1902		this.binding.snackbarMessage.setOnClickListener(null);
1903		this.binding.snackbarAction.setVisibility(clickListener == null ? View.GONE : View.VISIBLE);
1904		if (action != 0) {
1905			this.binding.snackbarAction.setText(action);
1906		}
1907		this.binding.snackbarAction.setOnClickListener(clickListener);
1908		this.binding.snackbarAction.setOnLongClickListener(longClickListener);
1909	}
1910
1911	protected void hideSnackbar() {
1912		this.binding.snackbar.setVisibility(View.GONE);
1913	}
1914
1915	protected void sendPlainTextMessage(Message message) {
1916		ConversationActivity activity = (ConversationActivity) getActivity();
1917		activity.xmppConnectionService.sendMessage(message);
1918		messageSent();
1919	}
1920
1921	protected void sendPgpMessage(final Message message) {
1922		final ConversationActivity activity = (ConversationActivity) getActivity();
1923		final XmppConnectionService xmppService = activity.xmppConnectionService;
1924		final Contact contact = message.getConversation().getContact();
1925		if (!activity.hasPgp()) {
1926			activity.showInstallPgpDialog();
1927			return;
1928		}
1929		if (conversation.getAccount().getPgpSignature() == null) {
1930			activity.announcePgp(conversation.getAccount(), conversation, null, activity.onOpenPGPKeyPublished);
1931			return;
1932		}
1933		if (!mSendingPgpMessage.compareAndSet(false, true)) {
1934			Log.d(Config.LOGTAG, "sending pgp message already in progress");
1935		}
1936		if (conversation.getMode() == Conversation.MODE_SINGLE) {
1937			if (contact.getPgpKeyId() != 0) {
1938				xmppService.getPgpEngine().hasKey(contact,
1939						new UiCallback<Contact>() {
1940
1941							@Override
1942							public void userInputRequried(PendingIntent pi,Contact contact) {
1943								activity.runIntent(pi,REQUEST_ENCRYPT_MESSAGE);
1944							}
1945
1946							@Override
1947							public void success(Contact contact) {
1948								encryptTextMessage(message);
1949							}
1950
1951							@Override
1952							public void error(int error, Contact contact) {
1953								activity.runOnUiThread(() -> Toast.makeText(activity,
1954										R.string.unable_to_connect_to_keychain,
1955										Toast.LENGTH_SHORT
1956								).show());
1957								mSendingPgpMessage.set(false);
1958							}
1959						});
1960
1961			} else {
1962				showNoPGPKeyDialog(false, (dialog, which) -> {
1963							conversation.setNextEncryption(Message.ENCRYPTION_NONE);
1964							xmppService.updateConversation(conversation);
1965							message.setEncryption(Message.ENCRYPTION_NONE);
1966							xmppService.sendMessage(message);
1967							messageSent();
1968						});
1969			}
1970		} else {
1971			if (conversation.getMucOptions().pgpKeysInUse()) {
1972				if (!conversation.getMucOptions().everybodyHasKeys()) {
1973					Toast warning = Toast
1974							.makeText(getActivity(),
1975									R.string.missing_public_keys,
1976									Toast.LENGTH_LONG);
1977					warning.setGravity(Gravity.CENTER_VERTICAL, 0, 0);
1978					warning.show();
1979				}
1980				encryptTextMessage(message);
1981			} else {
1982				showNoPGPKeyDialog(true, (dialog, which) -> {
1983							conversation.setNextEncryption(Message.ENCRYPTION_NONE);
1984							message.setEncryption(Message.ENCRYPTION_NONE);
1985							xmppService.updateConversation(conversation);
1986							xmppService.sendMessage(message);
1987							messageSent();
1988						});
1989			}
1990		}
1991	}
1992
1993	public void encryptTextMessage(Message message) {
1994		activity.xmppConnectionService.getPgpEngine().encrypt(message,
1995				new UiCallback<Message>() {
1996
1997					@Override
1998					public void userInputRequried(PendingIntent pi, Message message) {
1999						activity.runIntent(pi, REQUEST_SEND_MESSAGE);
2000					}
2001
2002					@Override
2003					public void success(Message message) {
2004						message.setEncryption(Message.ENCRYPTION_DECRYPTED);
2005						activity.xmppConnectionService.sendMessage(message);
2006						getActivity().runOnUiThread(() -> messageSent());
2007					}
2008
2009					@Override
2010					public void error(final int error, Message message) {
2011						getActivity().runOnUiThread(() -> {
2012							doneSendingPgpMessage();
2013							Toast.makeText(getActivity(),R.string.unable_to_connect_to_keychain,Toast.LENGTH_SHORT).show();
2014						});
2015
2016					}
2017				});
2018	}
2019
2020	public void showNoPGPKeyDialog(boolean plural, DialogInterface.OnClickListener listener) {
2021		AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
2022		builder.setIconAttribute(android.R.attr.alertDialogIcon);
2023		if (plural) {
2024			builder.setTitle(getString(R.string.no_pgp_keys));
2025			builder.setMessage(getText(R.string.contacts_have_no_pgp_keys));
2026		} else {
2027			builder.setTitle(getString(R.string.no_pgp_key));
2028			builder.setMessage(getText(R.string.contact_has_no_pgp_key));
2029		}
2030		builder.setNegativeButton(getString(R.string.cancel), null);
2031		builder.setPositiveButton(getString(R.string.send_unencrypted), listener);
2032		builder.create().show();
2033	}
2034
2035	protected void sendAxolotlMessage(final Message message) {
2036		final ConversationActivity activity = (ConversationActivity) getActivity();
2037		final XmppConnectionService xmppService = activity.xmppConnectionService;
2038		xmppService.sendMessage(message);
2039		messageSent();
2040	}
2041
2042	public void appendText(String text) {
2043		if (text == null) {
2044			return;
2045		}
2046		String previous = this.binding.textinput.getText().toString();
2047		if (previous.length() != 0 && !previous.endsWith(" ")) {
2048			text = " " + text;
2049		}
2050		this.binding.textinput.append(text);
2051	}
2052
2053	@Override
2054	public boolean onEnterPressed() {
2055		if (activity.enterIsSend()) {
2056			sendMessage();
2057			return true;
2058		} else {
2059			return false;
2060		}
2061	}
2062
2063	@Override
2064	public void onTypingStarted() {
2065		Account.State status = conversation.getAccount().getStatus();
2066		if (status == Account.State.ONLINE && conversation.setOutgoingChatState(ChatState.COMPOSING)) {
2067			activity.xmppConnectionService.sendChatState(conversation);
2068		}
2069		activity.hideConversationsOverview();
2070		updateSendButton();
2071	}
2072
2073	@Override
2074	public void onTypingStopped() {
2075		Account.State status = conversation.getAccount().getStatus();
2076		if (status == Account.State.ONLINE && conversation.setOutgoingChatState(ChatState.PAUSED)) {
2077			activity.xmppConnectionService.sendChatState(conversation);
2078		}
2079	}
2080
2081	@Override
2082	public void onTextDeleted() {
2083		Account.State status = conversation.getAccount().getStatus();
2084		if (status == Account.State.ONLINE && conversation.setOutgoingChatState(Config.DEFAULT_CHATSTATE)) {
2085			activity.xmppConnectionService.sendChatState(conversation);
2086		}
2087		updateSendButton();
2088	}
2089
2090	@Override
2091	public void onTextChanged() {
2092		if (conversation != null && conversation.getCorrectingMessage() != null) {
2093			updateSendButton();
2094		}
2095	}
2096
2097	@Override
2098	public boolean onTabPressed(boolean repeated) {
2099		if (conversation == null || conversation.getMode() == Conversation.MODE_SINGLE) {
2100			return false;
2101		}
2102		if (repeated) {
2103			completionIndex++;
2104		} else {
2105			lastCompletionLength = 0;
2106			completionIndex = 0;
2107			final String content = this.binding.textinput.getText().toString();
2108			lastCompletionCursor = this.binding.textinput.getSelectionEnd();
2109			int start = lastCompletionCursor > 0 ? content.lastIndexOf(" ", lastCompletionCursor - 1) + 1 : 0;
2110			firstWord = start == 0;
2111			incomplete = content.substring(start, lastCompletionCursor);
2112		}
2113		List<String> completions = new ArrayList<>();
2114		for (MucOptions.User user : conversation.getMucOptions().getUsers()) {
2115			String name = user.getName();
2116			if (name != null && name.startsWith(incomplete)) {
2117				completions.add(name + (firstWord ? ": " : " "));
2118			}
2119		}
2120		Collections.sort(completions);
2121		if (completions.size() > completionIndex) {
2122			String completion = completions.get(completionIndex).substring(incomplete.length());
2123			this.binding.textinput.getEditableText().delete(lastCompletionCursor, lastCompletionCursor + lastCompletionLength);
2124			this.binding.textinput.getEditableText().insert(lastCompletionCursor, completion);
2125			lastCompletionLength = completion.length();
2126		} else {
2127			completionIndex = -1;
2128			this.binding.textinput.getEditableText().delete(lastCompletionCursor, lastCompletionCursor + lastCompletionLength);
2129			lastCompletionLength = 0;
2130		}
2131		return true;
2132	}
2133
2134	public void onBackendConnected() {
2135		if (postponedActivityResult != null) {
2136			handleActivityResult(postponedActivityResult);
2137		}
2138		postponedActivityResult = null;
2139	}
2140
2141	public void clearPending() {
2142		if (postponedActivityResult != null) {
2143			Log.d(Config.LOGTAG,"cleared pending intent with unhandled result left");
2144		}
2145		postponedActivityResult = null;
2146	}
2147}