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