com.pdftron.pdf.controls.ReflowPagerAdapter.java Source code

Java tutorial

Introduction

Here is the source code for com.pdftron.pdf.controls.ReflowPagerAdapter.java

Source

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