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