ConversationFragment.java

  1package eu.siacs.conversations.ui;
  2
  3import java.util.ArrayList;
  4import java.util.List;
  5import java.util.Set;
  6
  7import net.java.otr4j.session.SessionStatus;
  8import eu.siacs.conversations.R;
  9import eu.siacs.conversations.crypto.PgpEngine;
 10import eu.siacs.conversations.entities.Account;
 11import eu.siacs.conversations.entities.Contact;
 12import eu.siacs.conversations.entities.Conversation;
 13import eu.siacs.conversations.entities.Message;
 14import eu.siacs.conversations.entities.MucOptions;
 15import eu.siacs.conversations.services.XmppConnectionService;
 16import eu.siacs.conversations.ui.EditMessage.OnEnterPressed;
 17import eu.siacs.conversations.ui.XmppActivity.OnPresenceSelected;
 18import eu.siacs.conversations.ui.adapter.MessageAdapter;
 19import eu.siacs.conversations.ui.adapter.MessageAdapter.OnContactPictureClicked;
 20import eu.siacs.conversations.ui.adapter.MessageAdapter.OnContactPictureLongClicked;
 21import eu.siacs.conversations.utils.UIHelper;
 22import android.app.AlertDialog;
 23import android.app.Fragment;
 24import android.app.PendingIntent;
 25import android.content.Context;
 26import android.content.DialogInterface;
 27import android.content.Intent;
 28import android.content.IntentSender;
 29import android.content.IntentSender.SendIntentException;
 30import android.os.Bundle;
 31import android.text.Editable;
 32import android.text.Selection;
 33import android.view.Gravity;
 34import android.view.KeyEvent;
 35import android.view.LayoutInflater;
 36import android.view.View;
 37import android.view.View.OnClickListener;
 38import android.view.ViewGroup;
 39import android.view.inputmethod.EditorInfo;
 40import android.view.inputmethod.InputMethodManager;
 41import android.widget.AbsListView.OnScrollListener;
 42import android.widget.TextView.OnEditorActionListener;
 43import android.widget.AbsListView;
 44
 45import android.widget.ListView;
 46import android.widget.ImageButton;
 47import android.widget.RelativeLayout;
 48import android.widget.TextView;
 49import android.widget.Toast;
 50
 51public class ConversationFragment extends Fragment {
 52
 53	protected Conversation conversation;
 54	protected ListView messagesView;
 55	protected LayoutInflater inflater;
 56	protected List<Message> messageList = new ArrayList<Message>();
 57	protected MessageAdapter messageListAdapter;
 58	protected Contact contact;
 59
 60	protected String queuedPqpMessage = null;
 61
 62	private EditMessage mEditMessage;
 63	private String pastedText = null;
 64	private RelativeLayout snackbar;
 65	private TextView snackbarMessage;
 66	private TextView snackbarAction;
 67
 68	private boolean messagesLoaded = false;
 69
 70	private IntentSender askForPassphraseIntent = null;
 71
 72	private OnEditorActionListener mEditorActionListener = new OnEditorActionListener() {
 73
 74		@Override
 75		public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
 76			if (actionId == EditorInfo.IME_ACTION_DONE) {
 77				InputMethodManager imm = (InputMethodManager) v.getContext()
 78						.getSystemService(Context.INPUT_METHOD_SERVICE);
 79				imm.hideSoftInputFromWindow(v.getWindowToken(), 0);
 80				return true;
 81			} else {
 82				return false;
 83			}
 84		}
 85	};
 86
 87	private OnClickListener mSendButtonListener = new OnClickListener() {
 88
 89		@Override
 90		public void onClick(View v) {
 91			sendMessage();
 92		}
 93	};
 94	protected OnClickListener clickToDecryptListener = new OnClickListener() {
 95
 96		@Override
 97		public void onClick(View v) {
 98			if (activity.hasPgp() && askForPassphraseIntent != null) {
 99				try {
100					getActivity().startIntentSenderForResult(
101							askForPassphraseIntent,
102							ConversationActivity.REQUEST_DECRYPT_PGP, null, 0,
103							0, 0);
104				} catch (SendIntentException e) {
105					//
106				}
107			}
108		}
109	};
110
111	private OnClickListener clickToMuc = new OnClickListener() {
112
113		@Override
114		public void onClick(View v) {
115			Intent intent = new Intent(getActivity(),
116					ConferenceDetailsActivity.class);
117			intent.setAction(ConferenceDetailsActivity.ACTION_VIEW_MUC);
118			intent.putExtra("uuid", conversation.getUuid());
119			startActivity(intent);
120		}
121	};
122
123	private OnClickListener leaveMuc = new OnClickListener() {
124
125		@Override
126		public void onClick(View v) {
127			activity.endConversation(conversation);
128		}
129	};
130
131	private OnScrollListener mOnScrollListener = new OnScrollListener() {
132
133		@Override
134		public void onScrollStateChanged(AbsListView view, int scrollState) {
135			// TODO Auto-generated method stub
136
137		}
138
139		@Override
140		public void onScroll(AbsListView view, int firstVisibleItem,
141				int visibleItemCount, int totalItemCount) {
142			if (firstVisibleItem == 0 && messagesLoaded) {
143				long timestamp = messageList.get(0).getTimeSent();
144				messagesLoaded = false;
145				List<Message> messages = activity.xmppConnectionService
146						.getMoreMessages(conversation, timestamp);
147				messageList.addAll(0, messages);
148				messageListAdapter.notifyDataSetChanged();
149				if (messages.size() != 0) {
150					messagesLoaded = true;
151				}
152				messagesView.setSelectionFromTop(messages.size() + 1, 0);
153			}
154		}
155	};
156
157	private ConversationActivity activity;
158
159	private void sendMessage() {
160		if (mEditMessage.getText().length() < 1) {
161			if (this.conversation.getMode() == Conversation.MODE_MULTI) {
162				conversation.setNextPresence(null);
163				updateChatMsgHint();
164			}
165			return;
166		}
167		Message message = new Message(conversation, mEditMessage.getText()
168				.toString(), conversation.getNextEncryption());
169		if (conversation.getMode() == Conversation.MODE_MULTI) {
170			if (conversation.getNextPresence() != null) {
171				message.setPresence(conversation.getNextPresence());
172				message.setType(Message.TYPE_PRIVATE);
173				conversation.setNextPresence(null);
174			}
175		}
176		if (conversation.getNextEncryption() == Message.ENCRYPTION_OTR) {
177			sendOtrMessage(message);
178		} else if (conversation.getNextEncryption() == Message.ENCRYPTION_PGP) {
179			sendPgpMessage(message);
180		} else {
181			sendPlainTextMessage(message);
182		}
183	}
184
185	public void updateChatMsgHint() {
186		if (conversation.getMode() == Conversation.MODE_MULTI
187				&& conversation.getNextPresence() != null) {
188			this.mEditMessage.setHint(getString(
189					R.string.send_private_message_to,
190					conversation.getNextPresence()));
191		} else {
192			switch (conversation.getNextEncryption()) {
193			case Message.ENCRYPTION_NONE:
194				mEditMessage
195						.setHint(getString(R.string.send_plain_text_message));
196				break;
197			case Message.ENCRYPTION_OTR:
198				mEditMessage.setHint(getString(R.string.send_otr_message));
199				break;
200			case Message.ENCRYPTION_PGP:
201				mEditMessage.setHint(getString(R.string.send_pgp_message));
202				break;
203			default:
204				break;
205			}
206		}
207	}
208
209	@Override
210	public View onCreateView(final LayoutInflater inflater,
211			ViewGroup container, Bundle savedInstanceState) {
212		final View view = inflater.inflate(R.layout.fragment_conversation,
213				container, false);
214		mEditMessage = (EditMessage) view.findViewById(R.id.textinput);
215		mEditMessage.setOnClickListener(new OnClickListener() {
216
217			@Override
218			public void onClick(View v) {
219				if (activity.getSlidingPaneLayout().isSlideable()) {
220					activity.getSlidingPaneLayout().closePane();
221				}
222			}
223		});
224		mEditMessage.setOnEditorActionListener(mEditorActionListener);
225		mEditMessage.setOnEnterPressedListener(new OnEnterPressed() {
226
227			@Override
228			public void onEnterPressed() {
229				sendMessage();
230			}
231		});
232
233		ImageButton sendButton = (ImageButton) view
234				.findViewById(R.id.textSendButton);
235		sendButton.setOnClickListener(this.mSendButtonListener);
236
237		snackbar = (RelativeLayout) view.findViewById(R.id.snackbar);
238		snackbarMessage = (TextView) view.findViewById(R.id.snackbar_message);
239		snackbarAction = (TextView) view.findViewById(R.id.snackbar_action);
240
241		messagesView = (ListView) view.findViewById(R.id.messages_view);
242		messagesView.setOnScrollListener(mOnScrollListener);
243		messagesView.setTranscriptMode(ListView.TRANSCRIPT_MODE_NORMAL);
244		messageListAdapter = new MessageAdapter(
245				(ConversationActivity) getActivity(), this.messageList);
246		messageListAdapter
247				.setOnContactPictureClicked(new OnContactPictureClicked() {
248
249					@Override
250					public void onContactPictureClicked(Message message) {
251						if (message.getConversation().getMode() == Conversation.MODE_MULTI) {
252							if (message.getPresence() != null) {
253								highlightInConference(message.getPresence());
254							} else {
255								highlightInConference(message.getCounterpart());
256							}
257						}
258					}
259				});
260		messageListAdapter
261				.setOnContactPictureLongClicked(new OnContactPictureLongClicked() {
262
263					@Override
264					public void onContactPictureLongClicked(Message message) {
265						if (message.getConversation().getMode() == Conversation.MODE_MULTI) {
266							if (message.getPresence() != null) {
267								privateMessageWith(message.getPresence());
268							} else {
269								privateMessageWith(message.getCounterpart());
270							}
271						}
272					}
273				});
274		messagesView.setAdapter(messageListAdapter);
275
276		return view;
277	}
278
279	protected void privateMessageWith(String counterpart) {
280		this.mEditMessage.setText("");
281		this.conversation.setNextPresence(counterpart);
282		updateChatMsgHint();
283	}
284
285	protected void highlightInConference(String nick) {
286		String oldString = mEditMessage.getText().toString().trim();
287		if (oldString.isEmpty() || mEditMessage.getSelectionStart() == 0) {
288			mEditMessage.getText().insert(0, nick + ": ");
289		} else {
290			if (mEditMessage.getText().charAt(mEditMessage.getSelectionStart()-1)!=' ') {
291				nick = " "+nick;
292			}
293			mEditMessage.getText().insert(mEditMessage.getSelectionStart(), nick + " ");
294		}
295	}
296
297	@Override
298	public void onStart() {
299		super.onStart();
300		this.activity = (ConversationActivity) getActivity();
301		if (activity.xmppConnectionServiceBound) {
302			this.onBackendConnected();
303		}
304	}
305
306	@Override
307	public void onStop() {
308		super.onStop();
309		if (this.conversation != null) {
310			this.conversation.setNextMessage(mEditMessage.getText().toString());
311		}
312	}
313
314	public void onBackendConnected() {
315		this.activity = (ConversationActivity) getActivity();
316		this.conversation = activity.getSelectedConversation();
317		if (this.conversation == null) {
318			return;
319		}
320		String oldString = conversation.getNextMessage().trim();
321		if (this.pastedText == null) {
322			this.mEditMessage.setText(oldString);
323		} else {
324
325			if (oldString.isEmpty()) {
326				mEditMessage.setText(pastedText);
327			} else {
328				mEditMessage.setText(oldString + " " + pastedText);
329			}
330			pastedText = null;
331		}
332		int position = mEditMessage.length();
333		Editable etext = mEditMessage.getText();
334		Selection.setSelection(etext, position);
335		if (activity.getSlidingPaneLayout().isSlideable()) {
336			if (!activity.shouldPaneBeOpen()) {
337				activity.getSlidingPaneLayout().closePane();
338				activity.getActionBar().setDisplayHomeAsUpEnabled(true);
339				activity.getActionBar().setHomeButtonEnabled(true);
340				activity.getActionBar().setTitle(
341						conversation.getName());
342				activity.invalidateOptionsMenu();
343			}
344		}
345		if (this.conversation.getMode() == Conversation.MODE_MULTI) {
346			conversation.setNextPresence(null);
347		}
348		updateMessages();
349	}
350
351	private void decryptMessage(Message message) {
352		PgpEngine engine = activity.xmppConnectionService.getPgpEngine();
353		if (engine != null) {
354			engine.decrypt(message, new UiCallback<Message>() {
355
356				@Override
357				public void userInputRequried(PendingIntent pi, Message message) {
358					askForPassphraseIntent = pi.getIntentSender();
359					showSnackbar(R.string.openpgp_messages_found,
360							R.string.decrypt, clickToDecryptListener);
361				}
362
363				@Override
364				public void success(Message message) {
365					activity.xmppConnectionService.databaseBackend
366							.updateMessage(message);
367					updateMessages();
368				}
369
370				@Override
371				public void error(int error, Message message) {
372					message.setEncryption(Message.ENCRYPTION_DECRYPTION_FAILED);
373					// updateMessages();
374				}
375			});
376		}
377	}
378
379	public void updateMessages() {
380		if (getView() == null) {
381			return;
382		}
383		hideSnackbar();
384		final ConversationActivity activity = (ConversationActivity) getActivity();
385		if (this.conversation != null) {
386			final Contact contact = this.conversation.getContact();
387			if (!contact.showInRoster()
388					&& contact
389							.getOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST)) {
390				showSnackbar(R.string.contact_added_you, R.string.add_back,
391						new OnClickListener() {
392
393							@Override
394							public void onClick(View v) {
395								activity.xmppConnectionService
396										.createContact(contact);
397								activity.switchToContactDetails(contact);
398							}
399						});
400			}
401			for (Message message : this.conversation.getMessages()) {
402				if ((message.getEncryption() == Message.ENCRYPTION_PGP)
403						&& ((message.getStatus() == Message.STATUS_RECEIVED) || (message
404								.getStatus() == Message.STATUS_SEND))) {
405					decryptMessage(message);
406					break;
407				}
408			}
409			if (this.conversation.getMessages().size() == 0) {
410				this.messageList.clear();
411				messagesLoaded = false;
412			} else {
413				for (Message message : this.conversation.getMessages()) {
414					if (!this.messageList.contains(message)) {
415						this.messageList.add(message);
416					}
417				}
418				messagesLoaded = true;
419				updateStatusMessages();
420			}
421			this.messageListAdapter.notifyDataSetChanged();
422			if (conversation.getMode() == Conversation.MODE_SINGLE) {
423				if (messageList.size() >= 1) {
424					makeFingerprintWarning(conversation.getLatestEncryption());
425				}
426			} else {
427				if (!conversation.getMucOptions().online()
428						&& conversation.getAccount().getStatus() == Account.STATUS_ONLINE) {
429					if (conversation.getMucOptions().getError() == MucOptions.ERROR_NICK_IN_USE) {
430						showSnackbar(R.string.nick_in_use, R.string.edit,
431								clickToMuc);
432					} else if (conversation.getMucOptions().getError() == MucOptions.ERROR_ROOM_NOT_FOUND) {
433						showSnackbar(R.string.conference_not_found,
434								R.string.leave, leaveMuc);
435					}
436				}
437			}
438			getActivity().invalidateOptionsMenu();
439			updateChatMsgHint();
440			if (!activity.shouldPaneBeOpen()) {
441				activity.xmppConnectionService.markRead(conversation);
442				// TODO update notifications
443				UIHelper.updateNotification(getActivity(),
444						activity.getConversationList(), null, false);
445				activity.updateConversationList();
446			}
447		}
448	}
449
450	private void messageSent() {
451		int size = this.messageList.size();
452		if (size >= 1) {
453			messagesView.setSelection(size - 1);
454		}
455		mEditMessage.setText("");
456		updateChatMsgHint();
457	}
458
459	protected void updateStatusMessages() {
460		boolean addedStatusMsg = false;
461		if (conversation.getMode() == Conversation.MODE_SINGLE) {
462			for (int i = this.messageList.size() - 1; i >= 0; --i) {
463				if (addedStatusMsg) {
464					if (this.messageList.get(i).getType() == Message.TYPE_STATUS) {
465						this.messageList.remove(i);
466						--i;
467					}
468				} else {
469					if (this.messageList.get(i).getStatus() == Message.STATUS_RECEIVED) {
470						addedStatusMsg = true;
471					} else {
472						if (this.messageList.get(i).getStatus() == Message.STATUS_SEND_DISPLAYED) {
473							this.messageList.add(i + 1,
474									Message.createStatusMessage(conversation));
475							addedStatusMsg = true;
476						}
477					}
478				}
479			}
480		}
481	}
482
483	protected void makeFingerprintWarning(int latestEncryption) {
484		Set<String> knownFingerprints = conversation.getContact()
485				.getOtrFingerprints();
486		if ((latestEncryption == Message.ENCRYPTION_OTR)
487				&& (conversation.hasValidOtrSession()
488						&& (conversation.getOtrSession().getSessionStatus() == SessionStatus.ENCRYPTED) && (!knownFingerprints
489							.contains(conversation.getOtrFingerprint())))) {
490			showSnackbar(R.string.unknown_otr_fingerprint, R.string.verify,
491					new OnClickListener() {
492
493						@Override
494						public void onClick(View v) {
495							if (conversation.getOtrFingerprint() != null) {
496								AlertDialog dialog = UIHelper
497										.getVerifyFingerprintDialog(
498												(ConversationActivity) getActivity(),
499												conversation, snackbar);
500								dialog.show();
501							}
502						}
503					});
504		}
505	}
506
507	protected void showSnackbar(int message, int action,
508			OnClickListener clickListener) {
509		snackbar.setVisibility(View.VISIBLE);
510		snackbar.setOnClickListener(null);
511		snackbarMessage.setText(message);
512		snackbarMessage.setOnClickListener(null);
513		snackbarAction.setText(action);
514		snackbarAction.setOnClickListener(clickListener);
515	}
516
517	protected void hideSnackbar() {
518		snackbar.setVisibility(View.GONE);
519	}
520
521	protected void sendPlainTextMessage(Message message) {
522		ConversationActivity activity = (ConversationActivity) getActivity();
523		activity.xmppConnectionService.sendMessage(message);
524		messageSent();
525	}
526
527	protected void sendPgpMessage(final Message message) {
528		final ConversationActivity activity = (ConversationActivity) getActivity();
529		final XmppConnectionService xmppService = activity.xmppConnectionService;
530		final Contact contact = message.getConversation().getContact();
531		if (activity.hasPgp()) {
532			if (conversation.getMode() == Conversation.MODE_SINGLE) {
533				if (contact.getPgpKeyId() != 0) {
534					xmppService.getPgpEngine().hasKey(contact,
535							new UiCallback<Contact>() {
536
537								@Override
538								public void userInputRequried(PendingIntent pi,
539										Contact contact) {
540									activity.runIntent(
541											pi,
542											ConversationActivity.REQUEST_ENCRYPT_MESSAGE);
543								}
544
545								@Override
546								public void success(Contact contact) {
547									messageSent();
548									activity.encryptTextMessage(message);
549								}
550
551								@Override
552								public void error(int error, Contact contact) {
553
554								}
555							});
556
557				} else {
558					showNoPGPKeyDialog(false,
559							new DialogInterface.OnClickListener() {
560
561								@Override
562								public void onClick(DialogInterface dialog,
563										int which) {
564									conversation
565											.setNextEncryption(Message.ENCRYPTION_NONE);
566									message.setEncryption(Message.ENCRYPTION_NONE);
567									xmppService.sendMessage(message);
568									messageSent();
569								}
570							});
571				}
572			} else {
573				if (conversation.getMucOptions().pgpKeysInUse()) {
574					if (!conversation.getMucOptions().everybodyHasKeys()) {
575						Toast warning = Toast
576								.makeText(getActivity(),
577										R.string.missing_public_keys,
578										Toast.LENGTH_LONG);
579						warning.setGravity(Gravity.CENTER_VERTICAL, 0, 0);
580						warning.show();
581					}
582					activity.encryptTextMessage(message);
583					messageSent();
584				} else {
585					showNoPGPKeyDialog(true,
586							new DialogInterface.OnClickListener() {
587
588								@Override
589								public void onClick(DialogInterface dialog,
590										int which) {
591									conversation
592											.setNextEncryption(Message.ENCRYPTION_NONE);
593									message.setEncryption(Message.ENCRYPTION_NONE);
594									xmppService.sendMessage(message);
595									messageSent();
596								}
597							});
598				}
599			}
600		} else {
601			activity.showInstallPgpDialog();
602		}
603	}
604
605	public void showNoPGPKeyDialog(boolean plural,
606			DialogInterface.OnClickListener listener) {
607		AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
608		builder.setIconAttribute(android.R.attr.alertDialogIcon);
609		if (plural) {
610			builder.setTitle(getString(R.string.no_pgp_keys));
611			builder.setMessage(getText(R.string.contacts_have_no_pgp_keys));
612		} else {
613			builder.setTitle(getString(R.string.no_pgp_key));
614			builder.setMessage(getText(R.string.contact_has_no_pgp_key));
615		}
616		builder.setNegativeButton(getString(R.string.cancel), null);
617		builder.setPositiveButton(getString(R.string.send_unencrypted),
618				listener);
619		builder.create().show();
620	}
621
622	protected void sendOtrMessage(final Message message) {
623		final ConversationActivity activity = (ConversationActivity) getActivity();
624		final XmppConnectionService xmppService = activity.xmppConnectionService;
625		if (conversation.hasValidOtrSession()) {
626			activity.xmppConnectionService.sendMessage(message);
627			messageSent();
628		} else {
629			activity.selectPresence(message.getConversation(),
630					new OnPresenceSelected() {
631
632						@Override
633						public void onPresenceSelected() {
634							message.setPresence(conversation.getNextPresence());
635							xmppService.sendMessage(message);
636							messageSent();
637						}
638					});
639		}
640	}
641
642	public void setText(String text) {
643		this.pastedText = text;
644	}
645
646	public void clearInputField() {
647		this.mEditMessage.setText("");
648	}
649}