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