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