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