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