ConversationFragment.java

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