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