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.annotation.SuppressLint;
20 import android.graphics.Bitmap;
21 import android.graphics.Point;
22 import android.os.RemoteException;
23 
24 import androidx.annotation.RestrictTo;
25 import androidx.pdf.R;
26 import androidx.pdf.models.Dimensions;
27 import androidx.pdf.models.GotoLink;
28 import androidx.pdf.models.LinkRects;
29 import androidx.pdf.models.MatchRects;
30 import androidx.pdf.models.PageSelection;
31 import androidx.pdf.models.SelectionBoundary;
32 import androidx.pdf.service.PdfDocumentRemoteProto;
33 import androidx.pdf.util.TileBoard.TileInfo;
34 
35 import org.jspecify.annotations.NonNull;
36 
37 import java.util.Collections;
38 import java.util.Iterator;
39 import java.util.List;
40 import java.util.Map;
41 import java.util.Map.Entry;
42 import java.util.Objects;
43 
44 /**
45  * Loads data for an individual page of the PDF document. Makes sure that if
46  * the same data is requested more than once, it doesn't lead to duplicate
47  * tasks being scheduled.
48  * <p>
49  * For all bitmap loading tasks, it will cancel any task for a lower or different resolution when
50  * a new request is accepted.
51  */
52 @RestrictTo(RestrictTo.Scope.LIBRARY)
53 public class PdfPageLoader {
54     public static final String TAG = PdfPageLoader.class.getSimpleName();
55 
56     /** Arbitrary dimensions used for pages that are broken. */
57     private static final Dimensions DEFAULT_PAGE = new Dimensions(400, 400);
58 
59     private final PdfLoader mParent;
60     private final int mPageNum;
61     private final boolean mHideTextAnnotations;
62     /** Currently scheduled tasks - null if no task of this type is scheduled. */
63     GetDimensionsTask mDimensionsTask;
64     RenderBitmapTask mBitmapTask;
65     GetPageTextTask mTextTask;
66     SearchPageTextTask mSearchPageTextTask;
67     SelectionTask mSelectionTask;
68     GetPageLinksTask mLinksTask;
69     GetPageGotoLinksTask mGotoLinksTask;
70     ReleasePageTask mReleasePageTask;
71 
72     /**
73      * All currently scheduled tile tasks.
74      *
75      * <p>Map is preferred to SparseArray because of frequent access by key, and leaner API (e.g.
76      * combined remove-get).
77      */
78     @SuppressLint({"UseSparseArrays", "BanConcurrentHashMap"})
79     Map<Integer, RenderTileTask> mTileTasks =
80             new java.util.concurrent.ConcurrentHashMap<Integer, RenderTileTask>();
81 
82     /** The reference pageWidth for all tile related tasks. */
83     int mTilePageWidth;
84     /**
85      * This flag is set when this page makes pdfClient crash, and we'd better avoid crashing it
86      * again.
87      */
88     private boolean mIsBroken = false;
89 
PdfPageLoader(PdfLoader parent, int pageNum, boolean hideTextAnnotations)90     PdfPageLoader(PdfLoader parent, int pageNum, boolean hideTextAnnotations) {
91         this.mParent = parent;
92         this.mPageNum = pageNum;
93         this.mHideTextAnnotations = hideTextAnnotations;
94     }
95 
96     /** Schedule task to get the page's dimensions. */
loadPageDimensions()97     public void loadPageDimensions() {
98         if (mDimensionsTask == null) {
99             mDimensionsTask = new GetDimensionsTask();
100             if (mIsBroken) {
101                 mDimensionsTask.reportError(mParent.getCallbacks());
102             } else {
103                 mParent.mExecutor.schedule(mDimensionsTask);
104             }
105         }
106     }
107 
cancelPageDimensions()108     private void cancelPageDimensions() {
109         if (mDimensionsTask != null) {
110             mDimensionsTask.cancel();
111             mDimensionsTask = null;
112         }
113     }
114 
115     /** Schedule task to render a page as a bitmap. */
loadPageBitmap(@onNull Dimensions pageSize)116     public void loadPageBitmap(@NonNull Dimensions pageSize) {
117         if (mBitmapTask != null && mBitmapTask.mDimensions.getWidth() < pageSize.getWidth()) {
118             cancelPageBitmap();
119         }
120         if (mBitmapTask == null) {
121             mBitmapTask = new RenderBitmapTask(pageSize);
122             if (mIsBroken) {
123                 mBitmapTask.reportError(mParent.getCallbacks());
124             } else {
125                 mParent.mExecutor.schedule(mBitmapTask);
126             }
127         }
128     }
129 
cancelPageBitmap()130     private void cancelPageBitmap() {
131         if (mBitmapTask != null) {
132             mBitmapTask.cancel();
133             mBitmapTask = null;
134         }
135     }
136 
137     /**
138      * Loads the given tiles. If there are any pending tasks targeting a different page size that
139      * this one, they are all canceled. If any tile request is a duplicate, it will not be
140      * scheduled.
141      */
loadTilesBitmaps(@onNull Dimensions pageSize, @NonNull Iterable<TileInfo> tiles)142     public void loadTilesBitmaps(@NonNull Dimensions pageSize, @NonNull Iterable<TileInfo> tiles) {
143         if (!mTileTasks.isEmpty() && mTilePageWidth != pageSize.getWidth()) {
144             cancelAllTileBitmaps();
145         }
146         if (!mIsBroken) {
147             for (TileInfo tile : tiles) {
148                 RenderTileTask tileTask = new RenderTileTask(pageSize, tile);
149                 if (!mTileTasks.containsKey(tile.getIndex())) {
150                     mTileTasks.put(tile.getIndex(), tileTask);
151                     mParent.mExecutor.schedule(tileTask);
152                 }
153             }
154             mTilePageWidth = pageSize.getWidth();
155         }
156     }
157 
cancelTileBitmaps(Iterable<Integer> tileIds)158     void cancelTileBitmaps(Iterable<Integer> tileIds) {
159         for (int tileId : tileIds) {
160             RenderTileTask task = mTileTasks.remove(tileId);
161             if (task != null) {
162                 task.cancel();
163             }
164         }
165     }
166 
cancelAllTileBitmaps()167     void cancelAllTileBitmaps() {
168         // Unusual iteration since cancelling a task removes it from the tileTasks,
169         // which can lead to ConcurrentModificationException if we are iterating over it.
170         Iterator<Entry<Integer, RenderTileTask>> it = mTileTasks.entrySet().iterator();
171         while (it.hasNext()) {
172             RenderTileTask task = it.next().getValue();
173             it.remove();
174             task.cancel();
175         }
176         mTileTasks.clear();
177         mTilePageWidth = 0;
178     }
179 
180     /** Schedule task to get a page's text. */
loadPageText()181     public void loadPageText() {
182         if (!mIsBroken && mTextTask == null) {
183             mTextTask = new GetPageTextTask();
184             mParent.mExecutor.schedule(mTextTask);
185         }
186     }
187 
cancelPageText()188     private void cancelPageText() {
189         if (mTextTask != null) {
190             mTextTask.cancel();
191             mTextTask = null;
192         }
193     }
194 
195     /** Schedule task to search a page's text. */
searchPageText(@onNull String query)196     public void searchPageText(@NonNull String query) {
197         if (!mIsBroken && mSearchPageTextTask != null && !mSearchPageTextTask.mQuery.equals(
198                 query)) {
199             cancelSearch();
200         }
201         if (mSearchPageTextTask == null) {
202             mSearchPageTextTask = new SearchPageTextTask(query);
203             mParent.mExecutor.schedule(mSearchPageTextTask);
204         }
205     }
206 
cancelSearch()207     private void cancelSearch() {
208         if (mSearchPageTextTask != null) {
209             mSearchPageTextTask.cancel();
210             mSearchPageTextTask = null;
211         }
212     }
213 
214     /** Schedule task to select some of the page text. */
selectPageText(@onNull SelectionBoundary start, @NonNull SelectionBoundary stop)215     public void selectPageText(@NonNull SelectionBoundary start, @NonNull SelectionBoundary stop) {
216         // These tasks will be requested almost continuously as long as the user
217         // is dragging a handle - only start a new one if we finished the last one.
218         if (mSelectionTask != null) {
219             if (Objects.equals(start, stop)) {
220                 cancelSelect();  // New selection at a single point - cancel existing task.
221             } else {
222                 return;  // Dragging: just wait for the currently running task to finish.
223             }
224         }
225         if (!mIsBroken && mSelectionTask == null) {
226             mSelectionTask = new SelectionTask(start, stop);
227             mParent.mExecutor.schedule(mSelectionTask);
228         }
229     }
230 
cancelSelect()231     private void cancelSelect() {
232         if (mSelectionTask != null) {
233             mSelectionTask.cancel();
234             mSelectionTask = null;
235         }
236     }
237 
238     /** Schedule task to get a page's url links. */
loadPageLinks()239     public void loadPageLinks() {
240         if (!mIsBroken && mLinksTask == null) {
241             mLinksTask = new GetPageLinksTask();
242             mParent.mExecutor.schedule(mLinksTask);
243         }
244     }
245 
cancelLinks()246     private void cancelLinks() {
247         if (mLinksTask != null) {
248             mLinksTask.cancel();
249             mLinksTask = null;
250         }
251     }
252 
253     /** Schedule task to get a page's goto links. */
loadPageGotoLinks()254     public void loadPageGotoLinks() {
255         if (!mIsBroken && mGotoLinksTask == null) {
256             mGotoLinksTask = new GetPageGotoLinksTask();
257             mParent.mExecutor.schedule(mGotoLinksTask);
258         }
259     }
260 
261     /** Releases object in memory related to a page when that page is no longer visible. */
releasePage()262     public void releasePage() {
263         if (mReleasePageTask == null) {
264             mReleasePageTask = new ReleasePageTask();
265             mParent.mExecutor.schedule(mReleasePageTask);
266         }
267     }
268 
269     /**
270      *
271      */
cancel()272     public void cancel() {
273         cancelExceptSearchAndFormFilling();
274         cancelSearch();
275     }
276 
277     /** Cancel all tasks except search and form-filling. */
cancelExceptSearchAndFormFilling()278     public void cancelExceptSearchAndFormFilling() {
279         cancelPageDimensions();
280         cancelPageBitmap();
281         cancelAllTileBitmaps();
282         cancelPageText();
283         cancelSelect();
284         cancelLinks();
285     }
286 
setBroken()287     private void setBroken() {
288         // TODO: Track the broken state of the FileInfo object.
289         if (!mIsBroken) {
290             mIsBroken = true;
291         }
292     }
293 
294     /** AsyncTask for getting a page's dimensions. */
295     class GetDimensionsTask extends AbstractPdfTask<Dimensions> {
296 
GetDimensionsTask()297         GetDimensionsTask() {
298             super(mParent, Priority.DIMENSIONS);
299         }
300 
301         @Override
getLogTag()302         protected String getLogTag() {
303             return "GetDimensionsTask";
304         }
305 
306         @Override
doInBackground(PdfDocumentRemoteProto pdfDocument)307         protected Dimensions doInBackground(PdfDocumentRemoteProto pdfDocument)
308                 throws RemoteException {
309             return pdfDocument.getPdfDocumentRemote().getPageDimensions(mPageNum);
310         }
311 
312         @Override
doCallback(PdfLoaderCallbacks callbacks, Dimensions result)313         protected void doCallback(PdfLoaderCallbacks callbacks, Dimensions result) {
314             // If invalid dimensions are returned, treat it as page broken and report error
315             if (!arePageDimensionsValid(result)) {
316                 reportError(callbacks);
317             } else {
318                 callbacks.setPageDimensions(mPageNum, result);
319             }
320         }
321 
arePageDimensionsValid(Dimensions dimensions)322         private boolean arePageDimensionsValid(Dimensions dimensions) {
323             return dimensions.getWidth() > 0 && dimensions.getHeight() > 0;
324         }
325 
326         @Override
reportError(PdfLoaderCallbacks callbacks)327         protected void reportError(PdfLoaderCallbacks callbacks) {
328             setBroken();
329             callbacks.setPageDimensions(mPageNum, DEFAULT_PAGE);
330             callbacks.pageBroken(mPageNum);
331         }
332 
333         @Override
cleanup()334         protected void cleanup() {
335             mDimensionsTask = null;
336         }
337     }
338 
339     /** AsyncTask for rendering a page as a bitmap. */
340     class RenderBitmapTask extends AbstractPdfTask<Bitmap> {
341 
342         final Dimensions mDimensions;
343 
RenderBitmapTask(Dimensions dimensions)344         RenderBitmapTask(Dimensions dimensions) {
345             super(mParent, Priority.BITMAP);
346             this.mDimensions = dimensions;
347         }
348 
349         @Override
getLogTag()350         protected String getLogTag() {
351             return "RenderBitmapTask";
352         }
353 
354         @Override
doInBackground(PdfDocumentRemoteProto pdfDocument)355         protected Bitmap doInBackground(PdfDocumentRemoteProto pdfDocument) throws RemoteException {
356             return pdfDocument.getPdfDocumentRemote().renderPage(mPageNum, mDimensions.getWidth(),
357                     mDimensions.getHeight(),
358                     mHideTextAnnotations);
359         }
360 
361         @Override
doCallback(PdfLoaderCallbacks callbacks, Bitmap result)362         protected void doCallback(PdfLoaderCallbacks callbacks, Bitmap result) {
363             if (result != null) {
364                 callbacks.setPageBitmap(mPageNum, result);
365             }
366         }
367 
368         @Override
cleanup()369         protected void cleanup() {
370             mBitmapTask = null;
371         }
372 
373         @Override
reportError(PdfLoaderCallbacks callbacks)374         protected void reportError(PdfLoaderCallbacks callbacks) {
375             setBroken();
376             callbacks.pageBroken(mPageNum);
377         }
378 
379         @Override
toString()380         public @NonNull String toString() {
381             return String.format("RenderBitmapTask(page=%d width=%d height=%d)",
382                     mPageNum, mDimensions.getWidth(), mDimensions.getHeight());
383         }
384     }
385 
386     /** AsyncTask for releasing page objects from memory after it is no longer visible. */
387     class ReleasePageTask extends AbstractPdfTask<Void> {
ReleasePageTask()388         ReleasePageTask() {
389             super(mParent, Priority.RELEASE);
390         }
391 
392         @Override
getLogTag()393         protected String getLogTag() {
394             return "ReleasePageTask";
395         }
396 
397         @Override
doInBackground(PdfDocumentRemoteProto pdfDocument)398         protected Void doInBackground(PdfDocumentRemoteProto pdfDocument) throws RemoteException {
399             pdfDocument.getPdfDocumentRemote().releasePage(mPageNum);
400             return null;
401         }
402 
403         @Override
doCallback(PdfLoaderCallbacks callbacks, Void unused)404         protected void doCallback(PdfLoaderCallbacks callbacks, Void unused) {
405             /* no-op */
406         }
407 
408         @Override
cleanup()409         protected void cleanup() {
410             mReleasePageTask = null;
411         }
412 
413         @Override
toString()414         public @NonNull String toString() {
415             return String.format("ReleasePageTask(page=%d)", mPageNum);
416         }
417     }
418 
419     /** AsyncTask for rendering a tile as a bitmap. */
420     class RenderTileTask extends AbstractPdfTask<Bitmap> {
421         final Dimensions mPageSize;
422         final TileInfo mTileInfo;
423 
RenderTileTask(Dimensions pageDimensions, TileInfo tileInfo)424         RenderTileTask(Dimensions pageDimensions, TileInfo tileInfo) {
425             super(mParent, Priority.BITMAP_TILE);
426             this.mPageSize = pageDimensions;
427             this.mTileInfo = tileInfo;
428         }
429 
430         @Override
getLogTag()431         protected String getLogTag() {
432             return "RenderTileTask";
433         }
434 
435         @Override
doInBackground(PdfDocumentRemoteProto pdfDocument)436         protected Bitmap doInBackground(PdfDocumentRemoteProto pdfDocument) throws RemoteException {
437             Point offset = mTileInfo.getOffset();
438             return pdfDocument.getPdfDocumentRemote().renderTile(
439                     mPageNum,
440                     mTileInfo.getSize().getWidth(),
441                     mTileInfo.getSize().getHeight(),
442                     mPageSize.getWidth(),
443                     mPageSize.getHeight(),
444                     offset.x,
445                     offset.y,
446                     mHideTextAnnotations);
447         }
448 
449         @Override
doCallback(PdfLoaderCallbacks callbacks, Bitmap result)450         protected void doCallback(PdfLoaderCallbacks callbacks, Bitmap result) {
451             if (result != null) {
452                 callbacks.setTileBitmap(mPageNum, mTileInfo, result);
453             }
454         }
455 
456         @Override
cleanup()457         protected void cleanup() {
458             mTileTasks.remove(mTileInfo.getIndex());
459         }
460 
461         @Override
toString()462         public @NonNull String toString() {
463             return String.format("RenderTileTask(page=%d width=%d height=%d tile=%s)",
464                     mPageNum, mPageSize.getWidth(), mPageSize.getHeight(), mTileInfo);
465         }
466 
467         @Override
equals(Object o)468         public boolean equals(Object o) {
469             if (o != null && o instanceof RenderTileTask) {
470                 RenderTileTask that = (RenderTileTask) o;
471                 return this.mPageSize.equals(that.mPageSize) && this.mTileInfo.equals(
472                         that.mTileInfo);
473             }
474             return false;
475         }
476 
477         @Override
hashCode()478         public int hashCode() {
479             return mTileInfo.hashCode();
480         }
481     }
482 
483     /** AsyncTask for getting a page's text. */
484     class GetPageTextTask extends AbstractPdfTask<String> {
GetPageTextTask()485         GetPageTextTask() {
486             super(mParent, Priority.TEXT);
487         }
488 
489         @Override
getLogTag()490         protected String getLogTag() {
491             return "GetPageTextTask";
492         }
493 
494         @Override
doInBackground(PdfDocumentRemoteProto pdfDocument)495         protected String doInBackground(PdfDocumentRemoteProto pdfDocument) throws RemoteException {
496             if (TaskDenyList.sDisableAltText) {
497                 return pdfDocument.getPdfDocumentRemote().getPageText(mPageNum);
498             } else {
499                 StringBuilder sb = new StringBuilder();
500                 sb.append(pdfDocument.getPdfDocumentRemote().getPageText(mPageNum));
501                 sb.append("\r\n");
502                 for (String altText : pdfDocument.getPdfDocumentRemote().getPageAltText(mPageNum)) {
503                     // Format the alt text appropriately, so that user knows it is alt text.
504                     altText = mParent.mContext.getString(R.string.desc_image_alt_text, altText);
505                     sb.append(altText).append("\r\n");
506                 }
507                 return sb.toString();
508             }
509         }
510 
511         @Override
doCallback(PdfLoaderCallbacks callbacks, String result)512         protected void doCallback(PdfLoaderCallbacks callbacks, String result) {
513             callbacks.setPageText(mPageNum, result);
514         }
515 
516         @Override
cleanup()517         protected void cleanup() {
518             mTextTask = null;
519         }
520 
521         @Override
toString()522         public @NonNull String toString() {
523             return String.format("GetPageTextTask(page=%d)", mPageNum);
524         }
525     }
526 
527     /** AsyncTask for searching a page's text. */
528     class SearchPageTextTask extends AbstractPdfTask<MatchRects> {
529         private final String mQuery;
530 
SearchPageTextTask(String query)531         SearchPageTextTask(String query) {
532             super(mParent, Priority.SEARCH);
533             this.mQuery = query;
534         }
535 
536         @Override
getLogTag()537         protected String getLogTag() {
538             return "SearchPageTextTask";
539         }
540 
541         @Override
doInBackground(PdfDocumentRemoteProto pdfDocument)542         protected MatchRects doInBackground(PdfDocumentRemoteProto pdfDocument)
543                 throws RemoteException {
544             return pdfDocument.getPdfDocumentRemote().searchPageText(mPageNum, mQuery);
545         }
546 
547         @Override
doCallback(PdfLoaderCallbacks callbacks, MatchRects matches)548         protected void doCallback(PdfLoaderCallbacks callbacks, MatchRects matches) {
549             callbacks.setSearchResults(mQuery, mPageNum, matches);
550         }
551 
552         @Override
cleanup()553         protected void cleanup() {
554             mSearchPageTextTask = null;
555         }
556 
557         @Override
toString()558         public @NonNull String toString() {
559             return String.format("SearchPageTextTask(page=%d, query=\"%s\")", mPageNum, mQuery);
560         }
561     }
562 
563     /** AsyncTask for selecting some of a page's text. */
564     class SelectionTask extends AbstractPdfTask<PageSelection> {
565         private final SelectionBoundary mStart;
566         private final SelectionBoundary mStop;
567 
SelectionTask(SelectionBoundary start, SelectionBoundary stop)568         SelectionTask(SelectionBoundary start, SelectionBoundary stop) {
569             super(mParent, Priority.SELECT);
570             this.mStart = start;
571             this.mStop = stop;
572         }
573 
574         @Override
getLogTag()575         protected String getLogTag() {
576             return "SelectionTask";
577         }
578 
579         @Override
doInBackground(PdfDocumentRemoteProto pdfDocument)580         protected PageSelection doInBackground(PdfDocumentRemoteProto pdfDocument)
581                 throws RemoteException {
582             return pdfDocument.getPdfDocumentRemote().selectPageText(mPageNum, mStart, mStop);
583         }
584 
585         @Override
doCallback(PdfLoaderCallbacks callbacks, PageSelection selection)586         protected void doCallback(PdfLoaderCallbacks callbacks, PageSelection selection) {
587             callbacks.setSelection(mPageNum, selection);
588         }
589 
590         @Override
cleanup()591         protected void cleanup() {
592             mSelectionTask = null;
593         }
594 
595         @Override
toString()596         public @NonNull String toString() {
597             return String.format("SelectionTask(page=%d, start=%s, stop=%s)", mPageNum, mStart,
598                     mStop);
599         }
600     }
601 
602     /** AsyncTask for getting a page's url links. */
603     class GetPageLinksTask extends AbstractPdfTask<LinkRects> {
GetPageLinksTask()604         GetPageLinksTask() {
605             super(mParent, Priority.LINKS);
606         }
607 
608         @Override
getLogTag()609         protected String getLogTag() {
610             return "GetPageLinksTask";
611         }
612 
613         @Override
doInBackground(PdfDocumentRemoteProto pdfDocument)614         protected LinkRects doInBackground(PdfDocumentRemoteProto pdfDocument)
615                 throws RemoteException {
616             if (TaskDenyList.sDisableLinks) {
617                 return LinkRects.NO_LINKS;
618             } else {
619                 return pdfDocument.getPdfDocumentRemote().getPageLinks(mPageNum);
620             }
621         }
622 
623         @Override
doCallback(PdfLoaderCallbacks callbacks, LinkRects result)624         protected void doCallback(PdfLoaderCallbacks callbacks, LinkRects result) {
625             callbacks.setPageUrlLinks(mPageNum, result);
626         }
627 
628         @Override
cleanup()629         protected void cleanup() {
630             mLinksTask = null;
631         }
632 
633         @Override
toString()634         public @NonNull String toString() {
635             return String.format("GetPageLinksTask(page=%d)", mPageNum);
636         }
637     }
638 
639     /** AsyncTask for getting a page's go-to links. */
640     class GetPageGotoLinksTask extends AbstractPdfTask<List<GotoLink>> {
GetPageGotoLinksTask()641         GetPageGotoLinksTask() {
642             // Go-to links are a subset of links so we will follow all link settings.
643             super(mParent, Priority.LINKS);
644         }
645 
646         @Override
getLogTag()647         protected String getLogTag() {
648             return "GetPageGotoLinksTask";
649         }
650 
651         @Override
doInBackground(PdfDocumentRemoteProto pdfDocument)652         protected List<GotoLink> doInBackground(PdfDocumentRemoteProto pdfDocument)
653                 throws RemoteException {
654             if (TaskDenyList.sDisableLinks) {
655                 return Collections.emptyList();
656             } else {
657                 return pdfDocument.getPdfDocumentRemote().getPageGotoLinks(mPageNum);
658             }
659         }
660 
661         @Override
doCallback(PdfLoaderCallbacks callbacks, List<GotoLink> links)662         protected void doCallback(PdfLoaderCallbacks callbacks, List<GotoLink> links) {
663             callbacks.setPageGotoLinks(mPageNum, links);
664         }
665 
666         @Override
cleanup()667         protected void cleanup() {
668             mGotoLinksTask = null;
669         }
670 
671         @Override
toString()672         public @NonNull String toString() {
673             return String.format("GetPageGotoLinksTask(page=%d)", mPageNum);
674         }
675     }
676 }
677