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.
1232 lines
42 KiB
1232 lines
42 KiB
/* |
|
* Copyright 2014 A.C.R. Development |
|
*/ |
|
|
|
package org.purplei2p.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.WebView; |
|
|
|
import com.anthonycr.bonsai.Schedulers; |
|
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 org.purplei2p.lightning.BrowserApp; |
|
import org.purplei2p.lightning.constant.BookmarkPage; |
|
import org.purplei2p.lightning.constant.Constants; |
|
import org.purplei2p.lightning.constant.DownloadsPage; |
|
import org.purplei2p.lightning.constant.StartPage; |
|
import org.purplei2p.lightning.controller.UIController; |
|
import org.purplei2p.lightning.dialog.LightningDialogBuilder; |
|
import org.purplei2p.lightning.download.LightningDownloadListener; |
|
import org.purplei2p.lightning.preference.PreferenceManager; |
|
import org.purplei2p.lightning.utils.Preconditions; |
|
import org.purplei2p.lightning.utils.ProxyUtils; |
|
import org.purplei2p.lightning.utils.UrlUtils; |
|
import org.purplei2p.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"; |
|
|
|
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(); |
|
private boolean mIsNewTab; |
|
private final boolean mIsIncognitoTab; |
|
private boolean mIsForegroundTab; |
|
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 mDialogBuilder; |
|
@Inject ProxyUtils mProxyUtils; |
|
|
|
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 whether this tab was the |
|
* result of a new intent sent |
|
* to the browser. |
|
* |
|
* @param isNewTab true if it's from |
|
* a new intent, |
|
* false otherwise. |
|
*/ |
|
public void setIsNewTab(boolean isNewTab) { |
|
mIsNewTab = isNewTab; |
|
} |
|
|
|
/** |
|
* Returns whether this tab was created |
|
* as a result of a new intent. |
|
* |
|
* @return true if it was a new intent, |
|
* false otherwise. |
|
*/ |
|
public boolean isNewTab() { |
|
return mIsNewTab; |
|
} |
|
|
|
/** |
|
* 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); |
|
} |
|
}); |
|
} |
|
|
|
/** |
|
* 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 loadDownloadspage() { |
|
new DownloadsPage().getDownloadsPage() |
|
.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); |
|
} |
|
|
|
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 setIsForegroundTab(boolean isForeground) { |
|
mIsForegroundTab = 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 mIsForegroundTab; |
|
} |
|
|
|
/** |
|
* 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 (UrlUtils.isHistoryUrl(currentUrl)) { |
|
if (url != null) { |
|
mDialogBuilder.showLongPressedHistoryLinkDialog(mActivity, mUIController, url); |
|
} else if (result != null && result.getExtra() != null) { |
|
final String newUrl = result.getExtra(); |
|
mDialogBuilder.showLongPressedHistoryLinkDialog(mActivity, mUIController, newUrl); |
|
} |
|
} else if (UrlUtils.isBookmarkUrl(currentUrl)) { |
|
if (url != null) { |
|
mDialogBuilder.showLongPressedDialogForBookmarkUrl(mActivity, mUIController, url); |
|
} else if (result != null && result.getExtra() != null) { |
|
final String newUrl = result.getExtra(); |
|
mDialogBuilder.showLongPressedDialogForBookmarkUrl(mActivity, mUIController, newUrl); |
|
} |
|
} else if (UrlUtils.isDownloadsUrl(currentUrl)) { |
|
if (url != null) { |
|
mDialogBuilder.showLongPressedDialogForDownloadUrl(mActivity, mUIController, url); |
|
} else if (result != null && result.getExtra() != null) { |
|
final String newUrl = result.getExtra(); |
|
mDialogBuilder.showLongPressedDialogForDownloadUrl(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) { |
|
mDialogBuilder.showLongPressImageDialog(mActivity, mUIController, url, getUserAgent()); |
|
} else { |
|
mDialogBuilder.showLongPressLinkDialog(mActivity, mUIController, url); |
|
} |
|
} else { |
|
mDialogBuilder.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) { |
|
mDialogBuilder.showLongPressImageDialog(mActivity, mUIController, newUrl, getUserAgent()); |
|
} else { |
|
mDialogBuilder.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; |
|
|
|
WebViewHandler(@NonNull 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); |
|
} |
|
} |
|
} |
|
}
|
|
|