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