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