ConversationFragment.java

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