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.utils.UIHelper;
 21import android.app.AlertDialog;
 22import android.app.Fragment;
 23import android.app.PendingIntent;
 24import android.content.Context;
 25import android.content.DialogInterface;
 26import android.content.Intent;
 27import android.content.IntentSender;
 28import android.content.SharedPreferences;
 29import android.content.IntentSender.SendIntentException;
 30import android.os.Bundle;
 31import android.preference.PreferenceManager;
 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 useSubject = true;
 70	private boolean messagesLoaded = false;
 71
 72	private IntentSender askForPassphraseIntent = null;
 73	
 74	private OnEditorActionListener mEditorActionListener = new OnEditorActionListener() {
 75		
 76		@Override
 77		public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
 78			if (actionId == EditorInfo.IME_ACTION_DONE) {
 79                InputMethodManager imm = (InputMethodManager) v.getContext()
 80                        .getSystemService(Context.INPUT_METHOD_SERVICE);
 81                imm.hideSoftInputFromWindow(v.getWindowToken(), 0);
 82                return true;
 83            } else {
 84            	return false;
 85            }
 86		}
 87	};
 88
 89	private OnClickListener mSendButtonListener = new OnClickListener() {
 90
 91		@Override
 92		public void onClick(View v) {
 93			sendMessage();
 94		}
 95	};
 96	protected OnClickListener clickToDecryptListener = new OnClickListener() {
 97
 98		@Override
 99		public void onClick(View v) {
100			if (activity.hasPgp() && askForPassphraseIntent != null) {
101				try {
102					getActivity().startIntentSenderForResult(
103							askForPassphraseIntent,
104							ConversationActivity.REQUEST_DECRYPT_PGP, null, 0,
105							0, 0);
106				} catch (SendIntentException e) {
107					//
108				}
109			}
110		}
111	};
112
113	private OnClickListener clickToMuc = new OnClickListener() {
114
115		@Override
116		public void onClick(View v) {
117			Intent intent = new Intent(getActivity(),
118					ConferenceDetailsActivity.class);
119			intent.setAction(ConferenceDetailsActivity.ACTION_VIEW_MUC);
120			intent.putExtra("uuid", conversation.getUuid());
121			startActivity(intent);
122		}
123	};
124
125	private OnClickListener leaveMuc = new OnClickListener() {
126
127		@Override
128		public void onClick(View v) {
129			activity.endConversation(conversation);
130		}
131	};
132
133	private OnScrollListener mOnScrollListener = new OnScrollListener() {
134
135		@Override
136		public void onScrollStateChanged(AbsListView view, int scrollState) {
137			// TODO Auto-generated method stub
138
139		}
140
141		@Override
142		public void onScroll(AbsListView view, int firstVisibleItem,
143				int visibleItemCount, int totalItemCount) {
144			if (firstVisibleItem == 0 && messagesLoaded) {
145				long timestamp = messageList.get(0).getTimeSent();
146				messagesLoaded = false;
147				List<Message> messages = activity.xmppConnectionService
148						.getMoreMessages(conversation, timestamp);
149				messageList.addAll(0, messages);
150				messageListAdapter.notifyDataSetChanged();
151				if (messages.size() != 0) {
152					messagesLoaded = true;
153				}
154				messagesView.setSelectionFromTop(messages.size() + 1, 0);
155			}
156		}
157	};
158
159	private ConversationActivity activity;
160	
161	
162	private void sendMessage() {
163		if (mEditMessage.getText().length() < 1)
164			return;
165		Message message = new Message(conversation, mEditMessage.getText()
166				.toString(), conversation.getNextEncryption());
167		if (conversation.getNextEncryption() == Message.ENCRYPTION_OTR) {
168			sendOtrMessage(message);
169		} else if (conversation.getNextEncryption() == Message.ENCRYPTION_PGP) {
170			sendPgpMessage(message);
171		} else {
172			sendPlainTextMessage(message);
173		}
174	}
175
176	public void updateChatMsgHint() {
177		switch (conversation.getNextEncryption()) {
178		case Message.ENCRYPTION_NONE:
179			mEditMessage.setHint(getString(R.string.send_plain_text_message));
180			break;
181		case Message.ENCRYPTION_OTR:
182			mEditMessage.setHint(getString(R.string.send_otr_message));
183			break;
184		case Message.ENCRYPTION_PGP:
185			mEditMessage.setHint(getString(R.string.send_pgp_message));
186			break;
187		default:
188			break;
189		}
190	}
191
192	@Override
193	public View onCreateView(final LayoutInflater inflater,
194			ViewGroup container, Bundle savedInstanceState) {
195		final View view = inflater.inflate(R.layout.fragment_conversation,
196				container, false);
197		mEditMessage = (EditMessage) view.findViewById(R.id.textinput);
198		mEditMessage.setOnClickListener(new OnClickListener() {
199
200			@Override
201			public void onClick(View v) {
202				if (activity.getSlidingPaneLayout().isSlideable()) {
203					activity.getSlidingPaneLayout().closePane();
204				}
205			}
206		});
207		mEditMessage.setOnEditorActionListener(mEditorActionListener);
208		mEditMessage.setOnEnterPressedListener(new OnEnterPressed() {
209			
210			@Override
211			public void onEnterPressed() {
212				sendMessage();
213			}
214		});
215		
216		ImageButton sendButton = (ImageButton) view
217				.findViewById(R.id.textSendButton);
218		sendButton.setOnClickListener(this.mSendButtonListener);
219
220		snackbar = (RelativeLayout) view.findViewById(R.id.snackbar);
221		snackbarMessage = (TextView) view.findViewById(R.id.snackbar_message);
222		snackbarAction = (TextView) view.findViewById(R.id.snackbar_action);
223
224		messagesView = (ListView) view.findViewById(R.id.messages_view);
225		messagesView.setOnScrollListener(mOnScrollListener);
226		messagesView.setTranscriptMode(ListView.TRANSCRIPT_MODE_NORMAL);
227		messageListAdapter = new MessageAdapter((ConversationActivity) getActivity(), this.messageList);
228		messageListAdapter.setOnContactPictureClicked(new OnContactPictureClicked() {
229			
230			@Override
231			public void onContactPictureClicked(Message message) {
232				if (message.getConversation().getMode() == Conversation.MODE_MULTI) {
233					highlightInConference(message.getCounterpart());
234				}
235			}
236		});
237		messagesView.setAdapter(messageListAdapter);
238
239		return view;
240	}
241
242	protected void highlightInConference(String nick) {
243		String oldString = mEditMessage.getText().toString().trim();
244		if (oldString.isEmpty()) {
245			mEditMessage.setText(nick + ": ");
246		} else {
247			mEditMessage.setText(oldString + " " + nick + " ");
248		}
249		int position = mEditMessage.length();
250		Editable etext = mEditMessage.getText();
251		Selection.setSelection(etext, position);
252	}
253
254	@Override
255	public void onStart() {
256		super.onStart();
257		this.activity = (ConversationActivity) getActivity();
258		SharedPreferences preferences = PreferenceManager
259				.getDefaultSharedPreferences(activity);
260		this.useSubject = preferences.getBoolean("use_subject_in_muc", true);
261		if (activity.xmppConnectionServiceBound) {
262			this.onBackendConnected();
263		}
264	}
265
266	@Override
267	public void onStop() {
268		super.onStop();
269		if (this.conversation != null) {
270			this.conversation.setNextMessage(mEditMessage.getText().toString());
271		}
272	}
273
274	public void onBackendConnected() {
275		this.activity = (ConversationActivity) getActivity();
276		this.conversation = activity.getSelectedConversation();
277		if (this.conversation == null) {
278			return;
279		}
280		String oldString = conversation.getNextMessage().trim();
281		if (this.pastedText == null) {
282			this.mEditMessage.setText(oldString);
283		} else {
284
285			if (oldString.isEmpty()) {
286				mEditMessage.setText(pastedText);
287			} else {
288				mEditMessage.setText(oldString + " " + pastedText);
289			}
290			pastedText = null;
291		}
292		int position = mEditMessage.length();
293		Editable etext = mEditMessage.getText();
294		Selection.setSelection(etext, position);
295		updateMessages();
296		if (activity.getSlidingPaneLayout().isSlideable()) {
297			if (!activity.shouldPaneBeOpen()) {
298				activity.getSlidingPaneLayout().closePane();
299				activity.getActionBar().setDisplayHomeAsUpEnabled(true);
300				activity.getActionBar().setHomeButtonEnabled(true);
301				activity.getActionBar().setTitle(
302						conversation.getName(useSubject));
303				activity.invalidateOptionsMenu();
304			}
305		}
306	}
307
308	private void decryptMessage(Message message) {
309		PgpEngine engine = activity.xmppConnectionService.getPgpEngine();
310		if (engine != null) {
311			engine.decrypt(message, new UiCallback<Message>() {
312
313				@Override
314				public void userInputRequried(PendingIntent pi, Message message) {
315					askForPassphraseIntent = pi.getIntentSender();
316					showSnackbar(R.string.openpgp_messages_found,
317							R.string.decrypt, clickToDecryptListener);
318				}
319
320				@Override
321				public void success(Message message) {
322					activity.xmppConnectionService.databaseBackend
323							.updateMessage(message);
324					updateMessages();
325				}
326
327				@Override
328				public void error(int error, Message message) {
329					message.setEncryption(Message.ENCRYPTION_DECRYPTION_FAILED);
330					// updateMessages();
331				}
332			});
333		}
334	}
335
336	public void updateMessages() {
337		if (getView() == null) {
338			return;
339		}
340		hideSnackbar();
341		final ConversationActivity activity = (ConversationActivity) getActivity();
342		if (this.conversation != null) {
343			final Contact contact = this.conversation.getContact();
344			if (!contact.showInRoster() && contact.getOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST)) {
345				showSnackbar(R.string.contact_added_you, R.string.add_back, new OnClickListener() {
346					
347					@Override
348					public void onClick(View v) {
349						activity.xmppConnectionService.createContact(contact);
350						activity.switchToContactDetails(contact);
351					}
352				});
353			}
354			for (Message message : this.conversation.getMessages()) {
355				if ((message.getEncryption() == Message.ENCRYPTION_PGP)
356						&& ((message.getStatus() == Message.STATUS_RECIEVED) || (message
357								.getStatus() == Message.STATUS_SEND))) {
358					decryptMessage(message);
359					break;
360				}
361			}
362			if (this.conversation.getMessages().size() == 0) {
363				this.messageList.clear();
364				messagesLoaded = false;
365			} else {
366				for (Message message : this.conversation.getMessages()) {
367					if (!this.messageList.contains(message)) {
368						this.messageList.add(message);
369					}
370				}
371				messagesLoaded = true;
372				updateStatusMessages();
373			}
374			this.messageListAdapter.notifyDataSetChanged();
375			if (conversation.getMode() == Conversation.MODE_SINGLE) {
376				if (messageList.size() >= 1) {
377					makeFingerprintWarning(conversation.getLatestEncryption());
378				}
379			} else {
380				if (!conversation.getMucOptions().online()
381						&& conversation.getAccount().getStatus() == Account.STATUS_ONLINE) {
382					if (conversation.getMucOptions().getError() == MucOptions.ERROR_NICK_IN_USE) {
383						showSnackbar(R.string.nick_in_use, R.string.edit,
384								clickToMuc);
385					} else if (conversation.getMucOptions().getError() == MucOptions.ERROR_ROOM_NOT_FOUND) {
386						showSnackbar(R.string.conference_not_found,
387								R.string.leave, leaveMuc);
388					}
389				}
390			}
391			getActivity().invalidateOptionsMenu();
392			updateChatMsgHint();
393			if (!activity.shouldPaneBeOpen()) {
394				activity.xmppConnectionService.markRead(conversation);
395				// TODO update notifications
396				UIHelper.updateNotification(getActivity(),
397						activity.getConversationList(), null, false);
398				activity.updateConversationList();
399			}
400		}
401	}
402
403	private void messageSent() {
404		int size = this.messageList.size();
405		if (size >= 1) {
406			messagesView.setSelection(size - 1);
407		}
408		mEditMessage.setText("");
409	}
410
411	protected void updateStatusMessages() {
412		boolean addedStatusMsg = false;
413		if (conversation.getMode() == Conversation.MODE_SINGLE) {
414			for (int i = this.messageList.size() - 1; i >= 0; --i) {
415				if (addedStatusMsg) {
416					if (this.messageList.get(i).getType() == Message.TYPE_STATUS) {
417						this.messageList.remove(i);
418						--i;
419					}
420				} else {
421					if (this.messageList.get(i).getStatus() == Message.STATUS_RECIEVED) {
422						addedStatusMsg = true;
423					} else {
424						if (this.messageList.get(i).getStatus() == Message.STATUS_SEND_DISPLAYED) {
425							this.messageList.add(i + 1,
426									Message.createStatusMessage(conversation));
427							addedStatusMsg = true;
428						}
429					}
430				}
431			}
432		}
433	}
434
435	protected void makeFingerprintWarning(int latestEncryption) {
436		Set<String> knownFingerprints = conversation.getContact()
437				.getOtrFingerprints();
438		if ((latestEncryption == Message.ENCRYPTION_OTR)
439				&& (conversation.hasValidOtrSession()
440						&& (conversation.getOtrSession().getSessionStatus() == SessionStatus.ENCRYPTED) && (!knownFingerprints
441							.contains(conversation.getOtrFingerprint())))) {
442			showSnackbar(R.string.unknown_otr_fingerprint, R.string.verify,
443					new OnClickListener() {
444
445						@Override
446						public void onClick(View v) {
447							if (conversation.getOtrFingerprint() != null) {
448								AlertDialog dialog = UIHelper
449										.getVerifyFingerprintDialog(
450												(ConversationActivity) getActivity(),
451												conversation, snackbar);
452								dialog.show();
453							}
454						}
455					});
456		}
457	}
458
459	protected void showSnackbar(int message, int action,
460			OnClickListener clickListener) {
461		snackbar.setVisibility(View.VISIBLE);
462		snackbarMessage.setText(message);
463		snackbarAction.setText(action);
464		snackbarAction.setOnClickListener(clickListener);
465	}
466
467	protected void hideSnackbar() {
468		snackbar.setVisibility(View.GONE);
469	}
470
471	protected void sendPlainTextMessage(Message message) {
472		ConversationActivity activity = (ConversationActivity) getActivity();
473		activity.xmppConnectionService.sendMessage(message);
474		messageSent();
475	}
476
477	protected void sendPgpMessage(final Message message) {
478		final ConversationActivity activity = (ConversationActivity) getActivity();
479		final XmppConnectionService xmppService = activity.xmppConnectionService;
480		final Contact contact = message.getConversation().getContact();
481		if (activity.hasPgp()) {
482			if (conversation.getMode() == Conversation.MODE_SINGLE) {
483				if (contact.getPgpKeyId() != 0) {
484					xmppService.getPgpEngine().hasKey(contact,
485							new UiCallback<Contact>() {
486
487								@Override
488								public void userInputRequried(PendingIntent pi,
489										Contact contact) {
490									activity.runIntent(
491											pi,
492											ConversationActivity.REQUEST_ENCRYPT_MESSAGE);
493								}
494
495								@Override
496								public void success(Contact contact) {
497									messageSent();
498									activity.encryptTextMessage(message);
499								}
500
501								@Override
502								public void error(int error, Contact contact) {
503
504								}
505							});
506
507				} else {
508					showNoPGPKeyDialog(false,
509							new DialogInterface.OnClickListener() {
510
511								@Override
512								public void onClick(DialogInterface dialog,
513										int which) {
514									conversation
515											.setNextEncryption(Message.ENCRYPTION_NONE);
516									message.setEncryption(Message.ENCRYPTION_NONE);
517									xmppService.sendMessage(message);
518									messageSent();
519								}
520							});
521				}
522			} else {
523				if (conversation.getMucOptions().pgpKeysInUse()) {
524					if (!conversation.getMucOptions().everybodyHasKeys()) {
525						Toast warning = Toast
526								.makeText(getActivity(),
527										R.string.missing_public_keys,
528										Toast.LENGTH_LONG);
529						warning.setGravity(Gravity.CENTER_VERTICAL, 0, 0);
530						warning.show();
531					}
532					activity.encryptTextMessage(message);
533					messageSent();
534				} else {
535					showNoPGPKeyDialog(true,
536							new DialogInterface.OnClickListener() {
537
538								@Override
539								public void onClick(DialogInterface dialog,
540										int which) {
541									conversation
542											.setNextEncryption(Message.ENCRYPTION_NONE);
543									message.setEncryption(Message.ENCRYPTION_NONE);
544									xmppService.sendMessage(message);
545									messageSent();
546								}
547							});
548				}
549			}
550		} else {
551			activity.showInstallPgpDialog();
552		}
553	}
554
555	public void showNoPGPKeyDialog(boolean plural,
556			DialogInterface.OnClickListener listener) {
557		AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
558		builder.setIconAttribute(android.R.attr.alertDialogIcon);
559		if (plural) {
560			builder.setTitle(getString(R.string.no_pgp_keys));
561			builder.setMessage(getText(R.string.contacts_have_no_pgp_keys));
562		} else {
563			builder.setTitle(getString(R.string.no_pgp_key));
564			builder.setMessage(getText(R.string.contact_has_no_pgp_key));
565		}
566		builder.setNegativeButton(getString(R.string.cancel), null);
567		builder.setPositiveButton(getString(R.string.send_unencrypted),
568				listener);
569		builder.create().show();
570	}
571
572	protected void sendOtrMessage(final Message message) {
573		final ConversationActivity activity = (ConversationActivity) getActivity();
574		final XmppConnectionService xmppService = activity.xmppConnectionService;
575		if (conversation.hasValidOtrSession()) {
576			activity.xmppConnectionService.sendMessage(message);
577			messageSent();
578		} else {
579			activity.selectPresence(message.getConversation(),
580					new OnPresenceSelected() {
581
582						@Override
583						public void onPresenceSelected() {
584							message.setPresence(conversation.getNextPresence());
585							xmppService.sendMessage(message);
586							messageSent();
587						}
588					});
589		}
590	}
591
592	public void setText(String text) {
593		this.pastedText = text;
594	}
595
596	public void clearInputField() {
597		this.mEditMessage.setText("");
598	}
599}