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