ConversationFragment.java

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