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
163 private void sendMessage() {
164 if (mEditMessage.getText().length() < 1) {
165 if (this.conversation.getMode() == Conversation.MODE_MULTI) {
166 conversation.setNextPresence(null);
167 updateChatMsgHint();
168 }
169 return;
170 }
171 Message message = new Message(conversation, mEditMessage.getText()
172 .toString(), conversation.getNextEncryption());
173 if (conversation.getMode() == Conversation.MODE_MULTI) {
174 if (conversation.getNextPresence() != null) {
175 message.setPresence(conversation.getNextPresence());
176 message.setType(Message.TYPE_PRIVATE);
177 conversation.setNextPresence(null);
178 }
179 }
180 if (conversation.getNextEncryption() == Message.ENCRYPTION_OTR) {
181 sendOtrMessage(message);
182 } else if (conversation.getNextEncryption() == Message.ENCRYPTION_PGP) {
183 sendPgpMessage(message);
184 } else {
185 sendPlainTextMessage(message);
186 }
187 }
188
189 public void updateChatMsgHint() {
190 if (conversation.getNextPresence() != null) {
191 this.mEditMessage.setHint(getString(R.string.send_private_message_to,conversation.getNextPresence()));
192 } else {
193 switch (conversation.getNextEncryption()) {
194 case Message.ENCRYPTION_NONE:
195 mEditMessage.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((ConversationActivity) getActivity(), this.messageList);
245 messageListAdapter.setOnContactPictureClicked(new OnContactPictureClicked() {
246
247 @Override
248 public void onContactPictureClicked(Message message) {
249 if (message.getConversation().getMode() == Conversation.MODE_MULTI) {
250 if (message.getPresence() != null) {
251 highlightInConference(message.getPresence());
252 } else {
253 highlightInConference(message.getCounterpart());
254 }
255 }
256 }
257 });
258 messageListAdapter.setOnContactPictureLongClicked(new OnContactPictureLongClicked() {
259
260 @Override
261 public void onContactPictureLongClicked(Message message) {
262 if (message.getConversation().getMode() == Conversation.MODE_MULTI) {
263 if (message.getPresence() != null) {
264 privateMessageWith(message.getPresence());
265 } else {
266 privateMessageWith(message.getCounterpart());
267 }
268 }
269 }
270 });
271 messagesView.setAdapter(messageListAdapter);
272
273 return view;
274 }
275
276 protected void privateMessageWith(String counterpart) {
277 this.mEditMessage.setText("");
278 this.conversation.setNextPresence(counterpart);
279 updateChatMsgHint();
280 }
281
282 protected void highlightInConference(String nick) {
283 String oldString = mEditMessage.getText().toString().trim();
284 if (oldString.isEmpty()) {
285 mEditMessage.setText(nick + ": ");
286 } else {
287 mEditMessage.setText(oldString + " " + nick + " ");
288 }
289 int position = mEditMessage.length();
290 Editable etext = mEditMessage.getText();
291 Selection.setSelection(etext, position);
292 }
293
294 @Override
295 public void onStart() {
296 super.onStart();
297 this.activity = (ConversationActivity) getActivity();
298 SharedPreferences preferences = PreferenceManager
299 .getDefaultSharedPreferences(activity);
300 this.useSubject = preferences.getBoolean("use_subject_in_muc", true);
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(useSubject));
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() && contact.getOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST)) {
388 showSnackbar(R.string.contact_added_you, R.string.add_back, new OnClickListener() {
389
390 @Override
391 public void onClick(View v) {
392 activity.xmppConnectionService.createContact(contact);
393 activity.switchToContactDetails(contact);
394 }
395 });
396 }
397 for (Message message : this.conversation.getMessages()) {
398 if ((message.getEncryption() == Message.ENCRYPTION_PGP)
399 && ((message.getStatus() == Message.STATUS_RECIEVED) || (message
400 .getStatus() == Message.STATUS_SEND))) {
401 decryptMessage(message);
402 break;
403 }
404 }
405 if (this.conversation.getMessages().size() == 0) {
406 this.messageList.clear();
407 messagesLoaded = false;
408 } else {
409 for (Message message : this.conversation.getMessages()) {
410 if (!this.messageList.contains(message)) {
411 this.messageList.add(message);
412 }
413 }
414 messagesLoaded = true;
415 updateStatusMessages();
416 }
417 this.messageListAdapter.notifyDataSetChanged();
418 if (conversation.getMode() == Conversation.MODE_SINGLE) {
419 if (messageList.size() >= 1) {
420 makeFingerprintWarning(conversation.getLatestEncryption());
421 }
422 } else {
423 if (!conversation.getMucOptions().online()
424 && conversation.getAccount().getStatus() == Account.STATUS_ONLINE) {
425 if (conversation.getMucOptions().getError() == MucOptions.ERROR_NICK_IN_USE) {
426 showSnackbar(R.string.nick_in_use, R.string.edit,
427 clickToMuc);
428 } else if (conversation.getMucOptions().getError() == MucOptions.ERROR_ROOM_NOT_FOUND) {
429 showSnackbar(R.string.conference_not_found,
430 R.string.leave, leaveMuc);
431 }
432 }
433 }
434 getActivity().invalidateOptionsMenu();
435 updateChatMsgHint();
436 if (!activity.shouldPaneBeOpen()) {
437 activity.xmppConnectionService.markRead(conversation);
438 // TODO update notifications
439 UIHelper.updateNotification(getActivity(),
440 activity.getConversationList(), null, false);
441 activity.updateConversationList();
442 }
443 }
444 }
445
446 private void messageSent() {
447 int size = this.messageList.size();
448 if (size >= 1) {
449 messagesView.setSelection(size - 1);
450 }
451 mEditMessage.setText("");
452 updateChatMsgHint();
453 }
454
455 protected void updateStatusMessages() {
456 boolean addedStatusMsg = false;
457 if (conversation.getMode() == Conversation.MODE_SINGLE) {
458 for (int i = this.messageList.size() - 1; i >= 0; --i) {
459 if (addedStatusMsg) {
460 if (this.messageList.get(i).getType() == Message.TYPE_STATUS) {
461 this.messageList.remove(i);
462 --i;
463 }
464 } else {
465 if (this.messageList.get(i).getStatus() == Message.STATUS_RECIEVED) {
466 addedStatusMsg = true;
467 } else {
468 if (this.messageList.get(i).getStatus() == Message.STATUS_SEND_DISPLAYED) {
469 this.messageList.add(i + 1,
470 Message.createStatusMessage(conversation));
471 addedStatusMsg = true;
472 }
473 }
474 }
475 }
476 }
477 }
478
479 protected void makeFingerprintWarning(int latestEncryption) {
480 Set<String> knownFingerprints = conversation.getContact()
481 .getOtrFingerprints();
482 if ((latestEncryption == Message.ENCRYPTION_OTR)
483 && (conversation.hasValidOtrSession()
484 && (conversation.getOtrSession().getSessionStatus() == SessionStatus.ENCRYPTED) && (!knownFingerprints
485 .contains(conversation.getOtrFingerprint())))) {
486 showSnackbar(R.string.unknown_otr_fingerprint, R.string.verify,
487 new OnClickListener() {
488
489 @Override
490 public void onClick(View v) {
491 if (conversation.getOtrFingerprint() != null) {
492 AlertDialog dialog = UIHelper
493 .getVerifyFingerprintDialog(
494 (ConversationActivity) getActivity(),
495 conversation, snackbar);
496 dialog.show();
497 }
498 }
499 });
500 }
501 }
502
503 protected void showSnackbar(int message, int action,
504 OnClickListener clickListener) {
505 snackbar.setVisibility(View.VISIBLE);
506 snackbar.setOnClickListener(null);
507 snackbarMessage.setText(message);
508 snackbarMessage.setOnClickListener(null);
509 snackbarAction.setText(action);
510 snackbarAction.setOnClickListener(clickListener);
511 }
512
513 protected void hideSnackbar() {
514 snackbar.setVisibility(View.GONE);
515 }
516
517 protected void sendPlainTextMessage(Message message) {
518 ConversationActivity activity = (ConversationActivity) getActivity();
519 activity.xmppConnectionService.sendMessage(message);
520 messageSent();
521 }
522
523 protected void sendPgpMessage(final Message message) {
524 final ConversationActivity activity = (ConversationActivity) getActivity();
525 final XmppConnectionService xmppService = activity.xmppConnectionService;
526 final Contact contact = message.getConversation().getContact();
527 if (activity.hasPgp()) {
528 if (conversation.getMode() == Conversation.MODE_SINGLE) {
529 if (contact.getPgpKeyId() != 0) {
530 xmppService.getPgpEngine().hasKey(contact,
531 new UiCallback<Contact>() {
532
533 @Override
534 public void userInputRequried(PendingIntent pi,
535 Contact contact) {
536 activity.runIntent(
537 pi,
538 ConversationActivity.REQUEST_ENCRYPT_MESSAGE);
539 }
540
541 @Override
542 public void success(Contact contact) {
543 messageSent();
544 activity.encryptTextMessage(message);
545 }
546
547 @Override
548 public void error(int error, Contact contact) {
549
550 }
551 });
552
553 } else {
554 showNoPGPKeyDialog(false,
555 new DialogInterface.OnClickListener() {
556
557 @Override
558 public void onClick(DialogInterface dialog,
559 int which) {
560 conversation
561 .setNextEncryption(Message.ENCRYPTION_NONE);
562 message.setEncryption(Message.ENCRYPTION_NONE);
563 xmppService.sendMessage(message);
564 messageSent();
565 }
566 });
567 }
568 } else {
569 if (conversation.getMucOptions().pgpKeysInUse()) {
570 if (!conversation.getMucOptions().everybodyHasKeys()) {
571 Toast warning = Toast
572 .makeText(getActivity(),
573 R.string.missing_public_keys,
574 Toast.LENGTH_LONG);
575 warning.setGravity(Gravity.CENTER_VERTICAL, 0, 0);
576 warning.show();
577 }
578 activity.encryptTextMessage(message);
579 messageSent();
580 } else {
581 showNoPGPKeyDialog(true,
582 new DialogInterface.OnClickListener() {
583
584 @Override
585 public void onClick(DialogInterface dialog,
586 int which) {
587 conversation
588 .setNextEncryption(Message.ENCRYPTION_NONE);
589 message.setEncryption(Message.ENCRYPTION_NONE);
590 xmppService.sendMessage(message);
591 messageSent();
592 }
593 });
594 }
595 }
596 } else {
597 activity.showInstallPgpDialog();
598 }
599 }
600
601 public void showNoPGPKeyDialog(boolean plural,
602 DialogInterface.OnClickListener listener) {
603 AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
604 builder.setIconAttribute(android.R.attr.alertDialogIcon);
605 if (plural) {
606 builder.setTitle(getString(R.string.no_pgp_keys));
607 builder.setMessage(getText(R.string.contacts_have_no_pgp_keys));
608 } else {
609 builder.setTitle(getString(R.string.no_pgp_key));
610 builder.setMessage(getText(R.string.contact_has_no_pgp_key));
611 }
612 builder.setNegativeButton(getString(R.string.cancel), null);
613 builder.setPositiveButton(getString(R.string.send_unencrypted),
614 listener);
615 builder.create().show();
616 }
617
618 protected void sendOtrMessage(final Message message) {
619 final ConversationActivity activity = (ConversationActivity) getActivity();
620 final XmppConnectionService xmppService = activity.xmppConnectionService;
621 if (conversation.hasValidOtrSession()) {
622 activity.xmppConnectionService.sendMessage(message);
623 messageSent();
624 } else {
625 activity.selectPresence(message.getConversation(),
626 new OnPresenceSelected() {
627
628 @Override
629 public void onPresenceSelected() {
630 message.setPresence(conversation.getNextPresence());
631 xmppService.sendMessage(message);
632 messageSent();
633 }
634 });
635 }
636 }
637
638 public void setText(String text) {
639 this.pastedText = text;
640 }
641
642 public void clearInputField() {
643 this.mEditMessage.setText("");
644 }
645}