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