1package eu.siacs.conversations.ui;
2
3import android.app.Activity;
4import android.support.v7.app.AlertDialog;
5import android.app.Fragment;
6import android.app.PendingIntent;
7import android.content.ActivityNotFoundException;
8import android.content.Context;
9import android.content.DialogInterface;
10import android.content.Intent;
11import android.content.IntentSender.SendIntentException;
12import android.os.Bundle;
13import android.os.Handler;
14import android.os.SystemClock;
15import android.support.v13.view.inputmethod.InputConnectionCompat;
16import android.support.v13.view.inputmethod.InputContentInfoCompat;
17import android.text.Editable;
18import android.text.InputType;
19import android.util.Log;
20import android.util.Pair;
21import android.view.ContextMenu;
22import android.view.ContextMenu.ContextMenuInfo;
23import android.view.Gravity;
24import android.view.KeyEvent;
25import android.view.LayoutInflater;
26import android.view.MenuItem;
27import android.view.MotionEvent;
28import android.view.View;
29import android.view.View.OnClickListener;
30import android.view.ViewGroup;
31import android.view.inputmethod.EditorInfo;
32import android.view.inputmethod.InputMethodManager;
33import android.widget.AbsListView;
34import android.widget.AbsListView.OnScrollListener;
35import android.widget.AdapterView;
36import android.widget.AdapterView.AdapterContextMenuInfo;
37import android.widget.ImageButton;
38import android.widget.ListView;
39import android.widget.PopupMenu;
40import android.widget.RelativeLayout;
41import android.widget.TextView;
42import android.widget.TextView.OnEditorActionListener;
43import android.widget.Toast;
44
45import java.util.ArrayList;
46import java.util.Arrays;
47import java.util.Collections;
48import java.util.HashSet;
49import java.util.List;
50import java.util.Set;
51import java.util.UUID;
52import java.util.concurrent.atomic.AtomicBoolean;
53
54import eu.siacs.conversations.Config;
55import eu.siacs.conversations.R;
56import eu.siacs.conversations.entities.Account;
57import eu.siacs.conversations.entities.Blockable;
58import eu.siacs.conversations.entities.Contact;
59import eu.siacs.conversations.entities.Conversation;
60import eu.siacs.conversations.entities.DownloadableFile;
61import eu.siacs.conversations.entities.Message;
62import eu.siacs.conversations.entities.MucOptions;
63import eu.siacs.conversations.entities.Presence;
64import eu.siacs.conversations.entities.ReadByMarker;
65import eu.siacs.conversations.entities.Transferable;
66import eu.siacs.conversations.entities.TransferablePlaceholder;
67import eu.siacs.conversations.http.HttpDownloadConnection;
68import eu.siacs.conversations.persistance.FileBackend;
69import eu.siacs.conversations.services.MessageArchiveService;
70import eu.siacs.conversations.services.XmppConnectionService;
71import eu.siacs.conversations.ui.XmppActivity.OnPresenceSelected;
72import eu.siacs.conversations.ui.XmppActivity.OnValueEdited;
73import eu.siacs.conversations.ui.adapter.MessageAdapter;
74import eu.siacs.conversations.ui.adapter.MessageAdapter.OnContactPictureClicked;
75import eu.siacs.conversations.ui.adapter.MessageAdapter.OnContactPictureLongClicked;
76import eu.siacs.conversations.ui.widget.EditMessage;
77import eu.siacs.conversations.utils.MessageUtils;
78import eu.siacs.conversations.utils.NickValidityChecker;
79import eu.siacs.conversations.utils.StylingHelper;
80import eu.siacs.conversations.utils.UIHelper;
81import eu.siacs.conversations.xmpp.XmppConnection;
82import eu.siacs.conversations.xmpp.chatstate.ChatState;
83import eu.siacs.conversations.xmpp.jid.Jid;
84
85public class ConversationFragment extends Fragment implements EditMessage.KeyboardListener {
86
87 final protected List<Message> messageList = new ArrayList<>();
88 protected Conversation conversation;
89 protected ListView messagesView;
90 protected MessageAdapter messageListAdapter;
91 private EditMessage mEditMessage;
92 private ImageButton mSendButton;
93 private RelativeLayout snackbar;
94 private TextView snackbarMessage;
95 private TextView snackbarAction;
96 private Toast messageLoaderToast;
97 private OnClickListener clickToMuc = new OnClickListener() {
98
99 @Override
100 public void onClick(View v) {
101 Intent intent = new Intent(getActivity(), ConferenceDetailsActivity.class);
102 intent.setAction(ConferenceDetailsActivity.ACTION_VIEW_MUC);
103 intent.putExtra("uuid", conversation.getUuid());
104 startActivity(intent);
105 }
106 };
107 private ConversationActivity activity;
108 private OnClickListener leaveMuc = new OnClickListener() {
109
110 @Override
111 public void onClick(View v) {
112 activity.endConversation(conversation);
113 }
114 };
115 private OnClickListener joinMuc = new OnClickListener() {
116
117 @Override
118 public void onClick(View v) {
119 activity.xmppConnectionService.joinMuc(conversation);
120 }
121 };
122 private OnClickListener enterPassword = new OnClickListener() {
123
124 @Override
125 public void onClick(View v) {
126 MucOptions muc = conversation.getMucOptions();
127 String password = muc.getPassword();
128 if (password == null) {
129 password = "";
130 }
131 activity.quickPasswordEdit(password, new OnValueEdited() {
132
133 @Override
134 public String onValueEdited(String value) {
135 activity.xmppConnectionService.providePasswordForMuc(conversation, value);
136 return null;
137 }
138 });
139 }
140 };
141 private OnScrollListener mOnScrollListener = new OnScrollListener() {
142
143 @Override
144 public void onScrollStateChanged(AbsListView view, int scrollState) {
145 // TODO Auto-generated method stub
146
147 }
148
149 @Override
150 public void onScroll(final AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
151 synchronized (ConversationFragment.this.messageList) {
152 if (firstVisibleItem < 5 && conversation != null && conversation.messagesLoaded.compareAndSet(true, false) && messageList.size() > 0) {
153 long timestamp;
154 if (messageList.get(0).getType() == Message.TYPE_STATUS && messageList.size() >= 2) {
155 timestamp = messageList.get(1).getTimeSent();
156 } else {
157 timestamp = messageList.get(0).getTimeSent();
158 }
159 activity.xmppConnectionService.loadMoreMessages(conversation, timestamp, new XmppConnectionService.OnMoreMessagesLoaded() {
160 @Override
161 public void onMoreMessagesLoaded(final int c, final Conversation conversation) {
162 if (ConversationFragment.this.conversation != conversation) {
163 conversation.messagesLoaded.set(true);
164 return;
165 }
166 activity.runOnUiThread(new Runnable() {
167 @Override
168 public void run() {
169 final int oldPosition = messagesView.getFirstVisiblePosition();
170 Message message = null;
171 int childPos;
172 for (childPos = 0; childPos + oldPosition < messageList.size(); ++childPos) {
173 message = messageList.get(oldPosition + childPos);
174 if (message.getType() != Message.TYPE_STATUS) {
175 break;
176 }
177 }
178 final String uuid = message != null ? message.getUuid() : null;
179 View v = messagesView.getChildAt(childPos);
180 final int pxOffset = (v == null) ? 0 : v.getTop();
181 ConversationFragment.this.conversation.populateWithMessages(ConversationFragment.this.messageList);
182 try {
183 updateStatusMessages();
184 } catch (IllegalStateException e) {
185 Log.d(Config.LOGTAG, "caught illegal state exception while updating status messages");
186 }
187 messageListAdapter.notifyDataSetChanged();
188 int pos = Math.max(getIndexOf(uuid, messageList), 0);
189 messagesView.setSelectionFromTop(pos, pxOffset);
190 if (messageLoaderToast != null) {
191 messageLoaderToast.cancel();
192 }
193 conversation.messagesLoaded.set(true);
194 }
195 });
196 }
197
198 @Override
199 public void informUser(final int resId) {
200
201 activity.runOnUiThread(new Runnable() {
202 @Override
203 public void run() {
204 if (messageLoaderToast != null) {
205 messageLoaderToast.cancel();
206 }
207 if (ConversationFragment.this.conversation != conversation) {
208 return;
209 }
210 messageLoaderToast = Toast.makeText(view.getContext(), resId, Toast.LENGTH_LONG);
211 messageLoaderToast.show();
212 }
213 });
214
215 }
216 });
217
218 }
219 }
220 }
221 };
222
223 private EditMessage.OnCommitContentListener mEditorContentListener = new EditMessage.OnCommitContentListener() {
224 @Override
225 public boolean onCommitContent(InputContentInfoCompat inputContentInfo, int flags, Bundle opts, String[] contentMimeTypes) {
226 // try to get permission to read the image, if applicable
227 if ((flags & InputConnectionCompat.INPUT_CONTENT_GRANT_READ_URI_PERMISSION) != 0) {
228 try {
229 inputContentInfo.requestPermission();
230 } catch (Exception e) {
231 Log.e(Config.LOGTAG, "InputContentInfoCompat#requestPermission() failed.", e);
232 Toast.makeText(
233 activity,
234 activity.getString(R.string.no_permission_to_access_x, inputContentInfo.getDescription()),
235 Toast.LENGTH_LONG
236 ).show();
237 return false;
238 }
239 }
240 if (activity.hasStoragePermission(ConversationActivity.REQUEST_ADD_EDITOR_CONTENT)) {
241 activity.attachImageToConversation(inputContentInfo.getContentUri());
242 } else {
243 activity.mPendingEditorContent = inputContentInfo.getContentUri();
244 }
245 return true;
246 }
247 };
248 private Message selectedMessage;
249 private OnClickListener mEnableAccountListener = new OnClickListener() {
250 @Override
251 public void onClick(View v) {
252 final Account account = conversation == null ? null : conversation.getAccount();
253 if (account != null) {
254 account.setOption(Account.OPTION_DISABLED, false);
255 activity.xmppConnectionService.updateAccount(account);
256 }
257 }
258 };
259 private OnClickListener mUnblockClickListener = new OnClickListener() {
260 @Override
261 public void onClick(final View v) {
262 v.post(new Runnable() {
263 @Override
264 public void run() {
265 v.setVisibility(View.INVISIBLE);
266 }
267 });
268 if (conversation.isDomainBlocked()) {
269 BlockContactDialog.show(activity, conversation);
270 } else {
271 activity.unblockConversation(conversation);
272 }
273 }
274 };
275 private OnClickListener mBlockClickListener = new OnClickListener() {
276 @Override
277 public void onClick(final View view) {
278 showBlockSubmenu(view);
279 }
280 };
281 private OnClickListener mAddBackClickListener = new OnClickListener() {
282
283 @Override
284 public void onClick(View v) {
285 final Contact contact = conversation == null ? null : conversation.getContact();
286 if (contact != null) {
287 activity.xmppConnectionService.createContact(contact);
288 activity.switchToContactDetails(contact);
289 }
290 }
291 };
292 private View.OnLongClickListener mLongPressBlockListener = new View.OnLongClickListener() {
293 @Override
294 public boolean onLongClick(View v) {
295 showBlockSubmenu(v);
296 return true;
297 }
298 };
299 private OnClickListener mAllowPresenceSubscription = new OnClickListener() {
300 @Override
301 public void onClick(View v) {
302 final Contact contact = conversation == null ? null : conversation.getContact();
303 if (contact != null) {
304 activity.xmppConnectionService.sendPresencePacket(contact.getAccount(),
305 activity.xmppConnectionService.getPresenceGenerator()
306 .sendPresenceUpdatesTo(contact));
307 hideSnackbar();
308 }
309 }
310 };
311
312 protected OnClickListener clickToDecryptListener = new OnClickListener() {
313
314 @Override
315 public void onClick(View v) {
316 PendingIntent pendingIntent = conversation.getAccount().getPgpDecryptionService().getPendingIntent();
317 if (pendingIntent != null) {
318 try {
319 activity.startIntentSenderForResult(pendingIntent.getIntentSender(),
320 ConversationActivity.REQUEST_DECRYPT_PGP,
321 null,
322 0,
323 0,
324 0);
325 } catch (SendIntentException e) {
326 Toast.makeText(activity, R.string.unable_to_connect_to_keychain, Toast.LENGTH_SHORT).show();
327 conversation.getAccount().getPgpDecryptionService().continueDecryption(true);
328 }
329 }
330 updateSnackBar(conversation);
331 }
332 };
333 private AtomicBoolean mSendingPgpMessage = new AtomicBoolean(false);
334 private OnEditorActionListener mEditorActionListener = new OnEditorActionListener() {
335
336 @Override
337 public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
338 if (actionId == EditorInfo.IME_ACTION_SEND) {
339 InputMethodManager imm = (InputMethodManager) v.getContext()
340 .getSystemService(Context.INPUT_METHOD_SERVICE);
341 if (imm.isFullscreenMode()) {
342 imm.hideSoftInputFromWindow(v.getWindowToken(), 0);
343 }
344 sendMessage();
345 return true;
346 } else {
347 return false;
348 }
349 }
350 };
351 private OnClickListener mSendButtonListener = new OnClickListener() {
352
353 @Override
354 public void onClick(View v) {
355 Object tag = v.getTag();
356 if (tag instanceof SendButtonAction) {
357 SendButtonAction action = (SendButtonAction) tag;
358 switch (action) {
359 case TAKE_PHOTO:
360 activity.attachFile(ConversationActivity.ATTACHMENT_CHOICE_TAKE_PHOTO);
361 break;
362 case RECORD_VIDEO:
363 activity.attachFile(ConversationActivity.ATTACHMENT_CHOICE_RECORD_VIDEO);
364 break;
365 case SEND_LOCATION:
366 activity.attachFile(ConversationActivity.ATTACHMENT_CHOICE_LOCATION);
367 break;
368 case RECORD_VOICE:
369 activity.attachFile(ConversationActivity.ATTACHMENT_CHOICE_RECORD_VOICE);
370 break;
371 case CHOOSE_PICTURE:
372 activity.attachFile(ConversationActivity.ATTACHMENT_CHOICE_CHOOSE_IMAGE);
373 break;
374 case CANCEL:
375 if (conversation != null) {
376 if (conversation.setCorrectingMessage(null)) {
377 mEditMessage.setText("");
378 mEditMessage.append(conversation.getDraftMessage());
379 conversation.setDraftMessage(null);
380 } else if (conversation.getMode() == Conversation.MODE_MULTI) {
381 conversation.setNextCounterpart(null);
382 }
383 updateChatMsgHint();
384 updateSendButton();
385 updateEditablity();
386 }
387 break;
388 default:
389 sendMessage();
390 }
391 } else {
392 sendMessage();
393 }
394 }
395 };
396 private int completionIndex = 0;
397 private int lastCompletionLength = 0;
398 private String incomplete;
399 private int lastCompletionCursor;
400 private boolean firstWord = false;
401
402 private int getIndexOf(String uuid, List<Message> messages) {
403 if (uuid == null) {
404 return messages.size() - 1;
405 }
406 for (int i = 0; i < messages.size(); ++i) {
407 if (uuid.equals(messages.get(i).getUuid())) {
408 return i;
409 } else {
410 Message next = messages.get(i);
411 while (next != null && next.wasMergedIntoPrevious()) {
412 if (uuid.equals(next.getUuid())) {
413 return i;
414 }
415 next = next.next();
416 }
417
418 }
419 }
420 return -1;
421 }
422
423 public Pair<Integer, Integer> getScrollPosition() {
424 if (this.messagesView.getCount() == 0 ||
425 this.messagesView.getLastVisiblePosition() == this.messagesView.getCount() - 1) {
426 return null;
427 } else {
428 final int pos = messagesView.getFirstVisiblePosition();
429 final View view = messagesView.getChildAt(0);
430 if (view == null) {
431 return null;
432 } else {
433 return new Pair<>(pos, view.getTop());
434 }
435 }
436 }
437
438 public void setScrollPosition(Pair<Integer, Integer> scrollPosition) {
439 if (scrollPosition != null) {
440 this.messagesView.setSelectionFromTop(scrollPosition.first, scrollPosition.second);
441 }
442 }
443
444 private void sendMessage() {
445 final String body = mEditMessage.getText().toString();
446 final Conversation conversation = this.conversation;
447 if (body.length() == 0 || conversation == null) {
448 return;
449 }
450 final Message message;
451 if (conversation.getCorrectingMessage() == null) {
452 message = new Message(conversation, body, conversation.getNextEncryption());
453 if (conversation.getMode() == Conversation.MODE_MULTI) {
454 final Jid nextCounterpart = conversation.getNextCounterpart();
455 if (nextCounterpart != null) {
456 message.setCounterpart(nextCounterpart);
457 message.setTrueCounterpart(conversation.getMucOptions().getTrueCounterpart(nextCounterpart));
458 message.setType(Message.TYPE_PRIVATE);
459 }
460 }
461 } else {
462 message = conversation.getCorrectingMessage();
463 message.setBody(body);
464 message.setEdited(message.getUuid());
465 message.setUuid(UUID.randomUUID().toString());
466 }
467 switch (message.getConversation().getNextEncryption()) {
468 case Message.ENCRYPTION_PGP:
469 sendPgpMessage(message);
470 break;
471 case Message.ENCRYPTION_AXOLOTL:
472 if (!activity.trustKeysIfNeeded(ConversationActivity.REQUEST_TRUST_KEYS_TEXT)) {
473 sendAxolotlMessage(message);
474 }
475 break;
476 default:
477 sendPlainTextMessage(message);
478 }
479 }
480
481 public void updateChatMsgHint() {
482 final boolean multi = conversation.getMode() == Conversation.MODE_MULTI;
483 if (conversation.getCorrectingMessage() != null) {
484 this.mEditMessage.setHint(R.string.send_corrected_message);
485 } else if (multi && conversation.getNextCounterpart() != null) {
486 this.mEditMessage.setHint(getString(
487 R.string.send_private_message_to,
488 conversation.getNextCounterpart().getResourcepart()));
489 } else if (multi && !conversation.getMucOptions().participating()) {
490 this.mEditMessage.setHint(R.string.you_are_not_participating);
491 } else {
492 this.mEditMessage.setHint(UIHelper.getMessageHint(activity, conversation));
493 getActivity().invalidateOptionsMenu();
494 }
495 }
496
497 public void setupIme() {
498 if (activity != null) {
499 if (activity.usingEnterKey() && activity.enterIsSend()) {
500 mEditMessage.setInputType(mEditMessage.getInputType() & (~InputType.TYPE_TEXT_FLAG_MULTI_LINE));
501 mEditMessage.setInputType(mEditMessage.getInputType() & (~InputType.TYPE_TEXT_VARIATION_SHORT_MESSAGE));
502 } else if (activity.usingEnterKey()) {
503 mEditMessage.setInputType(mEditMessage.getInputType() | InputType.TYPE_TEXT_FLAG_MULTI_LINE);
504 mEditMessage.setInputType(mEditMessage.getInputType() & (~InputType.TYPE_TEXT_VARIATION_SHORT_MESSAGE));
505 } else {
506 mEditMessage.setInputType(mEditMessage.getInputType() | InputType.TYPE_TEXT_FLAG_MULTI_LINE);
507 mEditMessage.setInputType(mEditMessage.getInputType() | InputType.TYPE_TEXT_VARIATION_SHORT_MESSAGE);
508 }
509 }
510 }
511
512 @Override
513 public View onCreateView(final LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
514 final View view = inflater.inflate(R.layout.fragment_conversation, container, false);
515 view.setOnClickListener(null);
516
517 mEditMessage = (EditMessage) view.findViewById(R.id.textinput);
518 mEditMessage.setOnClickListener(new OnClickListener() {
519
520 @Override
521 public void onClick(View v) {
522 if (activity != null) {
523 activity.hideConversationsOverview();
524 }
525 }
526 });
527
528 mEditMessage.addTextChangedListener(new StylingHelper.MessageEditorStyler(mEditMessage));
529
530 mEditMessage.setOnEditorActionListener(mEditorActionListener);
531 mEditMessage.setRichContentListener(new String[]{"image/*"}, mEditorContentListener);
532
533 mSendButton = (ImageButton) view.findViewById(R.id.textSendButton);
534 mSendButton.setOnClickListener(this.mSendButtonListener);
535
536 snackbar = (RelativeLayout) view.findViewById(R.id.snackbar);
537 snackbarMessage = (TextView) view.findViewById(R.id.snackbar_message);
538 snackbarAction = (TextView) view.findViewById(R.id.snackbar_action);
539
540 messagesView = (ListView) view.findViewById(R.id.messages_view);
541 messagesView.setOnScrollListener(mOnScrollListener);
542 messagesView.setTranscriptMode(ListView.TRANSCRIPT_MODE_NORMAL);
543 messageListAdapter = new MessageAdapter((ConversationActivity) getActivity(), this.messageList);
544 messageListAdapter.setOnContactPictureClicked(message -> {
545 final boolean received = message.getStatus() <= Message.STATUS_RECEIVED;
546 if (received) {
547 if (message.getConversation().getMode() == Conversation.MODE_MULTI) {
548 Jid user = message.getCounterpart();
549 if (user != null && !user.isBareJid()) {
550 if (!message.getConversation().getMucOptions().isUserInRoom(user)) {
551 Toast.makeText(activity, activity.getString(R.string.user_has_left_conference, user.getResourcepart()), Toast.LENGTH_SHORT).show();
552 }
553 highlightInConference(user.getResourcepart());
554 }
555 return;
556 } else {
557 if (!message.getContact().isSelf()) {
558 String fingerprint;
559 if (message.getEncryption() == Message.ENCRYPTION_PGP
560 || message.getEncryption() == Message.ENCRYPTION_DECRYPTED) {
561 fingerprint = "pgp";
562 } else {
563 fingerprint = message.getFingerprint();
564 }
565 activity.switchToContactDetails(message.getContact(), fingerprint);
566 return;
567 }
568 }
569 }
570 Account account = message.getConversation().getAccount();
571 Intent intent;
572 if (activity.manuallyChangePresence() && !received) {
573 intent = new Intent(activity, SetPresenceActivity.class);
574 intent.putExtra(SetPresenceActivity.EXTRA_ACCOUNT, account.getJid().toBareJid().toString());
575 } else {
576 intent = new Intent(activity, EditAccountActivity.class);
577 intent.putExtra("jid", account.getJid().toBareJid().toString());
578 String fingerprint;
579 if (message.getEncryption() == Message.ENCRYPTION_PGP
580 || message.getEncryption() == Message.ENCRYPTION_DECRYPTED) {
581 fingerprint = "pgp";
582 } else {
583 fingerprint = message.getFingerprint();
584 }
585 intent.putExtra("fingerprint", fingerprint);
586 }
587 startActivity(intent);
588 });
589 messageListAdapter.setOnContactPictureLongClicked(message -> {
590 if (message.getStatus() <= Message.STATUS_RECEIVED) {
591 if (message.getConversation().getMode() == Conversation.MODE_MULTI) {
592 final MucOptions mucOptions = conversation.getMucOptions();
593 if (!mucOptions.allowPm()) {
594 Toast.makeText(activity, R.string.private_messages_are_disabled, Toast.LENGTH_SHORT).show();
595 return;
596 }
597 Jid user = message.getCounterpart();
598 if (user != null && !user.isBareJid()) {
599 if (mucOptions.isUserInRoom(user)) {
600 privateMessageWith(user);
601 } else {
602 Toast.makeText(activity, activity.getString(R.string.user_has_left_conference, user.getResourcepart()), Toast.LENGTH_SHORT).show();
603 }
604 }
605 }
606 } else {
607 activity.showQrCode();
608 }
609 });
610 messageListAdapter.setOnQuoteListener(this::quoteText);
611 messagesView.setAdapter(messageListAdapter);
612
613 registerForContextMenu(messagesView);
614
615 return view;
616 }
617
618 private void quoteText(String text) {
619 if (mEditMessage.isEnabled()) {
620 text = text.replaceAll("(\n *){2,}", "\n").replaceAll("(^|\n)", "$1> ").replaceAll("\n$", "");
621 Editable editable = mEditMessage.getEditableText();
622 int position = mEditMessage.getSelectionEnd();
623 if (position == -1) position = editable.length();
624 if (position > 0 && editable.charAt(position - 1) != '\n') {
625 editable.insert(position++, "\n");
626 }
627 editable.insert(position, text);
628 position += text.length();
629 editable.insert(position++, "\n");
630 if (position < editable.length() && editable.charAt(position) != '\n') {
631 editable.insert(position, "\n");
632 }
633 mEditMessage.setSelection(position);
634 mEditMessage.requestFocus();
635 InputMethodManager inputMethodManager = (InputMethodManager) getActivity()
636 .getSystemService(Context.INPUT_METHOD_SERVICE);
637 if (inputMethodManager != null) {
638 inputMethodManager.showSoftInput(mEditMessage, InputMethodManager.SHOW_IMPLICIT);
639 }
640 }
641 }
642
643 private void quoteMessage(Message message) {
644 quoteText(MessageUtils.prepareQuote(message));
645 }
646
647 @Override
648 public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
649 synchronized (this.messageList) {
650 super.onCreateContextMenu(menu, v, menuInfo);
651 AdapterView.AdapterContextMenuInfo acmi = (AdapterContextMenuInfo) menuInfo;
652 this.selectedMessage = this.messageList.get(acmi.position);
653 populateContextMenu(menu);
654 }
655 }
656
657 private void populateContextMenu(ContextMenu menu) {
658 final Message m = this.selectedMessage;
659 final Transferable t = m.getTransferable();
660 Message relevantForCorrection = m;
661 while (relevantForCorrection.mergeable(relevantForCorrection.next())) {
662 relevantForCorrection = relevantForCorrection.next();
663 }
664 if (m.getType() != Message.TYPE_STATUS) {
665 final boolean treatAsFile = m.getType() != Message.TYPE_TEXT
666 && m.getType() != Message.TYPE_PRIVATE
667 && t == null;
668 final boolean encrypted = m.getEncryption() == Message.ENCRYPTION_DECRYPTION_FAILED
669 || m.getEncryption() == Message.ENCRYPTION_PGP;
670 activity.getMenuInflater().inflate(R.menu.message_context, menu);
671 menu.setHeaderTitle(R.string.message_options);
672 MenuItem copyMessage = menu.findItem(R.id.copy_message);
673 MenuItem quoteMessage = menu.findItem(R.id.quote_message);
674 MenuItem retryDecryption = menu.findItem(R.id.retry_decryption);
675 MenuItem correctMessage = menu.findItem(R.id.correct_message);
676 MenuItem shareWith = menu.findItem(R.id.share_with);
677 MenuItem sendAgain = menu.findItem(R.id.send_again);
678 MenuItem copyUrl = menu.findItem(R.id.copy_url);
679 MenuItem downloadFile = menu.findItem(R.id.download_file);
680 MenuItem cancelTransmission = menu.findItem(R.id.cancel_transmission);
681 MenuItem deleteFile = menu.findItem(R.id.delete_file);
682 MenuItem showErrorMessage = menu.findItem(R.id.show_error_message);
683 if (!treatAsFile && !encrypted && !m.isGeoUri() && !m.treatAsDownloadable()) {
684 copyMessage.setVisible(true);
685 quoteMessage.setVisible(MessageUtils.prepareQuote(m).length() > 0);
686 }
687 if (m.getEncryption() == Message.ENCRYPTION_DECRYPTION_FAILED) {
688 retryDecryption.setVisible(true);
689 }
690 if (relevantForCorrection.getType() == Message.TYPE_TEXT
691 && relevantForCorrection.isLastCorrectableMessage()
692 && (m.getConversation().getMucOptions().nonanonymous() || m.getConversation().getMode() == Conversation.MODE_SINGLE)) {
693 correctMessage.setVisible(true);
694 }
695 if (treatAsFile || (m.getType() == Message.TYPE_TEXT && !m.treatAsDownloadable())) {
696 shareWith.setVisible(true);
697 }
698 if (m.getStatus() == Message.STATUS_SEND_FAILED) {
699 sendAgain.setVisible(true);
700 }
701 if (m.hasFileOnRemoteHost()
702 || m.isGeoUri()
703 || m.treatAsDownloadable()
704 || (t != null && t instanceof HttpDownloadConnection)) {
705 copyUrl.setVisible(true);
706 }
707 if ((m.isFileOrImage() && t instanceof TransferablePlaceholder && m.hasFileOnRemoteHost())) {
708 downloadFile.setVisible(true);
709 downloadFile.setTitle(activity.getString(R.string.download_x_file, UIHelper.getFileDescriptionString(activity, m)));
710 }
711 boolean waitingOfferedSending = m.getStatus() == Message.STATUS_WAITING
712 || m.getStatus() == Message.STATUS_UNSEND
713 || m.getStatus() == Message.STATUS_OFFERED;
714 if ((t != null && !(t instanceof TransferablePlaceholder)) || waitingOfferedSending && m.needsUploading()) {
715 cancelTransmission.setVisible(true);
716 }
717 if (treatAsFile) {
718 String path = m.getRelativeFilePath();
719 if (path == null || !path.startsWith("/")) {
720 deleteFile.setVisible(true);
721 deleteFile.setTitle(activity.getString(R.string.delete_x_file, UIHelper.getFileDescriptionString(activity, m)));
722 }
723 }
724 if (m.getStatus() == Message.STATUS_SEND_FAILED && m.getErrorMessage() != null) {
725 showErrorMessage.setVisible(true);
726 }
727 }
728 }
729
730 @Override
731 public boolean onContextItemSelected(MenuItem item) {
732 switch (item.getItemId()) {
733 case R.id.share_with:
734 shareWith(selectedMessage);
735 return true;
736 case R.id.correct_message:
737 correctMessage(selectedMessage);
738 return true;
739 case R.id.copy_message:
740 copyMessage(selectedMessage);
741 return true;
742 case R.id.quote_message:
743 quoteMessage(selectedMessage);
744 return true;
745 case R.id.send_again:
746 resendMessage(selectedMessage);
747 return true;
748 case R.id.copy_url:
749 copyUrl(selectedMessage);
750 return true;
751 case R.id.download_file:
752 downloadFile(selectedMessage);
753 return true;
754 case R.id.cancel_transmission:
755 cancelTransmission(selectedMessage);
756 return true;
757 case R.id.retry_decryption:
758 retryDecryption(selectedMessage);
759 return true;
760 case R.id.delete_file:
761 deleteFile(selectedMessage);
762 return true;
763 case R.id.show_error_message:
764 showErrorMessage(selectedMessage);
765 return true;
766 default:
767 return super.onContextItemSelected(item);
768 }
769 }
770
771 private void showErrorMessage(final Message message) {
772 AlertDialog.Builder builder = new AlertDialog.Builder(activity);
773 builder.setTitle(R.string.error_message);
774 builder.setMessage(message.getErrorMessage());
775 builder.setPositiveButton(R.string.confirm, null);
776 builder.create().show();
777 }
778
779 private void shareWith(Message message) {
780 Intent shareIntent = new Intent();
781 shareIntent.setAction(Intent.ACTION_SEND);
782 if (message.isGeoUri()) {
783 shareIntent.putExtra(Intent.EXTRA_TEXT, message.getBody());
784 shareIntent.setType("text/plain");
785 } else if (!message.isFileOrImage()) {
786 shareIntent.putExtra(Intent.EXTRA_TEXT, message.getMergedBody().toString());
787 shareIntent.setType("text/plain");
788 } else {
789 final DownloadableFile file = activity.xmppConnectionService.getFileBackend().getFile(message);
790 try {
791 shareIntent.putExtra(Intent.EXTRA_STREAM, FileBackend.getUriForFile(activity, file));
792 } catch (SecurityException e) {
793 Toast.makeText(activity, activity.getString(R.string.no_permission_to_access_x, file.getAbsolutePath()), Toast.LENGTH_SHORT).show();
794 return;
795 }
796 shareIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
797 String mime = message.getMimeType();
798 if (mime == null) {
799 mime = "*/*";
800 }
801 shareIntent.setType(mime);
802 }
803 try {
804 activity.startActivity(Intent.createChooser(shareIntent, getText(R.string.share_with)));
805 } catch (ActivityNotFoundException e) {
806 //This should happen only on faulty androids because normally chooser is always available
807 Toast.makeText(activity, R.string.no_application_found_to_open_file, Toast.LENGTH_SHORT).show();
808 }
809 }
810
811 private void copyMessage(Message message) {
812 if (activity.copyTextToClipboard(message.getMergedBody().toString(), R.string.message)) {
813 Toast.makeText(activity, R.string.message_copied_to_clipboard, Toast.LENGTH_SHORT).show();
814 }
815 }
816
817 private void deleteFile(Message message) {
818 if (activity.xmppConnectionService.getFileBackend().deleteFile(message)) {
819 message.setTransferable(new TransferablePlaceholder(Transferable.STATUS_DELETED));
820 activity.updateConversationList();
821 updateMessages();
822 }
823 }
824
825 private void resendMessage(final Message message) {
826 if (message.isFileOrImage()) {
827 DownloadableFile file = activity.xmppConnectionService.getFileBackend().getFile(message);
828 if (file.exists()) {
829 final Conversation conversation = message.getConversation();
830 final XmppConnection xmppConnection = conversation.getAccount().getXmppConnection();
831 if (!message.hasFileOnRemoteHost()
832 && xmppConnection != null
833 && !xmppConnection.getFeatures().httpUpload(message.getFileParams().size)) {
834 activity.selectPresence(conversation, new OnPresenceSelected() {
835 @Override
836 public void onPresenceSelected() {
837 message.setCounterpart(conversation.getNextCounterpart());
838 activity.xmppConnectionService.resendFailedMessages(message);
839 }
840 });
841 return;
842 }
843 } else {
844 Toast.makeText(activity, R.string.file_deleted, Toast.LENGTH_SHORT).show();
845 message.setTransferable(new TransferablePlaceholder(Transferable.STATUS_DELETED));
846 activity.updateConversationList();
847 updateMessages();
848 return;
849 }
850 }
851 activity.xmppConnectionService.resendFailedMessages(message);
852 }
853
854 private void copyUrl(Message message) {
855 final String url;
856 final int resId;
857 if (message.isGeoUri()) {
858 resId = R.string.location;
859 url = message.getBody();
860 } else if (message.hasFileOnRemoteHost()) {
861 resId = R.string.file_url;
862 url = message.getFileParams().url.toString();
863 } else {
864 url = message.getBody().trim();
865 resId = R.string.file_url;
866 }
867 if (activity.copyTextToClipboard(url, resId)) {
868 Toast.makeText(activity, R.string.url_copied_to_clipboard,
869 Toast.LENGTH_SHORT).show();
870 }
871 }
872
873 private void downloadFile(Message message) {
874 activity.xmppConnectionService.getHttpConnectionManager().createNewDownloadConnection(message, true);
875 }
876
877 private void cancelTransmission(Message message) {
878 Transferable transferable = message.getTransferable();
879 if (transferable != null) {
880 transferable.cancel();
881 } else if (message.getStatus() != Message.STATUS_RECEIVED) {
882 activity.xmppConnectionService.markMessage(message, Message.STATUS_SEND_FAILED);
883 }
884 }
885
886 private void retryDecryption(Message message) {
887 message.setEncryption(Message.ENCRYPTION_PGP);
888 activity.updateConversationList();
889 updateMessages();
890 conversation.getAccount().getPgpDecryptionService().decrypt(message, false);
891 }
892
893 protected void privateMessageWith(final Jid counterpart) {
894 if (conversation.setOutgoingChatState(Config.DEFAULT_CHATSTATE)) {
895 activity.xmppConnectionService.sendChatState(conversation);
896 }
897 this.mEditMessage.setText("");
898 this.conversation.setNextCounterpart(counterpart);
899 updateChatMsgHint();
900 updateSendButton();
901 updateEditablity();
902 }
903
904 private void correctMessage(Message message) {
905 while (message.mergeable(message.next())) {
906 message = message.next();
907 }
908 this.conversation.setCorrectingMessage(message);
909 final Editable editable = mEditMessage.getText();
910 this.conversation.setDraftMessage(editable.toString());
911 this.mEditMessage.setText("");
912 this.mEditMessage.append(message.getBody());
913
914 }
915
916 protected void highlightInConference(String nick) {
917 final Editable editable = mEditMessage.getText();
918 String oldString = editable.toString().trim();
919 final int pos = mEditMessage.getSelectionStart();
920 if (oldString.isEmpty() || pos == 0) {
921 editable.insert(0, nick + ": ");
922 } else {
923 final char before = editable.charAt(pos - 1);
924 final char after = editable.length() > pos ? editable.charAt(pos) : '\0';
925 if (before == '\n') {
926 editable.insert(pos, nick + ": ");
927 } else {
928 if (pos > 2 && editable.subSequence(pos - 2, pos).toString().equals(": ")) {
929 if (NickValidityChecker.check(conversation, Arrays.asList(editable.subSequence(0, pos - 2).toString().split(", ")))) {
930 editable.insert(pos - 2, ", " + nick);
931 return;
932 }
933 }
934 editable.insert(pos, (Character.isWhitespace(before) ? "" : " ") + nick + (Character.isWhitespace(after) ? "" : " "));
935 if (Character.isWhitespace(after)) {
936 mEditMessage.setSelection(mEditMessage.getSelectionStart() + 1);
937 }
938 }
939 }
940 }
941
942 @Override
943 public void onStop() {
944 super.onStop();
945 if (activity == null || !activity.isChangingConfigurations()) {
946 messageListAdapter.stopAudioPlayer();
947 }
948 if (this.conversation != null) {
949 final String msg = mEditMessage.getText().toString();
950 if (this.conversation.setNextMessage(msg)) {
951 activity.xmppConnectionService.updateConversation(this.conversation);
952 }
953 updateChatState(this.conversation, msg);
954 }
955 }
956
957 private void updateChatState(final Conversation conversation, final String msg) {
958 ChatState state = msg.length() == 0 ? Config.DEFAULT_CHATSTATE : ChatState.PAUSED;
959 Account.State status = conversation.getAccount().getStatus();
960 if (status == Account.State.ONLINE && conversation.setOutgoingChatState(state)) {
961 activity.xmppConnectionService.sendChatState(conversation);
962 }
963 }
964
965 public boolean reInit(Conversation conversation) {
966 if (conversation == null) {
967 return false;
968 }
969 this.activity = (ConversationActivity) getActivity();
970 setupIme();
971 if (this.conversation != null) {
972 final String msg = mEditMessage.getText().toString();
973 if (this.conversation.setNextMessage(msg)) {
974 activity.xmppConnectionService.updateConversation(conversation);
975 }
976 if (this.conversation != conversation) {
977 updateChatState(this.conversation, msg);
978 messageListAdapter.stopAudioPlayer();
979 }
980 this.conversation.trim();
981
982 }
983
984 if (activity != null) {
985 this.mSendButton.setContentDescription(activity.getString(R.string.send_message_to_x, conversation.getName()));
986 }
987
988 this.conversation = conversation;
989 this.mEditMessage.setKeyboardListener(null);
990 this.mEditMessage.setText("");
991 this.mEditMessage.append(this.conversation.getNextMessage());
992 this.mEditMessage.setKeyboardListener(this);
993 messageListAdapter.updatePreferences();
994 this.messagesView.setAdapter(messageListAdapter);
995 updateMessages();
996 this.conversation.messagesLoaded.set(true);
997 synchronized (this.messageList) {
998 final Message first = conversation.getFirstUnreadMessage();
999 final int bottom = Math.max(0, this.messageList.size() - 1);
1000 final int pos;
1001 if (first == null) {
1002 pos = bottom;
1003 } else {
1004 int i = getIndexOf(first.getUuid(), this.messageList);
1005 pos = i < 0 ? bottom : i;
1006 }
1007 messagesView.setSelection(pos);
1008 return pos == bottom;
1009 }
1010 }
1011
1012 private void showBlockSubmenu(View view) {
1013 final Jid jid = conversation.getJid();
1014 if (jid.isDomainJid()) {
1015 BlockContactDialog.show(activity, conversation);
1016 } else {
1017 PopupMenu popupMenu = new PopupMenu(activity, view);
1018 popupMenu.inflate(R.menu.block);
1019 popupMenu.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() {
1020 @Override
1021 public boolean onMenuItemClick(MenuItem menuItem) {
1022 Blockable blockable;
1023 switch (menuItem.getItemId()) {
1024 case R.id.block_domain:
1025 blockable = conversation.getAccount().getRoster().getContact(jid.toDomainJid());
1026 break;
1027 default:
1028 blockable = conversation;
1029 }
1030 BlockContactDialog.show(activity, blockable);
1031 return true;
1032 }
1033 });
1034 popupMenu.show();
1035 }
1036 }
1037
1038 private void updateSnackBar(final Conversation conversation) {
1039 final Account account = conversation.getAccount();
1040 final XmppConnection connection = account.getXmppConnection();
1041 final int mode = conversation.getMode();
1042 final Contact contact = mode == Conversation.MODE_SINGLE ? conversation.getContact() : null;
1043 if (account.getStatus() == Account.State.DISABLED) {
1044 showSnackbar(R.string.this_account_is_disabled, R.string.enable, this.mEnableAccountListener);
1045 } else if (conversation.isBlocked()) {
1046 showSnackbar(R.string.contact_blocked, R.string.unblock, this.mUnblockClickListener);
1047 } else if (contact != null && !contact.showInRoster() && contact.getOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST)) {
1048 showSnackbar(R.string.contact_added_you, R.string.add_back, this.mAddBackClickListener, this.mLongPressBlockListener);
1049 } else if (contact != null && contact.getOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST)) {
1050 showSnackbar(R.string.contact_asks_for_presence_subscription, R.string.allow, this.mAllowPresenceSubscription, this.mLongPressBlockListener);
1051 } else if (mode == Conversation.MODE_MULTI
1052 && !conversation.getMucOptions().online()
1053 && account.getStatus() == Account.State.ONLINE) {
1054 switch (conversation.getMucOptions().getError()) {
1055 case NICK_IN_USE:
1056 showSnackbar(R.string.nick_in_use, R.string.edit, clickToMuc);
1057 break;
1058 case NO_RESPONSE:
1059 showSnackbar(R.string.joining_conference, 0, null);
1060 break;
1061 case SERVER_NOT_FOUND:
1062 if (conversation.receivedMessagesCount() > 0) {
1063 showSnackbar(R.string.remote_server_not_found, R.string.try_again, joinMuc);
1064 } else {
1065 showSnackbar(R.string.remote_server_not_found, R.string.leave, leaveMuc);
1066 }
1067 break;
1068 case PASSWORD_REQUIRED:
1069 showSnackbar(R.string.conference_requires_password, R.string.enter_password, enterPassword);
1070 break;
1071 case BANNED:
1072 showSnackbar(R.string.conference_banned, R.string.leave, leaveMuc);
1073 break;
1074 case MEMBERS_ONLY:
1075 showSnackbar(R.string.conference_members_only, R.string.leave, leaveMuc);
1076 break;
1077 case KICKED:
1078 showSnackbar(R.string.conference_kicked, R.string.join, joinMuc);
1079 break;
1080 case UNKNOWN:
1081 showSnackbar(R.string.conference_unknown_error, R.string.try_again, joinMuc);
1082 break;
1083 case INVALID_NICK:
1084 showSnackbar(R.string.invalid_muc_nick, R.string.edit, clickToMuc);
1085 case SHUTDOWN:
1086 showSnackbar(R.string.conference_shutdown, R.string.try_again, joinMuc);
1087 break;
1088 default:
1089 hideSnackbar();
1090 break;
1091 }
1092 } else if (account.hasPendingPgpIntent(conversation)) {
1093 showSnackbar(R.string.openpgp_messages_found, R.string.decrypt, clickToDecryptListener);
1094 } else if (connection != null
1095 && connection.getFeatures().blocking()
1096 && conversation.countMessages() != 0
1097 && !conversation.isBlocked()
1098 && conversation.isWithStranger()) {
1099 showSnackbar(R.string.received_message_from_stranger, R.string.block, mBlockClickListener);
1100 } else {
1101 hideSnackbar();
1102 }
1103 }
1104
1105 public void updateMessages() {
1106 synchronized (this.messageList) {
1107 if (getView() == null) {
1108 return;
1109 }
1110 final ConversationActivity activity = (ConversationActivity) getActivity();
1111 if (this.conversation != null) {
1112 conversation.populateWithMessages(ConversationFragment.this.messageList);
1113 updateSnackBar(conversation);
1114 updateStatusMessages();
1115 this.messageListAdapter.notifyDataSetChanged();
1116 updateChatMsgHint();
1117 if (!activity.isConversationsOverviewVisable() || !activity.isConversationsOverviewHideable()) {
1118 activity.sendReadMarkerIfNecessary(conversation);
1119 }
1120 updateSendButton();
1121 updateEditablity();
1122 }
1123 }
1124 }
1125
1126 protected void messageSent() {
1127 mSendingPgpMessage.set(false);
1128 mEditMessage.setText("");
1129 if (conversation.setCorrectingMessage(null)) {
1130 mEditMessage.append(conversation.getDraftMessage());
1131 conversation.setDraftMessage(null);
1132 }
1133 if (conversation.setNextMessage(mEditMessage.getText().toString())) {
1134 activity.xmppConnectionService.updateConversation(conversation);
1135 }
1136 updateChatMsgHint();
1137 new Handler().post(new Runnable() {
1138 @Override
1139 public void run() {
1140 int size = messageList.size();
1141 messagesView.setSelection(size - 1);
1142 }
1143 });
1144 }
1145
1146 public void setFocusOnInputField() {
1147 mEditMessage.requestFocus();
1148 }
1149
1150 public void doneSendingPgpMessage() {
1151 mSendingPgpMessage.set(false);
1152 }
1153
1154 private int getSendButtonImageResource(SendButtonAction action, Presence.Status status) {
1155 switch (action) {
1156 case TEXT:
1157 switch (status) {
1158 case CHAT:
1159 case ONLINE:
1160 return R.drawable.ic_send_text_online;
1161 case AWAY:
1162 return R.drawable.ic_send_text_away;
1163 case XA:
1164 case DND:
1165 return R.drawable.ic_send_text_dnd;
1166 default:
1167 return activity.getThemeResource(R.attr.ic_send_text_offline, R.drawable.ic_send_text_offline);
1168 }
1169 case RECORD_VIDEO:
1170 switch (status) {
1171 case CHAT:
1172 case ONLINE:
1173 return R.drawable.ic_send_videocam_online;
1174 case AWAY:
1175 return R.drawable.ic_send_videocam_away;
1176 case XA:
1177 case DND:
1178 return R.drawable.ic_send_videocam_dnd;
1179 default:
1180 return activity.getThemeResource(R.attr.ic_send_videocam_offline, R.drawable.ic_send_videocam_offline);
1181 }
1182 case TAKE_PHOTO:
1183 switch (status) {
1184 case CHAT:
1185 case ONLINE:
1186 return R.drawable.ic_send_photo_online;
1187 case AWAY:
1188 return R.drawable.ic_send_photo_away;
1189 case XA:
1190 case DND:
1191 return R.drawable.ic_send_photo_dnd;
1192 default:
1193 return activity.getThemeResource(R.attr.ic_send_photo_offline, R.drawable.ic_send_photo_offline);
1194 }
1195 case RECORD_VOICE:
1196 switch (status) {
1197 case CHAT:
1198 case ONLINE:
1199 return R.drawable.ic_send_voice_online;
1200 case AWAY:
1201 return R.drawable.ic_send_voice_away;
1202 case XA:
1203 case DND:
1204 return R.drawable.ic_send_voice_dnd;
1205 default:
1206 return activity.getThemeResource(R.attr.ic_send_voice_offline, R.drawable.ic_send_voice_offline);
1207 }
1208 case SEND_LOCATION:
1209 switch (status) {
1210 case CHAT:
1211 case ONLINE:
1212 return R.drawable.ic_send_location_online;
1213 case AWAY:
1214 return R.drawable.ic_send_location_away;
1215 case XA:
1216 case DND:
1217 return R.drawable.ic_send_location_dnd;
1218 default:
1219 return activity.getThemeResource(R.attr.ic_send_location_offline, R.drawable.ic_send_location_offline);
1220 }
1221 case CANCEL:
1222 switch (status) {
1223 case CHAT:
1224 case ONLINE:
1225 return R.drawable.ic_send_cancel_online;
1226 case AWAY:
1227 return R.drawable.ic_send_cancel_away;
1228 case XA:
1229 case DND:
1230 return R.drawable.ic_send_cancel_dnd;
1231 default:
1232 return activity.getThemeResource(R.attr.ic_send_cancel_offline, R.drawable.ic_send_cancel_offline);
1233 }
1234 case CHOOSE_PICTURE:
1235 switch (status) {
1236 case CHAT:
1237 case ONLINE:
1238 return R.drawable.ic_send_picture_online;
1239 case AWAY:
1240 return R.drawable.ic_send_picture_away;
1241 case XA:
1242 case DND:
1243 return R.drawable.ic_send_picture_dnd;
1244 default:
1245 return activity.getThemeResource(R.attr.ic_send_picture_offline, R.drawable.ic_send_picture_offline);
1246 }
1247 }
1248 return activity.getThemeResource(R.attr.ic_send_text_offline, R.drawable.ic_send_text_offline);
1249 }
1250
1251 private void updateEditablity() {
1252 boolean canWrite = this.conversation.getMode() == Conversation.MODE_SINGLE || this.conversation.getMucOptions().participating() || this.conversation.getNextCounterpart() != null;
1253 this.mEditMessage.setFocusable(canWrite);
1254 this.mEditMessage.setFocusableInTouchMode(canWrite);
1255 this.mSendButton.setEnabled(canWrite);
1256 this.mEditMessage.setCursorVisible(canWrite);
1257 }
1258
1259 public void updateSendButton() {
1260 final Conversation c = this.conversation;
1261 final SendButtonAction action;
1262 final Presence.Status status;
1263 final String text = this.mEditMessage == null ? "" : this.mEditMessage.getText().toString();
1264 final boolean empty = text.length() == 0;
1265 final boolean conference = c.getMode() == Conversation.MODE_MULTI;
1266 if (c.getCorrectingMessage() != null && (empty || text.equals(c.getCorrectingMessage().getBody()))) {
1267 action = SendButtonAction.CANCEL;
1268 } else if (conference && !c.getAccount().httpUploadAvailable()) {
1269 if (empty && c.getNextCounterpart() != null) {
1270 action = SendButtonAction.CANCEL;
1271 } else {
1272 action = SendButtonAction.TEXT;
1273 }
1274 } else {
1275 if (empty) {
1276 if (conference && c.getNextCounterpart() != null) {
1277 action = SendButtonAction.CANCEL;
1278 } else {
1279 String setting = activity.getPreferences().getString("quick_action", activity.getResources().getString(R.string.quick_action));
1280 if (!setting.equals("none") && UIHelper.receivedLocationQuestion(conversation.getLatestMessage())) {
1281 action = SendButtonAction.SEND_LOCATION;
1282 } else {
1283 if (setting.equals("recent")) {
1284 setting = activity.getPreferences().getString(ConversationActivity.RECENTLY_USED_QUICK_ACTION, SendButtonAction.TEXT.toString());
1285 action = SendButtonAction.valueOfOrDefault(setting, SendButtonAction.TEXT);
1286 } else {
1287 action = SendButtonAction.valueOfOrDefault(setting, SendButtonAction.TEXT);
1288 }
1289 }
1290 }
1291 } else {
1292 action = SendButtonAction.TEXT;
1293 }
1294 }
1295 if (activity.useSendButtonToIndicateStatus() && c.getAccount().getStatus() == Account.State.ONLINE) {
1296 if (activity.xmppConnectionService != null && activity.xmppConnectionService.getMessageArchiveService().isCatchingUp(c)) {
1297 status = Presence.Status.OFFLINE;
1298 } else if (c.getMode() == Conversation.MODE_SINGLE) {
1299 status = c.getContact().getShownStatus();
1300 } else {
1301 status = c.getMucOptions().online() ? Presence.Status.ONLINE : Presence.Status.OFFLINE;
1302 }
1303 } else {
1304 status = Presence.Status.OFFLINE;
1305 }
1306 this.mSendButton.setTag(action);
1307 this.mSendButton.setImageResource(getSendButtonImageResource(action, status));
1308 }
1309
1310 protected void updateDateSeparators() {
1311 synchronized (this.messageList) {
1312 for (int i = 0; i < this.messageList.size(); ++i) {
1313 final Message current = this.messageList.get(i);
1314 if (i == 0 || !UIHelper.sameDay(this.messageList.get(i - 1).getTimeSent(), current.getTimeSent())) {
1315 this.messageList.add(i, Message.createDateSeparator(current));
1316 i++;
1317 }
1318 }
1319 }
1320 }
1321
1322 protected void updateStatusMessages() {
1323 updateDateSeparators();
1324 synchronized (this.messageList) {
1325 if (showLoadMoreMessages(conversation)) {
1326 this.messageList.add(0, Message.createLoadMoreMessage(conversation));
1327 }
1328 if (conversation.getMode() == Conversation.MODE_SINGLE) {
1329 ChatState state = conversation.getIncomingChatState();
1330 if (state == ChatState.COMPOSING) {
1331 this.messageList.add(Message.createStatusMessage(conversation, getString(R.string.contact_is_typing, conversation.getName())));
1332 } else if (state == ChatState.PAUSED) {
1333 this.messageList.add(Message.createStatusMessage(conversation, getString(R.string.contact_has_stopped_typing, conversation.getName())));
1334 } else {
1335 for (int i = this.messageList.size() - 1; i >= 0; --i) {
1336 if (this.messageList.get(i).getStatus() == Message.STATUS_RECEIVED) {
1337 return;
1338 } else {
1339 if (this.messageList.get(i).getStatus() == Message.STATUS_SEND_DISPLAYED) {
1340 this.messageList.add(i + 1,
1341 Message.createStatusMessage(conversation, getString(R.string.contact_has_read_up_to_this_point, conversation.getName())));
1342 return;
1343 }
1344 }
1345 }
1346 }
1347 } else {
1348 final MucOptions mucOptions = conversation.getMucOptions();
1349 final List<MucOptions.User> allUsers = mucOptions.getUsers();
1350 final Set<ReadByMarker> addedMarkers = new HashSet<>();
1351 ChatState state = ChatState.COMPOSING;
1352 List<MucOptions.User> users = conversation.getMucOptions().getUsersWithChatState(state, 5);
1353 if (users.size() == 0) {
1354 state = ChatState.PAUSED;
1355 users = conversation.getMucOptions().getUsersWithChatState(state, 5);
1356 }
1357 if (mucOptions.isPrivateAndNonAnonymous()) {
1358 for (int i = this.messageList.size() - 1; i >= 0; --i) {
1359 final Set<ReadByMarker> markersForMessage = messageList.get(i).getReadByMarkers();
1360 final List<MucOptions.User> shownMarkers = new ArrayList<>();
1361 for (ReadByMarker marker : markersForMessage) {
1362 if (!ReadByMarker.contains(marker, addedMarkers)) {
1363 addedMarkers.add(marker); //may be put outside this condition. set should do dedup anyway
1364 MucOptions.User user = mucOptions.findUser(marker);
1365 if (user != null && !users.contains(user)) {
1366 shownMarkers.add(user);
1367 }
1368 }
1369 }
1370 final ReadByMarker markerForSender = ReadByMarker.from(messageList.get(i));
1371 final Message statusMessage;
1372 final int size = shownMarkers.size();
1373 if (size > 1) {
1374 final String body;
1375 if (size <= 4) {
1376 body = getString(R.string.contacts_have_read_up_to_this_point, UIHelper.concatNames(shownMarkers));
1377 } else {
1378 body = getString(R.string.contacts_and_n_more_have_read_up_to_this_point, UIHelper.concatNames(shownMarkers, 3), size - 3);
1379 }
1380 statusMessage = Message.createStatusMessage(conversation, body);
1381 statusMessage.setCounterparts(shownMarkers);
1382 } else if (size == 1) {
1383 statusMessage = Message.createStatusMessage(conversation, getString(R.string.contact_has_read_up_to_this_point, UIHelper.getDisplayName(shownMarkers.get(0))));
1384 statusMessage.setCounterpart(shownMarkers.get(0).getFullJid());
1385 statusMessage.setTrueCounterpart(shownMarkers.get(0).getRealJid());
1386 } else {
1387 statusMessage = null;
1388 }
1389 if (statusMessage != null) {
1390 this.messageList.add(i + 1, statusMessage);
1391 }
1392 addedMarkers.add(markerForSender);
1393 if (ReadByMarker.allUsersRepresented(allUsers, addedMarkers)) {
1394 break;
1395 }
1396 }
1397 }
1398 if (users.size() > 0) {
1399 Message statusMessage;
1400 if (users.size() == 1) {
1401 MucOptions.User user = users.get(0);
1402 int id = state == ChatState.COMPOSING ? R.string.contact_is_typing : R.string.contact_has_stopped_typing;
1403 statusMessage = Message.createStatusMessage(conversation, getString(id, UIHelper.getDisplayName(user)));
1404 statusMessage.setTrueCounterpart(user.getRealJid());
1405 statusMessage.setCounterpart(user.getFullJid());
1406 } else {
1407 int id = state == ChatState.COMPOSING ? R.string.contacts_are_typing : R.string.contacts_have_stopped_typing;
1408 statusMessage = Message.createStatusMessage(conversation, getString(id, UIHelper.concatNames(users)));
1409 statusMessage.setCounterparts(users);
1410 }
1411 this.messageList.add(statusMessage);
1412 }
1413
1414 }
1415 }
1416 }
1417
1418 public void stopScrolling() {
1419 long now = SystemClock.uptimeMillis();
1420 MotionEvent cancel = MotionEvent.obtain(now, now, MotionEvent.ACTION_CANCEL, 0, 0, 0);
1421 messagesView.dispatchTouchEvent(cancel);
1422 }
1423
1424 private boolean showLoadMoreMessages(final Conversation c) {
1425 final boolean mam = hasMamSupport(c) && !c.getContact().isBlocked();
1426 final MessageArchiveService service = activity.xmppConnectionService.getMessageArchiveService();
1427 return mam && (c.getLastClearHistory().getTimestamp() != 0 || (c.countMessages() == 0 && c.messagesLoaded.get() && c.hasMessagesLeftOnServer() && !service.queryInProgress(c)));
1428 }
1429
1430 private boolean hasMamSupport(final Conversation c) {
1431 if (c.getMode() == Conversation.MODE_SINGLE) {
1432 final XmppConnection connection = c.getAccount().getXmppConnection();
1433 return connection != null && connection.getFeatures().mam();
1434 } else {
1435 return c.getMucOptions().mamSupport();
1436 }
1437 }
1438
1439 protected void showSnackbar(final int message, final int action, final OnClickListener clickListener) {
1440 showSnackbar(message, action, clickListener, null);
1441 }
1442
1443 protected void showSnackbar(final int message, final int action, final OnClickListener clickListener, final View.OnLongClickListener longClickListener) {
1444 snackbar.setVisibility(View.VISIBLE);
1445 snackbar.setOnClickListener(null);
1446 snackbarMessage.setText(message);
1447 snackbarMessage.setOnClickListener(null);
1448 snackbarAction.setVisibility(clickListener == null ? View.GONE : View.VISIBLE);
1449 if (action != 0) {
1450 snackbarAction.setText(action);
1451 }
1452 snackbarAction.setOnClickListener(clickListener);
1453 snackbarAction.setOnLongClickListener(longClickListener);
1454 }
1455
1456 protected void hideSnackbar() {
1457 snackbar.setVisibility(View.GONE);
1458 }
1459
1460 protected void sendPlainTextMessage(Message message) {
1461 ConversationActivity activity = (ConversationActivity) getActivity();
1462 activity.xmppConnectionService.sendMessage(message);
1463 messageSent();
1464 }
1465
1466 protected void sendPgpMessage(final Message message) {
1467 final ConversationActivity activity = (ConversationActivity) getActivity();
1468 final XmppConnectionService xmppService = activity.xmppConnectionService;
1469 final Contact contact = message.getConversation().getContact();
1470 if (!activity.hasPgp()) {
1471 activity.showInstallPgpDialog();
1472 return;
1473 }
1474 if (conversation.getAccount().getPgpSignature() == null) {
1475 activity.announcePgp(conversation.getAccount(), conversation, null, activity.onOpenPGPKeyPublished);
1476 return;
1477 }
1478 if (!mSendingPgpMessage.compareAndSet(false, true)) {
1479 Log.d(Config.LOGTAG, "sending pgp message already in progress");
1480 }
1481 if (conversation.getMode() == Conversation.MODE_SINGLE) {
1482 if (contact.getPgpKeyId() != 0) {
1483 xmppService.getPgpEngine().hasKey(contact,
1484 new UiCallback<Contact>() {
1485
1486 @Override
1487 public void userInputRequried(PendingIntent pi,
1488 Contact contact) {
1489 activity.runIntent(
1490 pi,
1491 ConversationActivity.REQUEST_ENCRYPT_MESSAGE);
1492 }
1493
1494 @Override
1495 public void success(Contact contact) {
1496 activity.encryptTextMessage(message);
1497 }
1498
1499 @Override
1500 public void error(int error, Contact contact) {
1501 activity.runOnUiThread(new Runnable() {
1502 @Override
1503 public void run() {
1504 Toast.makeText(activity,
1505 R.string.unable_to_connect_to_keychain,
1506 Toast.LENGTH_SHORT
1507 ).show();
1508 }
1509 });
1510 mSendingPgpMessage.set(false);
1511 }
1512 });
1513
1514 } else {
1515 showNoPGPKeyDialog(false,
1516 new DialogInterface.OnClickListener() {
1517
1518 @Override
1519 public void onClick(DialogInterface dialog,
1520 int which) {
1521 conversation
1522 .setNextEncryption(Message.ENCRYPTION_NONE);
1523 xmppService.updateConversation(conversation);
1524 message.setEncryption(Message.ENCRYPTION_NONE);
1525 xmppService.sendMessage(message);
1526 messageSent();
1527 }
1528 });
1529 }
1530 } else {
1531 if (conversation.getMucOptions().pgpKeysInUse()) {
1532 if (!conversation.getMucOptions().everybodyHasKeys()) {
1533 Toast warning = Toast
1534 .makeText(getActivity(),
1535 R.string.missing_public_keys,
1536 Toast.LENGTH_LONG);
1537 warning.setGravity(Gravity.CENTER_VERTICAL, 0, 0);
1538 warning.show();
1539 }
1540 activity.encryptTextMessage(message);
1541 } else {
1542 showNoPGPKeyDialog(true,
1543 new DialogInterface.OnClickListener() {
1544
1545 @Override
1546 public void onClick(DialogInterface dialog,
1547 int which) {
1548 conversation
1549 .setNextEncryption(Message.ENCRYPTION_NONE);
1550 message.setEncryption(Message.ENCRYPTION_NONE);
1551 xmppService.updateConversation(conversation);
1552 xmppService.sendMessage(message);
1553 messageSent();
1554 }
1555 });
1556 }
1557 }
1558 }
1559
1560 public void showNoPGPKeyDialog(boolean plural,
1561 DialogInterface.OnClickListener listener) {
1562 AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
1563 builder.setIconAttribute(android.R.attr.alertDialogIcon);
1564 if (plural) {
1565 builder.setTitle(getString(R.string.no_pgp_keys));
1566 builder.setMessage(getText(R.string.contacts_have_no_pgp_keys));
1567 } else {
1568 builder.setTitle(getString(R.string.no_pgp_key));
1569 builder.setMessage(getText(R.string.contact_has_no_pgp_key));
1570 }
1571 builder.setNegativeButton(getString(R.string.cancel), null);
1572 builder.setPositiveButton(getString(R.string.send_unencrypted),
1573 listener);
1574 builder.create().show();
1575 }
1576
1577 protected void sendAxolotlMessage(final Message message) {
1578 final ConversationActivity activity = (ConversationActivity) getActivity();
1579 final XmppConnectionService xmppService = activity.xmppConnectionService;
1580 xmppService.sendMessage(message);
1581 messageSent();
1582 }
1583
1584 public void appendText(String text) {
1585 if (text == null) {
1586 return;
1587 }
1588 String previous = this.mEditMessage.getText().toString();
1589 if (previous.length() != 0 && !previous.endsWith(" ")) {
1590 text = " " + text;
1591 }
1592 this.mEditMessage.append(text);
1593 }
1594
1595 @Override
1596 public boolean onEnterPressed() {
1597 if (activity.enterIsSend()) {
1598 sendMessage();
1599 return true;
1600 } else {
1601 return false;
1602 }
1603 }
1604
1605 @Override
1606 public void onTypingStarted() {
1607 Account.State status = conversation.getAccount().getStatus();
1608 if (status == Account.State.ONLINE && conversation.setOutgoingChatState(ChatState.COMPOSING)) {
1609 activity.xmppConnectionService.sendChatState(conversation);
1610 }
1611 activity.hideConversationsOverview();
1612 updateSendButton();
1613 }
1614
1615 @Override
1616 public void onTypingStopped() {
1617 Account.State status = conversation.getAccount().getStatus();
1618 if (status == Account.State.ONLINE && conversation.setOutgoingChatState(ChatState.PAUSED)) {
1619 activity.xmppConnectionService.sendChatState(conversation);
1620 }
1621 }
1622
1623 @Override
1624 public void onTextDeleted() {
1625 Account.State status = conversation.getAccount().getStatus();
1626 if (status == Account.State.ONLINE && conversation.setOutgoingChatState(Config.DEFAULT_CHATSTATE)) {
1627 activity.xmppConnectionService.sendChatState(conversation);
1628 }
1629 updateSendButton();
1630 }
1631
1632 @Override
1633 public void onTextChanged() {
1634 if (conversation != null && conversation.getCorrectingMessage() != null) {
1635 updateSendButton();
1636 }
1637 }
1638
1639 @Override
1640 public boolean onTabPressed(boolean repeated) {
1641 if (conversation == null || conversation.getMode() == Conversation.MODE_SINGLE) {
1642 return false;
1643 }
1644 if (repeated) {
1645 completionIndex++;
1646 } else {
1647 lastCompletionLength = 0;
1648 completionIndex = 0;
1649 final String content = mEditMessage.getText().toString();
1650 lastCompletionCursor = mEditMessage.getSelectionEnd();
1651 int start = lastCompletionCursor > 0 ? content.lastIndexOf(" ", lastCompletionCursor - 1) + 1 : 0;
1652 firstWord = start == 0;
1653 incomplete = content.substring(start, lastCompletionCursor);
1654 }
1655 List<String> completions = new ArrayList<>();
1656 for (MucOptions.User user : conversation.getMucOptions().getUsers()) {
1657 String name = user.getName();
1658 if (name != null && name.startsWith(incomplete)) {
1659 completions.add(name + (firstWord ? ": " : " "));
1660 }
1661 }
1662 Collections.sort(completions);
1663 if (completions.size() > completionIndex) {
1664 String completion = completions.get(completionIndex).substring(incomplete.length());
1665 mEditMessage.getEditableText().delete(lastCompletionCursor, lastCompletionCursor + lastCompletionLength);
1666 mEditMessage.getEditableText().insert(lastCompletionCursor, completion);
1667 lastCompletionLength = completion.length();
1668 } else {
1669 completionIndex = -1;
1670 mEditMessage.getEditableText().delete(lastCompletionCursor, lastCompletionCursor + lastCompletionLength);
1671 lastCompletionLength = 0;
1672 }
1673 return true;
1674 }
1675
1676 @Override
1677 public void onActivityResult(int requestCode, int resultCode,
1678 final Intent data) {
1679 if (resultCode == Activity.RESULT_OK) {
1680 if (requestCode == ConversationActivity.REQUEST_DECRYPT_PGP) {
1681 activity.getSelectedConversation().getAccount().getPgpDecryptionService().continueDecryption(data);
1682 } else if (requestCode == ConversationActivity.REQUEST_TRUST_KEYS_TEXT) {
1683 final String body = mEditMessage.getText().toString();
1684 Message message = new Message(conversation, body, conversation.getNextEncryption());
1685 sendAxolotlMessage(message);
1686 } else if (requestCode == ConversationActivity.REQUEST_TRUST_KEYS_MENU) {
1687 int choice = data.getIntExtra("choice", ConversationActivity.ATTACHMENT_CHOICE_INVALID);
1688 activity.selectPresenceToAttachFile(choice, conversation.getNextEncryption());
1689 }
1690 } else if (resultCode == Activity.RESULT_CANCELED) {
1691 if (requestCode == ConversationActivity.REQUEST_DECRYPT_PGP) {
1692 // discard the message to prevent decryption being blocked
1693 conversation.getAccount().getPgpDecryptionService().giveUpCurrentDecryption();
1694 }
1695 }
1696 }
1697
1698 enum SendButtonAction {
1699 TEXT, TAKE_PHOTO, SEND_LOCATION, RECORD_VOICE, CANCEL, CHOOSE_PICTURE, RECORD_VIDEO;
1700
1701 public static SendButtonAction valueOfOrDefault(String setting, SendButtonAction text) {
1702 try {
1703 return valueOf(setting);
1704 } catch (IllegalArgumentException e) {
1705 return TEXT;
1706 }
1707 }
1708 }
1709
1710}