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