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