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