Skip to content
Merged
2 changes: 1 addition & 1 deletion firefox-ios/Client/Configuration/version.xcconfig
Original file line number Diff line number Diff line change
@@ -1 +1 @@
APP_VERSION = 145.0
APP_VERSION = 145.1
44 changes: 29 additions & 15 deletions firefox-ios/Client/Coordinators/Browser/BrowserCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -680,9 +680,13 @@ class BrowserCoordinator: BaseCoordinator,

func presentSavePDFController() {
guard let selectedTab = browserViewController.tabManager.selectedTab else { return }

if selectedTab.mimeType == MIMEType.PDF {
showShareSheetForCurrentlySelectedTab()
} else {
// Online PDFs viewed in a tab can be shared via this URL to other Firefox synced devices with Send to Device.
let remoteURL = selectedTab.webView?.url

selectedTab.webView?.createPDF { [weak self] result in
guard let self else { return }
switch result {
Expand All @@ -694,7 +698,7 @@ class BrowserCoordinator: BaseCoordinator,
do {
try data.write(to: outputURL)
startShareSheetCoordinator(
shareType: .file(url: outputURL),
shareType: .file(url: outputURL, remoteURL: remoteURL),
shareMessage: nil,
sourceView: self.browserViewController.addressToolbarContainer,
sourceRect: nil,
Expand Down Expand Up @@ -864,7 +868,7 @@ class BrowserCoordinator: BaseCoordinator,
/// There are many ways to share many types of content from various areas of the app. Code paths that go through this
/// method include:
/// * Sharing content from a long press on Home screen tiles (e.g. long press Jump Back In context menu)
/// * From the old Menu > Share and the new Menu > Tools > Share
/// * From the old Menu > Share and the new Menu > More > Share
/// * From the new toolbar share button beside the address bar
/// * From long pressing a link in the WKWebView and sharing from the context menu (via ActionProviderBuilder > addShare)
/// * Via the sharesheet deeplink path in `RouteBuilder` (e.g. tapping home cards that initiate sharing content)
Expand Down Expand Up @@ -894,8 +898,13 @@ class BrowserCoordinator: BaseCoordinator,
// FXIOS-10824 It's strange if the user has to wait a long time to download files that are literally already
// being shown in the webview.
var overrideShareType = shareType
if case ShareType.tab = shareType {
overrideShareType = await tryDownloadingTabFileToShare(shareType: shareType)
if case let ShareType.tab(url, tab) = shareType {
// For tabs displaying content other than HTML MIME types, we can download the temporary document (i.e. a PDF
// file) and share that instead.
overrideShareType = await tryDownloadingTabFileToShare(
withTabURL: url,
forShareTab: tab
)
}

await MainActor.run { [weak self, overrideShareType] in
Expand Down Expand Up @@ -1335,21 +1344,26 @@ class BrowserCoordinator: BaseCoordinator,

// MARK: - Private helpers

nonisolated private func tryDownloadingTabFileToShare(shareType: ShareType) async -> ShareType {
// We can only try to download files for `.tab` type shares that have a TemporaryDocument
guard case let ShareType.tab(_, tab) = shareType,
let temporaryDocument = await tab.temporaryDocument,
!temporaryDocument.isDownloading else {
return shareType
}

guard let fileURL = await temporaryDocument.download() else {
/// Tabs displaying content other than a HTML MIME type can be downloaded and treated as files when shared. This method
/// attempts to download any such files. If there is no file to download, returns just a regular `ShareType.tab`.
/// - Parameters:
/// - tabURL: The URL for the tab pointing to a website.
/// - tab: The current tab displaying the tabURL.
/// - Returns: Returns a `ShareType.file` containing a `file://` URL that points to a downloaded file on the device. If
/// no file was downloaded, then just returns a regular `ShareType.tab` with the passed in `tabURL` and `tab`.
private func tryDownloadingTabFileToShare(
withTabURL tabURL: URL,
forShareTab tab: ShareTab
) async -> ShareType {
guard let temporaryDocument = tab.temporaryDocument,
!temporaryDocument.isDownloading,
let fileURL = await temporaryDocument.download() else {
// If no file was downloaded, simply share the tab as usual with a web URL
return shareType
return .tab(url: tabURL, tab: tab)
}

// If we successfully got a temp file URL, share it like a downloaded file
return .file(url: fileURL)
return .file(url: fileURL, remoteURL: tabURL)
}

/// Utility. Performs the supplied action if a coordinator of the indicated type
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,12 @@ class DownloadsCoordinator: BaseCoordinator,
tabManager: tabManager
)
add(child: coordinator)

// Since this file is already downloaded, we don't have a remote URL to use for the "Send to Device" activity
let shareType = ShareType.file(url: file.path, remoteURL: nil)

coordinator.start(
shareType: .file(url: file.path),
shareType: shareType,
shareMessage: nil,
sourceView: sourceView,
sourceRect: nil,
Expand Down
17 changes: 13 additions & 4 deletions firefox-ios/Client/Coordinators/ShareSheetCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -90,17 +90,26 @@ class ShareSheetCoordinator: BaseCoordinator,
// be necessary, but this JS alert code is fragile right now so let's not touch it until FXIOS-10334 is underway.
switch activityType {
case CustomActivityAction.sendToDevice.actionType:
// Cannot send file:// URLs to another synced device
guard !shareType.wrappedURL.isFileURL else {
var sendURL: URL = shareType.wrappedURL

if case .file(_, let remoteURL) = shareType,
let remoteURL = remoteURL {
// Some files might have a remote URL as a fallback for Send to Device (i.e. PDFs navigated to in a tab, but
// not from Downloads Panel).
sendURL = remoteURL
}

// Note: Cannot send file:// URLs to another synced device
guard !sendURL.isFileURL else {
dequeueNotShownJSAlert()
return
}

switch shareType {
case let .tab(_, tab):
showSendToDevice(url: shareType.wrappedURL, relatedTab: tab)
showSendToDevice(url: sendURL, relatedTab: tab)
default:
showSendToDevice(url: shareType.wrappedURL, relatedTab: nil)
showSendToDevice(url: sendURL, relatedTab: nil)
}
case .copyToPasteboard:
if case .file = shareType {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -668,8 +668,10 @@ final class MainMenuActionHelper: PhotonActionSheetProtocol,
@MainActor
private func share(fileURL: URL, buttonView: UIView) {
TelemetryWrapper.recordEvent(category: .action, method: .tap, object: .sharePageWith)

// Since this file is already downloaded, we don't have a remote URL to use for the "Send to Device" activity
navigationHandler?.showShareSheet(
shareType: .file(url: fileURL),
shareType: .file(url: fileURL, remoteURL: nil),
shareMessage: nil,
sourceView: buttonView,
sourceRect: nil,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -157,8 +157,11 @@ class DownloadsPanel: UIViewController,
}

private func shareDownloadedFile(_ downloadedFile: DownloadedFile, indexPath: IndexPath) {
// Since this file is already downloaded, we don't have a remote URL to use for the "Send to Device" activity
let shareType = ShareType.file(url: downloadedFile.path, remoteURL: nil)

let shareActivityViewController = ShareManager.createActivityViewController(
shareType: .file(url: downloadedFile.path),
shareType: shareType,
shareMessage: nil,
completionHandler: { _, _ in }
)
Expand Down
19 changes: 15 additions & 4 deletions firefox-ios/Client/Frontend/Share/ShareManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ class ShareManager: NSObject {
var activityItems: [Any] = []

switch shareType {
case .file(let fileURL):
case .file(let fileURL, _):
activityItems.append(URLActivityItemProvider(url: fileURL))

if let explicitShareMessage {
Expand Down Expand Up @@ -121,11 +121,22 @@ class ShareManager: NSObject {
}

@MainActor
private static func getApplicationActivities(forShareType shareType: ShareType) -> [UIActivity] {
static func getApplicationActivities(forShareType shareType: ShareType) -> [UIActivity] {
var appActivities = [UIActivity]()

// Only acts on non-file URLs to send links to synced devices. Will ignore file URLs it can't handle.
appActivities.append(SendToDeviceActivity(activityType: .sendToDevice, url: shareType.wrappedURL))
// Set up the "Send to Device" activity, which shares URLs between a Firefox account user's synced devices. We can
// only share URLs to real websites, not internal `file://` URLs.
switch shareType {
case .file(_, let remoteURL):
// Some downloaded files may have an associated remote URL (if the file was just downloaded in the tab).
// Files which are shared from the Downloads Panel will NOT have any associated remote URL (we don't store that
// history).
if let remoteURL {
appActivities.append(SendToDeviceActivity(activityType: .sendToDevice, url: remoteURL))
}
default:
appActivities.append(SendToDeviceActivity(activityType: .sendToDevice, url: shareType.wrappedURL))
}

return appActivities
}
Expand Down
2 changes: 1 addition & 1 deletion firefox-ios/Client/Frontend/Share/ShareTab.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ protocol ShareTab: Sendable {
@MainActor
var webView: TabWebView? { get }

// Tabs displaying content other than HTML mime type can optionally be downloaded and treated as files when shared
// Tabs displaying content other than a HTML MIME type can optionally be downloaded and treated as files when shared.
@MainActor
var temporaryDocument: TemporaryDocument? { get }
}
9 changes: 6 additions & 3 deletions firefox-ios/Client/Frontend/Share/ShareType.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@
import Foundation

/// Preconfigured sharing schemes which the share manager knows how to handle.
/// file: Include a file URL (`file://`). Best used for sharing downloaded files.
/// file: Include a file URL (`file://`). Best used for sharing downloaded files. If possible, the remote URL is saved
/// as well for `Send to Device` and other activities which share the content off-device without an attachment.
/// Note that remote URLs are only set for online files (e.g. PDFs viewed in the tab), not downloaded files (PDFs
/// opened in a tab from the DownloadsPanel).
/// site: Include a website URL (`http(s)://`). Best used for sharing library/bookmarks, etc. without an active tab.
/// Shares configured using .site will not append a title to Messages but will have a subtitle in Mail.
/// tab: Include a URL and a tab to share. If sharing a tab with an active webView, then additional sharing
Expand All @@ -14,14 +17,14 @@ import Foundation
/// scheme instead of `http(s)://`, so certain options, like Send to Device / Add to Home Screen, may not be
/// available.
enum ShareType {
case file(url: URL)
case file(url: URL, remoteURL: URL?)
case site(url: URL)
case tab(url: URL, tab: any ShareTab)

/// The share URL wrapped by the given type.
var wrappedURL: URL {
switch self {
case let .file(url):
case let .file(url, _):
return url
case let .site(url):
return url
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,37 @@ final class ShareManagerTests: XCTestCase {

// MARK: - Test sharing a file

func testGetActivityItems_forFileURL_withNoShareText() throws {
func testGetActivityItems_forFileURL_withNoRemoteURL_withNoShareText() throws {
let testShareActivityType = UIActivity.ActivityType.message

let activityItems = ShareManager.getActivityItems(
forShareType: .file(url: testFileURL),
forShareType: .file(url: testFileURL, remoteURL: nil),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we want to add tests for when remoteURL != nil.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good call, I'll add this. Also, I'll look at adding some SendToDeviceActivity tests since I noticed we don't have any specific to custom app activities. It's a little different than the other share types.

withExplicitShareMessage: nil
)

let urlActivityItemProvider = try XCTUnwrap(activityItems[safe: 0] as? URLActivityItemProvider)
let itemForURLActivity = urlActivityItemProvider.activityViewController(
createStubActivityViewController(),
itemForActivityType: testShareActivityType
)

let telemetryActivityItemProvider = try XCTUnwrap(activityItems[safe: 1] as? ShareTelemetryActivityItemProvider)
let itemForShareActivity = telemetryActivityItemProvider.activityViewController(
createStubActivityViewController(),
itemForActivityType: testShareActivityType
)

XCTAssertEqual(activityItems.count, 2)
XCTAssertEqual(itemForURLActivity as? URL, testFileURL)
XCTAssertTrue(itemForShareActivity is NSNull)
}

func testGetActivityItems_forFileURL_withRemoteURL_withNoShareText() throws {
let testShareActivityType = UIActivity.ActivityType.message

// Should be no difference with or without remoteURL
let activityItems = ShareManager.getActivityItems(
forShareType: .file(url: testFileURL, remoteURL: testWebURL),
withExplicitShareMessage: nil
)

Expand All @@ -62,7 +88,7 @@ final class ShareManagerTests: XCTestCase {
let testMessage = "Test message"
let testSubtitle = "Test subtitle"
let activityItems = ShareManager.getActivityItems(
forShareType: .file(url: testFileURL),
forShareType: .file(url: testFileURL, remoteURL: nil),
withExplicitShareMessage: ShareMessage(message: testMessage, subtitle: testSubtitle)
)

Expand Down Expand Up @@ -109,7 +135,7 @@ final class ShareManagerTests: XCTestCase {
let testShareActivityType = UIActivity.ActivityType.message

let activityItems = ShareManager.getActivityItems(
forShareType: .file(url: testWebURL),
forShareType: .file(url: testWebURL, remoteURL: nil),
withExplicitShareMessage: nil
)

Expand All @@ -136,7 +162,7 @@ final class ShareManagerTests: XCTestCase {
let testSubtitle = "Test subtitle"

let activityItems = ShareManager.getActivityItems(
forShareType: .file(url: testWebURL),
forShareType: .file(url: testWebURL, remoteURL: nil),
withExplicitShareMessage: ShareMessage(message: testMessage, subtitle: testSubtitle)
)

Expand Down Expand Up @@ -312,6 +338,59 @@ final class ShareManagerTests: XCTestCase {
XCTAssertTrue(itemForShareActivity is NSNull)
}

// MARK: - Custom SendToDeviceActivity

func testCustomApplicationActivities_forSiteShare() throws {
let testShareActivityType = UIActivity.ActivityType("org.mozilla.ios.Fennec.sendToDevice")
let testActivityTitle = "Send Link to Device"

let testShareType = ShareType.site(url: testWebURL)

let activityItems = ShareManager.getApplicationActivities(forShareType: testShareType)

let customActivityType = try XCTUnwrap(activityItems[safe: 0] as? SendToDeviceActivity)
XCTAssertEqual(activityItems.count, 1)
XCTAssertEqual(customActivityType.activityTitle, testActivityTitle)
XCTAssertEqual(customActivityType.activityType, testShareActivityType)
}

func testCustomApplicationActivities_forTabShare() throws {
let testShareActivityType = UIActivity.ActivityType("org.mozilla.ios.Fennec.sendToDevice")
let testActivityTitle = "Send Link to Device"

let testShareType = ShareType.tab(url: testWebURL, tab: testTab)

let activityItems = ShareManager.getApplicationActivities(forShareType: testShareType)

let customActivityType = try XCTUnwrap(activityItems[safe: 0] as? SendToDeviceActivity)
XCTAssertEqual(activityItems.count, 1)
XCTAssertEqual(customActivityType.activityTitle, testActivityTitle)
XCTAssertEqual(customActivityType.activityType, testShareActivityType)
}

func testCustomApplicationActivities_forFileShareWithRemoteURL_AddsSendToDevice() throws {
let testShareActivityType = UIActivity.ActivityType("org.mozilla.ios.Fennec.sendToDevice")
let testActivityTitle = "Send Link to Device"

let testShareType = ShareType.file(url: testFileURL, remoteURL: testWebURL)

let activityItems = ShareManager.getApplicationActivities(forShareType: testShareType)

let customActivityType = try XCTUnwrap(activityItems[safe: 0] as? SendToDeviceActivity)
XCTAssertEqual(activityItems.count, 1)
XCTAssertEqual(customActivityType.activityTitle, testActivityTitle)
XCTAssertEqual(customActivityType.activityType, testShareActivityType)
}

func testCustomApplicationActivities_forFileShareWithNoRemoteURL_DoesNotAddSendToDevice() throws {
// Simulate file share for downloaded files
let testShareType = ShareType.file(url: testFileURL, remoteURL: nil)

let activityItems = ShareManager.getApplicationActivities(forShareType: testShareType)

XCTAssertEqual(activityItems.count, 0)
}

// MARK: - Helpers

private func createStubActivityViewController() -> UIActivityViewController {
Expand Down