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
119 public void hidePgpPassphraseBox() {
120 pgpInfo.setVisibility(View.GONE);
121 }
122
123 public void updateChatMsgHint() {
124 if (conversation.getMode() == Conversation.MODE_MULTI) {
125 chatMsg.setHint("Send message to conference");
126 } else {
127 switch (conversation.nextMessageEncryption) {
128 case Message.ENCRYPTION_NONE:
129 chatMsg.setHint("Send plain text message");
130 break;
131 case Message.ENCRYPTION_OTR:
132 chatMsg.setHint("Send OTR encrypted message");
133 break;
134 case Message.ENCRYPTION_PGP:
135 chatMsg.setHint("Send openPGP encryted messeage");
136 break;
137 case Message.ENCRYPTION_DECRYPTED:
138 chatMsg.setHint("Send openPGP encryted messeage");
139 break;
140 default:
141 break;
142 }
143 }
144 }
145
146 @Override
147 public View onCreateView(final LayoutInflater inflater,
148 ViewGroup container, Bundle savedInstanceState) {
149
150 this.inflater = inflater;
151
152 final View view = inflater.inflate(R.layout.fragment_conversation,
153 container, false);
154 chatMsg = (EditText) view.findViewById(R.id.textinput);
155
156 if (pastedText!=null) {
157 chatMsg.setText(pastedText);
158 }
159
160 ImageButton sendButton = (ImageButton) view
161 .findViewById(R.id.textSendButton);
162 sendButton.setOnClickListener(this.sendMsgListener);
163
164 pgpInfo = (LinearLayout) view.findViewById(R.id.pgp_keyentry);
165 pgpInfo.setOnClickListener(clickToDecryptListener);
166 mucError = (LinearLayout) view.findViewById(R.id.muc_error);
167 mucError.setOnClickListener(clickToMuc);
168 mucErrorText = (TextView) view.findViewById(R.id.muc_error_msg);
169
170 messagesView = (ListView) view.findViewById(R.id.messages_view);
171
172 messageListAdapter = new ArrayAdapter<Message>(this.getActivity()
173 .getApplicationContext(), R.layout.message_sent,
174 this.messageList) {
175
176 private static final int SENT = 0;
177 private static final int RECIEVED = 1;
178 private static final int ERROR = 2;
179
180 @Override
181 public int getViewTypeCount() {
182 return 3;
183 }
184
185 @Override
186 public int getItemViewType(int position) {
187 if (getItem(position).getStatus() == Message.STATUS_RECIEVED) {
188 return RECIEVED;
189 } else if (getItem(position).getStatus() == Message.STATUS_ERROR) {
190 return ERROR;
191 } else {
192 return SENT;
193 }
194 }
195
196 @Override
197 public View getView(int position, View view, ViewGroup parent) {
198 Message item = getItem(position);
199 int type = getItemViewType(position);
200 ViewHolder viewHolder;
201 if (view == null) {
202 viewHolder = new ViewHolder();
203 switch (type) {
204 case SENT:
205 view = (View) inflater.inflate(R.layout.message_sent,
206 null);
207 viewHolder.imageView = (ImageView) view
208 .findViewById(R.id.message_photo);
209 viewHolder.imageView.setImageBitmap(selfBitmap);
210 viewHolder.indicator = (ImageView) view.findViewById(R.id.security_indicator);
211 break;
212 case RECIEVED:
213 view = (View) inflater.inflate(
214 R.layout.message_recieved, null);
215 viewHolder.imageView = (ImageView) view
216 .findViewById(R.id.message_photo);
217 viewHolder.indicator = (ImageView) view.findViewById(R.id.security_indicator);
218 if (item.getConversation().getMode() == Conversation.MODE_SINGLE) {
219
220 viewHolder.imageView.setImageBitmap(mBitmapCache
221 .get(item.getConversation().getName(useSubject), item
222 .getConversation().getContact(),
223 getActivity()
224 .getApplicationContext()));
225
226 }
227 break;
228 case ERROR:
229 view = (View) inflater.inflate(R.layout.message_error,
230 null);
231 viewHolder.imageView = (ImageView) view
232 .findViewById(R.id.message_photo);
233 viewHolder.imageView.setImageBitmap(mBitmapCache
234 .getError());
235 break;
236 default:
237 viewHolder = null;
238 break;
239 }
240 viewHolder.messageBody = (TextView) view
241 .findViewById(R.id.message_body);
242 viewHolder.time = (TextView) view
243 .findViewById(R.id.message_time);
244 view.setTag(viewHolder);
245 } else {
246 viewHolder = (ViewHolder) view.getTag();
247 }
248 if (type == RECIEVED) {
249 if (item.getConversation().getMode() == Conversation.MODE_MULTI) {
250 if (item.getCounterpart() != null) {
251 viewHolder.imageView.setImageBitmap(mBitmapCache
252 .get(item.getCounterpart(), null,
253 getActivity()
254 .getApplicationContext()));
255 } else {
256 viewHolder.imageView.setImageBitmap(mBitmapCache
257 .get(item.getConversation().getName(useSubject),
258 null, getActivity()
259 .getApplicationContext()));
260 }
261 }
262 }
263 String body = item.getBody();
264 if (body != null) {
265 if (item.getEncryption() == Message.ENCRYPTION_PGP) {
266 viewHolder.messageBody
267 .setText(getString(R.string.encrypted_message));
268 viewHolder.messageBody.setTextColor(0xff33B5E5);
269 viewHolder.messageBody.setTypeface(null,
270 Typeface.ITALIC);
271 viewHolder.indicator.setVisibility(View.VISIBLE);
272 } else if ((item.getEncryption() == Message.ENCRYPTION_OTR)||(item.getEncryption() == Message.ENCRYPTION_DECRYPTED)) {
273 viewHolder.messageBody.setText(body.trim());
274 viewHolder.messageBody.setTextColor(0xff000000);
275 viewHolder.messageBody.setTypeface(null,
276 Typeface.NORMAL);
277 viewHolder.indicator.setVisibility(View.VISIBLE);
278 } else {
279 viewHolder.messageBody.setText(body.trim());
280 viewHolder.messageBody.setTextColor(0xff000000);
281 viewHolder.messageBody.setTypeface(null,
282 Typeface.NORMAL);
283 if (item.getStatus() != Message.STATUS_ERROR) {
284 viewHolder.indicator.setVisibility(View.GONE);
285 }
286 }
287 } else {
288 viewHolder.indicator.setVisibility(View.GONE);
289 }
290 if (item.getStatus() == Message.STATUS_UNSEND) {
291 viewHolder.time.setTypeface(null, Typeface.ITALIC);
292 viewHolder.time.setText("sending\u2026");
293 } else {
294 viewHolder.time.setTypeface(null, Typeface.NORMAL);
295 if ((item.getConversation().getMode() == Conversation.MODE_SINGLE)
296 || (type != RECIEVED)) {
297 viewHolder.time.setText(UIHelper
298 .readableTimeDifference(item.getTimeSent()));
299 } else {
300 viewHolder.time.setText(item.getCounterpart()
301 + " \u00B7 "
302 + UIHelper.readableTimeDifference(item
303 .getTimeSent()));
304 }
305 }
306 return view;
307 }
308 };
309 messagesView.setAdapter(messageListAdapter);
310
311 return view;
312 }
313
314 protected Bitmap findSelfPicture() {
315 SharedPreferences sharedPref = PreferenceManager
316 .getDefaultSharedPreferences(getActivity()
317 .getApplicationContext());
318 boolean showPhoneSelfContactPicture = sharedPref.getBoolean(
319 "show_phone_selfcontact_picture", true);
320
321 return UIHelper.getSelfContactPicture(conversation.getAccount(), 200,
322 showPhoneSelfContactPicture, getActivity());
323 }
324
325 @Override
326 public void onStart() {
327 super.onStart();
328 ConversationActivity activity = (ConversationActivity) getActivity();
329 SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(activity);
330 this.useSubject = preferences.getBoolean("use_subject_in_muc", true);
331 if (activity.xmppConnectionServiceBound) {
332 this.onBackendConnected();
333 }
334 }
335
336 public void onBackendConnected() {
337 final ConversationActivity activity = (ConversationActivity) getActivity();
338 activity.registerListener();
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 if (activity.hasPgp()) {
486 if (contact.getPgpKeyId() != 0) {
487 xmppService.sendMessage(message, null);
488 chatMsg.setText("");
489 } else {
490 AlertDialog.Builder builder = new AlertDialog.Builder(
491 getActivity());
492 builder.setTitle("No openPGP key found");
493 builder.setIconAttribute(android.R.attr.alertDialogIcon);
494 builder.setMessage("There is no openPGP key assoziated with this contact");
495 builder.setNegativeButton("Cancel", null);
496 builder.setPositiveButton("Send plain text",
497 new DialogInterface.OnClickListener() {
498
499 @Override
500 public void onClick(DialogInterface dialog,
501 int which) {
502 conversation.nextMessageEncryption = Message.ENCRYPTION_NONE;
503 message.setEncryption(Message.ENCRYPTION_NONE);
504 xmppService.sendMessage(message, null);
505 chatMsg.setText("");
506 }
507 });
508 builder.create().show();
509 }
510 }
511 }
512
513 protected void sendOtrMessage(final Message message) {
514 ConversationActivity activity = (ConversationActivity) getActivity();
515 final XmppConnectionService xmppService = activity.xmppConnectionService;
516 if (conversation.hasValidOtrSession()) {
517 activity.xmppConnectionService.sendMessage(message, null);
518 chatMsg.setText("");
519 } else {
520 Hashtable<String, Integer> presences;
521 if (conversation.getContact() != null) {
522 presences = conversation.getContact().getPresences();
523 } else {
524 presences = null;
525 }
526 if ((presences == null) || (presences.size() == 0)) {
527 AlertDialog.Builder builder = new AlertDialog.Builder(
528 getActivity());
529 builder.setTitle("Contact is offline");
530 builder.setIconAttribute(android.R.attr.alertDialogIcon);
531 builder.setMessage("Sending OTR encrypted messages to an offline contact is impossible.");
532 builder.setPositiveButton("Send plain text",
533 new DialogInterface.OnClickListener() {
534
535 @Override
536 public void onClick(DialogInterface dialog,
537 int which) {
538 conversation.nextMessageEncryption = Message.ENCRYPTION_NONE;
539 message.setEncryption(Message.ENCRYPTION_NONE);
540 xmppService.sendMessage(message, null);
541 chatMsg.setText("");
542 }
543 });
544 builder.setNegativeButton("Cancel", null);
545 builder.create().show();
546 } else if (presences.size() == 1) {
547 xmppService.sendMessage(message, (String) presences.keySet()
548 .toArray()[0]);
549 chatMsg.setText("");
550 } else {
551 AlertDialog.Builder builder = new AlertDialog.Builder(
552 getActivity());
553 builder.setTitle("Choose Presence");
554 final String[] presencesArray = new String[presences.size()];
555 presences.keySet().toArray(presencesArray);
556 builder.setItems(presencesArray,
557 new DialogInterface.OnClickListener() {
558
559 @Override
560 public void onClick(DialogInterface dialog,
561 int which) {
562 xmppService.sendMessage(message,
563 presencesArray[which]);
564 chatMsg.setText("");
565 }
566 });
567 builder.create().show();
568 }
569 }
570 }
571
572 private static class ViewHolder {
573
574 protected ImageView indicator;
575 protected TextView time;
576 protected TextView messageBody;
577 protected ImageView imageView;
578
579 }
580
581 private class BitmapCache {
582 private HashMap<String, Bitmap> bitmaps = new HashMap<String, Bitmap>();
583 private Bitmap error = null;
584
585 public Bitmap get(String name, Contact contact, Context context) {
586 if (bitmaps.containsKey(name)) {
587 return bitmaps.get(name);
588 } else {
589 Bitmap bm = UIHelper.getContactPicture(contact, name, 200, context);
590 bitmaps.put(name, bm);
591 return bm;
592 }
593 }
594
595 public Bitmap getError() {
596 if (error == null) {
597 error = UIHelper.getErrorPicture(200);
598 }
599 return error;
600 }
601 }
602
603 class DecryptMessage extends AsyncTask<Message, Void, Boolean> {
604
605 @Override
606 protected Boolean doInBackground(Message... params) {
607 final ConversationActivity activity = (ConversationActivity) getActivity();
608 askForPassphraseIntent = null;
609 for (int i = 0; i < params.length; ++i) {
610 if (params[i].getEncryption() == Message.ENCRYPTION_PGP) {
611 String body = params[i].getBody();
612 String decrypted = null;
613 if (activity == null) {
614 return false;
615 } else if (!activity.xmppConnectionServiceBound) {
616 return false;
617 }
618 try {
619 decrypted = activity.xmppConnectionService
620 .getPgpEngine().decrypt(body);
621 } catch (UserInputRequiredException e) {
622 askForPassphraseIntent = e.getPendingIntent()
623 .getIntentSender();
624 activity.runOnUiThread(new Runnable() {
625
626 @Override
627 public void run() {
628 pgpInfo.setVisibility(View.VISIBLE);
629 }
630 });
631
632 return false;
633
634 } catch (OpenPgpException e) {
635 Log.d("gultsch", "error decrypting pgp");
636 }
637 if (decrypted != null) {
638 params[i].setBody(decrypted);
639 params[i].setEncryption(Message.ENCRYPTION_DECRYPTED);
640 activity.xmppConnectionService.updateMessage(params[i]);
641 }
642 if (activity != null) {
643 activity.runOnUiThread(new Runnable() {
644
645 @Override
646 public void run() {
647 messageListAdapter.notifyDataSetChanged();
648 }
649 });
650 }
651 }
652 if (activity != null) {
653 activity.runOnUiThread(new Runnable() {
654
655 @Override
656 public void run() {
657 activity.updateConversationList();
658 }
659 });
660 }
661 }
662 return true;
663 }
664
665 }
666
667 public void setText(String text) {
668 this.pastedText = text;
669 }
670}