Detailed changes
@@ -99,6 +99,10 @@ dependencies {
implementation 'com.google.guava:guava:33.4.0-android'
quicksyImplementation 'io.michaelrocks:libphonenumber-android:8.13.52'
implementation 'im.conversations.webrtc:webrtc-android:129.0.0'
+
+ //Testing
+ testImplementation 'junit:junit:4.13.2'
+
}
ext {
@@ -15,12 +15,14 @@ public class MiniUri {
private static final String EMPTY_STRING = "";
+ private final String raw;
private final String scheme;
private final String authority;
private final String path;
private final Map<String, String> parameter;
public MiniUri(final String uri) {
+ this.raw = uri;
final var schemeAndRest = Splitter.on(':').limit(2).splitToList(uri);
if (schemeAndRest.size() < 2) {
this.scheme = uri;
@@ -104,6 +106,10 @@ public class MiniUri {
: '/' + this.path;
}
+ public String getRaw() {
+ return this.raw;
+ }
+
public Map<String, String> getParameter() {
return this.parameter;
}
@@ -6,7 +6,7 @@ public class Patterns {
public static final Pattern URI_GENERIC =
Pattern.compile(
- "(?<=^|\\p{Zs}|\\p{P})(tel|xmpp|http|https|geo|mailto|web\\+ap|gemini):[\\p{L}\\p{M}\\p{N}\\-._~:/?#\\[\\]@!$&'*+,;=%]+(\\([\\p{L}\\p{M}\\p{N}\\-._~:/?#\\[\\]@!$&'*+,;=%]+\\))*[\\p{L}\\p{M}\\p{N}\\-._~:/?#\\[\\]@!$&'*+,;=%]*");
+ "(?<=^|\\p{Z}|\\s|\\p{P})(tel|xmpp|http|https|geo|mailto|web\\+ap|gemini):[\\p{L}\\p{M}\\p{N}\\-._~:/?#\\[\\]@!$&'*+,;=%]+(\\([\\p{L}\\p{M}\\p{N}\\-._~:/?#\\[\\]@!$&'*+,;=%]+\\))*[\\p{L}\\p{M}\\p{N}\\-._~:/?#\\[\\]@!$&'*+,;=%]*");
public static final Pattern URI_TEL =
Pattern.compile("^tel:\\+?(\\d{1,4}[-./()\\s]?)*\\d{1,4}(;.*)?$");
@@ -31,7 +31,6 @@ import android.os.SystemClock;
import android.preference.PreferenceManager;
import android.provider.MediaStore;
import android.text.Editable;
-import android.text.SpannableStringBuilder;
import android.text.TextUtils;
import android.util.Log;
import android.view.ContextMenu;
@@ -65,6 +64,7 @@ import androidx.databinding.DataBindingUtil;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.google.common.base.Optional;
import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
import de.gultsch.common.Patterns;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.R;
@@ -100,6 +100,7 @@ import eu.siacs.conversations.ui.util.EditMessageActionModeCallback;
import eu.siacs.conversations.ui.util.ListViewUtils;
import eu.siacs.conversations.ui.util.MenuDoubleTabUtil;
import eu.siacs.conversations.ui.util.MucDetailsContextMenuHelper;
+import eu.siacs.conversations.ui.util.MyLinkify;
import eu.siacs.conversations.ui.util.PendingItem;
import eu.siacs.conversations.ui.util.PresenceSelector;
import eu.siacs.conversations.ui.util.ScrollState;
@@ -1347,13 +1348,22 @@ public class ConversationFragment extends XmppFragment
&& t == null) {
copyMessage.setVisible(true);
quoteMessage.setVisible(!showError && !MessageUtils.prepareQuote(m).isEmpty());
- final String scheme =
- ShareUtil.getLinkScheme(new SpannableStringBuilder(m.getBody()));
- if ("xmpp".equals(scheme)) {
- copyLink.setTitle(R.string.copy_jabber_id);
- copyLink.setVisible(true);
- } else if (scheme != null) {
+ final var firstUri = Iterables.getFirst(MyLinkify.getLinks(m.getBody()), null);
+ if (firstUri != null) {
+ final var scheme = firstUri.getScheme();
+ final @StringRes int resForScheme =
+ switch (scheme) {
+ case "xmpp" -> R.string.copy_jabber_id;
+ case "http", "https", "gemini" -> R.string.copy_link;
+ case "geo" -> R.string.copy_geo_uri;
+ case "tel" -> R.string.copy_telephone_number;
+ case "mailto" -> R.string.copy_email_address;
+ default -> R.string.copy_URI;
+ };
+ copyLink.setTitle(resForScheme);
copyLink.setVisible(true);
+ } else {
+ copyLink.setVisible(false);
}
}
if (m.getEncryption() == Message.ENCRYPTION_DECRYPTION_FAILED && !deleted) {
@@ -31,72 +31,50 @@ package eu.siacs.conversations.ui.util;
import android.net.Uri;
import android.text.Editable;
-import android.text.style.URLSpan;
import android.text.util.Linkify;
import com.google.common.base.Splitter;
-import com.google.common.collect.Collections2;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
-import com.google.common.collect.Lists;
+import de.gultsch.common.MiniUri;
import de.gultsch.common.Patterns;
import eu.siacs.conversations.ui.text.FixedURLSpan;
import eu.siacs.conversations.utils.XmppUri;
-import java.util.Arrays;
-import java.util.Collection;
-import java.util.Comparator;
import java.util.List;
-import java.util.Objects;
public class MyLinkify {
private static final Linkify.MatchFilter MATCH_FILTER =
- (s, start, end) -> {
- final var match = s.subSequence(start, end);
- final var scheme =
- Iterables.getFirst(Splitter.on(':').limit(2).splitToList(match), null);
- if (scheme == null) {
- return false;
- }
- return switch (scheme) {
- case "tel" -> Patterns.URI_TEL.matcher(match).matches();
- case "http", "https" -> Patterns.URI_HTTP.matcher(match).matches();
- case "geo" -> Patterns.URI_GEO.matcher(match).matches();
- case "xmpp" -> new XmppUri(Uri.parse(match.toString())).isValidJid();
- case "web+ap" -> Patterns.URI_WEB_AP.matcher(match).matches();
- default -> true;
- };
- };
+ (s, start, end) -> isPassAdditionalValidation(s.subSequence(start, end).toString());
+
+ private static boolean isPassAdditionalValidation(final String match) {
+ final var scheme = Iterables.getFirst(Splitter.on(':').limit(2).splitToList(match), null);
+ if (scheme == null) {
+ return false;
+ }
+ return switch (scheme) {
+ case "tel" -> Patterns.URI_TEL.matcher(match).matches();
+ case "http", "https" -> Patterns.URI_HTTP.matcher(match).matches();
+ case "geo" -> Patterns.URI_GEO.matcher(match).matches();
+ case "xmpp" -> new XmppUri(Uri.parse(match)).isValidJid();
+ case "web+ap" -> Patterns.URI_WEB_AP.matcher(match).matches();
+ default -> true;
+ };
+ }
public static void addLinks(final Editable body) {
Linkify.addLinks(body, Patterns.URI_GENERIC, null, MATCH_FILTER, null);
FixedURLSpan.fix(body);
}
- public static List<String> extractLinks(final Editable body) {
- MyLinkify.addLinks(body);
- final Collection<URLSpan> spans =
- Arrays.asList(body.getSpans(0, body.length() - 1, URLSpan.class));
- final Collection<UrlWrapper> urlWrappers =
- Collections2.filter(
- Collections2.transform(
- spans,
- s ->
- s == null
- ? null
- : new UrlWrapper(body.getSpanStart(s), s.getURL())),
- Objects::nonNull);
- List<UrlWrapper> sorted =
- ImmutableList.sortedCopyOf(Comparator.comparingInt(a -> a.position), urlWrappers);
- return Lists.transform(sorted, uw -> uw.url);
- }
-
- private static class UrlWrapper {
- private final int position;
- private final String url;
-
- private UrlWrapper(int position, String url) {
- this.position = position;
- this.url = url;
+ public static List<MiniUri> getLinks(final String body) {
+ final var builder = new ImmutableList.Builder<MiniUri>();
+ final var matcher = Patterns.URI_GENERIC.matcher(body);
+ while (matcher.find()) {
+ final var match = matcher.group();
+ if (isPassAdditionalValidation(match)) {
+ builder.add(new MiniUri(match));
+ }
}
+ return builder.build();
}
}
@@ -31,20 +31,23 @@ package eu.siacs.conversations.ui.util;
import android.content.ActivityNotFoundException;
import android.content.Intent;
-import android.net.Uri;
-import android.text.SpannableStringBuilder;
import android.widget.Toast;
+import androidx.annotation.StringRes;
+import com.google.common.collect.Iterables;
import eu.siacs.conversations.R;
import eu.siacs.conversations.entities.DownloadableFile;
import eu.siacs.conversations.entities.Message;
import eu.siacs.conversations.persistance.FileBackend;
import eu.siacs.conversations.ui.ConversationsActivity;
import eu.siacs.conversations.ui.XmppActivity;
-import eu.siacs.conversations.utils.XmppUri;
-import eu.siacs.conversations.xmpp.Jid;
+import java.util.Arrays;
+import java.util.Collection;
public class ShareUtil {
+ private static final Collection<String> SCHEMES_COPY_PATH_ONLY =
+ Arrays.asList("xmpp", "mailto", "tel");
+
public static void share(XmppActivity activity, Message message) {
Intent shareIntent = new Intent();
shareIntent.setAction(Intent.ACTION_SEND);
@@ -120,44 +123,32 @@ public class ShareUtil {
}
public static void copyLinkToClipboard(final XmppActivity activity, final Message message) {
- final SpannableStringBuilder body = new SpannableStringBuilder(message.getBody());
- for (final String url : MyLinkify.extractLinks(body)) {
- final Uri uri = Uri.parse(url);
- if ("xmpp".equals(uri.getScheme())) {
- try {
- final Jid jid = new XmppUri(uri).getJid();
- if (activity.copyTextToClipboard(
- jid.asBareJid().toString(), R.string.account_settings_jabber_id)) {
- Toast.makeText(
- activity,
- R.string.jabber_id_copied_to_clipboard,
- Toast.LENGTH_SHORT)
- .show();
- }
- return;
- } catch (final Exception e) {
- return;
- }
- } else {
- if (activity.copyTextToClipboard(url, R.string.web_address)) {
- Toast.makeText(activity, R.string.url_copied_to_clipboard, Toast.LENGTH_SHORT)
- .show();
- }
- return;
- }
+ final var firstUri = Iterables.getFirst(MyLinkify.getLinks(message.getBody()), null);
+ if (firstUri == null) {
+ return;
}
- }
-
- public static String getLinkScheme(final SpannableStringBuilder body) {
- MyLinkify.addLinks(body);
- for (final String url : MyLinkify.extractLinks(body)) {
- final Uri uri = Uri.parse(url);
- if ("xmpp".equals(uri.getScheme())) {
- return uri.getScheme();
- } else {
- return "http";
- }
+ final String clip;
+ if (SCHEMES_COPY_PATH_ONLY.contains(firstUri.getScheme())) {
+ clip = firstUri.getPath();
+ } else {
+ clip = firstUri.getRaw();
+ }
+ final @StringRes int label =
+ switch (firstUri.getScheme()) {
+ case "http", "https", "gemini" -> R.string.web_address;
+ case "xmpp" -> R.string.account_settings_jabber_id;
+ default -> R.string.uri;
+ };
+ final @StringRes int toast =
+ switch (firstUri.getScheme()) {
+ case "http", "https", "gemini", "web+ap" -> R.string.url_copied_to_clipboard;
+ case "xmpp" -> R.string.jabber_id_copied_to_clipboard;
+ case "tel" -> R.string.copied_phone_number;
+ case "mailto" -> R.string.copied_email_address;
+ default -> R.string.uri_copied_to_clipboard;
+ };
+ if (activity.copyTextToClipboard(clip, label)) {
+ Toast.makeText(activity, toast, Toast.LENGTH_SHORT).show();
}
- return null;
}
}
@@ -319,6 +319,8 @@
<string name="retry_with_p2p">Retry with P2P</string>
<string name="file_url">File URL</string>
<string name="url_copied_to_clipboard">Copied URL to clipboard</string>
+ <string name="uri_copied_to_clipboard">Copied URI to clipboard</string>
+ <string name="uri">URI</string>
<string name="jabber_id_copied_to_clipboard">Copied XMPP address to clipboard</string>
<string name="error_message_copied_to_clipboard">Copied error message to clipboard</string>
<string name="web_address">web address</string>
@@ -747,7 +749,13 @@
<string name="pref_use_share_location_plugin">Share Location Plugin</string>
<string name="pref_use_share_location_plugin_summary">Use the Share Location Plugin instead of the built-in map</string>
<string name="copy_link">Copy web address</string>
+ <string name="copy_URI">Copy URI</string>
+ <string name="copy_telephone_number">Copy phone number</string>
+ <string name="copy_geo_uri">Copy geo location</string>
<string name="copy_jabber_id">Copy XMPP address</string>
+ <string name="copy_email_address">Copy email address</string>
+ <string name="copied_email_address">Copied email address to clipboard</string>
+ <string name="copied_phone_number">Copied phone number to clipboard</string>
<string name="p1_s3_filetransfer">HTTP File Sharing for S3</string>
<string name="pref_start_search">Direct Search</string>
<string name="pref_start_search_summary">At βNew chatβ screen open keyboard and place cursor in search field</string>
@@ -91,4 +91,30 @@ public class PatternTest {
Assert.assertEquals(ImmutableList.of("https://conversations.im"), matches);
}
+
+ @Test
+ public void newLine() {
+ final var message = "\nxmpp:example.com";
+ final var matches =
+ Patterns.URI_GENERIC
+ .matcher(message)
+ .results()
+ .map(MatchResult::group)
+ .collect(Collectors.toList());
+
+ Assert.assertEquals(ImmutableList.of("xmpp:example.com"), matches);
+ }
+
+ @Test
+ public void code() {
+ final var message = "`xmpp:example.com`";
+ final var matches =
+ Patterns.URI_GENERIC
+ .matcher(message)
+ .results()
+ .map(MatchResult::group)
+ .collect(Collectors.toList());
+
+ Assert.assertTrue(matches.isEmpty());
+ }
}