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.isMuted()) {
410				showSnackbar(R.string.notifications_disabled, R.string.enable, new OnClickListener() {
411					
412					@Override
413					public void onClick(View v) {
414						conversation.setMutedTill(0);
415						updateMessages();
416					}
417				});
418			}
419			if (this.conversation.getMessages().size() == 0) {
420				this.messageList.clear();
421				messagesLoaded = false;
422			} else {
423				for (Message message : this.conversation.getMessages()) {
424					if (!this.messageList.contains(message)) {
425						this.messageList.add(message);
426					}
427				}
428				messagesLoaded = true;
429				updateStatusMessages();
430			}
431			this.messageListAdapter.notifyDataSetChanged();
432			if (conversation.getMode() == Conversation.MODE_SINGLE) {
433				if (messageList.size() >= 1) {
434					makeFingerprintWarning(conversation.getLatestEncryption());
435				}
436			} else {
437				if (!conversation.getMucOptions().online()
438						&& conversation.getAccount().getStatus() == Account.STATUS_ONLINE) {
439					if (conversation.getMucOptions().getError() == MucOptions.ERROR_NICK_IN_USE) {
440						showSnackbar(R.string.nick_in_use, R.string.edit,
441								clickToMuc);
442					} else if (conversation.getMucOptions().getError() == MucOptions.ERROR_ROOM_NOT_FOUND) {
443						showSnackbar(R.string.conference_not_found,
444								R.string.leave, leaveMuc);
445					}
446				}
447			}
448			getActivity().invalidateOptionsMenu();
449			updateChatMsgHint();
450			if (!activity.shouldPaneBeOpen()) {
451				activity.xmppConnectionService.markRead(conversation);
452				// TODO update notifications
453				UIHelper.updateNotification(getActivity(),
454						activity.getConversationList(), null, false);
455				activity.updateConversationList();
456			}
457		}
458	}
459
460	private void messageSent() {
461		int size = this.messageList.size();
462		if (size >= 1) {
463			messagesView.setSelection(size - 1);
464		}
465		mEditMessage.setText("");
466		updateChatMsgHint();
467	}
468
469	protected void updateStatusMessages() {
470		boolean addedStatusMsg = false;
471		if (conversation.getMode() == Conversation.MODE_SINGLE) {
472			for (int i = this.messageList.size() - 1; i >= 0; --i) {
473				if (addedStatusMsg) {
474					if (this.messageList.get(i).getType() == Message.TYPE_STATUS) {
475						this.messageList.remove(i);
476						--i;
477					}
478				} else {
479					if (this.messageList.get(i).getStatus() == Message.STATUS_RECEIVED) {
480						addedStatusMsg = true;
481					} else {
482						if (this.messageList.get(i).getStatus() == Message.STATUS_SEND_DISPLAYED) {
483							this.messageList.add(i + 1,
484									Message.createStatusMessage(conversation));
485							addedStatusMsg = true;
486						}
487					}
488				}
489			}
490		}
491	}
492
493	protected void makeFingerprintWarning(int latestEncryption) {
494		Set<String> knownFingerprints = conversation.getContact()
495				.getOtrFingerprints();
496		if ((latestEncryption == Message.ENCRYPTION_OTR)
497				&& (conversation.hasValidOtrSession()
498						&& (conversation.getOtrSession().getSessionStatus() == SessionStatus.ENCRYPTED) && (!knownFingerprints
499							.contains(conversation.getOtrFingerprint())))) {
500			showSnackbar(R.string.unknown_otr_fingerprint, R.string.verify,
501					new OnClickListener() {
502
503						@Override
504						public void onClick(View v) {
505							if (conversation.getOtrFingerprint() != null) {
506								AlertDialog dialog = UIHelper
507										.getVerifyFingerprintDialog(
508												(ConversationActivity) getActivity(),
509												conversation, snackbar);
510								dialog.show();
511							}
512						}
513					});
514		}
515	}
516
517	protected void showSnackbar(int message, int action,
518			OnClickListener clickListener) {
519		snackbar.setVisibility(View.VISIBLE);
520		snackbar.setOnClickListener(null);
521		snackbarMessage.setText(message);
522		snackbarMessage.setOnClickListener(null);
523		snackbarAction.setText(action);
524		snackbarAction.setOnClickListener(clickListener);
525	}
526
527	protected void hideSnackbar() {
528		snackbar.setVisibility(View.GONE);
529	}
530
531	protected void sendPlainTextMessage(Message message) {
532		ConversationActivity activity = (ConversationActivity) getActivity();
533		activity.xmppConnectionService.sendMessage(message);
534		messageSent();
535	}
536
537	protected void sendPgpMessage(final Message message) {
538		final ConversationActivity activity = (ConversationActivity) getActivity();
539		final XmppConnectionService xmppService = activity.xmppConnectionService;
540		final Contact contact = message.getConversation().getContact();
541		if (activity.hasPgp()) {
542			if (conversation.getMode() == Conversation.MODE_SINGLE) {
543				if (contact.getPgpKeyId() != 0) {
544					xmppService.getPgpEngine().hasKey(contact,
545							new UiCallback<Contact>() {
546
547								@Override
548								public void userInputRequried(PendingIntent pi,
549										Contact contact) {
550									activity.runIntent(
551											pi,
552											ConversationActivity.REQUEST_ENCRYPT_MESSAGE);
553								}
554
555								@Override
556								public void success(Contact contact) {
557									messageSent();
558									activity.encryptTextMessage(message);
559								}
560
561								@Override
562								public void error(int error, Contact contact) {
563
564								}
565							});
566
567				} else {
568					showNoPGPKeyDialog(false,
569							new DialogInterface.OnClickListener() {
570
571								@Override
572								public void onClick(DialogInterface dialog,
573										int which) {
574									conversation
575											.setNextEncryption(Message.ENCRYPTION_NONE);
576									message.setEncryption(Message.ENCRYPTION_NONE);
577									xmppService.sendMessage(message);
578									messageSent();
579								}
580							});
581				}
582			} else {
583				if (conversation.getMucOptions().pgpKeysInUse()) {
584					if (!conversation.getMucOptions().everybodyHasKeys()) {
585						Toast warning = Toast
586								.makeText(getActivity(),
587										R.string.missing_public_keys,
588										Toast.LENGTH_LONG);
589						warning.setGravity(Gravity.CENTER_VERTICAL, 0, 0);
590						warning.show();
591					}
592					activity.encryptTextMessage(message);
593					messageSent();
594				} else {
595					showNoPGPKeyDialog(true,
596							new DialogInterface.OnClickListener() {
597
598								@Override
599								public void onClick(DialogInterface dialog,
600										int which) {
601									conversation
602											.setNextEncryption(Message.ENCRYPTION_NONE);
603									message.setEncryption(Message.ENCRYPTION_NONE);
604									xmppService.sendMessage(message);
605									messageSent();
606								}
607							});
608				}
609			}
610		} else {
611			activity.showInstallPgpDialog();
612		}
613	}
614
615	public void showNoPGPKeyDialog(boolean plural,
616			DialogInterface.OnClickListener listener) {
617		AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
618		builder.setIconAttribute(android.R.attr.alertDialogIcon);
619		if (plural) {
620			builder.setTitle(getString(R.string.no_pgp_keys));
621			builder.setMessage(getText(R.string.contacts_have_no_pgp_keys));
622		} else {
623			builder.setTitle(getString(R.string.no_pgp_key));
624			builder.setMessage(getText(R.string.contact_has_no_pgp_key));
625		}
626		builder.setNegativeButton(getString(R.string.cancel), null);
627		builder.setPositiveButton(getString(R.string.send_unencrypted),
628				listener);
629		builder.create().show();
630	}
631
632	protected void sendOtrMessage(final Message message) {
633		final ConversationActivity activity = (ConversationActivity) getActivity();
634		final XmppConnectionService xmppService = activity.xmppConnectionService;
635		if (conversation.hasValidOtrSession()) {
636			activity.xmppConnectionService.sendMessage(message);
637			messageSent();
638		} else {
639			activity.selectPresence(message.getConversation(),
640					new OnPresenceSelected() {
641
642						@Override
643						public void onPresenceSelected() {
644							message.setPresence(conversation.getNextPresence());
645							xmppService.sendMessage(message);
646							messageSent();
647						}
648					});
649		}
650	}
651
652	public void setText(String text) {
653		this.pastedText = text;
654	}
655
656	public void clearInputField() {
657		this.mEditMessage.setText("");
658	}
659}