ConversationFragment.java

  1package eu.siacs.conversations.ui;
  2
  3import java.util.ArrayList;
  4import java.util.HashMap;
  5import java.util.Hashtable;
  6import java.util.LinkedList;
  7import java.util.List;
  8import java.util.Set;
  9
 10import net.java.otr4j.session.SessionStatus;
 11
 12import eu.siacs.conversations.R;
 13import eu.siacs.conversations.crypto.PgpEngine.OpenPgpException;
 14import eu.siacs.conversations.crypto.PgpEngine.UserInputRequiredException;
 15import eu.siacs.conversations.entities.Account;
 16import eu.siacs.conversations.entities.Contact;
 17import eu.siacs.conversations.entities.Conversation;
 18import eu.siacs.conversations.entities.Message;
 19import eu.siacs.conversations.entities.MucOptions;
 20import eu.siacs.conversations.entities.MucOptions.OnRenameListener;
 21import eu.siacs.conversations.services.XmppConnectionService;
 22import eu.siacs.conversations.utils.UIHelper;
 23import android.app.AlertDialog;
 24import android.app.Fragment;
 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.graphics.Bitmap;
 32import android.graphics.Typeface;
 33import android.os.AsyncTask;
 34import android.os.Bundle;
 35import android.preference.PreferenceManager;
 36import android.util.Log;
 37import android.view.LayoutInflater;
 38import android.view.View;
 39import android.view.View.OnClickListener;
 40import android.view.ViewGroup;
 41import android.widget.ArrayAdapter;
 42import android.widget.EditText;
 43import android.widget.LinearLayout;
 44import android.widget.ListView;
 45import android.widget.ImageButton;
 46import android.widget.ImageView;
 47import android.widget.TextView;
 48import android.widget.Toast;
 49
 50public class ConversationFragment extends Fragment {
 51
 52	protected Conversation conversation;
 53	protected ListView messagesView;
 54	protected LayoutInflater inflater;
 55	protected List<Message> messageList = new ArrayList<Message>();
 56	protected ArrayAdapter<Message> messageListAdapter;
 57	protected Contact contact;
 58	protected BitmapCache mBitmapCache = new BitmapCache();
 59
 60	protected String queuedPqpMessage = null;
 61
 62	private EditText chatMsg;
 63	private String pastedText = null;
 64
 65	protected Bitmap selfBitmap;
 66	
 67	private boolean useSubject = true;
 68
 69	private IntentSender askForPassphraseIntent = null;
 70
 71	private OnClickListener sendMsgListener = new OnClickListener() {
 72
 73		@Override
 74		public void onClick(View v) {
 75			if (chatMsg.getText().length() < 1)
 76				return;
 77			Message message = new Message(conversation, chatMsg.getText()
 78					.toString(), conversation.nextMessageEncryption);
 79			if (conversation.nextMessageEncryption == Message.ENCRYPTION_OTR) {
 80				sendOtrMessage(message);
 81			} else if (conversation.nextMessageEncryption == Message.ENCRYPTION_PGP) {
 82				sendPgpMessage(message);
 83			} else {
 84				sendPlainTextMessage(message);
 85			}
 86		}
 87	};
 88	protected OnClickListener clickToDecryptListener = new OnClickListener() {
 89
 90		@Override
 91		public void onClick(View v) {
 92			Log.d("gultsch", "clicked to decrypt");
 93			if (askForPassphraseIntent != null) {
 94				try {
 95					getActivity().startIntentSenderForResult(
 96							askForPassphraseIntent,
 97							ConversationActivity.REQUEST_DECRYPT_PGP, null, 0,
 98							0, 0);
 99				} catch (SendIntentException e) {
100					Log.d("gultsch", "couldnt fire intent");
101				}
102			}
103		}
104	};
105
106	private LinearLayout pgpInfo;
107	private LinearLayout mucError;
108	private TextView mucErrorText;
109	private OnClickListener clickToMuc = new OnClickListener() {
110
111		@Override
112		public void onClick(View v) {
113			Intent intent = new Intent(getActivity(), MucDetailsActivity.class);
114			intent.setAction(MucDetailsActivity.ACTION_VIEW_MUC);
115			intent.putExtra("uuid", conversation.getUuid());
116			startActivity(intent);
117		}
118	};
119	private ConversationActivity activity;
120
121	public void hidePgpPassphraseBox() {
122		pgpInfo.setVisibility(View.GONE);
123	}
124
125	public void updateChatMsgHint() {
126		if (conversation.getMode() == Conversation.MODE_MULTI) {
127			chatMsg.setHint("Send message to conference");
128		} else {
129			switch (conversation.nextMessageEncryption) {
130			case Message.ENCRYPTION_NONE:
131				chatMsg.setHint("Send plain text message");
132				break;
133			case Message.ENCRYPTION_OTR:
134				chatMsg.setHint("Send OTR encrypted message");
135				break;
136			case Message.ENCRYPTION_PGP:
137				chatMsg.setHint("Send openPGP encryted messeage");
138				break;
139			case Message.ENCRYPTION_DECRYPTED:
140				chatMsg.setHint("Send openPGP encryted messeage");
141				break;
142			default:
143				break;
144			}
145		}
146	}
147
148	@Override
149	public View onCreateView(final LayoutInflater inflater,
150			ViewGroup container, Bundle savedInstanceState) {
151
152		this.inflater = inflater;
153
154		final View view = inflater.inflate(R.layout.fragment_conversation,
155				container, false);
156		chatMsg = (EditText) view.findViewById(R.id.textinput);
157		
158		if (pastedText!=null) {
159			chatMsg.setText(pastedText);
160		}
161		
162		ImageButton sendButton = (ImageButton) view
163				.findViewById(R.id.textSendButton);
164		sendButton.setOnClickListener(this.sendMsgListener);
165
166		pgpInfo = (LinearLayout) view.findViewById(R.id.pgp_keyentry);
167		pgpInfo.setOnClickListener(clickToDecryptListener);
168		mucError = (LinearLayout) view.findViewById(R.id.muc_error);
169		mucError.setOnClickListener(clickToMuc);
170		mucErrorText = (TextView) view.findViewById(R.id.muc_error_msg);
171
172		messagesView = (ListView) view.findViewById(R.id.messages_view);
173
174		messageListAdapter = new ArrayAdapter<Message>(this.getActivity()
175				.getApplicationContext(), R.layout.message_sent,
176				this.messageList) {
177
178			private static final int SENT = 0;
179			private static final int RECIEVED = 1;
180			private static final int ERROR = 2;
181
182			@Override
183			public int getViewTypeCount() {
184				return 3;
185			}
186
187			@Override
188			public int getItemViewType(int position) {
189				if (getItem(position).getStatus() == Message.STATUS_RECIEVED) {
190					return RECIEVED;
191				} else if (getItem(position).getStatus() == Message.STATUS_ERROR) {
192					return ERROR;
193				} else {
194					return SENT;
195				}
196			}
197
198			@Override
199			public View getView(int position, View view, ViewGroup parent) {
200				Message item = getItem(position);
201				int type = getItemViewType(position);
202				ViewHolder viewHolder;
203				if (view == null) {
204					viewHolder = new ViewHolder();
205					switch (type) {
206					case SENT:
207						view = (View) inflater.inflate(R.layout.message_sent,
208								null);
209						viewHolder.imageView = (ImageView) view
210								.findViewById(R.id.message_photo);
211						viewHolder.imageView.setImageBitmap(selfBitmap);
212						viewHolder.indicator = (ImageView) view.findViewById(R.id.security_indicator);
213						break;
214					case RECIEVED:
215						view = (View) inflater.inflate(
216								R.layout.message_recieved, null);
217						viewHolder.imageView = (ImageView) view
218								.findViewById(R.id.message_photo);
219						viewHolder.indicator = (ImageView) view.findViewById(R.id.security_indicator);
220						if (item.getConversation().getMode() == Conversation.MODE_SINGLE) {
221
222							viewHolder.imageView.setImageBitmap(mBitmapCache
223									.get(item.getConversation().getName(useSubject), item
224											.getConversation().getContact(),
225											getActivity()
226													.getApplicationContext()));
227
228						}
229						break;
230					case ERROR:
231						view = (View) inflater.inflate(R.layout.message_error,
232								null);
233						viewHolder.imageView = (ImageView) view
234								.findViewById(R.id.message_photo);
235						viewHolder.imageView.setImageBitmap(mBitmapCache
236								.getError());
237						break;
238					default:
239						viewHolder = null;
240						break;
241					}
242					viewHolder.messageBody = (TextView) view
243							.findViewById(R.id.message_body);
244					viewHolder.time = (TextView) view
245							.findViewById(R.id.message_time);
246					view.setTag(viewHolder);
247				} else {
248					viewHolder = (ViewHolder) view.getTag();
249				}
250				if (type == RECIEVED) {
251					if (item.getConversation().getMode() == Conversation.MODE_MULTI) {
252						if (item.getCounterpart() != null) {
253							viewHolder.imageView.setImageBitmap(mBitmapCache
254									.get(item.getCounterpart(), null,
255											getActivity()
256													.getApplicationContext()));
257						} else {
258							viewHolder.imageView.setImageBitmap(mBitmapCache
259									.get(item.getConversation().getName(useSubject),
260											null, getActivity()
261													.getApplicationContext()));
262						}
263					}
264				}
265				String body = item.getBody();
266				if (body != null) {
267					if (item.getEncryption() == Message.ENCRYPTION_PGP) {
268						viewHolder.messageBody
269								.setText(getString(R.string.encrypted_message));
270						viewHolder.messageBody.setTextColor(0xff33B5E5);
271						viewHolder.messageBody.setTypeface(null,
272								Typeface.ITALIC);
273						viewHolder.indicator.setVisibility(View.VISIBLE);
274					} else if ((item.getEncryption() == Message.ENCRYPTION_OTR)||(item.getEncryption() == Message.ENCRYPTION_DECRYPTED)) {
275						viewHolder.messageBody.setText(body.trim());
276						viewHolder.messageBody.setTextColor(0xff000000);
277						viewHolder.messageBody.setTypeface(null,
278								Typeface.NORMAL);
279						viewHolder.indicator.setVisibility(View.VISIBLE);
280					} else {
281						viewHolder.messageBody.setText(body.trim());
282						viewHolder.messageBody.setTextColor(0xff000000);
283						viewHolder.messageBody.setTypeface(null,
284								Typeface.NORMAL);
285						if (item.getStatus() != Message.STATUS_ERROR) {
286							viewHolder.indicator.setVisibility(View.GONE);
287						}
288					}
289				} else {
290					viewHolder.indicator.setVisibility(View.GONE);
291				}
292				if (item.getStatus() == Message.STATUS_UNSEND) {
293					viewHolder.time.setTypeface(null, Typeface.ITALIC);
294					viewHolder.time.setText("sending\u2026");
295				} else {
296					viewHolder.time.setTypeface(null, Typeface.NORMAL);
297					if ((item.getConversation().getMode() == Conversation.MODE_SINGLE)
298							|| (type != RECIEVED)) {
299						viewHolder.time.setText(UIHelper
300								.readableTimeDifference(item.getTimeSent()));
301					} else {
302						viewHolder.time.setText(item.getCounterpart()
303								+ " \u00B7 "
304								+ UIHelper.readableTimeDifference(item
305										.getTimeSent()));
306					}
307				}
308				return view;
309			}
310		};
311		messagesView.setAdapter(messageListAdapter);
312
313		return view;
314	}
315
316	protected Bitmap findSelfPicture() {
317		SharedPreferences sharedPref = PreferenceManager
318				.getDefaultSharedPreferences(getActivity()
319						.getApplicationContext());
320		boolean showPhoneSelfContactPicture = sharedPref.getBoolean(
321				"show_phone_selfcontact_picture", true);
322
323		return UIHelper.getSelfContactPicture(conversation.getAccount(), 200,
324				showPhoneSelfContactPicture, getActivity());
325	}
326
327	@Override
328	public void onStart() {
329		super.onStart();
330		this.activity = (ConversationActivity) getActivity();
331		SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(activity);
332		this.useSubject = preferences.getBoolean("use_subject_in_muc", true);
333		if (activity.xmppConnectionServiceBound) {
334			this.onBackendConnected();
335		}
336	}
337
338	public void onBackendConnected() {
339		this.conversation = activity.getSelectedConversation();
340		if (this.conversation == null) {
341			return;
342		}
343		this.selfBitmap = findSelfPicture();
344		updateMessages();
345		// rendering complete. now go tell activity to close pane
346		if (activity.getSlidingPaneLayout().isSlideable()) {
347			if (!activity.shouldPaneBeOpen()) {
348				activity.getSlidingPaneLayout().closePane();
349				activity.getActionBar().setDisplayHomeAsUpEnabled(true);
350				activity.getActionBar().setTitle(conversation.getName(useSubject));
351				activity.invalidateOptionsMenu();
352
353			}
354		}
355		if (queuedPqpMessage != null) {
356			this.conversation.nextMessageEncryption = Message.ENCRYPTION_PGP;
357			Message message = new Message(conversation, queuedPqpMessage,
358					Message.ENCRYPTION_PGP);
359			sendPgpMessage(message);
360		}
361		if (conversation.getMode() == Conversation.MODE_MULTI) {
362			activity.xmppConnectionService
363					.setOnRenameListener(new OnRenameListener() {
364
365						@Override
366						public void onRename(final boolean success) {
367							activity.xmppConnectionService.updateConversation(conversation);
368							getActivity().runOnUiThread(new Runnable() {
369
370								@Override
371								public void run() {
372									if (success) {
373										Toast.makeText(
374												getActivity(),
375												"Your nickname has been changed",
376												Toast.LENGTH_SHORT).show();
377									} else {
378										Toast.makeText(getActivity(),
379												"Nichname is already in use",
380												Toast.LENGTH_SHORT).show();
381									}
382								}
383							});
384						}
385					});
386		}
387	}
388
389	public void updateMessages() {
390		ConversationActivity activity = (ConversationActivity) getActivity();
391		if (this.conversation != null) {
392			List<Message> encryptedMessages = new LinkedList<Message>();
393			for (Message message : this.conversation.getMessages()) {
394				if (message.getEncryption() == Message.ENCRYPTION_PGP) {
395					encryptedMessages.add(message);
396				}
397			}
398			if (encryptedMessages.size() > 0) {
399				DecryptMessage task = new DecryptMessage();
400				Message[] msgs = new Message[encryptedMessages.size()];
401				task.execute(encryptedMessages.toArray(msgs));
402			}
403			this.messageList.clear();
404			this.messageList.addAll(this.conversation.getMessages());
405			this.messageListAdapter.notifyDataSetChanged();
406			if (conversation.getMode() == Conversation.MODE_SINGLE) {
407				if (messageList.size() >= 1) {
408					int latestEncryption = this.conversation.getLatestMessage()
409							.getEncryption();
410					if (latestEncryption == Message.ENCRYPTION_DECRYPTED) {
411						conversation.nextMessageEncryption = Message.ENCRYPTION_PGP;
412					} else {
413						conversation.nextMessageEncryption = latestEncryption;
414					}
415					makeFingerprintWarning(latestEncryption);
416				}
417			} else {
418				if (conversation.getMucOptions().getError() != 0) {
419					mucError.setVisibility(View.VISIBLE);
420					if (conversation.getMucOptions().getError() == MucOptions.ERROR_NICK_IN_USE) {
421						mucErrorText.setText(getString(R.string.nick_in_use));
422					}
423				} else {
424					mucError.setVisibility(View.GONE);
425				}
426			}
427			getActivity().invalidateOptionsMenu();
428			updateChatMsgHint();
429			int size = this.messageList.size();
430			if (size >= 1)
431				messagesView.setSelection(size - 1);
432			if (!activity.shouldPaneBeOpen()) {
433				conversation.markRead();
434				// TODO update notifications
435				UIHelper.updateNotification(getActivity(),
436						activity.getConversationList(), null, false);
437				activity.updateConversationList();
438			}
439		}
440	}
441
442	protected void makeFingerprintWarning(int latestEncryption) {
443		final LinearLayout fingerprintWarning = (LinearLayout) getView()
444				.findViewById(R.id.new_fingerprint);
445		if (conversation.getContact() != null) {
446			Set<String> knownFingerprints = conversation.getContact()
447					.getOtrFingerprints();
448			if ((latestEncryption == Message.ENCRYPTION_OTR)
449					&& (conversation.hasValidOtrSession()
450							&& (conversation.getOtrSession().getSessionStatus() == SessionStatus.ENCRYPTED) && (!knownFingerprints
451								.contains(conversation.getOtrFingerprint())))) {
452				fingerprintWarning.setVisibility(View.VISIBLE);
453				TextView fingerprint = (TextView) getView().findViewById(
454						R.id.otr_fingerprint);
455				fingerprint.setText(conversation.getOtrFingerprint());
456				fingerprintWarning.setOnClickListener(new OnClickListener() {
457
458					@Override
459					public void onClick(View v) {
460						AlertDialog dialog = UIHelper
461								.getVerifyFingerprintDialog(
462										(ConversationActivity) getActivity(),
463										conversation, fingerprintWarning);
464						dialog.show();
465					}
466				});
467			} else {
468				fingerprintWarning.setVisibility(View.GONE);
469			}
470		} else {
471			fingerprintWarning.setVisibility(View.GONE);
472		}
473	}
474
475	protected void sendPlainTextMessage(Message message) {
476		ConversationActivity activity = (ConversationActivity) getActivity();
477		activity.xmppConnectionService.sendMessage(message, null);
478		chatMsg.setText("");
479	}
480
481	protected void sendPgpMessage(final Message message) {
482		ConversationActivity activity = (ConversationActivity) getActivity();
483		final XmppConnectionService xmppService = activity.xmppConnectionService;
484		Contact contact = message.getConversation().getContact();
485		Account account = message.getConversation().getAccount();
486		if (activity.hasPgp()) {
487			if (contact.getPgpKeyId() != 0) {
488				try {
489					message.setEncryptedBody(xmppService.getPgpEngine().encrypt(account, contact.getPgpKeyId(), message.getBody()));
490					xmppService.sendMessage(message, null);
491					chatMsg.setText("");
492				} catch (UserInputRequiredException e) {
493					try {
494						getActivity().startIntentSenderForResult(e.getPendingIntent().getIntentSender(),
495								ConversationActivity.REQUEST_SEND_MESSAGE, null, 0,
496								0, 0);
497					} catch (SendIntentException e1) {
498						Log.d("xmppService","failed to start intent to send message");
499					}
500				} catch (OpenPgpException e) {
501					Log.d("xmppService","error encrypting with pgp: "+e.getOpenPgpError().getMessage());
502				}
503			} else {
504				AlertDialog.Builder builder = new AlertDialog.Builder(
505						getActivity());
506				builder.setTitle("No openPGP key found");
507				builder.setIconAttribute(android.R.attr.alertDialogIcon);
508				builder.setMessage("There is no openPGP key assoziated with this contact");
509				builder.setNegativeButton("Cancel", null);
510				builder.setPositiveButton("Send plain text",
511						new DialogInterface.OnClickListener() {
512
513							@Override
514							public void onClick(DialogInterface dialog,
515									int which) {
516								conversation.nextMessageEncryption = Message.ENCRYPTION_NONE;
517								message.setEncryption(Message.ENCRYPTION_NONE);
518								xmppService.sendMessage(message, null);
519								chatMsg.setText("");
520							}
521						});
522				builder.create().show();
523			}
524		}
525	}
526
527	protected void sendOtrMessage(final Message message) {
528		ConversationActivity activity = (ConversationActivity) getActivity();
529		final XmppConnectionService xmppService = activity.xmppConnectionService;
530		if (conversation.hasValidOtrSession()) {
531			activity.xmppConnectionService.sendMessage(message, null);
532			chatMsg.setText("");
533		} else {
534			Hashtable<String, Integer> presences;
535			if (conversation.getContact() != null) {
536				presences = conversation.getContact().getPresences();
537			} else {
538				presences = null;
539			}
540			if ((presences == null) || (presences.size() == 0)) {
541				AlertDialog.Builder builder = new AlertDialog.Builder(
542						getActivity());
543				builder.setTitle("Contact is offline");
544				builder.setIconAttribute(android.R.attr.alertDialogIcon);
545				builder.setMessage("Sending OTR encrypted messages to an offline contact is impossible.");
546				builder.setPositiveButton("Send plain text",
547						new DialogInterface.OnClickListener() {
548
549							@Override
550							public void onClick(DialogInterface dialog,
551									int which) {
552								conversation.nextMessageEncryption = Message.ENCRYPTION_NONE;
553								message.setEncryption(Message.ENCRYPTION_NONE);
554								xmppService.sendMessage(message, null);
555								chatMsg.setText("");
556							}
557						});
558				builder.setNegativeButton("Cancel", null);
559				builder.create().show();
560			} else if (presences.size() == 1) {
561				xmppService.sendMessage(message, (String) presences.keySet()
562						.toArray()[0]);
563				chatMsg.setText("");
564			} else {
565				AlertDialog.Builder builder = new AlertDialog.Builder(
566						getActivity());
567				builder.setTitle("Choose Presence");
568				final String[] presencesArray = new String[presences.size()];
569				presences.keySet().toArray(presencesArray);
570				builder.setItems(presencesArray,
571						new DialogInterface.OnClickListener() {
572
573							@Override
574							public void onClick(DialogInterface dialog,
575									int which) {
576								xmppService.sendMessage(message,
577										presencesArray[which]);
578								chatMsg.setText("");
579							}
580						});
581				builder.create().show();
582			}
583		}
584	}
585
586	private static class ViewHolder {
587
588		protected ImageView indicator;
589		protected TextView time;
590		protected TextView messageBody;
591		protected ImageView imageView;
592
593	}
594
595	private class BitmapCache {
596		private HashMap<String, Bitmap> bitmaps = new HashMap<String, Bitmap>();
597		private Bitmap error = null;
598
599		public Bitmap get(String name, Contact contact, Context context) {
600			if (bitmaps.containsKey(name)) {
601				return bitmaps.get(name);
602			} else {
603				Bitmap bm = UIHelper.getContactPicture(contact, name, 200, context);
604				bitmaps.put(name, bm);
605				return bm;
606			}
607		}
608
609		public Bitmap getError() {
610			if (error == null) {
611				error = UIHelper.getErrorPicture(200);
612			}
613			return error;
614		}
615	}
616
617	class DecryptMessage extends AsyncTask<Message, Void, Boolean> {
618
619		@Override
620		protected Boolean doInBackground(Message... params) {
621			final ConversationActivity activity = (ConversationActivity) getActivity();
622			askForPassphraseIntent = null;
623			for (int i = 0; i < params.length; ++i) {
624				if (params[i].getEncryption() == Message.ENCRYPTION_PGP) {
625					String body = params[i].getBody();
626					String decrypted = null;
627					if (activity == null) {
628						return false;
629					} else if (!activity.xmppConnectionServiceBound) {
630						return false;
631					}
632					try {
633						decrypted = activity.xmppConnectionService
634								.getPgpEngine().decrypt(conversation.getAccount(),body);
635					} catch (UserInputRequiredException e) {
636						askForPassphraseIntent = e.getPendingIntent()
637								.getIntentSender();
638						activity.runOnUiThread(new Runnable() {
639
640							@Override
641							public void run() {
642								pgpInfo.setVisibility(View.VISIBLE);
643							}
644						});
645
646						return false;
647
648					} catch (OpenPgpException e) {
649						Log.d("gultsch", "error decrypting pgp");
650					}
651					if (decrypted != null) {
652						params[i].setBody(decrypted);
653						params[i].setEncryption(Message.ENCRYPTION_DECRYPTED);
654						activity.xmppConnectionService.updateMessage(params[i]);
655					}
656					if (activity != null) {
657						activity.runOnUiThread(new Runnable() {
658
659							@Override
660							public void run() {
661								messageListAdapter.notifyDataSetChanged();
662							}
663						});
664					}
665				}
666				if (activity != null) {
667					activity.runOnUiThread(new Runnable() {
668
669						@Override
670						public void run() {
671							activity.updateConversationList();
672						}
673					});
674				}
675			}
676			return true;
677		}
678
679	}
680
681	public void setText(String text) {
682		this.pastedText = text;
683	}
684}