mirror of
https://repo.dactyloidae.xyz/Dactyloidae/UXP.git
synced 2026-08-15 08:53:07 +09:00
884 lines
38 KiB
Swift
884 lines
38 KiB
Swift
/* 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 Shared
|
|
import Storage
|
|
import AVFoundation
|
|
import XCGLogger
|
|
import MessageUI
|
|
import SDWebImage
|
|
import SwiftKeychainWrapper
|
|
import LocalAuthentication
|
|
import SyncTelemetry
|
|
import SwiftRouter
|
|
import Sync
|
|
import CoreSpotlight
|
|
|
|
private let log = Logger.browserLogger
|
|
|
|
let LatestAppVersionProfileKey = "latestAppVersion"
|
|
let AllowThirdPartyKeyboardsKey = "settings.allowThirdPartyKeyboards"
|
|
private let InitialPingSentKey = "initialPingSent"
|
|
|
|
class AppDelegate: UIResponder, UIApplicationDelegate, UIViewControllerRestoration {
|
|
public static func viewController(withRestorationIdentifierPath identifierComponents: [Any], coder: NSCoder) -> UIViewController? {
|
|
return nil
|
|
}
|
|
|
|
var window: UIWindow?
|
|
var browserViewController: BrowserViewController!
|
|
var rootViewController: UIViewController!
|
|
weak var profile: Profile?
|
|
var tabManager: TabManager!
|
|
var adjustIntegration: AdjustIntegration?
|
|
var applicationCleanlyBackgrounded = true
|
|
|
|
weak var application: UIApplication?
|
|
var launchOptions: [AnyHashable: Any]?
|
|
|
|
let appVersion = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as! String
|
|
|
|
var openInFirefoxParams: LaunchParams?
|
|
|
|
var receivedURLs: [URL]?
|
|
var unifiedTelemetry: UnifiedTelemetry?
|
|
|
|
@discardableResult func application(_ application: UIApplication, willFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
|
|
//
|
|
// Determine if the application cleanly exited last time it was used. We default to true in
|
|
// case we have never done this before. Then check if the "ApplicationCleanlyBackgrounded" user
|
|
// default exists and whether was properly set to true on app exit.
|
|
//
|
|
// Then we always set the user default to false. It will be set to true when we the application
|
|
// is backgrounded.
|
|
//
|
|
|
|
self.applicationCleanlyBackgrounded = true
|
|
|
|
let defaults = UserDefaults()
|
|
if defaults.object(forKey: "ApplicationCleanlyBackgrounded") != nil {
|
|
self.applicationCleanlyBackgrounded = defaults.bool(forKey: "ApplicationCleanlyBackgrounded")
|
|
}
|
|
defaults.set(false, forKey: "ApplicationCleanlyBackgrounded")
|
|
defaults.synchronize()
|
|
|
|
// Hold references to willFinishLaunching parameters for delayed app launch
|
|
self.application = application
|
|
self.launchOptions = launchOptions
|
|
|
|
self.window = UIWindow(frame: UIScreen.main.bounds)
|
|
self.window!.backgroundColor = UIColor.white
|
|
|
|
// Short circuit the app if we want to email logs from the debug menu
|
|
if DebugSettingsBundleOptions.launchIntoEmailComposer {
|
|
self.window?.rootViewController = UIViewController()
|
|
presentEmailComposerWithLogs()
|
|
return true
|
|
} else {
|
|
return startApplication(application, withLaunchOptions: launchOptions)
|
|
}
|
|
}
|
|
|
|
@discardableResult fileprivate func startApplication(_ application: UIApplication, withLaunchOptions launchOptions: [AnyHashable: Any]?) -> Bool {
|
|
log.info("startApplication begin")
|
|
|
|
// Need to get "settings.sendUsageData" this way so that Sentry can be initialized
|
|
// before getting the Profile.
|
|
let sendUsageData = NSUserDefaultsPrefs(prefix: "profile").boolForKey(AppConstants.PrefSendUsageData) ?? true
|
|
Sentry.shared.setup(sendUsageData: sendUsageData)
|
|
|
|
// Set the Firefox UA for browsing.
|
|
setUserAgent()
|
|
|
|
// Start the keyboard helper to monitor and cache keyboard state.
|
|
KeyboardHelper.defaultHelper.startObserving()
|
|
|
|
DynamicFontHelper.defaultHelper.startObserving()
|
|
|
|
MenuHelper.defaultHelper.setItems()
|
|
|
|
let logDate = Date()
|
|
// Create a new sync log file on cold app launch. Note that this doesn't roll old logs.
|
|
Logger.syncLogger.newLogWithDate(logDate)
|
|
|
|
Logger.browserLogger.newLogWithDate(logDate)
|
|
|
|
let profile = getProfile(application)
|
|
|
|
unifiedTelemetry = UnifiedTelemetry(profile: profile)
|
|
|
|
if !DebugSettingsBundleOptions.disableLocalWebServer {
|
|
// Set up a web server that serves us static content. Do this early so that it is ready when the UI is presented.
|
|
setUpWebServer(profile)
|
|
}
|
|
|
|
do {
|
|
// for aural progress bar: play even with silent switch on, and do not stop audio from other apps (like music)
|
|
try AVAudioSession.sharedInstance().setCategory(AVAudioSessionCategoryPlayback, with: AVAudioSessionCategoryOptions.mixWithOthers)
|
|
} catch _ {
|
|
print("Error: Failed to assign AVAudioSession category to allow playing with silent switch on for aural progress bar")
|
|
}
|
|
|
|
let imageStore = DiskImageStore(files: profile.files, namespace: "TabManagerScreenshots", quality: UIConstants.ScreenshotQuality)
|
|
|
|
// Temporary fix for Bug 1390871 - NSInvalidArgumentException: -[WKContentView menuHelperFindInPage]: unrecognized selector
|
|
if #available(iOS 11, *) {
|
|
if let clazz = NSClassFromString("WKCont" + "ent" + "View"), let swizzledMethod = class_getInstanceMethod(TabWebViewMenuHelper.self, #selector(TabWebViewMenuHelper.swizzledMenuHelperFindInPage)) {
|
|
class_addMethod(clazz, MenuHelper.SelectorFindInPage, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod))
|
|
}
|
|
}
|
|
|
|
self.tabManager = TabManager(prefs: profile.prefs, imageStore: imageStore)
|
|
self.tabManager.stateDelegate = self
|
|
|
|
// Add restoration class, the factory that will return the ViewController we
|
|
// will restore with.
|
|
|
|
browserViewController = BrowserViewController(profile: self.profile!, tabManager: self.tabManager)
|
|
browserViewController.edgesForExtendedLayout = []
|
|
|
|
browserViewController.restorationIdentifier = NSStringFromClass(BrowserViewController.self)
|
|
browserViewController.restorationClass = AppDelegate.self
|
|
|
|
let navigationController = UINavigationController(rootViewController: browserViewController)
|
|
navigationController.delegate = self
|
|
navigationController.isNavigationBarHidden = true
|
|
navigationController.edgesForExtendedLayout = UIRectEdge(rawValue: 0)
|
|
rootViewController = navigationController
|
|
|
|
self.window!.rootViewController = rootViewController
|
|
|
|
NotificationCenter.default.addObserver(forName: NSNotification.Name.FSReadingListAddReadingListItem, object: nil, queue: nil) { (notification) -> Void in
|
|
if let userInfo = notification.userInfo, let url = userInfo["URL"] as? URL {
|
|
let title = (userInfo["Title"] as? String) ?? ""
|
|
profile.readingList?.createRecordWithURL(url.absoluteString, title: title, addedBy: UIDevice.current.name)
|
|
}
|
|
}
|
|
|
|
NotificationCenter.default.addObserver(forName: NotificationFirefoxAccountDeviceRegistrationUpdated, object: nil, queue: nil) { _ in
|
|
profile.flushAccount()
|
|
}
|
|
|
|
// check to see if we started 'cos someone tapped on a notification.
|
|
if let localNotification = launchOptions?[UIApplicationLaunchOptionsKey.localNotification] as? UILocalNotification {
|
|
viewURLInNewTab(localNotification)
|
|
}
|
|
|
|
adjustIntegration = AdjustIntegration(profile: profile)
|
|
|
|
if LeanPlumClient.shouldEnable(profile: profile) {
|
|
LeanPlumClient.shared.setup(profile: profile)
|
|
LeanPlumClient.shared.set(enabled: true)
|
|
}
|
|
|
|
self.updateAuthenticationInfo()
|
|
SystemUtils.onFirstRun()
|
|
|
|
let fxaLoginHelper = FxALoginHelper.sharedInstance
|
|
fxaLoginHelper.application(application, didLoadProfile: profile)
|
|
|
|
setUpDeepLinks(application: application)
|
|
|
|
log.info("startApplication end")
|
|
return true
|
|
}
|
|
|
|
func setUpDeepLinks(application: UIApplication) {
|
|
let router = Router.shared
|
|
let rootNav = rootViewController as! UINavigationController
|
|
|
|
router.map("homepanel/:page", handler: { (params: [String: String]?) -> (Bool) in
|
|
guard let page = params?["page"] else {
|
|
return false
|
|
}
|
|
|
|
assert(Thread.isMainThread, "Opening homepanels requires being invoked on the main thread")
|
|
|
|
switch page {
|
|
case "bookmarks":
|
|
self.browserViewController.openURLInNewTab(HomePanelType.bookmarks.localhostURL, isPrivileged: true)
|
|
case "history":
|
|
self.browserViewController.openURLInNewTab(HomePanelType.history.localhostURL, isPrivileged: true)
|
|
case "new-private-tab":
|
|
self.browserViewController.openBlankNewTab(focusLocationField: false, isPrivate: true)
|
|
default:
|
|
break
|
|
}
|
|
|
|
return true
|
|
})
|
|
|
|
// Route to general settings page like this: "...settings/general"
|
|
router.map("settings/:page", handler: { (params: [String: String]?) -> (Bool) in
|
|
guard let page = params?["page"] else {
|
|
return false
|
|
}
|
|
|
|
assert(Thread.isMainThread, "Opening settings requires being invoked on the main thread")
|
|
|
|
let settingsTableViewController = AppSettingsTableViewController()
|
|
settingsTableViewController.profile = self.profile
|
|
settingsTableViewController.tabManager = self.tabManager
|
|
settingsTableViewController.settingsDelegate = self.browserViewController
|
|
|
|
let controller = SettingsNavigationController(rootViewController: settingsTableViewController)
|
|
controller.popoverDelegate = self.browserViewController
|
|
controller.modalPresentationStyle = UIModalPresentationStyle.formSheet
|
|
|
|
rootNav.present(controller, animated: true, completion: nil)
|
|
|
|
switch page {
|
|
case "newtab":
|
|
let viewController = NewTabChoiceViewController(prefs: self.getProfile(application).prefs)
|
|
controller.pushViewController(viewController, animated: true)
|
|
case "homepage":
|
|
let viewController = HomePageSettingsViewController()
|
|
viewController.profile = self.getProfile(application)
|
|
viewController.tabManager = self.tabManager
|
|
controller.pushViewController(viewController, animated: true)
|
|
case "mailto":
|
|
let viewController = OpenWithSettingsViewController(prefs: self.getProfile(application).prefs)
|
|
controller.pushViewController(viewController, animated: true)
|
|
case "search":
|
|
let viewController = SearchSettingsTableViewController()
|
|
viewController.model = self.getProfile(application).searchEngines
|
|
viewController.profile = self.getProfile(application)
|
|
controller.pushViewController(viewController, animated: true)
|
|
case "clear-private-data":
|
|
let viewController = ClearPrivateDataTableViewController()
|
|
viewController.profile = self.getProfile(application)
|
|
viewController.tabManager = self.tabManager
|
|
controller.pushViewController(viewController, animated: true)
|
|
case "fxa":
|
|
self.browserViewController.presentSignInViewController()
|
|
default:
|
|
break
|
|
}
|
|
|
|
return true
|
|
})
|
|
}
|
|
|
|
func applicationWillTerminate(_ application: UIApplication) {
|
|
// We have only five seconds here, so let's hope this doesn't take too long.
|
|
self.profile?.shutdown()
|
|
|
|
// Allow deinitializers to close our database connections.
|
|
self.profile = nil
|
|
self.tabManager = nil
|
|
self.browserViewController = nil
|
|
self.rootViewController = nil
|
|
}
|
|
|
|
/**
|
|
* We maintain a weak reference to the profile so that we can pause timed
|
|
* syncs when we're backgrounded.
|
|
*
|
|
* The long-lasting ref to the profile lives in BrowserViewController,
|
|
* which we set in application:willFinishLaunchingWithOptions:.
|
|
*
|
|
* If that ever disappears, we won't be able to grab the profile to stop
|
|
* syncing... but in that case the profile's deinit will take care of things.
|
|
*/
|
|
func getProfile(_ application: UIApplication) -> Profile {
|
|
if let profile = self.profile {
|
|
return profile
|
|
}
|
|
let p = BrowserProfile(localName: "profile", syncDelegate: application.syncDelegate)
|
|
self.profile = p
|
|
return p
|
|
}
|
|
|
|
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
|
|
// Override point for customization after application launch.
|
|
var shouldPerformAdditionalDelegateHandling = true
|
|
|
|
adjustIntegration?.triggerApplicationDidFinishLaunchingWithOptions(launchOptions)
|
|
|
|
#if BUDDYBUILD
|
|
print("Setting up BuddyBuild SDK")
|
|
BuddyBuildSDK.setup()
|
|
#endif
|
|
|
|
window!.makeKeyAndVisible()
|
|
|
|
// Now roll logs.
|
|
DispatchQueue.global(qos: DispatchQoS.background.qosClass).async {
|
|
Logger.syncLogger.deleteOldLogsDownToSizeLimit()
|
|
Logger.browserLogger.deleteOldLogsDownToSizeLimit()
|
|
}
|
|
|
|
// If a shortcut was launched, display its information and take the appropriate action
|
|
if let shortcutItem = launchOptions?[UIApplicationLaunchOptionsKey.shortcutItem] as? UIApplicationShortcutItem {
|
|
|
|
QuickActions.sharedInstance.launchedShortcutItem = shortcutItem
|
|
// This will block "performActionForShortcutItem:completionHandler" from being called.
|
|
shouldPerformAdditionalDelegateHandling = false
|
|
}
|
|
|
|
return shouldPerformAdditionalDelegateHandling
|
|
}
|
|
|
|
func application(_ application: UIApplication, open url: URL, sourceApplication: String?, annotation: Any) -> Bool {
|
|
guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false) else {
|
|
return false
|
|
}
|
|
|
|
guard let urlTypes = Bundle.main.object(forInfoDictionaryKey: "CFBundleURLTypes") as? [AnyObject],
|
|
let urlSchemes = urlTypes.first?["CFBundleURLSchemes"] as? [String] else {
|
|
// Something very strange has happened; org.mozilla.Client should be the zeroeth URL type.
|
|
log.error("Custom URL schemes not available for validating")
|
|
return false
|
|
}
|
|
|
|
guard let scheme = components.scheme, urlSchemes.contains(scheme) else {
|
|
log.warning("Cannot handle \(components.scheme ?? "nil") URL scheme")
|
|
return false
|
|
}
|
|
|
|
guard let host = url.host else {
|
|
log.warning("Cannot handle nil URL host")
|
|
return false
|
|
}
|
|
|
|
let query = url.getQuery()
|
|
|
|
switch host {
|
|
case "open-url":
|
|
let url = query["url"]?.unescape() ?? ""
|
|
let isPrivate = NSString(string: query["private"] ?? "false").boolValue
|
|
|
|
let params = LaunchParams(url: URL(string: url), isPrivate: isPrivate)
|
|
|
|
if application.applicationState == .active {
|
|
// If we are active then we can ask the BVC to open the new tab right away.
|
|
// Otherwise, we remember the URL and we open it in applicationDidBecomeActive.
|
|
launchFromURL(params)
|
|
} else {
|
|
openInFirefoxParams = params
|
|
}
|
|
return true
|
|
case "deep-link":
|
|
guard let url = query["url"], Bundle.main.bundleIdentifier == sourceApplication else {
|
|
break
|
|
}
|
|
Router.shared.routeURL(url)
|
|
return true
|
|
case "fxa-signin":
|
|
if AppConstants.MOZ_FXA_DEEP_LINK_FORM_FILL {
|
|
// FxA form filling requires a `signin` query param and host = fxa-signin
|
|
// Ex. firefox://fxa-signin?signin=<token>&someQuery=<data>...
|
|
guard query["signin"] != nil else {
|
|
break
|
|
}
|
|
let fxaParams: FxALaunchParams
|
|
fxaParams = FxALaunchParams(query: query)
|
|
launchFxAFromURL(fxaParams)
|
|
return true
|
|
}
|
|
break
|
|
default: ()
|
|
}
|
|
return false
|
|
}
|
|
|
|
func launchFxAFromURL(_ params: FxALaunchParams) {
|
|
self.browserViewController.presentSignInViewController(params)
|
|
}
|
|
|
|
func launchFromURL(_ params: LaunchParams) {
|
|
let isPrivate = params.isPrivate ?? false
|
|
if let newURL = params.url {
|
|
self.browserViewController.switchToTabForURLOrOpen(newURL, isPrivate: isPrivate, isPrivileged: false)
|
|
} else {
|
|
self.browserViewController.openBlankNewTab(focusLocationField: true, isPrivate: isPrivate)
|
|
}
|
|
|
|
LeanPlumClient.shared.track(event: .openedNewTab, withParameters: ["Source": "External App or Extension" as AnyObject])
|
|
}
|
|
|
|
// We sync in the foreground only, to avoid the possibility of runaway resource usage.
|
|
// Eventually we'll sync in response to notifications.
|
|
func applicationDidBecomeActive(_ application: UIApplication) {
|
|
guard !DebugSettingsBundleOptions.launchIntoEmailComposer else {
|
|
return
|
|
}
|
|
|
|
//
|
|
// We are back in the foreground, so set CleanlyBackgrounded to false so that we can detect that
|
|
// the application was cleanly backgrounded later.
|
|
//
|
|
|
|
let defaults = UserDefaults()
|
|
defaults.set(false, forKey: "ApplicationCleanlyBackgrounded")
|
|
defaults.synchronize()
|
|
|
|
if let profile = self.profile {
|
|
profile.reopen()
|
|
|
|
if profile.prefs.boolForKey(PendingAccountDisconnectedKey) ?? false {
|
|
FxALoginHelper.sharedInstance.applicationDidDisconnect(application)
|
|
}
|
|
|
|
profile.syncManager.applicationDidBecomeActive()
|
|
}
|
|
|
|
// We could load these here, but then we have to futz with the tab counter
|
|
// and making NSURLRequests.
|
|
self.browserViewController.loadQueuedTabs(receivedURLs: self.receivedURLs)
|
|
self.receivedURLs = nil
|
|
application.applicationIconBadgeNumber = 0
|
|
|
|
// handle quick actions is available
|
|
let quickActions = QuickActions.sharedInstance
|
|
if let shortcut = quickActions.launchedShortcutItem {
|
|
// dispatch asynchronously so that BVC is all set up for handling new tabs
|
|
// when we try and open them
|
|
quickActions.handleShortCutItem(shortcut, withBrowserViewController: browserViewController)
|
|
quickActions.launchedShortcutItem = nil
|
|
}
|
|
|
|
// Check if we have a URL from an external app or extension waiting to launch,
|
|
// then launch it on the main thread.
|
|
if let params = openInFirefoxParams {
|
|
openInFirefoxParams = nil
|
|
DispatchQueue.main.async {
|
|
self.launchFromURL(params)
|
|
}
|
|
}
|
|
|
|
UnifiedTelemetry.recordEvent(category: .action, method: .foreground, object: .app)
|
|
}
|
|
|
|
func applicationDidEnterBackground(_ application: UIApplication) {
|
|
//
|
|
// At this point we are happy to mark the app as CleanlyBackgrounded. If a crash happens in background
|
|
// sync then that crash will still be reported. But we won't bother the user with the Restore Tabs
|
|
// dialog. We don't have to because at this point we already saved the tab state properly.
|
|
//
|
|
|
|
let defaults = UserDefaults()
|
|
defaults.set(true, forKey: "ApplicationCleanlyBackgrounded")
|
|
defaults.synchronize()
|
|
|
|
syncOnDidEnterBackground(application: application)
|
|
|
|
UnifiedTelemetry.recordEvent(category: .action, method: .background, object: .app)
|
|
}
|
|
|
|
fileprivate func syncOnDidEnterBackground(application: UIApplication) {
|
|
guard let profile = self.profile else {
|
|
return
|
|
}
|
|
|
|
profile.syncManager.applicationDidEnterBackground()
|
|
|
|
var taskId: UIBackgroundTaskIdentifier = 0
|
|
taskId = application.beginBackgroundTask (expirationHandler: { _ in
|
|
print("Running out of background time, but we have a profile shutdown pending.")
|
|
self.shutdownProfileWhenNotActive(application)
|
|
application.endBackgroundTask(taskId)
|
|
})
|
|
|
|
if profile.hasSyncableAccount() {
|
|
profile.syncManager.syncEverything(why: .backgrounded).uponQueue(DispatchQueue.main) { _ in
|
|
self.shutdownProfileWhenNotActive(application)
|
|
application.endBackgroundTask(taskId)
|
|
}
|
|
} else {
|
|
profile.shutdown()
|
|
application.endBackgroundTask(taskId)
|
|
}
|
|
}
|
|
|
|
fileprivate func shutdownProfileWhenNotActive(_ application: UIApplication) {
|
|
// Only shutdown the profile if we are not in the foreground
|
|
guard application.applicationState != UIApplicationState.active else {
|
|
return
|
|
}
|
|
|
|
profile?.shutdown()
|
|
}
|
|
|
|
func applicationWillEnterForeground(_ application: UIApplication) {
|
|
// The reason we need to call this method here instead of `applicationDidBecomeActive`
|
|
// is that this method is only invoked whenever the application is entering the foreground where as
|
|
// `applicationDidBecomeActive` will get called whenever the Touch ID authentication overlay disappears.
|
|
self.updateAuthenticationInfo()
|
|
}
|
|
|
|
fileprivate func updateAuthenticationInfo() {
|
|
if let authInfo = KeychainWrapper.sharedAppContainerKeychain.authenticationInfo() {
|
|
if !LAContext().canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: nil) {
|
|
authInfo.useTouchID = false
|
|
KeychainWrapper.sharedAppContainerKeychain.setAuthenticationInfo(authInfo)
|
|
}
|
|
}
|
|
}
|
|
|
|
fileprivate func setUpWebServer(_ profile: Profile) {
|
|
let server = WebServer.sharedInstance
|
|
ReaderModeHandlers.register(server, profile: profile)
|
|
ErrorPageHelper.register(server, certStore: profile.certStore)
|
|
AboutHomeHandler.register(server)
|
|
AboutLicenseHandler.register(server)
|
|
SessionRestoreHandler.register(server)
|
|
|
|
// Bug 1223009 was an issue whereby CGDWebserver crashed when moving to a background task
|
|
// catching and handling the error seemed to fix things, but we're not sure why.
|
|
// Either way, not implicitly unwrapping a try is not a great way of doing things
|
|
// so this is better anyway.
|
|
do {
|
|
try server.start()
|
|
} catch let err as NSError {
|
|
print("Error: Unable to start WebServer \(err)")
|
|
}
|
|
}
|
|
|
|
fileprivate func setUserAgent() {
|
|
let firefoxUA = UserAgent.defaultUserAgent()
|
|
|
|
// Set the UA for WKWebView (via defaults), the favicon fetcher, and the image loader.
|
|
// This only needs to be done once per runtime. Note that we use defaults here that are
|
|
// readable from extensions, so they can just use the cached identifier.
|
|
let defaults = UserDefaults(suiteName: AppInfo.sharedContainerIdentifier)!
|
|
defaults.register(defaults: ["UserAgent": firefoxUA])
|
|
|
|
SDWebImageDownloader.shared().setValue(firefoxUA, forHTTPHeaderField: "User-Agent")
|
|
|
|
// Record the user agent for use by search suggestion clients.
|
|
SearchViewController.userAgent = firefoxUA
|
|
|
|
// Some sites will only serve HTML that points to .ico files.
|
|
// The FaviconFetcher is explicitly for getting high-res icons, so use the desktop user agent.
|
|
FaviconFetcher.userAgent = UserAgent.desktopUserAgent()
|
|
}
|
|
|
|
func application(_ application: UIApplication, handleActionWithIdentifier identifier: String?, for notification: UILocalNotification, completionHandler: @escaping () -> Void) {
|
|
if let actionId = identifier {
|
|
if let action = SentTabAction(rawValue: actionId) {
|
|
viewURLInNewTab(notification)
|
|
switch action {
|
|
case .bookmark:
|
|
addBookmark(notification)
|
|
break
|
|
case .readingList:
|
|
addToReadingList(notification)
|
|
break
|
|
default:
|
|
break
|
|
}
|
|
} else {
|
|
print("ERROR: Unknown notification action received")
|
|
}
|
|
} else {
|
|
print("ERROR: Unknown notification received")
|
|
}
|
|
}
|
|
|
|
func application(_ application: UIApplication, didReceive notification: UILocalNotification) {
|
|
viewURLInNewTab(notification)
|
|
}
|
|
|
|
fileprivate func presentEmailComposerWithLogs() {
|
|
if let buildNumber = Bundle.main.object(forInfoDictionaryKey: String(kCFBundleVersionKey)) as? NSString {
|
|
let mailComposeViewController = MFMailComposeViewController()
|
|
mailComposeViewController.mailComposeDelegate = self
|
|
mailComposeViewController.setSubject("Debug Info for iOS client version v\(appVersion) (\(buildNumber))")
|
|
|
|
if DebugSettingsBundleOptions.attachLogsToDebugEmail {
|
|
do {
|
|
let logNamesAndData = try Logger.diskLogFilenamesAndData()
|
|
logNamesAndData.forEach { nameAndData in
|
|
if let data = nameAndData.1 {
|
|
mailComposeViewController.addAttachmentData(data, mimeType: "text/plain", fileName: nameAndData.0)
|
|
}
|
|
}
|
|
} catch _ {
|
|
print("Failed to retrieve logs from device")
|
|
}
|
|
}
|
|
|
|
if DebugSettingsBundleOptions.attachTabStateToDebugEmail {
|
|
if let tabStateDebugData = TabManager.tabRestorationDebugInfo().data(using: String.Encoding.utf8) {
|
|
mailComposeViewController.addAttachmentData(tabStateDebugData, mimeType: "text/plain", fileName: "tabState.txt")
|
|
}
|
|
|
|
if let tabStateData = TabManager.tabArchiveData() {
|
|
mailComposeViewController.addAttachmentData(tabStateData as Data, mimeType: "application/octet-stream", fileName: "tabsState.archive")
|
|
}
|
|
}
|
|
|
|
self.window?.rootViewController?.present(mailComposeViewController, animated: true, completion: nil)
|
|
}
|
|
}
|
|
|
|
func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([Any]?) -> Void) -> Bool {
|
|
|
|
// If the `NSUserActivity` has a `webpageURL`, it is either a deep link or an old history item
|
|
// reached via a "Spotlight" search before we began indexing visited pages via CoreSpotlight.
|
|
if let url = userActivity.webpageURL {
|
|
let query = url.getQuery()
|
|
|
|
// Check for fxa sign-in code and launch the login screen directly
|
|
if query["signin"] != nil {
|
|
browserViewController.launchFxAFromDeeplinkURL(url)
|
|
return true
|
|
}
|
|
|
|
// Per Adjust documenation, https://docs.adjust.com/en/universal-links/#running-campaigns-through-universal-links,
|
|
// it is recommended that links contain the `deep_link` query parameter. This link will also
|
|
// be url encoded.
|
|
if let deepLink = query["deep_link"]?.removingPercentEncoding, let url = URL(string: deepLink) {
|
|
browserViewController.switchToTabForURLOrOpen(url, isPrivileged: true)
|
|
return true
|
|
}
|
|
|
|
browserViewController.switchToTabForURLOrOpen(url, isPrivileged: true)
|
|
return true
|
|
}
|
|
|
|
// Otherwise, check if the `NSUserActivity` is a CoreSpotlight item and switch to its tab or
|
|
// open a new one.
|
|
if userActivity.activityType == CSSearchableItemActionType {
|
|
if let userInfo = userActivity.userInfo,
|
|
let urlString = userInfo[CSSearchableItemActivityIdentifier] as? String,
|
|
let url = URL(string: urlString) {
|
|
browserViewController.switchToTabForURLOrOpen(url, isPrivileged: true)
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
fileprivate func viewURLInNewTab(_ notification: UILocalNotification) {
|
|
if let alertURL = notification.userInfo?[TabSendURLKey] as? String {
|
|
if let urlToOpen = URL(string: alertURL) {
|
|
browserViewController.openURLInNewTab(urlToOpen, isPrivileged: true)
|
|
}
|
|
}
|
|
}
|
|
|
|
fileprivate func addBookmark(_ notification: UILocalNotification) {
|
|
if let alertURL = notification.userInfo?[TabSendURLKey] as? String,
|
|
let title = notification.userInfo?[TabSendTitleKey] as? String {
|
|
let tabState = TabState(isPrivate: false, desktopSite: false, isBookmarked: false, url: URL(string: alertURL), title: title, favicon: nil)
|
|
browserViewController.addBookmark(tabState)
|
|
|
|
let userData = [QuickActions.TabURLKey: alertURL,
|
|
QuickActions.TabTitleKey: title]
|
|
QuickActions.sharedInstance.addDynamicApplicationShortcutItemOfType(.openLastBookmark, withUserData: userData, toApplication: UIApplication.shared)
|
|
}
|
|
}
|
|
|
|
fileprivate func addToReadingList(_ notification: UILocalNotification) {
|
|
if let alertURL = notification.userInfo?[TabSendURLKey] as? String,
|
|
let title = notification.userInfo?[TabSendTitleKey] as? String {
|
|
if let urlToOpen = URL(string: alertURL) {
|
|
NotificationCenter.default.post(name: NSNotification.Name.FSReadingListAddReadingListItem, object: self, userInfo: ["URL": urlToOpen, "Title": title])
|
|
}
|
|
}
|
|
}
|
|
|
|
func application(_ application: UIApplication, performActionFor shortcutItem: UIApplicationShortcutItem, completionHandler: @escaping (Bool) -> Void) {
|
|
let handledShortCutItem = QuickActions.sharedInstance.handleShortCutItem(shortcutItem, withBrowserViewController: browserViewController)
|
|
|
|
completionHandler(handledShortCutItem)
|
|
}
|
|
}
|
|
|
|
// MARK: - Root View Controller Animations
|
|
extension AppDelegate: UINavigationControllerDelegate {
|
|
func navigationController(_ navigationController: UINavigationController, animationControllerFor operation: UINavigationControllerOperation, from fromVC: UIViewController, to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
|
|
if operation == UINavigationControllerOperation.push {
|
|
return BrowserToTrayAnimator()
|
|
} else if operation == UINavigationControllerOperation.pop {
|
|
return TrayToBrowserAnimator()
|
|
} else {
|
|
return nil
|
|
}
|
|
}
|
|
}
|
|
|
|
extension AppDelegate: TabManagerStateDelegate {
|
|
func tabManagerWillStoreTabs(_ tabs: [Tab]) {
|
|
// It is possible that not all tabs have loaded yet, so we filter out tabs with a nil URL.
|
|
let storedTabs: [RemoteTab] = tabs.flatMap( Tab.toTab )
|
|
|
|
// Don't insert into the DB immediately. We tend to contend with more important
|
|
// work like querying for top sites.
|
|
let queue = DispatchQueue.global(qos: DispatchQoS.background.qosClass)
|
|
queue.asyncAfter(deadline: DispatchTime.now() + Double(Int64(ProfileRemoteTabsSyncDelay * Double(NSEC_PER_MSEC))) / Double(NSEC_PER_SEC)) {
|
|
self.profile?.storeTabs(storedTabs)
|
|
}
|
|
}
|
|
}
|
|
|
|
extension AppDelegate: MFMailComposeViewControllerDelegate {
|
|
func mailComposeController(_ controller: MFMailComposeViewController, didFinishWith result: MFMailComposeResult, error: Error?) {
|
|
// Dismiss the view controller and start the app up
|
|
controller.dismiss(animated: true, completion: nil)
|
|
startApplication(application!, withLaunchOptions: self.launchOptions)
|
|
}
|
|
}
|
|
|
|
extension AppDelegate {
|
|
func application(_ application: UIApplication, didRegister notificationSettings: UIUserNotificationSettings) {
|
|
FxALoginHelper.sharedInstance.application(application, didRegisterUserNotificationSettings: notificationSettings)
|
|
}
|
|
}
|
|
|
|
extension AppDelegate {
|
|
func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
|
|
FxALoginHelper.sharedInstance.apnsRegisterDidSucceed(deviceToken)
|
|
}
|
|
|
|
func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) {
|
|
print("failed to register. \(error)")
|
|
FxALoginHelper.sharedInstance.apnsRegisterDidFail()
|
|
}
|
|
|
|
func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable: Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
|
|
if Logger.logPII && log.isEnabledFor(level: .info) {
|
|
NSLog("APNS NOTIFICATION \(userInfo)")
|
|
}
|
|
|
|
// At this point, we know that NotificationService has been run.
|
|
// We get to this point if the notification was received while the app was in the foreground
|
|
// OR the app was backgrounded and now the user has tapped on the notification.
|
|
// Either way, if this method is being run, then the app is foregrounded.
|
|
|
|
// Either way, we should zero the badge number.
|
|
application.applicationIconBadgeNumber = 0
|
|
|
|
guard let profile = self.profile else {
|
|
return completionHandler(.noData)
|
|
}
|
|
|
|
// NotificationService will have decrypted the push message, and done some syncing
|
|
// activity. If the `client` collection was synced, and there are `displayURI` commands (i.e. sent tabs)
|
|
// NotificationService will have collected them for us in the userInfo.
|
|
if let serializedTabs = userInfo["sentTabs"] as? [[String: String]] {
|
|
// Let's go ahead and open those.
|
|
let receivedURLs = serializedTabs.flatMap { item -> URL? in
|
|
guard let tabURL = item["url"] else {
|
|
return nil
|
|
}
|
|
return URL(string: tabURL)
|
|
}
|
|
|
|
if receivedURLs.count > 0 {
|
|
// Remember which URLs we received so we can filter them out later when
|
|
// loading the queued tabs.
|
|
self.receivedURLs = receivedURLs
|
|
|
|
// If we're in the foreground, load the queued tabs now.
|
|
if application.applicationState == UIApplicationState.active {
|
|
DispatchQueue.main.async {
|
|
self.browserViewController.loadQueuedTabs(receivedURLs: self.receivedURLs)
|
|
self.receivedURLs = nil
|
|
}
|
|
}
|
|
|
|
return completionHandler(.newData)
|
|
}
|
|
}
|
|
|
|
// By now, we've dealt with any sent tab notifications.
|
|
//
|
|
// The only thing left to do now is to perform actions that can only be performed
|
|
// while the app is foregrounded.
|
|
//
|
|
// Use the push message handler to re-parse the message,
|
|
// this time with a BrowserProfile and processing the return
|
|
// differently than in NotificationService.
|
|
let handler = FxAPushMessageHandler(with: profile)
|
|
handler.handle(userInfo: userInfo).upon { res in
|
|
if let message = res.successValue {
|
|
switch message {
|
|
case .accountVerified:
|
|
_ = handler.postVerification()
|
|
case .thisDeviceDisconnected:
|
|
FxALoginHelper.sharedInstance.applicationDidDisconnect(application)
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
|
|
completionHandler(res.isSuccess ? .newData : .failed)
|
|
}
|
|
}
|
|
|
|
func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable: Any]) {
|
|
let completionHandler: (UIBackgroundFetchResult) -> Void = { _ in }
|
|
self.application(application, didReceiveRemoteNotification: userInfo, fetchCompletionHandler: completionHandler)
|
|
}
|
|
}
|
|
|
|
struct FxALaunchParams {
|
|
var query: [String: String]
|
|
}
|
|
|
|
struct LaunchParams {
|
|
let url: URL?
|
|
let isPrivate: Bool?
|
|
}
|
|
|
|
extension UIApplication {
|
|
var syncDelegate: SyncDelegate {
|
|
return AppSyncDelegate(app: self)
|
|
}
|
|
|
|
static var isInPrivateMode: Bool {
|
|
let appDelegate = UIApplication.shared.delegate as? AppDelegate
|
|
return appDelegate?.browserViewController.tabManager.selectedTab?.isPrivate ?? false
|
|
}
|
|
}
|
|
|
|
class AppSyncDelegate: SyncDelegate {
|
|
let app: UIApplication
|
|
|
|
init(app: UIApplication) {
|
|
self.app = app
|
|
}
|
|
|
|
open func displaySentTab(for url: URL, title: String, from deviceName: String?) {
|
|
DispatchQueue.main.sync {
|
|
if let appDelegate = app.delegate as? AppDelegate, app.applicationState == .active {
|
|
appDelegate.browserViewController.switchToTabForURLOrOpen(url, isPrivileged: false)
|
|
return
|
|
}
|
|
|
|
// check to see what the current notification settings are and only try and send a notification if
|
|
// the user has agreed to them
|
|
if let currentSettings = app.currentUserNotificationSettings {
|
|
if currentSettings.types.rawValue & UIUserNotificationType.alert.rawValue != 0 {
|
|
if Logger.logPII {
|
|
log.info("Displaying notification for URL \(url.absoluteString)")
|
|
}
|
|
|
|
let notification = UILocalNotification()
|
|
notification.fireDate = Date()
|
|
notification.timeZone = NSTimeZone.default
|
|
let title: String
|
|
if let deviceName = deviceName {
|
|
title = String(format: Strings.SentTab_TabArrivingNotification_WithDevice_title, deviceName)
|
|
} else {
|
|
title = Strings.SentTab_TabArrivingNotification_NoDevice_title
|
|
}
|
|
notification.alertTitle = title
|
|
notification.alertBody = url.absoluteDisplayExternalString
|
|
notification.userInfo = [TabSendURLKey: url.absoluteString, TabSendTitleKey: title]
|
|
notification.alertAction = nil
|
|
|
|
// Restore this when we fix Bug 1364420.
|
|
// notification.category = TabSendCategory
|
|
|
|
app.presentLocalNotificationNow(notification)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|