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