ConversationFragment.java

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