Long press links

This commit is contained in:
Thomas 2022-05-14 19:24:58 +02:00
parent a8afc99401
commit 27ef0003d7
7 changed files with 475 additions and 5 deletions

View File

@ -27,13 +27,18 @@ import androidx.lifecycle.ViewModelProvider;
import androidx.lifecycle.ViewModelStoreOwner;
import androidx.preference.PreferenceManager;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
import app.fedilab.android.BaseMainActivity;
import app.fedilab.android.R;
import app.fedilab.android.activities.ComposeActivity;
import app.fedilab.android.activities.MainActivity;
import app.fedilab.android.client.entities.Account;
import app.fedilab.android.client.mastodon.MastodonSearchService;
import app.fedilab.android.client.mastodon.entities.Results;
import app.fedilab.android.client.mastodon.entities.Status;
import app.fedilab.android.exception.DBException;
import app.fedilab.android.ui.drawer.AccountsSearchAdapter;
@ -41,6 +46,11 @@ import app.fedilab.android.viewmodel.mastodon.AccountsVM;
import app.fedilab.android.viewmodel.mastodon.SearchVM;
import app.fedilab.android.viewmodel.mastodon.StatusesVM;
import es.dmoral.toasty.Toasty;
import okhttp3.OkHttpClient;
import retrofit2.Call;
import retrofit2.Response;
import retrofit2.Retrofit;
import retrofit2.converter.gson.GsonConverterFactory;
public class CrossActionHelper {
@ -235,6 +245,122 @@ public class CrossActionHelper {
}
}
private static MastodonSearchService init(Context context, @NonNull String instance) {
final OkHttpClient okHttpClient = new OkHttpClient.Builder()
.readTimeout(60, TimeUnit.SECONDS)
.connectTimeout(60, TimeUnit.SECONDS)
.proxy(Helper.getProxy(context))
.build();
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("https://" + instance + "/api/v2/")
.addConverterFactory(GsonConverterFactory.create())
.client(okHttpClient)
.build();
return retrofit.create(MastodonSearchService.class);
}
/**
* Fetch and federate the remote status
*/
public static void fetchRemoteStatus(@NonNull Context context, @NonNull Account ownerAccount, Status targetedStatus, Callback callback) {
MastodonSearchService mastodonSearchService = init(context, MainActivity.currentInstance);
new Thread(() -> {
Call<Results> resultsCall = mastodonSearchService.search(ownerAccount.token, targetedStatus.url, null, "statuses", false, true, false, 0, null, null, 1);
Results results = null;
if (resultsCall != null) {
try {
Response<Results> resultsResponse = resultsCall.execute();
if (resultsResponse.isSuccessful()) {
results = resultsResponse.body();
if (results != null) {
if (results.statuses == null) {
results.statuses = new ArrayList<>();
} else {
results.statuses = SpannableHelper.convertStatus(context, results.statuses);
}
if (results.accounts == null) {
results.accounts = new ArrayList<>();
}
if (results.hashtags == null) {
results.hashtags = new ArrayList<>();
}
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
Handler mainHandler = new Handler(Looper.getMainLooper());
Results finalResults = results;
Runnable myRunnable = () -> {
if (finalResults != null && finalResults.statuses != null && finalResults.statuses.size() > 0) {
callback.federatedStatus(finalResults.statuses.get(0));
}
};
mainHandler.post(myRunnable);
}).start();
}
/**
* Fetch and federate the remote status
*/
public static void fetchRemoteAccount(@NonNull Context context, @NonNull Account ownerAccount, app.fedilab.android.client.mastodon.entities.Account targetedAccount, Callback callback) {
MastodonSearchService mastodonSearchService = init(context, MainActivity.currentInstance);
String search;
if (targetedAccount.acct.contains("@")) { //Not from same instance
search = targetedAccount.acct;
} else {
search = targetedAccount.acct + "@" + BaseMainActivity.currentInstance;
}
new Thread(() -> {
Call<Results> resultsCall = mastodonSearchService.search(ownerAccount.token, search, null, "accounts", false, true, false, 0, null, null, 1);
Results results = null;
if (resultsCall != null) {
try {
Response<Results> resultsResponse = resultsCall.execute();
if (resultsResponse.isSuccessful()) {
results = resultsResponse.body();
if (results != null) {
if (results.statuses == null) {
results.statuses = new ArrayList<>();
} else {
results.statuses = SpannableHelper.convertStatus(context, results.statuses);
}
if (results.accounts == null) {
results.accounts = new ArrayList<>();
}
if (results.hashtags == null) {
results.hashtags = new ArrayList<>();
}
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
Handler mainHandler = new Handler(Looper.getMainLooper());
Results finalResults = results;
Runnable myRunnable = () -> {
if (finalResults != null && finalResults.accounts != null && finalResults.accounts.size() > 0) {
callback.federatedAccount(finalResults.accounts.get(0));
}
};
mainHandler.post(myRunnable);
}).start();
}
public interface Callback {
void federatedStatus(Status status);
void federatedAccount(app.fedilab.android.client.mastodon.entities.Account account);
}
public enum TypeOfCrossAction {
FOLLOW_ACTION,
UNFOLLOW_ACTION,

View File

@ -270,7 +270,10 @@ public class Helper {
public static final Pattern mediumPattern = Pattern.compile("([\\w@-]*)?\\.?medium.com/@?([/\\w-]+)");
public static final Pattern wikipediaPattern = Pattern.compile("([\\w_-]+)\\.wikipedia.org/(((?!([\"'<])).)*)");
public static final Pattern codePattern = Pattern.compile("code=([\\w-]+)");
public static final Pattern urlPattern = Pattern.compile(
"(?i)\\b((?:[a-z][\\w-]+:(?:/{1,3}|[a-z0-9%])|www\\d{0,3}[.]|[a-z0-9.\\-]+[.][a-z]{2,10}/)(?:[^\\s()<>]+|\\(([^\\s()<>]+|(\\([^\\s()<>]+\\)))*\\))+(?:\\(([^\\s()<>]+|(\\([^\\s()<>]+\\)))*\\)|[^\\s`!()\\[\\]{};:'\".,<>?«»“”‘’]))",
Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL);
/*
* List from ClearUrls
* https://gitlab.com/KevinRoebert/ClearUrls/blob/master/data/data.min.json#L106

View File

@ -0,0 +1,82 @@
package app.fedilab.android.helper;
import android.os.Handler;
import android.text.Layout;
import android.text.Selection;
import android.text.Spannable;
import android.text.method.LinkMovementMethod;
import android.text.method.MovementMethod;
import android.view.MotionEvent;
import android.widget.TextView;
//https://stackoverflow.com/a/20435892
public class LongClickLinkMovementMethod extends LinkMovementMethod {
private static LongClickLinkMovementMethod sInstance;
private Handler mLongClickHandler;
private boolean mIsLongPressed = false;
public static MovementMethod getInstance() {
if (sInstance == null) {
sInstance = new LongClickLinkMovementMethod();
sInstance.mLongClickHandler = new Handler();
}
return sInstance;
}
@Override
public boolean onTouchEvent(final TextView widget, Spannable buffer,
MotionEvent event) {
int action = event.getAction();
if (action == MotionEvent.ACTION_CANCEL) {
if (mLongClickHandler != null) {
mLongClickHandler.removeCallbacksAndMessages(null);
}
}
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);
final LongClickableSpan[] link = buffer.getSpans(off, off, LongClickableSpan.class);
if (link.length != 0) {
if (action == MotionEvent.ACTION_UP) {
if (mLongClickHandler != null) {
mLongClickHandler.removeCallbacksAndMessages(null);
}
if (!mIsLongPressed) {
link[0].onClick(widget);
}
mIsLongPressed = false;
} else {
Selection.setSelection(buffer,
buffer.getSpanStart(link[0]),
buffer.getSpanEnd(link[0]));
int LONG_CLICK_TIME = 1000;
mLongClickHandler.postDelayed(() -> {
link[0].onLongClick(widget);
mIsLongPressed = true;
widget.invalidate();
}, LONG_CLICK_TIME);
}
return true;
}
}
return super.onTouchEvent(widget, buffer, event);
}
}

View File

@ -0,0 +1,10 @@
package app.fedilab.android.helper;
import android.text.style.ClickableSpan;
import android.view.View;
public abstract class LongClickableSpan extends ClickableSpan {
abstract public void onLongClick(View view);
}

View File

@ -15,15 +15,21 @@ package app.fedilab.android.helper;
* 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;
@ -34,9 +40,12 @@ 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;
@ -47,15 +56,23 @@ 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.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;
@ -64,6 +81,8 @@ 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 {
@ -188,11 +207,172 @@ public class SpannableHelper {
}
if (matchStart >= 0 && matchEnd <= content.length() && matchEnd >= matchStart) {
content.setSpan(new ClickableSpan() {
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");
context.startActivity(Intent.createChooser(sendIntent, context.getString(R.string.share_with)));
alertDialog.dismiss();
});
popupLinksBinding.openOtherApp.setOnClickListener(v -> {
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setData(Uri.parse(url));
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(context, 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);
Helper.openBrowser(context, newURL);
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(), status, new CrossActionHelper.Callback() {
@Override
public void federatedStatus(Status status) {
Intent intent = new Intent(context, ContextActivity.class);
intent.putExtra(Helper.ARG_STATUS, status);
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);
context.startActivity(intent);
}
});
}
} else {
Helper.openBrowser(context, newURL);
}
}
@Override

View File

@ -42,7 +42,6 @@ import android.text.Layout;
import android.text.SpannableString;
import android.text.Spanned;
import android.text.TextUtils;
import android.text.method.LinkMovementMethod;
import android.text.style.ForegroundColorSpan;
import android.util.TypedValue;
import android.view.ContextThemeWrapper;
@ -116,6 +115,7 @@ import app.fedilab.android.databinding.LayoutMediaBinding;
import app.fedilab.android.databinding.LayoutPollItemBinding;
import app.fedilab.android.helper.CrossActionHelper;
import app.fedilab.android.helper.Helper;
import app.fedilab.android.helper.LongClickLinkMovementMethod;
import app.fedilab.android.helper.MastodonHelper;
import app.fedilab.android.helper.SpannableHelper;
import app.fedilab.android.helper.ThemeHelper;
@ -940,8 +940,8 @@ public class StatusAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
holder.binding.mediaContainer.setVisibility(View.GONE);
holder.binding.attachmentsListContainer.setVisibility(View.GONE);
}
holder.binding.statusContent.setMovementMethod(LinkMovementMethod.getInstance());
holder.binding.statusContent.setMovementMethod(LongClickLinkMovementMethod.getInstance());
//holder.binding.statusContent.setMovementMethod(LinkMovementMethod.getInstance());
holder.binding.reblogInfo.setOnClickListener(v -> {
if (remote) {
Toasty.info(context, context.getString(R.string.retrieve_remote_status), Toasty.LENGTH_SHORT).show();

View File

@ -0,0 +1,69 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="@dimen/fab_margin"
android:orientation="vertical"
android:padding="@dimen/fab_margin">
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/display_full_link"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:layout_marginBottom="10dp"
android:drawableEnd="@drawable/ic_baseline_navigate_next_24"
android:text="@string/display_full_link"
android:textColor="@color/cyanea_accent_reference"
android:textSize="16sp"
app:drawableTint="@color/cyanea_accent_reference" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/share_link"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:layout_marginBottom="10dp"
android:drawableEnd="@drawable/ic_baseline_navigate_next_24"
android:text="@string/share_link"
android:textColor="@color/cyanea_accent_reference"
android:textSize="16sp"
app:drawableTint="@color/cyanea_accent_reference" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/open_other_app"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:layout_marginBottom="10dp"
android:drawableEnd="@drawable/ic_baseline_navigate_next_24"
android:text="@string/open_other_app"
android:textColor="@color/cyanea_accent_reference"
android:textSize="16sp"
app:drawableTint="@color/cyanea_accent_reference" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/copy_link"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:layout_marginBottom="10dp"
android:drawableEnd="@drawable/ic_baseline_navigate_next_24"
android:text="@string/copy_link"
android:textColor="@color/cyanea_accent_reference"
android:textSize="16sp"
app:drawableTint="@color/cyanea_accent_reference" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/check_redirect"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:layout_marginBottom="10dp"
android:drawableEnd="@drawable/ic_baseline_navigate_next_24"
android:text="@string/check_redirect"
android:textColor="@color/cyanea_accent_reference"
android:textSize="16sp"
app:drawableTint="@color/cyanea_accent_reference" />
</LinearLayout>