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