Dactyloidae iOS initial commit

This commit is contained in:
wuggy 2026-06-26 21:04:09 -07:00
commit 7154a0497e
2123 changed files with 197052 additions and 0 deletions

View file

@ -0,0 +1,195 @@
/* 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 Shared
let NotificationDynamicFontChanged = Notification.Name("NotificationDynamicFontChanged")
private let iPadFactor: CGFloat = 1.06
private let iPhoneFactor: CGFloat = 0.88
class DynamicFontHelper: NSObject {
static var defaultHelper: DynamicFontHelper {
struct Singleton {
static let instance = DynamicFontHelper()
}
return Singleton.instance
}
override init() {
defaultStandardFontSize = UIFontDescriptor.preferredFontDescriptor(withTextStyle: UIFontTextStyle.body).pointSize // 14pt -> 17pt -> 23pt
deviceFontSize = defaultStandardFontSize * (UIDevice.current.userInterfaceIdiom == .pad ? iPadFactor : iPhoneFactor)
defaultMediumFontSize = UIFontDescriptor.preferredFontDescriptor(withTextStyle: UIFontTextStyle.footnote).pointSize // 12pt -> 13pt -> 19pt
defaultSmallFontSize = UIFontDescriptor.preferredFontDescriptor(withTextStyle: UIFontTextStyle.caption2).pointSize // 11pt -> 11pt -> 17pt
super.init()
}
/**
* Starts monitoring the ContentSizeCategory chantes
*/
func startObserving() {
NotificationCenter.default.addObserver(self, selector: #selector(DynamicFontHelper.SELcontentSizeCategoryDidChange(_:)), name: NSNotification.Name.UIContentSizeCategoryDidChange, object: nil)
}
deinit {
NotificationCenter.default.removeObserver(self)
}
/**
* Device specific
*/
fileprivate var deviceFontSize: CGFloat
var DeviceFontSize: CGFloat {
return deviceFontSize
}
var DeviceFont: UIFont {
return UIFont.systemFont(ofSize: deviceFontSize, weight: UIFontWeightMedium)
}
var DeviceFontLight: UIFont {
return UIFont.systemFont(ofSize: deviceFontSize, weight: UIFontWeightLight)
}
var DeviceFontSmall: UIFont {
return UIFont.systemFont(ofSize: deviceFontSize - 1, weight: UIFontWeightMedium)
}
var DeviceFontSmallLight: UIFont {
return UIFont.systemFont(ofSize: deviceFontSize - 1, weight: UIFontWeightLight)
}
var DeviceFontSmallHistoryPanel: UIFont {
return UIFont.systemFont(ofSize: deviceFontSize - 3, weight: UIFontWeightLight)
}
var DeviceFontHistoryPanel: UIFont {
return UIFont.systemFont(ofSize: deviceFontSize)
}
var DeviceFontSmallBold: UIFont {
return UIFont.boldSystemFont(ofSize: deviceFontSize - 1)
}
var DeviceFontLarge: UIFont {
return UIFont.systemFont(ofSize: deviceFontSize + 3)
}
var DeviceFontMedium: UIFont {
return UIFont.systemFont(ofSize: deviceFontSize + 1)
}
var DeviceFontLargeBold: UIFont {
return UIFont.boldSystemFont(ofSize: deviceFontSize + 2)
}
var DeviceFontMediumBold: UIFont {
return UIFont.boldSystemFont(ofSize: deviceFontSize + 1)
}
var DeviceFontExtraLargeBold: UIFont {
return UIFont.boldSystemFont(ofSize: deviceFontSize + 4)
}
/*
Activity Stream supports dynamic fonts up to a certain point. Large fonts dont work.
Max out the supported font size.
Small = 14, medium = 18, larger = 20
*/
var MediumSizeRegularWeightAS: UIFont {
let size = min(deviceFontSize, 18)
return UIFont.systemFont(ofSize: size)
}
var LargeSizeRegularWeightAS: UIFont {
let size = min(deviceFontSize + 2, 20)
return UIFont.systemFont(ofSize: size)
}
var MediumSizeHeavyWeightAS: UIFont {
let size = min(deviceFontSize + 2, 18)
return UIFont.systemFont(ofSize: size, weight: UIFontWeightHeavy)
}
var SmallSizeMediumWeightAS: UIFont {
let size = min(defaultSmallFontSize, 14)
return UIFont.systemFont(ofSize: size, weight: UIFontWeightMedium)
}
var MediumSizeBoldFontAS: UIFont {
let size = min(deviceFontSize, 18)
return UIFont.boldSystemFont(ofSize: size)
}
var SmallSizeRegularWeightAS: UIFont {
let size = min(defaultSmallFontSize, 14)
return UIFont.systemFont(ofSize: size)
}
/**
* Small
*/
fileprivate var defaultSmallFontSize: CGFloat
var DefaultSmallFontSize: CGFloat {
return defaultSmallFontSize
}
var DefaultSmallFont: UIFont {
return UIFont.systemFont(ofSize: defaultSmallFontSize, weight: UIFontWeightRegular)
}
var DefaultSmallFontBold: UIFont {
return UIFont.boldSystemFont(ofSize: defaultSmallFontSize)
}
/**
* Medium
*/
fileprivate var defaultMediumFontSize: CGFloat
var DefaultMediumFontSize: CGFloat {
return defaultMediumFontSize
}
var DefaultMediumFont: UIFont {
return UIFont.systemFont(ofSize: defaultMediumFontSize, weight: UIFontWeightRegular)
}
var DefaultMediumBoldFont: UIFont {
return UIFont.boldSystemFont(ofSize: defaultMediumFontSize)
}
/**
* Standard
*/
fileprivate var defaultStandardFontSize: CGFloat
var DefaultStandardFontSize: CGFloat {
return defaultStandardFontSize
}
var DefaultStandardFont: UIFont {
return UIFont.systemFont(ofSize: defaultStandardFontSize, weight: UIFontWeightRegular)
}
var DefaultStandardFontBold: UIFont {
return UIFont.boldSystemFont(ofSize: defaultStandardFontSize)
}
/**
* Reader mode
*/
var ReaderStandardFontSize: CGFloat {
return defaultStandardFontSize - 2
}
var ReaderBigFontSize: CGFloat {
return defaultStandardFontSize + 5
}
/**
* Intro mode
*/
var IntroStandardFontSize: CGFloat {
return min(defaultStandardFontSize - 1, 16)
}
var IntroBigFontSize: CGFloat {
return min(defaultStandardFontSize + 1, 18)
}
func refreshFonts() {
defaultStandardFontSize = UIFontDescriptor.preferredFontDescriptor(withTextStyle: UIFontTextStyle.body).pointSize
deviceFontSize = defaultStandardFontSize * (UIDevice.current.userInterfaceIdiom == .pad ? iPadFactor : iPhoneFactor)
defaultMediumFontSize = UIFontDescriptor.preferredFontDescriptor(withTextStyle: UIFontTextStyle.footnote).pointSize
defaultSmallFontSize = UIFontDescriptor.preferredFontDescriptor(withTextStyle: UIFontTextStyle.caption2).pointSize
}
func SELcontentSizeCategoryDidChange(_ notification: Notification) {
refreshFonts()
let notification = Notification(name: NotificationDynamicFontChanged, object: nil)
NotificationCenter.default.post(notification)
}
}

View file

@ -0,0 +1,356 @@
/* 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 Account
import Deferred
import Foundation
import Shared
import SwiftyJSON
import Sync
import UserNotifications
import XCGLogger
private let applicationDidRequestUserNotificationPermissionPrefKey = "applicationDidRequestUserNotificationPermissionPrefKey"
private let log = Logger.browserLogger
private let verificationPollingInterval = DispatchTimeInterval.seconds(3)
private let verificationMaxRetries = 100 // Poll every 3 seconds for 5 minutes.
protocol FxAPushLoginDelegate: class {
func accountLoginDidFail()
func accountLoginDidSucceed(withFlags flags: FxALoginFlags)
}
/// Small struct to keep together the immediately actionable flags that the UI is likely to immediately
/// following a successful login. This is not supposed to be a long lived object.
struct FxALoginFlags {
let pushEnabled: Bool
let verified: Bool
}
enum PushNotificationError: MaybeErrorType {
case registrationFailed
case userDisallowed
case wrongOSVersion
var description: String {
switch self {
case .registrationFailed:
return "The OS was unable to complete APNS registration"
case .userDisallowed:
return "User refused permission for notifications"
case .wrongOSVersion:
return "The version of iOS is not recent enough"
}
}
}
/// This class manages the from successful login for FxAccounts to
/// asking the user for notification permissions, registering for
/// remote push notifications (APNS), then creating an account and
/// storing it in the profile.
class FxALoginHelper {
static var sharedInstance: FxALoginHelper = {
return FxALoginHelper()
}()
weak var delegate: FxAPushLoginDelegate?
fileprivate weak var profile: Profile?
fileprivate var account: FirefoxAccount!
fileprivate var accountVerified: Bool!
fileprivate var pushClient: PushClient? {
guard let pushConfiguration = self.getPushConfiguration() ?? self.profile?.accountConfiguration.pushConfiguration,
let accountConfiguration = self.profile?.accountConfiguration else {
log.error("Push server endpoint could not be found")
return nil
}
// Experimental mode needs: a) the scheme to be Fennec, and b) the accountConfiguration to be flipped in debug mode.
let experimentalMode = (pushConfiguration.label == .fennec && accountConfiguration.label == .latestDev)
return PushClient(endpointURL: pushConfiguration.endpointURL, experimentalMode: experimentalMode)
}
fileprivate var apnsTokenDeferred: Deferred<Maybe<String>>!
// This should be called when the application has started.
// This configures the helper for logging into Firefox Accounts, and
// if already logged in, checking if anything needs to be done in response
// to changing of user settings and push notifications.
func application(_ application: UIApplication, didLoadProfile profile: Profile) {
self.profile = profile
self.account = profile.getAccount()
self.apnsTokenDeferred = Deferred()
guard let account = self.account else {
// There's no account, no further action.
return loginDidFail()
}
// accountVerified is needed by delegates.
accountVerified = account.actionNeeded != .needsVerification
guard AppConstants.MOZ_FXA_PUSH else {
return loginDidSucceed()
}
if let _ = account.pushRegistration {
// We have an account, and it's already registered for push notifications.
return loginDidSucceed()
}
// Now: we have an account that does not have push notifications set up.
// however, we need to deal with cases of asking for permissions too frequently.
let asked = profile.prefs.boolForKey(applicationDidRequestUserNotificationPermissionPrefKey) ?? true
let permitted = application.currentUserNotificationSettings!.types != .none
// If we've never asked(*), then we should probably ask.
// If we've asked already, then we should not ask again.
// TODO: add UI to tell the user to go flip the Setting app.
// (*) if we asked in a prior release, and the user was ok with it, then there is no harm asking again.
// If the user denied permission, or flipped permissions in the Settings app, then
// we'll bug them once, but this is probably unavoidable.
if asked && !permitted {
return loginDidSucceed()
}
// By the time we reach here, we haven't registered for APNS
// Either we've never asked the user, or the user declined, then re-enabled
// the notification in the Settings app.
requestUserNotifications(application)
}
// This is called when the user logs into a new FxA account.
// It manages the asking for user permission for notification and registration
// for APNS and WebPush notifications.
func application(_ application: UIApplication, didReceiveAccountJSON data: JSON) {
if data["keyFetchToken"].stringValue() == nil || data["unwrapBKey"].stringValue() == nil {
// The /settings endpoint sends a partial "login"; ignore it entirely.
log.error("Ignoring didSignIn with keyFetchToken or unwrapBKey missing.")
return self.loginDidFail()
}
assert(profile != nil, "Profile should still exist and be loaded into this FxAPushLoginStateMachine")
guard let profile = profile,
let account = FirefoxAccount.from(profile.accountConfiguration, andJSON: data) else {
return self.loginDidFail()
}
accountVerified = data["verified"].bool ?? false
self.account = account
if AppConstants.MOZ_SHOW_FXA_AVATAR {
account.updateProfile()
}
let leanplum = LeanPlumClient.shared
if leanplum.isLPEnabled() && leanplum.isFxAPrePushEnabled() {
// If Leanplum A/B push notification tests are enabled, defer to them for
// displaying the pre-push permission dialog. If user dismisses it, we will still have
// another chance to prompt them. Afterwards, Leanplum calls `apnsRegisterDidSucceed` or
// `apnsRegisterDidFail` to finish setting up Autopush.
return readyForSyncing()
}
requestUserNotifications(application)
}
func getDeviceToken(_ application: UIApplication) -> Deferred<Maybe<String>> {
self.requestUserNotifications(application)
return self.apnsTokenDeferred
}
func requestUserNotifications(_ application: UIApplication) {
if let deferred = self.apnsTokenDeferred, deferred.isFilled,
let token = deferred.value.successValue {
// If we have an account, then it'll go through ahead and register
// with autopush here.
// If not we'll just bail. The Deferred will do the rest.
return self.apnsRegisterDidSucceed(token)
}
DispatchQueue.main.async {
self.requestUserNotificationsMainThreadOnly(application)
}
}
fileprivate func requestUserNotificationsMainThreadOnly(_ application: UIApplication) {
assert(Thread.isMainThread, "requestAuthorization should be run on the main thread")
let center = UNUserNotificationCenter.current()
return center.requestAuthorization(options: [.alert, .badge, .sound]) { (granted, error) in
guard error == nil else {
return self.application(application, canDisplayUserNotifications: false)
}
self.application(application, canDisplayUserNotifications: granted)
}
}
// This is necessarily called from the AppDelegate.
// Once we have permission from the user to display notifications, we should
// try and register for APNS. If not, then start syncing.
func application(_ application: UIApplication, didRegisterUserNotificationSettings notificationSettings: UIUserNotificationSettings) {
let types = notificationSettings.types
let allowed = types != .none && types.rawValue != 0
self.application(application, canDisplayUserNotifications: allowed)
}
func application(_ application: UIApplication, canDisplayUserNotifications allowed: Bool) {
guard allowed else {
apnsTokenDeferred?.fillIfUnfilled(Maybe.failure(PushNotificationError.userDisallowed))
return readyForSyncing()
}
// Record that we have asked the user, and they have given an answer.
profile?.prefs.setBool(true, forKey: applicationDidRequestUserNotificationPermissionPrefKey)
if AppConstants.MOZ_FXA_PUSH {
DispatchQueue.main.async {
application.registerForRemoteNotifications()
}
} else {
readyForSyncing()
}
}
func getPushConfiguration() -> PushConfiguration? {
let label = PushConfigurationLabel(rawValue: AppConstants.scheme)
return label?.toConfiguration()
}
func apnsRegisterDidSucceed(_ deviceToken: Data) {
let apnsToken = deviceToken.hexEncodedString
self.apnsTokenDeferred?.fillIfUnfilled(Maybe(success: apnsToken))
self.apnsRegisterDidSucceed(apnsToken)
}
fileprivate func apnsRegisterDidSucceed(_ apnsToken: String) {
guard self.account != nil else {
// If we aren't logged in to FxA at this point
// we should bail.
return loginDidFail()
}
guard let pushClient = self.pushClient else {
return pushRegistrationDidFail()
}
if let pushRegistration = account.pushRegistration {
// Currently, we don't support routine changing of push subscriptions
// then we can assume that if we've already registered with the
// push server, then we don't need to do it again.
_ = pushClient.updateUAID(apnsToken, withRegistration: pushRegistration)
return
}
pushClient.register(apnsToken).upon { res in
guard let pushRegistration = res.successValue else {
return self.pushRegistrationDidFail()
}
return self.pushRegistrationDidSucceed(apnsToken: apnsToken, pushRegistration: pushRegistration)
}
}
func apnsRegisterDidFail() {
self.apnsTokenDeferred?.fillIfUnfilled(Maybe(failure: PushNotificationError.registrationFailed))
readyForSyncing()
}
fileprivate func pushRegistrationDidSucceed(apnsToken: String, pushRegistration: PushRegistration) {
account.pushRegistration = pushRegistration
readyForSyncing()
}
fileprivate func pushRegistrationDidFail() {
readyForSyncing()
}
func readyForSyncing() {
guard let profile = self.profile, let account = self.account else {
return loginDidFail()
}
profile.setAccount(account)
awaitVerification()
loginDidSucceed()
}
fileprivate func awaitVerification(_ attemptsLeft: Int = verificationMaxRetries) {
guard let account = account,
let profile = profile else {
return
}
if attemptsLeft == 0 {
return
}
// The only way we can tell if the account has been verified is to
// start a sync. If it works, then yay,
account.advance().upon { state in
guard state.actionNeeded == .needsVerification else {
// Verification has occurred remotely, and we can proceed.
// The state machine will have told any listening UIs that
// we're done.
return self.performVerifiedSync(profile, account: account)
}
let queue = DispatchQueue.global(qos: DispatchQoS.background.qosClass)
queue.asyncAfter(deadline: DispatchTime.now() + verificationPollingInterval) {
self.awaitVerification(attemptsLeft - 1)
}
}
}
fileprivate func loginDidSucceed() {
let flags = FxALoginFlags(pushEnabled: account?.pushRegistration != nil, verified: accountVerified)
delegate?.accountLoginDidSucceed(withFlags: flags)
}
fileprivate func loginDidFail() {
delegate?.accountLoginDidFail()
}
func performVerifiedSync(_ profile: Profile, account: FirefoxAccount) {
profile.syncManager.syncEverything(why: .didLogin)
}
}
extension FxALoginHelper {
func applicationDidDisconnect(_ application: UIApplication) {
// According to https://developer.apple.com/documentation/uikit/uiapplication/1623093-unregisterforremotenotifications
// we should be calling:
application.unregisterForRemoteNotifications()
// However, https://forums.developer.apple.com/message/179264#179264 advises against it, suggesting there is
// a 24h period after unregistering where re-registering fails. This doesn't seem to be the case (for me)
// but this may be useful to know if QA/user-testing find this a problem.
// Whatever, we should unregister from the autopush server. That means we definitely won't be getting any
// messages.
if let pushRegistration = self.account.pushRegistration,
let pushClient = self.pushClient {
_ = pushClient.unregister(pushRegistration)
}
// TODO: fix Bug 1168690, to tell Sync to delete this client and its tabs.
// i.e. upload a {deleted: true} client record.
// Tell FxA we're no longer attached.
self.account.destroyDevice()
// Cleanup the database.
self.profile?.removeAccount()
// Cleanup the FxALoginHelper.
self.account = nil
self.accountVerified = nil
self.profile?.prefs.removeObjectForKey(PendingAccountDisconnectedKey)
}
}

View file

@ -0,0 +1,47 @@
/* 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
@objc public protocol MenuHelperInterface {
@objc optional func menuHelperCopy()
@objc optional func menuHelperOpenAndFill()
@objc optional func menuHelperReveal()
@objc optional func menuHelperSecure()
@objc optional func menuHelperFindInPage()
}
open class MenuHelper: NSObject {
open static let SelectorCopy: Selector = #selector(MenuHelperInterface.menuHelperCopy)
open static let SelectorHide: Selector = #selector(MenuHelperInterface.menuHelperSecure)
open static let SelectorOpenAndFill: Selector = #selector(MenuHelperInterface.menuHelperOpenAndFill)
open static let SelectorReveal: Selector = #selector(MenuHelperInterface.menuHelperReveal)
open static let SelectorFindInPage: Selector = #selector(MenuHelperInterface.menuHelperFindInPage)
open class var defaultHelper: MenuHelper {
struct Singleton {
static let instance = MenuHelper()
}
return Singleton.instance
}
open func setItems() {
let revealPasswordTitle = NSLocalizedString("Reveal", tableName: "LoginManager", comment: "Reveal password text selection menu item")
let revealPasswordItem = UIMenuItem(title: revealPasswordTitle, action: MenuHelper.SelectorReveal)
let hidePasswordTitle = NSLocalizedString("Hide", tableName: "LoginManager", comment: "Hide password text selection menu item")
let hidePasswordItem = UIMenuItem(title: hidePasswordTitle, action: MenuHelper.SelectorHide)
let copyTitle = NSLocalizedString("Copy", tableName: "LoginManager", comment: "Copy password text selection menu item")
let copyItem = UIMenuItem(title: copyTitle, action: MenuHelper.SelectorCopy)
let openAndFillTitle = NSLocalizedString("Open & Fill", tableName: "LoginManager", comment: "Open and Fill website text selection menu item")
let openAndFillItem = UIMenuItem(title: openAndFillTitle, action: MenuHelper.SelectorOpenAndFill)
let findInPageTitle = NSLocalizedString("Find in Page", tableName: "FindInPage", comment: "Text selection menu item")
let findInPageItem = UIMenuItem(title: findInPageTitle, action: MenuHelper.SelectorFindInPage)
UIMenuController.shared.menuItems = [copyItem, revealPasswordItem, hidePasswordItem, openAndFillItem, findInPageItem]
}
}

View file

@ -0,0 +1,178 @@
/* 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 Storage
/**
* A handler can be a plain old swift object. It does not need to extend any
* other object, but can.
*
* Handlers should register for tab events with the `registerFor` method, and
* cleanup with the `unregister` method.
*
* ```
* class HandoffHandler {
* var tabObservers: TabObservers!
*
* init() {
* tabObservers = registerFor(.didLoadFavicon, .didLoadPageMetadata)
* }
*
* deinit {
* unregister(tabObservers)
* }
* }
* ```
*
* Handlers can implement any or all `TabEventHandler` methods. If you
* implement a method, you should probably `registerFor` the event above.
*
* ```
* extension HandoffHandler: TabEventHandler {
* func tab(_ tab: Tab, didLoadPageMetadata metadata: PageMetadata) {
* print("\(tab) has \(pageMetadata)")
* }
*
* func tab(_ tab: Tab, didLoadFavicon favicon: Favicon) {
* print("\(tab) has \(favicon)")
* }
* }
* ```
*
* Tab events should probably be only posted from one place, to avoid cycles.
*
* ```
* TabEvent.post(.didLoadPageMetadata(aPageMetadata), for: tab)
* ```
*
* In this manner, we are able to use the notification center and have type safety.
*
*/
// As we want more events we add more here.
// Each event needs:
// 1. a method in the TabEventHandler.
// 2. a default implementation of the method so not everyone needs to implement it
// 3. a TabEventLabel, which is needed for registration
// 4. a TabEvent, with whatever parameters are needed.
// i) a case to map the event to the event label (var label)
// ii) a case to map the event to the event handler (func handle:with:)
protocol TabEventHandler {
func tab(_ tab: Tab, didChangeURL url: URL)
func tab(_ tab: Tab, didLoadPageMetadata metadata: PageMetadata)
func tab(_ tab: Tab, didLoadFavicon favicon: Favicon?, with: Data?)
func tabDidGainFocus(_ tab: Tab)
func tabDidLoseFocus(_ tab: Tab)
func tabDidClose(_ tab: Tab)
}
// Provide default implmentations, because we don't want to litter the code with
// empty methods, and `@objc optional` doesn't really work very well.
extension TabEventHandler {
func tab(_ tab: Tab, didChangeURL url: URL) {}
func tab(_ tab: Tab, didLoadPageMetadata metadata: PageMetadata) {}
func tab(_ tab: Tab, didLoadFavicon favicon: Favicon?, with: Data?) {}
func tabDidGainFocus(_ tab: Tab) {}
func tabDidLoseFocus(_ tab: Tab) {}
func tabDidClose(_ tab: Tab) {}
}
enum TabEventLabel: String {
case didChangeURL
case didLoadPageMetadata
case didLoadFavicon
case didGainFocus
case didLoseFocus
case didClose
}
enum TabEvent {
case didChangeURL(URL)
case didLoadPageMetadata(PageMetadata)
case didLoadFavicon(Favicon?, with: Data?)
case didGainFocus
case didLoseFocus
case didClose
var label: TabEventLabel {
switch self {
case .didChangeURL:
return .didChangeURL
case .didLoadPageMetadata:
return .didLoadPageMetadata
case .didLoadFavicon:
return .didLoadFavicon
case .didGainFocus:
return .didGainFocus
case .didLoseFocus:
return .didLoseFocus
case .didClose:
return .didClose
}
}
func handle(_ tab: Tab, with handler: TabEventHandler) {
switch self {
case .didChangeURL(let url):
handler.tab(tab, didChangeURL: url)
case .didLoadPageMetadata(let metadata):
handler.tab(tab, didLoadPageMetadata: metadata)
case .didLoadFavicon(let favicon, let data):
handler.tab(tab, didLoadFavicon: favicon, with: data)
case .didGainFocus:
handler.tabDidGainFocus(tab)
case .didLoseFocus:
handler.tabDidLoseFocus(tab)
case .didClose:
handler.tabDidClose(tab)
}
}
}
// Hide some of the machinery away from the boiler plate above.
////////////////////////////////////////////////////////////////////////////////////////
extension TabEventLabel {
var name: Notification.Name {
return Notification.Name(self.rawValue)
}
}
extension TabEvent {
func notification(for tab: Any) -> Notification {
return Notification(name: label.name, object: tab, userInfo: ["payload": self])
}
/// Use this method to post notifications to any concerned listeners.
static func post(_ event: TabEvent, for tab: Any) {
center.post(event.notification(for: tab))
}
}
// These methods are used by TabEventHandler implementers.
// Their usage remains consistent, even as we add more event types and handler methods.
////////////////////////////////////////////////////////////////////////////////////////
private let center = NotificationCenter()
typealias TabObservers = [NSObjectProtocol]
extension TabEventHandler {
/// Implementations of handles should use this method to register for events.
/// `TabObservers` should be preserved for unregistering later.
func registerFor(_ tabEvents: TabEventLabel..., queue: OperationQueue? = nil) -> TabObservers {
return tabEvents.map { eventType in
center.addObserver(forName: eventType.name, object: nil, queue: queue) { notification in
guard let tab = notification.object as? Tab,
let event = notification.userInfo?["payload"] as? TabEvent else {
return
}
event.handle(tab, with: self)
}
}
}
func unregister(_ observers: TabObservers) {
observers.forEach { observer in
center.removeObserver(observer)
}
}
}

View file

@ -0,0 +1,71 @@
/* 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 Shared
import Storage
import CoreSpotlight
import MobileCoreServices
import WebKit
private let browsingActivityType: String = "org.mozilla.ios.firefox.browsing"
private let searchableIndex = CSSearchableIndex(name: "firefox")
class UserActivityHandler {
private var tabObservers: TabObservers!
init() {
self.tabObservers = registerFor(
.didLoseFocus,
.didGainFocus,
.didChangeURL,
// .didLoadMetadata // TODO: Bug 1390294
// .didLoadFavicon, // TODO: Bug 1390294
.didClose,
queue: .main)
}
deinit {
unregister(tabObservers)
}
class func clearSearchIndex(completionHandler: ((Error?) -> Void)? = nil) {
searchableIndex.deleteAllSearchableItems(completionHandler: completionHandler)
}
}
extension UserActivityHandler: TabEventHandler {
func tabDidGainFocus(_ tab: Tab) {
tab.userActivity?.becomeCurrent()
}
func tabDidLoseFocus(_ tab: Tab) {
tab.userActivity?.resignCurrent()
}
func tab(_ tab: Tab, didChangeURL url: URL) {
guard url.isWebPage(includeDataURIs: false), !url.isLocal else {
tab.userActivity?.resignCurrent()
tab.userActivity = nil
return
}
tab.userActivity?.invalidate()
let userActivity = NSUserActivity(activityType: browsingActivityType)
userActivity.webpageURL = url
userActivity.becomeCurrent()
tab.userActivity = userActivity
}
func tabDidClose(_ tab: Tab) {
guard let userActivity = tab.userActivity else {
return
}
tab.userActivity = nil
userActivity.invalidate()
}
}