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 static com.google.common.truth.Truth.assertThat;
20 
21 import static org.junit.Assert.fail;
22 import static org.mockito.ArgumentMatchers.any;
23 import static org.mockito.ArgumentMatchers.anyInt;
24 import static org.mockito.Mockito.verify;
25 import static org.mockito.Mockito.when;
26 
27 import android.content.Context;
28 import android.graphics.Rect;
29 import android.os.ParcelFileDescriptor;
30 import android.os.RemoteException;
31 
32 import androidx.pdf.data.DisplayData;
33 import androidx.pdf.data.Opener;
34 import androidx.pdf.data.PdfStatus;
35 import androidx.pdf.models.Dimensions;
36 import androidx.pdf.models.GotoLink;
37 import androidx.pdf.models.LinkRects;
38 import androidx.pdf.models.MatchRects;
39 import androidx.pdf.models.PageSelection;
40 import androidx.pdf.models.PdfDocumentRemote;
41 import androidx.pdf.models.SelectionBoundary;
42 import androidx.pdf.util.RectUtils;
43 import androidx.pdf.util.TileBoard;
44 import androidx.pdf.util.TileBoard.CancelTilesCallback;
45 import androidx.pdf.util.TileBoard.TileInfo;
46 import androidx.pdf.util.TileBoard.ViewAreaUpdateCallback;
47 import androidx.pdf.viewer.loader.PdfPageLoader.GetDimensionsTask;
48 import androidx.pdf.viewer.loader.PdfPageLoader.GetPageTextTask;
49 import androidx.pdf.viewer.loader.PdfPageLoader.RenderBitmapTask;
50 import androidx.pdf.viewer.loader.PdfPageLoader.RenderTileTask;
51 import androidx.pdf.viewer.loader.PdfPageLoader.SelectionTask;
52 import androidx.pdf.widget.WidgetType;
53 import androidx.test.annotation.UiThreadTest;
54 import androidx.test.core.app.ApplicationProvider;
55 import androidx.test.ext.junit.runners.AndroidJUnit4;
56 import androidx.test.filters.MediumTest;
57 
58 import com.google.common.base.Objects;
59 import com.google.common.collect.Iterables;
60 
61 import org.jspecify.annotations.NonNull;
62 import org.junit.After;
63 import org.junit.Before;
64 import org.junit.Ignore;
65 import org.junit.Test;
66 import org.junit.runner.RunWith;
67 import org.mockito.ArgumentCaptor;
68 import org.mockito.Captor;
69 import org.mockito.Mock;
70 import org.mockito.MockitoAnnotations;
71 
72 import java.io.File;
73 import java.io.FileOutputStream;
74 import java.util.ArrayList;
75 import java.util.Arrays;
76 import java.util.List;
77 import java.util.concurrent.CountDownLatch;
78 import java.util.concurrent.TimeUnit;
79 
80 /** Unit tests for {@link PdfLoader}. */
81 @RunWith(AndroidJUnit4.class)
82 @MediumTest
83 public class PdfLoaderTest {
84 
85     private static final int PAGE = 5;
86 
87     private Context mContext;
88     @Mock
89     private PdfDocumentRemote mPdfDocument;
90     @Mock
91     private DisplayData mDisplayData;
92     @Mock
93     private PdfConnection mConnection;
94     @Mock
95     private ParcelFileDescriptor mParcelFileDescriptor;
96 
97     @Captor
98     private ArgumentCaptor<List<WidgetType>> mListArgumentCaptor;
99 
100     private TestCallbacks mWeakPdfLoaderCallbacks;
101     private FileOutputStream mFileOutputStream;
102     private PdfLoader mPdfLoader;
103 
104     private static final int TEST_FD = 1234;
105     private static final String TEST_PW = "TESTPW";
106 
107     /** {@link PdfTaskExecutor} waits 10 seconds if it doesn't have any tasks, so we use 12. */
108     private static final int LATCH_TIMEOUT_MS = 12000;
109     private AutoCloseable mCloseable;
110 
111     @Before
setUp()112     public void setUp() throws Exception {
113         mCloseable = MockitoAnnotations.openMocks(this);
114         mContext = ApplicationProvider.getApplicationContext();
115 
116         when(mConnection.isLoaded()).thenReturn(true);
117         when(mConnection.getPdfDocument(any())).thenReturn(mPdfDocument);
118         when(mDisplayData.openFd(any(Opener.class))).thenReturn(mParcelFileDescriptor);
119         when(mParcelFileDescriptor.getFd()).thenReturn(TEST_FD);
120         when(mPdfDocument.create(mParcelFileDescriptor, TEST_PW))
121                 .thenReturn(PdfStatus.LOADED.getNumber());
122 
123         mWeakPdfLoaderCallbacks = new TestCallbacks();
124         mPdfLoader =
125                 new PdfLoader(
126                         mContext,
127                         mConnection,
128                         mDisplayData,
129                         TileBoard.DEFAULT_RECYCLER,
130                         mWeakPdfLoaderCallbacks,
131                         false /* hideTextAnnotations */);
132 
133         File file = new File(mContext.getCacheDir(), "test");
134         mFileOutputStream = new FileOutputStream(file);
135     }
136 
137     @After
cleanUp()138     public void cleanUp() {
139         try {
140             mCloseable.close();
141         } catch (Exception e) {
142             // No-op
143         }
144     }
145 
146     @Test
147     @UiThreadTest
testLoadDimensions()148     public void testLoadDimensions() {
149         Dimensions testDimensions = new Dimensions(100, 200);
150         try {
151             when(mPdfDocument.getPageDimensions(PAGE)).thenReturn(testDimensions);
152         } catch (RemoteException e) {
153             fail(e.getMessage());
154         }
155 
156         mPdfLoader.loadPageDimensions(PAGE);
157         GetDimensionsTask task = mPdfLoader.getPageLoader(PAGE).mDimensionsTask;
158         assertThat(task.isCancelled()).isFalse();
159 
160         mPdfLoader.loadPageDimensions(PAGE);
161         assertThat(mPdfLoader.getPageLoader(PAGE).mDimensionsTask).isSameInstanceAs(task);
162         assertThat(task.isCancelled()).isFalse();
163 
164         mPdfLoader.cancelExceptSearchAndFormFilling(PAGE);
165         assertThat(task.isCancelled()).isTrue();
166         assertThat(mPdfLoader.getPageLoader(PAGE).mDimensionsTask).isNull();
167     }
168 
169     @Test
170     @UiThreadTest
testLoadBitmap()171     public void testLoadBitmap() {
172         Dimensions original = new Dimensions(300, 400);
173         mPdfLoader.loadPageBitmap(PAGE, original);
174         RenderBitmapTask task1 = mPdfLoader.getPageLoader(PAGE).mBitmapTask;
175         assertThat(task1.mDimensions).isEqualTo(original);
176         assertThat(task1.isCancelled()).isFalse();
177 
178         mPdfLoader.loadPageBitmap(PAGE, original);
179         assertThat(mPdfLoader.getPageLoader(PAGE).mBitmapTask).isSameInstanceAs(task1);
180         assertThat(task1.isCancelled()).isFalse();
181 
182         Dimensions smaller = new Dimensions(150, 200);
183         mPdfLoader.loadPageBitmap(PAGE, smaller);
184         assertThat(mPdfLoader.getPageLoader(PAGE).mBitmapTask).isSameInstanceAs(task1);
185         assertThat(task1.isCancelled()).isFalse();
186 
187         Dimensions bigger = new Dimensions(600, 800);
188         mPdfLoader.loadPageBitmap(PAGE, bigger);
189         assertThat(mPdfLoader.getPageLoader(PAGE).mBitmapTask).isNotSameInstanceAs(task1);
190         assertThat(task1.isCancelled()).isTrue();
191 
192         RenderBitmapTask task2 = mPdfLoader.getPageLoader(PAGE).mBitmapTask;
193         assertThat(task2.mDimensions).isEqualTo(bigger);
194         assertThat(task2.isCancelled()).isFalse();
195 
196         mPdfLoader.cancel(PAGE);
197         assertThat(task2.isCancelled()).isTrue();
198         assertThat(mPdfLoader.getPageLoader(PAGE).mBitmapTask).isNull();
199     }
200 
201     @Test
202     @UiThreadTest
testLoadText()203     public void testLoadText() {
204         mPdfLoader.loadPageText(PAGE);
205         GetPageTextTask task = mPdfLoader.getPageLoader(PAGE).mTextTask;
206         assertThat(task.isCancelled()).isFalse();
207 
208         mPdfLoader.loadPageText(PAGE);
209         assertThat(mPdfLoader.getPageLoader(PAGE).mTextTask).isSameInstanceAs(task);
210         assertThat(task.isCancelled()).isFalse();
211 
212         mPdfLoader.cancel(PAGE);
213         assertThat(task.isCancelled()).isTrue();
214         assertThat(mPdfLoader.getPageLoader(PAGE).mBitmapTask).isNull();
215     }
216 
217     @Test
218     @UiThreadTest
testSelectTask()219     public void testSelectTask() throws RemoteException {
220         SelectionBoundary select1 = new SelectionBoundary(12, 24, 48, false);
221         SelectionBoundary select2 = new SelectionBoundary(9, 18, 27, false);
222         PageSelection selection = new PageSelection(PAGE, select1, select2, new ArrayList<Rect>(),
223                 "test");
224 
225         when(mPdfDocument.selectPageText(anyInt(), any(SelectionBoundary.class),
226                 any(SelectionBoundary.class))).thenReturn(selection);
227 
228         mPdfLoader.selectPageText(PAGE, select1, select2);
229         SelectionTask task = mPdfLoader.getPageLoader(PAGE).mSelectionTask;
230         assertThat(task.isCancelled()).isFalse();
231 
232         // We don't start a new task if there is already one ongoing.
233         mPdfLoader.selectPageText(PAGE, select2, select1);
234         assertThat(mPdfLoader.getPageLoader(PAGE).mSelectionTask).isSameInstanceAs(task);
235         assertThat(task.isCancelled()).isFalse();
236 
237         // We cancel the selection task if the selection boundary arguments are the same.
238         mPdfLoader.selectPageText(PAGE, select1, select1);
239         assertThat(task.isCancelled()).isTrue();
240         assertThat(mPdfLoader.getPageLoader(PAGE).mSelectionTask).isNotNull();
241     }
242 
243     @Ignore // b/342212541
244     @Test
245     @UiThreadTest
testLoadTiles()246     public void testLoadTiles() {
247         Dimensions pageSize = new Dimensions(2000, 1200);
248 
249         List<TileInfo> requestedTiles = getSomeTiles(pageSize);
250 
251         Iterable<TileInfo> aFewTiles = Iterables.limit(requestedTiles, 3);
252         Iterable<TileInfo> firstTile = Iterables.limit(requestedTiles, 1);
253 
254         mPdfLoader.loadTileBitmaps(PAGE, pageSize, aFewTiles);
255         assertThat(mPdfLoader.getPageLoader(PAGE).mTileTasks).hasSize(3);
256         RenderTileTask task1 = mPdfLoader.getPageLoader(PAGE).mTileTasks.get(
257                 Iterables.getOnlyElement(firstTile).getIndex());
258         for (RenderTileTask task : mPdfLoader.getPageLoader(PAGE).mTileTasks.values()) {
259             assertThat(task.isCancelled()).isFalse();
260         }
261 
262         // re-submit one tile
263         mPdfLoader.loadTileBitmaps(0, pageSize, firstTile);
264         assertThat(mPdfLoader.getPageLoader(PAGE).mTileTasks).hasSize(3);
265 
266         mPdfLoader.cancelTileBitmaps(PAGE, Arrays.asList(0));
267         for (RenderTileTask task : mPdfLoader.getPageLoader(PAGE).mTileTasks.values()) {
268             if (Objects.equal(task, task1)) {
269                 assertThat(task.isCancelled()).isTrue();
270             } else {
271                 assertThat(task.isCancelled()).isFalse();
272             }
273         }
274         mPdfLoader.cancelAllTileBitmaps(PAGE);
275         for (RenderTileTask task : mPdfLoader.getPageLoader(PAGE).mTileTasks.values()) {
276             assertThat(task.isCancelled()).isTrue();
277         }
278 
279         assertThat(mPdfLoader.getPageLoader(PAGE).mTileTasks).isEmpty();
280     }
281 
282     @Test
283     @UiThreadTest
testGotoLinksTask()284     public void testGotoLinksTask() throws RemoteException, InterruptedException {
285         getGotoLinks(mPdfLoader);
286         verify(mPdfDocument).getPageGotoLinks(PAGE);
287     }
288 
289     @Test
290     @UiThreadTest
testLoadDocumentTask()291     public void testLoadDocumentTask() throws InterruptedException, RemoteException {
292         CountDownLatch latch = new CountDownLatch(1);
293         mWeakPdfLoaderCallbacks.setDocumentLoadedLatch(latch);
294         // ensure document is not already loaded
295         when(mConnection.isLoaded()).thenReturn(false);
296         mPdfLoader.applyPassword(TEST_PW);
297         /** Wait for {@link TestCallbacks#documentLoaded(int)} ()} to be called. */
298         latch.await(LATCH_TIMEOUT_MS, TimeUnit.MILLISECONDS);
299         verify(mPdfDocument).create(mParcelFileDescriptor, TEST_PW);
300     }
301 
302     @Test
303     @UiThreadTest
testSearchTask()304     public void testSearchTask() throws InterruptedException, RemoteException {
305         CountDownLatch latch = new CountDownLatch(1);
306         mWeakPdfLoaderCallbacks.setSearchResultsLatch(latch);
307         mPdfLoader.searchPageText(PAGE, "testQuery");
308         /** Wait for {@link TestCallbacks#setSearchResults(String, int, MatchRects)} to be called
309          *  . */
310         latch.await(LATCH_TIMEOUT_MS, TimeUnit.MILLISECONDS);
311         verify(mPdfDocument).searchPageText(PAGE, "testQuery");
312     }
313 
314     @Test
315     @UiThreadTest
testPageUrlLinksTask()316     public void testPageUrlLinksTask() throws InterruptedException, RemoteException {
317         when(mPdfDocument.getPageLinks(anyInt())).thenReturn(new LinkRects(
318                 new ArrayList<Rect>(), new ArrayList<Integer>(), new ArrayList<String>()));
319         CountDownLatch latch = new CountDownLatch(1);
320         mWeakPdfLoaderCallbacks.setUrlLinksLatch(latch);
321         mPdfLoader.loadPageUrlLinks(PAGE);
322         /** Wait for {@link TestCallbacks#setPageUrlLinks(int, LinkRects)} to be called. */
323         latch.await(LATCH_TIMEOUT_MS, TimeUnit.MILLISECONDS);
324         verify(mPdfDocument).getPageLinks(PAGE);
325     }
getGotoLinks(PdfLoader pdfLoader)326     private void getGotoLinks(PdfLoader pdfLoader) throws InterruptedException, RemoteException {
327         when(mPdfDocument.getPageGotoLinks(anyInt())).thenReturn(new ArrayList<GotoLink>());
328         CountDownLatch latch = new CountDownLatch(1);
329         mWeakPdfLoaderCallbacks.setGotoLinksLatch(latch);
330         pdfLoader.loadPageGotoLinks(PAGE);
331         /** Wait for {@link TestCallbacks#setPageGotoLinks(int, List)} to be called. */
332         latch.await(LATCH_TIMEOUT_MS, TimeUnit.MILLISECONDS);
333     }
334 
getSomeTiles(Dimensions pageSize)335     private List<TileInfo> getSomeTiles(Dimensions pageSize) {
336         TileBoard board = new TileBoard(PAGE, pageSize, TileBoard.DEFAULT_RECYCLER,
337                 new CancelTilesCallback() {
338                     @Override
339                     public void cancelTiles(Iterable<Integer> tileIds) {
340                         // No action.
341                     }
342                 });
343         final List<TileInfo> requestedTiles = new ArrayList<TileInfo>();
344         board.updateViewArea(RectUtils.fromDimensions(pageSize), new ViewAreaUpdateCallback() {
345             @Override
346             public void requestNewTiles(Iterable<TileInfo> tiles) {
347                 Iterables.addAll(requestedTiles, tiles);
348             }
349 
350             @Override
351             public void discardTiles(Iterable<Integer> tileIds) {
352                 fail("No tile to discard.");
353             }
354         });
355         return requestedTiles;
356     }
357 
358     private static class TestCallbacks extends WeakPdfLoaderCallbacks {
359 
360         private CountDownLatch mClonedLatch;
361         private CountDownLatch mSearchLatch;
362         private CountDownLatch mDocumentLoadedLatch;
363         private CountDownLatch mLinksUrlLatch;
364         private CountDownLatch mGotoLinksLatch;
365 
TestCallbacks()366         private TestCallbacks() {
367             super(null);
368         }
369 
370         @Override
documentLoaded(int numPages, @NonNull DisplayData data)371         public void documentLoaded(int numPages, @NonNull DisplayData data) {
372             super.documentLoaded(numPages, data);
373             if (mDocumentLoadedLatch != null) {
374                 mDocumentLoadedLatch.countDown();
375             }
376         }
377 
378         @Override
setSearchResults(String query, int pageNum, MatchRects matches)379         public void setSearchResults(String query, int pageNum, MatchRects matches) {
380             super.setSearchResults(query, pageNum, matches);
381             if (mSearchLatch != null) {
382                 mSearchLatch.countDown();
383             }
384         }
385 
386         @Override
setPageUrlLinks(int pageNum, LinkRects links)387         public void setPageUrlLinks(int pageNum, LinkRects links) {
388             super.setPageUrlLinks(pageNum, links);
389             if (mLinksUrlLatch != null) {
390                 mLinksUrlLatch.countDown();
391             }
392         }
393         @Override
setPageGotoLinks(int pageNum, List<GotoLink> links)394         public void setPageGotoLinks(int pageNum, List<GotoLink> links) {
395             super.setPageGotoLinks(pageNum, links);
396             if (mLinksUrlLatch != null) {
397                 mLinksUrlLatch.countDown();
398             }
399         }
400 
setClonedLatch(CountDownLatch latch)401         public void setClonedLatch(CountDownLatch latch) {
402             mClonedLatch = latch;
403         }
404 
setDocumentLoadedLatch(CountDownLatch documentLoadedLatch)405         public void setDocumentLoadedLatch(CountDownLatch documentLoadedLatch) {
406             this.mDocumentLoadedLatch = documentLoadedLatch;
407         }
408 
setSearchResultsLatch(CountDownLatch searchResultsLatch)409         public void setSearchResultsLatch(CountDownLatch searchResultsLatch) {
410             this.mSearchLatch = searchResultsLatch;
411         }
412 
setUrlLinksLatch(CountDownLatch linksLatch)413         public void setUrlLinksLatch(CountDownLatch linksLatch) {
414             this.mLinksUrlLatch = linksLatch;
415         }
416 
setGotoLinksLatch(CountDownLatch linksLatch)417         public void setGotoLinksLatch(CountDownLatch linksLatch) {
418             this.mGotoLinksLatch = linksLatch;
419         }
420     }
421 }
422