ConversationFragment.java

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