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 (activity.hasPgp()) {
443 if (contact.getPgpKeyId() != 0) {
444 xmppService.sendMessage(message, null);
445 chatMsg.setText("");
446 } else {
447 AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
448 builder.setTitle("No openPGP key found");
449 builder.setIconAttribute(android.R.attr.alertDialogIcon);
450 builder.setMessage("There is no openPGP key assoziated with this contact");
451 builder.setNegativeButton("Cancel", null);
452 builder.setPositiveButton("Send plain text",
453 new DialogInterface.OnClickListener() {
454
455 @Override
456 public void onClick(DialogInterface dialog, int which) {
457 conversation.nextMessageEncryption = Message.ENCRYPTION_NONE;
458 message.setEncryption(Message.ENCRYPTION_NONE);
459 xmppService.sendMessage(message, null);
460 chatMsg.setText("");
461 }
462 });
463 builder.create().show();
464 }
465 }
466 }
467
468 protected void sendOtrMessage(final Message message) {
469 ConversationActivity activity = (ConversationActivity) getActivity();
470 final XmppConnectionService xmppService = activity.xmppConnectionService;
471 if (conversation.hasValidOtrSession()) {
472 activity.xmppConnectionService.sendMessage(message, null);
473 chatMsg.setText("");
474 } else {
475 Hashtable<String, Integer> presences;
476 if (conversation.getContact() != null) {
477 presences = conversation.getContact().getPresences();
478 } else {
479 presences = null;
480 }
481 if ((presences == null) || (presences.size() == 0)) {
482 AlertDialog.Builder builder = new AlertDialog.Builder(
483 getActivity());
484 builder.setTitle("Contact is offline");
485 builder.setIconAttribute(android.R.attr.alertDialogIcon);
486 builder.setMessage("Sending OTR encrypted messages to an offline contact is impossible.");
487 builder.setPositiveButton("Send plain text",
488 new DialogInterface.OnClickListener() {
489
490 @Override
491 public void onClick(DialogInterface dialog,
492 int which) {
493 conversation.nextMessageEncryption = Message.ENCRYPTION_NONE;
494 message.setEncryption(Message.ENCRYPTION_NONE);
495 xmppService.sendMessage(message, null);
496 chatMsg.setText("");
497 }
498 });
499 builder.setNegativeButton("Cancel", null);
500 builder.create().show();
501 } else if (presences.size() == 1) {
502 xmppService.sendMessage(message, (String) presences.keySet()
503 .toArray()[0]);
504 chatMsg.setText("");
505 } else {
506 AlertDialog.Builder builder = new AlertDialog.Builder(
507 getActivity());
508 builder.setTitle("Choose Presence");
509 final String[] presencesArray = new String[presences.size()];
510 presences.keySet().toArray(presencesArray);
511 builder.setItems(presencesArray,
512 new DialogInterface.OnClickListener() {
513
514 @Override
515 public void onClick(DialogInterface dialog,
516 int which) {
517 xmppService.sendMessage(message,
518 presencesArray[which]);
519 chatMsg.setText("");
520 }
521 });
522 builder.create().show();
523 }
524 }
525 }
526
527 private static class ViewHolder {
528
529 protected TextView time;
530 protected TextView messageBody;
531 protected ImageView imageView;
532
533 }
534
535 private class BitmapCache {
536 private HashMap<String, Bitmap> bitmaps = new HashMap<String, Bitmap>();
537 private Bitmap error = null;
538
539 public Bitmap get(String name, Uri uri) {
540 if (bitmaps.containsKey(name)) {
541 return bitmaps.get(name);
542 } else {
543 Bitmap bm;
544 if (uri != null) {
545 try {
546 bm = BitmapFactory.decodeStream(getActivity()
547 .getContentResolver().openInputStream(uri));
548 } catch (FileNotFoundException e) {
549 bm = UIHelper.getUnknownContactPicture(name, 200);
550 }
551 } else {
552 bm = UIHelper.getUnknownContactPicture(name, 200);
553 }
554 bitmaps.put(name, bm);
555 return bm;
556 }
557 }
558
559 public Bitmap getError() {
560 if (error == null) {
561 error = UIHelper.getErrorPicture(200);
562 }
563 return error;
564 }
565 }
566
567 class DecryptMessage extends AsyncTask<Message, Void, Boolean> {
568
569 @Override
570 protected Boolean doInBackground(Message... params) {
571 final ConversationActivity activity = (ConversationActivity) getActivity();
572 askForPassphraseIntent = null;
573 for(int i = 0; i < params.length; ++i) {
574 if (params[i].getEncryption() == Message.ENCRYPTION_PGP) {
575 String body = params[i].getBody();
576 String decrypted = null;
577 try {
578 if (activity==null) {
579 return false;
580 }
581 Log.d("gultsch","calling to decrypt message id #"+params[i].getUuid());
582 decrypted = activity.xmppConnectionService.getPgpEngine().decrypt(body);
583 } catch (UserInputRequiredException e) {
584 askForPassphraseIntent = e.getPendingIntent().getIntentSender();
585 activity.runOnUiThread(new Runnable() {
586
587 @Override
588 public void run() {
589 pgpInfo.setVisibility(View.VISIBLE);
590 }
591 });
592
593 return false;
594
595 } catch (OpenPgpException e) {
596 Log.d("gultsch","error decrypting pgp");
597 }
598 if (decrypted!=null) {
599 params[i].setBody(decrypted);
600 params[i].setEncryption(Message.ENCRYPTION_DECRYPTED);
601 activity.xmppConnectionService.updateMessage(params[i]);
602 }
603 if (activity!=null) {
604 activity.runOnUiThread(new Runnable() {
605
606 @Override
607 public void run() {
608 messageListAdapter.notifyDataSetChanged();
609 }
610 });
611 }
612 }
613 if (activity!=null) {
614 activity.runOnUiThread(new Runnable() {
615
616 @Override
617 public void run() {
618 activity.updateConversationList();
619 }
620 });
621 }
622 }
623 return true;
624 }
625
626 }
627}