ConversationFragment.java

   1package eu.siacs.conversations.ui;
   2
   3import android.app.Activity;
   4import android.app.AlertDialog;
   5import android.app.Fragment;
   6import android.app.PendingIntent;
   7import android.content.ActivityNotFoundException;
   8import android.content.Context;
   9import android.content.DialogInterface;
  10import android.content.Intent;
  11import android.content.IntentSender.SendIntentException;
  12import android.os.Bundle;
  13import android.support.annotation.Nullable;
  14import android.text.InputType;
  15import android.view.ContextMenu;
  16import android.view.ContextMenu.ContextMenuInfo;
  17import android.view.Gravity;
  18import android.view.KeyEvent;
  19import android.view.LayoutInflater;
  20import android.view.MenuItem;
  21import android.view.View;
  22import android.view.View.OnClickListener;
  23import android.view.ViewGroup;
  24import android.view.inputmethod.EditorInfo;
  25import android.view.inputmethod.InputMethodManager;
  26import android.widget.AbsListView;
  27import android.widget.AbsListView.OnScrollListener;
  28import android.widget.AdapterView;
  29import android.widget.AdapterView.AdapterContextMenuInfo;
  30import android.widget.ImageButton;
  31import android.widget.ListView;
  32import android.widget.RelativeLayout;
  33import android.widget.TextView;
  34import android.widget.TextView.OnEditorActionListener;
  35import android.widget.Toast;
  36
  37import net.java.otr4j.session.SessionStatus;
  38
  39import java.util.ArrayList;
  40import java.util.Collections;
  41import java.util.List;
  42import java.util.UUID;
  43
  44import eu.siacs.conversations.Config;
  45import eu.siacs.conversations.R;
  46import eu.siacs.conversations.crypto.axolotl.AxolotlService;
  47import eu.siacs.conversations.entities.Account;
  48import eu.siacs.conversations.entities.Contact;
  49import eu.siacs.conversations.entities.Conversation;
  50import eu.siacs.conversations.entities.DownloadableFile;
  51import eu.siacs.conversations.entities.Message;
  52import eu.siacs.conversations.entities.MucOptions;
  53import eu.siacs.conversations.entities.Presence;
  54import eu.siacs.conversations.entities.Transferable;
  55import eu.siacs.conversations.entities.TransferablePlaceholder;
  56import eu.siacs.conversations.services.MessageArchiveService;
  57import eu.siacs.conversations.services.XmppConnectionService;
  58import eu.siacs.conversations.ui.XmppActivity.OnPresenceSelected;
  59import eu.siacs.conversations.ui.XmppActivity.OnValueEdited;
  60import eu.siacs.conversations.ui.adapter.MessageAdapter;
  61import eu.siacs.conversations.ui.adapter.MessageAdapter.OnContactPictureClicked;
  62import eu.siacs.conversations.ui.adapter.MessageAdapter.OnContactPictureLongClicked;
  63import eu.siacs.conversations.utils.GeoHelper;
  64import eu.siacs.conversations.utils.UIHelper;
  65import eu.siacs.conversations.xmpp.XmppConnection;
  66import eu.siacs.conversations.xmpp.chatstate.ChatState;
  67import eu.siacs.conversations.xmpp.jid.Jid;
  68
  69public class ConversationFragment extends Fragment implements EditMessage.KeyboardListener {
  70
  71	protected Conversation conversation;
  72	private OnClickListener leaveMuc = new OnClickListener() {
  73
  74		@Override
  75		public void onClick(View v) {
  76			activity.endConversation(conversation);
  77		}
  78	};
  79	private OnClickListener joinMuc = new OnClickListener() {
  80
  81		@Override
  82		public void onClick(View v) {
  83			activity.xmppConnectionService.joinMuc(conversation);
  84		}
  85	};
  86	private OnClickListener enterPassword = new OnClickListener() {
  87
  88		@Override
  89		public void onClick(View v) {
  90			MucOptions muc = conversation.getMucOptions();
  91			String password = muc.getPassword();
  92			if (password == null) {
  93				password = "";
  94			}
  95			activity.quickPasswordEdit(password, new OnValueEdited() {
  96
  97				@Override
  98				public void onValueEdited(String value) {
  99					activity.xmppConnectionService.providePasswordForMuc(
 100							conversation, value);
 101				}
 102			});
 103		}
 104	};
 105	protected ListView messagesView;
 106	final protected List<Message> messageList = new ArrayList<>();
 107	protected MessageAdapter messageListAdapter;
 108	private EditMessage mEditMessage;
 109	private ImageButton mSendButton;
 110	private RelativeLayout snackbar;
 111	private TextView snackbarMessage;
 112	private TextView snackbarAction;
 113	private boolean messagesLoaded = true;
 114	private Toast messageLoaderToast;
 115
 116	private OnScrollListener mOnScrollListener = new OnScrollListener() {
 117
 118		@Override
 119		public void onScrollStateChanged(AbsListView view, int scrollState) {
 120			// TODO Auto-generated method stub
 121
 122		}
 123
 124		private int getIndexOf(String uuid, List<Message> messages) {
 125			if (uuid == null) {
 126				return messages.size() - 1;
 127			}
 128			for(int i = 0; i < messages.size(); ++i) {
 129				if (uuid.equals(messages.get(i).getUuid())) {
 130					return i;
 131				} else {
 132					Message next = messages.get(i);
 133					while(next != null && next.wasMergedIntoPrevious()) {
 134						if (uuid.equals(next.getUuid())) {
 135							return i;
 136						}
 137						next = next.next();
 138					}
 139
 140				}
 141			}
 142			return 0;
 143		}
 144
 145		@Override
 146		public void onScroll(AbsListView view, int firstVisibleItem,
 147							 int visibleItemCount, int totalItemCount) {
 148			synchronized (ConversationFragment.this.messageList) {
 149				if (firstVisibleItem < 5 && messagesLoaded && messageList.size() > 0) {
 150					long timestamp;
 151					if (messageList.get(0).getType() == Message.TYPE_STATUS && messageList.size() >= 2) {
 152						timestamp = messageList.get(1).getTimeSent();
 153					} else {
 154						timestamp = messageList.get(0).getTimeSent();
 155					}
 156					messagesLoaded = false;
 157					activity.xmppConnectionService.loadMoreMessages(conversation, timestamp, new XmppConnectionService.OnMoreMessagesLoaded() {
 158						@Override
 159						public void onMoreMessagesLoaded(final int c, Conversation conversation) {
 160							if (ConversationFragment.this.conversation != conversation) {
 161								return;
 162							}
 163							activity.runOnUiThread(new Runnable() {
 164								@Override
 165								public void run() {
 166									final int oldPosition = messagesView.getFirstVisiblePosition();
 167									final Message message;
 168									if (oldPosition < messageList.size()) {
 169										message = messageList.get(oldPosition);
 170									}  else {
 171										message = null;
 172									}
 173									String uuid = message != null ? message.getUuid() : null;
 174									View v = messagesView.getChildAt(0);
 175									final int pxOffset = (v == null) ? 0 : v.getTop();
 176									ConversationFragment.this.conversation.populateWithMessages(ConversationFragment.this.messageList);
 177									updateStatusMessages();
 178									messageListAdapter.notifyDataSetChanged();
 179									int pos = getIndexOf(uuid,messageList);
 180									messagesView.setSelectionFromTop(pos, pxOffset);
 181									messagesLoaded = true;
 182									if (messageLoaderToast != null) {
 183										messageLoaderToast.cancel();
 184									}
 185								}
 186							});
 187						}
 188
 189						@Override
 190						public void informUser(final int resId) {
 191
 192							activity.runOnUiThread(new Runnable() {
 193								@Override
 194								public void run() {
 195									if (messageLoaderToast != null) {
 196										messageLoaderToast.cancel();
 197									}
 198									if (ConversationFragment.this.conversation != conversation) {
 199										return;
 200									}
 201									messageLoaderToast = Toast.makeText(activity, resId, Toast.LENGTH_LONG);
 202									messageLoaderToast.show();
 203								}
 204							});
 205
 206						}
 207					});
 208
 209				}
 210			}
 211		}
 212	};
 213	private final int KEYCHAIN_UNLOCK_NOT_REQUIRED = 0;
 214	private final int KEYCHAIN_UNLOCK_REQUIRED = 1;
 215	private final int KEYCHAIN_UNLOCK_PENDING = 2;
 216	private int keychainUnlock = KEYCHAIN_UNLOCK_NOT_REQUIRED;
 217	protected OnClickListener clickToDecryptListener = new OnClickListener() {
 218
 219		@Override
 220		public void onClick(View v) {
 221			if (keychainUnlock == KEYCHAIN_UNLOCK_REQUIRED
 222					&& activity.hasPgp() && !conversation.getAccount().getPgpDecryptionService().isRunning()) {
 223				keychainUnlock = KEYCHAIN_UNLOCK_PENDING;
 224				updateSnackBar(conversation);
 225				Message message = getLastPgpDecryptableMessage();
 226				if (message != null) {
 227					activity.xmppConnectionService.getPgpEngine().decrypt(message, new UiCallback<Message>() {
 228						@Override
 229						public void success(Message object) {
 230							conversation.getAccount().getPgpDecryptionService().onKeychainUnlocked();
 231							keychainUnlock = KEYCHAIN_UNLOCK_NOT_REQUIRED;
 232						}
 233
 234						@Override
 235						public void error(int errorCode, Message object) {
 236							keychainUnlock = KEYCHAIN_UNLOCK_NOT_REQUIRED;
 237						}
 238
 239						@Override
 240						public void userInputRequried(PendingIntent pi, Message object) {
 241							try {
 242								activity.startIntentSenderForResult(pi.getIntentSender(),
 243										ConversationActivity.REQUEST_DECRYPT_PGP, null, 0, 0, 0);
 244							} catch (SendIntentException e) {
 245								keychainUnlock = KEYCHAIN_UNLOCK_NOT_REQUIRED;
 246								updatePgpMessages();
 247							}
 248						}
 249					});
 250				}
 251			} else {
 252				keychainUnlock = KEYCHAIN_UNLOCK_NOT_REQUIRED;
 253				updatePgpMessages();
 254			}
 255		}
 256	};
 257	protected OnClickListener clickToVerify = new OnClickListener() {
 258
 259		@Override
 260		public void onClick(View v) {
 261			activity.verifyOtrSessionDialog(conversation, v);
 262		}
 263	};
 264	private OnEditorActionListener mEditorActionListener = new OnEditorActionListener() {
 265
 266		@Override
 267		public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
 268			if (actionId == EditorInfo.IME_ACTION_SEND) {
 269				InputMethodManager imm = (InputMethodManager) v.getContext()
 270						.getSystemService(Context.INPUT_METHOD_SERVICE);
 271				if (imm.isFullscreenMode()) {
 272					imm.hideSoftInputFromWindow(v.getWindowToken(), 0);
 273				}
 274				sendMessage();
 275				return true;
 276			} else {
 277				return false;
 278			}
 279		}
 280	};
 281	private OnClickListener mSendButtonListener = new OnClickListener() {
 282
 283		@Override
 284		public void onClick(View v) {
 285			Object tag = v.getTag();
 286			if (tag instanceof SendButtonAction) {
 287				SendButtonAction action = (SendButtonAction) tag;
 288				switch (action) {
 289					case TAKE_PHOTO:
 290						activity.attachFile(ConversationActivity.ATTACHMENT_CHOICE_TAKE_PHOTO);
 291						break;
 292					case SEND_LOCATION:
 293						activity.attachFile(ConversationActivity.ATTACHMENT_CHOICE_LOCATION);
 294						break;
 295					case RECORD_VOICE:
 296						activity.attachFile(ConversationActivity.ATTACHMENT_CHOICE_RECORD_VOICE);
 297						break;
 298					case CHOOSE_PICTURE:
 299						activity.attachFile(ConversationActivity.ATTACHMENT_CHOICE_CHOOSE_IMAGE);
 300						break;
 301					case CANCEL:
 302						if (conversation != null) {
 303							if (conversation.getCorrectingMessage() != null) {
 304								conversation.setCorrectingMessage(null);
 305								mEditMessage.getEditableText().clear();
 306							}
 307							if (conversation.getMode() == Conversation.MODE_MULTI) {
 308								conversation.setNextCounterpart(null);
 309							}
 310							updateChatMsgHint();
 311							updateSendButton();
 312						}
 313						break;
 314					default:
 315						sendMessage();
 316				}
 317			} else {
 318				sendMessage();
 319			}
 320		}
 321	};
 322	private OnClickListener clickToMuc = new OnClickListener() {
 323
 324		@Override
 325		public void onClick(View v) {
 326			Intent intent = new Intent(getActivity(), ConferenceDetailsActivity.class);
 327			intent.setAction(ConferenceDetailsActivity.ACTION_VIEW_MUC);
 328			intent.putExtra("uuid", conversation.getUuid());
 329			startActivity(intent);
 330		}
 331	};
 332	private ConversationActivity activity;
 333	private Message selectedMessage;
 334
 335	public void setMessagesLoaded() {
 336		this.messagesLoaded = true;
 337	}
 338
 339	private void sendMessage() {
 340		final String body = mEditMessage.getText().toString();
 341		if (body.length() == 0 || this.conversation == null) {
 342			return;
 343		}
 344		final Message message;
 345		if (conversation.getCorrectingMessage() == null) {
 346			message = new Message(conversation, body, conversation.getNextEncryption());
 347			if (conversation.getMode() == Conversation.MODE_MULTI) {
 348				if (conversation.getNextCounterpart() != null) {
 349					message.setCounterpart(conversation.getNextCounterpart());
 350					message.setType(Message.TYPE_PRIVATE);
 351				}
 352			}
 353		} else {
 354			message = conversation.getCorrectingMessage();
 355			message.setBody(body);
 356			message.setEdited(message.getUuid());
 357			message.setUuid(UUID.randomUUID().toString());
 358			conversation.setCorrectingMessage(null);
 359		}
 360		switch (conversation.getNextEncryption()) {
 361			case Message.ENCRYPTION_OTR:
 362				sendOtrMessage(message);
 363				break;
 364			case Message.ENCRYPTION_PGP:
 365				sendPgpMessage(message);
 366				break;
 367			case Message.ENCRYPTION_AXOLOTL:
 368				if(!activity.trustKeysIfNeeded(ConversationActivity.REQUEST_TRUST_KEYS_TEXT)) {
 369					sendAxolotlMessage(message);
 370				}
 371				break;
 372			default:
 373				sendPlainTextMessage(message);
 374		}
 375	}
 376
 377	public void updateChatMsgHint() {
 378		final boolean multi = conversation.getMode() == Conversation.MODE_MULTI;
 379		if (conversation.getCorrectingMessage() != null) {
 380			this.mEditMessage.setHint(R.string.send_corrected_message);
 381		} else if (multi && conversation.getNextCounterpart() != null) {
 382			this.mEditMessage.setHint(getString(
 383					R.string.send_private_message_to,
 384					conversation.getNextCounterpart().getResourcepart()));
 385		} else if (multi && !conversation.getMucOptions().participating()) {
 386			this.mEditMessage.setHint(R.string.you_are_not_participating);
 387		} else {
 388			switch (conversation.getNextEncryption()) {
 389				case Message.ENCRYPTION_NONE:
 390					mEditMessage
 391							.setHint(getString(R.string.send_unencrypted_message));
 392					break;
 393				case Message.ENCRYPTION_OTR:
 394					mEditMessage.setHint(getString(R.string.send_otr_message));
 395					break;
 396				case Message.ENCRYPTION_AXOLOTL:
 397					AxolotlService axolotlService = conversation.getAccount().getAxolotlService();
 398					if (axolotlService != null && axolotlService.trustedSessionVerified(conversation)) {
 399						mEditMessage.setHint(getString(R.string.send_omemo_x509_message));
 400					} else {
 401						mEditMessage.setHint(getString(R.string.send_omemo_message));
 402					}
 403					break;
 404				case Message.ENCRYPTION_PGP:
 405					mEditMessage.setHint(getString(R.string.send_pgp_message));
 406					break;
 407				default:
 408					break;
 409			}
 410			getActivity().invalidateOptionsMenu();
 411		}
 412	}
 413
 414	public void setupIme() {
 415		if (activity == null) {
 416			return;
 417		} else if (activity.usingEnterKey() && activity.enterIsSend()) {
 418			mEditMessage.setInputType(mEditMessage.getInputType() & (~InputType.TYPE_TEXT_FLAG_MULTI_LINE));
 419			mEditMessage.setInputType(mEditMessage.getInputType() & (~InputType.TYPE_TEXT_VARIATION_SHORT_MESSAGE));
 420		} else if (activity.usingEnterKey()) {
 421			mEditMessage.setInputType(mEditMessage.getInputType() | InputType.TYPE_TEXT_FLAG_MULTI_LINE);
 422			mEditMessage.setInputType(mEditMessage.getInputType() & (~InputType.TYPE_TEXT_VARIATION_SHORT_MESSAGE));
 423		} else {
 424			mEditMessage.setInputType(mEditMessage.getInputType() | InputType.TYPE_TEXT_FLAG_MULTI_LINE);
 425			mEditMessage.setInputType(mEditMessage.getInputType() | InputType.TYPE_TEXT_VARIATION_SHORT_MESSAGE);
 426		}
 427	}
 428
 429	@Override
 430	public View onCreateView(final LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
 431		final View view = inflater.inflate(R.layout.fragment_conversation, container, false);
 432		view.setOnClickListener(null);
 433		mEditMessage = (EditMessage) view.findViewById(R.id.textinput);
 434		mEditMessage.setOnClickListener(new OnClickListener() {
 435
 436			@Override
 437			public void onClick(View v) {
 438				if (activity != null) {
 439					activity.hideConversationsOverview();
 440				}
 441			}
 442		});
 443		mEditMessage.setOnEditorActionListener(mEditorActionListener);
 444
 445		mSendButton = (ImageButton) view.findViewById(R.id.textSendButton);
 446		mSendButton.setOnClickListener(this.mSendButtonListener);
 447
 448		snackbar = (RelativeLayout) view.findViewById(R.id.snackbar);
 449		snackbarMessage = (TextView) view.findViewById(R.id.snackbar_message);
 450		snackbarAction = (TextView) view.findViewById(R.id.snackbar_action);
 451
 452		messagesView = (ListView) view.findViewById(R.id.messages_view);
 453		messagesView.setOnScrollListener(mOnScrollListener);
 454		messagesView.setTranscriptMode(ListView.TRANSCRIPT_MODE_NORMAL);
 455		messageListAdapter = new MessageAdapter((ConversationActivity) getActivity(), this.messageList);
 456		messageListAdapter.setOnContactPictureClicked(new OnContactPictureClicked() {
 457
 458			@Override
 459			public void onContactPictureClicked(Message message) {
 460				if (message.getStatus() <= Message.STATUS_RECEIVED) {
 461					if (message.getConversation().getMode() == Conversation.MODE_MULTI) {
 462						if (message.getCounterpart() != null) {
 463							String user = message.getCounterpart().isBareJid() ? message.getCounterpart().toString() : message.getCounterpart().getResourcepart();
 464							if (!message.getConversation().getMucOptions().isUserInRoom(user)) {
 465								Toast.makeText(activity,activity.getString(R.string.user_has_left_conference,user),Toast.LENGTH_SHORT).show();
 466							}
 467							highlightInConference(user);
 468						}
 469					} else {
 470						activity.switchToContactDetails(message.getContact(), message.getAxolotlFingerprint());
 471					}
 472				} else {
 473					Account account = message.getConversation().getAccount();
 474					Intent intent = new Intent(activity, EditAccountActivity.class);
 475					intent.putExtra("jid", account.getJid().toBareJid().toString());
 476					intent.putExtra("fingerprint", message.getAxolotlFingerprint());
 477					startActivity(intent);
 478				}
 479			}
 480		});
 481		messageListAdapter
 482				.setOnContactPictureLongClicked(new OnContactPictureLongClicked() {
 483
 484					@Override
 485					public void onContactPictureLongClicked(Message message) {
 486						if (message.getStatus() <= Message.STATUS_RECEIVED) {
 487							if (message.getConversation().getMode() == Conversation.MODE_MULTI) {
 488								if (message.getCounterpart() != null) {
 489									String user = message.getCounterpart().getResourcepart();
 490									if (user != null) {
 491										if (message.getConversation().getMucOptions().isUserInRoom(user)) {
 492											privateMessageWith(message.getCounterpart());
 493										} else {
 494											Toast.makeText(activity, activity.getString(R.string.user_has_left_conference, user), Toast.LENGTH_SHORT).show();
 495										}
 496									}
 497								}
 498							}
 499						} else {
 500							activity.showQrCode();
 501						}
 502					}
 503				});
 504		messagesView.setAdapter(messageListAdapter);
 505
 506		registerForContextMenu(messagesView);
 507
 508		return view;
 509	}
 510
 511	@Override
 512	public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
 513		synchronized (this.messageList) {
 514			super.onCreateContextMenu(menu, v, menuInfo);
 515			AdapterView.AdapterContextMenuInfo acmi = (AdapterContextMenuInfo) menuInfo;
 516			this.selectedMessage = this.messageList.get(acmi.position);
 517			populateContextMenu(menu);
 518		}
 519	}
 520
 521	private void populateContextMenu(ContextMenu menu) {
 522		final Message m = this.selectedMessage;
 523		Message relevantForCorrection = m;
 524		while(relevantForCorrection.mergeable(relevantForCorrection.next())) {
 525			relevantForCorrection = relevantForCorrection.next();
 526		}
 527		if (m.getType() != Message.TYPE_STATUS) {
 528			activity.getMenuInflater().inflate(R.menu.message_context, menu);
 529			menu.setHeaderTitle(R.string.message_options);
 530			MenuItem copyText = menu.findItem(R.id.copy_text);
 531			MenuItem retryDecryption = menu.findItem(R.id.retry_decryption);
 532			MenuItem correctMessage = menu.findItem(R.id.correct_message);
 533			MenuItem shareWith = menu.findItem(R.id.share_with);
 534			MenuItem sendAgain = menu.findItem(R.id.send_again);
 535			MenuItem copyUrl = menu.findItem(R.id.copy_url);
 536			MenuItem downloadFile = menu.findItem(R.id.download_file);
 537			MenuItem cancelTransmission = menu.findItem(R.id.cancel_transmission);
 538			if ((m.getType() == Message.TYPE_TEXT || m.getType() == Message.TYPE_PRIVATE)
 539					&& m.getTransferable() == null
 540					&& !GeoHelper.isGeoUri(m.getBody())
 541					&& m.treatAsDownloadable() != Message.Decision.MUST) {
 542				copyText.setVisible(true);
 543			}
 544			if (m.getEncryption() == Message.ENCRYPTION_DECRYPTION_FAILED) {
 545				retryDecryption.setVisible(true);
 546			}
 547			if (relevantForCorrection.getType() == Message.TYPE_TEXT
 548					&& relevantForCorrection.isLastCorrectableMessage()) {
 549				correctMessage.setVisible(true);
 550			}
 551			if ((m.getType() != Message.TYPE_TEXT
 552					&& m.getType() != Message.TYPE_PRIVATE
 553					&& m.getTransferable() == null)
 554					|| (GeoHelper.isGeoUri(m.getBody()))) {
 555				shareWith.setVisible(true);
 556			}
 557			if (m.getStatus() == Message.STATUS_SEND_FAILED) {
 558				sendAgain.setVisible(true);
 559			}
 560			if (m.hasFileOnRemoteHost()
 561					|| GeoHelper.isGeoUri(m.getBody())
 562					|| m.treatAsDownloadable() == Message.Decision.MUST) {
 563				copyUrl.setVisible(true);
 564			}
 565			if ((m.getType() == Message.TYPE_TEXT && m.getTransferable() == null && m.treatAsDownloadable() != Message.Decision.NEVER)
 566					|| (m.isFileOrImage() && m.getTransferable() instanceof TransferablePlaceholder && m.hasFileOnRemoteHost())){
 567				downloadFile.setVisible(true);
 568				downloadFile.setTitle(activity.getString(R.string.download_x_file,UIHelper.getFileDescriptionString(activity, m)));
 569			}
 570			if ((m.getTransferable() != null && !(m.getTransferable() instanceof TransferablePlaceholder))
 571					|| (m.isFileOrImage() && (m.getStatus() == Message.STATUS_WAITING
 572					|| m.getStatus() == Message.STATUS_OFFERED))) {
 573				cancelTransmission.setVisible(true);
 574			}
 575		}
 576	}
 577
 578	@Override
 579	public boolean onContextItemSelected(MenuItem item) {
 580		switch (item.getItemId()) {
 581			case R.id.share_with:
 582				shareWith(selectedMessage);
 583				return true;
 584			case R.id.copy_text:
 585				copyText(selectedMessage);
 586				return true;
 587			case R.id.correct_message:
 588				correctMessage(selectedMessage);
 589				return true;
 590			case R.id.send_again:
 591				resendMessage(selectedMessage);
 592				return true;
 593			case R.id.copy_url:
 594				copyUrl(selectedMessage);
 595				return true;
 596			case R.id.download_file:
 597				downloadFile(selectedMessage);
 598				return true;
 599			case R.id.cancel_transmission:
 600				cancelTransmission(selectedMessage);
 601				return true;
 602			case R.id.retry_decryption:
 603				retryDecryption(selectedMessage);
 604				return true;
 605			default:
 606				return super.onContextItemSelected(item);
 607		}
 608	}
 609
 610	private void shareWith(Message message) {
 611		Intent shareIntent = new Intent();
 612		shareIntent.setAction(Intent.ACTION_SEND);
 613		if (GeoHelper.isGeoUri(message.getBody())) {
 614			shareIntent.putExtra(Intent.EXTRA_TEXT, message.getBody());
 615			shareIntent.setType("text/plain");
 616		} else {
 617			shareIntent.putExtra(Intent.EXTRA_STREAM,
 618					activity.xmppConnectionService.getFileBackend()
 619							.getJingleFileUri(message));
 620			shareIntent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
 621			String mime = message.getMimeType();
 622			if (mime == null) {
 623				mime = "*/*";
 624			}
 625			shareIntent.setType(mime);
 626		}
 627		try {
 628			activity.startActivity(Intent.createChooser(shareIntent, getText(R.string.share_with)));
 629		} catch (ActivityNotFoundException e) {
 630			//This should happen only on faulty androids because normally chooser is always available
 631			Toast.makeText(activity,R.string.no_application_found_to_open_file,Toast.LENGTH_SHORT).show();
 632		}
 633	}
 634
 635	private void copyText(Message message) {
 636		if (activity.copyTextToClipboard(message.getMergedBody(),
 637				R.string.message_text)) {
 638			Toast.makeText(activity, R.string.message_copied_to_clipboard,
 639					Toast.LENGTH_SHORT).show();
 640		}
 641	}
 642
 643	private void resendMessage(Message message) {
 644		if (message.getType() == Message.TYPE_FILE || message.getType() == Message.TYPE_IMAGE) {
 645			DownloadableFile file = activity.xmppConnectionService.getFileBackend().getFile(message);
 646			if (!file.exists()) {
 647				Toast.makeText(activity, R.string.file_deleted, Toast.LENGTH_SHORT).show();
 648				message.setTransferable(new TransferablePlaceholder(Transferable.STATUS_DELETED));
 649				return;
 650			}
 651		}
 652		activity.xmppConnectionService.resendFailedMessages(message);
 653	}
 654
 655	private void copyUrl(Message message) {
 656		final String url;
 657		final int resId;
 658		if (GeoHelper.isGeoUri(message.getBody())) {
 659			resId = R.string.location;
 660			url = message.getBody();
 661		} else if (message.hasFileOnRemoteHost()) {
 662			resId = R.string.file_url;
 663			url = message.getFileParams().url.toString();
 664		} else {
 665			url = message.getBody().trim();
 666			resId = R.string.file_url;
 667		}
 668		if (activity.copyTextToClipboard(url, resId)) {
 669			Toast.makeText(activity, R.string.url_copied_to_clipboard,
 670					Toast.LENGTH_SHORT).show();
 671		}
 672	}
 673
 674	private void downloadFile(Message message) {
 675		activity.xmppConnectionService.getHttpConnectionManager()
 676				.createNewDownloadConnection(message,true);
 677	}
 678
 679	private void cancelTransmission(Message message) {
 680		Transferable transferable = message.getTransferable();
 681		if (transferable != null) {
 682			transferable.cancel();
 683		} else {
 684			activity.xmppConnectionService.markMessage(message, Message.STATUS_SEND_FAILED);
 685		}
 686	}
 687
 688	private void retryDecryption(Message message) {
 689		message.setEncryption(Message.ENCRYPTION_PGP);
 690		activity.xmppConnectionService.updateConversationUi();
 691		conversation.getAccount().getPgpDecryptionService().add(message);
 692	}
 693
 694	protected void privateMessageWith(final Jid counterpart) {
 695		this.mEditMessage.setText("");
 696		this.conversation.setNextCounterpart(counterpart);
 697		updateChatMsgHint();
 698		updateSendButton();
 699	}
 700
 701	private void correctMessage(Message message) {
 702		while(message.mergeable(message.next())) {
 703			message = message.next();
 704		}
 705		this.conversation.setCorrectingMessage(message);
 706		this.mEditMessage.getEditableText().clear();
 707		this.mEditMessage.getEditableText().append(message.getBody());
 708
 709	}
 710
 711	protected void highlightInConference(String nick) {
 712		String oldString = mEditMessage.getText().toString().trim();
 713		if (oldString.isEmpty() || mEditMessage.getSelectionStart() == 0) {
 714			mEditMessage.getText().insert(0, nick + ": ");
 715		} else {
 716			if (mEditMessage.getText().charAt(
 717					mEditMessage.getSelectionStart() - 1) != ' ') {
 718				nick = " " + nick;
 719			}
 720			mEditMessage.getText().insert(mEditMessage.getSelectionStart(),
 721					nick + " ");
 722		}
 723	}
 724
 725	@Override
 726	public void onStop() {
 727		super.onStop();
 728		if (this.conversation != null) {
 729			final String msg = mEditMessage.getText().toString();
 730			this.conversation.setNextMessage(msg);
 731			updateChatState(this.conversation, msg);
 732		}
 733	}
 734
 735	private void updateChatState(final Conversation conversation, final String msg) {
 736		ChatState state = msg.length() == 0 ? Config.DEFAULT_CHATSTATE : ChatState.PAUSED;
 737		Account.State status = conversation.getAccount().getStatus();
 738		if (status == Account.State.ONLINE && conversation.setOutgoingChatState(state)) {
 739			activity.xmppConnectionService.sendChatState(conversation);
 740		}
 741	}
 742
 743	public void reInit(Conversation conversation) {
 744		if (conversation == null) {
 745			return;
 746		}
 747		this.activity = (ConversationActivity) getActivity();
 748		setupIme();
 749		if (this.conversation != null) {
 750			final String msg = mEditMessage.getText().toString();
 751			this.conversation.setNextMessage(msg);
 752			if (this.conversation != conversation) {
 753				updateChatState(this.conversation, msg);
 754			}
 755			this.conversation.trim();
 756		}
 757
 758		this.keychainUnlock = KEYCHAIN_UNLOCK_NOT_REQUIRED;
 759		this.conversation = conversation;
 760		boolean canWrite = this.conversation.getMode() == Conversation.MODE_SINGLE || this.conversation.getMucOptions().participating();
 761		this.mEditMessage.setEnabled(canWrite);
 762		this.mSendButton.setEnabled(canWrite);
 763		this.mEditMessage.setKeyboardListener(null);
 764		this.mEditMessage.setText("");
 765		this.mEditMessage.append(this.conversation.getNextMessage());
 766		this.mEditMessage.setKeyboardListener(this);
 767		messageListAdapter.updatePreferences();
 768		this.messagesView.setAdapter(messageListAdapter);
 769		updateMessages();
 770		this.messagesLoaded = true;
 771		int size = this.messageList.size();
 772		if (size > 0) {
 773			messagesView.setSelection(size - 1);
 774		}
 775	}
 776
 777	private OnClickListener mUnblockClickListener = new OnClickListener() {
 778		@Override
 779		public void onClick(final View v) {
 780			v.post(new Runnable() {
 781				@Override
 782				public void run() {
 783					v.setVisibility(View.INVISIBLE);
 784				}
 785			});
 786			if (conversation.isDomainBlocked()) {
 787				BlockContactDialog.show(activity, activity.xmppConnectionService, conversation);
 788			} else {
 789				activity.unblockConversation(conversation);
 790			}
 791		}
 792	};
 793
 794	private OnClickListener mAddBackClickListener = new OnClickListener() {
 795
 796		@Override
 797		public void onClick(View v) {
 798			final Contact contact = conversation == null ? null : conversation.getContact();
 799			if (contact != null) {
 800				activity.xmppConnectionService.createContact(contact);
 801				activity.switchToContactDetails(contact);
 802			}
 803		}
 804	};
 805
 806	private OnClickListener mAnswerSmpClickListener = new OnClickListener() {
 807		@Override
 808		public void onClick(View view) {
 809			Intent intent = new Intent(activity, VerifyOTRActivity.class);
 810			intent.setAction(VerifyOTRActivity.ACTION_VERIFY_CONTACT);
 811			intent.putExtra("contact", conversation.getContact().getJid().toBareJid().toString());
 812			intent.putExtra(VerifyOTRActivity.EXTRA_ACCOUNT, conversation.getAccount().getJid().toBareJid().toString());
 813			intent.putExtra("mode", VerifyOTRActivity.MODE_ANSWER_QUESTION);
 814			startActivity(intent);
 815		}
 816	};
 817
 818	private void updateSnackBar(final Conversation conversation) {
 819		final Account account = conversation.getAccount();
 820		final Contact contact = conversation.getContact();
 821		final int mode = conversation.getMode();
 822		if (conversation.isBlocked()) {
 823			showSnackbar(R.string.contact_blocked, R.string.unblock, this.mUnblockClickListener);
 824		} else if (!contact.showInRoster() && contact.getOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST)) {
 825			showSnackbar(R.string.contact_added_you, R.string.add_back, this.mAddBackClickListener);
 826		} else if (mode == Conversation.MODE_MULTI
 827				&& !conversation.getMucOptions().online()
 828				&& account.getStatus() == Account.State.ONLINE) {
 829			switch (conversation.getMucOptions().getError()) {
 830				case NICK_IN_USE:
 831					showSnackbar(R.string.nick_in_use, R.string.edit, clickToMuc);
 832					break;
 833				case NO_RESPONSE:
 834					showSnackbar(R.string.joining_conference, 0, null);
 835					break;
 836				case PASSWORD_REQUIRED:
 837					showSnackbar(R.string.conference_requires_password, R.string.enter_password, enterPassword);
 838					break;
 839				case BANNED:
 840					showSnackbar(R.string.conference_banned, R.string.leave, leaveMuc);
 841					break;
 842				case MEMBERS_ONLY:
 843					showSnackbar(R.string.conference_members_only, R.string.leave, leaveMuc);
 844					break;
 845				case KICKED:
 846					showSnackbar(R.string.conference_kicked, R.string.join, joinMuc);
 847					break;
 848				case UNKNOWN:
 849					showSnackbar(R.string.conference_unknown_error, R.string.join, joinMuc);
 850					break;
 851				case SHUTDOWN:
 852					showSnackbar(R.string.conference_shutdown, R.string.join, joinMuc);
 853					break;
 854				default:
 855					break;
 856			}
 857		} else if (keychainUnlock == KEYCHAIN_UNLOCK_REQUIRED) {
 858			showSnackbar(R.string.openpgp_messages_found, R.string.decrypt, clickToDecryptListener);
 859		} else if (mode == Conversation.MODE_SINGLE
 860				&& conversation.smpRequested()) {
 861			showSnackbar(R.string.smp_requested, R.string.verify, this.mAnswerSmpClickListener);
 862		} else if (mode == Conversation.MODE_SINGLE
 863				&& conversation.hasValidOtrSession()
 864				&& (conversation.getOtrSession().getSessionStatus() == SessionStatus.ENCRYPTED)
 865				&& (!conversation.isOtrFingerprintVerified())) {
 866			showSnackbar(R.string.unknown_otr_fingerprint, R.string.verify, clickToVerify);
 867		} else {
 868			hideSnackbar();
 869		}
 870	}
 871
 872	public void updateMessages() {
 873		synchronized (this.messageList) {
 874			if (getView() == null) {
 875				return;
 876			}
 877			final ConversationActivity activity = (ConversationActivity) getActivity();
 878			if (this.conversation != null) {
 879				conversation.populateWithMessages(ConversationFragment.this.messageList);
 880				updatePgpMessages();
 881				updateSnackBar(conversation);
 882				updateStatusMessages();
 883				this.messageListAdapter.notifyDataSetChanged();
 884				updateChatMsgHint();
 885				if (!activity.isConversationsOverviewVisable() || !activity.isConversationsOverviewHideable()) {
 886					activity.sendReadMarkerIfNecessary(conversation);
 887				}
 888				this.updateSendButton();
 889			}
 890		}
 891	}
 892
 893	public void updatePgpMessages() {
 894		if (keychainUnlock != KEYCHAIN_UNLOCK_PENDING) {
 895			if (getLastPgpDecryptableMessage() != null
 896					&& !conversation.getAccount().getPgpDecryptionService().isRunning()) {
 897				keychainUnlock = KEYCHAIN_UNLOCK_REQUIRED;
 898			} else {
 899				keychainUnlock = KEYCHAIN_UNLOCK_NOT_REQUIRED;
 900			}
 901		}
 902	}
 903
 904	@Nullable
 905	private Message getLastPgpDecryptableMessage() {
 906		for (final Message message : this.messageList) {
 907			if (message.getEncryption() == Message.ENCRYPTION_PGP
 908					&& (message.getStatus() == Message.STATUS_RECEIVED || message.getStatus() >= Message.STATUS_SEND)
 909					&& message.getTransferable() == null) {
 910				return message;
 911			}
 912		}
 913		return null;
 914	}
 915
 916	private void messageSent() {
 917		int size = this.messageList.size();
 918		messagesView.setSelection(size - 1);
 919		mEditMessage.setText("");
 920		updateChatMsgHint();
 921	}
 922
 923	public void setFocusOnInputField() {
 924		mEditMessage.requestFocus();
 925	}
 926
 927	enum SendButtonAction {TEXT, TAKE_PHOTO, SEND_LOCATION, RECORD_VOICE, CANCEL, CHOOSE_PICTURE}
 928
 929	private int getSendButtonImageResource(SendButtonAction action, Presence.Status status) {
 930		switch (action) {
 931			case TEXT:
 932				switch (status) {
 933					case CHAT:
 934					case ONLINE:
 935						return R.drawable.ic_send_text_online;
 936					case AWAY:
 937						return R.drawable.ic_send_text_away;
 938					case XA:
 939					case DND:
 940						return R.drawable.ic_send_text_dnd;
 941					default:
 942						return R.drawable.ic_send_text_offline;
 943				}
 944			case TAKE_PHOTO:
 945				switch (status) {
 946					case CHAT:
 947					case ONLINE:
 948						return R.drawable.ic_send_photo_online;
 949					case AWAY:
 950						return R.drawable.ic_send_photo_away;
 951					case XA:
 952					case DND:
 953						return R.drawable.ic_send_photo_dnd;
 954					default:
 955						return R.drawable.ic_send_photo_offline;
 956				}
 957			case RECORD_VOICE:
 958				switch (status) {
 959					case CHAT:
 960					case ONLINE:
 961						return R.drawable.ic_send_voice_online;
 962					case AWAY:
 963						return R.drawable.ic_send_voice_away;
 964					case XA:
 965					case DND:
 966						return R.drawable.ic_send_voice_dnd;
 967					default:
 968						return R.drawable.ic_send_voice_offline;
 969				}
 970			case SEND_LOCATION:
 971				switch (status) {
 972					case CHAT:
 973					case ONLINE:
 974						return R.drawable.ic_send_location_online;
 975					case AWAY:
 976						return R.drawable.ic_send_location_away;
 977					case XA:
 978					case DND:
 979						return R.drawable.ic_send_location_dnd;
 980					default:
 981						return R.drawable.ic_send_location_offline;
 982				}
 983			case CANCEL:
 984				switch (status) {
 985					case CHAT:
 986					case ONLINE:
 987						return R.drawable.ic_send_cancel_online;
 988					case AWAY:
 989						return R.drawable.ic_send_cancel_away;
 990					case XA:
 991					case DND:
 992						return R.drawable.ic_send_cancel_dnd;
 993					default:
 994						return R.drawable.ic_send_cancel_offline;
 995				}
 996			case CHOOSE_PICTURE:
 997				switch (status) {
 998					case CHAT:
 999					case ONLINE:
1000						return R.drawable.ic_send_picture_online;
1001					case AWAY:
1002						return R.drawable.ic_send_picture_away;
1003					case XA:
1004					case DND:
1005						return R.drawable.ic_send_picture_dnd;
1006					default:
1007						return R.drawable.ic_send_picture_offline;
1008				}
1009		}
1010		return R.drawable.ic_send_text_offline;
1011	}
1012
1013	public void updateSendButton() {
1014		final Conversation c = this.conversation;
1015		final SendButtonAction action;
1016		final Presence.Status status;
1017		final String text = this.mEditMessage == null ? "" : this.mEditMessage.getText().toString();
1018		final boolean empty = text.length() == 0;
1019		final boolean conference = c.getMode() == Conversation.MODE_MULTI;
1020		if (c.getCorrectingMessage() != null && (empty || text.equals(c.getCorrectingMessage().getBody()))) {
1021			action = SendButtonAction.CANCEL;
1022		} else if (conference && !c.getAccount().httpUploadAvailable()) {
1023			if (empty && c.getNextCounterpart() != null) {
1024				action = SendButtonAction.CANCEL;
1025			} else {
1026				action = SendButtonAction.TEXT;
1027			}
1028		} else {
1029			if (empty) {
1030				if (conference && c.getNextCounterpart() != null) {
1031					action = SendButtonAction.CANCEL;
1032				} else {
1033					String setting = activity.getPreferences().getString("quick_action", "recent");
1034					if (!setting.equals("none") && UIHelper.receivedLocationQuestion(conversation.getLatestMessage())) {
1035						setting = "location";
1036					} else if (setting.equals("recent")) {
1037						setting = activity.getPreferences().getString("recently_used_quick_action", "text");
1038					}
1039					switch (setting) {
1040						case "photo":
1041							action = SendButtonAction.TAKE_PHOTO;
1042							break;
1043						case "location":
1044							action = SendButtonAction.SEND_LOCATION;
1045							break;
1046						case "voice":
1047							action = SendButtonAction.RECORD_VOICE;
1048							break;
1049						case "picture":
1050							action = SendButtonAction.CHOOSE_PICTURE;
1051							break;
1052						default:
1053							action = SendButtonAction.TEXT;
1054							break;
1055					}
1056				}
1057			} else {
1058				action = SendButtonAction.TEXT;
1059			}
1060		}
1061		if (activity.useSendButtonToIndicateStatus() && c != null
1062				&& c.getAccount().getStatus() == Account.State.ONLINE) {
1063			if (c.getMode() == Conversation.MODE_SINGLE) {
1064				status = c.getContact().getMostAvailableStatus();
1065			} else {
1066				status = c.getMucOptions().online() ? Presence.Status.ONLINE : Presence.Status.OFFLINE;
1067			}
1068		} else {
1069			status = Presence.Status.OFFLINE;
1070		}
1071		this.mSendButton.setTag(action);
1072		this.mSendButton.setImageResource(getSendButtonImageResource(action, status));
1073	}
1074
1075	protected void updateStatusMessages() {
1076		synchronized (this.messageList) {
1077			if (showLoadMoreMessages(conversation)) {
1078				this.messageList.add(0, Message.createLoadMoreMessage(conversation));
1079			}
1080			if (conversation.getMode() == Conversation.MODE_SINGLE) {
1081				ChatState state = conversation.getIncomingChatState();
1082				if (state == ChatState.COMPOSING) {
1083					this.messageList.add(Message.createStatusMessage(conversation, getString(R.string.contact_is_typing, conversation.getName())));
1084				} else if (state == ChatState.PAUSED) {
1085					this.messageList.add(Message.createStatusMessage(conversation, getString(R.string.contact_has_stopped_typing, conversation.getName())));
1086				} else {
1087					for (int i = this.messageList.size() - 1; i >= 0; --i) {
1088						if (this.messageList.get(i).getStatus() == Message.STATUS_RECEIVED) {
1089							return;
1090						} else {
1091							if (this.messageList.get(i).getStatus() == Message.STATUS_SEND_DISPLAYED) {
1092								this.messageList.add(i + 1,
1093										Message.createStatusMessage(conversation, getString(R.string.contact_has_read_up_to_this_point, conversation.getName())));
1094								return;
1095							}
1096						}
1097					}
1098				}
1099			}
1100		}
1101	}
1102
1103	private boolean showLoadMoreMessages(final Conversation c) {
1104		final boolean mam = hasMamSupport(c);
1105		final MessageArchiveService service = activity.xmppConnectionService.getMessageArchiveService();
1106		return mam && (c.getLastClearHistory() != 0  || (c.countMessages() == 0 && c.hasMessagesLeftOnServer()  && !service.queryInProgress(c)));
1107	}
1108
1109	private boolean hasMamSupport(final Conversation c) {
1110		if (c.getMode() == Conversation.MODE_SINGLE) {
1111			final XmppConnection connection = c.getAccount().getXmppConnection();
1112			return connection != null && connection.getFeatures().mam();
1113		} else {
1114			return c.getMucOptions().mamSupport();
1115		}
1116	}
1117
1118	protected void showSnackbar(final int message, final int action, final OnClickListener clickListener) {
1119		snackbar.setVisibility(View.VISIBLE);
1120		snackbar.setOnClickListener(null);
1121		snackbarMessage.setText(message);
1122		snackbarMessage.setOnClickListener(null);
1123		snackbarAction.setVisibility(clickListener == null ? View.GONE : View.VISIBLE);
1124		if (action != 0) {
1125			snackbarAction.setText(action);
1126		}
1127		snackbarAction.setOnClickListener(clickListener);
1128	}
1129
1130	protected void hideSnackbar() {
1131		snackbar.setVisibility(View.GONE);
1132	}
1133
1134	protected void sendPlainTextMessage(Message message) {
1135		ConversationActivity activity = (ConversationActivity) getActivity();
1136		activity.xmppConnectionService.sendMessage(message);
1137		messageSent();
1138	}
1139
1140	protected void sendPgpMessage(final Message message) {
1141		final ConversationActivity activity = (ConversationActivity) getActivity();
1142		final XmppConnectionService xmppService = activity.xmppConnectionService;
1143		final Contact contact = message.getConversation().getContact();
1144		if (!activity.hasPgp()) {
1145			activity.showInstallPgpDialog();
1146			return;
1147		}
1148		if (conversation.getAccount().getPgpSignature() == null) {
1149			activity.announcePgp(conversation.getAccount(), conversation);
1150			return;
1151		}
1152		if (conversation.getMode() == Conversation.MODE_SINGLE) {
1153			if (contact.getPgpKeyId() != 0) {
1154				xmppService.getPgpEngine().hasKey(contact,
1155						new UiCallback<Contact>() {
1156
1157							@Override
1158							public void userInputRequried(PendingIntent pi,
1159														  Contact contact) {
1160								activity.runIntent(
1161										pi,
1162										ConversationActivity.REQUEST_ENCRYPT_MESSAGE);
1163							}
1164
1165							@Override
1166							public void success(Contact contact) {
1167								messageSent();
1168								activity.encryptTextMessage(message);
1169							}
1170
1171							@Override
1172							public void error(int error, Contact contact) {
1173								System.out.println();
1174							}
1175						});
1176
1177			} else {
1178				showNoPGPKeyDialog(false,
1179						new DialogInterface.OnClickListener() {
1180
1181							@Override
1182							public void onClick(DialogInterface dialog,
1183												int which) {
1184								conversation
1185										.setNextEncryption(Message.ENCRYPTION_NONE);
1186								xmppService.databaseBackend
1187										.updateConversation(conversation);
1188								message.setEncryption(Message.ENCRYPTION_NONE);
1189								xmppService.sendMessage(message);
1190								messageSent();
1191							}
1192						});
1193			}
1194		} else {
1195			if (conversation.getMucOptions().pgpKeysInUse()) {
1196				if (!conversation.getMucOptions().everybodyHasKeys()) {
1197					Toast warning = Toast
1198							.makeText(getActivity(),
1199									R.string.missing_public_keys,
1200									Toast.LENGTH_LONG);
1201					warning.setGravity(Gravity.CENTER_VERTICAL, 0, 0);
1202					warning.show();
1203				}
1204				activity.encryptTextMessage(message);
1205				messageSent();
1206			} else {
1207				showNoPGPKeyDialog(true,
1208						new DialogInterface.OnClickListener() {
1209
1210							@Override
1211							public void onClick(DialogInterface dialog,
1212												int which) {
1213								conversation
1214										.setNextEncryption(Message.ENCRYPTION_NONE);
1215								message.setEncryption(Message.ENCRYPTION_NONE);
1216								xmppService.databaseBackend
1217										.updateConversation(conversation);
1218								xmppService.sendMessage(message);
1219								messageSent();
1220							}
1221						});
1222			}
1223		}
1224	}
1225
1226	public void showNoPGPKeyDialog(boolean plural,
1227								   DialogInterface.OnClickListener listener) {
1228		AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
1229		builder.setIconAttribute(android.R.attr.alertDialogIcon);
1230		if (plural) {
1231			builder.setTitle(getString(R.string.no_pgp_keys));
1232			builder.setMessage(getText(R.string.contacts_have_no_pgp_keys));
1233		} else {
1234			builder.setTitle(getString(R.string.no_pgp_key));
1235			builder.setMessage(getText(R.string.contact_has_no_pgp_key));
1236		}
1237		builder.setNegativeButton(getString(R.string.cancel), null);
1238		builder.setPositiveButton(getString(R.string.send_unencrypted),
1239				listener);
1240		builder.create().show();
1241	}
1242
1243	protected void sendAxolotlMessage(final Message message) {
1244		final ConversationActivity activity = (ConversationActivity) getActivity();
1245		final XmppConnectionService xmppService = activity.xmppConnectionService;
1246		xmppService.sendMessage(message);
1247		messageSent();
1248	}
1249
1250	protected void sendOtrMessage(final Message message) {
1251		final ConversationActivity activity = (ConversationActivity) getActivity();
1252		final XmppConnectionService xmppService = activity.xmppConnectionService;
1253		activity.selectPresence(message.getConversation(),
1254				new OnPresenceSelected() {
1255
1256					@Override
1257					public void onPresenceSelected() {
1258						message.setCounterpart(conversation.getNextCounterpart());
1259						xmppService.sendMessage(message);
1260						messageSent();
1261					}
1262				});
1263	}
1264
1265	public void appendText(String text) {
1266		if (text == null) {
1267			return;
1268		}
1269		String previous = this.mEditMessage.getText().toString();
1270		if (previous.length() != 0 && !previous.endsWith(" ")) {
1271			text = " " + text;
1272		}
1273		this.mEditMessage.append(text);
1274	}
1275
1276	@Override
1277	public boolean onEnterPressed() {
1278		if (activity.enterIsSend()) {
1279			sendMessage();
1280			return true;
1281		} else {
1282			return false;
1283		}
1284	}
1285
1286	@Override
1287	public void onTypingStarted() {
1288		Account.State status = conversation.getAccount().getStatus();
1289		if (status == Account.State.ONLINE && conversation.setOutgoingChatState(ChatState.COMPOSING)) {
1290			activity.xmppConnectionService.sendChatState(conversation);
1291		}
1292		activity.hideConversationsOverview();
1293		updateSendButton();
1294	}
1295
1296	@Override
1297	public void onTypingStopped() {
1298		Account.State status = conversation.getAccount().getStatus();
1299		if (status == Account.State.ONLINE && conversation.setOutgoingChatState(ChatState.PAUSED)) {
1300			activity.xmppConnectionService.sendChatState(conversation);
1301		}
1302	}
1303
1304	@Override
1305	public void onTextDeleted() {
1306		Account.State status = conversation.getAccount().getStatus();
1307		if (status == Account.State.ONLINE && conversation.setOutgoingChatState(Config.DEFAULT_CHATSTATE)) {
1308			activity.xmppConnectionService.sendChatState(conversation);
1309		}
1310		updateSendButton();
1311	}
1312
1313	@Override
1314	public void onTextChanged() {
1315		if (conversation != null && conversation.getCorrectingMessage() != null) {
1316			updateSendButton();
1317		}
1318	}
1319
1320	private int completionIndex = 0;
1321	private int lastCompletionLength = 0;
1322	private String incomplete;
1323	private int lastCompletionCursor;
1324	private boolean firstWord = false;
1325
1326	@Override
1327	public boolean onTabPressed(boolean repeated) {
1328		if (conversation == null || conversation.getMode() == Conversation.MODE_SINGLE) {
1329			return false;
1330		}
1331		if (repeated) {
1332			completionIndex++;
1333		} else {
1334			lastCompletionLength = 0;
1335			completionIndex = 0;
1336			final String content = mEditMessage.getText().toString();
1337			lastCompletionCursor = mEditMessage.getSelectionEnd();
1338			int start = lastCompletionCursor > 0 ? content.lastIndexOf(" ",lastCompletionCursor-1) + 1 : 0;
1339			firstWord = start == 0;
1340			incomplete = content.substring(start,lastCompletionCursor);
1341		}
1342		List<String> completions = new ArrayList<>();
1343		for(MucOptions.User user : conversation.getMucOptions().getUsers()) {
1344			if (user.getName().startsWith(incomplete)) {
1345				completions.add(user.getName()+(firstWord ? ": " : " "));
1346			}
1347		}
1348		Collections.sort(completions);
1349		if (completions.size() > completionIndex) {
1350			String completion = completions.get(completionIndex).substring(incomplete.length());
1351			mEditMessage.getEditableText().delete(lastCompletionCursor,lastCompletionCursor + lastCompletionLength);
1352			mEditMessage.getEditableText().insert(lastCompletionCursor, completion);
1353			lastCompletionLength = completion.length();
1354		} else {
1355			completionIndex = -1;
1356			mEditMessage.getEditableText().delete(lastCompletionCursor,lastCompletionCursor + lastCompletionLength);
1357			lastCompletionLength = 0;
1358		}
1359		return true;
1360	}
1361
1362	@Override
1363	public void onActivityResult(int requestCode, int resultCode,
1364	                                final Intent data) {
1365		if (resultCode == Activity.RESULT_OK) {
1366			if (requestCode == ConversationActivity.REQUEST_DECRYPT_PGP) {
1367				activity.getSelectedConversation().getAccount().getPgpDecryptionService().onKeychainUnlocked();
1368				keychainUnlock = KEYCHAIN_UNLOCK_NOT_REQUIRED;
1369				updatePgpMessages();
1370			} else if (requestCode == ConversationActivity.REQUEST_TRUST_KEYS_TEXT) {
1371				final String body = mEditMessage.getText().toString();
1372				Message message = new Message(conversation, body, conversation.getNextEncryption());
1373				sendAxolotlMessage(message);
1374			} else if (requestCode == ConversationActivity.REQUEST_TRUST_KEYS_MENU) {
1375				int choice = data.getIntExtra("choice", ConversationActivity.ATTACHMENT_CHOICE_INVALID);
1376				activity.selectPresenceToAttachFile(choice, conversation.getNextEncryption());
1377			}
1378		} else {
1379			if (requestCode == ConversationActivity.REQUEST_DECRYPT_PGP) {
1380				keychainUnlock = KEYCHAIN_UNLOCK_NOT_REQUIRED;
1381				updatePgpMessages();
1382			}
1383		}
1384	}
1385
1386}