From c3164a8e84535ea02166148bf4cc5a618407d8ba Mon Sep 17 00:00:00 2001 From: Ahmed Ibrahim Khalil Date: Tue, 28 Mar 2017 14:34:36 +0200 Subject: [PATCH] Support copying links on long click. Fixes #6343 Closes #6454 --- res/layout/conversation_item_received.xml | 4 +- res/layout/conversation_item_sent.xml | 2 - res/values/strings.xml | 1 + .../securesms/ConversationItem.java | 27 ++++- .../securesms/util/LongClickCopySpan.java | 73 +++++++++++++ .../util/LongClickMovementMethod.java | 103 ++++++++++++++++++ 6 files changed, 203 insertions(+), 7 deletions(-) create mode 100644 src/org/thoughtcrime/securesms/util/LongClickCopySpan.java create mode 100644 src/org/thoughtcrime/securesms/util/LongClickMovementMethod.java diff --git a/res/layout/conversation_item_received.xml b/res/layout/conversation_item_received.xml index 3752f5578..fa357bf2c 100644 --- a/res/layout/conversation_item_received.xml +++ b/res/layout/conversation_item_received.xml @@ -75,9 +75,7 @@ android:textAppearance="?android:attr/textAppearanceSmall" android:textColor="?conversation_item_received_text_primary_color" android:textColorLink="?conversation_item_received_text_primary_color" - android:textSize="@dimen/conversation_item_body_text_size" - android:autoLink="all" - android:linksClickable="true" /> + android:textSize="@dimen/conversation_item_body_text_size" /> Fallback to unencrypted MMS? This message will not be encrypted because the recipient is no longer a Signal user.\n\nSend unsecured message? Can\'t find an app able to open this media. + Copied %s from %s to %s diff --git a/src/org/thoughtcrime/securesms/ConversationItem.java b/src/org/thoughtcrime/securesms/ConversationItem.java index 9fb7909db..fa0b4d6ef 100644 --- a/src/org/thoughtcrime/securesms/ConversationItem.java +++ b/src/org/thoughtcrime/securesms/ConversationItem.java @@ -28,7 +28,10 @@ import android.os.AsyncTask; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.v7.app.AlertDialog; +import android.text.SpannableString; +import android.text.Spanned; import android.text.TextUtils; +import android.text.style.URLSpan; import android.text.util.Linkify; import android.util.AttributeSet; import android.util.Log; @@ -69,6 +72,8 @@ import org.thoughtcrime.securesms.recipients.Recipients; import org.thoughtcrime.securesms.service.ExpiringMessageManager; import org.thoughtcrime.securesms.util.DateUtils; import org.thoughtcrime.securesms.util.DynamicTheme; +import org.thoughtcrime.securesms.util.LongClickCopySpan; +import org.thoughtcrime.securesms.util.LongClickMovementMethod; import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.thoughtcrime.securesms.util.Util; import org.thoughtcrime.securesms.util.dualsim.SubscriptionInfoCompat; @@ -164,6 +169,8 @@ public class ConversationItem extends LinearLayout bodyText.setOnLongClickListener(passthroughClickListener); bodyText.setOnClickListener(passthroughClickListener); + + bodyText.setMovementMethod(LongClickMovementMethod.getInstance(getContext())); } @Override @@ -266,7 +273,6 @@ public class ConversationItem extends LinearLayout private void setInteractionState(MessageRecord messageRecord) { setSelected(batchSelected.contains(messageRecord)); - bodyText.setAutoLinkMask(batchSelected.isEmpty() ? Linkify.ALL : 0); if (mediaThumbnailStub.resolved()) { mediaThumbnailStub.get().setFocusable(!shouldInterceptClicks(messageRecord) && batchSelected.isEmpty()); @@ -310,7 +316,7 @@ public class ConversationItem extends LinearLayout if (isCaptionlessMms(messageRecord)) { bodyText.setVisibility(View.GONE); } else { - bodyText.setText(messageRecord.getDisplayBody()); + bodyText.setText(linkifyMessageBody(messageRecord.getDisplayBody(), batchSelected.isEmpty())); bodyText.setVisibility(View.VISIBLE); } } @@ -370,6 +376,20 @@ public class ConversationItem extends LinearLayout } } + private SpannableString linkifyMessageBody(SpannableString messageBody, boolean shouldLinkifyAllLinks) { + boolean hasLinks = Linkify.addLinks(messageBody, shouldLinkifyAllLinks ? Linkify.ALL : 0); + + if (hasLinks) { + URLSpan[] urlSpans = messageBody.getSpans(0, messageBody.length(), URLSpan.class); + for (URLSpan urlSpan : urlSpans) { + int start = messageBody.getSpanStart(urlSpan); + int end = messageBody.getSpanEnd(urlSpan); + messageBody.setSpan(new LongClickCopySpan(urlSpan.getURL()), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + } + } + return messageBody; + } + private void setStatusIcons(MessageRecord messageRecord) { indicatorText.setVisibility(View.GONE); @@ -578,6 +598,9 @@ public class ConversationItem extends LinearLayout @Override public boolean onLongClick(View v) { + if (bodyText.hasSelection()) { + return false; + } performLongClick(); return true; } diff --git a/src/org/thoughtcrime/securesms/util/LongClickCopySpan.java b/src/org/thoughtcrime/securesms/util/LongClickCopySpan.java new file mode 100644 index 000000000..c3a1d6820 --- /dev/null +++ b/src/org/thoughtcrime/securesms/util/LongClickCopySpan.java @@ -0,0 +1,73 @@ +package org.thoughtcrime.securesms.util; + +import android.annotation.TargetApi; +import android.content.ClipData; +import android.content.Context; +import android.support.annotation.ColorInt; +import android.text.TextPaint; +import android.text.style.URLSpan; +import android.view.View; +import android.widget.Toast; + +import org.thoughtcrime.securesms.R; + +public class LongClickCopySpan extends URLSpan { + private static final String PREFIX_MAILTO = "mailto:"; + private static final String PREFIX_TEL = "tel:"; + + private boolean isHighlighted; + @ColorInt + private int highlightColor; + + public LongClickCopySpan(String url) { + super(url); + } + + void onLongClick(View widget) { + Context context = widget.getContext(); + String preparedUrl = prepareUrl(getURL()); + copyUrl(context, preparedUrl); + Toast.makeText(context, + context.getString(R.string.ConversationItem_copied_text, preparedUrl), Toast.LENGTH_SHORT).show(); + } + + @Override + public void updateDrawState(TextPaint ds) { + super.updateDrawState(ds); + ds.bgColor = highlightColor; + ds.setUnderlineText(!isHighlighted); + } + + void setHighlighted(boolean highlighted, @ColorInt int highlightColor) { + this.isHighlighted = highlighted; + this.highlightColor = highlightColor; + } + + private void copyUrl(Context context, String url) { + int sdk = android.os.Build.VERSION.SDK_INT; + if (sdk < android.os.Build.VERSION_CODES.HONEYCOMB) { + @SuppressWarnings("deprecation") android.text.ClipboardManager clipboard = + (android.text.ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE); + clipboard.setText(url); + } else { + copyUriSdk11(context, url); + } + } + + @TargetApi(android.os.Build.VERSION_CODES.HONEYCOMB) + private void copyUriSdk11(Context context, String url) { + android.content.ClipboardManager clipboard = + (android.content.ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE); + ClipData clip = ClipData.newPlainText(context.getString(R.string.app_name), url); + clipboard.setPrimaryClip(clip); + } + + private String prepareUrl(String url) { + if (url.startsWith(PREFIX_MAILTO)) { + return url.substring(PREFIX_MAILTO.length()); + } else if (url.startsWith(PREFIX_TEL)) { + return url.substring(PREFIX_TEL.length()); + } + return url; + } +} diff --git a/src/org/thoughtcrime/securesms/util/LongClickMovementMethod.java b/src/org/thoughtcrime/securesms/util/LongClickMovementMethod.java new file mode 100644 index 000000000..48b34aa55 --- /dev/null +++ b/src/org/thoughtcrime/securesms/util/LongClickMovementMethod.java @@ -0,0 +1,103 @@ +package org.thoughtcrime.securesms.util; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.graphics.Color; +import android.support.v4.content.ContextCompat; +import android.text.Layout; +import android.text.Selection; +import android.text.Spannable; +import android.text.method.LinkMovementMethod; +import android.view.GestureDetector; +import android.view.MotionEvent; +import android.view.View; +import android.widget.TextView; + +import org.thoughtcrime.securesms.R; + +public class LongClickMovementMethod extends LinkMovementMethod { + @SuppressLint("StaticFieldLeak") + private static LongClickMovementMethod sInstance; + + private final GestureDetector gestureDetector; + private View widget; + private LongClickCopySpan currentSpan; + + private LongClickMovementMethod(final Context context) { + gestureDetector = new GestureDetector(context, new GestureDetector.SimpleOnGestureListener() { + @Override + public void onLongPress(MotionEvent e) { + if (currentSpan != null && widget != null) { + currentSpan.onLongClick(widget); + widget = null; + currentSpan = null; + } + } + + @Override + public boolean onSingleTapUp(MotionEvent e) { + if (currentSpan != null && widget != null) { + currentSpan.onClick(widget); + widget = null; + currentSpan = null; + } + return true; + } + }); + } + + @Override + public boolean onTouchEvent(TextView widget, Spannable buffer, MotionEvent event) { + int action = event.getAction(); + + if (action == MotionEvent.ACTION_UP || + action == MotionEvent.ACTION_DOWN) { + int x = (int) event.getX(); + int y = (int) event.getY(); + + x -= widget.getTotalPaddingLeft(); + y -= widget.getTotalPaddingTop(); + + x += widget.getScrollX(); + y += widget.getScrollY(); + + Layout layout = widget.getLayout(); + int line = layout.getLineForVertical(y); + int off = layout.getOffsetForHorizontal(line, x); + + LongClickCopySpan longClickCopySpan[] = buffer.getSpans(off, off, LongClickCopySpan.class); + if (longClickCopySpan.length != 0) { + LongClickCopySpan aSingleSpan = longClickCopySpan[0]; + if (action == MotionEvent.ACTION_DOWN) { + Selection.setSelection(buffer, buffer.getSpanStart(aSingleSpan), + buffer.getSpanEnd(aSingleSpan)); + aSingleSpan.setHighlighted(true, + ContextCompat.getColor(widget.getContext(), R.color.touch_highlight)); + } else { + Selection.removeSelection(buffer); + aSingleSpan.setHighlighted(false, Color.TRANSPARENT); + } + + this.currentSpan = aSingleSpan; + this.widget = widget; + return gestureDetector.onTouchEvent(event); + } + } else if (action == MotionEvent.ACTION_CANCEL) { + // Remove Selections. + LongClickCopySpan[] spans = buffer.getSpans(Selection.getSelectionStart(buffer), + Selection.getSelectionEnd(buffer), LongClickCopySpan.class); + for (LongClickCopySpan aSpan : spans) { + aSpan.setHighlighted(false, Color.TRANSPARENT); + } + Selection.removeSelection(buffer); + } + return super.onTouchEvent(widget, buffer, event); + } + + public static LongClickMovementMethod getInstance(Context context) { + if (sInstance == null) { + sInstance = new LongClickMovementMethod(context.getApplicationContext()); + } + return sInstance; + } +} \ No newline at end of file