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