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