ConversationFragment.java

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