display irregular unicode code points

Daniel Gultsch created

Change summary

src/main/java/eu/siacs/conversations/entities/Bookmark.java                   |  10 
src/main/java/eu/siacs/conversations/entities/Contact.java                    |   9 
src/main/java/eu/siacs/conversations/entities/ListItem.java                   |   2 
src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java           |  15 
src/main/java/eu/siacs/conversations/ui/TrustKeysActivity.java                |   6 
src/main/java/eu/siacs/conversations/ui/adapter/ListItemAdapter.java          |   8 
src/main/java/eu/siacs/conversations/utils/IrregularUnicodeBlockDetector.java | 160 
src/main/res/values/attrs.xml                                                 |   1 
src/main/res/values/colors.xml                                                |   2 
src/main/res/values/themes.xml                                                |   2 
10 files changed, 177 insertions(+), 38 deletions(-)

Detailed changes

src/main/java/eu/siacs/conversations/entities/Bookmark.java 🔗

@@ -63,16 +63,6 @@ public class Bookmark extends Element implements ListItem {
 		}
 	}
 
-	@Override
-	public String getDisplayJid() {
-		Jid jid = getJid();
-		if (jid != null) {
-			return jid.toString();
-		} else {
-			return getAttribute("jid"); //fallback if jid wasn't parsable
-		}
-	}
-
 	@Override
 	public Jid getJid() {
 		return this.getAttributeAsJid("jid");

src/main/java/eu/siacs/conversations/entities/Contact.java 🔗

@@ -127,15 +127,6 @@ public class Contact implements ListItem, Blockable {
 		}
 	}
 
-	@Override
-	public String getDisplayJid() {
-		if (jid != null) {
-			return jid.toString();
-		} else {
-			return null;
-		}
-	}
-
 	public String getProfilePhoto() {
 		return this.photoUri;
 	}

src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java 🔗

@@ -38,6 +38,7 @@ import eu.siacs.conversations.entities.Contact;
 import eu.siacs.conversations.entities.ListItem;
 import eu.siacs.conversations.services.XmppConnectionService.OnAccountUpdate;
 import eu.siacs.conversations.services.XmppConnectionService.OnRosterUpdate;
+import eu.siacs.conversations.utils.IrregularUnicodeBlockDetector;
 import eu.siacs.conversations.utils.UIHelper;
 import eu.siacs.conversations.utils.XmppUri;
 import eu.siacs.conversations.xml.Namespace;
@@ -129,8 +130,7 @@ public class ContactDetailsActivity extends OmemoActivity implements OnAccountUp
 				AlertDialog.Builder builder = new AlertDialog.Builder(
 						ContactDetailsActivity.this);
 				builder.setTitle(getString(R.string.action_add_phone_book));
-				builder.setMessage(getString(R.string.add_phone_book_text,
-						contact.getDisplayJid()));
+				builder.setMessage(getString(R.string.add_phone_book_text, contact.getJid().toString()));
 				builder.setNegativeButton(getString(R.string.cancel), null);
 				builder.setPositiveButton(getString(R.string.add), addToPhonebook);
 				builder.create().show();
@@ -235,9 +235,7 @@ public class ContactDetailsActivity extends OmemoActivity implements OnAccountUp
 				break;
 			case R.id.action_delete_contact:
 				builder.setTitle(getString(R.string.action_delete_contact))
-					.setMessage(
-							getString(R.string.remove_contact_text,
-								contact.getDisplayJid()))
+					.setMessage(getString(R.string.remove_contact_text, contact.getJid().toString()))
 					.setPositiveButton(getString(R.string.delete),
 							removeFromRoster).create().show();
 				break;
@@ -386,12 +384,7 @@ public class ContactDetailsActivity extends OmemoActivity implements OnAccountUp
 			}
 		}
 
-		if (contact.getPresences().size() > 1) {
-			binding.detailsContactjid.setText(contact.getDisplayJid() + " ("
-					+ contact.getPresences().size() + ")");
-		} else {
-			binding.detailsContactjid.setText(contact.getDisplayJid());
-		}
+		binding.detailsContactjid.setText(IrregularUnicodeBlockDetector.style(this,contact.getJid()));
 		String account;
 		if (Config.DOMAIN_LOCK != null) {
 			account = contact.getAccount().getJid().getLocal();

src/main/java/eu/siacs/conversations/ui/TrustKeysActivity.java 🔗

@@ -35,6 +35,7 @@ import eu.siacs.conversations.databinding.KeysCardBinding;
 import eu.siacs.conversations.entities.Account;
 import eu.siacs.conversations.entities.Conversation;
 import eu.siacs.conversations.utils.CryptoHelper;
+import eu.siacs.conversations.utils.IrregularUnicodeBlockDetector;
 import eu.siacs.conversations.utils.XmppUri;
 import eu.siacs.conversations.xmpp.OnKeyStatusUpdated;
 import rocks.xmpp.addr.Jid;
@@ -195,9 +196,8 @@ public class TrustKeysActivity extends OmemoActivity implements OnKeyStatusUpdat
 			for (Map.Entry<Jid, Map<String, Boolean>> entry : foreignKeysToTrust.entrySet()) {
 				hasForeignKeys = true;
 				KeysCardBinding keysCardBinding =  DataBindingUtil.inflate(getLayoutInflater(),R.layout.keys_card, binding.foreignKeys,false);
-				//final LinearLayout layout = (LinearLayout) getLayoutInflater().inflate(R.layout.keys_card, foreignKeys, false);
 				final Jid jid = entry.getKey();
-				keysCardBinding.foreignKeysTitle.setText(jid.toString());
+				keysCardBinding.foreignKeysTitle.setText(IrregularUnicodeBlockDetector.style(this,jid));
 				keysCardBinding.foreignKeysTitle.setOnClickListener(v -> switchToContactDetails(mAccount.getRoster().getContact(jid)));
 				final Map<String, Boolean> fingerprints = entry.getValue();
 				for (final String fingerprint : fingerprints.keySet()) {
@@ -397,7 +397,7 @@ public class TrustKeysActivity extends OmemoActivity implements OnKeyStatusUpdat
 					fingerprint,
 					FingerprintStatus.createActive(ownKeysToTrust.get(fingerprint)));
 		}
-		List<Jid> acceptedTargets = mConversation == null ? new ArrayList<Jid>() : mConversation.getAcceptedCryptoTargets();
+		List<Jid> acceptedTargets = mConversation == null ? new ArrayList<>() : mConversation.getAcceptedCryptoTargets();
 		synchronized (this.foreignKeysToTrust) {
 			for (Map.Entry<Jid, Map<String, Boolean>> entry : foreignKeysToTrust.entrySet()) {
 				Jid jid = entry.getKey();

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

@@ -1,6 +1,5 @@
 package eu.siacs.conversations.ui.adapter;
 
-import android.content.Context;
 import android.content.SharedPreferences;
 import android.content.res.Resources;
 import android.databinding.DataBindingUtil;
@@ -27,8 +26,11 @@ import eu.siacs.conversations.databinding.ContactBinding;
 import eu.siacs.conversations.entities.ListItem;
 import eu.siacs.conversations.ui.SettingsActivity;
 import eu.siacs.conversations.ui.XmppActivity;
+import eu.siacs.conversations.ui.util.Color;
 import eu.siacs.conversations.utils.EmojiWrapper;
+import eu.siacs.conversations.utils.IrregularUnicodeBlockDetector;
 import eu.siacs.conversations.utils.UIHelper;
+import rocks.xmpp.addr.Jid;
 
 public class ListItemAdapter extends ArrayAdapter<ListItem> {
 
@@ -108,10 +110,10 @@ public class ListItemAdapter extends ArrayAdapter<ListItem> {
 				viewHolder.tags.addView(tv);
 			}
 		}
-		final String jid = item.getDisplayJid();
+		final Jid jid = item.getJid();
 		if (jid != null) {
 			viewHolder.jid.setVisibility(View.VISIBLE);
-			viewHolder.jid.setText(jid);
+			viewHolder.jid.setText(IrregularUnicodeBlockDetector.style(activity, jid));
 		} else {
 			viewHolder.jid.setVisibility(View.GONE);
 		}

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

@@ -0,0 +1,160 @@
+/*
+ * 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.content.Context;
+import android.support.annotation.ColorInt;
+import android.text.Spannable;
+import android.text.SpannableString;
+import android.text.SpannableStringBuilder;
+import android.text.style.ForegroundColorSpan;
+import android.util.LruCache;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import eu.siacs.conversations.R;
+import eu.siacs.conversations.ui.util.Color;
+import rocks.xmpp.addr.Jid;
+
+public class IrregularUnicodeBlockDetector {
+
+	private static final Map<Character.UnicodeBlock,Character.UnicodeBlock> NORMALIZATION_MAP;
+
+	static {
+		Map<Character.UnicodeBlock,Character.UnicodeBlock> temp = new HashMap<>();
+		temp.put(Character.UnicodeBlock.LATIN_1_SUPPLEMENT, Character.UnicodeBlock.BASIC_LATIN);
+		NORMALIZATION_MAP = Collections.unmodifiableMap(temp);
+	}
+
+	private static Character.UnicodeBlock normalize(Character.UnicodeBlock in) {
+		if (NORMALIZATION_MAP.containsKey(in)) {
+			return NORMALIZATION_MAP.get(in);
+		} else {
+			return in;
+		}
+	}
+
+	private static final LruCache<Jid, Pattern> CACHE = new LruCache<>(100);
+
+	public static Spannable style(Context context, Jid jid) {
+		return style(jid, Color.get(context, R.attr.color_warning));
+	}
+
+	private static Spannable style(Jid jid, @ColorInt int color) {
+		SpannableStringBuilder builder = new SpannableStringBuilder();
+		if (jid.getLocal() != null) {
+			SpannableString local = new SpannableString(jid.getLocal());
+			Matcher matcher = find(jid).matcher(local);
+			while (matcher.find()) {
+				if (matcher.start() < matcher.end()) {
+					local.setSpan(new ForegroundColorSpan(color), matcher.start(), matcher.end(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+				}
+			}
+			builder.append(local);
+			builder.append('@');
+		}
+		if (jid.getDomain() != null) {
+			builder.append(jid.getDomain());
+		}
+		if (builder.length() != 0 && jid.getResource() != null) {
+			builder.append('/');
+			builder.append(jid.getResource());
+		}
+		return builder;
+	}
+
+	private static Map<Character.UnicodeBlock, List<String>> map(Jid jid) {
+		Map<Character.UnicodeBlock, List<String>> map = new HashMap<>();
+		String local = jid.getLocal();
+		final int length = local.length();
+		for (int offset = 0; offset < length; ) {
+			final int codePoint = local.codePointAt(offset);
+			Character.UnicodeBlock block = normalize(Character.UnicodeBlock.of(codePoint));
+			List<String> codePoints;
+			if (map.containsKey(block)) {
+				codePoints = map.get(block);
+			} else {
+				codePoints = new ArrayList<>();
+				map.put(block, codePoints);
+			}
+			codePoints.add(String.copyValueOf(Character.toChars(codePoint)));
+			offset += Character.charCount(codePoint);
+		}
+		return map;
+	}
+
+	private static Set<String> eliminateFirstAndGetCodePoints(Map<Character.UnicodeBlock, List<String>> map) {
+		Character.UnicodeBlock block = Character.UnicodeBlock.BASIC_LATIN;
+		int size = 0;
+		for (Map.Entry<Character.UnicodeBlock, List<String>> entry : map.entrySet()) {
+			if (entry.getValue().size() > size) {
+				size = entry.getValue().size();
+				block = entry.getKey();
+			}
+		}
+		map.remove(block);
+		Set<String> all = new HashSet<>();
+		for (List<String> codePoints : map.values()) {
+			all.addAll(codePoints);
+		}
+		return all;
+	}
+
+	private static Pattern find(Jid jid) {
+		synchronized (CACHE) {
+			Pattern pattern = CACHE.get(jid);
+			if (pattern != null) {
+				return pattern;
+			}
+			pattern = create(eliminateFirstAndGetCodePoints(map(jid)));
+			CACHE.put(jid, pattern);
+			return pattern;
+		}
+	}
+
+	private static Pattern create(Set<String> codePoints) {
+		final StringBuilder pattern = new StringBuilder();
+		for (String codePoint : codePoints) {
+			if (pattern.length() != 0) {
+				pattern.append('|');
+			}
+			pattern.append(Pattern.quote(codePoint));
+		}
+		return Pattern.compile(pattern.toString());
+	}
+}

src/main/res/values/attrs.xml 🔗

@@ -8,6 +8,7 @@
 
     <attr name="color_background_secondary" format="reference|color" />
     <attr name="color_background_primary" format="reference|color" />
+    <attr name="color_warning" format="reference|color"/>
 
     <attr name="ic_send_cancel_offline" format="reference"/>
     <attr name="ic_send_location_offline" format="reference"/>

src/main/res/values/colors.xml 🔗

@@ -19,6 +19,8 @@
 	<color name="grey800">#ff424242</color>
 	<color name="grey900">#ff282828</color>
 	<color name="red500">#fff44336</color>
+	<color name="red_a700">#ffd50000</color>
+	<color name="red_a100">#ffff8a80</color>
 	<color name="red800">#ffc62828</color>
 	<color name="orange500">#ffff9800</color>
 	<color name="green500">#ff259b24</color>

src/main/res/values/themes.xml 🔗

@@ -8,6 +8,7 @@
 
         <item name="color_background_primary">@color/grey50</item>
         <item name="color_background_secondary">@color/grey200</item>
+        <item name="color_warning">@color/red_a700</item>
 
         <item name="android:windowActionModeOverlay">true</item>
         <item name="android:actionModeBackground">@color/accent</item>
@@ -84,6 +85,7 @@
 
         <item name="color_background_primary">@color/grey800</item>
         <item name="color_background_secondary">@color/grey900</item>
+        <item name="color_warning">@color/red_a100</item>
 
         <item name="android:windowActionModeOverlay">true</item>
         <item name="android:actionModeBackground">@color/accent</item>