Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 59 additions & 0 deletions kDrive/UI/Controller/Files/Preview/PreviewBreadcrumb.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/*
Infomaniak kDrive - iOS App
Copyright (C) 2026 Infomaniak Network SA

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

import kDriveCore
import Sentry

/// Records breadcrumbs immediately, before preview operations that may crash.
enum PreviewBreadcrumb {
struct CellContext {
let sessionId: String
let item: Int
}

enum ReloadReason: String {
case fileUpdated
case networkChanged
case downloadCompleted
case downloadFailed
case officePreviewProgress
case officePreviewCompleted
}

static func record(_ message: String, metadata: [String: Any]) {
let breadcrumb = Breadcrumb(level: .info, category: "FilePreview")
breadcrumb.message = message
for (key, value) in metadata {
breadcrumb.setData(value: value, key: key)
}
SentrySDK.addBreadcrumb(breadcrumb)
}

static func record(_ message: String, error: DriveError?, metadata: [String: Any]) {
var metadata = metadata
switch error {
case nil:
metadata["outcome"] = "success"
case .taskCancelled?:
metadata["outcome"] = "cancelled"
default:
metadata["outcome"] = "failed"
}
record(message, metadata: metadata)
}
}
64 changes: 54 additions & 10 deletions kDrive/UI/Controller/Files/Preview/PreviewViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
import UIKit

@MainActor protocol PreviewContentCellDelegate: AnyObject {
func updateNavigationBar()
func pdfPreviewUpdated(fileId: Int, currentPage: Int, totalPages: Int)
func setFullscreen(_ fullscreen: Bool?)
func setPlaybackFullscreen(_ isPlaying: Bool)
func errorWhilePreviewing(fileId: Int, error: Error)
Expand All @@ -49,6 +49,7 @@
private var driveFileManager: DriveFileManager!
private var normalFolderHierarchy = true
private var initialLoading = true
private let breadcrumbSessionId = UUID().uuidString
var presentationOrigin = PresentationOrigin.fileList
private var shouldScrollToCurrentIndex = true
var currentIndex = IndexPath(row: 0, section: 0) {
Expand Down Expand Up @@ -296,7 +297,7 @@
self.currentFile = frozenFile

self.collectionView.endEditing(true)
self.reloadItemsAfterOneLoop(at: self.currentIndex)
self.reloadItemsAfterOneLoop(at: self.currentIndex, reason: .fileUpdated)
}
}
}
Expand All @@ -315,7 +316,7 @@
let file = self.previewFiles[index]
self.previewErrors[file.id] = nil

self.reloadItemsAfterOneLoop(at: IndexPath(item: index, section: 0))
self.reloadItemsAfterOneLoop(at: IndexPath(item: index, section: 0), reason: .networkChanged)
}
}
}
Expand Down Expand Up @@ -556,6 +557,13 @@
}
}

func pdfPreviewUpdated(fileId: Int, currentPage: Int, totalPages: Int) {
guard currentFile.id == fileId,
currentFile.convertedType == .pdf,
!currentFile.isLocalVersionOlderThanRemote else { return }
setNavbarForPdf(currentPage: currentPage, totalPages: totalPages)
}

private func setNavbarStandard() {
pdfPageLabel.isHidden = true
navigationItem.titleView = nil
Expand Down Expand Up @@ -843,15 +851,15 @@

PdfPreviewCache.shared.retrievePdf(forSafeFile: safeFile, driveFileManager: driveFileManager) { downloadTask in
previewError.addDownloadTask(downloadTask)
self.reloadItemsAfterOneLoop(at: IndexPath(item: previewIndex, section: 0))
self.reloadItemsAfterOneLoop(at: IndexPath(item: previewIndex, section: 0), reason: .officePreviewProgress)
} completion: { url, error in
previewError.removeDownloadTask()
if let url {
previewError.pdfUrl = url
} else {
previewError.underlyingError = error
}
self.reloadItemsAfterOneLoop(at: IndexPath(item: previewIndex, section: 0))
self.reloadItemsAfterOneLoop(at: IndexPath(item: previewIndex, section: 0), reason: .officePreviewCompleted)
}

previewErrors[safeFile.id] = previewError
Expand Down Expand Up @@ -962,6 +970,7 @@
Task { @MainActor [weak self] in
guard let self else { return }

PreviewBreadcrumb.record("download.completed", error: error, metadata: previewBreadcrumbMetadata(at: indexPath))
self.updateFileForCurrentIndex()
currentDownloadOperation = nil

Expand All @@ -970,14 +979,14 @@
if let error {
if error != .taskCancelled {
previewErrors[currentFile.id] = PreviewError(fileId: currentFile.id, underlyingError: error)
self.reloadItemsAfterOneLoop(at: indexPath)
self.reloadItemsAfterOneLoop(at: indexPath, reason: .downloadFailed)
}
} else {
(collectionView.cellForItem(at: indexPath) as? DownloadingPreviewCollectionViewCell)?
.previewDownloadTask?.cancel()
previewErrors[currentFile.id] = nil
collectionView.endEditing(true)
self.reloadItemsAfterOneLoop(at: indexPath)
self.reloadItemsAfterOneLoop(at: indexPath, reason: .downloadCompleted)
updateNavigationBar()
}
}
Expand Down Expand Up @@ -1069,6 +1078,9 @@

func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let file = previewFiles[indexPath.row]
let metadata = previewBreadcrumbMetadata(at: indexPath)
PreviewBreadcrumb.record("cell.provision.begin", metadata: metadata)
defer { PreviewBreadcrumb.record("cell.provision.end", metadata: metadata) }
// File is already downloaded and up to date OR we can remote play it (audio / video)
if previewErrors[file.id] == nil &&
(!file.isLocalVersionOlderThanRemote || ConvertedType.remotePlayableTypes.contains(file.convertedType)) {
Expand All @@ -1085,6 +1097,7 @@
case .pdf:
let cell = collectionView.dequeueReusableCell(type: PdfPreviewCollectionViewCell.self, for: indexPath)
cell.previewDelegate = self
cell.breadcrumbContext = PreviewBreadcrumb.CellContext(sessionId: breadcrumbSessionId, item: indexPath.item)
cell.configureWith(file: file)
return cell
case .video:
Expand Down Expand Up @@ -1123,7 +1136,8 @@
if let url = officePreviewError.pdfUrl {
let cell = collectionView.dequeueReusableCell(type: PdfPreviewCollectionViewCell.self, for: indexPath)
cell.previewDelegate = self
cell.configureWith(documentUrl: url)
cell.breadcrumbContext = PreviewBreadcrumb.CellContext(sessionId: breadcrumbSessionId, item: indexPath.item)
cell.configureWith(documentUrl: url, fileId: file.id)
return cell
} else {
let cell = collectionView.dequeueReusableCell(type: NoPreviewCollectionViewCell.self, for: indexPath)
Expand Down Expand Up @@ -1170,12 +1184,42 @@
}
}

private func reloadItemsAfterOneLoop(at indexPath: IndexPath) {
private func previewBreadcrumbMetadata(at indexPath: IndexPath) -> [String: Any] {
return [
"sessionId": breadcrumbSessionId,
"requestedItem": indexPath.item,
"currentItem": currentIndex.item,
"modelItemCount": previewFiles.count,
"isAttachedToWindow": viewIfLoaded?.window != nil
]
}

private func reloadBreadcrumbMetadata(
at indexPath: IndexPath,
reloadId: String,
reason: PreviewBreadcrumb.ReloadReason
) -> [String: Any] {
var metadata = previewBreadcrumbMetadata(at: indexPath)
metadata["reloadId"] = reloadId
metadata["reason"] = reason.rawValue
return metadata
}

private func reloadItemsAfterOneLoop(at indexPath: IndexPath, reason: PreviewBreadcrumb.ReloadReason) {
let reloadId = UUID().uuidString
let metadata = reloadBreadcrumbMetadata(at: indexPath, reloadId: reloadId, reason: reason)
PreviewBreadcrumb.record("reload.requested", metadata: metadata)
Task { @MainActor in
guard currentIndex.item < collectionView.numberOfItems(inSection: 0) else {
var metadata = reloadBreadcrumbMetadata(at: indexPath, reloadId: reloadId, reason: reason)
let itemCount = collectionView.numberOfItems(inSection: 0)
metadata["collectionItemCount"] = itemCount
guard (0..<itemCount).contains(indexPath.item) else {

Check warning on line 1216 in kDrive/UI/Controller/Files/Preview/PreviewViewController.swift

View workflow job for this annotation

GitHub Actions / format / format

Add or remove space around operators or delimiters. (spaceAroundOperators)
PreviewBreadcrumb.record("reload.skipped", metadata: metadata)
return
}
PreviewBreadcrumb.record("reload.begin", metadata: metadata)
collectionView.reloadItems(at: [indexPath])
PreviewBreadcrumb.record("reload.end", metadata: metadata)
}
}
}
Expand Down
84 changes: 78 additions & 6 deletions kDrive/UI/View/Files/Preview/PdfPreviewCollectionViewCell.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,28 @@ import UIKit

class PdfPreviewCollectionViewCell: PreviewCollectionViewCell, UIScrollViewDelegate {
@IBOutlet var pdfPreview: PDFView!
private var document: PDFDocument?
private var documentUrl: URL?
private var fileId: Int?
private var configurationId = UUID()
private var activationTask: Task<Void, Never>?
private var isDisplaying = false
private var hasActivatedDocument = false
var breadcrumbContext: PreviewBreadcrumb.CellContext?
private let breadcrumbCellId = UUID().uuidString

private func recordBreadcrumb(_ message: String) {
var metadata: [String: Any] = [
"cellId": breadcrumbCellId,
"configurationId": configurationId.uuidString,
"isDisplaying": isDisplaying,
"activationPending": activationTask != nil,
"hasActivatedDocument": hasActivatedDocument,
"hasDocument": pdfPreview.document != nil
]
metadata["sessionId"] = breadcrumbContext?.sessionId
metadata["item"] = breadcrumbContext?.item
PreviewBreadcrumb.record(message, metadata: metadata)
}

override func awakeFromNib() {
super.awakeFromNib()
Expand All @@ -34,29 +55,80 @@ class PdfPreviewCollectionViewCell: PreviewCollectionViewCell, UIScrollViewDeleg
}

@objc private func pageChanged() {
let configurationId = configurationId
Task { @MainActor [weak self] in
self?.previewDelegate?.updateNavigationBar()
guard let self, self.configurationId == configurationId, self.isDisplaying,
let fileId = self.fileId,
let document = self.pdfPreview.document,
let page = self.pdfPreview.currentPage else { return }
let pageIndex = document.index(for: page)
guard pageIndex != NSNotFound else { return }
self.previewDelegate?.pdfPreviewUpdated(
fileId: fileId,
currentPage: pageIndex + 1,
totalPages: document.pageCount
)
}
}

override func configureWith(file: File) {
configureWith(documentUrl: file.localUrl)
configureWith(documentUrl: file.localUrl, fileId: file.id)
}

func configureWith(documentUrl: URL, fileId: Int) {
self.documentUrl = documentUrl
self.fileId = fileId
configurationId = UUID()
recordBreadcrumb("pdf.configured")
}

override func willDisplayInCollectionView() {
isDisplaying = true
recordBreadcrumb("pdf.display.begin")
guard !hasActivatedDocument, activationTask == nil else { return }

let configurationId = configurationId
// PDFKit can force layout. Activate only after UIKit has finished provisioning and displaying the cell.
activationTask = Task { @MainActor [weak self] in
guard let self, !Task.isCancelled, self.configurationId == configurationId, self.isDisplaying,
let documentUrl = self.documentUrl else { return }
self.activationTask = nil
self.hasActivatedDocument = true
self.observePageChanges()
self.recordBreadcrumb("pdf.activation.begin")
self.pdfPreview.document = PDFDocument(url: documentUrl)
self.recordBreadcrumb("pdf.activation.end")
self.pageChanged()
}
}

override func didEndDisplayingInCollectionView() {
recordBreadcrumb("pdf.display.end")
isDisplaying = false
activationTask?.cancel()
activationTask = nil
}

func configureWith(documentUrl: URL) {
private func observePageChanges() {
NotificationCenter.default.addObserver(
self,
selector: #selector(pageChanged),
name: .PDFViewPageChanged,
object: pdfPreview
)
document = PDFDocument(url: documentUrl)
pdfPreview.document = document
}

override func prepareForReuse() {
recordBreadcrumb("pdf.reuse.begin")
super.prepareForReuse()
didEndDisplayingInCollectionView()
configurationId = UUID()
NotificationCenter.default.removeObserver(self, name: .PDFViewPageChanged, object: pdfPreview)
pdfPreview.document = nil
documentUrl = nil
fileId = nil
hasActivatedDocument = false
previewDelegate = nil
breadcrumbContext = nil
}
}
Loading
Loading