
If a background tab opened a URL that triggered the intent chooser, and the user chose to open the URL in the browser, the current tab would open the URL instead of the originating URL. Using the tab’s hashcode, the original tab now will open the URL.
1229 lines
41 KiB
Java
1229 lines
41 KiB
Java
/*
|
|
* Copyright 2014 A.C.R. Development
|
|
*/
|
|
|
|
package acr.browser.lightning.view;
|
|
|
|
import android.annotation.SuppressLint;
|
|
import android.app.Activity;
|
|
import android.content.Context;
|
|
import android.graphics.Bitmap;
|
|
import android.graphics.Color;
|
|
import android.graphics.ColorMatrix;
|
|
import android.graphics.ColorMatrixColorFilter;
|
|
import android.graphics.Paint;
|
|
import android.os.Build;
|
|
import android.os.Handler;
|
|
import android.os.Message;
|
|
import android.support.annotation.NonNull;
|
|
import android.support.annotation.Nullable;
|
|
import android.support.v4.util.ArrayMap;
|
|
import android.util.Log;
|
|
import android.view.GestureDetector;
|
|
import android.view.GestureDetector.SimpleOnGestureListener;
|
|
import android.view.MotionEvent;
|
|
import android.view.View;
|
|
import android.view.View.OnTouchListener;
|
|
import android.view.ViewConfiguration;
|
|
import android.view.ViewGroup;
|
|
import android.webkit.CookieManager;
|
|
import android.webkit.WebSettings;
|
|
import android.webkit.WebSettings.LayoutAlgorithm;
|
|
import android.webkit.WebSettings.PluginState;
|
|
import android.webkit.WebView;
|
|
|
|
import com.anthonycr.bonsai.Single;
|
|
import com.anthonycr.bonsai.SingleAction;
|
|
import com.anthonycr.bonsai.SingleOnSubscribe;
|
|
import com.anthonycr.bonsai.SingleSubscriber;
|
|
|
|
import java.io.File;
|
|
import java.lang.ref.WeakReference;
|
|
import java.util.Map;
|
|
|
|
import javax.inject.Inject;
|
|
|
|
import acr.browser.lightning.app.BrowserApp;
|
|
import acr.browser.lightning.constant.BookmarkPage;
|
|
import acr.browser.lightning.constant.Constants;
|
|
import acr.browser.lightning.constant.HistoryPage;
|
|
import acr.browser.lightning.constant.StartPage;
|
|
import acr.browser.lightning.controller.UIController;
|
|
import acr.browser.lightning.database.BookmarkManager;
|
|
import acr.browser.lightning.dialog.LightningDialogBuilder;
|
|
import acr.browser.lightning.download.LightningDownloadListener;
|
|
import acr.browser.lightning.preference.PreferenceManager;
|
|
|
|
import com.anthonycr.bonsai.Schedulers;
|
|
|
|
import acr.browser.lightning.utils.Preconditions;
|
|
import acr.browser.lightning.utils.ProxyUtils;
|
|
import acr.browser.lightning.utils.UrlUtils;
|
|
import acr.browser.lightning.utils.Utils;
|
|
|
|
/**
|
|
* {@link LightningView} acts as a tab for the browser,
|
|
* handling WebView creation and handling logic, as well
|
|
* as properly initialing it. All interactions with the
|
|
* WebView should be made through this class.
|
|
*/
|
|
public class LightningView {
|
|
|
|
private static final String TAG = LightningView.class.getSimpleName();
|
|
|
|
public static final String HEADER_REQUESTED_WITH = "X-Requested-With";
|
|
public static final String HEADER_WAP_PROFILE = "X-Wap-Profile";
|
|
private static final String HEADER_DNT = "DNT";
|
|
private static final int API = android.os.Build.VERSION.SDK_INT;
|
|
private static final int SCROLL_UP_THRESHOLD = Utils.dpToPx(10);
|
|
|
|
@Nullable private static String sHomepage;
|
|
@Nullable private static String sDefaultUserAgent;
|
|
private static float sMaxFling;
|
|
private static final float[] sNegativeColorArray = {
|
|
-1.0f, 0, 0, 0, 255, // red
|
|
0, -1.0f, 0, 0, 255, // green
|
|
0, 0, -1.0f, 0, 255, // blue
|
|
0, 0, 0, 1.0f, 0 // alpha
|
|
};
|
|
private static final float[] sIncreaseContrastColorArray = {
|
|
2.0f, 0, 0, 0, -160.f, // red
|
|
0, 2.0f, 0, 0, -160.f, // green
|
|
0, 0, 2.0f, 0, -160.f, // blue
|
|
0, 0, 0, 1.0f, 0 // alpha
|
|
};
|
|
|
|
@NonNull private final LightningViewTitle mTitle;
|
|
@Nullable private WebView mWebView;
|
|
@NonNull private final UIController mUIController;
|
|
@NonNull private final GestureDetector mGestureDetector;
|
|
@NonNull private final Activity mActivity;
|
|
@NonNull private final Paint mPaint = new Paint();
|
|
@Nullable private Object mTag;
|
|
private final boolean mIsIncognitoTab;
|
|
private boolean isForegroundTab;
|
|
private boolean mInvertPage = false;
|
|
private boolean mToggleDesktop = false;
|
|
@NonNull private final WebViewHandler mWebViewHandler = new WebViewHandler(this);
|
|
@NonNull private final Map<String, String> mRequestHeaders = new ArrayMap<>();
|
|
|
|
@Inject PreferenceManager mPreferences;
|
|
@Inject LightningDialogBuilder mBookmarksDialogBuilder;
|
|
@Inject ProxyUtils mProxyUtils;
|
|
@Inject BookmarkManager mBookmarkManager;
|
|
|
|
public LightningView(@NonNull Activity activity, @Nullable String url, boolean isIncognito) {
|
|
BrowserApp.getAppComponent().inject(this);
|
|
mActivity = activity;
|
|
mUIController = (UIController) activity;
|
|
mWebView = new WebView(activity);
|
|
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.JELLY_BEAN) {
|
|
mWebView.setId(View.generateViewId());
|
|
}
|
|
mIsIncognitoTab = isIncognito;
|
|
mTitle = new LightningViewTitle(activity);
|
|
|
|
sMaxFling = ViewConfiguration.get(activity).getScaledMaximumFlingVelocity();
|
|
|
|
mWebView.setDrawingCacheBackgroundColor(Color.WHITE);
|
|
mWebView.setFocusableInTouchMode(true);
|
|
mWebView.setFocusable(true);
|
|
mWebView.setDrawingCacheEnabled(false);
|
|
mWebView.setWillNotCacheDrawing(true);
|
|
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.LOLLIPOP_MR1) {
|
|
//noinspection deprecation
|
|
mWebView.setAnimationCacheEnabled(false);
|
|
//noinspection deprecation
|
|
mWebView.setAlwaysDrawnWithCacheEnabled(false);
|
|
}
|
|
mWebView.setBackgroundColor(Color.WHITE);
|
|
|
|
mWebView.setScrollbarFadingEnabled(true);
|
|
mWebView.setSaveEnabled(true);
|
|
mWebView.setNetworkAvailable(true);
|
|
mWebView.setWebChromeClient(new LightningChromeClient(activity, this));
|
|
mWebView.setWebViewClient(new LightningWebClient(activity, this));
|
|
mWebView.setDownloadListener(new LightningDownloadListener(activity));
|
|
mGestureDetector = new GestureDetector(activity, new CustomGestureListener());
|
|
mWebView.setOnTouchListener(new TouchListener());
|
|
sDefaultUserAgent = mWebView.getSettings().getUserAgentString();
|
|
initializeSettings();
|
|
initializePreferences(activity);
|
|
|
|
if (url != null) {
|
|
if (!url.trim().isEmpty()) {
|
|
mWebView.loadUrl(url, mRequestHeaders);
|
|
} else {
|
|
// don't load anything, the user is looking for a blank tab
|
|
}
|
|
} else {
|
|
loadHomepage();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Sets the tag on the object,
|
|
* a reference to this object is held
|
|
* indefinitely.
|
|
*
|
|
* @param tag the tag to set, may be null.
|
|
*/
|
|
public void setTag(@Nullable Object tag) {
|
|
mTag = tag;
|
|
}
|
|
|
|
/**
|
|
* The tag set on the object.
|
|
*
|
|
* @return the tag set on the object,
|
|
* may be null.
|
|
*/
|
|
@Nullable
|
|
public Object getTag() {
|
|
return mTag;
|
|
}
|
|
|
|
/**
|
|
* This method loads the homepage for the browser. Either
|
|
* it loads the URL stored as the homepage, or loads the
|
|
* startpage or bookmark page if either of those are set
|
|
* as the homepage.
|
|
*/
|
|
public void loadHomepage() {
|
|
if (mWebView == null) {
|
|
return;
|
|
}
|
|
|
|
Preconditions.checkNonNull(sHomepage);
|
|
switch (sHomepage) {
|
|
case Constants.SCHEME_HOMEPAGE:
|
|
loadStartpage();
|
|
break;
|
|
case Constants.SCHEME_BOOKMARKS:
|
|
loadBookmarkpage();
|
|
break;
|
|
default:
|
|
mWebView.loadUrl(sHomepage, mRequestHeaders);
|
|
break;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* This method gets the startpage URL from the {@link StartPage}
|
|
* class asynchronously and loads the URL in the WebView on the
|
|
* UI thread.
|
|
*/
|
|
private void loadStartpage() {
|
|
new StartPage().getHomepage()
|
|
.subscribeOn(Schedulers.io())
|
|
.observeOn(Schedulers.main())
|
|
.subscribe(new SingleOnSubscribe<String>() {
|
|
@Override
|
|
public void onItem(@Nullable String item) {
|
|
Preconditions.checkNonNull(item);
|
|
loadUrl(item);
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* This method gets the bookmark page URL from the {@link BookmarkPage}
|
|
* class asynchronously and loads the URL in the WebView on the
|
|
* UI thread. It also caches the default folder icon locally.
|
|
*/
|
|
public void loadBookmarkpage() {
|
|
new BookmarkPage(mActivity).getBookmarkPage()
|
|
.subscribeOn(Schedulers.io())
|
|
.observeOn(Schedulers.main())
|
|
.subscribe(new SingleOnSubscribe<String>() {
|
|
@Override
|
|
public void onItem(@Nullable String item) {
|
|
Preconditions.checkNonNull(item);
|
|
loadUrl(item);
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Initialize the preference driven settings of the WebView. This method
|
|
* must be called whenever the preferences are changed within SharedPreferences.
|
|
*
|
|
* @param context the context in which the WebView was created, it is used
|
|
* to get the default UserAgent for the WebView.
|
|
*/
|
|
@SuppressLint({"NewApi", "SetJavaScriptEnabled"})
|
|
public synchronized void initializePreferences(@NonNull Context context) {
|
|
if (mWebView == null) {
|
|
return;
|
|
}
|
|
|
|
WebSettings settings = mWebView.getSettings();
|
|
|
|
if (mPreferences.getDoNotTrackEnabled()) {
|
|
mRequestHeaders.put(HEADER_DNT, "1");
|
|
} else {
|
|
mRequestHeaders.remove(HEADER_DNT);
|
|
}
|
|
|
|
if (mPreferences.getRemoveIdentifyingHeadersEnabled()) {
|
|
mRequestHeaders.put(HEADER_REQUESTED_WITH, "");
|
|
mRequestHeaders.put(HEADER_WAP_PROFILE, "");
|
|
} else {
|
|
mRequestHeaders.remove(HEADER_REQUESTED_WITH);
|
|
mRequestHeaders.remove(HEADER_WAP_PROFILE);
|
|
}
|
|
|
|
settings.setDefaultTextEncodingName(mPreferences.getTextEncoding());
|
|
sHomepage = mPreferences.getHomepage();
|
|
setColorMode(mPreferences.getRenderingMode());
|
|
|
|
if (!mIsIncognitoTab) {
|
|
settings.setGeolocationEnabled(mPreferences.getLocationEnabled());
|
|
} else {
|
|
settings.setGeolocationEnabled(false);
|
|
}
|
|
if (API < Build.VERSION_CODES.KITKAT) {
|
|
switch (mPreferences.getFlashSupport()) {
|
|
case 0:
|
|
//noinspection deprecation
|
|
settings.setPluginState(PluginState.OFF);
|
|
break;
|
|
case 1:
|
|
//noinspection deprecation
|
|
settings.setPluginState(PluginState.ON_DEMAND);
|
|
break;
|
|
case 2:
|
|
//noinspection deprecation
|
|
settings.setPluginState(PluginState.ON);
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
setUserAgent(context, mPreferences.getUserAgentChoice());
|
|
|
|
if (mPreferences.getSavePasswordsEnabled() && !mIsIncognitoTab) {
|
|
if (API < Build.VERSION_CODES.JELLY_BEAN_MR2) {
|
|
//noinspection deprecation
|
|
settings.setSavePassword(true);
|
|
}
|
|
settings.setSaveFormData(true);
|
|
} else {
|
|
if (API < Build.VERSION_CODES.JELLY_BEAN_MR2) {
|
|
//noinspection deprecation
|
|
settings.setSavePassword(false);
|
|
}
|
|
settings.setSaveFormData(false);
|
|
}
|
|
|
|
if (mPreferences.getJavaScriptEnabled()) {
|
|
settings.setJavaScriptEnabled(true);
|
|
settings.setJavaScriptCanOpenWindowsAutomatically(true);
|
|
} else {
|
|
settings.setJavaScriptEnabled(false);
|
|
settings.setJavaScriptCanOpenWindowsAutomatically(false);
|
|
}
|
|
|
|
if (mPreferences.getTextReflowEnabled()) {
|
|
settings.setLayoutAlgorithm(LayoutAlgorithm.NARROW_COLUMNS);
|
|
if (API >= android.os.Build.VERSION_CODES.KITKAT) {
|
|
try {
|
|
settings.setLayoutAlgorithm(LayoutAlgorithm.TEXT_AUTOSIZING);
|
|
} catch (Exception e) {
|
|
// This shouldn't be necessary, but there are a number
|
|
// of KitKat devices that crash trying to set this
|
|
Log.e(TAG, "Problem setting LayoutAlgorithm to TEXT_AUTOSIZING");
|
|
}
|
|
}
|
|
} else {
|
|
settings.setLayoutAlgorithm(LayoutAlgorithm.NORMAL);
|
|
}
|
|
|
|
settings.setBlockNetworkImage(mPreferences.getBlockImagesEnabled());
|
|
if (!mIsIncognitoTab) {
|
|
settings.setSupportMultipleWindows(mPreferences.getPopupsEnabled());
|
|
} else {
|
|
settings.setSupportMultipleWindows(false);
|
|
}
|
|
settings.setUseWideViewPort(mPreferences.getUseWideViewportEnabled());
|
|
settings.setLoadWithOverviewMode(mPreferences.getOverviewModeEnabled());
|
|
switch (mPreferences.getTextSize()) {
|
|
case 0:
|
|
settings.setTextZoom(200);
|
|
break;
|
|
case 1:
|
|
settings.setTextZoom(150);
|
|
break;
|
|
case 2:
|
|
settings.setTextZoom(125);
|
|
break;
|
|
case 3:
|
|
settings.setTextZoom(100);
|
|
break;
|
|
case 4:
|
|
settings.setTextZoom(75);
|
|
break;
|
|
case 5:
|
|
settings.setTextZoom(50);
|
|
break;
|
|
}
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
|
CookieManager.getInstance().setAcceptThirdPartyCookies(mWebView,
|
|
!mPreferences.getBlockThirdPartyCookiesEnabled());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Initialize the settings of the WebView that are intrinsic to Lightning and cannot
|
|
* be altered by the user. Distinguish between Incognito and Regular tabs here.
|
|
*/
|
|
@SuppressLint("NewApi")
|
|
private void initializeSettings() {
|
|
if (mWebView == null) {
|
|
return;
|
|
}
|
|
final WebSettings settings = mWebView.getSettings();
|
|
if (API < Build.VERSION_CODES.JELLY_BEAN_MR2) {
|
|
//noinspection deprecation
|
|
settings.setAppCacheMaxSize(Long.MAX_VALUE);
|
|
}
|
|
if (API < Build.VERSION_CODES.JELLY_BEAN_MR1) {
|
|
//noinspection deprecation
|
|
settings.setEnableSmoothTransition(true);
|
|
}
|
|
if (API > Build.VERSION_CODES.JELLY_BEAN) {
|
|
settings.setMediaPlaybackRequiresUserGesture(true);
|
|
}
|
|
if (API >= Build.VERSION_CODES.LOLLIPOP && !mIsIncognitoTab) {
|
|
settings.setMixedContentMode(WebSettings.MIXED_CONTENT_COMPATIBILITY_MODE);
|
|
} else if (API >= Build.VERSION_CODES.LOLLIPOP) {
|
|
// We're in Incognito mode, reject
|
|
settings.setMixedContentMode(WebSettings.MIXED_CONTENT_NEVER_ALLOW);
|
|
}
|
|
if (!mIsIncognitoTab) {
|
|
settings.setDomStorageEnabled(true);
|
|
settings.setAppCacheEnabled(true);
|
|
settings.setCacheMode(WebSettings.LOAD_DEFAULT);
|
|
settings.setDatabaseEnabled(true);
|
|
} else {
|
|
settings.setDomStorageEnabled(false);
|
|
settings.setAppCacheEnabled(false);
|
|
settings.setDatabaseEnabled(false);
|
|
settings.setCacheMode(WebSettings.LOAD_NO_CACHE);
|
|
}
|
|
settings.setSupportZoom(true);
|
|
settings.setBuiltInZoomControls(true);
|
|
settings.setDisplayZoomControls(false);
|
|
settings.setAllowContentAccess(true);
|
|
settings.setAllowFileAccess(true);
|
|
if (API >= Build.VERSION_CODES.JELLY_BEAN) {
|
|
settings.setAllowFileAccessFromFileURLs(false);
|
|
settings.setAllowUniversalAccessFromFileURLs(false);
|
|
}
|
|
|
|
getPathObservable("appcache").subscribeOn(Schedulers.io())
|
|
.observeOn(Schedulers.main())
|
|
.subscribe(new SingleOnSubscribe<File>() {
|
|
@Override
|
|
public void onItem(@Nullable File item) {
|
|
Preconditions.checkNonNull(item);
|
|
settings.setAppCachePath(item.getPath());
|
|
}
|
|
});
|
|
|
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
|
|
getPathObservable("geolocation").subscribeOn(Schedulers.io())
|
|
.observeOn(Schedulers.main())
|
|
.subscribe(new SingleOnSubscribe<File>() {
|
|
@Override
|
|
public void onItem(@Nullable File item) {
|
|
Preconditions.checkNonNull(item);
|
|
//noinspection deprecation
|
|
settings.setGeolocationDatabasePath(item.getPath());
|
|
}
|
|
});
|
|
}
|
|
|
|
getPathObservable("databases").subscribeOn(Schedulers.io())
|
|
.observeOn(Schedulers.main())
|
|
.subscribe(new SingleOnSubscribe<File>() {
|
|
@Override
|
|
public void onItem(@Nullable File item) {
|
|
if (API < Build.VERSION_CODES.KITKAT) {
|
|
Preconditions.checkNonNull(item);
|
|
//noinspection deprecation
|
|
settings.setDatabasePath(item.getPath());
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void onComplete() {
|
|
}
|
|
});
|
|
|
|
}
|
|
|
|
@NonNull
|
|
private Single<File> getPathObservable(final String subFolder) {
|
|
return Single.create(new SingleAction<File>() {
|
|
@Override
|
|
public void onSubscribe(@NonNull SingleSubscriber<File> subscriber) {
|
|
File file = mActivity.getDir(subFolder, 0);
|
|
subscriber.onItem(file);
|
|
subscriber.onComplete();
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Getter for the {@link LightningViewTitle} of the
|
|
* current LightningView instance.
|
|
*
|
|
* @return a NonNull instance of LightningViewTitle
|
|
*/
|
|
@NonNull
|
|
public LightningViewTitle getTitleInfo() {
|
|
return mTitle;
|
|
}
|
|
|
|
/**
|
|
* Returns whether or not the current tab is incognito or not.
|
|
*
|
|
* @return true if this tab is incognito, false otherwise
|
|
*/
|
|
public boolean isIncognito() {
|
|
return mIsIncognitoTab;
|
|
}
|
|
|
|
/**
|
|
* This method is used to toggle the user agent between desktop
|
|
* and the current preference of the user.
|
|
*
|
|
* @param context the Context needed to set the user agent
|
|
*/
|
|
public void toggleDesktopUA(@NonNull Context context) {
|
|
if (mWebView == null)
|
|
return;
|
|
if (!mToggleDesktop)
|
|
mWebView.getSettings().setUserAgentString(Constants.DESKTOP_USER_AGENT);
|
|
else
|
|
setUserAgent(context, mPreferences.getUserAgentChoice());
|
|
mToggleDesktop = !mToggleDesktop;
|
|
}
|
|
|
|
/**
|
|
* This method sets the user agent of the current tab.
|
|
* There are four options, 1, 2, 3, 4.
|
|
* <p/>
|
|
* 1. use the default user agent
|
|
* <p/>
|
|
* 2. use the desktop user agent
|
|
* <p/>
|
|
* 3. use the mobile user agent
|
|
* <p/>
|
|
* 4. use a custom user agent, or the default user agent
|
|
* if none was set.
|
|
*
|
|
* @param context the context needed to get the default user agent.
|
|
* @param choice the choice of user agent to use, see above comments.
|
|
*/
|
|
@SuppressLint("NewApi")
|
|
private void setUserAgent(Context context, int choice) {
|
|
if (mWebView == null) return;
|
|
WebSettings settings = mWebView.getSettings();
|
|
switch (choice) {
|
|
case 1:
|
|
if (API >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
|
|
settings.setUserAgentString(WebSettings.getDefaultUserAgent(context));
|
|
} else {
|
|
settings.setUserAgentString(sDefaultUserAgent);
|
|
}
|
|
break;
|
|
case 2:
|
|
settings.setUserAgentString(Constants.DESKTOP_USER_AGENT);
|
|
break;
|
|
case 3:
|
|
settings.setUserAgentString(Constants.MOBILE_USER_AGENT);
|
|
break;
|
|
case 4:
|
|
String ua = mPreferences.getUserAgentString(sDefaultUserAgent);
|
|
if (ua == null || ua.isEmpty()) {
|
|
ua = " ";
|
|
}
|
|
settings.setUserAgentString(ua);
|
|
break;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* This method gets the additional headers that should be
|
|
* added with each request the browser makes.
|
|
*
|
|
* @return a non null Map of Strings with the additional
|
|
* request headers.
|
|
*/
|
|
@NonNull
|
|
Map<String, String> getRequestHeaders() {
|
|
return mRequestHeaders;
|
|
}
|
|
|
|
/**
|
|
* This method determines whether the current tab is visible or not.
|
|
*
|
|
* @return true if the WebView is non-null and visible, false otherwise.
|
|
*/
|
|
public boolean isShown() {
|
|
return mWebView != null && mWebView.isShown();
|
|
}
|
|
|
|
/**
|
|
* Pause the current WebView instance.
|
|
*/
|
|
public synchronized void onPause() {
|
|
if (mWebView != null) {
|
|
mWebView.onPause();
|
|
Log.d(TAG, "WebView onPause: " + mWebView.getId());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Resume the current WebView instance.
|
|
*/
|
|
public synchronized void onResume() {
|
|
if (mWebView != null) {
|
|
mWebView.onResume();
|
|
Log.d(TAG, "WebView onResume: " + mWebView.getId());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Notify the LightningView that there is low memory and
|
|
* for the WebView to free memory. Only applicable on
|
|
* pre-Lollipop devices.
|
|
*/
|
|
@Deprecated
|
|
public synchronized void freeMemory() {
|
|
if (mWebView != null && Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {
|
|
//noinspection deprecation
|
|
mWebView.freeMemory();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* This method sets the tab as the foreground tab or
|
|
* the background tab.
|
|
*
|
|
* @param isForeground true if the tab should be set as
|
|
* foreground, false otherwise.
|
|
*/
|
|
public void setForegroundTab(boolean isForeground) {
|
|
isForegroundTab = isForeground;
|
|
mUIController.tabChanged(this);
|
|
}
|
|
|
|
/**
|
|
* Determines if the tab is in the foreground or not.
|
|
*
|
|
* @return true if the tab is the foreground tab,
|
|
* false otherwise.
|
|
*/
|
|
public boolean isForegroundTab() {
|
|
return isForegroundTab;
|
|
}
|
|
|
|
/**
|
|
* Gets the current progress of the WebView.
|
|
*
|
|
* @return returns a number between 0 and 100 with
|
|
* the current progress of the WebView. If the WebView
|
|
* is null, then the progress returned will be 100.
|
|
*/
|
|
public int getProgress() {
|
|
if (mWebView != null) {
|
|
return mWebView.getProgress();
|
|
} else {
|
|
return 100;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Notify the WebView to stop the current load.
|
|
*/
|
|
public synchronized void stopLoading() {
|
|
if (mWebView != null) {
|
|
mWebView.stopLoading();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* This method forces the layer type to hardware, which
|
|
* enables hardware rendering on the WebView instance
|
|
* of the current LightningView.
|
|
*/
|
|
private void setHardwareRendering() {
|
|
if (mWebView == null) {
|
|
return;
|
|
}
|
|
mWebView.setLayerType(View.LAYER_TYPE_HARDWARE, mPaint);
|
|
}
|
|
|
|
/**
|
|
* This method sets the layer type to none, which
|
|
* means that either the GPU and CPU can both compose
|
|
* the layers when necessary.
|
|
*/
|
|
private void setNormalRendering() {
|
|
if (mWebView == null) {
|
|
return;
|
|
}
|
|
mWebView.setLayerType(View.LAYER_TYPE_NONE, null);
|
|
}
|
|
|
|
/**
|
|
* This method forces the layer type to software, which
|
|
* disables hardware rendering on the WebView instance
|
|
* of the current LightningView and makes the CPU render
|
|
* the view.
|
|
*/
|
|
public void setSoftwareRendering() {
|
|
if (mWebView == null) {
|
|
return;
|
|
}
|
|
mWebView.setLayerType(View.LAYER_TYPE_SOFTWARE, null);
|
|
}
|
|
|
|
/**
|
|
* Sets the current rendering color of the WebView instance
|
|
* of the current LightningView. The for modes are normal
|
|
* rendering (0), inverted rendering (1), grayscale rendering (2),
|
|
* and inverted grayscale rendering (3)
|
|
*
|
|
* @param mode the integer mode to set as the rendering mode.
|
|
* see the numbers in documentation above for the
|
|
* values this method accepts.
|
|
*/
|
|
private void setColorMode(int mode) {
|
|
mInvertPage = false;
|
|
switch (mode) {
|
|
case 0:
|
|
mPaint.setColorFilter(null);
|
|
// setSoftwareRendering(); // Some devices get segfaults
|
|
// in the WebView with Hardware Acceleration enabled,
|
|
// the only fix is to disable hardware rendering
|
|
setNormalRendering();
|
|
mInvertPage = false;
|
|
break;
|
|
case 1:
|
|
ColorMatrixColorFilter filterInvert = new ColorMatrixColorFilter(
|
|
sNegativeColorArray);
|
|
mPaint.setColorFilter(filterInvert);
|
|
setHardwareRendering();
|
|
|
|
mInvertPage = true;
|
|
break;
|
|
case 2:
|
|
ColorMatrix cm = new ColorMatrix();
|
|
cm.setSaturation(0);
|
|
ColorMatrixColorFilter filterGray = new ColorMatrixColorFilter(cm);
|
|
mPaint.setColorFilter(filterGray);
|
|
setHardwareRendering();
|
|
break;
|
|
case 3:
|
|
ColorMatrix matrix = new ColorMatrix();
|
|
matrix.set(sNegativeColorArray);
|
|
ColorMatrix matrixGray = new ColorMatrix();
|
|
matrixGray.setSaturation(0);
|
|
ColorMatrix concat = new ColorMatrix();
|
|
concat.setConcat(matrix, matrixGray);
|
|
ColorMatrixColorFilter filterInvertGray = new ColorMatrixColorFilter(concat);
|
|
mPaint.setColorFilter(filterInvertGray);
|
|
setHardwareRendering();
|
|
|
|
mInvertPage = true;
|
|
break;
|
|
|
|
case 4:
|
|
ColorMatrixColorFilter IncreaseHighContrast = new ColorMatrixColorFilter(
|
|
sIncreaseContrastColorArray);
|
|
mPaint.setColorFilter(IncreaseHighContrast);
|
|
setHardwareRendering();
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
/**
|
|
* Pauses the JavaScript timers of the
|
|
* WebView instance, which will trigger a
|
|
* pause for all WebViews in the app.
|
|
*/
|
|
public synchronized void pauseTimers() {
|
|
if (mWebView != null) {
|
|
mWebView.pauseTimers();
|
|
Log.d(TAG, "Pausing JS timers");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Resumes the JavaScript timers of the
|
|
* WebView instance, which will trigger a
|
|
* resume for all WebViews in the app.
|
|
*/
|
|
public synchronized void resumeTimers() {
|
|
if (mWebView != null) {
|
|
mWebView.resumeTimers();
|
|
Log.d(TAG, "Resuming JS timers");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Requests focus down on the WebView instance
|
|
* if the view does not already have focus.
|
|
*/
|
|
public void requestFocus() {
|
|
if (mWebView != null && !mWebView.hasFocus()) {
|
|
mWebView.requestFocus();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Sets the visibility of the WebView to either
|
|
* View.GONE, View.VISIBLE, or View.INVISIBLE.
|
|
* other values passed in will have no effect.
|
|
*
|
|
* @param visible the visibility to set on the WebView.
|
|
*/
|
|
public void setVisibility(int visible) {
|
|
if (mWebView != null) {
|
|
mWebView.setVisibility(visible);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Tells the WebView to reload the current page.
|
|
* If the proxy settings are not ready then the
|
|
* this method will not have an affect as the
|
|
* proxy must start before the load occurs.
|
|
*/
|
|
public synchronized void reload() {
|
|
// Check if configured proxy is available
|
|
if (!mProxyUtils.isProxyReady(mActivity)) {
|
|
// User has been notified
|
|
return;
|
|
}
|
|
|
|
if (mWebView != null) {
|
|
mWebView.reload();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Finds all the instances of the text passed to this
|
|
* method and highlights the instances of that text
|
|
* in the WebView.
|
|
*
|
|
* @param text the text to search for.
|
|
*/
|
|
@SuppressLint("NewApi")
|
|
public synchronized void find(String text) {
|
|
if (mWebView != null) {
|
|
if (API >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
|
|
mWebView.findAllAsync(text);
|
|
} else {
|
|
//noinspection deprecation
|
|
mWebView.findAll(text);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Notify the tab to shutdown and destroy
|
|
* its WebView instance and to remove the reference
|
|
* to it. After this method is called, the current
|
|
* instance of the LightningView is useless as
|
|
* the WebView cannot be recreated using the public
|
|
* api.
|
|
*/
|
|
// TODO fix bug where WebView.destroy is being called before the tab
|
|
// is removed and would cause a memory leak if the parent check
|
|
// was not in place.
|
|
public synchronized void onDestroy() {
|
|
if (mWebView != null) {
|
|
// Check to make sure the WebView has been removed
|
|
// before calling destroy() so that a memory leak is not created
|
|
ViewGroup parent = (ViewGroup) mWebView.getParent();
|
|
if (parent != null) {
|
|
Log.e(TAG, "WebView was not detached from window before onDestroy");
|
|
parent.removeView(mWebView);
|
|
}
|
|
mWebView.stopLoading();
|
|
mWebView.onPause();
|
|
mWebView.clearHistory();
|
|
mWebView.setVisibility(View.GONE);
|
|
mWebView.removeAllViews();
|
|
mWebView.destroyDrawingCache();
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
|
|
//this is causing the segfault occasionally below 4.2
|
|
mWebView.destroy();
|
|
}
|
|
mWebView = null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Tell the WebView to navigate backwards
|
|
* in its history to the previous page.
|
|
*/
|
|
public synchronized void goBack() {
|
|
if (mWebView != null) {
|
|
mWebView.goBack();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Tell the WebView to navigate forwards
|
|
* in its history to the next page.
|
|
*/
|
|
public synchronized void goForward() {
|
|
if (mWebView != null) {
|
|
mWebView.goForward();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get the current user agent used
|
|
* by the WebView.
|
|
*
|
|
* @return retuns the current user agent
|
|
* of the WebView instance, or an empty
|
|
* string if the WebView is null.
|
|
*/
|
|
@NonNull
|
|
private String getUserAgent() {
|
|
if (mWebView != null) {
|
|
return mWebView.getSettings().getUserAgentString();
|
|
} else {
|
|
return "";
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Move the highlighted text in the WebView
|
|
* to the next matched text. This method will
|
|
* only have an affect after {@link LightningView#find(String)}
|
|
* is called. Otherwise it will do nothing.
|
|
*/
|
|
public synchronized void findNext() {
|
|
if (mWebView != null) {
|
|
mWebView.findNext(true);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Move the highlighted text in the WebView
|
|
* to the previous matched text. This method will
|
|
* only have an affect after {@link LightningView#find(String)}
|
|
* is called. Otherwise it will do nothing.
|
|
*/
|
|
public synchronized void findPrevious() {
|
|
if (mWebView != null) {
|
|
mWebView.findNext(false);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Clear the highlighted text in the WebView after
|
|
* {@link LightningView#find(String)} has been called.
|
|
* Otherwise it will have no affect.
|
|
*/
|
|
public synchronized void clearFindMatches() {
|
|
if (mWebView != null) {
|
|
mWebView.clearMatches();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Gets whether or not the page rendering is inverted or not.
|
|
* The main purpose of this is to indicate that JavaScript
|
|
* should be run at the end of a page load to invert only
|
|
* the images back to their uninverted states.
|
|
*
|
|
* @return true if the page is in inverted mode, false otherwise.
|
|
*/
|
|
public boolean getInvertePage() {
|
|
return mInvertPage;
|
|
}
|
|
|
|
/**
|
|
* Handles a long click on the page and delegates the URL to the
|
|
* proper dialog if it is not null, otherwise, it tries to get the
|
|
* URL using HitTestResult.
|
|
*
|
|
* @param url the url that should have been obtained from the WebView touch node
|
|
* thingy, if it is null, this method tries to deal with it and find
|
|
* a workaround.
|
|
*/
|
|
private void longClickPage(@Nullable final String url) {
|
|
if (mWebView == null) {
|
|
return;
|
|
}
|
|
final WebView.HitTestResult result = mWebView.getHitTestResult();
|
|
String currentUrl = mWebView.getUrl();
|
|
if (currentUrl != null && UrlUtils.isSpecialUrl(currentUrl)) {
|
|
if (currentUrl.endsWith(HistoryPage.FILENAME)) {
|
|
if (url != null) {
|
|
mBookmarksDialogBuilder.showLongPressedHistoryLinkDialog(mActivity, mUIController, url);
|
|
} else if (result != null && result.getExtra() != null) {
|
|
final String newUrl = result.getExtra();
|
|
mBookmarksDialogBuilder.showLongPressedHistoryLinkDialog(mActivity, mUIController, newUrl);
|
|
}
|
|
} else if (currentUrl.endsWith(BookmarkPage.FILENAME)) {
|
|
if (url != null) {
|
|
mBookmarksDialogBuilder.showLongPressedDialogForBookmarkUrl(mActivity, mUIController, url);
|
|
} else if (result != null && result.getExtra() != null) {
|
|
final String newUrl = result.getExtra();
|
|
mBookmarksDialogBuilder.showLongPressedDialogForBookmarkUrl(mActivity, mUIController, newUrl);
|
|
}
|
|
}
|
|
} else {
|
|
if (url != null) {
|
|
if (result != null) {
|
|
if (result.getType() == WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE || result.getType() == WebView.HitTestResult.IMAGE_TYPE) {
|
|
mBookmarksDialogBuilder.showLongPressImageDialog(mActivity, mUIController, url, getUserAgent());
|
|
} else {
|
|
mBookmarksDialogBuilder.showLongPressLinkDialog(mActivity, mUIController, url);
|
|
}
|
|
} else {
|
|
mBookmarksDialogBuilder.showLongPressLinkDialog(mActivity, mUIController, url);
|
|
}
|
|
} else if (result != null && result.getExtra() != null) {
|
|
final String newUrl = result.getExtra();
|
|
if (result.getType() == WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE || result.getType() == WebView.HitTestResult.IMAGE_TYPE) {
|
|
mBookmarksDialogBuilder.showLongPressImageDialog(mActivity, mUIController, newUrl, getUserAgent());
|
|
} else {
|
|
mBookmarksDialogBuilder.showLongPressLinkDialog(mActivity, mUIController, newUrl);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Determines whether or not the WebView can go
|
|
* backward or if it as the end of its history.
|
|
*
|
|
* @return true if the WebView can go back, false otherwise.
|
|
*/
|
|
public boolean canGoBack() {
|
|
return mWebView != null && mWebView.canGoBack();
|
|
}
|
|
|
|
/**
|
|
* Determine whether or not the WebView can go
|
|
* forward or if it is at the front of its history.
|
|
*
|
|
* @return true if it can go forward, false otherwise.
|
|
*/
|
|
public boolean canGoForward() {
|
|
return mWebView != null && mWebView.canGoForward();
|
|
}
|
|
|
|
/**
|
|
* Gets the current WebView instance of the tab.
|
|
*
|
|
* @return the WebView instance of the tab, which
|
|
* can be null.
|
|
*/
|
|
@Nullable
|
|
public synchronized WebView getWebView() {
|
|
return mWebView;
|
|
}
|
|
|
|
/**
|
|
* Gets the favicon currently in use by
|
|
* the page. If the current page does not
|
|
* have a favicon, it returns a default
|
|
* icon.
|
|
*
|
|
* @return a non-null Bitmap with the
|
|
* current favicon.
|
|
*/
|
|
@NonNull
|
|
public Bitmap getFavicon() {
|
|
return mTitle.getFavicon(mUIController.getUseDarkTheme());
|
|
}
|
|
|
|
/**
|
|
* Loads the URL in the WebView. If the proxy settings
|
|
* are still initializing, then the URL will not load
|
|
* as it is necessary to have the settings initialized
|
|
* before a load occurs.
|
|
*
|
|
* @param url the non-null URL to attempt to load in
|
|
* the WebView.
|
|
*/
|
|
public synchronized void loadUrl(@NonNull String url) {
|
|
// Check if configured proxy is available
|
|
if (!mProxyUtils.isProxyReady(mActivity)) {
|
|
return;
|
|
}
|
|
|
|
if (mWebView != null) {
|
|
mWebView.loadUrl(url, mRequestHeaders);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get the current title of the page, retrieved from
|
|
* the title object.
|
|
*
|
|
* @return the title of the page, or an empty string
|
|
* if there is no title.
|
|
*/
|
|
@NonNull
|
|
public String getTitle() {
|
|
return mTitle.getTitle();
|
|
}
|
|
|
|
/**
|
|
* Get the current URL of the WebView, or an empty
|
|
* string if the WebView is null or the URL is null.
|
|
*
|
|
* @return the current URL or an empty string.
|
|
*/
|
|
@NonNull
|
|
public String getUrl() {
|
|
if (mWebView != null && mWebView.getUrl() != null) {
|
|
return mWebView.getUrl();
|
|
} else {
|
|
return "";
|
|
}
|
|
}
|
|
|
|
/**
|
|
* The OnTouchListener used by the WebView so we can
|
|
* get scroll events and show/hide the action bar when
|
|
* the page is scrolled up/down.
|
|
*/
|
|
private class TouchListener implements OnTouchListener {
|
|
|
|
float mLocation;
|
|
float mY;
|
|
int mAction;
|
|
|
|
@SuppressLint("ClickableViewAccessibility")
|
|
@Override
|
|
public boolean onTouch(@Nullable View view, @NonNull MotionEvent arg1) {
|
|
if (view == null)
|
|
return false;
|
|
|
|
if (!view.hasFocus()) {
|
|
view.requestFocus();
|
|
}
|
|
mAction = arg1.getAction();
|
|
mY = arg1.getY();
|
|
if (mAction == MotionEvent.ACTION_DOWN) {
|
|
mLocation = mY;
|
|
} else if (mAction == MotionEvent.ACTION_UP) {
|
|
final float distance = (mY - mLocation);
|
|
if (distance > SCROLL_UP_THRESHOLD && view.getScrollY() < SCROLL_UP_THRESHOLD) {
|
|
mUIController.showActionBar();
|
|
} else if (distance < -SCROLL_UP_THRESHOLD) {
|
|
mUIController.hideActionBar();
|
|
}
|
|
mLocation = 0;
|
|
}
|
|
mGestureDetector.onTouchEvent(arg1);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* The SimpleOnGestureListener used by the {@link TouchListener}
|
|
* in order to delegate show/hide events to the action bar when
|
|
* the user flings the page. Also handles long press events so
|
|
* that we can capture them accurately.
|
|
*/
|
|
private class CustomGestureListener extends SimpleOnGestureListener {
|
|
|
|
@Override
|
|
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
|
|
int power = (int) (velocityY * 100 / sMaxFling);
|
|
if (power < -10) {
|
|
mUIController.hideActionBar();
|
|
} else if (power > 15) {
|
|
mUIController.showActionBar();
|
|
}
|
|
return super.onFling(e1, e2, velocityX, velocityY);
|
|
}
|
|
|
|
/**
|
|
* Without this, onLongPress is not called when user is zooming using
|
|
* two fingers, but is when using only one.
|
|
* <p/>
|
|
* The required behaviour is to not trigger this when the user is
|
|
* zooming, it shouldn't matter how much fingers the user's using.
|
|
*/
|
|
private boolean mCanTriggerLongPress = true;
|
|
|
|
@Override
|
|
public void onLongPress(MotionEvent e) {
|
|
if (mCanTriggerLongPress) {
|
|
Message msg = mWebViewHandler.obtainMessage();
|
|
if (msg != null) {
|
|
msg.setTarget(mWebViewHandler);
|
|
if (mWebView == null) {
|
|
return;
|
|
}
|
|
mWebView.requestFocusNodeHref(msg);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Is called when the user is swiping after the doubletap, which in our
|
|
* case means that he is zooming.
|
|
*/
|
|
@Override
|
|
public boolean onDoubleTapEvent(MotionEvent e) {
|
|
mCanTriggerLongPress = false;
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Is called when something is starting being pressed, always before
|
|
* onLongPress.
|
|
*/
|
|
@Override
|
|
public void onShowPress(MotionEvent e) {
|
|
mCanTriggerLongPress = true;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* A Handler used to get the URL from a long click
|
|
* event on the WebView. It does not hold a hard
|
|
* reference to the WebView and therefore will not
|
|
* leak it if the WebView is garbage collected.
|
|
*/
|
|
private static class WebViewHandler extends Handler {
|
|
|
|
@NonNull private final WeakReference<LightningView> mReference;
|
|
|
|
public WebViewHandler(LightningView view) {
|
|
mReference = new WeakReference<>(view);
|
|
}
|
|
|
|
@Override
|
|
public void handleMessage(@NonNull Message msg) {
|
|
super.handleMessage(msg);
|
|
final String url = msg.getData().getString("url");
|
|
LightningView view = mReference.get();
|
|
if (view != null) {
|
|
view.longClickPage(url);
|
|
}
|
|
}
|
|
}
|
|
}
|