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 public void onBackendConnected() {
338 this.conversation = activity.getSelectedConversation();
339 if (this.conversation == null) {
340 return;
341 }
342 this.selfBitmap = findSelfPicture();
343 updateMessages();
344 // rendering complete. now go tell activity to close pane
345 if (activity.getSlidingPaneLayout().isSlideable()) {
346 if (!activity.shouldPaneBeOpen()) {
347 activity.getSlidingPaneLayout().closePane();
348 activity.getActionBar().setDisplayHomeAsUpEnabled(true);
349 activity.getActionBar().setTitle(conversation.getName(useSubject));
350 activity.invalidateOptionsMenu();
351
352 }
353 }
354 if (queuedPqpMessage != null) {
355 this.conversation.nextMessageEncryption = Message.ENCRYPTION_PGP;
356 Message message = new Message(conversation, queuedPqpMessage,
357 Message.ENCRYPTION_PGP);
358 sendPgpMessage(message);
359 }
360 if (conversation.getMode() == Conversation.MODE_MULTI) {
361 activity.xmppConnectionService
362 .setOnRenameListener(new OnRenameListener() {
363
364 @Override
365 public void onRename(final boolean success) {
366 activity.xmppConnectionService.updateConversation(conversation);
367 getActivity().runOnUiThread(new Runnable() {
368
369 @Override
370 public void run() {
371 if (success) {
372 Toast.makeText(
373 getActivity(),
374 "Your nickname has been changed",
375 Toast.LENGTH_SHORT).show();
376 } else {
377 Toast.makeText(getActivity(),
378 "Nichname is already in use",
379 Toast.LENGTH_SHORT).show();
380 }
381 }
382 });
383 }
384 });
385 }
386 }
387
388 public void updateMessages() {
389 ConversationActivity activity = (ConversationActivity) getActivity();
390 if (this.conversation != null) {
391 List<Message> encryptedMessages = new LinkedList<Message>();
392 for (Message message : this.conversation.getMessages()) {
393 if (message.getEncryption() == Message.ENCRYPTION_PGP) {
394 encryptedMessages.add(message);
395 }
396 }
397 if (encryptedMessages.size() > 0) {
398 DecryptMessage task = new DecryptMessage();
399 Message[] msgs = new Message[encryptedMessages.size()];
400 task.execute(encryptedMessages.toArray(msgs));
401 }
402 this.messageList.clear();
403 this.messageList.addAll(this.conversation.getMessages());
404 this.messageListAdapter.notifyDataSetChanged();
405 if (conversation.getMode() == Conversation.MODE_SINGLE) {
406 if (messageList.size() >= 1) {
407 int latestEncryption = this.conversation.getLatestMessage()
408 .getEncryption();
409 if (latestEncryption == Message.ENCRYPTION_DECRYPTED) {
410 conversation.nextMessageEncryption = Message.ENCRYPTION_PGP;
411 } else {
412 conversation.nextMessageEncryption = latestEncryption;
413 }
414 makeFingerprintWarning(latestEncryption);
415 }
416 } else {
417 if (conversation.getMucOptions().getError() != 0) {
418 mucError.setVisibility(View.VISIBLE);
419 if (conversation.getMucOptions().getError() == MucOptions.ERROR_NICK_IN_USE) {
420 mucErrorText.setText(getString(R.string.nick_in_use));
421 }
422 } else {
423 mucError.setVisibility(View.GONE);
424 }
425 }
426 getActivity().invalidateOptionsMenu();
427 updateChatMsgHint();
428 int size = this.messageList.size();
429 if (size >= 1)
430 messagesView.setSelection(size - 1);
431 if (!activity.shouldPaneBeOpen()) {
432 conversation.markRead();
433 // TODO update notifications
434 UIHelper.updateNotification(getActivity(),
435 activity.getConversationList(), null, false);
436 activity.updateConversationList();
437 }
438 }
439 }
440
441 protected void makeFingerprintWarning(int latestEncryption) {
442 final LinearLayout fingerprintWarning = (LinearLayout) getView()
443 .findViewById(R.id.new_fingerprint);
444 if (conversation.getContact() != null) {
445 Set<String> knownFingerprints = conversation.getContact()
446 .getOtrFingerprints();
447 if ((latestEncryption == Message.ENCRYPTION_OTR)
448 && (conversation.hasValidOtrSession()
449 && (conversation.getOtrSession().getSessionStatus() == SessionStatus.ENCRYPTED) && (!knownFingerprints
450 .contains(conversation.getOtrFingerprint())))) {
451 fingerprintWarning.setVisibility(View.VISIBLE);
452 TextView fingerprint = (TextView) getView().findViewById(
453 R.id.otr_fingerprint);
454 fingerprint.setText(conversation.getOtrFingerprint());
455 fingerprintWarning.setOnClickListener(new OnClickListener() {
456
457 @Override
458 public void onClick(View v) {
459 AlertDialog dialog = UIHelper
460 .getVerifyFingerprintDialog(
461 (ConversationActivity) getActivity(),
462 conversation, fingerprintWarning);
463 dialog.show();
464 }
465 });
466 } else {
467 fingerprintWarning.setVisibility(View.GONE);
468 }
469 } else {
470 fingerprintWarning.setVisibility(View.GONE);
471 }
472 }
473
474 protected void sendPlainTextMessage(Message message) {
475 ConversationActivity activity = (ConversationActivity) getActivity();
476 activity.xmppConnectionService.sendMessage(message, null);
477 chatMsg.setText("");
478 }
479
480 protected void sendPgpMessage(final Message message) {
481 ConversationActivity activity = (ConversationActivity) getActivity();
482 final XmppConnectionService xmppService = activity.xmppConnectionService;
483 Contact contact = message.getConversation().getContact();
484 if (activity.hasPgp()) {
485 if (contact.getPgpKeyId() != 0) {
486 xmppService.sendMessage(message, null);
487 chatMsg.setText("");
488 } else {
489 AlertDialog.Builder builder = new AlertDialog.Builder(
490 getActivity());
491 builder.setTitle("No openPGP key found");
492 builder.setIconAttribute(android.R.attr.alertDialogIcon);
493 builder.setMessage("There is no openPGP key assoziated with this contact");
494 builder.setNegativeButton("Cancel", null);
495 builder.setPositiveButton("Send plain text",
496 new DialogInterface.OnClickListener() {
497
498 @Override
499 public void onClick(DialogInterface dialog,
500 int which) {
501 conversation.nextMessageEncryption = Message.ENCRYPTION_NONE;
502 message.setEncryption(Message.ENCRYPTION_NONE);
503 xmppService.sendMessage(message, null);
504 chatMsg.setText("");
505 }
506 });
507 builder.create().show();
508 }
509 }
510 }
511
512 protected void sendOtrMessage(final Message message) {
513 ConversationActivity activity = (ConversationActivity) getActivity();
514 final XmppConnectionService xmppService = activity.xmppConnectionService;
515 if (conversation.hasValidOtrSession()) {
516 activity.xmppConnectionService.sendMessage(message, null);
517 chatMsg.setText("");
518 } else {
519 Hashtable<String, Integer> presences;
520 if (conversation.getContact() != null) {
521 presences = conversation.getContact().getPresences();
522 } else {
523 presences = null;
524 }
525 if ((presences == null) || (presences.size() == 0)) {
526 AlertDialog.Builder builder = new AlertDialog.Builder(
527 getActivity());
528 builder.setTitle("Contact is offline");
529 builder.setIconAttribute(android.R.attr.alertDialogIcon);
530 builder.setMessage("Sending OTR encrypted messages to an offline contact is impossible.");
531 builder.setPositiveButton("Send plain text",
532 new DialogInterface.OnClickListener() {
533
534 @Override
535 public void onClick(DialogInterface dialog,
536 int which) {
537 conversation.nextMessageEncryption = Message.ENCRYPTION_NONE;
538 message.setEncryption(Message.ENCRYPTION_NONE);
539 xmppService.sendMessage(message, null);
540 chatMsg.setText("");
541 }
542 });
543 builder.setNegativeButton("Cancel", null);
544 builder.create().show();
545 } else if (presences.size() == 1) {
546 xmppService.sendMessage(message, (String) presences.keySet()
547 .toArray()[0]);
548 chatMsg.setText("");
549 } else {
550 AlertDialog.Builder builder = new AlertDialog.Builder(
551 getActivity());
552 builder.setTitle("Choose Presence");
553 final String[] presencesArray = new String[presences.size()];
554 presences.keySet().toArray(presencesArray);
555 builder.setItems(presencesArray,
556 new DialogInterface.OnClickListener() {
557
558 @Override
559 public void onClick(DialogInterface dialog,
560 int which) {
561 xmppService.sendMessage(message,
562 presencesArray[which]);
563 chatMsg.setText("");
564 }
565 });
566 builder.create().show();
567 }
568 }
569 }
570
571 private static class ViewHolder {
572
573 protected ImageView indicator;
574 protected TextView time;
575 protected TextView messageBody;
576 protected ImageView imageView;
577
578 }
579
580 private class BitmapCache {
581 private HashMap<String, Bitmap> bitmaps = new HashMap<String, Bitmap>();
582 private Bitmap error = null;
583
584 public Bitmap get(String name, Contact contact, Context context) {
585 if (bitmaps.containsKey(name)) {
586 return bitmaps.get(name);
587 } else {
588 Bitmap bm = UIHelper.getContactPicture(contact, name, 200, context);
589 bitmaps.put(name, bm);
590 return bm;
591 }
592 }
593
594 public Bitmap getError() {
595 if (error == null) {
596 error = UIHelper.getErrorPicture(200);
597 }
598 return error;
599 }
600 }
601
602 class DecryptMessage extends AsyncTask<Message, Void, Boolean> {
603
604 @Override
605 protected Boolean doInBackground(Message... params) {
606 final ConversationActivity activity = (ConversationActivity) getActivity();
607 askForPassphraseIntent = null;
608 for (int i = 0; i < params.length; ++i) {
609 if (params[i].getEncryption() == Message.ENCRYPTION_PGP) {
610 String body = params[i].getBody();
611 String decrypted = null;
612 if (activity == null) {
613 return false;
614 } else if (!activity.xmppConnectionServiceBound) {
615 return false;
616 }
617 try {
618 decrypted = activity.xmppConnectionService
619 .getPgpEngine().decrypt(body);
620 } catch (UserInputRequiredException e) {
621 askForPassphraseIntent = e.getPendingIntent()
622 .getIntentSender();
623 activity.runOnUiThread(new Runnable() {
624
625 @Override
626 public void run() {
627 pgpInfo.setVisibility(View.VISIBLE);
628 }
629 });
630
631 return false;
632
633 } catch (OpenPgpException e) {
634 Log.d("gultsch", "error decrypting pgp");
635 }
636 if (decrypted != null) {
637 params[i].setBody(decrypted);
638 params[i].setEncryption(Message.ENCRYPTION_DECRYPTED);
639 activity.xmppConnectionService.updateMessage(params[i]);
640 }
641 if (activity != null) {
642 activity.runOnUiThread(new Runnable() {
643
644 @Override
645 public void run() {
646 messageListAdapter.notifyDataSetChanged();
647 }
648 });
649 }
650 }
651 if (activity != null) {
652 activity.runOnUiThread(new Runnable() {
653
654 @Override
655 public void run() {
656 activity.updateConversationList();
657 }
658 });
659 }
660 }
661 return true;
662 }
663
664 }
665
666 public void setText(String text) {
667 this.pastedText = text;
668 }
669}