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