ConversationFragment.java

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