Lightning browser with I2P configuration
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

374 lines
15 KiB

package acr.browser.lightning.view;
import android.annotation.TargetApi;
import android.app.Activity;
import android.app.Dialog;
import android.content.ActivityNotFoundException;
import android.content.DialogInterface;
import android.content.Intent;
import android.graphics.Bitmap;
import android.net.MailTo;
import android.net.http.SslError;
import android.os.Build;
import android.os.Message;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v7.app.AlertDialog;
import android.text.InputType;
import android.text.method.PasswordTransformationMethod;
import android.util.Log;
import android.webkit.HttpAuthHandler;
import android.webkit.SslErrorHandler;
import android.webkit.ValueCallback;
import android.webkit.WebResourceRequest;
import android.webkit.WebResourceResponse;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import android.widget.EditText;
import android.widget.LinearLayout;
import java.io.ByteArrayInputStream;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import javax.inject.Inject;
import acr.browser.lightning.R;
import acr.browser.lightning.app.BrowserApp;
import acr.browser.lightning.constant.Constants;
import acr.browser.lightning.controller.UIController;
import acr.browser.lightning.dialog.BrowserDialog;
import acr.browser.lightning.utils.AdBlock;
import acr.browser.lightning.utils.IntentUtils;
import acr.browser.lightning.utils.Preconditions;
import acr.browser.lightning.utils.ProxyUtils;
import acr.browser.lightning.utils.Utils;
public class LightningWebClient extends WebViewClient {
@NonNull private final Activity mActivity;
@NonNull private final LightningView mLightningView;
@NonNull private final UIController mUIController;
@NonNull private final IntentUtils mIntentUtils;
@Inject ProxyUtils mProxyUtils;
@Inject AdBlock mAdBlock;
LightningWebClient(@NonNull Activity activity, @NonNull LightningView lightningView) {
BrowserApp.getAppComponent().inject(this);
Preconditions.checkNonNull(activity);
Preconditions.checkNonNull(lightningView);
mActivity = activity;
mUIController = (UIController) activity;
mLightningView = lightningView;
mAdBlock.updatePreference();
mIntentUtils = new IntentUtils(activity);
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
@Override
public WebResourceResponse shouldInterceptRequest(WebView view, @NonNull WebResourceRequest request) {
if (mAdBlock.isAd(request.getUrl().toString())) {
ByteArrayInputStream EMPTY = new ByteArrayInputStream("".getBytes());
return new WebResourceResponse("text/plain", "utf-8", EMPTY);
}
return super.shouldInterceptRequest(view, request);
}
@Nullable
@SuppressWarnings("deprecation")
@TargetApi(Build.VERSION_CODES.KITKAT_WATCH)
@Override
public WebResourceResponse shouldInterceptRequest(WebView view, String url) {
if (mAdBlock.isAd(url)) {
ByteArrayInputStream EMPTY = new ByteArrayInputStream("".getBytes());
return new WebResourceResponse("text/plain", "utf-8", EMPTY);
}
return null;
}
@TargetApi(Build.VERSION_CODES.KITKAT)
@Override
public void onPageFinished(@NonNull WebView view, String url) {
if (view.isShown()) {
mUIController.updateUrl(url, true);
mUIController.setBackButtonEnabled(view.canGoBack());
mUIController.setForwardButtonEnabled(view.canGoForward());
view.postInvalidate();
}
if (view.getTitle() == null || view.getTitle().isEmpty()) {
mLightningView.getTitleInfo().setTitle(mActivity.getString(R.string.untitled));
} else {
mLightningView.getTitleInfo().setTitle(view.getTitle());
}
if (Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.KITKAT &&
mLightningView.getInvertePage()) {
view.evaluateJavascript(Constants.JAVASCRIPT_INVERT_PAGE, null);
}
mUIController.tabChanged(mLightningView);
}
@Override
public void onPageStarted(WebView view, String url, Bitmap favicon) {
mLightningView.getTitleInfo().setFavicon(null);
if (mLightningView.isShown()) {
mUIController.updateUrl(url, false);
mUIController.showActionBar();
}
mUIController.tabChanged(mLightningView);
}
@Override
public void onReceivedHttpAuthRequest(final WebView view, @NonNull final HttpAuthHandler handler,
final String host, final String realm) {
AlertDialog.Builder builder = new AlertDialog.Builder(mActivity);
final EditText name = new EditText(mActivity);
final EditText password = new EditText(mActivity);
LinearLayout passLayout = new LinearLayout(mActivity);
passLayout.setOrientation(LinearLayout.VERTICAL);
passLayout.addView(name);
passLayout.addView(password);
name.setHint(mActivity.getString(R.string.hint_username));
name.setSingleLine();
password.setInputType(InputType.TYPE_TEXT_VARIATION_PASSWORD);
password.setSingleLine();
password.setTransformationMethod(new PasswordTransformationMethod());
password.setHint(mActivity.getString(R.string.hint_password));
builder.setTitle(mActivity.getString(R.string.title_sign_in));
builder.setView(passLayout);
builder.setCancelable(true)
.setPositiveButton(mActivity.getString(R.string.title_sign_in),
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int id) {
String user = name.getText().toString();
String pass = password.getText().toString();
handler.proceed(user.trim(), pass.trim());
Log.d(Constants.TAG, "Request Login");
}
})
.setNegativeButton(mActivity.getString(R.string.action_cancel),
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int id) {
handler.cancel();
}
});
AlertDialog alert = builder.create();
alert.show();
BrowserDialog.setDialogSize(mActivity, alert);
}
private volatile boolean mIsRunning = false;
private float mZoomScale = 0.0f;
@TargetApi(Build.VERSION_CODES.KITKAT)
@Override
public void onScaleChanged(@NonNull final WebView view, final float oldScale, final float newScale) {
if (view.isShown() && mLightningView.mPreferences.getTextReflowEnabled() &&
Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.KITKAT) {
if (mIsRunning)
return;
float changeInPercent = Math.abs(100 - 100 / mZoomScale * newScale);
if (changeInPercent > 2.5f && !mIsRunning) {
mIsRunning = view.postDelayed(new Runnable() {
@Override
public void run() {
mZoomScale = newScale;
view.evaluateJavascript(Constants.JAVASCRIPT_TEXT_REFLOW, new ValueCallback<String>() {
@Override
public void onReceiveValue(String value) {
mIsRunning = false;
}
});
}
}, 100);
}
}
}
@NonNull
private static List<Integer> getAllSslErrorMessageCodes(@NonNull SslError error) {
List<Integer> errorCodeMessageCodes = new ArrayList<>(1);
if (error.hasError(SslError.SSL_DATE_INVALID)) {
errorCodeMessageCodes.add(R.string.message_certificate_date_invalid);
}
if (error.hasError(SslError.SSL_EXPIRED)) {
errorCodeMessageCodes.add(R.string.message_certificate_expired);
}
if (error.hasError(SslError.SSL_IDMISMATCH)) {
errorCodeMessageCodes.add(R.string.message_certificate_domain_mismatch);
}
if (error.hasError(SslError.SSL_NOTYETVALID)) {
errorCodeMessageCodes.add(R.string.message_certificate_not_yet_valid);
}
if (error.hasError(SslError.SSL_UNTRUSTED)) {
errorCodeMessageCodes.add(R.string.message_certificate_untrusted);
}
if (error.hasError(SslError.SSL_INVALID)) {
errorCodeMessageCodes.add(R.string.message_certificate_invalid);
}
return errorCodeMessageCodes;
}
@Override
public void onReceivedSslError(WebView view, @NonNull final SslErrorHandler handler, @NonNull SslError error) {
List<Integer> errorCodeMessageCodes = getAllSslErrorMessageCodes(error);
StringBuilder stringBuilder = new StringBuilder();
for (Integer messageCode : errorCodeMessageCodes) {
stringBuilder.append(" - ").append(mActivity.getString(messageCode)).append('\n');
}
String alertMessage =
mActivity.getString(R.string.message_insecure_connection, stringBuilder.toString());
AlertDialog.Builder builder = new AlertDialog.Builder(mActivity);
builder.setTitle(mActivity.getString(R.string.title_warning));
builder.setMessage(alertMessage)
.setCancelable(true)
.setPositiveButton(mActivity.getString(R.string.action_yes),
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int id) {
handler.proceed();
}
})
.setNegativeButton(mActivity.getString(R.string.action_no),
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int id) {
handler.cancel();
}
});
Dialog dialog = builder.show();
BrowserDialog.setDialogSize(mActivity, dialog);
}
@Override
public void onFormResubmission(WebView view, @NonNull final Message dontResend, @NonNull final Message resend) {
AlertDialog.Builder builder = new AlertDialog.Builder(mActivity);
builder.setTitle(mActivity.getString(R.string.title_form_resubmission));
builder.setMessage(mActivity.getString(R.string.message_form_resubmission))
.setCancelable(true)
.setPositiveButton(mActivity.getString(R.string.action_yes),
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int id) {
resend.sendToTarget();
}
})
.setNegativeButton(mActivity.getString(R.string.action_no),
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int id) {
dontResend.sendToTarget();
}
});
AlertDialog alert = builder.create();
alert.show();
BrowserDialog.setDialogSize(mActivity, alert);
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
@Override
public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) {
return shouldOverrideLoading(view, request.getUrl().toString()) || super.shouldOverrideUrlLoading(view, request);
}
@SuppressWarnings("deprecation")
@Override
public boolean shouldOverrideUrlLoading(@NonNull WebView view, @NonNull String url) {
return shouldOverrideLoading(view, url) || super.shouldOverrideUrlLoading(view, url);
}
private boolean shouldOverrideLoading(WebView view, String url) {
// Check if configured proxy is available
if (!mProxyUtils.isProxyReady(mActivity)) {
// User has been notified
return true;
}
Map<String, String> headers = mLightningView.getRequestHeaders();
// If the headers are empty, the user has not expressed the desire
// to use them and therefore we can revert back to the old way of loading
if (headers.isEmpty()) {
if (mLightningView.isIncognito()) {
// If we are in incognito, immediately load, we don't want the url to leave the app
return false;
}
if (url.startsWith(Constants.ABOUT)) {
// If this is an about page, immediately load, we don't need to leave the app
return false;
}
if (isMailOrIntent(url, view) || mIntentUtils.startActivityForUrl(view, url)) {
// If it was a mailto: link, or an intent, or could be launched elsewhere, do that
return true;
}
} else {
if (mLightningView.isIncognito() && Utils.doesSupportHeaders()) {
// If we are in incognito, immediately load, we don't want the url to leave the app
view.loadUrl(url, headers);
return true;
}
if (url.startsWith(Constants.ABOUT) && Utils.doesSupportHeaders()) {
// If this is an about page, immediately load, we don't need to leave the app
view.loadUrl(url, headers);
return true;
}
if (isMailOrIntent(url, view) || mIntentUtils.startActivityForUrl(view, url)) {
// If it was a mailto: link, or an intent, or could be launched elsewhere, do that
return true;
} else if (Utils.doesSupportHeaders()) {
// Otherwise, load the headers.
view.loadUrl(url, headers);
return true;
}
}
// If none of those instances was true, revert back to the old way of loading
return false;
}
private boolean isMailOrIntent(@NonNull String url, @NonNull WebView view) {
if (url.startsWith("mailto:")) {
MailTo mailTo = MailTo.parse(url);
Intent i = Utils.newEmailIntent(mailTo.getTo(), mailTo.getSubject(),
mailTo.getBody(), mailTo.getCc());
mActivity.startActivity(i);
view.reload();
return true;
} else if (url.startsWith("intent://")) {
Intent intent;
try {
intent = Intent.parseUri(url, Intent.URI_INTENT_SCHEME);
} catch (URISyntaxException ignored) {
intent = null;
}
if (intent != null) {
intent.addCategory(Intent.CATEGORY_BROWSABLE);
intent.setComponent(null);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1) {
intent.setSelector(null);
}
try {
mActivity.startActivity(intent);
} catch (ActivityNotFoundException e) {
Log.e(Constants.TAG, "ActivityNotFoundException");
}
return true;
}
}
return false;
}
}