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