1package eu.siacs.conversations.ui;
2
3import android.Manifest;
4import android.annotation.SuppressLint;
5import android.app.Activity;
6import android.content.SharedPreferences;
7import android.content.pm.PackageManager;
8import android.databinding.DataBindingUtil;
9import android.net.Uri;
10import android.os.Build;
11import android.preference.PreferenceManager;
12import android.provider.MediaStore;
13import android.support.annotation.IdRes;
14import android.support.v7.app.AlertDialog;
15import android.app.Fragment;
16import android.app.PendingIntent;
17import android.content.ActivityNotFoundException;
18import android.content.Context;
19import android.content.DialogInterface;
20import android.content.Intent;
21import android.content.IntentSender.SendIntentException;
22import android.os.Bundle;
23import android.os.Handler;
24import android.os.SystemClock;
25import android.support.v13.view.inputmethod.InputConnectionCompat;
26import android.support.v13.view.inputmethod.InputContentInfoCompat;
27import android.text.Editable;
28import android.util.Log;
29import android.util.Pair;
30import android.view.ContextMenu;
31import android.view.ContextMenu.ContextMenuInfo;
32import android.view.Gravity;
33import android.view.LayoutInflater;
34import android.view.Menu;
35import android.view.MenuInflater;
36import android.view.MenuItem;
37import android.view.MotionEvent;
38import android.view.View;
39import android.view.View.OnClickListener;
40import android.view.ViewGroup;
41import android.view.inputmethod.EditorInfo;
42import android.view.inputmethod.InputMethodManager;
43import android.widget.AbsListView;
44import android.widget.AbsListView.OnScrollListener;
45import android.widget.AdapterView;
46import android.widget.AdapterView.AdapterContextMenuInfo;
47import android.widget.CheckBox;
48import android.widget.ListView;
49import android.widget.PopupMenu;
50import android.widget.TextView.OnEditorActionListener;
51import android.widget.Toast;
52
53import org.openintents.openpgp.util.OpenPgpApi;
54
55import java.util.ArrayList;
56import java.util.Arrays;
57import java.util.Collections;
58import java.util.HashSet;
59import java.util.Iterator;
60import java.util.List;
61import java.util.Set;
62import java.util.UUID;
63import java.util.concurrent.atomic.AtomicBoolean;
64
65import eu.siacs.conversations.Config;
66import eu.siacs.conversations.R;
67import eu.siacs.conversations.crypto.axolotl.AxolotlService;
68import eu.siacs.conversations.crypto.axolotl.FingerprintStatus;
69import eu.siacs.conversations.databinding.FragmentConversationBinding;
70import eu.siacs.conversations.entities.Account;
71import eu.siacs.conversations.entities.Blockable;
72import eu.siacs.conversations.entities.Contact;
73import eu.siacs.conversations.entities.Conversation;
74import eu.siacs.conversations.entities.DownloadableFile;
75import eu.siacs.conversations.entities.Message;
76import eu.siacs.conversations.entities.MucOptions;
77import eu.siacs.conversations.entities.Presence;
78import eu.siacs.conversations.entities.ReadByMarker;
79import eu.siacs.conversations.entities.Transferable;
80import eu.siacs.conversations.entities.TransferablePlaceholder;
81import eu.siacs.conversations.http.HttpDownloadConnection;
82import eu.siacs.conversations.persistance.FileBackend;
83import eu.siacs.conversations.services.MessageArchiveService;
84import eu.siacs.conversations.services.XmppConnectionService;
85import eu.siacs.conversations.ui.adapter.MessageAdapter;
86import eu.siacs.conversations.ui.util.ActivityResult;
87import eu.siacs.conversations.ui.util.AttachmentTool;
88import eu.siacs.conversations.ui.util.ConversationMenuConfigurator;
89import eu.siacs.conversations.ui.util.PendingItem;
90import eu.siacs.conversations.ui.util.PresenceSelector;
91import eu.siacs.conversations.ui.util.SendButtonAction;
92import eu.siacs.conversations.ui.util.SendButtonTool;
93import eu.siacs.conversations.ui.widget.EditMessage;
94import eu.siacs.conversations.utils.MessageUtils;
95import eu.siacs.conversations.utils.NickValidityChecker;
96import eu.siacs.conversations.utils.StylingHelper;
97import eu.siacs.conversations.utils.UIHelper;
98import eu.siacs.conversations.xmpp.XmppConnection;
99import eu.siacs.conversations.xmpp.chatstate.ChatState;
100import eu.siacs.conversations.xmpp.jid.InvalidJidException;
101import eu.siacs.conversations.xmpp.jid.Jid;
102
103import static eu.siacs.conversations.ui.XmppActivity.EXTRA_ACCOUNT;
104import static eu.siacs.conversations.ui.XmppActivity.REQUEST_ANNOUNCE_PGP;
105import static eu.siacs.conversations.ui.XmppActivity.REQUEST_CHOOSE_PGP_ID;
106
107
108public class ConversationFragment extends XmppFragment implements EditMessage.KeyboardListener {
109
110
111 public static final int REQUEST_SEND_MESSAGE = 0x0201;
112 public static final int REQUEST_DECRYPT_PGP = 0x0202;
113 public static final int REQUEST_ENCRYPT_MESSAGE = 0x0207;
114 public static final int REQUEST_TRUST_KEYS_TEXT = 0x0208;
115 public static final int REQUEST_TRUST_KEYS_MENU = 0x0209;
116 public static final int REQUEST_START_DOWNLOAD = 0x0210;
117 public static final int REQUEST_ADD_EDITOR_CONTENT = 0x0211;
118 public static final int ATTACHMENT_CHOICE_CHOOSE_IMAGE = 0x0301;
119 public static final int ATTACHMENT_CHOICE_TAKE_PHOTO = 0x0302;
120 public static final int ATTACHMENT_CHOICE_CHOOSE_FILE = 0x0303;
121 public static final int ATTACHMENT_CHOICE_RECORD_VOICE = 0x0304;
122 public static final int ATTACHMENT_CHOICE_LOCATION = 0x0305;
123 public static final int ATTACHMENT_CHOICE_INVALID = 0x0306;
124 public static final int ATTACHMENT_CHOICE_RECORD_VIDEO = 0x0307;
125
126 public static final String RECENTLY_USED_QUICK_ACTION = "recently_used_quick_action";
127 public static final String STATE_CONVERSATION_UUID = ConversationFragment.class.getName() + ".uuid";
128 public static final String STATE_SCROLL_POSITION = ConversationFragment.class.getName() + ".scroll_position";
129 public static final String STATE_PHOTO_URI = ConversationFragment.class.getName()+".take_photo_uri";
130
131
132 final protected List<Message> messageList = new ArrayList<>();
133 private final PendingItem<ActivityResult> postponedActivityResult = new PendingItem<>();
134 private final PendingItem<String> pendingConversationsUuid = new PendingItem<>();
135 private final PendingItem<Bundle> pendingExtras = new PendingItem<>();
136 private final PendingItem<Uri> pendingTakePhotoUri = new PendingItem<>();
137 public Uri mPendingEditorContent = null;
138 protected MessageAdapter messageListAdapter;
139 private Conversation conversation;
140 private FragmentConversationBinding binding;
141 private Toast messageLoaderToast;
142 private ConversationActivity activity;
143
144 private boolean reInitRequiredOnStart = true;
145
146 private OnClickListener clickToMuc = new OnClickListener() {
147
148 @Override
149 public void onClick(View v) {
150 Intent intent = new Intent(getActivity(), ConferenceDetailsActivity.class);
151 intent.setAction(ConferenceDetailsActivity.ACTION_VIEW_MUC);
152 intent.putExtra("uuid", conversation.getUuid());
153 startActivity(intent);
154 }
155 };
156 private OnClickListener leaveMuc = new OnClickListener() {
157
158 @Override
159 public void onClick(View v) {
160 activity.xmppConnectionService.archiveConversation(conversation);
161 activity.onConversationArchived(conversation);
162 }
163 };
164 private OnClickListener joinMuc = new OnClickListener() {
165
166 @Override
167 public void onClick(View v) {
168 activity.xmppConnectionService.joinMuc(conversation);
169 }
170 };
171 private OnClickListener enterPassword = new OnClickListener() {
172
173 @Override
174 public void onClick(View v) {
175 MucOptions muc = conversation.getMucOptions();
176 String password = muc.getPassword();
177 if (password == null) {
178 password = "";
179 }
180 activity.quickPasswordEdit(password, value -> {
181 activity.xmppConnectionService.providePasswordForMuc(conversation, value);
182 return null;
183 });
184 }
185 };
186 private OnScrollListener mOnScrollListener = new OnScrollListener() {
187
188 @Override
189 public void onScrollStateChanged(AbsListView view, int scrollState) {
190 // TODO Auto-generated method stub
191
192 }
193
194 @Override
195 public void onScroll(final AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
196 synchronized (ConversationFragment.this.messageList) {
197 if (firstVisibleItem < 5 && conversation != null && conversation.messagesLoaded.compareAndSet(true, false) && messageList.size() > 0) {
198 long timestamp;
199 if (messageList.get(0).getType() == Message.TYPE_STATUS && messageList.size() >= 2) {
200 timestamp = messageList.get(1).getTimeSent();
201 } else {
202 timestamp = messageList.get(0).getTimeSent();
203 }
204 activity.xmppConnectionService.loadMoreMessages(conversation, timestamp, new XmppConnectionService.OnMoreMessagesLoaded() {
205 @Override
206 public void onMoreMessagesLoaded(final int c, final Conversation conversation) {
207 if (ConversationFragment.this.conversation != conversation) {
208 conversation.messagesLoaded.set(true);
209 return;
210 }
211 getActivity().runOnUiThread(() -> {
212 final int oldPosition = binding.messagesView.getFirstVisiblePosition();
213 Message message = null;
214 int childPos;
215 for (childPos = 0; childPos + oldPosition < messageList.size(); ++childPos) {
216 message = messageList.get(oldPosition + childPos);
217 if (message.getType() != Message.TYPE_STATUS) {
218 break;
219 }
220 }
221 final String uuid = message != null ? message.getUuid() : null;
222 View v = binding.messagesView.getChildAt(childPos);
223 final int pxOffset = (v == null) ? 0 : v.getTop();
224 ConversationFragment.this.conversation.populateWithMessages(ConversationFragment.this.messageList);
225 try {
226 updateStatusMessages();
227 } catch (IllegalStateException e) {
228 Log.d(Config.LOGTAG, "caught illegal state exception while updating status messages");
229 }
230 messageListAdapter.notifyDataSetChanged();
231 int pos = Math.max(getIndexOf(uuid, messageList), 0);
232 binding.messagesView.setSelectionFromTop(pos, pxOffset);
233 if (messageLoaderToast != null) {
234 messageLoaderToast.cancel();
235 }
236 conversation.messagesLoaded.set(true);
237 });
238 }
239
240 @Override
241 public void informUser(final int resId) {
242
243 getActivity().runOnUiThread(() -> {
244 if (messageLoaderToast != null) {
245 messageLoaderToast.cancel();
246 }
247 if (ConversationFragment.this.conversation != conversation) {
248 return;
249 }
250 messageLoaderToast = Toast.makeText(view.getContext(), resId, Toast.LENGTH_LONG);
251 messageLoaderToast.show();
252 });
253
254 }
255 });
256
257 }
258 }
259 }
260 };
261
262 private EditMessage.OnCommitContentListener mEditorContentListener = new EditMessage.OnCommitContentListener() {
263 @Override
264 public boolean onCommitContent(InputContentInfoCompat inputContentInfo, int flags, Bundle opts, String[] contentMimeTypes) {
265 // try to get permission to read the image, if applicable
266 if ((flags & InputConnectionCompat.INPUT_CONTENT_GRANT_READ_URI_PERMISSION) != 0) {
267 try {
268 inputContentInfo.requestPermission();
269 } catch (Exception e) {
270 Log.e(Config.LOGTAG, "InputContentInfoCompat#requestPermission() failed.", e);
271 Toast.makeText(getActivity(), activity.getString(R.string.no_permission_to_access_x, inputContentInfo.getDescription()), Toast.LENGTH_LONG
272 ).show();
273 return false;
274 }
275 }
276 if (hasStoragePermission(REQUEST_ADD_EDITOR_CONTENT)) {
277 attachImageToConversation(inputContentInfo.getContentUri());
278 } else {
279 mPendingEditorContent = inputContentInfo.getContentUri();
280 }
281 return true;
282 }
283 };
284 private Message selectedMessage;
285 private OnClickListener mEnableAccountListener = new OnClickListener() {
286 @Override
287 public void onClick(View v) {
288 final Account account = conversation == null ? null : conversation.getAccount();
289 if (account != null) {
290 account.setOption(Account.OPTION_DISABLED, false);
291 activity.xmppConnectionService.updateAccount(account);
292 }
293 }
294 };
295 private OnClickListener mUnblockClickListener = new OnClickListener() {
296 @Override
297 public void onClick(final View v) {
298 v.post(() -> v.setVisibility(View.INVISIBLE));
299 if (conversation.isDomainBlocked()) {
300 BlockContactDialog.show(activity, conversation);
301 } else {
302 unblockConversation(conversation);
303 }
304 }
305 };
306 private OnClickListener mBlockClickListener = this::showBlockSubmenu;
307 private OnClickListener mAddBackClickListener = new OnClickListener() {
308
309 @Override
310 public void onClick(View v) {
311 final Contact contact = conversation == null ? null : conversation.getContact();
312 if (contact != null) {
313 activity.xmppConnectionService.createContact(contact);
314 activity.switchToContactDetails(contact);
315 }
316 }
317 };
318 private View.OnLongClickListener mLongPressBlockListener = this::showBlockSubmenu;
319 private OnClickListener mAllowPresenceSubscription = new OnClickListener() {
320 @Override
321 public void onClick(View v) {
322 final Contact contact = conversation == null ? null : conversation.getContact();
323 if (contact != null) {
324 activity.xmppConnectionService.sendPresencePacket(contact.getAccount(),
325 activity.xmppConnectionService.getPresenceGenerator()
326 .sendPresenceUpdatesTo(contact));
327 hideSnackbar();
328 }
329 }
330 };
331
332 protected OnClickListener clickToDecryptListener = new OnClickListener() {
333
334 @Override
335 public void onClick(View v) {
336 PendingIntent pendingIntent = conversation.getAccount().getPgpDecryptionService().getPendingIntent();
337 if (pendingIntent != null) {
338 try {
339 getActivity().startIntentSenderForResult(pendingIntent.getIntentSender(),
340 REQUEST_DECRYPT_PGP,
341 null,
342 0,
343 0,
344 0);
345 } catch (SendIntentException e) {
346 Toast.makeText(getActivity(), R.string.unable_to_connect_to_keychain, Toast.LENGTH_SHORT).show();
347 conversation.getAccount().getPgpDecryptionService().continueDecryption(true);
348 }
349 }
350 updateSnackBar(conversation);
351 }
352 };
353 private AtomicBoolean mSendingPgpMessage = new AtomicBoolean(false);
354 private OnEditorActionListener mEditorActionListener = (v, actionId, event) -> {
355 if (actionId == EditorInfo.IME_ACTION_SEND) {
356 InputMethodManager imm = (InputMethodManager) v.getContext()
357 .getSystemService(Context.INPUT_METHOD_SERVICE);
358 if (imm.isFullscreenMode()) {
359 imm.hideSoftInputFromWindow(v.getWindowToken(), 0);
360 }
361 sendMessage();
362 return true;
363 } else {
364 return false;
365 }
366 };
367 private OnClickListener mSendButtonListener = new OnClickListener() {
368
369 @Override
370 public void onClick(View v) {
371 Object tag = v.getTag();
372 if (tag instanceof SendButtonAction) {
373 SendButtonAction action = (SendButtonAction) tag;
374 switch (action) {
375 case TAKE_PHOTO:
376 case RECORD_VIDEO:
377 case SEND_LOCATION:
378 case RECORD_VOICE:
379 case CHOOSE_PICTURE:
380 attachFile(action.toChoice());
381 break;
382 case CANCEL:
383 if (conversation != null) {
384 if (conversation.setCorrectingMessage(null)) {
385 binding.textinput.setText("");
386 binding.textinput.append(conversation.getDraftMessage());
387 conversation.setDraftMessage(null);
388 } else if (conversation.getMode() == Conversation.MODE_MULTI) {
389 conversation.setNextCounterpart(null);
390 }
391 updateChatMsgHint();
392 updateSendButton();
393 updateEditablity();
394 }
395 break;
396 default:
397 sendMessage();
398 }
399 } else {
400 sendMessage();
401 }
402 }
403 };
404 private int completionIndex = 0;
405 private int lastCompletionLength = 0;
406 private String incomplete;
407 private int lastCompletionCursor;
408 private boolean firstWord = false;
409 private Message mPendingDownloadableMessage;
410
411 private int getIndexOf(String uuid, List<Message> messages) {
412 if (uuid == null) {
413 return messages.size() - 1;
414 }
415 for (int i = 0; i < messages.size(); ++i) {
416 if (uuid.equals(messages.get(i).getUuid())) {
417 return i;
418 } else {
419 Message next = messages.get(i);
420 while (next != null && next.wasMergedIntoPrevious()) {
421 if (uuid.equals(next.getUuid())) {
422 return i;
423 }
424 next = next.next();
425 }
426
427 }
428 }
429 return -1;
430 }
431
432 public Pair<Integer, Integer> getScrollPosition() {
433 if (this.binding.messagesView.getCount() == 0 ||
434 this.binding.messagesView.getLastVisiblePosition() == this.binding.messagesView.getCount() - 1) {
435 return null;
436 } else {
437 final int pos = this.binding.messagesView.getFirstVisiblePosition();
438 final View view = this.binding.messagesView.getChildAt(0);
439 if (view == null) {
440 return null;
441 } else {
442 return new Pair<>(pos, view.getTop());
443 }
444 }
445 }
446
447 public void setScrollPosition(Pair<Integer, Integer> scrollPosition) {
448 if (scrollPosition != null) {
449 this.binding.messagesView.setSelectionFromTop(scrollPosition.first, scrollPosition.second);
450 }
451 }
452
453
454 private void attachLocationToConversation(Conversation conversation, Uri uri) {
455 if (conversation == null) {
456 return;
457 }
458 activity.xmppConnectionService.attachLocationToConversation(conversation, uri, new UiCallback<Message>() {
459
460 @Override
461 public void success(Message message) {
462 activity.xmppConnectionService.sendMessage(message);
463 }
464
465 @Override
466 public void error(int errorCode, Message object) {
467
468 }
469
470 @Override
471 public void userInputRequried(PendingIntent pi, Message object) {
472
473 }
474 });
475 }
476
477 private void attachFileToConversation(Conversation conversation, Uri uri) {
478 if (conversation == null) {
479 return;
480 }
481 final Toast prepareFileToast = Toast.makeText(getActivity(), getText(R.string.preparing_file), Toast.LENGTH_LONG);
482 prepareFileToast.show();
483 activity.delegateUriPermissionsToService(uri);
484 activity.xmppConnectionService.attachFileToConversation(conversation, uri, new UiInformableCallback<Message>() {
485 @Override
486 public void inform(final String text) {
487 hidePrepareFileToast(prepareFileToast);
488 getActivity().runOnUiThread(() -> activity.replaceToast(text));
489 }
490
491 @Override
492 public void success(Message message) {
493 getActivity().runOnUiThread(() -> activity.hideToast());
494 hidePrepareFileToast(prepareFileToast);
495 activity.xmppConnectionService.sendMessage(message);
496 }
497
498 @Override
499 public void error(final int errorCode, Message message) {
500 hidePrepareFileToast(prepareFileToast);
501 getActivity().runOnUiThread(() -> activity.replaceToast(getString(errorCode)));
502
503 }
504
505 @Override
506 public void userInputRequried(PendingIntent pi, Message message) {
507 hidePrepareFileToast(prepareFileToast);
508 }
509 });
510 }
511
512 public void attachImageToConversation(Uri uri) {
513 this.attachImageToConversation(conversation, uri);
514 }
515
516 private void attachImageToConversation(Conversation conversation, Uri uri) {
517 if (conversation == null) {
518 return;
519 }
520 final Toast prepareFileToast = Toast.makeText(getActivity(), getText(R.string.preparing_image), Toast.LENGTH_LONG);
521 prepareFileToast.show();
522 activity.delegateUriPermissionsToService(uri);
523 activity.xmppConnectionService.attachImageToConversation(conversation, uri,
524 new UiCallback<Message>() {
525
526 @Override
527 public void userInputRequried(PendingIntent pi, Message object) {
528 hidePrepareFileToast(prepareFileToast);
529 }
530
531 @Override
532 public void success(Message message) {
533 hidePrepareFileToast(prepareFileToast);
534 activity.xmppConnectionService.sendMessage(message);
535 }
536
537 @Override
538 public void error(final int error, Message message) {
539 hidePrepareFileToast(prepareFileToast);
540 activity.runOnUiThread(() -> activity.replaceToast(getString(error)));
541 }
542 });
543 }
544
545 private void hidePrepareFileToast(final Toast prepareFileToast) {
546 if (prepareFileToast != null) {
547 activity.runOnUiThread(prepareFileToast::cancel);
548 }
549 }
550
551 private void sendMessage() {
552 final String body = this.binding.textinput.getText().toString();
553 final Conversation conversation = this.conversation;
554 if (body.length() == 0 || conversation == null) {
555 return;
556 }
557 final Message message;
558 if (conversation.getCorrectingMessage() == null) {
559 message = new Message(conversation, body, conversation.getNextEncryption());
560 if (conversation.getMode() == Conversation.MODE_MULTI) {
561 final Jid nextCounterpart = conversation.getNextCounterpart();
562 if (nextCounterpart != null) {
563 message.setCounterpart(nextCounterpart);
564 message.setTrueCounterpart(conversation.getMucOptions().getTrueCounterpart(nextCounterpart));
565 message.setType(Message.TYPE_PRIVATE);
566 }
567 }
568 } else {
569 message = conversation.getCorrectingMessage();
570 message.setBody(body);
571 message.setEdited(message.getUuid());
572 message.setUuid(UUID.randomUUID().toString());
573 }
574 switch (message.getConversation().getNextEncryption()) {
575 case Message.ENCRYPTION_PGP:
576 sendPgpMessage(message);
577 break;
578 case Message.ENCRYPTION_AXOLOTL:
579 if (!trustKeysIfNeeded(REQUEST_TRUST_KEYS_TEXT)) {
580 sendAxolotlMessage(message);
581 }
582 break;
583 default:
584 sendPlainTextMessage(message);
585 }
586 }
587
588 protected boolean trustKeysIfNeeded(int requestCode) {
589 return trustKeysIfNeeded(requestCode, ATTACHMENT_CHOICE_INVALID);
590 }
591
592 protected boolean trustKeysIfNeeded(int requestCode, int attachmentChoice) {
593 AxolotlService axolotlService = conversation.getAccount().getAxolotlService();
594 final List<Jid> targets = axolotlService.getCryptoTargets(conversation);
595 boolean hasUnaccepted = !conversation.getAcceptedCryptoTargets().containsAll(targets);
596 boolean hasUndecidedOwn = !axolotlService.getKeysWithTrust(FingerprintStatus.createActiveUndecided()).isEmpty();
597 boolean hasUndecidedContacts = !axolotlService.getKeysWithTrust(FingerprintStatus.createActiveUndecided(), targets).isEmpty();
598 boolean hasPendingKeys = !axolotlService.findDevicesWithoutSession(conversation).isEmpty();
599 boolean hasNoTrustedKeys = axolotlService.anyTargetHasNoTrustedKeys(targets);
600 if (hasUndecidedOwn || hasUndecidedContacts || hasPendingKeys || hasNoTrustedKeys || hasUnaccepted) {
601 axolotlService.createSessionsIfNeeded(conversation);
602 Intent intent = new Intent(getActivity(), TrustKeysActivity.class);
603 String[] contacts = new String[targets.size()];
604 for (int i = 0; i < contacts.length; ++i) {
605 contacts[i] = targets.get(i).toString();
606 }
607 intent.putExtra("contacts", contacts);
608 intent.putExtra(EXTRA_ACCOUNT, conversation.getAccount().getJid().toBareJid().toString());
609 intent.putExtra("choice", attachmentChoice);
610 intent.putExtra("conversation", conversation.getUuid());
611 startActivityForResult(intent, requestCode);
612 return true;
613 } else {
614 return false;
615 }
616 }
617
618 public void updateChatMsgHint() {
619 final boolean multi = conversation.getMode() == Conversation.MODE_MULTI;
620 if (conversation.getCorrectingMessage() != null) {
621 this.binding.textinput.setHint(R.string.send_corrected_message);
622 } else if (multi && conversation.getNextCounterpart() != null) {
623 this.binding.textinput.setHint(getString(
624 R.string.send_private_message_to,
625 conversation.getNextCounterpart().getResourcepart()));
626 } else if (multi && !conversation.getMucOptions().participating()) {
627 this.binding.textinput.setHint(R.string.you_are_not_participating);
628 } else {
629 this.binding.textinput.setHint(UIHelper.getMessageHint(getActivity(), conversation));
630 getActivity().invalidateOptionsMenu();
631 }
632 }
633
634 public void setupIme() {
635 this.binding.textinput.refreshIme();
636 }
637
638 private void handleActivityResult(ActivityResult activityResult) {
639 if (activityResult.resultCode == Activity.RESULT_OK) {
640 handlePositiveActivityResult(activityResult.requestCode, activityResult.data);
641 } else {
642 handleNegativeActivityResult(activityResult.requestCode);
643 }
644 }
645
646 private void handlePositiveActivityResult(int requestCode, final Intent data) {
647 switch (requestCode) {
648 case REQUEST_TRUST_KEYS_TEXT:
649 final String body = this.binding.textinput.getText().toString();
650 Message message = new Message(conversation, body, conversation.getNextEncryption());
651 sendAxolotlMessage(message);
652 break;
653 case REQUEST_TRUST_KEYS_MENU:
654 int choice = data.getIntExtra("choice", ATTACHMENT_CHOICE_INVALID);
655 selectPresenceToAttachFile(choice);
656 break;
657 case REQUEST_CHOOSE_PGP_ID:
658 long id = data.getLongExtra(OpenPgpApi.EXTRA_SIGN_KEY_ID, 0);
659 if (id != 0) {
660 conversation.getAccount().setPgpSignId(id);
661 activity.announcePgp(conversation.getAccount(), null, null, activity.onOpenPGPKeyPublished);
662 } else {
663 activity.choosePgpSignId(conversation.getAccount());
664 }
665 break;
666 case REQUEST_ANNOUNCE_PGP:
667 activity.announcePgp(conversation.getAccount(), conversation, data, activity.onOpenPGPKeyPublished);
668 break;
669 case ATTACHMENT_CHOICE_CHOOSE_IMAGE:
670 List<Uri> imageUris = AttachmentTool.extractUriFromIntent(data);
671 for (Iterator<Uri> i = imageUris.iterator(); i.hasNext(); i.remove()) {
672 Log.d(Config.LOGTAG, "ConversationsActivity.onActivityResult() - attaching image to conversations. CHOOSE_IMAGE");
673 attachImageToConversation(conversation, i.next());
674 }
675 break;
676 case ATTACHMENT_CHOICE_TAKE_PHOTO:
677 Uri takePhotoUri = pendingTakePhotoUri.pop();
678 if (takePhotoUri != null) {
679 attachImageToConversation(conversation, takePhotoUri);
680 } else {
681 Log.d(Config.LOGTAG,"lost take photo uri. unable to to attach");
682 }
683 break;
684 case ATTACHMENT_CHOICE_CHOOSE_FILE:
685 case ATTACHMENT_CHOICE_RECORD_VIDEO:
686 case ATTACHMENT_CHOICE_RECORD_VOICE:
687 final List<Uri> fileUris = AttachmentTool.extractUriFromIntent(data);
688 final PresenceSelector.OnPresenceSelected callback = () -> {
689 for (Iterator<Uri> i = fileUris.iterator(); i.hasNext(); i.remove()) {
690 Log.d(Config.LOGTAG, "ConversationsActivity.onActivityResult() - attaching file to conversations. CHOOSE_FILE/RECORD_VOICE/RECORD_VIDEO");
691 attachFileToConversation(conversation, i.next());
692 }
693 };
694 if (conversation == null || conversation.getMode() == Conversation.MODE_MULTI || FileBackend.allFilesUnderSize(getActivity(), fileUris, getMaxHttpUploadSize(conversation))) {
695 callback.onPresenceSelected();
696 } else {
697 activity.selectPresence(conversation, callback);
698 }
699 break;
700 case ATTACHMENT_CHOICE_LOCATION:
701 double latitude = data.getDoubleExtra("latitude", 0);
702 double longitude = data.getDoubleExtra("longitude", 0);
703 Uri geo = Uri.parse("geo:" + String.valueOf(latitude) + "," + String.valueOf(longitude));
704 attachLocationToConversation(conversation, geo);
705 break;
706 }
707 }
708
709 private void handleNegativeActivityResult(int requestCode) {
710 switch (requestCode) {
711 //nothing to do for now
712 }
713 }
714
715 @Override
716 public void onActivityResult(int requestCode, int resultCode, final Intent data) {
717 super.onActivityResult(requestCode, resultCode, data);
718 ActivityResult activityResult = ActivityResult.of(requestCode, resultCode, data);
719 if (activity != null && activity.xmppConnectionService != null) {
720 handleActivityResult(activityResult);
721 } else {
722 this.postponedActivityResult.push(activityResult);
723 }
724 }
725
726 public void unblockConversation(final Blockable conversation) {
727 activity.xmppConnectionService.sendUnblockRequest(conversation);
728 }
729
730 @Override
731 public void onAttach(Activity activity) {
732 super.onAttach(activity);
733 Log.d(Config.LOGTAG, "ConversationFragment.onAttach()");
734 if (activity instanceof ConversationActivity) {
735 this.activity = (ConversationActivity) activity;
736 } else {
737 throw new IllegalStateException("Trying to attach fragment to activity that is not the ConversationActivity");
738 }
739 }
740
741 @Override
742 public void onDetach() {
743 super.onDetach();
744 this.activity = null;
745 }
746
747 @Override
748 public void onCreate(Bundle savedInstanceState) {
749 super.onCreate(savedInstanceState);
750 setHasOptionsMenu(true);
751 }
752
753
754 @Override
755 public void onCreateOptionsMenu(Menu menu, MenuInflater menuInflater) {
756 menuInflater.inflate(R.menu.fragment_conversation, menu);
757 final MenuItem menuMucDetails = menu.findItem(R.id.action_muc_details);
758 final MenuItem menuContactDetails = menu.findItem(R.id.action_contact_details);
759 final MenuItem menuInviteContact = menu.findItem(R.id.action_invite);
760 final MenuItem menuMute = menu.findItem(R.id.action_mute);
761 final MenuItem menuUnmute = menu.findItem(R.id.action_unmute);
762
763
764 if (conversation != null) {
765 if (conversation.getMode() == Conversation.MODE_MULTI) {
766 menuContactDetails.setVisible(false);
767 menuInviteContact.setVisible(conversation.getMucOptions().canInvite());
768 } else {
769 menuContactDetails.setVisible(!this.conversation.withSelf());
770 menuMucDetails.setVisible(false);
771 final XmppConnectionService service = activity.xmppConnectionService;
772 menuInviteContact.setVisible(service != null && service.findConferenceServer(conversation.getAccount()) != null);
773 }
774 if (conversation.isMuted()) {
775 menuMute.setVisible(false);
776 } else {
777 menuUnmute.setVisible(false);
778 }
779 ConversationMenuConfigurator.configureAttachmentMenu(conversation, menu);
780 ConversationMenuConfigurator.configureEncryptionMenu(conversation, menu);
781 }
782 super.onCreateOptionsMenu(menu, menuInflater);
783 }
784
785 @Override
786 public View onCreateView(final LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
787 this.binding = DataBindingUtil.inflate(inflater, R.layout.fragment_conversation, container, false);
788 binding.getRoot().setOnClickListener(null); //TODO why the fuck did we do this?
789
790 binding.textinput.addTextChangedListener(new StylingHelper.MessageEditorStyler(binding.textinput));
791
792 binding.textinput.setOnEditorActionListener(mEditorActionListener);
793 binding.textinput.setRichContentListener(new String[]{"image/*"}, mEditorContentListener);
794
795 binding.textSendButton.setOnClickListener(this.mSendButtonListener);
796
797 binding.messagesView.setOnScrollListener(mOnScrollListener);
798 binding.messagesView.setTranscriptMode(ListView.TRANSCRIPT_MODE_NORMAL);
799 messageListAdapter = new MessageAdapter((XmppActivity) getActivity(), this.messageList);
800 messageListAdapter.setOnContactPictureClicked(message -> {
801 final boolean received = message.getStatus() <= Message.STATUS_RECEIVED;
802 if (received) {
803 if (message.getConversation().getMode() == Conversation.MODE_MULTI) {
804 Jid user = message.getCounterpart();
805 if (user != null && !user.isBareJid()) {
806 if (!message.getConversation().getMucOptions().isUserInRoom(user)) {
807 Toast.makeText(getActivity(), activity.getString(R.string.user_has_left_conference, user.getResourcepart()), Toast.LENGTH_SHORT).show();
808 }
809 highlightInConference(user.getResourcepart());
810 }
811 return;
812 } else {
813 if (!message.getContact().isSelf()) {
814 String fingerprint;
815 if (message.getEncryption() == Message.ENCRYPTION_PGP
816 || message.getEncryption() == Message.ENCRYPTION_DECRYPTED) {
817 fingerprint = "pgp";
818 } else {
819 fingerprint = message.getFingerprint();
820 }
821 activity.switchToContactDetails(message.getContact(), fingerprint);
822 return;
823 }
824 }
825 }
826 Account account = message.getConversation().getAccount();
827 Intent intent;
828 if (activity.manuallyChangePresence() && !received) {
829 intent = new Intent(activity, SetPresenceActivity.class);
830 intent.putExtra(EXTRA_ACCOUNT, account.getJid().toBareJid().toString());
831 } else {
832 intent = new Intent(activity, EditAccountActivity.class);
833 intent.putExtra("jid", account.getJid().toBareJid().toString());
834 String fingerprint;
835 if (message.getEncryption() == Message.ENCRYPTION_PGP
836 || message.getEncryption() == Message.ENCRYPTION_DECRYPTED) {
837 fingerprint = "pgp";
838 } else {
839 fingerprint = message.getFingerprint();
840 }
841 intent.putExtra("fingerprint", fingerprint);
842 }
843 startActivity(intent);
844 });
845 messageListAdapter.setOnContactPictureLongClicked(message -> {
846 if (message.getStatus() <= Message.STATUS_RECEIVED) {
847 if (message.getConversation().getMode() == Conversation.MODE_MULTI) {
848 final MucOptions mucOptions = conversation.getMucOptions();
849 if (!mucOptions.allowPm()) {
850 Toast.makeText(getActivity(), R.string.private_messages_are_disabled, Toast.LENGTH_SHORT).show();
851 return;
852 }
853 Jid user = message.getCounterpart();
854 if (user != null && !user.isBareJid()) {
855 if (mucOptions.isUserInRoom(user)) {
856 privateMessageWith(user);
857 } else {
858 Toast.makeText(getActivity(), activity.getString(R.string.user_has_left_conference, user.getResourcepart()), Toast.LENGTH_SHORT).show();
859 }
860 }
861 }
862 } else {
863 activity.showQrCode(conversation.getAccount().getShareableUri());
864 }
865 });
866 messageListAdapter.setOnQuoteListener(this::quoteText);
867 binding.messagesView.setAdapter(messageListAdapter);
868
869 registerForContextMenu(binding.messagesView);
870
871 return binding.getRoot();
872 }
873
874 private void quoteText(String text) {
875 if (binding.textinput.isEnabled()) {
876 text = text.replaceAll("(\n *){2,}", "\n").replaceAll("(^|\n)", "$1> ").replaceAll("\n$", "");
877 Editable editable = binding.textinput.getEditableText();
878 int position = binding.textinput.getSelectionEnd();
879 if (position == -1) position = editable.length();
880 if (position > 0 && editable.charAt(position - 1) != '\n') {
881 editable.insert(position++, "\n");
882 }
883 editable.insert(position, text);
884 position += text.length();
885 editable.insert(position++, "\n");
886 if (position < editable.length() && editable.charAt(position) != '\n') {
887 editable.insert(position, "\n");
888 }
889 binding.textinput.setSelection(position);
890 binding.textinput.requestFocus();
891 InputMethodManager inputMethodManager = (InputMethodManager) getActivity().getSystemService(Context.INPUT_METHOD_SERVICE);
892 if (inputMethodManager != null) {
893 inputMethodManager.showSoftInput(binding.textinput, InputMethodManager.SHOW_IMPLICIT);
894 }
895 }
896 }
897
898 private void quoteMessage(Message message) {
899 quoteText(MessageUtils.prepareQuote(message));
900 }
901
902 @Override
903 public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
904 synchronized (this.messageList) {
905 super.onCreateContextMenu(menu, v, menuInfo);
906 AdapterView.AdapterContextMenuInfo acmi = (AdapterContextMenuInfo) menuInfo;
907 this.selectedMessage = this.messageList.get(acmi.position);
908 populateContextMenu(menu);
909 }
910 }
911
912 private void populateContextMenu(ContextMenu menu) {
913 final Message m = this.selectedMessage;
914 final Transferable t = m.getTransferable();
915 Message relevantForCorrection = m;
916 while (relevantForCorrection.mergeable(relevantForCorrection.next())) {
917 relevantForCorrection = relevantForCorrection.next();
918 }
919 if (m.getType() != Message.TYPE_STATUS) {
920 final boolean treatAsFile = m.getType() != Message.TYPE_TEXT
921 && m.getType() != Message.TYPE_PRIVATE
922 && t == null;
923 final boolean encrypted = m.getEncryption() == Message.ENCRYPTION_DECRYPTION_FAILED
924 || m.getEncryption() == Message.ENCRYPTION_PGP;
925 activity.getMenuInflater().inflate(R.menu.message_context, menu);
926 menu.setHeaderTitle(R.string.message_options);
927 MenuItem copyMessage = menu.findItem(R.id.copy_message);
928 MenuItem quoteMessage = menu.findItem(R.id.quote_message);
929 MenuItem retryDecryption = menu.findItem(R.id.retry_decryption);
930 MenuItem correctMessage = menu.findItem(R.id.correct_message);
931 MenuItem shareWith = menu.findItem(R.id.share_with);
932 MenuItem sendAgain = menu.findItem(R.id.send_again);
933 MenuItem copyUrl = menu.findItem(R.id.copy_url);
934 MenuItem downloadFile = menu.findItem(R.id.download_file);
935 MenuItem cancelTransmission = menu.findItem(R.id.cancel_transmission);
936 MenuItem deleteFile = menu.findItem(R.id.delete_file);
937 MenuItem showErrorMessage = menu.findItem(R.id.show_error_message);
938 if (!treatAsFile && !encrypted && !m.isGeoUri() && !m.treatAsDownloadable()) {
939 copyMessage.setVisible(true);
940 quoteMessage.setVisible(MessageUtils.prepareQuote(m).length() > 0);
941 }
942 if (m.getEncryption() == Message.ENCRYPTION_DECRYPTION_FAILED) {
943 retryDecryption.setVisible(true);
944 }
945 if (relevantForCorrection.getType() == Message.TYPE_TEXT
946 && relevantForCorrection.isLastCorrectableMessage()
947 && (m.getConversation().getMucOptions().nonanonymous() || m.getConversation().getMode() == Conversation.MODE_SINGLE)) {
948 correctMessage.setVisible(true);
949 }
950 if (treatAsFile || (m.getType() == Message.TYPE_TEXT && !m.treatAsDownloadable())) {
951 shareWith.setVisible(true);
952 }
953 if (m.getStatus() == Message.STATUS_SEND_FAILED) {
954 sendAgain.setVisible(true);
955 }
956 if (m.hasFileOnRemoteHost()
957 || m.isGeoUri()
958 || m.treatAsDownloadable()
959 || (t != null && t instanceof HttpDownloadConnection)) {
960 copyUrl.setVisible(true);
961 }
962 if ((m.isFileOrImage() && t instanceof TransferablePlaceholder && m.hasFileOnRemoteHost())) {
963 downloadFile.setVisible(true);
964 downloadFile.setTitle(activity.getString(R.string.download_x_file, UIHelper.getFileDescriptionString(activity, m)));
965 }
966 boolean waitingOfferedSending = m.getStatus() == Message.STATUS_WAITING
967 || m.getStatus() == Message.STATUS_UNSEND
968 || m.getStatus() == Message.STATUS_OFFERED;
969 if ((t != null && !(t instanceof TransferablePlaceholder)) || waitingOfferedSending && m.needsUploading()) {
970 cancelTransmission.setVisible(true);
971 }
972 if (treatAsFile) {
973 String path = m.getRelativeFilePath();
974 if (path == null || !path.startsWith("/")) {
975 deleteFile.setVisible(true);
976 deleteFile.setTitle(activity.getString(R.string.delete_x_file, UIHelper.getFileDescriptionString(activity, m)));
977 }
978 }
979 if (m.getStatus() == Message.STATUS_SEND_FAILED && m.getErrorMessage() != null) {
980 showErrorMessage.setVisible(true);
981 }
982 }
983 }
984
985 @Override
986 public boolean onContextItemSelected(MenuItem item) {
987 switch (item.getItemId()) {
988 case R.id.share_with:
989 shareWith(selectedMessage);
990 return true;
991 case R.id.correct_message:
992 correctMessage(selectedMessage);
993 return true;
994 case R.id.copy_message:
995 copyMessage(selectedMessage);
996 return true;
997 case R.id.quote_message:
998 quoteMessage(selectedMessage);
999 return true;
1000 case R.id.send_again:
1001 resendMessage(selectedMessage);
1002 return true;
1003 case R.id.copy_url:
1004 copyUrl(selectedMessage);
1005 return true;
1006 case R.id.download_file:
1007 startDownloadable(selectedMessage);
1008 return true;
1009 case R.id.cancel_transmission:
1010 cancelTransmission(selectedMessage);
1011 return true;
1012 case R.id.retry_decryption:
1013 retryDecryption(selectedMessage);
1014 return true;
1015 case R.id.delete_file:
1016 deleteFile(selectedMessage);
1017 return true;
1018 case R.id.show_error_message:
1019 showErrorMessage(selectedMessage);
1020 return true;
1021 default:
1022 return super.onContextItemSelected(item);
1023 }
1024 }
1025
1026 @Override
1027 public boolean onOptionsItemSelected(final MenuItem item) {
1028 if (conversation == null) {
1029 return super.onOptionsItemSelected(item);
1030 }
1031 switch (item.getItemId()) {
1032 case R.id.encryption_choice_axolotl:
1033 case R.id.encryption_choice_pgp:
1034 case R.id.encryption_choice_none:
1035 handleEncryptionSelection(item);
1036 break;
1037 case R.id.attach_choose_picture:
1038 case R.id.attach_take_picture:
1039 case R.id.attach_record_video:
1040 case R.id.attach_choose_file:
1041 case R.id.attach_record_voice:
1042 case R.id.attach_location:
1043 handleAttachmentSelection(item);
1044 break;
1045 case R.id.action_archive:
1046 activity.xmppConnectionService.archiveConversation(conversation);
1047 activity.onConversationArchived(conversation);
1048 break;
1049 case R.id.action_contact_details:
1050 activity.switchToContactDetails(conversation.getContact());
1051 break;
1052 case R.id.action_muc_details:
1053 Intent intent = new Intent(getActivity(), ConferenceDetailsActivity.class);
1054 intent.setAction(ConferenceDetailsActivity.ACTION_VIEW_MUC);
1055 intent.putExtra("uuid", conversation.getUuid());
1056 startActivity(intent);
1057 break;
1058 case R.id.action_invite:
1059 activity.inviteToConversation(conversation);
1060 break;
1061 case R.id.action_clear_history:
1062 clearHistoryDialog(conversation);
1063 break;
1064 case R.id.action_mute:
1065 muteConversationDialog(conversation);
1066 break;
1067 case R.id.action_unmute:
1068 unmuteConversation(conversation);
1069 break;
1070 case R.id.action_block:
1071 case R.id.action_unblock:
1072 final Activity activity = getActivity();
1073 if (activity instanceof XmppActivity) {
1074 BlockContactDialog.show((XmppActivity) activity, conversation);
1075 }
1076 break;
1077 default:
1078 break;
1079 }
1080 return super.onOptionsItemSelected(item);
1081 }
1082
1083 private void handleAttachmentSelection(MenuItem item) {
1084 switch (item.getItemId()) {
1085 case R.id.attach_choose_picture:
1086 attachFile(ATTACHMENT_CHOICE_CHOOSE_IMAGE);
1087 break;
1088 case R.id.attach_take_picture:
1089 attachFile(ATTACHMENT_CHOICE_TAKE_PHOTO);
1090 break;
1091 case R.id.attach_record_video:
1092 attachFile(ATTACHMENT_CHOICE_RECORD_VIDEO);
1093 break;
1094 case R.id.attach_choose_file:
1095 attachFile(ATTACHMENT_CHOICE_CHOOSE_FILE);
1096 break;
1097 case R.id.attach_record_voice:
1098 attachFile(ATTACHMENT_CHOICE_RECORD_VOICE);
1099 break;
1100 case R.id.attach_location:
1101 attachFile(ATTACHMENT_CHOICE_LOCATION);
1102 break;
1103 }
1104 }
1105
1106 private void handleEncryptionSelection(MenuItem item) {
1107 if (conversation == null) {
1108 return;
1109 }
1110 switch (item.getItemId()) {
1111 case R.id.encryption_choice_none:
1112 conversation.setNextEncryption(Message.ENCRYPTION_NONE);
1113 item.setChecked(true);
1114 break;
1115 case R.id.encryption_choice_pgp:
1116 if (activity.hasPgp()) {
1117 if (conversation.getAccount().getPgpSignature() != null) {
1118 conversation.setNextEncryption(Message.ENCRYPTION_PGP);
1119 item.setChecked(true);
1120 } else {
1121 activity.announcePgp(conversation.getAccount(), conversation, null, activity.onOpenPGPKeyPublished);
1122 }
1123 } else {
1124 activity.showInstallPgpDialog();
1125 }
1126 break;
1127 case R.id.encryption_choice_axolotl:
1128 Log.d(Config.LOGTAG, AxolotlService.getLogprefix(conversation.getAccount())
1129 + "Enabled axolotl for Contact " + conversation.getContact().getJid());
1130 conversation.setNextEncryption(Message.ENCRYPTION_AXOLOTL);
1131 item.setChecked(true);
1132 break;
1133 default:
1134 conversation.setNextEncryption(Message.ENCRYPTION_NONE);
1135 break;
1136 }
1137 activity.xmppConnectionService.updateConversation(conversation);
1138 updateChatMsgHint();
1139 getActivity().invalidateOptionsMenu();
1140 activity.refreshUi();
1141 }
1142
1143 public void attachFile(final int attachmentChoice) {
1144 if (attachmentChoice != ATTACHMENT_CHOICE_LOCATION) {
1145 if (!Config.ONLY_INTERNAL_STORAGE && !hasStoragePermission(attachmentChoice)) {
1146 return;
1147 }
1148 }
1149 try {
1150 activity.getPreferences().edit()
1151 .putString(RECENTLY_USED_QUICK_ACTION, SendButtonAction.of(attachmentChoice).toString())
1152 .apply();
1153 } catch (IllegalArgumentException e) {
1154 //just do not save
1155 }
1156 final int encryption = conversation.getNextEncryption();
1157 final int mode = conversation.getMode();
1158 if (encryption == Message.ENCRYPTION_PGP) {
1159 if (activity.hasPgp()) {
1160 if (mode == Conversation.MODE_SINGLE && conversation.getContact().getPgpKeyId() != 0) {
1161 activity.xmppConnectionService.getPgpEngine().hasKey(
1162 conversation.getContact(),
1163 new UiCallback<Contact>() {
1164
1165 @Override
1166 public void userInputRequried(PendingIntent pi, Contact contact) {
1167 startPendingIntent(pi, attachmentChoice);
1168 }
1169
1170 @Override
1171 public void success(Contact contact) {
1172 selectPresenceToAttachFile(attachmentChoice);
1173 }
1174
1175 @Override
1176 public void error(int error, Contact contact) {
1177 activity.replaceToast(getString(error));
1178 }
1179 });
1180 } else if (mode == Conversation.MODE_MULTI && conversation.getMucOptions().pgpKeysInUse()) {
1181 if (!conversation.getMucOptions().everybodyHasKeys()) {
1182 Toast warning = Toast.makeText(getActivity(), R.string.missing_public_keys, Toast.LENGTH_LONG);
1183 warning.setGravity(Gravity.CENTER_VERTICAL, 0, 0);
1184 warning.show();
1185 }
1186 selectPresenceToAttachFile(attachmentChoice);
1187 } else {
1188 final ConversationFragment fragment = (ConversationFragment) getFragmentManager()
1189 .findFragmentByTag("conversation");
1190 if (fragment != null) {
1191 fragment.showNoPGPKeyDialog(false, (dialog, which) -> {
1192 conversation.setNextEncryption(Message.ENCRYPTION_NONE);
1193 activity.xmppConnectionService.updateConversation(conversation);
1194 selectPresenceToAttachFile(attachmentChoice);
1195 });
1196 }
1197 }
1198 } else {
1199 activity.showInstallPgpDialog();
1200 }
1201 } else {
1202 if (encryption != Message.ENCRYPTION_AXOLOTL || !trustKeysIfNeeded(REQUEST_TRUST_KEYS_MENU, attachmentChoice)) {
1203 selectPresenceToAttachFile(attachmentChoice);
1204 }
1205 }
1206 }
1207
1208 @Override
1209 public void onRequestPermissionsResult(int requestCode, String permissions[], int[] grantResults) {
1210 if (grantResults.length > 0)
1211 if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
1212 if (requestCode == REQUEST_START_DOWNLOAD) {
1213 if (this.mPendingDownloadableMessage != null) {
1214 startDownloadable(this.mPendingDownloadableMessage);
1215 }
1216 } else if (requestCode == REQUEST_ADD_EDITOR_CONTENT) {
1217 if (this.mPendingEditorContent != null) {
1218 attachImageToConversation(this.mPendingEditorContent);
1219 }
1220 } else {
1221 attachFile(requestCode);
1222 }
1223 } else {
1224 Toast.makeText(getActivity(), R.string.no_storage_permission, Toast.LENGTH_SHORT).show();
1225 }
1226 }
1227
1228 public void startDownloadable(Message message) {
1229 if (!Config.ONLY_INTERNAL_STORAGE && !hasStoragePermission(REQUEST_START_DOWNLOAD)) {
1230 this.mPendingDownloadableMessage = message;
1231 return;
1232 }
1233 Transferable transferable = message.getTransferable();
1234 if (transferable != null) {
1235 if (!transferable.start()) {
1236 Toast.makeText(getActivity(), R.string.not_connected_try_again, Toast.LENGTH_SHORT).show();
1237 }
1238 } else if (message.treatAsDownloadable()) {
1239 activity.xmppConnectionService.getHttpConnectionManager().createNewDownloadConnection(message, true);
1240 }
1241 }
1242
1243 @SuppressLint("InflateParams")
1244 protected void clearHistoryDialog(final Conversation conversation) {
1245 AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
1246 builder.setTitle(getString(R.string.clear_conversation_history));
1247 final View dialogView = getActivity().getLayoutInflater().inflate(R.layout.dialog_clear_history, null);
1248 final CheckBox endConversationCheckBox = dialogView.findViewById(R.id.end_conversation_checkbox);
1249 builder.setView(dialogView);
1250 builder.setNegativeButton(getString(R.string.cancel), null);
1251 builder.setPositiveButton(getString(R.string.delete_messages), (dialog, which) -> {
1252 this.activity.xmppConnectionService.clearConversationHistory(conversation);
1253 if (endConversationCheckBox.isChecked()) {
1254 this.activity.xmppConnectionService.archiveConversation(conversation);
1255 this.activity.onConversationArchived(conversation);
1256 } else {
1257 activity.onConversationsListItemUpdated();
1258 refresh();
1259 }
1260 });
1261 builder.create().show();
1262 }
1263
1264 protected void muteConversationDialog(final Conversation conversation) {
1265 AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
1266 builder.setTitle(R.string.disable_notifications);
1267 final int[] durations = getResources().getIntArray(R.array.mute_options_durations);
1268 builder.setItems(R.array.mute_options_descriptions, (dialog, which) -> {
1269 final long till;
1270 if (durations[which] == -1) {
1271 till = Long.MAX_VALUE;
1272 } else {
1273 till = System.currentTimeMillis() + (durations[which] * 1000);
1274 }
1275 conversation.setMutedTill(till);
1276 activity.xmppConnectionService.updateConversation(conversation);
1277 activity.onConversationsListItemUpdated();
1278 refresh();
1279 getActivity().invalidateOptionsMenu();
1280 });
1281 builder.create().show();
1282 }
1283
1284 private boolean hasStoragePermission(int requestCode) {
1285 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
1286 if (activity.checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
1287 requestPermissions(new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, requestCode);
1288 return false;
1289 } else {
1290 return true;
1291 }
1292 } else {
1293 return true;
1294 }
1295 }
1296
1297 public void unmuteConversation(final Conversation conversation) {
1298 conversation.setMutedTill(0);
1299 this.activity.xmppConnectionService.updateConversation(conversation);
1300 this.activity.onConversationsListItemUpdated();
1301 refresh();
1302 getActivity().invalidateOptionsMenu();
1303 }
1304
1305 protected void selectPresenceToAttachFile(final int attachmentChoice) {
1306 final Account account = conversation.getAccount();
1307 final PresenceSelector.OnPresenceSelected callback = () -> {
1308 Intent intent = new Intent();
1309 boolean chooser = false;
1310 String fallbackPackageId = null;
1311 switch (attachmentChoice) {
1312 case ATTACHMENT_CHOICE_CHOOSE_IMAGE:
1313 intent.setAction(Intent.ACTION_GET_CONTENT);
1314 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
1315 intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true);
1316 }
1317 intent.setType("image/*");
1318 chooser = true;
1319 break;
1320 case ATTACHMENT_CHOICE_RECORD_VIDEO:
1321 intent.setAction(MediaStore.ACTION_VIDEO_CAPTURE);
1322 break;
1323 case ATTACHMENT_CHOICE_TAKE_PHOTO:
1324 final Uri uri = activity.xmppConnectionService.getFileBackend().getTakePhotoUri();
1325 pendingTakePhotoUri.push(uri);
1326 intent.putExtra(MediaStore.EXTRA_OUTPUT, uri);
1327 intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
1328 intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
1329 intent.setAction(MediaStore.ACTION_IMAGE_CAPTURE);
1330 break;
1331 case ATTACHMENT_CHOICE_CHOOSE_FILE:
1332 chooser = true;
1333 intent.setType("*/*");
1334 intent.addCategory(Intent.CATEGORY_OPENABLE);
1335 intent.setAction(Intent.ACTION_GET_CONTENT);
1336 break;
1337 case ATTACHMENT_CHOICE_RECORD_VOICE:
1338 intent.setAction(MediaStore.Audio.Media.RECORD_SOUND_ACTION);
1339 fallbackPackageId = "eu.siacs.conversations.voicerecorder";
1340 break;
1341 case ATTACHMENT_CHOICE_LOCATION:
1342 intent.setAction("eu.siacs.conversations.location.request");
1343 fallbackPackageId = "eu.siacs.conversations.sharelocation";
1344 break;
1345 }
1346 if (intent.resolveActivity(getActivity().getPackageManager()) != null) {
1347 if (chooser) {
1348 startActivityForResult(
1349 Intent.createChooser(intent, getString(R.string.perform_action_with)),
1350 attachmentChoice);
1351 } else {
1352 startActivityForResult(intent, attachmentChoice);
1353 }
1354 } else if (fallbackPackageId != null) {
1355 startActivity(getInstallApkIntent(fallbackPackageId));
1356 }
1357 };
1358 if (account.httpUploadAvailable() || attachmentChoice == ATTACHMENT_CHOICE_LOCATION) {
1359 conversation.setNextCounterpart(null);
1360 callback.onPresenceSelected();
1361 } else {
1362 activity.selectPresence(conversation, callback);
1363 }
1364 }
1365
1366 private Intent getInstallApkIntent(final String packageId) {
1367 Intent intent = new Intent(Intent.ACTION_VIEW);
1368 intent.setData(Uri.parse("market://details?id=" + packageId));
1369 if (intent.resolveActivity(getActivity().getPackageManager()) != null) {
1370 return intent;
1371 } else {
1372 intent.setData(Uri.parse("http://play.google.com/store/apps/details?id=" + packageId));
1373 return intent;
1374 }
1375 }
1376
1377 @Override
1378 public void onResume() {
1379 new Handler().post(() -> {
1380 final Activity activity = getActivity();
1381 if (activity == null) {
1382 return;
1383 }
1384 final PackageManager packageManager = activity.getPackageManager();
1385 ConversationMenuConfigurator.updateAttachmentAvailability(packageManager);
1386 getActivity().invalidateOptionsMenu();
1387 });
1388 super.onResume();
1389 }
1390
1391 private void showErrorMessage(final Message message) {
1392 AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
1393 builder.setTitle(R.string.error_message);
1394 builder.setMessage(message.getErrorMessage());
1395 builder.setPositiveButton(R.string.confirm, null);
1396 builder.create().show();
1397 }
1398
1399 private void shareWith(Message message) {
1400 Intent shareIntent = new Intent();
1401 shareIntent.setAction(Intent.ACTION_SEND);
1402 if (message.isGeoUri()) {
1403 shareIntent.putExtra(Intent.EXTRA_TEXT, message.getBody());
1404 shareIntent.setType("text/plain");
1405 } else if (!message.isFileOrImage()) {
1406 shareIntent.putExtra(Intent.EXTRA_TEXT, message.getMergedBody().toString());
1407 shareIntent.setType("text/plain");
1408 } else {
1409 final DownloadableFile file = activity.xmppConnectionService.getFileBackend().getFile(message);
1410 try {
1411 shareIntent.putExtra(Intent.EXTRA_STREAM, FileBackend.getUriForFile(getActivity(), file));
1412 } catch (SecurityException e) {
1413 Toast.makeText(getActivity(), activity.getString(R.string.no_permission_to_access_x, file.getAbsolutePath()), Toast.LENGTH_SHORT).show();
1414 return;
1415 }
1416 shareIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
1417 String mime = message.getMimeType();
1418 if (mime == null) {
1419 mime = "*/*";
1420 }
1421 shareIntent.setType(mime);
1422 }
1423 try {
1424 startActivity(Intent.createChooser(shareIntent, getText(R.string.share_with)));
1425 } catch (ActivityNotFoundException e) {
1426 //This should happen only on faulty androids because normally chooser is always available
1427 Toast.makeText(getActivity(), R.string.no_application_found_to_open_file, Toast.LENGTH_SHORT).show();
1428 }
1429 }
1430
1431 private void copyMessage(Message message) {
1432 if (activity.copyTextToClipboard(message.getMergedBody().toString(), R.string.message)) {
1433 Toast.makeText(getActivity(), R.string.message_copied_to_clipboard, Toast.LENGTH_SHORT).show();
1434 }
1435 }
1436
1437 private void deleteFile(Message message) {
1438 if (activity.xmppConnectionService.getFileBackend().deleteFile(message)) {
1439 message.setTransferable(new TransferablePlaceholder(Transferable.STATUS_DELETED));
1440 activity.onConversationsListItemUpdated();
1441 refresh();
1442 }
1443 }
1444
1445 private void resendMessage(final Message message) {
1446 if (message.isFileOrImage()) {
1447 DownloadableFile file = activity.xmppConnectionService.getFileBackend().getFile(message);
1448 if (file.exists()) {
1449 final Conversation conversation = message.getConversation();
1450 final XmppConnection xmppConnection = conversation.getAccount().getXmppConnection();
1451 if (!message.hasFileOnRemoteHost()
1452 && xmppConnection != null
1453 && !xmppConnection.getFeatures().httpUpload(message.getFileParams().size)) {
1454 activity.selectPresence(conversation, () -> {
1455 message.setCounterpart(conversation.getNextCounterpart());
1456 activity.xmppConnectionService.resendFailedMessages(message);
1457 });
1458 return;
1459 }
1460 } else {
1461 Toast.makeText(activity, R.string.file_deleted, Toast.LENGTH_SHORT).show();
1462 message.setTransferable(new TransferablePlaceholder(Transferable.STATUS_DELETED));
1463 activity.onConversationsListItemUpdated();
1464 refresh();
1465 return;
1466 }
1467 }
1468 activity.xmppConnectionService.resendFailedMessages(message);
1469 }
1470
1471 private void copyUrl(Message message) {
1472 final String url;
1473 final int resId;
1474 if (message.isGeoUri()) {
1475 resId = R.string.location;
1476 url = message.getBody();
1477 } else if (message.hasFileOnRemoteHost()) {
1478 resId = R.string.file_url;
1479 url = message.getFileParams().url.toString();
1480 } else {
1481 url = message.getBody().trim();
1482 resId = R.string.file_url;
1483 }
1484 if (activity.copyTextToClipboard(url, resId)) {
1485 Toast.makeText(getActivity(), R.string.url_copied_to_clipboard, Toast.LENGTH_SHORT).show();
1486 }
1487 }
1488
1489 public static void downloadFile(Activity activity, Message message) {
1490 Fragment fragment = activity.getFragmentManager().findFragmentById(R.id.main_fragment);
1491 if (fragment != null && fragment instanceof ConversationFragment) {
1492 ((ConversationFragment) fragment).startDownloadable(message);
1493 return;
1494 }
1495 fragment = activity.getFragmentManager().findFragmentById(R.id.secondary_fragment);
1496 if (fragment != null && fragment instanceof ConversationFragment) {
1497 ((ConversationFragment) fragment).startDownloadable(message);
1498 }
1499 }
1500
1501 private void cancelTransmission(Message message) {
1502 Transferable transferable = message.getTransferable();
1503 if (transferable != null) {
1504 transferable.cancel();
1505 } else if (message.getStatus() != Message.STATUS_RECEIVED) {
1506 activity.xmppConnectionService.markMessage(message, Message.STATUS_SEND_FAILED);
1507 }
1508 }
1509
1510 private void retryDecryption(Message message) {
1511 message.setEncryption(Message.ENCRYPTION_PGP);
1512 activity.onConversationsListItemUpdated();
1513 refresh();
1514 conversation.getAccount().getPgpDecryptionService().decrypt(message, false);
1515 }
1516
1517 private void privateMessageWith(final Jid counterpart) {
1518 if (conversation.setOutgoingChatState(Config.DEFAULT_CHATSTATE)) {
1519 activity.xmppConnectionService.sendChatState(conversation);
1520 }
1521 this.binding.textinput.setText("");
1522 this.conversation.setNextCounterpart(counterpart);
1523 updateChatMsgHint();
1524 updateSendButton();
1525 updateEditablity();
1526 }
1527
1528 private void correctMessage(Message message) {
1529 while (message.mergeable(message.next())) {
1530 message = message.next();
1531 }
1532 this.conversation.setCorrectingMessage(message);
1533 final Editable editable = binding.textinput.getText();
1534 this.conversation.setDraftMessage(editable.toString());
1535 this.binding.textinput.setText("");
1536 this.binding.textinput.append(message.getBody());
1537
1538 }
1539
1540 private void highlightInConference(String nick) {
1541 final Editable editable = this.binding.textinput.getText();
1542 String oldString = editable.toString().trim();
1543 final int pos = this.binding.textinput.getSelectionStart();
1544 if (oldString.isEmpty() || pos == 0) {
1545 editable.insert(0, nick + ": ");
1546 } else {
1547 final char before = editable.charAt(pos - 1);
1548 final char after = editable.length() > pos ? editable.charAt(pos) : '\0';
1549 if (before == '\n') {
1550 editable.insert(pos, nick + ": ");
1551 } else {
1552 if (pos > 2 && editable.subSequence(pos - 2, pos).toString().equals(": ")) {
1553 if (NickValidityChecker.check(conversation, Arrays.asList(editable.subSequence(0, pos - 2).toString().split(", ")))) {
1554 editable.insert(pos - 2, ", " + nick);
1555 return;
1556 }
1557 }
1558 editable.insert(pos, (Character.isWhitespace(before) ? "" : " ") + nick + (Character.isWhitespace(after) ? "" : " "));
1559 if (Character.isWhitespace(after)) {
1560 this.binding.textinput.setSelection(this.binding.textinput.getSelectionStart() + 1);
1561 }
1562 }
1563 }
1564 }
1565
1566
1567 @Override
1568 public void onSaveInstanceState(Bundle outState) {
1569 super.onSaveInstanceState(outState);
1570 if (conversation != null) {
1571 outState.putString(STATE_CONVERSATION_UUID, conversation.getUuid());
1572 Uri uri = pendingTakePhotoUri.pop();
1573 if (uri != null) {
1574 outState.putString(STATE_PHOTO_URI, uri.toString());
1575 }
1576 }
1577 }
1578
1579 @Override
1580 public void onActivityCreated(Bundle savedInstanceState) {
1581 super.onActivityCreated(savedInstanceState);
1582 if (savedInstanceState == null) {
1583 return;
1584 }
1585 String uuid = savedInstanceState.getString(STATE_CONVERSATION_UUID);
1586 if (uuid != null) {
1587 this.pendingConversationsUuid.push(uuid);
1588 String takePhotoUri = savedInstanceState.getString(STATE_PHOTO_URI);
1589 if (takePhotoUri != null) {
1590 pendingTakePhotoUri.push(Uri.parse(takePhotoUri));
1591 }
1592 }
1593 }
1594
1595 @Override
1596 public void onStart() {
1597 super.onStart();
1598 if (this.reInitRequiredOnStart) {
1599 reInit(conversation);
1600 final Bundle extras = pendingExtras.pop();
1601 if (extras != null) {
1602 processExtras(extras);
1603 }
1604 } else {
1605 Log.d(Config.LOGTAG,"skipped reinit on start");
1606 }
1607 }
1608
1609 @Override
1610 public void onStop() {
1611 super.onStop();
1612 final Activity activity = getActivity();
1613 if (activity == null || !activity.isChangingConfigurations()) {
1614 messageListAdapter.stopAudioPlayer();
1615 }
1616 if (this.conversation != null) {
1617 final String msg = this.binding.textinput.getText().toString();
1618 if (this.conversation.setNextMessage(msg)) {
1619 this.activity.xmppConnectionService.updateConversation(this.conversation);
1620 }
1621 updateChatState(this.conversation, msg);
1622 this.activity.xmppConnectionService.getNotificationService().setOpenConversation(null);
1623 }
1624 }
1625
1626 private void updateChatState(final Conversation conversation, final String msg) {
1627 ChatState state = msg.length() == 0 ? Config.DEFAULT_CHATSTATE : ChatState.PAUSED;
1628 Account.State status = conversation.getAccount().getStatus();
1629 if (status == Account.State.ONLINE && conversation.setOutgoingChatState(state)) {
1630 activity.xmppConnectionService.sendChatState(conversation);
1631 }
1632 }
1633
1634 private void saveMessageDraftStopAudioPlayer() {
1635 final Conversation previousConversation = this.conversation;
1636 if (this.activity == null || this.binding == null || previousConversation == null) {
1637 return;
1638 }
1639 Log.d(Config.LOGTAG,"ConversationFragment.saveMessageDraftStopAudioPlayer()");
1640 final String msg = this.binding.textinput.getText().toString();
1641 if (previousConversation.setNextMessage(msg)) {
1642 activity.xmppConnectionService.updateConversation(previousConversation);
1643 }
1644 updateChatState(this.conversation, msg);
1645 messageListAdapter.stopAudioPlayer();
1646 }
1647
1648 public void reInit(Conversation conversation, Bundle extras) {
1649 this.saveMessageDraftStopAudioPlayer();
1650 if (this.reInit(conversation)) {
1651 if (extras != null) {
1652 processExtras(extras);
1653 }
1654 this.reInitRequiredOnStart = false;
1655 } else {
1656 this.reInitRequiredOnStart = true;
1657 pendingExtras.push(extras);
1658 }
1659 }
1660
1661 private boolean reInit(Conversation conversation) {
1662 return reInit(conversation, false);
1663 }
1664
1665 private boolean reInit(Conversation conversation, boolean restore) {
1666 if (conversation == null) {
1667 return false;
1668 }
1669 this.conversation = conversation;
1670 //once we set the conversation all is good and it will automatically do the right thing in onStart()
1671 if (this.activity == null || this.binding == null) {
1672 return false;
1673 }
1674 Log.d(Config.LOGTAG, "reInit(restore="+Boolean.toString(restore)+")");
1675 setupIme();
1676 if (!restore) {
1677 this.conversation.trim();
1678 }
1679
1680 this.binding.textSendButton.setContentDescription(activity.getString(R.string.send_message_to_x, conversation.getName()));
1681 this.binding.textinput.setKeyboardListener(null);
1682 this.binding.textinput.setText("");
1683 this.binding.textinput.append(this.conversation.getNextMessage());
1684 this.binding.textinput.setKeyboardListener(this);
1685 messageListAdapter.updatePreferences();
1686 this.binding.messagesView.setAdapter(messageListAdapter);
1687 refresh(false);
1688 this.conversation.messagesLoaded.set(true);
1689 final boolean isAtBottom;
1690 synchronized (this.messageList) {
1691 final Message first = conversation.getFirstUnreadMessage();
1692 final int bottom = Math.max(0, this.messageList.size() - 1);
1693 final int pos;
1694 if (first == null) {
1695 pos = bottom;
1696 } else {
1697 int i = getIndexOf(first.getUuid(), this.messageList);
1698 pos = i < 0 ? bottom : i;
1699 }
1700 this.binding.messagesView.setSelection(pos);
1701 isAtBottom = pos == bottom;
1702 }
1703
1704 activity.onConversationRead(this.conversation);
1705 //TODO if we only do this when this fragment is running on main it won't *bing* in tablet layout which might be unnecessary since we can *see* it
1706 activity.xmppConnectionService.getNotificationService().setOpenConversation(this.conversation);
1707 return true;
1708 }
1709
1710 private void processExtras(Bundle extras) {
1711 final String downloadUuid = extras.getString(ConversationActivity.EXTRA_DOWNLOAD_UUID);
1712 final String text = extras.getString(ConversationActivity.EXTRA_TEXT);
1713 final String nick = extras.getString(ConversationActivity.EXTRA_NICK);
1714 final boolean pm = extras.getBoolean(ConversationActivity.EXTRA_IS_PRIVATE_MESSAGE, false);
1715 if (nick != null) {
1716 if (pm) {
1717 Jid jid = conversation.getJid();
1718 try {
1719 Jid next = Jid.fromParts(jid.getLocalpart(), jid.getDomainpart(), nick);
1720 privateMessageWith(next);
1721 } catch (final InvalidJidException ignored) {
1722 //do nothing
1723 }
1724 } else {
1725 highlightInConference(nick);
1726 }
1727 } else {
1728 appendText(text);
1729 }
1730 final Message message = downloadUuid == null ? null : conversation.findMessageWithFileAndUuid(downloadUuid);
1731 if (message != null) {
1732 startDownloadable(message);
1733 }
1734 }
1735
1736 private boolean showBlockSubmenu(View view) {
1737 final Jid jid = conversation.getJid();
1738 if (jid.isDomainJid()) {
1739 BlockContactDialog.show(activity, conversation);
1740 } else {
1741 PopupMenu popupMenu = new PopupMenu(getActivity(), view);
1742 popupMenu.inflate(R.menu.block);
1743 popupMenu.setOnMenuItemClickListener(menuItem -> {
1744 Blockable blockable;
1745 switch (menuItem.getItemId()) {
1746 case R.id.block_domain:
1747 blockable = conversation.getAccount().getRoster().getContact(jid.toDomainJid());
1748 break;
1749 default:
1750 blockable = conversation;
1751 }
1752 BlockContactDialog.show(activity, blockable);
1753 return true;
1754 });
1755 popupMenu.show();
1756 }
1757 return true;
1758 }
1759
1760 private void updateSnackBar(final Conversation conversation) {
1761 final Account account = conversation.getAccount();
1762 final XmppConnection connection = account.getXmppConnection();
1763 final int mode = conversation.getMode();
1764 final Contact contact = mode == Conversation.MODE_SINGLE ? conversation.getContact() : null;
1765 if (account.getStatus() == Account.State.DISABLED) {
1766 showSnackbar(R.string.this_account_is_disabled, R.string.enable, this.mEnableAccountListener);
1767 } else if (conversation.isBlocked()) {
1768 showSnackbar(R.string.contact_blocked, R.string.unblock, this.mUnblockClickListener);
1769 } else if (contact != null && !contact.showInRoster() && contact.getOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST)) {
1770 showSnackbar(R.string.contact_added_you, R.string.add_back, this.mAddBackClickListener, this.mLongPressBlockListener);
1771 } else if (contact != null && contact.getOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST)) {
1772 showSnackbar(R.string.contact_asks_for_presence_subscription, R.string.allow, this.mAllowPresenceSubscription, this.mLongPressBlockListener);
1773 } else if (mode == Conversation.MODE_MULTI
1774 && !conversation.getMucOptions().online()
1775 && account.getStatus() == Account.State.ONLINE) {
1776 switch (conversation.getMucOptions().getError()) {
1777 case NICK_IN_USE:
1778 showSnackbar(R.string.nick_in_use, R.string.edit, clickToMuc);
1779 break;
1780 case NO_RESPONSE:
1781 showSnackbar(R.string.joining_conference, 0, null);
1782 break;
1783 case SERVER_NOT_FOUND:
1784 if (conversation.receivedMessagesCount() > 0) {
1785 showSnackbar(R.string.remote_server_not_found, R.string.try_again, joinMuc);
1786 } else {
1787 showSnackbar(R.string.remote_server_not_found, R.string.leave, leaveMuc);
1788 }
1789 break;
1790 case PASSWORD_REQUIRED:
1791 showSnackbar(R.string.conference_requires_password, R.string.enter_password, enterPassword);
1792 break;
1793 case BANNED:
1794 showSnackbar(R.string.conference_banned, R.string.leave, leaveMuc);
1795 break;
1796 case MEMBERS_ONLY:
1797 showSnackbar(R.string.conference_members_only, R.string.leave, leaveMuc);
1798 break;
1799 case KICKED:
1800 showSnackbar(R.string.conference_kicked, R.string.join, joinMuc);
1801 break;
1802 case UNKNOWN:
1803 showSnackbar(R.string.conference_unknown_error, R.string.try_again, joinMuc);
1804 break;
1805 case INVALID_NICK:
1806 showSnackbar(R.string.invalid_muc_nick, R.string.edit, clickToMuc);
1807 case SHUTDOWN:
1808 showSnackbar(R.string.conference_shutdown, R.string.try_again, joinMuc);
1809 break;
1810 default:
1811 hideSnackbar();
1812 break;
1813 }
1814 } else if (account.hasPendingPgpIntent(conversation)) {
1815 showSnackbar(R.string.openpgp_messages_found, R.string.decrypt, clickToDecryptListener);
1816 } else if (connection != null
1817 && connection.getFeatures().blocking()
1818 && conversation.countMessages() != 0
1819 && !conversation.isBlocked()
1820 && conversation.isWithStranger()) {
1821 showSnackbar(R.string.received_message_from_stranger, R.string.block, mBlockClickListener);
1822 } else {
1823 hideSnackbar();
1824 }
1825 }
1826
1827 @Override
1828 public void refresh() {
1829 if (this.binding == null) {
1830 Log.d(Config.LOGTAG,"ConversationFragment.refresh() skipped updated because view binding was null");
1831 return;
1832 }
1833 this.refresh(true);
1834 }
1835
1836
1837 private void refresh(boolean notifyConversationRead) {
1838 synchronized (this.messageList) {
1839 if (this.conversation != null) {
1840 conversation.populateWithMessages(this.messageList);
1841 updateSnackBar(conversation);
1842 updateStatusMessages();
1843 this.messageListAdapter.notifyDataSetChanged();
1844 updateChatMsgHint();
1845 if (notifyConversationRead && activity != null) {
1846 activity.onConversationRead(this.conversation);
1847 }
1848 updateSendButton();
1849 updateEditablity();
1850 }
1851 }
1852 }
1853
1854 protected void messageSent() {
1855 mSendingPgpMessage.set(false);
1856 this.binding.textinput.setText("");
1857 if (conversation.setCorrectingMessage(null)) {
1858 this.binding.textinput.append(conversation.getDraftMessage());
1859 conversation.setDraftMessage(null);
1860 }
1861 if (conversation.setNextMessage(this.binding.textinput.getText().toString())) {
1862 activity.xmppConnectionService.updateConversation(conversation);
1863 }
1864 updateChatMsgHint();
1865 new Handler().post(() -> {
1866 int size = messageList.size();
1867 this.binding.messagesView.setSelection(size - 1);
1868 });
1869 }
1870
1871 public void setFocusOnInputField() {
1872 this.binding.textinput.requestFocus();
1873 }
1874
1875 public void doneSendingPgpMessage() {
1876 mSendingPgpMessage.set(false);
1877 }
1878
1879 public long getMaxHttpUploadSize(Conversation conversation) {
1880 final XmppConnection connection = conversation.getAccount().getXmppConnection();
1881 return connection == null ? -1 : connection.getFeatures().getMaxHttpUploadSize();
1882 }
1883
1884 private void updateEditablity() {
1885 boolean canWrite = this.conversation.getMode() == Conversation.MODE_SINGLE || this.conversation.getMucOptions().participating() || this.conversation.getNextCounterpart() != null;
1886 this.binding.textinput.setFocusable(canWrite);
1887 this.binding.textinput.setFocusableInTouchMode(canWrite);
1888 this.binding.textSendButton.setEnabled(canWrite);
1889 this.binding.textinput.setCursorVisible(canWrite);
1890 }
1891
1892 public void updateSendButton() {
1893 boolean useSendButtonToIndicateStatus = PreferenceManager.getDefaultSharedPreferences(getActivity()).getBoolean("send_button_status", getResources().getBoolean(R.bool.send_button_status));
1894 final Conversation c = this.conversation;
1895 final Presence.Status status;
1896 final String text = this.binding.textinput == null ? "" : this.binding.textinput.getText().toString();
1897 final SendButtonAction action = SendButtonTool.getAction(getActivity(), c, text);
1898 if (useSendButtonToIndicateStatus && c.getAccount().getStatus() == Account.State.ONLINE) {
1899 if (activity.xmppConnectionService != null && activity.xmppConnectionService.getMessageArchiveService().isCatchingUp(c)) {
1900 status = Presence.Status.OFFLINE;
1901 } else if (c.getMode() == Conversation.MODE_SINGLE) {
1902 status = c.getContact().getShownStatus();
1903 } else {
1904 status = c.getMucOptions().online() ? Presence.Status.ONLINE : Presence.Status.OFFLINE;
1905 }
1906 } else {
1907 status = Presence.Status.OFFLINE;
1908 }
1909 this.binding.textSendButton.setTag(action);
1910 this.binding.textSendButton.setImageResource(SendButtonTool.getSendButtonImageResource(getActivity(), action, status));
1911 }
1912
1913 protected void updateDateSeparators() {
1914 synchronized (this.messageList) {
1915 for (int i = 0; i < this.messageList.size(); ++i) {
1916 final Message current = this.messageList.get(i);
1917 if (i == 0 || !UIHelper.sameDay(this.messageList.get(i - 1).getTimeSent(), current.getTimeSent())) {
1918 this.messageList.add(i, Message.createDateSeparator(current));
1919 i++;
1920 }
1921 }
1922 }
1923 }
1924
1925 protected void updateStatusMessages() {
1926 updateDateSeparators();
1927 synchronized (this.messageList) {
1928 if (showLoadMoreMessages(conversation)) {
1929 this.messageList.add(0, Message.createLoadMoreMessage(conversation));
1930 }
1931 if (conversation.getMode() == Conversation.MODE_SINGLE) {
1932 ChatState state = conversation.getIncomingChatState();
1933 if (state == ChatState.COMPOSING) {
1934 this.messageList.add(Message.createStatusMessage(conversation, getString(R.string.contact_is_typing, conversation.getName())));
1935 } else if (state == ChatState.PAUSED) {
1936 this.messageList.add(Message.createStatusMessage(conversation, getString(R.string.contact_has_stopped_typing, conversation.getName())));
1937 } else {
1938 for (int i = this.messageList.size() - 1; i >= 0; --i) {
1939 if (this.messageList.get(i).getStatus() == Message.STATUS_RECEIVED) {
1940 return;
1941 } else {
1942 if (this.messageList.get(i).getStatus() == Message.STATUS_SEND_DISPLAYED) {
1943 this.messageList.add(i + 1,
1944 Message.createStatusMessage(conversation, getString(R.string.contact_has_read_up_to_this_point, conversation.getName())));
1945 return;
1946 }
1947 }
1948 }
1949 }
1950 } else {
1951 final MucOptions mucOptions = conversation.getMucOptions();
1952 final List<MucOptions.User> allUsers = mucOptions.getUsers();
1953 final Set<ReadByMarker> addedMarkers = new HashSet<>();
1954 ChatState state = ChatState.COMPOSING;
1955 List<MucOptions.User> users = conversation.getMucOptions().getUsersWithChatState(state, 5);
1956 if (users.size() == 0) {
1957 state = ChatState.PAUSED;
1958 users = conversation.getMucOptions().getUsersWithChatState(state, 5);
1959 }
1960 if (mucOptions.isPrivateAndNonAnonymous()) {
1961 for (int i = this.messageList.size() - 1; i >= 0; --i) {
1962 final Set<ReadByMarker> markersForMessage = messageList.get(i).getReadByMarkers();
1963 final List<MucOptions.User> shownMarkers = new ArrayList<>();
1964 for (ReadByMarker marker : markersForMessage) {
1965 if (!ReadByMarker.contains(marker, addedMarkers)) {
1966 addedMarkers.add(marker); //may be put outside this condition. set should do dedup anyway
1967 MucOptions.User user = mucOptions.findUser(marker);
1968 if (user != null && !users.contains(user)) {
1969 shownMarkers.add(user);
1970 }
1971 }
1972 }
1973 final ReadByMarker markerForSender = ReadByMarker.from(messageList.get(i));
1974 final Message statusMessage;
1975 final int size = shownMarkers.size();
1976 if (size > 1) {
1977 final String body;
1978 if (size <= 4) {
1979 body = getString(R.string.contacts_have_read_up_to_this_point, UIHelper.concatNames(shownMarkers));
1980 } else {
1981 body = getString(R.string.contacts_and_n_more_have_read_up_to_this_point, UIHelper.concatNames(shownMarkers, 3), size - 3);
1982 }
1983 statusMessage = Message.createStatusMessage(conversation, body);
1984 statusMessage.setCounterparts(shownMarkers);
1985 } else if (size == 1) {
1986 statusMessage = Message.createStatusMessage(conversation, getString(R.string.contact_has_read_up_to_this_point, UIHelper.getDisplayName(shownMarkers.get(0))));
1987 statusMessage.setCounterpart(shownMarkers.get(0).getFullJid());
1988 statusMessage.setTrueCounterpart(shownMarkers.get(0).getRealJid());
1989 } else {
1990 statusMessage = null;
1991 }
1992 if (statusMessage != null) {
1993 this.messageList.add(i + 1, statusMessage);
1994 }
1995 addedMarkers.add(markerForSender);
1996 if (ReadByMarker.allUsersRepresented(allUsers, addedMarkers)) {
1997 break;
1998 }
1999 }
2000 }
2001 if (users.size() > 0) {
2002 Message statusMessage;
2003 if (users.size() == 1) {
2004 MucOptions.User user = users.get(0);
2005 int id = state == ChatState.COMPOSING ? R.string.contact_is_typing : R.string.contact_has_stopped_typing;
2006 statusMessage = Message.createStatusMessage(conversation, getString(id, UIHelper.getDisplayName(user)));
2007 statusMessage.setTrueCounterpart(user.getRealJid());
2008 statusMessage.setCounterpart(user.getFullJid());
2009 } else {
2010 int id = state == ChatState.COMPOSING ? R.string.contacts_are_typing : R.string.contacts_have_stopped_typing;
2011 statusMessage = Message.createStatusMessage(conversation, getString(id, UIHelper.concatNames(users)));
2012 statusMessage.setCounterparts(users);
2013 }
2014 this.messageList.add(statusMessage);
2015 }
2016
2017 }
2018 }
2019 }
2020
2021 public void stopScrolling() {
2022 long now = SystemClock.uptimeMillis();
2023 MotionEvent cancel = MotionEvent.obtain(now, now, MotionEvent.ACTION_CANCEL, 0, 0, 0);
2024 binding.messagesView.dispatchTouchEvent(cancel);
2025 }
2026
2027 private boolean showLoadMoreMessages(final Conversation c) {
2028 final boolean mam = hasMamSupport(c) && !c.getContact().isBlocked();
2029 final MessageArchiveService service = activity.xmppConnectionService.getMessageArchiveService();
2030 return mam && (c.getLastClearHistory().getTimestamp() != 0 || (c.countMessages() == 0 && c.messagesLoaded.get() && c.hasMessagesLeftOnServer() && !service.queryInProgress(c)));
2031 }
2032
2033 private boolean hasMamSupport(final Conversation c) {
2034 if (c.getMode() == Conversation.MODE_SINGLE) {
2035 final XmppConnection connection = c.getAccount().getXmppConnection();
2036 return connection != null && connection.getFeatures().mam();
2037 } else {
2038 return c.getMucOptions().mamSupport();
2039 }
2040 }
2041
2042 protected void showSnackbar(final int message, final int action, final OnClickListener clickListener) {
2043 showSnackbar(message, action, clickListener, null);
2044 }
2045
2046 protected void showSnackbar(final int message, final int action, final OnClickListener clickListener, final View.OnLongClickListener longClickListener) {
2047 this.binding.snackbar.setVisibility(View.VISIBLE);
2048 this.binding.snackbar.setOnClickListener(null);
2049 this.binding.snackbarMessage.setText(message);
2050 this.binding.snackbarMessage.setOnClickListener(null);
2051 this.binding.snackbarAction.setVisibility(clickListener == null ? View.GONE : View.VISIBLE);
2052 if (action != 0) {
2053 this.binding.snackbarAction.setText(action);
2054 }
2055 this.binding.snackbarAction.setOnClickListener(clickListener);
2056 this.binding.snackbarAction.setOnLongClickListener(longClickListener);
2057 }
2058
2059 protected void hideSnackbar() {
2060 this.binding.snackbar.setVisibility(View.GONE);
2061 }
2062
2063 protected void sendPlainTextMessage(Message message) {
2064 activity.xmppConnectionService.sendMessage(message);
2065 messageSent();
2066 }
2067
2068 protected void sendPgpMessage(final Message message) {
2069 final XmppConnectionService xmppService = activity.xmppConnectionService;
2070 final Contact contact = message.getConversation().getContact();
2071 if (!activity.hasPgp()) {
2072 activity.showInstallPgpDialog();
2073 return;
2074 }
2075 if (conversation.getAccount().getPgpSignature() == null) {
2076 activity.announcePgp(conversation.getAccount(), conversation, null, activity.onOpenPGPKeyPublished);
2077 return;
2078 }
2079 if (!mSendingPgpMessage.compareAndSet(false, true)) {
2080 Log.d(Config.LOGTAG, "sending pgp message already in progress");
2081 }
2082 if (conversation.getMode() == Conversation.MODE_SINGLE) {
2083 if (contact.getPgpKeyId() != 0) {
2084 xmppService.getPgpEngine().hasKey(contact,
2085 new UiCallback<Contact>() {
2086
2087 @Override
2088 public void userInputRequried(PendingIntent pi, Contact contact) {
2089 startPendingIntent(pi, REQUEST_ENCRYPT_MESSAGE);
2090 }
2091
2092 @Override
2093 public void success(Contact contact) {
2094 encryptTextMessage(message);
2095 }
2096
2097 @Override
2098 public void error(int error, Contact contact) {
2099 activity.runOnUiThread(() -> Toast.makeText(activity,
2100 R.string.unable_to_connect_to_keychain,
2101 Toast.LENGTH_SHORT
2102 ).show());
2103 mSendingPgpMessage.set(false);
2104 }
2105 });
2106
2107 } else {
2108 showNoPGPKeyDialog(false, (dialog, which) -> {
2109 conversation.setNextEncryption(Message.ENCRYPTION_NONE);
2110 xmppService.updateConversation(conversation);
2111 message.setEncryption(Message.ENCRYPTION_NONE);
2112 xmppService.sendMessage(message);
2113 messageSent();
2114 });
2115 }
2116 } else {
2117 if (conversation.getMucOptions().pgpKeysInUse()) {
2118 if (!conversation.getMucOptions().everybodyHasKeys()) {
2119 Toast warning = Toast
2120 .makeText(getActivity(),
2121 R.string.missing_public_keys,
2122 Toast.LENGTH_LONG);
2123 warning.setGravity(Gravity.CENTER_VERTICAL, 0, 0);
2124 warning.show();
2125 }
2126 encryptTextMessage(message);
2127 } else {
2128 showNoPGPKeyDialog(true, (dialog, which) -> {
2129 conversation.setNextEncryption(Message.ENCRYPTION_NONE);
2130 message.setEncryption(Message.ENCRYPTION_NONE);
2131 xmppService.updateConversation(conversation);
2132 xmppService.sendMessage(message);
2133 messageSent();
2134 });
2135 }
2136 }
2137 }
2138
2139 public void encryptTextMessage(Message message) {
2140 activity.xmppConnectionService.getPgpEngine().encrypt(message,
2141 new UiCallback<Message>() {
2142
2143 @Override
2144 public void userInputRequried(PendingIntent pi, Message message) {
2145 startPendingIntent(pi, REQUEST_SEND_MESSAGE);
2146 }
2147
2148 @Override
2149 public void success(Message message) {
2150 message.setEncryption(Message.ENCRYPTION_DECRYPTED);
2151 activity.xmppConnectionService.sendMessage(message);
2152 getActivity().runOnUiThread(() -> messageSent());
2153 }
2154
2155 @Override
2156 public void error(final int error, Message message) {
2157 getActivity().runOnUiThread(() -> {
2158 doneSendingPgpMessage();
2159 Toast.makeText(getActivity(), R.string.unable_to_connect_to_keychain, Toast.LENGTH_SHORT).show();
2160 });
2161
2162 }
2163 });
2164 }
2165
2166 public void showNoPGPKeyDialog(boolean plural, DialogInterface.OnClickListener listener) {
2167 AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
2168 builder.setIconAttribute(android.R.attr.alertDialogIcon);
2169 if (plural) {
2170 builder.setTitle(getString(R.string.no_pgp_keys));
2171 builder.setMessage(getText(R.string.contacts_have_no_pgp_keys));
2172 } else {
2173 builder.setTitle(getString(R.string.no_pgp_key));
2174 builder.setMessage(getText(R.string.contact_has_no_pgp_key));
2175 }
2176 builder.setNegativeButton(getString(R.string.cancel), null);
2177 builder.setPositiveButton(getString(R.string.send_unencrypted), listener);
2178 builder.create().show();
2179 }
2180
2181 protected void sendAxolotlMessage(final Message message) {
2182 activity.xmppConnectionService.sendMessage(message);
2183 messageSent();
2184 }
2185
2186 public void appendText(String text) {
2187 if (text == null) {
2188 return;
2189 }
2190 String previous = this.binding.textinput.getText().toString();
2191 if (previous.length() != 0 && !previous.endsWith(" ")) {
2192 text = " " + text;
2193 }
2194 this.binding.textinput.append(text);
2195 }
2196
2197 @Override
2198 public boolean onEnterPressed() {
2199 SharedPreferences p = PreferenceManager.getDefaultSharedPreferences(getActivity());
2200 final boolean enterIsSend = p.getBoolean("enter_is_send", getResources().getBoolean(R.bool.enter_is_send));
2201 if (enterIsSend) {
2202 sendMessage();
2203 return true;
2204 } else {
2205 return false;
2206 }
2207 }
2208
2209 @Override
2210 public void onTypingStarted() {
2211 Account.State status = conversation.getAccount().getStatus();
2212 if (status == Account.State.ONLINE && conversation.setOutgoingChatState(ChatState.COMPOSING)) {
2213 activity.xmppConnectionService.sendChatState(conversation);
2214 }
2215 updateSendButton();
2216 }
2217
2218 @Override
2219 public void onTypingStopped() {
2220 Account.State status = conversation.getAccount().getStatus();
2221 if (status == Account.State.ONLINE && conversation.setOutgoingChatState(ChatState.PAUSED)) {
2222 activity.xmppConnectionService.sendChatState(conversation);
2223 }
2224 }
2225
2226 @Override
2227 public void onTextDeleted() {
2228 Account.State status = conversation.getAccount().getStatus();
2229 if (status == Account.State.ONLINE && conversation.setOutgoingChatState(Config.DEFAULT_CHATSTATE)) {
2230 activity.xmppConnectionService.sendChatState(conversation);
2231 }
2232 updateSendButton();
2233 }
2234
2235 @Override
2236 public void onTextChanged() {
2237 if (conversation != null && conversation.getCorrectingMessage() != null) {
2238 updateSendButton();
2239 }
2240 }
2241
2242 @Override
2243 public boolean onTabPressed(boolean repeated) {
2244 if (conversation == null || conversation.getMode() == Conversation.MODE_SINGLE) {
2245 return false;
2246 }
2247 if (repeated) {
2248 completionIndex++;
2249 } else {
2250 lastCompletionLength = 0;
2251 completionIndex = 0;
2252 final String content = this.binding.textinput.getText().toString();
2253 lastCompletionCursor = this.binding.textinput.getSelectionEnd();
2254 int start = lastCompletionCursor > 0 ? content.lastIndexOf(" ", lastCompletionCursor - 1) + 1 : 0;
2255 firstWord = start == 0;
2256 incomplete = content.substring(start, lastCompletionCursor);
2257 }
2258 List<String> completions = new ArrayList<>();
2259 for (MucOptions.User user : conversation.getMucOptions().getUsers()) {
2260 String name = user.getName();
2261 if (name != null && name.startsWith(incomplete)) {
2262 completions.add(name + (firstWord ? ": " : " "));
2263 }
2264 }
2265 Collections.sort(completions);
2266 if (completions.size() > completionIndex) {
2267 String completion = completions.get(completionIndex).substring(incomplete.length());
2268 this.binding.textinput.getEditableText().delete(lastCompletionCursor, lastCompletionCursor + lastCompletionLength);
2269 this.binding.textinput.getEditableText().insert(lastCompletionCursor, completion);
2270 lastCompletionLength = completion.length();
2271 } else {
2272 completionIndex = -1;
2273 this.binding.textinput.getEditableText().delete(lastCompletionCursor, lastCompletionCursor + lastCompletionLength);
2274 lastCompletionLength = 0;
2275 }
2276 return true;
2277 }
2278
2279 private void startPendingIntent(PendingIntent pendingIntent, int requestCode) {
2280 try {
2281 getActivity().startIntentSenderForResult(pendingIntent.getIntentSender(), requestCode, null, 0, 0, 0);
2282 } catch (final SendIntentException ignored) {
2283 }
2284 }
2285
2286 @Override
2287 public void onBackendConnected() {
2288 Log.d(Config.LOGTAG, "ConversationFragment.onBackendConnected()");
2289 String uuid = pendingConversationsUuid.pop();
2290 if (uuid != null) {
2291 Conversation conversation = activity.xmppConnectionService.findConversationByUuid(uuid);
2292 if (conversation == null) {
2293 Log.d(Config.LOGTAG, "unable to restore activity");
2294 clearPending();
2295 return;
2296 }
2297 reInit(conversation, true);
2298 }
2299 ActivityResult activityResult = postponedActivityResult.pop();
2300 if (activityResult != null) {
2301 handleActivityResult(activityResult);
2302 }
2303 }
2304
2305 public void clearPending() {
2306 if (postponedActivityResult.pop() != null) {
2307 Log.d(Config.LOGTAG, "cleared pending intent with unhandled result left");
2308 }
2309 }
2310
2311 public static Conversation getConversation(Activity activity) {
2312 return getConversation(activity, R.id.secondary_fragment);
2313 }
2314
2315 private static Conversation getConversation(Activity activity, @IdRes int res) {
2316 final Fragment fragment = activity.getFragmentManager().findFragmentById(res);
2317 if (fragment != null && fragment instanceof ConversationFragment) {
2318 return ((ConversationFragment) fragment).getConversation();
2319 } else {
2320 return null;
2321 }
2322 }
2323
2324 public static Conversation getConversationReliable(Activity activity) {
2325 final Conversation conversation = getConversation(activity, R.id.secondary_fragment);
2326 if (conversation != null) {
2327 return conversation;
2328 }
2329 return getConversation(activity, R.id.main_fragment);
2330 }
2331
2332 public Conversation getConversation() {
2333 return conversation;
2334 }
2335}