From ad5bf946ce4d6cdfb9193a590dad0f5fe4e6f1a0 Mon Sep 17 00:00:00 2001 From: Andreas Stefl Date: Sun, 16 Aug 2026 08:58:08 +0200 Subject: [PATCH] Claim the formats odrcore renders that iOS has no type for The document browser decides what the user may pick from `CFBundleDocumentTypes`, and the four broad types claimed there miss seven formats odrcore renders: the flat XML documents (.fodt .fodp .fods .fodg), an ODF master template (.otm), and .xlm and .xlt. The first six resolve to a dynamic type, which conforms to `public.data` and nothing else; .xlt has a system type that conforms to no claim of ours. All seven were greyed out. `UTImportedTypeDeclarations` gives the six a real type - imported, since none of them is ours - and they join `LSItemContentTypes` along with .xlt's. The plist cannot read odrcore's table, so `DeclaredDocumentTypesTests` holds one against the other, as OpenDocument.droid does with its manifest. Reverting the plist fails it with exactly those seven. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01UNugiHVRHVc1CGHgVGW7Dz --- CHANGELOG.md | 6 + OpenDocumentReader.xcodeproj/project.pbxproj | 4 + .../DeclaredDocumentTypesTests.swift | 87 +++++++++++ configs/full/Info.plist | 145 ++++++++++++++++++ configs/lite/Info.plist | 145 ++++++++++++++++++ fastlane/metadata/en-US/changelogs/1.41.txt | 1 + 6 files changed, 388 insertions(+) create mode 100644 OpenDocumentReaderTests/DeclaredDocumentTypesTests.swift diff --git a/CHANGELOG.md b/CHANGELOG.md index 7489b99..c948b3a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,12 @@ once the version tag exists. - The search button leaves the tool bar when the page cannot be searched, rather than greying out - the same as the edit button. +### Fixed + +- Flat XML documents (`.fodt`, `.fodp`, `.fods`, `.fodg`), `.otm`, `.xlt` and + `.xlm` can be picked in the document browser instead of being greyed out. + odrcore rendered them already; the app claimed no type that reached them. + ## [1.40] ### Changed diff --git a/OpenDocumentReader.xcodeproj/project.pbxproj b/OpenDocumentReader.xcodeproj/project.pbxproj index bb657a4..2554ff5 100644 --- a/OpenDocumentReader.xcodeproj/project.pbxproj +++ b/OpenDocumentReader.xcodeproj/project.pbxproj @@ -26,6 +26,7 @@ E24110312586349500800247 /* test.odt in Resources */ = {isa = PBXBuildFile; fileRef = E24110232586349500800247 /* test.odt */; }; E26C39392250DC6E009C484A /* WebKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E26C39382250DC6E009C484A /* WebKit.framework */; }; E2A17B0400000000000000A4 /* PageTabBarTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A17B0300000000000000A3 /* PageTabBarTests.swift */; }; + E2A17B0500000000000000A5 /* DeclaredDocumentTypesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A17B0600000000000000A6 /* DeclaredDocumentTypesTests.swift */; }; E2A17B1000000000000000B0 /* test.ods in Resources */ = {isa = PBXBuildFile; fileRef = E2A17B1200000000000000B2 /* test.ods */; }; E2A17B1100000000000000B1 /* test.odp in Resources */ = {isa = PBXBuildFile; fileRef = E2A17B1300000000000000B3 /* test.odp */; }; E2A17B1400000000000000B4 /* test.csv in Resources */ = {isa = PBXBuildFile; fileRef = E2A17B1500000000000000B5 /* test.csv */; }; @@ -64,6 +65,7 @@ E24110232586349500800247 /* test.odt */ = {isa = PBXFileReference; lastKnownFileType = file; path = test.odt; sourceTree = ""; }; E26C39382250DC6E009C484A /* WebKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WebKit.framework; path = System/Library/Frameworks/WebKit.framework; sourceTree = SDKROOT; }; E2A17B0300000000000000A3 /* PageTabBarTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageTabBarTests.swift; sourceTree = ""; }; + E2A17B0600000000000000A6 /* DeclaredDocumentTypesTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeclaredDocumentTypesTests.swift; sourceTree = ""; }; E2A17B1200000000000000B2 /* test.ods */ = {isa = PBXFileReference; lastKnownFileType = file; path = test.ods; sourceTree = ""; }; E2A17B1300000000000000B3 /* test.odp */ = {isa = PBXFileReference; lastKnownFileType = file; path = test.odp; sourceTree = ""; }; E2A17B1500000000000000B5 /* test.csv */ = {isa = PBXFileReference; lastKnownFileType = text; path = test.csv; sourceTree = ""; }; @@ -171,6 +173,7 @@ E2A17B1900000000000000B9 /* test-encrypted.pdf */, E22B252E2557F0E2001D0C52 /* OpenDocumentReaderTests.swift */, E2A17B0300000000000000A3 /* PageTabBarTests.swift */, + E2A17B0600000000000000A6 /* DeclaredDocumentTypesTests.swift */, E22B25302557F0E2001D0C52 /* Info.plist */, ); path = OpenDocumentReaderTests; @@ -388,6 +391,7 @@ files = ( E22B252F2557F0E2001D0C52 /* OpenDocumentReaderTests.swift in Sources */, E2A17B0400000000000000A4 /* PageTabBarTests.swift in Sources */, + E2A17B0500000000000000A5 /* DeclaredDocumentTypesTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/OpenDocumentReaderTests/DeclaredDocumentTypesTests.swift b/OpenDocumentReaderTests/DeclaredDocumentTypesTests.swift new file mode 100644 index 0000000..31df9ba --- /dev/null +++ b/OpenDocumentReaderTests/DeclaredDocumentTypesTests.swift @@ -0,0 +1,87 @@ +import OdrCoreObjC +import UniformTypeIdentifiers +import XCTest + +@testable import OpenDocumentReader + +/// What the app offers itself for, held against odrcore's format table the way +/// OpenDocument.droid holds its manifest against the same table. The plist +/// cannot read the table, so this is what keeps the two from drifting. +class DeclaredDocumentTypesTests: XCTestCase { + + /// Every `LSItemContentTypes` entry of the built app, not of a copy kept + /// here: the point is to test the plist that ships. + private lazy var claimed: [UTType] = { + let bundle = Bundle(for: DocumentViewController.self) + let entries = bundle.object(forInfoDictionaryKey: "CFBundleDocumentTypes") as? [[String: Any]] ?? [] + + return entries.flatMap { $0["LSItemContentTypes"] as? [String] ?? [] }.compactMap { UTType($0) } + }() + + /// What the app offers itself for: odrcore's document formats, plus the two + /// non-document ones worth opening a viewer for. Narrower than everything + /// odrcore translates - it does images, media, fonts and archives too, and + /// the app does not want to be the handler for an mp3. + private var offeredFileTypes: [FileType] { + Odr.allFileTypes.compactMap { FileType(rawValue: $0.intValue) } + .filter { type in + guard Odr.capabilities(fileType: type).translateHtml else { return false } + + return Odr.fileCategory(fileType: type) == .document + || [.textFile, .commaSeparatedValues].contains(type) + } + } + + func testEveryOfferedFormatIsClaimed() { + for type in offeredFileTypes { + for fileExtension in Odr.extensions(fileType: type) { + guard let resolved = UTType(filenameExtension: fileExtension) else { + XCTFail("iOS has no type for .\(fileExtension) (\(Odr.string(fileType: type)))") + + continue + } + + XCTAssertTrue( + claimed.contains(where: resolved.conforms(to:)), + ".\(fileExtension) resolves to \(resolved.identifier), which conforms to nothing the app claims") + } + } + } + + /// A dynamic type is what iOS invents for an extension it does not know. It + /// conforms to `public.data` and nothing else, so a file carrying one is + /// greyed out in the browser however broad the claims are. + func testNoOfferedFormatResolvesToADynamicType() { + for type in offeredFileTypes { + for fileExtension in Odr.extensions(fileType: type) { + let resolved = UTType(filenameExtension: fileExtension) + + XCTAssertEqual( + resolved?.isDynamic, false, + ".\(fileExtension) (\(Odr.string(fileType: type))) needs a UTImportedTypeDeclaration") + } + } + } + + /// The formats odrcore cannot render must not be claimed through one of the + /// declarations above, or the browser offers a file the app then refuses. + func testDeclarationsCoverOnlyWhatOdrcoreRenders() throws { + let declarations = + Bundle(for: DocumentViewController.self) + .object(forInfoDictionaryKey: "UTImportedTypeDeclarations") as? [[String: Any]] ?? [] + + XCTAssertFalse(declarations.isEmpty) + + for declaration in declarations { + let tags = declaration["UTTypeTagSpecification"] as? [String: Any] ?? [:] + + for fileExtension in tags["public.filename-extension"] as? [String] ?? [] { + let type = Odr.fileType(extension: fileExtension) + + XCTAssertTrue( + Odr.capabilities(fileType: type).translateHtml, + "odrcore does not render .\(fileExtension), so the app must not declare it") + } + } + } +} diff --git a/configs/full/Info.plist b/configs/full/Info.plist index 05a9697..459ce6d 100644 --- a/configs/full/Info.plist +++ b/configs/full/Info.plist @@ -35,6 +35,16 @@ public.image public.audiovisual-content public.composite-content + + com.microsoft.excel.xlt + com.microsoft.excel.xlm + org.oasis-open.opendocument.text-flat-xml + org.oasis-open.opendocument.presentation-flat-xml + org.oasis-open.opendocument.spreadsheet-flat-xml + org.oasis-open.opendocument.graphics-flat-xml + org.oasis-open.opendocument.text-master-template @@ -104,5 +114,140 @@ UISupportsDocumentBrowser + + UTImportedTypeDeclarations + + + UTTypeIdentifier + org.oasis-open.opendocument.text-flat-xml + UTTypeDescription + OpenDocument Text (Flat XML) + UTTypeConformsTo + + public.xml + public.composite-content + + UTTypeTagSpecification + + public.filename-extension + + fodt + + public.mime-type + + application/vnd.oasis.opendocument.text-flat-xml + + + + + UTTypeIdentifier + org.oasis-open.opendocument.presentation-flat-xml + UTTypeDescription + OpenDocument Presentation (Flat XML) + UTTypeConformsTo + + public.xml + public.composite-content + + UTTypeTagSpecification + + public.filename-extension + + fodp + + public.mime-type + + application/vnd.oasis.opendocument.presentation-flat-xml + + + + + UTTypeIdentifier + org.oasis-open.opendocument.spreadsheet-flat-xml + UTTypeDescription + OpenDocument Spreadsheet (Flat XML) + UTTypeConformsTo + + public.xml + public.composite-content + + UTTypeTagSpecification + + public.filename-extension + + fods + + public.mime-type + + application/vnd.oasis.opendocument.spreadsheet-flat-xml + + + + + UTTypeIdentifier + org.oasis-open.opendocument.graphics-flat-xml + UTTypeDescription + OpenDocument Drawing (Flat XML) + UTTypeConformsTo + + public.xml + public.composite-content + + UTTypeTagSpecification + + public.filename-extension + + fodg + + public.mime-type + + application/vnd.oasis.opendocument.graphics-flat-xml + + + + + UTTypeIdentifier + org.oasis-open.opendocument.text-master-template + UTTypeDescription + OpenDocument Master Document Template + UTTypeConformsTo + + public.zip-archive + public.composite-content + + UTTypeTagSpecification + + public.filename-extension + + otm + + public.mime-type + + application/vnd.oasis.opendocument.text-master-template + + + + + UTTypeIdentifier + com.microsoft.excel.xlm + UTTypeDescription + Microsoft Excel Macro Sheet + UTTypeConformsTo + + public.data + public.composite-content + + UTTypeTagSpecification + + public.filename-extension + + xlm + + + + diff --git a/configs/lite/Info.plist b/configs/lite/Info.plist index 60c6e06..0303725 100644 --- a/configs/lite/Info.plist +++ b/configs/lite/Info.plist @@ -35,6 +35,16 @@ public.image public.audiovisual-content public.composite-content + + com.microsoft.excel.xlt + com.microsoft.excel.xlm + org.oasis-open.opendocument.text-flat-xml + org.oasis-open.opendocument.presentation-flat-xml + org.oasis-open.opendocument.spreadsheet-flat-xml + org.oasis-open.opendocument.graphics-flat-xml + org.oasis-open.opendocument.text-master-template @@ -117,5 +127,140 @@ UISupportsDocumentBrowser + + UTImportedTypeDeclarations + + + UTTypeIdentifier + org.oasis-open.opendocument.text-flat-xml + UTTypeDescription + OpenDocument Text (Flat XML) + UTTypeConformsTo + + public.xml + public.composite-content + + UTTypeTagSpecification + + public.filename-extension + + fodt + + public.mime-type + + application/vnd.oasis.opendocument.text-flat-xml + + + + + UTTypeIdentifier + org.oasis-open.opendocument.presentation-flat-xml + UTTypeDescription + OpenDocument Presentation (Flat XML) + UTTypeConformsTo + + public.xml + public.composite-content + + UTTypeTagSpecification + + public.filename-extension + + fodp + + public.mime-type + + application/vnd.oasis.opendocument.presentation-flat-xml + + + + + UTTypeIdentifier + org.oasis-open.opendocument.spreadsheet-flat-xml + UTTypeDescription + OpenDocument Spreadsheet (Flat XML) + UTTypeConformsTo + + public.xml + public.composite-content + + UTTypeTagSpecification + + public.filename-extension + + fods + + public.mime-type + + application/vnd.oasis.opendocument.spreadsheet-flat-xml + + + + + UTTypeIdentifier + org.oasis-open.opendocument.graphics-flat-xml + UTTypeDescription + OpenDocument Drawing (Flat XML) + UTTypeConformsTo + + public.xml + public.composite-content + + UTTypeTagSpecification + + public.filename-extension + + fodg + + public.mime-type + + application/vnd.oasis.opendocument.graphics-flat-xml + + + + + UTTypeIdentifier + org.oasis-open.opendocument.text-master-template + UTTypeDescription + OpenDocument Master Document Template + UTTypeConformsTo + + public.zip-archive + public.composite-content + + UTTypeTagSpecification + + public.filename-extension + + otm + + public.mime-type + + application/vnd.oasis.opendocument.text-master-template + + + + + UTTypeIdentifier + com.microsoft.excel.xlm + UTTypeDescription + Microsoft Excel Macro Sheet + UTTypeConformsTo + + public.data + public.composite-content + + UTTypeTagSpecification + + public.filename-extension + + xlm + + + + diff --git a/fastlane/metadata/en-US/changelogs/1.41.txt b/fastlane/metadata/en-US/changelogs/1.41.txt index db64c34..59af5f0 100644 --- a/fastlane/metadata/en-US/changelogs/1.41.txt +++ b/fastlane/metadata/en-US/changelogs/1.41.txt @@ -1,2 +1,3 @@ - PDFs are drawn by the app's own engine, so they fit the screen and behave like every other document - The search and edit buttons are there only for documents that can be searched or edited +- LibreOffice's flat XML documents, master document templates and Excel templates can be opened from the document browser instead of being greyed out