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