1 /* <lambda>null2 * 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.view 18 19 import android.graphics.Canvas 20 import android.graphics.Point 21 import android.graphics.Rect 22 import android.util.Range 23 import android.util.SparseArray 24 import androidx.core.util.keyIterator 25 import androidx.core.util.valueIterator 26 import androidx.pdf.PdfDocument 27 import kotlinx.coroutines.CoroutineScope 28 import kotlinx.coroutines.flow.MutableSharedFlow 29 import kotlinx.coroutines.flow.SharedFlow 30 31 /** 32 * Manages a collection of [Page]s, each representing a single PDF page. Receives events to update 33 * pages' internal state, and produces events via a [SharedFlow] of type [Unit] to signal the host 34 * [PdfView] to invalidate itself when any page needs to be redrawn. Operations like drawing pages 35 * and handling touch events on pages may be delegated to this manager. 36 * 37 * Not thread safe 38 */ 39 internal class PageManager( 40 private val pdfDocument: PdfDocument, 41 private val backgroundScope: CoroutineScope, 42 /** 43 * The maximum size of any single [android.graphics.Bitmap] we render for a page, i.e. the 44 * threshold for tiled rendering 45 */ 46 private val maxBitmapSizePx: Point, 47 private val isTouchExplorationEnabled: Boolean, 48 /** Error flow for propagating error occurred while processing to [PdfView]. */ 49 private val errorFlow: MutableSharedFlow<Throwable> 50 ) { 51 /** 52 * Replay at least 1 value in case of an invalidation signal issued while [PdfView] is not 53 * collecting 54 */ 55 private val _invalidationSignalFlow = MutableSharedFlow<Unit>(replay = 1) 56 57 /** 58 * This [SharedFlow] serves as an event bus of sorts to signal our host [PdfView] to invalidate 59 * itself in a decoupled way. It is of type [Unit] because the reason for invalidation is 60 * inconsequential. The model is: we update the data that affects what will be drawn, we signal 61 * [PdfView] to invalidate itself, and the relevant changes in state will be reflected in the 62 * next call to [PdfView.onDraw] 63 */ 64 val invalidationSignalFlow: SharedFlow<Unit> 65 get() = _invalidationSignalFlow 66 67 internal val pages = SparseArray<Page>() 68 69 private val _pageTextReadyFlow = MutableSharedFlow<Int>(replay = 1) 70 val pageTextReadyFlow: SharedFlow<Int> 71 get() = _pageTextReadyFlow 72 73 /** 74 * [Highlight]s supplied by the developer to be drawn along with the pages they belong to 75 * 76 * We store these in a map keyed by page number for more efficient lookup at drawing time, even 77 * though each [Highlight] contains its own page number. 78 */ 79 private val highlights: MutableMap<Int, MutableList<Highlight>> = mutableMapOf() 80 81 /** 82 * Updates the visibility state of [Page]s owned by this manager. 83 * 84 * @param visiblePageAreas the visible area of each visible page, in page coordinates 85 * @param currentZoomLevel the current zoom level 86 * @param stablePosition true if we don't believe our position is actively changing 87 */ 88 fun updatePageVisibilities( 89 visiblePageAreas: SparseArray<Rect>, 90 currentZoomLevel: Float, 91 stablePosition: Boolean 92 ) { 93 // Start preparing UI for visible pages 94 visiblePageAreas.keyIterator().forEach { pageNum -> 95 pages[pageNum]?.setVisible( 96 currentZoomLevel, 97 visiblePageAreas.get(pageNum), 98 stablePosition 99 ) 100 } 101 102 // We put pages that are near the viewport in a "nearly visible" state where some data is 103 // retained. We release all data from pages well outside the viewport 104 val nearPages = 105 Range( 106 maxOf(0, visiblePageAreas.keyAt(0) - PAGE_RETENTION_RADIUS), 107 minOf( 108 visiblePageAreas.keyAt(visiblePageAreas.size() - 1) + PAGE_RETENTION_RADIUS, 109 pdfDocument.pageCount - 1 110 ), 111 ) 112 for (pageNum in pages.keyIterator()) { 113 if (pageNum < nearPages.lower || pageNum > nearPages.upper) { 114 pages[pageNum]?.setInvisible() 115 } else if (!visiblePageAreas.contains(pageNum)) { 116 pages[pageNum]?.setNearlyVisible() 117 } 118 } 119 } 120 121 /** 122 * Updates the set of [Page]s owned by this manager when a new Page's dimensions are loaded. 123 * Dimensions are the minimum data required to instantiate a page. 124 */ 125 fun addPage( 126 pageNum: Int, 127 size: Point, 128 currentZoomLevel: Float, 129 stablePosition: Boolean, 130 viewArea: Rect? = null 131 ) { 132 if (pages.contains(pageNum)) return 133 val page = 134 Page( 135 pageNum, 136 size, 137 pdfDocument, 138 backgroundScope, 139 maxBitmapSizePx, 140 isTouchExplorationEnabled, 141 onPageUpdate = { _invalidationSignalFlow.tryEmit(Unit) }, 142 onPageTextReady = { pageNumber -> _pageTextReadyFlow.tryEmit(pageNumber) }, 143 errorFlow = errorFlow 144 ) 145 .apply { 146 // If the page is visible, let it know 147 if (viewArea != null) { 148 setVisible(currentZoomLevel, viewArea, stablePosition) 149 } 150 } 151 pages.put(pageNum, page) 152 } 153 154 /** Adds [newHighlights]s to this manager to be drawn along with the pages they belong to */ 155 fun setHighlights(newHighlights: List<Highlight>) { 156 highlights.clear() 157 for (highlight in newHighlights) { 158 highlights.getOrPut(highlight.area.pageNum) { mutableListOf() }.add(highlight) 159 } 160 _invalidationSignalFlow.tryEmit(Unit) 161 } 162 163 /** Draws the [Page] at [pageNum] to the canvas at [locationInView] */ 164 fun drawPage(pageNum: Int, canvas: Canvas, locationInView: Rect) { 165 val highlightsForPage = highlights.getOrDefault(pageNum, EMPTY_HIGHLIGHTS) 166 pages.get(pageNum)?.draw(canvas, locationInView, highlightsForPage) 167 } 168 169 /** 170 * Sets all [Page]s owned by this manager to invisible, i.e. to reduce memory when the host 171 * [PdfView] is not in an interactive state. 172 */ 173 fun cleanup() { 174 for (page in pages.valueIterator()) { 175 page.setInvisible() 176 } 177 } 178 179 fun getLinkAtTapPoint(pdfPoint: PdfPoint): PdfDocument.PdfPageLinks? { 180 return pages[pdfPoint.pageNum]?.links 181 } 182 } 183 184 /** Constant empty list to avoid allocations during drawing */ 185 private val EMPTY_HIGHLIGHTS = listOf<Highlight>() 186 187 private val PAGE_RETENTION_RADIUS = 2 188