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