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