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.CHAT:
509 this.mSendButton
510 .setImageResource(R.drawable.ic_action_send_now_online);
511 break;
512 case Presences.ONLINE:
513 this.mSendButton
514 .setImageResource(R.drawable.ic_action_send_now_online);
515 break;
516 case Presences.AWAY:
517 this.mSendButton
518 .setImageResource(R.drawable.ic_action_send_now_away);
519 break;
520 case Presences.XA:
521 this.mSendButton
522 .setImageResource(R.drawable.ic_action_send_now_away);
523 break;
524 case Presences.DND:
525 this.mSendButton
526 .setImageResource(R.drawable.ic_action_send_now_dnd);
527 break;
528 default:
529 this.mSendButton
530 .setImageResource(R.drawable.ic_action_send_now_offline);
531 break;
532 }
533 } else if (c.getMode() == Conversation.MODE_MULTI) {
534 if (c.getMucOptions().online()) {
535 this.mSendButton
536 .setImageResource(R.drawable.ic_action_send_now_online);
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 } else {
546 this.mSendButton
547 .setImageResource(R.drawable.ic_action_send_now_offline);
548 }
549 }
550
551 protected void updateStatusMessages() {
552 if (conversation.getMode() == Conversation.MODE_SINGLE) {
553 for (int i = this.messageList.size() - 1; i >= 0; --i) {
554 if (this.messageList.get(i).getStatus() == Message.STATUS_RECEIVED) {
555 return;
556 } else {
557 if (this.messageList.get(i).getStatus() == Message.STATUS_SEND_DISPLAYED) {
558 this.messageList.add(i + 1,
559 Message.createStatusMessage(conversation));
560 return;
561 }
562 }
563 }
564 }
565 }
566
567 protected void makeFingerprintWarning(int latestEncryption) {
568 Set<String> knownFingerprints = conversation.getContact()
569 .getOtrFingerprints();
570 if ((latestEncryption == Message.ENCRYPTION_OTR)
571 && (conversation.hasValidOtrSession()
572 && (!conversation.isMuted())
573 && (conversation.getOtrSession().getSessionStatus() == SessionStatus.ENCRYPTED) && (!knownFingerprints
574 .contains(conversation.getOtrFingerprint())))) {
575 showSnackbar(R.string.unknown_otr_fingerprint, R.string.verify,
576 new OnClickListener() {
577
578 @Override
579 public void onClick(View v) {
580 if (conversation.getOtrFingerprint() != null) {
581 AlertDialog dialog = UIHelper
582 .getVerifyFingerprintDialog(
583 (ConversationActivity) getActivity(),
584 conversation, snackbar);
585 dialog.show();
586 }
587 }
588 });
589 }
590 }
591
592 protected void showSnackbar(int message, int action,
593 OnClickListener clickListener) {
594 snackbar.setVisibility(View.VISIBLE);
595 snackbar.setOnClickListener(null);
596 snackbarMessage.setText(message);
597 snackbarMessage.setOnClickListener(null);
598 snackbarAction.setText(action);
599 snackbarAction.setOnClickListener(clickListener);
600 }
601
602 protected void hideSnackbar() {
603 snackbar.setVisibility(View.GONE);
604 }
605
606 protected void sendPlainTextMessage(Message message) {
607 ConversationActivity activity = (ConversationActivity) getActivity();
608 activity.xmppConnectionService.sendMessage(message);
609 messageSent();
610 }
611
612 protected void sendPgpMessage(final Message message) {
613 final ConversationActivity activity = (ConversationActivity) getActivity();
614 final XmppConnectionService xmppService = activity.xmppConnectionService;
615 final Contact contact = message.getConversation().getContact();
616 if (activity.hasPgp()) {
617 if (conversation.getMode() == Conversation.MODE_SINGLE) {
618 if (contact.getPgpKeyId() != 0) {
619 xmppService.getPgpEngine().hasKey(contact,
620 new UiCallback<Contact>() {
621
622 @Override
623 public void userInputRequried(PendingIntent pi,
624 Contact contact) {
625 activity.runIntent(
626 pi,
627 ConversationActivity.REQUEST_ENCRYPT_MESSAGE);
628 }
629
630 @Override
631 public void success(Contact contact) {
632 messageSent();
633 activity.encryptTextMessage(message);
634 }
635
636 @Override
637 public void error(int error, Contact contact) {
638
639 }
640 });
641
642 } else {
643 showNoPGPKeyDialog(false,
644 new DialogInterface.OnClickListener() {
645
646 @Override
647 public void onClick(DialogInterface dialog,
648 int which) {
649 conversation
650 .setNextEncryption(Message.ENCRYPTION_NONE);
651 message.setEncryption(Message.ENCRYPTION_NONE);
652 xmppService.sendMessage(message);
653 messageSent();
654 }
655 });
656 }
657 } else {
658 if (conversation.getMucOptions().pgpKeysInUse()) {
659 if (!conversation.getMucOptions().everybodyHasKeys()) {
660 Toast warning = Toast
661 .makeText(getActivity(),
662 R.string.missing_public_keys,
663 Toast.LENGTH_LONG);
664 warning.setGravity(Gravity.CENTER_VERTICAL, 0, 0);
665 warning.show();
666 }
667 activity.encryptTextMessage(message);
668 messageSent();
669 } else {
670 showNoPGPKeyDialog(true,
671 new DialogInterface.OnClickListener() {
672
673 @Override
674 public void onClick(DialogInterface dialog,
675 int which) {
676 conversation
677 .setNextEncryption(Message.ENCRYPTION_NONE);
678 message.setEncryption(Message.ENCRYPTION_NONE);
679 xmppService.sendMessage(message);
680 messageSent();
681 }
682 });
683 }
684 }
685 } else {
686 activity.showInstallPgpDialog();
687 }
688 }
689
690 public void showNoPGPKeyDialog(boolean plural,
691 DialogInterface.OnClickListener listener) {
692 AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
693 builder.setIconAttribute(android.R.attr.alertDialogIcon);
694 if (plural) {
695 builder.setTitle(getString(R.string.no_pgp_keys));
696 builder.setMessage(getText(R.string.contacts_have_no_pgp_keys));
697 } else {
698 builder.setTitle(getString(R.string.no_pgp_key));
699 builder.setMessage(getText(R.string.contact_has_no_pgp_key));
700 }
701 builder.setNegativeButton(getString(R.string.cancel), null);
702 builder.setPositiveButton(getString(R.string.send_unencrypted),
703 listener);
704 builder.create().show();
705 }
706
707 protected void sendOtrMessage(final Message message) {
708 final ConversationActivity activity = (ConversationActivity) getActivity();
709 final XmppConnectionService xmppService = activity.xmppConnectionService;
710 if (conversation.hasValidOtrSession()) {
711 activity.xmppConnectionService.sendMessage(message);
712 messageSent();
713 } else {
714 activity.selectPresence(message.getConversation(),
715 new OnPresenceSelected() {
716
717 @Override
718 public void onPresenceSelected() {
719 message.setPresence(conversation.getNextPresence());
720 xmppService.sendMessage(message);
721 messageSent();
722 }
723 });
724 }
725 }
726
727 public void setText(String text) {
728 this.pastedText = text;
729 }
730
731 public void clearInputField() {
732 this.mEditMessage.setText("");
733 }
734}