Merge pull request #2999 from omnivore-app/fix/ios-pdf-downloads

iOS: fix for PDF downloads, always use the download signed URL, not the original URL
This commit is contained in:
Jackson Harper 2023-10-24 13:22:51 +08:00 committed by GitHub
commit 87fbeb54e9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 118 additions and 43 deletions

View file

@ -17,8 +17,8 @@ android {
applicationId "app.omnivore.omnivore"
minSdk 26
targetSdk 33
versionCode 118
versionName "0.0.118"
versionCode 122
versionName "0.0.122"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {

View file

@ -1,9 +1,15 @@
package app.omnivore.omnivore.networking
import android.util.Log
import app.omnivore.omnivore.graphql.generated.GetArticleQuery
import app.omnivore.omnivore.graphql.generated.type.ContentReader
import app.omnivore.omnivore.persistence.entities.SavedItem
import app.omnivore.omnivore.persistence.entities.SavedItemLabel
import app.omnivore.omnivore.persistence.entities.Highlight
import java.io.File
import java.net.URL
import java.nio.file.Files
import java.nio.file.StandardCopyOption
data class SavedItemQueryResponse(
val item: SavedItem?,
@ -60,7 +66,18 @@ suspend fun Networker.savedItem(slug: String): SavedItemQueryResponse {
)
}
// TODO: handle errors
var localPDFPath: String? = null
if (article.articleFields.contentReader == ContentReader.PDF) {
// download the PDF and save it locally
// article.articleFields.url
val localFile = File.createTempFile("pdf-" + article.articleFields.id, ".pdf", )
val url = URL(article.articleFields.url)
Log.d("pdf", "creating local file: $localFile")
url.openStream().use { Files.copy(it, localFile.toPath(), StandardCopyOption.REPLACE_EXISTING) }
localPDFPath = localFile.toPath().toString()
}
val savedItem = SavedItem(
savedItemId = article.articleFields.id,
@ -82,7 +99,8 @@ suspend fun Networker.savedItem(slug: String): SavedItemQueryResponse {
isArchived = article.articleFields.isArchived,
contentReader = article.articleFields.contentReader.rawValue,
content = article.articleFields.content,
wordsCount = article.articleFields.wordsCount
wordsCount = article.articleFields.wordsCount,
localPDFPath = localPDFPath
)
return SavedItemQueryResponse(item = savedItem, highlights, labels = savedItemLabels, state = article.articleFields.state?.rawValue ?: "")

View file

@ -13,7 +13,7 @@ import app.omnivore.omnivore.persistence.entities.*
SavedItemAndSavedItemLabelCrossRef::class,
SavedItemAndHighlightCrossRef::class
],
version = 11
version = 12
)
abstract class AppDatabase : RoomDatabase() {
abstract fun viewerDao(): ViewerDao

View file

@ -40,7 +40,8 @@ data class SavedItem(
@ColumnInfo(typeAffinity = ColumnInfo.BLOB) val pdfData: ByteArray? = null,
var serverSyncStatus: Int = 0,
val tempPDFURL: String? = null,
val wordsCount: Int? = null
val wordsCount: Int? = null,
val localPDFPath: String? = null
// hasMany highlights
// hasMany labels

View file

@ -7,7 +7,7 @@ import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import app.omnivore.omnivore.DatastoreRepository
import app.omnivore.omnivore.EventTracker
import app.omnivore.omnivore.dataService.DataService
import app.omnivore.omnivore.graphql.generated.type.CreateHighlightInput
import app.omnivore.omnivore.graphql.generated.type.MergeHighlightInput
import app.omnivore.omnivore.graphql.generated.type.UpdateHighlightInput
@ -21,11 +21,16 @@ import com.pspdfkit.document.download.DownloadJob
import com.pspdfkit.document.download.DownloadRequest
import com.pspdfkit.document.download.Progress
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.json.JSONObject
import java.io.File
import java.lang.Double.max
import java.lang.Double.min
import java.lang.Exception
import java.net.URLEncoder
import java.nio.file.FileSystem
import java.util.*
import javax.inject.Inject
@ -38,8 +43,8 @@ data class PDFReaderParams(
@HiltViewModel
class PDFReaderViewModel @Inject constructor(
private val datastoreRepo: DatastoreRepository,
private val networker: Networker,
private val eventTracker: EventTracker,
private val dataService: DataService,
private val networker: Networker
): ViewModel() {
var annotationUnderNoteEdit: Annotation? = null
val pdfReaderParamsLiveData = MutableLiveData<PDFReaderParams?>(null)
@ -47,21 +52,51 @@ class PDFReaderViewModel @Inject constructor(
fun loadItem(slug: String, context: Context) {
viewModelScope.launch {
loadItemFromDB(slug)
loadItemFromNetwork(slug, context)
}
}
private suspend fun loadItemFromDB(slug: String) {
withContext(Dispatchers.IO) {
val persistedItem = dataService.db.savedItemDao().getSavedItemWithLabelsAndHighlights(slug)
persistedItem?.let { persistedItem ->
persistedItem?.savedItem?.localPDF?.let { localPDF ->
val localFile = File(localPDF)
if (localFile.exists()) {
val articleContent = ArticleContent(
title = persistedItem.savedItem.title,
htmlContent = "",
highlights = persistedItem.highlights,
contentStatus = "SUCCEEDED",
objectID = "",
labelsJSONString = Gson().toJson(persistedItem.labels)
)
pdfReaderParamsLiveData.postValue(
PDFReaderParams(
persistedItem.savedItem,
articleContent,
Uri.fromFile(localFile)
)
)
}
}
}
}
}
private suspend fun loadItemFromNetwork(slug: String, context: Context) {
withContext(Dispatchers.IO) {
val articleQueryResult = networker.savedItem(slug)
val article = articleQueryResult.item ?: return@launch
val article = articleQueryResult.item ?: return@withContext
val request = DownloadRequest.Builder(context)
.uri(article.pageURLString)
.build()
val job = DownloadJob.startDownload(request)
job.setProgressListener(object : DownloadJob.ProgressListenerAdapter() {
override fun onProgress(progress: Progress) {
// progressBar.setProgress((100f * progress.bytesReceived / progress.totalBytes).toInt())
}
override fun onComplete(output: File) {
val articleContent = ArticleContent(
title = article.title,
@ -72,22 +107,18 @@ class PDFReaderViewModel @Inject constructor(
labelsJSONString = Gson().toJson(articleQueryResult.labels)
)
val pdfReaderParams = PDFReaderParams(article, articleContent, Uri.fromFile(output))
eventTracker.track("link_read",
com.posthog.android.Properties()
.putValue("linkID", pdfReaderParams.item.savedItemId)
.putValue("slug", pdfReaderParams.item.slug)
.putValue("originalArticleURL", pdfReaderParams.item.pageURLString)
.putValue("loaded_from", "network")
)
currentReadingProgress = article.readingProgress
pdfReaderParamsLiveData.postValue(pdfReaderParams)
pdfReaderParamsLiveData.postValue(
PDFReaderParams(
article,
articleContent,
Uri.fromFile(output)
)
)
}
override fun onError(exception: Throwable) {
// handleDownloadError(exception)
// handleDownloadError(exception)
}
})
}

View file

@ -91,7 +91,7 @@ data class WebReaderContent(
url: `${item.pageURLString}`,
title: `${articleContent.title.replace("`", "\\`")}`,
content: document.getElementById('_omnivore-htmlContent').innerHTML,
originalArticleUrl: "${item.pageURLString}",
originalArticleUrl: "${item.publisherURLString}",
contentReader: "WEB",
readingProgressPercent: ${item.readingProgress},
readingProgressAnchorIndex: ${item.readingProgressAnchor},

View file

@ -96,7 +96,18 @@ final class PDFViewerViewModel: ObservableObject {
}
}
return try await dataService.loadPDFData(slug: pdfItem.slug, pageURLString: pdfItem.originalArticleURL)
if let result = try? await dataService.loadPDFData(slug: pdfItem.slug, downloadURL: pdfItem.downloadURL) {
return result
}
// Downloading failed, try to get the article again, and then download
if let content = try? await dataService.loadArticleContentWithRetries(itemID: pdfItem.itemID, username: "me") {
// refetched the content, now try one more time then throw
if let result = try await dataService.loadPDFData(slug: pdfItem.slug, downloadURL: content.downloadURL) {
return result
}
}
return nil
} catch {
print("error downloading PDF", error)
return nil

View file

@ -46,7 +46,7 @@ import Views
private func trackReadEvent() {
guard let itemID = item?.unwrappedID ?? pdfItem?.itemID else { return }
guard let slug = item?.unwrappedSlug ?? pdfItem?.slug else { return }
guard let originalArticleURL = item?.unwrappedPageURLString ?? pdfItem?.originalArticleURL else { return }
guard let originalArticleURL = item?.unwrappedPageURLString ?? pdfItem?.downloadURL else { return }
EventTracker.track(
.linkRead(

View file

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="21513" systemVersion="21G531" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="22225" systemVersion="22G74" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
<entity name="Highlight" representedClassName="Highlight" syncable="YES" codeGenerationType="class">
<attribute name="annotation" optional="YES" attributeType="String"/>
<attribute name="color" optional="YES" attributeType="String"/>
@ -32,6 +32,7 @@
<attribute name="createdAt" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="createdId" optional="YES" attributeType="String"/>
<attribute name="descriptionText" optional="YES" attributeType="String"/>
<attribute name="downloadURL" optional="YES" attributeType="String"/>
<attribute name="htmlContent" optional="YES" attributeType="String"/>
<attribute name="id" attributeType="String"/>
<attribute name="imageURLString" optional="YES" attributeType="String"/>

View file

@ -16,19 +16,22 @@ public struct ArticleContent {
public let highlightsJSONString: String
public let contentStatus: ArticleContentStatus
public let objectID: NSManagedObjectID?
public let downloadURL: String
public init(
title: String,
htmlContent: String,
highlightsJSONString: String,
contentStatus: ArticleContentStatus,
objectID: NSManagedObjectID?
objectID: NSManagedObjectID?,
downloadURL: String
) {
self.title = title
self.htmlContent = htmlContent
self.highlightsJSONString = highlightsJSONString
self.contentStatus = contentStatus
self.objectID = objectID
self.downloadURL = downloadURL
}
}

View file

@ -56,12 +56,14 @@ public struct JSONArticle: Decodable {
public let isArchived: Bool
public let language: String?
public let wordsCount: Int?
public let downloadURL: String
}
public extension LinkedItem {
var unwrappedID: String { id ?? "" }
var unwrappedSlug: String { slug ?? "" }
var unwrappedTitle: String { title ?? "" }
var unwrappedDownloadURLString: String { downloadURL ?? "" }
var unwrappedPageURLString: String { pageURLString ?? "" }
var unwrappedSavedAt: Date { savedAt ?? Date() }
var unwrappedCreatedAt: Date { createdAt ?? Date() }

View file

@ -14,7 +14,7 @@ public struct PDFItem {
public let readingProgressAnchor: Int
public let isArchived: Bool
public let isRead: Bool
public let originalArticleURL: String
public let downloadURL: String
public let highlights: [Highlight]
public static func make(item: LinkedItem) -> PDFItem? {
@ -32,7 +32,7 @@ public struct PDFItem {
readingProgressAnchor: Int(item.readingProgressAnchor),
isArchived: item.isArchived,
isRead: item.isRead,
originalArticleURL: item.unwrappedPageURLString,
downloadURL: item.unwrappedPageURLString,
highlights: item.highlights.asArray(of: Highlight.self)
)
}

View file

@ -63,7 +63,8 @@ extension DataService {
htmlContent: fetchResult.htmlContent,
highlightsJSONString: fetchResult.highlights.asJSONString,
contentStatus: fetchResult.item.isPDF ? .succeeded : fetchResult.item.state,
objectID: objectID
objectID: objectID,
downloadURL: fetchResult.item.downloadURL
)
}
@ -91,7 +92,8 @@ extension DataService {
.filter { $0.serverSyncStatus != ServerSyncStatus.needsDeletion.rawValue }
.map { InternalHighlight.make(from: $0) }.asJSONString,
contentStatus: .succeeded,
objectID: linkedItem.objectID
objectID: linkedItem.objectID,
downloadURL: linkedItem.downloadURL ?? ""
)
}
}
@ -166,7 +168,7 @@ extension DataService {
}
if articleProps.item.isPDF, needsPDFDownload {
_ = try await loadPDFData(slug: articleProps.item.slug, pageURLString: articleProps.item.pageURLString)
_ = try await loadPDFData(slug: articleProps.item.slug, downloadURL: articleProps.item.downloadURL)
}
try await backgroundContext.perform { [weak self] in

View file

@ -4,8 +4,8 @@ import Models
import Utils
public extension DataService {
func loadPDFData(slug: String, pageURLString: String) async throws -> URL? {
guard let url = URL(string: pageURLString) else {
func loadPDFData(slug: String, downloadURL: String) async throws -> URL? {
guard let url = URL(string: downloadURL) else {
throw BasicError.message(messageText: "No PDF URL found")
}

View file

@ -45,6 +45,7 @@ extension DataService {
originalHtml: nil,
language: try $0.language(),
wordsCount: try $0.wordsCount(),
downloadURL: try $0.url(),
recommendations: try $0.recommendations(selection: recommendationSelection.list.nullable) ?? [],
labels: try $0.labels(selection: feedItemLabelSelection.list.nullable) ?? []
),

View file

@ -277,6 +277,7 @@ private let libraryArticleSelection = Selection.Article {
originalHtml: nil,
language: try $0.language(),
wordsCount: try $0.wordsCount(),
downloadURL: try $0.url(),
recommendations: try $0.recommendations(selection: recommendationSelection.list.nullable) ?? [],
labels: try $0.labels(selection: feedItemLabelSelection.list.nullable) ?? []
)
@ -316,6 +317,7 @@ private let searchItemSelection = Selection.SearchItem {
originalHtml: nil,
language: try $0.language(),
wordsCount: try $0.wordsCount(),
downloadURL: try $0.url(),
recommendations: try $0.recommendations(selection: recommendationSelection.list.nullable) ?? [],
labels: try $0.labels(selection: feedItemLabelSelection.list.nullable) ?? []
)

View file

@ -27,6 +27,7 @@ struct InternalLinkedItem {
let originalHtml: String?
let language: String?
let wordsCount: Int?
let downloadURL: String
let recommendations: [InternalRecommendation]
var labels: [InternalLinkedItemLabel]
@ -65,6 +66,7 @@ struct InternalLinkedItem {
linkedItem.originalHtml = originalHtml
linkedItem.language = language
linkedItem.wordsCount = Int64(wordsCount ?? 0)
linkedItem.downloadURL = downloadURL
// Remove existing labels in case a label had been deleted
if let existingLabels = linkedItem.labels {
@ -146,6 +148,7 @@ extension JSONArticle {
originalHtml: nil,
language: language,
wordsCount: wordsCount,
downloadURL: downloadURL,
recommendations: [],
labels: []
)

View file

@ -2,7 +2,7 @@
"name": "omnivore-app",
"version": "1.0.0",
"description": "",
"private": false,
"private": true,
"workspaces": [
"packages/*"
],
@ -40,4 +40,4 @@
"yarn": "1.22.19"
},
"dependencies": {}
}
}