/* * 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.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 acr.browser.lightning.app.BrowserApp; import acr.browser.lightning.constant.BookmarkPage; import acr.browser.lightning.constant.Constants; import acr.browser.lightning.constant.DownloadsPage; import acr.browser.lightning.constant.StartPage; import acr.browser.lightning.controller.UIController; import acr.browser.lightning.dialog.LightningDialogBuilder; import acr.browser.lightning.download.LightningDownloadListener; import acr.browser.lightning.preference.PreferenceManager; 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"; 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 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() { @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() { @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() { @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() { @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() { @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() { @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 getPathObservable(final String subFolder) { return Single.create(new SingleAction() { @Override public void onSubscribe(@NonNull SingleSubscriber 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. *

* 1. use the default user agent *

* 2. use the desktop user agent *

* 3. use the mobile user agent *

* 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 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. *

* 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 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); } } } }