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