1package eu.siacs.conversations.ui;
2
3import java.util.ArrayList;
4import java.util.List;
5import java.util.Set;
6
7import net.java.otr4j.session.SessionStatus;
8import eu.siacs.conversations.R;
9import eu.siacs.conversations.crypto.PgpEngine;
10import eu.siacs.conversations.entities.Account;
11import eu.siacs.conversations.entities.Contact;
12import eu.siacs.conversations.entities.Conversation;
13import eu.siacs.conversations.entities.Message;
14import eu.siacs.conversations.entities.MucOptions;
15import eu.siacs.conversations.entities.Presences;
16import eu.siacs.conversations.services.XmppConnectionService;
17import eu.siacs.conversations.ui.EditMessage.OnEnterPressed;
18import eu.siacs.conversations.ui.XmppActivity.OnPresenceSelected;
19import eu.siacs.conversations.ui.XmppActivity.OnValueEdited;
20import eu.siacs.conversations.ui.adapter.MessageAdapter;
21import eu.siacs.conversations.ui.adapter.MessageAdapter.OnContactPictureClicked;
22import eu.siacs.conversations.ui.adapter.MessageAdapter.OnContactPictureLongClicked;
23import eu.siacs.conversations.utils.UIHelper;
24import android.app.AlertDialog;
25import android.app.Fragment;
26import android.app.PendingIntent;
27import android.content.Context;
28import android.content.DialogInterface;
29import android.content.Intent;
30import android.content.IntentSender;
31import android.content.IntentSender.SendIntentException;
32import android.os.Bundle;
33import android.text.Editable;
34import android.text.Selection;
35import android.view.Gravity;
36import android.view.KeyEvent;
37import android.view.LayoutInflater;
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.OnScrollListener;
44import android.widget.TextView.OnEditorActionListener;
45import android.widget.AbsListView;
46
47import android.widget.ListView;
48import android.widget.ImageButton;
49import android.widget.RelativeLayout;
50import android.widget.TextView;
51import android.widget.Toast;
52
53public class ConversationFragment extends Fragment {
54
55 protected Conversation conversation;
56 protected ListView messagesView;
57 protected LayoutInflater inflater;
58 protected List<Message> messageList = new ArrayList<Message>();
59 protected MessageAdapter messageListAdapter;
60 protected Contact contact;
61
62 protected String queuedPqpMessage = null;
63
64 private EditMessage mEditMessage;
65 private ImageButton mSendButton;
66 private String pastedText = null;
67 private RelativeLayout snackbar;
68 private TextView snackbarMessage;
69 private TextView snackbarAction;
70
71 private boolean messagesLoaded = false;
72
73 private IntentSender askForPassphraseIntent = null;
74
75 private OnEditorActionListener mEditorActionListener = new OnEditorActionListener() {
76
77 @Override
78 public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
79 if (actionId == EditorInfo.IME_ACTION_DONE) {
80 InputMethodManager imm = (InputMethodManager) v.getContext()
81 .getSystemService(Context.INPUT_METHOD_SERVICE);
82 imm.hideSoftInputFromWindow(v.getWindowToken(), 0);
83 return true;
84 } else {
85 return false;
86 }
87 }
88 };
89
90 private OnClickListener mSendButtonListener = new OnClickListener() {
91
92 @Override
93 public void onClick(View v) {
94 sendMessage();
95 }
96 };
97 protected OnClickListener clickToDecryptListener = new OnClickListener() {
98
99 @Override
100 public void onClick(View v) {
101 if (activity.hasPgp() && askForPassphraseIntent != null) {
102 try {
103 getActivity().startIntentSenderForResult(
104 askForPassphraseIntent,
105 ConversationActivity.REQUEST_DECRYPT_PGP, null, 0,
106 0, 0);
107 } catch (SendIntentException e) {
108 //
109 }
110 }
111 }
112 };
113
114 private OnClickListener clickToMuc = new OnClickListener() {
115
116 @Override
117 public void onClick(View v) {
118 Intent intent = new Intent(getActivity(),
119 ConferenceDetailsActivity.class);
120 intent.setAction(ConferenceDetailsActivity.ACTION_VIEW_MUC);
121 intent.putExtra("uuid", conversation.getUuid());
122 startActivity(intent);
123 }
124 };
125
126 private OnClickListener leaveMuc = new OnClickListener() {
127
128 @Override
129 public void onClick(View v) {
130 activity.endConversation(conversation);
131 }
132 };
133
134 private OnClickListener enterPassword = new OnClickListener() {
135
136 @Override
137 public void onClick(View v) {
138 MucOptions muc = conversation.getMucOptions();
139 String password = muc.getPassword();
140 if (password == null) {
141 password = "";
142 }
143 activity.quickPasswordEdit(password, new OnValueEdited() {
144
145 @Override
146 public void onValueEdited(String value) {
147 activity.xmppConnectionService.providePasswordForMuc(
148 conversation, value);
149 }
150 });
151 }
152 };
153
154 private OnScrollListener mOnScrollListener = new OnScrollListener() {
155
156 @Override
157 public void onScrollStateChanged(AbsListView view, int scrollState) {
158 // TODO Auto-generated method stub
159
160 }
161
162 @Override
163 public void onScroll(AbsListView view, int firstVisibleItem,
164 int visibleItemCount, int totalItemCount) {
165 if (firstVisibleItem == 0 && messagesLoaded) {
166 long timestamp = messageList.get(0).getTimeSent();
167 messagesLoaded = false;
168 int size = activity.xmppConnectionService.loadMoreMessages(
169 conversation, timestamp);
170 messageList.clear();
171 messageList.addAll(conversation.getMessages());
172 messageListAdapter.notifyDataSetChanged();
173 if (size != 0) {
174 messagesLoaded = true;
175 }
176 messagesView.setSelectionFromTop(size + 1, 0);
177 }
178 }
179 };
180
181 private ConversationActivity activity;
182
183 private void sendMessage() {
184 if (this.conversation == null) {
185 return;
186 }
187 if (mEditMessage.getText().length() < 1) {
188 if (this.conversation.getMode() == Conversation.MODE_MULTI) {
189 conversation.setNextPresence(null);
190 updateChatMsgHint();
191 }
192 return;
193 }
194 Message message = new Message(conversation, mEditMessage.getText()
195 .toString(), conversation.getNextEncryption(activity
196 .forceEncryption()));
197 if (conversation.getMode() == Conversation.MODE_MULTI) {
198 if (conversation.getNextPresence() != null) {
199 message.setPresence(conversation.getNextPresence());
200 message.setType(Message.TYPE_PRIVATE);
201 conversation.setNextPresence(null);
202 }
203 }
204 if (conversation.getNextEncryption(activity.forceEncryption()) == Message.ENCRYPTION_OTR) {
205 sendOtrMessage(message);
206 } else if (conversation.getNextEncryption(activity.forceEncryption()) == Message.ENCRYPTION_PGP) {
207 sendPgpMessage(message);
208 } else {
209 sendPlainTextMessage(message);
210 }
211 }
212
213 public void updateChatMsgHint() {
214 if (conversation.getMode() == Conversation.MODE_MULTI
215 && conversation.getNextPresence() != null) {
216 this.mEditMessage.setHint(getString(
217 R.string.send_private_message_to,
218 conversation.getNextPresence()));
219 } else {
220 switch (conversation.getNextEncryption(activity.forceEncryption())) {
221 case Message.ENCRYPTION_NONE:
222 mEditMessage
223 .setHint(getString(R.string.send_plain_text_message));
224 break;
225 case Message.ENCRYPTION_OTR:
226 mEditMessage.setHint(getString(R.string.send_otr_message));
227 break;
228 case Message.ENCRYPTION_PGP:
229 mEditMessage.setHint(getString(R.string.send_pgp_message));
230 break;
231 default:
232 break;
233 }
234 }
235 }
236
237 @Override
238 public View onCreateView(final LayoutInflater inflater,
239 ViewGroup container, Bundle savedInstanceState) {
240 final View view = inflater.inflate(R.layout.fragment_conversation,
241 container, false);
242 mEditMessage = (EditMessage) view.findViewById(R.id.textinput);
243 mEditMessage.setOnClickListener(new OnClickListener() {
244
245 @Override
246 public void onClick(View v) {
247 if (activity.getSlidingPaneLayout().isSlideable()) {
248 activity.getSlidingPaneLayout().closePane();
249 }
250 }
251 });
252 mEditMessage.setOnEditorActionListener(mEditorActionListener);
253 mEditMessage.setOnEnterPressedListener(new OnEnterPressed() {
254
255 @Override
256 public void onEnterPressed() {
257 sendMessage();
258 }
259 });
260
261 mSendButton = (ImageButton) view.findViewById(R.id.textSendButton);
262 mSendButton.setOnClickListener(this.mSendButtonListener);
263
264 snackbar = (RelativeLayout) view.findViewById(R.id.snackbar);
265 snackbarMessage = (TextView) view.findViewById(R.id.snackbar_message);
266 snackbarAction = (TextView) view.findViewById(R.id.snackbar_action);
267
268 messagesView = (ListView) view.findViewById(R.id.messages_view);
269 messagesView.setOnScrollListener(mOnScrollListener);
270 messagesView.setTranscriptMode(ListView.TRANSCRIPT_MODE_NORMAL);
271 messageListAdapter = new MessageAdapter(
272 (ConversationActivity) getActivity(), this.messageList);
273 messageListAdapter
274 .setOnContactPictureClicked(new OnContactPictureClicked() {
275
276 @Override
277 public void onContactPictureClicked(Message message) {
278 if (message.getConversation().getMode() == Conversation.MODE_MULTI) {
279 if (message.getPresence() != null) {
280 highlightInConference(message.getPresence());
281 } else {
282 highlightInConference(message.getCounterpart());
283 }
284 }
285 }
286 });
287 messageListAdapter
288 .setOnContactPictureLongClicked(new OnContactPictureLongClicked() {
289
290 @Override
291 public void onContactPictureLongClicked(Message message) {
292 if (message.getConversation().getMode() == Conversation.MODE_MULTI) {
293 if (message.getPresence() != null) {
294 privateMessageWith(message.getPresence());
295 } else {
296 privateMessageWith(message.getCounterpart());
297 }
298 }
299 }
300 });
301 messagesView.setAdapter(messageListAdapter);
302
303 return view;
304 }
305
306 protected void privateMessageWith(String counterpart) {
307 this.mEditMessage.setText("");
308 this.conversation.setNextPresence(counterpart);
309 updateChatMsgHint();
310 }
311
312 protected void highlightInConference(String nick) {
313 String oldString = mEditMessage.getText().toString().trim();
314 if (oldString.isEmpty() || mEditMessage.getSelectionStart() == 0) {
315 mEditMessage.getText().insert(0, nick + ": ");
316 } else {
317 if (mEditMessage.getText().charAt(
318 mEditMessage.getSelectionStart() - 1) != ' ') {
319 nick = " " + nick;
320 }
321 mEditMessage.getText().insert(mEditMessage.getSelectionStart(),
322 nick + " ");
323 }
324 }
325
326 @Override
327 public void onStart() {
328 super.onStart();
329 this.activity = (ConversationActivity) getActivity();
330 if (activity.xmppConnectionServiceBound) {
331 this.onBackendConnected();
332 }
333 }
334
335 @Override
336 public void onStop() {
337 super.onStop();
338 if (this.conversation != null) {
339 this.conversation.setNextMessage(mEditMessage.getText().toString());
340 }
341 }
342
343 public void onBackendConnected() {
344 this.activity = (ConversationActivity) getActivity();
345 this.conversation = activity.getSelectedConversation();
346 if (this.conversation == null) {
347 return;
348 }
349 String oldString = conversation.getNextMessage().trim();
350 if (this.pastedText == null) {
351 this.mEditMessage.setText(oldString);
352 } else {
353
354 if (oldString.isEmpty()) {
355 mEditMessage.setText(pastedText);
356 } else {
357 mEditMessage.setText(oldString + " " + pastedText);
358 }
359 pastedText = null;
360 }
361 int position = mEditMessage.length();
362 Editable etext = mEditMessage.getText();
363 Selection.setSelection(etext, position);
364 if (activity.getSlidingPaneLayout().isSlideable()) {
365 if (!activity.shouldPaneBeOpen()) {
366 activity.getSlidingPaneLayout().closePane();
367 activity.getActionBar().setDisplayHomeAsUpEnabled(true);
368 activity.getActionBar().setHomeButtonEnabled(true);
369 activity.getActionBar().setTitle(conversation.getName());
370 activity.invalidateOptionsMenu();
371 }
372 }
373 if (this.conversation.getMode() == Conversation.MODE_MULTI) {
374 conversation.setNextPresence(null);
375 }
376 updateMessages();
377 }
378
379 private void decryptMessage(Message message) {
380 PgpEngine engine = activity.xmppConnectionService.getPgpEngine();
381 if (engine != null) {
382 engine.decrypt(message, new UiCallback<Message>() {
383
384 @Override
385 public void userInputRequried(PendingIntent pi, Message message) {
386 askForPassphraseIntent = pi.getIntentSender();
387 showSnackbar(R.string.openpgp_messages_found,
388 R.string.decrypt, clickToDecryptListener);
389 }
390
391 @Override
392 public void success(Message message) {
393 activity.xmppConnectionService.databaseBackend
394 .updateMessage(message);
395 updateMessages();
396 }
397
398 @Override
399 public void error(int error, Message message) {
400 message.setEncryption(Message.ENCRYPTION_DECRYPTION_FAILED);
401 // updateMessages();
402 }
403 });
404 }
405 }
406
407 public void updateMessages() {
408 if (getView() == null) {
409 return;
410 }
411 hideSnackbar();
412 final ConversationActivity activity = (ConversationActivity) getActivity();
413 if (this.conversation != null) {
414 final Contact contact = this.conversation.getContact();
415 if (this.conversation.isMuted()) {
416 showSnackbar(R.string.notifications_disabled, R.string.enable,
417 new OnClickListener() {
418
419 @Override
420 public void onClick(View v) {
421 conversation.setMutedTill(0);
422 updateMessages();
423 }
424 });
425 } else if (!contact.showInRoster()
426 && contact
427 .getOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST)) {
428 showSnackbar(R.string.contact_added_you, R.string.add_back,
429 new OnClickListener() {
430
431 @Override
432 public void onClick(View v) {
433 activity.xmppConnectionService
434 .createContact(contact);
435 activity.switchToContactDetails(contact);
436 }
437 });
438 }
439 for (Message message : this.conversation.getMessages()) {
440 if ((message.getEncryption() == Message.ENCRYPTION_PGP)
441 && ((message.getStatus() == Message.STATUS_RECEIVED) || (message
442 .getStatus() == Message.STATUS_SEND))) {
443 decryptMessage(message);
444 break;
445 }
446 }
447 this.messageList.clear();
448 if (this.conversation.getMessages().size() == 0) {
449 messagesLoaded = false;
450 } else {
451 this.messageList.addAll(this.conversation.getMessages());
452 messagesLoaded = true;
453 updateStatusMessages();
454 }
455 this.messageListAdapter.notifyDataSetChanged();
456 if (conversation.getMode() == Conversation.MODE_SINGLE) {
457 if (messageList.size() >= 1) {
458 makeFingerprintWarning(conversation.getLatestEncryption());
459 }
460 } else {
461 if (!conversation.getMucOptions().online()
462 && conversation.getAccount().getStatus() == Account.STATUS_ONLINE) {
463 int error = conversation.getMucOptions().getError();
464 switch (error) {
465 case MucOptions.ERROR_NICK_IN_USE:
466 showSnackbar(R.string.nick_in_use, R.string.edit,
467 clickToMuc);
468 break;
469 case MucOptions.ERROR_ROOM_NOT_FOUND:
470 showSnackbar(R.string.conference_not_found,
471 R.string.leave, leaveMuc);
472 break;
473 case MucOptions.ERROR_PASSWORD_REQUIRED:
474 showSnackbar(R.string.conference_requires_password,
475 R.string.enter_password, enterPassword);
476 break;
477 default:
478 break;
479 }
480 }
481 }
482 getActivity().invalidateOptionsMenu();
483 updateChatMsgHint();
484 if (!activity.shouldPaneBeOpen()) {
485 activity.xmppConnectionService.markRead(conversation);
486 UIHelper.updateNotification(getActivity(),
487 activity.getConversationList(), null, false);
488 activity.updateConversationList();
489 }
490 this.updateSendButton();
491 }
492 }
493
494 private void messageSent() {
495 int size = this.messageList.size();
496 if (size >= 1 && this.messagesView.getLastVisiblePosition() != size - 1) {
497 messagesView.setSelection(size - 1);
498 }
499 mEditMessage.setText("");
500 updateChatMsgHint();
501 }
502
503 public void updateSendButton() {
504 Conversation c = this.conversation;
505 if (activity.useSendButtonToIndicateStatus() && c != null
506 && c.getAccount().getStatus() == Account.STATUS_ONLINE) {
507 if (c.getMode() == Conversation.MODE_SINGLE) {
508 switch (c.getContact().getMostAvailableStatus()) {
509 case Presences.CHAT:
510 this.mSendButton
511 .setImageResource(R.drawable.ic_action_send_now_online);
512 break;
513 case Presences.ONLINE:
514 this.mSendButton
515 .setImageResource(R.drawable.ic_action_send_now_online);
516 break;
517 case Presences.AWAY:
518 this.mSendButton
519 .setImageResource(R.drawable.ic_action_send_now_away);
520 break;
521 case Presences.XA:
522 this.mSendButton
523 .setImageResource(R.drawable.ic_action_send_now_away);
524 break;
525 case Presences.DND:
526 this.mSendButton
527 .setImageResource(R.drawable.ic_action_send_now_dnd);
528 break;
529 default:
530 this.mSendButton
531 .setImageResource(R.drawable.ic_action_send_now_offline);
532 break;
533 }
534 } else if (c.getMode() == Conversation.MODE_MULTI) {
535 if (c.getMucOptions().online()) {
536 this.mSendButton
537 .setImageResource(R.drawable.ic_action_send_now_online);
538 } else {
539 this.mSendButton
540 .setImageResource(R.drawable.ic_action_send_now_offline);
541 }
542 } else {
543 this.mSendButton
544 .setImageResource(R.drawable.ic_action_send_now_offline);
545 }
546 } else {
547 this.mSendButton
548 .setImageResource(R.drawable.ic_action_send_now_offline);
549 }
550 }
551
552 protected void updateStatusMessages() {
553 if (conversation.getMode() == Conversation.MODE_SINGLE) {
554 for (int i = this.messageList.size() - 1; i >= 0; --i) {
555 if (this.messageList.get(i).getStatus() == Message.STATUS_RECEIVED) {
556 return;
557 } else {
558 if (this.messageList.get(i).getStatus() == Message.STATUS_SEND_DISPLAYED) {
559 this.messageList.add(i + 1,
560 Message.createStatusMessage(conversation));
561 return;
562 }
563 }
564 }
565 }
566 }
567
568 protected void makeFingerprintWarning(int latestEncryption) {
569 Set<String> knownFingerprints = conversation.getContact()
570 .getOtrFingerprints();
571 if ((latestEncryption == Message.ENCRYPTION_OTR)
572 && (conversation.hasValidOtrSession()
573 && (!conversation.isMuted())
574 && (conversation.getOtrSession().getSessionStatus() == SessionStatus.ENCRYPTED) && (!knownFingerprints
575 .contains(conversation.getOtrFingerprint())))) {
576 showSnackbar(R.string.unknown_otr_fingerprint, R.string.verify,
577 new OnClickListener() {
578
579 @Override
580 public void onClick(View v) {
581 if (conversation.getOtrFingerprint() != null) {
582 AlertDialog dialog = UIHelper
583 .getVerifyFingerprintDialog(
584 (ConversationActivity) getActivity(),
585 conversation, snackbar);
586 dialog.show();
587 }
588 }
589 });
590 }
591 }
592
593 protected void showSnackbar(int message, int action,
594 OnClickListener clickListener) {
595 snackbar.setVisibility(View.VISIBLE);
596 snackbar.setOnClickListener(null);
597 snackbarMessage.setText(message);
598 snackbarMessage.setOnClickListener(null);
599 snackbarAction.setText(action);
600 snackbarAction.setOnClickListener(clickListener);
601 }
602
603 protected void hideSnackbar() {
604 snackbar.setVisibility(View.GONE);
605 }
606
607 protected void sendPlainTextMessage(Message message) {
608 ConversationActivity activity = (ConversationActivity) getActivity();
609 activity.xmppConnectionService.sendMessage(message);
610 messageSent();
611 }
612
613 protected void sendPgpMessage(final Message message) {
614 final ConversationActivity activity = (ConversationActivity) getActivity();
615 final XmppConnectionService xmppService = activity.xmppConnectionService;
616 final Contact contact = message.getConversation().getContact();
617 if (activity.hasPgp()) {
618 if (conversation.getMode() == Conversation.MODE_SINGLE) {
619 if (contact.getPgpKeyId() != 0) {
620 xmppService.getPgpEngine().hasKey(contact,
621 new UiCallback<Contact>() {
622
623 @Override
624 public void userInputRequried(PendingIntent pi,
625 Contact contact) {
626 activity.runIntent(
627 pi,
628 ConversationActivity.REQUEST_ENCRYPT_MESSAGE);
629 }
630
631 @Override
632 public void success(Contact contact) {
633 messageSent();
634 activity.encryptTextMessage(message);
635 }
636
637 @Override
638 public void error(int error, Contact contact) {
639
640 }
641 });
642
643 } else {
644 showNoPGPKeyDialog(false,
645 new DialogInterface.OnClickListener() {
646
647 @Override
648 public void onClick(DialogInterface dialog,
649 int which) {
650 conversation
651 .setNextEncryption(Message.ENCRYPTION_NONE);
652 message.setEncryption(Message.ENCRYPTION_NONE);
653 xmppService.sendMessage(message);
654 messageSent();
655 }
656 });
657 }
658 } else {
659 if (conversation.getMucOptions().pgpKeysInUse()) {
660 if (!conversation.getMucOptions().everybodyHasKeys()) {
661 Toast warning = Toast
662 .makeText(getActivity(),
663 R.string.missing_public_keys,
664 Toast.LENGTH_LONG);
665 warning.setGravity(Gravity.CENTER_VERTICAL, 0, 0);
666 warning.show();
667 }
668 activity.encryptTextMessage(message);
669 messageSent();
670 } else {
671 showNoPGPKeyDialog(true,
672 new DialogInterface.OnClickListener() {
673
674 @Override
675 public void onClick(DialogInterface dialog,
676 int which) {
677 conversation
678 .setNextEncryption(Message.ENCRYPTION_NONE);
679 message.setEncryption(Message.ENCRYPTION_NONE);
680 xmppService.sendMessage(message);
681 messageSent();
682 }
683 });
684 }
685 }
686 } else {
687 activity.showInstallPgpDialog();
688 }
689 }
690
691 public void showNoPGPKeyDialog(boolean plural,
692 DialogInterface.OnClickListener listener) {
693 AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
694 builder.setIconAttribute(android.R.attr.alertDialogIcon);
695 if (plural) {
696 builder.setTitle(getString(R.string.no_pgp_keys));
697 builder.setMessage(getText(R.string.contacts_have_no_pgp_keys));
698 } else {
699 builder.setTitle(getString(R.string.no_pgp_key));
700 builder.setMessage(getText(R.string.contact_has_no_pgp_key));
701 }
702 builder.setNegativeButton(getString(R.string.cancel), null);
703 builder.setPositiveButton(getString(R.string.send_unencrypted),
704 listener);
705 builder.create().show();
706 }
707
708 protected void sendOtrMessage(final Message message) {
709 final ConversationActivity activity = (ConversationActivity) getActivity();
710 final XmppConnectionService xmppService = activity.xmppConnectionService;
711 if (conversation.hasValidOtrSession()) {
712 activity.xmppConnectionService.sendMessage(message);
713 messageSent();
714 } else {
715 activity.selectPresence(message.getConversation(),
716 new OnPresenceSelected() {
717
718 @Override
719 public void onPresenceSelected() {
720 message.setPresence(conversation.getNextPresence());
721 xmppService.sendMessage(message);
722 messageSent();
723 }
724 });
725 }
726 }
727
728 public void setText(String text) {
729 this.pastedText = text;
730 }
731
732 public void clearInputField() {
733 this.mEditMessage.setText("");
734 }
735}