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