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