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 case MucOptions.ERROR_PASSWORD_REQUIRED:
473 showSnackbar(R.string.conference_requires_password,
474 R.string.enter_password, enterPassword);
475 default:
476 break;
477 }
478 }
479 }
480 getActivity().invalidateOptionsMenu();
481 updateChatMsgHint();
482 if (!activity.shouldPaneBeOpen()) {
483 activity.xmppConnectionService.markRead(conversation);
484 UIHelper.updateNotification(getActivity(),
485 activity.getConversationList(), null, false);
486 activity.updateConversationList();
487 }
488 }
489 }
490
491 private void messageSent() {
492 int size = this.messageList.size();
493 if (size >= 1) {
494 messagesView.setSelection(size - 1);
495 }
496 mEditMessage.setText("");
497 updateChatMsgHint();
498 }
499
500 protected void updateStatusMessages() {
501 boolean addedStatusMsg = false;
502 if (conversation.getMode() == Conversation.MODE_SINGLE) {
503 for (int i = this.messageList.size() - 1; i >= 0; --i) {
504 if (addedStatusMsg) {
505 if (this.messageList.get(i).getType() == Message.TYPE_STATUS) {
506 this.messageList.remove(i);
507 --i;
508 }
509 } else {
510 if (this.messageList.get(i).getStatus() == Message.STATUS_RECEIVED) {
511 addedStatusMsg = true;
512 } else {
513 if (this.messageList.get(i).getStatus() == Message.STATUS_SEND_DISPLAYED) {
514 this.messageList.add(i + 1,
515 Message.createStatusMessage(conversation));
516 addedStatusMsg = true;
517 }
518 }
519 }
520 }
521 }
522 }
523
524 protected void makeFingerprintWarning(int latestEncryption) {
525 Set<String> knownFingerprints = conversation.getContact()
526 .getOtrFingerprints();
527 if ((latestEncryption == Message.ENCRYPTION_OTR)
528 && (conversation.hasValidOtrSession()
529 && (!conversation.isMuted())
530 && (conversation.getOtrSession().getSessionStatus() == SessionStatus.ENCRYPTED) && (!knownFingerprints
531 .contains(conversation.getOtrFingerprint())))) {
532 showSnackbar(R.string.unknown_otr_fingerprint, R.string.verify,
533 new OnClickListener() {
534
535 @Override
536 public void onClick(View v) {
537 if (conversation.getOtrFingerprint() != null) {
538 AlertDialog dialog = UIHelper
539 .getVerifyFingerprintDialog(
540 (ConversationActivity) getActivity(),
541 conversation, snackbar);
542 dialog.show();
543 }
544 }
545 });
546 }
547 }
548
549 protected void showSnackbar(int message, int action,
550 OnClickListener clickListener) {
551 snackbar.setVisibility(View.VISIBLE);
552 snackbar.setOnClickListener(null);
553 snackbarMessage.setText(message);
554 snackbarMessage.setOnClickListener(null);
555 snackbarAction.setText(action);
556 snackbarAction.setOnClickListener(clickListener);
557 }
558
559 protected void hideSnackbar() {
560 snackbar.setVisibility(View.GONE);
561 }
562
563 protected void sendPlainTextMessage(Message message) {
564 ConversationActivity activity = (ConversationActivity) getActivity();
565 activity.xmppConnectionService.sendMessage(message);
566 messageSent();
567 }
568
569 protected void sendPgpMessage(final Message message) {
570 final ConversationActivity activity = (ConversationActivity) getActivity();
571 final XmppConnectionService xmppService = activity.xmppConnectionService;
572 final Contact contact = message.getConversation().getContact();
573 if (activity.hasPgp()) {
574 if (conversation.getMode() == Conversation.MODE_SINGLE) {
575 if (contact.getPgpKeyId() != 0) {
576 xmppService.getPgpEngine().hasKey(contact,
577 new UiCallback<Contact>() {
578
579 @Override
580 public void userInputRequried(PendingIntent pi,
581 Contact contact) {
582 activity.runIntent(
583 pi,
584 ConversationActivity.REQUEST_ENCRYPT_MESSAGE);
585 }
586
587 @Override
588 public void success(Contact contact) {
589 messageSent();
590 activity.encryptTextMessage(message);
591 }
592
593 @Override
594 public void error(int error, Contact contact) {
595
596 }
597 });
598
599 } else {
600 showNoPGPKeyDialog(false,
601 new DialogInterface.OnClickListener() {
602
603 @Override
604 public void onClick(DialogInterface dialog,
605 int which) {
606 conversation
607 .setNextEncryption(Message.ENCRYPTION_NONE);
608 message.setEncryption(Message.ENCRYPTION_NONE);
609 xmppService.sendMessage(message);
610 messageSent();
611 }
612 });
613 }
614 } else {
615 if (conversation.getMucOptions().pgpKeysInUse()) {
616 if (!conversation.getMucOptions().everybodyHasKeys()) {
617 Toast warning = Toast
618 .makeText(getActivity(),
619 R.string.missing_public_keys,
620 Toast.LENGTH_LONG);
621 warning.setGravity(Gravity.CENTER_VERTICAL, 0, 0);
622 warning.show();
623 }
624 activity.encryptTextMessage(message);
625 messageSent();
626 } else {
627 showNoPGPKeyDialog(true,
628 new DialogInterface.OnClickListener() {
629
630 @Override
631 public void onClick(DialogInterface dialog,
632 int which) {
633 conversation
634 .setNextEncryption(Message.ENCRYPTION_NONE);
635 message.setEncryption(Message.ENCRYPTION_NONE);
636 xmppService.sendMessage(message);
637 messageSent();
638 }
639 });
640 }
641 }
642 } else {
643 activity.showInstallPgpDialog();
644 }
645 }
646
647 public void showNoPGPKeyDialog(boolean plural,
648 DialogInterface.OnClickListener listener) {
649 AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
650 builder.setIconAttribute(android.R.attr.alertDialogIcon);
651 if (plural) {
652 builder.setTitle(getString(R.string.no_pgp_keys));
653 builder.setMessage(getText(R.string.contacts_have_no_pgp_keys));
654 } else {
655 builder.setTitle(getString(R.string.no_pgp_key));
656 builder.setMessage(getText(R.string.contact_has_no_pgp_key));
657 }
658 builder.setNegativeButton(getString(R.string.cancel), null);
659 builder.setPositiveButton(getString(R.string.send_unencrypted),
660 listener);
661 builder.create().show();
662 }
663
664 protected void sendOtrMessage(final Message message) {
665 final ConversationActivity activity = (ConversationActivity) getActivity();
666 final XmppConnectionService xmppService = activity.xmppConnectionService;
667 if (conversation.hasValidOtrSession()) {
668 activity.xmppConnectionService.sendMessage(message);
669 messageSent();
670 } else {
671 activity.selectPresence(message.getConversation(),
672 new OnPresenceSelected() {
673
674 @Override
675 public void onPresenceSelected() {
676 message.setPresence(conversation.getNextPresence());
677 xmppService.sendMessage(message);
678 messageSent();
679 }
680 });
681 }
682 }
683
684 public void setText(String text) {
685 this.pastedText = text;
686 }
687
688 public void clearInputField() {
689 this.mEditMessage.setText("");
690 }
691}