mirror of
https://repo.dactyloidae.xyz/Dactyloidae/UXP.git
synced 2026-09-06 15:58: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/Application/AdjustIntegration.swift
Normal file
195
mobile/ios/Client/Application/AdjustIntegration.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
|
||||
import AdjustSdk
|
||||
|
||||
private let AdjustIntegrationErrorDomain = "org.mozilla.ios.Firefox.AdjustIntegrationErrorDomain"
|
||||
|
||||
private let AdjustAttributionFileName = "AdjustAttribution.json"
|
||||
|
||||
private let AdjustAppTokenKey = "AdjustAppToken"
|
||||
private let AdjustEnvironmentKey = "AdjustEnvironment"
|
||||
|
||||
private let AdjustSandboxEnvironment = "sandbox"
|
||||
private let AdjustProductionEnvironment = "production"
|
||||
|
||||
private enum AdjustEnvironment: String {
|
||||
case Sandbox = "sandbox"
|
||||
case Production = "production"
|
||||
}
|
||||
|
||||
private struct AdjustSettings {
|
||||
var appToken: String
|
||||
var environment: AdjustEnvironment
|
||||
}
|
||||
|
||||
/// Simple (singleton) object to contain all code related to Adjust. The idea is to capture all logic
|
||||
/// here so that we have one single place where we can see what Adjust is doing. Ideally you only call
|
||||
/// functions in this object from other parts of the application.
|
||||
|
||||
class AdjustIntegration: NSObject {
|
||||
let profile: Profile
|
||||
|
||||
init(profile: Profile) {
|
||||
self.profile = profile
|
||||
}
|
||||
|
||||
/// Return an ADJConfig object if Adjust has been enabled. It is determined from the values in
|
||||
/// the Info.plist file if Adjust should be enabled, and if so, what its application token and
|
||||
/// environment are. If those keys are either missing or empty in the Info.plist then it is
|
||||
/// assumed that Adjust is not enabled for this build.
|
||||
|
||||
fileprivate func getConfig() -> ADJConfig? {
|
||||
guard let settings = getSettings() else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let config = ADJConfig(appToken: settings.appToken, environment: settings.environment.rawValue)
|
||||
if settings.environment == .Sandbox {
|
||||
config?.logLevel = ADJLogLevelDebug
|
||||
}
|
||||
config?.delegate = self
|
||||
return config
|
||||
}
|
||||
|
||||
/// Returns the Adjust settings from our Info.plist. If the settings are missing or invalid, such as an unknown
|
||||
/// environment, then it will return nil.
|
||||
|
||||
fileprivate func getSettings() -> AdjustSettings? {
|
||||
let bundle = Bundle.main
|
||||
guard let adjustAppToken = bundle.object(forInfoDictionaryKey: AdjustAppTokenKey) as? String,
|
||||
let adjustEnvironment = bundle.object(forInfoDictionaryKey: AdjustEnvironmentKey) as? String else {
|
||||
return nil
|
||||
}
|
||||
guard !adjustAppToken.isEmpty && !adjustEnvironment.isEmpty else {
|
||||
return nil
|
||||
}
|
||||
guard let environment = AdjustEnvironment(rawValue: adjustEnvironment) else {
|
||||
Logger.browserLogger.error("Adjust - Invalid environment provided: \(adjustEnvironment)")
|
||||
return nil
|
||||
}
|
||||
return AdjustSettings(appToken: adjustAppToken, environment: environment)
|
||||
}
|
||||
|
||||
/// Returns true if the attribution file is present.
|
||||
|
||||
fileprivate func hasAttribution() throws -> Bool {
|
||||
return FileManager.default.fileExists(atPath: try getAttributionPath())
|
||||
}
|
||||
|
||||
/// Save an `ADJAttribution` instance to a JSON file. Throws an error if the file could not be written. The file
|
||||
/// written is a JSON file with a single dictionary in it. We add one extra item to it that contains the current
|
||||
/// timestamp in seconds since the UNIX epoch.
|
||||
|
||||
fileprivate func saveAttribution(_ attribution: ADJAttribution) throws {
|
||||
if let attributionDictionary = attribution.dictionary() {
|
||||
let dictionary = NSMutableDictionary(dictionary: attributionDictionary)
|
||||
dictionary["_timestamp"] = NSNumber(value: Int64(Date().timeIntervalSince1970) as Int64)
|
||||
let data = try JSONSerialization.data(withJSONObject: dictionary, options: [JSONSerialization.WritingOptions.prettyPrinted])
|
||||
try data.write(to: URL(fileURLWithPath: try getAttributionPath()), options: [])
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the path to the `AdjustAttribution.json` file. Throws an `NSError` if we could not build the path.
|
||||
|
||||
fileprivate func getAttributionPath() throws -> String {
|
||||
guard let url = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else {
|
||||
throw NSError(domain: AdjustIntegrationErrorDomain, code: -1,
|
||||
userInfo: [NSLocalizedDescriptionKey: "Could not build \(AdjustAttributionFileName) path"])
|
||||
}
|
||||
|
||||
return url.appendingPathComponent(AdjustAttributionFileName).path
|
||||
}
|
||||
|
||||
/// Return true if Adjust should be enabled. If the user has disabled the Send Anonymous Usage Data then we immediately
|
||||
/// return false. Otherwise we only do one ping, which means we only enable it if we have not seen the attributiond
|
||||
/// data yet.
|
||||
|
||||
fileprivate func shouldEnable() throws -> Bool {
|
||||
if profile.prefs.boolForKey(AppConstants.PrefSendUsageData) ?? true {
|
||||
return true
|
||||
}
|
||||
return try hasAttribution() == false
|
||||
}
|
||||
|
||||
/// Return true if retention (session) tracking should be enabled. This follows the Send Anonymous Usage Data
|
||||
/// setting.
|
||||
|
||||
fileprivate func shouldTrackRetention() -> Bool {
|
||||
return profile.prefs.boolForKey(AppConstants.PrefSendUsageData) ?? true
|
||||
}
|
||||
}
|
||||
|
||||
extension AdjustIntegration: AdjustDelegate {
|
||||
/// This is called as part of `UIApplication.didFinishLaunchingWithOptions()`. We always initialize the
|
||||
/// Adjust SDK. We always let it send the initial attribution ping. Session tracking is only enabled if
|
||||
/// the Send Anonymous Usage Data setting is turned on.
|
||||
|
||||
func triggerApplicationDidFinishLaunchingWithOptions(_ launchOptions: [AnyHashable: Any]?) {
|
||||
do {
|
||||
if let config = getConfig() {
|
||||
// Always initialize Adjust - otherwise we cannot enable/disable it later. Their SDK must be
|
||||
// initialized through appDidFinishLaunching otherwise it will be in a bad state.
|
||||
Adjust.appDidLaunch(config)
|
||||
|
||||
// Disable it right now if we have the attribution and if the user has disabled session tracking. If
|
||||
// we do not have attribution yet then we wait until it comes in and at that point make the decision
|
||||
// to disable Adjust again.
|
||||
if try hasAttribution() {
|
||||
if !shouldTrackRetention() {
|
||||
Logger.browserLogger.info("Adjust - Disabling because sending of usage data is not allowed")
|
||||
Adjust.setEnabled(false)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Logger.browserLogger.info("Adjust - Skipping because no or invalid config found")
|
||||
}
|
||||
} catch let error {
|
||||
Logger.browserLogger.error("Adjust - Disabling because we failed to configure: \(error)")
|
||||
Adjust.setEnabled(false)
|
||||
}
|
||||
}
|
||||
|
||||
/// This is called when Adjust has figured out the attribution. It will call us with a summary
|
||||
/// of all the things it knows. Like the campaign ID. We simply save this to a local file so
|
||||
/// that we know we have done a single attribution ping to Adjust.
|
||||
///
|
||||
/// Here we also disable Adjust based on the Send Anonymous Usage Data setting.
|
||||
|
||||
func adjustAttributionChanged(_ attribution: ADJAttribution!) {
|
||||
do {
|
||||
Logger.browserLogger.info("Adjust - Saving attribution info to disk")
|
||||
try saveAttribution(attribution)
|
||||
} catch let error {
|
||||
Logger.browserLogger.error("Adjust - Failed to save attribution: \(error)")
|
||||
}
|
||||
// Keep Adjust enabled only if the user has allowed this
|
||||
if shouldTrackRetention() {
|
||||
Logger.browserLogger.info("Adjust - Enabling because user allows anonymous usage data collection")
|
||||
Adjust.setEnabled(true)
|
||||
} else {
|
||||
Logger.browserLogger.info("Adjust - Disabling because user does not allow anonymous usage data collection")
|
||||
Adjust.setEnabled(false)
|
||||
}
|
||||
}
|
||||
|
||||
/// This is called from the Settings screen. The settings screen will remember the choice in the
|
||||
/// profile and then use this method to disable or enable Adjust.
|
||||
|
||||
static func setEnabled(_ enabled: Bool) {
|
||||
Adjust.setEnabled(enabled)
|
||||
}
|
||||
|
||||
/// Store the deeplink url from Adjust SDK. Per Adjust documentation, any interstitial view launched could interfere
|
||||
/// with launching the deeplink. We let the interstial view decide what to do with deeplink.
|
||||
/// Ref: https://github.com/adjust/ios_sdk#deferred-deep-linking-scenario
|
||||
|
||||
func adjustDeeplinkResponse(_ deeplink: URL!) -> Bool {
|
||||
profile.prefs.setString("\(deeplink)", forKey: "AdjustDeeplinkKey")
|
||||
return true
|
||||
}
|
||||
|
||||
}
|
||||
884
mobile/ios/Client/Application/AppDelegate.swift
Normal file
884
mobile/ios/Client/Application/AppDelegate.swift
Normal file
|
|
@ -0,0 +1,884 @@
|
|||
/* 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
13
mobile/ios/Client/Application/Crasher.h
Normal file
13
mobile/ios/Client/Application/Crasher.h
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
//
|
||||
// Crasher.h
|
||||
// Client
|
||||
//
|
||||
// Created by Steph Leroux on 2015-10-08.
|
||||
// Copyright © 2015 Mozilla. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
@interface Crasher : NSObject
|
||||
|
||||
@end
|
||||
13
mobile/ios/Client/Application/Crasher.m
Normal file
13
mobile/ios/Client/Application/Crasher.m
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
//
|
||||
// Crasher.m
|
||||
// Client
|
||||
//
|
||||
// Created by Steph Leroux on 2015-10-08.
|
||||
// Copyright © 2015 Mozilla. All rights reserved.
|
||||
//
|
||||
|
||||
#import "Crasher.h"
|
||||
|
||||
@implementation Crasher
|
||||
|
||||
@end
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
/* 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 MessageUI
|
||||
|
||||
struct DebugSettingsBundleOptions {
|
||||
|
||||
/// Don't restore tabs on app launch
|
||||
static var skipSessionRestore: Bool {
|
||||
return UserDefaults.standard.bool(forKey: "SettingsBundleSkipSessionRestore")
|
||||
}
|
||||
|
||||
/// Disable the local web server we use for restoration, error pages, etc
|
||||
static var disableLocalWebServer: Bool {
|
||||
return UserDefaults.standard.bool(forKey: "SettingsBundleDisableLocalWebServer")
|
||||
}
|
||||
|
||||
/// When enabled, the app launch will be replaced with the mail compose view appearing with the device
|
||||
/// logs pre-attached. When the mail is sent, the app continues launching normally.
|
||||
static var launchIntoEmailComposer: Bool {
|
||||
return ((attachTabStateToDebugEmail || attachLogsToDebugEmail) && MFMailComposeViewController.canSendMail())
|
||||
}
|
||||
|
||||
/// When enabled, the email composer will have the tab state attached.
|
||||
static var attachTabStateToDebugEmail: Bool {
|
||||
return UserDefaults.standard.bool(forKey: "SettingsBundleEmailTabState")
|
||||
}
|
||||
|
||||
/// When enabled, the email composer will have the application logs attached.
|
||||
static var attachLogsToDebugEmail: Bool {
|
||||
return UserDefaults.standard.bool(forKey: "SettingsBundleEmailLogsOnLaunch")
|
||||
}
|
||||
}
|
||||
34
mobile/ios/Client/Application/LaunchScreen.xib
Normal file
34
mobile/ios/Client/Application/LaunchScreen.xib
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="7706" systemVersion="14E26a" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES">
|
||||
<dependencies>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="7703"/>
|
||||
</dependencies>
|
||||
<objects>
|
||||
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
|
||||
<view contentMode="scaleToFill" id="iN0-l3-epB">
|
||||
<rect key="frame" x="0.0" y="0.0" width="480" height="480"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<imageView userInteractionEnabled="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="splash" translatesAutoresizingMaskIntoConstraints="NO" id="8jg-1f-0sB">
|
||||
<rect key="frame" x="175" y="175" width="130" height="130"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" constant="130" id="V8U-lj-86g"/>
|
||||
<constraint firstAttribute="height" constant="130" id="eV3-E9-QZk"/>
|
||||
</constraints>
|
||||
</imageView>
|
||||
</subviews>
|
||||
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>
|
||||
<constraints>
|
||||
<constraint firstItem="8jg-1f-0sB" firstAttribute="centerY" secondItem="iN0-l3-epB" secondAttribute="centerY" id="ITK-hU-DwR"/>
|
||||
<constraint firstItem="8jg-1f-0sB" firstAttribute="centerX" secondItem="iN0-l3-epB" secondAttribute="centerX" id="grm-jt-xY2"/>
|
||||
</constraints>
|
||||
<nil key="simulatedStatusBarMetrics"/>
|
||||
<freeformSimulatedSizeMetrics key="simulatedDestinationMetrics"/>
|
||||
<point key="canvasLocation" x="404" y="445"/>
|
||||
</view>
|
||||
</objects>
|
||||
<resources>
|
||||
<image name="splash" width="400" height="390"/>
|
||||
</resources>
|
||||
</document>
|
||||
320
mobile/ios/Client/Application/LeanplumIntegration.swift
Normal file
320
mobile/ios/Client/Application/LeanplumIntegration.swift
Normal file
|
|
@ -0,0 +1,320 @@
|
|||
/* 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 AdSupport
|
||||
import Shared
|
||||
import Leanplum
|
||||
|
||||
private let LPAppIdKey = "LeanplumAppId"
|
||||
private let LPProductionKeyKey = "LeanplumProductionKey"
|
||||
private let LPDevelopmentKeyKey = "LeanplumDevelopmentKey"
|
||||
private let AppRequestedUserNotificationsPrefKey = "applicationDidRequestUserNotificationPermissionPrefKey"
|
||||
|
||||
// FxA Custom Leanplum message template for A/B testing push notifications.
|
||||
private struct LPMessage {
|
||||
static let FxAPrePush = "FxA Prepush v1"
|
||||
static let ArgAcceptAction = "Accept action"
|
||||
static let ArgCancelAction = "Cancel action"
|
||||
static let ArgTitleText = "Title.Text"
|
||||
static let ArgTitleColor = "Title.Color"
|
||||
static let ArgMessageText = "Message.Text"
|
||||
static let ArgMessageColor = "Message.Color"
|
||||
static let ArgAcceptButtonText = "Accept button.Text"
|
||||
static let ArgCancelButtonText = "Cancel button.Text"
|
||||
static let ArgCancelButtonTextColor = "Cancel button.Text color"
|
||||
// These defaults are overridden though Leanplum webUI
|
||||
static let DefaultAskToAskTitle = NSLocalizedString("Firefox Sync Requires Push", comment: "Default push to ask title")
|
||||
static let DefaultAskToAskMessage = NSLocalizedString("Firefox will stay in sync faster with Push Notifications enabled.", comment: "Default push to ask message")
|
||||
static let DefaultOkButtonText = NSLocalizedString("Enable Push", comment: "Default push alert ok button text")
|
||||
static let DefaultLaterButtonText = NSLocalizedString("Don't Enable", comment: "Default push alert cancel button text")
|
||||
}
|
||||
|
||||
private let log = Logger.browserLogger
|
||||
|
||||
enum LPEvent: String {
|
||||
case firstRun = "E_First_Run"
|
||||
case secondRun = "E_Second_Run"
|
||||
case openedApp = "E_Opened_App"
|
||||
case dismissedOnboarding = "E_Dismissed_Onboarding"
|
||||
case openedLogins = "Opened Login Manager"
|
||||
case openedBookmark = "E_Opened_Bookmark"
|
||||
case openedNewTab = "E_Opened_New_Tab"
|
||||
case openedPocketStory = "E_Opened_Pocket_Story"
|
||||
case interactWithURLBar = "E_Interact_With_Search_URL_Area"
|
||||
case savedBookmark = "E_Saved_Bookmark"
|
||||
case openedTelephoneLink = "Opened Telephone Link"
|
||||
case openedMailtoLink = "E_Opened_Mailto_Link"
|
||||
case saveImage = "E_Download_Media_Saved_Image"
|
||||
case savedLoginAndPassword = "E_Saved_Login_And_Password"
|
||||
case clearPrivateData = "E_Cleared_Private_Data"
|
||||
case downloadedFocus = "E_User_Downloaded_Focus"
|
||||
case downloadedPocket = "E_User_Downloaded_Pocket"
|
||||
case userSharedWebpage = "E_User_Tapped_Share_Button"
|
||||
case signsInFxa = "E_User_Signed_In_To_FxA"
|
||||
case useReaderView = "E_User_Used_Reader_View"
|
||||
case trackingProtectionSettings = "E_Tracking_Protection_Settings_Changed"
|
||||
}
|
||||
|
||||
struct LPAttributeKey {
|
||||
static let focusInstalled = "Focus Installed"
|
||||
static let klarInstalled = "Klar Installed"
|
||||
static let signedInSync = "Signed In Sync"
|
||||
static let mailtoIsDefault = "Mailto Is Default"
|
||||
static let pocketInstalled = "Pocket Installed"
|
||||
static let telemetryOptIn = "Telemetry Opt In"
|
||||
}
|
||||
|
||||
struct MozillaAppSchemes {
|
||||
static let focus = "firefox-focus"
|
||||
static let focusDE = "firefox-klar"
|
||||
static let pocket = "pocket"
|
||||
}
|
||||
|
||||
private let supportedLocales = ["en_US", "de_DE", "en_GB", "en_CA", "en_AU", "zh_TW", "en_HK", "en_SG",
|
||||
"fr_FR", "it_IT", "id_ID", "id_ID", "pt_BR", "pl_PL", "ru_RU", "es_ES", "es_MX"]
|
||||
|
||||
private struct LPSettings {
|
||||
var appId: String
|
||||
var developmentKey: String
|
||||
var productionKey: String
|
||||
}
|
||||
|
||||
class LeanPlumClient {
|
||||
static let shared = LeanPlumClient()
|
||||
|
||||
// Setup
|
||||
private weak var profile: Profile?
|
||||
private var prefs: Prefs? { return profile?.prefs }
|
||||
private var enabled: Bool = true
|
||||
|
||||
// This defines an external Leanplum varible to enable/disable FxA prepush dialogs.
|
||||
// The primary result is having a feature flag controlled by Leanplum, and falling back
|
||||
// to prompting with native push permissions.
|
||||
private var useFxAPrePush: LPVar = LPVar.define("useFxAPrePush", with: false)
|
||||
|
||||
private func isPrivateMode() -> Bool {
|
||||
// Need to be run on main thread since isInPrivateMode requires to be on the main thread.
|
||||
assert(Thread.isMainThread)
|
||||
return UIApplication.isInPrivateMode
|
||||
}
|
||||
|
||||
func isLPEnabled() -> Bool {
|
||||
return enabled && Leanplum.hasStarted()
|
||||
}
|
||||
|
||||
static func shouldEnable(profile: Profile) -> Bool {
|
||||
return AppConstants.MOZ_ENABLE_LEANPLUM && (profile.prefs.boolForKey(AppConstants.PrefSendUsageData) ?? true)
|
||||
}
|
||||
|
||||
func setup(profile: Profile) {
|
||||
self.profile = profile
|
||||
}
|
||||
|
||||
fileprivate func start() {
|
||||
guard let settings = getSettings(), supportedLocales.contains(Locale.current.identifier), !Leanplum.hasStarted() else {
|
||||
enabled = false
|
||||
log.error("LeanplumIntegration - Could not be started")
|
||||
return
|
||||
}
|
||||
|
||||
if UIDevice.current.name.contains("MozMMADev") {
|
||||
log.info("LeanplumIntegration - Setting up for Development")
|
||||
Leanplum.setDeviceId(UIDevice.current.identifierForVendor?.uuidString)
|
||||
Leanplum.setAppId(settings.appId, withDevelopmentKey: settings.developmentKey)
|
||||
} else {
|
||||
log.info("LeanplumIntegration - Setting up for Production")
|
||||
Leanplum.setAppId(settings.appId, withProductionKey: settings.productionKey)
|
||||
}
|
||||
|
||||
Leanplum.syncResourcesAsync(true)
|
||||
|
||||
let attributes: [AnyHashable: Any] = [
|
||||
LPAttributeKey.mailtoIsDefault: mailtoIsDefault(),
|
||||
LPAttributeKey.focusInstalled: focusInstalled(),
|
||||
LPAttributeKey.klarInstalled: klarInstalled(),
|
||||
LPAttributeKey.pocketInstalled: pocketInstalled(),
|
||||
LPAttributeKey.signedInSync: profile?.hasAccount() ?? false
|
||||
]
|
||||
|
||||
self.setupCustomTemplates()
|
||||
|
||||
Leanplum.start(withUserId: nil, userAttributes: attributes, responseHandler: { _ in
|
||||
self.track(event: .openedApp)
|
||||
|
||||
// We need to check if the app is a clean install to use for
|
||||
// preventing the What's New URL from appearing.
|
||||
if self.prefs?.intForKey(IntroViewControllerSeenProfileKey) == nil {
|
||||
self.prefs?.setString(AppInfo.appVersion, forKey: LatestAppVersionProfileKey)
|
||||
self.track(event: .firstRun)
|
||||
} else if self.prefs?.boolForKey("SecondRun") == nil {
|
||||
self.prefs?.setBool(true, forKey: "SecondRun")
|
||||
self.track(event: .secondRun)
|
||||
}
|
||||
|
||||
self.checkIfAppWasInstalled(key: PrefsKeys.HasFocusInstalled, isAppInstalled: self.focusInstalled(), lpEvent: .downloadedFocus)
|
||||
self.checkIfAppWasInstalled(key: PrefsKeys.HasPocketInstalled, isAppInstalled: self.pocketInstalled(), lpEvent: .downloadedPocket)
|
||||
})
|
||||
}
|
||||
|
||||
// Events
|
||||
func track(event: LPEvent, withParameters parameters: [String: AnyObject]? = nil) {
|
||||
guard isLPEnabled() else {
|
||||
return
|
||||
}
|
||||
ensureMainThread {
|
||||
guard !self.isPrivateMode() else {
|
||||
return
|
||||
}
|
||||
if let params = parameters {
|
||||
Leanplum.track(event.rawValue, withParameters: params)
|
||||
} else {
|
||||
Leanplum.track(event.rawValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func set(attributes: [AnyHashable: Any]) {
|
||||
guard isLPEnabled() else {
|
||||
return
|
||||
}
|
||||
ensureMainThread {
|
||||
if !self.isPrivateMode() {
|
||||
Leanplum.setUserAttributes(attributes)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func set(enabled: Bool) {
|
||||
// Setting up Test Mode stops sending things to server.
|
||||
if enabled { start() }
|
||||
self.enabled = enabled
|
||||
Leanplum.setTestModeEnabled(!enabled)
|
||||
}
|
||||
|
||||
func isFxAPrePushEnabled() -> Bool {
|
||||
return AppConstants.MOZ_FXA_LEANPLUM_AB_PUSH_TEST && useFxAPrePush.boolValue()
|
||||
}
|
||||
|
||||
/*
|
||||
This is used to determine if an app was installed after firefox was installed
|
||||
*/
|
||||
private func checkIfAppWasInstalled(key: String, isAppInstalled: Bool, lpEvent: LPEvent) {
|
||||
// if no key is present. create one and set it.
|
||||
// if the app is already installed then the flag will set true and the second block will never run
|
||||
if self.prefs?.boolForKey(key) == nil {
|
||||
self.prefs?.setBool(isAppInstalled, forKey: key)
|
||||
}
|
||||
// on a subsquent launch if the app is installed and the key is false then switch the flag to true
|
||||
if !(self.prefs?.boolForKey(key) ?? false), isAppInstalled {
|
||||
self.prefs?.setBool(isAppInstalled, forKey: key)
|
||||
self.track(event: lpEvent)
|
||||
}
|
||||
}
|
||||
|
||||
private func canOpenApp(scheme: String) -> Bool {
|
||||
return URL(string: "\(scheme)://").flatMap { UIApplication.shared.canOpenURL($0) } ?? false
|
||||
}
|
||||
|
||||
private func focusInstalled() -> Bool {
|
||||
return canOpenApp(scheme: MozillaAppSchemes.focus)
|
||||
}
|
||||
|
||||
private func klarInstalled() -> Bool {
|
||||
return canOpenApp(scheme: MozillaAppSchemes.focusDE)
|
||||
}
|
||||
|
||||
private func pocketInstalled() -> Bool {
|
||||
return canOpenApp(scheme: MozillaAppSchemes.pocket)
|
||||
}
|
||||
|
||||
private func mailtoIsDefault() -> Bool {
|
||||
return (prefs?.stringForKey(PrefsKeys.KeyMailToOption) ?? "mailto:") == "mailto:"
|
||||
}
|
||||
|
||||
private func getSettings() -> LPSettings? {
|
||||
let bundle = Bundle.main
|
||||
guard let appId = bundle.object(forInfoDictionaryKey: LPAppIdKey) as? String,
|
||||
let productionKey = bundle.object(forInfoDictionaryKey: LPProductionKeyKey) as? String,
|
||||
let developmentKey = bundle.object(forInfoDictionaryKey: LPDevelopmentKeyKey) as? String else {
|
||||
return nil
|
||||
}
|
||||
return LPSettings(appId: appId, developmentKey: developmentKey, productionKey: productionKey)
|
||||
}
|
||||
|
||||
// This must be called before `Leanplum.start` in order to correctly setup
|
||||
// custom message templates.
|
||||
private func setupCustomTemplates() {
|
||||
// These properties are exposed through the Leanplum web interface.
|
||||
// Ref: https://github.com/Leanplum/Leanplum-iOS-Samples/blob/master/iOS_customMessageTemplates/iOS_customMessageTemplates/LPMessageTemplates.m
|
||||
let args: [LPActionArg] = [
|
||||
LPActionArg(named: LPMessage.ArgTitleText, with: LPMessage.DefaultAskToAskTitle),
|
||||
LPActionArg(named: LPMessage.ArgTitleColor, with: UIColor.black),
|
||||
LPActionArg(named: LPMessage.ArgMessageText, with: LPMessage.DefaultAskToAskMessage),
|
||||
LPActionArg(named: LPMessage.ArgMessageColor, with: UIColor.black),
|
||||
LPActionArg(named: LPMessage.ArgAcceptButtonText, with: LPMessage.DefaultOkButtonText),
|
||||
LPActionArg(named: LPMessage.ArgCancelAction, withAction: nil),
|
||||
LPActionArg(named: LPMessage.ArgCancelButtonText, with: LPMessage.DefaultLaterButtonText),
|
||||
LPActionArg(named: LPMessage.ArgCancelButtonTextColor, with: UIColor.gray)
|
||||
]
|
||||
|
||||
let responder: LeanplumActionBlock = { (context) -> Bool in
|
||||
// Before proceeding, double check that Leanplum FxA prepush config value has been enabled.
|
||||
if !self.isFxAPrePushEnabled() {
|
||||
return false
|
||||
}
|
||||
|
||||
guard let context = context else {
|
||||
return false
|
||||
}
|
||||
|
||||
// Don't display permission screen if they have already allowed/disabled push permissions
|
||||
if self.prefs?.boolForKey(AppRequestedUserNotificationsPrefKey) ?? false {
|
||||
FxALoginHelper.sharedInstance.readyForSyncing()
|
||||
return false
|
||||
}
|
||||
|
||||
// Present Alert View onto the current top view controller
|
||||
let rootViewController = UIApplication.topViewController()
|
||||
let alert = UIAlertController(title: context.stringNamed(LPMessage.ArgTitleText), message: context.stringNamed(LPMessage.ArgMessageText), preferredStyle: .alert)
|
||||
|
||||
alert.addAction(UIAlertAction(title: context.stringNamed(LPMessage.ArgCancelButtonText), style: .cancel, handler: { (action) -> Void in
|
||||
// Log cancel event and call ready for syncing
|
||||
context.runTrackedActionNamed(LPMessage.ArgCancelAction)
|
||||
FxALoginHelper.sharedInstance.readyForSyncing()
|
||||
}))
|
||||
|
||||
alert.addAction(UIAlertAction(title: context.stringNamed(LPMessage.ArgAcceptButtonText), style: .default, handler: { (action) -> Void in
|
||||
// Log accept event and present push permission modal
|
||||
context.runTrackedActionNamed(LPMessage.ArgAcceptAction)
|
||||
FxALoginHelper.sharedInstance.requestUserNotifications(UIApplication.shared)
|
||||
self.prefs?.setBool(true, forKey: AppRequestedUserNotificationsPrefKey)
|
||||
}))
|
||||
|
||||
rootViewController?.present(alert, animated: true, completion: nil)
|
||||
return true
|
||||
}
|
||||
|
||||
// Register or update the custom Leanplum message
|
||||
Leanplum.defineAction(LPMessage.FxAPrePush, of: kLeanplumActionKindMessage, withArguments: args, withOptions: [:], withResponder: responder)
|
||||
}
|
||||
}
|
||||
|
||||
extension UIApplication {
|
||||
// Extension to get the current top most view controller
|
||||
class func topViewController(base: UIViewController? = UIApplication.shared.keyWindow?.rootViewController) -> UIViewController? {
|
||||
if let nav = base as? UINavigationController {
|
||||
return topViewController(base: nav.visibleViewController)
|
||||
}
|
||||
if let tab = base as? UITabBarController {
|
||||
if let selected = tab.selectedViewController {
|
||||
return topViewController(base: selected)
|
||||
}
|
||||
}
|
||||
if let presented = base?.presentedViewController {
|
||||
return topViewController(base: presented)
|
||||
}
|
||||
return base
|
||||
}
|
||||
}
|
||||
142
mobile/ios/Client/Application/QuickActions.swift
Normal file
142
mobile/ios/Client/Application/QuickActions.swift
Normal file
|
|
@ -0,0 +1,142 @@
|
|||
/* 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
|
||||
|
||||
import Shared
|
||||
import XCGLogger
|
||||
|
||||
enum ShortcutType: String {
|
||||
case newTab = "NewTab"
|
||||
case newPrivateTab = "NewPrivateTab"
|
||||
case openLastBookmark = "OpenLastBookmark"
|
||||
case qrCode = "QRCode"
|
||||
|
||||
init?(fullType: String) {
|
||||
guard let last = fullType.components(separatedBy: ".").last else { return nil }
|
||||
|
||||
self.init(rawValue: last)
|
||||
}
|
||||
|
||||
var type: String {
|
||||
return Bundle.main.bundleIdentifier! + ".\(self.rawValue)"
|
||||
}
|
||||
}
|
||||
|
||||
protocol QuickActionHandlerDelegate {
|
||||
func handleShortCutItemType(_ type: ShortcutType, userData: [String: NSSecureCoding]?)
|
||||
}
|
||||
|
||||
class QuickActions: NSObject {
|
||||
|
||||
fileprivate let log = Logger.browserLogger
|
||||
|
||||
static let QuickActionsVersion = "1.0"
|
||||
static let QuickActionsVersionKey = "dynamicQuickActionsVersion"
|
||||
|
||||
static let TabURLKey = "url"
|
||||
static let TabTitleKey = "title"
|
||||
|
||||
fileprivate let lastBookmarkTitle = NSLocalizedString("Open Last Bookmark", tableName: "3DTouchActions", comment: "String describing the action of opening the last added bookmark from the home screen Quick Actions via 3D Touch")
|
||||
fileprivate let _lastTabTitle = NSLocalizedString("Open Last Tab", tableName: "3DTouchActions", comment: "String describing the action of opening the last tab sent to Firefox from the home screen Quick Actions via 3D Touch")
|
||||
|
||||
static var sharedInstance = QuickActions()
|
||||
|
||||
var launchedShortcutItem: UIApplicationShortcutItem?
|
||||
|
||||
// MARK: Administering Quick Actions
|
||||
func addDynamicApplicationShortcutItemOfType(_ type: ShortcutType, fromShareItem shareItem: ShareItem, toApplication application: UIApplication) {
|
||||
var userData = [QuickActions.TabURLKey: shareItem.url]
|
||||
if let title = shareItem.title {
|
||||
userData[QuickActions.TabTitleKey] = title
|
||||
}
|
||||
QuickActions.sharedInstance.addDynamicApplicationShortcutItemOfType(type, withUserData: userData, toApplication: application)
|
||||
}
|
||||
|
||||
@discardableResult func addDynamicApplicationShortcutItemOfType(_ type: ShortcutType, withUserData userData: [AnyHashable: Any] = [AnyHashable: Any](), toApplication application: UIApplication) -> Bool {
|
||||
// add the quick actions version so that it is always in the user info
|
||||
var userData: [AnyHashable: Any] = userData
|
||||
userData[QuickActions.QuickActionsVersionKey] = QuickActions.QuickActionsVersion
|
||||
var dynamicShortcutItems = application.shortcutItems ?? [UIApplicationShortcutItem]()
|
||||
switch type {
|
||||
case .openLastBookmark:
|
||||
let openLastBookmarkShortcut = UIMutableApplicationShortcutItem(type: ShortcutType.openLastBookmark.type,
|
||||
localizedTitle: lastBookmarkTitle,
|
||||
localizedSubtitle: userData[QuickActions.TabTitleKey] as? String,
|
||||
icon: UIApplicationShortcutIcon(templateImageName: "quick_action_last_bookmark"),
|
||||
userInfo: userData
|
||||
)
|
||||
if let index = (dynamicShortcutItems.index { $0.type == ShortcutType.openLastBookmark.type }) {
|
||||
dynamicShortcutItems[index] = openLastBookmarkShortcut
|
||||
} else {
|
||||
dynamicShortcutItems.append(openLastBookmarkShortcut)
|
||||
}
|
||||
default:
|
||||
log.warning("Cannot add static shortcut item of type \(type)")
|
||||
return false
|
||||
}
|
||||
application.shortcutItems = dynamicShortcutItems
|
||||
return true
|
||||
}
|
||||
|
||||
func removeDynamicApplicationShortcutItemOfType(_ type: ShortcutType, fromApplication application: UIApplication) {
|
||||
guard var dynamicShortcutItems = application.shortcutItems,
|
||||
let index = (dynamicShortcutItems.index { $0.type == type.type }) else { return }
|
||||
|
||||
dynamicShortcutItems.remove(at: index)
|
||||
application.shortcutItems = dynamicShortcutItems
|
||||
}
|
||||
|
||||
// MARK: Handling Quick Actions
|
||||
@discardableResult func handleShortCutItem(_ shortcutItem: UIApplicationShortcutItem, withBrowserViewController bvc: BrowserViewController ) -> Bool {
|
||||
|
||||
// Verify that the provided `shortcutItem`'s `type` is one handled by the application.
|
||||
guard let shortCutType = ShortcutType(fullType: shortcutItem.type) else { return false }
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self.handleShortCutItemOfType(shortCutType, userData: shortcutItem.userInfo, browserViewController: bvc)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
fileprivate func handleShortCutItemOfType(_ type: ShortcutType, userData: [String: NSSecureCoding]?, browserViewController: BrowserViewController) {
|
||||
switch type {
|
||||
case .newTab:
|
||||
handleOpenNewTab(withBrowserViewController: browserViewController, isPrivate: false)
|
||||
case .newPrivateTab:
|
||||
handleOpenNewTab(withBrowserViewController: browserViewController, isPrivate: true)
|
||||
// even though we're removing OpenLastTab, it's possible that someone will use an existing last tab quick action to open the app
|
||||
// the first time after upgrading, so we should still handle it
|
||||
case .openLastBookmark:
|
||||
if let urlToOpen = (userData?[QuickActions.TabURLKey] as? String)?.asURL {
|
||||
handleOpenURL(withBrowserViewController: browserViewController, urlToOpen: urlToOpen)
|
||||
}
|
||||
case .qrCode:
|
||||
handleQRCode(with: browserViewController)
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate func handleOpenNewTab(withBrowserViewController bvc: BrowserViewController, isPrivate: Bool) {
|
||||
bvc.openBlankNewTab(focusLocationField: true, isPrivate: isPrivate)
|
||||
}
|
||||
|
||||
fileprivate func handleOpenURL(withBrowserViewController bvc: BrowserViewController, urlToOpen: URL) {
|
||||
// open bookmark in a non-private browsing tab
|
||||
bvc.switchToPrivacyMode(isPrivate: false)
|
||||
|
||||
// find out if bookmarked URL is currently open
|
||||
// if so, open to that tab,
|
||||
// otherwise, create a new tab with the bookmarked URL
|
||||
bvc.switchToTabForURLOrOpen(urlToOpen, isPrivileged: true)
|
||||
}
|
||||
|
||||
fileprivate func handleQRCode(with vc: QRCodeViewControllerDelegate & UIViewController) {
|
||||
let qrCodeViewController = QRCodeViewController()
|
||||
qrCodeViewController.qrCodeDelegate = vc
|
||||
let controller = UINavigationController(rootViewController: qrCodeViewController)
|
||||
vc.present(controller, animated: true, completion: nil)
|
||||
}
|
||||
}
|
||||
57
mobile/ios/Client/Application/Settings.bundle/Root.plist
Normal file
57
mobile/ios/Client/Application/Settings.bundle/Root.plist
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>StringsTable</key>
|
||||
<string>Root</string>
|
||||
<key>PreferenceSpecifiers</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>Type</key>
|
||||
<string>PSGroupSpecifier</string>
|
||||
<key>Title</key>
|
||||
<string>Debug Settings</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>Type</key>
|
||||
<string>PSToggleSwitchSpecifier</string>
|
||||
<key>Title</key>
|
||||
<string>Email Saved Tab State</string>
|
||||
<key>Key</key>
|
||||
<string>SettingsBundleEmailTabState</string>
|
||||
<key>DefaultValue</key>
|
||||
<false/>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>Type</key>
|
||||
<string>PSToggleSwitchSpecifier</string>
|
||||
<key>Title</key>
|
||||
<string>Email Logs On Launch</string>
|
||||
<key>Key</key>
|
||||
<string>SettingsBundleEmailLogsOnLaunch</string>
|
||||
<key>DefaultValue</key>
|
||||
<false/>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>Type</key>
|
||||
<string>PSToggleSwitchSpecifier</string>
|
||||
<key>Title</key>
|
||||
<string>Disable Local Web Server</string>
|
||||
<key>Key</key>
|
||||
<string>SettingsBundleDisableLocalWebServer</string>
|
||||
<key>DefaultValue</key>
|
||||
<false/>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>Type</key>
|
||||
<string>PSToggleSwitchSpecifier</string>
|
||||
<key>Title</key>
|
||||
<string>Skip Session Restore</string>
|
||||
<key>Key</key>
|
||||
<string>SettingsBundleSkipSessionRestore</string>
|
||||
<key>DefaultValue</key>
|
||||
<false/>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
Binary file not shown.
96
mobile/ios/Client/Application/TestAppDelegate.swift
Normal file
96
mobile/ios/Client/Application/TestAppDelegate.swift
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
/* 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 SDWebImage
|
||||
import XCGLogger
|
||||
|
||||
private let log = Logger.browserLogger
|
||||
|
||||
class TestAppDelegate: AppDelegate {
|
||||
override func getProfile(_ application: UIApplication) -> Profile {
|
||||
if let profile = self.profile {
|
||||
return profile
|
||||
}
|
||||
|
||||
var profile: BrowserProfile
|
||||
let launchArguments = ProcessInfo.processInfo.arguments
|
||||
if launchArguments.contains(LaunchArguments.ClearProfile) {
|
||||
// Use a clean profile for each test session.
|
||||
log.debug("Deleting all files in 'Documents' directory to clear the profile")
|
||||
profile = BrowserProfile(localName: "testProfile", syncDelegate: application.syncDelegate, clear: true)
|
||||
} else {
|
||||
profile = BrowserProfile(localName: "testProfile", syncDelegate: application.syncDelegate)
|
||||
}
|
||||
|
||||
// Don't show the What's New page.
|
||||
if launchArguments.contains(LaunchArguments.SkipWhatsNew) {
|
||||
profile.prefs.setString(AppInfo.appVersion, forKey: LatestAppVersionProfileKey)
|
||||
}
|
||||
|
||||
// Skip the intro when requested by for example tests or automation
|
||||
if launchArguments.contains(LaunchArguments.SkipIntro) {
|
||||
profile.prefs.setInt(1, forKey: IntroViewControllerSeenProfileKey)
|
||||
}
|
||||
|
||||
self.profile = profile
|
||||
return profile
|
||||
}
|
||||
|
||||
override func application(_ application: UIApplication, willFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
|
||||
// If the app is running from a XCUITest reset all settings in the app
|
||||
if ProcessInfo.processInfo.arguments.contains(LaunchArguments.ClearProfile) {
|
||||
resetApplication()
|
||||
}
|
||||
|
||||
return super.application(application, willFinishLaunchingWithOptions: launchOptions)
|
||||
}
|
||||
|
||||
/**
|
||||
Use this to reset the application between tests.
|
||||
**/
|
||||
func resetApplication() {
|
||||
log.debug("Wiping everything for a clean start.")
|
||||
|
||||
// Clear image cache
|
||||
SDImageCache.shared().clearDisk()
|
||||
SDImageCache.shared().clearMemory()
|
||||
|
||||
// Clear the cookie/url cache
|
||||
URLCache.shared.removeAllCachedResponses()
|
||||
let storage = HTTPCookieStorage.shared
|
||||
if let cookies = storage.cookies {
|
||||
for cookie in cookies {
|
||||
storage.deleteCookie(cookie)
|
||||
}
|
||||
}
|
||||
|
||||
// Clear the documents directory
|
||||
var rootPath: String = ""
|
||||
let sharedContainerIdentifier = AppInfo.sharedContainerIdentifier
|
||||
if let url = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: sharedContainerIdentifier) {
|
||||
rootPath = url.path
|
||||
} else {
|
||||
rootPath = (NSSearchPathForDirectoriesInDomains(FileManager.SearchPathDirectory.documentDirectory, FileManager.SearchPathDomainMask.userDomainMask, true)[0])
|
||||
}
|
||||
let manager = FileManager.default
|
||||
let documents = URL(fileURLWithPath: rootPath)
|
||||
let docContents = try! manager.contentsOfDirectory(atPath: rootPath)
|
||||
for content in docContents {
|
||||
do {
|
||||
try manager.removeItem(at: documents.appendingPathComponent(content))
|
||||
} catch {
|
||||
log.debug("Couldn't delete some document contents.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
|
||||
// Speed up the animations to 100 times as fast.
|
||||
defer { application.keyWindow?.layer.speed = 100.0 }
|
||||
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
|
||||
}
|
||||
|
||||
}
|
||||
88
mobile/ios/Client/Application/WebServer.swift
Normal file
88
mobile/ios/Client/Application/WebServer.swift
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
/* 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 GCDWebServers
|
||||
import Shared
|
||||
|
||||
class WebServer {
|
||||
private let log = Logger.browserLogger
|
||||
|
||||
static let WebServerSharedInstance = WebServer()
|
||||
|
||||
class var sharedInstance: WebServer {
|
||||
return WebServerSharedInstance
|
||||
}
|
||||
|
||||
let server: GCDWebServer = GCDWebServer()
|
||||
|
||||
var base: String {
|
||||
return "http://localhost:\(server.port)"
|
||||
}
|
||||
|
||||
/// The private credentials for accessing resources on this Web server.
|
||||
let credentials: URLCredential
|
||||
|
||||
/// A random, transient token used for authenticating requests.
|
||||
/// Other apps are able to make requests to our local Web server,
|
||||
/// so this prevents them from accessing any resources.
|
||||
fileprivate let sessionToken = UUID().uuidString
|
||||
|
||||
init() {
|
||||
credentials = URLCredential(user: sessionToken, password: "", persistence: .forSession)
|
||||
}
|
||||
|
||||
@discardableResult func start() throws -> Bool {
|
||||
if !server.isRunning {
|
||||
try server.start(options: [
|
||||
GCDWebServerOption_Port: 6571,
|
||||
GCDWebServerOption_BindToLocalhost: true,
|
||||
GCDWebServerOption_AutomaticallySuspendInBackground: true,
|
||||
GCDWebServerOption_AuthenticationMethod: GCDWebServerAuthenticationMethod_Basic,
|
||||
GCDWebServerOption_AuthenticationAccounts: [sessionToken: ""]
|
||||
])
|
||||
}
|
||||
return server.isRunning
|
||||
}
|
||||
|
||||
/// Convenience method to register a dynamic handler. Will be mounted at $base/$module/$resource
|
||||
func registerHandlerForMethod(_ method: String, module: String, resource: String, handler: @escaping (_ request: GCDWebServerRequest?) -> GCDWebServerResponse!) {
|
||||
// Prevent serving content if the requested host isn't a whitelisted local host.
|
||||
let wrappedHandler = {(request: GCDWebServerRequest?) -> GCDWebServerResponse? in
|
||||
guard let request = request, request.url.isLocal else {
|
||||
return GCDWebServerResponse(statusCode: 403)
|
||||
}
|
||||
|
||||
return handler(request)
|
||||
}
|
||||
server.addHandler(forMethod: method, path: "/\(module)/\(resource)", request: GCDWebServerRequest.self, processBlock: wrappedHandler)
|
||||
}
|
||||
|
||||
/// Convenience method to register a resource in the main bundle. Will be mounted at $base/$module/$resource
|
||||
func registerMainBundleResource(_ resource: String, module: String) {
|
||||
if let path = Bundle.main.path(forResource: resource, ofType: nil) {
|
||||
server.addGETHandler(forPath: "/\(module)/\(resource)", filePath: path, isAttachment: false, cacheAge: UInt.max, allowRangeRequests: true)
|
||||
}
|
||||
}
|
||||
|
||||
/// Convenience method to register all resources in the main bundle of a specific type. Will be mounted at $base/$module/$resource
|
||||
func registerMainBundleResourcesOfType(_ type: String, module: String) {
|
||||
for path: String in Bundle.paths(forResourcesOfType: type, inDirectory: Bundle.main.bundlePath) {
|
||||
if let resource = NSURL(string: path)?.lastPathComponent {
|
||||
server.addGETHandler(forPath: "/\(module)/\(resource)", filePath: path as String, isAttachment: false, cacheAge: UInt.max, allowRangeRequests: true)
|
||||
} else {
|
||||
log.warning("Unable to locate resource at path: '\(path)'")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Return a full url, as a string, for a resource in a module. No check is done to find out if the resource actually exist.
|
||||
func URLForResource(_ resource: String, module: String) -> String {
|
||||
return "\(base)/\(module)/\(resource)"
|
||||
}
|
||||
|
||||
func baseReaderModeURL() -> String {
|
||||
return WebServer.sharedInstance.URLForResource("page", module: "reader-mode")
|
||||
}
|
||||
}
|
||||
16
mobile/ios/Client/Application/main.swift
Normal file
16
mobile/ios/Client/Application/main.swift
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
/* 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
|
||||
|
||||
private var appDelegate: AppDelegate.Type
|
||||
|
||||
if AppConstants.IsRunningTest {
|
||||
appDelegate = TestAppDelegate.self
|
||||
} else {
|
||||
appDelegate = AppDelegate.self
|
||||
}
|
||||
|
||||
private let pointer = UnsafeMutableRawPointer(CommandLine.unsafeArgv).bindMemory(to: UnsafeMutablePointer<Int8>.self, capacity: Int(CommandLine.argc))
|
||||
UIApplicationMain(CommandLine.argc, pointer, NSStringFromClass(UIApplication.self), NSStringFromClass(appDelegate))
|
||||
Loading…
Add table
Add a link
Reference in a new issue