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 List<Message> messages = activity.xmppConnectionService
169 .getMoreMessages(conversation, timestamp);
170 messageList.addAll(0, messages);
171 messageListAdapter.notifyDataSetChanged();
172 if (messages.size() != 0) {
173 messagesLoaded = true;
174 }
175 messagesView.setSelectionFromTop(messages.size() + 1, 0);
176 }
177 }
178 };
179
180 private ConversationActivity activity;
181
182 private void sendMessage() {
183 if (this.conversation == null) {
184 return;
185 }
186 if (mEditMessage.getText().length() < 1) {
187 if (this.conversation.getMode() == Conversation.MODE_MULTI) {
188 conversation.setNextPresence(null);
189 updateChatMsgHint();
190 }
191 return;
192 }
193 Message message = new Message(conversation, mEditMessage.getText()
194 .toString(), conversation.getNextEncryption(activity
195 .forceEncryption()));
196 if (conversation.getMode() == Conversation.MODE_MULTI) {
197 if (conversation.getNextPresence() != null) {
198 message.setPresence(conversation.getNextPresence());
199 message.setType(Message.TYPE_PRIVATE);
200 conversation.setNextPresence(null);
201 }
202 }
203 if (conversation.getNextEncryption(activity.forceEncryption()) == Message.ENCRYPTION_OTR) {
204 sendOtrMessage(message);
205 } else if (conversation.getNextEncryption(activity.forceEncryption()) == Message.ENCRYPTION_PGP) {
206 sendPgpMessage(message);
207 } else {
208 sendPlainTextMessage(message);
209 }
210 }
211
212 public void updateChatMsgHint() {
213 if (conversation.getMode() == Conversation.MODE_MULTI
214 && conversation.getNextPresence() != null) {
215 this.mEditMessage.setHint(getString(
216 R.string.send_private_message_to,
217 conversation.getNextPresence()));
218 } else {
219 switch (conversation.getNextEncryption(activity.forceEncryption())) {
220 case Message.ENCRYPTION_NONE:
221 mEditMessage
222 .setHint(getString(R.string.send_plain_text_message));
223 break;
224 case Message.ENCRYPTION_OTR:
225 mEditMessage.setHint(getString(R.string.send_otr_message));
226 break;
227 case Message.ENCRYPTION_PGP:
228 mEditMessage.setHint(getString(R.string.send_pgp_message));
229 break;
230 default:
231 break;
232 }
233 }
234 }
235
236 @Override
237 public View onCreateView(final LayoutInflater inflater,
238 ViewGroup container, Bundle savedInstanceState) {
239 final View view = inflater.inflate(R.layout.fragment_conversation,
240 container, false);
241 mEditMessage = (EditMessage) view.findViewById(R.id.textinput);
242 mEditMessage.setOnClickListener(new OnClickListener() {
243
244 @Override
245 public void onClick(View v) {
246 if (activity.getSlidingPaneLayout().isSlideable()) {
247 activity.getSlidingPaneLayout().closePane();
248 }
249 }
250 });
251 mEditMessage.setOnEditorActionListener(mEditorActionListener);
252 mEditMessage.setOnEnterPressedListener(new OnEnterPressed() {
253
254 @Override
255 public void onEnterPressed() {
256 sendMessage();
257 }
258 });
259
260 mSendButton = (ImageButton) view.findViewById(R.id.textSendButton);
261 mSendButton.setOnClickListener(this.mSendButtonListener);
262
263 snackbar = (RelativeLayout) view.findViewById(R.id.snackbar);
264 snackbarMessage = (TextView) view.findViewById(R.id.snackbar_message);
265 snackbarAction = (TextView) view.findViewById(R.id.snackbar_action);
266
267 messagesView = (ListView) view.findViewById(R.id.messages_view);
268 messagesView.setOnScrollListener(mOnScrollListener);
269 messagesView.setTranscriptMode(ListView.TRANSCRIPT_MODE_NORMAL);
270 messageListAdapter = new MessageAdapter(
271 (ConversationActivity) getActivity(), this.messageList);
272 messageListAdapter
273 .setOnContactPictureClicked(new OnContactPictureClicked() {
274
275 @Override
276 public void onContactPictureClicked(Message message) {
277 if (message.getConversation().getMode() == Conversation.MODE_MULTI) {
278 if (message.getPresence() != null) {
279 highlightInConference(message.getPresence());
280 } else {
281 highlightInConference(message.getCounterpart());
282 }
283 }
284 }
285 });
286 messageListAdapter
287 .setOnContactPictureLongClicked(new OnContactPictureLongClicked() {
288
289 @Override
290 public void onContactPictureLongClicked(Message message) {
291 if (message.getConversation().getMode() == Conversation.MODE_MULTI) {
292 if (message.getPresence() != null) {
293 privateMessageWith(message.getPresence());
294 } else {
295 privateMessageWith(message.getCounterpart());
296 }
297 }
298 }
299 });
300 messagesView.setAdapter(messageListAdapter);
301
302 return view;
303 }
304
305 protected void privateMessageWith(String counterpart) {
306 this.mEditMessage.setText("");
307 this.conversation.setNextPresence(counterpart);
308 updateChatMsgHint();
309 }
310
311 protected void highlightInConference(String nick) {
312 String oldString = mEditMessage.getText().toString().trim();
313 if (oldString.isEmpty() || mEditMessage.getSelectionStart() == 0) {
314 mEditMessage.getText().insert(0, nick + ": ");
315 } else {
316 if (mEditMessage.getText().charAt(
317 mEditMessage.getSelectionStart() - 1) != ' ') {
318 nick = " " + nick;
319 }
320 mEditMessage.getText().insert(mEditMessage.getSelectionStart(),
321 nick + " ");
322 }
323 }
324
325 @Override
326 public void onStart() {
327 super.onStart();
328 this.activity = (ConversationActivity) getActivity();
329 if (activity.xmppConnectionServiceBound) {
330 this.onBackendConnected();
331 }
332 }
333
334 @Override
335 public void onStop() {
336 super.onStop();
337 if (this.conversation != null) {
338 this.conversation.setNextMessage(mEditMessage.getText().toString());
339 }
340 }
341
342 public void onBackendConnected() {
343 this.activity = (ConversationActivity) getActivity();
344 this.conversation = activity.getSelectedConversation();
345 if (this.conversation == null) {
346 return;
347 }
348 String oldString = conversation.getNextMessage().trim();
349 if (this.pastedText == null) {
350 this.mEditMessage.setText(oldString);
351 } else {
352
353 if (oldString.isEmpty()) {
354 mEditMessage.setText(pastedText);
355 } else {
356 mEditMessage.setText(oldString + " " + pastedText);
357 }
358 pastedText = null;
359 }
360 int position = mEditMessage.length();
361 Editable etext = mEditMessage.getText();
362 Selection.setSelection(etext, position);
363 if (activity.getSlidingPaneLayout().isSlideable()) {
364 if (!activity.shouldPaneBeOpen()) {
365 activity.getSlidingPaneLayout().closePane();
366 activity.getActionBar().setDisplayHomeAsUpEnabled(true);
367 activity.getActionBar().setHomeButtonEnabled(true);
368 activity.getActionBar().setTitle(conversation.getName());
369 activity.invalidateOptionsMenu();
370 }
371 }
372 if (this.conversation.getMode() == Conversation.MODE_MULTI) {
373 conversation.setNextPresence(null);
374 }
375 updateMessages();
376 }
377
378 private void decryptMessage(Message message) {
379 PgpEngine engine = activity.xmppConnectionService.getPgpEngine();
380 if (engine != null) {
381 engine.decrypt(message, new UiCallback<Message>() {
382
383 @Override
384 public void userInputRequried(PendingIntent pi, Message message) {
385 askForPassphraseIntent = pi.getIntentSender();
386 showSnackbar(R.string.openpgp_messages_found,
387 R.string.decrypt, clickToDecryptListener);
388 }
389
390 @Override
391 public void success(Message message) {
392 activity.xmppConnectionService.databaseBackend
393 .updateMessage(message);
394 updateMessages();
395 }
396
397 @Override
398 public void error(int error, Message message) {
399 message.setEncryption(Message.ENCRYPTION_DECRYPTION_FAILED);
400 // updateMessages();
401 }
402 });
403 }
404 }
405
406 public void updateMessages() {
407 if (getView() == null) {
408 return;
409 }
410 hideSnackbar();
411 final ConversationActivity activity = (ConversationActivity) getActivity();
412 if (this.conversation != null) {
413 final Contact contact = this.conversation.getContact();
414 if (this.conversation.isMuted()) {
415 showSnackbar(R.string.notifications_disabled, R.string.enable,
416 new OnClickListener() {
417
418 @Override
419 public void onClick(View v) {
420 conversation.setMutedTill(0);
421 updateMessages();
422 }
423 });
424 } else if (!contact.showInRoster()
425 && contact
426 .getOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST)) {
427 showSnackbar(R.string.contact_added_you, R.string.add_back,
428 new OnClickListener() {
429
430 @Override
431 public void onClick(View v) {
432 activity.xmppConnectionService
433 .createContact(contact);
434 activity.switchToContactDetails(contact);
435 }
436 });
437 }
438 for (Message message : this.conversation.getMessages()) {
439 if ((message.getEncryption() == Message.ENCRYPTION_PGP)
440 && ((message.getStatus() == Message.STATUS_RECEIVED) || (message
441 .getStatus() == Message.STATUS_SEND))) {
442 decryptMessage(message);
443 break;
444 }
445 }
446 this.messageList.clear();
447 if (this.conversation.getMessages().size() == 0) {
448 messagesLoaded = false;
449 } else {
450 this.messageList.addAll(this.conversation.getMessages());
451 messagesLoaded = true;
452 updateStatusMessages();
453 }
454 this.messageListAdapter.notifyDataSetChanged();
455 if (conversation.getMode() == Conversation.MODE_SINGLE) {
456 if (messageList.size() >= 1) {
457 makeFingerprintWarning(conversation.getLatestEncryption());
458 }
459 } else {
460 if (!conversation.getMucOptions().online()
461 && conversation.getAccount().getStatus() == Account.STATUS_ONLINE) {
462 int error = conversation.getMucOptions().getError();
463 switch (error) {
464 case MucOptions.ERROR_NICK_IN_USE:
465 showSnackbar(R.string.nick_in_use, R.string.edit,
466 clickToMuc);
467 break;
468 case MucOptions.ERROR_ROOM_NOT_FOUND:
469 showSnackbar(R.string.conference_not_found,
470 R.string.leave, leaveMuc);
471 break;
472 case MucOptions.ERROR_PASSWORD_REQUIRED:
473 showSnackbar(R.string.conference_requires_password,
474 R.string.enter_password, enterPassword);
475 break;
476 default:
477 break;
478 }
479 }
480 }
481 getActivity().invalidateOptionsMenu();
482 updateChatMsgHint();
483 if (!activity.shouldPaneBeOpen()) {
484 activity.xmppConnectionService.markRead(conversation);
485 UIHelper.updateNotification(getActivity(),
486 activity.getConversationList(), null, false);
487 activity.updateConversationList();
488 }
489 this.updateSendButton();
490 }
491 }
492
493 private void messageSent() {
494 int size = this.messageList.size();
495 if (size >= 1) {
496 messagesView.setSelection(size - 1);
497 }
498 mEditMessage.setText("");
499 updateChatMsgHint();
500 }
501
502 public void updateSendButton() {
503 Conversation c = this.conversation;
504 if (activity.useSendButtonToIndicateStatus() && c != null
505 && c.getAccount().getStatus() == Account.STATUS_ONLINE) {
506 if (c.getMode() == Conversation.MODE_SINGLE) {
507 switch (c.getContact().getMostAvailableStatus()) {
508 case Presences.ONLINE:
509 this.mSendButton
510 .setImageResource(R.drawable.ic_action_send_now_online);
511 break;
512 case Presences.AWAY:
513 this.mSendButton
514 .setImageResource(R.drawable.ic_action_send_now_away);
515 break;
516 case Presences.XA:
517 this.mSendButton
518 .setImageResource(R.drawable.ic_action_send_now_away);
519 break;
520 case Presences.DND:
521 this.mSendButton
522 .setImageResource(R.drawable.ic_action_send_now_dnd);
523 break;
524 default:
525 this.mSendButton
526 .setImageResource(R.drawable.ic_action_send_now_offline);
527 break;
528 }
529 } else if (c.getMode() == Conversation.MODE_MULTI) {
530 if (c.getMucOptions().online()) {
531 this.mSendButton
532 .setImageResource(R.drawable.ic_action_send_now_online);
533 } else {
534 this.mSendButton
535 .setImageResource(R.drawable.ic_action_send_now_offline);
536 }
537 } else {
538 this.mSendButton
539 .setImageResource(R.drawable.ic_action_send_now_offline);
540 }
541 } else {
542 this.mSendButton
543 .setImageResource(R.drawable.ic_action_send_now_offline);
544 }
545 }
546
547 protected void updateStatusMessages() {
548 if (conversation.getMode() == Conversation.MODE_SINGLE) {
549 for (int i = this.messageList.size() - 1; i >= 0; --i) {
550 if (this.messageList.get(i).getStatus() == Message.STATUS_RECEIVED) {
551 return;
552 } else {
553 if (this.messageList.get(i).getStatus() == Message.STATUS_SEND_DISPLAYED) {
554 this.messageList.add(i + 1,
555 Message.createStatusMessage(conversation));
556 return;
557 }
558 }
559 }
560 }
561 }
562
563 protected void makeFingerprintWarning(int latestEncryption) {
564 Set<String> knownFingerprints = conversation.getContact()
565 .getOtrFingerprints();
566 if ((latestEncryption == Message.ENCRYPTION_OTR)
567 && (conversation.hasValidOtrSession()
568 && (!conversation.isMuted())
569 && (conversation.getOtrSession().getSessionStatus() == SessionStatus.ENCRYPTED) && (!knownFingerprints
570 .contains(conversation.getOtrFingerprint())))) {
571 showSnackbar(R.string.unknown_otr_fingerprint, R.string.verify,
572 new OnClickListener() {
573
574 @Override
575 public void onClick(View v) {
576 if (conversation.getOtrFingerprint() != null) {
577 AlertDialog dialog = UIHelper
578 .getVerifyFingerprintDialog(
579 (ConversationActivity) getActivity(),
580 conversation, snackbar);
581 dialog.show();
582 }
583 }
584 });
585 }
586 }
587
588 protected void showSnackbar(int message, int action,
589 OnClickListener clickListener) {
590 snackbar.setVisibility(View.VISIBLE);
591 snackbar.setOnClickListener(null);
592 snackbarMessage.setText(message);
593 snackbarMessage.setOnClickListener(null);
594 snackbarAction.setText(action);
595 snackbarAction.setOnClickListener(clickListener);
596 }
597
598 protected void hideSnackbar() {
599 snackbar.setVisibility(View.GONE);
600 }
601
602 protected void sendPlainTextMessage(Message message) {
603 ConversationActivity activity = (ConversationActivity) getActivity();
604 activity.xmppConnectionService.sendMessage(message);
605 messageSent();
606 }
607
608 protected void sendPgpMessage(final Message message) {
609 final ConversationActivity activity = (ConversationActivity) getActivity();
610 final XmppConnectionService xmppService = activity.xmppConnectionService;
611 final Contact contact = message.getConversation().getContact();
612 if (activity.hasPgp()) {
613 if (conversation.getMode() == Conversation.MODE_SINGLE) {
614 if (contact.getPgpKeyId() != 0) {
615 xmppService.getPgpEngine().hasKey(contact,
616 new UiCallback<Contact>() {
617
618 @Override
619 public void userInputRequried(PendingIntent pi,
620 Contact contact) {
621 activity.runIntent(
622 pi,
623 ConversationActivity.REQUEST_ENCRYPT_MESSAGE);
624 }
625
626 @Override
627 public void success(Contact contact) {
628 messageSent();
629 activity.encryptTextMessage(message);
630 }
631
632 @Override
633 public void error(int error, Contact contact) {
634
635 }
636 });
637
638 } else {
639 showNoPGPKeyDialog(false,
640 new DialogInterface.OnClickListener() {
641
642 @Override
643 public void onClick(DialogInterface dialog,
644 int which) {
645 conversation
646 .setNextEncryption(Message.ENCRYPTION_NONE);
647 message.setEncryption(Message.ENCRYPTION_NONE);
648 xmppService.sendMessage(message);
649 messageSent();
650 }
651 });
652 }
653 } else {
654 if (conversation.getMucOptions().pgpKeysInUse()) {
655 if (!conversation.getMucOptions().everybodyHasKeys()) {
656 Toast warning = Toast
657 .makeText(getActivity(),
658 R.string.missing_public_keys,
659 Toast.LENGTH_LONG);
660 warning.setGravity(Gravity.CENTER_VERTICAL, 0, 0);
661 warning.show();
662 }
663 activity.encryptTextMessage(message);
664 messageSent();
665 } else {
666 showNoPGPKeyDialog(true,
667 new DialogInterface.OnClickListener() {
668
669 @Override
670 public void onClick(DialogInterface dialog,
671 int which) {
672 conversation
673 .setNextEncryption(Message.ENCRYPTION_NONE);
674 message.setEncryption(Message.ENCRYPTION_NONE);
675 xmppService.sendMessage(message);
676 messageSent();
677 }
678 });
679 }
680 }
681 } else {
682 activity.showInstallPgpDialog();
683 }
684 }
685
686 public void showNoPGPKeyDialog(boolean plural,
687 DialogInterface.OnClickListener listener) {
688 AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
689 builder.setIconAttribute(android.R.attr.alertDialogIcon);
690 if (plural) {
691 builder.setTitle(getString(R.string.no_pgp_keys));
692 builder.setMessage(getText(R.string.contacts_have_no_pgp_keys));
693 } else {
694 builder.setTitle(getString(R.string.no_pgp_key));
695 builder.setMessage(getText(R.string.contact_has_no_pgp_key));
696 }
697 builder.setNegativeButton(getString(R.string.cancel), null);
698 builder.setPositiveButton(getString(R.string.send_unencrypted),
699 listener);
700 builder.create().show();
701 }
702
703 protected void sendOtrMessage(final Message message) {
704 final ConversationActivity activity = (ConversationActivity) getActivity();
705 final XmppConnectionService xmppService = activity.xmppConnectionService;
706 if (conversation.hasValidOtrSession()) {
707 activity.xmppConnectionService.sendMessage(message);
708 messageSent();
709 } else {
710 activity.selectPresence(message.getConversation(),
711 new OnPresenceSelected() {
712
713 @Override
714 public void onPresenceSelected() {
715 message.setPresence(conversation.getNextPresence());
716 xmppService.sendMessage(message);
717 messageSent();
718 }
719 });
720 }
721 }
722
723 public void setText(String text) {
724 this.pastedText = text;
725 }
726
727 public void clearInputField() {
728 this.mEditMessage.setText("");
729 }
730}