fedilab-Android-App/app/src/main/java/app/fedilab/android/helper/SpannableHelper.java

828 lines
44 KiB
Java

package app.fedilab.android.helper;
/* Copyright 2021 Thomas Schneider
*
* This file is a part of Fedilab
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Fedilab; if not,
* see <http://www.gnu.org/licenses>. */
import static app.fedilab.android.helper.Helper.USER_AGENT;
import static app.fedilab.android.helper.Helper.convertDpToPixel;
import static app.fedilab.android.helper.Helper.urlPattern;
import static app.fedilab.android.helper.ThemeHelper.linkColor;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.text.Html;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.TextPaint;
import android.text.style.ClickableSpan;
import android.text.style.ImageSpan;
import android.text.style.URLSpan;
import android.util.Patterns;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.preference.PreferenceManager;
import com.bumptech.glide.Glide;
import com.bumptech.glide.request.FutureTarget;
import com.github.penfeizhou.animation.apng.APNGDrawable;
import com.github.penfeizhou.animation.apng.decode.APNGParser;
import com.github.penfeizhou.animation.gif.GifDrawable;
import com.github.penfeizhou.animation.gif.decode.GifParser;
import java.io.File;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ExecutionException;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.net.ssl.HttpsURLConnection;
import app.fedilab.android.R;
import app.fedilab.android.activities.ContextActivity;
import app.fedilab.android.activities.HashTagActivity;
import app.fedilab.android.activities.MainActivity;
import app.fedilab.android.activities.ProfileActivity;
import app.fedilab.android.client.mastodon.entities.Account;
import app.fedilab.android.client.mastodon.entities.Attachment;
import app.fedilab.android.client.mastodon.entities.Emoji;
import app.fedilab.android.client.mastodon.entities.Field;
import app.fedilab.android.client.mastodon.entities.Mention;
import app.fedilab.android.client.mastodon.entities.Poll;
import app.fedilab.android.client.mastodon.entities.Status;
import app.fedilab.android.databinding.PopupLinksBinding;
import es.dmoral.toasty.Toasty;
public class SpannableHelper {
public static final String CLICKABLE_SPAN = "CLICKABLE_SPAN";
/**
* Convert HTML content to text. Also, it handles click on link and transform emoji
* This needs to be run asynchronously
*
* @param context {@link Context}
* @param status {@link Status} - Status concerned by the spannable transformation
* @param text String - text to convert, it can be content, spoiler, poll items, etc.
* @return Spannable string
*/
private static Spannable convert(@NonNull Context context, @NonNull Status status, @NonNull String text) {
SpannableString initialContent;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N)
initialContent = new SpannableString(Html.fromHtml(text, Html.FROM_HTML_MODE_LEGACY));
else
initialContent = new SpannableString(Html.fromHtml(text));
SpannableStringBuilder content = new SpannableStringBuilder(initialContent);
URLSpan[] urls = content.getSpans(0, (content.length() - 1), URLSpan.class);
for (URLSpan span : urls)
content.removeSpan(span);
//--- EMOJI ----
SharedPreferences sharedpreferences = PreferenceManager.getDefaultSharedPreferences(context);
boolean disableGif = sharedpreferences.getBoolean(context.getString(R.string.SET_DISABLE_GIF), false);
List<Emoji> emojiList = status.reblog != null ? status.reblog.emojis : status.emojis;
//Will convert emoji if asked
if (emojiList != null && emojiList.size() > 0) {
for (Emoji emoji : emojiList) {
if (Helper.isValidContextForGlide(context)) {
FutureTarget<File> futureTarget = Glide.with(context)
.asFile()
.load(disableGif ? emoji.static_url : emoji.url)
.submit();
try {
File file = futureTarget.get();
final String targetedEmoji = ":" + emoji.shortcode + ":";
if (content.toString().contains(targetedEmoji)) {
//emojis can be used several times so we have to loop
for (int startPosition = -1; (startPosition = content.toString().indexOf(targetedEmoji, startPosition + 1)) != -1; startPosition++) {
final int endPosition = startPosition + targetedEmoji.length();
if (endPosition <= content.toString().length() && endPosition >= startPosition) {
ImageSpan imageSpan;
if (APNGParser.isAPNG(file.getAbsolutePath())) {
APNGDrawable apngDrawable = APNGDrawable.fromFile(file.getAbsolutePath());
try {
apngDrawable.setBounds(0, 0, (int) convertDpToPixel(20, context), (int) convertDpToPixel(20, context));
apngDrawable.setVisible(true, true);
imageSpan = new ImageSpan(apngDrawable);
content.setSpan(
imageSpan, startPosition,
endPosition, Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
} catch (Exception ignored) {
}
} else if (GifParser.isGif(file.getAbsolutePath())) {
GifDrawable gifDrawable = GifDrawable.fromFile(file.getAbsolutePath());
try {
gifDrawable.setBounds(0, 0, (int) convertDpToPixel(20, context), (int) convertDpToPixel(20, context));
gifDrawable.setVisible(true, true);
imageSpan = new ImageSpan(gifDrawable);
content.setSpan(
imageSpan, startPosition,
endPosition, Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
} catch (Exception ignored) {
}
} else {
Drawable drawable = Drawable.createFromPath(file.getAbsolutePath());
try {
drawable.setBounds(0, 0, (int) convertDpToPixel(20, context), (int) convertDpToPixel(20, context));
drawable.setVisible(true, true);
imageSpan = new ImageSpan(drawable);
content.setSpan(
imageSpan, startPosition,
endPosition, Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
} catch (Exception ignored) {
}
}
}
}
}
} catch (ExecutionException | InterruptedException e) {
e.printStackTrace();
}
}
}
}
//--- URLs ----
Matcher matcherALink = Patterns.WEB_URL.matcher(content);
int offSetTruncate = 0;
while (matcherALink.find()) {
int matchStart = matcherALink.start() - offSetTruncate;
int matchEnd = matchStart + matcherALink.group().length();
if (matchEnd > content.toString().length()) {
matchEnd = content.toString().length();
}
if (content.toString().length() < matchEnd || matchStart < 0) {
continue;
}
final String url = content.toString().substring(matchStart, matchEnd);
String newURL = Helper.transformURL(context, url);
//If URL has been transformed
if (newURL.compareTo(url) != 0) {
content.replace(matchStart, matchEnd, newURL);
offSetTruncate += (newURL.length() - url.length());
matchEnd = matchStart + newURL.length();
}
//Truncate URL if needed
//TODO: add an option to disable truncated URLs
String urlText = newURL;
if (newURL.length() > 30) {
urlText = urlText.substring(0, 30);
urlText += "";
content.replace(matchStart, matchEnd, urlText);
matchEnd = matchStart + 31;
offSetTruncate += (newURL.length() - urlText.length());
}
if (!urlText.startsWith("http")) {
continue;
}
if (matchEnd <= content.length() && matchEnd >= matchStart) {
content.setSpan(new LongClickableSpan() {
@Override
public void onLongClick(View view) {
AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(view.getContext(), Helper.dialogStyle());
PopupLinksBinding popupLinksBinding = PopupLinksBinding.inflate(LayoutInflater.from(context));
dialogBuilder.setView(popupLinksBinding.getRoot());
AlertDialog alertDialog = dialogBuilder.create();
alertDialog.show();
popupLinksBinding.displayFullLink.setOnClickListener(v -> {
AlertDialog.Builder builder = new AlertDialog.Builder(context, Helper.dialogStyle());
builder.setMessage(url);
builder.setTitle(context.getString(R.string.display_full_link));
builder.setPositiveButton(R.string.close, (dialog, which) -> dialog.dismiss())
.show();
alertDialog.dismiss();
});
popupLinksBinding.shareLink.setOnClickListener(v -> {
Intent sendIntent = new Intent(Intent.ACTION_SEND);
sendIntent.putExtra(Intent.EXTRA_SUBJECT, context.getString(R.string.shared_via));
sendIntent.putExtra(Intent.EXTRA_TEXT, url);
sendIntent.setType("text/plain");
sendIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
Intent intentChooser = Intent.createChooser(sendIntent, context.getString(R.string.share_with));
intentChooser.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(intentChooser);
alertDialog.dismiss();
});
popupLinksBinding.openOtherApp.setOnClickListener(v -> {
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setData(Uri.parse(url));
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
try {
context.startActivity(intent);
} catch (Exception e) {
Toasty.error(context, context.getString(R.string.toast_error), Toast.LENGTH_LONG).show();
}
alertDialog.dismiss();
});
popupLinksBinding.copyLink.setOnClickListener(v -> {
ClipboardManager clipboard = (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE);
ClipData clip = ClipData.newPlainText(Helper.CLIP_BOARD, url);
if (clipboard != null) {
clipboard.setPrimaryClip(clip);
Toasty.info(context, context.getString(R.string.clipboard_url), Toast.LENGTH_LONG).show();
}
alertDialog.dismiss();
});
popupLinksBinding.checkRedirect.setOnClickListener(v -> {
try {
URL finalUrlCheck = new URL(url);
new Thread(() -> {
try {
String redirect = null;
HttpsURLConnection httpsURLConnection = (HttpsURLConnection) finalUrlCheck.openConnection();
httpsURLConnection.setConnectTimeout(10 * 1000);
httpsURLConnection.setRequestProperty("http.keepAlive", "false");
httpsURLConnection.setRequestProperty("User-Agent", USER_AGENT);
httpsURLConnection.setRequestMethod("HEAD");
if (httpsURLConnection.getResponseCode() == 301 || httpsURLConnection.getResponseCode() == 302) {
Map<String, List<String>> map = httpsURLConnection.getHeaderFields();
for (Map.Entry<String, List<String>> entry : map.entrySet()) {
if (entry.toString().toLowerCase().startsWith("location")) {
Matcher matcher = urlPattern.matcher(entry.toString());
if (matcher.find()) {
redirect = matcher.group(1);
}
}
}
}
httpsURLConnection.getInputStream().close();
if (redirect != null && redirect.compareTo(url) != 0) {
URL redirectURL = new URL(redirect);
String host = redirectURL.getHost();
String protocol = redirectURL.getProtocol();
if (protocol == null || host == null) {
redirect = null;
}
}
Handler mainHandler = new Handler(context.getMainLooper());
String finalRedirect = redirect;
Runnable myRunnable = () -> {
AlertDialog.Builder builder1 = new AlertDialog.Builder(view.getContext(), Helper.dialogStyle());
if (finalRedirect != null) {
builder1.setMessage(context.getString(R.string.redirect_detected, url, finalRedirect));
builder1.setNegativeButton(R.string.copy_link, (dialog, which) -> {
ClipboardManager clipboard1 = (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE);
ClipData clip1 = ClipData.newPlainText(Helper.CLIP_BOARD, finalRedirect);
if (clipboard1 != null) {
clipboard1.setPrimaryClip(clip1);
Toasty.info(context, context.getString(R.string.clipboard_url), Toast.LENGTH_LONG).show();
}
dialog.dismiss();
});
builder1.setNeutralButton(R.string.share_link, (dialog, which) -> {
Intent sendIntent1 = new Intent(Intent.ACTION_SEND);
sendIntent1.putExtra(Intent.EXTRA_SUBJECT, context.getString(R.string.shared_via));
sendIntent1.putExtra(Intent.EXTRA_TEXT, url);
sendIntent1.setType("text/plain");
context.startActivity(Intent.createChooser(sendIntent1, context.getString(R.string.share_with)));
dialog.dismiss();
});
} else {
builder1.setMessage(R.string.no_redirect);
}
builder1.setTitle(context.getString(R.string.check_redirect));
builder1.setPositiveButton(R.string.close, (dialog, which) -> dialog.dismiss())
.show();
};
mainHandler.post(myRunnable);
} catch (IOException e) {
e.printStackTrace();
}
}).start();
} catch (MalformedURLException e) {
e.printStackTrace();
}
alertDialog.dismiss();
});
}
@Override
public void onClick(@NonNull View textView) {
textView.setTag(CLICKABLE_SPAN);
Pattern link = Pattern.compile("https?://([\\da-z.-]+\\.[a-z.]{2,10})/(@[\\w._-]*[0-9]*)(/[0-9]+)?$");
Matcher matcherLink = link.matcher(url);
if (matcherLink.find() && !url.contains("medium.com")) {
if (matcherLink.group(3) != null && Objects.requireNonNull(matcherLink.group(3)).length() > 0) { //It's a toot
CrossActionHelper.fetchRemoteStatus(context, MainActivity.accountWeakReference.get(), url, new CrossActionHelper.Callback() {
@Override
public void federatedStatus(Status status) {
Intent intent = new Intent(context, ContextActivity.class);
intent.putExtra(Helper.ARG_STATUS, status);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(intent);
}
@Override
public void federatedAccount(Account account) {
}
});
} else {//It's an account
CrossActionHelper.fetchRemoteAccount(context, MainActivity.accountWeakReference.get(), status.account, new CrossActionHelper.Callback() {
@Override
public void federatedStatus(Status status) {
}
@Override
public void federatedAccount(Account account) {
Intent intent = new Intent(context, ProfileActivity.class);
Bundle b = new Bundle();
b.putSerializable(Helper.ARG_ACCOUNT, account);
intent.putExtras(b);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(intent);
}
});
}
} else {
Helper.openBrowser(context, newURL);
}
}
@Override
public void updateDrawState(@NonNull TextPaint ds) {
super.updateDrawState(ds);
ds.setUnderlineText(false);
ds.setColor(linkColor);
}
}, matchStart, matchEnd, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
}
}
// --- For all patterns defined in Helper class ---
for (Map.Entry<Helper.PatternType, Pattern> entry : Helper.patternHashMap.entrySet()) {
Helper.PatternType patternType = entry.getKey();
Pattern pattern = entry.getValue();
Matcher matcher = pattern.matcher(content);
while (matcher.find()) {
int matchStart = matcher.start();
int matchEnd = matcher.end();
String word = content.toString().substring(matchStart, matchEnd);
if (matchStart >= 0 && matchEnd <= content.toString().length() && matchEnd >= matchStart) {
URLSpan[] span = content.getSpans(matchStart, matchEnd, URLSpan.class);
content.removeSpan(span);
content.setSpan(new ClickableSpan() {
@Override
public void onClick(@NonNull View textView) {
textView.setTag(CLICKABLE_SPAN);
switch (patternType) {
case TAG:
Intent intent = new Intent(context, HashTagActivity.class);
Bundle b = new Bundle();
b.putString(Helper.ARG_SEARCH_KEYWORD, word.trim());
intent.putExtras(b);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(intent);
break;
case GROUP:
break;
case MENTION:
intent = new Intent(context, ProfileActivity.class);
b = new Bundle();
Mention targetedMention = null;
HashMap<String, Integer> countUsername = new HashMap<>();
for (Mention mention : status.mentions) {
Integer count = countUsername.get(mention.username);
if (count == null) {
count = 0;
}
if (countUsername.containsKey(mention.username)) {
countUsername.put(mention.username, count + 1);
} else {
countUsername.put(mention.username, 1);
}
}
for (Mention mention : status.mentions) {
Integer count = countUsername.get(mention.username);
if (count == null) {
count = 0;
}
if (word.trim().compareToIgnoreCase("@" + mention.username) == 0 && count == 1) {
targetedMention = mention;
break;
}
}
if (targetedMention != null) {
b.putString(Helper.ARG_USER_ID, targetedMention.id);
} else {
b.putString(Helper.ARG_MENTION, word.trim());
}
intent.putExtras(b);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(intent);
break;
case MENTION_LONG:
intent = new Intent(context, ProfileActivity.class);
b = new Bundle();
targetedMention = null;
for (Mention mention : status.mentions) {
if (word.trim().substring(1).compareToIgnoreCase("@" + mention.acct) == 0) {
targetedMention = mention;
break;
}
}
if (targetedMention != null) {
b.putString(Helper.ARG_USER_ID, targetedMention.id);
} else {
b.putString(Helper.ARG_MENTION, word.trim());
}
intent.putExtras(b);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(intent);
break;
}
}
@Override
public void updateDrawState(@NonNull TextPaint ds) {
super.updateDrawState(ds);
ds.setUnderlineText(false);
ds.setColor(linkColor);
}
}, matchStart, matchEnd, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
}
}
}
Matcher matcher = Helper.ouichesPattern.matcher(content);
while (matcher.find()) {
Attachment attachment = new Attachment();
attachment.type = "audio";
String tag = matcher.group(1);
attachment.id = tag;
if (tag == null) {
continue;
}
attachment.remote_url = "http://ouich.es/mp3/" + tag + ".mp3";
attachment.url = "http://ouich.es/mp3/" + tag + ".mp3";
if (status.media_attachments == null) {
status.media_attachments = new ArrayList<>();
}
boolean alreadyAdded = false;
for (Attachment at : status.media_attachments) {
if (tag.compareTo(at.id) == 0) {
alreadyAdded = true;
break;
}
}
if (!alreadyAdded) {
status.media_attachments.add(attachment);
}
}
return trimSpannable(new SpannableStringBuilder(content));
}
/**
* Remove extra carriage returns at the bottom due to <p> tags in toots
*
* @param spannable SpannableStringBuilder
* @return SpannableStringBuilder
*/
private static SpannableStringBuilder trimSpannable(SpannableStringBuilder spannable) {
int trimStart = 0;
int trimEnd = 0;
String text = spannable.toString();
while (text.length() > 0 && text.startsWith("\n")) {
text = text.substring(1);
trimStart += 1;
}
while (text.length() > 0 && text.endsWith("\n")) {
text = text.substring(0, text.length() - 1);
trimEnd += 1;
}
return spannable.delete(0, trimStart).delete(spannable.length() - trimEnd, spannable.length());
}
public static List<Status> convertStatus(Context context, List<Status> statuses) {
if (statuses != null) {
for (Status status : statuses) {
convertStatus(context, status);
}
}
return statuses;
}
public static Status convertStatus(Context context, Status status) {
if (status != null) {
status.span_content = SpannableHelper.convert(context, status, status.content);
status.span_spoiler_text = SpannableHelper.convert(context, status, status.spoiler_text);
if (status.translationContent != null) {
status.span_translate = SpannableHelper.convert(context, status, status.translationContent);
}
status.account.span_display_name = SpannableHelper.convertA(context, status.account, status.account.display_name, true);
if (status.poll != null) {
for (Poll.PollItem pollItem : status.poll.options) {
pollItem.span_title = SpannableHelper.convert(context, status, pollItem.title);
}
}
if (status.reblog != null) {
status.reblog.span_content = SpannableHelper.convert(context, status, status.reblog.content);
if (status.reblog.translationContent != null) {
status.reblog.span_translate = SpannableHelper.convert(context, status, status.reblog.translationContent);
}
status.reblog.span_spoiler_text = SpannableHelper.convert(context, status, status.reblog.spoiler_text);
status.reblog.account.span_display_name = SpannableHelper.convertA(context, status.reblog.account, status.reblog.account.display_name, true);
if (status.reblog.poll != null) {
for (Poll.PollItem pollItem : status.reblog.poll.options) {
pollItem.span_title = SpannableHelper.convert(context, status, pollItem.title);
}
}
}
}
return status;
}
public static List<Account> convertAccounts(Context context, List<Account> accounts) {
if (accounts != null) {
for (Account account : accounts) {
convertAccount(context, account);
}
}
return accounts;
}
public static Account convertAccount(Context context, Account account) {
if (account != null) {
account.span_display_name = SpannableHelper.convertA(context, account, account.display_name, true);
account.span_note = SpannableHelper.convertA(context, account, account.note, false);
if (account.fields != null && account.fields.size() > 0) {
List<Field> fields = new ArrayList<>();
for (Field field : account.fields) {
field.value_span = SpannableHelper.convertA(context, account, field.value, false);
fields.add(field);
}
account.fields = fields;
}
}
return account;
}
/**
* Convert HTML content to text. Also, it handles click on link and transform emoji
* This needs to be run asynchronously
*
* @param context {@link Context}
* @param account {@link Account} - Account concerned by the spannable transformation
* @param text String - text to convert, it can be display name or bio
* @return Spannable string
*/
private static Spannable convertA(@NonNull Context context, @NonNull Account account, @NonNull String text, boolean limitedToDisplayName) {
SpannableString initialContent;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N)
initialContent = new SpannableString(Html.fromHtml(text, Html.FROM_HTML_MODE_LEGACY));
else
initialContent = new SpannableString(Html.fromHtml(text));
SpannableStringBuilder content = new SpannableStringBuilder(initialContent);
URLSpan[] urls = content.getSpans(0, (content.length() - 1), URLSpan.class);
for (URLSpan span : urls)
content.removeSpan(span);
//--- EMOJI ----
SharedPreferences sharedpreferences = PreferenceManager.getDefaultSharedPreferences(context);
boolean disableGif = sharedpreferences.getBoolean(context.getString(R.string.SET_DISABLE_GIF), false);
//Will convert emoji if asked
if (account.emojis != null && account.emojis.size() > 0) {
for (Emoji emoji : account.emojis) {
if (Helper.isValidContextForGlide(context)) {
FutureTarget<File> futureTarget = Glide.with(context)
.asFile()
.load(disableGif ? emoji.static_url : emoji.url)
.submit();
try {
File file = futureTarget.get();
final String targetedEmoji = ":" + emoji.shortcode + ":";
if (content.toString().contains(targetedEmoji)) {
//emojis can be used several times so we have to loop
for (int startPosition = -1; (startPosition = content.toString().indexOf(targetedEmoji, startPosition + 1)) != -1; startPosition++) {
final int endPosition = startPosition + targetedEmoji.length();
if (endPosition <= content.toString().length() && endPosition >= startPosition) {
ImageSpan imageSpan;
if (APNGParser.isAPNG(file.getAbsolutePath())) {
APNGDrawable apngDrawable = APNGDrawable.fromFile(file.getAbsolutePath());
try {
apngDrawable.setBounds(0, 0, (int) convertDpToPixel(20, context), (int) convertDpToPixel(20, context));
apngDrawable.setVisible(true, true);
imageSpan = new ImageSpan(apngDrawable);
content.setSpan(
imageSpan, startPosition,
endPosition, Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
} catch (Exception ignored) {
}
} else if (GifParser.isGif(file.getAbsolutePath())) {
GifDrawable gifDrawable = GifDrawable.fromFile(file.getAbsolutePath());
try {
gifDrawable.setBounds(0, 0, (int) convertDpToPixel(20, context), (int) convertDpToPixel(20, context));
gifDrawable.setVisible(true, true);
imageSpan = new ImageSpan(gifDrawable);
content.setSpan(
imageSpan, startPosition,
endPosition, Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
} catch (Exception ignored) {
}
} else {
Drawable drawable = Drawable.createFromPath(file.getAbsolutePath());
try {
drawable.setBounds(0, 0, (int) convertDpToPixel(20, context), (int) convertDpToPixel(20, context));
drawable.setVisible(true, true);
imageSpan = new ImageSpan(drawable);
content.setSpan(
imageSpan, startPosition,
endPosition, Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
} catch (Exception ignored) {
}
}
}
}
}
} catch (ExecutionException | InterruptedException e) {
e.printStackTrace();
}
}
}
}
if (limitedToDisplayName) {
return content;
}
//--- URLs ----
Matcher matcherALink = Patterns.WEB_URL.matcher(content);
int offSetTruncate = 0;
while (matcherALink.find()) {
int matchStart = matcherALink.start() - offSetTruncate;
int matchEnd = matchStart + matcherALink.group().length();
//Get real URL
if (matcherALink.start(1) > matcherALink.end(1) || matcherALink.end() > content.length()) {
continue;
}
final String url = content.toString().substring(matchStart, matchEnd);
//Truncate URL if needed
//TODO: add an option to disable truncated URLs
String urlText = url;
if (url.length() > 30 && matchStart < matchEnd) {
urlText = urlText.substring(0, 30);
urlText += "";
content.replace(matchStart, matchEnd, urlText);
matchEnd = matcherALink.end() - (url.length() - urlText.length());
offSetTruncate += (url.length() - urlText.length());
}
if (!urlText.startsWith("http")) {
continue;
}
if (matchStart >= 0 && matchEnd <= content.toString().length() && matchEnd >= matchStart)
content.setSpan(new ClickableSpan() {
@Override
public void onClick(@NonNull View textView) {
textView.setTag(CLICKABLE_SPAN);
Helper.openBrowser(context, url);
}
@Override
public void updateDrawState(@NonNull TextPaint ds) {
super.updateDrawState(ds);
ds.setUnderlineText(false);
ds.setColor(linkColor);
}
}, matchStart, matchEnd, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
}
// --- For all patterns defined in Helper class ---
for (Map.Entry<Helper.PatternType, Pattern> entry : Helper.patternHashMap.entrySet()) {
Helper.PatternType patternType = entry.getKey();
Pattern pattern = entry.getValue();
Matcher matcher = pattern.matcher(content);
while (matcher.find()) {
int matchStart = matcher.start();
int matchEnd = matcher.end();
if (matchStart >= 0 && matchEnd <= content.toString().length() && matchEnd >= matchStart) {
URLSpan[] span = content.getSpans(matchStart, matchEnd, URLSpan.class);
content.removeSpan(span);
String word = content.toString().substring(matchStart, matchEnd);
content.setSpan(new ClickableSpan() {
@Override
public void onClick(@NonNull View textView) {
textView.setTag(CLICKABLE_SPAN);
switch (patternType) {
case TAG:
Intent intent = new Intent(context, HashTagActivity.class);
Bundle b = new Bundle();
b.putString(Helper.ARG_SEARCH_KEYWORD, word.trim());
intent.putExtras(b);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(intent);
break;
case GROUP:
break;
case MENTION_LONG:
case MENTION:
intent = new Intent(context, ProfileActivity.class);
b = new Bundle();
b.putString(Helper.ARG_MENTION, word.trim());
intent.putExtras(b);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(intent);
break;
}
}
@Override
public void updateDrawState(@NonNull TextPaint ds) {
super.updateDrawState(ds);
ds.setUnderlineText(false);
ds.setColor(linkColor);
}
}, matchStart, matchEnd, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
}
}
}
return trimSpannable(new SpannableStringBuilder(content));
}
/**
* Makes the move to account clickable
*
* @param context Context
* @return SpannableString
*/
public static SpannableString moveToText(final Context context, Account account) {
SpannableString spannableString = null;
if (account.moved != null) {
spannableString = new SpannableString(context.getString(R.string.account_moved_to, account.acct, "@" + account.moved.acct));
int startPosition = spannableString.toString().indexOf("@" + account.moved.acct);
int endPosition = startPosition + ("@" + account.moved.acct).length();
if (startPosition >= 0 && endPosition <= spannableString.toString().length() && endPosition >= startPosition)
spannableString.setSpan(new ClickableSpan() {
@Override
public void onClick(@NonNull View textView) {
Intent intent = new Intent(context, ProfileActivity.class);
Bundle b = new Bundle();
b.putSerializable(Helper.ARG_ACCOUNT, account.moved);
intent.putExtras(b);
context.startActivity(intent);
}
@Override
public void updateDrawState(@NonNull TextPaint ds) {
super.updateDrawState(ds);
}
},
startPosition, endPosition,
Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
}
return spannableString;
}
}