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