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						viewHolder.indicator = (ImageView) view.findViewById(R.id.security_indicator);
211						break;
212					case RECIEVED:
213						view = (View) inflater.inflate(
214								R.layout.message_recieved, null);
215						viewHolder.imageView = (ImageView) view
216								.findViewById(R.id.message_photo);
217						viewHolder.indicator = (ImageView) view.findViewById(R.id.security_indicator);
218						if (item.getConversation().getMode() == Conversation.MODE_SINGLE) {
219
220							viewHolder.imageView.setImageBitmap(mBitmapCache
221									.get(item.getConversation().getName(useSubject), item
222											.getConversation().getContact(),
223											getActivity()
224													.getApplicationContext()));
225
226						}
227						break;
228					case ERROR:
229						view = (View) inflater.inflate(R.layout.message_error,
230								null);
231						viewHolder.imageView = (ImageView) view
232								.findViewById(R.id.message_photo);
233						viewHolder.imageView.setImageBitmap(mBitmapCache
234								.getError());
235						break;
236					default:
237						viewHolder = null;
238						break;
239					}
240					viewHolder.messageBody = (TextView) view
241							.findViewById(R.id.message_body);
242					viewHolder.time = (TextView) view
243							.findViewById(R.id.message_time);
244					view.setTag(viewHolder);
245				} else {
246					viewHolder = (ViewHolder) view.getTag();
247				}
248				if (type == RECIEVED) {
249					if (item.getConversation().getMode() == Conversation.MODE_MULTI) {
250						if (item.getCounterpart() != null) {
251							viewHolder.imageView.setImageBitmap(mBitmapCache
252									.get(item.getCounterpart(), null,
253											getActivity()
254													.getApplicationContext()));
255						} else {
256							viewHolder.imageView.setImageBitmap(mBitmapCache
257									.get(item.getConversation().getName(useSubject),
258											null, getActivity()
259													.getApplicationContext()));
260						}
261					}
262				}
263				String body = item.getBody();
264				if (body != null) {
265					if (item.getEncryption() == Message.ENCRYPTION_PGP) {
266						viewHolder.messageBody
267								.setText(getString(R.string.encrypted_message));
268						viewHolder.messageBody.setTextColor(0xff33B5E5);
269						viewHolder.messageBody.setTypeface(null,
270								Typeface.ITALIC);
271						viewHolder.indicator.setVisibility(View.VISIBLE);
272					} else if ((item.getEncryption() == Message.ENCRYPTION_OTR)||(item.getEncryption() == Message.ENCRYPTION_DECRYPTED)) {
273						viewHolder.messageBody.setText(body.trim());
274						viewHolder.messageBody.setTextColor(0xff000000);
275						viewHolder.messageBody.setTypeface(null,
276								Typeface.NORMAL);
277						viewHolder.indicator.setVisibility(View.VISIBLE);
278					} else {
279						viewHolder.messageBody.setText(body.trim());
280						viewHolder.messageBody.setTextColor(0xff000000);
281						viewHolder.messageBody.setTypeface(null,
282								Typeface.NORMAL);
283						if (item.getStatus() != Message.STATUS_ERROR) {
284							viewHolder.indicator.setVisibility(View.GONE);
285						}
286					}
287				} else {
288					viewHolder.indicator.setVisibility(View.GONE);
289				}
290				if (item.getStatus() == Message.STATUS_UNSEND) {
291					viewHolder.time.setTypeface(null, Typeface.ITALIC);
292					viewHolder.time.setText("sending\u2026");
293				} else {
294					viewHolder.time.setTypeface(null, Typeface.NORMAL);
295					if ((item.getConversation().getMode() == Conversation.MODE_SINGLE)
296							|| (type != RECIEVED)) {
297						viewHolder.time.setText(UIHelper
298								.readableTimeDifference(item.getTimeSent()));
299					} else {
300						viewHolder.time.setText(item.getCounterpart()
301								+ " \u00B7 "
302								+ UIHelper.readableTimeDifference(item
303										.getTimeSent()));
304					}
305				}
306				return view;
307			}
308		};
309		messagesView.setAdapter(messageListAdapter);
310
311		return view;
312	}
313
314	protected Bitmap findSelfPicture() {
315		SharedPreferences sharedPref = PreferenceManager
316				.getDefaultSharedPreferences(getActivity()
317						.getApplicationContext());
318		boolean showPhoneSelfContactPicture = sharedPref.getBoolean(
319				"show_phone_selfcontact_picture", true);
320
321		return UIHelper.getSelfContactPicture(conversation.getAccount(), 200,
322				showPhoneSelfContactPicture, getActivity());
323	}
324
325	@Override
326	public void onStart() {
327		super.onStart();
328		ConversationActivity activity = (ConversationActivity) getActivity();
329		SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(activity);
330		this.useSubject = preferences.getBoolean("use_subject_in_muc", true);
331		if (activity.xmppConnectionServiceBound) {
332			this.onBackendConnected();
333		}
334	}
335
336	public void onBackendConnected() {
337		final ConversationActivity activity = (ConversationActivity) getActivity();
338		activity.registerListener();
339		this.conversation = activity.getSelectedConversation();
340		if (this.conversation == null) {
341			return;
342		}
343		this.selfBitmap = findSelfPicture();
344		updateMessages();
345		// rendering complete. now go tell activity to close pane
346		if (activity.getSlidingPaneLayout().isSlideable()) {
347			if (!activity.shouldPaneBeOpen()) {
348				activity.getSlidingPaneLayout().closePane();
349				activity.getActionBar().setDisplayHomeAsUpEnabled(true);
350				activity.getActionBar().setTitle(conversation.getName(useSubject));
351				activity.invalidateOptionsMenu();
352
353			}
354		}
355		if (queuedPqpMessage != null) {
356			this.conversation.nextMessageEncryption = Message.ENCRYPTION_PGP;
357			Message message = new Message(conversation, queuedPqpMessage,
358					Message.ENCRYPTION_PGP);
359			sendPgpMessage(message);
360		}
361		if (conversation.getMode() == Conversation.MODE_MULTI) {
362			activity.xmppConnectionService
363					.setOnRenameListener(new OnRenameListener() {
364
365						@Override
366						public void onRename(final boolean success) {
367							activity.xmppConnectionService.updateConversation(conversation);
368							getActivity().runOnUiThread(new Runnable() {
369
370								@Override
371								public void run() {
372									if (success) {
373										Toast.makeText(
374												getActivity(),
375												"Your nickname has been changed",
376												Toast.LENGTH_SHORT).show();
377									} else {
378										Toast.makeText(getActivity(),
379												"Nichname is already in use",
380												Toast.LENGTH_SHORT).show();
381									}
382								}
383							});
384						}
385					});
386		}
387	}
388
389	public void updateMessages() {
390		ConversationActivity activity = (ConversationActivity) getActivity();
391		if (this.conversation != null) {
392			List<Message> encryptedMessages = new LinkedList<Message>();
393			for (Message message : this.conversation.getMessages()) {
394				if (message.getEncryption() == Message.ENCRYPTION_PGP) {
395					encryptedMessages.add(message);
396				}
397			}
398			if (encryptedMessages.size() > 0) {
399				DecryptMessage task = new DecryptMessage();
400				Message[] msgs = new Message[encryptedMessages.size()];
401				task.execute(encryptedMessages.toArray(msgs));
402			}
403			this.messageList.clear();
404			this.messageList.addAll(this.conversation.getMessages());
405			this.messageListAdapter.notifyDataSetChanged();
406			if (conversation.getMode() == Conversation.MODE_SINGLE) {
407				if (messageList.size() >= 1) {
408					int latestEncryption = this.conversation.getLatestMessage()
409							.getEncryption();
410					if (latestEncryption == Message.ENCRYPTION_DECRYPTED) {
411						conversation.nextMessageEncryption = Message.ENCRYPTION_PGP;
412					} else {
413						conversation.nextMessageEncryption = latestEncryption;
414					}
415					makeFingerprintWarning(latestEncryption);
416				}
417			} else {
418				if (conversation.getMucOptions().getError() != 0) {
419					mucError.setVisibility(View.VISIBLE);
420					if (conversation.getMucOptions().getError() == MucOptions.ERROR_NICK_IN_USE) {
421						mucErrorText.setText(getString(R.string.nick_in_use));
422					}
423				} else {
424					mucError.setVisibility(View.GONE);
425				}
426			}
427			getActivity().invalidateOptionsMenu();
428			updateChatMsgHint();
429			int size = this.messageList.size();
430			if (size >= 1)
431				messagesView.setSelection(size - 1);
432			if (!activity.shouldPaneBeOpen()) {
433				conversation.markRead();
434				// TODO update notifications
435				UIHelper.updateNotification(getActivity(),
436						activity.getConversationList(), null, false);
437				activity.updateConversationList();
438			}
439		}
440	}
441
442	protected void makeFingerprintWarning(int latestEncryption) {
443		final LinearLayout fingerprintWarning = (LinearLayout) getView()
444				.findViewById(R.id.new_fingerprint);
445		if (conversation.getContact() != null) {
446			Set<String> knownFingerprints = conversation.getContact()
447					.getOtrFingerprints();
448			if ((latestEncryption == Message.ENCRYPTION_OTR)
449					&& (conversation.hasValidOtrSession()
450							&& (conversation.getOtrSession().getSessionStatus() == SessionStatus.ENCRYPTED) && (!knownFingerprints
451								.contains(conversation.getOtrFingerprint())))) {
452				fingerprintWarning.setVisibility(View.VISIBLE);
453				TextView fingerprint = (TextView) getView().findViewById(
454						R.id.otr_fingerprint);
455				fingerprint.setText(conversation.getOtrFingerprint());
456				fingerprintWarning.setOnClickListener(new OnClickListener() {
457
458					@Override
459					public void onClick(View v) {
460						AlertDialog dialog = UIHelper
461								.getVerifyFingerprintDialog(
462										(ConversationActivity) getActivity(),
463										conversation, fingerprintWarning);
464						dialog.show();
465					}
466				});
467			} else {
468				fingerprintWarning.setVisibility(View.GONE);
469			}
470		} else {
471			fingerprintWarning.setVisibility(View.GONE);
472		}
473	}
474
475	protected void sendPlainTextMessage(Message message) {
476		ConversationActivity activity = (ConversationActivity) getActivity();
477		activity.xmppConnectionService.sendMessage(message, null);
478		chatMsg.setText("");
479	}
480
481	protected void sendPgpMessage(final Message message) {
482		ConversationActivity activity = (ConversationActivity) getActivity();
483		final XmppConnectionService xmppService = activity.xmppConnectionService;
484		Contact contact = message.getConversation().getContact();
485		if (activity.hasPgp()) {
486			if (contact.getPgpKeyId() != 0) {
487				xmppService.sendMessage(message, null);
488				chatMsg.setText("");
489			} else {
490				AlertDialog.Builder builder = new AlertDialog.Builder(
491						getActivity());
492				builder.setTitle("No openPGP key found");
493				builder.setIconAttribute(android.R.attr.alertDialogIcon);
494				builder.setMessage("There is no openPGP key assoziated with this contact");
495				builder.setNegativeButton("Cancel", null);
496				builder.setPositiveButton("Send plain text",
497						new DialogInterface.OnClickListener() {
498
499							@Override
500							public void onClick(DialogInterface dialog,
501									int which) {
502								conversation.nextMessageEncryption = Message.ENCRYPTION_NONE;
503								message.setEncryption(Message.ENCRYPTION_NONE);
504								xmppService.sendMessage(message, null);
505								chatMsg.setText("");
506							}
507						});
508				builder.create().show();
509			}
510		}
511	}
512
513	protected void sendOtrMessage(final Message message) {
514		ConversationActivity activity = (ConversationActivity) getActivity();
515		final XmppConnectionService xmppService = activity.xmppConnectionService;
516		if (conversation.hasValidOtrSession()) {
517			activity.xmppConnectionService.sendMessage(message, null);
518			chatMsg.setText("");
519		} else {
520			Hashtable<String, Integer> presences;
521			if (conversation.getContact() != null) {
522				presences = conversation.getContact().getPresences();
523			} else {
524				presences = null;
525			}
526			if ((presences == null) || (presences.size() == 0)) {
527				AlertDialog.Builder builder = new AlertDialog.Builder(
528						getActivity());
529				builder.setTitle("Contact is offline");
530				builder.setIconAttribute(android.R.attr.alertDialogIcon);
531				builder.setMessage("Sending OTR encrypted messages to an offline contact is impossible.");
532				builder.setPositiveButton("Send plain text",
533						new DialogInterface.OnClickListener() {
534
535							@Override
536							public void onClick(DialogInterface dialog,
537									int which) {
538								conversation.nextMessageEncryption = Message.ENCRYPTION_NONE;
539								message.setEncryption(Message.ENCRYPTION_NONE);
540								xmppService.sendMessage(message, null);
541								chatMsg.setText("");
542							}
543						});
544				builder.setNegativeButton("Cancel", null);
545				builder.create().show();
546			} else if (presences.size() == 1) {
547				xmppService.sendMessage(message, (String) presences.keySet()
548						.toArray()[0]);
549				chatMsg.setText("");
550			} else {
551				AlertDialog.Builder builder = new AlertDialog.Builder(
552						getActivity());
553				builder.setTitle("Choose Presence");
554				final String[] presencesArray = new String[presences.size()];
555				presences.keySet().toArray(presencesArray);
556				builder.setItems(presencesArray,
557						new DialogInterface.OnClickListener() {
558
559							@Override
560							public void onClick(DialogInterface dialog,
561									int which) {
562								xmppService.sendMessage(message,
563										presencesArray[which]);
564								chatMsg.setText("");
565							}
566						});
567				builder.create().show();
568			}
569		}
570	}
571
572	private static class ViewHolder {
573
574		protected ImageView indicator;
575		protected TextView time;
576		protected TextView messageBody;
577		protected ImageView imageView;
578
579	}
580
581	private class BitmapCache {
582		private HashMap<String, Bitmap> bitmaps = new HashMap<String, Bitmap>();
583		private Bitmap error = null;
584
585		public Bitmap get(String name, Contact contact, Context context) {
586			if (bitmaps.containsKey(name)) {
587				return bitmaps.get(name);
588			} else {
589				Bitmap bm = UIHelper.getContactPicture(contact, name, 200, context);
590				bitmaps.put(name, bm);
591				return bm;
592			}
593		}
594
595		public Bitmap getError() {
596			if (error == null) {
597				error = UIHelper.getErrorPicture(200);
598			}
599			return error;
600		}
601	}
602
603	class DecryptMessage extends AsyncTask<Message, Void, Boolean> {
604
605		@Override
606		protected Boolean doInBackground(Message... params) {
607			final ConversationActivity activity = (ConversationActivity) getActivity();
608			askForPassphraseIntent = null;
609			for (int i = 0; i < params.length; ++i) {
610				if (params[i].getEncryption() == Message.ENCRYPTION_PGP) {
611					String body = params[i].getBody();
612					String decrypted = null;
613					if (activity == null) {
614						return false;
615					} else if (!activity.xmppConnectionServiceBound) {
616						return false;
617					}
618					try {
619						decrypted = activity.xmppConnectionService
620								.getPgpEngine().decrypt(body);
621					} catch (UserInputRequiredException e) {
622						askForPassphraseIntent = e.getPendingIntent()
623								.getIntentSender();
624						activity.runOnUiThread(new Runnable() {
625
626							@Override
627							public void run() {
628								pgpInfo.setVisibility(View.VISIBLE);
629							}
630						});
631
632						return false;
633
634					} catch (OpenPgpException e) {
635						Log.d("gultsch", "error decrypting pgp");
636					}
637					if (decrypted != null) {
638						params[i].setBody(decrypted);
639						params[i].setEncryption(Message.ENCRYPTION_DECRYPTED);
640						activity.xmppConnectionService.updateMessage(params[i]);
641					}
642					if (activity != null) {
643						activity.runOnUiThread(new Runnable() {
644
645							@Override
646							public void run() {
647								messageListAdapter.notifyDataSetChanged();
648							}
649						});
650					}
651				}
652				if (activity != null) {
653					activity.runOnUiThread(new Runnable() {
654
655						@Override
656						public void run() {
657							activity.updateConversationList();
658						}
659					});
660				}
661			}
662			return true;
663		}
664
665	}
666
667	public void setText(String text) {
668		this.pastedText = text;
669	}
670}