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