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