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	
163	private void sendMessage() {
164		if (mEditMessage.getText().length() < 1) {
165			if (this.conversation.getMode() == Conversation.MODE_MULTI) {
166				conversation.setNextPresence(null);
167				updateChatMsgHint();
168			}
169			return;
170		}
171		Message message = new Message(conversation, mEditMessage.getText()
172				.toString(), conversation.getNextEncryption());
173		if (conversation.getMode() == Conversation.MODE_MULTI) {
174			if (conversation.getNextPresence() != null) {
175				message.setPresence(conversation.getNextPresence());
176				message.setType(Message.TYPE_PRIVATE);
177				conversation.setNextPresence(null);
178			}
179		}
180		if (conversation.getNextEncryption() == Message.ENCRYPTION_OTR) {
181			sendOtrMessage(message);
182		} else if (conversation.getNextEncryption() == Message.ENCRYPTION_PGP) {
183			sendPgpMessage(message);
184		} else {
185			sendPlainTextMessage(message);
186		}
187	}
188
189	public void updateChatMsgHint() {
190		switch (conversation.getNextEncryption()) {
191		case Message.ENCRYPTION_NONE:
192			mEditMessage.setHint(getString(R.string.send_plain_text_message));
193			break;
194		case Message.ENCRYPTION_OTR:
195			mEditMessage.setHint(getString(R.string.send_otr_message));
196			break;
197		case Message.ENCRYPTION_PGP:
198			mEditMessage.setHint(getString(R.string.send_pgp_message));
199			break;
200		default:
201			break;
202		}
203	}
204
205	@Override
206	public View onCreateView(final LayoutInflater inflater,
207			ViewGroup container, Bundle savedInstanceState) {
208		final View view = inflater.inflate(R.layout.fragment_conversation,
209				container, false);
210		mEditMessage = (EditMessage) view.findViewById(R.id.textinput);
211		mEditMessage.setOnClickListener(new OnClickListener() {
212
213			@Override
214			public void onClick(View v) {
215				if (activity.getSlidingPaneLayout().isSlideable()) {
216					activity.getSlidingPaneLayout().closePane();
217				}
218			}
219		});
220		mEditMessage.setOnEditorActionListener(mEditorActionListener);
221		mEditMessage.setOnEnterPressedListener(new OnEnterPressed() {
222			
223			@Override
224			public void onEnterPressed() {
225				sendMessage();
226			}
227		});
228		
229		ImageButton sendButton = (ImageButton) view
230				.findViewById(R.id.textSendButton);
231		sendButton.setOnClickListener(this.mSendButtonListener);
232
233		snackbar = (RelativeLayout) view.findViewById(R.id.snackbar);
234		snackbarMessage = (TextView) view.findViewById(R.id.snackbar_message);
235		snackbarAction = (TextView) view.findViewById(R.id.snackbar_action);
236
237		messagesView = (ListView) view.findViewById(R.id.messages_view);
238		messagesView.setOnScrollListener(mOnScrollListener);
239		messagesView.setTranscriptMode(ListView.TRANSCRIPT_MODE_NORMAL);
240		messageListAdapter = new MessageAdapter((ConversationActivity) getActivity(), this.messageList);
241		messageListAdapter.setOnContactPictureClicked(new OnContactPictureClicked() {
242			
243			@Override
244			public void onContactPictureClicked(Message message) {
245				if (message.getConversation().getMode() == Conversation.MODE_MULTI) {
246					if (message.getPresence() != null) {
247						highlightInConference(message.getPresence());
248					} else {
249						highlightInConference(message.getCounterpart());
250					}
251				}
252			}
253		});
254		messageListAdapter.setOnContactPictureLongClicked(new OnContactPictureLongClicked() {
255			
256			@Override
257			public void onContactPictureLongClicked(Message message) {
258				if (message.getConversation().getMode() == Conversation.MODE_MULTI) {
259					if (message.getPresence() != null) {
260						privateMessageWith(message.getPresence());
261					} else {
262						privateMessageWith(message.getCounterpart());
263					}
264				}
265			}
266		});
267		messagesView.setAdapter(messageListAdapter);
268
269		return view;
270	}
271	
272	protected void privateMessageWith(String counterpart) {
273		this.mEditMessage.setHint(getString(R.string.send_private_message_to,counterpart));
274		this.mEditMessage.setText("");
275		this.conversation.setNextPresence(counterpart);
276	}
277
278	protected void highlightInConference(String nick) {
279		String oldString = mEditMessage.getText().toString().trim();
280		if (oldString.isEmpty()) {
281			mEditMessage.setText(nick + ": ");
282		} else {
283			mEditMessage.setText(oldString + " " + nick + " ");
284		}
285		int position = mEditMessage.length();
286		Editable etext = mEditMessage.getText();
287		Selection.setSelection(etext, position);
288	}
289
290	@Override
291	public void onStart() {
292		super.onStart();
293		this.activity = (ConversationActivity) getActivity();
294		SharedPreferences preferences = PreferenceManager
295				.getDefaultSharedPreferences(activity);
296		this.useSubject = preferences.getBoolean("use_subject_in_muc", true);
297		if (activity.xmppConnectionServiceBound) {
298			this.onBackendConnected();
299		}
300	}
301
302	@Override
303	public void onStop() {
304		super.onStop();
305		if (this.conversation != null) {
306			this.conversation.setNextMessage(mEditMessage.getText().toString());
307		}
308	}
309
310	public void onBackendConnected() {
311		this.activity = (ConversationActivity) getActivity();
312		this.conversation = activity.getSelectedConversation();
313		if (this.conversation == null) {
314			return;
315		}
316		String oldString = conversation.getNextMessage().trim();
317		if (this.pastedText == null) {
318			this.mEditMessage.setText(oldString);
319		} else {
320
321			if (oldString.isEmpty()) {
322				mEditMessage.setText(pastedText);
323			} else {
324				mEditMessage.setText(oldString + " " + pastedText);
325			}
326			pastedText = null;
327		}
328		int position = mEditMessage.length();
329		Editable etext = mEditMessage.getText();
330		Selection.setSelection(etext, position);
331		updateMessages();
332		if (activity.getSlidingPaneLayout().isSlideable()) {
333			if (!activity.shouldPaneBeOpen()) {
334				activity.getSlidingPaneLayout().closePane();
335				activity.getActionBar().setDisplayHomeAsUpEnabled(true);
336				activity.getActionBar().setHomeButtonEnabled(true);
337				activity.getActionBar().setTitle(
338						conversation.getName(useSubject));
339				activity.invalidateOptionsMenu();
340			}
341		}
342		if (this.conversation.getMode() == Conversation.MODE_MULTI) {
343			conversation.setNextPresence(null);
344		}
345	}
346
347	private void decryptMessage(Message message) {
348		PgpEngine engine = activity.xmppConnectionService.getPgpEngine();
349		if (engine != null) {
350			engine.decrypt(message, new UiCallback<Message>() {
351
352				@Override
353				public void userInputRequried(PendingIntent pi, Message message) {
354					askForPassphraseIntent = pi.getIntentSender();
355					showSnackbar(R.string.openpgp_messages_found,
356							R.string.decrypt, clickToDecryptListener);
357				}
358
359				@Override
360				public void success(Message message) {
361					activity.xmppConnectionService.databaseBackend
362							.updateMessage(message);
363					updateMessages();
364				}
365
366				@Override
367				public void error(int error, Message message) {
368					message.setEncryption(Message.ENCRYPTION_DECRYPTION_FAILED);
369					// updateMessages();
370				}
371			});
372		}
373	}
374
375	public void updateMessages() {
376		if (getView() == null) {
377			return;
378		}
379		hideSnackbar();
380		final ConversationActivity activity = (ConversationActivity) getActivity();
381		if (this.conversation != null) {
382			final Contact contact = this.conversation.getContact();
383			if (!contact.showInRoster() && contact.getOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST)) {
384				showSnackbar(R.string.contact_added_you, R.string.add_back, new OnClickListener() {
385					
386					@Override
387					public void onClick(View v) {
388						activity.xmppConnectionService.createContact(contact);
389						activity.switchToContactDetails(contact);
390					}
391				});
392			}
393			for (Message message : this.conversation.getMessages()) {
394				if ((message.getEncryption() == Message.ENCRYPTION_PGP)
395						&& ((message.getStatus() == Message.STATUS_RECIEVED) || (message
396								.getStatus() == Message.STATUS_SEND))) {
397					decryptMessage(message);
398					break;
399				}
400			}
401			if (this.conversation.getMessages().size() == 0) {
402				this.messageList.clear();
403				messagesLoaded = false;
404			} else {
405				for (Message message : this.conversation.getMessages()) {
406					if (!this.messageList.contains(message)) {
407						this.messageList.add(message);
408					}
409				}
410				messagesLoaded = true;
411				updateStatusMessages();
412			}
413			this.messageListAdapter.notifyDataSetChanged();
414			if (conversation.getMode() == Conversation.MODE_SINGLE) {
415				if (messageList.size() >= 1) {
416					makeFingerprintWarning(conversation.getLatestEncryption());
417				}
418			} else {
419				if (!conversation.getMucOptions().online()
420						&& conversation.getAccount().getStatus() == Account.STATUS_ONLINE) {
421					if (conversation.getMucOptions().getError() == MucOptions.ERROR_NICK_IN_USE) {
422						showSnackbar(R.string.nick_in_use, R.string.edit,
423								clickToMuc);
424					} else if (conversation.getMucOptions().getError() == MucOptions.ERROR_ROOM_NOT_FOUND) {
425						showSnackbar(R.string.conference_not_found,
426								R.string.leave, leaveMuc);
427					}
428				}
429			}
430			getActivity().invalidateOptionsMenu();
431			updateChatMsgHint();
432			if (!activity.shouldPaneBeOpen()) {
433				activity.xmppConnectionService.markRead(conversation);
434				// TODO update notifications
435				UIHelper.updateNotification(getActivity(),
436						activity.getConversationList(), null, false);
437				activity.updateConversationList();
438			}
439		}
440	}
441
442	private void messageSent() {
443		int size = this.messageList.size();
444		if (size >= 1) {
445			messagesView.setSelection(size - 1);
446		}
447		mEditMessage.setText("");
448		updateChatMsgHint();
449	}
450
451	protected void updateStatusMessages() {
452		boolean addedStatusMsg = false;
453		if (conversation.getMode() == Conversation.MODE_SINGLE) {
454			for (int i = this.messageList.size() - 1; i >= 0; --i) {
455				if (addedStatusMsg) {
456					if (this.messageList.get(i).getType() == Message.TYPE_STATUS) {
457						this.messageList.remove(i);
458						--i;
459					}
460				} else {
461					if (this.messageList.get(i).getStatus() == Message.STATUS_RECIEVED) {
462						addedStatusMsg = true;
463					} else {
464						if (this.messageList.get(i).getStatus() == Message.STATUS_SEND_DISPLAYED) {
465							this.messageList.add(i + 1,
466									Message.createStatusMessage(conversation));
467							addedStatusMsg = true;
468						}
469					}
470				}
471			}
472		}
473	}
474
475	protected void makeFingerprintWarning(int latestEncryption) {
476		Set<String> knownFingerprints = conversation.getContact()
477				.getOtrFingerprints();
478		if ((latestEncryption == Message.ENCRYPTION_OTR)
479				&& (conversation.hasValidOtrSession()
480						&& (conversation.getOtrSession().getSessionStatus() == SessionStatus.ENCRYPTED) && (!knownFingerprints
481							.contains(conversation.getOtrFingerprint())))) {
482			showSnackbar(R.string.unknown_otr_fingerprint, R.string.verify,
483					new OnClickListener() {
484
485						@Override
486						public void onClick(View v) {
487							if (conversation.getOtrFingerprint() != null) {
488								AlertDialog dialog = UIHelper
489										.getVerifyFingerprintDialog(
490												(ConversationActivity) getActivity(),
491												conversation, snackbar);
492								dialog.show();
493							}
494						}
495					});
496		}
497	}
498
499	protected void showSnackbar(int message, int action,
500			OnClickListener clickListener) {
501		snackbar.setVisibility(View.VISIBLE);
502		snackbar.setOnClickListener(null);
503		snackbarMessage.setText(message);
504		snackbarMessage.setOnClickListener(null);
505		snackbarAction.setText(action);
506		snackbarAction.setOnClickListener(clickListener);
507	}
508
509	protected void hideSnackbar() {
510		snackbar.setVisibility(View.GONE);
511	}
512
513	protected void sendPlainTextMessage(Message message) {
514		ConversationActivity activity = (ConversationActivity) getActivity();
515		activity.xmppConnectionService.sendMessage(message);
516		messageSent();
517	}
518
519	protected void sendPgpMessage(final Message message) {
520		final ConversationActivity activity = (ConversationActivity) getActivity();
521		final XmppConnectionService xmppService = activity.xmppConnectionService;
522		final Contact contact = message.getConversation().getContact();
523		if (activity.hasPgp()) {
524			if (conversation.getMode() == Conversation.MODE_SINGLE) {
525				if (contact.getPgpKeyId() != 0) {
526					xmppService.getPgpEngine().hasKey(contact,
527							new UiCallback<Contact>() {
528
529								@Override
530								public void userInputRequried(PendingIntent pi,
531										Contact contact) {
532									activity.runIntent(
533											pi,
534											ConversationActivity.REQUEST_ENCRYPT_MESSAGE);
535								}
536
537								@Override
538								public void success(Contact contact) {
539									messageSent();
540									activity.encryptTextMessage(message);
541								}
542
543								@Override
544								public void error(int error, Contact contact) {
545
546								}
547							});
548
549				} else {
550					showNoPGPKeyDialog(false,
551							new DialogInterface.OnClickListener() {
552
553								@Override
554								public void onClick(DialogInterface dialog,
555										int which) {
556									conversation
557											.setNextEncryption(Message.ENCRYPTION_NONE);
558									message.setEncryption(Message.ENCRYPTION_NONE);
559									xmppService.sendMessage(message);
560									messageSent();
561								}
562							});
563				}
564			} else {
565				if (conversation.getMucOptions().pgpKeysInUse()) {
566					if (!conversation.getMucOptions().everybodyHasKeys()) {
567						Toast warning = Toast
568								.makeText(getActivity(),
569										R.string.missing_public_keys,
570										Toast.LENGTH_LONG);
571						warning.setGravity(Gravity.CENTER_VERTICAL, 0, 0);
572						warning.show();
573					}
574					activity.encryptTextMessage(message);
575					messageSent();
576				} else {
577					showNoPGPKeyDialog(true,
578							new DialogInterface.OnClickListener() {
579
580								@Override
581								public void onClick(DialogInterface dialog,
582										int which) {
583									conversation
584											.setNextEncryption(Message.ENCRYPTION_NONE);
585									message.setEncryption(Message.ENCRYPTION_NONE);
586									xmppService.sendMessage(message);
587									messageSent();
588								}
589							});
590				}
591			}
592		} else {
593			activity.showInstallPgpDialog();
594		}
595	}
596
597	public void showNoPGPKeyDialog(boolean plural,
598			DialogInterface.OnClickListener listener) {
599		AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
600		builder.setIconAttribute(android.R.attr.alertDialogIcon);
601		if (plural) {
602			builder.setTitle(getString(R.string.no_pgp_keys));
603			builder.setMessage(getText(R.string.contacts_have_no_pgp_keys));
604		} else {
605			builder.setTitle(getString(R.string.no_pgp_key));
606			builder.setMessage(getText(R.string.contact_has_no_pgp_key));
607		}
608		builder.setNegativeButton(getString(R.string.cancel), null);
609		builder.setPositiveButton(getString(R.string.send_unencrypted),
610				listener);
611		builder.create().show();
612	}
613
614	protected void sendOtrMessage(final Message message) {
615		final ConversationActivity activity = (ConversationActivity) getActivity();
616		final XmppConnectionService xmppService = activity.xmppConnectionService;
617		if (conversation.hasValidOtrSession()) {
618			activity.xmppConnectionService.sendMessage(message);
619			messageSent();
620		} else {
621			activity.selectPresence(message.getConversation(),
622					new OnPresenceSelected() {
623
624						@Override
625						public void onPresenceSelected() {
626							message.setPresence(conversation.getNextPresence());
627							xmppService.sendMessage(message);
628							messageSent();
629						}
630					});
631		}
632	}
633
634	public void setText(String text) {
635		this.pastedText = text;
636	}
637
638	public void clearInputField() {
639		this.mEditMessage.setText("");
640	}
641}