ConversationFragment.java

  1package eu.siacs.conversations.ui;
  2
  3import android.app.AlertDialog;
  4import android.app.Fragment;
  5import android.app.PendingIntent;
  6import android.content.Context;
  7import android.content.DialogInterface;
  8import android.content.Intent;
  9import android.content.IntentSender;
 10import android.content.IntentSender.SendIntentException;
 11import android.os.Bundle;
 12import android.util.Log;
 13import android.view.ContextMenu;
 14import android.view.ContextMenu.ContextMenuInfo;
 15import android.view.Gravity;
 16import android.view.KeyEvent;
 17import android.view.LayoutInflater;
 18import android.view.MenuItem;
 19import android.view.View;
 20import android.view.View.OnClickListener;
 21import android.view.ViewGroup;
 22import android.view.inputmethod.EditorInfo;
 23import android.view.inputmethod.InputMethodManager;
 24import android.widget.AbsListView;
 25import android.widget.AbsListView.OnScrollListener;
 26import android.widget.AdapterView;
 27import android.widget.AdapterView.AdapterContextMenuInfo;
 28import android.widget.ImageButton;
 29import android.widget.ListView;
 30import android.widget.RelativeLayout;
 31import android.widget.TextView;
 32import android.widget.TextView.OnEditorActionListener;
 33import android.widget.Toast;
 34
 35import net.java.otr4j.session.SessionStatus;
 36
 37import java.util.ArrayList;
 38import java.util.List;
 39import java.util.NoSuchElementException;
 40import java.util.concurrent.ConcurrentLinkedQueue;
 41
 42import eu.siacs.conversations.Config;
 43import eu.siacs.conversations.R;
 44import eu.siacs.conversations.crypto.PgpEngine;
 45import eu.siacs.conversations.entities.Account;
 46import eu.siacs.conversations.entities.Contact;
 47import eu.siacs.conversations.entities.Conversation;
 48import eu.siacs.conversations.entities.Downloadable;
 49import eu.siacs.conversations.entities.DownloadableFile;
 50import eu.siacs.conversations.entities.DownloadablePlaceholder;
 51import eu.siacs.conversations.entities.Message;
 52import eu.siacs.conversations.entities.MucOptions;
 53import eu.siacs.conversations.entities.Presences;
 54import eu.siacs.conversations.services.XmppConnectionService;
 55import eu.siacs.conversations.ui.EditMessage.OnEnterPressed;
 56import eu.siacs.conversations.ui.XmppActivity.OnPresenceSelected;
 57import eu.siacs.conversations.ui.XmppActivity.OnValueEdited;
 58import eu.siacs.conversations.ui.adapter.MessageAdapter;
 59import eu.siacs.conversations.ui.adapter.MessageAdapter.OnContactPictureClicked;
 60import eu.siacs.conversations.ui.adapter.MessageAdapter.OnContactPictureLongClicked;
 61import eu.siacs.conversations.xmpp.jid.Jid;
 62
 63public class ConversationFragment extends Fragment {
 64
 65	protected Conversation conversation;
 66	private OnClickListener leaveMuc = new OnClickListener() {
 67
 68		@Override
 69		public void onClick(View v) {
 70			activity.endConversation(conversation);
 71		}
 72	};
 73	private OnClickListener joinMuc = new OnClickListener() {
 74
 75		@Override
 76		public void onClick(View v) {
 77			activity.xmppConnectionService.joinMuc(conversation);
 78		}
 79	};
 80	private OnClickListener enterPassword = new OnClickListener() {
 81
 82		@Override
 83		public void onClick(View v) {
 84			MucOptions muc = conversation.getMucOptions();
 85			String password = muc.getPassword();
 86			if (password == null) {
 87				password = "";
 88			}
 89			activity.quickPasswordEdit(password, new OnValueEdited() {
 90
 91				@Override
 92				public void onValueEdited(String value) {
 93					activity.xmppConnectionService.providePasswordForMuc(
 94							conversation, value);
 95				}
 96			});
 97		}
 98	};
 99	protected ListView messagesView;
100	final protected List<Message> messageList = new ArrayList<>();
101	protected MessageAdapter messageListAdapter;
102	protected Contact contact;
103	private EditMessage mEditMessage;
104	private ImageButton mSendButton;
105	private RelativeLayout snackbar;
106	private TextView snackbarMessage;
107	private TextView snackbarAction;
108	private boolean messagesLoaded = false;
109	private Toast messageLoaderToast;
110
111	private OnScrollListener mOnScrollListener = new OnScrollListener() {
112
113		@Override
114		public void onScrollStateChanged(AbsListView view, int scrollState) {
115			// TODO Auto-generated method stub
116
117		}
118
119		@Override
120		public void onScroll(AbsListView view, int firstVisibleItem,
121							 int visibleItemCount, int totalItemCount) {
122			synchronized (ConversationFragment.this.messageList) {
123				if (firstVisibleItem < 5 && messagesLoaded) {
124					long timestamp = ConversationFragment.this.messageList.get(0).getTimeSent();
125					messagesLoaded = false;
126					activity.xmppConnectionService.loadMoreMessages(conversation, timestamp, new XmppConnectionService.OnMoreMessagesLoaded() {
127						@Override
128						public void onMoreMessagesLoaded(final int count, Conversation conversation) {
129							if (ConversationFragment.this.conversation != conversation) {
130								return;
131							}
132							activity.runOnUiThread(new Runnable() {
133								@Override
134								public void run() {
135									int oldPosition = messagesView.getFirstVisiblePosition();
136									ConversationFragment.this.conversation.populateWithMessages(ConversationFragment.this.messageList);
137									updateStatusMessages();
138									messageListAdapter.notifyDataSetChanged();
139									if (count != 0) {
140										final int newPosition = oldPosition + count;
141										Message tmpMessage = messageList.get(newPosition);
142										int offset = 0;
143										while(tmpMessage.wasMergedIntoPrevious()) {
144											offset++;
145											tmpMessage = tmpMessage.prev();
146										}
147										messagesView.setSelectionFromTop(newPosition - offset, 0);
148										messagesLoaded = true;
149										if (messageLoaderToast != null) {
150											messageLoaderToast.cancel();
151										}
152									}
153								}
154							});
155						}
156
157						@Override
158						public void informUser(int resId) {
159							if (messageLoaderToast != null) {
160								messageLoaderToast.cancel();
161							}
162							messageLoaderToast = Toast.makeText(activity,resId,Toast.LENGTH_LONG);
163							messageLoaderToast.show();
164						}
165					});
166
167				}
168			}
169		}
170	};
171	private IntentSender askForPassphraseIntent = null;
172	protected OnClickListener clickToDecryptListener = new OnClickListener() {
173
174		@Override
175		public void onClick(View v) {
176			if (activity.hasPgp() && askForPassphraseIntent != null) {
177				try {
178					getActivity().startIntentSenderForResult(
179							askForPassphraseIntent,
180							ConversationActivity.REQUEST_DECRYPT_PGP, null, 0,
181							0, 0);
182				} catch (SendIntentException e) {
183					//
184				}
185			}
186		}
187	};
188	protected OnClickListener clickToVerify = new OnClickListener() {
189
190		@Override
191		public void onClick(View v) {
192			if (conversation.getOtrFingerprint() != null) {
193				Intent intent = new Intent(getActivity(), VerifyOTRActivity.class);
194				intent.setAction(VerifyOTRActivity.ACTION_VERIFY_CONTACT);
195				intent.putExtra("contact", conversation.getContact().getJid().toBareJid().toString());
196				intent.putExtra("account", conversation.getAccount().getJid().toBareJid().toString());
197				startActivity(intent);
198			}
199		}
200	};
201	private ConcurrentLinkedQueue<Message> mEncryptedMessages = new ConcurrentLinkedQueue<>();
202	private boolean mDecryptJobRunning = false;
203	private OnEditorActionListener mEditorActionListener = new OnEditorActionListener() {
204
205		@Override
206		public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
207			if (actionId == EditorInfo.IME_ACTION_SEND) {
208				InputMethodManager imm = (InputMethodManager) v.getContext()
209						.getSystemService(Context.INPUT_METHOD_SERVICE);
210				imm.hideSoftInputFromWindow(v.getWindowToken(), 0);
211				sendMessage();
212				return true;
213			} else {
214				return false;
215			}
216		}
217	};
218	private OnClickListener mSendButtonListener = new OnClickListener() {
219
220		@Override
221		public void onClick(View v) {
222			sendMessage();
223		}
224	};
225	private OnClickListener clickToMuc = new OnClickListener() {
226
227		@Override
228		public void onClick(View v) {
229			Intent intent = new Intent(getActivity(),
230					ConferenceDetailsActivity.class);
231			intent.setAction(ConferenceDetailsActivity.ACTION_VIEW_MUC);
232			intent.putExtra("uuid", conversation.getUuid());
233			startActivity(intent);
234		}
235	};
236	private ConversationActivity activity;
237	private Message selectedMessage;
238
239	private void sendMessage() {
240		if (this.conversation == null) {
241			return;
242		}
243		if (mEditMessage.getText().length() < 1) {
244			if (this.conversation.getMode() == Conversation.MODE_MULTI) {
245				conversation.setNextCounterpart(null);
246				updateChatMsgHint();
247			}
248			return;
249		}
250		Message message = new Message(conversation, mEditMessage.getText()
251				.toString(), conversation.getNextEncryption(activity
252				.forceEncryption()));
253		if (conversation.getMode() == Conversation.MODE_MULTI) {
254			if (conversation.getNextCounterpart() != null) {
255				message.setCounterpart(conversation.getNextCounterpart());
256				message.setType(Message.TYPE_PRIVATE);
257				conversation.setNextCounterpart(null);
258			}
259		}
260		if (conversation.getNextEncryption(activity.forceEncryption()) == Message.ENCRYPTION_OTR) {
261			sendOtrMessage(message);
262		} else if (conversation.getNextEncryption(activity.forceEncryption()) == Message.ENCRYPTION_PGP) {
263			sendPgpMessage(message);
264		} else {
265			sendPlainTextMessage(message);
266		}
267	}
268
269	public void updateChatMsgHint() {
270		if (conversation.getMode() == Conversation.MODE_MULTI
271				&& conversation.getNextCounterpart() != null) {
272			this.mEditMessage.setHint(getString(
273					R.string.send_private_message_to,
274					conversation.getNextCounterpart().getResourcepart()));
275		} else {
276			switch (conversation.getNextEncryption(activity.forceEncryption())) {
277				case Message.ENCRYPTION_NONE:
278					mEditMessage
279							.setHint(getString(R.string.send_plain_text_message));
280					break;
281				case Message.ENCRYPTION_OTR:
282					mEditMessage.setHint(getString(R.string.send_otr_message));
283					break;
284				case Message.ENCRYPTION_PGP:
285					mEditMessage.setHint(getString(R.string.send_pgp_message));
286					break;
287				default:
288					break;
289			}
290		}
291	}
292
293	@Override
294	public View onCreateView(final LayoutInflater inflater,
295							 ViewGroup container, Bundle savedInstanceState) {
296		final View view = inflater.inflate(R.layout.fragment_conversation,
297				container, false);
298		mEditMessage = (EditMessage) view.findViewById(R.id.textinput);
299		mEditMessage.setOnClickListener(new OnClickListener() {
300
301			@Override
302			public void onClick(View v) {
303				activity.hideConversationsOverview();
304			}
305		});
306		mEditMessage.setOnEditorActionListener(mEditorActionListener);
307		mEditMessage.setOnEnterPressedListener(new OnEnterPressed() {
308
309			@Override
310			public void onEnterPressed() {
311				sendMessage();
312			}
313		});
314
315		mSendButton = (ImageButton) view.findViewById(R.id.textSendButton);
316		mSendButton.setOnClickListener(this.mSendButtonListener);
317
318		snackbar = (RelativeLayout) view.findViewById(R.id.snackbar);
319		snackbarMessage = (TextView) view.findViewById(R.id.snackbar_message);
320		snackbarAction = (TextView) view.findViewById(R.id.snackbar_action);
321
322		messagesView = (ListView) view.findViewById(R.id.messages_view);
323		messagesView.setOnScrollListener(mOnScrollListener);
324		messagesView.setTranscriptMode(ListView.TRANSCRIPT_MODE_NORMAL);
325		messageListAdapter = new MessageAdapter((ConversationActivity) getActivity(), this.messageList);
326		messageListAdapter.setOnContactPictureClicked(new OnContactPictureClicked() {
327
328					@Override
329					public void onContactPictureClicked(Message message) {
330						if (message.getStatus() <= Message.STATUS_RECEIVED) {
331							if (message.getConversation().getMode() == Conversation.MODE_MULTI) {
332								if (message.getCounterpart() != null) {
333									if (!message.getCounterpart().isBareJid()) {
334										highlightInConference(message.getCounterpart().getResourcepart());
335									} else {
336										highlightInConference(message.getCounterpart().toString());
337									}
338								}
339							} else {
340								Contact contact = message.getConversation()
341										.getContact();
342								if (contact.showInRoster()) {
343									activity.switchToContactDetails(contact);
344								} else {
345									activity.showAddToRosterDialog(message
346											.getConversation());
347								}
348							}
349						} else {
350							Account account = message.getConversation().getAccount();
351							Intent intent = new Intent(activity, EditAccountActivity.class);
352							intent.putExtra("jid", account.getJid().toBareJid().toString());
353							startActivity(intent);
354						}
355					}
356				});
357		messageListAdapter
358				.setOnContactPictureLongClicked(new OnContactPictureLongClicked() {
359
360					@Override
361					public void onContactPictureLongClicked(Message message) {
362						if (message.getStatus() <= Message.STATUS_RECEIVED) {
363							if (message.getConversation().getMode() == Conversation.MODE_MULTI) {
364								if (message.getCounterpart() != null) {
365									privateMessageWith(message.getCounterpart());
366								}
367							}
368						}
369					}
370				});
371		messagesView.setAdapter(messageListAdapter);
372
373		registerForContextMenu(messagesView);
374
375		return view;
376	}
377
378	@Override
379	public void onCreateContextMenu(ContextMenu menu, View v,
380									ContextMenuInfo menuInfo) {
381		synchronized (this.messageList) {
382			super.onCreateContextMenu(menu, v, menuInfo);
383			AdapterView.AdapterContextMenuInfo acmi = (AdapterContextMenuInfo) menuInfo;
384			this.selectedMessage = this.messageList.get(acmi.position);
385			populateContextMenu(menu);
386		}
387	}
388
389	private void populateContextMenu(ContextMenu menu) {
390		if (this.selectedMessage.getType() != Message.TYPE_STATUS) {
391			activity.getMenuInflater().inflate(R.menu.message_context, menu);
392			menu.setHeaderTitle(R.string.message_options);
393			MenuItem copyText = menu.findItem(R.id.copy_text);
394			MenuItem shareImage = menu.findItem(R.id.share_image);
395			MenuItem sendAgain = menu.findItem(R.id.send_again);
396			MenuItem copyUrl = menu.findItem(R.id.copy_url);
397			MenuItem downloadImage = menu.findItem(R.id.download_image);
398			MenuItem cancelTransmission = menu.findItem(R.id.cancel_transmission);
399			if (this.selectedMessage.getType() != Message.TYPE_TEXT
400					|| this.selectedMessage.getDownloadable() != null) {
401				copyText.setVisible(false);
402			}
403			if (this.selectedMessage.getType() != Message.TYPE_IMAGE
404					|| this.selectedMessage.getDownloadable() != null) {
405				shareImage.setVisible(false);
406			}
407			if (this.selectedMessage.getStatus() != Message.STATUS_SEND_FAILED) {
408				sendAgain.setVisible(false);
409			}
410			if ((this.selectedMessage.getType() != Message.TYPE_IMAGE && this.selectedMessage
411					.getDownloadable() == null)
412					|| this.selectedMessage.getImageParams().url == null) {
413				copyUrl.setVisible(false);
414			}
415			if (this.selectedMessage.getType() != Message.TYPE_TEXT
416					|| this.selectedMessage.getDownloadable() != null
417					|| !this.selectedMessage.bodyContainsDownloadable()) {
418				downloadImage.setVisible(false);
419			}
420			if (this.selectedMessage.getDownloadable() == null
421					|| this.selectedMessage.getDownloadable() instanceof DownloadablePlaceholder) {
422				cancelTransmission.setVisible(false);
423			}
424		}
425	}
426
427	@Override
428	public boolean onContextItemSelected(MenuItem item) {
429		switch (item.getItemId()) {
430			case R.id.share_image:
431				shareImage(selectedMessage);
432				return true;
433			case R.id.copy_text:
434				copyText(selectedMessage);
435				return true;
436			case R.id.send_again:
437				resendMessage(selectedMessage);
438				return true;
439			case R.id.copy_url:
440				copyUrl(selectedMessage);
441				return true;
442			case R.id.download_image:
443				downloadImage(selectedMessage);
444				return true;
445			case R.id.cancel_transmission:
446				cancelTransmission(selectedMessage);
447				return true;
448			default:
449				return super.onContextItemSelected(item);
450		}
451	}
452
453	private void shareImage(Message message) {
454		Intent shareIntent = new Intent();
455		shareIntent.setAction(Intent.ACTION_SEND);
456		shareIntent.putExtra(Intent.EXTRA_STREAM,
457				activity.xmppConnectionService.getFileBackend()
458						.getJingleFileUri(message));
459		shareIntent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
460		shareIntent.setType("image/webp");
461		activity.startActivity(Intent.createChooser(shareIntent,
462				getText(R.string.share_with)));
463	}
464
465	private void copyText(Message message) {
466		if (activity.copyTextToClipboard(message.getMergedBody(),
467				R.string.message_text)) {
468			Toast.makeText(activity, R.string.message_copied_to_clipboard,
469					Toast.LENGTH_SHORT).show();
470		}
471	}
472
473	private void resendMessage(Message message) {
474		if (message.getType() == Message.TYPE_FILE || message.getType() == Message.TYPE_IMAGE) {
475			DownloadableFile file = activity.xmppConnectionService.getFileBackend().getFile(message);
476			if (!file.exists()) {
477				Toast.makeText(activity,R.string.file_deleted,Toast.LENGTH_SHORT).show();
478				message.setDownloadable(new DownloadablePlaceholder(Downloadable.STATUS_DELETED));
479				return;
480			}
481		}
482		activity.xmppConnectionService.resendFailedMessages(message);
483	}
484
485	private void copyUrl(Message message) {
486		if (activity.copyTextToClipboard(
487				message.getImageParams().url.toString(), R.string.image_url)) {
488			Toast.makeText(activity, R.string.url_copied_to_clipboard,
489					Toast.LENGTH_SHORT).show();
490		}
491	}
492
493	private void downloadImage(Message message) {
494		activity.xmppConnectionService.getHttpConnectionManager()
495				.createNewConnection(message);
496	}
497
498	private void cancelTransmission(Message message) {
499		Downloadable downloadable = message.getDownloadable();
500		if (downloadable!=null) {
501			downloadable.cancel();
502		}
503	}
504
505	protected void privateMessageWith(final Jid counterpart) {
506		this.mEditMessage.setText("");
507		this.conversation.setNextCounterpart(counterpart);
508		updateChatMsgHint();
509	}
510
511	protected void highlightInConference(String nick) {
512		String oldString = mEditMessage.getText().toString().trim();
513		if (oldString.isEmpty() || mEditMessage.getSelectionStart() == 0) {
514			mEditMessage.getText().insert(0, nick + ": ");
515		} else {
516			if (mEditMessage.getText().charAt(
517					mEditMessage.getSelectionStart() - 1) != ' ') {
518				nick = " " + nick;
519			}
520			mEditMessage.getText().insert(mEditMessage.getSelectionStart(),
521					nick + " ");
522		}
523	}
524
525	@Override
526	public void onStop() {
527		mDecryptJobRunning = false;
528		super.onStop();
529		if (this.conversation != null) {
530			this.conversation.setNextMessage(mEditMessage.getText().toString());
531		}
532	}
533
534	public void reInit(Conversation conversation) {
535		if (conversation == null) {
536			return;
537		}
538		if (this.conversation != null) {
539			this.conversation.setNextMessage(mEditMessage.getText().toString());
540		}
541		this.activity = (ConversationActivity) getActivity();
542		this.conversation = conversation;
543		this.mDecryptJobRunning = false;
544		this.mEncryptedMessages.clear();
545		if (this.conversation.getMode() == Conversation.MODE_MULTI) {
546			this.conversation.setNextCounterpart(null);
547		}
548		this.mEditMessage.setText("");
549		this.mEditMessage.append(this.conversation.getNextMessage());
550		this.messagesView.invalidate();
551		updateMessages();
552	}
553
554	public void updateMessages() {
555		synchronized (this.messageList) {
556			if (getView() == null) {
557				return;
558			}
559			hideSnackbar();
560			final ConversationActivity activity = (ConversationActivity) getActivity();
561			if (this.conversation != null) {
562				final Contact contact = this.conversation.getContact();
563				if (this.conversation.isMuted()) {
564					showSnackbar(R.string.notifications_disabled, R.string.enable,
565							new OnClickListener() {
566
567								@Override
568								public void onClick(View v) {
569									activity.unmuteConversation(conversation);
570								}
571							});
572				} else if (!contact.showInRoster()
573						&& contact
574						.getOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST)) {
575					showSnackbar(R.string.contact_added_you, R.string.add_back,
576							new OnClickListener() {
577
578								@Override
579								public void onClick(View v) {
580									activity.xmppConnectionService
581											.createContact(contact);
582									activity.switchToContactDetails(contact);
583								}
584							});
585				} else if (conversation.getMode() == Conversation.MODE_SINGLE) {
586					makeFingerprintWarning();
587				} else if (!conversation.getMucOptions().online()
588						&& conversation.getAccount().getStatus() == Account.State.ONLINE) {
589					int error = conversation.getMucOptions().getError();
590					switch (error) {
591						case MucOptions.ERROR_NICK_IN_USE:
592							showSnackbar(R.string.nick_in_use, R.string.edit,
593									clickToMuc);
594							break;
595						case MucOptions.ERROR_UNKNOWN:
596							showSnackbar(R.string.conference_not_found,
597									R.string.leave, leaveMuc);
598							break;
599						case MucOptions.ERROR_PASSWORD_REQUIRED:
600							showSnackbar(R.string.conference_requires_password,
601									R.string.enter_password, enterPassword);
602							break;
603						case MucOptions.ERROR_BANNED:
604							showSnackbar(R.string.conference_banned,
605									R.string.leave, leaveMuc);
606							break;
607						case MucOptions.ERROR_MEMBERS_ONLY:
608							showSnackbar(R.string.conference_members_only,
609									R.string.leave, leaveMuc);
610							break;
611						case MucOptions.KICKED_FROM_ROOM:
612							showSnackbar(R.string.conference_kicked, R.string.join,
613									joinMuc);
614							break;
615						default:
616							break;
617					}
618				}
619				conversation.populateWithMessages(ConversationFragment.this.messageList);
620				this.messagesLoaded = this.messageList.size() > 0;
621				for (Message message : this.messageList) {
622					if (message.getEncryption() == Message.ENCRYPTION_PGP
623							&& (message.getStatus() == Message.STATUS_RECEIVED || message
624							.getStatus() >= Message.STATUS_SEND)
625							&& message.getDownloadable() == null) {
626						if (!mEncryptedMessages.contains(message)) {
627							mEncryptedMessages.add(message);
628						}
629					}
630				}
631				decryptNext();
632				updateStatusMessages();
633				this.messageListAdapter.notifyDataSetChanged();
634				updateChatMsgHint();
635				if (!activity.isConversationsOverviewVisable() || !activity.isConversationsOverviewHideable()) {
636					activity.xmppConnectionService.markRead(conversation, true);
637					activity.updateConversationList();
638				}
639				this.updateSendButton();
640			}
641		}
642	}
643
644	private void decryptNext() {
645		Message next = this.mEncryptedMessages.peek();
646		PgpEngine engine = activity.xmppConnectionService.getPgpEngine();
647
648		if (next != null && engine != null && !mDecryptJobRunning) {
649			mDecryptJobRunning = true;
650			engine.decrypt(next, new UiCallback<Message>() {
651
652				@Override
653				public void userInputRequried(PendingIntent pi, Message message) {
654					mDecryptJobRunning = false;
655					askForPassphraseIntent = pi.getIntentSender();
656					showSnackbar(R.string.openpgp_messages_found,
657							R.string.decrypt, clickToDecryptListener);
658				}
659
660				@Override
661				public void success(Message message) {
662					mDecryptJobRunning = false;
663					try {
664						mEncryptedMessages.remove();
665					} catch (final NoSuchElementException ignored) {
666
667					}
668					activity.xmppConnectionService.updateMessage(message);
669				}
670
671				@Override
672				public void error(int error, Message message) {
673					message.setEncryption(Message.ENCRYPTION_DECRYPTION_FAILED);
674					mDecryptJobRunning = false;
675					try {
676						mEncryptedMessages.remove();
677					} catch (final NoSuchElementException ignored) {
678
679					}
680					activity.xmppConnectionService.updateConversationUi();
681				}
682			});
683		}
684	}
685
686	private void messageSent() {
687		int size = this.messageList.size();
688		messagesView.setSelection(size - 1);
689		mEditMessage.setText("");
690		updateChatMsgHint();
691	}
692
693	public void updateSendButton() {
694		Conversation c = this.conversation;
695		if (activity.useSendButtonToIndicateStatus() && c != null
696				&& c.getAccount().getStatus() == Account.State.ONLINE) {
697			if (c.getMode() == Conversation.MODE_SINGLE) {
698				switch (c.getContact().getMostAvailableStatus()) {
699					case Presences.CHAT:
700						this.mSendButton
701								.setImageResource(R.drawable.ic_action_send_now_online);
702						break;
703					case Presences.ONLINE:
704						this.mSendButton
705								.setImageResource(R.drawable.ic_action_send_now_online);
706						break;
707					case Presences.AWAY:
708						this.mSendButton
709								.setImageResource(R.drawable.ic_action_send_now_away);
710						break;
711					case Presences.XA:
712						this.mSendButton
713								.setImageResource(R.drawable.ic_action_send_now_away);
714						break;
715					case Presences.DND:
716						this.mSendButton
717								.setImageResource(R.drawable.ic_action_send_now_dnd);
718						break;
719					default:
720						this.mSendButton
721								.setImageResource(R.drawable.ic_action_send_now_offline);
722						break;
723				}
724			} else if (c.getMode() == Conversation.MODE_MULTI) {
725				if (c.getMucOptions().online()) {
726					this.mSendButton
727							.setImageResource(R.drawable.ic_action_send_now_online);
728				} else {
729					this.mSendButton
730							.setImageResource(R.drawable.ic_action_send_now_offline);
731				}
732			} else {
733				this.mSendButton
734						.setImageResource(R.drawable.ic_action_send_now_offline);
735			}
736		} else {
737			this.mSendButton
738					.setImageResource(R.drawable.ic_action_send_now_offline);
739		}
740	}
741
742	protected void updateStatusMessages() {
743		synchronized (this.messageList) {
744			if (conversation.getMode() == Conversation.MODE_SINGLE) {
745				for (int i = this.messageList.size() - 1; i >= 0; --i) {
746					if (this.messageList.get(i).getStatus() == Message.STATUS_RECEIVED) {
747						return;
748					} else {
749						if (this.messageList.get(i).getStatus() == Message.STATUS_SEND_DISPLAYED) {
750							this.messageList.add(i + 1,
751									Message.createStatusMessage(conversation));
752							return;
753						}
754					}
755				}
756			}
757		}
758	}
759
760	protected void makeFingerprintWarning() {
761		if (conversation.smpRequested()) {
762			showSnackbar(R.string.smp_requested, R.string.verify, clickToVerify);
763		} else if (conversation.hasValidOtrSession() && (conversation.getOtrSession().getSessionStatus() == SessionStatus.ENCRYPTED)
764				&& (!conversation.isOtrFingerprintVerified())) {
765			showSnackbar(R.string.unknown_otr_fingerprint, R.string.verify, clickToVerify);
766		}
767	}
768
769	protected void showSnackbar(int message, int action,
770								OnClickListener clickListener) {
771		snackbar.setVisibility(View.VISIBLE);
772		snackbar.setOnClickListener(null);
773		snackbarMessage.setText(message);
774		snackbarMessage.setOnClickListener(null);
775		snackbarAction.setText(action);
776		snackbarAction.setOnClickListener(clickListener);
777	}
778
779	protected void hideSnackbar() {
780		snackbar.setVisibility(View.GONE);
781	}
782
783	protected void sendPlainTextMessage(Message message) {
784		ConversationActivity activity = (ConversationActivity) getActivity();
785		activity.xmppConnectionService.sendMessage(message);
786		messageSent();
787	}
788
789	protected void sendPgpMessage(final Message message) {
790		final ConversationActivity activity = (ConversationActivity) getActivity();
791		final XmppConnectionService xmppService = activity.xmppConnectionService;
792		final Contact contact = message.getConversation().getContact();
793		if (activity.hasPgp()) {
794			if (conversation.getMode() == Conversation.MODE_SINGLE) {
795				if (contact.getPgpKeyId() != 0) {
796					xmppService.getPgpEngine().hasKey(contact,
797							new UiCallback<Contact>() {
798
799								@Override
800								public void userInputRequried(PendingIntent pi,
801															  Contact contact) {
802									activity.runIntent(
803											pi,
804											ConversationActivity.REQUEST_ENCRYPT_MESSAGE);
805								}
806
807								@Override
808								public void success(Contact contact) {
809									messageSent();
810									activity.encryptTextMessage(message);
811								}
812
813								@Override
814								public void error(int error, Contact contact) {
815
816								}
817							});
818
819				} else {
820					showNoPGPKeyDialog(false,
821							new DialogInterface.OnClickListener() {
822
823								@Override
824								public void onClick(DialogInterface dialog,
825													int which) {
826									conversation
827											.setNextEncryption(Message.ENCRYPTION_NONE);
828									xmppService.databaseBackend
829											.updateConversation(conversation);
830									message.setEncryption(Message.ENCRYPTION_NONE);
831									xmppService.sendMessage(message);
832									messageSent();
833								}
834							});
835				}
836			} else {
837				if (conversation.getMucOptions().pgpKeysInUse()) {
838					if (!conversation.getMucOptions().everybodyHasKeys()) {
839						Toast warning = Toast
840								.makeText(getActivity(),
841										R.string.missing_public_keys,
842										Toast.LENGTH_LONG);
843						warning.setGravity(Gravity.CENTER_VERTICAL, 0, 0);
844						warning.show();
845					}
846					activity.encryptTextMessage(message);
847					messageSent();
848				} else {
849					showNoPGPKeyDialog(true,
850							new DialogInterface.OnClickListener() {
851
852								@Override
853								public void onClick(DialogInterface dialog,
854													int which) {
855									conversation
856											.setNextEncryption(Message.ENCRYPTION_NONE);
857									message.setEncryption(Message.ENCRYPTION_NONE);
858									xmppService.databaseBackend
859											.updateConversation(conversation);
860									xmppService.sendMessage(message);
861									messageSent();
862								}
863							});
864				}
865			}
866		} else {
867			activity.showInstallPgpDialog();
868		}
869	}
870
871	public void showNoPGPKeyDialog(boolean plural,
872								   DialogInterface.OnClickListener listener) {
873		AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
874		builder.setIconAttribute(android.R.attr.alertDialogIcon);
875		if (plural) {
876			builder.setTitle(getString(R.string.no_pgp_keys));
877			builder.setMessage(getText(R.string.contacts_have_no_pgp_keys));
878		} else {
879			builder.setTitle(getString(R.string.no_pgp_key));
880			builder.setMessage(getText(R.string.contact_has_no_pgp_key));
881		}
882		builder.setNegativeButton(getString(R.string.cancel), null);
883		builder.setPositiveButton(getString(R.string.send_unencrypted),
884				listener);
885		builder.create().show();
886	}
887
888	protected void sendOtrMessage(final Message message) {
889		final ConversationActivity activity = (ConversationActivity) getActivity();
890		final XmppConnectionService xmppService = activity.xmppConnectionService;
891		activity.selectPresence(message.getConversation(),
892				new OnPresenceSelected() {
893
894					@Override
895					public void onPresenceSelected() {
896						message.setCounterpart(conversation.getNextCounterpart());
897						xmppService.sendMessage(message);
898						messageSent();
899					}
900				});
901	}
902
903	public void appendText(String text) {
904		String previous = this.mEditMessage.getText().toString();
905		if (previous.length() != 0 && !previous.endsWith(" ")) {
906			text = " " + text;
907		}
908		this.mEditMessage.append(text);
909	}
910
911	public void clearInputField() {
912		this.mEditMessage.setText("");
913	}
914}