package net.bible.android.view.activity.page; import android.annotation.SuppressLint; import android.annotation.TargetApi; import android.content.Context; import android.os.Build; import android.os.Handler; import android.util.Log; import android.view.KeyEvent; import android.view.MotionEvent; import android.view.View; import android.webkit.JsResult; import android.webkit.WebChromeClient; import android.webkit.WebView; import android.webkit.WebViewClient; import net.bible.android.SharedConstants; import net.bible.android.control.event.window.CurrentWindowChangedEvent; import net.bible.android.control.event.window.NumberOfWindowsChangedEvent; import net.bible.android.control.event.window.ScrollSecondaryWindowEvent; import net.bible.android.control.event.window.UpdateSecondaryWindowEvent; import net.bible.android.control.event.window.WindowSizeChangedEvent; import net.bible.android.control.link.LinkControl; import net.bible.android.control.page.PageControl; import net.bible.android.control.page.PageTiltScrollControl; import net.bible.android.control.page.window.Window; import net.bible.android.control.page.window.WindowControl; import net.bible.android.view.activity.base.DocumentView; import net.bible.android.view.activity.page.actionmode.VerseActionModeMediator; import net.bible.android.view.activity.page.screen.PageTiltScroller; import net.bible.android.view.util.UiUtils; import net.bible.service.common.CommonUtils; import net.bible.service.device.ScreenSettings; import de.greenrobot.event.EventBus; /** The WebView component that shows the main bible and commentary text * * @author Martin Denham [mjdenham at gmail dot com] * @see gnu.lgpl.License for license details.<br> * The copyright to this program is held by it's author. */ public class BibleView extends WebView implements DocumentView, VerseActionModeMediator.VerseHighlightControl { private final Window window; private final WindowControl windowControl; private final BibleKeyHandler bibleKeyHandler; private BibleJavascriptInterface bibleJavascriptInterface; private int mJumpToVerse = SharedConstants.NO_VALUE; private float mJumpToYOffsetRatio = SharedConstants.NO_VALUE; private boolean mIsVersePositionRecalcRequired = true; private PageTiltScroller mPageTiltScroller; private boolean hideScrollBar; private boolean wasAtRightEdge; private boolean wasAtLeftEdge; private final PageControl pageControl; private final PageTiltScrollControl pageTiltScrollControl; private final LinkControl linkControl; private int maintainMovingVerse = -1; private boolean kitKatPlus = CommonUtils.isKitKatPlus(); // struggling to ensure correct initial positioning of pages, giving the page a unique history url seemed to help - maybe it then is sure each page is unique so resets everything private static int historyUrlUniquify = 1; // never go to 0 because a bug in Android prevents invalidate after loadDataWithBaseURL so no scrollOrJumpToVerse will occur private static final int TOP_OF_SCREEN = 1; // remember current background colour so we know when it changes private Boolean wasNightMode; private static final String TAG = "BibleView"; /** * Constructor. This version is only needed if you will be instantiating * the object manually (not from a layout XML file). * @param context * @param windowControl * @param bibleKeyHandler * @param pageControl * @param pageTiltScrollControl * @param linkControl */ public BibleView(Context context, Window window, WindowControl windowControl, BibleKeyHandler bibleKeyHandler, PageControl pageControl, PageTiltScrollControl pageTiltScrollControl, LinkControl linkControl) { super(context); this.window = window; this.windowControl = windowControl; this.bibleKeyHandler = bibleKeyHandler; this.pageControl = pageControl; this.pageTiltScrollControl = pageTiltScrollControl; this.linkControl = linkControl; } /** * This is not passed into the constructor due to a cyclic dependency. bjsi -> */ public void setBibleJavascriptInterface(BibleJavascriptInterface bibleJavascriptInterface) { this.bibleJavascriptInterface = bibleJavascriptInterface; addJavascriptInterface(bibleJavascriptInterface, "jsInterface"); } @SuppressLint("SetJavaScriptEnabled") public void initialise() { /* WebViewClient must be set BEFORE calling loadUrl! */ setWebViewClient(new WebViewClient() { @Override public boolean shouldOverrideUrlLoading(WebView view, String url) { // load Strongs refs when a user clicks on a link if (linkControl.loadApplicationUrl(url)) { return true; } else { return super.shouldOverrideUrlLoading(view, url); } } @Override public void onLoadResource(WebView view, String url) { Log.d(TAG, "onLoadResource:"+url); super.onLoadResource(view, url); } @Override public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) { super.onReceivedError(view, errorCode, description, failingUrl); Log.e(TAG, description); } }); // handle alerts setWebChromeClient(new WebChromeClient() { @Override public boolean onJsAlert(WebView view, String url, String message, JsResult result) { Log.d(TAG, message); result.confirm(); return true; } }); // need javascript to enable jump to anchors/verses getSettings().setJavaScriptEnabled(true); applyPreferenceSettings(); mPageTiltScroller = new PageTiltScroller(this, pageTiltScrollControl); mPageTiltScroller.enableTiltScroll(true); // if this webview becomes (in)active then must start/stop auto-scroll EventBus.getDefault().register(this); // initialise split state related code - always screen1 is selected first onEvent(new CurrentWindowChangedEvent(windowControl.getActiveWindow())); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); // update the height in ScreenSettings ScreenSettings.setContentViewHeightPx(getMeasuredHeight()); } /** apply settings set by the user using Preferences */ @Override public void applyPreferenceSettings() { applyFontSize(); changeBackgroundColour(); ScreenSettings.setContentViewHeightPx(getHeight()); } private void applyFontSize() { int fontSize = pageControl.getDocumentFontSize(window); getSettings().setDefaultFontSize(fontSize); // 1.6 is taken from css - line-height: 1.6em; ScreenSettings.setLineHeightDips((int) (1.6 * fontSize)); } /** may need updating depending on environmental brightness */ @Override public boolean changeBackgroundColour() { // if night mode then set dark background colour final Boolean nightMode = ScreenSettings.isNightMode(); final boolean changed = !nightMode.equals(this.wasNightMode); if (changed) { UiUtils.setBibleViewBackgroundColour(this, nightMode); this.wasNightMode = nightMode; } return changed; } /** * Show a page from bible commentary */ @Override public void show(String html, int jumpToVerse, float jumpToYOffsetRatio) { Log.d(TAG, "Show(html," + jumpToVerse + "," + jumpToYOffsetRatio + ") Window:" + window); // set background colour if necessary changeBackgroundColour(); // call this from here because some documents may require an adjusted font size e.g. those using Greek font applyFontSize(); // scrollTo is used on kitkatplus but sometimes the later scrollTo was not working // If verse 1 then later code will jump to top of screen because it looks better than going to verse 1 if (kitKatPlus && jumpToVerse>1) { html = html.replace("</body>", "<script>$(window).load(function() {scrollToVerse('" + jumpToVerse + "');})</script></body>"); } else { setJumpToVerse(jumpToVerse); } mJumpToYOffsetRatio = jumpToYOffsetRatio; // either enable verse selection or the default text selection html = enableSelection(html); // allow zooming if map enableZoomForMap(pageControl.getCurrentPageManager().isMapShown()); loadDataWithBaseURL("file:///android_asset/", html, "text/html", "UTF-8", "http://historyUrl"+historyUrlUniquify++); // ensure jumpToOffset is eventually called during initialisation. It will normally be called automatically but sometimes is not i.e. after jump to verse 1 at top of screen then press back. // don't set this value too low or it may trigger before a proper upcoming computeVerticalScrollEvent // 100 was good for my Nexus 4 but 500 for my G1 - it would be good to get a reflection of processor speed and adjust appropriately invokeJumpToOffsetIfRequired(CommonUtils.isSlowDevice() ? 500 : 350); } /** * Enable or disable zoom controls depending on whether map is currently shown */ @SuppressLint("NewApi") protected void enableZoomForMap(boolean isMap) { getSettings().setBuiltInZoomControls(true); getSettings().setSupportZoom(isMap); if (CommonUtils.isHoneycombPlus()) { // Could not totally remove the zoom controls after returning to a Bible view so never display them getSettings().setDisplayZoomControls(false); } // http://stackoverflow.com/questions/3808532/how-to-set-the-initial-zoom-width-for-a-webview getSettings().setLoadWithOverviewMode(isMap); getSettings().setUseWideViewPort(isMap); } /** * This is called fairly late in initialisation so override to invoke jump to offset position */ @Override protected int computeVerticalScrollExtent() { int result = super.computeVerticalScrollExtent(); // trigger jump to appropriate verse or offset into a book or commentary page... invokeJumpToOffsetIfRequired(0); return result; } /** * Trigger jump to correct offset */ private void invokeJumpToOffsetIfRequired(long delay) { if (mJumpToVerse!=SharedConstants.NO_VALUE || mJumpToYOffsetRatio!=SharedConstants.NO_VALUE) { postDelayed(new Runnable() { @Override public void run() { jumpToOffset(); } }, delay); } } private void jumpToOffset() { if (getContentHeight() > 0) { if (mIsVersePositionRecalcRequired) { mIsVersePositionRecalcRequired = false; executeJavascript("registerVersePositions()"); } bibleJavascriptInterface.setNotificationsEnabled(windowControl.isActiveWindow(window)); // screen is changing shape/size so constantly maintain the current verse position // main difference from jumpToVerse is that this is not cleared after jump if (maintainMovingVerse>0) { scrollOrJumpToVerse(maintainMovingVerse); } // go to any specified verse or offset if (mJumpToVerse!=SharedConstants.NO_VALUE) { // must clear mJumpToVerse because setting location causes another onPageFinished int jumpToVerse = mJumpToVerse; mJumpToVerse = SharedConstants.NO_VALUE; scrollOrJumpToVerse(jumpToVerse); } else if (mJumpToYOffsetRatio!=SharedConstants.NO_VALUE) { int contentHeight = getContentHeight(); int y = (int) ((float)contentHeight*mJumpToYOffsetRatio); // must zero mJumpToVerse because setting location causes another onPageFinished mJumpToYOffsetRatio = SharedConstants.NO_VALUE; scrollTo(0, Math.max(y, TOP_OF_SCREEN)); } } } /** prevent swipe right if the user is scrolling the page right */ public boolean isPageNextOkay() { boolean isOkay = true; if (window.getPageManager().isMapShown()) { // allow swipe right if at right side of map boolean isAtRightEdge = (getScrollX() >= getMaxHorizontalScroll()); // the first side swipe takes us to the edge and second takes us to next page isOkay = isAtRightEdge && wasAtRightEdge; wasAtRightEdge = isAtRightEdge; wasAtLeftEdge = false; } return isOkay; } /** prevent swipe left if the user is scrolling the page left */ public boolean isPagePreviousOkay() { boolean isOkay = true; if (window.getPageManager().isMapShown()) { // allow swipe left if at left edge of map boolean isAtLeftEdge = (getScrollX() == 0); // the first side swipe takes us to the edge and second takes us to next page isOkay = isAtLeftEdge && wasAtLeftEdge; wasAtLeftEdge = isAtLeftEdge; wasAtRightEdge = false; } return isOkay; } @Override public void onWindowFocusChanged(boolean hasWindowFocus) { super.onWindowFocusChanged(hasWindowFocus); Log.d(TAG, "Focus changed so start/stop scroll"); if (hasWindowFocus) { resumeTiltScroll(); } else { pauseTiltScroll(); } } private void pauseTiltScroll() { Log.d(TAG, "Pausing tilt to scroll " + window); mPageTiltScroller.enableTiltScroll(false); } private void resumeTiltScroll() { // but if multiple windows then only if the current active window if (windowControl.isActiveWindow(window)) { Log.d(TAG, "Resuming tilt to scroll "+window); mPageTiltScroller.enableTiltScroll(true); } } /** ensure auto-scroll does not continue when screen is powered off */ @Override public void onScreenTurnedOn() { resumeTiltScroll(); } @Override public void onScreenTurnedOff() { pauseTiltScroll(); } @Override public boolean onTouchEvent(MotionEvent event) { //Log.d(TAG, "BibleView onTouchEvent"); windowControl.setActiveWindow(window); boolean handled = super.onTouchEvent(event); // Allow user to redefine viewing angle by touching screen mPageTiltScroller.recalculateViewingPosition(); return handled; } @Override public float getCurrentPosition() { // see http://stackoverflow.com/questions/1086283/getting-document-position-in-a-webview int contentHeight = getContentHeight(); int scrollY = getScrollY(); float ratio = ((float) scrollY / ((float) contentHeight)); return ratio; } @Override public boolean onKeyUp(int keyCode, KeyEvent event) { //TODO allow DPAD_LEFT to always change page and navigation between links using dpad // placing BibleKeyHandler second means that DPAD left is unable to move to prev page if strongs refs are shown // vice-versa (webview second) means right & left can not be used to navigate between Strongs links // common key handling i.e. KEYCODE_DPAD_RIGHT & KEYCODE_DPAD_LEFT to change chapter if (bibleKeyHandler.onKeyUp(keyCode, event)) { return true; } // allow movement from link to link in current page return super.onKeyUp(keyCode, event); } public boolean scroll(boolean forward, int scrollAmount) { boolean ok = false; hideScrollBar = true; for (int i=0; i<scrollAmount; i++) { if (forward) { // scroll down/forward if not at bottom if (getScrollY()+1 < getMaxVerticalScroll()) { scrollBy(0, 1); ok = true; } } else { // scroll up/backward if not at top if (getScrollY() > TOP_OF_SCREEN) { // scroll up/back scrollBy(0, -1); ok = true; } } } hideScrollBar = false; return ok; } /** Used to prevent scroll off bottom using auto-scroll * see http://stackoverflow.com/questions/5069765/android-webview-how-to-autoscroll-a-page */ private int getMaxVerticalScroll() { //TODO get these once, they probably won't change return computeVerticalScrollRange()-computeVerticalScrollExtent(); } private int getMaxHorizontalScroll() { return computeHorizontalScrollRange()-computeHorizontalScrollExtent(); } /** allow vertical scroll bar to be hidden during auto-scroll */ protected boolean awakenScrollBars(int startDelay, boolean invalidate) { if (!hideScrollBar) { return super.awakenScrollBars(startDelay, invalidate); } else { return false; } } @Override public View asView() { return this; } public void onEvent(CurrentWindowChangedEvent event) { if (window.equals(event.getActiveWindow())) { bibleJavascriptInterface.setNotificationsEnabled(true); resumeTiltScroll(); } else { bibleJavascriptInterface.setNotificationsEnabled(false); pauseTiltScroll(); } } public void onEvent(UpdateSecondaryWindowEvent event) { if (window.equals(event.getUpdateScreen())) { changeBackgroundColour(); show(event.getHtml(), event.getVerseNo(), SharedConstants.NO_VALUE); } } public void onEvent(ScrollSecondaryWindowEvent event) { if (window.equals(event.getWindow()) && getHandler()!=null) { scrollOrJumpToVerseOnUIThread(event.getVerseNo()); } } public void onEvent(WindowSizeChangedEvent event) { Log.d(TAG, "window size changed"); boolean isScreenVerse = event.isVerseNoSet(window); if (isScreenVerse) { this.maintainMovingVerse = event.getVerseNo(window); } // when move finished the verse positions will have changed if in Landscape so recalc positions boolean isMoveFinished = event.isFinished(); if (isMoveFinished && isScreenVerse) { final int verse = event.getVerseNo(window); setJumpToVerse(verse); final Handler handler = getHandler(); if (handler !=null) { handler.postDelayed(new Runnable() { @Override public void run() { // clear jump value if still set BibleView.this.maintainMovingVerse = SharedConstants.NO_VALUE; // ensure we are in the correct place after screen settles scrollOrJumpToVerse(verse); executeJavascript("registerVersePositions()"); } } , WindowControl.SCREEN_SETTLE_TIME_MILLIS/2); } } } @Override protected void onDetachedFromWindow() { super.onDetachedFromWindow(); Log.d(TAG, "Detached from window"); // prevent random verse changes while layout is being rebuild because of window changes bibleJavascriptInterface.setNotificationsEnabled(false); pauseTiltScroll(); } @Override protected void onAttachedToWindow() { super.onAttachedToWindow(); Log.d(TAG, "Attached to window"); if (windowControl.isActiveWindow(window)) { bibleJavascriptInterface.setNotificationsEnabled(true); // may have returned from MyNote view resumeTiltScroll(); } } public void onEvent(NumberOfWindowsChangedEvent event) { if (getVisibility()==View.VISIBLE && event.isVerseNoSet(window)) { setJumpToVerse(event.getVerseNo(window)); } } public Window getWindowNo() { return window; } public void setVersePositionRecalcRequired(boolean mIsVersePositionRecalcRequired) { this.mIsVersePositionRecalcRequired = mIsVersePositionRecalcRequired; } public void setJumpToVerse(int verseNo) { this.mJumpToVerse = verseNo; } /** move the view so the selected verse is at the top or at least visible */ private void scrollOrJumpToVerseOnUIThread(final int verse) { runOnUiThread(new Runnable() { @Override public void run() { scrollOrJumpToVerse(verse); } }); } /** move the view so the selected verse is at the top or at least visible */ private void scrollOrJumpToVerse(final int verse) { Log.d(TAG, "Scroll or jump to:" + verse); if (verse==SharedConstants.NO_VALUE) { // NOOP } else if (verse<=1) { // use scroll to because difficult to place a tag exactly at the top scrollTo(0, TOP_OF_SCREEN); } else { // jump to correct verse // but scrollTop does not work on Android 3.0-4.0 and changing document location does not work on latest WebView if (kitKatPlus) { // required format changed in 4.2 http://stackoverflow.com/questions/14771970/how-to-call-javascript-in-android-4-2 executeJavascript("scrollToVerse('" + verse + "')"); } else { executeJavascript("(function() { document.location = '#" + verse+"' })()"); } } } /** * Either enable verse selection or the default text selection */ private String enableSelection(String html) { if (window.getPageManager().isBibleShown()) { // handle long click ourselves and prevent webview showing text selection automatically setOnLongClickListener(new OnLongClickListener() { @Override public boolean onLongClick(View v) { return true; } }); setLongClickable(false); // need to enable verse selection after page load, but not always so can't use onload html += "<script>enableVerseLongTouchSelectionMode();</script>"; } else { // reset handling of long press setOnLongClickListener(null); } return html; } @Override public void enableVerseTouchSelection() { executeJavascriptOnUiThread("enableVerseTouchSelection()"); } @Override public void disableVerseTouchSelection() { executeJavascriptOnUiThread("disableVerseTouchSelection()"); } @Override public void highlightVerse(final int verseNo) { executeJavascriptOnUiThread("highlightVerse("+verseNo+")"); } @Override public void unhighlightVerse(final int verseNo) { executeJavascriptOnUiThread("unhighlightVerse("+verseNo+")"); } @Override public void clearVerseHighlight() { executeJavascriptOnUiThread("clearVerseHighlight()"); } private void executeJavascriptOnUiThread(final String javascript) { runOnUiThread(new Runnable() { @Override public void run() { executeJavascript(javascript); } }); } private void runOnUiThread(final Runnable runnable) { final Handler handler = getHandler(); if (handler !=null) { handler.post(runnable); } } @TargetApi(Build.VERSION_CODES.KITKAT) private void executeJavascript(String javascript) { Log.d(TAG, "Executing JS:"+javascript); if (kitKatPlus) { evaluateJavascript(javascript+";", null); } else { loadUrl("javascript:"+javascript+";"); } } }