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