2019-02-11 14:43:57 +01:00
|
|
|
/* Copyright 2017 Andrew Dawson
|
|
|
|
*
|
|
|
|
* This file is a part of Tusky.
|
|
|
|
*
|
|
|
|
* 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.
|
|
|
|
*
|
|
|
|
* Tusky 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 Tusky; if not,
|
|
|
|
* see <http://www.gnu.org/licenses>. */
|
|
|
|
|
|
|
|
package com.keylesspalace.tusky.util;
|
|
|
|
|
|
|
|
import android.content.ActivityNotFoundException;
|
|
|
|
import android.content.Context;
|
|
|
|
import android.content.Intent;
|
|
|
|
import android.net.Uri;
|
|
|
|
import android.text.SpannableStringBuilder;
|
|
|
|
import android.text.Spanned;
|
|
|
|
import android.text.method.LinkMovementMethod;
|
|
|
|
import android.text.style.ClickableSpan;
|
|
|
|
import android.text.style.URLSpan;
|
|
|
|
import android.util.Log;
|
|
|
|
import android.view.View;
|
|
|
|
import android.widget.TextView;
|
|
|
|
|
2019-10-22 21:18:20 +02:00
|
|
|
import androidx.annotation.NonNull;
|
|
|
|
import androidx.annotation.Nullable;
|
2021-01-25 16:23:32 +01:00
|
|
|
import androidx.browser.customtabs.CustomTabColorSchemeParams;
|
2019-10-22 21:18:20 +02:00
|
|
|
import androidx.browser.customtabs.CustomTabsIntent;
|
|
|
|
import androidx.preference.PreferenceManager;
|
|
|
|
|
2019-03-30 15:18:40 +01:00
|
|
|
import com.keylesspalace.tusky.R;
|
2019-02-11 14:43:57 +01:00
|
|
|
import com.keylesspalace.tusky.entity.Status;
|
|
|
|
import com.keylesspalace.tusky.interfaces.LinkListener;
|
|
|
|
|
|
|
|
import java.net.URI;
|
|
|
|
import java.net.URISyntaxException;
|
|
|
|
|
|
|
|
public class LinkHelper {
|
2019-06-11 15:56:27 +02:00
|
|
|
public static String getDomain(String urlString) {
|
2019-02-11 14:43:57 +01:00
|
|
|
URI uri;
|
|
|
|
try {
|
|
|
|
uri = new URI(urlString);
|
|
|
|
} catch (URISyntaxException e) {
|
|
|
|
return "";
|
|
|
|
}
|
|
|
|
String host = uri.getHost();
|
2019-07-02 10:55:02 +02:00
|
|
|
if(host == null) {
|
|
|
|
return "";
|
|
|
|
} else if (host.startsWith("www.")) {
|
2019-02-11 14:43:57 +01:00
|
|
|
return host.substring(4);
|
|
|
|
} else {
|
|
|
|
return host;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Finds links, mentions, and hashtags in a piece of text and makes them clickable, associating
|
|
|
|
* them with callbacks to notify when they're clicked.
|
|
|
|
*
|
|
|
|
* @param view the returned text will be put in
|
|
|
|
* @param content containing text with mentions, links, or hashtags
|
|
|
|
* @param mentions any '@' mentions which are known to be in the content
|
|
|
|
* @param listener to notify about particular spans that are clicked
|
|
|
|
*/
|
2020-05-15 22:09:12 +02:00
|
|
|
public static void setClickableText(TextView view, CharSequence content,
|
2019-12-20 20:08:02 +01:00
|
|
|
@Nullable Status.Mention[] mentions, final LinkListener listener) {
|
2020-05-15 22:09:12 +02:00
|
|
|
SpannableStringBuilder builder = SpannableStringBuilder.valueOf(content);
|
|
|
|
URLSpan[] urlSpans = builder.getSpans(0, content.length(), URLSpan.class);
|
2019-02-11 14:43:57 +01:00
|
|
|
for (URLSpan span : urlSpans) {
|
|
|
|
int start = builder.getSpanStart(span);
|
|
|
|
int end = builder.getSpanEnd(span);
|
|
|
|
int flags = builder.getSpanFlags(span);
|
|
|
|
CharSequence text = builder.subSequence(start, end);
|
|
|
|
ClickableSpan customSpan = null;
|
|
|
|
|
|
|
|
if (text.charAt(0) == '#') {
|
|
|
|
final String tag = text.subSequence(1, text.length()).toString();
|
|
|
|
customSpan = new ClickableSpanNoUnderline() {
|
|
|
|
@Override
|
2019-07-25 12:16:19 +02:00
|
|
|
public void onClick(@NonNull View widget) { listener.onViewTag(tag); }
|
2019-02-11 14:43:57 +01:00
|
|
|
};
|
|
|
|
} else if (text.charAt(0) == '@' && mentions != null && mentions.length > 0) {
|
|
|
|
String accountUsername = text.subSequence(1, text.length()).toString();
|
|
|
|
/* There may be multiple matches for users on different instances with the same
|
|
|
|
* username. If a match has the same domain we know it's for sure the same, but if
|
|
|
|
* that can't be found then just go with whichever one matched last. */
|
|
|
|
String id = null;
|
|
|
|
for (Status.Mention mention : mentions) {
|
|
|
|
if (mention.getLocalUsername().equalsIgnoreCase(accountUsername)) {
|
|
|
|
id = mention.getId();
|
|
|
|
if (mention.getUrl().contains(getDomain(span.getURL()))) {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (id != null) {
|
|
|
|
final String accountId = id;
|
|
|
|
customSpan = new ClickableSpanNoUnderline() {
|
|
|
|
@Override
|
2019-07-25 12:16:19 +02:00
|
|
|
public void onClick(@NonNull View widget) { listener.onViewAccount(accountId); }
|
2019-02-11 14:43:57 +01:00
|
|
|
};
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (customSpan == null) {
|
|
|
|
customSpan = new CustomURLSpan(span.getURL()) {
|
|
|
|
@Override
|
|
|
|
public void onClick(View widget) {
|
|
|
|
listener.onViewUrl(getURL());
|
|
|
|
}
|
|
|
|
};
|
|
|
|
}
|
|
|
|
builder.removeSpan(span);
|
|
|
|
builder.setSpan(customSpan, start, end, flags);
|
|
|
|
|
|
|
|
/* Add zero-width space after links in end of line to fix its too large hitbox.
|
|
|
|
* See also : https://github.com/tuskyapp/Tusky/issues/846
|
|
|
|
* https://github.com/tuskyapp/Tusky/pull/916 */
|
|
|
|
if (end >= builder.length() ||
|
|
|
|
builder.subSequence(end, end + 1).toString().equals("\n")){
|
|
|
|
builder.insert(end, "\u200B");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
view.setText(builder);
|
|
|
|
view.setMovementMethod(LinkMovementMethod.getInstance());
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Put mentions in a piece of text and makes them clickable, associating them with callbacks to
|
|
|
|
* notify when they're clicked.
|
|
|
|
*
|
|
|
|
* @param view the returned text will be put in
|
|
|
|
* @param mentions any '@' mentions which are known to be in the content
|
|
|
|
* @param listener to notify about particular spans that are clicked
|
|
|
|
*/
|
|
|
|
public static void setClickableMentions(
|
|
|
|
TextView view, @Nullable Status.Mention[] mentions, final LinkListener listener) {
|
|
|
|
if (mentions == null || mentions.length == 0) {
|
|
|
|
view.setText(null);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
SpannableStringBuilder builder = new SpannableStringBuilder();
|
|
|
|
int start = 0;
|
|
|
|
int end = 0;
|
|
|
|
int flags;
|
|
|
|
boolean firstMention = true;
|
|
|
|
for (Status.Mention mention : mentions) {
|
|
|
|
String accountUsername = mention.getLocalUsername();
|
|
|
|
final String accountId = mention.getId();
|
|
|
|
ClickableSpan customSpan = new ClickableSpanNoUnderline() {
|
|
|
|
@Override
|
2019-07-25 12:16:19 +02:00
|
|
|
public void onClick(@NonNull View widget) { listener.onViewAccount(accountId); }
|
2019-02-11 14:43:57 +01:00
|
|
|
};
|
|
|
|
|
|
|
|
end += 1 + accountUsername.length(); // length of @ + username
|
|
|
|
flags = builder.getSpanFlags(customSpan);
|
|
|
|
if (firstMention) {
|
|
|
|
firstMention = false;
|
|
|
|
} else {
|
|
|
|
builder.append(" ");
|
|
|
|
start += 1;
|
|
|
|
end += 1;
|
|
|
|
}
|
|
|
|
builder.append("@");
|
|
|
|
builder.append(accountUsername);
|
|
|
|
builder.setSpan(customSpan, start, end, flags);
|
|
|
|
builder.append("\u200B"); // same reasonning than in setClickableText
|
|
|
|
end += 1; // shift position to take the previous character into account
|
|
|
|
start = end;
|
|
|
|
}
|
|
|
|
view.setText(builder);
|
2019-07-25 12:16:19 +02:00
|
|
|
view.setMovementMethod(LinkMovementMethod.getInstance());
|
2019-02-11 14:43:57 +01:00
|
|
|
}
|
|
|
|
|
2019-12-21 18:56:16 +01:00
|
|
|
public static CharSequence createClickableText(String text, String link) {
|
|
|
|
URLSpan span = new CustomURLSpan(link);
|
|
|
|
|
|
|
|
SpannableStringBuilder clickableText = new SpannableStringBuilder(text);
|
|
|
|
clickableText.setSpan(span, 0, text.length(), Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
|
|
|
|
return clickableText;
|
|
|
|
}
|
|
|
|
|
2019-02-11 14:43:57 +01:00
|
|
|
/**
|
|
|
|
* Opens a link, depending on the settings, either in the browser or in a custom tab
|
|
|
|
*
|
|
|
|
* @param url a string containing the url to open
|
|
|
|
* @param context context
|
|
|
|
*/
|
|
|
|
public static void openLink(String url, Context context) {
|
|
|
|
Uri uri = Uri.parse(url).normalizeScheme();
|
|
|
|
|
|
|
|
boolean useCustomTabs = PreferenceManager.getDefaultSharedPreferences(context)
|
|
|
|
.getBoolean("customTabs", false);
|
|
|
|
if (useCustomTabs) {
|
|
|
|
openLinkInCustomTab(uri, context);
|
|
|
|
} else {
|
|
|
|
openLinkInBrowser(uri, context);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* opens a link in the browser via Intent.ACTION_VIEW
|
|
|
|
*
|
|
|
|
* @param uri the uri to open
|
|
|
|
* @param context context
|
|
|
|
*/
|
|
|
|
public static void openLinkInBrowser(Uri uri, Context context) {
|
|
|
|
Intent intent = new Intent(Intent.ACTION_VIEW, uri);
|
|
|
|
try {
|
|
|
|
context.startActivity(intent);
|
|
|
|
} catch (ActivityNotFoundException e) {
|
2019-07-27 21:53:28 +02:00
|
|
|
Log.w("LinkHelper", "Actvity was not found for intent, " + intent);
|
2019-02-11 14:43:57 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* tries to open a link in a custom tab
|
|
|
|
* falls back to browser if not possible
|
|
|
|
*
|
|
|
|
* @param uri the uri to open
|
|
|
|
* @param context context
|
|
|
|
*/
|
|
|
|
public static void openLinkInCustomTab(Uri uri, Context context) {
|
2020-01-30 21:37:28 +01:00
|
|
|
int toolbarColor = ThemeUtils.getColor(context, R.attr.colorSurface);
|
2021-01-25 16:23:32 +01:00
|
|
|
int navigationbarColor = ThemeUtils.getColor(context, android.R.attr.navigationBarColor);
|
|
|
|
int navigationbarDividerColor = ThemeUtils.getColor(context, R.attr.dividerColor);
|
2019-02-11 14:43:57 +01:00
|
|
|
|
2021-01-25 16:23:32 +01:00
|
|
|
CustomTabColorSchemeParams colorSchemeParams = new CustomTabColorSchemeParams.Builder()
|
2019-03-30 15:18:40 +01:00
|
|
|
.setToolbarColor(toolbarColor)
|
2021-01-25 16:23:32 +01:00
|
|
|
.setNavigationBarColor(navigationbarColor)
|
|
|
|
.setNavigationBarDividerColor(navigationbarDividerColor)
|
|
|
|
.build();
|
2019-12-20 20:08:02 +01:00
|
|
|
|
2021-01-25 16:23:32 +01:00
|
|
|
CustomTabsIntent customTabsIntent = new CustomTabsIntent.Builder()
|
|
|
|
.setDefaultColorSchemeParams(colorSchemeParams)
|
|
|
|
.setShowTitle(true)
|
|
|
|
.build();
|
2019-12-20 20:08:02 +01:00
|
|
|
|
2019-02-11 14:43:57 +01:00
|
|
|
try {
|
2019-03-30 15:18:40 +01:00
|
|
|
customTabsIntent.launchUrl(context, uri);
|
2019-02-11 14:43:57 +01:00
|
|
|
} catch (ActivityNotFoundException e) {
|
2019-07-27 21:53:28 +02:00
|
|
|
Log.w("LinkHelper", "Activity was not found for intent " + customTabsIntent);
|
2019-03-30 15:18:40 +01:00
|
|
|
openLinkInBrowser(uri, context);
|
2019-02-11 14:43:57 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|