package com.erakk.lnreader.UI.activity; import android.annotation.SuppressLint; import android.content.Context; import android.content.DialogInterface; import android.content.DialogInterface.OnClickListener; import android.content.Intent; import android.content.SharedPreferences; import android.content.pm.PackageInfo; import android.net.Uri; import android.os.AsyncTask; import android.os.Build; import android.os.Bundle; import android.preference.PreferenceManager; import android.speech.tts.TextToSpeech.OnInitListener; import android.support.v7.app.AlertDialog; import android.util.Log; import android.view.KeyEvent; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.view.WindowManager; import android.widget.Button; import android.widget.ListView; import android.widget.ProgressBar; import android.widget.RelativeLayout; import android.widget.TextView; import android.widget.Toast; import com.erakk.lnreader.Constants; import com.erakk.lnreader.LNReaderApplication; import com.erakk.lnreader.R; import com.erakk.lnreader.UIHelper; import com.erakk.lnreader.adapter.BookmarkModelAdapter; import com.erakk.lnreader.adapter.PageModelAdapter; import com.erakk.lnreader.callback.ICallbackEventData; import com.erakk.lnreader.callback.IExtendedCallbackNotifier; import com.erakk.lnreader.dao.NovelsDao; import com.erakk.lnreader.helper.BakaTsukiWebChromeClient; import com.erakk.lnreader.helper.BakaTsukiWebViewClient; import com.erakk.lnreader.helper.DisplayNovelContentHtmlHelper; import com.erakk.lnreader.helper.DisplayNovelContentTTSHelper; import com.erakk.lnreader.helper.DisplayNovelContentUIHelper; import com.erakk.lnreader.helper.NonLeakingWebView; import com.erakk.lnreader.helper.OnCompleteListener; import com.erakk.lnreader.helper.TtsHelper; import com.erakk.lnreader.helper.Util; import com.erakk.lnreader.model.BookModel; import com.erakk.lnreader.model.BookmarkModel; import com.erakk.lnreader.model.NovelCollectionModel; import com.erakk.lnreader.model.NovelContentModel; import com.erakk.lnreader.model.NovelContentUserModel; import com.erakk.lnreader.model.PageModel; import com.erakk.lnreader.task.AsyncTaskResult; import com.erakk.lnreader.task.LoadNovelContentTask; import com.erakk.lnreader.task.LoadWacTask; import java.io.File; import java.util.ArrayList; public class DisplayLightNovelContentActivity extends BaseActivity implements IExtendedCallbackNotifier<AsyncTaskResult<?>>, OnInitListener, OnCompleteListener { private static final String TAG = DisplayLightNovelContentActivity.class.toString(); public NovelContentModel content; public NovelContentUserModel contentUserData; private PageModel currPageModel = null; private NovelCollectionModel novelDetails; private LoadNovelContentTask task; private boolean restored; private boolean isFullscreen; private boolean isPageLoaded = false; private AlertDialog bookmarkMenu = null; private AlertDialog tocMenu = null; private Menu _menu; // region private helpers, init in onCreate() private DisplayNovelContentTTSHelper _tts; private DisplayNovelContentUIHelper _uih; // endregion public void updateCurrentPageModel(PageModel reference, NovelContentModel refContent, NovelContentUserModel refContentUser) { String pageModelStr = ""; String contentPageStr = ""; String contentUserPageStr = ""; if (reference != null) { this.currPageModel = reference; pageModelStr = reference.getPage(); } else if (currPageModel != null) { pageModelStr = currPageModel.getPage(); } if (refContent != null) { this.content = refContent; contentPageStr = refContent.getPage(); } else if (content != null) { contentPageStr = content.getPage(); } if (refContentUser != null) { this.contentUserData = refContentUser; contentUserPageStr = refContentUser.getPage(); } else if (contentUserData != null) { contentUserPageStr = contentUserData.getPage(); } final String message = String.format("PageModel: %s\nContent Page: %s\nContent User Page: %s", pageModelStr, contentPageStr, contentUserPageStr); runOnUiThread(new Runnable() { @Override public void run() { TextView txtDebug = (TextView) findViewById(R.id.txtDebug); try { PackageInfo pInfo = getPackageManager().getPackageInfo(getPackageName(), 0); if (!pInfo.versionName.contains("beta")) txtDebug.setMaxHeight(0); txtDebug.setText(message); } catch (Exception ex) { txtDebug.setVisibility(View.GONE); } } }); } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); restored = false; setContentView(R.layout.activity_display_light_novel_content); // custom link handler NonLeakingWebView webView = (NonLeakingWebView) findViewById(R.id.webViewContent); BakaTsukiWebViewClient client = new BakaTsukiWebViewClient(this); webView.setWebViewClient(client); BakaTsukiWebChromeClient chromeClient = new BakaTsukiWebChromeClient(this); webView.setWebChromeClient(chromeClient); // UI Helper _uih = new DisplayNovelContentUIHelper(this); _uih.prepareCompatSearchBox(webView); _uih.prepareTopDownButton(); isFullscreen = getFullscreenPreferences(); _uih.prepareFullscreenHandler(webView); _uih.toggleFullscreen(isFullscreen); _tts = new DisplayNovelContentTTSHelper(this); Log.d(TAG, "OnCreate Completed."); } @Override protected void onDestroy() { NonLeakingWebView webView = (NonLeakingWebView) findViewById(R.id.webViewContent); if (webView != null) { RelativeLayout rootView = (RelativeLayout) findViewById(R.id.rootView); rootView.removeView(webView); webView.removeAllViews(); webView.destroy(); } _tts.unbindTtsService(); getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); super.onDestroy(); } @Override protected void onStart() { super.onStart(); Log.d(TAG, "onStart Completed"); } @Override protected void onRestart() { super.onRestart(); // // re-enter immersive mode on restart // if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT && getFullscreenPreferences()) { // UIHelper.Recreate(this); // } restored = true; Log.d(TAG, "onRestart Completed"); } @Override public void onResume() { super.onResume(); // compare the settings from OnCreate and after user resume. if (isFullscreen != getFullscreenPreferences()) { UIHelper.Recreate(this); } // show/hide option button if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { Button btnOption = (Button) findViewById(R.id.btnMenu); // do not show option button for KitKat, immersive mode will show action bar if (getFullscreenPreferences() && Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) { btnOption.setVisibility(View.VISIBLE); } else { btnOption.setVisibility(View.GONE); } } // moved page loading here rather than onCreate // to avoid only the first page loaded when resume from sleep // (activity destroyed, onCreate called again) // when the user navigate using next/prev/jumpTo if (!restored) { String page = getIntent().getStringExtra(Constants.EXTRA_PAGE); PageModel pageModel = new PageModel(page); try { pageModel = NovelsDao.getInstance().getExistingPageModel(pageModel, null); if (pageModel == null) { Toast.makeText(this, getResources().getString(R.string.bookmark_content_load_error), Toast.LENGTH_LONG).show(); Log.w(TAG, "Missing page: " + page); onBackPressed(); } else { updateCurrentPageModel(pageModel, null, null); this.getIntent().putExtra(Constants.EXTRA_PAGE_IS_EXTERNAL, currPageModel.isExternal()); executeTask(pageModel, false); } } catch (Exception e) { Log.e(TAG, "Failed to get the PageModel for content: " + getIntent().getStringExtra(Constants.EXTRA_PAGE), e); } } setWebViewSettings(); if (UIHelper.isTTSEnabled(this)) _tts.setupTtsService(); Log.d(TAG, "onResume Completed"); } @Override public boolean onPrepareOptionsMenu(Menu menu) { super.onPrepareOptionsMenu(menu); _menu = menu; try { if (content != null) { setPrevNextButtonState(content.getPageModel()); _menu.findItem(R.id.menu_save_external).setVisible(false); _menu.findItem(R.id.menu_browser_back).setVisible(false); } else { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) _menu.findItem(R.id.menu_save_external).setVisible(true); _menu.findItem(R.id.menu_browser_back).setVisible(true); NonLeakingWebView webView = (NonLeakingWebView) findViewById(R.id.webViewContent); if (webView != null) _menu.findItem(R.id.menu_browser_back).setEnabled(webView.canGoBack()); } } catch (Exception e) { Log.w(TAG, "Cannot get current page model"); } return true; } @Override public void onPause() { super.onPause(); setLastReadState(); if (getTtsStopOnPause()) { _tts.stop(); } Log.d(TAG, "onPause Completed"); } @Override public void onSaveInstanceState(Bundle savedInstanceState) { super.onSaveInstanceState(savedInstanceState); try { if (content == null) savedInstanceState.putString(Constants.EXTRA_PAGE, getIntent().getStringExtra(Constants.EXTRA_PAGE)); else savedInstanceState.putString(Constants.EXTRA_PAGE, content.getPageModel().getPage()); if (currPageModel == null) savedInstanceState.putBoolean(Constants.EXTRA_PAGE_IS_EXTERNAL, false); else savedInstanceState.putBoolean(Constants.EXTRA_PAGE_IS_EXTERNAL, currPageModel.isExternal()); } catch (Exception e) { Log.e(TAG, "Error when saving instance", e); } Log.d(TAG, "onSaveInstanceState Completed"); } @Override public void onRestoreInstanceState(Bundle savedInstanceState) { super.onRestoreInstanceState(savedInstanceState); String restoredPage = savedInstanceState.getString(Constants.EXTRA_PAGE); try { // replace the current pageModel with the saved instance // if have different page PageModel pageModel = new PageModel(restoredPage); pageModel = NovelsDao.getInstance().getPageModel(pageModel, null); executeTask(pageModel, false); updateCurrentPageModel(pageModel, null, null); } catch (Exception e) { Log.e(TAG, "Error when restoring instance", e); } // flag that this activity is restored from pause restored = true; Log.d(TAG, "onRestoreInstanceState Completed"); } @Override public void onStop() { super.onStop(); Log.d(TAG, "onStop Completed"); } @Override @SuppressLint("NewApi") public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.activity_display_light_novel_content, menu); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { if (getFullscreenPreferences()) { menu.findItem(R.id.menu_chapter_next).setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER); menu.findItem(R.id.menu_chapter_previous).setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER); } } // disable invert option if using custom color or css MenuItem invertColor = menu.findItem(R.id.invert_colors); if (invertColor != null) { if (DisplayNovelContentHtmlHelper.getUseCustomCSS(this) || UIHelper.getCssUseCustomColorPreferences(this)) { invertColor.setEnabled(false); } else { invertColor.setEnabled(true); } } _tts.setupTTSMenu(menu); _menu = menu; return true; } @Override public boolean onOptionsItemSelected(MenuItem item) { NonLeakingWebView webView = (NonLeakingWebView) findViewById(R.id.webViewContent); switch (item.getItemId()) { case R.id.menu_refresh_chapter_content: /* * Implement code to refresh chapter content */ PageModel page = null; if (content != null) { try { page = content.getPageModel(); } catch (Exception e) { Log.e(TAG, "Cannot get current chapter.", e); } } else { String pageStr = getIntent().getStringExtra(Constants.EXTRA_PAGE); try { page = NovelsDao.getInstance().getExistingPageModel(new PageModel(pageStr), null); if (page == null) { // no page model, just url page = new PageModel(pageStr); page.setExternal(true); } } catch (Exception e) { Log.e(TAG, "Cannot get current chapter.", e); } } if (page != null) { executeTask(page, true); } return true; case R.id.invert_colors: /* * Implement code to invert colors */ UIHelper.ToggleColorPref(this); UIHelper.Recreate(this); return true; case R.id.menu_chapter_previous: /* * Implement code to move to previous chapter */ String currentPage = getIntent().getStringExtra(Constants.EXTRA_PAGE); try { if (novelDetails == null) novelDetails = NovelsDao.getInstance().getNovelDetails(content.getPageModel(), null, false); PageModel prev = novelDetails.getPrev(currentPage, UIHelper.getShowMissing(this), UIHelper.getShowRedlink(this)); if (prev != null) { jumpTo(prev); } else { Toast.makeText(this, getResources().getString(R.string.first_available_chapter), Toast.LENGTH_SHORT).show(); } } catch (Exception e) { Log.e(TAG, "Cannot get previous chapter.", e); } return true; case R.id.menu_chapter_next: /* * Implement code to move to next chapter */ String currentPage2 = getIntent().getStringExtra(Constants.EXTRA_PAGE); try { if (novelDetails == null) novelDetails = NovelsDao.getInstance().getNovelDetails(content.getPageModel(), null, false); PageModel next = novelDetails.getNext(currentPage2, UIHelper.getShowMissing(this), UIHelper.getShowRedlink(this)); if (next != null) { jumpTo(next); } else { Toast.makeText(this, getResources().getString(R.string.last_available_chapter), Toast.LENGTH_SHORT).show(); } } catch (Exception e) { Log.e(TAG, "Cannot get next chapter.", e); } return true; case R.id.menu_chapter_toc: if (tocMenu != null) tocMenu.show(); return true; case R.id.menu_search: showSearchBox(); return true; case R.id.menu_bookmarks_here: if (bookmarkMenu != null) bookmarkMenu.show(); return true; case R.id.menu_speak: _tts.start(webView, contentUserData.getLastYScroll()); return true; case R.id.menu_pause_tts: _tts.pause(); return true; case R.id.menu_save_external: // save based on current intent page name. String url = getIntent().getStringExtra(Constants.EXTRA_PAGE); if (!url.startsWith("http")) { url = getTitle().toString(); Log.w(TAG, "Current page is not started with http, resolve from current webView url: " + url); } if (webView != null && !Util.isStringNullOrEmpty(url)) webView.saveMyWebArchive(url); return true; case R.id.menu_browser_back: if (webView != null && webView.canGoBack()) { // only good for android 4.4++ webView.goBack(); } return true; case android.R.id.home: finish(); return true; case R.id.menu_fullscreen: isFullscreen = !isFullscreen; SharedPreferences.Editor edit = PreferenceManager.getDefaultSharedPreferences(this).edit(); edit.putBoolean(Constants.PREF_FULSCREEN, isFullscreen); edit.commit(); _uih.toggleFullscreen(isFullscreen); return true; default: return super.onOptionsItemSelected(item); } } @Override public void onBackPressed() { if (isTaskRoot()) { startActivity(new Intent(this, MainActivity.class)); finish(); } else { super.onBackPressed(); } } // region Volume key scrolling @Override public boolean onKeyDown(int keyCode, KeyEvent event) { boolean useVolumeRocker = PreferenceManager.getDefaultSharedPreferences(this).getBoolean(Constants.PREF_USE_VOLUME_FOR_SCROLL, false); if (useVolumeRocker) { int scrollSize = UIHelper.getIntFromPreferences(Constants.PREF_SCROLL_SIZE, 5) * 100; boolean invertScroll = PreferenceManager.getDefaultSharedPreferences(this).getBoolean(Constants.PREF_INVERT_SCROLL, false); if (invertScroll) scrollSize = scrollSize * -1; NonLeakingWebView webView = (NonLeakingWebView) findViewById(R.id.webViewContent); if (keyCode == KeyEvent.KEYCODE_VOLUME_UP) { webView.flingScroll(0, -scrollSize); Log.d("Volume", "Up Pressed"); return true; } else if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN) { webView.flingScroll(0, +scrollSize); Log.d("Volume", "Down Pressed"); return true; } else return super.onKeyDown(keyCode, event); } return super.onKeyDown(keyCode, event); } @Override public boolean onKeyUp(int keyCode, KeyEvent event) { if ((keyCode == KeyEvent.KEYCODE_VOLUME_UP) || (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN)) { boolean useVolumeRocker = PreferenceManager.getDefaultSharedPreferences(this).getBoolean(Constants.PREF_USE_VOLUME_FOR_SCROLL, false); if (!useVolumeRocker) { return false; } return true; } return super.onKeyUp(keyCode, event); } // endregion /** * Update last read chapter and the position. * Run async */ @SuppressWarnings("deprecation") public void setLastReadState() { final NonLeakingWebView wv = (NonLeakingWebView) findViewById(R.id.webViewContent); // get the values from UI Thread due to Android restriction. final float currentScale = wv.getScale(); final int lastY = wv.getScrollY() + wv.getBottom(); final int contentHeight = wv.getContentHeight(); // bug handling for Issue#213 // for some reason, the setLastReadState is called twice in landscape mode. if (contentHeight == 0) return; new Thread(new Runnable() { @Override public void run() { if (currPageModel == null) return; if (contentUserData == null) { contentUserData = new NovelContentUserModel(); contentUserData.setPage(currPageModel.getPage()); updateCurrentPageModel(null, null, contentUserData); } updateCurrentPageModel(null, null, contentUserData); checkLastYAndScale(); checkIsReadComplete(); try { NovelsDao.getInstance().updateNovelContentUserModel(contentUserData, null); Log.d(TAG, "Update Content:X=" + contentUserData.getLastXScroll() + ":Y=" + contentUserData.getLastYScroll() + ":Z=" + contentUserData.getLastZoom()); } catch (Exception ex) { Log.e(TAG, ex.getMessage(), ex); } String lastPage = saveLastReadChapter(); Log.i(TAG, "Last Read State Update complete: " + lastPage); } private void checkLastYAndScale() { contentUserData.setLastZoom(currentScale); // save zoom level, position is updated from updateLastLine() // for external page, use the px if (content == null) { contentUserData.setLastYScroll(wv.getScrollY()); } } private String saveLastReadChapter() { // save for jump to last read. SharedPreferences sharedPrefs = PreferenceManager.getDefaultSharedPreferences(LNReaderApplication.getInstance()); SharedPreferences.Editor editor = sharedPrefs.edit(); String lastPage; if (currPageModel.isExternal()) { lastPage = currPageModel.getPage(); } else if (content != null) { lastPage = content.getPage(); } else { lastPage = getIntent().getStringExtra(Constants.EXTRA_PAGE); } editor.putString(Constants.PREF_LAST_READ, lastPage); editor.commit(); return lastPage; } private void checkIsReadComplete() { // pixel, round to the nearest double isReadThreshold = (contentHeight * currentScale) - currentScale; try { if (isReadThreshold <= lastY && !currPageModel.getPage().endsWith("&action=edit&redlink=1")) { currPageModel.setFinishedRead(true); } Log.i(TAG, "Complete Read PageModel for Content: " + currPageModel.getPage() + " check value=" + isReadThreshold + " <= YPix=" + lastY + " ==> " + currPageModel.isFinishedRead()); NovelsDao.getInstance().updatePageModel(currPageModel); } catch (Exception ex) { Log.e(TAG, "Error updating PageModel for Content: " + currPageModel.getPage(), ex); } } }).start(); } /** * Load chapter from DB * * @param pageModel * @param refresh */ @SuppressLint("NewApi") private void executeTask(PageModel pageModel, boolean refresh) { NonLeakingWebView webView = (NonLeakingWebView) findViewById(R.id.webViewContent); if (pageModel.isExternal()) { loadExternalUrl(pageModel, refresh); } else { isPageLoaded = false; task = new LoadNovelContentTask(pageModel, refresh, this); String key = TAG + ":" + pageModel.getPage(); boolean isAdded = LNReaderApplication.getInstance().addTask(key, task); if (isAdded) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); else task.execute(); } else { if (getColorPreferences(this)) webView.loadData("<p style='background: black; color: white;'>" + getResources().getString(R.string.background_task_load) + "</p>", "text/html", "utf-8"); else webView.loadData("<p style='background: white; color: black;'>" + getResources().getString(R.string.background_task_load) + "</p>", "text/html", "utf-8"); LoadNovelContentTask tempTask = (LoadNovelContentTask) LNReaderApplication.getInstance().getTask(key); if (tempTask != null) { task = tempTask; task.owner = this; } toggleProgressBar(true, ""); } } setPrevNextButtonState(pageModel); updateCurrentPageModel(pageModel, null, null); } /** * Load chapter for external url (not hosted in Baka-Tsuki). * Used local cache if available (wac/mht). * * @param pageModel * @param refresh */ public void loadExternalUrl(PageModel pageModel, boolean refresh) { try { // check if .wac available String url = pageModel.getPage(); String wacName = Util.getSavedWacName(url); final NonLeakingWebView wv = (NonLeakingWebView) findViewById(R.id.webViewContent); final BakaTsukiWebViewClient client = (BakaTsukiWebViewClient) wv.getWebViewClient(); // available if (!Util.isStringNullOrEmpty(wacName) && !refresh) { client.setExternalNeedSave(false); String[] urlParts = url.split("#", 2); if (urlParts.length == 2) { executeLoadWacTask(wacName, urlParts[1], url); } else executeLoadWacTask(wacName, "", url); } else { // delete if refresh if (refresh) { Toast.makeText(this, "Refreshing WAC: " + wacName, Toast.LENGTH_SHORT).show(); // delete the WAC file File f = new File(wacName); if (f.exists()) f.delete(); Log.i(TAG, "Refreshing WAC: " + wacName); } else { Log.w(TAG, "WAC not available: " + wacName); } client.setExternalNeedSave(true); setWebViewSettings(); wv.loadUrl(url); Intent currIntent = this.getIntent(); currIntent.putExtra(Constants.EXTRA_PAGE, Util.SanitizeBaseUrl(url, false)); currIntent.putExtra(Constants.EXTRA_PAGE_IS_EXTERNAL, true); // sanitize here after redirect pageModel.setPage(Util.SanitizeBaseUrl(url, false)); } setChapterTitle(pageModel); buildTOCMenu(pageModel); content = null; contentUserData = getContentUserData(pageModel.getPage()); updateCurrentPageModel(pageModel, content, null); } catch (Exception ex) { Log.e(TAG, "Cannot load external content: " + pageModel.getPage(), ex); } } /** * Load saved external chapter from wac/mht * * @param wacName */ @SuppressLint({"InlinedApi", "NewApi"}) private void executeLoadWacTask(String wacName, String anchorLink, String historyUrl) { final NonLeakingWebView webView = (NonLeakingWebView) findViewById(R.id.webViewContent); final BakaTsukiWebViewClient client = (BakaTsukiWebViewClient) webView.getWebViewClient(); LoadWacTask task = new LoadWacTask(this, webView, wacName, client, anchorLink, historyUrl); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); else task.execute(); } /** * Setup the chapter from DB, Internal page only * Including JS for Bookmark highlighting, last read position, and CSS * * @param loadedContent */ @SuppressLint("NewApi") private void setContent(NovelContentModel loadedContent) { this.content = loadedContent; try { PageModel pageModel = content.getPageModel(); contentUserData = getContentUserData(content.getPage()); updateCurrentPageModel(pageModel, content, contentUserData); if (content.getLastUpdate().getTime() < pageModel.getLastUpdate().getTime()) Toast.makeText(this, getResources().getString(R.string.content_may_updated, content.getLastUpdate().toString(), pageModel.getLastUpdate().toString()), Toast.LENGTH_LONG).show(); // load the contents here final NonLeakingWebView wv = (NonLeakingWebView) findViewById(R.id.webViewContent); setWebViewSettings(); int lastPos = contentUserData.getLastYScroll(); int pIndex = getIntent().getIntExtra(Constants.EXTRA_P_INDEX, -1); if (pIndex > 0) lastPos = pIndex; if (contentUserData.getLastZoom() > 0) { wv.setInitialScale((int) (contentUserData.getLastZoom() * 100)); } else { wv.setInitialScale(this.getResources().getInteger(R.integer.default_zoom)); } StringBuilder html = new StringBuilder(); html.append("<html><head>"); html.append(DisplayNovelContentHtmlHelper.getCSSSheet()); html.append(DisplayNovelContentHtmlHelper.getViewPortMeta()); html.append(DisplayNovelContentHtmlHelper.prepareJavaScript(lastPos, content.getBookmarks(), getBookmarkPreferences())); html.append("</head><body onclick='toogleHighlight(this, event);' onload='setup();'>"); html.append(content.getContent()); html.append("</body></html>"); wv.loadDataWithBaseURL(UIHelper.getBaseUrl(this), html.toString(), "text/html", "utf-8", NonLeakingWebView.PREFIX_PAGEMODEL + content.getPage()); setChapterTitle(pageModel); Log.d(TAG, "Load Content:X=" + contentUserData.getLastXScroll() + ":Y=" + contentUserData.getLastYScroll() + ":Z=" + contentUserData.getLastZoom()); buildTOCMenu(pageModel); buildBookmarkMenu(); invalidateOptionsMenu(); Log.d(TAG, "Loaded: " + content.getPage()); Intent currIntent = this.getIntent(); currIntent.putExtra(Constants.EXTRA_PAGE, content.getPage()); currIntent.putExtra(Constants.EXTRA_PAGE_IS_EXTERNAL, false); } catch (Exception e) { Log.e(TAG, "Cannot load content.", e); } } // region webView related method /** * Setup webView */ @SuppressLint({"NewApi", "SetJavaScriptEnabled"}) private void setWebViewSettings() { NonLeakingWebView wv = (NonLeakingWebView) findViewById(R.id.webViewContent); wv.getSettings().setAllowFileAccess(true); wv.getSettings().setSupportZoom(UIHelper.getZoomPreferences(this)); wv.getSettings().setBuiltInZoomControls(UIHelper.getZoomPreferences(this)); wv.setDisplayZoomControl(UIHelper.getZoomControlPreferences(this)); wv.getSettings().setLoadWithOverviewMode(true); // wv.getSettings().setUseWideViewPort(true); wv.getSettings().setLoadsImagesAutomatically(getShowImagesPreferences()); if (getColorPreferences(this)) wv.setBackgroundColor(0); wv.getSettings().setJavaScriptEnabled(true); if (isPageLoaded) wv.loadUrl("javascript:toogleEnableBookmark(" + getBookmarkPreferences() + ")"); } /** * AsyncTask complete handler for: * - Novel Content saved in DB * - External Chapter */ @Override public void onCompleteCallback(ICallbackEventData message, AsyncTaskResult<?> result) { Exception e = result.getError(); if (e == null) { if (result.getResultType() == NovelContentModel.class) { NovelContentModel loadedContent = (NovelContentModel) result.getResult(); synchronized (this) { try { loadedContent.refreshPageModel(); // ensuring pageModel to be refreshed setContent(loadedContent); } catch (Exception e1) { Log.e(TAG, "Cannot load content.", e); } } } else if (result.getResultType() == Boolean.class) { // Load WAC Toast.makeText(this, message.getMessage(), Toast.LENGTH_SHORT).show(); boolean res = (Boolean) result.getResult(); if (!res) { String page = getIntent().getStringExtra(Constants.EXTRA_PAGE); PageModel p = new PageModel(page); try { contentUserData = getContentUserData(page); updateCurrentPageModel(null, null, contentUserData); } catch (Exception ex) { Log.e(TAG, ex.getMessage(), ex); } loadExternalUrl(p, true); } else { try { contentUserData = getContentUserData(currPageModel.getPage()); updateCurrentPageModel(null, null, contentUserData); final NonLeakingWebView webView = (NonLeakingWebView) findViewById(R.id.webViewContent); final BakaTsukiWebChromeClient chromeClient = (BakaTsukiWebChromeClient) webView.getWebChromeClient(); chromeClient.setScrollY(contentUserData.getLastYScroll()); } catch (Exception ex) { Log.e(TAG, ex.getMessage(), ex); } } } else { Log.w(TAG, "Unexpected result: " + result.getResultType().getName()); } } else { Log.e(TAG, "Error when loading novel content: " + e.getMessage(), e); Toast.makeText(this, e.getClass().toString() + ": " + e.getMessage(), Toast.LENGTH_SHORT).show(); } toggleProgressBar(false, null); } private NovelContentUserModel getContentUserData(String page) throws Exception { NovelContentUserModel temp = NovelsDao.getInstance().getNovelContentUserModel(page, null); if (temp == null) { temp = new NovelContentUserModel(); temp.setPage(page); } return temp; } /** * Used by ChromeClient to receive js update event for y-scrolling * * @param pIndex */ public void updateLastLine(int pIndex) { try { if (contentUserData == null) contentUserData = getContentUserData(currPageModel.getPage()); contentUserData.setLastYScroll(pIndex); updateCurrentPageModel(null, null, contentUserData); } catch (Exception ex) { Log.e(TAG, "updateLastLine(): " + ex.getMessage(), ex); } } /** * Used to move to the last read position upon receiving load complete event from webView client */ public void notifyLoadComplete() { NonLeakingWebView webView = (NonLeakingWebView) findViewById(R.id.webViewContent); isPageLoaded = true; if (webView != null && content != null) { final NonLeakingWebView _webView = webView; // move to last read paragraph, delay after webView load the pages. _webView.postDelayed(new Runnable() { @Override public void run() { try { int y = getIntent().getIntExtra(Constants.EXTRA_P_INDEX, contentUserData.getLastYScroll()); Log.d(TAG, "notifyLoadComplete(): Move to the saved pos: " + y); _webView.loadUrl("javascript:goToParagraph(" + y + ")"); } catch (NullPointerException ex) { Log.i(TAG, "Failed to load the content"); } } }, UIHelper.getIntFromPreferences(Constants.PREF_KITKAT_WEBVIEW_FIX_DELAY, 500) + 100); } } // endregion @Override public boolean downloadListSetup(String id, String toastText, int type, boolean hasError) { Log.d(TAG, "Setup of " + id + ": " + toastText + " (type: " + type + ")" + "hasError: " + hasError); return false; } // region PREFERENCES private boolean getShowImagesPreferences() { return PreferenceManager.getDefaultSharedPreferences(this).getBoolean(Constants.PREF_SHOW_IMAGE, true); } public boolean getFullscreenPreferences() { return PreferenceManager.getDefaultSharedPreferences(this).getBoolean(Constants.PREF_FULSCREEN, false); } private boolean getBookmarkPreferences() { return PreferenceManager.getDefaultSharedPreferences(this).getBoolean(Constants.PREF_ENABLE_BOOKMARK, true); } private boolean getHandleExternalLinkPreferences() { return PreferenceManager.getDefaultSharedPreferences(this).getBoolean(Constants.PREF_USE_INTERNAL_WEBVIEW, false); } // endregion // region Progress bar related @Override public void onProgressCallback(ICallbackEventData message) { toggleProgressBar(true, message.getMessage()); } public void toggleProgressBar(boolean show, String message) { NonLeakingWebView webView = (NonLeakingWebView) findViewById(R.id.webViewContent); TextView loadingText = (TextView) findViewById(R.id.emptyList); ProgressBar loadingBar = (ProgressBar) findViewById(R.id.loadProgress); if (webView == null || loadingBar == null || loadingText == null) return; synchronized (this) { if (show) { loadingText.setVisibility(TextView.VISIBLE); loadingText.setText(message); loadingBar.setVisibility(ProgressBar.VISIBLE); webView.setVisibility(ListView.GONE); } else { loadingText.setVisibility(TextView.GONE); loadingBar.setVisibility(ProgressBar.GONE); webView.setVisibility(ListView.VISIBLE); } } } // endregion // region Search box @SuppressWarnings("deprecation") private void showSearchBox() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { NonLeakingWebView webView = (NonLeakingWebView) findViewById(R.id.webViewContent); webView.showFindDialog("", true); } else { RelativeLayout searchBox = (RelativeLayout) findViewById(R.id.searchBox); searchBox.setVisibility(View.VISIBLE); } } public void searchNext(View view) { NonLeakingWebView webView = (NonLeakingWebView) findViewById(R.id.webViewContent); webView.findNext(true); } public void searchPrev(View view) { NonLeakingWebView webView = (NonLeakingWebView) findViewById(R.id.webViewContent); webView.findNext(false); } public void closeSearchBox(View view) { NonLeakingWebView webView = (NonLeakingWebView) findViewById(R.id.webViewContent); _uih.closeSearchBox(webView); } // endregion // region Top-Down button public void toggleTopButton(boolean enable) { _uih.toggleTopButton(enable); } public void toggleBottomButton(boolean enable) { _uih.toggleBottomButton(enable); } public void goTop(View view) { NonLeakingWebView webView = (NonLeakingWebView) findViewById(R.id.webViewContent); webView.pageUp(true); } public void goBottom(View view) { NonLeakingWebView webView = (NonLeakingWebView) findViewById(R.id.webViewContent); webView.pageDown(true); } // endregion // region TTS methods private boolean getTtsStopOnPause() { return PreferenceManager.getDefaultSharedPreferences(this).getBoolean(Constants.PREF_TTS_TTS_STOP_ON_LOST_FOCUS, true); } @Override public void onInit(int status) { invalidateOptionsMenu(); } @Override public void onComplete(Object i, Class<?> source) { Log.d(TAG, "Data: " + i + " from: " + source.getCanonicalName()); if (i != null && source == TtsHelper.class) { NonLeakingWebView webView = (NonLeakingWebView) findViewById(R.id.webViewContent); _tts.autoScroll(webView, i.toString()); } } public void sendHtmlForSpeak(String html) { _tts.start(html, contentUserData.getLastYScroll()); } // endregion // region private methods // region bookmark handler /** * Build Bookmarks-on-Chapter menu */ public void buildBookmarkMenu() { if (content != null) { try { int resourceId = R.layout.item_bookmark; if (UIHelper.isSmallScreen(this)) { resourceId = R.layout.item_bookmark_small; } final BookmarkModelAdapter bookmarkAdapter = new BookmarkModelAdapter(this, resourceId, content.getBookmarks(), content.getPageModel()); AlertDialog.Builder builder = new AlertDialog.Builder(this); builder.setTitle(getResources().getString(R.string.bookmarks)); builder.setAdapter(bookmarkAdapter, new OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { BookmarkModel bookmark = bookmarkAdapter.getItem(which); NonLeakingWebView wv = (NonLeakingWebView) findViewById(R.id.webViewContent); wv.loadUrl("javascript:goToParagraph(" + bookmark.getpIndex() + ")"); } }); builder.setNegativeButton(R.string.cancel, null); builder.setPositiveButton(R.string.menu_show_clear_all, new OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { int total = 0; for (BookmarkModel bookmark : content.getBookmarks()) { total += NovelsDao.getInstance().deleteBookmark(bookmark); NonLeakingWebView wv = (NonLeakingWebView) findViewById(R.id.webViewContent); wv.loadUrl("javascript:toogleHighlightById(" + bookmark.getpIndex() + ")"); } Toast.makeText(getBaseContext(), getString(R.string.toast_show_deleted_count, total), Toast.LENGTH_SHORT).show(); } }); bookmarkMenu = builder.create(); } catch (Exception e) { Log.e(TAG, "Error getting pageModel: " + e.getMessage(), e); } } } /** * Update Bookmark-on-Chapter data upon receiving event from webView client */ public void refreshBookmarkData() { if (bookmarkMenu != null) { BookmarkModelAdapter bookmarkAdapter = (BookmarkModelAdapter) bookmarkMenu.getListView().getAdapter(); if (bookmarkAdapter != null) { bookmarkAdapter.refreshData(); } } } // endregion // region TOC handler /** * Move between chapters * * @param page */ public void jumpTo(PageModel page) { setLastReadState(); _tts.stop(); Intent currIntent = this.getIntent(); currIntent.putExtra(Constants.EXTRA_PAGE, page.getPage()); currIntent.putExtra(Constants.EXTRA_PAGE_IS_EXTERNAL, page.isExternal()); // open external page as Intent to open browser if (page.isExternal() && !getHandleExternalLinkPreferences()) { try { Uri url = Uri.parse(page.getPage()); Intent browserIntent = new Intent(Intent.ACTION_VIEW, url); startActivity(browserIntent); } catch (Exception ex) { String message = getResources().getString(R.string.error_parsing_url, page.getPage()); Log.e(TAG, message, ex); Toast.makeText(this, message, Toast.LENGTH_SHORT).show(); } } else executeTask(page, false); } /** * Build Table-of-Contents * * @param referencePageModel */ private void buildTOCMenu(final PageModel referencePageModel) { Log.d(TAG, "Trying to create TOC"); try { BookModel book = referencePageModel.getBook(false); if (book != null) { ArrayList<PageModel> chapters = book.getChapterCollection(); for (PageModel chapter : chapters) { if (chapter.getPage().contentEquals(referencePageModel.getPage())) { chapter.setHighlighted(true); } else chapter.setHighlighted(false); } Log.d(TAG, "TOC Found: " + chapters.size()); final PageModelAdapter jumpAdapter = new PageModelAdapter(this, R.layout.item_jump_to, chapters); AlertDialog.Builder builder = new AlertDialog.Builder(this); builder.setTitle(getResources().getString(R.string.content_toc)); builder.setAdapter(jumpAdapter, new OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { PageModel page = jumpAdapter.getItem(which); jumpTo(page); } }); builder.setNegativeButton(R.string.cancel, null); builder.setPositiveButton(R.string.back_to_index, new OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { backToIndex(referencePageModel); } }); tocMenu = builder.create(); } } catch (Exception e) { Log.e(TAG, "Cannot get current page for TOC menu.", e); } } /** * Back to Novel Details */ public void backToIndex(PageModel referencePageModel) { try { PageModel pageModel = NovelsDao.getInstance().getExistingPageModel(referencePageModel, null).getParentPageModel(); Intent intent = new Intent(this, NovelListContainerActivity.class); intent.putExtra(Constants.EXTRA_ONLY_WATCHED, false); intent.putExtra(Constants.EXTRA_PAGE, pageModel.getPage()); this.startActivity(intent); finish(); } catch (Exception e) { Log.e(TAG, "Failed to get parent page model", e); } } // endregion /** * Used for floating button on fullscreen mode to open the menu. * * @param view */ public void openMenu(View view) { invalidateOptionsMenu(); openOptionsMenu(); } /** * Set activity title to current chapter title. * * @param pageModel */ private void setChapterTitle(PageModel pageModel) { String title = pageModel.getPage(); try { if (pageModel.getParent() != null) { Log.d(TAG, "Parent Page: " + pageModel.getParent()); novelDetails = NovelsDao.getInstance().getNovelDetails(pageModel.getParentPageModel(), null, false); String volume = pageModel.getParent().replace(pageModel.getParentPageModel().getPage() + Constants.NOVEL_BOOK_DIVIDER, ""); title = pageModel.getTitle() + " (" + volume + ")"; } } catch (Exception ex) { Log.e(TAG, "Error when setting title: " + ex.getMessage(), ex); } setTitle(title); } /** * Set Previous and Next button state. * * @param pageModel */ private void setPrevNextButtonState(PageModel pageModel) { if (_menu != null) { boolean isNextEnabled = false; boolean isPrevEnabled = false; try { PageModel prevPage = novelDetails.getPrev(pageModel.getPage(), UIHelper.getShowMissing(this), UIHelper.getShowRedlink(this)); if (prevPage != null) isPrevEnabled = true; } catch (Exception ex) { Log.e(TAG, "Failed to get prev chapter: " + pageModel.getPage(), ex); } try { PageModel nextPage = novelDetails.getNext(pageModel.getPage(), UIHelper.getShowMissing(this), UIHelper.getShowRedlink(this)); if (nextPage != null) isNextEnabled = true; } catch (Exception ex) { Log.e(TAG, "Failed to get next chapter: " + pageModel.getPage(), ex); } _menu.findItem(R.id.menu_chapter_next).setEnabled(isNextEnabled); _menu.findItem(R.id.menu_chapter_previous).setEnabled(isPrevEnabled); } } /** * Get invert color preferences * * @param ctx * @return true if dark theme. */ public static boolean getColorPreferences(Context ctx) { return PreferenceManager.getDefaultSharedPreferences(ctx).getBoolean(Constants.PREF_INVERT_COLOR, true); } // endregion }