ConversationFragment.java

  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}