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.conversation = activity.getSelectedConversation();
276 if (this.conversation == null) {
277 return;
278 }
279 String oldString = conversation.getNextMessage().trim();
280 if (this.pastedText == null) {
281 this.mEditMessage.setText(oldString);
282 } else {
283
284 if (oldString.isEmpty()) {
285 mEditMessage.setText(pastedText);
286 } else {
287 mEditMessage.setText(oldString + " " + pastedText);
288 }
289 pastedText = null;
290 }
291 int position = mEditMessage.length();
292 Editable etext = mEditMessage.getText();
293 Selection.setSelection(etext, position);
294 updateMessages();
295 if (activity.getSlidingPaneLayout().isSlideable()) {
296 if (!activity.shouldPaneBeOpen()) {
297 activity.getSlidingPaneLayout().closePane();
298 activity.getActionBar().setDisplayHomeAsUpEnabled(true);
299 activity.getActionBar().setHomeButtonEnabled(true);
300 activity.getActionBar().setTitle(
301 conversation.getName(useSubject));
302 activity.invalidateOptionsMenu();
303 }
304 }
305 }
306
307 private void decryptMessage(Message message) {
308 PgpEngine engine = activity.xmppConnectionService.getPgpEngine();
309 if (engine != null) {
310 engine.decrypt(message, new UiCallback<Message>() {
311
312 @Override
313 public void userInputRequried(PendingIntent pi, Message message) {
314 askForPassphraseIntent = pi.getIntentSender();
315 showSnackbar(R.string.openpgp_messages_found,
316 R.string.decrypt, clickToDecryptListener);
317 }
318
319 @Override
320 public void success(Message message) {
321 activity.xmppConnectionService.databaseBackend
322 .updateMessage(message);
323 updateMessages();
324 }
325
326 @Override
327 public void error(int error, Message message) {
328 message.setEncryption(Message.ENCRYPTION_DECRYPTION_FAILED);
329 // updateMessages();
330 }
331 });
332 }
333 }
334
335 public void updateMessages() {
336 if (getView() == null) {
337 return;
338 }
339 hideSnackbar();
340 final ConversationActivity activity = (ConversationActivity) getActivity();
341 if (this.conversation != null) {
342 final Contact contact = this.conversation.getContact();
343 if (!contact.showInRoster() && contact.getOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST)) {
344 showSnackbar(R.string.contact_added_you, R.string.add_back, new OnClickListener() {
345
346 @Override
347 public void onClick(View v) {
348 activity.xmppConnectionService.createContact(contact);
349 activity.switchToContactDetails(contact);
350 }
351 });
352 }
353 for (Message message : this.conversation.getMessages()) {
354 if ((message.getEncryption() == Message.ENCRYPTION_PGP)
355 && ((message.getStatus() == Message.STATUS_RECIEVED) || (message
356 .getStatus() == Message.STATUS_SEND))) {
357 decryptMessage(message);
358 break;
359 }
360 }
361 if (this.conversation.getMessages().size() == 0) {
362 this.messageList.clear();
363 messagesLoaded = false;
364 } else {
365 for (Message message : this.conversation.getMessages()) {
366 if (!this.messageList.contains(message)) {
367 this.messageList.add(message);
368 }
369 }
370 messagesLoaded = true;
371 updateStatusMessages();
372 }
373 this.messageListAdapter.notifyDataSetChanged();
374 if (conversation.getMode() == Conversation.MODE_SINGLE) {
375 if (messageList.size() >= 1) {
376 makeFingerprintWarning(conversation.getLatestEncryption());
377 }
378 } else {
379 if (!conversation.getMucOptions().online()
380 && conversation.getAccount().getStatus() == Account.STATUS_ONLINE) {
381 if (conversation.getMucOptions().getError() == MucOptions.ERROR_NICK_IN_USE) {
382 showSnackbar(R.string.nick_in_use, R.string.edit,
383 clickToMuc);
384 } else if (conversation.getMucOptions().getError() == MucOptions.ERROR_ROOM_NOT_FOUND) {
385 showSnackbar(R.string.conference_not_found,
386 R.string.leave, leaveMuc);
387 }
388 }
389 }
390 getActivity().invalidateOptionsMenu();
391 updateChatMsgHint();
392 if (!activity.shouldPaneBeOpen()) {
393 activity.xmppConnectionService.markRead(conversation);
394 // TODO update notifications
395 UIHelper.updateNotification(getActivity(),
396 activity.getConversationList(), null, false);
397 activity.updateConversationList();
398 }
399 }
400 }
401
402 private void messageSent() {
403 int size = this.messageList.size();
404 if (size >= 1) {
405 messagesView.setSelection(size - 1);
406 }
407 mEditMessage.setText("");
408 }
409
410 protected void updateStatusMessages() {
411 boolean addedStatusMsg = false;
412 if (conversation.getMode() == Conversation.MODE_SINGLE) {
413 for (int i = this.messageList.size() - 1; i >= 0; --i) {
414 if (addedStatusMsg) {
415 if (this.messageList.get(i).getType() == Message.TYPE_STATUS) {
416 this.messageList.remove(i);
417 --i;
418 }
419 } else {
420 if (this.messageList.get(i).getStatus() == Message.STATUS_RECIEVED) {
421 addedStatusMsg = true;
422 } else {
423 if (this.messageList.get(i).getStatus() == Message.STATUS_SEND_DISPLAYED) {
424 this.messageList.add(i + 1,
425 Message.createStatusMessage(conversation));
426 addedStatusMsg = true;
427 }
428 }
429 }
430 }
431 }
432 }
433
434 protected void makeFingerprintWarning(int latestEncryption) {
435 Set<String> knownFingerprints = conversation.getContact()
436 .getOtrFingerprints();
437 if ((latestEncryption == Message.ENCRYPTION_OTR)
438 && (conversation.hasValidOtrSession()
439 && (conversation.getOtrSession().getSessionStatus() == SessionStatus.ENCRYPTED) && (!knownFingerprints
440 .contains(conversation.getOtrFingerprint())))) {
441 showSnackbar(R.string.unknown_otr_fingerprint, R.string.verify,
442 new OnClickListener() {
443
444 @Override
445 public void onClick(View v) {
446 if (conversation.getOtrFingerprint() != null) {
447 AlertDialog dialog = UIHelper
448 .getVerifyFingerprintDialog(
449 (ConversationActivity) getActivity(),
450 conversation, snackbar);
451 dialog.show();
452 }
453 }
454 });
455 }
456 }
457
458 protected void showSnackbar(int message, int action,
459 OnClickListener clickListener) {
460 snackbar.setVisibility(View.VISIBLE);
461 snackbarMessage.setText(message);
462 snackbarAction.setText(action);
463 snackbarAction.setOnClickListener(clickListener);
464 }
465
466 protected void hideSnackbar() {
467 snackbar.setVisibility(View.GONE);
468 }
469
470 protected void sendPlainTextMessage(Message message) {
471 ConversationActivity activity = (ConversationActivity) getActivity();
472 activity.xmppConnectionService.sendMessage(message);
473 messageSent();
474 }
475
476 protected void sendPgpMessage(final Message message) {
477 final ConversationActivity activity = (ConversationActivity) getActivity();
478 final XmppConnectionService xmppService = activity.xmppConnectionService;
479 final Contact contact = message.getConversation().getContact();
480 if (activity.hasPgp()) {
481 if (conversation.getMode() == Conversation.MODE_SINGLE) {
482 if (contact.getPgpKeyId() != 0) {
483 xmppService.getPgpEngine().hasKey(contact,
484 new UiCallback<Contact>() {
485
486 @Override
487 public void userInputRequried(PendingIntent pi,
488 Contact contact) {
489 activity.runIntent(
490 pi,
491 ConversationActivity.REQUEST_ENCRYPT_MESSAGE);
492 }
493
494 @Override
495 public void success(Contact contact) {
496 messageSent();
497 activity.encryptTextMessage(message);
498 }
499
500 @Override
501 public void error(int error, Contact contact) {
502
503 }
504 });
505
506 } else {
507 showNoPGPKeyDialog(false,
508 new DialogInterface.OnClickListener() {
509
510 @Override
511 public void onClick(DialogInterface dialog,
512 int which) {
513 conversation
514 .setNextEncryption(Message.ENCRYPTION_NONE);
515 message.setEncryption(Message.ENCRYPTION_NONE);
516 xmppService.sendMessage(message);
517 messageSent();
518 }
519 });
520 }
521 } else {
522 if (conversation.getMucOptions().pgpKeysInUse()) {
523 if (!conversation.getMucOptions().everybodyHasKeys()) {
524 Toast warning = Toast
525 .makeText(getActivity(),
526 R.string.missing_public_keys,
527 Toast.LENGTH_LONG);
528 warning.setGravity(Gravity.CENTER_VERTICAL, 0, 0);
529 warning.show();
530 }
531 activity.encryptTextMessage(message);
532 messageSent();
533 } else {
534 showNoPGPKeyDialog(true,
535 new DialogInterface.OnClickListener() {
536
537 @Override
538 public void onClick(DialogInterface dialog,
539 int which) {
540 conversation
541 .setNextEncryption(Message.ENCRYPTION_NONE);
542 message.setEncryption(Message.ENCRYPTION_NONE);
543 xmppService.sendMessage(message);
544 messageSent();
545 }
546 });
547 }
548 }
549 } else {
550 activity.showInstallPgpDialog();
551 }
552 }
553
554 public void showNoPGPKeyDialog(boolean plural,
555 DialogInterface.OnClickListener listener) {
556 AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
557 builder.setIconAttribute(android.R.attr.alertDialogIcon);
558 if (plural) {
559 builder.setTitle(getString(R.string.no_pgp_keys));
560 builder.setMessage(getText(R.string.contacts_have_no_pgp_keys));
561 } else {
562 builder.setTitle(getString(R.string.no_pgp_key));
563 builder.setMessage(getText(R.string.contact_has_no_pgp_key));
564 }
565 builder.setNegativeButton(getString(R.string.cancel), null);
566 builder.setPositiveButton(getString(R.string.send_unencrypted),
567 listener);
568 builder.create().show();
569 }
570
571 protected void sendOtrMessage(final Message message) {
572 final ConversationActivity activity = (ConversationActivity) getActivity();
573 final XmppConnectionService xmppService = activity.xmppConnectionService;
574 if (conversation.hasValidOtrSession()) {
575 activity.xmppConnectionService.sendMessage(message);
576 messageSent();
577 } else {
578 activity.selectPresence(message.getConversation(),
579 new OnPresenceSelected() {
580
581 @Override
582 public void onPresenceSelected() {
583 message.setPresence(conversation.getNextPresence());
584 xmppService.sendMessage(message);
585 messageSent();
586 }
587 });
588 }
589 }
590
591 public void setText(String text) {
592 this.pastedText = text;
593 }
594
595 public void clearInputField() {
596 this.mEditMessage.setText("");
597 }
598}