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