1 /*
2  * Copyright (C) 2019 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 com.example.android.pdfrendererbasic;
18 
19 import android.app.Application;
20 import android.graphics.Bitmap;
21 import android.graphics.pdf.PdfRenderer;
22 import android.os.ParcelFileDescriptor;
23 import android.util.Log;
24 
25 import java.io.File;
26 import java.io.FileOutputStream;
27 import java.io.IOException;
28 import java.io.InputStream;
29 import java.util.concurrent.Executor;
30 import java.util.concurrent.Executors;
31 
32 import androidx.annotation.WorkerThread;
33 import androidx.lifecycle.AndroidViewModel;
34 import androidx.lifecycle.LiveData;
35 import androidx.lifecycle.MutableLiveData;
36 
37 /**
38  * This holds all the data we need for this sample app.
39  * <p>
40  * We use a {@link android.graphics.pdf.PdfRenderer} to render PDF pages as
41  * {@link android.graphics.Bitmap}s.
42  */
43 public class PdfRendererBasicViewModel extends AndroidViewModel {
44 
45     private static final String TAG = "PdfRendererBasic";
46 
47     /**
48      * The filename of the PDF.
49      */
50     private static final String FILENAME = "sample.pdf";
51 
52     private final MutableLiveData<PageInfo> mPageInfo = new MutableLiveData<>();
53     private final MutableLiveData<Bitmap> mPageBitmap = new MutableLiveData<>();
54     private final MutableLiveData<Boolean> mPreviousEnabled = new MutableLiveData<>();
55     private final MutableLiveData<Boolean> mNextEnabled = new MutableLiveData<>();
56 
57     private final Executor mExecutor;
58     private ParcelFileDescriptor mFileDescriptor;
59     private PdfRenderer mPdfRenderer;
60     private PdfRenderer.Page mCurrentPage;
61     private boolean mCleared;
62 
63     @SuppressWarnings("unused")
PdfRendererBasicViewModel(Application application)64     public PdfRendererBasicViewModel(Application application) {
65         this(application, false);
66     }
67 
PdfRendererBasicViewModel(Application application, boolean useInstantExecutor)68     PdfRendererBasicViewModel(Application application, boolean useInstantExecutor) {
69         super(application);
70         if (useInstantExecutor) {
71             mExecutor = Runnable::run;
72         } else {
73             mExecutor = Executors.newSingleThreadExecutor();
74         }
75         mExecutor.execute(() -> {
76             try {
77                 openPdfRenderer();
78                 showPage(0);
79                 if (mCleared) {
80                     closePdfRenderer();
81                 }
82             } catch (IOException e) {
83                 Log.e(TAG, "Failed to open PdfRenderer", e);
84             }
85         });
86     }
87 
88     @Override
onCleared()89     protected void onCleared() {
90         super.onCleared();
91         mExecutor.execute(() -> {
92             try {
93                 closePdfRenderer();
94                 mCleared = true;
95             } catch (IOException e) {
96                 Log.i(TAG, "Failed to close PdfRenderer", e);
97             }
98         });
99     }
100 
getPageInfo()101     LiveData<PageInfo> getPageInfo() {
102         return mPageInfo;
103     }
104 
getPageBitmap()105     LiveData<Bitmap> getPageBitmap() {
106         return mPageBitmap;
107     }
108 
getPreviousEnabled()109     LiveData<Boolean> getPreviousEnabled() {
110         return mPreviousEnabled;
111     }
112 
getNextEnabled()113     LiveData<Boolean> getNextEnabled() {
114         return mNextEnabled;
115     }
116 
showPrevious()117     void showPrevious() {
118         if (mPdfRenderer == null || mCurrentPage == null) {
119             return;
120         }
121         final int index = mCurrentPage.getIndex();
122         if (index > 0) {
123             mExecutor.execute(() -> showPage(index - 1));
124         }
125     }
126 
showNext()127     void showNext() {
128         if (mPdfRenderer == null || mCurrentPage == null) {
129             return;
130         }
131         final int index = mCurrentPage.getIndex();
132         if (index + 1 < mPdfRenderer.getPageCount()) {
133             mExecutor.execute(() -> showPage(index + 1));
134         }
135     }
136 
137     @WorkerThread
openPdfRenderer()138     private void openPdfRenderer() throws IOException {
139         final File file = new File(getApplication().getCacheDir(), FILENAME);
140         if (!file.exists()) {
141             // Since PdfRenderer cannot handle the compressed asset file directly, we copy it into
142             // the cache directory.
143             final InputStream asset = getApplication().getAssets().open(FILENAME);
144             final FileOutputStream output = new FileOutputStream(file);
145             final byte[] buffer = new byte[1024];
146             int size;
147             while ((size = asset.read(buffer)) != -1) {
148                 output.write(buffer, 0, size);
149             }
150             asset.close();
151             output.close();
152         }
153         mFileDescriptor = ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY);
154         if (mFileDescriptor != null) {
155             mPdfRenderer = new PdfRenderer(mFileDescriptor);
156         }
157     }
158 
159     @WorkerThread
closePdfRenderer()160     private void closePdfRenderer() throws IOException {
161         if (mCurrentPage != null) {
162             mCurrentPage.close();
163         }
164         if (mPdfRenderer != null) {
165             mPdfRenderer.close();
166         }
167         if (mFileDescriptor != null) {
168             mFileDescriptor.close();
169         }
170     }
171 
172     @WorkerThread
showPage(int index)173     private void showPage(int index) {
174         // Make sure to close the current page before opening another one.
175         if (null != mCurrentPage) {
176             mCurrentPage.close();
177         }
178         // Use `openPage` to open a specific page in PDF.
179         mCurrentPage = mPdfRenderer.openPage(index);
180         // Important: the destination bitmap must be ARGB (not RGB).
181         final Bitmap bitmap = Bitmap.createBitmap(mCurrentPage.getWidth(), mCurrentPage.getHeight(),
182                 Bitmap.Config.ARGB_8888);
183         // Here, we render the page onto the Bitmap.
184         // To render a portion of the page, use the second and third parameter. Pass nulls to get
185         // the default result.
186         // Pass either RENDER_MODE_FOR_DISPLAY or RENDER_MODE_FOR_PRINT for the last parameter.
187         mCurrentPage.render(bitmap, null, null, PdfRenderer.Page.RENDER_MODE_FOR_DISPLAY);
188         mPageBitmap.postValue(bitmap);
189         final int count = mPdfRenderer.getPageCount();
190         mPageInfo.postValue(new PageInfo(index, count));
191         mPreviousEnabled.postValue(index > 0);
192         mNextEnabled.postValue(index + 1 < count);
193     }
194 
195     static class PageInfo {
196         final int index;
197         final int count;
198 
PageInfo(int index, int count)199         PageInfo(int index, int count) {
200             this.index = index;
201             this.count = count;
202         }
203     }
204 
205 }
206