1package eu.siacs.conversations.ui;
2
3import java.util.ArrayList;
4import java.util.HashMap;
5import java.util.Hashtable;
6import java.util.LinkedList;
7import java.util.List;
8import java.util.Set;
9
10import net.java.otr4j.session.SessionStatus;
11
12import eu.siacs.conversations.R;
13import eu.siacs.conversations.crypto.PgpEngine.OpenPgpException;
14import eu.siacs.conversations.crypto.PgpEngine.UserInputRequiredException;
15import eu.siacs.conversations.entities.Account;
16import eu.siacs.conversations.entities.Contact;
17import eu.siacs.conversations.entities.Conversation;
18import eu.siacs.conversations.entities.Message;
19import eu.siacs.conversations.entities.MucOptions;
20import eu.siacs.conversations.entities.MucOptions.OnRenameListener;
21import eu.siacs.conversations.services.XmppConnectionService;
22import eu.siacs.conversations.utils.UIHelper;
23import android.app.AlertDialog;
24import android.app.Fragment;
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.graphics.Bitmap;
32import android.graphics.Typeface;
33import android.os.AsyncTask;
34import android.os.Bundle;
35import android.preference.PreferenceManager;
36import android.util.Log;
37import android.view.LayoutInflater;
38import android.view.View;
39import android.view.View.OnClickListener;
40import android.view.ViewGroup;
41import android.widget.ArrayAdapter;
42import android.widget.EditText;
43import android.widget.LinearLayout;
44import android.widget.ListView;
45import android.widget.ImageButton;
46import android.widget.ImageView;
47import android.widget.TextView;
48import android.widget.Toast;
49
50public class ConversationFragment extends Fragment {
51
52 protected Conversation conversation;
53 protected ListView messagesView;
54 protected LayoutInflater inflater;
55 protected List<Message> messageList = new ArrayList<Message>();
56 protected ArrayAdapter<Message> messageListAdapter;
57 protected Contact contact;
58 protected BitmapCache mBitmapCache = new BitmapCache();
59
60 protected String queuedPqpMessage = null;
61
62 private EditText chatMsg;
63 private String pastedText = null;
64
65 protected Bitmap selfBitmap;
66
67 private boolean useSubject = true;
68
69 private IntentSender askForPassphraseIntent = null;
70
71 private OnClickListener sendMsgListener = new OnClickListener() {
72
73 @Override
74 public void onClick(View v) {
75 if (chatMsg.getText().length() < 1)
76 return;
77 Message message = new Message(conversation, chatMsg.getText()
78 .toString(), conversation.nextMessageEncryption);
79 if (conversation.nextMessageEncryption == Message.ENCRYPTION_OTR) {
80 sendOtrMessage(message);
81 } else if (conversation.nextMessageEncryption == Message.ENCRYPTION_PGP) {
82 sendPgpMessage(message);
83 } else {
84 sendPlainTextMessage(message);
85 }
86 }
87 };
88 protected OnClickListener clickToDecryptListener = new OnClickListener() {
89
90 @Override
91 public void onClick(View v) {
92 if (askForPassphraseIntent != null) {
93 try {
94 getActivity().startIntentSenderForResult(
95 askForPassphraseIntent,
96 ConversationActivity.REQUEST_DECRYPT_PGP, null, 0,
97 0, 0);
98 } catch (SendIntentException e) {
99 Log.d("xmppService", "couldnt fire intent");
100 }
101 }
102 }
103 };
104
105 private LinearLayout pgpInfo;
106 private LinearLayout mucError;
107 private TextView mucErrorText;
108 private OnClickListener clickToMuc = new OnClickListener() {
109
110 @Override
111 public void onClick(View v) {
112 Intent intent = new Intent(getActivity(), MucDetailsActivity.class);
113 intent.setAction(MucDetailsActivity.ACTION_VIEW_MUC);
114 intent.putExtra("uuid", conversation.getUuid());
115 startActivity(intent);
116 }
117 };
118 private ConversationActivity activity;
119
120 public void hidePgpPassphraseBox() {
121 pgpInfo.setVisibility(View.GONE);
122 }
123
124 public void updateChatMsgHint() {
125 if (conversation.getMode() == Conversation.MODE_MULTI) {
126 chatMsg.setHint("Send message to conference");
127 } else {
128 switch (conversation.nextMessageEncryption) {
129 case Message.ENCRYPTION_NONE:
130 chatMsg.setHint("Send plain text message");
131 break;
132 case Message.ENCRYPTION_OTR:
133 chatMsg.setHint("Send OTR encrypted message");
134 break;
135 case Message.ENCRYPTION_PGP:
136 chatMsg.setHint("Send openPGP encryted messeage");
137 break;
138 case Message.ENCRYPTION_DECRYPTED:
139 chatMsg.setHint("Send openPGP encryted messeage");
140 break;
141 default:
142 break;
143 }
144 }
145 }
146
147 @Override
148 public View onCreateView(final LayoutInflater inflater,
149 ViewGroup container, Bundle savedInstanceState) {
150
151 this.inflater = inflater;
152
153 final View view = inflater.inflate(R.layout.fragment_conversation,
154 container, false);
155 chatMsg = (EditText) view.findViewById(R.id.textinput);
156
157 if (pastedText!=null) {
158 chatMsg.setText(pastedText);
159 }
160
161 ImageButton sendButton = (ImageButton) view
162 .findViewById(R.id.textSendButton);
163 sendButton.setOnClickListener(this.sendMsgListener);
164
165 pgpInfo = (LinearLayout) view.findViewById(R.id.pgp_keyentry);
166 pgpInfo.setOnClickListener(clickToDecryptListener);
167 mucError = (LinearLayout) view.findViewById(R.id.muc_error);
168 mucError.setOnClickListener(clickToMuc);
169 mucErrorText = (TextView) view.findViewById(R.id.muc_error_msg);
170
171 messagesView = (ListView) view.findViewById(R.id.messages_view);
172
173 messageListAdapter = new ArrayAdapter<Message>(this.getActivity()
174 .getApplicationContext(), R.layout.message_sent,
175 this.messageList) {
176
177 private static final int SENT = 0;
178 private static final int RECIEVED = 1;
179 private static final int ERROR = 2;
180
181 @Override
182 public int getViewTypeCount() {
183 return 3;
184 }
185
186 @Override
187 public int getItemViewType(int position) {
188 if (getItem(position).getStatus() == Message.STATUS_RECIEVED) {
189 return RECIEVED;
190 } else if (getItem(position).getStatus() == Message.STATUS_ERROR) {
191 return ERROR;
192 } else {
193 return SENT;
194 }
195 }
196
197 @Override
198 public View getView(int position, View view, ViewGroup parent) {
199 Message item = getItem(position);
200 int type = getItemViewType(position);
201 ViewHolder viewHolder;
202 if (view == null) {
203 viewHolder = new ViewHolder();
204 switch (type) {
205 case SENT:
206 view = (View) inflater.inflate(R.layout.message_sent,
207 null);
208 viewHolder.imageView = (ImageView) view
209 .findViewById(R.id.message_photo);
210 viewHolder.imageView.setImageBitmap(selfBitmap);
211 viewHolder.indicator = (ImageView) view.findViewById(R.id.security_indicator);
212 viewHolder.image = (ImageView) view.findViewById(R.id.message_image);
213 break;
214 case RECIEVED:
215 view = (View) inflater.inflate(
216 R.layout.message_recieved, null);
217 viewHolder.imageView = (ImageView) view
218 .findViewById(R.id.message_photo);
219 viewHolder.indicator = (ImageView) view.findViewById(R.id.security_indicator);
220 if (item.getConversation().getMode() == Conversation.MODE_SINGLE) {
221
222 viewHolder.imageView.setImageBitmap(mBitmapCache
223 .get(item.getConversation().getName(useSubject), item
224 .getConversation().getContact(),
225 getActivity()
226 .getApplicationContext()));
227
228 }
229 break;
230 case ERROR:
231 view = (View) inflater.inflate(R.layout.message_error,
232 null);
233 viewHolder.imageView = (ImageView) view
234 .findViewById(R.id.message_photo);
235 viewHolder.imageView.setImageBitmap(mBitmapCache
236 .getError());
237 break;
238 default:
239 viewHolder = null;
240 break;
241 }
242 viewHolder.messageBody = (TextView) view
243 .findViewById(R.id.message_body);
244 viewHolder.time = (TextView) view
245 .findViewById(R.id.message_time);
246 view.setTag(viewHolder);
247 } else {
248 viewHolder = (ViewHolder) view.getTag();
249 }
250 if (type == RECIEVED) {
251 if (item.getConversation().getMode() == Conversation.MODE_MULTI) {
252 if (item.getCounterpart() != null) {
253 viewHolder.imageView.setImageBitmap(mBitmapCache
254 .get(item.getCounterpart(), null,
255 getActivity()
256 .getApplicationContext()));
257 } else {
258 viewHolder.imageView.setImageBitmap(mBitmapCache
259 .get(item.getConversation().getName(useSubject),
260 null, getActivity()
261 .getApplicationContext()));
262 }
263 }
264 }
265 if (item.getType() == Message.TYPE_IMAGE) {
266 viewHolder.image.setVisibility(View.VISIBLE);
267 viewHolder.image.setImageBitmap(activity.xmppConnectionService.getFileBackend().getImageFromMessage(item));
268 viewHolder.messageBody.setVisibility(View.GONE);
269 } else {
270 if (viewHolder.image != null) viewHolder.image.setVisibility(View.GONE);
271 viewHolder.messageBody.setVisibility(View.VISIBLE);
272 String body = item.getBody();
273 if (body != null) {
274 if (item.getEncryption() == Message.ENCRYPTION_PGP) {
275 viewHolder.messageBody
276 .setText(getString(R.string.encrypted_message));
277 viewHolder.messageBody.setTextColor(0xff33B5E5);
278 viewHolder.messageBody.setTypeface(null,
279 Typeface.ITALIC);
280 viewHolder.indicator.setVisibility(View.VISIBLE);
281 } else if ((item.getEncryption() == Message.ENCRYPTION_OTR)||(item.getEncryption() == Message.ENCRYPTION_DECRYPTED)) {
282 viewHolder.messageBody.setText(body.trim());
283 viewHolder.messageBody.setTextColor(0xff000000);
284 viewHolder.messageBody.setTypeface(null,
285 Typeface.NORMAL);
286 viewHolder.indicator.setVisibility(View.VISIBLE);
287 } else {
288 viewHolder.messageBody.setText(body.trim());
289 viewHolder.messageBody.setTextColor(0xff000000);
290 viewHolder.messageBody.setTypeface(null,
291 Typeface.NORMAL);
292 if (item.getStatus() != Message.STATUS_ERROR) {
293 viewHolder.indicator.setVisibility(View.GONE);
294 }
295 }
296 } else {
297 viewHolder.indicator.setVisibility(View.GONE);
298 }
299 }
300 if (item.getStatus() == Message.STATUS_UNSEND) {
301 viewHolder.time.setTypeface(null, Typeface.ITALIC);
302 viewHolder.time.setText("sending\u2026");
303 } else {
304 viewHolder.time.setTypeface(null, Typeface.NORMAL);
305 if ((item.getConversation().getMode() == Conversation.MODE_SINGLE)
306 || (type != RECIEVED)) {
307 viewHolder.time.setText(UIHelper
308 .readableTimeDifference(item.getTimeSent()));
309 } else {
310 viewHolder.time.setText(item.getCounterpart()
311 + " \u00B7 "
312 + UIHelper.readableTimeDifference(item
313 .getTimeSent()));
314 }
315 }
316 return view;
317 }
318 };
319 messagesView.setAdapter(messageListAdapter);
320
321 return view;
322 }
323
324 protected Bitmap findSelfPicture() {
325 SharedPreferences sharedPref = PreferenceManager
326 .getDefaultSharedPreferences(getActivity()
327 .getApplicationContext());
328 boolean showPhoneSelfContactPicture = sharedPref.getBoolean(
329 "show_phone_selfcontact_picture", true);
330
331 return UIHelper.getSelfContactPicture(conversation.getAccount(), 200,
332 showPhoneSelfContactPicture, getActivity());
333 }
334
335 @Override
336 public void onStart() {
337 super.onStart();
338 this.activity = (ConversationActivity) getActivity();
339 SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(activity);
340 this.useSubject = preferences.getBoolean("use_subject_in_muc", true);
341 if (activity.xmppConnectionServiceBound) {
342 this.onBackendConnected();
343 }
344 }
345
346 public void onBackendConnected() {
347 this.conversation = activity.getSelectedConversation();
348 if (this.conversation == null) {
349 return;
350 }
351 this.selfBitmap = findSelfPicture();
352 updateMessages();
353 // rendering complete. now go tell activity to close pane
354 if (activity.getSlidingPaneLayout().isSlideable()) {
355 if (!activity.shouldPaneBeOpen()) {
356 activity.getSlidingPaneLayout().closePane();
357 activity.getActionBar().setDisplayHomeAsUpEnabled(true);
358 activity.getActionBar().setTitle(conversation.getName(useSubject));
359 activity.invalidateOptionsMenu();
360
361 }
362 }
363 if (queuedPqpMessage != null) {
364 this.conversation.nextMessageEncryption = Message.ENCRYPTION_PGP;
365 Message message = new Message(conversation, queuedPqpMessage,
366 Message.ENCRYPTION_PGP);
367 sendPgpMessage(message);
368 }
369 if (conversation.getMode() == Conversation.MODE_MULTI) {
370 activity.xmppConnectionService
371 .setOnRenameListener(new OnRenameListener() {
372
373 @Override
374 public void onRename(final boolean success) {
375 activity.xmppConnectionService.updateConversation(conversation);
376 getActivity().runOnUiThread(new Runnable() {
377
378 @Override
379 public void run() {
380 if (success) {
381 Toast.makeText(
382 getActivity(),
383 "Your nickname has been changed",
384 Toast.LENGTH_SHORT).show();
385 } else {
386 Toast.makeText(getActivity(),
387 "Nichname is already in use",
388 Toast.LENGTH_SHORT).show();
389 }
390 }
391 });
392 }
393 });
394 }
395 }
396
397 public void updateMessages() {
398 ConversationActivity activity = (ConversationActivity) getActivity();
399 if (this.conversation != null) {
400 List<Message> encryptedMessages = new LinkedList<Message>();
401 for (Message message : this.conversation.getMessages()) {
402 if (message.getEncryption() == Message.ENCRYPTION_PGP) {
403 encryptedMessages.add(message);
404 }
405 }
406 if (encryptedMessages.size() > 0) {
407 DecryptMessage task = new DecryptMessage();
408 Message[] msgs = new Message[encryptedMessages.size()];
409 task.execute(encryptedMessages.toArray(msgs));
410 }
411 this.messageList.clear();
412 this.messageList.addAll(this.conversation.getMessages());
413 this.messageListAdapter.notifyDataSetChanged();
414 if (conversation.getMode() == Conversation.MODE_SINGLE) {
415 if (messageList.size() >= 1) {
416 int latestEncryption = this.conversation.getLatestMessage()
417 .getEncryption();
418 if (latestEncryption == Message.ENCRYPTION_DECRYPTED) {
419 conversation.nextMessageEncryption = Message.ENCRYPTION_PGP;
420 } else {
421 conversation.nextMessageEncryption = latestEncryption;
422 }
423 makeFingerprintWarning(latestEncryption);
424 }
425 } else {
426 if (conversation.getMucOptions().getError() != 0) {
427 mucError.setVisibility(View.VISIBLE);
428 if (conversation.getMucOptions().getError() == MucOptions.ERROR_NICK_IN_USE) {
429 mucErrorText.setText(getString(R.string.nick_in_use));
430 }
431 } else {
432 mucError.setVisibility(View.GONE);
433 }
434 }
435 getActivity().invalidateOptionsMenu();
436 updateChatMsgHint();
437 int size = this.messageList.size();
438 if (size >= 1)
439 messagesView.setSelection(size - 1);
440 if (!activity.shouldPaneBeOpen()) {
441 conversation.markRead();
442 // TODO update notifications
443 UIHelper.updateNotification(getActivity(),
444 activity.getConversationList(), null, false);
445 activity.updateConversationList();
446 }
447 }
448 }
449
450 protected void makeFingerprintWarning(int latestEncryption) {
451 final LinearLayout fingerprintWarning = (LinearLayout) getView()
452 .findViewById(R.id.new_fingerprint);
453 if (conversation.getContact() != null) {
454 Set<String> knownFingerprints = conversation.getContact()
455 .getOtrFingerprints();
456 if ((latestEncryption == Message.ENCRYPTION_OTR)
457 && (conversation.hasValidOtrSession()
458 && (conversation.getOtrSession().getSessionStatus() == SessionStatus.ENCRYPTED) && (!knownFingerprints
459 .contains(conversation.getOtrFingerprint())))) {
460 fingerprintWarning.setVisibility(View.VISIBLE);
461 TextView fingerprint = (TextView) getView().findViewById(
462 R.id.otr_fingerprint);
463 fingerprint.setText(conversation.getOtrFingerprint());
464 fingerprintWarning.setOnClickListener(new OnClickListener() {
465
466 @Override
467 public void onClick(View v) {
468 AlertDialog dialog = UIHelper
469 .getVerifyFingerprintDialog(
470 (ConversationActivity) getActivity(),
471 conversation, fingerprintWarning);
472 dialog.show();
473 }
474 });
475 } else {
476 fingerprintWarning.setVisibility(View.GONE);
477 }
478 } else {
479 fingerprintWarning.setVisibility(View.GONE);
480 }
481 }
482
483 protected void sendPlainTextMessage(Message message) {
484 ConversationActivity activity = (ConversationActivity) getActivity();
485 activity.xmppConnectionService.sendMessage(message, null);
486 chatMsg.setText("");
487 }
488
489 protected void sendPgpMessage(final Message message) {
490 ConversationActivity activity = (ConversationActivity) getActivity();
491 final XmppConnectionService xmppService = activity.xmppConnectionService;
492 Contact contact = message.getConversation().getContact();
493 Account account = message.getConversation().getAccount();
494 if (activity.hasPgp()) {
495 if (contact.getPgpKeyId() != 0) {
496 try {
497 message.setEncryptedBody(xmppService.getPgpEngine().encrypt(account, contact.getPgpKeyId(), message.getBody()));
498 xmppService.sendMessage(message, null);
499 chatMsg.setText("");
500 } catch (UserInputRequiredException e) {
501 try {
502 getActivity().startIntentSenderForResult(e.getPendingIntent().getIntentSender(),
503 ConversationActivity.REQUEST_SEND_MESSAGE, null, 0,
504 0, 0);
505 } catch (SendIntentException e1) {
506 Log.d("xmppService","failed to start intent to send message");
507 }
508 } catch (OpenPgpException e) {
509 Log.d("xmppService","error encrypting with pgp: "+e.getOpenPgpError().getMessage());
510 }
511 } else {
512 AlertDialog.Builder builder = new AlertDialog.Builder(
513 getActivity());
514 builder.setTitle("No openPGP key found");
515 builder.setIconAttribute(android.R.attr.alertDialogIcon);
516 builder.setMessage("There is no openPGP key assoziated with this contact");
517 builder.setNegativeButton("Cancel", null);
518 builder.setPositiveButton("Send plain text",
519 new DialogInterface.OnClickListener() {
520
521 @Override
522 public void onClick(DialogInterface dialog,
523 int which) {
524 conversation.nextMessageEncryption = Message.ENCRYPTION_NONE;
525 message.setEncryption(Message.ENCRYPTION_NONE);
526 xmppService.sendMessage(message, null);
527 chatMsg.setText("");
528 }
529 });
530 builder.create().show();
531 }
532 }
533 }
534
535 protected void sendOtrMessage(final Message message) {
536 ConversationActivity activity = (ConversationActivity) getActivity();
537 final XmppConnectionService xmppService = activity.xmppConnectionService;
538 if (conversation.hasValidOtrSession()) {
539 activity.xmppConnectionService.sendMessage(message, null);
540 chatMsg.setText("");
541 } else {
542 Hashtable<String, Integer> presences;
543 if (conversation.getContact() != null) {
544 presences = conversation.getContact().getPresences();
545 } else {
546 presences = null;
547 }
548 if ((presences == null) || (presences.size() == 0)) {
549 AlertDialog.Builder builder = new AlertDialog.Builder(
550 getActivity());
551 builder.setTitle("Contact is offline");
552 builder.setIconAttribute(android.R.attr.alertDialogIcon);
553 builder.setMessage("Sending OTR encrypted messages to an offline contact is impossible.");
554 builder.setPositiveButton("Send plain text",
555 new DialogInterface.OnClickListener() {
556
557 @Override
558 public void onClick(DialogInterface dialog,
559 int which) {
560 conversation.nextMessageEncryption = Message.ENCRYPTION_NONE;
561 message.setEncryption(Message.ENCRYPTION_NONE);
562 xmppService.sendMessage(message, null);
563 chatMsg.setText("");
564 }
565 });
566 builder.setNegativeButton("Cancel", null);
567 builder.create().show();
568 } else if (presences.size() == 1) {
569 xmppService.sendMessage(message, (String) presences.keySet()
570 .toArray()[0]);
571 chatMsg.setText("");
572 } else {
573 AlertDialog.Builder builder = new AlertDialog.Builder(
574 getActivity());
575 builder.setTitle("Choose Presence");
576 final String[] presencesArray = new String[presences.size()];
577 presences.keySet().toArray(presencesArray);
578 builder.setItems(presencesArray,
579 new DialogInterface.OnClickListener() {
580
581 @Override
582 public void onClick(DialogInterface dialog,
583 int which) {
584 xmppService.sendMessage(message,
585 presencesArray[which]);
586 chatMsg.setText("");
587 }
588 });
589 builder.create().show();
590 }
591 }
592 }
593
594 private static class ViewHolder {
595
596 protected ImageView image;
597 protected ImageView indicator;
598 protected TextView time;
599 protected TextView messageBody;
600 protected ImageView imageView;
601
602 }
603
604 private class BitmapCache {
605 private HashMap<String, Bitmap> bitmaps = new HashMap<String, Bitmap>();
606 private Bitmap error = null;
607
608 public Bitmap get(String name, Contact contact, Context context) {
609 if (bitmaps.containsKey(name)) {
610 return bitmaps.get(name);
611 } else {
612 Bitmap bm = UIHelper.getContactPicture(contact, name, 200, context);
613 bitmaps.put(name, bm);
614 return bm;
615 }
616 }
617
618 public Bitmap getError() {
619 if (error == null) {
620 error = UIHelper.getErrorPicture(200);
621 }
622 return error;
623 }
624 }
625
626 class DecryptMessage extends AsyncTask<Message, Void, Boolean> {
627
628 @Override
629 protected Boolean doInBackground(Message... params) {
630 final ConversationActivity activity = (ConversationActivity) getActivity();
631 askForPassphraseIntent = null;
632 for (int i = 0; i < params.length; ++i) {
633 if (params[i].getEncryption() == Message.ENCRYPTION_PGP) {
634 String body = params[i].getBody();
635 String decrypted = null;
636 if (activity == null) {
637 return false;
638 } else if (!activity.xmppConnectionServiceBound) {
639 return false;
640 }
641 try {
642 decrypted = activity.xmppConnectionService
643 .getPgpEngine().decrypt(conversation.getAccount(),body);
644 } catch (UserInputRequiredException e) {
645 askForPassphraseIntent = e.getPendingIntent()
646 .getIntentSender();
647 activity.runOnUiThread(new Runnable() {
648
649 @Override
650 public void run() {
651 pgpInfo.setVisibility(View.VISIBLE);
652 }
653 });
654
655 return false;
656
657 } catch (OpenPgpException e) {
658 Log.d("gultsch", "error decrypting pgp");
659 }
660 if (decrypted != null) {
661 params[i].setBody(decrypted);
662 params[i].setEncryption(Message.ENCRYPTION_DECRYPTED);
663 activity.xmppConnectionService.updateMessage(params[i]);
664 }
665 if (activity != null) {
666 activity.runOnUiThread(new Runnable() {
667
668 @Override
669 public void run() {
670 messageListAdapter.notifyDataSetChanged();
671 }
672 });
673 }
674 }
675 if (activity != null) {
676 activity.runOnUiThread(new Runnable() {
677
678 @Override
679 public void run() {
680 activity.updateConversationList();
681 }
682 });
683 }
684 }
685 return true;
686 }
687
688 }
689
690 public void setText(String text) {
691 this.pastedText = text;
692 }
693}