ConversationFragment.java

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