ConversationFragment.java

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