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