Revision control

Copy as Markdown

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import Foundation
import UIKit
import SnapKit
import StoreKit
import Intents
import Glean
import Combine
import Onboarding
import AppShortcuts
import SwiftUI
class BrowserViewController: UIViewController {
private let mainContainerView = UIView(frame: .zero)
let darkView = UIView()
private lazy var trackingProtectionManager = TrackingProtectionManager(
isTrackingEnabled: {
Settings.getToggle(.trackingProtection)
})
private lazy var webViewController: LegacyWebViewController = {
var menuAction = WebMenuAction.live
menuAction.openLink = { url in
if let url = URIFixup.getURL(entry: url.absoluteString) {
self.submit(url: url, source: .action)
}
}
return LegacyWebViewController(trackingProtectionManager: trackingProtectionManager, webMenuAction: menuAction)
}()
private let legacyWebViewContainer = UIView()
var modalDelegate: ModalDelegate?
private var keyboardState: KeyboardState?
private var homeViewController: HomeViewController!
private let overlayView = OverlayView()
private let searchEngineManager = SearchEngineManager(prefs: UserDefaults.standard)
private let urlBarContainer = UIView()
private var urlBar: URLBar!
private lazy var browserToolbar = BrowserToolbar(viewModel: urlBarViewModel)
private lazy var urlBarViewModel = URLBarViewModel(
enableCustomDomainAutocomplete: { Settings.getToggle(.enableCustomDomainAutocomplete) },
getCustomDomainSetting: { Settings.getCustomDomainSetting() },
setCustomDomainSetting: { Settings.setCustomDomainSetting(domains: $0) },
enableDomainAutocomplete: { Settings.getToggle(.enableDomainAutocomplete) }
)
private let searchSuggestClient = SearchSuggestClient()
private var findInPageBar: FindInPageBar?
private var fillerView: UIView?
private let alertStackView = UIStackView() // All content that appears above the footer should be added to this view. (Find In Page/SnackBars)
private let shortcutsContainer = UIStackView()
private let shortcutsBackground = UIView()
private var toolbarBottomConstraint: Constraint!
private var urlBarTopConstraint: Constraint!
private var homeViewBottomConstraint: Constraint!
private var browserBottomConstraint: Constraint!
private var lastScrollOffset = CGPoint.zero
private var lastScrollTranslation = CGPoint.zero
private var scrollBarOffsetAlpha: CGFloat = 0
private var scrollBarState: URLBarScrollState = .expanded
private var background = UIImageView()
private var cancellables = Set<AnyCancellable>()
private var onboardingEventsHandler: OnboardingEventsHandling
private var themeManager: ThemeManager
private let shortcutsPresenter = ShortcutsPresenter()
private let onboardingTelemetry = OnboardingTelemetryHelper()
private enum URLBarScrollState {
case collapsed
case expanded
case transitioning
case animating
}
private var homeViewContainer = UIView()
fileprivate var showsToolsetInURLBar = false {
didSet {
if showsToolsetInURLBar {
browserBottomConstraint.deactivate()
} else {
browserBottomConstraint.activate()
}
}
}
private let searchSuggestionsDebouncer = Debouncer(timeInterval: 0.1)
private var shouldEnsureBrowsingMode = false
private var isIPadRegularDimensions = false {
didSet {
overlayView.isIpadView = isIPadRegularDimensions
}
}
private var initialUrl: URL?
private var orientationWillChange = false
private let tipManager: TipManager
internal let shortcutManager: ShortcutsManager
private let authenticationManager: AuthenticationManager
static let userDefaultsTrackersBlockedKey = "lifetimeTrackersBlocked"
init(
tipManager: TipManager = TipManager.shared,
shortcutManager: ShortcutsManager,
authenticationManager: AuthenticationManager,
onboardingEventsHandler: OnboardingEventsHandling,
themeManager: ThemeManager
) {
self.tipManager = tipManager
self.shortcutManager = shortcutManager
self.authenticationManager = authenticationManager
self.onboardingEventsHandler = onboardingEventsHandler
self.themeManager = themeManager
super.init(nibName: nil, bundle: nil)
KeyboardHelper.defaultHelper.addDelegate(delegate: self)
self.shortcutManager.onTap = { [unowned self] in self.shortcutTapped(url: $0) }
self.shortcutManager.onDismiss = { [unowned self] in self.dismissShortcut() }
self.shortcutManager.onRemove = { [unowned self] in self.removeFromShortcuts() }
self.shortcutManager.onShowRenameAlert = { [unowned self] in self.showRenameAlert(shortcut: $0) }
self.shortcutManager.shortcutsDidUpdate = { [unowned self] in self.shortcutsDidUpdate() }
self.shortcutManager.shortcutDidRemove = { [unowned self] in self.shortcutDidRemove(shortcut: $0) }
}
required init?(coder aDecoder: NSCoder) {
fatalError("BrowserViewController hasn't implemented init?(coder:)")
}
deinit {
NotificationCenter.default.removeObserver(self, name: UIDevice.orientationDidChangeNotification, object: nil)
}
fileprivate func addShortcutsBackgroundConstraints() {
shortcutsBackground.backgroundColor = isIPadRegularDimensions ? .systemBackground.withAlphaComponent(0.85) : .foundation
shortcutsBackground.layer.cornerRadius = isIPadRegularDimensions ? 10 : 0
if isIPadRegularDimensions {
shortcutsBackground.snp.makeConstraints { make in
make.top.equalTo(urlBarContainer.snp.bottom)
make.width.equalTo(UIConstants.layout.shortcutsBackgroundWidthIPad)
make.height.equalTo(UIConstants.layout.shortcutsBackgroundHeightIPad)
make.centerX.equalTo(urlBarContainer)
}
} else {
shortcutsBackground.snp.makeConstraints { make in
make.top.equalTo(urlBarContainer.snp.bottom)
make.left.right.equalToSuperview()
make.height.equalTo(UIConstants.layout.shortcutsBackgroundHeight)
}
}
}
fileprivate func addShortcutsContainerConstraints() {
let config: ShortcutView.LayoutConfiguration = isIPadRegularDimensions ? .iPad : .default
shortcutsContainer.snp.makeConstraints { make in
make.top.equalTo(urlBarContainer.snp.bottom).offset(isIPadRegularDimensions ? UIConstants.layout.shortcutsContainerOffsetIPad : UIConstants.layout.shortcutsContainerOffset)
make.width.equalTo(isIPadRegularDimensions ?
UIConstants.layout.shortcutsContainerWidthIPad :
UIConstants.layout.shortcutsContainerWidth).priority(.medium)
make.height.equalTo(config.height)
make.centerX.equalToSuperview()
make.leading.greaterThanOrEqualTo(mainContainerView).inset(8)
make.trailing.lessThanOrEqualTo(mainContainerView).inset(-8)
}
}
override func viewDidLoad() {
super.viewDidLoad()
NotificationCenter.default.addObserver(self, selector: #selector(orientationChanged), name: UIDevice.orientationDidChangeNotification, object: nil)
view.addSubview(mainContainerView)
isIPadRegularDimensions = traitCollection.horizontalSizeClass == .regular && traitCollection.verticalSizeClass == .regular
darkView.isHidden = true
darkView.backgroundColor = .ink90
darkView.alpha = 0.4
view.addSubview(darkView)
darkView.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
mainContainerView.snp.makeConstraints { make in
make.top.bottom.leading.width.equalToSuperview()
}
if WebEngineFlagManager.isWebEngineEnabled {
// FXIOS-8617 - #19118 ⁃ Integrate EngineSession in Focus iOS
} else {
// Legacy UI and related configuration
// TODO: [FXIOS-8616] Portions of this configuration will eventually be shared also by WebEngine.
configureLegacyUI()
}
}
private func configureLegacyUI() {
webViewController.delegate = self
setupBackgroundImage()
background.contentMode = .scaleAspectFill
mainContainerView.addSubview(background)
mainContainerView.addSubview(homeViewContainer)
legacyWebViewContainer.isHidden = true
mainContainerView.addSubview(legacyWebViewContainer)
urlBarContainer.alpha = 0
mainContainerView.addSubview(urlBarContainer)
browserToolbar.isHidden = true
browserToolbar.alpha = 0
browserToolbar.translatesAutoresizingMaskIntoConstraints = false
mainContainerView.addSubview(browserToolbar)
overlayView.isHidden = true
overlayView.alpha = 0
overlayView.delegate = self
overlayView.backgroundColor = isIPadRegularDimensions ? .clear : .scrim.withAlphaComponent(0.48)
overlayView.setSearchSuggestionsPromptViewDelegate(delegate: self)
mainContainerView.addSubview(overlayView)
shortcutsPresenter.shortcutsState = .createShortcutViews
background.snp.makeConstraints { make in
make.edges.equalTo(mainContainerView)
}
urlBarContainer.snp.makeConstraints { make in
make.top.leading.trailing.equalTo(mainContainerView)
make.height.equalTo(mainContainerView).multipliedBy(0.6).priority(500)
}
browserToolbar.snp.makeConstraints { make in
make.leading.trailing.equalTo(mainContainerView)
toolbarBottomConstraint = make.bottom.equalTo(mainContainerView).constraint
}
homeViewContainer.snp.makeConstraints { make in
make.top.equalTo(mainContainerView.safeAreaLayoutGuide.snp.top)
make.leading.trailing.equalTo(mainContainerView)
homeViewBottomConstraint = make.bottom.equalTo(mainContainerView).constraint
homeViewBottomConstraint.activate()
}
addWebViewConstraints()
legacyWebViewContainer.snp.makeConstraints { make in
browserBottomConstraint = make.bottom.equalTo(browserToolbar.snp.top).priority(1000).constraint
if !showsToolsetInURLBar {
browserBottomConstraint.activate()
}
}
view.addSubview(alertStackView)
alertStackView.axis = .vertical
alertStackView.alignment = .center
// true if device is an iPad or is an iPhone in landscape mode
showsToolsetInURLBar = (UIDevice.current.userInterfaceIdiom == .pad && (UIScreen.main.bounds.width == view.frame.size.width || view.frame.size.width > view.frame.size.height)) || (UIDevice.current.userInterfaceIdiom == .phone && view.frame.size.width > view.frame.size.height)
containWebView()
createHomeView()
createURLBar()
bindUrlBarViewModel()
updateViewConstraints()
overlayView.snp.makeConstraints { make in
make.top.equalTo(urlBarContainer.snp.bottom)
make.leading.trailing.equalTo(mainContainerView)
make.bottom.equalToSuperview().inset(UIConstants.layout.tipViewHeight)
}
// Listen for request desktop site notifications
NotificationCenter.default.addObserver(forName: Notification.Name(rawValue: UIConstants.strings.requestDesktopNotification), object: nil, queue: nil) { [weak self] _ in
// FXIOS-8638 - #19161 ⁃ Handle webViewController requestUserAgentChange
self?.webViewController.requestUserAgentChange()
}
// Listen for request mobile site notifications
NotificationCenter.default.addObserver(forName: Notification.Name(rawValue: UIConstants.strings.requestMobileNotification), object: nil, queue: nil) { [weak self] _ in
// FXIOS-8638 - #19161 ⁃ Handle webViewController requestUserAgentChange
self?.webViewController.requestUserAgentChange()
}
let dropInteraction = UIDropInteraction(delegate: self)
view.addInteraction(dropInteraction)
// Listen for find in page actvitiy notifications
NotificationCenter.default.addObserver(forName: Notification.Name(rawValue: UIConstants.strings.findInPageNotification), object: nil, queue: nil) { [weak self] _ in
self?.updateFindInPageVisibility(visible: true, text: "")
}
setupOnboardingEvents()
setupShortcutEvents()
trackingProtectionManager
.$trackingProtectionStatus
.sink { [unowned self] status in
updateLockIcon(trackingProtectionStatus: status)
}
.store(in: &cancellables)
guard shouldEnsureBrowsingMode else { return }
ensureBrowsingMode()
guard let url = initialUrl else { return }
submit(url: url, source: .action)
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
navigationController?.setNavigationBarHidden(true, animated: animated)
homeViewController.toolbar.layoutIfNeeded()
browserToolbar.layoutIfNeeded()
}
private func tooltipController(
anchoredBy sourceView: UIView,
sourceRect: CGRect, title: String = "",
body: String,
dismiss: @escaping () -> Void ) -> UIViewController {
let tooltipViewController = TooltipViewController()
tooltipViewController.set(title: title, body: body)
tooltipViewController.configure(anchoredBy: sourceView, sourceRect: sourceRect)
tooltipViewController.dismiss = dismiss
return tooltipViewController
}
func controller(for route: ToolTipRoute) -> UIViewController? {
switch route {
case .trackingProtectionShield(let version):
switch version {
case .v2:
return self.tooltipController(
anchoredBy: self.urlBar.shieldIconAnchor,
sourceRect: CGRect(x: self.urlBar.shieldIconAnchor.bounds.midX, y: self.urlBar.shieldIconAnchor.bounds.midY + 10, width: 0, height: 0),
body: UIConstants.strings.tooltipBodyTextForShieldIconV2,
dismiss: { [unowned self] in self.onboardingEventsHandler.route = nil
self.onboardingEventsHandler.send(.showTrash)
}
)
case .v1:
return self.tooltipController(
anchoredBy: self.urlBar.shieldIconAnchor,
sourceRect: CGRect(x: self.urlBar.shieldIconAnchor.bounds.midX, y: self.urlBar.shieldIconAnchor.bounds.midY + 10, width: 0, height: 0),
body: UIConstants.strings.tooltipBodyTextForShieldIcon,
dismiss: { [unowned self] in self.onboardingEventsHandler.route = nil }
)
}
case .trash(let version):
switch version {
case .v2:
let sourceButton = showsToolsetInURLBar ? urlBar.deleteButtonAnchor : browserToolbar.deleteButtonAnchor
let sourceRect = showsToolsetInURLBar ? CGRect(x: sourceButton.bounds.midX, y: sourceButton.bounds.maxY - 10, width: 0, height: 0) :
CGRect(x: sourceButton.bounds.midX, y: sourceButton.bounds.minY + 10, width: 0, height: 0)
return self.tooltipController(
anchoredBy: sourceButton,
sourceRect: sourceRect,
body: UIConstants.strings.tooltipBodyTextForTrashIconV2,
dismiss: { [unowned self] in self.onboardingEventsHandler.route = nil }
)
case .v1:
let sourceButton = showsToolsetInURLBar ? urlBar.deleteButtonAnchor : browserToolbar.deleteButtonAnchor
let sourceRect = showsToolsetInURLBar ? CGRect(x: sourceButton.bounds.midX, y: sourceButton.bounds.maxY - 10, width: 0, height: 0) :
CGRect(x: sourceButton.bounds.midX, y: sourceButton.bounds.minY + 10, width: 0, height: 0)
return self.tooltipController(
anchoredBy: sourceButton,
sourceRect: sourceRect,
body: UIConstants.strings.tooltipBodyTextForTrashIcon,
dismiss: { [unowned self] in self.onboardingEventsHandler.route = nil }
)
}
case .searchBar:
return self.tooltipController(
anchoredBy: self.urlBar.textFieldAnchor,
sourceRect: CGRect(
x: self.urlBar.textFieldAnchor.bounds.minX,
y: self.urlBar.textFieldAnchor.bounds.maxY,
width: 0,
height: 0
),
body: UIConstants.strings.tooltipBodyTextStartPrivateBrowsing,
dismiss: { [unowned self] in self.onboardingEventsHandler.route = nil }
)
case .onboarding(let onboardingType):
let dismissOnboarding = { [unowned self] in
UserDefaults.standard.set(true, forKey: OnboardingConstants.onboardingDidAppear)
urlBar.activateTextField()
onboardingEventsHandler.route = nil
onboardingEventsHandler.send(.enterHome)
}
return OnboardingFactory.make(onboardingType: onboardingType, dismissAction: dismissOnboarding, telemetry: onboardingTelemetry.handle(event:))
case .trackingProtection:
return nil
case .widget:
urlBar.dismiss()
let cardBanner = PortraitHostingController(
rootView: CardBannerView(
config: .init(
title: UIConstants.strings.widgetOnboardingCardTitle,
subtitle: UIConstants.strings.widgetOnboardingCardSubtitle,
actionButtonTitle: UIConstants.strings.widgetOnboardingCardActionButton,
widget: .init(
title: UIConstants.strings.searchInAppInstruction
)),
primaryAction: { [weak self] in
self?.onboardingEventsHandler.route = nil
self?.onboardingEventsHandler.send(.widgetDismissed)
self?.onboardingTelemetry.handle(event: .widgetPrimaryButtonTapped)
},
dismiss: { [weak self] in
self?.onboardingEventsHandler.route = nil
self?.urlBar.activateTextField()
self?.onboardingTelemetry.handle(event: .widgetCloseTapped)
}))
cardBanner.view.backgroundColor = .clear
cardBanner.modalPresentationStyle = .overFullScreen
return cardBanner
case .menu:
return self.tooltipController(
anchoredBy: self.urlBar.contextMenuButtonAnchor,
sourceRect: CGRect(x: self.urlBar.contextMenuButtonAnchor.bounds.maxX, y: self.urlBar.contextMenuButtonAnchor.bounds.midY + 12, width: 0, height: 0),
body: UIConstants.strings.tootipBodyTextForContextMenuIcon,
dismiss: { [unowned self] in self.onboardingEventsHandler.route = nil }
)
case .widgetTutorial:
let controller = PortraitHostingController(
rootView: ShowMeHowOnboardingView(
config: .init(
title: UIConstants.strings.titleShowMeHowOnboardingV2,
subtitleStep1: UIConstants.strings.subtitleStepOneShowMeHowOnboardingV2,
subtitleStep2: UIConstants.strings.subtitleStepTwoShowMeHowOnboardingV2,
subtitleStep3: UIConstants.strings.subtitleStepThreeShowMeHowOnboardingV2,
buttonText: UIConstants.strings.buttonTextShowMeHowOnboardingV2,
widgetText: UIConstants.strings.searchInAppInstruction),
dismissAction: { [unowned self] in self.onboardingEventsHandler.route = nil }))
controller.modalPresentationStyle = UIDevice.current.userInterfaceIdiom == .phone ? .overFullScreen : .formSheet
controller.isModalInPresentation = true
return controller
}
}
private func setupOnboardingEvents() {
var presentedController: UIViewController?
onboardingEventsHandler
.routePublisher
.sink { [unowned self] route in
switch route {
case .none:
presentedController?.dismiss(animated: true)
presentedController = nil
case .some(let route):
if let controller = controller(for: route) {
self.present(controller, animated: true)
presentedController = controller
switch route {
case .onboarding(let onboardingType):
if onboardingType == .v2 {
onboardingTelemetry.handle(event: .getStartedAppeared)
}
case .widget:
onboardingTelemetry.handle(event: .widgetCardAppeared)
default: break
}
}
}
}
.store(in: &cancellables)
}
private func setupShortcutEvents() {
shortcutsPresenter
.$shortcutsState
.sink { [unowned self] shortcutsState in
switch shortcutsState {
case .createShortcutViews:
self.mainContainerView.addSubview(shortcutsBackground)
shortcutsBackground.isHidden = true
addShortcutsBackgroundConstraints()
setupShortcuts()
self.mainContainerView.addSubview(shortcutsContainer)
addShortcutsContainerConstraints()
case .onHomeView:
shortcutsContainer.isHidden = false
shortcutsBackground.isHidden = true
case .editingURL(let text):
let shouldShowShortcuts = text.isEmpty && !shortcutManager.shortcutsViewModels.isEmpty
shortcutsContainer.isHidden = !shouldShowShortcuts
shortcutsBackground.isHidden = !urlBar.inBrowsingMode ? true : !shouldShowShortcuts
case .activeURLBar:
let shouldShowShortcuts = !shortcutManager.shortcutsViewModels.isEmpty
shortcutsContainer.isHidden = !shouldShowShortcuts
shortcutsBackground.isHidden = !shouldShowShortcuts || !urlBar.inBrowsingMode
case .dismissedURLBar:
// FXIOS-8636 - #19159 - Integrate onProgress and onNavigationStateChange in Focus iOS
shortcutsContainer.isHidden = urlBar.inBrowsingMode || webViewController.isLoading
shortcutsBackground.isHidden = true
case .none:
shortcutsContainer.isHidden = true
shortcutsBackground.isHidden = true
}
}
.store(in: &cancellables)
}
private func addShortcuts() {
if !shortcutManager.shortcutsViewModels.isEmpty {
for shortcutViewModel in shortcutManager.shortcutsViewModels {
let config: ShortcutView.LayoutConfiguration = isIPadRegularDimensions ? .iPad : .default
let shortcutView = ShortcutView(shortcutViewModel: shortcutViewModel, layoutConfiguration: config)
let interaction = UIContextMenuInteraction(delegate: shortcutView)
shortcutView.outerView.addInteraction(interaction)
shortcutView.setContentCompressionResistancePriority(.required, for: .horizontal)
shortcutView.setContentHuggingPriority(.required, for: .horizontal)
shortcutsContainer.addArrangedSubview(shortcutView)
shortcutView.snp.makeConstraints { make in
make.width.equalTo(config.width)
make.height.equalTo(config.height)
}
}
}
if shortcutManager.hasSpace {
shortcutsContainer.addArrangedSubview(UIView())
}
}
private func setupShortcuts() {
shortcutsContainer.axis = .horizontal
shortcutsContainer.alignment = .leading
shortcutsContainer.spacing = isIPadRegularDimensions ?
UIConstants.layout.shortcutsContainerSpacingIPad :
UIConstants.layout.shortcutsContainerSpacing
addShortcuts()
}
@objc
func orientationChanged() {
setupBackgroundImage()
}
func setupBackgroundImage() {
switch UIDevice.current.userInterfaceIdiom {
case .phone:
background.image = UIApplication.shared.orientation?.isLandscape == true ? #imageLiteral(resourceName: "background_iphone_landscape") : #imageLiteral(resourceName: "background_iphone_portrait")
case .pad:
background.image = UIApplication.shared.orientation?.isLandscape == true ? #imageLiteral(resourceName: "background_ipad_landscape") : #imageLiteral(resourceName: "background_ipad_portrait")
default:
background.image = #imageLiteral(resourceName: "background_iphone_portrait")
}
}
private func updateLockIcon(trackingProtectionStatus: TrackingProtectionStatus) {
// FXIOS-8643 - #19166 ⁃ Integrate content blocking in Focus iOS
let isSecureConnection = urlBar.inBrowsingMode ? self.webViewController.connectionIsSecure : true
let shieldIconStatus: ShieldIconStatus
if isSecureConnection {
switch trackingProtectionStatus {
case .on: shieldIconStatus = .on
case .off: shieldIconStatus = .off
}
} else {
shieldIconStatus = .connectionNotSecure
}
urlBarViewModel.connectionState = shieldIconStatus
}
// These functions are used to handle displaying and hiding the keyboard after the splash view is animated
public func activateUrlBarOnHomeView() {
// Do not activate if a modal is presented
if self.presentedViewController != nil {
return
}
// Do not activate if we are showing a web page, nor the overlayView hidden
if urlBar.inBrowsingMode {
return
}
urlBar.activateTextField()
}
public func deactivateUrlBarOnHomeView() {
urlBar.dismissTextField()
}
public func deactivateUrlBar() {
if urlBar.inBrowsingMode {
urlBar.dismiss()
}
}
public func dismissSettings() {
if self.presentedViewController?.children.first is SettingsViewController {
self.presentedViewController?.children.first?.dismiss(animated: true, completion: nil)
}
}
public func dismissActionSheet() {
if self.presentedViewController is PhotonActionSheet {
self.presentedViewController?.dismiss(animated: true, completion: nil)
photonActionSheetDidDismiss()
}
}
// FXIOS-8632 - #19158 - Integrate EngineSession full screen in Focus iOS
public func exitFullScreenVideo() {
let js = "closeFullScreen()"
webViewController.evaluate(js, completion: nil)
}
// FXIOS-8617 - #19118 ⁃ Integrate EngineSession in Focus iOS
private func containWebView() {
addChild(webViewController)
legacyWebViewContainer.addSubview(webViewController.view)
webViewController.didMove(toParent: self)
webViewController.view.snp.makeConstraints { make in
make.edges.equalTo(legacyWebViewContainer.snp.edges)
}
}
private func createHomeView() {
homeViewController = HomeViewController(tipManager: tipManager)
homeViewController.delegate = self
homeViewController.onboardingEventsHandler = onboardingEventsHandler
install(homeViewController, on: homeViewContainer)
}
private func createURLBar() {
urlBar = URLBar(viewModel: urlBarViewModel)
urlBar.delegate = self
urlBar.isIPadRegularDimensions = isIPadRegularDimensions
urlBar.shouldShowToolset = showsToolsetInURLBar
mainContainerView.insertSubview(urlBar, aboveSubview: urlBarContainer)
addURLBarConstraints()
}
private func bindUrlBarViewModel() {
urlBarViewModel.viewActionPublisher
.sink { [weak self] action in
guard let self = self else { return }
switch action {
case .contextMenuTap(let anchor):
self.updateFindInPageVisibility(visible: false)
self.presentContextMenu(from: anchor)
case .backButtonTap:
// FXIOS-8626 - #19148 - Integrate basics APIs of WebEngine in Focus iOS
self.webViewController.goBack()
case .forwardButtonTap:
// FXIOS-8626 - #19148 - Integrate basics APIs of WebEngine in Focus iOS
self.webViewController.goForward()
case .deleteButtonTap:
self.updateFindInPageVisibility(visible: false)
self.resetBrowser()
self.urlBarViewModel.resetToDefaults()
case .shieldIconButtonTap:
self.urlBarDidTapShield(self.urlBar)
case .stopButtonTap:
// FXIOS-8626 - #19148 - Integrate basics APIs of WebEngine in Focus iOS
self.webViewController.stop()
case .reloadButtonTap:
// FXIOS-8626 - #19148 - Integrate basics APIs of WebEngine in Focus iOS
self.webViewController.reload()
case .dragInteractionStarted:
GleanMetrics.UrlInteraction.dragStarted.record()
case .pasteAndGo:
GleanMetrics.UrlInteraction.pasteAndGo.record()
}
}.store(in: &cancellables)
}
private func addURLBarConstraints() {
urlBar.snp.makeConstraints { make in
urlBarTopConstraint = make.top.equalTo(mainContainerView.safeAreaLayoutGuide.snp.top).constraint
if isIPadRegularDimensions {
make.leading.trailing.equalToSuperview()
make.centerX.equalToSuperview()
make.bottom.equalTo(urlBarContainer)
} else {
make.bottom.equalTo(urlBarContainer)
make.leading.trailing.equalToSuperview()
}
}
}
private func addWebViewConstraints() {
let topConstraint = legacyWebViewContainer.topAnchor.constraint(equalTo: urlBarContainer.bottomAnchor)
topConstraint.priority = .defaultLow
let bottomConstraint = legacyWebViewContainer.bottomAnchor.constraint(equalTo: mainContainerView.bottomAnchor)
bottomConstraint.priority = .defaultLow
let leadingConstraint = legacyWebViewContainer.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor)
let trailingConstraint = legacyWebViewContainer.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor)
NSLayoutConstraint.activate([topConstraint, bottomConstraint, leadingConstraint, trailingConstraint])
}
override func updateViewConstraints() {
super.updateViewConstraints()
alertStackView.snp.remakeConstraints { make in
make.centerX.equalTo(self.view)
make.width.equalTo(self.view.snp.width)
if let keyboardHeight = keyboardState?.intersectionHeightForView(view: self.view), keyboardHeight > 0 {
make.bottom.equalTo(self.view).offset(-keyboardHeight)
} else if !browserToolbar.isHidden {
// is an iPhone
make.bottom.equalTo(self.browserToolbar.snp.top).priority(.low)
make.bottom.lessThanOrEqualTo(self.view.safeAreaLayoutGuide.snp.bottom).priority(.required)
} else {
// is an iPad
make.bottom.equalTo(self.view.safeAreaLayoutGuide.snp.bottom)
}
}
}
func updateFindInPageVisibility(visible: Bool, text: String = "") {
if visible {
if findInPageBar == nil {
urlBar.dismiss {
// Start our animation after urlBar dismisses
let findInPageBar = FindInPageBar()
self.findInPageBar = findInPageBar
let fillerView = UIView()
self.fillerView = fillerView
fillerView.backgroundColor = .grey70
findInPageBar.text = text
findInPageBar.delegate = self
self.alertStackView.addArrangedSubview(findInPageBar)
self.mainContainerView.insertSubview(fillerView, belowSubview: self.browserToolbar)
findInPageBar.snp.makeConstraints { make in
make.height.equalTo(UIConstants.layout.toolbarHeight)
make.leading.trailing.equalTo(self.alertStackView)
make.bottom.equalTo(self.alertStackView.snp.bottom)
}
fillerView.snp.makeConstraints { make in
make.top.equalTo(self.alertStackView.snp.bottom)
make.bottom.equalTo(self.view)
make.leading.trailing.equalTo(self.alertStackView)
}
self.view.layoutIfNeeded()
}
}
self.findInPageBar?.becomeFirstResponder()
} else if let findInPageBar = self.findInPageBar {
findInPageBar.endEditing(true)
// FXIOS-8631 - #19154 ⁃ Integrate EngineSession focus in Focus iOS
webViewController.focus()
// FXIOS-8628 - #19150 ⁃ Integrate EngineSession find in page in Focus iOS
// This is done through `func findInPageDone()`
webViewController.evaluate("__firefox__.findDone()", completion: nil)
findInPageBar.removeFromSuperview()
fillerView?.removeFromSuperview()
self.findInPageBar = nil
self.fillerView = nil
updateViewConstraints()
}
}
func resetBrowser(hidePreviousSession: Bool = false) {
// Used when biometrics fail and the previous session should be obscured
if hidePreviousSession {
clearBrowser()
urlBar.activateTextField()
return
}
// Screenshot the browser, showing the screenshot on top.
let screenshotView = view.snapshotView(afterScreenUpdates: true) ?? UIView()
mainContainerView.addSubview(screenshotView)
screenshotView.snp.makeConstraints { make in
make.edges.equalTo(mainContainerView)
}
clearBrowser()
UIView.animate(
withDuration: UIConstants.layout.deleteAnimationDuration,
animations: {
screenshotView.snp.remakeConstraints { make in
make.centerX.equalTo(self.mainContainerView)
make.top.equalTo(self.mainContainerView.snp.bottom)
make.size.equalTo(self.mainContainerView).multipliedBy(0.9)
}
screenshotView.alpha = 0
self.mainContainerView.layoutIfNeeded()
},
completion: { _ in
self.urlBar.activateTextField()
Toast(text: UIConstants.strings.eraseMessage).show()
screenshotView.removeFromSuperview()
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
self.onboardingEventsHandler.send(.clearTapped)
}
}
)
userActivity = SiriShortcuts().getActivity(for: .eraseAndOpen)
let interaction = INInteraction(intent: eraseIntent, response: nil)
interaction.donate { (error) in
if let error = error { print(error.localizedDescription) }
}
}
private func clearBrowser() {
// Helper function for resetBrowser that handles all the logic of actually clearing user data and the browsing session
overlayView.currentURL = ""
// FXIOS-8639 - #19162 - Handle reset webview in Focus iOS
webViewController.reset()
legacyWebViewContainer.isHidden = true
browserToolbar.isHidden = true
urlBarViewModel.canGoBack = false
urlBarViewModel.canGoForward = false
urlBarViewModel.canDelete = false
urlBar.dismiss()
urlBar.removeFromSuperview()
urlBarContainer.alpha = 0
homeViewController.refreshTipsDisplay()
homeViewController.view.isHidden = false
createURLBar()
updateLockIcon(trackingProtectionStatus: trackingProtectionManager.trackingProtectionStatus)
shortcutsPresenter.shortcutsState = .onHomeView
// Clear the cache and cookies, starting a new session.
WebCacheUtils.reset()
requestReviewIfNecessary()
mainContainerView.layoutIfNeeded()
}
func requestReviewIfNecessary() {
if AppInfo.isTesting() { return }
let currentLaunchCount = UserDefaults.standard.integer(forKey: UIConstants.strings.userDefaultsLaunchCountKey)
let threshold = UserDefaults.standard.integer(forKey: UIConstants.strings.userDefaultsLaunchThresholdKey)
if threshold == 0 {
UserDefaults.standard.set(14, forKey: UIConstants.strings.userDefaultsLaunchThresholdKey)
return
}
// Make sure the request isn't within 90 days of last request
let minimumDaysBetweenReviewRequest = 90
let daysSinceLastRequest: Int
if let previousRequest = UserDefaults.standard.object(forKey: UIConstants.strings.userDefaultsLastReviewRequestDate) as? Date {
daysSinceLastRequest = Calendar.current.dateComponents([.day], from: previousRequest, to: Date()).day ?? 0
} else {
// No previous request date found, meaning we've never asked for a review
daysSinceLastRequest = minimumDaysBetweenReviewRequest
}
if currentLaunchCount <= threshold || daysSinceLastRequest < minimumDaysBetweenReviewRequest {
return
}
UserDefaults.standard.set(Date(), forKey: UIConstants.strings.userDefaultsLastReviewRequestDate)
// Increment the threshold by 50 so the user is not constantly pestered with review requests
switch threshold {
case 14:
UserDefaults.standard.set(64, forKey: UIConstants.strings.userDefaultsLaunchThresholdKey)
case 64:
UserDefaults.standard.set(114, forKey: UIConstants.strings.userDefaultsLaunchThresholdKey)
default:
break
}
if let scene = UIApplication.shared.connectedScenes.first(where: { $0.activationState == .foregroundActive }) as? UIWindowScene {
SKStoreReviewController.requestReview(in: scene)
}
}
private func showSiriFavoriteSettings() {
guard let modalDelegate = modalDelegate else { return }
urlBar.shouldPresent = false
let siriFavoriteViewController = SiriFavoriteViewController()
let siriFavoriteNavController = UINavigationController(rootViewController: siriFavoriteViewController)
siriFavoriteNavController.modalPresentationStyle = .formSheet
modalDelegate.presentModal(viewController: siriFavoriteNavController, animated: true)
}
func ensureBrowsingMode() {
guard urlBar != nil else { shouldEnsureBrowsingMode = true; return }
guard !urlBar.inBrowsingMode else { return }
urlBarContainer.alpha = 1
urlBar.ensureBrowsingMode()
shouldEnsureBrowsingMode = false
}
func submit(text: String, source: Source) {
var url = URIFixup.getURL(entry: text)
if url == nil {
recordSearchEvent(source)
url = searchEngineManager.activeEngine.urlForQuery(text)
}
if let url = url {
submit(url: url, source: source)
}
}
fileprivate func recordSearchEvent(_ source: Source) {
let identifier = searchEngineManager.activeEngine.getNameOrCustom().lowercased()
let source = source.rawValue
GleanMetrics.BrowserSearch.searchCount["\(identifier).\(source)"].add()
}
func submit(url: URL, source: Source) {
// If this is the first navigation, show the browser and the toolbar.
guard isViewLoaded else { initialUrl = url; return }
shortcutsPresenter.shortcutsState = .none
SearchInContentTelemetry.shouldSetUrlTypeSearch = true
if isIPadRegularDimensions {
urlBar.snp.makeConstraints { make in
make.width.equalTo(view)
make.leading.equalTo(view)
}
}
if legacyWebViewContainer.isHidden {
legacyWebViewContainer.isHidden = false
homeViewController.view.isHidden = true
urlBar.inBrowsingMode = true
if !showsToolsetInURLBar {
browserToolbar.animateHidden(false, duration: UIConstants.layout.toolbarFadeAnimationDuration)
}
}
// FXIOS-8626 - #19148 - Integrate basics APIs of WebEngine in Focus iOS
webViewController.load(URLRequest(url: url))
if urlBar.url != url {
urlBar.url = url
}
onboardingEventsHandler.route = nil
onboardingEventsHandler.send(.startBrowsing)
urlBarViewModel.canDelete = true
guard let savedUrl = UserDefaults.standard.value(forKey: "favoriteUrl") as? String else { return }
if let currentDomain = url.baseDomain,
let savedDomain = URL(string: savedUrl, invalidCharacters: false)?.baseDomain,
currentDomain == savedDomain {
userActivity = SiriShortcuts().getActivity(for: .openURL)
}
}
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransition(to: size, with: coordinator)
// Fixes the issue of a user fresh-opening Focus via Split View
guard isViewLoaded else { return }
orientationWillChange = true
// UIDevice.current.orientation isn't reliable. See https://bugzilla.mozilla.org/show_bug.cgi?id=1315370#c5
// As a workaround, consider the phone to be in landscape if the new width is greater than the height.
showsToolsetInURLBar = (UIDevice.current.userInterfaceIdiom == .pad && (UIScreen.main.bounds.width == size.width || size.width > size.height)) || (UIDevice.current.userInterfaceIdiom == .phone && size.width > size.height)
// isIPadRegularDimensions check if the device is a Ipad and the app is not in split mode
isIPadRegularDimensions = ((UIDevice.current.userInterfaceIdiom == .pad && (UIScreen.main.bounds.width == size.width || size.width > size.height))) || (UIDevice.current.userInterfaceIdiom == .pad && UIApplication.shared.orientation?.isPortrait == true && UIScreen.main.bounds.width == size.width)
urlBar.isIPadRegularDimensions = isIPadRegularDimensions
if urlBar.state == .default {
urlBar.snp.removeConstraints()
addURLBarConstraints()
} else {
urlBarContainer.snp.makeConstraints { make in
make.width.equalTo(view)
make.leading.equalTo(view)
}
}
urlBar.updateConstraints()
browserToolbar.updateConstraints()
shortcutsContainer.spacing = size.width < UIConstants.layout.smallestSplitViewMaxWidthLimit ?
UIConstants.layout.shortcutsContainerSpacingSmallestSplitView :
(isIPadRegularDimensions ? UIConstants.layout.shortcutsContainerSpacingIPad : UIConstants.layout.shortcutsContainerSpacing)
coordinator.animate(alongsideTransition: { [weak self] _ in
guard let self = self else { return }
self.urlBar.shouldShowToolset = self.showsToolsetInURLBar
if self.homeViewController == nil && self.scrollBarState != .expanded {
self.hideToolbars()
}
self.browserToolbar.animateHidden(!self.urlBar.inBrowsingMode || self.showsToolsetInURLBar, duration: coordinator.transitionDuration, completion: {
self.updateViewConstraints()
// FXIOS-8627 - #19151 - Integrate EngineSession zoom in Focus iOS
self.webViewController.resetZoom()
})
})
shortcutsContainer.snp.removeConstraints()
addShortcutsContainerConstraints()
shortcutsBackground.snp.removeConstraints()
addShortcutsBackgroundConstraints()
DispatchQueue.main.async {
self.urlBar.updateCollapsedState()
if case let .trash(version) = self.onboardingEventsHandler.route {
self.onboardingEventsHandler.route = nil
self.onboardingEventsHandler.route = .trash(version)
}
}
}
@objc
private func selectLocationBar() {
showToolbars()
urlBar.activateTextField()
shortcutsPresenter.shortcutsState = .activeURLBar
}
@objc
private func reload() {
// FXIOS-8626 - #19148 - Integrate basics APIs of WebEngine in Focus iOS
webViewController.reload()
}
@objc
private func goBack() {
// FXIOS-8626 - #19148 - Integrate basics APIs of WebEngine in Focus iOS
webViewController.goBack()
}
@objc
private func goForward() {
// FXIOS-8626 - #19148 - Integrate basics APIs of WebEngine in Focus iOS
webViewController.goForward()
}
@objc
private func showFindInPage() {
self.updateFindInPageVisibility(visible: true)
}
private func toggleURLBarBackground(isBright: Bool) {
urlBarContainer.backgroundColor = urlBar.inBrowsingMode ? .foundation : .clear
}
override var keyCommands: [UIKeyCommand]? {
return [
UIKeyCommand(title: UIConstants.strings.selectLocationBarTitle,
image: nil,
action: #selector(BrowserViewController.selectLocationBar),
input: "l",
modifierFlags: .command,
propertyList: nil),
UIKeyCommand(title: UIConstants.strings.browserReload,
image: nil,
action: #selector(BrowserViewController.reload),
input: "r",
modifierFlags: .command,
propertyList: nil),
UIKeyCommand(title: UIConstants.strings.browserBack,
image: nil,
action: #selector(BrowserViewController.goBack),
input: "[",
modifierFlags: .command,
propertyList: nil),
UIKeyCommand(title: UIConstants.strings.browserForward,
image: nil,
action: #selector(BrowserViewController.goForward),
input: "]",
modifierFlags: .command,
propertyList: nil),
UIKeyCommand(title: UIConstants.strings.shareMenuFindInPage,
image: nil,
action: #selector(BrowserViewController.showFindInPage),
input: "f",
modifierFlags: .command,
propertyList: nil)
]
}
func refreshTipsDisplay() {
homeViewController.refreshTipsDisplay()
}
private func getNumberOfLifetimeTrackersBlocked(userDefaults: UserDefaults = UserDefaults.standard) -> Int {
return userDefaults.integer(forKey: BrowserViewController.userDefaultsTrackersBlockedKey)
}
private func setNumberOfLifetimeTrackersBlocked(numberOfTrackers: Int) {
UserDefaults.standard.set(numberOfTrackers, forKey: BrowserViewController.userDefaultsTrackersBlockedKey)
}
// FXIOS-8640 - #19163 - Clean up Focus iOS WebEngine related code
// This function won't exist as is as part of the WebEngine. URL bar gets updated through onLocationChange(url:)
func updateURLBar() {
if webViewController.url?.absoluteString != "about:blank" {
urlBar.url = webViewController.url
overlayView.currentURL = urlBar.url?.absoluteString ?? ""
}
}
@available(iOS 14, *)
func buildActions(for sender: UIView) -> [UIMenuElement] {
var actions: [UIMenuElement] = []
if let url = urlBar.url {
let utils = OpenUtils(url: url, webViewController: webViewController)
getShortcutsItem(for: url)
.map(UIAction.init)
.map { UIMenu(options: .displayInline, children: [$0]) }
.map {
actions.append($0)
}
var actionItems: [UIMenuElement] = [UIAction(findInPageItem)]
actionItems.append(
webViewController.requestMobileSite
? UIAction(requestMobileItem)
: UIAction(requestDesktopItem)
)
let actionMenu = UIMenu(options: .displayInline, children: actionItems)
actions.append(actionMenu)
var shareItems: [UIMenuElement?] = [UIAction(copyItem(url: url))]
shareItems.append(UIAction(sharePageItem(for: utils, sender: sender)))
shareItems.append(openInFireFoxItem(for: url).map(UIAction.init))
shareItems.append(openInChromeItem(for: url).map(UIAction.init))
shareItems.append(UIAction(openInDefaultBrowserItem(for: url)))
let shareMenu = UIMenu(options: .displayInline, children: shareItems.compactMap { $0 })
actions.append(shareMenu)
actions.append(UIMenu(options: .displayInline, children: [UIAction(settingsItem)]))
} else {
let actionMenu = UIMenu(options: .displayInline, children: [UIAction(helpItem), UIAction(settingsItem)])
actions.append(actionMenu)
}
return actions
}
func buildActions(for sender: UIView) -> [[PhotonActionSheetItem]] {
var actions: [[PhotonActionSheetItem]] = []
if let url = urlBar.url {
let utils = OpenUtils(url: url, webViewController: webViewController)
actions.append([getShortcutsItem(for: url).map(PhotonActionSheetItem.init)].compactMap { $0 })
var actionItems = [PhotonActionSheetItem(findInPageItem)]
actionItems.append(
webViewController.requestMobileSite
? PhotonActionSheetItem(requestMobileItem)
: PhotonActionSheetItem(requestDesktopItem)
)
var shareItems: [PhotonActionSheetItem?] = [PhotonActionSheetItem(copyItem(url: url))]
shareItems.append(PhotonActionSheetItem(sharePageItem(for: utils, sender: sender)))
shareItems.append(openInFireFoxItem(for: url).map(PhotonActionSheetItem.init))
shareItems.append(openInChromeItem(for: url).map(PhotonActionSheetItem.init))
shareItems.append(PhotonActionSheetItem(openInDefaultBrowserItem(for: url)))
actions.append(actionItems)
actions.append(shareItems.compactMap { $0 })
} else {
actions.append([PhotonActionSheetItem(helpItem)])
}
actions.append([PhotonActionSheetItem(settingsItem)])
return actions
}
func presentContextMenu(from sender: UIButton) {
if #available(iOS 14, *) {
sender.showsMenuAsPrimaryAction = true
sender.menu = UIMenu(children: buildActions(for: sender))
} else {
let pageActionsMenu = PhotonActionSheet(actions: buildActions(for: sender))
presentPhotonActionSheet(pageActionsMenu, from: sender)
}
}
}
extension BrowserViewController: MenuItemProvider {}
extension BrowserViewController: UIDropInteractionDelegate {
func dropInteraction(_ interaction: UIDropInteraction, canHandle session: UIDropSession) -> Bool {
return session.canLoadObjects(ofClass: URL.self)
}
func dropInteraction(_ interaction: UIDropInteraction, sessionDidUpdate session: UIDropSession) -> UIDropProposal {
return UIDropProposal(operation: .copy)
}
func dropInteraction(_ interaction: UIDropInteraction, performDrop session: UIDropSession) {
_ = session.loadObjects(ofClass: URL.self) { urls in
guard let url = urls.first else {
return
}
self.ensureBrowsingMode()
self.urlBar.fillUrlBar(text: url.absoluteString)
self.submit(url: url, source: .action)
GleanMetrics.UrlInteraction.dropEnded.record()
}
}
}
// FXIOS-8628 - #19150 ⁃ Integrate EngineSession find in page in Focus iOS
extension BrowserViewController: FindInPageBarDelegate {
func findInPage(_ findInPage: FindInPageBar, didTextChange text: String) {
find(text, function: "find")
}
func findInPage(_ findInPage: FindInPageBar, didFindNextWithText text: String) {
findInPageBar?.endEditing(true)
find(text, function: "findNext")
}
func findInPage(_ findInPage: FindInPageBar, didFindPreviousWithText text: String) {
findInPageBar?.endEditing(true)
find(text, function: "findPrevious")
}
func findInPageDidPressClose(_ findInPage: FindInPageBar) {
updateFindInPageVisibility(visible: false)
}
private func find(_ text: String, function: String) {
// FXIOS-8628 - #19150 ⁃ Integrate EngineSession find in page in Focus iOS
// This is done through `func findInPage(text: String, function: FindInPageFunction)`
let escaped = text.replacingOccurrences(of: "\\", with: "\\\\").replacingOccurrences(of: "\"", with: "\\\"")
webViewController.evaluate("__firefox__.\(function)(\"\(escaped)\")", completion: nil)
}
private func shortcutContextMenuIsOpenOnIpad() -> Bool {
var shortcutContextMenuIsDisplayed = false
for element in shortcutsContainer.subviews {
if let shortcut = element as? ShortcutView, shortcut.contextMenuIsDisplayed {
shortcutContextMenuIsDisplayed = true
}
}
return isIPadRegularDimensions && shortcutContextMenuIsDisplayed
}
}
extension BrowserViewController: URLBarDelegate {
func urlBar(_ urlBar: URLBar, didAddCustomURL url: URL) {
// Add the URL to the autocomplete list:
let autocompleteSource = CustomCompletionSource(
enableCustomDomainAutocomplete: { Settings.getToggle(.enableCustomDomainAutocomplete) },
getCustomDomainSetting: { Settings.getCustomDomainSetting() },
setCustomDomainSetting: { Settings.setCustomDomainSetting(domains: $0) }
)
switch autocompleteSource.add(suggestion: url.absoluteString) {
case .failure(.duplicateDomain):
break
case .failure(let error):
guard !error.message.isEmpty else { return }
Toast(text: error.message).show()
case .success:
Toast(text: UIConstants.strings.autocompleteCustomURLAdded).show()
}
}
func urlBar(_ urlBar: URLBar, didEnterText text: String) {
let trimmedText = text.trimmingCharacters(in: .whitespaces)
guard urlBar.url?.absoluteString != trimmedText else { return }
shortcutsPresenter.shortcutsState = .editingURL(text: trimmedText)
let isOnHomeView = !urlBar.inBrowsingMode
if Settings.getToggle(.enableSearchSuggestions), !trimmedText.isEmpty {
searchSuggestionsDebouncer.renewInterval()
searchSuggestionsDebouncer.completion = { [weak self] in
guard let self = self else { return }
self.searchSuggestClient.getSuggestions(trimmedText, callback: { suggestions, error in
let userInputText = urlBar.userInputText?.trimmingCharacters(in: .whitespaces) ?? ""
// Check if this callback is stale (new user input has been requested)
if userInputText.isEmpty || userInputText != trimmedText {
return
}
if userInputText == trimmedText {
let suggestions = suggestions ?? [trimmedText]
DispatchQueue.main.async {
self.overlayView.setColorstToSearchButtons()
self.overlayView.setSearchQuery(suggestions: suggestions, hideFindInPage: isOnHomeView || text.isEmpty, hideAddToComplete: true)
}
}
})
}
} else {
overlayView.setSearchQuery(suggestions: [trimmedText], hideFindInPage: isOnHomeView || text.isEmpty, hideAddToComplete: true)
}
}
func urlBarDidPressScrollTop(_: URLBar, tap: UITapGestureRecognizer) {
guard !urlBar.isEditing else { return }
switch scrollBarState {
case .expanded:
let y = tap.location(in: urlBar).y
// If the tap is greater than this threshold, the user wants to type in the URL bar
if y >= 10 {
urlBar.activateTextField()
return
}
// FXIOS-8635 - #19155 Integrate EngineSession scrolling to top in Focus iOS
// Just scroll the vertical position so the page doesn't appear under
// the notch on the iPhone X
var point = webViewController.scrollView.contentOffset
point.y = 0
webViewController.scrollView.setContentOffset(point, animated: true)
case .collapsed: showToolbars()
default: break
}
}
func urlBar(_ urlBar: URLBar, didSubmitText text: String, source: Source) {
let text = text.trimmingCharacters(in: .whitespaces)
guard !text.isEmpty else {
// FXIOS-8637 - #19160 - Integrate onTitleChange, onLocationChange in Focus iOS
urlBar.url = webViewController.url
return
}
SearchHistoryUtils.pushSearchToStack(with: text)
SearchHistoryUtils.isFromURLBar = true
var url = URIFixup.getURL(entry: text)
if url == nil {
recordSearchEvent(source)
url = searchEngineManager.activeEngine.urlForQuery(text)
}
if let urlBarURL = url {
submit(url: urlBarURL, source: source)
urlBar.url = urlBarURL
}
if let urlText = urlBar.url?.absoluteString {
overlayView.currentURL = urlText
}
urlBar.dismiss()
}
func urlBarDidDismiss(_ urlBar: URLBar) {
guard !shortcutContextMenuIsOpenOnIpad() else { return }
overlayView.dismiss()
// FXIOS-8636 - #19159 - Integrate onProgress and onNavigationStateChange in Focus iOS
toggleURLBarBackground(isBright: !webViewController.isLoading)
shortcutsPresenter.shortcutsState = .dismissedURLBar
// FXIOS-8631 - #19154 ⁃ Integrate EngineSession focus in Focus iOS
webViewController.focus()
}
func urlBarDidFocus(_ urlBar: URLBar) {
let isOnHomeView = !urlBar.inBrowsingMode
overlayView.present(isOnHomeView: isOnHomeView)
toggleURLBarBackground(isBright: false)
}
func urlBarDidActivate(_ urlBar: URLBar) {
shortcutsPresenter.shortcutsState = .activeURLBar
homeViewController.updateUI(urlBarIsActive: true, isBrowsing: urlBar.inBrowsingMode)
UIView.animate(withDuration: UIConstants.layout.urlBarTransitionAnimationDuration, animations: {
self.urlBarContainer.alpha = 1
self.updateFindInPageVisibility(visible: false)
self.view.layoutIfNeeded()
})
}
func urlBarDidDeactivate(_ urlBar: URLBar) {
homeViewController.updateUI(urlBarIsActive: false)
UIView.animate(withDuration: UIConstants.layout.urlBarTransitionAnimationDuration) {
self.urlBarContainer.alpha = 0
self.view.setNeedsLayout()
}
}
func urlBarDidTapShield(_ urlBar: URLBar) {
GleanMetrics.TrackingProtection.toolbarShieldClicked.add()
guard let modalDelegate = modalDelegate else { return }
// FXIOS-8634 - #19156 ⁃ Integrate EngineSession favicon in Focus iOS
let favIconPublisher: AnyPublisher<URL?, Never> =
webViewController
.getMetadata()
.map(\.icon)
.map { $0.flatMap(URL.init(string:)) }
.replaceError(with: nil)
.receive(on: DispatchQueue.main)
.eraseToAnyPublisher()
// FXIOS-8643 - #19166 ⁃ Integrate content blocking in Focus iOS
let state: TrackingProtectionState = urlBar.inBrowsingMode
? .browsing(status: SecureConnectionStatus(
url: webViewController.url!,
isSecureConnection: webViewController.connectionIsSecure))
: .homescreen
let trackingProtectionViewController = TrackingProtectionViewController(state: state, onboardingEventsHandler: onboardingEventsHandler, favIconPublisher: favIconPublisher)
trackingProtectionViewController.delegate = self
if UIDevice.current.userInterfaceIdiom == .pad {
trackingProtectionViewController.modalPresentationStyle = .popover
trackingProtectionViewController.popoverPresentationController?.sourceView = urlBar.shieldIconAnchor
modalDelegate.presentModal(viewController: trackingProtectionViewController, animated: true)
} else {
modalDelegate.presentSheet(viewController: trackingProtectionViewController)
}
}
func urlBarDidLongPress(_ urlBar: URLBar) { }
func urlBarDisplayTextForURL(_ url: URL?) -> (String?, Bool) {
// use the initial value for the URL so we can do proper pattern matching with search URLs
var searchURL = urlBar.url
if let url = searchURL, InternalURL.isValid(url: url) {
searchURL = url
}
if let searchURL = searchURL, let query = searchEngineManager.queryForSearchURL(searchURL as URL) {
return (query, true)
} else {
return (url?.absoluteString, false)
}
}
}
extension BrowserViewController: PhotonActionSheetDelegate {
func presentPhotonActionSheet(_ actionSheet: PhotonActionSheet, from sender: UIView, arrowDirection: UIPopoverArrowDirection = .any) {
actionSheet.modalPresentationStyle = .popover
actionSheet.delegate = self
if let popoverVC = actionSheet.popoverPresentationController {
popoverVC.delegate = self
popoverVC.sourceView = sender
popoverVC.permittedArrowDirections = arrowDirection
}
present(actionSheet, animated: true, completion: nil)
}
func photonActionSheetDidDismiss() {
darkView.isHidden = true
}
// FXIOS-8643 - #19166 ⁃ Integrate content blocking in Focus iOS
func photonActionSheetDidToggleProtection(enabled: Bool) {
if enabled {
webViewController.enableTrackingProtection()
} else {
webViewController.disableTrackingProtection()
}
TipManager.sitesNotWorkingTip = false
// FXIOS-8626 - #19148 - Integrate basics APIs of WebEngine in Focus iOS
webViewController.reload()
}
}
extension BrowserViewController: UIAdaptivePresentationControllerDelegate {
// Returning None here makes sure that the Popover is actually presented as a Popover and
// not as a full-screen modal, which is the default on compact device classes.
func adaptivePresentationStyle(for controller: UIPresentationController, traitCollection: UITraitCollection) -> UIModalPresentationStyle {
return .none
}
}
extension BrowserViewController: TrackingProtectionDelegate {
// FXIOS-8643 - #19166 ⁃ Integrate content blocking in Focus iOS
func trackingProtectionDidToggleProtection(enabled: Bool) {
if enabled {
webViewController.enableTrackingProtection()
} else {
webViewController.disableTrackingProtection()
}
TipManager.sitesNotWorkingTip = false
// FXIOS-8626 - #19148 - Integrate basics APIs of WebEngine in Focus iOS
webViewController.reload()
}
}
extension BrowserViewController: HomeViewControllerDelegate {
func homeViewControllerDidTouchEmptyArea(_ controller: HomeViewController) {
urlBar.dismiss()
}
func homeViewControllerDidTapShareTrackers(_ controller: HomeViewController, sender: UIButton) {
let numberOfTrackersBlocked = getNumberOfLifetimeTrackersBlocked()
let appStoreUrl = URL(string: String(format: "https://mzl.la/2GZBav0"), invalidCharacters: false)
// Add space after shareTrackerStatsText to add URL in sentence
let shareTrackerStatsText = "%@, the privacy browser from Mozilla, has already blocked %@ trackers for me. Fewer ads and trackers following me around means faster browsing! Get Focus for yourself here"
let text = String(format: shareTrackerStatsText + " ", AppInfo.productName, String(numberOfTrackersBlocked))
let shareController = UIActivityViewController(activityItems: [text, appStoreUrl as Any], applicationActivities: nil)
// Exact frame dimensions taken from presentPhotonActionSheet
shareController.popoverPresentationController?.sourceView = sender
shareController.popoverPresentationController?.sourceRect = CGRect(x: sender.frame.width/2, y: 0, width: 1, height: 1)
present(shareController, animated: true)
}
/// Visit the given URL. We make sure we are in browsing mode, and dismiss all modals. This is currently private
/// because I don't think it is the best API to expose.
private func visit(url: URL) {
ensureBrowsingMode()
deactivateUrlBarOnHomeView()
dismissSettings()
dismissActionSheet()
submit(url: url, source: .action)
}
func homeViewControllerDidTapTip(_ controller: HomeViewController, tip: TipManager.Tip) {
urlBar.dismiss()
guard let action = tip.action else { return }
switch action {
case .visit(let topic):
visit(url: URL(forSupportTopic: topic))
case .showSettings(let destination):
switch destination {
case .siri:
showSettings(shouldScrollToSiri: true)
case .biometric:
showSettings()
case .siriFavorite:
showSiriFavoriteSettings()
}
}
}
}
extension BrowserViewController: OverlayViewDelegate {
func overlayViewDidPressSettings(_ overlayView: OverlayView) {
urlBar.dismiss()
showSettings()
}
func overlayViewDidTouchEmptyArea(_ overlayView: OverlayView) {
urlBar.dismiss()
}
func overlayView(_ overlayView: OverlayView, didSearchForQuery query: String) {
if searchEngineManager.activeEngine.urlForQuery(query) != nil {
urlBar(urlBar, didSubmitText: query, source: .suggestion)
}
urlBar.dismiss()
}
func overlayView(_ overlayView: OverlayView, didSearchOnPage query: String) {
updateFindInPageVisibility(visible: true, text: query)
self.find(query, function: "find")
}
func overlayView(_ overlayView: OverlayView, didAddToAutocomplete query: String) {
urlBar.dismiss()
let autocompleteSource = CustomCompletionSource(
enableCustomDomainAutocomplete: { Settings.getToggle(.enableCustomDomainAutocomplete) },
getCustomDomainSetting: { Settings.getCustomDomainSetting() },
setCustomDomainSetting: { Settings.setCustomDomainSetting(domains: $0) }
)
switch autocompleteSource.add(suggestion: query) {
case .failure(.duplicateDomain):
Toast(text: UIConstants.strings.autocompleteCustomURLDuplicate).show()
case .failure(let error):
guard !error.message.isEmpty else { return }
Toast(text: error.message).show()
case .success:
Toast(text: UIConstants.strings.autocompleteCustomURLAdded).show()
}
}
func overlayView(_ overlayView: OverlayView, didSubmitText text: String) {
let text = text.trimmingCharacters(in: .whitespaces)
guard !text.isEmpty else {
// FXIOS-8637 - #19160 - Integrate onTitleChange, onLocationChange in Focus iOS
urlBar.url = webViewController.url
return
}
var url = URIFixup.getURL(entry: text)
if url == nil {
recordSearchEvent(.suggestion)
url = searchEngineManager.activeEngine.urlForQuery(text)
}
if let overlayURL = url {
submit(url: overlayURL, source: .suggestion)
urlBar.url = overlayURL
}
urlBar.dismiss()
}
func overlayView(_ overlayView: OverlayView, didTapArrowText text: String) {
urlBar.fillUrlBar(text: text + " ")
searchSuggestClient.getSuggestions(text) { [weak self] suggestions, error in
if error == nil, let suggestions = suggestions {
self?.overlayView.setSearchQuery(suggestions: suggestions, hideFindInPage: true, hideAddToComplete: true)
}
}
}
}
extension BrowserViewController: SearchSuggestionsPromptViewDelegate {
func searchSuggestionsPromptView(_ searchSuggestionsPromptView: SearchSuggestionsPromptView, didEnable: Bool) {
UserDefaults.standard.set(true, forKey: SearchSuggestionsPromptView.respondedToSearchSuggestionsPrompt)
Settings.set(didEnable, forToggle: SettingsToggle.enableSearchSuggestions)
overlayView.updateSearchSuggestionsPrompt(hidden: true)
if didEnable, let urlbar = self.urlBar, let value = self.urlBar?.userInputText {
urlBar(urlbar, didEnterText: value)
}
}
}
extension BrowserViewController: LegacyWebControllerDelegate {
func webControllerDidStartProvisionalNavigation(_ controller: LegacyWebController) {
urlBar.dismiss()
updateFindInPageVisibility(visible: false)
}
func webController(_ controller: LegacyWebController, didUpdateFindInPageResults currentResult: Int?, totalResults: Int?) {
if let total = totalResults {
findInPageBar?.totalResults = total
}
if let current = currentResult {
findInPageBar?.currentResult = current
}
}
func webControllerDidReload(_ controller: LegacyWebController) {
SearchHistoryUtils.isReload = true
}
func webControllerDidStartNavigation(_ controller: LegacyWebController) {
if !SearchHistoryUtils.isFromURLBar && !SearchHistoryUtils.isNavigating && !SearchHistoryUtils.isReload {
SearchHistoryUtils.pushSearchToStack(with: (urlBar.url?.absoluteString)!)
}
SearchHistoryUtils.isReload = false
SearchHistoryUtils.isNavigating = false
SearchHistoryUtils.isFromURLBar = false
urlBarViewModel.isLoading = true
toggleURLBarBackground(isBright: false)
updateURLBar()
}
func webControllerDidFinishNavigation(_ controller: LegacyWebController) {
updateURLBar()
urlBarViewModel.isLoading = false
urlBarViewModel.loadingProgres = 1
toggleURLBarBackground(isBright: !urlBar.isEditing)
GleanMetrics.Browser.totalUriCount.add()
// FXIOS-8641 - #19164 ⁃ Handle webViewController evaluateDocumentContentType
webViewController.evaluateDocumentContentType { documentType in
if documentType == "application/pdf" {
GleanMetrics.Browser.pdfViewerUsed.add()
}
}
Task {
// FXIOS-8634 - #19156 ⁃ Integrate EngineSession favicon in Focus iOS
let faviconURL = try? await webViewController.getMetadata().icon.flatMap(URL.init(string:))
guard let faviconURL = faviconURL, let url = urlBar.url else { return }
shortcutManager.update(faviconURL: faviconURL, for: url)
}
}
func webControllerURLDidChange(_ controller: LegacyWebController, url: URL) {
showToolbars()
}
func webController(_ controller: LegacyWebController, didFailNavigationWithError error: Error) {
// FXIOS-8637 - #19160 - Integrate onTitleChange, onLocationChange in Focus iOS
urlBar.url = webViewController.url
toggleURLBarBackground(isBright: true)
urlBarViewModel.isLoading = false
urlBarViewModel.loadingProgres = 1
}
func webController(_ controller: LegacyWebController, didUpdateCanGoBack canGoBack: Bool) {
urlBarViewModel.canGoBack = canGoBack
}
func webController(_ controller: LegacyWebController, didUpdateCanGoForward canGoForward: Bool) {
urlBarViewModel.canGoForward = canGoForward
}
// FXIOS-8636 - #19159 - Integrate onProgress and onNavigationStateChange in Focus iOS
func webController(_ controller: LegacyWebController, didUpdateEstimatedProgress estimatedProgress: Double) {
// Don't update progress if the home view is visible. This prevents the centered URL bar
// from catching the global progress events.
guard urlBar.inBrowsingMode else { return }
urlBarViewModel.loadingProgres = estimatedProgress
}
// FXIOS-8642 - #19165 ⁃ Integrate scroll controller delegate with Focus iOS
func webController(_ controller: LegacyWebController, scrollViewWillBeginDragging scrollView: UIScrollView) {
lastScrollOffset = scrollView.contentOffset
lastScrollTranslation = scrollView.panGestureRecognizer.translation(in: scrollView)
}
// FXIOS-8642 - #19165 ⁃ Integrate scroll controller delegate with Focus iOS
func webController(_ controller: LegacyWebController, scrollViewDidEndDragging scrollView: UIScrollView) {
snapToolbars(scrollView: scrollView)
}
// FXIOS-8642 - #19165 ⁃ Integrate scroll controller delegate with Focus iOS
func webController(_ controller: LegacyWebController, scrollViewDidScroll scrollView: UIScrollView) {
let translation = scrollView.panGestureRecognizer.translation(in: scrollView)
let isDragging = scrollView.panGestureRecognizer.state != .possible
// This will be 0 if we're moving but not dragging (i.e., gliding after dragging).
let dragDelta = translation.y - lastScrollTranslation.y
// This will match dragDelta unless the URL bar is transitioning.
let offsetDelta = scrollView.contentOffset.y - lastScrollOffset.y
lastScrollOffset = scrollView.contentOffset
lastScrollTranslation = translation
guard scrollBarState != .animating, !scrollView.isZooming else { return }
guard scrollView.contentOffset.y + scrollView.frame.height < scrollView.contentSize.height && (scrollView.contentOffset.y > 0 || scrollBarOffsetAlpha > 0) else {
// We're overscrolling, so don't do anything.
return
}
if !isDragging && offsetDelta < 0 {
// We're gliding up after dragging, so fully show the toolbars.
showToolbars()
return
}
let pageExtendsBeyondScrollView = scrollView.frame.height + (UIConstants.layout.browserToolbarHeight + view.safeAreaInsets.bottom) + UIConstants.layout.urlBarHeight < scrollView.contentSize.height
let toolbarsHiddenAtTopOfPage = scrollView.contentOffset.y <= 0 && scrollBarOffsetAlpha > 0
guard isDragging, (dragDelta < 0 && pageExtendsBeyondScrollView) || toolbarsHiddenAtTopOfPage || scrollBarState == .transitioning else { return }
let lastOffsetAlpha = scrollBarOffsetAlpha
scrollBarOffsetAlpha = (0 ... 1).clamp(scrollBarOffsetAlpha - dragDelta / UIConstants.layout.urlBarHeight)
switch scrollBarOffsetAlpha {
case 0:
scrollBarState = .expanded
case 1:
scrollBarState = .collapsed
default:
scrollBarState = .transitioning
}
let expandAlpha = max(0, (1 - scrollBarOffsetAlpha * 2))
let collapseAlpha = max(0, -(1 - scrollBarOffsetAlpha * 2))
if expandAlpha == 1, collapseAlpha == 0 {
self.urlBar.collapsedState = .extended
} else {
self.urlBar.collapsedState = .intermediate(expandAlpha: expandAlpha, collapseAlpha: collapseAlpha)
}
self.urlBarTopConstraint.update(offset: -scrollBarOffsetAlpha * (UIConstants.layout.urlBarHeight - UIConstants.layout.collapsedUrlBarHeight))
self.toolbarBottomConstraint.update(offset: scrollBarOffsetAlpha * (UIConstants.layout.browserToolbarHeight + view.safeAreaInsets.bottom))
updateViewConstraints()
scrollView.bounds.origin.y += (lastOffsetAlpha - scrollBarOffsetAlpha) * UIConstants.layout.urlBarHeight
lastScrollOffset = scrollView.contentOffset
}
// FXIOS-8642 - #19165 ⁃ Integrate scroll controller delegate with Focus iOS
func webControllerShouldScrollToTop(_ controller: LegacyWebController) -> Bool {
guard scrollBarOffsetAlpha == 0 else {
showToolbars()
return false
}
return true
}
func webControllerDidNavigateBack(_ controller: LegacyWebController) {
handleNavigationBack()
}
func webControllerDidNavigateForward(_ controller: LegacyWebController) {
handleNavigationForward()
}
private func handleNavigationBack() {
// Check if the previous site we were on was AMP
guard let navigatingFromAmpSite = SearchHistoryUtils.pullSearchFromStack()?.hasPrefix(UIConstants.strings.googleAmpURLPrefix) else {
return
}
// Make sure our navigation is not pushed to the SearchHistoryUtils stack (since it already exists there)
SearchHistoryUtils.isFromURLBar = true
// This function is now getting called after our new url is set!!
if !navigatingFromAmpSite {
SearchHistoryUtils.goBack()
}
}
private func handleNavigationForward() {
// Make sure our navigation is not pushed to the SearchHistoryUtils stack (since it already exists there)
SearchHistoryUtils.isFromURLBar = true
// Check if we're navigating to an AMP site *after* the URL bar is updated. This is intentionally grabbing the NEW url
guard let navigatingToAmpSite = urlBar.url?.absoluteString.hasPrefix(UIConstants.strings.googleAmpURLPrefix) else { return }
if !navigatingToAmpSite {
SearchHistoryUtils.goForward()
}
}
func webController(_ controller: LegacyWebController, didUpdateTrackingProtectionStatus trackingStatus: TrackingProtectionStatus, oldTrackingProtectionStatus: TrackingProtectionStatus) {
// Calculate the number of trackers blocked and add that to lifetime total
if case .on(let info) = trackingStatus,
case .on(let oldInfo) = oldTrackingProtectionStatus {
let differenceSinceLastUpdate = max(0, info.total - oldInfo.total)
let numberOfTrackersBlocked = getNumberOfLifetimeTrackersBlocked()
let totalNumberOfTrackersBlocked = numberOfTrackersBlocked + differenceSinceLastUpdate
if totalNumberOfTrackersBlocked > 0 { onboardingEventsHandler.send(.trackerBlocked) }
setNumberOfLifetimeTrackersBlocked(numberOfTrackers: totalNumberOfTrackersBlocked)
}
updateLockIcon(trackingProtectionStatus: trackingStatus)
}
// FXIOS-8642 - #19165 ⁃ Integrate scroll controller delegate with Focus iOS
private func showToolbars() {
let scrollView = webViewController.scrollView
scrollBarState = .animating
UIView.animate(
withDuration: UIConstants.layout.urlBarTransitionAnimationDuration,
delay: 0,
options: .allowUserInteraction,
animations: {
self.urlBar.collapsedState = .extended
self.urlBarTopConstraint.update(offset: 0)
self.toolbarBottomConstraint.update(inset: 0)
scrollView.bounds.origin.y += self.scrollBarOffsetAlpha * UIConstants.layout.urlBarHeight
self.scrollBarOffsetAlpha = 0
self.view.layoutIfNeeded()
},
completion: { _ in
self.scrollBarState = .expanded
}
)
}
// FXIOS-8642 - #19165 ⁃ Integrate scroll controller delegate with Focus iOS
private func hideToolbars() {
let scrollView = webViewController.scrollView
scrollBarState = .animating
UIView.animate(
withDuration: UIConstants.layout.urlBarTransitionAnimationDuration,
delay: 0,
options: .allowUserInteraction,
animations: {
self.urlBar.collapsedState = .collapsed
self.urlBarTopConstraint.update(offset: -UIConstants.layout.urlBarHeight + UIConstants.layout.collapsedUrlBarHeight)
self.toolbarBottomConstraint.update(offset: UIConstants.layout.browserToolbarHeight + self.view.safeAreaInsets.bottom)
scrollView.bounds.origin.y += (self.scrollBarOffsetAlpha - 1) * UIConstants.layout.urlBarHeight
self.scrollBarOffsetAlpha = 1
self.view.layoutIfNeeded()
},
completion: { _ in
self.scrollBarState = .collapsed
}
)
}
private func snapToolbars(scrollView: UIScrollView) {
guard scrollBarState == .transitioning else { return }
if scrollBarOffsetAlpha < 0.05 || scrollView.contentOffset.y < UIConstants.layout.urlBarHeight {
showToolbars()
} else {
hideToolbars()
}
}
}
extension BrowserViewController: KeyboardHelperDelegate {
func keyboardHelper(_ keyboardHelper: KeyboardHelper, keyboardWillShowWithState state: KeyboardState) {
keyboardState = state
self.updateViewConstraints()
UIView.animate(withDuration: state.animationDuration) {
self.homeViewBottomConstraint.update(offset: -state.intersectionHeightForView(view: self.view))
self.view.layoutIfNeeded()
}
}
func keyboardHelper(_ keyboardHelper: KeyboardHelper, keyboardWillHideWithState state: KeyboardState) {
keyboardState = state
self.updateViewConstraints()
UIView.animate(withDuration: state.animationDuration) {
self.homeViewBottomConstraint.update(offset: 0)
self.view.layoutIfNeeded()
}
}
func keyboardHelper(_ keyboardHelper: KeyboardHelper, keyboardDidHideWithState state: KeyboardState) {
if UIDevice.current.userInterfaceIdiom == .pad && !orientationWillChange {
urlBar.dismiss()
}
orientationWillChange = false
}
func keyboardHelper(_ keyboardHelper: KeyboardHelper, keyboardDidShowWithState state: KeyboardState) { }
}
extension BrowserViewController: UIPopoverPresentationControllerDelegate {
func popoverPresentationControllerDidDismissPopover(_ popoverPresentationController: UIPopoverPresentationController) {
darkView.isHidden = true
}
func popoverPresentationController(_ popoverPresentationController: UIPopoverPresentationController, willRepositionPopoverTo rect: UnsafeMutablePointer<CGRect>, in view: AutoreleasingUnsafeMutablePointer<UIView>) {
guard urlBar.inBrowsingMode else { return }
guard let menuSheet = popoverPresentationController.presentedViewController as? PhotonActionSheet, !(menuSheet.popoverPresentationController?.sourceView is ShortcutView) else {
return
}
view.pointee = self.showsToolsetInURLBar ? urlBar.contextMenuButtonAnchor : browserToolbar.contextMenuButtonAnchor
}
}
extension BrowserViewController {
public var eraseIntent: EraseIntent {
let intent = EraseIntent()
intent.suggestedInvocationPhrase = "Erase"
return intent
}
}
extension BrowserViewController: MenuActionable {
func openInFirefox(url: URL) {
guard let escaped = url.absoluteString.addingPercentEncoding(withAllowedCharacters: .urlQueryParameterAllowed),
let firefoxURL = URL(string: "firefox://open-url?url=\(escaped)&private=true", invalidCharacters: false),
UIApplication.shared.canOpenURL(firefoxURL) else {
return
}
UIApplication.shared.open(firefoxURL, options: [:])
GleanMetrics.BrowserMenu.browserMenuAction.record(GleanMetrics.BrowserMenu.BrowserMenuActionExtra(item: "open_in_firefox"))
}
func findInPage() {
NotificationCenter.default.post(Notification(name: Notification.Name(rawValue: UIConstants.strings.findInPageNotification)))
GleanMetrics.BrowserMenu.browserMenuAction.record(GleanMetrics.BrowserMenu.BrowserMenuActionExtra(item: "find_in_page"))
}
func openInDefaultBrowser(url: URL) {
UIApplication.shared.open(url, options: [:])
GleanMetrics.BrowserMenu.browserMenuAction.record(GleanMetrics.BrowserMenu.BrowserMenuActionExtra(item: "open_in_default_browser"))
}
func openInChrome(url: URL) {
// Replace the URL Scheme with the Chrome equivalent.
var chromeScheme: String?
if url.scheme == "http" {
chromeScheme = "googlechrome"
} else if url.scheme == "https" {
chromeScheme = "googlechromes"
}
// Proceed only if a valid Google Chrome URI Scheme is available.
guard let scheme = chromeScheme,
let rangeForScheme = url.absoluteString.range(of: ":"),
let chromeURL = URL(string: scheme + url.absoluteString[rangeForScheme.lowerBound...], invalidCharacters: false) else { return }
// Open the URL with Chrome.
UIApplication.shared.open(chromeURL, options: [:])
GleanMetrics.BrowserMenu.browserMenuAction.record(GleanMetrics.BrowserMenu.BrowserMenuActionExtra(item: "open_in_chrome"))
}
var canOpenInFirefox: Bool {
return UIApplication.shared.canOpenURL(URL(string: "firefox://", invalidCharacters: false)!)
}
var canOpenInChrome: Bool {
return UIApplication.shared.canOpenURL(URL(string: "googlechrome://", invalidCharacters: false)!)
}
func requestDesktopBrowsing() {
NotificationCenter.default.post(Notification(name: Notification.Name(rawValue: UIConstants.strings.requestDesktopNotification)))
GleanMetrics.BrowserMenu.browserMenuAction.record(GleanMetrics.BrowserMenu.BrowserMenuActionExtra(item: "desktop_view_on"))
}
func requestMobileBrowsing() {
NotificationCenter.default.post(Notification(name: Notification.Name(rawValue: UIConstants.strings.requestMobileNotification)))
GleanMetrics.BrowserMenu.browserMenuAction.record(GleanMetrics.BrowserMenu.BrowserMenuActionExtra(item: "desktop_view_off"))
}
func showSharePage(for utils: OpenUtils, sender: UIView) {
let shareVC = utils.buildShareViewController()
// Exact frame dimensions taken from presentPhotonActionSheet
shareVC.popoverPresentationController?.sourceView = sender
shareVC.popoverPresentationController?.sourceRect =
CGRect(
x: sender.frame.width/2,
y: sender.frame.size.height,
width: 1,
height: 1
)
shareVC.becomeFirstResponder()
self.present(shareVC, animated: true, completion: nil)
GleanMetrics.BrowserMenu.browserMenuAction.record(GleanMetrics.BrowserMenu.BrowserMenuActionExtra(item: "share"))
}
func showSettings(shouldScrollToSiri: Bool = false) {
guard let modalDelegate = modalDelegate else { return }
let settingsViewController = SettingsViewController(
searchEngineManager: searchEngineManager,
authenticationManager: authenticationManager,
onboardingEventsHandler: onboardingEventsHandler,
themeManager: themeManager,
dismissScreenCompletion: activateUrlBarOnHomeView,
shouldScrollToSiri: shouldScrollToSiri
)
let settingsNavController = UINavigationController(rootViewController: settingsViewController)
settingsNavController.modalPresentationStyle = .formSheet
modalDelegate.presentModal(viewController: settingsNavController, animated: true)
GleanMetrics.BrowserMenu.browserMenuAction.record(GleanMetrics.BrowserMenu.BrowserMenuActionExtra(item: "settings"))
}
func showHelp() {
}
func showCopy(url: URL) {
UIPasteboard.general.string = url.absoluteString
Toast(text: UIConstants.strings.copyURLToast).show()
GleanMetrics.BrowserMenu.browserMenuAction.record(GleanMetrics.BrowserMenu.BrowserMenuActionExtra(item: "copy_url"))
}
func addToShortcuts(url: URL) {
Task {
// FXIOS-8634 - #19156 ⁃ Integrate EngineSession favicon in Focus iOS
let imageURL = try? await webViewController.getMetadata().icon.flatMap(URL.init(string:))
let shortcut = Shortcut(url: url, imageURL: imageURL)
self.shortcutManager.add(shortcutViewModel: .init(shortcut: shortcut))
GleanMetrics.Shortcuts.shortcutAddedCounter.add()
TipManager.shortcutsTip = false
GleanMetrics.BrowserMenu.browserMenuAction.record(GleanMetrics.BrowserMenu.BrowserMenuActionExtra(item: "add_to_shortcuts"))
Toast(text: UIConstants.strings.shareMenuAddToShortcutsConfirmMessage).show()
}
}
func removeShortcut(url: URL) {
self.shortcutManager.remove(url)
GleanMetrics.Shortcuts.shortcutRemovedCounter["removed_from_browser_menu"].add()
GleanMetrics.BrowserMenu.browserMenuAction.record(GleanMetrics.BrowserMenu.BrowserMenuActionExtra(item: "remove_from_shortcuts"))
Toast(text: UIConstants.strings.shareMenuRemoveShortcutConfirmMessage).show()
}
}
// MARK: - Shortcuts Actions
extension BrowserViewController {
func showRenameAlert(shortcut: Shortcut) {
let alert = UIAlertController.renameAlertController(
currentName: shortcut.name,
renameAction: { [weak self] newName in
guard let self = self else { return }
self.shortcutManager.rename(shortcut: shortcut, newName: newName)
self.urlBar.activateTextField()
}, cancelAction: { [weak self] in
guard let self = self else { return }
self.urlBar.activateTextField()
})
self.show(alert, sender: nil)
}
func removeFromShortcuts() {
self.shortcutsBackground.isHidden = self.shortcutManager.shortcutsViewModels.isEmpty || !self.urlBar.inBrowsingMode ? true : false
GleanMetrics.Shortcuts.shortcutRemovedCounter["removed_from_home_screen"].add()
}
func dismissShortcut() {
guard isIPadRegularDimensions else { return }
urlBarDidDismiss(urlBar)
}
func shortcutTapped(url: URL) {
ensureBrowsingMode()
urlBar.url = url
deactivateUrlBarOnHomeView()
submit(url: url, source: .shortcut)
GleanMetrics.Shortcuts.shortcutOpenedCounter.add()
}
func shortcutDidRemove(shortcut: ShortcutViewModel) {
for subview in shortcutsContainer.subviews where subview is ShortcutView {
let subview = subview as? ShortcutView
if let subviewShortcut = subview?.viewModel, subviewShortcut.shortcut.url == shortcut.shortcut.url {
subview?.removeFromSuperview()
shortcutsContainer.addArrangedSubview(UIView())
}
}
}
func shortcutsDidUpdate() {
for subview in shortcutsContainer.subviews {
subview.removeFromSuperview()
}
addShortcuts()
}
}
extension BrowserViewController {
func openFromWidget() {
guard !urlBar.isEditing else { return }
switch scrollBarState {
case .expanded:
urlBar.activateTextField()
case .collapsed: showToolbars()
default: break
}
}
}