apply styling helper to conversation overview

Daniel Gultsch created

Change summary

src/main/java/eu/siacs/conversations/services/NotificationService.java   |  4 
src/main/java/eu/siacs/conversations/ui/adapter/ConversationAdapter.java |  4 
src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java      |  2 
src/main/java/eu/siacs/conversations/utils/CharSequenceUtils.java        | 86 
src/main/java/eu/siacs/conversations/utils/StylingHelper.java            | 47 
src/main/java/eu/siacs/conversations/utils/UIHelper.java                 | 30 
6 files changed, 158 insertions(+), 15 deletions(-)

Detailed changes

src/main/java/eu/siacs/conversations/services/NotificationService.java 🔗

@@ -548,10 +548,10 @@ public class NotificationService {
 		}
 		/** message preview for Android Auto **/
 		for (Message message : messages) {
-			Pair<String, Boolean> preview = UIHelper.getMessagePreview(mXmppConnectionService, message);
+			Pair<CharSequence, Boolean> preview = UIHelper.getMessagePreview(mXmppConnectionService, message);
 			// only show user written text
 			if (!preview.second) {
-				uBuilder.addMessage(preview.first);
+				uBuilder.addMessage(preview.first.toString());
 				uBuilder.setLatestTimestamp(message.getTimeSent());
 			}
 		}

src/main/java/eu/siacs/conversations/ui/adapter/ConversationAdapter.java 🔗

@@ -156,9 +156,9 @@ public class ConversationAdapter extends RecyclerView.Adapter<ConversationAdapte
 				viewHolder.lastMessageIcon.setVisibility(View.GONE);
 				showPreviewText = true;
 			}
-			final Pair<String, Boolean> preview = UIHelper.getMessagePreview(activity, message);
+			final Pair<CharSequence, Boolean> preview = UIHelper.getMessagePreview(activity, message, viewHolder.lastMessage.getCurrentTextColor());
 			if (showPreviewText) {
-				viewHolder.lastMessage.setText(EmojiWrapper.transform(preview.first));
+				viewHolder.lastMessage.setText(EmojiWrapper.transform(UIHelper.shorten(preview.first)));
 			} else {
 				viewHolder.lastMessageIcon.setContentDescription(preview.first);
 			}

src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java 🔗

@@ -360,7 +360,7 @@ public class MessageAdapter extends ArrayAdapter<Message> implements CopyTextVie
 		}
 	}
 
-	private void displayInfoMessage(ViewHolder viewHolder, String text, boolean darkBackground) {
+	private void displayInfoMessage(ViewHolder viewHolder, CharSequence text, boolean darkBackground) {
 		viewHolder.download_button.setVisibility(View.GONE);
 		viewHolder.audioPlayer.setVisibility(View.GONE);
 		viewHolder.image.setVisibility(View.GONE);

src/main/java/eu/siacs/conversations/utils/CharSequenceUtils.java 🔗

@@ -0,0 +1,86 @@
+/*
+ * Copyright (c) 2018, Daniel Gultsch All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without modification,
+ * are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation and/or
+ * other materials provided with the distribution.
+ *
+ * 3. Neither the name of the copyright holder nor the names of its contributors
+ * may be used to endorse or promote products derived from this software without
+ * specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
+ * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package eu.siacs.conversations.utils;
+
+import android.text.Spannable;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class CharSequenceUtils {
+
+	private static int getStartIndex(CharSequence input) {
+		int length = input.length();
+		int index = 0;
+		while (Character.isWhitespace(input.charAt(index))) {
+			++index;
+			if (index >= length) {
+				break;
+			}
+		}
+		return index;
+	}
+
+	private static int getEndIndex(CharSequence input) {
+		int index = input.length() - 1;
+		while (Character.isWhitespace(input.charAt(index))) {
+			--index;
+			if (index < 0) {
+				break;
+			}
+		}
+		return index;
+	}
+
+	public static CharSequence trim(CharSequence input) {
+		int begin = getStartIndex(input);
+		int end = getEndIndex(input);
+		if (begin > end) {
+			return "";
+		} else {
+			return StylingHelper.subSequence(input, begin, end + 1);
+		}
+	}
+
+	public static List<CharSequence> split(Spannable charSequence, char c) {
+		List<CharSequence> out = new ArrayList<>();
+		int begin = 0;
+		for (int i = 0; i < charSequence.length(); ++i) {
+			if (charSequence.charAt(i) == c) {
+				out.add(StylingHelper.subSequence(charSequence, begin, i));
+				begin = ++i;
+			}
+		}
+		if (begin < charSequence.length()) {
+			out.add(StylingHelper.subSequence(charSequence, begin, charSequence.length()));
+		}
+		return out;
+	}
+}

src/main/java/eu/siacs/conversations/utils/StylingHelper.java 🔗

@@ -36,6 +36,7 @@ import android.support.annotation.ColorInt;
 import android.support.v4.content.ContextCompat;
 import android.text.Editable;
 import android.text.ParcelableSpan;
+import android.text.Spannable;
 import android.text.SpannableString;
 import android.text.Spanned;
 import android.text.TextWatcher;
@@ -44,6 +45,7 @@ import android.text.style.ForegroundColorSpan;
 import android.text.style.StrikethroughSpan;
 import android.text.style.StyleSpan;
 import android.text.style.TypefaceSpan;
+import android.util.Log;
 import android.widget.EditText;
 import android.widget.TextView;
 
@@ -51,6 +53,7 @@ import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
 
+import eu.siacs.conversations.Config;
 import eu.siacs.conversations.R;
 import eu.siacs.conversations.entities.Message;
 import eu.siacs.conversations.ui.text.QuoteSpan;
@@ -93,7 +96,7 @@ public class StylingHelper {
 	}
 
 	public static void highlight(final Context context, final Editable editable, List<String> needles, boolean dark) {
-		for(String needle : needles) {
+		for (String needle : needles) {
 			if (!FtsUtils.isKeyword(needle)) {
 				highlight(context, editable, needle, dark);
 			}
@@ -102,7 +105,7 @@ public class StylingHelper {
 
 	public static List<String> filterHighlightedWords(List<String> terms) {
 		List<String> words = new ArrayList<>();
-		for(String term : terms) {
+		for (String term : terms) {
 			if (!FtsUtils.isKeyword(term)) {
 				StringBuilder builder = new StringBuilder();
 				for (int codepoint, i = 0; i < term.length(); i += Character.charCount(codepoint)) {
@@ -132,6 +135,44 @@ public class StylingHelper {
 
 	}
 
+	static CharSequence subSequence(CharSequence charSequence, int start, int end) {
+		if (start == 0 && charSequence.length() + 1 == end) {
+			return charSequence;
+		}
+		if (charSequence instanceof Spannable) {
+			Spannable spannable = (Spannable) charSequence;
+			Spannable sub = (Spannable) spannable.subSequence(start, end);
+			for (Class<? extends ParcelableSpan> clazz : SPAN_CLASSES) {
+				ParcelableSpan[] spannables = spannable.getSpans(start, end, clazz);
+				for (ParcelableSpan parcelableSpan : spannables) {
+					int beginSpan = spannable.getSpanStart(parcelableSpan);
+					int endSpan = spannable.getSpanEnd(parcelableSpan);
+					if (beginSpan >= start && endSpan <= end) {
+						continue;
+					}
+					sub.setSpan(clone(parcelableSpan), Math.max(beginSpan - start, 0), Math.min(sub.length() - 1, endSpan), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+				}
+			}
+			return sub;
+		} else {
+			return charSequence.subSequence(start, end);
+		}
+	}
+
+	private static ParcelableSpan clone(ParcelableSpan span) {
+		if (span instanceof ForegroundColorSpan) {
+			return new ForegroundColorSpan(((ForegroundColorSpan) span).getForegroundColor());
+		} else if (span instanceof TypefaceSpan) {
+			return new TypefaceSpan(((TypefaceSpan) span).getFamily());
+		} else if (span instanceof StyleSpan) {
+			return new StyleSpan(((StyleSpan) span).getStyle());
+		} else if (span instanceof StrikethroughSpan) {
+			return new StrikethroughSpan();
+		} else {
+			throw new AssertionError("Unknown Span");
+		}
+	}
+
 	public static boolean isDarkText(TextView textView) {
 		int argb = textView.getCurrentTextColor();
 		return Color.red(argb) + Color.green(argb) + Color.blue(argb) == 0;
@@ -163,7 +204,7 @@ public class StylingHelper {
 	private static
 	@ColorInt
 	int transformColor(@ColorInt int c) {
-		return Color.argb(Math.round(Color.alpha(c) * 0.6f), Color.red(c), Color.green(c), Color.blue(c));
+		return Color.argb(Math.round(Color.alpha(c) * 0.45f), Color.red(c), Color.green(c), Color.blue(c));
 	}
 
 	private static int indexOfIgnoreCase(final String haystack, final String needle, final int start) {

src/main/java/eu/siacs/conversations/utils/UIHelper.java 🔗

@@ -1,8 +1,12 @@
 package eu.siacs.conversations.utils;
 
 import android.content.Context;
+import android.support.annotation.ColorInt;
+import android.text.SpannableString;
+import android.text.SpannableStringBuilder;
 import android.text.format.DateFormat;
 import android.text.format.DateUtils;
+import android.util.Log;
 import android.util.Pair;
 import android.widget.PopupMenu;
 
@@ -10,6 +14,7 @@ import java.lang.reflect.Field;
 import java.lang.reflect.Method;
 import java.math.BigInteger;
 import java.security.MessageDigest;
+import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Calendar;
 import java.util.Date;
@@ -244,7 +249,11 @@ public class UIHelper {
 		}
 	}
 
-	public static Pair<String, Boolean> getMessagePreview(final Context context, final Message message) {
+	public static Pair<CharSequence, Boolean> getMessagePreview(final Context context, final Message message) {
+		return getMessagePreview(context, message, 0);
+	}
+
+	public static Pair<CharSequence, Boolean> getMessagePreview(final Context context, final Message message, @ColorInt int textColor) {
 		final Transferable d = message.getTransferable();
 		if (d != null) {
 			switch (d.getStatus()) {
@@ -293,14 +302,17 @@ public class UIHelper {
 				return new Pair<>(context.getString(R.string.x_file_offered_for_download,
 						getFileDescriptionString(context, message)), true);
 			} else {
-				String[] lines = body.split("\n");
-				StringBuilder builder = new StringBuilder();
-				for (String l : lines) {
+				SpannableStringBuilder styledBody = new SpannableStringBuilder(body);
+				if (textColor != 0) {
+					StylingHelper.format(styledBody, 0, styledBody.length() - 1, textColor);
+				}
+				SpannableStringBuilder builder = new SpannableStringBuilder();
+				for (CharSequence l : CharSequenceUtils.split(styledBody, '\n')) {
 					if (l.length() > 0) {
 						char first = l.charAt(0);
 						if ((first != '>' || !isPositionFollowedByQuoteableCharacter(l, 0)) && first != '\u00bb') {
-							String line = l.trim();
-							if (line.isEmpty()) {
+							CharSequence line = CharSequenceUtils.trim(l);
+							if (line.length() == 0) {
 								continue;
 							}
 							char last = line.charAt(line.length() - 1);
@@ -317,11 +329,15 @@ public class UIHelper {
 				if (builder.length() == 0) {
 					builder.append(body.trim());
 				}
-				return new Pair<>(builder.length() > 256 ? builder.substring(0, 256) : builder.toString(), false);
+				return new Pair<>(builder, false);
 			}
 		}
 	}
 
+	public static CharSequence shorten(CharSequence input) {
+		return input.length() > 256 ? StylingHelper.subSequence(input, 0, 256) : input;
+	}
+
 	public static boolean isPositionFollowedByQuoteableCharacter(CharSequence body, int pos) {
 		return !isPositionFollowedByNumber(body, pos)
 				&& !isPositionFollowedByEmoticon(body, pos)