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