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	public void onBackendConnected() {
338		this.conversation = activity.getSelectedConversation();
339		if (this.conversation == null) {
340			return;
341		}
342		this.selfBitmap = findSelfPicture();
343		updateMessages();
344		// rendering complete. now go tell activity to close pane
345		if (activity.getSlidingPaneLayout().isSlideable()) {
346			if (!activity.shouldPaneBeOpen()) {
347				activity.getSlidingPaneLayout().closePane();
348				activity.getActionBar().setDisplayHomeAsUpEnabled(true);
349				activity.getActionBar().setTitle(conversation.getName(useSubject));
350				activity.invalidateOptionsMenu();
351
352			}
353		}
354		if (queuedPqpMessage != null) {
355			this.conversation.nextMessageEncryption = Message.ENCRYPTION_PGP;
356			Message message = new Message(conversation, queuedPqpMessage,
357					Message.ENCRYPTION_PGP);
358			sendPgpMessage(message);
359		}
360		if (conversation.getMode() == Conversation.MODE_MULTI) {
361			activity.xmppConnectionService
362					.setOnRenameListener(new OnRenameListener() {
363
364						@Override
365						public void onRename(final boolean success) {
366							activity.xmppConnectionService.updateConversation(conversation);
367							getActivity().runOnUiThread(new Runnable() {
368
369								@Override
370								public void run() {
371									if (success) {
372										Toast.makeText(
373												getActivity(),
374												"Your nickname has been changed",
375												Toast.LENGTH_SHORT).show();
376									} else {
377										Toast.makeText(getActivity(),
378												"Nichname is already in use",
379												Toast.LENGTH_SHORT).show();
380									}
381								}
382							});
383						}
384					});
385		}
386	}
387
388	public void updateMessages() {
389		ConversationActivity activity = (ConversationActivity) getActivity();
390		if (this.conversation != null) {
391			List<Message> encryptedMessages = new LinkedList<Message>();
392			for (Message message : this.conversation.getMessages()) {
393				if (message.getEncryption() == Message.ENCRYPTION_PGP) {
394					encryptedMessages.add(message);
395				}
396			}
397			if (encryptedMessages.size() > 0) {
398				DecryptMessage task = new DecryptMessage();
399				Message[] msgs = new Message[encryptedMessages.size()];
400				task.execute(encryptedMessages.toArray(msgs));
401			}
402			this.messageList.clear();
403			this.messageList.addAll(this.conversation.getMessages());
404			this.messageListAdapter.notifyDataSetChanged();
405			if (conversation.getMode() == Conversation.MODE_SINGLE) {
406				if (messageList.size() >= 1) {
407					int latestEncryption = this.conversation.getLatestMessage()
408							.getEncryption();
409					if (latestEncryption == Message.ENCRYPTION_DECRYPTED) {
410						conversation.nextMessageEncryption = Message.ENCRYPTION_PGP;
411					} else {
412						conversation.nextMessageEncryption = latestEncryption;
413					}
414					makeFingerprintWarning(latestEncryption);
415				}
416			} else {
417				if (conversation.getMucOptions().getError() != 0) {
418					mucError.setVisibility(View.VISIBLE);
419					if (conversation.getMucOptions().getError() == MucOptions.ERROR_NICK_IN_USE) {
420						mucErrorText.setText(getString(R.string.nick_in_use));
421					}
422				} else {
423					mucError.setVisibility(View.GONE);
424				}
425			}
426			getActivity().invalidateOptionsMenu();
427			updateChatMsgHint();
428			int size = this.messageList.size();
429			if (size >= 1)
430				messagesView.setSelection(size - 1);
431			if (!activity.shouldPaneBeOpen()) {
432				conversation.markRead();
433				// TODO update notifications
434				UIHelper.updateNotification(getActivity(),
435						activity.getConversationList(), null, false);
436				activity.updateConversationList();
437			}
438		}
439	}
440
441	protected void makeFingerprintWarning(int latestEncryption) {
442		final LinearLayout fingerprintWarning = (LinearLayout) getView()
443				.findViewById(R.id.new_fingerprint);
444		if (conversation.getContact() != null) {
445			Set<String> knownFingerprints = conversation.getContact()
446					.getOtrFingerprints();
447			if ((latestEncryption == Message.ENCRYPTION_OTR)
448					&& (conversation.hasValidOtrSession()
449							&& (conversation.getOtrSession().getSessionStatus() == SessionStatus.ENCRYPTED) && (!knownFingerprints
450								.contains(conversation.getOtrFingerprint())))) {
451				fingerprintWarning.setVisibility(View.VISIBLE);
452				TextView fingerprint = (TextView) getView().findViewById(
453						R.id.otr_fingerprint);
454				fingerprint.setText(conversation.getOtrFingerprint());
455				fingerprintWarning.setOnClickListener(new OnClickListener() {
456
457					@Override
458					public void onClick(View v) {
459						AlertDialog dialog = UIHelper
460								.getVerifyFingerprintDialog(
461										(ConversationActivity) getActivity(),
462										conversation, fingerprintWarning);
463						dialog.show();
464					}
465				});
466			} else {
467				fingerprintWarning.setVisibility(View.GONE);
468			}
469		} else {
470			fingerprintWarning.setVisibility(View.GONE);
471		}
472	}
473
474	protected void sendPlainTextMessage(Message message) {
475		ConversationActivity activity = (ConversationActivity) getActivity();
476		activity.xmppConnectionService.sendMessage(message, null);
477		chatMsg.setText("");
478	}
479
480	protected void sendPgpMessage(final Message message) {
481		ConversationActivity activity = (ConversationActivity) getActivity();
482		final XmppConnectionService xmppService = activity.xmppConnectionService;
483		Contact contact = message.getConversation().getContact();
484		if (activity.hasPgp()) {
485			if (contact.getPgpKeyId() != 0) {
486				xmppService.sendMessage(message, null);
487				chatMsg.setText("");
488			} else {
489				AlertDialog.Builder builder = new AlertDialog.Builder(
490						getActivity());
491				builder.setTitle("No openPGP key found");
492				builder.setIconAttribute(android.R.attr.alertDialogIcon);
493				builder.setMessage("There is no openPGP key assoziated with this contact");
494				builder.setNegativeButton("Cancel", null);
495				builder.setPositiveButton("Send plain text",
496						new DialogInterface.OnClickListener() {
497
498							@Override
499							public void onClick(DialogInterface dialog,
500									int which) {
501								conversation.nextMessageEncryption = Message.ENCRYPTION_NONE;
502								message.setEncryption(Message.ENCRYPTION_NONE);
503								xmppService.sendMessage(message, null);
504								chatMsg.setText("");
505							}
506						});
507				builder.create().show();
508			}
509		}
510	}
511
512	protected void sendOtrMessage(final Message message) {
513		ConversationActivity activity = (ConversationActivity) getActivity();
514		final XmppConnectionService xmppService = activity.xmppConnectionService;
515		if (conversation.hasValidOtrSession()) {
516			activity.xmppConnectionService.sendMessage(message, null);
517			chatMsg.setText("");
518		} else {
519			Hashtable<String, Integer> presences;
520			if (conversation.getContact() != null) {
521				presences = conversation.getContact().getPresences();
522			} else {
523				presences = null;
524			}
525			if ((presences == null) || (presences.size() == 0)) {
526				AlertDialog.Builder builder = new AlertDialog.Builder(
527						getActivity());
528				builder.setTitle("Contact is offline");
529				builder.setIconAttribute(android.R.attr.alertDialogIcon);
530				builder.setMessage("Sending OTR encrypted messages to an offline contact is impossible.");
531				builder.setPositiveButton("Send plain text",
532						new DialogInterface.OnClickListener() {
533
534							@Override
535							public void onClick(DialogInterface dialog,
536									int which) {
537								conversation.nextMessageEncryption = Message.ENCRYPTION_NONE;
538								message.setEncryption(Message.ENCRYPTION_NONE);
539								xmppService.sendMessage(message, null);
540								chatMsg.setText("");
541							}
542						});
543				builder.setNegativeButton("Cancel", null);
544				builder.create().show();
545			} else if (presences.size() == 1) {
546				xmppService.sendMessage(message, (String) presences.keySet()
547						.toArray()[0]);
548				chatMsg.setText("");
549			} else {
550				AlertDialog.Builder builder = new AlertDialog.Builder(
551						getActivity());
552				builder.setTitle("Choose Presence");
553				final String[] presencesArray = new String[presences.size()];
554				presences.keySet().toArray(presencesArray);
555				builder.setItems(presencesArray,
556						new DialogInterface.OnClickListener() {
557
558							@Override
559							public void onClick(DialogInterface dialog,
560									int which) {
561								xmppService.sendMessage(message,
562										presencesArray[which]);
563								chatMsg.setText("");
564							}
565						});
566				builder.create().show();
567			}
568		}
569	}
570
571	private static class ViewHolder {
572
573		protected ImageView indicator;
574		protected TextView time;
575		protected TextView messageBody;
576		protected ImageView imageView;
577
578	}
579
580	private class BitmapCache {
581		private HashMap<String, Bitmap> bitmaps = new HashMap<String, Bitmap>();
582		private Bitmap error = null;
583
584		public Bitmap get(String name, Contact contact, Context context) {
585			if (bitmaps.containsKey(name)) {
586				return bitmaps.get(name);
587			} else {
588				Bitmap bm = UIHelper.getContactPicture(contact, name, 200, context);
589				bitmaps.put(name, bm);
590				return bm;
591			}
592		}
593
594		public Bitmap getError() {
595			if (error == null) {
596				error = UIHelper.getErrorPicture(200);
597			}
598			return error;
599		}
600	}
601
602	class DecryptMessage extends AsyncTask<Message, Void, Boolean> {
603
604		@Override
605		protected Boolean doInBackground(Message... params) {
606			final ConversationActivity activity = (ConversationActivity) getActivity();
607			askForPassphraseIntent = null;
608			for (int i = 0; i < params.length; ++i) {
609				if (params[i].getEncryption() == Message.ENCRYPTION_PGP) {
610					String body = params[i].getBody();
611					String decrypted = null;
612					if (activity == null) {
613						return false;
614					} else if (!activity.xmppConnectionServiceBound) {
615						return false;
616					}
617					try {
618						decrypted = activity.xmppConnectionService
619								.getPgpEngine().decrypt(body);
620					} catch (UserInputRequiredException e) {
621						askForPassphraseIntent = e.getPendingIntent()
622								.getIntentSender();
623						activity.runOnUiThread(new Runnable() {
624
625							@Override
626							public void run() {
627								pgpInfo.setVisibility(View.VISIBLE);
628							}
629						});
630
631						return false;
632
633					} catch (OpenPgpException e) {
634						Log.d("gultsch", "error decrypting pgp");
635					}
636					if (decrypted != null) {
637						params[i].setBody(decrypted);
638						params[i].setEncryption(Message.ENCRYPTION_DECRYPTED);
639						activity.xmppConnectionService.updateMessage(params[i]);
640					}
641					if (activity != null) {
642						activity.runOnUiThread(new Runnable() {
643
644							@Override
645							public void run() {
646								messageListAdapter.notifyDataSetChanged();
647							}
648						});
649					}
650				}
651				if (activity != null) {
652					activity.runOnUiThread(new Runnable() {
653
654						@Override
655						public void run() {
656							activity.updateConversationList();
657						}
658					});
659				}
660			}
661			return true;
662		}
663
664	}
665
666	public void setText(String text) {
667		this.pastedText = text;
668	}
669}