ConversationFragment.java

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