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