Java tutorial
//--------------------------------------------------------------------------------------- // Copyright (c) 2001-2016 by PDFTron Systems Inc. All Rights Reserved. // Consult legal.txt regarding legal and license information. //--------------------------------------------------------------------------------------- package com.pdftron.pdf.controls; import android.annotation.TargetApi; import android.content.Context; import android.content.Intent; import android.graphics.Color; import android.net.Uri; import android.net.http.SslError; import android.os.Build; import android.support.v4.view.PagerAdapter; import android.support.v4.view.ViewPager; import android.util.Log; import android.util.SparseArray; import android.view.MotionEvent; import android.view.ScaleGestureDetector; import android.view.View; import android.view.ViewGroup; import android.webkit.SslErrorHandler; import android.webkit.WebChromeClient; import android.webkit.WebSettings; import android.webkit.WebView; import android.webkit.WebViewClient; import android.widget.FrameLayout; import com.pdftron.pdf.PDFDoc; import com.pdftron.pdf.Page; import com.pdftron.pdf.ReflowProcessor; import com.pdftron.pdf.RequestHandler; import com.pdftron.pdf.utils.AnalyticsHandlerAdapter; import com.pdftron.pdf.utils.ReflowWebView; import com.pdftron.pdf.utils.Utils; import com.pdftron.pdf.tools.R; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; // NOTE: if the html file is very large, it may take time for viewpager to show the html content, // and therefore, before showing the content it may first show the content from another page public class ReflowPagerAdapter extends PagerAdapter implements ReflowWebView.ReflowWebViewCallback, RequestHandler.RequestHandlerCallback { private static final String TAG = "ReflowPagerAdapter"; private static final boolean DEBUG = true; private final static String NORMAL_MODE_LOADING_FILE = "file:///android_asset/loading_page.html"; private final static String NIGHT_MODE_LOADING_FILE = "file:///android_asset/loading_page_night.html"; private static boolean sIsNightMode = false; // night mode is set for all documents private RequestHandler mRequestHandler; private PDFDoc mDoc; private int mPageCount; private String mThemeName; private String mLoadingFile; private ViewPager mViewPager; // need it to get current page private SparseArray<String> mReflowFiles; private SparseArray<FrameLayout> mViewHolders; private SparseArray<Integer> mViewIndexes; private WebViewRepository mWebViewRepository; private boolean mIsRtlMode = false; /* * scaling parameters * NOTE: if change SCALES, you may also need to change mDefaultScaleIndex */ private static float[] SCALES = { 0.05f, 0.1f, 0.25f, 0.5f, 0.75f, 1.0f, 1.25f, 1.5f, 2.0f, 4.0f, 8.0f, 16.0f }; private final int mDefaultScaleIndex = 5; private final static int mMaxIndex = SCALES.length - 1; public final static int TH_MIN_SCAlE = Utils.isIceCreamSandwich() ? Math.round(SCALES[0] * 100) : 50; public final static int TH_MAX_SCAlE = Utils.isIceCreamSandwich() ? Math.round(SCALES[mMaxIndex] * 100) : 200; private final static float TH_SCAlE_GESTURE = 1.25f; private int mScaleIndex = mDefaultScaleIndex; private Context mContext; private WebSettings.TextSize mTextSize = WebSettings.TextSize.NORMAL; private float mScaleFactor; private float mLastScaleFactor; private float mThConsecutiveScales; private boolean mZoomInFlag; /* * call back interfaces for Tap */ public interface ReflowPagerAdapterCallback { void onReflowPagerSingleTapUp(MotionEvent event); } private class WebViewRepository { private ReflowWebView[] mWebViews = new ReflowWebView[9]; private boolean[] mAvailableFlags = new boolean[9]; // check if the corresponding WebView is currently available WebViewRepository(Context context) { // create persistently 9 WebViews (3 for view pager, 3 for recycling, and 3 for safe guard) for (int i = 0; i < 9; i++) { mWebViews[i] = new ReflowWebView(context); mAvailableFlags[i] = true; } } ReflowWebView[] getWebViews() { return mWebViews; } ReflowWebView getWebView(int index) { if (index < 0 || index >= 9) { AnalyticsHandlerAdapter.getInstance() .sendException(new Exception("Reflow: getWebView is called with an invalid index")); Log.e(TAG, "WebViewRepository.getWebView is called with an invalid index"); return null; } return mWebViews[index]; } /* get a free WebView' index from the repository */ int pop() { int i; for (i = 0; i < 9; i++) { if (mAvailableFlags[i] == true) { mAvailableFlags[i] = false; break; } } if (i == 9) { AnalyticsHandlerAdapter.getInstance() .sendException(new Exception("Reflow: there is no available WebView in repository")); Log.e(TAG, "there is no available WebView in repository"); } return i; } /* set this WebView available */ void push(int index) { if (index < 0 || index >= 9) { AnalyticsHandlerAdapter.getInstance() .sendException(new Exception("Reflow: push is called with an invalid index")); Log.e(TAG, "WebViewRepository.push is called with an invalid index"); } else { mAvailableFlags[index] = true; } } /* check if this WebView is available to use */ boolean isAvailable(int index) { if (index < 0 || index >= 9) { return false; } return mAvailableFlags[index]; } void reset() { for (int i = 0; i < 9; i++) { mAvailableFlags[i] = true; } } } private ReflowPagerAdapterCallback mCallback; public void setListener(ReflowPagerAdapterCallback listener) { mCallback = listener; } public ReflowPagerAdapter(ViewPager viewPager, PDFDoc doc, Context context) { mViewPager = viewPager; mDoc = doc; mContext = context; mRequestHandler = new RequestHandler(this); mThemeName = null; mPageCount = 0; try { mPageCount = mDoc.getPageCount(); } catch (Exception e) { e.printStackTrace(); } mReflowFiles = new SparseArray<>(mPageCount); mViewHolders = new SparseArray<>(mPageCount); mViewIndexes = new SparseArray<>(mPageCount); // NOTE: make sure that the off-screen page limit is set to one viewPager.setOffscreenPageLimit(1); mWebViewRepository = new WebViewRepository(mContext); } public void onPagesModified() { if (DEBUG) Log.d(TAG, "pages were modified."); mPageCount = 0; try { mPageCount = mDoc.getPageCount(); } catch (Exception e) { e.printStackTrace(); } mReflowFiles = new SparseArray<>(mPageCount); mViewHolders = new SparseArray<>(mPageCount); mViewIndexes = new SparseArray<>(mPageCount); } public void cleanup() { if (DEBUG) Log.d(TAG, "Cleanup"); ReflowProcessor.cancelAllRequests(); } public void initialize() { // set loading file depending on night mode if (sIsNightMode) { mLoadingFile = NIGHT_MODE_LOADING_FILE; } else { mLoadingFile = NORMAL_MODE_LOADING_FILE; } if (mWebViewRepository != null) { mWebViewRepository.reset(); } ReflowWebView[] webViews = mWebViewRepository.getWebViews(); for (final ReflowWebView webView : webViews) { webView.clearCache(true); // reset reading css file webView.getSettings().setJavaScriptEnabled(true); webView.setWillNotCacheDrawing(false); if (sIsNightMode) { webView.setBackgroundColor(Color.BLACK); } else { webView.setBackgroundColor(Color.WHITE); } webView.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); webView.loadUrl("about:blank"); webView.setListener(this); webView.setWebChromeClient(new WebChromeClient()); // enable the use of methods like alert in javascript webView.setWebViewClient(new WebViewClient() { // now all links the user clicks load in your WebView @Override public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) { super.onReceivedError(view, errorCode, description, failingUrl); Log.e(TAG, description + " url: " + failingUrl); } @Override public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) { super.onReceivedSslError(view, handler, error); Log.e(TAG, error.toString()); } @Override public boolean shouldOverrideUrlLoading(WebView view, String url) { if (url.startsWith("file:///") && url.endsWith(".html")) { int slashPos = url.lastIndexOf('/'); try { int objNum = Integer.parseInt(url.substring(slashPos + 1, url.length() - 5)); int pageNum = 0; for (int i = 1; i <= mPageCount; i++) { try { Page page = mDoc.getPage(i); if (page.getSDFObj().getObjNum() == objNum) { pageNum = i; break; } } catch (Exception e) { } } if (pageNum != 0) { mViewPager.setCurrentItem(pageNum - 1); } } catch (NumberFormatException e) { return true; } } else { if (url.startsWith("mailto:") || android.util.Patterns.EMAIL_ADDRESS.matcher(url).matches()) { if (url.startsWith("mailto:")) { url = url.substring(7); } Intent intent = new Intent(Intent.ACTION_SENDTO, Uri.fromParts("mailto", url, null)); mContext.startActivity(Intent.createChooser(intent, mContext.getResources().getString(R.string.tools_misc_sendemail))); } else { // ACTION_VIEW needs the address to have http or https if (!url.startsWith("https://") && !url.startsWith("http://")) { url = "http://" + url; } if (DEBUG) Log.d(TAG, url); Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url)); mContext.startActivity(Intent.createChooser(intent, mContext.getResources().getString(R.string.tools_misc_openwith))); } } return true; } }); } } public void setRightToLeftDirection(boolean isRtlMode) { mIsRtlMode = isRtlMode; Log.d("Reflow Right to Left", mIsRtlMode ? "True" : "False"); } public boolean isRightToLeftDirection() { return mIsRtlMode; } public void setNightMode(boolean isNightMode) { sIsNightMode = isNightMode; if (sIsNightMode) { mLoadingFile = NIGHT_MODE_LOADING_FILE; } else { mLoadingFile = NORMAL_MODE_LOADING_FILE; } if (mThemeName != null) { setTheme(); } ReflowWebView[] webViews = mWebViewRepository.getWebViews(); for (ReflowWebView webView : webViews) { webView.clearCache(true); // reset reading the css file webView.setWillNotCacheDrawing(false); if (sIsNightMode) { webView.setBackgroundColor(Color.BLACK); } else { webView.setBackgroundColor(Color.WHITE); } webView.loadUrl("about:blank"); } int curPosition = mViewPager.getCurrentItem(); mViewPager.setAdapter(this); // re-instantiate again to reload new data mViewPager.setCurrentItem(curPosition, false); } public boolean isNightMode() { return sIsNightMode; } private void setTheme() { File file = new File(mThemeName); try { if (file.exists()) { file.delete(); } file.createNewFile(); } catch (IOException e) { e.printStackTrace(); } OutputStream outputStream = null; try { outputStream = new FileOutputStream(mThemeName); String theme; if (sIsNightMode) { theme = "body{ -webkit-filter:invert(100%); filter:invert(100%);}"; } else { theme = ""; // NOTE: write invert(0%) make texts blurry in Android 4.4 } outputStream.write(theme.getBytes()); outputStream.flush(); outputStream.close(); } catch (IOException e) { e.printStackTrace(); } finally { try { if (outputStream != null) { outputStream.close(); } } catch (IOException e) { e.printStackTrace(); } } } private void setTextSizeLarger() { switch (mTextSize) { case LARGEST: break; case LARGER: mTextSize = WebSettings.TextSize.LARGEST; break; case NORMAL: mTextSize = WebSettings.TextSize.LARGER; break; case SMALLER: mTextSize = WebSettings.TextSize.NORMAL; break; case SMALLEST: mTextSize = WebSettings.TextSize.SMALLER; break; } } private void setTextSizeSmaller() { switch (mTextSize) { case LARGEST: mTextSize = WebSettings.TextSize.LARGER; break; case LARGER: mTextSize = WebSettings.TextSize.NORMAL; break; case NORMAL: mTextSize = WebSettings.TextSize.SMALLER; break; case SMALLER: mTextSize = WebSettings.TextSize.SMALLEST; break; case SMALLEST: break; } } public void setTextSizeInPercent(int textSize) { if (Utils.isIceCreamSandwich()) { mScaleIndex = mDefaultScaleIndex; for (int i = 0; i <= mMaxIndex; i++) { if (textSize == Math.round(SCALES[i] * 100)) { mScaleIndex = i; return; } } } else { switch (textSize) { case 200: mTextSize = WebSettings.TextSize.LARGEST; case 150: mTextSize = WebSettings.TextSize.LARGER; case 100: mTextSize = WebSettings.TextSize.NORMAL; case 75: mTextSize = WebSettings.TextSize.SMALLER; case 50: mTextSize = WebSettings.TextSize.SMALLEST; default: mTextSize = WebSettings.TextSize.NORMAL; } } } public int getTextSizeInPercent() { if (Utils.isIceCreamSandwich()) { return Math.round(SCALES[mScaleIndex] * 100); } else { switch (mTextSize) { case LARGEST: return 200; case LARGER: return 150; case NORMAL: return 100; case SMALLER: return 75; case SMALLEST: return 50; default: return 100; } } } public void zoomIn() { if (Utils.isIceCreamSandwich()) { if (mScaleIndex == mMaxIndex) { return; } mScaleIndex++; } else { if (mTextSize == WebSettings.TextSize.LARGEST) { return; } setTextSizeLarger(); } setTextZoomForAllView(); } public void zoomOut() { if (Utils.isIceCreamSandwich()) { if (mScaleIndex == 0) { return; } mScaleIndex--; } else { if (mTextSize == WebSettings.TextSize.SMALLEST) { return; } setTextSizeSmaller(); } setTextZoomForAllView(); } private void setTextZoomForAllView() { ReflowWebView[] webViews = mWebViewRepository.getWebViews(); for (ReflowWebView webView : webViews) { setTextZoom(webView); } } @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH) private void setTextZoom(ReflowWebView webView) { if (webView != null) { if (Utils.isIceCreamSandwich()) { webView.getSettings().setTextZoom(Math.round(SCALES[mScaleIndex] * 100)); } else { webView.getSettings().setTextSize(mTextSize); } } } @Override public void startUpdate(ViewGroup container) { } @Override public int getCount() { return mPageCount; } @Override public boolean isViewFromObject(View view, Object object) { return view == object; } @Override public void destroyItem(ViewGroup container, int position, Object object) { FrameLayout frameLayout = (FrameLayout) object; if (DEBUG) Log.d(TAG, "Removing page #" + (position + 1)); frameLayout.removeAllViews(); Integer indexI = mViewIndexes.get(position); if (indexI != null) { int index = indexI; ReflowWebView webView = mWebViewRepository.getWebView(index); webView.loadUrl("about:blank"); mWebViewRepository.push(index); } else { if (DEBUG) Log.d(TAG, "calling destroyItem for second time at one position"); } mViewHolders.put(position, null); mViewIndexes.put(position, null); container.removeView(frameLayout); } @Override public Object instantiateItem(final ViewGroup container, final int position) { int availIndex = mWebViewRepository.pop(); ReflowWebView webView = mWebViewRepository.getWebView(availIndex); if (webView == null) { AnalyticsHandlerAdapter.getInstance() .sendException(new Exception("Reflow: there is no available WebView in repository")); Log.e(TAG, "there is no available WebView in repository"); return null; } webView.loadUrl("about:blank"); int pagePosition = mIsRtlMode ? mPageCount - 1 - position : position; boolean need_load_flag = true; String filename = mReflowFiles.get(pagePosition); if (filename != null) { File file = new File(filename); if (file.exists()) { need_load_flag = false; if (DEBUG) Log.d(TAG, "the file at page #" + (position + 1) + " already received"); webView.loadUrl("file:///" + filename); setTextZoom(webView); } } if (need_load_flag) { webView.loadUrl(mLoadingFile); try { // request for reflow output if (DEBUG) Log.d(TAG, "request for page #" + (pagePosition + 1)); Page page = mDoc.getPage(pagePosition + 1); ReflowProcessor.getReflow(page, mRequestHandler, position); } catch (Exception e) { e.printStackTrace(); } } FrameLayout layout = new FrameLayout(mContext); FrameLayout parent = (FrameLayout) webView.getParent(); if (parent != null) { // note that we share WebViews, // so before adding a WebView make sure it's not a child of any layout parent.removeAllViews(); } layout.addView(webView); mViewHolders.put(position, layout); mViewIndexes.put(position, new Integer(availIndex)); container.addView(layout); return layout; } @Override public void RequestHandlerProc(int result, String out_filename, Object custom_data) { int position = (int) custom_data; int pagePosition = mIsRtlMode ? mPageCount - 1 - position : position; Integer indexI = mViewIndexes.get(position); if (indexI == null) { if (result == RequestHandler.JOB_REQUEST_RESULT_SUCCESS) { // save data for later use if (DEBUG) Log.d(TAG, "received page #" + (position + 1)); mReflowFiles.put(pagePosition, out_filename); } // had destroyed before the result was received return; } int index = indexI; ReflowWebView webView = mWebViewRepository.getWebView(index); if (webView == null) { AnalyticsHandlerAdapter.getInstance().sendException(new Exception("Reflow: The WebView has been lost")); Log.e(TAG, "The WebView at position " + position + " has been lost"); return; } if (mWebViewRepository.isAvailable(index)) { // had destroyed before the result was received AnalyticsHandlerAdapter.getInstance().sendException( new Exception("Reflow: Repository claims the WebView is available while it shouldn't be")); Log.e(TAG, "Repository claims the WebView is available while it shouldn't be"); return; } if (result == RequestHandler.JOB_REQUEST_RESULT_FAILURE) { // if there is an error in parsing show a blank page if (DEBUG) Log.d(TAG, "error in parsing page " + out_filename + " (" + (position + 1) + ")"); webView.loadUrl("about:blank"); return; } if (result == RequestHandler.JOB_REQUEST_RESULT_CANCEL) { if (DEBUG) Log.d(TAG, "cancelled parsing page " + out_filename + " (" + (position + 1) + ")"); webView.loadUrl("about:blank"); return; } if (DEBUG) Log.d(TAG, "received page #" + (position + 1)); mReflowFiles.put(pagePosition, out_filename); // crate the theme file inside the path of current document if (mThemeName == null) { File file = new File(out_filename); mThemeName = file.getParent() + "/theme.css"; setTheme(); } if (webView != null) { webView.loadUrl("file:///" + out_filename); setTextZoom(webView); } } @Override public boolean onReflowWebViewScaleBegin(ScaleGestureDetector detector) { mScaleFactor = mLastScaleFactor = SCALES[mScaleIndex]; mThConsecutiveScales = TH_SCAlE_GESTURE; mZoomInFlag = true; return true; } @Override public boolean onReflowWebViewScale(ScaleGestureDetector detector) { if (!Utils.isIceCreamSandwich()) { // not implement scale gesture for Android API prior to Ice Cream Sandwith return false; } mScaleFactor *= detector.getScaleFactor(); // avoid considering a small gesture as scaling if (Math.max(mLastScaleFactor / mScaleFactor, mScaleFactor / mLastScaleFactor) < TH_SCAlE_GESTURE) { return true; } if (mZoomInFlag && mScaleFactor > mLastScaleFactor && mScaleFactor / mLastScaleFactor < mThConsecutiveScales) { return true; } if (!mZoomInFlag && mLastScaleFactor > mScaleFactor && mLastScaleFactor / mScaleFactor < mThConsecutiveScales) { return true; } if (mLastScaleFactor > mScaleFactor) { if (mScaleIndex > 0) { mScaleIndex--; mLastScaleFactor = mScaleFactor = SCALES[mScaleIndex]; if (mZoomInFlag) { mThConsecutiveScales = TH_SCAlE_GESTURE; } mZoomInFlag = false; } } else { if (mScaleIndex < mMaxIndex) { mScaleIndex++; mLastScaleFactor = mScaleFactor = SCALES[mScaleIndex]; if (!mZoomInFlag) { mThConsecutiveScales = TH_SCAlE_GESTURE; } mZoomInFlag = true; } } int position = mViewPager.getCurrentItem(); Integer indexI = mViewIndexes.get(position); if (indexI == null) { AnalyticsHandlerAdapter.getInstance() .sendException(new Exception("Reflow: An error happened trying to retrieve a WebView")); Log.e(TAG, "An error happened trying to get WebView at position " + position); return false; } int index = indexI; ReflowWebView webView = mWebViewRepository.getWebView(index); if (webView == null || mWebViewRepository.isAvailable(index)) { AnalyticsHandlerAdapter.getInstance() .sendException(new Exception("Reflow: An error happened trying to retrieve a WebView")); Log.e(TAG, "An error happened trying to get WebView at position " + position); return false; } setTextZoom(webView); mThConsecutiveScales *= TH_SCAlE_GESTURE; return true; } @Override public void onReflowWebViewScaleEnd(ScaleGestureDetector detector) { setTextZoomForAllView(); } @Override public void onReflowWebViewSingleTapUp(MotionEvent event) { if (mCallback != null) { mCallback.onReflowPagerSingleTapUp(event); } } }