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