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