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