public class

PdfViewer

extends LoadingViewer

Gradle dependencies

compile group: 'androidx.pdf', name: 'pdf-viewer', version: '1.0.0-alpha02'

  • groupId: androidx.pdf
  • artifactId: pdf-viewer
  • version: 1.0.0-alpha02

Artifact androidx.pdf:pdf-viewer:1.0.0-alpha02 it located at Google repository (https://maven.google.com/)

Overview

A Viewer that can display paginated PDFs. Each page is rendered in its own View. Rendering is done in 2 passes:

  1. Layout: Request the dimensions of the page and set them as measure for the image view,
  2. Render: Create bitmap(s) at adequate dimensions and attach them to the page view.

The layout pass is progressive: starts with a few first pages of the document, then reach further as the user scrolls down (and ultimately spans the whole document). The rendering pass is tightly limited to the currently visible pages. Pages that are scrolled past (become not visible) have their bitmaps released to free up memory.

This is a LoadingViewer.SELF_MANAGED_CONTENTS Viewer: its contents and internal models are kept when the view is destroyed, and re-used when the view is re-created.

Major lifecycle events include:

  1. PdfViewer.onContentsAvailable(DisplayData, Bundle) / PdfViewer.onDestroy() : Content model is created.
  2. PdfViewer.onCreateView(LayoutInflater, ViewGroup, Bundle) / PdfViewer.destroyView() : All views are created, the pdf service is connected.

Summary

Fields
public final PdfLoaderCallbacksmPdfLoaderCallbacks

Callbacks of PDF loading asynchronous tasks.

from LoadingViewermFetcher, SELF_MANAGED_CONTENTS
from ViewerKEY_DATA, mContainer, mIsPasswordProtected, mViewState
from FragmentmPreviousWho
Constructors
publicPdfViewer()

Methods
public voidconfigureShareScroll(boolean left, boolean right, boolean top, boolean bottom)

Configures whether this viewer has to share scroll gestures in any direction with its container or any neighbouring view.

protected voiddestroyView()

Called when this viewer no longer needs (or has) a view.

public static ScreengetScreen()

protected voidhandleError()

public voidhideSpinner()

Hide the loading spinner.

public voidloadFile(Uri fileUri)

Load the PDF document.

public voidonActivityCreated(Bundle savedInstanceState)

Called when the fragment's activity has been created and this fragment's view hierarchy instantiated.

protected abstract voidonContentsAvailable(DisplayData contents, Bundle savedState)

Callback called when the full contents is re-loaded.

public voidonCreate(Bundle savedInstanceState)

Called to do initial creation of a fragment.

public ViewonCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)

Called to have the fragment instantiate its user interface view.

public voidonDestroy()

Called when the fragment is no longer in use.

public voidonDestroyView()

Called when the view previously created by Fragment.onCreateView(LayoutInflater, ViewGroup, Bundle) has been detached from the fragment.

protected voidonEnter()

Called after this viewer enters the screen and becomes visible.

protected voidonExit()

Called after this viewer exits the screen and becomes invisible to the user.

public voidonSaveInstanceState(Bundle outState)

Called to ask the fragment to save its current dynamic state, so it can later be reconstructed in a new instance if its process is restarted.

public PdfViewersetExitOnPasswordCancel(boolean shouldExitOnPasswordCancel)

If set, this viewer will finish the attached activity when the user presses cancel on the prompt for the document password.

public voidsetPassword(java.lang.String password)

public voidsetPasswordCancelError()

Create callback to retry password input when user cancels password prompt.

public PdfViewersetQuitOnError(boolean quit)

If set, this Viewer will call if it can't load the PDF.

public static voidsetScreenForTest(Context context)

public voidshowSpinner()

Show the loading spinner.

from LoadingViewerfeed, onStart, onStop, postContentsAvailable, restoreContents, setFetcher
from Viewerfinalize, isShowing, isStarted, participateInAccessibility, postEnter, saveToArguments, viewState
from Fragmentdump, equals, getActivity, getAllowEnterTransitionOverlap, getAllowReturnTransitionOverlap, getArguments, getChildFragmentManager, getContext, getDefaultViewModelCreationExtras, getDefaultViewModelProviderFactory, getEnterTransition, getExitTransition, getFragmentManager, getHost, getId, getLayoutInflater, getLayoutInflater, getLifecycle, getLoaderManager, getParentFragment, getParentFragmentManager, getReenterTransition, getResources, getRetainInstance, getReturnTransition, getSavedStateRegistry, getSharedElementEnterTransition, getSharedElementReturnTransition, getString, getString, getTag, getTargetFragment, getTargetRequestCode, getText, getUserVisibleHint, getView, getViewLifecycleOwner, getViewLifecycleOwnerLiveData, getViewModelStore, hashCode, hasOptionsMenu, instantiate, instantiate, isAdded, isDetached, isHidden, isInLayout, isMenuVisible, isRemoving, isResumed, isStateSaved, isVisible, onActivityResult, onAttach, onAttach, onAttachFragment, onConfigurationChanged, onContextItemSelected, onCreateAnimation, onCreateAnimator, onCreateContextMenu, onCreateOptionsMenu, onDestroyOptionsMenu, onDetach, onGetLayoutInflater, onHiddenChanged, onInflate, onInflate, onLowMemory, onMultiWindowModeChanged, onOptionsItemSelected, onOptionsMenuClosed, onPause, onPictureInPictureModeChanged, onPrepareOptionsMenu, onPrimaryNavigationFragmentChanged, onRequestPermissionsResult, onResume, onViewCreated, onViewStateRestored, postponeEnterTransition, postponeEnterTransition, registerForActivityResult, registerForActivityResult, registerForContextMenu, requestPermissions, requireActivity, requireArguments, requireContext, requireFragmentManager, requireHost, requireParentFragment, requireView, setAllowEnterTransitionOverlap, setAllowReturnTransitionOverlap, setArguments, setEnterSharedElementCallback, setEnterTransition, setExitSharedElementCallback, setExitTransition, setHasOptionsMenu, setInitialSavedState, setMenuVisibility, setReenterTransition, setRetainInstance, setReturnTransition, setSharedElementEnterTransition, setSharedElementReturnTransition, setTargetFragment, setUserVisibleHint, shouldShowRequestPermissionRationale, startActivity, startActivity, startActivityForResult, startActivityForResult, startIntentSenderForResult, startPostponedEnterTransition, toString, unregisterForContextMenu
from java.lang.Objectclone, getClass, notify, notifyAll, wait, wait, wait

Fields

public final PdfLoaderCallbacks mPdfLoaderCallbacks

Callbacks of PDF loading asynchronous tasks.

Constructors

public PdfViewer()

Methods

public void configureShareScroll(boolean left, boolean right, boolean top, boolean bottom)

Configures whether this viewer has to share scroll gestures in any direction with its container or any neighbouring view.

This call is only permitted when the viewer has a view, i.e. Viewer.mViewState reports at least Viewer.ViewState.VIEW_CREATED.

Parameters:

left: If true, will pass on scroll gestures that extend beyond the left bound.
right: If true, will pass on scroll gestures that extend beyond the right bound.
top: If true, will pass on scroll gestures that extend beyond the top bound.
bottom: If true, will pass on scroll gestures that extend beyond the bottom bound.

public PdfViewer setQuitOnError(boolean quit)

If set, this Viewer will call if it can't load the PDF. By default, the value is false.

public PdfViewer setExitOnPasswordCancel(boolean shouldExitOnPasswordCancel)

If set, this viewer will finish the attached activity when the user presses cancel on the prompt for the document password.

public void onCreate(Bundle savedInstanceState)

Called to do initial creation of a fragment. This is called after Fragment.onAttach(Activity) and before Fragment.onCreateView(LayoutInflater, ViewGroup, Bundle).

Note that this can be called while the fragment's activity is still in the process of being created. As such, you can not rely on things like the activity's content view hierarchy being initialized at this point. If you want to do work once the activity itself is created, add a LifecycleObserver on the activity's Lifecycle, removing it when it receives the callback.

Any restored child fragments will be created before the base Fragment.onCreate method returns.

Parameters:

savedInstanceState: If the fragment is being re-created from a previous saved state, this is the state.

public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)

Called to have the fragment instantiate its user interface view. This is optional, and non-graphical fragments can return null. This will be called between Fragment.onCreate(Bundle) and Fragment.onViewCreated(View, Bundle).

A default View can be returned by calling Fragment.Fragment(int) in your constructor. Otherwise, this method returns null.

It is recommended to only inflate the layout in this method and move logic that operates on the returned View to Fragment.onViewCreated(View, Bundle).

If you return a View from here, you will later be called in Fragment.onDestroyView() when the view is being released.

Parameters:

inflater: The LayoutInflater object that can be used to inflate any views in the fragment,
container: If non-null, this is the parent view that the fragment's UI should be attached to. The fragment should not add the view itself, but this can be used to generate the LayoutParams of the view.
savedInstanceState: If non-null, this fragment is being re-constructed from a previous saved state as given here.

Returns:

Return the View for the fragment's UI, or null.

public static Screen getScreen()

public static void setScreenForTest(Context context)

public void onActivityCreated(Bundle savedInstanceState)

Deprecated: use Fragment.onViewCreated(View, Bundle) for code touching the view created by Fragment.onCreateView(LayoutInflater, ViewGroup, Bundle) and Fragment.onCreate(Bundle) for other initialization. To get a callback specifically when a Fragment activity's is called, register a LifecycleObserver on the Activity's Lifecycle in Fragment.onAttach(Context), removing it when it receives the callback.

Called when the fragment's activity has been created and this fragment's view hierarchy instantiated. It can be used to do final initialization once these pieces are in place, such as retrieving views or restoring state. It is also useful for fragments that use Fragment.setRetainInstance(boolean) to retain their instance, as this callback tells the fragment when it is fully associated with the new activity instance. This is called after Fragment.onCreateView(LayoutInflater, ViewGroup, Bundle) and before Fragment.onViewStateRestored(Bundle).

Parameters:

savedInstanceState: If the fragment is being re-created from a previous saved state, this is the state.

protected abstract void onContentsAvailable(DisplayData contents, Bundle savedState)

Callback called when the full contents is re-loaded. This method should always run after LoadingViewer.onCreateView(LayoutInflater, ViewGroup, Bundle) and only on the UI thread.

Parameters:

contents: The fully-loaded contents.
savedState: If this instance is reborn, the saved state that was given to onCreate.

protected void onEnter()

Called after this viewer enters the screen and becomes visible.

protected void onExit()

Called after this viewer exits the screen and becomes invisible to the user.

public void setPassword(java.lang.String password)

protected void destroyView()

Called when this viewer no longer needs (or has) a view. Resets Viewer.mViewState to Viewer.ViewState.NO_VIEW. If the viewer is to be reused, it will restart its whole life-cycle including Viewer.onCreateView(LayoutInflater, ViewGroup, Bundle). When overridden by subclasses, it must be idempotent, and this method must be called. It is possible (and likely) it will be called more than once.

We could include this in Viewer.onDestroyView(), if only it was guaranteed to be called.

public void onDestroyView()

Called when the view previously created by Fragment.onCreateView(LayoutInflater, ViewGroup, Bundle) has been detached from the fragment. The next time the fragment needs to be displayed, a new view will be created. This is called after Fragment.onStop() and before Fragment.onDestroy(). It is called regardless of whether Fragment.onCreateView(LayoutInflater, ViewGroup, Bundle) returned a non-null view. Internally it is called after the view's state has been saved but before it has been removed from its parent.

public void onDestroy()

Called when the fragment is no longer in use. This is called after Fragment.onStop() and before Fragment.onDetach().

public void onSaveInstanceState(Bundle outState)

Called to ask the fragment to save its current dynamic state, so it can later be reconstructed in a new instance if its process is restarted. If a new instance of the fragment later needs to be created, the data you place in the Bundle here will be available in the Bundle given to Fragment.onCreate(Bundle), Fragment.onCreateView(LayoutInflater, ViewGroup, Bundle), and Fragment.onViewCreated(View, Bundle).

This corresponds to and most of the discussion there applies here as well. Note however: this method may be called at any time before Fragment.onDestroy(). There are many situations where a fragment may be mostly torn down (such as when placed on the back stack with no UI showing), but its state will not be saved until its owning activity actually needs to save its state.

Parameters:

outState: Bundle in which to place your saved state.

public void loadFile(Uri fileUri)

Load the PDF document.

Parameters:

fileUri: URI of the document.

public void showSpinner()

Show the loading spinner.

public void hideSpinner()

Hide the loading spinner.

protected void handleError()

public void setPasswordCancelError()

Create callback to retry password input when user cancels password prompt.

Source

/*
 * Copyright 2024 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package androidx.pdf.viewer;

import static android.view.View.VISIBLE;

import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.Rect;
import android.net.Uri;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import android.widget.ProgressBar;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
import androidx.annotation.UiThread;
import androidx.annotation.VisibleForTesting;
import androidx.fragment.app.DialogFragment;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.pdf.R;
import androidx.pdf.data.DisplayData;
import androidx.pdf.data.ErrorType;
import androidx.pdf.data.FutureValue;
import androidx.pdf.data.Openable;
import androidx.pdf.data.PdfStatus;
import androidx.pdf.data.Range;
import androidx.pdf.fetcher.Fetcher;
import androidx.pdf.find.FindInFileView;
import androidx.pdf.models.Dimensions;
import androidx.pdf.models.GotoLink;
import androidx.pdf.models.LinkRects;
import androidx.pdf.models.MatchRects;
import androidx.pdf.models.PageSelection;
import androidx.pdf.select.SelectionActionMode;
import androidx.pdf.util.AnnotationUtils;
import androidx.pdf.util.ObservableValue.ValueObserver;
import androidx.pdf.util.Preconditions;
import androidx.pdf.util.Screen;
import androidx.pdf.util.ThreadUtils;
import androidx.pdf.util.TileBoard;
import androidx.pdf.util.TileBoard.TileInfo;
import androidx.pdf.util.Toaster;
import androidx.pdf.util.Uris;
import androidx.pdf.viewer.PageViewFactory.PageView;
import androidx.pdf.viewer.loader.PdfLoader;
import androidx.pdf.viewer.loader.PdfLoaderCallbacks;
import androidx.pdf.widget.FastScrollView;
import androidx.pdf.widget.ZoomView;
import androidx.pdf.widget.ZoomView.ZoomScroll;

import com.google.android.material.floatingactionbutton.FloatingActionButton;
import com.google.android.material.snackbar.Snackbar;
import com.google.errorprone.annotations.CanIgnoreReturnValue;

import java.util.List;

/**
 * A {@link Viewer} that can display paginated PDFs. Each page is rendered in its own View.
 * Rendering is done in 2 passes:
 *
 * <ol>
 *   <li>Layout: Request the dimensions of the page and set them as measure for the image view,
 *   <li>Render: Create bitmap(s) at adequate dimensions and attach them to the page view.
 * </ol>
 *
 * <p>The layout pass is progressive: starts with a few first pages of the document, then reach
 * further as the user scrolls down (and ultimately spans the whole document). The rendering pass is
 * tightly limited to the currently visible pages. Pages that are scrolled past (become not visible)
 * have their bitmaps released to free up memory.
 *
 * <p>This is a {@link #SELF_MANAGED_CONTENTS} Viewer: its contents and internal models are kept
 * when the view is destroyed, and re-used when the view is re-created.
 *
 * <p>Major lifecycle events include:
 *
 * <ol>
 *   <li>{@link #onContentsAvailable} / {@link #onDestroy} : Content model is created.
 *   <li>{@link #onCreateView} / {@link #destroyView} : All views are created, the pdf service is
 *       connected.
 * </ol>
 */
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@SuppressWarnings({"UnusedMethod", "UnusedVariable"})
public class PdfViewer extends LoadingViewer {

    private static final String TAG = "PdfViewer";

    /** {@link View#setElevation(float)} value for PDF Pages (API 21+). */
    private static final int PAGE_ELEVATION_DP = 2;

    /** Key for saving page layout reach in bundles. */
    private static final String KEY_LAYOUT_REACH = "plr";
    private static final String KEY_QUIT_ON_ERROR = "quitOnError";
    private static final String KEY_EXIT_ON_CANCEL = "exitOnCancel";

    private static Screen sScreen;

    /** Single access to the PDF document: loads contents asynchronously (bitmaps, text,...) */
    private PdfLoader mPdfLoader;

    /** The file being displayed by this viewer. */
    private DisplayData mFileData;

    /** Callbacks of PDF loading asynchronous tasks. */
    @VisibleForTesting
    public final PdfLoaderCallbacks mPdfLoaderCallbacks;

    /** Observer of the page position that controls loading of relevant PDF assets. */
    private ValueObserver<ZoomScroll> mZoomScrollObserver;

    /** Observer to be set when the view is created. */
    @Nullable
    private ValueObserver<ZoomScroll> mPendingScrollPositionObserver;

    private Object mScrollPositionObserverKey;

    private ZoomView mZoomView;

    private PaginatedView mPaginatedView;
    private PaginationModel mPaginationModel;

    private SearchModel mSearchModel;
    private PdfSelectionModel mSelectionModel;
    private PdfSelectionHandles mSelectionHandles;

    private ValueObserver<String> mSearchQueryObserver;
    private ValueObserver<SelectedMatch> mSelectedMatchObserver;
    private ValueObserver<PageSelection> mSelectionObserver;

    private FastScrollView mFastScrollView;
    private ProgressBar mLoadingSpinner;

    private boolean mDocumentLoaded = false;
    private boolean mIsAnnotationIntentResolvable = false;

    /**
     * After the document content is saved over the original in InkActivity, we set this bit to true
     * so we know to call when the new document content is loaded.
     */
    private boolean mShouldRedrawOnDocumentLoaded = false;
    private Snackbar mSnackbar;

    private LayoutHandler mLayoutHandler;

    private Uri mLocalUri;
    private FrameLayout mPdfViewer;

    private FindInFileView mFindInFileView;

    private FloatingActionButton mAnnotationButton;

    private PageViewFactory mPageViewFactory;

    private SingleTapHandler mSingleTapHandler;

    private SelectionActionMode mSelectionActionMode;

    public PdfViewer() {
        super(SELF_MANAGED_CONTENTS);
    }

    @Override
    public void configureShareScroll(boolean left, boolean right, boolean top, boolean bottom) {
        mZoomView.setShareScroll(left, right, top, bottom);
    }

    /**
     * If set, this Viewer will call {@link Activity#finish()} if it can't load the PDF. By default,
     * the value is false.
     */
    @NonNull
    @CanIgnoreReturnValue
    public PdfViewer setQuitOnError(boolean quit) {
        getArguments().putBoolean(KEY_QUIT_ON_ERROR, quit);
        return this;
    }

    /**
     * If set, this viewer will finish the attached activity when the user presses cancel on the
     * prompt for the document password.
     */
    @NonNull
    @CanIgnoreReturnValue
    public PdfViewer setExitOnPasswordCancel(boolean shouldExitOnPasswordCancel) {
        getArguments().putBoolean(KEY_EXIT_ON_CANCEL, shouldExitOnPasswordCancel);
        return this;
    }

    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mFetcher = Fetcher.build(getContext(), 1);
        sScreen = new Screen(this.requireActivity().getApplicationContext());
    }

    @NonNull
    @SuppressLint("InflateParams")
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
            @Nullable Bundle savedState) {
        super.onCreateView(inflater, container, savedState);

        mPdfViewer = (FrameLayout) inflater.inflate(R.layout.pdf_viewer_container, container,
                false);
        mFindInFileView = mPdfViewer.findViewById(R.id.search);
        mFastScrollView = mPdfViewer.findViewById(R.id.fast_scroll_view);
        mPaginatedView = mPdfViewer.findViewById(R.id.pdf_view);
        mPaginationModel = mPaginatedView.getModel();
        mZoomView = mPdfViewer.findViewById(R.id.zoom_view);
        mLoadingSpinner = mPdfViewer.findViewById(R.id.progress_indicator);
        setUpEditFab();

        return mPdfViewer;
    }

    @Nullable
    public static Screen getScreen() {
        return sScreen;
    }

    @VisibleForTesting
    public static void setScreenForTest(@NonNull Context context) {
        sScreen = new Screen(context);
    }

    @Override
    public void onActivityCreated(@Nullable Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        if (mPendingScrollPositionObserver != null) {
            mScrollPositionObserverKey = mZoomView.zoomScroll().addObserver(
                    mPendingScrollPositionObserver);
            mPendingScrollPositionObserver = null;
        }
    }

    @Override
    protected void onContentsAvailable(@NonNull DisplayData contents, @Nullable Bundle savedState) {
        mFileData = contents;
        mLocalUri = contents.getUri();

        createContentModel(
                PdfLoader.create(
                        getActivity().getApplicationContext(),
                        contents,
                        TileBoard.DEFAULT_RECYCLER,
                        mPdfLoaderCallbacks,
                        false));
        mLayoutHandler = new LayoutHandler(mPdfLoader);
        mZoomView.setPdfSelectionModel(mSelectionModel);
        mPaginatedView.setSelectionModel(mSelectionModel);
        mPaginatedView.setSearchModel(mSearchModel);
        mPaginatedView.setPdfLoader(mPdfLoader);

        mSearchQueryObserver =
                new SearchQueryObserver(mPaginatedView);
        mSearchModel.query().addObserver(mSearchQueryObserver);

        mSingleTapHandler = new SingleTapHandler(getContext(), mAnnotationButton, mPaginatedView,
                mFindInFileView, mZoomView, mSelectionModel, mPaginationModel, mLayoutHandler);
        mPageViewFactory = new PageViewFactory(requireContext(), mPdfLoader,
                mPaginatedView, mZoomView, mSingleTapHandler, mFindInFileView);
        mPaginatedView.setPageViewFactory(mPageViewFactory);

        mSelectionObserver =
                new PageSelectionValueObserver(mPaginatedView, mPaginationModel, mPageViewFactory,
                        requireContext());
        mSelectionModel.selection().addObserver(mSelectionObserver);

        mSelectedMatchObserver =
                new SelectedMatchValueObserver(mPaginatedView, mPaginationModel, mPageViewFactory,
                        mZoomView, mLayoutHandler, requireContext());
        mSearchModel.selectedMatch().addObserver(mSelectedMatchObserver);

        mFindInFileView.setPaginatedView(mPaginatedView);
        mFindInFileView.setAnnotationIntentResolvable(mIsAnnotationIntentResolvable);

        if (savedState != null) {
            int layoutReach = savedState.getInt(KEY_LAYOUT_REACH);
            mLayoutHandler.setInitialPageLayoutReachWithMax(layoutReach);
        }
    }

    @Override
    protected void onEnter() {
        super.onEnter();
        // This is necessary for password protected PDF documents. If the user failed to produce the
        // correct password, we want to prompt for the correct password every time the film strip
        // comes back to this viewer.
        if (!mDocumentLoaded && mPdfLoader != null) {
            mPdfLoader.reconnect();
        }

        if (mPaginatedView != null && mPaginatedView.getChildCount() > 0) {
            mZoomView.loadPageAssets(mLayoutHandler, null);
        }
    }

    @Override
    public void onExit() {
        super.onExit();
        if (!mDocumentLoaded && mPdfLoader != null) {
            // e.g. a password-protected pdf that wasn't loaded.
            mPdfLoader.disconnect();
        }
    }

    private void createContentModel(PdfLoader pdfLoader) {
        this.mPdfLoader = pdfLoader;
        mFindInFileView.setPdfLoader(pdfLoader);

        mSearchModel = mFindInFileView.getSearchModel();

        mSelectionModel = new PdfSelectionModel(pdfLoader);

        mSelectionHandles = new PdfSelectionHandles(mSelectionModel, mZoomView, mPaginatedView,
                mSelectionActionMode);

    }

    private void destroyContentModel() {
        mSelectionHandles.destroy();
        mSelectionHandles = null;

        mSelectionModel.selection().removeObserver(mSelectionObserver);
        mSelectionModel = null;

        mSearchModel.selectedMatch().removeObserver(mSelectedMatchObserver);
        mSearchModel.query().removeObserver(mSearchQueryObserver);
        mSearchModel = null;

        mPdfLoader.disconnect();
        mPdfLoader = null;
        mDocumentLoaded = false;
    }

    /**
     *
     */
    public void setPassword(@NonNull String password) {
        if (mPdfLoader != null) {
            mPdfLoader.applyPassword(password);
        }
    }

    @Override
    public void destroyView() {
        if (mZoomView != null) {
            mZoomView.zoomScroll().removeObserver(mZoomScrollObserver);
            if (mScrollPositionObserverKey != null) {
                mZoomView.zoomScroll().removeObserver(mScrollPositionObserverKey);
            }
            mZoomView = null;
        }

        if (mPaginatedView != null) {
            mPaginatedView.removeAllViews();
            mPaginatedView = null;
        }

        if (mPdfLoader != null) {
            mPdfLoader.cancelAll();
            mPdfLoader.disconnect();
            mDocumentLoaded = false;
        }
        super.destroyView();
    }

    @Override
    public void onDestroyView() {
        super.onDestroyView();
        if (mSnackbar != null) {
            mSnackbar.dismiss();
        }
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        if (mPdfLoader != null) {
            destroyContentModel();
        }
    }

    @Override
    public void onSaveInstanceState(@NonNull Bundle outState) {
        outState.putInt(KEY_LAYOUT_REACH, mLayoutHandler.getPageLayoutReach());
    }

    /**
     * Load the PDF document.
     *
     * @param fileUri URI of the document.
     */
    public void loadFile(@NonNull Uri fileUri) {
        Preconditions.checkNotNull(fileUri);
        try {
            validateFileUri(fileUri);
        } catch (SecurityException e) {
            // TODO Toaster.LONG.popToast(this, R.string.problem_with_file);
            finishActivity();
        }

        showSpinner();
        fetchFile(fileUri);
        mLocalUri = fileUri;
        mIsAnnotationIntentResolvable = AnnotationUtils.resolveAnnotationIntent(requireContext(),
                mLocalUri);
        mSingleTapHandler.setAnnotationIntentResolvable(mIsAnnotationIntentResolvable);
    }

    private void validateFileUri(Uri fileUri) {
        if (!Uris.isContentUri(fileUri) && !Uris.isFileUri(fileUri)) {
            throw new IllegalArgumentException("Only content and file uri is supported");
        }

        // TODO confirm this exception
        if (Uris.isFileUriInSamePackageDataDir(fileUri)) {
            throw new SecurityException(
                    "Disallow opening file:// URIs in the parent package's data directory for "
                            + "security reasons");
        }
    }

    private void fetchFile(@NonNull final Uri fileUri) {
        Preconditions.checkNotNull(fileUri);
        final String fileName = getFileName(fileUri);
        final FutureValue<Openable> openable;
        openable = mFetcher.loadLocal(fileUri);

        // Only make this visible when we know a file needs to be fetched.
        // TODO loadingScreen.setVisibility(View.VISIBLE);

        openable.get(
                new FutureValue.Callback<Openable>() {
                    @Override
                    public void available(Openable openable) {
                        viewerAvailable(fileUri, fileName, openable);
                    }

                    @Override
                    public void failed(@NonNull Throwable thrown) {
                        finishActivity();
                    }

                    @Override
                    public void progress(float progress) {
                    }
                });
    }

    private void finishActivity() {
        if (getActivity() != null) {
            getActivity().finish();
        }
    }

    @Nullable
    private ContentResolver getResolver() {
        if (getActivity() != null) {
            return getActivity().getContentResolver();
        }
        return null;
    }

    private String getFileName(@NonNull Uri fileUri) {
        ContentResolver resolver = getResolver();
        return resolver != null ? Uris.extractName(fileUri, resolver) : Uris.extractFileName(
                fileUri);
    }

    private void viewerAvailable(Uri fileUri, String fileName, Openable openable) {
        DisplayData contents = new DisplayData(fileUri, fileName, openable);

        // TODO loadingScreen.setVisibility(View.GONE);

        startViewer(contents);
    }

    private void startViewer(@NonNull DisplayData contents) {
        Preconditions.checkNotNull(contents);

        setQuitOnError(true);
        setExitOnPasswordCancel(false);
        feed(contents);
        postEnter();
    }

    private boolean isPageCreated(int pageNum) {
        return pageNum < mPaginationModel.getSize() && mPaginatedView.getViewAt(pageNum) != null;
    }

    private PageView getPage(int pageNum) {
        return mPaginatedView.getViewAt(pageNum);
    }

    private void lookAtSelection(SelectedMatch selection) {
        if (selection == null || selection.isEmpty()) {
            return;
        }
        if (selection.getPage() >= mPaginationModel.getSize()) {
            mLayoutHandler.layoutPages(selection.getPage() + 1);
            return;
        }
        Rect rect = selection.getPageMatches().getFirstRect(selection.getSelected());
        int x = mPaginationModel.getLookAtX(selection.getPage(), rect.centerX());
        int y = mPaginationModel.getLookAtY(selection.getPage(), rect.centerY());
        mZoomView.centerAt(x, y);

        PageMosaicView pageView = (PageMosaicView) mPageViewFactory.getOrCreatePageView(
                selection.getPage(),
                sScreen.pxFromDp(PAGE_ELEVATION_DP),
                mPaginationModel.getPageSize(selection.getPage()));
        pageView.setOverlay(selection.getOverlay());
    }

    /** Show the loading spinner. */
    @UiThread
    public void showSpinner() {
        if (mLoadingSpinner != null) {
            mLoadingSpinner.post(() -> mLoadingSpinner.setVisibility(View.VISIBLE));
        }
    }

    /** Hide the loading spinner. */
    @UiThread
    public void hideSpinner() {
        if (mLoadingSpinner != null) {
            mLoadingSpinner.post(() -> mLoadingSpinner.setVisibility(View.GONE));
        }
    }

    // TODO: Revisit this method for its usage. Currently redundant

    { // Init pdfLoaderCallbacks
        mPdfLoaderCallbacks =
                new PdfLoaderCallbacks() {
                    static final String PASSWORD_DIALOG_TAG = "password-dialog";

                    @Nullable
                    private PdfPasswordDialog currentPasswordDialog(@Nullable FragmentManager fm) {
                        if (fm != null) {
                            Fragment passwordDialog = fm.findFragmentByTag(PASSWORD_DIALOG_TAG);
                            if (passwordDialog instanceof PdfPasswordDialog) {
                                return (PdfPasswordDialog) passwordDialog;
                            }
                        }
                        return null;
                    }

                    // Callbacks should exit early if viewState == NO_VIEW (typically a Destroy
                    // is in progress).
                    @Override
                    @SuppressWarnings("deprecation")
                    public void requestPassword(boolean incorrect) {
                        mIsPasswordProtected = true;

                        if (!isShowing()) {
                            // This would happen if the service decides to start while we're in
                            // the background.
                            // The dialog code below would then crash. We can't just bypass it
                            // because then we'd
                            // have
                            // a started service with no loaded PDF and no means to load it. The
                            // best way is to
                            // just
                            // kill the service which will restart on the next onStart.
                            if (mPdfLoader != null) {
                                mPdfLoader.disconnect();
                            }
                            return;
                        }

                        if (viewState().get() != ViewState.NO_VIEW) {
                            FragmentManager fm = requireActivity().getSupportFragmentManager();

                            PdfPasswordDialog passwordDialog = currentPasswordDialog(fm);
                            if (passwordDialog == null) {
                                passwordDialog = new PdfPasswordDialog();
                                passwordDialog.setTargetFragment(PdfViewer.this, 0);
                                passwordDialog.setFinishOnCancel(
                                        getArguments().getBoolean(KEY_EXIT_ON_CANCEL));
                                passwordDialog.show(fm, PASSWORD_DIALOG_TAG);
                            }

                            if (incorrect) {
                                passwordDialog.retry();
                            }
                        }
                    }

                    @Override
                    public void documentLoaded(int numPages, @NonNull DisplayData data) {
                        if (numPages <= 0) {
                            documentNotLoaded(PdfStatus.PDF_ERROR);
                            return;
                        }

                        mDocumentLoaded = true;
                        hideSpinner();

                        // Assume we see at least the first page
                        mPaginatedView.getPageRangeHandler().setMaxPage(1);
                        if (viewState().get() != ViewState.NO_VIEW) {
                            mPaginationModel.initialize(numPages);
                            mFastScrollView.setPaginationModel(mPaginationModel);

                            dismissPasswordDialog();
                            mLayoutHandler.maybeLayoutPages(1);
                            mSearchModel.setNumPages(numPages);
                        }

                        if (mShouldRedrawOnDocumentLoaded) {
                            mShouldRedrawOnDocumentLoaded = false;
                        }

                        if (mIsAnnotationIntentResolvable) {
                            mAnnotationButton.setVisibility(VISIBLE);
                        }
                    }

                    @Override
                    public void documentNotLoaded(@NonNull PdfStatus status) {
                        if (viewState().get() != ViewState.NO_VIEW) {
                            dismissPasswordDialog();
                            if (getArguments().getBoolean(KEY_QUIT_ON_ERROR)) {
                                getActivity().finish();
                            }
                            switch (status) {
                                case NONE:
                                case FILE_ERROR:
                                    handleError();
                                    break;
                                case PDF_ERROR:
                                    Toaster.LONG.popToast(
                                            getActivity(), R.string.error_file_format_pdf,
                                            mFileData.getName());
                                    break;
                                case LOADED:
                                case REQUIRES_PASSWORD:
                                    Preconditions.checkArgument(
                                            false,
                                            "Document not loaded but status " + status.getNumber());
                                    break;
                                case PAGE_BROKEN:
                                case NEED_MORE_DATA:
                                    // no op.
                            }
                            // TODO: Tracker render error.
                        }
                    }

                    @Override
                    public void pageBroken(int page) {
                        if (viewState().get() != ViewState.NO_VIEW) {
                            ((PageMosaicView) mPageViewFactory.getOrCreatePageView(
                                    page,
                                    sScreen.pxFromDp(PAGE_ELEVATION_DP),
                                    mPaginationModel.getPageSize(page)))
                                    .setFailure(getString(R.string.error_on_page, page + 1));
                            Toaster.LONG.popToast(getActivity(), R.string.error_on_page, page + 1);
                            // TODO: Track render error.
                        }
                    }

                    @SuppressWarnings("deprecation")
                    private void dismissPasswordDialog() {
                        DialogFragment passwordDialog = currentPasswordDialog(
                                requireActivity().getSupportFragmentManager());
                        if (passwordDialog != null
                                && PdfViewer.this.equals(passwordDialog.getTargetFragment())) {
                            passwordDialog.dismiss();
                        }
                    }

                    @Override
                    public void setPageDimensions(int pageNum, @NonNull Dimensions dimensions) {
                        if (viewState().get() != ViewState.NO_VIEW) {
                            mPaginationModel.addPage(pageNum, dimensions);
                            mLayoutHandler.setPageLayoutReach(mPaginationModel.getSize());

                            if (mSearchModel.query().get() != null
                                    && mSearchModel.selectedMatch().get() != null
                                    && mSearchModel.selectedMatch().get().getPage() == pageNum) {
                                // lookAtSelection is posted to run once layout has finished:
                                ThreadUtils.postOnUiThread(
                                        () -> {
                                            if (viewState().get() != ViewState.NO_VIEW) {
                                                lookAtSelection(mSearchModel.selectedMatch().get());
                                            }
                                        });
                            }

                            // The new page might actually be visible on the screen, so we need
                            // to fetch assets:
                            ZoomScroll position = mZoomView.zoomScroll().get();
                            Range newRange =
                                    mPaginatedView.getPageRangeHandler().computeVisibleRange(
                                            position.scrollY, position.zoom, mZoomView.getHeight(),
                                            true);
                            if (newRange.isEmpty()) {
                                mLayoutHandler.maybeLayoutPages(newRange.getLast());
                            } else if (newRange.contains(pageNum)) {
                                // The new page is visible, fetch its assets.
                                mZoomView.loadPageAssets(mLayoutHandler, null);
                            }
                        }
                    }

                    @Override
                    public void setTileBitmap(int pageNum, @NonNull TileInfo tileInfo,
                            @NonNull Bitmap bitmap) {
                        if (viewState().get() != ViewState.NO_VIEW && isPageCreated(pageNum)) {
                            getPage(pageNum).getPageView().setTileBitmap(tileInfo, bitmap);
                        }
                    }

                    @Override
                    public void setPageBitmap(int pageNum, @NonNull Bitmap bitmap) {
                        // We announce that the viewer is ready as soon as a bitmap is loaded
                        // (not before).
                        if (mViewState.get() == ViewState.VIEW_CREATED) {
                            mZoomView.setVisibility(View.VISIBLE);
                            mViewState.set(ViewState.VIEW_READY);
                        }
                        if (viewState().get() != ViewState.NO_VIEW && isPageCreated(pageNum)) {
                            getPage(pageNum).getPageView().setPageBitmap(bitmap);
                        }
                    }

                    @Override
                    public void setPageText(int pageNum, @NonNull String text) {
                        if (viewState().get() != ViewState.NO_VIEW && isPageCreated(pageNum)) {
                            getPage(pageNum).getPageView().setPageText(text);
                        }
                    }

                    @Override
                    public void setSearchResults(@NonNull String query, int pageNum,
                            @NonNull MatchRects matches) {
                        if (viewState().get() != ViewState.NO_VIEW && query.equals(
                                mSearchModel.query().get())) {
                            mSearchModel.updateMatches(query, pageNum, matches);
                            if (isPageCreated(pageNum)) {
                                getPage(pageNum)
                                        .getPageView()
                                        .setOverlay(
                                                mSearchModel.getOverlay(query, pageNum, matches));
                            }
                        }
                    }

                    @Override
                    public void setSelection(int pageNum, @Nullable PageSelection selection) {
                        if (viewState().get() == ViewState.NO_VIEW) {
                            return;
                        }
                        if (selection != null) {
                            // Clear searchModel - we hide the search and show the selection
                            // instead.
                            mSearchModel.setQuery(null, -1);
                        }
                        mSelectionModel.setSelection(selection);
                    }

                    @Override
                    public void setPageUrlLinks(int pageNum, @NonNull LinkRects links) {
                        if (viewState().get() != ViewState.NO_VIEW && links != null
                                && isPageCreated(pageNum)) {
                            getPage(pageNum).setPageUrlLinks(links);
                        }
                    }

                    @Override
                    public void setPageGotoLinks(int pageNum, @NonNull List<GotoLink> links) {
                        if (viewState().get() != ViewState.NO_VIEW && isPageCreated(pageNum)) {
                            getPage(pageNum).setPageGotoLinks(links);
                        }
                    }

                    /**
                     * Receives areas of a page that have been invalidated by an editing action
                     * and asks the
                     * appropriate page view to redraw them.
                     */
                    @Override
                    public void setInvalidRects(int pageNum, @NonNull List<Rect> invalidRects) {
                        if (viewState().get() != ViewState.NO_VIEW && isPageCreated(pageNum)) {
                            if (invalidRects == null || invalidRects.isEmpty()) {
                                return;
                            }
                            mPaginatedView.getViewAt(pageNum).getPageView().requestRedrawAreas(
                                    invalidRects);
                        }
                    }
                };
    }

    protected void handleError() {
        mViewState.set(ViewState.ERROR);
    }


    /** Create callback to retry password input when user cancels password prompt. */
    public void setPasswordCancelError() {
        Runnable retryCallback = () -> mPdfLoaderCallbacks.requestPassword(false);
        displayViewerError(ErrorType.FILE_PASSWORD_PROTECTED, this, retryCallback);
    }

    private void displayViewerError(ErrorType errorType, Viewer viewer, Runnable actionCallback) {
        switch (errorType) {
            case FILE_PASSWORD_PROTECTED:
                showSnackBar(R.string.password_not_entered, R.string.retry_button_text,
                        actionCallback);
                return;
            default:
                break;
        }

    }

    private void showSnackBar(int text, int actionText, Runnable actionCallback) {
        mSnackbar = Snackbar.make(mPdfViewer, text, Snackbar.LENGTH_INDEFINITE);
        View.OnClickListener mResolveClickListener =
                v -> {
                    actionCallback.run();
                };
        mSnackbar.setAction(actionText, mResolveClickListener);
        mSnackbar.show();
    }

    private void setUpEditFab() {
        mAnnotationButton = mPdfViewer.findViewById(R.id.edit_fab);
        mAnnotationButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                performEdit();
            }
        });

    }

    private void performEdit() {
        Intent intent = AnnotationUtils.getAnnotationIntent(mLocalUri);
        intent.setData(mLocalUri);
        startActivity(intent);
    }

}