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		if (conversation.getNextPresence() != null) {
191			this.mEditMessage.setHint(getString(R.string.send_private_message_to,conversation.getNextPresence()));
192		} else {
193			switch (conversation.getNextEncryption()) {
194			case Message.ENCRYPTION_NONE:
195				mEditMessage.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((ConversationActivity) getActivity(), this.messageList);
245		messageListAdapter.setOnContactPictureClicked(new OnContactPictureClicked() {
246			
247			@Override
248			public void onContactPictureClicked(Message message) {
249				if (message.getConversation().getMode() == Conversation.MODE_MULTI) {
250					if (message.getPresence() != null) {
251						highlightInConference(message.getPresence());
252					} else {
253						highlightInConference(message.getCounterpart());
254					}
255				}
256			}
257		});
258		messageListAdapter.setOnContactPictureLongClicked(new OnContactPictureLongClicked() {
259			
260			@Override
261			public void onContactPictureLongClicked(Message message) {
262				if (message.getConversation().getMode() == Conversation.MODE_MULTI) {
263					if (message.getPresence() != null) {
264						privateMessageWith(message.getPresence());
265					} else {
266						privateMessageWith(message.getCounterpart());
267					}
268				}
269			}
270		});
271		messagesView.setAdapter(messageListAdapter);
272
273		return view;
274	}
275	
276	protected void privateMessageWith(String counterpart) {
277		this.mEditMessage.setText("");
278		this.conversation.setNextPresence(counterpart);
279		updateChatMsgHint();
280	}
281
282	protected void highlightInConference(String nick) {
283		String oldString = mEditMessage.getText().toString().trim();
284		if (oldString.isEmpty()) {
285			mEditMessage.setText(nick + ": ");
286		} else {
287			mEditMessage.setText(oldString + " " + nick + " ");
288		}
289		int position = mEditMessage.length();
290		Editable etext = mEditMessage.getText();
291		Selection.setSelection(etext, position);
292	}
293
294	@Override
295	public void onStart() {
296		super.onStart();
297		this.activity = (ConversationActivity) getActivity();
298		SharedPreferences preferences = PreferenceManager
299				.getDefaultSharedPreferences(activity);
300		this.useSubject = preferences.getBoolean("use_subject_in_muc", true);
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(useSubject));
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 (!contact.showInRoster() && contact.getOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST)) {
388				showSnackbar(R.string.contact_added_you, R.string.add_back, new OnClickListener() {
389					
390					@Override
391					public void onClick(View v) {
392						activity.xmppConnectionService.createContact(contact);
393						activity.switchToContactDetails(contact);
394					}
395				});
396			}
397			for (Message message : this.conversation.getMessages()) {
398				if ((message.getEncryption() == Message.ENCRYPTION_PGP)
399						&& ((message.getStatus() == Message.STATUS_RECIEVED) || (message
400								.getStatus() == Message.STATUS_SEND))) {
401					decryptMessage(message);
402					break;
403				}
404			}
405			if (this.conversation.getMessages().size() == 0) {
406				this.messageList.clear();
407				messagesLoaded = false;
408			} else {
409				for (Message message : this.conversation.getMessages()) {
410					if (!this.messageList.contains(message)) {
411						this.messageList.add(message);
412					}
413				}
414				messagesLoaded = true;
415				updateStatusMessages();
416			}
417			this.messageListAdapter.notifyDataSetChanged();
418			if (conversation.getMode() == Conversation.MODE_SINGLE) {
419				if (messageList.size() >= 1) {
420					makeFingerprintWarning(conversation.getLatestEncryption());
421				}
422			} else {
423				if (!conversation.getMucOptions().online()
424						&& conversation.getAccount().getStatus() == Account.STATUS_ONLINE) {
425					if (conversation.getMucOptions().getError() == MucOptions.ERROR_NICK_IN_USE) {
426						showSnackbar(R.string.nick_in_use, R.string.edit,
427								clickToMuc);
428					} else if (conversation.getMucOptions().getError() == MucOptions.ERROR_ROOM_NOT_FOUND) {
429						showSnackbar(R.string.conference_not_found,
430								R.string.leave, leaveMuc);
431					}
432				}
433			}
434			getActivity().invalidateOptionsMenu();
435			updateChatMsgHint();
436			if (!activity.shouldPaneBeOpen()) {
437				activity.xmppConnectionService.markRead(conversation);
438				// TODO update notifications
439				UIHelper.updateNotification(getActivity(),
440						activity.getConversationList(), null, false);
441				activity.updateConversationList();
442			}
443		}
444	}
445
446	private void messageSent() {
447		int size = this.messageList.size();
448		if (size >= 1) {
449			messagesView.setSelection(size - 1);
450		}
451		mEditMessage.setText("");
452		updateChatMsgHint();
453	}
454
455	protected void updateStatusMessages() {
456		boolean addedStatusMsg = false;
457		if (conversation.getMode() == Conversation.MODE_SINGLE) {
458			for (int i = this.messageList.size() - 1; i >= 0; --i) {
459				if (addedStatusMsg) {
460					if (this.messageList.get(i).getType() == Message.TYPE_STATUS) {
461						this.messageList.remove(i);
462						--i;
463					}
464				} else {
465					if (this.messageList.get(i).getStatus() == Message.STATUS_RECIEVED) {
466						addedStatusMsg = true;
467					} else {
468						if (this.messageList.get(i).getStatus() == Message.STATUS_SEND_DISPLAYED) {
469							this.messageList.add(i + 1,
470									Message.createStatusMessage(conversation));
471							addedStatusMsg = true;
472						}
473					}
474				}
475			}
476		}
477	}
478
479	protected void makeFingerprintWarning(int latestEncryption) {
480		Set<String> knownFingerprints = conversation.getContact()
481				.getOtrFingerprints();
482		if ((latestEncryption == Message.ENCRYPTION_OTR)
483				&& (conversation.hasValidOtrSession()
484						&& (conversation.getOtrSession().getSessionStatus() == SessionStatus.ENCRYPTED) && (!knownFingerprints
485							.contains(conversation.getOtrFingerprint())))) {
486			showSnackbar(R.string.unknown_otr_fingerprint, R.string.verify,
487					new OnClickListener() {
488
489						@Override
490						public void onClick(View v) {
491							if (conversation.getOtrFingerprint() != null) {
492								AlertDialog dialog = UIHelper
493										.getVerifyFingerprintDialog(
494												(ConversationActivity) getActivity(),
495												conversation, snackbar);
496								dialog.show();
497							}
498						}
499					});
500		}
501	}
502
503	protected void showSnackbar(int message, int action,
504			OnClickListener clickListener) {
505		snackbar.setVisibility(View.VISIBLE);
506		snackbar.setOnClickListener(null);
507		snackbarMessage.setText(message);
508		snackbarMessage.setOnClickListener(null);
509		snackbarAction.setText(action);
510		snackbarAction.setOnClickListener(clickListener);
511	}
512
513	protected void hideSnackbar() {
514		snackbar.setVisibility(View.GONE);
515	}
516
517	protected void sendPlainTextMessage(Message message) {
518		ConversationActivity activity = (ConversationActivity) getActivity();
519		activity.xmppConnectionService.sendMessage(message);
520		messageSent();
521	}
522
523	protected void sendPgpMessage(final Message message) {
524		final ConversationActivity activity = (ConversationActivity) getActivity();
525		final XmppConnectionService xmppService = activity.xmppConnectionService;
526		final Contact contact = message.getConversation().getContact();
527		if (activity.hasPgp()) {
528			if (conversation.getMode() == Conversation.MODE_SINGLE) {
529				if (contact.getPgpKeyId() != 0) {
530					xmppService.getPgpEngine().hasKey(contact,
531							new UiCallback<Contact>() {
532
533								@Override
534								public void userInputRequried(PendingIntent pi,
535										Contact contact) {
536									activity.runIntent(
537											pi,
538											ConversationActivity.REQUEST_ENCRYPT_MESSAGE);
539								}
540
541								@Override
542								public void success(Contact contact) {
543									messageSent();
544									activity.encryptTextMessage(message);
545								}
546
547								@Override
548								public void error(int error, Contact contact) {
549
550								}
551							});
552
553				} else {
554					showNoPGPKeyDialog(false,
555							new DialogInterface.OnClickListener() {
556
557								@Override
558								public void onClick(DialogInterface dialog,
559										int which) {
560									conversation
561											.setNextEncryption(Message.ENCRYPTION_NONE);
562									message.setEncryption(Message.ENCRYPTION_NONE);
563									xmppService.sendMessage(message);
564									messageSent();
565								}
566							});
567				}
568			} else {
569				if (conversation.getMucOptions().pgpKeysInUse()) {
570					if (!conversation.getMucOptions().everybodyHasKeys()) {
571						Toast warning = Toast
572								.makeText(getActivity(),
573										R.string.missing_public_keys,
574										Toast.LENGTH_LONG);
575						warning.setGravity(Gravity.CENTER_VERTICAL, 0, 0);
576						warning.show();
577					}
578					activity.encryptTextMessage(message);
579					messageSent();
580				} else {
581					showNoPGPKeyDialog(true,
582							new DialogInterface.OnClickListener() {
583
584								@Override
585								public void onClick(DialogInterface dialog,
586										int which) {
587									conversation
588											.setNextEncryption(Message.ENCRYPTION_NONE);
589									message.setEncryption(Message.ENCRYPTION_NONE);
590									xmppService.sendMessage(message);
591									messageSent();
592								}
593							});
594				}
595			}
596		} else {
597			activity.showInstallPgpDialog();
598		}
599	}
600
601	public void showNoPGPKeyDialog(boolean plural,
602			DialogInterface.OnClickListener listener) {
603		AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
604		builder.setIconAttribute(android.R.attr.alertDialogIcon);
605		if (plural) {
606			builder.setTitle(getString(R.string.no_pgp_keys));
607			builder.setMessage(getText(R.string.contacts_have_no_pgp_keys));
608		} else {
609			builder.setTitle(getString(R.string.no_pgp_key));
610			builder.setMessage(getText(R.string.contact_has_no_pgp_key));
611		}
612		builder.setNegativeButton(getString(R.string.cancel), null);
613		builder.setPositiveButton(getString(R.string.send_unencrypted),
614				listener);
615		builder.create().show();
616	}
617
618	protected void sendOtrMessage(final Message message) {
619		final ConversationActivity activity = (ConversationActivity) getActivity();
620		final XmppConnectionService xmppService = activity.xmppConnectionService;
621		if (conversation.hasValidOtrSession()) {
622			activity.xmppConnectionService.sendMessage(message);
623			messageSent();
624		} else {
625			activity.selectPresence(message.getConversation(),
626					new OnPresenceSelected() {
627
628						@Override
629						public void onPresenceSelected() {
630							message.setPresence(conversation.getNextPresence());
631							xmppService.sendMessage(message);
632							messageSent();
633						}
634					});
635		}
636	}
637
638	public void setText(String text) {
639		this.pastedText = text;
640	}
641
642	public void clearInputField() {
643		this.mEditMessage.setText("");
644	}
645}