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