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