1 /*
2  * Copyright 2024 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package androidx.pdf.viewer.loader;
18 
19 import android.content.Context;
20 import android.os.ParcelFileDescriptor;
21 import android.os.RemoteException;
22 import android.text.TextUtils;
23 import android.util.SparseArray;
24 
25 import androidx.annotation.RestrictTo;
26 import androidx.pdf.data.DisplayData;
27 import androidx.pdf.data.Opener;
28 import androidx.pdf.data.PdfStatus;
29 import androidx.pdf.models.Dimensions;
30 import androidx.pdf.models.PdfDocumentRemote;
31 import androidx.pdf.models.SelectionBoundary;
32 import androidx.pdf.service.PdfDocumentRemoteProto;
33 import androidx.pdf.util.BitmapRecycler;
34 import androidx.pdf.util.TileBoard.TileInfo;
35 
36 import org.jspecify.annotations.NonNull;
37 import org.jspecify.annotations.Nullable;
38 
39 import java.lang.ref.WeakReference;
40 
41 /**
42  * Allows the caller to make asynchronous requests for data from a PdfDocument.
43  * The caller must provide a {@link PdfLoaderCallbacks}, which is only held as a
44  * {@link WeakReference} and called when the requested data is ready.
45  * <p>
46  * PdfLoader automatically opens and maintains a connection to the PdfDocumentService.
47  * This connection must be {@link #disconnect}ed when no longer needed, and {@link #reconnect}ed to
48  * when this activity is restarted.
49  */
50 @RestrictTo(RestrictTo.Scope.LIBRARY)
51 @SuppressWarnings("UnusedVariable")
52 public class PdfLoader {
53     private static final String TAG = PdfLoader.class.getSimpleName();
54 
55     final Context mContext;
56     private final Opener mOpener;
57     final PdfTaskExecutor mExecutor;
58     final BitmapRecycler mBitmapRecycler;
59     private final PdfConnection mConnection;
60 
61     // Keep a reference to the PDF data until it's successfully opened.
62     private final DisplayData mData;
63     private final boolean mHideTextAnnotations;
64 
65     private WeakPdfLoaderCallbacks mCallbacks;
66 
67     private final SparseArray<PdfPageLoader> mPageLoaders;
68     private String mLoadedPassword;
69     private int mNumPages;
70 
71     /** Creates a new {@link PdfLoader} and loads the document from the given data. */
create(@onNull Context context, @NonNull DisplayData data, @NonNull BitmapRecycler bitmaps, @NonNull PdfLoaderCallbacks callbacks)72     public static @NonNull PdfLoader create(@NonNull Context context, @NonNull DisplayData data,
73             @NonNull BitmapRecycler bitmaps,
74             @NonNull PdfLoaderCallbacks callbacks) {
75         return create(context, data, bitmaps, callbacks, false /* hideTextAnnotations */);
76     }
77 
78     /**
79      * Creates a new {@link PdfLoader} and loads the document from the given data.
80      *
81      * @param hideTextAnnotations whether to skip rendering text and highlight annotations in the
82      *                            PDF
83      */
create( @onNull Context context, @NonNull DisplayData data, @NonNull BitmapRecycler bitmaps, @NonNull PdfLoaderCallbacks callbacks, boolean hideTextAnnotations)84     public static @NonNull PdfLoader create(
85             @NonNull Context context,
86             @NonNull DisplayData data,
87             @NonNull BitmapRecycler bitmaps,
88             @NonNull PdfLoaderCallbacks callbacks,
89             boolean hideTextAnnotations) {
90         final WeakPdfLoaderCallbacks weakCallbacks = WeakPdfLoaderCallbacks.wrap(callbacks);
91         PdfConnection conn = new PdfConnection(context);
92         final PdfLoader pdfLoader =
93                 new PdfLoader(
94                         context,
95                         conn,
96                         data,
97                         bitmaps,
98                         weakCallbacks,
99                         hideTextAnnotations);
100         conn.setOnConnectInitializer(
101                 () -> pdfLoader.mExecutor.schedule(pdfLoader.new LoadDocumentTask()));
102         conn.setConnectionFailureHandler(() -> weakCallbacks.documentNotLoaded(PdfStatus.NONE));
103 
104         conn.connect(data.getUri());
105         return pdfLoader;
106     }
107 
PdfLoader( Context context, PdfConnection mConnection, DisplayData data, BitmapRecycler mBitmapRecycler, WeakPdfLoaderCallbacks callbacks, boolean hideTextAnnotations)108     PdfLoader(
109             Context context,
110             PdfConnection mConnection,
111             DisplayData data,
112             BitmapRecycler mBitmapRecycler,
113             WeakPdfLoaderCallbacks callbacks,
114             boolean hideTextAnnotations) {
115         this.mContext = context;
116         this.mOpener = new Opener(context);
117         this.mConnection = mConnection;
118         this.mData = data;
119         this.mHideTextAnnotations = hideTextAnnotations;
120         this.mExecutor = new PdfTaskExecutor();
121         this.mExecutor.start();
122         this.mBitmapRecycler = mBitmapRecycler;
123         this.mCallbacks = callbacks;
124         this.mPageLoaders = new SparseArray<>();
125     }
126 
getNumPages()127     public int getNumPages() {
128         return mNumPages;
129     }
130 
setNumPages(int numPages)131     public void setNumPages(int numPages) {
132         mNumPages = numPages;
133     }
134 
setCallbacks(@onNull WeakPdfLoaderCallbacks callbacks)135     public void setCallbacks(@NonNull WeakPdfLoaderCallbacks callbacks) {
136         mCallbacks = callbacks;
137     }
138 
139     /** Schedule task to load a PdfDocument. */
reloadDocument()140     public void reloadDocument() {
141         if (isConnected()) {
142             mExecutor.schedule(new LoadDocumentTask(mLoadedPassword));
143         } else {
144             /*
145             *  For password protected files we kill the service if the app goes into
146             *  background before the document is loaded hence here we just register a
147             *  task which will be executed once the service is reconnected onStart
148             */
149             mConnection.setOnConnectInitializer(
150                     () -> mExecutor.schedule(new LoadDocumentTask(mLoadedPassword)));
151             mConnection.setConnectionFailureHandler(
152                     () -> mCallbacks.documentNotLoaded(PdfStatus.NONE));
153         }
154     }
155 
156     /**
157      * Check if PdfLoader is connected to PdfDocumentService
158      */
isConnected()159     public boolean isConnected() {
160         return mConnection.isConnected();
161     }
162 
163     /**
164      * Reconnect to the PdfDocumentService if needed, after it may have been killed by the
165      * system.
166      */
reconnect()167     public void reconnect() {
168         mConnection.connect(mData.getUri());
169     }
170 
171     /** Disconnect from the PdfDocumentService, which will close itself and free its resources. */
disconnect()172     public void disconnect() {
173         mConnection.disconnect();
174     }
175 
176     /** Tries to re-open the PDF with the given password. */
applyPassword(@onNull String password)177     public void applyPassword(@NonNull String password) {
178         mExecutor.schedule(new LoadDocumentTask(password));
179     }
180 
181     /** Cancels all requests related to one page (bitmaps, texts,...). */
cancel(int pageNum)182     public void cancel(int pageNum) {
183         getPageLoader(pageNum).cancel();
184     }
185 
186     /** Cancel all tasks except search and form-filling. */
cancelExceptSearchAndFormFilling(int pageNum)187     public void cancelExceptSearchAndFormFilling(int pageNum) {
188         getPageLoader(pageNum).cancelExceptSearchAndFormFilling();
189     }
190 
191     /** Releases object in memory related to a page when that page is no longer visible. */
releasePage(int pageNum)192     public void releasePage(int pageNum) {
193         getPageLoader(pageNum).releasePage();
194     }
195 
196     /**
197      * Loads page dimensions for the given page - once it is ready, will call the
198      * {@link PdfLoaderCallbacks#setPageDimensions} callback.
199      */
loadPageDimensions(int pageNum)200     public void loadPageDimensions(int pageNum) {
201         getPageLoader(pageNum).loadPageDimensions();
202     }
203 
204     /**
205      * Renders a bitmap for the given page - once it is ready, will call the
206      * {@link PdfLoaderCallbacks#setPageBitmap} callback.
207      */
loadPageBitmap(int pageNum, @NonNull Dimensions pageSize)208     public void loadPageBitmap(int pageNum, @NonNull Dimensions pageSize) {
209         getPageLoader(pageNum).loadPageBitmap(pageSize);
210     }
211 
212     /**
213      * Renders bitmaps for the given tiles - once it is ready, will call the
214      * {@link PdfLoaderCallbacks#setTileBitmap} callback.
215      */
loadTileBitmaps(int pageNum, @NonNull Dimensions pageSize, @NonNull Iterable<TileInfo> tiles)216     public void loadTileBitmaps(int pageNum, @NonNull Dimensions pageSize,
217             @NonNull Iterable<TileInfo> tiles) {
218         getPageLoader(pageNum).loadTilesBitmaps(pageSize, tiles);
219     }
220 
221     /** Cancels requests for all tile bitmaps */
cancelAllTileBitmaps(int pageNum)222     public void cancelAllTileBitmaps(int pageNum) {
223         getPageLoader(pageNum).cancelAllTileBitmaps();
224     }
225 
226     /** Cancels requests for some tile bitmaps */
cancelTileBitmaps(int pageNum, @NonNull Iterable<Integer> tileIds)227     public void cancelTileBitmaps(int pageNum, @NonNull Iterable<Integer> tileIds) {
228         getPageLoader(pageNum).cancelTileBitmaps(tileIds);
229     }
230 
231     /**
232      * Loads text for the given page - once it is ready, will call the
233      * {@link PdfLoaderCallbacks#setPageText} callback.
234      */
loadPageText(int pageNum)235     public void loadPageText(int pageNum) {
236         getPageLoader(pageNum).loadPageText();
237     }
238 
239     /**
240      * Searches for the given term on the given page - once it is ready, will call the
241      * {@link PdfLoaderCallbacks#setSearchResults} callback.
242      */
searchPageText(int pageNum, @NonNull String query)243     public void searchPageText(int pageNum, @NonNull String query) {
244         getPageLoader(pageNum).searchPageText(query);
245     }
246 
247     /**
248      * Selects the text between the given two boundaries - once it is ready, will call the
249      * the {@link PdfLoaderCallbacks#setSelection} callback.
250      */
selectPageText(int pageNum, @NonNull SelectionBoundary start, @NonNull SelectionBoundary stop)251     public void selectPageText(int pageNum, @NonNull SelectionBoundary start,
252             @NonNull SelectionBoundary stop) {
253         getPageLoader(pageNum).selectPageText(start, stop);
254     }
255 
256     /**
257      * Finds all the url links on the page - once it is ready, will call the {@link
258      * PdfLoaderCallbacks#setPageUrlLinks} callback.
259      */
loadPageUrlLinks(int pageNum)260     public void loadPageUrlLinks(int pageNum) {
261         getPageLoader(pageNum).loadPageLinks();
262     }
263 
264     /**
265      * Finds all the go-to links on the page - once it is ready, will call the {@link
266      * PdfLoaderCallbacks#setPageGotoLinks} callback.
267      */
loadPageGotoLinks(int pageNum)268     public void loadPageGotoLinks(int pageNum) {
269         getPageLoader(pageNum).loadPageGotoLinks();
270     }
271 
272     /** Cancels all data requested for every page. */
cancelAll()273     public void cancelAll() {
274         for (int i = 0; i < mPageLoaders.size(); i++) {
275             mPageLoaders.valueAt(i).cancel();
276         }
277     }
278 
279     /**
280      * Returns a {@link PdfDocumentRemote} which maybe ready or not (i.e. still initializing).
281      */
getPdfDocument(@onNull String forTask)282     protected @NonNull PdfDocumentRemote getPdfDocument(@NonNull String forTask) {
283         return mConnection.getPdfDocument(forTask);
284     }
285 
releasePdfDocument()286     protected void releasePdfDocument() {
287         mConnection.releasePdfDocument();
288     }
289 
290     /**
291      * Returns a valid {@link PdfDocumentRemote} or null if there isn't one (i.e. the service is not
292      * currently bound, or it is but still initializing).
293      */
getLoadedPdfDocument(@onNull String forTask)294     protected @Nullable PdfDocumentRemote getLoadedPdfDocument(@NonNull String forTask) {
295         return mConnection.isLoaded() ? mConnection.getPdfDocument(forTask) : null;
296     }
297 
298     // Always returns a non-null callbacks - however the callbacks hold only a weak reference to the
299     // PdfViewer, so it can be garbage collected if no longer in use, in which case the callbacks
300     // all become no-ops.
getCallbacks()301     public @NonNull WeakPdfLoaderCallbacks getCallbacks() {
302         return mCallbacks;
303     }
304 
getPageLoader(int pageNum)305     PdfPageLoader getPageLoader(int pageNum) {
306         PdfPageLoader pageLoader = mPageLoaders.get(pageNum);
307         if (pageLoader == null) {
308             pageLoader = new PdfPageLoader(this, pageNum, mHideTextAnnotations);
309             mPageLoaders.put(pageNum, pageLoader);
310         }
311         return pageLoader;
312     }
313 
314     /** AsyncTask for loading a PdfDocument. */
315     class LoadDocumentTask extends AbstractPdfTask<PdfStatus> {
316         private final String mPassword;
317 //        private boolean mIsLinearized;
318 
LoadDocumentTask()319         LoadDocumentTask() {
320             this(null);
321         }
322 
LoadDocumentTask(String password)323         LoadDocumentTask(String password) {
324             super(PdfLoader.this, Priority.INITIALIZE);
325             this.mPassword = password;
326         }
327 
328         @Override
getLogTag()329         protected String getLogTag() {
330             return "LoadDocumentTask";
331         }
332 
333         @Override
getPdfDocument()334         protected PdfDocumentRemote getPdfDocument() {
335             // This Task needs to work with an uninitialized PdfDocument.
336             return PdfLoader.this.getPdfDocument(getLogTag());
337         }
338 
339         @Override
doInBackground(PdfDocumentRemoteProto pdfDocument)340         protected PdfStatus doInBackground(PdfDocumentRemoteProto pdfDocument)
341                 throws RemoteException {
342             PdfStatus result;
343             if (mConnection.isLoaded()) {
344                 // Already loaded, skip the loading process (e.g., during screen rotation).
345                 result = PdfStatus.LOADED;
346             } else {
347                 if (mData == null) {
348                     return PdfStatus.FILE_ERROR;
349                 }
350 
351                 // NOTE: This filedescriptor is not closed since it continues to be used by Pdfium.
352                 // TODO: StrictMode- Look into filedescriptors more and document
353                 // exactly when they should be opened and closed, making sure they are not leaked.
354                 ParcelFileDescriptor fd = mData.openFd(mOpener);
355 
356                 if (fd == null || fd.getFd() == -1) {
357                     return PdfStatus.FILE_ERROR;
358                 }
359                 int statusIndex = pdfDocument.getPdfDocumentRemote().create(fd, mPassword);
360                 result = PdfStatus.values()[statusIndex];
361             }
362 
363             if (result == PdfStatus.LOADED) {
364                 mNumPages = pdfDocument.getPdfDocumentRemote().numPages();
365 //                mIsLinearized = pdfDocument.getPdfDocumentRemote().isPdfLinearized();
366             }
367             return result;
368         }
369 
370         @Override
doCallback(PdfLoaderCallbacks callbacks, PdfStatus status)371         protected void doCallback(PdfLoaderCallbacks callbacks, PdfStatus status) {
372             // TODO: Track the state of the FileInfo object.
373             switch (status) {
374                 case LOADED:
375                     mLoadedPassword = mPassword;
376                     mConnection.setDocumentLoaded();
377                     // TODO: Track loaded PDF info.
378                     callbacks.documentLoaded(mNumPages, mData);
379                     break;
380                 case REQUIRES_PASSWORD:
381                     // TODO: Reflect this in the state of the FileInfo object.
382                     boolean wrongPasswordSupplied = !TextUtils.isEmpty(mPassword);
383                     callbacks.requestPassword(wrongPasswordSupplied);
384                     break;
385                 case FILE_ERROR:
386                 case PDF_ERROR:
387                 case NONE:
388                     callbacks.documentNotLoaded(status);
389                     break;
390                 default:
391                     // TODO: Add default case to non-exhaustive switches on Java enums
392             }
393         }
394 
395         @Override
cleanup()396         protected void cleanup() { /* nothing to do. */ }
397 
398         @Override
toString()399         public @NonNull String toString() {
400             return "LoadDocumentTask(" + mData + ")";
401         }
402     }
403 }
404