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