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