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