Dactyloidae iOS initial commit

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

View file

@ -0,0 +1,195 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import Foundation
import Shared
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
}
}

View 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)
}
}
}
}
}

View 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

View 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

View file

@ -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")
}
}

View 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>

View 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
}
}

View 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)
}
}

View 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>

View 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)
}
}

View 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")
}
}

View 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))