mirror of
https://repo.dactyloidae.xyz/Dactyloidae/UXP.git
synced 2026-09-08 08:48:39 +09:00
Dactyloidae iOS initial commit
This commit is contained in:
parent
daa6179d22
commit
7154a0497e
2123 changed files with 197052 additions and 0 deletions
195
mobile/ios/Client/Helpers/DynamicFontHelper.swift
Normal file
195
mobile/ios/Client/Helpers/DynamicFontHelper.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
356
mobile/ios/Client/Helpers/FxALoginHelper.swift
Normal file
356
mobile/ios/Client/Helpers/FxALoginHelper.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
47
mobile/ios/Client/Helpers/MenuHelper.swift
Normal file
47
mobile/ios/Client/Helpers/MenuHelper.swift
Normal 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]
|
||||
}
|
||||
}
|
||||
178
mobile/ios/Client/Helpers/TabEventHandler.swift
Normal file
178
mobile/ios/Client/Helpers/TabEventHandler.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
71
mobile/ios/Client/Helpers/UserActivityHandler.swift
Normal file
71
mobile/ios/Client/Helpers/UserActivityHandler.swift
Normal 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()
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue