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