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			mEditMessage.getText().insert(mEditMessage.getSelectionStart(), nick + " ");
294		}
295	}
296
297	@Override
298	public void onStart() {
299		super.onStart();
300		this.activity = (ConversationActivity) getActivity();
301		SharedPreferences preferences = PreferenceManager
302				.getDefaultSharedPreferences(activity);
303		this.useSubject = preferences.getBoolean("use_subject_in_muc", true);
304		if (activity.xmppConnectionServiceBound) {
305			this.onBackendConnected();
306		}
307	}
308
309	@Override
310	public void onStop() {
311		super.onStop();
312		if (this.conversation != null) {
313			this.conversation.setNextMessage(mEditMessage.getText().toString());
314		}
315	}
316
317	public void onBackendConnected() {
318		this.activity = (ConversationActivity) getActivity();
319		this.conversation = activity.getSelectedConversation();
320		if (this.conversation == null) {
321			return;
322		}
323		String oldString = conversation.getNextMessage().trim();
324		if (this.pastedText == null) {
325			this.mEditMessage.setText(oldString);
326		} else {
327
328			if (oldString.isEmpty()) {
329				mEditMessage.setText(pastedText);
330			} else {
331				mEditMessage.setText(oldString + " " + pastedText);
332			}
333			pastedText = null;
334		}
335		int position = mEditMessage.length();
336		Editable etext = mEditMessage.getText();
337		Selection.setSelection(etext, position);
338		if (activity.getSlidingPaneLayout().isSlideable()) {
339			if (!activity.shouldPaneBeOpen()) {
340				activity.getSlidingPaneLayout().closePane();
341				activity.getActionBar().setDisplayHomeAsUpEnabled(true);
342				activity.getActionBar().setHomeButtonEnabled(true);
343				activity.getActionBar().setTitle(
344						conversation.getName(useSubject));
345				activity.invalidateOptionsMenu();
346			}
347		}
348		if (this.conversation.getMode() == Conversation.MODE_MULTI) {
349			conversation.setNextPresence(null);
350		}
351		updateMessages();
352	}
353
354	private void decryptMessage(Message message) {
355		PgpEngine engine = activity.xmppConnectionService.getPgpEngine();
356		if (engine != null) {
357			engine.decrypt(message, new UiCallback<Message>() {
358
359				@Override
360				public void userInputRequried(PendingIntent pi, Message message) {
361					askForPassphraseIntent = pi.getIntentSender();
362					showSnackbar(R.string.openpgp_messages_found,
363							R.string.decrypt, clickToDecryptListener);
364				}
365
366				@Override
367				public void success(Message message) {
368					activity.xmppConnectionService.databaseBackend
369							.updateMessage(message);
370					updateMessages();
371				}
372
373				@Override
374				public void error(int error, Message message) {
375					message.setEncryption(Message.ENCRYPTION_DECRYPTION_FAILED);
376					// updateMessages();
377				}
378			});
379		}
380	}
381
382	public void updateMessages() {
383		if (getView() == null) {
384			return;
385		}
386		hideSnackbar();
387		final ConversationActivity activity = (ConversationActivity) getActivity();
388		if (this.conversation != null) {
389			final Contact contact = this.conversation.getContact();
390			if (!contact.showInRoster()
391					&& contact
392							.getOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST)) {
393				showSnackbar(R.string.contact_added_you, R.string.add_back,
394						new OnClickListener() {
395
396							@Override
397							public void onClick(View v) {
398								activity.xmppConnectionService
399										.createContact(contact);
400								activity.switchToContactDetails(contact);
401							}
402						});
403			}
404			for (Message message : this.conversation.getMessages()) {
405				if ((message.getEncryption() == Message.ENCRYPTION_PGP)
406						&& ((message.getStatus() == Message.STATUS_RECEIVED) || (message
407								.getStatus() == Message.STATUS_SEND))) {
408					decryptMessage(message);
409					break;
410				}
411			}
412			if (this.conversation.getMessages().size() == 0) {
413				this.messageList.clear();
414				messagesLoaded = false;
415			} else {
416				for (Message message : this.conversation.getMessages()) {
417					if (!this.messageList.contains(message)) {
418						this.messageList.add(message);
419					}
420				}
421				messagesLoaded = true;
422				updateStatusMessages();
423			}
424			this.messageListAdapter.notifyDataSetChanged();
425			if (conversation.getMode() == Conversation.MODE_SINGLE) {
426				if (messageList.size() >= 1) {
427					makeFingerprintWarning(conversation.getLatestEncryption());
428				}
429			} else {
430				if (!conversation.getMucOptions().online()
431						&& conversation.getAccount().getStatus() == Account.STATUS_ONLINE) {
432					if (conversation.getMucOptions().getError() == MucOptions.ERROR_NICK_IN_USE) {
433						showSnackbar(R.string.nick_in_use, R.string.edit,
434								clickToMuc);
435					} else if (conversation.getMucOptions().getError() == MucOptions.ERROR_ROOM_NOT_FOUND) {
436						showSnackbar(R.string.conference_not_found,
437								R.string.leave, leaveMuc);
438					}
439				}
440			}
441			getActivity().invalidateOptionsMenu();
442			updateChatMsgHint();
443			if (!activity.shouldPaneBeOpen()) {
444				activity.xmppConnectionService.markRead(conversation);
445				// TODO update notifications
446				UIHelper.updateNotification(getActivity(),
447						activity.getConversationList(), null, false);
448				activity.updateConversationList();
449			}
450		}
451	}
452
453	private void messageSent() {
454		int size = this.messageList.size();
455		if (size >= 1) {
456			messagesView.setSelection(size - 1);
457		}
458		mEditMessage.setText("");
459		updateChatMsgHint();
460	}
461
462	protected void updateStatusMessages() {
463		boolean addedStatusMsg = false;
464		if (conversation.getMode() == Conversation.MODE_SINGLE) {
465			for (int i = this.messageList.size() - 1; i >= 0; --i) {
466				if (addedStatusMsg) {
467					if (this.messageList.get(i).getType() == Message.TYPE_STATUS) {
468						this.messageList.remove(i);
469						--i;
470					}
471				} else {
472					if (this.messageList.get(i).getStatus() == Message.STATUS_RECEIVED) {
473						addedStatusMsg = true;
474					} else {
475						if (this.messageList.get(i).getStatus() == Message.STATUS_SEND_DISPLAYED) {
476							this.messageList.add(i + 1,
477									Message.createStatusMessage(conversation));
478							addedStatusMsg = true;
479						}
480					}
481				}
482			}
483		}
484	}
485
486	protected void makeFingerprintWarning(int latestEncryption) {
487		Set<String> knownFingerprints = conversation.getContact()
488				.getOtrFingerprints();
489		if ((latestEncryption == Message.ENCRYPTION_OTR)
490				&& (conversation.hasValidOtrSession()
491						&& (conversation.getOtrSession().getSessionStatus() == SessionStatus.ENCRYPTED) && (!knownFingerprints
492							.contains(conversation.getOtrFingerprint())))) {
493			showSnackbar(R.string.unknown_otr_fingerprint, R.string.verify,
494					new OnClickListener() {
495
496						@Override
497						public void onClick(View v) {
498							if (conversation.getOtrFingerprint() != null) {
499								AlertDialog dialog = UIHelper
500										.getVerifyFingerprintDialog(
501												(ConversationActivity) getActivity(),
502												conversation, snackbar);
503								dialog.show();
504							}
505						}
506					});
507		}
508	}
509
510	protected void showSnackbar(int message, int action,
511			OnClickListener clickListener) {
512		snackbar.setVisibility(View.VISIBLE);
513		snackbar.setOnClickListener(null);
514		snackbarMessage.setText(message);
515		snackbarMessage.setOnClickListener(null);
516		snackbarAction.setText(action);
517		snackbarAction.setOnClickListener(clickListener);
518	}
519
520	protected void hideSnackbar() {
521		snackbar.setVisibility(View.GONE);
522	}
523
524	protected void sendPlainTextMessage(Message message) {
525		ConversationActivity activity = (ConversationActivity) getActivity();
526		activity.xmppConnectionService.sendMessage(message);
527		messageSent();
528	}
529
530	protected void sendPgpMessage(final Message message) {
531		final ConversationActivity activity = (ConversationActivity) getActivity();
532		final XmppConnectionService xmppService = activity.xmppConnectionService;
533		final Contact contact = message.getConversation().getContact();
534		if (activity.hasPgp()) {
535			if (conversation.getMode() == Conversation.MODE_SINGLE) {
536				if (contact.getPgpKeyId() != 0) {
537					xmppService.getPgpEngine().hasKey(contact,
538							new UiCallback<Contact>() {
539
540								@Override
541								public void userInputRequried(PendingIntent pi,
542										Contact contact) {
543									activity.runIntent(
544											pi,
545											ConversationActivity.REQUEST_ENCRYPT_MESSAGE);
546								}
547
548								@Override
549								public void success(Contact contact) {
550									messageSent();
551									activity.encryptTextMessage(message);
552								}
553
554								@Override
555								public void error(int error, Contact contact) {
556
557								}
558							});
559
560				} else {
561					showNoPGPKeyDialog(false,
562							new DialogInterface.OnClickListener() {
563
564								@Override
565								public void onClick(DialogInterface dialog,
566										int which) {
567									conversation
568											.setNextEncryption(Message.ENCRYPTION_NONE);
569									message.setEncryption(Message.ENCRYPTION_NONE);
570									xmppService.sendMessage(message);
571									messageSent();
572								}
573							});
574				}
575			} else {
576				if (conversation.getMucOptions().pgpKeysInUse()) {
577					if (!conversation.getMucOptions().everybodyHasKeys()) {
578						Toast warning = Toast
579								.makeText(getActivity(),
580										R.string.missing_public_keys,
581										Toast.LENGTH_LONG);
582						warning.setGravity(Gravity.CENTER_VERTICAL, 0, 0);
583						warning.show();
584					}
585					activity.encryptTextMessage(message);
586					messageSent();
587				} else {
588					showNoPGPKeyDialog(true,
589							new DialogInterface.OnClickListener() {
590
591								@Override
592								public void onClick(DialogInterface dialog,
593										int which) {
594									conversation
595											.setNextEncryption(Message.ENCRYPTION_NONE);
596									message.setEncryption(Message.ENCRYPTION_NONE);
597									xmppService.sendMessage(message);
598									messageSent();
599								}
600							});
601				}
602			}
603		} else {
604			activity.showInstallPgpDialog();
605		}
606	}
607
608	public void showNoPGPKeyDialog(boolean plural,
609			DialogInterface.OnClickListener listener) {
610		AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
611		builder.setIconAttribute(android.R.attr.alertDialogIcon);
612		if (plural) {
613			builder.setTitle(getString(R.string.no_pgp_keys));
614			builder.setMessage(getText(R.string.contacts_have_no_pgp_keys));
615		} else {
616			builder.setTitle(getString(R.string.no_pgp_key));
617			builder.setMessage(getText(R.string.contact_has_no_pgp_key));
618		}
619		builder.setNegativeButton(getString(R.string.cancel), null);
620		builder.setPositiveButton(getString(R.string.send_unencrypted),
621				listener);
622		builder.create().show();
623	}
624
625	protected void sendOtrMessage(final Message message) {
626		final ConversationActivity activity = (ConversationActivity) getActivity();
627		final XmppConnectionService xmppService = activity.xmppConnectionService;
628		if (conversation.hasValidOtrSession()) {
629			activity.xmppConnectionService.sendMessage(message);
630			messageSent();
631		} else {
632			activity.selectPresence(message.getConversation(),
633					new OnPresenceSelected() {
634
635						@Override
636						public void onPresenceSelected() {
637							message.setPresence(conversation.getNextPresence());
638							xmppService.sendMessage(message);
639							messageSent();
640						}
641					});
642		}
643	}
644
645	public void setText(String text) {
646		this.pastedText = text;
647	}
648
649	public void clearInputField() {
650		this.mEditMessage.setText("");
651	}
652}