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.os.Handler;
  14import android.support.v13.view.inputmethod.InputConnectionCompat;
  15import android.support.v13.view.inputmethod.InputContentInfoCompat;
  16import android.text.Editable;
  17import android.text.InputType;
  18import android.text.TextWatcher;
  19import android.text.style.StyleSpan;
  20import android.util.Log;
  21import android.util.Pair;
  22import android.view.ContextMenu;
  23import android.view.ContextMenu.ContextMenuInfo;
  24import android.view.Gravity;
  25import android.view.KeyEvent;
  26import android.view.LayoutInflater;
  27import android.view.MenuItem;
  28import android.view.View;
  29import android.view.View.OnClickListener;
  30import android.view.ViewGroup;
  31import android.view.inputmethod.EditorInfo;
  32import android.view.inputmethod.InputMethodManager;
  33import android.widget.AbsListView;
  34import android.widget.AbsListView.OnScrollListener;
  35import android.widget.AdapterView;
  36import android.widget.AdapterView.AdapterContextMenuInfo;
  37import android.widget.ImageButton;
  38import android.widget.ListView;
  39import android.widget.PopupMenu;
  40import android.widget.RelativeLayout;
  41import android.widget.TextView;
  42import android.widget.TextView.OnEditorActionListener;
  43import android.widget.Toast;
  44
  45import net.java.otr4j.session.SessionStatus;
  46
  47import java.util.ArrayList;
  48import java.util.Arrays;
  49import java.util.Collections;
  50import java.util.List;
  51import java.util.UUID;
  52import java.util.concurrent.atomic.AtomicBoolean;
  53
  54import eu.siacs.conversations.Config;
  55import eu.siacs.conversations.R;
  56import eu.siacs.conversations.entities.Account;
  57import eu.siacs.conversations.entities.Blockable;
  58import eu.siacs.conversations.entities.Contact;
  59import eu.siacs.conversations.entities.Conversation;
  60import eu.siacs.conversations.entities.DownloadableFile;
  61import eu.siacs.conversations.entities.Message;
  62import eu.siacs.conversations.entities.MucOptions;
  63import eu.siacs.conversations.entities.Presence;
  64import eu.siacs.conversations.entities.Transferable;
  65import eu.siacs.conversations.entities.TransferablePlaceholder;
  66import eu.siacs.conversations.http.HttpDownloadConnection;
  67import eu.siacs.conversations.persistance.FileBackend;
  68import eu.siacs.conversations.services.MessageArchiveService;
  69import eu.siacs.conversations.services.XmppConnectionService;
  70import eu.siacs.conversations.ui.XmppActivity.OnPresenceSelected;
  71import eu.siacs.conversations.ui.XmppActivity.OnValueEdited;
  72import eu.siacs.conversations.ui.adapter.MessageAdapter;
  73import eu.siacs.conversations.ui.adapter.MessageAdapter.OnContactPictureClicked;
  74import eu.siacs.conversations.ui.adapter.MessageAdapter.OnContactPictureLongClicked;
  75import eu.siacs.conversations.ui.widget.EditMessage;
  76import eu.siacs.conversations.ui.widget.ListSelectionManager;
  77import eu.siacs.conversations.utils.NickValidityChecker;
  78import eu.siacs.conversations.utils.StylingHelper;
  79import eu.siacs.conversations.utils.UIHelper;
  80import eu.siacs.conversations.xmpp.XmppConnection;
  81import eu.siacs.conversations.xmpp.chatstate.ChatState;
  82import eu.siacs.conversations.xmpp.jid.Jid;
  83
  84public class ConversationFragment extends Fragment implements EditMessage.KeyboardListener {
  85
  86	protected Conversation conversation;
  87	private OnClickListener leaveMuc = new OnClickListener() {
  88
  89		@Override
  90		public void onClick(View v) {
  91			activity.endConversation(conversation);
  92		}
  93	};
  94	private OnClickListener joinMuc = new OnClickListener() {
  95
  96		@Override
  97		public void onClick(View v) {
  98			activity.xmppConnectionService.joinMuc(conversation);
  99		}
 100	};
 101	private OnClickListener enterPassword = new OnClickListener() {
 102
 103		@Override
 104		public void onClick(View v) {
 105			MucOptions muc = conversation.getMucOptions();
 106			String password = muc.getPassword();
 107			if (password == null) {
 108				password = "";
 109			}
 110			activity.quickPasswordEdit(password, new OnValueEdited() {
 111
 112				@Override
 113				public void onValueEdited(String value) {
 114					activity.xmppConnectionService.providePasswordForMuc(
 115							conversation, value);
 116				}
 117			});
 118		}
 119	};
 120	protected ListView messagesView;
 121	final protected List<Message> messageList = new ArrayList<>();
 122	protected MessageAdapter messageListAdapter;
 123	private EditMessage mEditMessage;
 124	private ImageButton mSendButton;
 125	private RelativeLayout snackbar;
 126	private TextView snackbarMessage;
 127	private TextView snackbarAction;
 128	private Toast messageLoaderToast;
 129
 130	private OnScrollListener mOnScrollListener = new OnScrollListener() {
 131
 132		@Override
 133		public void onScrollStateChanged(AbsListView view, int scrollState) {
 134			// TODO Auto-generated method stub
 135
 136		}
 137
 138		@Override
 139		public void onScroll(AbsListView view, int firstVisibleItem,
 140							 int visibleItemCount, int totalItemCount) {
 141			synchronized (ConversationFragment.this.messageList) {
 142				if (firstVisibleItem < 5 && conversation != null && conversation.messagesLoaded.compareAndSet(true,false) && messageList.size() > 0) {
 143					long timestamp;
 144					if (messageList.get(0).getType() == Message.TYPE_STATUS && messageList.size() >= 2) {
 145						timestamp = messageList.get(1).getTimeSent();
 146					} else {
 147						timestamp = messageList.get(0).getTimeSent();
 148					}
 149					activity.xmppConnectionService.loadMoreMessages(conversation, timestamp, new XmppConnectionService.OnMoreMessagesLoaded() {
 150						@Override
 151						public void onMoreMessagesLoaded(final int c, final Conversation conversation) {
 152							if (ConversationFragment.this.conversation != conversation) {
 153								conversation.messagesLoaded.set(true);
 154								return;
 155							}
 156							activity.runOnUiThread(new Runnable() {
 157								@Override
 158								public void run() {
 159									final int oldPosition = messagesView.getFirstVisiblePosition();
 160									Message message = null;
 161									int childPos;
 162									for(childPos = 0; childPos + oldPosition < messageList.size(); ++childPos) {
 163										message =  messageList.get(oldPosition + childPos);
 164										if (message.getType() != Message.TYPE_STATUS) {
 165											break;
 166										}
 167									}
 168									final String uuid = message != null ? message.getUuid() : null;
 169									View v = messagesView.getChildAt(childPos);
 170									final int pxOffset = (v == null) ? 0 : v.getTop();
 171									ConversationFragment.this.conversation.populateWithMessages(ConversationFragment.this.messageList);
 172									try {
 173										updateStatusMessages();
 174									} catch (IllegalStateException e) {
 175										Log.d(Config.LOGTAG,"caught illegal state exception while updating status messages");
 176									}
 177									messageListAdapter.notifyDataSetChanged();
 178									int pos = Math.max(getIndexOf(uuid,messageList),0);
 179									messagesView.setSelectionFromTop(pos, pxOffset);
 180									if (messageLoaderToast != null) {
 181										messageLoaderToast.cancel();
 182									}
 183									conversation.messagesLoaded.set(true);
 184								}
 185							});
 186						}
 187
 188						@Override
 189						public void informUser(final int resId) {
 190
 191							activity.runOnUiThread(new Runnable() {
 192								@Override
 193								public void run() {
 194									if (messageLoaderToast != null) {
 195										messageLoaderToast.cancel();
 196									}
 197									if (ConversationFragment.this.conversation != conversation) {
 198										return;
 199									}
 200									messageLoaderToast = Toast.makeText(activity, resId, Toast.LENGTH_LONG);
 201									messageLoaderToast.show();
 202								}
 203							});
 204
 205						}
 206					});
 207
 208				}
 209			}
 210		}
 211	};
 212
 213	private int getIndexOf(String uuid, List<Message> messages) {
 214		if (uuid == null) {
 215			return messages.size() - 1;
 216		}
 217		for(int i = 0; i < messages.size(); ++i) {
 218			if (uuid.equals(messages.get(i).getUuid())) {
 219				return i;
 220			} else {
 221				Message next = messages.get(i);
 222				while(next != null && next.wasMergedIntoPrevious()) {
 223					if (uuid.equals(next.getUuid())) {
 224						return i;
 225					}
 226					next = next.next();
 227				}
 228
 229			}
 230		}
 231		return -1;
 232	}
 233
 234	public Pair<Integer,Integer> getScrollPosition() {
 235		if (this.messagesView.getCount() == 0 ||
 236				this.messagesView.getLastVisiblePosition() == this.messagesView.getCount() - 1) {
 237			return null;
 238		} else {
 239			final int pos = messagesView.getFirstVisiblePosition();
 240			final View view = messagesView.getChildAt(0);
 241			if (view == null) {
 242				return null;
 243			} else {
 244				return new Pair<>(pos, view.getTop());
 245			}
 246		}
 247	}
 248
 249	public void setScrollPosition(Pair<Integer,Integer> scrollPosition) {
 250		if (scrollPosition != null) {
 251			this.messagesView.setSelectionFromTop(scrollPosition.first, scrollPosition.second);
 252		}
 253	}
 254
 255	protected OnClickListener clickToDecryptListener = new OnClickListener() {
 256
 257		@Override
 258		public void onClick(View v) {
 259			PendingIntent pendingIntent = conversation.getAccount().getPgpDecryptionService().getPendingIntent();
 260			if (pendingIntent != null) {
 261				try {
 262					activity.startIntentSenderForResult(pendingIntent.getIntentSender(),
 263                            ConversationActivity.REQUEST_DECRYPT_PGP,
 264                            null,
 265                            0,
 266                            0,
 267                            0);
 268				} catch (SendIntentException e) {
 269					Toast.makeText(activity,R.string.unable_to_connect_to_keychain, Toast.LENGTH_SHORT).show();
 270					conversation.getAccount().getPgpDecryptionService().continueDecryption(true);
 271				}
 272			}
 273			updateSnackBar(conversation);
 274		}
 275	};
 276	protected OnClickListener clickToVerify = new OnClickListener() {
 277
 278		@Override
 279		public void onClick(View v) {
 280			activity.verifyOtrSessionDialog(conversation, v);
 281		}
 282	};
 283	private OnEditorActionListener mEditorActionListener = new OnEditorActionListener() {
 284
 285		@Override
 286		public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
 287			if (actionId == EditorInfo.IME_ACTION_SEND) {
 288				InputMethodManager imm = (InputMethodManager) v.getContext()
 289						.getSystemService(Context.INPUT_METHOD_SERVICE);
 290				if (imm.isFullscreenMode()) {
 291					imm.hideSoftInputFromWindow(v.getWindowToken(), 0);
 292				}
 293				sendMessage();
 294				return true;
 295			} else {
 296				return false;
 297			}
 298		}
 299	};
 300	private EditMessage.OnCommitContentListener mEditorContentListener = new EditMessage.OnCommitContentListener() {
 301		@Override
 302		public boolean onCommitContent(InputContentInfoCompat inputContentInfo, int flags, Bundle opts, String[] contentMimeTypes) {
 303			// try to get permission to read the image, if applicable
 304			if ((flags & InputConnectionCompat.INPUT_CONTENT_GRANT_READ_URI_PERMISSION) != 0) {
 305				try {
 306					inputContentInfo.requestPermission();
 307				} catch (Exception e) {
 308					Log.e(Config.LOGTAG, "InputContentInfoCompat#requestPermission() failed.", e);
 309					Toast.makeText(
 310							activity,
 311							activity.getString(R.string.no_permission_to_access_x, inputContentInfo.getDescription()),
 312							Toast.LENGTH_LONG
 313					).show();
 314					return false;
 315				}
 316			}
 317			if (activity.hasStoragePermission(ConversationActivity.REQUEST_ADD_EDITOR_CONTENT)) {
 318				activity.attachImageToConversation(inputContentInfo.getContentUri());
 319			} else {
 320				activity.mPendingEditorContent = inputContentInfo.getContentUri();
 321			}
 322			return true;
 323		}
 324	};
 325	private OnClickListener mSendButtonListener = new OnClickListener() {
 326
 327		@Override
 328		public void onClick(View v) {
 329			Object tag = v.getTag();
 330			if (tag instanceof SendButtonAction) {
 331				SendButtonAction action = (SendButtonAction) tag;
 332				switch (action) {
 333					case TAKE_PHOTO:
 334						activity.attachFile(ConversationActivity.ATTACHMENT_CHOICE_TAKE_PHOTO);
 335						break;
 336					case RECORD_VIDEO:
 337						activity.attachFile(ConversationActivity.ATTACHMENT_CHOICE_RECORD_VIDEO);
 338						break;
 339					case SEND_LOCATION:
 340						activity.attachFile(ConversationActivity.ATTACHMENT_CHOICE_LOCATION);
 341						break;
 342					case RECORD_VOICE:
 343						activity.attachFile(ConversationActivity.ATTACHMENT_CHOICE_RECORD_VOICE);
 344						break;
 345					case CHOOSE_PICTURE:
 346						activity.attachFile(ConversationActivity.ATTACHMENT_CHOICE_CHOOSE_IMAGE);
 347						break;
 348					case CANCEL:
 349						if (conversation != null) {
 350							if(conversation.setCorrectingMessage(null)) {
 351								mEditMessage.setText("");
 352								mEditMessage.append(conversation.getDraftMessage());
 353								conversation.setDraftMessage(null);
 354							} else if (conversation.getMode() == Conversation.MODE_MULTI) {
 355								conversation.setNextCounterpart(null);
 356							}
 357							updateChatMsgHint();
 358							updateSendButton();
 359							updateEditablity();
 360						}
 361						break;
 362					default:
 363						sendMessage();
 364				}
 365			} else {
 366				sendMessage();
 367			}
 368		}
 369	};
 370	private OnClickListener clickToMuc = new OnClickListener() {
 371
 372		@Override
 373		public void onClick(View v) {
 374			Intent intent = new Intent(getActivity(), ConferenceDetailsActivity.class);
 375			intent.setAction(ConferenceDetailsActivity.ACTION_VIEW_MUC);
 376			intent.putExtra("uuid", conversation.getUuid());
 377			startActivity(intent);
 378		}
 379	};
 380	private ConversationActivity activity;
 381	private Message selectedMessage;
 382
 383	private void sendMessage() {
 384		final String body = mEditMessage.getText().toString();
 385		final Conversation conversation = this.conversation;
 386		if (body.length() == 0 || conversation == null) {
 387			return;
 388		}
 389		final Message message;
 390		if (conversation.getCorrectingMessage() == null) {
 391			message = new Message(conversation, body, conversation.getNextEncryption());
 392			if (conversation.getMode() == Conversation.MODE_MULTI) {
 393				if (conversation.getNextCounterpart() != null) {
 394					message.setCounterpart(conversation.getNextCounterpart());
 395					message.setType(Message.TYPE_PRIVATE);
 396				}
 397			}
 398		} else {
 399			message = conversation.getCorrectingMessage();
 400			message.setBody(body);
 401			message.setEdited(message.getUuid());
 402			message.setUuid(UUID.randomUUID().toString());
 403		}
 404		switch (message.getConversation().getNextEncryption()) {
 405			case Message.ENCRYPTION_OTR:
 406				sendOtrMessage(message);
 407				break;
 408			case Message.ENCRYPTION_PGP:
 409				sendPgpMessage(message);
 410				break;
 411			case Message.ENCRYPTION_AXOLOTL:
 412				if(!activity.trustKeysIfNeeded(ConversationActivity.REQUEST_TRUST_KEYS_TEXT)) {
 413					sendAxolotlMessage(message);
 414				}
 415				break;
 416			default:
 417				sendPlainTextMessage(message);
 418		}
 419	}
 420
 421	public void updateChatMsgHint() {
 422		final boolean multi = conversation.getMode() == Conversation.MODE_MULTI;
 423		if (conversation.getCorrectingMessage() != null) {
 424			this.mEditMessage.setHint(R.string.send_corrected_message);
 425		} else if (multi && conversation.getNextCounterpart() != null) {
 426			this.mEditMessage.setHint(getString(
 427					R.string.send_private_message_to,
 428					conversation.getNextCounterpart().getResourcepart()));
 429		} else if (multi && !conversation.getMucOptions().participating()) {
 430			this.mEditMessage.setHint(R.string.you_are_not_participating);
 431		} else {
 432			this.mEditMessage.setHint(UIHelper.getMessageHint(activity,conversation));
 433			getActivity().invalidateOptionsMenu();
 434		}
 435	}
 436
 437	public void setupIme() {
 438		if (activity != null) {
 439			if (activity.usingEnterKey() && activity.enterIsSend()) {
 440				mEditMessage.setInputType(mEditMessage.getInputType() & (~InputType.TYPE_TEXT_FLAG_MULTI_LINE));
 441				mEditMessage.setInputType(mEditMessage.getInputType() & (~InputType.TYPE_TEXT_VARIATION_SHORT_MESSAGE));
 442			} else if (activity.usingEnterKey()) {
 443				mEditMessage.setInputType(mEditMessage.getInputType() | InputType.TYPE_TEXT_FLAG_MULTI_LINE);
 444				mEditMessage.setInputType(mEditMessage.getInputType() & (~InputType.TYPE_TEXT_VARIATION_SHORT_MESSAGE));
 445			} else {
 446				mEditMessage.setInputType(mEditMessage.getInputType() | InputType.TYPE_TEXT_FLAG_MULTI_LINE);
 447				mEditMessage.setInputType(mEditMessage.getInputType() | InputType.TYPE_TEXT_VARIATION_SHORT_MESSAGE);
 448			}
 449		}
 450	}
 451
 452	@Override
 453	public View onCreateView(final LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
 454		final View view = inflater.inflate(R.layout.fragment_conversation, container, false);
 455		view.setOnClickListener(null);
 456
 457		mEditMessage = (EditMessage) view.findViewById(R.id.textinput);
 458		mEditMessage.setOnClickListener(new OnClickListener() {
 459
 460			@Override
 461			public void onClick(View v) {
 462				if (activity != null) {
 463					activity.hideConversationsOverview();
 464				}
 465			}
 466		});
 467
 468		mEditMessage.addTextChangedListener(new StylingHelper.MessageEditorStyler(mEditMessage));
 469
 470		mEditMessage.setOnEditorActionListener(mEditorActionListener);
 471		mEditMessage.setRichContentListener(new String[]{"image/*"}, mEditorContentListener);
 472
 473		mSendButton = (ImageButton) view.findViewById(R.id.textSendButton);
 474		mSendButton.setOnClickListener(this.mSendButtonListener);
 475
 476		snackbar = (RelativeLayout) view.findViewById(R.id.snackbar);
 477		snackbarMessage = (TextView) view.findViewById(R.id.snackbar_message);
 478		snackbarAction = (TextView) view.findViewById(R.id.snackbar_action);
 479
 480		messagesView = (ListView) view.findViewById(R.id.messages_view);
 481		messagesView.setOnScrollListener(mOnScrollListener);
 482		messagesView.setTranscriptMode(ListView.TRANSCRIPT_MODE_NORMAL);
 483		messageListAdapter = new MessageAdapter((ConversationActivity) getActivity(), this.messageList);
 484		messageListAdapter.setOnContactPictureClicked(new OnContactPictureClicked() {
 485
 486			@Override
 487			public void onContactPictureClicked(Message message) {
 488				if (message.getStatus() <= Message.STATUS_RECEIVED) {
 489					if (message.getConversation().getMode() == Conversation.MODE_MULTI) {
 490						Jid user = message.getCounterpart();
 491						if (user != null && !user.isBareJid()) {
 492							if (!message.getConversation().getMucOptions().isUserInRoom(user)) {
 493								Toast.makeText(activity,activity.getString(R.string.user_has_left_conference,user.getResourcepart()),Toast.LENGTH_SHORT).show();
 494							}
 495							highlightInConference(user.getResourcepart());
 496						}
 497					} else {
 498						if (!message.getContact().isSelf()) {
 499							String fingerprint;
 500							if (message.getEncryption() == Message.ENCRYPTION_PGP
 501									|| message.getEncryption() == Message.ENCRYPTION_DECRYPTED) {
 502								fingerprint = "pgp";
 503							} else {
 504								fingerprint = message.getFingerprint();
 505							}
 506							activity.switchToContactDetails(message.getContact(), fingerprint);
 507						}
 508					}
 509				} else {
 510					Account account = message.getConversation().getAccount();
 511					Intent intent;
 512					if (activity.manuallyChangePresence()) {
 513						intent = new Intent(activity, SetPresenceActivity.class);
 514						intent.putExtra(SetPresenceActivity.EXTRA_ACCOUNT, account.getJid().toBareJid().toString());
 515					} else {
 516						intent = new Intent(activity, EditAccountActivity.class);
 517						intent.putExtra("jid", account.getJid().toBareJid().toString());
 518						String fingerprint;
 519						if (message.getEncryption() == Message.ENCRYPTION_PGP
 520								|| message.getEncryption() == Message.ENCRYPTION_DECRYPTED) {
 521							fingerprint = "pgp";
 522						} else if (message.getEncryption() == Message.ENCRYPTION_OTR) {
 523							fingerprint = "otr";
 524						} else {
 525							fingerprint = message.getFingerprint();
 526						}
 527						intent.putExtra("fingerprint", fingerprint);
 528					}
 529					startActivity(intent);
 530				}
 531			}
 532		});
 533		messageListAdapter
 534				.setOnContactPictureLongClicked(new OnContactPictureLongClicked() {
 535
 536					@Override
 537					public void onContactPictureLongClicked(Message message) {
 538						if (message.getStatus() <= Message.STATUS_RECEIVED) {
 539							if (message.getConversation().getMode() == Conversation.MODE_MULTI) {
 540								Jid user = message.getCounterpart();
 541								if (user != null && !user.isBareJid()) {
 542									if (message.getConversation().getMucOptions().isUserInRoom(user)) {
 543										privateMessageWith(user);
 544									} else {
 545										Toast.makeText(activity, activity.getString(R.string.user_has_left_conference, user.getResourcepart()), Toast.LENGTH_SHORT).show();
 546									}
 547								}
 548							}
 549						} else {
 550							activity.showQrCode();
 551						}
 552					}
 553				});
 554		messageListAdapter.setOnQuoteListener(new MessageAdapter.OnQuoteListener() {
 555
 556			@Override
 557			public void onQuote(String text) {
 558				quoteText(text);
 559			}
 560		});
 561		messagesView.setAdapter(messageListAdapter);
 562
 563		registerForContextMenu(messagesView);
 564
 565		return view;
 566	}
 567
 568	private void quoteText(String text) {
 569		if (mEditMessage.isEnabled()) {
 570			text = text.replaceAll("(\n *){2,}", "\n").replaceAll("(^|\n)", "$1> ").replaceAll("\n$", "");
 571			Editable editable = mEditMessage.getEditableText();
 572			int position = mEditMessage.getSelectionEnd();
 573			if (position == -1) position = editable.length();
 574			if (position > 0 && editable.charAt(position - 1) != '\n') {
 575				editable.insert(position++, "\n");
 576			}
 577			editable.insert(position, text);
 578			position += text.length();
 579			editable.insert(position++, "\n");
 580			if (position < editable.length() && editable.charAt(position) != '\n') {
 581				editable.insert(position, "\n");
 582			}
 583			mEditMessage.setSelection(position);
 584			mEditMessage.requestFocus();
 585			InputMethodManager inputMethodManager = (InputMethodManager) getActivity()
 586					.getSystemService(Context.INPUT_METHOD_SERVICE);
 587			if (inputMethodManager != null) {
 588				inputMethodManager.showSoftInput(mEditMessage, InputMethodManager.SHOW_IMPLICIT);
 589			}
 590		}
 591	}
 592
 593	private void quoteMessage(Message message) {
 594		quoteText(message.getMergedBody().toString());
 595	}
 596
 597	@Override
 598	public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
 599		synchronized (this.messageList) {
 600			super.onCreateContextMenu(menu, v, menuInfo);
 601			AdapterView.AdapterContextMenuInfo acmi = (AdapterContextMenuInfo) menuInfo;
 602			this.selectedMessage = this.messageList.get(acmi.position);
 603			populateContextMenu(menu);
 604		}
 605	}
 606
 607	private void populateContextMenu(ContextMenu menu) {
 608		final Message m = this.selectedMessage;
 609		final Transferable t = m.getTransferable();
 610		Message relevantForCorrection = m;
 611		while(relevantForCorrection.mergeable(relevantForCorrection.next())) {
 612			relevantForCorrection = relevantForCorrection.next();
 613		}
 614		if (m.getType() != Message.TYPE_STATUS) {
 615			final boolean treatAsFile = m.getType() != Message.TYPE_TEXT
 616					&& m.getType() != Message.TYPE_PRIVATE
 617					&& t == null;
 618			final boolean encrypted = m.getEncryption() == Message.ENCRYPTION_DECRYPTION_FAILED
 619					|| m.getEncryption() == Message.ENCRYPTION_PGP;
 620			activity.getMenuInflater().inflate(R.menu.message_context, menu);
 621			menu.setHeaderTitle(R.string.message_options);
 622			MenuItem copyMessage = menu.findItem(R.id.copy_message);
 623			MenuItem quoteMessage = menu.findItem(R.id.quote_message);
 624			MenuItem retryDecryption = menu.findItem(R.id.retry_decryption);
 625			MenuItem correctMessage = menu.findItem(R.id.correct_message);
 626			MenuItem shareWith = menu.findItem(R.id.share_with);
 627			MenuItem sendAgain = menu.findItem(R.id.send_again);
 628			MenuItem copyUrl = menu.findItem(R.id.copy_url);
 629			MenuItem downloadFile = menu.findItem(R.id.download_file);
 630			MenuItem cancelTransmission = menu.findItem(R.id.cancel_transmission);
 631			MenuItem deleteFile = menu.findItem(R.id.delete_file);
 632			MenuItem showErrorMessage = menu.findItem(R.id.show_error_message);
 633			if (!treatAsFile && !encrypted && !m.isGeoUri() && !m.treatAsDownloadable()) {
 634				copyMessage.setVisible(true);
 635				quoteMessage.setVisible(true);
 636			}
 637			if (m.getEncryption() == Message.ENCRYPTION_DECRYPTION_FAILED) {
 638				retryDecryption.setVisible(true);
 639			}
 640			if (relevantForCorrection.getType() == Message.TYPE_TEXT
 641					&& relevantForCorrection.isLastCorrectableMessage()
 642					&& (m.getConversation().getMucOptions().nonanonymous() || m.getConversation().getMode() == Conversation.MODE_SINGLE)) {
 643				correctMessage.setVisible(true);
 644			}
 645			if (treatAsFile || (m.getType() == Message.TYPE_TEXT && !m.treatAsDownloadable())) {
 646				shareWith.setVisible(true);
 647			}
 648			if (m.getStatus() == Message.STATUS_SEND_FAILED) {
 649				sendAgain.setVisible(true);
 650			}
 651			if (m.hasFileOnRemoteHost()
 652					|| m.isGeoUri()
 653					|| m.treatAsDownloadable()
 654					|| (t != null && t instanceof HttpDownloadConnection)) {
 655				copyUrl.setVisible(true);
 656			}
 657			if ((m.isFileOrImage() && t instanceof TransferablePlaceholder && m.hasFileOnRemoteHost())) {
 658				downloadFile.setVisible(true);
 659				downloadFile.setTitle(activity.getString(R.string.download_x_file,UIHelper.getFileDescriptionString(activity, m)));
 660			}
 661			boolean waitingOfferedSending = m.getStatus() == Message.STATUS_WAITING
 662					|| m.getStatus() == Message.STATUS_UNSEND
 663					|| m.getStatus() == Message.STATUS_OFFERED;
 664			if ((t != null && !(t instanceof TransferablePlaceholder)) || waitingOfferedSending && m.needsUploading()) {
 665				cancelTransmission.setVisible(true);
 666			}
 667			if (treatAsFile) {
 668				String path = m.getRelativeFilePath();
 669				if (path == null || !path.startsWith("/")) {
 670					deleteFile.setVisible(true);
 671					deleteFile.setTitle(activity.getString(R.string.delete_x_file, UIHelper.getFileDescriptionString(activity, m)));
 672				}
 673			}
 674			if (m.getStatus() == Message.STATUS_SEND_FAILED && m.getErrorMessage() != null) {
 675				showErrorMessage.setVisible(true);
 676			}
 677		}
 678	}
 679
 680	@Override
 681	public boolean onContextItemSelected(MenuItem item) {
 682		switch (item.getItemId()) {
 683			case R.id.share_with:
 684				shareWith(selectedMessage);
 685				return true;
 686			case R.id.correct_message:
 687				correctMessage(selectedMessage);
 688				return true;
 689			case R.id.copy_message:
 690				copyMessage(selectedMessage);
 691				return true;
 692			case R.id.quote_message:
 693				quoteMessage(selectedMessage);
 694				return true;
 695			case R.id.send_again:
 696				resendMessage(selectedMessage);
 697				return true;
 698			case R.id.copy_url:
 699				copyUrl(selectedMessage);
 700				return true;
 701			case R.id.download_file:
 702				downloadFile(selectedMessage);
 703				return true;
 704			case R.id.cancel_transmission:
 705				cancelTransmission(selectedMessage);
 706				return true;
 707			case R.id.retry_decryption:
 708				retryDecryption(selectedMessage);
 709				return true;
 710			case R.id.delete_file:
 711				deleteFile(selectedMessage);
 712				return true;
 713			case R.id.show_error_message:
 714				showErrorMessage(selectedMessage);
 715				return true;
 716			default:
 717				return super.onContextItemSelected(item);
 718		}
 719	}
 720
 721	private void showErrorMessage(final Message message) {
 722		AlertDialog.Builder builder = new AlertDialog.Builder(activity);
 723		builder.setTitle(R.string.error_message);
 724		builder.setMessage(message.getErrorMessage());
 725		builder.setPositiveButton(R.string.confirm,null);
 726		builder.create().show();
 727	}
 728
 729	private void shareWith(Message message) {
 730		Intent shareIntent = new Intent();
 731		shareIntent.setAction(Intent.ACTION_SEND);
 732		if (message.isGeoUri()) {
 733			shareIntent.putExtra(Intent.EXTRA_TEXT, message.getBody());
 734			shareIntent.setType("text/plain");
 735		} else if (!message.isFileOrImage()) {
 736			shareIntent.putExtra(Intent.EXTRA_TEXT, message.getMergedBody().toString());
 737			shareIntent.setType("text/plain");
 738		} else {
 739			final DownloadableFile file = activity.xmppConnectionService.getFileBackend().getFile(message);
 740			try {
 741				shareIntent.putExtra(Intent.EXTRA_STREAM, FileBackend.getUriForFile(activity, file));
 742			} catch (SecurityException e) {
 743				Toast.makeText(activity, activity.getString(R.string.no_permission_to_access_x, file.getAbsolutePath()), Toast.LENGTH_SHORT).show();
 744				return;
 745			}
 746			shareIntent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
 747			String mime = message.getMimeType();
 748			if (mime == null) {
 749				mime = "*/*";
 750			}
 751			shareIntent.setType(mime);
 752		}
 753		try {
 754			activity.startActivity(Intent.createChooser(shareIntent, getText(R.string.share_with)));
 755		} catch (ActivityNotFoundException e) {
 756			//This should happen only on faulty androids because normally chooser is always available
 757			Toast.makeText(activity,R.string.no_application_found_to_open_file,Toast.LENGTH_SHORT).show();
 758		}
 759	}
 760
 761	 private void copyMessage(Message message) {
 762		if (activity.copyTextToClipboard(message.getMergedBody().toString(), R.string.message)) {
 763			Toast.makeText(activity, R.string.message_copied_to_clipboard, Toast.LENGTH_SHORT).show();
 764		}
 765	 }
 766
 767	private void deleteFile(Message message) {
 768		if (activity.xmppConnectionService.getFileBackend().deleteFile(message)) {
 769			message.setTransferable(new TransferablePlaceholder(Transferable.STATUS_DELETED));
 770			activity.updateConversationList();
 771			updateMessages();
 772		}
 773	}
 774
 775	private void resendMessage(final Message message) {
 776		if (message.isFileOrImage()) {
 777			DownloadableFile file = activity.xmppConnectionService.getFileBackend().getFile(message);
 778			if (file.exists()) {
 779				final Conversation conversation = message.getConversation();
 780				final XmppConnection xmppConnection = conversation.getAccount().getXmppConnection();
 781				if (!message.hasFileOnRemoteHost()
 782						&& xmppConnection != null
 783						&& !xmppConnection.getFeatures().httpUpload(message.getFileParams().size)) {
 784					activity.selectPresence(conversation, new OnPresenceSelected() {
 785						@Override
 786						public void onPresenceSelected() {
 787							message.setCounterpart(conversation.getNextCounterpart());
 788							activity.xmppConnectionService.resendFailedMessages(message);
 789						}
 790					});
 791					return;
 792				}
 793			} else {
 794				Toast.makeText(activity, R.string.file_deleted, Toast.LENGTH_SHORT).show();
 795				message.setTransferable(new TransferablePlaceholder(Transferable.STATUS_DELETED));
 796				activity.updateConversationList();
 797				updateMessages();
 798				return;
 799			}
 800		}
 801		activity.xmppConnectionService.resendFailedMessages(message);
 802	}
 803
 804	private void copyUrl(Message message) {
 805		final String url;
 806		final int resId;
 807		if (message.isGeoUri()) {
 808			resId = R.string.location;
 809			url = message.getBody();
 810		} else if (message.hasFileOnRemoteHost()) {
 811			resId = R.string.file_url;
 812			url = message.getFileParams().url.toString();
 813		} else {
 814			url = message.getBody().trim();
 815			resId = R.string.file_url;
 816		}
 817		if (activity.copyTextToClipboard(url, resId)) {
 818			Toast.makeText(activity, R.string.url_copied_to_clipboard,
 819					Toast.LENGTH_SHORT).show();
 820		}
 821	}
 822
 823	private void downloadFile(Message message) {
 824		activity.xmppConnectionService.getHttpConnectionManager().createNewDownloadConnection(message,true);
 825	}
 826
 827	private void cancelTransmission(Message message) {
 828		Transferable transferable = message.getTransferable();
 829		if (transferable != null) {
 830			transferable.cancel();
 831		} else if (message.getStatus() != Message.STATUS_RECEIVED) {
 832			activity.xmppConnectionService.markMessage(message,Message.STATUS_SEND_FAILED);
 833		}
 834	}
 835
 836	private void retryDecryption(Message message) {
 837		message.setEncryption(Message.ENCRYPTION_PGP);
 838		activity.updateConversationList();
 839		updateMessages();
 840		conversation.getAccount().getPgpDecryptionService().decrypt(message, false);
 841	}
 842
 843	protected void privateMessageWith(final Jid counterpart) {
 844		if (conversation.setOutgoingChatState(Config.DEFAULT_CHATSTATE)) {
 845			activity.xmppConnectionService.sendChatState(conversation);
 846		}
 847		this.mEditMessage.setText("");
 848		this.conversation.setNextCounterpart(counterpart);
 849		updateChatMsgHint();
 850		updateSendButton();
 851		updateEditablity();
 852	}
 853
 854	private void correctMessage(Message message) {
 855		while(message.mergeable(message.next())) {
 856			message = message.next();
 857		}
 858		this.conversation.setCorrectingMessage(message);
 859		final Editable editable = mEditMessage.getText();
 860		this.conversation.setDraftMessage(editable.toString());
 861		this.mEditMessage.setText("");
 862		this.mEditMessage.append(message.getBody());
 863
 864	}
 865
 866	protected void highlightInConference(String nick) {
 867		final Editable editable = mEditMessage.getText();
 868		String oldString = editable.toString().trim();
 869		final int pos = mEditMessage.getSelectionStart();
 870		if (oldString.isEmpty() || pos == 0) {
 871			editable.insert(0, nick + ": ");
 872		} else {
 873			final char before = editable.charAt(pos - 1);
 874			final char after = editable.length() > pos ? editable.charAt(pos) : '\0';
 875			if (before == '\n') {
 876				editable.insert(pos, nick + ": ");
 877			} else {
 878				if (pos > 2 && editable.subSequence(pos-2,pos).toString().equals(": ")) {
 879					if (NickValidityChecker.check(conversation,Arrays.asList(editable.subSequence(0,pos-2).toString().split(", ")))) {
 880						editable.insert(pos - 2, ", " + nick);
 881						return;
 882					}
 883				}
 884				editable.insert(pos, (Character.isWhitespace(before) ? "" : " ") + nick + (Character.isWhitespace(after) ? "" : " "));
 885				if (Character.isWhitespace(after)) {
 886					mEditMessage.setSelection(mEditMessage.getSelectionStart() + 1);
 887				}
 888			}
 889		}
 890	}
 891
 892	@Override
 893	public void onStop() {
 894		super.onStop();
 895		if (activity == null || !activity.isChangingConfigurations()) {
 896			messageListAdapter.stopAudioPlayer();
 897		}
 898		if (this.conversation != null) {
 899			final String msg = mEditMessage.getText().toString();
 900			if (this.conversation.setNextMessage(msg)) {
 901				activity.xmppConnectionService.updateConversation(this.conversation);
 902			}
 903			updateChatState(this.conversation, msg);
 904		}
 905	}
 906
 907	private void updateChatState(final Conversation conversation, final String msg) {
 908		ChatState state = msg.length() == 0 ? Config.DEFAULT_CHATSTATE : ChatState.PAUSED;
 909		Account.State status = conversation.getAccount().getStatus();
 910		if (status == Account.State.ONLINE && conversation.setOutgoingChatState(state)) {
 911			activity.xmppConnectionService.sendChatState(conversation);
 912		}
 913	}
 914
 915	public boolean reInit(Conversation conversation) {
 916		if (conversation == null) {
 917			return false;
 918		}
 919		this.activity = (ConversationActivity) getActivity();
 920		setupIme();
 921		if (this.conversation != null) {
 922			final String msg = mEditMessage.getText().toString();
 923			if (this.conversation.setNextMessage(msg)) {
 924				activity.xmppConnectionService.updateConversation(conversation);
 925			}
 926			if (this.conversation != conversation) {
 927				updateChatState(this.conversation, msg);
 928				messageListAdapter.stopAudioPlayer();
 929			}
 930			this.conversation.trim();
 931
 932		}
 933
 934		if (activity != null) {
 935			this.mSendButton.setContentDescription(activity.getString(R.string.send_message_to_x,conversation.getName()));
 936		}
 937
 938		this.conversation = conversation;
 939		this.mEditMessage.setKeyboardListener(null);
 940		this.mEditMessage.setText("");
 941		this.mEditMessage.append(this.conversation.getNextMessage());
 942		this.mEditMessage.setKeyboardListener(this);
 943		messageListAdapter.updatePreferences();
 944		this.messagesView.setAdapter(messageListAdapter);
 945		updateMessages();
 946		this.conversation.messagesLoaded.set(true);
 947		synchronized (this.messageList) {
 948			final Message first = conversation.getFirstUnreadMessage();
 949			final int bottom = Math.max(0, this.messageList.size() - 1);
 950			final int pos;
 951			if (first == null) {
 952				pos = bottom;
 953			} else {
 954				int i = getIndexOf(first.getUuid(), this.messageList);
 955				pos = i < 0 ? bottom : i;
 956			}
 957			messagesView.setSelection(pos);
 958			return pos == bottom;
 959		}
 960	}
 961
 962	private OnClickListener mEnableAccountListener = new OnClickListener() {
 963		@Override
 964		public void onClick(View v) {
 965			final Account account = conversation == null ? null : conversation.getAccount();
 966			if (account != null) {
 967				account.setOption(Account.OPTION_DISABLED, false);
 968				activity.xmppConnectionService.updateAccount(account);
 969			}
 970		}
 971	};
 972
 973	private OnClickListener mUnblockClickListener = new OnClickListener() {
 974		@Override
 975		public void onClick(final View v) {
 976			v.post(new Runnable() {
 977				@Override
 978				public void run() {
 979					v.setVisibility(View.INVISIBLE);
 980				}
 981			});
 982			if (conversation.isDomainBlocked()) {
 983				BlockContactDialog.show(activity, conversation);
 984			} else {
 985				activity.unblockConversation(conversation);
 986			}
 987		}
 988	};
 989
 990	private void showBlockSubmenu(View view) {
 991		final Jid jid = conversation.getJid();
 992			if (jid.isDomainJid()) {
 993				BlockContactDialog.show(activity, conversation);
 994			} else {
 995				PopupMenu popupMenu = new PopupMenu(activity, view);
 996				popupMenu.inflate(R.menu.block);
 997				popupMenu.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() {
 998					@Override
 999					public boolean onMenuItemClick(MenuItem menuItem) {
1000						Blockable blockable;
1001						switch (menuItem.getItemId()) {
1002							case R.id.block_domain:
1003								blockable = conversation.getAccount().getRoster().getContact(jid.toDomainJid());
1004								break;
1005							default:
1006								blockable = conversation;
1007						}
1008						BlockContactDialog.show(activity, blockable);
1009						return true;
1010					}
1011				});
1012				popupMenu.show();
1013			}
1014	}
1015
1016	private OnClickListener mBlockClickListener = new OnClickListener() {
1017		@Override
1018		public void onClick(final View view) {
1019			showBlockSubmenu(view);
1020		}
1021	};
1022
1023	private OnClickListener mAddBackClickListener = new OnClickListener() {
1024
1025		@Override
1026		public void onClick(View v) {
1027			final Contact contact = conversation == null ? null : conversation.getContact();
1028			if (contact != null) {
1029				activity.xmppConnectionService.createContact(contact);
1030				activity.switchToContactDetails(contact);
1031			}
1032		}
1033	};
1034
1035	private View.OnLongClickListener mLongPressBlockListener = new View.OnLongClickListener() {
1036		@Override
1037		public boolean onLongClick(View v) {
1038			showBlockSubmenu(v);
1039			return true;
1040		}
1041	};
1042
1043	private OnClickListener mAllowPresenceSubscription = new OnClickListener() {
1044		@Override
1045		public void onClick(View v) {
1046			final Contact contact = conversation == null ? null : conversation.getContact();
1047			if (contact != null) {
1048				activity.xmppConnectionService.sendPresencePacket(contact.getAccount(),
1049						activity.xmppConnectionService.getPresenceGenerator()
1050								.sendPresenceUpdatesTo(contact));
1051				hideSnackbar();
1052			}
1053		}
1054	};
1055
1056	private OnClickListener mAnswerSmpClickListener = new OnClickListener() {
1057		@Override
1058		public void onClick(View view) {
1059			Intent intent = new Intent(activity, VerifyOTRActivity.class);
1060			intent.setAction(VerifyOTRActivity.ACTION_VERIFY_CONTACT);
1061			intent.putExtra("contact", conversation.getContact().getJid().toBareJid().toString());
1062			intent.putExtra(VerifyOTRActivity.EXTRA_ACCOUNT, conversation.getAccount().getJid().toBareJid().toString());
1063			intent.putExtra("mode", VerifyOTRActivity.MODE_ANSWER_QUESTION);
1064			startActivity(intent);
1065		}
1066	};
1067
1068	private void updateSnackBar(final Conversation conversation) {
1069		final Account account = conversation.getAccount();
1070		final XmppConnection connection = account.getXmppConnection();
1071		final int mode = conversation.getMode();
1072		final Contact contact = mode == Conversation.MODE_SINGLE ? conversation.getContact() : null;
1073		if (account.getStatus() == Account.State.DISABLED) {
1074			showSnackbar(R.string.this_account_is_disabled, R.string.enable, this.mEnableAccountListener);
1075		} else if (conversation.isBlocked()) {
1076			showSnackbar(R.string.contact_blocked, R.string.unblock, this.mUnblockClickListener);
1077		} else if (contact != null && !contact.showInRoster() && contact.getOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST)) {
1078			showSnackbar(R.string.contact_added_you, R.string.add_back, this.mAddBackClickListener, this.mLongPressBlockListener);
1079		} else if (contact != null && contact.getOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST)) {
1080			showSnackbar(R.string.contact_asks_for_presence_subscription, R.string.allow, this.mAllowPresenceSubscription, this.mLongPressBlockListener);
1081		} else if (mode == Conversation.MODE_MULTI
1082				&& !conversation.getMucOptions().online()
1083				&& account.getStatus() == Account.State.ONLINE) {
1084			switch (conversation.getMucOptions().getError()) {
1085				case NICK_IN_USE:
1086					showSnackbar(R.string.nick_in_use, R.string.edit, clickToMuc);
1087					break;
1088				case NO_RESPONSE:
1089					showSnackbar(R.string.joining_conference, 0, null);
1090					break;
1091				case SERVER_NOT_FOUND:
1092					if (conversation.receivedMessagesCount() > 0) {
1093						showSnackbar(R.string.remote_server_not_found,R.string.try_again, joinMuc);
1094					} else {
1095						showSnackbar(R.string.remote_server_not_found, R.string.leave, leaveMuc);
1096					}
1097					break;
1098				case PASSWORD_REQUIRED:
1099					showSnackbar(R.string.conference_requires_password, R.string.enter_password, enterPassword);
1100					break;
1101				case BANNED:
1102					showSnackbar(R.string.conference_banned, R.string.leave, leaveMuc);
1103					break;
1104				case MEMBERS_ONLY:
1105					showSnackbar(R.string.conference_members_only, R.string.leave, leaveMuc);
1106					break;
1107				case KICKED:
1108					showSnackbar(R.string.conference_kicked, R.string.join, joinMuc);
1109					break;
1110				case UNKNOWN:
1111					showSnackbar(R.string.conference_unknown_error, R.string.join, joinMuc);
1112					break;
1113				case SHUTDOWN:
1114					showSnackbar(R.string.conference_shutdown, R.string.join, joinMuc);
1115					break;
1116				default:
1117					hideSnackbar();
1118					break;
1119			}
1120		} else if (account.hasPendingPgpIntent(conversation)) {
1121			showSnackbar(R.string.openpgp_messages_found, R.string.decrypt, clickToDecryptListener);
1122		} else if (mode == Conversation.MODE_SINGLE
1123				&& conversation.smpRequested()) {
1124			showSnackbar(R.string.smp_requested, R.string.verify, this.mAnswerSmpClickListener);
1125		} else if (mode == Conversation.MODE_SINGLE
1126				&& conversation.hasValidOtrSession()
1127				&& (conversation.getOtrSession().getSessionStatus() == SessionStatus.ENCRYPTED)
1128				&& (!conversation.isOtrFingerprintVerified())) {
1129			showSnackbar(R.string.unknown_otr_fingerprint, R.string.verify, clickToVerify);
1130		} else if (connection != null
1131				&& connection.getFeatures().blocking()
1132				&& conversation.countMessages() != 0
1133				&& !conversation.isBlocked()
1134				&& conversation.isWithStranger()) {
1135			showSnackbar(R.string.received_message_from_stranger,R.string.block, mBlockClickListener);
1136		} else {
1137			hideSnackbar();
1138		}
1139	}
1140
1141	public void updateMessages() {
1142		synchronized (this.messageList) {
1143			if (getView() == null) {
1144				return;
1145			}
1146			final ConversationActivity activity = (ConversationActivity) getActivity();
1147			if (this.conversation != null) {
1148				conversation.populateWithMessages(ConversationFragment.this.messageList);
1149				updateSnackBar(conversation);
1150				updateStatusMessages();
1151				this.messageListAdapter.notifyDataSetChanged();
1152				updateChatMsgHint();
1153				if (!activity.isConversationsOverviewVisable() || !activity.isConversationsOverviewHideable()) {
1154					activity.sendReadMarkerIfNecessary(conversation);
1155				}
1156				updateSendButton();
1157				updateEditablity();
1158			}
1159		}
1160	}
1161
1162	protected void messageSent() {
1163		mSendingPgpMessage.set(false);
1164		mEditMessage.setText("");
1165		if (conversation.setCorrectingMessage(null)) {
1166			mEditMessage.append(conversation.getDraftMessage());
1167			conversation.setDraftMessage(null);
1168		}
1169		if (conversation.setNextMessage(mEditMessage.getText().toString())) {
1170			activity.xmppConnectionService.updateConversation(conversation);
1171		}
1172		updateChatMsgHint();
1173		new Handler().post(new Runnable() {
1174			@Override
1175			public void run() {
1176				int size = messageList.size();
1177				messagesView.setSelection(size - 1);
1178			}
1179		});
1180	}
1181
1182	public void setFocusOnInputField() {
1183		mEditMessage.requestFocus();
1184	}
1185
1186	public void doneSendingPgpMessage() {
1187		mSendingPgpMessage.set(false);
1188	}
1189
1190	enum SendButtonAction {TEXT, TAKE_PHOTO, SEND_LOCATION, RECORD_VOICE, CANCEL, CHOOSE_PICTURE, RECORD_VIDEO;
1191
1192		public static SendButtonAction valueOfOrDefault(String setting, SendButtonAction text) {
1193			try {
1194				return valueOf(setting);
1195			} catch (IllegalArgumentException e) {
1196				return TEXT;
1197			}
1198		}
1199	}
1200
1201	private int getSendButtonImageResource(SendButtonAction action, Presence.Status status) {
1202		switch (action) {
1203			case TEXT:
1204				switch (status) {
1205					case CHAT:
1206					case ONLINE:
1207						return R.drawable.ic_send_text_online;
1208					case AWAY:
1209						return R.drawable.ic_send_text_away;
1210					case XA:
1211					case DND:
1212						return R.drawable.ic_send_text_dnd;
1213					default:
1214						return activity.getThemeResource(R.attr.ic_send_text_offline, R.drawable.ic_send_text_offline);
1215				}
1216			case RECORD_VIDEO:
1217				switch (status) {
1218					case CHAT:
1219					case ONLINE:
1220						return R.drawable.ic_send_videocam_online;
1221					case AWAY:
1222						return R.drawable.ic_send_videocam_away;
1223					case XA:
1224					case DND:
1225						return R.drawable.ic_send_videocam_dnd;
1226					default:
1227						return activity.getThemeResource(R.attr.ic_send_videocam_offline, R.drawable.ic_send_videocam_offline);
1228				}
1229			case TAKE_PHOTO:
1230				switch (status) {
1231					case CHAT:
1232					case ONLINE:
1233						return R.drawable.ic_send_photo_online;
1234					case AWAY:
1235						return R.drawable.ic_send_photo_away;
1236					case XA:
1237					case DND:
1238						return R.drawable.ic_send_photo_dnd;
1239					default:
1240						return activity.getThemeResource(R.attr.ic_send_photo_offline, R.drawable.ic_send_photo_offline);
1241				}
1242			case RECORD_VOICE:
1243				switch (status) {
1244					case CHAT:
1245					case ONLINE:
1246						return R.drawable.ic_send_voice_online;
1247					case AWAY:
1248						return R.drawable.ic_send_voice_away;
1249					case XA:
1250					case DND:
1251						return R.drawable.ic_send_voice_dnd;
1252					default:
1253						return activity.getThemeResource(R.attr.ic_send_voice_offline, R.drawable.ic_send_voice_offline);
1254				}
1255			case SEND_LOCATION:
1256				switch (status) {
1257					case CHAT:
1258					case ONLINE:
1259						return R.drawable.ic_send_location_online;
1260					case AWAY:
1261						return R.drawable.ic_send_location_away;
1262					case XA:
1263					case DND:
1264						return R.drawable.ic_send_location_dnd;
1265					default:
1266						return activity.getThemeResource(R.attr.ic_send_location_offline, R.drawable.ic_send_location_offline);
1267				}
1268			case CANCEL:
1269				switch (status) {
1270					case CHAT:
1271					case ONLINE:
1272						return R.drawable.ic_send_cancel_online;
1273					case AWAY:
1274						return R.drawable.ic_send_cancel_away;
1275					case XA:
1276					case DND:
1277						return R.drawable.ic_send_cancel_dnd;
1278					default:
1279						return activity.getThemeResource(R.attr.ic_send_cancel_offline, R.drawable.ic_send_cancel_offline);
1280				}
1281			case CHOOSE_PICTURE:
1282				switch (status) {
1283					case CHAT:
1284					case ONLINE:
1285						return R.drawable.ic_send_picture_online;
1286					case AWAY:
1287						return R.drawable.ic_send_picture_away;
1288					case XA:
1289					case DND:
1290						return R.drawable.ic_send_picture_dnd;
1291					default:
1292						return activity.getThemeResource(R.attr.ic_send_picture_offline, R.drawable.ic_send_picture_offline);
1293				}
1294		}
1295		return activity.getThemeResource(R.attr.ic_send_text_offline, R.drawable.ic_send_text_offline);
1296	}
1297
1298	private void updateEditablity() {
1299		boolean canWrite = this.conversation.getMode() == Conversation.MODE_SINGLE || this.conversation.getMucOptions().participating() || this.conversation.getNextCounterpart() != null;
1300		this.mEditMessage.setFocusable(canWrite);
1301		this.mEditMessage.setFocusableInTouchMode(canWrite);
1302		this.mSendButton.setEnabled(canWrite);
1303		this.mEditMessage.setCursorVisible(canWrite);
1304	}
1305
1306	public void updateSendButton() {
1307		final Conversation c = this.conversation;
1308		final SendButtonAction action;
1309		final Presence.Status status;
1310		final String text = this.mEditMessage == null ? "" : this.mEditMessage.getText().toString();
1311		final boolean empty = text.length() == 0;
1312		final boolean conference = c.getMode() == Conversation.MODE_MULTI;
1313		if (c.getCorrectingMessage() != null && (empty || text.equals(c.getCorrectingMessage().getBody()))) {
1314			action = SendButtonAction.CANCEL;
1315		} else if (conference && !c.getAccount().httpUploadAvailable()) {
1316			if (empty && c.getNextCounterpart() != null) {
1317				action = SendButtonAction.CANCEL;
1318			} else {
1319				action = SendButtonAction.TEXT;
1320			}
1321		} else {
1322			if (empty) {
1323				if (conference && c.getNextCounterpart() != null) {
1324					action = SendButtonAction.CANCEL;
1325				} else {
1326					String setting = activity.getPreferences().getString("quick_action", activity.getResources().getString(R.string.quick_action));
1327					if (!setting.equals("none") && UIHelper.receivedLocationQuestion(conversation.getLatestMessage())) {
1328						action = SendButtonAction.SEND_LOCATION;
1329					} else {
1330						if (setting.equals("recent")) {
1331							setting = activity.getPreferences().getString(ConversationActivity.RECENTLY_USED_QUICK_ACTION, SendButtonAction.TEXT.toString());
1332							action = SendButtonAction.valueOfOrDefault(setting,SendButtonAction.TEXT);
1333						} else {
1334							action = SendButtonAction.valueOfOrDefault(setting,SendButtonAction.TEXT);
1335						}
1336					}
1337				}
1338			} else {
1339				action = SendButtonAction.TEXT;
1340			}
1341		}
1342		if (activity.useSendButtonToIndicateStatus() && c.getAccount().getStatus() == Account.State.ONLINE) {
1343			if (activity.xmppConnectionService != null && activity.xmppConnectionService.getMessageArchiveService().isCatchingUp(c)) {
1344				status = Presence.Status.OFFLINE;
1345			} else if (c.getMode() == Conversation.MODE_SINGLE) {
1346				status = c.getContact().getShownStatus();
1347			} else {
1348				status = c.getMucOptions().online() ? Presence.Status.ONLINE : Presence.Status.OFFLINE;
1349			}
1350		} else {
1351			status = Presence.Status.OFFLINE;
1352		}
1353		this.mSendButton.setTag(action);
1354		this.mSendButton.setImageResource(getSendButtonImageResource(action, status));
1355	}
1356
1357	protected void updateDateSeparators() {
1358		synchronized (this.messageList) {
1359			for(int i = 0; i < this.messageList.size(); ++i) {
1360				final Message current = this.messageList.get(i);
1361				if (i == 0 || !UIHelper.sameDay(this.messageList.get(i-1).getTimeSent(),current.getTimeSent())) {
1362					this.messageList.add(i,Message.createDateSeparator(current));
1363					i++;
1364				}
1365			}
1366		}
1367	}
1368
1369	protected void updateStatusMessages() {
1370		updateDateSeparators();
1371		synchronized (this.messageList) {
1372			if (showLoadMoreMessages(conversation)) {
1373				this.messageList.add(0, Message.createLoadMoreMessage(conversation));
1374			}
1375			if (conversation.getMode() == Conversation.MODE_SINGLE) {
1376				ChatState state = conversation.getIncomingChatState();
1377				if (state == ChatState.COMPOSING) {
1378					this.messageList.add(Message.createStatusMessage(conversation, getString(R.string.contact_is_typing, conversation.getName())));
1379				} else if (state == ChatState.PAUSED) {
1380					this.messageList.add(Message.createStatusMessage(conversation, getString(R.string.contact_has_stopped_typing, conversation.getName())));
1381				} else {
1382					for (int i = this.messageList.size() - 1; i >= 0; --i) {
1383						if (this.messageList.get(i).getStatus() == Message.STATUS_RECEIVED) {
1384							return;
1385						} else {
1386							if (this.messageList.get(i).getStatus() == Message.STATUS_SEND_DISPLAYED) {
1387								this.messageList.add(i + 1,
1388										Message.createStatusMessage(conversation, getString(R.string.contact_has_read_up_to_this_point, conversation.getName())));
1389								return;
1390							}
1391						}
1392					}
1393				}
1394			} else {
1395				ChatState state = ChatState.COMPOSING;
1396				List<MucOptions.User> users = conversation.getMucOptions().getUsersWithChatState(state,5);
1397				if (users.size() == 0) {
1398					state = ChatState.PAUSED;
1399					users = conversation.getMucOptions().getUsersWithChatState(state, 5);
1400
1401				}
1402				if (users.size() > 0) {
1403					Message statusMessage;
1404					if (users.size() == 1) {
1405						MucOptions.User user = users.get(0);
1406						int id = state == ChatState.COMPOSING ? R.string.contact_is_typing : R.string.contact_has_stopped_typing;
1407						statusMessage = Message.createStatusMessage(conversation, getString(id, UIHelper.getDisplayName(user)));
1408						statusMessage.setTrueCounterpart(user.getRealJid());
1409						statusMessage.setCounterpart(user.getFullJid());
1410					} else {
1411						StringBuilder builder = new StringBuilder();
1412						for(MucOptions.User user : users) {
1413							if (builder.length() != 0) {
1414								builder.append(", ");
1415							}
1416							builder.append(UIHelper.getDisplayName(user));
1417						}
1418						int id = state == ChatState.COMPOSING ? R.string.contacts_are_typing : R.string.contacts_have_stopped_typing;
1419						statusMessage = Message.createStatusMessage(conversation, getString(id, builder.toString()));
1420					}
1421					this.messageList.add(statusMessage);
1422				}
1423
1424			}
1425		}
1426	}
1427
1428	private boolean showLoadMoreMessages(final Conversation c) {
1429		final boolean mam = hasMamSupport(c);
1430		final MessageArchiveService service = activity.xmppConnectionService.getMessageArchiveService();
1431		return mam && (c.getLastClearHistory().getTimestamp() != 0  || (c.countMessages() == 0 && c.messagesLoaded.get() && c.hasMessagesLeftOnServer()  && !service.queryInProgress(c)));
1432	}
1433
1434	private boolean hasMamSupport(final Conversation c) {
1435		if (c.getMode() == Conversation.MODE_SINGLE) {
1436			final XmppConnection connection = c.getAccount().getXmppConnection();
1437			return connection != null && connection.getFeatures().mam();
1438		} else {
1439			return c.getMucOptions().mamSupport();
1440		}
1441	}
1442
1443	protected void showSnackbar(final int message, final int action, final OnClickListener clickListener) {
1444		showSnackbar(message,action,clickListener,null);
1445	}
1446
1447	protected void showSnackbar(final int message, final int action, final OnClickListener clickListener, final View.OnLongClickListener longClickListener) {
1448		snackbar.setVisibility(View.VISIBLE);
1449		snackbar.setOnClickListener(null);
1450		snackbarMessage.setText(message);
1451		snackbarMessage.setOnClickListener(null);
1452		snackbarAction.setVisibility(clickListener == null ? View.GONE : View.VISIBLE);
1453		if (action != 0) {
1454			snackbarAction.setText(action);
1455		}
1456		snackbarAction.setOnClickListener(clickListener);
1457		snackbarAction.setOnLongClickListener(longClickListener);
1458	}
1459
1460	protected void hideSnackbar() {
1461		snackbar.setVisibility(View.GONE);
1462	}
1463
1464	protected void sendPlainTextMessage(Message message) {
1465		ConversationActivity activity = (ConversationActivity) getActivity();
1466		activity.xmppConnectionService.sendMessage(message);
1467		messageSent();
1468	}
1469
1470	private AtomicBoolean mSendingPgpMessage = new AtomicBoolean(false);
1471
1472	protected void sendPgpMessage(final Message message) {
1473		final ConversationActivity activity = (ConversationActivity) getActivity();
1474		final XmppConnectionService xmppService = activity.xmppConnectionService;
1475		final Contact contact = message.getConversation().getContact();
1476		if (!activity.hasPgp()) {
1477			activity.showInstallPgpDialog();
1478			return;
1479		}
1480		if (conversation.getAccount().getPgpSignature() == null) {
1481			activity.announcePgp(conversation.getAccount(), conversation, activity.onOpenPGPKeyPublished);
1482			return;
1483		}
1484		if (!mSendingPgpMessage.compareAndSet(false,true)) {
1485			Log.d(Config.LOGTAG,"sending pgp message already in progress");
1486		}
1487		if (conversation.getMode() == Conversation.MODE_SINGLE) {
1488			if (contact.getPgpKeyId() != 0) {
1489				xmppService.getPgpEngine().hasKey(contact,
1490						new UiCallback<Contact>() {
1491
1492							@Override
1493							public void userInputRequried(PendingIntent pi,
1494														  Contact contact) {
1495								activity.runIntent(
1496										pi,
1497										ConversationActivity.REQUEST_ENCRYPT_MESSAGE);
1498							}
1499
1500							@Override
1501							public void success(Contact contact) {
1502								activity.encryptTextMessage(message);
1503							}
1504
1505							@Override
1506							public void error(int error, Contact contact) {
1507								activity.runOnUiThread(new Runnable() {
1508									@Override
1509									public void run() {
1510										Toast.makeText(activity,
1511												R.string.unable_to_connect_to_keychain,
1512												Toast.LENGTH_SHORT
1513										).show();
1514									}
1515								});
1516								mSendingPgpMessage.set(false);
1517							}
1518						});
1519
1520			} else {
1521				showNoPGPKeyDialog(false,
1522						new DialogInterface.OnClickListener() {
1523
1524							@Override
1525							public void onClick(DialogInterface dialog,
1526												int which) {
1527								conversation
1528										.setNextEncryption(Message.ENCRYPTION_NONE);
1529								xmppService.updateConversation(conversation);
1530								message.setEncryption(Message.ENCRYPTION_NONE);
1531								xmppService.sendMessage(message);
1532								messageSent();
1533							}
1534						});
1535			}
1536		} else {
1537			if (conversation.getMucOptions().pgpKeysInUse()) {
1538				if (!conversation.getMucOptions().everybodyHasKeys()) {
1539					Toast warning = Toast
1540							.makeText(getActivity(),
1541									R.string.missing_public_keys,
1542									Toast.LENGTH_LONG);
1543					warning.setGravity(Gravity.CENTER_VERTICAL, 0, 0);
1544					warning.show();
1545				}
1546				activity.encryptTextMessage(message);
1547			} else {
1548				showNoPGPKeyDialog(true,
1549						new DialogInterface.OnClickListener() {
1550
1551							@Override
1552							public void onClick(DialogInterface dialog,
1553												int which) {
1554								conversation
1555										.setNextEncryption(Message.ENCRYPTION_NONE);
1556								message.setEncryption(Message.ENCRYPTION_NONE);
1557								xmppService.updateConversation(conversation);
1558								xmppService.sendMessage(message);
1559								messageSent();
1560							}
1561						});
1562			}
1563		}
1564	}
1565
1566	public void showNoPGPKeyDialog(boolean plural,
1567								   DialogInterface.OnClickListener listener) {
1568		AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
1569		builder.setIconAttribute(android.R.attr.alertDialogIcon);
1570		if (plural) {
1571			builder.setTitle(getString(R.string.no_pgp_keys));
1572			builder.setMessage(getText(R.string.contacts_have_no_pgp_keys));
1573		} else {
1574			builder.setTitle(getString(R.string.no_pgp_key));
1575			builder.setMessage(getText(R.string.contact_has_no_pgp_key));
1576		}
1577		builder.setNegativeButton(getString(R.string.cancel), null);
1578		builder.setPositiveButton(getString(R.string.send_unencrypted),
1579				listener);
1580		builder.create().show();
1581	}
1582
1583	protected void sendAxolotlMessage(final Message message) {
1584		final ConversationActivity activity = (ConversationActivity) getActivity();
1585		final XmppConnectionService xmppService = activity.xmppConnectionService;
1586		xmppService.sendMessage(message);
1587		messageSent();
1588	}
1589
1590	protected void sendOtrMessage(final Message message) {
1591		final ConversationActivity activity = (ConversationActivity) getActivity();
1592		final XmppConnectionService xmppService = activity.xmppConnectionService;
1593		activity.selectPresence(message.getConversation(),
1594				new OnPresenceSelected() {
1595
1596					@Override
1597					public void onPresenceSelected() {
1598						message.setCounterpart(conversation.getNextCounterpart());
1599						xmppService.sendMessage(message);
1600						messageSent();
1601					}
1602				});
1603	}
1604
1605	public void appendText(String text) {
1606		if (text == null) {
1607			return;
1608		}
1609		String previous = this.mEditMessage.getText().toString();
1610		if (previous.length() != 0 && !previous.endsWith(" ")) {
1611			text = " " + text;
1612		}
1613		this.mEditMessage.append(text);
1614	}
1615
1616	@Override
1617	public boolean onEnterPressed() {
1618		if (activity.enterIsSend()) {
1619			sendMessage();
1620			return true;
1621		} else {
1622			return false;
1623		}
1624	}
1625
1626	@Override
1627	public void onTypingStarted() {
1628		Account.State status = conversation.getAccount().getStatus();
1629		if (status == Account.State.ONLINE && conversation.setOutgoingChatState(ChatState.COMPOSING)) {
1630			activity.xmppConnectionService.sendChatState(conversation);
1631		}
1632		activity.hideConversationsOverview();
1633		updateSendButton();
1634	}
1635
1636	@Override
1637	public void onTypingStopped() {
1638		Account.State status = conversation.getAccount().getStatus();
1639		if (status == Account.State.ONLINE && conversation.setOutgoingChatState(ChatState.PAUSED)) {
1640			activity.xmppConnectionService.sendChatState(conversation);
1641		}
1642	}
1643
1644	@Override
1645	public void onTextDeleted() {
1646		Account.State status = conversation.getAccount().getStatus();
1647		if (status == Account.State.ONLINE && conversation.setOutgoingChatState(Config.DEFAULT_CHATSTATE)) {
1648			activity.xmppConnectionService.sendChatState(conversation);
1649		}
1650		updateSendButton();
1651	}
1652
1653	@Override
1654	public void onTextChanged() {
1655		if (conversation != null && conversation.getCorrectingMessage() != null) {
1656			updateSendButton();
1657		}
1658	}
1659
1660	private int completionIndex = 0;
1661	private int lastCompletionLength = 0;
1662	private String incomplete;
1663	private int lastCompletionCursor;
1664	private boolean firstWord = false;
1665
1666	@Override
1667	public boolean onTabPressed(boolean repeated) {
1668		if (conversation == null || conversation.getMode() == Conversation.MODE_SINGLE) {
1669			return false;
1670		}
1671		if (repeated) {
1672			completionIndex++;
1673		} else {
1674			lastCompletionLength = 0;
1675			completionIndex = 0;
1676			final String content = mEditMessage.getText().toString();
1677			lastCompletionCursor = mEditMessage.getSelectionEnd();
1678			int start = lastCompletionCursor > 0 ? content.lastIndexOf(" ",lastCompletionCursor-1) + 1 : 0;
1679			firstWord = start == 0;
1680			incomplete = content.substring(start,lastCompletionCursor);
1681		}
1682		List<String> completions = new ArrayList<>();
1683		for(MucOptions.User user : conversation.getMucOptions().getUsers()) {
1684			String name = user.getName();
1685			if (name != null && name.startsWith(incomplete)) {
1686				completions.add(name+(firstWord ? ": " : " "));
1687			}
1688		}
1689		Collections.sort(completions);
1690		if (completions.size() > completionIndex) {
1691			String completion = completions.get(completionIndex).substring(incomplete.length());
1692			mEditMessage.getEditableText().delete(lastCompletionCursor,lastCompletionCursor + lastCompletionLength);
1693			mEditMessage.getEditableText().insert(lastCompletionCursor, completion);
1694			lastCompletionLength = completion.length();
1695		} else {
1696			completionIndex = -1;
1697			mEditMessage.getEditableText().delete(lastCompletionCursor,lastCompletionCursor + lastCompletionLength);
1698			lastCompletionLength = 0;
1699		}
1700		return true;
1701	}
1702
1703	@Override
1704	public void onActivityResult(int requestCode, int resultCode,
1705	                                final Intent data) {
1706		if (resultCode == Activity.RESULT_OK) {
1707			if (requestCode == ConversationActivity.REQUEST_DECRYPT_PGP) {
1708				activity.getSelectedConversation().getAccount().getPgpDecryptionService().continueDecryption(true);
1709			} else if (requestCode == ConversationActivity.REQUEST_TRUST_KEYS_TEXT) {
1710				final String body = mEditMessage.getText().toString();
1711				Message message = new Message(conversation, body, conversation.getNextEncryption());
1712				sendAxolotlMessage(message);
1713			} else if (requestCode == ConversationActivity.REQUEST_TRUST_KEYS_MENU) {
1714				int choice = data.getIntExtra("choice", ConversationActivity.ATTACHMENT_CHOICE_INVALID);
1715				activity.selectPresenceToAttachFile(choice, conversation.getNextEncryption());
1716			}
1717		} else if (resultCode == Activity.RESULT_CANCELED) {
1718			if (requestCode == ConversationActivity.REQUEST_DECRYPT_PGP) {
1719				// discard the message to prevent decryption being blocked
1720				conversation.getAccount().getPgpDecryptionService().giveUpCurrentDecryption();
1721			}
1722		}
1723	}
1724
1725}