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