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