FairEmail/app/src/main/java/eu/faircode/email/FragmentDialogOpenLink.java

561 lines
23 KiB
Java

package eu.faircode.email;
/*
This file is part of FairEmail.
FairEmail 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.
FairEmail 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 FairEmail. If not, see <http://www.gnu.org/licenses/>.
Copyright 2018-2022 by Marcel Bokhorst (M66B)
*/
import android.app.Dialog;
import android.content.ActivityNotFoundException;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.SharedPreferences;
import android.graphics.Paint;
import android.graphics.Typeface;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.provider.Settings;
import android.text.Editable;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.TextUtils;
import android.text.TextWatcher;
import android.text.method.LinkMovementMethod;
import android.text.style.ForegroundColorSpan;
import android.text.style.StyleSpan;
import android.util.Pair;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.View;
import android.view.inputmethod.EditorInfo;
import android.widget.Button;
import android.widget.CheckBox;
import android.widget.CompoundButton;
import android.widget.EditText;
import android.widget.ImageButton;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.constraintlayout.widget.Group;
import androidx.core.net.MailTo;
import androidx.core.util.PatternsCompat;
import androidx.lifecycle.Lifecycle;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import androidx.preference.PreferenceManager;
import java.net.IDN;
import java.net.InetAddress;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class FragmentDialogOpenLink extends FragmentDialogBase {
private ImageButton ibMore;
private TextView tvMore;
private Button btnOwner;
private ContentLoadingProgressBar pbWait;
private TextView tvOwnerRemark;
private TextView tvHost;
private TextView tvOwner;
private Group grpOwner;
private Button btnSettings;
private Button btnDefault;
private TextView tvReset;
@NonNull
@Override
public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
Uri _uri = getArguments().getParcelable("uri");
String _title = getArguments().getString("title");
if (_title != null)
_title = _title.replace("\uFFFC", ""); // Object replacement character
if (TextUtils.isEmpty(_title))
_title = null;
final String title = _title;
final Context context = getContext();
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
boolean check_links_dbl = prefs.getBoolean("check_links_dbl", BuildConfig.PLAY_STORE_RELEASE);
boolean disconnect_links = prefs.getBoolean("disconnect_links", true);
// Preload web view
Helper.customTabsWarmup(context);
final Uri uri = UriHelper.guessScheme(_uri);
// Process link
final Uri sanitized;
if (uri.isOpaque())
sanitized = uri;
else {
Uri s = UriHelper.sanitize(uri);
sanitized = (s == null ? uri : s);
}
// Process title
final Uri uriTitle;
if (title != null && PatternsCompat.WEB_URL.matcher(title).matches()) {
String t = title.replaceAll("\\s+", "");
Uri u = Uri.parse(title.contains("://") ? t : "http://" + t);
String host = u.getHost(); // Capture1.PNG
uriTitle = (UriHelper.hasTld(context, host) ? u : null);
} else
uriTitle = null;
// Get views
final View dview = LayoutInflater.from(context).inflate(R.layout.dialog_open_link, null);
final ImageButton ibInfo = dview.findViewById(R.id.ibInfo);
final TextView tvTitle = dview.findViewById(R.id.tvTitle);
final ImageButton ibDifferent = dview.findViewById(R.id.ibDifferent);
final EditText etLink = dview.findViewById(R.id.etLink);
final TextView tvLink = dview.findViewById(R.id.tvLink);
final ImageButton ibSearch = dview.findViewById(R.id.ibSearch);
final ImageButton ibShare = dview.findViewById(R.id.ibShare);
final ImageButton ibCopy = dview.findViewById(R.id.ibCopy);
final TextView tvSuspicious = dview.findViewById(R.id.tvSuspicious);
final TextView tvDisconnect = dview.findViewById(R.id.tvDisconnect);
final TextView tvDisconnectCategories = dview.findViewById(R.id.tvDisconnectCategories);
final CheckBox cbSecure = dview.findViewById(R.id.cbSecure);
final CheckBox cbSanitize = dview.findViewById(R.id.cbSanitize);
final CheckBox cbNotAgain = dview.findViewById(R.id.cbNotAgain);
ibMore = dview.findViewById(R.id.ibMore);
tvMore = dview.findViewById(R.id.tvMore);
btnOwner = dview.findViewById(R.id.btnOwner);
pbWait = dview.findViewById(R.id.pbWait);
tvOwnerRemark = dview.findViewById(R.id.tvOwnerRemark);
tvHost = dview.findViewById(R.id.tvHost);
tvOwner = dview.findViewById(R.id.tvOwner);
grpOwner = dview.findViewById(R.id.grpOwner);
btnSettings = dview.findViewById(R.id.btnSettings);
btnDefault = dview.findViewById(R.id.btnDefault);
tvReset = dview.findViewById(R.id.tvReset);
final Group grpDifferent = dview.findViewById(R.id.grpDifferent);
// Wire
ibInfo.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Helper.viewFAQ(v.getContext(), 35);
}
});
ibDifferent.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
etLink.setText(format(uriTitle, context));
}
});
etLink.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {
}
@Override
public void onTextChanged(CharSequence text, int i, int i1, int i2) {
}
@Override
public void afterTextChanged(Editable editable) {
if (cbSecure == null)
return;
Uri uri = Uri.parse(editable.toString());
boolean secure = UriHelper.isSecure(uri);
boolean hyperlink = UriHelper.isHyperLink(uri);
cbSecure.setTag(secure);
cbSecure.setChecked(secure);
cbSecure.setText(
secure ? R.string.title_link_https : R.string.title_link_http);
cbSecure.setTextColor(Helper.resolveColor(context,
secure ? android.R.attr.textColorSecondary : R.attr.colorWarning));
cbSecure.setTypeface(
secure ? Typeface.DEFAULT : Typeface.DEFAULT_BOLD);
cbSecure.setVisibility(hyperlink ? View.VISIBLE : View.GONE);
}
});
etLink.setHorizontallyScrolling(false);
etLink.setMaxLines(10);
etLink.setOnEditorActionListener(new TextView.OnEditorActionListener() {
@Override
public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
if (actionId == EditorInfo.IME_ACTION_DONE) {
((AlertDialog) getDialog()).getButton(DialogInterface.BUTTON_POSITIVE).performClick();
return true;
} else
return false;
}
});
MailTo mailto = null;
if ("mailto".equals(uri.getScheme()))
try {
mailto = MailTo.parse(uri);
} catch (Throwable ex) {
Log.w(ex);
}
ibSearch.setVisibility(
mailto != null && !TextUtils.isEmpty(mailto.getTo())
? View.VISIBLE : View.GONE);
ibSearch.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
dismiss();
LocalBroadcastManager lbm = LocalBroadcastManager.getInstance(context);
lbm.sendBroadcast(
new Intent(ActivityView.ACTION_SEARCH_ADDRESS)
.putExtra("account", -1L)
.putExtra("folder", -1L)
.putExtra("query", MailTo.parse(uri).getTo()));
}
});
ibShare.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent send = new Intent();
send.setAction(Intent.ACTION_SEND);
send.putExtra(Intent.EXTRA_TEXT, etLink.getText().toString());
send.setType("text/plain");
startActivity(Intent.createChooser(send, title));
}
});
ibCopy.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
ClipboardManager clipboard = Helper.getSystemService(context, ClipboardManager.class);
if (clipboard == null)
return;
ClipData clip = ClipData.newPlainText(title, etLink.getText().toString());
clipboard.setPrimaryClip(clip);
ToastEx.makeText(context, R.string.title_clipboard_copied, Toast.LENGTH_LONG).show();
}
});
cbSecure.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton compoundButton, boolean checked) {
boolean tag = (Boolean) compoundButton.getTag();
if (tag == checked)
return;
Uri uri = Uri.parse(etLink.getText().toString());
etLink.setText(format(UriHelper.secure(uri, checked), context));
}
});
cbSanitize.setVisibility(uri.equals(sanitized) ? View.GONE : View.VISIBLE);
cbSanitize.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton compoundButton, boolean checked) {
Uri link = (checked ? sanitized : uri);
boolean secure = UriHelper.isSecure(link);
cbSecure.setTag(secure);
cbSecure.setChecked(secure);
etLink.setText(format(UriHelper.secure(link, secure), context));
}
});
cbNotAgain.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
prefs.edit().putBoolean(uri.getHost() + ".confirm_link", !isChecked).apply();
}
});
View.OnClickListener onMore = new View.OnClickListener() {
@Override
public void onClick(View v) {
setMore(btnOwner.getVisibility() == View.GONE);
}
};
ibMore.setOnClickListener(onMore);
tvMore.setOnClickListener(onMore);
btnOwner.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Bundle args = new Bundle();
args.putParcelable("uri", uri);
new SimpleTask<Pair<InetAddress, IPInfo.Organization>>() {
@Override
protected void onPreExecute(Bundle args) {
ibMore.setEnabled(false);
tvMore.setEnabled(false);
btnOwner.setEnabled(false);
pbWait.setVisibility(View.VISIBLE);
grpOwner.setVisibility(View.GONE);
}
@Override
protected void onPostExecute(Bundle args) {
ibMore.setEnabled(true);
tvMore.setEnabled(true);
btnOwner.setEnabled(true);
pbWait.setVisibility(View.GONE);
grpOwner.setVisibility(View.VISIBLE);
}
@Override
protected Pair<InetAddress, IPInfo.Organization> onExecute(Context context, Bundle args) throws Throwable {
Uri uri = args.getParcelable("uri");
return IPInfo.getOrganization(uri, context);
}
@Override
protected void onExecuted(Bundle args, Pair<InetAddress, IPInfo.Organization> data) {
tvHost.setText(data.first.toString());
tvOwner.setText(data.second.name == null ? "?" : data.second.name);
ApplicationEx.getMainHandler().post(new Runnable() {
@Override
public void run() {
if (!getLifecycle().getCurrentState().isAtLeast(Lifecycle.State.STARTED))
return;
dview.scrollTo(0, tvOwner.getBottom());
}
});
}
@Override
protected void onException(Bundle args, Throwable ex) {
tvHost.setText(ex.getClass().getName());
tvOwner.setText(ex.getMessage());
}
}.execute(FragmentDialogOpenLink.this, args, "link:owner");
}
});
tvOwnerRemark.setMovementMethod(LinkMovementMethod.getInstance());
btnSettings.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
v.getContext().startActivity(new Intent(v.getContext(), ActivitySetup.class)
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK)
.putExtra("tab", "privacy"));
}
});
final Intent manage = new Intent(Settings.ACTION_MANAGE_DEFAULT_APPS_SETTINGS);
btnDefault.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
v.getContext().startActivity(manage);
}
});
tvReset.setPaintFlags(tvReset.getPaintFlags() | Paint.UNDERLINE_TEXT_FLAG);
tvReset.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View c) {
Helper.view(c.getContext(), Uri.parse(Helper.URI_SUPPORT_RESET_OPEN), true);
}
});
// Initialize
tvTitle.setText(title);
tvTitle.setVisibility(TextUtils.isEmpty(title) ? View.GONE : View.VISIBLE);
String host = uri.getHost();
String thost = (uriTitle == null ? null : uriTitle.getHost());
String puny = null;
try {
if (host != null)
puny = IDN.toASCII(host, IDN.ALLOW_UNASSIGNED);
} catch (Throwable ex) {
Log.i(ex);
puny = host;
}
if (host != null && !host.equals(puny)) {
etLink.setText(format(uri.buildUpon().encodedAuthority(puny).build(), context));
tvLink.setText(uri.toString());
tvSuspicious.setVisibility(View.GONE);
} else {
etLink.setText(format(uri, context));
tvLink.setText(null);
tvSuspicious.setVisibility(Helper.isSingleScript(host) ? View.GONE : View.VISIBLE);
}
if (check_links_dbl &&
tvSuspicious.getVisibility() != View.VISIBLE) {
Bundle args = new Bundle();
args.putParcelable("uri", uri);
new SimpleTask<Boolean>() {
@Override
protected Boolean onExecute(Context context, Bundle args) throws Throwable {
Uri uri = args.getParcelable("uri");
return DnsBlockList.isJunk(context, uri);
}
@Override
protected void onExecuted(Bundle args, Boolean blocklist) {
if (blocklist != null && blocklist)
tvSuspicious.setVisibility(View.VISIBLE);
}
@Override
protected void onException(Bundle args, Throwable ex) {
// Ignored
}
}.execute(this, args, "link:blocklist");
}
grpDifferent.setVisibility(
host == null || thost == null || host.equalsIgnoreCase(thost)
? View.GONE : View.VISIBLE);
List<String> categories = null;
if (disconnect_links)
categories = DisconnectBlacklist.getCategories(uri.getHost());
if (categories != null)
tvDisconnectCategories.setText(TextUtils.join(", ", categories));
tvDisconnect.setVisibility(
categories == null ? View.GONE : View.VISIBLE);
tvDisconnectCategories.setVisibility(
categories == null || !BuildConfig.DEBUG ? View.GONE : View.VISIBLE);
cbNotAgain.setText(context.getString(R.string.title_no_ask_for_again, uri.getHost()));
cbNotAgain.setVisibility(
"https".equals(uri.getScheme()) && !TextUtils.isEmpty(uri.getHost())
? View.VISIBLE : View.GONE);
setMore(false);
return new AlertDialog.Builder(context)
.setView(dview)
.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
Uri uri = Uri.parse(etLink.getText().toString());
Helper.view(context, uri, false);
}
})
.setNeutralButton(R.string.title_browse, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
// https://developer.android.com/training/basics/intents/sending#AppChooser
Uri uri = Uri.parse(etLink.getText().toString());
Intent view = new Intent(Intent.ACTION_VIEW, uri);
Intent chooser = Intent.createChooser(view, context.getString(R.string.title_select_app));
try {
startActivity(chooser);
} catch (ActivityNotFoundException ex) {
Log.w(ex);
Helper.view(context, uri, true, true);
}
}
})
.setNegativeButton(android.R.string.cancel, null)
.create();
}
private void setMore(boolean show) {
boolean n = (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N);
ibMore.setImageLevel(show ? 0 : 1);
btnOwner.setVisibility(show ? View.VISIBLE : View.GONE);
pbWait.setVisibility(View.GONE);
tvOwnerRemark.setVisibility(show ? View.VISIBLE : View.GONE);
grpOwner.setVisibility(View.GONE);
btnSettings.setVisibility(show ? View.VISIBLE : View.GONE);
btnDefault.setVisibility(show && n ? View.VISIBLE : View.GONE);
tvReset.setVisibility(show ? View.VISIBLE : View.GONE);
}
private Spanned format(Uri uri, Context context) {
String scheme = uri.getScheme();
String host = uri.getHost();
String text = uri.toString();
SpannableStringBuilder ssb = new SpannableStringBuilderEx(text);
try {
int textColorLink = Helper.resolveColor(context, android.R.attr.textColorLink);
int textColorSecondary = Helper.resolveColor(context, android.R.attr.textColorSecondary);
int colorWarning = Helper.resolveColor(context, R.attr.colorWarning);
if ("tel".equals(scheme)) {
// tel://+123%2045%20678%123456
host = Uri.decode(host);
text = "tel://" + host;
} else if ("mailto".equals(scheme)) {
if (host == null) {
MailTo email = MailTo.parse(uri);
host = UriHelper.getEmailDomain(email.getTo());
}
}
if (scheme != null) {
int index = text.indexOf(scheme);
if (index >= 0)
if ("http".equals(scheme)) {
ssb.setSpan(new ForegroundColorSpan(colorWarning),
index, index + scheme.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
ssb.setSpan(new StyleSpan(Typeface.BOLD),
index, index + scheme.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
} else
ssb.setSpan(new ForegroundColorSpan(textColorSecondary),
index, index + scheme.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
if (host != null) {
int index = text.indexOf(host);
if (index >= 0)
ssb.setSpan(new ForegroundColorSpan(textColorLink),
index, index + host.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
if (uri.isHierarchical())
for (String name : uri.getQueryParameterNames()) {
Pattern pattern = Pattern.compile("[?&]" + Pattern.quote(name) + "=");
Matcher matcher = pattern.matcher(text);
while (matcher.find()) {
ssb.setSpan(new ForegroundColorSpan(textColorLink),
matcher.start() + 1, matcher.end() - 1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
}
} catch (Throwable ex) {
Log.e(ex);
}
return ssb;
}
}