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.Contact;
 16import eu.siacs.conversations.entities.Conversation;
 17import eu.siacs.conversations.entities.Message;
 18import eu.siacs.conversations.entities.MucOptions;
 19import eu.siacs.conversations.entities.MucOptions.OnRenameListener;
 20import eu.siacs.conversations.services.XmppConnectionService;
 21import eu.siacs.conversations.utils.UIHelper;
 22import android.app.AlertDialog;
 23import android.app.Fragment;
 24import android.content.Context;
 25import android.content.DialogInterface;
 26import android.content.Intent;
 27import android.content.IntentSender;
 28import android.content.SharedPreferences;
 29import android.content.IntentSender.SendIntentException;
 30import android.graphics.Bitmap;
 31import android.graphics.Typeface;
 32import android.os.AsyncTask;
 33import android.os.Bundle;
 34import android.preference.PreferenceManager;
 35import android.util.Log;
 36import android.view.LayoutInflater;
 37import android.view.View;
 38import android.view.View.OnClickListener;
 39import android.view.ViewGroup;
 40import android.widget.ArrayAdapter;
 41import android.widget.EditText;
 42import android.widget.LinearLayout;
 43import android.widget.ListView;
 44import android.widget.ImageButton;
 45import android.widget.ImageView;
 46import android.widget.TextView;
 47import android.widget.Toast;
 48
 49public class ConversationFragment extends Fragment {
 50
 51	protected Conversation conversation;
 52	protected ListView messagesView;
 53	protected LayoutInflater inflater;
 54	protected List<Message> messageList = new ArrayList<Message>();
 55	protected ArrayAdapter<Message> messageListAdapter;
 56	protected Contact contact;
 57	protected BitmapCache mBitmapCache = new BitmapCache();
 58
 59	protected String queuedPqpMessage = null;
 60
 61	private EditText chatMsg;
 62	private String pastedText = null;
 63
 64	protected Bitmap selfBitmap;
 65	
 66	private boolean useSubject = true;
 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(
 95							askForPassphraseIntent,
 96							ConversationActivity.REQUEST_DECRYPT_PGP, null, 0,
 97							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		
156		if (pastedText!=null) {
157			chatMsg.setText(pastedText);
158		}
159		
160		ImageButton sendButton = (ImageButton) view
161				.findViewById(R.id.textSendButton);
162		sendButton.setOnClickListener(this.sendMsgListener);
163
164		pgpInfo = (LinearLayout) view.findViewById(R.id.pgp_keyentry);
165		pgpInfo.setOnClickListener(clickToDecryptListener);
166		mucError = (LinearLayout) view.findViewById(R.id.muc_error);
167		mucError.setOnClickListener(clickToMuc);
168		mucErrorText = (TextView) view.findViewById(R.id.muc_error_msg);
169
170		messagesView = (ListView) view.findViewById(R.id.messages_view);
171
172		messageListAdapter = new ArrayAdapter<Message>(this.getActivity()
173				.getApplicationContext(), R.layout.message_sent,
174				this.messageList) {
175
176			private static final int SENT = 0;
177			private static final int RECIEVED = 1;
178			private static final int ERROR = 2;
179
180			@Override
181			public int getViewTypeCount() {
182				return 3;
183			}
184
185			@Override
186			public int getItemViewType(int position) {
187				if (getItem(position).getStatus() == Message.STATUS_RECIEVED) {
188					return RECIEVED;
189				} else if (getItem(position).getStatus() == Message.STATUS_ERROR) {
190					return ERROR;
191				} else {
192					return SENT;
193				}
194			}
195
196			@Override
197			public View getView(int position, View view, ViewGroup parent) {
198				Message item = getItem(position);
199				int type = getItemViewType(position);
200				ViewHolder viewHolder;
201				if (view == null) {
202					viewHolder = new ViewHolder();
203					switch (type) {
204					case SENT:
205						view = (View) inflater.inflate(R.layout.message_sent,
206								null);
207						viewHolder.imageView = (ImageView) view
208								.findViewById(R.id.message_photo);
209						viewHolder.imageView.setImageBitmap(selfBitmap);
210						break;
211					case RECIEVED:
212						view = (View) inflater.inflate(
213								R.layout.message_recieved, null);
214						viewHolder.imageView = (ImageView) view
215								.findViewById(R.id.message_photo);
216						if (item.getConversation().getMode() == Conversation.MODE_SINGLE) {
217
218							viewHolder.imageView.setImageBitmap(mBitmapCache
219									.get(item.getConversation().getName(useSubject), item
220											.getConversation().getContact(),
221											getActivity()
222													.getApplicationContext()));
223
224						}
225						break;
226					case ERROR:
227						view = (View) inflater.inflate(R.layout.message_error,
228								null);
229						viewHolder.imageView = (ImageView) view
230								.findViewById(R.id.message_photo);
231						viewHolder.imageView.setImageBitmap(mBitmapCache
232								.getError());
233						break;
234					default:
235						viewHolder = null;
236						break;
237					}
238					viewHolder.messageBody = (TextView) view
239							.findViewById(R.id.message_body);
240					viewHolder.time = (TextView) view
241							.findViewById(R.id.message_time);
242					view.setTag(viewHolder);
243				} else {
244					viewHolder = (ViewHolder) view.getTag();
245				}
246				if (type == RECIEVED) {
247					if (item.getConversation().getMode() == Conversation.MODE_MULTI) {
248						if (item.getCounterpart() != null) {
249							viewHolder.imageView.setImageBitmap(mBitmapCache
250									.get(item.getCounterpart(), null,
251											getActivity()
252													.getApplicationContext()));
253						} else {
254							viewHolder.imageView.setImageBitmap(mBitmapCache
255									.get(item.getConversation().getName(useSubject),
256											null, getActivity()
257													.getApplicationContext()));
258						}
259					}
260				}
261				String body = item.getBody();
262				if (body != null) {
263					if (item.getEncryption() == Message.ENCRYPTION_PGP) {
264						viewHolder.messageBody
265								.setText(getString(R.string.encrypted_message));
266						viewHolder.messageBody.setTextColor(0xff33B5E5);
267						viewHolder.messageBody.setTypeface(null,
268								Typeface.ITALIC);
269					} else {
270						viewHolder.messageBody.setText(body.trim());
271						viewHolder.messageBody.setTextColor(0xff000000);
272						viewHolder.messageBody.setTypeface(null,
273								Typeface.NORMAL);
274					}
275				}
276				if (item.getStatus() == Message.STATUS_UNSEND) {
277					viewHolder.time.setTypeface(null, Typeface.ITALIC);
278					viewHolder.time.setText("sending\u2026");
279				} else {
280					viewHolder.time.setTypeface(null, Typeface.NORMAL);
281					if ((item.getConversation().getMode() == Conversation.MODE_SINGLE)
282							|| (type != RECIEVED)) {
283						viewHolder.time.setText(UIHelper
284								.readableTimeDifference(item.getTimeSent()));
285					} else {
286						viewHolder.time.setText(item.getCounterpart()
287								+ " \u00B7 "
288								+ UIHelper.readableTimeDifference(item
289										.getTimeSent()));
290					}
291				}
292				return view;
293			}
294		};
295		messagesView.setAdapter(messageListAdapter);
296
297		return view;
298	}
299
300	protected Bitmap findSelfPicture() {
301		SharedPreferences sharedPref = PreferenceManager
302				.getDefaultSharedPreferences(getActivity()
303						.getApplicationContext());
304		boolean showPhoneSelfContactPicture = sharedPref.getBoolean(
305				"show_phone_selfcontact_picture", true);
306
307		return UIHelper.getSelfContactPicture(conversation.getAccount(), 200,
308				showPhoneSelfContactPicture, getActivity());
309	}
310
311	@Override
312	public void onStart() {
313		super.onStart();
314		ConversationActivity activity = (ConversationActivity) getActivity();
315
316		if (activity.xmppConnectionServiceBound) {
317			this.onBackendConnected();
318		}
319	}
320
321	public void onBackendConnected() {
322		final ConversationActivity activity = (ConversationActivity) getActivity();
323		activity.registerListener();
324		this.conversation = activity.getSelectedConversation();
325		if (this.conversation == null) {
326			return;
327		}
328		this.selfBitmap = findSelfPicture();
329		updateMessages();
330		// rendering complete. now go tell activity to close pane
331		if (activity.getSlidingPaneLayout().isSlideable()) {
332			if (!activity.shouldPaneBeOpen()) {
333				activity.getSlidingPaneLayout().closePane();
334				activity.getActionBar().setDisplayHomeAsUpEnabled(true);
335				activity.getActionBar().setTitle(conversation.getName(useSubject));
336				activity.invalidateOptionsMenu();
337
338			}
339		}
340		if (queuedPqpMessage != null) {
341			this.conversation.nextMessageEncryption = Message.ENCRYPTION_PGP;
342			Message message = new Message(conversation, queuedPqpMessage,
343					Message.ENCRYPTION_PGP);
344			sendPgpMessage(message);
345		}
346		if (conversation.getMode() == Conversation.MODE_MULTI) {
347			activity.xmppConnectionService
348					.setOnRenameListener(new OnRenameListener() {
349
350						@Override
351						public void onRename(final boolean success) {
352							activity.xmppConnectionService.updateConversation(conversation);
353							getActivity().runOnUiThread(new Runnable() {
354
355								@Override
356								public void run() {
357									if (success) {
358										Toast.makeText(
359												getActivity(),
360												"Your nickname has been changed",
361												Toast.LENGTH_SHORT).show();
362									} else {
363										Toast.makeText(getActivity(),
364												"Nichname is already in use",
365												Toast.LENGTH_SHORT).show();
366									}
367								}
368							});
369						}
370					});
371		}
372	}
373
374	public void updateMessages() {
375		ConversationActivity activity = (ConversationActivity) getActivity();
376		List<Message> encryptedMessages = new LinkedList<Message>();
377		for (Message message : this.conversation.getMessages()) {
378			if (message.getEncryption() == Message.ENCRYPTION_PGP) {
379				encryptedMessages.add(message);
380			}
381		}
382		if (encryptedMessages.size() > 0) {
383			DecryptMessage task = new DecryptMessage();
384			Message[] msgs = new Message[encryptedMessages.size()];
385			task.execute(encryptedMessages.toArray(msgs));
386		}
387		this.messageList.clear();
388		this.messageList.addAll(this.conversation.getMessages());
389		this.messageListAdapter.notifyDataSetChanged();
390		if (conversation.getMode() == Conversation.MODE_SINGLE) {
391			if (messageList.size() >= 1) {
392				int latestEncryption = this.conversation.getLatestMessage()
393						.getEncryption();
394				if (latestEncryption == Message.ENCRYPTION_DECRYPTED) {
395					conversation.nextMessageEncryption = Message.ENCRYPTION_PGP;
396				} else {
397					conversation.nextMessageEncryption = latestEncryption;
398				}
399				makeFingerprintWarning(latestEncryption);
400			}
401		} else {
402			if (conversation.getMucOptions().getError() != 0) {
403				mucError.setVisibility(View.VISIBLE);
404				if (conversation.getMucOptions().getError() == MucOptions.ERROR_NICK_IN_USE) {
405					mucErrorText.setText(getString(R.string.nick_in_use));
406				}
407			} else {
408				mucError.setVisibility(View.GONE);
409			}
410		}
411		getActivity().invalidateOptionsMenu();
412		updateChatMsgHint();
413		int size = this.messageList.size();
414		if (size >= 1)
415			messagesView.setSelection(size - 1);
416		if (!activity.shouldPaneBeOpen()) {
417			conversation.markRead();
418			// TODO update notifications
419			UIHelper.updateNotification(getActivity(),
420					activity.getConversationList(), null, false);
421			activity.updateConversationList();
422		}
423	}
424
425	protected void makeFingerprintWarning(int latestEncryption) {
426		final LinearLayout fingerprintWarning = (LinearLayout) getView()
427				.findViewById(R.id.new_fingerprint);
428		if (conversation.getContact() != null) {
429			Set<String> knownFingerprints = conversation.getContact()
430					.getOtrFingerprints();
431			if ((latestEncryption == Message.ENCRYPTION_OTR)
432					&& (conversation.hasValidOtrSession()
433							&& (conversation.getOtrSession().getSessionStatus() == SessionStatus.ENCRYPTED) && (!knownFingerprints
434								.contains(conversation.getOtrFingerprint())))) {
435				fingerprintWarning.setVisibility(View.VISIBLE);
436				TextView fingerprint = (TextView) getView().findViewById(
437						R.id.otr_fingerprint);
438				fingerprint.setText(conversation.getOtrFingerprint());
439				fingerprintWarning.setOnClickListener(new OnClickListener() {
440
441					@Override
442					public void onClick(View v) {
443						AlertDialog dialog = UIHelper
444								.getVerifyFingerprintDialog(
445										(ConversationActivity) getActivity(),
446										conversation, fingerprintWarning);
447						dialog.show();
448					}
449				});
450			} else {
451				fingerprintWarning.setVisibility(View.GONE);
452			}
453		} else {
454			fingerprintWarning.setVisibility(View.GONE);
455		}
456	}
457
458	protected void sendPlainTextMessage(Message message) {
459		ConversationActivity activity = (ConversationActivity) getActivity();
460		activity.xmppConnectionService.sendMessage(message, null);
461		chatMsg.setText("");
462	}
463
464	protected void sendPgpMessage(final Message message) {
465		ConversationActivity activity = (ConversationActivity) getActivity();
466		final XmppConnectionService xmppService = activity.xmppConnectionService;
467		Contact contact = message.getConversation().getContact();
468		if (activity.hasPgp()) {
469			if (contact.getPgpKeyId() != 0) {
470				xmppService.sendMessage(message, null);
471				chatMsg.setText("");
472			} else {
473				AlertDialog.Builder builder = new AlertDialog.Builder(
474						getActivity());
475				builder.setTitle("No openPGP key found");
476				builder.setIconAttribute(android.R.attr.alertDialogIcon);
477				builder.setMessage("There is no openPGP key assoziated with this contact");
478				builder.setNegativeButton("Cancel", null);
479				builder.setPositiveButton("Send plain text",
480						new DialogInterface.OnClickListener() {
481
482							@Override
483							public void onClick(DialogInterface dialog,
484									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, Contact contact, Context context) {
568			if (bitmaps.containsKey(name)) {
569				return bitmaps.get(name);
570			} else {
571				Bitmap bm = UIHelper.getContactPicture(contact, name, 200, context);
572				bitmaps.put(name, bm);
573				return bm;
574			}
575		}
576
577		public Bitmap getError() {
578			if (error == null) {
579				error = UIHelper.getErrorPicture(200);
580			}
581			return error;
582		}
583	}
584
585	class DecryptMessage extends AsyncTask<Message, Void, Boolean> {
586
587		@Override
588		protected Boolean doInBackground(Message... params) {
589			final ConversationActivity activity = (ConversationActivity) getActivity();
590			askForPassphraseIntent = null;
591			for (int i = 0; i < params.length; ++i) {
592				if (params[i].getEncryption() == Message.ENCRYPTION_PGP) {
593					String body = params[i].getBody();
594					String decrypted = null;
595					if (activity == null) {
596						return false;
597					} else if (!activity.xmppConnectionServiceBound) {
598						return false;
599					}
600					try {
601						decrypted = activity.xmppConnectionService
602								.getPgpEngine().decrypt(body);
603					} catch (UserInputRequiredException e) {
604						askForPassphraseIntent = e.getPendingIntent()
605								.getIntentSender();
606						activity.runOnUiThread(new Runnable() {
607
608							@Override
609							public void run() {
610								pgpInfo.setVisibility(View.VISIBLE);
611							}
612						});
613
614						return false;
615
616					} catch (OpenPgpException e) {
617						Log.d("gultsch", "error decrypting pgp");
618					}
619					if (decrypted != null) {
620						params[i].setBody(decrypted);
621						params[i].setEncryption(Message.ENCRYPTION_DECRYPTED);
622						activity.xmppConnectionService.updateMessage(params[i]);
623					}
624					if (activity != null) {
625						activity.runOnUiThread(new Runnable() {
626
627							@Override
628							public void run() {
629								messageListAdapter.notifyDataSetChanged();
630							}
631						});
632					}
633				}
634				if (activity != null) {
635					activity.runOnUiThread(new Runnable() {
636
637						@Override
638						public void run() {
639							activity.updateConversationList();
640						}
641					});
642				}
643			}
644			return true;
645		}
646
647	}
648
649	public void setText(String text) {
650		this.pastedText = text;
651	}
652}