MessageAdapter.java

   1package eu.siacs.conversations.ui.adapter;
   2
   3import android.content.ActivityNotFoundException;
   4import android.content.Intent;
   5import android.content.pm.PackageManager;
   6import android.content.pm.ResolveInfo;
   7import android.content.res.Resources;
   8import android.content.res.TypedArray;
   9import android.graphics.Bitmap;
  10import android.graphics.Typeface;
  11import android.graphics.drawable.BitmapDrawable;
  12import android.graphics.drawable.Drawable;
  13import android.net.Uri;
  14import android.os.AsyncTask;
  15import android.os.Build;
  16import android.support.v4.content.FileProvider;
  17import android.text.Spannable;
  18import android.text.SpannableString;
  19import android.text.SpannableStringBuilder;
  20import android.text.Spanned;
  21import android.text.style.ForegroundColorSpan;
  22import android.text.style.RelativeSizeSpan;
  23import android.text.style.StyleSpan;
  24import android.text.util.Linkify;
  25import android.util.DisplayMetrics;
  26import android.view.ActionMode;
  27import android.view.Menu;
  28import android.view.MenuItem;
  29import android.view.View;
  30import android.view.View.OnClickListener;
  31import android.view.View.OnLongClickListener;
  32import android.view.ViewGroup;
  33import android.widget.ArrayAdapter;
  34import android.widget.Button;
  35import android.widget.ImageView;
  36import android.widget.LinearLayout;
  37import android.widget.TextView;
  38import android.widget.Toast;
  39
  40import java.lang.ref.WeakReference;
  41import java.net.URL;
  42import java.util.List;
  43import java.util.Locale;
  44import java.util.concurrent.RejectedExecutionException;
  45import java.util.regex.Matcher;
  46import java.util.regex.Pattern;
  47
  48import eu.siacs.conversations.Config;
  49import eu.siacs.conversations.R;
  50import eu.siacs.conversations.crypto.axolotl.FingerprintStatus;
  51import eu.siacs.conversations.crypto.axolotl.XmppAxolotlSession;
  52import eu.siacs.conversations.entities.Account;
  53import eu.siacs.conversations.entities.Conversation;
  54import eu.siacs.conversations.entities.DownloadableFile;
  55import eu.siacs.conversations.entities.Message;
  56import eu.siacs.conversations.entities.Message.FileParams;
  57import eu.siacs.conversations.entities.Transferable;
  58import eu.siacs.conversations.persistance.FileBackend;
  59import eu.siacs.conversations.ui.ConversationActivity;
  60import eu.siacs.conversations.ui.text.DividerSpan;
  61import eu.siacs.conversations.ui.text.QuoteSpan;
  62import eu.siacs.conversations.ui.widget.ClickableMovementMethod;
  63import eu.siacs.conversations.ui.widget.CopyTextView;
  64import eu.siacs.conversations.ui.widget.ListSelectionManager;
  65import eu.siacs.conversations.utils.CryptoHelper;
  66import eu.siacs.conversations.utils.GeoHelper;
  67import eu.siacs.conversations.utils.Patterns;
  68import eu.siacs.conversations.utils.UIHelper;
  69
  70public class MessageAdapter extends ArrayAdapter<Message> implements CopyTextView.CopyHandler {
  71
  72	private static final int SENT = 0;
  73	private static final int RECEIVED = 1;
  74	private static final int STATUS = 2;
  75	private static final Pattern XMPP_PATTERN = Pattern
  76			.compile("xmpp\\:(?:(?:["
  77					+ Patterns.GOOD_IRI_CHAR
  78					+ "\\;\\/\\?\\@\\&\\=\\#\\~\\-\\.\\+\\!\\*\\'\\(\\)\\,\\_])"
  79					+ "|(?:\\%[a-fA-F0-9]{2}))+");
  80
  81	private static final Linkify.TransformFilter WEBURL_TRANSOFRM_FILTER = new Linkify.TransformFilter() {
  82		@Override
  83		public String transformUrl(Matcher matcher, String url) {
  84			if (url == null) {
  85				return null;
  86			}
  87			final String lcUrl = url.toLowerCase(Locale.US);
  88			if (lcUrl.startsWith("http://") || lcUrl.startsWith("https://")) {
  89				return url;
  90			} else {
  91				return "http://"+url;
  92			}
  93		}
  94	};
  95
  96	private ConversationActivity activity;
  97
  98	private DisplayMetrics metrics;
  99
 100	private OnContactPictureClicked mOnContactPictureClickedListener;
 101	private OnContactPictureLongClicked mOnContactPictureLongClickedListener;
 102
 103	private boolean mIndicateReceived = false;
 104	private boolean mUseGreenBackground = false;
 105
 106	private OnQuoteListener onQuoteListener;
 107
 108	private final ListSelectionManager listSelectionManager = new ListSelectionManager();
 109
 110	public MessageAdapter(ConversationActivity activity, List<Message> messages) {
 111		super(activity, 0, messages);
 112		this.activity = activity;
 113		metrics = getContext().getResources().getDisplayMetrics();
 114		updatePreferences();
 115	}
 116
 117	public void setOnContactPictureClicked(OnContactPictureClicked listener) {
 118		this.mOnContactPictureClickedListener = listener;
 119	}
 120
 121	public void setOnContactPictureLongClicked(
 122			OnContactPictureLongClicked listener) {
 123		this.mOnContactPictureLongClickedListener = listener;
 124			}
 125
 126	public void setOnQuoteListener(OnQuoteListener listener) {
 127		this.onQuoteListener = listener;
 128	}
 129
 130	@Override
 131	public int getViewTypeCount() {
 132		return 3;
 133	}
 134
 135	public int getItemViewType(Message message) {
 136		if (message.getType() == Message.TYPE_STATUS) {
 137			return STATUS;
 138		} else if (message.getStatus() <= Message.STATUS_RECEIVED) {
 139			return RECEIVED;
 140		}
 141
 142		return SENT;
 143	}
 144
 145	@Override
 146	public int getItemViewType(int position) {
 147		return this.getItemViewType(getItem(position));
 148	}
 149
 150	private int getMessageTextColor(boolean onDark, boolean primary) {
 151		if (onDark) {
 152			return activity.getResources().getColor(primary ? R.color.white : R.color.white70);
 153		} else {
 154			return activity.getResources().getColor(primary ? R.color.black87 : R.color.black54);
 155		}
 156	}
 157
 158	private void displayStatus(ViewHolder viewHolder, Message message, int type, boolean darkBackground, boolean inValidSession) {
 159		String filesize = null;
 160		String info = null;
 161		boolean error = false;
 162		if (viewHolder.indicatorReceived != null) {
 163			viewHolder.indicatorReceived.setVisibility(View.GONE);
 164		}
 165
 166		if (viewHolder.edit_indicator != null) {
 167			if (message.edited()) {
 168				viewHolder.edit_indicator.setVisibility(View.VISIBLE);
 169				viewHolder.edit_indicator.setImageResource(darkBackground ? R.drawable.ic_mode_edit_white_18dp : R.drawable.ic_mode_edit_black_18dp);
 170				viewHolder.edit_indicator.setAlpha(darkBackground ? 0.7f : 0.57f);
 171			} else {
 172				viewHolder.edit_indicator.setVisibility(View.GONE);
 173			}
 174		}
 175		boolean multiReceived = message.getConversation().getMode() == Conversation.MODE_MULTI
 176			&& message.getMergedStatus() <= Message.STATUS_RECEIVED;
 177		if (message.getType() == Message.TYPE_IMAGE || message.getType() == Message.TYPE_FILE || message.getTransferable() != null) {
 178			FileParams params = message.getFileParams();
 179			if (params.size > (1.5 * 1024 * 1024)) {
 180				filesize = params.size / (1024 * 1024)+ " MiB";
 181			} else if (params.size > 0) {
 182				filesize = params.size / 1024 + " KiB";
 183			}
 184			if (message.getTransferable() != null && message.getTransferable().getStatus() == Transferable.STATUS_FAILED) {
 185				error = true;
 186			}
 187		}
 188		switch (message.getMergedStatus()) {
 189			case Message.STATUS_WAITING:
 190				info = getContext().getString(R.string.waiting);
 191				break;
 192			case Message.STATUS_UNSEND:
 193				Transferable d = message.getTransferable();
 194				if (d!=null) {
 195					info = getContext().getString(R.string.sending_file,d.getProgress());
 196				} else {
 197					info = getContext().getString(R.string.sending);
 198				}
 199				break;
 200			case Message.STATUS_OFFERED:
 201				info = getContext().getString(R.string.offering);
 202				break;
 203			case Message.STATUS_SEND_RECEIVED:
 204				if (mIndicateReceived) {
 205					viewHolder.indicatorReceived.setVisibility(View.VISIBLE);
 206				}
 207				break;
 208			case Message.STATUS_SEND_DISPLAYED:
 209				if (mIndicateReceived) {
 210					viewHolder.indicatorReceived.setVisibility(View.VISIBLE);
 211				}
 212				break;
 213			case Message.STATUS_SEND_FAILED:
 214				info = getContext().getString(R.string.send_failed);
 215				error = true;
 216				break;
 217			default:
 218				if (multiReceived) {
 219					info = UIHelper.getMessageDisplayName(message);
 220				}
 221				break;
 222		}
 223		if (error && type == SENT) {
 224			viewHolder.time.setTextColor(activity.getWarningTextColor());
 225		} else {
 226			viewHolder.time.setTextColor(this.getMessageTextColor(darkBackground,false));
 227		}
 228		if (message.getEncryption() == Message.ENCRYPTION_NONE) {
 229			viewHolder.indicator.setVisibility(View.GONE);
 230		} else {
 231			boolean verified = false;
 232			if (message.getEncryption() == Message.ENCRYPTION_AXOLOTL) {
 233				final FingerprintStatus status = message.getConversation()
 234						.getAccount().getAxolotlService().getFingerprintTrust(
 235								message.getFingerprint());
 236				if (status != null && status.isVerified()) {
 237					verified = true;
 238				}
 239			}
 240			if (verified) {
 241				viewHolder.indicator.setImageResource(darkBackground ? R.drawable.ic_verified_user_white_18dp : R.drawable.ic_verified_user_black_18dp);
 242			} else {
 243				viewHolder.indicator.setImageResource(darkBackground ? R.drawable.ic_lock_white_18dp : R.drawable.ic_lock_black_18dp);
 244			}
 245			if (darkBackground) {
 246				viewHolder.indicator.setAlpha(0.7f);
 247			} else {
 248				viewHolder.indicator.setAlpha(0.57f);
 249			}
 250			viewHolder.indicator.setVisibility(View.VISIBLE);
 251		}
 252
 253		String formatedTime = UIHelper.readableTimeDifferenceFull(getContext(),
 254				message.getMergedTimeSent());
 255		if (message.getStatus() <= Message.STATUS_RECEIVED) {
 256			if ((filesize != null) && (info != null)) {
 257				viewHolder.time.setText(formatedTime + " \u00B7 " + filesize +" \u00B7 " + info);
 258			} else if ((filesize == null) && (info != null)) {
 259				viewHolder.time.setText(formatedTime + " \u00B7 " + info);
 260			} else if ((filesize != null) && (info == null)) {
 261				viewHolder.time.setText(formatedTime + " \u00B7 " + filesize);
 262			} else {
 263				viewHolder.time.setText(formatedTime);
 264			}
 265		} else {
 266			if ((filesize != null) && (info != null)) {
 267				viewHolder.time.setText(filesize + " \u00B7 " + info);
 268			} else if ((filesize == null) && (info != null)) {
 269				if (error) {
 270					viewHolder.time.setText(info + " \u00B7 " + formatedTime);
 271				} else {
 272					viewHolder.time.setText(info);
 273				}
 274			} else if ((filesize != null) && (info == null)) {
 275				viewHolder.time.setText(filesize + " \u00B7 " + formatedTime);
 276			} else {
 277				viewHolder.time.setText(formatedTime);
 278			}
 279		}
 280	}
 281
 282	private void displayInfoMessage(ViewHolder viewHolder, String text, boolean darkBackground) {
 283		if (viewHolder.download_button != null) {
 284			viewHolder.download_button.setVisibility(View.GONE);
 285		}
 286		viewHolder.image.setVisibility(View.GONE);
 287		viewHolder.messageBody.setVisibility(View.VISIBLE);
 288		viewHolder.messageBody.setText(text);
 289		viewHolder.messageBody.setTextColor(getMessageTextColor(darkBackground, false));
 290		viewHolder.messageBody.setTypeface(null, Typeface.ITALIC);
 291		viewHolder.messageBody.setTextIsSelectable(false);
 292	}
 293
 294	private void displayDecryptionFailed(ViewHolder viewHolder, boolean darkBackground) {
 295		if (viewHolder.download_button != null) {
 296			viewHolder.download_button.setVisibility(View.GONE);
 297		}
 298		viewHolder.image.setVisibility(View.GONE);
 299		viewHolder.messageBody.setVisibility(View.VISIBLE);
 300		viewHolder.messageBody.setText(getContext().getString(
 301				R.string.decryption_failed));
 302		viewHolder.messageBody.setTextColor(getMessageTextColor(darkBackground, false));
 303		viewHolder.messageBody.setTypeface(null, Typeface.NORMAL);
 304		viewHolder.messageBody.setTextIsSelectable(false);
 305	}
 306
 307	private void displayHeartMessage(final ViewHolder viewHolder, final String body) {
 308		if (viewHolder.download_button != null) {
 309			viewHolder.download_button.setVisibility(View.GONE);
 310		}
 311		viewHolder.image.setVisibility(View.GONE);
 312		viewHolder.messageBody.setVisibility(View.VISIBLE);
 313		viewHolder.messageBody.setIncludeFontPadding(false);
 314		Spannable span = new SpannableString(body);
 315		span.setSpan(new RelativeSizeSpan(4.0f), 0, body.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
 316		span.setSpan(new ForegroundColorSpan(activity.getWarningTextColor()), 0, body.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
 317		viewHolder.messageBody.setText(span);
 318	}
 319
 320	private int applyQuoteSpan(SpannableStringBuilder body, int start, int end, boolean darkBackground) {
 321		if (start > 1 && !"\n\n".equals(body.subSequence(start - 2, start).toString())) {
 322			body.insert(start++, "\n");
 323			body.setSpan(new DividerSpan(false), start - 2, start, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
 324			end++;
 325		}
 326		if (end < body.length() - 1 && !"\n\n".equals(body.subSequence(end, end + 2).toString())) {
 327			body.insert(end, "\n");
 328			body.setSpan(new DividerSpan(false), end, end + 2, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
 329		}
 330		int color = darkBackground ? this.getMessageTextColor(darkBackground, false)
 331				: getContext().getResources().getColor(R.color.bubble);
 332		DisplayMetrics metrics = getContext().getResources().getDisplayMetrics();
 333		body.setSpan(new QuoteSpan(color, metrics), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
 334		return 0;
 335	}
 336
 337	/**
 338	 * Applies QuoteSpan to group of lines which starts with > or » characters.
 339	 * Appends likebreaks and applies DividerSpan to them to show a padding between quote and text.
 340	 */
 341	private boolean handleTextQuotes(SpannableStringBuilder body, boolean darkBackground) {
 342		boolean startsWithQuote = false;
 343		char previous = '\n';
 344		int lineStart = -1;
 345		int lineTextStart = -1;
 346		int quoteStart = -1;
 347		for (int i = 0; i <= body.length(); i++) {
 348			char current = body.length() > i ? body.charAt(i) : '\n';
 349			if (lineStart == -1) {
 350				if (previous == '\n') {
 351					if (current == '>' || current == '\u00bb') {
 352						// Line start with quote
 353						lineStart = i;
 354						if (quoteStart == -1) quoteStart = i;
 355						if (i == 0) startsWithQuote = true;
 356					} else if (quoteStart >= 0) {
 357						// Line start without quote, apply spans there
 358						applyQuoteSpan(body, quoteStart, i - 1, darkBackground);
 359						quoteStart = -1;
 360					}
 361				}
 362			} else {
 363				// Remove extra spaces between > and first character in the line
 364				// > character will be removed too
 365				if (current != ' ' && lineTextStart == -1) {
 366					lineTextStart = i;
 367				}
 368				if (current == '\n') {
 369					body.delete(lineStart, lineTextStart);
 370					i -= lineTextStart - lineStart;
 371					if (i == lineStart) {
 372						// Avoid empty lines because span over empty line can be hidden
 373						body.insert(i++, " ");
 374					}
 375					lineStart = -1;
 376					lineTextStart = -1;
 377				}
 378			}
 379			previous = current;
 380		}
 381		if (quoteStart >= 0) {
 382			// Apply spans to finishing open quote
 383			applyQuoteSpan(body, quoteStart, body.length(), darkBackground);
 384		}
 385		return startsWithQuote;
 386	}
 387
 388	private void displayTextMessage(final ViewHolder viewHolder, final Message message, boolean darkBackground, int type) {
 389		if (viewHolder.download_button != null) {
 390			viewHolder.download_button.setVisibility(View.GONE);
 391		}
 392		viewHolder.image.setVisibility(View.GONE);
 393		viewHolder.messageBody.setVisibility(View.VISIBLE);
 394		viewHolder.messageBody.setIncludeFontPadding(true);
 395		if (message.getBody() != null) {
 396			final String nick = UIHelper.getMessageDisplayName(message);
 397			SpannableStringBuilder body = message.getMergedBody();
 398			boolean hasMeCommand = message.hasMeCommand();
 399			if (hasMeCommand) {
 400				body = body.replace(0, Message.ME_COMMAND.length(), nick + " ");
 401			}
 402			if (body.length() > Config.MAX_DISPLAY_MESSAGE_CHARS) {
 403				body = new SpannableStringBuilder(body, 0, Config.MAX_DISPLAY_MESSAGE_CHARS);
 404				body.append("\u2026");
 405			}
 406			Message.MergeSeparator[] mergeSeparators = body.getSpans(0, body.length(), Message.MergeSeparator.class);
 407			for (Message.MergeSeparator mergeSeparator : mergeSeparators) {
 408				int start = body.getSpanStart(mergeSeparator);
 409				int end = body.getSpanEnd(mergeSeparator);
 410				body.setSpan(new DividerSpan(true), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
 411			}
 412			boolean startsWithQuote = handleTextQuotes(body, darkBackground);
 413			if (message.getType() != Message.TYPE_PRIVATE) {
 414				if (hasMeCommand) {
 415					body.setSpan(new StyleSpan(Typeface.BOLD_ITALIC), 0, nick.length(),
 416							Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
 417				}
 418			} else {
 419				String privateMarker;
 420				if (message.getStatus() <= Message.STATUS_RECEIVED) {
 421					privateMarker = activity.getString(R.string.private_message);
 422				} else {
 423					final String to;
 424					if (message.getCounterpart() != null) {
 425						to = message.getCounterpart().getResourcepart();
 426					} else {
 427						to = "";
 428					}
 429					privateMarker = activity.getString(R.string.private_message_to, to);
 430				}
 431				body.insert(0, privateMarker);
 432				int privateMarkerIndex = privateMarker.length();
 433				if (startsWithQuote) {
 434					body.insert(privateMarkerIndex, "\n\n");
 435					body.setSpan(new DividerSpan(false), privateMarkerIndex, privateMarkerIndex + 2,
 436							Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
 437				} else {
 438					body.insert(privateMarkerIndex, " ");
 439				}
 440				body.setSpan(new ForegroundColorSpan(getMessageTextColor(darkBackground, false)),
 441						0, privateMarkerIndex, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
 442				body.setSpan(new StyleSpan(Typeface.BOLD),
 443						0, privateMarkerIndex, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
 444				if (hasMeCommand) {
 445					body.setSpan(new StyleSpan(Typeface.BOLD_ITALIC), privateMarkerIndex + 1,
 446							privateMarkerIndex + 1 + nick.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
 447				}
 448			}
 449			Linkify.addLinks(body, Patterns.AUTOLINK_WEB_URL, "http", null, WEBURL_TRANSOFRM_FILTER);
 450			Linkify.addLinks(body, XMPP_PATTERN, "xmpp");
 451			Linkify.addLinks(body, GeoHelper.GEO_URI, "geo");
 452			viewHolder.messageBody.setAutoLinkMask(0);
 453			viewHolder.messageBody.setText(body);
 454			viewHolder.messageBody.setTextIsSelectable(true);
 455			viewHolder.messageBody.setMovementMethod(ClickableMovementMethod.getInstance());
 456			listSelectionManager.onUpdate(viewHolder.messageBody, message);
 457		} else {
 458			viewHolder.messageBody.setText("");
 459			viewHolder.messageBody.setTextIsSelectable(false);
 460		}
 461		viewHolder.messageBody.setTextColor(this.getMessageTextColor(darkBackground, true));
 462		viewHolder.messageBody.setLinkTextColor(this.getMessageTextColor(darkBackground, true));
 463		viewHolder.messageBody.setHighlightColor(activity.getResources().getColor(darkBackground
 464				? (type == SENT || !mUseGreenBackground ? R.color.black26 : R.color.grey800) : R.color.grey500));
 465		viewHolder.messageBody.setTypeface(null, Typeface.NORMAL);
 466	}
 467
 468	private void displayDownloadableMessage(ViewHolder viewHolder,
 469			final Message message, String text) {
 470		viewHolder.image.setVisibility(View.GONE);
 471		viewHolder.messageBody.setVisibility(View.GONE);
 472		viewHolder.download_button.setVisibility(View.VISIBLE);
 473		viewHolder.download_button.setText(text);
 474		viewHolder.download_button.setOnClickListener(new OnClickListener() {
 475
 476			@Override
 477			public void onClick(View v) {
 478				activity.startDownloadable(message);
 479			}
 480		});
 481	}
 482
 483	private void displayOpenableMessage(ViewHolder viewHolder,final Message message) {
 484		viewHolder.image.setVisibility(View.GONE);
 485		viewHolder.messageBody.setVisibility(View.GONE);
 486		viewHolder.download_button.setVisibility(View.VISIBLE);
 487		viewHolder.download_button.setText(activity.getString(R.string.open_x_file, UIHelper.getFileDescriptionString(activity, message)));
 488		viewHolder.download_button.setOnClickListener(new OnClickListener() {
 489
 490			@Override
 491			public void onClick(View v) {
 492				openDownloadable(message);
 493			}
 494		});
 495	}
 496
 497	private void displayLocationMessage(ViewHolder viewHolder, final Message message) {
 498		viewHolder.image.setVisibility(View.GONE);
 499		viewHolder.messageBody.setVisibility(View.GONE);
 500		viewHolder.download_button.setVisibility(View.VISIBLE);
 501		viewHolder.download_button.setText(R.string.show_location);
 502		viewHolder.download_button.setOnClickListener(new OnClickListener() {
 503
 504			@Override
 505			public void onClick(View v) {
 506				showLocation(message);
 507			}
 508		});
 509	}
 510
 511	private void displayImageMessage(ViewHolder viewHolder,
 512			final Message message) {
 513		if (viewHolder.download_button != null) {
 514			viewHolder.download_button.setVisibility(View.GONE);
 515		}
 516		viewHolder.messageBody.setVisibility(View.GONE);
 517		viewHolder.image.setVisibility(View.VISIBLE);
 518		FileParams params = message.getFileParams();
 519		double target = metrics.density * 288;
 520		int scaledW;
 521		int scaledH;
 522		if (Math.max(params.height, params.width) * metrics.density <= target) {
 523			scaledW = (int) (params.width * metrics.density);
 524			scaledH = (int) (params.height * metrics.density);
 525		} else if (Math.max(params.height,params.width) <= target) {
 526			scaledW = params.width;
 527			scaledH = params.height;
 528		} else if (params.width <= params.height) {
 529			scaledW = (int) (params.width / ((double) params.height / target));
 530			scaledH = (int) target;
 531		} else {
 532			scaledW = (int) target;
 533			scaledH = (int) (params.height / ((double) params.width / target));
 534		}
 535		LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(scaledW, scaledH);
 536		layoutParams.setMargins(0, (int) (metrics.density * 4), 0, (int) (metrics.density * 4));
 537		viewHolder.image.setLayoutParams(layoutParams);
 538		activity.loadBitmap(message, viewHolder.image);
 539		viewHolder.image.setOnClickListener(new OnClickListener() {
 540
 541			@Override
 542			public void onClick(View v) {
 543				openDownloadable(message);
 544			}
 545		});
 546	}
 547
 548	private void loadMoreMessages(Conversation conversation) {
 549		conversation.setLastClearHistory(0);
 550		activity.xmppConnectionService.updateConversation(conversation);
 551		conversation.setHasMessagesLeftOnServer(true);
 552		conversation.setFirstMamReference(null);
 553		long timestamp = conversation.getLastMessageTransmitted();
 554		if (timestamp == 0) {
 555			timestamp = System.currentTimeMillis();
 556		}
 557		activity.setMessagesLoaded();
 558		activity.xmppConnectionService.getMessageArchiveService().query(conversation, 0, timestamp);
 559		Toast.makeText(activity, R.string.fetching_history_from_server,Toast.LENGTH_LONG).show();
 560	}
 561
 562	@Override
 563	public View getView(int position, View view, ViewGroup parent) {
 564		final Message message = getItem(position);
 565		final boolean omemoEncryption = message.getEncryption() == Message.ENCRYPTION_AXOLOTL;
 566		final boolean isInValidSession = message.isValidInSession() && (!omemoEncryption || message.isTrusted());
 567		final Conversation conversation = message.getConversation();
 568		final Account account = conversation.getAccount();
 569		final int type = getItemViewType(position);
 570		ViewHolder viewHolder;
 571		if (view == null) {
 572			viewHolder = new ViewHolder();
 573			switch (type) {
 574				case SENT:
 575					view = activity.getLayoutInflater().inflate(
 576							R.layout.message_sent, parent, false);
 577					viewHolder.message_box = (LinearLayout) view
 578						.findViewById(R.id.message_box);
 579					viewHolder.contact_picture = (ImageView) view
 580						.findViewById(R.id.message_photo);
 581					viewHolder.download_button = (Button) view
 582						.findViewById(R.id.download_button);
 583					viewHolder.indicator = (ImageView) view
 584						.findViewById(R.id.security_indicator);
 585					viewHolder.edit_indicator = (ImageView) view.findViewById(R.id.edit_indicator);
 586					viewHolder.image = (ImageView) view
 587						.findViewById(R.id.message_image);
 588					viewHolder.messageBody = (CopyTextView) view
 589						.findViewById(R.id.message_body);
 590					viewHolder.time = (TextView) view
 591						.findViewById(R.id.message_time);
 592					viewHolder.indicatorReceived = (ImageView) view
 593						.findViewById(R.id.indicator_received);
 594					break;
 595				case RECEIVED:
 596					view = activity.getLayoutInflater().inflate(
 597							R.layout.message_received, parent, false);
 598					viewHolder.message_box = (LinearLayout) view
 599						.findViewById(R.id.message_box);
 600					viewHolder.contact_picture = (ImageView) view
 601						.findViewById(R.id.message_photo);
 602					viewHolder.download_button = (Button) view
 603						.findViewById(R.id.download_button);
 604					viewHolder.indicator = (ImageView) view
 605						.findViewById(R.id.security_indicator);
 606					viewHolder.edit_indicator = (ImageView) view.findViewById(R.id.edit_indicator);
 607					viewHolder.image = (ImageView) view
 608						.findViewById(R.id.message_image);
 609					viewHolder.messageBody = (CopyTextView) view
 610						.findViewById(R.id.message_body);
 611					viewHolder.time = (TextView) view
 612						.findViewById(R.id.message_time);
 613					viewHolder.indicatorReceived = (ImageView) view
 614						.findViewById(R.id.indicator_received);
 615					viewHolder.encryption = (TextView) view.findViewById(R.id.message_encryption);
 616					break;
 617				case STATUS:
 618					view = activity.getLayoutInflater().inflate(R.layout.message_status, parent, false);
 619					viewHolder.contact_picture = (ImageView) view.findViewById(R.id.message_photo);
 620					viewHolder.status_message = (TextView) view.findViewById(R.id.status_message);
 621					viewHolder.load_more_messages = (Button) view.findViewById(R.id.load_more_messages);
 622					break;
 623				default:
 624					viewHolder = null;
 625					break;
 626			}
 627			if (viewHolder.messageBody != null) {
 628				listSelectionManager.onCreate(viewHolder.messageBody,
 629						new MessageBodyActionModeCallback(viewHolder.messageBody));
 630				viewHolder.messageBody.setCopyHandler(this);
 631			}
 632			view.setTag(viewHolder);
 633		} else {
 634			viewHolder = (ViewHolder) view.getTag();
 635			if (viewHolder == null) {
 636				return view;
 637			}
 638		}
 639
 640		boolean darkBackground = type == RECEIVED && (!isInValidSession || mUseGreenBackground) || activity.isDarkTheme();
 641
 642		if (type == STATUS) {
 643			if ("LOAD_MORE".equals(message.getBody())) {
 644				viewHolder.status_message.setVisibility(View.GONE);
 645				viewHolder.contact_picture.setVisibility(View.GONE);
 646				viewHolder.load_more_messages.setVisibility(View.VISIBLE);
 647				viewHolder.load_more_messages.setOnClickListener(new OnClickListener() {
 648					@Override
 649					public void onClick(View v) {
 650						loadMoreMessages(message.getConversation());
 651					}
 652				});
 653			} else {
 654				viewHolder.status_message.setVisibility(View.VISIBLE);
 655				viewHolder.contact_picture.setVisibility(View.VISIBLE);
 656				viewHolder.load_more_messages.setVisibility(View.GONE);
 657				if (conversation.getMode() == Conversation.MODE_SINGLE) {
 658					viewHolder.contact_picture.setImageBitmap(activity
 659							.avatarService().get(conversation.getContact(),
 660									activity.getPixel(32)));
 661					viewHolder.contact_picture.setAlpha(0.5f);
 662				}
 663				viewHolder.status_message.setText(message.getBody());
 664			}
 665			return view;
 666		} else {
 667			loadAvatar(message, viewHolder.contact_picture);
 668		}
 669
 670		viewHolder.contact_picture
 671			.setOnClickListener(new OnClickListener() {
 672
 673				@Override
 674				public void onClick(View v) {
 675					if (MessageAdapter.this.mOnContactPictureClickedListener != null) {
 676						MessageAdapter.this.mOnContactPictureClickedListener
 677								.onContactPictureClicked(message);
 678					}
 679
 680				}
 681			});
 682		viewHolder.contact_picture
 683			.setOnLongClickListener(new OnLongClickListener() {
 684
 685				@Override
 686				public boolean onLongClick(View v) {
 687					if (MessageAdapter.this.mOnContactPictureLongClickedListener != null) {
 688						MessageAdapter.this.mOnContactPictureLongClickedListener
 689								.onContactPictureLongClicked(message);
 690						return true;
 691					} else {
 692						return false;
 693					}
 694				}
 695			});
 696
 697		final Transferable transferable = message.getTransferable();
 698		if (transferable != null && transferable.getStatus() != Transferable.STATUS_UPLOADING) {
 699			if (transferable.getStatus() == Transferable.STATUS_OFFER) {
 700				displayDownloadableMessage(viewHolder,message,activity.getString(R.string.download_x_file, UIHelper.getFileDescriptionString(activity, message)));
 701			} else if (transferable.getStatus() == Transferable.STATUS_OFFER_CHECK_FILESIZE) {
 702				displayDownloadableMessage(viewHolder, message, activity.getString(R.string.check_x_filesize, UIHelper.getFileDescriptionString(activity, message)));
 703			} else {
 704				displayInfoMessage(viewHolder, UIHelper.getMessagePreview(activity, message).first,darkBackground);
 705			}
 706		} else if (message.getType() == Message.TYPE_IMAGE && message.getEncryption() != Message.ENCRYPTION_PGP && message.getEncryption() != Message.ENCRYPTION_DECRYPTION_FAILED) {
 707			displayImageMessage(viewHolder, message);
 708		} else if (message.getType() == Message.TYPE_FILE && message.getEncryption() != Message.ENCRYPTION_PGP && message.getEncryption() != Message.ENCRYPTION_DECRYPTION_FAILED) {
 709			if (message.getFileParams().width > 0) {
 710				displayImageMessage(viewHolder,message);
 711			} else {
 712				displayOpenableMessage(viewHolder, message);
 713			}
 714		} else if (message.getEncryption() == Message.ENCRYPTION_PGP) {
 715			if (account.isPgpDecryptionServiceConnected()) {
 716				if (!account.hasPendingPgpIntent(conversation)) {
 717					displayInfoMessage(viewHolder, activity.getString(R.string.message_decrypting), darkBackground);
 718				} else {
 719					displayInfoMessage(viewHolder, activity.getString(R.string.pgp_message), darkBackground);
 720				}
 721			} else {
 722				displayInfoMessage(viewHolder,activity.getString(R.string.install_openkeychain),darkBackground);
 723				if (viewHolder != null) {
 724					viewHolder.message_box
 725						.setOnClickListener(new OnClickListener() {
 726
 727							@Override
 728							public void onClick(View v) {
 729								activity.showInstallPgpDialog();
 730							}
 731						});
 732				}
 733			}
 734		} else if (message.getEncryption() == Message.ENCRYPTION_DECRYPTION_FAILED) {
 735			displayDecryptionFailed(viewHolder,darkBackground);
 736		} else {
 737			if (GeoHelper.isGeoUri(message.getBody())) {
 738				displayLocationMessage(viewHolder,message);
 739			} else if (message.bodyIsHeart()) {
 740				displayHeartMessage(viewHolder, message.getBody().trim());
 741			} else if (message.treatAsDownloadable() == Message.Decision.MUST) {
 742				try {
 743					URL url = new URL(message.getBody());
 744					displayDownloadableMessage(viewHolder,
 745							message,
 746							activity.getString(R.string.check_x_filesize_on_host,
 747									UIHelper.getFileDescriptionString(activity, message),
 748									url.getHost()));
 749				} catch (Exception e) {
 750					displayDownloadableMessage(viewHolder,
 751							message,
 752							activity.getString(R.string.check_x_filesize,
 753									UIHelper.getFileDescriptionString(activity, message)));
 754				}
 755			} else {
 756				displayTextMessage(viewHolder, message, darkBackground, type);
 757			}
 758		}
 759
 760		if (type == RECEIVED) {
 761			if(isInValidSession) {
 762				int bubble;
 763				if (!mUseGreenBackground) {
 764					bubble = activity.getThemeResource(R.attr.message_bubble_received_monochrome, R.drawable.message_bubble_received_white);
 765				} else {
 766					bubble = activity.getThemeResource(R.attr.message_bubble_received_green, R.drawable.message_bubble_received);
 767				}
 768				viewHolder.message_box.setBackgroundResource(bubble);
 769				viewHolder.encryption.setVisibility(View.GONE);
 770			} else {
 771				viewHolder.message_box.setBackgroundResource(R.drawable.message_bubble_received_warning);
 772				viewHolder.encryption.setVisibility(View.VISIBLE);
 773				if (omemoEncryption && !message.isTrusted()) {
 774					viewHolder.encryption.setText(R.string.not_trusted);
 775				} else {
 776					viewHolder.encryption.setText(CryptoHelper.encryptionTypeToText(message.getEncryption()));
 777				}
 778			}
 779		}
 780
 781		displayStatus(viewHolder, message, type, darkBackground, isInValidSession);
 782
 783		return view;
 784	}
 785
 786	@Override
 787	public void notifyDataSetChanged() {
 788		listSelectionManager.onBeforeNotifyDataSetChanged();
 789		super.notifyDataSetChanged();
 790		listSelectionManager.onAfterNotifyDataSetChanged();
 791	}
 792
 793	private String transformText(CharSequence text, int start, int end, boolean forCopy) {
 794		SpannableStringBuilder builder = new SpannableStringBuilder(text);
 795		Object copySpan = new Object();
 796		builder.setSpan(copySpan, start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
 797		DividerSpan[] dividerSpans = builder.getSpans(0, builder.length(), DividerSpan.class);
 798		for (DividerSpan dividerSpan : dividerSpans) {
 799			builder.replace(builder.getSpanStart(dividerSpan), builder.getSpanEnd(dividerSpan),
 800					dividerSpan.isLarge() ? "\n\n" : "\n");
 801		}
 802		start = builder.getSpanStart(copySpan);
 803		end = builder.getSpanEnd(copySpan);
 804		if (start == -1 || end == -1) return "";
 805		builder = new SpannableStringBuilder(builder, start, end);
 806		if (forCopy) {
 807			QuoteSpan[] quoteSpans = builder.getSpans(0, builder.length(), QuoteSpan.class);
 808			for (QuoteSpan quoteSpan : quoteSpans) {
 809				builder.insert(builder.getSpanStart(quoteSpan), "> ");
 810			}
 811		}
 812		return builder.toString();
 813	}
 814
 815	@Override
 816	public String transformTextForCopy(CharSequence text, int start, int end) {
 817		if (text instanceof Spanned) {
 818			return transformText(text, start, end, true);
 819		} else {
 820			return text.toString().substring(start, end);
 821		}
 822	}
 823
 824	public interface OnQuoteListener {
 825		public void onQuote(String text);
 826	}
 827
 828	private class MessageBodyActionModeCallback implements ActionMode.Callback {
 829
 830		private final TextView textView;
 831
 832		public MessageBodyActionModeCallback(TextView textView) {
 833			this.textView = textView;
 834		}
 835
 836		@Override
 837		public boolean onCreateActionMode(ActionMode mode, Menu menu) {
 838			if (onQuoteListener != null) {
 839				int quoteResId = activity.getThemeResource(R.attr.icon_quote, R.drawable.ic_action_reply);
 840				// 3rd item is placed after "copy" item
 841				menu.add(0, android.R.id.button1, 3, R.string.quote).setIcon(quoteResId)
 842						.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS | MenuItem.SHOW_AS_ACTION_WITH_TEXT);
 843			}
 844			return false;
 845		}
 846
 847		@Override
 848		public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
 849			return false;
 850		}
 851
 852		@Override
 853		public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
 854			if (item.getItemId() == android.R.id.button1) {
 855				int start = textView.getSelectionStart();
 856				int end = textView.getSelectionEnd();
 857				if (end > start) {
 858					String text = transformText(textView.getText(), start, end, false);
 859					if (onQuoteListener != null) {
 860						onQuoteListener.onQuote(text);
 861					}
 862					mode.finish();
 863				}
 864				return true;
 865			}
 866			return false;
 867		}
 868
 869		@Override
 870		public void onDestroyActionMode(ActionMode mode) {}
 871	}
 872
 873	public void openDownloadable(Message message) {
 874		DownloadableFile file = activity.xmppConnectionService.getFileBackend().getFile(message);
 875		if (!file.exists()) {
 876			Toast.makeText(activity,R.string.file_deleted,Toast.LENGTH_SHORT).show();
 877			return;
 878		}
 879		Intent openIntent = new Intent(Intent.ACTION_VIEW);
 880		String mime = file.getMimeType();
 881		if (mime == null) {
 882			mime = "*/*";
 883		}
 884		Uri uri;
 885		try {
 886			uri = FileBackend.getUriForFile(activity, file);
 887		} catch (SecurityException e) {
 888			Toast.makeText(activity, activity.getString(R.string.no_permission_to_access_x, file.getAbsolutePath()), Toast.LENGTH_SHORT).show();
 889			return;
 890		}
 891		openIntent.setDataAndType(uri, mime);
 892		openIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
 893		PackageManager manager = activity.getPackageManager();
 894		List<ResolveInfo> info = manager.queryIntentActivities(openIntent, 0);
 895		if (info.size() == 0) {
 896			openIntent.setDataAndType(uri,"*/*");
 897		}
 898		try {
 899			getContext().startActivity(openIntent);
 900		}  catch (ActivityNotFoundException e) {
 901			Toast.makeText(activity,R.string.no_application_found_to_open_file,Toast.LENGTH_SHORT).show();
 902		}
 903	}
 904
 905	public void showLocation(Message message) {
 906		for(Intent intent : GeoHelper.createGeoIntentsFromMessage(message)) {
 907			if (intent.resolveActivity(getContext().getPackageManager()) != null) {
 908				getContext().startActivity(intent);
 909				return;
 910			}
 911		}
 912		Toast.makeText(activity,R.string.no_application_found_to_display_location,Toast.LENGTH_SHORT).show();
 913	}
 914
 915	public void updatePreferences() {
 916		this.mIndicateReceived = activity.indicateReceived();
 917		this.mUseGreenBackground = activity.useGreenBackground();
 918	}
 919
 920	public TextView getMessageBody(View view) {
 921		final Object tag = view.getTag();
 922		if (tag instanceof ViewHolder) {
 923			final ViewHolder viewHolder = (ViewHolder) tag;
 924			return viewHolder.messageBody;
 925		}
 926		return null;
 927	}
 928
 929	public interface OnContactPictureClicked {
 930		void onContactPictureClicked(Message message);
 931	}
 932
 933	public interface OnContactPictureLongClicked {
 934		void onContactPictureLongClicked(Message message);
 935	}
 936
 937	private static class ViewHolder {
 938
 939		protected LinearLayout message_box;
 940		protected Button download_button;
 941		protected ImageView image;
 942		protected ImageView indicator;
 943		protected ImageView indicatorReceived;
 944		protected TextView time;
 945		protected CopyTextView messageBody;
 946		protected ImageView contact_picture;
 947		protected TextView status_message;
 948		protected TextView encryption;
 949		public Button load_more_messages;
 950		public ImageView edit_indicator;
 951	}
 952
 953	class BitmapWorkerTask extends AsyncTask<Message, Void, Bitmap> {
 954		private final WeakReference<ImageView> imageViewReference;
 955		private Message message = null;
 956
 957		public BitmapWorkerTask(ImageView imageView) {
 958			imageViewReference = new WeakReference<>(imageView);
 959		}
 960
 961		@Override
 962		protected Bitmap doInBackground(Message... params) {
 963			return activity.avatarService().get(params[0], activity.getPixel(48), isCancelled());
 964		}
 965
 966		@Override
 967		protected void onPostExecute(Bitmap bitmap) {
 968			if (bitmap != null && !isCancelled()) {
 969				final ImageView imageView = imageViewReference.get();
 970				if (imageView != null) {
 971					imageView.setImageBitmap(bitmap);
 972					imageView.setBackgroundColor(0x00000000);
 973				}
 974			}
 975		}
 976	}
 977
 978	public void loadAvatar(Message message, ImageView imageView) {
 979		if (cancelPotentialWork(message, imageView)) {
 980			final Bitmap bm = activity.avatarService().get(message, activity.getPixel(48), true);
 981			if (bm != null) {
 982				cancelPotentialWork(message, imageView);
 983				imageView.setImageBitmap(bm);
 984				imageView.setBackgroundColor(0x00000000);
 985			} else {
 986				imageView.setBackgroundColor(UIHelper.getColorForName(UIHelper.getMessageDisplayName(message)));
 987				imageView.setImageDrawable(null);
 988				final BitmapWorkerTask task = new BitmapWorkerTask(imageView);
 989				final AsyncDrawable asyncDrawable = new AsyncDrawable(activity.getResources(), null, task);
 990				imageView.setImageDrawable(asyncDrawable);
 991				try {
 992					task.execute(message);
 993				} catch (final RejectedExecutionException ignored) {
 994				}
 995			}
 996		}
 997	}
 998
 999	public static boolean cancelPotentialWork(Message message, ImageView imageView) {
1000		final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView);
1001
1002		if (bitmapWorkerTask != null) {
1003			final Message oldMessage = bitmapWorkerTask.message;
1004			if (oldMessage == null || message != oldMessage) {
1005				bitmapWorkerTask.cancel(true);
1006			} else {
1007				return false;
1008			}
1009		}
1010		return true;
1011	}
1012
1013	private static BitmapWorkerTask getBitmapWorkerTask(ImageView imageView) {
1014		if (imageView != null) {
1015			final Drawable drawable = imageView.getDrawable();
1016			if (drawable instanceof AsyncDrawable) {
1017				final AsyncDrawable asyncDrawable = (AsyncDrawable) drawable;
1018				return asyncDrawable.getBitmapWorkerTask();
1019			}
1020		}
1021		return null;
1022	}
1023
1024	static class AsyncDrawable extends BitmapDrawable {
1025		private final WeakReference<BitmapWorkerTask> bitmapWorkerTaskReference;
1026
1027		public AsyncDrawable(Resources res, Bitmap bitmap, BitmapWorkerTask bitmapWorkerTask) {
1028			super(res, bitmap);
1029			bitmapWorkerTaskReference = new WeakReference<>(bitmapWorkerTask);
1030		}
1031
1032		public BitmapWorkerTask getBitmapWorkerTask() {
1033			return bitmapWorkerTaskReference.get();
1034		}
1035	}
1036}