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