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